Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions lib/plugins/creative.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ function inject (bot) {

const creativeSlotsUpdates = []

// FIFO: the server answers stats requests in packet-arrival order, so each
// statistics packet settles the oldest waiter, and everything written
// before that waiter's request is processed by then.
const statsConfirms = []
bot._client.on('statistics', () => statsConfirms.shift()?.())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

statsConfirms.shift()?.()
What does this do?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed let's do something less weird here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote it, should make more sense now


function confirmServerProcessed (timeoutMs) {
return new Promise((resolve) => {
// Timing out resolves as success: a server that never answers stats
// degrades to the previous fixed-wait behavior, never a hang.
const timer = setTimeout(() => {
const i = statsConfirms.indexOf(settle)
if (i !== -1) statsConfirms.splice(i, 1)
resolve()
}, timeoutMs)
function settle () {
clearTimeout(timer)
resolve()
}
statsConfirms.push(settle)
bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 1 } : { actionId: 1 })
})
}

// WARN: This method should not be called twice on the same slot before first promise succeeds
async function setInventorySlot (slot, item, waitTimeout = 400) {
assert(slot >= 0 && slot <= 44)
Expand All @@ -38,7 +62,9 @@ function inject (bot) {
// No ack
bot._setSlot(slot, item)
if (waitTimeout === 0) return // no wait
// allow some time to see if server rejects
// A rejection is a set_slot correction the server sends while
// processing our packet on its main thread, so a stats round trip on
// the same ordered connection is proof the rejection window has passed.
return new Promise((resolve, reject) => {
function updateSlot (oldItem, newItem) {
if (newItem.itemId !== item.itemId) {
Expand All @@ -47,11 +73,11 @@ function inject (bot) {
}
}
bot.inventory.once(`updateSlot:${slot}`, updateSlot)
setTimeout(() => {
confirmServerProcessed(waitTimeout).then(() => {
bot.inventory.off(`updateSlot:${slot}`, updateSlot)
creativeSlotsUpdates[slot] = false
resolve()
}, waitTimeout)
})
})
}

Expand Down
11 changes: 11 additions & 0 deletions test/externalTests/creative.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,15 @@ module.exports = () => async (bot) => {
assert.strictEqual(bot.inventory.slots.filter(item => item).length, 9)
await bot.creative.clearInventory()
assert.strictEqual(bot.inventory.slots.filter(item => item).length, 0)
// Sets are server-confirmed, not fixed 400ms/call silence windows: three
// sequential sets must finish in well under 3x400ms.
if (bot.supportFeature('noAckOnCreateSetSlotPacket')) {
const before = Date.now()
for (let i = 0; i < 3; i++) {
await bot.creative.setInventorySlot(SLOT, new Item(5 + i, 1, 0))
}
const elapsed = Date.now() - before
assert.ok(elapsed < 1000, `3 sequential slot sets took ${elapsed}ms`)
await bot.creative.clearSlot(SLOT)
}
}
44 changes: 41 additions & 3 deletions test/externalTests/plugins/testCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,58 @@ function inject (bot, wrap) {

async function resetBlocksToSuperflat () {
const groundY = 4
const center = bot.entity.position.floored()
for (let y = groundY + 4; y >= groundY - 1; y--) {
const realY = y + bot.test.groundY - 4
bot.chat(`/fill ~-5 ${realY} ~-5 ~5 ${realY} ~5 ` + layerNames[y])
}
// The fills are fire-and-forget; a marker chat message on the same
// ordered connection confirms they have executed and their block updates
// have already arrived, without assuming how long a server tick takes.
// The marker echo only proves the fills executed: command feedback is
// sent immediately while block changes flush at tick end, so the client
// can still hold pre-fill blocks after the echo.
const marker = 'superflat-reset-done'
const echo = onceWithCleanup(bot, 'messagestr', {
timeout: 5000,
checkCondition: (message) => message.includes(marker)
})
bot.chat(marker)
await echo
const staleBlock = () => {
for (let y = groundY + 4; y >= groundY - 1; y--) {
const realY = y + bot.test.groundY - 4
const want = layerNames[y]
if (!want) continue
for (let dx = -5; dx <= 5; dx++) {
for (let dz = -5; dz <= 5; dz++) {
const block = bot.blockAt(new Vec3(center.x + dx, realY, center.z + dz))
if (!block || block.name !== want) {
return { pos: `${center.x + dx} ${realY} ${center.z + dz}`, want, desc: `${center.x + dx},${realY},${center.z + dz} is ${block?.name ?? 'unloaded'}, expected ${want}` }
}
}
}
}
return null
}
const deadline = Date.now() + 5000
let resyncAt = Date.now() + 1500
let stale
while ((stale = staleBlock()) !== null) {
if (Date.now() > deadline) throw new Error(`world not reset: ${stale.desc}`)
if (Date.now() > resyncAt) {
// A test can leave the client desynced on a block the server no
// longer has (e.g. a sign destroyed in the tick of its own editor
// interact), and then no correction ever comes. Two real changes
// force the server to rebroadcast the block either way.
bot.chat(`/setblock ${stale.pos} bedrock`)
bot.chat(`/setblock ${stale.pos} ${stale.want}`)
resyncAt = Date.now() + 1500
}
// Corrections arrive as block updates or, past 64 changed blocks per
// section, as a chunk resend, so wait on whichever comes first.
await Promise.race([
onceWithCleanup(bot.world, 'blockUpdate', { timeout: 500 }),
onceWithCleanup(bot.world, 'chunkColumnLoad', { timeout: 500 })
]).catch(() => {})
}
}

async function placeBlock (slot, position) {
Expand Down
11 changes: 8 additions & 3 deletions test/externalTests/sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ module.exports = () => async (bot) => {
assert.strictEqual(sign.signText.trimEnd(), '1\n2\n3')

if (sign.blockEntity) {
// Check block update
bot.activateBlock(sign)
assert.notStrictEqual(sign.blockEntity, undefined)
// Awaited so the interact is at least on the wire before the next
// test's reset; on 1.20+ (editable signs) it can still share a tick
// with the reset fills and desync the client, which the reset's
// resync path repairs.
resolve(bot.activateBlock(sign).then(() => {
assert.notStrictEqual(sign.blockEntity, undefined)
}))
return
}

resolve()
Expand Down
Loading