@s2script/sdk 0.31.0 → 0.33.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/clients.d.ts CHANGED
@@ -73,6 +73,94 @@ export declare class Client {
73
73
  * client.fakeCommand("say hello"); // as though the player typed it
74
74
  */
75
75
  fakeCommand(cmd: string): boolean;
76
+ /**
77
+ * Ask this client for its own value of a cvar (SourceMod `QueryClientConVar`). The client
78
+ * answers over the network, so this resolves on a later frame.
79
+ *
80
+ * Resolves `null` when no query was sent (a bot, a disconnected client, a name that is empty or
81
+ * contains whitespace, or a build where the reply hook is unavailable) or when the client
82
+ * disconnects or has not answered within 15 seconds. `value` is `""` unless `status` is `"ok"`.
83
+ *
84
+ * The value is what the client reports. A modified client can report anything, so treat it as a
85
+ * hint, not proof.
86
+ *
87
+ * @example
88
+ * const reply = await client.queryConVar("cl_interp_ratio");
89
+ * if (reply?.status === "ok") console.log(`${client.name}: ${reply.value}`);
90
+ */
91
+ queryConVar(name: string): Promise<ConVarQueryResult | null>;
92
+ /**
93
+ * Set this client's own copy of a replicated cvar without touching the server's value
94
+ * (SourceMod `SendConVarValue`). The client applies it only to cvars the server is allowed to
95
+ * replicate; the next server-side change to that cvar overwrites it. Booleans send `"1"`/`"0"`.
96
+ *
97
+ * Returns `false` for a bot, a disconnected client or an empty name.
98
+ *
99
+ * @example
100
+ * client.replicateConVar("sv_cheats", true); // client-side only, e.g. for a practice HUD
101
+ */
102
+ replicateConVar(name: string, value: string | number | boolean): boolean;
103
+ /**
104
+ * One snapshot of this client's network counters, read from the engine's net channel
105
+ * (SourceMod `GetClientLatency`, `GetClientAvgLoss`, `GetClientAvgChoke`, `GetClientAvgData`,
106
+ * `GetClientAvgPackets`, `GetClientTime`, `IsClientTimingOut` and `GetClientDataRate` in one
107
+ * call). Read it again for fresh values; nothing in it updates on its own.
108
+ *
109
+ * Returns `null` for a bot (bots have no net channel), a disconnected client, or a server build
110
+ * whose net-channel layout this build has not been verified against.
111
+ *
112
+ * @example
113
+ * const stats = client.netStats();
114
+ * if (stats) console.log(`${client.name}: ${Math.round(stats.avgLatency * 1000)} ms, ` +
115
+ * `${(stats.avgLoss.incoming * 100).toFixed(1)}% loss`);
116
+ */
117
+ netStats(): ClientNetStats | null;
118
+ }
119
+ /** A value measured separately for each direction, as seen from the server. */
120
+ export interface NetFlowPair {
121
+ /** Server to client. */
122
+ outgoing: number;
123
+ /** Client to server. */
124
+ incoming: number;
125
+ }
126
+ /** What {@link Client.netStats} returns. Times are in seconds. */
127
+ export interface ClientNetStats {
128
+ /** Average packet latency. */
129
+ avgLatency: number;
130
+ /** The engine's current round-trip estimate: more accurate than `avgLatency`, but jittery. */
131
+ engineLatency: number;
132
+ /** Average packet loss, from 0 to 1. */
133
+ avgLoss: NetFlowPair;
134
+ /** Average choke, from 0 to 1. */
135
+ avgChoke: NetFlowPair;
136
+ /** Average data rate in bytes per second. */
137
+ avgData: NetFlowPair;
138
+ /** Average packets per second. */
139
+ avgPackets: NetFlowPair;
140
+ /** Bytes sent or received since the client connected. */
141
+ totalData: NetFlowPair;
142
+ /** Packets sent or received since the client connected. */
143
+ totalPackets: NetFlowPair;
144
+ /** How long the client has been connected. */
145
+ timeConnected: number;
146
+ /** Time since the last packet from the client arrived. */
147
+ timeSinceLastReceived: number;
148
+ /** Whether the channel is about to time out. */
149
+ isTimingOut: boolean;
150
+ /** The send data rate the channel is allowed, in bytes per second. */
151
+ dataRate: number;
152
+ }
153
+ /**
154
+ * How a client answered {@link Client.queryConVar}: `"ok"` (value is set), `"notFound"` (no such
155
+ * cvar), `"notValid"` (the name is a command, not a cvar), `"protected"` (the client refused to
156
+ * reveal it), or `"unknown"` for a status this build does not recognise.
157
+ */
158
+ export type ConVarQueryStatus = "ok" | "notFound" | "notValid" | "protected" | "unknown";
159
+ /** The client's answer to {@link Client.queryConVar}. */
160
+ export interface ConVarQueryResult {
161
+ status: ConVarQueryStatus;
162
+ /** The client's value, or `""` when `status` is not `"ok"`. */
163
+ value: string;
76
164
  }
