Files
DarkflameServer/dDashboardServer/routes/WorldScene.h
Aaron Kimbrell 05e6bf18fe fix: give the destroyable and buff components their real component types
eReplicaComponentType had the destroyable component's registry type (7) named
BUFF, the real buff component (98) as BUFF_REAL, and a made-up DESTROYABLE =
1000 that DestroyableComponent was stored under. Now DESTROYABLE = 7 and
BUFF = 98, as in ComponentsRegistry and the client.

Undone with it:
- Destructible stats came from whichever of the "buff" (7), quick build and
  collectible registry ids was set, so the few objects without a type 7 entry
  read a DestructibleComponent row with an unrelated id (the NJ dragon relics
  16482-16485 via their collectible id, 125 quick build LOTs when placed with
  is_smashable). The type 7 entry is used now; objects without one keep the
  defaults (is_smashable objects: 1 health, smashable, factions -1 and 6;
  collectibles: an empty destroyable). The client does the same
  (LWODestroyableComponent::AllocateComponents / DoObjectComponentLoad).
- DestroyableComponent::Reinitialize, an unused copy of that pick order.
- WriteComponents' destroyableSerialized flags: the components are written from
  a list in the client's order, and where the destroyable goes (its own place
  after the buff, right before a quick build that has no registry entry for it,
  or after the render component) is one function.
- The dashboard's registry 7 -> DESTROYABLE mapping; the destroyable type also
  has a name there now (1000 was outside magic_enum's range).

Component types are not stored or sent as enum numbers anywhere besides the
CDClient's own values, which now match. migrations/cdserver/4 is unrelated (it
restores LOT 12916's registry rows that migration 0 overwrote) and stays.

Verified: dGameTests ReplicaComponentOrderTests serialize players, enemies,
smashables, quick builds and collectibles with and without a registry entry,
NPCs, pets, vehicles, models and an entity with every listed component with
the new code and a frozen copy of the old WriteComponents, and expect the same
bits for construction and serialization (a deliberately wrong destroyable place
fails them). Full ctest passes.

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

146 lines
6.6 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
};
// 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 = static_cast<eReplicaComponentType>(component);
for (size_t i = 0; i < best; i++) {
if (KINDS[i] == type) { best = i; break; }
}
}
return best;
}
}