From b282e611fc16484934bd8fa4754f2a9c21ac92c7 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Mon, 9 Sep 2024 18:46:39 +0200
Subject: [PATCH 01/17] refactor: extract PartClassificationState from
 JsonXModelLoader

---
 .../Game/T6/XModel/JsonXModelLoader.cpp       | 82 +------------------
 .../XModel/PartClassificationState.cpp        | 79 ++++++++++++++++++
 .../XModel/PartClassificationState.h          | 24 ++++++
 3 files changed, 105 insertions(+), 80 deletions(-)
 create mode 100644 src/ObjLoading/XModel/PartClassificationState.cpp
 create mode 100644 src/ObjLoading/XModel/PartClassificationState.h

diff --git a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp b/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
index bb8d40cd..2f0639c1 100644
--- a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
+++ b/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
@@ -15,6 +15,7 @@
 #include <Eigen>
 #pragma warning(pop)
 
+#include "XModel/PartClassificationState.h"
 #include "XModel/Tangentspace.h"
 
 #include <algorithm>
@@ -59,85 +60,6 @@ namespace
     };
     static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
 
-    class PartClassificationState final : public IZoneAssetLoaderState
-    {
-        // TODO: Use MP part classifications when building an mp fastfile
-        static constexpr auto PART_CLASSIFICATION_FILE = "partclassification.csv";
-
-    public:
-        PartClassificationState()
-            : m_loaded(false)
-        {
-        }
-
-        bool Load(const IAssetLoadingManager& manager)
-        {
-            if (m_loaded)
-                return true;
-
-            if (ObjLoading::Configuration.Verbose)
-                std::cout << "Loading part classification...\n";
-
-            const auto file = manager.GetAssetLoadingContext()->m_raw_search_path->Open(PART_CLASSIFICATION_FILE);
-            if (!file.IsOpen())
-            {
-                std::cerr << std::format("Could not load part classification: Failed to open {}\n", PART_CLASSIFICATION_FILE);
-                return false;
-            }
-
-            CsvInputStream csvStream(*file.m_stream);
-            std::vector<std::string> row;
-            auto rowIndex = 0u;
-            while (csvStream.NextRow(row))
-            {
-                if (!LoadRow(rowIndex++, row))
-                    return false;
-            }
-
-            m_loaded = true;
-
-            return false;
-        }
-
-        [[nodiscard]] unsigned GetPartClassificationForBoneName(const std::string& boneName) const
-        {
-            const auto entry = m_part_classifications.find(boneName);
-
-            return entry != m_part_classifications.end() ? entry->second : HITLOC_NONE;
-        }
-
-    private:
-        bool LoadRow(const unsigned rowIndex, std::vector<std::string>& row)
-        {
-            if (row.empty())
-                return true;
-
-            if (row.size() != 2)
-            {
-                std::cerr << "Could not load part classification: Invalid row\n";
-                return false;
-            }
-
-            utils::MakeStringLowerCase(row[0]);
-            utils::MakeStringLowerCase(row[1]);
-
-            const auto foundHitLoc = std::ranges::find(HITLOC_NAMES, row[1]);
-            if (foundHitLoc == std::end(HITLOC_NAMES))
-            {
-                std::cerr << std::format("Invalid hitloc name in row {}: {}\n", rowIndex + 1, row[1]);
-                return false;
-            }
-
-            const auto hitLocNum = std::distance(std::begin(HITLOC_NAMES), foundHitLoc);
-
-            m_part_classifications.emplace(row[0], hitLocNum);
-            return true;
-        }
-
-        bool m_loaded;
-        std::unordered_map<std::string, unsigned> m_part_classifications;
-    };
-
     class TangentData
     {
     public:
@@ -362,7 +284,7 @@ namespace
             if (common.m_bones.empty())
                 return true;
 
-            m_part_classification_state.Load(m_manager);
+            m_part_classification_state.Load(HITLOC_NAMES, std::extent_v<decltype(HITLOC_NAMES)>, m_manager);
 
             const auto boneCount = common.m_bones.size();
             constexpr auto maxBones = std::numeric_limits<decltype(XModel::numBones)>::max();
diff --git a/src/ObjLoading/XModel/PartClassificationState.cpp b/src/ObjLoading/XModel/PartClassificationState.cpp
new file mode 100644
index 00000000..a09e3374
--- /dev/null
+++ b/src/ObjLoading/XModel/PartClassificationState.cpp
@@ -0,0 +1,79 @@
+#include "PartClassificationState.h"
+
+#include "Csv/CsvStream.h"
+#include "ObjLoading.h"
+#include "Utils/StringUtils.h"
+
+#include <format>
+#include <iostream>
+
+PartClassificationState::PartClassificationState()
+    : m_loaded(false)
+{
+}
+
+bool PartClassificationState::Load(const char** hitLocNames, const size_t hitLocNameCount, const IAssetLoadingManager& manager)
+{
+    if (m_loaded)
+        return true;
+
+    if (ObjLoading::Configuration.Verbose)
+        std::cout << "Loading part classification...\n";
+
+    const auto file = manager.GetAssetLoadingContext()->m_raw_search_path->Open(PART_CLASSIFICATION_FILE);
+    if (!file.IsOpen())
+    {
+        std::cerr << std::format("Could not load part classification: Failed to open {}\n", PART_CLASSIFICATION_FILE);
+        return false;
+    }
+
+    const auto hitLocStart = hitLocNames;
+    const auto hitLocEnd = &hitLocNames[hitLocNameCount];
+
+    const CsvInputStream csvStream(*file.m_stream);
+    std::vector<std::string> row;
+    auto rowIndex = 0u;
+    while (csvStream.NextRow(row))
+    {
+        if (!LoadRow(hitLocStart, hitLocEnd, rowIndex++, row))
+            return false;
+    }
+
+    m_loaded = true;
+
+    return false;
+}
+
+[[nodiscard]] unsigned PartClassificationState::GetPartClassificationForBoneName(const std::string& boneName) const
+{
+    const auto entry = m_part_classifications.find(boneName);
+
+    return entry != m_part_classifications.end() ? entry->second : HITLOC_NONE;
+}
+
+bool PartClassificationState::LoadRow(const char** hitLocStart, const char** hitLocEnd, const unsigned rowIndex, std::vector<std::string>& row)
+{
+    if (row.empty())
+        return true;
+
+    if (row.size() != 2)
+    {
+        std::cerr << "Could not load part classification: Invalid row\n";
+        return false;
+    }
+
+    utils::MakeStringLowerCase(row[0]);
+    utils::MakeStringLowerCase(row[1]);
+
+    const auto foundHitLoc = std::find(hitLocStart, hitLocEnd, row[1]);
+    if (foundHitLoc == hitLocEnd)
+    {
+        std::cerr << std::format("Invalid hitloc name in row {}: {}\n", rowIndex + 1, row[1]);
+        return false;
+    }
+
+    const auto hitLocNum = std::distance(hitLocStart, foundHitLoc);
+
+    m_part_classifications.emplace(row[0], hitLocNum);
+    return true;
+}
diff --git a/src/ObjLoading/XModel/PartClassificationState.h b/src/ObjLoading/XModel/PartClassificationState.h
new file mode 100644
index 00000000..32bd4afc
--- /dev/null
+++ b/src/ObjLoading/XModel/PartClassificationState.h
@@ -0,0 +1,24 @@
+#pragma once
+#include "AssetLoading/IAssetLoadingManager.h"
+#include "AssetLoading/IZoneAssetLoaderState.h"
+
+class PartClassificationState final : public IZoneAssetLoaderState
+{
+    // TODO: Use MP part classifications when building an mp fastfile
+    static constexpr auto PART_CLASSIFICATION_FILE = "partclassification.csv";
+
+    static constexpr auto HITLOC_NONE = 0u;
+
+public:
+    PartClassificationState();
+
+    bool Load(const char** hitLocNames, size_t hitLocNameCount, const IAssetLoadingManager& manager);
+
+    [[nodiscard]] unsigned GetPartClassificationForBoneName(const std::string& boneName) const;
+
+private:
+    bool LoadRow(const char** hitLocStart, const char** hitLocEnd, unsigned rowIndex, std::vector<std::string>& row);
+
+    bool m_loaded;
+    std::unordered_map<std::string, unsigned> m_part_classifications;
+};

From 543a4f06e471d4aebea396198a96985849c0cfda Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Mon, 9 Sep 2024 18:53:36 +0200
Subject: [PATCH 02/17] refactor: extract TangentData from JsonXModelLoader

---
 .../Game/T6/XModel/JsonXModelLoader.cpp       | 52 +------------------
 src/ObjLoading/XModel/TangentData.cpp         | 47 +++++++++++++++++
 src/ObjLoading/XModel/TangentData.h           | 15 ++++++
 3 files changed, 63 insertions(+), 51 deletions(-)
 create mode 100644 src/ObjLoading/XModel/TangentData.cpp
 create mode 100644 src/ObjLoading/XModel/TangentData.h

diff --git a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp b/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
index 2f0639c1..8b3f6e9b 100644
--- a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
+++ b/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
@@ -16,6 +16,7 @@
 #pragma warning(pop)
 
 #include "XModel/PartClassificationState.h"
+#include "XModel/TangentData.h"
 #include "XModel/Tangentspace.h"
 
 #include <algorithm>
@@ -60,57 +61,6 @@ namespace
     };
     static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
 
-    class TangentData
-    {
-    public:
-        void CreateTangentData(const XModelCommon& common)
-        {
-            if (common.m_vertices.empty())
-                return;
-
-            const auto vertexCount = common.m_vertices.size();
-            m_tangents.resize(vertexCount);
-            m_binormals.resize(vertexCount);
-
-            auto triCount = 0u;
-            for (const auto& object : common.m_objects)
-                triCount += object.m_faces.size();
-
-            std::vector<uint16_t> indices(triCount * 3u);
-            auto triOffset = 0u;
-            for (const auto& object : common.m_objects)
-            {
-                for (const auto& face : object.m_faces)
-                {
-                    indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[0]);
-                    indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[1]);
-                    indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[2]);
-                }
-            }
-
-            const auto& firstVertex = common.m_vertices[0];
-
-            tangent_space::VertexData vertexData{
-                firstVertex.coordinates,
-                sizeof(XModelVertex),
-                firstVertex.normal,
-                sizeof(XModelVertex),
-                firstVertex.uv,
-                sizeof(XModelVertex),
-                m_tangents.data(),
-                sizeof(float) * 3,
-                m_binormals.data(),
-                sizeof(float) * 3,
-                indices.data(),
-            };
-
-            tangent_space::CalculateTangentSpace(vertexData, triCount, vertexCount);
-        }
-
-        std::vector<std::array<float, 3>> m_tangents;
-        std::vector<std::array<float, 3>> m_binormals;
-    };
-
     class JsonLoader
     {
     public:
diff --git a/src/ObjLoading/XModel/TangentData.cpp b/src/ObjLoading/XModel/TangentData.cpp
new file mode 100644
index 00000000..a09c25f3
--- /dev/null
+++ b/src/ObjLoading/XModel/TangentData.cpp
@@ -0,0 +1,47 @@
+#include "TangentData.h"
+
+#include "Tangentspace.h"
+
+void TangentData::CreateTangentData(const XModelCommon& common)
+{
+    if (common.m_vertices.empty())
+        return;
+
+    const auto vertexCount = common.m_vertices.size();
+    m_tangents.resize(vertexCount);
+    m_binormals.resize(vertexCount);
+
+    auto triCount = 0u;
+    for (const auto& object : common.m_objects)
+        triCount += object.m_faces.size();
+
+    std::vector<uint16_t> indices(triCount * 3u);
+    auto triOffset = 0u;
+    for (const auto& object : common.m_objects)
+    {
+        for (const auto& face : object.m_faces)
+        {
+            indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[0]);
+            indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[1]);
+            indices[triOffset++] = static_cast<uint16_t>(face.vertexIndex[2]);
+        }
+    }
+
+    const auto& firstVertex = common.m_vertices[0];
+
+    tangent_space::VertexData vertexData{
+        firstVertex.coordinates,
+        sizeof(XModelVertex),
+        firstVertex.normal,
+        sizeof(XModelVertex),
+        firstVertex.uv,
+        sizeof(XModelVertex),
+        m_tangents.data(),
+        sizeof(float) * 3,
+        m_binormals.data(),
+        sizeof(float) * 3,
+        indices.data(),
+    };
+
+    tangent_space::CalculateTangentSpace(vertexData, triCount, vertexCount);
+}
diff --git a/src/ObjLoading/XModel/TangentData.h b/src/ObjLoading/XModel/TangentData.h
new file mode 100644
index 00000000..87ae77a4
--- /dev/null
+++ b/src/ObjLoading/XModel/TangentData.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "XModel/XModelCommon.h"
+
+#include <array>
+#include <vector>
+
+class TangentData
+{
+public:
+    void CreateTangentData(const XModelCommon& common);
+
+    std::vector<std::array<float, 3>> m_tangents;
+    std::vector<std::array<float, 3>> m_binormals;
+};

From e9c66a2e28b99acb8cb6ae948d342899ef869217 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Mon, 9 Sep 2024 23:46:55 +0200
Subject: [PATCH 03/17] chore: move xmodel dumping and loading code into
 generic files

---
 .../T6/AssetLoaders/AssetLoaderXModel.cpp     |   4 +-
 .../Game/T6/XModel/JsonXModelLoader.h         |  13 -
 .../Game/T6/XModel/XModelLoaderT6.cpp         |  51 ++
 .../Game/T6/XModel/XModelLoaderT6.h           |  13 +
 .../GenericXModelLoader.inc.h}                |  71 +-
 .../T6/AssetDumpers/AssetDumperXModel.cpp     | 594 +--------------
 .../Game/T6/XModel/JsonXModelWriter.cpp       | 102 ---
 .../Game/T6/XModel/JsonXModelWriter.h         |  11 -
 .../Game/T6/XModel/XModelDumperT6.cpp         |  17 +
 .../Game/T6/XModel/XModelDumperT6.h           |   9 +
 .../XModel/GenericXModelDumper.inc.h          | 675 ++++++++++++++++++
 11 files changed, 781 insertions(+), 779 deletions(-)
 delete mode 100644 src/ObjLoading/Game/T6/XModel/JsonXModelLoader.h
 create mode 100644 src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
 create mode 100644 src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
 rename src/ObjLoading/{Game/T6/XModel/JsonXModelLoader.cpp => XModel/GenericXModelLoader.inc.h} (95%)
 delete mode 100644 src/ObjWriting/Game/T6/XModel/JsonXModelWriter.cpp
 delete mode 100644 src/ObjWriting/Game/T6/XModel/JsonXModelWriter.h
 create mode 100644 src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
 create mode 100644 src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
 create mode 100644 src/ObjWriting/XModel/GenericXModelDumper.inc.h

diff --git a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
index 8509d2d3..2c1ff6f1 100644
--- a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
+++ b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
@@ -1,7 +1,7 @@
 #include "AssetLoaderXModel.h"
 
 #include "Game/T6/T6.h"
-#include "Game/T6/XModel/JsonXModelLoader.h"
+#include "Game/T6/XModel/XModelLoaderT6.h"
 #include "Pool/GlobalAssetPool.h"
 
 #include <cstring>
@@ -33,7 +33,7 @@ bool AssetLoaderXModel::LoadFromRaw(
     xmodel->name = memory->Dup(assetName.c_str());
 
     std::vector<XAssetInfoGeneric*> dependencies;
-    if (LoadXModelAsJson(*file.m_stream, *xmodel, memory, manager, dependencies))
+    if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
         manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
     else
         std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
diff --git a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.h b/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.h
deleted file mode 100644
index d7747287..00000000
--- a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include "AssetLoading/IAssetLoadingManager.h"
-#include "Game/T6/T6.h"
-#include "Utils/MemoryManager.h"
-
-#include <istream>
-
-namespace T6
-{
-    bool LoadXModelAsJson(
-        std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
-} // namespace T6
diff --git a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
new file mode 100644
index 00000000..32e830a9
--- /dev/null
+++ b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
@@ -0,0 +1,51 @@
+#include "XModelLoaderT6.h"
+
+#include "Game/T6/CommonT6.h"
+#include "Game/T6/Json/JsonXModel.h"
+
+#define GAME_NAMESPACE T6
+
+namespace T6
+{
+    const char* HITLOC_NAMES[]{
+        // clang-format off
+        "none",
+        "helmet",
+        "head",
+        "neck",
+        "torso_upper",
+        "torso_middle",
+        "torso_lower",
+        "right_arm_upper",
+        "left_arm_upper",
+        "right_arm_lower",
+        "left_arm_lower",
+        "right_hand",
+        "left_hand",
+        "right_leg_upper",
+        "left_leg_upper",
+        "right_leg_lower",
+        "left_leg_lower",
+        "right_foot",
+        "left_foot",
+        "gun",
+        "shield",
+        // clang-format on
+    };
+    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
+} // namespace T6
+
+#include "XModel/GenericXModelLoader.inc.h"
+
+namespace T6
+{
+    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
+    {
+        std::set<XAssetInfoGeneric*> dependenciesSet;
+        XModelLoader loader(stream, *memory, *manager, dependenciesSet);
+
+        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
+
+        return loader.Load(xmodel);
+    }
+} // namespace T6
diff --git a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
new file mode 100644
index 00000000..e55883e5
--- /dev/null
+++ b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "AssetLoading/IAssetLoadingManager.h"
+#include "Game/T6/T6.h"
+#include "Utils/MemoryManager.h"
+
+#include <istream>
+#include <vector>
+
+namespace T6
+{
+    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
+}
diff --git a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp b/src/ObjLoading/XModel/GenericXModelLoader.inc.h
similarity index 95%
rename from src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
rename to src/ObjLoading/XModel/GenericXModelLoader.inc.h
index 8b3f6e9b..74ac5fd0 100644
--- a/src/ObjLoading/Game/T6/XModel/JsonXModelLoader.cpp
+++ b/src/ObjLoading/XModel/GenericXModelLoader.inc.h
@@ -1,8 +1,9 @@
-#include "JsonXModelLoader.h"
+#pragma once
+
+#ifndef GAME_NAMESPACE
+#error Must define GAME_NAMESPACE
+#endif
 
-#include "Csv/CsvStream.h"
-#include "Game/T6/CommonT6.h"
-#include "Game/T6/Json/JsonXModel.h"
 #include "ObjLoading.h"
 #include "Utils/QuatInt16.h"
 #include "Utils/StringUtils.h"
@@ -13,6 +14,7 @@
 
 #pragma warning(push, 0)
 #include <Eigen>
+#include <nlohmann/json.hpp>
 #pragma warning(pop)
 
 #include "XModel/PartClassificationState.h"
@@ -23,48 +25,15 @@
 #include <filesystem>
 #include <format>
 #include <iostream>
-#include <nlohmann/json.hpp>
 #include <numeric>
 #include <vector>
 
-using namespace nlohmann;
-using namespace T6;
-
-namespace fs = std::filesystem;
-
-namespace
+namespace GAME_NAMESPACE
 {
-    const char* HITLOC_NAMES[]{
-        // clang-format off
-        "none",
-        "helmet",
-        "head",
-        "neck",
-        "torso_upper",
-        "torso_middle",
-        "torso_lower",
-        "right_arm_upper",
-        "left_arm_upper",
-        "right_arm_lower",
-        "left_arm_lower",
-        "right_hand",
-        "left_hand",
-        "right_leg_upper",
-        "left_leg_upper",
-        "right_leg_lower",
-        "left_leg_lower",
-        "right_foot",
-        "left_foot",
-        "gun",
-        "shield",
-        // clang-format on
-    };
-    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
-
-    class JsonLoader
+    class XModelLoader
     {
     public:
-        JsonLoader(std::istream& stream, MemoryManager& memory, IAssetLoadingManager& manager, std::set<XAssetInfoGeneric*>& dependencies)
+        XModelLoader(std::istream& stream, MemoryManager& memory, IAssetLoadingManager& manager, std::set<XAssetInfoGeneric*>& dependencies)
             : m_stream(stream),
               m_memory(memory),
               m_script_strings(manager.GetAssetLoadingContext()->m_zone->m_script_strings),
@@ -77,7 +46,7 @@ namespace
 
         bool Load(XModel& xmodel)
         {
-            const auto jRoot = json::parse(m_stream);
+            const auto jRoot = nlohmann::json::parse(m_stream);
             std::string type;
             unsigned version;
 
@@ -95,7 +64,7 @@ namespace
                 const auto jXModel = jRoot.get<JsonXModel>();
                 return CreateXModelFromJson(jXModel, xmodel);
             }
-            catch (const json::exception& e)
+            catch (const nlohmann::json::exception& e)
             {
                 std::cerr << std::format("Failed to parse json of xmodel: {}\n", e.what());
             }
@@ -616,7 +585,7 @@ namespace
                 return false;
             }
 
-            auto extension = fs::path(jLod.file).extension().string();
+            auto extension = std::filesystem::path(jLod.file).extension().string();
             utils::MakeStringLowerCase(extension);
 
             const auto common = LoadModelByExtension(*file.m_stream, extension);
@@ -822,18 +791,4 @@ namespace
         PartClassificationState& m_part_classification_state;
         std::set<XAssetInfoGeneric*>& m_dependencies;
     };
-} // namespace
-
-namespace T6
-{
-    bool LoadXModelAsJson(
-        std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
-    {
-        std::set<XAssetInfoGeneric*> dependenciesSet;
-        JsonLoader loader(stream, *memory, *manager, dependenciesSet);
-
-        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
-
-        return loader.Load(xmodel);
-    }
-} // namespace T6
+} // namespace GAME_NAMESPACE
diff --git a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperXModel.cpp b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperXModel.cpp
index d5add6b3..24dc56dc 100644
--- a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperXModel.cpp
+++ b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperXModel.cpp
@@ -1,600 +1,9 @@
 #include "AssetDumperXModel.h"
 
-#include "Game/T6/CommonT6.h"
-#include "Game/T6/XModel/JsonXModelWriter.h"
-#include "ObjWriting.h"
-#include "Utils/DistinctMapper.h"
-#include "Utils/QuatInt16.h"
-#include "XModel/Export/XModelExportWriter.h"
-#include "XModel/Gltf/GltfBinOutput.h"
-#include "XModel/Gltf/GltfTextOutput.h"
-#include "XModel/Gltf/GltfWriter.h"
-#include "XModel/Obj/ObjWriter.h"
-#include "XModel/XModelWriter.h"
-
-#include <cassert>
-#include <format>
+#include "Game/T6/XModel/XModelDumperT6.h"
 
 using namespace T6;
 
-namespace
-{
-    std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
-    {
-        return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
-    }
-
-    GfxImage* GetMaterialColorMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
-                return def->image;
-        }
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
-                return def->image;
-        }
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
-                return def->image;
-        }
-
-        return potentialTextureDefs[0]->image;
-    }
-
-    GfxImage* GetMaterialNormalMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_NORMAL_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 'n' && def->nameEnd == 'p')
-                return def->image;
-        }
-
-        return potentialTextureDefs[0]->image;
-    }
-
-    GfxImage* GetMaterialSpecularMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_SPECULAR_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 's' && def->nameEnd == 'p')
-                return def->image;
-        }
-
-        return potentialTextureDefs[0]->image;
-    }
-
-    bool HasDefaultArmature(const XModel* model, const unsigned lod)
-    {
-        if (model->numRootBones != 1 || model->numBones != 1)
-            return false;
-
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
-            return true;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-
-            if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
-                return false;
-
-            const auto& vertList = surface.vertList[0];
-            if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
-                return false;
-        }
-
-        return true;
-    }
-
-    void OmitDefaultArmature(XModelCommon& common)
-    {
-        common.m_bones.clear();
-        common.m_bone_weight_data.weights.clear();
-        common.m_vertex_bone_weights.resize(common.m_vertices.size());
-        for (auto& vertexWeights : common.m_vertex_bone_weights)
-        {
-            vertexWeights.weightOffset = 0u;
-            vertexWeights.weightCount = 0u;
-        }
-    }
-
-    void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel* model)
-    {
-        for (auto boneNum = 0u; boneNum < model->numBones; boneNum++)
-        {
-            XModelBone bone;
-            if (model->boneNames[boneNum] < context.m_zone->m_script_strings.Count())
-                bone.name = context.m_zone->m_script_strings[model->boneNames[boneNum]];
-            else
-                bone.name = "INVALID_BONE_NAME";
-
-            if (boneNum >= model->numRootBones)
-                bone.parentIndex = static_cast<int>(boneNum - static_cast<unsigned int>(model->parentList[boneNum - model->numRootBones]));
-            else
-                bone.parentIndex = std::nullopt;
-
-            bone.scale[0] = 1.0f;
-            bone.scale[1] = 1.0f;
-            bone.scale[2] = 1.0f;
-
-            const auto& baseMat = model->baseMat[boneNum];
-            bone.globalOffset[0] = baseMat.trans.x;
-            bone.globalOffset[1] = baseMat.trans.y;
-            bone.globalOffset[2] = baseMat.trans.z;
-            bone.globalRotation = {
-                baseMat.quat.x,
-                baseMat.quat.y,
-                baseMat.quat.z,
-                baseMat.quat.w,
-            };
-
-            if (boneNum < model->numRootBones)
-            {
-                bone.localOffset[0] = 0;
-                bone.localOffset[1] = 0;
-                bone.localOffset[2] = 0;
-                bone.localRotation = {0, 0, 0, 1};
-            }
-            else
-            {
-                const auto* trans = &model->trans[(boneNum - model->numRootBones) * 3];
-                bone.localOffset[0] = trans[0];
-                bone.localOffset[1] = trans[1];
-                bone.localOffset[2] = trans[2];
-
-                const auto& quat = model->quats[boneNum - model->numRootBones];
-                bone.localRotation = {
-                    QuatInt16::ToFloat(quat.v[0]),
-                    QuatInt16::ToFloat(quat.v[1]),
-                    QuatInt16::ToFloat(quat.v[2]),
-                    QuatInt16::ToFloat(quat.v[3]),
-                };
-            }
-
-            out.m_bones.emplace_back(std::move(bone));
-        }
-    }
-
-    const char* AssetName(const char* input)
-    {
-        if (input && input[0] == ',')
-            return &input[1];
-
-        return input;
-    }
-
-    void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel* model)
-    {
-        for (auto surfaceMaterialNum = 0; surfaceMaterialNum < model->numsurfs; surfaceMaterialNum++)
-        {
-            Material* material = model->materialHandles[surfaceMaterialNum];
-            if (materialMapper.Add(material))
-            {
-                XModelMaterial xMaterial;
-                xMaterial.ApplyDefaults();
-
-                xMaterial.name = AssetName(material->info.name);
-                const auto* colorMap = GetMaterialColorMap(material);
-                if (colorMap)
-                    xMaterial.colorMapName = AssetName(colorMap->name);
-
-                const auto* normalMap = GetMaterialNormalMap(material);
-                if (normalMap)
-                    xMaterial.normalMapName = AssetName(normalMap->name);
-
-                const auto* specularMap = GetMaterialSpecularMap(material);
-                if (specularMap)
-                    xMaterial.specularMapName = AssetName(specularMap->name);
-
-                out.m_materials.emplace_back(std::move(xMaterial));
-            }
-        }
-    }
-
-    void AddXModelObjects(XModelCommon& out, const XModel* model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
-    {
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-        const auto baseSurfaceIndex = model->lodInfo[lod].surfIndex;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            XModelObject object;
-            object.name = std::format("surf{}", surfIndex);
-            object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
-
-            out.m_objects.emplace_back(std::move(object));
-        }
-    }
-
-    void AddXModelVertices(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
-            return;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-
-            for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
-            {
-                const auto& v = surface.verts0[vertexIndex];
-
-                XModelVertex vertex{};
-                vertex.coordinates[0] = v.xyz.x;
-                vertex.coordinates[1] = v.xyz.y;
-                vertex.coordinates[2] = v.xyz.z;
-                Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
-                Common::Vec4UnpackGfxColor(v.color, vertex.color);
-                Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
-
-                out.m_vertices.emplace_back(vertex);
-            }
-        }
-    }
-
-    void AllocateXModelBoneWeights(const XModel* model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
-            return;
-
-        auto totalWeightCount = 0u;
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-
-            if (surface.vertList)
-            {
-                totalWeightCount += surface.vertListCount;
-            }
-
-            if (surface.vertInfo.vertsBlend)
-            {
-                totalWeightCount += surface.vertInfo.vertCount[0] * 1;
-                totalWeightCount += surface.vertInfo.vertCount[1] * 2;
-                totalWeightCount += surface.vertInfo.vertCount[2] * 3;
-                totalWeightCount += surface.vertInfo.vertCount[3] * 4;
-            }
-        }
-
-        weightCollection.weights.resize(totalWeightCount);
-    }
-
-    float BoneWeight16(const uint16_t value)
-    {
-        return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
-    }
-
-    void AddXModelVertexBoneWeights(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-        auto& weightCollection = out.m_bone_weight_data;
-
-        if (!surfs)
-            return;
-
-        size_t weightOffset = 0u;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-            auto handledVertices = 0u;
-
-            if (surface.vertList)
-            {
-                for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
-                {
-                    const auto& vertList = surface.vertList[vertListIndex];
-                    const auto boneWeightOffset = weightOffset;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{vertList.boneOffset / sizeof(DObjSkelMat), 1.0f};
-
-                    for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
-                    {
-                        out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                    }
-                    handledVertices += vertList.vertCount;
-                }
-            }
-
-            auto vertsBlendOffset = 0u;
-            if (surface.vertInfo.vertsBlend)
-            {
-                // 1 bone weight
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, 1.0f};
-
-                    vertsBlendOffset += 1;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                }
-
-                // 2 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneWeight0 = 1.0f - boneWeight1;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-
-                    vertsBlendOffset += 3;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
-                }
-
-                // 3 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-
-                    vertsBlendOffset += 5;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
-                }
-
-                // 4 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
-                    const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex3, boneWeight3};
-
-                    vertsBlendOffset += 7;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
-                }
-
-                handledVertices +=
-                    surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
-            }
-
-            for (; handledVertices < surface.vertCount; handledVertices++)
-            {
-                out.m_vertex_bone_weights.emplace_back(0, 0);
-            }
-        }
-    }
-
-    void AddXModelFaces(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
-            return;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-            auto& object = out.m_objects[surfIndex];
-            object.m_faces.reserve(surface.triCount);
-
-            for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
-            {
-                const auto& tri = surface.triIndices[triIndex];
-
-                XModelFace face{};
-                face.vertexIndex[0] = tri.i[0] + surface.baseVertIndex;
-                face.vertexIndex[1] = tri.i[1] + surface.baseVertIndex;
-                face.vertexIndex[2] = tri.i[2] + surface.baseVertIndex;
-                object.m_faces.emplace_back(face);
-            }
-        }
-    }
-
-    void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel* model)
-    {
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-        AllocateXModelBoneWeights(model, lod, out.m_bone_weight_data);
-
-        out.m_name = std::format("{}_lod{}", model->name, lod);
-        AddXModelMaterials(out, materialMapper, model);
-        AddXModelObjects(out, model, lod, materialMapper);
-        AddXModelVertices(out, model, lod);
-        AddXModelFaces(out, model, lod);
-
-        if (!HasDefaultArmature(model, lod))
-        {
-            AddXModelBones(out, context, model);
-            AddXModelVertexBoneWeights(out, model, lod);
-        }
-        else
-        {
-            OmitDefaultArmature(out);
-        }
-    }
-
-    void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-        const auto mtlFile = context.OpenAssetFile(std::format("model_export/{}.mtl", model->name));
-
-        if (!mtlFile)
-            return;
-
-        const auto writer = obj::CreateMtlWriter(*mtlFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpObjLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".obj"));
-
-        if (!assetFile)
-            return;
-
-        const auto writer =
-            obj::CreateObjWriter(*assetFile, std::format("{}.mtl", model->name), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelExportLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".XMODEL_EXPORT"));
-
-        if (!assetFile)
-            return;
-
-        const auto writer = xmodel_export::CreateWriterForVersion6(*assetFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        writer->Write(common);
-    }
-
-    template<typename T>
-    void DumpGltfLod(
-        const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod, const std::string& extension)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, extension));
-
-        if (!assetFile)
-            return;
-
-        const auto output = std::make_unique<T>(*assetFile);
-        const auto writer = gltf::Writer::CreateWriter(output.get(), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-
-        for (auto currentLod = 0u; currentLod < model->numLods; currentLod++)
-        {
-            XModelCommon common;
-            PopulateXModelWriter(common, context, currentLod, asset->Asset());
-
-            switch (ObjWriting::Configuration.ModelOutputFormat)
-            {
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
-                DumpObjLod(common, context, asset, currentLod);
-                if (currentLod == 0u)
-                    DumpObjMtl(common, context, asset);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
-                DumpXModelExportLod(common, context, asset, currentLod);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
-                DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
-                DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
-                break;
-
-            default:
-                assert(false);
-                break;
-            }
-        }
-    }
-
-    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
-    {
-        const auto assetFile = context.OpenAssetFile(std::format("xmodel/{}.json", asset->m_name));
-        if (!assetFile)
-            return;
-
-        DumpXModelAsJson(*assetFile, asset->Asset(), context);
-    }
-} // namespace
-
 bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 {
     return !asset->m_name.empty() && asset->m_name[0] != ',';
@@ -602,6 +11,5 @@ bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 
 void AssetDumperXModel::DumpAsset(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
 {
-    DumpXModelSurfs(context, asset);
     DumpXModel(context, asset);
 }
diff --git a/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.cpp b/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.cpp
deleted file mode 100644
index 05104da2..00000000
--- a/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.cpp
+++ /dev/null
@@ -1,102 +0,0 @@
-#include "JsonXModelWriter.h"
-
-#include "Game/T6/CommonT6.h"
-#include "Game/T6/Json/JsonXModel.h"
-#include "ObjWriting.h"
-
-#include <cassert>
-#include <format>
-#include <iomanip>
-#include <nlohmann/json.hpp>
-
-using namespace nlohmann;
-using namespace T6;
-
-namespace
-{
-    class JsonDumper
-    {
-    public:
-        JsonDumper(AssetDumpingContext& context, std::ostream& stream)
-            : m_stream(stream)
-        {
-        }
-
-        void Dump(const XModel* xmodel) const
-        {
-            JsonXModel jsonXModel;
-            CreateJsonXModel(jsonXModel, *xmodel);
-            json jRoot = jsonXModel;
-
-            jRoot["_type"] = "xmodel";
-            jRoot["_version"] = 1;
-
-            m_stream << std::setw(4) << jRoot << "\n";
-        }
-
-    private:
-        static const char* AssetName(const char* input)
-        {
-            if (input && input[0] == ',')
-                return &input[1];
-
-            return input;
-        }
-
-        static const char* GetExtensionForModelByConfig()
-        {
-            switch (ObjWriting::Configuration.ModelOutputFormat)
-            {
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
-                return ".XMODEL_EXPORT";
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
-                return ".OBJ";
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
-                return ".GLTF";
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
-                return ".GLB";
-            default:
-                assert(false);
-                return "";
-            }
-        }
-
-        static void CreateJsonXModel(JsonXModel& jXModel, const XModel& xmodel)
-        {
-            if (xmodel.collLod >= 0)
-                jXModel.collLod = xmodel.collLod;
-
-            for (auto lodNumber = 0u; lodNumber < xmodel.numLods; lodNumber++)
-            {
-                JsonXModelLod lod;
-                lod.file = std::format("model_export/{}_lod{}{}", xmodel.name, lodNumber, GetExtensionForModelByConfig());
-                lod.distance = xmodel.lodInfo[lodNumber].dist;
-
-                jXModel.lods.emplace_back(std::move(lod));
-            }
-
-            if (xmodel.physPreset && xmodel.physPreset->name)
-                jXModel.physPreset = AssetName(xmodel.physPreset->name);
-
-            if (xmodel.physConstraints && xmodel.physConstraints->name)
-                jXModel.physConstraints = AssetName(xmodel.physConstraints->name);
-
-            jXModel.flags = xmodel.flags;
-            jXModel.lightingOriginOffset.x = xmodel.lightingOriginOffset.x;
-            jXModel.lightingOriginOffset.y = xmodel.lightingOriginOffset.y;
-            jXModel.lightingOriginOffset.z = xmodel.lightingOriginOffset.z;
-            jXModel.lightingOriginRange = xmodel.lightingOriginRange;
-        }
-
-        std::ostream& m_stream;
-    };
-} // namespace
-
-namespace T6
-{
-    void DumpXModelAsJson(std::ostream& stream, const XModel* xmodel, AssetDumpingContext& context)
-    {
-        const JsonDumper dumper(context, stream);
-        dumper.Dump(xmodel);
-    }
-} // namespace T6
diff --git a/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.h b/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.h
deleted file mode 100644
index f40f008d..00000000
--- a/src/ObjWriting/Game/T6/XModel/JsonXModelWriter.h
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma once
-
-#include "Dumping/AssetDumpingContext.h"
-#include "Game/T6/T6.h"
-
-#include <ostream>
-
-namespace T6
-{
-    void DumpXModelAsJson(std::ostream& stream, const XModel* xmodel, AssetDumpingContext& context);
-} // namespace T6
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
new file mode 100644
index 00000000..7ccdd75d
--- /dev/null
+++ b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
@@ -0,0 +1,17 @@
+#include "XModelDumperT6.h"
+
+#include "Game/T6/CommonT6.h"
+#include "Game/T6/Json/JsonXModel.h"
+
+#define GAME_NAMESPACE T6
+
+#include "XModel/GenericXModelDumper.inc.h"
+
+namespace T6
+{
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
+    {
+        DumpXModelJson(context, asset);
+        DumpXModelSurfs(context, asset);
+    }
+} // namespace T6
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
new file mode 100644
index 00000000..b63027f1
--- /dev/null
+++ b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "Dumping/AssetDumpingContext.h"
+#include "Game/T6/T6.h"
+
+namespace T6
+{
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset);
+}
diff --git a/src/ObjWriting/XModel/GenericXModelDumper.inc.h b/src/ObjWriting/XModel/GenericXModelDumper.inc.h
new file mode 100644
index 00000000..6a128bbe
--- /dev/null
+++ b/src/ObjWriting/XModel/GenericXModelDumper.inc.h
@@ -0,0 +1,675 @@
+#pragma once
+
+#ifndef GAME_NAMESPACE
+#error Must define GAME_NAMESPACE
+#endif
+
+#include "Game/T6/CommonT6.h"
+#include "ObjWriting.h"
+#include "Utils/DistinctMapper.h"
+#include "Utils/QuatInt16.h"
+#include "XModel/Export/XModelExportWriter.h"
+#include "XModel/Gltf/GltfBinOutput.h"
+#include "XModel/Gltf/GltfTextOutput.h"
+#include "XModel/Gltf/GltfWriter.h"
+#include "XModel/Obj/ObjWriter.h"
+#include "XModel/XModelWriter.h"
+
+#include <cassert>
+#include <format>
+
+namespace GAME_NAMESPACE
+{
+    inline std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
+    {
+        return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
+    }
+
+    inline GfxImage* GetMaterialColorMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return potentialTextureDefs[0]->image;
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
+                return def->image;
+        }
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
+                return def->image;
+        }
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
+                return def->image;
+        }
+
+        return potentialTextureDefs[0]->image;
+    }
+
+    inline GfxImage* GetMaterialNormalMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_NORMAL_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return potentialTextureDefs[0]->image;
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (def->nameStart == 'n' && def->nameEnd == 'p')
+                return def->image;
+        }
+
+        return potentialTextureDefs[0]->image;
+    }
+
+    inline GfxImage* GetMaterialSpecularMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_SPECULAR_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return potentialTextureDefs[0]->image;
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (def->nameStart == 's' && def->nameEnd == 'p')
+                return def->image;
+        }
+
+        return potentialTextureDefs[0]->image;
+    }
+
+    inline bool HasDefaultArmature(const XModel* model, const unsigned lod)
+    {
+        if (model->numRootBones != 1 || model->numBones != 1)
+            return false;
+
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return true;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
+                return false;
+
+            const auto& vertList = surface.vertList[0];
+            if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
+                return false;
+        }
+
+        return true;
+    }
+
+    inline void OmitDefaultArmature(XModelCommon& common)
+    {
+        common.m_bones.clear();
+        common.m_bone_weight_data.weights.clear();
+        common.m_vertex_bone_weights.resize(common.m_vertices.size());
+        for (auto& vertexWeights : common.m_vertex_bone_weights)
+        {
+            vertexWeights.weightOffset = 0u;
+            vertexWeights.weightCount = 0u;
+        }
+    }
+
+    inline void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel* model)
+    {
+        for (auto boneNum = 0u; boneNum < model->numBones; boneNum++)
+        {
+            XModelBone bone;
+            if (model->boneNames[boneNum] < context.m_zone->m_script_strings.Count())
+                bone.name = context.m_zone->m_script_strings[model->boneNames[boneNum]];
+            else
+                bone.name = "INVALID_BONE_NAME";
+
+            if (boneNum >= model->numRootBones)
+                bone.parentIndex = static_cast<int>(boneNum - static_cast<unsigned int>(model->parentList[boneNum - model->numRootBones]));
+            else
+                bone.parentIndex = std::nullopt;
+
+            bone.scale[0] = 1.0f;
+            bone.scale[1] = 1.0f;
+            bone.scale[2] = 1.0f;
+
+            const auto& baseMat = model->baseMat[boneNum];
+            bone.globalOffset[0] = baseMat.trans.x;
+            bone.globalOffset[1] = baseMat.trans.y;
+            bone.globalOffset[2] = baseMat.trans.z;
+            bone.globalRotation = {
+                baseMat.quat.x,
+                baseMat.quat.y,
+                baseMat.quat.z,
+                baseMat.quat.w,
+            };
+
+            if (boneNum < model->numRootBones)
+            {
+                bone.localOffset[0] = 0;
+                bone.localOffset[1] = 0;
+                bone.localOffset[2] = 0;
+                bone.localRotation = {0, 0, 0, 1};
+            }
+            else
+            {
+                const auto* trans = &model->trans[(boneNum - model->numRootBones) * 3];
+                bone.localOffset[0] = trans[0];
+                bone.localOffset[1] = trans[1];
+                bone.localOffset[2] = trans[2];
+
+                const auto& quat = model->quats[boneNum - model->numRootBones];
+                bone.localRotation = {
+                    QuatInt16::ToFloat(quat.v[0]),
+                    QuatInt16::ToFloat(quat.v[1]),
+                    QuatInt16::ToFloat(quat.v[2]),
+                    QuatInt16::ToFloat(quat.v[3]),
+                };
+            }
+
+            out.m_bones.emplace_back(std::move(bone));
+        }
+    }
+
+    inline const char* AssetName(const char* input)
+    {
+        if (input && input[0] == ',')
+            return &input[1];
+
+        return input;
+    }
+
+    inline void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel* model)
+    {
+        for (auto surfaceMaterialNum = 0; surfaceMaterialNum < model->numsurfs; surfaceMaterialNum++)
+        {
+            Material* material = model->materialHandles[surfaceMaterialNum];
+            if (materialMapper.Add(material))
+            {
+                XModelMaterial xMaterial;
+                xMaterial.ApplyDefaults();
+
+                xMaterial.name = AssetName(material->info.name);
+                const auto* colorMap = GetMaterialColorMap(material);
+                if (colorMap)
+                    xMaterial.colorMapName = AssetName(colorMap->name);
+
+                const auto* normalMap = GetMaterialNormalMap(material);
+                if (normalMap)
+                    xMaterial.normalMapName = AssetName(normalMap->name);
+
+                const auto* specularMap = GetMaterialSpecularMap(material);
+                if (specularMap)
+                    xMaterial.specularMapName = AssetName(specularMap->name);
+
+                out.m_materials.emplace_back(std::move(xMaterial));
+            }
+        }
+    }
+
+    inline void AddXModelObjects(XModelCommon& out, const XModel* model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
+    {
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+        const auto baseSurfaceIndex = model->lodInfo[lod].surfIndex;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            XModelObject object;
+            object.name = std::format("surf{}", surfIndex);
+            object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
+
+            out.m_objects.emplace_back(std::move(object));
+        }
+    }
+
+    inline void AddXModelVertices(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
+            {
+                const auto& v = surface.verts0[vertexIndex];
+
+                XModelVertex vertex{};
+                vertex.coordinates[0] = v.xyz.x;
+                vertex.coordinates[1] = v.xyz.y;
+                vertex.coordinates[2] = v.xyz.z;
+                Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
+                Common::Vec4UnpackGfxColor(v.color, vertex.color);
+                Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
+
+                out.m_vertices.emplace_back(vertex);
+            }
+        }
+    }
+
+    inline void AllocateXModelBoneWeights(const XModel* model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        auto totalWeightCount = 0u;
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            if (surface.vertList)
+            {
+                totalWeightCount += surface.vertListCount;
+            }
+
+            if (surface.vertInfo.vertsBlend)
+            {
+                totalWeightCount += surface.vertInfo.vertCount[0] * 1;
+                totalWeightCount += surface.vertInfo.vertCount[1] * 2;
+                totalWeightCount += surface.vertInfo.vertCount[2] * 3;
+                totalWeightCount += surface.vertInfo.vertCount[3] * 4;
+            }
+        }
+
+        weightCollection.weights.resize(totalWeightCount);
+    }
+
+    inline float BoneWeight16(const uint16_t value)
+    {
+        return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
+    }
+
+    inline void AddXModelVertexBoneWeights(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+        auto& weightCollection = out.m_bone_weight_data;
+
+        if (!surfs)
+            return;
+
+        size_t weightOffset = 0u;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+            auto handledVertices = 0u;
+
+            if (surface.vertList)
+            {
+                for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
+                {
+                    const auto& vertList = surface.vertList[vertListIndex];
+                    const auto boneWeightOffset = weightOffset;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{vertList.boneOffset / sizeof(DObjSkelMat), 1.0f};
+
+                    for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
+                    {
+                        out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
+                    }
+                    handledVertices += vertList.vertCount;
+                }
+            }
+
+            auto vertsBlendOffset = 0u;
+            if (surface.vertInfo.vertsBlend)
+            {
+                // 1 bone weight
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, 1.0f};
+
+                    vertsBlendOffset += 1;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
+                }
+
+                // 2 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneWeight0 = 1.0f - boneWeight1;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+
+                    vertsBlendOffset += 3;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
+                }
+
+                // 3 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
+                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
+                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
+
+                    vertsBlendOffset += 5;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
+                }
+
+                // 4 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
+                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
+                    const auto boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
+                    const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
+                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex3, boneWeight3};
+
+                    vertsBlendOffset += 7;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
+                }
+
+                handledVertices +=
+                    surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
+            }
+
+            for (; handledVertices < surface.vertCount; handledVertices++)
+            {
+                out.m_vertex_bone_weights.emplace_back(0, 0);
+            }
+        }
+    }
+
+    inline void AddXModelFaces(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+            auto& object = out.m_objects[surfIndex];
+            object.m_faces.reserve(surface.triCount);
+
+            for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
+            {
+                const auto& tri = surface.triIndices[triIndex];
+
+                XModelFace face{};
+                face.vertexIndex[0] = tri.i[0] + surface.baseVertIndex;
+                face.vertexIndex[1] = tri.i[1] + surface.baseVertIndex;
+                face.vertexIndex[2] = tri.i[2] + surface.baseVertIndex;
+                object.m_faces.emplace_back(face);
+            }
+        }
+    }
+
+    inline void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel* model)
+    {
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+        AllocateXModelBoneWeights(model, lod, out.m_bone_weight_data);
+
+        out.m_name = std::format("{}_lod{}", model->name, lod);
+        AddXModelMaterials(out, materialMapper, model);
+        AddXModelObjects(out, model, lod, materialMapper);
+        AddXModelVertices(out, model, lod);
+        AddXModelFaces(out, model, lod);
+
+        if (!HasDefaultArmature(model, lod))
+        {
+            AddXModelBones(out, context, model);
+            AddXModelVertexBoneWeights(out, model, lod);
+        }
+        else
+        {
+            OmitDefaultArmature(out);
+        }
+    }
+
+    inline void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
+    {
+        const auto* model = asset->Asset();
+        const auto mtlFile = context.OpenAssetFile(std::format("model_export/{}.mtl", model->name));
+
+        if (!mtlFile)
+            return;
+
+        const auto writer = obj::CreateMtlWriter(*mtlFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+
+        writer->Write(common);
+    }
+
+    inline void DumpObjLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".obj"));
+
+        if (!assetFile)
+            return;
+
+        const auto writer =
+            obj::CreateObjWriter(*assetFile, std::format("{}.mtl", model->name), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+
+        writer->Write(common);
+    }
+
+    inline void DumpXModelExportLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".XMODEL_EXPORT"));
+
+        if (!assetFile)
+            return;
+
+        const auto writer = xmodel_export::CreateWriterForVersion6(*assetFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        writer->Write(common);
+    }
+
+    template<typename T>
+    void DumpGltfLod(
+        const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod, const std::string& extension)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, extension));
+
+        if (!assetFile)
+            return;
+
+        const auto output = std::make_unique<T>(*assetFile);
+        const auto writer = gltf::Writer::CreateWriter(output.get(), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+
+        writer->Write(common);
+    }
+
+    inline void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
+    {
+        const auto* model = asset->Asset();
+
+        for (auto currentLod = 0u; currentLod < model->numLods; currentLod++)
+        {
+            XModelCommon common;
+            PopulateXModelWriter(common, context, currentLod, asset->Asset());
+
+            switch (ObjWriting::Configuration.ModelOutputFormat)
+            {
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
+                DumpObjLod(common, context, asset, currentLod);
+                if (currentLod == 0u)
+                    DumpObjMtl(common, context, asset);
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
+                DumpXModelExportLod(common, context, asset, currentLod);
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
+                DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
+                DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
+                break;
+
+            default:
+                assert(false);
+                break;
+            }
+        }
+    }
+
+    class JsonDumper
+    {
+    public:
+        JsonDumper(AssetDumpingContext& context, std::ostream& stream)
+            : m_stream(stream)
+        {
+        }
+
+        void Dump(const XModel* xmodel) const
+        {
+            JsonXModel jsonXModel;
+            CreateJsonXModel(jsonXModel, *xmodel);
+            nlohmann::json jRoot = jsonXModel;
+
+            jRoot["_type"] = "xmodel";
+            jRoot["_version"] = 1;
+
+            m_stream << std::setw(4) << jRoot << "\n";
+        }
+
+    private:
+        static const char* AssetName(const char* input)
+        {
+            if (input && input[0] == ',')
+                return &input[1];
+
+            return input;
+        }
+
+        static const char* GetExtensionForModelByConfig()
+        {
+            switch (ObjWriting::Configuration.ModelOutputFormat)
+            {
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
+                return ".XMODEL_EXPORT";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
+                return ".OBJ";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
+                return ".GLTF";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
+                return ".GLB";
+            default:
+                assert(false);
+                return "";
+            }
+        }
+
+        static void CreateJsonXModel(JsonXModel& jXModel, const XModel& xmodel)
+        {
+            if (xmodel.collLod >= 0)
+                jXModel.collLod = xmodel.collLod;
+
+            for (auto lodNumber = 0u; lodNumber < xmodel.numLods; lodNumber++)
+            {
+                JsonXModelLod lod;
+                lod.file = std::format("model_export/{}_lod{}{}", xmodel.name, lodNumber, GetExtensionForModelByConfig());
+                lod.distance = xmodel.lodInfo[lodNumber].dist;
+
+                jXModel.lods.emplace_back(std::move(lod));
+            }
+
+            if (xmodel.physPreset && xmodel.physPreset->name)
+                jXModel.physPreset = AssetName(xmodel.physPreset->name);
+
+            if (xmodel.physConstraints && xmodel.physConstraints->name)
+                jXModel.physConstraints = AssetName(xmodel.physConstraints->name);
+
+            jXModel.flags = xmodel.flags;
+            jXModel.lightingOriginOffset.x = xmodel.lightingOriginOffset.x;
+            jXModel.lightingOriginOffset.y = xmodel.lightingOriginOffset.y;
+            jXModel.lightingOriginOffset.z = xmodel.lightingOriginOffset.z;
+            jXModel.lightingOriginRange = xmodel.lightingOriginRange;
+        }
+
+        std::ostream& m_stream;
+    };
+
+    inline void DumpXModelJson(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
+    {
+        const auto assetFile = context.OpenAssetFile(std::format("xmodel/{}.json", asset->m_name));
+        if (!assetFile)
+            return;
+
+        const JsonDumper dumper(context, *assetFile);
+        dumper.Dump(asset->Asset());
+    }
+} // namespace GAME_NAMESPACE

