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;