feat: QOS (007: Quantum of Solace) support (#894)

* feat: QOS (007: Quantum of Solace) support

* feat: add qos localize loader

* feat: add qos stringtable loader

* chore: comment about official zones

* fix: add qos gfximage runtime fields

* chore: remove useless dev time comments

* fix: remove all `static_asserts`s from asset headers causing build fail on x64

* chore: change union ordering of qos `entryInternalData`

* chore: move qos asset type enum to qos.h

* fix: qos and t4 xsurface index buffer

* chore: implement qos MaterialInfo struct

* chore: adjust some gfxworld struct members

* chore: implement qos PhysPreset and PhysConstraints

* chore: fix and adjust slight inconsistencies with qos zone code

* chore: reorder qos ZoneCode.lua entries in order of asset type enum

* chore: remove asset types that are not actual asset types in zones

* chore: check common asset arrays with static_assert

* chore: rename qos assets

* chore: reorder qos in templates to be in alphabetical order

* chore: use function for CopyMipLevel in Dx9TextureLoader

* chore: use templates for qos MapEntsDumper

* chore: add missing braces in XModelToCommonConverter

* chore: add generic way to peek at zone data

* Qos does not use a classic ZoneHeader so code needs to be more generic

* fix: do not use raw_byte and raw_uint typedefs for qos

* fix: not being able to write clipmapmp

* chore: additional research on qos vertex property and stuff

---------

Co-authored-by: Jan Laupetin <[email protected]>
This commit is contained in:
mo
2026-08-05 00:32:23 +02:00
committed by GitHub
co-authored by Jan Laupetin
parent 5c0a7257fd
commit 9f49e0e31c
142 changed files with 6000 additions and 97 deletions
+4 -1
View File
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Font/FontDumper" + GAME + ".cpp"
@@ -8,6 +8,9 @@
#define GAME_LOWER "iw4"
#elif GAME == "IW5"
#define GAME_LOWER "iw5"
#elif GAME == "QOS"
#define FEATURE_QOS
#define GAME_LOWER "qos"
#elif GAME == "T4"
#define GAME_LOWER "t4"
#elif GAME == "T5"
+1 -1
View File
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Font/FontDumper" + GAME + ".h"
@@ -0,0 +1,42 @@
#include "LocalizeDumperQOS.h"
#include "Dumping/Localize/StringFileDumper.h"
#include "Localize/LocalizeCommon.h"
#include "Utils/Logging/Log.h"
#include <format>
using namespace QOS;
namespace localize
{
void DumperQOS::Dump(AssetDumpingContext& context)
{
auto localizeAssets = context.m_zone.m_pools.PoolAssets<AssetLocalize>();
if (localizeAssets.empty())
return;
const auto language = LocalizeCommon::GetNameOfLanguage(context.m_zone.m_language);
const auto assetFile = context.OpenAssetFile(std::format("{}/localizedstrings/{}.str", language, context.m_zone.m_name));
if (assetFile)
{
StringFileDumper stringFileDumper(context.m_zone, *assetFile);
stringFileDumper.SetLanguageName(language);
stringFileDumper.SetConfigFile(R"(C:/trees/cod3/cod3/bin/StringEd.cfg)");
stringFileDumper.SetNotes("");
for (const auto* localizeEntry : localizeAssets)
stringFileDumper.WriteLocalizeEntry(localizeEntry->m_name, localizeEntry->Asset()->value);
stringFileDumper.Finalize();
}
else
{
con::error("Could not create string file for dumping localized strings of zone '{}'", context.m_zone.m_name);
}
context.IncrementProgress();
}
} // namespace localize
@@ -0,0 +1,13 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/QOS/QOS.h"
namespace localize
{
class DumperQOS final : public AbstractSingleProgressAssetDumper<QOS::AssetLocalize>
{
public:
void Dump(AssetDumpingContext& context) override;
};
} // namespace localize
+22
View File
@@ -0,0 +1,22 @@
#include "ObjWriterQOS.h"
#include "Game/QOS/Font/FontDumperQOS.h"
#include "Game/QOS/Image/ImageDumperQOS.h"
#include "Game/QOS/Maps/MapEntsDumperQOS.h"
#include "Game/QOS/XModel/XModelDumperQOS.h"
#include "Localize/LocalizeDumperQOS.h"
#include "RawFile/RawFileDumperQOS.h"
#include "StringTable/StringTableDumperQOS.h"
using namespace QOS;
void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
{
RegisterAssetDumper(std::make_unique<font::JsonDumperQOS>());
RegisterAssetDumper(std::make_unique<image::DumperQOS>());
RegisterAssetDumper(std::make_unique<localize::DumperQOS>());
RegisterAssetDumper(std::make_unique<map_ents::DumperQOS>());
RegisterAssetDumper(std::make_unique<raw_file::DumperQOS>());
RegisterAssetDumper(std::make_unique<string_table::DumperQOS>());
RegisterAssetDumper(std::make_unique<xmodel::DumperQOS>());
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "ObjWriter.h"
namespace QOS
{
class ObjWriter final : public IObjWriter
{
protected:
void RegisterAssetDumpers(AssetDumpingContext& context) override;
};
} // namespace QOS
@@ -0,0 +1,18 @@
#include "RawFileDumperQOS.h"
using namespace QOS;
namespace raw_file
{
void DumperQOS::DumpAsset(AssetDumpingContext& context, const XAssetInfo<RawFile>& asset)
{
const auto* rawFile = asset.Asset();
const auto assetFile = context.OpenAssetFile(asset.m_name);
if (!assetFile)
return;
auto& stream = *assetFile;
stream.write(rawFile->buffer, rawFile->len);
}
} // namespace raw_file
@@ -0,0 +1,13 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/QOS/QOS.h"
namespace raw_file
{
class DumperQOS final : public AbstractAssetDumper<QOS::AssetRawFile>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<QOS::RawFile>& asset) override;
};
} // namespace raw_file
@@ -0,0 +1,29 @@
#include "StringTableDumperQOS.h"
#include "Csv/CsvStream.h"
using namespace QOS;
namespace string_table
{
void DumperQOS::DumpAsset(AssetDumpingContext& context, const XAssetInfo<StringTable>& asset)
{
const auto* stringTable = asset.Asset();
const auto assetFile = context.OpenAssetFile(asset.m_name);
if (!assetFile)
return;
CsvOutputStream csv(*assetFile);
for (auto row = 0; row < stringTable->rowCount; row++)
{
for (auto column = 0; column < stringTable->columnCount; column++)
{
csv.WriteColumn(stringTable->values[column + row * stringTable->columnCount]);
}
csv.NextRow();
}
}
} // namespace string_table
@@ -0,0 +1,13 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/QOS/QOS.h"
namespace string_table
{
class DumperQOS final : public AbstractAssetDumper<QOS::AssetStringTable>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<QOS::StringTable>& asset) override;
};
} // namespace string_table
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageDumper" + GAME + ".cpp"
@@ -18,6 +18,10 @@
#define FEATURE_IW5
#define DX9
#define IWI8
#elif GAME == "QOS"
#define FEATURE_QOS
#define DX9
#define IWI6
#elif GAME == "T4"
#define FEATURE_T4
#define DX9
+1 -1
View File
@@ -1,4 +1,4 @@
#options GAME(IW3, IW4, IW5, T4, T5, T6)
#options GAME(IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageDumper" + GAME + ".h"
@@ -3,6 +3,7 @@
#include "Game/IW3/Image/ImageToCommonConverterIW3.h"
#include "Game/IW4/Image/ImageToCommonConverterIW4.h"
#include "Game/IW5/Image/ImageToCommonConverterIW5.h"
#include "Game/QOS/Image/ImageToCommonConverterQOS.h"
#include "Game/T4/Image/ImageToCommonConverterT4.h"
#include "Game/T5/Image/ImageToCommonConverterT5.h"
#include "Game/T6/Image/ImageToCommonConverterT6.h"
@@ -17,6 +18,7 @@ namespace image
new ToCommonConverterIW3(),
new ToCommonConverterIW4(),
new ToCommonConverterIW5(),
new ToCommonConverterQOS(),
new ToCommonConverterT4(),
new ToCommonConverterT5(),
new ToCommonConverterT6(),
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageToCommonConverter" + GAME + ".cpp"
@@ -16,6 +16,10 @@
#define FEATURE_IW5
#define DX9
#define IWI8
#elif GAME == "QOS"
#define FEATURE_QOS
#define DX9
#define IWI6
#elif GAME == "T4"
#define FEATURE_T4
#define DX9
@@ -82,7 +86,7 @@ namespace
#endif
const auto& loadDef = *image.texture.loadDef;
#if defined(FEATURE_IW3) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_QOS) || defined(FEATURE_T4)
textureLoader.Width(loadDef.dimensions[0]).Height(loadDef.dimensions[1]).Depth(loadDef.dimensions[2]);
#else
textureLoader.Width(image.width).Height(image.height).Depth(image.depth);
@@ -106,6 +110,10 @@ namespace
#ifdef DX9
textureLoader.Format(static_cast<oat::D3DFORMAT>(loadDef.format));
#ifdef FEATURE_QOS
// The QOS executable uploads embedded load-def mip data from the smallest mip to the largest.
textureLoader.MipMapOrder(Dx9TextureLoader::MipMapDataOrder::SmallestToLargest);
#endif
#else
textureLoader.Format(static_cast<oat::DXGI_FORMAT>(loadDef.format));
#endif
@@ -1,4 +1,4 @@
#options GAME(IW3, IW4, IW5, T4, T5, T6)
#options GAME(IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Image/ImageToCommonConverter" + GAME + ".h"
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Maps/MapEntsDumper" + GAME + ".cpp"
+1 -1
View File
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/Maps/MapEntsDumper" + GAME + ".h"
+2
View File
@@ -3,6 +3,7 @@
#include "Game/IW3/ObjWriterIW3.h"
#include "Game/IW4/ObjWriterIW4.h"
#include "Game/IW5/ObjWriterIW5.h"
#include "Game/QOS/ObjWriterQOS.h"
#include "Game/T4/ObjWriterT4.h"
#include "Game/T5/ObjWriterT5.h"
#include "Game/T6/ObjWriterT6.h"
@@ -45,6 +46,7 @@ IObjWriter* IObjWriter::GetObjWriterForGame(GameId game)
new IW3::ObjWriter(),
new IW4::ObjWriter(),
new IW5::ObjWriter(),
new QOS::ObjWriter(),
new T4::ObjWriter(),
new T5::ObjWriter(),
new T6::ObjWriter(),
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".cpp"
@@ -16,6 +16,9 @@
#elif GAME == "IW5"
#define FEATURE_IW5
#define GAME_LOWER "iw5"
#elif GAME == "QOS"
#define FEATURE_QOS
#define GAME_LOWER "qos"
#elif GAME == "T4"
#define FEATURE_T4
#define GAME_LOWER "t4"
@@ -334,7 +337,7 @@ namespace
return false;
const auto& vertList = surface.vertList[0];
#if defined(FEATURE_IW3) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_QOS) || defined(FEATURE_T4)
// IW3 has some models that are missing 1 (a single) tri in its first lod.
// It is not contained in any vert list or blend
// I think this is a bug (?), so omit anyway.
@@ -398,7 +401,7 @@ namespace
jXModel.physCollmap = AssetName(model.physCollmap->name);
#endif
#if defined(FEATURE_T4) || defined(FEATURE_T5) || defined(FEATURE_T6)
#if defined(FEATURE_QOS) || defined(FEATURE_T4) || defined(FEATURE_T5) || defined(FEATURE_T6)
if (model.physConstraints && model.physConstraints->name)
jXModel.physConstraints = AssetName(model.physConstraints->name);
#endif
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelDumper" + GAME + ".h"
@@ -3,6 +3,7 @@
#include "Game/IW3/XModel/XModelToCommonConverterIW3.h"
#include "Game/IW4/XModel/XModelToCommonConverterIW4.h"
#include "Game/IW5/XModel/XModelToCommonConverterIW5.h"
#include "Game/QOS/XModel/XModelToCommonConverterQOS.h"
#include "Game/T4/XModel/XModelToCommonConverterT4.h"
#include "Game/T5/XModel/XModelToCommonConverterT5.h"
#include "Game/T6/XModel/XModelToCommonConverterT6.h"
@@ -17,6 +18,7 @@ namespace xmodel
new ToCommonConverterIW3(),
new ToCommonConverterIW4(),
new ToCommonConverterIW5(),
new ToCommonConverterQOS(),
new ToCommonConverterT4(),
new ToCommonConverterT5(),
new ToCommonConverterT6(),
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelToCommonConverter" + GAME + ".cpp"
@@ -11,6 +11,8 @@
#define FEATURE_IW4
#elif GAME == "IW5"
#define FEATURE_IW5
#elif GAME == "QOS"
#define FEATURE_QOS
#elif GAME == "T4"
#define FEATURE_T4
#elif GAME == "T5"
@@ -34,6 +36,7 @@
#include <cassert>
#include <format>
#include <string_view>
using namespace GAME;
@@ -56,7 +59,7 @@ namespace
{
MaterialTextureDef* def = &material->textureTable[textureIndex];
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_IW5) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_IW5) || defined(FEATURE_QOS) || defined(FEATURE_T4)
if (def->semantic == TS_COLOR_MAP)
potentialTextureDefs.push_back(def);
#else
@@ -70,6 +73,32 @@ namespace
if (potentialTextureDefs.size() == 1)
return GetImageFromTextureDef(*potentialTextureDefs[0]);
#ifdef FEATURE_QOS
// QoS can tag helper maps like *_edge as TS_COLOR_MAP; prefer diffuse-style names when several candidates exist.
GfxImage* qosFallback = nullptr;
for (const auto* def : potentialTextureDefs)
{
auto* image = GetImageFromTextureDef(*def);
if (!image || !image->name)
continue;
auto name = std::string_view(image->name);
while (!name.empty() && (name.front() == ',' || name.front() == '*'))
name.remove_prefix(1);
if (name.ends_with("_c") || name.ends_with("_col") || name.ends_with("_color") || name.ends_with("_diff"))
return image;
if (!qosFallback && !name.ends_with("_edge") && !name.ends_with("_print") && !name.ends_with("_prsz")
&& name.find("_edge_") == std::string_view::npos)
{
qosFallback = image;
}
}
if (qosFallback)
return qosFallback;
#endif
for (const auto* def : potentialTextureDefs)
{
if (tolower(def->nameStart) == 'c' && tolower(def->nameEnd) == 'p')
@@ -180,7 +209,7 @@ namespace
return false;
const auto& vertList = surface.vertList[0];
#if defined(FEATURE_IW3) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_QOS) || defined(FEATURE_T4)
// IW3 has some models that are missing 1 (a single) tri in its first lod.
// It is not contained in any vert list or blend
// I think this is a bug (?), so omit anyway.
@@ -298,9 +327,12 @@ namespace
if (colorMap)
xMaterial.colorMapName = AssetName(colorMap->name);
#ifndef FEATURE_QOS
// QoS normal map decoding/selection is not verified yet; exporting them makes the preview shading misleading.
const auto* normalMap = GetMaterialNormalMap(material);
if (normalMap)
xMaterial.normalMapName = AssetName(normalMap->name);
#endif
const auto* specularMap = GetMaterialSpecularMap(material);
if (specularMap)
@@ -346,8 +378,17 @@ namespace
vertex.coordinates[1] = v.xyz.y;
vertex.coordinates[2] = v.xyz.z;
Common::Vec3UnpackUnitVec(v.normal, vertex.normal);
#ifdef FEATURE_QOS
vertex.color[0] = 1.0f;
vertex.color[1] = 1.0f;
vertex.color[2] = 1.0f;
vertex.color[3] = 1.0f;
vertex.uv[0] = v.texCoord.x;
vertex.uv[1] = v.texCoord.y;
#else
Common::Vec4UnpackGfxColor(v.color, vertex.color);
Common::Vec2UnpackTexCoords(v.texCoord, vertex.uv);
#endif
out.m_vertices.emplace_back(vertex);
}
@@ -405,7 +446,7 @@ namespace
if (surface.vertList)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_QOS) || defined(FEATURE_T4)
assert(!surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) == 0);
@@ -430,7 +471,7 @@ namespace
auto vertsBlendOffset = 0u;
if (surface.vertInfo.vertsBlend)
{
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_T4)
#if defined(FEATURE_IW3) || defined(FEATURE_IW4) || defined(FEATURE_QOS) || defined(FEATURE_T4)
assert(surface.deformed);
#else
assert((surface.flags & XSURFACE_FLAG_DEFORMED) > 0);
@@ -532,7 +573,7 @@ namespace
auto& object = out.m_objects[surfIndex];
object.m_faces.reserve(surface.triCount);
#ifdef FEATURE_T4
#if defined(FEATURE_QOS) || defined(FEATURE_T4)
// T4 tri indices are local to the surface, while baseVertIndex does not match
// the flattened common vertex buffer we build here.
auto surfaceVertexOffset = 0u;
@@ -1,4 +1,4 @@
#options GAME (IW3, IW4, IW5, T4, T5, T6)
#options GAME (IW3, IW4, IW5, QOS, T4, T5, T6)
#filename "Game/" + GAME + "/XModel/XModelToCommonConverter" + GAME + ".h"