77
165
  /**
78
166
  * Look up connected clients by slot or enumerate them all.
package/dist/cli.js CHANGED
@@ -3517,7 +3517,7 @@ function stripJsonComments(src) {
3517
3517
  // src/gamedata/types.ts
3518
3518
  var PLATFORM = "linuxsteamrt64";
3519
3519
  var ARG_KINDS = ["bool", "int", "float", "string", "utlstring", "vector", "entity", "ptr"];
3520
- var RET_KINDS = ["void", "bool", "int", "float", "entity", "ptr"];
3520
+ var RET_KINDS = ["void", "bool", "int", "float", "entity", "ptr", "utlstringlist"];
3521
3521
  var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "utlstring", "vector", "entity", "ptr"]);
3522
3522
  var MAX_INT_ARGS = 9;
3523
3523
  var MAX_FLOAT_ARGS = 8;
@@ -3587,11 +3587,14 @@ function validatePluginGamedata(gd, opts) {
3587
3587
  }
3588
3588
  const decl = rawDecl;
3589
3589
  const recv = decl.receiver;
3590
- if (!recv || recv.kind !== "entity" && recv.kind !== "none") {
3591
- errs.push(`${where2}: receiver.kind must be "entity" or "none"`);
3592
- } else if (recv.kind === "none") {
3590
+ if (!recv || recv.kind !== "entity" && recv.kind !== "none" && recv.kind !== "interface") {
3591
+ errs.push(`${where2}: receiver.kind must be "entity", "none" or "interface"`);
3592
+ } else if (recv.kind === "none" || recv.kind === "interface") {
3593
3593
  if (recv.via !== void 0) {
3594
- errs.push(`${where2}: receiver.kind "none" cannot carry a 'via' sub-object hop`);
3594
+ errs.push(`${where2}: receiver.kind "${recv.kind}" cannot carry a 'via' sub-object hop`);
3595
+ }
3596
+ if (recv.kind === "interface" && (typeof recv.name !== "string" || !recv.name.length)) {
3597
+ errs.push(`${where2}: receiver.kind "interface" needs a 'name' (the interface version string)`);
3595
3598
  }
3596
3599
  } else if (recv.via !== void 0) {
3597
3600
  const via = recv.via;
@@ -3959,14 +3962,15 @@ var RET_TS = {
3959
3962
  int: "number | null",
3960
3963
  float: "number | null",
3961
3964
  entity: "EntityRef | null",
3962
- ptr: "Ptr | null"
3965
+ ptr: "Ptr | null",
3966
+ utlstringlist: "string[] | null"
3963
3967
  };
3964
3968
  function generateGamedataTypes(gd) {
3965
3969
  const calls = gd.calls ?? {};
3966
3970
  const lines = Object.keys(calls).sort().map((name) => {
3967
3971
  const decl = calls[name];
3968
3972
  const names = decl.argNames;
3969
- const receiverless = decl.receiver?.kind === "none";
3973
+ const receiverless = decl.receiver?.kind === "none" || decl.receiver?.kind === "interface";
3970
3974
  const params = [
3971
3975
  ...receiverless ? [] : ["self: EntityRef"],
3972
3976
  ...decl.args.map((a2, i2) => `${names?.[i2] ?? `a${i2}`}: ${ARG_TS[a2]}`)
package/entity.d.ts CHANGED
@@ -88,6 +88,25 @@ export declare class EntityRef {
88
88
  * at `finalOff` into a number[]. All in-core (raw pointers never cross); null if the root is stale or any hop
89
89
  * is null. */
90
90
  readFloatsChain(ptrOffs: number[], finalOff: number, count: number): number[] | null;
