@kzheart_/mc-pilot 0.1.0 → 0.1.2
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/bin/mct
CHANGED
|
@@ -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") {
|
|
@@ -111,7 +117,7 @@ function resolveLaunchRuntimePaths(context, options) {
|
|
|
111
117
|
function buildLaunchCommand(context, runtimePaths, variant) {
|
|
112
118
|
return [
|
|
113
119
|
process.execPath,
|
|
114
|
-
|
|
120
|
+
getLaunchScriptPath(),
|
|
115
121
|
"--instance-dir",
|
|
116
122
|
runtimePaths.instanceDir,
|
|
117
123
|
"--meta-dir",
|
|
@@ -130,7 +136,7 @@ function buildLaunchCommand(context, runtimePaths, variant) {
|
|
|
130
136
|
function buildManagedLaunchCommand(context, runtimeRootDir, versionId, gameDir) {
|
|
131
137
|
return [
|
|
132
138
|
process.execPath,
|
|
133
|
-
|
|
139
|
+
getLaunchScriptPath(),
|
|
134
140
|
"--runtime-root",
|
|
135
141
|
runtimeRootDir,
|
|
136
142
|
"--version-id",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kzheart_/mc-pilot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
+
});
|