@kzheart_/mc-pilot 0.1.1 → 0.1.3

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.
@@ -1,9 +1,15 @@
1
1
  import { mkdirSync, openSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { MctError } from "../util/errors.js";
5
6
  import { getListeningPids, isProcessRunning, killProcessTree } from "../util/process.js";
6
7
  import { WebSocketClient } from "./WebSocketClient.js";
8
+ function getLaunchScriptPath() {
9
+ const thisFile = fileURLToPath(import.meta.url);
10
+ // dist/client/ClientManager.js -> scripts/launch-fabric-client.mjs
11
+ return path.resolve(path.dirname(thisFile), "..", "..", "scripts", "launch-fabric-client.mjs");
12
+ }
7
13
  const CLIENT_STATE_FILE = "clients.json";
8
14
  function getDefaultSnapshot() {
9
15
  return {
@@ -46,11 +52,13 @@ export class ClientManager {
46
52
  await new Promise((resolve) => setTimeout(resolve, 250));
47
53
  }
48
54
  }
49
- const launchCommand = configured.launchCommand;
55
+ const launchCommand = configured.launchArgs
56
+ ? [process.execPath, getLaunchScriptPath(), ...configured.launchArgs]
57
+ : configured.launchCommand;
50
58
  if (!launchCommand || launchCommand.length === 0) {
51
59
  throw new MctError({
52
60
  code: "INVALID_PARAMS",
53
- message: `Client ${options.name} requires launchCommand in config`
61
+ message: `Client ${options.name} requires launchArgs (or launchCommand) in config`
54
62
  }, 4);
55
63
  }
56
64
  const cwd = configured.workingDir
@@ -36,7 +36,7 @@ export declare function downloadClientMod(context: CommandContext, options: Down
36
36
  jar: string;
37
37
  cachePath: string;
38
38
  clientName: string;
39
- launchCommandConfigured: boolean;
39
+ launchArgsConfigured: boolean;
40
40
  runtimeRootDir: string | undefined;
41
41
  runtimeVersionId: string | undefined;
42
42
  }>;
@@ -1,6 +1,7 @@
1
1
  import { access, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { DEFAULT_WS_PORT_BASE, loadConfig, writeConfig } from "../../util/config.js";
5
6
  import { MctError } from "../../util/errors.js";
6
7
  import { CacheManager } from "../CacheManager.js";
@@ -8,6 +9,11 @@ import { copyFileIfMissing, downloadFile } from "../DownloadUtils.js";
8
9
  import { detectJava } from "../JavaDetector.js";
9
10
  import { findVariantByVersionAndLoader, getDefaultVariant, getModArtifactFileName, loadModVariantCatalog } from "../ModVariantCatalog.js";
10
11
  import { prepareManagedFabricRuntime } from "./FabricRuntimeDownloader.js";
12
+ function getLaunchScriptPath() {
13
+ const thisFile = fileURLToPath(import.meta.url);
14
+ // dist/download/client/ClientDownloader.js -> scripts/launch-fabric-client.mjs
15
+ return path.resolve(path.dirname(thisFile), "..", "..", "..", "scripts", "launch-fabric-client.mjs");
16
+ }
11
17
  const GITHUB_RELEASE_BASE_URL = process.env.MCT_MOD_DOWNLOAD_BASE_URL || "https://github.com/kzheart/mc-pilot/releases/download";
12
18
  function ensureSupportedVariant(variant) {
13
19
  if (variant.loader !== "fabric") {
@@ -108,10 +114,8 @@ function resolveLaunchRuntimePaths(context, options) {
108
114
  : {})
109
115
  };
110
116
  }
111
- function buildLaunchCommand(context, runtimePaths, variant) {
117
+ function buildLaunchArgs(runtimePaths, variant) {
112
118
  return [
113
- process.execPath,
114
- path.join(context.cwd, "scripts", "launch-fabric-client.mjs"),
115
119
  "--instance-dir",
116
120
  runtimePaths.instanceDir,
117
121
  "--meta-dir",
@@ -127,10 +131,8 @@ function buildLaunchCommand(context, runtimePaths, variant) {
127
131
  variant.fabricLoaderVersion ?? "0.16.10"
128
132
  ];
129
133
  }
130
- function buildManagedLaunchCommand(context, runtimeRootDir, versionId, gameDir) {
134
+ function buildManagedLaunchArgs(runtimeRootDir, versionId, gameDir) {
131
135
  return [
132
- process.execPath,
133
- path.join(context.cwd, "scripts", "launch-fabric-client.mjs"),
134
136
  "--runtime-root",
135
137
  runtimeRootDir,
136
138
  "--version-id",
@@ -196,21 +198,22 @@ export async function downloadClientMod(context, options, dependencies = {}) {
196
198
  const managedRuntime = runtimePaths
197
199
  ? undefined
198
200
  : await prepareManagedRuntimeImpl(variant, clientRootDir, { fetchImpl });
199
- const generatedLaunchCommand = runtimePaths
200
- ? buildLaunchCommand(context, runtimePaths, variant)
201
- : buildManagedLaunchCommand(context, managedRuntime.runtimeRootDir, managedRuntime.versionId, managedRuntime.gameDir);
201
+ const generatedLaunchArgs = runtimePaths
202
+ ? buildLaunchArgs(runtimePaths, variant)
203
+ : buildManagedLaunchArgs(managedRuntime.runtimeRootDir, managedRuntime.versionId, managedRuntime.gameDir);
202
204
  latestConfig.clients[clientName] = {
203
205
  ...configuredClient,
204
206
  version: variant.minecraftVersion,
205
207
  wsPort: options.wsPort ?? configuredClient.wsPort ?? DEFAULT_WS_PORT_BASE,
206
208
  server: options.server ?? configuredClient.server ?? "localhost:25565",
207
209
  workingDir: path.relative(context.cwd, minecraftDir) || ".",
210
+ launchCommand: undefined,
211
+ launchArgs: generatedLaunchArgs,
208
212
  env: {
209
213
  ...configuredClient.env,
210
214
  MCT_CLIENT_MOD_VARIANT: variant.id,
211
215
  MCT_CLIENT_MOD_JAR: path.relative(context.cwd, targetJarPath)
212
216
  },
213
- ...(generatedLaunchCommand ? { launchCommand: generatedLaunchCommand } : {})
214
217
  };
215
218
  await writeConfig(context.configPath, context.cwd, latestConfig);
216
219
  return {
@@ -226,7 +229,7 @@ export async function downloadClientMod(context, options, dependencies = {}) {
226
229
  jar: targetJarPath,
227
230
  cachePath: artifact.cachePath,
228
231
  clientName,
229
- launchCommandConfigured: Boolean(generatedLaunchCommand),
232
+ launchArgsConfigured: Boolean(generatedLaunchArgs),
230
233
  runtimeRootDir: managedRuntime?.runtimeRootDir,
231
234
  runtimeVersionId: managedRuntime?.versionId
232
235
  };
@@ -13,6 +13,7 @@ export interface MctConfig {
13
13
  server?: string;
14
14
  headless?: boolean;
15
15
  launchCommand?: string[];
16
+ launchArgs?: string[];
16
17
  workingDir?: string;
17
18
  env?: Record<string, string>;
18
19
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Minecraft plugin/mod automated testing CLI – control a real Minecraft client to simulate player actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin/",
11
11
  "dist/",
12
12
  "data/",
13
+ "scripts/",
13
14
  "!dist/**/*.test.js",
14
15
  "!dist/**/*.test.d.ts",
15
16
  "!dist/**/*.e2e.test.js",
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import { access, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
5
+ import { readFileSync } from "node:fs";
6
+ import { execFileSync } from "node:child_process";
7
+ import path from "node:path";
8
+ import { spawn } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createQuickPlayMultiplayer, launch as launchMinecraft } from "@xmcl/core";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const VARIANTS_PATH = path.join(__dirname, "..", "data", "variants.json");
15
+
16
+ function readCatalog() {
17
+ return JSON.parse(readFileSync(VARIANTS_PATH, "utf8"));
18
+ }
19
+
20
+ function getDefaultVariant() {
21
+ const catalog = readCatalog();
22
+ return catalog.variants.find((variant) => variant.id === catalog.defaultVariant);
23
+ }
24
+
25
+ function getVariantById(variantId) {
26
+ const catalog = readCatalog();
27
+ return catalog.variants.find((variant) => variant.id === variantId);
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const parsed = {};
32
+
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const entry = argv[index];
35
+ if (!entry.startsWith("--")) {
36
+ continue;
37
+ }
38
+
39
+ const key = entry.slice(2);
40
+ const value = argv[index + 1];
41
+ if (!value || value.startsWith("--")) {
42
+ parsed[key] = "true";
43
+ continue;
44
+ }
45
+
46
+ parsed[key] = value;
47
+ index += 1;
48
+ }
49
+
50
+ return parsed;
51
+ }
52
+
53
+ function offlineUuid(username) {
54
+ const source = Buffer.from(`OfflinePlayer:${username}`, "utf8");
55
+ const digest = createHash("md5").update(source).digest();
56
+ digest[6] = (digest[6] & 0x0f) | 0x30;
57
+ digest[8] = (digest[8] & 0x3f) | 0x80;
58
+ const hex = digest.toString("hex");
59
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
60
+ }
61
+
62
+ function mavenPath(coordinate) {
63
+ const parts = coordinate.split(":");
64
+ if (parts.length < 3 || parts.length > 4) {
65
+ throw new Error(`Unsupported Maven coordinate: ${coordinate}`);
66
+ }
67
+
68
+ const [group, artifact, version, classifier] = parts;
69
+ const baseName = `${artifact}-${version}${classifier ? `-${classifier}` : ""}.jar`;
70
+ return path.join(...group.split("."), artifact, version, baseName);
71
+ }
72
+
73
+ function isRuleMatch(ruleOs = {}) {
74
+ const current = {
75
+ name: "osx-arm64",
76
+ arch: "arm64"
77
+ };
78
+
79
+ if (ruleOs.name && ruleOs.name !== "osx" && ruleOs.name !== current.name) {
80
+ return false;
81
+ }
82
+
83
+ if (ruleOs.arch && ruleOs.arch !== current.arch) {
84
+ return false;
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ function isAllowedByRules(rules) {
91
+ if (!rules || rules.length === 0) {
92
+ return true;
93
+ }
94
+
95
+ let allowed = false;
96
+ for (const rule of rules) {
97
+ if (!rule.os || isRuleMatch(rule.os)) {
98
+ allowed = rule.action === "allow";
99
+ }
100
+ }
101
+
102
+ return allowed;
103
+ }
104
+
105
+ function substitute(template, variables) {
106
+ return template.replace(/\$\{([^}]+)\}/g, (_, key) => variables[key] ?? "");
107
+ }
108
+
109
+ async function ensureFile(filePath, downloadUrl) {
110
+ try {
111
+ await access(filePath);
112
+ return;
113
+ } catch {
114
+ await mkdir(path.dirname(filePath), { recursive: true });
115
+ const response = await fetch(downloadUrl);
116
+ if (!response.ok) {
117
+ throw new Error(`Failed to download ${downloadUrl}: ${response.status}`);
118
+ }
119
+
120
+ const buffer = Buffer.from(await response.arrayBuffer());
121
+ await writeFile(filePath, buffer);
122
+ }
123
+ }
124
+
125
+ async function readJson(filePath) {
126
+ return JSON.parse(await readFile(filePath, "utf8"));
127
+ }
128
+
129
+ async function syncBuiltMod(instanceRoot, repoRoot, variantId) {
130
+ const artifactName = `mct-client-mod-${variantId.endsWith("-fabric") ? "fabric" : variantId.split("-").pop()}-${variantId.replace(/-fabric$|-forge$|-neoforge$/, "")}.jar`;
131
+ const sourceJar = path.join(repoRoot, "client-mod", "versions", variantId, "build", "libs", artifactName);
132
+ const targetDir = path.join(instanceRoot, "minecraft", "mods");
133
+ const targetJar = path.join(targetDir, artifactName);
134
+
135
+ try {
136
+ await access(sourceJar);
137
+ } catch {
138
+ return;
139
+ }
140
+
141
+ await mkdir(targetDir, { recursive: true });
142
+ await copyFile(sourceJar, targetJar);
143
+ }
144
+
145
+ async function syncConfiguredMod(gameDir) {
146
+ const configuredJar = process.env.MCT_CLIENT_MOD_JAR;
147
+ if (!configuredJar) {
148
+ return;
149
+ }
150
+
151
+ const sourceJar = path.isAbsolute(configuredJar) ? configuredJar : path.resolve(process.cwd(), configuredJar);
152
+ const targetDir = path.join(gameDir, "mods");
153
+ const targetJar = path.join(targetDir, path.basename(sourceJar));
154
+
155
+ try {
156
+ await access(sourceJar);
157
+ } catch {
158
+ return;
159
+ }
160
+
161
+ await mkdir(targetDir, { recursive: true });
162
+ await copyFile(sourceJar, targetJar);
163
+ }
164
+
165
+ async function ensureAutomationOptions(gameDir, server) {
166
+ const optionsPath = path.join(gameDir, "options.txt");
167
+ const values = new Map();
168
+
169
+ try {
170
+ const content = await readFile(optionsPath, "utf8");
171
+ for (const line of content.split(/\r?\n/)) {
172
+ const separator = line.indexOf(":");
173
+ if (separator < 0) {
174
+ continue;
175
+ }
176
+ values.set(line.slice(0, separator), line.slice(separator + 1));
177
+ }
178
+ } catch {}
179
+
180
+ values.set("onboardAccessibility", "false");
181
+ values.set("skipMultiplayerWarning", "true");
182
+ values.set("skipRealms32bitWarning", "true");
183
+ values.set("joinedFirstServer", "true");
184
+ values.set("tutorialStep", "none");
185
+ values.set("pauseOnLostFocus", "false");
186
+ if (server) {
187
+ values.set("lastServer", server);
188
+ }
189
+
190
+ const lines = [...values.entries()].map(([key, value]) => `${key}:${value}`);
191
+ await mkdir(gameDir, { recursive: true });
192
+ await writeFile(optionsPath, `${lines.join("\n")}\n`, "utf8");
193
+ }
194
+
195
+ async function buildLaunchSpec(options) {
196
+ const repoRoot = process.cwd();
197
+ const defaultVariant = getDefaultVariant();
198
+ const minecraftVersion = process.env.MCT_CLIENT_VERSION || options["minecraft-version"] || defaultVariant.minecraftVersion;
199
+ const modVariantId = process.env.MCT_CLIENT_MOD_VARIANT || options["mod-variant"] || `${minecraftVersion}-fabric`;
200
+ const selectedVariant = getVariantById(modVariantId) ?? defaultVariant;
201
+ const fabricLoaderVersion = options["fabric-loader-version"] || selectedVariant.fabricLoaderVersion || "0.16.10";
202
+ const instanceRoot = options["instance-dir"];
203
+ const metaRoot = options["meta-dir"];
204
+ const librariesRoot = options["libraries-dir"];
205
+ const assetsRoot = options["assets-dir"];
206
+ const nativesDir = options["natives-dir"] || path.join(instanceRoot, "natives");
207
+ const gameDir = path.join(instanceRoot, "minecraft");
208
+ const packMeta = await readJson(path.join(instanceRoot, "mmc-pack.json"));
209
+ await syncBuiltMod(instanceRoot, repoRoot, modVariantId);
210
+ const componentMetas = new Map();
211
+ for (const component of packMeta.components) {
212
+ const componentMetaPath = path.join(metaRoot, component.uid, `${component.version}.json`);
213
+ componentMetas.set(component.uid, await readJson(componentMetaPath));
214
+ }
215
+
216
+ const vanillaMeta = componentMetas.get("net.minecraft");
217
+ const fabricMeta = componentMetas.get("net.fabricmc.fabric-loader");
218
+ const intermediaryMeta = componentMetas.get("net.fabricmc.intermediary");
219
+
220
+ const libraries = [];
221
+ for (const component of packMeta.components) {
222
+ const meta = componentMetas.get(component.uid);
223
+ for (const library of meta.libraries ?? []) {
224
+ if (!isAllowedByRules(library.rules)) {
225
+ continue;
226
+ }
227
+
228
+ libraries.push({
229
+ coordinate: library.name,
230
+ path: library.downloads?.artifact?.path ?? mavenPath(library.name),
231
+ url:
232
+ library.downloads?.artifact?.url ??
233
+ `${library.url.replace(/\/$/, "")}/${mavenPath(library.name)}`
234
+ });
235
+ }
236
+ }
237
+
238
+ const mainJarPath =
239
+ vanillaMeta.mainJar?.downloads?.artifact?.path ??
240
+ mavenPath(vanillaMeta.mainJar?.name ?? `com.mojang:minecraft:${minecraftVersion}:client`);
241
+ const mainJarUrl = vanillaMeta.mainJar?.downloads?.artifact?.url;
242
+
243
+ for (const entry of libraries) {
244
+ if (entry.url) {
245
+ await ensureFile(path.join(librariesRoot, entry.path), entry.url);
246
+ }
247
+ }
248
+
249
+ if (mainJarUrl) {
250
+ await ensureFile(path.join(librariesRoot, mainJarPath), mainJarUrl);
251
+ }
252
+
253
+ await mkdir(nativesDir, { recursive: true });
254
+
255
+ const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
256
+ const accountUuid = offlineUuid(accountName);
257
+ const server = process.env.MCT_CLIENT_SERVER || "";
258
+ await ensureAutomationOptions(gameDir, server);
259
+ const [serverHost, serverPort = "25565"] = server.split(":");
260
+ const classpath = [
261
+ path.join(librariesRoot, mainJarPath),
262
+ ...libraries.map((entry) => path.join(librariesRoot, entry.path))
263
+ ].join(path.delimiter);
264
+ const substitutions = {
265
+ auth_player_name: accountName,
266
+ version_name: `fabric-loader-${fabricLoaderVersion}-${minecraftVersion}`,
267
+ game_directory: gameDir,
268
+ assets_root: assetsRoot,
269
+ assets_index_name: vanillaMeta.assetIndex.id,
270
+ auth_uuid: accountUuid,
271
+ auth_access_token: "0",
272
+ user_type: "legacy",
273
+ version_type: "release"
274
+ };
275
+ const gameArgs = vanillaMeta.minecraftArguments
276
+ .split(" ")
277
+ .filter(Boolean)
278
+ .map((entry) => substitute(entry, substitutions));
279
+
280
+ if (serverHost) {
281
+ gameArgs.push("--quickPlayMultiplayer", `${serverHost}:${serverPort}`);
282
+ }
283
+
284
+ return {
285
+ cwd: gameDir,
286
+ classpathEntries: [path.join(librariesRoot, mainJarPath), ...libraries.map((entry) => path.join(librariesRoot, entry.path))],
287
+ classpath,
288
+ gameArgs,
289
+ mainClass: fabricMeta.mainClass,
290
+ nativesDir,
291
+ javaBin: options.java || process.env.MCT_CLIENT_JAVA || "java",
292
+ javaArgs: [
293
+ "-XstartOnFirstThread",
294
+ "-Xms512m",
295
+ `-Xmx${options["max-mem"] || "1024m"}`,
296
+ "-Duser.language=en",
297
+ `-Djava.library.path=${nativesDir}`,
298
+ "-DFabricMcEmu=net.minecraft.client.main.Main"
299
+ ]
300
+ };
301
+ }
302
+
303
+ async function buildManifestLaunchSpec(options) {
304
+ const manifestPath = options.manifest;
305
+ if (!manifestPath) {
306
+ throw new Error("Missing required argument: --manifest");
307
+ }
308
+
309
+ const manifest = await readJson(manifestPath);
310
+ const gameDir = manifest.gameDir;
311
+ await syncConfiguredMod(gameDir);
312
+
313
+ const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
314
+ const accountUuid = offlineUuid(accountName);
315
+ const server = process.env.MCT_CLIENT_SERVER || "";
316
+ await ensureAutomationOptions(gameDir, server);
317
+ const [serverHost, serverPort = "25565"] = server.split(":");
318
+ const substitutions = {
319
+ auth_player_name: accountName,
320
+ version_name: `fabric-loader-${manifest.fabricLoaderVersion}-${manifest.minecraftVersion}`,
321
+ game_directory: gameDir,
322
+ assets_root: manifest.assetsDir,
323
+ assets_index_name: manifest.assetsIndexId,
324
+ auth_uuid: accountUuid,
325
+ auth_access_token: "0",
326
+ user_type: "legacy",
327
+ version_type: "release",
328
+ natives_directory: path.join(manifest.runtimeRootDir ?? path.dirname(manifestPath), "natives")
329
+ };
330
+ const gameArgs = manifest.gameArgs.map((entry) => substitute(entry, substitutions));
331
+ if (serverHost && !gameArgs.includes("--quickPlayMultiplayer")) {
332
+ gameArgs.push("--quickPlayMultiplayer", `${serverHost}:${serverPort}`);
333
+ }
334
+
335
+ const javaArgs = manifest.javaArgs
336
+ .map((entry) => substitute(entry, substitutions))
337
+ .filter((entry) => !entry.includes("${"));
338
+
339
+ return {
340
+ cwd: gameDir,
341
+ classpathEntries: manifest.classpathEntries,
342
+ classpath: manifest.classpathEntries.join(path.delimiter),
343
+ gameArgs,
344
+ mainClass: manifest.mainClass,
345
+ javaBin: options.java || process.env.MCT_CLIENT_JAVA || "java",
346
+ javaArgs
347
+ };
348
+ }
349
+
350
+ function parseMaxMemory(value) {
351
+ if (!value) {
352
+ return 1024;
353
+ }
354
+
355
+ const normalized = String(value).trim().toLowerCase();
356
+ if (normalized.endsWith("g")) {
357
+ return Number.parseInt(normalized.slice(0, -1), 10) * 1024;
358
+ }
359
+ if (normalized.endsWith("m")) {
360
+ return Number.parseInt(normalized.slice(0, -1), 10);
361
+ }
362
+ return Number.parseInt(normalized, 10);
363
+ }
364
+
365
+ async function launchXmclManagedClient(options) {
366
+ const runtimeRoot = options["runtime-root"];
367
+ const versionId = options["version-id"];
368
+ const gameDir = options["game-dir"];
369
+
370
+ if (!runtimeRoot || !versionId || !gameDir) {
371
+ throw new Error("Missing required arguments: --runtime-root, --version-id and --game-dir");
372
+ }
373
+
374
+ await syncConfiguredMod(gameDir);
375
+
376
+ const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
377
+ const accountUuid = offlineUuid(accountName).replaceAll("-", "");
378
+ const server = process.env.MCT_CLIENT_SERVER || "";
379
+ await ensureAutomationOptions(gameDir, server);
380
+ const [serverHost, serverPort = "25565"] = server.split(":");
381
+
382
+ return launchMinecraft({
383
+ gamePath: gameDir,
384
+ resourcePath: runtimeRoot,
385
+ javaPath: options.java || process.env.MCT_CLIENT_JAVA || "java",
386
+ minMemory: 512,
387
+ maxMemory: parseMaxMemory(options["max-mem"]),
388
+ version: versionId,
389
+ gameProfile: {
390
+ name: accountName,
391
+ id: accountUuid
392
+ },
393
+ accessToken: "0",
394
+ userType: "legacy",
395
+ launcherName: "mct",
396
+ launcherBrand: "mct",
397
+ ...(serverHost
398
+ ? {
399
+ quickPlayMultiplayer: createQuickPlayMultiplayer(serverHost, Number.parseInt(serverPort, 10)),
400
+ // Legacy fallback for MC < 1.19.1 that ignores --quickPlayMultiplayer.
401
+ // Don't pass both to 1.19.1+ to avoid "Attempt to connect while already connecting".
402
+ extraMCArgs: versionId.startsWith("1.18.") || versionId.startsWith("1.12.")
403
+ ? ["--server", serverHost, "--port", serverPort]
404
+ : []
405
+ }
406
+ : {}),
407
+ extraExecOption: {
408
+ cwd: gameDir,
409
+ env: {
410
+ ...process.env
411
+ },
412
+ stdio: "inherit"
413
+ }
414
+ });
415
+ }
416
+
417
+ async function main() {
418
+ const options = parseArgs(process.argv.slice(2));
419
+ if (options["runtime-root"]) {
420
+ const child = await launchXmclManagedClient(options);
421
+
422
+ const forwardSignal = (signal) => {
423
+ if (!child.killed) {
424
+ child.kill(signal);
425
+ }
426
+ };
427
+
428
+ process.on("SIGINT", forwardSignal);
429
+ process.on("SIGTERM", forwardSignal);
430
+
431
+ child.on("exit", (code, signal) => {
432
+ if (signal) {
433
+ process.kill(process.pid, signal);
434
+ return;
435
+ }
436
+ process.exit(code ?? 0);
437
+ });
438
+ return;
439
+ }
440
+
441
+ const launch = options.manifest
442
+ ? await buildManifestLaunchSpec(options)
443
+ : await (async () => {
444
+ const instanceDir = options["instance-dir"];
445
+ const metaDir = options["meta-dir"];
446
+ const librariesDir = options["libraries-dir"];
447
+ const assetsDir = options["assets-dir"];
448
+ if (!instanceDir || !metaDir || !librariesDir || !assetsDir) {
449
+ throw new Error("Missing required arguments: --manifest or --instance-dir, --meta-dir, --libraries-dir and --assets-dir");
450
+ }
451
+ return buildLaunchSpec(options);
452
+ })();
453
+ console.log(`[mct-launch] mainClass=${launch.mainClass}`);
454
+ console.log(`[mct-launch] classpathEntries=${launch.classpathEntries.length}`);
455
+ console.log(
456
+ `[mct-launch] lwjglCorePresent=${launch.classpathEntries.some((entry) => entry.endsWith(`${path.sep}org${path.sep}lwjgl${path.sep}lwjgl${path.sep}3.3.2${path.sep}lwjgl-3.3.2.jar`))}`
457
+ );
458
+ console.log(`[mct-launch] gameArgs=${launch.gameArgs.join(" ")}`);
459
+ const child = spawn(
460
+ launch.javaBin,
461
+ [...launch.javaArgs, "-cp", launch.classpath, launch.mainClass, ...launch.gameArgs],
462
+ {
463
+ cwd: launch.cwd,
464
+ env: {
465
+ ...process.env
466
+ },
467
+ stdio: "inherit"
468
+ }
469
+ );
470
+
471
+ const forwardSignal = (signal) => {
472
+ if (!child.killed) {
473
+ child.kill(signal);
474
+ }
475
+ };
476
+
477
+ process.on("SIGINT", forwardSignal);
478
+ process.on("SIGTERM", forwardSignal);
479
+
480
+ child.on("exit", (code, signal) => {
481
+ if (signal) {
482
+ process.kill(process.pid, signal);
483
+ return;
484
+ }
485
+ process.exit(code ?? 0);
486
+ });
487
+ }
488
+
489
+ main().catch((error) => {
490
+ console.error(error instanceof Error ? error.stack : String(error));
491
+ process.exit(1);
492
+ });