feat: T4 weapon loading and dumping (#869)

* feat: T4 weapon loading and dumping

* chore: adjust naming of weapon enum values

* chore: fix typo in type names

* chore: rename t4 weapon loader files

* feat: dump and load gdt weapons for t4

* chore: weapon loaders to use similar logic

---------

Co-authored-by: Jan Laupetin <[email protected]>
This commit is contained in:
mo
2026-07-02 07:10:24 +02:00
committed by GitHub
co-authored by Jan Laupetin
parent ee63355fd2
commit fb4b00398c
32 changed files with 2536 additions and 46 deletions
@@ -0,0 +1,123 @@
#include "InfoStringFromStructConverter.h"
#include <cassert>
using namespace T4;
void InfoStringFromStructConverter::FillFromBaseField(const cspField_t& field)
{
switch (static_cast<csParseFieldType_t>(field.iFieldType))
{
case CSPFT_STRING:
FillFromString(std::string(field.szName), field.iOffset);
break;
case CSPFT_STRING_MAX_STRING_CHARS:
FillFromStringBuffer(std::string(field.szName), field.iOffset, 1024);
break;
case CSPFT_STRING_MAX_QPATH:
FillFromStringBuffer(std::string(field.szName), field.iOffset, 64);
break;
case CSPFT_STRING_MAX_OSPATH:
FillFromStringBuffer(std::string(field.szName), field.iOffset, 256);
break;
case CSPFT_INT:
FillFromInt(std::string(field.szName), field.iOffset);
break;
case CSPFT_BOOL:
FillFromQBoolean(std::string(field.szName), field.iOffset);
break;
case CSPFT_FLOAT:
FillFromFloat(std::string(field.szName), field.iOffset);
break;
case CSPFT_MILLISECONDS:
FillFromMilliseconds(std::string(field.szName), field.iOffset);
break;
case CSPFT_FX:
{
const auto* fx = *reinterpret_cast<FxEffectDef**>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
if (fx)
m_info_string.SetValueForKey(std::string(field.szName), std::string(AssetName(fx->name)));
else
m_info_string.SetValueForKey(std::string(field.szName), "");
break;
}
case CSPFT_XMODEL:
{
const auto* model = *reinterpret_cast<XModel**>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
if (model)
m_info_string.SetValueForKey(std::string(field.szName), std::string(AssetName(model->name)));
else
m_info_string.SetValueForKey(std::string(field.szName), "");
break;
}
case CSPFT_MATERIAL:
{
const auto* material = *reinterpret_cast<Material**>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
if (material)
m_info_string.SetValueForKey(std::string(field.szName), std::string(AssetName(material->info.name)));
else
m_info_string.SetValueForKey(std::string(field.szName), "");
break;
}
case CSPFT_SOUND:
{
const auto* sndAlias = reinterpret_cast<SndAliasCustom*>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
if (sndAlias->name)
m_info_string.SetValueForKey(std::string(field.szName), std::string(sndAlias->name->soundName));
else
m_info_string.SetValueForKey(std::string(field.szName), "");
break;
}
case CSPFT_NUM_BASE_FIELD_TYPES:
default:
assert(false);
break;
}
}
void InfoStringFromStructConverter::FillInfoString()
{
for (auto fieldIndex = 0u; fieldIndex < m_field_count; fieldIndex++)
{
const auto& field = m_fields[fieldIndex];
assert(field.iFieldType >= 0);
if (field.iFieldType < CSPFT_NUM_BASE_FIELD_TYPES)
FillFromBaseField(field);
else
FillFromExtensionField(field);
}
}
InfoStringFromStructConverter::InfoStringFromStructConverter(const void* structure, const cspField_t* fields, const size_t fieldCount)
: InfoStringFromStructConverterBase(structure),
m_fields(fields),
m_field_count(fieldCount)
{
}
InfoStringFromStructConverter::InfoStringFromStructConverter(const void* structure,
const cspField_t* fields,
const size_t fieldCount,
std::function<std::string(scr_string_t)> scriptStringValueCallback)
: InfoStringFromStructConverterBase(structure, std::move(scriptStringValueCallback)),
m_fields(fields),
m_field_count(fieldCount)
{
}
@@ -0,0 +1,25 @@
#pragma once
#include "Game/T4/T4.h"
#include "InfoString/InfoStringFromStructConverterBase.h"
namespace T4
{
class InfoStringFromStructConverter : public InfoStringFromStructConverterBase
{
protected:
const cspField_t* m_fields;
size_t m_field_count;
virtual void FillFromExtensionField(const cspField_t& field) = 0;
void FillFromBaseField(const cspField_t& field);
void FillInfoString() override;
public:
InfoStringFromStructConverter(const void* structure, const cspField_t* fields, size_t fieldCount);
InfoStringFromStructConverter(const void* structure,
const cspField_t* fields,
size_t fieldCount,
std::function<std::string(scr_string_t)> scriptStringValueCallback);
};
} // namespace T4
+2
View File
@@ -8,6 +8,7 @@
#include "RawFile/RawFileDumperT4.h"
#include "Sound/LoadedSoundDumperT4.h"
#include "StringTable/StringTableDumperT4.h"
#include "Weapon/WeaponDumperT4.h"
using namespace T4;
@@ -19,6 +20,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context)
RegisterAssetDumper(std::make_unique<sound::LoadedSoundDumperT4>());
RegisterAssetDumper(std::make_unique<map_ents::DumperT4>());
RegisterAssetDumper(std::make_unique<localize::DumperT4>());
RegisterAssetDumper(std::make_unique<weapon::DumperT4>());
RegisterAssetDumper(std::make_unique<raw_file::DumperT4>());
RegisterAssetDumper(std::make_unique<string_table::DumperT4>());
}
@@ -0,0 +1,347 @@
#include "WeaponDumperT4.h"
#include "Dumping/SubAssetDeduplicationDumperState.h"
#include "Game/T4/InfoString/InfoStringFromStructConverter.h"
#include "Game/T4/ObjConstantsT4.h"
#include "Game/T4/Weapon/FlameTableFields.h"
#include "Game/T4/Weapon/WeaponFields.h"
#include "Game/T4/Weapon/WeaponStrings.h"
#include "InfoString/InfoString.h"
#include "Weapon/AccuracyGraphWriter.h"
#include "Weapon/WeaponCommon.h"
#include <cassert>
#include <sstream>
#include <type_traits>
using namespace T4;
namespace
{
class InfoStringFromWeaponConverter final : public InfoStringFromStructConverter
{
protected:
void FillFromExtensionField(const cspField_t& field) override
{
switch (static_cast<weapFieldType_t>(field.iFieldType))
{
case WFT_WEAPONTYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapTypeNames, std::extent_v<decltype(szWeapTypeNames)>);
break;
case WFT_WEAPONCLASS:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapClassNames, std::extent_v<decltype(szWeapClassNames)>);
break;
case WFT_OVERLAYRETICLE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapOverlayReticleNames, std::extent_v<decltype(szWeapOverlayReticleNames)>);
break;
case WFT_PENETRATE_TYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, penetrateTypeNames, std::extent_v<decltype(penetrateTypeNames)>);
break;
case WFT_IMPACT_TYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, impactTypeNames, std::extent_v<decltype(impactTypeNames)>);
break;
case WFT_STANCE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapStanceNames, std::extent_v<decltype(szWeapStanceNames)>);
break;
case WFT_PROJ_EXPLOSION:
FillFromEnumInt(std::string(field.szName), field.iOffset, szProjectileExplosionNames, std::extent_v<decltype(szProjectileExplosionNames)>);
break;
case WFT_OFFHAND_CLASS:
FillFromEnumInt(std::string(field.szName), field.iOffset, offhandClassNames, std::extent_v<decltype(offhandClassNames)>);
break;
case WFT_ANIMTYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, playerAnimTypeNames, std::extent_v<decltype(playerAnimTypeNames)>);
break;
case WFT_ACTIVE_RETICLE_TYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, activeReticleNames, std::extent_v<decltype(activeReticleNames)>);
break;
case WFT_GUIDED_MISSILE_TYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, guidedMissileNames, std::extent_v<decltype(guidedMissileNames)>);
break;
case WFT_BOUNCE_SOUND:
{
const auto* bounceSound = *reinterpret_cast<SndAliasCustom**>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
if (bounceSound && bounceSound->name)
{
const std::string firstBounceSound(bounceSound->name->soundName);
const auto endOfBouncePrefix = firstBounceSound.rfind("_default");
assert(endOfBouncePrefix != std::string::npos);
if (endOfBouncePrefix != std::string::npos)
{
m_info_string.SetValueForKey(std::string(field.szName), firstBounceSound.substr(0, endOfBouncePrefix));
}
else
m_info_string.SetValueForKey(std::string(field.szName), "");
}
else
m_info_string.SetValueForKey(std::string(field.szName), "");
break;
}
case WFT_STICKINESS:
FillFromEnumInt(std::string(field.szName), field.iOffset, stickinessNames, std::extent_v<decltype(stickinessNames)>);
break;
case WFT_OVERLAYINTERFACE:
FillFromEnumInt(std::string(field.szName), field.iOffset, overlayInterfaceNames, std::extent_v<decltype(overlayInterfaceNames)>);
break;
case WFT_INVENTORYTYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapInventoryTypeNames, std::extent_v<decltype(szWeapInventoryTypeNames)>);
break;
case WFT_FIRETYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapFireTypeNames, std::extent_v<decltype(szWeapFireTypeNames)>);
break;
case WFT_CLIPTYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, szWeapClipTypeNames, std::extent_v<decltype(szWeapClipTypeNames)>);
break;
case WFT_AMMOCOUNTER_CLIPTYPE:
FillFromEnumInt(std::string(field.szName), field.iOffset, ammoCounterClipNames, std::extent_v<decltype(ammoCounterClipNames)>);
break;
case WFT_ICONRATIO_HUD:
case WFT_ICONRATIO_AMMOCOUNTER:
case WFT_ICONRATIO_KILL:
case WFT_ICONRATIO_DPAD:
FillFromEnumInt(std::string(field.szName), field.iOffset, weapIconRatioNames, std::extent_v<decltype(weapIconRatioNames)>);
break;
case WFT_HIDETAGS:
{
const auto* hideTags = reinterpret_cast<scr_string_t*>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
std::stringstream ss;
bool first = true;
for (auto i = 0u; i < std::extent_v<decltype(WeaponDef::hideTags)>; i++)
{
const auto& str = m_get_scr_string(hideTags[i]);
if (!str.empty())
{
if (!first)
ss << "\n";
else
first = false;
ss << str;
}
}
m_info_string.SetValueForKey(std::string(field.szName), ss.str());
break;
}
case WFT_NOTETRACKSOUNDMAP:
{
const auto* keys = reinterpret_cast<scr_string_t*>(reinterpret_cast<uintptr_t>(m_structure) + field.iOffset);
const auto* values = &keys[std::extent_v<decltype(WeaponDef::notetrackSoundMapKeys)>];
std::stringstream ss;
bool first = true;
for (auto i = 0u; i < std::extent_v<decltype(WeaponDef::notetrackSoundMapKeys)>; i++)
{
const auto& key = m_get_scr_string(keys[i]);
const auto& value = m_get_scr_string(values[i]);
if (!key.empty())
{
if (!first)
ss << "\n";
else
first = false;
ss << key;
if (!value.empty())
ss << " " << value;
}
}
m_info_string.SetValueForKey(std::string(field.szName), ss.str());
break;
}
case WFT_ANIM_NAME:
FillFromString(std::string(field.szName), field.iOffset);
break;
case WFT_NUM_FIELD_TYPES:
default:
assert(false);
break;
}
}
public:
InfoStringFromWeaponConverter(const WeaponDef* structure,
const cspField_t* fields,
const size_t fieldCount,
std::function<std::string(scr_string_t)> scriptStringValueCallback)
: InfoStringFromStructConverter(structure, fields, fieldCount, std::move(scriptStringValueCallback))
{
}
};
class InfoStringFromFlameTableConverter final : public InfoStringFromStructConverter
{
protected:
void FillFromExtensionField(const cspField_t& field) override
{
assert(false);
}
public:
InfoStringFromFlameTableConverter(const FlameTable* structure,
const cspField_t* fields,
const size_t fieldCount,
std::function<std::string(scr_string_t)> scriptStringValueCallback)
: InfoStringFromStructConverter(structure, fields, fieldCount, std::move(scriptStringValueCallback))
{
}
};
InfoString CreateInfoString(const XAssetInfo<WeaponDef>& asset)
{
InfoStringFromWeaponConverter converter(asset.Asset(),
weapon_fields,
std::extent_v<decltype(weapon_fields)>,
[asset](const scr_string_t scrStr) -> std::string
{
assert(scrStr < asset.m_zone->m_script_strings.Count());
if (scrStr >= asset.m_zone->m_script_strings.Count())
return "";
return asset.m_zone->m_script_strings[scrStr];
});
return converter.Convert();
}
GenericGraph2D ConvertAccuracyGraph(std::string graphName, const vec2_t* originalKnots, const unsigned originalKnotCount)
{
GenericGraph2D graph;
graph.name = std::move(graphName);
graph.knots.resize(originalKnotCount);
for (auto i = 0u; i < originalKnotCount; i++)
{
auto& knot = graph.knots[i];
knot.x = originalKnots[i].x;
knot.y = originalKnots[i].y;
}
return graph;
}
void DumpAccuracyGraphs(AssetDumpingContext& context, const XAssetInfo<WeaponDef>& asset)
{
auto* accuracyGraphWriter = context.GetZoneAssetDumperState<AccuracyGraphWriter>();
const auto* weapon = asset.Asset();
if (weapon->aiVsAiAccuracyGraphName && weapon->originalAiVsAiAccuracyGraphKnots)
{
auto graphName = weapon::GetAssetNameForAiVsAiAccuracyGraph(weapon->aiVsAiAccuracyGraphName);
if (accuracyGraphWriter->ShouldDumpGraph(graphName))
{
const auto graph =
ConvertAccuracyGraph(std::move(graphName), weapon->originalAiVsAiAccuracyGraphKnots, weapon->originalAiVsAiAccuracyGraphKnotCount);
AccuracyGraphWriter::DumpGraph(context, graph);
}
}
if (weapon->aiVsPlayerAccuracyGraphName && weapon->originalAiVsPlayerAccuracyGraphKnots)
{
auto graphName = weapon::GetAssetNameForAiVsPlayerAccuracyGraph(weapon->aiVsPlayerAccuracyGraphName);
if (accuracyGraphWriter->ShouldDumpGraph(graphName))
{
const auto graph =
ConvertAccuracyGraph(std::move(graphName), weapon->originalAiVsPlayerAccuracyGraphKnots, weapon->originalAiVsPlayerAccuracyGraphKnotCount);
AccuracyGraphWriter::DumpGraph(context, graph);
}
}
}
void DumpFlameTable(const AssetDumpingContext& context,
SubAssetDeduplicationDumperState<FlameTable>& deduplicator,
const char* flameTableName,
const FlameTable* flameTable)
{
if (!flameTable || !flameTableName || flameTableName[0] == '\0')
return;
if (!deduplicator.ShouldDumpSubAsset(flameTable))
return;
const auto assetFile = context.OpenAssetFile(weapon::GetFileNameForFlameTable(flameTableName));
if (!assetFile)
return;
InfoStringFromFlameTableConverter converter(flameTable,
flameTableFields,
std::extent_v<decltype(flameTableFields)>,
[](const scr_string_t scrStr) -> std::string
{
assert(false);
return "";
});
const auto infoString = converter.Convert();
const auto stringValue = infoString.ToString(INFO_STRING_PREFIX_FLAME_TABLE);
assetFile->write(stringValue.c_str(), stringValue.size());
}
void DumpFlameTables(AssetDumpingContext& context, const XAssetInfo<WeaponDef>& asset)
{
auto* deduplicator = context.GetZoneAssetDumperState<SubAssetDeduplicationDumperState<FlameTable>>();
const auto* weapon = asset.Asset();
DumpFlameTable(context, *deduplicator, weapon->flameTableFirstPerson, weapon->flameTableFirstPersonPtr);
DumpFlameTable(context, *deduplicator, weapon->flameTableThirdPerson, weapon->flameTableThirdPersonPtr);
}
} // namespace
namespace weapon
{
void DumperT4::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetWeapon::Type>& asset)
{
// Only dump raw when no gdt available
if (context.m_gdt)
{
const auto infoString = CreateInfoString(asset);
GdtEntry gdtEntry(asset.m_name, GDF_FILENAME_WEAPON);
infoString.ToGdtProperties(INFO_STRING_PREFIX_WEAPON, gdtEntry);
context.m_gdt->WriteEntry(gdtEntry);
}
else
{
const auto assetFile = context.OpenAssetFile(GetFileNameForAssetName(asset.m_name));
if (!assetFile)
return;
auto& stream = *assetFile;
const auto infoString = CreateInfoString(asset);
const auto stringValue = infoString.ToString(INFO_STRING_PREFIX_WEAPON);
stream.write(stringValue.c_str(), stringValue.size());
}
DumpAccuracyGraphs(context, asset);
DumpFlameTables(context, asset);
}
} // namespace weapon
@@ -0,0 +1,13 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/T4/T4.h"
namespace weapon
{
class DumperT4 final : public AbstractAssetDumper<T4::AssetWeapon>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<T4::AssetWeapon::Type>& asset) override;
};
} // namespace weapon