diff options
author | HappyDOGE <28511119+HappyDOGE@users.noreply.github.com> | 2022-01-04 14:19:22 +0300 |
---|---|---|
committer | HappyDOGE <28511119+HappyDOGE@users.noreply.github.com> | 2022-01-04 14:19:22 +0300 |
commit | 0adbb7b60f5134a27558404c6c0ffacd42cb38c2 (patch) | |
tree | 53bcd279a5f2145955abe4449e83bebb230a8b09 | |
parent | 7d804649313949c1236463e7277c68200eb97769 (diff) | |
download | NorthstarLauncher-0adbb7b60f5134a27558404c6c0ffacd42cb38c2.tar.gz NorthstarLauncher-0adbb7b60f5134a27558404c6c0ffacd42cb38c2.zip |
audio override
-rw-r--r-- | NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj | 11 | ||||
-rw-r--r-- | NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters | 11 | ||||
-rw-r--r-- | NorthstarDedicatedTest/audio.cpp | 433 | ||||
-rw-r--r-- | NorthstarDedicatedTest/audio.h | 47 | ||||
-rw-r--r-- | NorthstarDedicatedTest/audio_asm.asm | 8 | ||||
-rw-r--r-- | NorthstarDedicatedTest/dllmain.cpp | 4 | ||||
-rw-r--r-- | NorthstarDedicatedTest/modmanager.cpp | 19 |
7 files changed, 533 insertions, 0 deletions
diff --git a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj index 299cf2ec..2b1cfb2d 100644 --- a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj +++ b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj @@ -34,6 +34,7 @@ </PropertyGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <ImportGroup Label="ExtensionSettings"> + <Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" /> </ImportGroup> <ImportGroup Label="Shared"> </ImportGroup> @@ -60,6 +61,8 @@ <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile> <LanguageStandard>stdcpp17</LanguageStandard> <AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <BufferSecurityCheck> + </BufferSecurityCheck> </ClCompile> <Link> <SubSystem>Windows</SubSystem> @@ -88,6 +91,8 @@ <LanguageStandard>stdcpp17</LanguageStandard> <AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + <BufferSecurityCheck> + </BufferSecurityCheck> </ClCompile> <Link> <SubSystem>Windows</SubSystem> @@ -106,6 +111,7 @@ </PreBuildEvent> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="audio.h" /> <ClInclude Include="bansystem.h" /> <ClInclude Include="chatcommand.h" /> <ClInclude Include="clientauthhooks.h" /> @@ -545,6 +551,7 @@ <ClInclude Include="squirrel.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="audio.cpp" /> <ClCompile Include="bansystem.cpp" /> <ClCompile Include="chatcommand.cpp" /> <ClCompile Include="clientauthhooks.cpp" /> @@ -619,7 +626,11 @@ <None Include="include\openssl\x509_vfy.h.in" /> <None Include="include\spdlog\fmt\bundled\LICENSE.rst" /> </ItemGroup> + <ItemGroup> + <MASM Include="audio_asm.asm" /> + </ItemGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <ImportGroup Label="ExtensionTargets"> + <Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" /> </ImportGroup> </Project>
\ No newline at end of file diff --git a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters index aaa99a14..41072c60 100644 --- a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters +++ b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters @@ -1434,6 +1434,9 @@ <ClInclude Include="languagehooks.h"> <Filter>Header Files\Client</Filter> </ClInclude> + <ClInclude Include="audio.h"> + <Filter>Header Files\Client</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="dllmain.cpp"> @@ -1556,6 +1559,9 @@ <ClCompile Include="languagehooks.cpp"> <Filter>Source Files\Client</Filter> </ClCompile> + <ClCompile Include="audio.cpp"> + <Filter>Source Files\Client</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="include\spdlog\fmt\bundled\LICENSE.rst"> @@ -1643,4 +1649,9 @@ <Filter>Header Files\include\openssl\crypto</Filter> </None> </ItemGroup> + <ItemGroup> + <MASM Include="audio_asm.asm"> + <Filter>Source Files\Client</Filter> + </MASM> + </ItemGroup> </Project>
\ No newline at end of file diff --git a/NorthstarDedicatedTest/audio.cpp b/NorthstarDedicatedTest/audio.cpp new file mode 100644 index 00000000..6559a12c --- /dev/null +++ b/NorthstarDedicatedTest/audio.cpp @@ -0,0 +1,433 @@ +#include "pch.h" +#include "audio.h" +#include "dedicated.h" + +#include "rapidjson/error/en.h" +#include <fstream> +#include <iostream> +#include <sstream> +#include <random> +#include "convar.h" + +extern "C" { + // should be called only in LoadSampleMetadata_Hook + extern void* __fastcall Audio_GetParentEvent(); +} + +ConVar* Cvar_ns_print_played_sounds; + +CustomAudioManager g_CustomAudioManager; + +EventOverrideData::EventOverrideData() +{ + spdlog::warn("Initialised struct EventOverrideData without any data!"); + LoadedSuccessfully = false; +} + +EventOverrideData::EventOverrideData(const std::string& data, const fs::path& path) +{ + if (data.length() <= 0) + { + spdlog::error("Failed reading audio override file {}: file is empty", path.string()); + return; + } + + fs::path samplesFolder = path; + samplesFolder = samplesFolder.replace_extension(); + + if (!fs::exists(samplesFolder)) + { + spdlog::error("Failed reading audio override file {}: samples folder doesn't exist; should be named the same as the definition file without JSON extension.", path.string()); + return; + } + + rapidjson_document dataJson; + dataJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(data); + + // fail if parse error + if (dataJson.HasParseError()) + { + spdlog::error("Failed reading audio override file {}: encountered parse error \"{}\" at offset {}", path.string(), GetParseError_En(dataJson.GetParseError()), dataJson.GetErrorOffset()); + return; + } + + // fail if it's not a json obj (could be an array, string, etc) + if (!dataJson.IsObject()) + { + spdlog::error("Failed reading audio override file {}: file is not a JSON object", path.string()); + return; + } + + // fail if no event ids given + if (!dataJson.HasMember("EventId")) + { + spdlog::error("Failed reading audio override file {}: JSON object does not have the EventId property", path.string()); + return; + } + + // array of event ids + if (dataJson["EventId"].IsArray()) + { + for (auto& eventId : dataJson["EventId"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error("Failed reading audio override file {}: EventId array has a value of invalid type, all must be strings", path.string()); + return; + } + + EventIds.push_back(eventId.GetString()); + } + } + // singular event id + else if (dataJson["EventId"].IsString()) + { + EventIds.push_back(dataJson["EventId"].GetString()); + } + // incorrect type + else + { + spdlog::error("Failed reading audio override file {}: EventId property is of invalid type (must be a string or an array of strings)", path.string()); + return; + } + + if (dataJson.HasMember("EventIdRegex")) + { + // array of event id regex + if (dataJson["EventIdRegex"].IsArray()) + { + for (auto& eventId : dataJson["EventIdRegex"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error("Failed reading audio override file {}: EventIdRegex array has a value of invalid type, all must be strings", path.string()); + return; + } + + const std::string& regex = eventId.GetString(); + + try { + EventIdsRegex.push_back({ regex, std::regex(regex) }); + } catch (...) { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + } + // singular event id regex + else if (dataJson["EventIdRegex"].IsString()) + { + const std::string& regex = dataJson["EventIdRegex"].GetString(); + try { + EventIdsRegex.push_back({ regex, std::regex(regex) }); + } + catch (...) { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + // incorrect type + else + { + spdlog::error("Failed reading audio override file {}: EventIdRegex property is of invalid type (must be a string or an array of strings)", path.string()); + return; + } + } + + if (dataJson.HasMember("AudioSelectionStrategy")) + { + if (!dataJson["AudioSelectionStrategy"].IsString()) + { + spdlog::error("Failed reading audio override file {}: AudioSelectionStrategy property must be a string", path.string()); + return; + } + + std::string strategy = dataJson["AudioSelectionStrategy"].GetString(); + + if (strategy == "sequential") + { + Strategy = AudioSelectionStrategy::SEQUENTIAL; + } + else if (strategy == "random") + { + Strategy = AudioSelectionStrategy::RANDOM; + } + else + { + spdlog::error("Failed reading audio override file {}: AudioSelectionStrategy string must be either \"sequential\" or \"random\"", path.string()); + return; + } + } + + // load samples + for (fs::directory_entry file : fs::recursive_directory_iterator(samplesFolder)) + { + if (file.is_regular_file() && file.path().extension().string() == ".wav") + { + // Open the file. + std::basic_ifstream<uint8_t> wavStream(file, std::ios::binary); + + if (wavStream.fail()) + { + spdlog::error("Failed reading audio sample {}", file.path().string()); + continue; + } + + // Read file into a vector and add it to the samples list. + Samples.push_back(std::vector<uint8_t>((std::istreambuf_iterator<uint8_t>(wavStream)), std::istreambuf_iterator<uint8_t>())); + + // Close the file. + wavStream.close(); + } + } + + /* + if (dataJson.HasMember("EnableOnLoopedSounds")) + { + if (!dataJson["EnableOnLoopedSounds"].IsBool()) + { + spdlog::error("Failed reading audio override file {}: EnableOnLoopedSounds property is of invalid type (must be a bool)", path.string()); + return; + } + + EnableOnLoopedSounds = dataJson["EnableOnLoopedSounds"].GetBool(); + } + */ + + if (Samples.size() == 0) + spdlog::warn("Audio override {} has no valid samples! Sounds will not play for this event.", path.string()); + + spdlog::info("Loaded audio override file {}", path.string()); + + LoadedSuccessfully = true; +} + +bool CustomAudioManager::TryLoadAudioOverride(const fs::path& defPath) +{ + std::ifstream jsonStream(defPath); + std::stringstream jsonStringStream; + + // fail if no audio json + if (jsonStream.fail()) + { + spdlog::warn("Unable to read audio override from file {}", defPath.string()); + return false; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + std::shared_ptr<EventOverrideData> data = std::make_shared<EventOverrideData>(jsonStringStream.str(), defPath); + + if (!data->LoadedSuccessfully) + return false; // no logging, the constructor has probably already logged + + for (const std::string& eventId : data->EventIds) + { + spdlog::info("Registering sound event {}", eventId); + m_loadedAudioOverrides.insert({ eventId, data }); + } + + for (const auto& eventIdRegexData : data->EventIdsRegex) + { + spdlog::info("Registering sound event regex {}", eventIdRegexData.first); + m_loadedAudioOverridesRegex.insert({ eventIdRegexData.first, data }); + } + + return true; +} + +void CustomAudioManager::ClearAudioOverrides() +{ + m_loadedAudioOverrides.clear(); + m_loadedAudioOverridesRegex.clear(); +} + +typedef bool (*LoadSampleMetadata_Type)(void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType); +LoadSampleMetadata_Type LoadSampleMetadata_Original; + +// Empty stereo 48000 WAVE file +unsigned char EMPTY_WAVE[45] = { + 0x52, 0x49, 0x46, 0x46, 0x25, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, + 0x66, 0x6D, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, + 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00, + 0x64, 0x61, 0x74, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00 +}; + +template<typename Iter, typename RandomGenerator> +Iter select_randomly(Iter start, Iter end, RandomGenerator& g) { + std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1); + std::advance(start, dis(g)); + return start; +} + +template<typename Iter> +Iter select_randomly(Iter start, Iter end) { + static std::random_device rd; + static std::mt19937 gen(rd()); + return select_randomly(start, end, gen); +} + +bool ShouldPlayAudioEvent(const char* eventName, const std::shared_ptr<EventOverrideData>& data) +{ + std::string eventNameString = eventName; + std::string eventNameStringBlacklistEntry = ("!" + eventNameString); + + for (const std::string& name : data->EventIds) + { + if (name == eventNameStringBlacklistEntry) + return false; // event blacklisted + + if (name == "*") + { + // check for bad sounds I guess? + // really feel like this should be an option but whatever + if (!!strstr(eventName, "_amb_") || !!strstr(eventName, "_emit_")) + return false; // would play static noise, I hate this + } + } + + return true; // good to go +} + +// DO NOT IMLINE THIS FUNCTION +// See comment below. +bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal(uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType) +{ + char* eventName = (char*)parentEvent + 0x110; + + if (Cvar_ns_print_played_sounds->m_nValue > 0) + spdlog::info("[AUDIO] Playing event {}", eventName); + + auto iter = g_CustomAudioManager.m_loadedAudioOverrides.find(eventName); + std::shared_ptr<EventOverrideData> overrideData; + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // override for that specific event not found, try wildcard + iter = g_CustomAudioManager.m_loadedAudioOverrides.find("*"); + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // not found + + // try regex + for (const auto& item : g_CustomAudioManager.m_loadedAudioOverridesRegex) + for (const auto& regexData : item.second->EventIdsRegex) + if (std::regex_search(eventName, regexData.second)) + overrideData = item.second; + + if (!overrideData) + // not found either + return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType); + else + { + // cache found pattern to improve performance + g_CustomAudioManager.m_loadedAudioOverrides[eventName] = overrideData; + } + } + else overrideData = iter->second; + } + else overrideData = iter->second; + + if (!ShouldPlayAudioEvent(eventName, overrideData)) + return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType); + + void* data = 0; + unsigned int dataLength = 0; + + if (overrideData->Samples.size() == 0) + { + // 0 samples, turn off this particular event. + + // using a dummy empty wave file + data = EMPTY_WAVE; + dataLength = sizeof(EMPTY_WAVE); + } + else + { + std::vector<uint8_t>* vec = NULL; + + switch (overrideData->Strategy) + { + case AudioSelectionStrategy::RANDOM: + vec = &*select_randomly(overrideData->Samples.begin(), overrideData->Samples.end()); + break; + case AudioSelectionStrategy::SEQUENTIAL: + default: + vec = &overrideData->Samples[overrideData->CurrentIndex++]; + if (overrideData->CurrentIndex >= overrideData->Samples.size()) + overrideData->CurrentIndex = 0; // reset back to the first sample entry + break; + } + + if (!vec) + spdlog::warn("Could not get sample data from override struct for event {}! Shouldn't happen", eventName); + else + { + data = vec->data(); + dataLength = vec->size(); + } + } + + if (!data) + { + spdlog::warn("Could not fetch override sample data for event {}! Using original data instead.", eventName); + return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType); + } + + audioBuffer = data; + audioBufferLength = dataLength; + + // most important change: set the sample class buffer so that the correct audio plays + *(void**)((uintptr_t)sample + 0xE8) = audioBuffer; + *(unsigned int*)((uintptr_t)sample + 0xF0) = audioBufferLength; + + // 64 - Auto-detect sample type + bool res = LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, 64); + if (!res) + spdlog::error("LoadSampleMetadata failed! The game will crash :("); + + return res; +} + +// DO NOT TOUCH THIS FUNCTION +// The actual logic of it in a separate function (forcefully not inlined) to preserve the r12 register, which holds the event pointer. +bool __fastcall LoadSampleMetadata_Hook(void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType) +{ + uintptr_t parentEvent = (uintptr_t)Audio_GetParentEvent(); + + // Raw source, used for voice data only + if (audioType == 0) + return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType); + + return LoadSampleMetadata_Internal(parentEvent, sample, audioBuffer, audioBufferLength, audioType); +} + +typedef bool (*MilesLog_Type)(int level, const char* string); +MilesLog_Type MilesLog_Original; + +void __fastcall MilesLog_Hook(int level, const char* string) +{ + spdlog::info("[MSS] {} - {}", level, string); +} + +void InitialiseMilesAudioHooks(HMODULE baseAddress) +{ + Cvar_ns_print_played_sounds = RegisterConVar("ns_print_played_sounds", "0", FCVAR_NONE, ""); + + if (IsDedicated()) + return; + + uintptr_t milesAudioBase = (uintptr_t)GetModuleHandleA("mileswin64.dll"); + + if (!milesAudioBase) + return spdlog::error("miles audio not found :terror:"); + + HookEnabler hook; + + ENABLER_CREATEHOOK(hook, (char*)milesAudioBase + 0xF110, &LoadSampleMetadata_Hook, reinterpret_cast<LPVOID*>(&LoadSampleMetadata_Original)); + ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x57DAD0, &MilesLog_Hook, reinterpret_cast<LPVOID*>(&MilesLog_Original)); +}
\ No newline at end of file diff --git a/NorthstarDedicatedTest/audio.h b/NorthstarDedicatedTest/audio.h new file mode 100644 index 00000000..67fe342f --- /dev/null +++ b/NorthstarDedicatedTest/audio.h @@ -0,0 +1,47 @@ +#pragma once + +#include <vector> +#include <filesystem> +#include <regex> + +namespace fs = std::filesystem; + +enum class AudioSelectionStrategy +{ + INVALID = -1, + SEQUENTIAL, + RANDOM +}; + +class EventOverrideData +{ +public: + EventOverrideData(const std::string&, const fs::path&); + EventOverrideData(); +public: + bool LoadedSuccessfully = false; + + std::vector<std::string> EventIds = {}; + std::vector<std::pair<std::string, std::regex>> EventIdsRegex = {}; + + std::vector<std::vector<std::uint8_t>> Samples = {}; + + AudioSelectionStrategy Strategy = AudioSelectionStrategy::SEQUENTIAL; + size_t CurrentIndex = 0; + + bool EnableOnLoopedSounds = false; +}; + +class CustomAudioManager +{ +public: + bool TryLoadAudioOverride(const fs::path&); + void ClearAudioOverrides(); + + std::unordered_map<std::string, std::shared_ptr<EventOverrideData>> m_loadedAudioOverrides = {}; + std::unordered_map<std::string, std::shared_ptr<EventOverrideData>> m_loadedAudioOverridesRegex = {}; +}; + +extern CustomAudioManager g_CustomAudioManager; + +void InitialiseMilesAudioHooks(HMODULE baseAddress);
\ No newline at end of file diff --git a/NorthstarDedicatedTest/audio_asm.asm b/NorthstarDedicatedTest/audio_asm.asm new file mode 100644 index 00000000..4c527f9d --- /dev/null +++ b/NorthstarDedicatedTest/audio_asm.asm @@ -0,0 +1,8 @@ +public Audio_GetParentEvent + +.code +Audio_GetParentEvent proc + mov rax, r12 + ret +Audio_GetParentEvent endp +end
\ No newline at end of file diff --git a/NorthstarDedicatedTest/dllmain.cpp b/NorthstarDedicatedTest/dllmain.cpp index 83e78f4e..fd82e382 100644 --- a/NorthstarDedicatedTest/dllmain.cpp +++ b/NorthstarDedicatedTest/dllmain.cpp @@ -30,6 +30,7 @@ #include "memalloc.h" #include "maxplayers.h" #include "languagehooks.h" +#include "audio.h" bool initialised = false; @@ -134,6 +135,9 @@ bool InitialiseNorthstar() AddDllLoadCallback("client.dll", InitialiseMaxPlayersOverride_Client); AddDllLoadCallback("server.dll", InitialiseMaxPlayersOverride_Server); + // audio hooks + AddDllLoadCallback("client.dll", InitialiseMilesAudioHooks); + // mod manager after everything else AddDllLoadCallback("engine.dll", InitialiseModManager); diff --git a/NorthstarDedicatedTest/modmanager.cpp b/NorthstarDedicatedTest/modmanager.cpp index 23dd2d6e..34bba6af 100644 --- a/NorthstarDedicatedTest/modmanager.cpp +++ b/NorthstarDedicatedTest/modmanager.cpp @@ -2,6 +2,7 @@ #include "modmanager.h" #include "convar.h" #include "concommand.h" +#include "audio.h" #include "rapidjson/error/en.h" #include "rapidjson/document.h" @@ -330,6 +331,22 @@ void ModManager::LoadMods() mod.Pdiff = pdiffStringStream.str(); } } + + // try to load audio + if (fs::exists(mod.ModDirectory / "audio")) + { + for (fs::directory_entry file : fs::directory_iterator(mod.ModDirectory / "audio")) + { + if (fs::is_regular_file(file) && file.path().extension().string() == ".json") + { + if (!g_CustomAudioManager.TryLoadAudioOverride(file.path())) + { + spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string()); + continue; + } + } + } + } } // in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised @@ -364,6 +381,8 @@ void ModManager::UnloadMods() m_modFiles.clear(); fs::remove_all(COMPILED_ASSETS_PATH); + g_CustomAudioManager.ClearAudioOverrides(); + if (!m_hasEnabledModsCfg) m_enabledModsCfg.SetObject(); |