Custom sound support

This commit is contained in:
Federico Cecchetto 2022-07-31 23:59:01 +02:00
parent f5970c6136
commit 141466035f
9 changed files with 1102 additions and 69 deletions

View File

@ -1,16 +1,18 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "filesystem.hpp"
#include "console.hpp"
#include "command.hpp"
#include "game/dvars.hpp"
#include "game/game.hpp"
#include "filesystem.hpp"
#include "console.hpp"
#include "command.hpp"
#include "sound.hpp"
#include <utils/io.hpp>
#include <utils/hook.hpp>
#include <utils/string.hpp>
#include <utils/flags.hpp>
namespace database
{
@ -74,64 +76,13 @@ namespace database
return get_sound_file_name(file_index, packed_file_offset);
}
void* disk_fs_sound_file_open_stub(game::StreamFile* file, game::StreamedSound* sound)
{
const auto name = "sound/" + get_sound_file_name(sound);
std::string path{};
if (!filesystem::find_file(name, &path))
{
return open_sound_handle_hook.invoke<void*>(file, sound);
}
const auto disk_fs = reinterpret_cast<game::DB_FileSysInterface*>(0x140BEFDC0);
const auto handle = disk_fs->vftbl->OpenFile(disk_fs, game::SF_ZONE_REGION, path.data());
if (handle)
{
const auto size = disk_fs->vftbl->Size(disk_fs, handle);
file->isPacked = 0;
file->length = size;
file->handle = handle;
}
else
{
file->handle = nullptr;
file->length = 0;
}
file->startOffset = 0;
return 0;
}
thread_local game::StreamedSound current_sound{};
void* sound_file_open_stub(game::StreamFile* file, game::StreamedSound* sound)
{
if (db_filesysImpl->current.integer == 1)
{
return disk_fs_sound_file_open_stub(file, sound);
}
else
{
current_sound = *sound;
return open_sound_handle_hook.invoke<void*>(file, sound);
}
}
game::DB_IFileSysFile* bnet_fs_open_file_stub(game::DB_FileSysInterface* this_, int folder, const char* file)
{
const auto _0 = gsl::finally([]
{
current_sound = {};
});
std::string name = file;
bool is_sound = false;
if (name.starts_with("soundfile") && name.ends_with(".pak"))
if (name.ends_with(".flac"))
{
is_sound = true;
name = "sound/" + get_sound_file_name(&current_sound);
name = "sound/" + name;
}
if (name.ends_with(".bik"))
@ -159,8 +110,6 @@ namespace database
bnet_file_handle_t bnet_handle{};
bnet_handle.stream = std::move(stream);
bnet_handle.offset = current_sound.filename.info.packed.offset;
bnet_file_handles[handle] = std::move(bnet_handle);
return handle;
}
@ -350,6 +299,12 @@ namespace database
if (result->asset.type == game::ASSET_TYPE_SOUND)
{
const auto sound = result->asset.header.sound;
if (utils::flags::has_flag("dumpsoundaliases"))
{
sound::dump_sound(sound);
}
for (auto i = 0; i < sound->count; i++)
{
const auto alias = &sound->head[i];
@ -423,7 +378,7 @@ namespace database
console::error("Error reading file %s\n", name);
}
std::vector<std::uint8_t> signature = {0x66, 0x4C, 0x61, 0x43, 0x00, 0x00, 0x00, 0x22};
std::vector<std::uint8_t> signature = {0x66, 0x4C, 0x61, 0x43};
const auto check_signature = [&](char* start)
{
@ -491,9 +446,6 @@ namespace database
db_fs_initialize_hook.create(game::DB_FSInitialize, db_fs_initialize_stub);
open_sound_handle_hook.create(0x1406233B0, sound_file_open_stub);
db_link_xasset_entry1_hook.create(0x140414900, db_link_xasset_entry1_stub);
// Allow bnet filesystem to also load files from disk
if (db_filesysImpl->current.integer == 0)
{
@ -510,6 +462,13 @@ namespace database
bink_io_seek_hook.create(0x140719200, bink_io_seek_stub);
}
if (!utils::flags::has_flag("sounddumputils"))
{
return;
}
db_link_xasset_entry1_hook.create(0x140414900, db_link_xasset_entry1_stub);
command::add("listSoundFiles", []()
{
for (const auto& packed : sound_files)

View File

@ -5,6 +5,7 @@
#include "command.hpp"
#include "console.hpp"
#include "localized_strings.hpp"
#include "sound.hpp"
#include <utils/hook.hpp>
#include <utils/concurrency.hpp>
@ -31,6 +32,15 @@ namespace fastfiles
game::XAssetHeader db_find_xasset_header_stub(game::XAssetType type, const char* name, int allow_create_default)
{
if (type == game::ASSET_TYPE_SOUND)
{
const auto res = sound::find_sound(name);
if (res.sound != nullptr)
{
return res;
}
}
const auto start = game::Sys_Milliseconds();
const auto result = db_find_xasset_header.invoke<game::XAssetHeader>(type, name, allow_create_default);
const auto diff = game::Sys_Milliseconds() - start;

View File

@ -48,7 +48,7 @@ namespace filesystem
{
std::vector<std::filesystem::path> paths{};
const auto code = game::SEH_GetCurrentLanguageCode();
const auto code = game::SEH_GetCurrentLanguageName();
paths.push_back(path);
paths.push_back(path / code);

View File

@ -13,6 +13,7 @@
#include "mapents.hpp"
#include "localized_strings.hpp"
#include "loadscreen.hpp"
#include "sound.hpp"
#include <utils/hook.hpp>
#include <utils/io.hpp>
@ -38,6 +39,7 @@ namespace mods
mapents::clear();
localized_strings::clear();
sound::clear();
db_release_xassets_hook.invoke<void>();
}

View File

@ -0,0 +1,953 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "game/dvars.hpp"
#include "sound.hpp"
#include "filesystem.hpp"
#include "console.hpp"
#include <utils/io.hpp>
#include <utils/memory.hpp>
#include <utils/hook.hpp>
#include <utils/concurrency.hpp>
#include <utils/string.hpp>
// https://github.com/skkuull/h1-zonetool/blob/main/src/client/zonetool/assets/sound.cpp
// https://github.com/skkuull/h1-zonetool/blob/main/src/client/zonetool/assets/loadedsound.cpp
namespace sound
{
namespace
{
utils::memory::allocator sound_allocator;
using loaded_sound_map = std::unordered_map<std::string, game::snd_alias_list_t*>;
utils::concurrency::container<loaded_sound_map, std::recursive_mutex> loaded_sounds;
#define FATAL(...) \
throw std::runtime_error(utils::string::va(__VA_ARGS__)); \
#define SOUND_STRING(entry, optional) \
if (j.HasMember(#entry) && j[#entry].IsString()) \
{ \
asset->entry = sound_allocator.duplicate_string(j[#entry].GetString()); \
} \
else if (!optional) \
{ \
FATAL("member '%s' does not exist or isn't of type 'String'\n", #entry); \
} \
#define SOUND_FLOAT(entry, optional) \
if (j.HasMember(#entry) && (j[#entry].IsFloat() || j[#entry].IsInt())) \
{ \
asset->entry = j[#entry].GetFloat(); \
} \
else if (!optional) \
{ \
FATAL("member '%s' does not exist or isn't of type 'Float'\n", #entry); \
} \
#define SOUND_INT(entry, optional) \
if (j.HasMember(#entry) && j[#entry].IsInt()) \
{ \
asset->entry = j[#entry].GetInt(); \
} \
else if (!optional) \
{ \
FATAL("member '%s' does not exist or isn't of type 'Int'\n", #entry); \
} \
#define SOUND_CHAR(entry, optional) \
if (j.HasMember(#entry) && j[#entry].IsInt()) \
{ \
asset->entry = static_cast<char>(j[#entry].GetInt()); \
} \
else if (!optional) \
{ \
FATAL("member '%s' does not exist or isn't of type 'Char'\n", #entry); \
} \
#define JSON_GET(parent, name, type, parent_name) \
(parent.HasMember(name) && parent[name].Is##type()) \
? parent[name].Get##type() \
: FATAL("'%s' member '%s' does not exist or it isn't of type '%s'", parent_name, name, #type) \
#define JSON_GET_OPTIONAL(parent, name, type, default_value) \
(parent.HasMember(name) && parent[name].Is##type()) \
? parent[name].Get##type() \
: default_value \
#define JSON_GET_CAST(parent, name, type, cast_type, parent_name) \
(parent.HasMember(name) && parent[name].Is##type()) \
? static_cast<cast_type>(parent[name].Get##type()) \
: FATAL("'%s' member '%s' does not exist or it isn't of type '%s'", parent_name, name, #type) \
#define JSON_CHECK(parent, name, type, parent_name) \
if (!parent.HasMember(name) || !parent[name].Is##type()) \
{ \
FATAL("'%s' member '%s' does not exist or it isn't of type '%s'", parent_name, name, #type) \
} \
std::string rapidjson_get_object_bytes(const rapidjson::Value& value)
{
std::string buffer{};
if (!value.IsArray())
{
return buffer;
}
for (auto i = 0; i < static_cast<int>(value.Size()); i++)
{
buffer += static_cast<char>(value[i].GetInt());
}
return buffer;
}
rapidjson::Value rapidjson_bytes_to_object(const std::vector<std::uint8_t>& bytes, rapidjson::Document& j)
{
rapidjson::Value arr{rapidjson::kArrayType};
for (const auto& byte : bytes)
{
arr.PushBack(byte, j.GetAllocator());
}
return arr;
}
game::LoadedSound* parse_flac(const std::string& name)
{
const auto path = "loaded_sound/"s + name + ".flac";
std::string data{};
if (!filesystem::read_file(path, &data))
{
return nullptr;
}
console::info("[Sound] Parsing flac %s\n", path.data());
auto* result = sound_allocator.allocate<game::LoadedSound>();
result->name = sound_allocator.duplicate_string(name);
result->info.loadedSize = static_cast<int>(data.size());
result->info.data = sound_allocator.allocate_array<char>(result->info.loadedSize);
std::memcpy(result->info.data, data.data(), data.size());
return result;
}
game::LoadedSound* parse_wav(const std::string& name)
{
const auto path = "loaded_sound/"s + name + ".wav";
std::string full_path{};
if (!filesystem::find_file(path, &full_path))
{
return nullptr;
}
console::info("[Sound] Parsing wav %s\n", path.data());
std::ifstream file;
file.open(full_path, std::ios::binary);
if (!file.is_open())
{
FATAL("failed to open loaded sound file: %s\n", name.data());
}
const auto result = sound_allocator.allocate<game::LoadedSound>();
unsigned int chunk_id_buffer{};
unsigned int chunk_size{};
file.read(reinterpret_cast<char*>(&chunk_id_buffer), 4);
if (chunk_id_buffer != 0x46464952) // RIFF
{
FATAL("%s: Invalid RIFF Header 0x%lX.", name.data(), chunk_id_buffer);
}
file.read(reinterpret_cast<char*>(&chunk_size), 4);
file.read(reinterpret_cast<char*>(&chunk_id_buffer), 4);
if (chunk_id_buffer != 0x45564157) // WAVE
{
FATAL("%s: Invalid WAVE Header 0x%lX.", name.data(), chunk_id_buffer);
}
while (!result->info.data && !file.eof())
{
file.read(reinterpret_cast<char*>(&chunk_id_buffer), 4);
file.read(reinterpret_cast<char*>(&chunk_size), 4);
switch (chunk_id_buffer)
{
case 0x20746D66: // fmt
if (chunk_size >= 16)
{
short format{};
file.read(reinterpret_cast<char*>(&format), 2);
if (format != 1 && format != 17)
{
FATAL("%s: Invalid wave format %i.", name.data(), format);
}
result->info.format = format;
short num_channels{};
file.read(reinterpret_cast<char*>(&num_channels), 2);
result->info.channels = static_cast<char>(num_channels);
int sample_rate{};
file.read(reinterpret_cast<char*>(&sample_rate), 4);
result->info.sampleRate = sample_rate;
int byte_rate{};
file.read(reinterpret_cast<char*>(&byte_rate), 4);
short block_align{};
file.read(reinterpret_cast<char*>(&block_align), 2);
result->info.blockAlign = static_cast<char>(block_align);
short bit_per_sample{};
file.read(reinterpret_cast<char*>(&bit_per_sample), 2);
result->info.numBits = static_cast<char>(bit_per_sample);
if (chunk_size > 16)
{
file.seekg(chunk_size - 16, std::ios::cur);
}
}
break;
case 0x61746164: // data
result->info.data = sound_allocator.allocate_array<char>(chunk_size);
file.read(result->info.data, chunk_size);
result->info.loadedSize = chunk_size;
result->info.dataByteCount = result->info.loadedSize;
result->info.numSamples = result->info.dataByteCount / (result->info.channels * result->info.numBits / 8);
break;
default:
if (chunk_size > 0)
{
file.seekg(chunk_size, std::ios::cur);
}
break;
}
}
if (!result->info.data)
{
FATAL("%s: Could not read sounddata.", name.data());
return nullptr;
}
result->name = sound_allocator.duplicate_string(name);
return result;
}
game::LoadedSound* parse_loaded_sound(const std::string& name)
{
std::string full_path{};
auto path = "loaded_sound/"s + name;
if (filesystem::find_file(path + ".wav", &full_path))
{
return parse_wav(name);
}
if (filesystem::find_file(path + ".flac", &full_path))
{
return parse_flac(name);
}
console::warn("Sound %s not found, falling back to default sound\n", path.data());
return game::DB_FindXAssetHeader(game::ASSET_TYPE_LOADED_SOUND, name.data(), true).loaded_sound;
}
void parse_sound_alias(const rapidjson::Value& j, game::snd_alias_t* asset)
{
SOUND_STRING(aliasName, false);
SOUND_STRING(secondaryAliasName, true);
SOUND_STRING(chainAliasName, true);
SOUND_STRING(subtitle, true);
SOUND_STRING(mixerGroup, true);
if (!j.HasMember("soundfile") || !j["soundfile"].IsObject())
{
FATAL("missing 'soundfile' object");
}
const auto& sound_file = j["soundfile"];
const auto sound_file_type = JSON_GET(sound_file, "type", Int, "soundfile");
asset->soundFile = sound_allocator.allocate<game::SoundFile>();
asset->soundFile->type = static_cast<game::snd_alias_type_t>(sound_file_type);
asset->soundFile->exists = true;
if (asset->soundFile->type == game::SAT_LOADED)
{
const auto name = JSON_GET(sound_file, "name", String, "soundfile is missing 'name'");
asset->soundFile->u.loadSnd = parse_loaded_sound(name);
}
else if (asset->soundFile->type == game::SAT_STREAMED)
{
asset->soundFile->u.streamSnd.totalMsec =
JSON_GET(sound_file, "totalMsec", Uint, "soundfile");
asset->soundFile->u.streamSnd.filename.isLocalized =
JSON_GET(sound_file, "isLocalized", Bool, "soundfile");
asset->soundFile->u.streamSnd.filename.isStreamed =
JSON_GET(sound_file, "isStreamed", Bool, "soundfile");
asset->soundFile->u.streamSnd.filename.fileIndex =
JSON_GET_CAST(sound_file, "fileIndex", Int, unsigned short, "soundfile");
if (asset->soundFile->u.streamSnd.filename.fileIndex)
{
JSON_CHECK(sound_file, "packed", Object, "soundfile");
asset->soundFile->u.streamSnd.filename.info.packed.offset =
JSON_GET(sound_file["packed"], "offset", Uint64, "soundfile.raw");
asset->soundFile->u.streamSnd.filename.info.packed.length =
JSON_GET(sound_file["packed"], "length", Uint64, "soundfile.raw");
}
else
{
JSON_CHECK(sound_file, "raw", Object, "soundfile");
const auto dir = JSON_GET(sound_file["raw"], "dir", String, "soundfile.raw");
const auto name = JSON_GET(sound_file["raw"], "name", String, "soundfile.raw");
asset->soundFile->u.streamSnd.filename.info.raw.dir = sound_allocator.duplicate_string(dir);
asset->soundFile->u.streamSnd.filename.info.raw.name = sound_allocator.duplicate_string(name);
}
}
else
{
FATAL("Sound alias has invalid soundFile type %i", asset->soundFile->type);
}
SOUND_INT(flags, false);
SOUND_INT(sequence, false);
SOUND_FLOAT(volMin, false);
SOUND_FLOAT(volMax, false);
SOUND_INT(volModIndex, false);
SOUND_FLOAT(pitchMin, false);
SOUND_FLOAT(pitchMax, false);
SOUND_FLOAT(distMin, false);
SOUND_FLOAT(distMax, false);
SOUND_FLOAT(velocityMin, false);
SOUND_CHAR(masterPriority, false);
SOUND_FLOAT(masterPercentage, false);
SOUND_FLOAT(slavePercentage, false);
SOUND_FLOAT(probability, false);
SOUND_INT(startDelay, false);
if (j.HasMember("sndContext") && j["sndContext"].IsString())
{
asset->sndContext = game::DB_FindXAssetHeader(game::ASSET_TYPE_SNDCONTEXT,
j["sndContext"].GetString(), false).snd_context;
}
if (j.HasMember("sndCurve") && j["sndCurve"].IsString())
{
asset->sndCurve = game::DB_FindXAssetHeader(game::ASSET_TYPE_SNDCURVE,
j["sndCurve"].GetString(), false).snd_curve;
}
if (j.HasMember("lpfCurve") && j["lpfCurve"].IsString())
{
asset->lpfCurve = game::DB_FindXAssetHeader(game::ASSET_TYPE_LPFCURVE,
j["lpfCurve"].GetString(), false).snd_curve;
}
if (j.HasMember("hpfCurve") && j["hpfCurve"].IsString())
{
asset->hpfCurve = game::DB_FindXAssetHeader(game::ASSET_TYPE_LPFCURVE,
j["hpfCurve"].GetString(), false).snd_curve;
}
if (j.HasMember("reverbSendCurve") && j["reverbSendCurve"].IsString())
{
asset->reverbSendCurve = game::DB_FindXAssetHeader(game::ASSET_TYPE_REVERBSENDCURVE,
j["reverbSendCurve"].GetString(), false).snd_curve;
}
if (j.HasMember("speakerMap") && j["speakerMap"].IsObject())
{
asset->speakerMap = sound_allocator.allocate<game::SpeakerMap>();
const auto& speaker_map = j["speakerMap"];
const auto speaker_map_name = JSON_GET(speaker_map, "name", String, "speakerMap");
const auto is_default = JSON_GET(speaker_map, "isDefault", Bool, "speakerMap");
asset->speakerMap->name = sound_allocator.duplicate_string(speaker_map_name);
asset->speakerMap->isDefault = is_default;
if (speaker_map.HasMember("channelMaps") && speaker_map["channelMaps"].IsArray())
{
const auto& channel_maps = speaker_map["channelMaps"];
for (char x = 0; x < 2; x++)
{
for (char y = 0; y < 2; y++)
{
const auto index = static_cast<unsigned int>((x & 0x01) << 1 | y & 0x01);
if (index >= channel_maps.Size() || !channel_maps[index].IsObject())
{
FATAL("channelMaps at index %i does not exist", index);
}
const auto& channel_map = channel_maps[index];
asset->speakerMap->channelMaps[x][y].speakerCount = JSON_GET(channel_map,
"speakerCount", Int, "speakerMap.channelMaps[]");
if (!channel_map.HasMember("speakers") || !channel_map["speakers"].IsArray())
{
FATAL("channelMap does not have a 'speakers' member or it isn't an array");
}
const auto& speakers = channel_map["speakers"];
for (auto speaker = 0; speaker < asset->speakerMap->channelMaps[x][y].speakerCount;
speaker++)
{
if (static_cast<unsigned int>(speaker) < speakers.Size() && speakers[speaker].IsObject())
{
const auto& jspeaker = speakers[speaker];
asset->speakerMap->channelMaps[x][y].speakers[speaker].speaker =
JSON_GET_CAST(jspeaker, "speaker", Int, char, "speakerMap.channelMaps.speakers[]");
asset->speakerMap->channelMaps[x][y].speakers[speaker].numLevels =
JSON_GET_CAST(jspeaker, "numLevels", Int, char, "speakerMap.channelMaps.speakers[]");
asset->speakerMap->channelMaps[x][y].speakers[speaker].levels[0] =
JSON_GET(jspeaker, "levels0", Float, "speakerMap.channelMaps.speakers[]");
asset->speakerMap->channelMaps[x][y].speakers[speaker].levels[1] =
JSON_GET(jspeaker, "levels1", Float, "speakerMap.channelMaps.speakers[]");
}
else
{
FATAL("speaker at index %i does not exist or is not an object", speaker);
}
}
}
}
}
}
/*SOUND_CHAR(allowDoppler);
if (j.HasMember("dopplerPreset") && !j["dopplerPreset"].IsNull())
{
asset->dopplerPreset = game::DB_FindXAssetHeader(game::ASSET_TYPE_DOPPLERPRESET,
j["dopplerPreset"].GetString(), false).doppler_preset;
}*/
if (j.HasMember("unknown") && j["unknown"].IsObject())
{
const auto& snd_unknown = j["unknown"];
const auto& pad0 = rapidjson_get_object_bytes(snd_unknown["pad"][0]);
const auto& pad1 = rapidjson_get_object_bytes(snd_unknown["pad"][1]);
const auto& pad2 = rapidjson_get_object_bytes(snd_unknown["pad"][2]);
const auto& pad3 = rapidjson_get_object_bytes(snd_unknown["pad"][3]);
std::memcpy(asset->__pad0, pad0.data(), pad0.size());
std::memcpy(asset->__pad1, pad1.data(), pad1.size());
std::memcpy(asset->__pad2, pad2.data(), pad2.size());
std::memcpy(asset->__pad3, pad3.data(), pad3.size());
asset->u4 = JSON_GET_OPTIONAL(snd_unknown, "u4", Int, 0);
asset->u5 = JSON_GET_OPTIONAL(snd_unknown, "u5", Int, 0);
asset->u18 = static_cast<char>(JSON_GET_OPTIONAL(snd_unknown, "u18", Int, 0));
asset->u20 = static_cast<char>(JSON_GET_OPTIONAL(snd_unknown, "u20", Int, 0));
asset->u34 = JSON_GET_OPTIONAL(snd_unknown, "u34", Float, 0.f);
}
}
game::snd_alias_list_t* parse_sound_alias_list(const rapidjson::Document& j)
{
const auto asset = sound_allocator.allocate<game::snd_alias_list_t>();
SOUND_STRING(aliasName, false);
asset->count = JSON_GET_CAST(j, "count", Int, char, "sound");
asset->head = sound_allocator.allocate_array<game::snd_alias_t>(asset->count);
JSON_CHECK(j, "head", Array, "sound");
const auto head = j["head"].GetArray();
for (auto i = 0; i < static_cast<int>(asset->count) && head.Size(); i++)
{
parse_sound_alias(head[i], &asset->head[i]);
}
if (j.HasMember("unknownArray") && j["unknownArray"].IsArray())
{
const auto& unk = j["unknownArray"];
asset->unkCount = static_cast<unsigned char>(unk.Size());
asset->unk = sound_allocator.allocate_array<short>(asset->unkCount);
for (unsigned char i = 0; i < asset->unkCount && unk.Size(); i++)
{
asset->unk[i] = static_cast<short>(unk[i].GetInt());
}
}
return asset;
}
bool sound_exists(const std::string& name)
{
return loaded_sounds.access<bool>([&](loaded_sound_map& map)
{
const auto i = map.find(name);
if (i != map.end())
{
return true;
}
return find_sound(name.data()).sound != nullptr;
});
}
utils::hook::detour db_is_xasset_default_hook;
bool db_is_xasset_default_stub(game::XAssetType type, const char* name)
{
if (type == game::ASSET_TYPE_SOUND)
{
const auto res = db_is_xasset_default_hook.invoke<bool>(type, name);
if (!res)
{
return res;
}
return !sound_exists(name);
}
return db_is_xasset_default_hook.invoke<bool>(type, name);
}
utils::hook::detour db_xasset_exists_hook;
bool db_xasset_exists_stub(game::XAssetType type, const char* name)
{
if (type == game::ASSET_TYPE_SOUND)
{
const auto res = db_xasset_exists_hook.invoke<bool>(type, name);
if (res)
{
return true;
}
return sound_exists(name);
}
return db_xasset_exists_hook.invoke<bool>(type, name);
}
utils::hook::detour scr_table_lookup_hook;
void scr_table_lookup_stub()
{
const auto table = game::Scr_GetString(0);
const auto search_column = game::Scr_GetInt(1);
const auto search_value = game::Scr_GetString(2);
const auto return_row = game::Scr_GetInt(3);
if (table != "mp/sound/soundlength.csv"s || search_column != 0 || return_row != 1)
{
return scr_table_lookup_hook.invoke<void>();
}
std::optional<int> new_value{};
loaded_sounds.access([&](loaded_sound_map& map)
{
const auto i = map.find(search_value);
if (i == map.end())
{
return;
}
const auto sound_list = i->second;
if (sound_list->count)
{
const auto sound = &sound_list->head[0];
if (sound->soundFile && sound->soundFile->type == game::SAT_STREAMED)
{
new_value = sound->soundFile->u.streamSnd.totalMsec;
}
}
});
if (new_value.has_value())
{
game::Scr_AddString(utils::string::va("%i\n", new_value.value()));
}
else
{
scr_table_lookup_hook.invoke<void>();
}
}
void com_sprintf_raw_sound_localized_stub(char* buffer, int size, const char* fmt,
const char* lang, const char* name, const char* extension)
{
sprintf_s(buffer, size, "%s%s", name, extension);
}
void com_sprintf_raw_sound_stub(char* buffer, int size, const char* fmt,
const char* name, const char* extension)
{
sprintf_s(buffer, size, "%s%s", name, extension);
}
utils::hook::detour snd_is_music_playing_hook;
bool snd_is_music_playing_stub(void* a1)
{
if (a1 == nullptr)
{
return true; // dont play music
}
return snd_is_music_playing_hook.invoke<bool>(a1);
}
}
void dump_sound(game::snd_alias_list_t* asset)
{
if (asset == nullptr)
{
return;
}
rapidjson::Document j;
j.SetObject();
j.AddMember("aliasName", rapidjson::StringRef(asset->aliasName), j.GetAllocator());
j.AddMember("count", asset->count, j.GetAllocator());
rapidjson::Value head{rapidjson::kArrayType};
for (auto i = 0; i < asset->count; i++)
{
const auto snd_head = &asset->head[i];
rapidjson::Value entry{rapidjson::kObjectType};
entry.AddMember("aliasName", rapidjson::StringRef(snd_head->aliasName), j.GetAllocator());
if (snd_head->secondaryAliasName)
{
entry.AddMember("secondaryAliasName", rapidjson::StringRef(snd_head->secondaryAliasName), j.GetAllocator());
}
else
{
entry.AddMember("secondaryAliasName", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->chainAliasName)
{
entry.AddMember("chainAliasName", rapidjson::StringRef(snd_head->chainAliasName), j.GetAllocator());
}
else
{
entry.AddMember("chainAliasName", rapidjson::Value{ rapidjson::kNullType }, j.GetAllocator());
}
if (snd_head->subtitle)
{
entry.AddMember("subtitle", rapidjson::StringRef(snd_head->subtitle), j.GetAllocator());
}
else
{
entry.AddMember("subtitle", rapidjson::Value{ rapidjson::kNullType }, j.GetAllocator());
}
if (snd_head->mixerGroup)
{
entry.AddMember("mixerGroup", rapidjson::StringRef(snd_head->mixerGroup), j.GetAllocator());
}
else
{
entry.AddMember("mixerGroup", rapidjson::Value{ rapidjson::kNullType }, j.GetAllocator());
}
if (snd_head->soundFile)
{
rapidjson::Value sound_file{rapidjson::kObjectType};
sound_file.AddMember("type", snd_head->soundFile->type, j.GetAllocator());
if (snd_head->soundFile->exists)
{
if (snd_head->soundFile->type == game::SAT_LOADED)
{
sound_file.AddMember("name", rapidjson::StringRef(snd_head->soundFile->u.loadSnd->name), j.GetAllocator());
}
else if (snd_head->soundFile->type == game::SAT_STREAMED)
{
sound_file.AddMember("totalMsec", snd_head->soundFile->u.streamSnd.totalMsec, j.GetAllocator());
sound_file.AddMember("isLocalized", snd_head->soundFile->u.streamSnd.filename.isLocalized, j.GetAllocator());
sound_file.AddMember("isStreamed", snd_head->soundFile->u.streamSnd.filename.isStreamed, j.GetAllocator());
sound_file.AddMember("fileIndex", snd_head->soundFile->u.streamSnd.filename.fileIndex, j.GetAllocator());
if (snd_head->soundFile->u.streamSnd.filename.fileIndex)
{
rapidjson::Value packed{rapidjson::kObjectType};
packed.AddMember("offset", snd_head->soundFile->u.streamSnd.filename.info.packed.offset, j.GetAllocator());
packed.AddMember("length", snd_head->soundFile->u.streamSnd.filename.info.packed.length, j.GetAllocator());
sound_file.AddMember("packed", packed, j.GetAllocator());
}
else
{
rapidjson::Value raw{rapidjson::kObjectType};
raw.AddMember("dir", rapidjson::StringRef(snd_head->soundFile->u.streamSnd.filename.info.raw.dir), j.GetAllocator());
raw.AddMember("name", rapidjson::StringRef(snd_head->soundFile->u.streamSnd.filename.info.raw.name), j.GetAllocator());
sound_file.AddMember("packed", raw, j.GetAllocator());
}
}
}
entry.AddMember("soundfile", sound_file, j.GetAllocator());
}
entry.AddMember("flags", snd_head->flags, j.GetAllocator());
entry.AddMember("sequence", snd_head->sequence, j.GetAllocator());
entry.AddMember("volMin", snd_head->volMin, j.GetAllocator());
entry.AddMember("volMax", snd_head->volMax, j.GetAllocator());
entry.AddMember("volModIndex", snd_head->volModIndex, j.GetAllocator());
entry.AddMember("pitchMin", snd_head->pitchMin, j.GetAllocator());
entry.AddMember("pitchMax", snd_head->pitchMax, j.GetAllocator());
entry.AddMember("distMin", snd_head->distMin, j.GetAllocator());
entry.AddMember("distMax", snd_head->distMax, j.GetAllocator());
entry.AddMember("velocityMin", snd_head->velocityMin, j.GetAllocator());
entry.AddMember("masterPriority", snd_head->masterPriority, j.GetAllocator());
entry.AddMember("masterPercentage", snd_head->masterPercentage, j.GetAllocator());
entry.AddMember("slavePercentage", snd_head->slavePercentage, j.GetAllocator());
entry.AddMember("probability", snd_head->probability, j.GetAllocator());
entry.AddMember("startDelay", snd_head->startDelay, j.GetAllocator());
if (snd_head->sndContext)
{
entry.AddMember("sndContext", rapidjson::StringRef(snd_head->sndContext->name), j.GetAllocator());
}
else
{
entry.AddMember("sndContext", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->sndCurve)
{
entry.AddMember("sndCurve", rapidjson::StringRef(snd_head->sndCurve->name), j.GetAllocator());
}
else
{
entry.AddMember("sndCurve", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->lpfCurve)
{
entry.AddMember("lpfCurve", rapidjson::StringRef(snd_head->lpfCurve->name), j.GetAllocator());
}
else
{
entry.AddMember("lpfCurve", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->hpfCurve)
{
entry.AddMember("hpfCurve", rapidjson::StringRef(snd_head->hpfCurve->name), j.GetAllocator());
}
else
{
entry.AddMember("hpfCurve", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->reverbSendCurve)
{
entry.AddMember("reverbSendCurve", rapidjson::StringRef(snd_head->reverbSendCurve->name), j.GetAllocator());
}
else
{
entry.AddMember("reverbSendCurve", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
if (snd_head->speakerMap)
{
rapidjson::Value speaker_map{rapidjson::kObjectType};
rapidjson::Value channel_maps{rapidjson::kArrayType};
for (char x = 0; x < 2; x++)
{
for (char y = 0; y < 2; y++)
{
rapidjson::Value channel_map{rapidjson::kObjectType};
channel_map.AddMember("speakerCount", snd_head->speakerMap->channelMaps[x][y].speakerCount, j.GetAllocator());
rapidjson::Value speakers{rapidjson::kArrayType};
for (int speaker = 0; speaker < snd_head->speakerMap->channelMaps[x][y].speakerCount; speaker++)
{
rapidjson::Value jspeaker{rapidjson::kObjectType};
jspeaker.AddMember("speaker", snd_head->speakerMap->channelMaps[x][y].speakers[speaker].speaker, j.GetAllocator());
jspeaker.AddMember("numLevels", snd_head->speakerMap->channelMaps[x][y].speakers[speaker].numLevels, j.GetAllocator());
jspeaker.AddMember("levels0", snd_head->speakerMap->channelMaps[x][y].speakers[speaker].levels[0], j.GetAllocator());
jspeaker.AddMember("levels1", snd_head->speakerMap->channelMaps[x][y].speakers[speaker].levels[1], j.GetAllocator());
speakers.PushBack(jspeaker, j.GetAllocator());
}
channel_map.AddMember("speakers", speakers, j.GetAllocator());
channel_maps.PushBack(channel_map, j.GetAllocator());
}
}
speaker_map.AddMember("name", rapidjson::StringRef(snd_head->speakerMap->name), j.GetAllocator());
speaker_map.AddMember("isDefault", snd_head->speakerMap->isDefault, j.GetAllocator());
speaker_map.AddMember("channelMaps", channel_maps, j.GetAllocator());
entry.AddMember("speakerMap", speaker_map, j.GetAllocator());
}
else
{
j.AddMember("speakerMap", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}
/*entry.AddMember("allowDoppler", snd_head->allowDoppler, j.GetAllocator());
if (snd_head->dopplerPreset)
{
entry.AddMember("dopplerPreset", rapidjson::StringRef(snd_head->dopplerPreset->name), j.GetAllocator());
}
else
{
entry.AddMember("dopplerPreset", rapidjson::Value{rapidjson::kNullType}, j.GetAllocator());
}*/
rapidjson::Value unknown{rapidjson::kObjectType};
rapidjson::Value pad{rapidjson::kArrayType};
const auto pad0 = std::vector<std::uint8_t>(snd_head->__pad0, snd_head->__pad0 + sizeof(snd_head->__pad0));
const auto pad1 = std::vector<std::uint8_t>(snd_head->__pad1, snd_head->__pad1 + sizeof(snd_head->__pad1));
const auto pad2 = std::vector<std::uint8_t>(snd_head->__pad2, snd_head->__pad2 + sizeof(snd_head->__pad2));
const auto pad3 = std::vector<std::uint8_t>(snd_head->__pad3, snd_head->__pad3 + sizeof(snd_head->__pad3));
auto rpad0 = rapidjson_bytes_to_object(pad0, j);
auto rpad1 = rapidjson_bytes_to_object(pad1, j);
auto rpad2 = rapidjson_bytes_to_object(pad2, j);
auto rpad3 = rapidjson_bytes_to_object(pad3, j);
pad.PushBack(rpad0, j.GetAllocator());
pad.PushBack(rpad1, j.GetAllocator());
pad.PushBack(rpad2, j.GetAllocator());
pad.PushBack(rpad3, j.GetAllocator());
unknown.AddMember("pad", pad, j.GetAllocator());
unknown.AddMember("u4", snd_head->u4, j.GetAllocator());
unknown.AddMember("u5", snd_head->u5, j.GetAllocator());
unknown.AddMember("u18", snd_head->u18, j.GetAllocator());
unknown.AddMember("u20", snd_head->u20, j.GetAllocator());
unknown.AddMember("u34", snd_head->u34, j.GetAllocator());
entry.AddMember("unknown", unknown, j.GetAllocator());
head.PushBack(entry, j.GetAllocator());
}
j.AddMember("head", head, j.GetAllocator());
rapidjson::Value unknown_array{rapidjson::kArrayType};
for (unsigned char i = 0; i < asset->unkCount; i++)
{
unknown_array.PushBack(asset->unk[i], j.GetAllocator());
}
j.AddMember("unknownArray", unknown_array, j.GetAllocator());
std::string path = "dumps/sounds/"s + asset->aliasName;
rapidjson::StringBuffer buffer{};
rapidjson::PrettyWriter<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
writer(buffer);
writer.SetIndent(' ', 4);
j.Accept(writer);
utils::io::write_file(path, std::string{buffer.GetString(), buffer.GetLength()}, false);
}
game::XAssetHeader find_sound(const char* name)
{
bool found = false;
const auto res = loaded_sounds.access<game::XAssetHeader>([&](loaded_sound_map& map)
{
const auto i = map.find(name);
if (i != map.end())
{
found = true;
return static_cast<game::XAssetHeader>(i->second);
}
return static_cast<game::XAssetHeader>(nullptr);
});
if (found)
{
return res;
}
std::string path{};
if (!filesystem::find_file("sounds/"s + name, &path))//
{
return static_cast<game::XAssetHeader>(nullptr);
}
const auto data = utils::io::read_file(path);
rapidjson::Document j;
j.Parse(data.data());
try
{
console::info("[Sound] Loading sound %s\n", name);
const auto sound = parse_sound_alias_list(j);
loaded_sounds.access([&](loaded_sound_map& map)
{
map[name] = sound;
});
return static_cast<game::XAssetHeader>(sound);
}
catch (const std::exception& e)
{
console::error("[Sound] Error loading sound %s: %s\n", name, e.what());
return static_cast<game::XAssetHeader>(nullptr);
}
}
void clear()
{
sound_allocator.clear();
loaded_sounds.access([](loaded_sound_map& map)
{
map.clear();
});
}
class component final : public component_interface
{
public:
void post_unpack() override
{
db_is_xasset_default_hook.create(0x1404143C0, db_is_xasset_default_stub);
db_xasset_exists_hook.create(0x140417FD0, db_xasset_exists_stub);
scr_table_lookup_hook.create(0x1404EFD40, scr_table_lookup_stub);
// remove raw/sound or raw/language/sound prefix when loading raw sounds
utils::hook::call(0x140622FEF, com_sprintf_raw_sound_localized_stub);
utils::hook::call(0x14062306C, com_sprintf_raw_sound_stub);
// fix playing non-existing music crashing
snd_is_music_playing_hook.create(0x1407C58A0, snd_is_music_playing_stub);
}
};
}
REGISTER_COMPONENT(sound::component)

View File

@ -0,0 +1,9 @@
#pragma once
namespace sound
{
void dump_sound(game::snd_alias_list_t* asset);
game::XAssetHeader find_sound(const char* name);
void clear();
}

View File

@ -679,9 +679,31 @@ namespace game
StreamFileNamePacked packed;
};
struct SpeakerLevels
{
char speaker;
char numLevels;
float levels[2];
};
struct ChannelMap
{
int speakerCount;
SpeakerLevels speakers[6];
};
struct SpeakerMap
{
bool isDefault;
const char* name;
int a;
ChannelMap channelMaps[2][2];
}; //static_assert(sizeof(SpeakerMap) == 0x148);
struct StreamFileName
{
unsigned __int16 isLocalized;
bool isLocalized;
bool isStreamed;
unsigned __int16 fileIndex;
StreamFileInfo info;
};
@ -726,19 +748,88 @@ namespace game
StreamedSound streamSnd;
};
enum snd_alias_type_t : std::int8_t
{
SAT_UNKNOWN = 0x0,
SAT_LOADED = 0x1,
SAT_STREAMED = 0x2,
SAT_PRIMED = 0x3,
SAT_COUNT = 0x4,
};
struct SoundFile
{
char type;
snd_alias_type_t type;
char exists;
SoundFileRef u;
};
struct SndContext
{
const char* name;
char __pad0[8];
};
struct SndCurve
{
bool isDefault;
union
{
const char* filename;
const char* name;
};
unsigned short knotCount;
float knots[16][2];
}; static_assert(sizeof(SndCurve) == 0x98);
struct DopplerPreset
{
const char* name;
float speedOfSound;
float playerVelocityScale;
float minPitch;
float maxPitch;
float smoothing;
}; static_assert(sizeof(DopplerPreset) == 0x20);
struct snd_alias_t
{
const char* aliasName;
char __pad0[24];
const char* subtitle;
const char* secondaryAliasName;
const char* chainAliasName;
SoundFile* soundFile;
char __pad1[216];
const char* mixerGroup;
char __pad0[8];
int sequence;
int u4;
int u5;
float volMin;
float volMax;
int volModIndex;
float pitchMin;
float pitchMax;
float distMin;
float distMax;
float velocityMin;
int flags;
char masterPriority;
float masterPercentage;
float slavePercentage;
char u18;
float probability;
char u20; // value: 0-4
SndContext* sndContext;
char __pad1[12];
int startDelay;
SndCurve* sndCurve;
char __pad2[8];
SndCurve* lpfCurve;
SndCurve* hpfCurve;
SndCurve* reverbSendCurve;
SpeakerMap* speakerMap;
char __pad3[47];
float u34;
};
static_assert(sizeof(snd_alias_t) == 256);
@ -747,7 +838,7 @@ namespace game
{
const char* aliasName;
snd_alias_t* head;
void* unk;
short* unk;
unsigned char count;
unsigned char unkCount;
char __pad0[6];
@ -872,6 +963,10 @@ namespace game
AddonMapEnts* addon_mapents;
LocalizeEntry* localize;
snd_alias_list_t* sound;
DopplerPreset* doppler_preset;
SndContext* snd_context;
SndCurve* snd_curve;
LoadedSound* loaded_sound;
};
struct XAsset

View File

@ -116,7 +116,9 @@ namespace game
WEAK symbol<unsigned int(unsigned int threadId)> Scr_GetSelf{0x1405C57C0};
WEAK symbol<void()> Scr_ErrorInternal{0x1405C6EC0};
WEAK symbol<const char*(unsigned int index)> Scr_GetString{0x1405C7C20};
WEAK symbol<int(unsigned int index)> Scr_GetInt{0x1405C7890};
WEAK symbol<void(int value)> Scr_AddInt{0x1405C69A0};
WEAK symbol<void(const char* value)> Scr_AddString{0x1405C6A80};
WEAK symbol<unsigned int(unsigned int localId, const char* pos, unsigned int paramcount)> VM_Execute{0x1405C8DB0};

View File

@ -79,6 +79,9 @@
#include <MinHook.h>
#include <tomcrypt.h>
#define RAPIDJSON_NOEXCEPT
#define RAPIDJSON_ASSERT(cond) if(cond); else throw std::runtime_error("rapidjson assert fail");
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>