import { Endian, createWriter } from "bufferstuff"; import AABB from "./AABB"; import Block from "./blocks/Block"; import Chunk from "./Chunk"; import EntityItem from "./entities/EntityItem"; import FunkyArray from "funky-array"; import IEntity from "./entities/IEntity"; import IGenerator from "./generators/IGenerator"; import IQueuedUpdate from "./queuedUpdateTypes/IQueuedUpdate"; import PacketBlockChange from "./packets/BlockChange"; import PacketDestroyEntity from "./packets/DestroyEntity"; import PacketPickupSpawn from "./packets/PickupSpawn"; import Player from "./entities/Player"; import QueuedBlockUpdate from "./queuedUpdateTypes/BlockUpdate"; import Random from "./Random"; import WorldSaveManager from "./WorldSaveManager"; export default class World { public static ENTITY_MAX_SEND_DISTANCE = 50; private static READ_CHUNKS_FROM_DISK = true; private readonly saveManager; private readonly chunksOnDisk:Array; public chunks:FunkyArray; public entites:FunkyArray; public players:FunkyArray; public playerHitboxes:FunkyArray; public queuedChunkBlocks:Array; public queuedUpdates:Array; public generator:IGenerator; public random:Random = new Random(); public readonly dimension:number; public constructor(saveManager:WorldSaveManager, dimension:number, seed:number, generator:IGenerator) { this.dimension = dimension; this.saveManager = saveManager; this.chunksOnDisk = this.saveManager.chunksOnDisk.get(dimension) ?? new Array; this.chunks = new FunkyArray(); this.entites = new FunkyArray(); this.players = new FunkyArray(); this.playerHitboxes = new FunkyArray(); this.queuedChunkBlocks = new Array(); this.queuedUpdates = new Array(); this.generator = generator; } public addEntity(entity:IEntity) { this.entites.set(entity.entityId, entity); this.playerHitboxes.set(entity.entityId, entity.entityAABB); if (entity instanceof Player) { this.players.set(entity.entityId, entity); } else if (entity instanceof EntityItem) { const packet = new PacketPickupSpawn(entity.entityId, entity.itemStack.itemID, entity.itemStack.size, entity.itemStack.damage, Math.round(entity.position.x * 32), Math.round(entity.position.y * 32), Math.round(entity.position.z * 32), 0, 0, 0).writeData(); entity.sendToNearby(packet); } } // TODO: getChunkByCoordPair failed in here during removeEntity, figure out why. public removeEntity(entity:IEntity) { if (entity instanceof Player) { for (const coordPair of entity.loadedChunks) { if (this.chunkExists(coordPair)) { const chunk = this.getChunkByCoordPair(coordPair); chunk.playersInChunk.remove(entity.entityId); if (!chunk.forceLoaded && chunk.playersInChunk.length === 0) { this.unloadChunk(coordPair); } } } // Clear player chunk list (they may be switching dimensions) entity.loadedChunks = new Array(); entity.justUnloaded = new Array(); this.playerHitboxes.remove(entity.entityId); this.players.remove(entity.entityId); if (!entity.isDead) { const writer = createWriter(Endian.BE); entity.toSave(writer); this.saveManager.writePlayerSaveToDisk(entity.username, writer); } } this.entites.remove(entity.entityId); this.sendToNearbyClients(entity, new PacketDestroyEntity(entity.entityId).writeData()); } public chunkExists(coordPairOrX:number, chunkZ?:number) { if (typeof(coordPairOrX) === "number" && typeof(chunkZ) === "number") { return this.chunks.has(Chunk.CreateCoordPair(coordPairOrX, chunkZ)); } return this.chunks.has(coordPairOrX); } public getChunk(x:number, z:number) { const coordPair = Chunk.CreateCoordPair(x, z); const existingChunk = this.chunks.get(coordPair); if (!(existingChunk instanceof Chunk)) { throw new Error(`BADLOOKUP: Chunk [${x}, ${z}] does not exist.`); } return existingChunk; } public getChunkSafe(x:number, z:number) { return new Promise((resolve) => { const coordPair = Chunk.CreateCoordPair(x, z); const existingChunk = this.chunks.get(coordPair); if (!(existingChunk instanceof Chunk)) { if (World.READ_CHUNKS_FROM_DISK && this.chunksOnDisk.includes(coordPair)) { return this.saveManager.readChunkFromDisk(this, x, z) .then(chunk => resolve(this.chunks.set(coordPair, chunk))); } else { resolve(this.chunks.set(coordPair, new Chunk(this, x, z, true))); if (World.READ_CHUNKS_FROM_DISK) { this.saveManager.writeChunkToDisk(this.getChunk(x, z)); } return; } } resolve(existingChunk); }); } public getChunkByCoordPair(coordPair:number) { const existingChunk = this.chunks.get(coordPair); if (!(existingChunk instanceof Chunk)) { throw new Error(`BADLOOKUP: Chunk ${coordPair} does not exist.`); } return existingChunk; } public getBlockId(x:number, y:number, z:number) { const chunkX = x >> 4, chunkZ = z >> 4; return this.getChunk(chunkX, chunkZ).getBlockId(x & 0xf, y, z & 0xf); } public getChunkBlockId(chunk:Chunk, x:number, y:number, z:number) { return chunk.getBlockId(x & 0xf, y, z & 0xf); } public getBlockMetadata(x:number, y:number, z:number) { const chunkX = x >> 4, chunkZ = z >> 4; return this.getChunk(chunkX, chunkZ).getBlockMetadata(x & 0xf, y, z & 0xf); } public getBlockLight(x:number, y:number, z:number) { const chunkX = x >> 4, chunkZ = z >> 4; return this.getChunk(chunkX, chunkZ).getBlockLight(x & 0xf, y, z & 0xf); } public getSkyLight(x:number, y:number, z:number) { const chunkX = x >> 4, chunkZ = z >> 4; return this.getChunk(chunkX, chunkZ).getSkyLight(x & 0xf, y, z & 0xf); } public setBlock(blockId:number, x:number, y:number, z:number, doBlockUpdate?:boolean) { const chunkX = x >> 4, chunkZ = z >> 4; const chunk = this.getChunk(chunkX, chunkZ); chunk.setBlockWithMetadata(blockId, 0, x & 0xf, y, z & 0xf); if (doBlockUpdate) { const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, 0).writeData(); // Send block update to all players that have this chunk loaded chunk.playersInChunk.forEach(player => { player.mpClient?.send(blockUpdatePacket); }); } } public setBlockWithMetadata(blockId:number, metadata:number, x:number, y:number, z:number, doBlockUpdate?:boolean) { const chunkX = x >> 4, chunkZ = z >> 4; const chunk = this.getChunk(chunkX, chunkZ); chunk.setBlockWithMetadata(blockId, metadata, x & 0xf, y, z & 0xf); if (doBlockUpdate) { const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, metadata).writeData(); // Send block update to all players that have this chunk loaded chunk.playersInChunk.forEach(player => { player.mpClient?.send(blockUpdatePacket); }); } } public setBlockMetadata(x:number, y:number, z:number, metadata:number, doBlockUpdate?:boolean) { const chunkX = x >> 4, chunkZ = z >> 4; const chunk = this.getChunk(chunkX, chunkZ); const xc = x & 0xf, zc = z & 0xf; chunk.setBlockMetadata(metadata, xc, y, zc); if (doBlockUpdate) { const blockId = chunk.getBlockId(xc, y, zc); const blockUpdatePacket = new PacketBlockChange(x, y, z, blockId, metadata).writeData(); // Send block update to all players that have this chunk loaded chunk.playersInChunk.forEach(player => { player.mpClient?.send(blockUpdatePacket); }); } } public setBlockWithNotify(x:number, y:number, z:number, blockId:number) { this.setBlock(blockId, x, y, z, true); this.notifyNeighborBlocksOfChange(x, y, z, blockId); } public setBlockMetadataWithNotify(x:number, y:number, z:number, metadata:number) { this.setBlockMetadata(x, y, z, metadata, true); this.notifyNeighborBlocksOfChange(x, y, z, this.getBlockId(x, y, z)); } public setBlockAndMetadataWithNotify(x:number, y:number, z:number, blockId:number, metadata:number) { this.setBlockWithMetadata(blockId, metadata, x, y, z, true); this.notifyNeighborBlocksOfChange(x, y, z, blockId); } public notifyNeighborBlocksOfChange(x:number, y:number, z:number, blockId:number) { this.notifyNeighborBlockOfChange(x - 1, y, z, blockId); this.notifyNeighborBlockOfChange(x + 1, y, z, blockId); this.notifyNeighborBlockOfChange(x, y - 1, z, blockId); this.notifyNeighborBlockOfChange(x, y + 1, z, blockId); this.notifyNeighborBlockOfChange(x, y, z - 1, blockId); this.notifyNeighborBlockOfChange(x, y, z + 1, blockId); } private notifyNeighborBlockOfChange(x:number, y:number, z:number, blockId:number) { const block = Block.blocks[this.getBlockId(x, y, z)]; if (block != null && block.blockId !== 0) { block.neighborBlockChange(this, x, y, z, block.blockId); } } public sendToNearbyClients(sentFrom:IEntity, buffer:Buffer) { this.players.forEach(player => { if (sentFrom.entityId !== player.entityId && Math.abs(sentFrom.distanceTo(player)) < World.ENTITY_MAX_SEND_DISTANCE) { player.mpClient?.send(buffer); } }); } public sendToNearbyAllNearbyClients(sentFrom:IEntity, buffer:Buffer) { this.players.forEach(player => { if (Math.abs(sentFrom.distanceTo(player)) < World.ENTITY_MAX_SEND_DISTANCE) { player.mpClient?.send(buffer); } }); } public async unloadChunk(coordPair:number) { const chunk = this.getChunkByCoordPair(coordPair); if (!chunk.savingToDisk) { chunk.savingToDisk = true; await this.saveManager.writeChunkToDisk(chunk); if (chunk.playersInChunk.length === 0) { this.chunks.remove(coordPair); return; } // A player loaded the chunk while we were, flushing to disk. // Keep it loaded. chunk.savingToDisk = false; } } public tick() { if (this.queuedUpdates.length > 0) { for (let i = this.queuedUpdates.length - 1; i >= 0; i--) { const update = this.queuedUpdates[i]; if (update instanceof QueuedBlockUpdate) { if (this.chunks.keys.includes(update.coordPair)) { this.queuedUpdates.splice(i, 1); const thatChunk = this.getChunkByCoordPair(update.coordPair); thatChunk.setBlockWithMetadata(update.blockId, update.metadata, update.x, update.y, update.z); if (thatChunk.playersInChunk.length > 0) { const blockUpdate = new PacketBlockChange((thatChunk.x << 4) + update.x, update.y, (thatChunk.z << 4) + update.z, update.blockId, update.metadata).writeData() thatChunk.playersInChunk.forEach(player => { player.mpClient?.send(blockUpdate); }); } } } } } this.entites.forEach(entity => { entity.onTick(); if (entity instanceof Player) { if (entity.justUnloaded.length > 0) { for (const coordPair of entity.justUnloaded) { if (this.chunks.get(coordPair) != undefined) { const chunkToUnload = this.getChunkByCoordPair(coordPair); chunkToUnload.playersInChunk.remove(entity.entityId); if (!chunkToUnload.forceLoaded && chunkToUnload.playersInChunk.length === 0) { this.unloadChunk(coordPair); } } } entity.justUnloaded = new Array(); } } if (entity.markedForDisposal) { this.removeEntity(entity); } }) } }