mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat: scene ghosting, as the client streams a zone's scenes
The client keeps the scene under the player loaded, the scenes the zone file's transitions connect to it, and the global scene (Zone::StreamScenesAroundPosition 0x0108a3f0, TerrainManager::GetSceneAtPos 0x01069010, the connected scenes at 0x01066500; transitions naming a missing scene dropped as Zone::FixupInvalidTransitions 0x010842e0 does). - ZoneScenes (dCommon): the terrain's scene map lookup and the scene graph, shared by the world server and the dashboard. - Objects remember the scene they were placed in (spawners pass theirs on). - ghosting_scenes=1 (world config, off by default): players get the objects of their loaded scenes instead of the ones within the ghosting distances; objects from no scene go by the scene under them. Zones without a scene map keep distance ghosting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ set(DCOMMON_SOURCES
|
||||
"ZoneFile.cpp"
|
||||
"LevelFile.cpp"
|
||||
"Raw.cpp"
|
||||
"ZoneScenes.cpp"
|
||||
"Process.cpp"
|
||||
"Permissions.cpp"
|
||||
"NifFile.cpp"
|
||||
|
||||
85
dCommon/ZoneScenes.cpp
Normal file
85
dCommon/ZoneScenes.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "ZoneScenes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace ZoneScenes {
|
||||
namespace {
|
||||
// The client's margin on chunk edges (FLOAT_01479f24)
|
||||
constexpr float EDGE = 0.001f;
|
||||
}
|
||||
|
||||
SceneMap::SceneMap(const Raw::Raw& raw) {
|
||||
bool first = true;
|
||||
for (const auto& chunk : raw.chunks) {
|
||||
if (!chunk.IsValidForSceneLookup() || chunk.sceneMap.size() < static_cast<size_t>(chunk.colorMapResolution) * chunk.colorMapResolution) continue;
|
||||
Chunk out;
|
||||
out.minX = chunk.offsetX;
|
||||
out.minZ = chunk.offsetZ;
|
||||
out.maxX = chunk.offsetX + static_cast<float>(chunk.width - 1) * chunk.scaleFactor;
|
||||
out.maxZ = chunk.offsetZ + static_cast<float>(chunk.height - 1) * chunk.scaleFactor;
|
||||
out.resolution = chunk.colorMapResolution;
|
||||
out.cellsPerUnitX = static_cast<float>(chunk.colorMapResolution) / (static_cast<float>(chunk.width - 1) * chunk.scaleFactor);
|
||||
out.cellsPerUnitZ = static_cast<float>(chunk.colorMapResolution) / (static_cast<float>(chunk.height - 1) * chunk.scaleFactor);
|
||||
out.scenes = chunk.sceneMap;
|
||||
m_MinX = first ? out.minX : std::min(m_MinX, out.minX);
|
||||
m_MinZ = first ? out.minZ : std::min(m_MinZ, out.minZ);
|
||||
m_MaxX = first ? out.maxX : std::max(m_MaxX, out.maxX);
|
||||
m_MaxZ = first ? out.maxZ : std::max(m_MaxZ, out.maxZ);
|
||||
first = false;
|
||||
m_Chunks.push_back(std::move(out));
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SceneMap::SceneAt(float x, float z) const {
|
||||
if (m_Chunks.empty() || !std::isfinite(x) || !std::isfinite(z)) return GLOBAL_SCENE;
|
||||
// As TerrainManager's chunk lookup (0x01065c00): clamped to the terrain, then to one cell inside its far edge
|
||||
x = std::clamp(x, m_MinX, m_MaxX);
|
||||
z = std::clamp(z, m_MinZ, m_MaxZ);
|
||||
for (const auto& chunk : m_Chunks) {
|
||||
const auto px = std::min(x, m_MaxX - 1.0f / chunk.cellsPerUnitX);
|
||||
const auto pz = std::min(z, m_MaxZ - 1.0f / chunk.cellsPerUnitZ);
|
||||
if (px < chunk.minX - EDGE || px >= chunk.maxX - EDGE || pz < chunk.minZ - EDGE || pz >= chunk.maxZ - EDGE) continue;
|
||||
const auto last = static_cast<int64_t>(chunk.resolution) - 1;
|
||||
const auto cellX = std::clamp(static_cast<int64_t>(std::floor(chunk.cellsPerUnitX * (px - chunk.minX) + 0.5f)), int64_t{ 0 }, last);
|
||||
const auto cellZ = std::clamp(static_cast<int64_t>(std::floor(chunk.cellsPerUnitZ * (pz - chunk.minZ) + 0.5f)), int64_t{ 0 }, last);
|
||||
const auto scene = chunk.scenes[static_cast<size_t>(cellX * chunk.resolution + cellZ)];
|
||||
return scene == NO_SCENE ? GLOBAL_SCENE : scene;
|
||||
}
|
||||
return GLOBAL_SCENE;
|
||||
}
|
||||
|
||||
SceneGraph::SceneGraph(const std::vector<ZoneScene>& scenes, const std::vector<SceneTransition>& transitions) {
|
||||
for (const auto& scene : scenes) m_Scenes.insert(scene.id);
|
||||
const auto link = [this](uint32_t from, uint32_t to) {
|
||||
auto it = std::find_if(m_Neighbours.begin(), m_Neighbours.end(), [from](const auto& entry) { return entry.first == from; });
|
||||
if (it == m_Neighbours.end()) it = m_Neighbours.insert(m_Neighbours.end(), { from, {} });
|
||||
it->second.insert(to);
|
||||
};
|
||||
for (const auto& transition : transitions) {
|
||||
if (transition.points.size() < 2) continue;
|
||||
// The low half of a point's LWOSCENEID is the scene, the high half its layer
|
||||
const auto a = static_cast<uint32_t>(transition.points[0].sceneID & 0xFFFFFFFF);
|
||||
const auto b = static_cast<uint32_t>(transition.points[1].sceneID & 0xFFFFFFFF);
|
||||
if (a == b || !m_Scenes.contains(a) || !m_Scenes.contains(b)) continue;
|
||||
link(a, b);
|
||||
link(b, a);
|
||||
}
|
||||
}
|
||||
|
||||
const std::set<uint32_t>& SceneGraph::Neighbours(uint32_t scene) const {
|
||||
static const std::set<uint32_t> NONE;
|
||||
if (scene == GLOBAL_SCENE) return NONE;
|
||||
const auto it = std::find_if(m_Neighbours.begin(), m_Neighbours.end(), [scene](const auto& entry) { return entry.first == scene; });
|
||||
return it == m_Neighbours.end() ? NONE : it->second;
|
||||
}
|
||||
|
||||
std::set<uint32_t> SceneGraph::Loaded(uint32_t scene) const {
|
||||
std::set<uint32_t> loaded{ GLOBAL_SCENE };
|
||||
if (scene == GLOBAL_SCENE) return loaded;
|
||||
loaded.insert(scene);
|
||||
const auto& neighbours = Neighbours(scene);
|
||||
loaded.insert(neighbours.begin(), neighbours.end());
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
76
dCommon/ZoneScenes.h
Normal file
76
dCommon/ZoneScenes.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "Raw.h"
|
||||
#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.
|
||||
*/
|
||||
namespace ZoneScenes {
|
||||
constexpr uint32_t GLOBAL_SCENE = 0;
|
||||
// What the scene map holds where it names no scene; the client reads it as the global scene
|
||||
constexpr uint8_t NO_SCENE = 0xFF;
|
||||
|
||||
// The terrain's scene map, kept apart from the rest of the .raw file (heights, textures and flairs aren't needed)
|
||||
class SceneMap {
|
||||
public:
|
||||
SceneMap() = default;
|
||||
explicit SceneMap(const Raw::Raw& raw);
|
||||
|
||||
// The scene at world position (x, z), as the client finds it: the global scene outside the map or where it has none
|
||||
uint32_t SceneAt(float x, float z) const;
|
||||
bool Empty() const { return m_Chunks.empty(); }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
float minX{}, minZ{}, maxX{}, maxZ{};
|
||||
float cellsPerUnitX{}, cellsPerUnitZ{}; // colorMapResolution / the chunk's width in world units
|
||||
uint32_t resolution{};
|
||||
std::vector<uint8_t> scenes; // resolution * resolution, x major
|
||||
};
|
||||
std::vector<Chunk> m_Chunks;
|
||||
float m_MinX{}, m_MinZ{}, m_MaxX{}, m_MaxZ{};
|
||||
};
|
||||
|
||||
// The scenes each scene is connected to, from the zone file's transitions
|
||||
class SceneGraph {
|
||||
public:
|
||||
SceneGraph() = default;
|
||||
/**
|
||||
* From a zone's scene transitions: each connects its first two points' scenes (the client's zone keeps them as
|
||||
* scene pairs). Transitions naming a scene the zone doesn't have are dropped, as the client's
|
||||
* Zone::FixupInvalidTransitions (0x010842e0) does.
|
||||
*/
|
||||
SceneGraph(const std::vector<ZoneScene>& scenes, const std::vector<SceneTransition>& transitions);
|
||||
|
||||
// The scenes connected to `scene` (not itself); none for the global scene
|
||||
const std::set<uint32_t>& Neighbours(uint32_t scene) const;
|
||||
|
||||
// What the client keeps loaded with the player in `scene`: the global scene, `scene` and its neighbours
|
||||
std::set<uint32_t> Loaded(uint32_t scene) const;
|
||||
|
||||
const std::set<uint32_t>& Scenes() const { return m_Scenes; }
|
||||
|
||||
private:
|
||||
std::set<uint32_t> m_Scenes;
|
||||
std::vector<std::pair<uint32_t, std::set<uint32_t>>> m_Neighbours;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a player with `loaded` scenes (SceneGraph::Loaded of the scene under them) gets an object: when its scene
|
||||
* is loaded. An object's scene is the scene it was placed in (`placedScene`, -1 for none: spawned at run time or a
|
||||
* player), else the scene under it (`sceneUnder`).
|
||||
*/
|
||||
inline bool InLoadedScene(int32_t placedScene, uint32_t sceneUnder, const std::set<uint32_t>& loaded) {
|
||||
return loaded.contains(placedScene >= 0 ? static_cast<uint32_t>(placedScene) : sceneUnder);
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,7 @@ namespace {
|
||||
c.Add(Format(Text(SHARED, "dump_folder", "Crash dump folder", "Where crash logs go. Empty turns them off.", "", true), eFormat::PATH));
|
||||
c.Add(Bool(WORLD, "generate_dump", "Crash dumps from world servers", "Write a dump when a world server crashes (needs the crash dump folder).", false, true));
|
||||
c.Add(Bool(WORLD, "save_lxfmls", "Save model files", "Save players' models (LXFML) to disk before they are split, for debugging.", false));
|
||||
c.Add(Bool(WORLD, "ghosting_scenes", "Scene ghosting", "Players get the objects of the scenes their game keeps loaded (the scene under them, the scenes connected to it and the global scene), as the client streams scenes, instead of the ones within 100 to 150 units. Needs the zone's terrain scene map; zones without one keep distance ghosting.", false, true));
|
||||
c.Add(Bool(WORLD, "bbb_consume_bricks", "Brick building uses bricks", "Saving a brick by brick model uses up the bricks in it, as live did. Off: the bricks go back to the backpack.", false));
|
||||
c.Add(Bool(SHARED, "dont_generate_dcf", "Don't build the chat filter file", "Skip compiling the chat word list to a file.", false, true));
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ Entity::Entity(const LWOOBJID& objectID, const EntityInfo& info, User* parentUse
|
||||
m_DefaultPosition = info.pos;
|
||||
m_DefaultRotation = info.rot;
|
||||
m_Scale = info.scale;
|
||||
m_Scene = info.scene;
|
||||
m_Spawner = info.spawner;
|
||||
m_SpawnerID = info.spawnerID;
|
||||
m_HasSpawnerNodeID = info.hasSpawnerNodeID;
|
||||
|
||||
@@ -349,6 +349,9 @@ public:
|
||||
// Scale will only be communicated to the client when the construction packet is sent
|
||||
void SetScale(const float scale) { m_Scale = scale; };
|
||||
|
||||
// The zone scene the object was placed in (EntityInfo::scene), -1 when it came from none
|
||||
int32_t GetScene() const { return m_Scene; }
|
||||
|
||||
void RegisterMsg(const MessageType::Game msgId, std::function<bool(GameMessages::GameMsg&)> handler);
|
||||
|
||||
bool HandleMsg(GameMessages::GameMsg& msg) const;
|
||||
@@ -392,6 +395,7 @@ private:
|
||||
NiPoint3 m_DefaultPosition;
|
||||
NiQuaternion m_DefaultRotation = QuatUtils::IDENTITY;
|
||||
float m_Scale;
|
||||
int32_t m_Scene = -1;
|
||||
|
||||
Spawner* m_Spawner;
|
||||
LWOOBJID m_SpawnerID;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "SwitchComponent.h"
|
||||
#include "UserManager.h"
|
||||
#include "dZoneManager.h"
|
||||
#include "Zone.h"
|
||||
#include "ZoneScenes.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
@@ -98,6 +100,13 @@ void EntityManager::Initialize() {
|
||||
Game::zoneManager->GetZoneID().GetMapID()
|
||||
) == m_GhostingExcludedZones.end();
|
||||
|
||||
// Scene ghosting needs the terrain's scene map; without one it stays off
|
||||
if (m_GhostingEnabled && GeneralUtils::TryParse<bool>(Game::config->GetValue("ghosting_scenes")).value_or(false)) {
|
||||
auto* zone = Game::zoneManager->GetZoneMut();
|
||||
m_SceneGhosting = zone && zone->LoadSceneMap();
|
||||
LOG("Scene ghosting is %s", m_SceneGhosting ? "on" : "off: the zone has no scene map");
|
||||
}
|
||||
|
||||
// grab hardcore mode settings and load them with sane defaults
|
||||
Game::config->AddConfigHandler([]() {Game::entityManager->ReloadConfig();});
|
||||
Game::entityManager->ReloadConfig();
|
||||
@@ -505,6 +514,8 @@ 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<uint32_t>{};
|
||||
|
||||
for (auto* entity : m_EntitiesToGhost) {
|
||||
const auto& entityPoint = entity->GetPosition();
|
||||
@@ -513,24 +524,32 @@ void EntityManager::UpdateGhosting(Entity* player) {
|
||||
|
||||
const auto observed = ghostComponent->IsObserved(id);
|
||||
|
||||
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
||||
bool tooFar = false, nearEnough = false;
|
||||
if (zone) {
|
||||
// As the client streams scenes: the objects of the scenes loaded around the player
|
||||
nearEnough = ZoneScenes::InLoadedScene(entity->GetScene(), zone->GetSceneMap().SceneAt(entityPoint.x, entityPoint.z), loaded);
|
||||
tooFar = !nearEnough;
|
||||
} else {
|
||||
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
||||
|
||||
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
|
||||
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
|
||||
|
||||
auto ghostingDistanceMax = m_GhostDistanceMaxSquared;
|
||||
auto ghostingDistanceMin = m_GhostDistanceMinSqaured;
|
||||
|
||||
const auto isAudioEmitter = entity->GetLOT() == 6368; // https://explorer.lu/objects/6368
|
||||
if (isAudioEmitter) {
|
||||
ghostingDistanceMax = ghostingDistanceMin;
|
||||
const auto isAudioEmitter = entity->GetLOT() == 6368; // https://explorer.lu/objects/6368
|
||||
if (isAudioEmitter) {
|
||||
ghostingDistanceMax = ghostingDistanceMin;
|
||||
}
|
||||
tooFar = distance > ghostingDistanceMax;
|
||||
nearEnough = ghostingDistanceMin > distance;
|
||||
}
|
||||
|
||||
if (observed && distance > ghostingDistanceMax && !isOverride) {
|
||||
if (observed && tooFar && !isOverride) {
|
||||
ghostComponent->GhostEntity(id);
|
||||
|
||||
DestructEntity(entity, player->GetSystemAddress());
|
||||
|
||||
entity->SetObservers(entity->GetObservers() - 1);
|
||||
} else if (!observed && ghostingDistanceMin > distance) {
|
||||
} else if (!observed && nearEnough) {
|
||||
// Check collectables, don't construct if it has been collected
|
||||
uint32_t collectionId = entity->GetCollectibleID();
|
||||
|
||||
@@ -555,6 +574,8 @@ void EntityManager::CheckGhosting(Entity* entity) {
|
||||
if (!entity) return;
|
||||
|
||||
const auto& referencePoint = entity->GetPosition();
|
||||
const auto* zone = m_SceneGhosting ? Game::zoneManager->GetZone() : nullptr;
|
||||
const auto entityScene = zone ? zone->GetSceneMap().SceneAt(referencePoint.x, referencePoint.z) : 0;
|
||||
for (auto* player : PlayerManager::GetAllPlayers()) {
|
||||
auto* ghostComponent = player->GetComponent<GhostComponent>();
|
||||
if (!ghostComponent) continue;
|
||||
@@ -562,13 +583,22 @@ void EntityManager::CheckGhosting(Entity* entity) {
|
||||
const auto& entityPoint = ghostComponent->GetGhostReferencePoint();
|
||||
const auto id = entity->GetObjectID();
|
||||
const auto observed = ghostComponent->IsObserved(id);
|
||||
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
||||
bool tooFar = false, nearEnough = false;
|
||||
if (zone) {
|
||||
const auto loaded = zone->GetSceneGraph().Loaded(zone->GetSceneMap().SceneAt(entityPoint.x, entityPoint.z));
|
||||
nearEnough = ZoneScenes::InLoadedScene(entity->GetScene(), entityScene, loaded);
|
||||
tooFar = !nearEnough;
|
||||
} else {
|
||||
const auto distance = NiPoint3::DistanceSquared(referencePoint, entityPoint);
|
||||
tooFar = distance > m_GhostDistanceMaxSquared;
|
||||
nearEnough = m_GhostDistanceMinSqaured > distance;
|
||||
}
|
||||
|
||||
if (observed && distance > m_GhostDistanceMaxSquared) {
|
||||
if (observed && tooFar) {
|
||||
ghostComponent->GhostEntity(id);
|
||||
DestructEntity(entity, player->GetSystemAddress());
|
||||
entity->SetObservers(entity->GetObservers() - 1);
|
||||
} else if (!observed && m_GhostDistanceMinSqaured > distance) {
|
||||
} else if (!observed && nearEnough) {
|
||||
ghostComponent->ObserveEntity(id);
|
||||
ConstructEntity(entity, player->GetSystemAddress());
|
||||
entity->SetObservers(entity->GetObservers() + 1);
|
||||
|
||||
@@ -62,6 +62,7 @@ public:
|
||||
void CheckGhosting(Entity* entity);
|
||||
Entity* GetGhostCandidate(LWOOBJID id) const;
|
||||
bool GetGhostingEnabled() const;
|
||||
bool GetSceneGhosting() const { return m_SceneGhosting; }
|
||||
|
||||
void ScheduleForKill(Entity* entity);
|
||||
|
||||
@@ -109,6 +110,9 @@ private:
|
||||
float m_GhostDistanceMinSqaured = 100 * 100;
|
||||
float m_GhostDistanceMaxSquared = 150 * 150;
|
||||
bool m_GhostingEnabled = true;
|
||||
// ghosting_scenes: players get the objects of the scenes their client keeps loaded (ZoneScenes) instead of the
|
||||
// ones within the ghosting distances
|
||||
bool m_SceneGhosting = false;
|
||||
|
||||
std::stack<uint16_t> m_LostNetworkIds;
|
||||
|
||||
|
||||
@@ -37,4 +37,6 @@ struct EntityInfo {
|
||||
LwoNameValue settings;
|
||||
LwoNameValue networkSettings;
|
||||
float scale;
|
||||
// The zone scene (its scene id) the object was placed in, for scene ghosting; -1: none (spawned at run time)
|
||||
int32_t scene = -1;
|
||||
};
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
#include "dConfig.h"
|
||||
#include <ranges>
|
||||
|
||||
Level::Level(Zone* parentZone, const std::string& filepath) {
|
||||
Level::Level(Zone* parentZone, const std::string& filepath, int32_t sceneID) {
|
||||
m_ParentZone = parentZone;
|
||||
m_SceneID = sceneID;
|
||||
|
||||
auto stream = Game::assetManager->GetFile(filepath.c_str());
|
||||
|
||||
@@ -40,8 +41,9 @@ Level::Level(Zone* parentZone, const std::string& filepath) {
|
||||
LoadSceneObjects(levelFile.objects);
|
||||
}
|
||||
|
||||
void Level::MakeSpawner(const SceneObject& obj) {
|
||||
void Level::MakeSpawner(const SceneObject& obj, int32_t sceneID) {
|
||||
SpawnerInfo spawnInfo = SpawnerInfo();
|
||||
spawnInfo.scene = sceneID;
|
||||
SpawnerNode* node = new SpawnerNode();
|
||||
spawnInfo.templateID = obj.lot;
|
||||
spawnInfo.spawnerID = obj.id;
|
||||
@@ -162,7 +164,7 @@ void Level::LoadSceneObjects(const std::vector<SceneObject>& objects) {
|
||||
}
|
||||
|
||||
if (obj.lot == 176) { //Spawner
|
||||
MakeSpawner(obj);
|
||||
MakeSpawner(obj, m_SceneID);
|
||||
} else { //Regular object
|
||||
EntityInfo info;
|
||||
info.spawnerID = 0;
|
||||
@@ -172,6 +174,7 @@ void Level::LoadSceneObjects(const std::vector<SceneObject>& objects) {
|
||||
info.rot = obj.rotation;
|
||||
info.settings = obj.settings;
|
||||
info.scale = obj.scale;
|
||||
info.scene = m_SceneID;
|
||||
Game::entityManager->CreateEntity(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,15 @@ public:
|
||||
};
|
||||
|
||||
public:
|
||||
Level(Zone* parentZone, const std::string& filepath);
|
||||
|
||||
static void MakeSpawner(const SceneObject& obj);
|
||||
// sceneID: the scene id of the zone scene the file is (EntityInfo::scene of its objects)
|
||||
Level(Zone* parentZone, const std::string& filepath, int32_t sceneID = -1);
|
||||
|
||||
static void MakeSpawner(const SceneObject& obj, int32_t sceneID = -1);
|
||||
|
||||
std::map<uint32_t, Header> m_ChunkHeaders;
|
||||
private:
|
||||
Zone* m_ParentZone;
|
||||
int32_t m_SceneID = -1;
|
||||
|
||||
//private functions:
|
||||
void LoadSceneObjects(const std::vector<SceneObject>& objects);
|
||||
|
||||
@@ -24,6 +24,7 @@ Spawner::Spawner(const SpawnerInfo& info) {
|
||||
|
||||
m_EntityInfo.lot = m_Info.templateID;
|
||||
m_EntityInfo.scale = m_Info.templateScale;
|
||||
m_EntityInfo.scene = m_Info.scene;
|
||||
|
||||
m_Start = m_Info.noTimedSpawn;
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ struct SpawnerInfo {
|
||||
|
||||
bool emulated = false;
|
||||
LWOOBJID emulator = LWOOBJID_EMPTY;
|
||||
int32_t scene = -1; // the zone scene the spawner was placed in, passed to what it spawns (EntityInfo::scene)
|
||||
};
|
||||
|
||||
class Spawner {
|
||||
|
||||
@@ -66,6 +66,7 @@ void Zone::LoadZoneIntoMemory() {
|
||||
m_SpawnpointRotation = zoneFile.spawnpointRotation;
|
||||
|
||||
m_SceneCount = zoneFile.scenes.size();
|
||||
m_SceneGraph = ZoneScenes::SceneGraph(zoneFile.scenes, zoneFile.sceneTransitions);
|
||||
for (auto& scene : zoneFile.scenes) {
|
||||
LoadScene(std::move(scene));
|
||||
}
|
||||
@@ -198,7 +199,7 @@ uint32_t Zone::CalculateChecksum() const {
|
||||
void Zone::LoadLevelsIntoMemory() {
|
||||
for (auto& [sceneID, scene] : m_Scenes) {
|
||||
if (scene.level) continue;
|
||||
scene.level = std::make_unique<Level>(this, m_ZonePath + scene.filename);
|
||||
scene.level = std::make_unique<Level>(this, m_ZonePath + scene.filename, sceneID.GetSceneID());
|
||||
|
||||
if (scene.level->m_ChunkHeaders.empty()) continue;
|
||||
|
||||
@@ -207,6 +208,22 @@ void Zone::LoadLevelsIntoMemory() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Zone::LoadSceneMap() {
|
||||
if (m_ZoneRawPath.empty()) return false;
|
||||
auto file = Game::assetManager->GetFile((m_ZonePath + m_ZoneRawPath).c_str());
|
||||
if (!file) {
|
||||
LOG("Could not open the terrain file %s for its scene map", (m_ZonePath + m_ZoneRawPath).c_str());
|
||||
return false;
|
||||
}
|
||||
Raw::Raw raw;
|
||||
if (!Raw::ReadRaw(file, raw)) {
|
||||
LOG("Could not read the terrain file %s for its scene map", (m_ZonePath + m_ZoneRawPath).c_str());
|
||||
return false;
|
||||
}
|
||||
m_SceneMap = ZoneScenes::SceneMap(raw);
|
||||
return !m_SceneMap.Empty();
|
||||
}
|
||||
|
||||
void Zone::AddRevision(LWOSCENEID sceneID, uint32_t revision) {
|
||||
if (m_MapRevisions.find(sceneID) == m_MapRevisions.end()) {
|
||||
m_MapRevisions.insert(std::make_pair(sceneID, revision));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "dZMCommon.h"
|
||||
#include "LDFFormat.h"
|
||||
#include "ZoneFile.h"
|
||||
#include "ZoneScenes.h"
|
||||
#include "tinyxml2.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -48,6 +49,12 @@ public:
|
||||
const NiPoint3& GetSpawnPos() const { return m_Spawnpoint; }
|
||||
const NiQuaternion& GetSpawnRot() const { return m_SpawnpointRotation; }
|
||||
|
||||
// Which scenes the client keeps loaded where (ZoneScenes); the scene map is empty unless LoadSceneMap read it
|
||||
const ZoneScenes::SceneGraph& GetSceneGraph() const { return m_SceneGraph; }
|
||||
const ZoneScenes::SceneMap& GetSceneMap() const { return m_SceneMap; }
|
||||
// Reads the terrain file's scene map (for scene ghosting); false when the zone has none
|
||||
bool LoadSceneMap();
|
||||
|
||||
void SetSpawnPos(const NiPoint3& pos) { m_Spawnpoint = pos; }
|
||||
void SetSpawnRot(const NiQuaternion& rot) { m_SpawnpointRotation = rot; }
|
||||
|
||||
@@ -70,6 +77,8 @@ private:
|
||||
|
||||
std::map<LWOSCENEID, SceneRef> m_Scenes;
|
||||
std::vector<SceneTransition> m_SceneTransitions;
|
||||
ZoneScenes::SceneGraph m_SceneGraph;
|
||||
ZoneScenes::SceneMap m_SceneMap;
|
||||
|
||||
uint32_t m_PathDataLength;
|
||||
uint32_t m_PathChunkVersion;
|
||||
|
||||
@@ -108,3 +108,8 @@ save_lxfmls=0
|
||||
|
||||
# 1: saving a brick by brick model uses up the bricks in it, as live did. 0: they go back to the backpack.
|
||||
bbb_consume_bricks=0
|
||||
|
||||
# 1: players get the objects of the scenes their game keeps loaded (the scene under them, the scenes connected to it
|
||||
# by the zone's scene transitions, and the global scene) instead of the objects within the ghosting distances.
|
||||
# Needs the zone's terrain scene map; zones without one keep distance ghosting.
|
||||
ghosting_scenes=0
|
||||
|
||||
@@ -22,6 +22,7 @@ set(DCOMMONTEST_SOURCES
|
||||
"ScheduleRulesTests.cpp"
|
||||
"VanityEventsTests.cpp"
|
||||
"ZoneFileTests.cpp"
|
||||
"ZoneScenesTests.cpp"
|
||||
"LiveOpsRulesTests.cpp"
|
||||
"ProcessAndLimitsTests.cpp"
|
||||
"HotPropertySlotsTests.cpp"
|
||||
|
||||
73
tests/dCommonTests/ZoneScenesTests.cpp
Normal file
73
tests/dCommonTests/ZoneScenesTests.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ZoneScenes.h"
|
||||
|
||||
namespace {
|
||||
// A chunk `cells` scene map cells wide covering (x, z) .. (x + 64, z + 64): 65 height samples, 1 unit apart
|
||||
Raw::Chunk MakeChunk(float x, float z, uint32_t cells, std::vector<uint8_t> scenes) {
|
||||
Raw::Chunk chunk{};
|
||||
chunk.width = chunk.height = 65;
|
||||
chunk.offsetX = x;
|
||||
chunk.offsetZ = z;
|
||||
chunk.scaleFactor = 1.0f;
|
||||
chunk.heightMap.assign(65 * 65, 0.0f);
|
||||
chunk.colorMapResolution = cells;
|
||||
chunk.sceneMap = std::move(scenes);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
SceneTransition Transition(uint32_t a, uint32_t b, uint32_t layer = 0) {
|
||||
SceneTransition transition;
|
||||
transition.points.push_back({ (static_cast<uint64_t>(layer) << 32) | a, {} });
|
||||
transition.points.push_back({ b, {} });
|
||||
return transition;
|
||||
}
|
||||
|
||||
std::vector<ZoneScene> Scenes(std::initializer_list<uint32_t> ids) {
|
||||
std::vector<ZoneScene> scenes;
|
||||
for (const auto id : ids) {
|
||||
ZoneScene scene;
|
||||
scene.id = id;
|
||||
scenes.push_back(scene);
|
||||
}
|
||||
return scenes;
|
||||
}
|
||||
}
|
||||
|
||||
// Cells are x major (index x * resolution + z) and a position takes the nearest cell, as the client's lookup does
|
||||
TEST(ZoneScenesTests, FindsTheSceneUnderAPosition) {
|
||||
Raw::Raw raw{};
|
||||
// 2x2 cells, each 32 units: x = 0 has scenes 1 (z low) and 2, x = 1 has 3 and no scene
|
||||
raw.chunks.push_back(MakeChunk(0, 0, 2, { 1, 2, 3, ZoneScenes::NO_SCENE }));
|
||||
raw.chunks.push_back(MakeChunk(64, 0, 1, { 7 }));
|
||||
const ZoneScenes::SceneMap map(raw);
|
||||
ASSERT_FALSE(map.Empty());
|
||||
EXPECT_EQ(map.SceneAt(1, 1), 1u);
|
||||
EXPECT_EQ(map.SceneAt(1, 17), 2u); // past half a cell: the next cell
|
||||
EXPECT_EQ(map.SceneAt(17, 1), 3u);
|
||||
EXPECT_EQ(map.SceneAt(17, 17), ZoneScenes::GLOBAL_SCENE); // no scene there
|
||||
EXPECT_EQ(map.SceneAt(100, 10), 7u);
|
||||
// Off the terrain: clamped to its edge
|
||||
EXPECT_EQ(map.SceneAt(-50, -50), 1u);
|
||||
EXPECT_EQ(map.SceneAt(500, 10), 7u);
|
||||
EXPECT_EQ(ZoneScenes::SceneMap().SceneAt(1, 1), ZoneScenes::GLOBAL_SCENE);
|
||||
}
|
||||
|
||||
TEST(ZoneScenesTests, LoadsTheConnectedScenes) {
|
||||
// 1-2 and 2-3 connect; 9 isn't in the zone, so its transition is dropped
|
||||
const ZoneScenes::SceneGraph graph(Scenes({ 0, 1, 2, 3, 4 }), { Transition(1, 2), Transition(3, 2, 1), Transition(2, 9), Transition(4, 4) });
|
||||
EXPECT_EQ(graph.Loaded(1), (std::set<uint32_t>{ 0, 1, 2 }));
|
||||
EXPECT_EQ(graph.Loaded(2), (std::set<uint32_t>{ 0, 1, 2, 3 }));
|
||||
EXPECT_EQ(graph.Loaded(4), (std::set<uint32_t>{ 0, 4 }));
|
||||
EXPECT_EQ(graph.Loaded(ZoneScenes::GLOBAL_SCENE), (std::set<uint32_t>{ 0 }));
|
||||
EXPECT_TRUE(graph.Neighbours(9).empty());
|
||||
}
|
||||
|
||||
TEST(ZoneScenesTests, GhostsObjectsByTheirScene) {
|
||||
const std::set<uint32_t> loaded{ 0, 1, 2 };
|
||||
EXPECT_TRUE(ZoneScenes::InLoadedScene(2, 7, loaded)); // placed in a loaded scene, wherever it is now
|
||||
EXPECT_FALSE(ZoneScenes::InLoadedScene(3, 1, loaded)); // placed in one that isn't
|
||||
EXPECT_TRUE(ZoneScenes::InLoadedScene(0, 9, loaded)); // the global scene's objects always
|
||||
EXPECT_TRUE(ZoneScenes::InLoadedScene(-1, 1, loaded)); // spawned at run time: the scene under it
|
||||
EXPECT_FALSE(ZoneScenes::InLoadedScene(-1, 5, loaded));
|
||||
}
|
||||
@@ -14,6 +14,7 @@ set(DGAMETEST_SOURCES
|
||||
"BrickByBrickTests.cpp"
|
||||
"ModularBuildTableTests.cpp"
|
||||
"UgcManifestTests.cpp"
|
||||
"SceneGhostingTests.cpp"
|
||||
)
|
||||
|
||||
add_subdirectory(dComponentsTests)
|
||||
|
||||
27
tests/dGameTests/SceneGhostingTests.cpp
Normal file
27
tests/dGameTests/SceneGhostingTests.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "GameDependencies.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Entity.h"
|
||||
#include "ZoneScenes.h"
|
||||
|
||||
class SceneGhostingTest : public GameDependenciesTest {
|
||||
protected:
|
||||
void SetUp() override { SetUpDependencies(); }
|
||||
void TearDown() override { TearDownDependencies(); }
|
||||
};
|
||||
|
||||
// Objects keep the scene they were placed in, which scene ghosting goes by; objects from no scene go by their position
|
||||
TEST_F(SceneGhostingTest, EntitiesKeepTheirScene) {
|
||||
EntityInfo placed = info;
|
||||
placed.scene = 12;
|
||||
const auto fromScene = std::make_unique<Entity>(20, placed);
|
||||
EXPECT_EQ(fromScene->GetScene(), 12);
|
||||
const auto spawned = std::make_unique<Entity>(21, info);
|
||||
EXPECT_EQ(spawned->GetScene(), -1);
|
||||
|
||||
// A player standing in scene 3, whose client keeps 0, 3 and 5 loaded
|
||||
const std::set<uint32_t> loaded{ 0, 3, 5 };
|
||||
EXPECT_FALSE(ZoneScenes::InLoadedScene(fromScene->GetScene(), 3, loaded)); // placed in scene 12, though it stands in 3
|
||||
EXPECT_TRUE(ZoneScenes::InLoadedScene(spawned->GetScene(), 5, loaded));
|
||||
EXPECT_FALSE(ZoneScenes::InLoadedScene(spawned->GetScene(), 12, loaded));
|
||||
}
|
||||
Reference in New Issue
Block a user