From 28ecee3a1d530ed0d373c13282e78bf9331e226c Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Fri, 13 Sep 2024 21:50:28 +0200
Subject: [PATCH 04/17] chore: move t6 xmodel json to proper folder

---
 src/ObjCommon/Game/T6/{Json => XModel}/JsonXModel.h | 4 ----
 src/ObjCommon/Json/JsonCommon.h                     | 3 +++
 src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp    | 2 +-
 3 files changed, 4 insertions(+), 5 deletions(-)
 rename src/ObjCommon/Game/T6/{Json => XModel}/JsonXModel.h (90%)

diff --git a/src/ObjCommon/Game/T6/Json/JsonXModel.h b/src/ObjCommon/Game/T6/XModel/JsonXModel.h
similarity index 90%
rename from src/ObjCommon/Game/T6/Json/JsonXModel.h
rename to src/ObjCommon/Game/T6/XModel/JsonXModel.h
index 26e0c2f5..79e6eb06 100644
--- a/src/ObjCommon/Game/T6/Json/JsonXModel.h
+++ b/src/ObjCommon/Game/T6/XModel/JsonXModel.h
@@ -1,11 +1,7 @@
 #pragma once
 
-#include "Game/T6/T6.h"
-
 #include "Json/JsonCommon.h"
-#include "Json/JsonExtension.h"
 #include <memory>
-#include <nlohmann/json.hpp>
 #include <optional>
 #include <string>
 #include <vector>
diff --git a/src/ObjCommon/Json/JsonCommon.h b/src/ObjCommon/Json/JsonCommon.h
index f1a9291e..6ac71235 100644
--- a/src/ObjCommon/Json/JsonCommon.h
+++ b/src/ObjCommon/Json/JsonCommon.h
@@ -1,7 +1,10 @@
 #pragma once
 
 #include "Json/JsonExtension.h"
+
+#pragma warning(push, 0)
 #include <nlohmann/json.hpp>
+#pragma warning(pop)
 
 class JsonVec2
 {
diff --git a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
index 32e830a9..3beb26cb 100644
--- a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
+++ b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
@@ -1,7 +1,7 @@
 #include "XModelLoaderT6.h"
 
 #include "Game/T6/CommonT6.h"
-#include "Game/T6/Json/JsonXModel.h"
+#include "Game/T6/XModel/JsonXModel.h"
 
 #define GAME_NAMESPACE T6
 

From a2735b4f23ff811bdc1fc5857e720f2272b9a870 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Fri, 13 Sep 2024 21:51:12 +0200
Subject: [PATCH 05/17] chore: use generic xmodel loader and dumper code for t5

---
 src/Common/Game/T5/T5_Assets.h                |  47 +-
 src/ObjCommon/Game/T5/XModel/JsonXModel.h     |  31 ++
 .../T5/AssetLoaders/AssetLoaderXModel.cpp     |  42 ++
 .../Game/T5/AssetLoaders/AssetLoaderXModel.h  |  19 +
 src/ObjLoading/Game/T5/ObjLoaderT5.cpp        |   3 +-
 .../Game/T5/XModel/XModelLoaderT5.cpp         |  49 ++
 .../Game/T5/XModel/XModelLoaderT5.h           |  13 +
 .../XModel/GenericXModelLoader.inc.h          |   3 +
 .../T5/AssetDumpers/AssetDumperXModel.cpp     | 514 +-----------------
 .../Game/T5/XModel/XModelDumperT5.cpp         |  17 +
 .../Game/T5/XModel/XModelDumperT5.h           |   9 +
 .../Game/T6/XModel/XModelDumperT6.cpp         |   3 +-
 .../XModel/GenericXModelDumper.inc.h          |  34 +-
 13 files changed, 243 insertions(+), 541 deletions(-)
 create mode 100644 src/ObjCommon/Game/T5/XModel/JsonXModel.h
 create mode 100644 src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
 create mode 100644 src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.h
 create mode 100644 src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
 create mode 100644 src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h
 create mode 100644 src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
 create mode 100644 src/ObjWriting/Game/T5/XModel/XModelDumperT5.h

diff --git a/src/Common/Game/T5/T5_Assets.h b/src/Common/Game/T5/T5_Assets.h
index 4c6de811..f055f58c 100644
--- a/src/Common/Game/T5/T5_Assets.h
+++ b/src/Common/Game/T5/T5_Assets.h
@@ -208,6 +208,8 @@ namespace T5
     typedef char cbrushedge_t;
     typedef tdef_align(128) unsigned int raw_uint128;
 
+    typedef uint16_t ScriptString;
+
     struct PhysPreset
     {
         const char* name;
@@ -459,8 +461,8 @@ namespace T5
 
     struct DObjAnimMat
     {
-        float quat[4];
-        float trans[3];
+        vec4_t quat;
+        vec3_t trans;
         float transWeight;
     };
 
@@ -490,7 +492,7 @@ namespace T5
 
     struct type_align(16) GfxPackedVertex
     {
-        float xyz[3];
+        vec3_t xyz;
         float binormalSign;
         GfxColor color;
         PackedTexCoords texCoord;
@@ -535,7 +537,12 @@ namespace T5
         XSurfaceCollisionTree* collisionTree;
     };
 
-    typedef tdef_align(16) uint16_t r_index16_t;
+    struct XSurfaceTri
+    {
+        uint16_t i[3];
+    };
+
+    typedef tdef_align(16) XSurfaceTri XSurfaceTri16;
 
     struct XSurface
     {
@@ -546,7 +553,7 @@ namespace T5
         uint16_t triCount;
         uint16_t baseTriIndex;
         uint16_t baseVertIndex;
-        r_index16_t (*triIndices)[3];
+        XSurfaceTri16* triIndices;
         XSurfaceVertexInfo vertInfo;
         GfxPackedVertex* verts0;
         void /*IDirect3DVertexBuffer9*/* vb0;
@@ -587,8 +594,8 @@ namespace T5
 
     struct XBoneInfo
     {
-        float bounds[2][3];
-        float offset[3];
+        vec3_t bounds[2];
+        vec3_t offset;
         float radiusSquared;
         char collmap;
     };
@@ -657,6 +664,14 @@ namespace T5
         PhysGeomList* geomList;
     };
 
+    enum XModelLodRampType : unsigned char
+    {
+        XMODEL_LOD_RAMP_RIGID = 0x0,
+        XMODEL_LOD_RAMP_SKINNED = 0x1,
+
+        XMODEL_LOD_RAMP_COUNT
+    };
+
     struct XModelQuat
     {
         int16_t v[4];
@@ -668,12 +683,12 @@ namespace T5
         unsigned char numBones;
         unsigned char numRootBones;
         unsigned char numsurfs;
-        char lodRampType;
-        uint16_t* boneNames;
-        char* parentList;
+        XModelLodRampType lodRampType;
+        ScriptString* boneNames;
+        unsigned char* parentList;
         XModelQuat* quats;
         float* trans;
-        char* partClassification;
+        unsigned char* partClassification;
         DObjAnimMat* baseMat;
         XSurface* surfs;
         Material** materialHandles;
@@ -684,13 +699,13 @@ namespace T5
         int contents;
         XBoneInfo* boneInfo;
         float radius;
-        float mins[3];
-        float maxs[3];
+        vec3_t mins;
+        vec3_t maxs;
         uint16_t numLods;
-        uint16_t collLod;
+        int16_t collLod;
         XModelStreamInfo streamInfo;
         int memUsage;
-        int flags;
+        unsigned int flags;
         bool bad;
         PhysPreset* physPreset;
         unsigned char numCollmaps;
@@ -775,7 +790,7 @@ namespace T5
         char nameStart;
         char nameEnd;
         char samplerState;
-        char semantic;
+        unsigned char semantic; // TextureSemantic
         char isMatureContent;
         char pad[3];
         MaterialTextureDefInfo u;
diff --git a/src/ObjCommon/Game/T5/XModel/JsonXModel.h b/src/ObjCommon/Game/T5/XModel/JsonXModel.h
new file mode 100644
index 00000000..5b52ca9a
--- /dev/null
+++ b/src/ObjCommon/Game/T5/XModel/JsonXModel.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "Json/JsonCommon.h"
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace T5
+{
+    class JsonXModelLod
+    {
+    public:
+        std::string file;
+        float distance;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonXModelLod, file, distance);
+
+    class JsonXModel
+    {
+    public:
+        std::vector<JsonXModelLod> lods;
+        std::optional<int> collLod;
+        std::optional<std::string> physPreset;
+        std::optional<std::string> physConstraints;
+        unsigned flags;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonXModel, lods, collLod, physPreset, physConstraints, flags);
+} // namespace T5
diff --git a/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
new file mode 100644
index 00000000..d419ce67
--- /dev/null
+++ b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
@@ -0,0 +1,42 @@
+#include "AssetLoaderXModel.h"
+
+#include "Game/T5/T5.h"
+#include "Game/T5/XModel/XModelLoaderT5.h"
+#include "Pool/GlobalAssetPool.h"
+
+#include <cstring>
+#include <format>
+#include <iostream>
+
+using namespace T5;
+
+void* AssetLoaderXModel::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
+{
+    auto* asset = memory->Alloc<AssetXModel::Type>();
+    asset->name = memory->Dup(assetName.c_str());
+    return asset;
+}
+
+bool AssetLoaderXModel::CanLoadFromRaw() const
+{
+    return true;
+}
+
+bool AssetLoaderXModel::LoadFromRaw(
+    const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const
+{
+    const auto file = searchPath->Open(std::format("xmodel/{}.json", assetName));
+    if (!file.IsOpen())
+        return false;
+
+    auto* xmodel = memory->Alloc<XModel>();
+    xmodel->name = memory->Dup(assetName.c_str());
+
+    std::vector<XAssetInfoGeneric*> dependencies;
+    if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
+        manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
+    else
+        std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
+
+    return true;
+}
diff --git a/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.h b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.h
new file mode 100644
index 00000000..4a669630
--- /dev/null
+++ b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "AssetLoading/BasicAssetLoader.h"
+#include "AssetLoading/IAssetLoadingManager.h"
+#include "Game/T5/T5.h"
+#include "SearchPath/ISearchPath.h"
+
+namespace T5
+{
+    class AssetLoaderXModel final : public BasicAssetLoader<AssetXModel>
+    {
+        static std::string GetFileNameForAsset(const std::string& assetName);
+
+    public:
+        _NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override;
+        _NODISCARD bool CanLoadFromRaw() const override;
+        bool
+            LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override;
+    };
+} // namespace T5
diff --git a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp
index 16b38fb2..0b98c959 100644
--- a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp
+++ b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp
@@ -3,6 +3,7 @@
 #include "AssetLoaders/AssetLoaderLocalizeEntry.h"
 #include "AssetLoaders/AssetLoaderRawFile.h"
 #include "AssetLoaders/AssetLoaderStringTable.h"
+#include "AssetLoaders/AssetLoaderXModel.h"
 #include "AssetLoading/AssetLoadingManager.h"
 #include "Game/T5/GameAssetPoolT5.h"
 #include "Game/T5/GameT5.h"
@@ -27,7 +28,7 @@ ObjLoader::ObjLoader()
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetPhysConstraints>)
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetDestructibleDef>)
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetXAnim>)
-    REGISTER_ASSET_LOADER(BasicAssetLoader<AssetXModel>)
+    REGISTER_ASSET_LOADER(AssetLoaderXModel)
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetMaterial>)
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetTechniqueSet>)
     REGISTER_ASSET_LOADER(BasicAssetLoader<AssetImage>)
diff --git a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp b/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
new file mode 100644
index 00000000..55f19595
--- /dev/null
+++ b/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
@@ -0,0 +1,49 @@
+#include "XModelLoaderT5.h"
+
+#include "Game/T5/CommonT5.h"
+#include "Game/T5/XModel/JsonXModel.h"
+
+#define GAME_NAMESPACE T5
+
+namespace T5
+{
+    const char* HITLOC_NAMES[]{
+        // clang-format off
+        "none",
+        "helmet",
+        "head",
+        "neck",
+        "torso_upper",
+        "torso_lower",
+        "right_arm_upper",
+        "left_arm_upper",
+        "right_arm_lower",
+        "left_arm_lower",
+        "right_hand",
+        "left_hand",
+        "right_leg_upper",
+        "left_leg_upper",
+        "right_leg_lower",
+        "left_leg_lower",
+        "right_foot",
+        "left_foot",
+        "gun",
+        // clang-format on
+    };
+    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
+} // namespace T5
+
+#include "XModel/GenericXModelLoader.inc.h"
+
+namespace T5
+{
+    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
+    {
+        std::set<XAssetInfoGeneric*> dependenciesSet;
+        XModelLoader loader(stream, *memory, *manager, dependenciesSet);
+
+        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
+
+        return loader.Load(xmodel);
+    }
+} // namespace T5
diff --git a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h b/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h
new file mode 100644
index 00000000..cf1c3951
--- /dev/null
+++ b/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "AssetLoading/IAssetLoadingManager.h"
+#include "Game/T5/T5.h"
+#include "Utils/MemoryManager.h"
+
+#include <istream>
+#include <vector>
+
+namespace T5
+{
+    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
+}
diff --git a/src/ObjLoading/XModel/GenericXModelLoader.inc.h b/src/ObjLoading/XModel/GenericXModelLoader.inc.h
index 74ac5fd0..2c9610c5 100644
--- a/src/ObjLoading/XModel/GenericXModelLoader.inc.h
+++ b/src/ObjLoading/XModel/GenericXModelLoader.inc.h
@@ -773,10 +773,13 @@ namespace GAME_NAMESPACE
             }
 
             xmodel.flags = jXModel.flags;
+
+#ifdef FEATURE_T6
             xmodel.lightingOriginOffset.x = jXModel.lightingOriginOffset.x;
             xmodel.lightingOriginOffset.y = jXModel.lightingOriginOffset.y;
             xmodel.lightingOriginOffset.z = jXModel.lightingOriginOffset.z;
             xmodel.lightingOriginRange = jXModel.lightingOriginRange;
+#endif
 
             return true;
         }
diff --git a/src/ObjWriting/Game/T5/AssetDumpers/AssetDumperXModel.cpp b/src/ObjWriting/Game/T5/AssetDumpers/AssetDumperXModel.cpp
index b7d80214..8eb958bb 100644
--- a/src/ObjWriting/Game/T5/AssetDumpers/AssetDumperXModel.cpp
+++ b/src/ObjWriting/Game/T5/AssetDumpers/AssetDumperXModel.cpp
@@ -1,519 +1,9 @@
 #include "AssetDumperXModel.h"
 
-#include "Game/T5/CommonT5.h"
-#include "ObjWriting.h"
-#include "Utils/DistinctMapper.h"
-#include "Utils/QuatInt16.h"
-#include "XModel/Export/XModelExportWriter.h"
-#include "XModel/Gltf/GltfBinOutput.h"
-#include "XModel/Gltf/GltfTextOutput.h"
-#include "XModel/Gltf/GltfWriter.h"
-#include "XModel/Obj/ObjWriter.h"
-#include "XModel/XModelWriter.h"
-
-#include <cassert>
-#include <format>
+#include "Game/T5/XModel/XModelDumperT5.h"
 
 using namespace T5;
 
