Alle Projekte

Eingestellt

Plugin-Hot-Reload

Ein Entwicklerwerkzeug, das einzelne Plugins statt des gesamten Gamemodes neu lud. Es trug vier Monate Feature-Arbeit und wurde dann eingestellt.

Rolle
Entwurf und Umsetzung
Stack
Lua (GLua), NutScript-Interna
Kontext
Lokaler Entwicklungs-Workflow
Status
Eingestellt

Das Problem

Jede Änderung an einer Gamemode-Datei lud den gesamten Gamemode neu und fror meinen lokalen Server jedes Mal etwa zwanzig Sekunden lang ein, wenn ich etwas testen wollte, selbst bei einer Änderung von einem einzigen Zeichen. Über einen Tag voller Iterationen summiert sich das zu sehr viel Warterei.

Wie es funktionierte

-- 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)
Aus dem Produktivcode übernommen und zur besseren Lesbarkeit leicht bearbeitet.

Was es getragen hat

Von Februar bis Juni 2025 habe ich damit die meisten neuen Features gebaut, darunter Fraktions-Events, Konflikt-Sperren, Verstecke von Kriminellen, Waffentaschen, Barrikaden, Lockpicking und die erste Version der Verbrechen.

Warum es eingestellt wurde

Seine Schwäche war der Fußabdruck. Jede hot-reloadbare Datei brauchte diesen Einzeiler, sodass eine Erleichterung für meinen eigenen Workflow Teil der Codebasis wurde, die alle anderen pflegen. Nach einem Kompatibilitätsproblem mit einem Plugin, das beim Start ungewöhnliche Dinge tat, haben der Eigentümer und ich uns darauf geeinigt, es einzustellen.

Würde ich es noch einmal bauen, würde ich es für den Code selbst unsichtbar machen: ein File-Watcher nur für die Entwicklung, der Plugins von außerhalb des Gamemodes neu lädt, ohne dass den Plugin-Dateien etwas hinzugefügt wird.