@kzheart_/mc-pilot 0.1.0
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/bin/mct +2 -0
- package/data/variants.json +139 -0
- package/dist/client/ClientManager.d.ts +82 -0
- package/dist/client/ClientManager.js +213 -0
- package/dist/client/WebSocketClient.d.ts +15 -0
- package/dist/client/WebSocketClient.js +76 -0
- package/dist/commands/block.d.ts +2 -0
- package/dist/commands/block.js +52 -0
- package/dist/commands/book.d.ts +2 -0
- package/dist/commands/book.js +21 -0
- package/dist/commands/channel.d.ts +2 -0
- package/dist/commands/channel.js +24 -0
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +31 -0
- package/dist/commands/client.d.ts +2 -0
- package/dist/commands/client.js +87 -0
- package/dist/commands/combat.d.ts +2 -0
- package/dist/commands/combat.js +46 -0
- package/dist/commands/craft.d.ts +5 -0
- package/dist/commands/craft.js +45 -0
- package/dist/commands/effects.d.ts +2 -0
- package/dist/commands/effects.js +16 -0
- package/dist/commands/entity.d.ts +2 -0
- package/dist/commands/entity.js +53 -0
- package/dist/commands/gui.d.ts +2 -0
- package/dist/commands/gui.js +49 -0
- package/dist/commands/hud.d.ts +2 -0
- package/dist/commands/hud.js +16 -0
- package/dist/commands/input.d.ts +2 -0
- package/dist/commands/input.js +124 -0
- package/dist/commands/inventory.d.ts +2 -0
- package/dist/commands/inventory.js +28 -0
- package/dist/commands/look.d.ts +2 -0
- package/dist/commands/look.js +37 -0
- package/dist/commands/move.d.ts +2 -0
- package/dist/commands/move.js +50 -0
- package/dist/commands/position.d.ts +2 -0
- package/dist/commands/position.js +7 -0
- package/dist/commands/request-helpers.d.ts +26 -0
- package/dist/commands/request-helpers.js +58 -0
- package/dist/commands/resourcepack.d.ts +2 -0
- package/dist/commands/resourcepack.js +9 -0
- package/dist/commands/rotation.d.ts +2 -0
- package/dist/commands/rotation.js +7 -0
- package/dist/commands/screen.d.ts +2 -0
- package/dist/commands/screen.js +7 -0
- package/dist/commands/screenshot.d.ts +2 -0
- package/dist/commands/screenshot.js +14 -0
- package/dist/commands/server.d.ts +2 -0
- package/dist/commands/server.js +66 -0
- package/dist/commands/sign.d.ts +2 -0
- package/dist/commands/sign.js +30 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +17 -0
- package/dist/commands/wait.d.ts +2 -0
- package/dist/commands/wait.js +23 -0
- package/dist/download/CacheManager.d.ts +20 -0
- package/dist/download/CacheManager.js +50 -0
- package/dist/download/DownloadUtils.d.ts +2 -0
- package/dist/download/DownloadUtils.js +23 -0
- package/dist/download/JavaDetector.d.ts +7 -0
- package/dist/download/JavaDetector.js +31 -0
- package/dist/download/ModVariantCatalog.d.ts +10 -0
- package/dist/download/ModVariantCatalog.js +52 -0
- package/dist/download/SearchCommand.d.ts +29 -0
- package/dist/download/SearchCommand.js +37 -0
- package/dist/download/VersionMatrix.d.ts +72 -0
- package/dist/download/VersionMatrix.js +227 -0
- package/dist/download/client/Arm64LwjglPatcher.d.ts +5 -0
- package/dist/download/client/Arm64LwjglPatcher.js +153 -0
- package/dist/download/client/ClientDownloader.d.ts +42 -0
- package/dist/download/client/ClientDownloader.js +233 -0
- package/dist/download/client/FabricRuntimeDownloader.d.ts +10 -0
- package/dist/download/client/FabricRuntimeDownloader.js +91 -0
- package/dist/download/server/ServerDownloader.d.ts +51 -0
- package/dist/download/server/ServerDownloader.js +196 -0
- package/dist/download/types.d.ts +37 -0
- package/dist/download/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +89 -0
- package/dist/server/ServerManager.d.ts +63 -0
- package/dist/server/ServerManager.js +114 -0
- package/dist/util/command.d.ts +10 -0
- package/dist/util/command.js +35 -0
- package/dist/util/config.d.ts +30 -0
- package/dist/util/config.js +59 -0
- package/dist/util/context.d.ts +17 -0
- package/dist/util/context.js +16 -0
- package/dist/util/errors.d.ts +20 -0
- package/dist/util/errors.js +37 -0
- package/dist/util/net.d.ts +5 -0
- package/dist/util/net.js +35 -0
- package/dist/util/output.d.ts +4 -0
- package/dist/util/output.js +23 -0
- package/dist/util/process.d.ts +3 -0
- package/dist/util/process.js +32 -0
- package/dist/util/state.d.ts +10 -0
- package/dist/util/state.js +40 -0
- package/package.json +54 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
|
+
import { MinecraftFolder, Version } from "@xmcl/core";
|
|
5
|
+
import { getVersionList, installDependencies, installFabric, installVersion } from "@xmcl/installer";
|
|
6
|
+
import { Agent, interceptors } from "undici";
|
|
7
|
+
import { MctError } from "../../util/errors.js";
|
|
8
|
+
import { applyArm64LwjglPatch } from "./Arm64LwjglPatcher.js";
|
|
9
|
+
const DOWNLOAD_DISPATCHER = new Agent({
|
|
10
|
+
connect: {
|
|
11
|
+
timeout: 30_000
|
|
12
|
+
},
|
|
13
|
+
connections: 2
|
|
14
|
+
}).compose(interceptors.retry({
|
|
15
|
+
maxRetries: 4,
|
|
16
|
+
minTimeout: 500,
|
|
17
|
+
maxTimeout: 5_000
|
|
18
|
+
}), interceptors.redirect({
|
|
19
|
+
maxRedirections: 5
|
|
20
|
+
}));
|
|
21
|
+
async function fetchWithRetry(input, init, attempts = 4) {
|
|
22
|
+
let lastError;
|
|
23
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(input, init);
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
29
|
+
if (response.status >= 500 && attempt < attempts) {
|
|
30
|
+
await sleep(attempt * 500);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
lastError = error;
|
|
37
|
+
if (attempt >= attempts) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
await sleep(attempt * 500);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw lastError;
|
|
44
|
+
}
|
|
45
|
+
export async function prepareManagedFabricRuntime(variant, clientRootDir, dependencies = {}) {
|
|
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
|
+
const runtimeRootDir = path.join(clientRootDir, "runtime");
|
|
55
|
+
const gameDir = path.join(clientRootDir, "minecraft");
|
|
56
|
+
await mkdir(runtimeRootDir, { recursive: true });
|
|
57
|
+
await mkdir(gameDir, { recursive: true });
|
|
58
|
+
const minecraft = new MinecraftFolder(runtimeRootDir);
|
|
59
|
+
const versionList = await getVersionList({ fetch: fetchImpl });
|
|
60
|
+
const versionMeta = versionList.versions.find((entry) => entry.id === variant.minecraftVersion);
|
|
61
|
+
if (!versionMeta) {
|
|
62
|
+
throw new MctError({
|
|
63
|
+
code: "UNSUPPORTED_VERSION",
|
|
64
|
+
message: `Minecraft ${variant.minecraftVersion} metadata was not found`
|
|
65
|
+
}, 4);
|
|
66
|
+
}
|
|
67
|
+
await installVersion(versionMeta, minecraft, {
|
|
68
|
+
side: "client",
|
|
69
|
+
dispatcher: DOWNLOAD_DISPATCHER
|
|
70
|
+
});
|
|
71
|
+
const versionId = await installFabric({
|
|
72
|
+
minecraftVersion: variant.minecraftVersion,
|
|
73
|
+
version: loaderVersion,
|
|
74
|
+
minecraft,
|
|
75
|
+
side: "client",
|
|
76
|
+
fetch: fetchImpl
|
|
77
|
+
});
|
|
78
|
+
const resolvedVersion = await Version.parse(minecraft, versionId);
|
|
79
|
+
await installDependencies(resolvedVersion, {
|
|
80
|
+
side: "client",
|
|
81
|
+
dispatcher: DOWNLOAD_DISPATCHER,
|
|
82
|
+
assetsDownloadConcurrency: 2,
|
|
83
|
+
librariesDownloadConcurrency: 2
|
|
84
|
+
});
|
|
85
|
+
await applyArm64LwjglPatch(runtimeRootDir, versionId, { fetchImpl });
|
|
86
|
+
return {
|
|
87
|
+
runtimeRootDir,
|
|
88
|
+
gameDir,
|
|
89
|
+
versionId
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CommandContext } from "../../util/context.js";
|
|
2
|
+
import { CacheManager } from "../CacheManager.js";
|
|
3
|
+
import { type ServerType } from "../VersionMatrix.js";
|
|
4
|
+
type ExecFileLike = (file: string, args: string[], options: {
|
|
5
|
+
cwd?: string;
|
|
6
|
+
maxBuffer?: number;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}>;
|
|
11
|
+
export interface DownloadServerOptions {
|
|
12
|
+
type?: ServerType;
|
|
13
|
+
version?: string;
|
|
14
|
+
build?: string;
|
|
15
|
+
dir?: string;
|
|
16
|
+
fixtures?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DownloadServerDependencies {
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
cacheManager?: CacheManager;
|
|
21
|
+
execFileImpl?: ExecFileLike;
|
|
22
|
+
}
|
|
23
|
+
export declare function resolveServerDownloadSpec(options: DownloadServerOptions): {
|
|
24
|
+
type: "vanilla";
|
|
25
|
+
version: string;
|
|
26
|
+
build: string;
|
|
27
|
+
fileName: string;
|
|
28
|
+
downloadUrl: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "spigot";
|
|
31
|
+
version: string;
|
|
32
|
+
build: string;
|
|
33
|
+
fileName: string;
|
|
34
|
+
downloadUrl: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "paper" | "purpur";
|
|
37
|
+
version: string;
|
|
38
|
+
build: string;
|
|
39
|
+
fileName: string;
|
|
40
|
+
downloadUrl: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function downloadServerJar(context: CommandContext, options: DownloadServerOptions, dependencies?: DownloadServerDependencies): Promise<{
|
|
43
|
+
downloaded: boolean;
|
|
44
|
+
type: "paper" | "purpur" | "spigot" | "vanilla";
|
|
45
|
+
version: string;
|
|
46
|
+
build: string;
|
|
47
|
+
cachePath: string;
|
|
48
|
+
jar: string;
|
|
49
|
+
dir: string;
|
|
50
|
+
}>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { MctError } from "../../util/errors.js";
|
|
7
|
+
import { loadConfig, writeConfig } from "../../util/config.js";
|
|
8
|
+
import { CacheManager } from "../CacheManager.js";
|
|
9
|
+
import { copyFileIfMissing, downloadFile } from "../DownloadUtils.js";
|
|
10
|
+
import { getMinecraftSupport } from "../VersionMatrix.js";
|
|
11
|
+
const SERVER_DOWNLOAD_BASE_URLS = {
|
|
12
|
+
paper: process.env.MCT_PAPER_API_BASE_URL || "https://api.papermc.io/v2/projects",
|
|
13
|
+
purpur: process.env.MCT_PURPUR_API_BASE_URL || "https://api.purpurmc.org/v2"
|
|
14
|
+
};
|
|
15
|
+
const MOJANG_VERSION_MANIFEST_URL = process.env.MCT_MOJANG_VERSION_MANIFEST_URL || "https://launchermeta.mojang.com/mc/game/version_manifest.json";
|
|
16
|
+
const SPIGOT_BUILD_TOOLS_URL = process.env.MCT_SPIGOT_BUILDTOOLS_URL ||
|
|
17
|
+
"https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar";
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
function resolveDownloadUrl(type, version, build) {
|
|
20
|
+
if (type === "paper") {
|
|
21
|
+
return `${SERVER_DOWNLOAD_BASE_URLS.paper}/paper/versions/${version}/builds/${build}/downloads/paper-${version}-${build}.jar`;
|
|
22
|
+
}
|
|
23
|
+
return `${SERVER_DOWNLOAD_BASE_URLS.purpur}/purpur/${version}/${build}/download`;
|
|
24
|
+
}
|
|
25
|
+
async function fetchJsonWithRetry(url, fetchImpl, attempts = 4) {
|
|
26
|
+
let lastError;
|
|
27
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetchImpl(url);
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
32
|
+
}
|
|
33
|
+
return (await response.json());
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
lastError = error;
|
|
37
|
+
if (attempt >= attempts) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
await sleep(attempt * 500);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw lastError;
|
|
44
|
+
}
|
|
45
|
+
async function resolveVanillaDownloadSpec(version, fetchImpl) {
|
|
46
|
+
const manifest = await fetchJsonWithRetry(MOJANG_VERSION_MANIFEST_URL, fetchImpl);
|
|
47
|
+
const versionEntry = manifest.versions.find((entry) => entry.id === version);
|
|
48
|
+
if (!versionEntry) {
|
|
49
|
+
throw new MctError({
|
|
50
|
+
code: "UNSUPPORTED_VERSION",
|
|
51
|
+
message: `Unsupported vanilla version ${version}`
|
|
52
|
+
}, 4);
|
|
53
|
+
}
|
|
54
|
+
const versionJson = await fetchJsonWithRetry(versionEntry.url, fetchImpl);
|
|
55
|
+
const serverDownload = versionJson.downloads?.server;
|
|
56
|
+
if (!serverDownload?.url) {
|
|
57
|
+
throw new MctError({
|
|
58
|
+
code: "UNSUPPORTED_VERSION",
|
|
59
|
+
message: `Vanilla server jar is not available for ${version}`
|
|
60
|
+
}, 4);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
type: "vanilla",
|
|
64
|
+
version,
|
|
65
|
+
build: "release",
|
|
66
|
+
fileName: `vanilla-${version}.jar`,
|
|
67
|
+
downloadUrl: serverDownload.url
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function buildSpigotServerJar(version, cacheManager, fetchImpl, execFileImpl) {
|
|
71
|
+
const buildToolsJarPath = cacheManager.getServerJarPath("spigot", "buildtools", "latest");
|
|
72
|
+
const cachePath = cacheManager.getServerJarPath("spigot", version, "buildtools");
|
|
73
|
+
const buildDir = path.join(cacheManager.getRootDir(), "server", "spigot", "build", version);
|
|
74
|
+
const builtJarPath = path.join(buildDir, `spigot-${version}.jar`);
|
|
75
|
+
try {
|
|
76
|
+
await access(cachePath);
|
|
77
|
+
return {
|
|
78
|
+
cachePath,
|
|
79
|
+
build: "buildtools"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
try {
|
|
84
|
+
await access(buildToolsJarPath);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
await downloadFile(SPIGOT_BUILD_TOOLS_URL, buildToolsJarPath, fetchImpl);
|
|
88
|
+
}
|
|
89
|
+
await mkdir(buildDir, { recursive: true });
|
|
90
|
+
await execFileImpl("java", ["-jar", buildToolsJarPath, "--rev", version, "--compile", "SPIGOT", "--disable-certificate-check", "--output-dir", "."], {
|
|
91
|
+
cwd: buildDir,
|
|
92
|
+
maxBuffer: 32 * 1024 * 1024
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
await access(builtJarPath);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new MctError({
|
|
99
|
+
code: "DOWNLOAD_FAILED",
|
|
100
|
+
message: `BuildTools did not produce spigot-${version}.jar`,
|
|
101
|
+
details: {
|
|
102
|
+
buildDir
|
|
103
|
+
}
|
|
104
|
+
}, 2);
|
|
105
|
+
}
|
|
106
|
+
await copyFileIfMissing(builtJarPath, cachePath);
|
|
107
|
+
return {
|
|
108
|
+
cachePath,
|
|
109
|
+
build: "buildtools"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function resolveServerDownloadSpec(options) {
|
|
113
|
+
const type = options.type ?? "paper";
|
|
114
|
+
const resolvedVersion = options.version ?? "1.21.4";
|
|
115
|
+
const versionEntry = getMinecraftSupport(resolvedVersion);
|
|
116
|
+
if (!versionEntry) {
|
|
117
|
+
throw new MctError({
|
|
118
|
+
code: "UNSUPPORTED_VERSION",
|
|
119
|
+
message: `Unsupported ${type} version ${options.version ?? ""}`.trim()
|
|
120
|
+
}, 4);
|
|
121
|
+
}
|
|
122
|
+
if (type === "vanilla") {
|
|
123
|
+
return {
|
|
124
|
+
type,
|
|
125
|
+
version: resolvedVersion,
|
|
126
|
+
build: "release",
|
|
127
|
+
fileName: `vanilla-${resolvedVersion}.jar`,
|
|
128
|
+
downloadUrl: ""
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (type === "spigot") {
|
|
132
|
+
return {
|
|
133
|
+
type,
|
|
134
|
+
version: resolvedVersion,
|
|
135
|
+
build: "buildtools",
|
|
136
|
+
fileName: `spigot-${resolvedVersion}.jar`,
|
|
137
|
+
downloadUrl: SPIGOT_BUILD_TOOLS_URL
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const resolvedBuild = options.build ?? versionEntry.servers[type].latestBuild?.toString();
|
|
141
|
+
if (!resolvedBuild) {
|
|
142
|
+
throw new MctError({
|
|
143
|
+
code: "INVALID_PARAMS",
|
|
144
|
+
message: `${type} ${resolvedVersion} requires an explicit build`
|
|
145
|
+
}, 4);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
type,
|
|
149
|
+
version: resolvedVersion,
|
|
150
|
+
build: resolvedBuild,
|
|
151
|
+
fileName: `${type}-${resolvedVersion}-${resolvedBuild}.jar`,
|
|
152
|
+
downloadUrl: resolveDownloadUrl(type, resolvedVersion, resolvedBuild)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export async function downloadServerJar(context, options, dependencies = {}) {
|
|
156
|
+
const cacheManager = dependencies.cacheManager ?? new CacheManager();
|
|
157
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
158
|
+
const execFileImpl = (dependencies.execFileImpl ?? execFileAsync);
|
|
159
|
+
const initialSpec = resolveServerDownloadSpec(options);
|
|
160
|
+
const spec = initialSpec.type === "vanilla"
|
|
161
|
+
? await resolveVanillaDownloadSpec(initialSpec.version, fetchImpl)
|
|
162
|
+
: initialSpec;
|
|
163
|
+
const cachePath = spec.type === "spigot"
|
|
164
|
+
? (await buildSpigotServerJar(spec.version, cacheManager, fetchImpl, execFileImpl)).cachePath
|
|
165
|
+
: cacheManager.getServerJarPath(spec.type, spec.version, spec.build);
|
|
166
|
+
if (spec.type !== "spigot") {
|
|
167
|
+
try {
|
|
168
|
+
await access(cachePath);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
await downloadFile(spec.downloadUrl, cachePath, fetchImpl);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const targetDir = path.resolve(context.cwd, options.dir ?? context.config.server.dir);
|
|
175
|
+
const targetJarPath = path.join(targetDir, spec.fileName);
|
|
176
|
+
await copyFileIfMissing(cachePath, targetJarPath);
|
|
177
|
+
if (options.fixtures) {
|
|
178
|
+
const fixturesPath = path.resolve(context.cwd, options.fixtures);
|
|
179
|
+
const pluginsDir = path.join(targetDir, "plugins");
|
|
180
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
181
|
+
await copyFileIfMissing(fixturesPath, path.join(pluginsDir, path.basename(fixturesPath)));
|
|
182
|
+
}
|
|
183
|
+
const latestConfig = await loadConfig(context.configPath, context.cwd);
|
|
184
|
+
latestConfig.server.jar = path.relative(context.cwd, targetJarPath);
|
|
185
|
+
latestConfig.server.dir = path.relative(context.cwd, targetDir) || ".";
|
|
186
|
+
await writeConfig(context.configPath, context.cwd, latestConfig);
|
|
187
|
+
return {
|
|
188
|
+
downloaded: true,
|
|
189
|
+
type: spec.type,
|
|
190
|
+
version: spec.version,
|
|
191
|
+
build: spec.build,
|
|
192
|
+
cachePath,
|
|
193
|
+
jar: targetJarPath,
|
|
194
|
+
dir: targetDir
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type LoaderType = "fabric" | "forge" | "neoforge";
|
|
2
|
+
export type ServerType = "paper" | "purpur" | "spigot" | "vanilla";
|
|
3
|
+
export type SupportLevel = "ready" | "configured" | "planned" | "unsupported";
|
|
4
|
+
export type ValidationLevel = "verified" | "limited" | "planned";
|
|
5
|
+
export interface ModVariant {
|
|
6
|
+
id: string;
|
|
7
|
+
minecraftVersion: string;
|
|
8
|
+
loader: LoaderType;
|
|
9
|
+
support: SupportLevel;
|
|
10
|
+
validation: ValidationLevel;
|
|
11
|
+
modVersion?: string;
|
|
12
|
+
fabricLoaderVersion?: string;
|
|
13
|
+
yarnMappings?: string;
|
|
14
|
+
forgeVersion?: string;
|
|
15
|
+
neoforgeVersion?: string;
|
|
16
|
+
javaVersion?: number;
|
|
17
|
+
notes?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ModVariantCatalog {
|
|
20
|
+
defaultVariant: string;
|
|
21
|
+
variants: ModVariant[];
|
|
22
|
+
}
|
|
23
|
+
export interface ServerVersionSupport {
|
|
24
|
+
type: ServerType;
|
|
25
|
+
version: string;
|
|
26
|
+
support: Exclude<SupportLevel, "unsupported">;
|
|
27
|
+
notes?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ClientVersionSupport {
|
|
30
|
+
version: string;
|
|
31
|
+
loader: LoaderType;
|
|
32
|
+
support: SupportLevel;
|
|
33
|
+
validation: ValidationLevel;
|
|
34
|
+
loaderVersion?: string;
|
|
35
|
+
modVersion?: string;
|
|
36
|
+
notes?: string;
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createBlockCommand } from "./commands/block.js";
|
|
4
|
+
import { createBookCommand } from "./commands/book.js";
|
|
5
|
+
import { createClientCommand } from "./commands/client.js";
|
|
6
|
+
import { createChatCommand } from "./commands/chat.js";
|
|
7
|
+
import { createCombatCommand } from "./commands/combat.js";
|
|
8
|
+
import { createAnvilCommand, createCraftCommand, createEnchantCommand, createTradeCommand } from "./commands/craft.js";
|
|
9
|
+
import { createEntityCommand } from "./commands/entity.js";
|
|
10
|
+
import { createGuiCommand } from "./commands/gui.js";
|
|
11
|
+
import { createHudCommand } from "./commands/hud.js";
|
|
12
|
+
import { createInputCommand } from "./commands/input.js";
|
|
13
|
+
import { createInventoryCommand } from "./commands/inventory.js";
|
|
14
|
+
import { createLookCommand } from "./commands/look.js";
|
|
15
|
+
import { createMoveCommand } from "./commands/move.js";
|
|
16
|
+
import { createPositionCommand } from "./commands/position.js";
|
|
17
|
+
import { createResourcepackCommand } from "./commands/resourcepack.js";
|
|
18
|
+
import { createRotationCommand } from "./commands/rotation.js";
|
|
19
|
+
import { createScreenCommand } from "./commands/screen.js";
|
|
20
|
+
import { createScreenshotCommand } from "./commands/screenshot.js";
|
|
21
|
+
import { createServerCommand } from "./commands/server.js";
|
|
22
|
+
import { createSignCommand } from "./commands/sign.js";
|
|
23
|
+
import { createStatusCommand } from "./commands/status.js";
|
|
24
|
+
import { createWaitCommand } from "./commands/wait.js";
|
|
25
|
+
import { attachGlobalOptions, wrapCommand } from "./util/command.js";
|
|
26
|
+
export function buildProgram() {
|
|
27
|
+
const program = new Command();
|
|
28
|
+
program
|
|
29
|
+
.name("mct")
|
|
30
|
+
.description("MC Pilot – Minecraft plugin/mod automated testing CLI\n\n" +
|
|
31
|
+
"Control a real Minecraft client via CLI to simulate player actions and verify plugin behavior.\n" +
|
|
32
|
+
"All commands output JSON by default. Use --human for human-readable output.\n\n" +
|
|
33
|
+
"Quick start:\n" +
|
|
34
|
+
" mct server download --type paper --version 1.20.4\n" +
|
|
35
|
+
" mct client download --version 1.20.4\n" +
|
|
36
|
+
" mct server start --eula && mct server wait-ready\n" +
|
|
37
|
+
" mct client launch default && mct client wait-ready default\n" +
|
|
38
|
+
" mct chat command \"gamemode creative\"\n" +
|
|
39
|
+
" mct move to 100 64 100\n" +
|
|
40
|
+
" mct screenshot --output ./test.png\n\n" +
|
|
41
|
+
"Multi-client:\n" +
|
|
42
|
+
" mct client download --version 1.20.4 --name p1 --ws-port 25560\n" +
|
|
43
|
+
" mct client download --version 1.20.4 --name p2 --ws-port 25561\n" +
|
|
44
|
+
" mct client launch p1 && mct client launch p2\n" +
|
|
45
|
+
" mct --client p1 chat send \"Hello from p1\"\n" +
|
|
46
|
+
" mct --client p2 chat send \"Hello from p2\"")
|
|
47
|
+
.version("0.1.0", "--cli-version", "Show CLI version");
|
|
48
|
+
attachGlobalOptions(program);
|
|
49
|
+
program
|
|
50
|
+
.command("config-show")
|
|
51
|
+
.description("Show current config and state directory")
|
|
52
|
+
.action(wrapCommand(async (context) => {
|
|
53
|
+
return {
|
|
54
|
+
cwd: context.cwd,
|
|
55
|
+
stateDir: context.state.getRootDir(),
|
|
56
|
+
config: context.config
|
|
57
|
+
};
|
|
58
|
+
}));
|
|
59
|
+
program.addCommand(createServerCommand());
|
|
60
|
+
program.addCommand(createClientCommand());
|
|
61
|
+
program.addCommand(createChatCommand());
|
|
62
|
+
program.addCommand(createInputCommand());
|
|
63
|
+
program.addCommand(createMoveCommand());
|
|
64
|
+
program.addCommand(createLookCommand());
|
|
65
|
+
program.addCommand(createPositionCommand());
|
|
66
|
+
program.addCommand(createRotationCommand());
|
|
67
|
+
program.addCommand(createBlockCommand());
|
|
68
|
+
program.addCommand(createEntityCommand());
|
|
69
|
+
program.addCommand(createInventoryCommand());
|
|
70
|
+
program.addCommand(createGuiCommand());
|
|
71
|
+
program.addCommand(createScreenshotCommand());
|
|
72
|
+
program.addCommand(createScreenCommand());
|
|
73
|
+
program.addCommand(createHudCommand());
|
|
74
|
+
program.addCommand(createStatusCommand());
|
|
75
|
+
program.addCommand(createSignCommand());
|
|
76
|
+
program.addCommand(createBookCommand());
|
|
77
|
+
program.addCommand(createResourcepackCommand());
|
|
78
|
+
program.addCommand(createCombatCommand());
|
|
79
|
+
program.addCommand(createCraftCommand());
|
|
80
|
+
program.addCommand(createAnvilCommand());
|
|
81
|
+
program.addCommand(createEnchantCommand());
|
|
82
|
+
program.addCommand(createTradeCommand());
|
|
83
|
+
program.addCommand(createWaitCommand());
|
|
84
|
+
return program;
|
|
85
|
+
}
|
|
86
|
+
const program = buildProgram();
|
|
87
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
88
|
+
program.parseAsync(process.argv);
|
|
89
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { CommandContext } from "../util/context.js";
|
|
2
|
+
export interface ServerRuntimeState {
|
|
3
|
+
pid: number;
|
|
4
|
+
jar: string;
|
|
5
|
+
dir: string;
|
|
6
|
+
port: number;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
logPath: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StartServerOptions {
|
|
11
|
+
jar?: string;
|
|
12
|
+
dir?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
eula?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class ServerManager {
|
|
17
|
+
private readonly context;
|
|
18
|
+
constructor(context: CommandContext);
|
|
19
|
+
start(options: StartServerOptions): Promise<{
|
|
20
|
+
pid: number;
|
|
21
|
+
jar: string;
|
|
22
|
+
dir: string;
|
|
23
|
+
port: number;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
logPath: string;
|
|
26
|
+
running: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
stop(): Promise<{
|
|
29
|
+
running: boolean;
|
|
30
|
+
stopped: boolean;
|
|
31
|
+
pid?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
running: boolean;
|
|
34
|
+
stopped: boolean;
|
|
35
|
+
pid: number;
|
|
36
|
+
}>;
|
|
37
|
+
status(): Promise<{
|
|
38
|
+
running: boolean;
|
|
39
|
+
} | {
|
|
40
|
+
pid: number;
|
|
41
|
+
jar: string;
|
|
42
|
+
dir: string;
|
|
43
|
+
port: number;
|
|
44
|
+
startedAt: string;
|
|
45
|
+
logPath: string;
|
|
46
|
+
running: boolean;
|
|
47
|
+
stale: boolean;
|
|
48
|
+
} | {
|
|
49
|
+
pid: number;
|
|
50
|
+
jar: string;
|
|
51
|
+
dir: string;
|
|
52
|
+
port: number;
|
|
53
|
+
startedAt: string;
|
|
54
|
+
logPath: string;
|
|
55
|
+
running: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
waitReady(timeoutSeconds: number): Promise<{
|
|
58
|
+
reachable: boolean;
|
|
59
|
+
host: string;
|
|
60
|
+
port: number;
|
|
61
|
+
}>;
|
|
62
|
+
getState(): Promise<ServerRuntimeState | null>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdirSync, openSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { MctError } from "../util/errors.js";
|
|
5
|
+
import { waitForTcpPort } from "../util/net.js";
|
|
6
|
+
import { isProcessRunning, killProcessTree } from "../util/process.js";
|
|
7
|
+
const SERVER_STATE_FILE = "server.json";
|
|
8
|
+
export class ServerManager {
|
|
9
|
+
context;
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
async start(options) {
|
|
14
|
+
const existing = await this.getState();
|
|
15
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
16
|
+
throw new MctError({
|
|
17
|
+
code: "SERVER_ALREADY_RUNNING",
|
|
18
|
+
message: "Server is already running",
|
|
19
|
+
details: existing
|
|
20
|
+
}, 5);
|
|
21
|
+
}
|
|
22
|
+
const jar = options.jar ?? this.context.config.server.jar;
|
|
23
|
+
if (!jar) {
|
|
24
|
+
throw new MctError({
|
|
25
|
+
code: "INVALID_PARAMS",
|
|
26
|
+
message: "Server jar is required"
|
|
27
|
+
}, 4);
|
|
28
|
+
}
|
|
29
|
+
const dir = path.resolve(this.context.cwd, options.dir ?? this.context.config.server.dir);
|
|
30
|
+
const port = options.port ?? this.context.config.server.port;
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
mkdirSync(path.join(this.context.state.getRootDir(), "logs"), { recursive: true });
|
|
33
|
+
if (options.eula) {
|
|
34
|
+
writeFileSync(path.join(dir, "eula.txt"), "eula=true\n", "utf8");
|
|
35
|
+
}
|
|
36
|
+
const logPath = path.join(this.context.state.getRootDir(), "logs", "paper-server.log");
|
|
37
|
+
const stdout = openSync(logPath, "a");
|
|
38
|
+
const child = spawn("java", [...this.context.config.server.jvmArgs, "-jar", path.resolve(this.context.cwd, jar), "nogui"], {
|
|
39
|
+
cwd: dir,
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: ["ignore", stdout, stdout],
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
MCT_SERVER_PORT: String(port)
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
child.unref();
|
|
48
|
+
const state = {
|
|
49
|
+
pid: child.pid ?? 0,
|
|
50
|
+
jar: path.resolve(this.context.cwd, jar),
|
|
51
|
+
dir,
|
|
52
|
+
port,
|
|
53
|
+
startedAt: new Date().toISOString(),
|
|
54
|
+
logPath
|
|
55
|
+
};
|
|
56
|
+
await this.context.state.writeJson(SERVER_STATE_FILE, state);
|
|
57
|
+
return {
|
|
58
|
+
running: true,
|
|
59
|
+
...state
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async stop() {
|
|
63
|
+
const state = await this.getState();
|
|
64
|
+
if (!state) {
|
|
65
|
+
return {
|
|
66
|
+
running: false,
|
|
67
|
+
stopped: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (isProcessRunning(state.pid)) {
|
|
71
|
+
killProcessTree(state.pid);
|
|
72
|
+
}
|
|
73
|
+
await this.context.state.remove(SERVER_STATE_FILE);
|
|
74
|
+
return {
|
|
75
|
+
running: false,
|
|
76
|
+
stopped: true,
|
|
77
|
+
pid: state.pid
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async status() {
|
|
81
|
+
const state = await this.getState();
|
|
82
|
+
if (!state) {
|
|
83
|
+
return {
|
|
84
|
+
running: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const running = isProcessRunning(state.pid);
|
|
88
|
+
if (!running) {
|
|
89
|
+
await this.context.state.remove(SERVER_STATE_FILE);
|
|
90
|
+
return {
|
|
91
|
+
running: false,
|
|
92
|
+
stale: true,
|
|
93
|
+
...state
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
running: true,
|
|
98
|
+
...state
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async waitReady(timeoutSeconds) {
|
|
102
|
+
const state = await this.getState();
|
|
103
|
+
if (!state) {
|
|
104
|
+
throw new MctError({
|
|
105
|
+
code: "SERVER_NOT_RUNNING",
|
|
106
|
+
message: "Server is not running"
|
|
107
|
+
}, 5);
|
|
108
|
+
}
|
|
109
|
+
return waitForTcpPort("127.0.0.1", state.port, timeoutSeconds);
|
|
110
|
+
}
|
|
111
|
+
async getState() {
|
|
112
|
+
return this.context.state.readJson(SERVER_STATE_FILE, null);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createCommandContext, type GlobalOptions } from "./context.js";
|
|
3
|
+
export type CommandAction<TOptions = Record<string, unknown>> = (context: Awaited<ReturnType<typeof createCommandContext>>, payload: {
|
|
4
|
+
args: string[];
|
|
5
|
+
options: TOptions;
|
|
6
|
+
command: Command;
|
|
7
|
+
globalOptions: GlobalOptions;
|
|
8
|
+
}) => Promise<unknown>;
|
|
9
|
+
export declare function attachGlobalOptions(command: Command): Command;
|
|
10
|
+
export declare function wrapCommand<TOptions = Record<string, unknown>>(action: CommandAction<TOptions>): (this: Command, ...input: unknown[]) => Promise<void>;
|