219 lines
7.1 KiB
Plaintext
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 |