2023-04-11 07:47:56 +01:00
|
|
|
import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync } from "fs";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { createWriter, createReader } from "../bufferStuff/index";
|
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";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { Endian } from "../bufferStuff/Endian";
|
2023-04-11 07:47:56 +01:00
|
|
|
|
|
|
|
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) {
|
2023-05-02 10:24:48 +01:00
|
|
|
const infoFileWriter = createWriter(Endian.BE, 26);
|
2023-04-11 07:47:56 +01:00
|
|
|
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() {
|
2023-05-02 10:24:48 +01:00
|
|
|
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();
|
|
|
|
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) {
|
2023-06-19 18:29:16 +01:00
|
|
|
return new Promise<boolean>((resolve, reject) => {
|
|
|
|
resolve(false);
|
|
|
|
});
|
2023-04-11 07:47:56 +01:00
|
|
|
return new Promise<boolean>((resolve, reject) => {
|
2023-04-17 02:05:01 +01:00
|
|
|
const saveType = this.config.saveCompression;
|
2023-05-02 10:24:48 +01:00
|
|
|
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
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
const chunkData = createWriter(Endian.BE)
|
2023-04-12 23:37:51 +01:00
|
|
|
.writeBuffer(Buffer.from(chunk.getData()))
|
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
|
|
|
|
|
|
|
if (saveType === SaveCompressionType.NONE) {
|
|
|
|
chunkFileWriter.writeInt(chunkData.length); // Data length
|
|
|
|
chunkFileWriter.writeBuffer(chunkData); // Chunk data
|
|
|
|
|
2023-06-19 18:29:16 +01:00
|
|
|
writeFile(`${this.worldChunksFolderPath}/${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 (!this.chunksOnDisk.includes(cPair)) {
|
|
|
|
this.chunksOnDisk.push(cPair);
|
|
|
|
}
|
|
|
|
|
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-06-19 18:29:16 +01:00
|
|
|
writeFile(`${this.worldChunksFolderPath}/${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 (!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) => {
|
2023-06-19 18:29:16 +01:00
|
|
|
readFile(`${this.worldChunksFolderPath}/${Chunk.CreateCoordPair(x, z).toString(16)}.hwc`, (err, data) => {
|
2023-04-11 07:47:56 +01:00
|
|
|
if (err) {
|
|
|
|
return reject(err);
|
|
|
|
}
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
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) {
|
2023-05-02 10:24:48 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
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) {
|
2023-05-02 10:24:48 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|