Divergent/mods/Busy Hands Detection and Blame/gamedata/scripts/a_rax_wd.script

219 lines
7.1 KiB
Plaintext

--[[
Watch Dog script or Busy Hands Detection and Blame
25FEB2024
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License
Author: RavenAscendant
Watch Dog timers to detect hangs in the the actor update callback and timed event queue the 2 major causes of the "Busy Hands" bug in anomaly.
--]]
local crash_on_error = true -- setting to false will not crash the game on first error. allowing you to keep playing, not a good idea.
local hang_time = 0.5 --time in seconds callback/event system is allowed to hang
local watch_list = {--actor and global callbacks only, will not work for NPC and mutant callbacks or any callback that can be sent from multiple sources. don't use for dxml callbacks either
actor_on_update = {},
}
base_calback_set = axr_main.callback_set
function axr_main.callback_set(name,func_or_userdata)
if not (watch_list[name]) then
base_calback_set(name,func_or_userdata)
else --only register it here if it is one of ours
if (func_or_userdata == nil) then
printf("![axr_main callback_set] trying to set callback %s to nil function!",name)
callstack()
return
end
watch_list[name][func_or_userdata] = true
end
end
base_callback_unset = axr_main.callback_unset
function axr_main.callback_unset(name,func_or_userdata)
if (watch_list[name]) then
watch_list[name][func_or_userdata] = nil
end
base_callback_unset(name,func_or_userdata) --unregister in both just in case someting snuck thru
end
base_make_callback = axr_main.make_callback
function axr_main.make_callback(name,...)
if (watch_list[name]) then
make_callback_watcher(name,...)
end
base_make_callback(name,...)--send in both just in case someting snuck thru
end
function make_callback_watcher(name,...)
if (watch_list[name]) then
local wd_name = "watch_dog_"..name
for func_or_userdata,v in pairs(watch_list[name]) do
if (type(func_or_userdata) == "function" ) then
CreateTimeEvent(wd_name, func_or_userdata,hang_time, watcher, name, func_or_userdata)
func_or_userdata(...)
RemoveTimeEvent(wd_name, func_or_userdata)
elseif (func_or_userdata[name] ) then
CreateTimeEvent(wd_name, func_or_userdata[name],hang_time, watcher, name, func_or_userdata[name])
func_or_userdata[name](func_or_userdata,...)
RemoveTimeEvent(wd_name, func_or_userdata[name])
end
end
else
printf("! [RAX WD] can't make callback to non existing intercept %s! how did we get here?!?!?",name)
callstack()
end
end
local save_detected = false
local saved = false
function watcher(name, func)
if save_detected then return false end
printf("! [RAX WD] Callback %s hung for > %s seconds!", name , hang_time )
local info = func and debug.getinfo(func,"S")
printf("! [RAX WD] Offending callback at line: %s of script:%s",info and info.linedefined, info and info.short_src)
if crash_on_error and not saved then
printf("! [RAX WD] Saving game.")
exec_console_cmd("save busy hands crash save" )
saved = true
return false
end
assert(not crash_on_error, 'The Actor Update system has crashed. Known as "Busy Hands" type 1, caused by script: '.. (info and info.short_src or "") .. ' | line: '.. (info and info.linedefined or "" ))
return true
end
--these lines can be entered one at a time in the debug lua console to validate this script for type 1 busyhands (note this cannot detect type 1 busyhands when type 2 is occuring)
--RegisterScriptCallback("actor_on_update", function() printf("cat") end ) --makes the timed event system spam the log with 'cat' so you can see when it stops working
--RegisterScriptCallback("actor_on_update", function() db.actor:give_game_news("cat") end ) --causes a busy hands type 1 hang
--these lines can be entered one at a time in the debug lua console to validate this script for type 2 busyhands
--CreateTimeEvent("foo", "foo1",0, function() printf("dog") end) --makes the timed event system spam the log with 'dog' so you can see when it stops working
--CreateTimeEvent("foo", "foo2",0, function() db.actor:give_game_news("dog") end) --causes a busy hands type 2 hang
local ev_queue = {}
function _G.CreateTimeEvent(ev_id,act_id,timer,f,...)
if not (ev_queue[ev_id]) then
ev_queue[ev_id] = {}
ev_queue[ev_id].__size = 0
end
if not (ev_queue[ev_id][act_id]) then
ev_queue[ev_id][act_id] = {}
ev_queue[ev_id][act_id].timer = time_global() + timer*1000
ev_queue[ev_id][act_id].f = f
ev_queue[ev_id][act_id].p = {...}
ev_queue[ev_id].__size = ev_queue[ev_id].__size + 1
end
end
function _G.RemoveTimeEvent(ev_id,act_id)
if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
ev_queue[ev_id][act_id] = nil
ev_queue[ev_id].__size = ev_queue[ev_id].__size - 1
end
end
function _G.ResetTimeEvent(ev_id,act_id,timer)
if (ev_queue[ev_id] and ev_queue[ev_id][act_id]) then
ev_queue[ev_id][act_id].timer = time_global() + timer*1000
end
end
function _G.ProcessEventQueue(force)
if has_alife_info("sleep_active") then
return false
end
for event_id,actions in pairs(ev_queue) do
for action_id,act in pairs(actions) do
if (action_id ~= "__size") then
if (force) or (time_global() >= act.timer) then
-- utils_data.debug_write(strformat("event_queue: event_id=%s action_id=%s",event_id,action_id))
local temp = track_event(event_id, action_id, act.f)
AddUniqueCall(temp)
if (act.f(unpack(act.p)) == true) then
ev_queue[event_id][action_id] = nil
ev_queue[event_id].__size = ev_queue[event_id].__size - 1
end
RemoveUniqueCall(temp)
end
end
end
if (ev_queue[event_id].__size == 0) then
ev_queue[event_id] = nil
end
end
return false
end
function _G.ProcessEventQueueState(m_data,save)
if (save) then
m_data.event_queue = ev_queue
else
ev_queue = m_data.event_queue or ev_queue
end
end
function track_event(event_id, action_id, func)
local tmr = time_global() + hang_time * 1000
return function()
if save_detected then return false end
if tmr > time_global() then return false end
printf("! [RAX WD] Event %s, %s hung for > %s seconds!", event_id,action_id, hang_time)
local info = func and debug.getinfo(func,"S")
printf("! [RAX WD] Offending event at line: %s of script:%s",info and info.linedefined, info and info.short_src)
if crash_on_error and not saved then
printf("! [RAX WD] Saving game.")
exec_console_cmd("save busy hands crash save" )
saved = true
return false
end
assert(not crash_on_error,'The Timed Event system has crashed. Known as "Busy Hands" type 2, caused by script: '.. (info and info.short_src or "") .. ' | line: '.. (info and info.linedefined or "" ))
return true
end
end
function on_game_start()
RegisterScriptCallback("save_state", save_state)
RegisterScriptCallback("ActorMenu_on_mode_changed", ActorMenu_on_mode_changed)
end
function ActorMenu_on_mode_changed(mode)
if mode ~= 0 then save_state() end --block WD when opening inventory.
end
function save_state()-- when saving set a flag for 5 seconds that prevents the WD functions from going off.
save_detected = true
--printf("save detected")
local tmr = time_global() + 5000
AddUniqueCall(
function()
if tmr > time_global() then return false end
save_detected = false
--printf("save detected removed")
return true
end
)
end