Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 7 additions & 7 deletions lib/plugins/book.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ function inject (bot) {
if (bot.supportFeature('editBookPacketUsesNbt')) { // 1.13 - 1.17
editBook = (book, pages, title, slot, signing = false, hand = 0) => {
bot._client.write('edit_book', {
hand: slot,
pages,
title
new_book: Item.toNotch(book),
signing,
hand
})
}
} else { // 1.18+
} else { // 1.17.1+
editBook = (book, pages, title, slot, signing = false, hand = 0) => {
bot._client.write('edit_book', {
new_book: Item.toNotch(book),
signing,
hand
hand: slot,
pages,
title
})
}
}
Expand Down
84 changes: 84 additions & 0 deletions test/externalTests/editBook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const assert = require('assert')

module.exports = () => async (bot) => {
const Item = require('prismarine-item')(bot.registry)
const useComponents = bot.registry.supportFeature('itemsWithComponents')

// Helper: extract pages from a book item regardless of NBT vs component format.
function getPages (book) {
if (useComponents && book.componentMap) {
// 1.20.5+: pages live inside writable_book_content or written_book_content component
const comp =
book.componentMap.get('writable_book_content') ||
book.componentMap.get('written_book_content')
if (comp && comp.data && comp.data.pages) {
// Component pages are objects with a {raw: string} shape or plain strings
return comp.data.pages.map(p => (typeof p === 'string' ? p : (p.raw || p.text || JSON.stringify(p))))
}
return null
}
// Legacy NBT path
if (book.nbt && book.nbt.value && book.nbt.value.pages) {
return book.nbt.value.pages.value.value
}
return null
}

// Helper: extract a top-level string field (title / author) from NBT or components.
function getStringField (book, nbtKey, componentName, componentDataKey) {
if (useComponents && book.componentMap) {
const comp = book.componentMap.get(componentName)
if (comp && comp.data) {
const val = comp.data[componentDataKey]
return typeof val === 'string' ? val : (val && val.raw) || null
}
return null
}
if (book.nbt && book.nbt.value && book.nbt.value[nbtKey]) {
return book.nbt.value[nbtKey].value
}
return null
}

// Place book in a non-hotbar slot (slot 18, inside main inventory) to
// exercise the moveToQuickBar code path inside bot.writeBook / bot.signBook.
const bookSlot = 18
await bot.test.setInventorySlot(bookSlot, new Item(bot.registry.itemsByName.writable_book.id, 1, 0))

const pages = ['Page one content', 'Page two content']

// Write pages into the book — this sends the edit_book packet whose field
// assignments were swapped between NBT / non-NBT paths (the fix under test).
await bot.writeBook(bookSlot, pages)

let book = bot.inventory.slots[bookSlot]
assert.ok(book, 'book should still be in the inventory after writing')
assert.strictEqual(book.type, bot.registry.itemsByName.writable_book.id,
'book should still be a writable_book after writeBook')

const writtenPages = getPages(book)
// The server may or may not echo pages back for a writable_book; only assert
// when the data is actually present to avoid false negatives.
if (writtenPages) {
assert.deepStrictEqual(writtenPages, pages, 'written pages should match input')
}

// Now sign the book — this also sends the edit_book packet with signing=true.
const title = 'Test Book'
await bot.signBook(bookSlot, pages, bot.username, title)

book = bot.inventory.slots[bookSlot]
assert.ok(book, 'book should still be in the inventory after signing')
assert.strictEqual(book.type, bot.registry.itemsByName.written_book.id,
'book should become a written_book after signing')

const gotTitle = getStringField(book, 'title', 'written_book_content', 'title')
const gotAuthor = getStringField(book, 'author', 'written_book_content', 'author')
// After signing, the server should always report title and author.
if (gotTitle !== null) {
assert.strictEqual(gotTitle, title, 'title should match')
}
if (gotAuthor !== null) {
assert.strictEqual(gotAuthor, bot.username, 'author should match')
}
}
65 changes: 65 additions & 0 deletions test/internalTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,71 @@ for (const supportedVersion of mineflayer.testedVersions) {
})
})

describe('editBook', () => {
it('sends correct edit_book packet fields', function (done) {
if (!registry.supportFeature('hasEditBookPacket')) {
this.skip()
return
}
const Item = require('prismarine-item')(registry)
const usesNbt = registry.supportFeature('editBookPacketUsesNbt')
const pages = ['Page 1', 'Page 2']

server.on('playerJoin', (client) => {
client.write('login', bot.test.generateLoginPacket())

// Place a writable_book in hotbar slot 36 (first hotbar slot)
const bookItem = new Item(registry.itemsByName.writable_book.id, 1, 0)
client.write('set_slot', {
windowId: 0,
slot: 36,
item: Item.toNotch(bookItem),
stateId: undefined
})

// Listen for the edit_book packet from the bot
client.on('edit_book', (packet) => {
try {
if (usesNbt) {
// 1.13 - 1.16.5: should send new_book (item NBT), signing, hand
assert.ok(packet.new_book !== undefined, 'edit_book packet should have new_book field for NBT versions')
assert.strictEqual(packet.signing, false)
assert.strictEqual(packet.hand, 0)
} else {
// 1.17.1+: should send hand (slot), pages, title
assert.strictEqual(packet.hand, 0)
assert.ok(Array.isArray(packet.pages), 'edit_book packet should have pages array for non-NBT versions')
assert.deepStrictEqual(packet.pages, pages)
}
// Send set_slot to complete the writeBook flow
const writtenBook = new Item(registry.itemsByName.writable_book.id, 1, 0)
writtenBook.nbt = {
type: 'compound',
name: '',
value: {
pages: { type: 'list', value: { type: 'string', value: pages } }
}
}
client.write('set_slot', {
windowId: 0,
slot: 36,
item: Item.toNotch(writtenBook),
stateId: undefined
})
done()
} catch (err) {
done(err)
}
})

// Wait a tick for inventory to be set, then call writeBook
setTimeout(() => {
bot.writeBook(36, pages).catch(done)
}, 100)
})
})
})

describe('tablist', () => {
it('handles newlines in header and footer', (done) => {
const HEADER = 'asd\ndsa'
Expand Down
Loading