CCCPaste

ServicesLoader (Module Loader)

--[[
    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