@s2script/sdk 0.21.0 → 0.23.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 CHANGED
@@ -3126,8 +3126,8 @@ var sharedCompilerOptionsJson = {
3126
3126
  noEmit: true,
3127
3127
  moduleResolution: "bundler",
3128
3128
  module: "ESNext",
3129
- target: "ES2020",
3130
- lib: ["ES2020"],
3129
+ target: "ES2024",
3130
+ lib: ["ES2024"],
3131
3131
  types: [],
3132
3132
  skipLibCheck: true,
3133
3133
  allowImportingTsExtensions: true
@@ -3139,7 +3139,7 @@ function sharedProgramOptions(tsMod) {
3139
3139
  allowImportingTsExtensions: sharedCompilerOptionsJson.allowImportingTsExtensions,
3140
3140
  moduleResolution: tsMod.ModuleResolutionKind.Bundler,
3141
3141
  module: tsMod.ModuleKind.ESNext,
3142
- target: tsMod.ScriptTarget.ES2020,
3142
+ target: tsMod.ScriptTarget.ES2024,
3143
3143
  lib: sharedCompilerOptionsJson.lib.map((l2) => `lib.${l2.toLowerCase()}.d.ts`),
3144
3144
  types: [...sharedCompilerOptionsJson.types],
3145
3145
  skipLibCheck: sharedCompilerOptionsJson.skipLibCheck
@@ -3766,8 +3766,12 @@ function localDeclarationFiles(pluginDir) {
3766
3766
  return readdirSync2(srcDir).filter((f) => f.endsWith(".d.ts")).map((f) => join7(srcDir, f));
3767
3767
  }
3768
3768
  function generatedDeclarationFiles(pluginDir) {
3769
- const gd = join7(pluginDir, ".s2script", "gamedata.d.ts");
3770
- return existsSync6(gd) ? [gd] : [];
3769
+ const out = [];
3770
+ for (const name of ["gamedata.d.ts", "hooks.d.ts"]) {
3771
+ const p = join7(pluginDir, ".s2script", name);
3772
+ if (existsSync6(p)) out.push(p);
3773
+ }
3774
+ return out;
3771
3775
  }
3772
3776
  function gamePackageDeclarationFiles(pkg, packagesDir) {
3773
3777
  const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
@@ -3791,7 +3795,7 @@ function typecheckPlugin(pluginDir, opts) {
3791
3795
  const entryRel = s2.main ?? pkg.main;
3792
3796
  if (!entryRel) throw new Error(`typecheckPlugin: no entry point in ${join7(absDir, "package.json")}`);
3793
3797
  const entry = resolve6(absDir, entryRel);
3794
- const isAlwaysResolved = (d) => d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
3798
+ const isAlwaysResolved = (d) => d === "@s2script/sdk" || d.startsWith("@s2script/sdk/") || d === "@s2script/cs2" || d.startsWith("@s2script/cs2/");
3795
3799
  const localDts = localDeclarationFiles(absDir);
3796
3800
  const locallyDeclared = declaredModules(localDts);
3797
3801
  const allDeclaredDeps = [
@@ -3825,10 +3829,12 @@ function typecheckPlugin(pluginDir, opts) {
3825
3829
  ...sharedProgramOptions(ts),
3826
3830
  baseUrl: packagesDir,
3827
3831
  paths: {
3828
- // Builtins are `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback
3829
- // now serves only @s2script/cs2 → packages/cs2/index.d.ts (the legacy per-package builtin
3830
- // dirs are deleted). tsc picks the longest matching prefix, so `@s2script/sdk/*` wins for
3831
- // sdk imports.
3832
+ // Builtins: the root barrel `@s2script/sdk` → packages/sdk/index.d.ts; subpaths
3833
+ // `@s2script/sdk/<cap>` → packages/sdk/<cap>.d.ts. The `@s2script/*` fallback serves
3834
+ // @s2script/cs2 → packages/cs2/index.d.ts (and would also hit sdk/index.d.ts; the exact
3835
+ // `@s2script/sdk` entry makes the barrel obvious). tsc picks the longest matching prefix,
3836
+ // so `@s2script/sdk/*` wins for capability imports.
3837
+ "@s2script/sdk": ["sdk/index.d.ts"],
3832
3838
  "@s2script/sdk/*": ["sdk/*.d.ts"],
3833
3839
  // Game-package SUBPATHS (e.g. @s2script/cs2/econ -> packages/cs2/econ.d.ts). Needed because
3834
3840
  // the `@s2script/*` fallback below maps to `<pkg>/index.d.ts`, which cannot express a
@@ -4167,31 +4173,60 @@ function isPluginContext(type) {
4167
4173
  const sym = type.getSymbol() ?? type.aliasSymbol;
4168
4174
  return sym?.getName() === "PluginContext";
4169
4175
  }
4176
+ function isFromPluginDts(checker, node) {
4177
+ let sym = checker.getSymbolAtLocation(node);
4178
+ if (sym === void 0) return false;
4179
+ if (sym.flags & ts2.SymbolFlags.Alias) {
4180
+ const aliased = checker.getAliasedSymbol(sym);
4181
+ if (aliased !== void 0) sym = aliased;
4182
+ }
4183
+ for (const d of sym.declarations ?? []) {
4184
+ const f = d.getSourceFile().fileName.replace(/\\/g, "/");
4185
+ if (f.endsWith("/plugin.d.ts")) return true;
4186
+ }
4187
+ return false;
4188
+ }
4189
+ function recordPublishOrUse(out, method, arg0, sf, node) {
4190
+ if (method === "publish") {
4191
+ if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4192
+ out.publishNames.push(arg0.text);
4193
+ } else {
4194
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
4195
+ out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
4196
+ }
4197
+ } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4198
+ out.useNames.push(arg0.text);
4199
+ }
4200
+ }
4201
+ function recordImportSpecifier(out, spec) {
4202
+ if (spec.startsWith(".") || spec.startsWith("@s2script/")) return;
4203
+ out.importNames.push(spec);
4204
+ }
4170
4205
  function scanPluginProgram(program, pluginDir) {
4171
4206
  const checker = program.getTypeChecker();
4172
- const out = { publishNames: [], dynamicPublishSites: [], useNames: [] };
4207
+ const out = { publishNames: [], dynamicPublishSites: [], useNames: [], importNames: [] };
4173
4208
  const dirPrefix = pluginDir.replace(/\\/g, "/").replace(/\/+$/, "") + "/";
4174
4209
  for (const sf of program.getSourceFiles()) {
4175
4210
  if (sf.isDeclarationFile) continue;
4176
4211
  if (!sf.fileName.replace(/\\/g, "/").startsWith(dirPrefix)) continue;
4177
4212
  const visit = (node) => {
4178
- if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression)) {
4179
- const method = node.expression.name.text;
4180
- if (method === "publish" || method === "use" || method === "tryUse") {
4181
- const recv = checker.getTypeAtLocation(node.expression.expression);
4182
- if (isPluginContext(recv)) {
4183
- const arg0 = node.arguments[0];
4184
- if (method === "publish") {
4185
- if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4186
- out.publishNames.push(arg0.text);
4187
- } else {
4188
- const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
4189
- out.dynamicPublishSites.push(`${sf.fileName}:${line + 1}`);
4190
- }
4191
- } else if (arg0 !== void 0 && ts2.isStringLiteralLike(arg0)) {
4192
- out.useNames.push(arg0.text);
4213
+ if ((ts2.isImportDeclaration(node) || ts2.isExportDeclaration(node)) && node.moduleSpecifier !== void 0 && ts2.isStringLiteralLike(node.moduleSpecifier)) {
4214
+ recordImportSpecifier(out, node.moduleSpecifier.text);
4215
+ }
4216
+ if (ts2.isCallExpression(node)) {
4217
+ if (ts2.isPropertyAccessExpression(node.expression)) {
4218
+ const method = node.expression.name.text;
4219
+ if (method === "publish" || method === "use" || method === "tryUse") {
4220
+ const recv = checker.getTypeAtLocation(node.expression.expression);
4221
+ if (isPluginContext(recv)) {
4222
+ recordPublishOrUse(out, method, node.arguments[0], sf, node);
4193
4223
  }
4194
4224
  }
4225
+ } else if (ts2.isIdentifier(node.expression)) {
4226
+ const name = node.expression.text;
4227
+ if ((name === "publish" || name === "use" || name === "tryUse") && isFromPluginDts(checker, node.expression)) {
4228
+ recordPublishOrUse(out, name, node.arguments[0], sf, node);
4229
+ }
4195
4230
  }
4196
4231
  }
4197
4232
  ts2.forEachChild(node, visit);
@@ -4200,30 +4235,37 @@ function scanPluginProgram(program, pluginDir) {
4200
4235
  }
4201
4236
  out.publishNames = [...new Set(out.publishNames)];
4202
4237
  out.useNames = [...new Set(out.useNames)];
4238
+ out.importNames = [...new Set(out.importNames)];
4203
4239
  return out;
4204
4240
  }
4205
4241
 
4206
4242
  // src/gamedata/types.ts
4207
4243
  var PLATFORM = "linuxsteamrt64";
4208
- var ARG_KINDS = ["bool", "int", "float", "string", "vector", "entity"];
4244
+ var ARG_KINDS = ["bool", "int", "float", "string", "utlstring", "vector", "entity"];
4209
4245
  var RET_KINDS = ["void", "bool", "int", "float", "entity"];
4210
- var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "vector", "entity"]);
4246
+ var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "utlstring", "vector", "entity"]);
4211
4247
  var MAX_INT_ARGS = 9;
4212
4248
  var MAX_FLOAT_ARGS = 8;
4249
+ var HOOK_SHAPES = ["this_void", "this_f32_i32_i32_i32", "this_f32_i32_i64_i64", "this_i64_i32_i64", "this_i64_i64_i64"];
4250
+ function hookShapeArity(shape) {
4251
+ if (!HOOK_SHAPES.includes(shape)) return void 0;
4252
+ if (shape === "this_void") return 0;
4253
+ return shape.slice("this_".length).split("_").length;
4254
+ }
4213
4255
 
4214
4256
  // src/gamedata/validate.ts
4215
- var ALLOWED_SECTIONS = /* @__PURE__ */ new Set(["signatures", "calls"]);
4257
+ var ALLOWED_SECTIONS = /* @__PURE__ */ new Set(["signatures", "calls", "hooks"]);
4216
4258
  var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4217
4259
  var RESERVED_CALL_NAMES = /* @__PURE__ */ new Set(["constructor", "prototype", "__proto__"]);
4218
4260
  var RESOLVE_KINDS = ["direct", "ctor-body-xref", "lea-disp"];
4219
- var KNOWN_PERMISSIONS = ["engine:calls"];
4261
+ var KNOWN_PERMISSIONS = ["engine:calls", "engine:hooks"];
4220
4262
  function validatePluginGamedata(gd, opts) {
4221
4263
  const errs = [];
4222
4264
  if (gd == null || typeof gd !== "object" || Array.isArray(gd)) return ["gamedata must be an object"];
4223
4265
  const g = gd;
4224
4266
  for (const key of Object.keys(g)) {
4225
4267
  if (!ALLOWED_SECTIONS.has(key)) {
4226
- errs.push(`gamedata section '${key}' is not supported in v1 (allowed: signatures, calls)`);
4268
+ errs.push(`gamedata section '${key}' is not supported in v1 (allowed: signatures, calls, hooks)`);
4227
4269
  }
4228
4270
  }
4229
4271
  const sigs = g.signatures ?? {};
@@ -4358,8 +4400,249 @@ function validatePluginGamedata(gd, opts) {
4358
4400
  errs.push(`${where2}: 'returns' must be one of ${RET_KINDS.join(", ")}`);
4359
4401
  }
4360
4402
  }
4403
+ validateHooks(g, sigs, callNames, opts.permissions, errs);
4361
4404
  return errs;
4362
4405
  }
4406
+ function isNonEmptyObject(v) {
4407
+ return v != null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0;
4408
+ }
4409
+ function validateHooks(g, sigs, callNames, permissions, errs) {
4410
+ if (g.hooks === void 0) return;
4411
+ if (g.hooks == null || typeof g.hooks !== "object" || Array.isArray(g.hooks)) {
4412
+ errs.push("hooks must be an object");
4413
+ return;
4414
+ }
4415
+ const hooks = g.hooks;
4416
+ const hookNames = Object.keys(hooks);
4417
+ if (hookNames.length && !permissions.includes("engine:hooks")) {
4418
+ errs.push('gamedata declares a `hooks` section but the manifest does not declare permission "engine:hooks"');
4419
+ }
4420
+ for (const [name, rawDecl] of Object.entries(hooks)) {
4421
+ const where2 = `hook '${name}'`;
4422
+ if (!IDENTIFIER.test(name)) {
4423
+ errs.push(
4424
+ `${where2}: hook name must be a plain identifier matching ${IDENTIFIER.source} \u2014 it is emitted verbatim as a TypeScript interface member in the generated .s2script/hooks.d.ts`
4425
+ );
4426
+ continue;
4427
+ }
4428
+ if (RESERVED_CALL_NAMES.has(name)) {
4429
+ errs.push(`${where2}: hook name '${name}' is reserved`);
4430
+ continue;
4431
+ }
4432
+ if (rawDecl == null || typeof rawDecl !== "object" || Array.isArray(rawDecl)) {
4433
+ errs.push(`${where2}: must be an object`);
4434
+ continue;
4435
+ }
4436
+ const decl = rawDecl;
4437
+ const ctx = decl.expose?.ctx;
4438
+ if (typeof ctx !== "string" || !ctx.length) {
4439
+ errs.push(`${where2}: missing 'expose.ctx' \u2014 nothing could subscribe to this hook`);
4440
+ } else if (!IDENTIFIER.test(ctx)) {
4441
+ errs.push(`${where2}: 'expose.ctx' must be a plain identifier, got ${JSON.stringify(ctx)}`);
4442
+ }
4443
+ const shape = decl.shape;
4444
+ if (typeof shape !== "string" || !HOOK_SHAPES.includes(shape)) {
4445
+ errs.push(
4446
+ `${where2}: unknown hook shape ${JSON.stringify(shape)} (this build compiles thunks for: ${HOOK_SHAPES.join(", ")})`
4447
+ );
4448
+ }
4449
+ const params = decl.params ?? [];
4450
+ if (!Array.isArray(params)) {
4451
+ errs.push(`${where2}: 'params' must be an array`);
4452
+ } else {
4453
+ const seen = /* @__PURE__ */ new Set();
4454
+ for (let i2 = 0; i2 < params.length; i2++) {
4455
+ const p = params[i2];
4456
+ if (typeof p !== "string" || !IDENTIFIER.test(p)) {
4457
+ errs.push(`${where2}: params[${i2}] = ${JSON.stringify(p)} must be a plain identifier`);
4458
+ continue;
4459
+ }
4460
+ if (seen.has(p)) errs.push(`${where2}: param name ${JSON.stringify(p)} is declared more than once`);
4461
+ seen.add(p);
4462
+ }
4463
+ const arity = typeof shape === "string" ? hookShapeArity(shape) : void 0;
4464
+ if (arity !== void 0 && params.length > arity) {
4465
+ errs.push(
4466
+ `${where2}: declares ${params.length} 'params' but shape ${JSON.stringify(shape)} passes only ${arity}`
4467
+ );
4468
+ }
4469
+ if (decl.mutable !== void 0) {
4470
+ const mutable = decl.mutable;
4471
+ if (!Array.isArray(mutable)) {
4472
+ errs.push(`${where2}: 'mutable' must be an array`);
4473
+ } else {
4474
+ for (const m of mutable) {
4475
+ if (typeof m !== "string" || !params.includes(m)) {
4476
+ errs.push(`${where2}: 'mutable' names ${JSON.stringify(m)}, which is not one of this hook's params`);
4477
+ }
4478
+ }
4479
+ }
4480
+ }
4481
+ }
4482
+ const recv = decl.receiver;
4483
+ if (recv !== void 0) {
4484
+ if (recv == null || typeof recv !== "object") {
4485
+ errs.push(`${where2}: 'receiver' must be an object`);
4486
+ } else {
4487
+ const rkind = recv.kind ?? "none";
4488
+ if (rkind !== "none" && rkind !== "entity") {
4489
+ errs.push(`${where2}: receiver.kind must be "none" or "entity"`);
4490
+ } else if (rkind === "entity") {
4491
+ if (typeof recv.as !== "string" || !IDENTIFIER.test(recv.as)) {
4492
+ errs.push(`${where2}: receiver.kind "entity" needs a plain-identifier 'as' name`);
4493
+ } else if (Array.isArray(params) && params.includes(recv.as)) {
4494
+ errs.push(`${where2}: receiver 'as' name ${JSON.stringify(recv.as)} collides with a param name`);
4495
+ }
4496
+ }
4497
+ }
4498
+ }
4499
+ if (decl.bypassWith !== void 0) {
4500
+ if (typeof decl.bypassWith !== "string" || !decl.bypassWith.length) {
4501
+ errs.push(`${where2}: 'bypassWith' must be a non-empty call name`);
4502
+ } else if (!callNames.includes(decl.bypassWith)) {
4503
+ errs.push(
4504
+ `${where2}: 'bypassWith' names ${JSON.stringify(decl.bypassWith)}, which is not a 'calls' descriptor in this owner's gamedata`
4505
+ );
4506
+ }
4507
+ }
4508
+ const target = decl.target;
4509
+ if (!target) {
4510
+ errs.push(`${where2}: missing 'target'`);
4511
+ continue;
4512
+ }
4513
+ if (target.kind === "signature") {
4514
+ if (typeof target.name !== "string") {
4515
+ errs.push(`${where2}: target.name must be a string`);
4516
+ } else if (!(target.name in sigs)) {
4517
+ errs.push(`${where2}: target.name '${target.name}' has no entry in 'signatures'`);
4518
+ }
4519
+ const sigPlat = typeof target.name === "string" ? sigs[target.name]?.[PLATFORM] : void 0;
4520
+ const inherited = sigPlat?.validate;
4521
+ if (!isNonEmptyObject(target.validate) && !isNonEmptyObject(inherited)) {
4522
+ errs.push(
4523
+ `${where2}: a hook target MUST carry a non-empty 'validate' (inline, or inherited from its named signature) \u2014 a wrong detour address overwrites the prologue of whatever is actually there`
4524
+ );
4525
+ }
4526
+ } else if (target.kind === "vtable") {
4527
+ if (typeof target.class !== "string") errs.push(`${where2}: vtable target requires 'class'`);
4528
+ const plat = target[PLATFORM];
4529
+ if (!plat) {
4530
+ errs.push(`${where2}: vtable target missing platform '${PLATFORM}'`);
4531
+ } else {
4532
+ if (!Number.isInteger(plat.index) || plat.index < 0) {
4533
+ errs.push(`${where2}: vtable index must be a non-negative integer`);
4534
+ }
4535
+ const prologue = plat.validate?.prologue;
4536
+ if (typeof prologue !== "string" || !prologue.length) {
4537
+ errs.push(`${where2}: a vtable target REQUIRES validate.prologue (a bare borrowed index is never trusted)`);
4538
+ }
4539
+ }
4540
+ } else {
4541
+ errs.push(`${where2}: target.kind must be "signature" or "vtable"`);
4542
+ }
4543
+ }
4544
+ }
4545
+
4546
+ // src/hookgen/model.ts
4547
+ function capitalize(name) {
4548
+ return name.length === 0 ? name : name.charAt(0).toUpperCase() + name.slice(1);
4549
+ }
4550
+ function hookDescriptor(name, decl) {
4551
+ const paramNames = decl.params ?? [];
4552
+ const mutableSet = new Set(decl.mutable ?? []);
4553
+ const params = paramNames.map((p) => ({ name: p, mutable: mutableSet.has(p) }));
4554
+ const receiverAs = decl.receiver?.kind === "entity" ? decl.receiver.as ?? null : null;
4555
+ return { name, shape: decl.shape ?? "", viewIface: `${capitalize(name)}View`, params, receiverAs };
4556
+ }
4557
+ function buildPluginHookList(hooks) {
4558
+ return Object.keys(hooks).sort().map((name) => hookDescriptor(name, hooks[name] ?? {}));
4559
+ }
4560
+ function buildHookModel(hooks) {
4561
+ const byNs = /* @__PURE__ */ new Map();
4562
+ for (const name of Object.keys(hooks).sort()) {
4563
+ const decl = hooks[name] ?? {};
4564
+ const ns = decl.expose?.ctx;
4565
+ if (!ns) continue;
4566
+ if (decl.expose?.handwritten) continue;
4567
+ const list = byNs.get(ns) ?? [];
4568
+ list.push(hookDescriptor(name, decl));
4569
+ byNs.set(ns, list);
4570
+ }
4571
+ return Array.from(byNs.keys()).sort().map((ns) => ({ ns, ctxIface: `Ctx${capitalize(ns)}`, hooks: byNs.get(ns) }));
4572
+ }
4573
+
4574
+ // src/hookgen/emit-dts.ts
4575
+ var HEADER = "// GENERATED by `s2script gen-hooks` from gamedata/cs2/game.cs2.jsonc's `hooks` \u2014 DO NOT EDIT.";
4576
+ var STRING_PARAMS = {
4577
+ this_i64_i64_i64: /* @__PURE__ */ new Set([0])
4578
+ };
4579
+ function emitViewIface(h2) {
4580
+ const out = [`export interface ${h2.viewIface} {`];
4581
+ const strings = STRING_PARAMS[h2.shape] ?? /* @__PURE__ */ new Set();
4582
+ h2.params.forEach((p, i2) => {
4583
+ const mod = p.mutable ? "" : "readonly ";
4584
+ out.push(` ${mod}${p.name}: ${strings.has(i2) ? "string" : "number"};`);
4585
+ });
4586
+ if (h2.receiverAs !== null) {
4587
+ out.push(` readonly ${h2.receiverAs}: EntityRef | null;`);
4588
+ }
4589
+ out.push("}", "");
4590
+ return out;
4591
+ }
4592
+ function emitHookDts(model) {
4593
+ const out = [
4594
+ HEADER,
4595
+ 'import type { HookResultValue } from "@s2script/sdk/events";',
4596
+ 'import type { EntityRef } from "@s2script/sdk/entity";',
4597
+ ""
4598
+ ];
4599
+ for (const { hooks } of model) {
4600
+ for (const h2 of hooks) {
4601
+ out.push(...emitViewIface(h2));
4602
+ }
4603
+ }
4604
+ for (const { ctxIface, hooks, ns } of model) {
4605
+ out.push(`export interface ${ctxIface} {`);
4606
+ for (const h2 of hooks) {
4607
+ out.push(` ${h2.name}(handler: (view: ${h2.viewIface}) => HookResultValue | void): void;`);
4608
+ }
4609
+ out.push("}", "");
4610
+ out.push(`export declare const ${ns}: ${ctxIface};`);
4611
+ out.push("");
4612
+ }
4613
+ out.push('declare module "@s2script/sdk/plugin" {');
4614
+ out.push(" interface PluginContext {");
4615
+ for (const { ns, ctxIface } of model) {
4616
+ out.push(` readonly ${ns}: ${ctxIface};`);
4617
+ }
4618
+ out.push(" }");
4619
+ out.push("}");
4620
+ return out.join("\n") + "\n";
4621
+ }
4622
+ var PLUGIN_HEADER = "// GENERATED by `s2s build` from the plugin's gamedata \u2014 DO NOT EDIT.";
4623
+ function emitPluginHookDts(hooks) {
4624
+ const list = buildPluginHookList(hooks);
4625
+ const out = [
4626
+ PLUGIN_HEADER,
4627
+ "// The EntityRef / HookResultValue imports are REQUIRED: a `declare module` augmentation",
4628
+ "// resolves type names in its own file scope.",
4629
+ 'import type { HookResultValue } from "@s2script/sdk/events";',
4630
+ 'import type { EntityRef } from "@s2script/sdk/entity";',
4631
+ ""
4632
+ ];
4633
+ for (const h2 of list) {
4634
+ out.push(...emitViewIface(h2));
4635
+ }
4636
+ out.push('declare module "@s2script/sdk/unsafe" {');
4637
+ out.push(" interface EngineHooks {");
4638
+ for (const h2 of list) {
4639
+ out.push(` ${h2.name}: (handler: (view: ${h2.viewIface}) => HookResultValue | void) => void;`);
4640
+ }
4641
+ out.push(" }");
4642
+ out.push("}");
4643
+ out.push("");
4644
+ return out.join("\n");
4645
+ }
4363
4646
 
4364
4647
  // src/gamedata/gen-types.ts
4365
4648
  var VECTOR_TS = "{ readonly x: number; readonly y: number; readonly z: number }";
@@ -4368,6 +4651,7 @@ var ARG_TS = {
4368
4651
  int: "number",
4369
4652
  float: "number",
4370
4653
  string: "string",
4654
+ utlstring: "string",
4371
4655
  vector: VECTOR_TS,
4372
4656
  entity: "EntityRef | null"
4373
4657
  };
@@ -4405,6 +4689,9 @@ ${lines.join("\n")}
4405
4689
  ""
4406
4690
  ].join("\n");
4407
4691
  }
4692
+ function generateHookTypes(gd) {
4693
+ return emitPluginHookDts(gd.hooks ?? {});
4694
+ }
4408
4695
 
4409
4696
  // src/gamedata/jsonc.ts
4410
4697
  function stripJsonComments(src) {
@@ -4514,8 +4801,14 @@ async function buildPlugin(dir, packagesDir) {
4514
4801
  const genDir = join11(absDir, ".s2script");
4515
4802
  mkdirSync2(genDir, { recursive: true });
4516
4803
  writeFileSync3(join11(genDir, "gamedata.d.ts"), generateGamedataTypes(gamedata));
4804
+ if (gamedata.hooks && Object.keys(gamedata.hooks).length) {
4805
+ writeFileSync3(join11(genDir, "hooks.d.ts"), generateHookTypes(gamedata));
4806
+ } else {
4807
+ rmSync2(join11(genDir, "hooks.d.ts"), { force: true });
4808
+ }
4517
4809
  } else {
4518
4810
  rmSync2(join11(absDir, ".s2script", "gamedata.d.ts"), { force: true });
4811
+ rmSync2(join11(absDir, ".s2script", "hooks.d.ts"), { force: true });
4519
4812
  }
4520
4813
  const declaredDepNames = [
4521
4814
  ...Object.keys(s2.pluginDependencies ?? {}),
@@ -4578,13 +4871,16 @@ ${lint.output}`);
4578
4871
  for (const used of scan.useNames) {
4579
4872
  if (!declaredDeps.has(used)) {
4580
4873
  console.warn(
4581
- `WARN: ctx.use/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
4874
+ `WARN: use()/tryUse(${JSON.stringify(used)}) is not declared under s2script.pluginDependencies/optionalPluginDependencies \u2014 it will throw at runtime`
4582
4875
  );
4583
4876
  }
4584
4877
  }
