dev:scriptref:gmcp

GMCP

Generated from smudgy v0.4.0 (smudgy-core.d.ts @ 3c804cdb8fa3). Index: scriptref.

GMCP is a data channel many MUDs run alongside the text you see: the server sends structured messages (vitals, room details, chat lines) that scripts read directly instead of parsing the screen. Smudgy negotiates it automatically and collects everything the server has sent into one live tree, consumed like any shared state (events).

import gmcp from "smudgy:state/gmcp";
 
// The latest reading, whenever a script needs it:
const hp = gmcp.value?.Char?.Vitals?.hp;
 
// Runs once per message at or under the path:
gmcp.watch("Char.Vitals", v => { ... });
gmcp.watch("Comm.Channel", m => { ... });

Each message the server sends is committed as its own update, so a watcher at or under the message's path runs once per message, repeats included. Message names match case-insensitively; subscriptions to Char.Vitals and cHaR.VItALs receive the same updates. The typed shapes are a widely-implemented set of GMCP state objects (Char.Vitals, Room.Info, Comm.Channel); every message the server sends appears in the tree either way.

Whether a game offers GMCP at all varies. gmcp.enabled from smudgy:core reports the current connection, and gmcp.onReady calls a callback immediately when GMCP is already active, otherwise once when negotiation completes.

TypeScript detail: Smudgy has no way to guarantee that any MUD will provide GMCP data in exactly the shape it expects. For example, a game might provide room vnums under Room.Info.id rather than Room.Info.num. A script can put corrected typings in place with a cast:

import gmcp from "smudgy:state/gmcp";
import type { StateConsumer, GmcpTree } from "smudgy:core";
 
interface FenworldGmcp extends GmcpTree {
  Room?: {
    Info?: { id?: number; name?: string; [field: string]: unknown };
    [message: string]: unknown;
  };
}
 
const fenGmcp = gmcp as StateConsumer<FenworldGmcp>;
 
const vnum = fenGmcp.value?.Room?.Info?.id;   // number | undefined

Adding a message these shapes don't declare, such as a game's Room.Weather, uses the same technique; GmcpTree shows the addition form.

The module's default export (gmcp).

const gmcp: StateConsumer<GmcpTree>;

The live GMCP tree, one entry per message name (see GmcpTree): read the latest value with gmcp.value, subscribe with gmcp.watch(path, ...), and wire widgets with gmcp.bind(path). Each message the server sends is committed as its own update, so a watcher at or under the message's path runs once per message, repeats included.

export interface GmcpTree {
  Char?: {
    Vitals?: { hp?: number; maxhp?: number; mp?: number; maxmp?: number; [field: string]: unknown };
    Status?: { level?: number; [field: string]: unknown };
    Name?: { name?: string; fullname?: string; [field: string]: unknown };
    [message: string]: unknown;
  };
  Room?: {
    Info?: {
      num?: number;
      name?: string;
      area?: string;
      zone?: string;
      environment?: string;
      terrain?: string;
      exits?: Record<string, number>;
      [field: string]: unknown;
    };
    [message: string]: unknown;
  };
  Comm?: {
    Channel?: { chan?: string; player?: string; msg?: string; [field: string]: unknown };
    [message: string]: unknown;
  };
  [pkg: string]: unknown;
}

Everything the server has sent over GMCP, one entry per message name. import gmcp from "smudgy:state/gmcp" serves the live tree: gmcp.value.Char.Vitals.hp is the latest reading, and gmcp.watch("Char.Vitals", ...) runs on each vitals message.

Paths reach inside payloads too: gmcp.watch("Char.Vitals.hp", ...) hands the handler just the number. It runs on every message that covers the path, so a vitals update that left hp unchanged still fires; compare against gmcp.previousValue to react to change alone.

Message names are matched case-insensitively, so Char.Vitals finds the data whether the server spells it Char.Vitals or char.vitals.

The declared entries are a widely-implemented set of GMCP state objects. Games send others, and every message the server sends appears in the tree whether or not it is declared here; an undeclared message reads as unknown. A script can type the messages of the game it supports by extending this interface and casting the handle. A game that adds a Room.Weather message keeps the declared Room.Info typing by intersecting:

import gmcp from "smudgy:state/gmcp";
import type { StateConsumer, GmcpTree } from "smudgy:core";
 
interface FenworldGmcp extends GmcpTree {
  Room?: NonNullable<GmcpTree['Room']> & {
    Weather?: { temp?: number; rain?: boolean };
  };
}
 
const fenGmcp = gmcp as StateConsumer<FenworldGmcp>;
const temp = fenGmcp.value?.Room?.Weather?.temp;  // number | undefined
export const gmcp: {
  readonly enabled: boolean;
  onReady(callback: () => void): void;
  send(name: string, data?: unknown): void;
  enableModule(name: string, version?: number): void;
  disableModule(name: string): void;
  mergeKeys(...names: string[]): void;
};

GMCP protocol status and control for the current session.

export const ready: EventConsumer<Record<string, never>>;

Fires once GMCP negotiation completes and the handshake has been sent; GMCP data starts flowing from this moment. For code that may load after the connection, gmcp.onReady from smudgy:core covers both orders.

export const closed: EventConsumer<Record<string, never>>;

Fires when GMCP stops on a live connection: the server withdrew it, or the connection dropped while it was active. The last-received data stays readable through smudgy:state/gmcp.


Script API reference · smudgy:core — Sessions & output · smudgy:core — Lines & buffer · smudgy:core — Shared state & events · smudgy:core — Automations · smudgy:core — Saved automations · smudgy:core — Panes · smudgy:widgets · Mapper · smudgy:params