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

Content Packs

Audience: Plugin authors · Prereqs: Static Assets

Canonical source: server-services/start-services.js (the initTables pipeline), server-services/src/data_management/recordsManagement.js (insertItems, insertMaps), plugins_default/legacyshellcore/index.js (the real reference implementation)

A "content pack" plugin ships new items, maps, and/or models - legacyshellcore (the maintainer's own catalog of custom content for the public instance) is the canonical example, and worth reading in full once this page makes sense. This is the pattern the README refers to when it says purely cosmetic content should go in a plugin rather than the base game.

Items

Default items are loaded from .js files in server-services/src/items/ - one file per weapon/category, each exporting a default object keyed by category name, containing an array of item definitions:

// server-services/src/items/CSG1.js (a real default file, shown for shape reference)
export default {
    "CSG1": [{
        "meta_id": 0,
        "id": 3400,
        "name": "CSG1",
        "price": 0,
        "item_type_id": 3,
        "item_type_name": "Primary",
        "category_name": "Ranger Primary Weapons",
        "exclusive_for_class": 2,
        "item_data": { "class": "CSG1", "meshName": "gun_csg1", "tags": ["DefaultUnlocks"] },
        "is_available": false
    }, /* ... */]
};

Your plugin ships its own directory of files in this same shape, and calls ss.recs.insertItems(yourDir) to load them - on services:initTables, not initTablesBefore:

this.plugins.on('services:initTables', this.initTables.bind(this));

async initTables(data) {
    await data.ss.recs.insertItems(path.join(this.thisDir, 'items'));
};

Use initTables, not initTablesBefore, for your own items

It's tempting to reach for initTablesBefore since the name suggests "before the defaults load" - but default items only ever get (re)inserted when the items table is completely empty (a genuinely fresh database), so initTablesBefore only fires once, ever, on a brand-new install. initTables fires on every single boot, after the empty-table check either ran or didn't - that's why legacyshellcore (the real reference implementation) uses it, and why your own item pack should too. This is the difference between your items appearing once and then never again after a database wipe, versus appearing reliably every time services starts.

Meta ID ranges

Item meta_ids share one numeric space across the whole game, so collisions are a real risk. The convention documented directly in the default item files:

RangeReserved for
0 - 10,000Original Shell Shockers items
10,000 - 20,000legacyshellcore
20,000+Fair game for other plugins

Pick a range for your plugin and stay inside it - there's no enforced uniqueness check beyond what SQLite's primary key does, so an accidental collision silently overwrites another plugin's item rather than erroring.

Maps

Default maps are .json files (in the actual Shell Shockers map-editor export format) in server-services/src/maps/, reloaded from scratch on every services boot (the entire maps table is deleted and rebuilt every time, no empty-check gate like items has). Ship your own directory of map JSONs and call ss.recs.insertMaps(yourDir) on services:initTablesMaps, which fires right after the defaults have been reloaded:

this.plugins.on('services:initTablesMaps', this.initTablesMaps.bind(this));

async initTablesMaps(data) {
    await data.ss.recs.insertMaps(path.join(this.thisDir, 'maps'));
};

Because this whole table is rebuilt every boot, there's no "only runs once" gotcha here the way there is for items - initTablesMaps is unconditionally the right hook.

Models

New 3D content (guns, hats, map blocks) needs actual .babylon model files merged into the game's model set, on both the client server (build-time) and the game server (runtime, for server-side collision meshes) - hence these hooks fire under both client: and game: prefixes identically:

this.plugins.on('client:prepareBabylonBefore', this.prepareBabylonBefore.bind(this));
this.plugins.on('game:prepareBabylonBefore', this.prepareBabylonBefore.bind(this));
this.plugins.on('client:prepareBabylon', this.prepareBabylon.bind(this));
this.plugins.on('game:prepareBabylon', this.prepareBabylon.bind(this));

async prepareBabylonBefore(data) {
    // list your own .babylon files so the build knows they exist
    const babylonPath = path.join(this.thisDir, 'models');
    this.babylonFiles = fs.readdirSync(babylonPath);
    for (const file of this.babylonFiles) {
        data.baseBabylons.push({ filename: file.replace('.babylon', ''), /* ...metadata... */ });
    };
};

async prepareBabylon(data) {
    // supply the actual file path when asked for a specific one
    for (const file of this.babylonFiles) {
        if (data.filename + ".babylon" === file) {
            data.extraBabylons.push({ /* path, overwrite/location metadata */ });
        };
    };
};

This two-step handshake (declare filenames first, then resolve them to actual paths when asked) is how the build step merges base models with every plugin's extra models into one final set. If you're only adding a genuinely new model (not modifying an existing one), see Dealing with Babylon Models first for how to actually produce a .babylon file - this page only covers wiring an existing one into the plugin/build system.

Serving the files themselves

None of the hooks above make your models//items//maps/ folders reachable over HTTP - that's a separate, simpler step covered in Static Assets (client:onStartServer + express.static). A typical content-pack plugin uses both: static-serving for the raw files the browser downloads, and the hooks on this page for telling the database/build pipeline those files exist.

Common Issues

My items appeared once and then vanished after I restarted services. You're hooking initTablesBefore - switch to initTables (see the warning above).

Two plugins' items are stomping on each other. Check for a meta_id range collision - see the table above.

Next: Networking.


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