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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/chunk-T3XTQIWG.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2571 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/package.json +46 -0
- package/templates/default/README.md +18 -0
- package/templates/default/gitignore +5 -0
- package/templates/default/package.json +20 -0
- package/templates/default/src/client/controls.ts +5 -0
- package/templates/default/src/client/features/module-toggle.ts +20 -0
- package/templates/default/src/client/main.ts +2 -0
- package/templates/default/src/server/features/gameplay.ts +12 -0
- package/templates/default/src/server/main.ts +1 -0
- package/templates/default/src/shared/settings.ts +7 -0
- package/templates/default/tsconfig.client.json +12 -0
- package/templates/default/tsconfig.json +7 -0
- package/templates/default/tsconfig.server.json +12 -0
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
|
+
}
|