From 900e4167db46e5e32bf664ea14fc5ce735c126ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Krupi=C5=84ski?= Date: Tue, 31 Oct 2023 19:53:33 +0100 Subject: [PATCH] Implement bomb plant sound visualization --- Source/CS2/Constants/SoundNames.h | 2 + Source/Endpoints.h | 2 + .../FeatureHelpers/{ => Sound}/PlayedSound.h | 0 .../{PlayedSounds.h => Sound/SoundWatcher.h} | 77 ++++++-- .../FeatureHelpers/Sound/WatchedSoundType.h | 3 +- Source/FeatureHelpers/Sound/WatchedSounds.h | 2 +- Source/Features/Features.h | 4 +- Source/Features/Sound/BombPlantVisualizer.h | 172 ++++++++++++++++++ Source/Features/Sound/FootstepVisualizer.h | 19 +- Source/Features/Sound/SoundFeatures.h | 7 +- Source/GlobalContext.h | 6 +- Source/Osiris.vcxproj | 5 +- Source/Osiris.vcxproj.filters | 15 +- Source/UI/Panorama/CreateGUI.js | 2 + Source/UI/Panorama/SetCommandHandler.h | 4 +- 15 files changed, 276 insertions(+), 44 deletions(-) rename Source/FeatureHelpers/{ => Sound}/PlayedSound.h (100%) rename Source/FeatureHelpers/{PlayedSounds.h => Sound/SoundWatcher.h} (53%) create mode 100644 Source/Features/Sound/BombPlantVisualizer.h diff --git a/Source/CS2/Constants/SoundNames.h b/Source/CS2/Constants/SoundNames.h index 70893987c9e..1d1c0663d09 100644 --- a/Source/CS2/Constants/SoundNames.h +++ b/Source/CS2/Constants/SoundNames.h @@ -9,8 +9,10 @@ namespace cs2 #if IS_WIN64() constexpr std::string_view kPlayerFootstepSoundsPath = "sounds\\player\\footsteps\\"; +constexpr std::string_view kBombPlantSoundPath = "sounds\\weapons\\c4\\c4_initiate.vsnd"; #elif IS_LINUX() constexpr std::string_view kPlayerFootstepSoundsPath = "sounds/player/footsteps/"; +constexpr std::string_view kBombPlantSoundPath = "sounds/weapons/c4/c4_initiate.vsnd"; #endif constexpr std::string_view kPlayerSuitSoundPrefix = "suit"; diff --git a/Source/Endpoints.h b/Source/Endpoints.h index 8a254837443..7075649247f 100644 --- a/Source/Endpoints.h +++ b/Source/Endpoints.h @@ -54,5 +54,7 @@ inline void* LoopModeGameHook::getWorldSession(cs2::CLoopModeGame* thisptr) noex inline void ViewRenderHook::onRenderStart(cs2::CViewRender* thisptr) noexcept { GlobalContext::instance().hooks->viewRenderHook.originalOnRenderStart(thisptr); + GlobalContext::instance().soundWatcher->update(); features().soundFeatures.footstepVisualizer.run(); + features().soundFeatures.bombPlantVisualizer.run(); } diff --git a/Source/FeatureHelpers/PlayedSound.h b/Source/FeatureHelpers/Sound/PlayedSound.h similarity index 100% rename from Source/FeatureHelpers/PlayedSound.h rename to Source/FeatureHelpers/Sound/PlayedSound.h diff --git a/Source/FeatureHelpers/PlayedSounds.h b/Source/FeatureHelpers/Sound/SoundWatcher.h similarity index 53% rename from Source/FeatureHelpers/PlayedSounds.h rename to Source/FeatureHelpers/Sound/SoundWatcher.h index e562626b584..0365a4aadf1 100644 --- a/Source/FeatureHelpers/PlayedSounds.h +++ b/Source/FeatureHelpers/Sound/SoundWatcher.h @@ -11,14 +11,20 @@ #include #include #include "PlayedSound.h" -#include "Sound/WatchedSounds.h" -#include "Sound/WatchedSoundType.h" +#include "WatchedSounds.h" +#include "WatchedSoundType.h" #include #include -class PlayedSounds { +class SoundWatcher { public: static constexpr auto kFootstepLifespan = 2.0f; + static constexpr auto kBombPlantLifespan = 2.5f; + + explicit SoundWatcher(GlobalVarsProvider globalVarsProvider) noexcept + : globalVarsProvider{ globalVarsProvider } + { + } template void startWatching() noexcept @@ -34,26 +40,49 @@ class PlayedSounds { watchedSounds.unset(); } - void update(float curtime) noexcept + void update() noexcept { + if (!globalVarsProvider || !globalVarsProvider.getGlobalVars()) + return; + if (watchedSounds.has()) { - getSounds().removeExpiredSounds(curtime, kFootstepLifespan); - collectSounds(curtime); + getSounds().removeExpiredSounds(globalVarsProvider.getGlobalVars()->curtime, kFootstepLifespan); + } + + if (watchedSounds.has()) { + getSounds().removeExpiredSounds(globalVarsProvider.getGlobalVars()->curtime, kBombPlantLifespan); } + + if (watchedSounds) + collectSounds(globalVarsProvider.getGlobalVars()->curtime); } - template + template void forEach(F&& f) const noexcept { - footsteps.forEach(std::forward(f)); + getSounds().forEach(std::forward(f)); } private: + template + [[nodiscard]] const WatchedSounds& getSounds() const noexcept + { + if constexpr (soundType == WatchedSoundType::Footsteps) { + return footsteps; + } else if constexpr (soundType == WatchedSoundType::BombPlant) { + return bombPlants; + } else { + static_assert(soundType != soundType, "Unknown sound type!"); + } + } + template [[nodiscard]] WatchedSounds& getSounds() noexcept { if constexpr (soundType == WatchedSoundType::Footsteps) { return footsteps; + } else if constexpr (soundType == WatchedSoundType::BombPlant) { + return bombPlants; } else { static_assert(soundType != soundType, "Unknown sound type!"); } @@ -61,9 +90,6 @@ class PlayedSounds { void collectSounds(float curtime) noexcept { - if (!watchedSounds) - return; - if (!soundChannels || !*soundChannels) return; @@ -86,16 +112,24 @@ class PlayedSounds { fileNames.getString(channel.sfx->fileNameHandle, buffer); buffer.back() = '\0'; - if (watchedSounds.has()) { - if (!isFootstepSound(std::string_view{buffer.data()})) - continue; + if (const auto sounds = getSoundsToAddTo(std::string_view{buffer.data()}, channel.guid)) + sounds->addSound(PlayedSound{ .guid = channel.guid, .spawnTime = curtime, .origin = channelInfo2.memory[i].origin }); + } + } - if (footsteps.hasSound(channel.guid)) - continue; + [[nodiscard]] WatchedSounds* getSoundsToAddTo(std::string_view soundName, int guid) noexcept + { + if (watchedSounds.has()) { + if (isFootstepSound(soundName) && !footsteps.hasSound(guid)) + return &footsteps; + } - footsteps.addSound(PlayedSound{ .guid = channel.guid, .spawnTime = curtime, .origin = channelInfo2.memory[i].origin }); - } + if (watchedSounds.has()) { + if (isBombPlantSound(soundName) && !bombPlants.hasSound(guid)) + return &bombPlants; } + + return nullptr; } [[nodiscard]] static bool isFootstepSound(std::string_view soundName) noexcept @@ -106,9 +140,16 @@ class PlayedSounds { return false; } + [[nodiscard]] static bool isBombPlantSound(std::string_view soundName) noexcept + { + return soundName == cs2::kBombPlantSoundPath; + } + cs2::SoundChannels** soundChannels{ SoundSystemPatterns::soundChannels() }; cs2::CBaseFileSystem** fileSystem{ FileSystemPatterns::fileSystem() }; + GlobalVarsProvider globalVarsProvider; BitFlags watchedSounds; WatchedSounds footsteps; + WatchedSounds bombPlants; }; diff --git a/Source/FeatureHelpers/Sound/WatchedSoundType.h b/Source/FeatureHelpers/Sound/WatchedSoundType.h index 9b774169997..af5f6002412 100644 --- a/Source/FeatureHelpers/Sound/WatchedSoundType.h +++ b/Source/FeatureHelpers/Sound/WatchedSoundType.h @@ -1,5 +1,6 @@ #pragma once enum class WatchedSoundType { - Footsteps + Footsteps, + BombPlant }; diff --git a/Source/FeatureHelpers/Sound/WatchedSounds.h b/Source/FeatureHelpers/Sound/WatchedSounds.h index 2f6962f37d2..45cf64e1afc 100644 --- a/Source/FeatureHelpers/Sound/WatchedSounds.h +++ b/Source/FeatureHelpers/Sound/WatchedSounds.h @@ -2,7 +2,7 @@ #include -#include +#include "PlayedSound.h" #include class WatchedSounds { diff --git a/Source/Features/Features.h b/Source/Features/Features.h index 61f730a150b..78ce232950f 100644 --- a/Source/Features/Features.h +++ b/Source/Features/Features.h @@ -7,10 +7,10 @@ #include "Visuals/VisualFeatures.h" struct Features { - Features(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, LoopModeGameHook& loopModeGameHook, ViewRenderHook& viewRenderHook) noexcept + Features(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, LoopModeGameHook& loopModeGameHook, ViewRenderHook& viewRenderHook, SoundWatcher& soundWatcher) noexcept : hudFeatures{ PlantedC4Provider{}, hudProvider, globalVarsProvider } , visuals{ hudProvider, loopModeGameHook } - , soundFeatures{ hudProvider, globalVarsProvider, viewRenderHook } + , soundFeatures{ hudProvider, globalVarsProvider, viewRenderHook, soundWatcher } { } diff --git a/Source/Features/Sound/BombPlantVisualizer.h b/Source/Features/Sound/BombPlantVisualizer.h new file mode 100644 index 00000000000..f1525371a74 --- /dev/null +++ b/Source/Features/Sound/BombPlantVisualizer.h @@ -0,0 +1,172 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class BombPlantVisualizer : public TogglableFeature { +public: + explicit BombPlantVisualizer(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, ViewRenderHook& viewRenderHook, SoundWatcher& soundWatcher) noexcept + : hudProvider{ hudProvider } + , globalVarsProvider{ globalVarsProvider } + , viewRenderHook{ viewRenderHook } + , soundWatcher{ soundWatcher } + { + } + + ~BombPlantVisualizer() noexcept + { + if (bombPlantContainerPanelPointer.getHandle().isValid()) + PanoramaUiEngine::onDeletePanel(bombPlantContainerPanelPointer.getHandle()); + } + + void run() noexcept + { + if (!isEnabled()) + return; + + if (!globalVarsProvider || !globalVarsProvider.getGlobalVars()) + return; + + if (!worldtoClipSpaceConverter) + return; + + createBombPlantPanels(); + + std::size_t currentIndex = 0; + soundWatcher.forEach([this, ¤tIndex] (const PlayedSound& sound) { + const auto soundInClipSpace = worldtoClipSpaceConverter.toClipSpace(sound.origin); + if (!soundInClipSpace.onScreen()) + return; + + const auto deviceCoordinates = soundInClipSpace.toNormalizedDeviceCoordinates(); + const auto panel = getBombPlantPanel(currentIndex); + if (!panel) + return; + + const auto style = panel.getStyle(); + if (!style) + return; + + const auto timeAlive = sound.getTimeAlive(globalVarsProvider.getGlobalVars()->curtime); + if (timeAlive >= SoundWatcher::kBombPlantLifespan - kBombPlantFadeAwayDuration) { + style.setOpacity((SoundWatcher::kBombPlantLifespan - timeAlive) / kBombPlantFadeAwayDuration); + } else { + style.setOpacity(1.0f); + } + + cs2::CTransform3D* transformations[]{ transformFactory.create( + (std::max)(1.0f - soundInClipSpace.z / 1000.0f, 0.4f), (std::max)(1.0f - soundInClipSpace.z / 1000.0f, 0.4f), 1.0f + ), transformFactory.create( + deviceCoordinates.getX(), + deviceCoordinates.getY(), + cs2::CUILength{ 0.0f, cs2::CUILength::k_EUILengthLength } + ) }; + + cs2::CUtlVector dummyVector; + dummyVector.allocationCount = 2; + dummyVector.memory = transformations; + dummyVector.growSize = 0; + dummyVector.size = 2; + + style.setTransform3D(dummyVector); + ++currentIndex; + }); + + hidePanels(currentIndex); + } + +private: + friend TogglableFeature; + + void onEnable() noexcept + { + viewRenderHook.incrementReferenceCount(); + soundWatcher.startWatching(); + } + + void onDisable() noexcept + { + viewRenderHook.decrementReferenceCount(); + soundWatcher.stopWatching(); + hidePanels(0); + } + + void createBombPlantPanels() noexcept + { + if (bombPlantContainerPanelPointer.get()) + return; + + const auto hudReticle = hudProvider.findChildInLayoutFile(cs2::HudReticle); + if (!hudReticle) + return; + + PanoramaUiEngine::runScript(hudReticle, + R"( +$.CreatePanel('Panel', $.GetContextPanel(), 'BombPlantContainer', { + style: 'width: 100%; height: 100%;' +});)", "", 0); + + bombPlantContainerPanelPointer = hudReticle.findChildInLayoutFile("BombPlantContainer"); + + for (std::size_t i = 0; i < kMaxNumberOfBombPlantsToDraw; ++i) { + PanoramaUiEngine::runScript(hudReticle, + R"( +(function() { +var bombPlantPanel = $.CreatePanel('Panel', $.GetContextPanel().FindChildInLayoutFile("BombPlantContainer"), '', { + style: 'width: 100px; height: 100px; x: -50px; y: -50px;' +}); + +$.CreatePanel('Image', bombPlantPanel, '', { + src: "s2r://panorama/images/icons/ui/chatwheel_bombat.svg", + style: "horizontal-align: center; vertical-align: center; img-shadow: 0px 0px 1px 3 #000000;", + textureheight: "64" +}); +})();)", "", 0); + } + } + + [[nodiscard]] PanoramaUiPanel getBombPlantPanel(std::size_t index) const noexcept + { + const auto bombPlantContainerPanel = bombPlantContainerPanelPointer.get(); + if (!bombPlantContainerPanel) + return PanoramaUiPanel{nullptr}; + + if (const auto children = bombPlantContainerPanel.children()) { + if (children->size > 0 && static_cast(children->size) > index) + return PanoramaUiPanel{ children->memory[index] }; + } + return PanoramaUiPanel{nullptr}; + } + + void hidePanels(std::size_t fromPanelIndex) const noexcept + { + for (std::size_t i = fromPanelIndex; i < kMaxNumberOfBombPlantsToDraw; ++i) { + const auto panel = getBombPlantPanel(i); + if (!panel) + break; + + if (const auto style = panel.getStyle()) + style.setOpacity(0.0f); + } + } + + HudProvider hudProvider; + PanoramaPanelPointer bombPlantContainerPanelPointer; + GlobalVarsProvider globalVarsProvider; + PanoramaTransformFactory transformFactory; + ViewRenderHook& viewRenderHook; + SoundWatcher& soundWatcher; + WorldToClipSpaceConverter worldtoClipSpaceConverter; + + static constexpr auto kMaxNumberOfBombPlantsToDraw = 5; + static constexpr auto kBombPlantFadeAwayDuration = 0.4f; +}; diff --git a/Source/Features/Sound/FootstepVisualizer.h b/Source/Features/Sound/FootstepVisualizer.h index cfa6802e5b5..ffa7679e31a 100644 --- a/Source/Features/Sound/FootstepVisualizer.h +++ b/Source/Features/Sound/FootstepVisualizer.h @@ -5,8 +5,7 @@ #include #include -#include -#include +#include #include #include #include @@ -16,10 +15,11 @@ class FootstepVisualizer : public TogglableFeature { public: - explicit FootstepVisualizer(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, ViewRenderHook& viewRenderHook) noexcept + explicit FootstepVisualizer(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, ViewRenderHook& viewRenderHook, SoundWatcher& soundWatcher) noexcept : hudProvider{ hudProvider } , globalVarsProvider{ globalVarsProvider } , viewRenderHook{ viewRenderHook } + , soundWatcher{ soundWatcher } { } @@ -41,10 +41,9 @@ class FootstepVisualizer : public TogglableFeature { return; createFootstepPanels(); - footsteps.update(globalVarsProvider.getGlobalVars()->curtime); std::size_t currentIndex = 0; - footsteps.forEach([this, ¤tIndex] (const PlayedSound& sound) { + soundWatcher.forEach([this, ¤tIndex] (const PlayedSound& sound) { const auto soundInClipSpace = worldtoClipSpaceConverter.toClipSpace(sound.origin); if (!soundInClipSpace.onScreen()) return; @@ -59,8 +58,8 @@ class FootstepVisualizer : public TogglableFeature { return; const auto timeAlive = sound.getTimeAlive(globalVarsProvider.getGlobalVars()->curtime); - if (timeAlive >= PlayedSounds::kFootstepLifespan - kFootstepFadeAwayDuration) { - style.setOpacity((PlayedSounds::kFootstepLifespan - timeAlive) / kFootstepFadeAwayDuration); + if (timeAlive >= SoundWatcher::kFootstepLifespan - kFootstepFadeAwayDuration) { + style.setOpacity((SoundWatcher::kFootstepLifespan - timeAlive) / kFootstepFadeAwayDuration); } else { style.setOpacity(1.0f); } @@ -92,13 +91,13 @@ class FootstepVisualizer : public TogglableFeature { void onEnable() noexcept { viewRenderHook.incrementReferenceCount(); - footsteps.startWatching(); + soundWatcher.startWatching(); } void onDisable() noexcept { viewRenderHook.decrementReferenceCount(); - footsteps.stopWatching(); + soundWatcher.stopWatching(); hidePanels(0); } @@ -167,7 +166,7 @@ var footstepPanel = $.CreatePanel('Panel', $.GetContextPanel().FindChildInLayout ViewRenderHook& viewRenderHook; WorldToClipSpaceConverter worldtoClipSpaceConverter; - PlayedSounds footsteps; + SoundWatcher& soundWatcher; static constexpr auto kMaxNumberOfFootstepsToDraw = 100; static constexpr auto kFootstepFadeAwayDuration = 0.4f; diff --git a/Source/Features/Sound/SoundFeatures.h b/Source/Features/Sound/SoundFeatures.h index aef7461b42d..ab6cd9f574e 100644 --- a/Source/Features/Sound/SoundFeatures.h +++ b/Source/Features/Sound/SoundFeatures.h @@ -1,14 +1,17 @@ #pragma once +#include "BombPlantVisualizer.h" #include "FootstepVisualizer.h" #include #include struct SoundFeatures { - SoundFeatures(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, ViewRenderHook& viewRenderHook) noexcept - : footstepVisualizer{ hudProvider, globalVarsProvider, viewRenderHook } + SoundFeatures(HudProvider hudProvider, GlobalVarsProvider globalVarsProvider, ViewRenderHook& viewRenderHook, SoundWatcher& soundWatcher) noexcept + : footstepVisualizer{ hudProvider, globalVarsProvider, viewRenderHook, soundWatcher } + , bombPlantVisualizer{ hudProvider, globalVarsProvider, viewRenderHook, soundWatcher } { } FootstepVisualizer footstepVisualizer; + BombPlantVisualizer bombPlantVisualizer; }; diff --git a/Source/GlobalContext.h b/Source/GlobalContext.h index 260a040297f..9fd2f4e670a 100644 --- a/Source/GlobalContext.h +++ b/Source/GlobalContext.h @@ -9,6 +9,7 @@ #include #include +#include #include #include "Platform/DynamicLibrary.h" @@ -28,6 +29,7 @@ struct GlobalContext { PatternFinder fileSystemPatternFinder{ DynamicLibrary{cs2::FILESYSTEM_DLL}.getCodeSection().raw(), PatternNotFoundLogger{} }; LazyInitialized gameClasses; LazyInitialized hooks; + LazyInitialized soundWatcher; LazyInitialized features; LazyInitialized panoramaGUI; @@ -57,7 +59,9 @@ struct GlobalContext { gameClasses.init(Tier0Dll{}); const VmtLengthCalculator clientVmtLengthCalculator{ DynamicLibrary{cs2::CLIENT_DLL}.getCodeSection(), DynamicLibrary{cs2::CLIENT_DLL}.getVmtSection() }; hooks.init(clientVmtLengthCalculator); - features.init(HudProvider{}, GlobalVarsProvider{}, hooks->loopModeGameHook, hooks->viewRenderHook); + GlobalVarsProvider globalVarsProvider; + soundWatcher.init(globalVarsProvider); + features.init(HudProvider{}, globalVarsProvider, hooks->loopModeGameHook, hooks->viewRenderHook, *soundWatcher); panoramaGUI.init(*features, unloadFlag); initializedFromGameThread = true; diff --git a/Source/Osiris.vcxproj b/Source/Osiris.vcxproj index c05242fab4b..1e3bc788b67 100644 --- a/Source/Osiris.vcxproj +++ b/Source/Osiris.vcxproj @@ -36,8 +36,8 @@ - - + + @@ -47,6 +47,7 @@ + diff --git a/Source/Osiris.vcxproj.filters b/Source/Osiris.vcxproj.filters index 769de03a37a..e3e25a243f0 100644 --- a/Source/Osiris.vcxproj.filters +++ b/Source/Osiris.vcxproj.filters @@ -597,9 +597,6 @@ FeatureHelpers - - FeatureHelpers - CS2\Constants @@ -609,9 +606,6 @@ GameClasses - - FeatureHelpers - Features\Sound @@ -636,6 +630,15 @@ FeatureHelpers\Sound + + Features\Sound + + + FeatureHelpers\Sound + + + FeatureHelpers\Sound + diff --git a/Source/UI/Panorama/CreateGUI.js b/Source/UI/Panorama/CreateGUI.js index 6bd274123a8..c426e888a44 100644 --- a/Source/UI/Panorama/CreateGUI.js +++ b/Source/UI/Panorama/CreateGUI.js @@ -202,6 +202,8 @@ $.Osiris = (function () { var sound = createTab('sound'); var visualization = createSection(sound, 'Visualization'); createYesNoDropDown(visualization, "Visualize Player Footsteps", 'sound', 'visualize_player_footsteps'); + $.CreatePanel('Panel', visualization, '', { class: "horizontal-separator" }); + createYesNoDropDown(visualization, "Visualize Bomb Plant", 'sound', 'visualize_bomb_plant'); $.Osiris.navigateToTab('hud'); })(); diff --git a/Source/UI/Panorama/SetCommandHandler.h b/Source/UI/Panorama/SetCommandHandler.h index 37c2331b3fa..ddabfb33c2e 100644 --- a/Source/UI/Panorama/SetCommandHandler.h +++ b/Source/UI/Panorama/SetCommandHandler.h @@ -37,7 +37,9 @@ struct SetCommandHandler { { if (const auto feature = parser.getLine('/'); feature == "visualize_player_footsteps") { handleTogglableFeature(features.soundFeatures.footstepVisualizer); - } + } else if (feature == "visualize_bomb_plant") { + handleTogglableFeature(features.soundFeatures.bombPlantVisualizer); + } } void handleVisualsSection() const noexcept