From 690c7be16f48fbf47ec260c0dca98b0629682627 Mon Sep 17 00:00:00 2001 From: Holly Date: Mon, 4 Sep 2023 01:43:11 +0100 Subject: [PATCH] Update WorldSaveManager to support multiple dimensions and add the nether --- server/MinecraftServer.ts | 47 +++++++++++++++++++++------ server/World.ts | 13 +++++--- server/WorldSaveManager.ts | 63 +++++++++++++++++++++++-------------- server/blocks/Block.ts | 2 ++ server/entities/Player.ts | 4 +-- server/generators/Hilly.ts | 20 +++--------- server/generators/Nether.ts | 61 +++++++++++++++++++++++++++++++++++ server/mulberry32.ts | 10 ++++++ 8 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 server/generators/Nether.ts create mode 100644 server/mulberry32.ts diff --git a/server/MinecraftServer.ts b/server/MinecraftServer.ts index d6ca29b..3c6d5bb 100644 --- a/server/MinecraftServer.ts +++ b/server/MinecraftServer.ts @@ -19,6 +19,8 @@ import { WorldSaveManager } from "./WorldSaveManager"; import { World } from "./World"; import { Chunk } from "./Chunk"; import { PacketTimeUpdate } from "./packets/TimeUpdate"; +import { HillyGenerator } from "./generators/Hilly"; +import { NetherGenerator } from "./generators/Nether"; export class MinecraftServer { private static readonly PROTOCOL_VERSION = 14; @@ -34,6 +36,7 @@ export class MinecraftServer { private worlds:FunkyArray; public saveManager:WorldSaveManager; private overworld:World; + private nether:World; // https://stackoverflow.com/a/7616484 // Good enough for the world seed. @@ -103,13 +106,14 @@ export class MinecraftServer { 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); + this.saveManager = new WorldSaveManager(this.config, [0, -1], worldSeed); if (this.saveManager.worldSeed !== Number.MIN_VALUE) { worldSeed = this.saveManager.worldSeed; } this.worlds = new FunkyArray(); - this.worlds.set(0, this.overworld = new World(this.saveManager, worldSeed)); + this.worlds.set(0, this.overworld = new World(this.saveManager, 0, worldSeed, new HillyGenerator(worldSeed))); + this.worlds.set(-1, this.nether = new World(this.saveManager, -1, worldSeed, new NetherGenerator(worldSeed))); // Generate spawn area (overworld) /*(async () => { @@ -122,16 +126,37 @@ export class MinecraftServer { } Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`); }).bind(this)();*/ + let chunksGenerated = 0; (async () => { const generateStartTime = Date.now(); - Console.printInfo("Generating spawn area..."); - for (let x = -5; x < 5; x++) { - for (let z = -5; z < 5; z++) { + let timer = Date.now(); + Console.printInfo("Generating spawn area for DIM0..."); + for (let x = -10; x < 10; x++) { + for (let z = -10; z < 10; z++) { const chunk = await this.overworld.getChunkSafe(x, z); chunk.forceLoaded = true; - } + chunksGenerated++; + if (Date.now() - timer >= 1000) { + Console.printInfo(`Progress [${chunksGenerated}/400] ${((chunksGenerated / 400) * 100).toFixed(2)}%`); + timer = Date.now(); + } + } + } + chunksGenerated = 0; + Console.printInfo("Generating spawn area for DIM-1..."); + for (let x = -10; x < 10; x++) { + for (let z = -10; z < 10; z++) { + const chunk = await this.nether.getChunkSafe(x, z); + chunk.forceLoaded = true; + chunksGenerated++; + if (Date.now() - timer >= 1000) { + Console.printInfo(`Progress [${chunksGenerated}/400] ${((chunksGenerated / 400) * 100).toFixed(2)}%`); + timer = Date.now(); + } + } } Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`); + this.initServer(); }).bind(this)(); this.serverClock = setInterval(() => { @@ -155,7 +180,10 @@ export class MinecraftServer { this.server = new Server(); this.server.on("connection", this.onConnection.bind(this)); - this.server.listen(config.port, () => Console.printInfo(`Minecraft server started at ${config.port}`)); + } + + initServer() { + this.server.listen(this.config.port, () => Console.printInfo(`Minecraft server started at ${this.config.port}`)); } sendToAllClients(buffer:Buffer) { @@ -180,7 +208,8 @@ export class MinecraftServer { return; } - const world = this.worlds.get(0); + const dimension = 0; + const world = this.worlds.get(dimension); if (world instanceof World) { const clientEntity = new Player(this, world, loginPacket.username); world.addEntity(clientEntity); @@ -192,7 +221,7 @@ export class MinecraftServer { this.sendChatMessage(`\u00a7e${loginPacket.username} joined the game`); - socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, 0).writeData()); + socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, dimension).writeData()); socket.write(new PacketSpawnPosition(8, 64, 8).writeData()); const thisPlayerSpawn = new PacketNamedEntitySpawn(clientEntity.entityId, clientEntity.username, clientEntity.absX, clientEntity.absY, clientEntity.absZ, clientEntity.absYaw, clientEntity.absPitch, 0).writeData(); diff --git a/server/World.ts b/server/World.ts index ab0338c..5c550e3 100644 --- a/server/World.ts +++ b/server/World.ts @@ -13,9 +13,10 @@ import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate"; export class World { public static ENTITY_MAX_SEND_DISTANCE = 50; - private static READ_CHUNKS_FROM_DISK = true; + private static READ_CHUNKS_FROM_DISK = false; private readonly saveManager; + private readonly chunksOnDisk:Array; public chunks:FunkyArray; public entites:FunkyArray; @@ -25,15 +26,19 @@ export class World { public queuedUpdates:Array; public generator:IGenerator; - public constructor(saveManager:WorldSaveManager, seed:number) { + 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.queuedChunkBlocks = new Array(); this.queuedUpdates = new Array(); - this.generator = new HillyGenerator(seed); + this.generator = generator; } public addEntity(entity:IEntity) { @@ -86,7 +91,7 @@ export class World { const coordPair = Chunk.CreateCoordPair(x, z); const existingChunk = this.chunks.get(coordPair); if (!(existingChunk instanceof Chunk)) { - if (World.READ_CHUNKS_FROM_DISK && this.saveManager.chunksOnDisk.includes(coordPair)) { + 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 { diff --git a/server/WorldSaveManager.ts b/server/WorldSaveManager.ts index cfc16bb..a747a5c 100644 --- a/server/WorldSaveManager.ts +++ b/server/WorldSaveManager.ts @@ -1,14 +1,16 @@ -import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync } from "fs"; +import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync, renameSync } from "fs"; import { createWriter, createReader, Endian } from "bufferstuff"; import { Config } from "../config"; import { Chunk } from "./Chunk"; import { SaveCompressionType } from "./enums/SaveCompressionType"; import { deflate, inflate } from "zlib"; import { World } from "./World"; +import { FunkyArray } from "../funkyArray"; +import { Console } from "hsconsole"; export class WorldSaveManager { private readonly worldFolderPath; - private readonly worldChunksFolderPath; + private readonly oldWorldChunksFolderPath; private readonly worldPlayerDataFolderPath; private readonly infoFilePath; @@ -18,13 +20,13 @@ export class WorldSaveManager { public worldLastLoadDate = new Date(); public worldSeed = Number.MIN_VALUE; - public chunksOnDisk:Array; + public chunksOnDisk:FunkyArray>; - public constructor(config:Config, numericalSeed:number) { - this.chunksOnDisk = new Array(); + public constructor(config:Config, dimensions:Array, numericalSeed:number) { + this.chunksOnDisk = new FunkyArray>(); this.worldFolderPath = `./${config.worldName}`; - this.worldChunksFolderPath = `${this.worldFolderPath}/chunks`; + this.oldWorldChunksFolderPath = `${this.worldFolderPath}/chunks`; this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`; this.infoFilePath = `${this.worldFolderPath}/info.hwd`; @@ -43,14 +45,20 @@ export class WorldSaveManager { this.createInfoFile(numericalSeed); } - if (!existsSync(this.worldChunksFolderPath)) { - mkdirSync(this.worldChunksFolderPath); - } else { - const chunkFiles = readdirSync(this.worldChunksFolderPath); - for (const file of chunkFiles) { - if (file.endsWith(".hwc")) { - const name = file.split(".")[0]; - this.chunksOnDisk.push(parseInt(name.startsWith("-") ? name.replace("-", "-0x") : `0x${name}`)); + for (const dimension of dimensions) { + const chunksArray = new Array(); + this.chunksOnDisk.set(dimension, chunksArray); + + const chunkFolderPath = `${this.worldFolderPath}/DIM${dimension}`; + if (!existsSync(chunkFolderPath)) { + mkdirSync(chunkFolderPath); + } else { + const chunkFiles = readdirSync(chunkFolderPath); + for (const file of chunkFiles) { + if (file.endsWith(".hwc")) { + const name = file.split(".")[0]; + chunksArray.push(parseInt(name.startsWith("-") ? name.replace("-", "-0x") : `0x${name}`)); + } } } } @@ -63,7 +71,7 @@ export class WorldSaveManager { private createInfoFile(numericalSeed:number) { const infoFileWriter = createWriter(Endian.BE, 26); infoFileWriter.writeUByte(0xFD); // Info File Magic - infoFileWriter.writeUByte(0); // File Version + infoFileWriter.writeUByte(1); // File Version infoFileWriter.writeLong(this.worldCreationDate.getTime()); // World creation date infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date infoFileWriter.writeLong(numericalSeed); @@ -78,10 +86,17 @@ export class WorldSaveManager { } const fileVersion = infoFileReader.readByte(); - if (fileVersion === 0) { + if (fileVersion === 0 || fileVersion === 1) { this.worldCreationDate = new Date(Number(infoFileReader.readLong())); infoFileReader.readLong(); // Last load time is currently ignored this.worldSeed = Number(infoFileReader.readLong()); + + // Upgrade v0 to v1 + if (fileVersion === 0) { + Console.printInfo("Upgrading world to format v1 from v0"); + renameSync(`${this.worldFolderPath}/chunks`, `${this.worldFolderPath}/DIM0`); + this.createInfoFile(this.worldSeed); + } } } @@ -103,14 +118,16 @@ export class WorldSaveManager { .writeBuffer(chunk.getBlockLightBuffer()) .writeBuffer(chunk.getSkyLightBuffer()).toBuffer(); + const codArr = this.chunksOnDisk.get(chunk.world.dimension); if (saveType === SaveCompressionType.NONE) { chunkFileWriter.writeInt(chunkData.length); // Data length chunkFileWriter.writeBuffer(chunkData); // Chunk data - writeFile(`${this.worldChunksFolderPath}/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => { + writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => { const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z); - if (!this.chunksOnDisk.includes(cPair)) { - this.chunksOnDisk.push(cPair); + + if (!codArr?.includes(cPair)) { + codArr?.push(cPair); } resolve(true); @@ -124,10 +141,10 @@ export class WorldSaveManager { chunkFileWriter.writeInt(data.length); chunkFileWriter.writeBuffer(data); - writeFile(`${this.worldChunksFolderPath}/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => { + writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => { const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z); - if (!this.chunksOnDisk.includes(cPair)) { - this.chunksOnDisk.push(cPair); + if (!codArr?.includes(cPair)) { + codArr?.push(cPair); } //console.log(`Wrote ${chunk.x},${chunk.z} to disk`); resolve(true); @@ -141,7 +158,7 @@ export class WorldSaveManager { readChunkFromDisk(world:World, x:number, z:number) { return new Promise((resolve, reject) => { - readFile(`${this.worldChunksFolderPath}/${Chunk.CreateCoordPair(x, z).toString(16)}.hwc`, (err, data) => { + readFile(`${this.worldFolderPath}/DIM${world.dimension}/${Chunk.CreateCoordPair(x, z).toString(16)}.hwc`, (err, data) => { if (err) { return reject(err); } diff --git a/server/blocks/Block.ts b/server/blocks/Block.ts index 25c15dd..7d124ea 100644 --- a/server/blocks/Block.ts +++ b/server/blocks/Block.ts @@ -90,4 +90,6 @@ export class Block { static readonly flowerRose = new Block(38).setLightPassage(255).setBlockName("Rose"); static readonly clay = new Block(82).setBlockName("Clay"); + + static readonly netherrack = new Block(87).setBlockName("Netherrack"); } \ No newline at end of file diff --git a/server/entities/Player.ts b/server/entities/Player.ts index fa69abc..a8cad27 100644 --- a/server/entities/Player.ts +++ b/server/entities/Player.ts @@ -47,8 +47,8 @@ export class Player extends EntityLiving { // Load or keep any chunks we need const currentLoads = []; - for (let x = bitX - 6; x < bitX + 6; x++) { - for (let z = bitZ - 6; z < bitZ + 6; z++) { + for (let x = bitX - 10; x < bitX + 10; x++) { + for (let z = bitZ - 10; z < bitZ + 10; z++) { const coordPair = Chunk.CreateCoordPair(x, z); if (!this.loadedChunks.includes(coordPair)) { const chunk = await this.world.getChunkSafe(x, z); diff --git a/server/generators/Hilly.ts b/server/generators/Hilly.ts index 3e3c9e9..5e3488d 100644 --- a/server/generators/Hilly.ts +++ b/server/generators/Hilly.ts @@ -4,6 +4,7 @@ import { IGenerator } from "./IGenerator"; import { Noise2D, makeNoise2D } from "../../external/OpenSimplex2D"; import { Noise3D, makeNoise3D } from "../../external/OpenSimplex3D"; import { QueuedBlockUpdate } from "../queuedUpdateTypes/BlockUpdate"; +import mulberry32 from "../mulberry32"; export class HillyGenerator implements IGenerator { private seed:number; @@ -31,7 +32,7 @@ export class HillyGenerator implements IGenerator { public constructor(seed:number) { this.seed = seed; - this.seedGenerator = this.mulberry32(this.seed); + this.seedGenerator = mulberry32(this.seed); this.generator = this.createGenerator2D(); this.generator1 = this.createGenerator2D(); @@ -67,21 +68,10 @@ export class HillyGenerator implements IGenerator { return num >= 0.5 ? (num | 0) + 1 : num | 0; } - // https://stackoverflow.com/a/47593316 - // This is good enough (and fast enough) for what is needed here. - private mulberry32(a:number) { - return function() { - let t = a += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - return ((t ^ t >>> 14) >>> 0) / 4294967296; - } - } - public generate(chunk:Chunk) { - const treeRNG = this.mulberry32(this.seed + chunk.x + chunk.z); - const grassRNG = this.mulberry32(this.seed + chunk.x + chunk.z); - const flowerRNG = this.mulberry32(this.seed + chunk.x + chunk.z); + const treeRNG = mulberry32(this.seed + chunk.x + chunk.z); + const grassRNG = mulberry32(this.seed + chunk.x + chunk.z); + const flowerRNG = mulberry32(this.seed + chunk.x + chunk.z); let colY = 0, colDirtMin = 0, colWaterY = 0, orgColY = 0; for (let x = 0; x < 16; x++) { diff --git a/server/generators/Nether.ts b/server/generators/Nether.ts new file mode 100644 index 0000000..50d9544 --- /dev/null +++ b/server/generators/Nether.ts @@ -0,0 +1,61 @@ +import { Block } from "../blocks/Block"; +import { Chunk } from "../Chunk"; +import { IGenerator } from "./IGenerator"; +import { Noise2D, makeNoise2D } from "../../external/OpenSimplex2D"; +import { Noise3D, makeNoise3D } from "../../external/OpenSimplex3D"; +import { QueuedBlockUpdate } from "../queuedUpdateTypes/BlockUpdate"; +import mulberry32 from "../mulberry32"; + +export class NetherGenerator implements IGenerator { + private seed:number; + seedGenerator:() => number; + + private generator:Noise3D; + private generator1:Noise3D; + private generator2:Noise3D; + private generator3:Noise3D; + private generator4:Noise3D; + private generator5:Noise3D; + + public constructor(seed:number) { + this.seed = seed; + this.seedGenerator = mulberry32(this.seed); + + this.generator = this.createGenerator3D(); + this.generator1 = this.createGenerator3D(); + this.generator2 = this.createGenerator3D(); + this.generator3 = this.createGenerator3D(); + this.generator4 = this.createGenerator3D(); + this.generator5 = this.createGenerator3D(); + } + + private createGenerator2D() { + return makeNoise2D(this.seedGenerator() * Number.MAX_SAFE_INTEGER); + } + + private createGenerator3D() { + return makeNoise3D(this.seedGenerator() * Number.MAX_SAFE_INTEGER); + } + + public generate(chunk:Chunk) { + for (let x = 0; x < 16; x++) { + for (let z = 0; z < 16; z++) { + for (let y = 0; y < 128; y++) { + if (y === 0) { + chunk.setBlock(Block.bedrock.blockId, x, y, z); + continue; + } + + const layer1 = (this.generator((chunk.x * 16 + x) / 32, y / 32, (chunk.z * 16 + z) / 32) + this.generator1((chunk.x * 16 + x) / 32, y / 32, (chunk.z * 16 + z) / 32)) / 2; + const layer2 = (this.generator2((chunk.x * 16 + x) / 128, y / 128, (chunk.z * 16 + z) / 128) + this.generator3((chunk.x * 16 + x) / 128, y / 128, (chunk.z * 16 + z) / 128)) / 2; + const layer3 = (this.generator4((chunk.x * 16 + x) / 16, y / 16, (chunk.z * 16 + z) / 16) + this.generator5((chunk.x * 16 + x) / 16, y / 16, (chunk.z * 16 + z) / 16)) / 2; + if ((layer1 + layer2 + layer3) / 3 >= 0.1) { + chunk.setBlock(Block.netherrack.blockId, x, y, z); + } else if (y < 10) { + chunk.setBlock(Block.lavaStill.blockId, x, y, z); + } + } + } + } + } +} \ No newline at end of file diff --git a/server/mulberry32.ts b/server/mulberry32.ts new file mode 100644 index 0000000..806fd4f --- /dev/null +++ b/server/mulberry32.ts @@ -0,0 +1,10 @@ +// https://stackoverflow.com/a/47593316 +// this is good enough (and fast enough) for what i need +export default function mulberry32(a:number) { + return function() { + let t = a += 0x6D2B79F5; + t = Math.imul(t ^ t >>> 15, t | 1); + t ^= t + Math.imul(t ^ t >>> 7, t | 61); + return ((t ^ t >>> 14) >>> 0) / 4294967296; + } +} \ No newline at end of file