diff --git a/gamedata/scripts/callbacks_gameobject.script b/gamedata/scripts/callbacks_gameobject.script index cbf87172f..b85cd9105 100644 --- a/gamedata/scripts/callbacks_gameobject.script +++ b/gamedata/scripts/callbacks_gameobject.script @@ -651,6 +651,25 @@ _G.COnBeforePlayHudSound = function(alias, parent) end end +--[[ +on_before_play_script_sound callback, scale or veto any script sound_object play (CScriptSound). +Return via result.volume_mult: 0 vetoes, <1 attenuates, nil leaves vanilla. A later engine +.volume write can override the attenuation. + +Arguments + file: resolved sound file name + pos: Fvector world position, nil when the play is 2D / positionless + obj: game_object that owns the sound, nil when absent +--]] +AddScriptCallback("on_before_play_script_sound") +_G.COnBeforePlayScriptSound = function(file, pos, obj) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_script_sound", file, pos, obj, result) + if result.volume_mult then + return result + 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. @@ -670,6 +689,58 @@ _G.COnAmbientBed = function(section, file, pos) end end +--[[ +on_before_play_ambient_effect callback, scale or veto the weather-effect recording sound +(CGamePersistent::WeathersUpdate). The particle burst and wind blast still play regardless. +Return via result.volume_mult: 0 vetoes the sound, <1 attenuates, nil leaves vanilla. + +Arguments + file: resolved effect sound file name + pos: Fvector world position the effect plays at +--]] +AddScriptCallback("on_before_play_ambient_effect") +_G.COnAmbientEffectSound = function(file, pos) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_ambient_effect", file, pos, result) + if result.volume_mult then + return result + end +end + +--[[ +on_before_play_thunderbolt callback, scale or veto the lightning-strike clap. Fires on the +last bolt of a burst only. +Return via result.volume_mult: 0 vetoes the clap, <1 attenuates, nil leaves vanilla. + +Arguments + file: resolved clap sound file name + distance: distance to the strike in metres +--]] +AddScriptCallback("on_before_play_thunderbolt") +_G.COnThunderboltSound = function(file, distance) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_thunderbolt", file, distance, result) + if result.volume_mult then + return result + end +end + +--[[ +on_before_play_rain callback, scale or veto the looped rain ambient (CEffect_Rain). Sampled +at rain onset, so a veto applies from the next time rain starts. +Return via result.volume_mult: 0 vetoes the loop, <1 attenuates, nil leaves vanilla. + +Arguments + file: resolved rain ambient file name +--]] +AddScriptCallback("on_before_play_rain") +_G.COnRainSound = function(file) + local result = { volume_mult = nil } + SendScriptCallback("on_before_play_rain", file, 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 diff --git a/src/xrEngine/IGame_Persistent.h b/src/xrEngine/IGame_Persistent.h index c40b35ba2..569f933bb 100644 --- a/src/xrEngine/IGame_Persistent.h +++ b/src/xrEngine/IGame_Persistent.h @@ -196,6 +196,12 @@ class ENGINE_API IGame_Persistent : virtual bool CanBePaused() { return true; } + // thunderbolt clap volume gate, overridden by the game layer to ask Lua. Returns 1.0 for vanilla. + // Appended at the end of the virtual block so it adds no vtable slot ahead of the existing ones. + virtual float OnThunderboltSound(LPCSTR file, float distance) { return 1.0f; } + // looped rain ambient volume gate, overridden by the game layer to ask Lua. Returns 1.0 for vanilla. + virtual float OnRainSound(LPCSTR file) { return 1.0f; } + struct pda_data { float pda_display_factor; diff --git a/src/xrEngine/Rain.cpp b/src/xrEngine/Rain.cpp index fbe22a930..82a574507 100644 --- a/src/xrEngine/Rain.cpp +++ b/src/xrEngine/Rain.cpp @@ -219,9 +219,16 @@ void CEffect_Rain::OnFrame() case stIdle: if (factor < EPS_L) return; state = stWorking; - snd_Ambient.play(0, sm_Looped); - snd_Ambient.set_position(Fvector().set(0, 0, 0)); - snd_Ambient.set_range(source_offset, source_offset * 2.f); + // Rain-loop hook, mirrors COnBeforePlayHudSound: veto (0) or attenuate, sampled once at onset. + rain_volume_mult = g_pGamePersistent + ? g_pGamePersistent->OnRainSound(snd_Ambient._handle() ? snd_Ambient._handle()->file_name() : "") + : 1.0f; + if (rain_volume_mult > EPS_S) + { + snd_Ambient.play(0, sm_Looped); + snd_Ambient.set_position(Fvector().set(0, 0, 0)); + snd_Ambient.set_range(source_offset, source_offset * 2.f); + } break; case stWorking: if (factor < EPS_L) @@ -242,7 +249,7 @@ void CEffect_Rain::OnFrame() // snd_Ambient.set_position(sndP); rain_volume = factor * hemi_factor; clamp(rain_volume, .1f, 1.f); - snd_Ambient.set_volume(rain_volume); + snd_Ambient.set_volume(rain_volume * rain_volume_mult); } } diff --git a/src/xrEngine/Rain.h b/src/xrEngine/Rain.h index 69402fb3c..6c826cb54 100644 --- a/src/xrEngine/Rain.h +++ b/src/xrEngine/Rain.h @@ -78,6 +78,7 @@ class ENGINE_API CEffect_Rain ref_sound snd_Ambient; float rain_volume; float rain_hemi = 0.0f; + float rain_volume_mult = 1.0f; // set once at rain onset by COnRainSound; 1.0 = vanilla // Utilities void p_create(); diff --git a/src/xrEngine/thunderbolt.cpp b/src/xrEngine/thunderbolt.cpp index 651ed52f4..0fd944b9d 100644 --- a/src/xrEngine/thunderbolt.cpp +++ b/src/xrEngine/thunderbolt.cpp @@ -232,7 +232,14 @@ void CEffect_Thunderbolt::Bolt(shared_str id, float period, float lt) else { next_lightning_time = Device.fTimeGlobal + period + Random.randF(-period * 0.3f, period * 0.3f); - current->snd.play_no_feedback(0, 0, dist / 300.f, &pos, 0, 0, &Fvector2().set(dist / 2, dist * 2.f)); + + // clap volume gate through the game layer (no script-engine reach from xrEngine). 0 skips the clap. + float clap_volume_mult = g_pGamePersistent + ? g_pGamePersistent->OnThunderboltSound(current->snd._handle() ? current->snd._handle()->file_name() : "", dist) + : 1.0f; + if (clap_volume_mult > EPS_S) + current->snd.play_no_feedback(0, 0, dist / 300.f, &pos, (clap_volume_mult < 1.0f) ? &clap_volume_mult : 0, 0, + &Fvector2().set(dist / 2, dist * 2.f)); } diff --git a/src/xrGame/GamePersistent.cpp b/src/xrGame/GamePersistent.cpp index a96bfbd3f..af01a8cb1 100644 --- a/src/xrGame/GamePersistent.cpp +++ b/src/xrGame/GamePersistent.cpp @@ -276,7 +276,7 @@ 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 +// { 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) @@ -288,6 +288,24 @@ static float ambient_hook_volume_mult(const ::luabind::object& output) return 1.0f; } +// Thunderbolt clap crossing. CEffect_Thunderbolt (xrEngine) has no script-engine reach, so it calls this +// IGame_Persistent virtual and the functor fires here, the COnBeforePlayHudSound shape. An unset global leaves vanilla. +float CGamePersistent::OnThunderboltSound(LPCSTR file, float distance) +{ + ::luabind::functor<::luabind::object> funct; + if (ai().script_engine().functor("_G.COnThunderboltSound", funct)) + return ambient_hook_volume_mult(funct(file, distance)); + return 1.0f; +} + +float CGamePersistent::OnRainSound(LPCSTR file) +{ + ::luabind::functor<::luabind::object> funct; + if (ai().script_engine().functor("_G.COnRainSound", funct)) + return ambient_hook_volume_mult(funct(file)); + return 1.0f; +} + void CGamePersistent::WeathersUpdate() { if (g_pGameLevel && !g_dedicated_server) @@ -396,7 +414,23 @@ void CGamePersistent::WeathersUpdate() offset.z += Random.randF(0.5f, 5.f) * (Random.randF(0.f, 1.f) < 0.5f ? -1.f : 1.f); pos.add(Device.vCameraPosition, offset); ambient_particles->play_at_pos(pos); - if (eff->sound._handle()) eff->sound.play_at_pos(0, pos); + + // Ambient-effect sound hook. Lua may veto (0) or attenuate the recording. The particle burst and wind blast play either way. + // An unset global leaves exact vanilla. + if (eff->sound._handle()) + { + float effect_volume_mult = 1.0f; + ::luabind::functor<::luabind::object> effect_funct; + if (ai().script_engine().functor("_G.COnAmbientEffectSound", effect_funct)) + effect_volume_mult = ambient_hook_volume_mult(effect_funct(eff->sound._handle()->file_name(), pos)); + + if (effect_volume_mult > EPS_S) + { + eff->sound.play_at_pos(0, pos); + if (effect_volume_mult < 1.0f) + eff->sound.set_volume(effect_volume_mult); + } + } Environment().wind_blast_strength_start_value = Environment().wind_strength_factor; diff --git a/src/xrGame/GamePersistent.h b/src/xrGame/GamePersistent.h index c6d6b0935..936d80bfa 100644 --- a/src/xrGame/GamePersistent.h +++ b/src/xrGame/GamePersistent.h @@ -50,6 +50,12 @@ class CGamePersistent : void WeathersUpdate(); void UpdateDof(); +public: + virtual float OnThunderboltSound(LPCSTR file, float distance); + virtual float OnRainSound(LPCSTR file); + +private: + public: ui_core* m_pUI_core; IReader* pDemoFile; diff --git a/src/xrGame/script_sound.cpp b/src/xrGame/script_sound.cpp index c44ad35ad..431537491 100644 --- a/src/xrGame/script_sound.cpp +++ b/src/xrGame/script_sound.cpp @@ -13,6 +13,38 @@ #include "ai_space.h" #include "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; +} + +// COnBeforePlayScriptSound(file, pos, obj) -> { volume_mult }, the COnBeforePlayHudSound shape. +// 0 vetoes the play, <1 attenuates, nil = vanilla. Absent args reach Lua as nil. An unset global costs one lookup. +static float script_sound_hook_volume_mult(LPCSTR file, const Fvector* pos, CScriptGameObject* object) +{ + ::luabind::functor<::luabind::object> funct; + if (!ai().script_engine().functor("_G.COnBeforePlayScriptSound", funct)) + return 1.0f; + Fvector obj_pos; // Play() carries no position: read the object's, but only past the gate + if (!pos && object) + { + obj_pos = object->object().Position(); + pos = &obj_pos; + } + if (pos && object) + return ambient_hook_volume_mult(funct(file, *pos, object)); + if (pos) + return ambient_hook_volume_mult(funct(file, *pos)); + return ambient_hook_volume_mult(funct(file)); +} + CScriptSound::CScriptSound(LPCSTR caSoundName, ESoundTypes sound_type) { m_caSoundToPlay = caSoundName; @@ -54,19 +86,33 @@ void CScriptSound::Play(CScriptGameObject* object, float delay, int flags) { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags %d",Device.dwTimeGlobal,m_sound._handle()->file_name(),delay,flags); + float volume_mult = script_sound_hook_volume_mult(*m_caSoundToPlay, NULL, object); + if (volume_mult <= EPS_S) + return; m_sound.play((object) ? &object->object() : NULL, flags, delay); + if (volume_mult < 1.0f) + m_sound.set_volume(volume_mult); } void CScriptSound::PlayAtPos(CScriptGameObject* object, const Fvector& position, float delay, int flags) { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); // Msg ("%6d : CScriptSound::Play (%s), delay %f, flags %d",m_sound._handle()->file_name(),delay,flags); + float volume_mult = script_sound_hook_volume_mult(*m_caSoundToPlay, &position, object); + if (volume_mult <= EPS_S) + return; m_sound.play_at_pos((object) ? &object->object() : NULL, position, flags, delay); + if (volume_mult < 1.0f) + m_sound.set_volume(volume_mult); } void CScriptSound::PlayNoFeedback(CScriptGameObject* object, u32 flags/*!< Looping */, float delay/*!< Delay */, Fvector pos, float vol, float freq) { THROW3(m_sound._handle(), "There is no sound", *m_caSoundToPlay); + float volume_mult = script_sound_hook_volume_mult(*m_caSoundToPlay, &pos, object); + if (volume_mult <= EPS_S) + return; + vol *= volume_mult; m_sound.play_no_feedback((object) ? &object->object() : NULL, flags, delay, &pos, &vol, &freq); }