@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.
@@ -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: false, notes: "当前未接入此 loader" },
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.x", modVersion: "0.9.1" },
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
- export function getVersionMatrix() {
154
- return VERSION_MATRIX.map((entry) => ({
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
- return VERSION_MATRIX.find((entry) => entry.minecraftVersion === version);
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 { prepareManagedFabricRuntime } from "./FabricRuntimeDownloader.js";
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 prepareManagedFabricRuntime;
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 { prepareManagedFabricRuntime } from "./FabricRuntimeDownloader.js";
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.loader !== "fabric") {
18
+ if (variant.support !== "ready" && variant.support !== "configured") {
19
19
  throw new MctError({
20
- code: "UNSUPPORTED_LOADER",
21
- message: `Loader ${variant.loader} is not implemented yet`
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 (!variant.fabricLoaderVersion || !variant.yarnMappings) {
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 ?? prepareManagedFabricRuntime;
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
- : getDefaultVariant(catalog);
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
- export async function prepareManagedFabricRuntime(variant, runtimeOptions, dependencies = {}) {
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 installFabric({
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 resolveRepoRoot() {
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(resolveRepoRoot(), relativePath);
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,
@@ -16,6 +16,7 @@ export interface ClientInstanceMeta {
16
16
  wsPort: number;
17
17
  account?: string;
18
18
  headless?: boolean;
19
+ mute?: boolean;
19
20
  launchArgs?: string[];
20
21
  env?: Record<string, string>;
21
22
  createdAt: string;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@kzheart_/mc-pilot",
3
- "version": "0.9.1",
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": "./bin/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"