Update WorldSaveManager to support multiple dimensions and add the nether
This commit is contained in:
parent
5c25dd7436
commit
690c7be16f
8 changed files with 167 additions and 53 deletions
|
@ -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<number, World>;
|
||||
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<number, World>();
|
||||
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();
|
||||
|
|
|
@ -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<number>;
|
||||
|
||||
public chunks:FunkyArray<number, Chunk>;
|
||||
public entites:FunkyArray<number, IEntity>;
|
||||
|
@ -25,15 +26,19 @@ export class World {
|
|||
public queuedUpdates:Array<IQueuedUpdate>;
|
||||
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<number>;
|
||||
|
||||
this.chunks = new FunkyArray<number, Chunk>();
|
||||
this.entites = new FunkyArray<number, IEntity>();
|
||||
this.players = new FunkyArray<number, Player>();
|
||||
this.queuedChunkBlocks = new Array<IQueuedUpdate>();
|
||||
this.queuedUpdates = new Array<IQueuedUpdate>();
|
||||
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 {
|
||||
|
|
|
@ -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<number>;
|
||||
public chunksOnDisk:FunkyArray<number, Array<number>>;
|
||||
|
||||
public constructor(config:Config, numericalSeed:number) {
|
||||
this.chunksOnDisk = new Array<number>();
|
||||
public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) {
|
||||
this.chunksOnDisk = new FunkyArray<number, Array<number>>();
|
||||
|
||||
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<number>();
|
||||
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<Chunk>((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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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++) {
|
||||
|
|
61
server/generators/Nether.ts
Normal file
61
server/generators/Nether.ts
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
server/mulberry32.ts
Normal file
10
server/mulberry32.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue