wip saving to disk / chunk async

This commit is contained in:
Holly Stubbs 2023-04-11 07:47:56 +01:00
parent 42cef0a838
commit 860c8f4866
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
14 changed files with 442 additions and 82 deletions

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
node_modules/ node_modules/
build/ build/
bundle/ bundle/
world/

View file

@ -7,6 +7,12 @@ export class Reader {
this.offset = 0; this.offset = 0;
} }
public readBuffer(bytes:number) {
const value = this.buffer.subarray(this.offset, this.offset + bytes);
this.offset += bytes;
return value;
}
public readByte() { public readByte() {
const value = this.buffer.readInt8(this.offset); const value = this.buffer.readInt8(this.offset);
this.offset++; this.offset++;

View file

@ -2,5 +2,7 @@
"port": 25565, "port": 25565,
"onlineMode": false, "onlineMode": false,
"maxPlayers": 20, "maxPlayers": 20,
"seed": "really janky" "seed": "really janky",
"saveCompression": "DEFLATE",
"worldName": "world"
} }

View file

@ -3,4 +3,6 @@ export interface Config {
onlineMode: boolean, onlineMode: boolean,
maxPlayers: number, maxPlayers: number,
seed: number|string, seed: number|string,
worldName: string,
saveCompression: "NONE"
} }

45
nibbleArray.ts Normal file
View file

@ -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);
}
}

View file

