From aca398f3c9f312faea2094bb7a220012a9d0246b Mon Sep 17 00:00:00 2001 From: Corantin H Date: Sat, 23 Dec 2023 14:31:19 +0100 Subject: [PATCH] Added dialog to fix invalid map settings on load --- src/engine/shared/config.cpp | 274 ++++++++ src/engine/shared/config.h | 289 +-------- src/engine/textrender.h | 2 +- src/game/editor/editor.cpp | 9 +- src/game/editor/editor.h | 13 +- src/game/editor/editor_actions.cpp | 1 - src/game/editor/editor_server_settings.cpp | 700 +++++++++++++++++++-- src/game/editor/editor_server_settings.h | 71 ++- src/game/editor/editor_ui.h | 2 +- 9 files changed, 1022 insertions(+), 339 deletions(-) diff --git a/src/engine/shared/config.cpp b/src/engine/shared/config.cpp index 8cbe3eb4f..d30467072 100644 --- a/src/engine/shared/config.cpp +++ b/src/engine/shared/config.cpp @@ -11,6 +11,280 @@ CConfig g_Config; +// ----------------------- Config Variables + +static void EscapeParam(char *pDst, const char *pSrc, int Size) +{ + str_escape(&pDst, pSrc, pDst + Size); +} + +void SConfigVariable::ExecuteLine(const char *pLine) const +{ + m_pConsole->ExecuteLine(pLine, (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : -1); +} + +bool SConfigVariable::CheckReadOnly() const +{ + if(!m_ReadOnly) + return false; + char aBuf[IConsole::CMDLINE_LENGTH + 64]; + str_format(aBuf, sizeof(aBuf), "The config variable '%s' cannot be changed right now.", m_pScriptName); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + return true; +} + +// ----- + +void SIntConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SIntConfigVariable *pData = static_cast(pUserData); + + if(pResult->NumArguments()) + { + if(pData->CheckReadOnly()) + return; + + int Value = pResult->GetInteger(0); + + // do clamping + if(pData->m_Min != pData->m_Max) + { + if(Value < pData->m_Min) + Value = pData->m_Min; + if(pData->m_Max != 0 && Value > pData->m_Max) + Value = pData->m_Max; + } + + *pData->m_pVariable = Value; + if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) + pData->m_OldValue = Value; + } + else + { + char aBuf[32]; + str_format(aBuf, sizeof(aBuf), "Value: %d", *pData->m_pVariable); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + } +} + +void SIntConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); +} + +bool SIntConfigVariable::IsDefault() const +{ + return *m_pVariable == m_Default; +} + +void SIntConfigVariable::Serialize(char *pOut, size_t Size, int Value) const +{ + str_format(pOut, Size, "%s %i", m_pScriptName, Value); +} + +void SIntConfigVariable::Serialize(char *pOut, size_t Size) const +{ + Serialize(pOut, Size, *m_pVariable); +} + +void SIntConfigVariable::SetValue(int Value) +{ + if(CheckReadOnly()) + return; + char aBuf[IConsole::CMDLINE_LENGTH]; + Serialize(aBuf, sizeof(aBuf), Value); + ExecuteLine(aBuf); +} + +void SIntConfigVariable::ResetToDefault() +{ + SetValue(m_Default); +} + +void SIntConfigVariable::ResetToOld() +{ + *m_pVariable = m_OldValue; +} + +// ----- + +void SColorConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SColorConfigVariable *pData = static_cast(pUserData); + + if(pResult->NumArguments()) + { + if(pData->CheckReadOnly()) + return; + + const ColorHSLA Color = pResult->GetColor(0, pData->m_Light); + const unsigned Value = Color.Pack(pData->m_Light ? 0.5f : 0.0f, pData->m_Alpha); + + *pData->m_pVariable = Value; + if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) + pData->m_OldValue = Value; + } + else + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "Value: %u", *pData->m_pVariable); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + + ColorHSLA Hsla = ColorHSLA(*pData->m_pVariable, true); + if(pData->m_Light) + Hsla = Hsla.UnclampLighting(); + str_format(aBuf, sizeof(aBuf), "H: %d°, S: %d%%, L: %d%%", round_truncate(Hsla.h * 360), round_truncate(Hsla.s * 100), round_truncate(Hsla.l * 100)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + + const ColorRGBA Rgba = color_cast(Hsla); + str_format(aBuf, sizeof(aBuf), "R: %d, G: %d, B: %d, #%06X", round_truncate(Rgba.r * 255), round_truncate(Rgba.g * 255), round_truncate(Rgba.b * 255), Rgba.Pack(false)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + + if(pData->m_Alpha) + { + str_format(aBuf, sizeof(aBuf), "A: %d%%", round_truncate(Hsla.a * 100)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + } + } +} + +void SColorConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); +} + +bool SColorConfigVariable::IsDefault() const +{ + return *m_pVariable == m_Default; +} + +void SColorConfigVariable::Serialize(char *pOut, size_t Size, unsigned Value) const +{ + str_format(pOut, Size, "%s %u", m_pScriptName, Value); +} + +void SColorConfigVariable::Serialize(char *pOut, size_t Size) const +{ + Serialize(pOut, Size, *m_pVariable); +} + +void SColorConfigVariable::SetValue(unsigned Value) +{ + if(CheckReadOnly()) + return; + char aBuf[IConsole::CMDLINE_LENGTH]; + Serialize(aBuf, sizeof(aBuf), Value); + ExecuteLine(aBuf); +} + +void SColorConfigVariable::ResetToDefault() +{ + SetValue(m_Default); +} + +void SColorConfigVariable::ResetToOld() +{ + *m_pVariable = m_OldValue; +} + +// ----- + +SStringConfigVariable::SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pStr(pStr), + m_pDefault(pDefault), + m_MaxSize(MaxSize), + m_pOldValue(pOldValue) +{ + str_copy(m_pStr, m_pDefault, m_MaxSize); + str_copy(m_pOldValue, m_pDefault, m_MaxSize); +} + +void SStringConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SStringConfigVariable *pData = static_cast(pUserData); + + if(pResult->NumArguments()) + { + if(pData->CheckReadOnly()) + return; + + const char *pString = pResult->GetString(0); + if(!str_utf8_check(pString)) + { + char aTemp[4]; + size_t Length = 0; + while(*pString) + { + size_t Size = str_utf8_encode(aTemp, static_cast(*pString++)); + if(Length + Size < pData->m_MaxSize) + { + mem_copy(pData->m_pStr + Length, aTemp, Size); + Length += Size; + } + else + break; + } + pData->m_pStr[Length] = '\0'; + } + else + str_copy(pData->m_pStr, pString, pData->m_MaxSize); + + if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) + str_copy(pData->m_pOldValue, pData->m_pStr, pData->m_MaxSize); + } + else + { + char aBuf[1024]; + str_format(aBuf, sizeof(aBuf), "Value: %s", pData->m_pStr); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + } +} + +void SStringConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?r", m_Flags, CommandCallback, this, m_pHelp); +} + +bool SStringConfigVariable::IsDefault() const +{ + return str_comp(m_pStr, m_pDefault) == 0; +} + +void SStringConfigVariable::Serialize(char *pOut, size_t Size, const char *pValue) const +{ + str_copy(pOut, m_pScriptName, Size); + str_append(pOut, " \"", Size); + const int OutLen = str_length(pOut); + EscapeParam(pOut + OutLen, pValue, Size - OutLen - 1); // -1 to ensure space for final quote + str_append(pOut, "\"", Size); +} + +void SStringConfigVariable::Serialize(char *pOut, size_t Size) const +{ + Serialize(pOut, Size, m_pStr); +} + +void SStringConfigVariable::SetValue(const char *pValue) +{ + if(CheckReadOnly()) + return; + char aBuf[2048]; + Serialize(aBuf, sizeof(aBuf), pValue); + ExecuteLine(aBuf); +} + +void SStringConfigVariable::ResetToDefault() +{ + SetValue(m_pDefault); +} + +void SStringConfigVariable::ResetToOld() +{ + str_copy(m_pStr, m_pOldValue, m_MaxSize); +} + +// ----------------------- Config Manager CConfigManager::CConfigManager() { m_pConsole = nullptr; diff --git a/src/engine/shared/config.h b/src/engine/shared/config.h index 2d8503aa9..bc82bfbd6 100644 --- a/src/engine/shared/config.h +++ b/src/engine/shared/config.h @@ -4,7 +4,6 @@ #define ENGINE_SHARED_CONFIG_H #include -#include #include #include @@ -59,11 +58,6 @@ enum CFGFLAG_INSENSITIVE = 1 << 12, }; -static void EscapeParam(char *pDst, const char *pSrc, int Size) -{ - str_escape(&pDst, pSrc, pDst + Size); -} - struct SConfigVariable { enum EVariableType @@ -99,20 +93,8 @@ struct SConfigVariable virtual void ResetToOld() = 0; protected: - void ExecuteLine(const char *pLine) const - { - m_pConsole->ExecuteLine(pLine, (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : -1); - } - - bool CheckReadOnly() const - { - if(!m_ReadOnly) - return false; - char aBuf[IConsole::CMDLINE_LENGTH + 64]; - str_format(aBuf, sizeof(aBuf), "The config variable '%s' cannot be changed right now.", m_pScriptName); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - return true; - } + void ExecuteLine(const char *pLine) const; + bool CheckReadOnly() const; }; struct SIntConfigVariable : public SConfigVariable @@ -136,76 +118,14 @@ struct SIntConfigVariable : public SConfigVariable ~SIntConfigVariable() override = default; - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) - { - SIntConfigVariable *pData = static_cast(pUserData); - - if(pResult->NumArguments()) - { - if(pData->CheckReadOnly()) - return; - - int Value = pResult->GetInteger(0); - - // do clamping - if(pData->m_Min != pData->m_Max) - { - if(Value < pData->m_Min) - Value = pData->m_Min; - if(pData->m_Max != 0 && Value > pData->m_Max) - Value = pData->m_Max; - } - - *pData->m_pVariable = Value; - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) - pData->m_OldValue = Value; - } - else - { - char aBuf[32]; - str_format(aBuf, sizeof(aBuf), "Value: %d", *pData->m_pVariable); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - } - } - - void Register() override - { - m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); - } - - bool IsDefault() const override - { - return *m_pVariable == m_Default; - } - - void Serialize(char *pOut, size_t Size, int Value) const - { - str_format(pOut, Size, "%s %i", m_pScriptName, Value); - } - - void Serialize(char *pOut, size_t Size) const override - { - Serialize(pOut, Size, *m_pVariable); - } - - void SetValue(int Value) - { - if(CheckReadOnly()) - return; - char aBuf[IConsole::CMDLINE_LENGTH]; - Serialize(aBuf, sizeof(aBuf), Value); - ExecuteLine(aBuf); - } - - void ResetToDefault() override - { - SetValue(m_Default); - } - - void ResetToOld() override - { - *m_pVariable = m_OldValue; - } + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, int Value) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(int Value); + void ResetToDefault() override; + void ResetToOld() override; }; struct SColorConfigVariable : public SConfigVariable @@ -229,84 +149,14 @@ struct SColorConfigVariable : public SConfigVariable ~SColorConfigVariable() override = default; - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) - { - SColorConfigVariable *pData = static_cast(pUserData); - - if(pResult->NumArguments()) - { - if(pData->CheckReadOnly()) - return; - - const ColorHSLA Color = pResult->GetColor(0, pData->m_Light); - const unsigned Value = Color.Pack(pData->m_Light ? 0.5f : 0.0f, pData->m_Alpha); - - *pData->m_pVariable = Value; - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) - pData->m_OldValue = Value; - } - else - { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Value: %u", *pData->m_pVariable); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - ColorHSLA Hsla = ColorHSLA(*pData->m_pVariable, true); - if(pData->m_Light) - Hsla = Hsla.UnclampLighting(); - str_format(aBuf, sizeof(aBuf), "H: %d°, S: %d%%, L: %d%%", round_truncate(Hsla.h * 360), round_truncate(Hsla.s * 100), round_truncate(Hsla.l * 100)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - const ColorRGBA Rgba = color_cast(Hsla); - str_format(aBuf, sizeof(aBuf), "R: %d, G: %d, B: %d, #%06X", round_truncate(Rgba.r * 255), round_truncate(Rgba.g * 255), round_truncate(Rgba.b * 255), Rgba.Pack(false)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - if(pData->m_Alpha) - { - str_format(aBuf, sizeof(aBuf), "A: %d%%", round_truncate(Hsla.a * 100)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - } - } - } - - void Register() override - { - m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); - } - - bool IsDefault() const override - { - return *m_pVariable == m_Default; - } - - void Serialize(char *pOut, size_t Size, unsigned Value) const - { - str_format(pOut, Size, "%s %u", m_pScriptName, Value); - } - - void Serialize(char *pOut, size_t Size) const override - { - Serialize(pOut, Size, *m_pVariable); - } - - void SetValue(unsigned Value) - { - if(CheckReadOnly()) - return; - char aBuf[IConsole::CMDLINE_LENGTH]; - Serialize(aBuf, sizeof(aBuf), Value); - ExecuteLine(aBuf); - } - - void ResetToDefault() override - { - SetValue(m_Default); - } - - void ResetToOld() override - { - *m_pVariable = m_OldValue; - } + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, unsigned Value) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(unsigned Value); + void ResetToDefault() override; + void ResetToOld() override; }; struct SStringConfigVariable : public SConfigVariable @@ -316,102 +166,17 @@ struct SStringConfigVariable : public SConfigVariable size_t m_MaxSize; char *m_pOldValue; - SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pStr(pStr), - m_pDefault(pDefault), - m_MaxSize(MaxSize), - m_pOldValue(pOldValue) - { - str_copy(m_pStr, m_pDefault, m_MaxSize); - str_copy(m_pOldValue, m_pDefault, m_MaxSize); - } - + SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue); ~SStringConfigVariable() override = default; - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) - { - SStringConfigVariable *pData = static_cast(pUserData); - - if(pResult->NumArguments()) - { - if(pData->CheckReadOnly()) - return; - - const char *pString = pResult->GetString(0); - if(!str_utf8_check(pString)) - { - char aTemp[4]; - size_t Length = 0; - while(*pString) - { - size_t Size = str_utf8_encode(aTemp, static_cast(*pString++)); - if(Length + Size < pData->m_MaxSize) - { - mem_copy(pData->m_pStr + Length, aTemp, Size); - Length += Size; - } - else - break; - } - pData->m_pStr[Length] = '\0'; - } - else - str_copy(pData->m_pStr, pString, pData->m_MaxSize); - - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) - str_copy(pData->m_pOldValue, pData->m_pStr, pData->m_MaxSize); - } - else - { - char aBuf[1024]; - str_format(aBuf, sizeof(aBuf), "Value: %s", pData->m_pStr); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - } - } - - void Register() override - { - m_pConsole->Register(m_pScriptName, "?r", m_Flags, CommandCallback, this, m_pHelp); - } - - bool IsDefault() const override - { - return str_comp(m_pStr, m_pDefault) == 0; - } - - void Serialize(char *pOut, size_t Size, const char *pValue) const - { - str_copy(pOut, m_pScriptName, Size); - str_append(pOut, " \"", Size); - const int OutLen = str_length(pOut); - EscapeParam(pOut + OutLen, pValue, Size - OutLen - 1); // -1 to ensure space for final quote - str_append(pOut, "\"", Size); - } - - void Serialize(char *pOut, size_t Size) const override - { - Serialize(pOut, Size, m_pStr); - } - - void SetValue(const char *pValue) - { - if(CheckReadOnly()) - return; - char aBuf[2048]; - Serialize(aBuf, sizeof(aBuf), pValue); - ExecuteLine(aBuf); - } - - void ResetToDefault() override - { - SetValue(m_pDefault); - } - - void ResetToOld() override - { - str_copy(m_pStr, m_pOldValue, m_MaxSize); - } + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, const char *pValue) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(const char *pValue); + void ResetToDefault() override; + void ResetToOld() override; }; class CConfigManager : public IConfigManager diff --git a/src/engine/textrender.h b/src/engine/textrender.h index 13a176531..067ab9062 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -147,7 +147,7 @@ MAYBE_UNUSED static const char *FONT_ICON_UNDO = "\xEF\x8B\xAA"; MAYBE_UNUSED static const char *FONT_ICON_REDO = "\xEF\x8B\xB9"; MAYBE_UNUSED static const char *FONT_ICON_ARROWS_ROTATE = "\xEF\x80\xA1"; -MAYBE_UNUSED static const char *FONT_ICON_QUESTION = "\x3F"; +MAYBE_UNUSED static const char *FONT_ICON_QUESTION = "?"; } // end namespace FontIcons enum ETextCursorSelectionMode diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 0e42b22b7..f03267982 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -779,7 +779,8 @@ bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUse if(pEditor->Load(pFileName, StorageType)) { pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder || (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentLink && str_comp(pEditor->m_aFileDialogCurrentLink, "themes") == 0)); - pEditor->m_Dialog = DIALOG_NONE; + if(pEditor->m_Dialog == DIALOG_FILE) + pEditor->m_Dialog = DIALOG_NONE; return true; } else @@ -8038,6 +8039,12 @@ void CEditor::Render() UI()->SetHotItem(&s_NullUiTarget); RenderFileDialog(); } + else if(m_Dialog == DIALOG_MAPSETTINGS_ERROR) + { + static int s_NullUiTarget = 0; + UI()->SetHotItem(&s_NullUiTarget); + RenderMapSettingsErrorDialog(); + } if(m_PopupEventActivated) { diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index e051656e5..243af6d1c 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -60,6 +60,7 @@ enum DIALOG_NONE = 0, DIALOG_FILE, + DIALOG_MAPSETTINGS_ERROR }; class CEditorImage; @@ -122,16 +123,7 @@ public: CMapInfo m_MapInfo; CMapInfo m_MapInfoTmp; - struct CSetting - { - char m_aCommand[256]; - - CSetting(const char *pCommand) - { - str_copy(m_aCommand, pCommand); - } - }; - std::vector m_vSettings; + std::vector m_vSettings; std::shared_ptr m_pGameLayer; std::shared_ptr m_pGameGroup; @@ -981,6 +973,7 @@ public: void RenderEnvelopeEditor(CUIRect View); + void RenderMapSettingsErrorDialog(); void RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast); static void MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits); diff --git a/src/game/editor/editor_actions.cpp b/src/game/editor/editor_actions.cpp index 9c63b8237..8f37233c6 100644 --- a/src/game/editor/editor_actions.cpp +++ b/src/game/editor/editor_actions.cpp @@ -1305,7 +1305,6 @@ void CEditorCommandAction::Undo() } case EType::EDIT: { - printf("Restoring %s\n", m_PreviousCommand.c_str()); str_copy(Map.m_vSettings[m_CommandIndex].m_aCommand, m_PreviousCommand.c_str()); *m_pSelectedCommandIndex = m_CommandIndex; break; diff --git a/src/game/editor/editor_server_settings.cpp b/src/game/editor/editor_server_settings.cpp index 25b533001..948f2e488 100644 --- a/src/game/editor/editor_server_settings.cpp +++ b/src/game/editor/editor_server_settings.cpp @@ -1,4 +1,4 @@ -#include "editor_server_settings.h" +#include "editor_server_settings.h" #include "editor.h" #include @@ -268,7 +268,7 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd if(s_CommandSelectedIndex != NewSelected || s_ListBox.WasItemSelected()) { s_CommandSelectedIndex = NewSelected; - if(m_SettingsCommandInput.IsEmpty() || Input()->ModifierIsPressed()) // Allow ctrl+click to fill the input even if empty + if(m_SettingsCommandInput.IsEmpty() || !Input()->ModifierIsPressed()) // Allow ctrl+click to only change selection { m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); m_MapSettingsCommandContext.Update(); @@ -288,6 +288,7 @@ void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, cons auto *pLineInput = pContext->LineInput(); auto &Context = *pContext; + Context.SetFontSize(FontSize); // Set current active context if input is active if(pLineInput->IsActive()) @@ -540,6 +541,464 @@ int CEditor::RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect V return -1; } +void CEditor::RenderMapSettingsErrorDialog() +{ + auto &LoadedMapSettings = m_MapSettingsBackend.m_LoadedMapSettings; + auto &vSettingsInvalid = LoadedMapSettings.m_vSettingsInvalid; + auto &vSettingsValid = LoadedMapSettings.m_vSettingsValid; + auto &SettingsDuplicate = LoadedMapSettings.m_SettingsDuplicate; + + UI()->MapScreen(); + CUIRect Overlay = *UI()->Screen(); + + Overlay.Draw(ColorRGBA(0, 0, 0, 0.33f), IGraphics::CORNER_NONE, 0.0f); + CUIRect Background; + Overlay.VMargin(150.0f, &Background); + Background.HMargin(50.0f, &Background); + Background.Draw(ColorRGBA(0, 0, 0, 0.80f), IGraphics::CORNER_ALL, 5.0f); + + CUIRect View; + Background.Margin(10.0f, &View); + + CUIRect Title, ButtonBar, Label; + View.HSplitTop(18.0f, &Title, &View); + View.HSplitTop(5.0f, nullptr, &View); // some spacing + View.HSplitBottom(18.0f, &View, &ButtonBar); + View.HSplitBottom(10.0f, &View, nullptr); // some spacing + + // title bar + Title.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 4.0f); + Title.VMargin(10.0f, &Title); + UI()->DoLabel(&Title, "Map settings error", 12.0f, TEXTALIGN_ML); + + // Render body + { + static CLineInputBuffered<256> s_Input; + static CMapSettingsBackend::CContext s_Context = m_MapSettingsBackend.NewContext(&s_Input); + + // Some text + SLabelProperties Props; + CUIRect Text; + View.HSplitTop(30.0f, &Text, &View); + Props.m_MaxWidth = Text.w; + UI()->DoLabel(&Text, "Below is a report of the invalid map settings found when loading the map. Please fix them before proceeding further.", 10.0f, TEXTALIGN_MC, Props); + + // Mixed list + CUIRect List = View; + View.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 3.0f); + + const float RowHeight = 18.0f; + static CScrollRegion s_ScrollRegion; + vec2 ScrollOffset(0.0f, 0.0f); + CScrollRegionParams ScrollParams; + ScrollParams.m_ScrollUnit = 120.0f; + s_ScrollRegion.Begin(&List, &ScrollOffset, &ScrollParams); + const float EndY = List.y + List.h; + List.y += ScrollOffset.y; + + List.HSplitTop(20.0f, nullptr, &List); + + static int s_FixingCommandIndex = -1; + + auto &&SetInput = [&](const char *pString) { + s_Input.Set(pString); + s_Context.Update(); + s_Context.UpdateCursor(true); + UI()->SetActiveItem(&s_Input); + }; + + CUIRect FixInput; + bool DisplayFixInput = false; + float DropdownHeight = 110.0f; + + for(int i = 0; i < (int)m_Map.m_vSettings.size(); i++) + { + CUIRect Slot; + + auto pInvalidSetting = std::find_if(vSettingsInvalid.begin(), vSettingsInvalid.end(), [i](const SInvalidSetting &Setting) { return Setting.m_Index == i; }); + if(pInvalidSetting != vSettingsInvalid.end()) + { // This setting is invalid, only display it if its not a duplicate + if(!(pInvalidSetting->m_Type & SInvalidSetting::TYPE_DUPLICATE)) + { + bool IsFixing = s_FixingCommandIndex == i; + List.HSplitTop(RowHeight, &Slot, &List); + + // Draw a reddish background if setting is marked as deleted + if(pInvalidSetting->m_Context.m_Deleted) + Slot.Draw(ColorRGBA(0.85f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_ALL, 3.0f); + + Slot.VMargin(5.0f, &Slot); + Slot.HMargin(1.0f, &Slot); + + if(!IsFixing && !pInvalidSetting->m_Context.m_Fixed) + { // Display "Fix" and "delete" buttons if we're not fixing the command and the command has not been fixed + CUIRect FixBtn, DelBtn; + Slot.VSplitRight(30.0f, &Slot, &DelBtn); + Slot.VSplitRight(5.0f, &Slot, nullptr); + DelBtn.HMargin(1.0f, &DelBtn); + + Slot.VSplitRight(30.0f, &Slot, &FixBtn); + Slot.VSplitRight(10.0f, &Slot, nullptr); + FixBtn.HMargin(1.0f, &FixBtn); + + // Delete button + if(DoButton_FontIcon(&pInvalidSetting->m_Context.m_Deleted, FONT_ICON_TRASH, pInvalidSetting->m_Context.m_Deleted, &DelBtn, 0, "Delete this command", IGraphics::CORNER_ALL, 10.0f)) + pInvalidSetting->m_Context.m_Deleted = !pInvalidSetting->m_Context.m_Deleted; + + // Fix button + if(DoButton_Editor(&pInvalidSetting->m_Context.m_Fixed, "Fix", !pInvalidSetting->m_Context.m_Deleted ? (s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1)) : -1, &FixBtn, 0, "Fix this command")) + { + s_FixingCommandIndex = i; + SetInput(pInvalidSetting->m_aSetting); + } + } + else if(IsFixing) + { // If we're fixing this command, then display "Done" and "Cancel" buttons + // Also setup the input rect + CUIRect OkBtn, CancelBtn; + Slot.VSplitRight(50.0f, &Slot, &CancelBtn); + Slot.VSplitRight(5.0f, &Slot, nullptr); + CancelBtn.HMargin(1.0f, &CancelBtn); + + Slot.VSplitRight(30.0f, &Slot, &OkBtn); + Slot.VSplitRight(10.0f, &Slot, nullptr); + OkBtn.HMargin(1.0f, &OkBtn); + + // Buttons + static int s_Cancel = 0, s_Ok = 0; + if(DoButton_Editor(&s_Cancel, "Cancel", 0, &CancelBtn, 0, "Cancel fixing this command") || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + { + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + + // "Done" button only enabled if the fixed setting is valid + // For that we use a local CContext s_Context and use it to check + // that the setting is valid and that it is not a duplicate + ECollisionCheckResult Res = ECollisionCheckResult::ERROR; + s_Context.CheckCollision(vSettingsValid, Res); + bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::ADD; + + if(DoButton_Editor(&s_Ok, "Done", Valid ? 0 : -1, &OkBtn, 0, "Confirm edition of this command") || (s_Input.IsActive() && Valid && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + { + if(Valid) // Just to make sure + { + // Mark the setting is being fixed + pInvalidSetting->m_Context.m_Fixed = true; + str_copy(pInvalidSetting->m_aSetting, s_Input.GetString()); + // Add it to the list for future collision checks + vSettingsValid.emplace_back(s_Input.GetString()); + + // Clear the input & fixing command index + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + } + } + + Label = Slot; + Props.m_EllipsisAtEnd = true; + Props.m_MaxWidth = Label.w; + + if(IsFixing) + { + // Setup input rect, which will be used to draw the map settings input later + Label.HMargin(1.0, &FixInput); + DisplayFixInput = true; + DropdownHeight = minimum(DropdownHeight, EndY - FixInput.y - 16.0f); + } + else + { + // Draw label in case we're not fixing this setting. + // Deleted settings are shown in gray with a red line through them + // Fixed settings are shown in green + // Invalid settings are shown in red + if(!pInvalidSetting->m_Context.m_Deleted) + { + if(pInvalidSetting->m_Context.m_Fixed) + TextRender()->TextColor(0.0f, 1.0f, 0.0f, 1.0f); + else + TextRender()->TextColor(1.0f, 0.0f, 0.0f, 1.0f); + UI()->DoLabel(&Label, pInvalidSetting->m_aSetting, 10.0f, TEXTALIGN_ML, Props); + } + else + { + TextRender()->TextColor(0.3f, 0.3f, 0.3f, 1.0f); + UI()->DoLabel(&Label, pInvalidSetting->m_aSetting, 10.0f, TEXTALIGN_ML, Props); + + CUIRect Line = Label; + Line.y = Label.y + Label.h / 2; + Line.h = 1; + Line.Draw(ColorRGBA(1, 0, 0, 1), IGraphics::CORNER_NONE, 0.0f); + } + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + } + else + { // This setting is valid + // Check for duplicates + const std::vector &vDuplicates = SettingsDuplicate.at(i); + int Chosen = -1; // This is the chosen duplicate setting. -1 means the first valid setting that was found which was not a duplicate + for(int d = 0; d < (int)vDuplicates.size(); d++) + { + int DupIndex = vDuplicates[d]; + if(vSettingsInvalid[DupIndex].m_Context.m_Chosen) + { + Chosen = d; + break; + } + } + + List.HSplitTop(RowHeight * (vDuplicates.size() + 1) + 2.0f, &Slot, &List); + Slot.HMargin(1.0f, &Slot); + + // Draw a background to highlight group of duplicates + if(!vDuplicates.empty()) + Slot.Draw(ColorRGBA(1, 1, 1, 0.15f), IGraphics::CORNER_ALL, 3.0f); + + Slot.VMargin(5.0f, &Slot); + Slot.HSplitTop(RowHeight, &Label, &Slot); + Label.HMargin(1.0f, &Label); + + // Draw a "choose" button next to the label in case we have duplicates for this line + if(!vDuplicates.empty()) + { + CUIRect ChooseBtn; + Label.VSplitRight(50.0f, &Label, &ChooseBtn); + Label.VSplitRight(5.0f, &Label, nullptr); + ChooseBtn.HMargin(1.0f, &ChooseBtn); + if(DoButton_Editor(&vDuplicates, "Choose", Chosen == -1, &ChooseBtn, 0, "Choose this command")) + { + if(Chosen != -1) + vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; + Chosen = -1; // Choosing this means that we do not choose any of the duplicates + } + } + + // Draw the label + Props.m_MaxWidth = Label.w; + UI()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); + + // Draw the list of duplicates, with a "Choose" button for each duplicate + // In case a duplicate is also invalid, then we draw a "Fix" button which behaves like the fix button above + // Duplicate settings name are shown in light blue, or in purple if they are also invalid + Slot.VSplitLeft(10.0f, nullptr, &Slot); + for(int DuplicateIndex = 0; DuplicateIndex < (int)vDuplicates.size(); DuplicateIndex++) + { + auto &Duplicate = vSettingsInvalid.at(vDuplicates[DuplicateIndex]); + bool IsFixing = s_FixingCommandIndex == Duplicate.m_Index; + bool IsInvalid = Duplicate.m_Type & SInvalidSetting::TYPE_INVALID; + + ColorRGBA Color(0.329f, 0.714f, 0.859f, 1.0f); + CUIRect SubSlot; + Slot.HSplitTop(RowHeight, &SubSlot, &Slot); + SubSlot.HMargin(1.0f, &SubSlot); + + if(!IsFixing) + { + // If not fixing, then display "Choose" and maybe "Fix" buttons. + + CUIRect ChooseBtn; + SubSlot.VSplitRight(50.0f, &SubSlot, &ChooseBtn); + SubSlot.VSplitRight(5.0f, &SubSlot, nullptr); + ChooseBtn.HMargin(1.0f, &ChooseBtn); + if(DoButton_Editor(&Duplicate.m_Context.m_Chosen, "Choose", IsInvalid && !Duplicate.m_Context.m_Fixed ? -1 : Duplicate.m_Context.m_Chosen, &ChooseBtn, 0, "Override with this command")) + { + Duplicate.m_Context.m_Chosen = !Duplicate.m_Context.m_Chosen; + if(Chosen != -1 && Chosen != DuplicateIndex) + vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; + Chosen = DuplicateIndex; + } + + if(IsInvalid) + { + if(!Duplicate.m_Context.m_Fixed) + { + Color = ColorRGBA(1, 0, 1, 1); + CUIRect FixBtn; + SubSlot.VSplitRight(30.0f, &SubSlot, &FixBtn); + SubSlot.VSplitRight(10.0f, &SubSlot, nullptr); + FixBtn.HMargin(1.0f, &FixBtn); + if(DoButton_Editor(&Duplicate.m_Context.m_Fixed, "Fix", s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1), &FixBtn, 0, "Fix this command (needed before it can be chosen)")) + { + s_FixingCommandIndex = Duplicate.m_Index; + SetInput(Duplicate.m_aSetting); + } + } + else + { + Color = ColorRGBA(0.329f, 0.714f, 0.859f, 1.0f); + } + } + } + else + { + // If we're fixing, display "Done" and "Cancel" buttons + CUIRect OkBtn, CancelBtn; + SubSlot.VSplitRight(50.0f, &SubSlot, &CancelBtn); + SubSlot.VSplitRight(5.0f, &SubSlot, nullptr); + CancelBtn.HMargin(1.0f, &CancelBtn); + + SubSlot.VSplitRight(30.0f, &SubSlot, &OkBtn); + SubSlot.VSplitRight(10.0f, &SubSlot, nullptr); + OkBtn.HMargin(1.0f, &OkBtn); + + static int s_Cancel = 0, s_Ok = 0; + if(DoButton_Editor(&s_Cancel, "Cancel", 0, &CancelBtn, 0, "Cancel fixing this command") || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + { + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + + // Use the local CContext s_Context to validate the input + // We also need to make sure the fixed setting matches the initial duplicate setting + // For example: + // sv_deepfly 0 + // sv_deepfly 5 <- This is invalid and duplicate. We can only fix it by writing "sv_deepfly 0" or "sv_deepfly 1". + // If we write any other setting, like "sv_hit 1", it won't work as it does not match "sv_deepfly". + // To do that, we use the context and we check for collision with the current map setting + ECollisionCheckResult Res = ECollisionCheckResult::ERROR; + s_Context.CheckCollision({m_Map.m_vSettings[i]}, Res); + bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::REPLACE; + + if(DoButton_Editor(&s_Ok, "Done", Valid ? 0 : -1, &OkBtn, 0, "Confirm edition of this command") || (s_Input.IsActive() && Valid && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + { + if(Valid) // Just to make sure + { + // Mark the setting as fixed + Duplicate.m_Context.m_Fixed = true; + str_copy(Duplicate.m_aSetting, s_Input.GetString()); + + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + } + } + + Label = SubSlot; + Props.m_MaxWidth = Label.w; + + if(IsFixing) + { + // Setup input rect in case we are fixing the setting + Label.HMargin(1.0, &FixInput); + DisplayFixInput = true; + DropdownHeight = minimum(DropdownHeight, EndY - FixInput.y - 16.0f); + } + else + { + // Otherwise, render the setting label + TextRender()->TextColor(Color); + UI()->DoLabel(&Label, Duplicate.m_aSetting, 10.0f, TEXTALIGN_ML, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + } + } + + // Finally, add the slot to the scroll region + s_ScrollRegion.AddRect(Slot); + } + + // Add some padding to the bottom so the dropdown can actually display some values in case we + // fix an invalid setting at the bottom of the list + CUIRect PaddingBottom; + List.HSplitTop(30.0f, &PaddingBottom, &List); + s_ScrollRegion.AddRect(PaddingBottom); + + // Display the map settings edit box after having rendered all the lines, so the dropdown shows in + // front of everything, but is still being clipped by the scroll region. + if(DisplayFixInput) + DoMapSettingsEditBox(&s_Context, &FixInput, 10.0f, maximum(DropdownHeight, 30.0f)); + + s_ScrollRegion.End(); + } + + // Confirm button + static int s_ConfirmButton = 0, s_CancelButton = 0; + CUIRect ConfimButton, CancelButton; + ButtonBar.VSplitLeft(110.0f, &CancelButton, &ButtonBar); + ButtonBar.VSplitRight(110.0f, &ButtonBar, &ConfimButton); + + bool CanConfirm = true; + for(auto &InvalidSetting : vSettingsInvalid) + { + if(!InvalidSetting.m_Context.m_Fixed && !InvalidSetting.m_Context.m_Deleted && !(InvalidSetting.m_Type & SInvalidSetting::TYPE_DUPLICATE)) + { + CanConfirm = false; + break; + } + } + + auto &&Execute = [&]() { + // Execute will modify the actual map settings according to the fixes that were just made within the dialog. + + // Fix fixed settings, erase deleted settings + for(auto &FixedSetting : vSettingsInvalid) + { + if(FixedSetting.m_Context.m_Fixed) + { + str_copy(m_Map.m_vSettings[FixedSetting.m_Index].m_aCommand, FixedSetting.m_aSetting); + } + } + + // Choose chosen settings + // => Erase settings that don't match + // => Erase settings that were not chosen + std::vector vSettingsToErase; + for(auto &Setting : vSettingsInvalid) + { + if(Setting.m_Type & SInvalidSetting::TYPE_DUPLICATE) + { + if(!Setting.m_Context.m_Chosen) + vSettingsToErase.emplace_back(Setting.m_aSetting); + else + vSettingsToErase.emplace_back(m_Map.m_vSettings[Setting.m_CollidingIndex].m_aCommand); + } + } + + // Erase deleted settings + for(auto &DeletedSetting : vSettingsInvalid) + { + if(DeletedSetting.m_Context.m_Deleted) + { + m_Map.m_vSettings.erase( + std::remove_if(m_Map.m_vSettings.begin(), m_Map.m_vSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, DeletedSetting.m_aSetting) == 0; + }), + m_Map.m_vSettings.end()); + } + } + + // Erase settings to erase + for(auto &Setting : vSettingsToErase) + { + m_Map.m_vSettings.erase( + std::remove_if(m_Map.m_vSettings.begin(), m_Map.m_vSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, Setting.m_aCommand) == 0; + }), + m_Map.m_vSettings.end()); + } + + m_Map.OnModify(); + }; + + // Confirm - execute the fixes + if(DoButton_Editor(&s_ConfirmButton, "Confirm", CanConfirm ? 0 : -1, &ConfimButton, 0, nullptr) || (CanConfirm && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + { + Execute(); + m_Dialog = DIALOG_NONE; + } + + // Cancel - we load a new empty map + if(DoButton_Editor(&s_CancelButton, "Cancel", 0, &CancelButton, 0, nullptr) || (UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE))) + { + Reset(); + m_aFileName[0] = 0; + m_Dialog = DIALOG_NONE; + } +} + void CEditor::MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits) { // Check the match argument index. @@ -661,8 +1120,7 @@ void CMapSettingsBackend::LoadSettingCommand(const std::shared_ptr= sizeof(SParsedMapSettingArg::m_aName)) - dbg_msg("editor", "Warning: length of server setting name exceeds limit."); + dbg_assert(Len + 1 < sizeof(SParsedMapSettingArg::m_aName), "Length of server setting name exceeds limit."); // Append parsed arg m_ParsedCommandArgs[pSetting].emplace_back(); @@ -739,6 +1197,11 @@ void CMapSettingsBackend::CContext::Reset() } void CMapSettingsBackend::CContext::Update() +{ + UpdateFromString(InputString()); +} + +void CMapSettingsBackend::CContext::UpdateFromString(const char *pStr) { // This is the main method that does all the argument parsing and validating. // It fills pretty much all the context values, the arguments, their position, @@ -747,7 +1210,6 @@ void CMapSettingsBackend::CContext::Update() m_pCurrentSetting = nullptr; m_vCurrentArgs.clear(); - const char *pStr = InputString(); const char *pIterator = pStr; // Get the command/setting @@ -768,10 +1230,10 @@ void CMapSettingsBackend::CContext::Update() } // Parse args - ParseArgs(pIterator); + ParseArgs(InputString(), pIterator); } -void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) +void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const char *pStr) { // This method parses the arguments of the current command, starting at pStr @@ -789,7 +1251,6 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) ClearError(); const char *pIterator = pStr; - const char *pLineInputStr = InputString(); if(!pStr || *pStr == '\0') return; // No arguments @@ -864,8 +1325,8 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) // Also keep track of the visual X position of each argument within the input float PosX = 0; - const float WW = m_pBackend->TextRender()->TextWidth(FONT_SIZE, " "); - PosX += m_pBackend->TextRender()->TextWidth(FONT_SIZE, m_aCommand); + const float WW = m_pBackend->TextRender()->TextWidth(m_FontSize, " "); + PosX += m_pBackend->TextRender()->TextWidth(m_FontSize, m_aCommand); // Parsing beings while(*pIterator) @@ -969,12 +1430,15 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) size_t ErrorArgIndex = m_vCurrentArgs.size() - 1; SCurrentSettingArg &ErrorArg = m_vCurrentArgs.back(); SParsedMapSettingArg &SettingArg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); + char aFormattedValue[256]; + FormatDisplayValue(ErrorArg.m_aValue, aFormattedValue); + if(Error == ERROR_INVALID_VALUE || Error == ERROR_UNKNOWN_VALUE) - str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "%s argument value: %s at position %d for argument '%s'", Error == ERROR_INVALID_VALUE ? "Invalid" : "Unknown", ErrorArg.m_aValue, (int)ErrorArg.m_Start, SettingArg.m_aName); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "%s argument value: %s at position %d for argument '%s'", Error == ERROR_INVALID_VALUE ? "Invalid" : "Unknown", aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName); else { std::shared_ptr pSettingInt = std::static_pointer_cast(m_pCurrentSetting); - str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Invalid argument value: %s at position %d for argument '%s': out of range [%d, %d]", ErrorArg.m_aValue, (int)ErrorArg.m_Start, SettingArg.m_aName, pSettingInt->m_Min, pSettingInt->m_Max); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Invalid argument value: %s at position %d for argument '%s': out of range [%d, %d]", aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName, pSettingInt->m_Min, pSettingInt->m_Max); } m_Error.m_ArgIndex = (int)ErrorArgIndex; break; @@ -989,13 +1453,15 @@ void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) } else if(!m_AllowUnknownCommands) { - str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", m_aCommand); + char aFormattedValue[256]; + FormatDisplayValue(m_aCommand, aFormattedValue); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", aFormattedValue); m_Error.m_ArgIndex = -1; break; } } - PosX += m_pBackend->TextRender()->TextWidth(FONT_SIZE, pArgStart, Length); // Advance argument position + PosX += m_pBackend->TextRender()->TextWidth(m_FontSize, pArgStart, Length); // Advance argument position ArgIndex++; } } @@ -1013,6 +1479,9 @@ bool CMapSettingsBackend::CContext::UpdateCursor(bool Force) // and the possible values matches if the argument index changes. // Returns true in case the cursor changed position + if(!m_pLineInput) + return false; + size_t Offset = m_pLineInput->GetCursorOffset(); if(Offset == m_LastCursorOffset && !Force) return false; @@ -1142,7 +1611,9 @@ void CMapSettingsBackend::CContext::UpdatePossibleMatches() if(m_vPossibleMatches.empty() && !m_AllowUnknownCommands) { // Fill the error if we do not allow unknown commands - str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", m_aCommand); + char aFormattedValue[256]; + FormatDisplayValue(m_aCommand, aFormattedValue); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", aFormattedValue); m_Error.m_ArgIndex = -1; } } @@ -1202,6 +1673,9 @@ void CMapSettingsBackend::CContext::UpdatePossibleMatches() bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event) { + if(!m_pLineInput) + return false; + if(!m_pLineInput->IsActive()) return false; @@ -1227,42 +1701,11 @@ bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event) const char *CMapSettingsBackend::CContext::InputString() const { + if(!m_pLineInput) + return nullptr; return m_pBackend->Input()->HasComposition() ? m_CompositionStringBuffer.c_str() : m_pLineInput->GetString(); } -void CMapSettingsBackend::CContext::UpdateCompositionString() -{ - const bool HasComposition = m_pBackend->Input()->HasComposition(); - - if(HasComposition) - { - const size_t CursorOffset = m_pLineInput->GetCursorOffset(); - const size_t DisplayCursorOffset = m_pLineInput->OffsetFromActualToDisplay(CursorOffset); - const std::string DisplayStr = std::string(m_pLineInput->GetString()); - std::string CompositionBuffer = DisplayStr.substr(0, DisplayCursorOffset) + m_pBackend->Input()->GetComposition() + DisplayStr.substr(DisplayCursorOffset); - if(CompositionBuffer != m_CompositionStringBuffer) - { - m_CompositionStringBuffer = CompositionBuffer; - Update(); - UpdateCursor(); - } - } -} - -bool CMapSettingsBackend::OnInput(const IInput::CEvent &Event) -{ - if(ms_pActiveContext) - return ms_pActiveContext->OnInput(Event); - - return false; -} - -void CMapSettingsBackend::OnUpdate() -{ - if(ms_pActiveContext && ms_pActiveContext->m_pLineInput->IsActive()) - ms_pActiveContext->UpdateCompositionString(); -} - const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentStringColor = ColorRGBA(84 / 255.0f, 1.0f, 1.0f, 1.0f); const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentNumberColor = ColorRGBA(0.1f, 0.9f, 0.05f, 1.0f); const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentUnknownColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); @@ -1290,7 +1733,7 @@ void CMapSettingsBackend::CContext::ColorArguments(std::vector vColorSplits.emplace_back(Argument.m_Start, Argument.m_End - Argument.m_Start, Color); } - if(!m_pLineInput->IsEmpty()) + if(m_pLineInput && !m_pLineInput->IsEmpty()) { if(!CommandIsValid() && !m_AllowUnknownCommands) { @@ -1306,6 +1749,16 @@ void CMapSettingsBackend::CContext::ColorArguments(std::vector } int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) const +{ + return CheckCollision(m_pBackend->Editor()->m_Map.m_vSettings, Result); +} + +int CMapSettingsBackend::CContext::CheckCollision(const std::vector &vSettings, ECollisionCheckResult &Result) const +{ + return CheckCollision(InputString(), vSettings, Result); +} + +int CMapSettingsBackend::CContext::CheckCollision(const char *pInputString, const std::vector &vSettings, ECollisionCheckResult &Result) const { // Checks for a collision with the current map settings. // A collision is when a setting with the same arguments already exists and that it can't be added multiple times. @@ -1315,7 +1768,7 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) // This method CheckCollision(ECollisionCheckResult&) returns an integer which is the index of the colliding line. If no // colliding line was found, then it returns -1. - const char *pInputString = InputString(); + const int InputLength = str_length(pInputString); struct SArgument { @@ -1332,8 +1785,6 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) std::vector m_vArgs; }; - auto &vSettings = m_pBackend->Editor()->m_Map.m_vSettings; - // For now we split each map setting corresponding to the setting we want to add by spaces auto &&SplitSetting = [](const char *pStr) { std::vector vaArgs; @@ -1355,7 +1806,7 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) if(!m_AllowUnknownCommands) return -1; - if(m_pLineInput->GetLength() == 0) + if(InputLength == 0) return -1; // If we get here, it means we allow unknown commands. @@ -1395,7 +1846,7 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) // can have is REPLACE. // In this case, the collision is found only by checking the command name for every setting in the current map settings. char aBuffer[256]; - auto It = std::find_if(vSettings.begin(), vSettings.end(), [&](const CEditorMap::CSetting &Setting) { + auto It = std::find_if(vSettings.begin(), vSettings.end(), [&](const CEditorMapSetting &Setting) { const char *pLineSettingValue = Setting.m_aCommand; // Get the map setting command pLineSettingValue = str_next_token(pLineSettingValue, " ", aBuffer, sizeof(aBuffer)); // Get the first token before the first space return str_comp_nocase(aBuffer, pSetting->m_pName) == 0; // Check if that equals our current command @@ -1427,10 +1878,10 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) std::shared_ptr pSettingCommand = std::static_pointer_cast(pSetting); // Get matching lines for that command - std::vector vvArgs; + std::vector vLineArgs; for(int i = 0; i < (int)vSettings.size(); i++) { - auto &Setting = vSettings.at(i); + const auto &Setting = vSettings.at(i); // Split this setting into its arguments std::vector vArgs = SplitSetting(Setting.m_aCommand); @@ -1439,7 +1890,7 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) { // When that's the case, we save them vArgs.erase(vArgs.begin()); - vvArgs.push_back(SLineArgs{ + vLineArgs.push_back(SLineArgs{ i, vArgs, }); @@ -1453,7 +1904,7 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) { bool Collide = false; const char *pValue = Arg(ArgIndex).m_aValue; - for(auto &Line : vvArgs) + for(auto &Line : vLineArgs) { // Check first colliding line if(str_comp_nocase(pValue, Line.m_vArgs[ArgIndex].m_aValue) == 0) @@ -1470,6 +1921,13 @@ int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) // (or if we had an error) if(!Collide || Error) break; + + // Otherwise, remove non-colliding args from the list + vLineArgs.erase( + std::remove_if(vLineArgs.begin(), vLineArgs.end(), [&](const SLineArgs &Line) { + return str_comp_nocase(pValue, Line.m_vArgs[ArgIndex].m_aValue) != 0; + }), + vLineArgs.end()); } // The result is either REPLACE when we found a collision, or ADD @@ -1496,8 +1954,7 @@ bool CMapSettingsBackend::CContext::Valid() const return false; // Check that we have the same number of arguments - const bool ArgCountValid = m_vCurrentArgs.size() == m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size(); - return ArgCountValid; + return m_vCurrentArgs.size() == m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size(); } else { @@ -1515,6 +1972,127 @@ void CMapSettingsBackend::CContext::GetCommandHelpText(char *pStr, int Length) c str_copy(pStr, m_pCurrentSetting->m_pHelp, Length); } +void CMapSettingsBackend::CContext::UpdateCompositionString() +{ + if(!m_pLineInput) + return; + + const bool HasComposition = m_pBackend->Input()->HasComposition(); + + if(HasComposition) + { + const size_t CursorOffset = m_pLineInput->GetCursorOffset(); + const size_t DisplayCursorOffset = m_pLineInput->OffsetFromActualToDisplay(CursorOffset); + const std::string DisplayStr = std::string(m_pLineInput->GetString()); + std::string CompositionBuffer = DisplayStr.substr(0, DisplayCursorOffset) + m_pBackend->Input()->GetComposition() + DisplayStr.substr(DisplayCursorOffset); + if(CompositionBuffer != m_CompositionStringBuffer) + { + m_CompositionStringBuffer = CompositionBuffer; + Update(); + UpdateCursor(); + } + } +} + +template +void CMapSettingsBackend::CContext::FormatDisplayValue(const char *pValue, char (&aOut)[N]) +{ + const int MaxLength = 32; + if(str_length(pValue) > MaxLength) + { + str_copy(aOut, pValue, MaxLength); + str_append(aOut, "..."); + } + else + { + str_copy(aOut, pValue); + } +} + +bool CMapSettingsBackend::OnInput(const IInput::CEvent &Event) +{ + if(ms_pActiveContext) + return ms_pActiveContext->OnInput(Event); + + return false; +} + +void CMapSettingsBackend::OnUpdate() +{ + if(ms_pActiveContext && ms_pActiveContext->m_pLineInput && ms_pActiveContext->m_pLineInput->IsActive()) + ms_pActiveContext->UpdateCompositionString(); +} + +void CMapSettingsBackend::OnMapLoad() +{ + // Load & validate all map settings + m_LoadedMapSettings.Reset(); + + auto &vLoadedMapSettings = Editor()->m_Map.m_vSettings; + + // Keep a vector of valid map settings, to check collision against: m_vValidLoadedMapSettings + + // Create a local context with no lineinput, only used to parse the commands + CContext LocalContext = NewContext(nullptr); + + // Iterate through map settings + // Two steps: + // 1. Save valid and invalid settings + // 2. Check for duplicates + + std::vector> vSettingsInvalid; + + for(int i = 0; i < (int)vLoadedMapSettings.size(); i++) + { + CEditorMapSetting &Setting = vLoadedMapSettings.at(i); + // Parse the setting using the context + LocalContext.UpdateFromString(Setting.m_aCommand); + + bool Valid = LocalContext.Valid(); + ECollisionCheckResult Result = ECollisionCheckResult::ERROR; + LocalContext.CheckCollision(Setting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid, Result); + + if(Valid && Result == ECollisionCheckResult::ADD) + m_LoadedMapSettings.m_vSettingsValid.emplace_back(Setting); + else + vSettingsInvalid.emplace_back(i, Valid, Setting); + + LocalContext.Reset(); + + // Empty duplicates for this line, might be filled later + m_LoadedMapSettings.m_SettingsDuplicate.insert({i, {}}); + } + + for(const auto &[Index, Valid, Setting] : vSettingsInvalid) + { + LocalContext.UpdateFromString(Setting.m_aCommand); + + ECollisionCheckResult Result = ECollisionCheckResult::ERROR; + int CollidingLineIndex = LocalContext.CheckCollision(Setting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid, Result); + int RealCollidingLineIndex = CollidingLineIndex; + + if(CollidingLineIndex != -1) + RealCollidingLineIndex = std::find_if(vLoadedMapSettings.begin(), vLoadedMapSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid.at(CollidingLineIndex).m_aCommand) == 0; + }) - vLoadedMapSettings.begin(); + + int Type = 0; + if(!Valid) + Type |= SInvalidSetting::TYPE_INVALID; + if(Result == ECollisionCheckResult::REPLACE) + Type |= SInvalidSetting::TYPE_DUPLICATE; + + m_LoadedMapSettings.m_vSettingsInvalid.emplace_back(Index, Setting.m_aCommand, Type, RealCollidingLineIndex); + if(Type & SInvalidSetting::TYPE_DUPLICATE) + m_LoadedMapSettings.m_SettingsDuplicate[RealCollidingLineIndex].emplace_back(m_LoadedMapSettings.m_vSettingsInvalid.size() - 1); + + LocalContext.Reset(); + } + + if(!m_LoadedMapSettings.m_vSettingsInvalid.empty()) + Editor()->m_Dialog = DIALOG_MAPSETTINGS_ERROR; +} + // ------ loaders void CMapSettingsBackend::InitValueLoaders() @@ -1552,4 +2130,4 @@ void SValueLoader::LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilde { ArgBuilder.Add(CTuningParams::Name(i)); } -} \ No newline at end of file +} diff --git a/src/game/editor/editor_server_settings.h b/src/game/editor/editor_server_settings.h index f2e09b6fd..5b7f4a3c7 100644 --- a/src/game/editor/editor_server_settings.h +++ b/src/game/editor/editor_server_settings.h @@ -14,6 +14,16 @@ struct SMapSettingCommand; struct IMapSetting; class CLineInput; +struct CEditorMapSetting +{ + char m_aCommand[256]; + + CEditorMapSetting(const char *pCommand) + { + str_copy(m_aCommand, pCommand); + } +}; + // A parsed map setting argument, storing the name and the type // Used for validation and to display arguments names struct SParsedMapSettingArg @@ -46,6 +56,32 @@ struct SCommandParseError int m_ArgIndex; }; +struct SInvalidSetting +{ + enum + { + TYPE_INVALID = 1 << 0, + TYPE_DUPLICATE = 1 << 1 + }; + int m_Index; // Index of the command in the loaded map settings list + char m_aSetting[256]; // String of the setting + int m_Type; // Type of that invalid setting + int m_CollidingIndex; // The colliding line index in case type is TYPE_DUPLICATE + + struct SContext + { + bool m_Fixed; + bool m_Deleted; + bool m_Chosen; + } m_Context; + + SInvalidSetting(int Index, const char *pSetting, int Type, int CollidingIndex) : + m_Index(Index), m_Type(Type), m_CollidingIndex(CollidingIndex), m_Context() + { + str_copy(m_aSetting, pSetting); + } +}; + // -------------------------------------- // Builder classes & methods to generate list of possible values // easily for specific settings and arguments. @@ -151,7 +187,8 @@ public: // General methods void Init(CEditor *pEditor) override; bool OnInput(const IInput::CEvent &Event) override; - void OnUpdate(); + void OnUpdate() override; + void OnMapLoad() override; public: // Constraints methods enum class EArgConstraint @@ -197,9 +234,14 @@ public: // CContext const SCurrentSettingArg &Arg(int Index) const { return m_vCurrentArgs.at(Index); } const std::shared_ptr &Setting() const { return m_pCurrentSetting; } CLineInput *LineInput() const { return m_pLineInput; } + void SetFontSize(float FontSize) { m_FontSize = FontSize; } int CheckCollision(ECollisionCheckResult &Result) const; + int CheckCollision(const std::vector &vSettings, ECollisionCheckResult &Result) const; + int CheckCollision(const char *pInputString, const std::vector &vSettings, ECollisionCheckResult &Result) const; + void Update(); + void UpdateFromString(const char *pStr); bool UpdateCursor(bool Force = false); void Reset(); void GetCommandHelpText(char *pStr, int Length) const; @@ -221,11 +263,14 @@ public: // CContext void ClearError(); EValidationResult ValidateArg(int Index, const char *pArg); void UpdatePossibleMatches(); - void ParseArgs(const char *pStr); + void ParseArgs(const char *pLineInputStr, const char *pStr); bool OnInput(const IInput::CEvent &Event); const char *InputString() const; void UpdateCompositionString(); + template + void FormatDisplayValue(const char *pValue, char (&aOut)[N]); + private: // Fields std::shared_ptr m_pCurrentSetting; // Current setting, can be nullptr in case of invalid setting name std::vector m_vCurrentArgs; // Current parsed arguments from lineinput string @@ -238,6 +283,7 @@ public: // CContext CMapSettingsBackend *m_pBackend; std::string m_CompositionStringBuffer; + float m_FontSize; }; CContext NewContext(CLineInput *pLineInput) @@ -328,6 +374,27 @@ private: // Backend fields static CContext *ms_pActiveContext; friend class CEditor; + +private: // Map settings validation on load + struct SLoadedMapSettings + { + std::vector m_vSettingsInvalid; + std::vector m_vSettingsValid; + std::map> m_SettingsDuplicate; + + SLoadedMapSettings() : + m_vSettingsInvalid(), m_vSettingsValid(), m_SettingsDuplicate() + { + } + + void Reset() + { + m_vSettingsInvalid.clear(); + m_vSettingsValid.clear(); + m_SettingsDuplicate.clear(); + } + + } m_LoadedMapSettings; }; #endif diff --git a/src/game/editor/editor_ui.h b/src/game/editor/editor_ui.h index 00799aefa..d06b5976b 100644 --- a/src/game/editor/editor_ui.h +++ b/src/game/editor/editor_ui.h @@ -14,4 +14,4 @@ struct SEditBoxDropdownContext bool m_ShouldHide = false; }; -#endif \ No newline at end of file +#endif