diff --git a/src/MuEditor/Config/MuEditorConfig.cpp b/src/MuEditor/Config/MuEditorConfig.cpp index 8d3be02f4..ee4c80e54 100644 --- a/src/MuEditor/Config/MuEditorConfig.cpp +++ b/src/MuEditor/Config/MuEditorConfig.cpp @@ -71,6 +71,10 @@ void CMuEditorConfig::Load() { m_columnVisibility[key] = (value == "1" || value == "true"); } + else if (currentSection == "SkillEditorColumnVisibility") + { + m_skillEditorColumnVisibility[key] = (value == "1" || value == "true"); + } } } @@ -100,6 +104,14 @@ void CMuEditorConfig::Save() { file << col.first << "=" << (col.second ? "1" : "0") << "\n"; } + file << "\n"; + + // Write [SkillEditorColumnVisibility] section + file << "[SkillEditorColumnVisibility]\n"; + for (const auto& col : m_skillEditorColumnVisibility) + { + file << col.first << "=" << (col.second ? "1" : "0") << "\n"; + } file.close(); } diff --git a/src/MuEditor/Config/MuEditorConfig.h b/src/MuEditor/Config/MuEditorConfig.h index 6e337736b..b14393193 100644 --- a/src/MuEditor/Config/MuEditorConfig.h +++ b/src/MuEditor/Config/MuEditorConfig.h @@ -20,14 +20,18 @@ class CMuEditorConfig std::string GetLanguage() const; void SetLanguage(const std::string& language); - // Column visibility settings + // Item Editor column visibility settings bool GetColumnVisibility(const std::string& columnName, bool defaultValue = false) const; void SetColumnVisibility(const std::string& columnName, bool visible); - // Get all column visibility settings + // Get all item editor column visibility settings const std::map& GetAllColumnVisibility() const { return m_columnVisibility; } void SetAllColumnVisibility(const std::map& visibility) { m_columnVisibility = visibility; } + // Skill Editor column visibility settings + const std::map& GetSkillEditorColumnVisibility() const { return m_skillEditorColumnVisibility; } + void SetSkillEditorColumnVisibility(const std::map& visibility) { m_skillEditorColumnVisibility = visibility; } + private: CMuEditorConfig(); @@ -36,7 +40,8 @@ class CMuEditorConfig // Settings storage std::string m_language; - std::map m_columnVisibility; + std::map m_columnVisibility; // Item Editor columns + std::map m_skillEditorColumnVisibility; // Skill Editor columns // Helper functions for INI parsing std::string Trim(const std::string& str); diff --git a/src/MuEditor/Core/MuEditorCore.cpp b/src/MuEditor/Core/MuEditorCore.cpp index e9dcba667..6be69822a 100644 --- a/src/MuEditor/Core/MuEditorCore.cpp +++ b/src/MuEditor/Core/MuEditorCore.cpp @@ -3,8 +3,6 @@ #ifdef _EDITOR #include "MuEditorCore.h" -#include "../MuEditor/UI/Common/MuEditorUI.h" -#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" #include "imgui.h" #include "imgui_impl_win32.h" #include "imgui_impl_opengl2.h" @@ -12,7 +10,11 @@ #include "../Config/MuEditorConfig.h" #include "../MuEditor/UI/Common/MuEditorCenterPaneUI.h" #include "../MuEditor/UI/ItemEditor/MuItemEditorUI.h" +#include "../MuEditor/UI/SkillEditor/MuSkillEditorUI.h" +#include "../UI/Common/MuEditorUI.h" +#include "../UI/Console/MuEditorConsoleUI.h" #include "Translation/i18n.h" +#include "Utilities/StringUtils.h" // Windows cursor display counter thresholds // The cursor is visible when the counter is >= CURSOR_VISIBLE_THRESHOLD @@ -30,6 +32,7 @@ CMuEditorCore::CMuEditorCore() , m_bInitialized(false) , m_bFrameStarted(false) , m_bShowItemEditor(false) + , m_bShowSkillEditor(false) , m_bHoveringUI(false) , m_bPreviousFrameHoveringUI(false) { @@ -65,6 +68,98 @@ void CMuEditorCore::Initialize(HWND hwnd, HDC hdc) fwprintf(stderr, L"[MuEditor] ImGui context created\n"); fflush(stderr); + // Load font with extended Unicode support for multiple languages + // This includes Latin, Cyrillic, Greek, and other common character sets + ImFontConfig fontConfig; + fontConfig.OversampleH = 2; + fontConfig.OversampleV = 2; + fontConfig.PixelSnapH = true; + + // Build font atlas with multiple Unicode ranges + ImFontGlyphRangesBuilder builder; + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); // Basic Latin + Latin Supplement + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); // Cyrillic (Russian, Ukrainian, etc.) + builder.AddRanges(io.Fonts->GetGlyphRangesGreek()); // Greek + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Includes common Asian characters + + // Add additional specific characters if needed + static const ImWchar additionalRanges[] = { + 0x0100, 0x017F, // Latin Extended-A (Polish, etc.) + 0x0180, 0x024F, // Latin Extended-B + 0x1E00, 0x1EFF, // Latin Extended Additional (Vietnamese) + 0, + }; + builder.AddRanges(additionalRanges); + + ImVector ranges; + builder.BuildRanges(&ranges); + + // Load default font with extended ranges + // Try platform-specific fonts that support extended Unicode + bool fontLoaded = false; + +#ifdef _WIN32 + // Windows: Get fonts directory dynamically + wchar_t windowsDir[MAX_PATH]; + if (GetWindowsDirectoryW(windowsDir, MAX_PATH) > 0) + { + std::wstring fontPathW = std::wstring(windowsDir) + L"\\Fonts\\segoeui.ttf"; + + // Convert to UTF-8 using StringUtils helper + std::string fontPath = StringUtils::WideToNarrow(fontPathW.c_str()); + if (!fontPath.empty()) + { + if (io.Fonts->AddFontFromFileTTF(fontPath.c_str(), 16.0f, &fontConfig, ranges.Data) != nullptr) + { + fontLoaded = true; + } + } + } +#elif __APPLE__ + // macOS: Try system fonts + const char* macFonts[] = { + "/System/Library/Fonts/Supplemental/Arial Unicode.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "/Library/Fonts/Arial Unicode.ttf" + }; + for (const char* fontPath : macFonts) + { + if (io.Fonts->AddFontFromFileTTF(fontPath, 16.0f, &fontConfig, ranges.Data) != nullptr) + { + fontLoaded = true; + break; + } + } +#else + // Linux: Try common fonts with Unicode support + const char* linuxFonts[] = { + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/TTF/DejaVuSans.ttf", + "/usr/share/fonts/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf" + }; + for (const char* fontPath : linuxFonts) + { + if (io.Fonts->AddFontFromFileTTF(fontPath, 16.0f, &fontConfig, ranges.Data) != nullptr) + { + fontLoaded = true; + break; + } + } +#endif + + // Fallback to ImGui's default font if no system font loaded + // Note: Default font only supports basic ASCII, not extended ranges + if (!fontLoaded) + { + io.Fonts->AddFontDefault(&fontConfig); + } + + // Note: Don't call io.Fonts->Build() - the backend will build it automatically + + fwprintf(stderr, L"[MuEditor] Font loaded with Unicode support\n"); + fflush(stderr); + // Dark theme ImGui::StyleColorsDark(); ImGuiStyle& style = ImGui::GetStyle(); @@ -148,6 +243,9 @@ void CMuEditorCore::Shutdown() // Save item editor preferences before shutting down g_MuItemEditorUI.SaveColumnPreferences(); + // Save skill editor preferences before shutting down + g_MuSkillEditorUI.SaveColumnPreferences(); + ImGui_ImplOpenGL2_Shutdown(); ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); @@ -305,12 +403,12 @@ void CMuEditorCore::Render() m_bHoveringUI = false; // Render toolbar (handles both open and closed states) - g_MuEditorUI.RenderToolbar(m_bEditorMode, m_bShowItemEditor); + g_MuEditorUI.RenderToolbar(m_bEditorMode, m_bShowItemEditor, m_bShowSkillEditor); if (m_bEditorMode) { // Render center pane (handles all editor windows and input blocking) - g_MuEditorCenterPaneUI.Render(m_bShowItemEditor); + g_MuEditorCenterPaneUI.Render(m_bShowItemEditor, m_bShowSkillEditor); // Render console g_MuEditorConsoleUI.Render(); diff --git a/src/MuEditor/Core/MuEditorCore.h b/src/MuEditor/Core/MuEditorCore.h index c0307b79e..5b25025ac 100644 --- a/src/MuEditor/Core/MuEditorCore.h +++ b/src/MuEditor/Core/MuEditorCore.h @@ -19,6 +19,7 @@ class CMuEditorCore void ToggleEditor() { m_bEditorMode = !m_bEditorMode; } bool IsShowingItemEditor() const { return m_bShowItemEditor; } + bool IsShowingSkillEditor() const { return m_bShowSkillEditor; } bool IsHoveringUI() const { return m_bHoveringUI; } void SetHoveringUI(bool hovering) { m_bHoveringUI = hovering; } @@ -30,6 +31,7 @@ class CMuEditorCore bool m_bInitialized; bool m_bFrameStarted; bool m_bShowItemEditor; + bool m_bShowSkillEditor; bool m_bHoveringUI; bool m_bPreviousFrameHoveringUI; // Store previous frame's hover state for input blocking }; diff --git a/src/MuEditor/UI/Common/MuEditorCenterPaneUI.cpp b/src/MuEditor/UI/Common/MuEditorCenterPaneUI.cpp index a5ba684ae..a98424af7 100644 --- a/src/MuEditor/UI/Common/MuEditorCenterPaneUI.cpp +++ b/src/MuEditor/UI/Common/MuEditorCenterPaneUI.cpp @@ -3,8 +3,8 @@ #ifdef _EDITOR #include "MuEditorCenterPaneUI.h" - #include "../MuEditor/UI/ItemEditor/MuItemEditorUI.h" +#include "../MuEditor/UI/SkillEditor/MuSkillEditorUI.h" CMuEditorCenterPaneUI& CMuEditorCenterPaneUI::GetInstance() { @@ -12,7 +12,7 @@ CMuEditorCenterPaneUI& CMuEditorCenterPaneUI::GetInstance() return instance; } -void CMuEditorCenterPaneUI::Render(bool& showItemEditor) +void CMuEditorCenterPaneUI::Render(bool& showItemEditor, bool& showSkillEditor) { // Simply render editor windows directly without a container // The container was causing an extra debug window to appear @@ -22,8 +22,10 @@ void CMuEditorCenterPaneUI::Render(bool& showItemEditor) g_MuItemEditorUI.Render(showItemEditor); } - // Future editor windows can be added here - // if (showOtherEditor) { g_MuOtherEditor.Render(showOtherEditor); } + if (showSkillEditor) + { + g_MuSkillEditorUI.Render(showSkillEditor); + } } #endif // _EDITOR diff --git a/src/MuEditor/UI/Common/MuEditorCenterPaneUI.h b/src/MuEditor/UI/Common/MuEditorCenterPaneUI.h index 2594b7609..f37006a15 100644 --- a/src/MuEditor/UI/Common/MuEditorCenterPaneUI.h +++ b/src/MuEditor/UI/Common/MuEditorCenterPaneUI.h @@ -7,7 +7,7 @@ class CMuEditorCenterPaneUI public: static CMuEditorCenterPaneUI& GetInstance(); - void Render(bool& showItemEditor); + void Render(bool& showItemEditor, bool& showSkillEditor); private: CMuEditorCenterPaneUI() = default; diff --git a/src/MuEditor/UI/Common/MuEditorUI.cpp b/src/MuEditor/UI/Common/MuEditorUI.cpp index ac9a52c55..118a0e332 100644 --- a/src/MuEditor/UI/Common/MuEditorUI.cpp +++ b/src/MuEditor/UI/Common/MuEditorUI.cpp @@ -23,11 +23,11 @@ CMuEditorUI& CMuEditorUI::GetInstance() return instance; } -void CMuEditorUI::RenderToolbar(bool& editorEnabled, bool& showItemEditor) +void CMuEditorUI::RenderToolbar(bool& editorEnabled, bool& showItemEditor, bool& showSkillEditor) { if (editorEnabled) { - RenderToolbarFull(editorEnabled, showItemEditor); + RenderToolbarFull(editorEnabled, showItemEditor, showSkillEditor); } else { @@ -109,7 +109,7 @@ void CMuEditorUI::RenderToolbarOpen(bool& editorEnabled) ImGui::PopStyleColor(2); } -void CMuEditorUI::RenderToolbarFull(bool& editorEnabled, bool& showItemEditor) +void CMuEditorUI::RenderToolbarFull(bool& editorEnabled, bool& showItemEditor, bool& showSkillEditor) { ImGui::SetNextWindowSize(ImVec2(ImGui::GetIO().DisplaySize.x, TOOLBAR_HEIGHT), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); @@ -138,6 +138,12 @@ void CMuEditorUI::RenderToolbarFull(bool& editorEnabled, bool& showItemEditor) showItemEditor = !showItemEditor; } + ImGui::SameLine(); + if (ImGui::Button("Skill Editor")) + { + showSkillEditor = !showSkillEditor; + } + // Language selector ImGui::SameLine(); ImGui::SetNextItemWidth(100.0f); diff --git a/src/MuEditor/UI/Common/MuEditorUI.h b/src/MuEditor/UI/Common/MuEditorUI.h index 2051aa090..48466a2e4 100644 --- a/src/MuEditor/UI/Common/MuEditorUI.h +++ b/src/MuEditor/UI/Common/MuEditorUI.h @@ -7,7 +7,7 @@ class CMuEditorUI public: static CMuEditorUI& GetInstance(); - void RenderToolbar(bool& editorEnabled, bool& showItemEditor); + void RenderToolbar(bool& editorEnabled, bool& showItemEditor, bool& showSkillEditor); void RenderCenterViewport(); private: @@ -15,7 +15,7 @@ class CMuEditorUI ~CMuEditorUI() = default; void RenderToolbarOpen(bool& editorEnabled); - void RenderToolbarFull(bool& editorEnabled, bool& showItemEditor); + void RenderToolbarFull(bool& editorEnabled, bool& showItemEditor, bool& showSkillEditor); }; #define g_MuEditorUI CMuEditorUI::GetInstance() diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorActions.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorActions.cpp index 399e13864..4251fdf13 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorActions.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorActions.cpp @@ -21,7 +21,7 @@ void CItemEditorActions::ConvertItemName(char* outBuffer, size_t bufferSize, con WideCharToMultiByte(CP_UTF8, 0, name, -1, outBuffer, (int)bufferSize, NULL, NULL); } -std::string CItemEditorActions::GetFieldValueAsString(const ITEM_ATTRIBUTE& item, const FieldDescriptor& desc) +std::string CItemEditorActions::GetFieldValueAsString(const ITEM_ATTRIBUTE& item, const ItemFieldDescriptor& desc) { std::stringstream ss; @@ -64,7 +64,7 @@ std::string CItemEditorActions::GetFieldValueAsString(const ITEM_ATTRIBUTE& item std::string CItemEditorActions::GetCSVHeader() { std::stringstream ss; - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); ss << "Index"; for (int i = 0; i < fieldCount; ++i) @@ -78,7 +78,7 @@ std::string CItemEditorActions::GetCSVHeader() std::string CItemEditorActions::ExportItemToReadable(int itemIndex, ITEM_ATTRIBUTE& item) { std::stringstream ss; - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); ss << "Row " << itemIndex << "\n"; ss << "Index = " << itemIndex; @@ -95,7 +95,7 @@ std::string CItemEditorActions::ExportItemToReadable(int itemIndex, ITEM_ATTRIBU std::string CItemEditorActions::ExportItemToCSV(int itemIndex, ITEM_ATTRIBUTE& item) { std::stringstream ss; - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); ss << itemIndex; @@ -168,9 +168,9 @@ void CItemEditorActions::RenderExportS6E3Button() if (ImGui::Button(EDITOR_TEXT("btn_export_s6e3"))) { wchar_t fileName[256]; - swprintf_s(fileName, _countof(fileName), L"Data\\Local\\%ls\\Item_%ls_S6E3.bmd", g_strSelectedML.c_str(), g_strSelectedML.c_str()); + swprintf_s(fileName, _countof(fileName), L"Data\\Local\\%ls\\Item_S6E3.bmd", g_strSelectedML.c_str()); - if (g_ItemDataHandler.SaveLegacy(fileName)) + if (g_ItemDataHandler.ExportAsS6E3(fileName)) { std::string filename_str = "Item_" + std::string(g_strSelectedML.begin(), g_strSelectedML.end()) + "_S6E3.bmd"; g_MuEditorConsoleUI.LogEditor("Exported items as S6E3 legacy format: " + filename_str); @@ -194,7 +194,7 @@ void CItemEditorActions::RenderExportCSVButton() if (ImGui::Button(EDITOR_TEXT("btn_export_csv"))) { wchar_t csvFileName[256]; - swprintf_s(csvFileName, _countof(csvFileName), L"Data\\Local\\%ls\\Item_%ls_export.csv", g_strSelectedML.c_str(), g_strSelectedML.c_str()); + swprintf_s(csvFileName, _countof(csvFileName), L"Data\\Local\\%ls\\Item.csv", g_strSelectedML.c_str()); if (g_ItemDataHandler.ExportToCsv(csvFileName)) { diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorActions.h b/src/MuEditor/UI/ItemEditor/ItemEditorActions.h index 551b1f016..78e6e5ae6 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorActions.h +++ b/src/MuEditor/UI/ItemEditor/ItemEditorActions.h @@ -1,4 +1,5 @@ #pragma once +#include "GameData/ItemData/ItemFieldMetadata.h" #ifdef _EDITOR @@ -34,7 +35,7 @@ class CItemEditorActions static void ConvertItemName(char* outBuffer, size_t bufferSize, const wchar_t* name); // Get field value as string (descriptor-driven) - static std::string GetFieldValueAsString(const ITEM_ATTRIBUTE& item, const struct FieldDescriptor& desc); + static std::string GetFieldValueAsString(const ITEM_ATTRIBUTE &item, const ItemFieldDescriptor &desc); }; #endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorColumns.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorColumns.cpp index 4060eda74..a9cd4e849 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorColumns.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorColumns.cpp @@ -22,15 +22,36 @@ extern ITEM_ATTRIBUTE* ItemAttribute; // ===== X-MACRO-DRIVEN RENDERING ===== -void CItemEditorColumns::RenderFieldByDescriptor(const FieldDescriptor& desc, int& colIdx, int itemIndex, +void CItemEditorColumns::RenderFieldByDescriptor(const ItemFieldDescriptor& desc, int& colIdx, int itemIndex, ITEM_ATTRIBUTE& item, bool& rowInteracted, bool isVisible) { - // Use the template helper function from ItemFieldDefs.h - ::RenderFieldByDescriptor(desc, this, item, colIdx, itemIndex, rowInteracted, isVisible); + // Use the template helper function from ItemFieldMetadata.h + ::RenderFieldByDescriptor(desc, this, item, colIdx, itemIndex, rowInteracted, isVisible, MAX_ITEM_NAME); } // ===== LOW-LEVEL TYPE-SPECIFIC RENDERING ===== +static std::string GetItemNameUtf8(int itemIndex) +{ + char nameBuf[256]{}; + if (ItemAttribute && itemIndex >= 0 && itemIndex < MAX_ITEM) + { + WideCharToMultiByte(CP_UTF8, 0, ItemAttribute[itemIndex].Name, -1, nameBuf, sizeof(nameBuf), NULL, NULL); + } + if (nameBuf[0] == '\0') + return ""; + return nameBuf; +} + +static void LogItemFieldChange(int itemIndex, const char* columnName, const std::string& newValue) +{ + g_MuEditorConsoleUI.LogEditor( + "Changed item " + std::to_string(itemIndex) + + " (" + GetItemNameUtf8(itemIndex) + ") " + + columnName + " to " + newValue + ); +} + void CItemEditorColumns::RenderByteColumn( const char* columnName, int& colIdx, int itemIndex, int uniqueId, BYTE& value, bool& rowInteracted, bool isVisible) @@ -47,7 +68,7 @@ void CItemEditorColumns::RenderByteColumn( if (intValue >= 0 && intValue <= 255) { value = (BYTE)intValue; - g_MuEditorConsoleUI.LogEditor("Changed item " + std::to_string(itemIndex) + " " + columnName + " to " + std::to_string(intValue)); + LogItemFieldChange(itemIndex, columnName, std::to_string(intValue)); } } @@ -71,7 +92,7 @@ void CItemEditorColumns::RenderWordColumn( if (intValue >= 0 && intValue <= 65535) { value = (WORD)intValue; - g_MuEditorConsoleUI.LogEditor("Changed item " + std::to_string(itemIndex) + " " + columnName + " to " + std::to_string(intValue)); + LogItemFieldChange(itemIndex, columnName, std::to_string(intValue)); } } @@ -91,12 +112,27 @@ void CItemEditorColumns::RenderIntColumn( if (ImGui::InputInt("##input", &value, 0, 0)) { - std::string logMsg = i18n::FormatEditor("log_changed_item", { - std::to_string(itemIndex), - columnName, - std::to_string(value) - }); - g_MuEditorConsoleUI.LogEditor(logMsg); + LogItemFieldChange(itemIndex, columnName, std::to_string(value)); + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CItemEditorColumns::RenderDWordColumn( + const char* columnName, int& colIdx, int itemIndex, int uniqueId, + DWORD& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(itemIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + // DWORD is unsigned, range 0 to 4294967295 + if (ImGui::InputScalar("##input", ImGuiDataType_U32, &value, nullptr, nullptr, "%u")) + { + LogItemFieldChange(itemIndex, columnName, std::to_string(value)); } if (ImGui::IsItemActivated()) rowInteracted = true; @@ -115,12 +151,7 @@ void CItemEditorColumns::RenderBoolColumn( if (ImGui::Checkbox("##checkbox", &value)) { - std::string logMsg = i18n::FormatEditor("log_changed_item", { - std::to_string(itemIndex), - columnName, - value ? "true" : "false" - }); - g_MuEditorConsoleUI.LogEditor(logMsg); + LogItemFieldChange(itemIndex, columnName, value ? "true" : "false"); } if (ImGui::IsItemActivated()) rowInteracted = true; @@ -143,7 +174,7 @@ void CItemEditorColumns::RenderWCharArrayColumn( if (ImGui::InputText("##input", editableBuffer, sizeof(editableBuffer))) { MultiByteToWideChar(CP_UTF8, 0, editableBuffer, -1, value, arraySize); - g_MuEditorConsoleUI.LogEditor("Changed item " + std::to_string(itemIndex) + " " + columnName + " to " + std::string(editableBuffer)); + LogItemFieldChange(itemIndex, columnName, std::string(editableBuffer)); } if (ImGui::IsItemActivated()) rowInteracted = true; diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorColumns.h b/src/MuEditor/UI/ItemEditor/ItemEditorColumns.h index 4f9fb0a6b..b30d28e52 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorColumns.h +++ b/src/MuEditor/UI/ItemEditor/ItemEditorColumns.h @@ -21,16 +21,16 @@ class CItemEditorColumns void SetTable(CItemEditorTable* table) { m_pTable = table; } // X-macro-driven rendering - automatically renders any field based on descriptor - void RenderFieldByDescriptor(const FieldDescriptor& desc, int& colIdx, int itemIndex, + void RenderFieldByDescriptor(const ItemFieldDescriptor& desc, int& colIdx, int itemIndex, ITEM_ATTRIBUTE& item, bool& rowInteracted, bool isVisible); // Render Index column (special - not part of ITEM_ATTRIBUTE fields) void RenderIndexColumn(int& colIdx, int itemIndex, bool& rowInteracted, bool isVisible); - // Friend declaration for template helper in ItemFieldMetadata.h - template - friend void ::RenderFieldByDescriptor(const FieldDescriptor& desc, TColumns* cols, ITEM_ATTRIBUTE& item, - int& colIdx, int itemIndex, bool& rowInteracted, bool isVisible); + // Friend declaration for template helper in FieldMetadataHelper.h + template + friend void ::RenderFieldByDescriptor(const FieldDescriptor& desc, TColumns* cols, TStruct& data, + int& colIdx, int dataIndex, bool& rowInteracted, bool isVisible, int maxStringLength); private: // Low-level type-specific rendering helpers @@ -40,6 +40,8 @@ class CItemEditorColumns WORD& value, bool& rowInteracted, bool isVisible); void RenderIntColumn(const char* columnName, int& colIdx, int itemIndex, int uniqueId, int& value, bool& rowInteracted, bool isVisible); + void RenderDWordColumn(const char* columnName, int& colIdx, int itemIndex, int uniqueId, + DWORD& value, bool& rowInteracted, bool isVisible); void RenderBoolColumn(const char* columnName, int& colIdx, int itemIndex, int uniqueId, bool& value, bool& rowInteracted, bool isVisible); void RenderWCharArrayColumn(const char* columnName, int& colIdx, int itemIndex, int uniqueId, diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp index 82fb30bf4..ec1d87574 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp @@ -53,7 +53,7 @@ void CItemEditorTable::Render( bool freezeColumns) { // Get metadata fields once at function scope - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); // Count visible columns - only count columns that actually exist int visibleColumnCount = 0; diff --git a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp index aa170a082..30308b24d 100644 --- a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp +++ b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp @@ -32,7 +32,7 @@ CMuItemEditorUI::CMuItemEditorUI() m_columnVisibility["Index"] = true; // Special column (not in metadata) // Get all fields from metadata and set default visibility - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); for (int i = 0; i < fieldCount; ++i) { // Default commonly used columns to visible, rest to hidden @@ -220,7 +220,7 @@ void CMuItemEditorUI::RenderColumnVisibilityMenu() changed |= ImGui::Checkbox(EDITOR_TEXT("label_index"), &m_columnVisibility["Index"]); // Get all fields from metadata and render checkboxes - const FieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); + const ItemFieldDescriptor* fields = GetFieldDescriptors(); const int fieldCount = GetFieldCount(); for (int i = 0; i < fieldCount; ++i) { const char* displayName = GetFieldDisplayName(fields[i].name); diff --git a/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.cpp b/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.cpp new file mode 100644 index 000000000..c4774f4a6 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.cpp @@ -0,0 +1,231 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "MuSkillEditorUI.h" +#include "SkillEditorTable.h" +#include "SkillEditorActions.h" +#include "SkillEditorPopups.h" +#include "GameData/SkillData/SkillFieldMetadata.h" +#include "../MuEditor/Core/MuEditorCore.h" +#include "../MuEditor/Config/MuEditorConfig.h" +#include "Translation/i18n.h" +#include "imgui.h" +#include +#include +#include + +#include "imgui_internal.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" + +CMuSkillEditorUI::CMuSkillEditorUI() + : m_selectedRow(-1) + , m_bFreezeColumns(false) + , m_pTable(nullptr) +{ + memset(m_szSkillSearchBuffer, 0, sizeof(m_szSkillSearchBuffer)); + m_pTable = new CSkillEditorTable(); + + // Initialize column visibility - AUTO-GENERATED from metadata + m_columnVisibility["Index"] = true; // Special column (not in metadata) + + // Get all fields from metadata and set default visibility + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + // Define commonly used fields that should be visible by default + static const std::unordered_set defaultVisibleFields = { + "Name", "Level", "Damage", "Mana", "Distance", + "Delay", "Energy", "Strength", "Dexterity" + }; + + for (int i = 0; i < fieldCount; ++i) + { + // Check if field is in the default visible set + bool defaultVisible = defaultVisibleFields.count(fields[i].name) > 0; + m_columnVisibility[fields[i].name] = defaultVisible; + } + + // Load column preferences from config (will override defaults if exists) + LoadColumnPreferences(); +} + +CMuSkillEditorUI::~CMuSkillEditorUI() +{ + // Save column preferences when destroying + SaveColumnPreferences(); + + if (m_pTable) + { + delete m_pTable; + m_pTable = nullptr; + } +} + +CMuSkillEditorUI& CMuSkillEditorUI::GetInstance() +{ + static CMuSkillEditorUI instance; + return instance; +} + +void CMuSkillEditorUI::Render(bool& showEditor) +{ + // Access external skill data + extern SKILL_ATTRIBUTE* SkillAttribute; + if (!SkillAttribute) + return; + + // Constants for layout + constexpr float TOOLBAR_HEIGHT = 40.0f; + constexpr float CONSOLE_HEIGHT = 200.0f; + + ImGuiIO& io = ImGui::GetIO(); + + // Calculate available space between toolbar and console + float availableTop = TOOLBAR_HEIGHT; + float availableBottom = io.DisplaySize.y - CONSOLE_HEIGHT; + float availableHeight = availableBottom - availableTop; + + // Set initial window size and constraints + ImGui::SetNextWindowSizeConstraints( + ImVec2(400, 300), // Min size + ImVec2(io.DisplaySize.x, availableHeight) // Max size + ); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; + if (ImGui::Begin(EDITOR_TEXT("label_skill_editor_title"), &showEditor, flags)) + { + // Clamp window position to stay within bounds + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 windowSize = ImGui::GetWindowSize(); + + // Clamp to keep window between toolbar and console + if (windowPos.y < availableTop) + { + ImGui::SetWindowPos(ImVec2(windowPos.x, availableTop)); + } + if (windowPos.y + windowSize.y > availableBottom) + { + ImGui::SetWindowPos(ImVec2(windowPos.x, availableBottom - windowSize.y)); + } + + // Clamp horizontally to stay within screen bounds + if (windowPos.x < 0) + { + ImGui::SetWindowPos(ImVec2(0, windowPos.y)); + } + if (windowPos.x + windowSize.x > io.DisplaySize.x) + { + ImGui::SetWindowPos(ImVec2(io.DisplaySize.x - windowSize.x, windowPos.y)); + } + + // Check if hovering this window OR any popup + bool isHovering = ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByPopup) || + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) || + ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId); + + if (isHovering) + { + g_MuEditorCore.SetHoveringUI(true); + } + + // Render action buttons (Save, Export CSV) + CSkillEditorActions::RenderAllButtons(); + ImGui::Separator(); + + RenderSearchBar(); + ImGui::SameLine(); + RenderColumnVisibilityMenu(); + + // Freeze columns checkbox + ImGui::SameLine(); + ImGui::Checkbox(EDITOR_TEXT("label_freeze_columns"), &m_bFreezeColumns); + + ImGui::Separator(); + + // Render table with current search filter + std::string searchFilter = m_szSkillSearchBuffer; + std::transform(searchFilter.begin(), searchFilter.end(), searchFilter.begin(), ::tolower); + + m_pTable->Render(searchFilter, m_columnVisibility, m_selectedRow, m_bFreezeColumns); + + // Render popups + CSkillEditorPopups::RenderPopups(); + } + ImGui::End(); +} + +void CMuSkillEditorUI::RenderSearchBar() +{ + ImGui::SetNextItemWidth(200); + ImGui::InputTextWithHint("##SkillSearch", EDITOR_TEXT("label_search_skills"), m_szSkillSearchBuffer, sizeof(m_szSkillSearchBuffer)); +} + +void CMuSkillEditorUI::RenderColumnVisibilityMenu() +{ + if (ImGui::Button(EDITOR_TEXT("btn_columns"))) + { + ImGui::OpenPopup("ColumnVisibility"); + } + + if (ImGui::BeginPopup("ColumnVisibility")) + { + ImGui::Text("%s", EDITOR_TEXT("popup_toggle_columns")); + ImGui::Separator(); + + // Select All / Unselect All buttons + if (ImGui::Button(EDITOR_TEXT("btn_select_all"), ImVec2(120, 0))) + { + for (auto& pair : m_columnVisibility) + { + pair.second = true; + } + } + ImGui::SameLine(); + if (ImGui::Button(EDITOR_TEXT("btn_unselect_all"), ImVec2(120, 0))) + { + for (auto& pair : m_columnVisibility) + { + pair.second = false; + } + } + + ImGui::Separator(); + + // Index column (special) + ImGui::Checkbox(EDITOR_TEXT("label_index"), &m_columnVisibility["Index"]); + + // All metadata fields + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + for (int i = 0; i < fieldCount; ++i) + { + const char* displayName = GetSkillFieldDisplayName(fields[i].name); + ImGui::Checkbox(displayName, &m_columnVisibility[fields[i].name]); + } + + ImGui::EndPopup(); + } +} + +void CMuSkillEditorUI::SaveColumnPreferences() +{ + g_MuEditorConfig.SetSkillEditorColumnVisibility(m_columnVisibility); + g_MuEditorConfig.Save(); +} + +void CMuSkillEditorUI::LoadColumnPreferences() +{ + const auto& savedPrefs = g_MuEditorConfig.GetSkillEditorColumnVisibility(); + if (!savedPrefs.empty()) + { + // Merge saved preferences with current defaults + for (const auto& pair : savedPrefs) + { + m_columnVisibility[pair.first] = pair.second; + } + } +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.h b/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.h new file mode 100644 index 000000000..582b9f0fb --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/MuSkillEditorUI.h @@ -0,0 +1,45 @@ +#pragma once + +#ifdef _EDITOR + +#include +#include + +// Forward declarations +class CSkillEditorTable; + +class CMuSkillEditorUI +{ +public: + static CMuSkillEditorUI& GetInstance(); + + void Render(bool& showEditor); + void ClearSearch() { m_szSkillSearchBuffer[0] = '\0'; } + void SaveColumnPreferences(); + void LoadColumnPreferences(); + +private: + CMuSkillEditorUI(); + ~CMuSkillEditorUI(); + + void RenderSearchBar(); + void RenderColumnVisibilityMenu(); + + char m_szSkillSearchBuffer[256]; + + // Column visibility state (cached from config) + std::map m_columnVisibility; + + // Selected row tracking + int m_selectedRow; + + // Column freezing state + bool m_bFreezeColumns; + + // Table renderer + CSkillEditorTable* m_pTable; +}; + +#define g_MuSkillEditorUI CMuSkillEditorUI::GetInstance() + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorActions.cpp b/src/MuEditor/UI/SkillEditor/SkillEditorActions.cpp new file mode 100644 index 000000000..75b9ae020 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorActions.cpp @@ -0,0 +1,235 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillEditorActions.h" +#include "SkillEditorPopups.h" +#include "GameData/SkillData/SkillFieldMetadata.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "Translation/i18n.h" +#include "imgui.h" +#include +#include + +#include "../../../source/DataHandler/SkillData/SkillDataHandler.h" + +extern std::wstring g_strSelectedML; + +// ===== HELPER FUNCTIONS ===== + +void CSkillEditorActions::ConvertSkillName(char* outBuffer, size_t bufferSize, const wchar_t* name) +{ + WideCharToMultiByte(CP_UTF8, 0, name, -1, outBuffer, (int)bufferSize, NULL, NULL); +} + +std::string CSkillEditorActions::GetFieldValueAsString(const SKILL_ATTRIBUTE& skill, const SkillFieldDescriptor& desc) +{ + std::stringstream ss; + + // Get pointer to the field using offset + const BYTE* skillPtr = reinterpret_cast(&skill); + const void* fieldPtr = skillPtr + desc.offset; + + switch (desc.type) + { + case ESkillFieldType::Bool: + ss << (*reinterpret_cast(fieldPtr) ? 1 : 0); + break; + + case ESkillFieldType::Byte: + ss << (int)*reinterpret_cast(fieldPtr); + break; + + case ESkillFieldType::Word: + ss << *reinterpret_cast(fieldPtr); + break; + + case ESkillFieldType::Int: + ss << *reinterpret_cast(fieldPtr); + break; + + case ESkillFieldType::DWord: + ss << *reinterpret_cast(fieldPtr); + break; + + case ESkillFieldType::WCharArray: + { + char buffer[256]; + ConvertSkillName(buffer, sizeof(buffer), reinterpret_cast(fieldPtr)); + ss << buffer; + break; + } + } + + return ss.str(); +} + +// ===== EXPORT FUNCTIONS ===== + +std::string CSkillEditorActions::GetCSVHeader() +{ + std::stringstream ss; + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + ss << "Index"; + for (int i = 0; i < fieldCount; ++i) + { + ss << "," << GetSkillFieldDisplayName(fields[i].name); + } + + return ss.str(); +} + +std::string CSkillEditorActions::ExportSkillToReadable(int skillIndex, SKILL_ATTRIBUTE& skill) +{ + std::stringstream ss; + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + ss << "Skill " << skillIndex << ":\n"; + + for (int i = 0; i < fieldCount; ++i) + { + const char* displayName = GetSkillFieldDisplayName(fields[i].name); + std::string value = GetFieldValueAsString(skill, fields[i]); + ss << " " << displayName << ": " << value << "\n"; + } + + return ss.str(); +} + +std::string CSkillEditorActions::ExportSkillToCSV(int skillIndex, SKILL_ATTRIBUTE& skill) +{ + std::stringstream ss; + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + ss << skillIndex; + for (int i = 0; i < fieldCount; ++i) + { + std::string value = GetFieldValueAsString(skill, fields[i]); + ss << "," << value; + } + + return ss.str(); +} + +std::string CSkillEditorActions::ExportSkillCombined(int skillIndex, SKILL_ATTRIBUTE& skill) +{ + std::stringstream ss; + ss << ExportSkillToReadable(skillIndex, skill); + ss << "\n"; + ss << "CSV: " << ExportSkillToCSV(skillIndex, skill); + ss << "\n"; + return ss.str(); +} + +// ===== ACTION BUTTONS ===== + +void CSkillEditorActions::RenderSaveButton() +{ + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.6f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.7f, 0.9f, 1.0f)); + + if (ImGui::Button(EDITOR_TEXT("btn_save_skills"))) + { + wchar_t fileName[256]; + swprintf_s(fileName, _countof(fileName), L"Data\\Local\\%ls\\Skill_%ls.bmd", g_strSelectedML.c_str(), g_strSelectedML.c_str()); + + std::string changeLog; + if (g_SkillDataHandler.Save(fileName, &changeLog)) + { + // Log change details first, then save completion message + g_MuEditorConsoleUI.LogEditor(changeLog); + g_MuEditorConsoleUI.LogEditor("=== SAVE COMPLETED ==="); + ImGui::OpenPopup("Save Success"); + } + else + { + // Check if it failed due to no changes + if (!changeLog.empty() && changeLog.find("No changes") != std::string::npos) + { + g_MuEditorConsoleUI.LogEditor(changeLog); + } + else + { + g_MuEditorConsoleUI.LogEditor(EDITOR_TEXT("msg_save_failed")); + ImGui::OpenPopup("Save Failed"); + } + } + } + + ImGui::PopStyleColor(2); +} + +void CSkillEditorActions::RenderExportLegacyButton() +{ + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.4f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.5f, 0.9f, 1.0f)); + + if (ImGui::Button(EDITOR_TEXT("btn_export_s6e3"))) + { + wchar_t fileName[256]; + swprintf_s(fileName, _countof(fileName), L"Data\\Local\\%ls\\Skill_S6E3.bmd", g_strSelectedML.c_str()); + + if (g_SkillDataHandler.ExportAsS6E3(fileName)) + { + std::string filename_str = "Skill_" + + std::string(g_strSelectedML.begin(), g_strSelectedML.end()) + + "_Legacy.bmd"; + g_MuEditorConsoleUI.LogEditor("Exported skills as legacy format (32-byte names): " + filename_str); + ImGui::OpenPopup("Export S6E3 Success"); + } + else + { + g_MuEditorConsoleUI.LogEditor(EDITOR_TEXT("msg_export_s6e3_failed")); + ImGui::OpenPopup("Export S6E3 Failed"); + } + } + + ImGui::PopStyleColor(2); +} + +void CSkillEditorActions::RenderExportCSVButton() +{ + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.8f, 0.6f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.9f, 0.7f, 1.0f)); + + if (ImGui::Button(EDITOR_TEXT("btn_export_csv_skills"), ImVec2(150, 0))) + { + wchar_t fileName[256]; + swprintf_s(fileName, _countof(fileName), L"Data\\Local\\%ls\\Skills.csv", g_strSelectedML.c_str()); + + if (g_SkillDataHandler.ExportToCsv(fileName)) + { + std::string filename_str = "Skills_" + + std::string(g_strSelectedML.begin(), g_strSelectedML.end()) + ".csv"; + g_MuEditorConsoleUI.LogEditor("Exported skills as CSV: " + filename_str); + ImGui::OpenPopup("Export CSV Success"); + } + else + { + g_MuEditorConsoleUI.LogEditor(EDITOR_TEXT("msg_export_csv_skills_failed")); + ImGui::OpenPopup("Export CSV Failed"); + } + } + + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Export all skills to CSV format"); + } + + ImGui::PopStyleColor(2); +} + +void CSkillEditorActions::RenderAllButtons() +{ + RenderSaveButton(); + ImGui::SameLine(); + RenderExportLegacyButton(); + ImGui::SameLine(); + RenderExportCSVButton(); +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorActions.h b/src/MuEditor/UI/SkillEditor/SkillEditorActions.h new file mode 100644 index 000000000..41f869bbe --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorActions.h @@ -0,0 +1,41 @@ +#pragma once + +#ifdef _EDITOR + +#include "GameData/SkillData/SkillFieldMetadata.h" +#include "_struct.h" +#include + +// Handles Save/Export action buttons for the Skill Editor +// Uses metadata-driven approach for automatic export generation +class CSkillEditorActions +{ +public: + static void RenderSaveButton(); + static void RenderExportLegacyButton(); + static void RenderExportCSVButton(); + + // Render all action buttons in a row + static void RenderAllButtons(); + + // Export skill data to CSV format (metadata-driven) + static std::string ExportSkillToCSV(int skillIndex, SKILL_ATTRIBUTE& skill); + + // Export skill data to readable format (key=value pairs, metadata-driven) + static std::string ExportSkillToReadable(int skillIndex, SKILL_ATTRIBUTE& skill); + + // Export both formats combined (readable + CSV) + static std::string ExportSkillCombined(int skillIndex, SKILL_ATTRIBUTE& skill); + + // Get CSV header row (metadata-driven) + static std::string GetCSVHeader(); + +private: + // Helper to convert skill name to UTF-8 + static void ConvertSkillName(char* outBuffer, size_t bufferSize, const wchar_t* name); + + // Get field value as string (descriptor-driven) + static std::string GetFieldValueAsString(const SKILL_ATTRIBUTE& skill, const SkillFieldDescriptor& desc); +}; + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorColumns.cpp b/src/MuEditor/UI/SkillEditor/SkillEditorColumns.cpp new file mode 100644 index 000000000..faa63bbd0 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorColumns.cpp @@ -0,0 +1,246 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillEditorColumns.h" +#include "SkillEditorTable.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "GameData/SkillData/SkillFieldDefs.h" +#include "Translation/i18n.h" +#include "_struct.h" +#include "_define.h" +#include "imgui.h" +#include +#include + +extern SKILL_ATTRIBUTE* SkillAttribute; + +// ===== HELPER FUNCTIONS ===== + +static std::string GetSkillNameUtf8(int skillIndex) +{ + char nameBuf[256]{}; + if (SkillAttribute && skillIndex >= 0 && skillIndex < MAX_SKILLS) + { + WideCharToMultiByte(CP_UTF8, 0, SkillAttribute[skillIndex].Name, -1, nameBuf, sizeof(nameBuf), NULL, NULL); + } + if (nameBuf[0] == '\0') + return ""; + return nameBuf; +} + +static void LogSkillFieldChange(int skillIndex, const char* columnName, const std::string& newValue) +{ + g_MuEditorConsoleUI.LogEditor( + "Changed skill " + std::to_string(skillIndex) + + " (" + GetSkillNameUtf8(skillIndex) + ") " + + columnName + " to " + newValue + ); +} + +// ===== X-MACRO-DRIVEN RENDERING ===== + +void CSkillEditorColumns::RenderFieldByDescriptor(const SkillFieldDescriptor& desc, int& colIdx, int skillIndex, + SKILL_ATTRIBUTE& skill, bool& rowInteracted, bool isVisible) +{ + // Use the template helper function from SkillFieldMetadata.h + ::RenderFieldByDescriptor(desc, this, skill, colIdx, skillIndex, rowInteracted, isVisible, MAX_SKILL_NAME); +} + +// ===== LOW-LEVEL TYPE-SPECIFIC RENDERING ===== + +void CSkillEditorColumns::RenderByteColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + BYTE& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + int intValue = value; + if (ImGui::InputInt("##input", &intValue, 0, 0)) + { + if (intValue >= 0 && intValue <= 255) + { + value = (BYTE)intValue; + LogSkillFieldChange(skillIndex, columnName, std::to_string(intValue)); + } + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CSkillEditorColumns::RenderWordColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + WORD& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + int intValue = value; + if (ImGui::InputInt("##input", &intValue, 0, 0)) + { + if (intValue >= 0 && intValue <= 65535) + { + value = (WORD)intValue; + LogSkillFieldChange(skillIndex, columnName, std::to_string(intValue)); + } + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CSkillEditorColumns::RenderIntColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + int& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + if (ImGui::InputInt("##input", &value, 0, 0)) + { + LogSkillFieldChange(skillIndex, columnName, std::to_string(value)); + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CSkillEditorColumns::RenderDWordColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + DWORD& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + // DWORD is unsigned, range 0 to 4294967295 + if (ImGui::InputScalar("##input", ImGuiDataType_U32, &value, nullptr, nullptr, "%u")) + { + LogSkillFieldChange(skillIndex, columnName, std::to_string(value)); + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CSkillEditorColumns::RenderBoolColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + bool& value, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + if (ImGui::Checkbox("##checkbox", &value)) + { + LogSkillFieldChange(skillIndex, columnName, value ? "true" : "false"); + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +void CSkillEditorColumns::RenderWCharArrayColumn( + const char* columnName, int& colIdx, int skillIndex, int uniqueId, + wchar_t* value, int arraySize, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + uniqueId); + ImGui::SetNextItemWidth(-FLT_MIN); + + char editableBuffer[256]; + WideCharToMultiByte(CP_UTF8, 0, value, -1, editableBuffer, sizeof(editableBuffer), NULL, NULL); + + if (ImGui::InputText("##input", editableBuffer, sizeof(editableBuffer))) + { + MultiByteToWideChar(CP_UTF8, 0, editableBuffer, -1, value, arraySize); + LogSkillFieldChange(skillIndex, columnName, std::string(editableBuffer)); + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +// ===== SPECIAL COLUMNS ===== + +void CSkillEditorColumns::RenderIndexColumn(int& colIdx, int skillIndex, bool& rowInteracted, bool isVisible) +{ + if (!isVisible) return; + + ImGui::TableSetColumnIndex(colIdx++); + ImGui::PushID(skillIndex * 100000 + 999999); + ImGui::SetNextItemWidth(-FLT_MIN); + + int newIndex = skillIndex; + ImGui::InputInt("##index", &newIndex, 0, 0); + + // Only process the change when the input is deactivated (Enter pressed or focus lost) + bool wasActive = ImGui::IsItemActive(); + bool wasDeactivated = ImGui::IsItemDeactivatedAfterEdit(); + + if (wasDeactivated && newIndex >= 0 && newIndex < MAX_SKILLS && newIndex != skillIndex) + { + char targetName[128]; + WideCharToMultiByte(CP_UTF8, 0, SkillAttribute[newIndex].Name, -1, targetName, sizeof(targetName), NULL, NULL); + + if (targetName[0] == '\0') + { + SKILL_ATTRIBUTE temp = SkillAttribute[skillIndex]; + SkillAttribute[skillIndex] = SkillAttribute[newIndex]; + SkillAttribute[newIndex] = temp; + + g_MuEditorConsoleUI.LogEditor("Moved skill from index " + std::to_string(skillIndex) + " to " + std::to_string(newIndex)); + + m_errorLogged = false; + + // Invalidate the filter to rebuild the skill list + if (m_pTable) + { + m_pTable->InvalidateFilter(); + } + + // Scroll to new index position + CSkillEditorTable::RequestScrollToIndex(newIndex); + } + else if (!m_errorLogged) + { + std::string errorMsg = i18n::FormatEditor("error_index_in_use", { + std::to_string(newIndex) + }); + g_MuEditorConsoleUI.LogEditor(errorMsg); + m_errorLogged = true; + } + } + + // Reset error flag when user starts editing a different field + if (wasActive) + { + if (m_lastEditedIndex != skillIndex) + { + m_errorLogged = false; + m_lastEditedIndex = skillIndex; + } + } + + if (ImGui::IsItemActivated()) rowInteracted = true; + ImGui::PopID(); +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorColumns.h b/src/MuEditor/UI/SkillEditor/SkillEditorColumns.h new file mode 100644 index 000000000..b29a9a416 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorColumns.h @@ -0,0 +1,58 @@ +#pragma once + +#ifdef _EDITOR + +#include +#include "GameData/SkillData/SkillFieldMetadata.h" +#include "_struct.h" + +// Forward declaration +class CSkillEditorTable; + +// Handles all column rendering for the Skill Editor +// Uses metadata-driven approach for automatic field rendering +class CSkillEditorColumns +{ +public: + CSkillEditorColumns() : m_pTable(nullptr), m_lastEditedIndex(-1), m_errorLogged(false) {} + ~CSkillEditorColumns() = default; + + // Set the parent table instance (needed for invalidating filter) + void SetTable(CSkillEditorTable* table) { m_pTable = table; } + + // X-macro-driven rendering - automatically renders any field based on descriptor + void RenderFieldByDescriptor(const SkillFieldDescriptor& desc, int& colIdx, int skillIndex, + SKILL_ATTRIBUTE& skill, bool& rowInteracted, bool isVisible); + + // Render Index column (special - not part of SKILL_ATTRIBUTE fields) + void RenderIndexColumn(int& colIdx, int skillIndex, bool& rowInteracted, bool isVisible); + + // Friend declaration for template helper in FieldMetadataHelper.h + template + friend void ::RenderFieldByDescriptor(const FieldDescriptor& desc, TColumns* cols, TStruct& data, + int& colIdx, int dataIndex, bool& rowInteracted, bool isVisible, int maxStringLength); + +private: + // Low-level type-specific rendering helpers + void RenderByteColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + BYTE& value, bool& rowInteracted, bool isVisible); + void RenderWordColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + WORD& value, bool& rowInteracted, bool isVisible); + void RenderIntColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + int& value, bool& rowInteracted, bool isVisible); + void RenderDWordColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + DWORD& value, bool& rowInteracted, bool isVisible); + void RenderBoolColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + bool& value, bool& rowInteracted, bool isVisible); + void RenderWCharArrayColumn(const char* columnName, int& colIdx, int skillIndex, int uniqueId, + wchar_t* value, int arraySize, bool& rowInteracted, bool isVisible); + + // Parent table reference + CSkillEditorTable* m_pTable; + + // Index column state tracking + int m_lastEditedIndex; + bool m_errorLogged; +}; + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorPopups.cpp b/src/MuEditor/UI/SkillEditor/SkillEditorPopups.cpp new file mode 100644 index 000000000..1ccd0ab8a --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorPopups.cpp @@ -0,0 +1,39 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillEditorPopups.h" +#include "imgui.h" +#include "Translation/i18n.h" + +bool CSkillEditorPopups::RenderSimplePopup(const char* popupId, const char* message) +{ + if (ImGui::BeginPopupModal(popupId, NULL, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("%s", message); + if (ImGui::Button(EDITOR_TEXT("btn_ok"), ImVec2(120, 0))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + return true; + } + return false; +} + +void CSkillEditorPopups::RenderPopups() +{ + // Save popups + RenderSimplePopup("Save Success", EDITOR_TEXT("popup_save_skills_success")); + RenderSimplePopup("Save Failed", EDITOR_TEXT("popup_save_skills_failed")); + + // Legacy Export popups + RenderSimplePopup("Export S6E3 Success", EDITOR_TEXT("popup_export_s6e3_success")); + RenderSimplePopup("Export S6E3 Failed", EDITOR_TEXT("popup_export_s6e3_failed")); + + // CSV Export popups + RenderSimplePopup("Export CSV Success", EDITOR_TEXT("popup_export_csv_skills_success")); + RenderSimplePopup("Export CSV Failed", EDITOR_TEXT("popup_export_csv_skills_failed")); +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorPopups.h b/src/MuEditor/UI/SkillEditor/SkillEditorPopups.h new file mode 100644 index 000000000..f755fadcd --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorPopups.h @@ -0,0 +1,17 @@ +#pragma once + +#ifdef _EDITOR + +// Handles popup dialogs for the Skill Editor +class CSkillEditorPopups +{ +public: + // Render all popups (call each frame) + static void RenderPopups(); + +private: + // Helper to render a simple popup with OK button + static bool RenderSimplePopup(const char* popupId, const char* message); +}; + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorTable.cpp b/src/MuEditor/UI/SkillEditor/SkillEditorTable.cpp new file mode 100644 index 000000000..81368dec9 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorTable.cpp @@ -0,0 +1,223 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillEditorTable.h" +#include "SkillEditorColumns.h" +#include "GameData/SkillData/SkillFieldMetadata.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "Translation/i18n.h" +#include "_struct.h" +#include "_define.h" +#include "imgui.h" +#include +#include + +extern SKILL_ATTRIBUTE* SkillAttribute; + +// Static member initialization +int CSkillEditorTable::s_scrollToIndex = -1; + +void CSkillEditorTable::RequestScrollToIndex(int index) +{ + s_scrollToIndex = index; +} + +void CSkillEditorTable::InvalidateFilter() +{ + m_isInitialized = false; +} + +CSkillEditorTable::CSkillEditorTable() + : m_isInitialized(false) + , m_pColumns(nullptr) +{ + m_pColumns = new CSkillEditorColumns(); + m_pColumns->SetTable(this); +} + +CSkillEditorTable::~CSkillEditorTable() +{ + if (m_pColumns) + { + delete m_pColumns; + m_pColumns = nullptr; + } +} + +void CSkillEditorTable::Render( + const std::string& searchFilter, + std::map& columnVisibility, + int& selectedRow, + bool freezeColumns) +{ + // Get metadata fields once at function scope + const SkillFieldDescriptor* fields = GetSkillFieldDescriptors(); + const int fieldCount = GetSkillFieldCount(); + + // Count visible columns + int visibleColumnCount = 0; + + // Count Index column if visible + if (columnVisibility.find("Index") != columnVisibility.end() && columnVisibility["Index"]) + { + visibleColumnCount++; + } + + // Count metadata fields that are visible + for (int i = 0; i < fieldCount; ++i) + { + if (columnVisibility.find(fields[i].name) != columnVisibility.end() && + columnVisibility[fields[i].name]) + { + visibleColumnCount++; + } + } + + if (visibleColumnCount == 0) + { + ImGui::Text(EDITOR_TEXT("label_no_columns")); + return; + } + + // Rebuild filtered list if search changed + if (searchFilter != m_lastSearchFilter || !m_isInitialized) + { + m_filteredSkills.clear(); + for (int i = 0; i < MAX_SKILLS; i++) + { + char nameBuffer[256]; + WideCharToMultiByte(CP_UTF8, 0, SkillAttribute[i].Name, -1, nameBuffer, sizeof(nameBuffer), NULL, NULL); + + if (nameBuffer[0] == '\0') continue; + + if (searchFilter.length() > 0) + { + std::string nameLower = nameBuffer; + std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower); + if (nameLower.find(searchFilter) == std::string::npos) + continue; + } + + m_filteredSkills.push_back(i); + } + + m_lastSearchFilter = searchFilter; + m_isInitialized = true; + } + + // Setup table flags + ImGuiTableFlags flags = + ImGuiTableFlags_Resizable | + ImGuiTableFlags_Reorderable | + ImGuiTableFlags_Hideable | + ImGuiTableFlags_Sortable | + ImGuiTableFlags_SortMulti | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_BordersOuter | + ImGuiTableFlags_BordersV | + ImGuiTableFlags_NoBordersInBody | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_ScrollX | + ImGuiTableFlags_SizingFixedFit; + + if (freezeColumns) + { + flags |= ImGuiTableFlags_ScrollX; + } + + // Begin table + if (ImGui::BeginTable("##SkillTable", visibleColumnCount, flags)) + { + // Freeze Index and Name columns if requested + if (freezeColumns) + { + int frozenColumns = 0; + if (columnVisibility.find("Index") != columnVisibility.end() && columnVisibility["Index"]) + frozenColumns++; + if (columnVisibility.find("Name") != columnVisibility.end() && columnVisibility["Name"]) + frozenColumns++; + + ImGui::TableSetupScrollFreeze(frozenColumns, 1); + } + + // Setup columns + bool indexVisible = columnVisibility.find("Index") != columnVisibility.end() && columnVisibility["Index"]; + if (indexVisible) + { + ImGui::TableSetupColumn(EDITOR_TEXT("label_index"), ImGuiTableColumnFlags_None, 60.0f); + } + + for (int i = 0; i < fieldCount; ++i) + { + if (columnVisibility.find(fields[i].name) != columnVisibility.end() && + columnVisibility[fields[i].name]) + { + const char* displayName = GetSkillFieldDisplayName(fields[i].name); + ImGui::TableSetupColumn(displayName, ImGuiTableColumnFlags_None, fields[i].width); + } + } + + ImGui::TableHeadersRow(); + + // Render rows + ImGuiListClipper clipper; + clipper.Begin((int)m_filteredSkills.size()); + + // Handle scroll request + if (s_scrollToIndex >= 0) + { + for (size_t i = 0; i < m_filteredSkills.size(); i++) + { + if (m_filteredSkills[i] == s_scrollToIndex) + { + ImGui::SetScrollY(i * ImGui::GetTextLineHeightWithSpacing()); + selectedRow = (int)i; + break; + } + } + s_scrollToIndex = -1; + } + + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) + { + int skillIndex = m_filteredSkills[row]; + SKILL_ATTRIBUTE& skill = SkillAttribute[skillIndex]; + + ImGui::TableNextRow(); + ImGui::PushID(skillIndex); + + bool rowInteracted = false; + int colIdx = 0; + + // Render Index column + if (indexVisible) + { + m_pColumns->RenderIndexColumn(colIdx, skillIndex, rowInteracted, true); + } + + // Render all metadata fields + for (int i = 0; i < fieldCount; ++i) + { + bool isVisible = columnVisibility.find(fields[i].name) != columnVisibility.end() && + columnVisibility[fields[i].name]; + m_pColumns->RenderFieldByDescriptor(fields[i], colIdx, skillIndex, skill, rowInteracted, isVisible); + } + + // Update selected row + if (rowInteracted) + { + selectedRow = row; + } + + ImGui::PopID(); + } + } + + ImGui::EndTable(); + } +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/SkillEditor/SkillEditorTable.h b/src/MuEditor/UI/SkillEditor/SkillEditorTable.h new file mode 100644 index 000000000..75b8e12e0 --- /dev/null +++ b/src/MuEditor/UI/SkillEditor/SkillEditorTable.h @@ -0,0 +1,45 @@ +#pragma once + +#ifdef _EDITOR + +#include +#include +#include + +// Forward declaration +class CSkillEditorColumns; + +// Handles the main table rendering for the Skill Editor +// Delegates column rendering to CSkillEditorColumns +class CSkillEditorTable +{ +public: + CSkillEditorTable(); + ~CSkillEditorTable(); + + // Main render function + void Render(const std::string& searchFilter, + std::map& columnVisibility, + int& selectedRow, + bool freezeColumns); + + // Request scroll to a specific skill index + static void RequestScrollToIndex(int index); + + // Force rebuild of filtered list (call after data changes) + void InvalidateFilter(); + +private: + // Filter state + std::vector m_filteredSkills; + std::string m_lastSearchFilter; + bool m_isInitialized; + + // Column renderer + CSkillEditorColumns* m_pColumns; + + // Scroll request state + static int s_scrollToIndex; +}; + +#endif // _EDITOR diff --git a/src/bin/Translations/de/editor.json b/src/bin/Translations/de/editor.json new file mode 100644 index 000000000..827dbc2ee --- /dev/null +++ b/src/bin/Translations/de/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - German", + "language_name": "Deutsch", + + "btn_save": "Gegenstände Speichern", + "btn_save_skills": "Fähigkeiten Speichern", + "btn_export_s6e3": "Als S6E3 Exportieren", + "btn_export_csv": "Als CSV Exportieren", + "btn_export_csv_skills": "Fähigkeiten Als CSV Exportieren", + "btn_columns": "Spalten", + "btn_select_all": "Alle Auswählen", + "btn_unselect_all": "Auswahl Aufheben", + "btn_ok": "OK", + + "msg_save_success": "Erfolgreich gespeichert!", + "msg_save_failed": "Fehler beim Speichern der Gegenstände!", + "msg_save_skills_success": "Fähigkeiten erfolgreich gespeichert!", + "msg_save_skills_failed": "Fehler beim Speichern der Fähigkeiten!", + "msg_export_s6e3_success": "S6E3 Export erfolgreich abgeschlossen!", + "msg_export_s6e3_failed": "Fehler beim Exportieren in das S6E3 Format!", + "msg_export_csv_success": "CSV Export erfolgreich abgeschlossen!", + "msg_export_csv_failed": "Fehler beim Exportieren als CSV!", + "msg_export_csv_skills_success": "Fähigkeiten erfolgreich als CSV exportiert!", + "msg_export_csv_skills_failed": "Fehler beim Exportieren der Fähigkeiten als CSV!", + + "error_file_not_found": "Datei nicht gefunden: {0}", + "error_invalid_index": "Ungültiger Index: {0}", + "error_index_in_use": "Fehler: Index {0} wird bereits verwendet!", + + "label_search_text": "Suchen:", + "label_freeze_columns": "Index/Name Fixieren", + "label_no_columns": "Keine Spalten ausgewählt. Klicken Sie auf 'Spalten', um Spalten anzuzeigen.", + "label_index": "Index", + + "popup_toggle_columns": "Spaltensichtbarkeit Umschalten:", + "popup_save_success": "Gegenstände erfolgreich gespeichert!", + "popup_save_failed": "Fehler beim Speichern der Gegenstände!", + "popup_save_skills_success": "Fähigkeiten erfolgreich gespeichert!", + "popup_save_skills_failed": "Fehler beim Speichern der Fähigkeiten!", + "popup_export_csv_success": "Gegenstände erfolgreich als CSV exportiert!", + "popup_export_csv_failed": "Fehler beim Exportieren der Gegenstandsattribute als CSV!", + "popup_export_csv_skills_success": "Fähigkeiten erfolgreich als CSV exportiert!", + "popup_export_csv_skills_failed": "Fehler beim Exportieren der Fähigkeitsattribute als CSV!", + "popup_export_s6e3_success": "Erfolgreich als Legacy-Format exportiert!", + "popup_export_s6e3_failed": "Fehler beim Export als Legacy-Format!", + + "label_skill_editor_title": "Fähigkeiten-Editor", + "label_search_skills": "Fähigkeiten Suchen:" +} diff --git a/src/bin/Translations/de/game.json b/src/bin/Translations/de/game.json new file mode 100644 index 000000000..618e3a91b --- /dev/null +++ b/src/bin/Translations/de/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - German", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/de/metadata.json b/src/bin/Translations/de/metadata.json new file mode 100644 index 000000000..9c6d40433 --- /dev/null +++ b/src/bin/Translations/de/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - German", + + "field_Name": "Name", + "field_TwoHand": "Zweihändig", + "field_Level": "Stufe", + "field_m_byItemSlot": "Slot", + "field_m_wSkillIndex": "Fähigkeit", + "field_Width": "Breite", + "field_Height": "Höhe", + "field_DamageMin": "Min Schaden", + "field_DamageMax": "Max Schaden", + "field_SuccessfulBlocking": "Blockrate", + "field_Defense": "Verteidigung", + "field_MagicDefense": "Magische Verteidigung", + "field_WeaponSpeed": "Angriffsgeschwindigkeit", + "field_WalkSpeed": "Bewegungsgeschwindigkeit", + "field_Durability": "Haltbarkeit", + "field_MagicDur": "Magische Haltbarkeit", + "field_MagicPower": "Magische Kraft", + "field_RequireStrength": "Benötigte Stärke", + "field_RequireDexterity": "Benötigte Geschicklichkeit", + "field_RequireEnergy": "Benötigte Energie", + "field_RequireVitality": "Benötigte Vitalität", + "field_RequireCharisma": "Benötigte Führung", + "field_RequireLevel": "Benötigte Stufe", + "field_Value": "Verkaufswert", + "field_iZen": "Kaufpreis", + "field_AttType": "Angriffstyp", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Eisresistenz", + "field_Resistance[1]": "Giftresistenz", + "field_Resistance[2]": "Blitzresistenz", + "field_Resistance[3]": "Feuerresistenz", + "field_Resistance[4]": "Erdresistenz", + "field_Resistance[5]": "Windresistenz", + "field_Resistance[6]": "Wasserresistenz", + "field_Resistance[7]": "Resistenz 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Schaden", + "field_Mana": "Mana", + "field_AbilityGuage": "AG Kosten", + "field_Distance": "Reichweite", + "field_Delay": "Abklingzeit", + "field_Energy": "Benötigte Energie", + "field_Charisma": "Benötigte Führung", + "field_MasteryType": "Meisterschaftstyp", + "field_SkillUseType": "Verwendungstyp", + "field_SkillBrand": "Marke", + "field_KillCount": "Killanzahl", + "field_SkillRank": "Rang", + "field_Magic_Icon": "Symbol", + "field_TypeSkill": "Typ", + "field_Strength": "Benötigte Stärke", + "field_Dexterity": "Benötigte Geschicklichkeit", + "field_ItemSkill": "Gegenstandsfähigkeit", + "field_IsDamage": "Verursacht Schaden", + "field_Effect": "Effekt", + + "field_RequireDutyClass[0]": "Pflicht 0", + "field_RequireDutyClass[1]": "Pflicht 1", + "field_RequireDutyClass[2]": "Pflicht 2" +} diff --git a/src/bin/Translations/en/editor.json b/src/bin/Translations/en/editor.json index 2e80b73fc..10d74bff4 100644 --- a/src/bin/Translations/en/editor.json +++ b/src/bin/Translations/en/editor.json @@ -3,8 +3,10 @@ "language_name": "English", "btn_save": "Save Items", + "btn_save_skills": "Save Skills", "btn_export_s6e3": "Export as S6E3", "btn_export_csv": "Export as CSV", + "btn_export_csv_skills": "Export Skills as CSV", "btn_columns": "Columns", "btn_select_all": "Select All", "btn_unselect_all": "Unselect All", @@ -12,10 +14,14 @@ "msg_save_success": "Save completed successfully!", "msg_save_failed": "Failed to save items!", + "msg_save_skills_success": "Skills saved successfully!", + "msg_save_skills_failed": "Failed to save skills!", "msg_export_s6e3_success": "S6E3 export completed successfully!", "msg_export_s6e3_failed": "Failed to export as S6E3 format!", "msg_export_csv_success": "CSV export completed successfully!", "msg_export_csv_failed": "Failed to export as CSV!", + "msg_export_csv_skills_success": "Skills exported as CSV successfully!", + "msg_export_csv_skills_failed": "Failed to export skills as CSV!", "error_file_not_found": "File not found: {0}", "error_invalid_index": "Invalid index: {0}", @@ -29,8 +35,15 @@ "popup_toggle_columns": "Toggle Column Visibility:", "popup_save_success": "Items saved successfully!", "popup_save_failed": "Failed to save items!", + "popup_save_skills_success": "Skills saved successfully!", + "popup_save_skills_failed": "Failed to save skills!", + "popup_export_s6e3_success": "Exported as legacy format successfully!", + "popup_export_s6e3_failed": "Failed to export as legacy format!", "popup_export_csv_success": "Items exported as CSV successfully!", "popup_export_csv_failed": "Failed to export item attributes as CSV!", - "popup_export_s6e3_success": "Items exported as S6E3 legacy format successfully!", - "popup_export_s6e3_failed": "Failed to export item attributes as S6E3 format!" + "popup_export_csv_skills_success": "Skills exported as CSV successfully!", + "popup_export_csv_skills_failed": "Failed to export skill attributes as CSV!", + + "label_skill_editor_title": "Skill Editor", + "label_search_skills": "Search Skills:" } diff --git a/src/bin/Translations/en/metadata.json b/src/bin/Translations/en/metadata.json index af448a1cb..e061d88bc 100644 --- a/src/bin/Translations/en/metadata.json +++ b/src/bin/Translations/en/metadata.json @@ -43,5 +43,30 @@ "field_Resistance[4]": "Earth Resistance", "field_Resistance[5]": "Wind Resistance", "field_Resistance[6]": "Water Resistance", - "field_Resistance[7]": "Resistance 7" + "field_Resistance[7]": "Resistance 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Damage", + "field_Mana": "Mana", + "field_AbilityGuage": "AG Cost", + "field_Distance": "Range", + "field_Delay": "Cooldown", + "field_Energy": "Req Energy", + "field_Charisma": "Req Leadership", + "field_MasteryType": "Mastery Type", + "field_SkillUseType": "Use Type", + "field_SkillBrand": "Brand", + "field_KillCount": "Kill Count", + "field_SkillRank": "Rank", + "field_Magic_Icon": "Icon", + "field_TypeSkill": "Type", + "field_Strength": "Req Strength", + "field_Dexterity": "Req Dexterity", + "field_ItemSkill": "Item Skill", + "field_IsDamage": "Is Damage", + "field_Effect": "Effect", + + "field_RequireDutyClass[0]": "Duty 0", + "field_RequireDutyClass[1]": "Duty 1", + "field_RequireDutyClass[2]": "Duty 2" } diff --git a/src/bin/Translations/es/editor.json b/src/bin/Translations/es/editor.json index 49db4b6e3..1ad99aa57 100644 --- a/src/bin/Translations/es/editor.json +++ b/src/bin/Translations/es/editor.json @@ -3,23 +3,29 @@ "language_name": "Español", "btn_save": "Guardar Objetos", + "btn_save_skills": "Guardar Habilidades", "btn_export_s6e3": "Exportar como S6E3", "btn_export_csv": "Exportar como CSV", + "btn_export_csv_skills": "Exportar Habilidades como CSV", "btn_columns": "Columnas", "btn_select_all": "Seleccionar Todo", "btn_unselect_all": "Deseleccionar Todo", "btn_ok": "Aceptar", "msg_save_success": "¡Guardado completado exitosamente!", - "msg_save_failed": "¡Error al guardar los objetos!", + "msg_save_failed": "¡Error al guardar objetos!", + "msg_save_skills_success": "¡Habilidades guardadas exitosamente!", + "msg_save_skills_failed": "¡Error al guardar habilidades!", "msg_export_s6e3_success": "¡Exportación S6E3 completada exitosamente!", "msg_export_s6e3_failed": "¡Error al exportar en formato S6E3!", "msg_export_csv_success": "¡Exportación CSV completada exitosamente!", "msg_export_csv_failed": "¡Error al exportar como CSV!", + "msg_export_csv_skills_success": "¡Habilidades exportadas como CSV exitosamente!", + "msg_export_csv_skills_failed": "¡Error al exportar habilidades como CSV!", "error_file_not_found": "Archivo no encontrado: {0}", - "error_invalid_index": "Índice inválido: {0}", - "error_index_in_use": "Error: ¡El índice {0} ya está en uso!", + "error_invalid_index": "Índice no válido: {0}", + "error_index_in_use": "¡Error: El índice {0} ya está en uso!", "label_search_text": "Buscar:", "label_freeze_columns": "Congelar Índice/Nombre", @@ -28,9 +34,16 @@ "popup_toggle_columns": "Alternar Visibilidad de Columnas:", "popup_save_success": "¡Objetos guardados exitosamente!", - "popup_save_failed": "¡Error al guardar los objetos!", + "popup_save_failed": "¡Error al guardar objetos!", + "popup_save_skills_success": "¡Habilidades guardadas exitosamente!", + "popup_save_skills_failed": "¡Error al guardar habilidades!", "popup_export_csv_success": "¡Objetos exportados como CSV exitosamente!", "popup_export_csv_failed": "¡Error al exportar atributos de objetos como CSV!", - "popup_export_s6e3_success": "¡Objetos exportados en formato S6E3 legacy exitosamente!", - "popup_export_s6e3_failed": "¡Error al exportar atributos de objetos en formato S6E3!" + "popup_export_csv_skills_success": "¡Habilidades exportadas como CSV exitosamente!", + "popup_export_csv_skills_failed": "¡Error al exportar atributos de habilidades como CSV!", + "popup_export_s6e3_success": "¡Exportado como formato heredado exitosamente!", + "popup_export_s6e3_failed": "¡Error al exportar como formato heredado!", + + "label_skill_editor_title": "Editor de Habilidades", + "label_search_skills": "Buscar Habilidades:" } diff --git a/src/bin/Translations/es/game.json b/src/bin/Translations/es/game.json index ce10aebc7..7b3f09bce 100644 --- a/src/bin/Translations/es/game.json +++ b/src/bin/Translations/es/game.json @@ -1,4 +1,4 @@ { - "_comment": "Game Content Translations - Spanish", - "_note": "Currently empty - for future game localization (items, NPCs, quests, etc.)" + "_comment": "Traducciones de Contenido del Juego - Español", + "_note": "Actualmente vacío - para futura localización del juego (objetos, NPCs, misiones, etc.)" } diff --git a/src/bin/Translations/es/metadata.json b/src/bin/Translations/es/metadata.json index b055821bb..1932fe3c4 100644 --- a/src/bin/Translations/es/metadata.json +++ b/src/bin/Translations/es/metadata.json @@ -1,5 +1,5 @@ { - "_comment": "Field Names and Metadata Translations - Spanish", + "_comment": "Traducciones de Nombres de Campos y Metadatos - Español", "field_Name": "Nombre", "field_TwoHand": "Dos Manos", @@ -8,8 +8,8 @@ "field_m_wSkillIndex": "Habilidad", "field_Width": "Ancho", "field_Height": "Alto", - "field_DamageMin": "Daño Mín", - "field_DamageMax": "Daño Máx", + "field_DamageMin": "Daño Mínimo", + "field_DamageMax": "Daño Máximo", "field_SuccessfulBlocking": "Tasa de Bloqueo", "field_Defense": "Defensa", "field_MagicDefense": "Defensa Mágica", @@ -43,5 +43,30 @@ "field_Resistance[4]": "Resistencia a la Tierra", "field_Resistance[5]": "Resistencia al Viento", "field_Resistance[6]": "Resistencia al Agua", - "field_Resistance[7]": "Resistencia 7" + "field_Resistance[7]": "Resistencia 7", + + "_comment_skills": "Traducciones de Campos de Habilidad", + "field_Damage": "Daño", + "field_Mana": "Maná", + "field_AbilityGuage": "Costo de AG", + "field_Distance": "Alcance", + "field_Delay": "Recarga", + "field_Energy": "Req Energía", + "field_Charisma": "Req Liderazgo", + "field_MasteryType": "Tipo de Maestría", + "field_SkillUseType": "Tipo de Uso", + "field_SkillBrand": "Marca", + "field_KillCount": "Contador de Muertes", + "field_SkillRank": "Rango", + "field_Magic_Icon": "Ícono", + "field_TypeSkill": "Tipo", + "field_Strength": "Req Fuerza", + "field_Dexterity": "Req Destreza", + "field_ItemSkill": "Habilidad de Objeto", + "field_IsDamage": "Es Daño", + "field_Effect": "Efecto", + + "field_RequireDutyClass[0]": "Deber 0", + "field_RequireDutyClass[1]": "Deber 1", + "field_RequireDutyClass[2]": "Deber 2" } diff --git a/src/bin/Translations/id/editor.json b/src/bin/Translations/id/editor.json new file mode 100644 index 000000000..e7e4976fa --- /dev/null +++ b/src/bin/Translations/id/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - Indonesian", + "language_name": "Bahasa Indonesia", + + "btn_save": "Simpan Item", + "btn_save_skills": "Simpan Skill", + "btn_export_s6e3": "Ekspor sebagai S6E3", + "btn_export_csv": "Ekspor sebagai CSV", + "btn_export_csv_skills": "Ekspor Skill sebagai CSV", + "btn_columns": "Kolom", + "btn_select_all": "Pilih Semua", + "btn_unselect_all": "Batalkan Pilihan", + "btn_ok": "OK", + + "msg_save_success": "Berhasil disimpan!", + "msg_save_failed": "Gagal menyimpan item!", + "msg_save_skills_success": "Skill berhasil disimpan!", + "msg_save_skills_failed": "Gagal menyimpan skill!", + "msg_export_s6e3_success": "Ekspor S6E3 berhasil diselesaikan!", + "msg_export_s6e3_failed": "Gagal mengekspor ke format S6E3!", + "msg_export_csv_success": "Ekspor CSV berhasil diselesaikan!", + "msg_export_csv_failed": "Gagal mengekspor sebagai CSV!", + "msg_export_csv_skills_success": "Skill berhasil diekspor sebagai CSV!", + "msg_export_csv_skills_failed": "Gagal mengekspor skill sebagai CSV!", + + "error_file_not_found": "File tidak ditemukan: {0}", + "error_invalid_index": "Indeks tidak valid: {0}", + "error_index_in_use": "Error: Indeks {0} sudah digunakan!", + + "label_search_text": "Cari:", + "label_freeze_columns": "Bekukan Indeks/Nama", + "label_no_columns": "Tidak ada kolom yang dipilih. Klik 'Kolom' untuk menampilkan kolom.", + "label_index": "Indeks", + + "popup_toggle_columns": "Alihkan Visibilitas Kolom:", + "popup_save_success": "Item berhasil disimpan!", + "popup_save_failed": "Gagal menyimpan item!", + "popup_save_skills_success": "Skill berhasil disimpan!", + "popup_save_skills_failed": "Gagal menyimpan skill!", + "popup_export_csv_success": "Item berhasil diekspor sebagai CSV!", + "popup_export_csv_failed": "Gagal mengekspor atribut item sebagai CSV!", + "popup_export_csv_skills_success": "Skill berhasil diekspor sebagai CSV!", + "popup_export_csv_skills_failed": "Gagal mengekspor atribut skill sebagai CSV!", + "popup_export_s6e3_success": "Berhasil diekspor sebagai format lawas!", + "popup_export_s6e3_failed": "Gagal mengekspor sebagai format lawas!", + + "label_skill_editor_title": "Editor Skill", + "label_search_skills": "Cari Skill:" +} diff --git a/src/bin/Translations/id/game.json b/src/bin/Translations/id/game.json new file mode 100644 index 000000000..ece3ab594 --- /dev/null +++ b/src/bin/Translations/id/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - Indonesian", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/id/metadata.json b/src/bin/Translations/id/metadata.json new file mode 100644 index 000000000..f58c05d24 --- /dev/null +++ b/src/bin/Translations/id/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - Indonesian", + + "field_Name": "Nama", + "field_TwoHand": "Dua Tangan", + "field_Level": "Level", + "field_m_byItemSlot": "Slot", + "field_m_wSkillIndex": "Skill", + "field_Width": "Lebar", + "field_Height": "Tinggi", + "field_DamageMin": "Damage Min", + "field_DamageMax": "Damage Maks", + "field_SuccessfulBlocking": "Tingkat Block", + "field_Defense": "Pertahanan", + "field_MagicDefense": "Pertahanan Sihir", + "field_WeaponSpeed": "Kecepatan Serangan", + "field_WalkSpeed": "Kecepatan Gerak", + "field_Durability": "Daya Tahan", + "field_MagicDur": "Daya Tahan Sihir", + "field_MagicPower": "Kekuatan Sihir", + "field_RequireStrength": "Strength Diperlukan", + "field_RequireDexterity": "Agility Diperlukan", + "field_RequireEnergy": "Energy Diperlukan", + "field_RequireVitality": "Vitality Diperlukan", + "field_RequireCharisma": "Command Diperlukan", + "field_RequireLevel": "Level Diperlukan", + "field_Value": "Nilai Jual", + "field_iZen": "Harga Beli", + "field_AttType": "Tipe Serangan", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Resistensi Es", + "field_Resistance[1]": "Resistensi Racun", + "field_Resistance[2]": "Resistensi Petir", + "field_Resistance[3]": "Resistensi Api", + "field_Resistance[4]": "Resistensi Tanah", + "field_Resistance[5]": "Resistensi Angin", + "field_Resistance[6]": "Resistensi Air", + "field_Resistance[7]": "Resistensi 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Damage", + "field_Mana": "Mana", + "field_AbilityGuage": "Biaya AG", + "field_Distance": "Jangkauan", + "field_Delay": "Cooldown", + "field_Energy": "Energy Diperlukan", + "field_Charisma": "Command Diperlukan", + "field_MasteryType": "Tipe Mastery", + "field_SkillUseType": "Tipe Penggunaan", + "field_SkillBrand": "Merek", + "field_KillCount": "Jumlah Kill", + "field_SkillRank": "Peringkat", + "field_Magic_Icon": "Ikon", + "field_TypeSkill": "Tipe", + "field_Strength": "Strength Diperlukan", + "field_Dexterity": "Agility Diperlukan", + "field_ItemSkill": "Skill Item", + "field_IsDamage": "Memberikan Damage", + "field_Effect": "Efek", + + "field_RequireDutyClass[0]": "Tugas 0", + "field_RequireDutyClass[1]": "Tugas 1", + "field_RequireDutyClass[2]": "Tugas 2" +} diff --git a/src/bin/Translations/pl/editor.json b/src/bin/Translations/pl/editor.json new file mode 100644 index 000000000..b2bfbd6c5 --- /dev/null +++ b/src/bin/Translations/pl/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - Polish", + "language_name": "Polski", + + "btn_save": "Zapisz Przedmioty", + "btn_save_skills": "Zapisz Umiejętności", + "btn_export_s6e3": "Eksportuj jako S6E3", + "btn_export_csv": "Eksportuj jako CSV", + "btn_export_csv_skills": "Eksportuj Umiejętności jako CSV", + "btn_columns": "Kolumny", + "btn_select_all": "Zaznacz Wszystkie", + "btn_unselect_all": "Odznacz Wszystkie", + "btn_ok": "OK", + + "msg_save_success": "Zapisano pomyślnie!", + "msg_save_failed": "Nie udało się zapisać przedmiotów!", + "msg_save_skills_success": "Umiejętności zapisane pomyślnie!", + "msg_save_skills_failed": "Nie udało się zapisać umiejętności!", + "msg_export_s6e3_success": "Eksport S6E3 zakończony pomyślnie!", + "msg_export_s6e3_failed": "Nie udało się wyeksportować do formatu S6E3!", + "msg_export_csv_success": "Eksport CSV zakończony pomyślnie!", + "msg_export_csv_failed": "Nie udało się wyeksportować do CSV!", + "msg_export_csv_skills_success": "Umiejętności wyeksportowane jako CSV pomyślnie!", + "msg_export_csv_skills_failed": "Nie udało się wyeksportować umiejętności do CSV!", + + "error_file_not_found": "Nie znaleziono pliku: {0}", + "error_invalid_index": "Nieprawidłowy indeks: {0}", + "error_index_in_use": "Błąd: Indeks {0} jest już używany!", + + "label_search_text": "Szukaj:", + "label_freeze_columns": "Zamroź Indeks/Nazwę", + "label_no_columns": "Nie wybrano kolumn. Kliknij 'Kolumny', aby pokazać kolumny.", + "label_index": "Indeks", + + "popup_toggle_columns": "Przełącz Widoczność Kolumn:", + "popup_save_success": "Przedmioty zapisane pomyślnie!", + "popup_save_failed": "Nie udało się zapisać przedmiotów!", + "popup_save_skills_success": "Umiejętności zapisane pomyślnie!", + "popup_save_skills_failed": "Nie udało się zapisać umiejętności!", + "popup_export_csv_success": "Przedmioty wyeksportowane jako CSV pomyślnie!", + "popup_export_csv_failed": "Nie udało się wyeksportować atrybutów przedmiotów do CSV!", + "popup_export_csv_skills_success": "Umiejętności wyeksportowane jako CSV pomyślnie!", + "popup_export_csv_skills_failed": "Nie udało się wyeksportować atrybutów umiejętności do CSV!", + "popup_export_s6e3_success": "Pomyślnie wyeksportowano jako format starszego typu!", + "popup_export_s6e3_failed": "Nie udało się wyeksportować jako format starszego typu!", + + "label_skill_editor_title": "Edytor Umiejętności", + "label_search_skills": "Szukaj Umiejętności:" +} diff --git a/src/bin/Translations/pl/game.json b/src/bin/Translations/pl/game.json new file mode 100644 index 000000000..573c6a75d --- /dev/null +++ b/src/bin/Translations/pl/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - Polish", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/pl/metadata.json b/src/bin/Translations/pl/metadata.json new file mode 100644 index 000000000..ca48e2a47 --- /dev/null +++ b/src/bin/Translations/pl/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - Polish", + + "field_Name": "Nazwa", + "field_TwoHand": "Dwuręczny", + "field_Level": "Poziom", + "field_m_byItemSlot": "Slot", + "field_m_wSkillIndex": "Umiejętność", + "field_Width": "Szerokość", + "field_Height": "Wysokość", + "field_DamageMin": "Min Obrażenia", + "field_DamageMax": "Maks Obrażenia", + "field_SuccessfulBlocking": "Wskaźnik Bloku", + "field_Defense": "Obrona", + "field_MagicDefense": "Obrona Magiczna", + "field_WeaponSpeed": "Prędkość Ataku", + "field_WalkSpeed": "Prędkość Ruchu", + "field_Durability": "Wytrzymałość", + "field_MagicDur": "Wytrzymałość Magiczna", + "field_MagicPower": "Moc Magiczna", + "field_RequireStrength": "Wymagana Siła", + "field_RequireDexterity": "Wymagana Zręczność", + "field_RequireEnergy": "Wymagana Energia", + "field_RequireVitality": "Wymagana Witalność", + "field_RequireCharisma": "Wymagane Przywództwo", + "field_RequireLevel": "Wymagany Poziom", + "field_Value": "Wartość Sprzedaży", + "field_iZen": "Cena Zakupu", + "field_AttType": "Typ Ataku", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Odporność na Lód", + "field_Resistance[1]": "Odporność na Truciznę", + "field_Resistance[2]": "Odporność na Błyskawice", + "field_Resistance[3]": "Odporność na Ogień", + "field_Resistance[4]": "Odporność na Ziemię", + "field_Resistance[5]": "Odporność na Wiatr", + "field_Resistance[6]": "Odporność na Wodę", + "field_Resistance[7]": "Odporność 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Obrażenia", + "field_Mana": "Mana", + "field_AbilityGuage": "Koszt AG", + "field_Distance": "Zasięg", + "field_Delay": "Czas Odnowienia", + "field_Energy": "Wymagana Energia", + "field_Charisma": "Wymagane Przywództwo", + "field_MasteryType": "Typ Mistrzostwa", + "field_SkillUseType": "Typ Użycia", + "field_SkillBrand": "Marka", + "field_KillCount": "Liczba Zabójstw", + "field_SkillRank": "Ranga", + "field_Magic_Icon": "Ikona", + "field_TypeSkill": "Typ", + "field_Strength": "Wymagana Siła", + "field_Dexterity": "Wymagana Zręczność", + "field_ItemSkill": "Umiejętność Przedmiotu", + "field_IsDamage": "Zadaje Obrażenia", + "field_Effect": "Efekt", + + "field_RequireDutyClass[0]": "Obowiązek 0", + "field_RequireDutyClass[1]": "Obowiązek 1", + "field_RequireDutyClass[2]": "Obowiązek 2" +} diff --git a/src/bin/Translations/pt/editor.json b/src/bin/Translations/pt/editor.json index b73546dd9..9da1ea79a 100644 --- a/src/bin/Translations/pt/editor.json +++ b/src/bin/Translations/pt/editor.json @@ -3,8 +3,10 @@ "language_name": "Português", "btn_save": "Salvar Itens", + "btn_save_skills": "Salvar Habilidades", "btn_export_s6e3": "Exportar como S6E3", "btn_export_csv": "Exportar como CSV", + "btn_export_csv_skills": "Exportar Habilidades como CSV", "btn_columns": "Colunas", "btn_select_all": "Selecionar Tudo", "btn_unselect_all": "Desselecionar Tudo", @@ -12,10 +14,14 @@ "msg_save_success": "Salvamento concluído com sucesso!", "msg_save_failed": "Falha ao salvar itens!", + "msg_save_skills_success": "Habilidades salvas com sucesso!", + "msg_save_skills_failed": "Falha ao salvar habilidades!", "msg_export_s6e3_success": "Exportação S6E3 concluída com sucesso!", "msg_export_s6e3_failed": "Falha ao exportar no formato S6E3!", "msg_export_csv_success": "Exportação CSV concluída com sucesso!", "msg_export_csv_failed": "Falha ao exportar como CSV!", + "msg_export_csv_skills_success": "Habilidades exportadas como CSV com sucesso!", + "msg_export_csv_skills_failed": "Falha ao exportar habilidades como CSV!", "error_file_not_found": "Arquivo não encontrado: {0}", "error_invalid_index": "Índice inválido: {0}", @@ -29,8 +35,15 @@ "popup_toggle_columns": "Alternar Visibilidade de Colunas:", "popup_save_success": "Itens salvos com sucesso!", "popup_save_failed": "Falha ao salvar itens!", + "popup_save_skills_success": "Habilidades salvas com sucesso!", + "popup_save_skills_failed": "Falha ao salvar habilidades!", "popup_export_csv_success": "Itens exportados como CSV com sucesso!", "popup_export_csv_failed": "Falha ao exportar atributos de itens como CSV!", - "popup_export_s6e3_success": "Itens exportados no formato S6E3 legacy com sucesso!", - "popup_export_s6e3_failed": "Falha ao exportar atributos de itens no formato S6E3!" + "popup_export_csv_skills_success": "Habilidades exportadas como CSV com sucesso!", + "popup_export_csv_skills_failed": "Falha ao exportar atributos de habilidades como CSV!", + "popup_export_s6e3_success": "Exportado como formato legado com sucesso!", + "popup_export_s6e3_failed": "Falha ao exportar como formato legado!", + + "label_skill_editor_title": "Editor de Habilidades", + "label_search_skills": "Pesquisar Habilidades:" } diff --git a/src/bin/Translations/pt/game.json b/src/bin/Translations/pt/game.json index ae2259b2e..bd804650e 100644 --- a/src/bin/Translations/pt/game.json +++ b/src/bin/Translations/pt/game.json @@ -1,4 +1,4 @@ { - "_comment": "Game Content Translations - Portuguese", - "_note": "Currently empty - for future game localization (items, NPCs, quests, etc.)" + "_comment": "Traduções de Conteúdo do Jogo - Português", + "_note": "Atualmente vazio - para futura localização do jogo (itens, NPCs, missões, etc.)" } diff --git a/src/bin/Translations/pt/metadata.json b/src/bin/Translations/pt/metadata.json index b7918b64d..8245a171b 100644 --- a/src/bin/Translations/pt/metadata.json +++ b/src/bin/Translations/pt/metadata.json @@ -1,5 +1,5 @@ { - "_comment": "Field Names and Metadata Translations - Portuguese", + "_comment": "Traduções de Nomes de Campos e Metadados - Português", "field_Name": "Nome", "field_TwoHand": "Duas Mãos", @@ -8,8 +8,8 @@ "field_m_wSkillIndex": "Habilidade", "field_Width": "Largura", "field_Height": "Altura", - "field_DamageMin": "Dano Mín", - "field_DamageMax": "Dano Máx", + "field_DamageMin": "Dano Mínimo", + "field_DamageMax": "Dano Máximo", "field_SuccessfulBlocking": "Taxa de Bloqueio", "field_Defense": "Defesa", "field_MagicDefense": "Defesa Mágica", @@ -43,5 +43,30 @@ "field_Resistance[4]": "Resistência à Terra", "field_Resistance[5]": "Resistência ao Vento", "field_Resistance[6]": "Resistência à Água", - "field_Resistance[7]": "Resistência 7" + "field_Resistance[7]": "Resistência 7", + + "_comment_skills": "Traduções de Campos de Habilidade", + "field_Damage": "Dano", + "field_Mana": "Mana", + "field_AbilityGuage": "Custo de AG", + "field_Distance": "Alcance", + "field_Delay": "Recarga", + "field_Energy": "Req Energia", + "field_Charisma": "Req Liderança", + "field_MasteryType": "Tipo de Maestria", + "field_SkillUseType": "Tipo de Uso", + "field_SkillBrand": "Marca", + "field_KillCount": "Contagem de Mortes", + "field_SkillRank": "Classificação", + "field_Magic_Icon": "Ícone", + "field_TypeSkill": "Tipo", + "field_Strength": "Req Força", + "field_Dexterity": "Req Destreza", + "field_ItemSkill": "Habilidade de Item", + "field_IsDamage": "É Dano", + "field_Effect": "Efeito", + + "field_RequireDutyClass[0]": "Dever 0", + "field_RequireDutyClass[1]": "Dever 1", + "field_RequireDutyClass[2]": "Dever 2" } diff --git a/src/bin/Translations/ru/editor.json b/src/bin/Translations/ru/editor.json new file mode 100644 index 000000000..7d60a1a03 --- /dev/null +++ b/src/bin/Translations/ru/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - Russian", + "language_name": "Русский", + + "btn_save": "Сохранить Предметы", + "btn_save_skills": "Сохранить Навыки", + "btn_export_s6e3": "Экспорт в S6E3", + "btn_export_csv": "Экспорт в CSV", + "btn_export_csv_skills": "Экспорт Навыков в CSV", + "btn_columns": "Столбцы", + "btn_select_all": "Выбрать Все", + "btn_unselect_all": "Снять Выбор", + "btn_ok": "ОК", + + "msg_save_success": "Сохранение завершено успешно!", + "msg_save_failed": "Не удалось сохранить предметы!", + "msg_save_skills_success": "Навыки сохранены успешно!", + "msg_save_skills_failed": "Не удалось сохранить навыки!", + "msg_export_s6e3_success": "Экспорт S6E3 завершён успешно!", + "msg_export_s6e3_failed": "Не удалось экспортировать в формат S6E3!", + "msg_export_csv_success": "Экспорт CSV завершён успешно!", + "msg_export_csv_failed": "Не удалось экспортировать в CSV!", + "msg_export_csv_skills_success": "Навыки экспортированы в CSV успешно!", + "msg_export_csv_skills_failed": "Не удалось экспортировать навыки в CSV!", + + "error_file_not_found": "Файл не найден: {0}", + "error_invalid_index": "Недопустимый индекс: {0}", + "error_index_in_use": "Ошибка: Индекс {0} уже используется!", + + "label_search_text": "Поиск:", + "label_freeze_columns": "Закрепить Индекс/Имя", + "label_no_columns": "Столбцы не выбраны. Нажмите 'Столбцы', чтобы показать столбцы.", + "label_index": "Индекс", + + "popup_toggle_columns": "Переключить Видимость Столбцов:", + "popup_save_success": "Предметы сохранены успешно!", + "popup_save_failed": "Не удалось сохранить предметы!", + "popup_save_skills_success": "Навыки сохранены успешно!", + "popup_save_skills_failed": "Не удалось сохранить навыки!", + "popup_export_csv_success": "Предметы экспортированы в CSV успешно!", + "popup_export_csv_failed": "Не удалось экспортировать атрибуты предметов в CSV!", + "popup_export_csv_skills_success": "Навыки экспортированы в CSV успешно!", + "popup_export_csv_skills_failed": "Не удалось экспортировать атрибуты навыков в CSV!", + "popup_export_s6e3_success": "Успешно экспортировано в устаревшем формате!", + "popup_export_s6e3_failed": "Не удалось экспортировать в устаревшем формате!", + + "label_skill_editor_title": "Редактор Навыков", + "label_search_skills": "Поиск Навыков:" +} diff --git a/src/bin/Translations/ru/game.json b/src/bin/Translations/ru/game.json new file mode 100644 index 000000000..758ab2dc5 --- /dev/null +++ b/src/bin/Translations/ru/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - Russian", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/ru/metadata.json b/src/bin/Translations/ru/metadata.json new file mode 100644 index 000000000..d1f57a089 --- /dev/null +++ b/src/bin/Translations/ru/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - Russian", + + "field_Name": "Название", + "field_TwoHand": "Двуручный", + "field_Level": "Уровень", + "field_m_byItemSlot": "Слот", + "field_m_wSkillIndex": "Навык", + "field_Width": "Ширина", + "field_Height": "Высота", + "field_DamageMin": "Мин Урон", + "field_DamageMax": "Макс Урон", + "field_SuccessfulBlocking": "Шанс Блока", + "field_Defense": "Защита", + "field_MagicDefense": "Магическая Защита", + "field_WeaponSpeed": "Скорость Атаки", + "field_WalkSpeed": "Скорость Движения", + "field_Durability": "Прочность", + "field_MagicDur": "Магическая Прочность", + "field_MagicPower": "Магическая Сила", + "field_RequireStrength": "Треб Сила", + "field_RequireDexterity": "Треб Ловкость", + "field_RequireEnergy": "Треб Энергия", + "field_RequireVitality": "Треб Выносливость", + "field_RequireCharisma": "Треб Лидерство", + "field_RequireLevel": "Треб Уровень", + "field_Value": "Цена Продажи", + "field_iZen": "Цена Покупки", + "field_AttType": "Тип Атаки", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Сопротивление Льду", + "field_Resistance[1]": "Сопротивление Яду", + "field_Resistance[2]": "Сопротивление Молнии", + "field_Resistance[3]": "Сопротивление Огню", + "field_Resistance[4]": "Сопротивление Земле", + "field_Resistance[5]": "Сопротивление Ветру", + "field_Resistance[6]": "Сопротивление Воде", + "field_Resistance[7]": "Сопротивление 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Урон", + "field_Mana": "Мана", + "field_AbilityGuage": "Стоимость AG", + "field_Distance": "Дальность", + "field_Delay": "Перезарядка", + "field_Energy": "Треб Энергия", + "field_Charisma": "Треб Лидерство", + "field_MasteryType": "Тип Мастерства", + "field_SkillUseType": "Тип Использования", + "field_SkillBrand": "Бренд", + "field_KillCount": "Счётчик Убийств", + "field_SkillRank": "Ранг", + "field_Magic_Icon": "Иконка", + "field_TypeSkill": "Тип", + "field_Strength": "Треб Сила", + "field_Dexterity": "Треб Ловкость", + "field_ItemSkill": "Навык Предмета", + "field_IsDamage": "Наносит Урон", + "field_Effect": "Эффект", + + "field_RequireDutyClass[0]": "Долг 0", + "field_RequireDutyClass[1]": "Долг 1", + "field_RequireDutyClass[2]": "Долг 2" +} diff --git a/src/bin/Translations/tl/editor.json b/src/bin/Translations/tl/editor.json new file mode 100644 index 000000000..abe10a12b --- /dev/null +++ b/src/bin/Translations/tl/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - Filipino (Tagalog)", + "language_name": "Tagalog", + + "btn_save": "I-save ang mga Aytem", + "btn_save_skills": "I-save ang mga Kasanayan", + "btn_export_s6e3": "I-export bilang S6E3", + "btn_export_csv": "I-export bilang CSV", + "btn_export_csv_skills": "I-export ang mga Kasanayan bilang CSV", + "btn_columns": "Mga Kolum", + "btn_select_all": "Piliin Lahat", + "btn_unselect_all": "I-unselect Lahat", + "btn_ok": "OK", + + "msg_save_success": "Matagumpay na nai-save!", + "msg_save_failed": "Hindi nai-save ang mga aytem!", + "msg_save_skills_success": "Matagumpay na nai-save ang mga kasanayan!", + "msg_save_skills_failed": "Hindi nai-save ang mga kasanayan!", + "msg_export_s6e3_success": "Matagumpay na nai-export ang S6E3!", + "msg_export_s6e3_failed": "Hindi nai-export sa S6E3 format!", + "msg_export_csv_success": "Matagumpay na nai-export ang CSV!", + "msg_export_csv_failed": "Hindi nai-export sa CSV!", + "msg_export_csv_skills_success": "Matagumpay na nai-export ang mga kasanayan bilang CSV!", + "msg_export_csv_skills_failed": "Hindi nai-export ang mga kasanayan sa CSV!", + + "error_file_not_found": "Hindi nahanap ang file: {0}", + "error_invalid_index": "Di-wastong index: {0}", + "error_index_in_use": "Error: Ginagamit na ang index {0}!", + + "label_search_text": "Maghanap:", + "label_freeze_columns": "I-freeze ang Index/Pangalan", + "label_no_columns": "Walang napiling kolum. I-click ang 'Mga Kolum' para ipakita ang mga kolum.", + "label_index": "Index", + + "popup_toggle_columns": "I-toggle ang Visibility ng Kolum:", + "popup_save_success": "Matagumpay na nai-save ang mga aytem!", + "popup_save_failed": "Hindi nai-save ang mga aytem!", + "popup_save_skills_success": "Matagumpay na nai-save ang mga kasanayan!", + "popup_save_skills_failed": "Hindi nai-save ang mga kasanayan!", + "popup_export_csv_success": "Matagumpay na nai-export ang mga aytem bilang CSV!", + "popup_export_csv_failed": "Hindi nai-export ang mga katangian ng aytem sa CSV!", + "popup_export_csv_skills_success": "Matagumpay na nai-export ang mga kasanayan bilang CSV!", + "popup_export_csv_skills_failed": "Hindi nai-export ang mga katangian ng kasanayan sa CSV!", + "popup_export_s6e3_success": "Matagumpay na na-export bilang legacy format!", + "popup_export_s6e3_failed": "Nabigo ang pag-export bilang legacy format!", + + "label_skill_editor_title": "Editor ng Kasanayan", + "label_search_skills": "Maghanap ng Kasanayan:" +} diff --git a/src/bin/Translations/tl/game.json b/src/bin/Translations/tl/game.json new file mode 100644 index 000000000..721c1f900 --- /dev/null +++ b/src/bin/Translations/tl/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - Filipino (Tagalog)", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/tl/metadata.json b/src/bin/Translations/tl/metadata.json new file mode 100644 index 000000000..ee0d01c1e --- /dev/null +++ b/src/bin/Translations/tl/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - Filipino (Tagalog)", + + "field_Name": "Pangalan", + "field_TwoHand": "Dalawang Kamay", + "field_Level": "Antas", + "field_m_byItemSlot": "Slot", + "field_m_wSkillIndex": "Kasanayan", + "field_Width": "Lapad", + "field_Height": "Taas", + "field_DamageMin": "Min na Pinsala", + "field_DamageMax": "Max na Pinsala", + "field_SuccessfulBlocking": "Rate ng Block", + "field_Defense": "Depensa", + "field_MagicDefense": "Magic na Depensa", + "field_WeaponSpeed": "Bilis ng Atake", + "field_WalkSpeed": "Bilis ng Galaw", + "field_Durability": "Tibay", + "field_MagicDur": "Magic na Tibay", + "field_MagicPower": "Kapangyarihan ng Magic", + "field_RequireStrength": "Kailangan na Lakas", + "field_RequireDexterity": "Kailangan na Liksi", + "field_RequireEnergy": "Kailangan na Enerhiya", + "field_RequireVitality": "Kailangan na Buhay", + "field_RequireCharisma": "Kailangan na Pamumuno", + "field_RequireLevel": "Kailangan na Antas", + "field_Value": "Halaga ng Benta", + "field_iZen": "Presyo ng Bili", + "field_AttType": "Uri ng Atake", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Resistensya sa Yelo", + "field_Resistance[1]": "Resistensya sa Lason", + "field_Resistance[2]": "Resistensya sa Kidlat", + "field_Resistance[3]": "Resistensya sa Apoy", + "field_Resistance[4]": "Resistensya sa Lupa", + "field_Resistance[5]": "Resistensya sa Hangin", + "field_Resistance[6]": "Resistensya sa Tubig", + "field_Resistance[7]": "Resistensya 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Pinsala", + "field_Mana": "Mana", + "field_AbilityGuage": "Gastos ng AG", + "field_Distance": "Saklaw", + "field_Delay": "Cooldown", + "field_Energy": "Kailangan na Enerhiya", + "field_Charisma": "Kailangan na Pamumuno", + "field_MasteryType": "Uri ng Mastery", + "field_SkillUseType": "Uri ng Paggamit", + "field_SkillBrand": "Tatak", + "field_KillCount": "Bilang ng Patay", + "field_SkillRank": "Ranggo", + "field_Magic_Icon": "Icon", + "field_TypeSkill": "Uri", + "field_Strength": "Kailangan na Lakas", + "field_Dexterity": "Kailangan na Liksi", + "field_ItemSkill": "Kasanayan ng Aytem", + "field_IsDamage": "May Pinsala", + "field_Effect": "Epekto", + + "field_RequireDutyClass[0]": "Tungkulin 0", + "field_RequireDutyClass[1]": "Tungkulin 1", + "field_RequireDutyClass[2]": "Tungkulin 2" +} diff --git a/src/bin/Translations/uk/editor.json b/src/bin/Translations/uk/editor.json new file mode 100644 index 000000000..389ab6022 --- /dev/null +++ b/src/bin/Translations/uk/editor.json @@ -0,0 +1,49 @@ +{ + "_comment": "MuEditor UI Translations - Ukrainian", + "language_name": "Українська", + + "btn_save": "Зберегти Предмети", + "btn_save_skills": "Зберегти Навички", + "btn_export_s6e3": "Експорт у S6E3", + "btn_export_csv": "Експорт у CSV", + "btn_export_csv_skills": "Експорт Навичок у CSV", + "btn_columns": "Стовпці", + "btn_select_all": "Вибрати Все", + "btn_unselect_all": "Зняти Вибір", + "btn_ok": "ОК", + + "msg_save_success": "Збереження завершено успішно!", + "msg_save_failed": "Не вдалося зберегти предмети!", + "msg_save_skills_success": "Навички збережено успішно!", + "msg_save_skills_failed": "Не вдалося зберегти навички!", + "msg_export_s6e3_success": "Експорт S6E3 завершено успішно!", + "msg_export_s6e3_failed": "Не вдалося експортувати у формат S6E3!", + "msg_export_csv_success": "Експорт CSV завершено успішно!", + "msg_export_csv_failed": "Не вдалося експортувати у CSV!", + "msg_export_csv_skills_success": "Навички експортовано у CSV успішно!", + "msg_export_csv_skills_failed": "Не вдалося експортувати навички у CSV!", + + "error_file_not_found": "Файл не знайдено: {0}", + "error_invalid_index": "Недопустимий індекс: {0}", + "error_index_in_use": "Помилка: Індекс {0} вже використовується!", + + "label_search_text": "Пошук:", + "label_freeze_columns": "Закріпити Індекс/Ім'я", + "label_no_columns": "Стовпці не вибрано. Натисніть 'Стовпці', щоб показати стовпці.", + "label_index": "Індекс", + + "popup_toggle_columns": "Перемикач Видимості Стовпців:", + "popup_save_success": "Предмети збережено успішно!", + "popup_save_failed": "Не вдалося зберегти предмети!", + "popup_save_skills_success": "Навички збережено успішно!", + "popup_save_skills_failed": "Не вдалося зберегти навички!", + "popup_export_csv_success": "Предмети експортовано у CSV успішно!", + "popup_export_csv_failed": "Не вдалося експортувати атрибути предметів у CSV!", + "popup_export_csv_skills_success": "Навички експортовано у CSV успішно!", + "popup_export_csv_skills_failed": "Не вдалося експортувати атрибути навичок у CSV!", + "popup_export_s6e3_success": "Успішно експортовано у застарілому форматі!", + "popup_export_s6e3_failed": "Не вдалося експортувати у застарілому форматі!", + + "label_skill_editor_title": "Редактор Навичок", + "label_search_skills": "Пошук Навичок:" +} diff --git a/src/bin/Translations/uk/game.json b/src/bin/Translations/uk/game.json new file mode 100644 index 000000000..b822198af --- /dev/null +++ b/src/bin/Translations/uk/game.json @@ -0,0 +1,4 @@ +{ + "_comment": "Game Translations - Ukrainian", + "_placeholder": "Game translations to be added" +} diff --git a/src/bin/Translations/uk/metadata.json b/src/bin/Translations/uk/metadata.json new file mode 100644 index 000000000..f90e96245 --- /dev/null +++ b/src/bin/Translations/uk/metadata.json @@ -0,0 +1,72 @@ +{ + "_comment": "Field Names and Metadata Translations - Ukrainian", + + "field_Name": "Назва", + "field_TwoHand": "Дворучний", + "field_Level": "Рівень", + "field_m_byItemSlot": "Слот", + "field_m_wSkillIndex": "Навичка", + "field_Width": "Ширина", + "field_Height": "Висота", + "field_DamageMin": "Мін Урон", + "field_DamageMax": "Макс Урон", + "field_SuccessfulBlocking": "Шанс Блоку", + "field_Defense": "Захист", + "field_MagicDefense": "Магічний Захист", + "field_WeaponSpeed": "Швидкість Атаки", + "field_WalkSpeed": "Швидкість Руху", + "field_Durability": "Міцність", + "field_MagicDur": "Магічна Міцність", + "field_MagicPower": "Магічна Сила", + "field_RequireStrength": "Потр Сила", + "field_RequireDexterity": "Потр Спритність", + "field_RequireEnergy": "Потр Енергія", + "field_RequireVitality": "Потр Витривалість", + "field_RequireCharisma": "Потр Лідерство", + "field_RequireLevel": "Потр Рівень", + "field_Value": "Ціна Продажу", + "field_iZen": "Ціна Купівлі", + "field_AttType": "Тип Атаки", + + "field_RequireClass[0]": "DW/SM", + "field_RequireClass[1]": "DK/BK", + "field_RequireClass[2]": "ELF/ME", + "field_RequireClass[3]": "MG/DM", + "field_RequireClass[4]": "DL/LE", + "field_RequireClass[5]": "SUM/BS", + "field_RequireClass[6]": "RF/FM", + + "field_Resistance[0]": "Опір Льоду", + "field_Resistance[1]": "Опір Отруті", + "field_Resistance[2]": "Опір Блискавці", + "field_Resistance[3]": "Опір Вогню", + "field_Resistance[4]": "Опір Землі", + "field_Resistance[5]": "Опір Вітру", + "field_Resistance[6]": "Опір Воді", + "field_Resistance[7]": "Опір 7", + + "_comment_skills": "Skill Field Translations", + "field_Damage": "Урон", + "field_Mana": "Мана", + "field_AbilityGuage": "Вартість AG", + "field_Distance": "Дальність", + "field_Delay": "Перезарядка", + "field_Energy": "Потр Енергія", + "field_Charisma": "Потр Лідерство", + "field_MasteryType": "Тип Майстерності", + "field_SkillUseType": "Тип Використання", + "field_SkillBrand": "Бренд", + "field_KillCount": "Лічильник Вбивств", + "field_SkillRank": "Ранг", + "field_Magic_Icon": "Іконка", + "field_TypeSkill": "Тип", + "field_Strength": "Потр Сила", + "field_Dexterity": "Потр Спритність", + "field_ItemSkill": "Навичка Предмета", + "field_IsDamage": "Завдає Урон", + "field_Effect": "Ефект", + + "field_RequireDutyClass[0]": "Обов'язок 0", + "field_RequireDutyClass[1]": "Обов'язок 1", + "field_RequireDutyClass[2]": "Обов'язок 2" +} diff --git a/src/source/DataHandler/CommonDataSaver.cpp b/src/source/DataHandler/CommonDataSaver.cpp new file mode 100644 index 000000000..69d5b27b3 --- /dev/null +++ b/src/source/DataHandler/CommonDataSaver.cpp @@ -0,0 +1,294 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "CommonDataSaver.h" +#include + +#include "UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" + +void CommonDataSaver::SplitPath(const wchar_t* fileName, std::wstring& outDir, std::wstring& outBaseName) +{ + outDir.clear(); + outBaseName.clear(); + + if (!fileName) + return; + + std::wstring full(fileName); + const size_t pos = full.find_last_of(L"\\/"); + + if (pos == std::wstring::npos) + { + outBaseName = full; + return; + } + + outDir = full.substr(0, pos + 1); + outBaseName = full.substr(pos + 1); +} + +// --- helper: does this file name look like a backup of `fileName`? +bool CommonDataSaver::MatchesBackupPattern(const wchar_t* fileName, const WIN32_FIND_DATAW& fd) +{ + // Expected (current theme): + // "_N_YyyyyMmmDddhHHmMMsSS.bak" + // NOTE: fd.cFileName is just the file name (no directory) + + if (!fileName || !fd.cFileName[0]) + return false; + + std::wstring dir, base; + SplitPath(fileName, dir, base); + + const std::wstring candidate = fd.cFileName; + + const std::wstring prefix = base + L"_N"; + if (candidate.rfind(prefix, 0) != 0) + return false; + + // Skip temp backups (whatever temp naming you use) + if (candidate.find(L"_TMP_") != std::wstring::npos || candidate.find(L"_TMP") != std::wstring::npos) + return false; + + // Must end with ".bak" + const std::wstring suffix = L".bak"; + if (candidate.size() < suffix.size() || candidate.compare(candidate.size() - suffix.size(), suffix.size(), suffix) != 0) + return false; + + // Must have "_Y" somewhere after the number (helps avoid matching other random *.bak) + if (candidate.find(L"_Y") == std::wstring::npos) + return false; + + return true; +} + +// --- helper: build backup name with number + local date/time +bool CommonDataSaver::BuildBackupName(const wchar_t* fileName, int backupNumber, wchar_t* outBackupName, size_t outBackupNameChars) +{ + if (!fileName || !outBackupName || outBackupNameChars == 0) + return false; + + std::wstring dir, base; + SplitPath(fileName, dir, base); + + SYSTEMTIME st; + GetLocalTime(&st); + + // Example: + // "_N02_Y2026M01D25h00m45s03.bak" + int written = _snwprintf_s( + outBackupName, + outBackupNameChars, + _TRUNCATE, + L"%ls%ls_N%02d_Y%04dM%02dD%02dh%02dm%02ds%02d.bak", + dir.c_str(), + base.c_str(), + backupNumber, + st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond + ); + + return written > 0; +} + +bool CommonDataSaver::BuildTempBackupName(const wchar_t* fileName, wchar_t* outTempBackupName, size_t outTempBackupNameChars) +{ + if (!fileName || !outTempBackupName || outTempBackupNameChars == 0) + return false; + + std::wstring dir, base; + SplitPath(fileName, dir, base); + + SYSTEMTIME st; + GetLocalTime(&st); + + DWORD pid = GetCurrentProcessId(); + DWORD tid = GetCurrentThreadId(); + + // Example: + // "_TMP_Y2026_M01_D25_h00_m45_s03_1234_56.bak" + int written = _snwprintf_s( + outTempBackupName, + outTempBackupNameChars, + _TRUNCATE, + L"%ls%ls_TMP_Y%04d_M%02d_D%02d_h%02d_m%02d_s%02d_%lu_%lu.bak", + dir.c_str(), + base.c_str(), + st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond, + (unsigned long)pid, (unsigned long)tid + ); + + return written > 0; +} + +void CommonDataSaver::EnsureMaxBackupsAfterSuccess(const wchar_t* fileName) +{ + std::wstring dir, base; + SplitPath(fileName, dir, base); + + const std::wstring pattern = dir + base + L"_N*.bak"; + + struct BackupEntry + { + std::wstring path; // full path for DeleteFileW + FILETIME lastWrite{}; + }; + + std::vector backups; + + WIN32_FIND_DATAW fd{}; + HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd); + if (hFind == INVALID_HANDLE_VALUE) + return; + + do + { + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + if (!MatchesBackupPattern(fileName, fd)) + continue; + + // Build full path for deletion + backups.push_back(BackupEntry{ dir + fd.cFileName, fd.ftLastWriteTime }); + + } while (FindNextFileW(hFind, &fd)); + + FindClose(hFind); + + // We are called AFTER temp backup creation but BEFORE publishing final backup. + // Keep room for the new final one: <= MaxBackups - 1. + while ((int)backups.size() > MaxBackups - 1) + { + auto oldestIt = std::min_element(backups.begin(), backups.end(), + [](const BackupEntry& a, const BackupEntry& b) + { + return CompareFileTime(&a.lastWrite, &b.lastWrite) < 0; + }); + + if (oldestIt == backups.end()) + break; + + if (!DeleteFileW(oldestIt->path.c_str())) + { + // Best-effort pruning; if deletion fails, stop to avoid infinite loop. + // (Optional: log warning here) + break; + } + + backups.erase(oldestIt); + } +} + +// --- helper: choose next number (1..10...) +int CommonDataSaver::GetNextBackupNumber(const wchar_t* fileName) +{ + std::wstring dir, base; + SplitPath(fileName, dir, base); + + // Search in directory; fd.cFileName will be base-name only. + const std::wstring pattern = dir + base + L"_N*.bak"; + + WIN32_FIND_DATAW fd{}; + HANDLE hFind = FindFirstFileW(pattern.c_str(), &fd); + if (hFind == INVALID_HANDLE_VALUE) + return 1; + + long long maxNum = 0; + + do + { + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + if (!MatchesBackupPattern(fileName, fd)) + continue; + + // Parse digits after "_N" until first non-digit + const std::wstring candidate = fd.cFileName; + const std::wstring prefix = base + L"_N"; + + size_t i = prefix.size(); + long long n = 0; + bool hasDigits = false; + + while (i < candidate.size()) + { + const wchar_t ch = candidate[i]; + if (ch < L'0' || ch > L'9') + break; + + hasDigits = true; + n = (n * 10) + (ch - L'0'); + ++i; + } + + if (hasDigits && n > maxNum) + maxNum = n; + + } while (FindNextFileW(hFind, &fd)); + + FindClose(hFind); + + // Monotonic / unbounded numbering: + // next = max + 1 (so after deleting N01, if N10 exists -> next is N11) + if (maxNum < 0) + maxNum = 0; + + // Clamp to int range if you want; otherwise keep long long internally. + if (maxNum >= (long long)INT_MAX) + return INT_MAX; + + return (int)(maxNum + 1); +} + +bool CommonDataSaver::CreateBackup(const wchar_t* fileName) +{ + if (!fileName || !fileName[0]) + return false; + + // If original file doesn't exist, nothing to back up + DWORD attr = GetFileAttributesW(fileName); + if (attr == INVALID_FILE_ATTRIBUTES || (attr & FILE_ATTRIBUTE_DIRECTORY)) + return true; + + // 1) Create TEMP backup first (no deletion yet) + wchar_t tempBackupName[1024]{}; + if (!BuildTempBackupName(fileName, tempBackupName, _countof(tempBackupName))) + return false; + + if (!CopyFileW(fileName, tempBackupName, FALSE)) + { + g_MuEditorConsoleUI.LogEditor("Warning: Could not create backup file"); + return false; + } + + // 2) Now that we have a successful new backup, prune old ones + EnsureMaxBackupsAfterSuccess(fileName); + + // 3) Compute final name (number + date) and "publish" by rename + const int backupNumber = GetNextBackupNumber(fileName); + + wchar_t finalBackupName[1024]{}; + if (!BuildBackupName(fileName, backupNumber, finalBackupName, _countof(finalBackupName))) + { + DeleteFileW(tempBackupName); // don’t leave junk behind + return false; + } + + if (!MoveFileExW(tempBackupName, finalBackupName, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) + { + // If rename failed, keep the temp backup rather than deleting older backups again. + // But we already pruned, so log loudly. + g_MuEditorConsoleUI.LogEditor("Warning: Backup created but could not be finalized (rename failed)"); + return false; + } + + g_MuEditorConsoleUI.LogEditor("Backup created " + StringUtils::WideToNarrow(finalBackupName)); + return true; +} + +#endif // _EDITOR diff --git a/src/source/DataHandler/CommonDataSaver.h b/src/source/DataHandler/CommonDataSaver.h new file mode 100644 index 000000000..07cecdfeb --- /dev/null +++ b/src/source/DataHandler/CommonDataSaver.h @@ -0,0 +1,30 @@ +#pragma once + +#ifdef _EDITOR + +#include + +// Item Data Saving Operations +class CommonDataSaver +{ +public: + // static bool Save(wchar_t* fileName, std::string* outChangeLog = nullptr); + + // Create a backup of the file before saving + static bool CreateBackup(const wchar_t* fileName); + +private: + static constexpr int MaxBackups = 10; + + static void SplitPath(const wchar_t* fileName, std::wstring& outDir, std::wstring& outBaseName); + + static bool BuildBackupName(const wchar_t* fileName, int backupNumber, wchar_t* outBackupName, size_t outBackupNameChars); + static bool BuildTempBackupName(const wchar_t* fileName, wchar_t* outTempBackupName, size_t outTempBackupNameChars); + + static void EnsureMaxBackupsAfterSuccess(const wchar_t* fileName); // prune only after temp is created + static int GetNextBackupNumber(const wchar_t* fileName); + + static bool MatchesBackupPattern(const wchar_t* fileName, const WIN32_FIND_DATAW& fd); +}; + +#endif // _EDITOR diff --git a/src/source/DataHandler/ItemData/ItemDataExporter.cpp b/src/source/DataHandler/ItemData/ItemDataExporter.cpp index d0781759a..fdd1f1984 100644 --- a/src/source/DataHandler/ItemData/ItemDataExporter.cpp +++ b/src/source/DataHandler/ItemData/ItemDataExporter.cpp @@ -7,6 +7,7 @@ #include "_define.h" #include "ZzzInfomation.h" #include "GameData/ItemData/ItemFieldDefs.h" +#include "Utilities/StringUtils.h" #include // External references @@ -48,12 +49,11 @@ bool ItemDataExporter::ExportToCsv(wchar_t* fileName) if (ItemAttribute[i].Name[0] != 0) { // Convert wide char name to UTF-8 - char utf8Name[MAX_ITEM_NAME * 3] = {0}; - WideCharToMultiByte(CP_UTF8, 0, ItemAttribute[i].Name, -1, utf8Name, sizeof(utf8Name), NULL, NULL); + std::string utf8Name = StringUtils::WideToNarrow(ItemAttribute[i].Name); - // Escape quotes in name using std::string + // Escape quotes in name std::string escapedName; - for (const char* p = utf8Name; *p != '\0'; ++p) + for (const char* p = utf8Name.c_str(); *p != '\0'; ++p) { if (*p == '"') { diff --git a/src/source/DataHandler/ItemData/ItemDataHandler.cpp b/src/source/DataHandler/ItemData/ItemDataHandler.cpp index 1f8c753e0..83cba12a0 100644 --- a/src/source/DataHandler/ItemData/ItemDataHandler.cpp +++ b/src/source/DataHandler/ItemData/ItemDataHandler.cpp @@ -52,7 +52,7 @@ bool CItemDataHandler::Save(wchar_t* fileName, std::string* outChangeLog) return ItemDataSaver::Save(fileName, outChangeLog); } -bool CItemDataHandler::SaveLegacy(wchar_t* fileName) +bool CItemDataHandler::ExportAsS6E3(wchar_t* fileName) { return ItemDataSaverLegacy::SaveLegacy(fileName); } diff --git a/src/source/DataHandler/ItemData/ItemDataHandler.h b/src/source/DataHandler/ItemData/ItemDataHandler.h index 524c919bc..2952f5693 100644 --- a/src/source/DataHandler/ItemData/ItemDataHandler.h +++ b/src/source/DataHandler/ItemData/ItemDataHandler.h @@ -12,7 +12,7 @@ class CItemDataHandler #ifdef _EDITOR bool Save(wchar_t* fileName, std::string* outChangeLog = nullptr); - bool SaveLegacy(wchar_t* fileName); + bool ExportAsS6E3(wchar_t* fileName); bool ExportToCsv(wchar_t* fileName); #endif diff --git a/src/source/DataHandler/ItemData/ItemDataLoader.cpp b/src/source/DataHandler/ItemData/ItemDataLoader.cpp index b55c5f02e..80751fb80 100644 --- a/src/source/DataHandler/ItemData/ItemDataLoader.cpp +++ b/src/source/DataHandler/ItemData/ItemDataLoader.cpp @@ -10,6 +10,11 @@ #include "CSChaosCastle.h" #include +#ifdef _EDITOR +#include "UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" +#endif + // External references extern ITEM_ATTRIBUTE* ItemAttribute; @@ -37,6 +42,13 @@ bool ItemDataLoader::Load(wchar_t* fileName) bool isLegacyFormat = (fileSize == expectedLegacySize); bool success = false; +#ifdef _EDITOR + if (isLegacyFormat) + { + g_MuEditorConsoleUI.LogEditor("Detected legacy item format (30-byte names)"); + } +#endif + if (isLegacyFormat) { success = LoadLegacyFormat(fp, fileSize); @@ -47,6 +59,26 @@ bool ItemDataLoader::Load(wchar_t* fileName) } fclose(fp); + +#ifdef _EDITOR + if (success) + { + // Count non-empty items (items with names) + int itemCount = 0; + for (int i = 0; i < MAX_ITEM; i++) + { + if (ItemAttribute[i].Name[0] != L'\0') + { + itemCount++; + } + } + + wchar_t successMsg[256]; + swprintf(successMsg, L"Loaded %d items from %ls", itemCount, fileName); + g_MuEditorConsoleUI.LogEditor(StringUtils::WideToNarrow(successMsg)); + } +#endif + return success; } diff --git a/src/source/DataHandler/ItemData/ItemDataSaver.cpp b/src/source/DataHandler/ItemData/ItemDataSaver.cpp index 8911b3e96..312d2af9d 100644 --- a/src/source/DataHandler/ItemData/ItemDataSaver.cpp +++ b/src/source/DataHandler/ItemData/ItemDataSaver.cpp @@ -15,6 +15,10 @@ #include #include +#include "DataHandler/CommonDataSaver.h" +#include "UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" + // External references extern ITEM_ATTRIBUTE* ItemAttribute; @@ -174,6 +178,9 @@ bool ItemDataSaver::Save(wchar_t* fileName, std::string* outChangeLog) return false; // Return false to indicate no save was needed } + // Create backup + CommonDataSaver::CreateBackup(fileName); + // Open file for writing only after we know we need to save FILE* fp = _wfopen(fileName, L"wb"); if (fp == NULL) diff --git a/src/source/DataHandler/SkillData/SkillDataExporter.cpp b/src/source/DataHandler/SkillData/SkillDataExporter.cpp new file mode 100644 index 000000000..ddd5f1763 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataExporter.cpp @@ -0,0 +1,95 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillDataExporter.h" +#include "GameData/SkillData/SkillStructs.h" +#include "GameData/SkillData/SkillFieldDefs.h" +#include "_struct.h" +#include "_define.h" +#include "ZzzInfomation.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" +#include + +// External references +extern SKILL_ATTRIBUTE* SkillAttribute; + +// X-Macro helpers for CSV generation +#define CSV_HEADER_SIMPLE(name, type, arraySize, width) "," #name +#define CSV_HEADER_ARRAY(nameWithIndex, baseName, index, type, width) "," #nameWithIndex + +// Complete CSV header with Index and Name +#define CSV_FULL_HEADER "Index,Name" SKILL_FIELDS_SIMPLE(CSV_HEADER_SIMPLE) SKILL_FIELDS_ARRAYS(CSV_HEADER_ARRAY) SKILL_FIELDS_AFTER_ARRAYS(CSV_HEADER_SIMPLE) + +// Helper to print field values +#define PRINT_FIELD_Byte(skill, name) fprintf(csvFp, ",%d", (skill).name) +#define PRINT_FIELD_Word(skill, name) fprintf(csvFp, ",%d", (skill).name) +#define PRINT_FIELD_Int(skill, name) fprintf(csvFp, ",%d", (skill).name) +#define PRINT_FIELD_DWord(skill, name) fprintf(csvFp, ",%u", (skill).name) + +#define CSV_PRINT_SIMPLE(name, type, arraySize, width) PRINT_FIELD_##type(skill, name); +#define CSV_PRINT_ARRAY(nameWithIndex, baseName, index, type, width) fprintf(csvFp, ",%d", skill.baseName[index]); + +bool SkillDataExporter::ExportToCsv(wchar_t* fileName) +{ + FILE* csvFp = _wfopen(fileName, L"w"); + if (!csvFp) + { + g_MuEditorConsoleUI.LogEditor("Failed to create CSV file for export"); + return false; + } + + // Write BOM for UTF-8 + fprintf(csvFp, "\xEF\xBB\xBF"); + + // Write CSV header using X-macros + fprintf(csvFp, CSV_FULL_HEADER "\n"); + + int exportedCount = 0; + for (int i = 0; i < MAX_SKILLS; i++) + { + // Only export skills with names + if (SkillAttribute[i].Name[0] != 0) + { + // Convert wide char name to UTF-8 using StringUtils + std::string utf8Name = StringUtils::WideToNarrow(SkillAttribute[i].Name); + + // Escape quotes in name + std::string escapedName; + escapedName.reserve(utf8Name.length() * 2); // Reserve space for worst case + for (char c : utf8Name) + { + if (c == '"') + { + escapedName += "\"\""; + } + else + { + escapedName += c; + } + } + + // Print index and name + fprintf(csvFp, "%d,\"%s\"", i, escapedName.c_str()); + + // Print all fields using X-macros + SKILL_ATTRIBUTE& skill = SkillAttribute[i]; + SKILL_FIELDS_SIMPLE(CSV_PRINT_SIMPLE) + SKILL_FIELDS_ARRAYS(CSV_PRINT_ARRAY) + SKILL_FIELDS_AFTER_ARRAYS(CSV_PRINT_SIMPLE) + + fprintf(csvFp, "\n"); + exportedCount++; + } + } + + fclose(csvFp); + + std::string successMsg = "Successfully exported " + std::to_string(exportedCount) + " skills to CSV"; + g_MuEditorConsoleUI.LogEditor(successMsg); + + return true; +} + +#endif // _EDITOR diff --git a/src/source/DataHandler/SkillData/SkillDataExporter.h b/src/source/DataHandler/SkillData/SkillDataExporter.h new file mode 100644 index 000000000..ff56220d0 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataExporter.h @@ -0,0 +1,15 @@ +#pragma once + +#ifdef _EDITOR + +// Skill Data Export Operations +// Exports skill data to CSV format for external editing +class SkillDataExporter +{ +public: + // Export all skills to CSV file + // Returns true on success, false on failure + static bool ExportToCsv(wchar_t* fileName); +}; + +#endif // _EDITOR diff --git a/src/source/DataHandler/SkillData/SkillDataHandler.cpp b/src/source/DataHandler/SkillData/SkillDataHandler.cpp new file mode 100644 index 000000000..5581ecdcc --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataHandler.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" +#include "SkillDataHandler.h" + +#include "SkillDataLoader.h" +#include "SkillDataSaver.h" +#include "SkillDataSaverLegacy.h" +#include "GameData/SkillData/SkillStructs.h" +#include "_struct.h" +#include "_define.h" +#include "ZzzInfomation.h" + +#ifdef _EDITOR +#include "SkillDataExporter.h" +#endif + +// External references +extern SKILL_ATTRIBUTE* SkillAttribute; + +CSkillDataHandler::CSkillDataHandler() +{ +} + +CSkillDataHandler& CSkillDataHandler::GetInstance() +{ + static CSkillDataHandler instance; + return instance; +} + +SKILL_ATTRIBUTE* CSkillDataHandler::GetSkillAttributes() +{ + return SkillAttribute; +} + +SKILL_ATTRIBUTE* CSkillDataHandler::GetSkillAttribute(int index) +{ + if (index >= 0 && index < MAX_SKILLS) + return &SkillAttribute[index]; + return nullptr; +} + +int CSkillDataHandler::GetSkillCount() const +{ + return MAX_SKILLS; +} + +bool CSkillDataHandler::Load(wchar_t* fileName) +{ + return SkillDataLoader::Load(fileName); +} + +#ifdef _EDITOR +bool CSkillDataHandler::Save(wchar_t* fileName, std::string* outChangeLog) +{ + return SkillDataSaver::Save(fileName, outChangeLog); +} + +bool CSkillDataHandler::ExportAsS6E3(wchar_t* fileName) +{ + return SkillDataSaverLegacy::SaveLegacy(fileName); +} + +bool CSkillDataHandler::ExportToCsv(wchar_t* fileName) +{ + return SkillDataExporter::ExportToCsv(fileName); +} +#endif diff --git a/src/source/DataHandler/SkillData/SkillDataHandler.h b/src/source/DataHandler/SkillData/SkillDataHandler.h new file mode 100644 index 000000000..4b77b4372 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataHandler.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include "GameData/SkillData/SkillStructs.h" + +// Skill Data Handler - Singleton Facade +// Provides centralized access to skill data operations +class CSkillDataHandler +{ +public: + static CSkillDataHandler& GetInstance(); + + // Data Operations - delegates to specialized classes + bool Load(wchar_t* fileName); + +#ifdef _EDITOR + bool Save(wchar_t* fileName, std::string* outChangeLog = nullptr); + bool ExportAsS6E3(wchar_t* fileName); + bool ExportToCsv(wchar_t* fileName); +#endif + + // Data Access + SKILL_ATTRIBUTE* GetSkillAttributes(); + SKILL_ATTRIBUTE* GetSkillAttribute(int index); + int GetSkillCount() const; + +private: + CSkillDataHandler(); + ~CSkillDataHandler() = default; + + // Prevent copying + CSkillDataHandler(const CSkillDataHandler&) = delete; + CSkillDataHandler& operator=(const CSkillDataHandler&) = delete; +}; + +#define g_SkillDataHandler CSkillDataHandler::GetInstance() diff --git a/src/source/DataHandler/SkillData/SkillDataLoader.cpp b/src/source/DataHandler/SkillData/SkillDataLoader.cpp new file mode 100644 index 000000000..b8f184df8 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataLoader.cpp @@ -0,0 +1,136 @@ +#include "stdafx.h" + +#include "DataHandler/SkillData/SkillDataLoader.h" +#include "GameData/SkillData/SkillStructs.h" +#include "_struct.h" +#include "_define.h" +#include "ZzzInfomation.h" +#include "MultiLanguage.h" + +#ifdef _EDITOR +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" +#include +#endif + +// External references +extern SKILL_ATTRIBUTE* SkillAttribute; +extern HWND g_hWnd; + +bool SkillDataLoader::Load(wchar_t* fileName) +{ + FILE* fp = _wfopen(fileName, L"rb"); + if (fp == NULL) + { + wchar_t errorMsg[256]; + swprintf(errorMsg, L"Skill file not found: %ls", fileName); +#ifdef _EDITOR + g_MuEditorConsoleUI.LogEditor(StringUtils::WideToNarrow(errorMsg)); +#else + MessageBox(g_hWnd, errorMsg, NULL, MB_OK); +#endif + return false; + } + + // Get file size to determine structure version + fseek(fp, 0, SEEK_END); + long fileSize = ftell(fp); + fseek(fp, 0, SEEK_SET); + + const int LegacySize = sizeof(SKILL_ATTRIBUTE_FILE_LEGACY); + const int NewSize = sizeof(SKILL_ATTRIBUTE_FILE); + const long expectedLegacySize = LegacySize * MAX_SKILLS + sizeof(DWORD); + const long expectedNewSize = NewSize * MAX_SKILLS + sizeof(DWORD); + + bool isLegacyFormat = (fileSize == expectedLegacySize); + int readSize = isLegacyFormat ? LegacySize : NewSize; + + // Validate file size + if (fileSize != expectedLegacySize && fileSize != expectedNewSize) + { + fclose(fp); + wchar_t errorMsg[256]; + swprintf(errorMsg, L"Skill file size mismatch. Expected: %ld (new) or %ld (legacy), Got: %ld", + expectedNewSize, expectedLegacySize, fileSize); +#ifdef _EDITOR + g_MuEditorConsoleUI.LogEditor(StringUtils::WideToNarrow(errorMsg)); +#else + MessageBox(g_hWnd, errorMsg, NULL, MB_OK); +#endif + return false; + } + +#ifdef _EDITOR + if (isLegacyFormat) + { + g_MuEditorConsoleUI.LogEditor("Detected legacy skill format (32-byte names)"); + } +#endif + + // Read file data + BYTE* Buffer = new BYTE[readSize * MAX_SKILLS]; + fread(Buffer, readSize * MAX_SKILLS, 1, fp); + + // Read checksum + DWORD dwCheckSum; + fread(&dwCheckSum, sizeof(DWORD), 1, fp); + fclose(fp); + + // Verify checksum + DWORD calculatedChecksum = GenerateCheckSum2(Buffer, readSize * MAX_SKILLS, 0x5A18); + if (dwCheckSum != calculatedChecksum) + { + delete[] Buffer; + wchar_t errorMsg[256]; + swprintf(errorMsg, L"Skill file corrupted: %ls (checksum mismatch)", fileName); +#ifdef _EDITOR + g_MuEditorConsoleUI.LogEditor(StringUtils::WideToNarrow(errorMsg)); +#else + MessageBox(g_hWnd, errorMsg, NULL, MB_OK); + SendMessage(g_hWnd, WM_DESTROY, 0, 0); +#endif + return false; + } + + // Decrypt and parse data + BYTE* pSeek = Buffer; + for (int i = 0; i < MAX_SKILLS; i++) + { + BuxConvert(pSeek, readSize); + + if (isLegacyFormat) + { + SKILL_ATTRIBUTE_FILE_LEGACY source; + memcpy(&source, pSeek, LegacySize); + CopySkillAttributeFromSource(SkillAttribute[i], source); + } + else + { + SKILL_ATTRIBUTE_FILE source; + memcpy(&source, pSeek, NewSize); + CopySkillAttributeFromSource(SkillAttribute[i], source); + } + + pSeek += readSize; + } + + delete[] Buffer; + +#ifdef _EDITOR + // Count non-empty skills (skills with names) + int skillCount = 0; + for (int i = 0; i < MAX_SKILLS; i++) + { + if (SkillAttribute[i].Name[0] != L'\0') + { + skillCount++; + } + } + + wchar_t successMsg[256]; + swprintf(successMsg, L"Loaded %d skills from %ls", skillCount, fileName); + g_MuEditorConsoleUI.LogEditor(StringUtils::WideToNarrow(successMsg)); +#endif + + return true; +} diff --git a/src/source/DataHandler/SkillData/SkillDataLoader.h b/src/source/DataHandler/SkillData/SkillDataLoader.h new file mode 100644 index 000000000..aea3bd470 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataLoader.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// Skill Data Loading Operations +// Loads skill data from encrypted BMD files with checksum verification +// This is the single source of truth for skill data loading (used in ALL builds) +class SkillDataLoader +{ +public: + // Load skill data from file + // Returns true on success, false on failure + static bool Load(wchar_t* fileName); +}; diff --git a/src/source/DataHandler/SkillData/SkillDataSaver.cpp b/src/source/DataHandler/SkillData/SkillDataSaver.cpp new file mode 100644 index 000000000..abfd09290 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataSaver.cpp @@ -0,0 +1,219 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillDataSaver.h" +#include "GameData/SkillData/SkillStructs.h" +#include "GameData/SkillData/SkillFieldDefs.h" +#include "_struct.h" +#include "_define.h" +#include "ZzzInfomation.h" +#include "MultiLanguage.h" +#include "../MuEditor/UI/Console/MuEditorConsoleUI.h" +#include "Utilities/StringUtils.h" +#include +#include + +#include "DataHandler/CommonDataSaver.h" + +// External references +extern SKILL_ATTRIBUTE* SkillAttribute; + +// X-Macro helpers for change tracking +#define COMPARE_FIELD_Byte(name, type, arraySize, width) \ + if (oldSkill.name != newSkill.name) { \ + skillChanges << " " #name ": " << (int)oldSkill.name << " -> " << (int)newSkill.name << "\n"; \ + changed = true; \ + } + +#define COMPARE_FIELD_Word(name, type, arraySize, width) \ + if (oldSkill.name != newSkill.name) { \ + skillChanges << " " #name ": " << oldSkill.name << " -> " << newSkill.name << "\n"; \ + changed = true; \ + } + +#define COMPARE_FIELD_Int(name, type, arraySize, width) \ + if (oldSkill.name != newSkill.name) { \ + skillChanges << " " #name ": " << oldSkill.name << " -> " << newSkill.name << "\n"; \ + changed = true; \ + } + +#define COMPARE_FIELD_DWord(name, type, arraySize, width) \ + if (oldSkill.name != newSkill.name) { \ + skillChanges << " " #name ": " << oldSkill.name << " -> " << newSkill.name << "\n"; \ + changed = true; \ + } + +#define COMPARE_ARRAY_FIELD(nameWithIndex, baseName, index, type, width) \ + if (oldSkill.baseName[index] != newSkill.baseName[index]) { \ + skillChanges << " " #nameWithIndex ": " << (int)oldSkill.baseName[index] << " -> " << (int)newSkill.baseName[index] << "\n"; \ + changed = true; \ + } + +bool SkillDataSaver::Save(wchar_t* fileName, std::string* outChangeLog) +{ + const int Size = sizeof(SKILL_ATTRIBUTE_FILE); + + // Load original file for comparison if change log is requested + SKILL_ATTRIBUTE* originalSkills = nullptr; + if (outChangeLog) + { + originalSkills = new SKILL_ATTRIBUTE[MAX_SKILLS]; + memset(originalSkills, 0, sizeof(SKILL_ATTRIBUTE) * MAX_SKILLS); + + FILE* fpOrig = _wfopen(fileName, L"rb"); + if (fpOrig) + { + // Get file size to determine structure version + fseek(fpOrig, 0, SEEK_END); + long fileSize = ftell(fpOrig); + fseek(fpOrig, 0, SEEK_SET); + + const int LegacySize = sizeof(SKILL_ATTRIBUTE_FILE_LEGACY); + const int NewSize = sizeof(SKILL_ATTRIBUTE_FILE); + const long expectedLegacySize = LegacySize * MAX_SKILLS + sizeof(DWORD); + const long expectedNewSize = NewSize * MAX_SKILLS + sizeof(DWORD); + + bool isLegacyFormat = (fileSize == expectedLegacySize); + int readSize = isLegacyFormat ? LegacySize : NewSize; + + BYTE* OrigBuffer = new BYTE[readSize * MAX_SKILLS]; + fread(OrigBuffer, readSize * MAX_SKILLS, 1, fpOrig); + DWORD dwOrigCheckSum; + fread(&dwOrigCheckSum, sizeof(DWORD), 1, fpOrig); + fclose(fpOrig); + + // Decrypt original data + BYTE* pSeek = OrigBuffer; + for (int i = 0; i < MAX_SKILLS; i++) + { + BuxConvert(pSeek, readSize); + + if (isLegacyFormat) + { + SKILL_ATTRIBUTE_FILE_LEGACY source; + memcpy(&source, pSeek, LegacySize); + CopySkillAttributeFromSource(originalSkills[i], source); + } + else + { + SKILL_ATTRIBUTE_FILE source; + memcpy(&source, pSeek, NewSize); + CopySkillAttributeFromSource(originalSkills[i], source); + } + + pSeek += readSize; + } + delete[] OrigBuffer; + } + } + + // Prepare new buffer + BYTE* Buffer = new BYTE[Size * MAX_SKILLS]; + BYTE* pSeek = Buffer; + + // Track changes + std::stringstream changeLog; + int changeCount = 0; + + // Convert SKILL_ATTRIBUTE to SKILL_ATTRIBUTE_FILE format + for (int i = 0; i < MAX_SKILLS; i++) + { + SKILL_ATTRIBUTE_FILE dest; + memset(&dest, 0, Size); + + CopySkillAttributeToDestination(dest, SkillAttribute[i]); + + // Track changes using X-macros + if (originalSkills && SkillAttribute[i].Name[0] != 0) + { + bool changed = false; + std::stringstream skillChanges; + + SKILL_ATTRIBUTE& oldSkill = originalSkills[i]; + SKILL_ATTRIBUTE& newSkill = SkillAttribute[i]; + + // Name (special case - wide char comparison) + if (wcscmp(oldSkill.Name, newSkill.Name) != 0) + { + char oldName[MAX_SKILL_NAME]; + char newName[MAX_SKILL_NAME]; + CMultiLanguage::ConvertToUtf8(oldName, oldSkill.Name, MAX_SKILL_NAME); + CMultiLanguage::ConvertToUtf8(newName, newSkill.Name, MAX_SKILL_NAME); + skillChanges << " Name: \"" << oldName << "\" -> \"" << newName << "\"\n"; + changed = true; + } + + // All other fields using X-macros + #define COMPARE_SIMPLE(name, type, arraySize, width) COMPARE_FIELD_##type(name, type, arraySize, width) + SKILL_FIELDS_SIMPLE(COMPARE_SIMPLE) + #undef COMPARE_SIMPLE + + SKILL_FIELDS_ARRAYS(COMPARE_ARRAY_FIELD) + + #define COMPARE_SIMPLE(name, type, arraySize, width) COMPARE_FIELD_##type(name, type, arraySize, width) + SKILL_FIELDS_AFTER_ARRAYS(COMPARE_SIMPLE) + #undef COMPARE_SIMPLE + + if (changed) + { + changeLog << "Skill " << i << ":\n" << skillChanges.str(); + changeCount++; + } + } + + // Encrypt and write to buffer + BuxConvert((BYTE*)&dest, Size); + memcpy(pSeek, &dest, Size); + pSeek += Size; + } + + // Check if we should skip saving when no changes detected + if (originalSkills && changeCount == 0) + { + // No changes - skip save + delete[] Buffer; + delete[] originalSkills; + + if (outChangeLog) + { + *outChangeLog = "No changes detected.\n"; + } + + return false; // Return false to indicate no save was needed + } + + // Generate checksum + DWORD dwCheckSum = GenerateCheckSum2(Buffer, Size * MAX_SKILLS, 0x5A18); + + // Create backup + CommonDataSaver::CreateBackup(fileName); + + // Open file for writing only after we know we need to save + FILE* fp = _wfopen(fileName, L"wb"); + if (fp == NULL) + { + delete[] Buffer; + if (originalSkills) delete[] originalSkills; + return false; + } + + fwrite(Buffer, Size * MAX_SKILLS, 1, fp); + fwrite(&dwCheckSum, sizeof(DWORD), 1, fp); + fclose(fp); + + delete[] Buffer; + + if (originalSkills) + { + delete[] originalSkills; + if (outChangeLog && changeCount > 0) + { + *outChangeLog = "=== Skill Changes (" + std::to_string(changeCount) + " skills modified) ===\n" + changeLog.str(); + } + } + + return true; +} + +#endif // _EDITOR diff --git a/src/source/DataHandler/SkillData/SkillDataSaver.h b/src/source/DataHandler/SkillData/SkillDataSaver.h new file mode 100644 index 000000000..79b4a1b85 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataSaver.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef _EDITOR + +#include + +// Skill Data Saving Operations +// Saves skill data to encrypted BMD files with checksum generation +class SkillDataSaver +{ +public: + // Save skill data to file + // Returns true on success, false on failure + // outChangeLog: Optional parameter to receive a detailed change log + static bool Save(wchar_t* fileName, std::string* outChangeLog = nullptr); +}; + +#endif // _EDITOR diff --git a/src/source/DataHandler/SkillData/SkillDataSaverLegacy.cpp b/src/source/DataHandler/SkillData/SkillDataSaverLegacy.cpp new file mode 100644 index 000000000..16825d853 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataSaverLegacy.cpp @@ -0,0 +1,53 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "SkillDataSaverLegacy.h" +#include "GameData/SkillData/SkillStructs.h" +#include "_struct.h" +#include "_define.h" +#include "ZzzInfomation.h" + +// External references +extern SKILL_ATTRIBUTE* SkillAttribute; + +bool SkillDataSaverLegacy::SaveLegacy(wchar_t* fileName) +{ + const int Size = sizeof(SKILL_ATTRIBUTE_FILE_LEGACY); + + FILE* fp = _wfopen(fileName, L"wb"); + if (fp == NULL) + { + return false; + } + + BYTE* Buffer = new BYTE[Size * MAX_SKILLS]; + BYTE* pSeek = Buffer; + + // Convert SKILL_ATTRIBUTE to SKILL_ATTRIBUTE_FILE_LEGACY format + for (int i = 0; i < MAX_SKILLS; i++) + { + SKILL_ATTRIBUTE_FILE_LEGACY dest; + memset(&dest, 0, Size); + + CopySkillAttributeToDestination(dest, SkillAttribute[i]); + + // Encrypt and write to buffer + BuxConvert((BYTE*)&dest, Size); + memcpy(pSeek, &dest, Size); + pSeek += Size; + } + + // Generate checksum + DWORD dwCheckSum = GenerateCheckSum2(Buffer, Size * MAX_SKILLS, 0x5A18); + + fwrite(Buffer, Size * MAX_SKILLS, 1, fp); + fwrite(&dwCheckSum, sizeof(DWORD), 1, fp); + fclose(fp); + + delete[] Buffer; + + return true; +} + +#endif // _EDITOR diff --git a/src/source/DataHandler/SkillData/SkillDataSaverLegacy.h b/src/source/DataHandler/SkillData/SkillDataSaverLegacy.h new file mode 100644 index 000000000..a385018a7 --- /dev/null +++ b/src/source/DataHandler/SkillData/SkillDataSaverLegacy.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef _EDITOR + +#include + +// Skill Data Legacy Format Saving Operations (32-byte name format) +class SkillDataSaverLegacy +{ +public: + static bool SaveLegacy(wchar_t* fileName); +}; + +#endif // _EDITOR diff --git a/src/source/GameData/Common/FieldMetadataHelper.h b/src/source/GameData/Common/FieldMetadataHelper.h new file mode 100644 index 000000000..00f9b5775 --- /dev/null +++ b/src/source/GameData/Common/FieldMetadataHelper.h @@ -0,0 +1,118 @@ +#pragma once + +#ifdef _EDITOR + +#include +#include +#include "Translation/i18n.h" + +// ============================================================================ +// SHARED FIELD TYPE SYSTEM +// ============================================================================ +// This is used by both ItemEditor and SkillEditor (and future editors) + +enum class EFieldType +{ + Bool, + Byte, + Word, + Int, + DWord, + WCharArray +}; + +// Helper macros to convert type enum to actual C++ types +#define FIELD_TYPE_Bool bool +#define FIELD_TYPE_Byte BYTE +#define FIELD_TYPE_Word WORD +#define FIELD_TYPE_Int int +#define FIELD_TYPE_DWord DWORD + +// ============================================================================ +// GENERIC FIELD DESCRIPTOR +// ============================================================================ + +template +struct FieldDescriptor +{ + const char* name; + EFieldType type; + size_t offset; + float width; +}; + +// ============================================================================ +// SHARED HELPER FUNCTIONS +// ============================================================================ + +// Get translated field name with fallback to raw field name +inline const char* GetFieldDisplayName(const char* fieldName) +{ + std::string translationKey = std::string("field_") + fieldName; + if (i18n::HasTranslation(i18n::Domain::Metadata, translationKey.c_str())) + { + return i18n::TranslateMetadata(translationKey.c_str(), fieldName); + } + return fieldName; // Fallback to raw field name if no translation +} + +// ============================================================================ +// GENERIC FIELD RENDERING TEMPLATE +// ============================================================================ +// This template renders any field based on its descriptor +// TColumns must provide: RenderBoolColumn, RenderByteColumn, RenderWordColumn, +// RenderIntColumn, RenderDWordColumn, RenderWCharArrayColumn +// TStruct is the data structure type (e.g., ITEM_ATTRIBUTE, SKILL_ATTRIBUTE) + +template +inline void RenderFieldByDescriptor( + const FieldDescriptor& desc, + TColumns* cols, + TStruct& data, + int& colIdx, + int dataIndex, + bool& rowInteracted, + bool isVisible, + int maxStringLength) +{ + if (!isVisible) return; + + BYTE* dataPtr = reinterpret_cast(&data); + void* fieldPtr = dataPtr + desc.offset; + const char* displayName = GetFieldDisplayName(desc.name); + + // Generate unique ID for ImGui + int uniqueId = 0; + for (const char* p = desc.name; *p; ++p) uniqueId = (uniqueId * 31) + *p; + + switch (desc.type) + { + case EFieldType::Bool: + cols->RenderBoolColumn(displayName, colIdx, dataIndex, uniqueId, + *reinterpret_cast(fieldPtr), rowInteracted, isVisible); + break; + case EFieldType::Byte: + cols->RenderByteColumn(displayName, colIdx, dataIndex, uniqueId, + *reinterpret_cast(fieldPtr), rowInteracted, isVisible); + break; + case EFieldType::Word: + cols->RenderWordColumn(displayName, colIdx, dataIndex, uniqueId, + *reinterpret_cast(fieldPtr), rowInteracted, isVisible); + break; + case EFieldType::Int: + cols->RenderIntColumn(displayName, colIdx, dataIndex, uniqueId, + *reinterpret_cast(fieldPtr), rowInteracted, isVisible); + break; + case EFieldType::DWord: + cols->RenderDWordColumn(displayName, colIdx, dataIndex, uniqueId, + *reinterpret_cast(fieldPtr), rowInteracted, isVisible); + break; + case EFieldType::WCharArray: + cols->RenderWCharArrayColumn(displayName, colIdx, dataIndex, uniqueId, + reinterpret_cast(fieldPtr), maxStringLength, + rowInteracted, isVisible); + break; + } +} + +#endif // _EDITOR diff --git a/src/source/GameData/ItemData/ItemFieldDefs.h b/src/source/GameData/ItemData/ItemFieldDefs.h index d7582db71..9941efc18 100644 --- a/src/source/GameData/ItemData/ItemFieldDefs.h +++ b/src/source/GameData/ItemData/ItemFieldDefs.h @@ -56,6 +56,7 @@ #define FIELD_TYPE_Byte BYTE #define FIELD_TYPE_Word WORD #define FIELD_TYPE_Int int +#define FIELD_TYPE_DWord DWORD // Generate struct field declarations from X-macro #define DECLARE_FIELD(name, type, arraySize, width) FIELD_TYPE_##type name; diff --git a/src/source/GameData/ItemData/ItemFieldMetadata.h b/src/source/GameData/ItemData/ItemFieldMetadata.h index e48c45eca..47a016400 100644 --- a/src/source/GameData/ItemData/ItemFieldMetadata.h +++ b/src/source/GameData/ItemData/ItemFieldMetadata.h @@ -6,59 +6,37 @@ #include #include "ItemStructs.h" #include "ItemFieldDefs.h" -#include "Translation/i18n.h" +#include "GameData/Common/FieldMetadataHelper.h" -// Helper to get translated field name with fallback -inline const char* GetFieldDisplayName(const char* fieldName) -{ - std::string translationKey = std::string("field_") + fieldName; - if (i18n::HasTranslation(i18n::Domain::Metadata, translationKey.c_str())) - { - return i18n::TranslateMetadata(translationKey.c_str(), fieldName); - } - return fieldName; -} - -// Type enum (kept minimal for type-based dispatch) -enum class EItemFieldType -{ - Bool, - Byte, - Word, - Int, - WCharArray -}; +// ============================================================================ +// ITEM-SPECIFIC METADATA +// ============================================================================ -#define FIELD_OFFSET(type, field) offsetof(type, field) +// Type alias for backward compatibility +using EItemFieldType = EFieldType; -// Simple field descriptor (replaces heavy ItemFieldMetadata struct) -struct FieldDescriptor -{ - const char* name; - EItemFieldType type; - size_t offset; - float width; -}; +// Item field descriptor (specialized from generic FieldDescriptor) +using ItemFieldDescriptor = FieldDescriptor; -// Macro to generate field descriptors +// Macro to generate item field descriptors #define MAKE_SIMPLE_FIELD_DESCRIPTOR(name, type, arraySize, width) \ - { #name, EItemFieldType::type, FIELD_OFFSET(ITEM_ATTRIBUTE, name), width }, + { #name, EFieldType::type, offsetof(ITEM_ATTRIBUTE, name), width }, #define MAKE_ARRAY_FIELD_DESCRIPTOR(nameWithIndex, baseName, index, type, width) \ - { #nameWithIndex, EItemFieldType::type, FIELD_OFFSET(ITEM_ATTRIBUTE, baseName[index]), width }, + { #nameWithIndex, EFieldType::type, offsetof(ITEM_ATTRIBUTE, baseName[index]), width }, // Internal: Single static array for all field descriptors (generated by X-macros) namespace ItemFieldMetadataInternal { - static const FieldDescriptor s_descriptors[] = { - { "Name", EItemFieldType::WCharArray, FIELD_OFFSET(ITEM_ATTRIBUTE, Name), 150.0f }, + static const ItemFieldDescriptor s_descriptors[] = { + { "Name", EFieldType::WCharArray, offsetof(ITEM_ATTRIBUTE, Name), 150.0f }, ITEM_FIELDS_SIMPLE(MAKE_SIMPLE_FIELD_DESCRIPTOR) ITEM_FIELDS_ARRAYS(MAKE_ARRAY_FIELD_DESCRIPTOR) }; } // Get pointer to field descriptor array -inline const FieldDescriptor* GetFieldDescriptors() +inline const ItemFieldDescriptor* GetFieldDescriptors() { return ItemFieldMetadataInternal::s_descriptors; } @@ -66,42 +44,15 @@ inline const FieldDescriptor* GetFieldDescriptors() // Get count of fields (auto-computed from array size) inline constexpr int GetFieldCount() { - return sizeof(ItemFieldMetadataInternal::s_descriptors) / sizeof(FieldDescriptor); + return sizeof(ItemFieldMetadataInternal::s_descriptors) / sizeof(ItemFieldDescriptor); } -// Helper to render a field by descriptor +// Helper to render an item field by descriptor (uses generic helper) template -inline void RenderFieldByDescriptor(const FieldDescriptor& desc, TColumns* cols, ITEM_ATTRIBUTE& item, +inline void RenderFieldByDescriptor(const ItemFieldDescriptor& desc, TColumns* cols, ITEM_ATTRIBUTE& item, int& colIdx, int itemIndex, bool& rowInteracted, bool isVisible) { - if (!isVisible) return; - - BYTE* itemPtr = reinterpret_cast(&item); - void* fieldPtr = itemPtr + desc.offset; - const char* displayName = GetFieldDisplayName(desc.name); - - // Generate unique ID - int uniqueId = 0; - for (const char* p = desc.name; *p; ++p) uniqueId = (uniqueId * 31) + *p; - - switch (desc.type) - { - case EItemFieldType::Bool: - cols->RenderBoolColumn(displayName, colIdx, itemIndex, uniqueId, *reinterpret_cast(fieldPtr), rowInteracted, isVisible); - break; - case EItemFieldType::Byte: - cols->RenderByteColumn(displayName, colIdx, itemIndex, uniqueId, *reinterpret_cast(fieldPtr), rowInteracted, isVisible); - break; - case EItemFieldType::Word: - cols->RenderWordColumn(displayName, colIdx, itemIndex, uniqueId, *reinterpret_cast(fieldPtr), rowInteracted, isVisible); - break; - case EItemFieldType::Int: - cols->RenderIntColumn(displayName, colIdx, itemIndex, uniqueId, *reinterpret_cast(fieldPtr), rowInteracted, isVisible); - break; - case EItemFieldType::WCharArray: - cols->RenderWCharArrayColumn(displayName, colIdx, itemIndex, uniqueId, reinterpret_cast(fieldPtr), MAX_ITEM_NAME, rowInteracted, isVisible); - break; - } + ::RenderFieldByDescriptor(desc, cols, item, colIdx, itemIndex, rowInteracted, isVisible, MAX_ITEM_NAME); } #endif // _EDITOR diff --git a/src/source/GameData/SkillData/SkillFieldDefs.h b/src/source/GameData/SkillData/SkillFieldDefs.h new file mode 100644 index 000000000..ffe34cdd6 --- /dev/null +++ b/src/source/GameData/SkillData/SkillFieldDefs.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +// X-Macro definition for all SKILL_ATTRIBUTE fields (except Name which is special) +// Format: X(FieldName, TypeEnum, ArraySize, DefaultColumnWidth) +// This is the SINGLE SOURCE OF TRUTH for skill field definitions + +#define SKILL_FIELDS_SIMPLE(X) \ + X(Level, Word, 1, 60.0f) \ + X(Damage, Word, 1, 70.0f) \ + X(Mana, Word, 1, 70.0f) \ + X(AbilityGuage, Word, 1, 80.0f) \ + X(Distance, DWord, 1, 80.0f) \ + X(Delay, Int, 1, 70.0f) \ + X(Energy, Int, 1, 70.0f) \ + X(Charisma, Word, 1, 80.0f) \ + X(MasteryType, Byte, 1, 80.0f) \ + X(SkillUseType, Byte, 1, 80.0f) \ + X(SkillBrand, DWord, 1, 90.0f) \ + X(KillCount, Byte, 1, 80.0f) + +#define SKILL_FIELDS_ARRAYS(X) \ + X(RequireDutyClass[0], RequireDutyClass, 0, Byte, 80.0f) \ + X(RequireDutyClass[1], RequireDutyClass, 1, Byte, 80.0f) \ + X(RequireDutyClass[2], RequireDutyClass, 2, Byte, 80.0f) \ + X(RequireClass[0], RequireClass, 0, Byte, 60.0f) \ + X(RequireClass[1], RequireClass, 1, Byte, 60.0f) \ + X(RequireClass[2], RequireClass, 2, Byte, 65.0f) \ + X(RequireClass[3], RequireClass, 3, Byte, 60.0f) \ + X(RequireClass[4], RequireClass, 4, Byte, 60.0f) \ + X(RequireClass[5], RequireClass, 5, Byte, 65.0f) \ + X(RequireClass[6], RequireClass, 6, Byte, 60.0f) + +// Helper macro to convert TypeEnum to actual C++ type (same as ItemFieldDefs.h) +#define SKILL_FIELD_TYPE_Bool bool +#define SKILL_FIELD_TYPE_Byte BYTE +#define SKILL_FIELD_TYPE_Word WORD +#define SKILL_FIELD_TYPE_Int int +#define SKILL_FIELD_TYPE_DWord DWORD + +// Generate struct field declarations from X-macro +#define DECLARE_SKILL_FIELD(name, type, arraySize, width) SKILL_FIELD_TYPE_##type name; + +// Fields that come after the arrays (must be in correct order for binary compatibility) +#define SKILL_FIELDS_AFTER_ARRAYS(X) \ + X(SkillRank, Byte, 1, 70.0f) \ + X(Magic_Icon, Word, 1, 80.0f) \ + X(TypeSkill, Byte, 1, 80.0f) \ + X(Strength, Int, 1, 80.0f) \ + X(Dexterity, Int, 1, 80.0f) \ + X(ItemSkill, Byte, 1, 80.0f) \ + X(IsDamage, Byte, 1, 70.0f) \ + X(Effect, Word, 1, 70.0f) + +// Macro to generate all fields for struct definition (excludes Name) +// IMPORTANT: Field order must match original binary format! +#define SKILL_ATTRIBUTE_FIELDS \ + SKILL_FIELDS_SIMPLE(DECLARE_SKILL_FIELD) \ + BYTE RequireDutyClass[MAX_DUTY_CLASS]; \ + BYTE RequireClass[MAX_CLASS]; \ + SKILL_FIELDS_AFTER_ARRAYS(DECLARE_SKILL_FIELD) + +// Note: WCharArray for Name field is handled specially in SkillFieldMetadata.h diff --git a/src/source/GameData/SkillData/SkillFieldMetadata.h b/src/source/GameData/SkillData/SkillFieldMetadata.h new file mode 100644 index 000000000..2afef0ead --- /dev/null +++ b/src/source/GameData/SkillData/SkillFieldMetadata.h @@ -0,0 +1,66 @@ +#pragma once + +#ifdef _EDITOR + +#include +#include +#include "SkillStructs.h" +#include "SkillFieldDefs.h" +#include "GameData/Common/FieldMetadataHelper.h" + +// ============================================================================ +// SKILL-SPECIFIC METADATA +// ============================================================================ + +// Type alias for clarity - skills use the same type system as items +using ESkillFieldType = EFieldType; + +// Skill field descriptor (specialized from generic FieldDescriptor) +using SkillFieldDescriptor = FieldDescriptor; + +// Macros for descriptor generation +#define MAKE_SKILL_FIELD_DESCRIPTOR(name, type, arraySize, width) \ + { #name, EFieldType::type, offsetof(SKILL_ATTRIBUTE, name), width }, + +#define MAKE_SKILL_ARRAY_DESCRIPTOR(nameWithIndex, baseName, index, type, width) \ + { #nameWithIndex, EFieldType::type, offsetof(SKILL_ATTRIBUTE, baseName[index]), width }, + +// Static descriptor array - automatically generated from X-macros +namespace SkillFieldMetadataInternal +{ + static const SkillFieldDescriptor s_descriptors[] = { + // Name field is special (UTF-16 string) + { "Name", EFieldType::WCharArray, offsetof(SKILL_ATTRIBUTE, Name), 150.0f }, + // All other fields from X-macros + SKILL_FIELDS_SIMPLE(MAKE_SKILL_FIELD_DESCRIPTOR) + SKILL_FIELDS_ARRAYS(MAKE_SKILL_ARRAY_DESCRIPTOR) + SKILL_FIELDS_AFTER_ARRAYS(MAKE_SKILL_FIELD_DESCRIPTOR) + }; +} + +// Accessor functions +inline const SkillFieldDescriptor* GetSkillFieldDescriptors() +{ + return SkillFieldMetadataInternal::s_descriptors; +} + +inline constexpr int GetSkillFieldCount() +{ + return sizeof(SkillFieldMetadataInternal::s_descriptors) / sizeof(SkillFieldDescriptor); +} + +// Get display name for a skill field (with translation support) +inline const char* GetSkillFieldDisplayName(const char* fieldName) +{ + return GetFieldDisplayName(fieldName); +} + +// Helper to render a skill field by descriptor (uses generic helper) +template +inline void RenderSkillFieldByDescriptor(const SkillFieldDescriptor& desc, TColumns* cols, SKILL_ATTRIBUTE& skill, + int& colIdx, int skillIndex, bool& rowInteracted, bool isVisible) +{ + ::RenderFieldByDescriptor(desc, cols, skill, colIdx, skillIndex, rowInteracted, isVisible, MAX_SKILL_NAME); +} + +#endif // _EDITOR diff --git a/src/source/GameData/SkillData/SkillStructs.h b/src/source/GameData/SkillData/SkillStructs.h new file mode 100644 index 000000000..2865e1fb0 --- /dev/null +++ b/src/source/GameData/SkillData/SkillStructs.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +// Forward declarations for constants +#ifndef MAX_CLASS +#define MAX_CLASS 7 +#endif + +#ifndef MAX_DUTY_CLASS +#define MAX_DUTY_CLASS 3 +#endif + +#ifndef MAX_SKILL_NAME +#define MAX_SKILL_NAME 50 +#endif + +// Include X-macro field definitions +#include "SkillFieldDefs.h" + +// Legacy file format structure (32-byte name) +// Used for backward compatibility with old BMD files +typedef struct +{ + char Name[32]; // Legacy format used 32 bytes + SKILL_ATTRIBUTE_FIELDS +} SKILL_ATTRIBUTE_FILE_LEGACY; + +// Current file format structure with MAX_SKILL_NAME byte name +// Used for reading/writing BMD files +typedef struct +{ + char Name[MAX_SKILL_NAME]; + SKILL_ATTRIBUTE_FIELDS +} SKILL_ATTRIBUTE_FILE; + +// Runtime structure with wide-character name +// Used in-memory during gameplay/editor +typedef struct +{ + wchar_t Name[MAX_SKILL_NAME]; + SKILL_ATTRIBUTE_FIELDS +} SKILL_ATTRIBUTE; + +// ============================================================================ +// COPY HELPERS (mirrors ItemStructs.h pattern) +// ============================================================================ + +// Generate field copy statements from X-macro +#define COPY_SKILL_FIELD(name, type, arraySize, width) (dest).name = (source).name; + +// Macro to copy all non-name fields from source to dest +#define COPY_SKILL_ATTRIBUTE_FIELDS(dest, source) \ + do { \ + SKILL_FIELDS_SIMPLE(COPY_SKILL_FIELD) \ + memcpy((dest).RequireDutyClass, (source).RequireDutyClass, sizeof((source).RequireDutyClass)); \ + memcpy((dest).RequireClass, (source).RequireClass, sizeof((source).RequireClass)); \ + SKILL_FIELDS_AFTER_ARRAYS(COPY_SKILL_FIELD) \ + } while(0) + +// Helper template to copy from file structure to runtime structure +// Requires: #include "MultiLanguage.h" +template +inline void CopySkillAttributeFromSource(SKILL_ATTRIBUTE& dest, TSource& source) +{ + CMultiLanguage::ConvertFromUtf8(dest.Name, source.Name, MAX_SKILL_NAME); + COPY_SKILL_ATTRIBUTE_FIELDS(dest, source); +} + +// Helper template to copy from runtime structure to file structure +// Requires: #include "MultiLanguage.h" +template +inline void CopySkillAttributeToDestination(TDest& dest, SKILL_ATTRIBUTE& source) +{ + CMultiLanguage::ConvertToUtf8(dest.Name, source.Name, sizeof(dest.Name)); + COPY_SKILL_ATTRIBUTE_FIELDS(dest, source); +} diff --git a/src/source/Utilities/StringUtils.h b/src/source/Utilities/StringUtils.h new file mode 100644 index 000000000..b66b3bb6b --- /dev/null +++ b/src/source/Utilities/StringUtils.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace StringUtils +{ + // Convert wide string (UTF-16) to narrow string (UTF-8) + inline std::string WideToNarrow(const wchar_t* wstr) + { + if (!wstr) return ""; + size_t len = wcslen(wstr); + if (len == 0) return ""; + + int size = WideCharToMultiByte(CP_UTF8, 0, wstr, (int)len, NULL, 0, NULL, NULL); + std::string result(size, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr, (int)len, &result[0], size, NULL, NULL); + return result; + } +} diff --git a/src/source/ZzzInfomation.cpp b/src/source/ZzzInfomation.cpp index 1e842a851..f89947716 100644 --- a/src/source/ZzzInfomation.cpp +++ b/src/source/ZzzInfomation.cpp @@ -242,57 +242,6 @@ void OpenNpcScript(wchar_t* FileName) fclose(SMDFile); } -void OpenSkillScript(wchar_t* FileName) -{ - FILE* fp = _wfopen(FileName, L"rb"); - if (fp != NULL) - { - int Size = sizeof(SKILL_ATTRIBUTE_FILE); - // 읽기 - BYTE* Buffer = new BYTE[Size * MAX_SKILLS]; - fread(Buffer, Size * MAX_SKILLS, 1, fp); - // crc 체크 - DWORD dwCheckSum; - fread(&dwCheckSum, sizeof(DWORD), 1, fp); - fclose(fp); - if (dwCheckSum != GenerateCheckSum2(Buffer, Size * MAX_SKILLS, 0x5A18)) - { - wchar_t Text[256]; - mu_swprintf(Text, L"%ls - File corrupted.", FileName); - g_ErrorReport.Write(Text); - MessageBox(g_hWnd, Text, NULL, MB_OK); - SendMessage(g_hWnd, WM_DESTROY, 0, 0); - } - else - { - BYTE* pSeek = Buffer; - for (int i = 0; i < MAX_SKILLS; i++) - { - BuxConvert(pSeek, Size); - // memcpy(&SkillAttribute[i], pSeek, Size); - - char rawName[MAX_SKILL_NAME]{}; - - memcpy(rawName, pSeek, MAX_SKILL_NAME); - CMultiLanguage::ConvertFromUtf8(SkillAttribute[i].Name, rawName); - pSeek += MAX_SKILL_NAME; - memcpy(&(SkillAttribute[i].Level), pSeek, Size - MAX_SKILL_NAME); - - pSeek += Size - MAX_SKILL_NAME; - } - } - delete[] Buffer; - } - else - { - wchar_t Text[256]; - mu_swprintf(Text, L"%ls - File not exist.", FileName); - g_ErrorReport.Write(Text); - MessageBox(g_hWnd, Text, NULL, MB_OK); - SendMessage(g_hWnd, WM_DESTROY, 0, 0); - } -} - BOOL IsValidateSkillIdx(INT iSkillIdx) { if (iSkillIdx >= MAX_SKILLS || iSkillIdx < 0) diff --git a/src/source/ZzzInfomation.h b/src/source/ZzzInfomation.h index 8b51566cb..a94bd7dcb 100644 --- a/src/source/ZzzInfomation.h +++ b/src/source/ZzzInfomation.h @@ -40,8 +40,6 @@ extern wchar_t* getMonsterName(int type); extern SKILL_ATTRIBUTE* SkillAttribute; -void OpenSkillScript(wchar_t* FileName); - BOOL IsValidateSkillIdx(INT iSkillIdx); BOOL IsCorrectSkillType(INT iSkillSeq, eTypeSkill iSkillTypeIdx); BOOL IsCorrectSkillType_FrendlySkill(INT iSkillSeq); diff --git a/src/source/ZzzOpenData.cpp b/src/source/ZzzOpenData.cpp index f8a1583c4..388a34c1d 100644 --- a/src/source/ZzzOpenData.cpp +++ b/src/source/ZzzOpenData.cpp @@ -28,6 +28,7 @@ #include "QuestMng.h" #include "ServerListManager.h" #include "MonkSystem.h" +#include "DataHandler/SkillData/SkillDataHandler.h" #include "DataHandler/ItemData/ItemDataHandler.h" #include "SocketSystem.h" @@ -5381,7 +5382,7 @@ void OpenBasicData(HDC hDC) g_csQuest.OpenQuestScript(Text); mu_swprintf(Text, L"Data\\Local\\%ls\\Skill_%ls.bmd", g_strSelectedML.c_str(), g_strSelectedML.c_str()); - OpenSkillScript(Text); + g_SkillDataHandler.Load(Text); mu_swprintf(Text, L"Data\\Local\\%ls\\SocketItem_%ls.bmd", g_strSelectedML.c_str(), g_strSelectedML.c_str()); g_SocketItemMgr.OpenSocketItemScript(Text); diff --git a/src/source/_define.h b/src/source/_define.h index 81be3a173..f885a3312 100644 --- a/src/source/_define.h +++ b/src/source/_define.h @@ -329,13 +329,10 @@ enum struct STORAGE_TYPE #define MAX_MINIMAP_NAME 100 -#define MAX_SKILL_NAME 32 #define MAX_MONSTER_NAME 40 constexpr int MAX_ITEM_TYPE = 16; -constexpr int MAX_ITEM_NAME = 50; - constexpr int MAX_ITEM_INDEX = 512; constexpr int MAX_ITEM = MAX_ITEM_TYPE * MAX_ITEM_INDEX; diff --git a/src/source/_struct.h b/src/source/_struct.h index 833ed0e96..2e6464088 100644 --- a/src/source/_struct.h +++ b/src/source/_struct.h @@ -271,59 +271,9 @@ typedef struct MONSTER_ATTRIBUTE Attribute; } MONSTER; -typedef struct -{ - /*+00*/ char Name[MAX_SKILL_NAME]; - /*+32*/ WORD Level; - /*+34*/ WORD Damage; - /*+36*/ WORD Mana; - /*+38*/ WORD AbilityGuage; - /*+40*/ DWORD Distance; - /*+44*/ int Delay; - /*+48*/ int Energy; - /*+52*/ WORD Charisma; - /*+54*/ BYTE MasteryType; - /*+55*/ BYTE SkillUseType; - /*+56*/ DWORD SkillBrand; - /*+60*/ BYTE KillCount; - /*+61*/ BYTE RequireDutyClass[MAX_DUTY_CLASS]; - /*+64*/ BYTE RequireClass[MAX_CLASS]; - /*+71*/ BYTE SkillRank; - /*+72*/ WORD Magic_Icon; - /*+74*/ BYTE TypeSkill; - /*+76*/ int Strength; - /*+80*/ int Dexterity; - /*+84*/ BYTE ItemSkill; - /*+85*/ BYTE IsDamage; - /*+86*/ WORD Effect; -} SKILL_ATTRIBUTE_FILE; - -typedef struct -{ - wchar_t Name[MAX_SKILL_NAME]; - WORD Level; - WORD Damage; - WORD Mana; - WORD AbilityGuage; - DWORD Distance; - int Delay; - int Energy; - WORD Charisma; - BYTE MasteryType; - BYTE SkillUseType; - DWORD SkillBrand; - BYTE KillCount; - BYTE RequireDutyClass[MAX_DUTY_CLASS]; - BYTE RequireClass[MAX_CLASS]; - BYTE SkillRank; - WORD Magic_Icon; - BYTE TypeSkill; - int Strength; - int Dexterity; - BYTE ItemSkill; - BYTE IsDamage; - WORD Effect; -} SKILL_ATTRIBUTE; +// SKILL_ATTRIBUTE_FILE and SKILL_ATTRIBUTE definitions moved to GameData/SkillData/SkillStructs.h +// This is now the single source of truth for ALL builds (editor and release) +#include "GameData/SkillData/SkillStructs.h" /* typedef struct {