@s2script/sdk 0.21.0 → 0.23.0

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/entity.d.ts CHANGED
@@ -220,7 +220,7 @@ export type EntityKeyValueMap = { [key: string]: string | number | boolean };
220
220
  */
221
221
  export declare function createEntity(className: string, keyvalues?: EntityKeyValueMap): EntityRef | null;
222
222
 
223
- /** The payload delivered to an `Entity.onOutput` handler. */
223
+ /** The payload delivered to an `onOutput` handler. */
224
224
  export interface OutputEvent {
225
225
  /** The output's name (e.g. "OnTrigger", "OnPressed", "OnStartTouch"). */
226
226
  output: string;
package/events.d.ts CHANGED
@@ -52,9 +52,9 @@ export type HookResultValue = 0 | 1 | 2 | 3;
52
52
  * Events.fire("player_changename", { userid: p.userId, oldname, newname });
53
53
  */
54
54
  export declare const Events: {
55
- /** Fire a game event. Returns the engine FireEvent result. */
55
+ /** Fire a game event (SourceMod `FireEvent`). Other plugins' `on` / `onPre`
56
+ * handlers run before this returns. Returns the engine FireEvent result. */
56
57
  fire(name: string, fields?: Record<string, number | string | boolean | bigint>, dontBroadcast?: boolean): boolean;
57
- /** Fire a game event to ONE client (SourceMod FireToClient). Same field type-inference as `fire`. Returns false on miss. */
58
58
  /**
59
59
  * Restrict the event currently being pre-dispatched to `slots`, instead of hiding it from everyone.
60
60
  *
@@ -73,6 +73,7 @@ export declare const Events: {
73
73
  * });
74
74
  */
75
75
  setRecipients(slots: readonly number[]): void;
76
-
76
+ /** Fire a game event to ONE client (SourceMod `FireToClient`). Does not go through
77
+ * `IGameEventManager2::FireEvent` (no `onPre`). Returns false on miss. */
77
78
  fireToClient(slot: number, name: string, fields?: Record<string, string | number | boolean | bigint>): boolean;
78
79
  };
package/index.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `@s2script/sdk` — the engine-generic authoring barrel.
3
+ *
4
+ * Subpath imports (`@s2script/sdk/commands`, `@s2script/sdk/plugin`, …) stay valid.
5
+ * This root re-exports the same names so a plugin can write:
6
+ *
7
+ * import { command, hook, HookResult, ADMFLAG } from "@s2script/sdk";
8
+ *
9
+ * `Player` / `Pawn` / CS2 schema types are NOT here — they live on `@s2script/cs2`.
10
+ * `@s2script/sdk/unsafe` stays a deliberate subpath (not re-exported).
11
+ *
12
+ * NO runtime code in this file: the engine injects `globalThis.__s2pkg_sdk` at load
13
+ * (`__s2require("@s2script/sdk")`).
14
+ */
15
+
16
+ export * from "./admin";
17
+ export * from "./bans";
18
+ export * from "./chat";
19
+ export * from "./clients";
20
+ export * from "./commands";
21
+ export * from "./config";
22
+ export * from "./cookies";
23
+ export * from "./damage";
24
+ export * from "./db";
25
+ export * from "./entity";
26
+ export * from "./events";
27
+ export * from "./http";
28
+ export * from "./interfaces";
29
+ export * from "./math";
30
+ export * from "./menu";
31
+ export * from "./net";
32
+ export * from "./phrases";
33
+ export * from "./plugin";
34
+ export * from "./plugins";
35
+ export * from "./sdkhooks";
36
+ export * from "./server";
37
+ export * from "./sound";
38
+ export * from "./timers";
39
+ export * from "./topmenu";
40
+ export * from "./trace";
41
+ export * from "./translations";
42
+ export * from "./transmit";
43
+ export * from "./usercmd";
44
+ export * from "./usermessages";
45
+ export * from "./voice";
46
+ export * from "./votes";
47
+ export * from "./ws";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2script/sdk",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "license": "MIT OR Apache-2.0",
5
5
  "description": "s2script SDK — the builtin capability types (imported as @s2script/sdk/entity, …) plus the `s2s` CLI for building Source 2 / CS2 plugins.",
6
6
  "type": "module",
@@ -13,7 +13,7 @@
13
13
  "test": "node --experimental-strip-types --no-warnings --test test/*.test.mjs"
14
14
  },
