Skip to content

Dispatcher

luau
local Operator = require(game:GetService("ServerStorage").Operator)
local Dispatcher = Operator.Dispatcher

Turns raw text into a run command. It takes the registry as a parameter rather than requiring it, so it has no opinion about where commands came from.

Functions

Dispatcher.new

luau
Dispatcher.new(registry: Registry, options: Options?): Dispatcher
luau
local dispatcher = Dispatcher.new(registry, {
	logger = Log.new(function(level, message) print(level, message) end),
	confirmHandler = confirm,
	guardTimeout = 5,
})

Methods

:run

luau
dispatcher:run(text: string, executor: Player?): Result

Tokenizes, parses, binds, transforms, runs the guards, then runs the handler. Omit executor when the server itself is running the command.

Path resolution prefers the longest match, so registering both a and a b means a b thing runs a b with thing as its argument.

luau
local result = dispatcher:run("kick @me being rude", player)
if not result.ok then
	warn(result.message)
end

:addGuard

luau
dispatcher:addGuard(guard: GuardFn, pathPrefix: { string }?): ()

Adds a guard. With no prefix it is global; with { "admin" } it applies to every command under that group.

Global guards run first, in registration order, then the command's own :guard(...) chain in declaration order.

luau
dispatcher:addGuard(function(context)
	if context.executor == nil then
		return true
	end
	return false, "moderators only"
end, { "admin" })

Guards fail closed

A guard that raises or exceeds guardTimeout denies the command rather than letting it through, and the real reason is logged.

:setConfirmHandler

luau
dispatcher:setConfirmHandler(handler: ConfirmHandler?): ()

Installs the handler that context:confirm and :destructive() call. Pass nil to remove it. Destructive commands are then cancelled rather than executed.

Types

Options

luau
type Options = {
	logger: Log.Logger?,
	confirmHandler: ConfirmHandler?,
	guardTimeout: number?,
	authorize: Authorize?,
	audit: Audit?,
	actionLog: ActionLog?,
}
FieldDefaultMeaning
loggersilenta Logger
confirmHandlernoneanswers destructive confirmations
guardTimeout5seconds before a yielding guard is denied
authorizenone(executor, command) -> boolean, usually roles:canRun
auditnonean Audit to record every dispatch into
actionLognonean ActionLog for moderation actions from context:logAction

Result

luau
type Result = {
	ok: boolean,
	status: Status,
	message: string?,
	path: { string },
	jobId: string,
	replies: { string },
	diagnostics: { any },
}
FieldMeaning
okwhether the command ran and reported no error
statusone of the Status values
messagethe first reply, or the reason it failed
paththe resolved command path
jobIdthe server's game.JobId, always server-generated
repliesevery message the handler sent
diagnosticsparse and binding diagnostics, for inline validation in a UI

Status

luau
Dispatcher.Status = {
	Ok = "Ok",
	NotFound = "NotFound",
	Invalid = "Invalid",
	Denied = "Denied",
	Cancelled = "Cancelled",
	Failed = "Failed",
	Errored = "Errored",
	ExecutorLeft = "ExecutorLeft",
}
StatusWhen
Okthe handler ran and reported no error
NotFoundno command matched the input
Invalidthe input failed to parse, bind, or transform
Denieda guard refused
Cancelleda destructive command was not confirmed
Failedthe handler called context:error(...)
Erroredthe handler raised
ExecutorLeftthe player left partway through

A handler that raises is caught: the caller gets a generic the command failed and the real error goes to the logger, never to the player.

NotFound, not Denied, over the wire

A command the player is not allowed to run comes back from the transport as NotFound, so probing the remote with guessed names cannot enumerate commands the player was never shown. A direct dispatcher:run on the server reports Denied normally.

Authorize

luau
type Authorize = (executor: Player, command: Command) -> boolean

Released under the MIT Licence.