@s2script/sdk 0.21.0 → 0.22.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/clients.d.ts +2 -7
- package/dist/cli.js +276 -83
- package/events.d.ts +4 -3
- package/package.json +1 -1
- package/server.d.ts +5 -5
- package/unsafe.d.ts +28 -8
package/clients.d.ts
CHANGED
|
@@ -60,13 +60,8 @@ export declare class Client {
|
|
|
60
60
|
* or a name that is not a registered console **command** (a ConVar such as `mp_friendlyfire` is
|
|
61
61
|
* refused: use `Server.command` for those).
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* the isolate borrow across all JS, so re-entering the dispatcher hits the documented
|
|
66
|
-
* re-entrancy skip. (This is not a permissions issue: every s2script command is registered
|
|
67
|
-
* client-executable, which is why a player can type it in their own console.) To invoke another
|
|
68
|
-
* plugin's behaviour, use a cross-plugin interface rather than faking its command. See
|
|
69
|
-
* docs/superpowers/specs/2026-07-25-client-command-design.md §12.
|
|
63
|
+
* Engine commands (`say`, `kill`, …) execute. A command registered by an s2script plugin runs
|
|
64
|
+
* that plugin's JS handler before this returns.
|
|
70
65
|
*
|
|
71
66
|
* @example
|
|
72
67
|
* client.fakeCommand("say hello"); // as though the player typed it
|
package/dist/cli.js
CHANGED
|
@@ -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
|
|
3770
|
-
|
|
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 ?? {} };
|
|
@@ -4205,25 +4209,31 @@ function scanPluginProgram(program, pluginDir) {
|
|
|
4205
4209
|
|
|
4206
4210
|
// src/gamedata/types.ts
|
|
4207
4211
|
var PLATFORM = "linuxsteamrt64";
|
|
4208
|
-
var ARG_KINDS = ["bool", "int", "float", "string", "vector", "entity"];
|
|
4212
|
+
var ARG_KINDS = ["bool", "int", "float", "string", "utlstring", "vector", "entity"];
|
|
4209
4213
|
var RET_KINDS = ["void", "bool", "int", "float", "entity"];
|
|
4210
|
-
var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "vector", "entity"]);
|
|
4214
|
+
var INT_CLASS_ARGS = /* @__PURE__ */ new Set(["bool", "int", "string", "utlstring", "vector", "entity"]);
|
|
4211
4215
|
var MAX_INT_ARGS = 9;
|
|
4212
4216
|
var MAX_FLOAT_ARGS = 8;
|
|
4217
|
+
var HOOK_SHAPES = ["this_void", "this_f32_i32_i32_i32", "this_f32_i32_i64_i64", "this_i64_i32_i64", "this_i64_i64_i64"];
|
|
4218
|
+
function hookShapeArity(shape) {
|
|
4219
|
+
if (!HOOK_SHAPES.includes(shape)) return void 0;
|
|
4220
|
+
if (shape === "this_void") return 0;
|
|
4221
|
+
return shape.slice("this_".length).split("_").length;
|
|
4222
|
+
}
|
|
4213
4223
|
|
|
4214
4224
|
// src/gamedata/validate.ts
|
|
4215
|
-
var ALLOWED_SECTIONS = /* @__PURE__ */ new Set(["signatures", "calls"]);
|
|
4225
|
+
var ALLOWED_SECTIONS = /* @__PURE__ */ new Set(["signatures", "calls", "hooks"]);
|
|
4216
4226
|
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
4217
4227
|
var RESERVED_CALL_NAMES = /* @__PURE__ */ new Set(["constructor", "prototype", "__proto__"]);
|
|
4218
4228
|
var RESOLVE_KINDS = ["direct", "ctor-body-xref", "lea-disp"];
|
|
4219
|
-
var KNOWN_PERMISSIONS = ["engine:calls"];
|
|
4229
|
+
var KNOWN_PERMISSIONS = ["engine:calls", "engine:hooks"];
|
|
4220
4230
|
function validatePluginGamedata(gd, opts) {
|
|
4221
4231
|
const errs = [];
|
|
4222
4232
|
if (gd == null || typeof gd !== "object" || Array.isArray(gd)) return ["gamedata must be an object"];
|
|
4223
4233
|
const g = gd;
|
|
4224
4234
|
for (const key of Object.keys(g)) {
|
|
4225
4235
|
if (!ALLOWED_SECTIONS.has(key)) {
|
|
4226
|
-
errs.push(`gamedata section '${key}' is not supported in v1 (allowed: signatures, calls)`);
|
|
4236
|
+
errs.push(`gamedata section '${key}' is not supported in v1 (allowed: signatures, calls, hooks)`);
|
|
4227
4237
|
}
|
|
4228
4238
|
}
|
|
4229
4239
|
const sigs = g.signatures ?? {};
|
|
@@ -4358,8 +4368,247 @@ function validatePluginGamedata(gd, opts) {
|
|
|
4358
4368
|
errs.push(`${where2}: 'returns' must be one of ${RET_KINDS.join(", ")}`);
|
|
4359
4369
|
}
|
|
4360
4370
|
}
|
|
4371
|
+
validateHooks(g, sigs, callNames, opts.permissions, errs);
|
|
4361
4372
|
return errs;
|
|
4362
4373
|
}
|
|
4374
|
+
function isNonEmptyObject(v) {
|
|
4375
|
+
return v != null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0;
|
|
4376
|
+
}
|
|
4377
|
+
function validateHooks(g, sigs, callNames, permissions, errs) {
|
|
4378
|
+
if (g.hooks === void 0) return;
|
|
4379
|
+
if (g.hooks == null || typeof g.hooks !== "object" || Array.isArray(g.hooks)) {
|
|
4380
|
+
errs.push("hooks must be an object");
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const hooks = g.hooks;
|
|
4384
|
+
const hookNames = Object.keys(hooks);
|
|
4385
|
+
if (hookNames.length && !permissions.includes("engine:hooks")) {
|
|
4386
|
+
errs.push('gamedata declares a `hooks` section but the manifest does not declare permission "engine:hooks"');
|
|
4387
|
+
}
|
|
4388
|
+
for (const [name, rawDecl] of Object.entries(hooks)) {
|
|
4389
|
+
const where2 = `hook '${name}'`;
|
|
4390
|
+
if (!IDENTIFIER.test(name)) {
|
|
4391
|
+
errs.push(
|
|
4392
|
+
`${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`
|
|
4393
|
+
);
|
|
4394
|
+
continue;
|
|
4395
|
+
}
|
|
4396
|
+
if (RESERVED_CALL_NAMES.has(name)) {
|
|
4397
|
+
errs.push(`${where2}: hook name '${name}' is reserved`);
|
|
4398
|
+
continue;
|
|
4399
|
+
}
|
|
4400
|
+
if (rawDecl == null || typeof rawDecl !== "object" || Array.isArray(rawDecl)) {
|
|
4401
|
+
errs.push(`${where2}: must be an object`);
|
|
4402
|
+
continue;
|
|
4403
|
+
}
|
|
4404
|
+
const decl = rawDecl;
|
|
4405
|
+
const ctx = decl.expose?.ctx;
|
|
4406
|
+
if (typeof ctx !== "string" || !ctx.length) {
|
|
4407
|
+
errs.push(`${where2}: missing 'expose.ctx' \u2014 nothing could subscribe to this hook`);
|
|
4408
|
+
} else if (!IDENTIFIER.test(ctx)) {
|
|
4409
|
+
errs.push(`${where2}: 'expose.ctx' must be a plain identifier, got ${JSON.stringify(ctx)}`);
|
|
4410
|
+
}
|
|
4411
|
+
const shape = decl.shape;
|
|
4412
|
+
if (typeof shape !== "string" || !HOOK_SHAPES.includes(shape)) {
|
|
4413
|
+
errs.push(
|
|
4414
|
+
`${where2}: unknown hook shape ${JSON.stringify(shape)} (this build compiles thunks for: ${HOOK_SHAPES.join(", ")})`
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
const params = decl.params ?? [];
|
|
4418
|
+
if (!Array.isArray(params)) {
|
|
4419
|
+
errs.push(`${where2}: 'params' must be an array`);
|
|
4420
|
+
} else {
|
|
4421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4422
|
+
for (let i2 = 0; i2 < params.length; i2++) {
|
|
4423
|
+
const p = params[i2];
|
|
4424
|
+
if (typeof p !== "string" || !IDENTIFIER.test(p)) {
|
|
4425
|
+
errs.push(`${where2}: params[${i2}] = ${JSON.stringify(p)} must be a plain identifier`);
|
|
4426
|
+
continue;
|
|
4427
|
+
}
|
|
4428
|
+
if (seen.has(p)) errs.push(`${where2}: param name ${JSON.stringify(p)} is declared more than once`);
|
|
4429
|
+
seen.add(p);
|
|
4430
|
+
}
|
|
4431
|
+
const arity = typeof shape === "string" ? hookShapeArity(shape) : void 0;
|
|
4432
|
+
if (arity !== void 0 && params.length > arity) {
|
|
4433
|
+
errs.push(
|
|
4434
|
+
`${where2}: declares ${params.length} 'params' but shape ${JSON.stringify(shape)} passes only ${arity}`
|
|
4435
|
+
);
|
|
4436
|
+
}
|
|
4437
|
+
if (decl.mutable !== void 0) {
|
|
4438
|
+
const mutable = decl.mutable;
|
|
4439
|
+
if (!Array.isArray(mutable)) {
|
|
4440
|
+
errs.push(`${where2}: 'mutable' must be an array`);
|
|
4441
|
+
} else {
|
|
4442
|
+
for (const m of mutable) {
|
|
4443
|
+
if (typeof m !== "string" || !params.includes(m)) {
|
|
4444
|
+
errs.push(`${where2}: 'mutable' names ${JSON.stringify(m)}, which is not one of this hook's params`);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
const recv = decl.receiver;
|
|
4451
|
+
if (recv !== void 0) {
|
|
4452
|
+
if (recv == null || typeof recv !== "object") {
|
|
4453
|
+
errs.push(`${where2}: 'receiver' must be an object`);
|
|
4454
|
+
} else {
|
|
4455
|
+
const rkind = recv.kind ?? "none";
|
|
4456
|
+
if (rkind !== "none" && rkind !== "entity") {
|
|
4457
|
+
errs.push(`${where2}: receiver.kind must be "none" or "entity"`);
|
|
4458
|
+
} else if (rkind === "entity") {
|
|
4459
|
+
if (typeof recv.as !== "string" || !IDENTIFIER.test(recv.as)) {
|
|
4460
|
+
errs.push(`${where2}: receiver.kind "entity" needs a plain-identifier 'as' name`);
|
|
4461
|
+
} else if (Array.isArray(params) && params.includes(recv.as)) {
|
|
4462
|
+
errs.push(`${where2}: receiver 'as' name ${JSON.stringify(recv.as)} collides with a param name`);
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
if (decl.bypassWith !== void 0) {
|
|
4468
|
+
if (typeof decl.bypassWith !== "string" || !decl.bypassWith.length) {
|
|
4469
|
+
errs.push(`${where2}: 'bypassWith' must be a non-empty call name`);
|
|
4470
|
+
} else if (!callNames.includes(decl.bypassWith)) {
|
|
4471
|
+
errs.push(
|
|
4472
|
+
`${where2}: 'bypassWith' names ${JSON.stringify(decl.bypassWith)}, which is not a 'calls' descriptor in this owner's gamedata`
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
const target = decl.target;
|
|
4477
|
+
if (!target) {
|
|
4478
|
+
errs.push(`${where2}: missing 'target'`);
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
4481
|
+
if (target.kind === "signature") {
|
|
4482
|
+
if (typeof target.name !== "string") {
|
|
4483
|
+
errs.push(`${where2}: target.name must be a string`);
|
|
4484
|
+
} else if (!(target.name in sigs)) {
|
|
4485
|
+
errs.push(`${where2}: target.name '${target.name}' has no entry in 'signatures'`);
|
|
4486
|
+
}
|
|
4487
|
+
const sigPlat = typeof target.name === "string" ? sigs[target.name]?.[PLATFORM] : void 0;
|
|
4488
|
+
const inherited = sigPlat?.validate;
|
|
4489
|
+
if (!isNonEmptyObject(target.validate) && !isNonEmptyObject(inherited)) {
|
|
4490
|
+
errs.push(
|
|
4491
|
+
`${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`
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
} else if (target.kind === "vtable") {
|
|
4495
|
+
if (typeof target.class !== "string") errs.push(`${where2}: vtable target requires 'class'`);
|
|
4496
|
+
const plat = target[PLATFORM];
|
|
4497
|
+
if (!plat) {
|
|
4498
|
+
errs.push(`${where2}: vtable target missing platform '${PLATFORM}'`);
|
|
4499
|
+
} else {
|
|
4500
|
+
if (!Number.isInteger(plat.index) || plat.index < 0) {
|
|
4501
|
+
errs.push(`${where2}: vtable index must be a non-negative integer`);
|
|
4502
|
+
}
|
|
4503
|
+
const prologue = plat.validate?.prologue;
|
|
4504
|
+
if (typeof prologue !== "string" || !prologue.length) {
|
|
4505
|
+
errs.push(`${where2}: a vtable target REQUIRES validate.prologue (a bare borrowed index is never trusted)`);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
} else {
|
|
4509
|
+
errs.push(`${where2}: target.kind must be "signature" or "vtable"`);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
// src/hookgen/model.ts
|
|
4515
|
+
function capitalize(name) {
|
|
4516
|
+
return name.length === 0 ? name : name.charAt(0).toUpperCase() + name.slice(1);
|
|
4517
|
+
}
|
|
4518
|
+
function hookDescriptor(name, decl) {
|
|
4519
|
+
const paramNames = decl.params ?? [];
|
|
4520
|
+
const mutableSet = new Set(decl.mutable ?? []);
|
|
4521
|
+
const params = paramNames.map((p) => ({ name: p, mutable: mutableSet.has(p) }));
|
|
4522
|
+
const receiverAs = decl.receiver?.kind === "entity" ? decl.receiver.as ?? null : null;
|
|
4523
|
+
return { name, shape: decl.shape ?? "", viewIface: `${capitalize(name)}View`, params, receiverAs };
|
|
4524
|
+
}
|
|
4525
|
+
function buildPluginHookList(hooks) {
|
|
4526
|
+
return Object.keys(hooks).sort().map((name) => hookDescriptor(name, hooks[name] ?? {}));
|
|
4527
|
+
}
|
|
4528
|
+
function buildHookModel(hooks) {
|
|
4529
|
+
const byNs = /* @__PURE__ */ new Map();
|
|
4530
|
+
for (const name of Object.keys(hooks).sort()) {
|
|
4531
|
+
const decl = hooks[name] ?? {};
|
|
4532
|
+
const ns = decl.expose?.ctx;
|
|
4533
|
+
if (!ns) continue;
|
|
4534
|
+
if (decl.expose?.handwritten) continue;
|
|
4535
|
+
const list = byNs.get(ns) ?? [];
|
|
4536
|
+
list.push(hookDescriptor(name, decl));
|
|
4537
|
+
byNs.set(ns, list);
|
|
4538
|
+
}
|
|
4539
|
+
return Array.from(byNs.keys()).sort().map((ns) => ({ ns, ctxIface: `Ctx${capitalize(ns)}`, hooks: byNs.get(ns) }));
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
// src/hookgen/emit-dts.ts
|
|
4543
|
+
var HEADER = "// GENERATED by `s2script gen-hooks` from gamedata/cs2/game.cs2.jsonc's `hooks` \u2014 DO NOT EDIT.";
|
|
4544
|
+
var STRING_PARAMS = {
|
|
4545
|
+
this_i64_i64_i64: /* @__PURE__ */ new Set([0])
|
|
4546
|
+
};
|
|
4547
|
+
function emitViewIface(h2) {
|
|
4548
|
+
const out = [`export interface ${h2.viewIface} {`];
|
|
4549
|
+
const strings = STRING_PARAMS[h2.shape] ?? /* @__PURE__ */ new Set();
|
|
4550
|
+
h2.params.forEach((p, i2) => {
|
|
4551
|
+
const mod = p.mutable ? "" : "readonly ";
|
|
4552
|
+
out.push(` ${mod}${p.name}: ${strings.has(i2) ? "string" : "number"};`);
|
|
4553
|
+
});
|
|
4554
|
+
if (h2.receiverAs !== null) {
|
|
4555
|
+
out.push(` readonly ${h2.receiverAs}: EntityRef | null;`);
|
|
4556
|
+
}
|
|
4557
|
+
out.push("}", "");
|
|
4558
|
+
return out;
|
|
4559
|
+
}
|
|
4560
|
+
function emitHookDts(model) {
|
|
4561
|
+
const out = [
|
|
4562
|
+
HEADER,
|
|
4563
|
+
'import type { HookResultValue } from "@s2script/sdk/events";',
|
|
4564
|
+
'import type { EntityRef } from "@s2script/sdk/entity";',
|
|
4565
|
+
""
|
|
4566
|
+
];
|
|
4567
|
+
for (const { hooks } of model) {
|
|
4568
|
+
for (const h2 of hooks) {
|
|
4569
|
+
out.push(...emitViewIface(h2));
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
for (const { ctxIface, hooks } of model) {
|
|
4573
|
+
out.push(`export interface ${ctxIface} {`);
|
|
4574
|
+
for (const h2 of hooks) {
|
|
4575
|
+
out.push(` ${h2.name}(handler: (view: ${h2.viewIface}) => HookResultValue | void): void;`);
|
|
4576
|
+
}
|
|
4577
|
+
out.push("}", "");
|
|
4578
|
+
}
|
|
4579
|
+
out.push('declare module "@s2script/sdk/plugin" {');
|
|
4580
|
+
out.push(" interface PluginContext {");
|
|
4581
|
+
for (const { ns, ctxIface } of model) {
|
|
4582
|
+
out.push(` readonly ${ns}: ${ctxIface};`);
|
|
4583
|
+
}
|
|
4584
|
+
out.push(" }");
|
|
4585
|
+
out.push("}");
|
|
4586
|
+
return out.join("\n") + "\n";
|
|
4587
|
+
}
|
|
4588
|
+
var PLUGIN_HEADER = "// GENERATED by `s2s build` from the plugin's gamedata \u2014 DO NOT EDIT.";
|
|
4589
|
+
function emitPluginHookDts(hooks) {
|
|
4590
|
+
const list = buildPluginHookList(hooks);
|
|
4591
|
+
const out = [
|
|
4592
|
+
PLUGIN_HEADER,
|
|
4593
|
+
"// The EntityRef / HookResultValue imports are REQUIRED: a `declare module` augmentation",
|
|
4594
|
+
"// resolves type names in its own file scope.",
|
|
4595
|
+
'import type { HookResultValue } from "@s2script/sdk/events";',
|
|
4596
|
+
'import type { EntityRef } from "@s2script/sdk/entity";',
|
|
4597
|
+
""
|
|
4598
|
+
];
|
|
4599
|
+
for (const h2 of list) {
|
|
4600
|
+
out.push(...emitViewIface(h2));
|
|
4601
|
+
}
|
|
4602
|
+
out.push('declare module "@s2script/sdk/unsafe" {');
|
|
4603
|
+
out.push(" interface EngineHooks {");
|
|
4604
|
+
for (const h2 of list) {
|
|
4605
|
+
out.push(` ${h2.name}: (handler: (view: ${h2.viewIface}) => HookResultValue | void) => void;`);
|
|
4606
|
+
}
|
|
4607
|
+
out.push(" }");
|
|
4608
|
+
out.push("}");
|
|
4609
|
+
out.push("");
|
|
4610
|
+
return out.join("\n");
|
|
4611
|
+
}
|
|
4363
4612
|
|
|
4364
4613
|
// src/gamedata/gen-types.ts
|
|
4365
4614
|
var VECTOR_TS = "{ readonly x: number; readonly y: number; readonly z: number }";
|
|
@@ -4368,6 +4617,7 @@ var ARG_TS = {
|
|
|
4368
4617
|
int: "number",
|
|
4369
4618
|
float: "number",
|
|
4370
4619
|
string: "string",
|
|
4620
|
+
utlstring: "string",
|
|
4371
4621
|
vector: VECTOR_TS,
|
|
4372
4622
|
entity: "EntityRef | null"
|
|
4373
4623
|
};
|
|
@@ -4405,6 +4655,9 @@ ${lines.join("\n")}
|
|
|
4405
4655
|
""
|
|
4406
4656
|
].join("\n");
|
|
4407
4657
|
}
|
|
4658
|
+
function generateHookTypes(gd) {
|
|
4659
|
+
return emitPluginHookDts(gd.hooks ?? {});
|
|
4660
|
+
}
|
|
4408
4661
|
|
|
4409
4662
|
// src/gamedata/jsonc.ts
|
|
4410
4663
|
function stripJsonComments(src) {
|
|
@@ -4514,8 +4767,14 @@ async function buildPlugin(dir, packagesDir) {
|
|
|
4514
4767
|
const genDir = join11(absDir, ".s2script");
|
|
4515
4768
|
mkdirSync2(genDir, { recursive: true });
|
|
4516
4769
|
writeFileSync3(join11(genDir, "gamedata.d.ts"), generateGamedataTypes(gamedata));
|
|
4770
|
+
if (gamedata.hooks && Object.keys(gamedata.hooks).length) {
|
|
4771
|
+
writeFileSync3(join11(genDir, "hooks.d.ts"), generateHookTypes(gamedata));
|
|
4772
|
+
} else {
|
|
4773
|
+
rmSync2(join11(genDir, "hooks.d.ts"), { force: true });
|
|
4774
|
+
}
|
|
4517
4775
|
} else {
|
|
4518
4776
|
rmSync2(join11(absDir, ".s2script", "gamedata.d.ts"), { force: true });
|
|
4777
|
+
rmSync2(join11(absDir, ".s2script", "hooks.d.ts"), { force: true });
|
|
4519
4778
|
}
|
|
4520
4779
|
const declaredDepNames = [
|
|
4521
4780
|
...Object.keys(s2.pluginDependencies ?? {}),
|
|
@@ -8837,7 +9096,7 @@ function buildModel(catalog, requested, embeddable = [], enumCatalog = {}) {
|
|
|
8837
9096
|
}
|
|
8838
9097
|
|
|
8839
9098
|
// src/schemagen/emit-dts.ts
|
|
8840
|
-
var
|
|
9099
|
+
var HEADER2 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
|
|
8841
9100
|
function emitDts(model) {
|
|
8842
9101
|
const enumTs = new Map(model.enums.map((e) => [e.className, e.tsName]));
|
|
8843
9102
|
const tsType = (f) => {
|
|
@@ -8845,7 +9104,7 @@ function emitDts(model) {
|
|
|
8845
9104
|
return id ? `${id} | null` : TSTYPE[f.accessorKind];
|
|
8846
9105
|
};
|
|
8847
9106
|
const names = new Set(model.classes.map((c2) => c2.className));
|
|
8848
|
-
const out = [
|
|
9107
|
+
const out = [HEADER2, 'import type { EntityRef } from "@s2script/sdk/entity";'];
|
|
8849
9108
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
8850
9109
|
for (const c2 of model.classes) for (const f of c2.ownFields) {
|
|
8851
9110
|
const vi = VEC_INFO[f.accessorKind];
|
|
@@ -8913,7 +9172,7 @@ function emitDts(model) {
|
|
|
8913
9172
|
}
|
|
8914
9173
|
|
|
8915
9174
|
// src/schemagen/emit-js.ts
|
|
8916
|
-
var
|
|
9175
|
+
var HEADER3 = "// GENERATED by `s2script gen-schema` from schema-catalog.json \u2014 DO NOT EDIT.";
|
|
8917
9176
|
var S = JSON.stringify;
|
|
8918
9177
|
function resolveExpr(f) {
|
|
8919
9178
|
const base = `off(${S(f.declaringClass)},${S(f.rawName)})`;
|
|
@@ -8940,7 +9199,7 @@ function accessorEntry(f, resolve22, guard2) {
|
|
|
8940
9199
|
return entry;
|
|
8941
9200
|
}
|
|
8942
9201
|
function emitJs(model) {
|
|
8943
|
-
const out = [
|
|
9202
|
+
const out = [HEADER3, "(function () {", " var off = __s2_schema_offset;", " var A = {};", " var E = {};"];
|
|
8944
9203
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
8945
9204
|
for (const c2 of model.classes) for (const f of c2.ownFields) {
|
|
8946
9205
|
const vi = VEC_INFO[f.accessorKind];
|
|
@@ -9102,7 +9361,7 @@ function buildEventModel(catalog) {
|
|
|
9102
9361
|
}
|
|
9103
9362
|
|
|
9104
9363
|
// src/eventgen/emit-dts.ts
|
|
9105
|
-
var
|
|
9364
|
+
var HEADER4 = "// GENERATED by `s2script gen-events` from event-catalog.json \u2014 DO NOT EDIT.";
|
|
9106
9365
|
var GETTER_RET = {
|
|
9107
9366
|
getBool: "boolean",
|
|
9108
9367
|
getFloat: "number",
|
|
@@ -9113,7 +9372,7 @@ var GETTER_RET = {
|
|
|
9113
9372
|
};
|
|
9114
9373
|
function emitEventDts(model) {
|
|
9115
9374
|
const out = [
|
|
9116
|
-
|
|
9375
|
+
HEADER4,
|
|
9117
9376
|
'import type { GameEvent, HookResultValue } from "@s2script/sdk/events";',
|
|
9118
9377
|
""
|
|
9119
9378
|
];
|
|
@@ -9232,7 +9491,7 @@ function buildNavModel(config, catalog) {
|
|
|
9232
9491
|
}
|
|
9233
9492
|
|
|
9234
9493
|
// src/navgen/emit-js.ts
|
|
9235
|
-
var
|
|
9494
|
+
var HEADER5 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
|
|
9236
9495
|
var S2 = JSON.stringify;
|
|
9237
9496
|
var READ_VIA = {
|
|
9238
9497
|
f32: "readFloat32Via",
|
|
@@ -9250,7 +9509,7 @@ var WRITE_VIA = {
|
|
|
9250
9509
|
i32: "writeInt32Via"
|
|
9251
9510
|
};
|
|
9252
9511
|
function emitNavJs(model) {
|
|
9253
|
-
const out = [
|
|
9512
|
+
const out = [HEADER5, "(function () {", " var off = __s2_schema_offset;"];
|
|
9254
9513
|
const vecClasses = /* @__PURE__ */ new Set();
|
|
9255
9514
|
for (const w of model.wrappers) {
|
|
9256
9515
|
for (const f of w.fields) {
|
|
@@ -9332,7 +9591,7 @@ function emitNavJs(model) {
|
|
|
9332
9591
|
}
|
|
9333
9592
|
|
|
9334
9593
|
// src/navgen/emit-dts.ts
|
|
9335
|
-
var
|
|
9594
|
+
var HEADER6 = "// GENERATED by `s2script gen-nav` from nav-targets.json \u2014 DO NOT EDIT.";
|
|
9336
9595
|
function emitNavDts(model) {
|
|
9337
9596
|
let needsEntityRef = false;
|
|
9338
9597
|
let needsVector = false;
|
|
@@ -9345,7 +9604,7 @@ function emitNavDts(model) {
|
|
|
9345
9604
|
if (vi?.cls === "QAngle") needsQAngle = true;
|
|
9346
9605
|
}
|
|
9347
9606
|
}
|
|
9348
|
-
const out = [
|
|
9607
|
+
const out = [HEADER6];
|
|
9349
9608
|
const mathTypes = [];
|
|
9350
9609
|
if (needsVector) mathTypes.push("Vector");
|
|
9351
9610
|
if (needsQAngle) mathTypes.push("QAngle");
|
|
@@ -9416,72 +9675,6 @@ function runGenNav(repoRoot2, opts) {
|
|
|
9416
9675
|
// src/hookgen/gen.ts
|
|
9417
9676
|
import { readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "node:fs";
|
|
9418
9677
|
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
9678
|
var GAMEDATA_PATH = "gamedata/cs2/game.cs2.jsonc";
|
|
9486
9679
|
var DTS_OUT4 = "packages/cs2/hooks.generated.d.ts";
|
|
9487
9680
|
function runGenHooks(repoRoot2, opts) {
|
package/events.d.ts
CHANGED
|
@@ -52,9 +52,9 @@ export type HookResultValue = 0 | 1 | 2 | 3;
|
|
|
52
52
|
* Events.fire("player_changename", { userid: p.userId, oldname, newname });
|
|
53
53
|
*/
|
|
54
54
|
export declare const Events: {
|
|
55
|
-
/** Fire a game event.
|
|
55
|
+
/** Fire a game event (SourceMod `FireEvent`). Other plugins' `on` / `onPre`
|
|
56
|
+
* handlers run before this returns. Returns the engine FireEvent result. */
|
|
56
57
|
fire(name: string, fields?: Record<string, number | string | boolean | bigint>, dontBroadcast?: boolean): boolean;
|
|
57
|
-
/** Fire a game event to ONE client (SourceMod FireToClient). Same field type-inference as `fire`. Returns false on miss. */
|
|
58
58
|
/**
|
|
59
59
|
* Restrict the event currently being pre-dispatched to `slots`, instead of hiding it from everyone.
|
|
60
60
|
*
|
|
@@ -73,6 +73,7 @@ export declare const Events: {
|
|
|
73
73
|
* });
|
|
74
74
|
*/
|
|
75
75
|
setRecipients(slots: readonly number[]): void;
|
|
76
|
-
|
|
76
|
+
/** Fire a game event to ONE client (SourceMod `FireToClient`). Does not go through
|
|
77
|
+
* `IGameEventManager2::FireEvent` (no `onPre`). Returns false on miss. */
|
|
77
78
|
fireToClient(slot: number, name: string, fields?: Record<string, string | number | boolean | bigint>): boolean;
|
|
78
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
package/server.d.ts
CHANGED
|
@@ -15,16 +15,16 @@ export declare const Server: {
|
|
|
15
15
|
/** A cvar's current value as a string. `""` if the cvar doesn't exist (or an unsupported type → `"<type>"`). */
|
|
16
16
|
getCvar(name: string): string;
|
|
17
17
|
/**
|
|
18
|
-
* Set a cvar
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Set a cvar through `ICvar` now (SourceMod `SetConVarString`). Not a console command: `;` in
|
|
19
|
+
* `value` is data, not a second command. `getCvar` and `onCvarChange` see the new value before
|
|
20
|
+
* this returns. `false` if the cvar does not exist or the string cannot become that type.
|
|
21
21
|
*/
|
|
22
|
-
setCvar(name: string, value: string):
|
|
22
|
+
setCvar(name: string, value: string): boolean;
|
|
23
23
|
/**
|
|
24
24
|
* Register a plugin-owned ConVar (CSSharp FakeConVar / SM CreateConVar parity). Idempotent —
|
|
25
25
|
* re-registering an existing name is a no-op success, and the cvar + its value persist across
|
|
26
26
|
* plugin reloads (SourceMod parity). The shim adds FCVAR_RELEASE (customer-visible); `flags`
|
|
27
|
-
* are additive raw FCVAR bits. Read the value with `getCvar`; set it with `setCvar
|
|
27
|
+
* are additive raw FCVAR bits. Read the value with `getCvar`; set it with `setCvar`.
|
|
28
28
|
* `min`/`max` apply to numeric types only.
|
|
29
29
|
*/
|
|
30
30
|
registerCvar(name: string, opts: {
|
package/unsafe.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @s2script/sdk/unsafe — plugin-declared engine calls. NO runtime code: the
|
|
3
|
-
* implementation at load (`__s2pkg_unsafe`).
|
|
2
|
+
* @s2script/sdk/unsafe — plugin-declared engine calls and inbound hooks. NO runtime code: the
|
|
3
|
+
* engine injects the implementation at load (`__s2pkg_unsafe`).
|
|
4
4
|
*
|
|
5
|
-
* UNSAFE by design. A declared call reaches a real engine function
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* UNSAFE by design. A declared call reaches a real engine function; a declared hook patches
|
|
6
|
+
* that function's prologue. The framework validates every descriptor at load and degrades a
|
|
7
|
+
* failure to `null`, but it cannot prove your signature or vtable index names the function you
|
|
8
|
+
* meant. Calls require manifest `permissions: ["engine:calls"]`; hooks require the separate
|
|
9
|
+
* `"engine:hooks"` permission. Both also need an operator allow-list entry
|
|
10
|
+
* (see docs/superpowers/specs/2026-07-24-plugin-gamedata-design.md §6 and ARCHITECTURE.md §2.0.7).
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -15,13 +17,21 @@
|
|
|
15
17
|
export interface EngineCalls {}
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
20
|
+
* The inbound hooks this plugin declares. The base interface is EMPTY — `s2s build` generates
|
|
21
|
+
* `.s2script/hooks.d.ts`, which augments this from your gamedata's `hooks` section.
|
|
22
|
+
*/
|
|
23
|
+
export interface EngineHooks {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Plugin-declared engine calls and inbound hooks, resolved from this plugin's own gamedata.
|
|
19
27
|
*
|
|
20
28
|
* @example
|
|
21
29
|
* import { Engine } from "@s2script/sdk/unsafe";
|
|
22
30
|
* // Resolve ONCE at load — null means the descriptor failed a load-time gate.
|
|
23
31
|
* const ignite = Engine.call("ignite");
|
|
24
32
|
* if (!ignite) console.log(`unavailable: ${Engine.status("ignite")}`);
|
|
33
|
+
* const onFoo = Engine.hook("onFoo");
|
|
34
|
+
* if (onFoo) onFoo((view) => { view.reason = 0; });
|
|
25
35
|
*/
|
|
26
36
|
export declare const Engine: {
|
|
27
37
|
/**
|
|
@@ -30,6 +40,16 @@ export declare const Engine: {
|
|
|
30
40
|
* operator-allow-listed). Guard once at load; the returned function is a plain callable.
|
|
31
41
|
*/
|
|
32
42
|
call<K extends keyof EngineCalls>(name: K): EngineCalls[K] | null;
|
|
33
|
-
/** Why a descriptor is unavailable; `"available"` when it resolved. For diagnostics/operator reports. */
|
|
43
|
+
/** Why a call descriptor is unavailable; `"available"` when it resolved. For diagnostics/operator reports. */
|
|
34
44
|
status(name: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* The declared hook's subscribe function, or `null` when its descriptor failed a load-time
|
|
47
|
+
* gate (same class of reasons as {@link Engine.call}, plus a missing `validate` or an unknown
|
|
48
|
+
* thunk shape). Guard once at load; the returned function records the handler and lazily
|
|
49
|
+
* installs the detour. The owner is always the calling plugin — you cannot subscribe to
|
|
50
|
+
* another plugin's hook through this factory (game-package hooks hang off `ctx.*`).
|
|
51
|
+
*/
|
|
52
|
+
hook<K extends keyof EngineHooks>(name: K): EngineHooks[K] | null;
|
|
53
|
+
/** Why a hook descriptor is unavailable; `"available"` when it resolved. */
|
|
54
|
+
hookStatus(name: string): string;
|
|
35
55
|
};
|