Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.18 and 1.18.2 support #662

Merged
merged 8 commits into from
Jan 4, 2025
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -52,4 +52,4 @@ jobs:
- name: Install Dependencies
run: npm install
- name: Start Tests
run: npm run mocha_test -- -g ${{ matrix.mcVersion }}v
run: npm run mocha_test -- -g ${{ matrix.mcVersion }}v
2 changes: 1 addition & 1 deletion config/default-settings.json
Original file line number Diff line number Diff line change
@@ -25,5 +25,5 @@
},
"everybody-op": false,
"max-entities":100,
"version": "1.17.1"
"version": "1.18.2"
}
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ flying-squid
Create Minecraft servers with a powerful, stable, and high level JavaScript API.

## Features
* Support for Minecraft 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16 and 1.17
* Support for Minecraft 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17 and 1.18
* Players can see the world
* Players see each other in-game and in tab
* Digging
@@ -139,7 +139,7 @@ mcServer.createMCServer({
},
'everybody-op': true,
'max-entities': 100,
'version': '1.17.1'
'version': '1.18'
})
```

7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ const { EventEmitter } = require('events')
const { testedVersions, latestSupportedVersion, oldestSupportedVersion } = require('./lib/version')
const Command = require('./lib/command')
const plugins = require('./lib/plugins')
const { onceWithTimeout } = require('./lib/utils')

module.exports = {
createMCServer,
@@ -60,8 +61,14 @@ class MCServer extends EventEmitter {
Promise.all(promises).then(() => {
this.emit('pluginsReady')
this.pluginsReady = true
this.debug?.('Loaded plugins')
})

this.waitForReady = (timeout) => {
if (this.pluginsReady) return Promise.resolve()
return onceWithTimeout(this, 'pluginsReady', timeout)
}

if (options.logging === true) this.createLog()
this._server.on('error', error => this.emit('error', error))
this._server.on('listening', () => this.emit('listening', this._server.socketServer.address().port))
2 changes: 1 addition & 1 deletion src/lib/plugins/digging.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const Vec3 = require('vec3').Vec3

module.exports.player = function (player, serv, { version }) {
const { registry } = serv.registry
const { registry } = serv
function cancelDig ({ position, block }) {
player.sendBlock(position, block.type)
}
1 change: 1 addition & 0 deletions src/lib/plugins/errorHandler.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports.player = async function (player, serv) {
if (globalThis.isMocha || serv.debug) return // Don't eat errors when debugging
function unhandledRejection (promise) {
serv.warn('-------------------------------')
serv.warn('Please report this error to flying-squid! This is can be bug')
8 changes: 7 additions & 1 deletion src/lib/plugins/log.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,12 @@ module.exports.server = function (serv, settings) {
serv.info(banner.username + ' banned ' + bannedUsername + (reason ? ' (' + reason + ')' : '')))
serv.on('seed', (seed) => serv.info('World seed: ' + seed))

serv._server.on('close', function () {
serv.info('Server is closed.')
// Remove server from managed list so GC can clean it up
_servers.splice(_servers.indexOf(serv), 1)
})

const logFile = path.join('logs', timeStarted + '.log')

serv.log = message => {
@@ -69,7 +75,7 @@ module.exports.server = function (serv, settings) {
serv.log('[' + colors.yellow('WARN') + ']: ' + message)
}

if (isInNode) {
if (isInNode && !process.env.CI) {
console.log = (function () {
const orig = console.log
return function () {
1 change: 1 addition & 0 deletions src/lib/plugins/login.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ module.exports.server = function (serv, options) {
serv._server.on('login', async (client) => {
if (!serv.pluginsReady) {
client.end('Server is still starting! Please wait before reconnecting.')
serv.info(`[${client.socket.remoteAddress}] ${client.username} (${client.uuid}) disconnected as server is still starting`)
return
}
serv.debug?.(`[login] ${client.socket?.remoteAddress} - ${client.username} (${client.uuid}) connected`, client.version, client.protocolVersion)
92 changes: 50 additions & 42 deletions src/lib/plugins/world.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,11 @@ const generations = require('../generations')
const playerDat = require('../playerDat')
const spiralloop = require('spiralloop')
const { level } = require('prismarine-provider-anvil')
const nbt = require('prismarine-nbt')

function sleep (ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms))
}

module.exports.server = async function (serv, options = {}) {
const { version, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options
@@ -30,7 +35,7 @@ module.exports.server = async function (serv, options = {}) {
await level.writeLevel(worldFolder + '/level.dat', {
RandomSeed: [seed, 0],
Version: { Name: options.version },
generatorName: { superfalt: 'flat', diamond_square: 'default' }[generation.name] || 'customized'
generatorName: { superflat: 'flat', diamond_square: 'default' }[generation.name] || 'customized'
})
}
} else {
@@ -170,52 +175,60 @@ module.exports.player = function (player, serv, settings) {
}
}

// let chunkSendCtr = 0
player.sendChunk = (chunkX, chunkZ, column) => {
// console.log(chunkSendCtr++, 'Sending chunk at', chunkX, chunkZ, 'to', player.username, chunkX * 16, chunkZ * 16)
return player.behavior('sendChunk', {
x: chunkX,
z: chunkZ,
chunk: column
}, ({ x, z, chunk }) => {
player._client.write('map_chunk', {
x,
z,
groundUp: true,
bitMap: chunk.getMask(),
biomes: chunk.dumpBiomes(),
ignoreOldData: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that?
heightmaps: {
type: 'compound',
name: '',
value: {
MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([0, 0]) }
}
}, // FIXME: fake heightmap
chunkData: chunk.dump(),
blockEntities: []
// FIXME: fake heightmap
const heightmaps = nbt.comp({
MOTION_BLOCKING: nbt.longArray(new Array(36).fill([0, 0]))
})

if (serv.supportFeature('newLightingDataFormat')) { // 1.17+
player._client.write('update_light', {
chunkX: x,
chunkZ: z,
trustEdges: true, // trust edges for lighting updates
const trustEdges = true // trust edges for lighting updates - should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that?
if (serv.supportFeature('tallWorld')) { // 1.18+ - merged chunk and light data
player._client.write('map_chunk', {
x,
z,
heightmaps,
chunkData: chunk.dump(),
blockEntities: [],
trustEdges,
...chunk.dumpLight()
})
} else if (serv.supportFeature('lightSentSeparately')) { // -1.16.5
player._client.write('update_light', {
chunkX: x,
chunkZ: z,
trustEdges: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that?
skyLightMask: chunk.skyLightMask,
blockLightMask: chunk.blockLightMask,
emptySkyLightMask: 0,
emptyBlockLightMask: 0,
data: chunk.dumpLight()
} else {
player._client.write('map_chunk', {
x,
z,
groundUp: true,
bitMap: chunk.getMask(),
biomes: chunk.dumpBiomes(),
ignoreOldData: true, // should be false when a chunk section is updated instead of the whole chunk being overwritten, do we ever do that?
heightmaps,
chunkData: chunk.dump(),
blockEntities: []
})

if (serv.supportFeature('newLightingDataFormat')) { // 1.17+
player._client.write('update_light', {
chunkX: x,
chunkZ: z,
trustEdges,
...chunk.dumpLight()
})
} else if (serv.supportFeature('lightSentSeparately')) { // -1.16.5
player._client.write('update_light', {
chunkX: x,
chunkZ: z,
trustEdges,
skyLightMask: chunk.skyLightMask,
blockLightMask: chunk.blockLightMask,
emptySkyLightMask: 0,
emptyBlockLightMask: 0,
data: chunk.dumpLight()
})
}
}
return Promise.resolve()
})
}

@@ -248,12 +261,7 @@ module.exports.player = function (player, serv, settings) {
.then(() => player.world.getColumn(chunkX, chunkZ))
.then((column) => player.sendChunk(chunkX, chunkZ, column))
return group ? p.then(() => sleep(5)) : p
}
, Promise.resolve())
}

function sleep (ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms))
}, Promise.resolve())
}

player.worldSendInitialChunks = () => {
14 changes: 14 additions & 0 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
@@ -4,9 +4,23 @@ function emitAsync (listener, event, ...args) {
return Promise.all(listeners.map(listener => listener(...args)))
}

function onceWithTimeout (emitter, event, timeout = 10000, checkCondition) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Timeout waiting for '${event}' event`))
}, timeout)
emitter.once(event, (data) => {
if (checkCondition && !checkCondition(data)) return
clearTimeout(timeoutId)
resolve(data)
})
})
}

