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