From 1ab364596fedc5070bba8ffe86d0a470c13bdd8e Mon Sep 17 00:00:00 2001 From: krzysztofmotas <127015975+krzysztofmotas@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:05:29 +0200 Subject: [PATCH] Snapshot gangzone IDs during checks Avoid iterator invalidation by taking a snapshot of checkingList IDs before iterating. Replace direct IGangZone* iteration with a DynamicArray of IDs, store entered zones as IDs, and re-validate get(id)/checkingList.valid(id)/isShownForPlayer before use. Apply same snapshot approach in onPlayerClickMap. This prevents crashes when a gangzone is destroyed during dispatch. --- Server/Components/GangZones/gangzone.cpp | 44 +++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/Server/Components/GangZones/gangzone.cpp b/Server/Components/GangZones/gangzone.cpp index a73d23481..f9c632d25 100644 --- a/Server/Components/GangZones/gangzone.cpp +++ b/Server/Components/GangZones/gangzone.cpp @@ -155,13 +155,28 @@ class GangZonesComponent final : public IGangZonesComponent, public PlayerConnec // Only go through those that are added to our checking list using IGangZonesComponent::useGangZoneCheck if (checkingList.entries().size()) { + // Snapshot IDs: destroying a gangzone during dispatch below must not mutate + // checkingList while we're iterating over it. + DynamicArray ids; + ids.reserve(checkingList.entries().size()); + for (auto gangzone : checkingList.entries()) + { + ids.push_back(gangzone->getID()); + } + const Vector3& playerPos = player.getPosition(); - DynamicArray enteredList; + DynamicArray enteredList; // Guarantee a single allocation - enteredList.reserve(checkingList.entries().size()); + enteredList.reserve(ids.size()); - for (auto gangzone : checkingList.entries()) + for (int id : ids) { + IGangZone* gangzone = get(id); + if (!gangzone || !checkingList.valid(id)) + { + continue; + } + // Only check visible gangzones if (!gangzone->isShownForPlayer(player)) { @@ -175,7 +190,7 @@ class GangZonesComponent final : public IGangZonesComponent, public PlayerConnec if (isPlayerInZoneArea && !isPlayerInInsideList) { // Collect entered gangzones to call events with them later after exit events - enteredList.push_back(gangzone); + enteredList.push_back(id); } else if (!isPlayerInZoneArea && isPlayerInInsideList) { @@ -190,8 +205,14 @@ class GangZonesComponent final : public IGangZonesComponent, public PlayerConnec } // Call enter gangzone events for all the gangzones in entered gangzone list - for (auto gangzone : enteredList) + for (int id : enteredList) { + IGangZone* gangzone = get(id); + if (!gangzone || !checkingList.valid(id) || !gangzone->isShownForPlayer(player)) + { + continue; + } + ScopedPoolReleaseLock lock(*this, *gangzone); static_cast(gangzone)->setPlayerInside(player, true); eventDispatcher.dispatch( @@ -319,8 +340,21 @@ class GangZonesComponent final : public IGangZonesComponent, public PlayerConnec void onPlayerClickMap(IPlayer& player, Vector3 clickPos) override { // Only go through those that are added to our checking list using IGangZonesComponent::toggleGangZoneCheck + DynamicArray ids; + ids.reserve(checkingList.entries().size()); for (auto gangzone : checkingList.entries()) { + ids.push_back(gangzone->getID()); + } + + for (int id : ids) + { + IGangZone* gangzone = get(id); + if (!gangzone || !checkingList.valid(id)) + { + continue; + } + // only check visible gangzones if (!gangzone->isShownForPlayer(player)) {