@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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
200
|
-
?
|
|
201
|
-
:
|
|
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
|
-
|
|
232
|
+
launchArgsConfigured: Boolean(generatedLaunchArgs),
|
|
230
233
|
runtimeRootDir: managedRuntime?.runtimeRootDir,
|
|
231
234
|
runtimeVersionId: managedRuntime?.versionId
|
|
232
235
|
};
|
package/dist/util/config.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kzheart_/mc-pilot",
|
|
3
|
-
"version": "0.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
|
+
});
|