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

Static Assets

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: server-client/start-client.js (onStartServer emit site), plugins_default/legacyshellcore/index.js (real example)

If your plugin needs to serve whole files - 3D models, textures, sounds, extra HTML pages - rather than inject inline JS, you don't need Client-Side Code's build-time splicing. Just mount your own folder as a normal Express static route.

The hook

client:onStartServer fires once the client server's Express app has just been created, before it starts listening:

this.plugins.on('client:onStartServer', this.onStartServer.bind(this));
async onStartServer(data) {
    let app = data.app;
    app.use(express.static(path.join(this.thisDir, 'client')));
};

That's the entire pattern - data.app is the live Express instance, express.static(...) is the same middleware any Express app uses to serve a folder verbatim. This exact snippet (usually pointed at a client/ subfolder inside the plugin's own directory) is used throughout the first-party plugins in this codebase - legacyshellcore, parkourmode, 5_crackshot, timemachine, fancygraphics, whatsapptheme, and the third-party plugins/_mcblocks all follow it.

Folder convention

There's no requirement to name your assets folder client/ - it's just the convention every existing plugin uses, chosen to make clear "this is what gets served to browsers" as distinct from server-only files sitting elsewhere in the same plugin folder (item definitions, maps, plugin logic). A typical layout:

plugins/yourplugin/
  index.js
  client/            <- mounted as a static route, contents served verbatim
    models/
      custom.babylon
    textures/
      skin.png
  items/              <- NOT served to the browser - read server-side by a content-pack hook instead, see below
    ...

Files end up served at the site root

express.static mounts the folder's contents starting from wherever app.use was called - here, with no path prefix, that's the site root. A file at plugins/yourplugin/client/models/custom.babylon becomes reachable at http://localhost:13370/models/custom.babylon, indistinguishable from any other static asset the base game serves. This is convenient (no extra path segment for the browser code that references it to know about) but also means plugins share one flat namespace - two plugins serving a file at the same relative path will silently conflict, whichever plugin's onStartServer listener happened to register first (or last, depending on how Express resolves overlapping static middlewares) wins. Keep your asset paths reasonably unique to your plugin to avoid this.

When you actually want this vs. content packs

Serving raw files is only half of "adding models" - the game also needs to know an item/map references that file, which is a database-level concern, not a static-serving one. If you're adding new items, hats, stamps, or maps (not just raw file hosting), see Content Packs for the other half of that story - client:onStartServer for the files, plus services:initTablesBefore/initTablesMaps for registering them.

Next: Content Packs.


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
Client-Side Code
Next
Content Packs