@s2script/sdk 0.20.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/commands.d.ts +8 -3
- package/dist/cli.js +276 -83
- package/entity.d.ts +34 -0
- package/events.d.ts +4 -3
- package/package.json +1 -1
- package/phrases.d.ts +57 -0
- package/plugin.d.ts +22 -0
- package/server.d.ts +5 -5
- package/sound.d.ts +14 -0
- package/translations.d.ts +39 -9
- 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/commands.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** @s2script/commands — register server commands. NO runtime code (injected at load). */
|
|
2
|
+
import type { PhraseKey } from "./phrases";
|
|
2
3
|
|
|
3
4
|
import type { HookResultValue } from "./events";
|
|
4
5
|
|
|
@@ -72,9 +73,13 @@ export interface CommandInvocation {
|
|
|
72
73
|
* is printed immediately. The server console (`callerSlot` `-1`) prints to the server console.
|
|
73
74
|
*/
|
|
74
75
|
replyToConsole(message: string): void;
|
|
75
|
-
/**
|
|
76
|
-
* `@s2script/translations` — degrades to the raw `key` if it isn't loaded.
|
|
77
|
-
|
|
76
|
+
/** Reply to the caller, translated for THEIR language (SM's `%t` on the reply path). Soft-deps
|
|
77
|
+
* `@s2script/translations` — degrades to the raw `key` if it isn't loaded.
|
|
78
|
+
*
|
|
79
|
+
* `key` is checked against this plugin's phrase file plus the shared one (see
|
|
80
|
+
* `@s2script/sdk/phrases`); it widens to `string` in a plugin that has no phrase file, so this is
|
|
81
|
+
* never in the way. */
|
|
82
|
+
replyT(key: PhraseKey, ...args: (string | number)[]): void;
|
|
78
83
|
}
|
|
79
84
|
/** A parsed chat trigger: which command + args, and whether it was the silent (`/`) trigger. */
|
|
80
85
|
export interface ChatTrigger {
|
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/entity.d.ts
CHANGED
|
@@ -158,6 +158,40 @@ export declare class EntityRef {
|
|
|
158
158
|
* `trigger_multiple` needs a model to build the physics volume that fires touch. Returns false
|
|
159
159
|
* if the op is unavailable or the ref is stale. */
|
|
160
160
|
setModel(name: string): boolean;
|
|
161
|
+
/** Set this entity's gravity multiplier (`CBaseEntity::SetGravityScale`). 1 is normal, 0 is
|
|
162
|
+
* weightless.
|
|
163
|
+
*
|
|
164
|
+
* Use this rather than writing `m_flGravityScale`: the engine setter early-returns when the value
|
|
165
|
+
* is unchanged and maintains a second field (`m_flActualGravityScale`), so a raw field write
|
|
166
|
+
* appears to succeed and does nothing. Returns false if the op is unavailable, the ref is stale,
|
|
167
|
+
* or `scale` is not finite. */
|
|
168
|
+
setGravityScale(scale: number): boolean;
|
|
169
|
+
/** Add to this entity's velocity, physics-aware (`CBaseEntity::ApplyAbsVelocityImpulse`) — for
|
|
170
|
+
* knockback, boosts, explosions.
|
|
171
|
+
*
|
|
172
|
+
* Additive, unlike `teleport(null, null, velocity)` which sets velocity absolutely; and unlike a
|
|
173
|
+
* direct `m_vecAbsVelocity` write, which skips the partition/physics update. `impulse` is an
|
|
174
|
+
* [x,y,z] triple. A zero impulse is a legal no-op. Returns false if the op is unavailable, the ref
|
|
175
|
+
* is stale, or any component is not finite. */
|
|
176
|
+
applyAbsVelocityImpulse(impulse: number[]): boolean;
|
|
177
|
+
/** Stop a sound playing on this entity (`CBaseEntity::StopSound`). `Sound.stop(name, { entity })`
|
|
178
|
+
* in `@s2script/sdk/sound` is the same call spelled next to `Sound.emit`, and
|
|
179
|
+
* `pawn.stopSound(name)` in `@s2script/cs2` is the pawn-shaped one. Returns false if the op is
|
|
180
|
+
* unavailable or the ref is stale. */
|
|
181
|
+
stopSound(name: string): boolean;
|
|
182
|
+
/** Set a model body group by name (`CBaseModelEntity::SetBodyGroupByName`).
|
|
183
|
+
*
|
|
184
|
+
* There is no schema equivalent — `m_bodyGroupChoices` is a `CUtlOrderedMap`, not a writable
|
|
185
|
+
* scalar. `group` is 32-bit engine-side. Returns false if the op is unavailable, the ref is stale,
|
|
186
|
+
* or `group` is out of 32-bit range. */
|
|
187
|
+
setBodyGroupByName(name: string, group: number): boolean;
|
|
188
|
+
/** Set this entity's model scale (`CBaseModelEntity::SetModelScale`).
|
|
189
|
+
*
|
|
190
|
+
* The argument shape is confirmed against the pinned build; the function's NAME is a catalogue
|
|
191
|
+
* attribution its body does not itself prove, so verify the effect before relying on this in a
|
|
192
|
+
* shipped plugin — see the gamedata comment. Calling it is safe either way. Returns false if the
|
|
193
|
+
* op is unavailable, the ref is stale, or `scale` is not finite. */
|
|
194
|
+
setModelScale(scale: number): boolean;
|
|
161
195
|
/** Read a CUtlVector<CHandle> at (ptrOffs chain -> vectorOff) as live liveness-gated EntityRefs.
|
|
162
196
|
* Follows the pointer chain, reads count@+0 / elements@+8, caps at maxCount. [] if stale/unresolved. */
|
|
163
197
|
readHandleVector(ptrOffs: number[], vectorOff: number, maxCount?: number): EntityRef[];
|
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/phrases.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @s2script/phrases — the phrase keys your plugin is allowed to use.
|
|
3
|
+
*
|
|
4
|
+
* **You do not import this module.** It exists so that `cmd.replyT(...)` and
|
|
5
|
+
* `Translations.translate(...)` know which keys are valid in your plugin, and the build fills it in
|
|
6
|
+
* from the phrase files you load.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* export default plugin((ctx) => {
|
|
10
|
+
* ctx.translations.load("basecomm", "common");
|
|
11
|
+
*
|
|
12
|
+
* cmd.replyT("Usage Gag"); // ok — in translations/basecomm.phrases.json
|
|
13
|
+
* cmd.replyT("No matching players"); // ok — in translations/common.phrases.json
|
|
14
|
+
* cmd.replyT("Usage Gagg"); // compile error — no such key in either
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* ## Writing phrases
|
|
18
|
+
*
|
|
19
|
+
* Create `translations/<your-plugin>.phrases.json` — a flat key to English map. `{1}`, `{2}` … are
|
|
20
|
+
* positional slots filled by the trailing arguments; `{green}`, `{default}` … are colour tags
|
|
21
|
+
* expanded on output (see `@s2script/sdk/chat`). Keys are human-readable English on purpose: an
|
|
22
|
+
* unresolved key renders as the key itself, so it should still read as a sentence.
|
|
23
|
+
*
|
|
24
|
+
* ```json
|
|
25
|
+
* { "Gagged Player": "{green}[SM]{default} Gagged {1} player." }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* A translator adds `translations/<code>/<your-plugin>.phrases.json` — no code change, no rebuild.
|
|
29
|
+
*
|
|
30
|
+
* ## How the checking works
|
|
31
|
+
*
|
|
32
|
+
* The build reads your `ctx.translations.load(...)` call and writes `src/phrases.generated.d.ts`,
|
|
33
|
+
* filling in {@link PhraseKeys} with the keys of exactly those files. Load a file and its keys
|
|
34
|
+
* become valid; don't, and they don't — so a forgotten `load` is a compile error rather than raw key
|
|
35
|
+
* text printed to a player at runtime. SourceMod enforces the same rule, but only when it happens.
|
|
36
|
+
*
|
|
37
|
+
* That file is derived and gitignored. Until it has been written — a brand-new plugin, say — keys
|
|
38
|
+
* fall back to plain `string` and go unchecked, so nothing is blocked on the build having run.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The set of valid phrase keys, filled in per plugin by its generated declaration.
|
|
43
|
+
*
|
|
44
|
+
* Empty here deliberately. Each plugin's `src/phrases.generated.d.ts` augments this interface, and
|
|
45
|
+
* interface merging turns that into the exact union {@link PhraseKey} resolves to. Never add keys
|
|
46
|
+
* here by hand — this is the shared module, and a key added here would apply to every plugin.
|
|
47
|
+
*/
|
|
48
|
+
export interface PhraseKeys {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every phrase key valid in this plugin — the union of the keys in every file it loads.
|
|
52
|
+
*
|
|
53
|
+
* Falls back to `string` when nothing has augmented {@link PhraseKeys}, so a plugin that loads no
|
|
54
|
+
* phrases (or whose generated declaration has not been written yet) compiles unchecked rather than
|
|
55
|
+
* failing to compile at all.
|
|
56
|
+
*/
|
|
57
|
+
export type PhraseKey = [keyof PhraseKeys] extends [never] ? string : Extract<keyof PhraseKeys, string>;
|
package/plugin.d.ts
CHANGED
|
@@ -114,6 +114,26 @@ export interface CtxCommands {
|
|
|
114
114
|
handler: (slot: number, argString: string) => HookResultValue | void,
|
|
115
115
|
): void;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* The phrase files this plugin uses ({@link PluginContext.translations}).
|
|
119
|
+
*
|
|
120
|
+
* Nothing is loaded automatically — a plugin declares what it needs, the same rule SourceMod's
|
|
121
|
+
* `LoadTranslations` enforces. The build reads this call to work out which keys `cmd.replyT` and
|
|
122
|
+
* `Translations.translate` will accept, so a key from a file you did not load is a compile error.
|
|
123
|
+
*/
|
|
124
|
+
export interface CtxTranslations {
|
|
125
|
+
/**
|
|
126
|
+
* Load `translations/<name>.phrases.json` for each name, in the order given.
|
|
127
|
+
*
|
|
128
|
+
* Order is significant: `translate` takes the first hit within each of its two passes (the
|
|
129
|
+
* client's language, then English), so list your own set before any shared one if you want to be
|
|
130
|
+
* able to override a shared phrase.
|
|
131
|
+
*
|
|
132
|
+
* @example ctx.translations.load("basecomm", "common");
|
|
133
|
+
*/
|
|
134
|
+
load(...names: string[]): void;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/** Config live-reload subscription on this plugin's load-scope ({@link PluginContext.config}). */
|
|
118
138
|
export interface CtxConfig {
|
|
119
139
|
/** Fires when the plugin's config file is re-materialized on disk; re-read values inside. */
|
|
@@ -175,6 +195,8 @@ export interface PluginContext {
|
|
|
175
195
|
readonly commands: CtxCommands;
|
|
176
196
|
/** Config live-reload subscription ({@link CtxConfig}). */
|
|
177
197
|
readonly config: CtxConfig;
|
|
198
|
+
/** Phrase files this plugin uses ({@link CtxTranslations}). */
|
|
199
|
+
readonly translations: CtxTranslations;
|
|
178
200
|
/** TopMenu (adminmenu) contribution ({@link CtxTopMenu}). */
|
|
179
201
|
readonly topmenu: CtxTopMenu;
|
|
180
202
|
/** Publish this plugin's manifest-declared interface. Buffered; goes live at Active. */
|
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/sound.d.ts
CHANGED
|
@@ -52,4 +52,18 @@ export declare const Sound: {
|
|
|
52
52
|
* function / stale source entity / an empty `recipients` array). An all-bot-skipped recipient
|
|
53
53
|
* set still emits to nobody (a real engine call, may return a nonzero GUID). */
|
|
54
54
|
emit(name: string, opts?: SoundEmitOptions): number;
|
|
55
|
+
/**
|
|
56
|
+
* Stop a named SoundEvent playing on an entity — the counterpart to {@link Sound.emit}.
|
|
57
|
+
*
|
|
58
|
+
* **`opts.entity` is required**, unlike `emit`. The engine call behind this is an instance method
|
|
59
|
+
* on the entity, so there is no global/2D form to default to the way `emit` falls back to
|
|
60
|
+
* worldspawn. `recipients` and `volume` do not apply: the engine stops the sound for everyone
|
|
61
|
+
* hearing it.
|
|
62
|
+
*
|
|
63
|
+
* Equivalent to `entity.stopSound(name)`; this is the discoverable spelling next to `emit`.
|
|
64
|
+
* `pawn.stopSound(name)` in `@s2script/cs2` is the pawn-shaped one.
|
|
65
|
+
*
|
|
66
|
+
* Returns false when `opts.entity` is absent or stale, or the engine op is unavailable.
|
|
67
|
+
*/
|
|
68
|
+
stop(name: string, opts: { entity: EntityRef }): boolean;
|
|
55
69
|
};
|
package/translations.d.ts
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
/** @s2script/translations — SourceMod-style i18n (per-client language, phrase files, {1} formatting). */
|
|
2
|
+
import type { PhraseKey } from "./phrases";
|
|
2
3
|
|
|
3
4
|
/** A phrase set: key → template string, where `{1}`/`{2}`/… are positional substitution slots. */
|
|
4
5
|
export type Phrases = Record<string, string>;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
|
-
* SourceMod-style translation registry
|
|
7
|
-
* `translations/<code>/<name>.phrases.json
|
|
8
|
+
* SourceMod-style translation registry. Phrases live in `translations/<name>.phrases.json`, shipped
|
|
9
|
+
* with the addon and edited by operators; `translations/<code>/<name>.phrases.json` overrides per
|
|
10
|
+
* language, added by a translator with no code change.
|
|
11
|
+
*
|
|
12
|
+
* A plugin declares the files it uses with {@link PluginContext.translations} — nothing is loaded
|
|
13
|
+
* for it automatically, the same rule SourceMod's `LoadTranslations` enforces:
|
|
14
|
+
*
|
|
8
15
|
* @example
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* export default plugin((ctx) => {
|
|
17
|
+
* ctx.translations.load("basecomm", "common");
|
|
18
|
+
* ctx.commands.registerAdmin("sm_gag", ADMFLAG.CHAT, (cmd) => {
|
|
19
|
+
* cmd.replyT("Usage Gag"); // key-checked
|
|
20
|
+
* Chat.toSlot(p.slot, Translations.translate(p.slot, "Gagged Player", n));
|
|
21
|
+
* });
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* Keys are checked against the files that plugin loads — see `@s2script/sdk/phrases`.
|
|
12
25
|
*/
|
|
13
26
|
export declare const Translations: {
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Register a phrase set by name, populating it from `translations/<name>.phrases.json`.
|
|
29
|
+
*
|
|
30
|
+
* Prefer {@link PluginContext.translations}`.load(...)`, which is what the build reads to work out
|
|
31
|
+
* which keys your plugin may use. This is the lower-level form, for a name computed at runtime or
|
|
32
|
+
* a set with an in-code default — a `seed` here is the starting content, which the file overrides.
|
|
33
|
+
*
|
|
34
|
+
* Registration order is significant: `translate` takes the first hit within each of its two passes
|
|
35
|
+
* (the client's language, then English), so load your own set before any shared one if you want to
|
|
36
|
+
* override a shared phrase.
|
|
37
|
+
*/
|
|
38
|
+
load(name: string, seed?: Phrases): void;
|
|
39
|
+
/**
|
|
40
|
+
* Translate `key` for `slot`'s language (slot < 0 = the server default), substituting positional
|
|
41
|
+
* {1}/{2} args.
|
|
42
|
+
*
|
|
43
|
+
* `key` is checked against the phrase files this plugin loads; it widens to `string` in a plugin
|
|
44
|
+
* that loads none, so this is never in the way. A key found in no loaded set returns the key
|
|
45
|
+
* itself — which is what the checking exists to prevent reaching a player.
|
|
46
|
+
*/
|
|
47
|
+
translate(slot: number, key: PhraseKey, ...args: (string | number)[]): string;
|
|
18
48
|
/** Set the server/console default language code (default "" = root/English). */
|
|
19
49
|
setDefaultLanguage(code: string): void;
|
|
20
50
|
};
|
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
|
};
|