diff --git a/game/neo/resource/neo_english.txt b/game/neo/resource/neo_english.txt index ab7e26a248..a0e9cc619a 100644 Binary files a/game/neo/resource/neo_english.txt and b/game/neo/resource/neo_english.txt differ diff --git a/src/game/client/c_playerresource.cpp b/src/game/client/c_playerresource.cpp index 85b2593fc1..f051775cc5 100644 --- a/src/game/client/c_playerresource.cpp +++ b/src/game/client/c_playerresource.cpp @@ -91,6 +91,9 @@ C_PlayerResource::C_PlayerResource() memset(m_szDispNameWDupeIdx, 0, sizeof(m_szDispNameWDupeIdx)); memset(m_iStar, 0, sizeof(m_iStar)); memset(m_szNeoClantag, 0, sizeof(m_szNeoClantag)); + + m_cachedPlayerNames.SetLessFunc(DefLessFunc(PlayerResource::useridCache_t)); + m_cachedPlayerNames.EnsureCapacity(gpGlobals->maxClients * 2); #endif memset( m_iScore, 0, sizeof( m_iScore ) ); memset( m_iDeaths, 0, sizeof( m_iDeaths ) ); @@ -141,6 +144,18 @@ void C_PlayerResource::OnDataChanged(DataUpdateType_t updateType) } } +#ifdef NEO +string_t C_PlayerResource::GetCachedName(int userid) const +{ + const auto idx = m_cachedPlayerNames.Find(userid); + if (idx == m_cachedPlayerNames.InvalidIndex()) + { + return ""; + } + return m_cachedPlayerNames.Element(idx); +} +#endif + void C_PlayerResource::UpdatePlayerName( int slot ) { if ( slot < 1 || slot > MAX_PLAYERS ) @@ -158,6 +173,20 @@ void C_PlayerResource::UpdatePlayerName( int slot ) if ( IsConnected( slot ) && engine->GetPlayerInfo( slot, &sPlayerInfo ) ) { m_szName[slot] = AllocPooledString( UTIL_GetFilteredPlayerName( slot, sPlayerInfo.name ) ); +#ifdef NEO + string_t name = m_szName[slot]; + const auto localPlayer = C_NEO_Player::GetLocalNEOPlayer(); + if (localPlayer && localPlayer->ClientWantNeoName()) + { + name = m_szNeoName[slot]; + } + + m_cachedPlayerNames.InsertOrReplace(sPlayerInfo.userID, name); + if (m_cachedPlayerNames.Count() >= (unsigned int)(gpGlobals->maxClients * 2)) + { + PurgeOldCachedNames(); + } +#endif } else { @@ -168,6 +197,22 @@ void C_PlayerResource::UpdatePlayerName( int slot ) } } +#ifdef NEO +void C_PlayerResource::PurgeOldCachedNames() +{ + for (auto i = m_cachedPlayerNames.FirstInorder(); i != m_cachedPlayerNames.InvalidIndex(); ) + { + const auto idx = i; + i = m_cachedPlayerNames.NextInorder(i); + // TODO: optimize; don't need to run the clients loop for each iteration + if (!UTIL_PlayerByUserId(m_cachedPlayerNames.Key(idx))) + { + m_cachedPlayerNames.RemoveAt(idx); + } + } +} +#endif + void C_PlayerResource::ClientThink() { BaseClass::ClientThink(); diff --git a/src/game/client/c_playerresource.h b/src/game/client/c_playerresource.h index 1aaff38d7c..be2af2ae11 100644 --- a/src/game/client/c_playerresource.h +++ b/src/game/client/c_playerresource.h @@ -23,6 +23,17 @@ #define PLAYER_UNCONNECTED_NAME "unconnected" #define PLAYER_ERROR_NAME "ERRORNAME" +#ifdef NEO +namespace PlayerResource { + typedef unsigned char useridCache_t; +#pragma push_macro("max") +#undef max + constexpr auto useridNumericLimit{ std::numeric_limits::max() }; +#pragma pop_macro("max") + static_assert(MAX_PLAYERS < useridNumericLimit); +} +#endif + class C_PlayerResource : public C_BaseEntity, public IGameResources { DECLARE_CLASS( C_PlayerResource, C_BaseEntity ); @@ -71,6 +82,10 @@ public : // IGameResources interface uint32 GetAccountID( int iIndex ); bool IsValid( int iIndex ); +#ifdef NEO + string_t GetCachedName(int userid) const; +#endif + protected: void UpdatePlayerName( int slot ); @@ -98,6 +113,14 @@ public : // IGameResources interface bool m_bValid[MAX_PLAYERS_ARRAY_SAFE]; int m_iUserID[MAX_PLAYERS_ARRAY_SAFE]; string_t m_szUnconnectedName; + +#ifdef NEO +private: + // This name cache is used for fixing player post-disconnect messages where the disconnecting player is already gone, + // but we may want to display their neo "fake" name instead of their Steam name, which gets reported by the disconnect msg. + CUtlMap m_cachedPlayerNames; + void PurgeOldCachedNames(); +#endif }; extern C_PlayerResource *g_PR; diff --git a/src/game/client/clientmode_shared.cpp b/src/game/client/clientmode_shared.cpp index 14ff3eab37..bfda2cb76e 100644 --- a/src/game/client/clientmode_shared.cpp +++ b/src/game/client/clientmode_shared.cpp @@ -72,6 +72,9 @@ extern ConVar replay_rendersetting_renderglow; #include #include "ui/neo_loading.h" #include "neo_gamerules.h" +#include "jobthread.h" + +#include #endif #ifdef GLOWS_ENABLE @@ -1086,6 +1089,102 @@ bool PlayerNameNotSetYet( const char *pszName ) return false; } +#ifdef NEO +// Delayed handling for a joining player, because we need to wait for them to fully connect first +// in order to pull some data required for printing the greeting. +class DeferredGreet : public CJob { +public: + DeferredGreet(int userid, CBaseHudChat* chat) + : m_userid(userid), m_chat(chat) + { + Assert(chat); + } + +private: + virtual JobStatus_t DoExecute() override final + { + JobStatus_t res = JOB_STATUS_ABORTED; + + constexpr auto sleepTimeMs = 500, maxTries = 20; + constexpr auto maxTimeSpentMs = sleepTimeMs * maxTries; + static_assert(maxTimeSpentMs <= 10000); + for (int i = 0; i < maxTries; ++i) + { + ThreadSleep(sleepTimeMs); + + if (!engine->IsInGame()) + { + DoRelease(); + return res; + } + + if (TryGreet(m_chat, m_userid)) + { + res = JOB_OK; + //Msg("Greet ok after %d retries\n", i); + break; + } + //Msg("Greet failed for userid %d\n", m_userid); + } + + if (res == JOB_STATUS_ABORTED) + { + PrintGenericGreeting(m_chat); + } + + DoRelease(); + return res; + } + + static void PrintGenericGreeting(CBaseHudChat* chat) + { + if (chat) + { + chat->Printf(CHAT_FILTER_JOINLEAVE, "Player joined the game"); + } + } + + static bool TryGreet(CBaseHudChat* chat, int userid) + { + Assert(engine->IsInGame()); + if (!chat) + { + return false; + } + + if (cl_neo_streamermode.GetBool()) + { + PrintGenericGreeting(chat); + return true; + } + + int iPlayerIndex = engine->GetPlayerForUserID(userid); + auto player = static_cast(UTIL_PlayerByIndex(iPlayerIndex)); + if (!player) + { + return false; + } + + wchar_t wszLocalized[100]; + wchar_t wszPlayerName[MAX_PLAYER_NAME_LENGTH]; + UTIL_GetFilteredPlayerNameAsWChar(iPlayerIndex, player->GetPlayerName(), wszPlayerName); + + auto tlString = player->IsNextBot() ? "#game_bot_joined_game" : "#game_player_joined_game"; + g_pVGuiLocalize->ConstructString_safe(wszLocalized, g_pVGuiLocalize->Find(tlString), 1, wszPlayerName); + + char szLocalized[100]; + g_pVGuiLocalize->ConvertUnicodeToANSI(wszLocalized, szLocalized, sizeof(szLocalized)); + + chat->Printf(CHAT_FILTER_JOINLEAVE, "%s", szLocalized); + + return true; + } + + int m_userid; + CBaseHudChat* m_chat; +}; +#endif + void ClientModeShared::FireGameEvent( IGameEvent *event ) { CBaseHudChat *hudChat = (CBaseHudChat *)GET_HUDELEMENT( CHudChat ); @@ -1102,15 +1201,13 @@ void ClientModeShared::FireGameEvent( IGameEvent *event ) if ( !IsInCommentaryMode() ) { #ifdef NEO - if (cl_neo_streamermode.GetBool()) - { - hudChat->Printf(CHAT_FILTER_JOINLEAVE, "Player connected"); - return; - } -#endif + Assert(g_pThreadPool); + g_pThreadPool->AddJob(new DeferredGreet(event->GetInt("userid"), hudChat)); +#else wchar_t wszLocalized[100]; wchar_t wszPlayerName[ MAX_PLAYER_NAME_LENGTH ]; int iPlayerIndex = engine->GetPlayerForUserID( event->GetInt( "userid" ) ); + UTIL_GetFilteredPlayerNameAsWChar( iPlayerIndex, event->GetString( "name" ), wszPlayerName ); g_pVGuiLocalize->ConstructString_safe( wszLocalized, g_pVGuiLocalize->Find( "#game_player_joined_game" ), 1, wszPlayerName ); @@ -1118,22 +1215,11 @@ void ClientModeShared::FireGameEvent( IGameEvent *event ) g_pVGuiLocalize->ConvertUnicodeToANSI( wszLocalized, szLocalized, sizeof(szLocalized) ); hudChat->Printf( CHAT_FILTER_JOINLEAVE, "%s", szLocalized ); +#endif } } else if ( Q_strcmp( "player_disconnect", eventname ) == 0 ) { -#ifdef NEO - if (cl_neo_streamermode.GetBool()) - { - hudChat->Printf(CHAT_FILTER_JOINLEAVE, "Player disconnected"); - return; - } -#endif - -#ifndef NEO // ??? - C_BasePlayer *pPlayer = USERID2PLAYER( event->GetInt("userid") ); - if ( !hudChat || !pPlayer ) -#endif // Josh: There used to be code here that would get the player entity here to get the name // Big problem with that. The player entity is probably already gone -- they disconnected after all! // So there used to be a bug where most of the time, disconnect messages just wouldn't show up in chat. @@ -1143,7 +1229,19 @@ void ClientModeShared::FireGameEvent( IGameEvent *event ) if ( !hudChat ) return; +#ifdef NEO + if (cl_neo_streamermode.GetBool()) + { + hudChat->Printf(CHAT_FILTER_JOINLEAVE, "Player disconnected"); + return; + } + + const char* pszPlayerName = g_PR->GetCachedName(event->GetInt("userid")); + if (!pszPlayerName || !pszPlayerName[0]) + pszPlayerName = event->GetString("name"); +#else const char* pszPlayerName = event->GetString( "name" ); +#endif if ( PlayerNameNotSetYet( pszPlayerName ) ) return; @@ -1155,12 +1253,23 @@ void ClientModeShared::FireGameEvent( IGameEvent *event ) wchar_t wszReason[64]; const char *pszReason = event->GetString( "reason" ); + if ( pszReason && ( pszReason[0] == '#' ) && g_pVGuiLocalize->Find( pszReason ) ) { V_wcsncpy( wszReason, g_pVGuiLocalize->Find( pszReason ), sizeof( wszReason ) ); } else { +#ifdef NEO + // The default message is in the form: + // Player left the game ( timed out) + // but for NT, we have to replace the engine-provided playername with the possible neo_name for consistency. + // Just simplify the "timed out" message here instead of manually fixing the duplicated player name. + if (std::string{ pszReason }.ends_with(" timed out")) + { + pszReason = "connection timed out"; + } +#endif g_pVGuiLocalize->ConvertANSIToUnicode( pszReason, wszReason, sizeof(wszReason) ); }