killscript 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,2571 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VERSION
4
+ } from "./chunk-T3XTQIWG.js";
5
+
6
+ // src/cli.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/commands/build.ts
10
+ import path6 from "path";
11
+
12
+ // src/config.ts
13
+ import { randomUUID } from "crypto";
14
+ import { mkdir, readFile, writeFile } from "fs/promises";
15
+ import path from "path";
16
+ var PROJECT_CONFIG_FILE = "killscript.config.json";
17
+ function createProjectConfig(input) {
18
+ return {
19
+ id: randomUUID(),
20
+ name: input.name,
21
+ version: "0.1.0",
22
+ author: input.author ?? "",
23
+ reflex: input.reflex ?? false
24
+ };
25
+ }
26
+ async function readProjectConfig(projectDir) {
27
+ const file = path.join(projectDir, PROJECT_CONFIG_FILE);
28
+ const parsed = JSON.parse(await readFile(file, "utf8"));
29
+ validateProjectConfig(parsed);
30
+ return parsed;
31
+ }
32
+ async function writeProjectConfig(projectDir, config) {
33
+ await writeFile(
34
+ path.join(projectDir, PROJECT_CONFIG_FILE),
35
+ `${JSON.stringify(config, null, 2)}
36
+ `
37
+ );
38
+ }
39
+ async function readLocalConfig(projectDir) {
40
+ try {
41
+ const parsed = JSON.parse(
42
+ await readFile(path.join(projectDir, ".killscript", "local.json"), "utf8")
43
+ );
44
+ if (parsed.modulesPath !== void 0 && typeof parsed.modulesPath !== "string") {
45
+ throw new Error(".killscript/local.json has an invalid modulesPath.");
46
+ }
47
+ if (parsed.installedTarget !== void 0 && typeof parsed.installedTarget !== "string") {
48
+ throw new Error(".killscript/local.json has an invalid installedTarget.");
49
+ }
50
+ return parsed;
51
+ } catch (error) {
52
+ if (error.code === "ENOENT") return {};
53
+ throw error;
54
+ }
55
+ }
56
+ async function writeLocalConfig(projectDir, config) {
57
+ await mkdir(path.join(projectDir, ".killscript"), { recursive: true });
58
+ await writeFile(
59
+ path.join(projectDir, ".killscript", "local.json"),
60
+ `${JSON.stringify(config, null, 2)}
61
+ `
62
+ );
63
+ }
64
+ function validateProjectConfig(config) {
65
+ if (typeof config.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(config.id)) {
66
+ throw new Error(`${PROJECT_CONFIG_FILE}: id must be a UUID.`);
67
+ }
68
+ if (typeof config.name !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(config.name)) {
69
+ throw new Error(`${PROJECT_CONFIG_FILE}: name must be a safe module name (1-64 characters).`);
70
+ }
71
+ if (typeof config.version !== "string" || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(config.version)) {
72
+ throw new Error(`${PROJECT_CONFIG_FILE}: version must be semantic, for example 1.2.0.`);
73
+ }
74
+ if (typeof config.author !== "string" || config.author.length > 128) {
75
+ throw new Error(`${PROJECT_CONFIG_FILE}: author must be a string up to 128 characters.`);
76
+ }
77
+ if (config.description !== void 0 && (typeof config.description !== "string" || config.description.length > 500)) {
78
+ throw new Error(`${PROJECT_CONFIG_FILE}: description must be a string up to 500 characters.`);
79
+ }
80
+ if (typeof config.reflex !== "boolean") {
81
+ throw new Error(`${PROJECT_CONFIG_FILE}: reflex must be true or false.`);
82
+ }
83
+ if (config.tags !== void 0 && (!Array.isArray(config.tags) || config.tags.length > 16 || config.tags.some((tag) => typeof tag !== "string" || !/^[A-Za-z0-9][A-Za-z0-9 _-]{0,31}$/.test(tag)) || new Set(config.tags.map((tag) => tag.toLowerCase())).size !== config.tags.length)) {
84
+ throw new Error(`${PROJECT_CONFIG_FILE}: tags must contain up to 16 unique safe strings.`);
85
+ }
86
+ }
87
+
88
+ // src/compiler/build.ts
89
+ import { cp, lstat, mkdir as mkdir2, readdir, rm, stat, writeFile as writeFile2 } from "fs/promises";
90
+ import path4 from "path";
91
+
92
+ // src/compiler/errors.ts
93
+ var CompileError = class extends Error {
94
+ file;
95
+ position;
96
+ constructor(message, node) {
97
+ super(message);
98
+ this.name = "CompileError";
99
+ let file;
100
+ let position;
101
+ if (node !== void 0) {
102
+ try {
103
+ file = node.getSourceFile();
104
+ if (file !== void 0) position = node.getStart(file);
105
+ } catch {
106
+ }
107
+ }
108
+ this.file = file;
109
+ this.position = position;
110
+ }
111
+ };
112
+ function formatCompileError(error) {
113
+ if (error.file === void 0 || error.position === void 0) return error.message;
114
+ const location = error.file.getLineAndCharacterOfPosition(error.position);
115
+ return `${error.file.fileName}:${location.line + 1}:${location.character + 1} - ${error.message}`;
116
+ }
117
+
118
+ // src/compiler/compile-context.ts
119
+ import path3 from "path";
120
+ import * as ts3 from "typescript";
121
+ import {
122
+ LuaLibImportKind,
123
+ LuaTarget,
124
+ Transpiler
125
+ } from "typescript-to-lua";
126
+
127
+ // src/compiler/extract.ts
128
+ import path2 from "path";
129
+ import * as ts from "typescript";
130
+ var keyboardPaths = Object.fromEntries([
131
+ ..."ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((key) => [key, key.toLowerCase()]),
132
+ ...Array.from({ length: 12 }, (_, index) => [`F${index + 1}`, `f${index + 1}`]),
133
+ ["Space", "space"],
134
+ ["Enter", "enter"],
135
+ ["Escape", "escape"],
136
+ ["Tab", "tab"],
137
+ ["Backspace", "backspace"],
138
+ ["LeftShift", "leftShift"],
139
+ ["RightShift", "rightShift"],
140
+ ["LeftCtrl", "leftCtrl"],
141
+ ["RightCtrl", "rightCtrl"],
142
+ ["LeftAlt", "leftAlt"],
143
+ ["RightAlt", "rightAlt"],
144
+ ["Up", "upArrow"],
145
+ ["Down", "downArrow"],
146
+ ["Left", "leftArrow"],
147
+ ["Right", "rightArrow"],
148
+ ...Array.from({ length: 10 }, (_, index) => [`Digit${index}`, String(index)]),
149
+ ["Home", "home"],
150
+ ["End", "end"],
151
+ ["PageUp", "pageUp"],
152
+ ["PageDown", "pageDown"],
153
+ ["Insert", "insert"],
154
+ ["Delete", "delete"],
155
+ ["CapsLock", "capsLock"],
156
+ ["Comma", "comma"],
157
+ ["Period", "period"],
158
+ ["Slash", "slash"],
159
+ ["Semicolon", "semicolon"],
160
+ ["Quote", "quote"],
161
+ ["Backquote", "backquote"],
162
+ ["Minus", "minus"],
163
+ ["Equals", "equals"],
164
+ ["LeftBracket", "leftBracket"],
165
+ ["RightBracket", "rightBracket"],
166
+ ["Backslash", "backslash"]
167
+ ]);
168
+ var mousePaths = {
169
+ Left: "leftButton",
170
+ Right: "rightButton",
171
+ Middle: "middleButton",
172
+ Back: "backButton",
173
+ Forward: "forwardButton",
174
+ Scroll: "scroll",
175
+ Delta: "delta",
176
+ Position: "position"
177
+ };
178
+ var gamepadPaths = {
179
+ South: "buttonSouth",
180
+ East: "buttonEast",
181
+ West: "buttonWest",
182
+ North: "buttonNorth",
183
+ LeftShoulder: "leftShoulder",
184
+ RightShoulder: "rightShoulder",
185
+ LeftTrigger: "leftTrigger",
186
+ RightTrigger: "rightTrigger",
187
+ LeftStick: "leftStick",
188
+ RightStick: "rightStick",
189
+ LeftStickPress: "leftStickPress",
190
+ RightStickPress: "rightStickPress",
191
+ Dpad: "dpad",
192
+ DpadUp: "dpad/up",
193
+ DpadDown: "dpad/down",
194
+ DpadLeft: "dpad/left",
195
+ DpadRight: "dpad/right",
196
+ Start: "start",
197
+ Select: "select"
198
+ };
199
+ function extractMacros(program2, projectSourceDir) {
200
+ const checker = program2.getTypeChecker();
201
+ const bindings = collectBindings(program2, checker);
202
+ const settings = [];
203
+ const controls = [];
204
+ const settingsHandles = /* @__PURE__ */ new Map();
205
+ const controlsHandles = /* @__PURE__ */ new Map();
206
+ const networkHandles = /* @__PURE__ */ new Map();
207
+ for (const sourceFile of program2.getSourceFiles()) {
208
+ if (sourceFile.isDeclarationFile || !isInside(sourceFile.fileName, projectSourceDir)) {
209
+ continue;
210
+ }
211
+ visitSourceFile(sourceFile);
212
+ }
213
+ assertUnique(settings, (item) => item.key, "setting");
214
+ assertUnique(controls, (item) => item.name, "control");
215
+ return { settings, controls, settingsHandles, controlsHandles, networkHandles, bindings };
216
+ function visitSourceFile(sourceFile) {
217
+ const visit = (node) => {
218
+ if (ts.isCallExpression(node)) {
219
+ if (isImportedIdentifier(node.expression, bindings.defineSettings, checker)) {
220
+ registerSettings(node);
221
+ } else if (isImportedIdentifier(node.expression, bindings.defineControls, checker)) {
222
+ registerControls(node);
223
+ } else if (isImportedIdentifier(node.expression, bindings.defineNetwork, checker)) {
224
+ registerNetwork(node);
225
+ }
226
+ }
227
+ ts.forEachChild(node, visit);
228
+ };
229
+ visit(sourceFile);
230
+ }
231
+ function registerNetwork(call) {
232
+ const declaration = getTopLevelVariableDeclaration(call, "defineNetwork");
233
+ if (call.arguments.length > 1) {
234
+ throw new CompileError("defineNetwork() accepts only an optional namespace.", call);
235
+ }
236
+ const namespace = call.arguments[0] === void 0 ? void 0 : readString(call.arguments[0]);
237
+ if (namespace !== void 0 && !/^[A-Za-z0-9_.-]+$/.test(namespace)) {
238
+ throw new CompileError("Network namespace may contain letters, numbers, _, . and -.", call);
239
+ }
240
+ const symbol = checker.getSymbolAtLocation(declaration.name);
241
+ if (symbol === void 0) throw new CompileError("Unable to resolve network handle.", declaration);
242
+ networkHandles.set(symbol, { ...namespace === void 0 ? {} : { namespace } });
243
+ }
244
+ function registerSettings(call) {
245
+ const declaration = getTopLevelVariableDeclaration(call, "defineSettings");
246
+ const { namespace, object } = readDefinitionArguments(call, "defineSettings");
247
+ const properties = /* @__PURE__ */ new Map();
248
+ for (const property of object.properties) {
249
+ if (!ts.isPropertyAssignment(property)) {
250
+ throw new CompileError("Settings must use plain property assignments.", property);
251
+ }
252
+ const sourceName = readPropertyName(property.name);
253
+ const key = namespace === void 0 ? sourceName : `${namespace}.${sourceName}`;
254
+ assertRegistrationId(key, "setting", property.name);
255
+ const registration = parseSetting(sourceName, key, property.initializer);
256
+ properties.set(sourceName, registration);
257
+ settings.push(registration);
258
+ }
259
+ const symbol = checker.getSymbolAtLocation(declaration.name);
260
+ if (symbol === void 0) throw new CompileError("Unable to resolve settings handle.", declaration);
261
+ settingsHandles.set(symbol, { properties });
262
+ }
263
+ function registerControls(call) {
264
+ const declaration = getTopLevelVariableDeclaration(call, "defineControls");
265
+ const { namespace, object } = readDefinitionArguments(call, "defineControls");
266
+ const properties = /* @__PURE__ */ new Map();
267
+ for (const property of object.properties) {
268
+ if (!ts.isPropertyAssignment(property)) {
269
+ throw new CompileError("Controls must use plain property assignments.", property);
270
+ }
271
+ const sourceName = readPropertyName(property.name);
272
+ const name = namespace === void 0 ? sourceName : `${namespace}.${sourceName}`;
273
+ assertRegistrationId(name, "control", property.name);
274
+ const registration = parseControl(sourceName, name, property.initializer);
275
+ properties.set(sourceName, registration);
276
+ controls.push(registration);
277
+ }
278
+ const symbol = checker.getSymbolAtLocation(declaration.name);
279
+ if (symbol === void 0) throw new CompileError("Unable to resolve controls handle.", declaration);
280
+ controlsHandles.set(symbol, { properties });
281
+ }
282
+ function parseSetting(sourceName, key, expression) {
283
+ expression = unwrap(expression);
284
+ const label = humanize(sourceName);
285
+ if (expression.kind === ts.SyntaxKind.TrueKeyword || expression.kind === ts.SyntaxKind.FalseKeyword) {
286
+ return { sourceName, key, label, type: "bool", value: expression.kind === ts.SyntaxKind.TrueKeyword };
287
+ }
288
+ if (ts.isNumericLiteral(expression) || isNegativeNumber(expression)) {
289
+ return { sourceName, key, label, type: "number", value: readNumber(expression) };
290
+ }
291
+ if (!ts.isCallExpression(expression)) {
292
+ throw new CompileError(
293
+ "Use a boolean, number or setting(...) declaration for this setting.",
294
+ expression
295
+ );
296
+ }
297
+ const callKind = getSettingCallKind(expression.expression);
298
+ if (callKind === void 0) {
299
+ throw new CompileError("Unsupported setting declaration.", expression);
300
+ }
301
+ if (callKind === "setting") {
302
+ const defaultValue = expression.arguments[0];
303
+ if (defaultValue === void 0) throw new CompileError("setting() needs a default value.", expression);
304
+ const options2 = readOptions(expression.arguments[1]);
305
+ const value = unwrap(defaultValue);
306
+ if (value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword) {
307
+ return {
308
+ sourceName,
309
+ key,
310
+ label: options2.label ?? label,
311
+ type: "bool",
312
+ value: value.kind === ts.SyntaxKind.TrueKeyword
313
+ };
314
+ }
315
+ const numericValue = readNumber(value);
316
+ validateNumberOptions(numericValue, options2, expression);
317
+ return {
318
+ sourceName,
319
+ key,
320
+ label: options2.label ?? label,
321
+ type: "number",
322
+ value: numericValue,
323
+ ...options2.min === void 0 ? {} : { min: options2.min },
324
+ ...options2.max === void 0 ? {} : { max: options2.max }
325
+ };
326
+ }
327
+ if (callKind === "choice") {
328
+ const choices = readStringArray(requiredArgument(expression, 0));
329
+ validateChoices(choices, expression);
330
+ const defaultValue = readString(requiredArgument(expression, 1));
331
+ const defaultIndex = choices.indexOf(defaultValue);
332
+ if (defaultIndex < 0) {
333
+ throw new CompileError(`Default choice '${defaultValue}' is not in the options.`, expression);
334
+ }
335
+ const options2 = readOptions(expression.arguments[2]);
336
+ return {
337
+ sourceName,
338
+ key,
339
+ label: options2.label ?? label,
340
+ type: "enum",
341
+ value: defaultIndex,
342
+ options: choices
343
+ };
344
+ }
345
+ if (callKind === "flags") {
346
+ const choices = readStringArray(requiredArgument(expression, 0));
347
+ validateChoices(choices, expression);
348
+ if (choices.length > 31) throw new CompileError("Flag settings support at most 31 options.", expression);
349
+ const defaults = expression.arguments[1] === void 0 ? [] : readStringArray(expression.arguments[1]);
350
+ let mask = 0;
351
+ for (const item of defaults) {
352
+ const index = choices.indexOf(item);
353
+ if (index < 0) throw new CompileError(`Default flag '${item}' is not in the options.`, expression);
354
+ mask |= 1 << index;
355
+ }
356
+ const options2 = readOptions(expression.arguments[2]);
357
+ return {
358
+ sourceName,
359
+ key,
360
+ label: options2.label ?? label,
361
+ type: "maskEnum",
362
+ value: mask,
363
+ options: choices
364
+ };
365
+ }
366
+ const rgba = readColor(requiredArgument(expression, 0));
367
+ if (rgba.some((component) => component < 0 || component > 1)) {
368
+ throw new CompileError("Color components must be between 0 and 1.", expression);
369
+ }
370
+ const options = readOptions(expression.arguments[1]);
371
+ return {
372
+ sourceName,
373
+ key,
374
+ label: options.label ?? label,
375
+ type: "color",
376
+ value: rgba
377
+ };
378
+ }
379
+ function parseControl(sourceName, name, rawExpression) {
380
+ const expression = unwrap(rawExpression);
381
+ const directPath = readBindingConstant(expression);
382
+ if (directPath !== void 0) {
383
+ return { sourceName, name, type: "Button", binding: { path: directPath } };
384
+ }
385
+ if (!ts.isCallExpression(expression)) {
386
+ throw new CompileError("Use Keyboard.*, Mouse.* or control(...) for a control.", expression);
387
+ }
388
+ const kind = getControlCallKind(expression.expression);
389
+ if (kind === void 0) throw new CompileError("Unsupported control declaration.", expression);
390
+ if (kind === "raw") {
391
+ const definition = readObject(requiredArgument(expression, 0));
392
+ assertKnownKeys(definition, ["type", "binding"], expression);
393
+ const type = readControlType(definition.get("type"), expression);
394
+ const bindingNode2 = definition.get("binding");
395
+ if (bindingNode2 === void 0) throw new CompileError("Raw control needs a binding.", expression);
396
+ return { sourceName, name, type, binding: readBinding(bindingNode2) };
397
+ }
398
+ if (["hold", "tap", "multiTap", "onRelease"].includes(kind)) {
399
+ const binding = readBinding(requiredArgument(expression, 0));
400
+ if (kind === "onRelease") {
401
+ return {
402
+ sourceName,
403
+ name,
404
+ type: "Button",
405
+ binding: { ...binding, interactions: "press(behavior=1)" }
406
+ };
407
+ }
408
+ if (kind === "multiTap") {
409
+ const tapCount = expression.arguments[1] === void 0 ? 2 : readNumber(expression.arguments[1]);
410
+ const maxDelay = expression.arguments[2] === void 0 ? 0.3 : readNumber(expression.arguments[2]);
411
+ if (!Number.isInteger(tapCount) || tapCount < 2) {
412
+ throw new CompileError("control.multiTap() needs an integer tap count of at least 2.", expression);
413
+ }
414
+ if (maxDelay <= 0) {
415
+ throw new CompileError("control.multiTap() max delay must be greater than zero.", expression);
416
+ }
417
+ return {
418
+ sourceName,
419
+ name,
420
+ type: "Button",
421
+ binding: {
422
+ ...binding,
423
+ interactions: `multiTap(tapCount=${tapCount},tapDelay=${maxDelay})`
424
+ }
425
+ };
426
+ }
427
+ const duration = expression.arguments[1] === void 0 ? kind === "hold" ? 0.4 : 0.2 : readNumber(expression.arguments[1]);
428
+ if (duration <= 0) {
429
+ throw new CompileError(`control.${kind}() duration must be greater than zero.`, expression);
430
+ }
431
+ return {
432
+ sourceName,
433
+ name,
434
+ type: "Button",
435
+ binding: { ...binding, interactions: `${kind}(duration=${duration})` }
436
+ };
437
+ }
438
+ const bindingNode = requiredArgument(expression, 0);
439
+ const options = expression.arguments[1] === void 0 ? /* @__PURE__ */ new Map() : readObject(expression.arguments[1]);
440
+ assertKnownKeys(options, ["type"], expression);
441
+ const typeNode = options.get("type");
442
+ return {
443
+ sourceName,
444
+ name,
445
+ type: typeNode === void 0 ? "Button" : readControlType(typeNode, expression),
446
+ binding: readBinding(bindingNode)
447
+ };
448
+ }
449
+ function getSettingCallKind(expression) {
450
+ if (isImportedIdentifier(expression, bindings.setting, checker)) return "setting";
451
+ if (ts.isPropertyAccessExpression(expression) && isImportedIdentifier(expression.expression, bindings.setting, checker)) {
452
+ if (["choice", "flags", "color"].includes(expression.name.text)) {
453
+ return expression.name.text;
454
+ }
455
+ }
456
+ return void 0;
457
+ }
458
+ function getControlCallKind(expression) {
459
+ if (isImportedIdentifier(expression, bindings.control, checker)) return "control";
460
+ if (ts.isPropertyAccessExpression(expression) && isImportedIdentifier(expression.expression, bindings.control, checker)) {
461
+ if (["raw", "hold", "tap", "multiTap", "onRelease"].includes(expression.name.text)) {
462
+ return expression.name.text;
463
+ }
464
+ }
465
+ return void 0;
466
+ }
467
+ function readBinding(expression) {
468
+ expression = unwrap(expression);
469
+ const constant = readBindingConstant(expression);
470
+ if (constant !== void 0) return { path: constant };
471
+ if (ts.isStringLiteralLike(expression)) return { path: expression.text };
472
+ const object = readObject(expression);
473
+ assertKnownKeys(object, ["path", "interactions", "processors", "groups"], expression);
474
+ const pathNode = object.get("path");
475
+ if (pathNode === void 0) throw new CompileError("Binding needs a path.", expression);
476
+ const pathValue = readBindingConstant(unwrap(pathNode)) ?? readString(pathNode);
477
+ const interactions = readOptionalString(object.get("interactions"));
478
+ const processors = readOptionalString(object.get("processors"));
479
+ const groups = readOptionalString(object.get("groups"));
480
+ return {
481
+ path: pathValue,
482
+ ...interactions === void 0 ? {} : { interactions },
483
+ ...processors === void 0 ? {} : { processors },
484
+ ...groups === void 0 ? {} : { groups }
485
+ };
486
+ }
487
+ function readBindingConstant(expression) {
488
+ if (!ts.isPropertyAccessExpression(expression)) return void 0;
489
+ if (isImportedIdentifier(expression.expression, bindings.keyboard, checker)) {
490
+ const value = keyboardPaths[expression.name.text];
491
+ if (value === void 0) throw new CompileError(`Unknown keyboard key '${expression.name.text}'.`, expression);
492
+ return `<Keyboard>/${value}`;
493
+ }
494
+ if (isImportedIdentifier(expression.expression, bindings.mouse, checker)) {
495
+ const value = mousePaths[expression.name.text];
496
+ if (value === void 0) throw new CompileError(`Unknown mouse control '${expression.name.text}'.`, expression);
497
+ return `<Mouse>/${value}`;
498
+ }
499
+ if (isImportedIdentifier(expression.expression, bindings.gamepad, checker)) {
500
+ const value = gamepadPaths[expression.name.text];
501
+ if (value === void 0) throw new CompileError(`Unknown gamepad control '${expression.name.text}'.`, expression);
502
+ return `<Gamepad>/${value}`;
503
+ }
504
+ return void 0;
505
+ }
506
+ }
507
+ function collectBindings(program2, checker) {
508
+ const result = {
509
+ defineSettings: /* @__PURE__ */ new Set(),
510
+ defineControls: /* @__PURE__ */ new Set(),
511
+ setting: /* @__PURE__ */ new Set(),
512
+ control: /* @__PURE__ */ new Set(),
513
+ keyboard: /* @__PURE__ */ new Set(),
514
+ mouse: /* @__PURE__ */ new Set(),
515
+ gamepad: /* @__PURE__ */ new Set(),
516
+ agents: /* @__PURE__ */ new Set(),
517
+ scheduler: /* @__PURE__ */ new Set(),
518
+ camera: /* @__PURE__ */ new Set(),
519
+ time: /* @__PURE__ */ new Set(),
520
+ arrays: /* @__PURE__ */ new Set(),
521
+ vector2: /* @__PURE__ */ new Set(),
522
+ vector3: /* @__PURE__ */ new Set(),
523
+ quaternion: /* @__PURE__ */ new Set(),
524
+ defineNetwork: /* @__PURE__ */ new Set(),
525
+ logger: /* @__PURE__ */ new Set(),
526
+ visuals: /* @__PURE__ */ new Set(),
527
+ input: /* @__PURE__ */ new Set(),
528
+ scope: /* @__PURE__ */ new Set()
529
+ };
530
+ for (const sourceFile of program2.getSourceFiles()) {
531
+ for (const statement of sourceFile.statements) {
532
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !statement.moduleSpecifier.text.startsWith("@killscript/sdk") || statement.importClause?.namedBindings === void 0 || !ts.isNamedImports(statement.importClause.namedBindings)) continue;
533
+ for (const specifier of statement.importClause.namedBindings.elements) {
534
+ const importedName = (specifier.propertyName ?? specifier.name).text;
535
+ const key = importNameToBinding(importedName);
536
+ if (key === void 0) continue;
537
+ const symbol = checker.getSymbolAtLocation(specifier.name);
538
+ if (symbol !== void 0) result[key].add(symbol);
539
+ }
540
+ }
541
+ }
542
+ return result;
543
+ }
544
+ function importNameToBinding(name) {
545
+ switch (name) {
546
+ case "defineSettings":
547
+ return "defineSettings";
548
+ case "defineControls":
549
+ return "defineControls";
550
+ case "setting":
551
+ return "setting";
552
+ case "control":
553
+ return "control";
554
+ case "Keyboard":
555
+ return "keyboard";
556
+ case "Mouse":
557
+ return "mouse";
558
+ case "Gamepad":
559
+ return "gamepad";
560
+ case "agents":
561
+ return "agents";
562
+ case "scheduler":
563
+ return "scheduler";
564
+ case "camera":
565
+ return "camera";
566
+ case "time":
567
+ return "time";
568
+ case "arrays":
569
+ return "arrays";
570
+ case "vector2":
571
+ return "vector2";
572
+ case "vector3":
573
+ return "vector3";
574
+ case "quaternion":
575
+ return "quaternion";
576
+ case "defineNetwork":
577
+ return "defineNetwork";
578
+ case "logger":
579
+ return "logger";
580
+ case "visuals":
581
+ return "visuals";
582
+ case "input":
583
+ return "input";
584
+ case "scope":
585
+ return "scope";
586
+ default:
587
+ return void 0;
588
+ }
589
+ }
590
+ function isImportedIdentifier(expression, symbols, checker) {
591
+ const symbol = checker.getSymbolAtLocation(expression);
592
+ return symbol !== void 0 && symbols.has(symbol);
593
+ }
594
+ function resolveAliasedSymbol(symbol, checker) {
595
+ return (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
596
+ }
597
+ function getTopLevelVariableDeclaration(call, macroName) {
598
+ const declaration = call.parent;
599
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name) || !ts.isVariableDeclarationList(declaration.parent) || !ts.isVariableStatement(declaration.parent.parent) || !ts.isSourceFile(declaration.parent.parent.parent)) {
600
+ throw new CompileError(`${macroName}() must initialize a top-level const.`, call);
601
+ }
602
+ return declaration;
603
+ }
604
+ function readDefinitionArguments(call, macroName) {
605
+ const first = call.arguments[0];
606
+ if (first === void 0) throw new CompileError(`${macroName}() needs a definition object.`, call);
607
+ if (ts.isStringLiteralLike(unwrap(first))) {
608
+ const second = call.arguments[1];
609
+ if (second === void 0) throw new CompileError(`${macroName}() needs a definition object.`, call);
610
+ const namespace = readString(first);
611
+ assertRegistrationId(namespace, `${macroName} namespace`, first);
612
+ return { namespace, object: readObjectLiteral(second) };
613
+ }
614
+ return { object: readObjectLiteral(first) };
615
+ }
616
+ function readObjectLiteral(expression) {
617
+ expression = unwrap(expression);
618
+ if (!ts.isObjectLiteralExpression(expression)) {
619
+ throw new CompileError("Compile-time definitions must use an object literal.", expression);
620
+ }
621
+ return expression;
622
+ }
623
+ function readObject(expression) {
624
+ const object = readObjectLiteral(expression);
625
+ const result = /* @__PURE__ */ new Map();
626
+ for (const property of object.properties) {
627
+ if (!ts.isPropertyAssignment(property)) {
628
+ throw new CompileError("Compile-time objects must use plain property assignments.", property);
629
+ }
630
+ const name = readPropertyName(property.name);
631
+ if (result.has(name)) throw new CompileError(`Duplicate compile-time property '${name}'.`, property);
632
+ result.set(name, property.initializer);
633
+ }
634
+ return result;
635
+ }
636
+ function readOptions(expression) {
637
+ if (expression === void 0) return {};
638
+ const object = readObject(expression);
639
+ assertKnownKeys(object, ["label", "min", "max"], expression);
640
+ const label = readOptionalString(object.get("label"));
641
+ const min = object.get("min") === void 0 ? void 0 : readNumber(object.get("min"));
642
+ const max = object.get("max") === void 0 ? void 0 : readNumber(object.get("max"));
643
+ if (label !== void 0 && (label.trim() === "" || label.length > 100)) {
644
+ throw new CompileError("Setting label must contain 1-100 characters.", expression);
645
+ }
646
+ if (min !== void 0 && max !== void 0 && min > max) {
647
+ throw new CompileError("Setting min cannot be greater than max.", expression);
648
+ }
649
+ return {
650
+ ...label === void 0 ? {} : { label },
651
+ ...min === void 0 ? {} : { min },
652
+ ...max === void 0 ? {} : { max }
653
+ };
654
+ }
655
+ function readPropertyName(name) {
656
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
657
+ return name.text;
658
+ }
659
+ throw new CompileError("Computed property names are not supported in compile-time definitions.", name);
660
+ }
661
+ function readString(expression) {
662
+ expression = unwrap(expression);
663
+ if (!ts.isStringLiteralLike(expression)) throw new CompileError("Expected a string literal.", expression);
664
+ return expression.text;
665
+ }
666
+ function readOptionalString(expression) {
667
+ return expression === void 0 ? void 0 : readString(expression);
668
+ }
669
+ function readNumber(expression) {
670
+ expression = unwrap(expression);
671
+ if (ts.isNumericLiteral(expression)) return Number(expression.text);
672
+ if (isNegativeNumber(expression)) return -Number(expression.operand.text);
673
+ throw new CompileError("Expected a number literal.", expression);
674
+ }
675
+ function isNegativeNumber(expression) {
676
+ return ts.isPrefixUnaryExpression(expression) && expression.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expression.operand);
677
+ }
678
+ function readStringArray(expression) {
679
+ expression = unwrap(expression);
680
+ if (!ts.isArrayLiteralExpression(expression)) throw new CompileError("Expected a string array literal.", expression);
681
+ return expression.elements.map((element) => readString(element));
682
+ }
683
+ function readColor(expression) {
684
+ expression = unwrap(expression);
685
+ if (ts.isStringLiteralLike(expression)) {
686
+ const normalized = expression.text.startsWith("#") ? expression.text.slice(1) : expression.text;
687
+ if (!/^[\da-f]{6}([\da-f]{2})?$/i.test(normalized)) {
688
+ throw new CompileError("Colors must use #RRGGBB or #RRGGBBAA.", expression);
689
+ }
690
+ return [
691
+ Number.parseInt(normalized.slice(0, 2), 16) / 255,
692
+ Number.parseInt(normalized.slice(2, 4), 16) / 255,
693
+ Number.parseInt(normalized.slice(4, 6), 16) / 255,
694
+ normalized.length === 8 ? Number.parseInt(normalized.slice(6, 8), 16) / 255 : 1
695
+ ];
696
+ }
697
+ if (!ts.isArrayLiteralExpression(expression) || expression.elements.length < 3 || expression.elements.length > 4) {
698
+ throw new CompileError("Colors need three or four numeric components.", expression);
699
+ }
700
+ return [
701
+ readNumber(expression.elements[0]),
702
+ readNumber(expression.elements[1]),
703
+ readNumber(expression.elements[2]),
704
+ expression.elements[3] === void 0 ? 1 : readNumber(expression.elements[3])
705
+ ];
706
+ }
707
+ function readControlType(expression, owner) {
708
+ if (expression === void 0) throw new CompileError("Control type is required.", owner);
709
+ const value = readString(expression);
710
+ if (value !== "Button" && value !== "Value" && value !== "PassThrough") {
711
+ throw new CompileError(`Unknown control type '${value}'.`, expression);
712
+ }
713
+ return value;
714
+ }
715
+ function requiredArgument(call, index) {
716
+ const value = call.arguments[index];
717
+ if (value === void 0) throw new CompileError(`Missing argument ${index + 1}.`, call);
718
+ return value;
719
+ }
720
+ function unwrap(expression) {
721
+ while (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression) || ts.isSatisfiesExpression(expression)) expression = expression.expression;
722
+ return expression;
723
+ }
724
+ function humanize(value) {
725
+ const spaced = value.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim();
726
+ return spaced.length === 0 ? value : spaced[0].toUpperCase() + spaced.slice(1);
727
+ }
728
+ function isInside(file, directory) {
729
+ const relative = path2.relative(path2.resolve(directory), path2.resolve(file));
730
+ return relative !== "" && !relative.startsWith("..") && !path2.isAbsolute(relative);
731
+ }
732
+ function assertUnique(values, key, kind) {
733
+ const seen = /* @__PURE__ */ new Set();
734
+ for (const value of values) {
735
+ const current = key(value);
736
+ if (seen.has(current)) throw new CompileError(`Duplicate ${kind} id '${current}'.`);
737
+ seen.add(current);
738
+ }
739
+ }
740
+ function assertRegistrationId(value, kind, owner) {
741
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(value)) {
742
+ throw new CompileError(`${kind} id must contain only letters, numbers, _, . and - (max 96).`, owner);
743
+ }
744
+ }
745
+ function validateChoices(values, owner) {
746
+ if (values.length === 0) throw new CompileError("Choice settings need at least one option.", owner);
747
+ if (new Set(values).size !== values.length) throw new CompileError("Choice options must be unique.", owner);
748
+ if (values.some((value) => value.length === 0 || value.length > 64)) {
749
+ throw new CompileError("Choice options must contain 1-64 characters.", owner);
750
+ }
751
+ }
752
+ function validateNumberOptions(value, options, owner) {
753
+ if (!Number.isFinite(value)) throw new CompileError("Setting values must be finite.", owner);
754
+ if (options.min !== void 0 && value < options.min) {
755
+ throw new CompileError(`Default value ${value} is below min ${options.min}.`, owner);
756
+ }
757
+ if (options.max !== void 0 && value > options.max) {
758
+ throw new CompileError(`Default value ${value} is above max ${options.max}.`, owner);
759
+ }
760
+ }
761
+ function assertKnownKeys(values, allowed, owner) {
762
+ for (const key of values.keys()) {
763
+ if (!allowed.includes(key)) throw new CompileError(`Unknown compile-time option '${key}'.`, owner);
764
+ }
765
+ }
766
+
767
+ // src/compiler/transform.ts
768
+ import * as ts2 from "typescript";
769
+ function createMacroTransformer(program2, extraction) {
770
+ const checker = program2.getTypeChecker();
771
+ return (context) => {
772
+ const factory = context.factory;
773
+ const visitor = (node) => {
774
+ if (ts2.isVariableStatement(node)) {
775
+ const declarations = node.declarationList.declarations.filter(
776
+ (declaration) => declaration.initializer === void 0 || !isDefinitionCall(declaration.initializer)
777
+ );
778
+ if (declarations.length === 0) return void 0;
779
+ if (declarations.length !== node.declarationList.declarations.length) {
780
+ return factory.updateVariableStatement(
781
+ node,
782
+ node.modifiers,
783
+ factory.updateVariableDeclarationList(node.declarationList, declarations)
784
+ );
785
+ }
786
+ }
787
+ if (ts2.isImportDeclaration(node) && ts2.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith("@killscript/sdk")) {
788
+ return void 0;
789
+ }
790
+ if (ts2.isImportDeclaration(node) && ts2.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".") && node.moduleSpecifier.text.endsWith(".js")) {
791
+ return factory.updateImportDeclaration(
792
+ node,
793
+ node.modifiers,
794
+ node.importClause,
795
+ factory.createStringLiteral(node.moduleSpecifier.text.slice(0, -3)),
796
+ node.attributes
797
+ );
798
+ }
799
+ if (ts2.isExportDeclaration(node) && node.moduleSpecifier !== void 0 && ts2.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".") && node.moduleSpecifier.text.endsWith(".js")) {
800
+ return factory.updateExportDeclaration(
801
+ node,
802
+ node.modifiers,
803
+ node.isTypeOnly,
804
+ node.exportClause,
805
+ factory.createStringLiteral(node.moduleSpecifier.text.slice(0, -3)),
806
+ node.attributes
807
+ );
808
+ }
809
+ if (ts2.isCallExpression(node)) {
810
+ const transformedCall = transformCall(node);
811
+ if (transformedCall !== void 0) return transformedCall;
812
+ }
813
+ if (ts2.isBinaryExpression(node)) {
814
+ const setting = matchSettingAccess(node.left);
815
+ if (setting?.type === "enum" && isAssignmentOperator(node.operatorToken.kind)) {
816
+ if (node.operatorToken.kind !== ts2.SyntaxKind.EqualsToken) {
817
+ throw new CompileError("Enum settings only support direct assignment.", node);
818
+ }
819
+ const options = setting.options ?? [];
820
+ const value = ts2.visitNode(node.right, visitor);
821
+ return factory.createBinaryExpression(
822
+ configAccess(setting.key),
823
+ factory.createToken(ts2.SyntaxKind.EqualsToken),
824
+ factory.createCallExpression(
825
+ factory.createParenthesizedExpression(
826
+ factory.createArrowFunction(
827
+ void 0,
828
+ void 0,
829
+ [
830
+ factory.createParameterDeclaration(
831
+ void 0,
832
+ void 0,
833
+ factory.createIdentifier("____settingValue")
834
+ )
835
+ ],
836
+ void 0,
837
+ factory.createToken(ts2.SyntaxKind.EqualsGreaterThanToken),
838
+ choiceIndexExpression(options)
839
+ )
840
+ ),
841
+ void 0,
842
+ [value]
843
+ )
844
+ );
845
+ }
846
+ }
847
+ if (ts2.isPropertyAccessExpression(node)) {
848
+ const setting = matchSettingAccess(node);
849
+ if (setting !== void 0) return settingRead(setting);
850
+ const control = matchControlAccess(node);
851
+ if (control !== void 0) return controlAction(control);
852
+ if (node.name.text === "length" && isArrayHelperResult(node.expression)) {
853
+ return helperCall("____killscriptTsArrayLength", [node.expression]);
854
+ }
855
+ }
856
+ return ts2.visitEachChild(node, visitor, context);
857
+ };
858
+ return (sourceFile) => ts2.visitNode(sourceFile, visitor);
859
+ function transformCall(node) {
860
+ if (isImportedIdentifier(node.expression, extraction.bindings.logger, checker)) {
861
+ return helperCall("____killscriptLogger", node.arguments);
862
+ }
863
+ if (isImportedIdentifier(node.expression, extraction.bindings.scope, checker)) {
864
+ if (node.arguments.length !== 0) throw new CompileError("scope() does not accept arguments.", node);
865
+ return helperCall("____killscriptScope", []);
866
+ }
867
+ if (isImportedIdentifier(node.expression, extraction.bindings.defineSettings, checker) || isImportedIdentifier(node.expression, extraction.bindings.defineControls, checker) || isImportedIdentifier(node.expression, extraction.bindings.defineNetwork, checker)) {
868
+ return factory.createObjectLiteralExpression();
869
+ }
870
+ if (ts2.isPropertyAccessExpression(node.expression)) {
871
+ const method = node.expression.name.text;
872
+ const control = matchControlAccess(node.expression.expression);
873
+ if (control !== void 0) return transformControlCall(control, method, node.arguments);
874
+ const network = matchNetworkHandle(node.expression.expression);
875
+ if (network !== void 0) return transformNetworkCall(network, method, node.arguments, node);
876
+ const owner = node.expression.expression;
877
+ if ((method === "reduce" || method === "reduceRight") && isTypeScriptArray(owner)) {
878
+ if (node.arguments.length !== 2) {
879
+ throw new CompileError(
880
+ `Array.${method}() requires an initial value in KILLSCRIPT modules.`,
881
+ node
882
+ );
883
+ }
884
+ return factory.createCallExpression(
885
+ factory.createIdentifier(
886
+ method === "reduce" ? "____killscriptTsArrayReduce" : "____killscriptTsArrayReduceRight"
887
+ ),
888
+ void 0,
889
+ [
890
+ ts2.visitNode(owner, visitor),
891
+ ts2.visitNode(node.arguments[0], visitor),
892
+ ts2.visitNode(node.arguments[1], visitor)
893
+ ]
894
+ );
895
+ }
896
+ if (isImportedIdentifier(owner, extraction.bindings.agents, checker)) {
897
+ if (method === "select") {
898
+ return helperCall("____killscriptAgentSelection", node.arguments);
899
+ }
900
+ const nativeMethod = agentMethod(method);
901
+ if (nativeMethod === void 0) throw new CompileError(`Unknown agents wrapper '${method}'.`, node);
902
+ return factory.createCallExpression(
903
+ factory.createPropertyAccessExpression(factory.createIdentifier("Agents"), nativeMethod),
904
+ void 0,
905
+ node.arguments.map((argument) => ts2.visitNode(argument, visitor))
906
+ );
907
+ }
908
+ if (isImportedIdentifier(owner, extraction.bindings.scheduler, checker)) {
909
+ const helper = {
910
+ afterTicks: "____killscriptSchedulerAfterTicks",
911
+ every: "____killscriptSchedulerEvery",
912
+ everyTicks: "____killscriptSchedulerEveryTicks",
913
+ until: "____killscriptSchedulerUntil",
914
+ debounce: "____killscriptSchedulerDebounce",
915
+ throttle: "____killscriptSchedulerThrottle"
916
+ }[method];
917
+ if (helper !== void 0) return helperCall(helper, node.arguments);
918
+ const nativeMethod = schedulerMethod(method);
919
+ if (nativeMethod === void 0) throw new CompileError(`Unknown scheduler wrapper '${method}'.`, node);
920
+ return factory.createCallExpression(
921
+ factory.createPropertyAccessExpression(factory.createIdentifier("Scheduler"), nativeMethod),
922
+ void 0,
923
+ node.arguments.map((argument) => ts2.visitNode(argument, visitor))
924
+ );
925
+ }
926
+ if (isImportedIdentifier(owner, extraction.bindings.camera, checker)) {
927
+ if (method === "main") {
928
+ return factory.createPropertyAccessExpression(factory.createIdentifier("Cameras"), "Main");
929
+ }
930
+ const nativeMethod = { create: "CreateCamera", remove: "RemoveCamera" }[method];
931
+ if (nativeMethod === void 0) throw new CompileError(`Unknown camera wrapper '${method}'.`, node);
932
+ return factory.createCallExpression(
933
+ factory.createPropertyAccessExpression(factory.createIdentifier("Cameras"), nativeMethod),
934
+ void 0,
935
+ node.arguments.map((argument) => ts2.visitNode(argument, visitor))
936
+ );
937
+ }
938
+ if (isImportedIdentifier(owner, extraction.bindings.time, checker)) {
939
+ const property = { seconds: "Seconds", tick: "Tick" }[method];
940
+ if (property !== void 0) {
941
+ return factory.createPropertyAccessExpression(factory.createIdentifier("Time"), property);
942
+ }
943
+ if (method === "ceilTicks") {
944
+ const seconds = node.arguments[0];
945
+ if (seconds === void 0) throw new CompileError("time.ceilTicks() needs seconds.", node);
946
+ return factory.createCallExpression(
947
+ factory.createIdentifier("____killscriptCeilTicks"),
948
+ void 0,
949
+ [ts2.visitNode(seconds, visitor)]
950
+ );
951
+ }
952
+ const nativeMethod = { toTicks: "SecondsToTick", toSeconds: "TickToSeconds" }[method];
953
+ if (nativeMethod === void 0) throw new CompileError(`Unknown time wrapper '${method}'.`, node);
954
+ return factory.createCallExpression(
955
+ factory.createPropertyAccessExpression(factory.createIdentifier("Time"), nativeMethod),
956
+ void 0,
957
+ node.arguments.map((argument) => ts2.visitNode(argument, visitor))
958
+ );
959
+ }
960
+ if (isImportedIdentifier(owner, extraction.bindings.arrays, checker)) {
961
+ const helper = {
962
+ forEach: "____killscriptArrayForEach",
963
+ some: "____killscriptArraySome",
964
+ every: "____killscriptArrayEvery",
965
+ find: "____killscriptArrayFind",
966
+ map: "____killscriptArrayMap",
967
+ filter: "____killscriptArrayFilter",
968
+ toArray: "____killscriptArrayToArray",
969
+ first: "____killscriptArrayFirst",
970
+ last: "____killscriptArrayLast"
971
+ }[method];
972
+ if (helper === void 0) throw new CompileError(`Unknown arrays helper '${method}'.`, node);
973
+ return factory.createCallExpression(
974
+ factory.createIdentifier(helper),
975
+ void 0,
976
+ node.arguments.map((argument) => ts2.visitNode(argument, visitor))
977
+ );
978
+ }
979
+ if (isImportedIdentifier(owner, extraction.bindings.visuals, checker)) {
980
+ const helper = {
981
+ line: "____killscriptVisualLine",
982
+ overlay: "____killscriptVisualOverlay",
983
+ remove: "____killscriptVisualRemove"
984
+ }[method];
985
+ if (helper === void 0) throw new CompileError(`Unknown visuals helper '${method}'.`, node);
986
+ return helperCall(helper, node.arguments);
987
+ }
988
+ if (isImportedIdentifier(owner, extraction.bindings.input, checker)) {
989
+ const helper = {
990
+ anyPressed: "____killscriptInputAnyPressed",
991
+ allPressed: "____killscriptInputAllPressed",
992
+ onAnyPressed: "____killscriptInputOnAnyPressed",
993
+ onChord: "____killscriptInputOnChord",
994
+ toggle: "____killscriptInputToggle"
995
+ }[method];
996
+ if (helper === void 0) throw new CompileError(`Unknown input helper '${method}'.`, node);
997
+ return helperCall(helper, node.arguments);
998
+ }
999
+ const operator = operatorForWrapper(owner, method);
1000
+ if (operator !== void 0) {
1001
+ if (node.arguments.length !== 2) throw new CompileError(`${operator.name}() needs two values.`, node);
1002
+ return factory.createBinaryExpression(
1003
+ ts2.visitNode(node.arguments[0], visitor),
1004
+ operator.token,
1005
+ ts2.visitNode(node.arguments[1], visitor)
1006
+ );
1007
+ }
1008
+ }
1009
+ return void 0;
1010
+ }
1011
+ function helperCall(name, args) {
1012
+ return factory.createCallExpression(
1013
+ factory.createIdentifier(name),
1014
+ void 0,
1015
+ args.map((argument) => ts2.visitNode(argument, visitor))
1016
+ );
1017
+ }
1018
+ function isTypeScriptArray(expression) {
1019
+ const type = checker.getTypeAtLocation(expression);
1020
+ return checker.isArrayType(type) || checker.isTupleType(type);
1021
+ }
1022
+ function isArrayHelperResult(expression) {
1023
+ if (!ts2.isCallExpression(expression) || !ts2.isPropertyAccessExpression(expression.expression)) {
1024
+ return false;
1025
+ }
1026
+ const method = expression.expression.name.text;
1027
+ return ["map", "filter", "toArray"].includes(method) && isImportedIdentifier(expression.expression.expression, extraction.bindings.arrays, checker);
1028
+ }
1029
+ function isDefinitionCall(expression) {
1030
+ return ts2.isCallExpression(expression) && (isImportedIdentifier(expression.expression, extraction.bindings.defineSettings, checker) || isImportedIdentifier(expression.expression, extraction.bindings.defineControls, checker) || isImportedIdentifier(expression.expression, extraction.bindings.defineNetwork, checker));
1031
+ }
1032
+ function transformNetworkCall(network, method, args, owner) {
1033
+ if (method !== "send" && method !== "on") {
1034
+ throw new CompileError(`Unknown network method '${method}'.`, owner);
1035
+ }
1036
+ if (args.length !== 2) throw new CompileError(`network.${method}() needs a kind and value.`, owner);
1037
+ const kind = args[0];
1038
+ if (!ts2.isStringLiteralLike(kind)) {
1039
+ throw new CompileError("Network message kind must be a string literal.", kind);
1040
+ }
1041
+ const qualified = network.namespace === void 0 ? kind.text : `${network.namespace}.${kind.text}`;
1042
+ return factory.createCallExpression(
1043
+ factory.createIdentifier(method === "send" ? "____killscriptNetworkSend" : "____killscriptNetworkOn"),
1044
+ void 0,
1045
+ [
1046
+ factory.createStringLiteral(qualified),
1047
+ ts2.visitNode(args[1], visitor)
1048
+ ]
1049
+ );
1050
+ }
1051
+ function transformControlCall(control, method, args) {
1052
+ const action = controlAction(control);
1053
+ if (method === "native") return action;
1054
+ const helper = {
1055
+ onPressed: "____killscriptControlOnPressed",
1056
+ onChanged: "____killscriptControlOnChanged",
1057
+ onReleased: "____killscriptControlOnReleased",
1058
+ whilePressed: "____killscriptControlWhilePressed",
1059
+ isPressed: "____killscriptControlIsPressed"
1060
+ }[method];
1061
+ if (helper !== void 0) {
1062
+ return factory.createCallExpression(
1063
+ factory.createIdentifier(helper),
1064
+ void 0,
1065
+ [action, ...args.map((argument) => ts2.visitNode(argument, visitor))]
1066
+ );
1067
+ }
1068
+ throw new CompileError(`Unknown control method '${method}'.`);
1069
+ }
1070
+ function controlAction(control) {
1071
+ return factory.createCallExpression(
1072
+ factory.createPropertyAccessExpression(factory.createIdentifier("InputActions"), "FindAction"),
1073
+ void 0,
1074
+ [factory.createStringLiteral(control.name)]
1075
+ );
1076
+ }
1077
+ function matchSettingAccess(node) {
1078
+ if (!ts2.isPropertyAccessExpression(node)) return void 0;
1079
+ const handle = resolveHandle(node.expression, extraction.settingsHandles);
1080
+ return handle?.properties.get(node.name.text);
1081
+ }
1082
+ function matchControlAccess(node, seen = /* @__PURE__ */ new Set()) {
1083
+ if (ts2.isPropertyAccessExpression(node)) {
1084
+ const handle = resolveHandle(node.expression, extraction.controlsHandles);
1085
+ return handle?.properties.get(node.name.text);
1086
+ }
1087
+ const symbol = symbolForExpression(node);
1088
+ if (symbol === void 0 || seen.has(symbol)) return void 0;
1089
+ seen.add(symbol);
1090
+ const initializer = variableInitializer(symbol);
1091
+ return initializer === void 0 ? void 0 : matchControlAccess(initializer, seen);
1092
+ }
1093
+ function matchNetworkHandle(node) {
1094
+ return resolveHandle(node, extraction.networkHandles);
1095
+ }
1096
+ function operatorForWrapper(owner, method) {
1097
+ if (isImportedIdentifier(owner, extraction.bindings.vector2, checker)) {
1098
+ if (method === "add") return { name: "vector2.add", token: ts2.SyntaxKind.PlusToken };
1099
+ if (method === "subtract") return { name: "vector2.subtract", token: ts2.SyntaxKind.MinusToken };
1100
+ }
1101
+ if (isImportedIdentifier(owner, extraction.bindings.vector3, checker)) {
1102
+ if (method === "add") return { name: "vector3.add", token: ts2.SyntaxKind.PlusToken };
1103
+ if (method === "subtract") return { name: "vector3.subtract", token: ts2.SyntaxKind.MinusToken };
1104
+ }
1105
+ if (isImportedIdentifier(owner, extraction.bindings.quaternion, checker)) {
1106
+ if (method === "multiply") return { name: "quaternion.multiply", token: ts2.SyntaxKind.AsteriskToken };
1107
+ if (method === "rotate") return { name: "quaternion.rotate", token: ts2.SyntaxKind.AsteriskToken };
1108
+ }
1109
+ return void 0;
1110
+ }
1111
+ function resolveHandle(expression, handles, seen = /* @__PURE__ */ new Set()) {
1112
+ const symbol = symbolForExpression(expression);
1113
+ if (symbol === void 0) return void 0;
1114
+ const direct = handles.get(symbol);
1115
+ if (direct !== void 0 || seen.has(symbol)) return direct;
1116
+ seen.add(symbol);
1117
+ const initializer = variableInitializer(symbol);
1118
+ return initializer === void 0 ? void 0 : resolveHandle(initializer, handles, seen);
1119
+ }
1120
+ function symbolForExpression(expression) {
1121
+ const target = ts2.isPropertyAccessExpression(expression) ? expression.name : expression;
1122
+ const symbol = checker.getSymbolAtLocation(target);
1123
+ return symbol === void 0 ? void 0 : resolveAliasedSymbol(symbol, checker);
1124
+ }
1125
+ function variableInitializer(symbol) {
1126
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
1127
+ return declaration !== void 0 && ts2.isVariableDeclaration(declaration) ? declaration.initializer : void 0;
1128
+ }
1129
+ function settingRead(setting) {
1130
+ const value = configAccess(setting.key);
1131
+ if (setting.type !== "enum") return value;
1132
+ return factory.createElementAccessExpression(
1133
+ factory.createArrayLiteralExpression(
1134
+ (setting.options ?? []).map((item) => factory.createStringLiteral(item))
1135
+ ),
1136
+ factory.createBinaryExpression(
1137
+ factory.createAsExpression(value, factory.createKeywordTypeNode(ts2.SyntaxKind.NumberKeyword)),
1138
+ factory.createToken(ts2.SyntaxKind.PlusToken),
1139
+ factory.createNumericLiteral(1)
1140
+ )
1141
+ );
1142
+ }
1143
+ function configAccess(key) {
1144
+ return factory.createElementAccessExpression(
1145
+ factory.createIdentifier("Config"),
1146
+ factory.createStringLiteral(key)
1147
+ );
1148
+ }
1149
+ function choiceIndexExpression(options) {
1150
+ let result = factory.createNumericLiteral(0);
1151
+ for (let index = options.length - 1; index >= 0; index -= 1) {
1152
+ result = factory.createConditionalExpression(
1153
+ factory.createBinaryExpression(
1154
+ factory.createIdentifier("____settingValue"),
1155
+ factory.createToken(ts2.SyntaxKind.EqualsEqualsEqualsToken),
1156
+ factory.createStringLiteral(options[index])
1157
+ ),
1158
+ factory.createToken(ts2.SyntaxKind.QuestionToken),
1159
+ factory.createNumericLiteral(index),
1160
+ factory.createToken(ts2.SyntaxKind.ColonToken),
1161
+ result
1162
+ );
1163
+ }
1164
+ return result;
1165
+ }
1166
+ };
1167
+ }
1168
+ function isAssignmentOperator(kind) {
1169
+ return kind >= ts2.SyntaxKind.FirstAssignment && kind <= ts2.SyntaxKind.LastAssignment;
1170
+ }
1171
+ function agentMethod(method) {
1172
+ return {
1173
+ local: "GetLocalAgent",
1174
+ localOrSpectated: "GetLocalOrSpectatedAgent",
1175
+ all: "GetAll",
1176
+ allies: "GetAllies",
1177
+ enemies: "GetEnemies"
1178
+ }[method];
1179
+ }
1180
+ function schedulerMethod(method) {
1181
+ return {
1182
+ frame: "OnFrame",
1183
+ tick: "OnTick",
1184
+ after: "Schedule"
1185
+ }[method];
1186
+ }
1187
+
1188
+ // src/compiler/compile-context.ts
1189
+ function compileContext(projectDir, context) {
1190
+ const sourceRoot = path3.join(projectDir, "src");
1191
+ const entry = path3.join(sourceRoot, context, "main.ts");
1192
+ const bundleName = context === "client" ? "main.lua" : "server.lua";
1193
+ const options = {
1194
+ target: ts3.ScriptTarget.ES2022,
1195
+ lib: ["lib.es2022.d.ts"],
1196
+ module: ts3.ModuleKind.ESNext,
1197
+ moduleResolution: ts3.ModuleResolutionKind.Bundler,
1198
+ strict: true,
1199
+ skipLibCheck: true,
1200
+ esModuleInterop: true,
1201
+ rootDir: sourceRoot,
1202
+ outDir: path3.join(projectDir, ".killscript", "compiler", context),
1203
+ types: [`@killscript/types/${context}`],
1204
+ luaTarget: LuaTarget.Lua54,
1205
+ luaLibImport: LuaLibImportKind.Inline,
1206
+ luaBundle: bundleName,
1207
+ luaBundleEntry: entry,
1208
+ noImplicitGlobalVariables: false,
1209
+ noHeader: false,
1210
+ sourceMap: false,
1211
+ declaration: false
1212
+ };
1213
+ const program2 = ts3.createProgram({ rootNames: [entry], options });
1214
+ validateProgram(program2, projectDir, sourceRoot, context);
1215
+ const diagnostics = ts3.getPreEmitDiagnostics(program2);
1216
+ const errors = diagnostics.filter((diagnostic) => diagnostic.category === ts3.DiagnosticCategory.Error);
1217
+ if (errors.length > 0) throw new CompileError(formatDiagnostics(errors, projectDir));
1218
+ const extraction = extractMacros(program2, sourceRoot);
1219
+ const files = /* @__PURE__ */ new Map();
1220
+ const result = new Transpiler().emit({
1221
+ program: program2,
1222
+ customTransformers: { before: [createMacroTransformer(program2, extraction)] },
1223
+ writeFile(fileName, data) {
1224
+ files.set(path3.basename(fileName), data);
1225
+ }
1226
+ });
1227
+ const emitErrors = result.diagnostics.filter(
1228
+ (diagnostic) => diagnostic.category === ts3.DiagnosticCategory.Error
1229
+ );
1230
+ if (emitErrors.length > 0) {
1231
+ throw new CompileError(formatDiagnostics(emitErrors, projectDir));
1232
+ }
1233
+ if (result.emitSkipped) throw new CompileError(`${context} Lua emit was skipped.`);
1234
+ const lua = files.get(bundleName);
1235
+ if (lua === void 0) {
1236
+ throw new CompileError(
1237
+ `${context} compilation did not emit ${bundleName}. Emitted: ${[...files.keys()].join(", ")}`
1238
+ );
1239
+ }
1240
+ const finalized = addCompilerRuntime(makeBundleSandboxSafe(lua), context, extraction);
1241
+ assertSandboxSafe(finalized, context);
1242
+ return { lua: finalized, extraction };
1243
+ }
1244
+ function validateProgram(program2, projectDir, sourceRoot, context) {
1245
+ for (const sourceFile of program2.getSourceFiles()) {
1246
+ if (sourceFile.isDeclarationFile || !isInside2(sourceFile.fileName, sourceRoot)) continue;
1247
+ const visit = (node) => {
1248
+ if (ts3.isTryStatement(node)) {
1249
+ throw new CompileError("try/catch is unavailable because the game sandbox does not expose pcall().", node);
1250
+ }
1251
+ if (ts3.isTypeOfExpression(node)) {
1252
+ throw new CompileError("typeof is unavailable because the game sandbox does not expose type().", node);
1253
+ }
1254
+ if (ts3.isThrowStatement(node)) {
1255
+ throw new CompileError("throw is unavailable because the game sandbox does not expose error().", node);
1256
+ }
1257
+ if (ts3.isAwaitExpression(node) || ts3.isYieldExpression(node) || hasAsyncModifier(node)) {
1258
+ throw new CompileError("async, await and generators are not supported by the KILLSCRIPT runtime.", node);
1259
+ }
1260
+ if (ts3.isCallExpression(node) && ts3.isIdentifier(node.expression) && ["pcall", "xpcall", "type", "load", "loadfile", "dofile", "require", "eval"].includes(node.expression.text)) {
1261
+ throw new CompileError(`Direct ${node.expression.text}() calls are not supported in module source.`, node);
1262
+ }
1263
+ if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword) {
1264
+ throw new CompileError("Dynamic import() is not supported; use static imports so the compiler can bundle them.", node);
1265
+ }
1266
+ ts3.forEachChild(node, visit);
1267
+ };
1268
+ visit(sourceFile);
1269
+ for (const statement of sourceFile.statements) {
1270
+ if (!ts3.isImportDeclaration(statement) || !ts3.isStringLiteral(statement.moduleSpecifier)) continue;
1271
+ const specifier = statement.moduleSpecifier.text;
1272
+ const typeOnly = isTypeOnlyImport(statement);
1273
+ if (specifier === `@killscript/sdk/${context}` || specifier === "@killscript/sdk" || specifier.startsWith(".")) {
1274
+ if (specifier.startsWith(".")) {
1275
+ const resolved = path3.resolve(path3.dirname(sourceFile.fileName), specifier);
1276
+ if (!isInside2(resolved, sourceRoot)) {
1277
+ throw new CompileError("Relative implementation imports must stay inside src/.", statement);
1278
+ }
1279
+ const opposite = context === "client" ? "server" : "client";
1280
+ if (!typeOnly && isInside2(resolved, path3.join(sourceRoot, opposite))) {
1281
+ throw new CompileError(`${context} code cannot import ${opposite} implementation files.`, statement);
1282
+ }
1283
+ }
1284
+ continue;
1285
+ }
1286
+ if (typeOnly) continue;
1287
+ throw new CompileError(
1288
+ `Runtime package import '${specifier}' cannot be bundled safely. Keep runtime code in src/ and use relative imports.`,
1289
+ statement
1290
+ );
1291
+ }
1292
+ }
1293
+ function hasAsyncModifier(node) {
1294
+ return ts3.canHaveModifiers(node) && (ts3.getModifiers(node)?.some((modifier) => modifier.kind === ts3.SyntaxKind.AsyncKeyword) ?? false);
1295
+ }
1296
+ function isTypeOnlyImport(node) {
1297
+ const clause = node.importClause;
1298
+ if (clause === void 0 || clause.name !== void 0) return clause?.isTypeOnly ?? false;
1299
+ if (clause.isTypeOnly) return true;
1300
+ const bindings = clause.namedBindings;
1301
+ return bindings !== void 0 && ts3.isNamedImports(bindings) && bindings.elements.length > 0 && bindings.elements.every((element) => element.isTypeOnly);
1302
+ }
1303
+ }
1304
+ function addCompilerRuntime(lua, context, extraction) {
1305
+ const blocks = [];
1306
+ if (lua.includes("____killscriptArray")) blocks.push(ARRAY_RUNTIME);
1307
+ if (lua.includes("____killscriptTsArrayReduce") || lua.includes("____killscriptTsArrayLength")) blocks.push(TS_ARRAY_RUNTIME);
1308
+ if (lua.includes("____killscriptCeilTicks")) blocks.push(TIME_RUNTIME);
1309
+ if (lua.includes("____killscriptLogger")) blocks.push(LOGGER_RUNTIME);
1310
+ if (lua.includes("____killscriptAgentSelection")) blocks.push(AGENT_SELECTION_RUNTIME);
1311
+ if (lua.includes("____killscriptScheduler")) blocks.push(schedulerRuntime(context));
1312
+ if (lua.includes("____killscriptInput") || lua.includes("____killscriptControl")) {
1313
+ blocks.push(INPUT_RUNTIME);
1314
+ }
1315
+ const needsScope = lua.includes("____killscriptScope");
1316
+ if (lua.includes("____killscriptVisual") || needsScope && context === "client") {
1317
+ blocks.push(VISUAL_RUNTIME);
1318
+ }
1319
+ if (needsScope) blocks.push(scopeRuntime(context));
1320
+ if (extraction.networkHandles.size > 0) blocks.push(networkRuntime(context));
1321
+ return blocks.length === 0 ? lua : `${blocks.join("\n\n")}
1322
+
1323
+ ${lua}`;
1324
+ }
1325
+ var ARRAY_RUNTIME = `local function ____killscriptArrayForEach(_, values, callback)
1326
+ for index = 1, values.Length do callback(nil, values[index], index) end
1327
+ end
1328
+
1329
+ local function ____killscriptArraySome(_, values, predicate)
1330
+ for index = 1, values.Length do
1331
+ if predicate(nil, values[index], index) then return true end
1332
+ end
1333
+ return false
1334
+ end
1335
+
1336
+ local function ____killscriptArrayEvery(_, values, predicate)
1337
+ for index = 1, values.Length do
1338
+ if not predicate(nil, values[index], index) then return false end
1339
+ end
1340
+ return true
1341
+ end
1342
+
1343
+ local function ____killscriptArrayFind(_, values, predicate)
1344
+ for index = 1, values.Length do
1345
+ local value = values[index]
1346
+ if predicate(nil, value, index) then return value end
1347
+ end
1348
+ return nil
1349
+ end
1350
+
1351
+ local function ____killscriptArrayMap(_, values, transform)
1352
+ local result = {}
1353
+ for index = 1, values.Length do result[index] = transform(nil, values[index], index) end
1354
+ return result
1355
+ end
1356
+
1357
+ local function ____killscriptArrayFilter(_, values, predicate)
1358
+ local result = {}
1359
+ for index = 1, values.Length do
1360
+ local value = values[index]
1361
+ if predicate(nil, value, index) then result[#result + 1] = value end
1362
+ end
1363
+ return result
1364
+ end
1365
+
1366
+ local function ____killscriptArrayToArray(_, values)
1367
+ local result = {}
1368
+ for index = 1, values.Length do result[index] = values[index] end
1369
+ return result
1370
+ end
1371
+
1372
+ local function ____killscriptArrayFirst(_, values)
1373
+ if values.Length == 0 then return nil end
1374
+ return values[1]
1375
+ end
1376
+
1377
+ local function ____killscriptArrayLast(_, values)
1378
+ if values.Length == 0 then return nil end
1379
+ return values[values.Length]
1380
+ end`;
1381
+ var TIME_RUNTIME = `local function ____killscriptCeilTicks(_, seconds)
1382
+ return math.ceil(seconds / Time:TickToSeconds(1))
1383
+ end`;
1384
+ var LOGGER_RUNTIME = `local function ____killscriptLogger(_, name)
1385
+ local prefix = "[" .. name .. "] "
1386
+ local seen = {}
1387
+ local lastPrintedAt = {}
1388
+ local result = {}
1389
+
1390
+ function result:info(message) print(prefix .. message) end
1391
+ function result:debug(message) print(prefix .. "[DEBUG] " .. message) end
1392
+ function result:warn(message) print(prefix .. "[WARN] " .. message) end
1393
+
1394
+ function result:batch(lines)
1395
+ local output = prefix
1396
+ for index = 1, #lines do
1397
+ if index > 1 then output = output .. "\\n" end
1398
+ output = output .. lines[index]
1399
+ end
1400
+ print(output)
1401
+ end
1402
+
1403
+ function result:once(key, message)
1404
+ if seen[key] then return end
1405
+ seen[key] = true
1406
+ print(prefix .. message)
1407
+ end
1408
+
1409
+ function result:throttled(key, seconds, message)
1410
+ local now = Time.Seconds
1411
+ local previous = lastPrintedAt[key]
1412
+ if previous ~= nil and now - previous < seconds then return end
1413
+ lastPrintedAt[key] = now
1414
+ print(prefix .. message)
1415
+ end
1416
+
1417
+ return result
1418
+ end`;
1419
+ var AGENT_SELECTION_RUNTIME = `local function ____killscriptAgentSelection(_, source)
1420
+ if source == nil then source = Agents:GetAll() end
1421
+ local values = {}
1422
+ local sourceLength = source.Length
1423
+ if sourceLength == nil then sourceLength = #source end
1424
+ for index = 1, sourceLength do values[index] = source[index] end
1425
+
1426
+ local result = {}
1427
+
1428
+ local function filter(predicate)
1429
+ local filtered = {}
1430
+ for index = 1, #values do
1431
+ local agent = values[index]
1432
+ if predicate(agent) then filtered[#filtered + 1] = agent end
1433
+ end
1434
+ values = filtered
1435
+ return result
1436
+ end
1437
+
1438
+ function result:where(predicate)
1439
+ return filter(function(agent) return predicate(nil, agent) end)
1440
+ end
1441
+
1442
+ function result:alive()
1443
+ return filter(function(agent)
1444
+ if not agent.IsVisible then return false end
1445
+ local stats = agent.Stats
1446
+ return stats ~= nil and stats.IsAlive
1447
+ end)
1448
+ end
1449
+
1450
+ function result:dead()
1451
+ return filter(function(agent)
1452
+ if not agent.IsVisible then return false end
1453
+ local stats = agent.Stats
1454
+ return stats ~= nil and not stats.IsAlive
1455
+ end)
1456
+ end
1457
+
1458
+ function result:visible()
1459
+ return filter(function(agent)
1460
+ if not agent.IsVisible then return false end
1461
+ local hitboxes = agent:GetHitboxes()
1462
+ for index = 1, hitboxes.Length do
1463
+ if hitboxes[index].IsVisible then return true end
1464
+ end
1465
+ return false
1466
+ end)
1467
+ end
1468
+
1469
+ function result:within(origin, distance)
1470
+ return filter(function(agent)
1471
+ if not agent.IsVisible then return false end
1472
+ local movement = agent.Movement
1473
+ return movement ~= nil and origin:Distance(movement.Position) <= distance
1474
+ end)
1475
+ end
1476
+
1477
+ function result:exclude(excluded)
1478
+ if excluded == nil then return result end
1479
+ return filter(function(agent) return agent ~= excluded end)
1480
+ end
1481
+
1482
+ function result:team(team)
1483
+ return filter(function(agent)
1484
+ return agent.IsVisible and agent.Team == team
1485
+ end)
1486
+ end
1487
+
1488
+ function result:first()
1489
+ return values[1]
1490
+ end
1491
+
1492
+ function result:nearestTo(origin)
1493
+ local nearest = nil
1494
+ local nearestDistance = nil
1495
+ for index = 1, #values do
1496
+ local agent = values[index]
1497
+ if agent.IsVisible then
1498
+ local movement = agent.Movement
1499
+ if movement ~= nil then
1500
+ local distance = origin:Distance(movement.Position)
1501
+ if nearestDistance == nil or distance < nearestDistance then
1502
+ nearest = agent
1503
+ nearestDistance = distance
1504
+ end
1505
+ end
1506
+ end
1507
+ end
1508
+ return nearest
1509
+ end
1510
+
1511
+ function result:count() return #values end
1512
+ function result:toArray()
1513
+ local copy = {}
1514
+ for index = 1, #values do copy[index] = values[index] end
1515
+ return copy
1516
+ end
1517
+ return result
1518
+ end`;
1519
+ function schedulerRuntime(context) {
1520
+ const updateMethod = context === "client" ? "OnFrame" : "OnTick";
1521
+ return `local ____killscriptSchedulerDebounces = {}
1522
+ local ____killscriptSchedulerThrottles = {}
1523
+
1524
+ local function ____killscriptSchedulerAfterTicks(_, ticks, callback)
1525
+ local target = Time.Tick + ticks
1526
+ local subscription = nil
1527
+ subscription = Scheduler:${updateMethod}(function()
1528
+ if Time.Tick < target then return end
1529
+ subscription:Cancel()
1530
+ callback(nil)
1531
+ end)
1532
+ return subscription
1533
+ end
1534
+
1535
+ local function ____killscriptSchedulerEvery(_, seconds, callback)
1536
+ local nextAt = Time.Seconds + seconds
1537
+ return Scheduler:${updateMethod}(function()
1538
+ local now = Time.Seconds
1539
+ if now < nextAt then return end
1540
+ nextAt = now + seconds
1541
+ callback(nil)
1542
+ end)
1543
+ end
1544
+
1545
+ local function ____killscriptSchedulerEveryTicks(_, ticks, callback)
1546
+ local nextAt = Time.Tick + ticks
1547
+ return Scheduler:${updateMethod}(function()
1548
+ local now = Time.Tick
1549
+ if now < nextAt then return end
1550
+ nextAt = now + ticks
1551
+ callback(nil)
1552
+ end)
1553
+ end
1554
+
1555
+ local function ____killscriptSchedulerUntil(_, predicate, callback)
1556
+ local subscription = nil
1557
+ subscription = Scheduler:${updateMethod}(function()
1558
+ if not predicate(nil) then return end
1559
+ subscription:Cancel()
1560
+ callback(nil)
1561
+ end)
1562
+ return subscription
1563
+ end
1564
+
1565
+ local function ____killscriptSchedulerDebounce(_, key, seconds, callback)
1566
+ local generation = (____killscriptSchedulerDebounces[key] or 0) + 1
1567
+ ____killscriptSchedulerDebounces[key] = generation
1568
+ Scheduler:Schedule(seconds, function()
1569
+ if ____killscriptSchedulerDebounces[key] ~= generation then return end
1570
+ ____killscriptSchedulerDebounces[key] = nil
1571
+ callback(nil)
1572
+ end)
1573
+ end
1574
+
1575
+ local function ____killscriptSchedulerThrottle(_, key, seconds, callback)
1576
+ local now = Time.Seconds
1577
+ local availableAt = ____killscriptSchedulerThrottles[key]
1578
+ if availableAt ~= nil and now < availableAt then return false end
1579
+ ____killscriptSchedulerThrottles[key] = now + seconds
1580
+ callback(nil)
1581
+ return true
1582
+ end`;
1583
+ }
1584
+ var INPUT_RUNTIME = `local function ____killscriptInputAnyPressed(_, actions)
1585
+ for index = 1, #actions do
1586
+ local action = actions[index]
1587
+ if action ~= nil and action:IsPressed() then return true end
1588
+ end
1589
+ return false
1590
+ end
1591
+
1592
+ local function ____killscriptInputAllPressed(_, actions)
1593
+ if #actions == 0 then return false end
1594
+ for index = 1, #actions do
1595
+ local action = actions[index]
1596
+ if action == nil or not action:IsPressed() then return false end
1597
+ end
1598
+ return true
1599
+ end
1600
+
1601
+ local function ____killscriptControlOnPressed(_, action, callback)
1602
+ if action == nil then return end
1603
+ action:OnPerformed(callback)
1604
+ end
1605
+
1606
+ local function ____killscriptControlIsPressed(_, action)
1607
+ return action ~= nil and action:IsPressed()
1608
+ end
1609
+
1610
+ local function ____killscriptControlOnChanged(_, action, callback)
1611
+ local previous = action ~= nil and action:IsPressed()
1612
+ return Scheduler:OnFrame(function()
1613
+ local current = action ~= nil and action:IsPressed()
1614
+ if current == previous then return end
1615
+ previous = current
1616
+ callback(nil, current)
1617
+ end)
1618
+ end
1619
+
1620
+ local function ____killscriptControlOnReleased(_, action, callback)
1621
+ local previous = action ~= nil and action:IsPressed()
1622
+ return Scheduler:OnFrame(function()
1623
+ local current = action ~= nil and action:IsPressed()
1624
+ if previous and not current then callback(nil) end
1625
+ previous = current
1626
+ end)
1627
+ end
1628
+
1629
+ local function ____killscriptControlWhilePressed(_, action, callback)
1630
+ return Scheduler:OnFrame(function()
1631
+ if action ~= nil and action:IsPressed() then callback(nil) end
1632
+ end)
1633
+ end
1634
+
1635
+ local function ____killscriptInputOnAnyPressed(_, actions, callback)
1636
+ local previous = ____killscriptInputAnyPressed(nil, actions)
1637
+ return Scheduler:OnFrame(function()
1638
+ local current = ____killscriptInputAnyPressed(nil, actions)
1639
+ if current and not previous then callback(nil) end
1640
+ previous = current
1641
+ end)
1642
+ end
1643
+
1644
+ local function ____killscriptInputOnChord(_, actions, callback)
1645
+ local previous = ____killscriptInputAllPressed(nil, actions)
1646
+ return Scheduler:OnFrame(function()
1647
+ local current = ____killscriptInputAllPressed(nil, actions)
1648
+ if current and not previous then callback(nil) end
1649
+ previous = current
1650
+ end)
1651
+ end
1652
+
1653
+ local function ____killscriptInputToggle(_, action, initialValue, callback)
1654
+ local result = { value = initialValue == true }
1655
+
1656
+ local function notify()
1657
+ if callback ~= nil then callback(nil, result.value) end
1658
+ end
1659
+
1660
+ function result:set(value)
1661
+ result.value = value == true
1662
+ notify()
1663
+ end
1664
+
1665
+ function result:flip()
1666
+ result.value = not result.value
1667
+ notify()
1668
+ return result.value
1669
+ end
1670
+
1671
+ if action ~= nil then
1672
+ action:OnPerformed(function() result:flip() end)
1673
+ end
1674
+ return result
1675
+ end`;
1676
+ var VISUAL_RUNTIME = `local ____killscriptRemovedWorldObjects = {}
1677
+
1678
+ local function ____killscriptVisualRemove(_, object)
1679
+ if object == nil or ____killscriptRemovedWorldObjects[object] then return end
1680
+ ____killscriptRemovedWorldObjects[object] = true
1681
+ WorldVisuals:RemoveObject(object)
1682
+ end
1683
+
1684
+ local function ____killscriptVisualLine(_, options)
1685
+ local object = WorldVisuals:CreateLineRenderer()
1686
+ if object == nil then return nil end
1687
+ local points = options.points
1688
+ object:SetPositionCount(#points)
1689
+ for index = 1, #points do object:SetPosition(index - 1, points[index]) end
1690
+ if options.color ~= nil then object:SetColor(options.color) end
1691
+ if options.width ~= nil then object:SetWidth(options.width) end
1692
+ if options.progress ~= nil then object:SetProgress(options.progress) end
1693
+ if options.patternEnabled ~= nil then object:SetPatternEnabled(options.patternEnabled) end
1694
+ if options.patternRepeat ~= nil then object:SetPatternRepeat(options.patternRepeat) end
1695
+ if options.patternTexture ~= nil then object:SetPatternTexture(options.patternTexture) end
1696
+ if options.occludedBrightness ~= nil or options.occludedTransparency ~= nil then
1697
+ object:SetOccludedVisibility(
1698
+ options.occludedBrightness or 1,
1699
+ options.occludedTransparency or 1
1700
+ )
1701
+ end
1702
+ if options.duration ~= nil and options.duration > 0 then
1703
+ Scheduler:Schedule(options.duration, function()
1704
+ ____killscriptVisualRemove(nil, object)
1705
+ end)
1706
+ end
1707
+ return object
1708
+ end
1709
+
1710
+ local function ____killscriptVisualOverlay(_, options)
1711
+ local object = WorldVisuals:CreateSurfaceOverlay()
1712
+ if object == nil then return nil end
1713
+ object:SetPosition(options.position)
1714
+ object:SetSize(options.size)
1715
+ if options.color ~= nil then object:SetColor(options.color) end
1716
+ if options.fillBase ~= nil then object:SetFillBase(options.fillBase) end
1717
+ if options.occlusionEnabled ~= nil then object:SetOcclusionEnabled(options.occlusionEnabled) end
1718
+ if options.visible ~= nil then object:SetVisible(options.visible) end
1719
+ if options.duration ~= nil and options.duration > 0 then
1720
+ Scheduler:Schedule(options.duration, function()
1721
+ ____killscriptVisualRemove(nil, object)
1722
+ end)
1723
+ end
1724
+ return object
1725
+ end`;
1726
+ function scopeRuntime(context) {
1727
+ const worldMethods = context === "client" ? ` local worlds = {}
1728
+
1729
+ function result:world(object)
1730
+ if object == nil then return nil end
1731
+ if disposed then
1732
+ ____killscriptVisualRemove(nil, object)
1733
+ else
1734
+ worlds[#worlds + 1] = object
1735
+ end
1736
+ return object
1737
+ end
1738
+ ` : "";
1739
+ const worldCleanup = context === "client" ? ` for index = #worlds, 1, -1 do
1740
+ ____killscriptVisualRemove(nil, worlds[index])
1741
+ end
1742
+ ` : "";
1743
+ return `local function ____killscriptScope()
1744
+ local disposed = false
1745
+ local subscriptions = {}
1746
+ local callbacks = {}
1747
+ local result = {}
1748
+ ${worldMethods}
1749
+ function result:using(subscription)
1750
+ if subscription == nil then return nil end
1751
+ if disposed then
1752
+ subscription:Cancel()
1753
+ else
1754
+ subscriptions[#subscriptions + 1] = subscription
1755
+ end
1756
+ return subscription
1757
+ end
1758
+
1759
+ function result:defer(callback)
1760
+ if disposed then
1761
+ callback(nil)
1762
+ else
1763
+ callbacks[#callbacks + 1] = callback
1764
+ end
1765
+ end
1766
+
1767
+ function result:dispose()
1768
+ if disposed then return end
1769
+ disposed = true
1770
+ for index = #subscriptions, 1, -1 do subscriptions[index]:Cancel() end
1771
+ ${worldCleanup} for index = #callbacks, 1, -1 do callbacks[index](nil) end
1772
+ end
1773
+
1774
+ function result:isDisposed() return disposed end
1775
+ return result
1776
+ end`;
1777
+ }
1778
+ var TS_ARRAY_RUNTIME = `local function ____killscriptTsArrayReduce(_, values, callback, initial)
1779
+ local result = initial
1780
+ for index = 1, #values do
1781
+ result = callback(nil, result, values[index], index - 1, values)
1782
+ end
1783
+ return result
1784
+ end
1785
+
1786
+ local function ____killscriptTsArrayLength(_, values)
1787
+ return #values
1788
+ end
1789
+
1790
+ local function ____killscriptTsArrayReduceRight(_, values, callback, initial)
1791
+ local result = initial
1792
+ for index = #values, 1, -1 do
1793
+ result = callback(nil, result, values[index], index - 1, values)
1794
+ end
1795
+ return result
1796
+ end`;
1797
+ function networkRuntime(context) {
1798
+ const scheduler = context === "client" ? `local ____killscriptNetworkLastTick = -1
1799
+ Scheduler:OnFrame(function()
1800
+ local currentTick = Time.Tick
1801
+ if currentTick == ____killscriptNetworkLastTick then return end
1802
+ ____killscriptNetworkLastTick = currentTick
1803
+ ____killscriptNetworkFlush()
1804
+ end)` : `Scheduler:OnTick(____killscriptNetworkFlush)`;
1805
+ return `local ____killscriptNetworkQueue = {}
1806
+ local ____killscriptNetworkListeners = {}
1807
+
1808
+ local function ____killscriptNetworkSend(_, kind, payload)
1809
+ ____killscriptNetworkQueue[#____killscriptNetworkQueue + 1] = {
1810
+ __killscript_channel = kind,
1811
+ payload = payload
1812
+ }
1813
+ end
1814
+
1815
+ local function ____killscriptNetworkOn(_, kind, callback)
1816
+ local listeners = ____killscriptNetworkListeners[kind]
1817
+ if listeners == nil then
1818
+ listeners = {}
1819
+ ____killscriptNetworkListeners[kind] = listeners
1820
+ end
1821
+ listeners[#listeners + 1] = callback
1822
+ end
1823
+
1824
+ local function ____killscriptNetworkDispatch(message)
1825
+ local listeners = ____killscriptNetworkListeners[message.__killscript_channel]
1826
+ if listeners == nil then return end
1827
+ for index = 1, #listeners do listeners[index](nil, message.payload) end
1828
+ end
1829
+
1830
+ Network:OnTableReceived(function(message)
1831
+ local batch = message.__killscript_batch
1832
+ if batch == nil then
1833
+ ____killscriptNetworkDispatch(message)
1834
+ return
1835
+ end
1836
+ for index = 1, #batch do
1837
+ ____killscriptNetworkDispatch(batch[index])
1838
+ end
1839
+ end)
1840
+
1841
+ local function ____killscriptNetworkFlush()
1842
+ if #____killscriptNetworkQueue == 0 then return end
1843
+ local batch = ____killscriptNetworkQueue
1844
+ ____killscriptNetworkQueue = {}
1845
+ Network:SendTable({ __killscript_batch = batch })
1846
+ end
1847
+
1848
+ ${scheduler}`;
1849
+ }
1850
+ function makeBundleSandboxSafe(lua) {
1851
+ return lua.replace(
1852
+ ' local value = nil\n if (select("#", ...) > 0) then value = module(...) else value = module(file) end',
1853
+ " local value = module(file)"
1854
+ ).replace(
1855
+ / else\n error\("module '" \.\. file \.\. "' not found"\)\n end/g,
1856
+ " end\n return nil"
1857
+ ).replace(/local ____entry = require\(("[^"]+"), \.\.\.\)/, "local ____entry = require($1)");
1858
+ }
1859
+ function assertSandboxSafe(lua, context) {
1860
+ const codeOnly = lua.replace(/--\[\[[\s\S]*?\]\]/g, "").replace(/--[^\n]*/g, "").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/'(?:\\.|[^'\\])*'/g, "''");
1861
+ const unavailable = [...codeOnly.matchAll(/\b(pcall|xpcall|select|type|error)\s*\(/g)].map((match) => match[1]).filter((value, index, all) => value !== void 0 && all.indexOf(value) === index);
1862
+ if (unavailable.length > 0) {
1863
+ throw new CompileError(
1864
+ `${context} output requires sandbox globals that the game does not expose: ${unavailable.join(", ")}. Replace the JavaScript built-in that generated them or use an SDK helper.`
1865
+ );
1866
+ }
1867
+ }
1868
+ function formatDiagnostics(diagnostics, projectDir) {
1869
+ return ts3.formatDiagnosticsWithColorAndContext(diagnostics, {
1870
+ getCanonicalFileName: (fileName) => path3.relative(projectDir, fileName),
1871
+ getCurrentDirectory: () => projectDir,
1872
+ getNewLine: () => "\n"
1873
+ });
1874
+ }
1875
+ function isInside2(file, directory) {
1876
+ const relative = path3.relative(path3.resolve(directory), path3.resolve(file));
1877
+ return relative !== "" && !relative.startsWith("..") && !path3.isAbsolute(relative);
1878
+ }
1879
+
1880
+ // src/compiler/build.ts
1881
+ async function buildProject(projectDir, config) {
1882
+ const client = compileContext(projectDir, "client");
1883
+ const server = config.reflex ? compileContext(projectDir, "server") : void 0;
1884
+ if (!config.reflex && client.extraction.networkHandles.size > 0) {
1885
+ throw new CompileError("defineNetwork() requires a Reflex module. Set reflex to true in killscript.config.json.");
1886
+ }
1887
+ const settings = mergeSettings([
1888
+ ...client.extraction.settings,
1889
+ ...server?.extraction.settings ?? []
1890
+ ]);
1891
+ const controls = client.extraction.controls;
1892
+ const outputDir = path4.join(projectDir, ".killscript", "build", config.name);
1893
+ await rm(outputDir, { recursive: true, force: true });
1894
+ await mkdir2(path4.join(outputDir, "scripts"), { recursive: true });
1895
+ await copyPublicAssets(projectDir, outputDir);
1896
+ await Promise.all([
1897
+ writeJson(path4.join(outputDir, "module.json"), {
1898
+ Id: config.id,
1899
+ Name: config.name,
1900
+ Version: config.version,
1901
+ Author: config.author,
1902
+ Description: config.description ?? null,
1903
+ IsReflex: config.reflex,
1904
+ Tags: config.tags ?? []
1905
+ }),
1906
+ writeJson(path4.join(outputDir, "config.json"), settings.map(toConfigJson)),
1907
+ writeJson(path4.join(outputDir, "inputs.json"), {
1908
+ Actions: controls.map(toInputJson)
1909
+ }),
1910
+ writeFile2(path4.join(outputDir, "scripts", "main.lua"), client.lua),
1911
+ ...server === void 0 ? [] : [writeFile2(path4.join(outputDir, "scripts", "server.lua"), server.lua)]
1912
+ ]);
1913
+ return { outputDir, settings: settings.length, controls: controls.length };
1914
+ }
1915
+ function mergeSettings(values) {
1916
+ const result = /* @__PURE__ */ new Map();
1917
+ for (const setting of values) {
1918
+ const previous = result.get(setting.key);
1919
+ if (previous === void 0) {
1920
+ result.set(setting.key, setting);
1921
+ continue;
1922
+ }
1923
+ if (JSON.stringify(toConfigJson(previous)) !== JSON.stringify(toConfigJson(setting))) {
1924
+ throw new CompileError(`Conflicting setting '${setting.key}' between client and server.`);
1925
+ }
1926
+ }
1927
+ return [...result.values()];
1928
+ }
1929
+ function toConfigJson(setting) {
1930
+ return {
1931
+ label: setting.label,
1932
+ key: setting.key,
1933
+ type: setting.type,
1934
+ value: setting.value,
1935
+ ...setting.min === void 0 ? {} : { min: setting.min },
1936
+ ...setting.max === void 0 ? {} : { max: setting.max },
1937
+ ...setting.options === void 0 ? {} : { options: setting.options }
1938
+ };
1939
+ }
1940
+ function toInputJson(control) {
1941
+ return {
1942
+ Name: control.name,
1943
+ Type: control.type,
1944
+ Binding: {
1945
+ Path: control.binding.path,
1946
+ ...control.binding.interactions === void 0 ? {} : { Interactions: control.binding.interactions },
1947
+ ...control.binding.processors === void 0 ? {} : { Processors: control.binding.processors },
1948
+ ...control.binding.groups === void 0 ? {} : { Groups: control.binding.groups }
1949
+ }
1950
+ };
1951
+ }
1952
+ async function writeJson(file, value) {
1953
+ await writeFile2(file, `${JSON.stringify(value, null, 2)}
1954
+ `);
1955
+ }
1956
+ async function copyPublicAssets(projectDir, outputDir) {
1957
+ const publicDir = path4.join(projectDir, "public");
1958
+ try {
1959
+ if (!(await stat(publicDir)).isDirectory()) return;
1960
+ await validatePublicAssets(publicDir);
1961
+ await cp(publicDir, outputDir, { recursive: true, force: true });
1962
+ } catch (error) {
1963
+ if (error.code !== "ENOENT") throw error;
1964
+ }
1965
+ }
1966
+ async function validatePublicAssets(publicDir) {
1967
+ const reserved = /* @__PURE__ */ new Set([
1968
+ "module.json",
1969
+ "config.json",
1970
+ "inputs.json",
1971
+ "scripts",
1972
+ ".killscript-managed.json"
1973
+ ]);
1974
+ for (const entry of await readdir(publicDir, { withFileTypes: true })) {
1975
+ if (reserved.has(entry.name.toLowerCase())) {
1976
+ throw new CompileError(`public/${entry.name} is reserved and generated by the compiler.`);
1977
+ }
1978
+ }
1979
+ await walk(publicDir);
1980
+ async function walk(directory) {
1981
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1982
+ const absolute = path4.join(directory, entry.name);
1983
+ const info = await lstat(absolute);
1984
+ if (info.isSymbolicLink()) {
1985
+ throw new CompileError(`Public assets cannot contain symbolic links: ${path4.relative(publicDir, absolute)}`);
1986
+ }
1987
+ if (info.isDirectory()) await walk(absolute);
1988
+ }
1989
+ }
1990
+ }
1991
+
1992
+ // src/pack.ts
1993
+ import { randomUUID as randomUUID2 } from "crypto";
1994
+ import { mkdir as mkdir3, readFile as readFile2, readdir as readdir2, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
1995
+ import path5 from "path";
1996
+ import { zipSync } from "fflate";
1997
+ async function packModule(moduleDir, destination) {
1998
+ const files = {};
1999
+ await walk("");
2000
+ await mkdir3(path5.dirname(destination), { recursive: true });
2001
+ const temporary = `${destination}.${randomUUID2()}.tmp`;
2002
+ try {
2003
+ await writeFile3(temporary, zipSync(files, { level: 9 }));
2004
+ await rm2(destination, { force: true });
2005
+ await rename(temporary, destination);
2006
+ } catch (error) {
2007
+ await rm2(temporary, { force: true });
2008
+ throw error;
2009
+ }
2010
+ async function walk(relativeDir) {
2011
+ const entries = await readdir2(path5.join(moduleDir, relativeDir), { withFileTypes: true });
2012
+ entries.sort((left, right) => left.name.localeCompare(right.name, "en"));
2013
+ for (const entry of entries) {
2014
+ const relative = path5.join(relativeDir, entry.name);
2015
+ if (entry.isDirectory()) await walk(relative);
2016
+ else if (entry.isFile()) {
2017
+ files[relative.replaceAll(path5.sep, "/")] = [
2018
+ await readFile2(path5.join(moduleDir, relative)),
2019
+ { mtime: /* @__PURE__ */ new Date("1980-01-01T00:00:00.000Z") }
2020
+ ];
2021
+ }
2022
+ }
2023
+ }
2024
+ }
2025
+
2026
+ // src/commands/build.ts
2027
+ async function runBuild(projectDir, options = {}) {
2028
+ const config = await readProjectConfig(projectDir);
2029
+ const result = await buildProject(projectDir, config);
2030
+ console.log(
2031
+ `Built ${config.name}: ${result.settings} settings, ${result.controls} controls -> ${result.outputDir}`
2032
+ );
2033
+ if (options.pack ?? true) {
2034
+ const destination = path6.join(projectDir, "dist", `${config.name}.KillScript`);
2035
+ await packModule(result.outputDir, destination);
2036
+ console.log(`Packed ${destination}`);
2037
+ }
2038
+ }
2039
+
2040
+ // src/commands/clean.ts
2041
+ import { rm as rm4 } from "fs/promises";
2042
+ import path8 from "path";
2043
+
2044
+ // src/output.ts
2045
+ import { createHash } from "crypto";
2046
+ import {
2047
+ copyFile,
2048
+ mkdir as mkdir4,
2049
+ readFile as readFile3,
2050
+ readdir as readdir3,
2051
+ rename as rename2,
2052
+ rm as rm3,
2053
+ stat as stat2,
2054
+ writeFile as writeFile4
2055
+ } from "fs/promises";
2056
+ import path7 from "path";
2057
+ var MARKER = ".killscript-managed.json";
2058
+ async function inspectManagedModule(targetDir, moduleId) {
2059
+ const marker = await readMarker(targetDir);
2060
+ if (marker !== void 0) return marker.id === moduleId ? "managed" : "foreign";
2061
+ try {
2062
+ const moduleJson = JSON.parse(await readFile3(path7.join(targetDir, "module.json"), "utf8"));
2063
+ return moduleJson.Id === moduleId ? "same-module" : "foreign";
2064
+ } catch (error) {
2065
+ if (error.code === "ENOENT") return "missing";
2066
+ throw error;
2067
+ }
2068
+ }
2069
+ async function removeManagedModule(targetDir, moduleId) {
2070
+ const status = await inspectManagedModule(targetDir, moduleId);
2071
+ if (status === "missing") return false;
2072
+ if (status !== "managed") {
2073
+ throw new Error(`Refusing to remove ${targetDir}: it is not managed by this project.`);
2074
+ }
2075
+ await rm3(targetDir, { recursive: true, force: true });
2076
+ return true;
2077
+ }
2078
+ async function syncManagedModule(sourceDir, targetDir, moduleId) {
2079
+ await assertSafeTarget(targetDir, moduleId);
2080
+ await mkdir4(targetDir, { recursive: true });
2081
+ const sourceFiles = await listFiles(sourceDir);
2082
+ const previous = await readMarker(targetDir);
2083
+ let changed = 0;
2084
+ const changedFiles = [];
2085
+ const inputsFile = sourceFiles.includes("inputs.json") ? "inputs.json" : void 0;
2086
+ for (const relative of sourceFiles) {
2087
+ if (relative === inputsFile) continue;
2088
+ const source = path7.join(sourceDir, relative);
2089
+ const target = path7.join(targetDir, relative);
2090
+ if (await filesEqual(source, target)) continue;
2091
+ await atomicCopy(source, target);
2092
+ changed += 1;
2093
+ changedFiles.push(relative);
2094
+ }
2095
+ for (const relative of previous?.files ?? []) {
2096
+ if (sourceFiles.includes(relative)) continue;
2097
+ await rm3(path7.join(targetDir, relative), { force: true });
2098
+ changed += 1;
2099
+ changedFiles.push(relative);
2100
+ }
2101
+ const markerTarget = path7.join(targetDir, MARKER);
2102
+ const markerContent = `${JSON.stringify(
2103
+ { id: moduleId, files: sourceFiles },
2104
+ null,
2105
+ 2
2106
+ )}
2107
+ `;
2108
+ if (!await contentEquals(markerTarget, markerContent)) {
2109
+ await atomicWrite(markerTarget, markerContent);
2110
+ }
2111
+ if (inputsFile !== void 0) {
2112
+ const source = path7.join(sourceDir, inputsFile);
2113
+ const target = path7.join(targetDir, inputsFile);
2114
+ if (!await filesEqual(source, target)) {
2115
+ await atomicCopy(source, target);
2116
+ changed += 1;
2117
+ changedFiles.push(inputsFile);
2118
+ }
2119
+ }
2120
+ return { changed, changedFiles };
2121
+ }
2122
+ async function assertSafeTarget(targetDir, moduleId) {
2123
+ const status = await inspectManagedModule(targetDir, moduleId);
2124
+ if (status === "missing" || status === "managed" || status === "same-module") return;
2125
+ throw new Error(`Refusing to overwrite unmanaged or foreign module folder ${targetDir}.`);
2126
+ }
2127
+ async function readMarker(targetDir) {
2128
+ try {
2129
+ return JSON.parse(await readFile3(path7.join(targetDir, MARKER), "utf8"));
2130
+ } catch (error) {
2131
+ if (error.code === "ENOENT") return void 0;
2132
+ throw error;
2133
+ }
2134
+ }
2135
+ async function listFiles(root) {
2136
+ const result = [];
2137
+ await walk("");
2138
+ return result.sort();
2139
+ async function walk(relativeDir) {
2140
+ const absolute = path7.join(root, relativeDir);
2141
+ for (const entry of await readdir3(absolute, { withFileTypes: true })) {
2142
+ const relative = path7.join(relativeDir, entry.name);
2143
+ if (entry.isDirectory()) await walk(relative);
2144
+ else if (entry.isFile()) result.push(relative);
2145
+ }
2146
+ }
2147
+ }
2148
+ async function filesEqual(left, right) {
2149
+ try {
2150
+ const [leftStat, rightStat] = await Promise.all([stat2(left), stat2(right)]);
2151
+ if (leftStat.size !== rightStat.size) return false;
2152
+ const [leftData, rightData] = await Promise.all([readFile3(left), readFile3(right)]);
2153
+ return hash(leftData) === hash(rightData);
2154
+ } catch (error) {
2155
+ if (error.code === "ENOENT") return false;
2156
+ throw error;
2157
+ }
2158
+ }
2159
+ async function contentEquals(file, expected) {
2160
+ try {
2161
+ return await readFile3(file, "utf8") === expected;
2162
+ } catch (error) {
2163
+ if (error.code === "ENOENT") return false;
2164
+ throw error;
2165
+ }
2166
+ }
2167
+ function hash(value) {
2168
+ return createHash("sha256").update(value).digest("hex");
2169
+ }
2170
+ async function atomicCopy(source, target) {
2171
+ await mkdir4(path7.dirname(target), { recursive: true });
2172
+ const temporary = `${target}.killscript-${process.pid}.tmp`;
2173
+ await copyFile(source, temporary);
2174
+ await replaceWithRetry(temporary, target);
2175
+ }
2176
+ async function atomicWrite(target, content) {
2177
+ await mkdir4(path7.dirname(target), { recursive: true });
2178
+ const temporary = `${target}.killscript-${process.pid}.tmp`;
2179
+ await writeFile4(temporary, content);
2180
+ await replaceWithRetry(temporary, target);
2181
+ }
2182
+ async function replaceWithRetry(temporary, target) {
2183
+ let lastError;
2184
+ for (let attempt = 0; attempt < 8; attempt += 1) {
2185
+ const backup = `${target}.killscript-${process.pid}.bak`;
2186
+ let backedUp = false;
2187
+ try {
2188
+ try {
2189
+ await rename2(temporary, target);
2190
+ return;
2191
+ } catch {
2192
+ }
2193
+ try {
2194
+ await rename2(target, backup);
2195
+ backedUp = true;
2196
+ } catch (error) {
2197
+ if (error.code !== "ENOENT") throw error;
2198
+ }
2199
+ await rename2(temporary, target);
2200
+ if (backedUp) await rm3(backup, { force: true });
2201
+ return;
2202
+ } catch (error) {
2203
+ lastError = error;
2204
+ if (backedUp) {
2205
+ try {
2206
+ await rename2(backup, target);
2207
+ } catch (restoreError) {
2208
+ await rm3(temporary, { force: true });
2209
+ throw new Error(`Failed to restore ${target} after an interrupted update.`, {
2210
+ cause: restoreError
2211
+ });
2212
+ }
2213
+ }
2214
+ await delay(25 * 2 ** attempt);
2215
+ }
2216
+ }
2217
+ await rm3(temporary, { force: true });
2218
+ throw lastError;
2219
+ }
2220
+ function delay(milliseconds) {
2221
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
2222
+ }
2223
+
2224
+ // src/commands/clean.ts
2225
+ async function runClean(projectDir, options = {}) {
2226
+ await Promise.all([
2227
+ rm4(path8.join(projectDir, ".killscript", "build"), { recursive: true, force: true }),
2228
+ rm4(path8.join(projectDir, ".killscript", "compiler"), { recursive: true, force: true }),
2229
+ rm4(path8.join(projectDir, "dist"), { recursive: true, force: true })
2230
+ ]);
2231
+ let gameMessage = "";
2232
+ if (options.game) {
2233
+ const [config, local] = await Promise.all([
2234
+ readProjectConfig(projectDir),
2235
+ readLocalConfig(projectDir)
2236
+ ]);
2237
+ if (local.modulesPath === void 0) {
2238
+ throw new Error("Cannot clean the game module: Modules path is not configured.");
2239
+ }
2240
+ const target = local.installedTarget ?? path8.join(path8.resolve(local.modulesPath), config.name);
2241
+ gameMessage = await removeManagedModule(target, config.id) ? ` Removed managed game folder ${target}.` : " No managed game folder existed.";
2242
+ }
2243
+ console.log(`Removed generated output.${gameMessage}`);
2244
+ }
2245
+
2246
+ // src/commands/dev.ts
2247
+ import path10 from "path";
2248
+ import { watch } from "chokidar";
2249
+
2250
+ // src/commands/install.ts
2251
+ import { stat as stat3 } from "fs/promises";
2252
+ import path9 from "path";
2253
+ async function installProject(projectDir, options = {}) {
2254
+ const local = await readLocalConfig(projectDir);
2255
+ const configuredPath = options.modules ?? local.modulesPath;
2256
+ if (configuredPath === void 0) {
2257
+ throw new Error("Modules path is not configured. Pass --modules <path> once.");
2258
+ }
2259
+ const modulesPath = await resolveGameModulesPath(projectDir, configuredPath);
2260
+ const config = await readProjectConfig(projectDir);
2261
+ const build = await buildProject(projectDir, config);
2262
+ const target = path9.join(modulesPath, config.name);
2263
+ const sync = await syncManagedModule(build.outputDir, target, config.id);
2264
+ let removedPreviousTarget;
2265
+ if (local.installedTarget !== void 0) {
2266
+ const previousTarget = path9.resolve(local.installedTarget);
2267
+ if (previousTarget !== target) {
2268
+ const status = await inspectManagedModule(previousTarget, config.id);
2269
+ if (status === "managed") {
2270
+ await removeManagedModule(previousTarget, config.id);
2271
+ removedPreviousTarget = previousTarget;
2272
+ }
2273
+ }
2274
+ }
2275
+ await writeLocalConfig(projectDir, {
2276
+ ...local,
2277
+ modulesPath,
2278
+ installedTarget: target
2279
+ });
2280
+ return {
2281
+ config,
2282
+ modulesPath,
2283
+ target,
2284
+ changed: sync.changed,
2285
+ changedFiles: sync.changedFiles,
2286
+ ...removedPreviousTarget === void 0 ? {} : { removedPreviousTarget }
2287
+ };
2288
+ }
2289
+ async function resolveGameModulesPath(projectDir, modulesArgument) {
2290
+ const modulesPath = path9.resolve(modulesArgument);
2291
+ const info = await stat3(modulesPath).catch(() => void 0);
2292
+ if (!info?.isDirectory()) {
2293
+ throw new Error(`Modules path does not exist or is not a directory: ${modulesPath}`);
2294
+ }
2295
+ const projectPath = path9.resolve(projectDir);
2296
+ const relative = path9.relative(modulesPath, projectPath);
2297
+ const projectIsInsideModules = relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
2298
+ if (projectIsInsideModules) {
2299
+ throw new Error(
2300
+ `Keep the TypeScript project outside the game Modules folder. Only generated output may be installed into ${modulesPath}.`
2301
+ );
2302
+ }
2303
+ return modulesPath;
2304
+ }
2305
+ async function runInstall(projectDir, options = {}) {
2306
+ const result = await installProject(projectDir, options);
2307
+ console.log(`Installed ${result.config.name} -> ${result.target} (${result.changed} files)`);
2308
+ if (result.changedFiles.includes("scripts/server.lua")) {
2309
+ console.log(
2310
+ "Reflex server changed: folder hot reload requested for the current match."
2311
+ );
2312
+ }
2313
+ if (result.removedPreviousTarget !== void 0) {
2314
+ console.log(`Removed previous managed folder ${result.removedPreviousTarget}`);
2315
+ }
2316
+ console.log("Open or reopen the in-game module list, then enable the module once.");
2317
+ }
2318
+
2319
+ // src/commands/dev.ts
2320
+ async function runDev(projectDir, options) {
2321
+ let building = false;
2322
+ let queued = false;
2323
+ let firstBuild = true;
2324
+ const rebuild = async (failFast = false) => {
2325
+ if (building) {
2326
+ queued = true;
2327
+ return;
2328
+ }
2329
+ building = true;
2330
+ try {
2331
+ const result = await installProject(projectDir, {
2332
+ ...options.modules === void 0 ? {} : { modules: options.modules }
2333
+ });
2334
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
2335
+ console.log(
2336
+ `[${time}] ${firstBuild ? "Ready" : "Updated"} ${result.config.name} (${result.changed} files)`
2337
+ );
2338
+ if (result.changedFiles.includes("scripts/server.lua")) {
2339
+ console.log(
2340
+ `[${time}] Reflex server changed: folder hot reload requested for the current match.`
2341
+ );
2342
+ }
2343
+ if (result.removedPreviousTarget !== void 0) {
2344
+ console.log(`[${time}] Removed previous managed folder ${result.removedPreviousTarget}`);
2345
+ }
2346
+ firstBuild = false;
2347
+ } catch (error) {
2348
+ if (failFast) throw error;
2349
+ const message = error instanceof CompileError ? formatCompileError(error) : String(error);
2350
+ console.error(`
2351
+ Build failed; the last good game module was kept.
2352
+ ${message}
2353
+ `);
2354
+ } finally {
2355
+ building = false;
2356
+ if (queued) {
2357
+ queued = false;
2358
+ void rebuild();
2359
+ }
2360
+ }
2361
+ };
2362
+ await rebuild(options.once === true);
2363
+ if (options.once) return;
2364
+ const watcher = watch(
2365
+ [path10.join(projectDir, "src"), path10.join(projectDir, "public"), path10.join(projectDir, "killscript.config.json")],
2366
+ {
2367
+ ignoreInitial: true,
2368
+ awaitWriteFinish: { stabilityThreshold: 80, pollInterval: 20 }
2369
+ }
2370
+ );
2371
+ watcher.on("all", () => void rebuild());
2372
+ console.log(`Watching ${projectDir}`);
2373
+ await new Promise((resolve) => {
2374
+ const stop = () => {
2375
+ void watcher.close().finally(resolve);
2376
+ };
2377
+ process.once("SIGINT", stop);
2378
+ process.once("SIGTERM", stop);
2379
+ });
2380
+ }
2381
+
2382
+ // src/commands/doctor.ts
2383
+ import { access, stat as stat4 } from "fs/promises";
2384
+ import path11 from "path";
2385
+ async function runDoctor(projectDir, options = {}) {
2386
+ const checks = [];
2387
+ const major = Number(process.versions.node.split(".")[0]);
2388
+ if (!Number.isFinite(major) || major < 20) {
2389
+ throw new Error(`Node.js 20 or newer is required; found ${process.versions.node}.`);
2390
+ }
2391
+ checks.push({ label: "Node", detail: process.versions.node });
2392
+ const config = await readProjectConfig(projectDir);
2393
+ checks.push({ label: "Project", detail: `${config.name} ${config.version}${config.reflex ? " (Reflex)" : ""}` });
2394
+ await access(path11.join(projectDir, "src", "client", "main.ts"));
2395
+ if (config.reflex) await access(path11.join(projectDir, "src", "server", "main.ts"));
2396
+ checks.push({ label: "Entries", detail: config.reflex ? "client + server" : "client" });
2397
+ const result = await buildProject(projectDir, config);
2398
+ checks.push({ label: "Compiler", detail: `${result.settings} settings, ${result.controls} controls` });
2399
+ const local = await readLocalConfig(projectDir);
2400
+ const modulesPath = options.modules ?? local.modulesPath;
2401
+ if (modulesPath === void 0) {
2402
+ checks.push({ label: "Game link", detail: "not configured (run killscript link <Modules>)" });
2403
+ } else {
2404
+ const absolute = path11.resolve(modulesPath);
2405
+ if (!(await stat4(absolute).catch(() => void 0))?.isDirectory()) {
2406
+ throw new Error(`Configured Modules folder is unavailable: ${absolute}`);
2407
+ }
2408
+ const target = path11.join(absolute, config.name);
2409
+ const status = await inspectManagedModule(target, config.id);
2410
+ if (status === "foreign") throw new Error(`Game target is owned by another module: ${target}`);
2411
+ checks.push({ label: "Game link", detail: `${absolute} (${status})` });
2412
+ }
2413
+ const width = Math.max(...checks.map((check) => check.label.length));
2414
+ for (const check of checks) console.log(`\u2713 ${check.label.padEnd(width)} ${check.detail}`);
2415
+ console.log("\nProject is ready.");
2416
+ }
2417
+
2418
+ // src/commands/link.ts
2419
+ async function runLink(projectDir, modulesArgument) {
2420
+ const modulesPath = await resolveGameModulesPath(projectDir, modulesArgument);
2421
+ const current = await readLocalConfig(projectDir);
2422
+ await writeLocalConfig(projectDir, { ...current, modulesPath });
2423
+ console.log(`Linked game Modules folder: ${modulesPath}`);
2424
+ }
2425
+
2426
+ // src/commands/new.ts
2427
+ import { spawn } from "child_process";
2428
+ import { mkdir as mkdir5, readFile as readFile4, readdir as readdir4, rm as rm5, writeFile as writeFile5 } from "fs/promises";
2429
+ import path12 from "path";
2430
+ import { fileURLToPath } from "url";
2431
+ async function runNew(destinationArgument, options) {
2432
+ const destination = path12.resolve(destinationArgument);
2433
+ const name = path12.basename(destination);
2434
+ assertProjectName(name);
2435
+ if (options.modules !== void 0) {
2436
+ await resolveGameModulesPath(destination, options.modules);
2437
+ }
2438
+ await mkdir5(destination, { recursive: false });
2439
+ await scaffold(destinationArgument, destination, name, options, true);
2440
+ }
2441
+ async function runInit(destinationArgument, options) {
2442
+ const destination = path12.resolve(destinationArgument);
2443
+ const name = path12.basename(destination);
2444
+ assertProjectName(name);
2445
+ if (options.modules !== void 0) {
2446
+ await resolveGameModulesPath(destination, options.modules);
2447
+ }
2448
+ await mkdir5(destination, { recursive: true });
2449
+ const existing = (await readdir4(destination)).filter((entry) => entry !== ".git");
2450
+ if (existing.length > 0) {
2451
+ throw new Error(`Cannot initialize ${destination}: the directory is not empty.`);
2452
+ }
2453
+ await scaffold(destinationArgument, destination, name, options, false);
2454
+ }
2455
+ async function scaffold(destinationArgument, destination, name, options, removeDestinationOnFailure) {
2456
+ try {
2457
+ const template = path12.resolve(
2458
+ path12.dirname(fileURLToPath(import.meta.url)),
2459
+ "..",
2460
+ "templates",
2461
+ "default"
2462
+ );
2463
+ const namespace = toKebabCase(name);
2464
+ await copyTemplate(template, destination, {
2465
+ "__MODULE_NAME__": name,
2466
+ "__MODULE_NAMESPACE__": namespace,
2467
+ "__TOOLCHAIN_VERSION__": VERSION
2468
+ });
2469
+ const config = createProjectConfig({
2470
+ name,
2471
+ ...options.author === void 0 ? {} : { author: options.author },
2472
+ ...options.reflex === void 0 ? {} : { reflex: options.reflex }
2473
+ });
2474
+ await writeProjectConfig(destination, config);
2475
+ if (!config.reflex) {
2476
+ await rm5(path12.join(destination, "src", "server"), { recursive: true, force: true });
2477
+ await rm5(path12.join(destination, "tsconfig.server.json"), { force: true });
2478
+ await writeFile5(
2479
+ path12.join(destination, "tsconfig.json"),
2480
+ `${JSON.stringify({ files: [], references: [{ path: "./tsconfig.client.json" }] }, null, 2)}
2481
+ `
2482
+ );
2483
+ }
2484
+ if (options.modules !== void 0) {
2485
+ await writeLocalConfig(destination, { modulesPath: path12.resolve(options.modules) });
2486
+ }
2487
+ const installDependencies = options.install ?? true;
2488
+ if (installDependencies) await run("npm", ["install"], destination);
2489
+ if (options.git ?? true) await run("git", ["init", "-b", "master"], destination);
2490
+ let installedTarget;
2491
+ if (options.modules !== void 0 && installDependencies) {
2492
+ installedTarget = (await installProject(destination)).target;
2493
+ }
2494
+ console.log(`
2495
+ Created ${name} in ${destination}`);
2496
+ if (installedTarget !== void 0) {
2497
+ console.log(`Installed game module in ${installedTarget}`);
2498
+ console.log("Open or reopen the in-game module list, then enable it once.");
2499
+ }
2500
+ console.log(` cd ${destinationArgument}`);
2501
+ console.log(
2502
+ options.modules === void 0 ? " npm run dev -- --modules <path-to-game-Modules>" : installDependencies ? " npm run dev" : " npm install && npm run install:game"
2503
+ );
2504
+ } catch (error) {
2505
+ if (removeDestinationOnFailure) {
2506
+ await rm5(destination, { recursive: true, force: true });
2507
+ } else {
2508
+ for (const entry of await readdir4(destination)) {
2509
+ if (entry !== ".git") await rm5(path12.join(destination, entry), { recursive: true, force: true });
2510
+ }
2511
+ }
2512
+ throw error;
2513
+ }
2514
+ }
2515
+ async function copyTemplate(source, destination, replacements) {
2516
+ for (const entry of await readdir4(source, { withFileTypes: true })) {
2517
+ const targetName = entry.name === "gitignore" ? ".gitignore" : entry.name;
2518
+ const from = path12.join(source, entry.name);
2519
+ const to = path12.join(destination, targetName);
2520
+ if (entry.isDirectory()) {
2521
+ await mkdir5(to, { recursive: true });
2522
+ await copyTemplate(from, to, replacements);
2523
+ continue;
2524
+ }
2525
+ if (!entry.isFile()) continue;
2526
+ let content = await readFile4(from, "utf8");
2527
+ for (const [search, replacement] of Object.entries(replacements)) {
2528
+ content = content.replaceAll(search, replacement);
2529
+ }
2530
+ await writeFile5(to, content);
2531
+ }
2532
+ }
2533
+ async function run(command, args, cwd) {
2534
+ await new Promise((resolve, reject) => {
2535
+ const child = spawn(command, args, { cwd, stdio: "inherit" });
2536
+ child.once("error", reject);
2537
+ child.once("exit", (code) => {
2538
+ if (code === 0) resolve();
2539
+ else reject(new Error(`${command} exited with code ${code ?? "unknown"}.`));
2540
+ });
2541
+ });
2542
+ }
2543
+ function assertProjectName(name) {
2544
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name)) {
2545
+ throw new Error(
2546
+ "Module name must start with a letter and contain only letters, numbers, _ or -."
2547
+ );
2548
+ }
2549
+ }
2550
+ function toKebabCase(value) {
2551
+ return value.replace(/([a-z\d])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
2552
+ }
2553
+
2554
+ // src/cli.ts
2555
+ var program = new Command().name("killscript").description("Build TypeScript modules for KILLSCRIPT").version(VERSION).showHelpAfterError().showSuggestionAfterError();
2556
+ program.command("new").description("create a new KILLSCRIPT module").argument("<directory>").option("--author <name>", "module author").option("--reflex", "include a Reflex server entry").option("--modules <path>", "remember the game Modules folder").option("--no-install", "skip npm install").option("--no-git", "skip git init").action(async (directory, options) => runNew(directory, options));
2557
+ program.command("init").description("initialize an empty directory as a KILLSCRIPT module").argument("[directory]", "empty directory", ".").option("--author <name>", "module author").option("--reflex", "include a Reflex server entry").option("--modules <path>", "remember the game Modules folder").option("--no-install", "skip npm install").option("--no-git", "skip git init").action(async (directory, options) => runInit(directory, options));
2558
+ program.command("build").description("compile and pack the current module").option("--no-pack", "only create the module folder").action(async (options) => runBuild(process.cwd(), options));
2559
+ program.command("check").description("type-check and compile without keeping a package").action(async () => runBuild(process.cwd(), { pack: false }));
2560
+ program.command("install").description("build and install the folder module into the game once").option("--modules <path>", "game Modules folder").action(async (options) => runInstall(process.cwd(), options));
2561
+ program.command("dev").description("watch sources and sync a folder module into the game").option("--modules <path>", "game Modules folder").option("--once", "build and synchronize once without watching").action(async (options) => runDev(process.cwd(), options));
2562
+ program.command("link").description("remember the game Modules folder for this project").argument("<path>").action(async (modulesPath) => runLink(process.cwd(), modulesPath));
2563
+ program.command("doctor").description("validate the project, compiler and game folder connection").option("--modules <path>", "temporarily check a game Modules folder").action(async (options) => runDoctor(process.cwd(), options));
2564
+ program.command("clean").description("remove generated output").option("--game", "also remove this project's managed folder from the game").action(async (options) => runClean(process.cwd(), options));
2565
+ try {
2566
+ await program.parseAsync();
2567
+ } catch (error) {
2568
+ console.error(`
2569
+ ${error instanceof Error ? error.message : String(error)}`);
2570
+ process.exitCode = 1;
2571
+ }