mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-16 09:34:08 +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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user