@s2script/sdk 0.22.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/README.md CHANGED
@@ -19,13 +19,21 @@ Copy the `.s2sp` into `addons/s2script/plugins/` on a running server and it load
19
19
  re-drop to hot-reload, delete to unload. No restart.
20
20
 
21
21
  ```ts
22
- import { plugin } from "@s2script/sdk/plugin";
22
+ import { command, hook } from "@s2script/sdk";
23
+ import type { Client } from "@s2script/sdk";
23
24
 
24
- export default plugin((ctx) => {
25
- ctx.commands.register("hello", (cmd) => {
25
+ export function OnPluginStart(): void {
26
+ command("hello", (cmd) => {
26
27
  cmd.reply("hello from s2script");
27
28
  });
28
- });
29
+ hook.on("player_spawn", (ev) => {
30
+ console.log("spawned", ev.getPlayerSlot("userid"));
31
+ });
32
+ }
33
+
34
+ export function OnClientPostAdminCheck(client: Client): void {
35
+ client.chat("welcome");
36
+ }
29
37
  ```
30
38
 
31
39
  This package is **types-only** — the engine injects the implementation at load time, and
package/commands.d.ts CHANGED
@@ -57,7 +57,7 @@ export interface CommandInvocation {
57
57
  *
58
58
  * @example
59
59
  * // `!help` answers in chat; `sm_help` typed at a console answers in that console.
60
- * ctx.commands.register("sm_help", (cmd) => cmd.reply("[SM] Commands: …"));
60
+ * command("sm_help", (cmd) => { cmd.reply("[SM] Commands: …"); return HookResult.Handled; });
61
61
  */
62
62
  reply(message: string): void;
63
63
  /**
@@ -81,6 +81,53 @@ export interface CommandInvocation {
81
81
  * never in the way. */
82
82
  replyT(key: PhraseKey, ...args: (string | number)[]): void;
83
83
  }
84
+
85
+ /** Alias for {@link CommandInvocation} — the SourceMod-shaped name for a parsed invocation. */
86
+ export type Command = CommandInvocation;
87
+
88
+ /**
89
+ * A command callback: the parsed invocation, plus an optional {@link HookResultValue}.
90
+ * SourceMod `Plugin_Handled` is {@link HookResult}.Handled — return it from a command you own
91
+ * (usage errors included; the command was still consumed). Omit the return (`void`) to continue —
92
+ * the same as returning {@link HookResult}.Continue. Engine SUPERCEDE-on-Continue is not this API;
93
+ * chat `!` vs `/` suppress is unchanged.
94
+ */
95
+ export type CommandHandler = (
96
+ cmd: CommandInvocation,
97
+ ) => HookResultValue | void | Promise<HookResultValue | void>;
98
+
99
+ /**
100
+ * Register a public command in the load window (SourceMod `RegConsoleCmd`).
101
+ * Callable only while `OnPluginStart` is running — throws after settle.
102
+ *
103
+ * `.admin` / `.server` are the `RegAdminCmd` / `RegServerCmd` shapes: name the handler anything.
104
+ *
105
+ * @example
106
+ * import { command, ADMFLAG, HookResult } from "@s2script/sdk";
107
+ * export function OnPluginStart(): void {
108
+ * command.admin("sm_kick", ADMFLAG.KICK, kick);
109
+ * }
110
+ * function kick(cmd: CommandInvocation): HookResultValue {
111
+ * cmd.reply("kicked");
112
+ * return HookResult.Handled;
113
+ * }
114
+ */
115
+ export declare const command: {
116
+ (name: string, handler: CommandHandler): void;
117
+ /** Register an admin command gated by `flags` (an `ADMFLAG` bitmask). */
118
+ admin(name: string, flags: number, handler: CommandHandler): void;
119
+ /** Register a server-only command (console/rcon, not client-runnable). */
120
+ server(name: string, handler: CommandHandler): void;
121
+ /**
122
+ * Observe an existing CLIENT command by name — SourceMod's `AddCommandListener`.
123
+ * Load-window only. Same contract as {@link CtxCommands.onClientCommand} (on PluginContext).
124
+ */
125
+ onClientCommand(
126
+ name: string,
127
+ handler: (slot: number, argString: string) => HookResultValue | void,
128
+ ): void;
129
+ };
130
+
84
131
  /** A parsed chat trigger: which command + args, and whether it was the silent (`/`) trigger. */
