From 0ae370bbf441c6f202f0f269d958a29947848be4 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:46:26 +0300 Subject: [PATCH 01/37] Add sound distance to world sound event --- Client/game_sa/CAudioEngineSA.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Client/game_sa/CAudioEngineSA.cpp b/Client/game_sa/CAudioEngineSA.cpp index c25464eb711..7cf171c7291 100644 --- a/Client/game_sa/CAudioEngineSA.cpp +++ b/Client/game_sa/CAudioEngineSA.cpp @@ -555,10 +555,7 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) pGameEntity = pAESound->pAudioEntity->pEntity; SWorldSoundEvent event = { - pAESound->usGroup, - pAESound->usIndex, - pGameEntity, - pAESound->m_vCurrPosn, + pAESound->usGroup, pAESound->usIndex, pGameEntity, pAESound->m_vCurrPosn, pAESound->m_fSoundDistance, }; return m_pWorldSoundHandler(event); From 03fb5878c09a35fe5242933d6703d0c017750d17 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:55:31 +0300 Subject: [PATCH 02/37] Integrate CClientWorldSoundManager into CClientGame Added CClientWorldSoundManager for managing world sounds and integrated it into the game logic. --- Client/mods/deathmatch/logic/CClientGame.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6ed7e017179..2f17322c3f8 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -40,6 +40,7 @@ #include #include "CServerInfo.h" #include "CClientPed.h" +#include "CClientWorldSoundManager.h" SString StringZeroPadout(const SString& strInput, uint uiPadoutSize) { @@ -199,6 +200,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) // Create the manager and grab the most important pointers m_pManager = new CClientManager; + m_pWorldSoundManager = new CClientWorldSoundManager(m_pManager); m_pCamera = m_pManager->GetCamera(); m_pMarkerManager = m_pManager->GetMarkerManager(); m_pObjectManager = m_pManager->GetObjectManager(); @@ -563,6 +565,7 @@ CClientGame::~CClientGame() // Destroy our stuff SAFE_DELETE(m_pManager); // Will trigger onClientResourceStop + SAFE_DELETE(m_pWorldSoundManager); SAFE_DELETE(m_pNametags); SAFE_DELETE(m_pSyncDebug); SAFE_DELETE(m_pNetworkStats); @@ -1558,6 +1561,11 @@ void CClientGame::DoPulses2(bool bCalledFromIdle) m_pManager->DoPulse(bDoStandardPulses, bDoVehicleManagerPulse); + if (m_pWorldSoundManager) + { + m_pWorldSoundManager->DoPulse(); + } + if (bDoStandardPulses) { m_pNetAPI->DoPulse(); @@ -6618,6 +6626,7 @@ bool CClientGame::WorldSoundHandler(const SWorldSoundEvent& event) if (!pEntity) pEntity = GetRootEntity(); + bool bAllowPlay = true; if (pEntity) { CLuaArguments Arguments; @@ -6626,10 +6635,15 @@ bool CClientGame::WorldSoundHandler(const SWorldSoundEvent& event) Arguments.PushNumber(event.vecPosition.fX); Arguments.PushNumber(event.vecPosition.fY); Arguments.PushNumber(event.vecPosition.fZ); - return pEntity->CallEvent("onClientWorldSound", Arguments, true); + bAllowPlay = pEntity->CallEvent("onClientWorldSound", Arguments, true); } - return true; + if (m_pWorldSoundManager && m_pWorldSoundManager->HandleWorldSound(event, bAllowPlay)) + { + return false; + } + + return bAllowPlay; } ////////////////////////////////////////////////////////////////// From 419c6df274521aa006044a081ba91b3ff9a39209 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:59:49 +0300 Subject: [PATCH 03/37] Add CClientWorldSoundManager to CClientGame --- Client/mods/deathmatch/logic/CClientGame.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index f010e6d0c57..370ad898821 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -50,6 +50,7 @@ #define INVALID_DOWNLOAD_PRIORITY_GROUP (INT_MIN) class CClientModelCacheManager; +class CClientWorldSoundManager; class CDebugHookManager; class CResourceFileDownloadManager; class CServerInfo; @@ -277,6 +278,7 @@ class CClientGame // Accessors CVoiceRecorder* GetVoiceRecorder() { return m_pVoiceRecorder; }; + CClientWorldSoundManager* GetWorldSoundManager() { return m_pWorldSoundManager; }; CClientManager* GetManager() { return m_pManager; }; CClientObjectManager* GetObjectManager() { return m_pObjectManager; }; CClientPickupManager* GetPickupManager() { return m_pPickupManager; }; @@ -732,6 +734,7 @@ class CClientGame bool m_bFirstPlaybackFrame; CClientManager* m_pManager; + CClientWorldSoundManager* m_pWorldSoundManager; CClientCamera* m_pCamera; CClientGUIManager* m_pGUIManager; CClientMarkerManager* m_pMarkerManager; From 59c7ab9f9ff19cd2b0365d1a3a77102df7f86c16 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:01:40 +0300 Subject: [PATCH 04/37] Implement ValidateSound function for audio validation Added ValidateSound function to check audio validity and handle errors. --- .../deathmatch/logic/CClientSoundManager.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.cpp b/Client/mods/deathmatch/logic/CClientSoundManager.cpp index 3f8c42d8a0c..beef138fefd 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientSoundManager.cpp @@ -319,6 +319,43 @@ void CClientSoundManager::UpdateVolume() BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, static_cast(fValue * 10000)); } +bool CClientSoundManager::ValidateSound(const SString& strSound, bool bIsRawData, SString* pOutError) +{ + HSTREAM hStream; + if (bIsRawData) + hStream = BASS_StreamCreateFile(true, strSound.data(), 0, static_cast(strSound.size()), BASS_STREAM_DECODE); + else + hStream = BASS_StreamCreateFile(false, FromUTF8(strSound), 0, 0, BASS_STREAM_DECODE | BASS_UNICODE); + + if (!hStream) + { + if (pOutError) + { + const int iError = BASS_ErrorGetCode(); + const char* szReason = "audio could not be decoded"; + switch (iError) + { + case BASS_ERROR_FILEOPEN: + szReason = "file cannot be opened (missing or access denied)"; + break; + case BASS_ERROR_FILEFORM: + szReason = "unsupported or corrupt audio format"; + break; + case BASS_ERROR_MEM: + szReason = "out of memory"; + break; + default: + break; + } + *pOutError = SString("%s (BASS error %d)", szReason, iError); + } + return false; + } + + BASS_StreamFree(hStream); + return true; +} + // // Lists // From cda584a910ea520ca9498b1b76cab5efb7896ffd Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:02:51 +0300 Subject: [PATCH 05/37] Add ValidateSound method to CClientSoundManager --- Client/mods/deathmatch/logic/CClientSoundManager.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.h b/Client/mods/deathmatch/logic/CClientSoundManager.h index ff75cb7658c..80cd28bf3ee 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientSoundManager.h @@ -36,6 +36,8 @@ class CClientSoundManager bool GetSFXStatus(eAudioLookupIndex containerIndex); + bool ValidateSound(const SString& strSound, bool bIsRawData, SString* pOutError = nullptr); + void AddToList(CClientSound* pSound); void RemoveFromList(CClientSound* pSound); From 74dfcb5f84e94382845f85a4f5340915834dec70 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:07:55 +0300 Subject: [PATCH 06/37] Add CClientWorldSoundManager for sound replacement Implement client-side world sound management with sound replacement functionality. --- .../logic/CClientWorldSoundManager.cpp | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp new file mode 100644 index 00000000000..28304227e22 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -0,0 +1,216 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/CClientWorldSoundManager.cpp + * PURPOSE: Client-side world sound replacement (engineReplaceWorldSound) + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CClientWorldSoundManager.h" +#include "CClientEntity.h" +#include "CClientManager.h" +#include "CClientSoundManager.h" +#include +#include + +CClientWorldSoundManager::CClientWorldSoundManager(CClientManager* pManager) : m_pManager(pManager), m_uiLastPruneTick(0) +{ +} + +CClientWorldSoundManager::~CClientWorldSoundManager() +{ + RestoreAll(); +} + +bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SString& strSound, bool bIsRawData, float fMinDistance, float fMaxDistance, + SString* pOutError) +{ + if (uiIndex != static_cast(-1) && uiIndex > 399) + { + if (pOutError) + *pOutError = SString("invalid sound index %u (maximum is 399)", uiIndex); + return false; + } + + if (!std::isfinite(fMinDistance) || !std::isfinite(fMaxDistance)) + { + if (pOutError) + *pOutError = "minDistance/maxDistance must be finite numbers (no NaN or infinity)"; + return false; + } + if (fMinDistance > 0.0f && fMaxDistance > 0.0f && fMinDistance > fMaxDistance) + { + if (pOutError) + *pOutError = SString("minDistance (%.2f) cannot be greater than maxDistance (%.2f)", fMinDistance, fMaxDistance); + return false; + } + + if (!m_pManager->GetSoundManager()->ValidateSound(strSound, bIsRawData, pOutError)) + return false; + + m_Replacements[MakeKey(uiGroup, uiIndex)] = {bIsRawData, strSound, std::max(0.0f, fMinDistance), std::max(0.0f, fMaxDistance)}; + return true; +} + +bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) +{ + if (uiIndex == static_cast(-1)) + { + bool bErased = false; + for (auto iter = m_Replacements.begin(); iter != m_Replacements.end();) + { + if ((iter->first >> 16) == uiGroup) + { + iter = m_Replacements.erase(iter); + bErased = true; + } + else + ++iter; + } + return bErased; + } + + return m_Replacements.erase(MakeKey(uiGroup, uiIndex)) > 0; +} + +void CClientWorldSoundManager::RestoreAll() +{ + m_Replacements.clear(); +} + +bool CClientWorldSoundManager::IsSoundReplaced(uint uiGroup, uint uiIndex) const +{ + if (uiIndex == static_cast(-1)) + { + for (const auto& iter : m_Replacements) + { + if ((iter.first >> 16) == uiGroup) + return true; + } + return false; + } + + const SReplacement* pReplacement = nullptr; + return FindReplacement(uiGroup, uiIndex, &pReplacement); +} + +bool CClientWorldSoundManager::FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const +{ + auto iter = m_Replacements.find(MakeKey(uiGroup, uiIndex)); + if (iter != m_Replacements.end()) + { + *ppOutReplacement = &iter->second; + return true; + } + + iter = m_Replacements.find(MakeKey(uiGroup, static_cast(-1))); + if (iter != m_Replacements.end()) + { + *ppOutReplacement = &iter->second; + return true; + } + + return false; +} + +bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event, bool bAllowPlay) +{ + const SReplacement* pReplacement = nullptr; + if (!FindReplacement(event.uiGroup, event.uiIndex, &pReplacement)) + return false; + + if (bAllowPlay) + { + const uint uiKey = MakeKey(event.uiGroup, event.uiIndex); + + bool bAlreadyPlayed = false; + auto iter = m_LastPlayed.find(uiKey); + if (iter != m_LastPlayed.end()) + { + const uint uiElapsed = GetTickCount32() - iter->second.uiTick; + const CVector vecDelta = event.vecPosition - iter->second.vecPosition; + if (uiElapsed < 40 && vecDelta.LengthSquared() < 2.25f) + bAlreadyPlayed = true; + } + + if (!bAlreadyPlayed) + { + CClientSound* pSound = + m_pManager->GetSoundManager()->PlaySound3D(pReplacement->strSound, false, pReplacement->bRawData, event.vecPosition, false, true); + if (!pSound) + { + return false; + } + + float fMaxDistance = pReplacement->fMaxDistance > 0.0f ? pReplacement->fMaxDistance : event.fMaxDistance; + if (fMaxDistance > 1.0f) + { + float fMinDistance = pReplacement->fMinDistance > 0.0f ? pReplacement->fMinDistance : std::min(5.0f, fMaxDistance * 0.25f); + fMinDistance = std::max(0.1f, std::min(fMinDistance, fMaxDistance * 0.95f)); + pSound->SetMinDistance(fMinDistance); + pSound->SetMaxDistance(fMaxDistance); + } + + float fVolume = g_pCore->GetCVars()->GetValue("sfxvolume", 1.0f); + float fMtaVolume = 1.0f; + if (g_pCore->GetCVars()->Get("mtavolume", fMtaVolume) && fMtaVolume > 0.0f) + fVolume = std::min(4.0f, fVolume / fMtaVolume); + + if (g_pCore->IsWindowMinimized() && + (g_pCore->GetCVars()->GetValue("mute_master_when_minimized") || g_pCore->GetCVars()->GetValue("mute_sfx_when_minimized"))) + { + fVolume = 0.0f; + } + + pSound->SetVolume(fVolume); + + if (event.pGameEntity) + m_FollowSounds.push_back({pSound, event.pGameEntity, GetTickCount32()}); + + m_LastPlayed[uiKey] = {GetTickCount32(), event.vecPosition}; + } + } + + return true; +} + +void CClientWorldSoundManager::DoPulse() +{ + const uint uiNow = GetTickCount32(); + + for (auto iter = m_FollowSounds.begin(); iter != m_FollowSounds.end();) + { + SFollowSound& entry = *iter; + + if (entry.pSound->IsFinished() || uiNow - entry.uiStartTick > 10000) + { + iter = m_FollowSounds.erase(iter); + continue; + } + + if (CClientEntity* pEntity = g_pGame->GetPools()->GetClientEntity((DWORD*)entry.pGameEntity)) + { + CVector vecPosition; + pEntity->GetPosition(vecPosition); + entry.pSound->SetPosition(vecPosition); + } + + ++iter; + } + + if (uiNow - m_uiLastPruneTick > 500) + { + m_uiLastPruneTick = uiNow; + for (auto iter = m_LastPlayed.begin(); iter != m_LastPlayed.end();) + { + if (uiNow - iter->second.uiTick > 1000) + iter = m_LastPlayed.erase(iter); + else + ++iter; + } + } +} From bda3f3f724f03d1b09f64d886e768a9beee6d9ca Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:10:19 +0300 Subject: [PATCH 07/37] Add CClientWorldSoundManager header file --- .../logic/CClientWorldSoundManager.h | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Client/mods/deathmatch/logic/CClientWorldSoundManager.h diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h new file mode 100644 index 00000000000..0286983cdec --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -0,0 +1,69 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/CClientWorldSoundManager.h + * PURPOSE: Client-side world sound replacement (engineReplaceWorldSound) + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +class CClientManager; +class CClientSound; + +class CClientWorldSoundManager +{ +public: + CClientWorldSoundManager(CClientManager* pManager); + ~CClientWorldSoundManager(); + + bool ReplaceSound(uint uiGroup, uint uiIndex, const SString& strSound, bool bIsRawData, float fMinDistance = 0.0f, float fMaxDistance = 0.0f, + SString* pOutError = nullptr); + bool RestoreSound(uint uiGroup, uint uiIndex); + void RestoreAll(); + + bool IsSoundReplaced(uint uiGroup, uint uiIndex) const; + + bool HandleWorldSound(const SWorldSoundEvent& event, bool bAllowPlay); + + void DoPulse(); + +private: + struct SReplacement + { + bool bRawData; + SString strSound; + float fMinDistance; + float fMaxDistance; + }; + + struct SLastPlayed + { + uint uiTick; + CVector vecPosition; + }; + + struct SFollowSound + { + CClientSound* pSound; + CEntitySAInterface* pGameEntity; + uint uiStartTick; + }; + + static uint MakeKey(uint uiGroup, uint uiIndex) { return (uiGroup << 16) | (uiIndex & 0xFFFF); } + + bool FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const; + + CClientManager* m_pManager; + std::unordered_map m_Replacements; + std::unordered_map m_LastPlayed; + std::list m_FollowSounds; + uint m_uiLastPruneTick; +}; From a804736c981a7ac62287a92914a0895a32d4e9fc Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:15:03 +0300 Subject: [PATCH 08/37] Add world sound management functions to CLuaEngineDefs --- .../logic/luadefs/CLuaEngineDefs.cpp | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index d8d393e085e..bd096b11976 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -16,6 +16,8 @@ #include #include #include "CLuaEngineDefs.h" +#include "CClientWorldSoundManager.h" +#include "CResourceManager.h" #include //! Set the CModelCacheManager limits @@ -96,6 +98,10 @@ void CLuaEngineDefs::LoadFunctions() {"engineReplaceCOL", EngineReplaceCOL}, {"engineRestoreCOL", EngineRestoreCOL}, {"engineReplaceModel", EngineReplaceModel}, + {"engineReplaceWorldSound", EngineReplaceWorldSound}, + {"engineRestoreWorldSound", EngineRestoreWorldSound}, + {"engineRestoreAllWorldSounds", EngineRestoreAllWorldSounds}, + {"isWorldSoundReplaced", EngineIsWorldSoundReplaced}, {"engineAddClothingModel", ArgumentParser}, {"engineRestoreModel", EngineRestoreModel}, {"engineReplaceAnimation", EngineReplaceAnimation}, @@ -177,12 +183,137 @@ void CLuaEngineDefs::LoadFunctions() CLuaCFunctions::AddFunction(name, func); } +int CLuaEngineDefs::EngineReplaceWorldSound(lua_State* luaVM) +{ + SString strSound; + int group; + int index = -1; + float fMinDistance = -1.0f; + float fMaxDistance = -1.0f; + CScriptArgReader argStream(luaVM); + argStream.ReadString(strSound); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + if (argStream.NextIsNumber()) + argStream.ReadNumber(fMinDistance); + if (argStream.NextIsNumber()) + argStream.ReadNumber(fMaxDistance); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + CLuaMain* pLuaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (!pLuaMain || !g_pClientGame) + { + lua_pushboolean(luaVM, false); + return 1; + } + + CResource* pResource = pLuaMain->GetResource(); + if (!pResource) + { + lua_pushboolean(luaVM, false); + return 1; + } + + SString strOriginal = strSound; + SString strFilename; + bool bIsRawData = false; + if (CResourceManager::ParseResourcePathInput(strSound, pResource, &strFilename, nullptr, true)) + strSound = strFilename; + else + bIsRawData = true; + + if (!g_pClientGame->GetWorldSoundManager()) + { + lua_pushboolean(luaVM, false); + return 1; + } + + SString strError; + bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, index, strSound, bIsRawData, fMinDistance, fMaxDistance, &strError); + if (!bSuccess && !strError.empty()) + m_pScriptDebugging->LogWarning(luaVM, "engineReplaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, index, strOriginal.c_str()); + + lua_pushboolean(luaVM, bSuccess); + return 1; +} + +int CLuaEngineDefs::EngineRestoreWorldSound(lua_State* luaVM) +{ + int group; + int index = -1; + CScriptArgReader argStream(luaVM); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + if (!g_pClientGame || !g_pClientGame->GetWorldSoundManager()) + { + lua_pushboolean(luaVM, false); + return 1; + } + + bool bSuccess = g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index); + lua_pushboolean(luaVM, bSuccess); + return 1; +} + +int CLuaEngineDefs::EngineRestoreAllWorldSounds(lua_State* luaVM) +{ + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + g_pClientGame->GetWorldSoundManager()->RestoreAll(); + + lua_pushboolean(luaVM, true); + return 1; +} + +int CLuaEngineDefs::EngineIsWorldSoundReplaced(lua_State* luaVM) +{ + int group; + int index = -1; + CScriptArgReader argStream(luaVM); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + bool bReplaced = false; + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + bReplaced = g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index); + + lua_pushboolean(luaVM, bReplaced); + return 1; +} + void CLuaEngineDefs::AddClass(lua_State* luaVM) { lua_newclass(luaVM); lua_classfunction(luaVM, "restoreCOL", "engineRestoreCOL"); lua_classfunction(luaVM, "restoreModel", "engineRestoreModel"); + lua_classfunction(luaVM, "replaceWorldSound", "engineReplaceWorldSound"); + lua_classfunction(luaVM, "restoreWorldSound", "engineRestoreWorldSound"); + lua_classfunction(luaVM, "restoreAllWorldSounds", "engineRestoreAllWorldSounds"); + lua_classfunction(luaVM, "isWorldSoundReplaced", "isWorldSoundReplaced"); lua_classfunction(luaVM, "setAsynchronousLoading", "engineSetAsynchronousLoading"); lua_classfunction(luaVM, "setModelLODDistance", "engineSetModelLODDistance"); lua_classfunction(luaVM, "resetModelLODDistance", "engineResetModelLODDistance"); From 0c7cdeb4131607e65c9ded22dc8425d3b3853cc6 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:16:18 +0300 Subject: [PATCH 09/37] Add new Lua engine sound replacement functions --- Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h index 6a107c0ded0..356a84ba343 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h @@ -28,6 +28,10 @@ class CLuaEngineDefs : public CLuaDefs LUA_DECLARE(EngineRestoreCOL); LUA_DECLARE(EngineReplaceModel); LUA_DECLARE(EngineRestoreModel); + LUA_DECLARE(EngineReplaceWorldSound); + LUA_DECLARE(EngineRestoreWorldSound); + LUA_DECLARE(EngineRestoreAllWorldSounds); + LUA_DECLARE(EngineIsWorldSoundReplaced); LUA_DECLARE(EngineRequestModel); LUA_DECLARE(EngineFreeModel); LUA_DECLARE(EngineReplaceAnimation); From cc4ff710c86452d1d05c5d6c22b3a801ff6f24f5 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 21:17:23 +0300 Subject: [PATCH 10/37] Add fMaxDistance member to CAudioEngine struct --- Client/sdk/game/CAudioEngine.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/sdk/game/CAudioEngine.h b/Client/sdk/game/CAudioEngine.h index e91bea204b1..72f149a7e59 100644 --- a/Client/sdk/game/CAudioEngine.h +++ b/Client/sdk/game/CAudioEngine.h @@ -26,6 +26,7 @@ struct SWorldSoundEvent unsigned int uiIndex; CEntitySAInterface* pGameEntity; CVector vecPosition; + float fMaxDistance; }; using WorldSoundHandler = bool(const SWorldSoundEvent& event); From 7375abd6697696d59957131f746fe9b42d5857ee Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:15 +0300 Subject: [PATCH 11/37] Add audio hardware bank loader and slot functions --- Client/game_sa/CAEAudioHardwareSA.cpp | 160 ++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/Client/game_sa/CAEAudioHardwareSA.cpp b/Client/game_sa/CAEAudioHardwareSA.cpp index 8eebfb1591c..9b88648cc9c 100644 --- a/Client/game_sa/CAEAudioHardwareSA.cpp +++ b/Client/game_sa/CAEAudioHardwareSA.cpp @@ -53,3 +53,163 @@ void CAEAudioHardwareSA::LoadSoundBank(short wSoundBankID, short wSoundBankSlotI } // clang-format on } + +namespace +{ + constexpr DWORD NUM_AudioHardwareBankLoaderOffset = 0xD98; + + constexpr DWORD NUM_BankLoaderSlotsOffset = 0x00; + constexpr DWORD NUM_BankLoaderSlotCountOffset = 0x0C; + constexpr DWORD NUM_BankLoaderBufferSizeOffset = 0x18; + constexpr DWORD NUM_BankLoaderBufferOffset = 0x1C; + + constexpr DWORD NUM_BankSlotSize = 0x12D4; + constexpr DWORD NUM_BankSlotOffsetBytesOffset = 0x00; + constexpr DWORD NUM_BankSlotNumBytesOffset = 0x04; + constexpr DWORD NUM_BankSlotNumSoundsOffset = 0x12; + constexpr DWORD NUM_BankSlotSoundsArrayOffset = 0x14; + + constexpr DWORD NUM_BankSlotItemSize = 0x0C; + constexpr DWORD NUM_BankSlotItemBufferOffsetOffset = 0x00; + constexpr DWORD NUM_BankSlotItemLoopOffsetOffset = 0x04; + constexpr DWORD NUM_BankSlotItemSampleFreqOffset = 0x08; + + constexpr DWORD NUM_BankStreamHeaderSize = 0x12C4; + constexpr uint NUM_MaxBankSounds = 400; + + const BYTE* GetBankLoader() + { + const BYTE* pAudioHardware = reinterpret_cast(CLASS_CAEAudioHardware); + return *reinterpret_cast(pAudioHardware + NUM_AudioHardwareBankLoaderOffset); + } + + const BYTE* GetBankSlot(uint usBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return nullptr; + + const ushort usSlotCount = *reinterpret_cast(pBankLoader + NUM_BankLoaderSlotCountOffset); + if (usBankSlot >= usSlotCount) + return nullptr; + + const BYTE* pBankSlots = *reinterpret_cast(pBankLoader + NUM_BankLoaderSlotsOffset); + if (!pBankSlots) + return nullptr; + + return pBankSlots + usBankSlot * NUM_BankSlotSize; + } + + const BYTE* GetBankSlotData(const BYTE* pBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return nullptr; + + const BYTE* pBuffer = *reinterpret_cast(pBankLoader + NUM_BankLoaderBufferOffset); + if (!pBuffer) + return nullptr; + + const uint uiBufferSize = *reinterpret_cast(pBankLoader + NUM_BankLoaderBufferSizeOffset); + const uint uiOffsetBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotOffsetBytesOffset); + const uint uiNumBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotNumBytesOffset); + + if (uiOffsetBytes + uiNumBytes > uiBufferSize || uiNumBytes <= NUM_BankStreamHeaderSize) + return nullptr; + + return pBuffer + uiOffsetBytes; + } + + const BYTE* GetBankSlotItem(const BYTE* pBankSlot, uint usIndex) + { + return pBankSlot + NUM_BankSlotSoundsArrayOffset + usIndex * NUM_BankSlotItemSize; + } +} + +bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, + unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const +{ + pOutPcmData = nullptr; + uiOutPcmSize = 0; + uiOutSampleRate = 0; + iOutLoopStartOffset = -1; + + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return false; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds < 0 || usIndex >= static_cast(sNumSounds) || usIndex >= NUM_MaxBankSounds) + return false; + + const BYTE* pSlotData = GetBankSlotData(pBankSlot); + if (!pSlotData) + return false; + + const uint uiNumBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotNumBytesOffset); + + const BYTE* pItem = GetBankSlotItem(pBankSlot, usIndex); + const uint uiBufferOffset = *reinterpret_cast(pItem + NUM_BankSlotItemBufferOffsetOffset); + + uint uiSize = 0; + if (usIndex + 1 < static_cast(sNumSounds)) + { + const uint uiNextOffset = *reinterpret_cast(GetBankSlotItem(pBankSlot, usIndex + 1) + NUM_BankSlotItemBufferOffsetOffset); + if (uiNextOffset <= uiBufferOffset) + return false; + uiSize = uiNextOffset - uiBufferOffset; + } + else + { + if (uiBufferOffset >= uiNumBytes - NUM_BankStreamHeaderSize) + return false; + uiSize = uiNumBytes - NUM_BankStreamHeaderSize - uiBufferOffset; + } + + const BYTE* pPcmData = pSlotData + NUM_BankStreamHeaderSize + uiBufferOffset; + if (pPcmData + uiSize > pSlotData + uiNumBytes) + return false; + + pOutPcmData = const_cast(pPcmData); + uiOutPcmSize = uiSize; + uiOutSampleRate = *reinterpret_cast(pItem + NUM_BankSlotItemSampleFreqOffset); + iOutLoopStartOffset = *reinterpret_cast(pItem + NUM_BankSlotItemLoopOffsetOffset); + return true; +} + +bool CAEAudioHardwareSA::PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) +{ + void* pPcmDataDst = nullptr; + uint uiPcmSize = 0; + uint uiSampleRate = 0; + int iLoopStartOffset = -1; + + if (!GetLoadedSoundInfo(usBankSlot, usIndex, pPcmDataDst, uiPcmSize, uiSampleRate, iLoopStartOffset)) + return false; + + if (!pPcmData || uiDataSize > uiPcmSize) + return false; + + memcpy(pPcmDataDst, pPcmData, uiDataSize); + if (uiDataSize < uiPcmSize) + { + BYTE* pPad = static_cast(pPcmDataDst) + uiDataSize; + uint uiPadSize = uiPcmSize - uiDataSize; + if (iLoopStartOffset >= 0) + { + while (uiPadSize > 0) + { + const uint uiCopy = std::min(uiPadSize, uiDataSize); + memcpy(pPad, pPcmData, uiCopy); + pPad += uiCopy; + uiPadSize -= uiCopy; + } + } + else + { + memset(pPad, 0, uiPadSize); + } + } + + return true; +} From 26c0552318786b0fe9c80306988bc5b5d8e86013 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:21 +0300 Subject: [PATCH 12/37] Add new methods for sound info and patching --- Client/game_sa/CAEAudioHardwareSA.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Client/game_sa/CAEAudioHardwareSA.h b/Client/game_sa/CAEAudioHardwareSA.h index 8ab5f343dae..f4877cd64e4 100644 --- a/Client/game_sa/CAEAudioHardwareSA.h +++ b/Client/game_sa/CAEAudioHardwareSA.h @@ -29,6 +29,10 @@ class CAEAudioHardwareSA : public CAEAudioHardware bool IsSoundBankLoaded(short wSoundBankID, short wSoundBankSlotID); void LoadSoundBank(short wSoundBankID, short wSoundBankSlotID); + bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, unsigned int& uiOutSampleRate, + int& iOutLoopStartOffset) const override; + bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) override; + private: CAEAudioHardwareSAInterface* m_pInterface; }; From b97eeafaa404ed4bf9aa27dc40074dae5d5e41f5 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:27 +0300 Subject: [PATCH 13/37] Implement PCM decoding in CClientSoundManager Added DecodeToPcm function to handle PCM decoding from sound files. --- .../deathmatch/logic/CClientSoundManager.cpp | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.cpp b/Client/mods/deathmatch/logic/CClientSoundManager.cpp index beef138fefd..3131effdcb8 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientSoundManager.cpp @@ -356,6 +356,38 @@ bool CClientSoundManager::ValidateSound(const SString& strSound, bool bIsRawData return true; } +bool CClientSoundManager::DecodeToPcm(const SString& strSound, bool bIsRawData, uint uiSampleRate, std::vector& outPcm) const +{ + outPcm.clear(); + + HSTREAM hStream; + if (bIsRawData) + hStream = BASS_StreamCreateFile(true, strSound.data(), 0, static_cast(strSound.size()), BASS_STREAM_DECODE | BASS_SAMPLE_MONO); + else + hStream = BASS_StreamCreateFile(false, FromUTF8(strSound), 0, 0, BASS_STREAM_DECODE | BASS_UNICODE | BASS_SAMPLE_MONO); + + if (!hStream) + return false; + + if (uiSampleRate > 0) + BASS_ChannelSetAttribute(hStream, BASS_ATTRIB_FREQ, static_cast(uiSampleRate)); + + const uint uiTotalBytes = static_cast(BASS_ChannelBytes2Seconds(hStream, BASS_ChannelGetLength(hStream, BASS_POS_BYTE)) * uiSampleRate * 2); + outPcm.reserve(uiTotalBytes); + + char buffer[8192]; + for (;;) + { + const DWORD dwRead = BASS_ChannelGetData(hStream, buffer, sizeof(buffer)); + if (dwRead == static_cast(-1) || dwRead == 0) + break; + outPcm.insert(outPcm.end(), buffer, buffer + dwRead); + } + + BASS_StreamFree(hStream); + return !outPcm.empty(); +} + // // Lists // From 6e1361cd360fc3fff16d3f418b357cf9e51b0fc3 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:32 +0300 Subject: [PATCH 14/37] Add DecodeToPcm method to CClientSoundManager --- Client/mods/deathmatch/logic/CClientSoundManager.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.h b/Client/mods/deathmatch/logic/CClientSoundManager.h index 80cd28bf3ee..22770826bd2 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientSoundManager.h @@ -38,6 +38,8 @@ class CClientSoundManager bool ValidateSound(const SString& strSound, bool bIsRawData, SString* pOutError = nullptr); + bool DecodeToPcm(const SString& strSound, bool bIsRawData, uint uiSampleRate, std::vector& outPcm) const; + void AddToList(CClientSound* pSound); void RemoveFromList(CClientSound* pSound); From 974cfee9bd88353bd01a3e0e06c689a621e18cc0 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:38 +0300 Subject: [PATCH 15/37] Refactor sound replacement logic in CClientWorldSoundManager --- .../logic/CClientWorldSoundManager.cpp | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp index 28304227e22..fbe8db5475a 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -15,6 +15,7 @@ #include "CClientManager.h" #include "CClientSoundManager.h" #include +#include #include CClientWorldSoundManager::CClientWorldSoundManager(CClientManager* pManager) : m_pManager(pManager), m_uiLastPruneTick(0) @@ -52,7 +53,20 @@ bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SS if (!m_pManager->GetSoundManager()->ValidateSound(strSound, bIsRawData, pOutError)) return false; - m_Replacements[MakeKey(uiGroup, uiIndex)] = {bIsRawData, strSound, std::max(0.0f, fMinDistance), std::max(0.0f, fMaxDistance)}; + SReplacement replacement; + replacement.bRawData = bIsRawData; + replacement.strSound = strSound; + replacement.fMinDistance = std::max(0.0f, fMinDistance); + replacement.fMaxDistance = std::max(0.0f, fMaxDistance); + replacement.bNativeWanted = (uiIndex != static_cast(-1)); + replacement.uiNativeLastTryTick = 0; + + const uint uiKey = MakeKey(uiGroup, uiIndex); + m_Replacements[uiKey] = replacement; + + if (replacement.bNativeWanted) + TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); + return true; } @@ -65,6 +79,7 @@ bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) { if ((iter->first >> 16) == uiGroup) { + RestoreSoundBuffer(iter->second, uiGroup, iter->first & 0xFFFF); iter = m_Replacements.erase(iter); bErased = true; } @@ -74,11 +89,26 @@ bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) return bErased; } - return m_Replacements.erase(MakeKey(uiGroup, uiIndex)) > 0; + auto iter = m_Replacements.find(MakeKey(uiGroup, uiIndex)); + if (iter == m_Replacements.end()) + return false; + + RestoreSoundBuffer(iter->second, uiGroup, uiIndex); + m_Replacements.erase(iter); + return true; } void CClientWorldSoundManager::RestoreAll() { + if (g_pGame) + { + for (auto& iter : m_Replacements) + { + const uint uiIndex = iter.first & 0xFFFF; + if (uiIndex != static_cast(-1)) + RestoreSoundBuffer(iter.second, iter.first >> 16, uiIndex); + } + } m_Replacements.clear(); } @@ -117,12 +147,88 @@ bool CClientWorldSoundManager::FindReplacement(uint uiGroup, uint uiIndex, const return false; } +bool CClientWorldSoundManager::TryApplyNativeReplacement(SReplacement& replacement, uint uiGroup, uint uiIndex) +{ + if (!g_pGame) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + void* pPcmData = nullptr; + uint uiPcmSize = 0; + uint uiSampleRate = 0; + int iLoopStartOffset = -1; + if (!pAudioHardware->GetLoadedSoundInfo(static_cast(uiGroup), static_cast(uiIndex), pPcmData, uiPcmSize, uiSampleRate, iLoopStartOffset)) + return false; + + if (replacement.pcmData.empty()) + { + replacement.originalPcm.assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); + + if (!m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiSampleRate, replacement.pcmData)) + return false; + + if (replacement.pcmData.size() > uiPcmSize) + return false; + } + + return pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.pcmData.data(), + static_cast(replacement.pcmData.size())); +} + +bool CClientWorldSoundManager::RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup, uint uiIndex) +{ + if (!replacement.bNativeApplied || replacement.originalPcm.empty() || !g_pGame) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + return pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.originalPcm.data(), + static_cast(replacement.originalPcm.size())); +} + +void CClientWorldSoundManager::ApplyNativeReplacements() +{ + const uint uiNow = GetTickCount32(); + + for (auto& iter : m_Replacements) + { + const uint uiIndex = iter.first & 0xFFFF; + if (uiIndex == static_cast(-1)) + continue; + + SReplacement& replacement = iter.second; + if (!replacement.bNativeWanted) + continue; + + if (!replacement.bNativeApplied) + { + if (uiNow - replacement.uiNativeLastTryTick < 500) + continue; + replacement.uiNativeLastTryTick = uiNow; + replacement.bNativeApplied = TryApplyNativeReplacement(replacement, iter.first >> 16, uiIndex); + } + else if (uiNow - replacement.uiNativeLastTryTick > 1000) + { + replacement.uiNativeLastTryTick = uiNow; + TryApplyNativeReplacement(replacement, iter.first >> 16, uiIndex); + } + } +} + bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event, bool bAllowPlay) { const SReplacement* pReplacement = nullptr; if (!FindReplacement(event.uiGroup, event.uiIndex, &pReplacement)) return false; + if (pReplacement->bNativeApplied) + return false; + if (bAllowPlay) { const uint uiKey = MakeKey(event.uiGroup, event.uiIndex); @@ -213,4 +319,6 @@ void CClientWorldSoundManager::DoPulse() ++iter; } } + + ApplyNativeReplacements(); } From 4808320e19b548080a687288e42a6405e7e61ceb Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:45 +0300 Subject: [PATCH 16/37] Enhance CClientWorldSoundManager with native sound support Added new members for native sound replacement handling. --- .../mods/deathmatch/logic/CClientWorldSoundManager.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h index 0286983cdec..6a4dc824302 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -13,6 +13,7 @@ #include #include +#include #include class CClientManager; @@ -42,6 +43,12 @@ class CClientWorldSoundManager SString strSound; float fMinDistance; float fMaxDistance; + + bool bNativeWanted = false; + bool bNativeApplied = false; + std::vector pcmData; + std::vector originalPcm; + uint uiNativeLastTryTick = 0; }; struct SLastPlayed @@ -61,6 +68,10 @@ class CClientWorldSoundManager bool FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const; + bool TryApplyNativeReplacement(SReplacement& replacement, uint uiGroup, uint uiIndex); + bool RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup, uint uiIndex); + void ApplyNativeReplacements(); + CClientManager* m_pManager; std::unordered_map m_Replacements; std::unordered_map m_LastPlayed; From f5a815ca730b310a026aba582b1531cc44b74db9 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:34:50 +0300 Subject: [PATCH 17/37] Add new methods for sound bank management --- Client/sdk/game/CAEAudioHardware.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Client/sdk/game/CAEAudioHardware.h b/Client/sdk/game/CAEAudioHardware.h index 2061112317e..503889bb276 100644 --- a/Client/sdk/game/CAEAudioHardware.h +++ b/Client/sdk/game/CAEAudioHardware.h @@ -65,4 +65,9 @@ class CAEAudioHardware public: virtual bool IsSoundBankLoaded(short wSoundBankID, short wSoundBankSlotID) = 0; virtual void LoadSoundBank(short wSoundBankID, short wSoundBankSlotID) = 0; + + virtual bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, + unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const = 0; + + virtual bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) = 0; }; From b56d4b8700ec606bb21d8192aa5e9dd0fb8c550d Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:29:35 +0300 Subject: [PATCH 18/37] Add GetChannelFrequencyScalingFactors method --- Client/sdk/game/CAEAudioHardware.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/sdk/game/CAEAudioHardware.h b/Client/sdk/game/CAEAudioHardware.h index 503889bb276..ca004602287 100644 --- a/Client/sdk/game/CAEAudioHardware.h +++ b/Client/sdk/game/CAEAudioHardware.h @@ -70,4 +70,5 @@ class CAEAudioHardware unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const = 0; virtual bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) = 0; + virtual void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const = 0; }; From 5d92852c2dfd726072e164b2d37e7bb48b92c581 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:29:39 +0300 Subject: [PATCH 19/37] Add GetChannelFrequencyScalingFactors method --- Client/game_sa/CAEAudioHardwareSA.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/game_sa/CAEAudioHardwareSA.h b/Client/game_sa/CAEAudioHardwareSA.h index f4877cd64e4..4e13e85b423 100644 --- a/Client/game_sa/CAEAudioHardwareSA.h +++ b/Client/game_sa/CAEAudioHardwareSA.h @@ -32,6 +32,7 @@ class CAEAudioHardwareSA : public CAEAudioHardware bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const override; bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) override; + void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const override; private: CAEAudioHardwareSAInterface* m_pInterface; From 4d5270d32af409cbe6654b943690d2fa2d678478 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:29:42 +0300 Subject: [PATCH 20/37] Add new constants for audio hardware offsets --- Client/game_sa/CAEAudioHardwareSA.cpp | 64 ++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/Client/game_sa/CAEAudioHardwareSA.cpp b/Client/game_sa/CAEAudioHardwareSA.cpp index 9b88648cc9c..2af781c49bd 100644 --- a/Client/game_sa/CAEAudioHardwareSA.cpp +++ b/Client/game_sa/CAEAudioHardwareSA.cpp @@ -58,14 +58,20 @@ namespace { constexpr DWORD NUM_AudioHardwareBankLoaderOffset = 0xD98; + constexpr DWORD NUM_AudioHardwareNumChannelsOffset = 0x8E; + constexpr DWORD NUM_AudioHardwareFreqScalingOffset = 0x310; + constexpr DWORD NUM_BankLoaderSlotsOffset = 0x00; + constexpr DWORD NUM_BankLoaderBankLkupsOffset = 0x04; constexpr DWORD NUM_BankLoaderSlotCountOffset = 0x0C; + constexpr DWORD NUM_BankLoaderBankLkupCntOffset = 0x0E; constexpr DWORD NUM_BankLoaderBufferSizeOffset = 0x18; constexpr DWORD NUM_BankLoaderBufferOffset = 0x1C; constexpr DWORD NUM_BankSlotSize = 0x12D4; constexpr DWORD NUM_BankSlotOffsetBytesOffset = 0x00; constexpr DWORD NUM_BankSlotNumBytesOffset = 0x04; + constexpr DWORD NUM_BankSlotBankIdOffset = 0x10; constexpr DWORD NUM_BankSlotNumSoundsOffset = 0x12; constexpr DWORD NUM_BankSlotSoundsArrayOffset = 0x14; @@ -74,8 +80,10 @@ namespace constexpr DWORD NUM_BankSlotItemLoopOffsetOffset = 0x04; constexpr DWORD NUM_BankSlotItemSampleFreqOffset = 0x08; - constexpr DWORD NUM_BankStreamHeaderSize = 0x12C4; - constexpr uint NUM_MaxBankSounds = 400; + constexpr DWORD NUM_BankLookupSize = 0x0C; + constexpr DWORD NUM_BankLookupNumBytesOffset = 0x08; + + constexpr uint NUM_MaxBankSounds = 400; const BYTE* GetBankLoader() { @@ -114,7 +122,7 @@ namespace const uint uiOffsetBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotOffsetBytesOffset); const uint uiNumBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotNumBytesOffset); - if (uiOffsetBytes + uiNumBytes > uiBufferSize || uiNumBytes <= NUM_BankStreamHeaderSize) + if (uiOffsetBytes + uiNumBytes > uiBufferSize || uiNumBytes == 0) return nullptr; return pBuffer + uiOffsetBytes; @@ -124,6 +132,24 @@ namespace { return pBankSlot + NUM_BankSlotSoundsArrayOffset + usIndex * NUM_BankSlotItemSize; } + + uint GetBankPcmSize(const BYTE* pBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return 0; + + const BYTE* pBankLkups = *reinterpret_cast(pBankLoader + NUM_BankLoaderBankLkupsOffset); + if (!pBankLkups) + return 0; + + const ushort usBankLkupCnt = *reinterpret_cast(pBankLoader + NUM_BankLoaderBankLkupCntOffset); + const short sBankId = *reinterpret_cast(pBankSlot + NUM_BankSlotBankIdOffset); + if (sBankId < 0 || sBankId >= static_cast(usBankLkupCnt)) + return 0; + + return *reinterpret_cast(pBankLkups + sBankId * NUM_BankLookupSize + NUM_BankLookupNumBytesOffset); + } } bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, @@ -161,12 +187,22 @@ bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned } else { - if (uiBufferOffset >= uiNumBytes - NUM_BankStreamHeaderSize) - return false; - uiSize = uiNumBytes - NUM_BankStreamHeaderSize - uiBufferOffset; + const uint uiBankPcmSize = GetBankPcmSize(pBankSlot); + if (uiBankPcmSize > 0) + { + if (uiBufferOffset >= uiBankPcmSize) + return false; + uiSize = uiBankPcmSize - uiBufferOffset; + } + else + { + if (uiBufferOffset >= uiNumBytes) + return false; + uiSize = uiNumBytes - uiBufferOffset; + } } - const BYTE* pPcmData = pSlotData + NUM_BankStreamHeaderSize + uiBufferOffset; + const BYTE* pPcmData = pSlotData + uiBufferOffset; if (pPcmData + uiSize > pSlotData + uiNumBytes) return false; @@ -177,6 +213,20 @@ bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned return true; } +void CAEAudioHardwareSA::GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const +{ + if (!pOutFactors || uiMax == 0) + return; + + const BYTE* pBase = reinterpret_cast(m_pInterface); + const ushort usNumChannels = *reinterpret_cast(pBase + NUM_AudioHardwareNumChannelsOffset); + const uint uiCount = std::min(uiMax, usNumChannels); + const float* pFactors = reinterpret_cast(pBase + NUM_AudioHardwareFreqScalingOffset); + + for (uint i = 0; i < uiCount; ++i) + pOutFactors[i] = pFactors[i]; +} + bool CAEAudioHardwareSA::PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) { void* pPcmDataDst = nullptr; From 7785107017947b7d90087a8413acab418f2fa726 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:29:45 +0300 Subject: [PATCH 21/37] Refactor sound replacement logic in CClientWorldSoundManager --- .../deathmatch/logic/CClientWorldSoundManager.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp index fbe8db5475a..c517fc82846 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -62,10 +62,18 @@ bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SS replacement.uiNativeLastTryTick = 0; const uint uiKey = MakeKey(uiGroup, uiIndex); + auto iterExisting = m_Replacements.find(uiKey); + if (iterExisting != m_Replacements.end()) + { + replacement.originalPcm = std::move(iterExisting->second.originalPcm); + } + m_Replacements[uiKey] = replacement; if (replacement.bNativeWanted) - TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); + { + m_Replacements[uiKey].bNativeApplied = TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); + } return true; } @@ -165,7 +173,8 @@ bool CClientWorldSoundManager::TryApplyNativeReplacement(SReplacement& replaceme if (replacement.pcmData.empty()) { - replacement.originalPcm.assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); + if (replacement.originalPcm.empty()) + replacement.originalPcm.assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); if (!m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiSampleRate, replacement.pcmData)) return false; From 6705351341ed1624fb554b06b1bf9df72e8213d2 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:35:58 +0300 Subject: [PATCH 22/37] Update purpose comment in CClientWorldSoundManager.cpp --- Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp index c517fc82846..01712b48a8d 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -3,7 +3,7 @@ * PROJECT: Multi Theft Auto v1.0 * LICENSE: See LICENSE in the top level directory * FILE: mods/deathmatch/logic/CClientWorldSoundManager.cpp - * PURPOSE: Client-side world sound replacement (engineReplaceWorldSound) + * PURPOSE: Client-side world sound replacement * * Multi Theft Auto is available from https://www.multitheftauto.com/ * From c9700f92849eb2f64291b8b66acd85bdbc917d5f Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:36:01 +0300 Subject: [PATCH 23/37] Update purpose comment in CClientWorldSoundManager.h --- Client/mods/deathmatch/logic/CClientWorldSoundManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h index 6a4dc824302..3602ceca523 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -3,7 +3,7 @@ * PROJECT: Multi Theft Auto v1.0 * LICENSE: See LICENSE in the top level directory * FILE: mods/deathmatch/logic/CClientWorldSoundManager.h - * PURPOSE: Client-side world sound replacement (engineReplaceWorldSound) + * PURPOSE: Client-side world sound replacement * * Multi Theft Auto is available from https://www.multitheftauto.com/ * From 7463bebbfef0703b2a67f8ecefbdfb899a59b74e Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:36:07 +0300 Subject: [PATCH 24/37] Add new audio functions for sound management --- .../logic/luadefs/CLuaAudioDefs.cpp | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp index 3feda344b83..41fde0b050d 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp @@ -12,6 +12,7 @@ #include "StdInc.h" #include #include "CBassAudio.h" +#include "CClientWorldSoundManager.h" void CLuaAudioDefs::LoadFunctions() { @@ -23,6 +24,10 @@ void CLuaAudioDefs::LoadFunctions() {"setWorldSoundEnabled", SetWorldSoundEnabled}, {"isWorldSoundEnabled", IsWorldSoundEnabled}, {"resetWorldSounds", ResetWorldSounds}, + {"replaceWorldSound", ReplaceWorldSound}, + {"restoreWorldSound", RestoreWorldSound}, + {"restoreAllWorldSounds", RestoreAllWorldSounds}, + {"isWorldSoundReplaced", IsWorldSoundReplaced}, {"playSFX", PlaySFX}, {"playSFX3D", PlaySFX3D}, {"getSFXStatus", GetSFXStatus}, @@ -144,6 +149,127 @@ void CLuaAudioDefs::AddClass(lua_State* luaVM) lua_registerclass(luaVM, "Sound3D", "Sound"); } +int CLuaAudioDefs::ReplaceWorldSound(lua_State* luaVM) +{ + SString strSound; + int group; + int index = -1; + float fMinDistance = -1.0f; + float fMaxDistance = -1.0f; + CScriptArgReader argStream(luaVM); + argStream.ReadString(strSound); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + if (argStream.NextIsNumber()) + argStream.ReadNumber(fMinDistance); + if (argStream.NextIsNumber()) + argStream.ReadNumber(fMaxDistance); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + CLuaMain* pLuaMain = m_pLuaManager->GetVirtualMachine(luaVM); + if (!pLuaMain || !g_pClientGame) + { + lua_pushboolean(luaVM, false); + return 1; + } + + CResource* pResource = pLuaMain->GetResource(); + if (!pResource) + { + lua_pushboolean(luaVM, false); + return 1; + } + + SString strOriginal = strSound; + SString strFilename; + bool bIsRawData = false; + if (CResourceManager::ParseResourcePathInput(strSound, pResource, &strFilename, nullptr, true)) + strSound = strFilename; + else + bIsRawData = true; + + if (!g_pClientGame->GetWorldSoundManager()) + { + lua_pushboolean(luaVM, false); + return 1; + } + + SString strError; + bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, index, strSound, bIsRawData, fMinDistance, fMaxDistance, &strError); + if (!bSuccess && !strError.empty()) + m_pScriptDebugging->LogWarning(luaVM, "replaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, index, strOriginal.c_str()); + + lua_pushboolean(luaVM, bSuccess); + return 1; +} + +int CLuaAudioDefs::RestoreWorldSound(lua_State* luaVM) +{ + int group; + int index = -1; + CScriptArgReader argStream(luaVM); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + if (!g_pClientGame || !g_pClientGame->GetWorldSoundManager()) + { + lua_pushboolean(luaVM, false); + return 1; + } + + bool bSuccess = g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index); + lua_pushboolean(luaVM, bSuccess); + return 1; +} + +int CLuaAudioDefs::RestoreAllWorldSounds(lua_State* luaVM) +{ + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + g_pClientGame->GetWorldSoundManager()->RestoreAll(); + + lua_pushboolean(luaVM, true); + return 1; +} + +int CLuaAudioDefs::IsWorldSoundReplaced(lua_State* luaVM) +{ + int group; + int index = -1; + CScriptArgReader argStream(luaVM); + argStream.ReadNumber(group); + if (argStream.NextIsNumber()) + argStream.ReadNumber(index); + + if (argStream.HasErrors()) + { + m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); + lua_pushboolean(luaVM, false); + return 1; + } + + bool bReplaced = false; + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + bReplaced = g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index); + + lua_pushboolean(luaVM, bReplaced); + return 1; +} + int CLuaAudioDefs::PlaySound(lua_State* luaVM) { SString strSound = ""; From 95e5b7b891a9d723a75c0349f7f54b676b61ac0b Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:36:11 +0300 Subject: [PATCH 25/37] Add new audio-related Lua declarations --- Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h index 2f9c63b3a32..d949462caa2 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h @@ -26,6 +26,10 @@ class CLuaAudioDefs : public CLuaDefs LUA_DECLARE(SetWorldSoundEnabled); LUA_DECLARE(IsWorldSoundEnabled); LUA_DECLARE(ResetWorldSounds); + LUA_DECLARE(ReplaceWorldSound); + LUA_DECLARE(RestoreWorldSound); + LUA_DECLARE(RestoreAllWorldSounds); + LUA_DECLARE(IsWorldSoundReplaced); LUA_DECLARE(PlaySFX); LUA_DECLARE(PlaySFX3D); LUA_DECLARE(GetSFXStatus); From a85ad27e5e2b2cc6ea5406acdad6eaff3b37b14d Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:36:15 +0300 Subject: [PATCH 26/37] Remove world sound management functions Removed functions related to world sound management from CLuaEngineDefs.cpp. --- .../logic/luadefs/CLuaEngineDefs.cpp | 129 ------------------ 1 file changed, 129 deletions(-) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index bd096b11976..fefa088e4ac 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -98,10 +98,6 @@ void CLuaEngineDefs::LoadFunctions() {"engineReplaceCOL", EngineReplaceCOL}, {"engineRestoreCOL", EngineRestoreCOL}, {"engineReplaceModel", EngineReplaceModel}, - {"engineReplaceWorldSound", EngineReplaceWorldSound}, - {"engineRestoreWorldSound", EngineRestoreWorldSound}, - {"engineRestoreAllWorldSounds", EngineRestoreAllWorldSounds}, - {"isWorldSoundReplaced", EngineIsWorldSoundReplaced}, {"engineAddClothingModel", ArgumentParser}, {"engineRestoreModel", EngineRestoreModel}, {"engineReplaceAnimation", EngineReplaceAnimation}, @@ -183,137 +179,12 @@ void CLuaEngineDefs::LoadFunctions() CLuaCFunctions::AddFunction(name, func); } -int CLuaEngineDefs::EngineReplaceWorldSound(lua_State* luaVM) -{ - SString strSound; - int group; - int index = -1; - float fMinDistance = -1.0f; - float fMaxDistance = -1.0f; - CScriptArgReader argStream(luaVM); - argStream.ReadString(strSound); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - if (argStream.NextIsNumber()) - argStream.ReadNumber(fMinDistance); - if (argStream.NextIsNumber()) - argStream.ReadNumber(fMaxDistance); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - - CLuaMain* pLuaMain = m_pLuaManager->GetVirtualMachine(luaVM); - if (!pLuaMain || !g_pClientGame) - { - lua_pushboolean(luaVM, false); - return 1; - } - - CResource* pResource = pLuaMain->GetResource(); - if (!pResource) - { - lua_pushboolean(luaVM, false); - return 1; - } - - SString strOriginal = strSound; - SString strFilename; - bool bIsRawData = false; - if (CResourceManager::ParseResourcePathInput(strSound, pResource, &strFilename, nullptr, true)) - strSound = strFilename; - else - bIsRawData = true; - - if (!g_pClientGame->GetWorldSoundManager()) - { - lua_pushboolean(luaVM, false); - return 1; - } - - SString strError; - bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, index, strSound, bIsRawData, fMinDistance, fMaxDistance, &strError); - if (!bSuccess && !strError.empty()) - m_pScriptDebugging->LogWarning(luaVM, "engineReplaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, index, strOriginal.c_str()); - - lua_pushboolean(luaVM, bSuccess); - return 1; -} - -int CLuaEngineDefs::EngineRestoreWorldSound(lua_State* luaVM) -{ - int group; - int index = -1; - CScriptArgReader argStream(luaVM); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - - if (!g_pClientGame || !g_pClientGame->GetWorldSoundManager()) - { - lua_pushboolean(luaVM, false); - return 1; - } - - bool bSuccess = g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index); - lua_pushboolean(luaVM, bSuccess); - return 1; -} - -int CLuaEngineDefs::EngineRestoreAllWorldSounds(lua_State* luaVM) -{ - if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) - g_pClientGame->GetWorldSoundManager()->RestoreAll(); - - lua_pushboolean(luaVM, true); - return 1; -} - -int CLuaEngineDefs::EngineIsWorldSoundReplaced(lua_State* luaVM) -{ - int group; - int index = -1; - CScriptArgReader argStream(luaVM); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - - bool bReplaced = false; - if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) - bReplaced = g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index); - - lua_pushboolean(luaVM, bReplaced); - return 1; -} - void CLuaEngineDefs::AddClass(lua_State* luaVM) { lua_newclass(luaVM); lua_classfunction(luaVM, "restoreCOL", "engineRestoreCOL"); lua_classfunction(luaVM, "restoreModel", "engineRestoreModel"); - lua_classfunction(luaVM, "replaceWorldSound", "engineReplaceWorldSound"); - lua_classfunction(luaVM, "restoreWorldSound", "engineRestoreWorldSound"); - lua_classfunction(luaVM, "restoreAllWorldSounds", "engineRestoreAllWorldSounds"); - lua_classfunction(luaVM, "isWorldSoundReplaced", "isWorldSoundReplaced"); lua_classfunction(luaVM, "setAsynchronousLoading", "engineSetAsynchronousLoading"); lua_classfunction(luaVM, "setModelLODDistance", "engineSetModelLODDistance"); lua_classfunction(luaVM, "resetModelLODDistance", "engineResetModelLODDistance"); From 00f53f2cf53417dca46e46f15b0662c18b68fc08 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:36:19 +0300 Subject: [PATCH 27/37] Remove unused world sound Lua engine declarations --- Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h index 356a84ba343..6a107c0ded0 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.h @@ -28,10 +28,6 @@ class CLuaEngineDefs : public CLuaDefs LUA_DECLARE(EngineRestoreCOL); LUA_DECLARE(EngineReplaceModel); LUA_DECLARE(EngineRestoreModel); - LUA_DECLARE(EngineReplaceWorldSound); - LUA_DECLARE(EngineRestoreWorldSound); - LUA_DECLARE(EngineRestoreAllWorldSounds); - LUA_DECLARE(EngineIsWorldSoundReplaced); LUA_DECLARE(EngineRequestModel); LUA_DECLARE(EngineFreeModel); LUA_DECLARE(EngineReplaceAnimation); From b2e0753d1b1538d3181bd119cb52eb10596c2130 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:42:25 +0300 Subject: [PATCH 28/37] Remove unused includes in CLuaEngineDefs.cpp Removed unused includes for CClientWorldSoundManager and CResourceManager. --- Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index fefa088e4ac..d8d393e085e 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -16,8 +16,6 @@ #include #include #include "CLuaEngineDefs.h" -#include "CClientWorldSoundManager.h" -#include "CResourceManager.h" #include //! Set the CModelCacheManager limits From 6e4455b003bc317a33c151f2b825b5926ac12fea Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:53:42 +0300 Subject: [PATCH 29/37] Refactor audio functions to use optional parameters --- .../logic/luadefs/CLuaAudioDefs.cpp | 127 ++++-------------- 1 file changed, 28 insertions(+), 99 deletions(-) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp index 41fde0b050d..034bb809a1e 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp @@ -24,10 +24,10 @@ void CLuaAudioDefs::LoadFunctions() {"setWorldSoundEnabled", SetWorldSoundEnabled}, {"isWorldSoundEnabled", IsWorldSoundEnabled}, {"resetWorldSounds", ResetWorldSounds}, - {"replaceWorldSound", ReplaceWorldSound}, - {"restoreWorldSound", RestoreWorldSound}, - {"restoreAllWorldSounds", RestoreAllWorldSounds}, - {"isWorldSoundReplaced", IsWorldSoundReplaced}, + {"replaceWorldSound", ArgumentParser}, + {"restoreWorldSound", ArgumentParser}, + {"restoreAllWorldSounds", ArgumentParser}, + {"isWorldSoundReplaced", ArgumentParser}, {"playSFX", PlaySFX}, {"playSFX3D", PlaySFX3D}, {"getSFXStatus", GetSFXStatus}, @@ -149,125 +149,54 @@ void CLuaAudioDefs::AddClass(lua_State* luaVM) lua_registerclass(luaVM, "Sound3D", "Sound"); } -int CLuaAudioDefs::ReplaceWorldSound(lua_State* luaVM) +bool CLuaAudioDefs::ReplaceWorldSound(lua_State* luaVM, std::string strSound, int group, std::optional index, std::optional fMinDistance, + std::optional fMaxDistance) { - SString strSound; - int group; - int index = -1; - float fMinDistance = -1.0f; - float fMaxDistance = -1.0f; - CScriptArgReader argStream(luaVM); - argStream.ReadString(strSound); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - if (argStream.NextIsNumber()) - argStream.ReadNumber(fMinDistance); - if (argStream.NextIsNumber()) - argStream.ReadNumber(fMaxDistance); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - - CLuaMain* pLuaMain = m_pLuaManager->GetVirtualMachine(luaVM); - if (!pLuaMain || !g_pClientGame) - { - lua_pushboolean(luaVM, false); - return 1; - } - - CResource* pResource = pLuaMain->GetResource(); - if (!pResource) - { - lua_pushboolean(luaVM, false); - return 1; - } + CLuaMain& luaMain = lua_getownercluamain(luaVM); + CResource* pResource = luaMain.GetResource(); + if (!pResource || !g_pClientGame || !g_pClientGame->GetWorldSoundManager()) + return false; - SString strOriginal = strSound; - SString strFilename; - bool bIsRawData = false; - if (CResourceManager::ParseResourcePathInput(strSound, pResource, &strFilename, nullptr, true)) - strSound = strFilename; + const int iIndex = index.value_or(-1); + SString strSoundCopy = strSound; + SString strFilename; + bool bIsRawData = false; + if (CResourceManager::ParseResourcePathInput(strSoundCopy, pResource, &strFilename, nullptr, true)) + strSoundCopy = strFilename; else bIsRawData = true; - if (!g_pClientGame->GetWorldSoundManager()) - { - lua_pushboolean(luaVM, false); - return 1; - } - SString strError; - bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, index, strSound, bIsRawData, fMinDistance, fMaxDistance, &strError); + bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, iIndex, strSoundCopy, bIsRawData, fMinDistance.value_or(-1.0f), + fMaxDistance.value_or(-1.0f), &strError); if (!bSuccess && !strError.empty()) - m_pScriptDebugging->LogWarning(luaVM, "replaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, index, strOriginal.c_str()); + m_pScriptDebugging->LogWarning(luaVM, "replaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, iIndex, strSound.c_str()); - lua_pushboolean(luaVM, bSuccess); - return 1; + return bSuccess; } -int CLuaAudioDefs::RestoreWorldSound(lua_State* luaVM) +bool CLuaAudioDefs::RestoreWorldSound(int group, std::optional index) { - int group; - int index = -1; - CScriptArgReader argStream(luaVM); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - if (!g_pClientGame || !g_pClientGame->GetWorldSoundManager()) - { - lua_pushboolean(luaVM, false); - return 1; - } + return false; - bool bSuccess = g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index); - lua_pushboolean(luaVM, bSuccess); - return 1; + return g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index.value_or(-1)); } -int CLuaAudioDefs::RestoreAllWorldSounds(lua_State* luaVM) +bool CLuaAudioDefs::RestoreAllWorldSounds() { if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) g_pClientGame->GetWorldSoundManager()->RestoreAll(); - lua_pushboolean(luaVM, true); - return 1; + return true; } -int CLuaAudioDefs::IsWorldSoundReplaced(lua_State* luaVM) +bool CLuaAudioDefs::IsWorldSoundReplaced(int group, std::optional index) { - int group; - int index = -1; - CScriptArgReader argStream(luaVM); - argStream.ReadNumber(group); - if (argStream.NextIsNumber()) - argStream.ReadNumber(index); - - if (argStream.HasErrors()) - { - m_pScriptDebugging->LogCustom(luaVM, argStream.GetFullErrorMessage()); - lua_pushboolean(luaVM, false); - return 1; - } - - bool bReplaced = false; if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) - bReplaced = g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index); + return g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index.value_or(-1)); - lua_pushboolean(luaVM, bReplaced); - return 1; + return false; } int CLuaAudioDefs::PlaySound(lua_State* luaVM) From 2116510031439c842343f1c43eb5858734f02ade Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:53:46 +0300 Subject: [PATCH 30/37] Add static methods for world sound management --- Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h index d949462caa2..cc8cf1f9b54 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h @@ -26,10 +26,13 @@ class CLuaAudioDefs : public CLuaDefs LUA_DECLARE(SetWorldSoundEnabled); LUA_DECLARE(IsWorldSoundEnabled); LUA_DECLARE(ResetWorldSounds); - LUA_DECLARE(ReplaceWorldSound); - LUA_DECLARE(RestoreWorldSound); - LUA_DECLARE(RestoreAllWorldSounds); - LUA_DECLARE(IsWorldSoundReplaced); + + static bool ReplaceWorldSound(lua_State* luaVM, std::string strSound, int group, std::optional index, std::optional fMinDistance, + std::optional fMaxDistance); + static bool RestoreWorldSound(int group, std::optional index); + static bool RestoreAllWorldSounds(); + static bool IsWorldSoundReplaced(int group, std::optional index); + LUA_DECLARE(PlaySFX); LUA_DECLARE(PlaySFX3D); LUA_DECLARE(GetSFXStatus); From 605a8a3639338e57fc49b6829df05c29a46b5698 Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:43:24 +0300 Subject: [PATCH 31/37] Add IsWorldSoundStillActive method to CAudioEngineSA --- Client/game_sa/CAudioEngineSA.cpp | 44 ++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/Client/game_sa/CAudioEngineSA.cpp b/Client/game_sa/CAudioEngineSA.cpp index 7cf171c7291..6d006766dd2 100644 --- a/Client/game_sa/CAudioEngineSA.cpp +++ b/Client/game_sa/CAudioEngineSA.cpp @@ -14,6 +14,7 @@ #include "CAEAudioHardwareSA.h" #include "CAudioEngineSA.h" #include "CGameSA.h" +#include "CPadSA.h" #include "CPhysicalSA.h" #include "CSettingsSA.h" @@ -555,7 +556,12 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) pGameEntity = pAESound->pAudioEntity->pEntity; SWorldSoundEvent event = { - pAESound->usGroup, pAESound->usIndex, pGameEntity, pAESound->m_vCurrPosn, pAESound->m_fSoundDistance, + pAESound->usGroup, + pAESound->usIndex, + pGameEntity, + pAESound->m_vCurrPosn, + pAESound->m_fSoundDistance, + pAESound->usGroup == BANKSLOT_HORNS || pAESound->m_nLoopCounter < 0 || pAESound->m_nLoopCounter > 1, }; return m_pWorldSoundHandler(event); @@ -564,6 +570,42 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) return true; } +bool CAudioEngineSA::IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const +{ + if (uiGroup == BANKSLOT_HORNS) + { + auto* pPad = dynamic_cast(pGame ? pGame->GetPad() : nullptr); + if (!pPad) + return false; + + const auto* pPadInterface = pPad->GetInterface(); + for (uint uiHistoryIndex = 0; uiHistoryIndex < MAX_HORN_HISTORY; ++uiHistoryIndex) + { + if (pPadInterface->bHornHistory[uiHistoryIndex]) + return true; + } + return false; + } + + const auto* pSoundManager = reinterpret_cast(CLASS_CAESoundManager); + if (!pSoundManager) + return false; + + for (const CAESound& sound : pSoundManager->m_aSound) + { + if (sound.m_nIsUsed != 0 && sound.usGroup == uiGroup && sound.usIndex == uiIndex) + { + if (!pEntity) + return true; + if (sound.pGameEntity == pEntity) + return true; + if (sound.pAudioEntity && sound.pAudioEntity->pEntity == pEntity) + return true; + } + } + return false; +} + //////////////////////////////////////////////////////////////////////////////////////////////// // Return false to skip sound static bool _cdecl OnRequestNewSound(CAESound* pAESound) From 6face62885fd88c9b0edc028ba53d526f1baeffb Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:43:28 +0300 Subject: [PATCH 32/37] Enhance sound management with looping support --- .../logic/CClientWorldSoundManager.cpp | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp index 01712b48a8d..524e7cf5092 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -254,8 +254,11 @@ bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event, b if (!bAlreadyPlayed) { + const bool bLoop = event.bLoop && event.uiGroup == BANKSLOT_HORNS && event.pGameEntity != nullptr && g_pGame && + g_pGame->GetPools()->GetVehicle((DWORD*)event.pGameEntity) != nullptr; + CClientSound* pSound = - m_pManager->GetSoundManager()->PlaySound3D(pReplacement->strSound, false, pReplacement->bRawData, event.vecPosition, false, true); + m_pManager->GetSoundManager()->PlaySound3D(pReplacement->strSound, false, pReplacement->bRawData, event.vecPosition, bLoop, true); if (!pSound) { return false; @@ -284,7 +287,7 @@ bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event, b pSound->SetVolume(fVolume); if (event.pGameEntity) - m_FollowSounds.push_back({pSound, event.pGameEntity, GetTickCount32()}); + m_FollowSounds.push_back({pSound, event.pGameEntity, GetTickCount32(), event.uiGroup, event.uiIndex, bLoop}); m_LastPlayed[uiKey] = {GetTickCount32(), event.vecPosition}; } @@ -301,10 +304,26 @@ void CClientWorldSoundManager::DoPulse() { SFollowSound& entry = *iter; - if (entry.pSound->IsFinished() || uiNow - entry.uiStartTick > 10000) + if (!entry.bLooping) { - iter = m_FollowSounds.erase(iter); - continue; + if (entry.pSound->IsFinished() || uiNow - entry.uiStartTick > 10000) + { + iter = m_FollowSounds.erase(iter); + continue; + } + } + else + { + const bool bOriginalActive = + g_pGame && g_pGame->GetAudioEngine() && g_pGame->GetAudioEngine()->IsWorldSoundStillActive(entry.uiGroup, entry.uiIndex, entry.pGameEntity); + const bool bSoundFinished = entry.pSound->IsFinished(); + if (!bOriginalActive || bSoundFinished || uiNow - entry.uiStartTick > 60000) + { + if (!bSoundFinished) + g_pClientGame->GetElementDeleter()->Delete(entry.pSound); + iter = m_FollowSounds.erase(iter); + continue; + } } if (CClientEntity* pEntity = g_pGame->GetPools()->GetClientEntity((DWORD*)entry.pGameEntity)) From 85bec494fbe600e030030b2d4200a3cdd5d860ae Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:43:31 +0300 Subject: [PATCH 33/37] Enhance sound structure with group and index fields Added uiGroup and uiIndex to sound structure for better management. --- Client/mods/deathmatch/logic/CClientWorldSoundManager.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h index 3602ceca523..08a51d64784 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -62,6 +62,9 @@ class CClientWorldSoundManager CClientSound* pSound; CEntitySAInterface* pGameEntity; uint uiStartTick; + uint uiGroup; + uint uiIndex; + bool bLooping; }; static uint MakeKey(uint uiGroup, uint uiIndex) { return (uiGroup << 16) | (uiIndex & 0xFFFF); } From 8ec657e2d3c80c6a909a7de0a85ad28a7ac9c03e Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:43:34 +0300 Subject: [PATCH 34/37] Add loop flag and IsWorldSoundStillActive method --- Client/sdk/game/CAudioEngine.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/sdk/game/CAudioEngine.h b/Client/sdk/game/CAudioEngine.h index 72f149a7e59..34701c43fca 100644 --- a/Client/sdk/game/CAudioEngine.h +++ b/Client/sdk/game/CAudioEngine.h @@ -27,6 +27,7 @@ struct SWorldSoundEvent CEntitySAInterface* pGameEntity; CVector vecPosition; float fMaxDistance; + bool bLoop; }; using WorldSoundHandler = bool(const SWorldSoundEvent& event); @@ -77,4 +78,6 @@ class CAudioEngine virtual void SetWorldSoundHandler(WorldSoundHandler* pHandler) = 0; virtual void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2) = 0; virtual void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical) = 0; + + virtual bool IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const = 0; }; From 63c5b727378771ae182abdba443fed8a2a10032f Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:43:37 +0300 Subject: [PATCH 35/37] Add member variable for channel sound table --- Client/game_sa/CAESoundManagerSA.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/game_sa/CAESoundManagerSA.h b/Client/game_sa/CAESoundManagerSA.h index eab09198450..eae8716744c 100644 --- a/Client/game_sa/CAESoundManagerSA.h +++ b/Client/game_sa/CAESoundManagerSA.h @@ -17,6 +17,7 @@ class CAESoundManagerSAInterface { +public: int16_t m_wNumAvailableChannels; // + 0x0000 // = CAEAudioHardware::GetNumAvailableChannels(...), [10, 300] int16_t m_wChannel; // + 0x0002 // = CAEAudioHardware::AllocateChannels(...), could be -1 CAESound m_aSound[300]; // + 0x0004 From 76080e5474aa06a9d376d9b45dc315056274538b Mon Sep 17 00:00:00 2001 From: Youssef Maged <57957016+iManGaaX@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:25:17 +0300 Subject: [PATCH 36/37] Refactor CAudioEngineSA.h structure and variables --- Client/game_sa/CAudioEngineSA.h | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Client/game_sa/CAudioEngineSA.h b/Client/game_sa/CAudioEngineSA.h index f86f5d8b0ce..09fcec88158 100644 --- a/Client/game_sa/CAudioEngineSA.h +++ b/Client/game_sa/CAudioEngineSA.h @@ -75,11 +75,9 @@ class CAESound float m_fCurrCamDist; // +72 float m_fPrevCamDist; // +76 float m_fTimeScale; // +80 - char unk2; // +84 = 31488 - char unk3; // +85 = 1005 union { - unsigned short m_wEnvironmentFlags; + unsigned short m_wEnvironmentFlags; // +84 struct { unsigned short m_bFrontEnd : 1; @@ -97,17 +95,18 @@ class CAESound unsigned short m_bForcedFront : 1; }; }; - unsigned short m_wIsUsed; // +88 - short unk4; // +90 = 1005 - short m_wCurrentPlayPosition; // +92 - short unk5; // +94 = 0 - float m_fFinalVolume; // +96 - float m_fFrequency; // +100 - short m_wPlayingState; // +104 - char unk6[2]; // +106 - float m_fSoundHeadRoom; // +108 - short m_wSoundLength; // +112 - short unk8; // +114 + short m_nLoopCounter; // +86 -1 = loop forever, >1 = replay N times, 0/1 = single play + short m_nMaxNumReplays; // +88 + short m_nIsUsed; // +90 1 while the sound pool slot is in use + short m_wCurrentPlayPosition; // +92 + short m_wLastPlayPosition; // +94 + float m_fFinalVolume; // +96 + float m_fFrequency; // +100 + short m_wPlayingState; // +104 + char unk6[2]; // +106 + float m_fSoundHeadRoom; // +108 + short m_wSoundLength; // +112 + short unk8; // +114 }; static_assert(sizeof(CAESound) == 0x74, "Invalid size for CAESound"); @@ -140,6 +139,7 @@ class CAudioEngineSA : public CAudioEngine void SetWorldSoundHandler(WorldSoundHandler* pHandler); void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2); void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical); + bool IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const; void UpdateAmbientSoundSettings(); bool OnWorldSound(CAESound* pAESound); From 70c939d40d6ef630661e1b4929852e9d2aff6414 Mon Sep 17 00:00:00 2001 From: Youssef Maged Date: Wed, 9 Sep 2026 02:50:52 +0300 Subject: [PATCH 37/37] Refactor world-sound replacement --- Client/game_sa/CAEAudioHardwareSA.cpp | 31 ++ Client/game_sa/CAEAudioHardwareSA.h | 2 + Client/game_sa/CAudioEngineSA.cpp | 9 + Client/game_sa/CAudioEngineSA.h | 1 + Client/mods/deathmatch/logic/CClientGame.cpp | 6 +- .../logic/CClientWorldSoundManager.cpp | 326 ++++++++++-------- .../logic/CClientWorldSoundManager.h | 41 +-- .../logic/luadefs/CLuaAudioDefs.cpp | 21 ++ .../deathmatch/logic/luadefs/CLuaAudioDefs.h | 2 + Client/sdk/game/CAEAudioHardware.h | 3 + Client/sdk/game/CAudioEngine.h | 3 + 11 files changed, 266 insertions(+), 179 deletions(-) diff --git a/Client/game_sa/CAEAudioHardwareSA.cpp b/Client/game_sa/CAEAudioHardwareSA.cpp index 2af781c49bd..a1d7eb03a4b 100644 --- a/Client/game_sa/CAEAudioHardwareSA.cpp +++ b/Client/game_sa/CAEAudioHardwareSA.cpp @@ -213,6 +213,19 @@ bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned return true; } +uint CAEAudioHardwareSA::GetNumSoundsInBankSlot(unsigned short usBankSlot) const +{ + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return 0; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds <= 0) + return 0; + + return static_cast(std::min(sNumSounds, NUM_MaxBankSounds)); +} + void CAEAudioHardwareSA::GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const { if (!pOutFactors || uiMax == 0) @@ -227,6 +240,24 @@ void CAEAudioHardwareSA::GetChannelFrequencyScalingFactors(float* pOutFactors, u pOutFactors[i] = pFactors[i]; } +bool CAEAudioHardwareSA::SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) +{ + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return false; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds < 0 || usIndex >= static_cast(sNumSounds) || usIndex >= NUM_MaxBankSounds) + return false; + + BYTE* pItem = const_cast(GetBankSlotItem(pBankSlot, usIndex)); + if (!pItem) + return false; + + *reinterpret_cast(pItem + NUM_BankSlotItemSampleFreqOffset) = usSampleRate; + return true; +} + bool CAEAudioHardwareSA::PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) { void* pPcmDataDst = nullptr; diff --git a/Client/game_sa/CAEAudioHardwareSA.h b/Client/game_sa/CAEAudioHardwareSA.h index 4e13e85b423..a9ea87e0116 100644 --- a/Client/game_sa/CAEAudioHardwareSA.h +++ b/Client/game_sa/CAEAudioHardwareSA.h @@ -31,7 +31,9 @@ class CAEAudioHardwareSA : public CAEAudioHardware bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const override; + uint GetNumSoundsInBankSlot(unsigned short usBankSlot) const override; bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) override; + bool SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) override; void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const override; private: diff --git a/Client/game_sa/CAudioEngineSA.cpp b/Client/game_sa/CAudioEngineSA.cpp index 6d006766dd2..27b58b0e3e1 100644 --- a/Client/game_sa/CAudioEngineSA.cpp +++ b/Client/game_sa/CAudioEngineSA.cpp @@ -562,6 +562,7 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) pAESound->m_vCurrPosn, pAESound->m_fSoundDistance, pAESound->usGroup == BANKSLOT_HORNS || pAESound->m_nLoopCounter < 0 || pAESound->m_nLoopCounter > 1, + pAESound, }; return m_pWorldSoundHandler(event); @@ -570,6 +571,14 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) return true; } +void CAudioEngineSA::SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance) +{ + if (!pAESound || fMaxDistance <= 0.0f) + return; + + pAESound->m_fSoundDistance = fMaxDistance; +} + bool CAudioEngineSA::IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const { if (uiGroup == BANKSLOT_HORNS) diff --git a/Client/game_sa/CAudioEngineSA.h b/Client/game_sa/CAudioEngineSA.h index 09fcec88158..8fdee32d028 100644 --- a/Client/game_sa/CAudioEngineSA.h +++ b/Client/game_sa/CAudioEngineSA.h @@ -137,6 +137,7 @@ class CAudioEngineSA : public CAudioEngine bool IsWorldSoundEnabled(uint uiGroup, uint uiIndex); void ResetWorldSounds(); void SetWorldSoundHandler(WorldSoundHandler* pHandler); + void SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance); void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2); void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical); bool IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const; diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 2f17322c3f8..b71da760605 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -6638,10 +6638,8 @@ bool CClientGame::WorldSoundHandler(const SWorldSoundEvent& event) bAllowPlay = pEntity->CallEvent("onClientWorldSound", Arguments, true); } - if (m_pWorldSoundManager && m_pWorldSoundManager->HandleWorldSound(event, bAllowPlay)) - { - return false; - } + if (m_pWorldSoundManager) + m_pWorldSoundManager->HandleWorldSound(event); return bAllowPlay; } diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp index 524e7cf5092..19c9441c918 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -11,14 +11,13 @@ #include "StdInc.h" #include "CClientWorldSoundManager.h" -#include "CClientEntity.h" +#include "CClientGame.h" #include "CClientManager.h" #include "CClientSoundManager.h" #include #include -#include -CClientWorldSoundManager::CClientWorldSoundManager(CClientManager* pManager) : m_pManager(pManager), m_uiLastPruneTick(0) +CClientWorldSoundManager::CClientWorldSoundManager(CClientManager* pManager) : m_pManager(pManager) { } @@ -58,7 +57,8 @@ bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SS replacement.strSound = strSound; replacement.fMinDistance = std::max(0.0f, fMinDistance); replacement.fMaxDistance = std::max(0.0f, fMaxDistance); - replacement.bNativeWanted = (uiIndex != static_cast(-1)); + replacement.bWholeGroup = (uiIndex == static_cast(-1)); + replacement.bNativeWanted = true; replacement.uiNativeLastTryTick = 0; const uint uiKey = MakeKey(uiGroup, uiIndex); @@ -66,14 +66,11 @@ bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SS if (iterExisting != m_Replacements.end()) { replacement.originalPcm = std::move(iterExisting->second.originalPcm); + replacement.pcmByRate = std::move(iterExisting->second.pcmByRate); } - m_Replacements[uiKey] = replacement; - - if (replacement.bNativeWanted) - { - m_Replacements[uiKey].bNativeApplied = TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); - } + m_Replacements[uiKey] = std::move(replacement); + m_Replacements[uiKey].bNativeApplied = TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); return true; } @@ -87,7 +84,7 @@ bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) { if ((iter->first >> 16) == uiGroup) { - RestoreSoundBuffer(iter->second, uiGroup, iter->first & 0xFFFF); + RestoreSoundBuffer(iter->second, uiGroup); iter = m_Replacements.erase(iter); bErased = true; } @@ -101,7 +98,7 @@ bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) if (iter == m_Replacements.end()) return false; - RestoreSoundBuffer(iter->second, uiGroup, uiIndex); + RestoreSoundBuffer(iter->second, uiGroup); m_Replacements.erase(iter); return true; } @@ -111,11 +108,7 @@ void CClientWorldSoundManager::RestoreAll() if (g_pGame) { for (auto& iter : m_Replacements) - { - const uint uiIndex = iter.first & 0xFFFF; - if (uiIndex != static_cast(-1)) - RestoreSoundBuffer(iter.second, iter.first >> 16, uiIndex); - } + RestoreSoundBuffer(iter.second, iter.first >> 16); } m_Replacements.clear(); } @@ -164,189 +157,230 @@ bool CClientWorldSoundManager::TryApplyNativeReplacement(SReplacement& replaceme if (!pAudioHardware) return false; - void* pPcmData = nullptr; - uint uiPcmSize = 0; - uint uiSampleRate = 0; - int iLoopStartOffset = -1; - if (!pAudioHardware->GetLoadedSoundInfo(static_cast(uiGroup), static_cast(uiIndex), pPcmData, uiPcmSize, uiSampleRate, iLoopStartOffset)) - return false; - - if (replacement.pcmData.empty()) + if (replacement.bWholeGroup) { - if (replacement.originalPcm.empty()) - replacement.originalPcm.assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); - - if (!m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiSampleRate, replacement.pcmData)) - return false; - - if (replacement.pcmData.size() > uiPcmSize) - return false; + const uint uiNumSounds = pAudioHardware->GetNumSoundsInBankSlot(static_cast(uiGroup)); + bool bAnyApplied = false; + for (uint i = 0; i < uiNumSounds; ++i) + { + if (PatchSoundBufferIndex(replacement, uiGroup, i)) + bAnyApplied = true; + } + return bAnyApplied; } - return pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.pcmData.data(), - static_cast(replacement.pcmData.size())); + return PatchSoundBufferIndex(replacement, uiGroup, uiIndex); } -bool CClientWorldSoundManager::RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup, uint uiIndex) +bool CClientWorldSoundManager::PatchSoundBufferIndex(SReplacement& replacement, uint uiGroup, uint uiIndex) { - if (!replacement.bNativeApplied || replacement.originalPcm.empty() || !g_pGame) + if (!g_pGame) return false; CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); if (!pAudioHardware) return false; - return pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.originalPcm.data(), - static_cast(replacement.originalPcm.size())); -} - -void CClientWorldSoundManager::ApplyNativeReplacements() -{ - const uint uiNow = GetTickCount32(); + void* pPcmData = nullptr; + uint uiPcmSize = 0; + uint uiCurrentRate = 0; + int iLoopStartOffset = -1; + if (!pAudioHardware->GetLoadedSoundInfo(static_cast(uiGroup), static_cast(uiIndex), pPcmData, uiPcmSize, uiCurrentRate, iLoopStartOffset)) + return false; - for (auto& iter : m_Replacements) + if (uiCurrentRate == 0) { - const uint uiIndex = iter.first & 0xFFFF; - if (uiIndex == static_cast(-1)) - continue; - - SReplacement& replacement = iter.second; - if (!replacement.bNativeWanted) - continue; - - if (!replacement.bNativeApplied) - { - if (uiNow - replacement.uiNativeLastTryTick < 500) - continue; - replacement.uiNativeLastTryTick = uiNow; - replacement.bNativeApplied = TryApplyNativeReplacement(replacement, iter.first >> 16, uiIndex); - } - else if (uiNow - replacement.uiNativeLastTryTick > 1000) - { - replacement.uiNativeLastTryTick = uiNow; - TryApplyNativeReplacement(replacement, iter.first >> 16, uiIndex); - } + LogResult(replacement, uiGroup, uiIndex, "invalid bank sample rate 0", false); + return false; } -} -bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event, bool bAllowPlay) -{ - const SReplacement* pReplacement = nullptr; - if (!FindReplacement(event.uiGroup, event.uiIndex, &pReplacement)) - return false; + const uint uiBaseRate = replacement.originalRate.contains(uiIndex) ? replacement.originalRate[uiIndex] : uiCurrentRate; - if (pReplacement->bNativeApplied) + std::vector& decoded = replacement.pcmByRate[uiBaseRate]; + if (decoded.empty() && !m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiBaseRate, decoded)) + { + LogResult(replacement, uiGroup, uiIndex, SString("failed to decode '%s'", *replacement.strSound), false); return false; + } - if (bAllowPlay) - { - const uint uiKey = MakeKey(event.uiGroup, event.uiIndex); + const uint uiSourceSize = static_cast(decoded.size()); + uint uiPatchRate = uiBaseRate; - bool bAlreadyPlayed = false; - auto iter = m_LastPlayed.find(uiKey); - if (iter != m_LastPlayed.end()) - { - const uint uiElapsed = GetTickCount32() - iter->second.uiTick; - const CVector vecDelta = event.vecPosition - iter->second.vecPosition; - if (uiElapsed < 40 && vecDelta.LengthSquared() < 2.25f) - bAlreadyPlayed = true; - } - - if (!bAlreadyPlayed) + if (uiSourceSize > uiPcmSize) + { + if (iLoopStartOffset >= 0) { - const bool bLoop = event.bLoop && event.uiGroup == BANKSLOT_HORNS && event.pGameEntity != nullptr && g_pGame && - g_pGame->GetPools()->GetVehicle((DWORD*)event.pGameEntity) != nullptr; - - CClientSound* pSound = - m_pManager->GetSoundManager()->PlaySound3D(pReplacement->strSound, false, pReplacement->bRawData, event.vecPosition, bLoop, true); - if (!pSound) + const uint uiRequiredRate = static_cast(uiBaseRate * (uiPcmSize * 0.98f) / uiSourceSize); + if (uiRequiredRate < 8000) { + LogResult( + replacement, uiGroup, uiIndex, + SString("replacement too long for looping sound: needs %u Hz to fit %u bytes into %u byte slot", uiRequiredRate, uiSourceSize, uiPcmSize), + false); return false; } - float fMaxDistance = pReplacement->fMaxDistance > 0.0f ? pReplacement->fMaxDistance : event.fMaxDistance; - if (fMaxDistance > 1.0f) + uiPatchRate = uiRequiredRate; + std::vector& fitted = replacement.pcmByRate[uiPatchRate]; + if (fitted.empty() && !m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiPatchRate, fitted)) + { + LogResult(replacement, uiGroup, uiIndex, SString("failed to decode '%s' at %u Hz", *replacement.strSound, uiPatchRate), false); + return false; + } + if (fitted.size() > uiPcmSize) { - float fMinDistance = pReplacement->fMinDistance > 0.0f ? pReplacement->fMinDistance : std::min(5.0f, fMaxDistance * 0.25f); - fMinDistance = std::max(0.1f, std::min(fMinDistance, fMaxDistance * 0.95f)); - pSound->SetMinDistance(fMinDistance); - pSound->SetMaxDistance(fMaxDistance); + LogResult(replacement, uiGroup, uiIndex, + SString("replacement too long for looping sound: still %u bytes at %u Hz", static_cast(fitted.size()), uiPatchRate), false); + return false; } + } + else + { + decoded.resize(uiPcmSize); - float fVolume = g_pCore->GetCVars()->GetValue("sfxvolume", 1.0f); - float fMtaVolume = 1.0f; - if (g_pCore->GetCVars()->Get("mtavolume", fMtaVolume) && fMtaVolume > 0.0f) - fVolume = std::min(4.0f, fVolume / fMtaVolume); - - if (g_pCore->IsWindowMinimized() && - (g_pCore->GetCVars()->GetValue("mute_master_when_minimized") || g_pCore->GetCVars()->GetValue("mute_sfx_when_minimized"))) + const uint uiTotalSamples = uiPcmSize / 2; + const uint uiFadeSamples = std::min(uiTotalSamples, std::max(1, uiBaseRate / 125)); + short* pSamples = reinterpret_cast(decoded.data()); + const uint uiFadeStart = uiTotalSamples - uiFadeSamples; + for (uint i = 0; i < uiFadeSamples; ++i) { - fVolume = 0.0f; + const float fGain = static_cast(uiFadeSamples - i) / static_cast(uiFadeSamples); + pSamples[uiFadeStart + i] = static_cast(static_cast(pSamples[uiFadeStart + i]) * fGain); } + } + } - pSound->SetVolume(fVolume); + if (!replacement.originalPcm.contains(uiIndex)) + { + replacement.originalPcm[uiIndex].assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); + replacement.originalRate[uiIndex] = static_cast(uiCurrentRate); + } - if (event.pGameEntity) - m_FollowSounds.push_back({pSound, event.pGameEntity, GetTickCount32(), event.uiGroup, event.uiIndex, bLoop}); + if (!pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.pcmByRate[uiPatchRate].data(), + static_cast(replacement.pcmByRate[uiPatchRate].size()))) + { + LogResult(replacement, uiGroup, uiIndex, "failed to patch bank slot", false); + return false; + } - m_LastPlayed[uiKey] = {GetTickCount32(), event.vecPosition}; - } + if (uiPatchRate != uiCurrentRate && + !pAudioHardware->SetSoundSampleRate(static_cast(uiGroup), static_cast(uiIndex), static_cast(uiPatchRate))) + { + LogResult(replacement, uiGroup, uiIndex, "failed to set bank sample rate", false); + return false; + } + + if (uiSourceSize > uiPcmSize) + { + if (iLoopStartOffset >= 0) + LogResult(replacement, uiGroup, uiIndex, + SString("looping sound rate-lowered to fit: %u bytes at %u Hz (source was %u bytes at %u Hz)", uiPcmSize, uiPatchRate, uiSourceSize, + uiBaseRate), + true); + else + LogResult(replacement, uiGroup, uiIndex, + SString("sound trimmed to fit: %u bytes at %u Hz (source was %u bytes)", uiPcmSize, uiBaseRate, uiSourceSize), true); } return true; } -void CClientWorldSoundManager::DoPulse() +bool CClientWorldSoundManager::RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup) { - const uint uiNow = GetTickCount32(); + if (replacement.originalPcm.empty() || !g_pGame) + return false; - for (auto iter = m_FollowSounds.begin(); iter != m_FollowSounds.end();) - { - SFollowSound& entry = *iter; + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; - if (!entry.bLooping) + bool bRestored = false; + for (const auto& entry : replacement.originalPcm) + { + const ushort usIndex = static_cast(entry.first); + if (pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), usIndex, entry.second.data(), static_cast(entry.second.size()))) { - if (entry.pSound->IsFinished() || uiNow - entry.uiStartTick > 10000) - { - iter = m_FollowSounds.erase(iter); - continue; - } + auto iterRate = replacement.originalRate.find(entry.first); + if (iterRate != replacement.originalRate.end()) + pAudioHardware->SetSoundSampleRate(static_cast(uiGroup), usIndex, iterRate->second); + bRestored = true; } + } + return bRestored; +} + +void CClientWorldSoundManager::LogResult(SReplacement& replacement, uint uiGroup, uint uiIndex, const SString& strResult, bool bWarning) +{ + if (replacement.bResultLogged) + return; + + replacement.bResultLogged = true; + if (g_pClientGame && g_pClientGame->GetScriptDebugging()) + { + if (bWarning) + g_pClientGame->GetScriptDebugging()->LogWarning(NULL, "WorldSound: %s (group %u index %u)", *strResult, uiGroup, uiIndex); else + g_pClientGame->GetScriptDebugging()->LogError(NULL, "WorldSound: %s (group %u index %u)", *strResult, uiGroup, uiIndex); + } +} + +void CClientWorldSoundManager::ApplyNativeReplacements() +{ + const uint uiNow = GetTickCount32(); + + for (auto& iter : m_Replacements) + { + SReplacement& replacement = iter.second; + if (!replacement.bNativeWanted) + continue; + + const uint uiStoredIndex = iter.first & 0xFFFF; + + if (!replacement.bNativeApplied) { - const bool bOriginalActive = - g_pGame && g_pGame->GetAudioEngine() && g_pGame->GetAudioEngine()->IsWorldSoundStillActive(entry.uiGroup, entry.uiIndex, entry.pGameEntity); - const bool bSoundFinished = entry.pSound->IsFinished(); - if (!bOriginalActive || bSoundFinished || uiNow - entry.uiStartTick > 60000) - { - if (!bSoundFinished) - g_pClientGame->GetElementDeleter()->Delete(entry.pSound); - iter = m_FollowSounds.erase(iter); + if (uiNow - replacement.uiNativeLastTryTick < 500) continue; - } + replacement.uiNativeLastTryTick = uiNow; + replacement.bNativeApplied = TryApplyNativeReplacement(replacement, iter.first >> 16, uiStoredIndex); } - - if (CClientEntity* pEntity = g_pGame->GetPools()->GetClientEntity((DWORD*)entry.pGameEntity)) + else if (uiNow - replacement.uiNativeLastTryTick > 2000) { - CVector vecPosition; - pEntity->GetPosition(vecPosition); - entry.pSound->SetPosition(vecPosition); + replacement.uiNativeLastTryTick = uiNow; + TryApplyNativeReplacement(replacement, iter.first >> 16, uiStoredIndex); } + } +} - ++iter; +bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event) +{ + SReplacement* pReplacement = nullptr; + auto iter = m_Replacements.find(MakeKey(event.uiGroup, event.uiIndex)); + if (iter != m_Replacements.end()) + pReplacement = &iter->second; + else + { + iter = m_Replacements.find(MakeKey(event.uiGroup, static_cast(-1))); + if (iter != m_Replacements.end()) + pReplacement = &iter->second; } - if (uiNow - m_uiLastPruneTick > 500) + if (!pReplacement) + return false; + + const uint uiNow = GetTickCount32(); + if (!pReplacement->bNativeApplied && uiNow - pReplacement->uiNativeLastTryTick >= 500) { - m_uiLastPruneTick = uiNow; - for (auto iter = m_LastPlayed.begin(); iter != m_LastPlayed.end();) - { - if (uiNow - iter->second.uiTick > 1000) - iter = m_LastPlayed.erase(iter); - else - ++iter; - } + pReplacement->uiNativeLastTryTick = uiNow; + pReplacement->bNativeApplied = TryApplyNativeReplacement(*pReplacement, event.uiGroup, event.uiIndex); } + if (pReplacement->fMaxDistance > 0.0f && event.pAESound && g_pGame && g_pGame->GetAudioEngine()) + g_pGame->GetAudioEngine()->SetWorldSoundMaxDistance(event.pAESound, pReplacement->fMaxDistance); + + return false; +} + +void CClientWorldSoundManager::DoPulse() +{ ApplyNativeReplacements(); } diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h index 08a51d64784..c965c04ecbc 100644 --- a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -11,13 +11,11 @@ #pragma once -#include #include #include #include class CClientManager; -class CClientSound; class CClientWorldSoundManager { @@ -31,8 +29,7 @@ class CClientWorldSoundManager void RestoreAll(); bool IsSoundReplaced(uint uiGroup, uint uiIndex) const; - - bool HandleWorldSound(const SWorldSoundEvent& event, bool bAllowPlay); + bool HandleWorldSound(const SWorldSoundEvent& event); void DoPulse(); @@ -44,27 +41,14 @@ class CClientWorldSoundManager float fMinDistance; float fMaxDistance; - bool bNativeWanted = false; - bool bNativeApplied = false; - std::vector pcmData; - std::vector originalPcm; - uint uiNativeLastTryTick = 0; - }; - - struct SLastPlayed - { - uint uiTick; - CVector vecPosition; - }; - - struct SFollowSound - { - CClientSound* pSound; - CEntitySAInterface* pGameEntity; - uint uiStartTick; - uint uiGroup; - uint uiIndex; - bool bLooping; + bool bWholeGroup = false; + bool bNativeWanted = false; + bool bNativeApplied = false; + bool bResultLogged = false; + std::unordered_map> pcmByRate; + std::unordered_map> originalPcm; + std::unordered_map originalRate; + uint uiNativeLastTryTick = 0; }; static uint MakeKey(uint uiGroup, uint uiIndex) { return (uiGroup << 16) | (uiIndex & 0xFFFF); } @@ -72,12 +56,11 @@ class CClientWorldSoundManager bool FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const; bool TryApplyNativeReplacement(SReplacement& replacement, uint uiGroup, uint uiIndex); - bool RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup, uint uiIndex); + bool PatchSoundBufferIndex(SReplacement& replacement, uint uiGroup, uint uiIndex); + bool RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup); void ApplyNativeReplacements(); + void LogResult(SReplacement& replacement, uint uiGroup, uint uiIndex, const SString& strResult, bool bWarning); CClientManager* m_pManager; std::unordered_map m_Replacements; - std::unordered_map m_LastPlayed; - std::list m_FollowSounds; - uint m_uiLastPruneTick; }; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp index 034bb809a1e..139a18036ad 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp @@ -13,6 +13,7 @@ #include #include "CBassAudio.h" #include "CClientWorldSoundManager.h" +#include void CLuaAudioDefs::LoadFunctions() { @@ -28,6 +29,7 @@ void CLuaAudioDefs::LoadFunctions() {"restoreWorldSound", ArgumentParser}, {"restoreAllWorldSounds", ArgumentParser}, {"isWorldSoundReplaced", ArgumentParser}, + {"getWorldSoundBankSlotInfo", ArgumentParser}, {"playSFX", PlaySFX}, {"playSFX3D", PlaySFX3D}, {"getSFXStatus", GetSFXStatus}, @@ -199,6 +201,25 @@ bool CLuaAudioDefs::IsWorldSoundReplaced(int group, std::optional index) return false; } +std::variant> CLuaAudioDefs::GetWorldSoundBankSlotInfo(int group, int index) +{ + if (!g_pGame || group < 0 || group > 44 || index < 0 || index > 399) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + void* pPcmData = nullptr; + uint uiPcmSize = 0; + uint uiSampleRate = 0; + int iLoopStartOffset = -1; + if (!pAudioHardware->GetLoadedSoundInfo(static_cast(group), static_cast(index), pPcmData, uiPcmSize, uiSampleRate, iLoopStartOffset)) + return false; + + return std::tuple(uiPcmSize, uiSampleRate); +} + int CLuaAudioDefs::PlaySound(lua_State* luaVM) { SString strSound = ""; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h index cc8cf1f9b54..400dd9e68f2 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h @@ -11,6 +11,7 @@ #pragma once #include "CLuaDefs.h" +#include class CLuaAudioDefs : public CLuaDefs { @@ -32,6 +33,7 @@ class CLuaAudioDefs : public CLuaDefs static bool RestoreWorldSound(int group, std::optional index); static bool RestoreAllWorldSounds(); static bool IsWorldSoundReplaced(int group, std::optional index); + static std::variant> GetWorldSoundBankSlotInfo(int group, int index); LUA_DECLARE(PlaySFX); LUA_DECLARE(PlaySFX3D); diff --git a/Client/sdk/game/CAEAudioHardware.h b/Client/sdk/game/CAEAudioHardware.h index ca004602287..1b085e32f80 100644 --- a/Client/sdk/game/CAEAudioHardware.h +++ b/Client/sdk/game/CAEAudioHardware.h @@ -69,6 +69,9 @@ class CAEAudioHardware virtual bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const = 0; + virtual uint GetNumSoundsInBankSlot(unsigned short usBankSlot) const = 0; + virtual bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) = 0; + virtual bool SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) = 0; virtual void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const = 0; }; diff --git a/Client/sdk/game/CAudioEngine.h b/Client/sdk/game/CAudioEngine.h index 34701c43fca..f0e9bf50a72 100644 --- a/Client/sdk/game/CAudioEngine.h +++ b/Client/sdk/game/CAudioEngine.h @@ -19,6 +19,7 @@ class CEntity; class CEntitySAInterface; class CPhysical; class CVector; +class CAESound; struct SWorldSoundEvent { @@ -28,6 +29,7 @@ struct SWorldSoundEvent CVector vecPosition; float fMaxDistance; bool bLoop; + CAESound* pAESound; }; using WorldSoundHandler = bool(const SWorldSoundEvent& event); @@ -76,6 +78,7 @@ class CAudioEngine virtual bool IsWorldSoundEnabled(uint uiGroup, uint uiIndex) = 0; virtual void ResetWorldSounds() = 0; virtual void SetWorldSoundHandler(WorldSoundHandler* pHandler) = 0; + virtual void SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance) = 0; virtual void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2) = 0; virtual void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical) = 0;