91
+ /**
92
+ * Zero-allocation bulk read for per-tick hot paths: one liveness check, then every field `plan`
93
+ * names, written as numbers into your preallocated `out` starting at `outIndex` (default 0).
94
+ *
95
+ * `plan` is a flat run of entries `[hops, hop0..hopN-1, finalOff, kind, count]`: follow `hops`
96
+ * (0..8) pointer offsets, then read `count` (1..16) contiguous {@link GatherKind} elements at
97
+ * `finalOff`. An entry whose chain hits a null pointer writes `NaN` into its slots; the rest of
98
+ * the plan still runs. Build the plan once and reuse both arrays every tick.
99
+ *
100
+ * Returns the slot count written, `-1` when the ref is stale, or `-2` for a malformed plan or an
101
+ * `out` too short for the whole plan (nothing is written in either failure case).
102
+ * @example
103
+ * import { GatherKind } from "@s2script/sdk/entity";
104
+ * // two floats at +0x10, then one u8 behind the pointer at +0x30
105
+ * const plan = new Int32Array([0, 0x10, GatherKind.Float32, 2, 1, 0x30, 0x8, GatherKind.UInt8, 1]);
106
+ * const out = new Float64Array(3);
107
+ * if (ref.gather(plan, out) === 3) console.log(out[0], out[1], out[2]);
108
+ */
109
+ gather(plan: Int32Array, out: Float64Array, outIndex?: number): number;
91
110
  /** The `Vector` at `offset`, or null if the ref is stale. */
92
111
  readVector(offset: number): Vector | null;
93
112
  /** The `QAngle` at `offset`, or null if the ref is stale. */
@@ -166,6 +185,38 @@ export declare class EntityRef {
166
185
  * `trigger_multiple` needs a model to build the physics volume that fires touch. Returns false
167
186
  * if the op is unavailable or the ref is stale. */
168
187
  setModel(name: string): boolean;
188
+ /**
189
+ * The index of the bone called `name` on this entity's model, for {@link getBoneTransform}.
190
+ * `-1` when the model has no such bone, the entity has no model, or the ref is stale.
191
+ *
192
+ * @example
193
+ * const head = pawn.ref.lookupBone("head_0");
194
+ * const t = head >= 0 ? pawn.ref.getBoneTransform(head) : null;
195
+ * if (t) console.log(`head at ${t.position}`);
196
+ */
197
+ lookupBone(name: string): number;
198
+ /**
199
+ * Where bone `bone` is right now, in world space: its position and its orientation as a
200
+ * quaternion. `null` for a stale ref, an entity with no model, or a negative index. An index past
201
+ * the model's last bone gives the entity's own transform, as the engine does.
202
+ */
203
+ getBoneTransform(bone: number): BoneTransform | null;
204
+ /**
205
+ * The index of the attachment called `name` on this entity's model (`"muzzle_flash"`,
206
+ * `"eyes"`), for {@link getAttachment}. `0` when there is no such attachment, the entity has no
207
+ * model, or the ref is stale; real attachments start at 1.
208
+ */
209
+ lookupAttachment(name: string): number;
210
+ /**
211
+ * Where attachment `attachment` is right now, in world space. `null` for a stale ref, an entity
212
+ * with no model, an index outside 1–255, or an attachment the engine did not find.
213
+ *
214
+ * @example
215
+ * const muzzle = weapon.ref.lookupAttachment("muzzle_flash");
216
+ * const at = weapon.ref.getAttachment(muzzle);
217
+ * if (at) console.log(`muzzle at ${at.origin}, facing ${at.angles}`);
218
+ */
219
+ getAttachment(attachment: number): AttachmentTransform | null;
169
220
  /** Set this entity's gravity multiplier (`CBaseEntity::SetGravityScale`). 1 is normal, 0 is
170
221
  * weightless.
171
222
  *
@@ -210,8 +261,50 @@ export declare class EntityRef {
210
261
  * output the input triggers. `delay` queues the event on the engine's same-tick I/O pump (0 = fires
211
262
  * this same tick — NOT synchronous-within-the-call). Returns false with no op / a stale ref. */
212
263
  acceptInput(input: string, value?: string, activator?: EntityRef, caller?: EntityRef, delay?: number): boolean;
264
+ /**
265
+ * Deal `damage` to this entity through the game's own damage path, so `OnTakeDamage` hooks and
266
+ * the game's rules (god mode, friendly fire, armor) still apply. The engine may still refuse it,
267
+ * for example when the entity doesn't take damage at all. Returns false when the call was refused:
268
+ * a stale victim or option entity, a non-finite `damage`, or an unavailable game function.
269
+ * @example
270
+ * // 25 damage from `attacker`'s pawn, as a bullet (CS2's DMG_BULLET is 2).
271
+ * victim.takeDamage(25, { attacker: attackerPawn.ref, damageType: 2 });
272
+ */
273
+ takeDamage(damage: number, opts?: TakeDamageOptions): boolean;
213
274
  }
214
275
 
