@kzheart_/mc-pilot 0.9.0 → 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 +24 -21
- 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 +23 -19
- package/dist/download/client/ClientDownloader.d.ts +2 -2
- package/dist/download/client/ClientDownloader.js +36 -8
- 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 +3 -0
- package/dist/instance/ClientInstanceManager.js +108 -98
- package/dist/instance/ServerInstanceManager.d.ts +5 -0
- package/dist/instance/ServerInstanceManager.js +85 -3
- package/dist/util/command-log.d.ts +12 -0
- package/dist/util/command-log.js +13 -0
- package/dist/util/global-state.d.ts +4 -0
- package/dist/util/global-state.js +22 -0
- package/dist/util/instance-types.d.ts +1 -0
- package/dist/util/net.d.ts +1 -0
- package/dist/util/net.js +14 -12
- package/dist/util/state.d.ts +5 -0
- package/dist/util/state.js +81 -2
- package/package.json +3 -1
- package/scripts/launch-fabric-client.mjs +50 -8
package/data/variants.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"loader": "fabric",
|
|
10
10
|
"support": "ready",
|
|
11
11
|
"validation": "verified",
|
|
12
|
-
"modVersion": "0.
|
|
12
|
+
"modVersion": "0.9.1",
|
|
13
13
|
"fabricLoaderVersion": "0.16.14",
|
|
14
14
|
"yarnMappings": "1.18.2+build.4",
|
|
15
15
|
"javaVersion": 17,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"loader": "fabric",
|
|
22
22
|
"support": "ready",
|
|
23
23
|
"validation": "verified",
|
|
24
|
-
"modVersion": "0.
|
|
24
|
+
"modVersion": "0.9.1",
|
|
25
25
|
"fabricLoaderVersion": "0.16.14",
|
|
26
26
|
"yarnMappings": "1.20.1+build.10",
|
|
27
27
|
"javaVersion": 17,
|
|
@@ -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": "
|
|
36
|
-
"modVersion": "0.
|
|
34
|
+
"support": "configured",
|
|
35
|
+
"validation": "limited",
|
|
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",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"loader": "fabric",
|
|
44
45
|
"support": "ready",
|
|
45
46
|
"validation": "verified",
|
|
46
|
-
"modVersion": "0.
|
|
47
|
+
"modVersion": "0.9.1",
|
|
47
48
|
"fabricLoaderVersion": "0.16.14",
|
|
48
49
|
"yarnMappings": "1.20.2+build.4",
|
|
49
50
|
"javaVersion": 17,
|
|
@@ -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": "
|
|
58
|
-
"modVersion": "0.
|
|
57
|
+
"support": "configured",
|
|
58
|
+
"validation": "limited",
|
|
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",
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
"loader": "fabric",
|
|
66
68
|
"support": "ready",
|
|
67
69
|
"validation": "verified",
|
|
68
|
-
"modVersion": "0.
|
|
70
|
+
"modVersion": "0.9.1",
|
|
69
71
|
"fabricLoaderVersion": "0.16.14",
|
|
70
72
|
"yarnMappings": "1.20.4+build.3",
|
|
71
73
|
"javaVersion": 17,
|
|
@@ -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
|
-
"modVersion": "0.
|
|
80
|
+
"support": "configured",
|
|
81
|
+
"validation": "limited",
|
|
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",
|
|
@@ -87,7 +90,7 @@
|
|
|
87
90
|
"loader": "neoforge",
|
|
88
91
|
"support": "planned",
|
|
89
92
|
"validation": "planned",
|
|
90
|
-
"modVersion": "0.
|
|
93
|
+
"modVersion": "0.9.1",
|
|
91
94
|
"neoforgeVersion": "20.4.237",
|
|
92
95
|
"javaVersion": 17
|
|
93
96
|
},
|
|
@@ -97,7 +100,7 @@
|
|
|
97
100
|
"loader": "fabric",
|
|
98
101
|
"support": "ready",
|
|
99
102
|
"validation": "verified",
|
|
100
|
-
"modVersion": "0.
|
|
103
|
+
"modVersion": "0.9.1",
|
|
101
104
|
"fabricLoaderVersion": "0.16.14",
|
|
102
105
|
"yarnMappings": "1.21.1+build.3",
|
|
103
106
|
"javaVersion": 21,
|
|
@@ -109,7 +112,7 @@
|
|
|
109
112
|
"loader": "neoforge",
|
|
110
113
|
"support": "planned",
|
|
111
114
|
"validation": "planned",
|
|
112
|
-
"modVersion": "0.
|
|
115
|
+
"modVersion": "0.9.1",
|
|
113
116
|
"neoforgeVersion": "21.1.77",
|
|
114
117
|
"javaVersion": 21
|
|
115
118
|
},
|
|
@@ -119,7 +122,7 @@
|
|
|
119
122
|
"loader": "fabric",
|
|
120
123
|
"support": "ready",
|
|
121
124
|
"validation": "verified",
|
|
122
|
-
"modVersion": "0.
|
|
125
|
+
"modVersion": "0.9.1",
|
|
123
126
|
"fabricLoaderVersion": "0.16.14",
|
|
124
127
|
"yarnMappings": "1.21.4+build.1",
|
|
125
128
|
"javaVersion": 21,
|
|
@@ -131,7 +134,7 @@
|
|
|
131
134
|
"loader": "neoforge",
|
|
132
135
|
"support": "planned",
|
|
133
136
|
"validation": "planned",
|
|
134
|
-
"modVersion": "0.
|
|
137
|
+
"modVersion": "0.9.1",
|
|
135
138
|
"neoforgeVersion": "21.4.75",
|
|
136
139
|
"javaVersion": 21
|
|
137
140
|
}
|
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.1.0";
|
|
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?: {
|
|
@@ -10,9 +10,9 @@ const VERSION_MATRIX = [
|
|
|
10
10
|
spigot: { supported: true, requiresBuildTools: true }
|
|
11
11
|
},
|
|
12
12
|
clients: {
|
|
13
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
13
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1", validation: "verified" },
|
|
14
14
|
forge: { supported: false, notes: "不支持此版本" },
|
|
15
|
-
neoforge: { supported: true, loaderVersion: "21.4.x", modVersion: "0.
|
|
15
|
+
neoforge: { supported: true, loaderVersion: "21.4.x", modVersion: "0.9.1" }
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
{
|
|
@@ -25,9 +25,9 @@ const VERSION_MATRIX = [
|
|
|
25
25
|
spigot: { supported: true, requiresBuildTools: true }
|
|
26
26
|
},
|
|
27
27
|
clients: {
|
|
28
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
28
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1", validation: "verified" },
|
|
29
29
|
forge: { supported: false, notes: "不支持此版本" },
|
|
30
|
-
neoforge: { supported: true, loaderVersion: "21.1.x", modVersion: "0.
|
|
30
|
+
neoforge: { supported: true, loaderVersion: "21.1.x", modVersion: "0.9.1" }
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -40,8 +40,8 @@ const VERSION_MATRIX = [
|
|
|
40
40
|
spigot: { supported: true, requiresBuildTools: true }
|
|
41
41
|
},
|
|
42
42
|
clients: {
|
|
43
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
44
|
-
forge: { supported: true, loaderVersion: "49.0.49", modVersion: "0.
|
|
43
|
+
fabric: { supported: true, loaderVersion: "0.16.14", 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
|
},
|
|
@@ -55,8 +55,8 @@ const VERSION_MATRIX = [
|
|
|
55
55
|
spigot: { supported: true, requiresBuildTools: true }
|
|
56
56
|
},
|
|
57
57
|
clients: {
|
|
58
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
59
|
-
forge: { supported:
|
|
58
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1", validation: "verified" },
|
|
59
|
+
forge: { supported: true, loaderVersion: "48.1.0", modVersion: "0.9.1", validation: "limited" },
|
|
60
60
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
61
61
|
}
|
|
62
62
|
},
|
|
@@ -70,7 +70,7 @@ const VERSION_MATRIX = [
|
|
|
70
70
|
spigot: { supported: true, requiresBuildTools: true }
|
|
71
71
|
},
|
|
72
72
|
clients: {
|
|
73
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
73
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1" },
|
|
74
74
|
forge: { supported: false, notes: "当前未接入此 loader" },
|
|
75
75
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
76
76
|
}
|
|
@@ -85,8 +85,8 @@ const VERSION_MATRIX = [
|
|
|
85
85
|
spigot: { supported: true, requiresBuildTools: true }
|
|
86
86
|
},
|
|
87
87
|
clients: {
|
|
88
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
89
|
-
forge: { supported: true, loaderVersion: "47.
|
|
88
|
+
fabric: { supported: true, loaderVersion: "0.16.14", 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
|
},
|
|
@@ -100,8 +100,8 @@ const VERSION_MATRIX = [
|
|
|
100
100
|
spigot: { supported: true, requiresBuildTools: true }
|
|
101
101
|
},
|
|
102
102
|
clients: {
|
|
103
|
-
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.
|
|
104
|
-
forge: { supported: true, loaderVersion: "40.x", modVersion: "0.
|
|
103
|
+
fabric: { supported: true, loaderVersion: "0.16.14", modVersion: "0.9.1", validation: "verified" },
|
|
104
|
+
forge: { supported: true, loaderVersion: "40.x", modVersion: "0.9.1" },
|
|
105
105
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
106
106
|
}
|
|
107
107
|
},
|
|
@@ -116,7 +116,7 @@ const VERSION_MATRIX = [
|
|
|
116
116
|
},
|
|
117
117
|
clients: {
|
|
118
118
|
fabric: { supported: false, notes: "当前未接入此版本 mod" },
|
|
119
|
-
forge: { supported: true, loaderVersion: "36.x", modVersion: "0.
|
|
119
|
+
forge: { supported: true, loaderVersion: "36.x", modVersion: "0.9.1" },
|
|
120
120
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
121
121
|
}
|
|
122
122
|
},
|
|
@@ -131,7 +131,7 @@ const VERSION_MATRIX = [
|
|
|
131
131
|
},
|
|
132
132
|
clients: {
|
|
133
133
|
fabric: { supported: false, notes: "不支持此版本" },
|
|
134
|
-
forge: { supported: true, loaderVersion: "14.23.x", modVersion: "0.
|
|
134
|
+
forge: { supported: true, loaderVersion: "14.23.x", modVersion: "0.9.1" },
|
|
135
135
|
neoforge: { supported: false, notes: "不支持此版本" }
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -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;
|