4878
+ const referenced = /* @__PURE__ */ new Set([...scan.useNames, ...scan.importNames]);
4585
4879
  for (const dep of declaredDeps) {
4586
- if (!scan.useNames.includes(dep)) {
4587
- console.warn(`WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d`);
4880
+ if (!referenced.has(dep)) {
4881
+ console.warn(
4882
+ `WARN: dependency ${JSON.stringify(dep)} is declared but never ctx.use()d or imported`
4883
+ );
4588
4884
  }
4589
4885
  }
4590
4886
  const { name, version } = pkg;
@@ -4612,7 +4908,7 @@ ${lint.output}`);
4612
4908
  platform: "neutral",
4613
4909
  format: "cjs",
4614
4910
  external,
4615
- target: "es2020",
4911
+ target: "es2024",
4616
4912
  // platform:"neutral" defaults mainFields to EMPTY, so a workspace-sibling package that
4617
4913
  // declares `main` (rather than `exports`) fails to resolve with an undiagnosable error.
4618
4914
  // Set it explicitly so a monorepo plugin bundles whichever field the author wrote.
@@ -7812,29 +8108,33 @@ function fileDevDeps(packagesDir, game) {
7812
8108
  }
7813
8109
  function pluginSource(game) {
7814
8110
  if (game === "cs2") {
7815
- return `import { plugin } from "@s2script/sdk/plugin";
7816
- import { Chat } from "@s2script/sdk/chat";
8111
+ return `import { command, Chat, HookResult } from "@s2script/sdk";
8112
+ import type { Command, HookResultValue } from "@s2script/sdk";
7817
8113
 
7818
- export default plugin((ctx) => {
7819
- ctx.commands.register("hello", (cmd) => {
7820
- cmd.reply("hello from s2script");
7821
- if (cmd.callerSlot >= 0) {
7822
- Chat.toSlot(cmd.callerSlot, "hello from s2script");
7823
- }
7824
- });
7825
- });
8114
+ export function OnPluginStart(): void {
8115
+ command("hello", hello);
8116
+ }
8117
+
8118
+ function hello(cmd: Command): HookResultValue {
8119
+ cmd.reply("hello from s2script");
8120
+ if (cmd.callerSlot >= 0) {
8121
+ Chat.toSlot(cmd.callerSlot, "hello from s2script");
8122
+ }
8123
+ return HookResult.Handled;
8124
+ }
7826
8125
  `;
7827
8126
  }
