@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.
Files changed (3) hide show
  1. package/dist/cli.js +116 -13
  2. package/entity.d.ts +14 -0
  3. 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 = 5;
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" in v1`);
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} (\`this\` occupies rdi)`);
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
- return { classes, embedded, collisions };
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}: ${TSTYPE[f.accessorKind]};`);
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}: ${TSTYPE[f.accessorKind]};`);
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
- const model = buildModel(catalog, list, embed);
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.10.1",
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",