Implement Entity / Player data saving

This commit is contained in:
Holly Stubbs 2023-11-09 21:59:45 +00:00
parent 5a250cb601
commit 604bec9e89
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
8 changed files with 145 additions and 16 deletions

View file

@ -77,6 +77,34 @@ export default class AABB {
return minY <= maxY ? maxY - minY : 0; return minY <= maxY ? maxY - minY : 0;
} }
public static intersectionX(a: AABB, b: AABB) {
const minX = Math.max(a.min.x, b.min.x);
const maxX = Math.min(a.max.x, b.max.x);
return minX <= maxX ? maxX - minX : 0;
}
public intersectionX(aabb: AABB) {
const minX = Math.max(this.min.x, aabb.min.x);
const maxX = Math.min(this.max.x, aabb.max.x);
return minX <= maxX ? maxX - minX : 0;
}
public static intersectionZ(a: AABB, b: AABB) {
const minZ = Math.max(a.min.z, b.min.z);
const maxZ = Math.min(a.max.z, b.max.z);
return minZ <= maxZ ? maxZ - minZ : 0;
}
public intersectionZ(aabb: AABB) {
const minZ = Math.max(this.min.z, aabb.min.z);
const maxZ = Math.min(this.max.z, aabb.max.z);
return minZ <= maxZ ? maxZ - minZ : 0;
}
public move(xOrVec3:Vec3 | number, y?:number, z?:number) { public move(xOrVec3:Vec3 | number, y?:number, z?:number) {
if (this.pooled) { if (this.pooled) {
throw new Error(`Attempted to move a pooled AABB. This is not allowed!`); throw new Error(`Attempted to move a pooled AABB. This is not allowed!`);

View file

@ -201,7 +201,7 @@ export class MinecraftServer {
Console.printInfo(`[CHAT] ${text}`); Console.printInfo(`[CHAT] ${text}`);
} }
handleLoginRequest(reader:IReader, socket:Socket, setMPClient:(mpclient:MPClient) => void) { async handleLoginRequest(reader:IReader, socket:Socket, setMPClient:(mpclient:MPClient) => void) {
const loginPacket = new PacketLoginRequest().readData(reader); const loginPacket = new PacketLoginRequest().readData(reader);
if (loginPacket.protocolVersion !== MinecraftServer.PROTOCOL_VERSION) { if (loginPacket.protocolVersion !== MinecraftServer.PROTOCOL_VERSION) {
if (loginPacket.protocolVersion > MinecraftServer.PROTOCOL_VERSION) { if (loginPacket.protocolVersion > MinecraftServer.PROTOCOL_VERSION) {
@ -216,6 +216,11 @@ export class MinecraftServer {
const world = this.worlds.get(dimension); const world = this.worlds.get(dimension);
if (world instanceof World) { if (world instanceof World) {
const clientEntity = new Player(this, world, loginPacket.username); const clientEntity = new Player(this, world, loginPacket.username);
if (this.saveManager.playerDataOnDisk.includes(clientEntity.username)) {
clientEntity.fromSave(await this.saveManager.readPlayerDataFromDisk(clientEntity.username));
} else {
clientEntity.position.set(8, 70, 8);
}
world.addEntity(clientEntity); world.addEntity(clientEntity);
const client = new MPClient(this, socket, clientEntity); const client = new MPClient(this, socket, clientEntity);
@ -241,7 +246,7 @@ export class MinecraftServer {
} }
}); });
socket.write(new PacketPlayerPositionLook(8, 70, 70.62, 8, 0, 0, false).writeData()); socket.write(new PacketPlayerPositionLook(clientEntity.position.x, clientEntity.position.y, clientEntity.position.y + 0.62, clientEntity.position.z, 0, 0, false).writeData());
const playerInventory = clientEntity.inventory; const playerInventory = clientEntity.inventory;
socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload()).writeData()); socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload()).writeData());

View file

@ -1,3 +1,4 @@
import { Endian, createWriter } from "bufferstuff";
import { FunkyArray } from "../funkyArray"; import { FunkyArray } from "../funkyArray";
import { Chunk } from "./Chunk"; import { Chunk } from "./Chunk";
import { WorldSaveManager } from "./WorldSaveManager"; import { WorldSaveManager } from "./WorldSaveManager";
@ -73,6 +74,11 @@ export class World {
this.players.remove(entity.entityId); this.players.remove(entity.entityId);
this.sendToNearbyClients(entity, new PacketDestroyEntity(entity.entityId).writeData()); this.sendToNearbyClients(entity, new PacketDestroyEntity(entity.entityId).writeData());
const writer = createWriter(Endian.BE);
entity.toSave(writer);
this.saveManager.writePlayerSaveToDisk(entity.username, writer);
} }
this.entites.remove(entity.entityId); this.entites.remove(entity.entityId);

View file

@ -1,5 +1,5 @@
import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync, renameSync } from "fs"; import { readFileSync, readFile, writeFile, existsSync, mkdirSync, writeFileSync, readdirSync, renameSync } from "fs";
import { createWriter, createReader, Endian } from "bufferstuff"; import { createWriter, createReader, Endian, IWriter, IReader } from "bufferstuff";
import { Config } from "../config"; import { Config } from "../config";
import { Chunk } from "./Chunk"; import { Chunk } from "./Chunk";
import { SaveCompressionType } from "./enums/SaveCompressionType"; import { SaveCompressionType } from "./enums/SaveCompressionType";
@ -8,6 +8,12 @@ import { World } from "./World";
import { FunkyArray } from "../funkyArray"; import { FunkyArray } from "../funkyArray";
import { Console } from "hsconsole"; import { Console } from "hsconsole";
enum FileMagic {
Chunk = 0xFC,
Info = 0xFD,
Player = 0xFE
}
export class WorldSaveManager { export class WorldSaveManager {
private readonly worldFolderPath; private readonly worldFolderPath;
private readonly globalDataPath; private readonly globalDataPath;
@ -21,9 +27,11 @@ export class WorldSaveManager {
public worldSeed = Number.MIN_VALUE; public worldSeed = Number.MIN_VALUE;
public chunksOnDisk:FunkyArray<number, Array<number>>; public chunksOnDisk:FunkyArray<number, Array<number>>;
public playerDataOnDisk:Array<string>;
public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) { public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) {
this.chunksOnDisk = new FunkyArray<number, Array<number>>(); this.chunksOnDisk = new FunkyArray<number, Array<number>>();
this.playerDataOnDisk = new Array<string>();
this.worldFolderPath = `./${config.worldName}`; this.worldFolderPath = `./${config.worldName}`;
this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`; this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`;
@ -69,11 +77,18 @@ export class WorldSaveManager {
if (!existsSync(this.worldPlayerDataFolderPath)) { if (!existsSync(this.worldPlayerDataFolderPath)) {
mkdirSync(this.worldPlayerDataFolderPath); mkdirSync(this.worldPlayerDataFolderPath);
} }
const playerDataFiles = readdirSync(this.worldPlayerDataFolderPath);
for (const dataFile of playerDataFiles) {
if (dataFile.endsWith(".hpd")) {
this.playerDataOnDisk.push(dataFile.replace(".hpd", ""));
}
}
} }
private createInfoFile(numericalSeed:number) { private createInfoFile(numericalSeed:number) {
const infoFileWriter = createWriter(Endian.BE, 26); const infoFileWriter = createWriter(Endian.BE, 26);
infoFileWriter.writeUByte(0xFD); // Info File Magic infoFileWriter.writeUByte(FileMagic.Info); // Info File Magic
infoFileWriter.writeUByte(2); // File Version infoFileWriter.writeUByte(2); // File Version
infoFileWriter.writeLong(this.worldCreationDate.getTime()); // World creation date infoFileWriter.writeLong(this.worldCreationDate.getTime()); // World creation date
infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date
@ -84,7 +99,7 @@ export class WorldSaveManager {
private readInfoFile() { private readInfoFile() {
const infoFileReader = createReader(Endian.BE, readFileSync(this.infoFilePath)); const infoFileReader = createReader(Endian.BE, readFileSync(this.infoFilePath));
const fileMagic = infoFileReader.readUByte(); const fileMagic = infoFileReader.readUByte();
if (fileMagic !== 0xFD) { if (fileMagic !== FileMagic.Info) {
throw new Error("World info file is invalid"); throw new Error("World info file is invalid");
} }
@ -123,7 +138,7 @@ export class WorldSaveManager {
return new Promise<boolean>((resolve, reject) => { return new Promise<boolean>((resolve, reject) => {
const saveType = this.config.saveCompression; const saveType = this.config.saveCompression;
const chunkFileWriter = createWriter(Endian.BE, 10); const chunkFileWriter = createWriter(Endian.BE, 10);
chunkFileWriter.writeUByte(0xFC); // Chunk File Magic chunkFileWriter.writeUByte(FileMagic.Chunk); // Chunk File Magic
// TODO: Change to 1 when lighting actually works // TODO: Change to 1 when lighting actually works
chunkFileWriter.writeUByte(1); // File Version chunkFileWriter.writeUByte(1); // File Version
chunkFileWriter.writeUByte(saveType); // Save compression type chunkFileWriter.writeUByte(saveType); // Save compression type
@ -185,7 +200,7 @@ export class WorldSaveManager {
const chunkFileReader = createReader(Endian.BE, data); const chunkFileReader = createReader(Endian.BE, data);
// Check file validity // Check file validity
if (chunkFileReader.readUByte() !== 0xFC) { if (chunkFileReader.readUByte() !== FileMagic.Chunk) {
return reject(new Error("Chunk file is invalid")); return reject(new Error("Chunk file is invalid"));
} }
@ -240,4 +255,45 @@ export class WorldSaveManager {
}); });
}); });
} }
writePlayerSaveToDisk(username:string, playerData:IWriter) {
return new Promise<boolean>((resolve, reject) => {
const playerDataWriter = createWriter(Endian.BE);
playerDataWriter.writeUByte(FileMagic.Player); // File magic
playerDataWriter.writeUByte(0); // File version
playerDataWriter.writeBuffer(playerData.toBuffer()); // Player data
writeFile(`${this.worldPlayerDataFolderPath}/${username}.hpd`, playerDataWriter.toBuffer(), (err) => {
if (err) {
return reject(err);
}
if (!this.playerDataOnDisk.includes(username)) {
this.playerDataOnDisk.push(username);
}
resolve(true);
})
});
}
readPlayerDataFromDisk(username:string) {
return new Promise<IReader>((resolve, reject) => {
readFile(`${this.worldPlayerDataFolderPath}/${username}.hpd`, (err, data) => {
if (err) {
return reject(err);
}
const reader = createReader(Endian.BE, data);
if (reader.readUByte() !== FileMagic.Player) {
return reject(new Error("Player data file is invalid"));
}
const fileVersion = reader.readUByte();
if (fileVersion === 0) {
resolve(reader);
}
});
});
}
} }

View file

@ -1,4 +1,4 @@
import { IReader, IWriter } from "bufferstuff"; import { Endian, IReader, IWriter, createWriter } from "bufferstuff";
import AABB from "../AABB"; import AABB from "../AABB";
import { Chunk } from "../Chunk"; import { Chunk } from "../Chunk";
import { MetadataEntry, MetadataWriter } from "../MetadataWriter"; import { MetadataEntry, MetadataWriter } from "../MetadataWriter";
@ -55,7 +55,7 @@ export class Entity implements IEntity {
public entityAABB:AABB; public entityAABB:AABB;
private readonly isPlayer:boolean; public readonly isPlayer:boolean;
private queuedChunkUpdate:boolean; private queuedChunkUpdate:boolean;
public constructor(world:World, isPlayer:boolean = false) { public constructor(world:World, isPlayer:boolean = false) {
@ -104,7 +104,7 @@ export class Entity implements IEntity {
public toSave(writer:IWriter) { public toSave(writer:IWriter) {
writer.writeDouble(this.position.x).writeDouble(this.position.y).writeDouble(this.position.z) // Position writer.writeDouble(this.position.x).writeDouble(this.position.y).writeDouble(this.position.z) // Position
.writeDouble(this.motion.x).writeDouble(this.motion.y).writeDouble(this.motion.z) // Motion .writeFloat(this.motion.x).writeFloat(this.motion.y).writeFloat(this.motion.z) // Motion
.writeFloat(this.rotation.x).writeFloat(this.rotation.y) // Rotation .writeFloat(this.rotation.x).writeFloat(this.rotation.y) // Rotation
.writeShort(this.fire) .writeShort(this.fire)
.writeFloat(this.fallDistance) .writeFloat(this.fallDistance)
@ -238,9 +238,14 @@ export class Entity implements IEntity {
this.entityAABB.move(this.position); this.entityAABB.move(this.position);
if (blockUnderEntity !== null) { if (blockUnderEntity !== null) {
const blockBoundingBox = blockUnderEntity.getBoundingBox(Math.floor(this.positionBeforeMove.x), Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z)); const blockBoundingBox = blockUnderEntity.getBoundingBox(Math.floor(this.positionBeforeMove.x), Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z));
// TODO: Handle X and Z collisions.
if (this.entityAABB.intersects(blockBoundingBox)) { if (this.entityAABB.intersects(blockBoundingBox)) {
const intersection = this.entityAABB.intersectionY(blockBoundingBox); const inersectionY = this.entityAABB.intersectionY(blockBoundingBox);
this.position.add(0, intersection, 0); this.position.add(
0,
inersectionY,
0
);
this.motion.y = 0; this.motion.y = 0;
this.onGround = true; this.onGround = true;
} }

View file

@ -1,3 +1,4 @@
import { IReader, IWriter } from "bufferstuff";
import { Rotation } from "../Rotation"; import { Rotation } from "../Rotation";
import Vec3 from "../Vec3"; import Vec3 from "../Vec3";
import { World } from "../World"; import { World } from "../World";
@ -13,7 +14,6 @@ import { Entity } from "./Entity";
import { IEntity } from "./IEntity"; import { IEntity } from "./IEntity";
export class EntityLiving extends Entity { export class EntityLiving extends Entity {
public fallDistance:number;
public timeInWater:number; public timeInWater:number;
public headHeight:number; public headHeight:number;
public lastHealth:number; public lastHealth:number;
@ -24,19 +24,30 @@ export class EntityLiving extends Entity {
this.timeInWater = 0; this.timeInWater = 0;
this.headHeight = 1.62; this.headHeight = 1.62;
this.fallDistance = 0;
this.lastHealth = this.health; this.lastHealth = this.health;
} }
public fromSave(reader:IReader) {
super.fromSave(reader);
this.timeInWater = reader.readShort();
}
public toSave(writer:IWriter) {
super.toSave(writer);
writer.writeShort(this.timeInWater == Number.MIN_SAFE_INTEGER ? 0 : this.timeInWater);
}
damageFrom(damage:number, entity?:IEntity) { damageFrom(damage:number, entity?:IEntity) {
if (this.health <= 0) { if (this.health <= 0) {
this.isDead = true;
return; return;
} }
super.damageFrom(damage, entity); super.damageFrom(damage, entity);
// Send Damage Animation packet or death packet // Send Damage Animation packet or death packet
if (this.health === 0) { if (this.isDead) {
this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Dead).writeData()); this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Dead).writeData());
} else { } else {
this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Hurt).writeData()); this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Hurt).writeData());
@ -57,6 +68,10 @@ export class EntityLiving extends Entity {
onTick() { onTick() {
super.onTick(); super.onTick();
if (!this.isPlayer && !this.motion.isZero) {
this.moveEntity(this.motion.x, this.motion.y, this.motion.z);
}
// Drowning // Drowning
if (this.isInWater(true)) { if (this.isInWater(true)) {
if (this.timeInWater == Number.MIN_SAFE_INTEGER) { if (this.timeInWater == Number.MIN_SAFE_INTEGER) {

View file

@ -6,6 +6,7 @@ export interface IEntity {
motion:Vec3, motion:Vec3,
lastPosition:Vec3, lastPosition:Vec3,
crouching:boolean, crouching:boolean,
isDead:boolean,
updateMetadata:() => void, updateMetadata:() => void,
distanceTo:(entity:IEntity) => number, distanceTo:(entity:IEntity) => number,
onTick:() => void onTick:() => void

View file

@ -12,6 +12,7 @@ import { Block } from "../blocks/Block";
import PlayerInventory from "../inventories/PlayerInventory"; import PlayerInventory from "../inventories/PlayerInventory";
import { Item } from "../items/Item"; import { Item } from "../items/Item";
import { PacketEntityEquipment } from "../packets/EntityEquipment"; import { PacketEntityEquipment } from "../packets/EntityEquipment";
import { IReader, IWriter } from "bufferstuff";
const CHUNK_LOAD_RANGE = 15; const CHUNK_LOAD_RANGE = 15;
@ -50,6 +51,18 @@ export class Player extends EntityLiving {
this.position.set(8, 64, 8); this.position.set(8, 64, 8);
} }
public fromSave(reader:IReader) {
super.fromSave(reader);
this.inventory.fromSave(reader);
}
public toSave(writer:IWriter) {
super.toSave(writer);
this.inventory.toSave(writer);
}
// Forces a player chunk update *next tick* // Forces a player chunk update *next tick*
public forceUpdatePlayerChunks() { public forceUpdatePlayerChunks() {
this.firstUpdate = true; this.firstUpdate = true;