flying-squid
Version:
A minecraft server written in node.js
320 lines (285 loc) • 10.8 kB
JavaScript
const spiralloop = require('spiralloop')
const Vec3 = require('vec3').Vec3
const generations = require('flying-squid').generations
const { promisify } = require('util')
const fs = require('fs')
const { level } = require('prismarine-provider-anvil')
const playerDat = require('../playerDat')
const fsStat = promisify(fs.stat)
const fsMkdir = promisify(fs.mkdir)
module.exports.server = async function (serv, options = {}) {
const { version, worldFolder, generation = { name: 'diamond_square', options: { worldHeight: 80 } } } = options
const World = require('prismarine-world')(version)
const registry = require('prismarine-registry')(version)
const Anvil = require('prismarine-provider-anvil').Anvil(version)
const newSeed = generation.options.seed || Math.floor(Math.random() * Math.pow(2, 31))
let seed
let regionFolder
if (worldFolder) {
regionFolder = worldFolder + '/region'
try {
await fsStat(regionFolder)
} catch (err) {
await fsMkdir(regionFolder, { recursive: true })
}
try {
const levelData = await level.readLevel(worldFolder + '/level.dat')
seed = levelData.RandomSeed[0]
} catch (err) {
seed = newSeed
await level.writeLevel(worldFolder + '/level.dat', {
RandomSeed: [seed, 0],
Version: { Name: options.version },
generatorName: generation.name === 'superflat' ? 'flat' : generation.name === 'diamond_square' ? 'default' : 'customized'
})
}
} else { seed = newSeed }
generation.options.seed = seed
generation.options.version = version
serv.emit('seed', generation.options.seed)
const generationModule = generations[generation.name] ? generations[generation.name] : require(generation.name)
serv.overworld = new World(generationModule(generation.options), regionFolder === undefined ? null : new Anvil(regionFolder))
serv.netherworld = new World(generations.nether(generation.options))
// serv.endworld = new World(generations["end"]({}));
serv.dimensionNames = {
'-1': 'minecraft:nether',
0: 'minecraft:overworld'
// 1: 'minecraft:end'
}
// WILL BE REMOVED WHEN ACTUALLY IMPLEMENTED
serv.overworld.blockEntityData = {}
serv.netherworld.blockEntityData = {}
serv.overworld.portals = []
serv.netherworld.portals = []
/// ///////////
serv.pregenWorld = (world, size = 3) => {
const promises = []
for (let x = -size; x < size; x++) {
for (let z = -size; z < size; z++) {
promises.push(world.getColumn(x, z))
}
}
return Promise.all(promises)
}
serv.setBlock = async (world, position, stateId) => {
serv.players
.filter(p => p.world === world)
.forEach(player => player.sendBlock(position, stateId))
await world.setBlockStateId(position, stateId)
if (stateId === 0) serv.notifyNeighborsOfStateChange(world, position, serv.tickCount, serv.tickCount, true)
else serv.updateBlock(world, position, serv.tickCount, serv.tickCount, true)
}
if (registry.supportFeature('theFlattening')) {
serv.setBlockType = async (world, position, id) => {
serv.setBlock(world, position, registry.blocks[id].minStateId)
}
} else {
serv.setBlockType = async (world, position, id) => {
serv.setBlock(world, position, id << 4)
}
}
serv.setBlockAction = async (world, position, actionId, actionParam) => {
const location = new Vec3(position.x, position.y, position.z)
const blockType = await world.getBlockType(location)
serv.players
.filter(p => p.world === world)
.forEach(player => player.sendBlockAction(position, actionId, actionParam, blockType))
}
serv.reloadChunks = (world, chunks) => {
serv.players
.filter(player => player.world === world)
.forEach(oPlayer => {
chunks
.filter(({ chunkX, chunkZ }) => oPlayer.loadedChunks[chunkX + ',' + chunkZ] !== undefined)
.forEach(({ chunkX, chunkZ }) => oPlayer._unloadChunk(chunkX, chunkZ))
oPlayer.sendRestMap()
})
}
serv.chunksUsed = {}
serv._loadPlayerChunk = (chunkX, chunkZ, player) => {
const id = chunkX + ',' + chunkZ
if (!serv.chunksUsed[id]) {
serv.chunksUsed[id] = 0
}
serv.chunksUsed[id]++
const loaded = player.loadedChunks[id]
if (!loaded) player.loadedChunks[id] = 1
return !loaded
}
serv._unloadPlayerChunk = (chunkX, chunkZ, player) => {
const id = chunkX + ',' + chunkZ
delete player.loadedChunks[id]
if (serv.chunksUsed[id] > 0) {
serv.chunksUsed[id]--
}
if (!serv.chunksUsed[id]) {
player.world.unloadColumn(chunkX, chunkZ)
return true
}
return false
}
// serv.pregenWorld(serv.overworld).then(() => serv.info('Pre-Generated Overworld'));
// serv.pregenWorld(serv.netherworld).then(() => serv.info('Pre-Generated Nether'));
serv.commands.add({
base: 'changeworld',
info: 'to change world',
usage: '/changeworld overworld|nether',
onlyPlayer: true,
op: true,
action (world, ctx) {
if (world === 'nether') ctx.player.changeWorld(serv.netherworld, { dimension: -1 })
if (world === 'overworld') ctx.player.changeWorld(serv.overworld, { dimension: 0 })
}
})
}
module.exports.player = function (player, serv, settings) {
const registry = require('prismarine-registry')(settings.version)
player.save = async () => {
await playerDat.save(player, settings.worldFolder, registry.supportFeature('attributeSnakeCase'), registry.supportFeature('theFlattening'))
}
player._unloadChunk = (chunkX, chunkZ) => {
serv._unloadPlayerChunk(chunkX, chunkZ, player)
if (registry.supportFeature('unloadChunkByEmptyChunk')) {
player._client.write('map_chunk', {
x: chunkX,
z: chunkZ,
groundUp: true,
bitMap: 0x0000,
chunkData: Buffer.alloc(0)
})
} else if (registry.supportFeature('unloadChunkDirect')) {
player._client.write('unload_chunk', {
chunkX,
chunkZ
})
}
}
player.sendChunk = (chunkX, chunkZ, column) => {
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: []
})
if (registry.supportFeature('lightSentSeparately')) {
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()
})
}
return Promise.resolve()
})
}
function spiral (arr) {
const t = []
spiralloop(arr, (x, z) => {
t.push([x, z])
})
return t
}
player.sendNearbyChunks = (view, group) => {
player.lastPositionChunkUpdated = player.position
const playerChunkX = Math.floor(player.position.x / 16)
const playerChunkZ = Math.floor(player.position.z / 16)
Object.keys(player.loadedChunks)
.map((key) => key.split(',').map(a => parseInt(a)))
.filter(([x, z]) => Math.abs(x - playerChunkX) > view || Math.abs(z - playerChunkZ) > view)
.forEach(([x, z]) => player._unloadChunk(x, z))
return spiral([view * 2, view * 2])
.map(t => ({
chunkX: playerChunkX + t[0] - view,
chunkZ: playerChunkZ + t[1] - view
}))
.filter(({ chunkX, chunkZ }) => serv._loadPlayerChunk(chunkX, chunkZ, player))
.reduce((acc, { chunkX, chunkZ }) => {
const p = acc
.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))
}
player.sendMap = () => {
return player.sendNearbyChunks(Math.min(3, settings['view-distance']))
.catch((err) => setTimeout(() => { throw err }), 0)
}
// todo as I understand need to handle difficulty packet instead?
player.on('playerChangeRenderDistance', (newDistance = player.view, unloadFirst = false) => {
player.view = newDistance
if (unloadFirst) player._unloadAllChunks()
player.sendRestMap()
})
player.sendRestMap = () => {
player.sendingChunks = true
player.sendNearbyChunks(Math.min(player.view, settings['view-distance']), true)
.then(() => { player.sendingChunks = false })
.catch((err) => setTimeout(() => { throw err }, 0))
}
player.sendSpawnPosition = () => {
player._client.write('spawn_position', {
location: player.spawnPoint
})
}
player._unloadAllChunks = () => {
if (!player?.loadedChunks) return
Object.keys(player.loadedChunks)
.map((key) => key.split(',').map(a => parseInt(a)))
.forEach(([x, z]) => player._unloadChunk(x, z))
}
player.changeWorld = async (world, opt) => {
if (player.world === world) return Promise.resolve()
opt = opt || {}
player.world = world
player._unloadAllChunks()
if (typeof opt.gamemode !== 'undefined') {
if (opt.gamemode !== player.gameMode) player.prevGameMode = player.gameMode
player.gameMode = opt.gamemode
}
player._client.write('respawn', {
previousGameMode: player.prevGameMode,
dimension: (registry.supportFeature('dimensionIsAString') || registry.supportFeature('dimensionIsAWorld')) ? serv.dimensionNames[opt.dimension || 0] : opt.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
})
await player.findSpawnPoint()
player.position = player.spawnPoint
player.sendSpawnPosition()
player.updateAndSpawn()
await player.sendMap()
player.sendSelfPosition()
player.emit('change_world')
await player.waitPlayerLogin()
player.sendRestMap()
}
}