From 604bec9e895a059a9300b0851fae7de0f846e23f Mon Sep 17 00:00:00 2001 From: Holly Date: Thu, 9 Nov 2023 21:59:45 +0000 Subject: [PATCH] Implement Entity / Player data saving --- server/AABB.ts | 28 ++++++++++++++ server/MinecraftServer.ts | 9 ++++- server/World.ts | 6 +++ server/WorldSaveManager.ts | 66 ++++++++++++++++++++++++++++++--- server/entities/Entity.ts | 15 +++++--- server/entities/EntityLiving.ts | 23 ++++++++++-- server/entities/IEntity.ts | 1 + server/entities/Player.ts | 13 +++++++ 8 files changed, 145 insertions(+), 16 deletions(-) diff --git a/server/AABB.ts b/server/AABB.ts index eef4a8b..e7a9ffd 100644 --- a/server/AABB.ts +++ b/server/AABB.ts @@ -77,6 +77,34 @@ export default class AABB { 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) { if (this.pooled) { throw new Error(`Attempted to move a pooled AABB. This is not allowed!`); diff --git a/server/MinecraftServer.ts b/server/MinecraftServer.ts index 3652839..72f520d 100644 --- a/server/MinecraftServer.ts +++ b/server/MinecraftServer.ts @@ -201,7 +201,7 @@ export class MinecraftServer { 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); 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); if (world instanceof World) { 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); 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; socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload()).writeData()); diff --git a/server/World.ts b/server/World.ts index d0497c1..baa0f45 100644 --- a/server/World.ts +++ b/server/World.ts @@ -1,3 +1,4 @@ +import { Endian, createWriter } from "bufferstuff"; import { FunkyArray } from "../funkyArray"; import { Chunk } from "./Chunk"; import { WorldSaveManager } from "./WorldSaveManager"; @@ -73,6 +74,11 @@ export class World { this.players.remove(entity.entityId); 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); diff --git a/server/WorldSaveManager.ts b/server/WorldSaveManager.ts index 59f9d58..25848b9 100644 --- a/server/WorldSaveManager.ts +++ b/server/WorldSaveManager.ts @@ -1,5 +1,5 @@ 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 { Chunk } from "./Chunk"; import { SaveCompressionType } from "./enums/SaveCompressionType"; @@ -8,6 +8,12 @@ import { World } from "./World"; import { FunkyArray } from "../funkyArray"; import { Console } from "hsconsole"; +enum FileMagic { + Chunk = 0xFC, + Info = 0xFD, + Player = 0xFE +} + export class WorldSaveManager { private readonly worldFolderPath; private readonly globalDataPath; @@ -21,9 +27,11 @@ export class WorldSaveManager { public worldSeed = Number.MIN_VALUE; public chunksOnDisk:FunkyArray>; + public playerDataOnDisk:Array; public constructor(config:Config, dimensions:Array, numericalSeed:number) { this.chunksOnDisk = new FunkyArray>(); + this.playerDataOnDisk = new Array(); this.worldFolderPath = `./${config.worldName}`; this.worldPlayerDataFolderPath = `${this.worldFolderPath}/playerdata`; @@ -69,11 +77,18 @@ export class WorldSaveManager { if (!existsSync(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) { 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.writeLong(this.worldCreationDate.getTime()); // World creation date infoFileWriter.writeLong(this.worldLastLoadDate.getTime()); // Last load date @@ -84,7 +99,7 @@ export class WorldSaveManager { private readInfoFile() { const infoFileReader = createReader(Endian.BE, readFileSync(this.infoFilePath)); const fileMagic = infoFileReader.readUByte(); - if (fileMagic !== 0xFD) { + if (fileMagic !== FileMagic.Info) { throw new Error("World info file is invalid"); } @@ -123,7 +138,7 @@ export class WorldSaveManager { return new Promise((resolve, reject) => { const saveType = this.config.saveCompression; 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 chunkFileWriter.writeUByte(1); // File Version chunkFileWriter.writeUByte(saveType); // Save compression type @@ -185,7 +200,7 @@ export class WorldSaveManager { const chunkFileReader = createReader(Endian.BE, data); // Check file validity - if (chunkFileReader.readUByte() !== 0xFC) { + if (chunkFileReader.readUByte() !== FileMagic.Chunk) { return reject(new Error("Chunk file is invalid")); } @@ -240,4 +255,45 @@ export class WorldSaveManager { }); }); } + + writePlayerSaveToDisk(username:string, playerData:IWriter) { + return new Promise((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((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); + } + }); + }); + } } \ No newline at end of file diff --git a/server/entities/Entity.ts b/server/entities/Entity.ts index 70247c8..f086301 100644 --- a/server/entities/Entity.ts +++ b/server/entities/Entity.ts @@ -1,4 +1,4 @@ -import { IReader, IWriter } from "bufferstuff"; +import { Endian, IReader, IWriter, createWriter } from "bufferstuff"; import AABB from "../AABB"; import { Chunk } from "../Chunk"; import { MetadataEntry, MetadataWriter } from "../MetadataWriter"; @@ -55,7 +55,7 @@ export class Entity implements IEntity { public entityAABB:AABB; - private readonly isPlayer:boolean; + public readonly isPlayer:boolean; private queuedChunkUpdate:boolean; public constructor(world:World, isPlayer:boolean = false) { @@ -104,7 +104,7 @@ export class Entity implements IEntity { public toSave(writer:IWriter) { 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 .writeShort(this.fire) .writeFloat(this.fallDistance) @@ -238,9 +238,14 @@ export class Entity implements IEntity { this.entityAABB.move(this.position); if (blockUnderEntity !== null) { 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)) { - const intersection = this.entityAABB.intersectionY(blockBoundingBox); - this.position.add(0, intersection, 0); + const inersectionY = this.entityAABB.intersectionY(blockBoundingBox); + this.position.add( + 0, + inersectionY, + 0 + ); this.motion.y = 0; this.onGround = true; } diff --git a/server/entities/EntityLiving.ts b/server/entities/EntityLiving.ts index 6ffbb3a..b1962d3 100644 --- a/server/entities/EntityLiving.ts +++ b/server/entities/EntityLiving.ts @@ -1,3 +1,4 @@ +import { IReader, IWriter } from "bufferstuff"; import { Rotation } from "../Rotation"; import Vec3 from "../Vec3"; import { World } from "../World"; @@ -13,7 +14,6 @@ import { Entity } from "./Entity"; import { IEntity } from "./IEntity"; export class EntityLiving extends Entity { - public fallDistance:number; public timeInWater:number; public headHeight:number; public lastHealth:number; @@ -24,19 +24,30 @@ export class EntityLiving extends Entity { this.timeInWater = 0; this.headHeight = 1.62; - this.fallDistance = 0; - 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) { if (this.health <= 0) { + this.isDead = true; return; } super.damageFrom(damage, entity); // Send Damage Animation packet or death packet - if (this.health === 0) { + if (this.isDead) { this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Dead).writeData()); } else { this.sendToAllNearby(new PacketEntityStatus(this.entityId, EntityStatus.Hurt).writeData()); @@ -57,6 +68,10 @@ export class EntityLiving extends Entity { onTick() { super.onTick(); + if (!this.isPlayer && !this.motion.isZero) { + this.moveEntity(this.motion.x, this.motion.y, this.motion.z); + } + // Drowning if (this.isInWater(true)) { if (this.timeInWater == Number.MIN_SAFE_INTEGER) { diff --git a/server/entities/IEntity.ts b/server/entities/IEntity.ts index f1a2788..e0224db 100644 --- a/server/entities/IEntity.ts +++ b/server/entities/IEntity.ts @@ -6,6 +6,7 @@ export interface IEntity { motion:Vec3, lastPosition:Vec3, crouching:boolean, + isDead:boolean, updateMetadata:() => void, distanceTo:(entity:IEntity) => number, onTick:() => void diff --git a/server/entities/Player.ts b/server/entities/Player.ts index b5e9a3c..8837141 100644 --- a/server/entities/Player.ts +++ b/server/entities/Player.ts @@ -12,6 +12,7 @@ import { Block } from "../blocks/Block"; import PlayerInventory from "../inventories/PlayerInventory"; import { Item } from "../items/Item"; import { PacketEntityEquipment } from "../packets/EntityEquipment"; +import { IReader, IWriter } from "bufferstuff"; const CHUNK_LOAD_RANGE = 15; @@ -50,6 +51,18 @@ export class Player extends EntityLiving { 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* public forceUpdatePlayerChunks() { this.firstUpdate = true;