@s2script/sdk 0.9.0 → 0.10.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 +36 -0
- package/contracts/README.md +37 -0
- package/contracts/workshop.d.ts +82 -0
- package/dist/cli.js +41 -3
- package/package.json +10 -3
- package/server.d.ts +22 -0
- package/timers.d.ts +39 -0
- package/unsafe.d.ts +9 -0
- package/voice.d.ts +59 -0
package/clients.d.ts
CHANGED
|
@@ -36,6 +36,42 @@ export declare class Client {
|
|
|
36
36
|
* reason in the server log), setting is an inert no-op and reads stay false.
|
|
37
37
|
*/
|
|
38
38
|
voiceMuted: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Tell this client to run `cmd` in their own console, as if they had typed it
|
|
41
|
+
* (SourceMod `ClientCommand`).
|
|
42
|
+
*
|
|
43
|
+
* Requires a real client: a bot has no console, so this is a no-op on bots — use
|
|
44
|
+
* {@link Client.fakeCommand} for server-side execution. Returns `false` when the command was not
|
|
45
|
+
* dispatched (empty text, bad slot, or the engine interface is unavailable on this build), never
|
|
46
|
+
* a silent no-op.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* client.command("play sounds/ui/beep.vsnd");
|
|
50
|
+
*/
|
|
51
|
+
command(cmd: string): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Have the SERVER process `cmd` as if this client had sent it (SourceMod `FakeClientCommand`).
|
|
54
|
+
*
|
|
55
|
+
* Unlike {@link Client.command} this works on bots, because nothing is sent to a client — the
|
|
56
|
+
* engine dispatches the command itself, attributed to this player's slot. Live-verified:
|
|
57
|
+
* `fakeCommand("say hi")` on slot 0 prints as that player, not as Console.
|
|
58
|
+
*
|
|
59
|
+
* Returns `false` — never a silent no-op — for a bad slot, empty text, an unavailable interface,
|
|
60
|
+
* or a name that is not a registered console **command** (a ConVar such as `mp_friendlyfire` is
|
|
61
|
+
* refused: use `Server.command` for those).
|
|
62
|
+
*
|
|
63
|
+
* KNOWN LIMITATION: engine commands (`say`, `kill`, …) execute. A command registered by an
|
|
64
|
+
* s2script plugin is dispatched by the engine, but its JS handler will NOT run — the core holds
|
|
65
|
+
* the isolate borrow across all JS, so re-entering the dispatcher hits the documented
|
|
66
|
+
* re-entrancy skip. (This is not a permissions issue: every s2script command is registered
|
|
67
|
+
* client-executable, which is why a player can type it in their own console.) To invoke another
|
|
68
|
+
* plugin's behaviour, use a cross-plugin interface rather than faking its command. See
|
|
69
|
+
* docs/superpowers/specs/2026-07-25-client-command-design.md §12.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* client.fakeCommand("say hello"); // as though the player typed it
|
|
73
|
+
*/
|
|
74
|
+
fakeCommand(cmd: string): boolean;
|
|
39
75
|
}
|
|
40
76
|
/**
|
|
41
77
|
* Look up connected clients by slot or enumerate them all.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Standard interface contracts
|
|
2
|
+
|
|
3
|
+
These are **type-only contracts for inter-plugin interfaces** — not SDK capability modules.
|
|
4
|
+
|
|
5
|
+
The framework deliberately ships **no implementation** for anything in here. Each contract describes
|
|
6
|
+
a shape that a *community plugin* implements and publishes, so that consumers can depend on one
|
|
7
|
+
agreed interface instead of a different ad-hoc shape per plugin:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// producer
|
|
11
|
+
import type { EconService } from "@s2script/cs2/econ";
|
|
12
|
+
const impl: EconService = { /* … */ };
|
|
13
|
+
ctx.publish("econ", impl); // manifest: s2script.publishes
|
|
14
|
+
|
|
15
|
+
// consumer
|
|
16
|
+
import type { EconService } from "@s2script/cs2/econ";
|
|
17
|
+
const econ = ctx.tryUse<EconService>("econ"); // null while unpublished
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Why contracts and not features.** Skins and workshop integration are Valve-backend and
|
|
21
|
+
game-content concerns, not engine touchpoints. Implementing them in the framework would drag the
|
|
22
|
+
core toward a specific game's economy and Steam's services, against *the core is engine-generic* and
|
|
23
|
+
*dependencies point one way: game → core*. Publishing the contract gets the ecosystem interop
|
|
24
|
+
without the framework taking on that surface.
|
|
25
|
+
|
|
26
|
+
**Rules that apply to every contract here** (they cross the plugin boundary, see
|
|
27
|
+
`docs/ARCHITECTURE.md`):
|
|
28
|
+
|
|
29
|
+
- Arguments and return values cross by **structured copy as JSON**. A `BigInt` throws and silently
|
|
30
|
+
drops the whole payload, so every 64-bit value (Steam IDs, workshop IDs, item IDs) is typed as a
|
|
31
|
+
**decimal string**, never `number` or `bigint`.
|
|
32
|
+
- `EntityRef` is the one live-ish exception: it crosses tagged and is revived serial-gated on the
|
|
33
|
+
other side, so it stays `T | null`-safe.
|
|
34
|
+
- A hard dependency (`ctx.use`) throws `InterfaceUnavailable` if the producer is absent; an optional
|
|
35
|
+
one (`ctx.tryUse`) returns `null`. For anything in here, prefer `tryUse` — these are third-party.
|
|
36
|
+
- Contracts are semver-governed. Adding an optional member is a minor; changing or removing one is a
|
|
37
|
+
major.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@s2script/sdk/contracts/workshop` — the standard contract for a **workshop / UGC** plugin.
|
|
3
|
+
*
|
|
4
|
+
* TYPE-ONLY. The framework ships no implementation: subscribing to, downloading and mounting UGC is
|
|
5
|
+
* a Steam-services concern, not a Source 2 engine touchpoint. A community plugin implements this and
|
|
6
|
+
* publishes it; consumers depend on the one agreed shape. See `contracts/README.md`.
|
|
7
|
+
*
|
|
8
|
+
* Engine-generic on purpose — every Source 2 title has workshop content, so nothing here is
|
|
9
|
+
* CS2-specific. Game-specific economy lives in `@s2script/cs2/econ`.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* import type { WorkshopService } from "@s2script/sdk/contracts/workshop";
|
|
13
|
+
* const ws = ctx.tryUse<WorkshopService>("workshop");
|
|
14
|
+
* if (ws) {
|
|
15
|
+
* const map = await ws.currentMap();
|
|
16
|
+
* console.log(map ? `${map.title} (${map.id})` : "not a workshop map");
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A workshop item. `id` is a **decimal string**, never a number: published-file IDs are 64-bit and a
|
|
22
|
+
* `BigInt` cannot cross the plugin boundary (it throws and drops the whole payload).
|
|
23
|
+
*/
|
|
24
|
+
export interface WorkshopItem {
|
|
25
|
+
/** Published-file ID as a decimal string, e.g. `"3070288894"`. */
|
|
26
|
+
readonly id: string;
|
|
27
|
+
/** Item title, or `""` when metadata has not been fetched yet. */
|
|
28
|
+
readonly title: string;
|
|
29
|
+
/** Owning author's SteamID64 as a decimal string, or `""` if unknown. */
|
|
30
|
+
readonly authorSteamId: string;
|
|
31
|
+
/** Bytes on disk once installed; `0` when not installed or unknown. */
|
|
32
|
+
readonly sizeBytes: number;
|
|
33
|
+
/** Last-updated Unix seconds; `0` if unknown. */
|
|
34
|
+
readonly updatedAt: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** An installed workshop map, as reported by {@link WorkshopService.currentMap}. */
|
|
38
|
+
export interface WorkshopMap extends WorkshopItem {
|
|
39
|
+
/** The BSP name the engine loads (what `Server.mapName` reports once live). */
|
|
40
|
+
readonly mapName: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Progress for an in-flight download. */
|
|
44
|
+
export interface WorkshopDownloadProgress {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly bytesDownloaded: number;
|
|
47
|
+
readonly bytesTotal: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The contract a workshop plugin publishes.
|
|
52
|
+
*
|
|
53
|
+
* Everything that touches Steam is asynchronous — an implementation must not block the game frame.
|
|
54
|
+
* Methods resolve to `null`/`false` rather than throwing when an item is unknown or Steam is
|
|
55
|
+
* unreachable, so a consumer degrades instead of crashing (the framework's standing posture).
|
|
56
|
+
*/
|
|
57
|
+
export interface WorkshopService {
|
|
58
|
+
/** The currently-running workshop map, or `null` when the server is on a stock map. */
|
|
59
|
+
currentMap(): Promise<WorkshopMap | null>;
|
|
60
|
+
|
|
61
|
+
/** Metadata for `id`, or `null` if unknown/unreachable. */
|
|
62
|
+
info(id: string): Promise<WorkshopItem | null>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure `id` is installed and up to date, resolving `true` once usable.
|
|
66
|
+
* Implementations should be idempotent — calling it for an already-current item is a cheap `true`.
|
|
67
|
+
*/
|
|
68
|
+
ensureInstalled(id: string): Promise<boolean>;
|
|
69
|
+
|
|
70
|
+
/** In-flight download progress, or `null` when nothing is downloading for `id`. */
|
|
71
|
+
progress(id: string): Promise<WorkshopDownloadProgress | null>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Change level to a workshop map. Resolves `false` when the item is not a map, is not installed
|
|
75
|
+
* and could not be fetched, or the change was refused. On success the server changes level, so a
|
|
76
|
+
* caller should not assume any code after this runs on the same map.
|
|
77
|
+
*/
|
|
78
|
+
changeMap(id: string): Promise<boolean>;
|
|
79
|
+
|
|
80
|
+
/** Every installed workshop item known to the implementation. */
|
|
81
|
+
installed(): Promise<readonly WorkshopItem[]>;
|
|
82
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1217,6 +1217,10 @@ function typecheckPlugin(pluginDir, opts) {
|
|
|
1217
1217
|
// dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
|
|
1218
1218
|
// sdk imports.
|
|
1219
1219
|
"@s2script/sdk/*": ["sdk/*.d.ts"],
|
|
1220
|
+
// Game-package SUBPATHS (e.g. @s2script/cs2/econ -> packages/cs2/econ.d.ts). Needed because
|
|
1221
|
+
// the `@s2script/*` fallback below maps to `<pkg>/index.d.ts`, which cannot express a
|
|
1222
|
+
// subpath. tsc picks the longest matching prefix, so this wins for cs2 subpath imports.
|
|
1223
|
+
"@s2script/cs2/*": ["cs2/*.d.ts"],
|
|
1220
1224
|
"@s2script/*": ["*/index.d.ts"],
|
|
1221
1225
|
...contractPaths
|
|
1222
1226
|
}
|
|
@@ -4963,21 +4967,37 @@ var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
|
|
|
4963
4967
|
"vector",
|
|
4964
4968
|
"qangle"
|
|
4965
4969
|
]);
|
|
4970
|
+
var WRITABLE_NAV_KINDS = /* @__PURE__ */ new Set(["f32", "bool", "i32"]);
|
|
4966
4971
|
function buildNavModel(config, catalog) {
|
|
4967
4972
|
const targetClasses = [...new Set(config.map((e) => e.target))];
|
|
4968
4973
|
const schemaModel = buildModel(catalog, targetClasses);
|
|
4969
4974
|
const sorted = [...config].sort((a2, b) => a2.wrapper < b.wrapper ? -1 : a2.wrapper > b.wrapper ? 1 : 0);
|
|
4970
4975
|
const wrappers = sorted.map((entry) => {
|
|
4971
4976
|
const all = flattenedFields(schemaModel, entry.target);
|
|
4977
|
+
const wantWritable = new Set(entry.writable ?? []);
|
|
4972
4978
|
const fields = [];
|
|
4973
4979
|
const skippedKinds = [];
|
|
4974
4980
|
for (const f of all) {
|
|
4975
4981
|
if (SUPPORTED_NAV_KINDS.has(f.accessorKind)) {
|
|
4976
|
-
fields.push(f);
|
|
4982
|
+
fields.push({ ...f, navWritable: wantWritable.has(f.rawName) });
|
|
4977
4983
|
} else {
|
|
4978
4984
|
skippedKinds.push({ propName: f.propName, accessorKind: f.accessorKind });
|
|
4979
4985
|
}
|
|
4980
4986
|
}
|
|
4987
|
+
for (const raw of wantWritable) {
|
|
4988
|
+
const f = fields.find((x) => x.rawName === raw);
|
|
4989
|
+
if (!f) {
|
|
4990
|
+
const known = all.some((x) => x.rawName === raw);
|
|
4991
|
+
throw new Error(
|
|
4992
|
+
`nav-targets: ${entry.wrapper}.writable lists ${JSON.stringify(raw)}, which ` + (known ? `exists on ${entry.target} but has an unsupported accessor kind, so it has no nav accessor at all.` : `does not exist on ${entry.target} or any of its ancestors (renamed or removed?).`)
|
|
4993
|
+
);
|
|
4994
|
+
}
|
|
4995
|
+
if (!WRITABLE_NAV_KINDS.has(f.accessorKind)) {
|
|
4996
|
+
throw new Error(
|
|
4997
|
+
`nav-targets: ${entry.wrapper}.writable lists ${JSON.stringify(raw)} (kind '${f.accessorKind}'), but there is no EntityRef.write*Via for that kind. Add the native and extend WRITABLE_NAV_KINDS, or drop the field from the allowlist.`
|
|
4998
|
+
);
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
4981
5001
|
return {
|
|
4982
5002
|
wrapper: entry.wrapper,
|
|
4983
5003
|
prop: entry.prop,
|
|
@@ -5004,6 +5024,11 @@ var READ_VIA = {
|
|
|
5004
5024
|
u16: "readUInt16Via",
|
|
5005
5025
|
u32: "readUInt32Via"
|
|
5006
5026
|
};
|
|
5027
|
+
var WRITE_VIA = {
|
|
5028
|
+
f32: "writeFloat32Via",
|
|
5029
|
+
bool: "writeBoolVia",
|
|
5030
|
+
i32: "writeInt32Via"
|
|
5031
|
+
};
|
|
5007
5032
|
function emitNavJs(model) {
|
|
5008
5033
|
const out = [HEADER4, "(function () {", " var off = __s2_schema_offset;"];
|
|
5009
5034
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
@@ -5037,7 +5062,9 @@ function emitNavJs(model) {
|
|
|
5037
5062
|
if (!method) continue;
|
|
5038
5063
|
getter = `get: function () { return this.root.${method}(this.path, ${resolve13}); }`;
|
|
5039
5064
|
}
|
|
5040
|
-
|
|
5065
|
+
const wmethod = f.navWritable ? WRITE_VIA[kind] : void 0;
|
|
5066
|
+
const setter = wmethod ? `, set: function (v) { this.root.${wmethod}(this.path, ${resolve13}, v); }` : "";
|
|
5067
|
+
out.push(` ${S2(f.propName)}: { ${getter}${setter} },`);
|
|
5041
5068
|
}
|
|
5042
5069
|
out.push(" });");
|
|
5043
5070
|
}
|
|
@@ -5092,9 +5119,20 @@ function emitNavDts(model) {
|
|
|
5092
5119
|
if (needsEntityRef) out.push('import type { EntityRef } from "@s2script/sdk/entity";');
|
|
5093
5120
|
out.push("");
|
|
5094
5121
|
for (const w of model.wrappers) {
|
|
5122
|
+
if (w.fields.some((f) => f.navWritable)) {
|
|
5123
|
+
out.push(`/**`);
|
|
5124
|
+
out.push(` * Fields NOT marked \`readonly\` are writable.`);
|
|
5125
|
+
out.push(` *`);
|
|
5126
|
+
out.push(` * A write takes effect immediately \u2014 the server reads these during movement \u2014 but it is **not**`);
|
|
5127
|
+
out.push(` * flagged for replication, because this object is reached through a pointer chain and the`);
|
|
5128
|
+
out.push(` * change-notifier addresses the root entity. A client predicting the old value may therefore`);
|
|
5129
|
+
out.push(` * see brief mismatch until the next authoritative correction. SourceMod's \`SetEntPropFloat\``);
|
|
5130
|
+
out.push(` * on the same fields behaves the same way.`);
|
|
5131
|
+
out.push(` */`);
|
|
5132
|
+
}
|
|
5095
5133
|
out.push(`export interface ${w.wrapper} {`);
|
|
5096
5134
|
for (const f of w.fields) {
|
|
5097
|
-
out.push(` readonly ${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
5135
|
+
out.push(` ${f.navWritable ? "" : "readonly "}${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
5098
5136
|
}
|
|
5099
5137
|
out.push("}", "");
|
|
5100
5138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"*.d.ts",
|
|
31
31
|
"README.md",
|
|
32
32
|
"LICENSE-MIT",
|
|
33
|
-
"LICENSE-APACHE"
|
|
33
|
+
"LICENSE-APACHE",
|
|
34
|
+
"contracts"
|
|
34
35
|
],
|
|
35
36
|
"exports": {
|
|
36
37
|
"./admin": {
|
|
@@ -120,13 +121,19 @@
|
|
|
120
121
|
"./usermessages": {
|
|
121
122
|
"types": "./usermessages.d.ts"
|
|
122
123
|
},
|
|
124
|
+
"./voice": {
|
|
125
|
+
"types": "./voice.d.ts"
|
|
126
|
+
},
|
|
123
127
|
"./votes": {
|
|
124
128
|
"types": "./votes.d.ts"
|
|
125
129
|
},
|
|
126
130
|
"./ws": {
|
|
127
131
|
"types": "./ws.d.ts"
|
|
128
132
|
},
|
|
129
|
-
"./package.json": "./package.json"
|
|
133
|
+
"./package.json": "./package.json",
|
|
134
|
+
"./contracts/workshop": {
|
|
135
|
+
"types": "./contracts/workshop.d.ts"
|
|
136
|
+
}
|
|
130
137
|
},
|
|
131
138
|
"repository": {
|
|
132
139
|
"type": "git",
|
package/server.d.ts
CHANGED
|
@@ -35,6 +35,28 @@ export declare const Server: {
|
|
|
35
35
|
min?: number;
|
|
36
36
|
max?: number;
|
|
37
37
|
}): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Watch a cvar for changes (SourceMod `HookConVarChange`, ModSharp's cvar change hook).
|
|
40
|
+
*
|
|
41
|
+
* `name` is a cvar name, or `"*"` for every cvar. The handler receives the cvar's name and its
|
|
42
|
+
* new and old values as strings — for `"*"` the name tells you which one moved.
|
|
43
|
+
*
|
|
44
|
+
* NOTIFY-only: the engine's global change callback runs **after** the value has been applied, so
|
|
45
|
+
* a handler cannot veto a change; returning anything is ignored. A handler that throws is logged
|
|
46
|
+
* and contained, and the remaining handlers still run.
|
|
47
|
+
*
|
|
48
|
+
* Subscriptions are ledgered — unload removes them whether or not `dispose()` is called.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* import { Server } from "@s2script/sdk/server";
|
|
52
|
+
* Server.onCvarChange("mp_friendlyfire", (name, next, prev) => {
|
|
53
|
+
* console.log(`${name}: ${prev} -> ${next}`);
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
onCvarChange(
|
|
57
|
+
name: string,
|
|
58
|
+
handler: (name: string, newValue: string, oldValue: string) => void,
|
|
59
|
+
): { dispose(): void };
|
|
38
60
|
/** The server's configured max client count (`GetMaxClients()`). `0` if unavailable. */
|
|
39
61
|
readonly maxPlayers: number;
|
|
40
62
|
/** The current map name (`GetMapName()`, the BSP). `""` if unavailable. */
|
package/timers.d.ts
CHANGED
|
@@ -20,3 +20,42 @@ export declare function nextFrame(): Promise<void>;
|
|
|
20
20
|
* Only valid inside a threadSleep-capable fiber context.
|
|
21
21
|
*/
|
|
22
22
|
export declare function threadSleep(ms: number): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A live callback timer, returned by {@link after} and {@link every}. Ledgered against the creating
|
|
26
|
+
* plugin: unload kills it whether or not `kill()` was called, so a repeating timer can never outlive
|
|
27
|
+
* its plugin and fire into a dead context.
|
|
28
|
+
*/
|
|
29
|
+
export interface Timer {
|
|
30
|
+
/** False once the timer has fired (one-shot), been killed, or had its plugin unloaded. */
|
|
31
|
+
readonly alive: boolean;
|
|
32
|
+
/** Cancel it. Idempotent — returns false if it was already dead. Safe to call from inside the callback. */
|
|
33
|
+
kill(): boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn` once after `ms` milliseconds (SourceMod `CreateTimer` without `TIMER_REPEAT`).
|
|
38
|
+
*
|
|
39
|
+
* Prefer {@link delay} when you can `await`; use this when you need something cancellable.
|
|
40
|
+
* A throwing callback is reported and contained — it never kills the frame or the timer system.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* import { after } from "@s2script/sdk/timers";
|
|
44
|
+
* const t = after(5000, () => console.log("5s later"));
|
|
45
|
+
* t.kill(); // ...unless cancelled first
|
|
46
|
+
*/
|
|
47
|
+
export declare function after(ms: number, fn: () => void): Timer;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` every `ms` milliseconds until killed (SourceMod `CreateTimer` with `TIMER_REPEAT`).
|
|
51
|
+
*
|
|
52
|
+
* The next firing is scheduled *after* the callback returns, so a slow callback cannot pile up.
|
|
53
|
+
* `ms` must be greater than 0 — a zero-interval repeat would re-arm every drain and starve the
|
|
54
|
+
* frame, so it throws rather than degrading.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* import { every } from "@s2script/sdk/timers";
|
|
58
|
+
* const tick = every(1000, () => console.log("tick"));
|
|
59
|
+
* // later: tick.kill();
|
|
60
|
+
*/
|
|
61
|
+
export declare function every(ms: number, fn: () => void): Timer;
|
package/unsafe.d.ts
CHANGED
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export interface EngineCalls {}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Plugin-declared engine calls, resolved from this plugin's own gamedata.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { Engine } from "@s2script/sdk/unsafe";
|
|
22
|
+
* // Resolve ONCE at load — null means the descriptor failed a load-time gate.
|
|
23
|
+
* const ignite = Engine.call("ignite");
|
|
24
|
+
* if (!ignite) console.log(`unavailable: ${Engine.status("ignite")}`);
|
|
25
|
+
*/
|
|
17
26
|
export declare const Engine: {
|
|
18
27
|
/**
|
|
19
28
|
* The declared call, or `null` when its descriptor failed a load-time gate (signature miss,
|
package/voice.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @s2script/sdk/voice — who can hear whom. NO runtime code: the engine injects the implementation
|
|
3
|
+
* at load (`__s2pkg_voice`).
|
|
4
|
+
*
|
|
5
|
+
* Layered UNDER `Client.voiceMuted`: a muted sender is inaudible to everyone regardless of any rule
|
|
6
|
+
* here, so admin moderation always beats a gameplay rule.
|
|
7
|
+
*
|
|
8
|
+
* Rules are declarative state, not a callback. The engine's listen matrix is re-asserted
|
|
9
|
+
* continuously and the underlying hook fires per (receiver, sender) pair, so a per-pair JS callback
|
|
10
|
+
* would run up to 64x64 times per refresh. Set a rule; the shim enforces it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Hot-path measurement counters returned by {@link Voice.stats}. */
|
|
14
|
+
export interface VoiceStats {
|
|
15
|
+
/** Listen-matrix decisions the engine has asked about. */
|
|
16
|
+
calls: number;
|
|
17
|
+
/** Senders currently carrying a merged rule. */
|
|
18
|
+
entries: number;
|
|
19
|
+
/** Times a rule actually silenced a pair. THE effect counter — a rule that never rewrites is not working. */
|
|
20
|
+
rewrites: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-(receiver, sender) voice hearability.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* import { Voice } from "@s2script/sdk/voice";
|
|
28
|
+
* // A dead player is heard only by other dead players.
|
|
29
|
+
* Voice.setAudibleTo(deadSlot, otherDeadSlots);
|
|
30
|
+
* // ...and on respawn, hand the slot back to the engine.
|
|
31
|
+
* Voice.reset(deadSlot);
|
|
32
|
+
*/
|
|
33
|
+
export declare const Voice: {
|
|
34
|
+
/**
|
|
35
|
+
* This sender is audible ONLY to `receivers` (0-based slots). An empty array means audible to
|
|
36
|
+
* nobody — distinct from {@link Voice.reset}, which removes the rule and lets the engine decide.
|
|
37
|
+
* Rules from multiple plugins AND-merge, so another plugin can only ever narrow yours, never widen it.
|
|
38
|
+
*
|
|
39
|
+
* A rule is dropped automatically when the sender disconnects: slots are recycled, and a rule is
|
|
40
|
+
* about the player who occupied one, not the seat.
|
|
41
|
+
*
|
|
42
|
+
* Returns `false` — never throws — when the rule was NOT applied: voice control is degraded or its
|
|
43
|
+
* engine hook never installed on this build, `sender` is outside `[0, 64)`, or any element of
|
|
44
|
+
* `receivers` is. Note this differs from {@link import("./transmit").Transmit.setVisibleTo}, which
|
|
45
|
+
* throws `RangeError` on a bad slot; check the return value here rather than relying on a throw.
|
|
46
|
+
*
|
|
47
|
+
* @throws `TypeError` if `receivers` is not an array.
|
|
48
|
+
*/
|
|
49
|
+
setAudibleTo(sender: number, receivers: readonly number[]): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Drop THIS plugin's rule for `sender`; other plugins' rules survive and are re-merged.
|
|
52
|
+
* `false` when you had no rule for that sender.
|
|
53
|
+
*/
|
|
54
|
+
reset(sender: number): boolean;
|
|
55
|
+
/** Drop every rule this plugin owns. Unload does this automatically. */
|
|
56
|
+
resetAll(): void;
|
|
57
|
+
/** Hot-path counters, or null when the running shim predates this capability. */
|
|
58
|
+
stats(): VoiceStats | null;
|
|
59
|
+
};
|