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:
@@ -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
|
||||
AddCSLuaFile("bladw_hud/client/cl_hud.lua")
|
||||
AddCSLuaFile("bladw_cMenu/client/cl_cMenu.lua")
|
||||
AddCSLuaFile("bladw_deathscreen/client/cl_interface_ds.lua")
|
||||
NoCode.config = {
|
||||
-- Dossier(s)-module(s) à charger.
|
||||
-- • sans wildcard -> nom exact d'UN dossier : "bladw_announcement"
|
||||
-- • 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
|
||||
-- ============================================================
|
||||
|
||||
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
|
||||
include("bladw_hud/client/cl_hud.lua")
|
||||
include("bladw_cMenu/client/cl_cMenu.lua")
|
||||
include("bladw_deathscreen/client/cl_interface_ds.lua")
|
||||
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
|
||||
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
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", {
|
||||
font = "Poppins-SemiBold",
|
||||
size = 25,
|
||||
@@ -52,4 +296,48 @@ else
|
||||
weight = 800,
|
||||
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, "")
|
||||
Reference in New Issue
Block a user