@s2script/sdk 0.19.0 → 0.21.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/commands.d.ts +8 -3
- package/dist/cli.js +119 -6
- package/entity.d.ts +34 -0
- package/package.json +1 -1
- package/phrases.d.ts +57 -0
- package/plugin.d.ts +22 -0
- package/sound.d.ts +14 -0
- package/translations.d.ts +39 -9
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
|
@@ -3769,6 +3769,12 @@ function generatedDeclarationFiles(pluginDir) {
|
|
|
3769
3769
|
const gd = join7(pluginDir, ".s2script", "gamedata.d.ts");
|
|
3770
3770
|
return existsSync6(gd) ? [gd] : [];
|
|
3771
3771
|
}
|
|
3772
|
+
function gamePackageDeclarationFiles(pkg, packagesDir) {
|
|
3773
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3774
|
+
if (!("@s2script/cs2" in deps)) return [];
|
|
3775
|
+
const hooks = join7(packagesDir, "cs2", "hooks.generated.d.ts");
|
|
3776
|
+
return existsSync6(hooks) ? [hooks] : [];
|
|
3777
|
+
}
|
|
3772
3778
|
function declaredModules(dtsFiles) {
|
|
3773
3779
|
const out = /* @__PURE__ */ new Set();
|
|
3774
3780
|
for (const f of dtsFiles) {
|
|
@@ -3837,7 +3843,8 @@ function typecheckPlugin(pluginDir, opts) {
|
|
|
3837
3843
|
entry,
|
|
3838
3844
|
join7(packagesDir, "sdk", "globals.d.ts"),
|
|
3839
3845
|
...localDts,
|
|
3840
|
-
...generatedDeclarationFiles(absDir)
|
|
3846
|
+
...generatedDeclarationFiles(absDir),
|
|
3847
|
+
...gamePackageDeclarationFiles(pkg, packagesDir)
|
|
3841
3848
|
];
|
|
3842
3849
|
const tmp = mkdtempSync(join7(tmpdir(), "s2tc-"));
|
|
3843
3850
|
try {
|
|
@@ -8528,7 +8535,7 @@ async function run8(argv2) {
|
|
|
8528
8535
|
}
|
|
8529
8536
|
|
|
8530
8537
|
// src/commands/codegen.ts
|
|
8531
|
-
import { dirname as dirname6, join as
|
|
8538
|
+
import { dirname as dirname6, join as join28 } from "node:path";
|
|
8532
8539
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8533
8540
|
|
|
8534
8541
|
// src/schemagen/gen.ts
|
|
@@ -9406,9 +9413,102 @@ function runGenNav(repoRoot2, opts) {
|
|
|
9406
9413
|
return { wrappers: model.wrappers.length, fields, drift };
|
|
9407
9414
|
}
|
|
9408
9415
|
|
|
9416
|
+
// src/hookgen/gen.ts
|
|
9417
|
+
import { readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "node:fs";
|
|
9418
|
+
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
|
+
var GAMEDATA_PATH = "gamedata/cs2/game.cs2.jsonc";
|
|
9486
|
+
var DTS_OUT4 = "packages/cs2/hooks.generated.d.ts";
|
|
9487
|
+
function runGenHooks(repoRoot2, opts) {
|
|
9488
|
+
const raw = readFileSync22(join27(repoRoot2, GAMEDATA_PATH), "utf8");
|
|
9489
|
+
const gd = JSON.parse(stripJsonComments(raw));
|
|
9490
|
+
const hooks = gd.hooks ?? {};
|
|
9491
|
+
const model = buildHookModel(hooks);
|
|
9492
|
+
const dts = emitHookDts(model);
|
|
9493
|
+
const drift = [];
|
|
9494
|
+
const abs = join27(repoRoot2, DTS_OUT4);
|
|
9495
|
+
if (opts.check) {
|
|
9496
|
+
let cur = "";
|
|
9497
|
+
try {
|
|
9498
|
+
cur = readFileSync22(abs, "utf8");
|
|
9499
|
+
} catch {
|
|
9500
|
+
}
|
|
9501
|
+
if (cur !== dts) drift.push(DTS_OUT4);
|
|
9502
|
+
} else {
|
|
9503
|
+
writeFileSync17(abs, dts);
|
|
9504
|
+
}
|
|
9505
|
+
const hookCount = model.reduce((n3, ns) => n3 + ns.hooks.length, 0);
|
|
9506
|
+
return { hooks: hookCount, drift };
|
|
9507
|
+
}
|
|
9508
|
+
|
|
9409
9509
|
// src/commands/codegen.ts
|
|
9410
9510
|
function repoRoot() {
|
|
9411
|
-
return
|
|
9511
|
+
return join28(dirname6(fileURLToPath3(import.meta.url)), "..", "..", "..");
|
|
9412
9512
|
}
|
|
9413
9513
|
async function run9(kind, argv2) {
|
|
9414
9514
|
const root = repoRoot();
|
|
@@ -9437,7 +9537,7 @@ async function run9(kind, argv2) {
|
|
|
9437
9537
|
} else {
|
|
9438
9538
|
console.log(`gen-events: wrote ${r2.events} events`);
|
|
9439
9539
|
}
|
|
9440
|
-
} else {
|
|
9540
|
+
} else if (kind === "nav") {
|
|
9441
9541
|
const r2 = runGenNav(root, { check });
|
|
9442
9542
|
if (check) {
|
|
9443
9543
|
if (r2.drift.length) {
|
|
@@ -9449,6 +9549,18 @@ async function run9(kind, argv2) {
|
|
|
9449
9549
|
} else {
|
|
9450
9550
|
console.log(`gen-nav: wrote ${r2.wrappers} wrappers, ${r2.fields} fields`);
|
|
9451
9551
|
}
|
|
9552
|
+
} else {
|
|
9553
|
+
const r2 = runGenHooks(root, { check });
|
|
9554
|
+
if (check) {
|
|
9555
|
+
if (r2.drift.length) {
|
|
9556
|
+
console.error(`FAIL: generated files out of date \u2014 run \`s2s gen-hooks\`:
|
|
9557
|
+
${r2.drift.join("\n ")}`);
|
|
9558
|
+
process.exit(1);
|
|
9559
|
+
}
|
|
9560
|
+
console.log(`hook codegen up to date (${r2.hooks} hooks)`);
|
|
9561
|
+
} else {
|
|
9562
|
+
console.log(`gen-hooks: wrote ${r2.hooks} hooks`);
|
|
9563
|
+
}
|
|
9452
9564
|
}
|
|
9453
9565
|
}
|
|
9454
9566
|
|
|
@@ -9464,7 +9576,8 @@ var COMMANDS = [
|
|
|
9464
9576
|
{ name: "config", summary: "Emit a plugin's default config file(s)", run: run8 },
|
|
9465
9577
|
{ name: "gen-schema", summary: "Regenerate schema accessors", run: (a2) => run9("schema", a2) },
|
|
9466
9578
|
{ name: "gen-events", summary: "Regenerate the event catalog", run: (a2) => run9("events", a2) },
|
|
9467
|
-
{ name: "gen-nav", summary: "Regenerate nav accessors", run: (a2) => run9("nav", a2) }
|
|
9579
|
+
{ name: "gen-nav", summary: "Regenerate nav accessors", run: (a2) => run9("nav", a2) },
|
|
9580
|
+
{ name: "gen-hooks", summary: "Regenerate the ctx hook augmentation", run: (a2) => run9("hooks", a2) }
|
|
9468
9581
|
];
|
|
9469
9582
|
function find(name) {
|
|
9470
9583
|
if (name === "publish") return COMMANDS.find((c2) => c2.name === "deploy");
|
|
@@ -9487,7 +9600,7 @@ var argv = process.argv.slice(2);
|
|
|
9487
9600
|
var command = argv[0];
|
|
9488
9601
|
function usage() {
|
|
9489
9602
|
console.error(
|
|
9490
|
-
"Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal] [--library]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s create --workspace <dir>\n s2s build [dir|workspace root] [--filter <pattern>]... [--stamp-version <v>]\n [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir|workspace root] [--filter <pattern>]... [--dry-run] [--yes|--ci]\n [--registry <url>] [--packages-dir <path>]\n s2s version [dir] [--since <ref>]\n s2s add <pkg>[@range] [--dir <plugin>] [--registry <url>]\n s2s install [name[@range]...] [--file <path>] [--dir <plugins dir>] [--registry <url>]\n s2s config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]\n\nEnv: S2SCRIPT_REGISTRY_URL S2SCRIPT_TOKEN (CI deploy)"
|
|
9603
|
+
"Usage:\n s2s create [path] [--game cs2|none] [--name <pkg>] [--template minimal] [--library]\n [--install npm|pnpm|yarn|bun|none] [--no-install] [-y]\n s2s create --workspace <dir>\n s2s build [dir|workspace root] [--filter <pattern>]... [--stamp-version <v>]\n [--packages-dir <path>]\n s2s login [--token s2s_\u2026] [--registry <url>]\n s2s deploy [dir|workspace root] [--filter <pattern>]... [--dry-run] [--yes|--ci]\n [--registry <url>] [--packages-dir <path>]\n s2s version [dir] [--since <ref>]\n s2s add <pkg>[@range] [--dir <plugin>] [--registry <url>]\n s2s install [name[@range]...] [--file <path>] [--dir <plugins dir>] [--registry <url>]\n s2s config gen <plugin.s2sp...> --out <dir>\n s2s gen-schema [--check]\n s2s gen-events [--check]\n s2s gen-nav [--check]\n s2s gen-hooks [--check]\n\nEnv: S2SCRIPT_REGISTRY_URL S2SCRIPT_TOKEN (CI deploy)"
|
|
9491
9604
|
);
|
|
9492
9605
|
}
|
|
9493
9606
|
if (!command) {
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@s2script/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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/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
|
};
|