15
15
  "dependencies": {
16
- "@s2script/eslint-plugin": "0.2.0",
16
+ "@s2script/eslint-plugin": "0.2.1",
17
17
  "esbuild": "^0.25.0",
18
18
  "eslint": "^10.7.0",
19
19
  "typescript": "^5.6.0"
@@ -35,6 +35,9 @@
35
35
  "contracts"
36
36
  ],
37
37
  "exports": {
38
+ ".": {
39
+ "types": "./index.d.ts"
40
+ },
38
41
  "./admin": {
39
42
  "types": "./admin.d.ts"
40
43
  },
@@ -92,6 +95,9 @@
92
95
  "./plugins": {
93
96
  "types": "./plugins.d.ts"
94
97
  },
98
+ "./sdkhooks": {
99
+ "types": "./sdkhooks.d.ts"
100
+ },
95
101
  "./server": {
96
102
  "types": "./server.d.ts"
97
103
  },
package/phrases.d.ts CHANGED
@@ -6,13 +6,14 @@
6
6
  * from the phrase files you load.
7
7
  *
8
8
  * @example
9
- * export default plugin((ctx) => {
10
- * ctx.translations.load("basecomm", "common");
9
+ * import { translations } from "@s2script/sdk";
10
+ * export function OnPluginStart(): void {
11
+ * translations.load("basecomm", "common");
11
12
  *
12
13
  * cmd.replyT("Usage Gag"); // ok — in translations/basecomm.phrases.json
13
14
  * cmd.replyT("No matching players"); // ok — in translations/common.phrases.json
14
15
  * cmd.replyT("Usage Gagg"); // compile error — no such key in either
15
- * });
16
+ * }
16
17
  *
17
18
  * ## Writing phrases
18
19
  *
@@ -29,7 +30,7 @@
29
30
  *
30
31
  * ## How the checking works
31
32
  *
32
- * The build reads your `ctx.translations.load(...)` call and writes `src/phrases.generated.d.ts`,
33
+ * The build reads your `translations.load(...)` call and writes `src/phrases.generated.d.ts`,
33
34
  * filling in {@link PhraseKeys} with the keys of exactly those files. Load a file and its keys
34
35
  * become valid; don't, and they don't — so a forgotten `load` is a compile error rather than raw key
35
36
  * text printed to a player at runtime. SourceMod enforces the same rule, but only when it happens.
package/plugin.d.ts CHANGED
@@ -1,15 +1,15 @@
1
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).
2
+ * @s2script/sdk/plugin — load-window authoring (`hook`, `publish`, `translations`, `Scope`)
3
+ * and the internal load-scoped {@link PluginContext} types. NO runtime code: the engine injects
4
+ * the implementation at load time (`__s2pkg_plugin`).
5
5
  *
6
- * See docs/superpowers/specs/2026-07-20-L1-lifecycle-v2-design.md §1-§3 for the full contract.
6
+ * The plugin artifact is `export function OnPluginStart` (plus optional named publics).
7
+ * See docs/superpowers/specs/2026-08-30-drop-plugin-factory-design.md.
7
8
  */
8
9
  import type { GameEvent, HookResultValue } from "./events";
9
10
  import type { Client } from "./clients";
10
11
  import type { EntityRef, OutputEvent } from "./entity";
11
- import type { DamageInfo } from "./damage";
12
- import type { CommandInvocation } from "./commands";
12
+ import type { CommandHandler } from "./commands";
13
13
  import type { Config } from "./config";
14
14
  import type { PublishHandle } from "./interfaces";
15
15
  import type { TopMenuItem } from "./topmenu";
@@ -30,7 +30,7 @@ export interface CtxEvents {
30
30
  }
