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

Recipe: Replacing Core Behaviour

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: server-client/src/prepare-modified.js

The plugins.cancel pattern, at its simplest: a plugin that tells core code to skip a default step entirely, with nothing else needed. This recipe cancels the client build's minification step - the smallest complete example of "replace," as opposed to "react to."

The pattern, in core code

// server-client/src/prepare-modified.js - real code
if (ss.config.client.minify) {
    await plugins.emit('minificationBefore', { this: this, ss, code, UglifyJS });

    if (!plugins.cancel) {
        var result = UglifyJS.minify(code.sourceJs);
        // ...applies result.code, throws on result.error...
    };
};

Every "replace this default behavior" hook in the codebase follows this exact shape: emit, then an if (!plugins.cancel) guard around whatever the default would otherwise do. See Events (concept) for the mechanism itself, and the Event Reference for every other hook that follows this same pattern (sendToAll for default packet fan-out, clientSyncLoop for default position data, and others).

The plugin

// plugins/skipminify/index.js
import log from 'puppylog';

export const PluginMeta = {
    identifier: "skipminify",
    name: 'Skip Minification',
    author: 'you',
    version: '1.0.0',
    descriptionShort: 'Cancels the default minification step entirely.',
    descriptionLong: 'A minimal example of plugins.cancel fully replacing (in this case, removing) a default build behavior.',
    legacyShellVersion: 598,
};

export class Plugin {
    constructor(plugins, thisDir) {
        this.plugins = plugins;
        this.thisDir = thisDir;

        this.plugins.on('client:minificationBefore', this.onMinificationBefore.bind(this));
    };

    onMinificationBefore(data) {
        this.plugins.cancel = true;
    };
};

That's the entire plugin. No executeClient/executeServer split, no client-injected code - minificationBefore only ever fires on the client server's own build process (Node-side), so this is a normal server-side-only plugin.

From "cancel" to "replace"

Cancelling alone just removes the default behavior - code.sourceJs is left exactly as it was before the cancelled step. To actually replace it with something else (a different minifier, a custom obfuscation pass), do the real work inside the same handler, using the payload you were given, before setting cancel:

onMinificationBefore(data) {
    data.code.sourceJs = someOtherMinifier.minify(data.code.sourceJs);
    this.plugins.cancel = true;
};

data.code is the same mutable object prepare-modified.js reads from afterward - mutating data.code.sourceJs directly is how a listener hands back a result, there's no separate "return value" mechanism for an event listener (see Events (concept) - listeners don't return anything meaningful to the emitting code, they communicate through the payload object and plugins.cancel).

What we validated - and a real, useful side effect

We loaded this exact plugin against a real (isolated, scratch) client server that, in earlier testing for other recipes on this page, reliably hit a pre-existing minification failure (Legacy octal literals are not allowed in strict mode) on every build. With this plugin installed: the log clearly shows the cancellation firing (skipminify: cancelling default minification), and the minification error genuinely stopped occurring - concrete, observed confirmation that plugins.cancel = true inside minificationBefore does exactly what this page claims, not just in theory. The build's subsequent steps (stamp spritesheet compositing) were slow enough in our test environment that we didn't wait for a fully completed build, but the specific mechanism this recipe teaches - cancelling minification - was directly, empirically confirmed.

Common Issues

Cancelling didn't seem to have any effect. Confirm your listener registration actually happened before the emit runs - client:minificationBefore only fires once, during that one client server's build; if your plugin loaded after the build already started (shouldn't normally happen given plugins load before start-client.js even runs, but worth checking if something's unusual about your setup) the cancellation would be too late.

I replaced minification with my own, but the resulting bundle doesn't parse. Whatever you assign to data.code.sourceJs needs to be complete, syntactically valid JavaScript on its own - the same requirement the default UglifyJS.minify call has to satisfy.


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
Recipe: Discord Integration
Next
Recipe: Persistent Plugin Storage