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.20.2 support #667

Merged
merged 7 commits into from
Jan 5, 2025
Merged
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
2 changes: 1 addition & 1 deletion config/default-settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
},
"everybody-op": false,
"max-entities":100,
"version": "1.19.4"
"version": "1.20.2"
}
8 changes: 8 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ serv.tabComplete.add('tabId', () => {

### Events

#### "ready"

Fires when the server is ready to accept connections (after `listening` and `pluginsReady` events).

#### "error" (error)

Fires when there is an error.
Expand Down Expand Up @@ -386,6 +390,10 @@ Emitted when `serv.pluginsReady` is set to `true`.

### Methods

#### async serv.waitForReady()

Returns a promise that resolves when the server is ready to accept connections.

#### serv.formatMessage(message)

You can override this function so you can process the message before sending it to the console.
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 1.17, 1.18, 1.19
* Support for Minecraft 1.8, 1.9, 1.10, 1.11, 1.12, 1.13, 1.14, 1.15, 1.16, 1.17, 1.18, 1.19, 1.20
* Players can see the world
* Players see each other in-game and in tab
* Digging
Expand Down
32 changes: 28 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ class MCServer extends EventEmitter {
this.registry = registry
this.supportFeature = feature => customFeatures[feature] ?? registry.supportFeature(feature)

Promise.all([onceWithTimeout(this, 'pluginsReady', 5000), onceWithTimeout(this._server, 'listening', 5000)]).then(() => {
this.isReady = true
this.emit('ready')
})

const promises = []
for (const plugin of plugins.builtinPlugins) {
promises.push(plugin.server?.(this, options))
Expand All @@ -66,14 +71,33 @@ class MCServer extends EventEmitter {
this.debug?.('Loaded plugins')
})

this.waitForReady = (timeout) => {
if (this.pluginsReady) return Promise.resolve()
return onceWithTimeout(this, 'pluginsReady', timeout)
this.waitForReady = async function (timeout) {
if (this.isReady) return true
return onceWithTimeout(this, 'ready', 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))
this._server.on('listening', () => {
this.listeningPort = this._server.socketServer.address().port
this.emit('listening', this.listeningPort)
})
this.emit('asap')
}

async destroy () {
this.isClosed = true
this.isReady = false
this.pluginsReady = false
this.emit('close')
for (const player of this.players) {
player._client.write = function () {
throw new Error('Tried to write to a closed connection')
}
}
if (this._server) {
this._server.close()
return onceWithTimeout(this._server, 'close', 1000)
}
}
}
6 changes: 3 additions & 3 deletions src/lib/plugins/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ module.exports.server = function (serv, { version }) {
info: 'Stop the server',
usage: '/stop',
op: true,
action () {
serv.quit('Server closed')
process.exit()
async action () {
await serv.quit('Server closed')
process.exit(0)
}
})

Expand Down
6 changes: 5 additions & 1 deletion src/lib/plugins/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ module.exports.server = function (serv) {
})
}

