From 8d6fe4b116a304cd4fe16ed673d91c383e1837ba Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 02:02:47 -0500 Subject: [PATCH] fix(ghosting): scene ghosting follows the client's scene streaming Re-checked against client 1.10.64: Zone::Run calls StreamScenesAroundPosition with the ghost reference position, and with the controlled object's own position only when the two differ (ghost reference override on). The client loads the scene under the reference point and the global scene; with the override on it also loads the scene under the player and its connected scenes. The cell lookup (floor(v + 0.5), x cell * resolution + z cell), the transition pairs and FixupInvalidTransitions match what ZoneScenes does. Scene ghosting now adds the scenes around the player while the ghost reference is overridden (cinematics), and the comments say the server keeps each scene's neighbours too (a superset, so objects across a transition exist before the player crosses it). Check in game (with ghosting_scenes=1): walk across scene transitions in Avant Gardens and Gnarled Forest; objects on both sides show up and nothing pops in at the line. Play a cinematic that moves the camera away (e.g. a mission cinematic) and objects around the player stay. Co-Authored-By: Claude Opus 5.5 --- dCommon/ZoneScenes.cpp | 6 ++++++ dCommon/ZoneScenes.h | 24 +++++++++++++++++------- dGame/EntityManager.cpp | 15 +++++++++++++-- tests/dCommonTests/ZoneScenesTests.cpp | 9 +++++++++ 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/dCommon/ZoneScenes.cpp b/dCommon/ZoneScenes.cpp index 62c3062ee..4cac2db59 100644 --- a/dCommon/ZoneScenes.cpp +++ b/dCommon/ZoneScenes.cpp @@ -82,6 +82,12 @@ namespace ZoneScenes { loaded.insert(neighbours.begin(), neighbours.end()); return loaded; } + + std::set SceneGraph::Loaded(uint32_t referenceScene, uint32_t positionScene, bool referenceOverridden) const { + auto loaded = Loaded(referenceScene); + if (referenceOverridden) loaded.merge(Loaded(positionScene)); + return loaded; + } } namespace ZoneScenes { diff --git a/dCommon/ZoneScenes.h b/dCommon/ZoneScenes.h index 32cca917a..dadfff09a 100644 --- a/dCommon/ZoneScenes.h +++ b/dCommon/ZoneScenes.h @@ -9,12 +9,16 @@ #include "ZoneFile.h" /** - * Which of a zone's scenes the game client keeps loaded, as it decides it (Zone::StreamScenesAroundPosition, - * 0x0108a3f0 in client 1.10.64): the scene under the player, found in the terrain's scene map - * (TerrainManager::GetSceneAtPos, 0x01069010), and the scenes connected to it by the zone file's scene transitions - * (LevelManager's connected scenes, 0x01066500); the global scene (0) always stays. A scene is a scene id: its layers - * (the audio scenes share their scene's id) come and go with it. Pure, so the world server and the dashboard use the - * same rules and both can be unit tested. + * Which of a zone's scenes a player gets the objects of, from how the game client streams scenes (client 1.10.64). + * Zone::Run (0x0108a500) calls Zone::StreamScenesAroundPosition (0x0108a3f0) with the ghost reference position and, + * only when it differs from it (ToggleGhostReferenceOverride on), the position of the object the player controls. + * The client loads the scene under the ghost reference position (TerrainManager::GetSceneAtPos, 0x01069010, via the + * cell lookup 0x01065c00) and the global scene; with the override on, also the scene under the player and the scenes + * connected to it by the zone file's scene transitions (Zone::AddConnectedScenes, 0x01066500). + * The server sends a little more than that: each scene's connected scenes as well, so the objects across a scene + * transition already exist when the player crosses it. A scene is a scene id: its layers (the audio scenes share + * their scene's id) come and go with it. Pure, so the world server and the dashboard use the same rules and both can + * be unit tested. */ namespace ZoneScenes { constexpr uint32_t GLOBAL_SCENE = 0; @@ -56,9 +60,15 @@ namespace ZoneScenes { // The scenes connected to `scene` (not itself); none for the global scene const std::set& Neighbours(uint32_t scene) const; - // What the client keeps loaded with the player in `scene`: the global scene, `scene` and its neighbours + // What a player in `scene` gets the objects of: the global scene, `scene` and its neighbours std::set Loaded(uint32_t scene) const; + /** + * Loaded(referenceScene), plus Loaded(positionScene) while the player's ghost reference is overridden: the + * client then also loads the scenes around the player itself (Zone::StreamScenesAroundPosition). + */ + std::set Loaded(uint32_t referenceScene, uint32_t positionScene, bool referenceOverridden) const; + const std::set& Scenes() const { return m_Scenes; } private: diff --git a/dGame/EntityManager.cpp b/dGame/EntityManager.cpp index d7c1730a7..8b057e635 100644 --- a/dGame/EntityManager.cpp +++ b/dGame/EntityManager.cpp @@ -507,6 +507,17 @@ void EntityManager::UpdateGhosting() { m_PlayersToUpdateGhosting.clear(); } +namespace { + // The scenes a player gets the objects of (ZoneScenes::SceneGraph::Loaded): around the ghost reference point, and + // around the player too while the reference is overridden + std::set ClientScenes(const Zone& zone, const GhostComponent& ghost) { + const auto& reference = ghost.GetGhostReferencePoint(); + const auto& position = ghost.GetOriginGhostReferencePoint(); + const auto& map = zone.GetSceneMap(); + return zone.GetSceneGraph().Loaded(map.SceneAt(reference.x, reference.z), map.SceneAt(position.x, position.z), ghost.GetGhostOverride()); + } +} + void EntityManager::UpdateGhosting(Entity* player) { if (!player) return; @@ -518,7 +529,7 @@ void EntityManager::UpdateGhosting(Entity* player) { const auto& referencePoint = ghostComponent->GetGhostReferencePoint(); const auto isOverride = ghostComponent->GetGhostOverride(); const auto* zone = m_SceneGhosting ? Game::zoneManager->GetZone() : nullptr; - const auto loaded = zone ? zone->GetSceneGraph().Loaded(zone->GetSceneMap().SceneAt(referencePoint.x, referencePoint.z)) : std::set{}; + const auto loaded = zone ? ClientScenes(*zone, *ghostComponent) : std::set{}; for (auto* entity : m_EntitiesToGhost) { const auto& entityPoint = entity->GetPosition(); @@ -588,7 +599,7 @@ void EntityManager::CheckGhosting(Entity* entity) { const auto observed = ghostComponent->IsObserved(id); bool tooFar = false, nearEnough = false; if (zone) { - const auto loaded = zone->GetSceneGraph().Loaded(zone->GetSceneMap().SceneAt(entityPoint.x, entityPoint.z)); + const auto loaded = ClientScenes(*zone, *ghostComponent); nearEnough = ZoneScenes::InLoadedScene(entity->GetScene(), entityScene, loaded); tooFar = !nearEnough; } else { diff --git a/tests/dCommonTests/ZoneScenesTests.cpp b/tests/dCommonTests/ZoneScenesTests.cpp index 2eda35a3f..bb60fd34c 100644 --- a/tests/dCommonTests/ZoneScenesTests.cpp +++ b/tests/dCommonTests/ZoneScenesTests.cpp @@ -63,6 +63,15 @@ TEST(ZoneScenesTests, LoadsTheConnectedScenes) { EXPECT_TRUE(graph.Neighbours(9).empty()); } +TEST(ZoneScenesTests, AddsTheScenesAroundThePlayerWhileTheReferenceIsOverridden) { + const ZoneScenes::SceneGraph graph(Scenes({ 0, 1, 2, 3, 4 }), { Transition(1, 2), Transition(3, 4) }); + // Not overridden: only the reference point's scenes, wherever the player is + EXPECT_EQ(graph.Loaded(1, 3, false), (std::set{ 0, 1, 2 })); + // Overridden (a cinematic moved it): the client also loads the scene under the player and its neighbours + EXPECT_EQ(graph.Loaded(1, 3, true), (std::set{ 0, 1, 2, 3, 4 })); + EXPECT_EQ(graph.Loaded(1, 1, true), graph.Loaded(1)); +} + TEST(ZoneScenesTests, SendsSceneMapsAsRuns) { std::vector cells(300, 4); cells[0] = 1;