@s2script/sdk 0.10.1 → 0.11.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/dist/cli.js +116 -13
- package/entity.d.ts +14 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1539,7 +1539,7 @@ var PLATFORM = "linuxsteamrt64";
|
|
|
1539
1539
|
var ARG_KINDS = ["bool", "int", "float", "string", "vector", "entity"];
|
|
1540
1540
|
var RET_KINDS = ["void", "bool", "int", "float", "entity"];
|
|
1541
1541
|
var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "vector", "entity"]);
|
|
1542
|
-
var MAX_INT_ARGS =
|
|
1542
|
+
var MAX_INT_ARGS = 9;
|
|
1543
1543
|
var MAX_FLOAT_ARGS = 8;
|
|
1544
1544
|
|
|
1545
1545
|
// src/gamedata/validate.ts
|
|
@@ -1604,8 +1604,12 @@ function validatePluginGamedata(gd, opts) {
|
|
|
1604
1604
|
}
|
|
1605
1605
|
const decl = rawDecl;
|
|
1606
1606
|
const recv = decl.receiver;
|
|
1607
|
-
if (!recv || recv.kind !== "entity") {
|
|
1608
|
-
errs.push(`${where}: receiver.kind must be "entity"
|
|
1607
|
+
if (!recv || recv.kind !== "entity" && recv.kind !== "none") {
|
|
1608
|
+
errs.push(`${where}: receiver.kind must be "entity" or "none"`);
|
|
1609
|
+
} else if (recv.kind === "none") {
|
|
1610
|
+
if (recv.via !== void 0) {
|
|
1611
|
+
errs.push(`${where}: receiver.kind "none" cannot carry a 'via' sub-object hop`);
|
|
1612
|
+
}
|
|
1609
1613
|
} else if (recv.via !== void 0) {
|
|
1610
1614
|
const via = recv.via;
|
|
1611
1615
|
if (typeof via?.class !== "string" || typeof via?.field !== "string") {
|
|
@@ -1649,7 +1653,7 @@ function validatePluginGamedata(gd, opts) {
|
|
|
1649
1653
|
else floats++;
|
|
1650
1654
|
}
|
|
1651
1655
|
if (ints > MAX_INT_ARGS) {
|
|
1652
|
-
errs.push(`${where}: ${ints} integer-class args exceeds the max of ${MAX_INT_ARGS}
|
|
1656
|
+
errs.push(`${where}: ${ints} integer-class args exceeds the max of ${MAX_INT_ARGS}`);
|
|
1653
1657
|
}
|
|
1654
1658
|
if (floats > MAX_FLOAT_ARGS) {
|
|
1655
1659
|
errs.push(`${where}: ${floats} float args exceeds the max of ${MAX_FLOAT_ARGS}`);
|
|
@@ -1710,8 +1714,9 @@ function generateGamedataTypes(gd) {
|
|
|
1710
1714
|
const lines = Object.keys(calls).sort().map((name) => {
|
|
1711
1715
|
const decl = calls[name];
|
|
1712
1716
|
const names = decl.argNames;
|
|
1717
|
+
const receiverless = decl.receiver?.kind === "none";
|
|
1713
1718
|
const params = [
|
|
1714
|
-
"self: EntityRef",
|
|
1719
|
+
...receiverless ? [] : ["self: EntityRef"],
|
|
1715
1720
|
...decl.args.map((a2, i2) => `${names?.[i2] ?? `a${i2}`}: ${ARG_TS[a2]}`)
|
|
1716
1721
|
];
|
|
1717
1722
|
return ` ${name}: (${params.join(", ")}) => ${RET_TS[decl.returns]};`;
|
|
@@ -4585,6 +4590,37 @@ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from
|
|
|
4585
4590
|
import { join as join13 } from "node:path";
|
|
4586
4591
|
|
|
4587
4592
|
// src/schemagen/model.ts
|
|
4593
|
+
function enumMemberNames(enumName, members) {
|
|
4594
|
+
const raw = {};
|
|
4595
|
+
for (const m of members) raw[m] = m;
|
|
4596
|
+
if (members.length === 0) return raw;
|
|
4597
|
+
const stem = enumName.replace(/_t$/, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
4598
|
+
if (stem === "") return raw;
|
|
4599
|
+
const stripped = {};
|
|
4600
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4601
|
+
for (const m of members) {
|
|
4602
|
+
const up = m.toUpperCase();
|
|
4603
|
+
let rest = null;
|
|
4604
|
+
for (const sep3 of ["_", ""]) {
|
|
4605
|
+
const pfx = stem + sep3;
|
|
4606
|
+
if (up.startsWith(pfx) && m.length > pfx.length) {
|
|
4607
|
+
rest = m.slice(pfx.length);
|
|
4608
|
+
break;
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
if (rest === null && /^k[A-Z]/.test(m)) rest = m.slice(1);
|
|
4612
|
+
if (rest === null) return raw;
|
|
4613
|
+
const name = /^[0-9]/.test(rest) ? `_${rest}` : rest;
|
|
4614
|
+
if (seen.has(name)) return raw;
|
|
4615
|
+
seen.add(name);
|
|
4616
|
+
stripped[m] = name;
|
|
4617
|
+
}
|
|
4618
|
+
return stripped;
|
|
4619
|
+
}
|
|
4620
|
+
function enumTsName(schemaName) {
|
|
4621
|
+
const id = schemaName.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
4622
|
+
return /^[0-9]/.test(id) ? `_${id}` : id;
|
|
4623
|
+
}
|
|
4588
4624
|
var READ = {
|
|
4589
4625
|
f32: "readFloat32",
|
|
4590
4626
|
bool: "readBool",
|
|
@@ -4683,7 +4719,7 @@ function classifyField(type, embeddable = /* @__PURE__ */ new Set()) {
|
|
|
4683
4719
|
}
|
|
4684
4720
|
if (type.kind === "enum") {
|
|
4685
4721
|
const w = ENUM_WIDTH[type.size ?? 0];
|
|
4686
|
-
if (w) return { accessorKind: w, writable: WRITE[w] !== void 0 };
|
|
4722
|
+
if (w) return { accessorKind: w, writable: WRITE[w] !== void 0, enumType: type.name };
|
|
4687
4723
|
return { skip: `enum '${type.name ?? ""}' byte width not stated by the schema` };
|
|
4688
4724
|
}
|
|
4689
4725
|
if (type.kind === "class") {
|
|
@@ -4705,7 +4741,7 @@ function flattenedFields(model, className) {
|
|
|
4705
4741
|
}
|
|
4706
4742
|
return chain.flatMap((c2) => c2.ownFields);
|
|
4707
4743
|
}
|
|
4708
|
-
function buildModel(catalog, requested, embeddable = []) {
|
|
4744
|
+
function buildModel(catalog, requested, embeddable = [], enumCatalog = {}) {
|
|
4709
4745
|
for (const n3 of embeddable) if (!catalog[n3]) throw new Error(`gen-schema: embedded class '${n3}' is not in the catalog`);
|
|
4710
4746
|
const restrict = embeddable.length > 0 ? new Set(embeddable) : null;
|
|
4711
4747
|
const canEmbed = (n3) => !!catalog[n3] && transparentValue(catalog, n3) === null && (restrict === null || restrict.has(n3));
|
|
@@ -4751,7 +4787,7 @@ function buildModel(catalog, requested, embeddable = []) {
|
|
|
4751
4787
|
emb.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, embeddedClass: c2.embedded });
|
|
4752
4788
|
continue;
|
|
4753
4789
|
}
|
|
4754
|
-
out.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, accessorKind: c2.accessorKind, writable: c2.writable, strLen: c2.strLen, addOffset: c2.addOffset });
|
|
4790
|
+
out.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, accessorKind: c2.accessorKind, writable: c2.writable, strLen: c2.strLen, addOffset: c2.addOffset, enumType: c2.enumType });
|
|
4755
4791
|
}
|
|
4756
4792
|
};
|
|
4757
4793
|
const classes = ordered.map((className) => {
|
|
@@ -4819,12 +4855,42 @@ function buildModel(catalog, requested, embeddable = []) {
|
|
|
4819
4855
|
collisions.push(`${prop} <- ${renamed.sort().join(", ")}${soleWinner ? ` (kept by ${winnerCls})` : ""}`);
|
|
4820
4856
|
}
|
|
4821
4857
|
collisions.sort();
|
|
4822
|
-
|
|
4858
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4859
|
+
const noteEnum = (f) => {
|
|
4860
|
+
if (f.type.kind === "enum" && f.type.name && enumCatalog[f.type.name]) referenced.add(f.type.name);
|
|
4861
|
+
};
|
|
4862
|
+
for (const cls of inClosure) for (const f of catalog[cls].fields) noteEnum(f);
|
|
4863
|
+
for (const e of embedded) for (const owner of new Set(e.fields.map((f) => f.declaringClass))) {
|
|
4864
|
+
for (const f of catalog[owner]?.fields ?? []) noteEnum(f);
|
|
4865
|
+
}
|
|
4866
|
+
const byTsName = /* @__PURE__ */ new Map();
|
|
4867
|
+
for (const n3 of referenced) {
|
|
4868
|
+
const id = enumTsName(n3);
|
|
4869
|
+
(byTsName.get(id) ?? byTsName.set(id, []).get(id)).push(n3);
|
|
4870
|
+
}
|
|
4871
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
4872
|
+
for (const [, group] of byTsName) if (group.length > 1) for (const n3 of group) ambiguous.add(n3);
|
|
4873
|
+
const enums = [...referenced].filter((n3) => !ambiguous.has(n3)).sort().map((className) => {
|
|
4874
|
+
const values = enumCatalog[className].values;
|
|
4875
|
+
const names = enumMemberNames(className, Object.keys(values));
|
|
4876
|
+
return {
|
|
4877
|
+
className,
|
|
4878
|
+
tsName: enumTsName(className),
|
|
4879
|
+
members: Object.keys(values).sort().map((raw) => ({ name: names[raw], raw, value: values[raw] }))
|
|
4880
|
+
};
|
|
4881
|
+
});
|
|
4882
|
+
for (const n3 of [...ambiguous].sort()) collisions.push(`enum ${n3} -> ${enumTsName(n3)} (ambiguous; emitted as number)`);
|
|
4883
|
+
return { classes, embedded, enums, collisions };
|
|
4823
4884
|
}
|
|
4824
4885
|
|
|
4825
4886
|
// src/schemagen/emit-dts.ts
|
|
4826
4887
|
var HEADER = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
|
|
4827
4888
|
function emitDts(model) {
|
|
4889
|
+
const enumTs = new Map(model.enums.map((e) => [e.className, e.tsName]));
|
|
4890
|
+
const tsType = (f) => {
|
|
4891
|
+
const id = f.enumType ? enumTs.get(f.enumType) : void 0;
|
|
4892
|
+
return id ? `${id} | null` : TSTYPE[f.accessorKind];
|
|
4893
|
+
};
|
|
4828
4894
|
const names = new Set(model.classes.map((c2) => c2.className));
|
|
4829
4895
|
const out = [HEADER, 'import type { EntityRef } from "@s2script/sdk/entity";'];
|
|
4830
4896
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
@@ -4838,10 +4904,21 @@ function emitDts(model) {
|
|
|
4838
4904
|
}
|
|
4839
4905
|
if (vecClasses.size) out.push(`import type { ${[...vecClasses].sort().join(", ")} } from "@s2script/sdk/math";`);
|
|
4840
4906
|
out.push("");
|
|
4907
|
+
for (const e of model.enums) {
|
|
4908
|
+
out.push(`export declare const ${e.tsName}: {`);
|
|
4909
|
+
for (const m of e.members) {
|
|
4910
|
+
const doc = m.name === m.raw ? "" : ` /** \`${m.raw}\` */
|
|
4911
|
+
`;
|
|
4912
|
+
out.push(`${doc} readonly ${m.name}: ${m.value};`);
|
|
4913
|
+
}
|
|
4914
|
+
out.push("};");
|
|
4915
|
+
if (e.tsName !== e.className) out.push(`/** Schema enum \`${e.className}\`. */`);
|
|
4916
|
+
out.push(`export type ${e.tsName} = (typeof ${e.tsName})[keyof typeof ${e.tsName}];`, "");
|
|
4917
|
+
}
|
|
4841
4918
|
for (const e of model.embedded) {
|
|
4842
4919
|
out.push(`export interface ${e.className} {`);
|
|
4843
4920
|
for (const f of e.fields) {
|
|
4844
|
-
out.push(` ${f.writable ? "" : "readonly "}${f.propName}: ${
|
|
4921
|
+
out.push(` ${f.writable ? "" : "readonly "}${f.propName}: ${tsType(f)};`);
|
|
4845
4922
|
}
|
|
4846
4923
|
for (const f of e.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
|
|
4847
4924
|
out.push("}", "");
|
|
@@ -4851,7 +4928,7 @@ function emitDts(model) {
|
|
|
4851
4928
|
out.push(`export interface ${c2.className}${ext} {`);
|
|
4852
4929
|
for (const f of c2.ownFields) {
|
|
4853
4930
|
const ro = f.writable ? "" : "readonly ";
|
|
4854
|
-
out.push(` ${ro}${f.propName}: ${
|
|
4931
|
+
out.push(` ${ro}${f.propName}: ${tsType(f)};`);
|
|
4855
4932
|
}
|
|
4856
4933
|
for (const f of c2.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
|
|
4857
4934
|
out.push("}", "");
|
|
@@ -4947,6 +5024,13 @@ function emitJs(model) {
|
|
|
4947
5024
|
out.push(" var P = {");
|
|
4948
5025
|
for (const c2 of model.classes) if (c2.parent) out.push(` ${S(c2.className)}: ${S(c2.parent)},`);
|
|
4949
5026
|
out.push(" };");
|
|
5027
|
+
if (model.enums.length > 0) {
|
|
5028
|
+
out.push(" var E_CONST = {};");
|
|
5029
|
+
for (const e of model.enums) {
|
|
5030
|
+
const body = e.members.map((m) => `${S(m.name)}: ${m.value}`).join(", ");
|
|
5031
|
+
out.push(` E_CONST[${S(e.tsName)}] = Object.freeze({ ${body} });`);
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
4950
5034
|
out.push(
|
|
4951
5035
|
" // Walk the ancestor chain rather than storing every inherited accessor per class. Flattening",
|
|
4952
5036
|
" // into each table duplicated the whole chain 3.77x on the CS2 class set and grew as",
|
|
@@ -4972,7 +5056,7 @@ function emitJs(model) {
|
|
|
4972
5056
|
" }",
|
|
4973
5057
|
" globalThis.__s2pkg_cs2_schema = { applyAccessors: applyAccessors, wrap: wrap };",
|
|
4974
5058
|
" // 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 });",
|
|
5059
|
+
" globalThis.__s2pkg_cs2 = Object.assign({}, globalThis.__s2pkg_cs2, { wrapEntity: wrap }" + (model.enums.length > 0 ? ", E_CONST);" : ");"),
|
|
4976
5060
|
"})();"
|
|
4977
5061
|
);
|
|
4978
5062
|
return out.join("\n");
|
|
@@ -4980,6 +5064,7 @@ function emitJs(model) {
|
|
|
4980
5064
|
|
|
4981
5065
|
// src/schemagen/gen.ts
|
|
4982
5066
|
var CATALOG_PATH = "games/cs2/gamedata/schema-catalog.json";
|
|
5067
|
+
var ENUMS_PATH = "games/cs2/gamedata/schema-enums.json";
|
|
4983
5068
|
var LIST_PATH = "games/cs2/codegen-classes.json";
|
|
4984
5069
|
var EMBED_PATH = "games/cs2/codegen-embedded.json";
|
|
4985
5070
|
var JS_OUT = "games/cs2/js/schema.generated.js";
|
|
@@ -4992,7 +5077,12 @@ function runGenSchema(repoRoot2, opts) {
|
|
|
4992
5077
|
embed = JSON.parse(readFileSync13(join13(repoRoot2, EMBED_PATH), "utf8"));
|
|
4993
5078
|
} catch {
|
|
4994
5079
|
}
|
|
4995
|
-
|
|
5080
|
+
let enumCatalog = {};
|
|
5081
|
+
try {
|
|
5082
|
+
enumCatalog = JSON.parse(readFileSync13(join13(repoRoot2, ENUMS_PATH), "utf8"));
|
|
5083
|
+
} catch {
|
|
5084
|
+
}
|
|
5085
|
+
const model = buildModel(catalog, list, embed, enumCatalog);
|
|
4996
5086
|
const dts = emitDts(model);
|
|
4997
5087
|
const js = emitJs(model);
|
|
4998
5088
|
const fields = model.classes.reduce((n3, c2) => n3 + c2.ownFields.length, 0);
|
|
@@ -5220,6 +5310,9 @@ function emitNavJs(model) {
|
|
|
5220
5310
|
}
|
|
5221
5311
|
for (const w of model.wrappers) {
|
|
5222
5312
|
out.push(` function ${w.wrapper}(root, path, base) { this.root = root; this.path = path; this.base = base; }`);
|
|
5313
|
+
out.push(` ${w.wrapper}.prototype.notifyChanged = function () {`);
|
|
5314
|
+
out.push(` if (this.path.length > 0) this.root.notifyStateChanged(this.path[0]);`);
|
|
5315
|
+
out.push(` };`);
|
|
5223
5316
|
out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
|
|
5224
5317
|
for (const f of w.fields) {
|
|
5225
5318
|
const own = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
|
|
@@ -5319,6 +5412,16 @@ function emitNavDts(model) {
|
|
|
5319
5412
|
out.push(` */`);
|
|
5320
5413
|
}
|
|
5321
5414
|
out.push(`export interface ${w.wrapper} {`);
|
|
5415
|
+
out.push(
|
|
5416
|
+
" /**",
|
|
5417
|
+
" * Flag this sub-object for network replication after writing to it.",
|
|
5418
|
+
" *",
|
|
5419
|
+
" * Setters do not do this for you. For most targets it does not matter \u2014 the server reads the",
|
|
5420
|
+
" * field every tick \u2014 but anything a CLIENT renders (a scoreboard counter, say) will not update",
|
|
5421
|
+
" * until you call this.",
|
|
5422
|
+
" */",
|
|
5423
|
+
" notifyChanged(): void;"
|
|
5424
|
+
);
|
|
5322
5425
|
for (const f of w.fields) {
|
|
5323
5426
|
out.push(` ${f.navWritable ? "" : "readonly "}${f.propName}: ${TSTYPE[f.accessorKind]};`);
|
|
5324
5427
|
}
|
package/entity.d.ts
CHANGED
|
@@ -116,6 +116,20 @@ export declare class EntityRef {
|
|
|
116
116
|
notifyStateChanged(offset: number): void;
|
|
117
117
|
/** Raw identity-slot flags (engine m_flags), or null when stale/unavailable. Bit meanings are game-specific. */
|
|
118
118
|
identityFlags(): number | null;
|
|
119
|
+
/**
|
|
120
|
+
* CLEAR identity-slot flag bits; returns the flags after the write, or null when stale.
|
|
121
|
+
*
|
|
122
|
+
* Clear-only by design: `mask` names bits to DROP and nothing can be raised, because handing a
|
|
123
|
+
* plugin the ability to set arbitrary identity bits is a far larger foot-gun than the one this
|
|
124
|
+
* closes. The invalid-ehandle bit is refused outright — a dead slot must never be presentable as
|
|
125
|
+
* live.
|
|
126
|
+
*
|
|
127
|
+
* The case this exists for is the staging bit. `setModel` asserts that an entity is NOT in the
|
|
128
|
+
* staging list, and a created-but-unspawned entity is, so the create -> setModel -> spawn ordering
|
|
129
|
+
* (what CS2's own body spawner does) is otherwise unavailable and the alternatives produce
|
|
130
|
+
* half-initialised entities that clients fail to copy.
|
|
131
|
+
*/
|
|
132
|
+
clearIdentityFlags(mask: number): number | null;
|
|
119
133
|
/** DispatchSpawn this created entity. With keyvalues, the entity's own Spawn() parses them (the
|
|
120
134
|
* SourceMod DispatchKeyValue / CSSharp DispatchSpawn(kv) mechanism). Returns false if stale,
|
|
121
135
|
* unresolved, or the keyvalue map is rejected (non-finite number, unsupported value type, empty
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|