const skipMcPrefix = (name) => typeof name === 'string' ? name.replace(/^minecraft:/, '') : name

module.exports = {
onceWithTimeout,
skipMcPrefix,
emitAsync
}
2 changes: 1 addition & 1 deletion src/lib/version.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1']
const testedVersions = ['1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18', '1.18.2']
module.exports = {
testedVersions,
latestSupportedVersion: testedVersions[testedVersions.length - 1],
106 changes: 73 additions & 33 deletions test/mineflayer.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
/* eslint-env mocha */

globalThis.isMocha = true
const fs = require('fs')
const { join } = require('path')
const squid = require('flying-squid')
const settings = require('../config/default-settings.json')
const mineflayer = require('mineflayer')
const { Vec3 } = require('vec3')
const { onceWithTimeout } = require('../src/lib/utils')
const expect = require('expect').default

function assertPosEqual (actual, expected, precision = 1) {
expect(actual.distanceTo(expected)).toBeLessThan(precision)
}

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}

const { once } = require('events')

squid.testedVersions.forEach((testedVersion, i) => {
@@ -39,11 +46,22 @@ squid.testedVersions.forEach((testedVersion, i) => {
}

async function waitMessage (bot, message) {
const [msg1] = await once(bot, 'message')
expect(msg1.extra[0].text).toEqual(message)
// const [msg1] = await once(bot, 'message')
// expect(msg1.extra[0].text).toEqual(message)
onceWithTimeout(bot, 'message', 5000, (msg) => {
console.log('*msg', msg)
return msg.toString() === message
})
}

beforeEach(async () => {
// Clear the world dir before each test
const worldFolder = 'world/test_' + testedVersion
const dir = join(__dirname, '../', worldFolder)
console.log('Clearing world dir', dir)
fs.rmSync(dir, { recursive: true, force: true })

beforeEach(async function () {
console.log('🔻 Running test: ' + this.currentTest.title)
const options = settings
options['online-mode'] = false
options['everybody-op'] = true
@@ -59,15 +77,19 @@ squid.testedVersions.forEach((testedVersion, i) => {
}
}

options.worldFolder = worldFolder
options.debug = console.log
serv = squid.createMCServer(options)
if (registry.supportFeature('entityCamelCase')) {
entityName = 'EnderDragon'
} else {
entityName = 'ender_dragon'
}

await once(serv, 'listening')
const port = serv._server.socketServer.address().port
console.log('[test] Waiting for server to start')
const [port] = await once(serv, 'listening')
await serv.waitForReady()
console.log('[test] Server is started on', port, version.minecraftVersion)
bot = mineflayer.createBot({
host: 'localhost',
port,
@@ -81,50 +103,69 @@ squid.testedVersions.forEach((testedVersion, i) => {
version: version.minecraftVersion
})

await Promise.all([once(bot, 'login'), once(bot2, 'login')])
await Promise.all([once(bot, 'spawn'), once(bot2, 'spawn'), waitForReady(bot), waitForReady(bot2)])
bot.entity.onGround = false
bot2.entity.onGround = false
})

afterEach(async () => {
await serv.quit()
// log what the bot is standing on for debugging
const bot1StandingOn = bot.blockAt(bot.entity.position.floored().offset(0, -1, 0))
const bot2StandingOn = bot2.blockAt(bot2.entity.position.floored().offset(0, -1, 0))
console.log('bot1 is standing on', bot1StandingOn)
console.log('bot2 is standing on', bot2StandingOn)
})

function waitSpawnZone (bot, view) {
const nbChunksExpected = (view * 2) * (view * 2)
let c = 0
return new Promise(resolve => {
const listener = () => {
c++
if (c === nbChunksExpected) {
bot.removeListener('chunkColumnLoad', listener)
function waitForReady (bot) {
const viewDistance = 2
const testExpectedNoChunks = (viewDistance * 2) * (viewDistance * 2)
return new Promise((resolve) => {
let recvChunks = 0
function onColumnLoad () {
recvChunks++
if (recvChunks === testExpectedNoChunks) {
bot.removeListener('chunkColumnLoad', onColumnLoad)
resolve()
}
}
bot.on('chunkColumnLoad', listener)
bot.on('chunkColumnLoad', onColumnLoad)
})
}

afterEach(async () => {
console.log('Quitting server...')
await serv.quit()
console.log('Quit server!')
})

describe('actions', () => {
it('can dig', async () => {
await Promise.all([waitSpawnZone(bot, 2), waitSpawnZone(bot2, 2), onGround(bot), onGround(bot2)])
// Log the name of the test being run
beforeEach(function () {
console.log('🔻 Running actions test: ' + this.currentTest.title)
})

it('can dig', async () => {
const pos = bot.entity.position.offset(0, -1, 0).floored()
const p = once(bot2, 'blockUpdate', { array: true })
// Set a dirt block below the bot so we can easily dig
bot.chat(`/setblock ${pos.x} ${pos.y} ${pos.z} dirt`)
await sleep(1000)
console.log('Block at', bot.entity.position, bot.blockAt(pos))

const p = once(bot2, 'blockUpdate')
bot.dig(bot.blockAt(pos))
console.log('Digging...')

const [, newBlock] = await p
console.log('Dug.')
assertPosEqual(newBlock.position, pos)
expect(newBlock.type).toEqual(0)
})

it('can place a block', async () => {
await Promise.all([waitSpawnZone(bot, 2), waitSpawnZone(bot2, 2), onGround(bot), onGround(bot2)])

const pos = bot.entity.position.offset(0, -2, 0).floored()
const digPromise = once(bot2, 'blockUpdate', { array: true })
const digPromise = once(bot2, 'blockUpdate')
bot.dig(bot.blockAt(pos))

console.log(' ✔️ dug block at', pos)

let [, newBlock] = await digPromise
assertPosEqual(newBlock.position, pos)
expect(newBlock.type).toEqual(0)
@@ -137,16 +178,17 @@ squid.testedVersions.forEach((testedVersion, i) => {
bot.creative.setInventorySlot(36, new Item(1, 1))
await invPromise

console.log(' ✔️ updated inventory')

const placePromise = once(bot2, 'blockUpdate', { array: true })
bot.placeBlock(bot.blockAt(pos.offset(0, -1, 0)), new Vec3(0, 1, 0));
[, newBlock] = await placePromise
console.log(' ✔️ placed block at', pos)
assertPosEqual(newBlock.position, pos)
expect(newBlock.type).toEqual(1)
})

it('can open and close a chest', async () => {
await Promise.all([waitSpawnZone(bot, 2), onGround(bot), waitSpawnZone(bot2, 2), onGround(bot2)])

const chestId = registry.blocksByName.chest.id
const [x, y, z] = [1, 2, 3]

@@ -188,8 +230,11 @@ squid.testedVersions.forEach((testedVersion, i) => {
})

describe('commands', () => {
beforeEach(function () {
console.log('🔻 Running commands test: ' + this.currentTest.title)
})

it('has an help command', async () => {
await waitMessagePromise('bot joined the game.')
bot.chat('/help')
await once(bot, 'message')
})
@@ -243,30 +288,26 @@ squid.testedVersions.forEach((testedVersion, i) => {
assertPosEqual(bot2.entity.position, bot.entity.position)
})
it('can tp with relative positions', async () => {
await onGround(bot)
const initialPosition = bot.entity.position.clone()
bot.chat('/tp ~1 ~-2 ~3')
await once(bot, 'forcedMove')
assertPosEqual(bot.entity.position, initialPosition.offset(1, -2, 3), 2)
})
it('can tp somebody else with relative positions', async () => {
await Promise.all([onGround(bot), onGround(bot2)])
const initialPosition = bot2.entity.position.clone()
bot.chat('/tp bot2 ~1 ~-2 ~3')
await once(bot2, 'forcedMove')
assertPosEqual(bot2.entity.position, initialPosition.offset(1, -2, 3), 2)
})
})
it('can use /deop', async () => {
await waitMessagePromise('bot joined the game.')
bot.chat('/deop bot')
await waitMessage(bot, '§7§o[Server: Deopped bot]')
bot.chat('/op bot')
await waitMessage(bot, 'You do not have permission to use this command')
serv.getPlayer('bot').op = true
})
it('can use /setblock', async () => {
await Promise.all([waitSpawnZone(bot, 2), onGround(bot)])
const chestId = registry.blocksByName.chest.id
const p = once(bot, 'blockUpdate:' + new Vec3(1, 2, 3), { array: true })
bot.chat(`/setblock 1 2 3 ${chestId} 0`)
@@ -301,7 +342,6 @@ squid.testedVersions.forEach((testedVersion, i) => {
}

it('can use /banlist, /ban, /pardon', async () => {
await waitMessagePromise('bot joined the game.')
bot.chat('/banlist')
await waitMessagePromise('There are 0 total banned players')
bot.chat('/ban bot2')
2 changes: 1 addition & 1 deletion test/portal.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-env mocha */

globalThis.isMocha = true
const squid = require('flying-squid')
const PortalDetector = require('../src/lib/portal_detector')
const expect = require('expect').default
2 changes: 1 addition & 1 deletion test/simple.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-env mocha */

globalThis.isMocha = true
const net = require('net')
const squid = require('flying-squid')