From ff3d4ad17fb0e98a34fbf7fbd1b85531fad36c8d Mon Sep 17 00:00:00 2001 From: Jonas Ornfelt Date: Tue, 10 Nov 2026 23:43:32 +0100 Subject: [PATCH 1/2] Detach player corpses from a map when their grid unloads Resurrectable corpses are world objects owned by the CorpseManager and outlive their grid. When the map itself goes away - an ended battleground, an unloaded instance - the corpse kept pointing at the deleted map, and the next visibility check of its dead owner (Creature::IsVisibleInGridForPlayer -> InReach -> InCommonFrame) read the freed map and crashed the server. Seen with a player who died in Warsong Gulch, logged out as a ghost and logged in after the battleground was gone. The grid unloader now takes the corpses of the grid out of the world and clears their map; the grid loader puts them back as before. InCommonFrame uses FindMap(), and the corpse users (both visibility checks, corpse reclaim) skip a corpse that is not in the world. --- src/game/Object/Creature.cpp | 3 ++- src/game/Object/Object.h | 5 +++++ src/game/Object/PlayerVisibility.cpp | 4 +++- src/game/Object/WorldObjectPosition.cpp | 5 +++-- src/game/WorldHandlers/MiscHandler.cpp | 3 ++- src/game/WorldHandlers/ObjectGridLoader.cpp | 24 +++++++++++++++++++++ src/game/WorldHandlers/ObjectGridLoader.h | 12 +++++++++++ 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/game/Object/Creature.cpp b/src/game/Object/Creature.cpp index 50a954e95..0badba801 100644 --- a/src/game/Object/Creature.cpp +++ b/src/game/Object/Creature.cpp @@ -2429,7 +2429,8 @@ bool Creature::IsVisibleInGridForPlayer(Player* pl) const if (IsAlive()) { Corpse* corpse = pl->GetCorpse(); - if (corpse) + // a corpse can be out of the world (its grid unloaded, its battleground ended): no map to compare + if (corpse && corpse->IsInWorld()) { // 20 - aggro distance for same level, 25 - max additional distance if player level less that creature level if (InReach(*corpse, *this, (20 + 25)*sWorld.getConfig(CONFIG_FLOAT_RATE_CREATURE_AGGRO))) diff --git a/src/game/Object/Object.h b/src/game/Object/Object.h index 8c9eadd73..b1d86d32f 100644 --- a/src/game/Object/Object.h +++ b/src/game/Object/Object.h @@ -863,6 +863,11 @@ class WorldObject : public Object // used to check all object's GetMap() calls when object is not in world! void ResetMap(); + /// Drops the map pointer of an object that outlives its map: a player corpse stays in the + /// CorpseManager after its grid unloaded, and its map can be destroyed (an ended battleground, + /// an unloaded instance). FindMap() is NULL afterwards; the grid loader sets the map again. + void ForgetMap() { m_currMap = NULL; } + TerrainInfo const* GetTerrain() const; void AddToClientUpdateList() override; diff --git a/src/game/Object/PlayerVisibility.cpp b/src/game/Object/PlayerVisibility.cpp index 57260b243..66b9d6e13 100644 --- a/src/game/Object/PlayerVisibility.cpp +++ b/src/game/Object/PlayerVisibility.cpp @@ -116,7 +116,9 @@ bool Player::IsVisibleInGridForPlayer(Player* pl) const // Dead player see live players near own corpse if (IsAlive()) { - if (Corpse* corpse = pl->GetCorpse()) + Corpse* corpse = pl->GetCorpse(); + // a corpse can be out of the world (its grid unloaded, its battleground ended): no map to compare + if (corpse && corpse->IsInWorld()) { // 20 - aggro distance for same level, 25 - max additional distance if player level less that creature level if (InReach(*corpse, *this, (20 + 25) * sWorld.getConfig(CONFIG_FLOAT_RATE_CREATURE_AGGRO))) diff --git a/src/game/Object/WorldObjectPosition.cpp b/src/game/Object/WorldObjectPosition.cpp index 548095105..5a4a982a3 100644 --- a/src/game/Object/WorldObjectPosition.cpp +++ b/src/game/Object/WorldObjectPosition.cpp @@ -250,8 +250,9 @@ static bool InCommonFrame(WorldObject const& a, WorldObject const& b, return true; } - TransportMap* va = a.GetMap() ? a.GetMap()->AsTransport() : NULL; - TransportMap* vb = b.GetMap() ? b.GetMap()->AsTransport() : NULL; + // FindMap, not GetMap: a player corpse whose grid unloaded has no map any more (GetMap asserts) + TransportMap* va = a.FindMap() ? a.FindMap()->AsTransport() : NULL; + TransportMap* vb = b.FindMap() ? b.FindMap()->AsTransport() : NULL; if (!va && !vb) { diff --git a/src/game/WorldHandlers/MiscHandler.cpp b/src/game/WorldHandlers/MiscHandler.cpp index 1bd220149..1530e17cf 100644 --- a/src/game/WorldHandlers/MiscHandler.cpp +++ b/src/game/WorldHandlers/MiscHandler.cpp @@ -668,7 +668,8 @@ void WorldSession::HandleReclaimCorpseOpcode(WorldPacket& recv_data) Corpse* corpse = GetPlayer()->GetCorpse(); - if (!corpse) + // out of the world, the corpse has no map to reach it in (its grid unloaded, its battleground ended) + if (!corpse || !corpse->IsInWorld()) { return; } diff --git a/src/game/WorldHandlers/ObjectGridLoader.cpp b/src/game/WorldHandlers/ObjectGridLoader.cpp index ac8543fe5..62c7e6d3b 100644 --- a/src/game/WorldHandlers/ObjectGridLoader.cpp +++ b/src/game/WorldHandlers/ObjectGridLoader.cpp @@ -441,6 +441,30 @@ ObjectGridUnloader::Unload(GridType& grid) { TypeContainerVisitor unloader(*this); grid.Visit(unloader); + + // the resurrectable player corpses are world objects and not deleted here + ObjectWorldUnloader worldUnloader; + TypeContainerVisitor corpses(worldUnloader); + grid.Visit(corpses); +} + +void ObjectWorldUnloader::Visit(CorpseMapType& m) +{ + // collect first: RemoveFromWorld must not run while the references are walked + std::vector corpses; + for (CorpseMapType::iterator iter = m.begin(); iter != m.end(); ++iter) + { + corpses.push_back(iter->getSource()); + } + + for (Corpse* corpse : corpses) + { + if (corpse->IsInWorld()) + { + corpse->RemoveFromWorld(); + } + corpse->ForgetMap(); + } } template diff --git a/src/game/WorldHandlers/ObjectGridLoader.h b/src/game/WorldHandlers/ObjectGridLoader.h index 7c616b157..87cdbc533 100644 --- a/src/game/WorldHandlers/ObjectGridLoader.h +++ b/src/game/WorldHandlers/ObjectGridLoader.h @@ -85,6 +85,18 @@ class ObjectGridUnloader NGridType& i_grid; }; +/** + * Takes the player corpses of a grid that unloads out of the world. The CorpseManager owns them and + * they outlive the grid - and the map, which an ended battleground or an unloaded instance deletes - + * so they must not keep pointing at it; LoadHelper puts them back when the grid loads again. + */ +class ObjectWorldUnloader +{ + public: + void Visit(CorpseMapType& m); + template void Visit(GridRefManager&) {} +}; + class ObjectGridStoper { public: From 30fedf4b81b2751db62ef289cb8cd0bcc023f114 Mon Sep 17 00:00:00 2001 From: Jonas Ornfelt Date: Wed, 11 Nov 2026 09:29:33 +0100 Subject: [PATCH 2/2] Detach corpses only on a whole-grid unload and add them to their own map AddCorpsesToGrid() links every corpse of a grid into the cell that loaded it, so detaching corpses in ObjectGridUnloader::Unload() also ran for Map::UnloadCell() and could take corpses of cells that stay loaded out of the world. Detach them in UnloadN() only. AddCorpsesToGrid() also matched the grid coordinates without the map, so loading a continent grid could link another map's corpse into it. Skip corpses of other maps. --- src/game/Object/CorpseManager.cpp | 6 ++++++ src/game/WorldHandlers/ObjectGridLoader.cpp | 15 +++++++++++++-- src/game/WorldHandlers/ObjectGridLoader.h | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/game/Object/CorpseManager.cpp b/src/game/Object/CorpseManager.cpp index 026a05c63..aa3b95637 100644 --- a/src/game/Object/CorpseManager.cpp +++ b/src/game/Object/CorpseManager.cpp @@ -152,6 +152,12 @@ void CorpseManager::AddCorpsesToGrid(GridPair const& gridpair, GridType& grid, M return; } + // the same grid coordinates exist on every map + if (corpse->GetMapId() != map->GetId()) + { + return; + } + // On an instanceable map take only the corpses belonging to this instance. if (map->Instanceable()) { diff --git a/src/game/WorldHandlers/ObjectGridLoader.cpp b/src/game/WorldHandlers/ObjectGridLoader.cpp index 62c7e6d3b..c7eeecabc 100644 --- a/src/game/WorldHandlers/ObjectGridLoader.cpp +++ b/src/game/WorldHandlers/ObjectGridLoader.cpp @@ -441,11 +441,22 @@ ObjectGridUnloader::Unload(GridType& grid) { TypeContainerVisitor unloader(*this); grid.Visit(unloader); +} - // the resurrectable player corpses are world objects and not deleted here +void ObjectGridUnloader::DetachCorpsesN() +{ + // the resurrectable player corpses are world objects and not deleted with the grid; only a whole + // grid detaches them - AddCorpsesToGrid puts every corpse of the grid in the cell that loaded it, + // so a single cell unload would also detach corpses of cells that stay loaded ObjectWorldUnloader worldUnloader; TypeContainerVisitor corpses(worldUnloader); - grid.Visit(corpses); + for (unsigned int x = 0; x < MAX_NUMBER_OF_CELLS; ++x) + { + for (unsigned int y = 0; y < MAX_NUMBER_OF_CELLS; ++y) + { + i_grid(x, y).Visit(corpses); + } + } } void ObjectWorldUnloader::Visit(CorpseMapType& m) diff --git a/src/game/WorldHandlers/ObjectGridLoader.h b/src/game/WorldHandlers/ObjectGridLoader.h index 87cdbc533..2c8a47116 100644 --- a/src/game/WorldHandlers/ObjectGridLoader.h +++ b/src/game/WorldHandlers/ObjectGridLoader.h @@ -77,9 +77,11 @@ class ObjectGridUnloader Unload(i_grid(x, y)); } } + DetachCorpsesN(); } void Unload(GridType& grid); + void DetachCorpsesN(); template void Visit(GridRefManager& m); private: NGridType& i_grid;