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: Rewarding Players with Currency

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: server-game/src/client.js (addEggsViaServices), server-services/start-services.js (addEggs)

Giving a player eggs (LegacyShell's in-game currency) from a game-server plugin - a custom kill bonus, an event reward, a command an admin can run. This is the exact same request the game server itself already makes for real kills, reused from plugin code.

The existing, real call this recipe mirrors

ClientConstructor.addEggsViaServices (server-game/src/client.js:682-692) is what actually credits a player after a kill:

// server-game/src/client.js - real code
async addEggsViaServices(eggAmount) {
    if (this.session && this.session.length > 0) {
        var response = await wsrequest({
            cmd: "addEggs",
            session: this.session,
            eggAmount,
        }, ss.config.game.services_server, ss.config.game.auth_key);

        this.sendUpdateBalance(response.currentBalance);
    };
};

wsrequest (#wsrequest, src/shell/general/wsrequest.js) opens a one-off authenticated WebSocket request to services and resolves with its JSON response - the same mechanism requestConfig polling uses (see Architecture Overview), just for a single command instead of a poll loop. services_server/auth_key come straight from game.yaml (see Adding Game Servers) - the same credentials that authorize this game server to talk to services at all.

The plugin

A command that rewards the calling player with a fixed number of eggs:

// plugins/eggreward/index.js
import wsrequest from '#wsrequest';
import { ss } from '#misc';
import log from 'puppylog';

export const PluginMeta = {
    identifier: "eggreward",
    name: 'Egg Reward Command',
    author: 'you',
    version: '1.0.0',
    descriptionShort: 'Adds a /reward eggs command.',
    descriptionLong: 'A minimal example of crediting a player with currency from a game-server plugin.',
    legacyShellVersion: 598,
};

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

        if (plugins.type !== "game") {
            log.orange(`${PluginMeta.identifier} won't run on this server type.`);
            return;
        };

        this.plugins.on('game:permissionsAfterSetup', this.onPermissionsAfterSetup.bind(this));
    };

    onPermissionsAfterSetup(data) {
        var ctx = data.this;

        ctx.newCommand({
            identifier: "rewardeggs",
            name: "reward",
            category: "reward",
            description: "Reward yourself 100 eggs.",
            permissionLevel: [ctx.ranksEnum.Guest, ctx.ranksEnum.Guest, false],
            inputType: [],
            executeServer: async ({ player }) => {
                await this.rewardEggs(player.client, 100);
                ctx.room.notify("Rewarded 100 eggs!", 5);
            },
        });
    };

    async rewardEggs(client, eggAmount) {
        if (!client.session || client.session.length === 0) return; // guests have no session, nothing to credit
        var response = await wsrequest({
            cmd: "addEggs",
            session: client.session,
            eggAmount,
        }, ss.config.game.services_server, ss.config.game.auth_key);

        client.sendUpdateBalance(response.currentBalance);
        return response.currentBalance;
    };
};

See Commands for the full newCommand contract this uses - the executeServer shape and permissionLevel tuple aren't specific to this recipe.

The guest check isn't optional

client.session is only set for a logged-in account (see Making an Account) - a guest player has no session at all. addEggsViaServices guards on this.session && this.session.length > 0 before ever making the request, and this recipe copies that guard for the same reason: sending session: undefined to services would either fail the request or, worse, resolve against whatever undefined happens to coerce to - always check first rather than relying on services to reject a malformed request cleanly.

eggAmount can be negative, and is scaled by the active multiplier

Services applies the request almost completely unvalidated - server-services/start-services.js's addEggs handler does userData.currentBalance += (msg.eggAmount * eggMultiplier). Two consequences: a negative eggAmount deducts currency (useful for a "buy" mechanic outside the normal shop flow, dangerous if a bug lets a player influence the value you send), and whatever number you pass gets multiplied by services' currently-active eggMultiplier (an event-driven global boost, separate from anything this recipe controls) - the amount a player actually receives may be more than what you requested.

What we validated

Loaded against a real (isolated, scratch) services + game server pair: the plugin loads cleanly with no errors on both the main thread and the spare room worker, and the /reward command registers without throwing (confirming the newCommand call shape and ranksEnum usage are correct). We did not execute a full end-to-end request with a real logged-in player as part of writing this recipe - addEggsViaServices is the exact, currently-shipping code path this plugin reuses (same cmd, same wsrequest call shape, same config keys), so the actual credit-a-real-account flow rests on tracing that real call site rather than a fresh live test of it.

Common Issues

The command runs but nothing happens, no error either. Confirm the calling player is actually logged in (guests never have a session, see above) and that ss.config.game.auth_key on this game server matches a row in services' game_servers table (see Adding Game Servers) - an unauthorized auth_key gets your request rejected by services' rate limiter rather than processed.

Balance updates on the server but the player's UI doesn't refresh. You skipped client.sendUpdateBalance(response.currentBalance) - services updating its own database doesn't push anything back to the browser by itself; the game server has to explicitly relay the new balance down to that client's own connection.


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: Persistent Plugin Storage
Next
Recipe: Custom Per-Player Data