85
132
  export interface ChatTrigger {
86
133
  /** `true` = the silent trigger (`/`, hidden); `false` = the public trigger (`!`). */
@@ -93,9 +140,9 @@ export interface ChatTrigger {
93
140
 
94
141
  /**
95
142
  * Command-registry utilities: dispatch by name, parse/route chat triggers, and enumerate the global
96
- * registry. Commands themselves are registered through the plugin context (`ctx.commands.register*`).
143
+ * registry. Commands themselves are registered in the load window via {@link command}.
97
144
  * @example
98
- * import { Commands } from "@s2script/sdk/commands";
145
+ * import { Commands } from "@s2script/sdk";
99
146
  * // sm_help backend: every registered command + its required admin flag mask.
100
147
  * const cmds = Commands.list().slice().sort((a, b) => (a.name < b.name ? -1 : 1));
101
148
  */
@@ -122,7 +169,7 @@ export declare const Commands: {
122
169
  /**
123
170
  * Observe a CLIENT command by name — SourceMod's `AddCommandListener`.
124
171
  *
125
- * Unlike {@link register}, which CREATES a command, this hooks one that already exists — including
172
+ * Unlike {@link command}, which CREATES a command, this hooks one that already exists — including
126
173
  * commands the engine itself owns (`player_ping`, `jointeam`, `drop`, `buy`). Registering a
127
174
  * ConCommand of an engine-owned name fails outright ("unable to link multiple ConCommands named
128
175
  * X"), so this is the only way to see those.
@@ -133,7 +180,7 @@ export declare const Commands: {
133
180
  *
134
181
  * @example
135
182
  * // Middle-mouse ping opens the shop, and still places the ping.
136
- * ctx.commands.onClientCommand("player_ping", (slot) => { openShop(slot); });
183
+ * command.onClientCommand("player_ping", (slot) => { openShop(slot); });
137
184
  */
138
185
  onClientCommand(name: string, handler: (slot: number, argString: string) => HookResultValue | void): void;
139
186
  /** Every globally-registered command with its required admin `flags`: `0` = anyone, `-1` = console/server-only,
package/config.d.ts CHANGED
@@ -23,4 +23,9 @@ export declare const config: {
23
23
  readFile(name: string): string | null;
24
24
  /** Write a raw file to the configs dir (creates/overwrites). */
25
25
  writeFile(name: string, content: string): void;
26
+ /**
27
+ * Fires when this plugin's config file is re-materialized on disk. Ledgered per plugin.
28
+ * Not load-window-gated (safe from `OnPluginStart` or from `ctx.config.onChange`).
29
+ */
30
+ onChange(handler: (cfg: Config) => void): void;
26
31
  };
package/damage.d.ts CHANGED
@@ -1,18 +1,23 @@
1
1
  /**
2
- * @s2script/damage — damage pre-hooks (SDKHooks-equivalent). NO runtime code (injected at load).
3
- * CTakeDamageInfo is a Source 2 engine type, so this module is engine-generic (lives in core).
2
+ * @s2script/damage — the block-scoped {@link DamageInfo} view of a live `CTakeDamageInfo`.
3
+ * NO runtime code (injected at load). Engine-generic: `CTakeDamageInfo` is a Source 2 type.
4
+ *
5
+ * Subscribe with `SDKHook` (`SDKHookType.OnTakeDamage` / `OnTakeDamagePost`), not a global mux.
4
6
  */
5
7
  import type { EntityRef } from "./entity";
6
8
 
7
9
  /**
8
- * A block-scoped view of the current damage event (valid only inside a Damage.onPre handler).
10
+ * A block-scoped view of the current damage event (valid only inside an `OnTakeDamage` or
11
+ * `OnTakeDamagePost` SDKHook).
9
12
  * @example
10
- * import type { DamageInfo } from "@s2script/sdk/damage";
11
- * // plugins/basecommands/src/plugin.ts:81 — halve the damage (assign 0 to block it entirely)
12
- * ctx.entities.onDamage((info: DamageInfo) => { info.damage = info.damage / 2; });
13
+ * import type { DamageInfo } from "@s2script/sdk";
14
+ * function onTakeDamage(info: DamageInfo) { info.damage = info.damage / 2; }
13
15
  */
14
16
  export interface DamageInfo {
15
- /** The damage amount (m_flDamage). Assigning MODIFIES the live damage; set to 0 to block. */
17
+ /**
18
+ * The damage amount (`m_flDamage`). On the pre-hook, assigning MODIFIES the live damage; set to
19
+ * 0 to block. On `OnTakeDamagePost`, assignment is ignored (the original already ran).
20
+ */
16
21
  damage: number;
17
22
  /** The damage-type bit flags (m_bitsDamageType). */
18
23
  readonly damageType: number;
package/dist/cli.js CHANGED
@@ -3126,8 +3126,8 @@ var sharedCompilerOptionsJson = {
3126
3126
  noEmit: true,
3127
3127
  moduleResolution: "bundler",
3128
3128
  module: "ESNext",
3129
- target: "ES2020",
3130
- lib: ["ES2020"],
3129
+ target: "ES2024",
3130
+ lib: ["ES2024"],
3131
3131
  types: [],
3132
3132
  skipLibCheck: true,
3133
3133
  allowImportingTsExtensions: true
@@ -3139,7 +3139,7 @@ function sharedProgramOptions(tsMod) {
3139
3139
  allowImportingTsExtensions: sharedCompilerOptionsJson.allowImportingTsExtensions,
3140
3140
  moduleResolution: tsMod.ModuleResolutionKind.Bundler,
3141
3141
  module: tsMod.ModuleKind.ESNext,
3142
- target: tsMod.ScriptTarget.ES2020,
3142
+ target: tsMod.ScriptTarget.ES2024,
3143
3143
  lib: sharedCompilerOptionsJson.lib.map((l2) => `lib.${l2.toLowerCase()}.d.ts`),
3144
3144
  types: [...sharedCompilerOptionsJson.types],
3145
3145
  skipLibCheck: sharedCompilerOptionsJson.skipLibCheck
@@ -3795,7 +3795,7 @@ function typecheckPlugin(pluginDir, opts) {
3795
3795
  const entryRel = s2.main ?? pkg.main;
3796
3796
  if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join7(absDir, "package.json")}`);
3797
3797
  const entry = resolve6(absDir, entryRel);
3798
- const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
3798
+ const isAlwaysResolved = (d) => d === "@s2script/sdk" || d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
3799
3799
  const localDts = localDeclarationFiles(absDir);
3800
3800
  const locallyDeclared = declaredModules(localDts);
3801
3801
  const allDeclaredDeps = [
@@ -3829,10 +3829,12 @@ function typecheckPlugin(pluginDir, opts) {
3829
3829
  ...sharedProgramOptions(ts),
3830
3830
  baseUrl: packagesDir,
3831
3831
  paths: {
3832
- // Builtins are `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback
3833
- // now serves only @s2script/cs2 → packages/cs2/index.d.ts (the legacy per-package builtin
3834
- // dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
3835
- // sdk imports.
3832
+ // Builtins: the root barrel `@s2script/sdk` → packages/sdk/index.d.ts; subpaths
3833
+ // `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback serves
3834
+ // @s2script/cs2 → packages/cs2/index.d.ts (and would also hit sdk/index.d.ts; the exact
3835
+ // `@s2script/sdk` entry makes the barrel obvious). tsc picks the longest matching prefix,
3836
+ // so `@s2script/sdk/*` wins for capability imports.
3837
+ "@s2script/sdk": ["sdk/index.d.ts"],
3836
3838
  "@s2script/sdk/*": ["sdk/*.d.ts"],
3837
3839
  // Game-package SUBPATHS (e.g. @s2script/cs2/econ -> packages/cs2/econ.d.ts). Needed because
3838
3840
  // the `@s2script/*` fallback below maps to `<pkg>/index.d.ts`, which cannot express a
@@ -4171,31 +4173,60 @@ function isPluginContext(type) {
4171
4173
  const sym = type.getSymbol() ?? type.aliasSymbol;
4172
4174
  return sym?.getName() === "PluginContext";
4173
4175
  }
4176
+ function isFromPluginDts(checker, node) {
4177
+ let sym = checker.getSymbolAtLocation(node);
4178
+ if (sym === void 0) return false;
4179
+ if (sym.flags & ts2.SymbolFlags.Alias) {
4180
+ const aliased = checker.getAliasedSymbol(sym);
4181
+ if (aliased !== void 0) sym = aliased;
4182
+ }
4183
+ for (const d of sym.declarations ?? []) {
4184
+ const f = d.getSourceFile().fileName.replace(/\\/g, "/");
4185
+ if (f.endsWith("/plugin.d.ts")) return true;
4186
+ }
4187
+ return false;
4188
+ }
4189
+ function recordPublishOrUse(out, method, arg0, sf, node) {
4190
+ if (method === "publish") {
4191
+ if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4192
+ out.publishNames.push(arg0.text);
4193
+ } else {
4194
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
4195
+ out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
4196
+ }
4197
+ } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4198
+ out.useNames.push(arg0.text);
4199
+ }
4200
+ }
4201
+ function recordImportSpecifier(out, spec) {
4202
+ if (spec.startsWith(".") || spec.startsWith("@s2script/")) return;
4203
+ out.importNames.push(spec);
4204
+ }
4174
4205
  function scanPluginProgram(program, pluginDir) {
4175
4206
  const checker = program.getTypeChecker();
4176
- const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
4207
+ const out = { publishNames: [], dynamicPublishSites: [], useNames: [], importNames: [] };
4177
4208
  const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
4178
4209
  for (const sf of program.getSourceFiles()) {
4179
4210
  if (sf.isDeclarationFile) continue;
4180
4211
  if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
4181
4212
  const visit = (node) => {
4182
- if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
4183
- const method = node.expression.name.text;
4184
- if (method === "publish" || method === "use" || method === "tryUse") {
4185
- const recv = checker.getTypeAtLocation(node.expression.expression);
4186
- if (isPluginContext(recv)) {
4187
- const arg0 = node.arguments[0];
4188
- if (method === "publish") {
4189
- if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4190
- out.publishNames.push(arg0.text);
4191
- } else {
4192
- const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
4193
- out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
4194
- }
4195
- } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4196
- out.useNames.push(arg0.text);
4213
+ if ((ts2.isImportDeclaration(node) || ts2.isExportDeclaration(node)) && node.moduleSpecifier !== void 0 && ts2.isStringLiteralLike(node.moduleSpecifier)) {
4214
+ recordImportSpecifier(out, node.moduleSpecifier.text);
4215
+ }
4216
+ if (ts2.isCallExpression(node)) {
4217
+ if (ts2.isPropertyAccessExpression(node.expression)) {
4218
+ const method = node.expression.name.text;
4219
+ if (method === "publish" || method === "use" || method === "tryUse") {
4220
+ const recv = checker.getTypeAtLocation(node.expression.expression);
4221
+ if (isPluginContext(recv)) {
4222
+ recordPublishOrUse(out, method, node.arguments[0], sf, node);
4197
4223
  }
4198
4224
  }
4225
+ } else if (ts2.isIdentifier(node.expression)) {
4226
+ const name = node.expression.text;
4227
+ if ((name === "publish" || name === "use" || name === "tryUse") && isFromPluginDts(checker, node.expression)) {
4228
+ recordPublishOrUse(out, name, node.arguments[0], sf, node);
4229
+ }
4199
4230
  }
4200
4231
  }
4201
4232
  ts2.forEachChild(node, visit);
@@ -4204,6 +4235,7 @@ function scanPluginProgram(program, pluginDir) {
4204
4235
  }
4205
4236
  out.publishNames = [...new Set(out.publishNames)];
4206
4237
  out.useNames = [...new Set(out.useNames)];
4238
+ out.importNames = [...new Set(out.importNames)];
4207
4239
  return out;
4208
4240
  }
4209
4241
 
@@ -4569,12 +4601,14 @@ function emitHookDts(model) {
4569
4601
  out.push(...emitViewIface(h2));
4570
4602
  }
4571
4603
  }
4572
- for (const { ctxIface, hooks } of model) {
4604
+ for (const { ctxIface, hooks, ns } of model) {
4573
4605
  out.push(`export interface ${ctxIface} {`);
4574
4606
  for (const h2 of hooks) {
4575
4607
  out.push(` ${h2.name}(handler: (view: ${h2.viewIface}) => HookResultValue | void): void;`);
4576
4608
  }
4577
4609
  out.push("}", "");
4610
+ out.push(`export declare const ${ns}: ${ctxIface};`);
4611
+ out.push("");
4578
4612
  }
4579
4613
  out.push('declare module "@s2script/sdk/plugin" {');
4580
4614
  out.push(" interface PluginContext {");
@@ -4837,13 +4871,16 @@ ${lint.output}`);
4837
4871
  for (const used of scan.useNames) {
4838
4872
  if (!declaredDeps.has(used)) {
4839
4873
  console.warn(
4840
- `WARN: ctx.use/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
4874
+ `WARN: use()/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
4841
4875
  );
4842
4876
  }
4843
4877
  }
4878
+ const referenced = /* @__PURE__ */ new Set([...scan.useNames, ...scan.importNames]);
4844
4879
  for (const dep of declaredDeps) {
4845
- if (!scan.useNames.includes(dep)) {
4846
- console.warn(`WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d`);
4880
+ if (!referenced.has(dep)) {
4881
+ console.warn(
4882
+ `WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d or imported`
4883
+ );
4847
4884
  }
4848
4885
  }
4849
4886
  const { name, version } = pkg;
@@ -4871,7 +4908,7 @@ ${lint.output}`);
4871
4908
  platform: "neutral",
4872
4909
  format: "cjs",
4873
4910
  external,
4874
- target: "es2020",
4911
+ target: "es2024",
4875
4912
  // platform:"neutral" defaults mainFields to EMPTY, so a workspace-sibling package that
4876
4913
  // declares `main` (rather than `exports`) fails to resolve with an undiagnosable error.
4877
4914
  // Set it explicitly so a monorepo plugin bundles whichever field the author wrote.
@@ -8071,29 +8108,33 @@ function fileDevDeps(packagesDir, game) {
8071
8108
  }
8072
8109
  function pluginSource(game) {
8073
8110
  if (game === "cs2") {
8074
- return `import { plugin } from "@s2script/sdk/plugin";
8075
- import { Chat } from "@s2script/sdk/chat";
8111
+ return `import { command, Chat, HookResult } from "@s2script/sdk";
8112
+ import type { Command, HookResultValue } from "@s2script/sdk";
8076
8113
 
8077
- export default plugin((ctx) => {
8078
- ctx.commands.register("hello", (cmd) => {
8079
- cmd.reply("hello from s2script");
8080
- if (cmd.callerSlot >= 0) {
8081
- Chat.toSlot(cmd.callerSlot, "hello from s2script");
8082
- }
8083
- });
8084
- });
8114
+ export function OnPluginStart(): void {
8115
+ command("hello", hello);
8116
+ }
8117
+
8118
+ function hello(cmd: Command): HookResultValue {
8119
+ cmd.reply("hello from s2script");
8120
+ if (cmd.callerSlot >= 0) {
8121
+ Chat.toSlot(cmd.callerSlot, "hello from s2script");
8122
+ }
8123
+ return HookResult.Handled;
8124
+ }
8085
8125
  `;
8086
8126
  }
8087
- return `import { plugin } from "@s2script/sdk/plugin";
8088
- import { delay } from "@s2script/sdk/timers";
8127
+ return `import { delay } from "@s2script/sdk";
8089
8128
 
8090
- export default plugin((ctx) => {
8091
- let n = 0;
8092
- ctx.server.onGameFrame(() => {
8093
- n += 1;
8094
- });
8129
+ let n = 0;
8130
+
8131
+ export function OnPluginStart(): void {
8095
8132
  void delay(1000).then(() => console.log("s2script plugin alive; frames so far:", n));
8096
- });
8133
+ }
8134
+
8135
+ export function OnGameFrame(): void {
8136
+ n += 1;
8137
+ }
8097
8138
  `;
8098
8139
  }
8099
8140
  var LIBRARY_ENTRY = `/** Encode a string. Replace this with the library's real surface. */
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/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.22.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;
package/sdkhooks.d.ts ADDED
@@ -0,0 +1,543 @@
1
+ /**
2
+ * @s2script/sdkhooks — per-entity SDKHooks (SourceMod `SDKHook` / `SDKUnhook`).
3
+ * NO runtime code: the engine injects the implementation at load time.
4
+ */
5
+ import type { EntityRef } from "./entity";
6
+ import type { DamageInfo } from "./damage";
7
+ import type { HookResultValue } from "./events";
8
+ import type { Client } from "./clients";
9
+
10
+ type TouchCallback = (entity: EntityRef, other: EntityRef | null) => HookResultValue | void;
11
+ type TouchPostCallback = (entity: EntityRef, other: EntityRef | null) => void;
12
+ type WeaponCallback = (entity: EntityRef, weapon: EntityRef | null) => HookResultValue | void;
13
+ type WeaponPostCallback = (entity: EntityRef, weapon: EntityRef | null) => void;
14
+ type ThisCallback = (entity: EntityRef) => HookResultValue | void;
15
+ type ThisVoidCallback = (entity: EntityRef) => void;
16
+ type FireBulletsPostCallback = (entity: EntityRef, shots: number, weaponName: string) => void;
17
+ type ReloadPostCallback = (weapon: EntityRef, successful: boolean) => void;
18
+ type UseCallback = (
19
+ entity: EntityRef,
20
+ activator: EntityRef | null,
21
+ caller: EntityRef | null,
22
+ type: UseTypeValue,
23
+ value: number,
24
+ ) => HookResultValue | void;
25
+ type UsePostCallback = (
26
+ entity: EntityRef,
27
+ activator: EntityRef | null,
28
+ caller: EntityRef | null,
29
+ type: UseTypeValue,
30
+ value: number,
31
+ ) => void;
32
+
33
+ /**
34
+ * `CBaseEntity::Use` use-type (`USE_OFF` / `USE_ON` / `USE_SET` / `USE_TOGGLE`).
35
+ */
36
+ export type UseTypeValue = 0 | 1 | 2 | 3;
37
+
38
+ /**
39
+ * `CBaseEntity::Use` use-type constants (wiki `USE_*`).
40
+ */
41
+ export declare const UseType: {
42
+ /** `USE_OFF` — turn off. */
43
+ readonly Off: 0;
44
+ /** `USE_ON` — turn on. */
45
+ readonly On: 1;
46
+ /** `USE_SET` — set to `value`. */
47
+ readonly Set: 2;
48
+ /** `USE_TOGGLE` — toggle. */
49
+ readonly Toggle: 3;
50
+ };
51
+
52
+ /**
53
+ * Shipped SDKHook types. Wiki names without the `SDKHook_` prefix. A wiki name whose engine
54
+ * backing failed or is absent returns `false` from {@link SDKHook} (does not throw). A string
55
+ * that is not a member here throws.
56
+ */
57
+ export declare const SDKHookType: {
58
+ /** Pre-apply damage (`DispatchTraceAttack`). Mutate {@link DamageInfo.damage} in place. */
59
+ readonly OnTakeDamage: "OnTakeDamage";
60
+ /**
61
+ * CheckTransmit mux (wiki `SDKHook_SetTransmit`). `Handled` / `Stop` hide this entity from this
62
+ * viewer. AND-merge with `Transmit.setVisibleTo`: either API can hide; SetTransmit cannot
63
+ * un-hide a native mask clear. There is no `SetTransmitPost`.
64
+ */
65
+ readonly SetTransmit: "SetTransmit";
66
+ /** `CBaseEntity::StartTouch` pre. `Handled` / `Stop` skip the original virtual. */
67
+ readonly StartTouch: "StartTouch";
68
+ /** `CBaseEntity::Touch` pre. `Handled` / `Stop` skip the original virtual. */
69
+ readonly Touch: "Touch";
70
+ /** `CBaseEntity::EndTouch` pre. `Handled` / `Stop` skip the original virtual. */
71
+ readonly EndTouch: "EndTouch";
72
+ /** `CBaseEntity::Blocked` pre. `Handled` / `Stop` skip the original virtual. */
73
+ readonly Blocked: "Blocked";
74
+ /** `StartTouch` post. Return is ignored. */
75
+ readonly StartTouchPost: "StartTouchPost";
76
+ /** `Touch` post. Return is ignored. */
77
+ readonly TouchPost: "TouchPost";
78
+ /** `EndTouch` post. Return is ignored. */
79
+ readonly EndTouchPost: "EndTouchPost";
80
+ /** `Blocked` post. Return is ignored. */
81
+ readonly BlockedPost: "BlockedPost";
82
+ /** `CBaseEntity::Spawn` pre. `Handled` / `Stop` skip the original virtual. */
83
+ readonly Spawn: "Spawn";
84
+ /** `Spawn` post. Return is ignored. */
85
+ readonly SpawnPost: "SpawnPost";
86
+ /** `CBaseEntity::Think` pre. `Handled` / `Stop` skip the original virtual. */
87
+ readonly Think: "Think";
88
+ /** `Think` post. Return is ignored. */
89
+ readonly ThinkPost: "ThinkPost";
90
+ /** Player `PreThink`. Return is ignored. */
91
+ readonly PreThink: "PreThink";
92
+ /** `PreThink` post. Return is ignored. */
93
+ readonly PreThinkPost: "PreThinkPost";
94
+ /** Player `PostThink`. Return is ignored. */
95
+ readonly PostThink: "PostThink";
96
+ /** `PostThink` post. Return is ignored. */
97
+ readonly PostThinkPost: "PostThinkPost";
98
+ /** `CBaseEntity::Use` pre. `Handled` / `Stop` skip the original virtual. */
99
+ readonly Use: "Use";
100
+ /** `Use` post. Return is ignored. */
101
+ readonly UsePost: "UsePost";
102
+ /** `GetMaxHealth`. Mutate `info.maxHealth`. `Handled` / `Stop` SUPERCEDE the original. */
103
+ readonly GetMaxHealth: "GetMaxHealth";
104
+ /** `ShouldCollide`. Return a boolean (not {@link HookResultValue}); last defined wins. */
105
+ readonly ShouldCollide: "ShouldCollide";
106
+ /** `VPhysicsUpdate`. Return is ignored. */
107
+ readonly VPhysicsUpdate: "VPhysicsUpdate";
108
+ /** `VPhysicsUpdate` post. Return is ignored. */
109
+ readonly VPhysicsUpdatePost: "VPhysicsUpdatePost";
110
+ /** `GroundEntChanged` post. Return is ignored. */
111
+ readonly GroundEntChangedPost: "GroundEntChangedPost";
112
+ /** `CanBeAutobalanced`. Return a boolean (not {@link HookResultValue}); last defined wins. */
113
+ readonly CanBeAutobalanced: "CanBeAutobalanced";
114
+ /**
115
+ * Post-apply damage (`DispatchTraceAttack` after the original). {@link DamageInfo} is read-only
116
+ * for effect; mutations are not written back. Return is ignored.
117
+ */
118
+ readonly OnTakeDamagePost: "OnTakeDamagePost";
119
+ /**
120
+ * `OnTakeDamageAlive` pre. CS2 has no distinct function yet — {@link SDKHook} returns `false`.
121
+ */
122
+ readonly OnTakeDamageAlive: "OnTakeDamageAlive";
123
+ /**
124
+ * `OnTakeDamageAlive` post. CS2 has no distinct function yet — {@link SDKHook} returns `false`.
125
+ */
126
+ readonly OnTakeDamageAlivePost: "OnTakeDamageAlivePost";
127
+ /**
128
+ * `TraceAttack` pre ({@link TraceAttackInfo}). No distinct CS2 virtual yet — {@link SDKHook}
129
+ * returns `false` (OnTakeDamage remains the DTA mux).
130
+ */
131
+ readonly TraceAttack: "TraceAttack";
132
+ /**
133
+ * `TraceAttack` post. No distinct CS2 virtual yet — {@link SDKHook} returns `false`.
134
+ */
135
+ readonly TraceAttackPost: "TraceAttackPost";
136
+ /**
137
+ * `FireBulletsPost`. Wiki already documents this as often absent. {@link SDKHook} returns `false`
138
+ * until a live path is found.
139
+ */
140
+ readonly FireBulletsPost: "FireBulletsPost";
141
+ /** Weapon reload pre. `Handled` / `Stop` skip the original virtual. Missing VP → `false`. */
142
+ readonly Reload: "Reload";
143
+ /** Weapon reload post. `(weapon, successful)`. No bool-return thunk yet — {@link SDKHook} returns `false`. */
144
+ readonly ReloadPost: "ReloadPost";
145
+ /** `WeaponCanUse` pre. Callback is `(entity, weapon)`. Missing ItemServices VP → `false`. */
146
+ readonly WeaponCanUse: "WeaponCanUse";
147
+ /** `WeaponCanUse` post. Return is ignored. Missing VP → `false`. */
148
+ readonly WeaponCanUsePost: "WeaponCanUsePost";
149
+ /** `WeaponCanSwitchTo` pre. Callback is `(entity, weapon)`. Missing VP → `false`. */
150
+ readonly WeaponCanSwitchTo: "WeaponCanSwitchTo";
151
+ /** `WeaponCanSwitchTo` post. Return is ignored. Missing VP → `false`. */
152
+ readonly WeaponCanSwitchToPost: "WeaponCanSwitchToPost";
153
+ /** `WeaponDrop` pre. Callback is `(entity, weapon)`. Missing VP → `false`. */
154
+ readonly WeaponDrop: "WeaponDrop";
155
+ /** `WeaponDrop` post. Return is ignored. Missing VP → `false`. */
156
+ readonly WeaponDropPost: "WeaponDropPost";
157
+ /** `WeaponEquip` pre. Callback is `(entity, weapon)`. Missing VP → `false`. */
158
+ readonly WeaponEquip: "WeaponEquip";
159
+ /** `WeaponEquip` post. Return is ignored. Missing VP → `false`. */
160
+ readonly WeaponEquipPost: "WeaponEquipPost";
161
+ /** `WeaponSwitch` pre. Callback is `(entity, weapon)`. Missing VP → `false`. */
162
+ readonly WeaponSwitch: "WeaponSwitch";
163
+ /** `WeaponSwitch` post. Return is ignored. Missing VP → `false`. */
164
+ readonly WeaponSwitchPost: "WeaponSwitchPost";
165
+ };
166
+
167
+ /**
168
+ * Block-scoped view of a `TraceAttack` callback. Mutate {@link TraceAttackInfo.damage} on the
169
+ * pre-hook. Valid only during the handler.
170
+ */
171
+ export interface TraceAttackInfo {
172
+ /** Incoming damage. Mutate in place on the pre-hook. */
173
+ damage: number;
174
+ /** Damage type bits. */
175
+ readonly damageType: number;
176
+ /** Ammo type index. */
177
+ readonly ammoType: number;
178
+ /** Hitbox index. */
179
+ readonly hitbox: number;
180
+ /** Hitgroup index. */
181
+ readonly hitgroup: number;
182
+ /** Attacking entity, books-gated. */
183
+ readonly attacker: EntityRef | null;
184
+ /** Inflicting entity, books-gated. */
185
+ readonly inflictor: EntityRef | null;
186
+ /** Victim entity, books-gated. */
187
+ readonly victim: EntityRef | null;
188
+ }
189
+
190
+ /**
191
+ * Hook `entity` for `type`. Auto-unhooked on entity destroy and plugin unload.
192
+ *
193
+ * Not load-window-only — call from `OnEntityCreated`, `OnPluginStart` (ents already live), or any
194
+ * time you hold a live {@link EntityRef}.
195
+ *
196
+ * `OnTakeDamage`: mutate `info.damage` in place. No return needed. Return `HookResult.Handled` to
197
+ * zero the hit; `HookResult.Stop` to also skip later hooks.
198
+ *
199
+ * @returns `true` if the hook was recorded. `false` on `null`/stale `entity` (does not throw).
200
+ * @example
201
+ * import { SDKHook, SDKHookType, Entity } from "@s2script/sdk";
202
+ * import type { EntityRef, DamageInfo } from "@s2script/sdk";
203
+ *
204
+ * export function OnPluginStart(): void {
205
+ * for (const pawn of Entity.findByClass("player")) {
206
+ * SDKHook(pawn, SDKHookType.OnTakeDamage, onTakeDamage);
207
+ * }
208
+ * }
209
+ * export function OnEntityCreated(entity: EntityRef | null, className: string): void {
210
+ * if (!entity || className !== "player") return;
211
+ * SDKHook(entity, SDKHookType.OnTakeDamage, onTakeDamage);
212
+ * }
213
+ * function onTakeDamage(info: DamageInfo) {
214
+ * info.damage /= 2;
215
+ * }
216
+ */
217
+ export declare function SDKHook(
218
+ entity: EntityRef | null,
219
+ type: "OnTakeDamage",
220
+ callback: (info: DamageInfo) => HookResultValue | void,
221
+ ): boolean;
222
+ /**
223
+ * Post-apply damage. {@link DamageInfo} is a read of the live info after the original ran.
224
+ * Return is ignored; Handled does not zero the hit (that is the pre-hook).
225
+ */
226
+ export declare function SDKHook(
227
+ entity: EntityRef | null,
228
+ type: "OnTakeDamagePost",
229
+ callback: (info: DamageInfo) => void,
230
+ ): boolean;
231
+ /**
232
+ * `OnTakeDamageAlive` pre. No CS2 backing yet — always returns `false`.
233
+ */
234
+ export declare function SDKHook(
235
+ entity: EntityRef | null,
236
+ type: "OnTakeDamageAlive",
237
+ callback: (info: DamageInfo) => HookResultValue | void,
238
+ ): boolean;
239
+ /**
240
+ * `OnTakeDamageAlive` post. No CS2 backing yet — always returns `false`.
241
+ */
242
+ export declare function SDKHook(
243
+ entity: EntityRef | null,
244
+ type: "OnTakeDamageAlivePost",
245
+ callback: (info: DamageInfo) => void,
246
+ ): boolean;
247
+ /**
248
+ * `TraceAttack` pre. No distinct CS2 virtual yet — always returns `false`.
249
+ */
250
+ export declare function SDKHook(
251
+ entity: EntityRef | null,
252
+ type: "TraceAttack",
253
+ callback: (info: TraceAttackInfo) => HookResultValue | void,
254
+ ): boolean;
255
+ /**
256
+ * `TraceAttack` post. No distinct CS2 virtual yet — always returns `false`.
257
+ */
258
+ export declare function SDKHook(
259
+ entity: EntityRef | null,
260
+ type: "TraceAttackPost",
261
+ callback: (info: TraceAttackInfo) => void,
262
+ ): boolean;
263
+ /**
264
+ * `FireBulletsPost`. No CS2 backing yet — always returns `false`.
265
+ */
266
+ export declare function SDKHook(
267
+ entity: EntityRef | null,
268
+ type: "FireBulletsPost",
269
+ callback: FireBulletsPostCallback,
270
+ ): boolean;
271
+ /**
272
+ * Touch-family pre-hook. Callback is `(entity, other)`. Omit return = Continue.
273
+ * `Handled` / `Stop` SUPERCEDE the original virtual (`Stop` also skips later callbacks).
274
+ */
275
+ export declare function SDKHook(
276
+ entity: EntityRef | null,
277
+ type: "StartTouch" | "Touch" | "EndTouch" | "Blocked",
278
+ callback: TouchCallback,
279
+ ): boolean;
280
+ /**
281
+ * Touch-family post-hook. Return is ignored; the original virtual already ran.
282
+ */
283
+ export declare function SDKHook(
284
+ entity: EntityRef | null,
285
+ type: "StartTouchPost" | "TouchPost" | "EndTouchPost" | "BlockedPost",
286
+ callback: TouchPostCallback,
287
+ ): boolean;
288
+ /**
289
+ * Spawn / Think pre-hook. Callback is `(entity)`. Omit return = Continue.
290
+ * `Handled` / `Stop` SUPERCEDE the original virtual (`Stop` also skips later callbacks).
291
+ */
292
+ export declare function SDKHook(
293
+ entity: EntityRef | null,
294
+ type: "Spawn" | "Think" | "Reload",
295
+ callback: ThisCallback,
296
+ ): boolean;
297
+ /**
298
+ * Void this-only hooks (Posts, PreThink/PostThink, VPhysicsUpdate, GroundEntChangedPost).
299
+ * Return is ignored.
300
+ */
301
+ export declare function SDKHook(
302
+ entity: EntityRef | null,
303
+ type:
304
+ | "SpawnPost"
305
+ | "ThinkPost"
306
+ | "PreThink"
307
+ | "PreThinkPost"
308
+ | "PostThink"
309
+ | "PostThinkPost"
310
+ | "VPhysicsUpdate"
311
+ | "VPhysicsUpdatePost"
312
+ | "GroundEntChangedPost",
313
+ callback: ThisVoidCallback,
314
+ ): boolean;
315
+ /**
316
+ * `Use` pre-hook. `Handled` / `Stop` SUPERCEDE the original virtual.
317
+ */
318
+ export declare function SDKHook(
319
+ entity: EntityRef | null,
320
+ type: "Use",
321
+ callback: UseCallback,
322
+ ): boolean;
323
+ /**
324
+ * `Use` post-hook. Return is ignored.
325
+ */
326
+ export declare function SDKHook(
327
+ entity: EntityRef | null,
328
+ type: "UsePost",
329
+ callback: UsePostCallback,
330
+ ): boolean;
331
+ /**
332
+ * `GetMaxHealth`. Mutate `info.maxHealth` in place. `Handled` / `Stop` SUPERCEDE with the new value.
333
+ */
334
+ export declare function SDKHook(
335
+ entity: EntityRef | null,
336
+ type: "GetMaxHealth",
337
+ callback: (info: { maxHealth: number }) => HookResultValue | void,
338
+ ): boolean;
339
+ /**
340
+ * `ShouldCollide`. Return a boolean (not HookResult); last defined return wins, default original.
341
+ */
342
+ export declare function SDKHook(
343
+ entity: EntityRef | null,
344
+ type: "ShouldCollide",
345
+ callback: (
346
+ entity: EntityRef,
347
+ collisionGroup: number,
348
+ contentsMask: number,
349
+ originalResult: boolean,
350
+ ) => boolean,
351
+ ): boolean;
352
+ /**
353
+ * `CanBeAutobalanced`. Return a boolean (not HookResult); last defined return wins, default original.
354
+ * Callback is skipped when the hooked entity has no {@link Client} — never a raw slot `0`.
355
+ */
356
+ export declare function SDKHook(
357
+ entity: EntityRef | null,
358
+ type: "CanBeAutobalanced",
359
+ callback: (client: Client, origRet: boolean) => boolean,
360
+ ): boolean;
361
+ /**
362
+ * SetTransmit. Callback is `(entity, client)`. Omit return = Continue.
363
+ * `Handled` / `Stop` hide this entity from this viewer (AND-merge with `Transmit.setVisibleTo`;
364
+ * cannot un-hide a native mask clear). `Stop` also skips later SetTransmit callbacks on that pair.
365
+ */
366
+ export declare function SDKHook(
367
+ entity: EntityRef | null,
368
+ type: "SetTransmit",
369
+ callback: (entity: EntityRef, client: Client) => HookResultValue | void,
370
+ ): boolean;
371
+ /**
372
+ * Weapon-family pre-hook. Callback is `(entity, weapon)`. Omit return = Continue.
373
+ * `Handled` / `Stop` SUPERCEDE the original virtual. Missing ItemServices VP → `false`.
374
+ */
375
+ export declare function SDKHook(
376
+ entity: EntityRef | null,
377
+ type: "WeaponCanUse" | "WeaponCanSwitchTo" | "WeaponDrop" | "WeaponEquip" | "WeaponSwitch",
378
+ callback: WeaponCallback,
379
+ ): boolean;
380
+ /**
381
+ * Weapon-family post-hook. Return is ignored. Missing VP → `false`.
382
+ */
383
+ export declare function SDKHook(
384
+ entity: EntityRef | null,
385
+ type:
386
+ | "WeaponCanUsePost"
387
+ | "WeaponCanSwitchToPost"
388
+ | "WeaponDropPost"
389
+ | "WeaponEquipPost"
390
+ | "WeaponSwitchPost",
391
+ callback: WeaponPostCallback,
392
+ ): boolean;
393
+ /**
394
+ * `ReloadPost`. Callback is `(weapon, successful)`. No CS2 backing yet — always returns `false`.
395
+ */
396
+ export declare function SDKHook(
397
+ entity: EntityRef | null,
398
+ type: "ReloadPost",
399
+ callback: ReloadPostCallback,
400
+ ): boolean;
401
+
402
+ /**
403
+ * Remove one matching `(entity, type, callback)` hook. Callback identity is the function reference.
404
+ * @returns `true` if an entry was removed.
405
+ */
406
+ export declare function SDKUnhook(
407
+ entity: EntityRef | null,
408
+ type: "OnTakeDamage",
409
+ callback: (info: DamageInfo) => HookResultValue | void,
410
+ ): boolean;
411
+ /** Remove an `OnTakeDamagePost` hook. */
412
+ export declare function SDKUnhook(
413
+ entity: EntityRef | null,
414
+ type: "OnTakeDamagePost",
415
+ callback: (info: DamageInfo) => void,
416
+ ): boolean;
417
+ /** Remove an `OnTakeDamageAlive` hook. */
418
+ export declare function SDKUnhook(
419
+ entity: EntityRef | null,
420
+ type: "OnTakeDamageAlive",
421
+ callback: (info: DamageInfo) => HookResultValue | void,
422
+ ): boolean;
423
+ /** Remove an `OnTakeDamageAlivePost` hook. */
424
+ export declare function SDKUnhook(
425
+ entity: EntityRef | null,
426
+ type: "OnTakeDamageAlivePost",
427
+ callback: (info: DamageInfo) => void,
428
+ ): boolean;
429
+ /** Remove a `TraceAttack` hook. */
430
+ export declare function SDKUnhook(
431
+ entity: EntityRef | null,
432
+ type: "TraceAttack",
433
+ callback: (info: TraceAttackInfo) => HookResultValue | void,
434
+ ): boolean;
435
+ /** Remove a `TraceAttackPost` hook. */
436
+ export declare function SDKUnhook(
437
+ entity: EntityRef | null,
438
+ type: "TraceAttackPost",
439
+ callback: (info: TraceAttackInfo) => void,
440
+ ): boolean;
441
+ /** Remove a `FireBulletsPost` hook. */
442
+ export declare function SDKUnhook(
443
+ entity: EntityRef | null,
444
+ type: "FireBulletsPost",
445
+ callback: FireBulletsPostCallback,
446
+ ): boolean;
447
+ /** Remove a Touch-family pre-hook. */
448
+ export declare function SDKUnhook(
449
+ entity: EntityRef | null,
450
+ type: "StartTouch" | "Touch" | "EndTouch" | "Blocked",
451
+ callback: TouchCallback,
452
+ ): boolean;
453
+ /** Remove a Touch-family post-hook. */
454
+ export declare function SDKUnhook(
455
+ entity: EntityRef | null,
456
+ type: "StartTouchPost" | "TouchPost" | "EndTouchPost" | "BlockedPost",
457
+ callback: TouchPostCallback,
458
+ ): boolean;
459
+ /** Remove a Spawn / Think pre-hook. */
460
+ export declare function SDKUnhook(
461
+ entity: EntityRef | null,
462
+ type: "Spawn" | "Think" | "Reload",
463
+ callback: ThisCallback,
464
+ ): boolean;
465
+ /** Remove a void this-only hook. */
466
+ export declare function SDKUnhook(
467
+ entity: EntityRef | null,
468
+ type:
469
+ | "SpawnPost"
470
+ | "ThinkPost"
471
+ | "PreThink"
472
+ | "PreThinkPost"
473
+ | "PostThink"
474
+ | "PostThinkPost"
475
+ | "VPhysicsUpdate"
476
+ | "VPhysicsUpdatePost"
477
+ | "GroundEntChangedPost",
478
+ callback: ThisVoidCallback,
479
+ ): boolean;
480
+ /** Remove a `Use` pre-hook. */
481
+ export declare function SDKUnhook(
482
+ entity: EntityRef | null,
483
+ type: "Use",
484
+ callback: UseCallback,
485
+ ): boolean;
486
+ /** Remove a `Use` post-hook. */
487
+ export declare function SDKUnhook(
488
+ entity: EntityRef | null,
489
+ type: "UsePost",
490
+ callback: UsePostCallback,
491
+ ): boolean;
492
+ /** Remove a `GetMaxHealth` hook. */
493
+ export declare function SDKUnhook(
494
+ entity: EntityRef | null,
495
+ type: "GetMaxHealth",
496
+ callback: (info: { maxHealth: number }) => HookResultValue | void,
497
+ ): boolean;
498
+ /** Remove a `ShouldCollide` hook. */
499
+ export declare function SDKUnhook(
500
+ entity: EntityRef | null,
501
+ type: "ShouldCollide",
502
+ callback: (
503
+ entity: EntityRef,
504
+ collisionGroup: number,
505
+ contentsMask: number,
506
+ originalResult: boolean,
507
+ ) => boolean,
508
+ ): boolean;
509
+ /** Remove a `CanBeAutobalanced` hook. */
510
+ export declare function SDKUnhook(
511
+ entity: EntityRef | null,
512
+ type: "CanBeAutobalanced",
513
+ callback: (client: Client, origRet: boolean) => boolean,
514
+ ): boolean;
515
+ /** Remove a SetTransmit hook. */
516
+ export declare function SDKUnhook(
517
+ entity: EntityRef | null,
518
+ type: "SetTransmit",
519
+ callback: (entity: EntityRef, client: Client) => HookResultValue | void,
520
+ ): boolean;
521
+ /** Remove a Weapon-family pre-hook. */
522
+ export declare function SDKUnhook(
523
+ entity: EntityRef | null,
524
+ type: "WeaponCanUse" | "WeaponCanSwitchTo" | "WeaponDrop" | "WeaponEquip" | "WeaponSwitch",
525
+ callback: WeaponCallback,
526
+ ): boolean;
527
+ /** Remove a Weapon-family post-hook. */
528
+ export declare function SDKUnhook(
529
+ entity: EntityRef | null,
530
+ type:
531
+ | "WeaponCanUsePost"
532
+ | "WeaponCanSwitchToPost"
533
+ | "WeaponDropPost"
534
+ | "WeaponEquipPost"
535
+ | "WeaponSwitchPost",
536
+ callback: WeaponPostCallback,
537
+ ): boolean;
538
+ /** Remove a `ReloadPost` hook. */
539
+ export declare function SDKUnhook(
540
+ entity: EntityRef | null,
541
+ type: "ReloadPost",
542
+ callback: ReloadPostCallback,
543
+ ): boolean;
package/translations.d.ts CHANGED
@@ -13,13 +13,15 @@ export type Phrases = Record<string, string>;
13
13
  * for it automatically, the same rule SourceMod's `LoadTranslations` enforces:
14
14
  *
15
15
  * @example
16
- * export default plugin((ctx) => {
17
- * ctx.translations.load("basecomm", "common");
18
- * ctx.commands.registerAdmin("sm_gag", ADMFLAG.CHAT, (cmd) => {
16
+ * import { command, translations } from "@s2script/sdk";
17
+ * import { ADMFLAG } from "@s2script/sdk";
18
+ * export function OnPluginStart(): void {
19
+ * translations.load("basecomm", "common");
20
+ * command.admin("sm_gag", ADMFLAG.CHAT, (cmd) => {
19
21
  * cmd.replyT("Usage Gag"); // key-checked
20
22
  * Chat.toSlot(p.slot, Translations.translate(p.slot, "Gagged Player", n));
21
23
  * });
22
- * });
24
+ * }
23
25
  *
24
26
  * Keys are checked against the files that plugin loads — see `@s2script/sdk/phrases`.
25
27
  */