-namespace
-{
-    std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
-    {
-        return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
-    }
-
-    GfxImage* GetMaterialColorMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 'c' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    GfxImage* GetMaterialNormalMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_NORMAL_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 'n' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    GfxImage* GetMaterialSpecularMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_SPECULAR_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 's' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel* model)
-    {
-        for (auto boneNum = 0u; boneNum < model->numBones; boneNum++)
-        {
-            XModelBone bone;
-            if (model->boneNames[boneNum] < context.m_zone->m_script_strings.Count())
-                bone.name = context.m_zone->m_script_strings[model->boneNames[boneNum]];
-            else
-                bone.name = "INVALID_BONE_NAME";
-
-            if (boneNum >= model->numRootBones)
-                bone.parentIndex = boneNum - static_cast<unsigned int>(model->parentList[boneNum - model->numRootBones]);
-            else
-                bone.parentIndex = std::nullopt;
-
-            bone.scale[0] = 1.0f;
-            bone.scale[1] = 1.0f;
-            bone.scale[2] = 1.0f;
-
-            bone.globalOffset[0] = model->baseMat[boneNum].trans[0];
-            bone.globalOffset[1] = model->baseMat[boneNum].trans[1];
-            bone.globalOffset[2] = model->baseMat[boneNum].trans[2];
-            bone.globalRotation = {
-                model->baseMat[boneNum].quat[0],
-                model->baseMat[boneNum].quat[1],
-                model->baseMat[boneNum].quat[2],
-                model->baseMat[boneNum].quat[3],
-            };
-
-            if (boneNum < model->numRootBones)
-            {
-                bone.localOffset[0] = 0;
-                bone.localOffset[1] = 0;
-                bone.localOffset[2] = 0;
-                bone.localRotation = {0, 0, 0, 1};
-            }
-            else
-            {
-                const auto* trans = &model->trans[(boneNum - model->numRootBones) * 3];
-                bone.localOffset[0] = trans[0];
-                bone.localOffset[1] = trans[1];
-                bone.localOffset[2] = trans[2];
-
-                const auto& quat = model->quats[boneNum - model->numRootBones];
-                bone.localRotation = {
-                    QuatInt16::ToFloat(quat.v[0]),
-                    QuatInt16::ToFloat(quat.v[1]),
-                    QuatInt16::ToFloat(quat.v[2]),
-                    QuatInt16::ToFloat(quat.v[3]),
-                };
-            }
-
-            out.m_bones.emplace_back(std::move(bone));
-        }
-    }
-
-    const char* AssetName(const char* input)
-    {
-        if (input && input[0] == ',')
-            return &input[1];
-
-        return input;
-    }
-
-    void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel* model)
-    {
-        for (auto surfaceMaterialNum = 0; surfaceMaterialNum < model->numsurfs; surfaceMaterialNum++)
-        {
-            Material* material = model->materialHandles[surfaceMaterialNum];
-            if (materialMapper.Add(material))
-            {
-                XModelMaterial xMaterial;
-                xMaterial.ApplyDefaults();
-
-                xMaterial.name = AssetName(material->info.name);
-                const auto* colorMap = GetMaterialColorMap(material);
-                if (colorMap)
-                    xMaterial.colorMapName = AssetName(colorMap->name);
-
-                const auto* normalMap = GetMaterialNormalMap(material);
-                if (normalMap)
-                    xMaterial.normalMapName = AssetName(normalMap->name);
-
-                const auto* specularMap = GetMaterialSpecularMap(material);
-                if (specularMap)
-                    xMaterial.specularMapName = AssetName(specularMap->name);
-
-                out.m_materials.emplace_back(std::move(xMaterial));
-            }
-        }
-    }
-
-    void AddXModelObjects(XModelCommon& out, const XModel* model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
-    {
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-        const auto baseSurfaceIndex = model->lodInfo[lod].surfIndex;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            XModelObject object;
-            object.name = std::format("surf{}", surfIndex);
-            object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
-
-            out.m_objects.emplace_back(std::move(object));
-        }
-    }
-
-    void AddXModelVertices(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-
-            for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
-            {
-                const auto& v = surface.verts0[vertexIndex];
-
-                XModelVertex vertex{};
-                vertex.coordinates[0] = v.xyz[0];
-                vertex.coordinates[1] = v.xyz[1];
-                vertex.coordinates[2] = v.xyz[2];
-                Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
-                Common::Vec4UnpackGfxColor(v.color, vertex.color);
-                Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
-
-                out.m_vertices.emplace_back(vertex);
-            }
-        }
-    }
-
-    void AllocateXModelBoneWeights(const XModel* model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        auto totalWeightCount = 0u;
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-
-            if (surface.vertList)
-            {
-                totalWeightCount += surface.vertListCount;
-            }
-
-            if (surface.vertInfo.vertsBlend)
-            {
-                totalWeightCount += surface.vertInfo.vertCount[0] * 1;
-                totalWeightCount += surface.vertInfo.vertCount[1] * 2;
-                totalWeightCount += surface.vertInfo.vertCount[2] * 3;
-                totalWeightCount += surface.vertInfo.vertCount[3] * 4;
-            }
-        }
-
-        weightCollection.weights.resize(totalWeightCount);
-    }
-
-    float BoneWeight16(const uint16_t value)
-    {
-        return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
-    }
-
-    void AddXModelVertexBoneWeights(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-        auto& weightCollection = out.m_bone_weight_data;
-
-        size_t weightOffset = 0u;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-            auto handledVertices = 0u;
-
-            if (surface.vertList)
-            {
-                for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
-                {
-                    const auto& vertList = surface.vertList[vertListIndex];
-                    const auto boneWeightOffset = weightOffset;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{vertList.boneOffset / sizeof(DObjSkelMat), 1.0f};
-
-                    for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
-                    {
-                        out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                    }
-                    handledVertices += vertList.vertCount;
-                }
-            }
-
-            auto vertsBlendOffset = 0u;
-            if (surface.vertInfo.vertsBlend)
-            {
-                // 1 bone weight
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, 1.0f};
-
-                    vertsBlendOffset += 1;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                }
-
-                // 2 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneWeight0 = 1.0f - boneWeight1;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-
-                    vertsBlendOffset += 3;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
-                }
-
-                // 3 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-
-                    vertsBlendOffset += 5;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
-                }
-
-                // 4 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
-                    const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex3, boneWeight3};
-
-                    vertsBlendOffset += 7;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
-                }
-
-                handledVertices +=
-                    surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
-            }
-
-            for (; handledVertices < surface.vertCount; handledVertices++)
-            {
-                out.m_vertex_bone_weights.emplace_back(0, 0);
-            }
-        }
-    }
-
-    void AddXModelFaces(XModelCommon& out, const XModel* model, const unsigned lod)
-    {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
-        {
-            const auto& surface = surfs[surfIndex];
-            auto& object = out.m_objects[surfIndex];
-            object.m_faces.reserve(surface.triCount);
-
-            for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
-            {
-                const auto& tri = surface.triIndices[triIndex];
-
-                XModelFace face{};
-                face.vertexIndex[0] = tri[0] + surface.baseVertIndex;
-                face.vertexIndex[1] = tri[1] + surface.baseVertIndex;
-                face.vertexIndex[2] = tri[2] + surface.baseVertIndex;
-                object.m_faces.emplace_back(face);
-            }
-        }
-    }
-
-    void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel* model)
-    {
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-        AllocateXModelBoneWeights(model, lod, out.m_bone_weight_data);
-
-        out.m_name = std::format("{}_lod{}", model->name, lod);
-        AddXModelBones(out, context, model);
-        AddXModelMaterials(out, materialMapper, model);
-        AddXModelObjects(out, model, lod, materialMapper);
-        AddXModelVertices(out, model, lod);
-        AddXModelVertexBoneWeights(out, model, lod);
-        AddXModelFaces(out, model, lod);
-    }
-
-    void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-        const auto mtlFile = context.OpenAssetFile(std::format("model_export/{}.mtl", model->name));
-
-        if (!mtlFile)
-            return;
-
-        const auto writer = obj::CreateMtlWriter(*mtlFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpObjLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".obj"));
-
-        if (!assetFile)
-            return;
-
-        const auto writer =
-            obj::CreateObjWriter(*assetFile, std::format("{}.mtl", model->name), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelExportLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".XMODEL_EXPORT"));
-
-        if (!assetFile)
-            return;
-
-        const auto writer = xmodel_export::CreateWriterForVersion6(*assetFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        writer->Write(common);
-    }
-
-    template<typename T>
-    void DumpGltfLod(
-        const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod, const std::string& extension)
-    {
-        const auto* model = asset->Asset();
-        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, extension));
-
-        if (!assetFile)
-            return;
-
-        const auto output = std::make_unique<T>(*assetFile);
-        const auto writer = gltf::Writer::CreateWriter(output.get(), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-
-        for (auto currentLod = 0u; currentLod < model->numLods; currentLod++)
-        {
-            XModelCommon common;
-            PopulateXModelWriter(common, context, currentLod, asset->Asset());
-
-            switch (ObjWriting::Configuration.ModelOutputFormat)
-            {
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
-                DumpObjLod(common, context, asset, currentLod);
-                if (currentLod == 0u)
-                    DumpObjMtl(common, context, asset);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
-                DumpXModelExportLod(common, context, asset, currentLod);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
-                DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
-                DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
-                break;
-
-            default:
-                assert(false);
-                break;
-            }
-        }
-    }
-} // namespace
-
 bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 {
     return !asset->m_name.empty() && asset->m_name[0] != ',';
@@ -521,5 +11,5 @@ bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 
 void AssetDumperXModel::DumpAsset(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
 {
-    DumpXModelSurfs(context, asset);
+    DumpXModel(context, asset);
 }
diff --git a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
new file mode 100644
index 00000000..a29f79f6
--- /dev/null
+++ b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
@@ -0,0 +1,17 @@
+#include "XModelDumperT5.h"
+
+#include "Game/T5/CommonT5.h"
+#include "Game/T5/XModel/JsonXModel.h"
+
+#define GAME_NAMESPACE T5
+
+#include "XModel/GenericXModelDumper.inc.h"
+
+namespace T5
+{
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
+    {
+        DumpXModelJson(context, asset);
+        DumpXModelSurfs(context, asset);
+    }
+} // namespace T5
diff --git a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h
new file mode 100644
index 00000000..a5198e38
--- /dev/null
+++ b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "Dumping/AssetDumpingContext.h"
+#include "Game/T5/T5.h"
+
+namespace T5
+{
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset);
+}
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
index 7ccdd75d..e629d886 100644
--- a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
+++ b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
@@ -1,9 +1,10 @@
 #include "XModelDumperT6.h"
 
 #include "Game/T6/CommonT6.h"
-#include "Game/T6/Json/JsonXModel.h"
+#include "Game/T6/XModel/JsonXModel.h"
 
 #define GAME_NAMESPACE T6
+#define FEATURE_T6
 
 #include "XModel/GenericXModelDumper.inc.h"
 
diff --git a/src/ObjWriting/XModel/GenericXModelDumper.inc.h b/src/ObjWriting/XModel/GenericXModelDumper.inc.h
index 6a128bbe..9e44946e 100644
--- a/src/ObjWriting/XModel/GenericXModelDumper.inc.h
+++ b/src/ObjWriting/XModel/GenericXModelDumper.inc.h
@@ -25,6 +25,15 @@ namespace GAME_NAMESPACE
         return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
     }
 
+    inline GfxImage* GetImageFromTextureDef(const MaterialTextureDef& textureDef)
+    {
+#ifdef FEATURE_T6
+        return textureDef.image;
+#else
+        return textureDef.u.image;
+#endif
+    }
+
     inline GfxImage* GetMaterialColorMap(const Material* material)
     {
         std::vector<MaterialTextureDef*> potentialTextureDefs;
@@ -40,27 +49,27 @@ namespace GAME_NAMESPACE
         if (potentialTextureDefs.empty())
             return nullptr;
         if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
 
         for (const auto* def : potentialTextureDefs)
         {
             if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
-                return def->image;
+                return GetImageFromTextureDef(*def);
         }
 
         for (const auto* def : potentialTextureDefs)
         {
             if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
-                return def->image;
+                return GetImageFromTextureDef(*def);
         }
 
         for (const auto* def : potentialTextureDefs)
         {
             if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
-                return def->image;
+                return GetImageFromTextureDef(*def);
         }
 
-        return potentialTextureDefs[0]->image;
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
     }
 
     inline GfxImage* GetMaterialNormalMap(const Material* material)
@@ -78,15 +87,15 @@ namespace GAME_NAMESPACE
         if (potentialTextureDefs.empty())
             return nullptr;
         if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
 
         for (const auto* def : potentialTextureDefs)
         {
             if (def->nameStart == 'n' && def->nameEnd == 'p')
-                return def->image;
+                return GetImageFromTextureDef(*def);
         }
 
-        return potentialTextureDefs[0]->image;
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
     }
 
     inline GfxImage* GetMaterialSpecularMap(const Material* material)
@@ -104,15 +113,15 @@ namespace GAME_NAMESPACE
         if (potentialTextureDefs.empty())
             return nullptr;
         if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->image;
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
 
         for (const auto* def : potentialTextureDefs)
         {
             if (def->nameStart == 's' && def->nameEnd == 'p')
-                return def->image;
+                return GetImageFromTextureDef(*def);
         }
 
-        return potentialTextureDefs[0]->image;
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
     }
 
     inline bool HasDefaultArmature(const XModel* model, const unsigned lod)
@@ -654,10 +663,13 @@ namespace GAME_NAMESPACE
                 jXModel.physConstraints = AssetName(xmodel.physConstraints->name);
 
             jXModel.flags = xmodel.flags;
+
+#ifdef FEATURE_T6
             jXModel.lightingOriginOffset.x = xmodel.lightingOriginOffset.x;
             jXModel.lightingOriginOffset.y = xmodel.lightingOriginOffset.y;
             jXModel.lightingOriginOffset.z = xmodel.lightingOriginOffset.z;
             jXModel.lightingOriginRange = xmodel.lightingOriginRange;
+#endif
         }
 
         std::ostream& m_stream;

From 7227c84cde7513ba51ddccbd33a0ca3b84e6bcee Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sat, 14 Sep 2024 12:21:43 +0200
Subject: [PATCH 06/17] chore: use RawTemplater to template XModel loading code
 for t5 and t6

---
 premake5.lua                                  |  1 +
 .../XModel/{JsonXModel.h => JsonXModelT5.h}   |  0
 .../Game/T5/XModel/XModelConstantsT5.h        | 31 +++++++++
 .../XModel/{JsonXModel.h => JsonXModelT6.h}   |  0
 .../Game/T6/XModel/XModelConstantsT6.h        | 33 ++++++++++
 src/ObjLoading.lua                            |  2 +
 .../Game/T5/XModel/XModelLoaderT5.cpp         | 49 --------------
 .../Game/T6/XModel/XModelLoaderT6.cpp         | 51 ---------------
 .../Game/T6/XModel/XModelLoaderT6.h           | 13 ----
 src/ObjLoading/XModel/Gltf/GltfLoader.h       |  4 +-
 src/ObjLoading/XModel/XModelFileLoader.h      | 18 ++++++
 ...Loader.inc.h => XModelLoader.cpp.template} | 36 +++++++++--
 src/ObjLoading/XModel/XModelLoader.h          | 18 ------
 .../XModelLoader.h.template}                  | 10 ++-
 .../Game/T5/XModel/XModelDumperT5.cpp         |  2 +-
 .../Game/T6/XModel/XModelDumperT6.cpp         |  2 +-
 tools/scripts/source_templating.lua           | 64 +++++++++++++++++++
 17 files changed, 192 insertions(+), 142 deletions(-)
 rename src/ObjCommon/Game/T5/XModel/{JsonXModel.h => JsonXModelT5.h} (100%)
 create mode 100644 src/ObjCommon/Game/T5/XModel/XModelConstantsT5.h
 rename src/ObjCommon/Game/T6/XModel/{JsonXModel.h => JsonXModelT6.h} (100%)
 create mode 100644 src/ObjCommon/Game/T6/XModel/XModelConstantsT6.h
 delete mode 100644 src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
 delete mode 100644 src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
 delete mode 100644 src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
 create mode 100644 src/ObjLoading/XModel/XModelFileLoader.h
 rename src/ObjLoading/XModel/{GenericXModelLoader.inc.h => XModelLoader.cpp.template} (97%)
 delete mode 100644 src/ObjLoading/XModel/XModelLoader.h
 rename src/ObjLoading/{Game/T5/XModel/XModelLoaderT5.h => XModel/XModelLoader.h.template} (62%)
 create mode 100644 tools/scripts/source_templating.lua

diff --git a/premake5.lua b/premake5.lua
index 9e850d85..0ac4a6ac 100644
--- a/premake5.lua
+++ b/premake5.lua
@@ -4,6 +4,7 @@ include "tools/scripts/linking.lua"
 include "tools/scripts/options.lua"
 include "tools/scripts/platform.lua"
 include "tools/scripts/version.lua"
+include "tools/scripts/source_templating.lua"
 
 -- ==================
 -- Workspace
diff --git a/src/ObjCommon/Game/T5/XModel/JsonXModel.h b/src/ObjCommon/Game/T5/XModel/JsonXModelT5.h
similarity index 100%
rename from src/ObjCommon/Game/T5/XModel/JsonXModel.h
rename to src/ObjCommon/Game/T5/XModel/JsonXModelT5.h
diff --git a/src/ObjCommon/Game/T5/XModel/XModelConstantsT5.h b/src/ObjCommon/Game/T5/XModel/XModelConstantsT5.h
new file mode 100644
index 00000000..0481cd27
--- /dev/null
+++ b/src/ObjCommon/Game/T5/XModel/XModelConstantsT5.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "Game/T5/T5.h"
+
+namespace T5
+{
+    inline const char* HITLOC_NAMES[]{
+        // clang-format off
+        "none",
+        "helmet",
+        "head",
+        "neck",
+        "torso_upper",
+        "torso_lower",
+        "right_arm_upper",
+        "left_arm_upper",
+        "right_arm_lower",
+        "left_arm_lower",
+        "right_hand",
+        "left_hand",
+        "right_leg_upper",
+        "left_leg_upper",
+        "right_leg_lower",
+        "left_leg_lower",
+        "right_foot",
+        "left_foot",
+        "gun",
+        // clang-format on
+    };
+    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
+} // namespace T5
diff --git a/src/ObjCommon/Game/T6/XModel/JsonXModel.h b/src/ObjCommon/Game/T6/XModel/JsonXModelT6.h
similarity index 100%
rename from src/ObjCommon/Game/T6/XModel/JsonXModel.h
rename to src/ObjCommon/Game/T6/XModel/JsonXModelT6.h
diff --git a/src/ObjCommon/Game/T6/XModel/XModelConstantsT6.h b/src/ObjCommon/Game/T6/XModel/XModelConstantsT6.h
new file mode 100644
index 00000000..e7ae94cd
--- /dev/null
+++ b/src/ObjCommon/Game/T6/XModel/XModelConstantsT6.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "Game/T6/T6.h"
+
+namespace T6
+{
+    inline const char* HITLOC_NAMES[]{
+        // clang-format off
+        "none",
+        "helmet",
+        "head",
+        "neck",
+        "torso_upper",
+        "torso_middle",
+        "torso_lower",
+        "right_arm_upper",
+        "left_arm_upper",
+        "right_arm_lower",
+        "left_arm_lower",
+        "right_hand",
+        "left_hand",
+        "right_leg_upper",
+        "left_leg_upper",
+        "right_leg_lower",
+        "left_leg_lower",
+        "right_foot",
+        "left_foot",
+        "gun",
+        "shield",
+        // clang-format on
+    };
+    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
+} // namespace T6
diff --git a/src/ObjLoading.lua b/src/ObjLoading.lua
index 9452e56e..45f110c2 100644
--- a/src/ObjLoading.lua
+++ b/src/ObjLoading.lua
@@ -49,6 +49,8 @@ function ObjLoading:project()
 				path.join(folder, "ObjLoading")
 			}
 		}
+
+		useSourceTemplating("ObjLoading")
 		
 		self:include(includes)
 		Crypto:include(includes)
diff --git a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp b/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
deleted file mode 100644
index 55f19595..00000000
--- a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.cpp
+++ /dev/null
@@ -1,49 +0,0 @@
-#include "XModelLoaderT5.h"
-
-#include "Game/T5/CommonT5.h"
-#include "Game/T5/XModel/JsonXModel.h"
-
-#define GAME_NAMESPACE T5
-
-namespace T5
-{
-    const char* HITLOC_NAMES[]{
-        // clang-format off
-        "none",
-        "helmet",
-        "head",
-        "neck",
-        "torso_upper",
-        "torso_lower",
-        "right_arm_upper",
-        "left_arm_upper",
-        "right_arm_lower",
-        "left_arm_lower",
-        "right_hand",
-        "left_hand",
-        "right_leg_upper",
-        "left_leg_upper",
-        "right_leg_lower",
-        "left_leg_lower",
-        "right_foot",
-        "left_foot",
-        "gun",
-        // clang-format on
-    };
-    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
-} // namespace T5
-
-#include "XModel/GenericXModelLoader.inc.h"
-
-namespace T5
-{
-    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
-    {
-        std::set<XAssetInfoGeneric*> dependenciesSet;
-        XModelLoader loader(stream, *memory, *manager, dependenciesSet);
-
-        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
-
-        return loader.Load(xmodel);
-    }
-} // namespace T5
diff --git a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
deleted file mode 100644
index 3beb26cb..00000000
--- a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.cpp
+++ /dev/null
@@ -1,51 +0,0 @@
-#include "XModelLoaderT6.h"
-
-#include "Game/T6/CommonT6.h"
-#include "Game/T6/XModel/JsonXModel.h"
-
-#define GAME_NAMESPACE T6
-
-namespace T6
-{
-    const char* HITLOC_NAMES[]{
-        // clang-format off
-        "none",
-        "helmet",
-        "head",
-        "neck",
-        "torso_upper",
-        "torso_middle",
-        "torso_lower",
-        "right_arm_upper",
-        "left_arm_upper",
-        "right_arm_lower",
-        "left_arm_lower",
-        "right_hand",
-        "left_hand",
-        "right_leg_upper",
-        "left_leg_upper",
-        "right_leg_lower",
-        "left_leg_lower",
-        "right_foot",
-        "left_foot",
-        "gun",
-        "shield",
-        // clang-format on
-    };
-    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
-} // namespace T6
-
-#include "XModel/GenericXModelLoader.inc.h"
-
-namespace T6
-{
-    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
-    {
-        std::set<XAssetInfoGeneric*> dependenciesSet;
-        XModelLoader loader(stream, *memory, *manager, dependenciesSet);
-
-        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
-
-        return loader.Load(xmodel);
-    }
-} // namespace T6
diff --git a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h b/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
deleted file mode 100644
index e55883e5..00000000
--- a/src/ObjLoading/Game/T6/XModel/XModelLoaderT6.h
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma once
-
-#include "AssetLoading/IAssetLoadingManager.h"
-#include "Game/T6/T6.h"
-#include "Utils/MemoryManager.h"
-
-#include <istream>
-#include <vector>
-
-namespace T6
-{
-    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
-}
diff --git a/src/ObjLoading/XModel/Gltf/GltfLoader.h b/src/ObjLoading/XModel/Gltf/GltfLoader.h
index b12d8d98..872394e4 100644
--- a/src/ObjLoading/XModel/Gltf/GltfLoader.h
+++ b/src/ObjLoading/XModel/Gltf/GltfLoader.h
@@ -2,14 +2,14 @@
 
 #include "GltfInput.h"
 #include "XModel/Gltf/JsonGltf.h"
-#include "XModel/XModelLoader.h"
+#include "XModel/XModelFileLoader.h"
 
 #include <memory>
 #include <ostream>
 
 namespace gltf
 {
-    class Loader : public XModelLoader
+    class Loader : public XModelFileLoader
     {
     public:
         Loader() = default;
diff --git a/src/ObjLoading/XModel/XModelFileLoader.h b/src/ObjLoading/XModel/XModelFileLoader.h
new file mode 100644
index 00000000..ad5bd918
--- /dev/null
+++ b/src/ObjLoading/XModel/XModelFileLoader.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "XModel/XModelCommon.h"
+
+#include <memory>
+
+class XModelFileLoader
+{
+public:
+    XModelFileLoader() = default;
+    virtual ~XModelFileLoader() = default;
+    XModelFileLoader(const XModelFileLoader& other) = default;
+    XModelFileLoader(XModelFileLoader&& other) noexcept = default;
+    XModelFileLoader& operator=(const XModelFileLoader& other) = default;
+    XModelFileLoader& operator=(XModelFileLoader&& other) noexcept = default;
+
+    virtual std::unique_ptr<XModelCommon> Load() = 0;
+};
diff --git a/src/ObjLoading/XModel/GenericXModelLoader.inc.h b/src/ObjLoading/XModel/XModelLoader.cpp.template
similarity index 97%
rename from src/ObjLoading/XModel/GenericXModelLoader.inc.h
rename to src/ObjLoading/XModel/XModelLoader.cpp.template
index 2c9610c5..5cdd8575 100644
--- a/src/ObjLoading/XModel/GenericXModelLoader.inc.h
+++ b/src/ObjLoading/XModel/XModelLoader.cpp.template
@@ -1,9 +1,24 @@
-#pragma once
+#options GAME (T5, T6)
 
-#ifndef GAME_NAMESPACE
-#error Must define GAME_NAMESPACE
+#filename "Game/" + GAME + "/XModel/XModelLoader" + GAME + ".cpp"
+
+#set LOADER_HEADER "\"XModelLoader" + GAME + ".h\""
+#set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\""
+#set CONSTANTS_HEADER "\"Game/" + GAME + "/XModel/XModelConstants" + GAME + ".h\""
+#set JSON_HEADER "\"Game/" + GAME + "/XModel/JsonXModel" + GAME + ".h\""
+
+#if GAME == "T5"
+#define FEATURE_T5
+#elif GAME == "T6"
+#define FEATURE_T6
 #endif
 
+#include LOADER_HEADER
+
+#include COMMON_HEADER
+#include CONSTANTS_HEADER
+#include JSON_HEADER
+
 #include "ObjLoading.h"
 #include "Utils/QuatInt16.h"
 #include "Utils/StringUtils.h"
@@ -26,9 +41,10 @@
 #include <format>
 #include <iostream>
 #include <numeric>
+#include <set>
 #include <vector>
 
-namespace GAME_NAMESPACE
+namespace GAME
 {
     class XModelLoader
     {
@@ -794,4 +810,14 @@ namespace GAME_NAMESPACE
         PartClassificationState& m_part_classification_state;
         std::set<XAssetInfoGeneric*>& m_dependencies;
     };
-} // namespace GAME_NAMESPACE
+
+    bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
+    {
+        std::set<XAssetInfoGeneric*> dependenciesSet;
+        XModelLoader loader(stream, *memory, *manager, dependenciesSet);
+
+        dependencies.assign(dependenciesSet.cbegin(), dependenciesSet.cend());
+
+        return loader.Load(xmodel);
+    }
+} // namespace GAME
diff --git a/src/ObjLoading/XModel/XModelLoader.h b/src/ObjLoading/XModel/XModelLoader.h
deleted file mode 100644
index 8aeca27a..00000000
--- a/src/ObjLoading/XModel/XModelLoader.h
+++ /dev/null
@@ -1,18 +0,0 @@
-#pragma once
-
-#include "XModel/XModelCommon.h"
-
-#include <memory>
-
-class XModelLoader
-{
-public:
-    XModelLoader() = default;
-    virtual ~XModelLoader() = default;
-    XModelLoader(const XModelLoader& other) = default;
-    XModelLoader(XModelLoader&& other) noexcept = default;
-    XModelLoader& operator=(const XModelLoader& other) = default;
-    XModelLoader& operator=(XModelLoader&& other) noexcept = default;
-
-    virtual std::unique_ptr<XModelCommon> Load() = 0;
-};
diff --git a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h b/src/ObjLoading/XModel/XModelLoader.h.template
similarity index 62%
rename from src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h
rename to src/ObjLoading/XModel/XModelLoader.h.template
index cf1c3951..6aeb6877 100644
--- a/src/ObjLoading/Game/T5/XModel/XModelLoaderT5.h
+++ b/src/ObjLoading/XModel/XModelLoader.h.template
@@ -1,13 +1,19 @@
+#options GAME (T5, T6)
+
+#filename "Game/" + GAME + "/XModel/XModelLoader" + GAME + ".h"
+
+#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
+
 #pragma once
 
 #include "AssetLoading/IAssetLoadingManager.h"
-#include "Game/T5/T5.h"
+#include GAME_HEADER
 #include "Utils/MemoryManager.h"
 
 #include <istream>
 #include <vector>
 
-namespace T5
+namespace GAME
 {
     bool LoadXModel(std::istream& stream, XModel& xmodel, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
 }
diff --git a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
index a29f79f6..d9ece077 100644
--- a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
+++ b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
@@ -1,7 +1,7 @@
 #include "XModelDumperT5.h"
 
 #include "Game/T5/CommonT5.h"
-#include "Game/T5/XModel/JsonXModel.h"
+#include "Game/T5/XModel/JsonXModelT5.h"
 
 #define GAME_NAMESPACE T5
 
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
index e629d886..59dbe4cd 100644
--- a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
+++ b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
@@ -1,7 +1,7 @@
 #include "XModelDumperT6.h"
 
 #include "Game/T6/CommonT6.h"
-#include "Game/T6/XModel/JsonXModel.h"
+#include "Game/T6/XModel/JsonXModelT6.h"
 
 #define GAME_NAMESPACE T6
 #define FEATURE_T6
diff --git a/tools/scripts/source_templating.lua b/tools/scripts/source_templating.lua
new file mode 100644
index 00000000..a64ee53f
--- /dev/null
+++ b/tools/scripts/source_templating.lua
@@ -0,0 +1,64 @@
+function useSourceTemplating(projectName)
+    local projectFolder = path.join(ProjectFolder(), projectName)
+    local templateFiles = os.matchfiles(path.join(projectFolder, "**.template"))
+
+    local createdFiles = {}
+
+    for i = 1, #templateFiles do
+        local templateFile = templateFiles[i]
+        local relativeTemplatePath = path.getrelative(projectFolder, templateFile)
+        local relativeResultPath = path.replaceextension(relativeTemplatePath, "")
+        local resultExtension = path.getextension(relativeResultPath)
+
+        local data = io.readfile(templateFile)
+        local gameOptionsStart, gameOptionsCount = string.find(data, "#options%s+GAME%s*%(")
+
+        if gameOptionsStart == nil then
+            error("Source template " .. relativeTemplatePath .. " must define an option called GAME")
+        end
+
+        local gameOptionsPos, gameOptionsLenPlusOne = string.find(data, "[%a%d%s,]+%)", gameOptionsStart + gameOptionsCount)
+
+        if gameOptionsPos ~= gameOptionsStart + gameOptionsCount then
+            error("Source template " .. relativeTemplatePath .. " must define an option called GAME")
+        end
+
+        local gameOptions = string.sub(data, gameOptionsPos, gameOptionsLenPlusOne - 1)
+        local games = string.explode(gameOptions, ",%s*")
+
+        files {
+            templateFile
+        }
+
+        filter("files:" .. templateFile)
+            buildmessage("Templating source file " .. relativeTemplatePath)
+            buildinputs {
+                TargetDirectoryBuildTools .. "/" .. ExecutableByOs('RawTemplater')
+            }
+            buildcommands {
+                '"' .. TargetDirectoryBuildTools .. '/' .. ExecutableByOs('RawTemplater') .. '"' 
+                .. ' -o "%{prj.location}/"'
+                .. " %{file.relpath}"
+            }
+            for i = 1, #games do
+                local gameName = games[i]
+                local outputFileName = path.replaceextension(path.replaceextension(relativeResultPath, "") .. gameName, resultExtension)
+                local outputFile = "%{prj.location}/Game/" .. gameName .. "/" .. outputFileName
+
+                table.insert(createdFiles, outputFile)
+
+                buildoutputs {
+                    outputFile
+                }
+            end
+        filter {}
+
+		includedirs {
+			"%{prj.location}"
+		}
+
+        files {
+            createdFiles
+        }
+    end
+end

From fc216a153f28c0423718487c515f980ab2fe7e2d Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sat, 14 Sep 2024 12:46:57 +0200
Subject: [PATCH 07/17] chore: use RawTemplater to template XModel dumping code
 for t5 and t6

---
 src/ObjWriting.lua                            |   2 +
 .../Game/T5/XModel/XModelDumperT5.cpp         |  17 -
 .../Game/T5/XModel/XModelDumperT5.h           |   9 -
 .../Game/T6/XModel/XModelDumperT6.cpp         |  18 -
 .../Game/T6/XModel/XModelDumperT6.h           |   9 -
 .../XModel/XModelDumper.cpp.template          | 705 ++++++++++++++++++
 src/ObjWriting/XModel/XModelDumper.h.template |  15 +
 7 files changed, 722 insertions(+), 53 deletions(-)
 delete mode 100644 src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
 delete mode 100644 src/ObjWriting/Game/T5/XModel/XModelDumperT5.h
 delete mode 100644 src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
 delete mode 100644 src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
 create mode 100644 src/ObjWriting/XModel/XModelDumper.cpp.template
 create mode 100644 src/ObjWriting/XModel/XModelDumper.h.template

diff --git a/src/ObjWriting.lua b/src/ObjWriting.lua
index 6ca48e19..39f15dca 100644
--- a/src/ObjWriting.lua
+++ b/src/ObjWriting.lua
@@ -50,6 +50,8 @@ function ObjWriting:project()
 				path.join(folder, "ObjWriting")
 			}
 		}
+
+		useSourceTemplating("ObjWriting")
 		
         self:include(includes)
 		Utils:include(includes)
