Hello, recently I built a module loader (server-side) and would like to hear some opinions on it, as well as suggestions on what could be improved or where I might be doing things wrong.
Also, I’m wondering if it’s worth using strict type checking and annotating types everywhere.
I don’t want to overcomplicate it, but I want to make it safe.
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AttributeUtils = require(ReplicatedStorage.Shared.Modules.Core.AttributeUtils)
local DataManager = require(ServerScriptService.Data.DataManager)
local moduleFolders = {
ServerScriptService:WaitForChild("Controllers"),
ServerScriptService:WaitForChild("Services")
}
local modules = {}
local playerHandlers = {}
local characterHandlers = {}
local initializedModules = {}
local handledPlayers = {}
local connections = {}
local function sortByPriority(list)
table.sort(list, function(a, b)
return (a.Priority or 0) > (b.Priority or 0)
end)
end
local function registerModule(moduleScript)
table.insert(modules, moduleScript)
if moduleScript.SetupPlayer then
table.insert(playerHandlers, moduleScript)
end
if moduleScript.SetupCharacter then
table.insert(characterHandlers, moduleScript)
end
end
local function gatherModules()
for _, folder in ipairs(moduleFolders) do
for _, obj in ipairs(folder:GetDescendants()) do
if not obj:IsA("ModuleScript") then
continue
end
local success, moduleScript = pcall(require, obj)
if not success or type(moduleScript) ~= "table" or not moduleScript.Init then
continue
end
moduleScript.Priority = moduleScript.Priority or 0
registerModule(moduleScript)
end
end
sortByPriority(modules)
sortByPriority(playerHandlers)
sortByPriority(characterHandlers)
end
local function initModules()
for _, moduleScript in ipairs(modules) do
if not initializedModules[moduleScript] then
initializedModules[moduleScript] = true
moduleScript.Init()
end
end
end
local function runCharacterModules(player, character)
if character:GetAttribute("Initialized") then
return
end
character:SetAttribute("Initialized", true)
for _, moduleScript in ipairs(characterHandlers) do
moduleScript:SetupCharacter(player, character)
end
end
local function cleanupPlayer(player)
if connections[player] then
for _, conn in ipairs(connections[player]) do
conn:Disconnect()
end
connections[player] = nil
end
handledPlayers[player] = nil
end
local function track(player, conn)
connections[player] = connections[player] or {}
table.insert(connections[player], conn)
end
local function handlePlayer(player)
if handledPlayers[player] then return end
handledPlayers[player] = true
local loaded = AttributeUtils.WaitForAttributes(player, {"DataLoaded"}, 10)
if not loaded and not DataManager.Profiles[player] then
warn("Data not loaded for " .. player.Name .. " (timeout)")
return
end
for _, moduleScript in ipairs(playerHandlers) do
moduleScript:SetupPlayer(player)
end
track(player, player.CharacterAdded:Connect(function(character)
runCharacterModules(player, character)
end))
if player.Character then
runCharacterModules(player, player.Character)
end
end
gatherModules()
initModules()
Players.PlayerAdded:Connect(handlePlayer)
Players.PlayerRemoving:Connect(cleanupPlayer)
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(handlePlayer, player)
end