#include "AssetDumperSndBank.h" #include "Csv/CsvStream.h" #include "Game/T6/CommonT6.h" #include "Game/T6/GameAssetPoolT6.h" #include "Game/T6/GameT6.h" #include "Game/T6/SoundConstantsT6.h" #include "ObjContainer/SoundBank/SoundBank.h" #include "Sound/WavWriter.h" #include "nlohmann/json.hpp" #include #include #include #include #include #include #include using namespace T6; namespace fs = std::filesystem; namespace { const std::string ALIAS_HEADERS[]{ "Name", "FileSource", "Secondary", "Storage", "Bus", "VolumeGroup", "DuckGroup", "Duck", "ReverbSend", "CenterSend", "VolMin", "VolMax", "DistMin", "DistMaxDry", "DistMaxWet", "DryMinCurve", "DryMaxCurve", "WetMinCurve", "WetMaxCurve", "LimitCount", "EntityLimitCount", "LimitType", "EntityLimitType", "PitchMin", "PitchMax", "PriorityMin", "PriorityMax", "PriorityThresholdMin", "PriorityThresholdMax", "PanType", "Pan", "Looping", "RandomizeType", "Probability", "StartDelay", "EnvelopMin", "EnvelopMax", "EnvelopPercent", "OcclusionLevel", "IsBig", "DistanceLpf", "FluxType", "FluxTime", "Subtitle", "Doppler", "ContextType", "ContextValue", "Timescale", "IsMusic", "IsCinematic", "FadeIn", "FadeOut", "Pauseable", "StopOnEntDeath", "StopOnPlay", "DopplerScale", "FutzPatch", "VoiceLimit", "IgnoreMaxDist", "NeverPlayTwice", }; const std::string REVERB_HEADERS[]{ "name", "smoothing", "earlyTime", "lateTime", "earlyGain", "lateGain", "returnGain", "earlyLpf", "lateLpf", "inputLpf", "dampLpf", "wallReflect", "dryGain", "earlySize", "lateSize", "diffusion", "returnHighpass", }; const std::string PREFIXES_TO_DROP[]{ "raw/", "devraw/", }; constexpr unsigned FRAME_RATE_FOR_INDEX[]{ 8000, 12000, 16000, 24000, 32000, 44100, 48000, 96000, 192000, }; constexpr const char* KNOWN_CONTEXT_TYPES[]{ "", "plr_stance", "grass", "f35", "ringoff_plr", "mature", }; constexpr const char* KNOWN_CONTEXT_VALUES[]{ "", "stand", "crouch", "prone", "no_grass", "in_grass", "interior", "exterior", "outdoor", "indoor", "safe", "explicit", }; constexpr const char* KNOWN_FUTZ_IDS[]{ "", "bfutz", "default", "defaultbkp", "dlc_res_1", "dlc_res_2", "dlc_res_3", "dlc_res_4", "dlc_res_5", "dlc_res_6", "dlc_res_7", "dlc_res_8", "good_1", "jet_wing_helmet", "jet_wing_helmet_flying", "mpl_agr_pov", "mpl_chopper_pov", "mpl_quad_pov", "mpl_reaper_pov", "no_gfutz", "spl_asd_pov", "spl_bigdog_pov", "spl_heli_future", "spl_quad_pov", "spl_spiderbot_pov", "spl_spymic", "spl_tow_missile", "spl_turret", "spl_war_command", "test_1", "test_2", "tueyeckert", }; std::unordered_map CreateSoundHashMap(const char* const* values, const unsigned valueCount) { std::unordered_map result; for (auto i = 0u; i < valueCount; i++) result.emplace(Common::SND_HashName(values[i]), values[i]); return result; } const auto CURVES_MAP = CreateSoundHashMap(SOUND_CURVES, std::extent_v); const auto CONTEXT_TYPES_MAP = CreateSoundHashMap(KNOWN_CONTEXT_TYPES, std::extent_v); const auto CONTEXT_VALUES_MAP = CreateSoundHashMap(KNOWN_CONTEXT_VALUES, std::extent_v); const auto FUTZ_IDS_MAP = CreateSoundHashMap(KNOWN_FUTZ_IDS, std::extent_v); constexpr auto FORMATTING_RETRIES = 5; class LoadedSoundBankHashes { public: void Initialize() { for (const auto& zone : IGame::GetGameById(GameId::T6)->GetZones()) { auto& sndBankPool = *dynamic_cast(zone->m_pools.get())->m_sound_bank; for (auto* entry : sndBankPool) { const auto& sndBank = *entry->Asset(); m_alias_names.reserve(m_alias_names.size() + sndBank.aliasCount); for (auto aliasIndex = 0u; aliasIndex < sndBank.aliasCount; aliasIndex++) { const auto& alias = sndBank.alias[aliasIndex]; m_alias_names.emplace(alias.id, alias.name); } m_duck_names.reserve(m_duck_names.size() + sndBank.duckCount); for (auto duckIndex = 0u; duckIndex < sndBank.duckCount; duckIndex++) { const auto& duck = sndBank.ducks[duckIndex]; m_duck_names.emplace(duck.id, duck.name); } } } } [[nodiscard]] std::optional GetAliasName(const unsigned hash) const { if (hash == 0) return ""; const auto knownAliasHash = m_alias_names.find(hash); if (knownAliasHash != m_alias_names.end()) return knownAliasHash->second; return std::nullopt; } [[nodiscard]] std::optional GetDuckName(const unsigned hash) const { if (hash == 0) return ""; const auto knownDuckHash = m_duck_names.find(hash); if (knownDuckHash != m_duck_names.end()) return knownDuckHash->second; return std::nullopt; } private: std::unordered_map m_alias_names; std::unordered_map m_duck_names; }; [[nodiscard]] std::string GetAssetFilename(const AssetDumpingContext& context, std::string outputFileName, const std::string& extension) { fs::path assetPath(context.m_base_path); std::ranges::replace(outputFileName, '\\', '/'); for (const auto& droppedPrefix : PREFIXES_TO_DROP) { if (outputFileName.rfind(droppedPrefix, 0) != std::string::npos) { outputFileName.erase(0, droppedPrefix.size()); break; } } assetPath.append(outputFileName); if (!extension.empty()) assetPath.concat(extension); return assetPath.string(); } std::unique_ptr OpenAssetOutputFile(const AssetDumpingContext& context, const std::string& outputFileName, const std::string& extension) { fs::path assetPath(GetAssetFilename(context, outputFileName, extension)); auto assetDir(assetPath); assetDir.remove_filename(); create_directories(assetDir); auto outputStream = std::make_unique(assetPath, std::ios_base::out | std::ios_base::binary); if (outputStream->is_open()) { return std::move(outputStream); } return nullptr; } void WriteAliasFileHeader(CsvOutputStream& stream) { for (const auto& headerField : ALIAS_HEADERS) { stream.WriteColumn(headerField); } stream.NextRow(); } void WriteReverbFileHeader(CsvOutputStream& stream) { for (const auto& headerField : REVERB_HEADERS) { stream.WriteColumn(headerField); } stream.NextRow(); } const char* ExtensionForSndFormat(const snd_asset_format format) { switch (format) { case SND_ASSET_FORMAT_PCMS16: return ".wav"; case SND_ASSET_FORMAT_FLAC: return ".flac"; default: assert(false); return ""; } } void WriteColumnString(CsvOutputStream& stream, const std::string& stringValue) { stream.WriteColumn(stringValue); } void WriteColumnString(CsvOutputStream& stream, const char* stringValue) { stream.WriteColumn(stringValue ? std::string(stringValue) : std::string()); } template void WriteColumnIntegral(CsvOutputStream& stream, const T value) { stream.WriteColumn(std::format("{}", value)); } void WriteColumnEnumWithSize(CsvOutputStream& stream, const unsigned value, const char* const* enumValues, const size_t enumValueCount) { assert(value < enumValueCount); stream.WriteColumn(value < enumValueCount ? enumValues[value] : ""); } template void WriteColumnEnum(CsvOutputStream& stream, const unsigned value, const char* const (&enumValues)[Size]) { WriteColumnEnumWithSize(stream, value, enumValues, Size); } void WriteColumnEnumFlagsWithSize(CsvOutputStream& stream, const unsigned value, const char* const* enumValues, const size_t enumValueCount) { assert(enumValueCount <= 32u); std::ostringstream ss; auto first = true; for (auto i = 0u; i < enumValueCount; i++) { const auto flagValue = 1u << i; if (value & flagValue) { if (first) first = false; else ss << ' '; ss << enumValues[i]; } } stream.WriteColumn(ss.str()); } template void WriteColumnEnumFlags(CsvOutputStream& stream, const unsigned value, const char* const (&enumValues)[Size]) { WriteColumnEnumFlagsWithSize(stream, value, enumValues, Size); } void WriteColumnVolumeLinear(CsvOutputStream& stream, const uint16_t value) { const auto linear = static_cast(value) / static_cast(std::numeric_limits::max()); const auto dbSpl = std::clamp(Common::LinearToDbspl(linear), 0.0f, 100.0f); std::string dbSplFormat; for (auto i = 0; i < FORMATTING_RETRIES; i++) { dbSplFormat = std::format("{:.{}f}", dbSpl, i); const auto dbSplRound = std::stof(dbSplFormat); const auto dbSplRoundToValue = static_cast(Common::DbsplToLinear(dbSplRound) * static_cast(std::numeric_limits::max())); if (dbSplRoundToValue == value) break; } stream.WriteColumn(dbSplFormat); } void WriteColumnPitchHertz(CsvOutputStream& stream, const uint16_t value) { const auto hertz = static_cast(value) / static_cast(std::numeric_limits::max()); const auto cents = std::clamp(Common::HertzToCents(hertz), -2400.0f, 1200.0f); std::string centsFormat; for (auto i = 0; i < FORMATTING_RETRIES; i++) { centsFormat = std::format("{:.{}f}", cents, i); const auto centsRound = std::stof(centsFormat); const auto centsRoundToValue = static_cast(Common::CentsToHertz(centsRound) * static_cast(std::numeric_limits::max())); if (centsRoundToValue == value) break; } stream.WriteColumn(centsFormat); } void WriteColumnNormByte(CsvOutputStream& stream, const uint8_t value) { const auto normValue = static_cast(value) / static_cast(std::numeric_limits::max()); std::string normValueFormat; for (auto i = 0; i < FORMATTING_RETRIES; i++) { normValueFormat = std::format("{:.{}f}", normValue, i); const auto normValueRound = std::stof(normValueFormat); const auto normValueRoundToValue = static_cast(normValueRound * static_cast(std::numeric_limits::max())); if (normValueRoundToValue == value) break; } stream.WriteColumn(normValueFormat); } void WriteColumnWithKnownHashes(CsvOutputStream& stream, const std::unordered_map& knownValues, const unsigned value) { const auto knownValue = knownValues.find(value); if (knownValue != knownValues.end()) stream.WriteColumn(knownValue->second); else stream.WriteColumn(std::format("@{:x}", value)); } void WriteColumnWithAliasHash(CsvOutputStream& stream, const LoadedSoundBankHashes& hashes, const unsigned value) { const auto name = hashes.GetAliasName(value); if (name) stream.WriteColumn(*name); else stream.WriteColumn(std::format("@{:x}", value)); } void WriteColumnWithDuckHash(CsvOutputStream& stream, const LoadedSoundBankHashes& hashes, const unsigned value) { const auto name = hashes.GetDuckName(value); if (name) stream.WriteColumn(*name); else stream.WriteColumn(std::format("@{:x}", value)); } void WriteAliasToFile(CsvOutputStream& stream, const SndAlias& alias, const std::optional maybeFormat, const LoadedSoundBankHashes& hashes) { // Name WriteColumnString(stream, alias.name); // FileSource const auto* extension = maybeFormat ? ExtensionForSndFormat(*maybeFormat) : ""; WriteColumnString(stream, alias.assetFileName ? std::format("{}{}", alias.assetFileName, extension) : ""); // Secondary WriteColumnString(stream, alias.secondaryName); // Storage WriteColumnEnum(stream, alias.flags.loadType, SOUND_LOAD_TYPES); // Bus WriteColumnEnum(stream, alias.flags.busType, SOUND_BUS_IDS); // VolumeGroup WriteColumnEnum(stream, alias.flags.volumeGroup, SOUND_GROUPS); // DuckGroup WriteColumnEnum(stream, alias.duckGroup, SOUND_DUCK_GROUPS); // Duck WriteColumnWithDuckHash(stream, hashes, alias.duck); // ReverbSend WriteColumnVolumeLinear(stream, alias.reverbSend); // CenterSend WriteColumnVolumeLinear(stream, alias.centerSend); // VolMin WriteColumnVolumeLinear(stream, alias.volMin); // VolMax WriteColumnVolumeLinear(stream, alias.volMax); // DistMin WriteColumnIntegral(stream, alias.distMin); // DistMaxDry WriteColumnIntegral(stream, alias.distMax); // DistMaxWet WriteColumnIntegral(stream, alias.distReverbMax); // DryMinCurve WriteColumnEnum(stream, alias.flags.volumeMinFalloffCurve, SOUND_CURVES); // DryMaxCurve WriteColumnEnum(stream, alias.flags.volumeFalloffCurve, SOUND_CURVES); // WetMinCurve WriteColumnEnum(stream, alias.flags.reverbMinFalloffCurve, SOUND_CURVES); // WetMaxCurve WriteColumnEnum(stream, alias.flags.reverbFalloffCurve, SOUND_CURVES); // LimitCount WriteColumnIntegral(stream, alias.limitCount); // EntityLimitCount WriteColumnIntegral(stream, alias.entityLimitCount); // LimitType WriteColumnEnum(stream, alias.flags.limitType, SOUND_LIMIT_TYPES); // EntityLimitType WriteColumnEnum(stream, alias.flags.entityLimitType, SOUND_LIMIT_TYPES); // PitchMin WriteColumnPitchHertz(stream, alias.pitchMin); // PitchMax WriteColumnPitchHertz(stream, alias.pitchMax); // PriorityMin WriteColumnIntegral(stream, alias.minPriority); // PriorityMax WriteColumnIntegral(stream, alias.maxPriority); // PriorityThresholdMin WriteColumnNormByte(stream, alias.minPriorityThreshold); // PriorityThresholdMax WriteColumnNormByte(stream, alias.maxPriorityThreshold); // PanType WriteColumnEnum(stream, alias.flags.panType, SOUND_PAN_TYPES); // Pan WriteColumnEnum(stream, alias.pan, SOUND_PANS); // Looping WriteColumnEnum(stream, alias.flags.looping, SOUND_LOOP_TYPES); // RandomizeType WriteColumnEnumFlags(stream, alias.flags.randomizeType, SOUND_RANDOMIZE_TYPES); // Probability WriteColumnNormByte(stream, alias.probability); // StartDelay WriteColumnIntegral(stream, alias.startDelay); // EnvelopMin WriteColumnIntegral(stream, alias.envelopMin); // EnvelopMax WriteColumnIntegral(stream, alias.envelopMax); // EnvelopPercent WriteColumnVolumeLinear(stream, alias.envelopPercentage); // OcclusionLevel WriteColumnNormByte(stream, alias.occlusionLevel); // IsBig WriteColumnEnum(stream, alias.flags.isBig, SOUND_NO_YES); // DistanceLpf WriteColumnEnum(stream, alias.flags.distanceLpf, SOUND_NO_YES); // FluxType WriteColumnEnum(stream, alias.flags.fluxType, SOUND_MOVE_TYPES); // FluxTime WriteColumnIntegral(stream, alias.fluxTime); // Subtitle WriteColumnString(stream, alias.subtitle); // Doppler WriteColumnEnum(stream, alias.flags.doppler, SOUND_NO_YES); // ContextType WriteColumnWithKnownHashes(stream, CONTEXT_TYPES_MAP, alias.contextType); // ContextValue WriteColumnWithKnownHashes(stream, CONTEXT_VALUES_MAP, alias.contextValue); // Timescale WriteColumnEnum(stream, alias.flags.timescale, SOUND_NO_YES); // IsMusic WriteColumnEnum(stream, alias.flags.isMusic, SOUND_NO_YES); // IsCinematic WriteColumnEnum(stream, alias.flags.isCinematic, SOUND_NO_YES); // FadeIn WriteColumnIntegral(stream, alias.fadeIn); // FadeOut WriteColumnIntegral(stream, alias.fadeOut); // Pauseable WriteColumnEnum(stream, alias.flags.pauseable, SOUND_NO_YES); // StopOnEntDeath WriteColumnEnum(stream, alias.flags.stopOnEntDeath, SOUND_NO_YES); // StopOnPlay WriteColumnWithAliasHash(stream, hashes, alias.stopOnPlay); // DopplerScale WriteColumnIntegral(stream, alias.dopplerScale); // FutzPatch WriteColumnWithKnownHashes(stream, FUTZ_IDS_MAP, alias.futzPatch); // VoiceLimit WriteColumnEnum(stream, alias.flags.voiceLimit, SOUND_NO_YES); // IgnoreMaxDist WriteColumnEnum(stream, alias.flags.ignoreMaxDist, SOUND_NO_YES); // NeverPlayTwice WriteColumnEnum(stream, alias.flags.neverPlayTwice, SOUND_NO_YES); } SoundBankEntryInputStream FindSoundDataInSoundBanks(const unsigned assetId) { for (const auto* soundBank : SoundBank::Repository) { auto soundFile = soundBank->GetEntryStream(assetId); if (soundFile.IsOpen()) return soundFile; } return {}; } void DumpSoundFilePcm(const AssetDumpingContext& context, const char* assetFileName, const SoundBankEntryInputStream& soundFile, const unsigned bitsPerSample) { const auto outFile = OpenAssetOutputFile(context, assetFileName, ".wav"); if (!outFile) { std::cerr << std::format("Failed to open sound output file: \"{}\"\n", assetFileName); return; } const WavWriter writer(*outFile); if (soundFile.m_entry.frameRateIndex >= std::extent_v) return; const WavMetaData metaData{soundFile.m_entry.channelCount, FRAME_RATE_FOR_INDEX[soundFile.m_entry.frameRateIndex], bitsPerSample}; writer.WritePcmHeader(metaData, soundFile.m_entry.size); while (!soundFile.m_stream->eof()) { char buffer[2048]; soundFile.m_stream->read(buffer, sizeof(buffer)); const auto readSize = soundFile.m_stream->gcount(); outFile->write(buffer, readSize); } } void DumpSoundFilePassthrough(const AssetDumpingContext& context, const char* assetFileName, const SoundBankEntryInputStream& soundFile, const std::string& extension) { const auto outFile = OpenAssetOutputFile(context, assetFileName, extension); if (!outFile) { std::cerr << std::format("Failed to open sound output file: \"{}\"\n", assetFileName); return; } while (!soundFile.m_stream->eof()) { char buffer[2048]; soundFile.m_stream->read(buffer, sizeof(buffer)); const auto readSize = soundFile.m_stream->gcount(); outFile->write(buffer, readSize); } } [[nodiscard]] std::optional DumpSndAlias(const AssetDumpingContext& context, const SndAlias& alias) { const auto soundFile = FindSoundDataInSoundBanks(alias.assetId); if (soundFile.IsOpen()) { const auto format = static_cast(soundFile.m_entry.format); switch (format) { case SND_ASSET_FORMAT_PCMS16: DumpSoundFilePcm(context, alias.assetFileName, soundFile, 16u); break; case SND_ASSET_FORMAT_FLAC: DumpSoundFilePassthrough(context, alias.assetFileName, soundFile, ".flac"); break; case SND_ASSET_FORMAT_PCMS24: case SND_ASSET_FORMAT_PCMS32: case SND_ASSET_FORMAT_IEEE: case SND_ASSET_FORMAT_XMA4: case SND_ASSET_FORMAT_MSADPCM: case SND_ASSET_FORMAT_WMA: case SND_ASSET_FORMAT_WIIUADPCM: case SND_ASSET_FORMAT_MPC: std::cerr << std::format("Cannot dump sound (Unknown sound format {}): \"{}\"\n", static_cast(format), alias.assetFileName); break; default: assert(false); std::cerr << std::format("Cannot dump sound (Unknown sound format {}): \"{}\"\n", static_cast(format), alias.assetFileName); break; } return format; } std::cerr << std::format("Could not find data for sound \"{}\"\n", alias.assetFileName); return {}; } void DumpSndBankAliases(const AssetDumpingContext& context, const LoadedSoundBankHashes& hashes, const SndBank& sndBank) { std::unordered_map> dumpedAssets; const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.aliases", sndBank.name), ".csv"); if (!outFile) { std::cerr << std::format("Failed to open sound alias output file: \"\"\n", sndBank.name); return; } CsvOutputStream csvStream(*outFile); WriteAliasFileHeader(csvStream); for (auto i = 0u; i < sndBank.aliasCount; i++) { const auto& aliasList = sndBank.alias[i]; for (auto j = 0; j < aliasList.count; j++) { const auto& alias = aliasList.head[j]; std::optional maybeFormat; if (alias.assetId && alias.assetFileName) { const auto previouslyDeterminedFormat = dumpedAssets.find(alias.assetId); if (previouslyDeterminedFormat == dumpedAssets.end()) { maybeFormat = DumpSndAlias(context, alias); dumpedAssets[alias.assetId] = maybeFormat; } else { maybeFormat = previouslyDeterminedFormat->second; } } WriteAliasToFile(csvStream, alias, maybeFormat, hashes); csvStream.NextRow(); } } } void DumpSoundRadverb(const AssetDumpingContext& context, const SndBank& sndBank) { if (sndBank.radverbCount <= 0) return; const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.reverbs", sndBank.name), ".csv"); if (!outFile) { std::cerr << std::format("Failed to open sound reverb output file: \"{}\"\n", sndBank.name); return; } CsvOutputStream csvStream(*outFile); WriteReverbFileHeader(csvStream); for (auto i = 0u; i < sndBank.radverbCount; i++) { const auto& reverb = sndBank.radverbs[i]; csvStream.WriteColumn(reverb.name); csvStream.WriteColumn(std::to_string(reverb.smoothing)); csvStream.WriteColumn(std::to_string(reverb.earlyTime)); csvStream.WriteColumn(std::to_string(reverb.lateTime)); csvStream.WriteColumn(std::to_string(reverb.earlyGain)); csvStream.WriteColumn(std::to_string(reverb.lateGain)); csvStream.WriteColumn(std::to_string(reverb.returnGain)); csvStream.WriteColumn(std::to_string(reverb.earlyLpf)); csvStream.WriteColumn(std::to_string(reverb.lateLpf)); csvStream.WriteColumn(std::to_string(reverb.inputLpf)); csvStream.WriteColumn(std::to_string(reverb.dampLpf)); csvStream.WriteColumn(std::to_string(reverb.wallReflect)); csvStream.WriteColumn(std::to_string(reverb.dryGain)); csvStream.WriteColumn(std::to_string(reverb.earlySize)); csvStream.WriteColumn(std::to_string(reverb.lateSize)); csvStream.WriteColumn(std::to_string(reverb.diffusion)); csvStream.WriteColumn(std::to_string(reverb.returnHighpass)); csvStream.NextRow(); } } void DumpSoundDucks(const AssetDumpingContext& context, const SndBank& sndBank) { if (sndBank.duckCount <= 0) return; const auto outFile = OpenAssetOutputFile(context, std::format("soundbank/{}.ducklist", sndBank.name), ".csv"); if (!outFile) { std::cerr << std::format("Failed to open sound reverb output file: \"{}\"\n", sndBank.name); return; } CsvOutputStream csvStream(*outFile); csvStream.WriteColumn("name"); csvStream.NextRow(); for (auto i = 0u; i < sndBank.duckCount; i++) { const auto& duck = sndBank.ducks[i]; csvStream.WriteColumn(duck.name); csvStream.NextRow(); const auto duckFile = OpenAssetOutputFile(context, std::format("soundbank/ducks/{}", duck.name), ".duk"); if (!outFile) { std::cerr << std::format("Failed to open sound duck output file: \"{}\"\n", duck.name); return; } nlohmann::json duckObj{}; duckObj["fadeIn"] = duck.fadeIn; duckObj["fadeOut"] = duck.fadeOut; duckObj["startDelay"] = duck.startDelay; duckObj["distance"] = duck.distance; duckObj["length"] = duck.length; duckObj["fadeInCurveId"] = duck.fadeInCurve; duckObj["fadeOutCurveId"] = duck.fadeOutCurve; duckObj["updateWhilePaused"] = duck.updateWhilePaused; auto fadeInItr = CURVES_MAP.find(duck.fadeInCurve); if (fadeInItr != CURVES_MAP.end()) { duckObj["fadeInCurve"] = fadeInItr->second; } auto fadeOutItr = CURVES_MAP.find(duck.fadeOutCurve); if (fadeOutItr != CURVES_MAP.end()) { duckObj["fadeOutCurve"] = fadeOutItr->second; } auto values = std::vector{}; for (auto j = 0u; j < 32u; j++) { values.push_back({ {"duckGroup", SOUND_DUCK_GROUPS[j]}, {"attenuation", duck.attenuation[j] }, {"filter", duck.filter[j] } }); } duckObj["values"] = values; *duckFile << duckObj.dump(4) << "\n"; } } void DumpSndBank(const AssetDumpingContext& context, const LoadedSoundBankHashes& hashes, const XAssetInfo& sndBankInfo) { const auto* sndBank = sndBankInfo.Asset(); DumpSndBankAliases(context, hashes, *sndBank); DumpSoundRadverb(context, *sndBank); DumpSoundDucks(context, *sndBank); } } // namespace void AssetDumperSndBank::DumpPool(AssetDumpingContext& context, AssetPool* pool) { LoadedSoundBankHashes soundBankHashes; soundBankHashes.Initialize(); for (const auto* assetInfo : *pool) { if (!assetInfo->m_name.empty() && assetInfo->m_name[0] == ',') continue; DumpSndBank(context, soundBankHashes, *assetInfo); } }