diff --git a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
deleted file mode 100644
index d9ece077..00000000
--- a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#include "XModelDumperT5.h"
-
-#include "Game/T5/CommonT5.h"
-#include "Game/T5/XModel/JsonXModelT5.h"
-
-#define GAME_NAMESPACE T5
-
-#include "XModel/GenericXModelDumper.inc.h"
-
-namespace T5
-{
-    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
-    {
-        DumpXModelJson(context, asset);
-        DumpXModelSurfs(context, asset);
-    }
-} // namespace T5
diff --git a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h b/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h
deleted file mode 100644
index a5198e38..00000000
--- a/src/ObjWriting/Game/T5/XModel/XModelDumperT5.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma once
-
-#include "Dumping/AssetDumpingContext.h"
-#include "Game/T5/T5.h"
-
-namespace T5
-{
-    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset);
-}
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
deleted file mode 100644
index 59dbe4cd..00000000
--- a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.cpp
+++ /dev/null
@@ -1,18 +0,0 @@
-#include "XModelDumperT6.h"
-
-#include "Game/T6/CommonT6.h"
-#include "Game/T6/XModel/JsonXModelT6.h"
-
-#define GAME_NAMESPACE T6
-#define FEATURE_T6
-
-#include "XModel/GenericXModelDumper.inc.h"
-
-namespace T6
-{
-    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
-    {
-        DumpXModelJson(context, asset);
-        DumpXModelSurfs(context, asset);
-    }
-} // namespace T6
diff --git a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h b/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
deleted file mode 100644
index b63027f1..00000000
--- a/src/ObjWriting/Game/T6/XModel/XModelDumperT6.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma once
-
-#include "Dumping/AssetDumpingContext.h"
-#include "Game/T6/T6.h"
-
-namespace T6
-{
-    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset);
-}
diff --git a/src/ObjWriting/XModel/XModelDumper.cpp.template b/src/ObjWriting/XModel/XModelDumper.cpp.template
new file mode 100644
index 00000000..0a775f19
--- /dev/null
+++ b/src/ObjWriting/XModel/XModelDumper.cpp.template
@@ -0,0 +1,705 @@
+#options GAME (T5, T6)
+
+#filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".cpp"
+
+#set DUMPER_HEADER "\"XModelDumper" + GAME + ".h\""
+#set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\""
+#set JSON_HEADER "\"Game/" + GAME + "/XModel/JsonXModel" + GAME + ".h\""
+
+#if GAME == "T5"
+#define FEATURE_T5
+#elif GAME == "T6"
+#define FEATURE_T6
+#endif
+
+#include DUMPER_HEADER
+
+#include COMMON_HEADER
+#include JSON_HEADER
+
+#include "ObjWriting.h"
+#include "Utils/DistinctMapper.h"
+#include "Utils/QuatInt16.h"
+#include "XModel/Export/XModelExportWriter.h"
+#include "XModel/Gltf/GltfBinOutput.h"
+#include "XModel/Gltf/GltfTextOutput.h"
+#include "XModel/Gltf/GltfWriter.h"
+#include "XModel/Obj/ObjWriter.h"
+#include "XModel/XModelWriter.h"
+
+#include <cassert>
+#include <format>
+
+namespace GAME
+{
+    std::string GetFileNameForLod(const std::string& modelName, const unsigned lod, const std::string& extension)
+    {
+        return std::format("model_export/{}_lod{}{}", modelName, lod, extension);
+    }
+
+    GfxImage* GetImageFromTextureDef(const MaterialTextureDef& textureDef)
+    {
+#ifdef FEATURE_T6
+        return textureDef.image;
+#else
+        return textureDef.u.image;
+#endif
+    }
+
+    GfxImage* GetMaterialColorMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
+                return GetImageFromTextureDef(*def);
+        }
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'r' && tolower(def->nameEnd) == 'k')
+                return GetImageFromTextureDef(*def);
+        }
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (tolower(def->nameStart) == 'd' && tolower(def->nameEnd) == 'p')
+                return GetImageFromTextureDef(*def);
+        }
+
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
+    }
+
+    GfxImage* GetMaterialNormalMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_NORMAL_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (def->nameStart == 'n' && def->nameEnd == 'p')
+                return GetImageFromTextureDef(*def);
+        }
+
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
+    }
+
+    GfxImage* GetMaterialSpecularMap(const Material* material)
+    {
+        std::vector<MaterialTextureDef*> potentialTextureDefs;
+
+        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
+        {
+            MaterialTextureDef* def = &material->textureTable[textureIndex];
+
+            if (def->semantic == TS_SPECULAR_MAP)
+                potentialTextureDefs.push_back(def);
+        }
+
+        if (potentialTextureDefs.empty())
+            return nullptr;
+        if (potentialTextureDefs.size() == 1)
+            return GetImageFromTextureDef(*potentialTextureDefs[0]);
+
+        for (const auto* def : potentialTextureDefs)
+        {
+            if (def->nameStart == 's' && def->nameEnd == 'p')
+                return GetImageFromTextureDef(*def);
+        }
+
+        return GetImageFromTextureDef(*potentialTextureDefs[0]);
+    }
+
+    bool HasDefaultArmature(const XModel* model, const unsigned lod)
+    {
+        if (model->numRootBones != 1 || model->numBones != 1)
+            return false;
+
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return true;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            if (surface.vertListCount != 1 || surface.vertInfo.vertsBlend)
+                return false;
+
+            const auto& vertList = surface.vertList[0];
+            if (vertList.boneOffset != 0 || vertList.triOffset != 0 || vertList.triCount != surface.triCount || vertList.vertCount != surface.vertCount)
+                return false;
+        }
+
+        return true;
+    }
+
+    void OmitDefaultArmature(XModelCommon& common)
+    {
+        common.m_bones.clear();
+        common.m_bone_weight_data.weights.clear();
+        common.m_vertex_bone_weights.resize(common.m_vertices.size());
+        for (auto& vertexWeights : common.m_vertex_bone_weights)
+        {
+            vertexWeights.weightOffset = 0u;
+            vertexWeights.weightCount = 0u;
+        }
+    }
+
+    void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel* model)
+    {
+        for (auto boneNum = 0u; boneNum < model->numBones; boneNum++)
+        {
+            XModelBone bone;
+            if (model->boneNames[boneNum] < context.m_zone->m_script_strings.Count())
+                bone.name = context.m_zone->m_script_strings[model->boneNames[boneNum]];
+            else
+                bone.name = "INVALID_BONE_NAME";
+
+            if (boneNum >= model->numRootBones)
+                bone.parentIndex = static_cast<int>(boneNum - static_cast<unsigned int>(model->parentList[boneNum - model->numRootBones]));
+            else
+                bone.parentIndex = std::nullopt;
+
+            bone.scale[0] = 1.0f;
+            bone.scale[1] = 1.0f;
+            bone.scale[2] = 1.0f;
+
+            const auto& baseMat = model->baseMat[boneNum];
+            bone.globalOffset[0] = baseMat.trans.x;
+            bone.globalOffset[1] = baseMat.trans.y;
+            bone.globalOffset[2] = baseMat.trans.z;
+            bone.globalRotation = {
+                baseMat.quat.x,
+                baseMat.quat.y,
+                baseMat.quat.z,
+                baseMat.quat.w,
+            };
+
+            if (boneNum < model->numRootBones)
+            {
+                bone.localOffset[0] = 0;
+                bone.localOffset[1] = 0;
+                bone.localOffset[2] = 0;
+                bone.localRotation = {0, 0, 0, 1};
+            }
+            else
+            {
+                const auto* trans = &model->trans[(boneNum - model->numRootBones) * 3];
+                bone.localOffset[0] = trans[0];
+                bone.localOffset[1] = trans[1];
+                bone.localOffset[2] = trans[2];
+
+                const auto& quat = model->quats[boneNum - model->numRootBones];
+                bone.localRotation = {
+                    QuatInt16::ToFloat(quat.v[0]),
+                    QuatInt16::ToFloat(quat.v[1]),
+                    QuatInt16::ToFloat(quat.v[2]),
+                    QuatInt16::ToFloat(quat.v[3]),
+                };
+            }
+
+            out.m_bones.emplace_back(std::move(bone));
+        }
+    }
+
+    const char* AssetName(const char* input)
+    {
+        if (input && input[0] == ',')
+            return &input[1];
+
+        return input;
+    }
+
+    void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel* model)
+    {
+        for (auto surfaceMaterialNum = 0; surfaceMaterialNum < model->numsurfs; surfaceMaterialNum++)
+        {
+            Material* material = model->materialHandles[surfaceMaterialNum];
+            if (materialMapper.Add(material))
+            {
+                XModelMaterial xMaterial;
+                xMaterial.ApplyDefaults();
+
+                xMaterial.name = AssetName(material->info.name);
+                const auto* colorMap = GetMaterialColorMap(material);
+                if (colorMap)
+                    xMaterial.colorMapName = AssetName(colorMap->name);
+
+                const auto* normalMap = GetMaterialNormalMap(material);
+                if (normalMap)
+                    xMaterial.normalMapName = AssetName(normalMap->name);
+
+                const auto* specularMap = GetMaterialSpecularMap(material);
+                if (specularMap)
+                    xMaterial.specularMapName = AssetName(specularMap->name);
+
+                out.m_materials.emplace_back(std::move(xMaterial));
+            }
+        }
+    }
+
+    void AddXModelObjects(XModelCommon& out, const XModel* model, const unsigned lod, const DistinctMapper<Material*>& materialMapper)
+    {
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+        const auto baseSurfaceIndex = model->lodInfo[lod].surfIndex;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            XModelObject object;
+            object.name = std::format("surf{}", surfIndex);
+            object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
+
+            out.m_objects.emplace_back(std::move(object));
+        }
+    }
+
+    void AddXModelVertices(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
+            {
+                const auto& v = surface.verts0[vertexIndex];
+
+                XModelVertex vertex{};
+                vertex.coordinates[0] = v.xyz.x;
+                vertex.coordinates[1] = v.xyz.y;
+                vertex.coordinates[2] = v.xyz.z;
+                Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
+                Common::Vec4UnpackGfxColor(v.color, vertex.color);
+                Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
+
+                out.m_vertices.emplace_back(vertex);
+            }
+        }
+    }
+
+    void AllocateXModelBoneWeights(const XModel* model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        auto totalWeightCount = 0u;
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+
+            if (surface.vertList)
+            {
+                totalWeightCount += surface.vertListCount;
+            }
+
+            if (surface.vertInfo.vertsBlend)
+            {
+                totalWeightCount += surface.vertInfo.vertCount[0] * 1;
+                totalWeightCount += surface.vertInfo.vertCount[1] * 2;
+                totalWeightCount += surface.vertInfo.vertCount[2] * 3;
+                totalWeightCount += surface.vertInfo.vertCount[3] * 4;
+            }
+        }
+
+        weightCollection.weights.resize(totalWeightCount);
+    }
+
+    float BoneWeight16(const uint16_t value)
+    {
+        return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
+    }
+
+    void AddXModelVertexBoneWeights(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+        auto& weightCollection = out.m_bone_weight_data;
+
+        if (!surfs)
+            return;
+
+        size_t weightOffset = 0u;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+            auto handledVertices = 0u;
+
+            if (surface.vertList)
+            {
+                for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
+                {
+                    const auto& vertList = surface.vertList[vertListIndex];
+                    const auto boneWeightOffset = weightOffset;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{vertList.boneOffset / sizeof(DObjSkelMat), 1.0f};
+
+                    for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
+                    {
+                        out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
+                    }
+                    handledVertices += vertList.vertCount;
+                }
+            }
+
+            auto vertsBlendOffset = 0u;
+            if (surface.vertInfo.vertsBlend)
+            {
+                // 1 bone weight
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, 1.0f};
+
+                    vertsBlendOffset += 1;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
+                }
+
+                // 2 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneWeight0 = 1.0f - boneWeight1;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+
+                    vertsBlendOffset += 3;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
+                }
+
+                // 3 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
+                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
+                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
+
+                    vertsBlendOffset += 5;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
+                }
+
+                // 4 bone weights
+                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
+                {
+                    const auto boneWeightOffset = weightOffset;
+                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
+                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
+                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
+                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
+                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
+                    const auto boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
+                    const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
+                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
+
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
+                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex3, boneWeight3};
+
+                    vertsBlendOffset += 7;
+
+                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
+                }
+
+                handledVertices +=
+                    surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
+            }
+
+            for (; handledVertices < surface.vertCount; handledVertices++)
+            {
+                out.m_vertex_bone_weights.emplace_back(0, 0);
+            }
+        }
+    }
+
+    void AddXModelFaces(XModelCommon& out, const XModel* model, const unsigned lod)
+    {
+        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        const auto surfCount = model->lodInfo[lod].numsurfs;
+
+        if (!surfs)
+            return;
+
+        for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
+        {
+            const auto& surface = surfs[surfIndex];
+            auto& object = out.m_objects[surfIndex];
+            object.m_faces.reserve(surface.triCount);
+
+            for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
+            {
+                const auto& tri = surface.triIndices[triIndex];
+
+                XModelFace face{};
+                face.vertexIndex[0] = tri.i[0] + surface.baseVertIndex;
+                face.vertexIndex[1] = tri.i[1] + surface.baseVertIndex;
+                face.vertexIndex[2] = tri.i[2] + surface.baseVertIndex;
+                object.m_faces.emplace_back(face);
+            }
+        }
+    }
+
+    void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel* model)
+    {
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+        AllocateXModelBoneWeights(model, lod, out.m_bone_weight_data);
+
+        out.m_name = std::format("{}_lod{}", model->name, lod);
+        AddXModelMaterials(out, materialMapper, model);
+        AddXModelObjects(out, model, lod, materialMapper);
+        AddXModelVertices(out, model, lod);
+        AddXModelFaces(out, model, lod);
+
+        if (!HasDefaultArmature(model, lod))
+        {
+            AddXModelBones(out, context, model);
+            AddXModelVertexBoneWeights(out, model, lod);
+        }
+        else
+        {
+            OmitDefaultArmature(out);
+        }
+    }
+
+    void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
+    {
+        const auto* model = asset->Asset();
+        const auto mtlFile = context.OpenAssetFile(std::format("model_export/{}.mtl", model->name));
+
+        if (!mtlFile)
+            return;
+
+        const auto writer = obj::CreateMtlWriter(*mtlFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+
+        writer->Write(common);
+    }
+
+    void DumpObjLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".obj"));
+
+        if (!assetFile)
+            return;
+
+        const auto writer =
+            obj::CreateObjWriter(*assetFile, std::format("{}.mtl", model->name), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        DistinctMapper<Material*> materialMapper(model->numsurfs);
+
+        writer->Write(common);
+    }
+
+    void DumpXModelExportLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, ".XMODEL_EXPORT"));
+
+        if (!assetFile)
+            return;
+
+        const auto writer = xmodel_export::CreateWriterForVersion6(*assetFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+        writer->Write(common);
+    }
+
+    template<typename T>
+    void DumpGltfLod(
+        const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod, const std::string& extension)
+    {
+        const auto* model = asset->Asset();
+        const auto assetFile = context.OpenAssetFile(GetFileNameForLod(model->name, lod, extension));
+
+        if (!assetFile)
+            return;
+
+        const auto output = std::make_unique<T>(*assetFile);
+        const auto writer = gltf::Writer::CreateWriter(output.get(), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
+
+        writer->Write(common);
+    }
+
+    void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
+    {
+        const auto* model = asset->Asset();
+
+        for (auto currentLod = 0u; currentLod < model->numLods; currentLod++)
+        {
+            XModelCommon common;
+            PopulateXModelWriter(common, context, currentLod, asset->Asset());
+
+            switch (ObjWriting::Configuration.ModelOutputFormat)
+            {
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
+                DumpObjLod(common, context, asset, currentLod);
+                if (currentLod == 0u)
+                    DumpObjMtl(common, context, asset);
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
+                DumpXModelExportLod(common, context, asset, currentLod);
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
+                DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
+                break;
+
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
+                DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
+                break;
+
+            default:
+                assert(false);
+                break;
+            }
+        }
+    }
+
+    class JsonDumper
+    {
+    public:
+        JsonDumper(AssetDumpingContext& context, std::ostream& stream)
+            : m_stream(stream)
+        {
+        }
+
+        void Dump(const XModel* xmodel) const
+        {
+            JsonXModel jsonXModel;
+            CreateJsonXModel(jsonXModel, *xmodel);
+            nlohmann::json jRoot = jsonXModel;
+
+            jRoot["_type"] = "xmodel";
+            jRoot["_version"] = 1;
+
+            m_stream << std::setw(4) << jRoot << "\n";
+        }
+
+    private:
+        static const char* AssetName(const char* input)
+        {
+            if (input && input[0] == ',')
+                return &input[1];
+
+            return input;
+        }
+
+        static const char* GetExtensionForModelByConfig()
+        {
+            switch (ObjWriting::Configuration.ModelOutputFormat)
+            {
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
+                return ".XMODEL_EXPORT";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
+                return ".OBJ";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
+                return ".GLTF";
+            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
+                return ".GLB";
+            default:
+                assert(false);
+                return "";
+            }
+        }
+
+        static void CreateJsonXModel(JsonXModel& jXModel, const XModel& xmodel)
+        {
+            if (xmodel.collLod >= 0)
+                jXModel.collLod = xmodel.collLod;
+
+            for (auto lodNumber = 0u; lodNumber < xmodel.numLods; lodNumber++)
+            {
+                JsonXModelLod lod;
+                lod.file = std::format("model_export/{}_lod{}{}", xmodel.name, lodNumber, GetExtensionForModelByConfig());
+                lod.distance = xmodel.lodInfo[lodNumber].dist;
+
+                jXModel.lods.emplace_back(std::move(lod));
+            }
+
+            if (xmodel.physPreset && xmodel.physPreset->name)
+                jXModel.physPreset = AssetName(xmodel.physPreset->name);
+
+            if (xmodel.physConstraints && xmodel.physConstraints->name)
+                jXModel.physConstraints = AssetName(xmodel.physConstraints->name);
+
+            jXModel.flags = xmodel.flags;
+
+#ifdef FEATURE_T6
+            jXModel.lightingOriginOffset.x = xmodel.lightingOriginOffset.x;
+            jXModel.lightingOriginOffset.y = xmodel.lightingOriginOffset.y;
+            jXModel.lightingOriginOffset.z = xmodel.lightingOriginOffset.z;
+            jXModel.lightingOriginRange = xmodel.lightingOriginRange;
+#endif
+        }
+
+        std::ostream& m_stream;
+    };
+
+    void DumpXModelJson(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
+    {
+        const auto assetFile = context.OpenAssetFile(std::format("xmodel/{}.json", asset->m_name));
+        if (!assetFile)
+            return;
+
+        const JsonDumper dumper(context, *assetFile);
+        dumper.Dump(asset->Asset());
+    }
+
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
+    {
+        DumpXModelJson(context, asset);
+        DumpXModelSurfs(context, asset);
+    }
+} // namespace GAME
diff --git a/src/ObjWriting/XModel/XModelDumper.h.template b/src/ObjWriting/XModel/XModelDumper.h.template
new file mode 100644
index 00000000..9094c78f
--- /dev/null
+++ b/src/ObjWriting/XModel/XModelDumper.h.template
@@ -0,0 +1,15 @@
+#options GAME (T5, T6)
+
+#filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".h"
+
+#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
+
+#pragma once
+
+#include "Dumping/AssetDumpingContext.h"
+#include GAME_HEADER
+
+namespace GAME
+{
+    void DumpXModel(AssetDumpingContext& context, XAssetInfo<XModel>* asset);
+}

From 0eb321f8c80064ffa09da85a1560e95a2d4309b5 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sat, 14 Sep 2024 12:57:33 +0200
Subject: [PATCH 08/17] chore: make sure components using source templating
 depend on RawTemplater

---
 tools/scripts/source_templating.lua | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tools/scripts/source_templating.lua b/tools/scripts/source_templating.lua
index a64ee53f..a87d4d53 100644
--- a/tools/scripts/source_templating.lua
+++ b/tools/scripts/source_templating.lua
@@ -60,5 +60,7 @@ function useSourceTemplating(projectName)
         files {
             createdFiles
         }
+        
+        RawTemplater:use()
     end
 end

From d05c1730fa9e4f41810050982510f762c3cdde83 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sat, 14 Sep 2024 16:28:46 +0200
Subject: [PATCH 09/17] feat: dump and load xmodels for IW5 via template

---
 src/Common/Game/IW5/IW5_Assets.h              | 108 ++--
 src/ObjCommon/Game/IW5/XModel/JsonXModelIW5.h |  32 ++
 .../Game/IW5/XModel/XModelConstantsIW5.h      |  32 ++
 .../IW5/AssetLoaders/AssetLoaderWeapon.cpp    |   4 +-
 .../IW5/AssetLoaders/AssetLoaderXModel.cpp    |  35 +-
 .../Game/IW5/AssetLoaders/AssetLoaderXModel.h |   7 +-
 .../XModel/XModelLoader.cpp.template          | 169 ++++--
 src/ObjLoading/XModel/XModelLoader.h.template |   2 +-
 .../IW5/AssetDumpers/AssetDumperWeapon.cpp    |   4 +-
 .../IW5/AssetDumpers/AssetDumperXModel.cpp    | 501 +-----------------
 .../XModel/XModelDumper.cpp.template          |  74 ++-
 src/ObjWriting/XModel/XModelDumper.h.template |   2 +-
 src/ZoneCode/Game/IW5/XAssets/XModel.txt      |   2 +-
 src/ZoneCode/Game/IW5/XAssets/XModelSurfs.txt |  12 +-
 14 files changed, 362 insertions(+), 622 deletions(-)
 create mode 100644 src/ObjCommon/Game/IW5/XModel/JsonXModelIW5.h
 create mode 100644 src/ObjCommon/Game/IW5/XModel/XModelConstantsIW5.h

diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h
index 5c7e004c..129c9f2c 100644
--- a/src/Common/Game/IW5/IW5_Assets.h
+++ b/src/Common/Game/IW5/IW5_Assets.h
@@ -177,11 +177,50 @@ namespace IW5
         void* data;
     };
 
-    typedef float vec2_t[2];
-    typedef float vec3_t[3];
-    typedef float vec4_t[4];
+    union vec2_t
+    {
+        float v[2];
+
+        struct
+        {
+            float x;
+            float y;
+        };
+    };
+
+    union vec3_t
+    {
+        struct
+        {
+            float x;
+            float y;
+            float z;
+        };
+
+        float v[3];
+    };
+
+    union vec4_t
+    {
+        float v[4];
+
+        struct
+        {
+            float x;
+            float y;
+            float z;
+            float w;
+        };
+
+        struct
+        {
+            float r;
+            float g;
+            float b;
+            float a;
+        };
+    };
 
-    typedef tdef_align(16) uint16_t r_index16_t;
     typedef tdef_align(16) char raw_byte16;
     typedef tdef_align(16) float raw_float16;
     typedef tdef_align(128) unsigned int raw_uint128;
@@ -220,8 +259,8 @@ namespace IW5
 
     struct Bounds
     {
-        float midPoint[3];
-        float halfSize[3];
+        vec3_t midPoint;
+        vec3_t halfSize;
     };
 
     struct cplane_s
@@ -446,34 +485,15 @@ namespace IW5
         unsigned int packed;
     };
 
-    struct GfxQuantizedNoColorVertex
-    {
-        short xyz[3];
-        short binormalSign;
-        PackedUnitVec normal;
-        PackedUnitVec tangent;
-        PackedTexCoords texCoord;
-    };
-
     union GfxColor
     {
         unsigned int packed;
         unsigned char array[4];
     };
 
-    struct GfxQuantizedVertex
-    {
-        short xyz[3];
-        short binormalSign;
-        PackedUnitVec normal;
-        PackedUnitVec tangent;
-        PackedTexCoords texCoord;
-        GfxColor color;
-    };
-
     struct type_align(16) GfxPackedVertex
     {
-        float xyz[3];
+        vec3_t xyz;
         float binormalSign;
         GfxColor color;
         PackedTexCoords texCoord;
@@ -481,14 +501,6 @@ namespace IW5
         PackedUnitVec tangent;
     };
 
-    union GfxVertexUnion0
-    {
-        GfxQuantizedNoColorVertex* quantizedNoColorVerts0;
-        GfxQuantizedVertex* quantizedVerts0;
-        GfxPackedVertex* packedVerts0;
-        void* verts0;
-    };
-
     struct XSurfaceCollisionAabb
     {
         unsigned short mins[3];
@@ -526,6 +538,13 @@ namespace IW5
         XSurfaceCollisionTree* collisionTree;
     };
 
+    struct XSurfaceTri
+    {
+        uint16_t i[3];
+    };
+
+    typedef tdef_align(16) XSurfaceTri XSurfaceTri16;
+
     struct XSurface
     {
         unsigned char tileMode;
@@ -536,9 +555,9 @@ namespace IW5
         uint16_t baseTriIndex;
         uint16_t baseVertIndex;
         float quantizeScale;
-        r_index16_t (*triIndices)[3];
+        XSurfaceTri16* triIndices;
         XSurfaceVertexInfo vertInfo;
-        GfxVertexUnion0 verts0;
+        GfxPackedVertex* verts0;
         unsigned int vertListCount;
         XRigidVertList* vertList;
         int partBits[6];
@@ -554,8 +573,8 @@ namespace IW5
 
     struct DObjAnimMat
     {
-        float quat[4];
-        float trans[3];
+        vec4_t quat;
+        vec3_t trans;
         float transWeight;
     };
 
@@ -567,7 +586,7 @@ namespace IW5
         XModelSurfs* modelSurfs;
         int partBits[6];
         XSurface* surfs;
-        char lod;
+        unsigned char lod;
         char smcBaseIndexPlusOne;
         char smcSubIndexMask;
         char smcBucket;
@@ -596,6 +615,11 @@ namespace IW5
         float radiusSquared;
     };
 
+    struct XModelQuat
+    {
+        int16_t v[4];
+    };
+
     struct XModel
     {
         const char* name;
@@ -606,15 +630,15 @@ namespace IW5
         unsigned int noScalePartBits[6];
         ScriptString* boneNames;
         unsigned char* parentList;
-        short (*quats)[4];
-        float (*trans)[3];
+        XModelQuat* quats;
+        float* trans;
         unsigned char* partClassification;
         DObjAnimMat* baseMat;
         Material** materialHandles;
         XModelLodInfo lodInfo[4];
         char maxLoadedLod;
         unsigned char numLods;
-        unsigned char collLod;
+        char collLod;
         unsigned char flags;
         XModelCollSurf_s* collSurfs;
         int numCollSurfs;
diff --git a/src/ObjCommon/Game/IW5/XModel/JsonXModelIW5.h b/src/ObjCommon/Game/IW5/XModel/JsonXModelIW5.h
new file mode 100644
index 00000000..ce7424ff
--- /dev/null
+++ b/src/ObjCommon/Game/IW5/XModel/JsonXModelIW5.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "Json/JsonCommon.h"
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace IW5
+{
+    class JsonXModelLod
+    {
+    public:
+        std::string file;
+        float distance;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonXModelLod, file, distance);
+
+    class JsonXModel
+    {
+    public:
+        std::vector<JsonXModelLod> lods;
+        std::optional<int> collLod;
+        std::optional<std::string> physPreset;
+        std::optional<std::string> physCollmap;
+        uint8_t flags;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonXModel, lods, collLod, physPreset, physCollmap, flags);
+} // namespace IW5
diff --git a/src/ObjCommon/Game/IW5/XModel/XModelConstantsIW5.h b/src/ObjCommon/Game/IW5/XModel/XModelConstantsIW5.h
new file mode 100644
index 00000000..a0a129a3
--- /dev/null
+++ b/src/ObjCommon/Game/IW5/XModel/XModelConstantsIW5.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "Game/IW5/IW5.h"
+
+namespace IW5
+{
+    inline const char* HITLOC_NAMES[]{
+        // clang-format off
+        "none",
+        "helmet",
+        "head",
+        "neck",
+        "torso_upper",
+        "torso_lower",
+        "right_arm_upper",
+        "left_arm_upper",
+        "right_arm_lower",
+        "left_arm_lower",
+        "right_hand",
+        "left_hand",
+        "right_leg_upper",
+        "left_leg_upper",
+        "right_leg_lower",
+        "left_leg_lower",
+        "right_foot",
+        "left_foot",
+        "gun",
+        "shield",
+        // clang-format on
+    };
+    static_assert(std::extent_v<decltype(HITLOC_NAMES)> == HITLOC_COUNT);
+} // namespace IW5
diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderWeapon.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderWeapon.cpp
index 18b3007a..19713878 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderWeapon.cpp
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderWeapon.cpp
@@ -814,8 +814,8 @@ namespace
         for (auto i = 0u; i < originalGraphKnotCount; i++)
         {
             const auto& commonKnot = graph.knots[i];
-            originalGraphKnots[i][0] = static_cast<float>(commonKnot.x);
-            originalGraphKnots[i][1] = static_cast<float>(commonKnot.y);
+            originalGraphKnots[i].x = static_cast<float>(commonKnot.x);
+            originalGraphKnots[i].y = static_cast<float>(commonKnot.y);
         }
 
         graphKnots = originalGraphKnots;
diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
index 2822c257..c4689b39 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
@@ -1,17 +1,42 @@
 #include "AssetLoaderXModel.h"
 
 #include "Game/IW5/IW5.h"
-#include "ObjLoading.h"
+#include "Game/IW5/XModel/XModelLoaderIW5.h"
 #include "Pool/GlobalAssetPool.h"
 
 #include <cstring>
+#include <format>
+#include <iostream>
 
 using namespace IW5;
 
 void* AssetLoaderXModel::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
 {
-    auto* model = memory->Create<XModel>();
-    memset(model, 0, sizeof(XModel));
-    model->name = memory->Dup(assetName.c_str());
-    return model;
+    auto* asset = memory->Alloc<AssetXModel::Type>();
+    asset->name = memory->Dup(assetName.c_str());
+    return asset;
+}
+
+bool AssetLoaderXModel::CanLoadFromRaw() const
+{
+    return true;
+}
+
+bool AssetLoaderXModel::LoadFromRaw(
+    const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const
+{
+    const auto file = searchPath->Open(std::format("xmodel/{}.json", assetName));
+    if (!file.IsOpen())
+        return false;
+
+    auto* xmodel = memory->Alloc<XModel>();
+    xmodel->name = memory->Dup(assetName.c_str());
+
+    std::vector<XAssetInfoGeneric*> dependencies;
+    if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
+        manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
+    else
+        std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
+
+    return true;
 }
diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.h b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.h
index 1ea4565d..3bee159f 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.h
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.h
@@ -1,6 +1,6 @@
 #pragma once
-
 #include "AssetLoading/BasicAssetLoader.h"
+#include "AssetLoading/IAssetLoadingManager.h"
 #include "Game/IW5/IW5.h"
 #include "SearchPath/ISearchPath.h"
 
@@ -8,7 +8,12 @@ namespace IW5
 {
     class AssetLoaderXModel final : public BasicAssetLoader<AssetXModel>
     {
+        static std::string GetFileNameForAsset(const std::string& assetName);
+
     public:
         _NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override;
+        _NODISCARD bool CanLoadFromRaw() const override;
+        bool
+            LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override;
     };
 } // namespace IW5
diff --git a/src/ObjLoading/XModel/XModelLoader.cpp.template b/src/ObjLoading/XModel/XModelLoader.cpp.template
index 5cdd8575..100acd52 100644
--- a/src/ObjLoading/XModel/XModelLoader.cpp.template
+++ b/src/ObjLoading/XModel/XModelLoader.cpp.template
@@ -1,4 +1,4 @@
-#options GAME (T5, T6)
+#options GAME(IW5, T5, T6)
 
 #filename "Game/" + GAME + "/XModel/XModelLoader" + GAME + ".cpp"
 
@@ -7,7 +7,9 @@
 #set CONSTANTS_HEADER "\"Game/" + GAME + "/XModel/XModelConstants" + GAME + ".h\""
 #set JSON_HEADER "\"Game/" + GAME + "/XModel/JsonXModel" + GAME + ".h\""
 
-#if GAME == "T5"
+#if GAME == "IW5"
+#define FEATURE_IW5
+#elif GAME == "T5"
 #define FEATURE_T5
 #elif GAME == "T6"
 #define FEATURE_T6
@@ -173,15 +175,24 @@ namespace GAME
             if (common.m_bone_weight_data.weights.empty())
                 return;
 
-            info.bounds[0].x = 0.0f;
-            info.bounds[0].y = 0.0f;
-            info.bounds[0].z = 0.0f;
-            info.bounds[1].x = 0.0f;
-            info.bounds[1].y = 0.0f;
-            info.bounds[1].z = 0.0f;
-            info.offset.x = 0.0f;
-            info.offset.y = 0.0f;
-            info.offset.z = 0.0f;
+#ifdef FEATURE_IW5
+            vec3_t minCoordinate, maxCoordinate;
+            auto& offset = info.bounds.midPoint;
+#else
+            auto& offset = info.offset;
+            auto& minCoordinate = info.bounds[0];
+            auto& maxCoordinate = info.bounds[1];
+#endif
+
+            minCoordinate.x = 0.0f;
+            minCoordinate.y = 0.0f;
+            minCoordinate.z = 0.0f;
+            maxCoordinate.x = 0.0f;
+            maxCoordinate.y = 0.0f;
+            maxCoordinate.z = 0.0f;
+            offset.x = 0.0f;
+            offset.y = 0.0f;
+            offset.z = 0.0f;
             info.radiusSquared = 0.0f;
 
             const auto vertexCount = common.m_vertex_bone_weights.size();
@@ -196,22 +207,31 @@ namespace GAME
                     if (weight.boneIndex != boneIndex)
                         continue;
 
-                    info.bounds[0].x = std::min(info.bounds[0].x, vertex.coordinates[0]);
-                    info.bounds[0].y = std::min(info.bounds[0].y, vertex.coordinates[1]);
-                    info.bounds[0].z = std::min(info.bounds[0].z, vertex.coordinates[2]);
-                    info.bounds[1].x = std::max(info.bounds[1].x, vertex.coordinates[0]);
-                    info.bounds[1].y = std::max(info.bounds[1].y, vertex.coordinates[1]);
-                    info.bounds[1].z = std::max(info.bounds[1].z, vertex.coordinates[2]);
+                    minCoordinate.x = std::min(minCoordinate.x, vertex.coordinates[0]);
+                    minCoordinate.y = std::min(minCoordinate.y, vertex.coordinates[1]);
+                    minCoordinate.z = std::min(minCoordinate.z, vertex.coordinates[2]);
+                    maxCoordinate.x = std::max(maxCoordinate.x, vertex.coordinates[0]);
+                    maxCoordinate.y = std::max(maxCoordinate.y, vertex.coordinates[1]);
+                    maxCoordinate.z = std::max(maxCoordinate.z, vertex.coordinates[2]);
                 }
             }
 
-            const Eigen::Vector3f minEigen(info.bounds[0].x, info.bounds[0].y, info.bounds[0].z);
-            const Eigen::Vector3f maxEigen(info.bounds[1].x, info.bounds[1].y, info.bounds[1].z);
+            const Eigen::Vector3f minEigen(minCoordinate.x, minCoordinate.y, minCoordinate.z);
+            const Eigen::Vector3f maxEigen(maxCoordinate.x, maxCoordinate.y, maxCoordinate.z);
             const Eigen::Vector3f boundsCenter = (minEigen + maxEigen) * 0.5f;
-            info.offset.x = boundsCenter.x();
-            info.offset.y = boundsCenter.y();
-            info.offset.z = boundsCenter.z();
-            info.radiusSquared = Eigen::Vector3f(maxEigen - boundsCenter).squaredNorm();
+            const Eigen::Vector3f halfSizeEigen = maxEigen - boundsCenter;
+#ifdef FEATURE_IW5
+
+            info.bounds.halfSize.x = halfSizeEigen.x();
+            info.bounds.halfSize.y = halfSizeEigen.y();
+            info.bounds.halfSize.z = halfSizeEigen.z();
+#endif
+
+            offset.x = boundsCenter.x();
+            offset.y = boundsCenter.y();
+            offset.z = boundsCenter.z();
+
+            info.radiusSquared = halfSizeEigen.squaredNorm();
         }
 
         bool ApplyCommonBonesToXModel(const JsonXModelLod& jLod, XModel& xmodel, unsigned lodNumber, const XModelCommon& common) const
@@ -272,8 +292,10 @@ namespace GAME
                 ApplyBasePose(xmodel.baseMat[boneIndex], bone);
                 CalculateBoneBounds(xmodel.boneInfo[boneIndex], boneIndex, common);
 
+#if defined(FEATURE_T5) || defined(FEATURE_T6)
                 // Other boneInfo data is filled when calculating bone bounds
                 xmodel.boneInfo[boneIndex].collmap = -1;
+#endif
 
                 if (xmodel.numRootBones <= boneIndex)
                 {
@@ -678,17 +700,52 @@ namespace GAME
                     lodInfo.partBits[i] |= surface.partBits[i];
             }
 
+#ifdef FEATURE_IW5
+            auto* modelSurfs = m_memory.Alloc<XModelSurfs>();
+            const auto modelSurfsName = std::format("{}_lod{}", xmodel.name, lodNumber);
+            modelSurfs->name = m_memory.Dup(modelSurfsName.c_str());
+
+            static_assert(std::extent_v<decltype(XModelLodInfo::partBits)> == std::extent_v<decltype(XModelSurfs::partBits)>);
+            memcpy(modelSurfs->partBits, lodInfo.partBits, sizeof(XModelLodInfo::partBits));
+
+            modelSurfs->numsurfs = lodInfo.numsurfs;
+            modelSurfs->surfs = m_memory.Alloc<XSurface>(modelSurfs->numsurfs);
+            memcpy(modelSurfs->surfs, &m_surfaces[lodInfo.surfIndex], sizeof(XSurface) * modelSurfs->numsurfs);
+
+            m_manager.AddAsset<AssetXModelSurfs>(modelSurfsName, modelSurfs);
+
+            lodInfo.modelSurfs = modelSurfs;
+            lodInfo.surfs = modelSurfs->surfs;
+
+            lodInfo.lod = static_cast<decltype(XModelLodInfo::lod)>(lodNumber);
+#endif
+
             return true;
         }
 
         static void CalculateModelBounds(XModel& xmodel)
         {
+#ifdef FEATURE_IW5
+            if (!xmodel.lodInfo[0].modelSurfs || !xmodel.lodInfo[0].modelSurfs->surfs)
+                return;
+
+            const auto* surfs = xmodel.lodInfo[0].modelSurfs->surfs;
+            vec3_t minCoordinate, maxCoordinate;
+#else
             if (!xmodel.surfs)
                 return;
 
+            auto& minCoordinate = xmodel.mins;
+            auto& maxCoordinate = xmodel.maxs;
+#endif
+
             for (auto surfaceIndex = 0u; surfaceIndex < xmodel.lodInfo[0].numsurfs; surfaceIndex++)
             {
+#ifdef FEATURE_IW5
+                const auto& surface = surfs[surfaceIndex];
+#else
                 const auto& surface = xmodel.surfs[surfaceIndex + xmodel.lodInfo[0].surfIndex];
+#endif
 
                 if (!surface.verts0)
                     continue;
@@ -697,19 +754,35 @@ namespace GAME
                 {
                     const auto& vertex = surface.verts0[vertIndex];
 
-                    xmodel.mins.x = std::min(xmodel.mins.x, vertex.xyz.v[0]);
-                    xmodel.mins.y = std::min(xmodel.mins.y, vertex.xyz.v[1]);
-                    xmodel.mins.z = std::min(xmodel.mins.z, vertex.xyz.v[2]);
-                    xmodel.maxs.x = std::max(xmodel.maxs.x, vertex.xyz.v[0]);
-                    xmodel.maxs.y = std::max(xmodel.maxs.y, vertex.xyz.v[1]);
-                    xmodel.maxs.z = std::max(xmodel.maxs.z, vertex.xyz.v[2]);
+                    minCoordinate.x = std::min(minCoordinate.x, vertex.xyz.v[0]);
+                    minCoordinate.y = std::min(minCoordinate.y, vertex.xyz.v[1]);
+                    minCoordinate.z = std::min(minCoordinate.z, vertex.xyz.v[2]);
+                    maxCoordinate.x = std::max(maxCoordinate.x, vertex.xyz.v[0]);
+                    maxCoordinate.y = std::max(maxCoordinate.y, vertex.xyz.v[1]);
+                    maxCoordinate.z = std::max(maxCoordinate.z, vertex.xyz.v[2]);
                 }
             }
 
-            const auto maxX = std::max(std::abs(xmodel.mins.x), std::abs(xmodel.maxs.x));
-            const auto maxY = std::max(std::abs(xmodel.mins.y), std::abs(xmodel.maxs.y));
-            const auto maxZ = std::max(std::abs(xmodel.mins.z), std::abs(xmodel.maxs.z));
+#ifdef FEATURE_IW5
+            const Eigen::Vector3f minEigen(minCoordinate.x, minCoordinate.y, minCoordinate.z);
+            const Eigen::Vector3f maxEigen(maxCoordinate.x, maxCoordinate.y, maxCoordinate.z);
+            const Eigen::Vector3f boundsCenter = (minEigen + maxEigen) * 0.5f;
+            const Eigen::Vector3f halfSizeEigen = maxEigen - boundsCenter;
+
+            xmodel.bounds.halfSize.x = halfSizeEigen.x();
+            xmodel.bounds.halfSize.y = halfSizeEigen.y();
+            xmodel.bounds.halfSize.z = halfSizeEigen.z();
+
+            xmodel.bounds.midPoint.x = boundsCenter.x();
+            xmodel.bounds.midPoint.y = boundsCenter.y();
+            xmodel.bounds.midPoint.z = boundsCenter.z();
+            xmodel.radius = halfSizeEigen.norm();
+#else
+            const auto maxX = std::max(std::abs(minCoordinate.x), std::abs(maxCoordinate.x));
+            const auto maxY = std::max(std::abs(minCoordinate.y), std::abs(maxCoordinate.y));
+            const auto maxZ = std::max(std::abs(minCoordinate.z), std::abs(maxCoordinate.z));
             xmodel.radius = Eigen::Vector3f(maxX, maxY, maxZ).norm();
+#endif
         }
 
         bool CreateXModelFromJson(const JsonXModel& jXModel, XModel& xmodel)
@@ -722,7 +795,7 @@ namespace GAME
             }
 
             auto lodNumber = 0u;
-            xmodel.numLods = static_cast<uint16_t>(jXModel.lods.size());
+            xmodel.numLods = static_cast<decltype(XModel::numLods)>(jXModel.lods.size());
             for (const auto& jLod : jXModel.lods)
             {
                 if (!LoadLod(jLod, xmodel, lodNumber++))
@@ -736,10 +809,14 @@ namespace GAME
                 return false;
             }
 
