vitaminmcp 2.1.1 → 2.2.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/checksums.json +3 -3
- package/package.json +1 -1
- package/runner/src/actions.mjs +72 -3
- package/runner/src/bots.mjs +34 -0
- package/runner/src/dispatch.mjs +3 -2
package/checksums.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.
|
|
2
|
+
"version": "2.2.0",
|
|
3
3
|
"jars": {
|
|
4
|
-
"mcp-server.jar": "
|
|
4
|
+
"mcp-server.jar": "27f1543ced4e59ed6501aa4238eab76473cbf4ff1612a4622b5bb27455447685"
|
|
5
5
|
},
|
|
6
6
|
"assets": {
|
|
7
|
-
"bot-runner-win-x64.exe": "
|
|
7
|
+
"bot-runner-win-x64.exe": "82438774fc5b43e1a348daade6cc22e5be1e97f39cd63ee8b8b65afa242b2109"
|
|
8
8
|
}
|
|
9
9
|
}
|
package/package.json
CHANGED
package/runner/src/actions.mjs
CHANGED
|
@@ -45,24 +45,93 @@ function requireInWorld(bot, name) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/** How long the server gets to confirm a dig before it counts as never having arrived. */
|
|
49
|
+
const DIG_ACK_TIMEOUT_MILLIS = 1500;
|
|
50
|
+
|
|
51
|
+
/** How long the resulting block change gets to come back after the confirmation. */
|
|
52
|
+
const DIG_SETTLE_MILLIS = 300;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Breaks a block, and says what became of the attempt.
|
|
56
|
+
*
|
|
57
|
+
* <p>This used to write two packets and answer `sent`, which made a dig the server cancelled and
|
|
58
|
+
* a dig the server never received the same observation — and a plugin that cancels
|
|
59
|
+
* BlockBreakEvent silently is exactly the thing someone reaches for this tool to find. A whole
|
|
60
|
+
* dogfooding round went on telling those apart by hand (dogfood/JOURNAL.md, 2026-08-23).
|
|
61
|
+
*
|
|
62
|
+
* <p>The server settles it for us. Every block action carries a sequence number, and the server
|
|
63
|
+
* answers with the highest one it has resolved — that is what the field is for, so the client
|
|
64
|
+
* knows when to stop predicting and accept what it is told. An acknowledgement means the dig
|
|
65
|
+
* reached the world and was dealt with; the block then says whether it was allowed.
|
|
66
|
+
*/
|
|
67
|
+
export async function breakBlock(bot, name, x, y, z) {
|
|
49
68
|
requireInWorld(bot, name);
|
|
50
69
|
const location = { x, y, z };
|
|
70
|
+
const at = new Vec3(x, y, z);
|
|
71
|
+
|
|
72
|
+
const before = bot.blockAt(at)?.name ?? null;
|
|
73
|
+
if (before === null) {
|
|
74
|
+
// Not a refusal and not a failure to send: this client has never been told what is there,
|
|
75
|
+
// which usually means it has only just joined and its chunks have not arrived.
|
|
76
|
+
return `the bot's client has no block at ${x}, ${y}, ${z} yet, so nothing was dug`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const started = nextSequence(bot);
|
|
80
|
+
const finished = nextSequence(bot);
|
|
81
|
+
|
|
82
|
+
// Registered before the packets go out. The acknowledgement can come back inside the same tick.
|
|
83
|
+
const confirmed = acknowledgement(bot, finished);
|
|
51
84
|
|
|
52
85
|
bot._client.write('block_dig', {
|
|
53
86
|
status: START_DIGGING,
|
|
54
87
|
location,
|
|
55
88
|
face: FACES.up,
|
|
56
|
-
sequence:
|
|
89
|
+
sequence: started,
|
|
57
90
|
});
|
|
58
91
|
bot._client.write('block_dig', {
|
|
59
92
|
status: FINISH_DIGGING,
|
|
60
93
|
location,
|
|
61
94
|
face: FACES.up,
|
|
62
|
-
sequence:
|
|
95
|
+
sequence: finished,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!await confirmed) {
|
|
99
|
+
return `the server did not acknowledge the dig within ${DIG_ACK_TIMEOUT_MILLIS}ms, `
|
|
100
|
+
+ 'so it never reached the world';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await delay(DIG_SETTLE_MILLIS);
|
|
104
|
+
const after = bot.blockAt(at)?.name ?? null;
|
|
105
|
+
|
|
106
|
+
return after === before
|
|
107
|
+
? `the server acknowledged the dig and left ${before} in place, so something refused it`
|
|
108
|
+
: `broke ${before}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Resolves true once the server says it has resolved everything up to `sequence`. */
|
|
112
|
+
function acknowledgement(bot, sequence) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
const finish = (value) => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
bot._client.removeListener('acknowledge_player_digging', onAcknowledged);
|
|
117
|
+
resolve(value);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const onAcknowledged = (packet) => {
|
|
121
|
+
if (packet?.sequenceId >= sequence) {
|
|
122
|
+
finish(true);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const timer = setTimeout(() => finish(false), DIG_ACK_TIMEOUT_MILLIS);
|
|
127
|
+
bot._client.on('acknowledge_player_digging', onAcknowledged);
|
|
63
128
|
});
|
|
64
129
|
}
|
|
65
130
|
|
|
131
|
+
function delay(millis) {
|
|
132
|
+
return new Promise((resolve) => setTimeout(resolve, millis));
|
|
133
|
+
}
|
|
134
|
+
|
|
66
135
|
/** Runs a command as the bot. The leading slash is not part of the packet. */
|
|
67
136
|
export function command(bot, name, line) {
|
|
68
137
|
requireInWorld(bot, name);
|
package/runner/src/bots.mjs
CHANGED
|
@@ -16,6 +16,9 @@ const SETTLE_TIMEOUT_MILLIS = 15_000;
|
|
|
16
16
|
const SETTLED_CHECKS = 5;
|
|
17
17
|
const SETTLE_POLL_MILLIS = 50;
|
|
18
18
|
|
|
19
|
+
/** How long the client gets to be sent the chunk it is standing in. */
|
|
20
|
+
const WORLD_TIMEOUT_MILLIS = 15_000;
|
|
21
|
+
|
|
19
22
|
/** The bots this runner holds, and the server they all connect to. */
|
|
20
23
|
export class BotRegistry {
|
|
21
24
|
#host;
|
|
@@ -73,6 +76,7 @@ export class BotRegistry {
|
|
|
73
76
|
this.#bots.set(name, bot);
|
|
74
77
|
configurePathfinder(bot);
|
|
75
78
|
await settle(bot, name);
|
|
79
|
+
await worldKnown(bot, name);
|
|
76
80
|
return position(bot);
|
|
77
81
|
}
|
|
78
82
|
|
|
@@ -186,6 +190,36 @@ async function settle(bot, name) {
|
|
|
186
190
|
);
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Waits until the bot's client knows the world it is standing in.
|
|
195
|
+
*
|
|
196
|
+
* `spawn` fires on the position packet, which can arrive before the chunk does. A bot that acts in
|
|
197
|
+
* that gap sends block actions against blocks it has never been told about, and the server answers
|
|
198
|
+
* with nothing at all — indistinguishable, from the caller's side, from a plugin cancelling the
|
|
199
|
+
* action silently. A dogfooding round spent most of itself on that ambiguity
|
|
200
|
+
* (dogfood/JOURNAL.md, 2026-08-23).
|
|
201
|
+
*
|
|
202
|
+
* This closes the client half. The server half — a plugin, or Paper itself, dropping interactions
|
|
203
|
+
* from a player who has only just joined — cannot be waited out from here, and is why
|
|
204
|
+
* `breakBlock` reports whether the server acknowledged the dig.
|
|
205
|
+
*/
|
|
206
|
+
async function worldKnown(bot, name) {
|
|
207
|
+
const deadline = Date.now() + WORLD_TIMEOUT_MILLIS;
|
|
208
|
+
|
|
209
|
+
while (Date.now() < deadline) {
|
|
210
|
+
const at = bot.entity?.position;
|
|
211
|
+
if (at && bot.blockAt(at.offset(0, -1, 0))) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await delay(SETTLE_POLL_MILLIS);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Bot ${name} joined but its client was never sent the world around it within `
|
|
219
|
+
+ `${WORLD_TIMEOUT_MILLIS}ms`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
189
223
|
/** Resolves when the bot is in the world; rejects on a kick, an error, or the timeout. */
|
|
190
224
|
function joined(bot, name) {
|
|
191
225
|
return new Promise((resolve, reject) => {
|
package/runner/src/dispatch.mjs
CHANGED
|
@@ -64,14 +64,15 @@ export class Dispatch {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
case protocol.BREAK: {
|
|
67
|
-
|
|
67
|
+
// Carries what became of the dig, which is the whole point of waiting for it.
|
|
68
|
+
const outcome = await actions.breakBlock(
|
|
68
69
|
this.#bots.require(command[1]),
|
|
69
70
|
command[1],
|
|
70
71
|
Number(command[2]),
|
|
71
72
|
Number(command[3]),
|
|
72
73
|
Number(command[4]),
|
|
73
74
|
);
|
|
74
|
-
return
|
|
75
|
+
return protocol.encode(protocol.OK, verb, outcome);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
case protocol.COMMAND: {
|