@s2script/sdk 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/plugin.d.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * @s2script/sdk/plugin — the typed plugin artifact: `plugin(factory)`, the load-scoped `PluginContext`,
3
+ * and `Scope`. NO runtime code: the engine injects the implementation at load time
4
+ * (`__s2pkg_plugin`, resolved via the existing `@s2script/sdk/*` prefix strip).
5
+ *
6
+ * See docs/superpowers/specs/2026-07-20-L1-lifecycle-v2-design.md §1-§3 for the full contract.
7
+ */
8
+ import type { GameEvent, HookResultValue } from "./events";
9
+ import type { Client } from "./clients";
10
+ import type { EntityRef, OutputEvent } from "./entity";
11
+ import type { DamageInfo } from "./damage";
12
+ import type { CommandInvocation } from "./commands";
13
+ import type { Config } from "./config";
14
+ import type { PublishHandle } from "./interfaces";
15
+ import type { TopMenuItem } from "./topmenu";
16
+ import type { PrecacheContext } from "./sound";
17
+ import type { UserCmdView } from "./usercmd";
18
+
19
+ /** Game-event subscriptions on this plugin's load-scope ({@link PluginContext.events}). */
20
+ export interface CtxEvents {
21
+ /**
22
+ * Subscribe to a fired game event (post-phase). The {@link GameEvent} is valid only synchronously.
23
+ * @example
24
+ * // plugins/disabled/nextmap/src/plugin.ts:133
25
+ * ctx.events.on("round_end", () => console.log("round ended"));
26
+ */
27
+ on(name: string, handler: (ev: GameEvent) => void): void;
28
+ /** Pre-hook a game event: return a {@link HookResultValue} (`Handled`/`Stop` suppress the client broadcast). */
29
+ onPre(name: string, handler: (ev: GameEvent) => HookResultValue | void): void;
30
+ }
31
+ /**
32
+ * Handlers fire for clients that connect AFTER Active. To cover already-connected clients, seed
33
+ * explicitly in the factory: `for (const c of Clients.all()) { … }` — there is no framework
34
+ * replay (replaying `onConnect` for pre-existing clients would fire auth/ban/reservation logic
35
+ * out of its real order).
36
+ */
37
+ export interface CtxClients {
38
+ /** A client began connecting (pre-auth). */
39
+ onConnect(handler: (client: Client) => void | Promise<void>): void;
40
+ /** A client's entity was put in the server (`ClientPutInServer`). */
41
+ onPutInServer(handler: (client: Client) => void | Promise<void>): void;
42
+ /** A client became fully active (in-game, receiving snapshots). */
43
+ onActive(handler: (client: Client) => void | Promise<void>): void;
44
+ /** A client finished authenticating (Steam ticket validated). */
45
+ onFullyConnect(handler: (client: Client) => void | Promise<void>): void;
46
+ /** A client disconnected. */
47
+ onDisconnect(handler: (client: Client) => void): void;
48
+ /** A client's convars/settings changed (`ClientSettingsChanged`). */
49
+ onSettingsChanged(handler: (client: Client) => void): void;
50
+ /** A client sent a voice packet (per-frame while speaking). */
51
+ onVoice(handler: (client: Client) => void): void;
52
+ /** A client's persisted cookies finished loading and are now readable. */
53
+ onCookiesCached(handler: (client: Client) => void): void;
54
+ /** A client sent chat: return a {@link HookResultValue} to suppress it. @param teamonly - team-channel say. */
55
+ onSay(handler: (slot: number, text: string, teamonly: boolean) => HookResultValue | void): void;
56
+ /** Per-tick usercmd hook (SM `OnPlayerRunCmd`): read/modify {@link UserCmdView}; return `Handled` to block the tick. */
57
+ onRunCmd(handler: (cmd: UserCmdView, info: { slot: number }) => HookResultValue | void): void;
58
+ }
59
+ /** Entity lifecycle + damage subscriptions on this plugin's load-scope ({@link PluginContext.entities}). */
60
+ export interface CtxEntities {
61
+ /** An entity of `className` was created (not yet spawned). @param className - match, or `"*"` for all. */
62
+ onCreate(className: string, handler: (entity: EntityRef | null, className: string) => void): void;
63
+ /** An entity of `className` spawned (post-`DispatchSpawn`). */
64
+ onSpawn(className: string, handler: (entity: EntityRef | null, className: string) => void): void;
65
+ /** An entity of `className` is being deleted; the ref goes stale right after. */
66
+ onDelete(className: string, handler: (entity: EntityRef | null, className: string) => void): void;
67
+ /** Hook a named entity output (`FireOutputInternal`); return a {@link HookResultValue} to suppress it. */
68
+ onOutput(classname: string, output: string, handler: (ev: OutputEvent) => HookResultValue | void): void;
69
+ /**
70
+ * Damage pre-hook (SDKHooks-equivalent): read/modify {@link DamageInfo}; return `Handled` semantics via `info`.
71
+ * @example
72
+ * // plugins/basecommands/src/plugin.ts:81 — halve incoming damage
73
+ * ctx.entities.onDamage((info) => { info.damage = info.damage / 2; });
74
+ */
75
+ onDamage(handler: (info: DamageInfo) => HookResultValue | void): void;
76
+ }
77
+ /** Per-frame + map/precache hooks on this plugin's load-scope ({@link PluginContext.server}). */
78
+ export interface CtxServer {
79
+ /** Run `fn` every game frame. @param opts - `priority` orders it within the frame (`monitor` runs last, read-only). */
80
+ onGameFrame(fn: () => void, opts?: { priority?: "high" | "normal" | "low" | "monitor" }): void;
81
+ /** A new map became live; `mapName` is the BSP name. */
82
+ onMapStart(handler: (mapName: string) => void): void;
83
+ /** Precache window — register models/sounds to precache for the current map. */
84
+ onPrecache(handler: (pc: PrecacheContext) => void): void;
85
+ }
86
+ /** Console/chat command registration on this plugin's load-scope ({@link PluginContext.commands}). */
87
+ export interface CtxCommands {
88
+ /** Register a public command (any client may run it). */
89
+ register(name: string, handler: (cmd: CommandInvocation) => void): void;
90
+ /** Register a server-only command (console/rcon, not client-runnable). */
91
+ registerServer(name: string, handler: (cmd: CommandInvocation) => void): void;
92
+ /** Register an admin command gated by `flags` (an `ADMFLAG` bitmask; fail-safe default-deny). */
93
+ registerAdmin(name: string, flags: number, handler: (cmd: CommandInvocation) => void): void;
94
+ }
95
+ /** Config live-reload subscription on this plugin's load-scope ({@link PluginContext.config}). */
96
+ export interface CtxConfig {
97
+ /** Fires when the plugin's config file is re-materialized on disk; re-read values inside. */
98
+ onChange(handler: (cfg: Config) => void): void;
99
+ }
100
+ /** TopMenu (adminmenu) contribution on this plugin's load-scope ({@link PluginContext.topmenu}). */
101
+ export interface CtxTopMenu {
102
+ /** Add (or reuse) a top-level menu category. */
103
+ addCategory(name: string): void;
104
+ /** Add an item under an existing category. */
105
+ addItem(category: string, item: TopMenuItem): void;
106
+ }
107
+
108
+ /** A producer-backed inter-plugin interface: its methods, plus forward subscriptions. */
109
+ export type InterfaceHandle<T extends object> = T & {
110
+ /** Subscribe to a producer forward. Load-window only (buffered, armed at Active) — like every registration. */
111
+ on(event: string, handler: (payload: any) => void): void;
112
+ };
113
+
114
+ /**
115
+ * A disposable bundle of subscriptions ({@link PluginContext.createScope}). Registering through a scope
116
+ * lets you drop the whole group at once with {@link Scope.clear} without unloading the plugin.
117
+ */
118
+ export interface Scope {
119
+ /** Game-event subscriptions bound to this scope. */
120
+ readonly events: CtxEvents;
121
+ /** Client-lifecycle subscriptions bound to this scope. */
122
+ readonly clients: CtxClients;
123
+ /** Entity/damage subscriptions bound to this scope. */
124
+ readonly entities: CtxEntities;
125
+ /** Per-frame/map subscriptions bound to this scope. */
126
+ readonly server: CtxServer;
127
+ /** Remove every subscription this scope holds; the scope stays usable (re-register on next open). */
128
+ clear(): void;
129
+ /** clear() + permanently retire the scope. Idempotent. */
130
+ dispose(): void;
131
+ /** True once {@link Scope.dispose} has run. */
132
+ readonly disposed: boolean;
133
+ }
134
+
135
+ /**
136
+ * The load-scoped context passed to a plugin's {@link PluginFactory}. Its sub-objects register every
137
+ * subscription; all registration is load-window only (buffered, armed at Active).
138
+ */
139
+ export interface PluginContext {
140
+ /** This plugin's id (manifest `id`). */
141
+ readonly id: string;
142
+ /** The revived hot-reload handoff (the previous instance's `state()` return), or undefined. */
143
+ readonly previous: unknown;
144
+ /** Game-event subscriptions ({@link CtxEvents}). */
145
+ readonly events: CtxEvents;
146
+ /** Client-lifecycle subscriptions ({@link CtxClients}). */
147
+ readonly clients: CtxClients;
148
+ /** Entity/damage subscriptions ({@link CtxEntities}). */
149
+ readonly entities: CtxEntities;
150
+ /** Per-frame/map/precache subscriptions ({@link CtxServer}). */
151
+ readonly server: CtxServer;
152
+ /** Console/chat command registration ({@link CtxCommands}). */
153
+ readonly commands: CtxCommands;
154
+ /** Config live-reload subscription ({@link CtxConfig}). */
155
+ readonly config: CtxConfig;
156
+ /** TopMenu (adminmenu) contribution ({@link CtxTopMenu}). */
157
+ readonly topmenu: CtxTopMenu;
158
+ /** Publish this plugin's manifest-declared interface. Buffered; goes live at Active. */
159
+ publish<T extends object>(name: string, impl: T): PublishHandle;
160
+ /** Resolve a HARD dep (must be in `pluginDependencies`). Immediate — the proxy is callable inside the factory. */
161
+ use<T extends object>(name: string): InterfaceHandle<T>;
162
+ /** Resolve an OPTIONAL dep (must be in `optionalPluginDependencies`); null while unpublished. */
163
+ tryUse<T extends object>(name: string): InterfaceHandle<T> | null;
164
+ /** Allocate a disposable subscription scope (load-window only — the capability originates at load). */
165
+ createScope(): Scope;
166
+ }
167
+
168
+ /** Optional lifecycle hooks a {@link PluginFactory} may return to participate in unload + hot-reload. */
169
+ export interface PluginHooks {
170
+ /** Best-effort cleanup at unload (the ledger remains the teardown authority). */
171
+ onUnload?(): void;
172
+ /** Hot-reload handoff capture; JSON-serialized (EntityRef-aware) and revived as the next instance's ctx.previous. */
173
+ state?(): unknown;
174
+ }
175
+ /**
176
+ * A plugin's body: called once at load with the {@link PluginContext}, optionally returning
177
+ * {@link PluginHooks}. May be async (the load waits for it to settle).
178
+ */
179
+ export type PluginFactory = (ctx: PluginContext) => void | PluginHooks | Promise<void | PluginHooks>;
180
+ /** The opaque artifact {@link plugin} returns; a module must `export default` it to be a valid plugin. */
181
+ export interface PluginDefinition {
182
+ /** Brand tag proving this object came from {@link plugin} (host-checked at load). */
183
+ readonly __s2plugin: 1;
184
+ }
185
+ /**
186
+ * Define a plugin from its factory. `export default` the result — the host calls the factory once at load.
187
+ * @example
188
+ * import { plugin } from "@s2script/sdk/plugin";
189
+ * // examples/greeter-plugin/src/plugin.ts:8
190
+ * export default plugin((ctx) => {
191
+ * const handle = ctx.publish("@demo/greeter", impl);
192
+ * ctx.server.onGameFrame(() => handle.emit("greeted", { slot: 0 }));
193
+ * });
194
+ */
195
+ export declare function plugin(factory: PluginFactory): PluginDefinition;
package/plugins.d.ts CHANGED
@@ -1,10 +1,29 @@
1
1
  /** @s2script/plugins — runtime plugin management (the SM `sm plugins` backend). NO runtime code (injected). */
