@kzheart_/mc-pilot 0.9.5 → 0.9.7
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/data/variants.json +13 -1
- package/dist/commands/client.js +5 -3
- package/dist/commands/craft.d.ts +1 -0
- package/dist/commands/craft.js +29 -3
- package/dist/commands/events.js +33 -8
- package/dist/commands/gui.js +20 -3
- package/dist/commands/project.js +17 -3
- package/dist/commands/screenshot.js +37 -1
- package/dist/commands/server.js +25 -0
- package/dist/commands/wait-log.d.ts +2 -0
- package/dist/commands/wait-log.js +31 -0
- package/dist/download/VersionMatrix.js +15 -0
- package/dist/download/client/FabricRuntimeDownloader.js +6 -3
- package/dist/index.js +4 -1
- package/dist/instance/ClientInstanceManager.d.ts +2 -0
- package/dist/instance/ClientInstanceManager.js +6 -2
- package/dist/instance/ServerInstanceManager.d.ts +33 -0
- package/dist/instance/ServerInstanceManager.js +57 -3
- package/dist/util/instance-types.d.ts +5 -0
- package/dist/util/project.js +16 -9
- package/package.json +1 -1
- package/scripts/launch-fabric-client.mjs +41 -9
package/data/variants.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"defaultVariant": "1.20.4-fabric",
|
|
3
3
|
"buildSystem": "multi-module",
|
|
4
|
-
"versions": ["1.18.2", "1.20.1", "1.20.2", "1.20.4", "1.21.1", "1.21.4"],
|
|
4
|
+
"versions": ["1.18.2", "1.20.1", "1.20.2", "1.20.4", "1.21.1", "1.21.4", "1.21.11"],
|
|
5
5
|
"variants": [
|
|
6
6
|
{
|
|
7
7
|
"id": "1.18.2-fabric",
|
|
@@ -137,6 +137,18 @@
|
|
|
137
137
|
"modVersion": "0.9.1",
|
|
138
138
|
"neoforgeVersion": "21.4.75",
|
|
139
139
|
"javaVersion": 21
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "1.21.11-fabric",
|
|
143
|
+
"minecraftVersion": "1.21.11",
|
|
144
|
+
"loader": "fabric",
|
|
145
|
+
"support": "ready",
|
|
146
|
+
"validation": "verified",
|
|
147
|
+
"modVersion": "0.9.1",
|
|
148
|
+
"fabricLoaderVersion": "0.19.2",
|
|
149
|
+
"yarnMappings": "1.21.11+build.5",
|
|
150
|
+
"javaVersion": 21,
|
|
151
|
+
"gradleModule": "version-1.21.11"
|
|
140
152
|
}
|
|
141
153
|
]
|
|
142
154
|
}
|
package/dist/commands/client.js
CHANGED
|
@@ -48,7 +48,7 @@ export function createClientCommand() {
|
|
|
48
48
|
.option("--ws-port <port>", "WebSocket port (auto-assigned if omitted)", Number)
|
|
49
49
|
.option("--account <account>", "Offline username or account identifier")
|
|
50
50
|
.option("--headless", "Launch in headless mode")
|
|
51
|
-
.option("--mute", "Mute all in-game audio for this client")
|
|
51
|
+
.option("--mute", "Mute all in-game audio for this client (default)")
|
|
52
52
|
.option("--no-mute", "Keep in-game audio enabled for this client")
|
|
53
53
|
.option("--java <command>", "Java command to use")
|
|
54
54
|
.action(wrapCommand(async (_context, { args, options }) => {
|
|
@@ -74,7 +74,9 @@ export function createClientCommand() {
|
|
|
74
74
|
env: {
|
|
75
75
|
MCT_CLIENT_MOD_VARIANT: downloaded.variantId,
|
|
76
76
|
MCT_CLIENT_MOD_JAR: downloaded.jar
|
|
77
|
-
}
|
|
77
|
+
},
|
|
78
|
+
javaCommand: downloaded.javaCommand,
|
|
79
|
+
javaVersion: downloaded.javaVersion
|
|
78
80
|
});
|
|
79
81
|
return {
|
|
80
82
|
created: true,
|
|
@@ -94,7 +96,7 @@ export function createClientCommand() {
|
|
|
94
96
|
.option("--account <account>", "Offline username or account identifier")
|
|
95
97
|
.option("--ws-port <port>", "WebSocket port override", Number)
|
|
96
98
|
.option("--headless", "Launch in headless mode")
|
|
97
|
-
.option("--mute", "Mute all in-game audio for this launch")
|
|
99
|
+
.option("--mute", "Mute all in-game audio for this launch (default)")
|
|
98
100
|
.option("--no-mute", "Keep in-game audio enabled for this launch")
|
|
99
101
|
.option("--force", "Kill any existing client with the same name before launching")
|
|
100
102
|
.action(wrapCommand(async (context, { args, options }) => {
|
package/dist/commands/craft.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare function createCraftCommand(): Command;
|
|
3
|
+
export declare function createRecipeCommand(): Command;
|
|
3
4
|
export declare function createAnvilCommand(): Command;
|
|
4
5
|
export declare function createEnchantCommand(): Command;
|
|
5
6
|
export declare function createTradeCommand(): Command;
|
package/dist/commands/craft.js
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { MctError } from "../util/errors.js";
|
|
2
3
|
import { createRequestAction, parseJson } from "./request-helpers.js";
|
|
4
|
+
function normalizeCraftRecipe(raw) {
|
|
5
|
+
if (Array.isArray(raw)) {
|
|
6
|
+
return raw;
|
|
7
|
+
}
|
|
8
|
+
if (raw && typeof raw === "object" && Array.isArray(raw.slots)) {
|
|
9
|
+
const slots = raw.slots;
|
|
10
|
+
if (slots.length !== 9) {
|
|
11
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "recipe.slots must contain exactly 9 entries" }, 4);
|
|
12
|
+
}
|
|
13
|
+
return [slots.slice(0, 3), slots.slice(3, 6), slots.slice(6, 9)];
|
|
14
|
+
}
|
|
15
|
+
throw new MctError({
|
|
16
|
+
code: "INVALID_PARAMS",
|
|
17
|
+
message: "recipe must be a 3x3 row array or {\"slots\":[...9 entries...]}"
|
|
18
|
+
}, 4);
|
|
19
|
+
}
|
|
3
20
|
export function createCraftCommand() {
|
|
4
21
|
return new Command("craft")
|
|
5
22
|
.description("Crafting table recipe\n" +
|
|
6
23
|
"Prerequisite: open crafting table with \"block interact <x> <y> <z>\"\n" +
|
|
7
24
|
"Auto-places materials from inventory, crafts, and moves result to inventory.\n" +
|
|
8
25
|
"GUI closes automatically after crafting.")
|
|
9
|
-
.requiredOption("--recipe <json>", "Recipe JSON: 9
|
|
26
|
+
.requiredOption("--recipe <json>", "Recipe JSON: 3 row arrays, or {\"slots\":[...9 entries...]} in row-major order.\n" +
|
|
10
27
|
"Use item IDs without namespace (e.g. \"oak_planks\", not \"minecraft:oak_planks\").\n" +
|
|
11
|
-
"Example: '
|
|
12
|
-
.action(createRequestAction("craft.craft", ({ options }) => ({ recipe: parseJson(String(options.recipe), "recipe") })));
|
|
28
|
+
"Example: '[[\"oak_planks\",null,null],[\"oak_planks\",null,null],[null,null,null]]'")
|
|
29
|
+
.action(createRequestAction("craft.craft", ({ options }) => ({ recipe: normalizeCraftRecipe(parseJson(String(options.recipe), "recipe")) })));
|
|
30
|
+
}
|
|
31
|
+
export function createRecipeCommand() {
|
|
32
|
+
const command = new Command("recipe").description("Task-level recipe helpers");
|
|
33
|
+
command
|
|
34
|
+
.command("craft-table")
|
|
35
|
+
.description("Craft with the currently open crafting table")
|
|
36
|
+
.requiredOption("--recipe <json>", "Recipe JSON: 3 row arrays or {\"slots\":[...9 entries...]}")
|
|
37
|
+
.action(createRequestAction("craft.craft", ({ options }) => ({ recipe: normalizeCraftRecipe(parseJson(String(options.recipe), "recipe")) })));
|
|
38
|
+
return command;
|
|
13
39
|
}
|
|
14
40
|
export function createAnvilCommand() {
|
|
15
41
|
return new Command("anvil")
|
package/dist/commands/events.js
CHANGED
|
@@ -13,6 +13,20 @@ function resolveEventsFile(clientName) {
|
|
|
13
13
|
}
|
|
14
14
|
return path.join(resolveMctHome(), "logs", clientName, "events.jsonl");
|
|
15
15
|
}
|
|
16
|
+
function resolveEventClientNames(context, explicitClient, allClients) {
|
|
17
|
+
if (allClients) {
|
|
18
|
+
const clients = context.activeProfile?.clients ?? [];
|
|
19
|
+
if (clients.length === 0) {
|
|
20
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "--all-clients requires an active profile with clients" }, 4);
|
|
21
|
+
}
|
|
22
|
+
return clients;
|
|
23
|
+
}
|
|
24
|
+
const clientName = explicitClient ?? context.activeProfile?.clients[0];
|
|
25
|
+
if (!clientName) {
|
|
26
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "Client name is required. Use --client <name> or set an active profile." }, 4);
|
|
27
|
+
}
|
|
28
|
+
return [clientName];
|
|
29
|
+
}
|
|
16
30
|
function parseSince(raw, nowMs) {
|
|
17
31
|
if (!raw)
|
|
18
32
|
return undefined;
|
|
@@ -176,18 +190,29 @@ export function createEventsCommand() {
|
|
|
176
190
|
command
|
|
177
191
|
.command("clear")
|
|
178
192
|
.description("Truncate the event log for the active client")
|
|
193
|
+
.option("--all-clients", "Clear event logs for every client in the active profile")
|
|
179
194
|
.option("--file <path>", "Override the log file path")
|
|
180
195
|
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
181
196
|
const { truncateSync, existsSync: exists, mkdirSync } = await import("node:fs");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
197
|
+
if (options.file) {
|
|
198
|
+
if (exists(options.file)) {
|
|
199
|
+
truncateSync(options.file, 0);
|
|
200
|
+
return { cleared: true, file: options.file };
|
|
201
|
+
}
|
|
202
|
+
mkdirSync(path.dirname(options.file), { recursive: true });
|
|
203
|
+
return { cleared: false, reason: "file_not_found", file: options.file };
|
|
187
204
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
205
|
+
const clients = resolveEventClientNames(context, globalOptions.client, Boolean(options.allClients));
|
|
206
|
+
const results = clients.map((clientName) => {
|
|
207
|
+
const filePath = resolveEventsFile(clientName);
|
|
208
|
+
if (exists(filePath)) {
|
|
209
|
+
truncateSync(filePath, 0);
|
|
210
|
+
return { client: clientName, cleared: true, file: filePath };
|
|
211
|
+
}
|
|
212
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
213
|
+
return { client: clientName, cleared: false, reason: "file_not_found", file: filePath };
|
|
214
|
+
});
|
|
215
|
+
return { cleared: results.some((result) => result.cleared), clients: results };
|
|
191
216
|
}));
|
|
192
217
|
command
|
|
193
218
|
.command("path")
|
package/dist/commands/gui.js
CHANGED
|
@@ -46,10 +46,27 @@ export function createGuiCommand() {
|
|
|
46
46
|
.command("screenshot")
|
|
47
47
|
.description("Take a screenshot of the current GUI")
|
|
48
48
|
.option("--output <path>", "Output file path (default: project screenshot directory)")
|
|
49
|
+
.option("--timeout <seconds>", "Screenshot response timeout in seconds (default 30)", Number)
|
|
49
50
|
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
50
|
-
return sendClientRequest(context, globalOptions.client ?? context.activeProfile?.clients[0], "gui.screenshot", {
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
return sendClientRequest(context, globalOptions.client ?? context.activeProfile?.clients[0], "gui.screenshot", { output: resolveScreenshotOutputPath(context, options.output, "gui") }, options.timeout ?? Math.max(30, context.timeout("default")));
|
|
52
|
+
}));
|
|
53
|
+
command
|
|
54
|
+
.command("click-title")
|
|
55
|
+
.description("Click a GUI slot by title text match")
|
|
56
|
+
.argument("<title>", "Regex matched against item displayName or type")
|
|
57
|
+
.option("--button <button>", "Click button: left|right|middle|shift-left|shift-right", "left")
|
|
58
|
+
.action(wrapCommand(async (context, { args, options, globalOptions }) => {
|
|
59
|
+
const clientName = globalOptions.client ?? context.activeProfile?.clients[0];
|
|
60
|
+
const snapshot = await sendClientRequest(context, clientName, "gui.snapshot", {});
|
|
61
|
+
const slots = ((snapshot.data?.data?.slots)
|
|
62
|
+
?? (snapshot.data?.slots)
|
|
63
|
+
?? []);
|
|
64
|
+
const pattern = new RegExp(String(args[0]));
|
|
65
|
+
const found = slots.find((slot) => slot.item && (pattern.test(String(slot.item.displayName ?? "")) || pattern.test(String(slot.item.type ?? ""))));
|
|
66
|
+
if (!found || found.slot === undefined) {
|
|
67
|
+
return { clicked: false, matched: false, title: args[0], slots: slots.length };
|
|
68
|
+
}
|
|
69
|
+
return sendClientRequest(context, clientName, "gui.click", { slot: found.slot, button: options.button ?? "left" });
|
|
53
70
|
}));
|
|
54
71
|
return command;
|
|
55
72
|
}
|
package/dist/commands/project.js
CHANGED
|
@@ -53,6 +53,8 @@ export function createUpCommand() {
|
|
|
53
53
|
.description("Deploy plugins, start server and clients, wait for ready")
|
|
54
54
|
.option("--profile <name>", "Profile name")
|
|
55
55
|
.option("--eula", "Auto-accept EULA")
|
|
56
|
+
.option("--server-only-ok", "Only deploy/start/wait for the server; skip launching and waiting for clients")
|
|
57
|
+
.option("--skip-client-ready", "Launch/reconnect clients but do not wait for them to join a world")
|
|
56
58
|
.action(wrapCommand(async (context, { options }) => {
|
|
57
59
|
const { projectFile, projectId, projectRootDir } = context;
|
|
58
60
|
if (!projectFile || !projectId || !projectRootDir) {
|
|
@@ -73,7 +75,12 @@ export function createUpCommand() {
|
|
|
73
75
|
results.server = await serverManager.start(profile.server, { eula: options.eula });
|
|
74
76
|
// 3. Wait for server
|
|
75
77
|
const serverMeta = await serverManager.loadMeta(profile.server);
|
|
76
|
-
await serverManager.waitReady(profile.server, context.timeout("serverReady"));
|
|
78
|
+
results.serverReady = await serverManager.waitReady(profile.server, context.timeout("serverReady"));
|
|
79
|
+
if (options.serverOnlyOk) {
|
|
80
|
+
results.ready = true;
|
|
81
|
+
results.clientsSkipped = true;
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
77
84
|
// 4. Launch clients (reuse running clients via reconnect)
|
|
78
85
|
const serverAddress = `localhost:${serverMeta.port}`;
|
|
79
86
|
const clientResults = [];
|
|
@@ -89,8 +96,15 @@ export function createUpCommand() {
|
|
|
89
96
|
}
|
|
90
97
|
results.clients = clientResults;
|
|
91
98
|
// 5. Wait for clients (WS connected + in-world)
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
if (options.skipClientReady) {
|
|
100
|
+
results.clientReadySkipped = true;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const readyClients = [];
|
|
104
|
+
for (const clientName of profile.clients) {
|
|
105
|
+
readyClients.push(await clientManager.waitReady(clientName, context.timeout("clientReady")));
|
|
106
|
+
}
|
|
107
|
+
results.clientReady = readyClients;
|
|
94
108
|
}
|
|
95
109
|
results.ready = true;
|
|
96
110
|
return results;
|
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import { ClientInstanceManager } from "../instance/ClientInstanceManager.js";
|
|
2
3
|
import { wrapCommand } from "../util/command.js";
|
|
4
|
+
import { MctError } from "../util/errors.js";
|
|
3
5
|
import { resolveScreenshotOutputPath, sendClientRequest } from "./request-helpers.js";
|
|
6
|
+
async function captureWithRetry(context, clientName, params, options) {
|
|
7
|
+
const timeout = options.timeout ?? Math.max(30, context.timeout("default"));
|
|
8
|
+
const retries = options.retries ?? 1;
|
|
9
|
+
let lastError;
|
|
10
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
11
|
+
try {
|
|
12
|
+
return await sendClientRequest(context, clientName, "capture.screenshot", params, timeout);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
lastError = error;
|
|
16
|
+
if (attempt < retries) {
|
|
17
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const manager = new ClientInstanceManager(context.globalState);
|
|
22
|
+
const diagnostics = await manager.getClient(clientName).catch((error) => ({ unavailable: String(error.message) }));
|
|
23
|
+
throw new MctError({
|
|
24
|
+
code: "TIMEOUT",
|
|
25
|
+
message: `Screenshot request failed after ${retries + 1} attempt(s) with ${timeout}s timeout`,
|
|
26
|
+
details: {
|
|
27
|
+
client: clientName ?? context.activeProfile?.clients[0] ?? null,
|
|
28
|
+
timeout,
|
|
29
|
+
retries,
|
|
30
|
+
lastError: lastError instanceof Error ? lastError.message : String(lastError),
|
|
31
|
+
diagnostics
|
|
32
|
+
}
|
|
33
|
+
}, 2);
|
|
34
|
+
}
|
|
4
35
|
export function createScreenshotCommand() {
|
|
5
36
|
return new Command("screenshot")
|
|
6
37
|
.description("Take a screenshot")
|
|
7
38
|
.option("--output <path>", "Output file path (default: project screenshot directory)")
|
|
8
39
|
.option("--region <region>", "Capture a sub-region, format: x,y,w,h")
|
|
9
40
|
.option("--gui", "Capture the current GUI screen")
|
|
41
|
+
.option("--timeout <seconds>", "Screenshot response timeout in seconds (default 30)", Number)
|
|
42
|
+
.option("--retries <count>", "Retry count after timeout/failure (default 1)", Number)
|
|
10
43
|
.action(wrapCommand(async (context, { options, globalOptions }) => {
|
|
11
|
-
return
|
|
44
|
+
return captureWithRetry(context, globalOptions.client ?? context.activeProfile?.clients[0], {
|
|
12
45
|
output: resolveScreenshotOutputPath(context, options.output, "screenshot"),
|
|
13
46
|
region: options.region,
|
|
14
47
|
gui: Boolean(options.gui)
|
|
48
|
+
}, {
|
|
49
|
+
timeout: options.timeout,
|
|
50
|
+
retries: options.retries
|
|
15
51
|
});
|
|
16
52
|
}));
|
|
17
53
|
}
|
package/dist/commands/server.js
CHANGED
|
@@ -122,6 +122,16 @@ export function createServerCommand() {
|
|
|
122
122
|
const manager = new ServerInstanceManager(context.globalState, project);
|
|
123
123
|
return manager.waitReady(serverName, options.timeout ?? context.timeout("serverReady"));
|
|
124
124
|
}));
|
|
125
|
+
command
|
|
126
|
+
.command("readiness")
|
|
127
|
+
.description("Report process, port and log readiness diagnostics")
|
|
128
|
+
.argument("[name]", "Server instance name (default: from active profile)")
|
|
129
|
+
.action(wrapCommand(async (context, { args }) => {
|
|
130
|
+
const project = requireProject(context);
|
|
131
|
+
const serverName = resolveServerName(context, args[0]);
|
|
132
|
+
const manager = new ServerInstanceManager(context.globalState, project);
|
|
133
|
+
return manager.readiness(serverName);
|
|
134
|
+
}));
|
|
125
135
|
command
|
|
126
136
|
.command("exec")
|
|
127
137
|
.description("Send a console command directly to the server stdin FIFO (bypasses client chat)")
|
|
@@ -140,6 +150,8 @@ export function createServerCommand() {
|
|
|
140
150
|
.option("--tail <n>", "Show only the last N lines", Number)
|
|
141
151
|
.option("--grep <pattern>", "Filter lines by regex")
|
|
142
152
|
.option("--since <lineNumber>", "Skip the first N lines (0-indexed)", Number)
|
|
153
|
+
.option("--since-start", "Only read log content written after the current server process was started")
|
|
154
|
+
.option("--after-marker <marker>", "Only read log lines after the last line containing this marker")
|
|
143
155
|
.option("--follow", "Wait for new log lines (requires --timeout)")
|
|
144
156
|
.option("--timeout <seconds>", "Max seconds to wait when --follow is set", Number)
|
|
145
157
|
.option("--first-match", "With --follow, exit as soon as the first matching line appears")
|
|
@@ -161,8 +173,21 @@ export function createServerCommand() {
|
|
|
161
173
|
tail: options.tail,
|
|
162
174
|
grep: options.grep,
|
|
163
175
|
since: options.since,
|
|
176
|
+
sinceStart: Boolean(options.sinceStart),
|
|
177
|
+
afterMarker: options.afterMarker,
|
|
164
178
|
rawColors: Boolean(options.rawColors)
|
|
165
179
|
});
|
|
166
180
|
}));
|
|
181
|
+
command
|
|
182
|
+
.command("logs-mark")
|
|
183
|
+
.description("Append a marker line to the server log and return the marker")
|
|
184
|
+
.argument("[name]", "Server instance name (default: from active profile)")
|
|
185
|
+
.option("--label <label>", "Optional marker label")
|
|
186
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
187
|
+
const project = requireProject(context);
|
|
188
|
+
const serverName = resolveServerName(context, args[0]);
|
|
189
|
+
const manager = new ServerInstanceManager(context.globalState, project);
|
|
190
|
+
return manager.markLogs(serverName, options.label);
|
|
191
|
+
}));
|
|
167
192
|
return command;
|
|
168
193
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
|
|
3
|
+
import { MctError } from "../util/errors.js";
|
|
4
|
+
import { wrapCommand } from "../util/command.js";
|
|
5
|
+
function requireProject(context) {
|
|
6
|
+
if (!context.projectId) {
|
|
7
|
+
throw new MctError({ code: "NO_PROJECT", message: "No project context. Run inside an mct project or use --project <id>." }, 4);
|
|
8
|
+
}
|
|
9
|
+
return context.projectId;
|
|
10
|
+
}
|
|
11
|
+
export function createWaitLogCommand() {
|
|
12
|
+
return new Command("wait-log")
|
|
13
|
+
.description("Wait for a server log line matching a regex")
|
|
14
|
+
.option("--server <name>", "Server instance name (default: from active profile)")
|
|
15
|
+
.requiredOption("--grep <pattern>", "Regex to match")
|
|
16
|
+
.option("--timeout <seconds>", "Timeout in seconds (default 30)", Number)
|
|
17
|
+
.option("--first-match", "Exit after the first matching line", true)
|
|
18
|
+
.action(wrapCommand(async (context, { options }) => {
|
|
19
|
+
const project = requireProject(context);
|
|
20
|
+
const serverName = options.server ?? context.activeProfile?.server;
|
|
21
|
+
if (!serverName) {
|
|
22
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "Server name is required" }, 4);
|
|
23
|
+
}
|
|
24
|
+
const manager = new ServerInstanceManager(context.globalState, project);
|
|
25
|
+
return manager.followLogs(serverName, {
|
|
26
|
+
grep: options.grep,
|
|
27
|
+
timeoutSeconds: options.timeout ?? context.timeout("default"),
|
|
28
|
+
firstMatchOnly: options.firstMatch !== false
|
|
29
|
+
});
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { loadModVariantCatalogSync } from "./ModVariantCatalog.js";
|
|
2
2
|
const VERSION_MATRIX = [
|
|
3
|
+
{
|
|
4
|
+
minecraftVersion: "1.21.11",
|
|
5
|
+
javaVersion: "21+",
|
|
6
|
+
servers: {
|
|
7
|
+
vanilla: { supported: true },
|
|
8
|
+
paper: { supported: true, latestBuild: 69 },
|
|
9
|
+
purpur: { supported: true, latestBuild: 2568 },
|
|
10
|
+
spigot: { supported: true, requiresBuildTools: true }
|
|
11
|
+
},
|
|
12
|
+
clients: {
|
|
13
|
+
fabric: { supported: true, loaderVersion: "0.19.2", modVersion: "0.9.1", validation: "verified" },
|
|
14
|
+
forge: { supported: false, notes: "不支持此版本" },
|
|
15
|
+
neoforge: { supported: false, validation: "planned", notes: "计划中" }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
3
18
|
{
|
|
4
19
|
minecraftVersion: "1.21.4",
|
|
5
20
|
javaVersion: "21+",
|
|
@@ -10,7 +10,10 @@ const DOWNLOAD_DISPATCHER = new Agent({
|
|
|
10
10
|
connect: {
|
|
11
11
|
timeout: 30_000
|
|
12
12
|
},
|
|
13
|
-
|
|
13
|
+
headersTimeout: 30_000,
|
|
14
|
+
bodyTimeout: 30_000,
|
|
15
|
+
connections: 1,
|
|
16
|
+
pipelining: 0
|
|
14
17
|
}).compose(interceptors.retry({
|
|
15
18
|
maxRetries: 4,
|
|
16
19
|
minTimeout: 500,
|
|
@@ -74,8 +77,8 @@ async function prepareManagedRuntime(variant, runtimeOptions, dependencies, expe
|
|
|
74
77
|
await installDependencies(resolvedVersion, {
|
|
75
78
|
side: "client",
|
|
76
79
|
dispatcher: DOWNLOAD_DISPATCHER,
|
|
77
|
-
assetsDownloadConcurrency:
|
|
78
|
-
librariesDownloadConcurrency:
|
|
80
|
+
assetsDownloadConcurrency: 1,
|
|
81
|
+
librariesDownloadConcurrency: 1
|
|
79
82
|
});
|
|
80
83
|
await applyArm64LwjglPatch(runtimeRootDir, installedVersionId, { fetchImpl });
|
|
81
84
|
await writeFile(readyMarker, new Date().toISOString(), "utf8");
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createBookCommand } from "./commands/book.js";
|
|
|
7
7
|
import { createClientCommand } from "./commands/client.js";
|
|
8
8
|
import { createChatCommand } from "./commands/chat.js";
|
|
9
9
|
import { createCombatCommand } from "./commands/combat.js";
|
|
10
|
-
import { createAnvilCommand, createCraftCommand, createEnchantCommand, createTradeCommand } from "./commands/craft.js";
|
|
10
|
+
import { createAnvilCommand, createCraftCommand, createEnchantCommand, createRecipeCommand, createTradeCommand } from "./commands/craft.js";
|
|
11
11
|
import { createEntityCommand } from "./commands/entity.js";
|
|
12
12
|
import { createEventsCommand } from "./commands/events.js";
|
|
13
13
|
import { createGuiCommand } from "./commands/gui.js";
|
|
@@ -27,6 +27,7 @@ import { createServerCommand } from "./commands/server.js";
|
|
|
27
27
|
import { createSignCommand } from "./commands/sign.js";
|
|
28
28
|
import { createStatusCommand } from "./commands/status.js";
|
|
29
29
|
import { createWaitCommand } from "./commands/wait.js";
|
|
30
|
+
import { createWaitLogCommand } from "./commands/wait-log.js";
|
|
30
31
|
import { createPluginCommand } from "./commands/plugin.js";
|
|
31
32
|
import { createInitCommand, createDeployCommand, createUpCommand, createDownCommand, createUseCommand } from "./commands/project.js";
|
|
32
33
|
import { attachGlobalOptions, wrapCommand } from "./util/command.js";
|
|
@@ -94,10 +95,12 @@ export function buildProgram() {
|
|
|
94
95
|
program.addCommand(createResourcepackCommand());
|
|
95
96
|
program.addCommand(createCombatCommand());
|
|
96
97
|
program.addCommand(createCraftCommand());
|
|
98
|
+
program.addCommand(createRecipeCommand());
|
|
97
99
|
program.addCommand(createAnvilCommand());
|
|
98
100
|
program.addCommand(createEnchantCommand());
|
|
99
101
|
program.addCommand(createTradeCommand());
|
|
100
102
|
program.addCommand(createWaitCommand());
|
|
103
|
+
program.addCommand(createWaitLogCommand());
|
|
101
104
|
program.addCommand(createEventsCommand());
|
|
102
105
|
return program;
|
|
103
106
|
}
|
|
@@ -8,6 +8,7 @@ import { MctError } from "../util/errors.js";
|
|
|
8
8
|
import { getListeningPids, isProcessRunning, killProcessTree } from "../util/process.js";
|
|
9
9
|
import { WebSocketClient } from "../client/WebSocketClient.js";
|
|
10
10
|
const INSTANCE_FILE = "instance.json";
|
|
11
|
+
const DEFAULT_CLIENT_MUTE = true;
|
|
11
12
|
function getLaunchScriptPath() {
|
|
12
13
|
const thisFile = fileURLToPath(import.meta.url);
|
|
13
14
|
// dist/instance/ClientInstanceManager.js -> scripts/launch-fabric-client.mjs
|
|
@@ -30,9 +31,11 @@ export class ClientInstanceManager {
|
|
|
30
31
|
wsPort,
|
|
31
32
|
account: options.account,
|
|
32
33
|
headless: options.headless,
|
|
33
|
-
mute: options.mute,
|
|
34
|
+
mute: options.mute ?? DEFAULT_CLIENT_MUTE,
|
|
34
35
|
launchArgs: options.launchArgs,
|
|
35
36
|
env: options.env,
|
|
37
|
+
javaCommand: options.javaCommand,
|
|
38
|
+
javaVersion: options.javaVersion,
|
|
36
39
|
createdAt: new Date().toISOString()
|
|
37
40
|
};
|
|
38
41
|
await writeFile(path.join(instanceDir, INSTANCE_FILE), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
@@ -58,7 +61,7 @@ export class ClientInstanceManager {
|
|
|
58
61
|
const meta = await this.loadMeta(clientName);
|
|
59
62
|
const instanceDir = resolveClientInstanceDir(clientName);
|
|
60
63
|
const wsPort = options.wsPort ?? meta.wsPort;
|
|
61
|
-
const mute = options.mute ?? meta.mute;
|
|
64
|
+
const mute = options.mute ?? meta.mute ?? DEFAULT_CLIENT_MUTE;
|
|
62
65
|
if (!meta.launchArgs || meta.launchArgs.length === 0) {
|
|
63
66
|
throw new MctError({ code: "INVALID_PARAMS", message: `Client ${clientName} has no launchArgs configured` }, 4);
|
|
64
67
|
}
|
|
@@ -94,6 +97,7 @@ export class ClientInstanceManager {
|
|
|
94
97
|
MCT_CLIENT_SERVER: options.server ?? "",
|
|
95
98
|
MCT_CLIENT_WS_PORT: String(wsPort),
|
|
96
99
|
MCT_CLIENT_HEADLESS: String(options.headless ?? meta.headless ?? false),
|
|
100
|
+
...(meta.javaCommand ? { MCT_CLIENT_JAVA: meta.javaCommand } : {}),
|
|
97
101
|
...(mute === undefined ? {} : { MCT_CLIENT_MUTE: String(mute) })
|
|
98
102
|
}
|
|
99
103
|
});
|
|
@@ -9,6 +9,8 @@ export interface CreateServerOptions {
|
|
|
9
9
|
version: string;
|
|
10
10
|
port?: number;
|
|
11
11
|
jvmArgs?: string[];
|
|
12
|
+
javaCommand?: string;
|
|
13
|
+
javaVersion?: number;
|
|
12
14
|
eula?: boolean;
|
|
13
15
|
cachedJarPath?: string;
|
|
14
16
|
}
|
|
@@ -48,6 +50,7 @@ export declare class ServerInstanceManager {
|
|
|
48
50
|
startedAt: string;
|
|
49
51
|
logPath: string;
|
|
50
52
|
instanceDir: string;
|
|
53
|
+
logStartOffset?: number;
|
|
51
54
|
stdinPipe?: string;
|
|
52
55
|
running: boolean;
|
|
53
56
|
stale: boolean;
|
|
@@ -59,6 +62,7 @@ export declare class ServerInstanceManager {
|
|
|
59
62
|
startedAt: string;
|
|
60
63
|
logPath: string;
|
|
61
64
|
instanceDir: string;
|
|
65
|
+
logStartOffset?: number;
|
|
62
66
|
stdinPipe?: string;
|
|
63
67
|
running: boolean;
|
|
64
68
|
}>;
|
|
@@ -71,6 +75,11 @@ export declare class ServerInstanceManager {
|
|
|
71
75
|
host: string;
|
|
72
76
|
port: number;
|
|
73
77
|
phase: string;
|
|
78
|
+
signals: {
|
|
79
|
+
processAlive: boolean;
|
|
80
|
+
portReachable: boolean;
|
|
81
|
+
readyLineSeen: boolean;
|
|
82
|
+
};
|
|
74
83
|
logPath: string;
|
|
75
84
|
lastLine: string | null;
|
|
76
85
|
recentLines: string[];
|
|
@@ -84,6 +93,8 @@ export declare class ServerInstanceManager {
|
|
|
84
93
|
tail?: number;
|
|
85
94
|
grep?: string;
|
|
86
95
|
since?: number;
|
|
96
|
+
sinceStart?: boolean;
|
|
97
|
+
afterMarker?: string;
|
|
87
98
|
rawColors?: boolean;
|
|
88
99
|
}): Promise<{
|
|
89
100
|
logPath: string;
|
|
@@ -91,6 +102,10 @@ export declare class ServerInstanceManager {
|
|
|
91
102
|
returnedLines: number;
|
|
92
103
|
lines: string[];
|
|
93
104
|
}>;
|
|
105
|
+
markLogs(serverName: string, label?: string): Promise<{
|
|
106
|
+
logPath: string;
|
|
107
|
+
marker: string;
|
|
108
|
+
}>;
|
|
94
109
|
followLogs(serverName: string, options: {
|
|
95
110
|
grep?: string;
|
|
96
111
|
timeoutSeconds: number;
|
|
@@ -104,6 +119,24 @@ export declare class ServerInstanceManager {
|
|
|
104
119
|
}>;
|
|
105
120
|
private requireRuntimeEntry;
|
|
106
121
|
private describeStartup;
|
|
122
|
+
readiness(serverName: string): Promise<{
|
|
123
|
+
process: {
|
|
124
|
+
alive: boolean;
|
|
125
|
+
pid: number;
|
|
126
|
+
};
|
|
127
|
+
port: {
|
|
128
|
+
reachable: boolean;
|
|
129
|
+
host: string;
|
|
130
|
+
port: number;
|
|
131
|
+
};
|
|
132
|
+
log: {
|
|
133
|
+
phase: string;
|
|
134
|
+
readyLineSeen: boolean;
|
|
135
|
+
lastLine: string | null;
|
|
136
|
+
recentLines: string[];
|
|
137
|
+
path: string;
|
|
138
|
+
};
|
|
139
|
+
}>;
|
|
107
140
|
private requireRunning;
|
|
108
141
|
list(): Promise<ServerInstanceMeta[]>;
|
|
109
142
|
static listAll(globalState: GlobalStateStore): Promise<ServerInstanceMeta[]>;
|
|
@@ -67,6 +67,8 @@ export class ServerInstanceManager {
|
|
|
67
67
|
mcVersion: options.version,
|
|
68
68
|
port,
|
|
69
69
|
jvmArgs: options.jvmArgs ?? [],
|
|
70
|
+
javaCommand: options.javaCommand,
|
|
71
|
+
javaVersion: options.javaVersion,
|
|
70
72
|
createdAt: new Date().toISOString()
|
|
71
73
|
};
|
|
72
74
|
await writeFile(path.join(instanceDir, INSTANCE_FILE), `${JSON.stringify(meta, null, 2)}\n`, "utf8");
|
|
@@ -95,8 +97,10 @@ export class ServerInstanceManager {
|
|
|
95
97
|
mkdirSync(logsDir, { recursive: true });
|
|
96
98
|
mkdirSync(stateDir, { recursive: true });
|
|
97
99
|
const logPath = path.join(logsDir, `server-${this.project}-${serverName}.log`);
|
|
100
|
+
const logStartOffset = await stat(logPath).then((value) => value.size).catch(() => 0);
|
|
98
101
|
const stdout = openSync(logPath, "a");
|
|
99
102
|
const jvmArgs = options.jvmArgs ?? meta.jvmArgs;
|
|
103
|
+
const javaCommand = meta.javaCommand ?? "java";
|
|
100
104
|
// Create a named pipe (FIFO) for stdin so external tools (GUI) can send commands
|
|
101
105
|
const stdinPipe = path.join(stateDir, `stdin-${this.project}-${serverName}.fifo`);
|
|
102
106
|
try {
|
|
@@ -109,7 +113,7 @@ export class ServerInstanceManager {
|
|
|
109
113
|
// then exec java with stdin reading from the FIFO
|
|
110
114
|
const child = spawn("bash", [
|
|
111
115
|
"-c",
|
|
112
|
-
'exec 3<>"$MCT_STDIN_PIPE"; exec
|
|
116
|
+
'exec 3<>"$MCT_STDIN_PIPE"; exec "$MCT_SERVER_JAVA" "$@" 0<&3',
|
|
113
117
|
"mct-server",
|
|
114
118
|
...jvmArgs, "-jar", jarFile, "nogui"
|
|
115
119
|
], {
|
|
@@ -119,7 +123,8 @@ export class ServerInstanceManager {
|
|
|
119
123
|
env: {
|
|
120
124
|
...process.env,
|
|
121
125
|
MCT_SERVER_PORT: String(meta.port),
|
|
122
|
-
MCT_STDIN_PIPE: stdinPipe
|
|
126
|
+
MCT_STDIN_PIPE: stdinPipe,
|
|
127
|
+
MCT_SERVER_JAVA: javaCommand
|
|
123
128
|
}
|
|
124
129
|
});
|
|
125
130
|
child.unref();
|
|
@@ -131,6 +136,7 @@ export class ServerInstanceManager {
|
|
|
131
136
|
startedAt: new Date().toISOString(),
|
|
132
137
|
logPath,
|
|
133
138
|
instanceDir,
|
|
139
|
+
logStartOffset,
|
|
134
140
|
stdinPipe
|
|
135
141
|
};
|
|
136
142
|
state.servers[stateKey] = entry;
|
|
@@ -232,6 +238,11 @@ export class ServerInstanceManager {
|
|
|
232
238
|
host: "127.0.0.1",
|
|
233
239
|
port: entry.port,
|
|
234
240
|
phase: snapshot.phase,
|
|
241
|
+
signals: {
|
|
242
|
+
processAlive: true,
|
|
243
|
+
portReachable: true,
|
|
244
|
+
readyLineSeen: snapshot.phase === "ready"
|
|
245
|
+
},
|
|
235
246
|
logPath: snapshot.logPath,
|
|
236
247
|
lastLine: snapshot.lastLine,
|
|
237
248
|
recentLines: snapshot.recentLines
|
|
@@ -247,6 +258,11 @@ export class ServerInstanceManager {
|
|
|
247
258
|
host: "127.0.0.1",
|
|
248
259
|
port: entry.port,
|
|
249
260
|
phase: snapshot.phase,
|
|
261
|
+
signals: {
|
|
262
|
+
processAlive: isProcessRunning(entry.pid),
|
|
263
|
+
portReachable: false,
|
|
264
|
+
readyLineSeen: snapshot.phase === "ready"
|
|
265
|
+
},
|
|
250
266
|
logPath: snapshot.logPath,
|
|
251
267
|
lastLine: snapshot.lastLine,
|
|
252
268
|
recentLines: snapshot.recentLines
|
|
@@ -282,11 +298,14 @@ export class ServerInstanceManager {
|
|
|
282
298
|
async readLogs(serverName, options = {}) {
|
|
283
299
|
const entry = await this.requireRuntimeEntry(serverName);
|
|
284
300
|
const logPath = entry.logPath;
|
|
285
|
-
|
|
301
|
+
let raw = await readFile(logPath, "utf8").catch((error) => {
|
|
286
302
|
if (error.code === "ENOENT")
|
|
287
303
|
return "";
|
|
288
304
|
throw error;
|
|
289
305
|
});
|
|
306
|
+
if (options.sinceStart && entry.logStartOffset && entry.logStartOffset > 0) {
|
|
307
|
+
raw = raw.slice(entry.logStartOffset);
|
|
308
|
+
}
|
|
290
309
|
let lines = raw.split("\n");
|
|
291
310
|
if (lines.length > 0 && lines[lines.length - 1] === "")
|
|
292
311
|
lines = lines.slice(0, -1);
|
|
@@ -297,6 +316,18 @@ export class ServerInstanceManager {
|
|
|
297
316
|
if (options.since !== undefined && options.since > 0) {
|
|
298
317
|
lines = lines.slice(Math.max(0, options.since));
|
|
299
318
|
}
|
|
319
|
+
if (options.afterMarker) {
|
|
320
|
+
let markerIndex = -1;
|
|
321
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
322
|
+
if (lines[index].includes(options.afterMarker)) {
|
|
323
|
+
markerIndex = index;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (markerIndex >= 0) {
|
|
328
|
+
lines = lines.slice(markerIndex + 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
300
331
|
if (options.grep) {
|
|
301
332
|
const re = new RegExp(options.grep);
|
|
302
333
|
lines = lines.filter((line) => re.test(line));
|
|
@@ -306,6 +337,12 @@ export class ServerInstanceManager {
|
|
|
306
337
|
}
|
|
307
338
|
return { logPath, totalLines: total, returnedLines: lines.length, lines };
|
|
308
339
|
}
|
|
340
|
+
async markLogs(serverName, label) {
|
|
341
|
+
const entry = await this.requireRuntimeEntry(serverName);
|
|
342
|
+
const marker = `MCT_MARK ${new Date().toISOString()} ${label ?? ""}`.trim();
|
|
343
|
+
await writeFile(entry.logPath, `\n${marker}\n`, { flag: "a", encoding: "utf8" });
|
|
344
|
+
return { logPath: entry.logPath, marker };
|
|
345
|
+
}
|
|
309
346
|
async followLogs(serverName, options) {
|
|
310
347
|
const entry = await this.requireRuntimeEntry(serverName);
|
|
311
348
|
const logPath = entry.logPath;
|
|
@@ -398,6 +435,23 @@ export class ServerInstanceManager {
|
|
|
398
435
|
lastLine
|
|
399
436
|
};
|
|
400
437
|
}
|
|
438
|
+
async readiness(serverName) {
|
|
439
|
+
const entry = await this.requireRuntimeEntry(serverName);
|
|
440
|
+
const processAlive = isProcessRunning(entry.pid);
|
|
441
|
+
const portReachable = await isTcpPortReachable("127.0.0.1", entry.port);
|
|
442
|
+
const snapshot = await this.describeStartup(entry.logPath);
|
|
443
|
+
return {
|
|
444
|
+
process: { alive: processAlive, pid: entry.pid },
|
|
445
|
+
port: { reachable: portReachable, host: "127.0.0.1", port: entry.port },
|
|
446
|
+
log: {
|
|
447
|
+
phase: snapshot.phase,
|
|
448
|
+
readyLineSeen: snapshot.phase === "ready",
|
|
449
|
+
lastLine: snapshot.lastLine,
|
|
450
|
+
recentLines: snapshot.recentLines,
|
|
451
|
+
path: snapshot.logPath
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
401
455
|
async requireRunning(serverName) {
|
|
402
456
|
const entry = await this.requireRuntimeEntry(serverName);
|
|
403
457
|
if (!isProcessRunning(entry.pid)) {
|
|
@@ -7,6 +7,8 @@ export interface ServerInstanceMeta {
|
|
|
7
7
|
mcVersion: string;
|
|
8
8
|
port: number;
|
|
9
9
|
jvmArgs: string[];
|
|
10
|
+
javaCommand?: string;
|
|
11
|
+
javaVersion?: number;
|
|
10
12
|
createdAt: string;
|
|
11
13
|
}
|
|
12
14
|
export interface ClientInstanceMeta {
|
|
@@ -19,6 +21,8 @@ export interface ClientInstanceMeta {
|
|
|
19
21
|
mute?: boolean;
|
|
20
22
|
launchArgs?: string[];
|
|
21
23
|
env?: Record<string, string>;
|
|
24
|
+
javaCommand?: string;
|
|
25
|
+
javaVersion?: number;
|
|
22
26
|
createdAt: string;
|
|
23
27
|
}
|
|
24
28
|
export interface ServerRuntimeEntry {
|
|
@@ -29,6 +33,7 @@ export interface ServerRuntimeEntry {
|
|
|
29
33
|
startedAt: string;
|
|
30
34
|
logPath: string;
|
|
31
35
|
instanceDir: string;
|
|
36
|
+
logStartOffset?: number;
|
|
32
37
|
stdinPipe?: string;
|
|
33
38
|
}
|
|
34
39
|
export interface ClientRuntimeEntry {
|
package/dist/util/project.js
CHANGED
|
@@ -29,16 +29,23 @@ export async function loadProjectFileById(projectId) {
|
|
|
29
29
|
return JSON.parse(raw);
|
|
30
30
|
}
|
|
31
31
|
export async function loadProjectFileForCwd(cwd) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
let current = normalizeProjectRoot(cwd);
|
|
33
|
+
while (true) {
|
|
34
|
+
const projectId = slugifyProjectId(current);
|
|
35
|
+
const projectFile = await loadProjectFileById(projectId);
|
|
36
|
+
if (projectFile) {
|
|
37
|
+
return {
|
|
38
|
+
projectId,
|
|
39
|
+
filePath: resolveProjectFilePath(projectId),
|
|
40
|
+
projectFile
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const parent = path.dirname(current);
|
|
44
|
+
if (parent === current) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
current = parent;
|
|
36
48
|
}
|
|
37
|
-
return {
|
|
38
|
-
projectId,
|
|
39
|
-
filePath: resolveProjectFilePath(projectId),
|
|
40
|
-
projectFile
|
|
41
|
-
};
|
|
42
49
|
}
|
|
43
50
|
export async function loadProjectFileForId(projectId) {
|
|
44
51
|
const projectFile = await loadProjectFileById(projectId);
|
package/package.json
CHANGED
|
@@ -7,11 +7,12 @@ import { execFileSync } from "node:child_process";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { createQuickPlayMultiplayer, launch as launchMinecraft } from "@xmcl/core";
|
|
10
|
+
import { DEFAULT_EXTRA_JVM_ARGS, createQuickPlayMultiplayer, launch as launchMinecraft } from "@xmcl/core";
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const VARIANTS_PATH = path.join(__dirname, "..", "data", "variants.json");
|
|
15
|
+
const DEFAULT_CLIENT_LANGUAGE = "zh_cn";
|
|
15
16
|
|
|
16
17
|
function readCatalog() {
|
|
17
18
|
return JSON.parse(readFileSync(VARIANTS_PATH, "utf8"));
|
|
@@ -121,6 +122,30 @@ function parseOptionalBoolean(value) {
|
|
|
121
122
|
return undefined;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function getClientLanguage() {
|
|
126
|
+
return process.env.MCT_CLIENT_LANGUAGE || DEFAULT_CLIENT_LANGUAGE;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function localeJavaArgs(language) {
|
|
130
|
+
const [languageCode = "zh", countryCode] = String(language).replace("-", "_").split("_");
|
|
131
|
+
return [
|
|
132
|
+
`-Duser.language=${languageCode || "zh"}`,
|
|
133
|
+
...(countryCode ? [`-Duser.country=${countryCode.toUpperCase()}`] : [])
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeLocaleJavaArgs(javaArgs, language) {
|
|
138
|
+
return [
|
|
139
|
+
...javaArgs.filter((entry) => !entry.startsWith("-Duser.language=") && !entry.startsWith("-Duser.country=")),
|
|
140
|
+
...localeJavaArgs(language)
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function xmclExtraJvmArgs(language, maxMemory) {
|
|
145
|
+
const defaultArgs = maxMemory ? DEFAULT_EXTRA_JVM_ARGS.filter((entry) => entry !== "-Xmx2G") : DEFAULT_EXTRA_JVM_ARGS;
|
|
146
|
+
return normalizeLocaleJavaArgs([...defaultArgs], language);
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
async function ensureFile(filePath, downloadUrl) {
|
|
125
150
|
try {
|
|
126
151
|
await access(filePath);
|
|
@@ -185,7 +210,7 @@ async function syncConfiguredMod(gameDir) {
|
|
|
185
210
|
await copyFile(sourceJar, targetJar);
|
|
186
211
|
}
|
|
187
212
|
|
|
188
|
-
async function ensureAutomationOptions(gameDir, server, mute) {
|
|
213
|
+
async function ensureAutomationOptions(gameDir, server, mute, language = DEFAULT_CLIENT_LANGUAGE) {
|
|
189
214
|
const optionsPath = path.join(gameDir, "options.txt");
|
|
190
215
|
const values = new Map();
|
|
191
216
|
|
|
@@ -206,6 +231,7 @@ async function ensureAutomationOptions(gameDir, server, mute) {
|
|
|
206
231
|
values.set("joinedFirstServer", "true");
|
|
207
232
|
values.set("tutorialStep", "none");
|
|
208
233
|
values.set("pauseOnLostFocus", "false");
|
|
234
|
+
values.set("lang", language);
|
|
209
235
|
if (mute !== undefined) {
|
|
210
236
|
const volume = mute ? "0.0" : "1.0";
|
|
211
237
|
for (const category of [
|
|
@@ -247,6 +273,7 @@ async function buildLaunchSpec(options) {
|
|
|
247
273
|
const gameDir = path.join(instanceRoot, "minecraft");
|
|
248
274
|
const packMeta = await readJson(path.join(instanceRoot, "mmc-pack.json"));
|
|
249
275
|
const mute = parseOptionalBoolean(process.env.MCT_CLIENT_MUTE);
|
|
276
|
+
const language = getClientLanguage();
|
|
250
277
|
await syncBuiltMod(instanceRoot, repoRoot, selectedVariant);
|
|
251
278
|
const componentMetas = new Map();
|
|
252
279
|
for (const component of packMeta.components) {
|
|
@@ -296,7 +323,7 @@ async function buildLaunchSpec(options) {
|
|
|
296
323
|
const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
|
|
297
324
|
const accountUuid = offlineUuid(accountName);
|
|
298
325
|
const server = process.env.MCT_CLIENT_SERVER || "";
|
|
299
|
-
await ensureAutomationOptions(gameDir, server, mute);
|
|
326
|
+
await ensureAutomationOptions(gameDir, server, mute, language);
|
|
300
327
|
const [serverHost, serverPort = "25565"] = server.split(":");
|
|
301
328
|
const classpath = [
|
|
302
329
|
path.join(librariesRoot, mainJarPath),
|
|
@@ -334,7 +361,7 @@ async function buildLaunchSpec(options) {
|
|
|
334
361
|
"-XstartOnFirstThread",
|
|
335
362
|
"-Xms512m",
|
|
336
363
|
`-Xmx${options["max-mem"] || "1024m"}`,
|
|
337
|
-
|
|
364
|
+
...localeJavaArgs(language),
|
|
338
365
|
`-Djava.library.path=${nativesDir}`,
|
|
339
366
|
"-DFabricMcEmu=net.minecraft.client.main.Main"
|
|
340
367
|
]
|
|
@@ -350,12 +377,13 @@ async function buildManifestLaunchSpec(options) {
|
|
|
350
377
|
const manifest = await readJson(manifestPath);
|
|
351
378
|
const gameDir = manifest.gameDir;
|
|
352
379
|
const mute = parseOptionalBoolean(process.env.MCT_CLIENT_MUTE);
|
|
380
|
+
const language = getClientLanguage();
|
|
353
381
|
await syncConfiguredMod(gameDir);
|
|
354
382
|
|
|
355
383
|
const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
|
|
356
384
|
const accountUuid = offlineUuid(accountName);
|
|
357
385
|
const server = process.env.MCT_CLIENT_SERVER || "";
|
|
358
|
-
await ensureAutomationOptions(gameDir, server, mute);
|
|
386
|
+
await ensureAutomationOptions(gameDir, server, mute, language);
|
|
359
387
|
const [serverHost, serverPort = "25565"] = server.split(":");
|
|
360
388
|
const substitutions = {
|
|
361
389
|
auth_player_name: accountName,
|
|
@@ -374,9 +402,9 @@ async function buildManifestLaunchSpec(options) {
|
|
|
374
402
|
gameArgs.push("--quickPlayMultiplayer", `${serverHost}:${serverPort}`);
|
|
375
403
|
}
|
|
376
404
|
|
|
377
|
-
const javaArgs = manifest.javaArgs
|
|
405
|
+
const javaArgs = normalizeLocaleJavaArgs(manifest.javaArgs
|
|
378
406
|
.map((entry) => substitute(entry, substitutions))
|
|
379
|
-
.filter((entry) => !entry.includes("${"));
|
|
407
|
+
.filter((entry) => !entry.includes("${")), language);
|
|
380
408
|
|
|
381
409
|
return {
|
|
382
410
|
cwd: gameDir,
|
|
@@ -418,15 +446,19 @@ async function launchXmclManagedClient(options) {
|
|
|
418
446
|
const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
|
|
419
447
|
const accountUuid = offlineUuid(accountName).replaceAll("-", "");
|
|
420
448
|
const server = process.env.MCT_CLIENT_SERVER || "";
|
|
421
|
-
|
|
449
|
+
const mute = parseOptionalBoolean(process.env.MCT_CLIENT_MUTE);
|
|
450
|
+
const language = getClientLanguage();
|
|
451
|
+
await ensureAutomationOptions(gameDir, server, mute, language);
|
|
422
452
|
const [serverHost, serverPort = "25565"] = server.split(":");
|
|
453
|
+
const maxMemory = parseMaxMemory(options["max-mem"]);
|
|
423
454
|
|
|
424
455
|
return launchMinecraft({
|
|
425
456
|
gamePath: gameDir,
|
|
426
457
|
resourcePath: runtimeRoot,
|
|
427
458
|
javaPath: options.java || process.env.MCT_CLIENT_JAVA || "java",
|
|
428
459
|
minMemory: 512,
|
|
429
|
-
maxMemory
|
|
460
|
+
maxMemory,
|
|
461
|
+
extraJVMArgs: xmclExtraJvmArgs(language, maxMemory),
|
|
430
462
|
version: versionId,
|
|
431
463
|
gameProfile: {
|
|
432
464
|
name: accountName,
|