refactor(loader): replace manual loader with generic recursive autoloader system

Replace the hardcoded module loader with a flexible, recursive autoloader (NoCode) that:

- Supports glob patterns (wildcards) for module discovery
- Recursively scans folders without depth limits
- Properly handles shared (sh_), server (sv_), and client (cl_) file prefixes
- Implements safe loading with error handling and statistics
- Searches both LUA and GAME paths simultaneously
- Sends library files to clients without auto-including them
- Adds comprehensive logging with color-coded output
- Excludes configured files/folders (template.lua, _dev, disabled)

The new system is more maintainable and allows adding new modules without modifying the loader.
This commit is contained in:
2026-07-20 13:47:39 +02:00
parent 75428ef21d
commit f6ec6b1236
+302 -14
View File
@@ -1,23 +1,267 @@
xyghtBladWinRPMenu = xyghtBladWinRPMenu or {} -- ============================================================
-- AUTOLOADER nocode — Dual-path récursif
-- Fichier : lua/autorun/bladWinRpMenu_loader.lua
-- ============================================================
AddCSLuaFile("bladw_deathscreen/config/sh_config.lua") local NoCode = {}
if SERVER then NoCode.config = {
AddCSLuaFile("bladw_hud/client/cl_hud.lua") -- Dossier(s)-module(s) à charger.
AddCSLuaFile("bladw_cMenu/client/cl_cMenu.lua") -- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
AddCSLuaFile("bladw_deathscreen/client/cl_interface_ds.lua") -- • avec wildcard -> glob, charge TOUS ceux qui matchent :
-- "bladw_*" (préfixe) / "*_menu" (suffixe) / "bladw_*_v2" (milieu)
root = "bladw_announcement",
-- Dossiers de bibliothèques : seulement envoyés au client (AddCSLuaFile),
-- jamais auto-inclus. Ils sont chargés à la demande via include("lib/...").
libs = { "lib" },
safe_load = true,
verbose = true,
exclude = { "template.lua", "disabled", "_dev" },
}
include("bladw_deathscreen/server/sv_hooks.lua") -- ============================================================
-- Logs
-- ============================================================
local C = {
ok = Color(100, 220, 100),
err = Color(255, 80, 80),
info = Color(120, 180, 255),
warn = Color(255, 200, 50),
dim = Color(160, 160, 160),
title = Color(200, 140, 255),
}
local function Log(col, msg)
MsgC(col, msg .. "\n")
end
-- ============================================================
-- Utilitaires
-- ============================================================
local function StartsWith(str, prefix)
return str:sub(1, #prefix) == prefix
end
local function IsExcluded(name)
for _, ex in ipairs(NoCode.config.exclude) do
if name == ex then return true end
end
return false
end
local function SortFiles(files)
table.sort(files, function(a, b)
local function rank(n)
if StartsWith(n, "sh_") then return 1 end
if StartsWith(n, "sv_") then return 2 end
if StartsWith(n, "cl_") then return 3 end
return 4
end
return rank(a) < rank(b)
end)
end
--- Fusionne deux tables en évitant les doublons
local function Merge(t1, t2)
local seen = {}
local result = {}
for _, v in ipairs(t1 or {}) do
if not seen[v] then seen[v] = true; table.insert(result, v) end
end
for _, v in ipairs(t2 or {}) do
if not seen[v] then seen[v] = true; table.insert(result, v) end
end
return result
end
-- ============================================================
-- Compteurs
-- ============================================================
local stats = { loaded = 0, sent = 0, errors = 0 }
-- ============================================================
-- Chargement d'un fichier unique
-- ============================================================
local function LoadFile(path)
local filename = path:match("([^/]+)$")
local realm, shouldLoad, shouldSend
if StartsWith(filename, "sh_") then
realm = "SHARED"
shouldSend = SERVER
shouldLoad = true
elseif StartsWith(filename, "sv_") then
realm = "SERVER"
shouldLoad = SERVER
elseif StartsWith(filename, "cl_") then
realm = "CLIENT"
shouldSend = SERVER
shouldLoad = not SERVER
resource.AddSingleFile("resource/fonts/Poppins-SemiBold.ttf")
resource.AddSingleFile("resource/fonts/Poppins-Medium.ttf")
resource.AddSingleFile("resource/fonts/Poppins-Bold.ttf")
resource.AddSingleFile("resource/fonts/Poppins-Regular.ttf")
else else
include("bladw_hud/client/cl_hud.lua") return
include("bladw_cMenu/client/cl_cMenu.lua") end
include("bladw_deathscreen/client/cl_interface_ds.lua")
if shouldSend then
AddCSLuaFile(path)
stats.sent = stats.sent + 1
if NoCode.config.verbose and not shouldLoad then
Log(C.info, (" [→] [%s] %s"):format(realm, path))
end
end
if not shouldLoad then return end
if NoCode.config.safe_load then
local ok, err = pcall(include, path)
if ok then
stats.loaded = stats.loaded + 1
if NoCode.config.verbose then
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
end
else
stats.errors = stats.errors + 1
Log(C.err, (" [✗] [%s] %s"):format(realm, path))
Log(C.err, (" ↳ %s"):format(tostring(err)))
end
else
include(path)
stats.loaded = stats.loaded + 1
if NoCode.config.verbose then
Log(C.ok, (" [✓] [%s] %s"):format(realm, path))
end
end
end
-- ============================================================
-- Scan récursif — cherche TOUJOURS dans LUA et GAME en même temps
-- ============================================================
local function LoadFolder(dir, depth)
depth = depth or 0
local basename = dir:match("([^/]+)$") or dir
if IsExcluded(basename) then
Log(C.warn, (" [~] Dossier ignoré : %s"):format(dir))
return
end
-- On cherche dans les deux paths et on fusionne les résultats
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
local files = Merge(files1, files2)
local dirs = Merge(dirs1, dirs2)
if #files == 0 and #dirs == 0 then
if NoCode.config.verbose and depth > 0 then
Log(C.warn, (" [!] Dossier vide ou introuvable : %s"):format(dir))
end
return
end
if NoCode.config.verbose and depth > 0 then
Log(C.dim, string.rep(" ", depth) .. "📂 " .. basename .. "/")
end
SortFiles(files)
for _, fname in ipairs(files) do
if fname:EndsWith(".lua") and not IsExcluded(fname) then
LoadFile(dir .. "/" .. fname)
end
end
-- Récursion sur tous les sous-dossiers sans limite de profondeur
for _, subdir in ipairs(dirs) do
if not IsExcluded(subdir) then
LoadFolder(dir .. "/" .. subdir, depth + 1)
end
end
end
-- ============================================================
-- Envoi récursif d'une bibliothèque au client (sans l'inclure)
-- Les fichiers de lib/ ne suivent pas le préfixe sh_/sv_/cl_ ;
-- on les expédie simplement pour que include("lib/...") fonctionne.
-- ============================================================
local function SendFolder(dir)
if not SERVER then return end
local files1, dirs1 = file.Find(dir .. "/*", "LUA")
local files2, dirs2 = file.Find("lua/" .. dir .. "/*", "GAME")
local files = Merge(files1, files2)
local dirs = Merge(dirs1, dirs2)
for _, fname in ipairs(files) do
if fname:EndsWith(".lua") and not IsExcluded(fname) then
local path = dir .. "/" .. fname
AddCSLuaFile(path)
stats.sent = stats.sent + 1
if NoCode.config.verbose then
Log(C.info, (" [→] [LIB] %s"):format(path))
end
end
end
for _, subdir in ipairs(dirs) do
if not IsExcluded(subdir) then
SendFolder(dir .. "/" .. subdir)
end
end
end
-- ============================================================
-- Découverte des dossiers-modules
-- (file.Find ne supporte le wildcard que sur le dernier segment,
-- donc on liste la racine lua/ puis on filtre nous-mêmes.)
-- • pattern sans "*" -> match exact (un seul dossier)
-- • pattern avec "*" -> glob converti en motif Lua (préfixe/suffixe/milieu)
-- ============================================================
--- Échappe les caractères magiques d'un motif Lua.
local function EscapePattern(s)
return (s:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
end
--- Transforme un glob simple ("bladw_*") en motif Lua ancré ("^bladw_.*$").
local function GlobToPattern(glob)
return "^" .. EscapePattern(glob):gsub("%%%*", ".*") .. "$"
end
local function FindRootFolders(pattern)
local roots = {}
-- Construit la fonction de test selon la présence ou non d'un wildcard.
local matches
if pattern:find("*", 1, true) then
local lua_pat = GlobToPattern(pattern)
matches = function(d) return d:match(lua_pat) ~= nil end
else
matches = function(d) return d == pattern end
end
local _, dirs1 = file.Find("*", "LUA")
local _, dirs2 = file.Find("lua/*", "GAME")
for _, d in ipairs(Merge(dirs1, dirs2)) do
if matches(d) and not IsExcluded(d) then
table.insert(roots, d)
end
end
table.sort(roots)
return roots
end
if CLIENT then
surface.CreateFont("bladw_text", { surface.CreateFont("bladw_text", {
font = "Poppins-SemiBold", font = "Poppins-SemiBold",
size = 25, size = 25,
@@ -52,4 +296,48 @@ else
weight = 800, weight = 800,
antialias = true antialias = true
}) })
end end
-- ============================================================
-- Point d'entrée
-- ============================================================
Log(C.title, "")
Log(C.title, " ╔══════════════════════════════════════╗")
Log(C.title, " ║ NoCode — Autoloader ║")
Log(C.title, " ╚══════════════════════════════════════╝")
Log(C.info, " Dossier racine : lua/" .. NoCode.config.root)
Log(C.dim, "")
-- 1) Expédie les bibliothèques partagées au client (rndx, etc.)
for _, lib in ipairs(NoCode.config.libs or {}) do
SendFolder(lib)
end
-- 2) Charge chaque module racine correspondant au préfixe
local roots = FindRootFolders(NoCode.config.root)
if #roots == 0 then
Log(C.warn, (" [!] Aucun module trouvé pour le préfixe : %s"):format(NoCode.config.root))
else
for _, root in ipairs(roots) do
if NoCode.config.verbose then
Log(C.title, "" .. root)
end
LoadFolder(root)
end
end
Log(C.dim, "")
Log(C.title, " ╔══════════════════════════════════════╗")
if SERVER then
Log(C.ok, (" ║ ✓ %d chargé(s) / %d envoyé(s) au client"):format(stats.loaded, stats.sent))
else
Log(C.ok, (" ║ ✓ %d fichier(s) chargé(s) [CLIENT]"):format(stats.loaded))
end
if stats.errors > 0 then
Log(C.err, (" ║ ✗ %d erreur(s)"):format(stats.errors))
end
Log(C.title, " ╚══════════════════════════════════════╝")
Log(C.dim, "")