2
+
3
+ /** A snapshot of one plugin's identity and lifecycle state, as returned by {@link Plugins.list}. */
2
4
  export interface PluginInfo {
3
5
  /** the plugin id (from its manifest). */
4
6
  readonly id: string;
5
- /** true = running; false = manually unloaded (on disk but not running, via `unload`). */
7
+ /** true = running; false = not running (loading, waiting, failed, or manually unloaded). Exactly
8
+ * `state === "running"`. */
6
9
  readonly loaded: boolean;
10
+ /** the plugin's lifecycle state (L1 lifecycle v2):
11
+ * - `"running"` — Active (factory settled, armed).
12
+ * - `"loading"` — an in-flight factory load (async factory not yet settled).
13
+ * - `"waiting"` — parked until a hard-dependency producer is published (or a ~30s timeout).
14
+ * - `"failed"` — the load failed (bad artifact, throwing/rejecting factory, publishes mismatch,
15
+ * or a load timeout); the plugin is NOT running.
16
+ * - `"unloaded"` — on disk but manually unloaded via `unload` (not auto-reloaded until `load`/`reload`).
17
+ */
18
+ readonly state: "running" | "loading" | "waiting" | "failed" | "unloaded";
7
19
  }
20
+ /**
21
+ * Enumerate and drive the plugin set at runtime (list / load / unload / reload).
22
+ * @example
23
+ * import { Plugins } from "@s2script/sdk/plugins";
24
+ * // plugins/basecommands/src/plugin.ts:126 — sm plugins list
25
+ * for (const p of Plugins.list()) cmd.reply(p.id + " — " + p.state);
26
+ */
8
27
  export declare const Plugins: {
9
28
  /** Every loaded/unloaded plugin. */
10
29
  list(): PluginInfo[];
package/server.d.ts CHANGED
@@ -1,4 +1,12 @@
1
1
  /** @s2script/server — server control (run console commands, query map validity). NO runtime code (injected at load). */
2
+
3
+ /**
4
+ * Server console + globals: run commands, register/read/set cvars, and query map/time state.
5
+ * @example
6
+ * import { Server } from "@s2script/sdk/server";
7
+ * // plugins/basecommands/src/plugin.ts:46 — sm_map
8
+ * Server.command("changelevel " + map);
9
+ */
2
10
  export declare const Server: {
3
11
  /** Run `cmd` at the server console (queued; executes next frame). */
4
12
  command(cmd: string): void;
@@ -27,13 +35,6 @@ export declare const Server: {
27
35
  min?: number;
28
36
  max?: number;
29
37
  }): boolean;
30
- /**
31
- * Subscribe to map start (fires on every server map startup — the framework event that replaces
32
- * polling `Server.mapName` on OnGameFrame). Boot-loaded plugins receive the first map's fire;
33
- * a plugin hot-loaded mid-map does NOT get a synthetic fire for the current map — read
34
- * `Server.mapName` at load. Torn down automatically on plugin unload.
35
- */
36
- onMapStart(handler: (mapName: string) => void): void;
37
38
  /** The server's configured max client count (`GetMaxClients()`). `0` if unavailable. */
38
39
  readonly maxPlayers: number;
39
40
  /** The current map name (`GetMapName()`, the BSP). `""` if unavailable. */
package/sound.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /** @s2script/sound — engine-generic sound: emit a named SoundEvent + register custom precache paths. */
2
2
  import type { EntityRef } from "./entity";
3
3
 
4
+ /** Options for {@link Sound.emit} — the source entity, recipient slots, and volume. */
4
5
  export interface SoundEmitOptions {
5
6
  /** Source entity (serial-gated; a stale ref emits nothing and returns 0). Omitted -> worldspawn
6
7
  * (a global/2D sound). */
@@ -22,14 +23,18 @@ export interface PrecacheContext {
22
23
  add(path: string): boolean;
23
24
  }
24
25
 
26
+ /**
27
+ * Engine-generic sound entry point — play a named SoundEvent to some or all clients.
28
+ * @example
29
+ * import { Sound } from "@s2script/sdk/sound";
30
+ * // global 2D broadcast to every valid client (worldspawn source):
31
+ * const guid = Sound.emit("MyPack.Ping");
32
+ * if (guid === 0) console.log("emit failed (unresolved name / stale source)");
33
+ */
25
34
  export declare const Sound: {
26
35
  /** Play a named SoundEvent (the engine resolves name->hash; built-in soundevents need no
27
36
  * precache). Returns the engine sound GUID (nonzero) or 0 on failure (unresolved engine
28
37
  * function / stale source entity / an empty `recipients` array). An all-bot-skipped recipient
29
38
  * set still emits to nobody (a real engine call, may return a nonzero GUID). */
30
39
  emit(name: string, opts?: SoundEmitOptions): number;
31
- /** Subscribe to the session resource-manifest build (fires at map load / mapchange). Register
32
- * custom .vsndevts/.vsnd content here so a later emit can play it. A plugin hot-loaded mid-map
33
- * gets its first fire at the NEXT map change. */
34
- onPrecache(handler: (ctx: PrecacheContext) => void): void;
35
40
  };
package/timers.d.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  * NO runtime code: the engine injects the implementation at load time.
4
4
  */
5
5
 
6
- /** Await a delay of `ms` milliseconds before continuing. */
6
+ /**
7
+ * Await a delay of `ms` milliseconds before continuing. Tick-integrated (resumes on a game frame).
8
+ * @example
9
+ * import { delay } from "@s2script/sdk/timers";
10
+ * // plugins/funcommands/src/plugin.ts:77 — sm_freeze auto-unfreeze after `secs`
11
+ * delay(secs * 1000).then(() => { const q = Player.fromSlot(slot); if (q && q.pawn) q.pawn.moveType = WALK; });
12
+ */
7
13
  export declare function delay(ms: number): Promise<void>;
8
14
  /** Yield to the next microtick. */
9
15
  export declare function nextTick(): Promise<void>;
package/topmenu.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  /** @s2script/topmenu — the extensible admin/top menu registry. NO runtime code (injected at load). */
2
+
3
+ /** A single registered top-menu entry (an admin command surfaced in the adminmenu). */
2
4
  export interface TopMenuItem {
3
5
  /** Plugin-namespaced unique id, e.g. "playercommands:slap". A duplicate id replaces. */
4
6
  id: string;
@@ -9,15 +11,24 @@ export interface TopMenuItem {
9
11
  /** Runs in THIS plugin's context when an admin selects the item; receives the admin's 0-based slot. */
10
12
  onSelect: (adminSlot: number) => void;
11
13
  }
14
+ /** A handler-free copy of the whole registry — what a menu renderer reads to build the display. */
12
15
  export interface TopMenuSnapshot {
16
+ /** All category names, in registration order. */
13
17
  categories: string[];
18
+ /** Every registered item's metadata (no `onSelect`), tagged with its category. */
14
19
  items: { id: string; category: string; name: string; flags: number }[];
15
20
  }
21
+ /**
22
+ * The shared admin/top-menu registry — renderers read {@link TopMenu.snapshot} and dispatch picks
23
+ * through {@link TopMenu.select}; items are contributed by plugins via their own ctx.topmenu.
24
+ * @example
25
+ * import { TopMenu } from "@s2script/sdk/topmenu";
26
+ * const snap = TopMenu.snapshot();
27
+ * for (const cat of snap.categories) renderCategory(cat);
28
+ * // when the admin picks an item id, dispatch it back to its owner plugin:
29
+ * m.onSelect(e => { TopMenu.select(e.info, slot); });
30
+ */
16
31
  export declare const TopMenu: {
17
- /** Register a category (idempotent; order = first-registration order). */
18
- addCategory(name: string): void;
19
- /** Register/replace an item under a category (auto-creates the category). */
20
- addItem(category: string, item: TopMenuItem): void;
21
32
  /** All categories + item metadata (no handlers) — the adminmenu renderer reads this. */
22
33
  snapshot(): TopMenuSnapshot;
23
34
  /** Fire an item's onSelect (dispatched post-frame to the owner's context). */
package/trace.d.ts CHANGED
@@ -57,6 +57,14 @@ export interface TraceOptions {
57
57
  ignoreEntity?: EntityRef;
58
58
  }
59
59
 
60
+ /**
61
+ * Ray-tracing entry point — `line`/`ray`/`hull` traces over the Source 2 physics scene.
62
+ * @example
63
+ * import { Trace } from "@s2script/sdk/trace";
64
+ * import { Vector } from "@s2script/sdk/math";
65
+ * const down = Trace.line(new Vector(o.x, o.y, o.z + 500), new Vector(o.x, o.y, o.z - 100));
66
+ * if (down.didHit) console.log(down.fraction, down.endPos);
67
+ */
60
68
  export declare const Trace: {
61
69
  /** A line trace from `start` to `end`. */
62
70
  line(start: Vector, end: Vector, opts?: TraceOptions): TraceHit;
package/translations.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  /** @s2script/translations — SourceMod-style i18n (per-client language, phrase files, {1} formatting). */
2
+
3
+ /** A phrase set: key → template string, where `{1}`/`{2}`/… are positional substitution slots. */
2
4
  export type Phrases = Record<string, string>;
5
+ /**
6
+ * SourceMod-style translation registry: seed a default phrase set, override it per language from
7
+ * `translations/<code>/<name>.phrases.json`, then translate a key for a given client's language.
8
+ * @example
9
+ * import { Translations } from "@s2script/sdk/translations";
10
+ * Translations.load("trdemo", { Greeting: "Hello {1}", Bye: "Goodbye {1}" });
11
+ * console.log(Translations.translate(-1, "Greeting", "world")); // "Hello world"
12
+ */
3
13
  export declare const Translations: {
4
14
  /** Register a phrase set: `seed` is the built-in English default; translations/<code>/<name>.phrases.json overrides per language. */
5
15
  load(name: string, seed: Phrases): void;
package/transmit.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * engine-side hook enforces it every snapshot — zero JS runs in the per-client hot path. */
4
4
  import type { EntityRef } from "./entity";
5
5
 
6
+ /** Hot-path measurement counters returned by {@link Transmit.stats}. */
6
7
  export interface TransmitStats {
7
8
  /** CheckTransmit invocations observed since load/reset. */
8
9
  snapshots: number;
@@ -16,6 +17,13 @@ export interface TransmitStats {
16
17
  nsMax: number;
17
18
  }
18
19
 
20
+ /**
21
+ * Declarative per-client entity visibility: hide an entity from some viewers, enforced engine-side each snapshot.
22
+ * @example
23
+ * import { Transmit } from "@s2script/sdk/transmit";
24
+ * // examples/transmit-demo/src/plugin.ts:58 — show `prop` ONLY to one viewer slot
25
+ * const ok = Transmit.setVisibleTo(prop, [slot]);
26
+ */
19
27
  export declare const Transmit: {
20
28
  /** Replace this plugin's visibility rule for `entity`: it is transmitted ONLY to the given viewer
21
29
  * slots (an empty array = hidden from everyone). Multiple plugins AND-merge — an entity reaches a
package/usercmd.d.ts CHANGED
@@ -5,14 +5,20 @@
5
5
  * Source2-shared concept (`usercmd.proto`), so this module is engine-generic.
6
6
  */
7
7
  import type { QAngle } from "./math";
8
- import type { HookResultValue } from "./events";
9
8
 
10
9
  /**
11
- * A block-scoped view of the CURRENT tick's usercmd (valid only during a `UserCmd.onRun` handler —
12
- * a stashed `Cmd` used after the handler returns, or across an `await`, reads/writes nothing).
13
- * There is exactly ONE `Cmd` instance for the whole process; every handler call operates on it.
10
+ * A block-scoped view of the CURRENT tick's usercmd (valid only during a `ctx.clients.onRunCmd`
11
+ * handler — a stashed `UserCmdView` used after the handler returns, or across an `await`,
12
+ * reads/writes nothing). There is exactly ONE `UserCmdView` instance for the whole process; every
13
+ * handler call operates on it.
14
+ * @example
15
+ * import type { UserCmdView } from "@s2script/sdk/usercmd";
16
+ * // examples/usercmd-demo/src/plugin.ts:21 — read this tick's input
17
+ * ctx.clients.onRunCmd((cmd: UserCmdView, info: { slot: number }) => {
18
+ * console.log(`slot=${info.slot} fwd=${cmd.forwardMove} btn=${cmd.buttons}`);
19
+ * });
14
20
  */
15
- export interface Cmd {
21
+ export interface UserCmdView {
16
22
  /** +forward / -back. Normalized to roughly [-1, 1] (not the legacy ±450 units). */
17
23
  forwardMove: number;
18
24
  /**
@@ -36,16 +42,3 @@ export interface Cmd {
36
42
  */
37
43
  clearSubtickMoves(): void;
38
44
  }
39
-
40
- export declare const UserCmd: {
41
- /**
42
- * Subscribe to the per-tick input hook. The handler runs SYNCHRONOUSLY during the engine's
43
- * usercmd processing, once per player per batched tick; `cmd` is the singleton, block-scoped
44
- * current usercmd and `ctx.slot` is the firing player's 0-based slot.
45
- *
46
- * Return a `HookResultValue >= Handled` to SUPPRESS this tick's input (the game processes a
47
- * zeroed/idle command instead); return `Continue`/`undefined` to let the (possibly modified)
48
- * command through unblocked.
49
- */
50
- onRun(handler: (cmd: Cmd, ctx: { slot: number }) => HookResultValue | void): void;
51
- };
package/usermessages.d.ts CHANGED
@@ -3,9 +3,13 @@ import type { HookResultValue } from "./events";
3
3
  /** A general protobuf user-message builder. Build then send in one synchronous burst. */
4
4
  export class UserMessage {
5
5
  constructor(name: string);
6
+ /** Set an integer field. Returns `this` for chaining. */
6
7
  setInt(field: string, value: number): this;
8
+ /** Set a float field. Returns `this` for chaining. */
7
9
  setFloat(field: string, value: number): this;
10
+ /** Set a string field. Returns `this` for chaining. */
8
11
  setString(field: string, value: string): this;
12
+ /** Set a boolean field. Returns `this` for chaining. */
9
13
  setBool(field: string, value: boolean): this;
10
14
  /** Infer the setter from the JS value type. */
11
15
  set(field: string, value: number | string | boolean): this;
@@ -28,15 +32,29 @@ export interface UserMessageView {
28
32
  readonly recipients: number[];
29
33
  /** protobuf TextFormat dump — the documented FALLBACK for unmapped messages only; prefer typed reads. */
30
34
  readonly debugString: string;
35
+ /** Whether `path` is present on the current message. Dotted nested paths supported ("origin.x"). */
31
36
  hasField(path: string): boolean;
32
37
  /** Scalar int read (int32/uint32/fixed32/enum; bool as 0/1). Dotted nested paths supported
33
38
  * ("origin.x" walks sub-messages). null = no such field / repeated / no current message. */
34
39
  readInt(path: string): number | null;
40
+ /** Scalar float read (float/double). Dotted nested paths supported. null if absent/repeated/no message. */
35
41
  readFloat(path: string): number | null;
42
+ /** Scalar bool read. Dotted nested paths supported. null if absent/repeated/no message. */
36
43
  readBool(path: string): boolean | null;
44
+ /** Scalar string/bytes read. Dotted nested paths supported. null if absent/repeated/no message. */
37
45
  readString(path: string): string | null;
38
46
  }
39
47
 
48
+ /**
49
+ * Intercept outbound user messages before delivery (read typed fields, optionally suppress the send).
50
+ * @example
51
+ * import { UserMessages } from "@s2script/sdk/usermessages";
52
+ * import { HookResult, type HookResultValue } from "@s2script/sdk/events";
53
+ * // examples/usermsg-demo/src/plugin.ts:15 — blanket-block radio text
54
+ * UserMessages.onPre("CCSUsrMsg_RadioText", (m): HookResultValue | void => {
55
+ * if (blockRadio) return HookResult.Handled;
56
+ * });
57
+ */
40
58
  export declare const UserMessages: {
41
59
  /** Pre-hook an outbound user message by unscoped name (partial match, SayText2-style; the view
42
60
  * carries the canonical name). Runs SYNCHRONOUSLY before delivery. Return >= HookResult.Handled
package/votes.d.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  /** @s2script/votes — chat-ballot voting with revote + an optional live center tally. NO runtime code (injected at load). */
2
- export interface VoteResult { readonly winner: number | null; readonly counts: number[]; readonly total: number; }
2
+
3
+ /** The outcome delivered to {@link VoteConfig.onEnd} when a vote ends. */
4
+ export interface VoteResult {
5
+ /** 0-based index of the winning option, or null on a tie or zero votes. */
6
+ readonly winner: number | null;
7
+ /** Per-option vote counts, parallel to {@link VoteConfig.options}. */
8
+ readonly counts: number[];
9
+ /** Total votes cast. */
10
+ readonly total: number;
11
+ }
12
+ /** Configuration for {@link Vote.start}. */
3
13
  export interface VoteConfig {
14
+ /** The question shown to voters. */
4
15
  question: string;
5
16
  /** 2..9 options (chat votes are single-digit). */
6
17
  options: string[];
@@ -8,13 +19,45 @@ export interface VoteConfig {
8
19
  duration: number;
9
20
  /** show a live center tally (default false — the SM chat-only way). */
10
21
  showLiveTally?: boolean;
22
+ /** Called once when the vote ends (or is exhausted), with the tallied {@link VoteResult}. It may start a new vote (the lock is released first). */
11
23
  onEnd: (result: VoteResult) => void;
12
24
  }
13
- export interface VoteTally { question: string; options: { label: string; count: number }[]; total: number; secondsLeft: number; }
14
- export interface VoteTallyRenderer { show(slot: number, tally: VoteTally): void; clear(slot: number): void; }
25
+ /** A per-tick snapshot of an in-progress vote, handed to a {@link VoteTallyRenderer}. */
26
+ export interface VoteTally {
27
+ /** The question being voted on. */
28
+ question: string;
29
+ /** Each option's label and current vote count. */
30
+ options: { label: string; count: number }[];
31
+ /** Total votes cast so far. */
32
+ total: number;
33
+ /** Seconds remaining before the vote ends. */
34
+ secondsLeft: number;
35
+ }
36
+ /** A live-tally display backend for votes — registered via {@link Vote.registerTallyRenderer}. */
37
+ export interface VoteTallyRenderer {
38
+ /** Show/update the running tally for `slot`. */
39
+ show(slot: number, tally: VoteTally): void;
40
+ /** Remove the tally display for `slot` (the vote ended or the player left). */
41
+ clear(slot: number): void;
42
+ }
43
+ /**
44
+ * The voting entry point — one active chat-ballot vote at a time, tallied on close.
45
+ * @example
46
+ * import { Vote } from "@s2script/sdk/votes";
47
+ * Vote.start({
48
+ * question: `Kick ${name}?`,
49
+ * options: ["Yes", "No"],
50
+ * duration: 20,
51
+ * showLiveTally: true,
52
+ * onEnd: (r) => {
53
+ * if (r.winner === 0 && r.counts[0] > r.total / 2) kick(name);
54
+ * },
55
+ * });
56
+ */
15
57
  export declare const Vote: {
16
58
  /** Start a vote (chat ballot to all connected players). Returns false if one is already active. */
17
59
  start(config: VoteConfig): boolean;
60
+ /** True while a vote is running (the single-vote lock is held). */
18
61
  isActive(): boolean;
19
62
  /** Abort the active vote (no onEnd). */
20
63
  cancel(): void;
package/ws.d.ts CHANGED
@@ -1,12 +1,29 @@
1
1
  /** @s2script/ws — client WebSocket. NO runtime code (injected as __s2pkg_ws). */
2
+
3
+ /** An open WebSocket connection — a per-plugin handle over a copied message stream (no live socket crosses to JS). */
2
4
  export interface WebSocket {
5
+ /** Register a handler invoked for each inbound text frame. */
3
6
  onMessage(handler: (data: string) => void): void;
7
+ /** Register a handler for connection close; `code`/`reason` come from the close frame. */
4
8
  onClose(handler: (code: number, reason: string) => void): void;
9
+ /** Register a handler for a transport error; `err` is the error text. */
5
10
  onError(handler: (err: string) => void): void;
11
+ /** Send a text frame. */
6
12
  send(data: string): void;
13
+ /** Close the connection. */
7
14
  close(): void;
8
15
  }
16
+ /** Entry point for opening WebSocket connections. */
9
17
  export declare const WebSocket: {
10
- /** Connect to a WebSocket server (wss:// for TLS). Resolves on the open handshake; rejects on connect failure. */
18
+ /**
19
+ * Connect to a WebSocket server (`wss://` for TLS) off the game thread.
20
+ * @returns Resolves on the open handshake with the live {@link WebSocket} handle.
21
+ * @throws Rejects on connect failure (bad URL, refused, TLS/handshake error).
22
+ * @example
23
+ * import { WebSocket } from "@s2script/sdk/ws";
24
+ * const ws = await WebSocket.connect("wss://ws.postman-echo.com/raw");
25
+ * ws.onMessage((data) => { console.log("echo:", data); ws.close(); });
26
+ * ws.send("hello-from-s2script");
27
+ */
11
28
  connect(url: string): Promise<WebSocket>;
12
29
  };
package/frame.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * @s2script/frame — author-time type stubs for the per-frame subscription API.
3
- * NO runtime code: the engine injects the implementation at load time.
4
- */
5
-
6
- export interface SubscribeOptions {
7
- priority?: number;
8
- }
9
-
10
- export declare const OnGameFrame: {
11
- /** Register a callback that fires every game frame. */
12
- subscribe(fn: () => void, opts?: SubscribeOptions): void;
13
- };