31
31
  /**
32
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
33
+ * explicitly in OnPluginStart: `for (const c of Clients.all()) { … }` — there is no framework
34
34
  * replay (replaying `onConnect` for pre-existing clients would fire auth/ban/reservation logic
35
35
  * out of its real order).
36
36
  */
@@ -56,7 +56,7 @@ export interface CtxClients {
56
56
  /** Per-tick usercmd hook (SM `OnPlayerRunCmd`): read/modify {@link UserCmdView}; return `Handled` to block the tick. */
57
57
  onRunCmd(handler: (cmd: UserCmdView, info: { slot: number }) => HookResultValue | void): void;
58
58
  }
59
- /** Entity lifecycle + damage subscriptions on this plugin's load-scope ({@link PluginContext.entities}). */
59
+ /** Entity lifecycle / I/O subscriptions on this plugin's load-scope ({@link PluginContext.entities}). */
60
60
  export interface CtxEntities {
61
61
  /** An entity of `className` was created (not yet spawned). @param className - match, or `"*"` for all. */
62
62
  onCreate(className: string, handler: (entity: EntityRef | null, className: string) => void): void;
@@ -66,17 +66,9 @@ export interface CtxEntities {
66
66
  onDelete(className: string, handler: (entity: EntityRef | null, className: string) => void): void;
67
67
  /** Hook a named entity output (`FireOutputInternal`); return a {@link HookResultValue} to suppress it. */
68
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
69
  }
77
70
  /** Per-frame + map/precache hooks on this plugin's load-scope ({@link PluginContext.server}). */
78
71
  export interface CtxServer {
79
- /** Run `fn` every game frame. @param opts - `priority` orders it within the frame (`monitor` runs last, read-only). */
80
72
  /**
81
73
  * Run `fn` every game frame.
82
74
  *
@@ -97,11 +89,11 @@ export interface CtxServer {
97
89
  /** Console/chat command registration on this plugin's load-scope ({@link PluginContext.commands}). */
98
90
  export interface CtxCommands {
99
91
  /** Register a public command (any client may run it). */
100
- register(name: string, handler: (cmd: CommandInvocation) => void): void;
92
+ register(name: string, handler: CommandHandler): void;
101
93
  /** Register a server-only command (console/rcon, not client-runnable). */
102
- registerServer(name: string, handler: (cmd: CommandInvocation) => void): void;
94
+ registerServer(name: string, handler: CommandHandler): void;
103
95
  /** Register an admin command gated by `flags` (an `ADMFLAG` bitmask; fail-safe default-deny). */
104
- registerAdmin(name: string, flags: number, handler: (cmd: CommandInvocation) => void): void;
96
+ registerAdmin(name: string, flags: number, handler: CommandHandler): void;
105
97
  /**
106
98
  * Observe an existing CLIENT command by name — SourceMod's `AddCommandListener`.
107
99
  *
@@ -162,7 +154,7 @@ export interface Scope {
162
154
  readonly events: CtxEvents;
163
155
  /** Client-lifecycle subscriptions bound to this scope. */
164
156
  readonly clients: CtxClients;
165
- /** Entity/damage subscriptions bound to this scope. */
157
+ /** Entity lifecycle / I/O subscriptions bound to this scope. */
166
158
  readonly entities: CtxEntities;
167
159
  /** Per-frame/map subscriptions bound to this scope. */
168
160
  readonly server: CtxServer;
@@ -175,8 +167,9 @@ export interface Scope {
175
167
  }
176
168
 
177
169
  /**
178
- * The load-scoped context passed to a plugin's {@link PluginFactory}. Its sub-objects register every
179
- * subscription; all registration is load-window only (buffered, armed at Active).
170
+ * The load-scoped context the host builds for one plugin load. Public authoring uses
171
+ * {@link hook} / {@link command} / named publics instead of receiving this object.
172
+ * {@link Scope} reuses the same subscription namespaces.
180
173
  */
181
174
  export interface PluginContext {
182
175
  /** This plugin's id (manifest `id`). */
@@ -187,7 +180,7 @@ export interface PluginContext {
187
180
  readonly events: CtxEvents;
188
181
  /** Client-lifecycle subscriptions ({@link CtxClients}). */
189
182
  readonly clients: CtxClients;
190
- /** Entity/damage subscriptions ({@link CtxEntities}). */
183
+ /** Entity lifecycle / I/O subscriptions ({@link CtxEntities}). */
191
184
  readonly entities: CtxEntities;
192
185
  /** Per-frame/map/precache subscriptions ({@link CtxServer}). */
193
186
  readonly server: CtxServer;
@@ -201,7 +194,7 @@ export interface PluginContext {
201
194
  readonly topmenu: CtxTopMenu;
202
195
  /** Publish this plugin's manifest-declared interface. Buffered; goes live at Active. */
203
196
  publish<T extends object>(name: string, impl: T): PublishHandle;
204
- /** Resolve a HARD dep (must be in `pluginDependencies`). Immediate — the proxy is callable inside the factory. */
197
+ /** Resolve a HARD dep (must be in `pluginDependencies`). Immediate — the proxy is callable during OnPluginStart. */
205
198
  use<T extends object>(name: string): InterfaceHandle<T>;
206
199
  /** Resolve an OPTIONAL dep (must be in `optionalPluginDependencies`); null while unpublished. */
207
200
  tryUse<T extends object>(name: string): InterfaceHandle<T> | null;
@@ -209,31 +202,98 @@ export interface PluginContext {
209
202
  createScope(): Scope;
210
203
  }
211
204
 
212
- /** Optional lifecycle hooks a {@link PluginFactory} may return to participate in unload + hot-reload. */
205
+ /** Optional lifecycle hooks a plugin may return to participate in unload + hot-reload.
206
+ * Public authoring uses `export function OnPluginEnd` / `OnPluginState` instead. */
213
207
  export interface PluginHooks {
214
208
  /** Best-effort cleanup at unload (the ledger remains the teardown authority). */
215
209
  onUnload?(): void;
216
- /** Hot-reload handoff capture; JSON-serialized (EntityRef-aware) and revived as the next instance's ctx.previous. */
210
+ /** Hot-reload handoff capture; JSON-serialized (EntityRef-aware) and revived as the next instance's {@link previous}. */
217
211
  state?(): unknown;
218
212
  }
213
+
219
214
  /**
220
- * A plugin's body: called once at load with the {@link PluginContext}, optionally returning
221
- * {@link PluginHooks}. May be async (the load waits for it to settle).
215
+ * Named publics the host finds on `module.exports` (missing export = not subscribed):
216
+ *
217
+ * Plugin: `OnPluginStart` / `OnPluginEnd` / `OnPluginState` / `OnAllPluginsLoaded` / `OnConfigsExecuted`
218
+ * Map/frame: `OnMapStart(map)` / `OnMapEnd()` / `OnGameFrame()` / `OnPrecache(pc)`
219
+ * Client: `OnClientConnected` / `PutInServer` / `Active` / `PostAdminCheck` / `Disconnect` /
220
+ * `SayCommand` / `SettingsChanged` / `Voice` / `CookiesCached` / `OnPlayerRunCmd`
221
+ * Entity: `OnEntityCreated` / `OnEntitySpawned` / `OnEntityDestroyed`
222
+ *
223
+ * `OnGameFrame` is SourceMod's public (before simulation). Post-simulation paint is
224
+ * {@link Scope}'s `server.onGameFrame(fn, { phase: "post" })` — Metamod's post hook, not a SM public.
225
+ * Filtered entity I/O is {@link onOutput}, not a public — SourceMod `HookEntityOutput`.
226
+ * Per-entity damage is `SDKHook` (`SDKHookType.OnTakeDamage`), not a named public.
222
227
  */
223
- export type PluginFactory = (ctx: PluginContext) => void | PluginHooks | Promise<void | PluginHooks>;
224
- /** The opaque artifact {@link plugin} returns; a module must `export default` it to be a valid plugin. */
225
- export interface PluginDefinition {
226
- /** Brand tag proving this object came from {@link plugin} (host-checked at load). */
227
- readonly __s2plugin: 1;
228
- }
228
+
229
+ /**
230
+ * Game-event catalog. Load-window only — throws after settle (same window as {@link command}).
231
+ *
232
+ * `hook.on` is post-fire; `hook.onPre` is pre (`Handled`/`Stop` suppress the client broadcast).
233
+ * The {@link GameEvent} is valid only synchronously.
234
+ */
235
+ export declare const hook: {
236
+ /** Post-phase (`ctx.events.on`). */
237
+ on(name: string, handler: (ev: GameEvent) => void): void;
238
+ /**
239
+ * Pre-phase (`ctx.events.onPre`). Return {@link HookResultValue} `Handled`/`Stop` to suppress
240
+ * the client broadcast.
241
+ */
242
+ onPre(name: string, handler: (ev: GameEvent) => HookResultValue | void): void;
243
+ };
244
+
245
+ /**
246
+ * Load-window entity I/O subscribe (SourceMod `HookEntityOutput`). Keyed by `(classname, output)`
247
+ * at the native mux — use `"*"` for either side. Throws after settle.
248
+ */
249
+ export declare function onOutput(
250
+ classname: string,
251
+ output: string,
252
+ handler: (ev: OutputEvent) => HookResultValue | void,
253
+ ): void;
254
+
255
+ /**
256
+ * The revived hot-reload handoff (the previous instance's `OnPluginState` return), or `undefined`.
257
+ * Load-window only.
258
+ */
259
+ export declare function previous(): unknown;
260
+ /**
261
+ * This plugin's id (manifest `id`). Load-window only.
262
+ */
263
+ export declare function pluginId(): string;
264
+
265
+ /**
266
+ * Allocate a disposable subscription scope. Load-window only — same contract as
267
+ * {@link PluginContext.createScope}.
268
+ */
269
+ export declare function createScope(): Scope;
270
+
271
+ /**
272
+ * Publish this plugin's manifest-declared interface. Load-window only (buffered, armed at Active).
273
+ * Same contract as {@link PluginContext.publish}.
274
+ */
275
+ export declare function publish<T extends object>(name: string, impl: T): PublishHandle;
276
+ /**
277
+ * Resolve a HARD dep (must be in `pluginDependencies`). Load-window only.
278
+ * Same contract as {@link PluginContext.use}. Prefer `import { greet } from "@demo/greeter"` for
279
+ * the producer-as-import form; `use()` remains the explicit load-window form and the optional-dep path.
280
+ */
281
+ export declare function use<T extends object>(name: string): InterfaceHandle<T>;
282
+ /**
283
+ * Resolve an OPTIONAL dep (must be in `optionalPluginDependencies`); null while unpublished.
284
+ * Load-window only. Same contract as {@link PluginContext.tryUse}.
285
+ */
286
+ export declare function tryUse<T extends object>(name: string): InterfaceHandle<T> | null;
287
+
288
+ /**
289
+ * Load-window TopMenu contribution. Same contract as {@link PluginContext.topmenu}.
290
+ * Throws after settle.
291
+ */
292
+ export declare const topmenu: CtxTopMenu;
293
+
229
294
  /**
230
- * Define a plugin from its factory. `export default` the result — the host calls the factory once at load.
231
- * @example
232
- * import { plugin } from "@s2script/sdk/plugin";
233
- * // examples/greeter-plugin/src/plugin.ts:8
234
- * export default plugin((ctx) => {
235
- * const handle = ctx.publish("@demo/greeter", impl);
236
- * ctx.server.onGameFrame(() => handle.emit("greeted", { slot: 0 }));
237
- * });
295
+ * Load-window phrase-file declaration. Same contract as {@link PluginContext.translations}.
296
+ * Throws after settle. `s2s build` / `sync-phrase-types.mjs` collect `translations.load(...)`
297
+ * the same way they collect `ctx.translations.load(...)`.
238
298
  */
239
- export declare function plugin(factory: PluginFactory): PluginDefinition;
299
+ export declare const translations: CtxTranslations;