-            xmodel.numsurfs = static_cast<unsigned char>(m_surfaces.size());
+            xmodel.numsurfs = static_cast<decltype(XModel::numsurfs)>(m_surfaces.size());
+
+#if defined(FEATURE_T5) || defined(FEATURE_T6)
             xmodel.surfs = m_memory.Alloc<XSurface>(xmodel.numsurfs);
-            xmodel.materialHandles = m_memory.Alloc<Material*>(xmodel.numsurfs);
             memcpy(xmodel.surfs, m_surfaces.data(), sizeof(XSurface) * xmodel.numsurfs);
+#endif
+
+            xmodel.materialHandles = m_memory.Alloc<Material*>(xmodel.numsurfs);
             memcpy(xmodel.materialHandles, m_materials.data(), sizeof(Material*) * xmodel.numsurfs);
 
             CalculateModelBounds(xmodel);
@@ -751,7 +828,7 @@ namespace GAME
                     PrintError(xmodel, "Collision lod is not a valid lod");
                     return false;
                 }
-                xmodel.collLod = static_cast<int16_t>(jXModel.collLod.value());
+                xmodel.collLod = static_cast<decltype(XModel::collLod)>(jXModel.collLod.value());
             }
             else
                 xmodel.collLod = -1;
@@ -772,6 +849,25 @@ namespace GAME
                 xmodel.physPreset = nullptr;
             }
 
