Implement Entity / Player data saving
This commit is contained in:
parent
5a250cb601
commit
604bec9e89
8 changed files with 145 additions and 16 deletions
|
@ -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!`);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<number, Array<number>>;
|
||||
public playerDataOnDisk:Array<string>;
|
||||
|
||||
public constructor(config:Config, dimensions:Array<number>, numericalSeed:number) {
|
||||
this.chunksOnDisk = new FunkyArray<number, Array<number>>();
|
||||
this.playerDataOnDisk = new Array<string>();
|
||||
|
||||
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<boolean>((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<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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface IEntity {
|
|||
motion:Vec3,
|
||||
lastPosition:Vec3,
|
||||
crouching:boolean,
|
||||
isDead:boolean,
|
||||
updateMetadata:() => void,
|
||||
distanceTo:(entity:IEntity) => number,
|
||||
onTick:() => void
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue