Skip to content

Commit

Permalink
Add ability to use an anvil fully (#1666)
Browse files Browse the repository at this point in the history
* Add ability to use an anvil fully

* create example code

* fix pr

* Update api.md

* Update blocks.js

* shorten matchWindowType

* rename example

* pass an item not a slot to anvil funcs

* make working test

* simplify anvil.js

* vastly improve test

* make transfer work with nbt

* cleanup rename

* enable second test

* add many rename tests

* add to docs

* fix tests

* actually fix tests

* fix the customname

* use nbt in putsomething

* greatly improve tests

* make putaway match nbt so it doesnt stack 2 swords and break stuff

* fix anvil tests

* temp fix by await in test instead of implem

* set not add xp

* fix xp level for real

* stop closing anvil in combine and rename and improve resolve of putSelectedItemRange

* don't fail fast

* wait for experience in anvil methods

* add noWaiting in putAway for villager

either this is correct and client needs to simulate on itself
either this is just hiding a bug in villager implem

removed callbackify for this as it's not part of API and more convenient for opt arg

* add close to anvil example, delete example.js

* improve anvil example

* improve anvil example more and fix creative resolve

Co-authored-by: U9G <[email protected]>
Co-authored-by: Romain Beaumont <[email protected]>
  • Loading branch information
3 people authored Mar 11, 2021
1 parent a665909 commit 02769f3
Show file tree
Hide file tree
Showing 12 changed files with 479 additions and 46 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
matrix:
node-version: [14.x]
mcVersionIndex: [0, 1, 2, 3, 4, 5, 6, 7, 8]
fail-fast: false

steps:
- uses: actions/checkout@v2
Expand Down
33 changes: 27 additions & 6 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,16 +509,28 @@ This function also returns a `Promise`, with `void` as its argument upon complet

* `callback(err)`

#### enchantmentTable.putLapis(item, [callback])
#### enchantmentTable.putLapis(item, [callback])

This function also returns a `Promise`, with `void` as its argument upon completion.

* `callback(err)`

### mineflayer.Villager
### mineflayer.anvil

Extends windows.Window for villagers
See `bot.openVillager(villagerEntity)`.
Extends windows.Window for anvils
See `bot.openAnvil(anvilBlock)`.

#### anvil.combine(itemOne, itemTwo[, name, callback])

This function also returns a `Promise`, with `void` as its argument upon completion.

* `callback(err)` - in order to use callback, pass an empty string ('') for name

#### anvil.combine(item[, name, callback])

This function also returns a `Promise`, with `void` as its argument upon completion.

* `callback(err)`

#### villager "ready"

Expand Down Expand Up @@ -1637,6 +1649,10 @@ Deprecated. Same as `openContainer`
Returns a promise on an `EnchantmentTable` instance which represents the enchantment table
you are opening.

#### bot.openAnvil(anvilBlock)

Returns a promise on an `anvil` instance which represents the anvil you are opening.

#### bot.openVillager(villagerEntity)

Returns a promise on a `Villager` instance which represents the trading window you are opening.
Expand Down Expand Up @@ -1683,15 +1699,20 @@ This function also returns a `Promise`, with `void` as its argument upon complet

Click on the current window. See details at https://wiki.vg/Protocol#Click_Window

#### bot.putSelectedItemRange(start, end, window, slot, cb)
#### bot.putSelectedItemRange(start, end, window, slot, noWaiting)

This function also returns a `Promise`, with `void` as its argument upon completion.

Put the item at `slot` in the specified range.

#### bot.putAway(slot, cb)
`noWaiting` will not wait for items to be moved.
Can be useful in case the client is supposed to simulate without feedback from the server.

#### bot.putAway(slot, noWaiting)

This function also returns a `Promise`, with `void` as its argument upon completion.
`noWaiting` calls putSelectedItemRange with `noWaiting` option: it will not wait for items to be moved.
Can be useful in case the client is supposed to simulate without feedback from the server.

Put the item at `slot` in the inventory.

Expand Down
147 changes: 147 additions & 0 deletions examples/anvil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* This example demonstrates how to use anvils w/ mineflayer
* the options are: (<Option> are required, [<Option>] are optional)
* 1. "anvil combine <itemName1> <itemName2> [<name>]"
* 2. "anvil rename <itemName> <name>"
*
* to use this:
* /op anvilman
* /gamemode anvilman creative
* /xp set anvilman 999 levels
*
* Put an anvil near the bot
* Give him a sword and an enchanted book
* say list
* say xp
* say anvil combine diamond_sword enchanted_book
*/
const mineflayer = require('mineflayer')

if (process.argv.length < 4 || process.argv.length > 6) {
console.log('Usage : node use_anvil.js <host> <port> [<name>] [<password>]')
process.exit(1)
}

const bot = mineflayer.createBot({
host: process.argv[2],
port: parseInt(process.argv[3]),
username: process.argv[4] ? process.argv[4] : 'anvilman',
password: process.argv[5]
})

let mcData

bot.on('spawn', () => { mcData = require('minecraft-data')(bot.version) })

bot.on('chat', async (username, message) => {
const command = message.split(' ')

switch (true) {
case /^list$/.test(message):
sayItems()
break
case /^toss \w+$/.test(message):
// toss name
// ex: toss diamond
tossItem(command[1])
break
case /^xp$/.test(message):
bot.chat(bot.experience.level)
break
case /^gamemode$/.test(message):
bot.chat(bot.game.gameMode)
break
case /^anvil combine \w+ \w+$/.test(message): // anvil firstSlot secondSlot
combine(bot, command[2], command[3])
break
case /^anvil combine \w+ \w+ (.+)$/.test(message): // anvil firstSlot secondSlot name
combine(bot, command[2], command[3], command.slice(4).join(' '))
break
case /^anvil rename \w+ (.+)/.test((message)):
rename(bot, command[2], command.slice(3).join(' '))
break
}
})

function tossItem (name, amount) {
amount = parseInt(amount, 10)
const item = itemByName(name)
if (!item) {
bot.chat(`I have no ${name}`)
} else if (amount) {
bot.toss(item.type, null, amount, checkIfTossed)
} else {
bot.tossStack(item, checkIfTossed)
}

function checkIfTossed (err) {
if (err) {
bot.chat(`unable to toss: ${err.message}`)
} else if (amount) {
bot.chat(`tossed ${amount} x ${name}`)
} else {
bot.chat(`tossed ${name}`)
}
}
}

function itemByName (name) {
return bot.inventory.items().filter(item => item.name === name)[0]
}

function itemToString (item) {
if (item) {
return `${item.name} x ${item.count}`
} else {
return '(nothing)'
}
}

function sayItems (items = bot.inventory.items()) {
const output = items.map(itemToString).join(', ')
if (output) {
bot.chat(output)
} else {
bot.chat('empty')
}
}

function getAnvilIds () {
const matchingBlocks = [mcData.blocksByName.anvil.id]
if (mcData.blocksByName?.chipped_anvil) {
matchingBlocks.push(mcData.blocksByName.chipped_anvil.id)
matchingBlocks.push(mcData.blocksByName.damaged_anvil.id)
}
return matchingBlocks
}

async function rename (bot, itemName, name) {
const anvilBlock = bot.findBlock({
matching: getAnvilIds()
})
const anvil = await bot.openAnvil(anvilBlock)
try {
await anvil.rename(itemByName(itemName), name)
bot.chat('Anvil used successfully.')
} catch (err) {
bot.chat(err.message)
}
anvil.close()
}

async function combine (bot, itemName1, itemName2, name) {
const anvilBlock = bot.findBlock({
matching: getAnvilIds()
})
const anvil = await bot.openAnvil(anvilBlock)
try {
bot.chat('Using the anvil...')
await anvil.combine(itemByName(itemName1), itemByName(itemName2), name)
bot.chat('Anvil used successfully.')
} catch (err) {
bot.chat(err.message)
}
anvil.close()
}

bot.on('error', console.log)
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ export interface Bot extends TypedEmitter<BotEvents> {

openEnchantmentTable(enchantmentTable: Block): EnchantmentTable;

openAnvil(anvil: Block): Anvil;

openVillager(
villager: Entity
): Villager;
Expand Down Expand Up @@ -705,6 +707,11 @@ export class EnchantmentTable extends (EventEmitter as new () => TypedEmitter<Co
putLapis(item: Item, cb?: (err: Error | null) => void): Promise<Item>;
}

export class Anvil {
combine(itemOne: Item, itemTwo: Item, name?: string): Promise<void>
rename(item: Item, name?: string): Promise<void>
}

export type Enchantment = {
level: number;
};
Expand Down
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const plugins = {
spawn_point: require('./lib/plugins/spawn_point'),
tablist: require('./lib/plugins/tablist'),
time: require('./lib/plugins/time'),
villager: require('./lib/plugins/villager')
villager: require('./lib/plugins/villager'),
anvil: require('./lib/plugins/anvil')
}

const supportedVersions = require('./lib/version').supportedVersions
Expand Down
5 changes: 5 additions & 0 deletions lib/features.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@
"description": "The parameter metadata of the setblock command is a number",
"versions": ["1.8", "1.12.2"]
},
{
"name": "useMCItemName",
"description": "send item name for anvil using plugin channel MC|TrList",
"versions": ["1.8", "1.12.2"]
},
{
"name": "selectingTradeMovesItems",
"description": "Selecting a trade automatically puts the required items into trading slots.",
Expand Down
115 changes: 115 additions & 0 deletions lib/plugins/anvil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const assert = require('assert')
const { callbackify, sleep } = require('../promise_utils')
const { once } = require('events')

module.exports = inject

function inject (bot) {
const Item = require('prismarine-item')(bot.version)

const matchWindowType = window => /minecraft:(?:chipped_|damaged_)?anvil/.test(window.type)

async function openAnvil (anvilBlock) {
const anvil = await bot.openBlock(anvilBlock)
if (!matchWindowType(anvil)) {
throw new Error('This is not a anvil-like window')
}

function err (name) {
anvil.close()
throw new Error(name)
}

function sendItemName (name) {
if (bot.supportFeature('useMCItemName')) {
bot._client.writeChannel('MC|ItemName', name)
} else {
bot._client.write('name_item', { name })
}
}

async function addCustomName (name) {
if (!name) return
for (let i = 1; i < name.length + 1; i++) {
sendItemName(name.substring(0, i))
await sleep(50)
}
}
async function putInAnvil (itemOne, itemTwo) {
await putSomething(0, itemOne.type, itemOne.metadata, itemOne.count, itemOne.nbt)
sendItemName('') // sent like this by vnailla
if (!bot.supportFeature('useMCItemName')) sendItemName('')
await putSomething(1, itemTwo.type, itemTwo.metadata, itemTwo.count, itemTwo.nbt)
}

async function combine (itemOne, itemTwo, name) {
if (name?.length > 35) err('Name is too long.')
if (bot.supportFeature('useMCItemName')) {
bot._client.registerChannel('MC|ItemName', 'string')
}

assert.ok(itemOne && itemTwo)
const { xpCost: normalCost } = Item.anvil(itemOne, itemTwo, bot.game.gameMode === 'creative', name)
const { xpCost: inverseCost } = Item.anvil(itemTwo, itemOne, bot.game.gameMode === 'creative', name)
if (normalCost === 0 && inverseCost === 0) err('Not anvil-able (in either direction), cancelling.')

const smallest = (normalCost < inverseCost ? normalCost : inverseCost) === 0 ? inverseCost : 0
if (bot.game.gameMode !== 'creative' && bot.experience.level < smallest) {
err('Player does not have enough xp to do action, cancelling.')
}

const xpPromise = bot.game.gameMode === 'creative' ? Promise.resolve() : once(bot, 'experience')
if (normalCost === 0) await putInAnvil(itemTwo, itemOne)
else if (inverseCost === 0) await putInAnvil(itemOne, itemTwo)
else if (normalCost < inverseCost) await putInAnvil(itemOne, itemTwo)
else await putInAnvil(itemTwo, itemOne)

await addCustomName(name)
await bot.putAway(2)
await xpPromise
}

async function rename (item, name) {
if (name?.length > 35) err('Name is too long.')
if (bot.supportFeature('useMCItemName')) {
bot._client.registerChannel('MC|ItemName', 'string')
}
assert.ok(item)
const { xpCost: normalCost } = Item.anvil(item, null, bot.game.gameMode === 'creative', name)
if (normalCost === 0) err('Not valid rename, cancelling.')

if (bot.game.gameMode !== 'creative' && bot.experience.level < normalCost) {
err('Player does not have enough xp to do action, cancelling.')
}
const xpPromise = once(bot, 'experience')
await putSomething(0, item.type, item.metadata, item.count, item.nbt)
sendItemName('') // sent like this by vnailla
if (!bot.supportFeature('useMCItemName')) sendItemName('')
await addCustomName(name)
await bot.putAway(2)
await xpPromise
}

async function putSomething (destSlot, itemId, metadata, count, nbt) {
const options = {
window: anvil,
itemType: itemId,
metadata,
count,
nbt,
sourceStart: anvil.inventoryStart,
sourceEnd: anvil.inventoryEnd,
destStart: destSlot,
destEnd: destSlot + 1
}
await bot.transfer(options)
}

anvil.combine = callbackify(combine)
anvil.rename = callbackify(rename)

return anvil
}

bot.openAnvil = callbackify(openAnvil)
}
Loading

0 comments on commit 02769f3

Please sign in to comment.