--[[
ServiceLoader Module
--------------------
Dynamically loads and manages lifecycle hooks (Start, Update, FixedUpdate, HeartbeatUpdate)
for services in a given folder.
Tags:
- Optional attributes:
- "Disabled" (boolean): skips loading the service
- `FixedFrames` (number): required if using FixedUpdate
Lifecycle Functions (optional in service modules):
- Start()
- Update(dt)
- FixedUpdate(dt)
- HeartbeatUpdate(dt)
--]]
local ServiceLoader = {}
function ServiceLoader.LoadServices(ServicesFolder: Folder)
assert(ServicesFolder and ServicesFolder:IsA("Folder"), "[ServiceLoader]: Invalid ServicesFolder")
local RunService = game:GetService("RunService")
local IsServer = RunService:IsServer()
local UpdateEvent = IsServer and RunService.Heartbeat or RunService.RenderStepped
local FixedUpdateEvent = RunService.PreSimulation
local HeartbeatEvent = RunService.Heartbeat
local runningUpdates = {}
local runningFixed = {}
local runningHeartbeat = {}
local function safeCall(fn, ...)
local ok, res = pcall(fn, ...)
if not ok then
warn("[ServiceLoader]: Error in service callback:", res)
end
return ok, res
end
print("[ServiceLoader] Loading all services from:", ServicesFolder:GetFullName())
for _, module in ipairs(ServicesFolder:GetChildren()) do
if not module:IsA("ModuleScript") or module:GetAttribute("Disabled") then
continue
end
task.spawn(function()
local success, service = pcall(require, module)
if not success then
warn(("[ServiceLoader] Failed to require '%s': %s"):format(module.Name, tostring(service)))
return
end
--// Call Start function
if type(service.Start) == "function" then
task.spawn(function()
local ok, err = pcall(service.Start)
if not ok then
warn(("[ServiceLoader] Start error in '%s': %s"):format(module.Name, tostring(err)))
end
end)
end
--// Register Update
if type(service.Update) == "function" then
table.insert(runningUpdates, {
fn = service.Update,
name = module.Name,
})
end
--// Register FixedUpdate (with interval)
if type(service.FixedUpdate) == "function" and service.FixedFrames then
table.insert(runningFixed, {
fn = service.FixedUpdate,
interval = 1 / service.FixedFrames,
elapsed = 0,
})
end
--// Register HeartbeatUpdate
if type(service.HeartbeatUpdate) == "function" then
table.insert(runningHeartbeat, {
fn = service.HeartbeatUpdate,
name = module.Name,
})
end
end)
end
--// Update loop
local updateConnection = UpdateEvent:Connect(function(dt)
for _, entry in ipairs(runningUpdates) do
task.spawn(function()
safeCall(entry.fn, dt)
end)
end
end)
--// FixedUpdate loop
local fixedUpdateConnection = FixedUpdateEvent:Connect(function(dt)
for _, entry in runningFixed do
entry.elapsed += dt
if entry.elapsed >= entry.interval then
task.spawn(entry.fn, entry.interval)
entry.elapsed = 0
end
end
end)
--// HeartbeatUpdate loop
local heartbeatConnection = HeartbeatEvent:Connect(function(dt)
for _, entry in ipairs(runningHeartbeat) do
task.spawn(function()
safeCall(entry.fn, dt)
end)
end
end)
return {
UpdateConnection = updateConnection,
FixedUpdateConnection = fixedUpdateConnection,
HeartbeatConnection = heartbeatConnection,
}
end
return ServiceLoader