diff --git a/.gitignore b/.gitignore index 692a327..cd483d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ build/ -bundle/ \ No newline at end of file +bundle/ +world/ \ No newline at end of file diff --git a/bufferStuff.ts b/bufferStuff.ts index 6338cb1..2116cf3 100644 --- a/bufferStuff.ts +++ b/bufferStuff.ts @@ -7,6 +7,12 @@ export class Reader { this.offset = 0; } + public readBuffer(bytes:number) { + const value = this.buffer.subarray(this.offset, this.offset + bytes); + this.offset += bytes; + return value; + } + public readByte() { const value = this.buffer.readInt8(this.offset); this.offset++; diff --git a/config.json b/config.json index a88c081..a65479e 100644 --- a/config.json +++ b/config.json @@ -2,5 +2,7 @@ "port": 25565, "onlineMode": false, "maxPlayers": 20, - "seed": "really janky" + "seed": "really janky", + "saveCompression": "DEFLATE", + "worldName": "world" } \ No newline at end of file diff --git a/config.ts b/config.ts index 3404424..c1fcff2 100644 --- a/config.ts +++ b/config.ts @@ -3,4 +3,6 @@ export interface Config { onlineMode: boolean, maxPlayers: number, seed: number|string, + worldName: string, + saveCompression: "NONE" } \ No newline at end of file diff --git a/nibbleArray.ts b/nibbleArray.ts new file mode 100644 index 0000000..622b2b2 --- /dev/null +++ b/nibbleArray.ts @@ -0,0 +1,45 @@ +export class NibbleArray { + private array:Uint8Array; + + public constructor(size:number|ArrayBuffer|Uint8Array) { + if (size instanceof ArrayBuffer) { + this.array = new Uint8Array(size); + } else if (size instanceof Uint8Array) { + this.array = new Uint8Array(size); + } else { + this.array = new Uint8Array(Math.round(size / 2)); + } + } + + // We can determine which side of the byte to read + // from if the halved index has a remainder. + private isLowOrHighNibble(index:number) { + return index % 1 !== 0; + } + + public get(index:number) { + index = index / 2; + + const arrayIndex = index | 0; + if (this.isLowOrHighNibble(index)) { + return this.array[arrayIndex] >> 4; + } else { + return this.array[arrayIndex] & 0x0f; + } + } + + public set(index:number, value:number) { + index = index / 2; + + const arrayIndex = index | 0; + if (this.isLowOrHighNibble(index)) { + this.array[arrayIndex] = value << 4 | this.array[arrayIndex] & 0xf; + } else { + this.array[arrayIndex] = this.array[arrayIndex] & 0xf0 | value; + } + } + + public toBuffer() { + return Buffer.from(this.array); + } +} \ No newline at end of file diff --git a/server/Chunk.ts b/server/Chunk.ts index 69b44f0..06383d7 100644 --- a/server/Chunk.ts +++ b/server/Chunk.ts @@ -1,4 +1,5 @@ import { FunkyArray } from "../funkyArray"; +import { NibbleArray } from "../nibbleArray"; import { Player } from "./entities/Player"; import { World } from "./World"; @@ -9,31 +10,65 @@ export class Chunk { public readonly z:number; public readonly playersInChunk:FunkyArray; + public savingToDisk:boolean = false; + public forceLoaded:boolean = false; + private blocks:Uint8Array; + private metadata:NibbleArray; public static CreateCoordPair(x:number, z:number) { return (x >= 0 ? 0 : 2147483648) | (x & 0x7fff) << 16 | (z >= 0 ? 0 : 0x8000) | z & 0x7fff; } - public constructor(world:World, x:number, z:number) { + public constructor(world:World, x:number, z:number, generateOrBlockData?:boolean|ArrayBuffer, metadata?:ArrayBuffer) { this.world = world; this.x = x; this.z = z; this.playersInChunk = new FunkyArray(); - this.blocks = new Uint8Array(16 * 16 * this.MAX_HEIGHT); + if (generateOrBlockData instanceof ArrayBuffer && metadata instanceof ArrayBuffer) { + this.blocks = new Uint8Array(generateOrBlockData); + this.metadata = new NibbleArray(metadata); + } else { + this.blocks = new Uint8Array(16 * 16 * this.MAX_HEIGHT); + this.metadata = new NibbleArray(16 * 16 * this.MAX_HEIGHT); - this.world.generator.generate(this); + if (generateOrBlockData) { + this.world.generator.generate(this); + } + } } public setBlock(blockId:number, x:number, y:number, z:number) { + if (x < 0 || x > 15 || y < 0 || y > 127 || z < 0 || z > 15) { + return; + } + this.blocks[x << 11 | z << 7 | y] = blockId; } + public setBlockWithMetadata(blockId:number, metadata:number, x:number, y:number, z:number) { + if (x < 0 || x > 15 || y < 0 || y > 127 || z < 0 || z > 15) { + return; + } + x = x << 11 | z << 7 | y; + + this.blocks[x] = blockId; + this.metadata.set(x, metadata); + } + public getBlockId(x:number, y:number, z:number) { return this.blocks[x << 11 | z << 7 | y]; } + public getBlockMetadata(x:number, y:number, z:number) { + return this.metadata.get(x << 11 | z << 7 | y); + } + + public getMetadataBuffer() { + return this.metadata.toBuffer(); + } + public getData() { return this.blocks; } diff --git a/server/MinecraftServer.ts b/server/MinecraftServer.ts index cfc716d..37ee378 100644 --- a/server/MinecraftServer.ts +++ b/server/MinecraftServer.ts @@ -15,6 +15,7 @@ import { PacketSpawnPosition } from "./packets/SpawnPosition"; import { PacketPlayerPositionLook } from "./packets/PlayerPositionLook"; import { PacketChat } from "./packets/Chat"; import { PacketNamedEntitySpawn } from "./packets/NamedEntitySpawn"; +import { WorldSaveManager } from "./WorldSaveManager"; export class MinecraftServer { private static readonly PROTOCOL_VERSION = 14; @@ -28,6 +29,7 @@ export class MinecraftServer { private tickCounter:number = 0; private clients:FunkyArray; private worlds:FunkyArray; + public saveManager:WorldSaveManager; private overworld:World; // https://stackoverflow.com/a/7616484 @@ -48,27 +50,38 @@ export class MinecraftServer { public constructor(config:Config) { this.config = config; + if (this.config.saveCompression === "NONE") { + Console.printWarn("=============- WARNING -============="); + Console.printWarn(" Chunk compression is disabled. This"); + Console.printWarn(" will lead to large file sizes!"); + Console.printWarn("====================================="); + } + this.clients = new FunkyArray(); // Convert seed if needed - const worldSeed = typeof(this.config.seed) === "string" ? this.hashCode(this.config.seed) : this.config.seed; + let worldSeed = typeof(this.config.seed) === "string" ? this.hashCode(this.config.seed) : this.config.seed; + + // Init save manager and load seed from it if possible + this.saveManager = new WorldSaveManager(this.config, worldSeed); + if (this.saveManager.worldSeed !== Number.MIN_VALUE) { + worldSeed = this.saveManager.worldSeed; + } this.worlds = new FunkyArray(); - this.worlds.set(0, this.overworld = new World(worldSeed)); + this.worlds.set(0, this.overworld = new World(this.saveManager, worldSeed)); // Generate spawn area (overworld) - const generateStartTime = Date.now(); - Console.printInfo("Generating spawn area..."); - let generatedCount = 0; - for (let x = -3; x < 3; x++) { - for (let z = -3; z < 3; z++) { - this.overworld.getChunk(x, z); - if (generatedCount++ % 5 === 0) { - Console.printInfo(`Generating spawn area... ${Math.floor(generatedCount / 36 * 100)}%`); - } - } - } - Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`); + (async () => { + const generateStartTime = Date.now(); + Console.printInfo("Generating spawn area..."); + for (let x = -3; x < 3; x++) { + for (let z = -3; z < 3; z++) { + await this.overworld.getChunkSafe(x, z); + } + } + Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`); + }).bind(this)(); this.serverClock = setInterval(() => { // Every 1 sec diff --git a/server/World.ts b/server/World.ts index 196f545..efc6afd 100644 --- a/server/World.ts +++ b/server/World.ts @@ -1,5 +1,6 @@ import { FunkyArray } from "../funkyArray"; import { Chunk } from "./Chunk"; +import { WorldSaveManager } from "./WorldSaveManager"; import { IEntity } from "./entities/IEntity"; import { Player } from "./entities/Player"; //import { FlatGenerator } from "./generators/Flat"; @@ -10,13 +11,17 @@ import { PacketBlockChange } from "./packets/BlockChange"; export class World { public static ENTITY_MAX_SEND_DISTANCE = 50; + private readonly saveManager; + public chunks:FunkyArray; public entites:FunkyArray; public players:FunkyArray; public generator:IGenerator; - public constructor(seed:number) { + public constructor(saveManager:WorldSaveManager, seed:number) { + this.saveManager = saveManager; + this.chunks = new FunkyArray(); this.entites = new FunkyArray(); this.players = new FunkyArray(); @@ -37,7 +42,7 @@ export class World { const chunk = this.getChunkByCoordPair(coordPair); chunk.playersInChunk.remove(entity.entityId); - if (chunk.playersInChunk.length === 0) { + if (!chunk.forceLoaded && chunk.playersInChunk.length === 0) { this.unloadChunk(coordPair); } } @@ -52,11 +57,36 @@ export class World { const coordPair = Chunk.CreateCoordPair(x, z); const existingChunk = this.chunks.get(coordPair); if (!(existingChunk instanceof Chunk)) { - if (generate) { - return this.chunks.set(coordPair, new Chunk(this, x, z)); + throw new Error(`BADLOOKUP: Chunk [${x}, ${z}] does not exist.`); + } + + return existingChunk; + } + + public getChunkSafe(x:number, z:number) { + return new Promise((resolve, reject) => { + const coordPair = Chunk.CreateCoordPair(x, z); + const existingChunk = this.chunks.get(coordPair); + if (!(existingChunk instanceof Chunk)) { + if (this.saveManager.chunksOnDisk.includes(coordPair)) { + return this.saveManager.readChunkFromDisk(this, x, z) + .then(chunk => { + //console.log("Loaded " + x + "," + z + " from disk"); + resolve(this.chunks.set(coordPair, chunk)); + }); + } else { + return resolve(this.chunks.set(coordPair, new Chunk(this, x, z, true))); + } } - throw new Error(`BADLOOKUP: Chunk [${x}, ${z}] does not exist.`); + 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; @@ -93,18 +123,22 @@ export class World { }); } - public getChunkByCoordPair(coordPair:number) { - const existingChunk = this.chunks.get(coordPair); - if (!(existingChunk instanceof Chunk)) { - throw new Error(`BADLOOKUP: Chunk ${coordPair} does not exist.`); + 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; } - - return existingChunk; - } - - public unloadChunk(coordPair:number) { - // TODO: Save to disk - this.chunks.remove(coordPair); } public tick() { @@ -116,7 +150,7 @@ export class World { for (const coordPair of entity.justUnloaded) { const chunkToUnload = this.getChunkByCoordPair(coordPair); chunkToUnload.playersInChunk.remove(entity.entityId); - if (chunkToUnload.playersInChunk.length === 0) { + if (!chunkToUnload.forceLoaded && chunkToUnload.playersInChunk.length === 0) { this.unloadChunk(coordPair); } } diff --git a/server/WorldSaveManager.ts b/server/WorldSaveManager.ts new file mode 100644 index 0000000..d6c4d98 --- /dev/null +++ b/server/WorldSaveManager.ts @@ -0,0 +1,174 @@ +import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync } from "fs"; +import { Reader, Writer } from "../bufferStuff"; +import { Config } from "../config"; +import { Chunk } from "./Chunk"; +import { SaveCompressionType } from "./enums/SaveCompressionType"; +import { deflate, inflate } from "zlib"; +import { World } from "./World"; + +export class WorldSaveManager { + private readonly worldFolderPath; + private readonly worldChunksFolderPath; + private readonly worldPlayerDataFolderPath; + private readonly infoFilePath; + + private readonly config:Config; + + public worldCreationDate = new Date(); + public worldLastLoadDate = new Date(); + public worldSeed = Number.MIN_VALUE; + + public chunksOnDisk:Array; + + public constructor(config:Config, numericalSeed:number) { + this.chunksOnDisk = new Array(); + + this.worldFolderPath = `./${config.worldName}`; + this.worldChunksFolderPath = `${this.worldFolderPath}/chunks`; + this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`; + this.infoFilePath = `${this.worldFolderPath}/info.hwd`; + + this.config = config; + + // Create world folder if it doesn't exist + if (!existsSync(this.worldFolderPath)) { + mkdirSync(this.worldFolderPath); + } + + if (existsSync(this.infoFilePath)) { + this.readInfoFile(); + } else { + // World info file does not exist + this.worldSeed = numericalSeed; + this.createInfoFile(numericalSeed); + } + + if (!existsSync(this.worldChunksFolderPath)) { + mkdirSync(this.worldChunksFolderPath); + } else { + const chunkFiles = readdirSync(this.worldChunksFolderPath); + for (let file of chunkFiles) { + if (file.endsWith(".hwc")) { + const numbers = file.split(".")[0].split(","); + this.chunksOnDisk.push(Chunk.CreateCoordPair(parseInt(numbers[0]), parseInt(numbers[1]))); + } + } + } + + if (!existsSync(this.worldPlayerDataFolderPath)) { + mkdirSync(this.worldPlayerDataFolderPath); + } + } + + private createInfoFile(numericalSeed:number) { + const infoFileWriter = new Writer(26); + infoFileWriter.writeUByte(0xFD); // Info File Magic + infoFileWriter.writeUByte(0); // File Version + infoFileWriter.writeLong(this.worldCreationDate.getTime()); // World creation date + infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date + infoFileWriter.writeLong(numericalSeed); + writeFileSync(this.infoFilePath, infoFileWriter.toBuffer()); + } + + private readInfoFile() { + const infoFileReader = new Reader(readFileSync(this.infoFilePath)); + const fileMagic = infoFileReader.readUByte(); + if (fileMagic !== 0xFD) { + throw new Error("World info file is invalid"); + } + + const fileVersion = infoFileReader.readByte(); + if (fileVersion === 0) { + this.worldCreationDate = new Date(Number(infoFileReader.readLong())); + infoFileReader.readLong(); // Last load time is currently ignored + this.worldSeed = Number(infoFileReader.readLong()); + } + } + + public writeChunkToDisk(chunk:Chunk) { + return new Promise((resolve, reject) => { + const saveType = SaveCompressionType[this.config.saveCompression]; + const chunkFileWriter = new Writer(10); + chunkFileWriter.writeUByte(0xFC); // Chunk File Magic + chunkFileWriter.writeUByte(0); // File Version + chunkFileWriter.writeUByte(saveType); // Save compression type + chunkFileWriter.writeUByte(16); // Chunk X + chunkFileWriter.writeUByte(128); // Chunk Y + chunkFileWriter.writeUByte(16); // Chunk Z + + const chunkData = new Writer().writeBuffer(Buffer.from(chunk.getData())).writeBuffer(chunk.getMetadataBuffer()).toBuffer(); + + if (saveType === SaveCompressionType.NONE) { + chunkFileWriter.writeInt(chunkData.length); // Data length + chunkFileWriter.writeBuffer(chunkData); // Chunk data + + writeFile(`${this.worldChunksFolderPath}/${chunk.x},${chunk.z}.hwc`, chunkFileWriter.toBuffer(), () => { + resolve(true); + }); + } else if (saveType === SaveCompressionType.DEFLATE) { + deflate(chunkData, (err, data) => { + if (err) { + return reject(err); + } + + chunkFileWriter.writeInt(data.length); + chunkFileWriter.writeBuffer(data); + + writeFile(`${this.worldChunksFolderPath}/${chunk.x},${chunk.z}.hwc`, chunkFileWriter.toBuffer(), () => { + const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z); + if (!this.chunksOnDisk.includes(cPair)) { + this.chunksOnDisk.push(cPair); + } + //console.log(`Wrote ${chunk.x},${chunk.z} to disk`); + resolve(true); + }); + }) + } else if (saveType === SaveCompressionType.XZ) { + // TODO: Implement XZ chunk saving + } + }); + } + + readChunkFromDisk(world:World, x:number, z:number) { + return new Promise((resolve, reject) => { + readFile(`${this.worldChunksFolderPath}/${x},${z}.hwc`, (err, data) => { + if (err) { + return reject(err); + } + + const chunkFileReader = new Reader(data); + + // Check file validity + if (chunkFileReader.readUByte() !== 0xFC) { + return reject(new Error("Chunk file is invalid")); + } + + const fileVersion = chunkFileReader.readUByte(); + if (fileVersion === 0) { + const saveCompressionType:SaveCompressionType = chunkFileReader.readUByte(); + const chunkX = chunkFileReader.readUByte(); + const chunkY = chunkFileReader.readUByte(); + const chunkZ = chunkFileReader.readUByte(); + const totalByteSize = chunkX * chunkZ * chunkY; + + const contentLength = chunkFileReader.readInt(); + if (saveCompressionType === SaveCompressionType.NONE) { + const chunkData = new Reader(chunkFileReader.readBuffer(contentLength)); + const chunk = new Chunk(world, x, z, chunkData.readBuffer(totalByteSize).buffer, chunkData.readBuffer(totalByteSize / 2).buffer); + resolve(chunk); + } else if (saveCompressionType === SaveCompressionType.DEFLATE) { + inflate(chunkFileReader.readBuffer(contentLength), (err, data) => { + if (err) { + return reject(err); + } + + const chunkData = new Reader(data); + const chunk = new Chunk(world, x, z, chunkData.readBuffer(totalByteSize).buffer, chunkData.readBuffer(totalByteSize / 2).buffer); + resolve(chunk); + }); + } + } + }); + }); + } +} \ No newline at end of file diff --git a/server/blocks/Block.ts b/server/blocks/Block.ts index 48badfe..a3998ec 100644 --- a/server/blocks/Block.ts +++ b/server/blocks/Block.ts @@ -10,18 +10,15 @@ export class Block { static readonly grass = new Block(2); static readonly dirt = new Block(3); - - static readonly bedrock = new Block(7); static readonly waterStill = new Block(9); - - - - - + static readonly sand = new Block(12); + static readonly gravel = new Block(13); static readonly wood = new Block(17); static readonly leaves = new Block(18); + + static readonly clay = new Block(82); } \ No newline at end of file diff --git a/server/entities/Player.ts b/server/entities/Player.ts index 48b7d5c..c2e46d7 100644 --- a/server/entities/Player.ts +++ b/server/entities/Player.ts @@ -27,7 +27,7 @@ export class Player extends EntityLiving { this.z = 8; } - onTick() { + private async updatePlayerChunks() { const bitX = this.x >> 4; const bitZ = this.z >> 4; if (bitX != this.lastX >> 4 || bitZ != this.lastZ >> 4 || this.firstUpdate) { @@ -35,11 +35,9 @@ export class Player extends EntityLiving { this.firstUpdate = false; // TODO: Make this based on the player's coords this.mpClient?.send(new PacketPreChunk(0, 0, true).writeData()); - const chunk = this.world.getChunk(0, 0); - (async () => { - const chunkData = await (new PacketMapChunk(0, 0, 0, 15, 127, 15, chunk).writeData()); - this.mpClient?.send(chunkData); - })(); + const chunk = await this.world.getChunkSafe(0, 0); + const chunkData = await (new PacketMapChunk(0, 0, 0, 15, 127, 15, chunk).writeData()); + this.mpClient?.send(chunkData); } // Load or keep any chunks we need @@ -48,14 +46,12 @@ export class Player extends EntityLiving { for (let z = bitZ - 6; z < bitZ + 6; z++) { const coordPair = Chunk.CreateCoordPair(x, z); if (!this.loadedChunks.includes(coordPair)) { - const chunk = this.world.getChunk(x, z); + const chunk = await this.world.getChunkSafe(x, z); this.mpClient?.send(new PacketPreChunk(x, z, true).writeData()); this.loadedChunks.push(coordPair); chunk.playersInChunk.set(this.entityId, this); - (async () => { - const chunkData = await (new PacketMapChunk(x, 0, z, 15, 127, 15, chunk).writeData()); - this.mpClient?.send(chunkData); - })(); + const chunkData = await (new PacketMapChunk(x, 0, z, 15, 127, 15, chunk).writeData()); + this.mpClient?.send(chunkData); } currentLoads.push(coordPair); } @@ -73,6 +69,10 @@ export class Player extends EntityLiving { // Overwrite loaded chunks this.loadedChunks = currentLoads; } + } + + public onTick() { + this.updatePlayerChunks(); super.onTick(); } diff --git a/server/enums/SaveCompressionType.ts b/server/enums/SaveCompressionType.ts new file mode 100644 index 0000000..4c70312 --- /dev/null +++ b/server/enums/SaveCompressionType.ts @@ -0,0 +1,5 @@ +export enum SaveCompressionType { + NONE = 0, + DEFLATE = 1, + XZ = 2 +} \ No newline at end of file diff --git a/server/generators/Hilly.ts b/server/generators/Hilly.ts index 2bc51ab..0cd2a2e 100644 --- a/server/generators/Hilly.ts +++ b/server/generators/Hilly.ts @@ -5,6 +5,8 @@ import { Noise2D, makeNoise2D } from "../../external/OpenSimplex2D"; export class HillyGenerator implements IGenerator { private seed:number; + seedGenerator:() => number; + private generator:Noise2D; private generator1:Noise2D; private generator2:Noise2D; @@ -14,20 +16,35 @@ export class HillyGenerator implements IGenerator { private generator6:Noise2D; private oceanGenerator:Noise2D; private mountainGenerator:Noise2D; + private underwaterGravelGenerator:Noise2D; + private underwaterSandGenerator:Noise2D; + private underwaterClayGenerator:Noise2D; public constructor(seed:number) { this.seed = seed; + this.seedGenerator = this.mulberry32(this.seed); - const generatorSeed = this.mulberry32(this.seed); - this.generator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator1 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator2 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator3 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator4 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator5 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.generator6 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.oceanGenerator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); - this.mountainGenerator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); + this.generator = this.createGenerator(); + this.generator1 = this.createGenerator(); + this.generator2 = this.createGenerator(); + this.generator3 = this.createGenerator(); + this.generator4 = this.createGenerator(); + this.generator5 = this.createGenerator(); + this.generator6 = this.createGenerator(); + this.oceanGenerator = this.createGenerator(); + this.mountainGenerator = this.createGenerator(); + this.underwaterGravelGenerator = this.createGenerator(); + this.underwaterSandGenerator = this.createGenerator(); + this.underwaterClayGenerator = this.createGenerator(); + } + + private createGenerator() { + return makeNoise2D(this.seedGenerator() * Number.MAX_SAFE_INTEGER); + } + + // This is soooo much faster than using Math.round in here + private fastRound(num:number) { + return num >= 0.5 ? (num | 0) + 1 : num | 0; } // https://stackoverflow.com/a/47593316 @@ -47,7 +64,7 @@ export class HillyGenerator implements IGenerator { for (let x = 0; x < 16; x++) { for (let z = 0; z < 16; z++) { const oceanValue = this.oceanGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128) * 100; - orgColY = colWaterY = colY = 60 + ( + orgColY = colWaterY = colY = 60 + this.fastRound(( this.generator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 + this.generator1((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 + this.generator2((chunk.x * 16 + x) / 8, (chunk.z * 16 + z) / 8) * 8 + @@ -57,9 +74,14 @@ export class HillyGenerator implements IGenerator { this.generator6((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 + oceanValue + (Math.max(this.mountainGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128), 0) * 50 + Math.min(oceanValue, 0)) - ) / 9; + ) / 9); colDirtMin = colY - 2; - chunk.setBlock(Block.grass.blockId, x, colY, z); + const sandNoise = this.underwaterSandGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16); + if (colY === 59 && sandNoise > 0.5) { + chunk.setBlock(Block.sand.blockId, x, colY, z); + } else { + chunk.setBlock(Block.grass.blockId, x, colY, z); + } while (colY-- > 0) { if (colY >= colDirtMin) { @@ -71,29 +93,55 @@ export class HillyGenerator implements IGenerator { } } + // Generate underwater blocks if (colWaterY <= 58) { - chunk.setBlock(Block.dirt.blockId, x, colWaterY, z); + if (this.underwaterGravelGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) > 0.3) { + chunk.setBlock(Block.gravel.blockId, x, colWaterY, z); + } else if (sandNoise > 0.4) { + chunk.setBlock(Block.sand.blockId, x, colWaterY, z); + } else if (this.underwaterClayGenerator((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) > 0.5) { + chunk.setBlock(Block.clay.blockId, x, colWaterY, z); + } else { + chunk.setBlock(Block.dirt.blockId, x, colWaterY, z); + } } while (colWaterY <= 58) { colWaterY++; chunk.setBlock(Block.waterStill.blockId, x, colWaterY, z); } + // TODO: Move trees to it's own generator if (chunk.getBlockId(x, orgColY + 1, z) !== Block.waterStill.blockId && chunk.getBlockId(x, orgColY, z) === Block.grass.blockId && treeRNG() > 0.995) { + const treeType = treeRNG() >= 0.5; chunk.setBlock(Block.dirt.blockId, x, orgColY, z); - let tY = orgColY + 1; - while (tY < orgColY + 5) { - chunk.setBlock(Block.wood.blockId, x, tY, z); + let tYT = 0, tY = tYT = orgColY + 4 + this.fastRound(treeRNG() - 0.2), tLY = 0; + while (tY > orgColY) { + chunk.setBlockWithMetadata(Block.wood.blockId, treeType ? 2 : 0, x, tY, z); + if (tLY !== 0 && tLY < 3) { + for (let tX = -2; tX <= 2; tX++) { + for (let tZ = -2; tZ <= 2; tZ++) { + if (tX === 0 && tZ === 0) { + continue; + } + chunk.setBlockWithMetadata(Block.leaves.blockId, treeType ? 2 : 0, x + tX, tY, z + tZ); + } + } + } + tY--; + tLY++; + } + tY = 0; + while (tY < 2) { + for (let tX = -1; tX < 2; tX++) { + for (let tZ = -1; tZ < 2; tZ++) { + if (tX === 0 && tZ === 0 && tY !== 1) { + continue; + } + chunk.setBlockWithMetadata(Block.leaves.blockId, treeType ? 2 : 0, x + tX, tYT + tY, z + tZ); + } + } tY++; } - chunk.setBlock(Block.leaves.blockId, x - 1, tY - 2, z); - chunk.setBlock(Block.leaves.blockId, x + 1, tY - 2, z); - chunk.setBlock(Block.leaves.blockId, x, tY - 2, z - 1); - chunk.setBlock(Block.leaves.blockId, x, tY - 2, z + 1); - chunk.setBlock(Block.leaves.blockId, x - 2, tY - 2, z); - chunk.setBlock(Block.leaves.blockId, x + 2, tY - 2, z); - chunk.setBlock(Block.leaves.blockId, x, tY - 2, z - 2); - chunk.setBlock(Block.leaves.blockId, x, tY - 2, z + 2); } } } diff --git a/server/packets/MapChunk.ts b/server/packets/MapChunk.ts index 45dcf5a..5467955 100644 --- a/server/packets/MapChunk.ts +++ b/server/packets/MapChunk.ts @@ -34,7 +34,6 @@ export class PacketMapChunk implements IPacket { public writeData() { return new Promise((resolve, reject) => { const blocks = new Writer(32768); - const metadata = new Writer(16384); const lighting = new Writer(32768); let blockMeta = false; @@ -43,7 +42,6 @@ export class PacketMapChunk implements IPacket { for (let y = 0; y < 128; y++) { blocks.writeUByte(this.chunk.getBlockId(x, y, z)); if (blockMeta) { - metadata.writeUByte(0); // Light level 15 for 2 blocks (1111 1111) lighting.writeUByte(0xff); // TODO: Lighting (Client seems to do it's own (when a block update happens) so it's not top priority) lighting.writeUByte(0xff); @@ -55,7 +53,7 @@ export class PacketMapChunk implements IPacket { } // Write meta and lighting data into block buffer for compression - blocks.writeBuffer(metadata.toBuffer()).writeBuffer(lighting.toBuffer()); + blocks.writeBuffer(this.chunk.getMetadataBuffer()).writeBuffer(lighting.toBuffer()); deflate(blocks.toBuffer(), (err, data) => { if (err) {