@ -1,4 +1,5 @@
import { FunkyArray } from "../funkyArray"; import { FunkyArray } from "../funkyArray";
import { NibbleArray } from "../nibbleArray";
import { Player } from "./entities/Player"; import { Player } from "./entities/Player";
import { World } from "./World"; import { World } from "./World";
@ -9,31 +10,65 @@ export class Chunk {
public readonly z:number; public readonly z:number;
public readonly playersInChunk:FunkyArray<number, Player>; public readonly playersInChunk:FunkyArray<number, Player>;
public savingToDisk:boolean = false;
public forceLoaded:boolean = false;
private blocks:Uint8Array; private blocks:Uint8Array;
private metadata:NibbleArray;
public static CreateCoordPair(x:number, z:number) { public static CreateCoordPair(x:number, z:number) {
return (x >= 0 ? 0 : 2147483648) | (x & 0x7fff) << 16 | (z >= 0 ? 0 : 0x8000) | z & 0x7fff; 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.world = world;
this.x = x; this.x = x;
this.z = z; this.z = z;
this.playersInChunk = new FunkyArray<number, Player>(); this.playersInChunk = new FunkyArray<number, Player>();
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) { 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; 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) { public getBlockId(x:number, y:number, z:number) {
return this.blocks[x << 11 | z << 7 | y]; 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() { public getData() {
return this.blocks; return this.blocks;
} }

View file

@ -15,6 +15,7 @@ import { PacketSpawnPosition } from "./packets/SpawnPosition";
import { PacketPlayerPositionLook } from "./packets/PlayerPositionLook"; import { PacketPlayerPositionLook } from "./packets/PlayerPositionLook";
import { PacketChat } from "./packets/Chat"; import { PacketChat } from "./packets/Chat";
import { PacketNamedEntitySpawn } from "./packets/NamedEntitySpawn"; import { PacketNamedEntitySpawn } from "./packets/NamedEntitySpawn";
import { WorldSaveManager } from "./WorldSaveManager";
export class MinecraftServer { export class MinecraftServer {
private static readonly PROTOCOL_VERSION = 14; private static readonly PROTOCOL_VERSION = 14;
@ -28,6 +29,7 @@ export class MinecraftServer {
private tickCounter:number = 0; private tickCounter:number = 0;
private clients:FunkyArray<string, MPClient>; private clients:FunkyArray<string, MPClient>;
private worlds:FunkyArray<number, World>; private worlds:FunkyArray<number, World>;
public saveManager:WorldSaveManager;
private overworld:World; private overworld:World;
// https://stackoverflow.com/a/7616484 // https://stackoverflow.com/a/7616484
@ -48,27 +50,38 @@ export class MinecraftServer {
public constructor(config:Config) { public constructor(config:Config) {
this.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<string, MPClient>(); this.clients = new FunkyArray<string, MPClient>();
// Convert seed if needed // 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<number, World>(); this.worlds = new FunkyArray<number, World>();
this.worlds.set(0, this.overworld = new World(worldSeed)); this.worlds.set(0, this.overworld = new World(this.saveManager, worldSeed));
// Generate spawn area (overworld) // Generate spawn area (overworld)
const generateStartTime = Date.now(); (async () => {
Console.printInfo("Generating spawn area..."); const generateStartTime = Date.now();
let generatedCount = 0; Console.printInfo("Generating spawn area...");
for (let x = -3; x < 3; x++) { for (let x = -3; x < 3; x++) {
for (let z = -3; z < 3; z++) { for (let z = -3; z < 3; z++) {
this.overworld.getChunk(x, z); await this.overworld.getChunkSafe(x, z);
if (generatedCount++ % 5 === 0) { }
Console.printInfo(`Generating spawn area... ${Math.floor(generatedCount / 36 * 100)}%`); }
} Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
} }).bind(this)();
}
Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
this.serverClock = setInterval(() => { this.serverClock = setInterval(() => {
// Every 1 sec // Every 1 sec

View file

@ -1,5 +1,6 @@
import { FunkyArray } from "../funkyArray"; import { FunkyArray } from "../funkyArray";
import { Chunk } from "./Chunk"; import { Chunk } from "./Chunk";
import { WorldSaveManager } from "./WorldSaveManager";
import { IEntity } from "./entities/IEntity"; import { IEntity } from "./entities/IEntity";
import { Player } from "./entities/Player"; import { Player } from "./entities/Player";
//import { FlatGenerator } from "./generators/Flat"; //import { FlatGenerator } from "./generators/Flat";
@ -10,13 +11,17 @@ import { PacketBlockChange } from "./packets/BlockChange";
export class World { export class World {
public static ENTITY_MAX_SEND_DISTANCE = 50; public static ENTITY_MAX_SEND_DISTANCE = 50;
private readonly saveManager;
public chunks:FunkyArray<number, Chunk>; public chunks:FunkyArray<number, Chunk>;
public entites:FunkyArray<number, IEntity>; public entites:FunkyArray<number, IEntity>;
public players:FunkyArray<number, Player>; public players:FunkyArray<number, Player>;
public generator:IGenerator; public generator:IGenerator;
public constructor(seed:number) { public constructor(saveManager:WorldSaveManager, seed:number) {
this.saveManager = saveManager;
this.chunks = new FunkyArray<number, Chunk>(); this.chunks = new FunkyArray<number, Chunk>();
this.entites = new FunkyArray<number, IEntity>(); this.entites = new FunkyArray<number, IEntity>();
this.players = new FunkyArray<number, Player>(); this.players = new FunkyArray<number, Player>();
@ -37,7 +42,7 @@ export class World {
const chunk = this.getChunkByCoordPair(coordPair); const chunk = this.getChunkByCoordPair(coordPair);
chunk.playersInChunk.remove(entity.entityId); chunk.playersInChunk.remove(entity.entityId);
if (chunk.playersInChunk.length === 0) { if (!chunk.forceLoaded && chunk.playersInChunk.length === 0) {
this.unloadChunk(coordPair); this.unloadChunk(coordPair);
} }
} }
@ -52,11 +57,36 @@ export class World {
const coordPair = Chunk.CreateCoordPair(x, z); const coordPair = Chunk.CreateCoordPair(x, z);
const existingChunk = this.chunks.get(coordPair); const existingChunk = this.chunks.get(coordPair);
if (!(existingChunk instanceof Chunk)) { if (!(existingChunk instanceof Chunk)) {
if (generate) { throw new Error(`BADLOOKUP: Chunk [${x}, ${z}] does not exist.`);
return this.chunks.set(coordPair, new Chunk(this, x, z)); }
return existingChunk;
}
public getChunkSafe(x:number, z:number) {
return new Promise<Chunk>((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; return existingChunk;
@ -93,18 +123,22 @@ export class World {
}); });
} }
public getChunkByCoordPair(coordPair:number) { public async unloadChunk(coordPair:number) {
const existingChunk = this.chunks.get(coordPair); const chunk = this.getChunkByCoordPair(coordPair);
if (!(existingChunk instanceof Chunk)) { if (!chunk.savingToDisk) {
throw new Error(`BADLOOKUP: Chunk ${coordPair} does not exist.`); 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() { public tick() {
@ -116,7 +150,7 @@ export class World {
for (const coordPair of entity.justUnloaded) { for (const coordPair of entity.justUnloaded) {
const chunkToUnload = this.getChunkByCoordPair(coordPair); const chunkToUnload = this.getChunkByCoordPair(coordPair);
chunkToUnload.playersInChunk.remove(entity.entityId); chunkToUnload.playersInChunk.remove(entity.entityId);
if (chunkToUnload.playersInChunk.length === 0) { if (!chunkToUnload.forceLoaded && chunkToUnload.playersInChunk.length === 0) {
this.unloadChunk(coordPair); this.unloadChunk(coordPair);
} }
} }

174
server/WorldSaveManager.ts Normal file
View file

@ -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<number>;
public constructor(config:Config, numericalSeed:number) {
this.chunksOnDisk = new Array<number>();
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<boolean>((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<Chunk>((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);
});
}
}
});
});
}
}

View file

@ -10,18 +10,15 @@ export class Block {
static readonly grass = new Block(2); static readonly grass = new Block(2);
static readonly dirt = new Block(3); static readonly dirt = new Block(3);
static readonly bedrock = new Block(7); static readonly bedrock = new Block(7);
static readonly waterStill = new Block(9); 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 wood = new Block(17);
static readonly leaves = new Block(18); static readonly leaves = new Block(18);
static readonly clay = new Block(82);
} }

View file

@ -27,7 +27,7 @@ export class Player extends EntityLiving {
this.z = 8; this.z = 8;
} }
onTick() { private async updatePlayerChunks() {
const bitX = this.x >> 4; const bitX = this.x >> 4;
const bitZ = this.z >> 4; const bitZ = this.z >> 4;
if (bitX != this.lastX >> 4 || bitZ != this.lastZ >> 4 || this.firstUpdate) { if (bitX != this.lastX >> 4 || bitZ != this.lastZ >> 4 || this.firstUpdate) {
@ -35,11 +35,9 @@ export class Player extends EntityLiving {
this.firstUpdate = false; this.firstUpdate = false;
// TODO: Make this based on the player's coords // TODO: Make this based on the player's coords
this.mpClient?.send(new PacketPreChunk(0, 0, true).writeData()); this.mpClient?.send(new PacketPreChunk(0, 0, true).writeData());
const chunk = this.world.getChunk(0, 0); const chunk = await this.world.getChunkSafe(0, 0);
(async () => { const chunkData = await (new PacketMapChunk(0, 0, 0, 15, 127, 15, chunk).writeData());
const chunkData = await (new PacketMapChunk(0, 0, 0, 15, 127, 15, chunk).writeData()); this.mpClient?.send(chunkData);
this.mpClient?.send(chunkData);
})();
} }
// Load or keep any chunks we need // 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++) { for (let z = bitZ - 6; z < bitZ + 6; z++) {
const coordPair = Chunk.CreateCoordPair(x, z); const coordPair = Chunk.CreateCoordPair(x, z);
if (!this.loadedChunks.includes(coordPair)) { 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.mpClient?.send(new PacketPreChunk(x, z, true).writeData());
this.loadedChunks.push(coordPair); this.loadedChunks.push(coordPair);
chunk.playersInChunk.set(this.entityId, this); chunk.playersInChunk.set(this.entityId, this);
(async () => { const chunkData = await (new PacketMapChunk(x, 0, z, 15, 127, 15, chunk).writeData());
const chunkData = await (new PacketMapChunk(x, 0, z, 15, 127, 15, chunk).writeData()); this.mpClient?.send(chunkData);
this.mpClient?.send(chunkData);
})();
} }
currentLoads.push(coordPair); currentLoads.push(coordPair);
} }
@ -73,6 +69,10 @@ export class Player extends EntityLiving {
// Overwrite loaded chunks // Overwrite loaded chunks
this.loadedChunks = currentLoads; this.loadedChunks = currentLoads;
} }
}
public onTick() {
this.updatePlayerChunks();
super.onTick(); super.onTick();
} }

View file

@ -0,0 +1,5 @@
export enum SaveCompressionType {
NONE = 0,
DEFLATE = 1,
XZ = 2
}

View file

@ -5,6 +5,8 @@ import { Noise2D, makeNoise2D } from "../../external/OpenSimplex2D";
export class HillyGenerator implements IGenerator { export class HillyGenerator implements IGenerator {
private seed:number; private seed:number;
seedGenerator:() => number;
private generator:Noise2D; private generator:Noise2D;
private generator1:Noise2D; private generator1:Noise2D;
private generator2:Noise2D; private generator2:Noise2D;
@ -14,20 +16,35 @@ export class HillyGenerator implements IGenerator {
private generator6:Noise2D; private generator6:Noise2D;
private oceanGenerator:Noise2D; private oceanGenerator:Noise2D;
private mountainGenerator:Noise2D; private mountainGenerator:Noise2D;
private underwaterGravelGenerator:Noise2D;
private underwaterSandGenerator:Noise2D;
private underwaterClayGenerator:Noise2D;
public constructor(seed:number) { public constructor(seed:number) {
this.seed = seed; this.seed = seed;
this.seedGenerator = this.mulberry32(this.seed);
const generatorSeed = this.mulberry32(this.seed); this.generator = this.createGenerator();
this.generator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator1 = this.createGenerator();
this.generator1 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator2 = this.createGenerator();
this.generator2 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator3 = this.createGenerator();
this.generator3 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator4 = this.createGenerator();
this.generator4 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator5 = this.createGenerator();
this.generator5 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.generator6 = this.createGenerator();
this.generator6 = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.oceanGenerator = this.createGenerator();
this.oceanGenerator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); this.mountainGenerator = this.createGenerator();
this.mountainGenerator = makeNoise2D(generatorSeed() * Number.MAX_SAFE_INTEGER); 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 // https://stackoverflow.com/a/47593316
@ -47,7 +64,7 @@ export class HillyGenerator implements IGenerator {
for (let x = 0; x < 16; x++) { for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) { for (let z = 0; z < 16; z++) {
const oceanValue = this.oceanGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128) * 100; 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.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.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 + 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 + this.generator6((chunk.x * 16 + x) / 16, (chunk.z * 16 + z) / 16) * 16 +
oceanValue + oceanValue +
(Math.max(this.mountainGenerator((chunk.x * 16 + x) / 128, (chunk.z * 16 + z) / 128), 0) * 50 + Math.min(oceanValue, 0)) (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; 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) { while (colY-- > 0) {
if (colY >= colDirtMin) { if (colY >= colDirtMin) {
@ -71,29 +93,55 @@ export class HillyGenerator implements IGenerator {
} }
} }
// Generate underwater blocks
if (colWaterY <= 58) { 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) { while (colWaterY <= 58) {
colWaterY++; colWaterY++;
chunk.setBlock(Block.waterStill.blockId, x, colWaterY, z); 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) { 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); chunk.setBlock(Block.dirt.blockId, x, orgColY, z);
let tY = orgColY + 1; let tYT = 0, tY = tYT = orgColY + 4 + this.fastRound(treeRNG() - 0.2), tLY = 0;
while (tY < orgColY + 5) { while (tY > orgColY) {
chunk.setBlock(Block.wood.blockId, x, tY, z); 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++; 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);
} }
} }
} }

View file

@ -34,7 +34,6 @@ export class PacketMapChunk implements IPacket {
public writeData() { public writeData() {
return new Promise<Buffer>((resolve, reject) => { return new Promise<Buffer>((resolve, reject) => {
const blocks = new Writer(32768); const blocks = new Writer(32768);
const metadata = new Writer(16384);
const lighting = new Writer(32768); const lighting = new Writer(32768);
let blockMeta = false; let blockMeta = false;
@ -43,7 +42,6 @@ export class PacketMapChunk implements IPacket {
for (let y = 0; y < 128; y++) { for (let y = 0; y < 128; y++) {
blocks.writeUByte(this.chunk.getBlockId(x, y, z)); blocks.writeUByte(this.chunk.getBlockId(x, y, z));
if (blockMeta) { if (blockMeta) {
metadata.writeUByte(0);
// Light level 15 for 2 blocks (1111 1111) // 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); // TODO: Lighting (Client seems to do it's own (when a block update happens) so it's not top priority)
lighting.writeUByte(0xff); lighting.writeUByte(0xff);
@ -55,7 +53,7 @@ export class PacketMapChunk implements IPacket {
} }
// Write meta and lighting data into block buffer for compression // 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) => { deflate(blocks.toBuffer(), (err, data) => {
if (err) { if (err) {