From 66442b7bc60b9d271ab22893bb2cf0f2e4f2f2fc Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Sat, 15 Nov 2025 23:13:14 +0800 Subject: [PATCH 1/3] fix: Ensure CPlayer vtable validity before virtual calls --- src/scripting/server/player.cpp | 112 +++++++++++++++++++++----------- src/server/players/manager.cpp | 88 ++++++++++++++++--------- 2 files changed, 130 insertions(+), 70 deletions(-) diff --git a/src/scripting/server/player.cpp b/src/scripting/server/player.cpp index 1a273c38a..f6d16a94a 100644 --- a/src/scripting/server/player.cpp +++ b/src/scripting/server/player.cpp @@ -25,7 +25,8 @@ void Bridge_Player_SendMessage(int playerid, int kind, const char* message, int { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; player->SendMsg((MessageType)kind, message, duration); } @@ -34,7 +35,8 @@ bool Bridge_Player_IsFakeClient(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return true; + if (!player) + return true; return player->IsFakeClient(); } @@ -43,7 +45,8 @@ bool Bridge_Player_IsAuthorized(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return false; + if (!player) + return false; return player->IsAuthorized(); } @@ -52,7 +55,8 @@ uint32_t Bridge_Player_GetConnectedTime(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; return player->GetConnectedTime(); } @@ -61,7 +65,8 @@ uint64_t Bridge_Player_GetUnauthorizedSteamID(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; return player->GetUnauthorizedSteamID(); } @@ -70,7 +75,8 @@ uint64_t Bridge_Player_GetSteamID(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; return player->GetSteamID(); } @@ -79,7 +85,8 @@ void* Bridge_Player_GetController(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return nullptr; + if (!player) + return nullptr; return player->GetController(); } @@ -88,7 +95,8 @@ void* Bridge_Player_GetPawn(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return nullptr; + if (!player) + return nullptr; return player->GetPawn(); } @@ -97,7 +105,8 @@ void* Bridge_Player_GetPlayerPawn(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return nullptr; + if (!player) + return nullptr; return player->GetPlayerPawn(); } @@ -106,7 +115,8 @@ uint64_t Bridge_Player_GetPressedButtons(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; return player->GetPressedButtons(); } @@ -115,7 +125,8 @@ void Bridge_Player_PerformCommand(int playerid, const char* command) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; player->PerformCommand(command); } @@ -124,12 +135,14 @@ int Bridge_Player_GetIPAddress(char* out, int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; static std::string s; s = player->GetIPAddress(); - if (out != nullptr) strcpy(out, s.c_str()); + if (out != nullptr) + strcpy(out, s.c_str()); return s.size(); } @@ -138,40 +151,49 @@ void Bridge_Player_Kick(int playerid, const char* reason, int gamereason) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; player->Kick(reason, gamereason); } void Bridge_Player_ShouldBlockTransmitEntity(int playerid, int entityidx, bool shouldBlockTransmit) { - if (playerid + 1 == entityidx) return; + if (playerid + 1 == entityidx) + return; static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; auto& bv = player->GetBlockedTransmittingBits(); auto dword = entityidx / 64; - if (shouldBlockTransmit) { + if (shouldBlockTransmit) + { bool wasEmpty = (bv.blockedMask[dword] == 0); bv.blockedMask[dword] |= (1 << (entityidx % 64)); - if (wasEmpty) bv.activeMasks.push_back(dword); + if (wasEmpty) + bv.activeMasks.push_back(dword); } - else { + else + { bv.blockedMask[dword] &= ~(1 << (entityidx % 64)); - if (bv.blockedMask[dword] == 0) bv.activeMasks.erase(std::find(bv.activeMasks.begin(), bv.activeMasks.end(), dword)); + if (bv.blockedMask[dword] == 0) + bv.activeMasks.erase(std::find(bv.activeMasks.begin(), bv.activeMasks.end(), dword)); } } bool Bridge_Player_IsTransmitEntityBlocked(int playerid, int entityidx) { - if (playerid + 1 == entityidx) return false; + if (playerid + 1 == entityidx) + return false; static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return false; + if (!player) + return false; auto& bv = player->GetBlockedTransmittingBits(); return (bv.blockedMask[entityidx / 64] & (1 << (entityidx % 64))) != 0; @@ -181,10 +203,12 @@ void Bridge_Player_ClearTransmitEntityBlocked(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; auto& bv = player->GetBlockedTransmittingBits(); - for (int i = 0; i < 512; i++) bv.blockedMask[i] = 0; + for (int i = 0; i < 512; i++) + bv.blockedMask[i] = 0; bv.activeMasks.clear(); } @@ -192,7 +216,8 @@ void Bridge_Player_ChangeTeam(int playerid, int newteam) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; static auto gamedata = g_ifaceService.FetchInterface(GAMEDATA_INTERFACE_VERSION); CALL_VIRTUAL(void, gamedata->GetOffsets()->Fetch("CCSPlayerController::ChangeTeam"), player->GetController(), newteam); @@ -202,30 +227,33 @@ void Bridge_Player_SwitchTeam(int playerid, int newteam) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; static auto gamedata = g_ifaceService.FetchInterface(GAMEDATA_INTERFACE_VERSION); if (newteam == 0 || newteam == 1) CALL_VIRTUAL(void, gamedata->GetOffsets()->Fetch("CCSPlayerController::ChangeTeam"), player->GetController(), newteam); else - reinterpret_cast(gamedata->GetSignatures()->Fetch("CCSPlayerController::SwitchTeam"))(player->GetController(), newteam); + reinterpret_cast(gamedata->GetSignatures()->Fetch("CCSPlayerController::SwitchTeam"))(player->GetController(), newteam); } void Bridge_Player_TakeDamage(int playerid, void* dmginfo) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; static auto gamedata = g_ifaceService.FetchInterface(GAMEDATA_INTERFACE_VERSION); - reinterpret_cast(gamedata->GetSignatures()->Fetch("CBaseEntity::TakeDamage"))(player->GetPawn(), dmginfo, 0); + reinterpret_cast(gamedata->GetSignatures()->Fetch("CBaseEntity::TakeDamage"))(player->GetPawn(), dmginfo, 0); } void Bridge_Player_Teleport(int playerid, Vector pos, QAngle angle, Vector vel) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; static auto gamedata = g_ifaceService.FetchInterface(GAMEDATA_INTERFACE_VERSION); CALL_VIRTUAL(void, gamedata->GetOffsets()->Fetch("CBaseEntity::Teleport"), player->GetPawn(), &pos, &angle, &vel); @@ -235,12 +263,14 @@ int Bridge_Player_GetLanguage(char* out, int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return 0; + if (!player) + return 0; static std::string s; s = player->GetLanguage(); - if (out != nullptr) strcpy(out, s.c_str()); + if (out != nullptr) + strcpy(out, s.c_str()); return s.size(); } @@ -249,7 +279,8 @@ void Bridge_Player_SetCenterMenuRender(int playerid, const char* text) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; player->RenderMenuCenterText(text); } @@ -258,7 +289,8 @@ void Bridge_Player_ClearCenterMenuRender(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; player->ClearRenderMenuCenterText(); } @@ -267,7 +299,8 @@ bool Bridge_Player_HasMenuShown(int playerid) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return false; + if (!player) + return false; return player->HasMenuShown(); } @@ -276,18 +309,21 @@ void Bridge_Player_ExecuteCommand(int playerid, const char* command) { static auto playerManager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto player = playerManager->GetPlayer(playerid); - if (!player) return; + if (!player) + return; CCommand cmd; cmd.Tokenize(command); ConCommandRef cmdRef(cmd[0]); - if (cmdRef.IsValidRef()) { + if (cmdRef.IsValidRef()) + { CCommandContext context(CommandTarget_t::CT_FIRST_SPLITSCREEN_CLIENT, CPlayerSlot(player->GetSlot())); cmdRef.Dispatch(context, cmd); } - else { + else + { static auto engine = g_ifaceService.FetchInterface(INTERFACEVERSION_VENGINESERVER); engine->ClientCommand(player->GetSlot(), command); } diff --git a/src/server/players/manager.cpp b/src/server/players/manager.cpp index 203c1fa73..7cfe085f6 100644 --- a/src/server/players/manager.cpp +++ b/src/server/players/manager.cpp @@ -60,8 +60,9 @@ void CheckTransmitHook(void* _this, CCheckTransmitInfo** ppInfoList, int infoCou void CPlayerManager::Initialize() { - g_Players = new CPlayer * [g_SwiftlyCore.GetMaxGameClients()]; - for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) { + g_Players = new CPlayer*[g_SwiftlyCore.GetMaxGameClients()]; + for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) + { g_Players[i] = nullptr; } @@ -108,9 +109,12 @@ void CPlayerManager::Initialize() g_pProcessUserCmdsHook->Enable(); } -void CPlayerManager::Shutdown() { - for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) { - if (g_Players[i] != nullptr) { +void CPlayerManager::Shutdown() +{ + for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) + { + if (g_Players[i] != nullptr) + { delete g_Players[i]; } } @@ -175,7 +179,7 @@ void OnClientPutInServerHook(void* _this, CPlayerSlot slot, char const* pszName, reinterpret_cast(g_pClientPutInServerHook->GetOriginal())(_this, slot, pszName, type, xuid); if (g_pOnClientPutInServerCallback) - reinterpret_cast(g_pOnClientPutInServerCallback)(slot.Get(), type); + reinterpret_cast(g_pOnClientPutInServerCallback)(slot.Get(), type); } extern void* g_pOnClientProcessUsercmdsCallback; @@ -184,12 +188,12 @@ void* ProcessUsercmdsHook(void* pController, CUserCmd* cmds, int numcmds, bool p { auto playerid = ((CEntityInstance*)pController)->m_pEntity->m_EHandle.GetEntryIndex() - 1; - google::protobuf::Message** pMsg = new google::protobuf::Message * [numcmds]; + google::protobuf::Message** pMsg = new google::protobuf::Message*[numcmds]; for (int i = 0; i < numcmds; i++) pMsg[i] = (google::protobuf::Message*)&cmds[i].cmd; if (g_pOnClientProcessUsercmdsCallback) - reinterpret_cast(g_pOnClientProcessUsercmdsCallback)(playerid, pMsg, numcmds, paused, margin); + reinterpret_cast(g_pOnClientProcessUsercmdsCallback)(playerid, pMsg, numcmds, paused, margin); delete[] pMsg; @@ -205,7 +209,8 @@ void CheckTransmitHook(void* _this, CCheckTransmitInfo** ppInfoList, int infoCou { auto& pInfo = ppInfoList[i]; int playerid = pInfo->m_nPlayerSlot.Get(); - if (!playermanager->IsPlayerOnline(playerid)) continue; + if (!playermanager->IsPlayerOnline(playerid)) + continue; auto player = playermanager->GetPlayer(playerid); auto& blockedBits = player->GetBlockedTransmittingBits(); @@ -215,7 +220,8 @@ void CheckTransmitHook(void* _this, CCheckTransmitInfo** ppInfoList, int infoCou auto& activeMasks = blockedBits.activeMasks; // NUM_MASKS_ACTIVE ops = NUM_MASKS_ACTIVE*64 bits -> 64 players -> NUM_MASKS_ACTIVE*64 ops - for (auto& dword : activeMasks) { + for (auto& dword : activeMasks) + { base[dword] &= ~blockedBits.blockedMask[dword]; baseAlways[dword] &= ~blockedBits.blockedMask[dword]; } @@ -229,7 +235,7 @@ void CheckTransmitHook(void* _this, CCheckTransmitInfo** ppInfoList, int infoCou // wordAlways &= ~blockedBase[i]; // } - //16k ops = 16k bits -> 64 players -> 1M ops + // 16k ops = 16k bits -> 64 players -> 1M ops /* for (int i = 0; i < 16384; i++) if (blockedBits.IsBitSet(i)) @@ -247,12 +253,15 @@ void OnGameFramePlayerHook(void* _this, bool simulate, bool first, bool last) static auto playermanager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); static auto vgui = g_ifaceService.FetchInterface(VGUI_INTERFACE_VERSION); - if (g_pOnGameTickCallback) reinterpret_cast(g_pOnGameTickCallback)(simulate, first, last); + if (g_pOnGameTickCallback) + reinterpret_cast(g_pOnGameTickCallback)(simulate, first, last); for (int i = 0; i < 64; i++) - if (playermanager->IsPlayerOnline(i)) { + if (playermanager->IsPlayerOnline(i)) + { auto player = playermanager->GetPlayer(i); - if (!player) continue; + if (!player) + continue; player->Think(); } @@ -270,7 +279,7 @@ bool ClientConnectHook(void* _this, CPlayerSlot slot, const char* pszName, uint6 player->SetUnauthorizedSteamID(xuid); if (g_pOnClientConnectCallback) - if (reinterpret_cast(g_pOnClientConnectCallback)(playerid) == false) + if (reinterpret_cast(g_pOnClientConnectCallback)(playerid) == false) return false; return reinterpret_cast(g_pClientConnectHook->GetOriginal())(_this, slot, pszName, xuid, pszNetworkID, unk1, pRejectReason); @@ -280,11 +289,13 @@ void OnClientConnectedHook(void* _this, CPlayerSlot slot, const char* pszName, u { static auto playermanager = g_ifaceService.FetchInterface(PLAYERMANAGER_INTERFACE_VERSION); auto playerid = slot.Get(); - if (bFakePlayer) { + if (bFakePlayer) + { auto player = playermanager->RegisterPlayer(playerid); player->Initialize(playerid); } - else { + else + { auto cvarmanager = g_ifaceService.FetchInterface(CONVARMANAGER_INTERFACE_VERSION); cvarmanager->QueryClientConvar(playerid, "cl_language"); } @@ -302,16 +313,18 @@ void ClientDisconnectHook(void* _this, CPlayerSlot slot, int reason, const char* auto playerid = slot.Get(); if (g_pOnClientDisconnectCallback) - reinterpret_cast(g_pOnClientDisconnectCallback)(playerid, reason); + reinterpret_cast(g_pOnClientDisconnectCallback)(playerid, reason); playermanager->UnregisterPlayer(playerid); } IPlayer* CPlayerManager::RegisterPlayer(int playerid) { - if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) return nullptr; + if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) + return nullptr; - if (g_Players[playerid] != nullptr) UnregisterPlayer(playerid); + if (g_Players[playerid] != nullptr) + UnregisterPlayer(playerid); g_Players[playerid] = new CPlayer(); g_Players[playerid]->Initialize(playerid); @@ -321,8 +334,10 @@ IPlayer* CPlayerManager::RegisterPlayer(int playerid) void CPlayerManager::UnregisterPlayer(int playerid) { - if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) return; - if (g_Players[playerid] == nullptr) return; + if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) + return; + if (g_Players[playerid] == nullptr) + return; static auto vgui = g_ifaceService.FetchInterface(VGUI_INTERFACE_VERSION); @@ -335,14 +350,17 @@ void CPlayerManager::UnregisterPlayer(int playerid) IPlayer* CPlayerManager::GetPlayer(int playerid) { - if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) return nullptr; - if (IsPlayerOnline(playerid)) return g_Players[playerid]; - return nullptr; + if (!IsPlayerOnline(playerid)) + return nullptr; + + auto player = g_Players[playerid]; + return *(void***)player ? player : nullptr; } bool CPlayerManager::IsPlayerOnline(int playerid) { - if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) return false; + if (playerid < 0 || playerid >= g_SwiftlyCore.GetMaxGameClients()) + return false; static auto engine = g_ifaceService.FetchInterface(INTERFACEVERSION_VENGINESERVER); return (engine->GetClientSteamID(playerid) != nullptr); } @@ -353,7 +371,8 @@ int CPlayerManager::GetPlayerCount() int count = 0; for (int i = 0; i < GetPlayerCap(); i++) - if (engine->GetClientSteamID(i)) ++count; + if (engine->GetClientSteamID(i)) + ++count; return count; } @@ -365,9 +384,11 @@ int CPlayerManager::GetPlayerCap() void CPlayerManager::SendMsg(MessageType type, const std::string& message, int duration) { - for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) { + for (int i = 0; i < g_SwiftlyCore.GetMaxGameClients(); i++) + { IPlayer* player = GetPlayer(i); - if (player) player->SendMsg(type, message, duration); + if (player) + player->SendMsg(type, message, duration); } } @@ -380,10 +401,13 @@ void CPlayerManager::OnValidateAuthTicket(ValidateAuthTicketResponse_t* response { uint64_t steamid = response->m_SteamID.ConvertToUint64(); - for (int i = 0; i < GetPlayerCap(); i++) { + for (int i = 0; i < GetPlayerCap(); i++) + { auto player = GetPlayer(i); - if (!player) continue; - if (player->GetUnauthorizedSteamID() != steamid) continue; + if (!player) + continue; + if (player->GetUnauthorizedSteamID() != steamid) + continue; player->ChangeAuthorizationState(response->m_eAuthSessionResponse == k_EAuthSessionResponseOK); break; From 66083b802d71b4d35355fc3a60500d337658252c Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Sat, 15 Nov 2025 23:14:25 +0800 Subject: [PATCH 2/3] fix: Ensure options are properly locked for thread safety --- .../SwiftlyS2.Core/Modules/Menus/MenuAPI.cs | 149 ++++++++++-------- 1 file changed, 86 insertions(+), 63 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs index 5a158bd77..4c5fc84ce 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs @@ -117,7 +117,7 @@ public IReadOnlyList Options { // private readonly ConcurrentDictionary> visibleOptionsCache = new(); private readonly ConcurrentDictionary autoCloseCancelTokens = new(); - private readonly ConcurrentDictionary renderCache = new(); + // private readonly ConcurrentDictionary renderCache = new(); private readonly CancellationTokenSource renderLoopCancellationTokenSource = new(); private volatile bool disposed; @@ -137,18 +137,21 @@ public MenuAPI( ISwiftlyCore core, MenuConfiguration configuration, MenuKeybindO Builder = builder; // Parent = parent; - options.Clear(); + lock (optionsLock) + { + options.Clear(); + } selectedOptionIndex.Clear(); desiredOptionIndex.Clear(); // selectedDisplayLine.Clear(); autoCloseCancelTokens.Clear(); // visibleOptionsCache.Clear(); - renderCache.Clear(); + // renderCache.Clear(); maxOptions = 0; // maxDisplayLines = 0; - core.Event.OnTick += OnTick; + // core.Event.OnTick += OnTick; _ = Task.Run(async () => { @@ -200,19 +203,22 @@ public void Dispose() } }); - // options.ForEach(option => option.Dispose()); - options.Clear(); + lock (optionsLock) + { + // options.ForEach(option => option.Dispose()); + options.Clear(); + } selectedOptionIndex.Clear(); desiredOptionIndex.Clear(); // selectedDisplayLine.Clear(); autoCloseCancelTokens.Clear(); // visibleOptionsCache.Clear(); - renderCache.Clear(); + // renderCache.Clear(); maxOptions = 0; // maxDisplayLines = 0; - core.Event.OnTick -= OnTick; + // core.Event.OnTick -= OnTick; renderLoopCancellationTokenSource?.Cancel(); renderLoopCancellationTokenSource?.Dispose(); @@ -221,24 +227,24 @@ public void Dispose() GC.SuppressFinalize(this); } - private void OnTick() - { - if (maxOptions <= 0) - { - return; - } + // private void OnTick() + // { + // if (maxOptions <= 0) + // { + // return; + // } - foreach (var kvp in renderCache) - { - var player = kvp.Key; - if (!player.IsValid || player.IsFakeClient) - { - continue; - } + // foreach (var kvp in renderCache) + // { + // var player = kvp.Key; + // if (!player.IsValid || player.IsFakeClient) + // { + // continue; + // } - NativePlayer.SetCenterMenuRender(player.PlayerID, kvp.Value); - } - } + // NativePlayer.SetCenterMenuRender(player.PlayerID, kvp.Value); + // } + // } private void OnRender() { @@ -264,22 +270,25 @@ private void OnRender() : Math.Clamp(baseMaxVisibleItems, 1, 5); var halfVisible = maxVisibleItems / 2; - lock (optionsLock) + foreach (var (player, desiredIndex, selectedIndex) in playerStates) { - foreach (var (player, desiredIndex, selectedIndex) in playerStates) - { - ProcessPlayerMenu(player, desiredIndex, selectedIndex, maxOptions, maxVisibleItems, halfVisible); - } + ProcessPlayerMenu(player, desiredIndex, selectedIndex, maxOptions, maxVisibleItems, halfVisible); } } private void ProcessPlayerMenu( IPlayer player, int desiredIndex, int selectedIndex, int maxOptions, int maxVisibleItems, int halfVisible ) { - var filteredOptions = options.Where(opt => opt.Visible && opt.GetVisible(player)).ToList(); - if (filteredOptions.Count == 0) + var filteredOptions = new List(); + lock (optionsLock) + { + filteredOptions = options.Where(opt => opt.Visible && opt.GetVisible(player)).ToList(); + } + + if (filteredOptions.Count <= 0) { var emptyHtml = BuildMenuHtml(player, [], 0, 0, maxOptions, maxVisibleItems); - _ = renderCache.AddOrUpdate(player, emptyHtml, ( _, _ ) => emptyHtml); + // _ = renderCache.AddOrUpdate(player, emptyHtml, ( _, _ ) => emptyHtml); + core.Scheduler.NextTick(() => NativePlayer.SetCenterMenuRender(player.PlayerID, emptyHtml)); return; } @@ -293,33 +302,40 @@ private void ProcessPlayerMenu( IPlayer player, int desiredIndex, int selectedIn }); var html = BuildMenuHtml(player, visibleOptions, safeArrowPosition, clampedDesiredIndex, maxOptions, maxVisibleItems); - _ = renderCache.AddOrUpdate(player, html, ( _, _ ) => html); - - var currentOption = visibleOptions[safeArrowPosition]; - var currentOriginalIndex = options.IndexOf(currentOption); + // _ = renderCache.AddOrUpdate(player, html, ( _, _ ) => html); + core.Scheduler.NextTick(() => NativePlayer.SetCenterMenuRender(player.PlayerID, html)); - if (currentOriginalIndex != selectedIndex) + lock (optionsLock) { - var updateResult = selectedOptionIndex.TryUpdate(player, currentOriginalIndex, selectedIndex); - if (updateResult && currentOriginalIndex != desiredIndex) + var currentOriginalIndex = options.IndexOf(visibleOptions[safeArrowPosition]); + + if (currentOriginalIndex != selectedIndex) { - _ = desiredOptionIndex.TryUpdate(player, currentOriginalIndex, desiredIndex); + var updateResult = selectedOptionIndex.TryUpdate(player, currentOriginalIndex, selectedIndex); + if (updateResult && currentOriginalIndex != desiredIndex) + { + _ = desiredOptionIndex.TryUpdate(player, currentOriginalIndex, desiredIndex); + } } } } private (IReadOnlyList VisibleOptions, int ArrowPosition) GetVisibleOptionsAndArrowPosition( List filteredOptions, int clampedDesiredIndex, int maxVisibleItems, int halfVisible ) { - var filteredMaxOptions = filteredOptions.Count; - var desiredOption = options[clampedDesiredIndex]; - var mappedDesiredIndex = filteredOptions.IndexOf(desiredOption); - - if (mappedDesiredIndex < 0) + var filteredMaxOptions = -1; + var mappedDesiredIndex = -1; + lock (optionsLock) { - mappedDesiredIndex = filteredOptions - .Select(( opt, idx ) => (Index: idx, Distance: Math.Abs(options.IndexOf(opt) - clampedDesiredIndex))) - .MinBy(x => x.Distance) - .Index; + filteredMaxOptions = filteredOptions.Count; + mappedDesiredIndex = filteredOptions.IndexOf(options[clampedDesiredIndex]); + + if (mappedDesiredIndex < 0) + { + mappedDesiredIndex = filteredOptions + .Select(( opt, idx ) => (Index: idx, Distance: Math.Abs(options.IndexOf(opt) - clampedDesiredIndex))) + .MinBy(x => x.Distance) + .Index; + } } if (filteredMaxOptions <= maxVisibleItems) @@ -467,7 +483,7 @@ public void HideForPlayer( IPlayer player ) SetFreezeState(player, false); - _ = renderCache.TryRemove(player, out _); + // _ = renderCache.TryRemove(player, out _); if (autoCloseCancelTokens.TryRemove(player, out var token)) { @@ -521,25 +537,29 @@ public bool RemoveOption( IMenuOption option ) public bool MoveToOption( IPlayer player, IMenuOption option ) { - return MoveToOptionIndex(player, options.IndexOf(option)); + lock (optionsLock) + { + return MoveToOptionIndex(player, options.IndexOf(option)); + } } public bool MoveToOptionIndex( IPlayer player, int index ) { - lock (optionsLock) + + if (maxOptions == 0 || !desiredOptionIndex.TryGetValue(player, out var oldIndex)) { - if (maxOptions == 0 || !desiredOptionIndex.TryGetValue(player, out var oldIndex)) - { - return false; - } + return false; + } - var targetIndex = ((index % maxOptions) + maxOptions) % maxOptions; - var direction = Math.Sign(targetIndex - oldIndex); - if (direction == 0) - { - return true; - } + var targetIndex = ((index % maxOptions) + maxOptions) % maxOptions; + var direction = Math.Sign(targetIndex - oldIndex); + if (direction == 0) + { + return true; + } + lock (optionsLock) + { var visibleIndex = Enumerable.Range(0, maxOptions) .Select(i => (((targetIndex + (i * direction)) % maxOptions) + maxOptions) % maxOptions) .FirstOrDefault(idx => options[idx].Visible && options[idx].GetVisible(player), -1); @@ -550,7 +570,10 @@ public bool MoveToOptionIndex( IPlayer player, int index ) public IMenuOption? GetCurrentOption( IPlayer player ) { - return selectedOptionIndex.TryGetValue(player, out var index) ? options[index] : null; + lock (optionsLock) + { + return selectedOptionIndex.TryGetValue(player, out var index) ? options[index] : null; + } } public int GetCurrentOptionIndex( IPlayer player ) From 6ca89fcea3b10381a59d193e9abe3c2e4a0f278b Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Sat, 15 Nov 2025 23:38:14 +0800 Subject: [PATCH 3/3] fix: Add null check for CPlayer pointer before vtable validation --- src/server/players/manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/players/manager.cpp b/src/server/players/manager.cpp index 7cfe085f6..f88c4b68d 100644 --- a/src/server/players/manager.cpp +++ b/src/server/players/manager.cpp @@ -354,7 +354,7 @@ IPlayer* CPlayerManager::GetPlayer(int playerid) return nullptr; auto player = g_Players[playerid]; - return *(void***)player ? player : nullptr; + return player && *(void***)player ? player : nullptr; } bool CPlayerManager::IsPlayerOnline(int playerid)