From ec39f29587d6d49a41a672607e4b7c7406e22426 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 27 Nov 2025 21:31:53 +0200 Subject: [PATCH 01/16] Strip name, neo_name, and neo_clantag --- src/game/server/neo/neo_player.cpp | 10 ++++++---- src/game/server/neo/neo_player.h | 4 ++-- src/game/shared/neo/neo_gamerules.cpp | 28 ++++++++++++++++++++------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index d75a80409..672441fc5 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -535,7 +535,7 @@ CNEO_Player::CNEO_Player() m_iNeoStar = NEO_DEFAULT_STAR; m_iXP.GetForModify() = 0; V_memset(m_szNeoName.GetForModify(), 0, sizeof(m_szNeoName)); - m_szNeoNameHasSet = false; + m_bNeoNameHasSet = false; V_memset(m_szNeoClantag.GetForModify(), 0, sizeof(m_szNeoClantag)); V_memset(m_szNeoCrosshair.GetForModify(), 0, sizeof(m_szNeoCrosshair)); @@ -1819,17 +1819,19 @@ const char *CNEO_Player::GetNeoPlayerName(const CNEO_Player *viewFrom) const const char *CNEO_Player::GetNeoPlayerNameDirect() const { - return m_szNeoNameHasSet ? m_szNeoName.Get() : NULL; + return m_bNeoNameHasSet ? m_szNeoName.Get() : NULL; } -void CNEO_Player::SetNeoPlayerName(const char *newNeoName) +bool CNEO_Player::SetNeoPlayerName(const char *newNeoName) { // NEO NOTE (nullsystem): Generally it's never NULL but just incase if (newNeoName) { V_memcpy(m_szNeoName.GetForModify(), newNeoName, sizeof(m_szNeoName)-1); - m_szNeoNameHasSet = true; + m_bNeoNameHasSet = (m_szNeoName.Get()[0] != 0); + return m_bNeoNameHasSet; } + return false; } void CNEO_Player::SetClientWantNeoName(const bool b) diff --git a/src/game/server/neo/neo_player.h b/src/game/server/neo/neo_player.h index 26a8680e5..3d4ea8abc 100644 --- a/src/game/server/neo/neo_player.h +++ b/src/game/server/neo/neo_player.h @@ -150,7 +150,7 @@ class CNEO_Player : public CHL2MP_Player const char *GetNeoPlayerName(const CNEO_Player *viewFrom = nullptr) const; // "neo_name" even if it's nothing const char *GetNeoPlayerNameDirect() const; - void SetNeoPlayerName(const char *newNeoName); + [[nodiscard]] bool SetNeoPlayerName(const char *newNeoName); void SetClientWantNeoName(const bool b); const char *GetNeoClantag() const; @@ -326,7 +326,7 @@ class CNEO_Player : public CHL2MP_Player bool m_bFirstDeathTick; bool m_bCorpseSet; bool m_bPreviouslyReloading; - bool m_szNeoNameHasSet; + bool m_bNeoNameHasSet; float m_flLastAirborneJumpOkTime; float m_flLastSuperJumpTime; diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index b891d26b2..23de65a71 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -3507,10 +3507,20 @@ void CNEORules::ClientSettingsChanged(CBasePlayer *pPlayer) pNEOPlayer->Weapon_SetZoom(pNEOPlayer->m_bInAim); } - const char *pszSteamName = engine->GetClientConVarValue(pPlayer->entindex(), "name"); - const bool clientAllowsNeoName = (0 == StrToInt(engine->GetClientConVarValue(engine->IndexOfEdict(pNEOPlayer->edict()), "cl_onlysteamnick"))); - const char *pszNeoName = engine->GetClientConVarValue(pNEOPlayer->entindex(), "neo_name"); + + char szSteamName[MAX_PLACE_NAME_LENGTH] = ""; + const char* pszSteamName = &szSteamName[0]; + V_strcpy_safe(szSteamName, engine->GetClientConVarValue(pPlayer->entindex(), "name")); + V_StripTrailingWhitespace(&szSteamName[0]); + V_StripLeadingWhitespace(&szSteamName[0]); + + char szNeoName[MAX_PLAYER_NAME_LENGTH] = ""; + const char* pszNeoName = &szNeoName[0]; + V_strcpy_safe(szNeoName, engine->GetClientConVarValue(pNEOPlayer->entindex(), "neo_name")); + V_StripTrailingWhitespace(&szNeoName[0]); + V_StripLeadingWhitespace(&szNeoName[0]); + const char *pszOldNeoName = pNEOPlayer->GetNeoPlayerNameDirect(); bool updateDupeCheck = false; @@ -3526,19 +3536,23 @@ void CNEORules::ClientSettingsChanged(CBasePlayer *pPlayer) { event->SetInt("userid", pNEOPlayer->GetUserID()); event->SetString("oldname", (pszOldNeoName[0] == '\0') ? pszSteamName : pszOldNeoName); - event->SetString("newname", (pszNeoName[0] == '\0') ? pszSteamName : pszNeoName); + event->SetString("newname", (szNeoName[0] == '\0') ? pszSteamName : pszNeoName); gameeventmanager->FireEvent(event); } } - pNEOPlayer->SetNeoPlayerName(pszNeoName); - updateDupeCheck = true; + if (pNEOPlayer->SetNeoPlayerName(pszNeoName)) + updateDupeCheck = true; } pNEOPlayer->SetClientWantNeoName(clientAllowsNeoName); const auto optClStreamerMode = StrToInt(engine->GetClientConVarValue(engine->IndexOfEdict(pNEOPlayer->edict()), "cl_neo_streamermode")); pNEOPlayer->m_bClientStreamermode = (optClStreamerMode && *optClStreamerMode); - const char *pszNeoClantag = engine->GetClientConVarValue(pNEOPlayer->entindex(), "neo_clantag"); + char szNeoClanTag[NEO_MAX_CLANTAG_LENGTH] = ""; + const char* pszNeoClantag = &szNeoClanTag[0]; + V_strcpy_safe(szNeoClanTag, engine->GetClientConVarValue(pNEOPlayer->entindex(), "neo_clantag")); + V_StripTrailingWhitespace(&szNeoClanTag[0]); + V_StripLeadingWhitespace(&szNeoClanTag[0]); const char *pszOldNeoClantag = pNEOPlayer->GetNeoClantag(); if (V_strcmp(pszOldNeoClantag, pszNeoClantag) != 0) { From 749521cd198709a3c2fa6d64fb01816cad0449c5 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 02:55:27 +0200 Subject: [PATCH 02/16] Code review: also trim in options UI --- src/game/client/cdll_client_int.cpp | 27 ++------------ src/game/client/cdll_client_int.h | 31 +++++++++++++++- src/game/client/neo/ui/neo_root_settings.cpp | 37 ++++++++++++++++++-- src/game/client/neo/ui/neo_ui.cpp | 8 ----- src/game/client/neo/ui/neo_ui.h | 15 ++++++-- src/public/tier1/strtools.h | 6 ++++ src/tier1/strtools.cpp | 6 ++++ 7 files changed, 92 insertions(+), 38 deletions(-) diff --git a/src/game/client/cdll_client_int.cpp b/src/game/client/cdll_client_int.cpp index 7c39d5fa0..a761c67b0 100644 --- a/src/game/client/cdll_client_int.cpp +++ b/src/game/client/cdll_client_int.cpp @@ -1250,27 +1250,6 @@ bool CHLClient::ReplayPostInit() #ifdef NEO extern void NeoToggleConsoleEnforce(); - -template -static void NeoConVarStrLimitChangeCallback(IConVar *cvar, [[maybe_unused]] const char *pOldVal, [[maybe_unused]] float flOldVal) -{ - static bool bStaticCallbackChangedCVar = false; - if (bStaticCallbackChangedCVar) - { - return; - } - - ConVarRef cvarRef(cvar); - if (V_strlen(cvarRef.GetString()) >= STR_LIMIT_SIZE) - { - bStaticCallbackChangedCVar = true; - char mutStr[STR_LIMIT_SIZE]; - V_strcpy_safe(mutStr, cvarRef.GetString()); - Q_UnicodeRepair(mutStr); - cvarRef.SetValue(mutStr); - bStaticCallbackChangedCVar = false; - } -} #endif #ifdef NEO @@ -1418,9 +1397,9 @@ void CHLClient::PostInit() if (g_pCVar) { - g_pCVar->FindVar("neo_name")->InstallChangeCallback(NeoConVarStrLimitChangeCallback); - g_pCVar->FindVar("neo_clantag")->InstallChangeCallback(NeoConVarStrLimitChangeCallback); - g_pCVar->FindVar("cl_neo_crosshair")->InstallChangeCallback(NeoConVarStrLimitChangeCallback); + g_pCVar->FindVar("neo_name")->InstallChangeCallback(NeoConVarFixPrintable); + g_pCVar->FindVar("neo_clantag")->InstallChangeCallback(NeoConVarFixPrintable); + g_pCVar->FindVar("cl_neo_crosshair")->InstallChangeCallback(NeoConVarFixPrintable); g_pCVar->FindVar("sv_use_steam_networking")->SetValue(false); RestrictNeoClientCheats(); diff --git a/src/game/client/cdll_client_int.h b/src/game/client/cdll_client_int.h index ce5da343e..b409c7b6a 100644 --- a/src/game/client/cdll_client_int.h +++ b/src/game/client/cdll_client_int.h @@ -126,7 +126,36 @@ extern AchievementsAndStatsInterface* g_pAchievementsAndStatsInterface; extern bool g_bLevelInitialized; extern bool g_bTextMode; - +#ifdef NEO +template + requires (STR_LIMIT_SIZE > 0) +// De-mangle bad Unicode and trim leading and trailing whitespace. +static void NeoConVarFixPrintable(IConVar *cvar, const char *, float) +{ + // prevent reentrancy + static bool bStaticCallbackChangedCVar = false; + if (bStaticCallbackChangedCVar) + { + return; + } + + char mutStr[STR_LIMIT_SIZE]; + ConVarRef cvarRef(cvar); + V_strcpy_safe(mutStr, cvarRef.GetString()); + + if (V_strlen(cvarRef.GetString()) >= STR_LIMIT_SIZE) + { + Q_UnicodeRepair(mutStr); + } + + V_StripTrailingWhitespace(mutStr); + V_StripLeadingWhitespace(mutStr); + + bStaticCallbackChangedCVar = true; + cvarRef.SetValue(mutStr); + bStaticCallbackChangedCVar = false; +} +#endif // Returns true if a new OnDataChanged event is registered for this frame. bool AddDataChangeEvent( IClientNetworkable *ent, DataUpdateType_t updateType, int *pStoredEvent ); diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 44a6eb96e..ad168dbcf 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -11,7 +11,6 @@ #include #include #include -#include #include "vgui/ISystem.h" #include "neo_hud_killer_damage_info.h" #include "voice_status.h" @@ -21,6 +20,8 @@ #include "neo/ui/neo_utils.h" #include "neo_theme.h" +#include + // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" @@ -1000,6 +1001,36 @@ static const wchar_t *EQUIP_UTILITY_PRIORITY_LABELS[NeoSettings::EquipUtilityPri L"Class Specific First" // EQUIP_UTILITY_PRIORITY_CLASS_SPECIFIC }; +// Trims empty whitespaces from the left-hand side of a text variable. +// Allows one contiguous whitespace on the right side for space-separated input, but no more. +template + requires (maxlen > 1) +FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) +{ + constexpr auto predicate = [](wchar_t c)->bool { + return (V_IsDeprecatedW(c) || + V_IsMeanSpaceW(c) || + std::iswblank(c)); + }; + + if (predicate(input[0])) + { + std::memmove(input, &input[1], sizeof(input) - sizeof(input[0])); + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = 0; + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = 0; + } + + const auto lastNonTerminatingChar = Min(V_wcslen(input) - 1, ARRAYSIZE(input) - 2); + if (lastNonTerminatingChar > 1 && predicate(input[lastNonTerminatingChar])) + { + // Two spaces in a row, delete one. But don't delete both so people can have singular spaces in their variable. + if (predicate(input[lastNonTerminatingChar - 1])) + { + input[lastNonTerminatingChar] = L'\0'; + } + } +} + void NeoSettings_General(NeoSettings *ns) { NeoSettings::General *pGeneral = &ns->general; @@ -1016,8 +1047,8 @@ void NeoSettings_General(NeoSettings *ns) NeoUI::RingBox(L"Selected Background", const_cast(ns->p2WszCBList), ns->iCBListSize, &pGeneral->iBackground); NeoUI::Divider(L"MULTIPLAYER"); - NeoUI::TextEdit(L"Name", pGeneral->wszNeoName, MAX_PLAYER_NAME_LENGTH - 1); - NeoUI::TextEdit(L"Clan tag", pGeneral->wszNeoClantag, NEO_MAX_CLANTAG_LENGTH - 1); + NeoUI::TextEdit(L"Name", pGeneral->wszNeoName, ARRAYSIZE(pGeneral->wszNeoName) - 1, 0, [pGeneral]() { VarTrimmer(pGeneral->wszNeoName); }); + NeoUI::TextEdit(L"Clan tag", pGeneral->wszNeoClantag, NEO_MAX_CLANTAG_LENGTH - 1, 0, [pGeneral]() { VarTrimmer(pGeneral->wszNeoClantag); }); NeoUI::RingBoxBool(L"Show only steam name", &pGeneral->bOnlySteamNick); NeoUI::RingBoxBool(L"Only show clantags when spectator", &pGeneral->bMarkerSpecOnlyClantag); diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index 28607a409..c4487fdf5 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -2161,14 +2161,6 @@ static void BuildUpIrTextWidths(const wchar_t *wszVisText, const int iWszTextSiz } } -void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags) -{ - BeginMultiWidgetHighlighter(2); - Label(wszLeftLabel); - TextEdit(wszText, iMaxWszTextSize, flags); - EndMultiWidgetHighlighter(); -} - void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags) { WidgetFlag wdgFlags = WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE; diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index b7f3d00d0..8ca58c167 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -491,8 +491,19 @@ enum TextEditFlag_ TEXTEDITFLAG_FORCEACTIVE = 1 << 1, // Enforce hot+active focusing in this text edit widget all time, only suitable for single-textedit popups }; typedef int TextEditFlags; -/*1W*/ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); -/*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); +/*1W*/ void TextEdit(wchar_t* wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); +template + requires (std::is_invocable_v || std::is_same_v) +/*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, + const TextEditFlags flags = TEXTEDITFLAG_NONE, + const NextFn&& next=nullptr) +{ + BeginMultiWidgetHighlighter(2); + Label(wszLeftLabel); + TextEdit(wszText, iMaxWszTextSize, flags); + EndMultiWidgetHighlighter(); + if constexpr (std::is_invocable_v) next(); +} /*SW*/ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg = L"", const char *szTextureGroup = ""); // NeoUI::Texture is non-widget, but utilizes NeoUI's image/texture handling diff --git a/src/public/tier1/strtools.h b/src/public/tier1/strtools.h index aa408853b..07232994f 100644 --- a/src/public/tier1/strtools.h +++ b/src/public/tier1/strtools.h @@ -890,10 +890,16 @@ bool V_BBCodeToHTML( OUT_Z_CAP( nDestSize ) char *pDest, const int nDestSize, ch // helper to identify "mean" spaces, which we don't like in visible identifiers // such as player Name +#ifdef NEO +constexpr +#endif bool V_IsMeanSpaceW( wchar_t wch ); // helper to identify characters which are deprecated in Unicode, // and we simply don't accept +#ifdef NEO +constexpr +#endif bool V_IsDeprecatedW( wchar_t wch ); //----------------------------------------------------------------------------- diff --git a/src/tier1/strtools.cpp b/src/tier1/strtools.cpp index 081e0d46f..e57206484 100644 --- a/src/tier1/strtools.cpp +++ b/src/tier1/strtools.cpp @@ -3942,6 +3942,9 @@ bool V_IsMeanUnderscoreW( wchar_t wch ) // characters in this set are removed from the beginning and/or end of strings // by Q_AggressiveStripPrecedingAndTrailingWhitespaceW() //----------------------------------------------------------------------------- +#ifdef NEO +constexpr +#endif bool V_IsMeanSpaceW( wchar_t wch ) { bool bIsMean = false; @@ -4029,6 +4032,9 @@ bool V_IsMeanSpaceW( wchar_t wch ) // Ideally, we'd perfectly support these end-to-end but we never realistically will. // The benefit of doing so far outweighs the cost, anyway. //----------------------------------------------------------------------------- +#ifdef NEO +constexpr +#endif bool V_IsDeprecatedW( wchar_t wch ) { bool bIsDeprecated = false; From c1b7a079e75c90ac8d58a0388c926bf85e4594de Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 02:56:23 +0200 Subject: [PATCH 03/16] Mark neo_name and _clantag FCVAR_PRINTABLEONLY --- src/game/shared/neo/neo_gamerules.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 23de65a71..8505d870d 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -53,10 +53,10 @@ ConVar sv_neo_player_restore("sv_neo_player_restore", "1", FCVAR_REPLICATED, "If ConVar sv_neo_spraydisable("sv_neo_spraydisable", "0", FCVAR_REPLICATED, "If enabled, disables the players ability to spray.", true, 0.0f, true, 1.0f); #ifdef CLIENT_DLL -ConVar neo_name("neo_name", "", FCVAR_USERINFO | FCVAR_ARCHIVE, "The nickname to set instead of the steam profile name."); +ConVar neo_name("neo_name", "", FCVAR_USERINFO | FCVAR_ARCHIVE | FCVAR_PRINTABLEONLY, "The nickname to set instead of the steam profile name."); ConVar cl_onlysteamnick("cl_onlysteamnick", "0", FCVAR_USERINFO | FCVAR_ARCHIVE, "Only show players Steam names, otherwise show player set names.", true, 0.0f, true, 1.0f); -ConVar neo_clantag("neo_clantag", "", FCVAR_USERINFO | FCVAR_ARCHIVE, "The clantag to set."); +ConVar neo_clantag("neo_clantag", "", FCVAR_USERINFO | FCVAR_ARCHIVE | FCVAR_PRINTABLEONLY, "The clantag to set."); #endif ConVar sv_neo_clantag_allow("sv_neo_clantag_allow", "1", FCVAR_REPLICATED, "", true, 0.0f, true, 1.0f); #ifdef DEBUG From 466484e604f3118cfe0d0d197ad33de0990166eb Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 03:16:07 +0200 Subject: [PATCH 04/16] address review feedback --- src/game/client/neo/ui/neo_root_settings.cpp | 6 ++++-- src/game/client/neo/ui/neo_ui.cpp | 8 ++++++++ src/game/client/neo/ui/neo_ui.h | 15 ++------------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index ad168dbcf..ecf68d021 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1047,8 +1047,10 @@ void NeoSettings_General(NeoSettings *ns) NeoUI::RingBox(L"Selected Background", const_cast(ns->p2WszCBList), ns->iCBListSize, &pGeneral->iBackground); NeoUI::Divider(L"MULTIPLAYER"); - NeoUI::TextEdit(L"Name", pGeneral->wszNeoName, ARRAYSIZE(pGeneral->wszNeoName) - 1, 0, [pGeneral]() { VarTrimmer(pGeneral->wszNeoName); }); - NeoUI::TextEdit(L"Clan tag", pGeneral->wszNeoClantag, NEO_MAX_CLANTAG_LENGTH - 1, 0, [pGeneral]() { VarTrimmer(pGeneral->wszNeoClantag); }); + NeoUI::TextEdit(L"Name", pGeneral->wszNeoName, ARRAYSIZE(pGeneral->wszNeoName) - 1); + VarTrimmer(pGeneral->wszNeoName); + NeoUI::TextEdit(L"Clan tag", pGeneral->wszNeoClantag, ARRAYSIZE(pGeneral->wszNeoClantag) - 1); + VarTrimmer(pGeneral->wszNeoClantag); NeoUI::RingBoxBool(L"Show only steam name", &pGeneral->bOnlySteamNick); NeoUI::RingBoxBool(L"Only show clantags when spectator", &pGeneral->bMarkerSpecOnlyClantag); diff --git a/src/game/client/neo/ui/neo_ui.cpp b/src/game/client/neo/ui/neo_ui.cpp index c4487fdf5..28607a409 100644 --- a/src/game/client/neo/ui/neo_ui.cpp +++ b/src/game/client/neo/ui/neo_ui.cpp @@ -2161,6 +2161,14 @@ static void BuildUpIrTextWidths(const wchar_t *wszVisText, const int iWszTextSiz } } +void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags) +{ + BeginMultiWidgetHighlighter(2); + Label(wszLeftLabel); + TextEdit(wszText, iMaxWszTextSize, flags); + EndMultiWidgetHighlighter(); +} + void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags) { WidgetFlag wdgFlags = WIDGETFLAG_MOUSE | WIDGETFLAG_MARKACTIVE; diff --git a/src/game/client/neo/ui/neo_ui.h b/src/game/client/neo/ui/neo_ui.h index 8ca58c167..b7f3d00d0 100644 --- a/src/game/client/neo/ui/neo_ui.h +++ b/src/game/client/neo/ui/neo_ui.h @@ -491,19 +491,8 @@ enum TextEditFlag_ TEXTEDITFLAG_FORCEACTIVE = 1 << 1, // Enforce hot+active focusing in this text edit widget all time, only suitable for single-textedit popups }; typedef int TextEditFlags; -/*1W*/ void TextEdit(wchar_t* wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); -template - requires (std::is_invocable_v || std::is_same_v) -/*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, - const TextEditFlags flags = TEXTEDITFLAG_NONE, - const NextFn&& next=nullptr) -{ - BeginMultiWidgetHighlighter(2); - Label(wszLeftLabel); - TextEdit(wszText, iMaxWszTextSize, flags); - EndMultiWidgetHighlighter(); - if constexpr (std::is_invocable_v) next(); -} +/*1W*/ void TextEdit(wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); +/*2W*/ void TextEdit(const wchar_t *wszLeftLabel, wchar_t *wszText, const int iMaxWszTextSize, const TextEditFlags flags = TEXTEDITFLAG_NONE); /*SW*/ void ImageTexture(const char *szTexturePath, const wchar_t *wszErrorMsg = L"", const char *szTextureGroup = ""); // NeoUI::Texture is non-widget, but utilizes NeoUI's image/texture handling From fa06b787ac67f08c7fc41b48f0aee6a112662fcb Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 04:19:35 +0200 Subject: [PATCH 05/16] Fix Linux build --- src/public/tier1/strtools.h | 6 ------ src/tier1/strtools.cpp | 6 ------ 2 files changed, 12 deletions(-) diff --git a/src/public/tier1/strtools.h b/src/public/tier1/strtools.h index 07232994f..aa408853b 100644 --- a/src/public/tier1/strtools.h +++ b/src/public/tier1/strtools.h @@ -890,16 +890,10 @@ bool V_BBCodeToHTML( OUT_Z_CAP( nDestSize ) char *pDest, const int nDestSize, ch // helper to identify "mean" spaces, which we don't like in visible identifiers // such as player Name -#ifdef NEO -constexpr -#endif bool V_IsMeanSpaceW( wchar_t wch ); // helper to identify characters which are deprecated in Unicode, // and we simply don't accept -#ifdef NEO -constexpr -#endif bool V_IsDeprecatedW( wchar_t wch ); //----------------------------------------------------------------------------- diff --git a/src/tier1/strtools.cpp b/src/tier1/strtools.cpp index e57206484..081e0d46f 100644 --- a/src/tier1/strtools.cpp +++ b/src/tier1/strtools.cpp @@ -3942,9 +3942,6 @@ bool V_IsMeanUnderscoreW( wchar_t wch ) // characters in this set are removed from the beginning and/or end of strings // by Q_AggressiveStripPrecedingAndTrailingWhitespaceW() //----------------------------------------------------------------------------- -#ifdef NEO -constexpr -#endif bool V_IsMeanSpaceW( wchar_t wch ) { bool bIsMean = false; @@ -4032,9 +4029,6 @@ bool V_IsMeanSpaceW( wchar_t wch ) // Ideally, we'd perfectly support these end-to-end but we never realistically will. // The benefit of doing so far outweighs the cost, anyway. //----------------------------------------------------------------------------- -#ifdef NEO -constexpr -#endif bool V_IsDeprecatedW( wchar_t wch ) { bool bIsDeprecated = false; From 4e32eb3bbf285ffe8f15ab5abab3a1238b8312f7 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 13:52:32 +0200 Subject: [PATCH 06/16] Improve blocking logic Block all bad characters --- src/game/client/neo/ui/neo_root_settings.cpp | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index ecf68d021..cf6f35602 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1003,28 +1003,35 @@ static const wchar_t *EQUIP_UTILITY_PRIORITY_LABELS[NeoSettings::EquipUtilityPri // Trims empty whitespaces from the left-hand side of a text variable. // Allows one contiguous whitespace on the right side for space-separated input, but no more. -template +template requires (maxlen > 1) FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) { - constexpr auto predicate = [](wchar_t c)->bool { - return (V_IsDeprecatedW(c) || - V_IsMeanSpaceW(c) || - std::iswblank(c)); - }; - - if (predicate(input[0])) + constexpr auto sizeOfElem = sizeof(input[0]); + const auto zeroIdx = Clamp(V_wcslen(input), 0, maxlen - 1); + // First disallow any bad characters + for (int i = 0; i < zeroIdx; ++i) { - std::memmove(input, &input[1], sizeof(input) - sizeof(input[0])); + if (V_IsDeprecatedW(input[i]) || + V_IsMeanSpaceW(input[i])) + { + std::wmemmove(&input[i], &input[i + 1], zeroIdx-i); + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = i; + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i; + } + } + // Then block leading spaces + if (std::iswblank(input[0])) + { + std::memmove(input, &input[1], maxlen - sizeOfElem); NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = 0; NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = 0; } - + // Finally block trailing multi-spaces const auto lastNonTerminatingChar = Min(V_wcslen(input) - 1, ARRAYSIZE(input) - 2); - if (lastNonTerminatingChar > 1 && predicate(input[lastNonTerminatingChar])) + if (lastNonTerminatingChar > 1 && std::iswblank(input[lastNonTerminatingChar])) { - // Two spaces in a row, delete one. But don't delete both so people can have singular spaces in their variable. - if (predicate(input[lastNonTerminatingChar - 1])) + if (std::iswblank(input[lastNonTerminatingChar - 1])) { input[lastNonTerminatingChar] = L'\0'; } From b700c33aa645d4f9452e4d5cec7b49e951874b6b Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 14:04:20 +0200 Subject: [PATCH 07/16] Use wmemmove, block sequences of >1 space at all positions --- src/game/client/neo/ui/neo_root_settings.cpp | 41 +++++++++++--------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index cf6f35602..9a6347a22 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1007,35 +1007,40 @@ template requires (maxlen > 1) FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) { - constexpr auto sizeOfElem = sizeof(input[0]); - const auto zeroIdx = Clamp(V_wcslen(input), 0, maxlen - 1); - // First disallow any bad characters + for (int i = maxlen - 1; i >= 0; --i) + { + // Zero-pad until we find the terminator + if (!input[i]) + break; + input[i] = '\0'; + } + + auto zeroIdx = Clamp(V_wcslen(input), 0, maxlen - 1); for (int i = 0; i < zeroIdx; ++i) { - if (V_IsDeprecatedW(input[i]) || - V_IsMeanSpaceW(input[i])) + const bool hasBadCharInPos = ( + V_IsDeprecatedW(input[i]) || + V_IsMeanSpaceW(input[i])); + + const bool hasDoubleBlankInPos = ( + (i + 1 < zeroIdx) && + std::iswblank(input[i]) && + std::iswblank(input[i + 1])); + + if (hasBadCharInPos || hasDoubleBlankInPos) { std::wmemmove(&input[i], &input[i + 1], zeroIdx-i); - NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = i; - NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i; + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = i+hasDoubleBlankInPos; + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i+hasDoubleBlankInPos; } } - // Then block leading spaces + // Block leading spaces if (std::iswblank(input[0])) { - std::memmove(input, &input[1], maxlen - sizeOfElem); + std::wmemmove(input, &input[1], maxlen - 1); NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = 0; NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = 0; } - // Finally block trailing multi-spaces - const auto lastNonTerminatingChar = Min(V_wcslen(input) - 1, ARRAYSIZE(input) - 2); - if (lastNonTerminatingChar > 1 && std::iswblank(input[lastNonTerminatingChar])) - { - if (std::iswblank(input[lastNonTerminatingChar - 1])) - { - input[lastNonTerminatingChar] = L'\0'; - } - } } void NeoSettings_General(NeoSettings *ns) From 28551cd0e95f1f0e7dbaf8cb327a330a04ee81b2 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 18 Mar 2026 14:07:06 +0200 Subject: [PATCH 08/16] Remove paranoid check --- src/game/client/neo/ui/neo_root_settings.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 9a6347a22..5688f214b 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1007,14 +1007,6 @@ template requires (maxlen > 1) FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) { - for (int i = maxlen - 1; i >= 0; --i) - { - // Zero-pad until we find the terminator - if (!input[i]) - break; - input[i] = '\0'; - } - auto zeroIdx = Clamp(V_wcslen(input), 0, maxlen - 1); for (int i = 0; i < zeroIdx; ++i) { From 39eb888c547402769065a3d88cb954a67f2ef7e2 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 15:45:07 +0300 Subject: [PATCH 09/16] Fix empty ret vals from vguilocalize NeoName and NeoClantag wchar conversions via g_pVGuiLocalize->ConvertANSIToUnicode will return "#empty" for an empty input. In those cases, move the null terminator to the beginning of the string, so that we get: L"#empty" -> L"" because we don't want to display the value "#empty" for the user in the UI. --- src/game/client/neo/ui/neo_root_settings.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 5688f214b..c878ea58c 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -387,6 +387,12 @@ void NeoSettingsRestore(NeoSettings *ns, const NeoSettings::Keys::Flags flagsKey NeoSettings::General *pGeneral = &ns->general; g_pVGuiLocalize->ConvertANSIToUnicode(cvr->neo_name.GetString(), pGeneral->wszNeoName, sizeof(pGeneral->wszNeoName)); g_pVGuiLocalize->ConvertANSIToUnicode(cvr->neo_clantag.GetString(), pGeneral->wszNeoClantag, sizeof(pGeneral->wszNeoClantag)); + + if (V_wcscmp(pGeneral->wszNeoName, L"#empty") == 0) + pGeneral->wszNeoName[0] = L'\0'; + if (V_wcscmp(pGeneral->wszNeoClantag, L"#empty") == 0) + pGeneral->wszNeoClantag[0] = L'\0'; + pGeneral->bOnlySteamNick = cvr->cl_onlysteamnick.GetBool(); pGeneral->bReloadEmpty = cvr->cl_autoreload_when_empty.GetBool(); pGeneral->bViewmodelRighthand = cvr->cl_righthand.GetBool(); From 2b16761deeffe66494eeeac61d6ca572b66a3639 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 15:49:07 +0300 Subject: [PATCH 10/16] Review feedback: Remove redundant iTextSelCur assignment Remove the typo'd redundant line. --- src/game/client/neo/ui/neo_root_settings.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index c878ea58c..5d51f0708 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1028,7 +1028,6 @@ FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) if (hasBadCharInPos || hasDoubleBlankInPos) { std::wmemmove(&input[i], &input[i + 1], zeroIdx-i); - NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = i+hasDoubleBlankInPos; NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i+hasDoubleBlankInPos; } } From 7716e9cde5e2d0d569de0101ad40f80970702bae Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 15:52:38 +0300 Subject: [PATCH 11/16] Review feedback: Fix typo in szSteamName alloc size MAX_PLACE_NAME_LENGTH -> MAX_PLAYER_NAME_LENGTH --- src/game/shared/neo/neo_gamerules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 8505d870d..869d56e2c 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -3509,7 +3509,7 @@ void CNEORules::ClientSettingsChanged(CBasePlayer *pPlayer) const bool clientAllowsNeoName = (0 == StrToInt(engine->GetClientConVarValue(engine->IndexOfEdict(pNEOPlayer->edict()), "cl_onlysteamnick"))); - char szSteamName[MAX_PLACE_NAME_LENGTH] = ""; + char szSteamName[MAX_PLAYER_NAME_LENGTH] = ""; const char* pszSteamName = &szSteamName[0]; V_strcpy_safe(szSteamName, engine->GetClientConVarValue(pPlayer->entindex(), "name")); V_StripTrailingWhitespace(&szSteamName[0]); From fa79e0a390cd771567ee1324d803b5f82a82cdc6 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 30 Mar 2026 16:00:41 +0300 Subject: [PATCH 12/16] Review feedback: Fix loop idx OBOE after memmove After the VarTrimmer input is moved one position to the left, the loop index and the loop condition index must be decremented by one to compensate for it. This happened to work previously by coincidence in the IMGUI loop, since it would fix 1 trailing character at a time before incorrectly exiting the loop, and continue the work on the next UI draw call. But indeed the correct behaviour would be to process the whole input in one loop. --- src/game/client/neo/ui/neo_root_settings.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index 5d51f0708..def133059 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1029,6 +1029,9 @@ FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) { std::wmemmove(&input[i], &input[i + 1], zeroIdx-i); NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i+hasDoubleBlankInPos; + // memmove has shifted contents one position to the left, so compensate by decrementing + zeroIdx = Max(0, zeroIdx - 1); + i -= 1; } } // Block leading spaces From ba4ac3764f8ada38fbcd88ba0f9586e6033af816 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 31 Mar 2026 15:32:52 +0300 Subject: [PATCH 13/16] Review feedback: inline NeoConVarFixPrintable --- src/game/client/cdll_client_int.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game/client/cdll_client_int.h b/src/game/client/cdll_client_int.h index b409c7b6a..9d22e9f87 100644 --- a/src/game/client/cdll_client_int.h +++ b/src/game/client/cdll_client_int.h @@ -130,7 +130,7 @@ extern bool g_bTextMode; template requires (STR_LIMIT_SIZE > 0) // De-mangle bad Unicode and trim leading and trailing whitespace. -static void NeoConVarFixPrintable(IConVar *cvar, const char *, float) +inline void NeoConVarFixPrintable(IConVar *cvar, const char *, float) { // prevent reentrancy static bool bStaticCallbackChangedCVar = false; From d5c9dffb64e006239f75c4c104eb8ec1ab22c8fe Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 31 Mar 2026 15:33:23 +0300 Subject: [PATCH 14/16] Review feedback: Use SDK memmove --- src/game/client/neo/ui/neo_root_settings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/client/neo/ui/neo_root_settings.cpp b/src/game/client/neo/ui/neo_root_settings.cpp index def133059..468b73932 100644 --- a/src/game/client/neo/ui/neo_root_settings.cpp +++ b/src/game/client/neo/ui/neo_root_settings.cpp @@ -1027,7 +1027,7 @@ FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) if (hasBadCharInPos || hasDoubleBlankInPos) { - std::wmemmove(&input[i], &input[i + 1], zeroIdx-i); + V_memmove(&input[i], &input[i + 1], (zeroIdx - i) * sizeof(input[0])); NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = i+hasDoubleBlankInPos; // memmove has shifted contents one position to the left, so compensate by decrementing zeroIdx = Max(0, zeroIdx - 1); @@ -1037,7 +1037,7 @@ FORCEINLINE void VarTrimmer(wchar_t (&input)[maxlen]) // Block leading spaces if (std::iswblank(input[0])) { - std::wmemmove(input, &input[1], maxlen - 1); + V_memmove(input, &input[1], (maxlen - 1) * sizeof(input[0])); NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = 0; NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = 0; } From 95a662798de74099718cd4c79c07ba814acfa26a Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 31 Mar 2026 15:52:19 +0300 Subject: [PATCH 15/16] Review feedback: repair all printables --- src/game/client/cdll_client_int.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/game/client/cdll_client_int.h b/src/game/client/cdll_client_int.h index 9d22e9f87..40b4fc294 100644 --- a/src/game/client/cdll_client_int.h +++ b/src/game/client/cdll_client_int.h @@ -143,10 +143,7 @@ inline void NeoConVarFixPrintable(IConVar *cvar, const char *, float) ConVarRef cvarRef(cvar); V_strcpy_safe(mutStr, cvarRef.GetString()); - if (V_strlen(cvarRef.GetString()) >= STR_LIMIT_SIZE) - { - Q_UnicodeRepair(mutStr); - } + Q_UnicodeRepair(mutStr); V_StripTrailingWhitespace(mutStr); V_StripLeadingWhitespace(mutStr); From df9f1a9d394e678246d794a3fff62089bb1295d3 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 31 Mar 2026 17:11:26 +0300 Subject: [PATCH 16/16] Fix scoreboard handling of `#empty` names --- src/game/server/neo/neo_player.cpp | 12 ++++++++++-- src/game/shared/neo/neo_gamerules.cpp | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/game/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index 672441fc5..667ea91a7 100644 --- a/src/game/server/neo/neo_player.cpp +++ b/src/game/server/neo/neo_player.cpp @@ -1827,8 +1827,16 @@ bool CNEO_Player::SetNeoPlayerName(const char *newNeoName) // NEO NOTE (nullsystem): Generally it's never NULL but just incase if (newNeoName) { - V_memcpy(m_szNeoName.GetForModify(), newNeoName, sizeof(m_szNeoName)-1); - m_bNeoNameHasSet = (m_szNeoName.Get()[0] != 0); + if (FStrEq(newNeoName, "#empty")) + { + m_szNeoName.GetForModify()[0] = '\0'; + m_bNeoNameHasSet = false; + } + else + { + V_memcpy(m_szNeoName.GetForModify(), newNeoName, sizeof(m_szNeoName)-1); + m_bNeoNameHasSet = (m_szNeoName.Get()[0] != 0); + } return m_bNeoNameHasSet; } return false; diff --git a/src/game/shared/neo/neo_gamerules.cpp b/src/game/shared/neo/neo_gamerules.cpp index 869d56e2c..07185d4f5 100644 --- a/src/game/shared/neo/neo_gamerules.cpp +++ b/src/game/shared/neo/neo_gamerules.cpp @@ -3556,7 +3556,9 @@ void CNEORules::ClientSettingsChanged(CBasePlayer *pPlayer) const char *pszOldNeoClantag = pNEOPlayer->GetNeoClantag(); if (V_strcmp(pszOldNeoClantag, pszNeoClantag) != 0) { - V_strncpy(pNEOPlayer->m_szNeoClantag.GetForModify(), pszNeoClantag, NEO_MAX_CLANTAG_LENGTH); + V_strncpy(pNEOPlayer->m_szNeoClantag.GetForModify(), + (FStrEq(pszNeoClantag, "#empty") ? "" : pszNeoClantag), + NEO_MAX_CLANTAG_LENGTH); m_bThinkCheckClantags = true; }