11 Modules and configuration
Before you start and what you will change
Finish the working boost in Remotes and validation. Save a checkpoint copy of the place before refactoring. You will move tuning values into a ModuleScript and replace the existing BoostServer's contents with an equivalent implementation. The station, client UI, remote names, and collectible script remain as they are.
This is a small refactoring exercise with a deliberately narrow success criterion: players should observe the same behavior, while future tuning requires editing one clearly named file. A module is useful when it gives something one owner or a testable interface. Merely moving every ten lines into another file would not improve this example.
Understand the execution model
A ModuleScript returns one value to require, commonly a table of configuration or functions. Within one execution environment, subsequent requires reuse its result. A module required on the server and a client executes separately in those environments; mutable tables do not become a networked shared object. See Reuse code.
A ModuleScript in ServerScriptService is appropriate here because only the server consumes authoritative tuning. If a future interface needs to display duration or remaining cooldown, send the display data deliberately, or put genuinely shared non-secret definitions in ReplicatedStorage. Do not move secret keys or server-only logic into replicated storage to make a client require succeed.
Create the configuration module
Stop Play. Add a ModuleScript directly under ServerScriptService and name it BoostConfig. Keep the existing Script named BoostServer.
ServerScriptService
Collectibles existing Script
BoostConfig new ModuleScript
BoostServer existing Script, contents replaced below
Paste this complete file into BoostConfig:
Download 11-boost-config.module.luau
-- ModuleScript: ServerScriptService > BoostConfig
local Config = {
RequiredCoins = 3,
Radius = 12,
Speed = 28,
Duration = 5,
Cooldown = 20,
RequestInterval = 0.5,
}
local function finiteNumber(value)
return typeof(value) == "number"
and value == value and math.abs(value) < math.huge
end
for key, value in pairs(Config) do
assert(finiteNumber(value), key .. " must be a finite number")
end
assert(Config.RequiredCoins >= 0 and Config.RequiredCoins % 1 == 0,
"RequiredCoins must be a nonnegative integer")
assert(Config.Radius > 0 and Config.Radius <= 30, "Radius must be in (0, 30]")
assert(Config.Speed > 0 and Config.Speed <= 40, "Speed must be in (0, 40]")
assert(Config.Duration > 0, "Duration must be positive")
assert(Config.Cooldown >= Config.Duration, "Cooldown must cover Duration")
assert(Config.RequestInterval >= 0.1, "RequestInterval must be at least 0.1")
return table.freeze(Config)
The numeric checks reject NaN, infinity, strings, and values outside this tutorial's intended ranges. These limits are design choices for this small game, not universal Roblox limits. For example, Radius at most 30 keeps this station local; another design might reasonably use a different bound.
table.freeze stops ordinary writes to the returned table. It is shallow: if this table contained nested tables, freezing the outer table would not freeze those nested tables. All our fields are primitive numbers, so shallow freezing is sufficient. The relevant semantics are documented in the Luau standard library.
Replace the existing server implementation
Replace all the contents of the existing ServerScriptService.BoostServer with the file below. Do not add a second Script named BoostServerModular. 11-boost-server-modular.server.luau is the downloadable file's name, not an additional runtime object name. Keep the lesson 10 BoostClient unchanged.
Download 11-boost-server-modular.server.luau
-- Script: ServerScriptService > BoostServer
-- Lesson 11: replace the existing BoostServer contents with this whole file.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = require(script.Parent:WaitForChild("BoostConfig"))
local station = workspace:WaitForChild("BoostStation", 10)
assert(station and station:IsA("BasePart"), "Create Workspace.BoostStation Part")
assert(not ReplicatedStorage:FindFirstChild("BoostRemotes"), "Run only one BoostServer")
local remotes = Instance.new("Folder")
remotes.Name = "BoostRemotes"
local request = Instance.new("RemoteEvent")
request.Name = "RequestBoost"
request.Parent = remotes
local feedback = Instance.new("RemoteEvent")
feedback.Name = "BoostFeedback"
feedback.Parent = remotes
remotes.Parent = ReplicatedStorage
local lastRequest = {}
local nextAllowed = {}
local active = {}
local function reply(player, requestId, ok, message)
feedback:FireClient(player, requestId, ok, message)
end
request.OnServerEvent:Connect(function(player, action, requestId)
local now = os.clock()
if now - (lastRequest[player] or -math.huge) < Config.RequestInterval then
return -- Do not multiply a flood by sending a reply for every rejected request.
end
lastRequest[player] = now
if typeof(requestId) ~= "number" or requestId % 1 ~= 0
or requestId < 1 or requestId > 1000000000 then
return -- Correlation metadata must also be bounded and well-formed.
end
if typeof(action) ~= "string" or action ~= "speed" then
reply(player, requestId, false, "Unknown request")
return
end
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
local root = character and character:FindFirstChild("HumanoidRootPart")
if not humanoid or humanoid.Health <= 0 or not root or not root:IsA("BasePart") then
reply(player, requestId, false, "Respawn before requesting a boost")
return
end
local distance = (root.Position - station.Position).Magnitude
if not (distance <= Config.Radius) then -- Also rejects NaN distances.
reply(player, requestId, false, "Move closer to the green boost station")
return
end
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if not coins or not coins:IsA("IntValue") or coins.Value < Config.RequiredCoins then
reply(player, requestId, false, "Collect " .. Config.RequiredCoins .. " coins first")
return
end
local remaining = (nextAllowed[player] or 0) - now
if remaining > 0 or active[player] then
reply(player, requestId, false, "Boost cooling down; try again shortly")
return
end
-- No yielding between final validation and committing the state change.
nextAllowed[player] = now + Config.Cooldown
local token = {}
active[player] = token
local previousSpeed = humanoid.WalkSpeed
humanoid.WalkSpeed = Config.Speed
reply(player, requestId, true, "Boost active for " .. Config.Duration .. " seconds")
task.delay(Config.Duration, function()
if active[player] ~= token then return end
active[player] = nil
if player.Character == character and humanoid.Parent
and humanoid.WalkSpeed == Config.Speed then
humanoid.WalkSpeed = previousSpeed
end
if player.Parent == Players then
reply(player, requestId, true, "Boost ended; wait for the cooldown before reusing")
end
end)
end)
Players.PlayerRemoving:Connect(function(player)
lastRequest[player] = nil
nextAllowed[player] = nil
active[player] = nil
end)
Why validate configuration at startup
A mistaken negative duration or a cooldown shorter than the active effect can turn a harmless tuning edit into confusing behavior. Failing at startup with a specific assertion exposes the faulty input before a player reaches that branch. The module completes before the server creates remotes, so a rejected configuration leaves no partially working boost endpoint.
This is also a lesson about dependencies. BoostServer depends on BoostConfig. BoostConfig has no dependency back on BoostServer, does not connect to events, and does not spawn tasks. Keeping the module free of gameplay side effects makes it easier to reason about when the boost system begins operating. Circular require chains obscure initialization and should be removed rather than “fixed” with arbitrary waits.
As the project grows, a separate BoostService module could expose TryActivate(player) and return an explicit result. That would let a button and a proximity interaction use the same business rule. Do not implement that extra layer until there is a real second caller or testability benefit. Configuration extraction alone is enough for this lesson.
Verify unchanged behavior and intentional failures
Run all seven acceptance tests from lesson 10. The default configuration should produce identical thresholds, duration, and cooldown. Compare the source before and after: the key change is the require statement replacing the inline Config table.
Next, stop Play, change RequiredCoins to 2, and start a fresh test. Two collected coins should now qualify. Restore it to three afterward. Then temporarily set Cooldown to 2 while Duration is five. Startup should report Cooldown must cover Duration, and the boost UI may wait for remotes that were intentionally never created. Restore the valid configuration before continuing.
Finally, try adding Config.Speed = 999 immediately after require as a temporary test. The frozen table should reject the assignment. Remove this line. No source in this manual was executed in Studio during authoring; these are verification steps for your own place.
Troubleshooting
- Waits for BoostConfig: inspect the exact name and parent; it must be a sibling of BoostServer.
- Require returns the wrong thing: use a ModuleScript with a final
return, not a regular Script. - A tune appears unchanged: stop and start a new test after editing the source. Do not confuse a runtime copy or a cached require result with a fresh session.
- Duplicate remotes assertion: you added the replacement as another Script. Keep one BoostServer and remove the duplicate source object.
- Assertion after a tune: read the named constraint before changing validation. Decide whether the design really needs a new range.
Exercises
- Add a positive
FeedbackSecondssetting only when you have a concrete server use for it; otherwise explain why an unused option adds confusion. - Sketch a pure function
CanAfford(balance, required)with tests for below, equal, and above the threshold. Keep Roblox Instance access out of that function. - Explain why freezing a replicated module's client table would not protect the server from fabricated remote requests.
Sources and navigation
Last checked 2026-10-03: ModuleScript API, reuse and require behavior, Luau library and table.freeze.
Previous: 10 Remotes and validation · Next: 12 Co-op rounds
Related: 03 Luau for coders · 09 Client-server boundary · 16 Debugging and performance