diff --git a/Sources/Core/Sources/ECS/Components/EntityAttributes.swift b/Sources/Core/Sources/ECS/Components/EntityAttributes.swift index aaa5d322..b5f54633 100644 --- a/Sources/Core/Sources/ECS/Components/EntityAttributes.swift +++ b/Sources/Core/Sources/ECS/Components/EntityAttributes.swift @@ -1,6 +1,7 @@ import FirebladeECS -/// A component storing an entity's attributes. +/// A component storing an entity's attributes. See ``EntityMetadata`` for a +/// discussion on the difference between metadata and attributes. public class EntityAttributes: Component { /// The attributes as key-value pairs. private var attributes: [EntityAttributeKey: EntityAttributeValue] = [:] diff --git a/Sources/Core/Sources/ECS/Components/EntityMetadata.swift b/Sources/Core/Sources/ECS/Components/EntityMetadata.swift new file mode 100644 index 00000000..58916370 --- /dev/null +++ b/Sources/Core/Sources/ECS/Components/EntityMetadata.swift @@ -0,0 +1,12 @@ +import FirebladeECS + +/// The distinction between entity metadata and entity attributes is that entity +/// attributes are for properties that can have modifiers applied (e.g. speed, +/// max health, etc). +public class EntityMetadata: Component { + /// If an entity doesn't have AI, we should ignore its velocity. For some reason the + /// server still sends us the velocity even when the entity isn't moving. + public var noAI = false + + public init() {} +} diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index bbc03795..31d29720 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -10,12 +10,13 @@ public struct EntityMovementSystem: System { EntityVelocity.self, EntityRotation.self, EntityLerpState.self, + EntityMetadata.self, EntityKindId.self, EntityOnGround.self, excludesAll: ClientPlayerEntity.self ) - for (position, velocity, rotation, lerpState, kind, onGround) in physicsEntities { + for (position, velocity, rotation, lerpState, metadata, kind, onGround) in physicsEntities { guard let kind = RegistryStore.shared.entityRegistry.entity(withId: kind.id) else { log.warning("Unknown entity kind '\(kind.id)'") continue @@ -53,7 +54,9 @@ public struct EntityMovementSystem: System { velocity.vector.z = 0 } - position.move(by: velocity.vector) + if !metadata.noAI { + position.move(by: velocity.vector) + } } } } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 70f4fd03..aa65434c 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -291,12 +291,16 @@ public final class Game: @unchecked Sendable { /// - Parameters: /// - id: The id of the entity to access. /// - action: The action to perform on the entity if it exists. - public func accessEntity(id: Int, acquireLock: Bool = true, action: (Entity) -> Void) { + public func accessEntity( + id: Int, + acquireLock: Bool = true, + action: (Entity) throws -> Void + ) rethrows { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } if let identifier = entityIdToEntityIdentifier[id] { - action(nexus.entity(from: identifier)) + try action(nexus.entity(from: identifier)) } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift index a6845d19..f79f2d46 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift @@ -14,6 +14,13 @@ public enum ClientboundPacketError: LocalizedError { case invalidBossBarStyleId(Int) case duplicateBossBar(UUID) case noSuchBossBar(UUID) + case invalidPoseId(Int) + case invalidEntityMetadataDatatypeId(Int) + case incorrectEntityMetadataDatatype( + property: String, + expectedType: String, + value: EntityMetadataPacket.Value + ) public var errorDescription: String? { switch self { @@ -21,59 +28,76 @@ public enum ClientboundPacketError: LocalizedError { return "Invalid difficulty." case let .invalidGamemode(rawValue): return """ - Invalid gamemode. - Raw value: \(rawValue) - """ + Invalid gamemode. + Raw value: \(rawValue) + """ case .invalidServerId: return "Invalid server Id." case .invalidJSONString: return "Invalid JSON string." case let .invalidInventorySlotCount(slotCount): return """ - Invalid inventory slot count. - Slot count: \(slotCount) - """ + Invalid inventory slot count. + Slot count: \(slotCount) + """ case let .invalidInventorySlotIndex(slotIndex, windowId): return """ - Invalid inventory slot index. - Slot index: \(slotIndex) - Window Id: \(windowId) - """ + Invalid inventory slot index. + Slot index: \(slotIndex) + Window Id: \(windowId) + """ case let .invalidChangeGameStateReasonRawValue(rawValue): return """ - Invalid change game state reason. - Raw value: \(rawValue) - """ + Invalid change game state reason. + Raw value: \(rawValue) + """ case let .invalidDimension(identifier): return """ - Invalid dimension. - Identifier: \(identifier) - """ + Invalid dimension. + Identifier: \(identifier) + """ case let .invalidBossBarActionId(actionId): return """ - Invalid boss bar action id. - Id: \(actionId) - """ + Invalid boss bar action id. + Id: \(actionId) + """ case let .invalidBossBarColorId(colorId): return """ - Invalid boss bar color id. - Id: \(colorId) - """ + Invalid boss bar color id. + Id: \(colorId) + """ case let .invalidBossBarStyleId(styleId): return """ - Invalid boss bar style id. - Id: \(styleId) - """ + Invalid boss bar style id. + Id: \(styleId) + """ case let .duplicateBossBar(uuid): return """ - Received duplicate boss bar. - UUID: \(uuid.uuidString) - """ + Received duplicate boss bar. + UUID: \(uuid.uuidString) + """ case let .noSuchBossBar(uuid): return """ - Received update for non-existent boss bar. - UUID: \(uuid) - """ + Received update for non-existent boss bar. + UUID: \(uuid) + """ + case let .invalidPoseId(poseId): + return """ + Received invalid pose id. + Id: \(poseId) + """ + case let .invalidEntityMetadataDatatypeId(datatypeId): + return """ + Received invalid entity metadata datatype id. + Id: \(datatypeId) + """ + case let .incorrectEntityMetadataDatatype(property, expectedType, value): + return """ + Received entity metadata property with invalid data type. + Property name: \(property) + Expected type: \(expectedType) + Value: \(value) + """ } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/PacketReader.swift b/Sources/Core/Sources/Network/Protocol/Packets/PacketReader.swift index 2e52701b..03c8c9e3 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/PacketReader.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/PacketReader.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation /// A wrapper around ``Buffer`` that is specialized for reading Minecraft packets. public struct PacketReader { @@ -46,6 +46,25 @@ public struct PacketReader { return bool } + /// Optionally reads a value (assuming that the value's presence is indicated by a boolean + /// field directly preceding it). + public mutating func readOptional(_ inner: (inout Self) throws -> T) throws -> T? { + if try readBool() { + return try inner(&self) + } else { + return nil + } + } + + /// Reads a direction (represented as a VarInt). + public mutating func readDirection() throws -> Direction { + let rawValue = try readVarInt() + guard let direction = Direction(rawValue: rawValue) else { + throw PacketReaderError.invalidDirection(rawValue) + } + return direction + } + /// Reads a signed byte. /// - Returns: A signed byte. /// - Throws: A ``BufferError`` if out of bounds. @@ -224,9 +243,9 @@ public struct PacketReader { let val = try buffer.readLong(endianness: .big) // Extract the bit patterns (in the order x, then z, then y) - var x = UInt32(val >> 38) // x is 26 bit - var z = UInt32((val << 26) >> 38) // z is 26 bit - var y = UInt32(val & 0xfff) // y is 12 bit + var x = UInt32(val >> 38) // x is 26 bit + var z = UInt32((val << 26) >> 38) // z is 26 bit + var y = UInt32(val & 0xfff) // y is 12 bit // x and z are 26-bit signed integers, y is a 12-bit signed integer let xSignBit = (x & (1 << 25)) >> 25 @@ -238,7 +257,7 @@ public struct PacketReader { x |= 0b111111 << 26 } if ySignBit == 1 { - y |= 0b11111111111111111111 << 12 + y |= 0b1111_11111111_11111111 << 12 } if zSignBit == 1 { z |= 0b111111 << 26 @@ -257,7 +276,9 @@ public struct PacketReader { /// - Parameter pitchFirst: If `true`, pitch is read before yaw. /// - Returns: An entity rotation in radians. /// - Throws: A ``BufferError`` if any reads go out of bounds. - public mutating func readEntityRotation(pitchFirst: Bool = false) throws -> (pitch: Float, yaw: Float) { + public mutating func readEntityRotation(pitchFirst: Bool = false) throws -> ( + pitch: Float, yaw: Float + ) { var pitch: Float = 0 if pitchFirst { pitch = try readAngle() diff --git a/Sources/Core/Sources/Network/Protocol/Packets/PacketReaderError.swift b/Sources/Core/Sources/Network/Protocol/Packets/PacketReaderError.swift index 402cdb4a..937170fd 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/PacketReaderError.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/PacketReaderError.swift @@ -5,4 +5,5 @@ public enum PacketReaderError: Error { case stringTooLong(length: Int) case invalidNBT(Error) case invalidIdentifier(String) + case invalidDirection(Int) } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift index b0164b04..53882cdc 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift @@ -1,12 +1,222 @@ import Foundation +// TODO: Update this when adding a new protocol version (the format changes each version). public struct EntityMetadataPacket: ClientboundPacket { public static let id: Int = 0x44 - + public var entityId: Int + public var metadata: [MetadataEntry] + + public struct MetadataEntry { + public var index: Int + public var value: Value + } + + public enum Value { + case byte(Int8) + case varInt(Int) + case float(Float) + case string(String) + case chat(ChatComponent) + case optionalChat(ChatComponent?) + case slot(Slot) + case bool(Bool) + case rotation(Vec3f) + case position(BlockPosition) + case optionalPosition(BlockPosition?) + case direction(Direction) + case optionalUUID(UUID?) + case optionalBlockStateId(Int?) + case nbt(NBT.Compound) + case particle(Particle) + case villagerData(type: Int, profession: Int, level: Int) + case entityId(Int?) + case pose(Pose) + + public enum Pose: Int { + case standing = 0 + case fallFlying = 1 + case sleeping = 2 + case swimming = 3 + case spinAttack = 4 + case sneaking = 5 + case longJumping = 6 + case dying = 7 + case croaking = 8 + case usingTongue = 9 + case sitting = 10 + case roaring = 11 + case sniffing = 12 + case emerging = 13 + case digging = 14 + } + + public struct Particle { + // TODO: These will need updating when adding support for new protocol versions. Ideally + // they should be loaded dynamically from either pixlyzer (I don't think it has the right + // data), or from our own data files of some sort. + public static let blockParticleId = 3 + public static let dustParticleId = 14 + public static let fallingDustParticleId = 23 + public static let itemParticleId = 32 + + public var id: Int + public var data: Data? + + public enum Data { + case block(blockStateId: Int) + case dust(red: Float, green: Float, blue: Float, scale: Float) + case fallingDust(blockStateId: Int) + case item(Slot) + } + } + } + public init(from packetReader: inout PacketReader) throws { entityId = try packetReader.readVarInt() - // IMPLEMENT: the rest of this packet + + metadata = [] + while true { + let index = try packetReader.readUnsignedByte() + if index == 0xff { + break + } + + let type = try packetReader.readVarInt() + let value: Value + switch type { + case 0: + value = .byte(try packetReader.readByte()) + case 1: + value = .varInt(try packetReader.readVarInt()) + case 2: + value = .float(try packetReader.readFloat()) + case 3: + value = .string(try packetReader.readString()) + case 4: + value = .chat(try packetReader.readChat()) + case 5: + value = .optionalChat( + try packetReader.readOptional { reader in + try reader.readChat() + } + ) + case 6: + value = .slot(try packetReader.readSlot()) + case 7: + value = .bool(try packetReader.readBool()) + case 8: + value = .rotation( + Vec3f( + try packetReader.readFloat(), + try packetReader.readFloat(), + try packetReader.readFloat() + ) + ) + case 9: + value = .position(try packetReader.readBlockPosition()) + case 10: + value = .optionalPosition( + try packetReader.readOptional { reader in + try reader.readBlockPosition() + } + ) + case 11: + value = .direction(try packetReader.readDirection()) + case 12: + value = .optionalUUID( + try packetReader.readOptional { reader in + try reader.readUUID() + } + ) + case 13: + let rawValue = try packetReader.readVarInt() + if rawValue == 0 { + value = .optionalBlockStateId(nil) + } else { + value = .optionalBlockStateId(rawValue - 1) + } + case 14: + value = .nbt(try packetReader.readNBTCompound()) + case 15: + let particleId = try packetReader.readVarInt() + let data: Value.Particle.Data? + switch particleId { + case Value.Particle.blockParticleId: + data = .block(blockStateId: try packetReader.readVarInt()) + case Value.Particle.dustParticleId: + data = .dust( + red: try packetReader.readFloat(), + green: try packetReader.readFloat(), + blue: try packetReader.readFloat(), + scale: try packetReader.readFloat() + ) + case Value.Particle.fallingDustParticleId: + data = .fallingDust(blockStateId: try packetReader.readVarInt()) + case Value.Particle.itemParticleId: + data = .item(try packetReader.readSlot()) + default: + data = nil + } + value = .particle(Value.Particle(id: particleId, data: data)) + case 16: + value = .villagerData( + type: try packetReader.readVarInt(), + profession: try packetReader.readVarInt(), + level: try packetReader.readVarInt() + ) + case 17: + // Value is an optional varint, but 0 represents `nil` and any other value + // represents `1 + value` + let rawValue = try packetReader.readVarInt() + if rawValue == 0 { + value = .entityId(nil) + } else { + value = .entityId(rawValue - 1) + } + case 18: + let rawValue = try packetReader.readVarInt() + guard let pose = Value.Pose(rawValue: rawValue) else { + throw ClientboundPacketError.invalidPoseId(rawValue) + } + value = .pose(pose) + default: + throw ClientboundPacketError.invalidEntityMetadataDatatypeId(type) + } + + metadata.append(MetadataEntry(index: Int(index), value: value)) + } + } + + public func handle(for client: Client) throws { + try client.game.accessEntity(id: entityId) { entity in + guard + let metadataComponent = entity.get(component: EntityMetadata.self), + let kindId = entity.get(component: EntityKindId.self) + else { + log.warning("Entity '\(entityId)' is missing components required to handle \(Self.self)") + return + } + + guard let kind = kindId.entityKind else { + log.warning("Invalid entity kind id '\(kindId.id)'") + return + } + + for entry in metadata { + if kind.inheritanceChain.contains("MobEntity"), entry.index == 14 { + guard case let .byte(flags) = entry.value else { + throw ClientboundPacketError.incorrectEntityMetadataDatatype( + property: "Mob.noAI", + expectedType: "byte", + value: entry.value + ) + } + + metadataComponent.noAI = flags & 0x01 == 0x01 + } + } + } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift index 19c27fde..d0c66af4 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation public struct EntityPositionAndRotationPacket: ClientboundEntityPacket { public static let id: Int = 0x29 @@ -43,6 +43,9 @@ public struct EntityPositionAndRotationPacket: ClientboundEntityPacket { let kind = entity.get(component: EntityKindId.self)?.entityKind, let onGroundComponent = entity.get(component: EntityOnGround.self) else { + log.warning( + "Entity '\(entityId)' is missing required components to handle \(Self.self)" + ) return } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift index e6a4b197..d28de644 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift @@ -40,7 +40,7 @@ public struct EntityPositionPacket: ClientboundEntityPacket { let onGroundComponent = entity.get(component: EntityOnGround.self) else { log.warning( - "Entity '\(entityId)' is missing required components to handle EntityPositionPacket" + "Entity '\(entityId)' is missing required components to handle \(Self.self)" ) return } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift index cdad976d..872395ae 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift @@ -27,6 +27,9 @@ public struct EntityRotationPacket: ClientboundEntityPacket { let kind = entity.get(component: EntityKindId.self)?.entityKind, let onGroundComponent = entity.get(component: EntityOnGround.self) else { + log.warning( + "Entity '\(entityId)' is missing required components to handle \(Self.self)" + ) return } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift index 02411bb0..4e942be3 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift @@ -21,6 +21,7 @@ public struct EntityVelocityPacket: ClientboundEntityPacket { EntityVelocity.self, acquireLock: false ) { velocityComponent in + // I think this packet is the cause of most of our weird entity behaviour // TODO: Figure out why handling velocity is causing entities to drift (observe spiders for a while // to reproduce issue). Works best if spider is trying to climb a wall but it stuck under a roof. velocityComponent.vector = velocity diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift index f6614224..eff98d50 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift @@ -47,6 +47,7 @@ public struct SpawnEntityPacket: ClientboundPacket { EntityRotation(pitch: pitch, yaw: yaw) EntityLerpState() EntityAttributes() + EntityMetadata() } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift index 9a926a56..24962080 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift @@ -42,6 +42,7 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { EntityHeadYaw(headYaw) EntityLerpState() EntityAttributes() + EntityMetadata() } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift index 0f5b06f0..0fd3ffec 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift @@ -37,6 +37,7 @@ public struct SpawnPlayerPacket: ClientboundPacket { EntityRotation(pitch: pitch, yaw: yaw) EntityLerpState() EntityAttributes() + EntityMetadata() } } } diff --git a/Sources/Core/Sources/Player/Player.swift b/Sources/Core/Sources/Player/Player.swift index 7ba1a0a8..e1e3686f 100644 --- a/Sources/Core/Sources/Player/Player.swift +++ b/Sources/Core/Sources/Player/Player.swift @@ -1,6 +1,6 @@ -import Foundation import FirebladeECS import FirebladeMath +import Foundation /// Allows easy access to the player's components. /// @@ -38,6 +38,8 @@ public struct Player { public private(set) var playerAttributes: PlayerAttributes /// The component storing the player's entity attributes. public private(set) var entityAttributes: EntityAttributes + /// The component storing the player's entity metadata + public private(set) var entityMetadata: EntityMetadata /// The component storing the player's gamemode related information. public private(set) var gamemode: PlayerGamemode /// The component storing the player's inventory. @@ -56,7 +58,7 @@ public struct Player { /// Creates a player. public init() { let playerEntity = RegistryStore.shared.entityRegistry.playerEntityKind - entityId = EntityId(-1) // Temporary value until the actual id is received from the server. + entityId = EntityId(-1) // Temporary value until the actual id is received from the server. onGround = EntityOnGround(true) // Having smoothing set to slightly more than a tick smooths out any hick ups caused by late ticks position = EntityPosition(0, 0, 0, smoothingAmount: 1 / 18) @@ -72,6 +74,7 @@ public struct Player { nutrition = EntityNutrition() playerAttributes = PlayerAttributes() entityAttributes = EntityAttributes() + entityMetadata = EntityMetadata() camera = EntityCamera() gamemode = PlayerGamemode() inventory = PlayerInventory() @@ -83,10 +86,10 @@ public struct Player { /// - Parameter nexus: The game to create the player's entity in. public mutating func add(to game: Game) { game.createEntity(id: -1) { - LivingEntity() // Mark it as a living entity - PlayerEntity() // Mark it as a player - ClientPlayerEntity() // Mark it as the current player - EntityKindId(RegistryStore.shared.entityRegistry.playerEntityKindId) // Give it the entity kind id for player + LivingEntity() // Mark it as a living entity + PlayerEntity() // Mark it as a player + ClientPlayerEntity() // Mark it as the current player + EntityKindId(RegistryStore.shared.entityRegistry.playerEntityKindId) // Give it the entity kind id for player entityId onGround position @@ -102,6 +105,7 @@ public struct Player { nutrition playerAttributes entityAttributes + entityMetadata camera gamemode inventory diff --git a/Sources/Core/Sources/Registry/Entity/EntityKind.swift b/Sources/Core/Sources/Registry/Entity/EntityKind.swift index 18ca7baa..b4f00b9c 100644 --- a/Sources/Core/Sources/Registry/Entity/EntityKind.swift +++ b/Sources/Core/Sources/Registry/Entity/EntityKind.swift @@ -10,8 +10,13 @@ public struct EntityKind: Codable { public var height: Float /// Attributes that are the same for every entity of this kind (e.g. maximum health). public var attributes: [EntityAttributeKey: Float] - /// Whether the entity is living or not. + /// Whether the entity is living or not. Corresponds to ``inheritanceChain`` containing + /// `"LivingEntity"`, but precomputed to avoid an array search every time it's accessed. public var isLiving: Bool + // TODO: Parse into an array of enum values (strings are slow, and there are only a limited number + // of entity classes). + /// The chain of class inheritance for this entity kind in vanilla. + public var inheritanceChain: [String] /// The default duration of position/rotation linear interpolation (measured in ticks) /// to use for this kind of entity. @@ -30,7 +35,8 @@ public struct EntityKind: Codable { width: Float, height: Float, attributes: [EntityAttributeKey: Float], - isLiving: Bool + isLiving: Bool, + inheritanceChain: [String] ) { self.identifier = identifier self.id = id @@ -38,5 +44,6 @@ public struct EntityKind: Codable { self.height = height self.attributes = attributes self.isLiving = isLiving + self.inheritanceChain = inheritanceChain } } diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift index d148c375..3dc5fcdc 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift @@ -16,21 +16,24 @@ public struct PixlyzerEntity: Decodable { public var parent: String? } -public extension EntityKind { +extension EntityKind { /// Returns nil if the pixlyzer entity doesn't correspond to a Vanilla minecraft entity kind. /// Throws on unknown entity attributes. - init?(from pixlyzerEntity: PixlyzerEntity, isLiving: Bool, identifier: Identifier) throws { + public init?( + from pixlyzerEntity: PixlyzerEntity, inheritanceChain: [String], identifier: Identifier + ) throws { guard let id = pixlyzerEntity.id else { return nil } - + self.id = id self.identifier = identifier - self.isLiving = isLiving - + self.isLiving = inheritanceChain.contains("LivingEntity") + self.inheritanceChain = inheritanceChain + width = pixlyzerEntity.width ?? 0 height = pixlyzerEntity.height ?? 0 - + attributes = [:] for (attribute, value) in pixlyzerEntity.attributes ?? [:] { guard let attribute = EntityAttributeKey(rawValue: attribute) else { diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift index a955842e..1527016f 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift @@ -22,23 +22,23 @@ public enum PixlyzerError: LocalizedError { return "The block with id: \(id) is missing." case let .invalidAABBVertexLength(length): return """ - An AABB's vertex is of invalid length. - length: \(length) - """ + An AABB's vertex is of invalid length. + length: \(length) + """ case .entityRegistryMissingPlayer: return "The entity registry does not contain the player entity." case let .invalidUTF8BlockName(blockName): return """ - The block name could not be converted to data using UTF8. - Block name: \(blockName) - """ + The block name could not be converted to data using UTF8. + Block name: \(blockName) + """ case .failedToGetWaterFluid: return "Failed to get the water fluid from the fluid registry." case let .unknownEntityAttribute(attribute): return """ - Unknown entity attribute in Pixlyzer entity registry. - Attribute: \(attribute) - """ + Unknown entity attribute in Pixlyzer entity registry. + Attribute: \(attribute) + """ case let .missingEntity(name): return "Expected entity kind '\(name)' to be present in Pixlyzer entity registry." } @@ -52,7 +52,8 @@ public enum PixlyzerFormatter { public static func downloadAndFormatRegistries(_ version: String) throws -> RegistryStore { let pixlyzerCommit = "7cceb5481e6f035d274204494030a76f47af9bb5" let pixlyzerItemCommit = "c623c21be12aa1f9be3f36f0e32fbc61f8f16bd1" - let baseURL = "https://gitlab.bixilon.de/bixilon/pixlyzer-data/-/raw/\(pixlyzerCommit)/version/\(version)" + let baseURL = + "https://gitlab.bixilon.de/bixilon/pixlyzer-data/-/raw/\(pixlyzerCommit)/version/\(version)" // swiftlint:disable force_unwrapping let fluidsDownloadURL = URL(string: "\(baseURL)/fluids.min.json")! @@ -60,26 +61,36 @@ public enum PixlyzerFormatter { let biomesDownloadURL = URL(string: "\(baseURL)/biomes.min.json")! let entitiesDownloadURL = URL(string: "\(baseURL)/entities.min.json")! let shapeRegistryDownloadURL = URL(string: "\(baseURL)/shapes.min.json")! - let itemsDownloadURL = URL(string: "https://gitlab.bixilon.de/bixilon/pixlyzer-data/-/raw/\(pixlyzerItemCommit)/version/\(version)/items.min.json")! + let itemsDownloadURL = URL( + string: + "https://gitlab.bixilon.de/bixilon/pixlyzer-data/-/raw/\(pixlyzerItemCommit)/version/\(version)/items.min.json" + )! // swiftlint:enable force_unwrapping // Load and decode pixlyzer data log.info("Downloading and decoding pixlyzer items") - let pixlyzerItems: [String: PixlyzerItem] = try downloadJSON(itemsDownloadURL, convertSnakeCase: false) + let pixlyzerItems: [String: PixlyzerItem] = try downloadJSON( + itemsDownloadURL, convertSnakeCase: false) log.info("Downloading and decoding pixlyzer fluids") - let pixlyzerFluids: [String: PixlyzerFluid] = try downloadJSON(fluidsDownloadURL, convertSnakeCase: true) + let pixlyzerFluids: [String: PixlyzerFluid] = try downloadJSON( + fluidsDownloadURL, convertSnakeCase: true) log.info("Downloading and decoding pixlyzer biomes") - let pixlyzerBiomes: [String: PixlyzerBiome] = try downloadJSON(biomesDownloadURL, convertSnakeCase: true) + let pixlyzerBiomes: [String: PixlyzerBiome] = try downloadJSON( + biomesDownloadURL, convertSnakeCase: true) log.info("Downloading and decoding pixlyzer blocks") - let pixlyzerBlocks: [String: PixlyzerBlock] = try downloadJSON(blocksDownloadURL, convertSnakeCase: false, useZippyJSON: false) + let pixlyzerBlocks: [String: PixlyzerBlock] = try downloadJSON( + blocksDownloadURL, convertSnakeCase: false, useZippyJSON: false) log.info("Downloading and decoding pixlyzer entities") - let pixlyzerEntities: [String: PixlyzerEntity] = try downloadJSON(entitiesDownloadURL, convertSnakeCase: true) + let pixlyzerEntities: [String: PixlyzerEntity] = try downloadJSON( + entitiesDownloadURL, convertSnakeCase: true) log.info("Downloading and decoding pixlyzer shapes") - let pixlyzerShapeRegistry: PixlyzerShapeRegistry = try downloadJSON(shapeRegistryDownloadURL, convertSnakeCase: false) + let pixlyzerShapeRegistry: PixlyzerShapeRegistry = try downloadJSON( + shapeRegistryDownloadURL, convertSnakeCase: false) // Process fluids log.info("Processing pixlyzer fluid registry") - let (fluidRegistry, pixlyzerFluidIdToFluidId) = try Self.createFluidRegistry(from: pixlyzerFluids) + let (fluidRegistry, pixlyzerFluidIdToFluidId) = try Self.createFluidRegistry( + from: pixlyzerFluids) // Process biomes log.info("Processing pixlyzer biome registry") @@ -147,10 +158,15 @@ public enum PixlyzerFormatter { } } - return (fluidRegistry: FluidRegistry(fluids: fluids), pixlyzerFluidIdToFluidId: pixlyzerFluidIdToFluidId) + return ( + fluidRegistry: FluidRegistry(fluids: fluids), + pixlyzerFluidIdToFluidId: pixlyzerFluidIdToFluidId + ) } - private static func createBiomeRegistry(from pixlyzerBiomes: [String: PixlyzerBiome]) throws -> BiomeRegistry { + private static func createBiomeRegistry(from pixlyzerBiomes: [String: PixlyzerBiome]) throws + -> BiomeRegistry + { var biomes: [Int: Biome] = [:] for (identifier, pixlyzerBiome) in pixlyzerBiomes { let identifier = try Identifier(identifier) @@ -161,31 +177,35 @@ public enum PixlyzerFormatter { return BiomeRegistry(biomes: biomes) } - private static func createEntityRegistry(from pixlyzerEntities: [String: PixlyzerEntity]) throws -> EntityRegistry { + private static func createEntityRegistry(from pixlyzerEntities: [String: PixlyzerEntity]) throws + -> EntityRegistry + { var entities: [Int: EntityKind] = [:] for (identifier, pixlyzerEntity) in pixlyzerEntities { if let identifier = try? Identifier(identifier) { - var isLiving = false var parent = pixlyzerEntity.parent + var inheritanceChain: [String] = [] while let currentParent = parent { - if currentParent == "LivingEntity" { - isLiving = true - break - } else { - guard - let parentEntity = - pixlyzerEntities[currentParent] - ?? pixlyzerEntities.values.first(where: { $0.class == currentParent }) - else { - throw PixlyzerError.missingEntity(currentParent) - } - parent = parentEntity.parent + inheritanceChain.append(currentParent) + + guard + let parentEntity = + pixlyzerEntities[currentParent] + ?? pixlyzerEntities.values.first(where: { $0.class == currentParent }) + else { + throw PixlyzerError.missingEntity(currentParent) } + + parent = parentEntity.parent } // Some entities don't correspond to Vanilla entity kinds (in which case the initializer returns nil, // not an error). - if let entity = try EntityKind(from: pixlyzerEntity, isLiving: isLiving, identifier: identifier) { + if let entity = try EntityKind( + from: pixlyzerEntity, + inheritanceChain: inheritanceChain, + identifier: identifier + ) { entities[entity.id] = entity } } @@ -239,7 +259,9 @@ public enum PixlyzerFormatter { } for (stateId, pixlyzerState) in pixlyzerBlock.states { - let isWaterlogged = pixlyzerState.properties?.waterlogged == true || BlockRegistry.waterloggedBlockClasses.contains(pixlyzerBlock.className) + let isWaterlogged = + pixlyzerState.properties?.waterlogged == true + || BlockRegistry.waterloggedBlockClasses.contains(pixlyzerBlock.className) let isBubbleColumn = identifier == Identifier(name: "block/bubble_column") let fluid = isWaterlogged || isBubbleColumn ? water : fluid let block = Block( @@ -277,7 +299,9 @@ public enum PixlyzerFormatter { return BlockRegistry(blocks: blockArray, renderDescriptors: renderDescriptors) } - private static func createItemRegistry(from pixlyzerItems: [String: PixlyzerItem]) throws -> ItemRegistry { + private static func createItemRegistry(from pixlyzerItems: [String: PixlyzerItem]) throws + -> ItemRegistry + { var items: [Int: Item] = [:] for (identifierString, pixlyzerItem) in pixlyzerItems { var identifier = try Identifier(identifierString)