diff --git a/src/Common/Game/T6/T6_Assets.h b/src/Common/Game/T6/T6_Assets.h index 1a7b9b0e3..d62c378c4 100644 --- a/src/Common/Game/T6/T6_Assets.h +++ b/src/Common/Game/T6/T6_Assets.h @@ -1246,13 +1246,13 @@ namespace T6 struct GfxWorldVertexData0 { - byte128* data; + byte128* data; // GfxPackedWorldVertex void /*ID3D11Buffer*/* vb; }; struct GfxWorldVertexData1 { - byte128* data; + byte128* data; // GfxPackedWorldVertex void /*ID3D11Buffer*/* vb; }; @@ -1278,6 +1278,16 @@ namespace T6 typedef tdef_align32(4) char aligned_byte_pointer; typedef tdef_align32(4) GfxCompressedLightGridCoeffs GfxCompressedLightGridCoeffs_align4; + struct GfxLightGridRow + { + uint16_t colStart; + uint16_t colCount; + uint16_t zStart; + uint16_t zCount; + unsigned int firstEntry; + char lookupTable[1]; // The lookup table has a variable length + }; + struct GfxLightGrid { unsigned int sunPrimaryLightIndex; @@ -1288,7 +1298,7 @@ namespace T6 unsigned int colAxis; uint16_t* rowDataStart; unsigned int rawRowDataSize; - aligned_byte_pointer* rawRowData; + aligned_byte_pointer* rawRowData; // GfxLightGridRow unsigned int entryCount; GfxLightGridEntry* entries; unsigned int colorCount; @@ -3898,6 +3908,17 @@ namespace T6 int baseIndex; }; + enum GfxSurfaceFlags + { + GFX_SURFACE_CASTS_SUN_SHADOW = 0x1, + GFX_SURFACE_HAS_PRIMARY_LIGHT_CONFLICT = 0x2, + GFX_SURFACE_IS_SKY = 0x4, + GFX_SURFACE_NO_DRAW = 0x8, + GFX_SURFACE_CASTS_SHADOW = 0x10, + GFX_SURFACE_QUANTIZED = 0x20, + GFX_SURFACE_NO_COLOR = 0x40 + }; + struct type_align32(16) GfxSurface { srfTriangles_t tris; @@ -3930,6 +3951,12 @@ namespace T6 uint16_t numLmapVertexColors; }; + enum StaticModelFlag + { + STATIC_MODEL_FLAG_NO_SHADOW = 0x1, + STATIC_MODEL_FLAG_LIGHTMAP_VC = 0x2, + }; + struct GfxStaticModelDrawInst { float cullDist; @@ -5728,6 +5755,11 @@ namespace T6 unsigned int packed; }; + union PackedLmapCoords + { + unsigned int packed; + }; + struct type_align(16) GfxPackedVertex { vec3_t xyz; @@ -5738,6 +5770,17 @@ namespace T6 PackedUnitVec tangent; }; + struct GfxPackedWorldVertex + { + vec3_t xyz; + float binormalSign; + GfxColor color; + PackedTexCoords texCoord; + PackedUnitVec normal; + PackedUnitVec tangent; + PackedLmapCoords lmapCoord; + }; + struct XRigidVertList { uint16_t boneOffset; @@ -7053,6 +7096,23 @@ namespace T6 char* stringValue; }; + enum GfxLightType + { + GFX_LIGHT_TYPE_NONE = 0x0, + GFX_LIGHT_TYPE_DIR = 0x1, + GFX_LIGHT_TYPE_SPOT = 0x2, + GFX_LIGHT_TYPE_SPOT_SQUARE = 0x3, + GFX_LIGHT_TYPE_SPOT_ROUND = 0x4, + GFX_LIGHT_TYPE_OMNI = 0x5, + GFX_LIGHT_TYPE_COUNT = 0x6, + GFX_LIGHT_TYPE_DIR_SHADOWMAP = 0x6, + GFX_LIGHT_TYPE_SPOT_SHADOWMAP = 0x7, + GFX_LIGHT_TYPE_SPOT_SQUARE_SHADOWMAP = 0x8, + GFX_LIGHT_TYPE_SPOT_ROUND_SHADOWMAP = 0x9, + GFX_LIGHT_TYPE_OMNI_SHADOWMAP = 0xA, + GFX_LIGHT_TYPE_COUNT_WITH_SHADOWMAP_VERSIONS = 0xB, + }; + #ifndef __zonecodegenerator } // namespace T6 #endif diff --git a/src/Linker/Linker.cpp b/src/Linker/Linker.cpp index b7dce2bd6..4c98307b6 100644 --- a/src/Linker/Linker.cpp +++ b/src/Linker/Linker.cpp @@ -1,5 +1,4 @@ #include "Linker.h" - #include "LinkerArgs.h" #include "LinkerPaths.h" #include "ObjContainer/SoundBank/SoundBankWriter.h" @@ -330,7 +329,7 @@ class LinkerImpl final : public Linker PathGameContext gameContext(paths, projectName, zoneDefinition->m_game); - if (!zoneDefinition->m_assets.empty()) + if (!zoneDefinition->m_assets.empty() || zoneDefinition->m_map_type != ZoneDefinitionMapType::NONE) { if (!BuildFastFile(paths, projectName, targetName, *zoneDefinition)) return false; diff --git a/src/Linker/ZoneCreation/ZoneCreator.cpp b/src/Linker/ZoneCreation/ZoneCreator.cpp index c7cb4d5c3..efcd60c06 100644 --- a/src/Linker/ZoneCreation/ZoneCreator.cpp +++ b/src/Linker/ZoneCreation/ZoneCreator.cpp @@ -72,7 +72,7 @@ namespace zone_creator OutputPathFilesystem cacheDir(context.m_cache_dir); objCompiler->ConfigureCreatorCollection( creatorCollection, *zone, zoneDefinitionContext, *context.m_asset_search_path, lookup, creationContext, outDir, cacheDir); - objLoader->ConfigureCreatorCollection(creatorCollection, *zone, *context.m_asset_search_path, lookup); + objLoader->ConfigureCreatorCollection(creatorCollection, *zone, *context.m_asset_search_path, lookup, *context.m_definition); for (const auto& assetEntry : context.m_definition->m_assets) { @@ -84,7 +84,8 @@ namespace zone_creator ++zoneDefinitionContext.m_asset_index_in_definition; } - creatorCollection.FinalizeZone(creationContext); + if (!creatorCollection.FinalizeZone(creationContext)) + return nullptr; return zone; } diff --git a/src/ObjCommon/XModel/Gltf/JsonGltf.h b/src/ObjCommon/XModel/Gltf/JsonGltf.h index 91315e675..3e48b072a 100644 --- a/src/ObjCommon/XModel/Gltf/JsonGltf.h +++ b/src/ObjCommon/XModel/Gltf/JsonGltf.h @@ -18,6 +18,74 @@ namespace gltf NLOHMANN_DEFINE_TYPE_EXTENSION(JsonAsset, version, generator); + class JsonPunctualSpotLightProperties + { + public: + std::optional innerConeAngle; + std::optional outerConeAngle; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPunctualSpotLightProperties, innerConeAngle, outerConeAngle); + + enum class JsonPunctualLightType + { + DIRECTIONAL, + POINT, + SPOT + }; + + NLOHMANN_JSON_SERIALIZE_ENUM(JsonPunctualLightType, + { + {JsonPunctualLightType::DIRECTIONAL, "directional"}, + {JsonPunctualLightType::POINT, "point" }, + {JsonPunctualLightType::SPOT, "spot" }, + }); + + class JsonPunctualLight + { + public: + std::optional> color; + std::optional intensity; + std::optional name; + std::optional range; + std::optional spot; + JsonPunctualLightType type; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPunctualLight, color, intensity, name, range, spot, type); + + class JsonPunctualLightsExt + { + public: + std::optional> lights; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPunctualLightsExt, lights); + + class JsonExtension + { + public: + std::optional KHR_lights_punctual; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonExtension, KHR_lights_punctual); + + class JsonPunctualLightIndex + { + public: + int light; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPunctualLightIndex, light); + + class JsonNodeExtension + { + public: + std::optional KHR_lights_punctual; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonNodeExtension, KHR_lights_punctual); + class JsonNode { public: @@ -29,9 +97,12 @@ namespace gltf std::optional> children; std::optional skin; std::optional mesh; + + std::optional extensions; + std::optional extras; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonNode, name, translation, rotation, scale, matrix, children, skin, mesh); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonNode, name, translation, rotation, scale, matrix, children, skin, mesh, extensions, extras); class JsonBuffer { @@ -207,10 +278,11 @@ namespace gltf { public: std::optional baseColorTexture; + std::optional> baseColorFactor; std::optional metallicFactor; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPbrMetallicRoughness, baseColorTexture, metallicFactor); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonPbrMetallicRoughness, baseColorTexture, baseColorFactor, metallicFactor); class JsonNormalTextureInfo { @@ -220,6 +292,14 @@ namespace gltf NLOHMANN_DEFINE_TYPE_EXTENSION(JsonNormalTextureInfo, index); + class JsonMaterialExtras + { + public: + std::optional type; + }; + + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMaterialExtras, type); + class JsonMaterial { public: @@ -227,9 +307,11 @@ namespace gltf std::optional pbrMetallicRoughness; std::optional normalTexture; std::optional doubleSided; + + std::optional extras; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMaterial, name, pbrMetallicRoughness, normalTexture, doubleSided); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMaterial, name, pbrMetallicRoughness, normalTexture, doubleSided, extras); enum class JsonMeshPrimitivesMode { @@ -318,9 +400,12 @@ namespace gltf { public: std::optional uri; + + std::optional bufferView; + std::optional mimeType; }; - NLOHMANN_DEFINE_TYPE_EXTENSION(JsonImage, uri); + NLOHMANN_DEFINE_TYPE_EXTENSION(JsonImage, uri, bufferView, mimeType); class JsonRoot { @@ -338,8 +423,9 @@ namespace gltf std::optional scene; std::optional> scenes; std::optional> textures; + std::optional extensions; }; NLOHMANN_DEFINE_TYPE_EXTENSION( - JsonRoot, accessors, animations, asset, buffers, bufferViews, images, materials, meshes, nodes, skins, scene, scenes, textures); + JsonRoot, accessors, animations, asset, buffers, bufferViews, images, materials, meshes, nodes, skins, scene, scenes, textures, extensions); } // namespace gltf diff --git a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp index 35724a687..3d52dab79 100644 --- a/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp +++ b/src/ObjCompiling/Game/T6/KeyValuePairs/KeyValuePairsCompilerT6.cpp @@ -32,12 +32,12 @@ namespace return AssetCreationResult::NoAction(); } - void FinalizeZone(AssetCreationContext& context) override + bool FinalizeZone(AssetCreationContext& context) override { m_kvp_creator.Finalize(m_zone_definition); const auto commonKvps = m_kvp_creator.GetFinalKeyValuePairs(); if (commonKvps.empty()) - return; + return true; auto* gameKvps = m_memory.Alloc(); gameKvps->name = m_memory.Dup(m_zone.m_name.c_str()); @@ -61,6 +61,8 @@ namespace } context.AddAsset(AssetRegistration(m_zone.m_name, gameKvps)); + + return true; } private: diff --git a/src/ObjLoading.lua b/src/ObjLoading.lua index 9325ca884..d9994a1f0 100644 --- a/src/ObjLoading.lua +++ b/src/ObjLoading.lua @@ -64,4 +64,4 @@ function ObjLoading:project() eigen:include(includes) json:include(includes) libtomcrypt:include(includes) -end +end \ No newline at end of file diff --git a/src/ObjLoading/Asset/AssetCreationContext.cpp b/src/ObjLoading/Asset/AssetCreationContext.cpp index 7624716c8..4fb6b778c 100644 --- a/src/ObjLoading/Asset/AssetCreationContext.cpp +++ b/src/ObjLoading/Asset/AssetCreationContext.cpp @@ -120,6 +120,9 @@ XAssetInfoGeneric* AssetCreationContext::LoadDependencyGeneric(const asset_type_ return LoadDefaultAssetDependency(assetType, std::format(",{}", assetName)); } + if (assetName.starts_with(',')) + return LoadDefaultAssetDependency(assetType, assetName); + if (m_ignored_asset_lookup->IsAssetIgnored(assetType, assetName)) return LoadDefaultAssetDependency(assetType, std::format(",{}", assetName)); diff --git a/src/ObjLoading/Asset/AssetCreatorCollection.cpp b/src/ObjLoading/Asset/AssetCreatorCollection.cpp index ee237293f..f6eb27547 100644 --- a/src/ObjLoading/Asset/AssetCreatorCollection.cpp +++ b/src/ObjLoading/Asset/AssetCreatorCollection.cpp @@ -79,10 +79,15 @@ AssetCreationResult AssetCreatorCollection::CreateDefaultAsset(const asset_type_ return AssetCreationResult::NoAction(); } -void AssetCreatorCollection::FinalizeZone(AssetCreationContext& context) const +bool AssetCreatorCollection::FinalizeZone(AssetCreationContext& context) const { for (const auto& creator : m_asset_creators) - creator->FinalizeZone(context); + { + if (!creator->FinalizeZone(context)) + return false; + } for (const auto& postProcessor : m_asset_post_processors) postProcessor->FinalizeZone(context); + + return true; } diff --git a/src/ObjLoading/Asset/AssetCreatorCollection.h b/src/ObjLoading/Asset/AssetCreatorCollection.h index 9b720b118..d11a237bf 100644 --- a/src/ObjLoading/Asset/AssetCreatorCollection.h +++ b/src/ObjLoading/Asset/AssetCreatorCollection.h @@ -27,7 +27,7 @@ class AssetCreatorCollection AssetCreationResult CreateAsset(asset_type_t assetType, const std::string& assetName, AssetCreationContext& context) const; AssetCreationResult CreateDefaultAsset(asset_type_t assetType, const std::string& assetName, AssetCreationContext& context) const; - void FinalizeZone(AssetCreationContext& context) const; + bool FinalizeZone(AssetCreationContext& context) const; private: std::vector> m_asset_creators_by_type; diff --git a/src/ObjLoading/Asset/IAssetCreator.h b/src/ObjLoading/Asset/IAssetCreator.h index 42dfeed0c..f44329c0d 100644 --- a/src/ObjLoading/Asset/IAssetCreator.h +++ b/src/ObjLoading/Asset/IAssetCreator.h @@ -26,7 +26,10 @@ class IAssetCreator [[nodiscard]] virtual std::optional GetHandlingAssetType() const = 0; virtual AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) = 0; - virtual void FinalizeZone(AssetCreationContext& context) {} + virtual bool FinalizeZone(AssetCreationContext& context) + { + return true; + }; }; template class AssetCreator : public IAssetCreator diff --git a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp index fb2c16275..1f346d8df 100644 --- a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp +++ b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp @@ -118,7 +118,8 @@ namespace } } // namespace -void ObjLoader::ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const +void ObjLoader::ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const { ConfigureDefaultCreators(collection, zone); ConfigureLoaders(collection, zone, searchPath); diff --git a/src/ObjLoading/Game/IW3/ObjLoaderIW3.h b/src/ObjLoading/Game/IW3/ObjLoaderIW3.h index a11c9d518..b2cde58c1 100644 --- a/src/ObjLoading/Game/IW3/ObjLoaderIW3.h +++ b/src/ObjLoading/Game/IW3/ObjLoaderIW3.h @@ -11,6 +11,7 @@ namespace IW3 void LoadReferencedContainersForZone(ISearchPath& searchPath, Zone& zone) const override; void UnloadContainersOfZone(Zone& zone) const override; - void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const override; + void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const override; }; } // namespace IW3 diff --git a/src/ObjLoading/Game/IW4/Menu/LoaderMenuListIW4.cpp b/src/ObjLoading/Game/IW4/Menu/LoaderMenuListIW4.cpp index 98ede01af..bd40d0d29 100644 --- a/src/ObjLoading/Game/IW4/Menu/LoaderMenuListIW4.cpp +++ b/src/ObjLoading/Game/IW4/Menu/LoaderMenuListIW4.cpp @@ -73,9 +73,11 @@ namespace return AssetCreationResult::Success(context.AddAsset(std::move(registration))); } - void FinalizeZone(AssetCreationContext& context) override + bool FinalizeZone(AssetCreationContext& context) override { context.GetZoneAssetCreationState().FinalizeSupportingData(); + + return true; } private: diff --git a/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp b/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp index ad5b83c4e..ab915e6ef 100644 --- a/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp +++ b/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp @@ -159,7 +159,8 @@ namespace } } // namespace -void ObjLoader::ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const +void ObjLoader::ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const { ConfigureDefaultCreators(collection, zone); ConfigureLoaders(collection, zone, searchPath, gdt); diff --git a/src/ObjLoading/Game/IW4/ObjLoaderIW4.h b/src/ObjLoading/Game/IW4/ObjLoaderIW4.h index 4d080914e..7f9b5cad3 100644 --- a/src/ObjLoading/Game/IW4/ObjLoaderIW4.h +++ b/src/ObjLoading/Game/IW4/ObjLoaderIW4.h @@ -12,6 +12,7 @@ namespace IW4 void LoadReferencedContainersForZone(ISearchPath& searchPath, Zone& zone) const override; void UnloadContainersOfZone(Zone& zone) const override; - void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const override; + void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const override; }; } // namespace IW4 diff --git a/src/ObjLoading/Game/IW5/Menu/LoaderMenuListIW5.cpp b/src/ObjLoading/Game/IW5/Menu/LoaderMenuListIW5.cpp index b16d7fcf6..5f4297b6c 100644 --- a/src/ObjLoading/Game/IW5/Menu/LoaderMenuListIW5.cpp +++ b/src/ObjLoading/Game/IW5/Menu/LoaderMenuListIW5.cpp @@ -73,9 +73,11 @@ namespace return AssetCreationResult::Success(context.AddAsset(std::move(registration))); } - void FinalizeZone(AssetCreationContext& context) override + bool FinalizeZone(AssetCreationContext& context) override { context.GetZoneAssetCreationState().FinalizeSupportingData(); + + return true; } private: diff --git a/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp b/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp index cb3d96cd8..c90ec6a3f 100644 --- a/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp +++ b/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp @@ -165,7 +165,8 @@ namespace } } // namespace -void ObjLoader::ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const +void ObjLoader::ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const { ConfigureDefaultCreators(collection, zone); ConfigureLoaders(collection, zone, searchPath, gdt); diff --git a/src/ObjLoading/Game/IW5/ObjLoaderIW5.h b/src/ObjLoading/Game/IW5/ObjLoaderIW5.h index 2b882ecb1..3337c4bc4 100644 --- a/src/ObjLoading/Game/IW5/ObjLoaderIW5.h +++ b/src/ObjLoading/Game/IW5/ObjLoaderIW5.h @@ -11,6 +11,7 @@ namespace IW5 void LoadReferencedContainersForZone(ISearchPath& searchPath, Zone& zone) const override; void UnloadContainersOfZone(Zone& zone) const override; - void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const override; + void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const override; }; } // namespace IW5 diff --git a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp index cc67cc5a5..a10683755 100644 --- a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp +++ b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp @@ -135,7 +135,8 @@ namespace } } // namespace -void ObjLoader::ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const +void ObjLoader::ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const { ConfigureDefaultCreators(collection, zone); ConfigureLoaders(collection, zone, searchPath); diff --git a/src/ObjLoading/Game/T5/ObjLoaderT5.h b/src/ObjLoading/Game/T5/ObjLoaderT5.h index 99fc99c83..3da8ab8d5 100644 --- a/src/ObjLoading/Game/T5/ObjLoaderT5.h +++ b/src/ObjLoading/Game/T5/ObjLoaderT5.h @@ -11,6 +11,7 @@ namespace T5 void LoadReferencedContainersForZone(ISearchPath& searchPath, Zone& zone) const override; void UnloadContainersOfZone(Zone& zone) const override; - void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const override; + void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const override; }; } // namespace T5 diff --git a/src/ObjLoading/Game/T6/BSP/BSP.h b/src/ObjLoading/Game/T6/BSP/BSP.h new file mode 100644 index 000000000..385ce73eb --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSP.h @@ -0,0 +1,189 @@ +#pragma once + +#include "BSPFlags.h" +#include "Game/T6/T6.h" +#include "Utils/Logging/Log.h" + +#include +#include +#include +#include +using namespace T6; + +namespace BSP +{ + enum BSPMaterialType + { + MATERIAL_TYPE_COLOUR, + MATERIAL_TYPE_TEXTURE + }; + + struct BSPVertex + { + vec3_t pos; + vec4_t color; + vec2_t texCoord; + vec3_t normal; + vec3_t tangent; + vec3_t binormal; + }; + + struct BSPMaterial + { + std::string materialName; + BSPMaterialType materialType; + vec4_t materialColour; + + int surfaceFlags; + int contentFlags; + }; + + struct BSPSurface + { + size_t materialIndex; + uint16_t vertexCount; + uint16_t triCount; + int indexOfFirstVertex; + int indexOfFirstIndex; + }; + + struct BSPXModel + { + std::string name; + + vec3_t origin; + vec4_t rotationQuaternion; + float scale; + + bool areBoundsValid; + vec3_t mins; + vec3_t maxs; + + bool doesCastShadow; + }; + + struct BSPBoxBrush + { + size_t vertexIndex; + size_t vertexCount; + + int surfaceFlags; + int contentFlags; + }; + + struct BSPWorld + { + std::vector staticSurfaces; + std::vector scriptSurfaces; + + std::vector vertices; + std::vector indices; + std::vector materials; + std::vector xmodels; + + std::vector scriptBoxBrushes; + std::vector boxBrushVerts; + }; + + enum BSPLightType + { + LIGHT_TYPE_DIRECTIONAL, + LIGHT_TYPE_SPOT + }; + + struct BSPLight + { + BSPLightType type; + vec3_t colour; + float range; + float intensity; + + vec3_t pos; + vec3_t direction; + + // angle is in radians. only used on spot lights + float innerConeAngle; + float outerConeAngle; + }; + + struct BSPSpawnPoint + { + vec3_t origin; + vec3_t forward; + std::string spawnpointGroupName; + }; + + struct BSPZoneZM + { + vec3_t origin; + std::string zoneName; + std::string spawnerGroupName; + std::string spawnpointGroupName; + size_t modelIndex; + }; + + struct BSPZSpawnerZM + { + std::string spawnerGroupName; + vec3_t origin; + vec3_t forward; + }; + + struct BSPModel + { + bool isGfxModel; + + size_t surfaceIndex; + size_t surfaceCount; + + bool hasBrush; + size_t brushIndex; + }; + + struct BSPEntityEntry + { + std::string key; + std::string value; + }; + + struct BSPEntity + { + vec3_t origin; + vec4_t rotationQuaternion; + size_t modelIndex; + + std::vector entries; + }; + + struct BSPData + { + std::string name; + std::string bspName; + bool isZombiesMap; + + BSPWorld gfxWorld; + BSPWorld colWorld; + + bool hasSunlightBeenSet; + BSPLight sunlight; + std::vector lights; + + std::vector spawnpoints; + std::vector zm_zones; + std::vector zm_spawners; + + bool containsWorldspawn; + bool containsIntermssion; + std::vector entities; + BSPEntity worldspawn; + + std::vector models; + }; + + enum BSPDefaultLights + { + EMPTY_LIGHT_INDEX = 0, + SUN_LIGHT_INDEX = 1, + BSP_DEFAULT_LIGHT_COUNT = 2 + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPCalculation.cpp b/src/ObjLoading/Game/T6/BSP/BSPCalculation.cpp new file mode 100644 index 000000000..c80920a23 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPCalculation.cpp @@ -0,0 +1,160 @@ +#include "BSPCalculation.h" + +namespace BSP +{ + constexpr int MAX_NODE_SIZE = 512; // maximum size a BSP node can be before it becomes a leaf + + BSPObject::BSPObject(float xMin, float yMin, float zMin, float xMax, float yMax, float zMax, int objPartitionIndex) + { + min.x = xMin; + min.y = yMin; + min.z = zMin; + max.x = xMax; + max.y = yMax; + max.z = zMax; + partitionIndex = objPartitionIndex; + } + + void BSPLeaf::addObject(std::shared_ptr object) + { + objectList.emplace_back(std::move(object)); + } + + BSPObject* BSPLeaf::getObject(size_t index) + { + return objectList.at(index).get(); + } + + size_t BSPLeaf::getObjectCount() + { + return objectList.size(); + } + + BSPNode::BSPNode(std::unique_ptr frontTree, std::unique_ptr backTree, PlaneAxis nodeAxis, float nodeDistance) + { + front = std::move(frontTree); + back = std::move(backTree); + axis = nodeAxis; + distance = nodeDistance; + } + + PlaneSide BSPNode::objectIsInFront(BSPObject* object) + { + float minCoord, maxCoord; + + // Select the relevant coordinate based on the plane's axis + if (axis == AXIS_X) + { + minCoord = object->min.x; + maxCoord = object->max.x; + } + else if (axis == AXIS_Y) + { + minCoord = object->min.y; + maxCoord = object->max.y; + } + else // axis == AXIS_Z + { + minCoord = object->min.z; + maxCoord = object->max.z; + } + + // Compare with the plane's distance + if (maxCoord < distance) + { + return SIDE_BACK; // Object is entirely on the negative side + } + else if (minCoord > distance) + { + return SIDE_FRONT; // Object is entirely on the positive side + } + else + { + return SIDE_INTERSECTS; + } + } + + BSPTree::BSPTree(float xMin, float yMin, float zMin, float xMax, float yMax, float zMax, int treeLevel) + { + min.x = xMin; + min.y = yMin; + min.z = zMin; + max.x = xMax; + max.y = yMax; + max.z = zMax; + level = treeLevel; + splitTree(); + } + + void BSPTree::splitTree() + { + std::unique_ptr front; + std::unique_ptr back; + float halfLength; + + if (max.x - min.x > MAX_NODE_SIZE) + { + // split along the x axis + halfLength = (min.x + max.x) * 0.5f; + front = std::make_unique(halfLength, min.y, min.z, max.x, max.y, max.z, level + 1); + back = std::make_unique(min.x, min.y, min.z, halfLength, max.y, max.z, level + 1); + + isLeaf = false; + node = std::make_unique(std::move(front), std::move(back), AXIS_X, halfLength); + leaf = nullptr; + } + else if (max.y - min.y > MAX_NODE_SIZE) + { + // split along the x axis + halfLength = (min.y + max.y) * 0.5f; + front = std::make_unique(min.x, halfLength, min.z, max.x, max.y, max.z, level + 1); + back = std::make_unique(min.x, min.y, min.z, max.x, halfLength, max.z, level + 1); + + isLeaf = false; + node = std::make_unique(std::move(front), std::move(back), AXIS_Y, halfLength); + leaf = nullptr; + } + else if (max.z - min.z > MAX_NODE_SIZE) + { + // split along the z axis + halfLength = (min.z + max.z) * 0.5f; + front = std::make_unique(min.x, min.y, halfLength, max.x, max.y, max.z, level + 1); + back = std::make_unique(min.x, min.y, min.z, max.x, max.y, halfLength, level + 1); + + isLeaf = false; + node = std::make_unique(std::move(front), std::move(back), AXIS_Z, halfLength); + leaf = nullptr; + } + else + { + isLeaf = true; + node = nullptr; + leaf = std::make_unique(); + } + } + + void BSPTree::addObjectToTree(std::shared_ptr object) + { + if (isLeaf) + { + leaf->addObject(std::move(object)); + } + else + { + PlaneSide side = node->objectIsInFront(object.get()); + if (side == SIDE_FRONT) + { + node->front->addObjectToTree(std::move(object)); + } + else if (side == SIDE_BACK) + { + node->back->addObjectToTree(std::move(object)); + } + else // intersects + { + node->front->addObjectToTree(object); + node->back->addObjectToTree(object); + } + } + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPCalculation.h b/src/ObjLoading/Game/T6/BSP/BSPCalculation.h new file mode 100644 index 000000000..1d35cbe99 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPCalculation.h @@ -0,0 +1,71 @@ +#pragma once + +#include "BSP.h" + +namespace BSP +{ + enum PlaneAxis + { + AXIS_X, + AXIS_Y, + AXIS_Z + }; + + enum PlaneSide + { + SIDE_FRONT, + SIDE_BACK, + SIDE_INTERSECTS + }; + + class BSPObject + { + public: + vec3_t min; + vec3_t max; + int partitionIndex; // index of the partition the object is contained in + + BSPObject(float xMin, float yMin, float zMin, float xMax, float yMax, float zMax, int objPartitionIndex); + }; + + class BSPLeaf + { + public: + std::vector> objectList; + + void addObject(std::shared_ptr object); + BSPObject* getObject(size_t index); + size_t getObjectCount(); + }; + + class BSPTree; + + class BSPNode + { + public: + std::unique_ptr front; + std::unique_ptr back; + + PlaneAxis axis; // axis that the split plane is on + float distance; // distance from the origin (0, 0, 0) to the plane + + BSPNode(std::unique_ptr frontTree, std::unique_ptr backTree, PlaneAxis nodeAxis, float nodeDistance); + PlaneSide objectIsInFront(BSPObject* object); + }; + + class BSPTree + { + public: + bool isLeaf; + std::unique_ptr leaf; + std::unique_ptr node; + + int level; // level in the BSP tree + vec3_t min; + vec3_t max; + + BSPTree(float xMin, float yMin, float zMin, float xMax, float yMax, float zMax, int treeLevel); + void splitTree(); + void addObjectToTree(std::shared_ptr object); + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPCreator.cpp b/src/ObjLoading/Game/T6/BSP/BSPCreator.cpp new file mode 100644 index 000000000..e4d10c90f --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPCreator.cpp @@ -0,0 +1,1447 @@ +#include "BSPCreator.h" + +#include "BSPUtil.h" +#include "Utils/StringUtils.h" +#include "XModel/Gltf/GltfBinInput.h" +#include "XModel/Gltf/GltfTextInput.h" +#include "XModel/Gltf/Internal/GltfAccessor.h" +#include "XModel/Gltf/Internal/GltfBuffer.h" +#include "XModel/Gltf/Internal/GltfBufferView.h" +#include "XModel/Gltf/JsonGltf.h" +#include "XModel/Tangentspace.h" + +#pragma warning(push, 0) +#include +#pragma warning(pop) + +#include +#include +#include +#include +#include +#include +#include + +using namespace BSPFlags; + +namespace +{ + struct AccessorsForVertex + { + unsigned m_position_accessor; + unsigned m_normal_accessor; + std::optional m_color_accessor; + std::optional m_uv_accessor; + unsigned m_index_accessor; + }; + + void RhcToLhcQuaternion(float (&coords)[4]) + { + const float two[4]{coords[0], coords[1], coords[2], coords[3]}; + + coords[0] = two[0]; + coords[1] = -two[2]; + coords[2] = two[1]; + coords[3] = two[3]; + } + + void RhcToLhcCoordinates(float (&coords)[3]) + { + const float two[3]{coords[0], coords[1], coords[2]}; + + coords[0] = two[0]; + coords[1] = -two[2]; + coords[2] = two[1]; + } + + void RhcToLhcIndices(unsigned (&indices)[3]) + { + const unsigned two[3]{indices[0], indices[1], indices[2]}; + + indices[0] = two[2]; + indices[1] = two[1]; + indices[2] = two[0]; + } +} // namespace + +namespace +{ + using namespace BSP; + using namespace gltf; + + class GltfLoadException final : std::exception + { + public: + explicit GltfLoadException(std::string message) + : m_message(std::move(message)) + { + } + + [[nodiscard]] const std::string& Str() const + { + return m_message; + } + + [[nodiscard]] const char* what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; + }; + + class BSPLoader + { + private: + BSPData* m_bsp; + BSPWorld* m_curr_bsp_world; + bool m_is_world_gfx; + size_t m_emptyMaterialIndex; + + std::vector> m_accessors; + std::vector> m_buffer_views; + std::vector> m_buffers; + + std::string getWorldTypeName() + { + return m_is_world_gfx ? "gfx" : "col"; + } + + std::optional GetAccessorForIndex(const char* attributeName, + const std::optional index, + std::initializer_list allowedAccessorTypes, + std::initializer_list allowedAccessorComponentTypes) const + { + if (!index) + return std::nullopt; + + if (*index > m_accessors.size()) + throw GltfLoadException(std::format("Index for {} accessor out of bounds", attributeName)); + + auto* accessor = m_accessors[*index].get(); + + const auto maybeType = accessor->GetType(); + if (maybeType) + { + if (std::ranges::find(allowedAccessorTypes, *maybeType) == allowedAccessorTypes.end()) + throw GltfLoadException(std::format("Accessor for {} has unsupported type {}", attributeName, static_cast(*maybeType))); + } + + const auto maybeComponentType = accessor->GetComponentType(); + if (maybeComponentType) + { + if (std::ranges::find(allowedAccessorComponentTypes, *maybeComponentType) == allowedAccessorComponentTypes.end()) + throw GltfLoadException( + std::format("Accessor for {} has unsupported component type {}", attributeName, static_cast(*maybeComponentType))); + } + + return accessor; + } + + static void VerifyAccessorVertexCount(const char* accessorType, const Accessor* accessor, const size_t vertexCount) + { + if (accessor->GetCount() != vertexCount) + throw GltfLoadException(std::format("Element count of {} accessor does not match expected vertex count of {}", accessorType, vertexCount)); + } + + using Transform3f = Eigen::Transform; + + Eigen::Matrix4f createNodeMatrix(const gltf::JsonNode& node) + { + if (node.matrix) + return Eigen::Matrix4f({ + {(*node.matrix)[0], (*node.matrix)[4], (*node.matrix)[8], (*node.matrix)[12]}, + {(*node.matrix)[1], (*node.matrix)[5], (*node.matrix)[9], (*node.matrix)[13]}, + {(*node.matrix)[2], (*node.matrix)[6], (*node.matrix)[10], (*node.matrix)[14]}, + {(*node.matrix)[3], (*node.matrix)[7], (*node.matrix)[11], (*node.matrix)[15]} + }); + + float localTranslation[3]; + float localRotation[4]; + float localScale[3]; + if (node.translation) + { + localTranslation[0] = (*node.translation)[0]; + localTranslation[1] = (*node.translation)[1]; + localTranslation[2] = (*node.translation)[2]; + } + else + { + localTranslation[0] = 0.0f; + localTranslation[1] = 0.0f; + localTranslation[2] = 0.0f; + } + + if (node.rotation) + { + localRotation[0] = (*node.rotation)[0]; + localRotation[1] = (*node.rotation)[1]; + localRotation[2] = (*node.rotation)[2]; + localRotation[3] = (*node.rotation)[3]; + } + else + { + localRotation[0] = 0.0f; + localRotation[1] = 0.0f; + localRotation[2] = 0.0f; + localRotation[3] = 1.0f; + } + + if (node.scale) + { + localScale[0] = (*node.scale)[0]; + localScale[1] = (*node.scale)[1]; + localScale[2] = (*node.scale)[2]; + } + else + { + localScale[0] = 1.0f; + localScale[1] = 1.0f; + localScale[2] = 1.0f; + } + + Eigen::Vector3f translation(localTranslation[0], localTranslation[1], localTranslation[2]); + Eigen::Quaternionf rotation(localRotation[3], localRotation[0], localRotation[1], localRotation[2]); // GLTF is XYZW, Eigen is WXYZ + Eigen::Vector3f scale(localScale[0], localScale[1], localScale[2]); + + Transform3f T; + T = T.fromPositionOrientationScale(translation, rotation, scale); + return T.matrix(); + } + + unsigned CreateSurface(const AccessorsForVertex& accessorsForVertex, + Eigen::Matrix4f& nodeMatrix, + size_t materialIndex, + bool convertWorldToLocalPos, + const gltf::JsonNode& node, + BSPSurface& out_surface) + { + // clang-format off + const auto* positionAccessor = GetAccessorForIndex( + "POSITION", + accessorsForVertex.m_position_accessor, + { JsonAccessorType::VEC3 }, + { JsonAccessorComponentType::FLOAT } + ).value_or(nullptr); + // clang-format on + assert(positionAccessor != nullptr); + + const auto vertexCount = positionAccessor->GetCount(); + NullAccessor nullAccessor(vertexCount); + OnesAccessor onesAccessor(vertexCount); + + // clang-format off + const auto* normalAccessor = GetAccessorForIndex( + "NORMAL", + accessorsForVertex.m_normal_accessor, + { JsonAccessorType::VEC3 }, + { JsonAccessorComponentType::FLOAT } + ).value_or(nullptr); + VerifyAccessorVertexCount("NORMAL", normalAccessor, vertexCount); + assert(normalAccessor != nullptr); + + const auto* uvAccessor = GetAccessorForIndex( + "TEXCOORD_0", + accessorsForVertex.m_uv_accessor, + { JsonAccessorType::VEC2 }, + { JsonAccessorComponentType::FLOAT, JsonAccessorComponentType::UNSIGNED_BYTE, JsonAccessorComponentType::UNSIGNED_SHORT } + ).value_or(&nullAccessor); + VerifyAccessorVertexCount("TEXCOORD_0", uvAccessor, vertexCount); + + const auto* colorAccessor = GetAccessorForIndex( + "COLOR_0", + accessorsForVertex.m_color_accessor, + { JsonAccessorType::VEC3, JsonAccessorType::VEC4 }, + { JsonAccessorComponentType::FLOAT, JsonAccessorComponentType::UNSIGNED_BYTE, JsonAccessorComponentType::UNSIGNED_SHORT } + ).value_or(&onesAccessor); + VerifyAccessorVertexCount("COLOR_0", colorAccessor, vertexCount); + + const auto* indexAccessor = GetAccessorForIndex( + "INDICES", + accessorsForVertex.m_index_accessor, + { JsonAccessorType::SCALAR }, + { JsonAccessorComponentType::UNSIGNED_BYTE, JsonAccessorComponentType::UNSIGNED_SHORT, JsonAccessorComponentType::UNSIGNED_INT } + ).value_or(nullptr); + assert(indexAccessor != nullptr); + // clang-format on + + const auto indexCount = indexAccessor->GetCount(); + if (indexCount % 3 != 0) + throw GltfLoadException("Index count must be dividable by 3 for triangles"); + const auto faceCount = indexCount / 3u; + if (faceCount > UINT16_MAX) + throw GltfLoadException(std::format("Face count ({}) on node {} exceeded the UINT16_MAX", faceCount, node.name.value_or("unnamed node"))); + if (vertexCount > UINT16_MAX) + throw GltfLoadException(std::format("Vertex count ({}) on node {} exceeded the UINT16_MAX", vertexCount, node.name.value_or("unnamed node"))); + + out_surface.vertexCount = static_cast(vertexCount); + out_surface.triCount = static_cast(faceCount); + out_surface.indexOfFirstIndex = static_cast(m_curr_bsp_world->indices.size()); + out_surface.indexOfFirstVertex = static_cast(m_curr_bsp_world->vertices.size()); + out_surface.materialIndex = materialIndex; + vec4_t materialColor = m_curr_bsp_world->materials.at(materialIndex).materialColour; + + Eigen::Vector4f tempPosition(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * tempPosition; + vec3_t surfaceOrigin; + surfaceOrigin.x = transformedPosition.x(); + surfaceOrigin.y = transformedPosition.y(); + surfaceOrigin.z = transformedPosition.z(); + RhcToLhcCoordinates(surfaceOrigin.v); + + for (auto faceIndex = 0u; faceIndex < faceCount; faceIndex++) + { + unsigned indices[3]; + if (!indexAccessor->GetUnsigned(faceIndex * 3u + 0u, indices[0]) || !indexAccessor->GetUnsigned(faceIndex * 3u + 1u, indices[1]) + || !indexAccessor->GetUnsigned(faceIndex * 3u + 2u, indices[2])) + { + assert(false); + } + if (indices[0] > UINT16_MAX || indices[1] > UINT16_MAX || indices[2] > UINT16_MAX) + throw GltfLoadException("Index number exceeded the UINT16_MAX"); + + RhcToLhcIndices(indices); + m_curr_bsp_world->indices.emplace_back(static_cast(indices[0])); + m_curr_bsp_world->indices.emplace_back(static_cast(indices[1])); + m_curr_bsp_world->indices.emplace_back(static_cast(indices[2])); + } + + const auto vertexOffset = static_cast(m_curr_bsp_world->vertices.size()); + m_curr_bsp_world->vertices.reserve(vertexOffset + vertexCount); + for (auto vertexIndex = 0u; vertexIndex < vertexCount; vertexIndex++) + { + BSPVertex vertex; + + if (!positionAccessor->GetFloatVec3(vertexIndex, vertex.pos.v) || !normalAccessor->GetFloatVec3(vertexIndex, vertex.normal.v) + || !colorAccessor->GetFloatVec4(vertexIndex, vertex.color.v) || !uvAccessor->GetFloatVec2(vertexIndex, vertex.texCoord.v)) + { + assert(false); + } + + vertex.color.x *= materialColor.x; + vertex.color.y *= materialColor.y; + vertex.color.z *= materialColor.z; + vertex.color.w *= materialColor.w; + + Eigen::Vector4f position(vertex.pos.x, vertex.pos.y, vertex.pos.z, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vertex.pos.x = transformedPosition.x(); + vertex.pos.y = transformedPosition.y(); + vertex.pos.z = transformedPosition.z(); + + RhcToLhcCoordinates(vertex.pos.v); + RhcToLhcCoordinates(vertex.normal.v); + + if (convertWorldToLocalPos) + { + vertex.pos.x -= surfaceOrigin.x; + vertex.pos.y -= surfaceOrigin.y; + vertex.pos.z -= surfaceOrigin.z; + } + + m_curr_bsp_world->vertices.emplace_back(vertex); + } + + // generate tangent and binormal vectors + tangent_space::VertexData vertexData{ + &m_curr_bsp_world->vertices[out_surface.indexOfFirstVertex].pos, + sizeof(BSPVertex), + &m_curr_bsp_world->vertices[out_surface.indexOfFirstVertex].normal, + sizeof(BSPVertex), + &m_curr_bsp_world->vertices[out_surface.indexOfFirstVertex].texCoord, + sizeof(BSPVertex), + &m_curr_bsp_world->vertices[out_surface.indexOfFirstVertex].tangent, + sizeof(BSPVertex), + &m_curr_bsp_world->vertices[out_surface.indexOfFirstVertex].binormal, + sizeof(BSPVertex), + &m_curr_bsp_world->indices[out_surface.indexOfFirstIndex], + }; + tangent_space::CalculateTangentSpace(vertexData, faceCount, vertexCount); + + return vertexOffset; + } + + bool addLightNode(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + if (!m_is_world_gfx || !jRoot.extensions || !jRoot.extensions->KHR_lights_punctual || !jRoot.extensions->KHR_lights_punctual->lights) + return false; + assert(node.extensions); + assert(node.extensions->KHR_lights_punctual); + + int lightIndex = node.extensions->KHR_lights_punctual->light; + const JsonPunctualLight& jsLight = jRoot.extensions->KHR_lights_punctual->lights->at(lightIndex); + BSPLight light{}; + + light.outerConeAngle = std::numbers::pi_v / 4.0f; /// spec of 45 degrees + light.innerConeAngle = 0.0f; + if (jsLight.type == JsonPunctualLightType::DIRECTIONAL) + { + light.type = LIGHT_TYPE_DIRECTIONAL; + } + else if (jsLight.type == JsonPunctualLightType::POINT) + { + con::warn("Ignoring point light as BO2 does not support point lights."); + return false; + } + else // JsonPunctualLightType::SPOT + { + light.type = LIGHT_TYPE_SPOT; + + assert(jsLight.spot); + if (jsLight.spot->innerConeAngle) + light.innerConeAngle = *jsLight.spot->innerConeAngle; + + if (jsLight.spot->outerConeAngle) + light.outerConeAngle = *jsLight.spot->outerConeAngle; + } + + if (!jsLight.color) + { + light.colour.x = 1.0f; + light.colour.y = 1.0f; + light.colour.z = 1.0f; + } + else + { + light.colour.x = (*jsLight.color)[0]; + light.colour.y = (*jsLight.color)[1]; + light.colour.z = (*jsLight.color)[2]; + } + + if (!jsLight.intensity) + light.intensity = 1000.0f; // adjusted from spec to better match BO2 + else + light.intensity = *jsLight.intensity; + + if (!jsLight.range) + light.range = 1000.0f; // adjusted from spec to better match BO2 + else + light.range = *jsLight.range; + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + light.pos = vec3_t{transformedPosition.x(), transformedPosition.y(), transformedPosition.z()}; + RhcToLhcCoordinates(light.pos.v); + + Eigen::Vector3f defaultDirection(0.0f, 0.0f, 1.0f); // lights use this value for the forward angle instead in BO2 + Eigen::Affine3f affineTransform(nodeMatrix); + Eigen::Matrix3f rotationMatrix = affineTransform.rotation(); + Eigen::Vector3f outputDirection = rotationMatrix * defaultDirection; + outputDirection.normalize(); + light.direction.x = outputDirection.x(); + light.direction.y = outputDirection.y(); + light.direction.z = outputDirection.z(); + RhcToLhcCoordinates(light.direction.v); + + if (jsLight.type == JsonPunctualLightType::DIRECTIONAL) + { + if (m_bsp->hasSunlightBeenSet) + con::warn("WARNING: multiple sunlight nodes found, only one will be used as the sun."); + m_bsp->sunlight = light; + m_bsp->hasSunlightBeenSet = true; + } + else + m_bsp->lights.emplace_back(light); + + return true; + } + + bool getSurfaceAndContentFlags(const std::string& flagStr, int& surfaceFlags, int& contentFlags) + { + bool matchedAnyFlag = false; + std::vector flagStrVec = utils::StringSplit(flagStr, ','); + for (std::string& flag : flagStrVec) + { + utils::MakeStringLowerCase(flag); + utils::StringTrim(flag); + for (size_t typeIdx = 0; typeIdx < BSPFlags::SURF_TYPE_COUNT; typeIdx++) + { + if (!flag.compare(BSPFlags::surfaceTypeToNameMap[typeIdx])) + { + BSPFlags::s_SurfaceTypeFlags flags = BSPFlags::surfaceTypeToFlagMap[typeIdx]; + surfaceFlags |= flags.surfaceFlags; + contentFlags |= flags.contentFlags; + matchedAnyFlag = true; + break; + } + } + } + return matchedAnyFlag; + } + + size_t createMaterialWithFlags(size_t originalMaterialIdx, const std::string& flags) + { + BSPMaterial newMaterial = m_curr_bsp_world->materials.at(originalMaterialIdx); + + bool matchedAnyFlag = false; + std::vector flagStrVec = utils::StringSplit(flags, ','); + for (std::string& flag : flagStrVec) + { + bool foundMatchingName = false; + utils::MakeStringLowerCase(flag); + utils::StringTrim(flag); + size_t typeNameCount = std::extent::value; + for (size_t typeIdx = 0; typeIdx < typeNameCount; typeIdx++) + { + BSPFlags::SurfaceType surfType = BSPFlags::materialFlags[typeIdx]; + if (!flag.compare(BSPFlags::surfaceTypeToNameMap[surfType])) + { + BSPFlags::s_SurfaceTypeFlags flags = BSPFlags::surfaceTypeToFlagMap[surfType]; + newMaterial.surfaceFlags |= flags.surfaceFlags; + newMaterial.contentFlags |= flags.contentFlags; + foundMatchingName = true; + matchedAnyFlag = true; + break; + } + } + if (!foundMatchingName) + con::warn("material {} has invalid flag name: {}", newMaterial.materialName, flag); + } + if (!matchedAnyFlag) + return originalMaterialIdx; + + // the first content flag bit must be set to 1 for the surface to have collision + if ((newMaterial.surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NONSOLID].surfaceFlags) != 0) + newMaterial.contentFlags &= 0xFFFFFFFE; + else + newMaterial.contentFlags |= 1; + + size_t newMaterialIndex = m_curr_bsp_world->materials.size(); + m_curr_bsp_world->materials.emplace_back(newMaterial); + return newMaterialIndex; + } + + bool addMeshNode(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.mesh); + assert(jRoot.meshes); + + const auto& mesh = jRoot.meshes.value()[node.mesh.value()]; + for (const auto& primitive : mesh.primitives) + { + if (!primitive.indices) + throw GltfLoadException("Requires primitives indices"); + if (primitive.mode.value_or(JsonMeshPrimitivesMode::TRIANGLES) != JsonMeshPrimitivesMode::TRIANGLES) + throw GltfLoadException("Only triangles are supported"); + if (!primitive.attributes.POSITION) + throw GltfLoadException("Requires primitives attribute POSITION"); + if (!primitive.attributes.NORMAL) + throw GltfLoadException("Requires primitives attribute NORMAL"); + + const AccessorsForVertex accessorsForVertex{ + .m_position_accessor = *primitive.attributes.POSITION, + .m_normal_accessor = *primitive.attributes.NORMAL, + .m_color_accessor = primitive.attributes.COLOR_0, + .m_uv_accessor = primitive.attributes.TEXCOORD_0, + .m_index_accessor = *primitive.indices, + }; + + size_t materialIndex; + if (primitive.material) + materialIndex = *primitive.material; + else + materialIndex = m_emptyMaterialIndex; + if (node.extras && node.extras->contains("flags")) + materialIndex = createMaterialWithFlags(materialIndex, node.extras->at("flags")); + + BSPSurface surface; + CreateSurface(accessorsForVertex, nodeMatrix, materialIndex, false, node, surface); + m_curr_bsp_world->staticSurfaces.emplace_back(surface); + } + + return true; + } + + void calculateXmodelBounds(BSPXModel& xmodel, std::optional meshIndex, Eigen::Matrix4f& nodeMatrix, const JsonRoot& jRoot) + { + if (meshIndex) + { + xmodel.areBoundsValid = true; + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + xmodel.mins.x = transformedPosition.x(); + xmodel.mins.y = transformedPosition.y(); + xmodel.mins.z = transformedPosition.z(); + xmodel.maxs.x = transformedPosition.x(); + xmodel.maxs.y = transformedPosition.y(); + xmodel.maxs.z = transformedPosition.z(); + RhcToLhcCoordinates(xmodel.mins.v); + RhcToLhcCoordinates(xmodel.maxs.v); + + const auto& mesh = jRoot.meshes.value()[*meshIndex]; + for (size_t primIdx = 0; primIdx < mesh.primitives.size(); primIdx++) + { + const auto& primitive = mesh.primitives.at(primIdx); + + if (!primitive.attributes.POSITION) + throw GltfLoadException("Requires primitives attribute POSITION"); + + // clang-format off + const auto* positionAccessor = GetAccessorForIndex( + "POSITION", + primitive.attributes.POSITION, + { JsonAccessorType::VEC3 }, + { JsonAccessorComponentType::FLOAT } + ).value_or(nullptr); + // clang-format on + assert(positionAccessor != nullptr); + + for (size_t vertexIndex = 0u; vertexIndex < positionAccessor->GetCount(); vertexIndex++) + { + vec3_t vertex; + if (!positionAccessor->GetFloatVec3(vertexIndex, vertex.v)) + assert(false); + + Eigen::Vector4f position(vertex.x, vertex.y, vertex.z, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vertex.x = transformedPosition.x(); + vertex.y = transformedPosition.y(); + vertex.z = transformedPosition.z(); + RhcToLhcCoordinates(vertex.v); + + if (vertexIndex == 0 && primIdx == 0) + { + xmodel.mins = vertex; + xmodel.maxs = vertex; + } + else + BSPUtil::updateAABBWithPoint(vertex, xmodel.mins, xmodel.maxs); + } + } + } + else + { + xmodel.areBoundsValid = false; + xmodel.mins.x = 0.0f; + xmodel.mins.y = 0.0f; + xmodel.mins.z = 0.0f; + xmodel.maxs.x = 0.0f; + xmodel.maxs.y = 0.0f; + xmodel.maxs.z = 0.0f; + } + } + + bool addXModelNode(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.extras); + assert(node.extras->contains("xmodel")); + + BSPXModel xmodel; + xmodel.name = node.extras->at("xmodel"); + + int surfaceFlags = 0; + int contentFlags = 0; + if (node.extras && node.extras->contains("flags")) + getSurfaceAndContentFlags(node.extras->at("flags"), surfaceFlags, contentFlags); + + bool isNoDraw = (surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NODRAW].surfaceFlags) != 0; + bool isNoCastShadow = (surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NOCASTSHADOW].surfaceFlags) != 0; + bool isNonSolid = (surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NONSOLID].surfaceFlags) != 0; + if (m_is_world_gfx && isNoDraw) + return true; + if (!m_is_world_gfx && isNonSolid) + return true; + xmodel.doesCastShadow = !isNoCastShadow; + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + xmodel.origin.x = transformedPosition.x(); + xmodel.origin.y = transformedPosition.y(); + xmodel.origin.z = transformedPosition.z(); + RhcToLhcCoordinates(xmodel.origin.v); + + Eigen::Affine3f affineTransform(nodeMatrix); + Eigen::Quaternionf rotationQuat(affineTransform.rotation()); + rotationQuat.normalize(); + xmodel.rotationQuaternion.x = rotationQuat.x(); + xmodel.rotationQuaternion.y = rotationQuat.y(); + xmodel.rotationQuaternion.z = rotationQuat.z(); + xmodel.rotationQuaternion.w = rotationQuat.w(); + RhcToLhcQuaternion(xmodel.rotationQuaternion.v); + + xmodel.scale = 1.0f; + + calculateXmodelBounds(xmodel, node.mesh, nodeMatrix, jRoot); + + m_curr_bsp_world->xmodels.emplace_back(xmodel); + + return true; + } + + bool addSpawnPointNode(const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.extras); + assert(node.extras->contains("spawnpoint")); + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vec3_t origin; + origin.x = transformedPosition.x(); + origin.y = transformedPosition.y(); + origin.z = transformedPosition.z(); + RhcToLhcCoordinates(origin.v); + + // GLTF default direction is +Y up + Eigen::Vector3f defaultDirection(0.0f, 1.0f, 0.0f); + Eigen::Affine3f affineTransform(nodeMatrix); + Eigen::Matrix3f rotationMatrix = affineTransform.rotation(); + Eigen::Vector3f outputDirection = rotationMatrix * defaultDirection; + outputDirection.normalize(); + vec3_t forward; + forward.x = outputDirection.x(); + forward.y = outputDirection.y(); + forward.z = outputDirection.z(); + RhcToLhcCoordinates(forward.v); + + std::string team = node.extras->at("spawnpoint"); + if (!m_bsp->isZombiesMap && team.compare("attacker") && team.compare("defender") && team.compare("all")) + { + con::warn("Ignoring spawn point with an invalid type (must be attacker, defender or all)"); + return false; + } + + BSPSpawnPoint spawnPoint; + spawnPoint.origin = origin; + spawnPoint.forward = forward; + spawnPoint.spawnpointGroupName = team; + m_bsp->spawnpoints.emplace_back(spawnPoint); + + return true; + } + + size_t addScriptBrushModel(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(!m_is_world_gfx); + + if (!node.mesh || !jRoot.meshes) + throw new GltfLoadException("Script model created with no mesh data"); + + const auto& mesh = jRoot.meshes.value()[node.mesh.value()]; + + if (mesh.primitives.size() == 0) + throw new GltfLoadException("Script model created with no mesh data"); + + Eigen::Vector4f tempPosition(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * tempPosition; + vec3_t origin; + origin.x = transformedPosition.x(); + origin.y = transformedPosition.y(); + origin.z = transformedPosition.z(); + RhcToLhcCoordinates(origin.v); + + BSPModel model; + model.isGfxModel = m_is_world_gfx; + model.surfaceIndex = 0; + model.surfaceCount = 0; + model.hasBrush = true; + model.brushIndex = m_curr_bsp_world->scriptBoxBrushes.size(); + m_bsp->models.emplace_back(model); + + size_t materialIndex = m_emptyMaterialIndex; + if (node.extras && node.extras->contains("flags")) + materialIndex = createMaterialWithFlags(materialIndex, node.extras->at("flags")); + BSPBoxBrush boxBrush; + boxBrush.vertexIndex = m_curr_bsp_world->boxBrushVerts.size(); + boxBrush.contentFlags = m_curr_bsp_world->materials.at(materialIndex).contentFlags; + boxBrush.surfaceFlags = m_curr_bsp_world->materials.at(materialIndex).surfaceFlags; + for (size_t primIdx = 0; primIdx < mesh.primitives.size(); primIdx++) + { + const auto& primitive = mesh.primitives.at(primIdx); + + if (!primitive.attributes.POSITION) + throw GltfLoadException("Requires primitives attribute POSITION"); + + // clang-format off + const auto* positionAccessor = GetAccessorForIndex( + "POSITION", + primitive.attributes.POSITION, + { JsonAccessorType::VEC3 }, + { JsonAccessorComponentType::FLOAT } + ).value_or(nullptr); + // clang-format on + assert(positionAccessor != nullptr); + + if (positionAccessor->GetCount() == 0) + throw GltfLoadException("positionAccessor has count of 0"); + + for (size_t vertexIndex = 0u; vertexIndex < positionAccessor->GetCount(); vertexIndex++) + { + vec3_t vertex; + if (!positionAccessor->GetFloatVec3(vertexIndex, vertex.v)) + assert(false); + + Eigen::Vector4f position(vertex.x, vertex.y, vertex.z, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vertex.x = transformedPosition.x(); + vertex.y = transformedPosition.y(); + vertex.z = transformedPosition.z(); + RhcToLhcCoordinates(vertex.v); + + // brush verts use local position + vertex.x -= origin.x; + vertex.y -= origin.y; + vertex.z -= origin.z; + + m_curr_bsp_world->boxBrushVerts.emplace_back(vertex); + } + } + boxBrush.vertexCount = m_curr_bsp_world->boxBrushVerts.size() - boxBrush.vertexIndex; + m_curr_bsp_world->scriptBoxBrushes.emplace_back(boxBrush); + + return m_bsp->models.size(); // script model index starts at 1 + } + + size_t addScriptTerrainModel(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + if (!node.mesh || !jRoot.meshes) + throw new GltfLoadException("Script model created with no mesh data"); + + const auto& mesh = jRoot.meshes.value()[node.mesh.value()]; + + if (mesh.primitives.size() == 0) + throw new GltfLoadException("Script model created with no mesh data"); + + BSPModel model; + model.isGfxModel = m_is_world_gfx; + model.surfaceIndex = m_curr_bsp_world->scriptSurfaces.size(); + model.surfaceCount = mesh.primitives.size(); + model.hasBrush = false; + model.brushIndex = 0; + m_bsp->models.emplace_back(model); + + for (const auto& primitive : mesh.primitives) + { + if (!primitive.indices) + throw GltfLoadException("Requires primitives indices"); + if (primitive.mode.value_or(JsonMeshPrimitivesMode::TRIANGLES) != JsonMeshPrimitivesMode::TRIANGLES) + throw GltfLoadException("Only triangles are supported"); + if (!primitive.attributes.POSITION) + throw GltfLoadException("Requires primitives attribute POSITION"); + if (!primitive.attributes.NORMAL) + throw GltfLoadException("Requires primitives attribute NORMAL"); + + const AccessorsForVertex accessorsForVertex{ + .m_position_accessor = *primitive.attributes.POSITION, + .m_normal_accessor = *primitive.attributes.NORMAL, + .m_color_accessor = primitive.attributes.COLOR_0, + .m_uv_accessor = primitive.attributes.TEXCOORD_0, + .m_index_accessor = *primitive.indices, + }; + + size_t materialIndex; + if (primitive.material) + { + size_t originalMaterialIdx = *primitive.material; + if (node.extras && node.extras->contains("flags")) + materialIndex = createMaterialWithFlags(originalMaterialIdx, node.extras->at("flags")); + else + materialIndex = originalMaterialIdx; + } + else + materialIndex = m_emptyMaterialIndex; + + BSPSurface surface; + CreateSurface(accessorsForVertex, nodeMatrix, materialIndex, true, node, surface); + m_curr_bsp_world->scriptSurfaces.emplace_back(surface); + } + + return m_bsp->models.size(); // script model index starts at 1 + } + + bool addZoneNode(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.extras); + assert(node.extras->contains("zone")); + + if (!node.extras->contains("spawner_group") || !node.extras->contains("spawnpoint_group")) + { + con::error("ignoring zone: Zone object must have a valid spawner_group and spawnpoint_group property"); + return false; + } + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vec3_t origin; + origin.x = transformedPosition.x(); + origin.y = transformedPosition.y(); + origin.z = transformedPosition.z(); + RhcToLhcCoordinates(origin.v); + + BSPZoneZM zone; + zone.origin = origin; + zone.zoneName = node.extras->at("zone"); + zone.spawnerGroupName = node.extras->at("spawner_group"); + zone.spawnpointGroupName = node.extras->at("spawnpoint_group"); + zone.modelIndex = addScriptBrushModel(jRoot, node, nodeMatrix); + m_bsp->zm_zones.emplace_back(zone); + + return true; + } + + bool addZSpawnerNode(const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.extras); + assert(node.extras->contains("spawner")); + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + vec3_t origin; + origin.x = transformedPosition.x(); + origin.y = transformedPosition.y(); + origin.z = transformedPosition.z(); + RhcToLhcCoordinates(origin.v); + + // GLTF default direction is +Y up + Eigen::Vector3f defaultDirection(0.0f, 1.0f, 0.0f); + Eigen::Affine3f affineTransform(nodeMatrix); + Eigen::Matrix3f rotationMatrix = affineTransform.rotation(); + Eigen::Vector3f outputDirection = rotationMatrix * defaultDirection; + outputDirection.normalize(); + vec3_t forward; + forward.x = outputDirection.x(); + forward.y = outputDirection.y(); + forward.z = outputDirection.z(); + RhcToLhcCoordinates(forward.v); + + BSPZSpawnerZM spawner; + spawner.origin = origin; + spawner.forward = forward; + spawner.spawnerGroupName = node.extras->at("spawner"); + m_bsp->zm_spawners.emplace_back(spawner); + + return true; + } + + bool addClassNode(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + assert(node.extras); + assert(node.extras->contains("classname")); + + BSPEntity entity; + + std::string classname = node.extras->at("classname"); + if (!classname.compare("script_brushmodel_gfx")) + entity.modelIndex = addScriptTerrainModel(jRoot, node, nodeMatrix); + else if (!classname.compare("script_brushmodel") || (classname.starts_with("trigger_") || !classname.compare("info_volume"))) + entity.modelIndex = addScriptBrushModel(jRoot, node, nodeMatrix); + else + entity.modelIndex = 0; + + if (entity.modelIndex != 0 && node.extras->contains("model")) + { + con::error("Node {} cannot have a model property when its class is a trigger, info_volume, script_brushmodel_gfx or script_brushmodel"); + return false; + } + + for (auto& element : node.extras->items()) + { + std::string key = element.key(); + std::string value = element.value(); + if (!key.compare("origin") || !key.compare("angles") || !key.compare("flags")) + continue; + + if (!key.compare("classname") && !value.compare("script_brushmodel_gfx")) + value = "script_brushmodel"; + + BSPEntityEntry entry; + entry.key = key; + entry.value = value; + entity.entries.emplace_back(entry); + } + + Eigen::Vector4f position(0, 0, 0, 1.0f); + Eigen::Vector4f transformedPosition = nodeMatrix * position; + entity.origin.x = transformedPosition.x(); + entity.origin.y = transformedPosition.y(); + entity.origin.z = transformedPosition.z(); + RhcToLhcCoordinates(entity.origin.v); + + Eigen::Affine3f affineTransform(nodeMatrix); + Eigen::Quaternionf rotationQuat(affineTransform.rotation()); + rotationQuat.normalize(); + entity.rotationQuaternion.x = rotationQuat.x(); + entity.rotationQuaternion.y = rotationQuat.y(); + entity.rotationQuaternion.z = rotationQuat.z(); + entity.rotationQuaternion.w = rotationQuat.w(); + RhcToLhcQuaternion(entity.rotationQuaternion.v); + + if (!classname.compare("worldspawn")) + { + if (m_bsp->containsWorldspawn) + con::warn("WARNING: multiple worldspawn classes found, only one will be used."); + m_bsp->worldspawn = entity; + m_bsp->containsWorldspawn = true; + } + else if (!classname.compare("mp_global_intermission")) + { + if (m_bsp->containsIntermssion) + con::warn("WARNING: multiple mp_global_intermission classes found, only one will be used."); + m_bsp->containsIntermssion = true; + } + else + m_bsp->entities.emplace_back(entity); + return true; + } + + bool addNodeToBSP(const JsonRoot& jRoot, const gltf::JsonNode& node, Eigen::Matrix4f& nodeMatrix) + { + if (m_is_world_gfx && node.extensions && node.extensions->KHR_lights_punctual) + return addLightNode(jRoot, node, nodeMatrix); + + if (node.extras) + { + bool hasSpawnpoint = node.extras->contains("spawnpoint"); + bool hasZone = node.extras->contains("zone"); + bool hasSpawner = node.extras->contains("spawner"); + bool hasXmodel = node.extras->contains("xmodel"); + bool hasClassname = node.extras->contains("classname"); + + if (m_is_world_gfx && (hasSpawnpoint || hasZone || hasSpawner)) + return true; + + if (hasClassname) + { + std::string classnameVal = node.extras->at("classname"); + if (m_is_world_gfx && classnameVal.compare("script_brushmodel_gfx")) + return true; // skip any gfx node with classname not script_brushmodel_gfx + if (!m_is_world_gfx && !classnameVal.compare("script_brushmodel_gfx")) + return true; // skip any col node with classname script_brushmodel_gfx + + return addClassNode(jRoot, node, nodeMatrix); + } + + if (hasXmodel) + return addXModelNode(jRoot, node, nodeMatrix); + + if (hasSpawnpoint) + return addSpawnPointNode(node, nodeMatrix); + + if (m_bsp->isZombiesMap) + { + if (hasZone) + return addZoneNode(jRoot, node, nodeMatrix); + + if (hasSpawner) + return addZSpawnerNode(node, nodeMatrix); + } + } + + if (node.mesh) + { + int surfaceFlags = 0; + int contentFlags = 0; + if (node.extras && node.extras->contains("flags")) + getSurfaceAndContentFlags(node.extras->at("flags"), surfaceFlags, contentFlags); + + bool isNoDraw = (surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NODRAW].surfaceFlags) != 0; + if (m_is_world_gfx && isNoDraw) + return true; + + return addMeshNode(jRoot, node, nodeMatrix); + } + + return false; + } + + static std::vector GetRootNodes(const JsonRoot& jRoot) + { + if (!jRoot.nodes || jRoot.nodes->empty()) + return {}; + + const auto nodeCount = jRoot.nodes->size(); + std::vector rootNodes; + std::vector isChild(nodeCount); + + for (const auto& node : jRoot.nodes.value()) + { + if (!node.children) + continue; + + for (const auto childIndex : node.children.value()) + { + if (childIndex >= nodeCount) + throw GltfLoadException("Illegal child index"); + + if (isChild[childIndex]) + throw GltfLoadException("Node hierarchy is not a set of disjoint strict trees"); + + isChild[childIndex] = true; + } + } + + for (auto nodeIndex = 0u; nodeIndex < nodeCount; nodeIndex++) + { + if (!isChild[nodeIndex]) + rootNodes.emplace_back(nodeIndex); + } + + return rootNodes; + } + + void LoadMaterials(const JsonRoot& jRoot) + { + if (jRoot.materials) + { + m_curr_bsp_world->materials.reserve((*jRoot.materials).size()); + for (auto& jsMaterial : *jRoot.materials) + { + BSPMaterial material; + + if (jsMaterial.name && (*jsMaterial.name).length() != 0) + material.materialName = *jsMaterial.name; + else + throw GltfLoadException("Materials must have a name."); + + material.materialType = MATERIAL_TYPE_TEXTURE; + material.materialColour.x = 1.0f; + material.materialColour.y = 1.0f; + material.materialColour.z = 1.0f; + material.materialColour.w = 1.0f; + + material.surfaceFlags = 0; + material.contentFlags = 1; // all materials start out as solid + if (jsMaterial.extras && jsMaterial.extras->type) + { + bool foundType = false; + size_t typeNameCount = std::extent::value; + for (size_t typeIdx = 0; typeIdx < typeNameCount; typeIdx++) + { + BSPFlags::SurfaceType surfType = BSPFlags::materialTypes[typeIdx]; + if (!jsMaterial.extras->type->compare(BSPFlags::surfaceTypeToNameMap[surfType])) + { + BSPFlags::s_SurfaceTypeFlags flags = BSPFlags::surfaceTypeToFlagMap[surfType]; + material.surfaceFlags |= flags.surfaceFlags; + material.contentFlags |= flags.contentFlags; + foundType = true; + break; + } + } + if (!foundType) + con::warn("material {} with invalid type name: {}", material.materialName, *jsMaterial.extras->type); + } + + m_curr_bsp_world->materials.emplace_back(material); + } + } + + m_emptyMaterialIndex = m_curr_bsp_world->materials.size(); + BSPMaterial emptyMaterial; + emptyMaterial.materialType = MATERIAL_TYPE_COLOUR; + emptyMaterial.surfaceFlags = 0; + emptyMaterial.contentFlags = 1; + emptyMaterial.materialName = ""; + emptyMaterial.materialColour.x = 1.0f; + emptyMaterial.materialColour.y = 1.0f; + emptyMaterial.materialColour.z = 1.0f; + emptyMaterial.materialColour.w = 1.0f; + m_curr_bsp_world->materials.emplace_back(emptyMaterial); + } + + void TraverseNodes(const JsonRoot& jRoot) + { + // Make sure there are any nodes to traverse + if (!jRoot.nodes || jRoot.nodes->empty()) + return; + + struct s_nodes + { + unsigned int nodeIndex; + Eigen::Matrix4f parentNodeMatrix; + }; + + std::deque nodeQueue; + for (const auto rootNode : GetRootNodes(jRoot)) + nodeQueue.emplace_back(s_nodes{rootNode, Eigen::Matrix4f::Identity()}); + + while (!nodeQueue.empty()) + { + const auto& node = jRoot.nodes.value()[nodeQueue.front().nodeIndex]; + Eigen::Matrix4f parentNodeMatrix = nodeQueue.front().parentNodeMatrix; + nodeQueue.pop_front(); + + Eigen::Matrix4f nodeMatrix = createNodeMatrix(node); + Eigen::Matrix4f transformedNodeMatrix = parentNodeMatrix * nodeMatrix; + + if (node.children) + { + for (const auto childIndex : *node.children) + nodeQueue.emplace_back(s_nodes{childIndex, transformedNodeMatrix}); + } + + if (!addNodeToBSP(jRoot, node, transformedNodeMatrix)) + con::warn("({}) Ignoring node: {}", getWorldTypeName(), node.name.value_or("unnamed node")); + } + } + + void CreateBuffers(const JsonRoot& jRoot, Input& gltfInput) + { + if (!jRoot.buffers) + return; + + m_buffers.reserve(jRoot.buffers->size()); + for (const auto& jBuffer : *jRoot.buffers) + { + if (!jBuffer.uri) + { + const void* embeddedBufferPtr = nullptr; + size_t embeddedBufferSize = 0u; + if (!gltfInput.GetEmbeddedBuffer(embeddedBufferPtr, embeddedBufferSize) || embeddedBufferSize == 0u) + throw GltfLoadException("Buffer tried to access embedded data when there is none"); + + m_buffers.emplace_back(std::make_unique(embeddedBufferPtr, embeddedBufferSize)); + } + else if (DataUriBuffer::IsDataUri(*jBuffer.uri)) + { + auto dataUriBuffer = std::make_unique(); + if (!dataUriBuffer->ReadDataFromUri(*jBuffer.uri)) + throw GltfLoadException("Buffer has invalid data uri"); + + m_buffers.emplace_back(std::move(dataUriBuffer)); + } + else + { + throw GltfLoadException("File buffers are not supported"); + } + } + } + + void CreateBufferViews(const JsonRoot& jRoot) + { + if (!jRoot.bufferViews) + return; + + m_buffer_views.reserve(jRoot.bufferViews->size()); + for (const auto& jBufferView : *jRoot.bufferViews) + { + if (jBufferView.buffer >= m_buffers.size()) + throw GltfLoadException("Buffer view references invalid buffer"); + + const auto* buffer = m_buffers[jBufferView.buffer].get(); + const auto offset = jBufferView.byteOffset.value_or(0u); + const auto length = jBufferView.byteLength; + const auto stride = jBufferView.byteStride.value_or(0u); + + if (offset + length > buffer->GetSize()) + throw GltfLoadException("Buffer view is defined larger as underlying buffer"); + + m_buffer_views.emplace_back(std::make_unique(buffer, offset, length, stride)); + } + } + + void CreateAccessors(const JsonRoot& jRoot) + { + if (!jRoot.accessors) + return; + + m_accessors.reserve(jRoot.accessors->size()); + for (const auto& jAccessor : *jRoot.accessors) + { + if (!jAccessor.bufferView) + { + m_accessors.emplace_back(std::make_unique(jAccessor.count)); + continue; + } + + if (*jAccessor.bufferView >= m_buffer_views.size()) + throw GltfLoadException("Accessor references invalid buffer view"); + + const auto* bufferView = m_buffer_views[*jAccessor.bufferView].get(); + const auto byteOffset = jAccessor.byteOffset.value_or(0u); + if (jAccessor.componentType == JsonAccessorComponentType::FLOAT) + m_accessors.emplace_back(std::make_unique(bufferView, jAccessor.type, byteOffset, jAccessor.count)); + else if (jAccessor.componentType == JsonAccessorComponentType::UNSIGNED_BYTE) + m_accessors.emplace_back(std::make_unique(bufferView, jAccessor.type, byteOffset, jAccessor.count)); + else if (jAccessor.componentType == JsonAccessorComponentType::UNSIGNED_SHORT) + m_accessors.emplace_back(std::make_unique(bufferView, jAccessor.type, byteOffset, jAccessor.count)); + else if (jAccessor.componentType == JsonAccessorComponentType::UNSIGNED_INT) + m_accessors.emplace_back(std::make_unique(bufferView, jAccessor.type, byteOffset, jAccessor.count)); + else + throw GltfLoadException(std::format("Accessor has unsupported component type {}", static_cast(jAccessor.componentType))); + } + } + + public: + bool addGLTFDataToBSP(Input& gltfInput, bool isGfxWorld) + { + JsonRoot jRoot; + try + { + jRoot = gltfInput.GetJson().get(); + } + catch (const nlohmann::json::exception& e) + { + con::error("Failed to parse GLTF JSON: {}", e.what()); + return false; + } + + try + { + m_is_world_gfx = isGfxWorld; + if (isGfxWorld) + m_curr_bsp_world = &m_bsp->gfxWorld; + else + m_curr_bsp_world = &m_bsp->colWorld; + m_accessors.clear(); + m_buffer_views.clear(); + m_buffers.clear(); + + CreateBuffers(jRoot, gltfInput); + CreateBufferViews(jRoot); + CreateAccessors(jRoot); + + LoadMaterials(jRoot); + TraverseNodes(jRoot); // requires materials and lights + } + catch (const GltfLoadException& e) + { + con::error("Failed to load GLTF: {}", e.Str()); + return false; + } + + return true; + } + + BSPLoader(BSPData* bsp) + : m_bsp(bsp) + { + m_curr_bsp_world = nullptr; + m_is_world_gfx = false; + }; + }; +} // namespace + +namespace BSP +{ + std::unique_ptr createBSPData(std::string& mapName, ISearchPath& searchPath, bool isZombiesMap) + { + bool seperateColFile = true; + bool isGfxFileGltf = true; + bool isColFileGltf = true; + + std::string gfxFilePath = BSPUtil::getFileNameForBSPAsset("map_gfx.gltf"); + auto gfxFile = searchPath.Open(gfxFilePath); + if (!gfxFile.IsOpen()) + { + isGfxFileGltf = false; + gfxFilePath = BSPUtil::getFileNameForBSPAsset("map_gfx.glb"); + gfxFile = searchPath.Open(gfxFilePath); + if (!gfxFile.IsOpen()) + { + con::error("BSP Creator: Can't find map_gfx.gltf or map_gfx.glb."); + return nullptr; + } + } + + std::string colFilePath = BSPUtil::getFileNameForBSPAsset("map_col.gltf"); + auto colFile = searchPath.Open(colFilePath); + if (!colFile.IsOpen()) + { + isColFileGltf = false; + colFilePath = BSPUtil::getFileNameForBSPAsset("map_col.glb"); + colFile = searchPath.Open(colFilePath); + if (!colFile.IsOpen()) + { + con::info("BSP Creator: generating colision data from GLTF graphics data."); + seperateColFile = false; + } + } + + std::unique_ptr bsp = std::make_unique(); + bsp->name = mapName; + bsp->bspName = "maps/mp/" + mapName + ".d3dbsp"; + bsp->isZombiesMap = isZombiesMap; + bsp->hasSunlightBeenSet = false; + bsp->containsIntermssion = false; + bsp->containsWorldspawn = false; + + con::warn("XModels don't support scale currently, keep it at 1 in your editor"); + con::warn("All brushmodels, zones, triggers, and info_volumes must be an axis aligned box with 6 sides to work correctly."); + + BSPLoader loader(bsp.get()); + if (isGfxFileGltf) + { + gltf::TextInput input; + if (!input.ReadGltfData(*gfxFile.m_stream)) + return nullptr; + if (!loader.addGLTFDataToBSP(input, true)) + return nullptr; + if (!seperateColFile) + if (!loader.addGLTFDataToBSP(input, false)) + return nullptr; + } + else + { + gltf::BinInput input; + if (!input.ReadGltfData(*gfxFile.m_stream)) + return nullptr; + if (!loader.addGLTFDataToBSP(input, true)) + return nullptr; + if (!seperateColFile) + if (!loader.addGLTFDataToBSP(input, false)) + return nullptr; + } + + if (seperateColFile) + { + if (isColFileGltf) + { + gltf::TextInput input; + if (!input.ReadGltfData(*colFile.m_stream)) + return nullptr; + if (!loader.addGLTFDataToBSP(input, false)) + return nullptr; + } + else + { + gltf::BinInput input; + if (!input.ReadGltfData(*colFile.m_stream)) + return nullptr; + if (!loader.addGLTFDataToBSP(input, false)) + return nullptr; + } + } + + if (!bsp->hasSunlightBeenSet) + { + con::info("Writing default sun values"); + bsp->sunlight.type = LIGHT_TYPE_DIRECTIONAL; + bsp->sunlight.colour = {1.0f, 1.0f, 1.0f}; + bsp->sunlight.range = 1000.0f; + bsp->sunlight.intensity = 1000.0f; + bsp->sunlight.pos = {0.0f, 0.0f, 0.0f}; + bsp->sunlight.direction = {0.0f, -1.0f, 0.0f}; + bsp->sunlight.innerConeAngle = 0.0f; + bsp->sunlight.outerConeAngle = 0.0f; + } + if (!bsp->containsIntermssion) + { + con::error("Map does not contain a mp_global_intermission class"); + return nullptr; + } + if (!bsp->containsWorldspawn) + { + con::error("Map does not contain a worldspawn class"); + return nullptr; + } + if (bsp->spawnpoints.size() == 0) + { + con::error("Map must have spawn points!"); + return nullptr; + } + if (bsp->gfxWorld.staticSurfaces.size() + bsp->gfxWorld.scriptSurfaces.size() == 0 || bsp->gfxWorld.vertices.size() == 0 + || bsp->gfxWorld.indices.size() == 0) + { + con::error("GFX world has no surfaces, indicies or vertices!"); + return nullptr; + } + if (bsp->colWorld.staticSurfaces.size() + bsp->colWorld.scriptSurfaces.size() == 0 || bsp->colWorld.vertices.size() == 0 + || bsp->colWorld.indices.size() == 0) + { + con::error("Collision world has no surfaces, indicies or vertices!"); + return nullptr; + } + + return bsp; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPCreator.h b/src/ObjLoading/Game/T6/BSP/BSPCreator.h new file mode 100644 index 000000000..bdd0a20cc --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPCreator.h @@ -0,0 +1,9 @@ +#pragma once + +#include "BSP.h" +#include "SearchPath/ISearchPath.h" + +namespace BSP +{ + std::unique_ptr createBSPData(std::string& mapName, ISearchPath& searchPath, bool isZombiesMap); +}; // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPFlags.h b/src/ObjLoading/Game/T6/BSP/BSPFlags.h new file mode 100644 index 000000000..936c18632 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPFlags.h @@ -0,0 +1,263 @@ +#pragma once + +namespace BSPFlags +{ + enum SurfaceType + { + SURF_TYPE_BARK, + SURF_TYPE_BRICK, + SURF_TYPE_CARPET, + SURF_TYPE_CLOTH, + SURF_TYPE_CONCRETE, + SURF_TYPE_DIRT, + SURF_TYPE_FLESH, + SURF_TYPE_FOLIAGE, + SURF_TYPE_GLASS, + SURF_TYPE_GRASS, + SURF_TYPE_GRAVEL, + SURF_TYPE_ICE, + SURF_TYPE_METAL, + SURF_TYPE_MUD, + SURF_TYPE_PAPER, + SURF_TYPE_PLASTER, + SURF_TYPE_ROCK, + SURF_TYPE_SAND, + SURF_TYPE_SNOW, + SURF_TYPE_WATER, + SURF_TYPE_WOOD, + SURF_TYPE_ASPHALT, + SURF_TYPE_CERAMIC, + SURF_TYPE_PLASTIC, + SURF_TYPE_RUBBER, + SURF_TYPE_CUSHION, + SURF_TYPE_FRUIT, + SURF_TYPE_PAINTEDMETAL, + SURF_TYPE_PLAYER, + SURF_TYPE_TALLGRASS, + SURF_TYPE_RIOTSHIELD, + SURF_TYPE_OPAQUEGLASS, + SURF_TYPE_CLIPMISSILE, + SURF_TYPE_AI_NOSIGHT, + SURF_TYPE_CLIPSHOT, + SURF_TYPE_PLAYERCLIP, + SURF_TYPE_MONSTERCLIP, + SURF_TYPE_VEHICLECLIP, + SURF_TYPE_ITEMCLIP, + SURF_TYPE_NODROP, + SURF_TYPE_NONSOLID, + SURF_TYPE_DETAIL, + SURF_TYPE_STRUCTURAL, + SURF_TYPE_PORTAL, + SURF_TYPE_CANSHOOTCLIP, + SURF_TYPE_ORIGIN, + SURF_TYPE_SKY, + SURF_TYPE_NOCASTSHADOW, + SURF_TYPE_ONLYCASTSHADOW, + SURF_TYPE_PHYSICSGEOM, + SURF_TYPE_LIGHTPORTAL, + SURF_TYPE_CAULK, + SURF_TYPE_AREALIGHT, + SURF_TYPE_SLICK, + SURF_TYPE_NOIMPACT, + SURF_TYPE_NOMARKS, + SURF_TYPE_NOPENETRATE, + SURF_TYPE_LADDER, + SURF_TYPE_NODAMAGE, + SURF_TYPE_MANTLEON, + SURF_TYPE_MANTLEOVER, + SURF_TYPE_MOUNT, + SURF_TYPE_NOSTEPS, + SURF_TYPE_NODRAW, + SURF_TYPE_NORECEIVEDYNAMICSHADOW, + SURF_TYPE_NODLIGHT, + SURF_TYPE_NONE, // Custom value + SURF_TYPE_COUNT + }; + + struct s_SurfaceTypeFlags + { + unsigned int surfaceFlags; + unsigned int contentFlags; + }; + + constexpr s_SurfaceTypeFlags surfaceTypeToFlagMap[SURF_TYPE_COUNT] = { + {0x100000, 0 }, // bark + {0x200000, 0 }, // brick + {0x300000, 0 }, // carpet + {0x400000, 0 }, // cloth + {0x500000, 0 }, // concrete + {0x600000, 0 }, // dirt + {0x700000, 0 }, // flesh + {0x800000, 2 }, // foliage + {0x900000, 0x10 }, // glass + {0x0A00000, 0 }, // grass + {0x0B00000, 0 }, // gravel + {0x0C00000, 0 }, // ice + {0x0D00000, 0 }, // metal + {0x0E00000, 0 }, // mud + {0x0F00000, 0 }, // paper + {0x1000000, 0 }, // plaster + {0x1100000, 0 }, // rock + {0x1200000, 0 }, // sand + {0x1300000, 0 }, // snow + {0x1400000, 0x20 }, // water + {0x1500000, 0 }, // wood + {0x1600000, 0 }, // asphalt + {0x1700000, 0 }, // ceramic + {0x1800000, 0 }, // plastic + {0x1900000, 0 }, // rubber + {0x1A00000, 0 }, // cushion + {0x1B00000, 0 }, // fruit + {0x1C00000, 0 }, // paintedmetal + {0x1D00000, 0 }, // player + {0x1E00000, 0 }, // tallgrass + {0x1F00000, 0 }, // riotshield + {0x900000, 0 }, // opaqueglass + {0, 0x80 }, // clipmissile + {0, 0x1000 }, // ai_nosight + {0, 0x2000 }, // clipshot + {0, 0x10000 }, // playerclip + {0, 0x20000 }, // monsterclip + {0, 0x200 }, // vehicleclip + {0, 0x400 }, // itemclip + {0, 0x80000000}, // noDrop + {0x4000, 0 }, // nonSolid + {0, 0x8000000 }, // detail + {0, 0x10000000}, // structural + {0x80000000, 0 }, // portal + {0, 0x40 }, // canShootClip + {0, 0 }, // origin + {4, 0x800 }, // sky + {0x40000, 0 }, // noCastShadow + {0x80000, 0 }, // onlyCastShadow + {0, 0 }, // physicsGeom + {0, 0 }, // lightPortal + {0x1000, 0 }, // caulk + {0x8000, 0 }, // areaLight + {2, 0 }, // slick + {0x10, 0 }, // noImpact + {0x20, 0 }, // noMarks + {0x100, 0 }, // noPenetrate + {8, 0 }, // ladder + {1, 0 }, // noDamage + {0x4000000, 0x1000000 }, // mantleOn + {0x8000000, 0x1000000 }, // mantleOver + {0x10000000, 0x1000000 }, // mount + {0x2000, 0 }, // noSteps + {0x80, 0 }, // noDraw + {0x800, 0 }, // noReceiveDynamicShadow + {0x20000, 0 }, // noDlight + {0, 0 } // none + }; + + constexpr SurfaceType materialTypes[] = { + SURF_TYPE_NONE, SURF_TYPE_BARK, SURF_TYPE_BRICK, SURF_TYPE_CARPET, SURF_TYPE_CLOTH, SURF_TYPE_CONCRETE, SURF_TYPE_DIRT, + SURF_TYPE_FLESH, SURF_TYPE_FOLIAGE, SURF_TYPE_GLASS, SURF_TYPE_GRASS, SURF_TYPE_GRAVEL, SURF_TYPE_ICE, SURF_TYPE_METAL, + SURF_TYPE_MUD, SURF_TYPE_PAPER, SURF_TYPE_PLASTER, SURF_TYPE_ROCK, SURF_TYPE_SAND, SURF_TYPE_SNOW, SURF_TYPE_WATER, + SURF_TYPE_WOOD, SURF_TYPE_ASPHALT, SURF_TYPE_CERAMIC, SURF_TYPE_PLASTIC, SURF_TYPE_RUBBER, SURF_TYPE_CUSHION, SURF_TYPE_FRUIT, + SURF_TYPE_PAINTEDMETAL, SURF_TYPE_PLAYER, SURF_TYPE_TALLGRASS, SURF_TYPE_RIOTSHIELD, SURF_TYPE_OPAQUEGLASS, + }; + + constexpr SurfaceType materialFlags[] = { + SURF_TYPE_NONE, + SURF_TYPE_CLIPMISSILE, + SURF_TYPE_AI_NOSIGHT, + SURF_TYPE_CLIPSHOT, + SURF_TYPE_PLAYERCLIP, + SURF_TYPE_MONSTERCLIP, + SURF_TYPE_VEHICLECLIP, + SURF_TYPE_ITEMCLIP, + SURF_TYPE_NODROP, + SURF_TYPE_NONSOLID, + SURF_TYPE_DETAIL, + SURF_TYPE_STRUCTURAL, + SURF_TYPE_PORTAL, + SURF_TYPE_CANSHOOTCLIP, + SURF_TYPE_SKY, + SURF_TYPE_NOCASTSHADOW, + SURF_TYPE_ONLYCASTSHADOW, + SURF_TYPE_CAULK, + SURF_TYPE_AREALIGHT, + SURF_TYPE_SLICK, + SURF_TYPE_NOIMPACT, + SURF_TYPE_NOMARKS, + SURF_TYPE_NOPENETRATE, + SURF_TYPE_LADDER, + SURF_TYPE_NODAMAGE, + SURF_TYPE_MANTLEON, + SURF_TYPE_MANTLEOVER, + SURF_TYPE_MOUNT, + SURF_TYPE_NOSTEPS, + SURF_TYPE_NODRAW, + SURF_TYPE_NORECEIVEDYNAMICSHADOW, + SURF_TYPE_NODLIGHT, + }; + + constexpr const char* surfaceTypeToNameMap[SURF_TYPE_COUNT] = {"bark", + "brick", + "carpet", + "cloth", + "concrete", + "dirt", + "flesh", + "foliage", + "glass", + "grass", + "gravel", + "ice", + "metal", + "mud", + "paper", + "plaster", + "rock", + "sand", + "snow", + "water", + "wood", + "asphalt", + "ceramic", + "plastic", + "rubber", + "cushion", + "fruit", + "paintedmetal", + "player", + "tallgrass", + "riotshield", + "opaqueglass", + "clipmissile", + "ai_nosight", + "clipshot", + "playerclip", + "monsterclip", + "vehicleclip", + "itemclip", + "nodrop", + "nonsolid", + "detail", + "structural", + "portal", + "canshootclip", + "origin", + "sky", + "nocastshadow", + "onlycastshadow", + "physicsgeom", + "lightportal", + "caulk", + "arealight", + "slick", + "noimpact", + "nomarks", + "nopenetrate", + "ladder", + "nodamage", + "mantleon", + "mantleover", + "mount", + "nosteps", + "nodraw", + "noreceivedynamicshadow", + "nodlight", + "none"}; +} // namespace BSPFlags diff --git a/src/ObjLoading/Game/T6/BSP/BSPUtil.cpp b/src/ObjLoading/Game/T6/BSP/BSPUtil.cpp new file mode 100644 index 000000000..20ef6458b --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPUtil.cpp @@ -0,0 +1,311 @@ +#pragma once + +#include "BSPUtil.h" + +#include +#include +#include + +namespace BSP +{ + std::string BSPUtil::getFileNameForBSPAsset(const std::string& assetName) + { + return std::format("BSP/{}", assetName); + } + + vec3_t BSPUtil::convertToBO2Coords(vec3_t& coordinate) + { + vec3_t result; + result.x = coordinate.x; + result.y = -coordinate.z; + result.z = coordinate.y; + return result; + } + + vec3_t BSPUtil::convertFromBO2Coords(vec3_t& coordinate) + { + vec3_t result; + result.x = coordinate.x; + result.y = coordinate.z; + result.z = -coordinate.y; + return result; + } + + void BSPUtil::updateAABB(vec3_t& newAABBMins, vec3_t& newAABBMaxs, vec3_t& AABBMins, vec3_t& AABBMaxs) + { + if (AABBMins.x > newAABBMins.x) + AABBMins.x = newAABBMins.x; + + if (newAABBMaxs.x > AABBMaxs.x) + AABBMaxs.x = newAABBMaxs.x; + + if (AABBMins.y > newAABBMins.y) + AABBMins.y = newAABBMins.y; + + if (newAABBMaxs.y > AABBMaxs.y) + AABBMaxs.y = newAABBMaxs.y; + + if (AABBMins.z > newAABBMins.z) + AABBMins.z = newAABBMins.z; + + if (newAABBMaxs.z > AABBMaxs.z) + AABBMaxs.z = newAABBMaxs.z; + } + + void BSPUtil::updateAABBWithPoint(vec3_t& point, vec3_t& AABBMins, vec3_t& AABBMaxs) + { + if (AABBMins.x > point.x) + AABBMins.x = point.x; + + if (point.x > AABBMaxs.x) + AABBMaxs.x = point.x; + + if (AABBMins.y > point.y) + AABBMins.y = point.y; + + if (point.y > AABBMaxs.y) + AABBMaxs.y = point.y; + + if (AABBMins.z > point.z) + AABBMins.z = point.z; + + if (point.z > AABBMaxs.z) + AABBMaxs.z = point.z; + } + + vec3_t BSPUtil::calcMiddleOfAABB(vec3_t& mins, vec3_t& maxs) + { + vec3_t result; + result.x = (mins.x + maxs.x) * 0.5f; + result.y = (mins.y + maxs.y) * 0.5f; + result.z = (mins.z + maxs.z) * 0.5f; + return result; + } + + vec3_t BSPUtil::calcHalfSizeOfAABB(vec3_t& mins, vec3_t& maxs) + { + vec3_t result; + result.x = (maxs.x - mins.x) * 0.5f; + result.y = (maxs.y - mins.y) * 0.5f; + result.z = (maxs.z - mins.z) * 0.5f; + return result; + } + + size_t BSPUtil::allignBy128(size_t size) + { + return ((size + 127) & 0xFFFFFF80); + } + + float BSPUtil::distBetweenPoints(vec3_t& p1, vec3_t& p2) + { + float x = p2.x - p1.x; + float y = p2.y - p1.y; + float z = p2.z - p1.z; + return sqrtf((x * x) + (y * y) + (z * z)); + } + + void BSPUtil::calculateXmodelBounds(XModel* xmodel, vec3_t axis[3], vec3_t& out_mins, vec3_t& out_maxs) + { + out_mins.x = 0.0f; + out_mins.y = 0.0f; + out_mins.z = 0.0f; + out_maxs.x = 0.0f; + out_maxs.y = 0.0f; + out_maxs.z = 0.0f; + + for (auto surfaceIndex = 0u; surfaceIndex < xmodel->lodInfo[0].numsurfs; surfaceIndex++) + { + const auto& surface = xmodel->surfs[surfaceIndex + xmodel->lodInfo[0].surfIndex]; + + if (!surface.verts0) + continue; + + for (auto vertIndex = 0u; vertIndex < surface.vertCount; vertIndex++) + { + const auto& vertex = surface.verts0[vertIndex].xyz; + + vec3_t rotatedVert; + rotatedVert.x = (vertex.x * axis[0].x) + (vertex.y * axis[1].x) + (vertex.z * axis[2].x); + rotatedVert.y = (vertex.x * axis[0].y) + (vertex.y * axis[1].y) + (vertex.z * axis[2].y); + rotatedVert.z = (vertex.x * axis[0].z) + (vertex.y * axis[1].z) + (vertex.z * axis[2].z); + + if (vertIndex == 0 && surfaceIndex == 0) + { + out_mins = rotatedVert; + out_maxs = rotatedVert; + } + else + BSPUtil::updateAABBWithPoint(rotatedVert, out_mins, out_maxs); + } + } + } + + void BSPUtil::convertAnglesToAxis(vec3_t* angles, vec3_t axis[3]) + { + float cosX = cos(angles->x); + float sinX = sin(angles->x); + float cosY = cos(angles->y); + float sinY = sin(angles->y); + float cosZ = cos(angles->z); + float sinZ = sin(angles->z); + + axis[0].x = cosX * cosY; + axis[0].y = cosX * sinY; + axis[0].z = -sinX; + axis[1].x = ((sinZ * sinX) * cosY) - (cosZ * sinY); + axis[1].y = ((sinZ * sinX) * sinY) + (cosZ * cosY); + axis[1].z = sinZ * cosX; + axis[2].x = ((cosZ * sinX) * cosY) + (sinZ * sinY); + axis[2].y = ((cosZ * sinX) * sinY) - (sinZ * cosY); + axis[2].z = cosZ * cosX; + } + + void BSPUtil::convertQuaternionToAxis(vec4_t* quat, vec3_t axis[3]) + { + float quatX = quat->v[0]; + float quatY = quat->v[1]; + float quatZ = quat->v[2]; + float quatW = quat->v[3]; + + float xx = (quatX * 2.0f) * quatX; + float xy = (quatX * 2.0f) * quatY; + float xz = (quatX * 2.0f) * quatZ; + float xw = (quatX * 2.0f) * quatW; + + float yy = (quatY * 2.0f) * quatY; + float yz = (quatY * 2.0f) * quatZ; + float yw = (quatY * 2.0f) * quatW; + + float zz = (quatZ * 2.0f) * quatZ; + float zw = (quatZ * 2.0f) * quatW; + + axis->x = 1.0f - (zz + yy); + axis->y = zw + xy; + axis->z = xz - yw; + + axis[1].x = xy - zw; + axis[1].y = 1.0f - (zz + xx); + axis[1].z = yz + xw; + + axis[2].x = yw + xz; + axis[2].y = yz - xw; + axis[2].z = 1.0f - (yy + xx); + } + + vec3_t BSPUtil::convertForwardVectorToViewAngles(vec3_t& forwardVec) + { + vec3_t viewAngles; + + if (forwardVec.x == 0.0f && forwardVec.y == 0.0f) + { + if (-forwardVec.z < 0.0f) + viewAngles.x = 270.0f; + else + viewAngles.x = 90.0f; + viewAngles.y = 0.0f; + viewAngles.z = 0.0f; + return viewAngles; + } + + float xAndYDist = sqrtf((forwardVec.x * forwardVec.x) + (forwardVec.y * forwardVec.y)); + float atanXRadians = atan2f(forwardVec.z, xAndYDist); + float atanXDegrees = atanXRadians * (-180.0f / std::numbers::pi_v); + if (atanXDegrees < 0.0f) + atanXDegrees += 360.0f; + viewAngles.x = atanXDegrees; + + float atanYRadians = atan2f(forwardVec.y, forwardVec.x); + float atanYDegrees = atanYRadians * (180.0f / std::numbers::pi_v); + if (atanYDegrees < 0.0f) + atanYDegrees += 360.0f; + viewAngles.y = atanYDegrees; + + viewAngles.z = 0.0f; + + return viewAngles; + } + + float BSPUtil::getPitchFromVector(vec3_t& vector) + { + if (vector.x == 0.0f && vector.y == 0.0f) + { + if (-vector.z < 0.0f) + return -90.0f; + else + return 90.0f; + } + + float xAndYDist = sqrtf((vector.x * vector.x) + (vector.y * vector.y)); + float pitchRadians = atan2f(vector.z, xAndYDist); + float pitchDegrees = pitchRadians * (-180.0f / std::numbers::pi_v); + return pitchDegrees; + } + + vec3_t BSPUtil::convertAxisToAngles(vec3_t axis[3]) + { + vec3_t tempAngles = convertForwardVectorToViewAngles(axis[0]); + float xRadiansNeg = -tempAngles.x * (std::numbers::pi_v / 180.0f); + float yRadiansNeg = -tempAngles.y * (std::numbers::pi_v / 180.0f); + float cosX = cos(xRadiansNeg); + float sinX = sin(xRadiansNeg); + float cosY = cos(yRadiansNeg); + float sinY = sin(yRadiansNeg); + + vec3_t tempVec; + float tempFloat = (axis[1].x * cosY) - (axis[1].y * sinY); + tempVec.x = (axis[1].z * sinX) + (tempFloat * cosX); + tempVec.y = (axis[1].x * sinY) + (axis[1].y * cosY); + tempVec.z = (axis[1].z * cosX) - (tempFloat * sinX); + float pitch = getPitchFromVector(tempVec); + if (tempVec.y >= 0.0f) + tempAngles.z = -pitch; + else if (pitch >= 0.0f) + tempAngles.z = pitch - 180.0f; + else + tempAngles.z = pitch + 180.0f; + return tempAngles; + } + + void BSPUtil::matrixTranspose3x3(const vec3_t* in, vec3_t* out) + { + out[0].x = in[0].x; + out[0].y = in[1].x; + out[0].z = in[2].x; + out[1].x = in[0].y; + out[1].y = in[1].y; + out[1].z = in[2].y; + out[2].x = in[0].z; + out[2].y = in[1].z; + out[2].z = in[2].z; + } + + vec3_t BSPUtil::convertStringToVec3(std::string& str) + { + std::string v1Str = str; + + int nextValIndex = 0; + while (v1Str[nextValIndex] != ' ') + nextValIndex++; + nextValIndex++; // skip past space + std::string v2Str = &v1Str[nextValIndex]; + + nextValIndex = 0; + while (v2Str[nextValIndex] != ' ') + nextValIndex++; + nextValIndex++; // skip past space + std::string v3Str = &v2Str[nextValIndex]; + + vec3_t result; + result.x = static_cast(atof(v1Str.c_str())); + result.y = static_cast(atof(v2Str.c_str())); + result.z = static_cast(atof(v3Str.c_str())); + return result; + } + + std::string BSPUtil::convertVec3ToString(vec3_t& vec) + { + std::string result = std::format("{} {} {}", roundf(vec.x), roundf(vec.y), roundf(vec.z)); + return result; + } + +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/BSPUtil.h b/src/ObjLoading/Game/T6/BSP/BSPUtil.h new file mode 100644 index 000000000..d319748b9 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/BSPUtil.h @@ -0,0 +1,29 @@ +#pragma once + +#include "BSP.h" + +namespace BSP +{ + class BSPUtil + { + public: + static std::string getFileNameForBSPAsset(const std::string& assetName); + static vec3_t convertToBO2Coords(vec3_t& OGL_coordinate); + static vec3_t convertFromBO2Coords(vec3_t& bo2_coordinate); + static void updateAABB(vec3_t& newAABBMins, vec3_t& newAABBMaxs, vec3_t& AABBMins, vec3_t& AABBMaxs); + static void updateAABBWithPoint(vec3_t& point, vec3_t& AABBMins, vec3_t& AABBMaxs); + static vec3_t calcMiddleOfAABB(vec3_t& mins, vec3_t& maxs); + static vec3_t calcHalfSizeOfAABB(vec3_t& mins, vec3_t& maxs); + static size_t allignBy128(size_t size); + static float distBetweenPoints(vec3_t& p1, vec3_t& p2); + static void calculateXmodelBounds(XModel* xmodel, vec3_t axis[3], vec3_t& out_mins, vec3_t& out_maxs); + static void convertAnglesToAxis(vec3_t* angles, vec3_t axis[3]); + static void convertQuaternionToAxis(vec4_t* quat, vec3_t axis[3]); + static vec3_t convertForwardVectorToViewAngles(vec3_t& forwardVec); + static float getPitchFromVector(vec3_t& vector); + static vec3_t convertAxisToAngles(vec3_t axis[3]); + static void matrixTranspose3x3(const vec3_t* in, vec3_t* out); + static vec3_t convertStringToVec3(std::string& str); + static std::string convertVec3ToString(vec3_t& vec); + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.cpp new file mode 100644 index 000000000..786364bea --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.cpp @@ -0,0 +1,111 @@ +#include "BSPLinker.h" + +#include "ClipMapLinker.h" +#include "ComWorldLinker.h" +#include "GameWorldMpLinker.h" +#include "GfxWorldLinker.h" +#include "MapEntsLinker.h" +#include "SkinnedVertsLinker.h" + +namespace BSP +{ + FootstepTableDef* BSPLinker::addEmptyFootstepTableAsset(std::string assetName) + { + if (assetName.length() == 0) + return nullptr; + + FootstepTableDef* footstepTable = m_memory.Alloc(); + footstepTable->name = m_memory.Dup(assetName.c_str()); + memset(footstepTable->sndAliasTable, 0, sizeof(footstepTable->sndAliasTable)); + + m_context.AddAsset(assetName, footstepTable); + + return footstepTable; + } + + bool BSPLinker::addDefaultRequiredAssets(BSPData* bsp) + { + if (m_context.LoadDependency("maps/mp/" + bsp->name + ".gsc") == nullptr) + { + con::error("maps/mp/" + bsp->name + ".gsc not found, make sure GSC file is in another fastfile."); + } + else + { + if (m_context.LoadDependency("maps/mp/" + bsp->name + "_amb.gsc") == nullptr) + return false; + if (m_context.LoadDependency("maps/mp/" + bsp->name + "_fx.gsc") == nullptr) + return false; + + if (m_context.LoadDependency("clientscripts/mp/" + bsp->name + ".csc") == nullptr) + return false; + if (m_context.LoadDependency("clientscripts/mp/" + bsp->name + "_amb.csc") == nullptr) + return false; + if (m_context.LoadDependency("clientscripts/mp/" + bsp->name + "_fx.csc") == nullptr) + return false; + } + + addEmptyFootstepTableAsset("default_1st_person"); + addEmptyFootstepTableAsset("default_3rd_person"); + addEmptyFootstepTableAsset("default_1st_person_quiet"); + addEmptyFootstepTableAsset("default_3rd_person_quiet"); + addEmptyFootstepTableAsset("default_3rd_person_loud"); + addEmptyFootstepTableAsset("default_ai"); + + if (m_context.LoadDependency("animtrees/fxanim_props.atr") == nullptr) + return false; + + return true; + } + + BSPLinker::BSPLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + bool BSPLinker::linkBSP(BSPData* bsp) + { + if (!addDefaultRequiredAssets(bsp)) + return false; + + ComWorldLinker comWorldLinker(m_memory, m_search_path, m_context); + ClipMapLinker clipMapLinker(m_memory, m_search_path, m_context); + GameWorldMpLinker gameWorldMpLinker(m_memory, m_search_path, m_context); + GfxWorldLinker gfxWorldLinker(m_memory, m_search_path, m_context); + MapEntsLinker mapEntsLinker(m_memory, m_search_path, m_context); + SkinnedVertsLinker skinnedVertsLinker(m_memory, m_search_path, m_context); + + ComWorld* comWorld = comWorldLinker.linkComWorld(bsp); + if (comWorld == nullptr) + return false; + m_context.AddAsset(comWorld->name, comWorld); + + MapEnts* mapEnts = mapEntsLinker.linkMapEnts(bsp); + if (mapEnts == nullptr) + return false; + m_context.AddAsset(mapEnts->name, mapEnts); + + GameWorldMp* gameWorldMp = gameWorldMpLinker.linkGameWorldMp(bsp); + if (gameWorldMp == nullptr) + return false; + m_context.AddAsset(gameWorldMp->name, gameWorldMp); + + SkinnedVertsDef* skinnedVerts = skinnedVertsLinker.linkSkinnedVerts(bsp); + if (skinnedVerts == nullptr) + return false; + m_context.AddAsset(skinnedVerts->name, skinnedVerts); + + GfxWorld* gfxWorld = gfxWorldLinker.linkGfxWorld(bsp); + if (gfxWorld == nullptr) + return false; + m_context.AddAsset(gfxWorld->name, gfxWorld); + + clipMap_t* clipMap = clipMapLinker.linkClipMap(bsp); // requires mapents asset + if (clipMap == nullptr) + return false; + m_context.AddAsset(clipMap->name, clipMap); + + return true; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.h new file mode 100644 index 000000000..e4d53a2a3 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/BSPLinker.h @@ -0,0 +1,24 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class BSPLinker + { + public: + BSPLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + bool linkBSP(BSPData* bsp); + + private: + FootstepTableDef* addEmptyFootstepTableAsset(std::string assetName); + bool addDefaultRequiredAssets(BSPData* bsp); + + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.cpp new file mode 100644 index 000000000..4951eb83a --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.cpp @@ -0,0 +1,923 @@ +#include "ClipMapLinker.h" + +#include "../BSPUtil.h" + +namespace +{ + struct uniqueMatData + { + size_t materialIndex; + std::vector partitionIndexes; + }; + + constexpr size_t MAX_AABB_TREE_CHILDREN = 128; +} // namespace + +namespace BSP +{ + ClipMapLinker::ClipMapLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + void ClipMapLinker::loadDynEnts(clipMap_t* clipMap) + { + int dynEntCount = 0; + clipMap->originalDynEntCount = dynEntCount; + clipMap->dynEntCount[0] = clipMap->originalDynEntCount + 256; // the game allocs 256 empty dynents, as they may be used ingame + clipMap->dynEntCount[1] = 0; + clipMap->dynEntCount[2] = 0; + clipMap->dynEntCount[3] = 0; + + clipMap->dynEntClientList[0] = m_memory.Alloc(clipMap->dynEntCount[0]); + clipMap->dynEntClientList[1] = nullptr; + + clipMap->dynEntServerList[0] = nullptr; + clipMap->dynEntServerList[1] = nullptr; + + clipMap->dynEntCollList[0] = m_memory.Alloc(clipMap->dynEntCount[0]); + clipMap->dynEntCollList[1] = nullptr; + clipMap->dynEntCollList[2] = nullptr; + clipMap->dynEntCollList[3] = nullptr; + + clipMap->dynEntPoseList[0] = m_memory.Alloc(clipMap->dynEntCount[0]); + clipMap->dynEntPoseList[1] = nullptr; + + clipMap->dynEntDefList[0] = m_memory.Alloc(clipMap->dynEntCount[0]); + clipMap->dynEntDefList[1] = nullptr; + } + + void ClipMapLinker::loadVisibility(clipMap_t* clipMap) + { + // Only use one visbility cluster for the entire map + clipMap->numClusters = 1; + clipMap->vised = 0; + clipMap->clusterBytes = ((clipMap->numClusters + 63) >> 3) & 0xFFFFFFF8; + clipMap->visibility = m_memory.Alloc(clipMap->clusterBytes); + // Official maps set visibility to all 0xFF + memset(clipMap->visibility, 0xFF, clipMap->clusterBytes); + } + + void ClipMapLinker::loadBoxData(clipMap_t* clipMap) + { + // box_model and box_brush are what are used by game traces as "temporary" collision when + // no brush or model is specified to do the trace with. + // All values in this function are taken from official map BSPs + + // for some reason the maxs are negative, and mins are positive + // float box_mins = 3.4028235e38; + // float box_maxs = -3.4028235e38; + // hack: the floats above can't be safely converted to 32 bit floats, and the game requires them to be exact + // so we use the hex representation and set it using int pointers. + unsigned int box_mins = 0x7F7FFFFF; + unsigned int box_maxs = 0xFF7FFFFF; + *(reinterpret_cast(&clipMap->box_model.leaf.mins.x)) = box_mins; + *(reinterpret_cast(&clipMap->box_model.leaf.mins.y)) = box_mins; + *(reinterpret_cast(&clipMap->box_model.leaf.mins.z)) = box_mins; + *(reinterpret_cast(&clipMap->box_model.leaf.maxs.x)) = box_maxs; + *(reinterpret_cast(&clipMap->box_model.leaf.maxs.y)) = box_maxs; + *(reinterpret_cast(&clipMap->box_model.leaf.maxs.z)) = box_maxs; + + clipMap->box_model.leaf.brushContents = -1; + clipMap->box_model.leaf.terrainContents = 0; + clipMap->box_model.leaf.cluster = 0; + clipMap->box_model.leaf.collAabbCount = 0; + clipMap->box_model.leaf.firstCollAabbIndex = 0; + clipMap->box_model.leaf.leafBrushNode = 0; + clipMap->box_model.mins.x = 0.0f; + clipMap->box_model.mins.y = 0.0f; + clipMap->box_model.mins.z = 0.0f; + clipMap->box_model.maxs.x = 0.0f; + clipMap->box_model.maxs.y = 0.0f; + clipMap->box_model.maxs.z = 0.0f; + clipMap->box_model.radius = 0.0f; + clipMap->box_model.info = nullptr; + + clipMap->box_brush = m_memory.Alloc(); + clipMap->box_brush->axial_sflags[0][0] = -1; + clipMap->box_brush->axial_sflags[0][1] = -1; + clipMap->box_brush->axial_sflags[0][2] = -1; + clipMap->box_brush->axial_sflags[1][0] = -1; + clipMap->box_brush->axial_sflags[1][1] = -1; + clipMap->box_brush->axial_sflags[1][2] = -1; + clipMap->box_brush->axial_cflags[0][0] = -1; + clipMap->box_brush->axial_cflags[0][1] = -1; + clipMap->box_brush->axial_cflags[0][2] = -1; + clipMap->box_brush->axial_cflags[1][0] = -1; + clipMap->box_brush->axial_cflags[1][1] = -1; + clipMap->box_brush->axial_cflags[1][2] = -1; + clipMap->box_brush->contents = -1; + clipMap->box_brush->mins.x = 0.0f; + clipMap->box_brush->mins.y = 0.0f; + clipMap->box_brush->mins.z = 0.0f; + clipMap->box_brush->maxs.x = 0.0f; + clipMap->box_brush->maxs.y = 0.0f; + clipMap->box_brush->maxs.z = 0.0f; + clipMap->box_brush->numsides = 0; + clipMap->box_brush->numverts = 0; + clipMap->box_brush->sides = nullptr; + clipMap->box_brush->verts = nullptr; + } + + void ClipMapLinker::loadRopesAndConstraints(clipMap_t* clipMap) + { + clipMap->num_constraints = 0; // max 511 + clipMap->constraints = nullptr; + + // The game allocates 32 empty ropes + clipMap->max_ropes = 32; // max 300 + clipMap->ropes = m_memory.Alloc(clipMap->max_ropes); + } + + bool ClipMapLinker::loadXModelCollision(clipMap_t* clipMap, BSPData* bsp) + { + // it seems like for players to be able to collide with xmodels, it requires xmodel->collmaps to be valid. + // A lot of XModels don't implement collmaps (OAT also doesn't generate these collmaps atm), and + // even official maps instead use terrain or brushes to cover where the collision should be. + + clipMap->numStaticModels = bsp->colWorld.xmodels.size(); + clipMap->staticModelList = m_memory.Alloc(clipMap->numStaticModels); + + if (clipMap->numStaticModels == 0) + return true; + if (clipMap->numStaticModels > 0xFFFF) + { + con::error("COLWorld exceeded the maximum number of static xmodels (65535 max, map count: {})", clipMap->numStaticModels); + return false; + } + + for (unsigned int i = 0; i < clipMap->numStaticModels; i++) + { + cStaticModel_s* currModel = &clipMap->staticModelList[i]; + BSPXModel& bspModel = bsp->colWorld.xmodels.at(i); + + vec3_t xmodelAxis[3]; + BSPUtil::convertQuaternionToAxis(&bspModel.rotationQuaternion, xmodelAxis); + + auto xModelAsset = m_context.LoadDependency(bspModel.name); + if (xModelAsset == nullptr) + { + con::error("Unable to load xmodel asset: \"{}\"", bspModel.name); + return false; + } + else + currModel->xmodel = (XModel*)xModelAsset->Asset(); + + currModel->origin.x = bspModel.origin.x; + currModel->origin.y = bspModel.origin.y; + currModel->origin.z = bspModel.origin.z; + + // tracing only checks if this is equal to 0 or not + currModel->contents = 1; + + if (!xModelAsset->IsReference()) + { + BSPUtil::calculateXmodelBounds(currModel->xmodel, xmodelAxis, currModel->absmin, currModel->absmax); + currModel->absmin.x = (currModel->absmin.x * bspModel.scale) + bspModel.origin.x; + currModel->absmin.y = (currModel->absmin.y * bspModel.scale) + bspModel.origin.y; + currModel->absmin.z = (currModel->absmin.z * bspModel.scale) + bspModel.origin.z; + currModel->absmax.x = (currModel->absmax.x * bspModel.scale) + bspModel.origin.x; + currModel->absmax.y = (currModel->absmax.y * bspModel.scale) + bspModel.origin.y; + currModel->absmax.z = (currModel->absmax.z * bspModel.scale) + bspModel.origin.z; + + if (currModel->xmodel->numCollmaps == 0) + con::warn("Xmodel \"{}\" has no colision data", bspModel.name); + } + else + { + if (bspModel.areBoundsValid) + { + currModel->absmin = bspModel.mins; + currModel->absmax = bspModel.maxs; + } + else + { + con::warn("Unable to determine the bounds of xmodel: \"{}\"", bspModel.name); + currModel->absmin.x = bspModel.origin.x - 1.0f; + currModel->absmin.y = bspModel.origin.y - 1.0f; + currModel->absmin.z = bspModel.origin.z - 1.0f; + currModel->absmax.x = bspModel.origin.x + 1.0f; + currModel->absmax.y = bspModel.origin.y + 1.0f; + currModel->absmax.z = bspModel.origin.z + 1.0f; + } + } + + BSPUtil::matrixTranspose3x3(xmodelAxis, currModel->invScaledAxis); + currModel->invScaledAxis[0].x = (1.0f / bspModel.scale) * currModel->invScaledAxis[0].x; + currModel->invScaledAxis[0].y = (1.0f / bspModel.scale) * currModel->invScaledAxis[0].y; + currModel->invScaledAxis[0].z = (1.0f / bspModel.scale) * currModel->invScaledAxis[0].z; + currModel->invScaledAxis[1].x = (1.0f / bspModel.scale) * currModel->invScaledAxis[1].x; + currModel->invScaledAxis[1].y = (1.0f / bspModel.scale) * currModel->invScaledAxis[1].y; + currModel->invScaledAxis[1].z = (1.0f / bspModel.scale) * currModel->invScaledAxis[1].z; + currModel->invScaledAxis[2].x = (1.0f / bspModel.scale) * currModel->invScaledAxis[2].x; + currModel->invScaledAxis[2].y = (1.0f / bspModel.scale) * currModel->invScaledAxis[2].y; + currModel->invScaledAxis[2].z = (1.0f / bspModel.scale) * currModel->invScaledAxis[2].z; + + memset(&currModel->writable, 0, sizeof(cStaticModelWritable)); + } + + return true; + } + + // out_mins and out_maxs are initialised in the function + void calculatePartitionAABB(clipMap_t* clipMap, CollisionPartition* partition, vec3_t& out_mins, vec3_t& out_maxs) + { + for (int uindIdx = 0; uindIdx < partition->nuinds; uindIdx++) + { + uint16_t uind = clipMap->info.uinds[partition->fuind + uindIdx]; + vec3_t vert = clipMap->verts[uind]; + + // initalise the AABB with the first vertex + if (uindIdx == 0) + { + out_mins = vert; + out_maxs = vert; + } + else + BSPUtil::updateAABBWithPoint(vert, out_mins, out_maxs); + } + } + + void ClipMapLinker::addAABBTreeFromPartitions( + clipMap_t* clipMap, std::vector& partitions, size_t* out_parentCount, size_t* out_parentStartIndex, int* out_treeContents) + { + size_t partitionCount = partitions.size(); + assert(partitionCount > 0); + if (partitionCount > highestPartitionCountForAABB) + highestPartitionCountForAABB = partitionCount; + + std::vector uniqueMaterials; + for (size_t partArrayIdx = 0; partArrayIdx < partitionCount; partArrayIdx++) + { + int partitionIdx = partitions.at(partArrayIdx); + size_t materialIndex = collisionSurfaceVec.at(partitionToColSurfaceMap.at(partitionIdx)).materialIndex; + bool foundIdx = false; + for (auto& uniqueMat : uniqueMaterials) + { + if (uniqueMat.materialIndex == materialIndex) + { + uniqueMat.partitionIndexes.emplace_back(partitionIdx); + foundIdx = true; + break; + } + } + if (!foundIdx) + { + uniqueMatData data; + data.materialIndex = materialIndex; + data.partitionIndexes.emplace_back(partitionIdx); + uniqueMaterials.emplace_back(data); + } + } + + // BO2 has a maximum limit of 128 children per AABB tree (essentially), + // so this is fixed by adding multiple parent AABB trees that hold 128 children each + size_t totalParentCount = 0; + for (auto& matData : uniqueMaterials) + { + size_t objCount = matData.partitionIndexes.size(); + size_t result = objCount / MAX_AABB_TREE_CHILDREN; + size_t remainder = objCount % MAX_AABB_TREE_CHILDREN; + if (remainder > 0) + result++; + totalParentCount += result; + } + + // every parent node needs to be contiguous in memory + size_t parentAABBArrayIndex = AABBTreeVec.size(); + AABBTreeVec.resize(AABBTreeVec.size() + totalParentCount); + *out_parentCount = totalParentCount; + *out_parentStartIndex = parentAABBArrayIndex; + + for (auto& matData : uniqueMaterials) + { + size_t matPartCount = matData.partitionIndexes.size(); + size_t parentCount = matPartCount / MAX_AABB_TREE_CHILDREN; + size_t remainder = matPartCount % MAX_AABB_TREE_CHILDREN; + if (remainder > 0) + parentCount++; + + size_t unaddedObjectCount = matPartCount; + size_t addedObjectCount = 0; + for (size_t parentIdx = 0; parentIdx < parentCount; parentIdx++) + { + size_t currChildObjectCount = MAX_AABB_TREE_CHILDREN; + if (unaddedObjectCount <= MAX_AABB_TREE_CHILDREN) + currChildObjectCount = unaddedObjectCount; + else + unaddedObjectCount -= MAX_AABB_TREE_CHILDREN; + + vec3_t parentMins; + vec3_t parentMaxs; + size_t childObjectStartIndex = AABBTreeVec.size(); + for (size_t objectIdx = 0; objectIdx < currChildObjectCount; objectIdx++) + { + // create a child AABBTree with the partition and add it to AABBTreeVec + int partitionIndex = matData.partitionIndexes.at(addedObjectCount + objectIdx); + CollisionPartition* partition = &clipMap->partitions[partitionIndex]; + vec3_t childMins; + vec3_t childMaxs; + calculatePartitionAABB(clipMap, partition, childMins, childMaxs); + + CollisionAabbTree childAABBTree; + childAABBTree.materialIndex = static_cast(matData.materialIndex); + childAABBTree.childCount = 0; + childAABBTree.u.partitionIndex = partitionIndex; + childAABBTree.origin = BSPUtil::calcMiddleOfAABB(childMins, childMaxs); + childAABBTree.halfSize = BSPUtil::calcHalfSizeOfAABB(childMins, childMaxs); + AABBTreeVec.emplace_back(childAABBTree); + + // update the parent AABB with the child AABB + if (objectIdx == 0) + { + parentMins = childMins; + parentMaxs = childMaxs; + } + else + BSPUtil::updateAABB(childMins, childMaxs, parentMins, parentMaxs); + } + + CollisionAabbTree parentAABB; + parentAABB.materialIndex = static_cast(matData.materialIndex); + parentAABB.origin = BSPUtil::calcMiddleOfAABB(parentMins, parentMaxs); + parentAABB.halfSize = BSPUtil::calcHalfSizeOfAABB(parentMins, parentMaxs); + parentAABB.childCount = static_cast(currChildObjectCount); + parentAABB.u.firstChildIndex = static_cast(childObjectStartIndex); + AABBTreeVec.at(parentAABBArrayIndex + parentIdx) = parentAABB; + + addedObjectCount += currChildObjectCount; + } + + parentAABBArrayIndex += parentCount; + } + + *out_treeContents = 0; + for (auto& matData : uniqueMaterials) + *out_treeContents |= clipMap->info.materials[matData.materialIndex].contentFlags; + } + + constexpr vec3_t normalX = {1.0f, 0.0f, 0.0f}; + constexpr vec3_t normalY = {0.0f, 1.0f, 0.0f}; + constexpr vec3_t normalZ = {0.0f, 0.0f, 1.0f}; + + // returns the index of the node/leaf parsed by the function + // Nodes are indexed by their index in the node array + // Leafs are indexed by (-1 - ) + // See https://developer.valvesoftware.com/wiki/BSP_(Source) + int16_t ClipMapLinker::loadBSPNode(clipMap_t* clipMap, BSPTree* tree, bool isRoot) + { + if (tree->isLeaf) + { + if (isRoot) + { + cplane_s plane; + plane.dist = 0; + plane.normal = normalX; + plane.type = 0; + plane.signbits = 0; + plane.pad[0] = 0; + plane.pad[1] = 0; + planeVec.emplace_back(plane); + + cNode_t node; + node.plane = nullptr; // initalised after the BSP tree has been loaded + node.children[0] = -1; + node.children[1] = -1; + nodeVec.emplace_back(node); + } + + cLeaf_s leaf; + + leaf.cluster = 0; // always use cluster 0 + + // no brushes used so data is set to 0 + leaf.brushContents = 0; + leaf.mins = {}; + leaf.maxs = {}; + leaf.leafBrushNode = 0; + + if (tree->leaf->getObjectCount() > 0) + { + size_t parentCount = 0; + size_t parentStartIndex = 0; + int treeContents = 0; + std::vector partitions; + for (size_t objIdx = 0; objIdx < tree->leaf->getObjectCount(); objIdx++) + partitions.emplace_back(tree->leaf->getObject(objIdx)->partitionIndex); + addAABBTreeFromPartitions(clipMap, partitions, &parentCount, &parentStartIndex, &treeContents); + leaf.collAabbCount = static_cast(parentCount); + leaf.firstCollAabbIndex = static_cast(parentStartIndex); + leaf.terrainContents = treeContents; + } + else + { + leaf.firstCollAabbIndex = 0; + leaf.collAabbCount = 0; + leaf.terrainContents = 0; + } + + uint16_t leafIndex = static_cast(leafVec.size()); + leafVec.emplace_back(leaf); + + return -1 - leafIndex; + } + else + { + cplane_s plane; + plane.dist = tree->node->distance; + if (tree->node->axis == AXIS_X) + { + plane.normal = normalX; + plane.type = 0; + } + else if (tree->node->axis == AXIS_Y) + { + plane.normal = normalY; + plane.type = 1; + } + else // tree->node->axis == AXIS_Z + { + plane.normal = normalZ; + plane.type = 2; + } + + plane.signbits = 0; + if (plane.normal.x < 0.0f) + plane.signbits |= 1; + if (plane.normal.y < 0.0f) + plane.signbits |= 2; + if (plane.normal.z < 0.0f) + plane.signbits |= 4; + + plane.pad[0] = 0; + plane.pad[1] = 0; + + planeVec.emplace_back(plane); + + // The recursion of adding the children through loadBSPNode means the parent node needs to be added before the chilren are loaded + size_t nodeIndex = nodeVec.size(); + nodeVec.emplace_back(); + + cNode_t node; + node.plane = nullptr; // initalised after the BSP tree has been loaded + node.children[0] = loadBSPNode(clipMap, tree->node->front.get(), false); + node.children[1] = loadBSPNode(clipMap, tree->node->back.get(), false); + + nodeVec.at(nodeIndex) = node; + + return static_cast(nodeIndex); + } + } + + bool ClipMapLinker::loadBSPTree(clipMap_t* clipMap, BSPData* bsp) + { + vec3_t worldMins; + vec3_t worldMaxs; + for (unsigned int vertIdx = 0; vertIdx < clipMap->vertCount; vertIdx++) + { + vec3_t vertex = clipMap->verts[vertIdx]; + // initalise AABB with the first vertex + if (vertIdx == 0) + { + worldMins = vertex; + worldMaxs = vertex; + } + BSPUtil::updateAABBWithPoint(vertex, worldMins, worldMaxs); + } + std::unique_ptr tree = std::make_unique(worldMins.x, worldMins.y, worldMins.z, worldMaxs.x, worldMaxs.y, worldMaxs.z, 0); + + for (size_t surfIdx = 0; surfIdx < bsp->colWorld.staticSurfaces.size(); surfIdx++) // only add static surfaces + { + ColSurface& colSurface = collisionSurfaceVec.at(surfIdx); + for (size_t partitionIdx = 0; partitionIdx < colSurface.partitionCount; partitionIdx++) + { + vec3_t partitionMins; + vec3_t partitionMaxs; + size_t clipMapPartitionIdx = colSurface.partitionStartIndex + partitionIdx; + CollisionPartition* partition = &clipMap->partitions[clipMapPartitionIdx]; + calculatePartitionAABB(clipMap, partition, partitionMins, partitionMaxs); + std::shared_ptr currObject = std::make_shared( + partitionMins.x, partitionMins.y, partitionMins.z, partitionMaxs.x, partitionMaxs.y, partitionMaxs.z, clipMapPartitionIdx); + tree->addObjectToTree(std::move(currObject)); + } + } + + // load planes, nodes, leafs, and AABB trees + loadBSPNode(clipMap, tree.get(), true); + + con::info("Highest AABB tree partition count: {}", highestPartitionCountForAABB); + + return true; + } + + bool ClipMapLinker::loadPartitions(clipMap_t* clipMap, BSPData* bsp) + { + // due to tris using uint16_t as the type for indexing the vert array, + // any vertex count over the uint16_t max means the vertices above the uint16_t max can't be indexed + if (static_cast(bsp->colWorld.vertices.size()) > UINT16_MAX) + { + con::error("ERROR: collision vertex count {} exceeds the maximum number: {}!\n", bsp->colWorld.vertices.size(), UINT16_MAX); + return false; + } + + clipMap->vertCount = static_cast(bsp->colWorld.vertices.size()); + clipMap->verts = m_memory.Alloc(clipMap->vertCount); + for (unsigned int vertIdx = 0; vertIdx < clipMap->vertCount; vertIdx++) + clipMap->verts[vertIdx] = bsp->colWorld.vertices[vertIdx].pos; + + size_t staticSurfaceCount = bsp->colWorld.staticSurfaces.size(); + size_t scriptSurfaceCount = bsp->colWorld.scriptSurfaces.size(); + size_t totalSurfaceCount = staticSurfaceCount + scriptSurfaceCount; + + std::vector triIndexVec; + std::vector partitionVec; + std::vector uniqueIndicesVec; + for (size_t surfIdx = 0; surfIdx < totalSurfaceCount; surfIdx++) + { + BSPSurface surface; + if (surfIdx < staticSurfaceCount) + surface = bsp->colWorld.staticSurfaces.at(surfIdx); + else + surface = bsp->colWorld.scriptSurfaces.at(surfIdx - staticSurfaceCount); + + int indexOfFirstIndex = surface.indexOfFirstIndex; + int indexOfFirstTri = surface.indexOfFirstIndex / 3; + int indexOfFirstVertex = surface.indexOfFirstVertex; + + ColSurface colSurface; + colSurface.materialIndex = surface.materialIndex; + colSurface.partitionCount = surface.triCount; + colSurface.partitionStartIndex = partitionVec.size(); + collisionSurfaceVec.emplace_back(colSurface); + + for (int triIdx = 0; triIdx < surface.triCount; triIdx++) + { + // The clipmap index buffer has a unique index for each vertex in the world, compared to the gfxworld's + // index buffer having a unique index for each vertex on a surface. This code converts gfxworld indices to clipmap indices. + int triIndex0 = bsp->colWorld.indices[indexOfFirstIndex + (triIdx * 3) + 0] + indexOfFirstVertex; + int triIndex1 = bsp->colWorld.indices[indexOfFirstIndex + (triIdx * 3) + 1] + indexOfFirstVertex; + int triIndex2 = bsp->colWorld.indices[indexOfFirstIndex + (triIdx * 3) + 2] + indexOfFirstVertex; + triIndexVec.emplace_back(triIndex0); + triIndexVec.emplace_back(triIndex1); + triIndexVec.emplace_back(triIndex2); + + // partitions are "containers" for vertices. BSP tree leafs contain a list of these partitions to determine the collision within a leaf. + // partitions are made for each triangle, not one for each surface. + // one for each surface causes physics bugs, as the entire bounding box is considered solid instead of the surface itself (for some reason). + // so a partition is made for each triangle which removes the physics bugs but likely makes the game run slower + CollisionPartition partition; + partition.triCount = 1; + partition.firstTri = indexOfFirstTri + triIdx; + + partition.nuinds = 3; + partition.fuind = static_cast(uniqueIndicesVec.size()); + + // All tri indices are unique since there is only one tri per partition + int indexOfTriIndex = partition.firstTri * 3; + uniqueIndicesVec.emplace_back(triIndex0); + uniqueIndicesVec.emplace_back(triIndex1); + uniqueIndicesVec.emplace_back(triIndex2); + + partitionVec.emplace_back(partition); + partitionToColSurfaceMap.emplace_back(collisionSurfaceVec.size() - 1); // -1 as the colSurface has already been added + } + } + + clipMap->triCount = static_cast(triIndexVec.size() / 3); + clipMap->triIndices = reinterpret_cast(m_memory.Alloc(triIndexVec.size())); + memcpy(clipMap->triIndices, &triIndexVec[0], sizeof(uint16_t) * triIndexVec.size()); + + clipMap->partitionCount = static_cast(partitionVec.size()); + clipMap->partitions = m_memory.Alloc(clipMap->partitionCount); + memcpy(clipMap->partitions, partitionVec.data(), sizeof(CollisionPartition) * partitionVec.size()); + + clipMap->info.nuinds = static_cast(uniqueIndicesVec.size()); + clipMap->info.uinds = m_memory.Alloc(uniqueIndicesVec.size()); + memcpy(clipMap->info.uinds, uniqueIndicesVec.data(), sizeof(uint16_t) * uniqueIndicesVec.size()); + + return true; + } + + bool ClipMapLinker::loadSubModelCollision(clipMap_t* clipMap, BSPData* bsp) + { + // Submodels are used for the world and map ent collision (triggers, bomb zones, etc) + clipMap->numSubModels = static_cast(bsp->models.size() + 1); + clipMap->cmodels = m_memory.Alloc(clipMap->numSubModels); + + if (clipMap->numSubModels > 0x3FFF) + { + con::error("ERROR: There are more than 0x3FFF entity brushmodels."); + return false; + } + // first model is always the world model + for (unsigned int vertIdx = 0; vertIdx < clipMap->vertCount; vertIdx++) + { + vec3_t vertex = clipMap->verts[vertIdx]; + if (vertIdx == 0) + { + clipMap->cmodels[0].mins = vertex; + clipMap->cmodels[0].maxs = vertex; + } + else + BSPUtil::updateAABBWithPoint(vertex, clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs); + } + clipMap->cmodels[0].radius = BSPUtil::distBetweenPoints(clipMap->cmodels[0].mins, clipMap->cmodels[0].maxs) / 2; + // The world sub model has no data apart from the bounds + clipMap->cmodels[0].leaf.firstCollAabbIndex = 0; + clipMap->cmodels[0].leaf.collAabbCount = 0; + clipMap->cmodels[0].leaf.brushContents = 0; + clipMap->cmodels[0].leaf.terrainContents = 0; + clipMap->cmodels[0].leaf.mins.x = 0.0f; + clipMap->cmodels[0].leaf.mins.y = 0.0f; + clipMap->cmodels[0].leaf.mins.z = 0.0f; + clipMap->cmodels[0].leaf.maxs.x = 0.0f; + clipMap->cmodels[0].leaf.maxs.y = 0.0f; + clipMap->cmodels[0].leaf.maxs.z = 0.0f; + clipMap->cmodels[0].leaf.leafBrushNode = 0; + clipMap->cmodels[0].leaf.cluster = 0; + clipMap->cmodels[0].info = nullptr; + + for (size_t modelIdx = 0; modelIdx < bsp->models.size(); modelIdx++) + { + auto clipModel = &clipMap->cmodels[modelIdx + 1]; + auto& bspModel = bsp->models.at(modelIdx); + + if (bspModel.isGfxModel) + { + memset(clipModel, 0, sizeof(cmodel_t)); + continue; + } + + if (bspModel.surfaceCount != 0) + { + std::vector partitionIndexes; + for (size_t surfIdx = 0; surfIdx < bspModel.surfaceCount; surfIdx++) + { + ColSurface& surf = collisionSurfaceVec.at(bsp->colWorld.staticSurfaces.size() + bspModel.surfaceIndex + surfIdx); + for (size_t partitionIdx = 0; partitionIdx < surf.partitionCount; partitionIdx++) + { + size_t clipMapPartitionIndex = surf.partitionStartIndex + partitionIdx; + partitionIndexes.emplace_back(clipMapPartitionIndex); + vec3_t mins; + vec3_t maxs; + calculatePartitionAABB(clipMap, &clipMap->partitions[clipMapPartitionIndex], mins, maxs); + if (surfIdx == 0 && partitionIdx == 0) + { + clipModel->mins = mins; + clipModel->maxs = maxs; + } + else + BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs); + } + } + + int terrainContents = 0; + size_t firstCollAabbIndex = 0; + size_t collAabbCount = 0; + addAABBTreeFromPartitions(clipMap, partitionIndexes, &collAabbCount, &firstCollAabbIndex, &terrainContents); + clipModel->leaf.terrainContents = terrainContents; + clipModel->leaf.firstCollAabbIndex = static_cast(firstCollAabbIndex); + clipModel->leaf.collAabbCount = static_cast(collAabbCount); + } + else + { + clipModel->leaf.terrainContents = 0; + clipModel->leaf.firstCollAabbIndex = 0; + clipModel->leaf.collAabbCount = 0; + } + + if (bspModel.hasBrush) + { + BSPBoxBrush& bspBrush = bsp->colWorld.scriptBoxBrushes.at(bspModel.brushIndex); + vec3_t mins = {}; + vec3_t maxs = {}; + for (size_t vertIdx = 0; vertIdx < bspBrush.vertexCount; vertIdx++) + { + vec3_t* vertex = &clipMap->info.brushVerts[bspBrush.vertexIndex + vertIdx]; + if (vertIdx == 0) + { + mins = *vertex; + maxs = *vertex; + } + else + BSPUtil::updateAABBWithPoint(*vertex, mins, maxs); + } + + clipModel->leaf.mins = mins; + clipModel->leaf.maxs = maxs; + clipModel->leaf.brushContents = bspBrush.contentFlags; + clipModel->leaf.leafBrushNode = static_cast(brushNodeVec.size()); + assert(clipModel->leaf.leafBrushNode != 0); + + cLeafBrushNode_s brushNode; + brushNode.axis = 0; + brushNode.contents = bspBrush.contentFlags; + brushNode.leafBrushCount = 1; + brushNode.data.leaf.brushes = m_memory.Alloc(1); + brushNode.data.leaf.brushes[0] = static_cast(brushVec.size()); + brushNodeVec.emplace_back(brushNode); + + cbrush_array_t brush; + memset(&brush, 0, sizeof(cbrush_array_t)); // if not sides or verts are given, the mins/maxs are used instead + brush.numverts = static_cast(bspBrush.vertexCount); + brush.verts = &clipMap->info.brushVerts[bspBrush.vertexIndex]; + brush.contents = bspBrush.contentFlags; + brush.mins = mins; + brush.maxs = maxs; + brush.axial_cflags[0][0] = bspBrush.contentFlags; + brush.axial_cflags[0][1] = bspBrush.contentFlags; + brush.axial_cflags[0][2] = bspBrush.contentFlags; + brush.axial_cflags[1][0] = bspBrush.contentFlags; + brush.axial_cflags[1][1] = bspBrush.contentFlags; + brush.axial_cflags[1][2] = bspBrush.contentFlags; + brush.axial_sflags[0][0] = bspBrush.surfaceFlags; + brush.axial_sflags[0][1] = bspBrush.surfaceFlags; + brush.axial_sflags[0][2] = bspBrush.surfaceFlags; + brush.axial_sflags[1][0] = bspBrush.surfaceFlags; + brush.axial_sflags[1][1] = bspBrush.surfaceFlags; + brush.axial_sflags[1][2] = bspBrush.surfaceFlags; + brushVec.emplace_back(brush); + + if (bspModel.surfaceCount != 0) + BSPUtil::updateAABB(mins, maxs, clipModel->mins, clipModel->maxs); + else + { + clipModel->mins = mins; + clipModel->maxs = maxs; + } + } + else + { + clipModel->leaf.brushContents = 0; + clipModel->leaf.mins.x = 0.0f; + clipModel->leaf.mins.y = 0.0f; + clipModel->leaf.mins.z = 0.0f; + clipModel->leaf.maxs.x = 0.0f; + clipModel->leaf.maxs.y = 0.0f; + clipModel->leaf.maxs.z = 0.0f; + clipModel->leaf.leafBrushNode = 0; + } + + if (bspModel.surfaceCount == 0 && !bspModel.hasBrush) + { + clipModel->mins = {0.0f, 0.0f, 0.0f}; + clipModel->maxs = {0.0f, 0.0f, 0.0f}; + clipModel->radius = 0.0f; + } + else + clipModel->radius = BSPUtil::distBetweenPoints(clipModel->mins, clipModel->maxs) / 2; + clipModel->leaf.cluster = 0; + clipModel->info = nullptr; + } + + return true; + } + + bool ClipMapLinker::loadWorldCollision(clipMap_t* clipMap, BSPData* bsp) + { + // unused brush data + clipMap->info.numBrushSides = 0; + clipMap->info.brushsides = nullptr; + clipMap->info.numLeafBrushes = 0; + clipMap->info.leafbrushes = nullptr; + clipMap->info.brushBounds = nullptr; + clipMap->info.brushContents = nullptr; + + // first brush node is always empty + cLeafBrushNode_s tempNode; + memset(&tempNode, 0, sizeof(cLeafBrushNode_s)); + brushNodeVec.emplace_back(tempNode); + + clipMap->info.numBrushVerts = static_cast(bsp->colWorld.boxBrushVerts.size()); + clipMap->info.brushVerts = m_memory.Alloc(bsp->colWorld.boxBrushVerts.size()); + memcpy(clipMap->info.brushVerts, bsp->colWorld.boxBrushVerts.data(), sizeof(vec3_t) * bsp->colWorld.boxBrushVerts.size()); + + // load verts, tris, uinds and partitions + if (!loadPartitions(clipMap, bsp)) + return false; + + if (!loadBSPTree(clipMap, bsp)) + return false; + + if (!loadSubModelCollision(clipMap, bsp)) // requires tri verts + return false; + + if (nodeVec.size() > 0xFFFF) + { + con::error("exceeded 0xFFFF nodes in clipmap"); + return false; + } + if (leafVec.size() > 0xFFFF) + { + con::error("exceeded 0xFFFF leafs in clipmap"); + return false; + } + if (AABBTreeVec.size() > 0xFFFF) + { + con::error("exceeded 0xFFFF AABBTrees in clipmap"); + return false; + } + if (brushVec.size() > 0xFFFF) + { + con::error("exceeded 0xFFFF brushes in clipmap"); + return false; + } + + clipMap->info.planeCount = static_cast(planeVec.size()); + clipMap->info.planes = m_memory.Alloc(planeVec.size()); + memcpy(clipMap->info.planes, planeVec.data(), sizeof(cplane_s) * planeVec.size()); + + clipMap->numNodes = static_cast(nodeVec.size()); + clipMap->nodes = m_memory.Alloc(nodeVec.size()); + memcpy(clipMap->nodes, nodeVec.data(), sizeof(cNode_t) * nodeVec.size()); + + clipMap->numLeafs = static_cast(leafVec.size()); + clipMap->leafs = m_memory.Alloc(leafVec.size()); + memcpy(clipMap->leafs, leafVec.data(), sizeof(cLeaf_s) * leafVec.size()); + + clipMap->aabbTreeCount = static_cast(AABBTreeVec.size()); + clipMap->aabbTrees = m_memory.Alloc(AABBTreeVec.size()); + memcpy(clipMap->aabbTrees, AABBTreeVec.data(), sizeof(CollisionAabbTree) * AABBTreeVec.size()); + + clipMap->info.leafbrushNodesCount = static_cast(brushNodeVec.size()); + clipMap->info.leafbrushNodes = m_memory.Alloc(brushNodeVec.size()); + memcpy(clipMap->info.leafbrushNodes, brushNodeVec.data(), sizeof(cLeafBrushNode_s) * brushNodeVec.size()); + + clipMap->info.numBrushes = static_cast(brushVec.size()); + clipMap->info.brushes = m_memory.Alloc(brushVec.size()); + memcpy(clipMap->info.brushes, brushVec.data(), sizeof(cbrush_array_t) * brushVec.size()); + + // The plane of each node have the same index + for (size_t nodeIdx = 0; nodeIdx < nodeVec.size(); nodeIdx++) + clipMap->nodes[nodeIdx].plane = &clipMap->info.planes[nodeIdx]; + + return true; + } + + bool ClipMapLinker::loadMaterials(clipMap_t* clipMap, BSPData* bsp) + { + // Clipmap materials define the properties of a material (bullet penetration, no collision, water, etc) + + if (bsp->colWorld.materials.size() > UINT16_MAX) + { + con::error("Collision map exceeds 0xFFFF materials"); + return false; + } + + clipMap->info.numMaterials = static_cast(bsp->colWorld.materials.size()); + clipMap->info.materials = m_memory.Alloc(clipMap->info.numMaterials); + for (size_t matIdx = 0; matIdx < bsp->colWorld.materials.size(); matIdx++) + { + ClipMaterial* clipMat = &clipMap->info.materials[matIdx]; + BSPMaterial bspMat = bsp->colWorld.materials.at(matIdx); + + clipMat->name = m_memory.Dup(bspMat.materialName.c_str()); + clipMat->contentFlags = bspMat.contentFlags; + clipMat->surfaceFlags = bspMat.surfaceFlags; + } + + return true; + } + + clipMap_t* ClipMapLinker::linkClipMap(BSPData* bsp) + { + clipMap_t* clipMap = m_memory.Alloc(); + clipMap->name = m_memory.Dup(bsp->bspName.c_str()); + + clipMap->isInUse = true; + clipMap->checksum = 0; + clipMap->pInfo = nullptr; + + std::string mapEntsName = bsp->bspName; + auto mapEntsAsset = m_context.LoadDependency(mapEntsName); + assert(mapEntsAsset != nullptr); + clipMap->mapEnts = mapEntsAsset->Asset(); + + loadBoxData(clipMap); + + loadVisibility(clipMap); + + loadRopesAndConstraints(clipMap); + + loadDynEnts(clipMap); + + if (!loadXModelCollision(clipMap, bsp)) + return nullptr; + + if (!loadMaterials(clipMap, bsp)) + return nullptr; + + if (!loadWorldCollision(clipMap, bsp)) // requires materials + return nullptr; + + // set all edges to walkable + // might do weird stuff on walls, but from testing doesnt seem to do anything + int walkableEdgeSize = (3 * clipMap->triCount + 31) / 32 * 4; + clipMap->triEdgeIsWalkable = m_memory.Alloc(walkableEdgeSize); + memset(clipMap->triEdgeIsWalkable, 1, walkableEdgeSize * sizeof(char)); + + return clipMap; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.h new file mode 100644 index 000000000..5b921355c --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/ClipMapLinker.h @@ -0,0 +1,56 @@ +#pragma once + +#include "../BSP.h" +#include "../BSPCalculation.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + struct ColSurface + { + size_t materialIndex; + size_t partitionCount; + size_t partitionStartIndex; + }; + + class ClipMapLinker + { + public: + ClipMapLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + clipMap_t* linkClipMap(BSPData* bsp); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + + void loadBoxData(clipMap_t* clipMap); + void loadVisibility(clipMap_t* clipMap); + void loadDynEnts(clipMap_t* clipMap); + void loadRopesAndConstraints(clipMap_t* clipMap); + bool loadSubModelCollision(clipMap_t* clipMap, BSPData* bsp); + bool loadXModelCollision(clipMap_t* clipMap, BSPData* bsp); + + std::vector planeVec; + std::vector nodeVec; + std::vector leafVec; + std::vector AABBTreeVec; + size_t highestPartitionCountForAABB = 0; + + std::vector brushNodeVec; + std::vector brushVec; + + std::vector collisionSurfaceVec; + std::vector partitionToColSurfaceMap; + + void addAABBTreeFromPartitions( + clipMap_t* clipMap, std::vector& partitions, size_t* out_parentCount, size_t* out_parentStartIndex, int* out_treeContents); + int16_t loadBSPNode(clipMap_t* clipMap, BSPTree* tree, bool isRoot); + bool loadBSPTree(clipMap_t* clipMap, BSPData* bsp); + bool loadPartitions(clipMap_t* clipMap, BSPData* bsp); + bool loadWorldCollision(clipMap_t* clipMap, BSPData* bsp); + bool loadMaterials(clipMap_t* clipMap, BSPData* bsp); + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.cpp new file mode 100644 index 000000000..a2c29f075 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.cpp @@ -0,0 +1,120 @@ +#include "ComWorldLinker.h" + +#include + +namespace +{ + constexpr const char* DEFAULT_LIGHTDEF_NAME = "white_light"; + constexpr short LIGHT_CULLDIST = 10000; +} // namespace + +namespace BSP +{ + ComWorldLinker::ComWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + bool ComWorldLinker::createLightDefs() + { + T6::GfxLightDef* lightDef2d = m_memory.Alloc(); + lightDef2d->name = m_memory.Dup(DEFAULT_LIGHTDEF_NAME); + lightDef2d->lmapLookupStart = 0; // always 0 + lightDef2d->attenuation.samplerState = 115; // always 115 + auto image2dAsset = m_context.LoadDependency(",$white"); + if (image2dAsset == nullptr) + return false; + lightDef2d->attenuation.image = image2dAsset->Asset(); + m_context.AddAsset(lightDef2d->name, lightDef2d); + + return true; + } + + ComWorld* ComWorldLinker::linkComWorld(BSPData* bsp) + { + ComWorld* comWorld = m_memory.Alloc(); + comWorld->name = m_memory.Dup(bsp->bspName.c_str()); + comWorld->isInUse = 1; + + // first two lights are the empty light and the sun light. + size_t totalLightCount = bsp->lights.size() + BSP_DEFAULT_LIGHT_COUNT; + comWorld->primaryLightCount = static_cast(totalLightCount); + comWorld->primaryLights = m_memory.Alloc(totalLightCount); + + if (!createLightDefs()) + { + con::error("Unable to create lightdef assets."); + return nullptr; + } + + for (size_t lightIdx = 0; lightIdx < totalLightCount; lightIdx++) + { + ComPrimaryLight* light = &comWorld->primaryLights[lightIdx]; + if (lightIdx == EMPTY_LIGHT_INDEX) + continue; // first (empty) light has no data + else if (lightIdx == SUN_LIGHT_INDEX) + { + BSPLight* bspLight = &bsp->sunlight; + + light->type = GFX_LIGHT_TYPE_DIR; + light->dir.x = bspLight->direction.x; + light->dir.y = bspLight->direction.y; + light->dir.z = bspLight->direction.z; + light->diffuseColor.x = bspLight->colour.x; + light->diffuseColor.y = bspLight->colour.y; + light->diffuseColor.z = bspLight->colour.z; + light->color.x = bspLight->colour.x; + light->color.y = bspLight->colour.y; + light->color.z = bspLight->colour.z; + } + else + { + BSPLight* bspLight = &bsp->lights.at(lightIdx - BSP_DEFAULT_LIGHT_COUNT); + + light->type = GFX_LIGHT_TYPE_SPOT; + + light->color.x = bspLight->colour.x; + light->color.y = bspLight->colour.y; + light->color.z = bspLight->colour.z; + light->diffuseColor.x = bspLight->colour.x; + light->diffuseColor.y = bspLight->colour.y; + light->diffuseColor.z = bspLight->colour.z; + + light->cosHalfFovInner = cosf(bspLight->innerConeAngle); + light->cosHalfFovOuter = cosf(bspLight->outerConeAngle); + light->cosHalfFovExpanded = cosf(bspLight->outerConeAngle); + + light->dir.x = bspLight->direction.x; + light->dir.y = bspLight->direction.y; + light->dir.z = bspLight->direction.z; + + light->falloff.y = bspLight->range; + light->radius = bspLight->range; + light->mipDistance = bspLight->range; + + light->origin.x = bspLight->pos.x; + light->origin.y = bspLight->pos.y; + light->origin.z = bspLight->pos.z; + + light->dAttenuation = bspLight->intensity; + + // I think AABB controls the lens shape of the light + light->aAbB.x = 0.75f; + light->aAbB.y = 1.0f; + light->aAbB.z = 0.75f; + light->aAbB.w = 1.0f; + + light->cullDist = LIGHT_CULLDIST; + light->defName = DEFAULT_LIGHTDEF_NAME; // all lights that aren't the sunlight or default light need their own GfxLightDef asset + light->rotationLimit = 1.0f; // 1.0f - doesn't rotate, -1.0f - unclamped rotation + light->translationLimit = 0.0f; // 0.0f - doesn't translate, above 0.0f - distance per game update translated + light->roundness = 1.0f; // 0.0f - light is a square. 1.0f - light is a circle + light->canUseShadowMap = 1; // light does not show up with this set to 0 + } + } + + return comWorld; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.h new file mode 100644 index 000000000..25ae1a037 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/ComWorldLinker.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class ComWorldLinker + { + public: + ComWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + ComWorld* linkComWorld(BSPData* bsp); + bool createLightDefs(); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.cpp new file mode 100644 index 000000000..6852b1aaf --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.cpp @@ -0,0 +1,34 @@ +#include "GameWorldMpLinker.h" + +namespace BSP +{ + GameWorldMpLinker::GameWorldMpLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + GameWorldMp* GameWorldMpLinker::linkGameWorldMp(BSPData* bsp) + { + GameWorldMp* gameWorldMp = m_memory.Alloc(); + + gameWorldMp->name = m_memory.Dup(bsp->bspName.c_str()); + + gameWorldMp->path.nodeCount = 0; + gameWorldMp->path.originalNodeCount = 0; + gameWorldMp->path.visBytes = 0; + gameWorldMp->path.smoothBytes = 0; + gameWorldMp->path.nodeTreeCount = 0; + + // The game has 128 empty nodes allocated + int extraNodeCount = gameWorldMp->path.nodeCount + 128; + gameWorldMp->path.nodes = m_memory.Alloc(extraNodeCount); + gameWorldMp->path.basenodes = m_memory.Alloc(extraNodeCount); + gameWorldMp->path.pathVis = nullptr; + gameWorldMp->path.smoothCache = nullptr; + gameWorldMp->path.nodeTree = nullptr; + + return gameWorldMp; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.h new file mode 100644 index 000000000..69c8bef59 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/GameWorldMpLinker.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class GameWorldMpLinker + { + public: + GameWorldMpLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + GameWorldMp* linkGameWorldMp(BSPData* bsp); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.cpp new file mode 100644 index 000000000..ade30d93f --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.cpp @@ -0,0 +1,884 @@ +#include "GfxWorldLinker.h" + +#include "../BSPCalculation.h" +#include "../BSPUtil.h" +#include "Utils/Pack.h" + +namespace +{ + unsigned int packLmapCoord(float X, float Y) + { + float XStep = X * 65535.0f; + float YStep = Y * 65535.0f; + + unsigned int result = 0; + result |= (unsigned int)XStep; + result &= 0xFFFF; + result |= (unsigned int)YStep << 16; + result &= 0xFFFFFFFF; + + return result; + } + + constexpr const char* DEFAULT_IMAGE_NAME = ",mc/lambert1"; + constexpr char DEFAULT_PRIMARYLIGHT_INDEX = 1; // max 254 + constexpr char DEFAULT_LIGHTMAP_INDEX = 0; // max 30 + constexpr char DEFAULT_RPROBE_INDEX = 0; // max 254 + constexpr float XMODEL_CULL_DIST = 10000.0f; + constexpr char DEFAULT_LIGHTGRID_COLOUR = 32; +} // namespace + +namespace BSP +{ + GfxWorldLinker::GfxWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + void GfxWorldLinker::loadDrawData(BSPData* bsp, GfxWorld* gfxWorld) + { + size_t vertexCount = bsp->gfxWorld.vertices.size(); + gfxWorld->draw.vertexCount = static_cast(vertexCount); + gfxWorld->draw.vertexDataSize0 = static_cast(vertexCount * sizeof(GfxPackedWorldVertex)); + GfxPackedWorldVertex* vertexBuffer = m_memory.Alloc(vertexCount); + for (size_t vertIdx = 0; vertIdx < vertexCount; vertIdx++) + { + BSPVertex& bspVertex = bsp->gfxWorld.vertices.at(vertIdx); + GfxPackedWorldVertex* gfxVertex = &vertexBuffer[vertIdx]; + + gfxVertex->xyz = bspVertex.pos; + gfxVertex->color.packed = pack32::Vec4PackGfxColor(bspVertex.color.v); + gfxVertex->texCoord.packed = pack32::Vec2PackTexCoordsUV(bspVertex.texCoord.v); + gfxVertex->normal.packed = pack32::Vec3PackUnitVecThirdBased(bspVertex.normal.v); + gfxVertex->tangent.packed = pack32::Vec3PackUnitVecThirdBased(bspVertex.tangent.v); + gfxVertex->binormalSign = bspVertex.binormal.v[0] > 0.0f ? 1.0f : -1.0f; + gfxVertex->lmapCoord.packed = packLmapCoord(0.0f, 0.0f); + } + gfxWorld->draw.vd0.data = reinterpret_cast(vertexBuffer); + + // vd1 is unused but still needs to be initialised + // the data type varies and 0x20 is enough for all types + gfxWorld->draw.vertexDataSize1 = 0x20; + gfxWorld->draw.vd1.data = m_memory.Alloc(gfxWorld->draw.vertexDataSize1); + + size_t indexCount = bsp->gfxWorld.indices.size(); + assert(indexCount % 3 == 0); + gfxWorld->draw.indexCount = static_cast(indexCount); + gfxWorld->draw.indices = m_memory.Alloc(indexCount); // overflow checked in bspcreator + static_assert(sizeof(bsp->gfxWorld.indices.data()[0]) == sizeof(uint16_t)); + memcpy(gfxWorld->draw.indices, bsp->gfxWorld.indices.data(), sizeof(uint16_t) * indexCount); + } + + bool GfxWorldLinker::loadMapSurfaces(BSPData* bsp, GfxWorld* gfxWorld) + { + loadDrawData(bsp, gfxWorld); + + size_t staticSurfaceCount = bsp->gfxWorld.staticSurfaces.size(); + size_t scriptSurfaceCount = bsp->gfxWorld.scriptSurfaces.size(); + size_t totalSurfaceCount = staticSurfaceCount + scriptSurfaceCount; + + if (staticSurfaceCount > 0xffff) + { + con::error("There are more than 65535 Static surfaces. count: {}", staticSurfaceCount); + return false; + } + + // Static surfaces go first in the array then script surfaces after + gfxWorld->surfaceCount = static_cast(totalSurfaceCount); + gfxWorld->dpvs.staticSurfaceCount = static_cast(staticSurfaceCount); + gfxWorld->dpvs.surfaces = m_memory.Alloc(totalSurfaceCount); + for (size_t surfIdx = 0; surfIdx < totalSurfaceCount; surfIdx++) + { + BSPSurface bspSurface; + if (surfIdx < staticSurfaceCount) + bspSurface = bsp->gfxWorld.staticSurfaces.at(surfIdx); + else + bspSurface = bsp->gfxWorld.scriptSurfaces.at(surfIdx - staticSurfaceCount); + GfxSurface* gfxSurface = &gfxWorld->dpvs.surfaces[surfIdx]; + + gfxSurface->tris.triCount = bspSurface.triCount; // overflow checked in bspcreator + gfxSurface->tris.baseIndex = bspSurface.indexOfFirstIndex; + gfxSurface->tris.vertexCount = bspSurface.vertexCount; // overflow checked in bspcreator + gfxSurface->tris.firstVertex = bspSurface.indexOfFirstVertex; + + gfxSurface->tris.vertexDataOffset0 = bspSurface.indexOfFirstVertex * sizeof(GfxPackedWorldVertex); + gfxSurface->tris.vertexDataOffset1 = 0; // vd1 is unused + + BSPMaterial bspMaterial = bsp->gfxWorld.materials.at(bspSurface.materialIndex); + + std::string materialName; + if (bspMaterial.materialType == MATERIAL_TYPE_TEXTURE) + materialName = bspMaterial.materialName; + else // MATERIAL_TYPE_COLOUR + materialName = DEFAULT_IMAGE_NAME; + + auto surfMaterialAsset = m_context.LoadDependency(materialName); + if (surfMaterialAsset == nullptr) + { + surfMaterialAsset = m_context.LoadDependency(DEFAULT_IMAGE_NAME); + assert(surfMaterialAsset != nullptr); + } + gfxSurface->material = surfMaterialAsset->Asset(); + + GfxPackedWorldVertex* firstVert = reinterpret_cast(&gfxWorld->draw.vd0.data[gfxSurface->tris.vertexDataOffset0]); + gfxSurface->bounds[0] = firstVert[0].xyz; + gfxSurface->bounds[1] = firstVert[0].xyz; + for (size_t indexIdx = 0; indexIdx < static_cast(gfxSurface->tris.triCount * 3); indexIdx++) + { + uint16_t vertIndex = gfxWorld->draw.indices[gfxSurface->tris.baseIndex + indexIdx]; + BSPUtil::updateAABBWithPoint(firstVert[vertIndex].xyz, gfxSurface->bounds[0], gfxSurface->bounds[1]); + } + gfxSurface->tris.mins = gfxSurface->bounds[0]; + gfxSurface->tris.maxs = gfxSurface->bounds[1]; + + gfxSurface->flags = 0; + if ((bspMaterial.contentFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_SKY].contentFlags) != 0) + gfxSurface->flags |= GFX_SURFACE_IS_SKY; + if ((bspMaterial.contentFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NODRAW].contentFlags) != 0) + gfxSurface->flags |= GFX_SURFACE_NO_DRAW; + if ((bspMaterial.surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_ONLYCASTSHADOW].surfaceFlags) != 0) + gfxSurface->flags |= GFX_SURFACE_CASTS_SUN_SHADOW; + if ((bspMaterial.surfaceFlags & BSPFlags::surfaceTypeToFlagMap[BSPFlags::SURF_TYPE_NOCASTSHADOW].surfaceFlags) == 0) + gfxSurface->flags |= GFX_SURFACE_CASTS_SHADOW; + + gfxSurface->primaryLightIndex = DEFAULT_PRIMARYLIGHT_INDEX; + gfxSurface->lightmapIndex = DEFAULT_LIGHTMAP_INDEX; + gfxSurface->reflectionProbeIndex = DEFAULT_RPROBE_INDEX; + } + + // Some code uses Sorted surfs to index surfaces, so for simplicity keep the indexes sequential and from 0 + gfxWorld->dpvs.sortedSurfIndex = m_memory.Alloc(staticSurfaceCount); + for (size_t surfIdx = 0; surfIdx < staticSurfaceCount; surfIdx++) + gfxWorld->dpvs.sortedSurfIndex[surfIdx] = static_cast(surfIdx); + + // surface materials are written to by the game + gfxWorld->dpvs.surfaceMaterials = m_memory.Alloc(staticSurfaceCount); + + // set all surface types to lit opaque + gfxWorld->dpvs.litSurfsBegin = 0; + gfxWorld->dpvs.litSurfsEnd = static_cast(staticSurfaceCount); + gfxWorld->dpvs.emissiveOpaqueSurfsBegin = static_cast(staticSurfaceCount); + gfxWorld->dpvs.emissiveOpaqueSurfsEnd = static_cast(staticSurfaceCount); + gfxWorld->dpvs.emissiveTransSurfsBegin = static_cast(staticSurfaceCount); + gfxWorld->dpvs.emissiveTransSurfsEnd = static_cast(staticSurfaceCount); + gfxWorld->dpvs.litTransSurfsBegin = static_cast(staticSurfaceCount); + gfxWorld->dpvs.litTransSurfsEnd = static_cast(staticSurfaceCount); + + // visdata is written to by the game + // all visdata is alligned by 128 + size_t allignedSurfaceCount = BSPUtil::allignBy128(staticSurfaceCount); + gfxWorld->dpvs.surfaceVisDataCount = static_cast(allignedSurfaceCount); + gfxWorld->dpvs.surfaceVisData[0] = m_memory.Alloc(allignedSurfaceCount); + gfxWorld->dpvs.surfaceVisData[1] = m_memory.Alloc(allignedSurfaceCount); + gfxWorld->dpvs.surfaceVisData[2] = m_memory.Alloc(allignedSurfaceCount); + gfxWorld->dpvs.surfaceVisDataCameraSaved = m_memory.Alloc(allignedSurfaceCount); + gfxWorld->dpvs.surfaceCastsShadow = m_memory.Alloc(allignedSurfaceCount); + gfxWorld->dpvs.surfaceCastsSunShadow = m_memory.Alloc(allignedSurfaceCount); + + return true; + } + + bool GfxWorldLinker::loadXModels(BSPData* bsp, GfxWorld* gfxWorld) + { + size_t modelCount = bsp->gfxWorld.xmodels.size(); + gfxWorld->dpvs.smodelCount = modelCount; + gfxWorld->dpvs.smodelInsts = m_memory.Alloc(modelCount); + gfxWorld->dpvs.smodelDrawInsts = m_memory.Alloc(modelCount); + + if (modelCount == 0) + return true; + if (modelCount > 0xFFFF) + { + con::error("GFXWorld exceeded the maximum number of static xmodels (65535 max, map count: {})", modelCount); + return false; + } + + char color = DEFAULT_LIGHTGRID_COLOUR; + float sunIntensity = bsp->sunlight.intensity; + if (sunIntensity < 0.0f || sunIntensity > 20000.0f) + { + con::error("Sun intensity ({}) needs to be between 0 and 20000", sunIntensity); + return false; + } + else + { + float normalised = sunIntensity / 20000.0f; + normalised *= 255.0f; + float clean = roundf(normalised); + color = static_cast(clean); + } + + for (size_t modelIdx = 0; modelIdx < modelCount; modelIdx++) + { + auto currModel = &gfxWorld->dpvs.smodelDrawInsts[modelIdx]; + auto currModelInst = &gfxWorld->dpvs.smodelInsts[modelIdx]; + BSPXModel& bspModel = bsp->gfxWorld.xmodels.at(modelIdx); + + auto xModelAsset = m_context.LoadDependency(bspModel.name); + if (xModelAsset == nullptr) + { + con::error("Unable to load xmodel asset: \"{}\"", bspModel.name); + return false; + } + else + currModel->model = (XModel*)xModelAsset->Asset(); + + currModel->placement.origin = bspModel.origin; + BSPUtil::convertQuaternionToAxis(&bspModel.rotationQuaternion, currModel->placement.axis); + currModel->placement.scale = bspModel.scale; + + currModel->flags = 0; + if (!bspModel.doesCastShadow) + currModel->flags |= STATIC_MODEL_FLAG_NO_SHADOW; + + currModel->cullDist = XMODEL_CULL_DIST; + currModel->primaryLightIndex = DEFAULT_PRIMARYLIGHT_INDEX; + currModel->reflectionProbeIndex = DEFAULT_RPROBE_INDEX; + currModel->smid = modelIdx; + + currModelInst->lightingOrigin = bspModel.origin; + + if (!xModelAsset->IsReference()) + { + BSPUtil::calculateXmodelBounds(currModel->model, currModel->placement.axis, currModelInst->mins, currModelInst->maxs); + currModelInst->mins.x = (currModelInst->mins.x * bspModel.scale) + bspModel.origin.x; + currModelInst->mins.y = (currModelInst->mins.y * bspModel.scale) + bspModel.origin.y; + currModelInst->mins.z = (currModelInst->mins.z * bspModel.scale) + bspModel.origin.z; + currModelInst->maxs.x = (currModelInst->maxs.x * bspModel.scale) + bspModel.origin.x; + currModelInst->maxs.y = (currModelInst->maxs.y * bspModel.scale) + bspModel.origin.y; + currModelInst->maxs.z = (currModelInst->maxs.z * bspModel.scale) + bspModel.origin.z; + + for (uint16_t lodIdx = 0; lodIdx < currModel->model->numLods; lodIdx++) + { + uint16_t vertCount = 0; + for (auto surfaceIndex = 0u; surfaceIndex < currModel->model->lodInfo[lodIdx].numsurfs; surfaceIndex++) + { + const auto& surface = currModel->model->surfs[surfaceIndex + currModel->model->lodInfo[lodIdx].surfIndex]; + vertCount += surface.vertCount; + } + + currModel->lmapVertexInfo[lodIdx].numLmapVertexColors = vertCount; + currModel->lmapVertexInfo[lodIdx].lmapVertexColors = m_memory.Alloc(vertCount); + memset(currModel->lmapVertexInfo[lodIdx].lmapVertexColors, color, sizeof(unsigned int) * vertCount); + } + } + else + { + if (bspModel.areBoundsValid) + { + currModelInst->mins = bspModel.mins; + currModelInst->maxs = bspModel.maxs; + } + else + { + con::warn("Unable to determine the bounds of xmodel: \"{}\"", bspModel.name); + currModelInst->mins.x = bspModel.origin.x - 1.0f; + currModelInst->mins.y = bspModel.origin.y - 1.0f; + currModelInst->mins.z = bspModel.origin.z - 1.0f; + currModelInst->maxs.x = bspModel.origin.x + 1.0f; + currModelInst->maxs.y = bspModel.origin.y + 1.0f; + currModelInst->maxs.z = bspModel.origin.z + 1.0f; + } + } + } + + // visdata is written to by the game + // all visdata is alligned by 128 + size_t allignedModelCount = BSPUtil::allignBy128(modelCount); + gfxWorld->dpvs.smodelVisDataCount = static_cast(allignedModelCount); + gfxWorld->dpvs.smodelVisData[0] = m_memory.Alloc(allignedModelCount); + gfxWorld->dpvs.smodelVisData[1] = m_memory.Alloc(allignedModelCount); + gfxWorld->dpvs.smodelVisData[2] = m_memory.Alloc(allignedModelCount); + gfxWorld->dpvs.smodelVisDataCameraSaved = m_memory.Alloc(allignedModelCount); + gfxWorld->dpvs.smodelCastsShadow = m_memory.Alloc(allignedModelCount); + for (size_t i = 0; i < modelCount; i++) + { + if ((gfxWorld->dpvs.smodelDrawInsts[i].flags & STATIC_MODEL_FLAG_NO_SHADOW) == 0) + gfxWorld->dpvs.smodelCastsShadow[i] = 1; + } + + return true; + } + + void GfxWorldLinker::cleanGfxWorld(GfxWorld* gfxWorld) + { + // checksum is generated by the game + gfxWorld->checksum = 0; + + // official maps set this to 0 + gfxWorld->dpvs.usageCount = 0; + + // Remove Coronas + gfxWorld->coronaCount = 0; + gfxWorld->coronas = nullptr; + + // Remove exposure volumes + gfxWorld->exposureVolumeCount = 0; + gfxWorld->exposureVolumes = nullptr; + gfxWorld->exposureVolumePlaneCount = 0; + gfxWorld->exposureVolumePlanes = nullptr; + + // Remove hero lights + gfxWorld->heroLightCount = 0; + gfxWorld->heroLights = nullptr; + gfxWorld->heroLightTreeCount = 0; + gfxWorld->heroLightTree = nullptr; + + // remove LUT data + gfxWorld->lutVolumeCount = 0; + gfxWorld->lutVolumes = nullptr; + gfxWorld->lutVolumePlaneCount = 0; + gfxWorld->lutVolumePlanes = nullptr; + + // remove occluders + gfxWorld->numOccluders = 0; + gfxWorld->occluders = nullptr; + + // remove Siege Skins + gfxWorld->numSiegeSkinInsts = 0; + gfxWorld->siegeSkinInsts = nullptr; + + // remove outdoor bounds + gfxWorld->numOutdoorBounds = 0; + gfxWorld->outdoorBounds = nullptr; + + // remove materials + gfxWorld->ropeMaterial = nullptr; + gfxWorld->lutMaterial = nullptr; + gfxWorld->waterMaterial = nullptr; + gfxWorld->coronaMaterial = nullptr; + + // remove shadow maps + gfxWorld->shadowMapVolumeCount = 0; + gfxWorld->shadowMapVolumes = nullptr; + gfxWorld->shadowMapVolumePlaneCount = 0; + gfxWorld->shadowMapVolumePlanes = nullptr; + + // remove stream info + gfxWorld->streamInfo.aabbTreeCount = 0; + gfxWorld->streamInfo.aabbTrees = nullptr; + gfxWorld->streamInfo.leafRefCount = 0; + gfxWorld->streamInfo.leafRefs = nullptr; + + // remove sun data + memset(&gfxWorld->sun, 0, sizeof(sunflare_t)); + gfxWorld->sun.hasValidData = false; + + // Remove Water + gfxWorld->waterDirection = 0.0f; + gfxWorld->waterBuffers[0].bufferSize = 0; + gfxWorld->waterBuffers[0].buffer = nullptr; + gfxWorld->waterBuffers[1].bufferSize = 0; + gfxWorld->waterBuffers[1].buffer = nullptr; + + // Remove Fog + gfxWorld->worldFogModifierVolumeCount = 0; + gfxWorld->worldFogModifierVolumes = nullptr; + gfxWorld->worldFogModifierVolumePlaneCount = 0; + gfxWorld->worldFogModifierVolumePlanes = nullptr; + gfxWorld->worldFogVolumeCount = 0; + gfxWorld->worldFogVolumes = nullptr; + gfxWorld->worldFogVolumePlaneCount = 0; + gfxWorld->worldFogVolumePlanes = nullptr; + + // materialMemory is unused + gfxWorld->materialMemoryCount = 0; + gfxWorld->materialMemory = nullptr; + + // sunLight is overwritten by the game, just needs to be a valid pointer + gfxWorld->sunLight = m_memory.Alloc(); + } + + bool GfxWorldLinker::loadGfxLights(BSPData* bsp, GfxWorld* gfxWorld) + { + // there must be 2 or more lights, first is the static light and second is the sun light + gfxWorld->primaryLightCount = BSP_DEFAULT_LIGHT_COUNT + static_cast(bsp->lights.size()); + gfxWorld->sunPrimaryLightIndex = SUN_LIGHT_INDEX; + + if (gfxWorld->primaryLightCount > 254) + { + con::error("Exceeded 254 lights in BSP, count: {}", gfxWorld->primaryLightCount); + return false; + } + + gfxWorld->shadowGeom = m_memory.Alloc(gfxWorld->primaryLightCount); + for (unsigned int lightIdx = 0; lightIdx < gfxWorld->primaryLightCount; lightIdx++) + { + // smodelCount and smodelIndex is filled next loop + gfxWorld->shadowGeom[lightIdx].smodelCount = 0; + gfxWorld->shadowGeom[lightIdx].smodelIndex = m_memory.Alloc(gfxWorld->dpvs.smodelCount); + + // sorted surfs and surfaceCount is recalculated each frame + gfxWorld->shadowGeom[lightIdx].surfaceCount = gfxWorld->dpvs.staticSurfaceCount; + gfxWorld->shadowGeom[lightIdx].sortedSurfIndex = m_memory.Alloc(gfxWorld->dpvs.staticSurfaceCount); + } + for (unsigned int modelIdx = 0; modelIdx < gfxWorld->dpvs.smodelCount; modelIdx++) + { + if ((gfxWorld->dpvs.smodelDrawInsts[modelIdx].flags & STATIC_MODEL_FLAG_NO_SHADOW) != 0) + continue; + unsigned char lightIndex = gfxWorld->dpvs.smodelDrawInsts[modelIdx].primaryLightIndex; + gfxWorld->shadowGeom[lightIndex].smodelIndex[gfxWorld->shadowGeom[lightIndex].smodelCount] = modelIdx; + gfxWorld->shadowGeom[lightIndex].smodelCount++; + } + + gfxWorld->lightRegion = m_memory.Alloc(gfxWorld->primaryLightCount); + for (unsigned int lightIdx = 0; lightIdx < gfxWorld->primaryLightCount; lightIdx++) + { + gfxWorld->lightRegion[lightIdx].hullCount = 0; + gfxWorld->lightRegion[lightIdx].hulls = nullptr; + } + + unsigned int lightEntShadowVisSize = (gfxWorld->primaryLightCount - gfxWorld->sunPrimaryLightIndex - 1) * 8192; + if (lightEntShadowVisSize != 0) + gfxWorld->primaryLightEntityShadowVis = m_memory.Alloc(lightEntShadowVisSize); + else + gfxWorld->primaryLightEntityShadowVis = nullptr; + + return true; + } + + bool GfxWorldLinker::loadLightGrid(BSPData* bsp, GfxWorld* gfxWorld) + { + // world to lightgrid coords conversion: + // l_x = (w_x + 131072.0f) / 32.0f; + // l_y = (w_y + 131072.0f) / 32.0f; + // l_z = (w_z + 131072.0f) / 64.0f; + // + // lightgrid to world coords conversion: + // w_x = (double)l_x * 32.0f - 131072.0f; + // w_y = (double)l_y * 32.0f - 131072.0f; + // w_z = (double)l_z * 64.0f - 131072.0f; + // + // Lightrid coords are bounded by the uint16 limits: + // w_minX = -131072, w_maxX = 1966048 + // w_minY = -131072, w_maxY = 1966048 + // w_minZ = -131072, w_maxZ = 4063168 + + if (gfxWorld->mins.x < -131072.0f || gfxWorld->mins.y < -131072.0f || gfxWorld->mins.z < -131072.0f || gfxWorld->maxs.x > 1966048.0f + || gfxWorld->maxs.y > 1966048.0f || gfxWorld->maxs.z > 4063168.0f) + { + con::error("Lightrid mins/maxs exceeded"); + return false; + } + + // integer-only method used by the game + gfxWorld->lightGrid.mins[0] = static_cast((static_cast(gfxWorld->mins.x) + 0x20000) >> 5); + gfxWorld->lightGrid.mins[1] = static_cast((static_cast(gfxWorld->mins.y) + 0x20000) >> 5); + gfxWorld->lightGrid.mins[2] = static_cast((static_cast(gfxWorld->mins.z) + 0x20000) >> 6); + gfxWorld->lightGrid.maxs[0] = static_cast((static_cast(gfxWorld->maxs.x) + 0x20000) >> 5); + gfxWorld->lightGrid.maxs[1] = static_cast((static_cast(gfxWorld->maxs.y) + 0x20000) >> 5); + gfxWorld->lightGrid.maxs[2] = static_cast((static_cast(gfxWorld->maxs.z) + 0x20000) >> 6); + + gfxWorld->lightGrid.rowAxis = 0; // default value + gfxWorld->lightGrid.colAxis = 1; // default value + gfxWorld->lightGrid.sunPrimaryLightIndex = SUN_LIGHT_INDEX; + gfxWorld->lightGrid.offset = 0.0f; // default value + + // setting all rowDataStart indexes to 0 will always index the first row in rawRowData + int rowDataStartSize = gfxWorld->lightGrid.maxs[gfxWorld->lightGrid.rowAxis] - gfxWorld->lightGrid.mins[gfxWorld->lightGrid.rowAxis] + 1; + gfxWorld->lightGrid.rowDataStart = m_memory.Alloc(rowDataStartSize); + + // Adding 0x1FF so the lookup table will be 0x200 bytes in size + gfxWorld->lightGrid.rawRowDataSize = static_cast(sizeof(GfxLightGridRow) + 0x1FF); + GfxLightGridRow* row = static_cast(m_memory.AllocRaw(gfxWorld->lightGrid.rawRowDataSize)); + row->colStart = 0; + row->colCount = 0xFFFF; + row->zStart = 0; + row->zCount = 0xFF; // 0xFF as this seems to be the sweet spot of values + row->firstEntry = 0; + for (int i = 0; i < 0x200; i++) + row->lookupTable[i] = -1; // values in the lookup table are subtracted from the sample pos until it reaches 0, so use 0xFF + gfxWorld->lightGrid.rawRowData = reinterpret_cast(row); + + // entries are looked up based on the lightgrid sample pos (given ingame) and the lightgrid lookup table + gfxWorld->lightGrid.entryCount = 60000; // 60000 as it should be enough entries to be indexed by all lightgrid sample positions + GfxLightGridEntry* entryArray = m_memory.Alloc(gfxWorld->lightGrid.entryCount); + for (unsigned int i = 0; i < gfxWorld->lightGrid.entryCount; i++) + { + entryArray[i].colorsIndex = 0; // always index first colour + entryArray[i].primaryLightIndex = DEFAULT_PRIMARYLIGHT_INDEX; + entryArray[i].visibility = 0; + } + gfxWorld->lightGrid.entries = entryArray; + + char color = DEFAULT_LIGHTGRID_COLOUR; + float sunIntensity = bsp->sunlight.intensity; + if (sunIntensity < 0.0f || sunIntensity > 20000.0f) + { + con::error("Sun intensity ({}) needs to be between 0 and 20000", sunIntensity); + return false; + } + else + { + float normalised = sunIntensity / 20000.0f; + normalised *= 255.0f; + float clean = roundf(normalised); + color = static_cast(clean); + } + + // colours are looked up with a lightgrid entries colorsIndex + gfxWorld->lightGrid.colorCount = 0x1000; // 0x1000 as it should be enough to hold every index + gfxWorld->lightGrid.colors = m_memory.Alloc(gfxWorld->lightGrid.colorCount); + memset(gfxWorld->lightGrid.colors, color, rowDataStartSize * sizeof(uint16_t)); + + // we use the colours array instead of coeffs array + gfxWorld->lightGrid.coeffCount = 0; + gfxWorld->lightGrid.coeffs = nullptr; + gfxWorld->lightGrid.skyGridVolumeCount = 0; + gfxWorld->lightGrid.skyGridVolumes = nullptr; + + return true; + } + + struct mnode_t + { + unsigned __int16 cellIndex; + unsigned __int16 rightChildOffset; + }; + + void GfxWorldLinker::loadGfxCells(GfxWorld* gfxWorld) + { + // Cells are basically data used to determine what can be seen and what cant be seen + // Right now custom maps have no optimisation so there is only 1 cell + int cellCount = 1; + + gfxWorld->dpvsPlanes.cellCount = cellCount; + gfxWorld->cellBitsCount = ((cellCount + 127) >> 3) & 0x1FFFFFF0; + + int cellCasterBitsCount = cellCount * ((cellCount + 31) / 32); + gfxWorld->cellCasterBits = m_memory.Alloc(cellCasterBitsCount); + + int sceneEntCellBitsCount = cellCount * 512; + gfxWorld->dpvsPlanes.sceneEntCellBits = m_memory.Alloc(sceneEntCellBitsCount); + + gfxWorld->cells = m_memory.Alloc(cellCount); + gfxWorld->cells[0].portalCount = 0; + gfxWorld->cells[0].portals = nullptr; + gfxWorld->cells[0].mins = gfxWorld->mins; + gfxWorld->cells[0].maxs = gfxWorld->maxs; + + // there is only 1 reflection probe + gfxWorld->cells[0].reflectionProbeCount = 1; + gfxWorld->cells[0].reflectionProbes = m_memory.Alloc(gfxWorld->cells[0].reflectionProbeCount); + gfxWorld->cells[0].reflectionProbes[0] = DEFAULT_RPROBE_INDEX; + + // AABB trees are used to detect what should be rendered and what shouldn't + // Just use the first AABB node to hold all models, no optimisation but all models/surfaces wil lbe drawn + gfxWorld->cells[0].aabbTreeCount = 1; + gfxWorld->cells[0].aabbTree = m_memory.Alloc(gfxWorld->cells[0].aabbTreeCount); + gfxWorld->cells[0].aabbTree[0].childCount = 0; + gfxWorld->cells[0].aabbTree[0].childrenOffset = 0; + gfxWorld->cells[0].aabbTree[0].startSurfIndex = 0; + gfxWorld->cells[0].aabbTree[0].surfaceCount = static_cast(gfxWorld->dpvs.staticSurfaceCount); + gfxWorld->cells[0].aabbTree[0].smodelIndexCount = static_cast(gfxWorld->dpvs.smodelCount); + gfxWorld->cells[0].aabbTree[0].smodelIndexes = m_memory.Alloc(gfxWorld->dpvs.smodelCount); + for (unsigned short smodelIdx = 0; smodelIdx < gfxWorld->dpvs.smodelCount; smodelIdx++) + { + gfxWorld->cells[0].aabbTree[0].smodelIndexes[smodelIdx] = smodelIdx; + } + gfxWorld->cells[0].aabbTree[0].mins = gfxWorld->mins; + gfxWorld->cells[0].aabbTree[0].maxs = gfxWorld->maxs; + + // nodes have the struct mnode_t, and there must be at least 1 node (similar to BSP nodes) + // Nodes mnode_t.cellIndex indexes gfxWorld->cells + // and (mnode_t.cellIndex - (world->dpvsPlanes.cellCount + 1) indexes world->dpvsPlanes.planes + // Use only one node as there is no optimisation in custom maps + + gfxWorld->nodeCount = 1; + gfxWorld->dpvsPlanes.nodes = m_memory.Alloc(gfxWorld->nodeCount); + gfxWorld->dpvsPlanes.nodes[0] = 1; // nodes reference cells by index + 1 + + // planes are overwritten by the clipmap loading code ingame + gfxWorld->planeCount = 0; + gfxWorld->dpvsPlanes.planes = nullptr; + } + + void GfxWorldLinker::loadWorldBounds(GfxWorld* gfxWorld) + { + gfxWorld->mins = gfxWorld->dpvs.surfaces[0].bounds[0]; + gfxWorld->maxs = gfxWorld->dpvs.surfaces[0].bounds[1]; + for (int surfIdx = 0; surfIdx < gfxWorld->surfaceCount; surfIdx++) + { + BSPUtil::updateAABB(gfxWorld->dpvs.surfaces[surfIdx].bounds[0], gfxWorld->dpvs.surfaces[surfIdx].bounds[1], gfxWorld->mins, gfxWorld->maxs); + } + + for (unsigned int smodeldx = 0; smodeldx < gfxWorld->dpvs.smodelCount; smodeldx++) + { + BSPUtil::updateAABB(gfxWorld->dpvs.smodelInsts[smodeldx].mins, gfxWorld->dpvs.smodelInsts[smodeldx].maxs, gfxWorld->mins, gfxWorld->maxs); + } + } + + void GfxWorldLinker::loadModels(BSPData* bsp, GfxWorld* gfxWorld) + { + // Models (Submodels in the clipmap code) are used for the world and map ent collision (triggers, bomb zones, etc) + // bounds are checked in clipmap linker + gfxWorld->modelCount = static_cast(bsp->models.size() + 1); + gfxWorld->models = m_memory.Alloc(gfxWorld->modelCount); + + // first model is always the world model + gfxWorld->models[0].startSurfIndex = 0; + gfxWorld->models[0].surfaceCount = static_cast(gfxWorld->dpvs.staticSurfaceCount); + for (unsigned int surfIdx = 0; surfIdx < gfxWorld->dpvs.staticSurfaceCount; surfIdx++) + { + if (surfIdx == 0) + { + gfxWorld->models[0].bounds[0] = gfxWorld->dpvs.surfaces[surfIdx].bounds[0]; + gfxWorld->models[0].bounds[1] = gfxWorld->dpvs.surfaces[surfIdx].bounds[1]; + } + else + BSPUtil::updateAABB(gfxWorld->dpvs.surfaces[surfIdx].bounds[0], + gfxWorld->dpvs.surfaces[surfIdx].bounds[1], + gfxWorld->models[0].bounds[0], + gfxWorld->models[0].bounds[1]); + } + if (gfxWorld->dpvs.staticSurfaceCount == 0) + { + gfxWorld->models[0].bounds[0] = {}; + gfxWorld->models[0].bounds[1] = {}; + } + memset(&gfxWorld->models[0].writable, 0, sizeof(GfxBrushModelWritable)); + + for (size_t modelIdx = 0; modelIdx < bsp->models.size(); modelIdx++) + { + auto currEntModel = &gfxWorld->models[modelIdx + 1]; + auto& bspModel = bsp->models.at(modelIdx); + + if (bspModel.isGfxModel && bspModel.surfaceCount != 0) + { + currEntModel->startSurfIndex = bsp->gfxWorld.staticSurfaces.size() + bspModel.surfaceIndex; + currEntModel->surfaceCount = bspModel.surfaceCount; + for (size_t surfIdx = 0; surfIdx < bspModel.surfaceCount; surfIdx++) + { + GfxSurface* surf = &gfxWorld->dpvs.surfaces[currEntModel->startSurfIndex + surfIdx]; + if (surfIdx == 0) + { + currEntModel->bounds[0] = surf->bounds[0]; + currEntModel->bounds[1] = surf->bounds[1]; + } + else + BSPUtil::updateAABB(surf->bounds[0], surf->bounds[1], currEntModel->bounds[0], currEntModel->bounds[1]); + } + } + else + { + currEntModel->startSurfIndex = -1; // -1 when it doesn't use map surfaces + currEntModel->surfaceCount = 0; + currEntModel->bounds[0] = {}; + currEntModel->bounds[1] = {}; + } + memset(&gfxWorld->models[0].writable, 0, sizeof(GfxBrushModelWritable)); + } + } + + void GfxWorldLinker::loadSunData(BSPData* bsp, GfxWorld* gfxWorld) + { + BSPLight& sunlight = bsp->sunlight; + vec3_t viewAngles = BSPUtil::convertForwardVectorToViewAngles(sunlight.direction); + gfxWorld->sunParse.initWorldSun->angles = viewAngles; + gfxWorld->sunParse.initWorldSun->sunCd.x = sunlight.colour.x; + gfxWorld->sunParse.initWorldSun->sunCd.y = sunlight.colour.y; + gfxWorld->sunParse.initWorldSun->sunCd.z = sunlight.colour.z; + gfxWorld->sunParse.initWorldSun->sunCd.w = 1.0f; + + // fog is not implemented yet, values taken from mp_dig + gfxWorld->sunParse.initWorldFog->baseDist = 150.0f; + gfxWorld->sunParse.initWorldFog->baseHeight = -100.0f; + gfxWorld->sunParse.initWorldFog->fogColor.x = 2.35f; + gfxWorld->sunParse.initWorldFog->fogColor.y = 3.10f; + gfxWorld->sunParse.initWorldFog->fogColor.z = 3.84f; + gfxWorld->sunParse.initWorldFog->fogOpacity = 0.52f; + gfxWorld->sunParse.initWorldFog->halfDist = 4450.f; + gfxWorld->sunParse.initWorldFog->halfHeight = 2000.f; + gfxWorld->sunParse.initWorldFog->sunFogColor.x = 5.27f; + gfxWorld->sunParse.initWorldFog->sunFogColor.y = 4.73f; + gfxWorld->sunParse.initWorldFog->sunFogColor.z = 3.88f; + gfxWorld->sunParse.initWorldFog->sunFogInner = 0.0f; + gfxWorld->sunParse.initWorldFog->sunFogOpacity = 0.67f; + gfxWorld->sunParse.initWorldFog->sunFogOuter = 80.84f; + gfxWorld->sunParse.initWorldFog->sunFogPitch = -29.0f; + gfxWorld->sunParse.initWorldFog->sunFogYaw = 254.0f; + } + + bool GfxWorldLinker::loadReflectionProbeData(GfxWorld* gfxWorld) + { + gfxWorld->draw.reflectionProbeCount = 1; + + gfxWorld->draw.reflectionProbeTextures = m_memory.Alloc(gfxWorld->draw.reflectionProbeCount); + + // default values taken from mp_dig + gfxWorld->draw.reflectionProbes = m_memory.Alloc(gfxWorld->draw.reflectionProbeCount); + gfxWorld->draw.reflectionProbes[0].mipLodBias = -8.0; // always -8.0f + gfxWorld->draw.reflectionProbes[0].origin = {}; + gfxWorld->draw.reflectionProbes[0].lightingSH.V0 = {}; + gfxWorld->draw.reflectionProbes[0].lightingSH.V1 = {}; + gfxWorld->draw.reflectionProbes[0].lightingSH.V2 = {}; + + gfxWorld->draw.reflectionProbes[0].probeVolumeCount = 0; + gfxWorld->draw.reflectionProbes[0].probeVolumes = nullptr; + + std::string probeImageName = ",$black"; + auto probeImageAsset = m_context.LoadDependency(probeImageName); + if (probeImageAsset == nullptr) + { + con::error("ERROR! unable to find reflection probe image {}!", probeImageName); + return false; + } + gfxWorld->draw.reflectionProbes[0].reflectionImage = probeImageAsset->Asset(); + + return true; + } + + bool GfxWorldLinker::loadLightmapData(GfxWorld* gfxWorld) + { + gfxWorld->draw.lightmapCount = 1; + + gfxWorld->draw.lightmapPrimaryTextures = m_memory.Alloc(gfxWorld->draw.lightmapCount); + gfxWorld->draw.lightmapSecondaryTextures = m_memory.Alloc(gfxWorld->draw.lightmapCount); + + std::string secondaryTexture = ",$gray"; // gray makes shadows a nice looking shade of black + auto secondaryTextureAsset = m_context.LoadDependency(secondaryTexture); + if (secondaryTextureAsset == nullptr) + { + con::error("ERROR! unable to find lightmap image {}!", secondaryTexture); + return false; + } + gfxWorld->draw.lightmaps = m_memory.Alloc(gfxWorld->draw.lightmapCount); + gfxWorld->draw.lightmaps[0].primary = nullptr; // always nullptr + gfxWorld->draw.lightmaps[0].secondary = secondaryTextureAsset->Asset(); + + return true; + } + + void GfxWorldLinker::loadSkyBox(BSPData* projInfo, GfxWorld* gfxWorld) + { + std::string skyBoxName = "skybox_" + projInfo->name; + gfxWorld->skyBoxModel = m_memory.Dup(skyBoxName.c_str()); + + if (m_context.LoadDependency(skyBoxName) == nullptr) + { + con::warn("WARN: Unable to load the skybox xmodel {}!", skyBoxName); + } + + // always 0 and 1 + gfxWorld->skyDynIntensity.angle0 = 0.0f; + gfxWorld->skyDynIntensity.angle1 = 0.0f; + gfxWorld->skyDynIntensity.factor0 = 1.0f; + gfxWorld->skyDynIntensity.factor1 = 1.0f; + } + + void GfxWorldLinker::loadDynEntData(GfxWorld* gfxWorld) + { + int dynEntCount = 0; + gfxWorld->dpvsDyn.dynEntClientCount[0] = dynEntCount + 256; // the game allocs 256 empty dynents, as they may be used ingame + gfxWorld->dpvsDyn.dynEntClientCount[1] = 0; + + // +100: there is a crash that happens when ragdolls are created, and dynEntClientWordCount[0] is the issue. + // Making the value much larger than required fixes it, but unsure what the root cause is + gfxWorld->dpvsDyn.dynEntClientWordCount[0] = ((gfxWorld->dpvsDyn.dynEntClientCount[0] + 31) >> 5) + 100; + gfxWorld->dpvsDyn.dynEntClientWordCount[1] = 0; + gfxWorld->dpvsDyn.usageCount = 0; + + int dynEntCellBitsSize = gfxWorld->dpvsDyn.dynEntClientWordCount[0] * gfxWorld->dpvsPlanes.cellCount; + gfxWorld->dpvsDyn.dynEntCellBits[0] = m_memory.Alloc(dynEntCellBitsSize); + gfxWorld->dpvsDyn.dynEntCellBits[1] = nullptr; + + int dynEntVisData0Size = gfxWorld->dpvsDyn.dynEntClientWordCount[0] * 32; + gfxWorld->dpvsDyn.dynEntVisData[0][0] = m_memory.Alloc(dynEntVisData0Size); + gfxWorld->dpvsDyn.dynEntVisData[0][1] = m_memory.Alloc(dynEntVisData0Size); + gfxWorld->dpvsDyn.dynEntVisData[0][2] = m_memory.Alloc(dynEntVisData0Size); + gfxWorld->dpvsDyn.dynEntVisData[1][0] = nullptr; + gfxWorld->dpvsDyn.dynEntVisData[1][1] = nullptr; + gfxWorld->dpvsDyn.dynEntVisData[1][2] = nullptr; + + unsigned int dynEntShadowVisCount = gfxWorld->dpvsDyn.dynEntClientCount[0] * (gfxWorld->primaryLightCount - gfxWorld->sunPrimaryLightIndex - 1); + gfxWorld->primaryLightDynEntShadowVis[0] = m_memory.Alloc(dynEntShadowVisCount); + gfxWorld->primaryLightDynEntShadowVis[1] = nullptr; + + gfxWorld->sceneDynModel = m_memory.Alloc(gfxWorld->dpvsDyn.dynEntClientCount[0]); + gfxWorld->sceneDynBrush = nullptr; + } + + bool GfxWorldLinker::loadOutdoors(GfxWorld* gfxWorld) + { + float xRecip = 1.0f / (gfxWorld->maxs.x - gfxWorld->mins.x); + float xScale = -(xRecip * gfxWorld->mins.x); + + float yRecip = 1.0f / (gfxWorld->maxs.y - gfxWorld->mins.y); + float yScale = -(yRecip * gfxWorld->mins.y); + + float zRecip = 1.0f / (gfxWorld->maxs.z - gfxWorld->mins.z); + float zScale = -(zRecip * gfxWorld->mins.z); + + memset(gfxWorld->outdoorLookupMatrix, 0, sizeof(gfxWorld->outdoorLookupMatrix)); + + gfxWorld->outdoorLookupMatrix[0].x = xRecip; + gfxWorld->outdoorLookupMatrix[1].y = yRecip; + gfxWorld->outdoorLookupMatrix[2].z = zRecip; + gfxWorld->outdoorLookupMatrix[3].x = xScale; + gfxWorld->outdoorLookupMatrix[3].y = yScale; + gfxWorld->outdoorLookupMatrix[3].z = zScale; + gfxWorld->outdoorLookupMatrix[3].w = 1.0f; + + std::string outdoorImageName = std::string(",$black"); + auto outdoorImageAsset = m_context.LoadDependency(outdoorImageName); + if (outdoorImageAsset == nullptr) + { + con::error("ERROR! unable to find outdoor image!"); + return false; + } + gfxWorld->outdoorImage = outdoorImageAsset->Asset(); + + return true; + } + + GfxWorld* GfxWorldLinker::linkGfxWorld(BSPData* bsp) + { + GfxWorld* gfxWorld = m_memory.Alloc(); + gfxWorld->baseName = m_memory.Dup(bsp->name.c_str()); + gfxWorld->name = m_memory.Dup(bsp->bspName.c_str()); + + // Default values taken from origins + gfxWorld->lightingFlags = 4; + gfxWorld->lightingQuality = 10000; + + cleanGfxWorld(gfxWorld); + + if (!loadMapSurfaces(bsp, gfxWorld)) + return nullptr; + + if (!loadXModels(bsp, gfxWorld)) + return nullptr; + + if (!loadLightmapData(gfxWorld)) + return nullptr; + + loadSkyBox(bsp, gfxWorld); + + if (!loadReflectionProbeData(gfxWorld)) + return nullptr; + + // world bounds are based on loaded surface mins/maxs and xmodels + loadWorldBounds(gfxWorld); + + if (!loadOutdoors(gfxWorld)) // requires world mins/maxs + return nullptr; + + // gfx cells depend on surface/smodel count + loadGfxCells(gfxWorld); + + loadLightGrid(bsp, gfxWorld); // requires world mins/maxs + + if (!loadGfxLights(bsp, gfxWorld)) // requires xmodels and surfaces + return nullptr; + + loadModels(bsp, gfxWorld); // requires surfaces + + loadSunData(bsp, gfxWorld); + + loadDynEntData(gfxWorld); // requires cells and lights + + return gfxWorld; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.h new file mode 100644 index 000000000..e3fa09629 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/GfxWorldLinker.h @@ -0,0 +1,37 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class GfxWorldLinker + { + public: + GfxWorldLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + GfxWorld* linkGfxWorld(BSPData* bsp); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + + void loadDrawData(BSPData* projInfo, GfxWorld* gfxWorld); + bool loadMapSurfaces(BSPData* projInfo, GfxWorld* gfxWorld); + bool loadXModels(BSPData* projInfo, GfxWorld* gfxWorld); + void cleanGfxWorld(GfxWorld* gfxWorld); + bool loadGfxLights(BSPData* bsp, GfxWorld* gfxWorld); + bool loadLightGrid(BSPData* bsp, GfxWorld* gfxWorld); + void loadGfxCells(GfxWorld* gfxWorld); + void loadModels(BSPData* bsp, GfxWorld* gfxWorld); + bool loadReflectionProbeData(GfxWorld* gfxWorld); + bool loadLightmapData(GfxWorld* gfxWorld); + void loadSkyBox(BSPData* projInfo, GfxWorld* gfxWorld); + void loadDynEntData(GfxWorld* gfxWorld); + bool loadOutdoors(GfxWorld* gfxWorld); + void loadSunData(BSPData* projInfo, GfxWorld* gfxWorld); + void loadWorldBounds(GfxWorld* gfxWorld); + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.cpp new file mode 100644 index 000000000..13f72665a --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.cpp @@ -0,0 +1,174 @@ +#include "MapEntsLinker.h" + +#include "../BSPUtil.h" + +namespace +{ + inline const std::vector DEFENDER_SPAWN_POINT_NAMES = {"mp_ctf_spawn_allies", + "mp_ctf_spawn_allies_start", + "mp_sd_spawn_defender", + "mp_dom_spawn_allies_start", + "mp_dem_spawn_defender_start", + "mp_dem_spawn_defenderOT_start", + "mp_dem_spawn_defender", + "mp_tdm_spawn_allies_start", + "mp_tdm_spawn_team1_start", + "mp_tdm_spawn_team2_start", + "mp_tdm_spawn_team3_start"}; + + inline const std::vector ATTACKER_SPAWN_POINT_NAMES = {"mp_ctf_spawn_axis", + "mp_ctf_spawn_axis_start", + "mp_sd_spawn_attacker", + "mp_dom_spawn_axis_start", + "mp_dem_spawn_attacker_start", + "mp_dem_spawn_attackerOT_start", + "mp_dem_spawn_attacker", + "mp_tdm_spawn_axis_start", + "mp_tdm_spawn_team4_start", + "mp_tdm_spawn_team5_start", + "mp_tdm_spawn_team6_start"}; + + inline const std::vector FFA_SPAWN_POINT_NAMES = {"mp_tdm_spawn", "mp_dm_spawn", "mp_dom_spawn"}; + + void addSpawnsToEntString(BSP::BSPData* bsp, std::string& entityString) + { + if (bsp->isZombiesMap) + { + for (auto& spawnPoint : bsp->spawnpoints) + { + entityString.append("{\n"); + entityString.append("\"classname\" \"script_struct\"\n"); + entityString.append(std::format("\"targetname\" \"{}\"\n", spawnPoint.spawnpointGroupName)); + entityString.append(std::format("\"origin\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(spawnPoint.origin))); + vec3_t angles = BSP::BSPUtil::convertForwardVectorToViewAngles(spawnPoint.forward); + entityString.append(std::format("\"angles\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(angles))); + entityString.append("}\n"); + } + return; + } + + size_t defenderNameCount = std::extent::value; + size_t attackerNameCount = std::extent::value; + size_t ffaNameCount = std::extent::value; + + for (auto& spawnPoint : bsp->spawnpoints) + { + vec3_t origin = spawnPoint.origin; + vec3_t angles = BSP::BSPUtil::convertForwardVectorToViewAngles(spawnPoint.forward); + + std::string originStr = std::format("\"origin\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(origin)); + std::string anglesStr = std::format("\"angles\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(angles)); + + const std::vector* spawnPointList; + if (!spawnPoint.spawnpointGroupName.compare("defender")) + spawnPointList = &DEFENDER_SPAWN_POINT_NAMES; + else if (!spawnPoint.spawnpointGroupName.compare("attacker")) + spawnPointList = &ATTACKER_SPAWN_POINT_NAMES; + else // SPAWNPOINT_TYPE_ALL + spawnPointList = &FFA_SPAWN_POINT_NAMES; + + for (const char* spawnName : *spawnPointList) + { + entityString.append("{\n"); + entityString.append(originStr); + entityString.append(anglesStr); + entityString.append(std::format("\"classname\" \"{}\"\n", spawnName)); + entityString.append("}\n"); + } + } + } + + void addZombiesEntitiesToEntString(BSP::BSPData* bsp, std::string& entityString) + { + for (auto& zone : bsp->zm_zones) + { + entityString.append("{\n"); + entityString.append("\"classname\" \"info_volume\"\n"); + entityString.append("\"script_noteworthy\" \"player_volume\"\n"); + entityString.append(std::format("\"targetname\" \"{}\"\n", zone.zoneName)); + entityString.append(std::format("\"target\" \"{}\"\n", zone.spawnerGroupName)); + entityString.append(std::format("\"origin\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(zone.origin))); + entityString.append(std::format("\"model\" \"*{}\"\n", zone.modelIndex)); + entityString.append("}\n"); + + entityString.append("{\n"); + entityString.append("\"classname\" \"script_struct\"\n"); + entityString.append(std::format("\"targetname\" \"player_respawn_point\"\n")); + entityString.append(std::format("\"script_noteworthy\" \"{}\"\n", zone.zoneName)); + entityString.append(std::format("\"target\" \"{}\"\n", zone.spawnpointGroupName)); + entityString.append("}\n"); + } + + for (auto& zSpawner : bsp->zm_spawners) + { + entityString.append("{\n"); + entityString.append("\"classname\" \"script_struct\"\n"); + entityString.append("\"script_noteworthy\" \"riser_location\"\n"); + entityString.append("\"script_string\" \"find_flesh\"\n"); + entityString.append(std::format("\"targetname\" \"{}\"\n", zSpawner.spawnerGroupName)); + entityString.append(std::format("\"origin\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(zSpawner.origin))); + vec3_t angles = BSP::BSPUtil::convertForwardVectorToViewAngles(zSpawner.forward); + entityString.append(std::format("\"angles\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(angles))); + entityString.append("}\n"); + } + } + + void addClassEntitiesToEntString(BSP::BSPData* bsp, std::string& entityString) + { + for (auto& entity : bsp->entities) + { + vec3_t origin = entity.origin; + vec3_t axis[3]; + BSP::BSPUtil::convertQuaternionToAxis(&entity.rotationQuaternion, axis); + vec3_t angles = BSP::BSPUtil::convertAxisToAngles(axis); + + entityString.append("{\n"); + entityString.append(std::format("\"origin\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(origin))); + + for (auto& entry : entity.entries) + entityString.append(std::format("\"{}\" \"{}\"\n", entry.key, entry.value)); + if (entity.modelIndex != 0) + entityString.append(std::format("\"model\" \"*{}\"\n", entity.modelIndex)); + else // entities with generated models can't have rotation data + entityString.append(std::format("\"angles\" \"{}\"\n", BSP::BSPUtil::convertVec3ToString(angles))); + + entityString.append("}\n"); + } + } +} // namespace + +namespace BSP +{ + MapEntsLinker::MapEntsLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + MapEnts* MapEntsLinker::linkMapEnts(BSPData* bsp) + { + std::string entityString; + + // worldspawn must be the first entity + entityString.append("{\n"); + for (auto& entry : bsp->worldspawn.entries) + entityString.append(std::format("\"{}\" \"{}\"\n", entry.key, entry.value)); + entityString.append("}\n"); + + addSpawnsToEntString(bsp, entityString); + + if (bsp->isZombiesMap) + addZombiesEntitiesToEntString(bsp, entityString); + + addClassEntitiesToEntString(bsp, entityString); + + MapEnts* mapEnts = m_memory.Alloc(); + mapEnts->name = m_memory.Dup(bsp->bspName.c_str()); + + mapEnts->entityString = m_memory.Dup(entityString.c_str()); + mapEnts->numEntityChars = static_cast(entityString.length() + 1); // numEntityChars includes the null character + + return mapEnts; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.h new file mode 100644 index 000000000..0c7681b31 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/MapEntsLinker.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class MapEntsLinker + { + public: + MapEntsLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + MapEnts* linkMapEnts(BSPData* bsp); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.cpp b/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.cpp new file mode 100644 index 000000000..541c7515d --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.cpp @@ -0,0 +1,22 @@ +#include "SkinnedVertsLinker.h" + +namespace BSP +{ + SkinnedVertsLinker::SkinnedVertsLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context) + : m_memory(memory), + m_search_path(searchPath), + m_context(context) + { + } + + SkinnedVertsDef* SkinnedVertsLinker::linkSkinnedVerts(BSPData* bsp) + { + // maxSkinnedVerts defines how many model verts can be drawn at once + // Low values cause models not to be drawn, so double origin's maxSkinnedVerts (163840) is used as a safe bet + SkinnedVertsDef* skinnedVerts = m_memory.Alloc(); + skinnedVerts->name = m_memory.Dup("skinnedverts"); + skinnedVerts->maxSkinnedVerts = 300000; + + return skinnedVerts; + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.h b/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.h new file mode 100644 index 000000000..d64b0c20f --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/Linker/SkinnedVertsLinker.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../BSP.h" +#include "Asset/IAssetCreator.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +namespace BSP +{ + class SkinnedVertsLinker + { + public: + SkinnedVertsLinker(MemoryManager& memory, ISearchPath& searchPath, AssetCreationContext& context); + SkinnedVertsDef* linkSkinnedVerts(BSPData* bsp); + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + AssetCreationContext& m_context; + }; +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.cpp b/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.cpp new file mode 100644 index 000000000..ec5ac2715 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.cpp @@ -0,0 +1,62 @@ +#include "LoaderBSP_T6.h" + +#include "BSPCreator.h" +#include "BSPUtil.h" +#include "Linker/BSPLinker.h" + +namespace +{ + using namespace BSP; + + class BSPLoader final : public IAssetCreator + { + public: + BSPLoader(MemoryManager& memory, ISearchPath& searchPath, Zone& zone, ZoneDefinitionMapType mapType) + : m_memory(memory), + m_search_path(searchPath), + m_zone(zone), + m_mapType(mapType) + { + } + + std::optional GetHandlingAssetType() const override + { + // don't handle any asset types + return std::nullopt; + } + + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + // BSP assets are added in the finalize zone step + return AssetCreationResult::NoAction(); + } + + bool FinalizeZone(AssetCreationContext& context) override + { + std::unique_ptr bsp = BSP::createBSPData(m_zone.m_name, m_search_path, m_mapType == ZoneDefinitionMapType::ZM); + if (bsp == nullptr) + return false; + + BSPLinker linker(m_memory, m_search_path, context); + bool result = linker.linkBSP(bsp.get()); + if (!result) + con::error("BSP link has failed."); + + return result; + } + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + Zone& m_zone; + ZoneDefinitionMapType m_mapType; + }; +} // namespace + +namespace BSP +{ + std::unique_ptr CreateLoaderT6(MemoryManager& memory, ISearchPath& searchPath, Zone& zone, ZoneDefinitionMapType mapType) + { + return std::make_unique(memory, searchPath, zone, mapType); + } +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.h b/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.h new file mode 100644 index 000000000..9d0d04059 --- /dev/null +++ b/src/ObjLoading/Game/T6/BSP/LoaderBSP_T6.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Asset/IAssetCreator.h" +#include "Game/T6/T6.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" +#include "Zone/Definition/ZoneDefinition.h" + +#include + +namespace BSP +{ + std::unique_ptr CreateLoaderT6(MemoryManager& memory, ISearchPath& searchPath, Zone& zone, ZoneDefinitionMapType mapType); +} // namespace BSP diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp index 796278d6b..d48c7e732 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp @@ -1,6 +1,7 @@ #include "ObjLoaderT6.h" #include "Asset/GlobalAssetPoolsLoader.h" +#include "BSP/LoaderBSP_T6.h" #include "FontIcon/CsvLoaderFontIconT6.h" #include "FontIcon/JsonLoaderFontIconT6.h" #include "Game/T6/CommonT6.h" @@ -28,6 +29,7 @@ #include "Slug/LoaderSlugT6.h" #include "Sound/LoaderSoundBankT6.h" #include "StringTable/LoaderStringTableT6.h" +#include "TechniqueSet/LoaderTechniqueSetT6.h" #include "Tracer/GdtLoaderTracerT6.h" #include "Tracer/RawLoaderTracerT6.h" #include "Utils/Logging/Log.h" @@ -380,7 +382,7 @@ namespace T6 collection.AddAssetCreator(std::make_unique>(zone)); } - void ConfigureLoaders(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) + void ConfigureLoaders(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) { auto& memory = zone.Memory(); @@ -392,7 +394,7 @@ namespace T6 // collection.AddAssetCreator(std::make_unique(memory)); collection.AddAssetCreator(xmodel::CreateLoaderT6(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderT6(memory, searchPath)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(technique_set::CreateLoaderT6(memory, searchPath)); collection.AddAssetCreator(image::CreateLoaderT6(memory, searchPath)); collection.AddAssetCreator(sound::CreateSoundBankLoaderT6(memory, searchPath)); // collection.AddAssetCreator(std::make_unique(memory)); @@ -439,13 +441,17 @@ namespace T6 // collection.AddAssetCreator(std::make_unique(memory)); collection.AddAssetCreator(z_barrier::CreateRawLoaderT6(memory, searchPath, zone)); collection.AddAssetCreator(z_barrier::CreateGdtLoaderT6(memory, searchPath, gdt, zone)); + + if (definition.m_map_type != ZoneDefinitionMapType::NONE) + collection.AddAssetCreator(BSP::CreateLoaderT6(memory, searchPath, zone, definition.m_map_type)); } } // namespace - void ObjLoader::ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const + void ObjLoader::ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const { ConfigureDefaultCreators(collection, zone); - ConfigureLoaders(collection, zone, searchPath, gdt); + ConfigureLoaders(collection, zone, searchPath, gdt, definition); ConfigureGlobalAssetPoolsLoaders(collection, zone); } } // namespace T6 diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.h b/src/ObjLoading/Game/T6/ObjLoaderT6.h index 6e210b85b..f373ef2b5 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.h +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.h @@ -18,7 +18,8 @@ namespace T6 void LoadReferencedContainersForZone(ISearchPath& searchPath, Zone& zone) const override; void UnloadContainersOfZone(Zone& zone) const override; - void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const override; + void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const override; private: static bool VerifySoundBankChecksum(const SoundBank& soundBank, const SndRuntimeAssetBank& sndRuntimeAssetBank); diff --git a/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.cpp b/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.cpp new file mode 100644 index 000000000..43cc6e0b5 --- /dev/null +++ b/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.cpp @@ -0,0 +1,186 @@ +#include "LoaderTechniqueSetT6.h" + +#include "Game/T6/T6.h" +#include "Shader/ShaderCommon.h" + +#include +#include +#include + +using namespace T6; + +namespace +{ + class TechniqueSetLoader final : public AssetCreator + { + public: + TechniqueSetLoader(MemoryManager& memory, ISearchPath& searchPath) + : m_memory(memory), + m_search_path(searchPath) + { + } + + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + const auto fileName = std::format("techniquesets/{}.json", assetName); + const auto file = m_search_path.Open(fileName); + if (!file.IsOpen()) + return AssetCreationResult::NoAction(); + + MaterialTechniqueSet* techniqueSet = new MaterialTechniqueSet; + memset(techniqueSet, 0, sizeof(MaterialTechniqueSet)); + + const auto js = nlohmann::json::parse(*file.m_stream.get()); + + std::string techSetName = js["name"]; + techniqueSet->name = _strdup(techSetName.c_str()); + techniqueSet->worldVertFormat = (unsigned char)js["worldVertFormat"]; + + for (int i = 0; i < 36; i++) + { + nlohmann::json techniqueJs = js["techniques"][i]; + + if (techniqueJs.size() == 0) + { + techniqueSet->techniques[i] = nullptr; + } + else + { + MaterialTechnique* technique = new MaterialTechnique; + techniqueSet->techniques[i] = technique; + + std::string techName = techniqueJs["name"]; + technique->name = _strdup(techName.c_str()); + technique->flags = techniqueJs["flags"]; + technique->passCount = techniqueJs["passCount"]; + assert(technique->passCount == 1); + + for (auto passIndex = 0u; passIndex < technique->passCount; passIndex++) + { + MaterialPass* currPass = &technique->passArray[passIndex]; + nlohmann::json passJs = techniqueJs["passArray"][passIndex]; + + currPass->perPrimArgCount = (unsigned char)passJs["perPrimArgCount"]; + currPass->perObjArgCount = (unsigned char)passJs["perObjArgCount"]; + currPass->stableArgCount = (unsigned char)passJs["stableArgCount"]; + currPass->customSamplerFlags = (unsigned char)passJs["customSamplerFlags"]; + currPass->precompiledIndex = (unsigned char)passJs["precompiledIndex"]; + currPass->materialType = (unsigned char)passJs["materialType"]; + + int argCount = currPass->perPrimArgCount + currPass->perObjArgCount + currPass->stableArgCount; + if (argCount == 0) + { + currPass->args = nullptr; + } + else + { + currPass->args = new MaterialShaderArgument[argCount]; + for (int i = 0; i < argCount; i++) + { + MaterialShaderArgument* currArg = &currPass->args[i]; + auto currArgJs = passJs["args"][i]; + + currArg->type = currArgJs["type"]; + currArg->location.offset = currArgJs["location"]; + currArg->size = currArgJs["size"]; + currArg->buffer = currArgJs["buffer"]; + if (currArg->type == MTL_ARG_LITERAL_VERTEX_CONST || currArg->type == MTL_ARG_LITERAL_PIXEL_CONST) + { + float* consts = new float[4]; + consts[0] = currArgJs["u"]["const0"]; + consts[1] = currArgJs["u"]["const1"]; + consts[2] = currArgJs["u"]["const2"]; + consts[3] = currArgJs["u"]["const3"]; + currArg->u.literalConst = (float(*)[4])consts; + } + else + { + currArg->u.nameHash = currArgJs["u"]["value"]; + } + } + } + + if (passJs["vertexDecl"].size() == 0) + { + currPass->vertexDecl = nullptr; + } + else + { + currPass->vertexDecl = new MaterialVertexDeclaration; + currPass->vertexDecl->streamCount = (unsigned char)passJs["vertexDecl"]["streamCount"]; + currPass->vertexDecl->hasOptionalSource = passJs["vertexDecl"]["hasOptionalSource"]; + currPass->vertexDecl->isLoaded = passJs["vertexDecl"]["isLoaded"]; + for (int i = 0; i < 16; i++) + { + currPass->vertexDecl->routing.data[i].source = (unsigned char)passJs["vertexDecl"]["routing"][i]["source"]; + currPass->vertexDecl->routing.data[i].dest = (unsigned char)passJs["vertexDecl"]["routing"][i]["dest"]; + currPass->vertexDecl->routing.decl[i] = nullptr; + } + } + + if (passJs["pixelShader"].size() == 0) + { + currPass->pixelShader = nullptr; + } + else + { + currPass->pixelShader = new MaterialPixelShader; + + std::string pixelName = passJs["pixelShader"]["name"]; + currPass->pixelShader->name = _strdup(pixelName.c_str()); + currPass->pixelShader->prog.ps = nullptr; + + const auto psFileName = shader::GetFileNameForPixelShaderAssetName(pixelName); + const auto psFile = m_search_path.Open(psFileName); + if (!psFile.IsOpen()) + { + printf("ERROR: Cant find pixel shader %s\n", psFileName.c_str()); + return AssetCreationResult::Failure(); + } + currPass->pixelShader->prog.loadDef.programSize = (unsigned int)psFile.m_length; + currPass->pixelShader->prog.loadDef.program = new char[(unsigned int)psFile.m_length]; + psFile.m_stream->read(currPass->pixelShader->prog.loadDef.program, psFile.m_length); + } + + if (passJs["vertexShader"].size() == 0) + { + currPass->vertexShader = nullptr; + } + else + { + currPass->vertexShader = new MaterialVertexShader; + + std::string vertexName = passJs["vertexShader"]["name"]; + currPass->vertexShader->name = _strdup(vertexName.c_str()); + currPass->vertexShader->prog.vs = nullptr; + + const auto vsFileName = shader::GetFileNameForVertexShaderAssetName(vertexName); + const auto vsFile = m_search_path.Open(vsFileName); + if (!vsFile.IsOpen()) + { + printf("ERROR: Cant find vertex shader %s\n", vsFileName.c_str()); + return AssetCreationResult::Failure(); + } + currPass->vertexShader->prog.loadDef.programSize = (unsigned int)vsFile.m_length; + currPass->vertexShader->prog.loadDef.program = new char[(unsigned int)vsFile.m_length]; + vsFile.m_stream->read(currPass->vertexShader->prog.loadDef.program, vsFile.m_length); + } + } + } + } + return AssetCreationResult::Success(context.AddAsset(assetName, techniqueSet)); + } + + private: + MemoryManager& m_memory; + ISearchPath& m_search_path; + }; +} // namespace + +namespace technique_set +{ + std::unique_ptr> CreateLoaderT6(MemoryManager& memory, ISearchPath& searchPath) + { + return std::make_unique(memory, searchPath); + } +} // namespace technique_set diff --git a/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.h b/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.h new file mode 100644 index 000000000..35b2b7108 --- /dev/null +++ b/src/ObjLoading/Game/T6/TechniqueSet/LoaderTechniqueSetT6.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Asset/IAssetCreator.h" +#include "Game/T6/T6.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace technique_set +{ + std::unique_ptr> CreateLoaderT6(MemoryManager& memory, ISearchPath& searchPath); +} // namespace technique_set diff --git a/src/ObjLoading/IObjLoader.h b/src/ObjLoading/IObjLoader.h index b534cac63..15aa82781 100644 --- a/src/ObjLoading/IObjLoader.h +++ b/src/ObjLoading/IObjLoader.h @@ -3,6 +3,7 @@ #include "Asset/AssetCreatorCollection.h" #include "Gdt/IGdtQueryable.h" #include "SearchPath/ISearchPath.h" +#include "Zone/Definition/ZoneDefinition.h" #include "Zone/Zone.h" class IObjLoader @@ -28,7 +29,8 @@ class IObjLoader */ virtual void UnloadContainersOfZone(Zone& zone) const = 0; - virtual void ConfigureCreatorCollection(AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt) const = 0; + virtual void ConfigureCreatorCollection( + AssetCreatorCollection& collection, Zone& zone, ISearchPath& searchPath, IGdtQueryable& gdt, ZoneDefinition& definition) const = 0; static const IObjLoader* GetObjLoaderForGame(GameId game); }; diff --git a/src/ObjLoading/Material/JsonMaterialLoader.cpp.template b/src/ObjLoading/Material/JsonMaterialLoader.cpp.template index 248d12ccd..1be82e990 100644 --- a/src/ObjLoading/Material/JsonMaterialLoader.cpp.template +++ b/src/ObjLoading/Material/JsonMaterialLoader.cpp.template @@ -535,4 +535,4 @@ namespace GAME return loader.Load(material); } -} // namespace GAME +} // namespace GAME \ No newline at end of file diff --git a/src/ObjLoading/Material/JsonMaterialLoader.h.template b/src/ObjLoading/Material/JsonMaterialLoader.h.template index 04b768df0..0de4f692f 100644 --- a/src/ObjLoading/Material/JsonMaterialLoader.h.template +++ b/src/ObjLoading/Material/JsonMaterialLoader.h.template @@ -20,4 +20,4 @@ namespace GAME { bool LoadMaterialAsJson( std::istream& stream, Material& material, MemoryManager& memory, AssetCreationContext& context, AssetRegistration& registration); -} // namespace GAME +} // namespace GAME \ No newline at end of file diff --git a/src/ObjWriting/Game/T6/Techset/TechsetDumperT6.cpp b/src/ObjWriting/Game/T6/Techset/TechsetDumperT6.cpp index 157b17eaa..8285281e6 100644 --- a/src/ObjWriting/Game/T6/Techset/TechsetDumperT6.cpp +++ b/src/ObjWriting/Game/T6/Techset/TechsetDumperT6.cpp @@ -2,9 +2,11 @@ #include "Shader/ShaderCommon.h" +#include #include #include +using namespace nlohmann; using namespace T6; namespace @@ -87,21 +89,111 @@ namespace techset const auto* techniqueSet = asset.Asset(); auto* shaderState = context.GetZoneAssetDumperState(); + const auto assetFile = context.OpenAssetFile(std::format("techniquesets/{}.json", techniqueSet->name)); + if (!assetFile) + return; + + json js; + + js["name"] = techniqueSet->name; + js["worldVertFormat"] = techniqueSet->worldVertFormat; + + js["techniques"] = json::array(); for (const auto* technique : techniqueSet->techniques) { - if (!technique || !shaderState->ShouldDumpTechnique(technique)) - continue; + json techniqueJs = json::object(); - for (auto passIndex = 0u; passIndex < technique->passCount; passIndex++) + if (technique != nullptr) { - const auto* pixelShader = technique->passArray[passIndex].pixelShader; - if (pixelShader && shaderState->ShouldDumpPixelShader(pixelShader)) - DumpPixelShader(context, *pixelShader); - - const auto* vertexShader = technique->passArray[passIndex].vertexShader; - if (vertexShader && shaderState->ShouldDumpVertexShader(vertexShader)) - DumpVertexShader(context, *vertexShader); + techniqueJs["name"] = technique->name; + techniqueJs["flags"] = technique->flags; + techniqueJs["passCount"] = technique->passCount; + + assert(technique->passCount == 1); + + techniqueJs["passArray"] = json::array(); + for (auto passIndex = 0u; passIndex < technique->passCount; passIndex++) + { + const MaterialPass* currPass = &technique->passArray[passIndex]; + json passJs = json::object(); + + passJs["perPrimArgCount"] = currPass->perPrimArgCount; + passJs["perObjArgCount"] = currPass->perObjArgCount; + passJs["stableArgCount"] = currPass->stableArgCount; + passJs["customSamplerFlags"] = currPass->customSamplerFlags; + passJs["precompiledIndex"] = currPass->precompiledIndex; + passJs["materialType"] = currPass->materialType; + + json vertDeclJs = json::object(); + if (currPass->vertexDecl != nullptr) + { + vertDeclJs["streamCount"] = currPass->vertexDecl->streamCount; + vertDeclJs["hasOptionalSource"] = currPass->vertexDecl->hasOptionalSource; + vertDeclJs["isLoaded"] = currPass->vertexDecl->isLoaded; + for (int i = 0; i < 16; i++) + { + vertDeclJs["routing"][i]["source"] = currPass->vertexDecl->routing.data[i].source; + vertDeclJs["routing"][i]["dest"] = currPass->vertexDecl->routing.data[i].dest; + + assert(currPass->vertexDecl->routing.decl[i] == nullptr); + } + } + passJs["vertexDecl"] = vertDeclJs; + + passJs["args"] = json::array(); + if (currPass->args != nullptr) + { + for (int i = 0; i < currPass->perPrimArgCount + currPass->perObjArgCount + currPass->stableArgCount; i++) + { + json argsJs = json::object(); + MaterialShaderArgument* currArg = &currPass->args[i]; + + argsJs["type"] = currArg->type; + argsJs["location"] = currArg->location.offset; + argsJs["size"] = currArg->size; + argsJs["buffer"] = currArg->buffer; + if (currArg->type == MTL_ARG_LITERAL_VERTEX_CONST || currArg->type == MTL_ARG_LITERAL_PIXEL_CONST) + { + argsJs["u"]["const0"] = currArg->u.literalConst[0]; + argsJs["u"]["const1"] = currArg->u.literalConst[1]; + argsJs["u"]["const2"] = currArg->u.literalConst[2]; + argsJs["u"]["const3"] = currArg->u.literalConst[3]; + } + else + { + argsJs["u"]["value"] = currArg->u.nameHash; + } + + passJs["args"].push_back(argsJs); + } + } + + json pixelJs = json::object(); + if (currPass->pixelShader != nullptr) + { + pixelJs["name"] = currPass->pixelShader->name; + if (shaderState->ShouldDumpPixelShader(currPass->pixelShader)) + DumpPixelShader(context, *currPass->pixelShader); + } + passJs["pixelShader"] = pixelJs; + + json vertexJs = json::object(); + if (currPass->vertexShader != nullptr) + { + vertexJs["name"] = currPass->vertexShader->name; + if (shaderState->ShouldDumpVertexShader(currPass->vertexShader)) + DumpVertexShader(context, *currPass->vertexShader); + } + passJs["vertexShader"] = vertexJs; + + techniqueJs["passArray"].push_back(passJs); + } } + + js["techniques"].push_back(techniqueJs); } + + std::string jsonString = js.dump(4); + assetFile->write(jsonString.c_str(), jsonString.size()); } } // namespace techset diff --git a/src/ZoneCommon/Zone/Definition/Parsing/Sequence/SequenceZoneDefinitionMetaData.cpp b/src/ZoneCommon/Zone/Definition/Parsing/Sequence/SequenceZoneDefinitionMetaData.cpp index 617528421..8b40c24fb 100644 --- a/src/ZoneCommon/Zone/Definition/Parsing/Sequence/SequenceZoneDefinitionMetaData.cpp +++ b/src/ZoneCommon/Zone/Definition/Parsing/Sequence/SequenceZoneDefinitionMetaData.cpp @@ -10,6 +10,8 @@ namespace { + constexpr auto METADATA_MAP_TYPE = "map"; + constexpr auto METADATA_GAME = "game"; constexpr auto METADATA_GDT = "gdt"; constexpr auto METADATA_NAME = "name"; @@ -134,6 +136,25 @@ void SequenceZoneDefinitionMetaData::ProcessMatch(ZoneDefinitionParserState* sta { ProcessMetaDataGame(state, valueToken, value); } + else if (key == METADATA_MAP_TYPE) + { + std::string valueLowerCase = value; + std::transform(valueLowerCase.begin(), + valueLowerCase.end(), + valueLowerCase.begin(), + [](unsigned char c) + { + return std::tolower(c); + }); + if (valueLowerCase.compare("sp") == 0) + state->SetMapType(ZoneDefinitionMapType::SP); + else if (valueLowerCase.compare("mp") == 0) + state->SetMapType(ZoneDefinitionMapType::MP); + else if (valueLowerCase.compare("zm") == 0) + state->SetMapType(ZoneDefinitionMapType::ZM); + else + throw ParsingException(valueToken.GetPos(), "map must be SP, MP or ZM"); + } else if (key == METADATA_GDT) { state->m_definition->m_gdts.emplace_back(value); diff --git a/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.cpp b/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.cpp index af7a1a622..53aaa0a10 100644 --- a/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.cpp +++ b/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.cpp @@ -19,6 +19,11 @@ void ZoneDefinitionParserState::SetGame(const GameId game) m_asset_name_resolver = IAssetNameResolver::GetResolverForGame(game); } +void ZoneDefinitionParserState::SetMapType(ZoneDefinitionMapType mapType) +{ + m_definition->m_map_type = mapType; +} + namespace { void AddCurrentObjContainerToDefinitionIfNecessary(ZoneDefinition& zoneDefinition, std::optional& maybeObjContainer) diff --git a/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.h b/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.h index 4fa605152..74d9a0810 100644 --- a/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.h +++ b/src/ZoneCommon/Zone/Definition/Parsing/ZoneDefinitionParserState.h @@ -17,6 +17,8 @@ class ZoneDefinitionParserState void SetGame(GameId game); + void SetMapType(ZoneDefinitionMapType mapType); + void StartIPak(std::string ipakName); void StartIwd(std::string iwdName); diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp index 41c401da9..53233f546 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp @@ -34,6 +34,7 @@ void ZoneDefinitionProperties::Include(const ZoneDefinitionProperties& otherProp } ZoneDefinition::ZoneDefinition() - : m_game(GameId::COUNT) + : m_game(GameId::COUNT), + m_map_type(ZoneDefinitionMapType::NONE) { } diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h index 0d64f161d..e774b1f26 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h @@ -15,6 +15,14 @@ enum class ZoneDefinitionObjContainerType : uint8_t IPAK }; +enum class ZoneDefinitionMapType : uint8_t +{ + NONE, + SP, + MP, + ZM +}; + class ZoneDefinitionObjContainer { public: @@ -69,4 +77,5 @@ class ZoneDefinition std::vector m_gdts; std::vector m_assets; std::vector m_obj_containers; + ZoneDefinitionMapType m_map_type; };