module.exports.entity = function (entity) {
module.exports.entity = function (entity, serv) {
entity.sendMetadata = (data) => {
if (serv.registry.version['>=']('1.20.2')) {
// todo: fix in mcdata
return
}
entity._writeOthersNearby('entity_metadata', {
entityId: entity.id,
metadata: data
Expand Down
2 changes: 1 addition & 1 deletion src/lib/plugins/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ module.exports.server = function (serv, settings) {
serv.log('[' + colors.yellow('WARN') + ']: ' + message)
}

if (isInNode && !process.env.CI) {
if (isInNode && !process.env.CI && process.stdout.isTTY) {
console.log = (function () {
const orig = console.log
return function () {
Expand Down
15 changes: 8 additions & 7 deletions src/lib/plugins/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports.server = function (serv, options) {
client.on('error', error => serv.emit('clientError', client, error))
})

serv._server.on('login', async (client) => {
serv._server.on('playerJoin', 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`)
Expand Down Expand Up @@ -44,8 +44,6 @@ module.exports.server = function (serv, options) {
module.exports.player = async function (player, serv, settings) {
const Item = require('prismarine-item')(serv.registry)

let playerData

async function addPlayer () {
player.type = 'player'
player.crouching = false // Needs added in prismarine-entity later
Expand All @@ -55,16 +53,16 @@ module.exports.player = async function (player, serv, settings) {

await player.findSpawnPoint()

playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder)
Object.keys(playerData.player).forEach(k => { player[k] = playerData.player[k] })
const playerData = player._playerData = await playerDat.read(player.uuid, player.spawnPoint, settings.worldFolder)
for (const key in playerData.player) player[key] = playerData.player[key]

serv.players.push(player)
serv.uuidToPlayer[player.uuid] = player
player.loadedChunks = {}
}

function updateInventory () {
playerData.inventory.forEach((item) => {
player._playerData.inventory.forEach((item) => {
const itemName = item.id.value.slice(10) // skip game brand prefix
const theItem = serv.registry.itemsByName[itemName] || serv.registry.blocksByName[itemName]

Expand Down Expand Up @@ -258,6 +256,7 @@ module.exports.player = async function (player, serv, settings) {
}

await addPlayer()
const pos = player.position
sendLogin()
sendStatus()
player.sendSpawnPosition()
Expand All @@ -280,6 +279,8 @@ module.exports.player = async function (player, serv, settings) {
player.worldSendRestOfChunks()
sendChunkWhenMove()

player.save()
// Prevent player from sometimes falling through the world on login by resending their login position
player.sendSelfPosition(pos)
await player.save()
}
}
5 changes: 2 additions & 3 deletions src/lib/plugins/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ module.exports.server = function (serv) {
player.kick(reason)
return once(player, 'disconnected')
}))
serv._server.close()
await once(serv._server, 'close')
await serv.destroy()
}
}

Expand All @@ -18,7 +17,7 @@ module.exports.player = function (player, serv, { worldFolder }) {

player._client.on('end', async () => {
if (player && player.username) {
player._unloadAllChunks()
player._unloadAllChunks(true /* becasuePlayerLeft */)
serv.broadcast(serv.color.yellow + player.username + ' left the game.')
serv._sendPlayerEventLeave(player)
player.nearbyPlayers().forEach(otherPlayer => otherPlayer.despawnEntities([player]))
Expand Down
29 changes: 17 additions & 12 deletions src/lib/plugins/respawn.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,28 @@ module.exports.player = function (player, serv, { version }) {

if (actionId === 0) {
player.behavior('requestRespawn', {}, () => {
player._client.write('respawn', {
previousGameMode: player.prevGameMode,
dimension: (serv.supportFeature('dimensionIsAString') || serv.supportFeature('dimensionIsAWorld')) ? serv.dimensionNames[0] : 0,
worldName: serv.dimensionNames[0],
difficulty: serv.difficulty,
hashedSeed: serv.hashedSeed,
gamemode: player.gameMode,
levelType: 'default',
isDebug: false,
isFlat: false,
copyMetadata: false
})
player._sendRespawn()
player.sendSelfPosition()
player.updateHealth(20)
player.nearbyEntities = []
player.updateAndSpawn()
})
}
})

player._sendRespawn = function (newDifficulty, newGameMode, newDimension) {
player._client.write('respawn', {
previousGameMode: player.prevGameMode,
dimension: serv.registry.loginPacket?.dimension || 0,
worldName: serv.dimensionNames[newDimension || 0],
difficulty: newDifficulty ?? serv.difficulty,
hashedSeed: serv.hashedSeed,
gamemode: newGameMode ?? player.gameMode,
levelType: 'default',
isDebug: false,
isFlat: false,
copyMetadata: false,
portalCooldown: 0 // 1.20
})
}
}
6 changes: 4 additions & 2 deletions src/lib/plugins/signs.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ module.exports.server = (serv, { version }) => {
}

player._client.write('open_sign_entity', {
location: placedPosition
location: placedPosition,
isFrontText: true // 1.20 allows sign text both sides, unsupported atm
})

const data = serv.setBlockDataProperties(block.defaultState - block.minStateId, block.states, properties)
Expand All @@ -32,7 +33,8 @@ module.exports.server = (serv, { version }) => {
if (direction === 0) return { id: -1, data: 0 }

player._client.write('open_sign_entity', {
location: placedPosition
location: placedPosition,
isFrontText: true // 1.20 allows sign text both sides, unsupported atm
})

if (direction === 1) {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/plugins/spawn.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ module.exports.entity = function (entity, serv) {
entity.bornTime = Date.now()
serv.entities[entity.id] = entity

if (entity.type === 'player') entity.spawnPacketName = 'named_entity_spawn'
if (serv.supportFeature('unifiedPlayerAndEntitySpawnPacket')) entity.spawnPacketName = 'spawn_entity'
else if (entity.type === 'player') entity.spawnPacketName = 'named_entity_spawn'
else if (entity.type === 'object') entity.spawnPacketName = 'spawn_entity'
else if (entity.type === 'mob') {
if (serv.supportFeature('consolidatedEntitySpawnPacket')) entity.spawnPacketName = 'spawn_entity'
Expand Down
3 changes: 2 additions & 1 deletion src/lib/plugins/updatePositions.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ module.exports.player = function (player) {
sendLook(yaw, pitch, onGround)
})

player.sendSelfPosition = () => {
player.sendSelfPosition = (newPosition) => {
if (newPosition) player.position = newPosition
// double position in all versions
player._client.write('position', {
x: player.position.x,
Expand Down
22 changes: 7 additions & 15 deletions src/lib/plugins/world.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,9 @@ module.exports.player = function (player, serv, settings) {
await playerDat.save(player, settings.worldFolder, serv.supportFeature('attributeSnakeCase'), serv.supportFeature('theFlattening'))
}

player._unloadChunk = (chunkX, chunkZ) => {
player._unloadChunk = (chunkX, chunkZ, isBecausePlayerLeft) => {
serv._worldUnloadPlayerChunk(chunkX, chunkZ, player)
if (isBecausePlayerLeft) return

if (serv.supportFeature('unloadChunkByEmptyChunk')) {
player._client.write('map_chunk', {
Expand Down Expand Up @@ -289,11 +290,11 @@ module.exports.player = function (player, serv, settings) {
player.worldSendRestOfChunks()
})

player._unloadAllChunks = () => {
player._unloadAllChunks = (isBecausePlayerLeft) => {
if (!player?.loadedChunks) return
Object.keys(player.loadedChunks)
.map((key) => key.split(',').map(a => parseInt(a)))
.forEach(([x, z]) => player._unloadChunk(x, z))
.forEach(([x, z]) => player._unloadChunk(x, z, isBecausePlayerLeft))
}

player.changeWorld = async (world, opt) => {
Expand All @@ -305,18 +306,7 @@ module.exports.player = function (player, serv, settings) {
if (opt.gamemode !== player.gameMode) player.prevGameMode = player.gameMode
player.gameMode = opt.gamemode
}
player._client.write('respawn', {
previousGameMode: player.prevGameMode,
dimension: serv.registry.loginPacket?.dimension || 0,
worldName: serv.dimensionNames[opt.dimension || 0],
difficulty: opt.difficulty || serv.difficulty,
hashedSeed: serv.hashedSeed,
gamemode: opt.gamemode || player.gameMode,
levelType: 'default',
isDebug: false,
isFlat: false,
copyMetadata: true
})
player._sendRespawn(opt.difficulty, opt.gamemode, opt.dimension)
await player.findSpawnPoint()
player.position = player.spawnPoint
player.sendSpawnPosition()
Expand All @@ -329,5 +319,7 @@ module.exports.player = function (player, serv, settings) {

await player.waitPlayerLogin()
player.worldSendRestOfChunks()
// Prevent player from falling through the world
player.sendSelfPosition(player.spawnPoint)
}
}
17 changes: 12 additions & 5 deletions src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ function onceWithTimeout (emitter, event, timeout = 10000, checkCondition) {
const timeoutId = setTimeout(() => {
reject(new Error(`Timeout waiting for '${event}' event`))
}, timeout)
emitter.once(event, (data) => {
if (checkCondition && !checkCondition(data)) return
function onEvent (...data) {
if (checkCondition && !checkCondition(...data)) return
clearTimeout(timeoutId)
resolve(data)
})
resolve([...data])
emitter.off(event, onEvent)
}
emitter.on(event, onEvent)
})
}

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

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

module.exports = {
onceWithTimeout,
skipMcPrefix,
emitAsync
emitAsync,
sleep
}
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', '1.18', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.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', '1.18', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.2']
module.exports = {
testedVersions,
latestSupportedVersion: testedVersions[testedVersions.length - 1],
Expand Down
Loading
Loading