vitaminmcp 2.2.0 → 3.0.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/README.md +10 -2
- package/bin/vitaminmcp.mjs +21 -4
- package/checksums.json +8 -3
- package/lib/jars.mjs +3 -0
- package/lib/node.mjs +13 -3
- package/lib/viewer-loader.mjs +74 -0
- package/package.json +1 -1
- package/runner/src/actions.mjs +37 -1
- package/runner/src/bots.mjs +5 -3
- package/runner/src/clientview.mjs +43 -2
- package/runner/src/dispatch.mjs +15 -2
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ agent leaves its host, ports and token where this server reads them.
|
|
|
60
60
|
| `VITAMINMCP_RUNNER_JAR` | use this runner path instead of automatic selection |
|
|
61
61
|
| `VITAMINMCP_NODE` | Node executable for the source runner fallback |
|
|
62
62
|
| `VITAMINMCP_NODE_RUNNER` | bundled `runner.mjs` path when Node is available |
|
|
63
|
+
| `VITAMINMCP_VIEWER_PATH` | local viewer module to use instead of the pinned optional asset |
|
|
63
64
|
|
|
64
65
|
## What it downloads
|
|
65
66
|
|
|
@@ -71,8 +72,15 @@ matching this package's version, into `~/.vitaminmcp/jars/<version>/`:
|
|
|
71
72
|
- otherwise one platform runner asset (`win-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` or
|
|
72
73
|
`darwin-arm64`), checked against a SHA-256 pinned into this package
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
The trimmed `bot-runner-viewer-win-x64.tgz` is separate and is downloaded into
|
|
76
|
+
`~/.vitaminmcp/assets/<version>/` only when `bot_view` asks for a world view. It is checked against
|
|
77
|
+
the SHA-256 pinned into this package at publish time, extracted on first use, and never fetched by
|
|
78
|
+
an installation that only spawns bots or uses the inventory viewer. The current native viewer asset
|
|
79
|
+
is Windows x64; on Linux and macOS, set `VITAMINMCP_VIEWER_PATH` to a local sidecar until those
|
|
80
|
+
assets are released.
|
|
81
|
+
|
|
82
|
+
Every downloaded file is checked against its pinned SHA-256. A file that does not match is deleted
|
|
83
|
+
rather than run.
|
|
76
84
|
|
|
77
85
|
Full documentation, design notes and the plugin itself:
|
|
78
86
|
**[github.com/Backas03/VitaminMCP-minecraft](https://github.com/Backas03/VitaminMCP-minecraft)**
|
package/bin/vitaminmcp.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
MCP_SERVER_JAR, assetPath, ensureAsset, ensureJar,
|
|
11
11
|
} from '../lib/jars.mjs';
|
|
12
12
|
import { checkJava, findJava } from '../lib/java.mjs';
|
|
13
|
-
import { checkNode, findNode, runnerAssetName } from '../lib/node.mjs';
|
|
13
|
+
import { checkNode, findNode, runnerAssetName, viewerAssetName } from '../lib/node.mjs';
|
|
14
14
|
|
|
15
15
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
|
|
@@ -91,6 +91,13 @@ async function main() {
|
|
|
91
91
|
const configuredRunner = process.env.VITAMINMCP_RUNNER_JAR;
|
|
92
92
|
const node = findNode();
|
|
93
93
|
const nodeCheck = checkNode(node);
|
|
94
|
+
let viewerAsset;
|
|
95
|
+
try {
|
|
96
|
+
viewerAsset = viewerAssetName();
|
|
97
|
+
} catch {
|
|
98
|
+
// The source runner can still be used on platforms whose native viewer asset is not released.
|
|
99
|
+
viewerAsset = '';
|
|
100
|
+
}
|
|
94
101
|
const sourceRunner = process.env.VITAMINMCP_NODE_RUNNER
|
|
95
102
|
?? path.join(HERE, '..', 'runner', 'runner.mjs');
|
|
96
103
|
let runner;
|
|
@@ -114,7 +121,7 @@ async function main() {
|
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
await runnerReady;
|
|
117
|
-
return await run(java, server, runner);
|
|
124
|
+
return await run(java, server, runner, release, viewerAsset);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
/**
|
|
@@ -124,10 +131,20 @@ async function main() {
|
|
|
124
131
|
* `.part` file it leaves is claimed again by the next start, and a client waiting on a process
|
|
125
132
|
* that no longer serves anything is worse than a jar fetched twice.
|
|
126
133
|
*/
|
|
127
|
-
function run(java, server, runner) {
|
|
134
|
+
function run(java, server, runner, release, viewerAsset) {
|
|
135
|
+
const env = {
|
|
136
|
+
...process.env,
|
|
137
|
+
VITAMINMCP_RUNNER_JAR: runner,
|
|
138
|
+
VITAMINMCP_VERSION: release,
|
|
139
|
+
VITAMINMCP_VIEWER_ASSET: viewerAsset,
|
|
140
|
+
};
|
|
141
|
+
// The loader imports the pinned archive only when the Node runner receives bot_view(world).
|
|
142
|
+
if (!env.VITAMINMCP_VIEWER_PATH) {
|
|
143
|
+
env.VITAMINMCP_VIEWER_PATH = path.join(HERE, '..', 'lib', 'viewer-loader.mjs');
|
|
144
|
+
}
|
|
128
145
|
const child = spawn(java, ['-jar', server], {
|
|
129
146
|
stdio: 'inherit',
|
|
130
|
-
env
|
|
147
|
+
env,
|
|
131
148
|
});
|
|
132
149
|
|
|
133
150
|
return new Promise((resolve) => {
|
package/checksums.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "3.0.0",
|
|
3
3
|
"jars": {
|
|
4
|
-
"mcp-server.jar": "
|
|
4
|
+
"mcp-server.jar": "576610b6f7deb691fad5633533678ef7a9266574ad731094cc78a01bb24be480"
|
|
5
5
|
},
|
|
6
6
|
"assets": {
|
|
7
|
-
"bot-runner-win-x64.exe": "
|
|
7
|
+
"bot-runner-win-x64.exe": "1a762d0773269ea3205ccb6816e964f33f3ddd15fd1003c6cadd5af4460572ca",
|
|
8
|
+
"bot-runner-linux-x64": "a2e975d331461fc699c1998e86a847f8ca161a266caae57c6060bc74bf87569c",
|
|
9
|
+
"bot-runner-linux-arm64": "31aad6207a268ddb70298ca1eaa2d443c2f097e1e5b714a2db6b40eea57ec749",
|
|
10
|
+
"bot-runner-darwin-x64": "573eb872b1965fe6b8575611f42336fd4dda436e749196e4a1373cffd2939bde",
|
|
11
|
+
"bot-runner-darwin-arm64": "853183f7da381f9b831aef9824c69563a732205c61d300fad908cd0f502a73e0",
|
|
12
|
+
"bot-runner-viewer-win-x64.tgz": "e39a36cdf92b9223aa7c9610f8231afc7a577e8ed007bc3571895bd945baf3e1"
|
|
8
13
|
}
|
|
9
14
|
}
|
package/lib/jars.mjs
CHANGED
|
@@ -14,6 +14,9 @@ const RELEASES = 'https://github.com/Backas03/VitaminMCP-minecraft/releases/down
|
|
|
14
14
|
/** The jar this package launches, and the one it launches in turn. */
|
|
15
15
|
export const MCP_SERVER_JAR = 'mcp-server.jar';
|
|
16
16
|
|
|
17
|
+
/** The optional viewer archive fetched only when a world view is requested. */
|
|
18
|
+
export const VIEWER_ASSET = 'bot-runner-viewer-win-x64.tgz';
|
|
19
|
+
|
|
17
20
|
let checksumsPromise;
|
|
18
21
|
let manifestPromise;
|
|
19
22
|
|
package/lib/node.mjs
CHANGED
|
@@ -33,11 +33,21 @@ export function checkNode(node) {
|
|
|
33
33
|
/** The release asset name for the current SEA target. */
|
|
34
34
|
export function runnerAssetName(platform = process.platform, arch = process.arch) {
|
|
35
35
|
if (platform === 'win32' && arch === 'x64') return 'bot-runner-win-x64.exe';
|
|
36
|
+
if (platform === 'linux' && arch === 'x64') return 'bot-runner-linux-x64';
|
|
37
|
+
if (platform === 'linux' && arch === 'arm64') return 'bot-runner-linux-arm64';
|
|
38
|
+
if (platform === 'darwin' && arch === 'x64') return 'bot-runner-darwin-x64';
|
|
39
|
+
if (platform === 'darwin' && arch === 'arm64') return 'bot-runner-darwin-arm64';
|
|
40
|
+
throw new Error(`No VitaminMCP runner asset exists for ${platform}-${arch}.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The optional world-view asset for the platforms currently published. */
|
|
44
|
+
export function viewerAssetName(platform = process.platform, arch = process.arch) {
|
|
45
|
+
if (platform === 'win32' && arch === 'x64') return 'bot-runner-viewer-win-x64.tgz';
|
|
36
46
|
if (platform === 'linux' || platform === 'darwin') {
|
|
37
47
|
throw new Error(
|
|
38
|
-
`
|
|
39
|
-
+ '
|
|
48
|
+
`Optional world viewer assets for ${platform}-${arch} are planned but not released yet. `
|
|
49
|
+
+ 'Set VITAMINMCP_VIEWER_PATH to a local prismarine-viewer sidecar.',
|
|
40
50
|
);
|
|
41
51
|
}
|
|
42
|
-
throw new Error(`No VitaminMCP
|
|
52
|
+
throw new Error(`No VitaminMCP world viewer asset exists for ${platform}-${arch}.`);
|
|
43
53
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy bridge from the npm launcher's pinned asset fetcher to the Node runner's viewer import.
|
|
3
|
+
*
|
|
4
|
+
* This module is imported only when `bot_view` asks for a world view. The normal runner startup
|
|
5
|
+
* therefore never downloads or extracts the optional viewer archive.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
assetCacheDirectory,
|
|
15
|
+
ensureAsset,
|
|
16
|
+
} from './jars.mjs';
|
|
17
|
+
|
|
18
|
+
const version = process.env.VITAMINMCP_VERSION;
|
|
19
|
+
if (!version) throw new Error('VITAMINMCP_VERSION is required to load the optional viewer asset.');
|
|
20
|
+
const asset = process.env.VITAMINMCP_VIEWER_ASSET;
|
|
21
|
+
if (!asset) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'No packaged world viewer asset exists for this platform. '
|
|
24
|
+
+ 'Set VITAMINMCP_VIEWER_PATH to a local prismarine-viewer sidecar.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const archive = await ensureAsset(version, asset, {
|
|
29
|
+
log: (message) => process.stderr.write(`[vitaminmcp] ${message}\n`),
|
|
30
|
+
});
|
|
31
|
+
const extractionRoot = path.join(assetCacheDirectory(version), 'viewer');
|
|
32
|
+
const entry = path.join(extractionRoot, 'package', 'node_modules', 'prismarine-viewer', 'index.js');
|
|
33
|
+
const marker = path.join(extractionRoot, '.ready');
|
|
34
|
+
|
|
35
|
+
if (!(await exists(marker)) || !(await exists(entry))) {
|
|
36
|
+
await fs.rm(extractionRoot, { recursive: true, force: true });
|
|
37
|
+
await fs.mkdir(extractionRoot, { recursive: true });
|
|
38
|
+
await extract(archive, extractionRoot);
|
|
39
|
+
if (!(await exists(entry))) {
|
|
40
|
+
throw new Error(`The viewer asset did not contain ${entry}.`);
|
|
41
|
+
}
|
|
42
|
+
await fs.writeFile(marker, 'ready\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const viewerModule = await import(pathToFileURL(entry).href);
|
|
46
|
+
const viewer = viewerModule.default ?? viewerModule;
|
|
47
|
+
|
|
48
|
+
export default viewer;
|
|
49
|
+
export const mineflayer = viewer.mineflayer;
|
|
50
|
+
export const supportedVersions = viewer.supportedVersions;
|
|
51
|
+
|
|
52
|
+
async function exists(file) {
|
|
53
|
+
try {
|
|
54
|
+
await fs.access(file);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extract(file, directory) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const child = spawn('tar', ['-xzf', file, '-C', directory], {
|
|
64
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
65
|
+
});
|
|
66
|
+
let error = '';
|
|
67
|
+
child.stderr.on('data', (chunk) => { error += chunk; });
|
|
68
|
+
child.once('error', reject);
|
|
69
|
+
child.once('close', (code) => {
|
|
70
|
+
if (code === 0) resolve();
|
|
71
|
+
else reject(new Error(`Could not extract viewer asset (tar exited ${code}): ${error}`));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
package/package.json
CHANGED
package/runner/src/actions.mjs
CHANGED
|
@@ -51,6 +51,12 @@ const DIG_ACK_TIMEOUT_MILLIS = 1500;
|
|
|
51
51
|
/** How long the resulting block change gets to come back after the confirmation. */
|
|
52
52
|
const DIG_SETTLE_MILLIS = 300;
|
|
53
53
|
|
|
54
|
+
/** How long a client gets to receive the block before an interaction is sent. */
|
|
55
|
+
const BLOCK_KNOWN_TIMEOUT_MILLIS = 15_000;
|
|
56
|
+
|
|
57
|
+
/** One client tick for a block update already queued by a preceding server command. */
|
|
58
|
+
const BLOCK_UPDATE_SETTLE_MILLIS = 50;
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
61
|
* Breaks a block, and says what became of the attempt.
|
|
56
62
|
*
|
|
@@ -158,7 +164,7 @@ export function chat(bot, name, message) {
|
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/** Right-clicks a block — which is how a container or a plugin menu gets opened. */
|
|
161
|
-
export function useBlock(bot, name, x, y, z, face) {
|
|
167
|
+
export async function useBlock(bot, name, x, y, z, face) {
|
|
162
168
|
requireInWorld(bot, name);
|
|
163
169
|
|
|
164
170
|
const direction = !face || !face.trim() ? FACES.up : FACES[face.trim().toLowerCase()];
|
|
@@ -168,6 +174,9 @@ export function useBlock(bot, name, x, y, z, face) {
|
|
|
168
174
|
);
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
const location = new Vec3(x, y, z);
|
|
178
|
+
await blockKnown(bot, name, location);
|
|
179
|
+
|
|
171
180
|
bot._client.write('block_place', {
|
|
172
181
|
hand: MAIN_HAND,
|
|
173
182
|
location: { x, y, z },
|
|
@@ -181,6 +190,33 @@ export function useBlock(bot, name, x, y, z, face) {
|
|
|
181
190
|
});
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Waits until the client has a block record for an interaction target.
|
|
195
|
+
*
|
|
196
|
+
* <p>A console command can change a block before its block update reaches the bot. Sending the
|
|
197
|
+
* interaction in that gap is legal at the protocol level, but the client has not loaded the target
|
|
198
|
+
* yet and Paper may drop the window-opening packet. This was the intermittent compatibility
|
|
199
|
+
* failure on the otherwise deterministic chest check.
|
|
200
|
+
*/
|
|
201
|
+
async function blockKnown(bot, name, location) {
|
|
202
|
+
const deadline = Date.now() + BLOCK_KNOWN_TIMEOUT_MILLIS;
|
|
203
|
+
while (Date.now() < deadline) {
|
|
204
|
+
if (bot.blockAt(location)) {
|
|
205
|
+
// A command_exec that changed this block has already completed on the server, but the
|
|
206
|
+
// corresponding packet may still be queued on the bot socket. Give that one client tick
|
|
207
|
+
// a chance to settle before sending the interaction packet.
|
|
208
|
+
await delay(BLOCK_UPDATE_SETTLE_MILLIS);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await delay(50);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Bot ${name} was not told about the block at ${location.x} ${location.y} ${location.z} within `
|
|
216
|
+
+ `${BLOCK_KNOWN_TIMEOUT_MILLIS}ms`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
184
220
|
/** Right-clicks the nearest entity to a point, and returns which one it was. */
|
|
185
221
|
export function useEntity(bot, name, x, y, z, radius, type) {
|
|
186
222
|
requireInWorld(bot, name);
|
package/runner/src/bots.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { configurePathfinder, loadPathfinder, moveTo } from './movement.mjs';
|
|
|
3
3
|
import { reachable } from './movement.mjs';
|
|
4
4
|
import { stopAllViews, stopView as stopBotView, view as startView } from './viewer.mjs';
|
|
5
5
|
|
|
6
|
-
import { collect } from './clientview.mjs';
|
|
6
|
+
import { collect, forget } from './clientview.mjs';
|
|
7
7
|
import { addressField, identity } from './identity.mjs';
|
|
8
8
|
|
|
9
9
|
/** How long a bot has to get from a socket to standing in the world. */
|
|
@@ -58,7 +58,7 @@ export class BotRegistry {
|
|
|
58
58
|
// Before waiting to join, not after: messages are events, and a plugin that greets or refuses
|
|
59
59
|
// on join says so within a tick of the bot arriving. Attaching afterwards loses exactly the
|
|
60
60
|
// messages most worth having.
|
|
61
|
-
collect(bot);
|
|
61
|
+
collect(bot, name);
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
64
|
await joined(bot, name);
|
|
@@ -108,6 +108,7 @@ export class BotRegistry {
|
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
this.#bots.delete(name);
|
|
111
|
+
forget(name);
|
|
111
112
|
stopBotView(bot);
|
|
112
113
|
quietly(() => bot.quit());
|
|
113
114
|
}
|
|
@@ -119,7 +120,8 @@ export class BotRegistry {
|
|
|
119
120
|
/** Disconnects every bot. */
|
|
120
121
|
shutdown() {
|
|
121
122
|
stopAllViews();
|
|
122
|
-
for (const bot of this.#bots
|
|
123
|
+
for (const [name, bot] of this.#bots) {
|
|
124
|
+
forget(name);
|
|
123
125
|
quietly(() => bot.quit());
|
|
124
126
|
}
|
|
125
127
|
this.#bots.clear();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
1
3
|
import { menu } from './actions.mjs';
|
|
2
4
|
import { plainText, stripLegacyCodes, untag } from './text.mjs';
|
|
3
5
|
|
|
@@ -20,27 +22,63 @@ const MAX_MESSAGES = 100;
|
|
|
20
22
|
|
|
21
23
|
const state = new WeakMap();
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The active message stream for each bot name.
|
|
27
|
+
*
|
|
28
|
+
* Each client connection gets a random id, so a cursor cannot be mistaken for one from another
|
|
29
|
+
* session, runner process or same-named replacement bot.
|
|
30
|
+
*/
|
|
31
|
+
const messageStreamsByName = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drops the message stream a despawned bot left behind.
|
|
35
|
+
*
|
|
36
|
+
* The stream outlives the bot on purpose while a replacement is connecting, but a bot that is gone
|
|
37
|
+
* for good would otherwise keep its entry for the lifetime of the runner. The next bot of the same
|
|
38
|
+
* name gets a new id either way, so nothing that survives here is ever reused.
|
|
39
|
+
*/
|
|
40
|
+
export function forget(name) {
|
|
41
|
+
const stream = messageStreamsByName.get(name);
|
|
42
|
+
if (stream) stream.activeCollector = null;
|
|
43
|
+
messageStreamsByName.delete(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
/**
|
|
24
47
|
* Starts collecting what the server tells this bot.
|
|
25
48
|
*
|
|
26
49
|
* Must be called as soon as the bot exists: this is all events, so anything said before the
|
|
27
50
|
* listeners attach is gone, and a plugin greets or refuses within a tick of the bot arriving.
|
|
28
51
|
*/
|
|
29
|
-
export function collect(bot) {
|
|
52
|
+
export function collect(bot, name) {
|
|
53
|
+
const previous = messageStreamsByName.get(name);
|
|
54
|
+
if (previous) previous.activeCollector = null;
|
|
55
|
+
|
|
56
|
+
const stream = { id: randomUUID(), nextSequence: 0, activeCollector: null };
|
|
57
|
+
|
|
30
58
|
const own = {
|
|
31
59
|
messages: [],
|
|
32
60
|
bossBars: new Map(),
|
|
33
61
|
objectiveTitles: new Map(),
|
|
34
62
|
objectiveScores: new Map(),
|
|
35
63
|
sidebar: null,
|
|
64
|
+
messageStream: stream,
|
|
36
65
|
};
|
|
66
|
+
stream.activeCollector = own;
|
|
67
|
+
messageStreamsByName.set(name, stream);
|
|
37
68
|
state.set(bot, own);
|
|
38
69
|
|
|
39
70
|
const remember = (text) => {
|
|
71
|
+
// Closing a replaced bot is asynchronous. Ignore anything its socket delivers after the new
|
|
72
|
+
// bot took over, otherwise an invisible message would consume a sequence and look like loss.
|
|
73
|
+
if (stream.activeCollector !== own) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
40
76
|
if (text == null || String(text).trim() === '') {
|
|
41
77
|
return;
|
|
42
78
|
}
|
|
43
|
-
|
|
79
|
+
const sequence = stream.nextSequence;
|
|
80
|
+
stream.nextSequence += 1;
|
|
81
|
+
own.messages.push({ sequence, timestamp: Date.now(), text: String(text) });
|
|
44
82
|
while (own.messages.length > MAX_MESSAGES) {
|
|
45
83
|
own.messages.shift();
|
|
46
84
|
}
|
|
@@ -90,10 +128,13 @@ export function collect(bot) {
|
|
|
90
128
|
/** Everything the client was told, in the shape `RunnerDispatch` writes. */
|
|
91
129
|
export function inspect(bot, name) {
|
|
92
130
|
const own = state.get(bot) ?? { messages: [], bossBars: new Map() };
|
|
131
|
+
const stream = own.messageStream ?? messageStreamsByName.get(name);
|
|
93
132
|
return {
|
|
94
133
|
menu: menu(bot, name),
|
|
95
134
|
items: menuItems(bot),
|
|
96
135
|
messages: [...own.messages],
|
|
136
|
+
nextMessageSequence: stream?.nextSequence ?? 0,
|
|
137
|
+
messageStreamId: stream?.id ?? '',
|
|
97
138
|
bossBars: [...own.bossBars.values()],
|
|
98
139
|
scoreboard: sidebarOf(own),
|
|
99
140
|
health: Number.isFinite(bot.health) ? bot.health : null,
|
package/runner/src/dispatch.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export class Dispatch {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
case protocol.USE: {
|
|
89
|
-
actions.useBlock(
|
|
89
|
+
await actions.useBlock(
|
|
90
90
|
this.#bots.require(command[1]),
|
|
91
91
|
command[1],
|
|
92
92
|
Number(command[2]),
|
|
@@ -208,7 +208,7 @@ export class Dispatch {
|
|
|
208
208
|
String(open === null ? -1 : open.containerId),
|
|
209
209
|
open === null ? '' : protocol.sanitize(open.title),
|
|
210
210
|
items(view.items),
|
|
211
|
-
|
|
211
|
+
messageList(view.messages),
|
|
212
212
|
bossBarList(view.bossBars),
|
|
213
213
|
board === null ? '' : protocol.sanitize(board.title),
|
|
214
214
|
board === null ? '' : recordList(board.lines),
|
|
@@ -218,6 +218,8 @@ export class Dispatch {
|
|
|
218
218
|
view.totalExperience == null ? '' : String(view.totalExperience),
|
|
219
219
|
view.experienceProgress == null ? '' : protocol.javaFloat(view.experienceProgress),
|
|
220
220
|
recordList(view.effects),
|
|
221
|
+
String(view.nextMessageSequence),
|
|
222
|
+
protocol.sanitize(view.messageStreamId),
|
|
221
223
|
);
|
|
222
224
|
}
|
|
223
225
|
|
|
@@ -264,6 +266,17 @@ function items(list) {
|
|
|
264
266
|
.join(protocol.RECORD_SEPARATOR);
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/** `sequence ␟ timestamp ␟ text`, oldest first, joined by ␞. */
|
|
270
|
+
function messageList(messages) {
|
|
271
|
+
return messages
|
|
272
|
+
.map((message) => [
|
|
273
|
+
message.sequence,
|
|
274
|
+
message.timestamp,
|
|
275
|
+
protocol.sanitize(message.text),
|
|
276
|
+
].join(protocol.UNIT_SEPARATOR))
|
|
277
|
+
.join(protocol.RECORD_SEPARATOR);
|
|
278
|
+
}
|
|
279
|
+
|
|
267
280
|
/** `title ␟ progress ␟ colour`, one record each. */
|
|
268
281
|
function bossBarList(bars) {
|
|
269
282
|
return bars
|