> ## Documentation Index
> Fetch the complete documentation index at: https://docs.seraph.si/llms.txt
> Use this file to discover all available pages before exploring further.

# Examples

> Complete plugins you can drop into the folder and adapt.

Every plugin below is a whole file. Save it into your plugins folder, enable it in
`/seraph config` under **Plugins**, and it runs. Nothing here needs a build step or an install.

## Lobby greeter

Watches the tab list and welcomes people as they arrive, with the greeting and a quiet mode
exposed as settings.

```js theme={null}
declareModule({
    name: "lobby-greeter",
    displayName: "Lobby Greeter",
    version: "1.0.0",
    description: "Greets players as they join your lobby.",
    permissions: ["chat"],
    config: {
        greeting: "welcome",
        onlyInParty: false,
    },
});

events.on("playerJoin", (data) => {
    if (!data.name) return;
    if (config.onlyInParty && !party.isInParty()) return;

    chat.addChatMessage("§8[§bGreeter§8] §7" + config.greeting + " §f" + data.name);
});
```

`config` is live, so a change made in game takes effect on the next join without a reload.

## Health overlay

Draws a small readout in the corner of the screen, toggled with a key.

```js theme={null}
declareModule({
    name: "health-hud",
    displayName: "Health HUD",
    permissions: ["render", "player"],
    config: { visible: true },
});

events.on("renderOverlay", () => {
    if (!config.visible) return;

    const line = "§c" + player.getHealth().toFixed(1) + "§7/§c" + player.getMaxHealth().toFixed(1);
    const width = render.getTextWidth(line);
    const x = render.getScreenWidth() - width - 8;
    const y = render.getScreenHeight() - 20;

    render.drawRect(x - 3, y - 3, width + 6, 14, 0x80000000);
    render.drawText(line, x, y, 0xFFFFFF, true);
});

let wasDown = false;

events.on("tick", () => {
    const down = input.isKeyDown("H");
    if (down && !wasDown) {
        config.visible = !config.visible;
        savePluginConfig();
    }
    wasDown = down;
});
```

`renderOverlay` runs every frame and only while no screen is open, so the readout hides itself
whenever you open your inventory. Keep the body cheap: work out anything expensive in `tick` and
draw the result here.

## Webhook relay

Forwards matching chat lines to a Discord webhook, with the URL hidden in the menu.

```js theme={null}
declareModule({
    name: "webhook-relay",
    displayName: "Webhook Relay",
    permissions: ["network", "chat"],
    config: {
        webhook: "",
        match: "has joined",
    },
    secrets: ["webhook"],
});

events.on("chat", (event) => {
    if (!config.webhook) return;
    if (!event.message.includes(config.match)) return;

    http.fetch(config.webhook, "POST", { content: event.message }, {
        "Content-Type": "application/json",
    })
        .then((response) => {
            if (response.responseCode >= 400) {
                console.warn("Webhook refused: " + response.responseCode);
            }
        })
        .catch((reason) => console.error("Webhook failed: " + reason));
});
```

Naming `webhook` in `secrets` draws it as dots with a reveal button, so it is not read off the
screen during a share. See [Manifest and permissions](/docs/features/plugin-api/manifest).

<Warning>
  The `network` permission is limited to 30 requests a minute per plugin. A chat listener can fire
  far faster than that in a busy lobby, so match narrowly, or collect lines and send them on a
  timer rather than one request per message.
</Warning>

## A custom anti-cheat check

Flags a player whose view snaps further in a tick than a person could turn. Seraph's own flag
message and report pipeline takes over from there.

```js theme={null}
declareModule({
    name: "snap-check",
    displayName: "Snap Aim",
    permissions: ["anticheat"],
    config: { threshold: 90 },
});

const lastYaw = {};

anticheat.registerCheck("snap-aim", (data) => {
    const previous = lastYaw[data.uuid];
    lastYaw[data.uuid] = data.rotationYaw;

    if (previous === undefined) return false;

    let delta = Math.abs(data.rotationYaw - previous) % 360;
    if (delta > 180) delta = 360 - delta;

    return delta > config.threshold && data.isSwingInProgress;
}, "Snap Aim");

anticheat.onFlag((uuid, checkId, name) => {
    console.info(uuid + " flagged for " + name + " (" + checkId + ")");
});
```

The tick function runs once per tick for every observed player, so it is the hottest code a plugin
can write. Keep it to arithmetic, and never call the network from inside it.

## A stats command

Registers `/whois`, resolves a name through Seraph's own API layer and tags the player in the
world.

```js theme={null}
declareModule({
    name: "whois",
    displayName: "Who Is",
    permissions: ["commands", "chat", "network", "render"],
});

events.registerCommand(
    "whois",
    (args) => {
        const target = args[0];
        if (!target) {
            chat.addChatMessage("§cusage: /whois <player>");
            return;
        }

        stats.getPlayer(target)
            .then((profile) => {
                if (!profile) {
                    chat.addChatMessage("§7no such player: §f" + target);
                    return;
                }

                chat.addChatMessage(
                    chat.createBuilder()
                        .text("[whois] ")
                        .color("b")
                        .text(profile.name)
                        .color("f")
                        .hoverText("§7" + profile.uuid)
                        .clickSuggest("/whois " + profile.name),
                );

                nametags.set(profile.name, "§b[?] ");
            })
            .catch((reason) => chat.addChatMessage("§clookup failed: " + reason));
    },
    (args) => events.completePlayers(args),
);
```

`stats.getPlayer` shares the mod's caching and proxy handling, so repeated lookups of the same
player in a lobby cost nothing extra. `nametags` only ever clears decorations your own plugin
added.

## A module with a schedule

Modules are compiled, stay loaded for the session and get `enable` and `disable` hooks, which is
what anything with a cron schedule wants. Save this as `standup.mod.js`.

```js theme={null}
declareModule({
    name: "standup",
    displayName: "Standup",
    permissions: ["chat", "notify"],
    config: { hourly: true },
});

const listeners = [];
const schedules = [];
let seen = 0;

exports.enable = () => {
    listeners.push(events.on("playerJoin", () => { seen += 1; }));

    if (config.hourly) {
        schedules.push(cron("0 * * * *", () => {
            hud.actionBar("§7" + seen + " players seen this hour");
            seen = 0;
        }));
    }
};

exports.disable = () => {
    for (const id of listeners) events.off(id);
    for (const id of schedules) clearCron(id);
    listeners.length = 0;
    schedules.length = 0;
};
```

Editing a `.mod.js` file does not hot reload it. Seraph tells you in chat that a restart is needed
instead, and ordinary scripts carry on reloading around it. See
[Scripts and modules](/docs/features/plugin-api/modules).

## Sharing code between plugins

Put the shared half in its own plugin, name it in `dependsOn` and `require` it:

```js theme={null}
declareModule({ name: "colours" });

exports.tag = (text) => "§8[§b" + text + "§8] §7";
```

```js theme={null}
declareModule({ name: "greeter", dependsOn: ["colours"], permissions: ["chat"] });

const colours = require("colours");

chat.addChatMessage(colours.tag("greeter") + "ready");
```

Anything that is not the name of a loaded plugin is treated as a path relative to the requiring
file, so `require("./lib/helpers")` keeps shared code in a subfolder even though only top level
`.js` files are discovered.

## Next

<CardGroup cols={2}>
  <Card title="Events" icon="bolt" href="/docs/features/plugin-api/events">
    Every event, what it hands you and when it runs.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/docs/features/plugin-api/troubleshooting">
    Why a plugin is not loading, running or drawing.
  </Card>
</CardGroup>