276
+ /** An entity option of {@link EntityRef.takeDamage}: an EntityRef, or a wrapper that holds one. */
277
+ export type DamageEntity = EntityRef | { readonly ref: EntityRef };
278
+
279
+ /** Options for {@link EntityRef.takeDamage}. Every field is optional. */
280
+ export interface TakeDamageOptions {
281
+ /** Who gets credit for the damage (the damage info's attacker). */
282
+ attacker?: DamageEntity | null;
283
+ /** What dealt it, such as a grenade or the attacker's weapon. Defaults to the attacker. */
284
+ inflictor?: DamageEntity | null;
285
+ /** The weapon or ability it came from (SourceMod's `weapon`). */
286
+ ability?: DamageEntity | null;
287
+ /** Damage-type bit flags; the values are game facts (CS2: `DamageTypes_t`). Defaults to 0 (generic). */
288
+ damageType?: number;
289
+ /** Damage force, `[x, y, z]`. Defaults to zero. */
290
+ force?: ArrayLike<number> | null;
291
+ /** Where the damage lands, `[x, y, z]`. Defaults to zero. */
292
+ position?: ArrayLike<number> | null;
293
+ }
294
+
295
+ /** Element kinds for {@link EntityRef.gather} plans — the scalar kinds a `Float64Array` holds exactly. */
296
+ export declare const GatherKind: {
297
+ readonly Int32: number;
298
+ readonly Float32: number;
299
+ readonly Bool: number;
300
+ readonly Int8: number;
301
+ readonly Int16: number;
302
+ readonly UInt8: number;
303
+ readonly UInt16: number;
304
+ readonly UInt32: number;
305
+ readonly Float64: number;
306
+ };
307
+
215
308
  /** Keyvalues for a CEntityKeyValues-configured spawn. Inference: string -> SetString,
216
309
  * boolean -> SetBool, integer (int32) -> SetInt, other finite number -> SetFloat.
217
310
  * Keys are case-insensitive (hashed via MurmurHash2LowerCase, the engine's own keying). */
@@ -242,14 +335,58 @@ export interface OutputEvent {
242
335
  delay: number;
243
336
  }
244
337
 
338
+ /** The payload delivered to an `onInput` handler. */
339
+ export interface InputEvent {
340
+ /** The input's name as the engine spelled it (e.g. "Kill", "Open", "SetHealth"). */
341
+ input: string;
342
+ /** The entity receiving the input, or null if it is no longer live. */
343
+ entity: EntityRef | null;
344
+ /** The entity that activated the chain leading to this input, or null. */
345
+ activator: EntityRef | null;
346
+ /** The entity that sent the input (the output's owner, for map I/O), or null. */
347
+ caller: EntityRef | null;
348
+ /**
349
+ * The input's parameter, formatted as a string ("" when it has none). Inputs fired with a Pulse
350
+ * argument pack instead of a variant also report "".
351
+ */
352
+ value: string;
353
+ }
354
+
245
355
  /**
246
- * Entry point for entity lookup by class name.
356
+ * Entry point for entity lookup by class name, targetname or position.
247
357
  * @example
248
358
  * import { Entity } from "@s2script/sdk/entity";
249
359
  * const triggers = Entity.findByClass("trigger_multiple");
250
360
  * for (const t of triggers) console.log(t.index, t.name);
361
+ * const doors = Entity.findByName("cell_doors");
362
+ * const nearby = Entity.findInSphere([0, 0, 64], 256, "prop_physics");
251
363
  */
252
364
  export declare const Entity: {
253
365
  /** Find every entity whose designer-name (class) exactly matches `className`. Returns liveness-gated refs. */
254
366
  findByClass(className: string): EntityRef[];
367
+ /** Find every entity whose targetname (`name`, the map's `targetname`) exactly matches `name`.
368
+ * An empty `name` matches nothing. Returns liveness-gated refs. */
369
+ findByName(name: string): EntityRef[];
370
+ /** Every entity whose world origin is within `radius` units of `center` (inclusive), optionally only
371
+ * those of `className`. Entities without a readable origin are skipped. Returns `[]` for a
372
+ * non-finite center or a negative radius. Without `className` this walks every entity, so prefer
373
+ * passing one in hot paths. */
374
+ findInSphere(center: Vector | { x: number; y: number; z: number } | [number, number, number],
375
+ radius: number, className?: string): EntityRef[];
255
376
  };
