LegacyShell WikiLegacyShell Wiki
Back to LegacyShell
Home
Wiki
Plugins
Docs
Back to LegacyShell
Home
Wiki
Plugins
Docs
  • Documentation

    • Getting Started

      • What is LegacyShell?
      • Speed Setup
      • Requirements
      • Installation
      • First Run
      • Config Files
      • Making an Account
      • Troubleshooting (Getting Started)
    • Running a Server

      • Architecture Overview
      • The Database
      • Users and Ranks
      • Adding Game Servers
      • Client Mirrors
      • Perpetual
      • Backups
      • Rate Limiting
      • Moderation
      • Closed Mode
      • Deployment
      • Troubleshooting (Running a Server)
      • Hosting for Someone Else's Instance
    • Content Creation

      • Maps
      • Dealing with Babylon Models
      • Map Blocks
      • Items and Skins
      • Hats and Stamps
      • Sounds
      • Gamemodes
      • Seasonal Events
    • Plugin Development

      • Quickstart
      • I Want To...
      • Anatomy of a Plugin
      • Lifecycle
      • Dependencies
      • Events (Concept)
      • Event Reference

        • services: events
        • game: events — shared logic (src/shell/)
        • game: events — main-thread server process
        • game: events — per-connection client object
        • game: events — room lifecycle & tick loop
        • game: events — in-browser gameplay
        • client: events — client server & build pipeline
      • Commands
      • Client-Side Code
      • Static Assets
      • Content Packs
      • Networking
      • Workers and State
      • Prediction and Authority
      • Recipes

        • Recipe: Killstreaks
        • Recipe: New Pickup Item
        • Recipe: New Gamemode
        • Recipe: Custom Weapon
        • Recipe: UI Modification
        • Recipe: Discord Integration
        • Recipe: Replacing Core Behaviour
        • Recipe: Persistent Plugin Storage
        • Recipe: Rewarding Players with Currency
        • Recipe: Custom Per-Player Data
        • Recipe: Custom Theme
      • Publishing
      • Pitfalls
      • Modifiers
      • Sound and Apollo
    • Codebase Reference

      • Repo Layout
      • Shared Shell Layer
      • Server-Only Markers
      • The ss Object
      • Build Pipeline
      • Stamps and Babylons
      • Game Loop
      • Rooms and Workers
      • Wire Protocol
      • Generated

        • Wire Protocol Opcodes
        • Enums & Lookup Tables
        • Database Schema
        • Config Reference
        • Slash Command Reference
      • Services Internals
      • Catalog and Items
      • Permissions Internals
      • Physics and Collision
      • Known Quirks
      • Codebase Anecdotes
      • Development Timeline
    • Contributing

      • Documentation Style Guide
      • Generators
      • For AI Agents

Commands

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: src/shell/permissions.js (PermissionsConstructor, Command)

LegacyShell's slash-command system (/room lock true, /change speed 1.5, etc.) isn't a separate plugin API - it's built entirely on the event mechanism from Events (concept), plus one method: newCommand. This is probably the single most common thing a new plugin actually does, so it gets its own page.

The hook

PermissionsConstructor registers all of LegacyShell's ~20 built-in commands in its own constructor, then, as the very last thing it does, emits:

plugins.emit('permissionsAfterSetup', { this: this });

That's your entry point. Register a listener for game:permissionsAfterSetup, and data.this gives you the live permissions instance for the current room (server-side) or the client-side equivalent (in the browser) - either way, it has a newCommand method and a ranksEnum lookup ready to use.

Registering a command

The full, real example from plugins_samples/sample1cmd/:

import { isClient } from "#constants";

export const samplePlugin = {
    registerListeners: function (pluginManager) {
        this.plugins = pluginManager;
        this.plugins.on('game:permissionsAfterSetup', this.registerSampleCommand.bind(this));
    },

    registerSampleCommand: function (data) {
        var ctx = data.this;

        ctx.newCommand({
            identifier: "sampletest",
            name: "test",
            category: "sample",
            description: "Test command.",
            example: "just run it",
            permissionLevel: [ctx.ranksEnum.Moderator, ctx.ranksEnum.Guest, true],
            inputType: ["string"],
            executeClient: ({ player, opts, mentions }) => { },
            executeServer: ({ player, opts, mentions }) => {
                ctx.room.notify("You did it! Woohoo.", 5);
            }
        });
    },
};

if (isClient) samplePlugin.registerListeners(plugins);

Typed in-game, this becomes /sample test.

newCommand options

FieldWhat it does
identifierUnique key for this command (used for lookup, not shown to players).
nameThe subcommand name, e.g. "test" in /sample test.
categoryThe top-level group, e.g. "sample" in /sample test.
descriptionShown in the in-game command list.
exampleShown as usage help.
usageOptional - overrides the auto-generated usage string.
autocompleteOptional autocomplete trigger character, e.g. "@".
mentionTypesOptional, defaults to {player: true, group: true} - restricts which @mention kinds this command accepts (see below).
isCheatOptional, defaults to falsy - if true, additionally requires the room's gameOptions.cheatsEnabled flag, regardless of rank.
warningTextOptional extra text shown alongside the command.
permissionLevel[bypassRank, privateRoomRank, requireGameOwnerInPrivate] - see Users and Ranks for the full explanation of this tuple.
inputType["string"], ["bool"], or ["number", min, max, step] - governs both input parsing and the auto-generated usage text.
executeClient({ player, opts, mentions, mentionsLiteral }) => {...} - runs immediately, client-side, for responsiveness.
executeServerSame signature - runs when the server actually receives and validates the command.

Why both executeClient and executeServer

This mirrors the client-prediction pattern used throughout the shared game logic (see Prediction and Authority for the full treatment): a single Command.execute() call runs on both sides, but only invokes the half matching where it's currently running (isClient ? this.executeClient(...) : this.executeServer(...)). Client-side gives the player instant feedback; server-side is what's actually authoritative and gets sent to other players. Only registering executeServer means nothing visible happens until a round-trip to the server completes - fine for something that isn't latency-sensitive (like the sample above, which just posts a room notification), but noticeably laggy for anything a player expects to feel instant.

@mentions

Commands that operate on other players parse @-prefixed tokens out of the raw input automatically, before your executeClient/executeServer runs - you receive the parsed result as mentions (and mentionsLiteral, the raw un-resolved text), not raw string parsing you have to do yourself. The built-in mention kinds:

TokenResolves to
@aAll players in the room.
@tYour own team.
@oThe opposing team.
@mYourself.
@usernameA specific player by name.

Restrict which kinds a command accepts with mentionTypes - e.g. { group: true } (seen on the built-in speed command) allows team/all/opposing-team mentions but not an individual @username, while the default {player: true, group: true} allows both.

Common Issues

My command doesn't do anything when typed. Check you actually implemented the half that matters for what you're testing - if you only wrote executeServer and expected to see something happen the instant you hit enter, that's expected; the visible effect only appears once the server's response comes back. Also double check category/name don't collide with a built-in command (change, admin, mod, room, rounds, time, weather are all already taken categories).

"Insufficient permissions" even though I think I should have access. Re-check the permissionLevel tuple against your actual rank and whether you're testing in a public or private room - the private-room allowance never applies in public rooms, regardless of rank. See Users and Ranks.

Next: Client-Side Code.


This page was drafted with AI assistance and reviewed for accuracy. If something looks wrong, please open a PR or flag it.

Edit this page on GitHub
Next
Client-Side Code