Checking player input#
Anything a client sends can be anything. A remote's accepts already refuses the wrong kinds (see
What a message may carry), but it cannot tell a speed of 5
from a speed of 5000, or a name of 12 letters from one of 12000. @moud/validate checks the rest on
the server: ranges, lengths, choices and the shape of tables.
A shop is the usual case. The client says what it wants to buy, and the server decides everything else: whether the item exists, what it costs and whether the player can pay.
-- server
local v = require("@moud/validate")
local SHOP = {
sword = { price = 50, item = "minecraft:iron_sword" },
bow = { price = 80, item = "minecraft:bow" },
}
local names = {}
for name in SHOP do
table.insert(names, name)
end
local buy = game.world:add("Remote", { name = "buy", accepts = "string" })
game.players.joined:connect(function(player)
player.leaderstats:add("NumberValue", { name = "Coins", value = 100 })
end)
buy.onServerPlayer:connect(v.guard({ v.oneOf(names) }, function(player, name)
local coins = player.leaderstats:find("Coins") :: NumberValue
local entry = SHOP[name]
if player.character == nil or coins.value < entry.price then
return
end
coins.value -= entry.price
player.character.backpack:add("Tool", { name = name, item = entry.item })
end))- The client only ever sends a name. The price comes from
SHOPon the server, so a changed client cannot buy a sword for nothing. v.oneOf(names)drops any name that is not in the shop before the handler runs, soSHOP[name]is never nil.
v.guard(checks, handler) hands back a function for onServer or onServerPlayer. It checks the
arguments after the sender against checks, in order, and calls handler only when all of them pass.
A call that fails is dropped and warned about, once per sender, so a client spamming bad calls does
not flood the output.
Checks#
Each check is a function that takes a value and hands back true, or false and the reason.
| Check | Passes |
|---|---|
v.any() |
anything but nil |
v.boolean() |
true or false |
v.number({ min, max, integer, allowInfinite }) |
a number in range. NaN never passes, and infinity only with allowInfinite |
v.integer({ min, max }) |
a whole number in range |
v.string({ min, max, pattern, allowEmpty }) |
text, its length counted in characters. Empty text only with allowEmpty or min = 0. Invalid UTF-8 never passes |
v.oneOf({ ... }) |
one of the values listed |
v.literal(value) |
exactly that value |
v.vector3({ maxMagnitude }) |
a finite Vector3, no longer than maxMagnitude |
v.instance(className?) |
an instance that still exists, of that class when one is given |
v.typeOf(name) |
a value whose typeof is name, like "CFrame" |
v.optional(check) |
nil, or what check passes |
v.union(a, b, ...) |
what any of the checks passes |
v.list(check, { min, max }) |
a list without holes, every item passing check |
v.map(keyCheck, valueCheck, { max }) |
a table whose keys and values pass |
v.shape({ field = check }, { extra }) |
a table with these fields, and no others unless extra is true |
Checking a table#
A guard drops a bad call quietly. When you want to know why, or answer the player, run the checks
yourself. A loadout a menu sends as one table is checked with v.shape:
-- server
local v = require("@moud/validate")
local loadouts = store("loadouts")
local saveLoadout = game.world:add("Remote", { name = "saveLoadout", accepts = "table" })
local LOADOUT = v.shape({
primary = v.oneOf({ "rifle", "shotgun", "smg" }),
perks = v.list(v.string({ max = 20 }), { max = 3 }),
})
saveLoadout.onServerPlayer:connect(function(player, choice)
local ok, why = v.check(LOADOUT, choice)
if not ok then
warn(player.name, "sent a bad loadout:", why)
return
end
loadouts:set(player.id, choice)
end)v.check(check, value)runs one check and hands backtrue, orfalseand the reason.v.args(...)makes one function that checks a call's arguments in order, the same wayguarddoes. More arguments than checks fails too.
The reason names what was wrong and where: perks: item 3: expected at most 20 characters, got 31.
Keep it for your logs. A player who sent it on purpose does not need to be told.