377
+
378
+ /** What {@link EntityRef.getBoneTransform} returns. */
379
+ export interface BoneTransform {
380
+ /** The bone's world position. */
381
+ position: Vector;
382
+ /** The bone's world orientation, as a unit quaternion. */
383
+ rotation: { x: number; y: number; z: number; w: number };
384
+ }
385
+
386
+ /** What {@link EntityRef.getAttachment} returns. */
387
+ export interface AttachmentTransform {
388
+ /** The attachment's world position. */
389
+ origin: Vector;
390
+ /** The attachment's world angles. */
391
+ angles: QAngle;
392
+ }
package/files.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @s2script/files — sandboxed, off-thread file IO. Reads, writes (optionally gzip-compressed),
3
+ * lists and removes run on the shared async runtime's worker threads; the game thread only copies
4
+ * the payload and later receives the result on a frame drain. NO runtime code (injected at load).
5
+ *
6
+ * Every path is relative to this plugin's own root (`<data dir>/files/<plugin id>/`), `/`-separated,
7
+ * with segments of `[A-Za-z0-9._-]` (no `.`/`..`, at most 8 deep, 256 bytes). Anything else rejects
8
+ * with an `InvalidPath` error before any IO. One payload is capped by the async policy's
9
+ * `input_item_bytes` (8 MiB by default, measured after decompression) — see docs/ASYNC_LIMITS.md.
10
+ */
11
+
12
+ /** Options for {@link Files.read} / {@link Files.readText}. */
13
+ export interface FileReadOptions {
14
+ /** Decompress a gzip file (written with `{ gzip: true }`) off-thread. Default false. */
15
+ gzip?: boolean;
16
+ }
17
+
18
+ /** Options for {@link Files.write}. */
19
+ export interface FileWriteOptions {
20
+ /** Compress with gzip off-thread before writing. Default false. Cannot combine with `append`. */
21
+ gzip?: boolean;
22
+ /**
23
+ * Append instead of replace. Default false: a replacing write is atomic (temp file + rename), so
24
+ * a crash or a concurrent read never observes a torn file. Appends are not atomic.
25
+ */
26
+ append?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Off-thread file IO in this plugin's sandbox. Promises reject with `AsyncQueueFull`,
31
+ * `AsyncPayloadTooLarge`, `AsyncCancelled` (the plugin unloaded first) or a descriptive IO error.
32
+ * @example
33
+ * import { Files } from "@s2script/sdk/files";
34
+ * // persist a binary blob compressed, then read it back — neither step blocks the game thread
35
+ * await Files.write("replays/surf_utopia/wr.bin", frames, { gzip: true });
36
+ * const back = await Files.read("replays/surf_utopia/wr.bin", { gzip: true });
37
+ * if (back) console.log(`replay is ${back.length} bytes`);
38
+ */
39
+ export declare const Files: {
40
+ /** Read a file as bytes (a fresh `Uint8Array`). Resolves `null` when the file does not exist. */
41
+ read(path: string, opts?: FileReadOptions): Promise<Uint8Array | null>;
42
+ /** Read a file as UTF-8 text (invalid sequences become U+FFFD). Resolves `null` when absent. */
43
+ readText(path: string, opts?: FileReadOptions): Promise<string | null>;
44
+ /**
45
+ * Write `data` (bytes, or a string as UTF-8), creating parent directories. The payload is copied
46
+ * synchronously, so the caller may reuse its buffer as soon as this returns.
47
+ */
48
+ write(path: string, data: Uint8Array | string, opts?: FileWriteOptions): Promise<void>;
49
+ /** Delete a file. Resolves `true` if it existed. */
50
+ remove(path: string): Promise<boolean>;
51
+ /**
52
+ * The sorted entry names in a directory (default: the plugin root). Subdirectories end in `/`.
53
+ * A missing directory resolves `[]`.
54
+ */
55
+ list(dir?: string): Promise<string[]>;
56
+ };
package/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./damage";
24
24
  export * from "./db";
25
25
  export * from "./entity";
26
26
  export * from "./events";
27
+ export * from "./files";
27
28
  export * from "./http";
28
29
  export * from "./interfaces";
29
30
  export * from "./math";
@@ -43,5 +44,6 @@ export * from "./transmit";
43
44
  export * from "./usercmd";
44
45
  export * from "./usermessages";
45
46
  export * from "./voice";
47
+ export * from "./stringtables";
46
48
  export * from "./votes";
47
49
  export * from "./ws";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2script/sdk",
3
- "version": "0.31.0",
3
+ "version": "0.33.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",
@@ -80,6 +80,9 @@
80
80
  "./http": {
81
81
  "types": "./http.d.ts"
82
82
  },
83
+ "./files": {
84
+ "types": "./files.d.ts"
85
+ },
83
86
  "./interfaces": {
84
87
  "types": "./interfaces.d.ts"
85
88
  },
@@ -131,6 +134,9 @@
131
134
  "./usermessages": {
132
135
  "types": "./usermessages.d.ts"
133
136
  },
137
+ "./stringtables": {
138
+ "types": "./stringtables.d.ts"
139
+ },
134
140
  "./voice": {
135
141
  "types": "./voice.d.ts"
136
142
  },
package/plugin.d.ts CHANGED
@@ -18,7 +18,7 @@ import type {
18
18
  */
19
19
  import type { GameEvent, HookResultValue } from "./events";
20
20
  import type { Client } from "./clients";
21
- import type { EntityRef, OutputEvent } from "./entity";
21
+ import type { EntityRef, InputEvent, OutputEvent } from "./entity";
22
22
  import type { CommandHandler } from "./commands";
23
23
  import type { Config } from "./config";
24
24
  import type { PublishHandle } from "./interfaces";
@@ -100,6 +100,12 @@ export interface CtxEntities {
100
100
  output: string,
101
101
  handler: (ev: OutputEvent) => HookResultValue | void
102
102
  ): void;
103
+ /** Hook an input an entity receives (`AcceptInput`); return a {@link HookResultValue} to drop it. */
104
+ onInput(
105
+ classname: string,
106
+ input: string,
107
+ handler: (ev: InputEvent) => HookResultValue | void
108
+ ): void;
103
109
  }
104
110
  /** Per-frame + map/precache hooks on this plugin's load-scope ({@link PluginContext.server}). */
105
111
  export interface CtxServer {
@@ -298,7 +304,7 @@ export interface PluginHooks {
298
304
  *
299
305
  * `OnGameFrame` is SourceMod's public (before simulation). Post-simulation paint is
300
306
  * {@link Scope}'s `server.onGameFrame(fn, { phase: "post" })` — Metamod's post hook, not a SM public.
301
- * Filtered entity I/O is {@link onOutput}, not a public — SourceMod `HookEntityOutput`.
307
+ * Filtered entity I/O is {@link onOutput} / {@link onInput}, not a public — SourceMod `HookEntityOutput`.
302
308
  * Per-entity damage is `SDKHook` (`SDKHookType.OnTakeDamage`), not a named public.
303
309
  */
304
310
 
@@ -328,6 +334,18 @@ export declare function onOutput(
328
334
  handler: (ev: OutputEvent) => HookResultValue | void
329
335
  ): void;
330
336
 
337
+ /**
338
+ * Load-window entity input hook (SourceMod-style `AcceptEntityInput` pre-hook), called before the
339
+ * receiving entity runs the input. Keyed by `(classname, input)`; input names match
340
+ * case-insensitively and `"*"` wildcards either side. Return {@link HookResultValue} `Handled`/`Stop`
341
+ * to drop the input. Throws after settle.
342
+ */
343
+ export declare function onInput(
344
+ classname: string,
345
+ input: string,
346
+ handler: (ev: InputEvent) => HookResultValue | void
347
+ ): void;
348
+
331
349
  /**
332
350
  * The revived hot-reload handoff (the previous instance's `OnPluginState` return), or `undefined`.
333
351
  * Load-window only.
package/sdkhooks.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @s2script/sdkhooks — per-entity SDKHooks (SourceMod `SDKHook` / `SDKUnhook`).
3
3
  * NO runtime code: the engine injects the implementation at load time.
4
4
  */
5
- import type { EntityRef } from "./entity";
5
+ import type { DamageEntity, EntityRef } from "./entity";
6
6
  import type { DamageInfo } from "./damage";
7
7
  import type { HookResultValue } from "./events";
8
8
  import type { Client } from "./clients";
@@ -87,7 +87,10 @@ export declare const SDKHookType: {
87
87
  readonly Spawn: "Spawn";
88
88
  /** `Spawn` post. Return is ignored. */
89
89
  readonly SpawnPost: "SpawnPost";
90
- /** `CBaseEntity::Think` pre. `Handled` / `Stop` skip the original virtual. */
90
+ /**
91
+ * `CBaseEntity::Think` pre. Source 2 has no Think virtual (thinks are `SetThink` function
92
+ * pointers), so on CS2 {@link SDKHook} returns `false` for this type.
93
+ */
91
94
  readonly Think: "Think";
92
95
  /** `Think` post. Return is ignored. */
93
96
  readonly ThinkPost: "ThinkPost";
@@ -99,7 +102,10 @@ export declare const SDKHookType: {
99
102
  readonly PostThink: "PostThink";
100
103
  /** `PostThink` post. Return is ignored. */
101
104
  readonly PostThinkPost: "PostThinkPost";
102
- /** `CBaseEntity::Use` pre. `Handled` / `Stop` skip the original virtual. */
105
+ /**
106
+ * `CBaseEntity::Use` pre (`+use` on a door, button or prop). `Handled` / `Stop` skip the
107
+ * original virtual.
108
+ */
103
109
  readonly Use: "Use";
104
110
  /** `Use` post. Return is ignored. */
105
111
  readonly UsePost: "UsePost";
@@ -546,3 +552,20 @@ export declare function SDKUnhook(
546
552
  type: "ReloadPost",
547
553
  callback: ReloadPostCallback,
548
554
  ): boolean;
555
+
556
+ /**
557
+ * SourceMod's `SDKHooks_TakeDamage`: deal damage to `entity` through the game's own damage path.
558
+ * The same call as {@link EntityRef.takeDamage}; `weapon` is its `ability`. Unlike SourceMod there
559
+ * is no `bypassHooks`: `OnTakeDamage` hooks always see damage a plugin deals. Returns false when
560
+ * the call was refused (a null or stale entity, a non-finite `damage`, an unavailable game function).
561
+ */
562
+ export declare function SDKHooks_TakeDamage(
563
+ entity: DamageEntity | null,
564
+ inflictor: DamageEntity | null,
565
+ attacker: DamageEntity | null,
566
+ damage: number,
567
+ damageType?: number,
568
+ weapon?: DamageEntity | null,
569
+ force?: ArrayLike<number> | null,
570
+ position?: ArrayLike<number> | null,
571
+ ): boolean;
package/server.d.ts CHANGED
@@ -63,4 +63,15 @@ export declare const Server: {
63
63
  readonly mapName: string;
64
64
  /** The current map time in seconds (`GetGlobals()->curtime`). `0` if unavailable. */
65
65
  readonly gameTime: number;
66
+ /**
67
+ * The integer simulation tick (`GetGlobals()->tickcount`). Monotonic within a map and frozen
68
+ * while the game is paused — count ticks for anything timer-grade instead of differencing the
69
+ * float {@link Server.gameTime}. `0` if unavailable.
70
+ */
71
+ readonly tickCount: number;
72
+ /**
73
+ * Seconds per simulation tick, from the game's own server config (`0.015625` on a 64-tick
74
+ * server). Fixed for the server's lifetime. `0` only when the engine cannot report it.
75
+ */
76
+ readonly tickInterval: number;
66
77
  };
package/sound.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /** @s2script/sound — engine-generic sound: emit a named SoundEvent + register custom precache paths. */
2
2
  import type { EntityRef } from "./entity";
3
+ import type { Vector } from "./math";
3
4
 
4
5
  /** Options for {@link Sound.emit} — the source entity, recipient slots, and volume. */
5
6
  export interface SoundEmitOptions {
@@ -14,6 +15,42 @@ export interface SoundEmitOptions {
14
15
  volume?: number;
15
16
  }
16
17
 
18
+ /** Recipients for {@link SoundEvent.setParam} and {@link SoundEvent.stop}. */
19
+ export interface SoundEventRecipients {
20
+ /** Player slots the change goes to. Omitted -> every valid client. Bots are skipped. */
21
+ recipients?: number[];
22
+ }
23
+
24
+ /**
25
+ * A sound started with {@link Sound.start}: the engine's event guid and stack hash. It is plain
26
+ * data, so it is safe to keep. Once the sound has finished, `setParam` and `stop` still return true
27
+ * but the engine ignores them.
28
+ */
29
+ export interface SoundEvent {
30
+ /** The engine sound event guid, the same number {@link Sound.emit} returns. */
31
+ readonly guid: number;
32
+ /** The event's stack hash, which the engine needs alongside the guid to set a parameter. */
33
+ readonly stackHash: number;
34
+ /**
35
+ * Set a soundevent parameter on the playing sound, for example `"volume"`, `"pitch"` or
36
+ * `"position"`, or one of the event's own `public.*` fields. A number sets a float field and a
37
+ * Vector sets a three-float field.
38
+ *
39
+ * @example
40
+ * const ev = Sound.start("MyPack.Siren", { entity: pawn.ref });
41
+ * ev?.setParam("volume", 0.4);
42
+ * ev?.setParam("position", pawn.origin);
43
+ *
44
+ * Returns true when the engine was called. The engine does not report whether the event has a
45
+ * parameter by that name, and it drops a write to one that doesn't exist. Returns false for any
46
+ * other value type, a non-finite number, or when the engine function did not resolve.
47
+ */
48
+ setParam(name: string, value: number | Vector, opts?: SoundEventRecipients): boolean;
49
+ /** Stop this sound for the recipients. Returns false only when the engine function did not
50
+ * resolve. */
51
+ stop(opts?: SoundEventRecipients): boolean;
52
+ }
53
+
17
54
  /** Block-scoped precache context — valid ONLY during the onPrecache dispatch. Synchronous use
18
55
  * only: a stashed context used after the handler returns (or past an await) is a no-op `false`
19
56
  * (the engine manifest is gone). */
@@ -52,6 +89,23 @@ export declare const Sound: {
52
89
  * function / stale source entity / an empty `recipients` array). An all-bot-skipped recipient
53
90
  * set still emits to nobody (a real engine call, may return a nonzero GUID). */
54
91
  emit(name: string, opts?: SoundEmitOptions): number;
92
+ /** {@link Sound.emit}, keeping a handle so you can change the sound's parameters while it plays
93
+ * or stop that one instance. Returns null where `emit` would return 0. */
94
+ start(name: string, opts?: SoundEmitOptions): SoundEvent | null;
95
+ /**
96
+ * The length of a sound event in seconds, as the engine's sound event manager reports it.
97
+ *
98
+ * @example
99
+ * const len = Sound.duration("MyPack.RoundMusic");
100
+ * if (len !== null) delay(len * 1000).then(() => Sound.emit("MyPack.RoundMusicEnd"));
101
+ *
102
+ * Returns null when the engine has no event by that name (check with {@link Sound.isValid}), or
103
+ * when the engine function did not resolve.
104
+ */
105
+ duration(name: string): number | null;
106
+ /** Whether the engine knows a sound event by that name: a loaded soundevents file defines it.
107
+ * Returns false when the engine function did not resolve. */
108
+ isValid(name: string): boolean;
55
109
  /**
56
110
  * Stop a named SoundEvent playing on an entity — the counterpart to {@link Sound.emit}.
57
111
  *
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @s2script/sdk/stringtables — the engine's network string tables. NO runtime code: the engine
3
+ * injects the implementation at load (`__s2pkg_stringtables`).
4
+ *
5
+ * String tables are how the server networks indexed strings to clients: precache lists, effect
6
+ * names, and per-entry user data. The engine rebuilds them on every map change, so a
7
+ * {@link StringTable} is plain data (its id and name) and every call looks the table up again. A
8
+ * handle kept past a map change answers -1 / null / false rather than reaching a freed table; get
9
+ * a fresh one with {@link StringTables.find}.
10
+ */
11
+
12
+ /** A string table handle. Safe to keep: it holds no pointer. */
13
+ export interface StringTable {
14
+ /** The table's id, its position in the container. */
15
+ readonly id: number;
16
+ /** The table's name, for example `"EffectDispatch"`. */
17
+ readonly name: string;
18
+ /** How many strings the table holds, or -1 when the handle is stale. */
19
+ count(): number;
20
+ /** The index of `value` in the table, or -1 when it is absent or the handle is stale. */
21
+ findString(value: string): number;
22
+ /** The string at `index`, or null for an index out of range or a stale handle. */
23
+ getString(index: number): string | null;
24
+ /**
25
+ * A copy of the user data stored with the string at `index`. An entry without user data gives an
26
+ * empty array; null means an index out of range or a stale handle.
27
+ */
28
+ getData(index: number): Uint8Array | null;
29
+ /**
30
+ * Replace the user data of the string at `index`. A string is stored as its UTF-8 bytes. Returns
31
+ * false for an index out of range, a stale handle, or a value that is neither bytes nor a string.
32
+ */
33
+ setData(index: number, data: Uint8Array | string): boolean;
34
+ /**
35
+ * Add `value` to the table, with optional user data, and return its index. Adding a string the
36
+ * table already has returns its existing index. Returns -1 when the table is full or the handle
37
+ * is stale. The engine networks the new entry to clients.
38
+ *
39
+ * @example
40
+ * const t = StringTables.find("EffectDispatch");
41
+ * const i = t?.addString("MyPack.Effect") ?? -1;
42
+ */
43
+ addString(value: string, data?: Uint8Array | string): number;
44
+ }
45
+
46
+ /**
47
+ * Find and list the server's string tables.
48
+ *
49
+ * @example
50
+ * import { StringTables } from "@s2script/sdk/stringtables";
51
+ * const effects = StringTables.find("EffectDispatch");
52
+ * const particle = effects?.findString("ParticleEffect") ?? -1;
53
+ */
54
+ export declare const StringTables: {
55
+ /** The table with that name, or null. */
56
+ find(name: string): StringTable | null;
57
+ /** The table with that id, or null. */
58
+ get(id: number): StringTable | null;
59
+ /** How many tables the server has. 0 before the first map or when unavailable. */
60
+ count(): number;
61
+ /** Every table, in id order. */
62
+ all(): StringTable[];
63
+ };