#pragma once #include #include #include #include #include #include #include #include #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 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 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(object.lot), static_cast(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(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 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 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& registryComponents) { size_t best = KINDS.size(); for (const auto component : registryComponents) { const auto type = static_cast(component); for (size_t i = 0; i < best; i++) { if (KINDS[i] == type) { best = i; break; } } } return best; } }