Skip to content

Chat and commands

import { command, arg, CommandError, Commands } from "@sagl/server";

Every player has a chat, as SA-MP's: the message log top-left, and an input line, opened with T or F6. Enter sends it, Escape closes it, Up/Down go back through what they sent; Ctrl+C, Ctrl+X, Ctrl+V and Ctrl+A copy, cut, paste and select all. While it's open, PageUp/PageDown and the mouse wheel scroll back through the log (its last 100 lines). Messages (player.send, server.broadcast) colour parts of themselves with {RRGGBB}, as SA-MP's.

/ opens it with the slash already typed. A line is either chat or a /command. Typing a command, the chat suggests as Minecraft's does: the commands they can use, how each is used (the argument being typed marked), and words for it (subcommands, choices, player names, your own suggestions). Tab takes the suggestion, Up/Down choose another. Commands are listed alphabetically, except the last ten they used, which come first, newest first (remembered between sessions).

Chat

What a player says goes to everyone as server.chat.format makes it (default Name: text, the name in player.color). playerChat sees it first:

server.on("playerChat", (player, text) => {
  // Local chat, as roleplay servers do: only those near hear it.
  for (const p of server.players) {
    if (p.isNear(player.position, 20)) p.send(`${player.name} says: ${text}`, 0xe6e6e6);
  }
  return false;   // not to everyone
});

Return false to drop the line, or a string to send instead of their text.

server.chat Default
format(player, text) "{RRGGBB}Name{FFFFFF}: text" A line as everyone sees it.
floodLines 5 The flood limit: a burst of this many lines...
floodPerSecond 1 ...then this many a second. Past it: "You're sending messages too fast."

The player's own commands

Every player's chat has a few of its own, handled on their game (as SA-MP's client commands), and suggested like the rest:

Command
/quit, /q Leaves the game.
/pagesize <5-30> How many chat lines show (remembered, in sagl.ini).
/timestamp Times on chat lines, on or off (remembered).
/clear Empties their chat.
/save [note] Their position, heading and interior, written to sagl/savedpositions.txt as code to paste ({ x: ..., y: ..., z: ... }, heading): for building gamemodes.

A command of the server's or a client script's with the same name takes its place.

Clearing it

player.clearChat() empties one player's log, server.clearChat() everyone's; a client script's sagl.chat.clear() its own player's.

Commands

server.commands.register(...commands)

Commands are built up, as Minecraft's Brigadier: a name, arguments, subcommands, who may use it, and what it does.

server.commands.register(
  command("pay").description("Give money to a player").alias("give")
    .argument("player", arg.player())
    .argument("amount", arg.integer({ min: 1 }))
    .executes((player, { player: to, amount }) => {
      if (player.money < amount) throw new CommandError("You don't have that much");
      player.money -= amount;
      to.money += amount;
    }),

  command("me").description("An action").argument("action", arg.text())
    .executes((player, { action }) => server.broadcast(`{C2A2DA}* ${player.name} ${action}`)),

  command("faction").requires((p) => p.data.leader === true)
    .then(command("invite").argument("player", arg.player()).executes((p, { player }) => { ... }))
    .then(command("kick").argument("player", arg.player()).argument("reason", arg.text(), { optional: true })
      .executes((p, { player, reason }) => { ... })),
);
CommandBuilder
command(name) Starts one.
.alias(...names) Other names for it.
.description(text) Shown with its usage in the suggestions.
.requires(fn) fn(player): who may use it, and see it suggested.
.argument(name, type, { optional? }) The next argument (optional ones last; arg.text() takes the rest of the line).
.then(sub) A subcommand: /faction invite ....
.executes(fn) fn(player, args, line): args by name. Throw CommandError to tell them what's wrong (their usage follows).

Wrong usage is answered for you, as Minecraft's: what's wrong ("Missing amount", "x" isn't a whole number, More than one player matches "tom"), then how it's used (Usage: /pay <player> <amount>).

Argument Takes Gives
arg.player() an id, a name, or the start of one (one match) Player
arg.integer({ min?, max? }) a whole number number
arg.number({ min?, max? }) a number number
arg.word() one word string
arg.text() the rest of the line string
arg.boolean() yes/no, on/off, true/false boolean
arg.choice([...]) one of these words (suggested) the word
arg.vehicle() a vehicle's id Vehicle
arg.custom({ label, parse, suggest? }) yours: parse(input, player) (throw CommandError to refuse), suggest(player, partial) the words the chat suggests (asked of the server as they type) what parse returns
const job = arg.custom({
  label: "job",
  parse: (s) => { if (!JOBS.includes(s)) throw new CommandError(`No job "${s}"`); return s; },
  suggest: (player, partial) => JOBS.filter((j) => j.startsWith(partial)),
});
server.commands
register(...commands) Adds them; players' chats suggest them at once.
unregister(name) Removes one.
refresh(player) Sends their chat the commands they can use now: after what requires checks changes (an admin level, a job).

Old-style commands

A /command no registered command is called goes to playerCommandText with the whole line, as SA-MP's OnPlayerCommandText: return true if you handled it; otherwise they're told it's an unknown command. A port can start there and move commands over one at a time.

server.on("playerCommandText", (player, text) => {
  const [cmd, ...params] = text.split(" ");
  if (cmd === "/kill") { player.health = 0; return true; }
});

Client scripts

Client scripts have the chat too (sagl.chat) and commands of their own (sagl.commands), built the same way, suggested with the server's, run on their game.