All work

Retired

Plugin hot reload

A developer tool that reloaded individual plugins instead of the whole gamemode. It carried four months of feature work, then was retired.

Role
Design and implementation
Stack
Lua (GLua), NutScript internals
Context
Local development workflow
Status
Retired

The problem

Changing any gamemode file reloaded the entire gamemode, freezing my local server for about twenty seconds every time I wanted to test something, even for a one-character change. Over a day of iteration, that adds up to a lot of waiting.

How it worked

-- The one line at the top of every plugin file:
--
--     if (HR and HR()) then return end
--
-- When the engine refreshes a single plugin file, this hands the reload to my
-- loader and stops the stale copy from running on its own.

local function hotreloadPlugins()
    if (HR_HOTRELOADING) then return end
    HR_HOTRELOADING = true

    hr.plugin.loadFromDir("plugins-hr")   -- mirrors the framework's own loader (omitted)

    -- Rebuilding the spawn menu was the main lag source, so it's suppressed during reloads.
    hook.Add("SpawnMenuEnabled", "hotreload-lagfix", function() return false end)

    -- Replay the framework's start-up events, skipping plugins on the
    -- opt-out list that can't handle running them twice.
    local skipped = {}
    for uniqueID in pairs(skipInitializedPlugins) do
        local plugin = nut.plugin.list[uniqueID]
        local fn = hook.GetTable()["InitializedPlugins"][plugin]
        if (fn) then
            hook.Remove("InitializedPlugins", plugin)
            skipped[plugin] = fn
        end
    end
    hook.Run("InitializedPlugins")
    for plugin, fn in pairs(skipped) do
        hook.Add("InitializedPlugins", plugin, fn)
    end

    hook.Remove("SpawnMenuEnabled", "hotreload-lagfix")
    hook.Run("InitializedItems")

    HR_HOTRELOADING = nil
end

function HR()
    -- Only plugins from the hot-reload folder are handled.
    local src = debug.getinfo(2, "S").source
    if (src and not string.find(src, "hotreload%-plugins")) then return end

    -- Re-entrancy: never start a reload while one is running, while the loader
    -- is including plugin files itself, or right after a full gamemode reload.
    if (HR_HOTRELOADING) then return end
    if (HR_PLUGIN_LOADING or HR_PLUGINS_ALREADY_LOADED) then return end

    hotreloadPlugins()
    return true
end

-- A full gamemode reload loads the plugins once, then blocks the per-file
-- guard for a second so every plugin file doesn't trigger its own reload.
hook.Add("OnReloaded", "hotreload-gm-reload", function()
    if (HR_PLUGINS_ALREADY_LOADED) then return end
    timer.Simple(0, hotreloadPlugins)
    HR_PLUGINS_ALREADY_LOADED = true
    timer.Create("hotreload-gm-reload", 1, 1, function()
        HR_PLUGINS_ALREADY_LOADED = false
    end)
end)
Adapted from production code, lightly edited for readability.

What it carried

From February to June 2025 it was how I built most new features, including faction events, conflict locks, criminal stashes, gun bags, barricades, lockpicking and the first version of felonies.

Why it was retired

Its weakness was its footprint. Every hot-reloadable file needed that one-line guard, so a convenience for my own workflow became part of the codebase everyone else maintains. After a compatibility issue with one plugin that did unusual things at start-up, the owner and I agreed to retire it.

If I built it again, I’d make it invisible to the code itself: a development-only file watcher that reloads plugins from outside the gamemode, with nothing added to plugin files.