Divergent/mods/Devices of Anomaly Redone/gamedata/scripts/item_radio.script

1384 lines
42 KiB
Plaintext
Raw Normal View History

2024-03-17 20:18:03 -04:00
-- ======================================================================
--[[ New Extensible RF Sources
-- ======================================================================
Author: Catspaw
https://www.youtube.com/channel/UCtG8fiWPUZEzWlkUn60btAw
Source: https://www.moddb.com/mods/stalker-anomaly/addons/new-extensible-rf-sources/
Version: 1.5-DAR
Updated: 20231130
Stripped-down version for redistribution with:
Devices of Anomaly Redone
Includes optional support for:
TB's RF Tasks (integrated)
Reworked RF Receiver (integrated, with MCM options)
Remote-Controlled Explosives (integrated)
Additional support for Bizarre Interference and other features are
available in the full version of NERFS on Moddb.
Credits: Tronex, RavenAscendant, Coverdrave, Tweeki Breeki, demonized
Based on the vanilla Anomaly radio script created by Tronex
Created: 2018/10/15 by Tronex
RF Receiver and sources management
2020/4/26: Added 3D UI to the detector
-- ===================================================================--]]
local debuglogs = false
local verbose = false
local logprefix = "<NERFS> "
script_version = "1.5-DAR"
local rand = math.random
local sfind = string.find
local ssub = string.sub
local ts = game.translate_string
local mwheel_supported_ver = 20230701
local game_version = ts("ui_st_game_version")
local gamma_modpack = game_version:find("G.A.M.M.A.") -- GAMMA now supports mouse wheel scripting
local mwheel_avail = gamma_modpack or (MODDED_EXES_VERSION and (MODDED_EXES_VERSION >= mwheel_supported_ver))
local mwheel_enabled = false
local enable_debug = false
local enable_acc_widget = false
local signal_pulse_length = 1
emission_functor = nil
bgnoise_functor = nil
local rfsrc_ini = ini_file_ex("scripts\\rf_sources\\signal_sources.ltx")
local rfcfg_ini = ini_file_ex("scripts\\rf_sources\\rf_config.ltx")
local rfcfg = rfcfg_ini:collect_section("config_defaults")
wid_pos = {x=491,y=670}
-- == Addon Compatibility settings =======================================
-- RCE compatibility
explosive_triggered = false
-- RRFR and TB compatibility
enable_rrfr = false
rrfr_update_sources = false
rrfr_must_equip_det = false
local tg_beep = 0
local min_gap = tonumber(rfcfg and rfcfg.rf_min_gap) or 400
local beep_gap = 0
local tg_cache = 0
local min_flash_time = tonumber(rfcfg and rfcfg.rf_min_flash_time) or 100
local flash_time = 0
local tg_tb_flash = 0
tb_flash_time = 0
tb_in_range = false
tb_sound_object = false
--[[=====================================================================
-Receiver bands-
The below table exists in vanilla, but is almost entirely unused. The
receiver is hardcoded into VHF with its 30/300 Mhz bounds. There's no
way to change it in-game. However, some of the vanilla monsters and
artifacts have frequencies outside of these bounds.
I've set the min and max to utilize the entire three-digit range. It
spans several different bands, but they're arbitrary categories
anyway. You can change this value in MCM.
I've left _band in just in case any addon tries to reference it, but
vanilla doesn't.
The task/reward scripts do reference _min and _max via functions, so
they should automatically pick up this change and assign stashes in
the new range.
-- ==================================================================--]]
bands = {
["ELF"] = { _min = 3 , _max = 30 , _unit = game.translate_string("st_hz") },
["SLF"] = { _min = 30 , _max = 300 , _unit = game.translate_string("st_hz") },
["ULF"] = { _min = 300 , _max = 3000 , _unit = game.translate_string("st_hz") },
["VLF"] = { _min = 3 , _max = 30 , _unit = game.translate_string("st_khz") },
["LF"] = { _min = 30 , _max = 300 , _unit = game.translate_string("st_khz") },
["MF"] = { _min = 300 , _max = 3000 , _unit = game.translate_string("st_khz") },
["HF"] = { _min = 3 , _max = 30 , _unit = game.translate_string("st_mhz") },
["VHF"] = { _min = 30 , _max = 300 , _unit = game.translate_string("st_mhz") },
["UHF"] = { _min = 300 , _max = 3000 , _unit = game.translate_string("st_mhz") },
["SHF"] = { _min = 3 , _max = 30 , _unit = game.translate_string("st_ghz") },
["EHF"] = { _min = 30 , _max = 300 , _unit = game.translate_string("st_ghz") },
["THF"] = { _min = 300 , _max = 3000 , _unit = game.translate_string("st_ghz") },
}
local _band = bands["VHF"]
-- Overriding this until/unless we get the ability to change bands,
-- it's pointless otherwise
--local _min = _band._min
--local _max = _band._max
local _min = 1
local _max = tonumber(rfcfg and rfcfg.rf_freq_max) or 999 -- configurable in MCM
local _unit = _band._unit
local _range = 2
local _freq = math.random(_min,_max)
local _device = "detector_radio"
local scan_time = tonumber(rfcfg and rfcfg.rf_scan_time) or 5000 -- 5 seconds
local clamp = clamp
-- ======================================================================
--[[ Sound Effects and Signal Sources
Most SFX are now loaded dynamically from rfcfg_*.ltx
If your addon needs to modify any of the RF Receiver's vanilla sound
effects, check those files first to see if a simple LTX change will
help avoid conflicts.
If you want to add or change sound effects, there is no need to
modify the script--just add a new rfcfg_youraddon.ltx file that
contains the necessary path and filename info. These will all be
loaded into snd_path_table. See the existing files for examples.
You can also dynamically manage sound data in snd_path_table using
the get_signal_sound() and set_signal_sound() functions later in
this script.
For special cases such as background or emission noise, these each
have their own functors which can be monkeypatched or redirected to
a different script.
-- ====================================================================]]
stalker_pda_frequencies = {} -- PDA frequencies of known stalkers
local signal_sources = {}
local active_sources = {} -- all online RF sources
local RF = {} -- sources to process
RF_stashes = {} -- stored stashes with RF source
RF_targets = {} -- stored specific targets with RF source
local exclude = {}
local presets = {}
local num_presets = 9
local last_preset = 1
for i=1,num_presets do presets[i] = {kb=-1,mk=0,f=1} end
action_defs = {}
local snd_emission = {}
local snd_random = {}
local snd_talk = {}
local path_silence = "$no_sound"
local snd_silence = xr_sound.get_safe_sound_object(path_silence)
local snd_on = xr_sound.get_safe_sound_object(rfcfg.path_sound_on)
local snd_off = xr_sound.get_safe_sound_object(rfcfg.path_sound_off)
local path_default_noise= rfcfg.path_default_noise
local path_default_beep = rfcfg.path_default_beep
local snd_beep = sound_object(path_default_beep)
local snd_noise = xr_sound.get_safe_sound_object(path_default_noise)
local snd_path_table = {
silence = path_silence,
noise = path_default_noise,
white_noise = path_default_noise,
rfid_beep = path_default_beep,
quest_beep = path_default_beep,
-- For compatibility with TB's RF tasks:
tb_beep = rfcfg.tb_beep,
random = {},
emission = {},
chatter = {},
gsm = {},
psy = {},
}
-- ======================================================================
-- Utilities
-- ======================================================================
function dl(logtext,...)
-- Debug logging - to disable, set debuglogs to false
if logtext and debuglogs then
printf(logprefix..logtext,...)
end
end
function vl(logtext,...)
-- Verbose logging - to disable, set debuglogs and/or verbose to false
if logtext and debuglogs and verbose then
dl("[V] "..logtext,...)
end
end
function dotip(tiptext,dur,src,beep,icon,snd)
vl("Tip call received: dur %s | src \"%s\" | beep %s\n\"%s\"",dur,src,beep,tiptext)
if tiptext == nil then return end
db.actor:give_game_news(src or "RF Receiver", tiptext, icon or "ui_inGame2_Radiopomehi", 0, dur or 5000)
if beep then
xr_sound.set_sound_play(AC_ID, snd or "pda_tips")
end
end
function exec(str,...)
-- Adapted from Haruka's Skill System
local res = nil
if str then
str = str_explode(str,"%.")
if str[1] and str[2] and _G[ str[1] ] and _G[ str[1] ][ str[2] ] then
res = _G[ str[1] ][ str[2] ](...)
else
dl("Could not exec function %s", str)
end
end
return res
end
function ltxbool(st)
if st == "true" then
return true
elseif st == "false" then
return false
else return st end
end
function get_freq()
return _freq
end
function change_freq(num,force_set)
vl("change_freq(%s) | currently %s",num,_freq)
local hud = actor_menu.get_last_mode()
if (hud ~= 0) or item_device.is_pda_active() then
return
end
local oldf = _freq
local newf = clamp(_freq + num, _min, _max)
if force_set then
newf = clamp(num, _min, _max)
end
_freq = newf
if enable_rrfr and reworked_rf_receiver_mcm.get_config("update_sources_on_frequency_change") then
scan_online_sources()
end
SendScriptCallback("actor_on_frequency_change", oldf, newf)
end
function is_in_range(freq)
if type(freq) == "number" then
local a = math.abs(freq - _freq)
if (a < _range) then
--vl("Freq %s is in range (%s/%s)",freq,a,_range)
return true
end
elseif type(freq) == "table" then
for i = 1,#freq do
local inr = is_in_range(freq[i])
if inr then return true end
end
end
return false
end
function get_vol_range(freq)
local nearest
if type(freq) == "number" then
local a = math.abs(freq - _freq)
if (a < _range) then
return (1 - a/_range)
end
elseif type(freq) == "table" then
for i = 1,#freq do
local a = get_vol_range(freq[i])
if (not nearest) or (a < nearest) then nearest = a end
end
return (1 - nearest/_range)
end
return 0
end
function get_random_freq()
return math.random(_min,_max)
end
function add_stash(lvl,id, freq)
if lvl and RF_stashes[lvl] then
RF_stashes[lvl][id] = freq or get_random_freq()
end
end
function clear_stash(lvl,id)
if lvl and RF_stashes[lvl] then
RF_stashes[lvl][id] = nil
end
end
function add_target(id, freq, dist, snd)
local called_by_tb = (debug.getinfo(2, 'n').name == "tb_radio_thing")
snd = (called_by_tb and snd_path_table.tb_beep) or snd
-- for compatibility with TB's RF tasks
freq = freq or get_random_freq()
dist = dist or 200
RF_targets[id] = { freq = freq , dist = dist , snd = snd}
end
function clear_target(id)
if id then
RF_targets[id] = nil
end
end
function create_rf_table(id, cls, dist_pos, freq, dist, snd, sec, srcdef)
local rft = {
id = id,
clsid = cls,
dist_pos = dist_pos,
freq = freq,
dist = dist,
snd = snd,
sec = sec
}
if snd == "func" then rft.snd = get_sfx_for_signal(rft,srcdef and srcdef.func) end
return rft
end
function validate_RF_targets() -- Clear non-existing RF targets
for k,v in pairs(RF_targets) do
local se = alife_object(k)
if (not se) then
RF_targets[k] = nil
end
end
for k,v in pairs(stalker_pda_frequencies) do
local se = alife_object(k)
if (not se) or ((se and se:name()) ~= v.name) then
stalker_pda_frequencies[k] = nil
end
end
end
function init_new_active_source(id,cls,sec,dist_pos,freq,dist,snd,srcdef)
vl("init_new_active_source: id: %s | cls: %s | sec: %s | dist_pos %s | freq %s | dist %s | snd %s",id,cls,sec,dist_pos,(srcdef and srcdef.freqs) or freq,dist,snd)
active_sources[#active_sources + 1] = create_rf_table(id,cls,dist_pos,freq,dist,snd,sec,srcdef)
end
function debug_print_stalker_pdas()
if not (debuglogs and verbose) then return end
local spf = stalker_pda_frequencies
if spf and not is_empty(spf) then
local dump = "**** Current known stalker PDAs:"
for k,v in pairs (spf) do
local obj = v and v.id and alife_object(v.id)
dump = dump..string.format("\n | %s (%s) : %s Mhz |",v.name,v.id,v.freq)
end
vl(dump)
end
end
function get_rnd_freq_from_tbl(freqtbl)
if not (freqtbl and #freqtbl) then end
return freqtbl[rand(#freqtbl)]
end
function get_random_pda_freq(defs)
-- Every stalker's PDA has its own unique frequency that
-- is somewhere within the range of one of the defined
-- GSM bands (in rfsrc_cellnoise.ltx)
local posneg = {[1]=1,[2]=-1}
local variance = defs and defs.variance or 15
local base_freq = get_rnd_freq_from_tbl(defs.freq) or 915
local freq = clamp(tonumber(base_freq + (rand(1,variance) * posneg[rand(2)])),1,999)
dl("get_random_pda_freq for id %s: base_freq %s | variance %s | final %s",defs and defs.id,base_freq,variance,freq)
debug_print_stalker_pdas()
return freq
end
function new_stalker_contact(obj,action_defs)
if not (obj and action_defs) then return end
local defs = action_defs
local id = defs.id
if not id then return end
local spf = stalker_pda_frequencies
local freq = spf and spf[id] and tonumber(spf[id].freq)
if not freq then
vl("id %s not found in stalker_pda_frequencies, generating new freq",id)
freq = get_random_pda_freq(defs)
spf[id] = {
freq = freq,
id = id,
name = obj:name(),
}
end
init_new_active_source(
id,
defs.cls,
defs.sec,
defs.dist_pos,
freq,
defs.srcdef.dist,
defs.srcdef.snd,
defs.srcdef
)
return true
end
function signal_active_artifact(obj,action_defs)
if not (obj and action_defs) then return end
local defs = action_defs
local srcdef = defs.srcdef
local sec = defs.sec
local cls = defs.cls
local snd = signal_sources[sec].snd or signal_sources[cls].snd
vl("initializing new artifact signal: %s | snd %s | func %s",sec,snd,func)
init_new_active_source(
defs.id,
cls,
sec,
defs.dist_pos,
signal_sources[sec].freq or signal_sources[cls].freq,
signal_sources[sec].dist or signal_sources[cls].dist,
snd,
srcdef
)
end
function signal_stash_rfid(obj,action_defs)
if not (obj and action_defs) then return end
local defs = action_defs
local id = defs.id
--vl("initializing new rfid signal: stash id %s",id)
local cls = defs.cls
init_new_active_source(
id,
cls,
defs.sec,
defs.dist_pos,
defs.freq,
defs.srcdef.dist,
defs.srcdef.snd,
defs.srcdef
)
end
function scan_online_sources()
--vl("scan_online_sources")
local pos = db.actor:position()
local st = db.storage
-- Search
empty_table(active_sources)
for i=1,65534 do
local obj = st[i] and st[i].object or level.object_by_id(i)
if obj then
local matched = false
local cls = obj:clsid()
local id = obj:id()
local dist_pos = pos:distance_to(obj:position())
local src_cls = signal_sources[cls]
local rfid_target = (RF_targets[id] and (dist_pos < RF_targets[id].dist))
if src_cls or rfid_target then
local is_invbox = (cls == clsid.inventory_box) or (cls == clsid.inventory_box_s)
local sec = obj:section()
local _level = level.name()
local dist = (src_cls and src_cls.dist) or (RF_targets[id] and RF_targets[id].dist) or -1
local src_sec = signal_sources[sec]
local rft_def = RF_targets[id]
local rfs_def = RF_stashes[_level] and RF_stashes[_level][id]
local strict_match = src_cls and src_cls.exact_match
local bysec,srcdef,freq,other_target,rfid_stash
if strict_match then
local mb = src_cls.match_by
local rfs = is_invbox and (mb == "rfid") and rfs_def
bysec = (mb == "section") and src_sec
if rfs and dist and (dist_pos < dist) then
--vl("stash strict match by freq: %s",rfs_def)
freq = rfs_def
srcdef = src_cls
rfid_stash = true
elseif bysec then
vl("strict match by section: %s",sec)
srcdef = src_sec
other_target = true
end
else
srcdef = src_cls
other_target = (dist_pos < dist)
--vl("other target: %s",other_target)
end
if rfid_target then
--vl("rfid target")
freq = rft_def.freq
snd = rft_def.snd or snd_path_table.quest_beep or path_default_beep
init_new_active_source(id,cls,sec,dist_pos,freq,dist,snd)
elseif rfid_stash or other_target then
--vl("cls %s | sec %s | rfid_target %s | other_target %s",cls,sec,rfid_target,other_target)
freq = srcdef and srcdef.freq or freq
local snd = srcdef and srcdef.snd or path_default_beep
if not freq then freq = get_random_freq() end
--vl("final signal source values for id %s: cls %s | sec %s | dist %s of %sm | freq %s | snd %s",id,cls,sec,dist_pos,dist,freq,snd)
local action = srcdef.action
if action then
action_defs = {
obj = obj,
id = id,
cls = cls,
sec = sec,
freq = freq,
dist_pos = dist_pos,
srcdef = srcdef,
snd = snd,
func = srcdef.func,
}
--vl("action found for %s, calling functor %s",sec,action)
exec(action,obj,action_defs)
if action_defs.ret_functor then
exec(ret_functor,action_defs.ret_args)
end
else
init_new_active_source(id,cls,sec,dist_pos,freq,dist,snd,srcdef)
end
end
end
end
end
-- store closest 3 sources
empty_table(RF)
empty_table(exclude)
for j=1,3 do
local smallest_distance = 200
local last_id = nil
for i=1,#active_sources do
if (not exclude[i]) and is_in_range(active_sources[i].freq) and (active_sources[i].dist_pos < smallest_distance) then
smallest_distance = active_sources[i].dist_pos
last_id = i
end
end
if (last_id ~= nil) and (not exclude[last_id]) then
RF[#RF+1] = active_sources[last_id] -- fill
exclude[last_id] = true
end
end
if (#RF > 0) then
refresh = true -- require sounds update
end
dl("active_sources: %s | RF: %s", #active_sources, #RF)
end
local d_state = false
local vol_n = 0
function sound_trigger(state)
if state and (not d_state) then
snd_on:play(db.actor, 0, sound_object.s2d)
d_state = true
vol_n = 0.7
snd_noise.volume = vol_n
elseif (not state) and d_state then
snd_off:play(db.actor, 0, sound_object.s2d)
d_state = false
vol_n = 0
snd_noise.volume = vol_n
end
end
--[[=====================================================================
EXTERNAL HOOKS
The following functions provide a way for addons or other external
scripts to modify the table of signals and source sounds at
runtime.
--===================================================================--]]
function get_signal_sound(snd_id)
return snd_path_table and snd_path_table[snd_id]
end
function set_signal_sound(snd_id,snd_data)
-- snd_data can be either a string path, or a numeric
-- index of such paths that will be shuffled randomly
if not snd_id then return end
snd_path_table[snd_id] = snd_data
return true
end
function get_signal_source(ssrc_id)
if not signal_sources and ssrc_id then return end
return signal_sources[ssrc_id]
end
function inject_signal_source(key,raw_table)
-- for those who understand the table structure and want
-- a bit more control over changes to the signal sources
if not (key and raw_table and (type(raw_table) == "table")) then return end
signal_sources[key] = raw_table
return true
end
function set_signal_source(ssrc_id,dist,snd,clsid,section,freq,func)
if not ssrc_id and dist and snd and (clsid or section) then return end
signal_sources[ssrc_id] = {
dist = dist,
snd = snd,
clsid = clsid,
section = section,
freq = freq,
func = func,
}
return true
end
function del_signal_source(ssrc_id)
if not ssrc_id and signal_sources and signal_sources[ssrc_id] then return end
signal_sources[ssrc_id] = nil
return true
end
----------------------------------------------------------------------
-- UI
----------------------------------------------------------------------
local detector_rf_ui = nil
local tg_led = 0
local tg_emission = 0
class "UI3D_RF" (CUIScriptWnd)
function UI3D_RF:__init() super()
self:Show (true)
self:Enable (true)
self.freq = nil
local xml = CScriptXmlInit()
self.xml = xml
xml:ParseFile ("ui_detector_rf.xml")
xml:InitWindow ("detector_rfproxy", 0, self)
--self.m_area_b = xml:InitStatic("detector_rfproxy:area_b", self)
self.m_area_r = xml:InitStatic("detector_rfproxy:area_r", self)
self.m_seg1 = xml:InitStatic("detector_rfproxy:seg1", self)
self.m_seg2 = xml:InitStatic("detector_rfproxy:seg2", self)
self.m_seg3 = xml:InitStatic("detector_rfproxy:seg3", self)
self.m_led = xml:InitStatic("detector_rfproxy:led", self)
end
function UI3D_RF:__finalize()
detector_rf_ui = nil
end
function UI3D_RF:Update()
CUIScriptWnd.Update(self)
local tg = time_global()
-- LED flashing
if (not enable_rrfr) and (tg > tg_led + 1000) then
tg_led = tg
self.m_led:Show(not self.m_led:IsShown())
end
-- Emissions
if GetEvent("surge", "state") or GetEvent("psi_storm", "state") then
if (tg > tg_emission + 200) then
tg_emission = tg
self.m_led:Show((math.random(1,100) < 50) and true or false)
_freq = math.random(_min,_max)
else
return
end
-- Normal readings
else
if (not enable_rrfr) and (self.freq == _freq) then
return
end
self.freq = _freq
local tg = time_global()
if not explosive_triggered then -- RCE compatibility
if enable_rrfr then
-- If RRFR is enabled, LED light now only flashes for quest beeps or TB's packages
if tb_in_range then
if tg > tg_tb_flash and tb_sound_object and tb_sound_object:playing() and not self.m_led:IsShown() then
tg_tb_flash = tg + tb_flash_time
self.m_led:Show(true)
elseif tg > tg_tb_flash and self.m_led:IsShown() then
self.m_led:Show(false)
end
else
local tg = time_global()
if (tg > tg_led) and snd_beep:playing() and not self.m_led:IsShown() then
tg_led = tg + beep_gap
tg_cache = tg
self.m_led:Show(true)
elseif (tg > tg_cache + flash_time) and self.m_led:IsShown() then
self.m_led:Show(false)
end
end
end
elseif (tg > tg_led + 50) then
tg_led = tg
self.m_led:Show(not self.m_led:IsShown())
end
end
local s_freq = tostring(_freq)
local seg1, seg2, seg3
if (_freq > 99) then
seg1 = strformat("green_%s", s_freq:sub(1, 1))
seg2 = strformat("green_%s", s_freq:sub(2, 2))
seg3 = strformat("green_%s", s_freq:sub(3, 3))
elseif (_freq > 9) then
seg1 = "green_0"
seg2 = strformat("green_%s", s_freq:sub(1, 1))
seg3 = strformat("green_%s", s_freq:sub(2, 2))
elseif (_freq > 0) then
seg1 = "green_0"
seg2 = "green_0"
seg3 = strformat("green_%s", s_freq:sub(1, 1))
else
seg1 = "green_0"
seg2 = "green_0"
seg3 = "green_0"
end
self.m_seg1:InitTextureEx(seg1, "hud\\p3d")
self.m_seg2:InitTextureEx(seg2, "hud\\p3d")
self.m_seg3:InitTextureEx(seg3, "hud\\p3d")
end
function get_UI()
if (detector_rf_ui == nil) then
detector_rf_ui = UI3D_RF()
end
return detector_rf_ui
end
-- =======================================================================
-- Accessibility indicator based on RavenAscendant's work
-- =======================================================================
HUD_IND = nil
function enable_rf_widget()
if not (enable_acc_widget and ui_rf_widget and db.actor) then return end
if (HUD_IND == nil) then
HUD_IND = ui_rf_widget.UIRFWidget("rax_rf_indicator.xml", "indicator", nil, wid_pos)
if HUD_IND then
get_hud():AddDialogToRender(HUD_IND)
else
dl("ERROR: Unable to load ui_rf_widget.script or initialize its HUD class")
end
end
end
function disable_rf_widget()
if not db.actor then return end
if (HUD_IND ~= nil) then
HUD_IND:ShowDialog(false)
get_hud():RemoveDialogToRender(HUD_IND)
HUD_IND = nil
end
end
function show_indicator(tf)
if not db.actor then return end
if HUD_IND then HUD_IND:ShowIndicator(enable_acc_widget and tf) end
end
--=======================================================================
function sfx_random_noise(rf_tbl)
--vl("sfx_random_noise called")
return snd_path_table and snd_path_table.random
end
function sfx_random_noise_psy(rf_tbl)
--vl("sfx_random_noise_psy called")
return snd_path_table and snd_path_table.psy
end
function sfx_radio_chatter(rf_tbl)
-- possble to add logic later that produces different chatter by faction
return snd_path_table and snd_path_table.chatter
end
function sfx_gsm_noise(rf_tbl)
return snd_path_table and snd_path_table.gsm
end
function sfx_background_noise(rf_tbl)
printf("sfx_background_noise at %s",time_global())
return snd_path_table and snd_path_table.white_noise
end
function sfx_emission(rf_tbl)
printf("sfx_emission at %s",time_global())
local emsnds = snd_path_table and snd_path_table.emission
return emsnds[math.random(#emsnds)]
end
function load_ltx_data()
dl("Loading sound data")
local loaded_manually = {
["config_sfx_defaults"] = true,
["config_sfx_random"] = true,
["config_sfx_bgnoise"] = true,
}
local rndcfg = rfcfg_ini:collect_section("config_sfx_random")
local bgcfg = rfcfg_ini:collect_section("config_sfx_bgnoise")
local sndpath = rndcfg.sfx_path.."\\"..rndcfg.sfx_filename
for i=1,rndcfg.sfx_max do
snd_path_table.random[i] = sndpath..tostring(i)
vl("Populating snd_path_table[random][%s] with %s",i,sndpath..tostring(i))
snd_random[i] = xr_sound.get_safe_sound_object(snd_path_table.random[i])
end
local rfcfg_ltx = rfcfg_ini:get_sections(true)
for k,_ in pairs(rfcfg_ltx) do
local rfx = rfcfg_ini:collect_section(k)
local strex = str_explode(k,"_sfx_")
local sndpath = rfx.sfx_path
local sndfile = rfx.sfx_filename
if (strex and (strex[1] == "config")) and (not loaded_manually[k]) and sndpath and sndfile then
local sndsec = strex[2]
sndpath = sndpath.."\\"..sndfile
vl("sndpath for %s is %s",k,sndpath)
local sndmax = rfx.sfx_max or 1
local ind = 0
for i=1,sndmax do
if not snd_path_table[sndsec] then
snd_path_table[sndsec] = {}
end
local sndname = sndpath..tostring(i)
snd_path_table[sndsec][i] = sndname
ind = ind + 1
vl("Populating snd_path_table[%s][%s] with %s",sndsec,ind,sndname)
end
local startind = ind + 1
if rfx.insert_random then
vl("Inserting %s entries of random noise into %s",rfx.insert_random,sndsec)
for i=startind,(startind + tonumber(rfx.insert_random)) do
snd_path_table[sndsec][i] = snd_path_table.random[math.random(1,rndcfg.sfx_max)]
ind = ind + 1
end
end
startind = ind + 1
if rfx.insert_silence then
vl("Inserting %s entries of dead silence into %s",rfx.insert_silence,sndsec)
for i=startind,(startind + tonumber(rfx.insert_silence)) do
snd_path_table[sndsec][i] = path_silence
ind = ind + 1
end
end
if rfx.sfx_functor then
rfcfg[sndsec.."_functor"] = rfx.sfx_functor
end
end
end
local snd_e = snd_path_table.emission
local snd_t = snd_path_table.chatter
for i=1,#snd_e do
vl("Populating sound object for snd_emission[%s] (%s)",i,snd_e[i])
snd_emission[i] = xr_sound.get_safe_sound_object(snd_e[i])
end
for i=1,#snd_t do
vl("Populating sound object for snd_talk[%s] (%s)",i,snd_t[i])
snd_talk[i] = xr_sound.get_safe_sound_object(snd_t[i])
end
emission_functor = rfcfg["emission_functor"]
bgnoise_functor = bgcfg["sfx_functor"]
dl("Noise functors found:\n * background = %s\n * emission = %s",bgnoise_functor,emission_functor)
local typedefs = {
["dist"] = "number",
["freq"] = "number",
["variance"] = "number",
["match_key"] = "boolean",
["exact_match"] = "boolean",
}
local rfsrc_ltx = rfsrc_ini:get_sections(true)
for s,d in pairs(rfsrc_ltx) do
local srcdef = rfsrc_ini:collect_section(s)
dl("loading signal def for %s",s)
local key = s
if srcdef and srcdef.clsid then
key = clsid[srcdef.clsid]
elseif srcdef and srcdef.section then
key = srcdef.section
--[[
elseif srcdef and srcdef.match_key then
key = srcdef[srcdef.match_key] or key
--]]
end
signal_sources[key] = {}
for attr,v in pairs(srcdef) do
local val = v
local csv = string.find(v,",")
vl("%s = %s (csv %s)",attr,v)
if (attr == "freq") and (csv and csv > 0) then
dl("%s is multi-frequency",attr)
local fr = str_explode(v,",")
for i = 1,#fr do
printf("%s",fr[i])
fr[i] = tonumber(fr[i])
vl("valid freq for %s: %s",attr,fr[i])
end
signal_sources[key].freqs = v
val = fr
elseif attr == "clsid" then
val = clsid[v]
elseif typedefs[attr] == "number" then
val = tonumber(v) or 0
elseif typedefs[attr] == "boolean" then
val = ltxbool(v)
end
signal_sources[key][attr] = val
signal_sources[key].key = key
end
end
end
----------------------------------------------------------------------
-- Callbacks
----------------------------------------------------------------------
local tg_scan = 0
local tg_random = math.random(20,40)*1000
-- added for accessibility indicator
local tg_interferance = tg_random
local vol_interferance = 0
local snds = {}
local refresh = false
local emission = false
local flag_10,flag_50,flag_preset
local function server_entity_on_unregister(se_obj, typ)
RF_targets[se_obj.id] = nil
end
local function actor_on_first_update()
-- create RF stashes
if is_empty(RF_stashes) then
local sim = alife()
local gg = game_graph()
-- gather all stashes
local lvls = {}
for id,v in pairs(treasure_manager.caches) do
local se = sim:object(id)
if se then
local lvl = sim:level_name(gg:vertex(se.m_game_vertex_id):level_id())
lvls[lvl] = lvls[lvl] or {} -- create level table
lvls[lvl][#lvls[lvl] + 1] = id -- add stashes from the same level
end
end
-- fill stashes table with a few selected targets
for lvl,v in pairs(lvls) do
RF_stashes[lvl] = {}
local size = (#v > 15) and #v or 0
for i=1,size,(size/5) do
local indx = math.ceil(i)
local id = v[indx]
RF_stashes[lvl][id] = get_random_freq()
if enable_debug then
level.map_add_object_spot_ser(id, "treasure", "RF Source\\nFrequency: " .. tostring(RF_stashes[lvl][id])) -- test
end
dl("RF_stashes[%s][%s] = %s",lvl,id,RF_stashes[lvl][id])
end
end
end
on_option_change()
end
function get_sfx_for_signal(rf_tbl,snd_functor)
local snd = rf_tbl and rf_tbl.snd
--vl("get_sfx_for_signal called with snd %s | func %s",snd,snd_functor)
local sndpath = (snd_path_table and snd_path_table[snd])
if (snd == "func") and snd_functor then
--vl("executing %s",snd_functor)
sndpath = exec(snd_functor,rf_tbl)
end
--vl("sndpath: %s",sndpath)
return sndpath or snd_path_table.noise
end
local function calc_beep_gap(dist_to_RF, RF_range)
local gap = (dist_to_RF / RF_range) * 3000
if gap < min_gap then return min_gap end
--clamp minimum gap between beeps so that player doesn't get tinnitus
return gap
end
function match_multi_freq(curr_freq,match_freq)
local freq = match_freq
if type(freq) == "table" then
for i = 1,#freq do
if curr_freq == freq[i] then return true end
end
end
return curr_freq == freq
end
local function calc_flash_time(dist_to_RF, RF_range)
-- [Coverdrave]: Calculate the time the flashing LED is on based on distance, only for quest targets.
local flash = (dist_to_RF / RF_range) * 500
if flash < min_flash_time then return min_flash_time end
return flash
end
function actor_has_valid_device(must_be_active)
local obj_det
if (not must_be_active) and reworked_rf_receiver_mcm and reworked_rf_receiver_mcm.get_config("only_equip_rf") then
obj_det = db.actor:item_in_slot(9)
else
obj_det = db.actor:active_detector()
end
return (obj_det and obj_det:section() == _device and obj_det:condition() >= obj_det:power_critical())
end
function set_indicator_color(signal, interference,emission, tg)
if HUD_IND then
HUD_IND:SetIndicatorColor(signal, interference,emission, tg)
end
end
local tg_signal_pulse = 0
local vol_signal_highest
local function actor_on_update()
if enable_acc_widget and not indicator then
enable_rf_widget()
end
-- return if radio detector is no active
if not actor_has_valid_device() then
sound_trigger(false)
show_indicator(false)
return
end
sound_trigger(true)
-- Emissions
emission = GetEvent("surge", "state") or GetEvent("psi_storm", "state")
show_indicator(enable_acc_widget)
-- Scan for any nearby radio wave source
local tg = time_global()
if (tg > tg_scan) then
tg_scan = tg + scan_time
scan_online_sources()
end
-- Signal sounds
local vol_noise_lowest = 0.7
vol_signal_highest = (tg_signal_pulse > tg) and vol_signal_highest or 0
for i=1,#RF do
local obj = level.object_by_id(RF[i].id)
if obj and (not emission) then
local pass_range = is_in_range(RF[i].freq) -- RRFR
local dist = db.actor:position():distance_to(obj:position()) or 0
local rfidist = RF[i].dist or 1
if pass_range and dist < rfidist then -- RRFR
local id = obj:id()
local sec = obj:section()
local clsid = obj:clsid()
local dist_ratio = dist / rfidist
local vol_range = get_vol_range(RF[i].freq)
local vol_signal = clamp( (1-dist_ratio) * vol_range , 0 , 1) or 0
local vol_noise = clamp( dist_ratio * vol_range , 0.2 , 0.7 ) or 0.7
vol_noise_lowest = (vol_noise < vol_noise_lowest) and vol_noise or vol_noise_lowest
dl("RF[%s] - %s (%s) - RF dist: %s - current dist: %s - dist_ratio: %s", i, obj:section(), RF[i].id, rfidist, dist, dist_ratio)
--dl("vol_range: %s - vol_signal: %s - vol_noise: %s", vol_range, vol_signal, vol_noise)
if enable_rrfr and (RF_targets[id] and not string.find(sec,"quest_tb_package_")) then
-- Use Reworked RF Receiver's method for playing beeps
if RF_targets[id] and not string.find(sec,"quest_tb_package_") then
beep_gap = calc_beep_gap(dist, rfidist)
local timeglobal = time_global()
if (timeglobal > tg_beep) then
tg_beep = timeglobal + beep_gap
flash_time = calc_flash_time(dist, rfidist)
snd_beep:play(db.actor, 0, sound_object.s2d)
snd_beep.volume = (reworked_rf_receiver_mcm.get_config("custom_beep_volume") and reworked_rf_receiver_mcm.get_config("beep_volume")) or vol_signal
end
-- All other sources, using old beeping method
elseif RF_database[clsid] then
if (not snds[i]) or (snds[i] and (not snds[i]:playing())) or refresh then
local snd_paths = RF[i].snd
snds[i] = sound_object(snd_paths[math.random(#snd_paths)])
snds[i]:play(db.actor, 0, sound_object.s2d)
print_dbg("snds[%s] - #s: %s", i, #snd_paths)
end
snds[i].volume = (reworked_rf_receiver_mcm.get_config("custom_beep_volume") and reworked_rf_receiver_mcm.get_config("beep_volume")) or vol_signal
end
else
if (not snds[i]) or (snds[i] and (not snds[i]:playing())) or refresh then
local snd_paths = RF[i].snd
local sndpath = snd_paths
if type(snd_paths) == "table" then
sndpath = snd_paths[math.random(#snd_paths)]
end
snds[i] = xr_sound.get_safe_sound_object(sndpath)
snds[i]:play(db.actor, 0, sound_object.s2d)
dl("snds[%s] - #s: %s", i, #snd_paths)
if (vol_signal > vol_signal_highest) then
vol_signal_highest = vol_signal
local pulse = tg + signal_pulse_length * snds[i]:length() * vol_signal_highest
tg_signal_pulse = pulse< tg_signal_pulse and tg_signal_pulse or pulse
end
snds[i].volume = vol_signal
end
end
end
end
end
vol_n = vol_noise_lowest
-- White noise sound
if (not snd_noise:playing()) then
local noisefx,functor
if emission then
functor = emission_functor
else
functor = bgnoise_functor
end
noisefx = functor and exec(functor)
if noisefx then
printf("found noisefx %s",noisefx)
snd_noise = xr_sound.get_safe_sound_object(noisefx)
snd_noise:play(db.actor, 0, sound_object.s2d)
else
printf("no noisefx returned by %s, playing silence",functor)
snd_noise = snd_silence
snd_noise:play(db.actor, 0, sound_object.s2d)
vol_n = 0
end
end
snd_noise.volume = vol_n
refresh = false
-- Random sounds
if (tg > tg_random) then
tg_random = tg + math.random(20,40)*1000
local randfac = (math.random(100) > 50)
local randsnd = snd_random[math.random(#snd_random)]
local talksnd = snd_talk[math.random(#snd_talk)]
local snd_random_now = randfac and randsnd or talksnd -- or snd_silence
printf("snd_random_now: %s | randfac %s | randsnd %s | talksnd %s",snd_random_now,randfac,randsnd,talksnd)
--local snd_random_now = (math.random(100) > 50) and snd_random[math.random(#snd_random)] or snd_talk[math.random(#snd_talk)]
snd_random_now:play(db.actor, 0, sound_object.s2d)
snd_random_now.volume = math.random(10,100)/100
vol_interferance = math.random(10,100)/100
snd_random_now.volume = vol_interferance
tg_interferance = tg + snd_random_now:length()
--vl("snd_random_now:length():%s", snd_random_now:length())
end
if (tg > tg_interferance) then
vol_interferance = 0
end
set_indicator_color(vol_signal_highest, vol_interferance, emission, tg)
end
local function load_state(m_data)
RF_stashes = m_data.RF_stashes or {}
RF_targets = m_data.RF_targets or {}
_freq = m_data.RF_freq or math.random(_min,_max)
stalker_pda_frequencies = m_data.stalker_pda_frequencies or {}
end
local function save_state(m_data)
validate_RF_targets()
m_data.RF_stashes = RF_stashes
m_data.RF_targets = RF_targets
m_data.RF_freq = _freq
m_data.stalker_pda_frequencies = stalker_pda_frequencies
end
local function on_key_hold(key)
if (key == DIK_keys["DIK_LSHIFT"]) then
flag_10 = true
elseif (key == DIK_keys["DIK_LMENU"]) then
flag_50 = true
elseif (key == DIK_keys["DIK_LCONTROL"]) then
flag_preset = true -- not implemented yet
end
end
function on_option_change()
if ui_mcm then
enable_acc_widget = ui_mcm.get("nerfs/nerfwidget/show")
local dbg = ui_mcm.get("nerfs/nerfmain/debuglogs")
if dbg ~= nil then debuglogs = dbg end
_max = ui_mcm.get("nerfs/nerfmain/max_freq") or _max
signal_low = {
r = ui_mcm.get("nerfs/nerfwidget/lo_r") or signal_low.r,
g = ui_mcm.get("nerfs/nerfwidget/lo_g") or signal_low.g,
b = ui_mcm.get("nerfs/nerfwidget/lo_b") or signal_low.b,
a = ui_mcm.get("nerfs/nerfwidget/lo_a") or signal_low.a,
}
signal_high = {
r = ui_mcm.get("nerfs/nerfwidget/hi_r") or signal_high.r,
g = ui_mcm.get("nerfs/nerfwidget/hi_g") or signal_high.g,
b = ui_mcm.get("nerfs/nerfwidget/hi_b") or signal_high.b,
a = ui_mcm.get("nerfs/nerfwidget/hi_a") or signal_high.a,
}
signal_emission = {
r = ui_mcm.get("nerfs/nerfwidget/em_r") or signal_emission.r,
g = ui_mcm.get("nerfs/nerfwidget/em_g") or signal_emission.g,
b = ui_mcm.get("nerfs/nerfwidget/em_b") or signal_emission.b,
a = ui_mcm.get("nerfs/nerfwidget/em_a") or signal_emission.a,
}
local oldpos = wid_pos
wid_pos.x = ui_mcm.get("nerfs/nerfwidget/pos_x")
wid_pos.y = ui_mcm.get("nerfs/nerfwidget/pos_y")
if (oldpos.x ~= wid_pos.x) or (oldpos.y ~= wid_pos.y) then
HUD_IND:SetPos(wid_pos.x, wid_pos.y)
end
dl("Loaded widget pos %s,%s from MCM, enable_acc_widget %s",wid_pos.x,wid_pos.y,enable_acc_widget)
indicator = nil
for i=1,num_presets do
local ind = tostring(i)
presets[i].kb = ui_mcm.get("nerfs/nerfpresets/kb_preset"..ind)
presets[i].mk = ui_mcm.get("nerfs/nerfpresets/mk_preset"..ind)
presets[i].f = ui_mcm.get("nerfs/nerfpresets/preset"..ind.."_freq")
dl("Populating preset %s: kb %s | mk %s | freq %s",i,presets[i].kb,presets[i].mk,presets[i].f)
end
-- Reworked RF Receiver compatibility
local enrrfr = ui_mcm.get("nerfs/nerfmain/enable_rrfr")
if enrrfr ~= nil then rrfr_compatibility(enrrfr) end
if mwheel_avail then
mwheel_enabled = ui_mcm.get("nerfs/nerfmain/use_mwheel")
else
ui_mcm.set("nerfs/nerfmain/use_mwheel",false)
end
end
end
local function mod_key_pressed(key)
local mkp = ui_mcm and ui_mcm.get_mod_key(key) or false
printf("mod_key_pressed(%s): %s",key,mkp)
return mkp
end
function on_before_key_press(key,bind,dis,flags)
if not ui_mcm then return end
local detector = db.actor:active_detector()
local wep = db.actor:active_item()
if detector and (detector:section() == _device) and (not wep) then
for i=1,num_presets do
printf("presets: kb %s | mk %s | freq %s",presets[i].kb,presets[i].mk,presets[i].f)
if presets and presets[i] then
if (key == presets[i].kb) and
mod_key_pressed(presets[i].mk) then
local newf = presets[i].f or 1
change_freq(newf,true)
flags.ret_value = false
local tiptext = string.format(ts("st_nerfs_freq_preset"),i,newf)
dotip(tiptext)
end
end
end
end
end
function on_key_release(key)
local detector = db.actor:active_detector()
local wep = db.actor:active_item()
if detector and (detector:section() == _device) and (not wep) then
local num = (flag_50 and 50) or (flag_10 and 10) or 1
if (key == DIK_keys["MOUSE_1"]) then
change_freq(num)
elseif (key == DIK_keys["MOUSE_2"]) then
change_freq(-num)
end
end
flag_10 = false
flag_50 = false
flag_preset = false
end
function on_mouse_wheel(scroll_dir, flags)
local pda_active = item_device and item_device.is_pda_active()
if pda_active or not (mwheel_enabled and actor_has_valid_device(true)) then return end
local num = (flag_50 and 50) or (flag_10 and 10) or 1
--vl("on_mouse_wheel(%s) | flag_10 %s | flag_50 %s | num %s",scroll_dir,flag_10,flag_50,num)
if flag_preset then
local inc_dir = -1
if scroll_dir and (scroll_dir > 0) then
inc_dir = 1
end
local npre = clamp(last_preset + inc_dir,1,num_presets)
if npre ~= last_preset then
last_preset = npre
local newf = presets[npre].f or 1
change_freq(newf,true)
local tiptext = string.format(ts("st_nerfs_freq_preset"),npre,newf)
dotip(tiptext)
end
else
if scroll_dir and (scroll_dir > 0) then
vl("current frequency %s, raising by %s",_freq,num)
change_freq(num)
else
vl("current frequency %s, lowering by %s",_freq,num)
change_freq(-num)
end
end
flags.ret_value = false
end
function actor_on_item_take(obj)
-- Reworked RF Receiver compatibility
local id = obj:id()
if RF_targets[id] then
clear_target(id)
scan_online_sources()
end
end
function rrfr_compatibility(onoff)
if onoff == nil then return enable_rrfr end
enable_rrfr = (onoff == true)
if enable_rrfr and reworked_rf_receiver_mcm then
local rrfr_us = reworked_rf_receiver_mcm.get_config("update_sources_on_frequency_change")
local rrfr_eq = reworked_rf_receiver_mcm.get_config("only_equip_rf")
if rrfr_us ~= nil then rrfr_update_sources = rrfr_us end
if rrfr_eq ~= nil then rrfr_must_equip_det = rrfr_eq end
RegisterScriptCallback("actor_on_item_take",actor_on_item_take)
else
UnregisterScriptCallback("actor_on_item_take",actor_on_item_take)
end
end
function on_game_start()
load_ltx_data()
if mwheel_avail then
RegisterScriptCallback("on_mouse_wheel",on_mouse_wheel)
end
RegisterScriptCallback("on_option_change",on_option_change)
RegisterScriptCallback("on_key_hold",on_key_hold)
RegisterScriptCallback("on_before_key_press",on_before_key_press)
RegisterScriptCallback("on_key_release",on_key_release)
RegisterScriptCallback("save_state",save_state)
RegisterScriptCallback("load_state",load_state)
RegisterScriptCallback("actor_on_first_update",actor_on_first_update)
RegisterScriptCallback("actor_on_update",actor_on_update)
RegisterScriptCallback("server_entity_on_unregister",server_entity_on_unregister)
end