Skip to content

First Plugin

Andrew R. edited this page Jul 24, 2026 · 1 revision

First Paper Plugin

This walkthrough creates a Paper plugin that logs lifecycle changes, observes joins through the current data-only event callback, and registers /shamoo-hello. It deliberately uses an exported class with no constructor dependencies because that is what the current bundled adapter can instantiate.

Complete source for a comparable minimal project is available in examples/hello-world. The richer examples/complete-paper-plugin demonstrates more declarations, but some generated JVM-typed event operations exceed the current data-only adapter.

1. Create the project

Run the CLI from an environment where all v0.1.0-rc.1 release tarballs were installed together as described in Installation:

pnpm exec shamoo create first-shamoo-plugin \
  --name @example/first-shamoo-plugin \
  --platform paper
cd first-shamoo-plugin

create does not run a package installer. The generated version ranges describe package compatibility but are not a claim that npm hosts those packages. Install all downloaded GitHub release tarballs into this project together:

pnpm add /absolute/path/to/shamoo-ts-v0.1.0-rc.1/*.tgz

2. Package manifest

The project needs @shamoo/decorators, the Paper facade, the generated Paper raw event declarations, the CLI, and TypeScript. A concise package.json is:

{
  "name": "@example/first-shamoo-plugin",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "shamoo build",
    "deploy": "shamoo deploy",
    "dev": "shamoo dev",
    "doctor": "shamoo doctor",
    "typecheck": "tsc --project tsconfig.json --pretty false"
  },
  "dependencies": {
    "@shamoo/decorators": "0.1.0-rc.1",
    "@shamoo/paper": "0.1.0-rc.1",
    "@shamoo/paper-raw": "0.1.0-rc.1"
  },
  "devDependencies": {
    "@shamoo/cli": "0.1.0-rc.1",
    "typescript": "5.8.3"
  }
}

Those versions identify the release. With the current distribution, the lockfile and usually the manifest must resolve them to the local .tgz files installed in the previous step. Do not replace them with registry fetches unless npm view <package>@0.1.0-rc.1 version has independently confirmed a deliberate publication.

3. TypeScript configuration

Parameter decorators require legacy decorator syntax because the current standard decorator proposal has no parameter decorators. This example has no parameter decorators, but enabling the option keeps the project ready for them:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "experimentalDecorators": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"]
}

Shamoo does not use emitDecoratorMetadata; leave it off.

4. Shamoo configuration

Create shamoo.config.json:

{
  "name": "@example/first-shamoo-plugin",
  "displayName": "First Shamoo Plugin",
  "version": "0.1.0",
  "platforms": ["paper"],
  "entrypoint": "src/plugin.ts",
  "paperEntrypoint": "src/paper.ts",
  "tsconfig": "tsconfig.json",
  "outDir": "dist",
  "permissions": {
    "builtins": [],
    "filesystem": { "read": [], "write": [] },
    "network": false,
    "workers": false,
    "childProcess": false,
    "nativeAddons": false
  },
  "communication": {
    "services": [],
    "events": [],
    "consumers": []
  },
  "deploy": {
    "paper": "/srv/paper/plugins/ShamooRuntime/plugins"
  }
}

Change deploy.paper to the actual ShamooRuntime watched root. The default is <paper-server>/plugins/ShamooRuntime/plugins, not <paper-server>/plugins.

5. Common plugin source

Create src/plugin.ts:

import { Command, Context, OnDisable, OnEnable, Plugin } from '@shamoo/decorators';
import type { PaperCommandContext } from '@shamoo/paper';
import { OnPlayerJoinEvent } from '@shamoo/paper-raw';

interface PaperEventPayload {
  readonly type: string;
  readonly asynchronous: boolean;
}

@Plugin({ name: 'first-shamoo-plugin' })
export class FirstShamooPlugin {
  private joins = 0;

  @OnEnable()
  public enable(): void {
    console.info('[first-shamoo-plugin] enabled');
  }

  @OnPlayerJoinEvent()
  public joined(@Context() event: PaperEventPayload): void {
    this.joins += 1;
    console.info(
      `[first-shamoo-plugin] ${event.type}; joins=${String(this.joins)}`,
    );
  }

  @Command('shamoo-hello')
  public hello(@Context() context: PaperCommandContext): boolean {
    return context.reply(`Hello ${context.sender.name}; observed joins=${String(this.joins)}`);
  }

  @OnDisable()
  public disable(): void {
    console.info('[first-shamoo-plugin] disabled');
  }
}

Why the event parameter is not typed as generated PlayerJoinEvent: the generated declaration accurately describes the pinned Paper API at compile time, but the current Runtime callback intentionally crosses only copied data. It supplies { type, asynchronous }, not a live Bukkit object. Calling event.getPlayer() in this deployed path would be incorrect. The command path has a supported data-only PaperCommandContext with reply, player lookup, and main-hand operations.

6. Paper entrypoint

Create src/paper.ts:

export * from './plugin.js';

import { definePaperEntrypoint } from '@shamoo/paper';

export default definePaperEntrypoint({
  enable() {
    console.info('[first-shamoo-plugin] Paper bundle enabled');
  },
  disable() {
    console.info('[first-shamoo-plugin] Paper bundle disabled');
  },
});

Although the public entrypoint type accepts PaperEntrypointContext, the current bundled adapter invokes entrypoint hooks with no arguments. Use zero-argument hooks for deployed code.

7. Check, build, and deploy

pnpm typecheck
pnpm doctor
pnpm build
pnpm deploy

The build writes:

dist/paper/index.js
dist/paper/index.js.map
dist/shamoo.metadata.json

Deployment writes an installation similar to:

/srv/paper/plugins/ShamooRuntime/plugins/first-shamoo-plugin/
├── paper/index.js
├── paper/index.js.map
├── shamoo-plugin.json
└── shamoo.metadata.json

ShamooRuntime waits for a stable file inventory, stages the candidate, validates descriptor and compatibility data, and then loads or replaces the generation. Watch the Paper log for the Runtime initialization message and the plugin's enable messages. Run /shamoo-hello from a player or console.

For active development:

pnpm dev

This is complete generation rebuild/redeployment, not in-place module hot replacement.

Dual-platform conversion

Create or add src/velocity.ts, set platforms to ["paper", "velocity"], set velocityEntrypoint, and configure both deploy roots. Keep src/plugin.ts platform-neutral; imports reachable only from src/paper.ts may use @shamoo/paper*, while imports reachable only from src/velocity.ts may use @shamoo/velocity*. The compiler rejects opposite-platform imports and the bundler creates independent artifacts. See the dual-platform example.

Clone this wiki locally