From 593585f08147f63cc2720f1658ba89490a84887a Mon Sep 17 00:00:00 2001 From: "dami.sirbu" Date: Sat, 22 Aug 2026 14:45:49 +0300 Subject: [PATCH 1/2] feat: add Lua trace/veto hooks for the ambient bed and level music Only HUD sounds had a Lua seam (COnBeforePlayHudSound). Mods driving atmosphere could neither trace nor suppress the engine's own ambient plays. Add a named _G callback at each site, mirroring COnBeforePlayHudSound: the engine calls it only when the global is set (zero cost when absent) and applies { volume_mult } from the return (0 vetoes/silences, <1 attenuates, nil = vanilla). Neutral lever, no engine-side condition. - COnAmbientBed: System A per-weather background bed (WeathersUpdate); skip on veto, set_volume on attenuate. - COnLevelMusic: level music track (SMusicTrack::Play); volume-only. Manifest entries added to lua_help_ex.script. --- gamedata/scripts/lua_help_ex.script | 6 ++++++ src/xrGame/GamePersistent.cpp | 29 ++++++++++++++++++++++++++++- src/xrGame/level_sounds.cpp | 25 ++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/gamedata/scripts/lua_help_ex.script b/gamedata/scripts/lua_help_ex.script index 97d0205257..01a74eac10 100644 --- a/gamedata/scripts/lua_help_ex.script +++ b/gamedata/scripts/lua_help_ex.script @@ -155,6 +155,12 @@ ClrSetB(number argb, number b) } + engine callbacks (Lua defines the _G function; the engine calls it only if present) { + -- Each returns { volume_mult = number } to scale the play (0 vetoes/silences, <1 attenuates), or nil for vanilla. Zero cost when undefined. Mirror _G.COnBeforePlayHudSound. + table COnAmbientBed(string section, string file, Fvector pos) -- System A per-weather background bed play site (CGamePersistent::WeathersUpdate, GamePersistent.cpp). 0 vetoes the play. + table COnLevelMusic(string file) -- level music track in SMusicTrack::Play (level_sounds.cpp). Volume-only (0 silences the track). + } + get_console() { table get_variable_bounds(cmd) } diff --git a/src/xrGame/GamePersistent.cpp b/src/xrGame/GamePersistent.cpp index 19a8afb755..a96bfbd3fa 100644 --- a/src/xrGame/GamePersistent.cpp +++ b/src/xrGame/GamePersistent.cpp @@ -276,6 +276,18 @@ void CGamePersistent::OnGameEnd() xr_delete(g_stalker_velocity_holder); } +// { volume_mult = number } from a sound hook's return; 1.0 (vanilla) when nil, not a table, or not a number +static float ambient_hook_volume_mult(const ::luabind::object& output) +{ + if (output && output.type() == LUA_TTABLE) + { + auto volume_mult_obj = output["volume_mult"]; + if (volume_mult_obj.type() == LUA_TNUMBER) + return ::luabind::object_cast(volume_mult_obj); + } + return 1.0f; +} + void CGamePersistent::WeathersUpdate() { if (g_pGameLevel && !g_dedicated_server) @@ -315,7 +327,22 @@ void CGamePersistent::WeathersUpdate() pos.z = _sin(angle); pos.normalize().mul(ch.get_rnd_sound_dist()).add(Device.vCameraPosition); pos.y += 10.f; - snd.play_at_pos(0, pos); + + // Ambient bed hook, mirrors COnBeforePlayHudSound: Lua may veto (0) or attenuate; absent global = exact vanilla + float bed_volume_mult = 1.0f; + ::luabind::functor<::luabind::object> bed_funct; + if (ai().script_engine().functor("_G.COnAmbientBed", bed_funct)) + { + LPCSTR bed_file = snd._handle() ? snd._handle()->file_name() : ""; + bed_volume_mult = ambient_hook_volume_mult(bed_funct(ch.m_load_section.c_str(), bed_file, pos)); + } + + if (bed_volume_mult > EPS_S) + { + snd.play_at_pos(0, pos); + if (bed_volume_mult < 1.0f) + snd.set_volume(bed_volume_mult); + } #ifdef DEBUG if (!snd._handle() && strstr(Core.Params, "-nosound")) diff --git a/src/xrGame/level_sounds.cpp b/src/xrGame/level_sounds.cpp index 31baef11f7..91cb8841cc 100644 --- a/src/xrGame/level_sounds.cpp +++ b/src/xrGame/level_sounds.cpp @@ -5,6 +5,20 @@ #include "level.h" #include "level_sounds.h" +#include "ai_space.h" +#include "../xrServerEntities/script_engine.h" + +// { volume_mult = number } from a sound hook's return; 1.0 (vanilla) when nil, not a table, or not a number +static float ambient_hook_volume_mult(const ::luabind::object& output) +{ + if (output && output.type() == LUA_TTABLE) + { + auto volume_mult_obj = output["volume_mult"]; + if (volume_mult_obj.type() == LUA_TNUMBER) + return ::luabind::object_cast(volume_mult_obj); + } + return 1.0f; +} //----------------------------------------------------------------------------- // static level sounds @@ -129,7 +143,16 @@ BOOL SMusicTrack::in(u32 game_time) void SMusicTrack::Play() { m_SourceStereo.play_at_pos(0, Fvector().set(0.0f, 0.0f, 0.0f), sm_Intro); - SetVolume(1.0f); + + // Level-music hook: volume-only (a skip would make the manager re-select every frame); absent global = vanilla + float music_volume_mult = 1.0f; + ::luabind::functor<::luabind::object> music_funct; + if (ai().script_engine().functor("_G.COnLevelMusic", music_funct)) + { + LPCSTR music_file = m_SourceStereo._handle() ? m_SourceStereo._handle()->file_name() : ""; + music_volume_mult = ambient_hook_volume_mult(music_funct(music_file)); + } + SetVolume(music_volume_mult); } BOOL SMusicTrack::IsPlaying() From 182a0997f21ebb0634428f3255771be530d85d2d Mon Sep 17 00:00:00 2001 From: "dami.sirbu" Date: Wed, 9 Sep 2026 04:02:09 +0300 Subject: [PATCH 2/2] feat(callbacks): register ambient bed and level music sound hooks Add the _G.C* relay plus AddScriptCallback for on_before_play_ambient_bed and on_before_play_level_music, so a third party subscribes via RegisterScriptCallback and scales or vetoes through the result table. Mirrors COnBeforePlayHudSound. Drop the lua_help_ex engine-callbacks block: a script callback is documented at its registration, not in the bind manifest. --- gamedata/scripts/callbacks_gameobject.script | 37 ++++++++++++++++++++ gamedata/scripts/lua_help_ex.script | 6 ---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/gamedata/scripts/callbacks_gameobject.script b/gamedata/scripts/callbacks_gameobject.script index fafd64c23d..86d10ab495 100644 --- a/gamedata/scripts/callbacks_gameobject.script +++ b/gamedata/scripts/callbacks_gameobject.script @@ -651,6 +651,43 @@ _G.COnBeforePlayHudSound = function(alias, parent) end end +--[[ +on_before_play_ambient_bed callback, scale or veto the per-weather ambient background bed. +Fired from CGamePersistent::WeathersUpdate (GamePersistent.cpp) at each bed play site. +Return via result.volume_mult: 0 vetoes the play, <1 attenuates, nil leaves vanilla. + +Arguments + section: weather ambient section the bed loads from + file: resolved sound file name, "" when the handle is not ready yet + pos: Fvector world position the bed plays at +--]] +AddScriptCallback("on_before_play_ambient_bed") +_G.COnAmbientBed = function(section, file, pos) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_ambient_bed", section, file, pos, result) + if result.volume_mult then + return result + end +end + +--[[ +on_before_play_level_music callback, scale or silence the level music track. +Fired from SMusicTrack::Play (level_sounds.cpp). Volume-only: a skip would make the +manager re-select every frame, so 0 silences rather than stops the track. +Return via result.volume_mult: 0 silences, <1 attenuates, nil leaves vanilla. + +Arguments + file: resolved music track file name, "" when the handle is not ready yet +--]] +AddScriptCallback("on_before_play_level_music") +_G.COnLevelMusic = function(file) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_level_music", file, result) + if result.volume_mult then + return result + end +end + -- NLTP_ASHES & demonized : Actor on death callback AddScriptCallback("actor_on_death") function actor_on_first_update() diff --git a/gamedata/scripts/lua_help_ex.script b/gamedata/scripts/lua_help_ex.script index 01a74eac10..97d0205257 100644 --- a/gamedata/scripts/lua_help_ex.script +++ b/gamedata/scripts/lua_help_ex.script @@ -155,12 +155,6 @@ ClrSetB(number argb, number b) } - engine callbacks (Lua defines the _G function; the engine calls it only if present) { - -- Each returns { volume_mult = number } to scale the play (0 vetoes/silences, <1 attenuates), or nil for vanilla. Zero cost when undefined. Mirror _G.COnBeforePlayHudSound. - table COnAmbientBed(string section, string file, Fvector pos) -- System A per-weather background bed play site (CGamePersistent::WeathersUpdate, GamePersistent.cpp). 0 vetoes the play. - table COnLevelMusic(string file) -- level music track in SMusicTrack::Play (level_sounds.cpp). Volume-only (0 silences the track). - } - get_console() { table get_variable_bounds(cmd) }