From 9cc8a28305e69f573f6c2616e217d50ef5f0bf67 Mon Sep 17 00:00:00 2001 From: Corantin H Date: Mon, 18 Dec 2023 02:08:12 +0100 Subject: [PATCH] Better map settings input (autocomplete, validation) --- CMakeLists.txt | 3 + src/base/system.cpp | 24 + src/base/system.h | 2 + src/engine/client/text.cpp | 6 +- src/engine/config.h | 3 + src/engine/shared/config.cpp | 371 +---- src/engine/shared/config.h | 362 ++++- src/engine/textrender.h | 3 + src/game/client/gameclient.cpp | 4 +- src/game/client/lineinput.cpp | 4 +- src/game/client/lineinput.h | 2 +- src/game/client/ui.cpp | 9 +- src/game/client/ui.h | 5 +- src/game/editor/editor.cpp | 198 +-- src/game/editor/editor.h | 27 +- src/game/editor/editor_actions.cpp | 1 + src/game/editor/editor_server_settings.cpp | 1555 ++++++++++++++++++++ src/game/editor/editor_server_settings.h | 333 +++++ src/game/editor/editor_ui.h | 17 + 19 files changed, 2367 insertions(+), 562 deletions(-) create mode 100644 src/game/editor/editor_server_settings.cpp create mode 100644 src/game/editor/editor_server_settings.h create mode 100644 src/game/editor/editor_ui.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ff427ad5..2793cc623 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2299,8 +2299,11 @@ if(CLIENT) editor_object.cpp editor_object.h editor_props.cpp + editor_server_settings.cpp + editor_server_settings.h editor_trackers.cpp editor_trackers.h + editor_ui.h explanations.cpp map_grid.cpp map_grid.h diff --git a/src/base/system.cpp b/src/base/system.cpp index f7584a11f..99ca3b84a 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -3590,6 +3590,18 @@ int str_toint(const char *str) return str_toint_base(str, 10); } +bool str_toint(const char *str, int *out) +{ + // returns true if conversion was successful + char *end; + int value = strtol(str, &end, 10); + if(*end != '\0') + return false; + if(out != nullptr) + *out = value; + return true; +} + int str_toint_base(const char *str, int base) { return strtol(str, nullptr, base); @@ -3610,6 +3622,18 @@ float str_tofloat(const char *str) return strtod(str, nullptr); } +bool str_tofloat(const char *str, float *out) +{ + // returns true if conversion was successful + char *end; + float value = strtod(str, &end); + if(*end != '\0') + return false; + if(out != nullptr) + *out = value; + return true; +} + void str_from_int(int value, char *buffer, size_t buffer_size) { buffer[0] = '\0'; // Fix false positive clang-analyzer-core.UndefinedBinaryOperatorResult when using result diff --git a/src/base/system.h b/src/base/system.h index 7c9386170..48046daaf 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2120,10 +2120,12 @@ typedef struct void net_stats(NETSTATS *stats); int str_toint(const char *str); +bool str_toint(const char *str, int *out); int str_toint_base(const char *str, int base); unsigned long str_toulong_base(const char *str, int base); int64_t str_toint64_base(const char *str, int base = 10); float str_tofloat(const char *str); +bool str_tofloat(const char *str, float *out); void str_from_int(int value, char *buffer, size_t buffer_size); diff --git a/src/engine/client/text.cpp b/src/engine/client/text.cpp index aa214ddda..73c551290 100644 --- a/src/engine/client/text.cpp +++ b/src/engine/client/text.cpp @@ -1668,7 +1668,7 @@ public: while(pCurrent < pBatchEnd && pCurrent != pEllipsis) { - const int PrevCharCount = pCursor->m_CharCount; + const int PrevCharCount = pCursor->m_GlyphCount; pCursor->m_CharCount += pTmp - pCurrent; pCurrent = pTmp; int Character = NextCharacter; @@ -1754,9 +1754,9 @@ public: if(ColorOption < (int)pCursor->m_vColorSplits.size()) { STextColorSplit &Split = pCursor->m_vColorSplits.at(ColorOption); - if(PrevCharCount >= Split.m_CharIndex && PrevCharCount < Split.m_CharIndex + Split.m_Length) + if(PrevCharCount >= Split.m_CharIndex && (Split.m_Length == -1 || PrevCharCount < Split.m_CharIndex + Split.m_Length)) Color = Split.m_Color; - if(PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) + if(Split.m_Length != -1 && PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) ColorOption++; } diff --git a/src/engine/config.h b/src/engine/config.h index bccf50437..4123bad7e 100644 --- a/src/engine/config.h +++ b/src/engine/config.h @@ -10,6 +10,7 @@ class IConfigManager : public IInterface MACRO_INTERFACE("config") public: typedef void (*SAVECALLBACKFUNC)(IConfigManager *pConfig, void *pUserData); + typedef void (*POSSIBLECFGFUNC)(const struct SConfigVariable *, void *pUserData); virtual void Init() = 0; virtual void Reset(const char *pScriptName) = 0; @@ -23,6 +24,8 @@ public: virtual void WriteLine(const char *pLine) = 0; virtual void StoreUnknownCommand(const char *pCommand) = 0; + + virtual void PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) = 0; }; extern IConfigManager *CreateConfigManager(); diff --git a/src/engine/shared/config.cpp b/src/engine/shared/config.cpp index 4ab49a23f..8cbe3eb4f 100644 --- a/src/engine/shared/config.cpp +++ b/src/engine/shared/config.cpp @@ -11,361 +11,6 @@ CConfig g_Config; -static void EscapeParam(char *pDst, const char *pSrc, int Size) -{ - str_escape(&pDst, pSrc, pDst + Size); -} - -struct SConfigVariable -{ - enum EVariableType - { - VAR_INT, - VAR_COLOR, - VAR_STRING, - }; - IConsole *m_pConsole; - const char *m_pScriptName; - EVariableType m_Type; - int m_Flags; - const char *m_pHelp; - // Note that this only applies to the console command and the SetValue function, - // but the underlying config variable can still be modified programatically. - bool m_ReadOnly = false; - - SConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp) : - m_pConsole(pConsole), - m_pScriptName(pScriptName), - m_Type(Type), - m_Flags(Flags), - m_pHelp(pHelp) - { - } - - virtual ~SConfigVariable() = default; - - virtual void Register() = 0; - virtual bool IsDefault() const = 0; - virtual void Serialize(char *pOut, size_t Size) const = 0; - virtual void ResetToDefault() = 0; - 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; - } -}; - -struct SIntConfigVariable : public SConfigVariable -{ - int *m_pVariable; - int m_Default; - int m_Min; - int m_Max; - int m_OldValue; - - SIntConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, int *pVariable, int Default, int Min, int Max) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pVariable(pVariable), - m_Default(Default), - m_Min(Min), - m_Max(Max), - m_OldValue(Default) - { - *m_pVariable = m_Default; - } - - ~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; - } -}; - -struct SColorConfigVariable : public SConfigVariable -{ - unsigned *m_pVariable; - unsigned m_Default; - bool m_Light; - bool m_Alpha; - unsigned m_OldValue; - - SColorConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, unsigned *pVariable, unsigned Default) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pVariable(pVariable), - m_Default(Default), - m_Light(Flags & CFGFLAG_COLLIGHT), - m_Alpha(Flags & CFGFLAG_COLALPHA), - m_OldValue(Default) - { - *m_pVariable = m_Default; - } - - ~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; - } -}; - -struct SStringConfigVariable : public SConfigVariable -{ - char *m_pStr; - const char *m_pDefault; - 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() 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); - } -}; - CConfigManager::CConfigManager() { m_pConsole = nullptr; @@ -388,7 +33,7 @@ void CConfigManager::Init() #define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \ { \ - const char *pHelp = Min == Max ? Desc " (default: " #Def ")" : Max == 0 ? Desc " (default: " #Def ", min: " #Min ")" : Desc " (default: " #Def ", min: " #Min ", max: " #Max ")"; \ + const char *pHelp = Min == Max ? Desc " (default: " #Def ")" : (Max == 0 ? Desc " (default: " #Def ", min: " #Min ")" : Desc " (default: " #Def ", min: " #Min ", max: " #Max ")"); \ AddVariable(m_ConfigHeap.Allocate(m_pConsole, #ScriptName, SConfigVariable::VAR_INT, Flags, pHelp, &g_Config.m_##Name, Def, Min, Max)); \ } @@ -549,6 +194,20 @@ void CConfigManager::StoreUnknownCommand(const char *pCommand) m_vpUnknownCommands.push_back(m_ConfigHeap.StoreString(pCommand)); } +void CConfigManager::PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) +{ + for(const SConfigVariable *pVariable : m_vpAllVariables) + { + if(pVariable->m_Flags & FlagMask) + { + if(str_find_nocase(pVariable->m_pScriptName, pStr)) + { + pfnCallback(pVariable, pUserData); + } + } + } +} + void CConfigManager::Con_Reset(IConsole::IResult *pResult, void *pUserData) { static_cast(pUserData)->Reset(pResult->GetString(0)); diff --git a/src/engine/shared/config.h b/src/engine/shared/config.h index 826c0dd61..2d8503aa9 100644 --- a/src/engine/shared/config.h +++ b/src/engine/shared/config.h @@ -4,6 +4,7 @@ #define ENGINE_SHARED_CONFIG_H #include +#include #include #include @@ -58,6 +59,361 @@ 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 + { + VAR_INT, + VAR_COLOR, + VAR_STRING, + }; + IConsole *m_pConsole; + const char *m_pScriptName; + EVariableType m_Type; + int m_Flags; + const char *m_pHelp; + // Note that this only applies to the console command and the SetValue function, + // but the underlying config variable can still be modified programatically. + bool m_ReadOnly = false; + + SConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp) : + m_pConsole(pConsole), + m_pScriptName(pScriptName), + m_Type(Type), + m_Flags(Flags), + m_pHelp(pHelp) + { + } + + virtual ~SConfigVariable() = default; + + virtual void Register() = 0; + virtual bool IsDefault() const = 0; + virtual void Serialize(char *pOut, size_t Size) const = 0; + virtual void ResetToDefault() = 0; + 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; + } +}; + +struct SIntConfigVariable : public SConfigVariable +{ + int *m_pVariable; + int m_Default; + int m_Min; + int m_Max; + int m_OldValue; + + SIntConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, int *pVariable, int Default, int Min, int Max) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pVariable(pVariable), + m_Default(Default), + m_Min(Min), + m_Max(Max), + m_OldValue(Default) + { + *m_pVariable = m_Default; + } + + ~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; + } +}; + +struct SColorConfigVariable : public SConfigVariable +{ + unsigned *m_pVariable; + unsigned m_Default; + bool m_Light; + bool m_Alpha; + unsigned m_OldValue; + + SColorConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, unsigned *pVariable, unsigned Default) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pVariable(pVariable), + m_Default(Default), + m_Light(Flags & CFGFLAG_COLLIGHT), + m_Alpha(Flags & CFGFLAG_COLALPHA), + m_OldValue(Default) + { + *m_pVariable = m_Default; + } + + ~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; + } +}; + +struct SStringConfigVariable : public SConfigVariable +{ + char *m_pStr; + const char *m_pDefault; + 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() 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); + } +}; + class CConfigManager : public IConfigManager { IConsole *m_pConsole; @@ -79,8 +435,8 @@ class CConfigManager : public IConfigManager }; std::vector m_vCallbacks; - std::vector m_vpAllVariables; - std::vector m_vpGameVariables; + std::vector m_vpAllVariables; + std::vector m_vpGameVariables; std::vector m_vpUnknownCommands; CHeap m_ConfigHeap; @@ -103,6 +459,8 @@ public: void WriteLine(const char *pLine) override; void StoreUnknownCommand(const char *pCommand) override; + + void PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) override; }; #endif diff --git a/src/engine/textrender.h b/src/engine/textrender.h index 930fd3017..13a176531 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -145,6 +145,9 @@ MAYBE_UNUSED static const char *FONT_ICON_DICE_SIX = "\xEF\x94\xA6"; MAYBE_UNUSED static const char *FONT_ICON_LAYER_GROUP = "\xEF\x97\xBD"; 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"; } // end namespace FontIcons enum ETextCursorSelectionMode diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 27e8025d3..dd81dc24f 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -166,7 +166,7 @@ void CGameClient::OnConsoleInit() Console()->Register("kill", "", CFGFLAG_CLIENT, ConKill, this, "Kill yourself to restart"); // register server dummy commands for tab completion - Console()->Register("tune", "s[tuning] ?i[value]", CFGFLAG_SERVER, 0, 0, "Tune variable to value or show current value"); + Console()->Register("tune", "s[tuning] ?f[value]", CFGFLAG_SERVER, 0, 0, "Tune variable to value or show current value"); Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, 0, 0, "Reset all or one tuning variable to default"); Console()->Register("tunes", "", CFGFLAG_SERVER, 0, 0, "List all tuning variables and their values"); Console()->Register("change_map", "?r[map]", CFGFLAG_SERVER, 0, 0, "Change map"); @@ -185,7 +185,7 @@ void CGameClient::OnConsoleInit() Console()->Register("shuffle_teams", "", CFGFLAG_SERVER, 0, 0, "Shuffle the current teams"); // register tune zone command to allow the client prediction to load tunezones from the map - Console()->Register("tune_zone", "i[zone] s[tuning] i[value]", CFGFLAG_CLIENT | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); + Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_CLIENT | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); for(auto &pComponent : m_vpAll) pComponent->m_pClient = this; diff --git a/src/game/client/lineinput.cpp b/src/game/client/lineinput.cpp index 7af53941a..770b4a890 100644 --- a/src/game/client/lineinput.cpp +++ b/src/game/client/lineinput.cpp @@ -389,7 +389,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) return m_WasChanged || m_WasCursorChanged || KeyHandled; } -STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing) +STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing, const std::vector &vColorSplits) { // update derived attributes to handle external changes to the buffer UpdateStrData(); @@ -432,6 +432,7 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al Cursor.m_LineSpacing = LineSpacing; Cursor.m_PressMouse.x = m_MouseSelection.m_PressMouse.x; Cursor.m_ReleaseMouse.x = m_MouseSelection.m_ReleaseMouse.x; + Cursor.m_vColorSplits = vColorSplits; if(LineWidth < 0.0f) { // Using a Y position that's always inside the line input makes it so the selection does not reset when @@ -512,6 +513,7 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al TextRender()->SetCursor(&Cursor, CursorPos.x, CursorPos.y, FontSize, TEXTFLAG_RENDER); Cursor.m_LineWidth = LineWidth; Cursor.m_LineSpacing = LineSpacing; + Cursor.m_vColorSplits = vColorSplits; TextRender()->TextEx(&Cursor, pDisplayStr); } diff --git a/src/game/client/lineinput.h b/src/game/client/lineinput.h index 0bd405a04..cbf4e42e9 100644 --- a/src/game/client/lineinput.h +++ b/src/game/client/lineinput.h @@ -187,7 +187,7 @@ public: return Changed; } - STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing); + STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing, const std::vector &vColorSplits = {}); SMouseSelection *GetMouseSelection() { return &m_MouseSelection; } const void *GetClearButtonId() const { return &m_ClearButtonId; } diff --git a/src/game/client/ui.cpp b/src/game/client/ui.cpp index fdcdd1851..f1004cd71 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -674,6 +674,7 @@ void CUI::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align CTextCursor Cursor; TextRender()->SetCursor(&Cursor, CursorPos.x, CursorPos.y, Size, TEXTFLAG_RENDER | Flags); + Cursor.m_vColorSplits = LabelProps.m_vColorSplits; Cursor.m_LineWidth = (float)LabelProps.m_MaxWidth; TextRender()->TextEx(&Cursor, pText, -1); } @@ -761,7 +762,7 @@ void CUI::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRe } } -bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners) +bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector &vColorSplits) { const bool Inside = MouseHovered(pRect); const bool Active = m_pLastActiveItem == pLineInput; @@ -843,7 +844,7 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize pRect->Draw(ms_LightButtonColorFunction.GetColor(Active, HotItem() == pLineInput), Corners, 3.0f); ClipEnable(pRect); Textbox.x -= ScrollOffset; - const STextBoundingBox BoundingBox = pLineInput->Render(&Textbox, FontSize, TEXTALIGN_ML, Changed || CursorChanged, -1.0f, 0.0f); + const STextBoundingBox BoundingBox = pLineInput->Render(&Textbox, FontSize, TEXTALIGN_ML, Changed || CursorChanged, -1.0f, 0.0f, vColorSplits); ClipDisable(); // Scroll left or right if necessary @@ -864,12 +865,12 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize return Changed; } -bool CUI::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners) +bool CUI::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector &vColorSplits) { CUIRect EditBox, ClearButton; pRect->VSplitRight(pRect->h, &EditBox, &ClearButton); - bool ReturnValue = DoEditBox(pLineInput, &EditBox, FontSize, Corners & ~IGraphics::CORNER_R); + bool ReturnValue = DoEditBox(pLineInput, &EditBox, FontSize, Corners & ~IGraphics::CORNER_R, vColorSplits); ClearButton.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f * ButtonColorMul(pLineInput->GetClearButtonId())), Corners & ~IGraphics::CORNER_L, 3.0f); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 6c3c5d959..89a19c048 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -212,6 +212,7 @@ struct SLabelProperties bool m_StopAtEnd = false; bool m_EllipsisAtEnd = false; bool m_EnableWidthCheck = true; + std::vector m_vColorSplits = {}; }; struct SMenuButtonProperties @@ -513,8 +514,8 @@ public: void DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps = {}, int StrLen = -1, const CTextCursor *pReadCursor = nullptr) const; void DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps = {}, int StrLen = -1, const CTextCursor *pReadCursor = nullptr) const; - bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL); - bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL); + bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const std::vector &vColorSplits = {}); + bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const std::vector &vColorSplits = {}); int DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const std::function &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props = {}); // only used for popup menus diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 335662511..0e42b22b7 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -41,6 +41,7 @@ #include "editor_actions.h" #include +#include #include #include @@ -113,16 +114,16 @@ void CEditor::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, v OTHER *********************************************************/ -bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip) +bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector &vColorSplits) { UpdateTooltip(pLineInput, pRect, pToolTip); - return UI()->DoEditBox(pLineInput, pRect, FontSize, Corners); + return UI()->DoEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits); } -bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip) +bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector &vColorSplits) { UpdateTooltip(pLineInput, pRect, pToolTip); - return UI()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners); + return UI()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits); } ColorRGBA CEditor::GetButtonColor(const void *pID, int Checked) @@ -7515,189 +7516,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } -void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast) -{ - // TODO: improve validation (https://github.com/ddnet/ddnet/issues/1406) - // Returns true if the argument is a valid server setting - const auto &&ValidateServerSetting = [](const char *pStr) { - return str_find(pStr, " ") != nullptr; - }; - - static int s_CommandSelectedIndex = -1; - static CListBox s_ListBox; - s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); - - bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); - const bool CurrentInputValid = ValidateServerSetting(m_SettingsCommandInput.GetString()); - - CUIRect ToolBar, Button, Label, List, DragBar; - View.HSplitTop(22.0f, &DragBar, nullptr); - DragBar.y -= 2.0f; - DragBar.w += 2.0f; - DragBar.h += 4.0f; - DoEditorDragBar(View, &DragBar, EDragSide::SIDE_TOP, &m_aExtraEditorSplits[EXTRAEDITOR_SERVER_SETTINGS]); - View.HSplitTop(20.0f, &ToolBar, &View); - View.HSplitTop(2.0f, nullptr, &List); - ToolBar.HMargin(2.0f, &ToolBar); - - // delete button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - static int s_DeleteButton = 0; - if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, GotSelection ? 0 : -1, &Button, 0, "[Delete] Delete the selected command from the command list.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - - m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); - if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) - s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; - if(s_CommandSelectedIndex >= 0) - m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // move down button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1; - static int s_DownButton = 0; - if(DoButton_FontIcon(&s_DownButton, FONT_ICON_SORT_DOWN, CanMoveDown ? 0 : -1, &Button, 0, "[Alt+Down] Move the selected command down.", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_DOWN, &s_CommandSelectedIndex, s_CommandSelectedIndex)); - - std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex + 1]); - s_CommandSelectedIndex++; - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // move up button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0; - static int s_UpButton = 0; - if(DoButton_FontIcon(&s_UpButton, FONT_ICON_SORT_UP, CanMoveUp ? 0 : -1, &Button, 0, "[Alt+Up] Move the selected command up.", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_UP, &s_CommandSelectedIndex, s_CommandSelectedIndex)); - - std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex - 1]); - s_CommandSelectedIndex--; - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // redo button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - static int s_RedoButton = 0; - if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_ServerSettingsHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo command edit", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) - { - m_ServerSettingsHistory.Redo(); - } - - // undo button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - static int s_UndoButton = 0; - if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_ServerSettingsHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo command edit", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) - { - m_ServerSettingsHistory.Undo(); - } - - GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); - - // update button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()) != 0; - static int s_UpdateButton = 0; - if(DoButton_FontIcon(&s_UpdateButton, FONT_ICON_PENCIL, CanUpdate ? 0 : -1, &Button, 0, "[Alt+Enter] Update the selected command based on the entered value.", IGraphics::CORNER_R, 9.0f) == 1 || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) - { - bool Found = false; - int i; - for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i) - { - if(i != s_CommandSelectedIndex && !str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) - { - Found = true; - break; - } - } - if(Found) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); - s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; - } - else - { - const char *pStr = m_SettingsCommandInput.GetString(); - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); - str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); - } - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - UI()->SetActiveItem(&m_SettingsCommandInput); - } - - // add button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(100.0f, &ToolBar, nullptr); - const bool CanAdd = s_ListBox.Active() && CurrentInputValid; - static int s_AddButton = 0; - if(DoButton_FontIcon(&s_AddButton, FONT_ICON_PLUS, CanAdd ? 0 : -1, &Button, 0, "[Enter] Add a command to the command list.", IGraphics::CORNER_L) == 1 || (CanAdd && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) - { - bool Found = false; - for(size_t i = 0; i < m_Map.m_vSettings.size(); ++i) - { - if(!str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) - { - s_CommandSelectedIndex = i; - Found = true; - break; - } - } - - if(!Found) - { - m_Map.m_vSettings.emplace_back(m_SettingsCommandInput.GetString()); - s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::ADD, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - m_Map.OnModify(); - } - s_ListBox.ScrollToSelected(); - UI()->SetActiveItem(&m_SettingsCommandInput); - } - - // command input (use remaining toolbar width) - if(!ShowServerSettingsEditorLast) // Just activated - UI()->SetActiveItem(&m_SettingsCommandInput); - m_SettingsCommandInput.SetEmptyText("Command"); - DoClearableEditBox(&m_SettingsCommandInput, &ToolBar, 12.0f, IGraphics::CORNER_ALL, "Enter a server setting."); - - // command list - s_ListBox.DoStart(15.0f, m_Map.m_vSettings.size(), 1, 3, s_CommandSelectedIndex, &List); - - for(size_t i = 0; i < m_Map.m_vSettings.size(); i++) - { - const CListboxItem Item = s_ListBox.DoNextItem(&m_Map.m_vSettings[i], s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i); - if(!Item.m_Visible) - continue; - - Item.m_Rect.VMargin(5.0f, &Label); - - SLabelProperties Props; - Props.m_MaxWidth = Label.w; - Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); - } - - const int NewSelected = s_ListBox.DoEnd(); - if(s_CommandSelectedIndex != NewSelected) - { - s_CommandSelectedIndex = NewSelected; - m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); - } -} - void CEditor::RenderEditorHistory(CUIRect View) { enum EHistoryType @@ -8507,6 +8325,8 @@ void CEditor::Reset(bool CreateDefault) m_EnvOpTracker.m_pEditor = this; m_EnvOpTracker.Reset(); + + m_MapSettingsCommandContext.Reset(); } int CEditor::GetTextureUsageFlag() @@ -8560,7 +8380,8 @@ void CEditor::Init() { m_pInput = Kernel()->RequestInterface(); m_pClient = Kernel()->RequestInterface(); - m_pConfig = Kernel()->RequestInterface()->Values(); + m_pConfigManager = Kernel()->RequestInterface(); + m_pConfig = m_pConfigManager->Values(); m_pConsole = Kernel()->RequestInterface(); m_pEngine = Kernel()->RequestInterface(); m_pGraphics = Kernel()->RequestInterface(); @@ -8577,6 +8398,7 @@ void CEditor::Init() m_Map.m_pEditor = this; m_vComponents.emplace_back(m_MapView); + m_vComponents.emplace_back(m_MapSettingsBackend); for(CEditorComponent &Component : m_vComponents) Component.Init(this); diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 84916ec74..e051656e5 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -23,6 +24,7 @@ #include #include +#include #include #include #include @@ -31,7 +33,9 @@ #include "auto_map.h" #include "editor_history.h" +#include "editor_server_settings.h" #include "editor_trackers.h" +#include "editor_ui.h" #include "map_view.h" #include "smooth_value.h" @@ -43,6 +47,8 @@ #include typedef std::function FIndexModifyFunction; +template +using FDropdownRenderCallback = std::function &)>; // CEditor SPECIFIC enum @@ -267,6 +273,7 @@ class CEditor : public IEditor { class IInput *m_pInput = nullptr; class IClient *m_pClient = nullptr; + class IConfigManager *m_pConfigManager = nullptr; class CConfig *m_pConfig = nullptr; class IConsole *m_pConsole = nullptr; class IEngine *m_pEngine = nullptr; @@ -305,6 +312,7 @@ class CEditor : public IEditor public: class IInput *Input() { return m_pInput; } class IClient *Client() { return m_pClient; } + class IConfigManager *ConfigManager() { return m_pConfigManager; } class CConfig *Config() { return m_pConfig; } class IConsole *Console() { return m_pConsole; } class IEngine *Engine() { return m_pEngine; } @@ -320,7 +328,8 @@ public: CEditor() : m_ZoomEnvelopeX(1.0f, 0.1f, 600.0f), - m_ZoomEnvelopeY(640.0f, 0.1f, 32000.0f) + m_ZoomEnvelopeY(640.0f, 0.1f, 32000.0f), + m_MapSettingsCommandContext(m_MapSettingsBackend.NewContext(&m_SettingsCommandInput)) { m_EntitiesTexture.Invalidate(); m_FrontTexture.Invalidate(); @@ -786,6 +795,8 @@ public: static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); CLineInputBuffered<256> m_SettingsCommandInput; + CMapSettingsBackend m_MapSettingsBackend; + CMapSettingsBackend::CContext m_MapSettingsCommandContext; CImageInfo m_TileartImageInfo; char m_aTileartFilename[IO_MAX_PATH_LENGTH]; @@ -811,8 +822,15 @@ public: int DoButton_DraggableEx(const void *pID, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip = nullptr, int Corners = IGraphics::CORNER_ALL, float FontSize = 10.0f); - bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); - bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); + bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr, const std::vector &vColorSplits = {}); + bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr, const std::vector &vColorSplits = {}); + + void DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); + + template + int DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &fnMatchCallback); + template + int RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &fnMatchCallback); void RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness); @@ -962,7 +980,10 @@ public: void RenderTooltip(CUIRect TooltipRect); void RenderEnvelopeEditor(CUIRect View); + void RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast); + static void MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits); + void RenderEditorHistory(CUIRect View); enum class EDragSide // Which side is the drag bar on diff --git a/src/game/editor/editor_actions.cpp b/src/game/editor/editor_actions.cpp index 8f37233c6..9c63b8237 100644 --- a/src/game/editor/editor_actions.cpp +++ b/src/game/editor/editor_actions.cpp @@ -1305,6 +1305,7 @@ 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 new file mode 100644 index 000000000..25b533001 --- /dev/null +++ b/src/game/editor/editor_server_settings.cpp @@ -0,0 +1,1555 @@ +#include "editor_server_settings.h" +#include "editor.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace FontIcons; + +static const int FONT_SIZE = 12.0f; + +struct IMapSetting +{ + enum EType + { + SETTING_INT, + SETTING_COMMAND, + }; + const char *m_pName; + const char *m_pHelp; + EType m_Type; + + IMapSetting(const char *pName, const char *pHelp, EType Type) : + m_pName(pName), m_pHelp(pHelp), m_Type(Type) {} +}; +struct SMapSettingInt : public IMapSetting +{ + int m_Default; + int m_Min; + int m_Max; + + SMapSettingInt(const char *pName, const char *pHelp, int Default, int Min, int Max) : + IMapSetting(pName, pHelp, IMapSetting::SETTING_INT), m_Default(Default), m_Min(Min), m_Max(Max) {} +}; +struct SMapSettingCommand : public IMapSetting +{ + const char *m_pArgs; + + SMapSettingCommand(const char *pName, const char *pHelp, const char *pArgs) : + IMapSetting(pName, pHelp, IMapSetting::SETTING_COMMAND), m_pArgs(pArgs) {} +}; + +void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast) +{ + static int s_CommandSelectedIndex = -1; + static CListBox s_ListBox; + s_ListBox.SetActive(!m_MapSettingsCommandContext.m_DropdownContext.m_ListBox.Active() && m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); + + bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + const bool CurrentInputValid = m_MapSettingsCommandContext.Valid(); // Use the context to validate the input + + CUIRect ToolBar, Button, Label, List, DragBar; + View.HSplitTop(22.0f, &DragBar, nullptr); + DragBar.y -= 2.0f; + DragBar.w += 2.0f; + DragBar.h += 4.0f; + DoEditorDragBar(View, &DragBar, EDragSide::SIDE_TOP, &m_aExtraEditorSplits[EXTRAEDITOR_SERVER_SETTINGS]); + View.HSplitTop(20.0f, &ToolBar, &View); + View.HSplitTop(2.0f, nullptr, &List); + ToolBar.HMargin(2.0f, &ToolBar); + + // delete button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_DeleteButton = 0; + if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, GotSelection ? 0 : -1, &Button, 0, "[Delete] Delete the selected command from the command list.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); + if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) + s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; + if(s_CommandSelectedIndex >= 0) + m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + } + + // move down button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1; + static int s_DownButton = 0; + if(DoButton_FontIcon(&s_DownButton, FONT_ICON_SORT_DOWN, CanMoveDown ? 0 : -1, &Button, 0, "[Alt+Down] Move the selected command down.", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_DOWN, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex + 1]); + s_CommandSelectedIndex++; + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + } + + // move up button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0; + static int s_UpButton = 0; + if(DoButton_FontIcon(&s_UpButton, FONT_ICON_SORT_UP, CanMoveUp ? 0 : -1, &Button, 0, "[Alt+Up] Move the selected command up.", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_UP, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex - 1]); + s_CommandSelectedIndex--; + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + } + + // redo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_ServerSettingsHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo command edit", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) + { + m_ServerSettingsHistory.Redo(); + } + + // undo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_ServerSettingsHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo command edit", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) + { + m_ServerSettingsHistory.Undo(); + } + + GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + + int CollidingCommandIndex = -1; + ECollisionCheckResult CheckResult = ECollisionCheckResult::ERROR; + if(CurrentInputValid) + CollidingCommandIndex = m_MapSettingsCommandContext.CheckCollision(CheckResult); + + // update button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + const bool CanAdd = CheckResult == ECollisionCheckResult::ADD; + const bool CanReplace = CheckResult == ECollisionCheckResult::REPLACE; + + const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()) != 0; + + static int s_UpdateButton = 0; + if(DoButton_FontIcon(&s_UpdateButton, FONT_ICON_PENCIL, CanUpdate ? 0 : -1, &Button, 0, "[Alt+Enter] Update the selected command based on the entered value.", IGraphics::CORNER_R, 9.0f) == 1 || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + { + if(CollidingCommandIndex == -1) + { + bool Found = false; + int i; + for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i) + { + if(i != s_CommandSelectedIndex && !str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) + { + Found = true; + break; + } + } + if(Found) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); + s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; + } + else + { + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + } + else + { + if(s_CommandSelectedIndex == CollidingCommandIndex) + { // If we are editing the currently collinding line, then we can just call EDIT on it + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + else + { // If not, then editing the current selected line will result in the deletion of the colliding line, and the editing of the selected line + const char *pStr = m_SettingsCommandInput.GetString(); + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "Delete command %d; Edit command %d", CollidingCommandIndex, s_CommandSelectedIndex); + + m_ServerSettingsHistory.BeginBulk(); + // Delete the colliding command + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, CollidingCommandIndex, m_Map.m_vSettings[CollidingCommandIndex].m_aCommand)); + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + CollidingCommandIndex); + // Edit the selected command + s_CommandSelectedIndex = s_CommandSelectedIndex > CollidingCommandIndex ? s_CommandSelectedIndex - 1 : s_CommandSelectedIndex; + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + + m_ServerSettingsHistory.EndBulk(aBuf); + } + } + + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + m_SettingsCommandInput.Clear(); + m_MapSettingsCommandContext.Reset(); // Reset context + UI()->SetActiveItem(&m_SettingsCommandInput); + } + + // add button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(100.0f, &ToolBar, nullptr); + + static int s_AddButton = 0; + if(DoButton_FontIcon(&s_AddButton, CanReplace ? FONT_ICON_ARROWS_ROTATE : FONT_ICON_PLUS, CanAdd || CanReplace ? 0 : -1, &Button, 0, CanReplace ? "[Enter] Replace the corresponding command in the command list." : "[Enter] Add a command to the command list.", IGraphics::CORNER_L) == 1 || ((CanAdd || CanReplace) && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + { + if(CanReplace) + { + dbg_assert(CollidingCommandIndex != -1, "Could not replace command"); + s_CommandSelectedIndex = CollidingCommandIndex; + + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + else if(CanAdd) + { + m_Map.m_vSettings.emplace_back(m_SettingsCommandInput.GetString()); + s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::ADD, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + m_Map.OnModify(); + } + s_ListBox.ScrollToSelected(); + m_SettingsCommandInput.Clear(); + m_MapSettingsCommandContext.Reset(); // Reset context + UI()->SetActiveItem(&m_SettingsCommandInput); + } + + // command input (use remaining toolbar width) + if(!ShowServerSettingsEditorLast) // Just activated + UI()->SetActiveItem(&m_SettingsCommandInput); + m_SettingsCommandInput.SetEmptyText("Command"); + + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + // command list + s_ListBox.DoStart(15.0f, m_Map.m_vSettings.size(), 1, 3, s_CommandSelectedIndex, &List); + + for(size_t i = 0; i < m_Map.m_vSettings.size(); i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&m_Map.m_vSettings[i], s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i); + if(!Item.m_Visible) + continue; + + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + UI()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); + } + + const int NewSelected = s_ListBox.DoEnd(); + 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 + { + m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); + m_MapSettingsCommandContext.Update(); + m_MapSettingsCommandContext.UpdateCursor(true); + } + m_MapSettingsCommandContext.m_DropdownContext.m_ShouldHide = true; + UI()->SetActiveItem(&m_SettingsCommandInput); + } + + // Map setting input + DoMapSettingsEditBox(&m_MapSettingsCommandContext, &ToolBar, FONT_SIZE, List.h); +} + +void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners, const char *pToolTip) +{ + // Main method to do the full featured map settings edit box + + auto *pLineInput = pContext->LineInput(); + auto &Context = *pContext; + + // Set current active context if input is active + if(pLineInput->IsActive()) + CMapSettingsBackend::ms_pActiveContext = pContext; + + // Small utility to render a floating part above the input rect. + // Use to display either the error or the current argument name + const float PartMargin = 4.0f; + auto &&RenderFloatingPart = [&](CUIRect *pInputRect, float x, const char *pStr) { + CUIRect Background; + Background.x = x - PartMargin; + Background.y = pInputRect->y - pInputRect->h - 6.0f; + Background.w = TextRender()->TextWidth(FontSize, pStr) + 2 * PartMargin; + Background.h = pInputRect->h; + Background.Draw(ColorRGBA(0, 0, 0, 0.9f), IGraphics::CORNER_ALL, 3.0f); + + CUIRect Label; + Background.VSplitLeft(PartMargin, nullptr, &Label); + TextRender()->TextColor(0.8f, 0.8f, 0.8f, 1.0f); + UI()->DoLabel(&Label, pStr, FontSize, TEXTALIGN_MIDDLE); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + }; + + // If we have a valid command, display the help in the tooltip + if(Context.CommandIsValid()) + Context.GetCommandHelpText(m_aTooltip, sizeof(m_aTooltip)); + + CUIRect ToolBar = *pRect; + CUIRect Button; + ToolBar.VSplitRight(ToolBar.h, &ToolBar, &Button); + + // Do the unknown command toggle button + if(DoButton_FontIcon(&Context.m_AllowUnknownCommands, FONT_ICON_QUESTION, Context.m_AllowUnknownCommands, &Button, 0, "Disallow/allow unknown commands", IGraphics::CORNER_R)) + { + Context.m_AllowUnknownCommands = !Context.m_AllowUnknownCommands; + Context.Update(); + } + + // Color the arguments + std::vector vColorSplits; + Context.ColorArguments(vColorSplits); + + // Do and render clearable edit box with the colors + if(DoClearableEditBox(pLineInput, &ToolBar, FontSize, IGraphics::CORNER_L, "Enter a server setting.", vColorSplits)) + { + Context.Update(); // Update the context when contents change + Context.m_DropdownContext.m_ShouldHide = false; + } + + // Update/track the cursor + if(Context.UpdateCursor()) + Context.m_DropdownContext.m_ShouldHide = false; + + // Calculate x position of the dropdown and the floating part + float x = ToolBar.x + Context.CurrentArgPos() - pLineInput->GetScrollOffset(); + x = clamp(x, ToolBar.x + PartMargin, ToolBar.x + ToolBar.w); + + if(pLineInput->IsActive()) + { + // If line input is active, let's display a floating part for either the current argument name + // or for the error, if any. The error is only displayed when the cursor is at the end of the input. + const bool IsAtEnd = pLineInput->GetCursorOffset() == pLineInput->GetLength(); + + if(Context.CurrentArgName() && (!Context.HasError() || !IsAtEnd)) // Render argument name + RenderFloatingPart(&ToolBar, x, Context.CurrentArgName()); + else if(Context.HasError() && IsAtEnd) // Render error + RenderFloatingPart(&ToolBar, ToolBar.x + PartMargin, Context.Error()); + } + + // If we have possible matches for the current argument, let's display an editbox suggestions dropdown + const auto &vPossibleCommands = Context.PossibleMatches(); + int Selected = DoEditBoxDropdown(&Context.m_DropdownContext, pLineInput, &ToolBar, x - PartMargin, DropdownMaxHeight, Context.CurrentArg() >= 0, vPossibleCommands, MapSettingsDropdownRenderCallback); + + // If the dropdown just became visible, update the context + // This is needed when input loses focus and then we click a command in the map settings list + if(Context.m_DropdownContext.m_DidBecomeVisible) + { + Context.Update(); + Context.UpdateCursor(true); + } + + if(!vPossibleCommands.empty()) + { + // Check if the completion index has changed + if(Selected != pContext->m_CurrentCompletionIndex) + { + // If so, we should autocomplete the selected option + if(Selected != -1) + { + const char *pStr = vPossibleCommands[Selected].m_pValue; + int Len = pContext->m_CurrentCompletionIndex == -1 ? str_length(Context.CurrentArgValue()) : (pContext->m_CurrentCompletionIndex < (int)vPossibleCommands.size() ? str_length(vPossibleCommands[pContext->m_CurrentCompletionIndex].m_pValue) : 0); + size_t Start = Context.CurrentArgOffset(); + size_t End = Start + Len; + pLineInput->SetRange(pStr, Start, End); + } + + pContext->m_CurrentCompletionIndex = Selected; + } + } + else + { + Context.m_DropdownContext.m_ListBox.SetActive(false); + } +} + +template +int CEditor::DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &fnMatchCallback) +{ + // Do an edit box with a possible dropdown + // This is a generic method which can display any data we want + + pDropdown->m_Selected = clamp(pDropdown->m_Selected, -1, (int)vData.size() - 1); + + if(Input()->KeyPress(KEY_SPACE) && Input()->ModifierIsPressed()) + { // Handle Ctrl+Space to show available options + pDropdown->m_ShortcutUsed = true; + // Remove inserted space + pLineInput->SetRange("", pLineInput->GetCursorOffset() - 1, pLineInput->GetCursorOffset()); + } + + if((!pDropdown->m_ShouldHide && !pLineInput->IsEmpty() && (pLineInput->IsActive() || pDropdown->m_MousePressedInside)) || pDropdown->m_ShortcutUsed) + { + if(!pDropdown->m_Visible) + { + pDropdown->m_DidBecomeVisible = true; + pDropdown->m_Visible = true; + } + else if(pDropdown->m_DidBecomeVisible) + pDropdown->m_DidBecomeVisible = false; + + if(!pLineInput->IsEmpty() || !pLineInput->IsActive()) + pDropdown->m_ShortcutUsed = false; + + int CurrentSelected = pDropdown->m_Selected; + + // Use tab to navigate through entries + if(UI()->ConsumeHotkey(CUI::HOTKEY_TAB) && !vData.empty()) + { + int Direction = Input()->ShiftIsPressed() ? -1 : 1; + + pDropdown->m_Selected += Direction; + if(pDropdown->m_Selected < 0) + pDropdown->m_Selected = (int)vData.size() - 1; + pDropdown->m_Selected %= vData.size(); + } + + int Selected = RenderEditBoxDropdown(pDropdown, *pEditBoxRect, pLineInput, x, MaxHeight, AutoWidth, vData, fnMatchCallback); + if(Selected != -1) + pDropdown->m_Selected = Selected; + + if(CurrentSelected != pDropdown->m_Selected) + pDropdown->m_ListBox.ScrollToSelected(); + + return pDropdown->m_Selected; + } + else + { + pDropdown->m_ShortcutUsed = false; + pDropdown->m_Visible = false; + pDropdown->m_ListBox.SetActive(false); + pDropdown->m_Selected = -1; + } + + return -1; +} + +template +int CEditor::RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &fnMatchCallback) +{ + // Render a dropdown tied to an edit box/line input + auto *pListBox = &pDropdown->m_ListBox; + + pListBox->SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen() && pLineInput->IsActive()); + pListBox->SetScrollbarWidth(15.0f); + + const int NumEntries = vData.size(); + + // Setup the rect + static float s_Width = View.w; + CUIRect CommandsDropdown = View; + CommandsDropdown.y += View.h + 0.1f; + CommandsDropdown.x = x; + if(AutoWidth) + CommandsDropdown.w = s_Width + pListBox->ScrollbarWidth(); + + pListBox->SetActive(NumEntries > 0); + if(NumEntries > 0) + { + // Draw the background + CommandsDropdown.h = minimum(NumEntries * 15.0f + 1.0f, MaxHeight); + CommandsDropdown.Draw(ColorRGBA(0.1f, 0.1f, 0.1f, 0.9f), IGraphics::CORNER_ALL, 3.0f); + + if(UI()->MouseButton(0) && UI()->MouseInside(&CommandsDropdown)) + pDropdown->m_MousePressedInside = true; + + // Do the list box + int Selected = pDropdown->m_Selected; + pListBox->DoStart(15.0f, NumEntries, 1, 3, Selected, &CommandsDropdown); + CUIRect Label; + + int NewIndex = Selected; + float LargestWidth = 0; + for(int i = 0; i < NumEntries; i++) + { + const CListboxItem Item = pListBox->DoNextItem(&vData[i], Selected == i); + + Item.m_Rect.VMargin(4.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + + // Call the callback to fill the current line string + char aBuf[128]; + fnMatchCallback(vData.at(i), aBuf, Props.m_vColorSplits); + + LargestWidth = maximum(LargestWidth, TextRender()->TextWidth(12.0f, aBuf) + 10.0f); + if(!Item.m_Visible) + continue; + + UI()->DoLabel(&Label, aBuf, 12.0f, TEXTALIGN_ML, Props); + + if(UI()->ActiveItem() == &vData[i]) + { + // If we selected an item (by clicking on it for example), then set the active item back to the + // line input so we don't loose focus + NewIndex = i; + UI()->SetActiveItem(pLineInput); + } + } + + s_Width = LargestWidth; + + int EndIndex = pListBox->DoEnd(); + if(NewIndex == Selected) + NewIndex = EndIndex; + + if(pDropdown->m_MousePressedInside && !UI()->MouseButton(0)) + { + UI()->SetActiveItem(pLineInput); + pDropdown->m_MousePressedInside = false; + } + + if(NewIndex != Selected) + { + UI()->SetActiveItem(pLineInput); + return NewIndex; + } + } + return -1; +} + +void CEditor::MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits) +{ + // Check the match argument index. + // If it's -1, we're displaying the list of available map settings names + // If its >= 0, we're displaying the list of possible values matches for that argument + if(Match.m_ArgIndex == -1) + { + IMapSetting *pInfo = (IMapSetting *)Match.m_pData; + vColorSplits = { + {str_length(pInfo->m_pName) + 1, -1, ColorRGBA(0.6f, 0.6f, 0.6f, 1)}, // Darker arguments + }; + + if(pInfo->m_Type == IMapSetting::SETTING_INT) + { + str_format(aOutput, sizeof(aOutput), "%s i[value]", pInfo->m_pName); + } + else if(pInfo->m_Type == IMapSetting::SETTING_COMMAND) + { + SMapSettingCommand *pCommand = (SMapSettingCommand *)pInfo; + str_format(aOutput, sizeof(aOutput), "%s %s", pCommand->m_pName, pCommand->m_pArgs); + } + } + else + { + str_copy(aOutput, Match.m_pValue); + } +} + +// ---------------------------------------- + +CMapSettingsBackend::CContext *CMapSettingsBackend::ms_pActiveContext = nullptr; + +void CMapSettingsBackend::Init(CEditor *pEditor) +{ + CEditorComponent::Init(pEditor); + + // Register values loader + InitValueLoaders(); + + // Load settings/commands + LoadAllMapSettings(); + + CValuesBuilder Builder(&m_PossibleValuesPerCommand); + + // Load and parse static map settings so we can use them here + for(auto &pSetting : m_vpMapSettings) + { + // We want to parse the arguments of each map setting so we can autocomplete them later + // But that depends on the type of the setting. + // If we have a INT setting, then we know we can only ever have 1 argument which is a integer value + // If we have a COMMAND setting, then we need to parse its arguments + if(pSetting->m_Type == IMapSetting::SETTING_INT) + LoadSettingInt(std::static_pointer_cast(pSetting)); + else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) + LoadSettingCommand(std::static_pointer_cast(pSetting)); + + LoadPossibleValues(Builder(pSetting->m_pName), pSetting); + } + + // Init constraints + LoadConstraints(); +} + +void CMapSettingsBackend::LoadAllMapSettings() +{ + // Gather all config variables having the flag CFGFLAG_GAME + Editor()->ConfigManager()->PossibleConfigVariables("", CFGFLAG_GAME, PossibleConfigVariableCallback, this); + + // Load list of commands + LoadCommand("tune", "s[tuning] f[value]", "Tune variable to value or show current value"); + LoadCommand("tune_zone", "i[zone] s[tuning] f[value]", "Tune in zone a variable to value"); + LoadCommand("tune_zone_enter", "i[zone] r[message]", "which message to display on zone enter; use 0 for normal area"); + LoadCommand("tune_zone_leave", "i[zone] r[message]", "which message to display on zone leave; use 0 for normal area"); + LoadCommand("mapbug", "s[mapbug]", "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)"); + LoadCommand("switch_open", "i[switch]", "Whether a switch is deactivated by default (otherwise activated)"); +} + +void CMapSettingsBackend::LoadCommand(const char *pName, const char *pArgs, const char *pHelp) +{ + m_vpMapSettings.emplace_back(std::make_shared(pName, pHelp, pArgs)); +} + +void CMapSettingsBackend::LoadSettingInt(const std::shared_ptr &pSetting) +{ + // We load an int argument here + m_ParsedCommandArgs[pSetting].emplace_back(); + auto &Arg = m_ParsedCommandArgs[pSetting].back(); + str_copy(Arg.m_aName, "value"); + Arg.m_Type = 'i'; +} + +void CMapSettingsBackend::LoadSettingCommand(const std::shared_ptr &pSetting) +{ + // This method parses a setting into its arguments (name and type) so we can later + // use them to validate the current input as well as display the current argument value + // over the line input. + + m_ParsedCommandArgs[pSetting].clear(); + const char *pIterator = pSetting->m_pArgs; + + char Type; + + while(*pIterator) + { + if(*pIterator == '?') // Skip optional values as a map setting should not have optional values + pIterator++; + + Type = *pIterator; + pIterator++; + while(*pIterator && *pIterator != '[') + pIterator++; + pIterator++; // skip '[' + + const char *pNameStart = pIterator; + + while(*pIterator && *pIterator != ']') + pIterator++; + + size_t Len = pIterator - pNameStart; + pIterator++; // Skip ']' + + if(Len + 1 >= sizeof(SParsedMapSettingArg::m_aName)) + dbg_msg("editor", "Warning: length of server setting name exceeds limit."); + + // Append parsed arg + m_ParsedCommandArgs[pSetting].emplace_back(); + auto &Arg = m_ParsedCommandArgs[pSetting].back(); + str_copy(Arg.m_aName, pNameStart, Len + 1); + Arg.m_Type = Type; + + pIterator = str_skip_whitespaces_const(pIterator); + } +} + +void CMapSettingsBackend::LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr &pSetting) +{ + // Call the value loader for that setting + auto Iter = m_LoaderFunctions.find(pSetting->m_pName); + if(Iter == m_LoaderFunctions.end()) + return; + + (*Iter->second)(Builder); +} + +void CMapSettingsBackend::RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader) +{ + // Registers a value loader function for a specific setting name + m_LoaderFunctions[pSettingName] = pfnLoader; +} + +void CMapSettingsBackend::LoadConstraints() +{ + // Make an instance of constraint builder + CCommandArgumentConstraintBuilder Command(&m_ArgConstraintsPerCommand); + + // Define constraints like this + // This is still a bit sad as we have to do it manually here. + Command("tune", 2).Unique(0); + Command("tune_zone", 3).Multiple(0).Unique(1); + Command("tune_zone_enter", 2).Unique(0); + Command("tune_zone_leave", 2).Unique(0); + Command("switch_open", 1).Unique(0); + Command("mapbug", 1).Unique(0); +} + +void CMapSettingsBackend::PossibleConfigVariableCallback(const SConfigVariable *pVariable, void *pUserData) +{ + CMapSettingsBackend *pBackend = (CMapSettingsBackend *)pUserData; + + if(pVariable->m_Type == SConfigVariable::VAR_INT) + { + SIntConfigVariable *pIntVariable = (SIntConfigVariable *)pVariable; + pBackend->m_vpMapSettings.emplace_back(std::make_shared( + pIntVariable->m_pScriptName, + pIntVariable->m_pHelp, + pIntVariable->m_Default, + pIntVariable->m_Min, + pIntVariable->m_Max)); + } +} + +void CMapSettingsBackend::CContext::Reset() +{ + m_LastCursorOffset = 0; + m_CursorArgIndex = -1; + m_pCurrentSetting = nullptr; + m_vCurrentArgs.clear(); + m_aCommand[0] = '\0'; + m_DropdownContext.m_Selected = -1; + m_CurrentCompletionIndex = -1; + m_DropdownContext.m_ShortcutUsed = false; + m_DropdownContext.m_MousePressedInside = false; + m_DropdownContext.m_Visible = false; + m_DropdownContext.m_ShouldHide = false; + + ClearError(); +} + +void CMapSettingsBackend::CContext::Update() +{ + // 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, + // if they are valid or not, etc. + + m_pCurrentSetting = nullptr; + m_vCurrentArgs.clear(); + + const char *pStr = InputString(); + const char *pIterator = pStr; + + // Get the command/setting + m_aCommand[0] = '\0'; + while(pIterator && *pIterator != ' ' && *pIterator != '\0') + pIterator++; + + str_copy(m_aCommand, pStr, (pIterator - pStr) + 1); + + // Get the command if it is a recognized one + for(auto &pSetting : m_pBackend->m_vpMapSettings) + { + if(str_comp_nocase(m_aCommand, pSetting->m_pName) == 0) + { + m_pCurrentSetting = pSetting; + break; + } + } + + // Parse args + ParseArgs(pIterator); +} + +void CMapSettingsBackend::CContext::ParseArgs(const char *pStr) +{ + // This method parses the arguments of the current command, starting at pStr + + enum EError + { + ERROR_NONE = 0, + ERROR_TOO_MANY_ARGS, + ERROR_INVALID_VALUE, + ERROR_UNKNOWN_VALUE, + ERROR_INCOMPLETE, + ERROR_OUT_OF_RANGE, + ERROR_UNKNOWN + }; + + ClearError(); + + const char *pIterator = pStr; + const char *pLineInputStr = InputString(); + + if(!pStr || *pStr == '\0') + return; // No arguments + + // NextArg is used to get the contents of the current argument and go to the next argument position + // It outputs the length of the argument in pLength and returns a boolean indicating if the parsing + // of that argument is valid or not (only the case when using strings with quotes (")) + auto &&NextArg = [&](const char *pArg, int *pLength) { + if(*pIterator == '"') + { + pIterator++; + bool Valid = true; + bool IsEscape = false; + + while(true) + { + if(pIterator[0] == '"' && !IsEscape) + break; + else if(pIterator[0] == 0) + { + Valid = false; + break; + } + + if(pIterator[0] == '\\' && !IsEscape) + IsEscape = true; + else if(IsEscape) + IsEscape = false; + + pIterator++; + } + const char *pEnd = ++pIterator; + pIterator = str_skip_to_whitespace_const(pIterator); + + // Make sure there are no other characters at the end, otherwise the string is invalid. + // E.g. "abcd"ef is invalid + Valid = Valid && pIterator == pEnd; + *pLength = pEnd - pArg; + + return Valid; + } + else + { + pIterator = str_skip_to_whitespace_const(pIterator); + *pLength = pIterator - pArg; + return true; + } + }; + + // Simple validation of string. Checks that it does not contain unescaped " in the middle of it. + auto &&ValidateStr = [](const char *pString) -> bool { + const char *pIt = pString; + bool IsEscape = false; + while(*pIt) + { + if(pIt[0] == '"' && !IsEscape) + return false; + + if(pIt[0] == '\\' && !IsEscape) + IsEscape = true; + else if(IsEscape) + IsEscape = false; + + pIt++; + } + return true; + }; + + const int CommandArgCount = m_pCurrentSetting != nullptr ? m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size() : 0; + int ArgIndex = 0; + EError Error = ERROR_NONE; + + // 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); + + // Parsing beings + while(*pIterator) + { + pIterator++; // Skip whitespace + PosX += WW; // Add whitespace width + + // Insert argument here + char Char = *pIterator; + const char *pArgStart = pIterator; + int Length; + bool Valid = NextArg(pArgStart, &Length); // Get contents and go to next argument position + size_t Offset = pArgStart - pLineInputStr; // Compute offset from the start of the input + + // Add new argument, copy the argument contents + m_vCurrentArgs.emplace_back(); + auto &NewArg = m_vCurrentArgs.back(); + str_copy(NewArg.m_aValue, pArgStart, Length + 1); + + // Validate argument from the parsed argument of the current setting. + // If current setting is not valid, then there are no arguments which results in an error. + + char Type = 'u'; // u = unknown, only possible for unknown commands when m_AllowUnknownCommands is true. + if(ArgIndex < CommandArgCount) + { + SParsedMapSettingArg &Arg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); + if(Arg.m_Type == 'r') + { + // Rest of string, should add all the string if there was no quotes + // Otherwise, only get the contents in the quotes, and consider content after that as other arguments + if(Char != '"') + { + while(*pIterator) + pIterator++; + Length = pIterator - pArgStart; + str_copy(NewArg.m_aValue, pArgStart, Length + 1); + } + + if(!Valid || (Char != '"' && !ValidateStr(NewArg.m_aValue))) + Error = ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 'i') + { + // Validate int + if(!str_toint(NewArg.m_aValue, nullptr)) + Error = ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 'f') + { + // Validate float + if(!str_tofloat(NewArg.m_aValue, nullptr)) + Error = ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 's') + { + // Validate string + if(!Valid) + Error = ERROR_INVALID_VALUE; + } + + // Extended argument validation: + // for int settings it checks that the value is in range + // for command settings, it checks that the value is one of the possible values if there are any + EValidationResult Result = ValidateArg(ArgIndex, NewArg.m_aValue); + if(Length && !Error && Result != EValidationResult::VALID) + { + if(Result == EValidationResult::ERROR) + Error = ERROR_INVALID_VALUE; // Invalid argument value (invalid int, invalid float) + else if(Result == EValidationResult::UNKNOWN) + Error = ERROR_UNKNOWN_VALUE; // Unknown argument value + else if(Result == EValidationResult::INCOMPLETE) + Error = ERROR_INCOMPLETE; // Incomplete argument in case of possible values + else if(Result == EValidationResult::OUT_OF_RANGE) + Error = ERROR_OUT_OF_RANGE; // Out of range argument value in case of int settings + else + Error = ERROR_UNKNOWN; // Unknown error + } + + Type = Arg.m_Type; + } + else + { + // Error: too many arguments + Error = ERROR_TOO_MANY_ARGS; + } + + // Fill argument informations + NewArg.m_X = PosX; + NewArg.m_Start = Offset; + NewArg.m_End = Offset + Length; + NewArg.m_Error = Error != ERROR_NONE || Length == 0; + NewArg.m_ExpectedType = Type; + + // Do not emit an error if we allow unknown commands and the current setting is invalid + if(m_AllowUnknownCommands && m_pCurrentSetting == nullptr) + NewArg.m_Error = false; + + // Check error and fill the error field with different messages + if(Error == ERROR_INVALID_VALUE || Error == ERROR_UNKNOWN_VALUE || Error == ERROR_OUT_OF_RANGE) + { + size_t ErrorArgIndex = m_vCurrentArgs.size() - 1; + SCurrentSettingArg &ErrorArg = m_vCurrentArgs.back(); + SParsedMapSettingArg &SettingArg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); + 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); + 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); + } + m_Error.m_ArgIndex = (int)ErrorArgIndex; + break; + } + else if(Error == ERROR_TOO_MANY_ARGS) + { + if(m_pCurrentSetting != nullptr) + { + str_copy(m_Error.m_aMessage, "Too many arguments"); + m_Error.m_ArgIndex = ArgIndex; + break; + } + else if(!m_AllowUnknownCommands) + { + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", m_aCommand); + m_Error.m_ArgIndex = -1; + break; + } + } + + PosX += m_pBackend->TextRender()->TextWidth(FONT_SIZE, pArgStart, Length); // Advance argument position + ArgIndex++; + } +} + +void CMapSettingsBackend::CContext::ClearError() +{ + m_Error.m_aMessage[0] = '\0'; +} + +bool CMapSettingsBackend::CContext::UpdateCursor(bool Force) +{ + // This method updates the cursor offset in this class from + // the cursor offset of the line input. + // It also updates the argument index where the cursor is at + // and the possible values matches if the argument index changes. + // Returns true in case the cursor changed position + + size_t Offset = m_pLineInput->GetCursorOffset(); + if(Offset == m_LastCursorOffset && !Force) + return false; + + m_LastCursorOffset = Offset; + int NewArg = m_CursorArgIndex; + + // Update current argument under cursor + bool FoundArg = false; + for(int i = (int)m_vCurrentArgs.size() - 1; i >= 0; i--) + { + if(Offset >= m_vCurrentArgs[i].m_Start) + { + NewArg = i; + FoundArg = true; + break; + } + } + + if(!FoundArg) + NewArg = -1; + + bool ShouldUpdate = NewArg != m_CursorArgIndex; + m_CursorArgIndex = NewArg; + + if(m_DropdownContext.m_Selected == -1 || ShouldUpdate || Force) + { + // Update possible commands from cursor + UpdatePossibleMatches(); + } + + return true; +} + +EValidationResult CMapSettingsBackend::CContext::ValidateArg(int Index, const char *pArg) +{ + if(!m_pCurrentSetting) + return EValidationResult::ERROR; + + // Check if this argument is valid against current argument + if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) + { + std::shared_ptr pSetting = std::static_pointer_cast(m_pCurrentSetting); + if(Index > 0) + return EValidationResult::ERROR; + + int Value; + if(!str_toint(pArg, &Value)) // Try parse the integer + return EValidationResult::ERROR; + + return Value >= pSetting->m_Min && Value <= pSetting->m_Max ? EValidationResult::VALID : EValidationResult::OUT_OF_RANGE; + } + else if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_COMMAND) + { + auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting); + if(Index < (int)vArgs.size()) + { + auto It = m_pBackend->m_PossibleValuesPerCommand.find(m_pCurrentSetting->m_pName); + if(It != m_pBackend->m_PossibleValuesPerCommand.end()) + { + auto ValuesIt = It->second.find(Index); + if(ValuesIt != It->second.end()) + { + // This means that we have possible values for this argument for this setting + // In order to validate such arg, we have to check if it maches any of the possible values + const bool EqualsAny = std::any_of(ValuesIt->second.begin(), ValuesIt->second.end(), [pArg](auto *pValue) { return str_comp_nocase(pArg, pValue) == 0; }); + + // If equals, then argument is valid + if(EqualsAny) + return EValidationResult::VALID; + else if(Index != m_CursorArgIndex) + return EValidationResult::ERROR; // If we're not checking current argument, this is an error + + // Here we check if argument is incomplete + const bool StartsAny = std::any_of(ValuesIt->second.begin(), ValuesIt->second.end(), [pArg](auto *pValue) { return str_startswith_nocase(pValue, pArg) != nullptr; }); + if(StartsAny) + return EValidationResult::INCOMPLETE; + + return EValidationResult::ERROR; + } + } + } + + // If we get here, it means there are no posssible values for that specific argument. + // The validation for specific types such as int and floats were done earlier so if we get here + // we know the argument is valid. + // String and "rest of string" types are valid by default. + return EValidationResult::VALID; + } + + return EValidationResult::ERROR; +} + +void CMapSettingsBackend::CContext::UpdatePossibleMatches() +{ + // This method updates the possible values matches based on the cursor position within the current argument in the line input. + // For example ("|" is the cursor): + // - Typing "sv_deep|" will show "sv_deepfly" as a possible match in the dropdown + // Moving the cursor: "sv_|deep" will show all possible commands starting with "sv_" + // - Typing "tune ground_frict|" will show "ground_friction" as possible match + // Moving the cursor: "tune ground_|frict" will show all possible values starting with "ground_" for that argument (argument 0 of "tune" setting) + + m_vPossibleMatches.clear(); + m_DropdownContext.m_Selected = -1; + + // First case: argument index under cursor is -1 => we're on the command/setting name + if(m_CursorArgIndex == -1) + { + // Use a substring from the start of the input to the cursor offset + char aSubString[128]; + str_copy(aSubString, m_aCommand, minimum((int)m_LastCursorOffset + 1, 128)); + + // Iterate through available map settings and find those which the beginning matches with the command/setting name we are writing + for(auto &pSetting : m_pBackend->m_vpMapSettings) + { + if(str_startswith_nocase(pSetting->m_pName, aSubString)) + { + m_vPossibleMatches.emplace_back(SPossibleValueMatch{ + pSetting->m_pName, + m_CursorArgIndex, + pSetting.get(), + }); + } + } + + // If there are no matches, then the command is unknown + 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); + m_Error.m_ArgIndex = -1; + } + } + else + { + // Second case: we are on an argument + if(!m_pCurrentSetting) // If we are on an argument of an unknown setting, we can't handle it => no possible values, ever. + return; + + if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) + { + // No possible values for int settings. + // Maybe we can add "0" and "1" as possible values for settings that are binary. + } + else + { + // Get the parsed arguments for the current setting + auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting); + // Make sure we are not out of bounds + if(m_CursorArgIndex < (int)vArgs.size() && m_CursorArgIndex < (int)m_vCurrentArgs.size()) + { + // Check if there are possible values for this command + auto It = m_pBackend->m_PossibleValuesPerCommand.find(m_pCurrentSetting->m_pName); + if(It != m_pBackend->m_PossibleValuesPerCommand.end()) + { + // If that's the case, then check if there are possible values for the current argument index the cursor is on + auto ValuesIt = It->second.find(m_CursorArgIndex); + if(ValuesIt != It->second.end()) + { + // If that's the case, then do the same as previously, we check for each value if they match + // with the current argument value + + auto &CurrentArg = m_vCurrentArgs.at(m_CursorArgIndex); + int SubstringLength = minimum(m_LastCursorOffset, CurrentArg.m_End) - CurrentArg.m_Start; + + // Substring based on the cursor position inside that argument + char aSubString[160]; + str_copy(aSubString, CurrentArg.m_aValue, SubstringLength + 1); + + for(auto &pValue : ValuesIt->second) + { + if(str_startswith_nocase(pValue, aSubString)) + { + m_vPossibleMatches.emplace_back(SPossibleValueMatch{ + pValue, + m_CursorArgIndex, + nullptr, + }); + } + } + } + } + } + } + } +} + +bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event) +{ + if(!m_pLineInput->IsActive()) + return false; + + if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT) && !m_pBackend->Input()->ModifierIsPressed() && !m_pBackend->Input()->AltIsPressed()) + { + // How to make this better? + // This checks when we press any key that is not handled by the dropdown + // When that's the case, it means we confirm the completion if we have a valid completion index + if(Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT && Event.m_Key != KEY_UP && Event.m_Key != KEY_DOWN && !(Event.m_Key >= KEY_MOUSE_1 && Event.m_Key <= KEY_MOUSE_WHEEL_RIGHT)) + { + if(m_CurrentCompletionIndex != -1) + { + m_CurrentCompletionIndex = -1; + m_DropdownContext.m_Selected = -1; + Update(); + UpdateCursor(true); + } + } + } + + return false; +} + +const char *CMapSettingsBackend::CContext::InputString() const +{ + 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); +const ColorRGBA CMapSettingsBackend::CContext::ms_ErrorColor = ColorRGBA(240 / 255.0f, 70 / 255.0f, 70 / 255.0f, 1.0f); + +void CMapSettingsBackend::CContext::ColorArguments(std::vector &vColorSplits) const +{ + // Get argument color based on its type + auto &&GetArgumentColor = [](char Type) -> ColorRGBA { + if(Type == 'u') + return ms_ArgumentUnknownColor; + else if(Type == 's' || Type == 'r') + return ms_ArgumentStringColor; + else if(Type == 'i' || Type == 'f') + return ms_ArgumentNumberColor; + return ms_ErrorColor; // Invalid arg type + }; + + // Iterate through all the current arguments and color them + for(int i = 0; i < ArgCount(); i++) + { + const auto &Argument = Arg(i); + // Color is based on the error flag and the type of the argument + auto Color = Argument.m_Error ? ms_ErrorColor : GetArgumentColor(Argument.m_ExpectedType); + vColorSplits.emplace_back(Argument.m_Start, Argument.m_End - Argument.m_Start, Color); + } + + if(!m_pLineInput->IsEmpty()) + { + if(!CommandIsValid() && !m_AllowUnknownCommands) + { + // If command is invalid, override color splits with red + vColorSplits = {{0, -1, ms_ErrorColor}}; + } + else if(HasError()) + { + // If there is an error, then color the wrong part of the input + vColorSplits.emplace_back(ErrorOffset(), -1, ms_ErrorColor); + } + } +} + +int CMapSettingsBackend::CContext::CheckCollision(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. + // For this, we use argument constraints that we define in CMapSettingsCommandObject::LoadConstraints(). + // For example, the "tune" command can be added multiple times, but only if the actual tune argument is different, thus + // the tune argument must be defined as UNIQUE. + // 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(); + + struct SArgument + { + char m_aValue[128]; + SArgument(const char *pStr) + { + str_copy(m_aValue, pStr); + } + }; + + struct SLineArgs + { + int m_Index; + 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; + const char *pIt = pStr; + char aBuffer[128]; + while((pIt = str_next_token(pIt, " ", aBuffer, 128))) + vaArgs.emplace_back(aBuffer); + return vaArgs; + }; + + // Define the result of the check + Result = ECollisionCheckResult::ERROR; + + // First case: the command is not a valid (recognized) command. + if(!CommandIsValid()) + { + // If we don't allow unknown commands, then we know there is no collision + // and the check results in an error. + if(!m_AllowUnknownCommands) + return -1; + + if(m_pLineInput->GetLength() == 0) + return -1; + + // If we get here, it means we allow unknown commands. + // For them, we need to check if a similar exact command exists or not in the settings list. + // If it does, then we found a collision, and the result is REPLACE. + for(int i = 0; i < (int)vSettings.size(); i++) + { + if(str_comp_nocase(vSettings[i].m_aCommand, pInputString) == 0) + { + Result = ECollisionCheckResult::REPLACE; + return i; + } + } + + // If nothing was found, then we must ensure that the command, although unknown, is somewhat valid + // by checking if the command contains a space and that there is at least one non-empty argument. + const char *pSpace = str_find(pInputString, " "); + if(!pSpace || !*(pSpace + 1)) + Result = ECollisionCheckResult::ERROR; + else + Result = ECollisionCheckResult::ADD; + + return -1; // No collision + } + + // Second case: the command is valid. + // In this case, we know we have a valid setting name, which means we can use everything we have in this class which are + // related to valid map settings, such as parsed command arguments, etc. + + const std::shared_ptr &pSetting = Setting(); + if(pSetting->m_Type == IMapSetting::SETTING_INT) + { + // For integer settings, the check is quite simple as we know + // we can only ever have 1 argument. + + // The integer setting cannot be added multiple times, which means if a collision was found, then the only result we + // 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) { + 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 + }); + + if(It == vSettings.end()) + { + // If nothing was found, then there is no collision and we can add that command to the list + Result = ECollisionCheckResult::ADD; + return -1; + } + else + { + // Otherwise, we can only replace it + Result = ECollisionCheckResult::REPLACE; + return It - vSettings.begin(); // This is the index of the colliding line + } + } + else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) + { + // For "command" settings, this is a bit more complex as we have to use argument constraints. + // The general idea is to split every map setting in their arguments separated by spaces. + // Then, for each argument, we check if it collides with any of the map settings. When that's the case, + // we need to check the constraint of the argument. If set to UNIQUE, then that's a collision and we can only + // replace the command in the list. + // If set to anything else, we consider that it is not a collision and we move to the next argument. + // This system is simple and somewhat flexible as we only need to declare the constraints, the rest should be + // handled automatically. + + std::shared_ptr pSettingCommand = std::static_pointer_cast(pSetting); + // Get matching lines for that command + std::vector vvArgs; + for(int i = 0; i < (int)vSettings.size(); i++) + { + auto &Setting = vSettings.at(i); + + // Split this setting into its arguments + std::vector vArgs = SplitSetting(Setting.m_aCommand); + // Only keep settings that match with the current input setting name + if(!vArgs.empty() && str_comp_nocase(vArgs[0].m_aValue, pSettingCommand->m_pName) == 0) + { + // When that's the case, we save them + vArgs.erase(vArgs.begin()); + vvArgs.push_back(SLineArgs{ + i, + vArgs, + }); + } + } + + // Here is the simple algorithm to check for collisions according to argument constraints + bool Error = false; + int CollidingLineIndex = -1; + for(int ArgIndex = 0; ArgIndex < ArgCount(); ArgIndex++) + { + bool Collide = false; + const char *pValue = Arg(ArgIndex).m_aValue; + for(auto &Line : vvArgs) + { + // Check first colliding line + if(str_comp_nocase(pValue, Line.m_vArgs[ArgIndex].m_aValue) == 0) + { + Collide = true; + CollidingLineIndex = Line.m_Index; + Error = m_pBackend->ArgConstraint(pSetting->m_pName, ArgIndex) == CMapSettingsBackend::EArgConstraint::UNIQUE; + } + if(Error) + break; + } + + // If we did not collide with any of the lines for that argument, we're good to go + // (or if we had an error) + if(!Collide || Error) + break; + } + + // The result is either REPLACE when we found a collision, or ADD + Result = Error ? ECollisionCheckResult::REPLACE : ECollisionCheckResult::ADD; + return CollidingLineIndex; + } + + return -1; +} + +bool CMapSettingsBackend::CContext::Valid() const +{ + // Check if the entire setting is valid or not + + // Check if command is valid + if(m_pCurrentSetting) + { + // Check if all arguments are valid + const bool ArgumentsValid = std::all_of(m_vCurrentArgs.begin(), m_vCurrentArgs.end(), [](const SCurrentSettingArg &Arg) { + return !Arg.m_Error; + }); + + if(!ArgumentsValid) + 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; + } + else + { + // If we have an invalid setting, then we consider the entire setting as valid if we allow unknown commands + // as we cannot handle them. + return m_AllowUnknownCommands; + } +} + +void CMapSettingsBackend::CContext::GetCommandHelpText(char *pStr, int Length) const +{ + if(!m_pCurrentSetting) + return; + + str_copy(pStr, m_pCurrentSetting->m_pHelp, Length); +} + +// ------ loaders + +void CMapSettingsBackend::InitValueLoaders() +{ + // Load the different possible values for some specific settings + RegisterLoader("tune", SValueLoader::LoadTuneValues); + RegisterLoader("tune_zone", SValueLoader::LoadTuneZoneValues); + RegisterLoader("mapbug", SValueLoader::LoadMapBugs); +} + +void SValueLoader::LoadTuneValues(const CSettingValuesBuilder &TuneBuilder) +{ + // Add available tuning names to argument 0 of setting "tune" + LoadArgumentTuneValues(TuneBuilder.Argument(0)); +} + +void SValueLoader::LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder) +{ + // Add available tuning names to argument 1 of setting "tune_zone" + LoadArgumentTuneValues(TuneZoneBuilder.Argument(1)); +} + +void SValueLoader::LoadMapBugs(const CSettingValuesBuilder &BugBuilder) +{ + // Get argument 0 of setting "mapbug" + auto ArgBuilder = BugBuilder.Argument(0); + // Add available map bugs options + ArgBuilder.Add("grenade-doubleexplosion@ddnet.tw"); +} + +void SValueLoader::LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder) +{ + // Iterate through available tunings add their name to the list + for(int i = 0; i < CTuningParams::Num(); i++) + { + 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 new file mode 100644 index 000000000..f2e09b6fd --- /dev/null +++ b/src/game/editor/editor_server_settings.h @@ -0,0 +1,333 @@ +#ifndef GAME_EDITOR_EDITOR_SERVER_SETTINGS_H +#define GAME_EDITOR_EDITOR_SERVER_SETTINGS_H + +#include "component.h" +#include "editor_ui.h" + +#include +#include +#include + +class CEditor; +struct SMapSettingInt; +struct SMapSettingCommand; +struct IMapSetting; +class CLineInput; + +// A parsed map setting argument, storing the name and the type +// Used for validation and to display arguments names +struct SParsedMapSettingArg +{ + char m_aName[32]; + char m_Type; +}; + +// An argument for the current setting +struct SCurrentSettingArg +{ + char m_aValue[160]; // Value of the argument + float m_X; // The X position + size_t m_Start; // Start offset within the input string + size_t m_End; // End offset within the input string + bool m_Error; // If the argument is wrong or not + char m_ExpectedType; // The expected type +}; + +struct SPossibleValueMatch +{ + const char *m_pValue; // Possible value string + int m_ArgIndex; // Argument for that possible value + const void *m_pData; // Generic pointer to pass specific data +}; + +struct SCommandParseError +{ + char m_aMessage[256]; + int m_ArgIndex; +}; + +// -------------------------------------- +// Builder classes & methods to generate list of possible values +// easily for specific settings and arguments. +// It uses a container stored inside CMapSettingsBackend. +// Usage: +// CValuesBuilder Builder(&m_Container); +// // Either do it in one go: +// Builder("tune").Argument(0).Add("value_1").Add("value_2"); +// // Or reference the builder (useful when using in a loop): +// auto TuneBuilder = Builder("tune").Argument(0); +// TuneBuilder.Add("value_1"); +// TuneBuilder.Add("value_2"); +// // ... + +using TArgumentValuesList = std::vector; // List of possible values +using TSettingValues = std::map; // Possible values per argument +using TSettingsArgumentValues = std::map; // Possible values per argument, per command/setting name + +class CValuesBuilder; +class CSettingValuesBuilder; +class CArgumentValuesListBuilder +{ +public: + CArgumentValuesListBuilder &Add(const char *pString) + { + m_pContainer->emplace_back(pString); + return *this; + } + +private: + CArgumentValuesListBuilder(std::vector *pContainer) : + m_pContainer(pContainer) {} + + std::vector *m_pContainer; + friend class CSettingValuesBuilder; +}; + +class CSettingValuesBuilder +{ +public: + CArgumentValuesListBuilder Argument(int Arg) const + { + return CArgumentValuesListBuilder(&(*m_pContainer)[Arg]); + } + +private: + CSettingValuesBuilder(TSettingValues *pContainer) : + m_pContainer(pContainer) {} + + friend class CValuesBuilder; + TSettingValues *m_pContainer; +}; + +class CValuesBuilder +{ +public: + CValuesBuilder(TSettingsArgumentValues *pContainer) : + m_pContainer(pContainer) + { + } + + CSettingValuesBuilder operator()(const char *pSettingName) const + { + return CSettingValuesBuilder(&(*m_pContainer)[pSettingName]); + } + +private: + TSettingsArgumentValues *m_pContainer; +}; + +// -------------------------------------- + +struct SValueLoader +{ + static void LoadTuneValues(const CSettingValuesBuilder &TuneBuilder); + static void LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder); + static void LoadMapBugs(const CSettingValuesBuilder &BugBuilder); + static void LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder); +}; + +enum class EValidationResult +{ + VALID = 0, + ERROR, + INCOMPLETE, + UNKNOWN, + OUT_OF_RANGE, +}; + +enum class ECollisionCheckResult +{ + ERROR, + REPLACE, + ADD +}; + +class CMapSettingsBackend : public CEditorComponent +{ + typedef void (*FLoaderFunction)(const CSettingValuesBuilder &); + +public: // General methods + CMapSettingsBackend() = default; + + void Init(CEditor *pEditor) override; + bool OnInput(const IInput::CEvent &Event) override; + void OnUpdate(); + +public: // Constraints methods + enum class EArgConstraint + { + DEFAULT = 0, + UNIQUE, + MULTIPLE, + }; + + EArgConstraint ArgConstraint(const char *pSettingName, int Arg) const + { + return m_ArgConstraintsPerCommand.at(pSettingName).at(Arg); + } + +public: // Backend methods + const std::vector &ParsedArgs(const std::shared_ptr &pSetting) const + { + return m_ParsedCommandArgs.at(pSetting); + } + +public: // CContext + class CContext + { + static const ColorRGBA ms_ArgumentStringColor; + static const ColorRGBA ms_ArgumentNumberColor; + static const ColorRGBA ms_ArgumentUnknownColor; + static const ColorRGBA ms_ErrorColor; + + friend class CMapSettingsBackend; + + public: + bool CommandIsValid() const { return m_pCurrentSetting != nullptr; } + int CurrentArg() const { return m_CursorArgIndex; } + const char *CurrentArgName() const { return (!m_pCurrentSetting || m_CursorArgIndex < 0 || m_CursorArgIndex >= (int)m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size()) ? nullptr : m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).at(m_CursorArgIndex).m_aName; } + float CurrentArgPos() const { return m_CursorArgIndex == -1 ? 0 : m_vCurrentArgs[m_CursorArgIndex].m_X; } + size_t CurrentArgOffset() const { return m_CursorArgIndex == -1 ? 0 : m_vCurrentArgs[m_CursorArgIndex].m_Start; } + const char *CurrentArgValue() const { return m_CursorArgIndex == -1 ? m_aCommand : m_vCurrentArgs[m_CursorArgIndex].m_aValue; } + const std::vector &PossibleMatches() const { return m_vPossibleMatches; } + bool HasError() const { return m_Error.m_aMessage[0] != '\0'; } + size_t ErrorOffset() const { return m_Error.m_ArgIndex < 0 ? 0 : m_vCurrentArgs.at(m_Error.m_ArgIndex).m_Start; } + const char *Error() const { return m_Error.m_aMessage; } + int ArgCount() const { return (int)m_vCurrentArgs.size(); } + 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; } + + int CheckCollision(ECollisionCheckResult &Result) const; + void Update(); + bool UpdateCursor(bool Force = false); + void Reset(); + void GetCommandHelpText(char *pStr, int Length) const; + bool Valid() const; + void ColorArguments(std::vector &vColorSplits) const; + + bool m_AllowUnknownCommands; + SEditBoxDropdownContext m_DropdownContext; + int m_CurrentCompletionIndex; + + private: // Methods + CContext(CMapSettingsBackend *pMaster, CLineInput *pLineinput) : + m_DropdownContext(), m_pLineInput(pLineinput), m_pBackend(pMaster) + { + m_AllowUnknownCommands = false; + Reset(); + } + + void ClearError(); + EValidationResult ValidateArg(int Index, const char *pArg); + void UpdatePossibleMatches(); + void ParseArgs(const char *pStr); + bool OnInput(const IInput::CEvent &Event); + const char *InputString() const; + void UpdateCompositionString(); + + 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 + int m_CursorArgIndex; // The current argument the cursor is over + std::vector m_vPossibleMatches; // The current matches from cursor argument + size_t m_LastCursorOffset; // Last cursor offset + CLineInput *m_pLineInput; + char m_aCommand[128]; // The current command, not necessarily valid + SCommandParseError m_Error; // Error + + CMapSettingsBackend *m_pBackend; + std::string m_CompositionStringBuffer; + }; + + CContext NewContext(CLineInput *pLineInput) + { + return CContext(this, pLineInput); + } + +private: // Loader methods + void LoadAllMapSettings(); + void LoadCommand(const char *pName, const char *pArgs, const char *pHelp); + void LoadSettingInt(const std::shared_ptr &pSetting); + void LoadSettingCommand(const std::shared_ptr &pSetting); + void InitValueLoaders(); + void LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr &pSetting); + void RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader); + void LoadConstraints(); + + static void PossibleConfigVariableCallback(const struct SConfigVariable *pVariable, void *pUserData); + +private: // Argument constraints + using TArgumentConstraints = std::map; // Constraint per argument index + using TCommandArgumentConstraints = std::map; // Constraints per command/setting name + + // Argument constraints builder + // Used to define arguments constraints for specific commands + // It uses a container stored in CMapSettingsBackend. + // Usage: + // CCommandArgumentConstraintBuilder Command(&m_Container); + // Command("tune", 2).Unique(0); // Defines argument 0 of command "tune" having 2 args as UNIQUE + // Command("tune_zone", 3).Multiple(0).Unique(1); + // // ^ Multiple() currently is only for readable purposes. It can be omited: + // // Command("tune_zone", 3).Unique(1); + // + + class CCommandArgumentConstraintBuilder; + + class CArgumentConstraintsBuilder + { + friend class CCommandArgumentConstraintBuilder; + + private: + CArgumentConstraintsBuilder(TArgumentConstraints *pContainer) : + m_pContainer(pContainer){}; + + TArgumentConstraints *m_pContainer; + + public: + CArgumentConstraintsBuilder &Multiple(int Arg) + { + // Define a multiple argument constraint + (*m_pContainer)[Arg] = EArgConstraint::MULTIPLE; + return *this; + } + + CArgumentConstraintsBuilder &Unique(int Arg) + { + // Define a unique argument constraint + (*m_pContainer)[Arg] = EArgConstraint::UNIQUE; + return *this; + } + }; + + class CCommandArgumentConstraintBuilder + { + public: + CCommandArgumentConstraintBuilder(TCommandArgumentConstraints *pContainer) : + m_pContainer(pContainer) {} + + CArgumentConstraintsBuilder operator()(const char *pSettingName, int ArgCount) + { + for(int i = 0; i < ArgCount; i++) + (*m_pContainer)[pSettingName][i] = EArgConstraint::DEFAULT; + return CArgumentConstraintsBuilder(&(*m_pContainer)[pSettingName]); + } + + private: + TCommandArgumentConstraints *m_pContainer; + }; + + TCommandArgumentConstraints m_ArgConstraintsPerCommand; + +private: // Backend fields + std::vector> m_vpMapSettings; + std::map, std::vector> m_ParsedCommandArgs; // Parsed available settings arguments, used for validation + TSettingsArgumentValues m_PossibleValuesPerCommand; + std::map m_LoaderFunctions; + + static CContext *ms_pActiveContext; + + friend class CEditor; +}; + +#endif diff --git a/src/game/editor/editor_ui.h b/src/game/editor/editor_ui.h new file mode 100644 index 000000000..00799aefa --- /dev/null +++ b/src/game/editor/editor_ui.h @@ -0,0 +1,17 @@ +#ifndef GAME_EDITOR_EDITOR_UI_H +#define GAME_EDITOR_EDITOR_UI_H + +#include + +struct SEditBoxDropdownContext +{ + bool m_Visible = false; + int m_Selected = -1; + CListBox m_ListBox; + bool m_ShortcutUsed = false; + bool m_DidBecomeVisible = false; + bool m_MousePressedInside = false; + bool m_ShouldHide = false; +}; + +#endif \ No newline at end of file