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

Client-Side Code

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: server-client/src/prepare-modified.js (pluginSourceInsertion handling, lines ~88-110)

The browser game is one statically-built file (shellshock.min.js), assembled at build time from src/shell/*.js plus a handful of other pieces (see Codebase Reference once you're there for the full build pipeline). A plugin that needs to run code in the browser - modifying UI, reacting to gameplay events client-side, adding a keybind - can't just rely on a normal server-side import. Instead, it hooks into that build step directly.

The hook

On the client server only, server-client/src/prepare-modified.js emits client:pluginSourceInsertion while assembling the bundle, once per build:

this.plugins.on('client:pluginSourceInsertion', this.pluginSourceInsertion.bind(this));
pluginSourceInsertion(data) {
    data.pluginInsertion.files.push({
        insertBefore: '\nconsole.log("inserting before...");',
        filepath: path.join(this.thisDir, 'client.js'),
        insertAfter: '\nconsole.log("inserting after!");',
        position: 'before'
    });
};

You push a descriptor describing a file on disk (usually inside your own plugin folder) - not inline code - onto data.pluginInsertion.files. insertBefore/insertAfter are optional literal strings wrapped around your file's content (handy as visible markers in the built output while debugging).

Your file gets read, run through the exact same transform every src/shell/*.js file goes through (misc.prepareForClient - see Codebase Reference for the full mechanism) - meaning import/export lines are automatically commented out, and any //(server-only-start)///(server-only-end) blocks are stripped - and then concatenated into the final bundle. Practically: write plain browser JS, don't worry about stripping your own imports, and use the server-only-marker convention if the same file also needs to run server-side.

The three insertion positions

position must be one of three fixed anchor points in the built file:

PositionWhere in the bundleWhat's already defined there
beforebeforeThe very start of the file.Almost nothing from src/shell/ yet - only isClient/isServer and the plugins singleton itself are defined this early.
beforeRight after beforebefore, still near the top.Same as above - still before constants.js, comm.js, catalog.js, player.js, etc. are spliced in.
afterThe very end of the file, after the entire game script (including all shared modules) has run.Everything - safe to reference any shared game object directly.

beforebefore and before run before most shared modules exist

Both real sample plugins in this codebase (sample1cmd, sample2dependency) use position: 'before', and it works precisely because they only register event listeners at the top level - they never reference Comm, catalog, or other shared globals directly outside of a listener callback. If your inserted file tries to read something like catalog.someMethod() at the top level (not inside a function that runs later), and you've used before/beforebefore, it'll throw - those globals don't exist yet at that point in the concatenated script. Either move that logic inside a listener/callback (which only runs once everything has actually loaded), or use position: 'after' if you genuinely need top-level access to fully-loaded game systems.

The isClient guard

Since your file's imports get commented out but the rest of the code runs exactly as written, a file meant to be spliced into the browser bundle needs to guard any self-registration so it doesn't also try to run in contexts where it doesn't belong (most plugin authors write one shared file that's both imported normally server-side and injected into the client bundle):

import { isClient } from "#constants";

export const samplePlugin = {
    registerListeners: function (pluginManager) {
        this.plugins = pluginManager;
        this.plugins.on('game:startUp', this.startUp.bind(this));
    },
    startUp: function (data) {
        console.log("Client started up");
    },
};

if (isClient) samplePlugin.registerListeners(plugins);

The last line only runs client-side (isClient is true in the browser, false on any Node server) - this is what stops the same file from double-registering if it's ever imported server-side too. Note the bare plugins reference here (not this.plugins or an import) - inside the spliced bundle, plugins is a global (see the table above: it's defined before both before and beforebefore), not something you import.

Common Issues

ReferenceError: X is not defined in the browser console, only in production builds. Almost always a beforebefore/before top-level reference to something not yet loaded at that point - see the warning above. Move the reference inside a callback, or switch to position: 'after'.

My changes don't show up after editing the file. The client bundle is only rebuilt when the client server (re)starts (see Codebase Reference for the build pipeline) - there's no hot reload for this, restart npm run client after every change.

I just want to serve a whole folder of static assets (models, images, extra HTML pages), not inject inline JS. You don't need pluginSourceInsertion for that - see Static Assets instead.

Next: Static Assets.


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
Commands
Next
Static Assets