@s2script/sdk 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clients.d.ts +36 -0
- package/contracts/README.md +37 -0
- package/contracts/workshop.d.ts +82 -0
- package/dist/cli.js +275 -49
- 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
|
}
|
|
@@ -4636,9 +4640,15 @@ var ATOMIC = {
|
|
|
4636
4640
|
uint32: { k: "u32", w: true },
|
|
4637
4641
|
uint64: { k: "u64", w: false },
|
|
4638
4642
|
int64: { k: "i64", w: false },
|
|
4639
|
-
float64: { k: "f64", w: false }
|
|
4643
|
+
float64: { k: "f64", w: false },
|
|
4640
4644
|
// no narrow 64-bit writers
|
|
4645
|
+
// `Color` is a 4-byte RGBA struct, not a scalar, but it is laid out exactly as a little-endian
|
|
4646
|
+
// uint32 (R in the low byte) and every engine consumer treats it that way. Mapping it here rather
|
|
4647
|
+
// than adding a value class keeps the surface small and matches how packed colours are already
|
|
4648
|
+
// passed elsewhere in the SDK (e.g. the fade user-message).
|
|
4649
|
+
Color: { k: "u32", w: true }
|
|
4641
4650
|
};
|
|
4651
|
+
var ENUM_WIDTH = { 1: "u8", 2: "u16", 4: "u32", 8: "u64" };
|
|
4642
4652
|
var VEC = { Vector: "vector", VectorWS: "vector", QAngle: "qangle" };
|
|
4643
4653
|
var VEC_INFO = {
|
|
4644
4654
|
vector: { cls: "Vector", count: 3 },
|
|
@@ -4651,7 +4661,13 @@ function idiomaticName(raw) {
|
|
|
4651
4661
|
const core = m && KNOWN_TAGS.has(m[1]) ? m[2] : s;
|
|
4652
4662
|
return core.charAt(0).toLowerCase() + core.slice(1);
|
|
4653
4663
|
}
|
|
4654
|
-
function
|
|
4664
|
+
function transparentValue(catalog, className) {
|
|
4665
|
+
const e = catalog[className];
|
|
4666
|
+
if (!e || e.parent || e.fields.length !== 1) return null;
|
|
4667
|
+
const f = e.fields[0];
|
|
4668
|
+
return f.name === "m_Value" && f.type.kind === "atomic" ? f : null;
|
|
4669
|
+
}
|
|
4670
|
+
function classifyField(type, embeddable = /* @__PURE__ */ new Set()) {
|
|
4655
4671
|
if (type.kind === "handle") return { accessorKind: "handle", writable: false };
|
|
4656
4672
|
if (type.kind === "atomic") {
|
|
4657
4673
|
const vk = VEC[type.name ?? ""];
|
|
@@ -4665,8 +4681,16 @@ function classifyField(type) {
|
|
|
4665
4681
|
if (cm) return { accessorKind: "str", writable: false, strLen: parseInt(cm[1], 10) };
|
|
4666
4682
|
return { skip: `unmapped 'unknown' type '${type.name ?? ""}'` };
|
|
4667
4683
|
}
|
|
4668
|
-
if (type.kind === "enum")
|
|
4669
|
-
|
|
4684
|
+
if (type.kind === "enum") {
|
|
4685
|
+
const w = ENUM_WIDTH[type.size ?? 0];
|
|
4686
|
+
if (w) return { accessorKind: w, writable: WRITE[w] !== void 0 };
|
|
4687
|
+
return { skip: `enum '${type.name ?? ""}' byte width not stated by the schema` };
|
|
4688
|
+
}
|
|
4689
|
+
if (type.kind === "class") {
|
|
4690
|
+
const n3 = type.name ?? "";
|
|
4691
|
+
if (embeddable.has(n3)) return { embedded: n3 };
|
|
4692
|
+
return { skip: `embedded class '${n3}' deferred` };
|
|
4693
|
+
}
|
|
4670
4694
|
if (type.kind === "ptr") return { skip: "raw pointer" };
|
|
4671
4695
|
return { skip: `unmapped kind '${type.kind}'` };
|
|
4672
4696
|
}
|
|
@@ -4681,7 +4705,11 @@ function flattenedFields(model, className) {
|
|
|
4681
4705
|
}
|
|
4682
4706
|
return chain.flatMap((c2) => c2.ownFields);
|
|
4683
4707
|
}
|
|
4684
|
-
function buildModel(catalog, requested) {
|
|
4708
|
+
function buildModel(catalog, requested, embeddable = []) {
|
|
4709
|
+
for (const n3 of embeddable) if (!catalog[n3]) throw new Error(`gen-schema: embedded class '${n3}' is not in the catalog`);
|
|
4710
|
+
const restrict = embeddable.length > 0 ? new Set(embeddable) : null;
|
|
4711
|
+
const canEmbed = (n3) => !!catalog[n3] && transparentValue(catalog, n3) === null && (restrict === null || restrict.has(n3));
|
|
4712
|
+
const embedSet = new Set(Object.keys(catalog).filter(canEmbed));
|
|
4685
4713
|
const inClosure = /* @__PURE__ */ new Set();
|
|
4686
4714
|
for (const start of requested) {
|
|
4687
4715
|
if (!catalog[start]) throw new Error(`gen-schema: requested class '${start}' is not in the catalog`);
|
|
@@ -4700,34 +4728,98 @@ function buildModel(catalog, requested) {
|
|
|
4700
4728
|
return d;
|
|
4701
4729
|
};
|
|
4702
4730
|
const ordered = [...inClosure].sort((a2, b) => depth(a2) - depth(b) || (a2 < b ? -1 : a2 > b ? 1 : 0));
|
|
4703
|
-
const
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4731
|
+
const usedEmbeds = /* @__PURE__ */ new Set();
|
|
4732
|
+
const classify = (f) => {
|
|
4733
|
+
if (f.type.kind === "class") {
|
|
4734
|
+
const inner = transparentValue(catalog, f.type.name ?? "");
|
|
4735
|
+
if (inner) {
|
|
4736
|
+
const c2 = classifyField(inner.type);
|
|
4737
|
+
if (!("skip" in c2) && !("embedded" in c2)) return { ...c2, addOffset: inner.offset };
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
return classifyField(f.type, embedSet);
|
|
4741
|
+
};
|
|
4742
|
+
const fieldsOf = (owner, out, emb, skipped) => {
|
|
4743
|
+
for (const f of catalog[owner].fields) {
|
|
4744
|
+
const c2 = classify(f);
|
|
4709
4745
|
if ("skip" in c2) {
|
|
4710
|
-
skipped.push({ className, rawName: f.name, reason: c2.skip });
|
|
4746
|
+
skipped.push({ className: owner, rawName: f.name, reason: c2.skip });
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
if ("embedded" in c2) {
|
|
4750
|
+
usedEmbeds.add(c2.embedded);
|
|
4751
|
+
emb.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, embeddedClass: c2.embedded });
|
|
4711
4752
|
continue;
|
|
4712
4753
|
}
|
|
4713
|
-
|
|
4754
|
+
out.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, accessorKind: c2.accessorKind, writable: c2.writable, strLen: c2.strLen, addOffset: c2.addOffset });
|
|
4714
4755
|
}
|
|
4715
|
-
|
|
4756
|
+
};
|
|
4757
|
+
const classes = ordered.map((className) => {
|
|
4758
|
+
const parent = catalog[className].parent;
|
|
4759
|
+
const ownFields = [];
|
|
4760
|
+
const embeddedFields = [];
|
|
4761
|
+
const skipped = [];
|
|
4762
|
+
fieldsOf(className, ownFields, embeddedFields, skipped);
|
|
4763
|
+
return { className, parent: parent && inClosure.has(parent) ? parent : null, ownFields, embeddedFields, skipped };
|
|
4716
4764
|
});
|
|
4765
|
+
const embedded = [];
|
|
4766
|
+
const done = /* @__PURE__ */ new Set();
|
|
4767
|
+
for (; ; ) {
|
|
4768
|
+
const next = [...usedEmbeds].filter((n3) => !done.has(n3)).sort();
|
|
4769
|
+
if (next.length === 0) break;
|
|
4770
|
+
for (const className of next) {
|
|
4771
|
+
done.add(className);
|
|
4772
|
+
const fields = [];
|
|
4773
|
+
const embeddedFields = [];
|
|
4774
|
+
const skipped = [];
|
|
4775
|
+
const chain = [];
|
|
4776
|
+
for (let cur = className; cur && catalog[cur]; cur = catalog[cur].parent) chain.unshift(cur);
|
|
4777
|
+
for (const owner of chain) fieldsOf(owner, fields, embeddedFields, skipped);
|
|
4778
|
+
embedded.push({ className, fields, embeddedFields, skipped });
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
embedded.sort((a2, b) => a2.className < b.className ? -1 : a2.className > b.className ? 1 : 0);
|
|
4782
|
+
const ancestors = (cls) => {
|
|
4783
|
+
const out = /* @__PURE__ */ new Set();
|
|
4784
|
+
for (let cur = catalog[cls]?.parent ?? null; cur && catalog[cur]; cur = catalog[cur].parent) out.add(cur);
|
|
4785
|
+
return out;
|
|
4786
|
+
};
|
|
4717
4787
|
const byProp = /* @__PURE__ */ new Map();
|
|
4718
|
-
for (const c2 of classes) for (const f of c2.ownFields) {
|
|
4788
|
+
for (const c2 of classes) for (const f of [...c2.ownFields, ...c2.embeddedFields]) {
|
|
4719
4789
|
(byProp.get(f.propName) ?? byProp.set(f.propName, []).get(f.propName)).push(f);
|
|
4720
4790
|
}
|
|
4721
4791
|
const collisions = [];
|
|
4722
4792
|
for (const [prop, fields] of byProp) {
|
|
4723
|
-
const
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4793
|
+
const decls = /* @__PURE__ */ new Map();
|
|
4794
|
+
for (const f of fields) (decls.get(`${f.declaringClass}.${f.rawName}`) ?? decls.set(`${f.declaringClass}.${f.rawName}`, []).get(`${f.declaringClass}.${f.rawName}`)).push(f);
|
|
4795
|
+
if (decls.size < 2) continue;
|
|
4796
|
+
const keys = [...decls.keys()].sort();
|
|
4797
|
+
const clsOf = (k) => decls.get(k)[0].declaringClass;
|
|
4798
|
+
const conflicting = /* @__PURE__ */ new Set();
|
|
4799
|
+
for (let i2 = 0; i2 < keys.length; i2++) {
|
|
4800
|
+
for (let j = i2 + 1; j < keys.length; j++) {
|
|
4801
|
+
const a2 = clsOf(keys[i2]), b = clsOf(keys[j]);
|
|
4802
|
+
if (a2 === b || ancestors(a2).has(b) || ancestors(b).has(a2)) {
|
|
4803
|
+
conflicting.add(keys[i2]);
|
|
4804
|
+
conflicting.add(keys[j]);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4727
4807
|
}
|
|
4808
|
+
if (conflicting.size === 0) continue;
|
|
4809
|
+
const depthOf = (k) => ancestors(clsOf(k)).size;
|
|
4810
|
+
const inConflict = [...conflicting].sort((x, y) => depthOf(x) - depthOf(y) || (x < y ? -1 : 1));
|
|
4811
|
+
const winnerCls = clsOf(inConflict[0]);
|
|
4812
|
+
const soleWinner = inConflict.filter((k) => clsOf(k) === winnerCls).length === 1;
|
|
4813
|
+
const renamed = [];
|
|
4814
|
+
for (const k of inConflict) {
|
|
4815
|
+
if (soleWinner && k === inConflict[0]) continue;
|
|
4816
|
+
for (const f of decls.get(k)) f.propName = f.rawName;
|
|
4817
|
+
renamed.push(k);
|
|
4818
|
+
}
|
|
4819
|
+
collisions.push(`${prop} <- ${renamed.sort().join(", ")}${soleWinner ? ` (kept by ${winnerCls})` : ""}`);
|
|
4728
4820
|
}
|
|
4729
4821
|
collisions.sort();
|
|
4730
|
-
return { classes, collisions };
|
|
4822
|
+
return { classes, embedded, collisions };
|
|
4731
4823
|
}
|
|
4732
4824
|
|
|
4733
4825
|
// src/schemagen/emit-dts.ts
|
|
@@ -4740,8 +4832,20 @@ function emitDts(model) {
|
|
|
4740
4832
|
const vi = VEC_INFO[f.accessorKind];
|
|
4741
4833
|
if (vi) vecClasses.add(vi.cls);
|
|
4742
4834
|
}
|
|
4835
|
+
for (const e of model.embedded) for (const f of e.fields) {
|
|
4836
|
+
const vi = VEC_INFO[f.accessorKind];
|
|
4837
|
+
if (vi) vecClasses.add(vi.cls);
|
|
4838
|
+
}
|
|
4743
4839
|
if (vecClasses.size) out.push(`import type { ${[...vecClasses].sort().join(", ")} } from "@s2script/sdk/math";`);
|
|
4744
4840
|
out.push("");
|
|
4841
|
+
for (const e of model.embedded) {
|
|
4842
|
+
out.push(`export interface ${e.className} {`);
|
|
4843
|
+
for (const f of e.fields) {
|
|
4844
|
+
out.push(` ${f.writable ? "" : "readonly "}${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
4845
|
+
}
|
|
4846
|
+
for (const f of e.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
|
|
4847
|
+
out.push("}", "");
|
|
4848
|
+
}
|
|
4745
4849
|
for (const c2 of model.classes) {
|
|
4746
4850
|
const ext = c2.parent && names.has(c2.parent) ? ` extends ${c2.parent}` : "";
|
|
4747
4851
|
out.push(`export interface ${c2.className}${ext} {`);
|
|
@@ -4749,18 +4853,66 @@ function emitDts(model) {
|
|
|
4749
4853
|
const ro = f.writable ? "" : "readonly ";
|
|
4750
4854
|
out.push(` ${ro}${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
4751
4855
|
}
|
|
4856
|
+
for (const f of c2.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
|
|
4752
4857
|
out.push("}", "");
|
|
4753
4858
|
}
|
|
4859
|
+
out.push("/** Every wrappable entity class, by schema name. */", "export interface SchemaClasses {");
|
|
4860
|
+
for (const c2 of model.classes) out.push(` ${c2.className}: ${c2.className};`);
|
|
4861
|
+
out.push("}", "");
|
|
4862
|
+
out.push(
|
|
4863
|
+
"/**",
|
|
4864
|
+
" * Apply this schema class's field accessors to an entity reference.",
|
|
4865
|
+
" *",
|
|
4866
|
+
" * `createEntity` returns a bare `EntityRef` with no schema fields on it \u2014 this is how you get",
|
|
4867
|
+
" * them. `Player.pawn` is already wrapped; this is for entities you create or resolve yourself.",
|
|
4868
|
+
" *",
|
|
4869
|
+
" * The wrapper is a view, not a copy: it holds the ref and reads through it on every access, so",
|
|
4870
|
+
" * it stays correct as the entity changes and costs nothing to keep. Field reads return null",
|
|
4871
|
+
" * once the ref goes stale, exactly as they do on a pawn.",
|
|
4872
|
+
" *",
|
|
4873
|
+
" * @example",
|
|
4874
|
+
' * const prop = createEntity("prop_dynamic");',
|
|
4875
|
+
" * prop.spawn();",
|
|
4876
|
+
' * const fields = wrapEntity("CBaseModelEntity", prop);',
|
|
4877
|
+
" * fields.glow.glowing = true;",
|
|
4878
|
+
" */",
|
|
4879
|
+
"export declare function wrapEntity<K extends keyof SchemaClasses>(className: K, ref: EntityRef): SchemaClasses[K];",
|
|
4880
|
+
""
|
|
4881
|
+
);
|
|
4754
4882
|
return out.join("\n");
|
|
4755
4883
|
}
|
|
4756
4884
|
|
|
4757
4885
|
// src/schemagen/emit-js.ts
|
|
4758
4886
|
var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
|
|
4759
4887
|
var S = JSON.stringify;
|
|
4888
|
+
function resolveExpr(f) {
|
|
4889
|
+
const base = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
4890
|
+
return f.addOffset ? `${base} + ${f.addOffset}` : base;
|
|
4891
|
+
}
|
|
4892
|
+
function accessorEntry(f, resolve13, guard2) {
|
|
4893
|
+
const g = guard2 === "" ? "" : `${guard2} `;
|
|
4894
|
+
const gs = guard2 === "" ? "" : `${guard2.replace("return null;", "return;")} `;
|
|
4895
|
+
let entry;
|
|
4896
|
+
if (f.accessorKind === "str") {
|
|
4897
|
+
entry = `get: function () { ${g}return this.ref.readString(${resolve13}, ${f.strLen}); }`;
|
|
4898
|
+
} else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
|
|
4899
|
+
entry = `get: function () { ${g}var v = this.ref.${READ[f.accessorKind]}(${resolve13}); return v === null ? null : v.toString(); }`;
|
|
4900
|
+
} else if (VEC_INFO[f.accessorKind]) {
|
|
4901
|
+
const vi = VEC_INFO[f.accessorKind];
|
|
4902
|
+
entry = `get: function () { ${g}var a = this.ref.readFloats(${resolve13}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
4903
|
+
} else {
|
|
4904
|
+
entry = `get: function () { ${g}return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
|
|
4905
|
+
}
|
|
4906
|
+
if (f.writable) {
|
|
4907
|
+
const wm = WRITE[f.accessorKind];
|
|
4908
|
+
entry += `, set: function (v) { ${gs}var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
|
|
4909
|
+
}
|
|
4910
|
+
return entry;
|
|
4911
|
+
}
|
|
4760
4912
|
function emitJs(model) {
|
|
4761
|
-
const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};"];
|
|
4913
|
+
const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};", " var E = {};"];
|
|
4762
4914
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
4763
|
-
for (const c2 of model.classes) for (const f of
|
|
4915
|
+
for (const c2 of model.classes) for (const f of c2.ownFields) {
|
|
4764
4916
|
const vi = VEC_INFO[f.accessorKind];
|
|
4765
4917
|
if (vi) vecClasses.add(vi.cls);
|
|
4766
4918
|
}
|
|
@@ -4769,36 +4921,58 @@ function emitJs(model) {
|
|
|
4769
4921
|
}
|
|
4770
4922
|
for (const c2 of model.classes) {
|
|
4771
4923
|
out.push(` A[${S(c2.className)}] = {`);
|
|
4772
|
-
for (const f of
|
|
4773
|
-
const
|
|
4774
|
-
let entry;
|
|
4775
|
-
if (f.accessorKind === "str") {
|
|
4776
|
-
entry = `get: function () { return this.ref.readString(${resolve13}, ${f.strLen}); }`;
|
|
4777
|
-
} else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
|
|
4778
|
-
entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve13}); return v === null ? null : v.toString(); }`;
|
|
4779
|
-
} else if (VEC_INFO[f.accessorKind]) {
|
|
4780
|
-
const vi = VEC_INFO[f.accessorKind];
|
|
4781
|
-
entry = `get: function () { var a = this.ref.readFloats(${resolve13}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
|
|
4782
|
-
} else {
|
|
4783
|
-
entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
|
|
4784
|
-
}
|
|
4785
|
-
if (f.writable) {
|
|
4786
|
-
const wm = WRITE[f.accessorKind];
|
|
4787
|
-
entry += `, set: function (v) { var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
|
|
4788
|
-
}
|
|
4924
|
+
for (const f of c2.ownFields) {
|
|
4925
|
+
const entry = accessorEntry(f, resolveExpr(f), "");
|
|
4789
4926
|
out.push(` ${S(f.propName)}: { ${entry} },`);
|
|
4790
4927
|
}
|
|
4928
|
+
for (const f of c2.embeddedFields) {
|
|
4929
|
+
out.push(` ${S(f.propName)}: { get: function () { return embed(${S(f.embeddedClass)}, this.ref, off(${S(f.declaringClass)},${S(f.rawName)})); } },`);
|
|
4930
|
+
}
|
|
4931
|
+
out.push(" };");
|
|
4932
|
+
}
|
|
4933
|
+
for (const e of model.embedded) {
|
|
4934
|
+
out.push(` E[${S(e.className)}] = {`);
|
|
4935
|
+
for (const f of e.fields) {
|
|
4936
|
+
const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
4937
|
+
const resolve13 = f.addOffset ? `this.base + ${sub} + ${f.addOffset}` : `this.base + ${sub}`;
|
|
4938
|
+
const guard2 = `if (this.base < 0 || ${sub} < 0) return null;`;
|
|
4939
|
+
out.push(` ${S(f.propName)}: { ${accessorEntry(f, resolve13, guard2)} },`);
|
|
4940
|
+
}
|
|
4941
|
+
for (const f of e.embeddedFields) {
|
|
4942
|
+
const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
4943
|
+
out.push(` ${S(f.propName)}: { get: function () { var s = ${sub}; return embed(${S(f.embeddedClass)}, this.ref, (this.base < 0 || s < 0) ? -1 : this.base + s); } },`);
|
|
4944
|
+
}
|
|
4791
4945
|
out.push(" };");
|
|
4792
4946
|
}
|
|
4947
|
+
out.push(" var P = {");
|
|
4948
|
+
for (const c2 of model.classes) if (c2.parent) out.push(` ${S(c2.className)}: ${S(c2.parent)},`);
|
|
4949
|
+
out.push(" };");
|
|
4793
4950
|
out.push(
|
|
4951
|
+
" // Walk the ancestor chain rather than storing every inherited accessor per class. Flattening",
|
|
4952
|
+
" // into each table duplicated the whole chain 3.77x on the CS2 class set and grew as",
|
|
4953
|
+
" // O(classes * depth), so every class added inflated every other one; this is O(total fields).",
|
|
4954
|
+
" // Ancestors are applied FIRST so a derived class's own definition wins if a name repeats.",
|
|
4794
4955
|
" function applyAccessors(proto, className) {",
|
|
4795
|
-
" var
|
|
4796
|
-
" for (
|
|
4797
|
-
"
|
|
4956
|
+
" var chain = [], c = className;",
|
|
4957
|
+
" for (; c; c = P[c]) chain.unshift(c);",
|
|
4958
|
+
" for (var i = 0; i < chain.length; i++) {",
|
|
4959
|
+
" var defs = A[chain[i]]; if (!defs) continue;",
|
|
4960
|
+
" for (var name in defs) {",
|
|
4961
|
+
" Object.defineProperty(proto, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
|
|
4962
|
+
" }",
|
|
4798
4963
|
" }",
|
|
4799
4964
|
" }",
|
|
4800
4965
|
" function wrap(className, ref) { var o = { ref: ref }; applyAccessors(o, className); return o; }",
|
|
4966
|
+
" function embed(className, ref, base) {",
|
|
4967
|
+
" var o = { ref: ref, base: base }, defs = E[className];",
|
|
4968
|
+
" if (defs) for (var name in defs) {",
|
|
4969
|
+
" Object.defineProperty(o, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
|
|
4970
|
+
" }",
|
|
4971
|
+
" return o;",
|
|
4972
|
+
" }",
|
|
4801
4973
|
" globalThis.__s2pkg_cs2_schema = { applyAccessors: applyAccessors, wrap: wrap };",
|
|
4974
|
+
" // Merge (not overwrite): this prelude runs FIRST, and pawn.js merges onto whatever it finds.",
|
|
4975
|
+
" globalThis.__s2pkg_cs2 = Object.assign({}, globalThis.__s2pkg_cs2, { wrapEntity: wrap });",
|
|
4802
4976
|
"})();"
|
|
4803
4977
|
);
|
|
4804
4978
|
return out.join("\n");
|
|
@@ -4807,12 +4981,18 @@ function emitJs(model) {
|
|
|
4807
4981
|
// src/schemagen/gen.ts
|
|
4808
4982
|
var CATALOG_PATH = "games/cs2/gamedata/schema-catalog.json";
|
|
4809
4983
|
var LIST_PATH = "games/cs2/codegen-classes.json";
|
|
4984
|
+
var EMBED_PATH = "games/cs2/codegen-embedded.json";
|
|
4810
4985
|
var JS_OUT = "games/cs2/js/schema.generated.js";
|
|
4811
4986
|
var DTS_OUT = "packages/cs2/schema.generated.d.ts";
|
|
4812
4987
|
function runGenSchema(repoRoot2, opts) {
|
|
4813
4988
|
const catalog = JSON.parse(readFileSync13(join13(repoRoot2, CATALOG_PATH), "utf8"));
|
|
4814
4989
|
const list = JSON.parse(readFileSync13(join13(repoRoot2, LIST_PATH), "utf8"));
|
|
4815
|
-
|
|
4990
|
+
let embed = [];
|
|
4991
|
+
try {
|
|
4992
|
+
embed = JSON.parse(readFileSync13(join13(repoRoot2, EMBED_PATH), "utf8"));
|
|
4993
|
+
} catch {
|
|
4994
|
+
}
|
|
4995
|
+
const model = buildModel(catalog, list, embed);
|
|
4816
4996
|
const dts = emitDts(model);
|
|
4817
4997
|
const js = emitJs(model);
|
|
4818
4998
|
const fields = model.classes.reduce((n3, c2) => n3 + c2.ownFields.length, 0);
|
|
@@ -4963,27 +5143,44 @@ var SUPPORTED_NAV_KINDS = /* @__PURE__ */ new Set([
|
|
|
4963
5143
|
"vector",
|
|
4964
5144
|
"qangle"
|
|
4965
5145
|
]);
|
|
5146
|
+
var WRITABLE_NAV_KINDS = /* @__PURE__ */ new Set(["f32", "bool", "i32"]);
|
|
4966
5147
|
function buildNavModel(config, catalog) {
|
|
4967
5148
|
const targetClasses = [...new Set(config.map((e) => e.target))];
|
|
4968
5149
|
const schemaModel = buildModel(catalog, targetClasses);
|
|
4969
5150
|
const sorted = [...config].sort((a2, b) => a2.wrapper < b.wrapper ? -1 : a2.wrapper > b.wrapper ? 1 : 0);
|
|
4970
5151
|
const wrappers = sorted.map((entry) => {
|
|
4971
5152
|
const all = flattenedFields(schemaModel, entry.target);
|
|
5153
|
+
const wantWritable = new Set(entry.writable ?? []);
|
|
4972
5154
|
const fields = [];
|
|
4973
5155
|
const skippedKinds = [];
|
|
4974
5156
|
for (const f of all) {
|
|
4975
5157
|
if (SUPPORTED_NAV_KINDS.has(f.accessorKind)) {
|
|
4976
|
-
fields.push(f);
|
|
5158
|
+
fields.push({ ...f, navWritable: wantWritable.has(f.rawName) });
|
|
4977
5159
|
} else {
|
|
4978
5160
|
skippedKinds.push({ propName: f.propName, accessorKind: f.accessorKind });
|
|
4979
5161
|
}
|
|
4980
5162
|
}
|
|
5163
|
+
for (const raw of wantWritable) {
|
|
5164
|
+
const f = fields.find((x) => x.rawName === raw);
|
|
5165
|
+
if (!f) {
|
|
5166
|
+
const known = all.some((x) => x.rawName === raw);
|
|
5167
|
+
throw new Error(
|
|
5168
|
+
`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?).`)
|
|
5169
|
+
);
|
|
5170
|
+
}
|
|
5171
|
+
if (!WRITABLE_NAV_KINDS.has(f.accessorKind)) {
|
|
5172
|
+
throw new Error(
|
|
5173
|
+
`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.`
|
|
5174
|
+
);
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
4981
5177
|
return {
|
|
4982
5178
|
wrapper: entry.wrapper,
|
|
4983
5179
|
prop: entry.prop,
|
|
4984
5180
|
source: entry.source,
|
|
4985
5181
|
target: entry.target,
|
|
4986
5182
|
path: entry.path,
|
|
5183
|
+
base: entry.base ?? [],
|
|
4987
5184
|
fields,
|
|
4988
5185
|
skippedKinds
|
|
4989
5186
|
};
|
|
@@ -5004,6 +5201,11 @@ var READ_VIA = {
|
|
|
5004
5201
|
u16: "readUInt16Via",
|
|
5005
5202
|
u32: "readUInt32Via"
|
|
5006
5203
|
};
|
|
5204
|
+
var WRITE_VIA = {
|
|
5205
|
+
f32: "writeFloat32Via",
|
|
5206
|
+
bool: "writeBoolVia",
|
|
5207
|
+
i32: "writeInt32Via"
|
|
5208
|
+
};
|
|
5007
5209
|
function emitNavJs(model) {
|
|
5008
5210
|
const out = [HEADER4, "(function () {", " var off = __s2_schema_offset;"];
|
|
5009
5211
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
@@ -5017,10 +5219,12 @@ function emitNavJs(model) {
|
|
|
5017
5219
|
out.push(` var ${cls} = __s2require("@s2script/sdk/math").${cls};`);
|
|
5018
5220
|
}
|
|
5019
5221
|
for (const w of model.wrappers) {
|
|
5020
|
-
out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
|
|
5222
|
+
out.push(` function ${w.wrapper}(root, path, base) { this.root = root; this.path = path; this.base = base; }`);
|
|
5021
5223
|
out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
|
|
5022
5224
|
for (const f of w.fields) {
|
|
5023
|
-
const
|
|
5225
|
+
const own = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
|
|
5226
|
+
const withDelta = f.addOffset ? `${own} + ${f.addOffset}` : own;
|
|
5227
|
+
const resolve13 = `${withDelta} + this.base`;
|
|
5024
5228
|
let getter;
|
|
5025
5229
|
const kind = f.accessorKind;
|
|
5026
5230
|
if (kind === "u64") {
|
|
@@ -5037,7 +5241,9 @@ function emitNavJs(model) {
|
|
|
5037
5241
|
if (!method) continue;
|
|
5038
5242
|
getter = `get: function () { return this.root.${method}(this.path, ${resolve13}); }`;
|
|
5039
5243
|
}
|
|
5040
|
-
|
|
5244
|
+
const wmethod = f.navWritable ? WRITE_VIA[kind] : void 0;
|
|
5245
|
+
const setter = wmethod ? `, set: function (v) { this.root.${wmethod}(this.path, ${resolve13}, v); }` : "";
|
|
5246
|
+
out.push(` ${S2(f.propName)}: { ${getter}${setter} },`);
|
|
5041
5247
|
}
|
|
5042
5248
|
out.push(" });");
|
|
5043
5249
|
}
|
|
@@ -5057,7 +5263,16 @@ function emitNavJs(model) {
|
|
|
5057
5263
|
const h2 = w.path[i2];
|
|
5058
5264
|
out.push(` var o${i2} = off(${S2(h2.cls)},${S2(h2.field)}); if (o${i2} < 0) return null; _p.push(o${i2});`);
|
|
5059
5265
|
}
|
|
5060
|
-
|
|
5266
|
+
if (w.base.length > 0) {
|
|
5267
|
+
out.push(` var _b = 0;`);
|
|
5268
|
+
for (let i2 = 0; i2 < w.base.length; i2++) {
|
|
5269
|
+
const h2 = w.base[i2];
|
|
5270
|
+
out.push(` var b${i2} = off(${S2(h2.cls)},${S2(h2.field)}); if (b${i2} < 0) return null; _b += b${i2};`);
|
|
5271
|
+
}
|
|
5272
|
+
out.push(` return new ${w.wrapper}(this.ref, _p, _b);`);
|
|
5273
|
+
} else {
|
|
5274
|
+
out.push(` return new ${w.wrapper}(this.ref, _p, 0);`);
|
|
5275
|
+
}
|
|
5061
5276
|
out.push(` }, enumerable: true, configurable: true,`);
|
|
5062
5277
|
out.push(` });`);
|
|
5063
5278
|
}
|
|
@@ -5092,9 +5307,20 @@ function emitNavDts(model) {
|
|
|
5092
5307
|
if (needsEntityRef) out.push('import type { EntityRef } from "@s2script/sdk/entity";');
|
|
5093
5308
|
out.push("");
|
|
5094
5309
|
for (const w of model.wrappers) {
|
|
5310
|
+
if (w.fields.some((f) => f.navWritable)) {
|
|
5311
|
+
out.push(`/**`);
|
|
5312
|
+
out.push(` * Fields NOT marked \`readonly\` are writable.`);
|
|
5313
|
+
out.push(` *`);
|
|
5314
|
+
out.push(` * A write takes effect immediately \u2014 the server reads these during movement \u2014 but it is **not**`);
|
|
5315
|
+
out.push(` * flagged for replication, because this object is reached through a pointer chain and the`);
|
|
5316
|
+
out.push(` * change-notifier addresses the root entity. A client predicting the old value may therefore`);
|
|
5317
|
+
out.push(` * see brief mismatch until the next authoritative correction. SourceMod's \`SetEntPropFloat\``);
|
|
5318
|
+
out.push(` * on the same fields behaves the same way.`);
|
|
5319
|
+
out.push(` */`);
|
|
5320
|
+
}
|
|
5095
5321
|
out.push(`export interface ${w.wrapper} {`);
|
|
5096
5322
|
for (const f of w.fields) {
|
|
5097
|
-
out.push(` readonly ${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
5323
|
+
out.push(` ${f.navWritable ? "" : "readonly "}${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
5098
5324
|
}
|
|
5099
5325
|
out.push("}", "");
|
|
5100
5326
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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
|
+
};
|