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

Pitfalls

Audience: Plugin authors · Prereqs: Everything else in this section

Canonical source: src/shell/plugins.js

A condensed, skimmable list of the sharpest edges in the plugin API - things that fail quietly rather than loudly, or that only show up under specific conditions. Each links to the page with the full explanation.

plugins.cancel is one flag, not scoped per-listener

If two plugins listen to the same event and only one means to suppress the default behavior, the other still sees whatever plugins.cancel was last set to by the time it runs - there's no per-listener isolation. Only set it when you specifically mean to override the default for that emit, and know that plugin load order (alphabetical by PluginMeta.identifier) decides which listener's decision "wins" if two disagree. → Events (concept)

Not every event name that sounds like it should exist, does

There's no typed/enforced event registry - on(event, ...) silently accepts any string, including one that's never actually emitted anywhere. A listener registered against a nonexistent event just never fires, with no warning that anything's wrong. Always cross-check an event name against an actual plugins.emit(...) call site before relying on it - see the Event Reference or grep the source directly (grep -rn "plugins\.emit(" src/shell server-services server-game server-client). → Events (concept)

Room-worker plugin instances don't share state with the main thread, or each other

Your Plugin class is instantiated independently in the game server's main thread and separately inside every room's worker thread - these are different JS execution contexts with no shared memory. Storing what you think is "one global counter" on this inside a game:-targeting plugin actually gives you one separate copy per room. This is empirically confirmed, not theoretical - see the worked example. → Workers and State

A slow or blocking constructor delays every plugin loaded after it

Plugin instantiation happens sequentially, awaited one at a time, in alphabetical order by identifier - not in parallel. A constructor that does slow synchronous work (or an unawaited-but-blocking operation) doesn't just slow down your own plugin's boot, it delays every plugin sorted after it too. Keep constructors fast; do slow setup (large file reads, network calls) asynchronously in the background rather than blocking the constructor itself where possible. → Lifecycle

Content-pack items only persist if you hook the right event

initTablesBefore only ever fires on a genuinely empty items table (once, ever, on a fresh install) - a plugin relying on it alone for its own items will see them vanish after any database reset and never come back. Use initTables instead, which fires on every boot. → Content Packs

Git auto-pull means every restart is a potential silent update

Any plugin folder that's a git repository gets git pull'd automatically on every server boot. If you're actively developing a plugin locally and it also happens to track a remote (yours or someone else's), an unexpected restart can pull in changes you didn't intend to test yet. Know what remote (if any) your working copy is tracking while developing. → Lifecycle

beforebefore/before client injection runs before most shared game modules exist

If your client-side code injection references something like Comm or catalog at the top level (not inside a callback) and uses position: 'before' or 'beforebefore', it'll throw - those globals aren't defined that early in the built file. Either defer the reference inside a listener, or use position: 'after'. → Client-Side Code

Forgetting .bind(this) on a listener

A very ordinary JavaScript footgun, but common enough in plugin code to call out: this.plugins.on('game:someEvent', this.handler) (without .bind(this)) means this inside handler won't be your plugin instance when it's called later - it'll be whatever PluginManager.emit happens to call it with. Every real example in this codebase uses .bind(this) (or an arrow function closing over the right scope) for exactly this reason.

Only implementing one half of client/server logic

A command, weapon, or gameplay feature with only executeServer/an isServer branch works, but feels laggy (visible delay before anything happens on screen). One with only the client half isn't real - not authoritative, not visible to other players, trivially fakeable. → Prediction and Authority


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
Prev
Publishing
Next
Modifiers