mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-09 06:13:43 +00:00
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>
146 lines
6.6 KiB
C++
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;
|
|
}
|
|
}
|