From 63333a04aa4cb57395c16b47b2753216ccb7fe1a Mon Sep 17 00:00:00 2001 From: Holly Date: Thu, 9 Nov 2023 16:30:40 +0000 Subject: [PATCH] Implement AABB collision and add EntityItem properly. The items are fully functional apart from picking them up, they are commented out in the MPClient breakBlock function if you want to play with them. --- server/AABB.ts | 54 ++++++++++++++++++++--- server/MPClient.ts | 4 ++ server/Vec3.ts | 22 +++++++++- server/World.ts | 5 +++ server/blocks/Block.ts | 14 ++++++ server/blocks/BlockBehaviour.ts | 2 + server/blocks/BlockBehaviourFlower.ts | 5 +++ server/blocks/IBlockBehaviour.ts | 4 +- server/entities/Entity.ts | 33 +++++++++++++- server/entities/EntityItem.ts | 35 +++++++++++++++ server/entities/EntityLiving.ts | 4 +- server/entities/Player.ts | 5 ++- server/enums/Packet.ts | 1 + server/packets/PickupSpawn.ts | 62 +++++++++++++++++++++++++++ 14 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 server/packets/PickupSpawn.ts diff --git a/server/AABB.ts b/server/AABB.ts index 00cf3b3..7d37359 100644 --- a/server/AABB.ts +++ b/server/AABB.ts @@ -7,11 +7,15 @@ export default class AABB { private static readonly aabbPool:FunkyArray = new FunkyArray(); public readonly aabbPoolString:string; + public readonly pooled:boolean; + + public initMin:Vec3; + public initMax:Vec3; public min:Vec3; public max:Vec3; - public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number) { + public constructor(minXOrMin:Vec3 | number, minYOrMax:Vec3 | number, minZ?:number, maxX?:number, maxY?:number, maxZ?:number, pooled:boolean = false) { if (minXOrMin instanceof Vec3 && minYOrMax instanceof Vec3) { this.min = minXOrMin; this.max = minYOrMax; @@ -21,6 +25,11 @@ export default class AABB { } else { throw new Error("Invalid input parameters: AABB must be supplied with either two Vec3 with the min and max bounds or the raw bounds."); } + + this.initMin = new Vec3(this.min); + this.initMax = new Vec3(this.max); + + this.pooled = pooled; this.aabbPoolString = AABB.createAABBPoolString(this.min.x, this.min.y, this.min.z, this.max.x, this.max.y, this.max.z); if (!AABB.aabbPool.has(this.aabbPoolString)) { @@ -36,18 +45,53 @@ export default class AABB { public static getAABB(minX:number, minY:number, minZ:number, maxX:number, maxY:number, maxZ:number) { const aabbPoolString = this.createAABBPoolString(minX, minY, minZ, maxX, maxY, maxZ); - if (!AABB.aabbPool.has(aabbPoolString)) { - return AABB.aabbPool.get(aabbPoolString); + if (AABB.aabbPool.has(aabbPoolString)) { + const aabb = AABB.aabbPool.get(aabbPoolString); + if (aabb === undefined) { + throw new Error(`Pooled AABB was ${typeof(aabb)}! This should be impossible.`); + } + + return aabb; } return new AABB(minX, minY, minZ, maxX, maxY, maxZ); } - intersects(a:AABB, b:AABB) { + public static intersects(a:AABB, b:AABB) { return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z; } - intersectionAmount(a:AABB, b:AABB) { + public intersects(aabb:AABB) { + return this.min.x <= aabb.max.x && this.max.x >= aabb.min.x && this.min.y <= aabb.max.y && this.max.y >= aabb.min.y && this.min.z <= aabb.max.z && this.max.z >= aabb.min.z; + } + public static intersectionY(a: AABB, b: AABB) { + const minY = Math.max(a.min.y, b.min.y); + const maxY = Math.min(a.max.y, b.max.y); + + return minY <= maxY ? maxY - minY : 0; + } + + public intersectionY(aabb: AABB) { + const minY = Math.max(this.min.y, aabb.min.y); + const maxY = Math.min(this.max.y, aabb.max.y); + + return minY <= maxY ? maxY - minY : 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!`); + } + + this.min.set(this.initMin); + this.max.set(this.initMax); + if (xOrVec3 instanceof Vec3) { + this.min.add(xOrVec3); + this.max.add(xOrVec3); + } else if (typeof(xOrVec3) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.min.add(xOrVec3, y, z); + this.max.add(xOrVec3, y, z); + } } } \ No newline at end of file diff --git a/server/MPClient.ts b/server/MPClient.ts index a8425b8..92e8ca7 100644 --- a/server/MPClient.ts +++ b/server/MPClient.ts @@ -22,6 +22,7 @@ import { PacketDisconnectKick } from "./packets/DisconnectKick"; import { ItemStack } from "./inventories/ItemStack"; import { PacketWindowItems } from "./packets/WindowItems"; import { Block } from "./blocks/Block"; +import { EntityItem } from "./entities/EntityItem"; export class MPClient { private readonly mcServer:MinecraftServer; @@ -160,6 +161,9 @@ export class MPClient { this.entity.world.setBlockWithNotify(this.diggingAt.x, this.diggingAt.y, this.diggingAt.z, 0); this.inventory.addItemStack(new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata)); this.send(new PacketWindowItems(0, this.inventory.getInventorySize(), this.inventory.constructInventoryPayload()).writeData()); + /*const itemEntity = new EntityItem(this.entity.world, new ItemStack(Block.blockBehaviours[brokenBlockId].droppedItem(brokenBlockId), 1, metadata)); + itemEntity.position.set(x + 0.5, y + 0.5, z + 0.5); + this.entity.world.addEntity(itemEntity);*/ } // TODO: Cap how far away a player is able to break blocks diff --git a/server/Vec3.ts b/server/Vec3.ts index 9ccb132..76af4c3 100644 --- a/server/Vec3.ts +++ b/server/Vec3.ts @@ -35,7 +35,27 @@ export default class Vec3 { this.y = y; this.z = z; } else { - this.x = this.y = this.z = 0; + throw new Error(`Invalid arguments for Vec3.set : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); + } + } + + add(x:Vec3 | number, y?:number, z?:number) { + if (x instanceof Vec3) { + this.set(this.x + x.x, this.y + x.y, this.z + x.z); + } else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.set(this.x + x, this.y + y, this.z + z); + } else { + throw new Error(`Invalid arguments for Vec3.add : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); + } + } + + mult(x:Vec3 | number, y?:number, z?:number) { + if (x instanceof Vec3) { + this.set(this.x * x.x, this.y * x.y, this.z * x.z); + } else if (typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number") { + this.set(this.x * x, this.y * y, this.z * z); + } else { + throw new Error(`Invalid arguments for Vec3.mult : ${typeof(x)}, ${typeof(y)}, ${typeof(z)}`); } } } \ No newline at end of file diff --git a/server/World.ts b/server/World.ts index cda91e6..d0497c1 100644 --- a/server/World.ts +++ b/server/World.ts @@ -2,6 +2,7 @@ import { FunkyArray } from "../funkyArray"; import { Chunk } from "./Chunk"; import { WorldSaveManager } from "./WorldSaveManager"; import { Block } from "./blocks/Block"; +import { EntityItem } from "./entities/EntityItem"; import { IEntity } from "./entities/IEntity"; import { Player } from "./entities/Player"; //import { FlatGenerator } from "./generators/Flat"; @@ -9,6 +10,7 @@ import { HillyGenerator } from "./generators/Hilly"; import { IGenerator } from "./generators/IGenerator"; import { PacketBlockChange } from "./packets/BlockChange"; import { PacketDestroyEntity } from "./packets/DestroyEntity"; +import { PacketPickupSpawn } from "./packets/PickupSpawn"; import { QueuedBlockUpdate } from "./queuedUpdateTypes/BlockUpdate"; import { IQueuedUpdate } from "./queuedUpdateTypes/IQueuedUpdate"; @@ -46,6 +48,9 @@ export class World { this.entites.set(entity.entityId, entity); if (entity instanceof Player) { this.players.set(entity.entityId, entity); + } else if (entity instanceof EntityItem) { + const packet = new PacketPickupSpawn(entity.entityId, entity.itemStack.itemID, entity.itemStack.size, entity.itemStack.damage, Math.round(entity.position.x * 32), Math.round(entity.position.y * 32), Math.round(entity.position.z * 32), 0, 0, 0).writeData(); + entity.sendToNearby(packet); } } diff --git a/server/blocks/Block.ts b/server/blocks/Block.ts index 1682ac9..34787fb 100644 --- a/server/blocks/Block.ts +++ b/server/blocks/Block.ts @@ -1,3 +1,4 @@ +import AABB from "../AABB"; import { World } from "../World"; import { BlockBehaviour } from "./BlockBehaviour"; import { BlockBehaviourFlower } from "./BlockBehaviourFlower"; @@ -20,6 +21,7 @@ export class Block { public static readonly blocks:Array = new Array(); public static readonly lightPassage:Array = new Array(); public static readonly hardness:Array = new Array(); + public static readonly blockAABBs:Array = new Array(); public static readonly blockBehaviours:Array = new Array(); public static readonly blockNames:Array = new Array(); @@ -47,6 +49,14 @@ export class Block { Block.hardness[this.blockId] = value; } + private get blockAABB() { + return Block.blockAABBs[this.blockId]; + } + + private set blockAABB(value:AABB) { + Block.blockAABBs[this.blockId] = value; + } + public get blockName() { return Block.blockNames[this.blockId]; } @@ -107,6 +117,10 @@ export class Block { // TODO: Have the 1 be based on current tool ig return 1 / this.hardness / 100; } + + public getBoundingBox(x:number, y:number, z:number) { + return this.behaviour.getBoundingBox(x, y, z); + } // Define statics here static readonly stone = new Block(1).setHardness(1.5).setBehaviour(Behaviour.stone).setBlockName("Stone"); diff --git a/server/blocks/BlockBehaviour.ts b/server/blocks/BlockBehaviour.ts index c4ff9fa..c492e4e 100644 --- a/server/blocks/BlockBehaviour.ts +++ b/server/blocks/BlockBehaviour.ts @@ -1,7 +1,9 @@ +import AABB from "../AABB"; import { World } from "../World"; import { IBlockBehaviour } from "./IBlockBehaviour"; export class BlockBehaviour implements IBlockBehaviour { public neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number) {} public droppedItem(blockId:number) { return blockId; } + public getBoundingBox(x:number, y:number, z:number) { return AABB.getAABB(0 + x, 0 + y, 0 + z, 1 + x, 1 + y, 1 + z); } } \ No newline at end of file diff --git a/server/blocks/BlockBehaviourFlower.ts b/server/blocks/BlockBehaviourFlower.ts index eb1aa66..8418405 100644 --- a/server/blocks/BlockBehaviourFlower.ts +++ b/server/blocks/BlockBehaviourFlower.ts @@ -1,3 +1,4 @@ +import AABB from "../AABB"; import { World } from "../World"; import { Block } from "./Block"; import { BlockBehaviour } from "./BlockBehaviour"; @@ -9,4 +10,8 @@ export class BlockBehaviourFlower extends BlockBehaviour { world.setBlockWithNotify(x, y, z, 0); } } + + public getBoundingBox() { + return AABB.getAABB(0, 0, 0, 0, 0, 0); + } } \ No newline at end of file diff --git a/server/blocks/IBlockBehaviour.ts b/server/blocks/IBlockBehaviour.ts index 56cce8f..81915ff 100644 --- a/server/blocks/IBlockBehaviour.ts +++ b/server/blocks/IBlockBehaviour.ts @@ -1,6 +1,8 @@ +import AABB from "../AABB"; import { World } from "../World"; export interface IBlockBehaviour { neighborBlockChange(world:World, x:number, y:number, z:number, blockId:number): void, - droppedItem: (blockId:number) => number + droppedItem: (blockId:number) => number, + getBoundingBox: (x:number, y:number, z:number) => AABB, } \ No newline at end of file diff --git a/server/entities/Entity.ts b/server/entities/Entity.ts index 43dfc65..a3beae4 100644 --- a/server/entities/Entity.ts +++ b/server/entities/Entity.ts @@ -1,9 +1,11 @@ +import AABB from "../AABB"; import { Chunk } from "../Chunk"; import { MetadataEntry, MetadataWriter } from "../MetadataWriter"; import { Rotation } from "../Rotation"; import { Vec2 } from "../Vec2"; import Vec3 from "../Vec3"; import { World } from "../World"; +import { Block } from "../blocks/Block"; import { MetadataFieldType } from "../enums/MetadataFieldType"; import { PacketEntityLook } from "../packets/EntityLook"; import { PacketEntityLookRelativeMove } from "../packets/EntityLookRelativeMove"; @@ -28,6 +30,8 @@ export class Entity implements IEntity { public lastAbsPosition:Vec3; public motion:Vec3; + private positionBeforeMove:Vec3; + public rotation:Rotation; public lastRotation:Rotation; public absRotation:Rotation; @@ -50,12 +54,18 @@ export class Entity implements IEntity { private lastCrouchState:boolean; private lastFireState:boolean; + public entityAABB:AABB; + + private readonly isPlayer:boolean; private queuedChunkUpdate:boolean; - public constructor(world:World) { + public constructor(world:World, isPlayer:boolean = false) { this.entityId = Entity.nextEntityId++; + this.isPlayer = isPlayer; + this.entitySize = new Vec2(0.6, 1.8); + this.entityAABB = new AABB(-this.entitySize.x / 2, 0, -this.entitySize.x / 2, this.entitySize.x / 2, this.entitySize.y, this.entitySize.x / 2); this.fire = this.fallDistance = 0; this.onGround = false; @@ -68,6 +78,8 @@ export class Entity implements IEntity { this.lastAbsPosition = new Vec3(); this.motion = new Vec3(); + this.positionBeforeMove = new Vec3(); + this.rotation = new Rotation(); this.lastRotation = new Rotation(); this.absRotation = new Rotation(); @@ -201,6 +213,25 @@ export class Entity implements IEntity { } } + moveEntity(motionX:number, motionY:number, motionZ:number) { + this.positionBeforeMove.set(this.position); + const blockId = this.chunk.getBlockId(Math.floor(this.positionBeforeMove.x) & 0xf, Math.floor(this.positionBeforeMove.y), Math.floor(this.positionBeforeMove.z) & 0xf); + const blockUnderEntity = blockId > 0 ? Block.blocks[blockId] : null; + + this.position.add(motionX, motionY, motionZ); + + 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)); + if (this.entityAABB.intersects(blockBoundingBox)) { + const intersection = this.entityAABB.intersectionY(blockBoundingBox); + this.position.add(0, intersection, 0); + this.motion.y = 0; + this.onGround = true; + } + } + } + onTick() { this.updateMetadata(); this.updateEntityChunk(); diff --git a/server/entities/EntityItem.ts b/server/entities/EntityItem.ts index 413bec4..f2cf04b 100644 --- a/server/entities/EntityItem.ts +++ b/server/entities/EntityItem.ts @@ -6,12 +6,47 @@ export class EntityItem extends Entity { public age:number; public itemStack:ItemStack; + public pickupDelay:number; + public constructor(world:World, itemStack:ItemStack) { super(world); this.itemStack = itemStack; + this.entitySize.set(0.2, 0.2); + + this.pickupDelay = 0; + + this.motion.set(Math.random() * 0.2 - 0.1, 0.2, Math.random() * 0.2 - 0.1); + this.age = 0; this.health = 5; } + + onTick() { + super.onTick(); + if (this.pickupDelay > 0) { + this.pickupDelay--; + } + + this.motion.add(0, -0.04, 0); + this.moveEntity(this.motion.x, this.motion.y, this.motion.z); + + let xyMult = 0.98; + if (this.onGround) { + xyMult = 0.59; + } + + // TODO: Change the x and z based on the slipperiness of a block + this.motion.mult(xyMult, 0.98, xyMult); + + if (this.onGround) { + this.motion.y *= -0.5; + } + + this.age++; + if (this.age >= 6000) { + // TODO: Kill entity + } + } } \ No newline at end of file diff --git a/server/entities/EntityLiving.ts b/server/entities/EntityLiving.ts index cdd92e8..6ffbb3a 100644 --- a/server/entities/EntityLiving.ts +++ b/server/entities/EntityLiving.ts @@ -18,8 +18,8 @@ export class EntityLiving extends Entity { public headHeight:number; public lastHealth:number; - public constructor(world:World) { - super(world); + public constructor(world:World, isPlayer:boolean = false) { + super(world, isPlayer); this.timeInWater = 0; this.headHeight = 1.62; diff --git a/server/entities/Player.ts b/server/entities/Player.ts index 4cf1f29..b5e9a3c 100644 --- a/server/entities/Player.ts +++ b/server/entities/Player.ts @@ -27,7 +27,7 @@ export class Player extends EntityLiving { public trackedEquipment:Array; public constructor(server:MinecraftServer, world:World, username:string) { - super(world); + super(world, true); this.server = server; this.firstUpdate = true; this.loadedChunks = new Array(); @@ -140,6 +140,9 @@ export class Player extends EntityLiving { // Calculate player motion since we don't have it serverside. this.motion.set(this.position.x - this.lastPosition.x, this.position.y - this.lastPosition.y, this.position.z - this.lastPosition.z); + if (!this.motion.isZero) { + this.entityAABB.move(this.position); + } super.onTick(); diff --git a/server/enums/Packet.ts b/server/enums/Packet.ts index a743e1d..3de4911 100644 --- a/server/enums/Packet.ts +++ b/server/enums/Packet.ts @@ -21,6 +21,7 @@ export enum Packet { Animation = 0x12, EntityAction = 0x13, NamedEntitySpawn = 0x14, + PickupSpawn = 0x15, EntityVelocity = 0x1C, DestroyEntity = 0x1D, diff --git a/server/packets/PickupSpawn.ts b/server/packets/PickupSpawn.ts new file mode 100644 index 0000000..d9fa560 --- /dev/null +++ b/server/packets/PickupSpawn.ts @@ -0,0 +1,62 @@ +import { createWriter, IReader, Endian } from "bufferstuff"; +import { IPacket } from "./IPacket"; +import { Packet } from "../enums/Packet"; + +export class PacketPickupSpawn implements IPacket { + public packetId = Packet.PickupSpawn; + public entityId:number; + public item:number; + public count:number; + public damage:number; + public x:number; + public y:number; + public z:number; + public yaw:number; + public pitch:number; + public roll:number; + + public constructor(entityId?:number, item?:number, count?:number, damage?:number, x?:number, y?:number, z?:number, yaw?:number, pitch?:number, roll?:number) { + if (typeof(entityId) === "number" && typeof(item) === "number" && typeof(count) === "number" && typeof(damage) === "number" && typeof(x) === "number" && typeof(y) === "number" && typeof(z) === "number" && typeof(yaw) === "number" && typeof(pitch) === "number" && typeof(roll) === "number") { + this.entityId = entityId; + this.item = item; + this.count = count; + this.damage = damage; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + this.roll = roll; + } else { + this.entityId = Number.MIN_VALUE; + this.item = Number.MIN_VALUE; + this.count = Number.MIN_VALUE; + this.damage = Number.MIN_VALUE; + this.x = Number.MIN_VALUE; + this.y = Number.MIN_VALUE; + this.z = Number.MIN_VALUE; + this.yaw = Number.MIN_VALUE; + this.pitch = Number.MIN_VALUE; + this.roll = Number.MIN_VALUE; + } + } + + public readData(reader:IReader) { + this.entityId = reader.readInt(); + this.item = reader.readShort(); + this.count = reader.readByte(); + this.damage = reader.readShort(); + this.x = reader.readInt(); + this.y = reader.readInt(); + this.z = reader.readInt(); + this.yaw = reader.readByte(); + this.pitch = reader.readByte(); + this.roll = reader.readByte(); + + return this; + } + + public writeData() { + return createWriter(Endian.BE, 25).writeUByte(this.packetId).writeInt(this.entityId).writeShort(this.item).writeByte(this.count).writeShort(this.damage).writeInt(this.x).writeInt(this.y).writeInt(this.z).writeByte(this.yaw).writeByte(this.pitch).writeByte(this.roll).toBuffer(); + } +} \ No newline at end of file