mc-beta-server/server/WorldSaveManager.ts

243 lines
9.3 KiB
TypeScript
Raw Normal View History

import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync, renameSync } from "fs";
2023-06-22 12:43:28 +01:00
import { createWriter, createReader, Endian } from "bufferstuff";
2023-04-11 07:47:56 +01:00
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";
2023-04-11 07:47:56 +01:00
export class WorldSaveManager {
private readonly worldFolderPath;
2023-11-07 20:46:17 +00:00
private readonly globalDataPath;
2023-04-11 07:47:56 +01:00
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:FunkyArray<number, Array<number>>;
2023-04-11 07:47:56 +01:00
public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) {
this.chunksOnDisk = new FunkyArray<number, Array<number>>();
2023-04-11 07:47:56 +01:00
this.worldFolderPath = `./${config.worldName}`;
this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`;
2023-11-07 20:46:17 +00:00
this.globalDataPath = `${this.worldFolderPath}/data`;
2023-04-11 07:47:56 +01:00
this.infoFilePath = `${this.worldFolderPath}/info.hwd`;
this.config = config;
// Create world folder if it doesn't exist
if (!existsSync(this.worldFolderPath)) {
mkdirSync(this.worldFolderPath);
2023-11-07 20:46:17 +00:00
mkdirSync(this.globalDataPath);
2023-04-11 07:47:56 +01:00
}
if (existsSync(this.infoFilePath)) {
this.readInfoFile();
} else {
// World info file does not exist
this.worldSeed = numericalSeed;
this.createInfoFile(numericalSeed);
}
for (const dimension of dimensions) {
const chunksArray = new Array<number>();
this.chunksOnDisk.set(dimension, chunksArray);
2023-11-07 20:46:17 +00:00
const dimensionFolderPath = `${this.worldFolderPath}/DIM${dimension}`
if (!existsSync(dimensionFolderPath)) {
mkdirSync(dimensionFolderPath);
mkdirSync(`${dimensionFolderPath}/chunks`);
mkdirSync(`${dimensionFolderPath}/data`);
} else {
2023-11-07 20:46:17 +00:00
const chunkFiles = readdirSync(`${dimensionFolderPath}/chunks`);
for (const file of chunkFiles) {
if (file.endsWith(".hwc")) {
const name = file.split(".")[0];
chunksArray.push(parseInt(name.startsWith("-") ? name.replace("-", "-0x") : `0x${name}`));
}
2023-04-11 07:47:56 +01:00
}
}
}
if (!existsSync(this.worldPlayerDataFolderPath)) {
mkdirSync(this.worldPlayerDataFolderPath);
}
}
private createInfoFile(numericalSeed:number) {
const infoFileWriter = createWriter(Endian.BE, 26);
2023-04-11 07:47:56 +01:00
infoFileWriter.writeUByte(0xFD); // Info File Magic
2023-11-07 20:46:17 +00:00
infoFileWriter.writeUByte(2); // File Version
2023-04-11 07:47:56 +01:00
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 = createReader(Endian.BE, readFileSync(this.infoFilePath));
2023-04-11 07:47:56 +01:00
const fileMagic = infoFileReader.readUByte();
if (fileMagic !== 0xFD) {
throw new Error("World info file is invalid");
}
const fileVersion = infoFileReader.readByte();
2023-11-07 20:46:17 +00:00
// v0, v1 and v2 all contain the same data apart from version numbers
// All that changed between them was the folder format.
if (fileVersion === 0 || fileVersion === 1 || fileVersion === 2) {
2023-04-11 07:47:56 +01:00
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);
}
2023-11-07 20:46:17 +00:00
// Upgrade v1 to v2
if (fileVersion === 1) {
Console.printInfo("Upgrading world to format v2 from v1");
const files = readdirSync(`${this.worldFolderPath}/`);
for (const file of files) {
if (file.startsWith("DIM")) {
renameSync(`${this.worldFolderPath}/${file}`, `${this.worldFolderPath}/OLD${file}`);
mkdirSync(`${this.worldFolderPath}/${file}`);
mkdirSync(`${this.worldFolderPath}/${file}/data`);
renameSync(`${this.worldFolderPath}/OLD${file}`, `${this.worldFolderPath}/${file}/chunks`);
}
}
this.createInfoFile(this.worldSeed);
}
2023-04-11 07:47:56 +01:00
}
}
public writeChunkToDisk(chunk:Chunk) {
return new Promise<boolean>((resolve, reject) => {
const saveType = this.config.saveCompression;
const chunkFileWriter = createWriter(Endian.BE, 10);
2023-04-11 07:47:56 +01:00
chunkFileWriter.writeUByte(0xFC); // Chunk File Magic
2023-05-02 08:50:49 +01:00
// TODO: Change to 1 when lighting actually works
2023-06-19 18:29:16 +01:00
chunkFileWriter.writeUByte(1); // File Version
2023-04-11 07:47:56 +01:00
chunkFileWriter.writeUByte(saveType); // Save compression type
chunkFileWriter.writeUByte(16); // Chunk X
chunkFileWriter.writeUByte(128); // Chunk Y
chunkFileWriter.writeUByte(16); // Chunk Z
const chunkData = createWriter(Endian.BE)
2023-08-20 01:18:05 +01:00
.writeBuffer(Buffer.from(chunk.getBlockData()))
2023-06-19 18:29:16 +01:00
.writeBuffer(chunk.getMetadataBuffer())
.writeBuffer(chunk.getBlockLightBuffer())
.writeBuffer(chunk.getSkyLightBuffer()).toBuffer();
2023-04-11 07:47:56 +01:00
const codArr = this.chunksOnDisk.get(chunk.world.dimension);
2023-04-11 07:47:56 +01:00
if (saveType === SaveCompressionType.NONE) {
chunkFileWriter.writeInt(chunkData.length); // Data length
chunkFileWriter.writeBuffer(chunkData); // Chunk data
2023-11-07 20:46:17 +00:00
writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/chunks/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => {
2023-04-12 23:01:23 +01:00
const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z);
if (!codArr?.includes(cPair)) {
codArr?.push(cPair);
2023-04-12 23:01:23 +01:00
}
2023-04-11 07:47:56 +01:00
resolve(true);
});
} else if (saveType === SaveCompressionType.DEFLATE) {
deflate(chunkData, (err, data) => {
if (err) {
return reject(err);
}
chunkFileWriter.writeInt(data.length);
chunkFileWriter.writeBuffer(data);
2023-11-07 20:46:17 +00:00
writeFile(`${this.worldFolderPath}/DIM${chunk.world.dimension}/chunks/${Chunk.CreateCoordPair(chunk.x, chunk.z).toString(16)}.hwc`, chunkFileWriter.toBuffer(), () => {
2023-04-11 07:47:56 +01:00
const cPair = Chunk.CreateCoordPair(chunk.x, chunk.z);
if (!codArr?.includes(cPair)) {
codArr?.push(cPair);
2023-04-11 07:47:56 +01:00
}
//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) => {
2023-11-07 20:46:17 +00:00
readFile(`${this.worldFolderPath}/DIM${world.dimension}/chunks/${Chunk.CreateCoordPair(x, z).toString(16)}.hwc`, (err, data) => {
2023-04-11 07:47:56 +01:00
if (err) {
return reject(err);
}
const chunkFileReader = createReader(Endian.BE, data);
2023-04-11 07:47:56 +01:00
// 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 = createReader(Endian.BE, chunkFileReader.readBuffer(contentLength));
2023-04-12 23:37:51 +01:00
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2));
2023-04-11 07:47:56 +01:00
resolve(chunk);
} else if (saveCompressionType === SaveCompressionType.DEFLATE) {
inflate(chunkFileReader.readBuffer(contentLength), (err, data) => {
if (err) {
return reject(err);
}
const chunkData = createReader(Endian.BE, data);
2023-04-12 23:38:27 +01:00
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2));
2023-04-11 07:47:56 +01:00
resolve(chunk);
});
}
2023-05-02 08:50:49 +01:00
} else if (fileVersion === 1) {
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 = createReader(Endian.BE, chunkFileReader.readBuffer(contentLength));
2023-06-19 18:29:16 +01:00
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2));
2023-05-02 08:50:49 +01:00
resolve(chunk);
} else if (saveCompressionType === SaveCompressionType.DEFLATE) {
inflate(chunkFileReader.readBuffer(contentLength), (err, data) => {
if (err) {
return reject(err);
}
const chunkData = createReader(Endian.BE, data);
2023-05-02 08:50:49 +01:00
const chunk = new Chunk(world, x, z, chunkData.readUint8Array(totalByteSize), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2), chunkData.readUint8Array(totalByteSize / 2));
resolve(chunk);
});
}
2023-04-11 07:47:56 +01:00
}
});
});
}
}