@kzheart_/mc-pilot 0.9.1 → 0.9.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/data/variants.json +12 -9
- package/dist/commands/client.js +22 -73
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +211 -0
- package/dist/download/VersionMatrix.d.ts +1 -15
- package/dist/download/VersionMatrix.js +11 -7
- package/dist/download/client/ClientDownloader.d.ts +2 -2
- package/dist/download/client/ClientDownloader.js +35 -7
- package/dist/download/client/FabricRuntimeDownloader.d.ts +2 -0
- package/dist/download/client/FabricRuntimeDownloader.js +47 -17
- package/dist/index.js +2 -0
- package/dist/instance/ClientInstanceManager.d.ts +2 -0
- package/dist/instance/ClientInstanceManager.js +4 -1
- package/dist/util/instance-types.d.ts +1 -0
- package/package.json +3 -1
- package/scripts/launch-fabric-client.mjs +37 -3
package/data/variants.json
CHANGED
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
"id": "1.20.1-forge",
|
|
32
32
|
"minecraftVersion": "1.20.1",
|
|
33
33
|
"loader": "forge",
|
|
34
|
-
"support": "
|
|
35
|
-
"validation": "
|
|
34
|
+
"support": "configured",
|
|
35
|
+
"validation": "limited",
|
|
36
36
|
"modVersion": "0.9.1",
|
|
37
37
|
"forgeVersion": "47.3.0",
|
|
38
|
-
"javaVersion": 17
|
|
38
|
+
"javaVersion": 17,
|
|
39
|
+
"gradleModule": "version-1.20.1-forge"
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
"id": "1.20.2-fabric",
|
|
@@ -53,11 +54,12 @@
|
|
|
53
54
|
"id": "1.20.2-forge",
|
|
54
55
|
"minecraftVersion": "1.20.2",
|
|
55
56
|
"loader": "forge",
|
|
56
|
-
"support": "
|
|
57
|
-
"validation": "
|
|
57
|
+
"support": "configured",
|
|
58
|
+
"validation": "limited",
|
|
58
59
|
"modVersion": "0.9.1",
|
|
59
60
|
"forgeVersion": "48.1.0",
|
|
60
|
-
"javaVersion": 17
|
|
61
|
+
"javaVersion": 17,
|
|
62
|
+
"gradleModule": "version-1.20.2-forge"
|
|
61
63
|
},
|
|
62
64
|
{
|
|
63
65
|
"id": "1.20.4-fabric",
|
|
@@ -75,11 +77,12 @@
|
|
|
75
77
|
"id": "1.20.4-forge",
|
|
76
78
|
"minecraftVersion": "1.20.4",
|
|
77
79
|
"loader": "forge",
|
|
78
|
-
"support": "
|
|
79
|
-
"validation": "
|
|
80
|
+
"support": "configured",
|
|
81
|
+
"validation": "limited",
|
|
80
82
|
"modVersion": "0.9.1",
|
|
81
83
|
"forgeVersion": "49.0.49",
|
|
82
|
-
"javaVersion": 17
|
|
84
|
+
"javaVersion": 17,
|
|
85
|
+
"gradleModule": "version-1.20.4-forge"
|
|
83
86
|
},
|
|
84
87
|
{
|
|
85
88
|
"id": "1.20.4-neoforge",
|
package/dist/commands/client.js
CHANGED
|
@@ -5,14 +5,8 @@ import { ServerInstanceManager } from "../instance/ServerInstanceManager.js";
|
|
|
5
5
|
import { MctError } from "../util/errors.js";
|
|
6
6
|
import { createRequestAction } from "./request-helpers.js";
|
|
7
7
|
import { wrapCommand } from "../util/command.js";
|
|
8
|
-
import {
|
|
9
|
-
import { findVariantByVersionAndLoader, getModArtifactFileName, loadModVariantCatalog } from "../download/ModVariantCatalog.js";
|
|
10
|
-
import { detectJava } from "../download/JavaDetector.js";
|
|
11
|
-
import { prepareManagedFabricRuntime } from "../download/client/FabricRuntimeDownloader.js";
|
|
12
|
-
import { copyFileIfMissing, downloadFile } from "../download/DownloadUtils.js";
|
|
8
|
+
import { downloadClientModToDir } from "../download/client/ClientDownloader.js";
|
|
13
9
|
import { resolveClientInstanceDir } from "../util/paths.js";
|
|
14
|
-
import { access, mkdir } from "node:fs/promises";
|
|
15
|
-
import path from "node:path";
|
|
16
10
|
export async function resolveProfileServerAddress(context, explicitServer, loadPort) {
|
|
17
11
|
if (explicitServer) {
|
|
18
12
|
return explicitServer;
|
|
@@ -50,93 +44,46 @@ export function createClientCommand() {
|
|
|
50
44
|
.description("Create a new client instance")
|
|
51
45
|
.argument("<name>", "Client instance name (e.g. fabric-1.20.4)")
|
|
52
46
|
.option("--version <version>", "Minecraft version (default: 1.21.4)")
|
|
53
|
-
.option("--loader <loader>", "Client loader: fabric (default: fabric)")
|
|
47
|
+
.option("--loader <loader>", "Client loader: fabric|forge (default: fabric)")
|
|
54
48
|
.option("--ws-port <port>", "WebSocket port (auto-assigned if omitted)", Number)
|
|
55
49
|
.option("--account <account>", "Offline username or account identifier")
|
|
56
50
|
.option("--headless", "Launch in headless mode")
|
|
51
|
+
.option("--mute", "Mute all in-game audio for this client")
|
|
52
|
+
.option("--no-mute", "Keep in-game audio enabled for this client")
|
|
57
53
|
.option("--java <command>", "Java command to use")
|
|
58
54
|
.action(wrapCommand(async (_context, { args, options }) => {
|
|
59
55
|
const clientName = args[0];
|
|
60
56
|
const loader = options.loader ?? "fabric";
|
|
61
57
|
const version = options.version ?? "1.21.4";
|
|
62
|
-
const cacheManager = new CacheManager();
|
|
63
|
-
const catalog = await loadModVariantCatalog();
|
|
64
|
-
const variant = findVariantByVersionAndLoader(catalog, version, loader);
|
|
65
|
-
if (!variant) {
|
|
66
|
-
throw new MctError({ code: "VARIANT_NOT_FOUND", message: `No mod variant found for ${version} / ${loader}` }, 4);
|
|
67
|
-
}
|
|
68
|
-
if (variant.loader !== "fabric") {
|
|
69
|
-
throw new MctError({ code: "UNSUPPORTED_LOADER", message: `Loader ${variant.loader} is not implemented yet` }, 4);
|
|
70
|
-
}
|
|
71
|
-
// Check Java
|
|
72
|
-
const java = await detectJava(options.java ?? "java");
|
|
73
|
-
const requiredJava = variant.javaVersion ?? 17;
|
|
74
|
-
if (!java.available || (java.majorVersion ?? 0) < requiredJava) {
|
|
75
|
-
throw new MctError({ code: "JAVA_NOT_FOUND", message: `Java ${requiredJava}+ is required for ${variant.id}` }, 4);
|
|
76
|
-
}
|
|
77
|
-
// Resolve mod artifact
|
|
78
|
-
const artifactFileName = getModArtifactFileName(variant);
|
|
79
|
-
const cacheArtifactPath = cacheManager.getModFile(artifactFileName);
|
|
80
|
-
const gradleModule = variant.gradleModule ?? `version-${variant.minecraftVersion}`;
|
|
81
|
-
const localBuildPath = path.join(process.cwd(), "client-mod", gradleModule, "build", "libs", artifactFileName);
|
|
82
|
-
let sourcePath;
|
|
83
|
-
try {
|
|
84
|
-
await access(localBuildPath);
|
|
85
|
-
sourcePath = localBuildPath;
|
|
86
|
-
await copyFileIfMissing(localBuildPath, cacheArtifactPath);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
try {
|
|
90
|
-
await access(cacheArtifactPath);
|
|
91
|
-
sourcePath = cacheArtifactPath;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
const modVersion = variant.modVersion ?? "0.9.1";
|
|
95
|
-
const baseUrl = process.env.MCT_MOD_DOWNLOAD_BASE_URL || "https://github.com/kzheart/mc-pilot/releases/download";
|
|
96
|
-
const downloadUrl = `${baseUrl}/v${modVersion}/${artifactFileName}`;
|
|
97
|
-
await downloadFile(downloadUrl, cacheArtifactPath, fetch);
|
|
98
|
-
sourcePath = cacheArtifactPath;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// Set up client instance directory
|
|
102
58
|
const instanceDir = resolveClientInstanceDir(clientName);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const runtimeRootDir = path.join(cacheManager.getRootDir(), "client", "runtime", variant.minecraftVersion);
|
|
109
|
-
const managedRuntime = await prepareManagedFabricRuntime(variant, {
|
|
110
|
-
runtimeRootDir,
|
|
111
|
-
gameDir: minecraftDir
|
|
112
|
-
}, { fetchImpl: fetch });
|
|
113
|
-
const launchArgs = [
|
|
114
|
-
"--runtime-root", managedRuntime.runtimeRootDir,
|
|
115
|
-
"--version-id", managedRuntime.versionId,
|
|
116
|
-
"--game-dir", managedRuntime.gameDir
|
|
117
|
-
];
|
|
59
|
+
const downloaded = await downloadClientModToDir(process.cwd(), instanceDir, {
|
|
60
|
+
version,
|
|
61
|
+
loader,
|
|
62
|
+
java: options.java
|
|
63
|
+
});
|
|
118
64
|
const manager = new ClientInstanceManager(_context.globalState);
|
|
119
65
|
const meta = await manager.create({
|
|
120
66
|
name: clientName,
|
|
121
|
-
loader,
|
|
122
|
-
version:
|
|
67
|
+
loader: downloaded.loader,
|
|
68
|
+
version: downloaded.minecraftVersion,
|
|
123
69
|
wsPort: options.wsPort,
|
|
124
70
|
account: options.account,
|
|
125
71
|
headless: options.headless,
|
|
126
|
-
|
|
72
|
+
mute: options.mute,
|
|
73
|
+
launchArgs: downloaded.launchArgs,
|
|
127
74
|
env: {
|
|
128
|
-
MCT_CLIENT_MOD_VARIANT:
|
|
129
|
-
MCT_CLIENT_MOD_JAR:
|
|
75
|
+
MCT_CLIENT_MOD_VARIANT: downloaded.variantId,
|
|
76
|
+
MCT_CLIENT_MOD_JAR: downloaded.jar
|
|
130
77
|
}
|
|
131
78
|
});
|
|
132
79
|
return {
|
|
133
80
|
created: true,
|
|
134
81
|
...meta,
|
|
135
|
-
javaCommand:
|
|
136
|
-
javaVersion:
|
|
137
|
-
modsDir,
|
|
138
|
-
runtimeRootDir:
|
|
139
|
-
runtimeVersionId:
|
|
82
|
+
javaCommand: downloaded.javaCommand,
|
|
83
|
+
javaVersion: downloaded.javaVersion,
|
|
84
|
+
modsDir: downloaded.modsDir,
|
|
85
|
+
runtimeRootDir: downloaded.runtimeRootDir,
|
|
86
|
+
runtimeVersionId: downloaded.runtimeVersionId
|
|
140
87
|
};
|
|
141
88
|
}));
|
|
142
89
|
command
|
|
@@ -147,6 +94,8 @@ export function createClientCommand() {
|
|
|
147
94
|
.option("--account <account>", "Offline username or account identifier")
|
|
148
95
|
.option("--ws-port <port>", "WebSocket port override", Number)
|
|
149
96
|
.option("--headless", "Launch in headless mode")
|
|
97
|
+
.option("--mute", "Mute all in-game audio for this launch")
|
|
98
|
+
.option("--no-mute", "Keep in-game audio enabled for this launch")
|
|
150
99
|
.option("--force", "Kill any existing client with the same name before launching")
|
|
151
100
|
.action(wrapCommand(async (context, { args, options }) => {
|
|
152
101
|
const clientName = args[0] ?? context.activeProfile?.clients[0];
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PNG } from "pngjs";
|
|
5
|
+
import { wrapCommand } from "../util/command.js";
|
|
6
|
+
import { MctError } from "../util/errors.js";
|
|
7
|
+
import { resolveProjectRelativePath } from "./request-helpers.js";
|
|
8
|
+
function parseRect(value, field) {
|
|
9
|
+
if (!value)
|
|
10
|
+
return undefined;
|
|
11
|
+
const parts = value.split(",").map((part) => Number(part.trim()));
|
|
12
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) {
|
|
13
|
+
throw new MctError({ code: "INVALID_PARAMS", message: `${field} must use x,y,w,h format.` }, 4);
|
|
14
|
+
}
|
|
15
|
+
return { x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
16
|
+
}
|
|
17
|
+
function parseRgb(value) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return undefined;
|
|
20
|
+
const parts = value.split(",").map((part) => Number(part.trim()));
|
|
21
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part) || part < 0 || part > 255)) {
|
|
22
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "--background must use r,g,b format." }, 4);
|
|
23
|
+
}
|
|
24
|
+
return [parts[0], parts[1], parts[2]];
|
|
25
|
+
}
|
|
26
|
+
function loadPng(file) {
|
|
27
|
+
return PNG.sync.read(readFileSync(file));
|
|
28
|
+
}
|
|
29
|
+
function pixelOffset(image, x, y) {
|
|
30
|
+
return (image.width * y + x) << 2;
|
|
31
|
+
}
|
|
32
|
+
function colorDistance(a, b) {
|
|
33
|
+
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
|
|
34
|
+
}
|
|
35
|
+
function clampRect(rect, image) {
|
|
36
|
+
const raw = rect ?? { x: 0, y: 0, width: image.width, height: image.height };
|
|
37
|
+
const x = Math.max(0, Math.min(image.width, Math.floor(raw.x)));
|
|
38
|
+
const y = Math.max(0, Math.min(image.height, Math.floor(raw.y)));
|
|
39
|
+
const right = Math.max(x, Math.min(image.width, Math.floor(raw.x + raw.width)));
|
|
40
|
+
const bottom = Math.max(y, Math.min(image.height, Math.floor(raw.y + raw.height)));
|
|
41
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
42
|
+
}
|
|
43
|
+
function findForegroundBounds(image, options) {
|
|
44
|
+
const region = clampRect(options.region, image);
|
|
45
|
+
const background = options.background ?? [
|
|
46
|
+
image.data[pixelOffset(image, 0, 0)],
|
|
47
|
+
image.data[pixelOffset(image, 0, 0) + 1],
|
|
48
|
+
image.data[pixelOffset(image, 0, 0) + 2]
|
|
49
|
+
];
|
|
50
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
51
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
52
|
+
let maxX = -1;
|
|
53
|
+
let maxY = -1;
|
|
54
|
+
let pixels = 0;
|
|
55
|
+
for (let y = region.y; y < region.y + region.height; y++) {
|
|
56
|
+
for (let x = region.x; x < region.x + region.width; x++) {
|
|
57
|
+
const offset = pixelOffset(image, x, y);
|
|
58
|
+
const a = image.data[offset + 3];
|
|
59
|
+
const rgb = [image.data[offset], image.data[offset + 1], image.data[offset + 2]];
|
|
60
|
+
if (a > options.alphaThreshold && colorDistance(rgb, background) > options.threshold) {
|
|
61
|
+
minX = Math.min(minX, x);
|
|
62
|
+
minY = Math.min(minY, y);
|
|
63
|
+
maxX = Math.max(maxX, x);
|
|
64
|
+
maxY = Math.max(maxY, y);
|
|
65
|
+
pixels++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (pixels === 0)
|
|
70
|
+
return null;
|
|
71
|
+
return { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1, pixels };
|
|
72
|
+
}
|
|
73
|
+
function collectTemplatePixels(template, alphaThreshold, maxSamples) {
|
|
74
|
+
const pixels = [];
|
|
75
|
+
for (let y = 0; y < template.height; y++) {
|
|
76
|
+
for (let x = 0; x < template.width; x++) {
|
|
77
|
+
const offset = pixelOffset(template, x, y);
|
|
78
|
+
if (template.data[offset + 3] > alphaThreshold) {
|
|
79
|
+
pixels.push({ x, y, r: template.data[offset], g: template.data[offset + 1], b: template.data[offset + 2] });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (pixels.length <= maxSamples)
|
|
84
|
+
return pixels;
|
|
85
|
+
const stride = pixels.length / maxSamples;
|
|
86
|
+
const sampled = [];
|
|
87
|
+
for (let i = 0; i < maxSamples; i++) {
|
|
88
|
+
sampled.push(pixels[Math.floor(i * stride)]);
|
|
89
|
+
}
|
|
90
|
+
return sampled;
|
|
91
|
+
}
|
|
92
|
+
function scoreTemplateAt(screenshot, samples, x, y, scale) {
|
|
93
|
+
let score = 0;
|
|
94
|
+
let compared = 0;
|
|
95
|
+
for (const sample of samples) {
|
|
96
|
+
const sx = Math.round(x + sample.x * scale);
|
|
97
|
+
const sy = Math.round(y + sample.y * scale);
|
|
98
|
+
if (sx < 0 || sy < 0 || sx >= screenshot.width || sy >= screenshot.height)
|
|
99
|
+
continue;
|
|
100
|
+
const offset = pixelOffset(screenshot, sx, sy);
|
|
101
|
+
score += Math.abs(screenshot.data[offset] - sample.r) +
|
|
102
|
+
Math.abs(screenshot.data[offset + 1] - sample.g) +
|
|
103
|
+
Math.abs(screenshot.data[offset + 2] - sample.b);
|
|
104
|
+
compared++;
|
|
105
|
+
}
|
|
106
|
+
return compared === 0 ? Number.POSITIVE_INFINITY : score / compared;
|
|
107
|
+
}
|
|
108
|
+
export function createImageCommand() {
|
|
109
|
+
const command = new Command("image").description("Image analysis helpers for screenshots and UI offset tuning");
|
|
110
|
+
command
|
|
111
|
+
.command("bbox")
|
|
112
|
+
.description("Find the foreground bounding box in a PNG image")
|
|
113
|
+
.argument("<image>", "PNG image path")
|
|
114
|
+
.option("--background <rgb>", "Background color to ignore, format r,g,b. Defaults to top-left pixel.")
|
|
115
|
+
.option("--threshold <value>", "RGB distance threshold", (value) => Number(value), 24)
|
|
116
|
+
.option("--alpha-threshold <value>", "Alpha threshold", (value) => Number(value), 8)
|
|
117
|
+
.option("--region <rect>", "Optional scan region x,y,w,h")
|
|
118
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
119
|
+
const imagePath = resolveProjectRelativePath(context, args[0] ?? "");
|
|
120
|
+
const image = loadPng(imagePath);
|
|
121
|
+
const bbox = findForegroundBounds(image, {
|
|
122
|
+
background: parseRgb(options.background),
|
|
123
|
+
threshold: Number(options.threshold),
|
|
124
|
+
alphaThreshold: Number(options.alphaThreshold),
|
|
125
|
+
region: parseRect(options.region, "--region")
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
image: path.resolve(imagePath),
|
|
129
|
+
size: { width: image.width, height: image.height },
|
|
130
|
+
bbox,
|
|
131
|
+
center: bbox ? { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 } : null
|
|
132
|
+
};
|
|
133
|
+
}));
|
|
134
|
+
command
|
|
135
|
+
.command("locate-template")
|
|
136
|
+
.description("Locate a transparent PNG template inside a screenshot, useful for tuning GUI title offsets")
|
|
137
|
+
.argument("<screenshot>", "Screenshot PNG path")
|
|
138
|
+
.argument("<template>", "Template PNG path")
|
|
139
|
+
.option("--region <rect>", "Screenshot search region x,y,w,h")
|
|
140
|
+
.option("--expected <rect>", "Expected rectangle x,y,w,h; output includes delta from it")
|
|
141
|
+
.option("--scale <value>", "Single scale to test", (value) => Number(value))
|
|
142
|
+
.option("--min-scale <value>", "Minimum scale", (value) => Number(value), 0.5)
|
|
143
|
+
.option("--max-scale <value>", "Maximum scale", (value) => Number(value), 3)
|
|
144
|
+
.option("--scale-step <value>", "Scale step", (value) => Number(value), 0.1)
|
|
145
|
+
.option("--stride <pixels>", "Search stride in screenshot pixels", (value) => Number(value), 2)
|
|
146
|
+
.option("--alpha-threshold <value>", "Template alpha threshold", (value) => Number(value), 16)
|
|
147
|
+
.option("--max-samples <count>", "Maximum template pixels to sample", (value) => Number(value), 4000)
|
|
148
|
+
.action(wrapCommand(async (context, { args, options }) => {
|
|
149
|
+
const screenshotPath = resolveProjectRelativePath(context, args[0] ?? "");
|
|
150
|
+
const templatePath = resolveProjectRelativePath(context, args[1] ?? "");
|
|
151
|
+
const screenshot = loadPng(screenshotPath);
|
|
152
|
+
const template = loadPng(templatePath);
|
|
153
|
+
const region = clampRect(parseRect(options.region, "--region"), screenshot);
|
|
154
|
+
const expected = parseRect(options.expected, "--expected");
|
|
155
|
+
const samples = collectTemplatePixels(template, Number(options.alphaThreshold), Number(options.maxSamples));
|
|
156
|
+
if (samples.length === 0) {
|
|
157
|
+
throw new MctError({ code: "INVALID_PARAMS", message: "Template has no visible pixels after alpha filtering." }, 4);
|
|
158
|
+
}
|
|
159
|
+
const scales = options.scale
|
|
160
|
+
? [Number(options.scale)]
|
|
161
|
+
: Array.from({ length: Math.max(1, Math.floor((Number(options.maxScale) - Number(options.minScale)) / Number(options.scaleStep)) + 1) }, (_, index) => Number((Number(options.minScale) + index * Number(options.scaleStep)).toFixed(4)));
|
|
162
|
+
let best;
|
|
163
|
+
for (const scale of scales) {
|
|
164
|
+
const width = Math.round(template.width * scale);
|
|
165
|
+
const height = Math.round(template.height * scale);
|
|
166
|
+
for (let y = region.y - height; y <= region.y + region.height; y += Number(options.stride)) {
|
|
167
|
+
for (let x = region.x - width; x <= region.x + region.width; x += Number(options.stride)) {
|
|
168
|
+
const score = scoreTemplateAt(screenshot, samples, x, y, scale);
|
|
169
|
+
if (!best || score < best.score) {
|
|
170
|
+
best = { x, y, scale, score, compared: samples.length };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!best) {
|
|
176
|
+
throw new MctError({ code: "INVALID_STATE", message: "No template match candidate found." }, 3);
|
|
177
|
+
}
|
|
178
|
+
const bbox = {
|
|
179
|
+
x: best.x,
|
|
180
|
+
y: best.y,
|
|
181
|
+
width: Math.round(template.width * best.scale),
|
|
182
|
+
height: Math.round(template.height * best.scale)
|
|
183
|
+
};
|
|
184
|
+
const center = { x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2 };
|
|
185
|
+
const expectedCenter = expected ? { x: expected.x + expected.width / 2, y: expected.y + expected.height / 2 } : undefined;
|
|
186
|
+
return {
|
|
187
|
+
screenshot: path.resolve(screenshotPath),
|
|
188
|
+
template: path.resolve(templatePath),
|
|
189
|
+
screenshotSize: { width: screenshot.width, height: screenshot.height },
|
|
190
|
+
templateSize: { width: template.width, height: template.height },
|
|
191
|
+
samples: samples.length,
|
|
192
|
+
match: {
|
|
193
|
+
bbox,
|
|
194
|
+
center,
|
|
195
|
+
scale: best.scale,
|
|
196
|
+
score: Number(best.score.toFixed(3))
|
|
197
|
+
},
|
|
198
|
+
expected: expected
|
|
199
|
+
? {
|
|
200
|
+
bbox: expected,
|
|
201
|
+
center: expectedCenter,
|
|
202
|
+
delta: {
|
|
203
|
+
x: Number((center.x - expectedCenter.x).toFixed(3)),
|
|
204
|
+
y: Number((center.y - expectedCenter.y).toFixed(3))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
: undefined
|
|
208
|
+
};
|
|
209
|
+
}));
|
|
210
|
+
return command;
|
|
211
|
+
}
|
|
@@ -40,21 +40,7 @@ export interface ClientSearchResult {
|
|
|
40
40
|
notes?: string;
|
|
41
41
|
javaVersion: string;
|
|
42
42
|
}
|
|
43
|
-
export declare function getVersionMatrix():
|
|
44
|
-
minecraftVersion: string;
|
|
45
|
-
javaVersion: string;
|
|
46
|
-
servers: {
|
|
47
|
-
paper: ServerSupportInfo;
|
|
48
|
-
purpur: ServerSupportInfo;
|
|
49
|
-
vanilla: ServerSupportInfo;
|
|
50
|
-
spigot: ServerSupportInfo;
|
|
51
|
-
};
|
|
52
|
-
clients: {
|
|
53
|
-
fabric: ClientLoaderSupportInfo;
|
|
54
|
-
forge: ClientLoaderSupportInfo;
|
|
55
|
-
neoforge: ClientLoaderSupportInfo;
|
|
56
|
-
};
|
|
57
|
-
}[];
|
|
43
|
+
export declare function getVersionMatrix(): MinecraftSupportEntry[];
|
|
58
44
|
export declare function getSupportedMinecraftVersions(): string[];
|
|
59
45
|
export declare function getMinecraftSupport(version: string): MinecraftSupportEntry | undefined;
|
|
60
46
|
export declare function searchServerVersions(filter?: {
|
|
@@ -41,7 +41,7 @@ const VERSION_MATRIX = [
|
|
|
41
41
|
},
|
|
42
42
|
clients: {
|
|
43
43
|
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1" },
|
|
44
|
-
forge: { supported: true, loaderVersion: "49.0.49", modVersion: "0.9.1" },
|
|
44
|
+
forge: { supported: true, loaderVersion: "49.0.49", modVersion: "0.9.1", validation: "limited" },
|
|
45
45
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
46
46
|
}
|
|
47
47
|
},
|
|
@@ -56,7 +56,7 @@ const VERSION_MATRIX = [
|
|
|
56
56
|
},
|
|
57
57
|
clients: {
|
|
58
58
|
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1", validation: "verified" },
|
|
59
|
-
forge: { supported:
|
|
59
|
+
forge: { supported: true, loaderVersion: "48.1.0", modVersion: "0.9.1", validation: "limited" },
|
|
60
60
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
61
61
|
}
|
|
62
62
|
},
|
|
@@ -86,7 +86,7 @@ const VERSION_MATRIX = [
|
|
|
86
86
|
},
|
|
87
87
|
clients: {
|
|
88
88
|
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1" },
|
|
89
|
-
forge: { supported: true, loaderVersion: "47.
|
|
89
|
+
forge: { supported: true, loaderVersion: "47.3.0", modVersion: "0.9.1", validation: "limited" },
|
|
90
90
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
91
91
|
}
|
|
92
92
|
},
|
|
@@ -150,8 +150,8 @@ function overlayClientSupport(entry, loader) {
|
|
|
150
150
|
notes: variant.notes
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
return
|
|
153
|
+
function overlayMinecraftSupport(entry) {
|
|
154
|
+
return {
|
|
155
155
|
minecraftVersion: entry.minecraftVersion,
|
|
156
156
|
javaVersion: entry.javaVersion,
|
|
157
157
|
servers: { ...entry.servers },
|
|
@@ -160,13 +160,17 @@ export function getVersionMatrix() {
|
|
|
160
160
|
forge: overlayClientSupport(entry, "forge"),
|
|
161
161
|
neoforge: overlayClientSupport(entry, "neoforge")
|
|
162
162
|
}
|
|
163
|
-
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function getVersionMatrix() {
|
|
166
|
+
return VERSION_MATRIX.map((entry) => overlayMinecraftSupport(entry));
|
|
164
167
|
}
|
|
165
168
|
export function getSupportedMinecraftVersions() {
|
|
166
169
|
return VERSION_MATRIX.map((entry) => entry.minecraftVersion);
|
|
167
170
|
}
|
|
168
171
|
export function getMinecraftSupport(version) {
|
|
169
|
-
|
|
172
|
+
const entry = VERSION_MATRIX.find((candidate) => candidate.minecraftVersion === version);
|
|
173
|
+
return entry ? overlayMinecraftSupport(entry) : undefined;
|
|
170
174
|
}
|
|
171
175
|
export function searchServerVersions(filter) {
|
|
172
176
|
const types = filter?.type ? [filter.type] : getServerTypes();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CacheManager } from "../CacheManager.js";
|
|
2
2
|
import { detectJava } from "../JavaDetector.js";
|
|
3
3
|
import type { LoaderType, ModVariant } from "../types.js";
|
|
4
|
-
import {
|
|
4
|
+
import { prepareManagedClientRuntime } from "./FabricRuntimeDownloader.js";
|
|
5
5
|
export interface DownloadClientOptions {
|
|
6
6
|
loader?: LoaderType;
|
|
7
7
|
version?: string;
|
|
@@ -20,7 +20,7 @@ export interface DownloadClientDependencies {
|
|
|
20
20
|
cacheManager?: CacheManager;
|
|
21
21
|
detectJavaImpl?: typeof detectJava;
|
|
22
22
|
fetchImpl?: typeof fetch;
|
|
23
|
-
prepareManagedRuntimeImpl?: typeof
|
|
23
|
+
prepareManagedRuntimeImpl?: typeof prepareManagedClientRuntime;
|
|
24
24
|
}
|
|
25
25
|
export declare function resolveArtifact(cwd: string, variant: ModVariant, cacheManager: CacheManager, fetchImpl?: typeof fetch): Promise<{
|
|
26
26
|
sourcePath: string;
|
|
@@ -7,7 +7,7 @@ import { CacheManager } from "../CacheManager.js";
|
|
|
7
7
|
import { copyFileIfMissing, downloadFile } from "../DownloadUtils.js";
|
|
8
8
|
import { detectJava } from "../JavaDetector.js";
|
|
9
9
|
import { findVariantByVersionAndLoader, getDefaultVariant, getModArtifactFileName, loadModVariantCatalog } from "../ModVariantCatalog.js";
|
|
10
|
-
import {
|
|
10
|
+
import { prepareManagedClientRuntime } from "./FabricRuntimeDownloader.js";
|
|
11
11
|
function getLaunchScriptPath() {
|
|
12
12
|
const thisFile = fileURLToPath(import.meta.url);
|
|
13
13
|
// dist/download/client/ClientDownloader.js -> scripts/launch-fabric-client.mjs
|
|
@@ -15,13 +15,27 @@ function getLaunchScriptPath() {
|
|
|
15
15
|
}
|
|
16
16
|
const GITHUB_RELEASE_BASE_URL = process.env.MCT_MOD_DOWNLOAD_BASE_URL || "https://github.com/kzheart/mc-pilot/releases/download";
|
|
17
17
|
function ensureSupportedVariant(variant) {
|
|
18
|
-
if (variant.
|
|
18
|
+
if (variant.support !== "ready" && variant.support !== "configured") {
|
|
19
19
|
throw new MctError({
|
|
20
|
-
code: "
|
|
21
|
-
message: `
|
|
20
|
+
code: "VARIANT_NOT_BUILDABLE",
|
|
21
|
+
message: `Variant ${variant.id} is not buildable yet`,
|
|
22
|
+
details: {
|
|
23
|
+
support: variant.support,
|
|
24
|
+
validation: variant.validation
|
|
25
|
+
}
|
|
26
|
+
}, 4);
|
|
27
|
+
}
|
|
28
|
+
if (variant.loader === "fabric" && (!variant.fabricLoaderVersion || !variant.yarnMappings)) {
|
|
29
|
+
throw new MctError({
|
|
30
|
+
code: "VARIANT_NOT_BUILDABLE",
|
|
31
|
+
message: `Variant ${variant.id} is not buildable yet`,
|
|
32
|
+
details: {
|
|
33
|
+
support: variant.support,
|
|
34
|
+
validation: variant.validation
|
|
35
|
+
}
|
|
22
36
|
}, 4);
|
|
23
37
|
}
|
|
24
|
-
if (
|
|
38
|
+
if (variant.loader === "forge" && !variant.forgeVersion) {
|
|
25
39
|
throw new MctError({
|
|
26
40
|
code: "VARIANT_NOT_BUILDABLE",
|
|
27
41
|
message: `Variant ${variant.id} is not buildable yet`,
|
|
@@ -31,6 +45,12 @@ function ensureSupportedVariant(variant) {
|
|
|
31
45
|
}
|
|
32
46
|
}, 4);
|
|
33
47
|
}
|
|
48
|
+
if (variant.loader !== "fabric" && variant.loader !== "forge") {
|
|
49
|
+
throw new MctError({
|
|
50
|
+
code: "UNSUPPORTED_LOADER",
|
|
51
|
+
message: `Loader ${variant.loader} is not implemented yet`
|
|
52
|
+
}, 4);
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
export async function resolveArtifact(cwd, variant, cacheManager, fetchImpl = fetch) {
|
|
36
56
|
const artifactFileName = getModArtifactFileName(variant);
|
|
@@ -114,6 +134,12 @@ function resolveLaunchRuntimePaths(cwd, options) {
|
|
|
114
134
|
};
|
|
115
135
|
}
|
|
116
136
|
function buildLaunchArgs(runtimePaths, variant) {
|
|
137
|
+
if (variant.loader !== "fabric") {
|
|
138
|
+
throw new MctError({
|
|
139
|
+
code: "INVALID_PARAMS",
|
|
140
|
+
message: `Custom runtime directories are not supported for ${variant.loader} clients yet`
|
|
141
|
+
}, 4);
|
|
142
|
+
}
|
|
117
143
|
return [
|
|
118
144
|
"--instance-dir",
|
|
119
145
|
runtimePaths.instanceDir,
|
|
@@ -169,11 +195,13 @@ export async function downloadClientModToDir(cwd, targetDir, options, dependenci
|
|
|
169
195
|
const cacheManager = dependencies.cacheManager ?? new CacheManager();
|
|
170
196
|
const detectJavaImpl = dependencies.detectJavaImpl ?? detectJava;
|
|
171
197
|
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
172
|
-
const prepareManagedRuntimeImpl = dependencies.prepareManagedRuntimeImpl ??
|
|
198
|
+
const prepareManagedRuntimeImpl = dependencies.prepareManagedRuntimeImpl ?? prepareManagedClientRuntime;
|
|
173
199
|
const catalog = await loadModVariantCatalog();
|
|
174
200
|
const variant = options.version
|
|
175
201
|
? findVariantByVersionAndLoader(catalog, options.version, loader)
|
|
176
|
-
:
|
|
202
|
+
: loader === "fabric"
|
|
203
|
+
? getDefaultVariant(catalog)
|
|
204
|
+
: undefined;
|
|
177
205
|
if (!variant) {
|
|
178
206
|
throw new MctError({
|
|
179
207
|
code: "VARIANT_NOT_FOUND",
|
|
@@ -12,3 +12,5 @@ export interface PrepareFabricRuntimeOptions {
|
|
|
12
12
|
gameDir: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function prepareManagedFabricRuntime(variant: ModVariant, runtimeOptions: PrepareFabricRuntimeOptions, dependencies?: PrepareFabricRuntimeDependencies): Promise<PreparedFabricRuntime>;
|
|
15
|
+
export declare function prepareManagedForgeRuntime(variant: ModVariant, runtimeOptions: PrepareFabricRuntimeOptions, dependencies?: PrepareFabricRuntimeDependencies): Promise<PreparedFabricRuntime>;
|
|
16
|
+
export declare function prepareManagedClientRuntime(variant: ModVariant, runtimeOptions: PrepareFabricRuntimeOptions, dependencies?: PrepareFabricRuntimeDependencies): Promise<PreparedFabricRuntime>;
|
|
@@ -2,7 +2,7 @@ import { access, mkdir, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
import { MinecraftFolder, Version } from "@xmcl/core";
|
|
5
|
-
import { getVersionList, installDependencies, installFabric, installVersion } from "@xmcl/installer";
|
|
5
|
+
import { getVersionList, installDependencies, installFabric, installForge, installVersion } from "@xmcl/installer";
|
|
6
6
|
import { Agent, interceptors } from "undici";
|
|
7
7
|
import { MctError } from "../../util/errors.js";
|
|
8
8
|
import { applyArm64LwjglPatch } from "./Arm64LwjglPatcher.js";
|
|
@@ -42,19 +42,11 @@ async function fetchWithRetry(input, init, attempts = 4) {
|
|
|
42
42
|
}
|
|
43
43
|
throw lastError;
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
async function prepareManagedRuntime(variant, runtimeOptions, dependencies, expectedVersionId, installLoader) {
|
|
46
46
|
const fetchImpl = dependencies.fetchImpl ?? fetchWithRetry;
|
|
47
|
-
const loaderVersion = variant.fabricLoaderVersion;
|
|
48
|
-
if (!loaderVersion) {
|
|
49
|
-
throw new MctError({
|
|
50
|
-
code: "VARIANT_NOT_BUILDABLE",
|
|
51
|
-
message: `Variant ${variant.id} does not define a Fabric loader version`
|
|
52
|
-
}, 4);
|
|
53
|
-
}
|
|
54
47
|
const { runtimeRootDir, gameDir } = runtimeOptions;
|
|
55
48
|
await mkdir(runtimeRootDir, { recursive: true });
|
|
56
49
|
await mkdir(gameDir, { recursive: true });
|
|
57
|
-
const expectedVersionId = `${variant.minecraftVersion}-fabric${loaderVersion}`;
|
|
58
50
|
const readyMarker = path.join(runtimeRootDir, `.ready-${expectedVersionId}`);
|
|
59
51
|
try {
|
|
60
52
|
await access(readyMarker);
|
|
@@ -77,13 +69,7 @@ export async function prepareManagedFabricRuntime(variant, runtimeOptions, depen
|
|
|
77
69
|
side: "client",
|
|
78
70
|
dispatcher: DOWNLOAD_DISPATCHER
|
|
79
71
|
});
|
|
80
|
-
const installedVersionId = await
|
|
81
|
-
minecraftVersion: variant.minecraftVersion,
|
|
82
|
-
version: loaderVersion,
|
|
83
|
-
minecraft,
|
|
84
|
-
side: "client",
|
|
85
|
-
fetch: fetchImpl
|
|
86
|
-
});
|
|
72
|
+
const installedVersionId = await installLoader(minecraft, fetchImpl);
|
|
87
73
|
const resolvedVersion = await Version.parse(minecraft, installedVersionId);
|
|
88
74
|
await installDependencies(resolvedVersion, {
|
|
89
75
|
side: "client",
|
|
@@ -99,3 +85,47 @@ export async function prepareManagedFabricRuntime(variant, runtimeOptions, depen
|
|
|
99
85
|
versionId: installedVersionId
|
|
100
86
|
};
|
|
101
87
|
}
|
|
88
|
+
export async function prepareManagedFabricRuntime(variant, runtimeOptions, dependencies = {}) {
|
|
89
|
+
const loaderVersion = variant.fabricLoaderVersion;
|
|
90
|
+
if (!loaderVersion) {
|
|
91
|
+
throw new MctError({
|
|
92
|
+
code: "VARIANT_NOT_BUILDABLE",
|
|
93
|
+
message: `Variant ${variant.id} does not define a Fabric loader version`
|
|
94
|
+
}, 4);
|
|
95
|
+
}
|
|
96
|
+
return prepareManagedRuntime(variant, runtimeOptions, dependencies, `${variant.minecraftVersion}-fabric${loaderVersion}`, (minecraft, fetchImpl) => installFabric({
|
|
97
|
+
minecraftVersion: variant.minecraftVersion,
|
|
98
|
+
version: loaderVersion,
|
|
99
|
+
minecraft,
|
|
100
|
+
side: "client",
|
|
101
|
+
fetch: fetchImpl
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
export async function prepareManagedForgeRuntime(variant, runtimeOptions, dependencies = {}) {
|
|
105
|
+
const forgeVersion = variant.forgeVersion;
|
|
106
|
+
if (!forgeVersion) {
|
|
107
|
+
throw new MctError({
|
|
108
|
+
code: "VARIANT_NOT_BUILDABLE",
|
|
109
|
+
message: `Variant ${variant.id} does not define a Forge version`
|
|
110
|
+
}, 4);
|
|
111
|
+
}
|
|
112
|
+
return prepareManagedRuntime(variant, runtimeOptions, dependencies, `${variant.minecraftVersion}-forge-${forgeVersion}`, (minecraft) => installForge({
|
|
113
|
+
mcversion: variant.minecraftVersion,
|
|
114
|
+
version: forgeVersion
|
|
115
|
+
}, minecraft, {
|
|
116
|
+
side: "client",
|
|
117
|
+
dispatcher: DOWNLOAD_DISPATCHER
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
export async function prepareManagedClientRuntime(variant, runtimeOptions, dependencies = {}) {
|
|
121
|
+
if (variant.loader === "fabric") {
|
|
122
|
+
return prepareManagedFabricRuntime(variant, runtimeOptions, dependencies);
|
|
123
|
+
}
|
|
124
|
+
if (variant.loader === "forge") {
|
|
125
|
+
return prepareManagedForgeRuntime(variant, runtimeOptions, dependencies);
|
|
126
|
+
}
|
|
127
|
+
throw new MctError({
|
|
128
|
+
code: "UNSUPPORTED_LOADER",
|
|
129
|
+
message: `Loader ${variant.loader} is not implemented yet`
|
|
130
|
+
}, 4);
|
|
131
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createEntityCommand } from "./commands/entity.js";
|
|
|
12
12
|
import { createEventsCommand } from "./commands/events.js";
|
|
13
13
|
import { createGuiCommand } from "./commands/gui.js";
|
|
14
14
|
import { createHudCommand } from "./commands/hud.js";
|
|
15
|
+
import { createImageCommand } from "./commands/image.js";
|
|
15
16
|
import { createInputCommand } from "./commands/input.js";
|
|
16
17
|
import { createInventoryCommand } from "./commands/inventory.js";
|
|
17
18
|
import { createLookCommand } from "./commands/look.js";
|
|
@@ -83,6 +84,7 @@ export function buildProgram() {
|
|
|
83
84
|
program.addCommand(createEntityCommand());
|
|
84
85
|
program.addCommand(createInventoryCommand());
|
|
85
86
|
program.addCommand(createGuiCommand());
|
|
87
|
+
program.addCommand(createImageCommand());
|
|
86
88
|
program.addCommand(createScreenshotCommand());
|
|
87
89
|
program.addCommand(createScreenCommand());
|
|
88
90
|
program.addCommand(createHudCommand());
|
|
@@ -7,6 +7,7 @@ export interface CreateClientOptions {
|
|
|
7
7
|
wsPort?: number;
|
|
8
8
|
account?: string;
|
|
9
9
|
headless?: boolean;
|
|
10
|
+
mute?: boolean;
|
|
10
11
|
launchArgs?: string[];
|
|
11
12
|
env?: Record<string, string>;
|
|
12
13
|
}
|
|
@@ -15,6 +16,7 @@ export interface LaunchClientOptions {
|
|
|
15
16
|
account?: string;
|
|
16
17
|
wsPort?: number;
|
|
17
18
|
headless?: boolean;
|
|
19
|
+
mute?: boolean;
|
|
18
20
|
force?: boolean;
|
|
19
21
|
}
|
|
20
22
|
export interface WaitReadyOptions {
|
|
@@ -30,6 +30,7 @@ export class ClientInstanceManager {
|
|
|
30
30
|
wsPort,
|
|
31
31
|
account: options.account,
|
|
32
32
|
headless: options.headless,
|
|
33
|
+
mute: options.mute,
|
|
33
34
|
launchArgs: options.launchArgs,
|
|
34
35
|
env: options.env,
|
|
35
36
|
createdAt: new Date().toISOString()
|
|
@@ -57,6 +58,7 @@ export class ClientInstanceManager {
|
|
|
57
58
|
const meta = await this.loadMeta(clientName);
|
|
58
59
|
const instanceDir = resolveClientInstanceDir(clientName);
|
|
59
60
|
const wsPort = options.wsPort ?? meta.wsPort;
|
|
61
|
+
const mute = options.mute ?? meta.mute;
|
|
60
62
|
if (!meta.launchArgs || meta.launchArgs.length === 0) {
|
|
61
63
|
throw new MctError({ code: "INVALID_PARAMS", message: `Client ${clientName} has no launchArgs configured` }, 4);
|
|
62
64
|
}
|
|
@@ -91,7 +93,8 @@ export class ClientInstanceManager {
|
|
|
91
93
|
MCT_CLIENT_ACCOUNT: options.account ?? meta.account ?? "",
|
|
92
94
|
MCT_CLIENT_SERVER: options.server ?? "",
|
|
93
95
|
MCT_CLIENT_WS_PORT: String(wsPort),
|
|
94
|
-
MCT_CLIENT_HEADLESS: String(options.headless ?? meta.headless ?? false)
|
|
96
|
+
MCT_CLIENT_HEADLESS: String(options.headless ?? meta.headless ?? false),
|
|
97
|
+
...(mute === undefined ? {} : { MCT_CLIENT_MUTE: String(mute) })
|
|
95
98
|
}
|
|
96
99
|
});
|
|
97
100
|
child.unref();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kzheart_/mc-pilot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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": {
|
|
@@ -44,10 +44,12 @@
|
|
|
44
44
|
"@xmcl/file-transfer": "^2.0.3",
|
|
45
45
|
"@xmcl/installer": "^6.1.2",
|
|
46
46
|
"commander": "^14.0.1",
|
|
47
|
+
"pngjs": "^7.0.0",
|
|
47
48
|
"ws": "^8.19.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "^24.5.2",
|
|
52
|
+
"@types/pngjs": "^6.0.5",
|
|
51
53
|
"@types/ws": "^8.18.1",
|
|
52
54
|
"tsx": "^4.20.5",
|
|
53
55
|
"typescript": "^5.9.2"
|
|
@@ -106,6 +106,21 @@ function substitute(template, variables) {
|
|
|
106
106
|
return template.replace(/\$\{([^}]+)\}/g, (_, key) => variables[key] ?? "");
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function parseOptionalBoolean(value) {
|
|
110
|
+
if (value === undefined || value === null || value === "") {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalized = String(value).trim().toLowerCase();
|
|
115
|
+
if (["true", "1", "yes", "on"].includes(normalized)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (["false", "0", "no", "off"].includes(normalized)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
async function ensureFile(filePath, downloadUrl) {
|
|
110
125
|
try {
|
|
111
126
|
await access(filePath);
|
|
@@ -170,7 +185,7 @@ async function syncConfiguredMod(gameDir) {
|
|
|
170
185
|
await copyFile(sourceJar, targetJar);
|
|
171
186
|
}
|
|
172
187
|
|
|
173
|
-
async function ensureAutomationOptions(gameDir, server) {
|
|
188
|
+
async function ensureAutomationOptions(gameDir, server, mute) {
|
|
174
189
|
const optionsPath = path.join(gameDir, "options.txt");
|
|
175
190
|
const values = new Map();
|
|
176
191
|
|
|
@@ -191,6 +206,23 @@ async function ensureAutomationOptions(gameDir, server) {
|
|
|
191
206
|
values.set("joinedFirstServer", "true");
|
|
192
207
|
values.set("tutorialStep", "none");
|
|
193
208
|
values.set("pauseOnLostFocus", "false");
|
|
209
|
+
if (mute !== undefined) {
|
|
210
|
+
const volume = mute ? "0.0" : "1.0";
|
|
211
|
+
for (const category of [
|
|
212
|
+
"master",
|
|
213
|
+
"music",
|
|
214
|
+
"record",
|
|
215
|
+
"weather",
|
|
216
|
+
"block",
|
|
217
|
+
"hostile",
|
|
218
|
+
"neutral",
|
|
219
|
+
"player",
|
|
220
|
+
"ambient",
|
|
221
|
+
"voice"
|
|
222
|
+
]) {
|
|
223
|
+
values.set(`soundCategory_${category}`, volume);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
194
226
|
if (server) {
|
|
195
227
|
values.set("lastServer", server);
|
|
196
228
|
}
|
|
@@ -214,6 +246,7 @@ async function buildLaunchSpec(options) {
|
|
|
214
246
|
const nativesDir = options["natives-dir"] || path.join(instanceRoot, "natives");
|
|
215
247
|
const gameDir = path.join(instanceRoot, "minecraft");
|
|
216
248
|
const packMeta = await readJson(path.join(instanceRoot, "mmc-pack.json"));
|
|
249
|
+
const mute = parseOptionalBoolean(process.env.MCT_CLIENT_MUTE);
|
|
217
250
|
await syncBuiltMod(instanceRoot, repoRoot, selectedVariant);
|
|
218
251
|
const componentMetas = new Map();
|
|
219
252
|
for (const component of packMeta.components) {
|
|
@@ -263,7 +296,7 @@ async function buildLaunchSpec(options) {
|
|
|
263
296
|
const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
|
|
264
297
|
const accountUuid = offlineUuid(accountName);
|
|
265
298
|
const server = process.env.MCT_CLIENT_SERVER || "";
|
|
266
|
-
await ensureAutomationOptions(gameDir, server);
|
|
299
|
+
await ensureAutomationOptions(gameDir, server, mute);
|
|
267
300
|
const [serverHost, serverPort = "25565"] = server.split(":");
|
|
268
301
|
const classpath = [
|
|
269
302
|
path.join(librariesRoot, mainJarPath),
|
|
@@ -316,12 +349,13 @@ async function buildManifestLaunchSpec(options) {
|
|
|
316
349
|
|
|
317
350
|
const manifest = await readJson(manifestPath);
|
|
318
351
|
const gameDir = manifest.gameDir;
|
|
352
|
+
const mute = parseOptionalBoolean(process.env.MCT_CLIENT_MUTE);
|
|
319
353
|
await syncConfiguredMod(gameDir);
|
|
320
354
|
|
|
321
355
|
const accountName = process.env.MCT_CLIENT_ACCOUNT || options.account || "TEST1";
|
|
322
356
|
const accountUuid = offlineUuid(accountName);
|
|
323
357
|
const server = process.env.MCT_CLIENT_SERVER || "";
|
|
324
|
-
await ensureAutomationOptions(gameDir, server);
|
|
358
|
+
await ensureAutomationOptions(gameDir, server, mute);
|
|
325
359
|
const [serverHost, serverPort = "25565"] = server.split(":");
|
|
326
360
|
const substitutions = {
|
|
327
361
|
auth_player_name: accountName,
|