@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
package/bin/mct
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"defaultVariant": "1.20.4-fabric",
|
|
3
|
+
"buildSystem": "multi-module",
|
|
4
|
+
"versions": ["1.18.2", "1.20.1", "1.20.2", "1.20.4", "1.21.1", "1.21.4"],
|
|
5
|
+
"variants": [
|
|
6
|
+
{
|
|
7
|
+
"id": "1.18.2-fabric",
|
|
8
|
+
"minecraftVersion": "1.18.2",
|
|
9
|
+
"loader": "fabric",
|
|
10
|
+
"support": "ready",
|
|
11
|
+
"validation": "verified",
|
|
12
|
+
"modVersion": "0.1.0",
|
|
13
|
+
"fabricLoaderVersion": "0.16.10",
|
|
14
|
+
"yarnMappings": "1.18.2+build.4",
|
|
15
|
+
"javaVersion": 17,
|
|
16
|
+
"gradleModule": "version-1.18.2"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "1.20.1-fabric",
|
|
20
|
+
"minecraftVersion": "1.20.1",
|
|
21
|
+
"loader": "fabric",
|
|
22
|
+
"support": "ready",
|
|
23
|
+
"validation": "verified",
|
|
24
|
+
"modVersion": "0.1.0",
|
|
25
|
+
"fabricLoaderVersion": "0.16.10",
|
|
26
|
+
"yarnMappings": "1.20.1+build.10",
|
|
27
|
+
"javaVersion": 17,
|
|
28
|
+
"gradleModule": "version-1.20.1"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "1.20.1-forge",
|
|
32
|
+
"minecraftVersion": "1.20.1",
|
|
33
|
+
"loader": "forge",
|
|
34
|
+
"support": "planned",
|
|
35
|
+
"validation": "planned",
|
|
36
|
+
"modVersion": "0.1.0",
|
|
37
|
+
"forgeVersion": "47.3.0",
|
|
38
|
+
"javaVersion": 17
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "1.20.2-fabric",
|
|
42
|
+
"minecraftVersion": "1.20.2",
|
|
43
|
+
"loader": "fabric",
|
|
44
|
+
"support": "ready",
|
|
45
|
+
"validation": "verified",
|
|
46
|
+
"modVersion": "0.1.0",
|
|
47
|
+
"fabricLoaderVersion": "0.16.10",
|
|
48
|
+
"yarnMappings": "1.20.2+build.4",
|
|
49
|
+
"javaVersion": 17,
|
|
50
|
+
"gradleModule": "version-1.20.2"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "1.20.2-forge",
|
|
54
|
+
"minecraftVersion": "1.20.2",
|
|
55
|
+
"loader": "forge",
|
|
56
|
+
"support": "planned",
|
|
57
|
+
"validation": "planned",
|
|
58
|
+
"modVersion": "0.1.0",
|
|
59
|
+
"forgeVersion": "48.1.0",
|
|
60
|
+
"javaVersion": 17
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "1.20.4-fabric",
|
|
64
|
+
"minecraftVersion": "1.20.4",
|
|
65
|
+
"loader": "fabric",
|
|
66
|
+
"support": "ready",
|
|
67
|
+
"validation": "verified",
|
|
68
|
+
"modVersion": "0.1.0",
|
|
69
|
+
"fabricLoaderVersion": "0.16.10",
|
|
70
|
+
"yarnMappings": "1.20.4+build.3",
|
|
71
|
+
"javaVersion": 17,
|
|
72
|
+
"gradleModule": "version-1.20.4"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "1.20.4-forge",
|
|
76
|
+
"minecraftVersion": "1.20.4",
|
|
77
|
+
"loader": "forge",
|
|
78
|
+
"support": "planned",
|
|
79
|
+
"validation": "planned",
|
|
80
|
+
"modVersion": "0.1.0",
|
|
81
|
+
"forgeVersion": "49.0.49",
|
|
82
|
+
"javaVersion": 17
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "1.20.4-neoforge",
|
|
86
|
+
"minecraftVersion": "1.20.4",
|
|
87
|
+
"loader": "neoforge",
|
|
88
|
+
"support": "planned",
|
|
89
|
+
"validation": "planned",
|
|
90
|
+
"modVersion": "0.1.0",
|
|
91
|
+
"neoforgeVersion": "20.4.237",
|
|
92
|
+
"javaVersion": 17
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "1.21.1-fabric",
|
|
96
|
+
"minecraftVersion": "1.21.1",
|
|
97
|
+
"loader": "fabric",
|
|
98
|
+
"support": "ready",
|
|
99
|
+
"validation": "verified",
|
|
100
|
+
"modVersion": "0.1.0",
|
|
101
|
+
"fabricLoaderVersion": "0.16.10",
|
|
102
|
+
"yarnMappings": "1.21.1+build.3",
|
|
103
|
+
"javaVersion": 21,
|
|
104
|
+
"gradleModule": "version-1.21.1"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "1.21.1-neoforge",
|
|
108
|
+
"minecraftVersion": "1.21.1",
|
|
109
|
+
"loader": "neoforge",
|
|
110
|
+
"support": "planned",
|
|
111
|
+
"validation": "planned",
|
|
112
|
+
"modVersion": "0.1.0",
|
|
113
|
+
"neoforgeVersion": "21.1.77",
|
|
114
|
+
"javaVersion": 21
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "1.21.4-fabric",
|
|
118
|
+
"minecraftVersion": "1.21.4",
|
|
119
|
+
"loader": "fabric",
|
|
120
|
+
"support": "ready",
|
|
121
|
+
"validation": "verified",
|
|
122
|
+
"modVersion": "0.1.0",
|
|
123
|
+
"fabricLoaderVersion": "0.16.10",
|
|
124
|
+
"yarnMappings": "1.21.4+build.1",
|
|
125
|
+
"javaVersion": 21,
|
|
126
|
+
"gradleModule": "version-1.21.4"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"id": "1.21.4-neoforge",
|
|
130
|
+
"minecraftVersion": "1.21.4",
|
|
131
|
+
"loader": "neoforge",
|
|
132
|
+
"support": "planned",
|
|
133
|
+
"validation": "planned",
|
|
134
|
+
"modVersion": "0.1.0",
|
|
135
|
+
"neoforgeVersion": "21.4.75",
|
|
136
|
+
"javaVersion": 21
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { CommandContext } from "../util/context.js";
|
|
2
|
+
export interface ClientRuntimeState {
|
|
3
|
+
name: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
account?: string;
|
|
6
|
+
server?: string;
|
|
7
|
+
wsPort: number;
|
|
8
|
+
headless: boolean;
|
|
9
|
+
pid: number;
|
|
10
|
+
startedAt: string;
|
|
11
|
+
logPath: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LaunchClientOptions {
|
|
14
|
+
name: string;
|
|
15
|
+
version?: string;
|
|
16
|
+
account?: string;
|
|
17
|
+
server?: string;
|
|
18
|
+
wsPort?: number;
|
|
19
|
+
headless?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class ClientManager {
|
|
22
|
+
private readonly context;
|
|
23
|
+
constructor(context: CommandContext);
|
|
24
|
+
launch(options: LaunchClientOptions): Promise<ClientRuntimeState>;
|
|
25
|
+
stop(name: string): Promise<{
|
|
26
|
+
stopped: boolean;
|
|
27
|
+
name: string;
|
|
28
|
+
pid?: undefined;
|
|
29
|
+
} | {
|
|
30
|
+
stopped: boolean;
|
|
31
|
+
name: string;
|
|
32
|
+
pid: number;
|
|
33
|
+
}>;
|
|
34
|
+
list(): Promise<{
|
|
35
|
+
defaultClient: string | undefined;
|
|
36
|
+
clients: ({
|
|
37
|
+
running: boolean;
|
|
38
|
+
detached: boolean;
|
|
39
|
+
name: string;
|
|
40
|
+
version?: string;
|
|
41
|
+
account?: string;
|
|
42
|
+
server?: string;
|
|
43
|
+
wsPort: number;
|
|
44
|
+
headless: boolean;
|
|
45
|
+
pid: number;
|
|
46
|
+
startedAt: string;
|
|
47
|
+
logPath: string;
|
|
48
|
+
} | {
|
|
49
|
+
running: boolean;
|
|
50
|
+
stale: boolean;
|
|
51
|
+
name: string;
|
|
52
|
+
version?: string;
|
|
53
|
+
account?: string;
|
|
54
|
+
server?: string;
|
|
55
|
+
wsPort: number;
|
|
56
|
+
headless: boolean;
|
|
57
|
+
pid: number;
|
|
58
|
+
startedAt: string;
|
|
59
|
+
logPath: string;
|
|
60
|
+
} | {
|
|
61
|
+
running: boolean;
|
|
62
|
+
name: string;
|
|
63
|
+
version?: string;
|
|
64
|
+
account?: string;
|
|
65
|
+
server?: string;
|
|
66
|
+
wsPort: number;
|
|
67
|
+
headless: boolean;
|
|
68
|
+
pid: number;
|
|
69
|
+
startedAt: string;
|
|
70
|
+
logPath: string;
|
|
71
|
+
})[];
|
|
72
|
+
}>;
|
|
73
|
+
waitReady(name: string, timeoutSeconds: number): Promise<{
|
|
74
|
+
connected: boolean;
|
|
75
|
+
url: string;
|
|
76
|
+
}>;
|
|
77
|
+
getClient(name?: string): Promise<ClientRuntimeState>;
|
|
78
|
+
getWsUrl(wsPort: number): string;
|
|
79
|
+
private getSnapshot;
|
|
80
|
+
private writeSnapshot;
|
|
81
|
+
private isWsReachable;
|
|
82
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { mkdirSync, openSync } 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 { getListeningPids, isProcessRunning, killProcessTree } from "../util/process.js";
|
|
6
|
+
import { WebSocketClient } from "./WebSocketClient.js";
|
|
7
|
+
const CLIENT_STATE_FILE = "clients.json";
|
|
8
|
+
function getDefaultSnapshot() {
|
|
9
|
+
return {
|
|
10
|
+
clients: {}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export class ClientManager {
|
|
14
|
+
context;
|
|
15
|
+
constructor(context) {
|
|
16
|
+
this.context = context;
|
|
17
|
+
}
|
|
18
|
+
async launch(options) {
|
|
19
|
+
const snapshot = await this.getSnapshot();
|
|
20
|
+
const existing = snapshot.clients[options.name];
|
|
21
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
22
|
+
throw new MctError({
|
|
23
|
+
code: "CLIENT_ALREADY_RUNNING",
|
|
24
|
+
message: `Client ${options.name} is already running`,
|
|
25
|
+
details: existing
|
|
26
|
+
}, 3);
|
|
27
|
+
}
|
|
28
|
+
const configured = this.context.config.clients[options.name] ?? {};
|
|
29
|
+
const wsPort = options.wsPort ?? configured.wsPort;
|
|
30
|
+
if (!wsPort) {
|
|
31
|
+
throw new MctError({
|
|
32
|
+
code: "INVALID_PARAMS",
|
|
33
|
+
message: `Client ${options.name} requires wsPort`
|
|
34
|
+
}, 4);
|
|
35
|
+
}
|
|
36
|
+
const listeningPids = getListeningPids(wsPort);
|
|
37
|
+
for (const pid of listeningPids) {
|
|
38
|
+
killProcessTree(pid);
|
|
39
|
+
}
|
|
40
|
+
if (listeningPids.length > 0) {
|
|
41
|
+
const deadline = Date.now() + 10_000;
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
if (getListeningPids(wsPort).length === 0) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const launchCommand = configured.launchCommand;
|
|
50
|
+
if (!launchCommand || launchCommand.length === 0) {
|
|
51
|
+
throw new MctError({
|
|
52
|
+
code: "INVALID_PARAMS",
|
|
53
|
+
message: `Client ${options.name} requires launchCommand in config`
|
|
54
|
+
}, 4);
|
|
55
|
+
}
|
|
56
|
+
const cwd = configured.workingDir
|
|
57
|
+
? path.resolve(this.context.cwd, configured.workingDir)
|
|
58
|
+
: this.context.cwd;
|
|
59
|
+
mkdirSync(path.join(this.context.state.getRootDir(), "logs"), { recursive: true });
|
|
60
|
+
const logPath = path.join(this.context.state.getRootDir(), "logs", `client-${options.name}.log`);
|
|
61
|
+
const stdout = openSync(logPath, "a");
|
|
62
|
+
const child = spawn(launchCommand[0], launchCommand.slice(1), {
|
|
63
|
+
cwd,
|
|
64
|
+
detached: true,
|
|
65
|
+
stdio: ["ignore", stdout, stdout],
|
|
66
|
+
env: {
|
|
67
|
+
...process.env,
|
|
68
|
+
...configured.env,
|
|
69
|
+
MCT_CLIENT_NAME: options.name,
|
|
70
|
+
MCT_CLIENT_VERSION: options.version ?? configured.version ?? "",
|
|
71
|
+
MCT_CLIENT_ACCOUNT: options.account ?? configured.account ?? "",
|
|
72
|
+
MCT_CLIENT_SERVER: options.server ?? configured.server ?? "",
|
|
73
|
+
MCT_CLIENT_WS_PORT: String(wsPort),
|
|
74
|
+
MCT_CLIENT_HEADLESS: String(options.headless ?? configured.headless ?? false)
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
child.unref();
|
|
78
|
+
const clientState = {
|
|
79
|
+
name: options.name,
|
|
80
|
+
version: options.version ?? configured.version,
|
|
81
|
+
account: options.account ?? configured.account,
|
|
82
|
+
server: options.server ?? configured.server,
|
|
83
|
+
wsPort,
|
|
84
|
+
headless: options.headless ?? configured.headless ?? false,
|
|
85
|
+
pid: child.pid ?? 0,
|
|
86
|
+
startedAt: new Date().toISOString(),
|
|
87
|
+
logPath
|
|
88
|
+
};
|
|
89
|
+
snapshot.defaultClient ??= options.name;
|
|
90
|
+
snapshot.clients[options.name] = clientState;
|
|
91
|
+
await this.writeSnapshot(snapshot);
|
|
92
|
+
return clientState;
|
|
93
|
+
}
|
|
94
|
+
async stop(name) {
|
|
95
|
+
const snapshot = await this.getSnapshot();
|
|
96
|
+
const client = snapshot.clients[name];
|
|
97
|
+
if (!client) {
|
|
98
|
+
return {
|
|
99
|
+
stopped: false,
|
|
100
|
+
name
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (isProcessRunning(client.pid)) {
|
|
104
|
+
killProcessTree(client.pid);
|
|
105
|
+
}
|
|
106
|
+
for (const pid of getListeningPids(client.wsPort)) {
|
|
107
|
+
if (pid !== client.pid) {
|
|
108
|
+
killProcessTree(pid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
delete snapshot.clients[name];
|
|
112
|
+
if (snapshot.defaultClient === name) {
|
|
113
|
+
snapshot.defaultClient = Object.keys(snapshot.clients)[0];
|
|
114
|
+
}
|
|
115
|
+
await this.writeSnapshot(snapshot);
|
|
116
|
+
return {
|
|
117
|
+
stopped: true,
|
|
118
|
+
name,
|
|
119
|
+
pid: client.pid
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async list() {
|
|
123
|
+
const snapshot = await this.getSnapshot();
|
|
124
|
+
const clients = await Promise.all(Object.values(snapshot.clients).map(async (client) => {
|
|
125
|
+
const running = isProcessRunning(client.pid);
|
|
126
|
+
if (!running) {
|
|
127
|
+
const wsReachable = await this.isWsReachable(client.wsPort, 1);
|
|
128
|
+
if (wsReachable) {
|
|
129
|
+
return {
|
|
130
|
+
...client,
|
|
131
|
+
running: true,
|
|
132
|
+
detached: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
...client,
|
|
137
|
+
running: false,
|
|
138
|
+
stale: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...client,
|
|
143
|
+
running: true
|
|
144
|
+
};
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
defaultClient: snapshot.defaultClient,
|
|
148
|
+
clients
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async waitReady(name, timeoutSeconds) {
|
|
152
|
+
const client = await this.getClient(name);
|
|
153
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
154
|
+
while (Date.now() < deadline) {
|
|
155
|
+
try {
|
|
156
|
+
const ws = new WebSocketClient(this.getWsUrl(client.wsPort));
|
|
157
|
+
return await ws.ping(1);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
await new Promise((resolve) => {
|
|
161
|
+
setTimeout(resolve, 500);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new MctError({
|
|
166
|
+
code: "TIMEOUT",
|
|
167
|
+
message: `Timed out waiting for client ${name} to open WebSocket on ${this.getWsUrl(client.wsPort)}`
|
|
168
|
+
}, 2);
|
|
169
|
+
}
|
|
170
|
+
async getClient(name) {
|
|
171
|
+
const snapshot = await this.getSnapshot();
|
|
172
|
+
const resolvedName = name ?? snapshot.defaultClient;
|
|
173
|
+
if (!resolvedName) {
|
|
174
|
+
throw new MctError({
|
|
175
|
+
code: "CLIENT_NOT_FOUND",
|
|
176
|
+
message: "No client is configured or running"
|
|
177
|
+
}, 3);
|
|
178
|
+
}
|
|
179
|
+
const client = snapshot.clients[resolvedName];
|
|
180
|
+
if (!client) {
|
|
181
|
+
throw new MctError({
|
|
182
|
+
code: "CLIENT_NOT_FOUND",
|
|
183
|
+
message: `Client ${resolvedName} was not found`
|
|
184
|
+
}, 3);
|
|
185
|
+
}
|
|
186
|
+
if (!isProcessRunning(client.pid) && !(await this.isWsReachable(client.wsPort, 1))) {
|
|
187
|
+
throw new MctError({
|
|
188
|
+
code: "CLIENT_NOT_RUNNING",
|
|
189
|
+
message: `Client ${resolvedName} is not running`
|
|
190
|
+
}, 3);
|
|
191
|
+
}
|
|
192
|
+
return client;
|
|
193
|
+
}
|
|
194
|
+
getWsUrl(wsPort) {
|
|
195
|
+
return `ws://127.0.0.1:${wsPort}`;
|
|
196
|
+
}
|
|
197
|
+
async getSnapshot() {
|
|
198
|
+
return this.context.state.readJson(CLIENT_STATE_FILE, getDefaultSnapshot());
|
|
199
|
+
}
|
|
200
|
+
async writeSnapshot(snapshot) {
|
|
201
|
+
await this.context.state.writeJson(CLIENT_STATE_FILE, snapshot);
|
|
202
|
+
}
|
|
203
|
+
async isWsReachable(wsPort, timeoutSeconds) {
|
|
204
|
+
try {
|
|
205
|
+
const ws = new WebSocketClient(this.getWsUrl(wsPort));
|
|
206
|
+
await ws.ping(timeoutSeconds);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WsRequest {
|
|
2
|
+
id: string;
|
|
3
|
+
action: string;
|
|
4
|
+
params?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export declare class WebSocketClient {
|
|
7
|
+
private readonly url;
|
|
8
|
+
constructor(url: string);
|
|
9
|
+
send(action: string, params?: Record<string, unknown>, timeoutSeconds?: number): Promise<unknown>;
|
|
10
|
+
ping(timeoutSeconds?: number): Promise<{
|
|
11
|
+
connected: boolean;
|
|
12
|
+
url: string;
|
|
13
|
+
}>;
|
|
14
|
+
private connect;
|
|
15
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { MctError } from "../util/errors.js";
|
|
4
|
+
export class WebSocketClient {
|
|
5
|
+
url;
|
|
6
|
+
constructor(url) {
|
|
7
|
+
this.url = url;
|
|
8
|
+
}
|
|
9
|
+
async send(action, params = {}, timeoutSeconds = 10) {
|
|
10
|
+
const request = {
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
action,
|
|
13
|
+
params
|
|
14
|
+
};
|
|
15
|
+
const socket = await this.connect(timeoutSeconds);
|
|
16
|
+
try {
|
|
17
|
+
const response = await new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
reject(new MctError({
|
|
20
|
+
code: "TIMEOUT",
|
|
21
|
+
message: `Timed out waiting for response from ${this.url}`
|
|
22
|
+
}, 2));
|
|
23
|
+
}, timeoutSeconds * 1000);
|
|
24
|
+
socket.once("message", (data) => {
|
|
25
|
+
clearTimeout(timeout);
|
|
26
|
+
try {
|
|
27
|
+
resolve(JSON.parse(data.toString()));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
reject(error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
socket.once("error", (error) => {
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
socket.send(JSON.stringify(request));
|
|
38
|
+
});
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
socket.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async ping(timeoutSeconds = 10) {
|
|
46
|
+
const socket = await this.connect(timeoutSeconds);
|
|
47
|
+
socket.close();
|
|
48
|
+
return {
|
|
49
|
+
connected: true,
|
|
50
|
+
url: this.url
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async connect(timeoutSeconds) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const socket = new WebSocket(this.url);
|
|
56
|
+
const timeout = setTimeout(() => {
|
|
57
|
+
socket.terminate();
|
|
58
|
+
reject(new MctError({
|
|
59
|
+
code: "TIMEOUT",
|
|
60
|
+
message: `Timed out connecting to ${this.url}`
|
|
61
|
+
}, 2));
|
|
62
|
+
}, timeoutSeconds * 1000);
|
|
63
|
+
socket.once("open", () => {
|
|
64
|
+
clearTimeout(timeout);
|
|
65
|
+
resolve(socket);
|
|
66
|
+
});
|
|
67
|
+
socket.once("error", () => {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
reject(new MctError({
|
|
70
|
+
code: "CONNECTION_FAILED",
|
|
71
|
+
message: `Unable to connect to ${this.url}`
|
|
72
|
+
}, 1));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequestAction } from "./request-helpers.js";
|
|
3
|
+
export function createBlockCommand() {
|
|
4
|
+
const command = new Command("block").description("Block interaction");
|
|
5
|
+
command
|
|
6
|
+
.command("break")
|
|
7
|
+
.description("Break a block")
|
|
8
|
+
.argument("<x>", "X coordinate")
|
|
9
|
+
.argument("<y>", "Y coordinate")
|
|
10
|
+
.argument("<z>", "Z coordinate")
|
|
11
|
+
.action(createRequestAction("block.break", ({ args }) => ({
|
|
12
|
+
x: Number(args[0]),
|
|
13
|
+
y: Number(args[1]),
|
|
14
|
+
z: Number(args[2])
|
|
15
|
+
})));
|
|
16
|
+
command
|
|
17
|
+
.command("place")
|
|
18
|
+
.description("Place the held block at the given position")
|
|
19
|
+
.argument("<x>", "X coordinate")
|
|
20
|
+
.argument("<y>", "Y coordinate")
|
|
21
|
+
.argument("<z>", "Z coordinate")
|
|
22
|
+
.requiredOption("--face <face>", "Block face to place against: up|down|north|south|east|west")
|
|
23
|
+
.action(createRequestAction("block.place", ({ args, options }) => ({
|
|
24
|
+
x: Number(args[0]),
|
|
25
|
+
y: Number(args[1]),
|
|
26
|
+
z: Number(args[2]),
|
|
27
|
+
face: options.face
|
|
28
|
+
})));
|
|
29
|
+
command
|
|
30
|
+
.command("interact")
|
|
31
|
+
.description("Right-click a block (e.g. open chest, crafting table, door)")
|
|
32
|
+
.argument("<x>", "X coordinate")
|
|
33
|
+
.argument("<y>", "Y coordinate")
|
|
34
|
+
.argument("<z>", "Z coordinate")
|
|
35
|
+
.action(createRequestAction("block.interact", ({ args }) => ({
|
|
36
|
+
x: Number(args[0]),
|
|
37
|
+
y: Number(args[1]),
|
|
38
|
+
z: Number(args[2])
|
|
39
|
+
})));
|
|
40
|
+
command
|
|
41
|
+
.command("get")
|
|
42
|
+
.description("Query block info at coordinates")
|
|
43
|
+
.argument("<x>", "X coordinate")
|
|
44
|
+
.argument("<y>", "Y coordinate")
|
|
45
|
+
.argument("<z>", "Z coordinate")
|
|
46
|
+
.action(createRequestAction("block.get", ({ args }) => ({
|
|
47
|
+
x: Number(args[0]),
|
|
48
|
+
y: Number(args[1]),
|
|
49
|
+
z: Number(args[2])
|
|
50
|
+
})));
|
|
51
|
+
return command;
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequestAction } from "./request-helpers.js";
|
|
3
|
+
export function createBookCommand() {
|
|
4
|
+
const command = new Command("book").description("Book and quill operations (must hold a writable book)");
|
|
5
|
+
command.command("read").description("Read book contents").action(createRequestAction("book.read", () => ({})));
|
|
6
|
+
command
|
|
7
|
+
.command("write")
|
|
8
|
+
.description("Write book pages")
|
|
9
|
+
.requiredOption("--pages <pages...>", "Page contents, e.g. --pages \"Page 1 text\" \"Page 2 text\"")
|
|
10
|
+
.action(createRequestAction("book.write", ({ options }) => ({ pages: options.pages })));
|
|
11
|
+
command
|
|
12
|
+
.command("sign")
|
|
13
|
+
.description("Sign and close the book")
|
|
14
|
+
.requiredOption("--title <title>", "Book title")
|
|
15
|
+
.requiredOption("--author <author>", "Author name")
|
|
16
|
+
.action(createRequestAction("book.sign", ({ options }) => ({
|
|
17
|
+
title: options.title,
|
|
18
|
+
author: options.author
|
|
19
|
+
})));
|
|
20
|
+
return command;
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequestAction, parseJson, withTransportTimeoutBuffer } from "./request-helpers.js";
|
|
3
|
+
export function createChannelCommand() {
|
|
4
|
+
const command = new Command("channel").description("Plugin Channel");
|
|
5
|
+
command
|
|
6
|
+
.command("send")
|
|
7
|
+
.description("发送频道消息")
|
|
8
|
+
.argument("<channel>", "频道名称")
|
|
9
|
+
.requiredOption("--data <json>", "JSON 数据")
|
|
10
|
+
.action(createRequestAction("channel.send", ({ args, options }) => ({
|
|
11
|
+
channel: args[0],
|
|
12
|
+
data: parseJson(String(options.data), "data")
|
|
13
|
+
})));
|
|
14
|
+
command
|
|
15
|
+
.command("listen")
|
|
16
|
+
.description("监听频道消息")
|
|
17
|
+
.argument("<channel>", "频道名称")
|
|
18
|
+
.option("--timeout <seconds>", "等待超时秒数", Number)
|
|
19
|
+
.action(createRequestAction("channel.listen", ({ args, options }) => ({
|
|
20
|
+
channel: args[0],
|
|
21
|
+
timeout: options.timeout
|
|
22
|
+
}), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
|
|
23
|
+
return command;
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequestAction, withTransportTimeoutBuffer } from "./request-helpers.js";
|
|
3
|
+
export function createChatCommand() {
|
|
4
|
+
const command = new Command("chat").description("Chat and server commands");
|
|
5
|
+
command
|
|
6
|
+
.command("send")
|
|
7
|
+
.description("Send a chat message")
|
|
8
|
+
.argument("<message>", "Message text")
|
|
9
|
+
.action(createRequestAction("chat.send", ({ args }) => ({ message: args[0] })));
|
|
10
|
+
command
|
|
11
|
+
.command("command")
|
|
12
|
+
.description("Execute a server command (no / prefix needed)")
|
|
13
|
+
.argument("<command>", "Command text, e.g. \"gamemode creative\"")
|
|
14
|
+
.action(createRequestAction("chat.command", ({ args }) => ({ command: args[0] })));
|
|
15
|
+
command
|
|
16
|
+
.command("history")
|
|
17
|
+
.description("Get chat history")
|
|
18
|
+
.option("--last <count>", "Number of recent messages (default: 10)", Number)
|
|
19
|
+
.action(createRequestAction("chat.history", ({ options }) => ({ last: options.last ?? 10 })));
|
|
20
|
+
command
|
|
21
|
+
.command("wait")
|
|
22
|
+
.description("Wait for a chat message matching a pattern")
|
|
23
|
+
.requiredOption("--match <pattern>", "Substring match by default; prefix with / for regex (e.g. /player\\d+/)")
|
|
24
|
+
.option("--timeout <seconds>", "Timeout in seconds", Number)
|
|
25
|
+
.action(createRequestAction("chat.wait", ({ options }) => ({ match: options.match, timeout: options.timeout }), ({ options }, context) => withTransportTimeoutBuffer(options.timeout ? Number(options.timeout) : undefined, context.config.timeout.default)));
|
|
26
|
+
command
|
|
27
|
+
.command("last")
|
|
28
|
+
.description("Get the last chat message")
|
|
29
|
+
.action(createRequestAction("chat.last", () => ({})));
|
|
30
|
+
return command;
|
|
31
|
+
}
|