7828
- return `import { plugin } from "@s2script/sdk/plugin";
7829
- import { delay } from "@s2script/sdk/timers";
8127
+ return `import { delay } from "@s2script/sdk";
7830
8128
 
7831
- export default plugin((ctx) => {
7832
- let n = 0;
7833
- ctx.server.onGameFrame(() => {
7834
- n += 1;
7835
- });
8129
+ let n = 0;
8130
+
8131
+ export function OnPluginStart(): void {
7836
8132
  void delay(1000).then(() => console.log("s2script plugin alive; frames so far:", n));
7837
- });
8133
+ }
8134
+
8135
+ export function OnGameFrame(): void {
8136
+ n += 1;
8137
+ }
7838
8138
  `;
7839
8139
  }
7840
8140
  var LIBRARY_ENTRY = `/** Encode a string. Replace this with the library's real surface. */
@@ -8837,7 +9137,7 @@ function buildModel(catalog, requested, embeddable = [], enumCatalog = {}) {
8837
9137
  }
8838
9138
 
8839
9139
  // src/schemagen/emit-dts.ts
8840
- var HEADER = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
9140
+ var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
8841
9141
  function emitDts(model) {
8842
9142
  const enumTs = new Map(model.enums.map((e) => [e.className, e.tsName]));
8843
9143
  const tsType = (f) => {
@@ -8845,7 +9145,7 @@ function emitDts(model) {
8845
9145
  return id ? `${id} | null` : TSTYPE[f.accessorKind];
8846
9146
  };
8847
9147
  const names = new Set(model.classes.map((c2) => c2.className));
8848
- const out = [HEADER, 'import type { EntityRef } from "@s2script/sdk/entity";'];
9148
+ const out = [HEADER2, 'import type { EntityRef } from "@s2script/sdk/entity";'];
8849
9149
  const vecClasses = /* @__PURE__ */ new Set();
8850
9150
  for (const c2 of model.classes) for (const f of c2.ownFields) {
8851
9151
  const vi = VEC_INFO[f.accessorKind];
@@ -8913,7 +9213,7 @@ function emitDts(model) {
8913
9213
  }
8914
9214
 
8915
9215
  // src/schemagen/emit-js.ts
8916
- var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
9216
+ var HEADER3 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
8917
9217
  var S = JSON.stringify;
8918
9218
  function resolveExpr(f) {
8919
9219
  const base = `off(${S(f.declaringClass)},${S(f.rawName)})`;
@@ -8940,7 +9240,7 @@ function accessorEntry(f, resolve22, guard2) {
8940
9240
  return entry;
8941
9241
  }
8942
9242
  function emitJs(model) {
8943
- const out = [HEADER2, "(function () {", " var off = __s2_schema_offset;", " var A = {};", " var E = {};"];
9243
+ const out = [HEADER3, "(function () {", " var off = __s2_schema_offset;", " var A = {};", " var E = {};"];
8944
9244
  const vecClasses = /* @__PURE__ */ new Set();
8945
9245
  for (const c2 of model.classes) for (const f of c2.ownFields) {
8946
9246
  const vi = VEC_INFO[f.accessorKind];
@@ -9102,7 +9402,7 @@ function buildEventModel(catalog) {
9102
9402
  }
9103
9403
 
9104
9404
  // src/eventgen/emit-dts.ts
9105
- var HEADER3 = "// GENERATED by `s2script gen-events` from event-catalog.json \u2014 DO NOT EDIT.";
9405
+ var HEADER4 = "// GENERATED by `s2script gen-events` from event-catalog.json \u2014 DO NOT EDIT.";
9106
9406
  var GETTER_RET = {
9107
9407
  getBool: "boolean",
9108
9408
  getFloat: "number",
@@ -9113,7 +9413,7 @@ var GETTER_RET = {
9113
9413
  };
9114
9414
  function emitEventDts(model) {
9115
9415
  const out = [
9116
- HEADER3,
9416
+ HEADER4,
9117
9417
  'import type { GameEvent, HookResultValue } from "@s2script/sdk/events";',
9118
9418
  ""
9119
9419
  ];
@@ -9232,7 +9532,7 @@ function buildNavModel(config, catalog) {
9232
9532
  }
9233
9533
 
9234
9534
  // src/navgen/emit-js.ts
9235
- var HEADER4 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
9535
+ var HEADER5 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
9236
9536
  var S2 = JSON.stringify;
9237
9537
  var READ_VIA = {
9238
9538
  f32: "readFloat32Via",
@@ -9250,7 +9550,7 @@ var WRITE_VIA = {
9250
9550
  i32: "writeInt32Via"
9251
9551
  };
9252
9552
  function emitNavJs(model) {
9253
- const out = [HEADER4, "(function () {", " var off = __s2_schema_offset;"];
9553
+ const out = [HEADER5, "(function () {", " var off = __s2_schema_offset;"];
9254
9554
  const vecClasses = /* @__PURE__ */ new Set();
9255
9555
  for (const w of model.wrappers) {
9256
9556
  for (const f of w.fields) {
@@ -9332,7 +9632,7 @@ function emitNavJs(model) {
9332
9632
  }
9333
9633
 
9334
9634
  // src/navgen/emit-dts.ts
9335
- var HEADER5 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
9635
+ var HEADER6 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
9336
9636
  function emitNavDts(model) {
9337
9637
  let needsEntityRef = false;
9338
9638
  let needsVector = false;
@@ -9345,7 +9645,7 @@ function emitNavDts(model) {
9345
9645
  if (vi?.cls === "QAngle") needsQAngle = true;
9346
9646
  }
9347
9647
  }
9348
- const out = [HEADER5];
9648
+ const out = [HEADER6];
9349
9649
  const mathTypes = [];
9350
9650
  if (needsVector) mathTypes.push("Vector");
9351
9651
  if (needsQAngle) mathTypes.push("QAngle");
@@ -9416,72 +9716,6 @@ function runGenNav(repoRoot2, opts) {
9416
9716
  // src/hookgen/gen.ts
9417
9717
  import { readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "node:fs";
9418
9718
  import { join as join27 } from "node:path";
9419
-
9420
- // src/hookgen/model.ts
9421
- function capitalize(name) {
9422
- return name.length === 0 ? name : name.charAt(0).toUpperCase() + name.slice(1);
9423
- }
9424
- function buildHookModel(hooks) {
9425
- const byNs = /* @__PURE__ */ new Map();
9426
- for (const name of Object.keys(hooks).sort()) {
9427
- const decl = hooks[name] ?? {};
9428
- const ns = decl.expose?.ctx;
9429
- if (!ns) continue;
9430
- const paramNames = decl.params ?? [];
9431
- const mutableSet = new Set(decl.mutable ?? []);
9432
- const params = paramNames.map((p) => ({ name: p, mutable: mutableSet.has(p) }));
9433
- const receiverAs = decl.receiver?.kind === "entity" ? decl.receiver.as ?? null : null;
9434
- const list = byNs.get(ns) ?? [];
9435
- list.push({ name, viewIface: `${capitalize(name)}View`, params, receiverAs });
9436
- byNs.set(ns, list);
9437
- }
9438
- return Array.from(byNs.keys()).sort().map((ns) => ({ ns, ctxIface: `Ctx${capitalize(ns)}`, hooks: byNs.get(ns) }));
9439
- }
9440
-
9441
- // src/hookgen/emit-dts.ts
9442
- var HEADER6 = "// GENERATED by `s2script gen-hooks` from gamedata/cs2/game.cs2.jsonc's `hooks` \u2014 DO NOT EDIT.";
9443
- function emitViewIface(h2) {
9444
- const out = [`export interface ${h2.viewIface} {`];
9445
- for (const p of h2.params) {
9446
- const mod = p.mutable ? "" : "readonly ";
9447
- out.push(` ${mod}${p.name}: number;`);
9448
- }
9449
- if (h2.receiverAs !== null) {
9450
- out.push(` readonly ${h2.receiverAs}: EntityRef | null;`);
9451
- }
9452
- out.push("}", "");
9453
- return out;
9454
- }
9455
- function emitHookDts(model) {
9456
- const out = [
9457
- HEADER6,
9458
- 'import type { HookResultValue } from "@s2script/sdk/events";',
9459
- 'import type { EntityRef } from "@s2script/sdk/entity";',
9460
- ""
9461
- ];
9462
- for (const { hooks } of model) {
9463
- for (const h2 of hooks) {
9464
- out.push(...emitViewIface(h2));
9465
- }
9466
- }
9467
- for (const { ctxIface, hooks } of model) {
9468
- out.push(`export interface ${ctxIface} {`);
9469
- for (const h2 of hooks) {
9470
- out.push(` ${h2.name}(handler: (view: ${h2.viewIface}) => HookResultValue | void): void;`);
9471
- }
9472
- out.push("}", "");
9473
- }
9474
- out.push('declare module "@s2script/sdk/plugin" {');
9475
- out.push(" interface PluginContext {");
9476
- for (const { ns, ctxIface } of model) {
9477
- out.push(` readonly ${ns}: ${ctxIface};`);
9478
- }
9479
- out.push(" }");
9480
- out.push("}");
9481
- return out.join("\n") + "\n";
9482
- }
9483
-
9484
- // src/hookgen/gen.ts
9485
9719
  var GAMEDATA_PATH = "gamedata/cs2/game.cs2.jsonc";
9486
9720
  var DTS_OUT4 = "packages/cs2/hooks.generated.d.ts";
9487
9721
  function runGenHooks(repoRoot2, opts) {