diff --git a/Sources/Core/Sources/Cache/Registry/BlockRegistry+BinaryCacheable.swift b/Sources/Core/Sources/Cache/Registry/BlockRegistry+BinaryCacheable.swift index 195a5d58..c47dc7c2 100644 --- a/Sources/Core/Sources/Cache/Registry/BlockRegistry+BinaryCacheable.swift +++ b/Sources/Core/Sources/Cache/Registry/BlockRegistry+BinaryCacheable.swift @@ -38,7 +38,8 @@ extension Block: BinarySerializable { fluidState.serialize(into: &buffer) tint.serialize(into: &buffer) offset.serialize(into: &buffer) - material.serialize(into: &buffer) + vanillaMaterialIdentifier.serialize(into: &buffer) + physicalMaterial.serialize(into: &buffer) lightMaterial.serialize(into: &buffer) soundMaterial.serialize(into: &buffer) shape.serialize(into: &buffer) @@ -54,7 +55,8 @@ extension Block: BinarySerializable { fluidState: try .deserialize(from: &buffer), tint: try .deserialize(from: &buffer), offset: try .deserialize(from: &buffer), - material: try .deserialize(from: &buffer), + vanillaMaterialIdentifier: try .deserialize(from: &buffer), + physicalMaterial: try .deserialize(from: &buffer), lightMaterial: try .deserialize(from: &buffer), soundMaterial: try .deserialize(from: &buffer), shape: try .deserialize(from: &buffer), diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index 8c475641..45b54b08 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -93,6 +93,21 @@ public class PlayerInventory: Component { window.slots[Self.offHandIndex] } + /// The item in the currently selected hotbar slot, `nil` if the slot is empty + /// or the item stack is invalid. + public var mainHandItem: Item? { + guard let stack = hotbar[selectedHotbarSlot].stack else { + return nil + } + + guard let item = RegistryStore.shared.itemRegistry.item(withId: stack.itemId) else { + log.warning("Non-existent item with id \(stack.itemId) selected in hotbar") + return nil + } + + return item + } + /// Creates the player's inventory state. /// - Parameter selectedHotbarSlot: Defaults to 0 (the first slot from the left in the main hotbar). /// - Precondition: The length of `slots` must match ``PlayerInventory/slotCount``. diff --git a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift index 71315afa..9abdd743 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift @@ -171,7 +171,7 @@ public struct PlayerAccelerationSystem: System { z: Int(Foundation.floor(position.z)) ) let block = world.getBlock(at: blockPosition) - let slipperiness = block.material.slipperiness + let slipperiness = block.physicalMaterial.slipperiness speed = movementSpeed * 0.216 / (slipperiness * slipperiness * slipperiness) } else if isFlying { diff --git a/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift index 8e6d36c5..f88c3fc6 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift @@ -17,7 +17,7 @@ public struct PlayerFrictionSystem: System { var multiplier: Double = 0.91 if onGround.previousOnGround { let blockPosition = position.blockUnderneath - let material = world.getBlock(at: blockPosition).material + let material = world.getBlock(at: blockPosition).physicalMaterial multiplier *= material.slipperiness } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index c558d571..6fe327ee 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -76,6 +76,10 @@ public final class PlayerInputSystem: System { suppressInput = try handleChat(event, inputState, guiState) } + if !suppressInput { + suppressInput = try handleInventory(event, inventory, guiState, eventBus, connection) + } + if !suppressInput { suppressInput = try handleWindow(event, guiState, eventBus, connection) } @@ -89,17 +93,10 @@ public final class PlayerInputSystem: System { case .toggleDebugHUD: guiState.showDebugScreen = !guiState.showDebugScreen case .toggleInventory: - guiState.showInventory = !guiState.showInventory - if !guiState.showInventory { - // Weirdly enough, the vanilla client sends a close window packet when closing the player's - // inventory even though it never tells the server that it opened the inventory in the first - // place. Likely just for the server to verify the slots and chuck out anything in the crafting - // area. - try inventory.window.close(mouseStack: &guiState.mouseItemStack, eventBus: eventBus, connection: connection) - } else { - inputState.releaseAll() - eventBus.dispatch(ReleaseCursorEvent()) - } + // Closing the inventory is handled by `handleInventory` + guiState.showInventory = true + inputState.releaseAll() + eventBus.dispatch(ReleaseCursorEvent()) case .slot1: inventory.selectedHotbarSlot = 0 case .slot2: @@ -282,6 +279,31 @@ public final class PlayerInputSystem: System { return guiState.showChat } + /// - Returns: Whether to suppress the input associated with the event or not. + private func handleInventory( + _ event: KeyPressEvent, + _ inventory: PlayerInventory, + _ guiState: GUIStateStorage, + _ eventBus: EventBus, + _ connection: ServerConnection? + ) throws -> Bool { + guard guiState.showInventory else { + return false + } + + if event.key == .escape || event.input == .toggleInventory { + // Weirdly enough, the vanilla client sends a close window packet when closing the player's + // inventory even though it never tells the server that it opened the inventory in the first + // place. Likely just for the server to verify the slots and chuck out anything in the crafting + // area. + try inventory.window.close(mouseStack: &guiState.mouseItemStack, eventBus: eventBus, connection: connection) + guiState.showInventory = false + } + + return true + } + + /// - Returns: Whether to suppress the input associated with the event or not. private func handleWindow( _ event: KeyPressEvent, diff --git a/Sources/Core/Sources/ECS/Systems/PlayerJumpSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerJumpSystem.swift index d11ac07d..2185b7df 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerJumpSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerJumpSystem.swift @@ -30,7 +30,7 @@ public struct PlayerJumpSystem: System { ) let block = world.getBlock(at: blockPosition) - let jumpPower = 0.42 * Double(block.material.jumpVelocityMultiplier) + let jumpPower = 0.42 * Double(block.physicalMaterial.jumpVelocityMultiplier) velocity.y = jumpPower // Add a bit of extra acceleration if the player is sprinting (this makes sprint jumping faster than sprinting) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerVelocitySystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerVelocitySystem.swift index ce8afad1..df8653d5 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerVelocitySystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerVelocitySystem.swift @@ -32,7 +32,7 @@ public struct PlayerVelocitySystem: System { x: Int(position.x.rounded(.down)), y: Int((position.y - 0.5).rounded(.down)), z: Int(position.z.rounded(.down))) - let material = world.getBlock(at: blockPosition).material + let material = world.getBlock(at: blockPosition).physicalMaterial velocity.x *= material.velocityMultiplier velocity.z *= material.velocityMultiplier diff --git a/Sources/Core/Sources/Registry/Block/Block.swift b/Sources/Core/Sources/Registry/Block/Block.swift index f16999bc..10b9f598 100644 --- a/Sources/Core/Sources/Registry/Block/Block.swift +++ b/Sources/Core/Sources/Registry/Block/Block.swift @@ -17,8 +17,10 @@ public struct Block: Codable { public var tint: Tint? /// A type of random position offset to apply to the block. public var offset: Offset? + /// The material identifier that vanilla gives to this block. + public var vanillaMaterialIdentifier: Identifier /// Information about the physical properties of the block. - public var material: PhysicalMaterial + public var physicalMaterial: PhysicalMaterial /// Information about the way the block interacts with light. public var lightMaterial: LightMaterial /// Information about the sound properties of the block. @@ -48,7 +50,8 @@ public struct Block: Codable { fluidState: FluidState? = nil, tint: Tint? = nil, offset: Offset? = nil, - material: PhysicalMaterial, + vanillaMaterialIdentifier: Identifier, + physicalMaterial: PhysicalMaterial, lightMaterial: LightMaterial, soundMaterial: SoundMaterial, shape: Shape, @@ -61,7 +64,8 @@ public struct Block: Codable { self.fluidState = fluidState self.tint = tint self.offset = offset - self.material = material + self.vanillaMaterialIdentifier = vanillaMaterialIdentifier + self.physicalMaterial = physicalMaterial self.lightMaterial = lightMaterial self.soundMaterial = soundMaterial self.shape = shape @@ -104,7 +108,8 @@ public struct Block: Codable { fluidState: nil, tint: nil, offset: nil, - material: PhysicalMaterial.default, + vanillaMaterialIdentifier: Identifier(name: "missing"), + physicalMaterial: PhysicalMaterial.default, lightMaterial: LightMaterial.default, soundMaterial: SoundMaterial.default, shape: Shape.default, diff --git a/Sources/Core/Sources/Registry/Item/Item.swift b/Sources/Core/Sources/Registry/Item/Item.swift index a34ea9c4..7fa8a138 100644 --- a/Sources/Core/Sources/Registry/Item/Item.swift +++ b/Sources/Core/Sources/Registry/Item/Item.swift @@ -18,4 +18,123 @@ public struct Item: Codable { public var translationKey: String /// The id of the block corresponding to this item. public var blockId: Int? + /// The properties of the item specific to the type of item. `nil` if the item + /// a just a plain old item (e.g. a stick) rather than a tool or an armor piece + /// etc. + public var properties: Properties? + + public enum Properties: Codable { + case armor(ArmorProperties) + case tool(ToolProperties) + } + + public struct ArmorProperties: Codable { + public var equipmentSlot: EquipmentSlot + public var defense: Int + public var toughness: Double + public var material: Identifier + public var knockbackResistance: Double + + public init( + equipmentSlot: Item.ArmorProperties.EquipmentSlot, + defense: Int, + toughness: Double, + material: Identifier, + knockbackResistance: Double + ) { + self.equipmentSlot = equipmentSlot + self.defense = defense + self.toughness = toughness + self.material = material + self.knockbackResistance = knockbackResistance + } + + public enum EquipmentSlot: String, Codable { + case head + case chest + case legs + case feet + } + } + + public struct ToolProperties: Codable { + public var uses: Int + public var level: Int + public var speed: Double + public var attackDamage: Double + public var attackDamageBonus: Double + public var enchantmentValue: Int + /// Blocks that can be mined faster using this tool. Doesn't include + /// blocks covered by ``ToolProperties/effectiveMaterials``. + public var mineableBlocks: [Int] + /// When tools are used to right click blocks, they can cause the block + /// to change state, e.g. a log gets stripped if you right click it with + /// an axe. This mapping doesn't include blocks which are always right + /// clickable. + public var blockInteractions: [Int: Int] + public var kind: ToolKind + /// Materials which this tool is effective on. Used to minimise the length + /// of ``BlockInteractions/mineableBlocks`` by covering large categories of + /// blocks at a time. + public var effectiveMaterials: [Identifier] + + public init( + uses: Int, + level: Int, + speed: Double, + attackDamage: Double, + attackDamageBonus: Double, + enchantmentValue: Int, + mineableBlocks: [Int], + blockInteractions: [Int : Int], + kind: Item.ToolProperties.ToolKind, + effectiveMaterials: [Identifier] + ) { + self.uses = uses + self.level = level + self.speed = speed + self.attackDamage = attackDamage + self.attackDamageBonus = attackDamageBonus + self.enchantmentValue = enchantmentValue + self.mineableBlocks = mineableBlocks + self.blockInteractions = blockInteractions + self.kind = kind + self.effectiveMaterials = effectiveMaterials + } + + public func isEffective(on block: Block) -> Bool { + effectiveMaterials.contains(block.vanillaMaterialIdentifier) + || mineableBlocks.contains(block.id) + } + + public enum ToolKind: String, Codable { + case sword + case pickaxe + case shovel + case hoe + case axe + } + } + + public init( + id: Int, + identifier: Identifier, + rarity: ItemRarity, + maximumStackSize: Int, + maximumDamage: Int, + isFireResistant: Bool, + translationKey: String, + blockId: Int? = nil, + properties: Item.Properties? = nil + ) { + self.id = id + self.identifier = identifier + self.rarity = rarity + self.maximumStackSize = maximumStackSize + self.maximumDamage = maximumDamage + self.isFireResistant = isFireResistant + self.translationKey = translationKey + self.blockId = blockId + self.properties = properties + } } diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerBlock.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerBlock.swift index ccbd8f2e..9b6fb93c 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerBlock.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerBlock.swift @@ -66,7 +66,7 @@ extension Block { tint = nil } - let material = Block.PhysicalMaterial( + let physicalMaterial = Block.PhysicalMaterial( explosionResistance: pixlyzerBlock.explosionResistance, slipperiness: pixlyzerBlock.friction ?? 0.6, velocityMultiplier: pixlyzerBlock.velocityMultiplier ?? 1, @@ -124,7 +124,8 @@ extension Block { fluidState: fluidState, tint: tint, offset: pixlyzerBlock.offsetType, - material: material, + vanillaMaterialIdentifier: pixlyzerState.material, + physicalMaterial: physicalMaterial, lightMaterial: lightMaterial, soundMaterial: soundMaterial, shape: shape, diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift index b5bf52a7..8db5ec39 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift @@ -251,7 +251,7 @@ public enum PixlyzerFormatter { for (identifierString, pixlyzerItem) in pixlyzerItems { var identifier = try Identifier(identifierString) identifier.name = "item/\(identifier.name)" - let item = Item(from: pixlyzerItem, identifier: identifier) + let item = try Item(from: pixlyzerItem, identifier: identifier) items[item.id] = item } diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift index 61c34b30..175244e2 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift @@ -1,5 +1,34 @@ import Foundation +public enum PixlyzerItemError: LocalizedError { + case missingRequiredPropertiesForEquipment(id: Int) + case missingRequiredPropertiesForTool(id: Int) + case unhandledToolClass(String) + case axeMissingStrippableBlocks(id: Int) + case hoeMissingTillableBlocks(id: Int) + case shovelMissingFlattenableBlocks(id: Int) + case invalidBlockIdInteger(String) + + public var errorDescription: String? { + switch self { + case let .missingRequiredPropertiesForEquipment(id): + return "Missing required properties for equipment item with id: \(id)." + case let .missingRequiredPropertiesForTool(id): + return "Missing required properties for tool item with id: \(id)." + case let .unhandledToolClass(className): + return "Encountered unhandled tool class '\(className)'." + case let .axeMissingStrippableBlocks(id): + return "Axe with id '\(id)' missing strippable blocks." + case let .hoeMissingTillableBlocks(id): + return "Hoe with id '\(id)' missing tillable blocks." + case let .shovelMissingFlattenableBlocks(id): + return "Shovel with id '\(id)' missing flattenable blocks." + case let .invalidBlockIdInteger(id): + return "Invalid block id '\(id)' (expected an integer)." + } + } +} + public struct PixlyzerItem: Decodable { public var id: Int public var category: Int? @@ -9,6 +38,22 @@ public struct PixlyzerItem: Decodable { public var isFireResistant: Bool public var isComplex: Bool public var translationKey: String + public var equipmentSlot: Item.ArmorProperties.EquipmentSlot? + public var defense: Int? + public var toughness: Double? + public var armorMaterial: Identifier? + public var knockbackResistance: Double? + public var uses: Int? + public var speed: Double? + public var attackDamage: Double? + public var attackDamageBonus: Double? + public var level: Int? + public var enchantmentValue: Int? + public var diggableBlocks: [Int]? + public var strippableBlocks: [String: Int]? + public var tillableBlocks: [String: Int]? + public var flattenableBlocks: [String: Int]? + public var effectiveMaterials: [Identifier]? public var block: Int? public var className: String @@ -21,20 +66,117 @@ public struct PixlyzerItem: Decodable { case isFireResistant = "is_fire_resistant" case isComplex = "is_complex" case translationKey = "translation_key" + case equipmentSlot = "equipment_slot" + case defense + case toughness + case armorMaterial = "armor_material" + case knockbackResistance = "knockback_resistance" + case uses + case speed + case attackDamage = "attack_damage" + case attackDamageBonus = "attack_damage_bonus" + case level + case enchantmentValue = "enchantment_value" + case diggableBlocks = "diggable_blocks" + case strippableBlocks = "strippables_blocks" + case tillableBlocks = "tillables_block_states" + case flattenableBlocks = "flattenables_block_states" + case effectiveMaterials = "effective_materials" case block case className = "class" } } extension Item { - public init(from pixlyzerItem: PixlyzerItem, identifier: Identifier) { + public init(from pixlyzerItem: PixlyzerItem, identifier: Identifier) throws { id = pixlyzerItem.id self.identifier = identifier rarity = pixlyzerItem.rarity maximumStackSize = pixlyzerItem.maximumStackSize maximumDamage = pixlyzerItem.maximumDamage isFireResistant = pixlyzerItem.isFireResistant - translationKey = "" // pixlyzerItem.translationKey + translationKey = pixlyzerItem.translationKey blockId = pixlyzerItem.block + + if let equipmentSlot = pixlyzerItem.equipmentSlot { + guard + let defense = pixlyzerItem.defense, + let toughness = pixlyzerItem.toughness, + let armorMaterial = pixlyzerItem.armorMaterial, + let knockbackResistance = pixlyzerItem.knockbackResistance + else { + throw PixlyzerItemError.missingRequiredPropertiesForEquipment(id: pixlyzerItem.id) + } + + properties = .armor(Item.ArmorProperties( + equipmentSlot: equipmentSlot, + defense: defense, + toughness: toughness, + material: armorMaterial, + knockbackResistance: knockbackResistance + )) + } else if let uses = pixlyzerItem.uses { + guard + let speed = pixlyzerItem.speed, + let attackDamage = pixlyzerItem.attackDamage, + let attackDamageBonus = pixlyzerItem.attackDamageBonus, + let level = pixlyzerItem.level, + let enchantmentValue = pixlyzerItem.enchantmentValue + else { + throw PixlyzerItemError.missingRequiredPropertiesForTool(id: pixlyzerItem.id) + } + + let interactions: [String: Int] + let kind: Item.ToolProperties.ToolKind + switch pixlyzerItem.className { + case "SwordItem": + interactions = [:] + kind = .sword + case "PickaxeItem": + interactions = [:] + kind = .pickaxe + case "AxeItem": + guard let strippableBlocks = pixlyzerItem.strippableBlocks else { + throw PixlyzerItemError.axeMissingStrippableBlocks(id: pixlyzerItem.id) + } + interactions = strippableBlocks + kind = .axe + case "ShovelItem": + guard let flattenableBlocks = pixlyzerItem.flattenableBlocks else { + throw PixlyzerItemError.shovelMissingFlattenableBlocks(id: pixlyzerItem.id) + } + interactions = flattenableBlocks + kind = .shovel + case "HoeItem": + guard let tillableBlocks = pixlyzerItem.tillableBlocks else { + throw PixlyzerItemError.hoeMissingTillableBlocks(id: pixlyzerItem.id) + } + interactions = tillableBlocks + kind = .hoe + default: + throw PixlyzerItemError.unhandledToolClass(pixlyzerItem.className) + } + + var parsedInteractions: [Int: Int] = [:] + for (key, value) in interactions { + guard let parsedKey = Int(key) else { + throw PixlyzerItemError.invalidBlockIdInteger(key) + } + parsedInteractions[parsedKey] = value + } + + properties = .tool(Item.ToolProperties( + uses: uses, + level: level, + speed: speed, + attackDamage: attackDamage, + attackDamageBonus: attackDamageBonus, + enchantmentValue: enchantmentValue, + mineableBlocks: pixlyzerItem.diggableBlocks ?? [], + blockInteractions: parsedInteractions, + kind: kind, + effectiveMaterials: pixlyzerItem.effectiveMaterials ?? [] + )) + } } }