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..40b4fc294 100644 --- a/src/game/client/cdll_client_int.h +++ b/src/game/client/cdll_client_int.h @@ -126,7 +126,33 @@ 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. +inline 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()); + + 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..468b73932 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" @@ -386,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(); @@ -1000,6 +1007,42 @@ 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]) +{ + auto zeroIdx = Clamp(V_wcslen(input), 0, maxlen - 1); + for (int i = 0; i < zeroIdx; ++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) + { + 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); + i -= 1; + } + } + // Block leading spaces + if (std::iswblank(input[0])) + { + V_memmove(input, &input[1], (maxlen - 1) * sizeof(input[0])); + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelCur = 0; + NeoUI::CurrentContext()->iTextSelCur = NeoUI::CurrentContext()->iTextSelStart = 0; + } +} + void NeoSettings_General(NeoSettings *ns) { NeoSettings::General *pGeneral = &ns->general; @@ -1016,8 +1059,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, 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); + 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/server/neo/neo_player.cpp b/src/game/server/neo/neo_player.cpp index d75a80409..667ea91a7 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,27 @@ 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; + 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; } 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..07185d4f5 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 @@ -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_PLAYER_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,23 +3536,29 @@ 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) { - 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; }