From 62ee9dab409559210a50f35d8e6b458497556308 Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 29 Jun 2026 17:59:28 +0100 Subject: [PATCH] feat: T4 LoadedSound dumper (#855) * feat: T4 LoadedSound dumper * chore: make t4 loaded sound dumper not parse wav --------- Co-authored-by: Jan Laupetin --- docs/SupportedAssetTypes.md | 2 +- src/ObjWriting/Game/T4/ObjWriterT4.cpp | 2 + .../Game/T4/Sound/LoadedSoundDumperT4.cpp | 94 +++++++++++++++++++ .../Game/T4/Sound/LoadedSoundDumperT4.h | 13 +++ .../Game/T4/Sound/LoadedSoundDumperT4Test.cpp | 76 +++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.cpp create mode 100644 src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.h create mode 100644 test/ObjWritingTests/Game/T4/Sound/LoadedSoundDumperT4Test.cpp diff --git a/docs/SupportedAssetTypes.md b/docs/SupportedAssetTypes.md index f57ef1b1..45b1cdb9 100644 --- a/docs/SupportedAssetTypes.md +++ b/docs/SupportedAssetTypes.md @@ -137,7 +137,7 @@ using `Linker`): | GfxImage | ✅ | ❌ | A few special image encodings are not yet supported. | | snd_alias_list_t | ❌ | ❌ | | | SndDriverGlobals | ❌ | ❌ | | -| LoadedSound | ❌ | ❌ | | +| LoadedSound | ✅ | ❌ | | | clipMap_t | ❌ | ❌ | | | ComWorld | ❌ | ❌ | | | GameWorldSp | ❌ | ❌ | | diff --git a/src/ObjWriting/Game/T4/ObjWriterT4.cpp b/src/ObjWriting/Game/T4/ObjWriterT4.cpp index 95b23659..7e95b891 100644 --- a/src/ObjWriting/Game/T4/ObjWriterT4.cpp +++ b/src/ObjWriting/Game/T4/ObjWriterT4.cpp @@ -6,6 +6,7 @@ #include "Localize/LocalizeDumperT4.h" #include "Maps/MapEntsDumperT4.h" #include "RawFile/RawFileDumperT4.h" +#include "Sound/LoadedSoundDumperT4.h" #include "StringTable/StringTableDumperT4.h" using namespace T4; @@ -15,6 +16,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); diff --git a/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.cpp b/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.cpp new file mode 100644 index 00000000..eeb011cf --- /dev/null +++ b/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.cpp @@ -0,0 +1,94 @@ +#include "LoadedSoundDumperT4.h" + +#include "Utils/Logging/Log.h" + +#include +#include +#include +#include + +using namespace T4; + +namespace fs = std::filesystem; + +namespace +{ + constexpr auto FOURCC_SIZE = 4uz; + constexpr auto RIFF_WAVE_HEADER_SIZE = 12uz; + constexpr auto RIFF_CHUNK_HEADER_SIZE = 8uz; + + constexpr std::string_view RIFF_ID = "RIFF"; + constexpr std::string_view WAVE_FORM_TYPE = "WAVE"; + constexpr std::string_view XWMA_FORM_TYPE = "XWMA"; + + [[nodiscard]] bool IsFourCc(const char* data, const std::string_view fourCc) + { + return fourCc.size() == FOURCC_SIZE && std::equal(fourCc.begin(), fourCc.end(), data); + } + + enum class LoadedSoundType : std::uint8_t + { + WAV, + XWMA + }; + + std::string GetExtensionBySoundType(const LoadedSoundType soundType) + { + switch (soundType) + { + case LoadedSoundType::WAV: + return ".wav"; + case LoadedSoundType::XWMA: + return ".xwma"; + } + + assert(false); + return ""; + } + + std::optional DetermineLoadedSoundType(const char* data, const size_t dataSize) + { + if (dataSize < RIFF_WAVE_HEADER_SIZE) + return std::nullopt; + + if (!IsFourCc(data, RIFF_ID)) + return std::nullopt; + + if (IsFourCc(&data[RIFF_CHUNK_HEADER_SIZE], WAVE_FORM_TYPE)) + return LoadedSoundType::WAV; + + if (IsFourCc(&data[RIFF_CHUNK_HEADER_SIZE], XWMA_FORM_TYPE)) + return LoadedSoundType::XWMA; + + return std::nullopt; + } +} // namespace + +namespace sound +{ + void LoadedSoundDumperT4::DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) + { + const auto* loadedSound = asset.Asset(); + if (!loadedSound->sound.data || loadedSound->sound.data_size <= 0) + return; + + const auto* data = loadedSound->sound.data; + const auto dataSize = static_cast(loadedSound->sound.data_size); + + const auto soundType = DetermineLoadedSoundType(data, dataSize); + if (soundType) + { + auto assetNameWithProperExtension = fs::path(asset.m_name); + assetNameWithProperExtension.replace_extension(GetExtensionBySoundType(*soundType)); + const auto assetFile = context.OpenAssetFile(std::format("sound/{}", assetNameWithProperExtension.string())); + if (!assetFile) + return; + + assetFile->write(data, static_cast(dataSize)); + } + else + { + con::error("Failed to dump loaded sound {} due to unknown sound type", asset.m_name); + } + } +} // namespace sound diff --git a/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.h b/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.h new file mode 100644 index 00000000..7b6ba26a --- /dev/null +++ b/src/ObjWriting/Game/T4/Sound/LoadedSoundDumperT4.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/T4/T4.h" + +namespace sound +{ + class LoadedSoundDumperT4 final : public AbstractAssetDumper + { + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace sound diff --git a/test/ObjWritingTests/Game/T4/Sound/LoadedSoundDumperT4Test.cpp b/test/ObjWritingTests/Game/T4/Sound/LoadedSoundDumperT4Test.cpp new file mode 100644 index 00000000..26796c3d --- /dev/null +++ b/test/ObjWritingTests/Game/T4/Sound/LoadedSoundDumperT4Test.cpp @@ -0,0 +1,76 @@ +#include "Game/T4/Sound/LoadedSoundDumperT4.h" + +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include + +using namespace T4; + +namespace +{ + void AppendU16(std::string& data, const uint16_t value) + { + data.push_back(static_cast(value & 0xFF)); + data.push_back(static_cast((value >> 8) & 0xFF)); + } + + void AppendU32(std::string& data, const uint32_t value) + { + data.push_back(static_cast(value & 0xFF)); + data.push_back(static_cast((value >> 8) & 0xFF)); + data.push_back(static_cast((value >> 16) & 0xFF)); + data.push_back(static_cast((value >> 24) & 0xFF)); + } + + std::string CreatePcmWav() + { + constexpr auto dataSize = 8u; + + std::string result; + result.append("RIFF", 4); + AppendU32(result, 36u + dataSize); + result.append("WAVE", 4); + result.append("fmt ", 4); + AppendU32(result, 16u); + AppendU16(result, 1u); + AppendU16(result, 1u); + AppendU32(result, 8000u); + AppendU32(result, 16000u); + AppendU16(result, 2u); + AppendU16(result, 16u); + result.append("data", 4); + AppendU32(result, dataSize); + AppendU16(result, 0u); + AppendU16(result, 1000u); + AppendU16(result, static_cast(-1000)); + AppendU16(result, 32767u); + return result; + } + + TEST_CASE("LoadedSoundDumperT4: Can dump PCM WAV loaded sound", "[t4][loaded-sound][assetdumper]") + { + Zone zone("MockZone", 0, GameId::T4, GamePlatform::PC); + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + AssetDumpingContext context(zone, "", mockOutput, mockObjPath, std::nullopt); + + auto wavData = CreatePcmWav(); + auto loadedSound = std::make_unique(); + loadedSound->name = "test.wav"; + loadedSound->sound.data = wavData.data(); + loadedSound->sound.data_size = static_cast(wavData.size()); + + zone.m_pools.AddAsset(std::make_unique>(ASSET_TYPE_LOADED_SOUND, loadedSound->name, loadedSound.get())); + + sound::LoadedSoundDumperT4 dumper; + dumper.Dump(context); + + const auto* file = mockOutput.GetMockedFile("sound/test.wav"); + REQUIRE(file != nullptr); + REQUIRE(file->AsString() == wavData); + } +} // namespace