diff --git a/lib/plugins/book.js b/lib/plugins/book.js index 9c1616979..9b71d7dea 100644 --- a/lib/plugins/book.js +++ b/lib/plugins/book.js @@ -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 }) } } diff --git a/test/externalTests/editBook.js b/test/externalTests/editBook.js new file mode 100644 index 000000000..e6ded1ac4 --- /dev/null +++ b/test/externalTests/editBook.js @@ -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') + } +} diff --git a/test/internalTest.js b/test/internalTest.js index c46414b7f..9aa8ac1e6 100644 --- a/test/internalTest.js +++ b/test/internalTest.js @@ -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'