Files
DarkflameServer/dDashboardServer/routes/WorldScene.h
Aaron Kimbrell 2d1ef0e370 feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:33:10 -05:00

151 lines
6.9 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdint>
#include <exception>
#include <sstream>
#include <string>
#include <vector>
#include "GeneralUtils.h"
#include "LevelFile.h"
#include "eReplicaComponentType.h"
/**
* What the 3D world view draws of a zone besides its terrain: every object in its scene files (.lvl), read with LevelFile
* (dCommon) like the world server's Level, sorted into a few kinds by the components their LOT has. Pure (bytes and
* component lists in, objects and kinds out) so it can be unit tested; the route reads the files and the CDClient.
*/
namespace WorldScene {
struct Object {
LWOOBJID id{};
uint32_t lot{};
uint32_t templateLot{}; // what a spawner (LOT 176) spawns (spawntemplate); the object's own LOT otherwise
float x{}, y{}, z{};
float scale{ 1.0f };
float qx{}, qy{}, qz{}, qw{ 1.0f }; // rotation
bool spawner{};
// The client draws no model for it (ObjectLoader2::LoadRenderComponent): renderDisabled or CreateNULLRender is
// set, whatever the value (trigger volumes, markers)
bool renderDisabled{};
// carver_only: only carves the navigation mesh; the client never loads the object at all
// (LWOResMgr2Interface::Run, 0x010610e5 in client 1.10.64), so it's never drawn (trigger volumes like LOT 5651)
bool carverOnly{};
std::string nifName; // nif_name: the model to draw instead of its render component's
bool clientOnly{}; // loadOnClientOnly: the client draws it, the world server never loads it
std::string name; // spawner_name, else respawnname, else empty
};
constexpr uint32_t SPAWNER_LOT = 176;
// Every object in a scene file, as far as it could be read
inline std::vector<Object> ReadObjects(const std::string& lvl) {
std::istringstream stream(lvl);
LevelFile level;
try {
level.Read(stream);
} catch (const std::exception&) {
// A damaged file: keep the objects read before the damage
}
std::vector<Object> objects;
objects.reserve(level.objects.size());
for (const auto& object : level.objects) {
const auto setting = [&object](const std::u16string& key) {
const auto it = object.settings.find(key);
auto value = it == object.settings.end() || !it->second ? std::string{} : it->second->GetValueAsString();
while (!value.empty() && (value.back() == '\r' || value.back() == ' ')) value.pop_back();
return value;
};
Object out{ object.id, static_cast<uint32_t>(object.lot), static_cast<uint32_t>(object.lot),
object.position.x, object.position.y, object.position.z, object.scale,
object.rotation.x, object.rotation.y, object.rotation.z, object.rotation.w };
out.spawner = object.lot == SPAWNER_LOT;
if (out.spawner) out.templateLot = GeneralUtils::TryParse<uint32_t>(setting(u"spawntemplate")).value_or(0);
out.clientOnly = GeneralUtils::TryParse(setting(u"loadOnClientOnly"), false);
out.renderDisabled = object.settings.find(u"renderDisabled") != object.settings.end() || object.settings.find(u"CreateNULLRender") != object.settings.end();
out.carverOnly = GeneralUtils::TryParse(setting(u"carver_only"), false);
out.nifName = setting(u"nif_name");
out.name = setting(u"spawner_name");
if (out.name.empty()) out.name = setting(u"respawnname");
objects.push_back(std::move(out));
}
return objects;
}
enum class eClientDraw : uint8_t {
NO_MODEL, // nothing to draw: a spawner itself, or a model built from parts at run time
HIDDEN, // it has a model, but the client doesn't draw it (volumes, triggers, markers)
DRAWN
};
/**
* Whether the client draws an object's model, given its LOT's Objects.type. As the client's
* ObjectLoader2::LoadRenderComponent (0x01053ca5 in client 1.10.64): nothing for renderDisabled or CreateNULLRender,
* blocking volumes and a few LOTs it leaves out by number (6368 is the 3D ambient sound, which gets an effect
* instead); and objects marked carver_only are never loaded at all (see carverOnly).
*/
inline eClientDraw ClientDraws(const Object& object, const std::string& type) {
static constexpr std::array<uint32_t, 7> NOT_DRAWN{ 5937, 5938, 9741, 9742, 9862, 9863, 6368 };
const auto lot = object.spawner ? object.templateLot : object.lot;
if (lot == SPAWNER_LOT || lot == 0 || type == "PrimitiveModels") return eClientDraw::NO_MODEL;
if (object.carverOnly || object.renderDisabled || type == "BlockingVolume" || std::find(NOT_DRAWN.begin(), NOT_DRAWN.end(), lot) != NOT_DRAWN.end()) return eClientDraw::HIDDEN;
return eClientDraw::DRAWN;
}
/**
* The sky of a scene file: the first file name of its environment chunk's skydome info (the model the client draws
* around the camera, e.g. mesh\env\env_sky_won_ag_property.nif), empty when it has none. The chunk starts with
* the file offsets of its lighting, skydome and editor settings; the skydome info is length-prefixed file names.
*/
inline std::string ReadSkydome(const std::string& lvl) {
std::istringstream stream(lvl);
LevelFile level;
try {
level.Read(stream);
} catch (const std::exception&) {
// The chunk headers read before any damage are enough
}
const auto read32 = [&lvl](size_t offset, uint32_t& value) {
if (offset > lvl.size() || lvl.size() - offset < 4) return false;
std::memcpy(&value, lvl.data() + offset, 4);
return true;
};
for (const auto& [id, header] : level.chunkHeaders) {
if (id != LevelFile::SceneEnviroment) continue;
uint32_t skydome{}, length{};
if (!read32(header.startPosition + 4, skydome) || !read32(skydome, length) || length == 0 || length > 260 || skydome + 4 + length > lvl.size()) return {};
return lvl.substr(skydome + 4, length);
}
return {};
}
/**
* The kinds objects are drawn as, most telling first: an enemy that also offers missions is an enemy. Anything with
* none of these components is "other" (scenery, volumes, markers). Names come from the enum (GameLabels).
*/
constexpr std::array<eReplicaComponentType, 8> KINDS{
eReplicaComponentType::BASE_COMBAT_AI, eReplicaComponentType::VENDOR, eReplicaComponentType::MISSION_OFFER,
eReplicaComponentType::QUICK_BUILD, eReplicaComponentType::ROCKET_LAUNCH, eReplicaComponentType::COLLECTIBLE,
eReplicaComponentType::SCRIPTED_ACTIVITY, eReplicaComponentType::DESTROYABLE
};
// ComponentsRegistry stores the destroyable component as 7 (the enum's BUFF; see eReplicaComponentType)
inline eReplicaComponentType FromRegistry(uint32_t componentType) {
return componentType == 7 ? eReplicaComponentType::DESTROYABLE : static_cast<eReplicaComponentType>(componentType);
}
// Index into KINDS of an object with these ComponentsRegistry component types, or KINDS.size() for "other"
inline size_t Classify(const std::vector<uint32_t>& registryComponents) {
size_t best = KINDS.size();
for (const auto component : registryComponents) {
const auto type = FromRegistry(component);
for (size_t i = 0; i < best; i++) {
if (KINDS[i] == type) { best = i; break; }
}
}
return best;
}
}