@s2script/sdk 0.10.0 → 0.10.2

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 (2) hide show
  1. package/dist/cli.js +320 -47
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -4585,6 +4585,37 @@ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from
4585
4585
  import { join as join13 } from "node:path";
4586
4586
 
4587
4587
  // src/schemagen/model.ts
4588
+ function enumMemberNames(enumName, members) {
4589
+ const raw = {};
4590
+ for (const m of members) raw[m] = m;
4591
+ if (members.length === 0) return raw;
4592
+ const stem = enumName.replace(/_t$/, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase();
4593
+ if (stem === "") return raw;
4594
+ const stripped = {};
4595
+ const seen = /* @__PURE__ */ new Set();
4596
+ for (const m of members) {
4597
+ const up = m.toUpperCase();
4598
+ let rest = null;
4599
+ for (const sep3 of ["_", ""]) {
4600
+ const pfx = stem + sep3;
4601
+ if (up.startsWith(pfx) && m.length > pfx.length) {
4602
+ rest = m.slice(pfx.length);
4603
+ break;
4604
+ }
4605
+ }
4606
+ if (rest === null && /^k[A-Z]/.test(m)) rest = m.slice(1);
4607
+ if (rest === null) return raw;
4608
+ const name = /^[0-9]/.test(rest) ? `_${rest}` : rest;
4609
+ if (seen.has(name)) return raw;
4610
+ seen.add(name);
4611
+ stripped[m] = name;
4612
+ }
4613
+ return stripped;
4614
+ }
4615
+ function enumTsName(schemaName) {
4616
+ const id = schemaName.replace(/[^A-Za-z0-9_$]/g, "_");
4617
+ return /^[0-9]/.test(id) ? `_${id}` : id;
4618
+ }
4588
4619
  var READ = {
4589
4620
  f32: "readFloat32",
4590
4621
  bool: "readBool",
@@ -4640,9 +4671,15 @@ var ATOMIC = {
4640
4671
  uint32: { k: "u32", w: true },
4641
4672
  uint64: { k: "u64", w: false },
4642
4673
  int64: { k: "i64", w: false },
4643
- float64: { k: "f64", w: false }
4674
+ float64: { k: "f64", w: false },
4644
4675
  // no narrow 64-bit writers
4676
+ // `Color` is a 4-byte RGBA struct, not a scalar, but it is laid out exactly as a little-endian
4677
+ // uint32 (R in the low byte) and every engine consumer treats it that way. Mapping it here rather
4678
+ // than adding a value class keeps the surface small and matches how packed colours are already
4679
+ // passed elsewhere in the SDK (e.g. the fade user-message).
4680
+ Color: { k: "u32", w: true }
4645
4681
  };
4682
+ var ENUM_WIDTH = { 1: "u8", 2: "u16", 4: "u32", 8: "u64" };
4646
4683
  var VEC = { Vector: "vector", VectorWS: "vector", QAngle: "qangle" };
4647
4684
  var VEC_INFO = {
4648
4685
  vector: { cls: "Vector", count: 3 },
@@ -4655,7 +4692,13 @@ function idiomaticName(raw) {
4655
4692
  const core = m && KNOWN_TAGS.has(m[1]) ? m[2] : s;
4656
4693
  return core.charAt(0).toLowerCase() + core.slice(1);
4657
4694
  }
4658
- function classifyField(type) {
4695
+ function transparentValue(catalog, className) {
4696
+ const e = catalog[className];
4697
+ if (!e || e.parent || e.fields.length !== 1) return null;
4698
+ const f = e.fields[0];
4699
+ return f.name === "m_Value" && f.type.kind === "atomic" ? f : null;
4700
+ }
4701
+ function classifyField(type, embeddable = /* @__PURE__ */ new Set()) {
4659
4702
  if (type.kind === "handle") return { accessorKind: "handle", writable: false };
4660
4703
  if (type.kind === "atomic") {
4661
4704
  const vk = VEC[type.name ?? ""];
@@ -4669,8 +4712,16 @@ function classifyField(type) {
4669
4712
  if (cm) return { accessorKind: "str", writable: false, strLen: parseInt(cm[1], 10) };
4670
4713
  return { skip: `unmapped 'unknown' type '${type.name ?? ""}'` };
4671
4714
  }
4672
- if (type.kind === "enum") return { skip: "enum byte-width absent from catalog (deferred)" };
4673
- if (type.kind === "class") return { skip: `embedded class '${type.name ?? ""}' deferred` };
4715
+ if (type.kind === "enum") {
4716
+ const w = ENUM_WIDTH[type.size ?? 0];
4717
+ if (w) return { accessorKind: w, writable: WRITE[w] !== void 0, enumType: type.name };
4718
+ return { skip: `enum '${type.name ?? ""}' byte width not stated by the schema` };
4719
+ }
4720
+ if (type.kind === "class") {
4721
+ const n3 = type.name ?? "";
4722
+ if (embeddable.has(n3)) return { embedded: n3 };
4723
+ return { skip: `embedded class '${n3}' deferred` };
4724
+ }
4674
4725
  if (type.kind === "ptr") return { skip: "raw pointer" };
4675
4726
  return { skip: `unmapped kind '${type.kind}'` };
4676
4727
  }
@@ -4685,7 +4736,11 @@ function flattenedFields(model, className) {
4685
4736
  }
4686
4737
  return chain.flatMap((c2) => c2.ownFields);
4687
4738
  }
4688
- function buildModel(catalog, requested) {
4739
+ function buildModel(catalog, requested, embeddable = [], enumCatalog = {}) {
4740
+ for (const n3 of embeddable) if (!catalog[n3]) throw new Error(`gen-schema: embedded class '${n3}' is not in the catalog`);
4741
+ const restrict = embeddable.length > 0 ? new Set(embeddable) : null;
4742
+ const canEmbed = (n3) => !!catalog[n3] && transparentValue(catalog, n3) === null && (restrict === null || restrict.has(n3));
4743
+ const embedSet = new Set(Object.keys(catalog).filter(canEmbed));
4689
4744
  const inClosure = /* @__PURE__ */ new Set();
4690
4745
  for (const start of requested) {
4691
4746
  if (!catalog[start]) throw new Error(`gen-schema: requested class '${start}' is not in the catalog`);
@@ -4704,39 +4759,133 @@ function buildModel(catalog, requested) {
4704
4759
  return d;
4705
4760
  };
4706
4761
  const ordered = [...inClosure].sort((a2, b) => depth(a2) - depth(b) || (a2 < b ? -1 : a2 > b ? 1 : 0));
4707
- const classes = ordered.map((className) => {
4708
- const parent = catalog[className].parent;
4709
- const ownFields = [];
4710
- const skipped = [];
4711
- for (const f of catalog[className].fields) {
4712
- const c2 = classifyField(f.type);
4762
+ const usedEmbeds = /* @__PURE__ */ new Set();
4763
+ const classify = (f) => {
4764
+ if (f.type.kind === "class") {
4765
+ const inner = transparentValue(catalog, f.type.name ?? "");
4766
+ if (inner) {
4767
+ const c2 = classifyField(inner.type);
4768
+ if (!("skip" in c2) && !("embedded" in c2)) return { ...c2, addOffset: inner.offset };
4769
+ }
4770
+ }
4771
+ return classifyField(f.type, embedSet);
4772
+ };
4773
+ const fieldsOf = (owner, out, emb, skipped) => {
4774
+ for (const f of catalog[owner].fields) {
4775
+ const c2 = classify(f);
4713
4776
  if ("skip" in c2) {
4714
- skipped.push({ className, rawName: f.name, reason: c2.skip });
4777
+ skipped.push({ className: owner, rawName: f.name, reason: c2.skip });
4778
+ continue;
4779
+ }
4780
+ if ("embedded" in c2) {
4781
+ usedEmbeds.add(c2.embedded);
4782
+ emb.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: owner, embeddedClass: c2.embedded });
4715
4783
  continue;
4716
4784
  }
4717
- ownFields.push({ propName: idiomaticName(f.name), rawName: f.name, declaringClass: className, accessorKind: c2.accessorKind, writable: c2.writable, strLen: c2.strLen });
4785
+ 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 });
4718
4786
  }
4719
- return { className, parent: parent && inClosure.has(parent) ? parent : null, ownFields, skipped };
4787
+ };
4788
+ const classes = ordered.map((className) => {
4789
+ const parent = catalog[className].parent;
4790
+ const ownFields = [];
4791
+ const embeddedFields = [];
4792
+ const skipped = [];
4793
+ fieldsOf(className, ownFields, embeddedFields, skipped);
4794
+ return { className, parent: parent && inClosure.has(parent) ? parent : null, ownFields, embeddedFields, skipped };
4720
4795
  });
4796
+ const embedded = [];
4797
+ const done = /* @__PURE__ */ new Set();
4798
+ for (; ; ) {
4799
+ const next = [...usedEmbeds].filter((n3) => !done.has(n3)).sort();
4800
+ if (next.length === 0) break;
4801
+ for (const className of next) {
4802
+ done.add(className);
4803
+ const fields = [];
4804
+ const embeddedFields = [];
4805
+ const skipped = [];
4806
+ const chain = [];
4807
+ for (let cur = className; cur && catalog[cur]; cur = catalog[cur].parent) chain.unshift(cur);
4808
+ for (const owner of chain) fieldsOf(owner, fields, embeddedFields, skipped);
4809
+ embedded.push({ className, fields, embeddedFields, skipped });
4810
+ }
4811
+ }
4812
+ embedded.sort((a2, b) => a2.className < b.className ? -1 : a2.className > b.className ? 1 : 0);
4813
+ const ancestors = (cls) => {
4814
+ const out = /* @__PURE__ */ new Set();
4815
+ for (let cur = catalog[cls]?.parent ?? null; cur && catalog[cur]; cur = catalog[cur].parent) out.add(cur);
4816
+ return out;
4817
+ };
4721
4818
  const byProp = /* @__PURE__ */ new Map();
4722
- for (const c2 of classes) for (const f of c2.ownFields) {
4819
+ for (const c2 of classes) for (const f of [...c2.ownFields, ...c2.embeddedFields]) {
4723
4820
  (byProp.get(f.propName) ?? byProp.set(f.propName, []).get(f.propName)).push(f);
4724
4821
  }
4725
4822
  const collisions = [];
4726
4823
  for (const [prop, fields] of byProp) {
4727
- const distinct = new Set(fields.map((f) => `${f.declaringClass}.${f.rawName}`));
4728
- if (distinct.size >= 2) {
4729
- for (const f of fields) f.propName = f.rawName;
4730
- collisions.push(`${prop} \u2190 ${[...distinct].sort().join(", ")}`);
4824
+ const decls = /* @__PURE__ */ new Map();
4825
+ for (const f of fields) (decls.get(`${f.declaringClass}.${f.rawName}`) ?? decls.set(`${f.declaringClass}.${f.rawName}`, []).get(`${f.declaringClass}.${f.rawName}`)).push(f);
4826
+ if (decls.size < 2) continue;
4827
+ const keys = [...decls.keys()].sort();
4828
+ const clsOf = (k) => decls.get(k)[0].declaringClass;
4829
+ const conflicting = /* @__PURE__ */ new Set();
4830
+ for (let i2 = 0; i2 < keys.length; i2++) {
4831
+ for (let j = i2 + 1; j < keys.length; j++) {
4832
+ const a2 = clsOf(keys[i2]), b = clsOf(keys[j]);
4833
+ if (a2 === b || ancestors(a2).has(b) || ancestors(b).has(a2)) {
4834
+ conflicting.add(keys[i2]);
4835
+ conflicting.add(keys[j]);
4836
+ }
4837
+ }
4731
4838
  }
4839
+ if (conflicting.size === 0) continue;
4840
+ const depthOf = (k) => ancestors(clsOf(k)).size;
4841
+ const inConflict = [...conflicting].sort((x, y) => depthOf(x) - depthOf(y) || (x < y ? -1 : 1));
4842
+ const winnerCls = clsOf(inConflict[0]);
4843
+ const soleWinner = inConflict.filter((k) => clsOf(k) === winnerCls).length === 1;
4844
+ const renamed = [];
4845
+ for (const k of inConflict) {
4846
+ if (soleWinner && k === inConflict[0]) continue;
4847
+ for (const f of decls.get(k)) f.propName = f.rawName;
4848
+ renamed.push(k);
4849
+ }
4850
+ collisions.push(`${prop} <- ${renamed.sort().join(", ")}${soleWinner ? ` (kept by ${winnerCls})` : ""}`);
4732
4851
  }
4733
4852
  collisions.sort();
4734
- return { classes, collisions };
4853
+ const referenced = /* @__PURE__ */ new Set();
4854
+ const noteEnum = (f) => {
4855
+ if (f.type.kind === "enum" && f.type.name && enumCatalog[f.type.name]) referenced.add(f.type.name);
4856
+ };
4857
+ for (const cls of inClosure) for (const f of catalog[cls].fields) noteEnum(f);
4858
+ for (const e of embedded) for (const owner of new Set(e.fields.map((f) => f.declaringClass))) {
4859
+ for (const f of catalog[owner]?.fields ?? []) noteEnum(f);
4860
+ }
4861
+ const byTsName = /* @__PURE__ */ new Map();
4862
+ for (const n3 of referenced) {
4863
+ const id = enumTsName(n3);
4864
+ (byTsName.get(id) ?? byTsName.set(id, []).get(id)).push(n3);
4865
+ }
4866
+ const ambiguous = /* @__PURE__ */ new Set();
4867
+ for (const [, group] of byTsName) if (group.length > 1) for (const n3 of group) ambiguous.add(n3);
4868
+ const enums = [...referenced].filter((n3) => !ambiguous.has(n3)).sort().map((className) => {
4869
+ const values = enumCatalog[className].values;
4870
+ const names = enumMemberNames(className, Object.keys(values));
4871
+ return {
4872
+ className,
4873
+ tsName: enumTsName(className),
4874
+ members: Object.keys(values).sort().map((raw) => ({ name: names[raw], raw, value: values[raw] }))
4875
+ };
4876
+ });
4877
+ for (const n3 of [...ambiguous].sort()) collisions.push(`enum ${n3} -> ${enumTsName(n3)} (ambiguous; emitted as number)`);
4878
+ return { classes, embedded, enums, collisions };
4735
4879
  }
4736
4880
 
4737
4881
  // src/schemagen/emit-dts.ts
4738
4882
  var HEADER = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
4739
4883
  function emitDts(model) {
4884
+ const enumTs = new Map(model.enums.map((e) => [e.className, e.tsName]));
4885
+ const tsType = (f) => {
4886
+ const id = f.enumType ? enumTs.get(f.enumType) : void 0;
4887
+ return id ? `${id} | null` : TSTYPE[f.accessorKind];
4888
+ };
4740
4889
  const names = new Set(model.classes.map((c2) => c2.className));
4741
4890
  const out = [HEADER, 'import type { EntityRef } from "@s2script/sdk/entity";'];
4742
4891
  const vecClasses = /* @__PURE__ */ new Set();
@@ -4744,27 +4893,98 @@ function emitDts(model) {
4744
4893
  const vi = VEC_INFO[f.accessorKind];
4745
4894
  if (vi) vecClasses.add(vi.cls);
4746
4895
  }
4896
+ for (const e of model.embedded) for (const f of e.fields) {
4897
+ const vi = VEC_INFO[f.accessorKind];
4898
+ if (vi) vecClasses.add(vi.cls);
4899
+ }
4747
4900
  if (vecClasses.size) out.push(`import type { ${[...vecClasses].sort().join(", ")} } from "@s2script/sdk/math";`);
4748
4901
  out.push("");
4902
+ for (const e of model.enums) {
4903
+ out.push(`export declare const ${e.tsName}: {`);
4904
+ for (const m of e.members) {
4905
+ const doc = m.name === m.raw ? "" : ` /** \`${m.raw}\` */
4906
+ `;
4907
+ out.push(`${doc} readonly ${m.name}: ${m.value};`);
4908
+ }
4909
+ out.push("};");
4910
+ if (e.tsName !== e.className) out.push(`/** Schema enum \`${e.className}\`. */`);
4911
+ out.push(`export type ${e.tsName} = (typeof ${e.tsName})[keyof typeof ${e.tsName}];`, "");
4912
+ }
4913
+ for (const e of model.embedded) {
4914
+ out.push(`export interface ${e.className} {`);
4915
+ for (const f of e.fields) {
4916
+ out.push(` ${f.writable ? "" : "readonly "}${f.propName}: ${tsType(f)};`);
4917
+ }
4918
+ for (const f of e.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
4919
+ out.push("}", "");
4920
+ }
4749
4921
  for (const c2 of model.classes) {
4750
4922
  const ext = c2.parent && names.has(c2.parent) ? ` extends ${c2.parent}` : "";
4751
4923
  out.push(`export interface ${c2.className}${ext} {`);
4752
4924
  for (const f of c2.ownFields) {
4753
4925
  const ro = f.writable ? "" : "readonly ";
4754
- out.push(` ${ro}${f.propName}: ${TSTYPE[f.accessorKind]};`);
4926
+ out.push(` ${ro}${f.propName}: ${tsType(f)};`);
4755
4927
  }
4928
+ for (const f of c2.embeddedFields) out.push(` readonly ${f.propName}: ${f.embeddedClass};`);
4756
4929
  out.push("}", "");
4757
4930
  }
4931
+ out.push("/** Every wrappable entity class, by schema name. */", "export interface SchemaClasses {");
4932
+ for (const c2 of model.classes) out.push(` ${c2.className}: ${c2.className};`);
4933
+ out.push("}", "");
4934
+ out.push(
4935
+ "/**",
4936
+ " * Apply this schema class's field accessors to an entity reference.",
4937
+ " *",
4938
+ " * `createEntity` returns a bare `EntityRef` with no schema fields on it \u2014 this is how you get",
4939
+ " * them. `Player.pawn` is already wrapped; this is for entities you create or resolve yourself.",
4940
+ " *",
4941
+ " * The wrapper is a view, not a copy: it holds the ref and reads through it on every access, so",
4942
+ " * it stays correct as the entity changes and costs nothing to keep. Field reads return null",
4943
+ " * once the ref goes stale, exactly as they do on a pawn.",
4944
+ " *",
4945
+ " * @example",
4946
+ ' * const prop = createEntity("prop_dynamic");',
4947
+ " * prop.spawn();",
4948
+ ' * const fields = wrapEntity("CBaseModelEntity", prop);',
4949
+ " * fields.glow.glowing = true;",
4950
+ " */",
4951
+ "export declare function wrapEntity<K extends keyof SchemaClasses>(className: K, ref: EntityRef): SchemaClasses[K];",
4952
+ ""
4953
+ );
4758
4954
  return out.join("\n");
4759
4955
  }
4760
4956
 
4761
4957
  // src/schemagen/emit-js.ts
4762
4958
  var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
4763
4959
  var S = JSON.stringify;
4960
+ function resolveExpr(f) {
4961
+ const base = `off(${S(f.declaringClass)},${S(f.rawName)})`;
4962
+ return f.addOffset ? `${base} + ${f.addOffset}` : base;
4963
+ }
4964
+ function accessorEntry(f, resolve13, guard2) {
4965
+ const g = guard2 === "" ? "" : `${guard2} `;
4966
+ const gs = guard2 === "" ? "" : `${guard2.replace("return null;", "return;")} `;
4967
+ let entry;
4968
+ if (f.accessorKind === "str") {
4969
+ entry = `get: function () { ${g}return this.ref.readString(${resolve13}, ${f.strLen}); }`;
4970
+ } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
4971
+ entry = `get: function () { ${g}var v = this.ref.${READ[f.accessorKind]}(${resolve13}); return v === null ? null : v.toString(); }`;
4972
+ } else if (VEC_INFO[f.accessorKind]) {
4973
+ const vi = VEC_INFO[f.accessorKind];
4974
+ 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]); }`;
4975
+ } else {
4976
+ entry = `get: function () { ${g}return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
4977
+ }
4978
+ if (f.writable) {
4979
+ const wm = WRITE[f.accessorKind];
4980
+ entry += `, set: function (v) { ${gs}var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
4981
+ }
4982
+ return entry;
4983
+ }
4764
4984
  function emitJs(model) {
4765
- const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};"];
4985
+ const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};", " var E = {};"];
4766
4986
  const vecClasses = /* @__PURE__ */ new Set();
4767
- for (const c2 of model.classes) for (const f of flattenedFields(model, c2.className)) {
4987
+ for (const c2 of model.classes) for (const f of c2.ownFields) {
4768
4988
  const vi = VEC_INFO[f.accessorKind];
4769
4989
  if (vi) vecClasses.add(vi.cls);
4770
4990
  }
@@ -4773,36 +4993,65 @@ function emitJs(model) {
4773
4993
  }
4774
4994
  for (const c2 of model.classes) {
4775
4995
  out.push(` A[${S(c2.className)}] = {`);
4776
- for (const f of flattenedFields(model, c2.className)) {
4777
- const resolve13 = `off(${S(f.declaringClass)},${S(f.rawName)})`;
4778
- let entry;
4779
- if (f.accessorKind === "str") {
4780
- entry = `get: function () { return this.ref.readString(${resolve13}, ${f.strLen}); }`;
4781
- } else if (f.accessorKind === "u64" || f.accessorKind === "i64") {
4782
- entry = `get: function () { var v = this.ref.${READ[f.accessorKind]}(${resolve13}); return v === null ? null : v.toString(); }`;
4783
- } else if (VEC_INFO[f.accessorKind]) {
4784
- const vi = VEC_INFO[f.accessorKind];
4785
- entry = `get: function () { var a = this.ref.readFloats(${resolve13}, ${vi.count}); return a === null ? null : new ${vi.cls}(a[0], a[1], a[2]); }`;
4786
- } else {
4787
- entry = `get: function () { return this.ref.${READ[f.accessorKind]}(${resolve13}); }`;
4788
- }
4789
- if (f.writable) {
4790
- const wm = WRITE[f.accessorKind];
4791
- entry += `, set: function (v) { var o = ${resolve13}; if (this.ref.${wm}(o, v)) this.ref.notifyStateChanged(o); }`;
4792
- }
4996
+ for (const f of c2.ownFields) {
4997
+ const entry = accessorEntry(f, resolveExpr(f), "");
4793
4998
  out.push(` ${S(f.propName)}: { ${entry} },`);
4794
4999
  }
5000
+ for (const f of c2.embeddedFields) {
5001
+ out.push(` ${S(f.propName)}: { get: function () { return embed(${S(f.embeddedClass)}, this.ref, off(${S(f.declaringClass)},${S(f.rawName)})); } },`);
5002
+ }
5003
+ out.push(" };");
5004
+ }
5005
+ for (const e of model.embedded) {
5006
+ out.push(` E[${S(e.className)}] = {`);
5007
+ for (const f of e.fields) {
5008
+ const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
5009
+ const resolve13 = f.addOffset ? `this.base + ${sub} + ${f.addOffset}` : `this.base + ${sub}`;
5010
+ const guard2 = `if (this.base < 0 || ${sub} < 0) return null;`;
5011
+ out.push(` ${S(f.propName)}: { ${accessorEntry(f, resolve13, guard2)} },`);
5012
+ }
5013
+ for (const f of e.embeddedFields) {
5014
+ const sub = `off(${S(f.declaringClass)},${S(f.rawName)})`;
5015
+ 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); } },`);
5016
+ }
4795
5017
  out.push(" };");
4796
5018
  }
5019
+ out.push(" var P = {");
5020
+ for (const c2 of model.classes) if (c2.parent) out.push(` ${S(c2.className)}: ${S(c2.parent)},`);
5021
+ out.push(" };");
5022
+ if (model.enums.length > 0) {
5023
+ out.push(" var E_CONST = {};");
5024
+ for (const e of model.enums) {
5025
+ const body = e.members.map((m) => `${S(m.name)}: ${m.value}`).join(", ");
5026
+ out.push(` E_CONST[${S(e.tsName)}] = Object.freeze({ ${body} });`);
5027
+ }
5028
+ }
4797
5029
  out.push(
5030
+ " // Walk the ancestor chain rather than storing every inherited accessor per class. Flattening",
5031
+ " // into each table duplicated the whole chain 3.77x on the CS2 class set and grew as",
5032
+ " // O(classes * depth), so every class added inflated every other one; this is O(total fields).",
5033
+ " // Ancestors are applied FIRST so a derived class's own definition wins if a name repeats.",
4798
5034
  " function applyAccessors(proto, className) {",
4799
- " var defs = A[className]; if (!defs) return;",
4800
- " for (var name in defs) {",
4801
- " Object.defineProperty(proto, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
5035
+ " var chain = [], c = className;",
5036
+ " for (; c; c = P[c]) chain.unshift(c);",
5037
+ " for (var i = 0; i < chain.length; i++) {",
5038
+ " var defs = A[chain[i]]; if (!defs) continue;",
5039
+ " for (var name in defs) {",
5040
+ " Object.defineProperty(proto, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
5041
+ " }",
4802
5042
  " }",
4803
5043
  " }",
4804
5044
  " function wrap(className, ref) { var o = { ref: ref }; applyAccessors(o, className); return o; }",
5045
+ " function embed(className, ref, base) {",
5046
+ " var o = { ref: ref, base: base }, defs = E[className];",
5047
+ " if (defs) for (var name in defs) {",
5048
+ " Object.defineProperty(o, name, { get: defs[name].get, set: defs[name].set, enumerable: true, configurable: true });",
5049
+ " }",
5050
+ " return o;",
5051
+ " }",
4805
5052
  " globalThis.__s2pkg_cs2_schema = { applyAccessors: applyAccessors, wrap: wrap };",
5053
+ " // Merge (not overwrite): this prelude runs FIRST, and pawn.js merges onto whatever it finds.",
5054
+ " globalThis.__s2pkg_cs2 = Object.assign({}, globalThis.__s2pkg_cs2, { wrapEntity: wrap }" + (model.enums.length > 0 ? ", E_CONST);" : ");"),
4806
5055
  "})();"
4807
5056
  );
4808
5057
  return out.join("\n");
@@ -4810,13 +5059,25 @@ function emitJs(model) {
4810
5059
 
4811
5060
  // src/schemagen/gen.ts
4812
5061
  var CATALOG_PATH = "games/cs2/gamedata/schema-catalog.json";
5062
+ var ENUMS_PATH = "games/cs2/gamedata/schema-enums.json";
4813
5063
  var LIST_PATH = "games/cs2/codegen-classes.json";
5064
+ var EMBED_PATH = "games/cs2/codegen-embedded.json";
4814
5065
  var JS_OUT = "games/cs2/js/schema.generated.js";
4815
5066
  var DTS_OUT = "packages/cs2/schema.generated.d.ts";
4816
5067
  function runGenSchema(repoRoot2, opts) {
4817
5068
  const catalog = JSON.parse(readFileSync13(join13(repoRoot2, CATALOG_PATH), "utf8"));
4818
5069
  const list = JSON.parse(readFileSync13(join13(repoRoot2, LIST_PATH), "utf8"));
4819
- const model = buildModel(catalog, list);
5070
+ let embed = [];
5071
+ try {
5072
+ embed = JSON.parse(readFileSync13(join13(repoRoot2, EMBED_PATH), "utf8"));
5073
+ } catch {
5074
+ }
5075
+ let enumCatalog = {};
5076
+ try {
5077
+ enumCatalog = JSON.parse(readFileSync13(join13(repoRoot2, ENUMS_PATH), "utf8"));
5078
+ } catch {
5079
+ }
5080
+ const model = buildModel(catalog, list, embed, enumCatalog);
4820
5081
  const dts = emitDts(model);
4821
5082
  const js = emitJs(model);
4822
5083
  const fields = model.classes.reduce((n3, c2) => n3 + c2.ownFields.length, 0);
@@ -5004,6 +5265,7 @@ function buildNavModel(config, catalog) {
5004
5265
  source: entry.source,
5005
5266
  target: entry.target,
5006
5267
  path: entry.path,
5268
+ base: entry.base ?? [],
5007
5269
  fields,
5008
5270
  skippedKinds
5009
5271
  };
@@ -5042,10 +5304,12 @@ function emitNavJs(model) {
5042
5304
  out.push(` var ${cls} = __s2require("@s2script/sdk/math").${cls};`);
5043
5305
  }
5044
5306
  for (const w of model.wrappers) {
5045
- out.push(` function ${w.wrapper}(root, path) { this.root = root; this.path = path; }`);
5307
+ out.push(` function ${w.wrapper}(root, path, base) { this.root = root; this.path = path; this.base = base; }`);
5046
5308
  out.push(` Object.defineProperties(${w.wrapper}.prototype, {`);
5047
5309
  for (const f of w.fields) {
5048
- const resolve13 = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
5310
+ const own = `off(${S2(f.declaringClass)},${S2(f.rawName)})`;
5311
+ const withDelta = f.addOffset ? `${own} + ${f.addOffset}` : own;
5312
+ const resolve13 = `${withDelta} + this.base`;
5049
5313
  let getter;
5050
5314
  const kind = f.accessorKind;
5051
5315
  if (kind === "u64") {
@@ -5084,7 +5348,16 @@ function emitNavJs(model) {
5084
5348
  const h2 = w.path[i2];
5085
5349
  out.push(` var o${i2} = off(${S2(h2.cls)},${S2(h2.field)}); if (o${i2} < 0) return null; _p.push(o${i2});`);
5086
5350
  }
5087
- out.push(` return new ${w.wrapper}(this.ref, _p);`);
5351
+ if (w.base.length > 0) {
5352
+ out.push(` var _b = 0;`);
5353
+ for (let i2 = 0; i2 < w.base.length; i2++) {
5354
+ const h2 = w.base[i2];
5355
+ out.push(` var b${i2} = off(${S2(h2.cls)},${S2(h2.field)}); if (b${i2} < 0) return null; _b += b${i2};`);
5356
+ }
5357
+ out.push(` return new ${w.wrapper}(this.ref, _p, _b);`);
5358
+ } else {
5359
+ out.push(` return new ${w.wrapper}(this.ref, _p, 0);`);
5360
+ }
5088
5361
  out.push(` }, enumerable: true, configurable: true,`);
5089
5362
  out.push(` });`);
5090
5363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@s2script/sdk",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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",