@kzheart_/mc-pilot 0.9.1 → 0.9.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.
- package/data/protocol/actions.json +231 -0
- package/data/protocol/errors.json +80 -0
- package/data/protocol/queries.json +160 -0
- 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/schema.js +6 -6
- package/dist/util/instance-types.d.ts +1 -0
- package/package.json +5 -3
- package/scripts/launch-fabric-client.mjs +37 -3
|
@@ -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/dist/schema.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
function
|
|
5
|
-
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
|
4
|
+
function resolvePackageRoot() {
|
|
5
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
6
|
}
|
|
7
7
|
function loadJsonFile(relativePath) {
|
|
8
|
-
const filePath = path.join(
|
|
8
|
+
const filePath = path.join(resolvePackageRoot(), relativePath);
|
|
9
9
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
10
10
|
}
|
|
11
11
|
function serializeOption(option) {
|
|
@@ -55,9 +55,9 @@ function collectLeafCommands(commands) {
|
|
|
55
55
|
return leaves;
|
|
56
56
|
}
|
|
57
57
|
export function buildSchemaDocument(program) {
|
|
58
|
-
const actions = loadJsonFile("protocol/actions.json");
|
|
59
|
-
const queries = loadJsonFile("protocol/queries.json");
|
|
60
|
-
const errors = loadJsonFile("protocol/errors.json");
|
|
58
|
+
const actions = loadJsonFile("data/protocol/actions.json");
|
|
59
|
+
const queries = loadJsonFile("data/protocol/queries.json");
|
|
60
|
+
const errors = loadJsonFile("data/protocol/errors.json");
|
|
61
61
|
const commands = program.commands.map((command) => serializeCommand(command));
|
|
62
62
|
return {
|
|
63
63
|
schemaVersion: 1,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kzheart_/mc-pilot",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.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": {
|
|
7
|
-
"mct": "
|
|
7
|
+
"mct": "bin/mct"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/kzheart/mc-pilot.git",
|
|
30
|
+
"url": "git+https://github.com/kzheart/mc-pilot.git",
|
|
31
31
|
"directory": "cli"
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
@@ -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"
|