-- ====================================================================== --[[ 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 = " " 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