+#if defined(FEATURE_IW5)
+            if (jXModel.physCollmap)
+            {
+                auto* physCollmap = m_manager.LoadDependency<AssetPhysCollMap>(jXModel.physCollmap.value());
+                if (!physCollmap)
+                {
+                    PrintError(xmodel, "Could not find phys collmap");
+                    return false;
+                }
+                m_dependencies.emplace(physCollmap);
+                xmodel.physCollmap = physCollmap->Asset();
+            }
+            else
+            {
+                xmodel.physCollmap = nullptr;
+            }
+#endif
+
+#if defined(FEATURE_T5) || defined(FEATURE_T6)
             if (jXModel.physConstraints)
             {
                 auto* physConstraints = m_manager.LoadDependency<AssetPhysConstraints>(jXModel.physConstraints.value());
@@ -787,6 +883,7 @@ namespace GAME
             {
                 xmodel.physConstraints = nullptr;
             }
+#endif
 
             xmodel.flags = jXModel.flags;
 
diff --git a/src/ObjLoading/XModel/XModelLoader.h.template b/src/ObjLoading/XModel/XModelLoader.h.template
index 6aeb6877..3b9d0729 100644
--- a/src/ObjLoading/XModel/XModelLoader.h.template
+++ b/src/ObjLoading/XModel/XModelLoader.h.template
@@ -1,4 +1,4 @@
-#options GAME (T5, T6)
+#options GAME (IW5, T5, T6)
 
 #filename "Game/" + GAME + "/XModel/XModelLoader" + GAME + ".h"
 
diff --git a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperWeapon.cpp b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperWeapon.cpp
index dd1bd9d2..15d5fd5b 100644
--- a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperWeapon.cpp
+++ b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperWeapon.cpp
@@ -545,8 +545,8 @@ namespace IW5
         for (auto i = 0u; i < originalKnotCount; i++)
         {
             auto& knot = graph.knots[i];
-            knot.x = originalKnots[i][0];
-            knot.y = originalKnots[i][1];
+            knot.x = originalKnots[i].x;
+            knot.y = originalKnots[i].y;
         }
 
         return graph;
diff --git a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperXModel.cpp b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperXModel.cpp
index 6a0af227..bd882e41 100644
--- a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperXModel.cpp
+++ b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperXModel.cpp
@@ -1,506 +1,9 @@
 #include "AssetDumperXModel.h"
 
-#include "Game/IW5/CommonIW5.h"
-#include "ObjWriting.h"
-#include "Utils/DistinctMapper.h"
-#include "Utils/HalfFloat.h"
-#include "Utils/QuatInt16.h"
-#include "XModel/Export/XModelExportWriter.h"
-#include "XModel/Gltf/GltfBinOutput.h"
-#include "XModel/Gltf/GltfTextOutput.h"
-#include "XModel/Gltf/GltfWriter.h"
-#include "XModel/Obj/ObjWriter.h"
-#include "XModel/XModelWriter.h"
-
-#include <cassert>
-#include <format>
+#include "Game/IW5/XModel/XModelDumperIW5.h"
 
 using namespace IW5;
 
-namespace
-{
-    GfxImage* GetMaterialColorMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_COLOR_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 'c' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    GfxImage* GetMaterialNormalMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_NORMAL_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 'n' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    GfxImage* GetMaterialSpecularMap(const Material* material)
-    {
-        std::vector<MaterialTextureDef*> potentialTextureDefs;
-
-        for (auto textureIndex = 0u; textureIndex < material->textureCount; textureIndex++)
-        {
-            MaterialTextureDef* def = &material->textureTable[textureIndex];
-
-            if (def->semantic == TS_SPECULAR_MAP)
-                potentialTextureDefs.push_back(def);
-        }
-
-        if (potentialTextureDefs.empty())
-            return nullptr;
-        if (potentialTextureDefs.size() == 1)
-            return potentialTextureDefs[0]->u.image;
-
-        for (const auto* def : potentialTextureDefs)
-        {
-            if (def->nameStart == 's' && def->nameEnd == 'p')
-                return def->u.image;
-        }
-
-        return potentialTextureDefs[0]->u.image;
-    }
-
-    void AddXModelBones(XModelCommon& out, const AssetDumpingContext& context, const XModel* model)
-    {
-        for (auto boneNum = 0u; boneNum < model->numBones; boneNum++)
-        {
-            XModelBone bone;
-            if (model->boneNames[boneNum] < context.m_zone->m_script_strings.Count())
-                bone.name = context.m_zone->m_script_strings[model->boneNames[boneNum]];
-            else
-                bone.name = "INVALID_BONE_NAME";
-
-            if (boneNum >= model->numRootBones)
-                bone.parentIndex = boneNum - static_cast<unsigned int>(model->parentList[boneNum - model->numRootBones]);
-            else
-                bone.parentIndex = std::nullopt;
-
-            bone.scale[0] = 1.0f;
-            bone.scale[1] = 1.0f;
-            bone.scale[2] = 1.0f;
-
-            bone.globalOffset[0] = model->baseMat[boneNum].trans[0];
-            bone.globalOffset[1] = model->baseMat[boneNum].trans[1];
-            bone.globalOffset[2] = model->baseMat[boneNum].trans[2];
-            bone.globalRotation = {
-                model->baseMat[boneNum].quat[0],
-                model->baseMat[boneNum].quat[1],
-                model->baseMat[boneNum].quat[2],
-                model->baseMat[boneNum].quat[3],
-            };
-
-            if (boneNum < model->numRootBones)
-            {
-                bone.localOffset[0] = 0;
-                bone.localOffset[1] = 0;
-                bone.localOffset[2] = 0;
-                bone.localRotation = {0, 0, 0, 1};
-            }
-            else
-            {
-                bone.localOffset[0] = model->trans[boneNum - model->numRootBones][0];
-                bone.localOffset[1] = model->trans[boneNum - model->numRootBones][1];
-                bone.localOffset[2] = model->trans[boneNum - model->numRootBones][2];
-                bone.localRotation = {
-                    QuatInt16::ToFloat(model->quats[boneNum - model->numRootBones][0]),
-                    QuatInt16::ToFloat(model->quats[boneNum - model->numRootBones][1]),
-                    QuatInt16::ToFloat(model->quats[boneNum - model->numRootBones][2]),
-                    QuatInt16::ToFloat(model->quats[boneNum - model->numRootBones][3]),
-                };
-            }
-
-            out.m_bones.emplace_back(std::move(bone));
-        }
-    }
-
-    const char* AssetName(const char* input)
-    {
-        if (input && input[0] == ',')
-            return &input[1];
-
-        return input;
-    }
-
-    void AddXModelMaterials(XModelCommon& out, DistinctMapper<Material*>& materialMapper, const XModel* model)
-    {
-        for (auto surfaceMaterialNum = 0; surfaceMaterialNum < model->numsurfs; surfaceMaterialNum++)
-        {
-            Material* material = model->materialHandles[surfaceMaterialNum];
-            if (materialMapper.Add(material))
-            {
-                XModelMaterial xMaterial;
-                xMaterial.ApplyDefaults();
-
-                xMaterial.name = AssetName(material->info.name);
-                const auto* colorMap = GetMaterialColorMap(material);
-                if (colorMap)
-                    xMaterial.colorMapName = AssetName(colorMap->name);
-
-                const auto* normalMap = GetMaterialNormalMap(material);
-                if (normalMap)
-                    xMaterial.normalMapName = AssetName(normalMap->name);
-
-                const auto* specularMap = GetMaterialSpecularMap(material);
-                if (specularMap)
-                    xMaterial.specularMapName = AssetName(specularMap->name);
-
-                out.m_materials.emplace_back(std::move(xMaterial));
-            }
-        }
-    }
-
-    void AddXModelObjects(XModelCommon& out, const XModelSurfs* modelSurfs, const DistinctMapper<Material*>& materialMapper, const int baseSurfaceIndex)
-    {
-        for (auto surfIndex = 0u; surfIndex < modelSurfs->numsurfs; surfIndex++)
-        {
-            XModelObject object;
-            object.name = std::format("surf{}", surfIndex);
-            object.materialIndex = static_cast<int>(materialMapper.GetDistinctPositionByInputPosition(surfIndex + baseSurfaceIndex));
-
-            out.m_objects.emplace_back(std::move(object));
-        }
-    }
-
-    void AddXModelVertices(XModelCommon& out, const XModelSurfs* modelSurfs)
-    {
-        for (auto surfIndex = 0u; surfIndex < modelSurfs->numsurfs; surfIndex++)
-        {
-            const auto& surface = modelSurfs->surfs[surfIndex];
-
-            for (auto vertexIndex = 0u; vertexIndex < surface.vertCount; vertexIndex++)
-            {
-                const auto& v = surface.verts0.packedVerts0[vertexIndex];
-
-                XModelVertex vertex{};
-                vertex.coordinates[0] = v.xyz[0];
-                vertex.coordinates[1] = v.xyz[1];
-                vertex.coordinates[2] = v.xyz[2];
-                Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
-                Common::Vec4UnpackGfxColor(v.color, vertex.color);
-                Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
-
-                out.m_vertices.emplace_back(vertex);
-            }
-        }
-    }
-
-    void AllocateXModelBoneWeights(const XModelSurfs* modelSurfs, XModelVertexBoneWeightCollection& weightCollection)
-    {
-        auto totalWeightCount = 0u;
-        for (auto surfIndex = 0u; surfIndex < modelSurfs->numsurfs; surfIndex++)
-        {
-            const auto& surface = modelSurfs->surfs[surfIndex];
-
-            if (surface.vertList)
-            {
-                totalWeightCount += surface.vertListCount;
-            }
-
-            if (surface.vertInfo.vertsBlend)
-            {
-                totalWeightCount += surface.vertInfo.vertCount[0] * 1;
-                totalWeightCount += surface.vertInfo.vertCount[1] * 2;
-                totalWeightCount += surface.vertInfo.vertCount[2] * 3;
-                totalWeightCount += surface.vertInfo.vertCount[3] * 4;
-            }
-        }
-
-        weightCollection.weights.resize(totalWeightCount);
-    }
-
-    float BoneWeight16(const uint16_t value)
-    {
-        return static_cast<float>(value) / static_cast<float>(std::numeric_limits<uint16_t>::max());
-    }
-
-    void AddXModelVertexBoneWeights(XModelCommon& out, const XModelSurfs* modelSurfs)
-    {
-        auto& weightCollection = out.m_bone_weight_data;
-        size_t weightOffset = 0u;
-
-        for (auto surfIndex = 0u; surfIndex < modelSurfs->numsurfs; surfIndex++)
-        {
-            const auto& surface = modelSurfs->surfs[surfIndex];
-            auto handledVertices = 0u;
-
-            if (surface.vertList)
-            {
-                for (auto vertListIndex = 0u; vertListIndex < surface.vertListCount; vertListIndex++)
-                {
-                    const auto& vertList = surface.vertList[vertListIndex];
-                    const auto boneWeightOffset = weightOffset;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{vertList.boneOffset / sizeof(DObjSkelMat), 1.0f};
-
-                    for (auto vertListVertexOffset = 0u; vertListVertexOffset < vertList.vertCount; vertListVertexOffset++)
-                    {
-                        out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                    }
-                    handledVertices += vertList.vertCount;
-                }
-            }
-
-            auto vertsBlendOffset = 0u;
-            if (surface.vertInfo.vertsBlend)
-            {
-                // 1 bone weight
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[0]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, 1.0f};
-
-                    vertsBlendOffset += 1;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 1);
-                }
-
-                // 2 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[1]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneWeight0 = 1.0f - boneWeight1;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-
-                    vertsBlendOffset += 3;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 2);
-                }
-
-                // 3 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[2]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-
-                    vertsBlendOffset += 5;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 3);
-                }
-
-                // 4 bone weights
-                for (auto vertIndex = 0; vertIndex < surface.vertInfo.vertCount[3]; vertIndex++)
-                {
-                    const auto boneWeightOffset = weightOffset;
-                    const auto boneIndex0 = surface.vertInfo.vertsBlend[vertsBlendOffset + 0] / sizeof(DObjSkelMat);
-                    const auto boneIndex1 = surface.vertInfo.vertsBlend[vertsBlendOffset + 1] / sizeof(DObjSkelMat);
-                    const auto boneWeight1 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 2]);
-                    const auto boneIndex2 = surface.vertInfo.vertsBlend[vertsBlendOffset + 3] / sizeof(DObjSkelMat);
-                    const auto boneWeight2 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 4]);
-                    const auto boneIndex3 = surface.vertInfo.vertsBlend[vertsBlendOffset + 5] / sizeof(DObjSkelMat);
-                    const auto boneWeight3 = BoneWeight16(surface.vertInfo.vertsBlend[vertsBlendOffset + 6]);
-                    const auto boneWeight0 = 1.0f - boneWeight1 - boneWeight2 - boneWeight3;
-
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex0, boneWeight0};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex1, boneWeight1};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex2, boneWeight2};
-                    weightCollection.weights[weightOffset++] = XModelBoneWeight{boneIndex3, boneWeight3};
-
-                    vertsBlendOffset += 7;
-
-                    out.m_vertex_bone_weights.emplace_back(boneWeightOffset, 4);
-                }
-
-                handledVertices +=
-                    surface.vertInfo.vertCount[0] + surface.vertInfo.vertCount[1] + surface.vertInfo.vertCount[2] + surface.vertInfo.vertCount[3];
-            }
-
-            for (; handledVertices < surface.vertCount; handledVertices++)
-            {
-                out.m_vertex_bone_weights.emplace_back(0, 0);
-            }
-        }
-    }
-
-    void AddXModelFaces(XModelCommon& out, const XModelSurfs* modelSurfs)
-    {
-        for (auto surfIndex = 0u; surfIndex < modelSurfs->numsurfs; surfIndex++)
-        {
-            const auto& surface = modelSurfs->surfs[surfIndex];
-            auto& object = out.m_objects[surfIndex];
-            object.m_faces.reserve(surface.triCount);
-
-            for (auto triIndex = 0u; triIndex < surface.triCount; triIndex++)
-            {
-                const auto& tri = surface.triIndices[triIndex];
-
-                XModelFace face{};
-                face.vertexIndex[0] = tri[0] + surface.baseVertIndex;
-                face.vertexIndex[1] = tri[1] + surface.baseVertIndex;
-                face.vertexIndex[2] = tri[2] + surface.baseVertIndex;
-                object.m_faces.emplace_back(face);
-            }
-        }
-    }
-
-    void PopulateXModelWriter(XModelCommon& out, const AssetDumpingContext& context, const unsigned lod, const XModel* model)
-    {
-        const auto* modelSurfs = model->lodInfo[lod].modelSurfs;
-
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-        AllocateXModelBoneWeights(modelSurfs, out.m_bone_weight_data);
-
-        out.m_name = modelSurfs->name;
-        AddXModelBones(out, context, model);
-        AddXModelMaterials(out, materialMapper, model);
-        AddXModelObjects(out, modelSurfs, materialMapper, model->lodInfo[lod].surfIndex);
-        AddXModelVertices(out, modelSurfs);
-        AddXModelVertexBoneWeights(out, modelSurfs);
-        AddXModelFaces(out, modelSurfs);
-    }
-
-    void DumpObjMtl(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-        const auto mtlFile = context.OpenAssetFile(std::format("model_export/{}.mtl", model->name));
-
-        if (!mtlFile)
-            return;
-
-        const auto writer = obj::CreateMtlWriter(*mtlFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpObjLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto* modelSurfs = model->lodInfo[lod].modelSurfs;
-
-        if (modelSurfs->name[0] == ',' || modelSurfs->surfs == nullptr)
-            return;
-
-        const auto assetFile = context.OpenAssetFile(std::format("model_export/{}.obj", modelSurfs->name));
-
-        if (!assetFile)
-            return;
-
-        const auto writer =
-            obj::CreateObjWriter(*assetFile, std::format("{}.mtl", model->name), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        DistinctMapper<Material*> materialMapper(model->numsurfs);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelExportLod(const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod)
-    {
-        const auto* model = asset->Asset();
-        const auto* modelSurfs = model->lodInfo[lod].modelSurfs;
-        const auto assetFile = context.OpenAssetFile(std::format("model_export/{}.XMODEL_EXPORT", modelSurfs->name));
-
-        if (!assetFile)
-            return;
-
-        const auto writer = xmodel_export::CreateWriterForVersion6(*assetFile, context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-        writer->Write(common);
-    }
-
-    template<typename T>
-    void DumpGltfLod(
-        const XModelCommon& common, const AssetDumpingContext& context, const XAssetInfo<XModel>* asset, const unsigned lod, const std::string& extension)
-    {
-        const auto* model = asset->Asset();
-        const auto* modelSurfs = model->lodInfo[lod].modelSurfs;
-        const auto assetFile = context.OpenAssetFile(std::format("model_export/{}{}", modelSurfs->name, extension));
-
-        if (!assetFile)
-            return;
-
-        const auto output = std::make_unique<T>(*assetFile);
-        const auto writer = gltf::Writer::CreateWriter(output.get(), context.m_zone->m_game->GetShortName(), context.m_zone->m_name);
-
-        writer->Write(common);
-    }
-
-    void DumpXModelSurfs(const AssetDumpingContext& context, const XAssetInfo<XModel>* asset)
-    {
-        const auto* model = asset->Asset();
-
-        for (auto currentLod = 0u; currentLod < model->numLods; currentLod++)
-        {
-            XModelCommon common;
-            PopulateXModelWriter(common, context, currentLod, asset->Asset());
-
-            switch (ObjWriting::Configuration.ModelOutputFormat)
-            {
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ:
-                DumpObjLod(common, context, asset, currentLod);
-                if (currentLod == 0u)
-                    DumpObjMtl(common, context, asset);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT:
-                DumpXModelExportLod(common, context, asset, currentLod);
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF:
-                DumpGltfLod<gltf::TextOutput>(common, context, asset, currentLod, ".gltf");
-                break;
-
-            case ObjWriting::Configuration_t::ModelOutputFormat_e::GLB:
-                DumpGltfLod<gltf::BinOutput>(common, context, asset, currentLod, ".glb");
-                break;
-
-            default:
-                assert(false);
-                break;
-            }
-        }
-    }
-} // namespace
-
 bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 {
     return !asset->m_name.empty() && asset->m_name[0] != ',';
@@ -508,5 +11,5 @@ bool AssetDumperXModel::ShouldDump(XAssetInfo<XModel>* asset)
 
 void AssetDumperXModel::DumpAsset(AssetDumpingContext& context, XAssetInfo<XModel>* asset)
 {
-    DumpXModelSurfs(context, asset);
+    DumpXModel(context, asset);
 }
diff --git a/src/ObjWriting/XModel/XModelDumper.cpp.template b/src/ObjWriting/XModel/XModelDumper.cpp.template
index 0a775f19..f3644849 100644
--- a/src/ObjWriting/XModel/XModelDumper.cpp.template
+++ b/src/ObjWriting/XModel/XModelDumper.cpp.template
@@ -1,4 +1,4 @@
-#options GAME (T5, T6)
+#options GAME (IW5, T5, T6)
 
 #filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".cpp"
 
@@ -6,7 +6,9 @@
 #set COMMON_HEADER "\"Game/" + GAME + "/Common" + GAME + ".h\""
 #set JSON_HEADER "\"Game/" + GAME + "/XModel/JsonXModel" + GAME + ".h\""
 
-#if GAME == "T5"
+#if GAME == "IW5"
+#define FEATURE_IW5
+#elif GAME == "T5"
 #define FEATURE_T5
 #elif GAME == "T6"
 #define FEATURE_T6
@@ -54,8 +56,13 @@ namespace GAME
         {
             MaterialTextureDef* def = &material->textureTable[textureIndex];
 
+#ifdef FEATURE_IW5            
+            if (def->semantic == TS_COLOR_MAP)
+                potentialTextureDefs.push_back(def);
+#else
             if (def->semantic == TS_COLOR_MAP || def->semantic >= TS_COLOR0_MAP && def->semantic <= TS_COLOR15_MAP)
                 potentialTextureDefs.push_back(def);
+#endif
         }
 
         if (potentialTextureDefs.empty())
@@ -136,15 +143,33 @@ namespace GAME
         return GetImageFromTextureDef(*potentialTextureDefs[0]);
     }
 
+    bool GetSurfaces(const XModel* model, const unsigned lod, XSurface*& surfs, unsigned& surfCount)
+    {
+#ifdef FEATURE_IW5
+        if (!model->lodInfo[lod].modelSurfs || !model->lodInfo[lod].modelSurfs->surfs)
+            return false;
+
+        surfs = model->lodInfo[lod].modelSurfs->surfs;
+        surfCount = model->lodInfo[lod].modelSurfs->numsurfs;
+#else
+        if (!model->surfs)
+            return false;
+
+        surfs = &model->surfs[model->lodInfo[lod].surfIndex];
+        surfCount = model->lodInfo[lod].numsurfs;
+#endif
+
+        return true;
+    }
+
     bool HasDefaultArmature(const XModel* model, const unsigned lod)
     {
         if (model->numRootBones != 1 || model->numBones != 1)
             return false;
 
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
+        XSurface* surfs;
+        unsigned surfCount;
+        if (!GetSurfaces(model, lod, surfs, surfCount))
             return true;
 
         for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
@@ -284,10 +309,9 @@ namespace GAME
 
     void AddXModelVertices(XModelCommon& out, const XModel* model, const unsigned lod)
     {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
+        XSurface* surfs;
+        unsigned surfCount;
+        if (!GetSurfaces(model, lod, surfs, surfCount))
             return;
 
         for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
@@ -313,10 +337,9 @@ namespace GAME
 
     void AllocateXModelBoneWeights(const XModel* model, const unsigned lod, XModelVertexBoneWeightCollection& weightCollection)
     {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
+        XSurface* surfs;
+        unsigned surfCount;
+        if (!GetSurfaces(model, lod, surfs, surfCount))
             return;
 
         auto totalWeightCount = 0u;
@@ -348,13 +371,12 @@ namespace GAME
 
     void AddXModelVertexBoneWeights(XModelCommon& out, const XModel* model, const unsigned lod)
     {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-        auto& weightCollection = out.m_bone_weight_data;
-
-        if (!surfs)
+        XSurface* surfs;
+        unsigned surfCount;
+        if (!GetSurfaces(model, lod, surfs, surfCount))
             return;
 
+        auto& weightCollection = out.m_bone_weight_data;
         size_t weightOffset = 0u;
 
         for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
@@ -467,10 +489,9 @@ namespace GAME
 
     void AddXModelFaces(XModelCommon& out, const XModel* model, const unsigned lod)
     {
-        const auto* surfs = &model->surfs[model->lodInfo[lod].surfIndex];
-        const auto surfCount = model->lodInfo[lod].numsurfs;
-
-        if (!surfs)
+        XSurface* surfs;
+        unsigned surfCount;
+        if (!GetSurfaces(model, lod, surfs, surfCount))
             return;
 
         for (auto surfIndex = 0u; surfIndex < surfCount; surfIndex++)
@@ -671,8 +692,15 @@ namespace GAME
             if (xmodel.physPreset && xmodel.physPreset->name)
                 jXModel.physPreset = AssetName(xmodel.physPreset->name);
 
+#ifdef FEATURE_IW5
+            if (xmodel.physCollmap && xmodel.physCollmap->name)
+                jXModel.physCollmap = AssetName(xmodel.physCollmap->name);
+#endif
+
+#if defined(FEATURE_T5) || defined(FEATURE_T6)
             if (xmodel.physConstraints && xmodel.physConstraints->name)
                 jXModel.physConstraints = AssetName(xmodel.physConstraints->name);
+#endif
 
             jXModel.flags = xmodel.flags;
 
diff --git a/src/ObjWriting/XModel/XModelDumper.h.template b/src/ObjWriting/XModel/XModelDumper.h.template
index 9094c78f..c981ef6b 100644
--- a/src/ObjWriting/XModel/XModelDumper.h.template
+++ b/src/ObjWriting/XModel/XModelDumper.h.template
@@ -1,4 +1,4 @@
-#options GAME (T5, T6)
+#options GAME (IW5, T5, T6)
 
 #filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".h"
 
diff --git a/src/ZoneCode/Game/IW5/XAssets/XModel.txt b/src/ZoneCode/Game/IW5/XAssets/XModel.txt
index 43670572..72a48947 100644
--- a/src/ZoneCode/Game/IW5/XAssets/XModel.txt
+++ b/src/ZoneCode/Game/IW5/XAssets/XModel.txt
@@ -13,7 +13,7 @@ set count parentList numBones - numRootBones;
 set reusable quats;
 set count quats numBones - numRootBones;
 set reusable trans;
-set count trans numBones - numRootBones;
+set count trans (numBones - numRootBones) * 3;
 set reusable partClassification;
 set count partClassification numBones;
 set reusable baseMat;
diff --git a/src/ZoneCode/Game/IW5/XAssets/XModelSurfs.txt b/src/ZoneCode/Game/IW5/XAssets/XModelSurfs.txt
index dd41e7c3..1e939bbf 100644
--- a/src/ZoneCode/Game/IW5/XAssets/XModelSurfs.txt
+++ b/src/ZoneCode/Game/IW5/XAssets/XModelSurfs.txt
@@ -14,6 +14,9 @@ set count vertList vertListCount;
 set reusable triIndices;
 set block triIndices XFILE_BLOCK_INDEX;
 set count triIndices triCount;
+set reusable verts0;
+set block verts0 XFILE_BLOCK_VERTEX;
+set count verts0 XSurface::vertCount;
 reorder:
     zoneHandle
     vertInfo
@@ -29,15 +32,6 @@ set count vertsBlend vertCount[0]
 						+ 5 * vertCount[2]
 						+ 7 * vertCount[3];
 
-// GfxVertexUnion0
-use GfxVertexUnion0;
-set condition quantizedNoColorVerts0 never;
-set condition quantizedVerts0 never;
-set condition verts0 never;
-set reusable packedVerts0;
-set block packedVerts0 XFILE_BLOCK_VERTEX;
-set count packedVerts0 XSurface::vertCount;
-
 // XRigidVertList
 set reusable XRigidVertList::collisionTree;
 

From e814515b53dd92c2be53e6e72f2fd1d41e5e4ee2 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Fri, 20 Sep 2024 19:39:26 +0200
Subject: [PATCH 10/17] fix: failure to load xmodel does not abort loading
 process

---
 src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp | 5 +++++
 src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp  | 5 +++++
 src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp  | 5 +++++
 3 files changed, 15 insertions(+)

diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
index c4689b39..4f7f9629 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderXModel.cpp
@@ -34,9 +34,14 @@ bool AssetLoaderXModel::LoadFromRaw(
 
     std::vector<XAssetInfoGeneric*> dependencies;
     if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
+    {
         manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
+    }
     else
+    {
         std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
+        return false;
+    }
 
     return true;
 }
diff --git a/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
index d419ce67..22dee30f 100644
--- a/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
+++ b/src/ObjLoading/Game/T5/AssetLoaders/AssetLoaderXModel.cpp
@@ -34,9 +34,14 @@ bool AssetLoaderXModel::LoadFromRaw(
 
     std::vector<XAssetInfoGeneric*> dependencies;
     if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
+    {
         manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
+    }
     else
+    {
         std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
+        return false;
+    }
 
     return true;
 }
diff --git a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
index 2c1ff6f1..28dfcc01 100644
--- a/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
+++ b/src/ObjLoading/Game/T6/AssetLoaders/AssetLoaderXModel.cpp
@@ -34,9 +34,14 @@ bool AssetLoaderXModel::LoadFromRaw(
 
     std::vector<XAssetInfoGeneric*> dependencies;
     if (LoadXModel(*file.m_stream, *xmodel, memory, manager, dependencies))
+    {
         manager->AddAsset<AssetXModel>(assetName, xmodel, std::move(dependencies));
+    }
     else
+    {
         std::cerr << std::format("Failed to load xmodel \"{}\"\n", assetName);
+        return false;
+    }
 
     return true;
 }

From 1fef66397f6e7f08952bc5bb0c1d3ca8cb611b8c Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Fri, 20 Sep 2024 21:26:47 +0200
Subject: [PATCH 11/17] chore: implement scale based unit vec3 packing

---
 src/Common/Utils/Pack.cpp | 55 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 52 insertions(+), 3 deletions(-)

diff --git a/src/Common/Utils/Pack.cpp b/src/Common/Utils/Pack.cpp
index 2d0f4920..dec622e9 100644
--- a/src/Common/Utils/Pack.cpp
+++ b/src/Common/Utils/Pack.cpp
@@ -4,6 +4,7 @@
 
 #include <algorithm>
 #include <cassert>
+#include <cmath>
 #include <limits>
 
 union PackUtil32
@@ -17,6 +18,8 @@ union PackUtil32
 
 namespace pack32
 {
+    typedef float pvec3[3];
+
     uint32_t Vec2PackTexCoordsUV(const float (&in)[2])
     {
         return static_cast<uint32_t>(HalfFloat::ToHalf(in[1])) << 16 | HalfFloat::ToHalf(in[0]);
@@ -27,11 +30,57 @@ namespace pack32
         return static_cast<uint32_t>(HalfFloat::ToHalf(in[0])) << 16 | HalfFloat::ToHalf(in[1]);
     }
 
+    float Vec3_Normalize(pvec3& vector)
+    {
+        float length = std::sqrt(vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
+        if (-length >= 0.0f)
+            length = 1.0f;
+        const auto lengthInv = 1.0f / length;
+        vector[0] = lengthInv * vector[0];
+        vector[1] = lengthInv * vector[1];
+        vector[2] = lengthInv * vector[2];
+        return length;
+    }
+
     uint32_t Vec3PackUnitVecScaleBased(const float (&in)[3])
     {
-        // TODO: Implement
-        assert(false);
-        return 0;
+        PackUtil32 testEncoding{};
+        float normalized[3]{in[0], in[1], in[2]};
+        float decoded[3];
+
+        Vec3_Normalize(normalized);
+        uint32_t out = 0u;
+        auto bestDirError = 3.4028235e38f;
+        auto bestLenError = 3.4028235e38f;
+        testEncoding.uc[3] = 0u;
+        do
+        {
+            const auto encodeScale = 32385.0f / (static_cast<float>(testEncoding.uc[3]) - -192.0f);
+            testEncoding.c[0] = static_cast<int8_t>(normalized[0] * encodeScale + 127.5f);
+            testEncoding.c[1] = static_cast<int8_t>(normalized[1] * encodeScale + 127.5f);
+            testEncoding.c[2] = static_cast<int8_t>(normalized[2] * encodeScale + 127.5f);
+            const auto decodeScale = (static_cast<float>(testEncoding.uc[3]) - -192.0f) / 32385.0f;
+            decoded[0] = (static_cast<float>(testEncoding.uc[0]) - 127.0f) * decodeScale;
+            decoded[1] = (static_cast<float>(testEncoding.uc[1]) - 127.0f) * decodeScale;
+            decoded[2] = (static_cast<float>(testEncoding.uc[2]) - 127.0f) * decodeScale;
+            const auto v2 = Vec3_Normalize(decoded) - 1.0f;
+            const auto lenError = std::abs(v2);
+            if (lenError < 0.001f)
+            {
+                const auto dirError = std::abs(decoded[0] * normalized[0] + decoded[1] * normalized[1] + decoded[2] * normalized[2] - 1.0f);
+                if (bestDirError > dirError || bestDirError <= dirError && bestLenError > lenError)
+                {
+                    bestDirError = dirError;
+                    bestLenError = lenError;
+                    out = testEncoding.u;
+                    if (lenError + dirError == 0.0f)
+                        return out;
+                }
+            }
+            ++testEncoding.c[3];
+        } while (testEncoding.c[3]);
+
+        return out;
     }
 
     uint32_t Vec3PackUnitVecThirdBased(const float (&in)[3])

From 88499b64287905518a62a7d5b223f2d590277425 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Fri, 20 Sep 2024 21:27:34 +0200
Subject: [PATCH 12/17] chore: create temporary iw5 partbits

---
 raw/iw5/partclassification.csv    | 21 +++++++++++++++++++++
 raw/iw5/partclassification_mp.csv | 19 +++++++++++++++++++
 2 files changed, 40 insertions(+)
 create mode 100644 raw/iw5/partclassification.csv
 create mode 100644 raw/iw5/partclassification_mp.csv

diff --git a/raw/iw5/partclassification.csv b/raw/iw5/partclassification.csv
new file mode 100644
index 00000000..66ac687f
--- /dev/null
+++ b/raw/iw5/partclassification.csv
@@ -0,0 +1,21 @@
+J_Hip_RI,right_leg_upper
+J_Hip_LE,left_leg_upper
+J_Knee_RI,right_leg_lower
+J_SpineUpper,torso_upper
+J_Knee_LE,left_leg_lower
+J_Ankle_RI,right_foot
+J_Ankle_LE,left_foot
+J_Clavicle_RI,torso_upper
+J_Clavicle_LE,torso_upper
+J_Shoulder_RI,right_arm_upper
+J_Shoulder_LE,left_arm_upper
+J_Neck,neck
+J_Head,head
+J_Elbow_RI,right_arm_lower
+J_Elbow_LE,left_arm_lower
+J_Wrist_RI,right_hand
+J_Wrist_LE,left_hand
+J_MainRoot,torso_lower
+TAG_WEAPON_LEFT,gun
+TAG_WEAPON_RIGHT,gun
+J_Helmet,helmet
diff --git a/raw/iw5/partclassification_mp.csv b/raw/iw5/partclassification_mp.csv
new file mode 100644
index 00000000..87ee201f
--- /dev/null
+++ b/raw/iw5/partclassification_mp.csv
@@ -0,0 +1,19 @@
+J_Hip_RI,right_leg_upper
+J_Hip_LE,left_leg_upper
+J_Knee_RI,right_leg_lower
+J_SpineUpper,torso_lower
+J_SpineLower,torso_lower
+J_MainRoot,torso_lower
+J_Knee_LE,left_leg_lower
+J_Ankle_RI,right_foot
+J_Ankle_LE,left_foot
+J_Clavicle_RI,torso_upper
+J_Clavicle_LE,torso_upper
+J_Shoulder_RI,right_arm_upper
+J_Shoulder_LE,left_arm_upper
+J_Neck,neck
+J_Head,head
+J_Elbow_RI,right_arm_lower
+J_Elbow_LE,left_arm_lower
+J_Wrist_RI,right_hand
+J_Wrist_LE,left_hand

From 3b59dad109b6ca89098505f6eabbfc638211f016 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sun, 22 Sep 2024 15:10:01 +0200
Subject: [PATCH 13/17] chore: generalize material constant zone state

---
 src/Common/Game/T6/CommonT6.h                 |   5 +
 src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp   |  16 +-
 src/ObjCommon/Shader/D3D9ShaderAnalyser.h     |   2 +-
 .../T6/Material/MaterialConstantZoneState.cpp | 120 +------------
 .../T6/Material/MaterialConstantZoneState.h   |  24 +--
 .../AbstractMaterialConstantZoneState.cpp     | 164 ++++++++++++++++++
 .../AbstractMaterialConstantZoneState.h       |  41 +++++
 7 files changed, 229 insertions(+), 143 deletions(-)
 create mode 100644 src/ObjWriting/Material/AbstractMaterialConstantZoneState.cpp
 create mode 100644 src/ObjWriting/Material/AbstractMaterialConstantZoneState.h

diff --git a/src/Common/Game/T6/CommonT6.h b/src/Common/Game/T6/CommonT6.h
index 98e0cc6e..9f7fbb2b 100644
--- a/src/Common/Game/T6/CommonT6.h
+++ b/src/Common/Game/T6/CommonT6.h
@@ -21,6 +21,11 @@ namespace T6
             return hash;
         }
 
+        static constexpr uint32_t R_HashString(const char* string)
+        {
+            return R_HashString(string, 0u);
+        }
+
         static constexpr uint32_t SND_HashName(const char* str)
         {
             if (!str || !*str)
diff --git a/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp b/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp
index 918d3c16..4872c82b 100644
--- a/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp
+++ b/src/ObjCommon/Shader/D3D9ShaderAnalyser.cpp
@@ -61,12 +61,12 @@ namespace d3d9
         uint32_t TypeInfo;
     };
 
-    bool PopulateVersionInfo(ShaderInfo& shaderInfo, const uint32_t* shaderByteCode, const size_t shaderByteCodeSize)
+    bool PopulateVersionInfo(ShaderInfo& shaderInfo, const void* shaderByteCode, const size_t shaderByteCodeSize)
     {
         if (shaderByteCodeSize < sizeof(uint32_t))
             return false;
 
-        const auto version = *shaderByteCode;
+        const auto version = *static_cast<const uint32_t*>(shaderByteCode);
         shaderInfo.m_version_minor = version & 0xFF;
         shaderInfo.m_version_major = (version & 0xFF00) >> 8;
 
@@ -91,10 +91,10 @@ namespace d3d9
         return false;
     }
 
-    bool FindComment(const uint32_t* shaderByteCode, const size_t shaderByteCodeSize, const uint32_t magic, const char*& commentStart, size_t& commentSize)
+    bool FindComment(const uint8_t* shaderByteCode, const size_t shaderByteCodeSize, const uint32_t magic, const char*& commentStart, size_t& commentSize)
     {
-        const uint32_t* currentPos = shaderByteCode + 1;
-        size_t currentOffset = sizeof(uint32_t);
+        const auto* currentPos = reinterpret_cast<const uint32_t*>(shaderByteCode + sizeof(uint32_t));
+        auto currentOffset = sizeof(uint32_t);
         while (*currentPos != OPCODE_END && (currentOffset + sizeof(uint32_t) - 1) < shaderByteCodeSize)
         {
             const auto currentValue = *currentPos;
@@ -215,7 +215,7 @@ namespace d3d9
         return true;
     }
 
-    bool PopulateShaderInfoFromShaderByteCode(ShaderInfo& shaderInfo, const uint32_t* shaderByteCode, const size_t shaderByteCodeSize)
+    bool PopulateShaderInfoFromShaderByteCode(ShaderInfo& shaderInfo, const uint8_t* shaderByteCode, const size_t shaderByteCodeSize)
     {
         if (!PopulateVersionInfo(shaderInfo, shaderByteCode, shaderByteCodeSize))
             return false;
@@ -236,14 +236,14 @@ namespace d3d9
     }
 } // namespace d3d9
 
-std::unique_ptr<ShaderInfo> ShaderAnalyser::GetShaderInfo(const uint32_t* shaderByteCode, const size_t shaderByteCodeSize)
+std::unique_ptr<ShaderInfo> ShaderAnalyser::GetShaderInfo(const void* shaderByteCode, const size_t shaderByteCodeSize)
 {
     if (shaderByteCode == nullptr || shaderByteCodeSize == 0)
         return nullptr;
 
     auto shaderInfo = std::make_unique<ShaderInfo>();
 
-    if (!PopulateShaderInfoFromShaderByteCode(*shaderInfo, shaderByteCode, shaderByteCodeSize))
+    if (!PopulateShaderInfoFromShaderByteCode(*shaderInfo, static_cast<const uint8_t*>(shaderByteCode), shaderByteCodeSize))
         return nullptr;
 
     return shaderInfo;
diff --git a/src/ObjCommon/Shader/D3D9ShaderAnalyser.h b/src/ObjCommon/Shader/D3D9ShaderAnalyser.h
index e58f1202..7e186c8d 100644
--- a/src/ObjCommon/Shader/D3D9ShaderAnalyser.h
+++ b/src/ObjCommon/Shader/D3D9ShaderAnalyser.h
@@ -95,6 +95,6 @@ namespace d3d9
     class ShaderAnalyser
     {
     public:
-        static std::unique_ptr<ShaderInfo> GetShaderInfo(const uint32_t* shaderByteCode, size_t shaderByteCodeSize);
+        static std::unique_ptr<ShaderInfo> GetShaderInfo(const void* shaderByteCode, size_t shaderByteCodeSize);
     };
 } // namespace d3d9
diff --git a/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.cpp b/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.cpp
index f92b3783..0219063e 100644
--- a/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.cpp
+++ b/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.cpp
@@ -4,16 +4,9 @@
 #include "Game/T6/GameAssetPoolT6.h"
 #include "Game/T6/GameT6.h"
 #include "ObjWriting.h"
-#include "Shader/D3D11ShaderAnalyser.h"
-
-#include <chrono>
 
 namespace T6
 {
-    static constexpr const char* SAMPLER_STR = "Sampler";
-    static constexpr const char* GLOBALS_CBUFFER_NAME = "$Globals";
-    static constexpr const char* PER_OBJECT_CONSTS_CBUFFER_NAME = "PerObjectConsts";
-
     const char* KNOWN_CONSTANT_NAMES[]{
         "AngularVelocityScale",
         "AnimSpeed",
@@ -478,15 +471,8 @@ namespace T6
         "ui3dSampler",
     };
 
-    void MaterialConstantZoneState::ExtractNamesFromZone()
+    void MaterialConstantZoneState::ExtractNamesFromZoneInternal()
     {
-        if (ObjWriting::Configuration.Verbose)
-            std::cout << "Building material constant name lookup...\n";
-
-        const auto begin = std::chrono::high_resolution_clock::now();
-
-        AddStaticKnownNames();
-
         for (const auto* zone : g_GameT6.GetZones())
         {
             const auto* t6AssetPools = dynamic_cast<const GameAssetPoolT6*>(zone->m_pools.get());
@@ -504,49 +490,18 @@ namespace T6
                 }
             }
         }
-
-        const auto end = std::chrono::high_resolution_clock::now();
-
-        if (ObjWriting::Configuration.Verbose)
-        {
-            const auto durationInMs = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
-            std::cout << "Built material constant name lookup in " << durationInMs.count() << "ms: " << m_constant_names_from_shaders.size()
-                      << " constant names; " << m_texture_def_names_from_shaders.size() << " texture def names\n";
-        }
-    }
-
-    bool MaterialConstantZoneState::GetConstantName(const unsigned hash, std::string& constantName) const
-    {
-        const auto existingConstantName = m_constant_names_from_shaders.find(hash);
-        if (existingConstantName != m_constant_names_from_shaders.end())
-        {
-            constantName = existingConstantName->second;
-            return true;
-        }
-
-        return false;
     }
 
-    bool MaterialConstantZoneState::GetTextureDefName(const unsigned hash, std::string& textureDefName) const
+    unsigned MaterialConstantZoneState::HashString(const std::string& str)
     {
-        const auto existingTextureDefName = m_texture_def_names_from_shaders.find(hash);
-        if (existingTextureDefName != m_texture_def_names_from_shaders.end())
-        {
-            textureDefName = existingTextureDefName->second;
-            return true;
-        }
-
-        return false;
+        return Common::R_HashString(str.c_str());
     }
 
     void MaterialConstantZoneState::ExtractNamesFromTechnique(const MaterialTechnique* technique)
     {
-        const auto existingTechnique = m_dumped_techniques.find(technique);
-        if (existingTechnique != m_dumped_techniques.end())
+        if (!ShouldDumpFromStruct(technique))
             return;
 
-        m_dumped_techniques.emplace(technique);
-
         for (auto passIndex = 0u; passIndex < technique->passCount; passIndex++)
         {
             const auto& pass = technique->passArray[passIndex];
@@ -559,54 +514,6 @@ namespace T6
         }
     }
 
-    void MaterialConstantZoneState::ExtractNamesFromShader(const char* shader, const size_t shaderSize)
-    {
-        const auto shaderInfo = d3d11::ShaderAnalyser::GetShaderInfo(reinterpret_cast<const uint8_t*>(shader), shaderSize);
-        if (!shaderInfo)
-            return;
-
-        const auto globalsConstantBuffer = std::ranges::find_if(std::as_const(shaderInfo->m_constant_buffers),
-                                                                [](const d3d11::ConstantBuffer& constantBuffer)
-                                                                {
-                                                                    return constantBuffer.m_name == GLOBALS_CBUFFER_NAME;
-                                                                });
-
-        const auto perObjectConsts = std::ranges::find_if(std::as_const(shaderInfo->m_constant_buffers),
-                                                          [](const d3d11::ConstantBuffer& constantBuffer)
-                                                          {
-                                                              return constantBuffer.m_name == PER_OBJECT_CONSTS_CBUFFER_NAME;
-                                                          });
-
-        if (globalsConstantBuffer != shaderInfo->m_constant_buffers.end())
-        {
-            for (const auto& variable : globalsConstantBuffer->m_variables)
-                AddConstantName(variable.m_name);
-        }
-
-        if (perObjectConsts != shaderInfo->m_constant_buffers.end())
-        {
-            for (const auto& variable : perObjectConsts->m_variables)
-                AddConstantName(variable.m_name);
-        }
-
-        for (const auto& boundResource : shaderInfo->m_bound_resources)
-        {
-            if (boundResource.m_type == d3d11::BoundResourceType::SAMPLER || boundResource.m_type == d3d11::BoundResourceType::TEXTURE)
-            {
-                if (AddTextureDefName(boundResource.m_name))
-                {
-                    const auto samplerPos = boundResource.m_name.rfind(SAMPLER_STR);
-                    if (samplerPos != std::string::npos)
-                    {
-                        auto nameWithoutSamplerStr = boundResource.m_name;
-                        nameWithoutSamplerStr.erase(samplerPos, std::char_traits<char>::length(SAMPLER_STR));
-                        AddTextureDefName(std::move(nameWithoutSamplerStr));
-                    }
-                }
-            }
-        }
-    }
-
     void MaterialConstantZoneState::AddStaticKnownNames()
     {
         for (const auto* knownConstantName : KNOWN_CONSTANT_NAMES)
@@ -614,23 +521,4 @@ namespace T6
         for (const auto* knownTextureDefName : KNOWN_TEXTURE_DEF_NAMES)
             AddTextureDefName(knownTextureDefName);
     }
-
-    void MaterialConstantZoneState::AddConstantName(std::string constantName)
-    {
-        const auto hash = Common::R_HashString(constantName.c_str(), 0);
-        if (m_constant_names_from_shaders.contains(hash))
-            return;
-
-        m_constant_names_from_shaders.emplace(hash, std::move(constantName));
-    }
-
-    bool MaterialConstantZoneState::AddTextureDefName(std::string textureDefName)
-    {
-        const auto hash = Common::R_HashString(textureDefName.c_str(), 0);
-        if (m_texture_def_names_from_shaders.contains(hash))
-            return false;
-
-        m_texture_def_names_from_shaders.emplace(hash, std::move(textureDefName));
-        return true;
-    }
 } // namespace T6
diff --git a/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.h b/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.h
index 27f3f49e..148169eb 100644
--- a/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.h
+++ b/src/ObjWriting/Game/T6/Material/MaterialConstantZoneState.h
@@ -1,30 +1,18 @@
 #pragma once
 
-#include "Dumping/IZoneAssetDumperState.h"
 #include "Game/T6/T6.h"
+#include "Material/AbstractMaterialConstantZoneState.h"
 
 #include <string>
-#include <unordered_map>
-#include <unordered_set>
 
 namespace T6
 {
-    class MaterialConstantZoneState final : public IZoneAssetDumperState
+    class MaterialConstantZoneState final : public AbstractMaterialConstantZoneStateDx11
     {
-    public:
-        void ExtractNamesFromZone();
-        bool GetConstantName(unsigned hash, std::string& constantName) const;
-        bool GetTextureDefName(unsigned hash, std::string& textureDefName) const;
-
-    private:
+    protected:
+        void ExtractNamesFromZoneInternal() override;
         void ExtractNamesFromTechnique(const MaterialTechnique* technique);
-        void ExtractNamesFromShader(const char* shader, size_t shaderSize);
-        void AddStaticKnownNames();
-        void AddConstantName(std::string constantName);
-        bool AddTextureDefName(std::string textureDefName);
-
-        std::unordered_set<const MaterialTechnique*> m_dumped_techniques;
-        std::unordered_map<unsigned, std::string> m_constant_names_from_shaders;
-        std::unordered_map<unsigned, std::string> m_texture_def_names_from_shaders;
+        void AddStaticKnownNames() override;
+        unsigned HashString(const std::string& str) override;
     };
 } // namespace T6
diff --git a/src/ObjWriting/Material/AbstractMaterialConstantZoneState.cpp b/src/ObjWriting/Material/AbstractMaterialConstantZoneState.cpp
new file mode 100644
index 00000000..bdbee0c2
--- /dev/null
+++ b/src/ObjWriting/Material/AbstractMaterialConstantZoneState.cpp
@@ -0,0 +1,164 @@
+#include "AbstractMaterialConstantZoneState.h"
+
+#include "ObjWriting.h"
+#include "Shader/D3D11ShaderAnalyser.h"
+#include "Shader/D3D9ShaderAnalyser.h"
+
+#include <chrono>
+
+namespace
+{
+    constexpr const char* SAMPLER_STR = "Sampler";
+    constexpr const char* GLOBALS_CBUFFER_NAME = "$Globals";
+    constexpr const char* PER_OBJECT_CONSTS_CBUFFER_NAME = "PerObjectConsts";
+} // namespace
+
+void AbstractMaterialConstantZoneState::ExtractNamesFromZone()
+{
+    if (ObjWriting::Configuration.Verbose)
+        std::cout << "Building material constant name lookup...\n";
+
+    const auto begin = std::chrono::high_resolution_clock::now();
+
+    AddStaticKnownNames();
+
+    ExtractNamesFromZoneInternal();
+
+    const auto end = std::chrono::high_resolution_clock::now();
+
+    if (ObjWriting::Configuration.Verbose)
+    {
+        const auto durationInMs = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
+        std::cout << std::format("Built material constant name lookup in {}ms: {} constant names; {} texture def names\n",
+                                 durationInMs.count(),
+                                 m_constant_names_from_shaders.size(),
+                                 m_texture_def_names_from_shaders.size());
+    }
+}
+
+bool AbstractMaterialConstantZoneState::GetConstantName(const unsigned hash, std::string& constantName) const
+{
+    const auto existingConstantName = m_constant_names_from_shaders.find(hash);
+    if (existingConstantName != m_constant_names_from_shaders.end())
+    {
+        constantName = existingConstantName->second;
+        return true;
+    }
+
+    return false;
+}
+
+bool AbstractMaterialConstantZoneState::GetTextureDefName(const unsigned hash, std::string& textureDefName) const
+{
+    const auto existingTextureDefName = m_texture_def_names_from_shaders.find(hash);
+    if (existingTextureDefName != m_texture_def_names_from_shaders.end())
+    {
+        textureDefName = existingTextureDefName->second;
+        return true;
+    }
+
+    return false;
+}
+
+bool AbstractMaterialConstantZoneState::ShouldDumpFromStruct(const void* pStruct)
+{
+    const auto existingTextureDefName = m_dumped_structs.find(pStruct);
+    if (existingTextureDefName != m_dumped_structs.end())
+        return false;
+
+    m_dumped_structs.emplace(pStruct);
+    return true;
+}
+
+void AbstractMaterialConstantZoneState::AddConstantName(const std::string& constantName)
+{
+    const auto hash = HashString(constantName);
+    if (m_constant_names_from_shaders.contains(hash))
+        return;
+
+    m_constant_names_from_shaders.emplace(hash, constantName);
+}
+
+bool AbstractMaterialConstantZoneState::AddTextureDefName(const std::string& textureDefName)
+{
+    const auto hash = HashString(textureDefName);
+    if (m_texture_def_names_from_shaders.contains(hash))
+        return false;
+
+    m_texture_def_names_from_shaders.emplace(hash, textureDefName);
+    return true;
+}
+
+void AbstractMaterialConstantZoneStateDx9::ExtractNamesFromShader(const void* shader, const size_t shaderSize)
+{
+    const auto shaderInfo = d3d9::ShaderAnalyser::GetShaderInfo(shader, shaderSize);
+    if (!shaderInfo)
+        return;
+
+    for (const auto& constant : shaderInfo->m_constants)
+    {
+        if (constant.m_register_set == d3d9::RegisterSet::SAMPLER)
+        {
+            if (AddTextureDefName(constant.m_name))
+            {
+                const auto samplerPos = constant.m_name.rfind(SAMPLER_STR);
+                if (samplerPos != std::string::npos)
+                {
+                    auto nameWithoutSamplerStr = constant.m_name;
+                    nameWithoutSamplerStr.erase(samplerPos, std::char_traits<char>::length(SAMPLER_STR));
+                    AddTextureDefName(nameWithoutSamplerStr);
+                }
+            }
+        }
+        else
+            AddConstantName(constant.m_name);
+    }
+}
+
+void AbstractMaterialConstantZoneStateDx11::ExtractNamesFromShader(const void* shader, const size_t shaderSize)
+{
+    const auto shaderInfo = d3d11::ShaderAnalyser::GetShaderInfo(static_cast<const uint8_t*>(shader), shaderSize);
+    if (!shaderInfo)
+        return;
+
+    const auto globalsConstantBuffer = std::ranges::find_if(std::as_const(shaderInfo->m_constant_buffers),
+                                                            [](const d3d11::ConstantBuffer& constantBuffer)
+                                                            {
+                                                                return constantBuffer.m_name == GLOBALS_CBUFFER_NAME;
+                                                            });
+
+    const auto perObjectConsts = std::ranges::find_if(std::as_const(shaderInfo->m_constant_buffers),
+                                                      [](const d3d11::ConstantBuffer& constantBuffer)
+                                                      {
+                                                          return constantBuffer.m_name == PER_OBJECT_CONSTS_CBUFFER_NAME;
+                                                      });
+
+    if (globalsConstantBuffer != shaderInfo->m_constant_buffers.end())
+    {
+        for (const auto& variable : globalsConstantBuffer->m_variables)
+            AddConstantName(variable.m_name);
+    }
+
+    if (perObjectConsts != shaderInfo->m_constant_buffers.end())
+    {
+        for (const auto& variable : perObjectConsts->m_variables)
+            AddConstantName(variable.m_name);
+    }
+
+    for (const auto& boundResource : shaderInfo->m_bound_resources)
+    {
+        if (boundResource.m_type == d3d11::BoundResourceType::SAMPLER || boundResource.m_type == d3d11::BoundResourceType::TEXTURE)
+        {
+            if (AddTextureDefName(boundResource.m_name))
+            {
+                const auto samplerPos = boundResource.m_name.rfind(SAMPLER_STR);
+                if (samplerPos != std::string::npos)
+                {
+                    auto nameWithoutSamplerStr = boundResource.m_name;
+                    nameWithoutSamplerStr.erase(samplerPos, std::char_traits<char>::length(SAMPLER_STR));
+                    AddTextureDefName(nameWithoutSamplerStr);
+                }
+            }
+        }
+    }
+}
diff --git a/src/ObjWriting/Material/AbstractMaterialConstantZoneState.h b/src/ObjWriting/Material/AbstractMaterialConstantZoneState.h
new file mode 100644
index 00000000..2a7734d8
--- /dev/null
+++ b/src/ObjWriting/Material/AbstractMaterialConstantZoneState.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "Dumping/IZoneAssetDumperState.h"
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+class AbstractMaterialConstantZoneState : public IZoneAssetDumperState
+{
+public:
+    void ExtractNamesFromZone();
+    bool GetConstantName(unsigned hash, std::string& constantName) const;
+    bool GetTextureDefName(unsigned hash, std::string& textureDefName) const;
+
+protected:
+    virtual void ExtractNamesFromShader(const void* shader, size_t shaderSize) = 0;
+    virtual void ExtractNamesFromZoneInternal() = 0;
+    virtual void AddStaticKnownNames() = 0;
+    virtual unsigned HashString(const std::string& str) = 0;
+
+    bool ShouldDumpFromStruct(const void* pStruct);
+    void AddConstantName(const std::string& constantName);
+    bool AddTextureDefName(const std::string& textureDefName);
+
+    std::unordered_set<const void*> m_dumped_structs;
+    std::unordered_map<unsigned, std::string> m_constant_names_from_shaders;
+    std::unordered_map<unsigned, std::string> m_texture_def_names_from_shaders;
+};
+
+class AbstractMaterialConstantZoneStateDx9 : public AbstractMaterialConstantZoneState
+{
+protected:
+    void ExtractNamesFromShader(const void* shader, size_t shaderSize) override;
+};
+
+class AbstractMaterialConstantZoneStateDx11 : public AbstractMaterialConstantZoneState
+{
+protected:
+    void ExtractNamesFromShader(const void* shader, size_t shaderSize) override;
+};

From 7649e5d58fc1ecef42d77e68ea2b482cff48d5b7 Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sun, 22 Sep 2024 15:10:54 +0200
Subject: [PATCH 14/17] chore: generalize base64 usage

---
 src/Crypto/Impl/Base64.cpp                    | 49 +++++++++++++++++++
 src/Crypto/Impl/Base64.h                      | 12 +++++
 .../XModel/Gltf/Internal/GltfBuffer.cpp       | 15 ++----
 src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp | 11 ++---
 4 files changed, 71 insertions(+), 16 deletions(-)
 create mode 100644 src/Crypto/Impl/Base64.cpp
 create mode 100644 src/Crypto/Impl/Base64.h

diff --git a/src/Crypto/Impl/Base64.cpp b/src/Crypto/Impl/Base64.cpp
new file mode 100644
index 00000000..f4400d6a
--- /dev/null
+++ b/src/Crypto/Impl/Base64.cpp
@@ -0,0 +1,49 @@
+#include "Base64.h"
+
+#define LTC_NO_PROTOTYPES
+#include <tomcrypt.h>
+
+namespace base64
+{
+    std::string EncodeBase64(const void* inputData, const size_t inputLength)
+    {
+        const auto base64BufferSize = GetBase64EncodeOutputLength(inputLength);
+
+        std::string output(base64BufferSize, '\0');
+        const auto outLength = base64BufferSize + 1u;
+
+        const auto result = EncodeBase64(inputData, inputLength, output.data(), outLength);
+        assert(result);
+
+        return output;
+    }
+
+    bool EncodeBase64(const void* inputData, const size_t inputLength, void* outputBuffer, const size_t outputBufferSize)
+    {
+        unsigned long outLength = outputBufferSize;
+        const auto result = base64_encode(static_cast<const unsigned char*>(inputData), inputLength, static_cast<char*>(outputBuffer), &outLength);
+        return result == CRYPT_OK;
+    }
+
+    size_t GetBase64EncodeOutputLength(const size_t inputLength)
+    {
+        return 4u * ((inputLength + 2u) / 3u);
+    }
+
+    size_t DecodeBase64(const void* base64Data, const size_t inputLength, void* outputBuffer, const size_t outputBufferSize)
+    {
+        unsigned long outLength = GetBase64DecodeOutputLength(inputLength);
+        if (outLength > outputBufferSize)
+            return 0u;
+
+        const auto result = base64_decode(static_cast<const char*>(base64Data), inputLength, static_cast<unsigned char*>(outputBuffer), &outLength);
+        assert(result == CRYPT_OK);
+
+        return static_cast<size_t>(outLength);
+    }
+
+    size_t GetBase64DecodeOutputLength(const size_t inputLength)
+    {
+        return inputLength / 4u;
+    }
+} // namespace base64
diff --git a/src/Crypto/Impl/Base64.h b/src/Crypto/Impl/Base64.h
new file mode 100644
index 00000000..1e1ec51d
--- /dev/null
+++ b/src/Crypto/Impl/Base64.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <string>
+
+namespace base64
+{
+    std::string EncodeBase64(const void* inputData, size_t inputLength);
+    bool EncodeBase64(const void* inputData, size_t inputLength, void* outputBuffer, size_t outputBufferSize);
+    size_t GetBase64EncodeOutputLength(size_t inputLength);
+
+    size_t DecodeBase64(const void* base64Data, size_t inputLength, void* outputBuffer, size_t outputBufferSize);
+    size_t GetBase64DecodeOutputLength(size_t inputLength);
+} // namespace base64
diff --git a/src/ObjLoading/XModel/Gltf/Internal/GltfBuffer.cpp b/src/ObjLoading/XModel/Gltf/Internal/GltfBuffer.cpp
index 95ec99a1..a0edf7f0 100644
--- a/src/ObjLoading/XModel/Gltf/Internal/GltfBuffer.cpp
+++ b/src/ObjLoading/XModel/Gltf/Internal/GltfBuffer.cpp
@@ -1,14 +1,12 @@
 #include "GltfBuffer.h"
 
+#include "Impl/Base64.h"
 #include "XModel/Gltf/GltfConstants.h"
 
 #include <cassert>
 #include <cstdint>
 #include <cstring>
 
-#define LTC_NO_PROTOTYPES
-#include <tomcrypt.h>
-
 using namespace gltf;
 
 EmbeddedBuffer::EmbeddedBuffer(const void* data, const size_t dataSize)
@@ -51,15 +49,12 @@ bool DataUriBuffer::ReadDataFromUri(const std::string& uri)
         return false;
 
     const auto base64DataLength = uri.size() - URI_PREFIX_LENGTH;
+    m_data_size = base64::GetBase64DecodeOutputLength(base64DataLength);
+    m_data = std::make_unique<uint8_t[]>(m_data_size);
 
-    unsigned long outLength = base64DataLength / 4u;
-    m_data = std::make_unique<uint8_t[]>(outLength);
-    const auto result = base64_decode(&uri[URI_PREFIX_LENGTH], base64DataLength, m_data.get(), &outLength);
-    m_data_size = static_cast<size_t>(outLength);
-
-    assert(result == CRYPT_OK);
+    m_data_size = base64::DecodeBase64(&uri[URI_PREFIX_LENGTH], base64DataLength, m_data.get(), m_data_size);
 
-    return false;
+    return m_data_size > 0;
 }
 
 bool DataUriBuffer::ReadData(void* dest, const size_t offset, const size_t count) const
diff --git a/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp b/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp
index effb524a..49afebd6 100644
--- a/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp
+++ b/src/ObjWriting/XModel/Gltf/GltfTextOutput.cpp
@@ -6,6 +6,8 @@
 #include <nlohmann/json.hpp>
 
 #define LTC_NO_PROTOTYPES
+#include "Impl/Base64.h"
+
 #include <tomcrypt.h>
 
 using namespace gltf;
@@ -17,18 +19,15 @@ TextOutput::TextOutput(std::ostream& stream)
 
 std::optional<std::string> TextOutput::CreateBufferUri(const void* buffer, const size_t bufferSize) const
 {
-    const auto base64Length = 4u * ((bufferSize + 2u) / 3u);
+    const auto base64Length = base64::GetBase64EncodeOutputLength(bufferSize);
     const auto base64BufferSize = URI_PREFIX_LENGTH + base64Length;
 
     std::string output(base64BufferSize, '\0');
 
     std::memcpy(output.data(), GLTF_DATA_URI_PREFIX, URI_PREFIX_LENGTH);
 
-    unsigned long outLength = base64Length + 1u;
-    const auto result = base64_encode(static_cast<const unsigned char*>(buffer), bufferSize, &output[URI_PREFIX_LENGTH], &outLength);
-
-    assert(result == CRYPT_OK);
-    assert(outLength == base64Length);
+    auto result = base64::EncodeBase64(buffer, bufferSize, &output[URI_PREFIX_LENGTH], base64Length + 1u);
+    assert(result);
 
     return output;
 }

From 28a4fbd0d697390bd863aad570c20868b62cc94a Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sun, 22 Sep 2024 15:11:40 +0200
Subject: [PATCH 15/17] feat: dump iw5 materials as json

---
 src/Common/Game/IW5/CommonIW5.h               |  15 +
 src/Common/Game/IW5/IW5_Assets.h              | 228 +++++++++-
 .../Game/IW5/Material/JsonMaterial.h          | 391 ++++++++++++++++++
 .../IW5/AssetDumpers/AssetDumperMaterial.cpp  |  48 +++
 .../IW5/AssetDumpers/AssetDumperMaterial.h    |  21 +
 .../Game/IW5/Material/JsonMaterialWriter.cpp  | 293 +++++++++++++
 .../Game/IW5/Material/JsonMaterialWriter.h    |  11 +
 .../Material/MaterialConstantZoneState.cpp    | 236 +++++++++++
 .../IW5/Material/MaterialConstantZoneState.h  |  16 +
 src/ObjWriting/Game/IW5/ZoneDumperIW5.cpp     |   3 +-
 10 files changed, 1256 insertions(+), 6 deletions(-)
 create mode 100644 src/ObjCommon/Game/IW5/Material/JsonMaterial.h
 create mode 100644 src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.cpp
 create mode 100644 src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.h
 create mode 100644 src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.cpp
 create mode 100644 src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.h
 create mode 100644 src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.cpp
 create mode 100644 src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.h

diff --git a/src/Common/Game/IW5/CommonIW5.h b/src/Common/Game/IW5/CommonIW5.h
index 8347674c..33fac614 100644
--- a/src/Common/Game/IW5/CommonIW5.h
+++ b/src/Common/Game/IW5/CommonIW5.h
@@ -9,6 +9,21 @@ namespace IW5
     public:
         static int StringTable_HashString(const char* str);
 
+        static constexpr uint32_t R_HashString(const char* str, uint32_t hash)
+        {
+            for (const auto* pos = str; *pos; pos++)
+            {
+                hash = 33 * hash ^ (*pos | 0x20);
+            }
+
+            return hash;
+        }
+
+        static constexpr uint32_t R_HashString(const char* string)
+        {
+            return R_HashString(string, 0u);
+        }
+
         static PackedTexCoords Vec2PackTexCoords(const float (&in)[2]);
         static PackedUnitVec Vec3PackUnitVec(const float (&in)[3]);
         static GfxColor Vec4PackGfxColor(const float (&in)[4]);
diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h
index 129c9f2c..2d45909d 100644
--- a/src/Common/Game/IW5/IW5_Assets.h
+++ b/src/Common/Game/IW5/IW5_Assets.h
@@ -675,6 +675,23 @@ namespace IW5
         gcc_align(8) uint64_t packed;
     };
 
+    enum MaterialGameFlags
+    {
+        MTL_GAMEFLAG_1 = 0x1,
+        MTL_GAMEFLAG_2 = 0x2,
+        MTL_GAMEFLAG_4 = 0x4,
+        MTL_GAMEFLAG_8 = 0x8,
+        MTL_GAMEFLAG_10 = 0x10,
+        MTL_GAMEFLAG_20 = 0x20,
+        MTL_GAMEFLAG_40 = 0x40,
+        MTL_GAMEFLAG_80 = 0x80,
+        MTL_GAMEFLAG_100 = 0x100,
+        MTL_GAMEFLAG_200 = 0x200,
+        MTL_GAMEFLAG_400 = 0x400,
+        MTL_GAMEFLAG_800 = 0x800,
+        MTL_GAMEFLAG_1000 = 0x1000,
+    };
+
     struct MaterialInfo
     {
         const char* name;
@@ -737,13 +754,71 @@ namespace IW5
         water_t* water;
     };
 
+    enum TextureFilter
+    {
+        TEXTURE_FILTER_DISABLED = 0x0,
+        TEXTURE_FILTER_NEAREST = 0x1,
+        TEXTURE_FILTER_LINEAR = 0x2,
+        TEXTURE_FILTER_ANISO2X = 0x3,
+        TEXTURE_FILTER_ANISO4X = 0x4,
+
+        TEXTURE_FILTER_COUNT
+    };
+
+    enum SamplerStateBitsMipMap_e
+    {
+        SAMPLER_MIPMAP_ENUM_DISABLED,
+        SAMPLER_MIPMAP_ENUM_NEAREST,
+        SAMPLER_MIPMAP_ENUM_LINEAR,
+
+        SAMPLER_MIPMAP_ENUM_COUNT
+    };
+
+    enum SamplerStateBits_e
+    {
+        SAMPLER_FILTER_SHIFT = 0x0,
+        SAMPLER_FILTER_NEAREST = 0x1,
+        SAMPLER_FILTER_LINEAR = 0x2,
+        SAMPLER_FILTER_ANISO2X = 0x3,
+        SAMPLER_FILTER_ANISO4X = 0x4,
+        SAMPLER_FILTER_MASK = 0x7,
+
+        SAMPLER_MIPMAP_SHIFT = 0x3,
+        SAMPLER_MIPMAP_DISABLED = 0x0,
+        SAMPLER_MIPMAP_NEAREST = 0x8,
+        SAMPLER_MIPMAP_LINEAR = 0x10,
+        SAMPLER_MIPMAP_COUNT = 0x3,
+        SAMPLER_MIPMAP_MASK = 0x18,
+
+        SAMPLER_CLAMP_U_SHIFT = 0x5,
+        SAMPLER_CLAMP_V_SHIFT = 0x6,
+        SAMPLER_CLAMP_W_SHIFT = 0x7,
+        SAMPLER_CLAMP_U = 0x20,
+        SAMPLER_CLAMP_V = 0x40,
+        SAMPLER_CLAMP_W = 0x80,
+        SAMPLER_CLAMP_MASK = 0xE0,
+    };
+
+    struct MaterialTextureDefSamplerState
+    {
+        unsigned char filter : 3;
+        unsigned char mipMap : 2;
+        unsigned char clampU : 1;
+        unsigned char clampV : 1;
+        unsigned char clampW : 1;
+    };
+
+#ifndef __zonecodegenerator
+    static_assert(sizeof(MaterialTextureDefSamplerState) == 1u);
+#endif
+
     struct MaterialTextureDef
     {
         unsigned int nameHash;
         char nameStart;
         char nameEnd;
-        unsigned char samplerState;
-        unsigned char semantic;
+        MaterialTextureDefSamplerState samplerState;
+        unsigned char semantic; // TextureSemantic
         MaterialTextureDefInfo u;
     };
 
@@ -751,18 +826,161 @@ namespace IW5
     {
         unsigned int nameHash;
         char name[12];
-        float literal[4];
+        vec4_t literal;
+    };
+
+    enum GfxBlend
+    {
+        GFXS_BLEND_DISABLED = 0x0,
+        GFXS_BLEND_ZERO = 0x1,
+        GFXS_BLEND_ONE = 0x2,
+        GFXS_BLEND_SRCCOLOR = 0x3,
+        GFXS_BLEND_INVSRCCOLOR = 0x4,
+        GFXS_BLEND_SRCALPHA = 0x5,
+        GFXS_BLEND_INVSRCALPHA = 0x6,
+        GFXS_BLEND_DESTALPHA = 0x7,
+        GFXS_BLEND_INVDESTALPHA = 0x8,
+        GFXS_BLEND_DESTCOLOR = 0x9,
+        GFXS_BLEND_INVDESTCOLOR = 0xA,
+
+        GFXS_BLEND_COUNT
+    };
+
+    enum GfxBlendOp
+    {
+        GFXS_BLENDOP_DISABLED = 0x0,
+        GFXS_BLENDOP_ADD = 0x1,
+        GFXS_BLENDOP_SUBTRACT = 0x2,
+        GFXS_BLENDOP_REVSUBTRACT = 0x3,
+        GFXS_BLENDOP_MIN = 0x4,
+        GFXS_BLENDOP_MAX = 0x5,
+
+        GFXS_BLENDOP_COUNT
     };
 
+    enum GfxAlphaTest_e
+    {
+        GFXS_ALPHA_TEST_GT_0 = 1,
+        GFXS_ALPHA_TEST_LT_128 = 2,
+        GFXS_ALPHA_TEST_GE_128 = 3,
+
+        GFXS_ALPHA_TEST_COUNT
+    };
+
+    enum GfxCullFace_e
+    {
+        GFXS_CULL_NONE = 1,
+        GFXS_CULL_BACK = 2,
+        GFXS_CULL_FRONT = 3,
+    };
+
+    enum GfxDepthTest_e
+    {
+        GFXS_DEPTHTEST_ALWAYS = 0,
+        GFXS_DEPTHTEST_LESS = 1,
+        GFXS_DEPTHTEST_EQUAL = 2,
+        GFXS_DEPTHTEST_LESSEQUAL = 3
+    };
+
+    enum GfxPolygonOffset_e
+    {
+        GFXS_POLYGON_OFFSET_0 = 0,
+        GFXS_POLYGON_OFFSET_1 = 1,
+        GFXS_POLYGON_OFFSET_2 = 2,
+        GFXS_POLYGON_OFFSET_SHADOWMAP = 3
+    };
+
+    enum GfxStencilOp
+    {
+        GFXS_STENCILOP_KEEP = 0x0,
+        GFXS_STENCILOP_ZERO = 0x1,
+        GFXS_STENCILOP_REPLACE = 0x2,
+        GFXS_STENCILOP_INCRSAT = 0x3,
+        GFXS_STENCILOP_DECRSAT = 0x4,
+        GFXS_STENCILOP_INVERT = 0x5,
+        GFXS_STENCILOP_INCR = 0x6,
+        GFXS_STENCILOP_DECR = 0x7
+    };
+
+    enum GfxStencilFunc
+    {
+        GFXS_STENCILFUNC_NEVER = 0x0,
+        GFXS_STENCILFUNC_LESS = 0x1,
+        GFXS_STENCILFUNC_EQUAL = 0x2,
+        GFXS_STENCILFUNC_LESSEQUAL = 0x3,
+        GFXS_STENCILFUNC_GREATER = 0x4,
+        GFXS_STENCILFUNC_NOTEQUAL = 0x5,
+        GFXS_STENCILFUNC_GREATEREQUAL = 0x6,
+        GFXS_STENCILFUNC_ALWAYS = 0x7
+    };
+
+    struct GfxStateBitsLoadBitsStructured
+    {
+        // Byte 0
+        unsigned int srcBlendRgb : 4;       // 0-3
+        unsigned int dstBlendRgb : 4;       // 4-7
+        unsigned int blendOpRgb : 3;        // 8-10
+        unsigned int alphaTestDisabled : 1; // 11
+        unsigned int alphaTest : 2;         // 12-13
+        unsigned int cullFace : 2;          // 14-15
+        unsigned int srcBlendAlpha : 4;     // 16-19
+        unsigned int dstBlendAlpha : 4;     // 20-23
+        unsigned int blendOpAlpha : 3;      // 24-26
+        unsigned int colorWriteRgb : 1;     // 27
+        unsigned int colorWriteAlpha : 1;   // 28
+        unsigned int unused0 : 1;           // 29
+        unsigned int gammaWrite : 1;        // 30
+        unsigned int polymodeLine : 1;      // 31
+
+        // Byte 1
+        unsigned int depthWrite : 1;          // 0
+        unsigned int depthTestDisabled : 1;   // 1
+        unsigned int depthTest : 2;           // 2-3
+        unsigned int polygonOffset : 2;       // 4-5
+        unsigned int stencilFrontEnabled : 1; // 6
+        unsigned int stencilBackEnabled : 1;  // 7
+        unsigned int stencilFrontPass : 3;    // 8-10
+        unsigned int stencilFrontFail : 3;    // 11-13
+        unsigned int stencilFrontZFail : 3;   // 14-16
+        unsigned int stencilFrontFunc : 3;    // 17-19
+        unsigned int stencilBackPass : 3;     // 20-22
+        unsigned int stencilBackFail : 3;     // 23-25
+        unsigned int stencilBackZFail : 3;    // 26-28
+        unsigned int stencilBackFunc : 3;     // 29-31
+    };
+
+    union GfxStateBitsLoadBits
+    {
+        unsigned int raw[2];
+        GfxStateBitsLoadBitsStructured structured;
+    };
+
+#ifndef __zonecodegenerator
+    static_assert(sizeof(GfxStateBitsLoadBits) == 8);
+    static_assert(sizeof(GfxStateBitsLoadBitsStructured) == 8);
+#endif
+
     struct GfxStateBits
     {
-        unsigned int loadBits[2];
+        GfxStateBitsLoadBits loadBits;
+    };
+
+    enum GfxCameraRegionType
+    {
+        CAMERA_REGION_LIT_OPAQUE = 0x0,
+        CAMERA_REGION_LIT_TRANS = 0x1,
+        CAMERA_REGION_EMISSIVE = 0x2,
+        CAMERA_REGION_DEPTH_HACK = 0x3,
+        CAMERA_REGION_LIGHT_MAP_OPAQUE = 0x4,
+
+        CAMERA_REGION_COUNT,
+        CAMERA_REGION_NONE = CAMERA_REGION_COUNT,
     };
 
     struct Material
     {
         MaterialInfo info;
-        unsigned char stateBitsEntry[54];
+        char stateBitsEntry[54];
         unsigned char textureCount;
         unsigned char constantCount;
         unsigned char stateBitsCount;
diff --git a/src/ObjCommon/Game/IW5/Material/JsonMaterial.h b/src/ObjCommon/Game/IW5/Material/JsonMaterial.h
new file mode 100644
index 00000000..b6dd0d25
--- /dev/null
+++ b/src/ObjCommon/Game/IW5/Material/JsonMaterial.h
@@ -0,0 +1,391 @@
+#pragma once
+
+#include "Game/IW5/IW5.h"
+
+#include "Json/JsonExtension.h"
+#include <memory>
+#include <nlohmann/json.hpp>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace IW5
+{
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxStencilOp,
+                                 {
+                                     {GFXS_STENCILOP_KEEP,    "keep"   },
+                                     {GFXS_STENCILOP_ZERO,    "zero"   },
+                                     {GFXS_STENCILOP_REPLACE, "replace"},
+                                     {GFXS_STENCILOP_INCRSAT, "incrsat"},
+                                     {GFXS_STENCILOP_DECRSAT, "decrsat"},
+                                     {GFXS_STENCILOP_INVERT,  "invert" },
+                                     {GFXS_STENCILOP_INCR,    "incr"   },
+                                     {GFXS_STENCILOP_DECR,    "decr"   },
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxStencilFunc,
+                                 {
+                                     {GFXS_STENCILFUNC_NEVER,        "never"       },
+                                     {GFXS_STENCILFUNC_LESS,         "less"        },
+                                     {GFXS_STENCILFUNC_EQUAL,        "equal"       },
+                                     {GFXS_STENCILFUNC_LESSEQUAL,    "lessequal"   },
+                                     {GFXS_STENCILFUNC_GREATER,      "greater"     },
+                                     {GFXS_STENCILFUNC_NOTEQUAL,     "notequal"    },
+                                     {GFXS_STENCILFUNC_GREATEREQUAL, "greaterequal"},
+                                     {GFXS_STENCILFUNC_ALWAYS,       "always"      },
+    });
+
+    class JsonStencil
+    {
+    public:
+        GfxStencilOp pass;
+        GfxStencilOp fail;
+        GfxStencilOp zfail;
+        GfxStencilFunc func;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonStencil, pass, fail, zfail, func);
+
+    enum class JsonAlphaTest
+    {
+        INVALID,
+        DISABLED,
+        GT0,
+        LT128,
+        GE128
+    };
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(JsonAlphaTest,
+                                 {
+                                     {JsonAlphaTest::INVALID,  nullptr   },
+                                     {JsonAlphaTest::DISABLED, "disabled"},
+                                     {JsonAlphaTest::GT0,      "gt0"     },
+                                     {JsonAlphaTest::LT128,    "lt128"   },
+                                     {JsonAlphaTest::GE128,    "ge128"   }
+    });
+
+    enum class JsonCullFace
+    {
+        INVALID,
+        NONE,
+        BACK,
+        FRONT
+    };
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(
+        JsonCullFace,
+        {
+            {JsonCullFace::INVALID, nullptr},
+            {JsonCullFace::NONE,    "none" },
+            {JsonCullFace::BACK,    "back" },
+            {JsonCullFace::FRONT,   "front"}
+    });
+
+    enum class JsonDepthTest
+    {
+        INVALID,
+        DISABLED,
+        ALWAYS,
+        LESS,
+        EQUAL,
+        LESS_EQUAL
+    };
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(JsonDepthTest,
+                                 {
+                                     {JsonDepthTest::INVALID,    nullptr     },
+                                     {JsonDepthTest::DISABLED,   "disabled"  },
+                                     {JsonDepthTest::ALWAYS,     "always"    },
+                                     {JsonDepthTest::LESS,       "less"      },
+                                     {JsonDepthTest::EQUAL,      "equal"     },
+                                     {JsonDepthTest::LESS_EQUAL, "less_equal"}
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxBlend,
+                                 {
+                                     {GFXS_BLEND_DISABLED,     "disabled"    },
+                                     {GFXS_BLEND_ZERO,         "zero"        },
+                                     {GFXS_BLEND_ONE,          "one"         },
+                                     {GFXS_BLEND_SRCCOLOR,     "srccolor"    },
+                                     {GFXS_BLEND_INVSRCCOLOR,  "invsrccolor" },
+                                     {GFXS_BLEND_SRCALPHA,     "srcalpha"    },
+                                     {GFXS_BLEND_INVSRCALPHA,  "invsrcalpha" },
+                                     {GFXS_BLEND_DESTALPHA,    "destalpha"   },
+                                     {GFXS_BLEND_INVDESTALPHA, "invdestalpha"},
+                                     {GFXS_BLEND_DESTCOLOR,    "destcolor"   },
+                                     {GFXS_BLEND_INVDESTCOLOR, "invdestcolor"},
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxBlendOp,
+                                 {
+                                     {GFXS_BLENDOP_DISABLED,    "disabled"   },
+                                     {GFXS_BLENDOP_ADD,         "add"        },
+                                     {GFXS_BLENDOP_SUBTRACT,    "subtract"   },
+                                     {GFXS_BLENDOP_REVSUBTRACT, "revsubtract"},
+                                     {GFXS_BLENDOP_MIN,         "min"        },
+                                     {GFXS_BLENDOP_MAX,         "max"        },
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxPolygonOffset_e,
+                                 {
+                                     {GFXS_POLYGON_OFFSET_0,         "offset0"        },
+                                     {GFXS_POLYGON_OFFSET_1,         "offset1"        },
+                                     {GFXS_POLYGON_OFFSET_2,         "offset2"        },
+                                     {GFXS_POLYGON_OFFSET_SHADOWMAP, "offsetShadowmap"},
+    });
+
+    class JsonStateBitsTableEntry
+    {
+    public:
+        GfxBlend srcBlendRgb;
+        GfxBlend dstBlendRgb;
+        GfxBlendOp blendOpRgb;
+        JsonAlphaTest alphaTest;
+        JsonCullFace cullFace;
+        GfxBlend srcBlendAlpha;
+        GfxBlend dstBlendAlpha;
+        GfxBlendOp blendOpAlpha;
+        bool colorWriteRgb;
+        bool colorWriteAlpha;
+        bool gammaWrite;
+        bool polymodeLine;
+        bool depthWrite;
+        JsonDepthTest depthTest;
+        GfxPolygonOffset_e polygonOffset;
+        std::optional<JsonStencil> stencilFront;
+        std::optional<JsonStencil> stencilBack;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonStateBitsTableEntry,
+                                   srcBlendRgb,
+                                   dstBlendRgb,
+                                   blendOpRgb,
+                                   alphaTest,
+                                   cullFace,
+                                   srcBlendAlpha,
+                                   dstBlendAlpha,
+                                   blendOpAlpha,
+                                   colorWriteRgb,
+                                   colorWriteAlpha,
+                                   polymodeLine,
+                                   depthWrite,
+                                   depthWrite,
+                                   depthTest,
+                                   polygonOffset,
+                                   stencilFront,
+                                   stencilBack);
+
+    class JsonConstant
+    {
+    public:
+        std::optional<std::string> name;
+        std::optional<std::string> nameFragment;
+        std::optional<unsigned> nameHash;
+        std::vector<float> literal;
+    };
+
+    inline void to_json(nlohmann::json& out, const JsonConstant& in)
+    {
+        if (in.name.has_value())
+        {
+            optional_to_json(out, "name", in.name);
+        }
+        else
+        {
+            optional_to_json(out, "nameFragment", in.nameFragment);
+            optional_to_json(out, "nameHash", in.nameHash);
+        }
+
+        out["literal"] = in.literal;
+    }
+
+    inline void from_json(const nlohmann::json& in, JsonConstant& out)
+    {
+        optional_from_json(in, "name", out.name);
+        optional_from_json(in, "nameFragment", out.nameFragment);
+        optional_from_json(in, "nameHash", out.nameHash);
+        in.at("literal").get_to(out.literal);
+    };
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(TextureFilter,
+                                 {
+                                     {TEXTURE_FILTER_DISABLED, "disabled"},
+                                     {TEXTURE_FILTER_NEAREST,  "nearest" },
+                                     {TEXTURE_FILTER_LINEAR,   "linear"  },
+                                     {TEXTURE_FILTER_ANISO2X,  "aniso2x" },
+                                     {TEXTURE_FILTER_ANISO4X,  "aniso4x" },
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(SamplerStateBitsMipMap_e,
+                                 {
+                                     {SAMPLER_MIPMAP_ENUM_DISABLED, "disabled"},
+                                     {SAMPLER_MIPMAP_ENUM_NEAREST,  "nearest" },
+                                     {SAMPLER_MIPMAP_ENUM_LINEAR,   "linear"  },
+    });
+
+    class JsonSamplerState
+    {
+    public:
+        TextureFilter filter;
+        SamplerStateBitsMipMap_e mipMap;
+        bool clampU;
+        bool clampV;
+        bool clampW;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonSamplerState, filter, mipMap, clampU, clampV, clampW);
+
+    class JsonComplex
+    {
+    public:
+        float real;
+        float imag;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonComplex, real, imag);
+
+    class JsonWater
+    {
+    public:
+        float floatTime;
+        int m;
+        int n;
+        std::string h0;
+        std::string wTerm;
+        float lx;
+        float lz;
+        float gravity;
+        float windvel;
+        std::array<float, 2> winddir;
+        float amplitude;
+        std::array<float, 4> codeConstant;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonWater, floatTime, m, n, h0, wTerm, lx, lz, gravity, windvel, winddir, amplitude, codeConstant);
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(TextureSemantic,
+                                 {
+                                     {TS_2D,               "2D"             },
+                                     {TS_FUNCTION,         "function"       },
+                                     {TS_COLOR_MAP,        "colorMap"       },
+                                     {TS_DETAIL_MAP,       "detailMap"      },
+                                     {TS_UNUSED_2,         "unused2"        },
+                                     {TS_NORMAL_MAP,       "normalMap"      },
+                                     {TS_UNUSED_3,         "unused3"        },
+                                     {TS_UNUSED_4,         "unused4"        },
+                                     {TS_SPECULAR_MAP,     "specularMap"    },
+                                     {TS_UNUSED_5,         "unused5"        },
+                                     {TS_UNUSED_6,         "unused6"        },
+                                     {TS_WATER_MAP,        "waterMap"       },
+                                     {TS_DISPLACEMENT_MAP, "displacementMap"},
+    });
+
+    class JsonTexture
+    {
+    public:
+        std::optional<std::string> name;
+        std::optional<unsigned> nameHash;
+        std::optional<std::string> nameStart;
+        std::optional<std::string> nameEnd;
+        TextureSemantic semantic;
+        JsonSamplerState samplerState;
+        std::string image;
+        std::optional<JsonWater> water;
+    };
+
+    inline void to_json(nlohmann::json& out, const JsonTexture& in)
+    {
+        if (in.name.has_value())
+        {
+            optional_to_json(out, "name", in.name);
+        }
+        else
+        {
+            optional_to_json(out, "nameHash", in.nameHash);
+            optional_to_json(out, "nameStart", in.nameStart);
+            optional_to_json(out, "nameEnd", in.nameEnd);
+        }
+
+        out["semantic"] = in.semantic;
+        out["samplerState"] = in.samplerState;
+        out["image"] = in.image;
+        optional_to_json(out, "water", in.water);
+    }
+
+    inline void from_json(const nlohmann::json& in, JsonTexture& out)
+    {
+        optional_from_json(in, "name", out.name);
+        optional_from_json(in, "nameHash", out.nameHash);
+        optional_from_json(in, "nameStart", out.nameStart);
+        optional_from_json(in, "nameEnd", out.nameEnd);
+        in.at("semantic").get_to(out.semantic);
+        in.at("samplerState").get_to(out.samplerState);
+        in.at("image").get_to(out.image);
+        optional_from_json(in, "water", out.water);
+    };
+
+    class JsonTextureAtlas
+    {
+    public:
+        uint8_t rows;
+        uint8_t columns;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonTextureAtlas, rows, columns);
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(MaterialGameFlags,
+                                 {
+                                     {MTL_GAMEFLAG_1,    "1"   },
+                                     {MTL_GAMEFLAG_2,    "2"   },
+                                     {MTL_GAMEFLAG_4,    "4"   },
+                                     {MTL_GAMEFLAG_8,    "8"   },
+                                     {MTL_GAMEFLAG_10,   "10"  },
+                                     {MTL_GAMEFLAG_20,   "20"  },
+                                     {MTL_GAMEFLAG_40,   "40"  },
+                                     {MTL_GAMEFLAG_80,   "80"  },
+                                     {MTL_GAMEFLAG_100,  "100" },
+                                     {MTL_GAMEFLAG_200,  "200" },
+                                     {MTL_GAMEFLAG_400,  "400" },
+                                     {MTL_GAMEFLAG_800,  "800" },
+                                     {MTL_GAMEFLAG_1000, "1000"},
+    });
+
+    NLOHMANN_JSON_SERIALIZE_ENUM(GfxCameraRegionType,
+                                 {
+                                     {CAMERA_REGION_LIT_OPAQUE,       "litOpaque"     },
+                                     {CAMERA_REGION_LIT_TRANS,        "litTrans"      },
+                                     {CAMERA_REGION_EMISSIVE,         "emissive"      },
+                                     {CAMERA_REGION_DEPTH_HACK,       "depthHack"     },
+                                     {CAMERA_REGION_LIGHT_MAP_OPAQUE, "lightMapOpaque"},
+                                     {CAMERA_REGION_NONE,             "none"          },
+    });
+
+    class JsonMaterial
+    {
+    public:
+        std::vector<MaterialGameFlags> gameFlags;
+        unsigned sortKey;
+        std::optional<JsonTextureAtlas> textureAtlas;
+        unsigned surfaceTypeBits;
+        std::vector<int8_t> stateBitsEntry;
+        unsigned stateFlags;
+        GfxCameraRegionType cameraRegion;
+        std::string techniqueSet;
+        std::vector<JsonTexture> textures;
+        std::vector<JsonConstant> constants;
+        std::vector<JsonStateBitsTableEntry> stateBits;
+    };
+
+    NLOHMANN_DEFINE_TYPE_EXTENSION(JsonMaterial,
+                                   gameFlags,
+                                   sortKey,
+                                   textureAtlas,
+                                   surfaceTypeBits,
+                                   stateBitsEntry,
+                                   stateFlags,
+                                   cameraRegion,
+                                   techniqueSet,
+                                   textures,
+                                   constants,
+                                   stateBits);
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.cpp b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.cpp
new file mode 100644
index 00000000..c4efb8f9
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.cpp
@@ -0,0 +1,48 @@
+#include "AssetDumperMaterial.h"
+
+#include "Game/IW5/Material/JsonMaterialWriter.h"
+#include "Game/IW5/Material/MaterialConstantZoneState.h"
+
+#include <algorithm>
+#include <format>
+#include <ranges>
+
+using namespace IW5;
+
+std::string AssetDumperMaterial::GetFileNameForAsset(const std::string& assetName)
+{
+    std::string sanitizedFileName(assetName);
+    if (sanitizedFileName[0] == '*')
+    {
+        std::ranges::replace(sanitizedFileName, '*', '_');
+        const auto parenthesisPos = sanitizedFileName.find('(');
+        if (parenthesisPos != std::string::npos)
+            sanitizedFileName.erase(parenthesisPos);
+        sanitizedFileName = "generated/" + sanitizedFileName;
+    }
+
+    return std::format("materials/{}.json", sanitizedFileName);
+}
+
+bool AssetDumperMaterial::ShouldDump(XAssetInfo<Material>* asset)
+{
+    return true;
+}
+
+void AssetDumperMaterial::DumpAsset(AssetDumpingContext& context, XAssetInfo<Material>* asset)
+{
+    const auto assetFile = context.OpenAssetFile(GetFileNameForAsset(asset->m_name));
+
+    if (!assetFile)
+        return;
+
+    DumpMaterialAsJson(*assetFile, asset->Asset(), context);
+}
+
+void AssetDumperMaterial::DumpPool(AssetDumpingContext& context, AssetPool<Material>* pool)
+{
+    auto* materialConstantState = context.GetZoneAssetDumperState<MaterialConstantZoneState>();
+    materialConstantState->ExtractNamesFromZone();
+
+    AbstractAssetDumper::DumpPool(context, pool);
+}
diff --git a/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.h b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.h
new file mode 100644
index 00000000..04c8e914
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/AssetDumpers/AssetDumperMaterial.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "Dumping/AbstractAssetDumper.h"
+#include "Game/IW5/IW5.h"
+
+#include <string>
+
+namespace IW5
+{
+    class AssetDumperMaterial final : public AbstractAssetDumper<Material>
+    {
+        static std::string GetFileNameForAsset(const std::string& assetName);
+
+    protected:
+        bool ShouldDump(XAssetInfo<Material>* asset) override;
+        void DumpAsset(AssetDumpingContext& context, XAssetInfo<Material>* asset) override;
+
+    public:
+        void DumpPool(AssetDumpingContext& context, AssetPool<Material>* pool) override;
+    };
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.cpp b/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.cpp
new file mode 100644
index 00000000..66ec64b4
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.cpp
@@ -0,0 +1,293 @@
+#include "JsonMaterialWriter.h"
+
+#include "Game/IW5/CommonIW5.h"
+#include "Game/IW5/Material/JsonMaterial.h"
+#include "Impl/Base64.h"
+#include "MaterialConstantZoneState.h"
+
+#include <iomanip>
+#include <nlohmann/json.hpp>
+
+using namespace nlohmann;
+using namespace IW5;
+
+namespace
+{
+    class JsonDumper
+    {
+    public:
+        JsonDumper(AssetDumpingContext& context, std::ostream& stream)
+            : m_stream(stream),
+              m_material_constants(*context.GetZoneAssetDumperState<MaterialConstantZoneState>())
+        {
+        }
+
+        void Dump(const Material* material) const
+        {
+            JsonMaterial jsonMaterial;
+            CreateJsonMaterial(jsonMaterial, *material);
+            json jRoot = jsonMaterial;
+
+            jRoot["_type"] = "material";
+            jRoot["_game"] = "iw5";
+            jRoot["_version"] = 1;
+
+            m_stream << std::setw(4) << jRoot << "\n";
+        }
+
+    private:
+        static const char* AssetName(const char* input)
+        {
+            if (input && input[0] == ',')
+                return &input[1];
+
+            return input;
+        }
+
+        static void CreateJsonGameFlags(JsonMaterial& jMaterial, const unsigned gameFlags)
+        {
+            jMaterial.gameFlags.clear();
+            for (auto i = 0u; i < sizeof(gameFlags) * 8u; i++)
+            {
+                const auto flag = static_cast<MaterialGameFlags>(1 << i);
+
+                if (gameFlags & flag)
+                    jMaterial.gameFlags.emplace_back(flag);
+            }
+        }
+
+        static void CreateJsonSamplerState(JsonSamplerState& jSamplerState, const MaterialTextureDefSamplerState& samplerState)
+        {
+            jSamplerState.filter = static_cast<TextureFilter>(samplerState.filter);
+            jSamplerState.mipMap = static_cast<SamplerStateBitsMipMap_e>(samplerState.mipMap);
+            jSamplerState.clampU = samplerState.clampU;
+            jSamplerState.clampV = samplerState.clampV;
+            jSamplerState.clampW = samplerState.clampW;
+        }
+
+        static void CreateJsonWater(JsonWater& jWater, const water_t& water)
+        {
+            jWater.floatTime = water.writable.floatTime;
+            jWater.m = water.M;
+            jWater.n = water.N;
+            jWater.lx = water.Lx;
+            jWater.lz = water.Lz;
+            jWater.gravity = water.gravity;
+            jWater.windvel = water.windvel;
+            jWater.winddir[0] = water.winddir[0];
+            jWater.winddir[1] = water.winddir[1];
+            jWater.amplitude = water.amplitude;
+            jWater.codeConstant[0] = water.codeConstant[0];
+            jWater.codeConstant[1] = water.codeConstant[1];
+            jWater.codeConstant[2] = water.codeConstant[2];
+            jWater.codeConstant[3] = water.codeConstant[3];
+
+            if (water.H0)
+            {
+                const auto count = water.M * water.N;
+                jWater.h0 = base64::EncodeBase64(water.H0, sizeof(complex_s) * count);
+            }
+
+            if (water.wTerm)
+            {
+                const auto count = water.M * water.N;
+                jWater.wTerm = base64::EncodeBase64(water.wTerm, sizeof(float) * count);
+            }
+        }
+
+        void CreateJsonTexture(JsonTexture& jTextureDef, const MaterialTextureDef& textureDef) const
+        {
+            std::string textureDefName;
+            if (m_material_constants.GetTextureDefName(textureDef.nameHash, textureDefName))
+            {
+                jTextureDef.name = textureDefName;
+            }
+            else
+            {
+                jTextureDef.nameHash = textureDef.nameHash;
+                jTextureDef.nameStart = std::string(1u, textureDef.nameStart);
+                jTextureDef.nameEnd = std::string(1u, textureDef.nameEnd);
+            }
+
+            jTextureDef.semantic = static_cast<TextureSemantic>(textureDef.semantic);
+
+            CreateJsonSamplerState(jTextureDef.samplerState, textureDef.samplerState);
+
+            if (textureDef.semantic == TS_WATER_MAP)
+            {
+                if (textureDef.u.water)
+                {
+                    const auto& water = *textureDef.u.water;
+                    if (water.image && water.image->name)
+                        jTextureDef.image = AssetName(water.image->name);
+
+                    JsonWater jWater;
+                    CreateJsonWater(jWater, water);
+
+                    jTextureDef.water = std::move(jWater);
+                }
+            }
+            else
+            {
+                if (textureDef.u.image && textureDef.u.image->name)
+                    jTextureDef.image = AssetName(textureDef.u.image->name);
+            }
+        }
+
+        void CreateJsonConstant(JsonConstant& jConstantDef, const MaterialConstantDef& constantDef) const
+        {
+            const auto fragmentLength = strnlen(constantDef.name, std::extent_v<decltype(MaterialConstantDef::name)>);
+            const std::string nameFragment(constantDef.name, fragmentLength);
+            std::string knownConstantName;
+
+            if (fragmentLength < std::extent_v<decltype(MaterialConstantDef::name)> || Common::R_HashString(nameFragment.c_str(), 0) == constantDef.nameHash)
+            {
+                jConstantDef.name = nameFragment;
+            }
+            else if (m_material_constants.GetConstantName(constantDef.nameHash, knownConstantName))
+            {
+                jConstantDef.name = knownConstantName;
+            }
+            else
+            {
+                jConstantDef.nameHash = constantDef.nameHash;
+                jConstantDef.nameFragment = nameFragment;
+            }
+
+            jConstantDef.literal = std::vector({
+                constantDef.literal.x,
+                constantDef.literal.y,
+                constantDef.literal.z,
+                constantDef.literal.w,
+            });
+        }
+
+        static void CreateJsonStencil(JsonStencil& jStencil, const unsigned pass, const unsigned fail, const unsigned zFail, const unsigned func)
+        {
+            jStencil.pass = static_cast<GfxStencilOp>(pass);
+            jStencil.fail = static_cast<GfxStencilOp>(fail);
+            jStencil.zfail = static_cast<GfxStencilOp>(zFail);
+            jStencil.func = static_cast<GfxStencilFunc>(func);
+        }
+
+        static void CreateJsonStateBitsTableEntry(JsonStateBitsTableEntry& jStateBitsTableEntry, const GfxStateBits& stateBitsTableEntry)
+        {
+            const auto& structured = stateBitsTableEntry.loadBits.structured;
+
+            jStateBitsTableEntry.srcBlendRgb = static_cast<GfxBlend>(structured.srcBlendRgb);
+            jStateBitsTableEntry.dstBlendRgb = static_cast<GfxBlend>(structured.dstBlendRgb);
+            jStateBitsTableEntry.blendOpRgb = static_cast<GfxBlendOp>(structured.blendOpRgb);
+
+            assert(structured.alphaTestDisabled || structured.alphaTest == GFXS_ALPHA_TEST_GT_0 || structured.alphaTest == GFXS_ALPHA_TEST_LT_128
+                   || structured.alphaTest == GFXS_ALPHA_TEST_GE_128);
+            if (structured.alphaTestDisabled)
+                jStateBitsTableEntry.alphaTest = JsonAlphaTest::DISABLED;
+            else if (structured.alphaTest == GFXS_ALPHA_TEST_GT_0)
+                jStateBitsTableEntry.alphaTest = JsonAlphaTest::GT0;
+            else if (structured.alphaTest == GFXS_ALPHA_TEST_LT_128)
+                jStateBitsTableEntry.alphaTest = JsonAlphaTest::LT128;
+            else if (structured.alphaTest == GFXS_ALPHA_TEST_GE_128)
+                jStateBitsTableEntry.alphaTest = JsonAlphaTest::GE128;
+            else
+                jStateBitsTableEntry.alphaTest = JsonAlphaTest::INVALID;
+
+            assert(structured.cullFace == GFXS_CULL_NONE || structured.cullFace == GFXS_CULL_BACK || structured.cullFace == GFXS_CULL_FRONT);
+            if (structured.cullFace == GFXS_CULL_NONE)
+                jStateBitsTableEntry.cullFace = JsonCullFace::NONE;
+            else if (structured.cullFace == GFXS_CULL_BACK)
+                jStateBitsTableEntry.cullFace = JsonCullFace::BACK;
+            else if (structured.cullFace == GFXS_CULL_FRONT)
+                jStateBitsTableEntry.cullFace = JsonCullFace::FRONT;
+            else
+                jStateBitsTableEntry.cullFace = JsonCullFace::INVALID;
+
+            jStateBitsTableEntry.srcBlendAlpha = static_cast<GfxBlend>(structured.srcBlendAlpha);
+            jStateBitsTableEntry.dstBlendAlpha = static_cast<GfxBlend>(structured.dstBlendAlpha);
+            jStateBitsTableEntry.blendOpAlpha = static_cast<GfxBlendOp>(structured.blendOpAlpha);
+            jStateBitsTableEntry.colorWriteRgb = structured.colorWriteRgb;
+            jStateBitsTableEntry.colorWriteAlpha = structured.colorWriteAlpha;
+            jStateBitsTableEntry.gammaWrite = structured.gammaWrite;
+            jStateBitsTableEntry.polymodeLine = structured.polymodeLine;
+            jStateBitsTableEntry.depthWrite = structured.depthWrite;
+
+            assert(structured.depthTestDisabled || structured.depthTest == GFXS_DEPTHTEST_ALWAYS || structured.depthTest == GFXS_DEPTHTEST_LESS
+                   || structured.depthTest == GFXS_DEPTHTEST_EQUAL || structured.depthTest == GFXS_DEPTHTEST_LESSEQUAL);
+            if (structured.depthTestDisabled)
+                jStateBitsTableEntry.depthTest = JsonDepthTest::DISABLED;
+            else if (structured.depthTest == GFXS_DEPTHTEST_ALWAYS)
+                jStateBitsTableEntry.depthTest = JsonDepthTest::ALWAYS;
+            else if (structured.depthTest == GFXS_DEPTHTEST_LESS)
+                jStateBitsTableEntry.depthTest = JsonDepthTest::LESS;
+            else if (structured.depthTest == GFXS_DEPTHTEST_EQUAL)
+                jStateBitsTableEntry.depthTest = JsonDepthTest::EQUAL;
+            else if (structured.depthTest == GFXS_DEPTHTEST_LESSEQUAL)
+                jStateBitsTableEntry.depthTest = JsonDepthTest::LESS_EQUAL;
+            else
+                jStateBitsTableEntry.depthTest = JsonDepthTest::INVALID;
+
+            jStateBitsTableEntry.polygonOffset = static_cast<GfxPolygonOffset_e>(structured.polygonOffset);
+
+            if (structured.stencilFrontEnabled)
+            {
+                JsonStencil jStencilFront;
+                CreateJsonStencil(
+                    jStencilFront, structured.stencilFrontPass, structured.stencilFrontFail, structured.stencilFrontZFail, structured.stencilFrontFunc);
+                jStateBitsTableEntry.stencilFront = jStencilFront;
+            }
+
+            if (structured.stencilBackEnabled)
+            {
+                JsonStencil jStencilBack;
+                CreateJsonStencil(
+                    jStencilBack, structured.stencilBackPass, structured.stencilBackFail, structured.stencilBackZFail, structured.stencilBackFunc);
+                jStateBitsTableEntry.stencilBack = jStencilBack;
+            }
+        }
+
+        void CreateJsonMaterial(JsonMaterial& jMaterial, const Material& material) const
+        {
+            CreateJsonGameFlags(jMaterial, material.info.gameFlags);
+            jMaterial.sortKey = material.info.sortKey;
+
+            jMaterial.textureAtlas = JsonTextureAtlas();
+            jMaterial.textureAtlas->rows = material.info.textureAtlasRowCount;
+            jMaterial.textureAtlas->columns = material.info.textureAtlasColumnCount;
+
+            jMaterial.surfaceTypeBits = material.info.surfaceTypeBits;
+
+            jMaterial.stateBitsEntry.resize(std::extent_v<decltype(Material::stateBitsEntry)>);
+            for (auto i = 0u; i < std::extent_v<decltype(Material::stateBitsEntry)>; i++)
+                jMaterial.stateBitsEntry[i] = material.stateBitsEntry[i];
+
+            jMaterial.stateFlags = material.stateFlags;
+            jMaterial.cameraRegion = static_cast<GfxCameraRegionType>(material.cameraRegion);
+
+            if (material.techniqueSet && material.techniqueSet->name)
+                jMaterial.techniqueSet = AssetName(material.techniqueSet->name);
+
+            jMaterial.textures.resize(material.textureCount);
+            for (auto i = 0u; i < material.textureCount; i++)
+                CreateJsonTexture(jMaterial.textures[i], material.textureTable[i]);
+
+            jMaterial.constants.resize(material.constantCount);
+            for (auto i = 0u; i < material.constantCount; i++)
+                CreateJsonConstant(jMaterial.constants[i], material.constantTable[i]);
+
+            jMaterial.stateBits.resize(material.stateBitsCount);
+            for (auto i = 0u; i < material.stateBitsCount; i++)
+                CreateJsonStateBitsTableEntry(jMaterial.stateBits[i], material.stateBitsTable[i]);
+        }
+
+        std::ostream& m_stream;
+        const MaterialConstantZoneState& m_material_constants;
+    };
+} // namespace
+
+namespace IW5
+{
+    void DumpMaterialAsJson(std::ostream& stream, const Material* material, AssetDumpingContext& context)
+    {
+        const JsonDumper dumper(context, stream);
+        dumper.Dump(material);
+    }
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.h b/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.h
new file mode 100644
index 00000000..c7869286
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/Material/JsonMaterialWriter.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "Dumping/AssetDumpingContext.h"
+#include "Game/IW5/IW5.h"
+
+#include <ostream>
+
+namespace IW5
+{
+    void DumpMaterialAsJson(std::ostream& stream, const Material* material, AssetDumpingContext& context);
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.cpp b/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.cpp
new file mode 100644
index 00000000..033421f8
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.cpp
@@ -0,0 +1,236 @@
+#include "MaterialConstantZoneState.h"
+
+#include "Game/IW5/CommonIW5.h"
+#include "Game/IW5/GameAssetPoolIW5.h"
+#include "Game/IW5/GameIW5.h"
+#include "ObjWriting.h"
+
+namespace IW5
+{
+    const char* KNOWN_CONSTANT_NAMES[]{
+        "worldViewProjectionMatrix",
+        "worldViewMatrix2",
+        "worldViewMatrix1",
+        "worldViewMatrix",
+        "worldOutdoorLookupMatrix",
+        "worldMatrix",
+        "waterColor",
+        "viewportDimensions",
+        "viewProjectionMatrix",
+        "uvScale",
+        "uvAnimParms",
+        "thermalColorOffset",
+        "sunShadowmapPixelAdjust",
+        "ssaoParms",
+        "spotShadowmapPixelAdjust",
+        "shadowmapSwitchPartition",
+        "shadowmapScale",
+        "shadowmapPolygonOffset",
+        "shadowLookupMatrix",
+        "renderTargetSize",
+        "renderSourceSize",
+        "projectionMatrix",
+        "playlistPopulationParams",
+        "pixelCostFracs",
+        "pixelCostDecode",
+        "particleCloudSparkColor2",
+        "particleCloudSparkColor1",
+        "particleCloudSparkColor0",
+        "particleCloudMatrix2",
+        "particleCloudMatrix1",
+        "particleCloudMatrix",
+        "particleCloudColor",
+        "outdoorFeatherParms",
+        "oceanUVAnimParmPaintedFoam",
+        "oceanUVAnimParmOctave2",
+        "oceanUVAnimParmOctave1",
+        "oceanUVAnimParmOctave0",
+        "oceanUVAnimParmFoam",
+        "oceanUVAnimParmDetail1",
+        "oceanUVAnimParmDetail0",
+        "oceanScrollParms",
+        "oceanMiscParms",
+        "oceanFoamParms",
+        "oceanAmplitude",
+        "materialColor",
+        "lightprobeAmbient",
+        "lightingLookupScale",
+        "lightSpotFactors",
+        "lightSpotDir",
+        "lightSpecular",
+        "lightPosition",
+        "lightFalloffPlacement",
+        "lightDiffuse",
+        "inverseWorldViewMatrix",
+        "inverseViewProjectionMatrix",
+        "inverseTransposeWorldViewMatrix",
+        "heatMapDetail",
+        "glowSetup",
+        "glowApply",
+        "gameTime",
+        "fullscreenDistortion",
+        "fogSunDir",
+        "fogSunConsts",
+        "fogSunColorLinear",
+        "fogSunColorGamma",
+        "fogConsts",
+        "fogColorLinear",
+        "fogColorGamma",
+        "flagParms",
+        "filterTap",
+        "featherParms",
+        "falloffParms",
+        "falloffEndColor",
+        "falloffBeginColor",
+        "fadeEffect",
+        "eyeOffsetParms",
+        "eyeOffset",
+        "envMapParms",
+        "dustTint",
+        "dustParms",
+        "dustEyeParms",
+        "dofRowDelta",
+        "dofLerpScale",
+        "dofLerpBias",
+        "dofEquationViewModelAndFarBlur",
+        "dofEquationScene",
+        "distortionScale",
+        "detailScale",
+        "depthFromClip",
+        "debugBumpmap",
+        "colorTintQuadraticDelta",
+        "colorTintDelta",
+        "colorTintBase",
+        "colorSaturationR",
+        "colorSaturationG",
+        "colorSaturationB",
+        "colorObjMin",
+        "colorObjMax",
+        "colorMatrixR",
+        "colorMatrixG",
+        "colorMatrixB",
+        "colorBias",
+        "codeMeshArg",
+        "clipSpaceLookupScale",
+        "clipSpaceLookupOffset",
+        "baseLightingCoords",
+    };
+
+    const char* KNOWN_TEXTURE_DEF_NAMES[]{
+        "attenuation",
+        "attenuationSampler",
+        "cinematicA",
+        "cinematicASampler",
+        "cinematicCb",
+        "cinematicCbSampler",
+        "cinematicCr",
+        "cinematicCrSampler",
+        "cinematicY",
+        "cinematicYSampler",
+        "colorMap",
+        "colorMap1",
+        "colorMap2",
+        "colorMapPostSun",
+        "colorMapPostSunSampler",
+        "colorMapSampler",
+        "colorMapSampler1",
+        "colorMapSampler2",
+        "cucoloris",
+        "cucolorisSampler",
+        "detailMap",
+        "detailMapSampler",
+        "dust",
+        "dustSampler",
+        "fadeMap",
+        "fadeMapSampler",
+        "floatZ",
+        "floatZSampler",
+        "grainMap",
+        "grainMapSampler",
+        "halfParticleColor",
+        "halfParticleColorSampler",
+        "halfParticleDepth",
+        "halfParticleDepthSampler",
+        "heatmap",
+        "heatmapSampler",
+        "lightmapPrimary",
+        "lightmapSamplerPrimary",
+        "lightmapSamplerSecondary",
+        "lightmapSecondary",
+        "lookupMap",
+        "lookupMapSampler",
+        "modelLighting",
+        "modelLightingSampler",
+        "normalMap",
+        "normalMapSampler",
+        "oceanColorRamp",
+        "oceanColorRampSampler",
+        "oceanDetailNormal",
+        "oceanDetailNormalSampler",
+        "oceanDisplacement",
+        "oceanDisplacementSampler",
+        "oceanEnv",
+        "oceanEnvSampler",
+        "oceanFoam",
+        "oceanFoamSampler",
+        "oceanHeightNormal",
+        "oceanHeightNormalSampler",
+        "oceanPaintedFoam",
+        "oceanPaintedFoamSampler",
+        "outdoorMap",
+        "outdoorMapSampler",
+        "population",
+        "populationSampler",
+        "reflectionProbe",
+        "reflectionProbeSampler",
+        "shadowmapSamplerSpot",
+        "shadowmapSamplerSun",
+        "shadowmapSpot",
+        "shadowmapSun",
+        "skyMap",
+        "skyMapSampler",
+        "specularMap",
+        "specularMapSampler",
+        "ssao",
+        "ssaoSampler",
+        "worldMap",
+        "worldMapSampler",
+    };
+
+    void MaterialConstantZoneState::ExtractNamesFromZoneInternal()
+    {
+        for (const auto* zone : g_GameIW5.GetZones())
+        {
+            const auto* iw5AssetPools = dynamic_cast<const GameAssetPoolIW5*>(zone->m_pools.get());
+            if (!iw5AssetPools)
+                return;
+
+            for (const auto* vertexShaderAsset : *iw5AssetPools->m_material_vertex_shader)
+            {
+                const auto* vertexShader = vertexShaderAsset->Asset();
+                if (ShouldDumpFromStruct(vertexShader))
+                    ExtractNamesFromShader(vertexShader->prog.loadDef.program, static_cast<size_t>(vertexShader->prog.loadDef.programSize) * sizeof(uint32_t));
+            }
+
+            for (const auto* pixelShaderAsset : *iw5AssetPools->m_material_pixel_shader)
+            {
+                const auto* pixelShader = pixelShaderAsset->Asset();
+                if (ShouldDumpFromStruct(pixelShader))
+                    ExtractNamesFromShader(pixelShader->prog.loadDef.program, static_cast<size_t>(pixelShader->prog.loadDef.programSize) * sizeof(uint32_t));
+            }
+        }
+    }
+
+    void MaterialConstantZoneState::AddStaticKnownNames()
+    {
+        for (const auto* knownConstantName : KNOWN_CONSTANT_NAMES)
+            AddConstantName(knownConstantName);
+        for (const auto* knownTextureDefName : KNOWN_TEXTURE_DEF_NAMES)
+            AddTextureDefName(knownTextureDefName);
+    }
+
+    unsigned MaterialConstantZoneState::HashString(const std::string& str)
+    {
+        return Common::R_HashString(str.c_str());
+    }
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.h b/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.h
new file mode 100644
index 00000000..d8a33350
--- /dev/null
+++ b/src/ObjWriting/Game/IW5/Material/MaterialConstantZoneState.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "Material/AbstractMaterialConstantZoneState.h"
+
+#include <string>
+
+namespace IW5
+{
+    class MaterialConstantZoneState final : public AbstractMaterialConstantZoneStateDx9
+    {
+    protected:
+        void ExtractNamesFromZoneInternal() override;
+        void AddStaticKnownNames() override;
+        unsigned HashString(const std::string& str) override;
+    };
+} // namespace IW5
diff --git a/src/ObjWriting/Game/IW5/ZoneDumperIW5.cpp b/src/ObjWriting/Game/IW5/ZoneDumperIW5.cpp
index ea18d89c..f102023c 100644
--- a/src/ObjWriting/Game/IW5/ZoneDumperIW5.cpp
+++ b/src/ObjWriting/Game/IW5/ZoneDumperIW5.cpp
@@ -5,6 +5,7 @@
 #include "AssetDumpers/AssetDumperLeaderboardDef.h"
 #include "AssetDumpers/AssetDumperLoadedSound.h"
 #include "AssetDumpers/AssetDumperLocalizeEntry.h"
+#include "AssetDumpers/AssetDumperMaterial.h"
 #include "AssetDumpers/AssetDumperMenuDef.h"
 #include "AssetDumpers/AssetDumperMenuList.h"
 #include "AssetDumpers/AssetDumperRawFile.h"
@@ -39,7 +40,7 @@ bool ZoneDumper::DumpZone(AssetDumpingContext& context) const
     // DUMP_ASSET_POOL(AssetDumperXAnimParts, m_xanim_parts, ASSET_TYPE_XANIMPARTS)
     // DUMP_ASSET_POOL(AssetDumperXModelSurfs, m_xmodel_surfs, ASSET_TYPE_XMODEL_SURFS)
     DUMP_ASSET_POOL(AssetDumperXModel, m_xmodel, ASSET_TYPE_XMODEL)
-    // DUMP_ASSET_POOL(AssetDumperMaterial, m_material, ASSET_TYPE_MATERIAL)
+    DUMP_ASSET_POOL(AssetDumperMaterial, m_material, ASSET_TYPE_MATERIAL)
     // DUMP_ASSET_POOL(AssetDumperMaterialPixelShader, m_material_pixel_shader, ASSET_TYPE_PIXELSHADER)
     // DUMP_ASSET_POOL(AssetDumperMaterialVertexShader, m_material_vertex_shader, ASSET_TYPE_VERTEXSHADER)
     // DUMP_ASSET_POOL(AssetDumperMaterialVertexDeclaration, m_material_vertex_decl, ASSET_TYPE_VERTEXDECL)

From 7b28b574d24bbb8fae57bb9bf2f5becaced4238d Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sun, 22 Sep 2024 16:59:31 +0200
Subject: [PATCH 16/17] feat: load iw5 materials from json

---
 .../IW5/AssetLoaders/AssetLoaderMaterial.cpp  |  47 +-
 .../IW5/AssetLoaders/AssetLoaderMaterial.h    |   7 +-
 .../Game/IW5/Material/JsonMaterialLoader.cpp  | 447 ++++++++++++++++++
 .../Game/IW5/Material/JsonMaterialLoader.h    |  13 +
 4 files changed, 509 insertions(+), 5 deletions(-)
 create mode 100644 src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp
 create mode 100644 src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h

diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp
index 6075ad87..13170e20 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.cpp
@@ -1,18 +1,57 @@
 #include "AssetLoaderMaterial.h"
 
 #include "Game/IW5/IW5.h"
-#include "ObjLoading.h"
+#include "Game/IW5/Material/JsonMaterialLoader.h"
 #include "Pool/GlobalAssetPool.h"
 
 #include <cstring>
+#include <format>
+#include <iostream>
 
 using namespace IW5;
 
 void* AssetLoaderMaterial::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
 {
-    auto* material = memory->Create<Material>();
-    memset(material, 0, sizeof(Material));
+    auto* asset = memory->Alloc<AssetMaterial::Type>();
+    asset->info.name = memory->Dup(assetName.c_str());
+    return asset;
+}
+
+bool AssetLoaderMaterial::CanLoadFromRaw() const
+{
+    return true;
+}
+
+std::string AssetLoaderMaterial::GetFileNameForAsset(const std::string& assetName)
+{
+    std::string sanitizedFileName(assetName);
+    if (sanitizedFileName[0] == '*')
+    {
+        std::ranges::replace(sanitizedFileName, '*', '_');
+        const auto parenthesisPos = sanitizedFileName.find('(');
+        if (parenthesisPos != std::string::npos)
+            sanitizedFileName.erase(parenthesisPos);
+        sanitizedFileName = "generated/" + sanitizedFileName;
+    }
+
+    return std::format("materials/{}.json", sanitizedFileName);
+}
+
+bool AssetLoaderMaterial::LoadFromRaw(
+    const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const
+{
+    const auto file = searchPath->Open(GetFileNameForAsset(assetName));
+    if (!file.IsOpen())
+        return false;
+
+    auto* material = memory->Alloc<Material>();
     material->info.name = memory->Dup(assetName.c_str());
 
-    return material;
+    std::vector<XAssetInfoGeneric*> dependencies;
+    if (LoadMaterialAsJson(*file.m_stream, *material, memory, manager, dependencies))
+        manager->AddAsset<AssetMaterial>(assetName, material, std::move(dependencies));
+    else
+        std::cerr << std::format("Failed to load material \"{}\"\n", assetName);
+
+    return true;
 }
diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h
index b61c8965..f9f45d71 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderMaterial.h
@@ -1,6 +1,6 @@
 #pragma once
-
 #include "AssetLoading/BasicAssetLoader.h"
+#include "AssetLoading/IAssetLoadingManager.h"
 #include "Game/IW5/IW5.h"
 #include "SearchPath/ISearchPath.h"
 
@@ -8,7 +8,12 @@ namespace IW5
 {
     class AssetLoaderMaterial final : public BasicAssetLoader<AssetMaterial>
     {
+        static std::string GetFileNameForAsset(const std::string& assetName);
+
     public:
         _NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override;
+        _NODISCARD bool CanLoadFromRaw() const override;
+        bool
+            LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override;
     };
 } // namespace IW5
diff --git a/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp
new file mode 100644
index 00000000..a567e9dd
--- /dev/null
+++ b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.cpp
@@ -0,0 +1,447 @@
+#include "JsonMaterialLoader.h"
+
+#include "Game/IW5/CommonIW5.h"
+#include "Game/IW5/Material/JsonMaterial.h"
+#include "Impl/Base64.h"
+
+#include <format>
+#include <iostream>
+#include <nlohmann/json.hpp>
+
+using namespace nlohmann;
+using namespace IW5;
+
+namespace
+{
+    class JsonLoader
+    {
+    public:
+        JsonLoader(std::istream& stream, MemoryManager& memory, IAssetLoadingManager& manager, std::vector<XAssetInfoGeneric*>& dependencies)
+            : m_stream(stream),
+              m_memory(memory),
+              m_manager(manager),
+              m_dependencies(dependencies)
+
+        {
+        }
+
+        bool Load(Material& material) const
+        {
+            const auto jRoot = json::parse(m_stream);
+            std::string game;
+            std::string type;
+            unsigned version;
+
+            jRoot.at("_type").get_to(type);
+            jRoot.at("_game").get_to(game);
+            jRoot.at("_version").get_to(version);
+
+            if (type != "material" || version != 1u || game != "iw5")
+            {
+                std::cerr << std::format("Tried to load material \"{}\" but did not find expected type material of version 1\n", material.info.name);
+                return false;
+            }
+
+            try
+            {
+                const auto jMaterial = jRoot.get<JsonMaterial>();
+                return CreateMaterialFromJson(jMaterial, material);
+            }
+            catch (const json::exception& e)
+            {
+                std::cerr << std::format("Failed to parse json of material: {}\n", e.what());
+            }
+
+            return false;
+        }
+
+    private:
+        static void PrintError(const Material& material, const std::string& message)
+        {
+            std::cerr << std::format("Cannot load material \"{}\": {}\n", material.info.name, message);
+        }
+
+        static bool CreateGameFlagsFromJson(const JsonMaterial& jMaterial, unsigned char& gameFlags)
+        {
+            for (const auto gameFlag : jMaterial.gameFlags)
+                gameFlags |= gameFlag;
+
+            return true;
+        }
+
+        static void CreateSamplerStateFromJson(const JsonSamplerState& jSamplerState, MaterialTextureDefSamplerState& samplerState)
+        {
+            samplerState.filter = jSamplerState.filter;
+            samplerState.mipMap = jSamplerState.mipMap;
+            samplerState.clampU = jSamplerState.clampU;
+            samplerState.clampV = jSamplerState.clampV;
+            samplerState.clampW = jSamplerState.clampW;
+        }
+
+        bool CreateWaterFromJson(const JsonWater& jWater, water_t& water, const Material& material) const
+        {
+            water.writable.floatTime = jWater.floatTime;
+            water.M = jWater.m;
+            water.N = jWater.n;
+            water.Lx = jWater.lx;
+            water.Lz = jWater.lz;
+            water.gravity = jWater.gravity;
+            water.windvel = jWater.windvel;
+            water.winddir[0] = jWater.winddir[0];
+            water.winddir[1] = jWater.winddir[1];
+            water.amplitude = jWater.amplitude;
+            water.codeConstant[0] = jWater.codeConstant[0];
+            water.codeConstant[1] = jWater.codeConstant[1];
+            water.codeConstant[2] = jWater.codeConstant[2];
+            water.codeConstant[3] = jWater.codeConstant[3];
+
+            const auto expectedH0Size = water.M * water.N * sizeof(complex_s);
+            if (expectedH0Size > 0)
+            {
+                water.H0 = m_memory.Alloc<complex_s>(water.M * water.N);
+                const auto h0Size = base64::DecodeBase64(jWater.h0.data(), jWater.h0.size(), water.H0, expectedH0Size);
+                if (h0Size != expectedH0Size)
+                {
+                    PrintError(material, std::format("Water h0 size {} does not match expected {}", h0Size, expectedH0Size));
+                    return false;
+                }
+            }
+
+            const auto expectedWTermSize = water.M * water.N * sizeof(float);
+            if (expectedWTermSize > 0)
+            {
+                water.wTerm = m_memory.Alloc<float>(water.M * water.N);
+                auto wTermSize = base64::DecodeBase64(jWater.wTerm.data(), jWater.wTerm.size(), water.wTerm, expectedWTermSize);
+                if (wTermSize != expectedWTermSize)
+                {
+                    PrintError(material, std::format("Water wTerm size {} does not match expected {}", wTermSize, expectedWTermSize));
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        bool CreateTextureDefFromJson(const JsonTexture& jTexture, MaterialTextureDef& textureDef, const Material& material) const
+        {
+            if (jTexture.name)
+            {
+                if (jTexture.name->empty())
+                {
+                    PrintError(material, "textureDef name cannot be empty");
+                    return false;
+                }
+
+                textureDef.nameStart = jTexture.name.value()[0];
+                textureDef.nameEnd = jTexture.name.value()[jTexture.name->size() - 1];
+                textureDef.nameHash = Common::R_HashString(jTexture.name.value().c_str(), 0);
+            }
+            else
+            {
+                if (!jTexture.nameStart || !jTexture.nameEnd || !jTexture.nameHash)
+                {
+                    PrintError(material, "textureDefs without name must have nameStart, nameEnd and nameHash");
+                    return false;
+                }
+
+                if (jTexture.nameStart->size() != 1 || jTexture.nameEnd->size() != 1)
+                {
+                    PrintError(material, "nameStart and nameEnd must be a string of exactly one character");
+                    return false;
+                }
+
+                textureDef.nameStart = jTexture.nameStart.value()[0];
+                textureDef.nameEnd = jTexture.nameEnd.value()[0];
+                textureDef.nameHash = jTexture.nameHash.value();
+            }
+
+            CreateSamplerStateFromJson(jTexture.samplerState, textureDef.samplerState);
+
+            textureDef.semantic = jTexture.semantic;
+
+            auto* imageAsset = m_manager.LoadDependency<AssetImage>(jTexture.image);
+            if (!imageAsset)
+            {
+                PrintError(material, std::format("Could not find textureDef image: {}", jTexture.image));
+                return false;
+            }
+            m_dependencies.push_back(imageAsset);
+
+            if (jTexture.water)
+            {
+                if (jTexture.semantic != TS_WATER_MAP)
+                {
+                    PrintError(material, "Only textureDefs with semantic waterMap can define water params");
+                    return false;
+                }
+            }
+            else
+            {
+                if (jTexture.semantic == TS_WATER_MAP)
+                {
+                    PrintError(material, "TextureDefs with semantic waterMap must define water params");
+                    return false;
+                }
+            }
+
+            if (jTexture.water)
+            {
+                auto* water = m_memory.Alloc<water_t>();
+                water->image = imageAsset->Asset();
+
+                if (!CreateWaterFromJson(*jTexture.water, *water, material))
+                    return false;
+
+                textureDef.u.water = water;
+            }
+            else
+                textureDef.u.image = imageAsset->Asset();
+
+            return true;
+        }
+
+        static bool CreateConstantDefFromJson(const JsonConstant& jConstant, MaterialConstantDef& constantDef, const Material& material)
+        {
+            if (jConstant.name)
+            {
+                const auto copyCount = std::min(jConstant.name->size() + 1, std::extent_v<decltype(MaterialConstantDef::name)>);
+                strncpy(constantDef.name, jConstant.name->c_str(), copyCount);
+                if (copyCount < std::extent_v<decltype(MaterialConstantDef::name)>)
+                    memset(&constantDef.name[copyCount], 0, std::extent_v<decltype(MaterialConstantDef::name)> - copyCount);
+                constantDef.nameHash = Common::R_HashString(jConstant.name->c_str(), 0);
+            }
+            else
+            {
+                if (!jConstant.nameFragment || !jConstant.nameHash)
+                {
+                    PrintError(material, "constantDefs without name must have nameFragment and nameHash");
+                    return false;
+                }
+
+                const auto copyCount = std::min(jConstant.nameFragment->size() + 1, std::extent_v<decltype(MaterialConstantDef::name)>);
+                strncpy(constantDef.name, jConstant.nameFragment->c_str(), copyCount);
+                if (copyCount < std::extent_v<decltype(MaterialConstantDef::name)>)
+                    memset(&constantDef.name[copyCount], 0, std::extent_v<decltype(MaterialConstantDef::name)> - copyCount);
+                constantDef.nameHash = jConstant.nameHash.value();
+            }
+
+            if (jConstant.literal.size() != 4)
+            {
+                PrintError(material, "constantDef literal must be array of size 4");
+                return false;
+            }
+
+            constantDef.literal.x = jConstant.literal[0];
+            constantDef.literal.y = jConstant.literal[1];
+            constantDef.literal.z = jConstant.literal[2];
+            constantDef.literal.w = jConstant.literal[3];
+
+            return true;
+        }
+
+        static bool
+            CreateStateBitsTableEntryFromJson(const JsonStateBitsTableEntry& jStateBitsTableEntry, GfxStateBits& stateBitsTableEntry, const Material& material)
+        {
+            auto& structured = stateBitsTableEntry.loadBits.structured;
+
+            structured.srcBlendRgb = jStateBitsTableEntry.srcBlendRgb;
+            structured.dstBlendRgb = jStateBitsTableEntry.dstBlendRgb;
+            structured.blendOpRgb = jStateBitsTableEntry.blendOpRgb;
+
+            if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::DISABLED)
+            {
+                structured.alphaTestDisabled = 1;
+                structured.alphaTest = 0;
+            }
+            else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::GT0)
+            {
+                structured.alphaTestDisabled = 0;
+                structured.alphaTest = GFXS_ALPHA_TEST_GT_0;
+            }
+            else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::LT128)
+            {
+                structured.alphaTestDisabled = 0;
+                structured.alphaTest = GFXS_ALPHA_TEST_LT_128;
+            }
+            else if (jStateBitsTableEntry.alphaTest == JsonAlphaTest::GE128)
+            {
+                structured.alphaTestDisabled = 0;
+                structured.alphaTest = GFXS_ALPHA_TEST_GE_128;
+            }
+            else
+            {
+                PrintError(material, "Invalid value for alphaTest");
+                return false;
+            }
+
+            if (jStateBitsTableEntry.cullFace == JsonCullFace::NONE)
+                structured.cullFace = GFXS_CULL_NONE;
+            else if (jStateBitsTableEntry.cullFace == JsonCullFace::BACK)
+                structured.cullFace = GFXS_CULL_BACK;
+            else if (jStateBitsTableEntry.cullFace == JsonCullFace::FRONT)
+                structured.cullFace = GFXS_CULL_FRONT;
+            else
+            {
+                PrintError(material, "Invalid value for cull face");
+                return false;
+            }
+
+            structured.srcBlendAlpha = jStateBitsTableEntry.srcBlendAlpha;
+            structured.dstBlendAlpha = jStateBitsTableEntry.dstBlendAlpha;
+            structured.blendOpAlpha = jStateBitsTableEntry.blendOpAlpha;
+            structured.colorWriteRgb = jStateBitsTableEntry.colorWriteRgb;
+            structured.colorWriteAlpha = jStateBitsTableEntry.colorWriteAlpha;
+            structured.gammaWrite = jStateBitsTableEntry.gammaWrite;
+            structured.polymodeLine = jStateBitsTableEntry.polymodeLine;
+            structured.depthWrite = jStateBitsTableEntry.depthWrite;
+
+            if (jStateBitsTableEntry.depthTest == JsonDepthTest::DISABLED)
+                structured.depthTestDisabled = 1;
+            else if (jStateBitsTableEntry.depthTest == JsonDepthTest::ALWAYS)
+                structured.depthTest = GFXS_DEPTHTEST_ALWAYS;
+            else if (jStateBitsTableEntry.depthTest == JsonDepthTest::LESS)
+                structured.depthTest = GFXS_DEPTHTEST_LESS;
+            else if (jStateBitsTableEntry.depthTest == JsonDepthTest::EQUAL)
+                structured.depthTest = GFXS_DEPTHTEST_EQUAL;
+            else if (jStateBitsTableEntry.depthTest == JsonDepthTest::LESS_EQUAL)
+                structured.depthTest = GFXS_DEPTHTEST_LESSEQUAL;
+            else
+            {
+                PrintError(material, "Invalid value for depth test");
+                return false;
+            }
+
+            structured.polygonOffset = jStateBitsTableEntry.polygonOffset;
+
+            if (jStateBitsTableEntry.stencilFront)
+            {
+                structured.stencilFrontEnabled = 1;
+                structured.stencilFrontPass = jStateBitsTableEntry.stencilFront->pass;
+                structured.stencilFrontFail = jStateBitsTableEntry.stencilFront->fail;
+                structured.stencilFrontZFail = jStateBitsTableEntry.stencilFront->zfail;
+                structured.stencilFrontFunc = jStateBitsTableEntry.stencilFront->func;
+            }
+
+            if (jStateBitsTableEntry.stencilBack)
+            {
+                structured.stencilBackEnabled = 1;
+                structured.stencilBackPass = jStateBitsTableEntry.stencilBack->pass;
+                structured.stencilBackFail = jStateBitsTableEntry.stencilBack->fail;
+                structured.stencilBackZFail = jStateBitsTableEntry.stencilBack->zfail;
+                structured.stencilBackFunc = jStateBitsTableEntry.stencilBack->func;
+            }
+
+            return true;
+        }
+
+        bool CreateMaterialFromJson(const JsonMaterial& jMaterial, Material& material) const
+        {
+            if (!CreateGameFlagsFromJson(jMaterial, material.info.gameFlags))
+                return false;
+
+            material.info.sortKey = static_cast<unsigned char>(jMaterial.sortKey);
+
+            if (jMaterial.textureAtlas)
+            {
+                material.info.textureAtlasRowCount = jMaterial.textureAtlas->rows;
+                material.info.textureAtlasColumnCount = jMaterial.textureAtlas->columns;
+            }
+            else
+            {
+                material.info.textureAtlasRowCount = 0;
+                material.info.textureAtlasColumnCount = 0;
+            }
+
+            material.info.surfaceTypeBits = jMaterial.surfaceTypeBits;
+
+            if (jMaterial.stateBitsEntry.size() != std::extent_v<decltype(Material::stateBitsEntry)>)
+            {
+                PrintError(material, std::format("StateBitsEntry size is not {}", std::extent_v<decltype(Material::stateBitsEntry)>));
+                return false;
+            }
+            for (auto i = 0u; i < std::extent_v<decltype(Material::stateBitsEntry)>; i++)
+                material.stateBitsEntry[i] = jMaterial.stateBitsEntry[i];
+
+            material.stateFlags = static_cast<unsigned char>(jMaterial.stateFlags);
+            material.cameraRegion = jMaterial.cameraRegion;
+
+            auto* techniqueSet = m_manager.LoadDependency<AssetTechniqueSet>(jMaterial.techniqueSet);
+            if (!techniqueSet)
+            {
+                PrintError(material, "Could not find technique set");
+                return false;
+            }
+            m_dependencies.push_back(techniqueSet);
+            material.techniqueSet = techniqueSet->Asset();
+
+            if (!jMaterial.textures.empty())
+            {
+                material.textureCount = static_cast<unsigned char>(jMaterial.textures.size());
+                material.textureTable = m_memory.Alloc<MaterialTextureDef>(material.textureCount);
+
+                for (auto i = 0u; i < material.textureCount; i++)
+                {
+                    if (!CreateTextureDefFromJson(jMaterial.textures[i], material.textureTable[i], material))
+                        return false;
+                }
+            }
+            else
+            {
+                material.textureCount = 0;
+                material.textureTable = nullptr;
+            }
+
+            if (!jMaterial.constants.empty())
+            {
+                material.constantCount = static_cast<unsigned char>(jMaterial.constants.size());
+                material.constantTable = m_memory.Alloc<MaterialConstantDef>(material.constantCount);
+
+                for (auto i = 0u; i < material.constantCount; i++)
+                {
+                    if (!CreateConstantDefFromJson(jMaterial.constants[i], material.constantTable[i], material))
+                        return false;
+                }
+            }
+            else
+            {
+                material.constantCount = 0;
+                material.constantTable = nullptr;
+            }
+
+            if (!jMaterial.stateBits.empty())
+            {
+                material.stateBitsCount = static_cast<unsigned char>(jMaterial.stateBits.size());
+                material.stateBitsTable = m_memory.Alloc<GfxStateBits>(material.stateBitsCount);
+
+                for (auto i = 0u; i < material.stateBitsCount; i++)
+                {
+                    if (!CreateStateBitsTableEntryFromJson(jMaterial.stateBits[i], material.stateBitsTable[i], material))
+                        return false;
+                }
+            }
+            else
+            {
+                material.stateBitsCount = 0;
+                material.stateBitsTable = nullptr;
+            }
+
+            return true;
+        }
+
+        std::istream& m_stream;
+        MemoryManager& m_memory;
+        IAssetLoadingManager& m_manager;
+        std::vector<XAssetInfoGeneric*>& m_dependencies;
+    };
+} // namespace
+
+namespace IW5
+{
+    bool LoadMaterialAsJson(
+        std::istream& stream, Material& material, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies)
+    {
+        const JsonLoader loader(stream, *memory, *manager, dependencies);
+
+        return loader.Load(material);
+    }
+} // namespace IW5
diff --git a/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h
new file mode 100644
index 00000000..b498852d
--- /dev/null
+++ b/src/ObjLoading/Game/IW5/Material/JsonMaterialLoader.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include "AssetLoading/IAssetLoadingManager.h"
+#include "Game/IW5/IW5.h"
+#include "Utils/MemoryManager.h"
+
+#include <istream>
+
+namespace IW5
+{
+    bool LoadMaterialAsJson(
+        std::istream& stream, Material& material, MemoryManager* memory, IAssetLoadingManager* manager, std::vector<XAssetInfoGeneric*>& dependencies);
+} // namespace IW5

From d4d8e83169c76389e8dd0ace59f47a78655c1c3e Mon Sep 17 00:00:00 2001
From: Jan <jan@laupetin.net>
Date: Sun, 22 Sep 2024 16:59:56 +0200
Subject: [PATCH 17/17] feat: load iw5 images from raw

---
 .../IW5/AssetLoaders/AssetLoaderGfxImage.cpp  | 49 ++++++++++++++++++-
 .../IW5/AssetLoaders/AssetLoaderGfxImage.h    |  5 +-
 2 files changed, 51 insertions(+), 3 deletions(-)

diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.cpp b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.cpp
index 42ea92ac..174b568c 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.cpp
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.cpp
@@ -1,17 +1,62 @@
 #include "AssetLoaderGfxImage.h"
 
 #include "Game/IW5/IW5.h"
-#include "ObjLoading.h"
+#include "Image/IwiLoader.h"
 #include "Pool/GlobalAssetPool.h"
 
 #include <cstring>
+#include <format>
+#include <iostream>
+#include <sstream>
 
 using namespace IW5;
 
 void* AssetLoaderGfxImage::CreateEmptyAsset(const std::string& assetName, MemoryManager* memory)
 {
+    auto* asset = memory->Alloc<AssetImage::Type>();
+    asset->name = memory->Dup(assetName.c_str());
+    return asset;
+}
+
+bool AssetLoaderGfxImage::CanLoadFromRaw() const
+{
+    return true;
+}
+
+bool AssetLoaderGfxImage::LoadFromRaw(
+    const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const
+{
+    const auto fileName = std::format("images/{}.iwi", assetName);
+    const auto file = searchPath->Open(fileName);
+    if (!file.IsOpen())
+        return false;
+
+    const auto fileSize = static_cast<size_t>(file.m_length);
+    const auto fileData = std::make_unique<char[]>(fileSize);
+    file.m_stream->read(fileData.get(), fileSize);
+
+    MemoryManager tempMemory;
+    IwiLoader iwiLoader(&tempMemory);
+    std::istringstream ss(std::string(fileData.get(), fileSize));
+    const auto texture = iwiLoader.LoadIwi(ss);
+    if (!texture)
+    {
+        std::cerr << std::format("Failed to load texture from: {}\n", fileName);
+        return false;
+    }
+
     auto* image = memory->Create<GfxImage>();
     memset(image, 0, sizeof(GfxImage));
+
     image->name = memory->Dup(assetName.c_str());
-    return image;
+    image->noPicmip = !texture->HasMipMaps();
+    image->width = static_cast<uint16_t>(texture->GetWidth());
+    image->height = static_cast<uint16_t>(texture->GetHeight());
+    image->depth = static_cast<uint16_t>(texture->GetDepth());
+
+    image->texture.loadDef = memory->Alloc<GfxImageLoadDef>();
+
+    manager->AddAsset<AssetImage>(assetName, image);
+
+    return true;
 }
diff --git a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.h b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.h
index d334ca35..7f2bec24 100644
--- a/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.h
+++ b/src/ObjLoading/Game/IW5/AssetLoaders/AssetLoaderGfxImage.h
@@ -1,6 +1,6 @@
 #pragma once
-
 #include "AssetLoading/BasicAssetLoader.h"
+#include "AssetLoading/IAssetLoadingManager.h"
 #include "Game/IW5/IW5.h"
 #include "SearchPath/ISearchPath.h"
 
@@ -10,5 +10,8 @@ namespace IW5
     {
     public:
         _NODISCARD void* CreateEmptyAsset(const std::string& assetName, MemoryManager* memory) override;
+        _NODISCARD bool CanLoadFromRaw() const override;
+        bool
+            LoadFromRaw(const std::string& assetName, ISearchPath* searchPath, MemoryManager* memory, IAssetLoadingManager* manager, Zone* zone) const override;
     };
 } // namespace IW5