Path: blob/master/test/externalTests/plugins/testCommon.js
9427 views
const { Vec3 } = require('vec3')12const { spawn } = require('child_process')3const { once } = require('../../../lib/promise_utils')4const process = require('process')5const assert = require('assert')6const { sleep, onceWithCleanup } = require('../../../lib/promise_utils')78const timeout = 50009module.exports = inject1011function inject (bot, wrap) {12console.log(bot.version)1314bot.test = {}15bot.test.groundY = bot.supportFeature('tallWorld') ? -60 : 416bot.test.sayEverywhere = sayEverywhere17bot.test.clearInventory = clearInventory18bot.test.becomeSurvival = becomeSurvival19bot.test.becomeCreative = becomeCreative20bot.test.fly = fly21bot.test.teleport = teleport22bot.test.resetState = resetState23bot.test.setInventorySlot = setInventorySlot24bot.test.placeBlock = placeBlock25bot.test.runExample = runExample26bot.test.tellAndListen = tellAndListen27bot.test.selfKill = selfKill28bot.test.wait = function (ms) {29return new Promise((resolve) => { setTimeout(resolve, ms) })30}3132bot.test.awaitItemReceived = async (command) => {33const p = once(bot.inventory, 'updateSlot')34bot.chat(command)35await p // await getting the item36}37// setting relative to true makes x, y, & z relative using ~38bot.test.setBlock = async ({ x = 0, y = 0, z = 0, relative, blockName }) => {39const { x: _x, y: _y, z: _z } = relative ? bot.entity.position.floored().offset(x, y, z) : { x, y, z }40const block = bot.blockAt(new Vec3(_x, _y, _z))41if (block.name === blockName) {42return43}44const p = once(bot.world, `blockUpdate:(${_x}, ${_y}, ${_z})`)45const prefix = relative ? '~' : ''46bot.chat(`/setblock ${prefix}${x} ${prefix}${y} ${prefix}${z} ${blockName}`)47await p48}4950let grassName51if (bot.supportFeature('itemsAreNotBlocks')) {52grassName = 'grass_block'53} else if (bot.supportFeature('itemsAreAlsoBlocks')) {54grassName = 'grass'55}5657const layerNames = [58'bedrock',59'dirt',60'dirt',61grassName,62'air',63'air',64'air',65'air',66'air'67]6869async function resetBlocksToSuperflat () {70const groundY = 471for (let y = groundY + 4; y >= groundY - 1; y--) {72const realY = y + bot.test.groundY - 473bot.chat(`/fill ~-5 ${realY} ~-5 ~5 ${realY} ~5 ` + layerNames[y])74}75await bot.test.wait(100)76}7778async function placeBlock (slot, position) {79bot.setQuickBarSlot(slot - 36)80// always place the block on the top of the block below it, i guess.81const referenceBlock = bot.blockAt(position.plus(new Vec3(0, -1, 0)))82return bot.placeBlock(referenceBlock, new Vec3(0, 1, 0))83}8485// always leaves you in creative mode86async function resetState () {87await becomeCreative()88await clearInventory()89bot.creative.startFlying()90await teleport(new Vec3(0, bot.test.groundY, 0))91await bot.waitForChunksToLoad()92await resetBlocksToSuperflat()93await clearInventory()94}9596async function becomeCreative () {97// console.log('become creative')98return setCreativeMode(true)99}100101async function becomeSurvival () {102return setCreativeMode(false)103}104105async function setCreativeMode (value) {106const mode = value ? 'creative' : 'survival'107const modeId = value ? 1 : 0108if (bot.game.gameMode === mode) return109// Use server console for instant, reliable gamemode change.110// The old approach (triple chat command + message parsing) was fragile111// and the most common source of flaky test timeouts.112const gameModePromise = onceWithCleanup(bot._client, 'game_state_change', {113timeout,114checkCondition: (packet) => {115// reason is 3 (number) on old versions, 'change_game_mode' (string) on new116const isGameModeChange = packet.reason === 3 || packet.reason === 'change_game_mode'117return isGameModeChange && Math.floor(packet.gameMode) === modeId118}119})120wrap.writeServer(`gamemode ${mode} flatbot\n`)121await gameModePromise122}123124async function clearInventory () {125// Use bot.chat for /give (server console /give doesn't send inventory126// update packets on 1.21.9+). Use server console for /clear.127bot.chat('/give @a stone 1')128await onceWithCleanup(bot.inventory, 'updateSlot', {129timeout: 10000,130checkCondition: (slot, oldItem, newItem) => newItem?.name === 'stone'131})132const clearMsg = onceWithCleanup(bot, 'message', {133timeout: 10000,134checkCondition: msg => msg.translate === 'commands.clear.success.single' || msg.translate === 'commands.clear.success'135})136bot.chat('/clear')137await clearMsg138}139140// you need to be in creative mode for this to work141async function setInventorySlot (targetSlot, item) {142assert(item === null || item.name !== 'unknown', `item should not be unknown ${JSON.stringify(item)}`)143return bot.creative.setInventorySlot(targetSlot, item)144}145146async function teleport (position) {147// Use server console for teleport — works even if bot is in a bad state148if (bot.supportFeature('hasExecuteCommand')) {149wrap.writeServer(`execute in overworld run teleport ${bot.username} ${position.x} ${position.y} ${position.z}\n`)150} else {151wrap.writeServer(`tp ${bot.username} ${position.x} ${position.y} ${position.z}\n`)152}153return onceWithCleanup(bot, 'move', {154timeout,155checkCondition: () => bot.entity.position.distanceTo(position) < 0.9156})157}158159function sayEverywhere (message) {160bot.chat(message)161console.log(message)162}163164async function fly (delta) {165return bot.creative.flyTo(bot.entity.position.plus(delta))166}167168async function tellAndListen (to, what, listen) {169const chatMessagePromise = onceWithCleanup(bot, 'chat', {170timeout,171checkCondition: (username, message) => username === to && listen(message)172})173174bot.chat(what)175176return chatMessagePromise177}178179async function runExample (file, run) {180let childBotName181182const detectChildJoin = async () => {183const [message] = await onceWithCleanup(bot, 'message', {184checkCondition: message => message.json.translate === 'multiplayer.player.joined'185})186childBotName = message.json.with[0].insertion187bot.chat(`/tp ${childBotName} 50 ${bot.test.groundY} 0`)188// Wait for the child entity to arrive at the teleport target,189// confirming the server has processed the TP190const targetPos = new Vec3(50, bot.test.groundY, 0)191while (!bot.players[childBotName]?.entity ||192bot.players[childBotName].entity.position.distanceTo(targetPos) > 5) {193await sleep(100)194}195// Let the child's physics engine initialize at the new position196// (ground detection, chunk processing) before starting the test197await bot.waitForTicks(60)198bot.chat('loaded')199}200201const runExampleOnReady = async () => {202await onceWithCleanup(bot, 'chat', {203checkCondition: (username, message) => message === 'Ready!'204})205return run(childBotName)206}207208const child = spawn('node', [file, '127.0.0.1', `${bot.test.port}`])209210// Useful to debug child processes:211child.stdout.on('data', (data) => { console.log(`${data}`) })212child.stderr.on('data', (data) => { console.error(`${data}`) })213214const closeExample = async (err) => {215console.log('kill process ' + child.pid)216217try {218process.kill(child.pid, 'SIGTERM')219const [code] = await onceWithCleanup(child, 'close', { timeout: 5000 })220console.log('close requested', code)221} catch (e) {222console.log(e)223console.log('process termination failed, process may already be closed')224}225226if (err) {227throw err228}229}230231// Let mocha's test-level timeout (90s) be the backstop instead of232// an inner withTimeout, which was causing premature failures on233// slow CI runners.234try {235await Promise.all([detectChildJoin(), runExampleOnReady()])236} catch (err) {237console.log(err)238return closeExample(err)239}240return closeExample()241}242243function selfKill () {244bot.chat('/kill @p')245}246247// Debug packet IO when tests are re-run with "Enable debug logging" - https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables248if (process.env.RUNNER_DEBUG) {249bot._client.on('packet', function (data, meta) {250if (['chunk', 'time', 'light', 'alive'].some(e => meta.name.includes(e))) return251console.log('->', meta.name, JSON.stringify(data)?.slice(0, 250))252})253const oldWrite = bot._client.write254bot._client.write = function (name, data) {255if (['alive', 'pong', 'ping'].some(e => name.includes(e))) return256console.log('<-', name, JSON.stringify(data)?.slice(0, 250))257oldWrite.apply(bot._client, arguments)258}259BigInt.prototype.toJSON ??= function () { // eslint-disable-line260return this.toString()261}262}263}264265266