From 4d7f4340751dfe3c34c60fbeb1ca7c0c0e49dc89 Mon Sep 17 00:00:00 2001 From: stackotter Date: Fri, 15 Dec 2023 22:04:22 +1000 Subject: [PATCH 01/84] Render inventory items (except for items in the armor, offhand, and crafting slots) --- Sources/Core/Renderer/GUI/GUI.swift | 49 ++++++++++++++++++- .../ECS/Components/PlayerInventory.swift | 21 ++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift index 97d8fcf5..6a9beb18 100644 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ b/Sources/Core/Renderer/GUI/GUI.swift @@ -133,7 +133,54 @@ struct GUI { ), .center ) - parentGroup.add(GUISprite.inventory, .center) + + var group = GUIGroupElement([GUISprite.inventory.descriptor.size.x, GUISprite.inventory.descriptor.size.y]) + group.add(GUISprite.inventory, .center) + + let (mainAreaRows, hotbar) = client.game.accessPlayer { player in + (player.inventory.mainAreaRows, player.inventory.hotbar) + } + + for (y, row) in mainAreaRows.enumerated() { + for (x, slot) in row.enumerated() { + if let stack = slot.stack { + group.add(GUIInventoryItem(itemId: stack.itemId), .top(18 * y + 84), .left(18 * x + 8)) + + // Item count + // TODO: Move count rendering into GUIInventoryItem + if stack.count != 1 { + let bottom = 18 * (mainAreaRows.count - y - 1) + 29 + let right = 18 * (row.count - x - 1) + 8 + group.add( + GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), + .bottom(bottom - 1), + .right(right - 1) + ) + group.add(String(stack.count), .bottom(bottom), .right(right)) + } + } + } + } + + for (x, slot) in hotbar.enumerated() { + if let stack = slot.stack { + group.add(GUIInventoryItem(itemId: stack.itemId), .top(142), .left(18 * x + 8)) + + // Item count + if stack.count != 1 { + let bottom = 8 + let right = 18 * (hotbar.count - x - 1) + 8 + group.add( + GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), + .bottom(bottom - 1), + .right(right - 1) + ) + group.add(String(stack.count), .bottom(bottom), .right(right)) + } + } + } + + parentGroup.add(group, .center) } func chat( diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index f9de84d4..99fb17cb 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -10,6 +10,13 @@ public class PlayerInventory: Component { public static let hotbarSlotStartIndex = 36 /// The index of the last hotbar slot. public static let hotbarSlotEndIndex = 44 + /// The index of the first slot of the main inventory area (the 3 by 9 grid). + public static let mainAreaStartIndex = 9 + + /// The width of the main area. + public static let mainAreaWidth = 9 + /// The height of the main area. + public static let mainAreaHeight = 3 /// The inventory's contents. public var slots: [Slot] @@ -20,6 +27,20 @@ public class PlayerInventory: Component { return Array(slots[Self.hotbarSlotStartIndex...Self.hotbarSlotEndIndex]) } + /// The rows of the main 3 by 9 area of the inventory. + public var mainAreaRows: [[Slot]] { + var rows: [[Slot]] = [] + for y in 0.. Date: Fri, 15 Dec 2023 23:11:33 +1000 Subject: [PATCH 02/84] Fix rotation of block items in inventory and hotbar For the fix to take effect you need to delete your resource pack cache. When I implement cache versioning that won't be required anymore. Definitely need to do that soon. --- Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift | 6 +++--- .../Sources/Resources/Model/Block/BlockModelPalette.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift index eb53611f..788b555b 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift @@ -36,9 +36,9 @@ struct GUIInventoryItem: GUIElement { } transformation *= MatrixUtil.translationMatrix([-0.5, -0.5, -0.5]) - transformation *= MatrixUtil.rotationMatrix(x: .pi) - transformation *= MatrixUtil.rotationMatrix(y: -.pi / 4) - transformation *= MatrixUtil.rotationMatrix(x: -.pi / 6) + * MatrixUtil.rotationMatrix(x: .pi) + * MatrixUtil.rotationMatrix(y: -.pi / 4) + * MatrixUtil.rotationMatrix(x: -.pi / 6) var geometry = Geometry() var translucentGeometry = SortableMeshElement() diff --git a/Sources/Core/Sources/Resources/Model/Block/BlockModelPalette.swift b/Sources/Core/Sources/Resources/Model/Block/BlockModelPalette.swift index 88f4656d..20766842 100644 --- a/Sources/Core/Sources/Resources/Model/Block/BlockModelPalette.swift +++ b/Sources/Core/Sources/Resources/Model/Block/BlockModelPalette.swift @@ -136,7 +136,7 @@ public struct BlockModelPalette: Equatable { for: [BlockModelRenderDescriptor( model: identifier, xRotationDegrees: 0, - yRotationDegrees: 0, + yRotationDegrees: 90, uvLock: false )], from: intermediateBlockModelPalette, From 30d89c7a802548cc11e711f6719909e90674d9d6 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 17 Dec 2023 22:11:13 +1000 Subject: [PATCH 03/84] Create GUIInventorySlot GUI element to reduce boilerplate in inventory rendering code --- Sources/Core/Renderer/GUI/GUI.swift | 44 ++----------------- .../GUI/GUIElement/GUIInventorySlot.swift | 30 +++++++++++++ 2 files changed, 33 insertions(+), 41 deletions(-) create mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift index 6a9beb18..91c5e5bc 100644 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ b/Sources/Core/Renderer/GUI/GUI.swift @@ -143,41 +143,12 @@ struct GUI { for (y, row) in mainAreaRows.enumerated() { for (x, slot) in row.enumerated() { - if let stack = slot.stack { - group.add(GUIInventoryItem(itemId: stack.itemId), .top(18 * y + 84), .left(18 * x + 8)) - - // Item count - // TODO: Move count rendering into GUIInventoryItem - if stack.count != 1 { - let bottom = 18 * (mainAreaRows.count - y - 1) + 29 - let right = 18 * (row.count - x - 1) + 8 - group.add( - GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), - .bottom(bottom - 1), - .right(right - 1) - ) - group.add(String(stack.count), .bottom(bottom), .right(right)) - } - } + group.add(GUIInventorySlot(slot: slot), .top(18 * y + 84), .left(18 * x + 8)) } } for (x, slot) in hotbar.enumerated() { - if let stack = slot.stack { - group.add(GUIInventoryItem(itemId: stack.itemId), .top(142), .left(18 * x + 8)) - - // Item count - if stack.count != 1 { - let bottom = 8 - let right = 18 * (hotbar.count - x - 1) + 8 - group.add( - GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), - .bottom(bottom - 1), - .right(right - 1) - ) - group.add(String(stack.count), .bottom(bottom), .right(right)) - } - } + group.add(GUIInventorySlot(slot: slot), .top(142), .left(18 * x + 8)) } parentGroup.add(group, .center) @@ -295,16 +266,7 @@ struct GUI { group.add(GUISprite.selectedHotbarSlot, .bottom(0), .left(20 * selectedSlot)) for (i, slot) in slots.enumerated() { - if let stack = slot.stack { - group.add(GUIInventoryItem(itemId: stack.itemId), .bottom(4), .left(20 * i + 4)) - - // Item count - if stack.count != 1 { - let offset = 20 * (8 - i) + 4 - group.add(GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), .bottom(2), .right(offset - 1)) - group.add(String(stack.count), .bottom(3), .right(offset)) - } - } + group.add(GUIInventorySlot(slot: slot), .bottom(4), .left(20 * i + 4)) } } diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift new file mode 100644 index 00000000..d14cdc2d --- /dev/null +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift @@ -0,0 +1,30 @@ +import FirebladeMath +import DeltaCore + +struct GUIInventorySlot: GUIElement { + var slot: Slot + + func meshes(context: GUIContext) throws -> [GUIElementMesh] { + guard let stack = slot.stack else { + return [] + } + + // Starts in the upper left corner of the slot and extends 1 pixel further to the + // right and downwards (due to the placement of the count text). + var group = GUIGroupElement(Vec2i(17, 18)) + group.add(GUIInventoryItem(itemId: stack.itemId), .position(0, 0)) + + if stack.count != 1 { + // Drop shadow for the count + group.add( + GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), + .bottom(0), + .right(0) + ) + + group.add(String(stack.count), .bottom(1), .right(1)) + } + + return try group.meshes(context: context) + } +} From 4abf4bf265e6d3b51234e3b9f13d97dda97fd574 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 20 Dec 2023 20:04:23 +1000 Subject: [PATCH 04/84] Render remaining inventory slots (armor, off-hand, and crafting) --- Sources/Core/Renderer/GUI/GUI.swift | 27 ++++++++-- .../Renderer/GUI/GUIElement/GUIList.swift | 2 +- .../Core/Renderer/Shader/ScreenShaders.metal | 1 - .../ECS/Components/PlayerInventory.swift | 50 ++++++++++++++++++- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift index 91c5e5bc..2aaad4bc 100644 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ b/Sources/Core/Renderer/GUI/GUI.swift @@ -137,11 +137,32 @@ struct GUI { var group = GUIGroupElement([GUISprite.inventory.descriptor.size.x, GUISprite.inventory.descriptor.size.y]) group.add(GUISprite.inventory, .center) - let (mainAreaRows, hotbar) = client.game.accessPlayer { player in - (player.inventory.mainAreaRows, player.inventory.hotbar) + let (armorSlots, offHand, craftingArea, craftingResult, mainArea, hotbar) = client.game.accessPlayer { player in + ( + player.inventory.armorSlots, + player.inventory.offHand, + player.inventory.craftingArea, + player.inventory.craftingResult, + player.inventory.mainArea, + player.inventory.hotbar + ) + } + + for (y, slot) in armorSlots.enumerated() { + group.add(GUIInventorySlot(slot: slot), .top(18 * y + 8), .left(8)) } - for (y, row) in mainAreaRows.enumerated() { + group.add(GUIInventorySlot(slot: offHand), .top(62), .left(77)) + + for (y, row) in craftingArea.enumerated() { + for (x, slot) in row.enumerated() { + group.add(GUIInventorySlot(slot: slot), .top(18 * y + 18), .left(18 * x + 98)) + } + } + + group.add(GUIInventorySlot(slot: craftingResult), .top(28), .left(154)) + + for (y, row) in mainArea.enumerated() { for (x, slot) in row.enumerated() { group.add(GUIInventorySlot(slot: slot), .top(18 * y + 84), .left(18 * x + 8)) } diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift index 0709611d..45013f14 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift @@ -33,7 +33,7 @@ struct GUIList: GUIElement { var maxWidth = 0 var currentY = 0 - // Find the widest element while caching meshes to prevent generating them twice + // Cache meshes while finding widest element to prevent generating them again. for item in items { switch item { case .element(let element): diff --git a/Sources/Core/Renderer/Shader/ScreenShaders.metal b/Sources/Core/Renderer/Shader/ScreenShaders.metal index eb2c455b..b9e14381 100644 --- a/Sources/Core/Renderer/Shader/ScreenShaders.metal +++ b/Sources/Core/Renderer/Shader/ScreenShaders.metal @@ -28,7 +28,6 @@ fragment float4 screenFragmentFunction(QuadVertex vert [[stage_in]], depth2d offscreenResultDepth [[texture(1)]], constant struct FogUniforms &fogUniforms [[buffer(0)]]) { constexpr sampler smplr(coord::normalized); - float z = offscreenResultDepth.sample(smplr, vert.uv); float4 color = offscreenResult.sample(smplr, vert.uv); return color; }; diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index 99fb17cb..c7bcb659 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -10,14 +10,31 @@ public class PlayerInventory: Component { public static let hotbarSlotStartIndex = 36 /// The index of the last hotbar slot. public static let hotbarSlotEndIndex = 44 + /// The index of the first slot of the main inventory area (the 3 by 9 grid). public static let mainAreaStartIndex = 9 - /// The width of the main area. public static let mainAreaWidth = 9 /// The height of the main area. public static let mainAreaHeight = 3 + /// The index of the first slot of the inventory's crafting area. + public static let craftingAreaStartIndex = 1 + /// The width of the inventory's crafting area. + public static let craftingAreaWidth = 2 + /// The height of the inventory's crafting area. + public static let craftingAreaHeight = 2 + /// The index of the crafting result slot. + public static let craftingResultIndex = 0 + + /// The index of the first armor slot. + public static let armorSlotsStartIndex = 5 + /// The number of armor slots. + public static let armorSlotsCount = 4 + + /// The index of the player's off-hand slot. + public static let offHandIndex = 45 + /// The inventory's contents. public var slots: [Slot] /// The player's currently selected hotbar slot. @@ -28,7 +45,7 @@ public class PlayerInventory: Component { } /// The rows of the main 3 by 9 area of the inventory. - public var mainAreaRows: [[Slot]] { + public var mainArea: [[Slot]] { var rows: [[Slot]] = [] for y in 0.. Date: Wed, 20 Dec 2023 23:03:53 +1000 Subject: [PATCH 05/84] Get absolute mouse position in InputView (coordinate systems are annoying) and make it available to DeltaCore via InputState --- Sources/Client/Extensions/View.swift | 15 +++++ Sources/Client/Views/Play/GameView.swift | 4 +- Sources/Client/Views/Play/InputView.swift | 57 +++++++++++++++++-- Sources/Core/Sources/Client.swift | 16 +++++- .../Core/Sources/ECS/Singles/InputState.swift | 22 +++++-- Sources/Core/Sources/Game.swift | 10 +++- 6 files changed, 108 insertions(+), 16 deletions(-) diff --git a/Sources/Client/Extensions/View.swift b/Sources/Client/Extensions/View.swift index e5361faa..e034e0f8 100644 --- a/Sources/Client/Extensions/View.swift +++ b/Sources/Client/Extensions/View.swift @@ -53,4 +53,19 @@ extension View { action(argument1, argument2, argument3) } } + + /// Appends an action to an action stored property. Useful for implementing custom + /// view modifiers such as `onClick` etc. Allows the modifier to be called multiple + /// times without overwriting previous actions. If the stored action is nil, the + /// given action becomes the stored action, otherwise the new action is appended to + /// the existing action. + func appendingAction( + to keyPath: WritableKeyPath Void)?>, + _ action: @escaping (T, U, V, W) -> Void + ) -> Self { + with(keyPath) { argument1, argument2, argument3, argument4 in + self[keyPath: keyPath]?(argument1, argument2, argument3, argument4) + action(argument1, argument2, argument3, argument4) + } + } } diff --git a/Sources/Client/Views/Play/GameView.swift b/Sources/Client/Views/Play/GameView.swift index 4ab134ad..cd33bf71 100644 --- a/Sources/Client/Views/Play/GameView.swift +++ b/Sources/Client/Views/Play/GameView.swift @@ -61,11 +61,11 @@ struct GameView: View { .onKeyRelease { [weak client] key in client?.release(key) } - .onMouseMove { [weak client] deltaX, deltaY in + .onMouseMove { [weak client] x, y, deltaX, deltaY in // TODO: Formalise this adjustment factor somewhere let sensitivityAdjustmentFactor: Float = 0.004 let sensitivity = sensitivityAdjustmentFactor * managedConfig.mouseSensitivity - client?.moveMouse(sensitivity * deltaX, sensitivity * deltaY) + client?.moveMouse(x: x, y: y, deltaX: sensitivity * deltaX, deltaY: sensitivity * deltaY) } .passthroughClicks(!cursorCaptured) } diff --git a/Sources/Client/Views/Play/InputView.swift b/Sources/Client/Views/Play/InputView.swift index 374abdf3..3bb8e8e4 100644 --- a/Sources/Client/Views/Play/InputView.swift +++ b/Sources/Client/Views/Play/InputView.swift @@ -2,6 +2,9 @@ import SwiftUI import DeltaCore struct InputView: View { + @EnvironmentObject var modal: Modal + @EnvironmentObject var appState: StateWrapper + @State var monitorsAdded = false @State var scrollWheelDeltaY: Float = 0 @@ -15,7 +18,12 @@ struct InputView: View { private var handleKeyRelease: ((Key) -> Void)? private var handleKeyPress: ((Key, [Character]) -> Void)? - private var handleMouseMove: ((_ deltaX: Float, _ deltaY: Float) -> Void)? + private var handleMouseMove: (( + _ x: Float, + _ y: Float, + _ deltaX: Float, + _ deltaY: Float + ) -> Void)? private var handleScroll: ((_ deltaY: Float) -> Void)? private var shouldPassthroughClicks = false @@ -67,7 +75,7 @@ struct InputView: View { } /// Adds an action to run when the mouse is moved. - func onMouseMove(_ action: @escaping (_ deltaX: Float, _ deltaY: Float) -> Void) -> Self { + func onMouseMove(_ action: @escaping (_ x: Float, _ y: Float, _ deltaX: Float, _ deltaY: Float) -> Void) -> Self { appendingAction(to: \.handleMouseMove, action) } @@ -76,8 +84,39 @@ struct InputView: View { appendingAction(to: \.handleScroll, action) } + func mousePositionInView(with geometry: GeometryProxy) -> (x: Float, y: Float)? { + // This assumes that there's only one window and that this is only called once + // the view's body has been evaluated at least once. + guard let window = NSApplication.shared.orderedWindows.first else { + return nil + } + + let viewFrame = geometry.frame(in: .global) + let x = (NSEvent.mouseLocation.x - window.frame.minX) - viewFrame.minX + let y = window.frame.maxY - NSEvent.mouseLocation.y - viewFrame.minY + return (Float(x), Float(y)) + } + var body: some View { - content() + GeometryReader { geometry in + contentWithEventListeners(geometry) + } + } + + func contentWithEventListeners(_ geometry: GeometryProxy) -> some View { + // Make sure that the latest position is known to any observers (e.g. if + // listening was disabled and now isn't, observers won't have been told + // about any changes that occured during the period in which listening + // was disabled). + if let mousePosition = mousePositionInView(with: geometry) { + handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0) + } else { + modal.error("Failed to get mouse position (on demand)") { + appState.update(to: .serverList) + } + } + + return content() .frame(maxWidth: .infinity, maxHeight: .infinity) #if os(iOS) .gesture(TapGesture(count: 2).onEnded { _ in @@ -104,10 +143,20 @@ struct InputView: View { return event } + guard let mousePosition = mousePositionInView(with: geometry) else { + modal.error("Failed to get mouse position") { + appState.update(to: .serverList) + } + return event + } + + let x = mousePosition.x + let y = mousePosition.y + let deltaX = Float(event.deltaX) let deltaY = Float(event.deltaY) - handleMouseMove?(deltaX, deltaY) + handleMouseMove?(x, y, deltaX, deltaY) return event }) diff --git a/Sources/Core/Sources/Client.swift b/Sources/Core/Sources/Client.swift index 83a785f1..6e63f17e 100644 --- a/Sources/Core/Sources/Client.swift +++ b/Sources/Core/Sources/Client.swift @@ -129,11 +129,23 @@ public final class Client: @unchecked Sendable { } /// Moves the mouse. + /// + /// `deltaX` and `deltaY` aren't just the difference between the current and + /// previous values of `x` and `y` because there are ways for the mouse to + /// appear at a new position without causing in-game movement (e.g. if the + /// user opens the in-game menu, moves the mouse, and then closes the in-game + /// menu). /// - Parameters: + /// - x: The absolute mouse x (relative to the play area's top left corner). + /// - y: The absolute mouse y (relative to the play area's top left corner). /// - deltaX: The change in mouse x. /// - deltaY: The change in mouse y. - public func moveMouse(_ deltaX: Float, _ deltaY: Float) { - game.moveMouse(deltaX, deltaY) + public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) { + // TODO: Update this API (and everything else reliant on it) so that DeltaCore + // is the one that decides which input events to ignore (instead of InputView + // (and similar) deciding whether an event should be given to Client or not). + // This will allow the deltaX and deltaY parameters to be removed. + game.moveMouse(x: x, y: y, deltaX: deltaX, deltaY: deltaY) } /// Moves the left thumbstick. diff --git a/Sources/Core/Sources/ECS/Singles/InputState.swift b/Sources/Core/Sources/ECS/Singles/InputState.swift index 3a4b113f..512ac646 100644 --- a/Sources/Core/Sources/ECS/Singles/InputState.swift +++ b/Sources/Core/Sources/ECS/Singles/InputState.swift @@ -3,14 +3,15 @@ import FirebladeMath /// The game's input state. public final class InputState: SingleComponent { - /// The maximum number of ticks between consecutive inputs to count as a double tap. + /// The maximum number of ticks between consecutive inputs to count as a + /// double tap. public static let maximumDoubleTapDelay = 6 - /// The newly pressed keys in the order that they were pressed. Only includes presses since last - /// call to ``flushInputs()``. + /// The newly pressed keys in the order that they were pressed. Only includes + /// presses since last call to ``flushInputs()``. public private(set) var newlyPressed: [KeyPressEvent] = [] - /// The newly released keys in the order that they were released. Only includes releases since - /// last call to ``flushInputs()``. + /// The newly released keys in the order that they were released. Only includes + /// releases since last call to ``flushInputs()``. public private(set) var newlyReleased: [KeyReleaseEvent] = [] /// The currently pressed keys. @@ -18,6 +19,8 @@ public final class InputState: SingleComponent { /// The currently pressed inputs. public private(set) var inputs: Set = [] + /// The current absolute mouse position relative to the play area's top left corner. + public private(set) var mousePosition: Vec2f = Vec2f(0, 0) /// The mouse delta since the last call to ``resetMouseDelta()``. public private(set) var mouseDelta: Vec2f = Vec2f(0, 0) /// The position of the left thumbstick. @@ -78,11 +81,18 @@ public final class InputState: SingleComponent { } /// Updates the current mouse delta by adding the given delta. + /// + /// See ``Client/moveMouse(x:y:deltaX:deltaY:)`` for the reasoning behind + /// having both absolute and relative parameters (it's currently necessary + /// but could be fixed by cleaning up the input handling architecture). /// - Parameters: + /// - x: The absolute mouse x (relative to the play area's top left corner). + /// - y: The absolute mouse y (relative to the play area's top left corner). /// - deltaX: The change in mouse x. /// - deltaY: The change in mouse y. - public func moveMouse(_ deltaX: Float, _ deltaY: Float) { + public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) { mouseDelta += Vec2f(deltaX, deltaY) + mousePosition = Vec2f(x, y) } /// Updates the current position of the left thumbstick. diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index be5ab45f..4d7264cb 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -130,13 +130,19 @@ public final class Game: @unchecked Sendable { } /// Moves the mouse. + /// + /// See ``Client/moveMouse(x:y:deltaX:deltaY:)`` for the reasoning behind + /// having both absolute and relative parameters (it's currently necessary + /// but could be fixed by cleaning up the input handling architecture). /// - Parameters: + /// - x: The absolute mouse x (relative to the play area's top left corner). + /// - y: The absolute mouse y (relative to the play area's top left corner). /// - deltaX: The change in mouse x. /// - deltaY: The change in mouse y. - public func moveMouse(_ deltaX: Float, _ deltaY: Float) { + public func moveMouse(x: Float, y: Float, deltaX: Float, deltaY: Float) { nexusLock.acquireWriteLock() defer { nexusLock.unlock() } - inputState.moveMouse(deltaX, deltaY) + inputState.moveMouse(x: x, y: y, deltaX: deltaX, deltaY: deltaY) } /// Moves the left thumbstick. From 5801b3fa924465d6bacae1dfd2c56eea91c45ca3 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 14 Jan 2024 13:22:07 +1000 Subject: [PATCH 06/84] Introduce GUIFixedSizeElement protocol to workaround some annoying constraint solving limitations (fixes #189) --- Sources/Core/Renderer/GUI/GUI.swift | 2 +- Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift | 4 ++-- Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift | 4 +++- Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift | 4 +++- Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift | 5 +++++ 5 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift index 2aaad4bc..d3c31d3f 100644 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ b/Sources/Core/Renderer/GUI/GUI.swift @@ -287,7 +287,7 @@ struct GUI { group.add(GUISprite.selectedHotbarSlot, .bottom(0), .left(20 * selectedSlot)) for (i, slot) in slots.enumerated() { - group.add(GUIInventorySlot(slot: slot), .bottom(4), .left(20 * i + 4)) + group.add(GUIInventorySlot(slot: slot), .bottom(2), .left(20 * i + 4)) } } diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift index 5a81fdf1..f71916e6 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift @@ -1,6 +1,6 @@ import FirebladeMath -struct GUIGroupElement: GUIElement { +struct GUIGroupElement: GUIFixedSizeElement { var size: Vec2i var children: [(GUIElement, Constraints)] @@ -27,7 +27,7 @@ struct GUIGroupElement: GUIElement { var elementMeshes = try element.meshes(context: context) let elementSize: Vec2i - if let group = element as? GUIGroupElement { + if let group = element as? GUIFixedSizeElement { elementSize = group.size } else { elementSize = elementMeshes.size() diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift index 788b555b..e5c363c4 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift @@ -2,9 +2,11 @@ import Foundation import FirebladeMath import DeltaCore -struct GUIInventoryItem: GUIElement { +struct GUIInventoryItem: GUIElement, GUIFixedSizeElement { var itemId: Int + let size = Vec2i(16, 16) + func meshes(context: GUIContext) throws -> [GUIElementMesh] { guard let model = context.itemModelPalette.model(for: itemId) else { throw GUIRendererError.invalidItemId(itemId) diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift index d14cdc2d..f421bdbd 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift +++ b/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift @@ -1,9 +1,11 @@ import FirebladeMath import DeltaCore -struct GUIInventorySlot: GUIElement { +struct GUIInventorySlot: GUIElement, GUIFixedSizeElement { var slot: Slot + let size = Vec2i(17, 18) + func meshes(context: GUIContext) throws -> [GUIElementMesh] { guard let stack = slot.stack else { return [] diff --git a/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift b/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift new file mode 100644 index 00000000..f41399d7 --- /dev/null +++ b/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift @@ -0,0 +1,5 @@ +import FirebladeMath + +protocol GUIFixedSizeElement: GUIElement { + var size: Vec2i { get } +} From 612d8807f9988ac4d5b2257c5d8ef14434cb707a Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 14 Apr 2024 00:36:08 +1000 Subject: [PATCH 07/84] Fix KeymapEditorView layout by making InputView's internal GeometryReader toggleable (and disabled in KeymapEditorView) --- Sources/Client/Views/Play/InputView.swift | 35 +++++++++++++------ .../Views/Settings/KeymapEditorView.swift | 1 + .../Core/Renderer/World/WorldRenderer.swift | 2 +- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Sources/Client/Views/Play/InputView.swift b/Sources/Client/Views/Play/InputView.swift index 3bb8e8e4..73a78373 100644 --- a/Sources/Client/Views/Play/InputView.swift +++ b/Sources/Client/Views/Play/InputView.swift @@ -26,6 +26,7 @@ struct InputView: View { ) -> Void)? private var handleScroll: ((_ deltaY: Float) -> Void)? private var shouldPassthroughClicks = false + private var shouldAvoidGeometryReader = false init( listening: Binding, @@ -64,6 +65,13 @@ struct InputView: View { with(\.shouldPassthroughClicks, passthroughClicks) } + /// When `true`, `GeometryReader` won't be used, but mouse movements + /// won't be tracked. This can be used when mouse movements aren't + /// required but the `GeometryReader` is messing with layouts. + func avoidGeometryReader(_ avoidGeometryReader: Bool = true) -> Self { + with(\.shouldAvoidGeometryReader, avoidGeometryReader) + } + /// Adds an action to run when a key is released. func onKeyRelease(_ action: @escaping (Key) -> Void) -> Self { appendingAction(to: \.handleKeyRelease, action) @@ -98,26 +106,33 @@ struct InputView: View { } var body: some View { - GeometryReader { geometry in - contentWithEventListeners(geometry) + VStack { + if shouldAvoidGeometryReader { + contentWithEventListeners() + } else { + GeometryReader { geometry in + contentWithEventListeners(geometry) + } + } } } - func contentWithEventListeners(_ geometry: GeometryProxy) -> some View { + func contentWithEventListeners(_ geometry: GeometryProxy? = nil) -> some View { // Make sure that the latest position is known to any observers (e.g. if // listening was disabled and now isn't, observers won't have been told // about any changes that occured during the period in which listening // was disabled). - if let mousePosition = mousePositionInView(with: geometry) { - handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0) - } else { - modal.error("Failed to get mouse position (on demand)") { - appState.update(to: .serverList) + if let geometry = geometry { + if let mousePosition = mousePositionInView(with: geometry) { + handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0) + } else { + modal.error("Failed to get mouse position (on demand)") { + appState.update(to: .serverList) + } } } return content() - .frame(maxWidth: .infinity, maxHeight: .infinity) #if os(iOS) .gesture(TapGesture(count: 2).onEnded { _ in handleKeyPress?(.escape, []) @@ -139,7 +154,7 @@ struct InputView: View { .onAppear { if !monitorsAdded { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged], handler: { event in - if !listening { + guard listening, let geometry = geometry else { return event } diff --git a/Sources/Client/Views/Settings/KeymapEditorView.swift b/Sources/Client/Views/Settings/KeymapEditorView.swift index a90c2916..f4187d93 100644 --- a/Sources/Client/Views/Settings/KeymapEditorView.swift +++ b/Sources/Client/Views/Settings/KeymapEditorView.swift @@ -52,6 +52,7 @@ struct KeymapEditorView: View { } } .passthroughClicks() + .avoidGeometryReader() .onKeyPress { key, _ in guard let selectedInput = selectedInput else { return diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 65ec575b..e42b8a7b 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -3,7 +3,7 @@ import MetalKit import FirebladeMath import DeltaCore -/// A renderer that renders a `World` +/// A renderer that renders a `World` along with its associated entities (from `Game.nexus`). public final class WorldRenderer: Renderer { // MARK: Private properties From f92533f09ec173f0dd2e4eef9a92f59a92e6062c Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 14 Apr 2024 23:51:31 +1000 Subject: [PATCH 08/84] Add tvOS support (and tweak most UI and nav code to support tvOS navigation) --- Bundler.toml | 2 +- Package.resolved | 20 ++++++- Package.swift | 4 +- .../ButtonStyles/PrimaryButtonStyle.swift | 22 ++++---- .../ButtonStyles/SecondaryButtonStyle.swift | 22 ++++---- .../EditableList/EditableList.swift | 21 ++++++++ Sources/Client/Components/IconButton.swift | 2 + .../Components/LegacyFormattedTextView.swift | 16 +++--- Sources/Client/StorageDirectory.swift | 4 ++ Sources/Client/Utility/Clipboard.swift | 8 +-- .../Client/Views/Play/DirectConnectView.swift | 7 +++ Sources/Client/Views/Play/GameView.swift | 12 +++-- Sources/Client/Views/Play/InGameMenu.swift | 14 +++++ Sources/Client/Views/Play/InputView.swift | 43 ++++++++------- Sources/Client/Views/Play/MetalView.swift | 4 +- Sources/Client/Views/Play/PlayView.swift | 1 - Sources/Client/Views/Play/SelectOption.swift | 41 +++++++++----- .../Views/ServerList/ServerDetail.swift | 4 ++ .../Views/ServerList/ServerListView.swift | 53 ++++++++++++++----- .../Settings/Account/AccountLoginView.swift | 7 +++ .../Account/AccountSettingsView.swift | 5 ++ .../Settings/Account/MicrosoftLoginView.swift | 41 ++++++++++---- .../Settings/Account/MojangLoginView.swift | 2 + .../Settings/Account/OfflineLoginView.swift | 2 + .../Views/Settings/ControlsSettingsView.swift | 2 + .../Settings/Server/EditServerListView.swift | 5 ++ .../Settings/Server/ServerEditorView.swift | 2 + .../Client/Views/Settings/SettingsView.swift | 11 +++- .../Views/Settings/TroubleShootingView.swift | 2 + .../Views/Settings/VideoSettingsView.swift | 4 +- Sources/Core/Package.swift | 2 +- Sources/Core/Renderer/GUI/GUIRenderer.swift | 6 +-- .../Core/Renderer/Resources/Font+Metal.swift | 2 +- .../Resources/MetalTexturePalette.swift | 2 +- .../Renderer/Shader/ChunkOITShaders.metal | 2 +- Sources/Core/Renderer/Util/MetalUtil.swift | 5 +- .../Core/Renderer/World/WorldRenderer.swift | 35 ++++++++---- .../LegacyFormattedText.swift | 4 +- Sources/Core/Sources/RenderError.swift | 5 +- Sources/Core/Sources/Util/ColorUtil.swift | 4 +- .../Core/Sources/Util/CustomJSONDecoder.swift | 10 ++-- Sources/Core/Sources/Util/FontUtil.swift | 4 +- 42 files changed, 332 insertions(+), 132 deletions(-) diff --git a/Bundler.toml b/Bundler.toml index 0f0f125f..cb5d2a04 100644 --- a/Bundler.toml +++ b/Bundler.toml @@ -1,7 +1,7 @@ [apps.DeltaClient] product = 'DeltaClient' version = 'v0.1.0-alpha.1' -bundle_identifier = 'dev.stackotter.delta-client' +bundle_identifier = 'dev.stackotter.TVHelloWorld' category = 'public.app-category.games' minimum_macos_version = '11.0' icon = 'AppIcon.icns' diff --git a/Package.resolved b/Package.resolved index eb787311..8906cc5c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -172,6 +172,24 @@ "version": null } }, + { + "package": "Swift Grammar", + "repositoryURL": "https://github.com/tayloraswift/swift-grammar", + "state": { + "branch": null, + "revision": "163ce5d4d88db7d94f2f4ca1cabcb2ae65af8af7", + "version": "0.3.3" + } + }, + { + "package": "swift-hash", + "repositoryURL": "https://github.com/tayloraswift/swift-hash", + "state": { + "branch": null, + "revision": "c7ba0cde5eb63042c2196b02b65a770101c1ac11", + "version": "0.5.0" + } + }, { "package": "SwiftImage", "repositoryURL": "https://github.com/stackotter/swift-image.git", @@ -240,7 +258,7 @@ "repositoryURL": "https://github.com/stackotter/swift-png", "state": { "branch": null, - "revision": "b11a34847192cec24fa91c23fe771e95eb2c08cc", + "revision": "dee856ec2cad5a91060ace4729db7e6d747572b3", "version": null } }, diff --git a/Package.swift b/Package.swift index 0cfe184c..dc4eedfd 100644 --- a/Package.swift +++ b/Package.swift @@ -62,7 +62,7 @@ targets.append(.executableTarget( name: "DeltaClient", dependencies: [ "DynamicShim", - .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), + // .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), .product(name: "ArgumentParser", package: "swift-argument-parser") ], path: "Sources/Client" @@ -71,7 +71,7 @@ targets.append(.executableTarget( let package = Package( name: "DeltaClient", - platforms: [.macOS(.v11), .iOS(.v15)], + platforms: [.macOS(.v11), .iOS(.v15), .tvOS(.v15)], products: products, dependencies: [ // See Notes/PluginSystem.md for more details on the architecture of the project in regards to dependencies, targets and linking diff --git a/Sources/Client/Components/ButtonStyles/PrimaryButtonStyle.swift b/Sources/Client/Components/ButtonStyles/PrimaryButtonStyle.swift index b133ae3d..aade247a 100644 --- a/Sources/Client/Components/ButtonStyles/PrimaryButtonStyle.swift +++ b/Sources/Client/Components/ButtonStyles/PrimaryButtonStyle.swift @@ -1,13 +1,17 @@ import SwiftUI -struct PrimaryButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - HStack { - Spacer() - configuration.label.foregroundColor(.white) - Spacer() +#if os(tvOS) + typealias PrimaryButtonStyle = DefaultButtonStyle +#else + struct PrimaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + Spacer() + configuration.label.foregroundColor(.white) + Spacer() + } + .padding(6) + .background(Color.accentColor.brightness(configuration.isPressed ? 0.15 : 0).cornerRadius(4)) } - .padding(6) - .background(Color.accentColor.brightness(configuration.isPressed ? 0.15 : 0).cornerRadius(4)) } -} +#endif diff --git a/Sources/Client/Components/ButtonStyles/SecondaryButtonStyle.swift b/Sources/Client/Components/ButtonStyles/SecondaryButtonStyle.swift index 62914ebd..2b7430ca 100644 --- a/Sources/Client/Components/ButtonStyles/SecondaryButtonStyle.swift +++ b/Sources/Client/Components/ButtonStyles/SecondaryButtonStyle.swift @@ -1,13 +1,17 @@ import SwiftUI -struct SecondaryButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - HStack { - Spacer() - configuration.label.foregroundColor(.white) - Spacer() +#if os(tvOS) + typealias SecondaryButtonStyle = DefaultButtonStyle +#else + struct SecondaryButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + Spacer() + configuration.label.foregroundColor(.white) + Spacer() + } + .padding(6) + .background(Color.secondary.brightness(configuration.isPressed ? 0 : -0.15).cornerRadius(4)) } - .padding(6) - .background(Color.secondary.brightness(configuration.isPressed ? 0 : -0.15).cornerRadius(4)) } -} +#endif diff --git a/Sources/Client/Components/EditableList/EditableList.swift b/Sources/Client/Components/EditableList/EditableList.swift index 562daf9c..8c7ee02d 100644 --- a/Sources/Client/Components/EditableList/EditableList.swift +++ b/Sources/Client/Components/EditableList/EditableList.swift @@ -17,6 +17,10 @@ enum EditableListState { struct EditableList: View { @State var state: EditableListState = .list + #if os(tvOS) + @Namespace var focusNamespace + #endif + @Binding var items: [ItemEditor.Item] @Binding var selected: Int? @@ -120,11 +124,17 @@ struct EditableList: View { } } } + #if os(tvOS) + .focusSection() + #endif VStack { Button("Add") { state = .addItem } + #if os(tvOS) + .prefersDefaultFocus(in: focusNamespace) + #endif .buttonStyle(SecondaryButtonStyle()) if save != nil || cancel != nil { @@ -140,8 +150,15 @@ struct EditableList: View { } } } + #if !os(tvOS) .frame(width: 200) + #else + .focusSection() + #endif } + #if os(tvOS) + .focusSection() + #endif case .addItem: itemEditor.init(nil, completion: { newItem in items.append(newItem) @@ -159,6 +176,10 @@ struct EditableList: View { }) } } + #if !os(tvOS) .frame(width: 400) + #else + .focusScope(focusNamespace) + #endif } } diff --git a/Sources/Client/Components/IconButton.swift b/Sources/Client/Components/IconButton.swift index c7fb8fdc..99886030 100644 --- a/Sources/Client/Components/IconButton.swift +++ b/Sources/Client/Components/IconButton.swift @@ -15,7 +15,9 @@ struct IconButton: View { Button(action: action, label: { Image(systemName: icon) }) + #if !os(tvOS) .buttonStyle(BorderlessButtonStyle()) + #endif .disabled(isDisabled) } } diff --git a/Sources/Client/Components/LegacyFormattedTextView.swift b/Sources/Client/Components/LegacyFormattedTextView.swift index e4d060b2..30c8281b 100644 --- a/Sources/Client/Components/LegacyFormattedTextView.swift +++ b/Sources/Client/Components/LegacyFormattedTextView.swift @@ -23,14 +23,18 @@ struct LegacyFormattedTextView: View { } var body: some View { - NSAttributedTextView( - attributedString: attributedString, - alignment: alignment - ).frame(width: attributedString.size().width) + #if os(tvOS) + Text(attributedString.string) + #else + NSAttributedTextView( + attributedString: attributedString, + alignment: alignment + ).frame(width: attributedString.size().width) + #endif } } -#if os(macOS) +#if canImport(AppKit) struct NSAttributedTextView: NSViewRepresentable { var attributedString: NSAttributedString var alignment: NSTextAlignment @@ -49,7 +53,7 @@ struct NSAttributedTextView: NSViewRepresentable { label.alignment = .left } } -#elseif os(iOS) +#elseif canImport(UIKit) struct NSAttributedTextView: UIViewRepresentable { var attributedString: NSAttributedString var alignment: NSTextAlignment diff --git a/Sources/Client/StorageDirectory.swift b/Sources/Client/StorageDirectory.swift index 42bd910c..f93a26ec 100644 --- a/Sources/Client/StorageDirectory.swift +++ b/Sources/Client/StorageDirectory.swift @@ -36,6 +36,10 @@ struct StorageDirectory { let options = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) let base = options.first?.appendingPathComponent("dev.stackotter.delta-client") return base.map(StorageDirectory.init) + #elseif os(tvOS) + let base = FileManager.default.temporaryDirectory.appendingPathComponent("dev.stackotter.delta-client") + try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true) + return StorageDirectory(base) #elseif os(Linux) #error("Linux storage directory not implemented") #endif diff --git a/Sources/Client/Utility/Clipboard.swift b/Sources/Client/Utility/Clipboard.swift index 0a741310..11a5b6f2 100644 --- a/Sources/Client/Utility/Clipboard.swift +++ b/Sources/Client/Utility/Clipboard.swift @@ -1,6 +1,6 @@ -#if os(macOS) +#if canImport(AppKit) import AppKit -#elseif os(iOS) +#elseif canImport(UIKit) import UIKit #endif @@ -8,11 +8,13 @@ import UIKit enum Clipboard { /// Sets the contents of the user's clipboard to a given string. static func copy(_ contents: String) { - #if os(macOS) + #if canImport(AppKit) NSPasteboard.general.clearContents() NSPasteboard.general.setString(contents, forType: .string) #elseif os(iOS) UIPasteboard.general.string = contents + #elseif os(tvOS) + #warning("Remove dependence on copy on tvOS") #else #error("Unsupported platform, unknown clipboard implementation") #endif diff --git a/Sources/Client/Views/Play/DirectConnectView.swift b/Sources/Client/Views/Play/DirectConnectView.swift index 5b4ae5d1..a5807f7b 100644 --- a/Sources/Client/Views/Play/DirectConnectView.swift +++ b/Sources/Client/Views/Play/DirectConnectView.swift @@ -44,6 +44,13 @@ struct DirectConnectView: View { } .padding(.top, 16) } + #if !os(tvOS) .frame(width: 200) + #endif + #if !os(iOS) + .onExitCommand { + appState.update(to: .serverList) + } + #endif } } diff --git a/Sources/Client/Views/Play/GameView.swift b/Sources/Client/Views/Play/GameView.swift index cd33bf71..2ab9411e 100644 --- a/Sources/Client/Views/Play/GameView.swift +++ b/Sources/Client/Views/Play/GameView.swift @@ -90,6 +90,12 @@ struct GameView: View { client?.moveRightThumbstick(x, y) } } + #if os(tvOS) + .focusable(!inGameMenuPresented) + .onExitCommand { + inGameMenuPresented = true + } + #endif } case .gpuFrameCaptureComplete(let file): frameCaptureResult(file) @@ -209,11 +215,9 @@ struct GameView: View { Button("Show in finder") { NSWorkspace.shared.activateFileViewerSelecting([file]) }.buttonStyle(SecondaryButtonStyle()) - #elseif os(iOS) - // TODO: Add a file sharing menu for iOS - Text("I have no clue how to get hold of the file") #else - #error("Unsupported platform, no file opening method") + // TODO: Add a file sharing menu for iOS and tvOS + Text("I have no clue how to get hold of the file") #endif Button("OK") { diff --git a/Sources/Client/Views/Play/InGameMenu.swift b/Sources/Client/Views/Play/InGameMenu.swift index b88167a6..4db9895b 100644 --- a/Sources/Client/Views/Play/InGameMenu.swift +++ b/Sources/Client/Views/Play/InGameMenu.swift @@ -6,6 +6,10 @@ struct InGameMenu: View { case settings } + #if os(tvOS) + @Namespace var focusNamespace + #endif + @EnvironmentObject var appState: StateWrapper @Binding var presented: Bool @@ -25,7 +29,11 @@ struct InGameMenu: View { Button("Back to game") { presented = false } + #if !os(tvOS) .keyboardShortcut(.escape, modifiers: []) + #else + .prefersDefaultFocus(in: focusNamespace) + #endif .buttonStyle(PrimaryButtonStyle()) Button("Settings") { @@ -38,7 +46,9 @@ struct InGameMenu: View { } .buttonStyle(SecondaryButtonStyle()) } + #if !os(tvOS) .frame(width: 200) + #endif case .settings: SettingsView(isInGame: true) { state = .menu @@ -47,6 +57,10 @@ struct InGameMenu: View { } .frame(width: geometry.size.width, height: geometry.size.height) .background(Color.black.opacity(0.702), alignment: .center) + #if os(tvOS) + .focusSection() + .focusScope(focusNamespace) + #endif } } } diff --git a/Sources/Client/Views/Play/InputView.swift b/Sources/Client/Views/Play/InputView.swift index 73a78373..0673806e 100644 --- a/Sources/Client/Views/Play/InputView.swift +++ b/Sources/Client/Views/Play/InputView.swift @@ -92,18 +92,20 @@ struct InputView: View { appendingAction(to: \.handleScroll, action) } - func mousePositionInView(with geometry: GeometryProxy) -> (x: Float, y: Float)? { - // This assumes that there's only one window and that this is only called once - // the view's body has been evaluated at least once. - guard let window = NSApplication.shared.orderedWindows.first else { - return nil - } + #if os(macOS) + func mousePositionInView(with geometry: GeometryProxy) -> (x: Float, y: Float)? { + // This assumes that there's only one window and that this is only called once + // the view's body has been evaluated at least once. + guard let window = NSApplication.shared.orderedWindows.first else { + return nil + } - let viewFrame = geometry.frame(in: .global) - let x = (NSEvent.mouseLocation.x - window.frame.minX) - viewFrame.minX - let y = window.frame.maxY - NSEvent.mouseLocation.y - viewFrame.minY - return (Float(x), Float(y)) - } + let viewFrame = geometry.frame(in: .global) + let x = (NSEvent.mouseLocation.x - window.frame.minX) - viewFrame.minX + let y = window.frame.maxY - NSEvent.mouseLocation.y - viewFrame.minY + return (Float(x), Float(y)) + } + #endif var body: some View { VStack { @@ -122,15 +124,17 @@ struct InputView: View { // listening was disabled and now isn't, observers won't have been told // about any changes that occured during the period in which listening // was disabled). - if let geometry = geometry { - if let mousePosition = mousePositionInView(with: geometry) { - handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0) - } else { - modal.error("Failed to get mouse position (on demand)") { - appState.update(to: .serverList) + #if os(macOS) + if let geometry = geometry { + if let mousePosition = mousePositionInView(with: geometry) { + handleMouseMove?(mousePosition.x, mousePosition.y, 0, 0) + } else { + modal.error("Failed to get mouse position (on demand)") { + appState.update(to: .serverList) + } } } - } + #endif return content() #if os(iOS) @@ -141,7 +145,10 @@ struct InputView: View { handleKeyPress?(.f3, []) }) .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onChanged { value in + // TODO: Implement absolute handleMouseMove?( + Float(value.location.x), + Float(value.location.y), Float(value.translation.width), Float(value.translation.height) ) diff --git a/Sources/Client/Views/Play/MetalView.swift b/Sources/Client/Views/Play/MetalView.swift index 75514ffd..f7ac0cb7 100644 --- a/Sources/Client/Views/Play/MetalView.swift +++ b/Sources/Client/Views/Play/MetalView.swift @@ -72,7 +72,7 @@ final class MetalViewClass { } -#if os(macOS) +#if canImport(AppKit) @available(macOS 13, *) extension MetalView: NSViewRepresentable { func makeNSView(context: Context) -> some NSView { @@ -89,7 +89,7 @@ final class MetalViewClass { func updateNSView(_ view: NSViewType, context: Context) {} } -#elseif os(iOS) +#elseif canImport(UIKit) @available(iOS 16, *) extension MetalView: UIViewRepresentable { func makeUIView(context: Context) -> some UIView { diff --git a/Sources/Client/Views/Play/PlayView.swift b/Sources/Client/Views/Play/PlayView.swift index d23047b0..9d10df71 100644 --- a/Sources/Client/Views/Play/PlayView.swift +++ b/Sources/Client/Views/Play/PlayView.swift @@ -29,7 +29,6 @@ struct PlayView: View { controllerOnly: false, inGameMenuPresented: $inGameMenuPresented ) - } } else { HStack(spacing: 0) { diff --git a/Sources/Client/Views/Play/SelectOption.swift b/Sources/Client/Views/Play/SelectOption.swift index 5b1f72e2..b3e29cdb 100644 --- a/Sources/Client/Views/Play/SelectOption.swift +++ b/Sources/Client/Views/Play/SelectOption.swift @@ -40,24 +40,35 @@ struct SelectOption: View { VStack { Divider() ForEach(options, id: \.self) { option in - HStack { - row(option) + #if !os(tvOS) + HStack { + row(option) - Spacer() + Spacer() - Image(systemName: "chevron.right") - } - .contentShape(Rectangle()) - .onTapGesture { - guard !excludedOptions.contains(option) else { - return + Image(systemName: "chevron.right") } - selectedOption = option - } - .padding(.top, 0.3) - .foregroundColor(excludedOptions.contains(option) ? .gray : .primary) + .contentShape(Rectangle()) + #if !os(tvOS) + .onTapGesture { + guard !excludedOptions.contains(option) else { + return + } + selectedOption = option + } + #endif + .padding(.top, 0.3) + .foregroundColor(excludedOptions.contains(option) ? .gray : .primary) - Divider() + Divider() + #else + Button { + selectedOption = option + } label: { + row(option) + } + .disabled(excludedOptions.contains(option)) + #endif } Button("Cancel", action: cancellationHandler) @@ -66,7 +77,9 @@ struct SelectOption: View { } .padding(.bottom, 10) } + #if !os(tvOS) .frame(width: 300) + #endif } } } diff --git a/Sources/Client/Views/ServerList/ServerDetail.swift b/Sources/Client/Views/ServerList/ServerDetail.swift index c7685ebc..54d2a30e 100644 --- a/Sources/Client/Views/ServerList/ServerDetail.swift +++ b/Sources/Client/Views/ServerList/ServerDetail.swift @@ -34,13 +34,17 @@ struct ServerDetail: View { appState.update(to: .playServer(descriptor, paneCount: 1)) } .buttonStyle(PrimaryButtonStyle()) + #if !os(tvOS) .frame(width: 150) + #endif Button("Play splitscreen") { appState.update(to: .playServer(descriptor, paneCount: 2)) } .buttonStyle(SecondaryButtonStyle()) + #if !os(tvOS) .frame(width: 150) + #endif case let .failure(error): Text(error.localizedDescription) .padding(.bottom, 8) diff --git a/Sources/Client/Views/ServerList/ServerListView.swift b/Sources/Client/Views/ServerList/ServerListView.swift index 7d2a3831..8b65d3f0 100644 --- a/Sources/Client/Views/ServerList/ServerListView.swift +++ b/Sources/Client/Views/ServerList/ServerListView.swift @@ -24,7 +24,9 @@ struct ServerListView: View { Text("no servers").italic() } - Divider() + #if !os(tvOS) + Divider() + #endif if let lanServerEnumerator = lanServerEnumerator { LANServerList(lanServerEnumerator: lanServerEnumerator) @@ -32,29 +34,49 @@ struct ServerListView: View { Text("LAN scan failed").italic() } - HStack { - // Edit server list - IconButton("square.and.pencil") { + #if os(tvOS) + Divider() + + Button("Edit servers") { appState.update(to: .editServerList) } - // Refresh server list (ping all servers) and discovered LAN servers - IconButton("arrow.clockwise") { + Button("Refresh servers") { refresh() } - // Direct connect - IconButton("personalhotspot") { + Button("Direct connect") { appState.update(to: .directConnect) } - #if os(iOS) - // Settings - IconButton("gear") { - appState.update(to: .settings(nil)) + Button("Settings") { + appState.update(to: .settings(nil)) + } + #else + HStack { + // Edit server list + IconButton("square.and.pencil") { + appState.update(to: .editServerList) } - #endif - } + + // Refresh server list (ping all servers) and discovered LAN servers + IconButton("arrow.clockwise") { + refresh() + } + + // Direct connect + IconButton("personalhotspot") { + appState.update(to: .directConnect) + } + + #if os(iOS) || os(tvOS) + // Settings + IconButton("gear") { + appState.update(to: .settings(nil)) + } + #endif + } + #endif if (updateAvailable) { Button("Update") { @@ -62,7 +84,10 @@ struct ServerListView: View { }.padding(.top, 5) } } + #if !os(tvOS) + // TODO: Does this even do anything? .listStyle(SidebarListStyle()) + #endif } .onAppear { // Check for updates diff --git a/Sources/Client/Views/Settings/Account/AccountLoginView.swift b/Sources/Client/Views/Settings/Account/AccountLoginView.swift index 731c0887..f932133a 100644 --- a/Sources/Client/Views/Settings/Account/AccountLoginView.swift +++ b/Sources/Client/Views/Settings/Account/AccountLoginView.swift @@ -47,7 +47,14 @@ struct AccountLoginView: EditorView { }.buttonStyle(SecondaryButtonStyle()) } .navigationTitle("Account Login") + #if !os(iOS) + .onExitCommand { + cancelationHandler?() + } + #endif + #if !os(tvOS) .frame(width: 200) + #endif case .loginMicrosoft: MicrosoftLoginView(loginViewState: $state, completionHandler: completionHandler) case .loginMojang: diff --git a/Sources/Client/Views/Settings/Account/AccountSettingsView.swift b/Sources/Client/Views/Settings/Account/AccountSettingsView.swift index f7d033d5..d719cd21 100644 --- a/Sources/Client/Views/Settings/Account/AccountSettingsView.swift +++ b/Sources/Client/Views/Settings/Account/AccountSettingsView.swift @@ -56,8 +56,13 @@ struct AccountSettingsView: View { Button("Select") { handler(.select) } .disabled(selected) + #if !os(tvOS) .buttonStyle(BorderlessButtonStyle()) + #endif IconButton("xmark") { handler(.delete) } + #if os(tvOS) + .padding(.trailing, 20) + #endif } } diff --git a/Sources/Client/Views/Settings/Account/MicrosoftLoginView.swift b/Sources/Client/Views/Settings/Account/MicrosoftLoginView.swift index d9fc98be..ff3c9aa1 100644 --- a/Sources/Client/Views/Settings/Account/MicrosoftLoginView.swift +++ b/Sources/Client/Views/Settings/Account/MicrosoftLoginView.swift @@ -22,12 +22,19 @@ struct MicrosoftLoginView: View { case authenticatingUser } + #if os(tvOS) + @Namespace var focusNamespace + #endif + @EnvironmentObject var modal: Modal @EnvironmentObject var appState: StateWrapper @State var state: MicrosoftState = .authorizingDevice @Binding var loginViewState: LoginViewState + #if os(tvOS) + @Environment(\.resetFocus) var resetFocus + #endif var completionHandler: (Account) -> Void @@ -39,24 +46,30 @@ struct MicrosoftLoginView: View { case .login(let response): Text(response.message) - Link("Open in browser", destination: response.verificationURI) - .padding(10) + #if !os(tvOS) + Link("Open in browser", destination: response.verificationURI) + .padding(10) - Button("Copy code") { - Clipboard.copy(response.userCode) - } - .buttonStyle(PrimaryButtonStyle()) - .frame(width: 200) - .padding(.bottom, 10) - - Spacer().frame(height: 16) + Button("Copy code") { + Clipboard.copy(response.userCode) + } + .buttonStyle(PrimaryButtonStyle()) + .frame(width: 200) + .padding(.bottom, 26) + #endif Button("Done") { state = .authenticatingUser authenticate(with: response.deviceCode) } .buttonStyle(PrimaryButtonStyle()) + #if !os(tvOS) .frame(width: 200) + #else + .onAppear { + resetFocus(in: focusNamespace) + } + #endif case .authenticatingUser: Text("Authenticating...") } @@ -65,10 +78,16 @@ struct MicrosoftLoginView: View { loginViewState = .chooseAccountType } .buttonStyle(SecondaryButtonStyle()) + #if !os(tvOS) .frame(width: 200) - }.onAppear { + #endif + } + .onAppear { authorizeDevice() } + #if os(tvOS) + .focusScope(focusNamespace) + #endif } func authorizeDevice() { diff --git a/Sources/Client/Views/Settings/Account/MojangLoginView.swift b/Sources/Client/Views/Settings/Account/MojangLoginView.swift index b47f4ba5..a99095d9 100644 --- a/Sources/Client/Views/Settings/Account/MojangLoginView.swift +++ b/Sources/Client/Views/Settings/Account/MojangLoginView.swift @@ -38,7 +38,9 @@ struct MojangLoginView: View { } } } + #if !os(tvOS) .frame(width: 200) + #endif } func login() { diff --git a/Sources/Client/Views/Settings/Account/OfflineLoginView.swift b/Sources/Client/Views/Settings/Account/OfflineLoginView.swift index 139bbf0c..e6fd7e6a 100644 --- a/Sources/Client/Views/Settings/Account/OfflineLoginView.swift +++ b/Sources/Client/Views/Settings/Account/OfflineLoginView.swift @@ -27,7 +27,9 @@ struct OfflineLoginView: View { }.buttonStyle(PrimaryButtonStyle()) } } + #if !os(tvOS) .frame(width: 200) + #endif } func login() { diff --git a/Sources/Client/Views/Settings/ControlsSettingsView.swift b/Sources/Client/Views/Settings/ControlsSettingsView.swift index 3bef21ec..4995bd05 100644 --- a/Sources/Client/Views/Settings/ControlsSettingsView.swift +++ b/Sources/Client/Views/Settings/ControlsSettingsView.swift @@ -1,5 +1,6 @@ import SwiftUI +#if !os(tvOS) struct ControlsSettingsView: View { @EnvironmentObject var managedConfig: ManagedConfig @@ -78,3 +79,4 @@ struct ControlsSettingsView: View { return "\(Int(roundSensitivity(sensitivity) * 100))%" } } +#endif diff --git a/Sources/Client/Views/Settings/Server/EditServerListView.swift b/Sources/Client/Views/Settings/Server/EditServerListView.swift index bfed3b69..0c18eca6 100644 --- a/Sources/Client/Views/Settings/Server/EditServerListView.swift +++ b/Sources/Client/Views/Settings/Server/EditServerListView.swift @@ -15,10 +15,12 @@ struct EditServerListView: View { itemEditor: ServerEditorView.self, row: { item, selected, isFirst, isLast, handler in HStack { + #if !os(tvOS) VStack { IconButton("chevron.up", isDisabled: isFirst) { handler(.moveUp) } IconButton("chevron.down", isDisabled: isLast) { handler(.moveDown) } } + #endif VStack(alignment: .leading) { Text(item.name) @@ -32,6 +34,9 @@ struct EditServerListView: View { HStack { IconButton("square.and.pencil") { handler(.edit) } IconButton("xmark") { handler(.delete) } + #if os(tvOS) + .padding(.trailing, 20) + #endif } } }, diff --git a/Sources/Client/Views/Settings/Server/ServerEditorView.swift b/Sources/Client/Views/Settings/Server/ServerEditorView.swift index aa2f1c93..7f350d8f 100644 --- a/Sources/Client/Views/Settings/Server/ServerEditorView.swift +++ b/Sources/Client/Views/Settings/Server/ServerEditorView.swift @@ -51,6 +51,8 @@ struct ServerEditorView: EditorView { } .padding(.top, 8) } + #if !os(tvOS) .frame(width: 200) + #endif } } diff --git a/Sources/Client/Views/Settings/SettingsView.swift b/Sources/Client/Views/Settings/SettingsView.swift index a438ffb8..e7d73ee9 100644 --- a/Sources/Client/Views/Settings/SettingsView.swift +++ b/Sources/Client/Views/Settings/SettingsView.swift @@ -36,6 +36,7 @@ struct SettingsView: View { var body: some View { NavigationView { List { + #if !os(tvOS) NavigationLink( "Video", destination: VideoSettingsView().padding(), @@ -48,6 +49,7 @@ struct SettingsView: View { tag: SettingsState.controls, selection: $currentPage ) + #endif if !isInGame { NavigationLink( @@ -81,13 +83,18 @@ struct SettingsView: View { } Button("Done", action: { - withAnimation(nil) { done() } + withAnimation(nil) { + done() + } }) + #if !os(tvOS) .buttonStyle(BorderlessButtonStyle()) - .padding(.top, 8) .keyboardShortcut(.escape, modifiers: []) + #endif } + #if !os(tvOS) .listStyle(SidebarListStyle()) + #endif } .navigationTitle("Settings") } diff --git a/Sources/Client/Views/Settings/TroubleShootingView.swift b/Sources/Client/Views/Settings/TroubleShootingView.swift index 0e9f2c79..aa171e00 100644 --- a/Sources/Client/Views/Settings/TroubleShootingView.swift +++ b/Sources/Client/Views/Settings/TroubleShootingView.swift @@ -111,7 +111,9 @@ struct TroubleshootingView: View { .padding(.top, 10) } } + #if !os(tvOS) .frame(width: 200) + #endif } private func perform( diff --git a/Sources/Client/Views/Settings/VideoSettingsView.swift b/Sources/Client/Views/Settings/VideoSettingsView.swift index 4e5bda19..dfa2ddf0 100644 --- a/Sources/Client/Views/Settings/VideoSettingsView.swift +++ b/Sources/Client/Views/Settings/VideoSettingsView.swift @@ -1,6 +1,7 @@ import SwiftUI import DeltaCore +#if !os(tvOS) struct VideoSettingsView: View { @EnvironmentObject var managedConfig: ManagedConfig @@ -45,7 +46,7 @@ struct VideoSettingsView: View { } #if os(macOS) .pickerStyle(RadioGroupPickerStyle()) - #elseif os(iOS) + #elseif os(iOS) || os(tvOS) .pickerStyle(DefaultPickerStyle()) #endif .frame(width: 220) @@ -67,3 +68,4 @@ struct VideoSettingsView: View { .navigationTitle("Video") } } +#endif diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index fce95379..803816db 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -86,7 +86,7 @@ let package = Package( .package(url: "https://github.com/fourplusone/swift-package-zlib", from: "1.2.11"), .package(url: "https://github.com/stackotter/swift-image.git", branch: "master"), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), - .package(url: "https://github.com/stackotter/swift-png", revision: "b11a34847192cec24fa91c23fe771e95eb2c08cc"), + .package(url: "https://github.com/stackotter/swift-png", revision: "dee856ec2cad5a91060ace4729db7e6d747572b3"), .package(url: "https://github.com/stackotter/ASN1Parser", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.6.0"), .package(url: "https://github.com/Kitura/SwiftyRequest.git", from: "3.1.0"), diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 8ca1b55e..73a44efd 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -2,7 +2,7 @@ import MetalKit import FirebladeMath import DeltaCore -#if os(iOS) +#if canImport(UIKit) import UIKit #endif @@ -207,9 +207,9 @@ public final class GUIRenderer: Renderer { static func adjustScale(_ scale: Float) -> Float { // Adjust scale per screen scale factor - #if os(macOS) + #if canImport(AppKit) let screenScaleFactor = Float(NSApp.windows.first?.screen?.backingScaleFactor ?? 1) - #elseif os(iOS) + #elseif canImport(UIKit) let screenScaleFactor = Float(UIScreen.main.scale) #else #error("Unsupported platform, unknown screen scale factor") diff --git a/Sources/Core/Renderer/Resources/Font+Metal.swift b/Sources/Core/Renderer/Resources/Font+Metal.swift index 2cf401c4..12aaf785 100644 --- a/Sources/Core/Renderer/Resources/Font+Metal.swift +++ b/Sources/Core/Renderer/Resources/Font+Metal.swift @@ -37,7 +37,7 @@ extension Font { textureDescriptor.textureType = .type2DArray #if os(macOS) textureDescriptor.storageMode = .managed - #elseif os(iOS) + #elseif os(iOS) || os(tvOS) textureDescriptor.storageMode = .shared #else #error("Unsupported platform, can't determine storageMode for texture") diff --git a/Sources/Core/Renderer/Resources/MetalTexturePalette.swift b/Sources/Core/Renderer/Resources/MetalTexturePalette.swift index 27e36dee..af6caaec 100644 --- a/Sources/Core/Renderer/Resources/MetalTexturePalette.swift +++ b/Sources/Core/Renderer/Resources/MetalTexturePalette.swift @@ -144,7 +144,7 @@ public struct MetalTexturePalette { #if os(macOS) textureDescriptor.storageMode = .managed - #elseif os(iOS) + #elseif os(iOS) || os(tvOS) textureDescriptor.storageMode = .shared #else #error("Unsupported platform, can't determine storageMode for texture") diff --git a/Sources/Core/Renderer/Shader/ChunkOITShaders.metal b/Sources/Core/Renderer/Shader/ChunkOITShaders.metal index b4fe514d..ba37d8a9 100644 --- a/Sources/Core/Renderer/Shader/ChunkOITShaders.metal +++ b/Sources/Core/Renderer/Shader/ChunkOITShaders.metal @@ -105,7 +105,7 @@ vertex OITCompositingRasterizerData chunkOITCompositingVertexShader(uint vertexI return out; } -fragment float4 chunkOITCompositingFragmentShader(RasterizerData in [[stage_in]], +fragment float4 chunkOITCompositingFragmentShader(OITCompositingRasterizerData in [[stage_in]], float4 accumulation [[color(1)]], float revealage [[color(2)]]) { return float4(accumulation.rgb / max(min(accumulation.a, 5e4), 1e-4), 1 - revealage); diff --git a/Sources/Core/Renderer/Util/MetalUtil.swift b/Sources/Core/Renderer/Util/MetalUtil.swift index 1d3bf624..86f50227 100644 --- a/Sources/Core/Renderer/Util/MetalUtil.swift +++ b/Sources/Core/Renderer/Util/MetalUtil.swift @@ -40,7 +40,8 @@ public enum MetalUtil { do { return try device.makeRenderPipelineState(descriptor: descriptor) } catch { - throw RenderError.failedToCreateEntityRenderPipelineState(error) + // TODO: Update error name + throw RenderError.failedToCreateEntityRenderPipelineState(error, label: label) } } @@ -50,7 +51,7 @@ public enum MetalUtil { public static func loadDefaultLibrary(_ device: MTLDevice) throws -> MTLLibrary { #if os(macOS) let bundlePath = "Contents/Resources/DeltaCore_DeltaRenderer.bundle" - #elseif os(iOS) + #elseif os(iOS) || os(tvOS) let bundlePath = "DeltaCore_DeltaRenderer.bundle" #else #error("Unsupported platform, unknown DeltaCore bundle location") diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index e42b8a7b..5a6213aa 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -12,10 +12,12 @@ public final class WorldRenderer: Renderer { /// Render pipeline used for rendering world geometry. private var renderPipelineState: MTLRenderPipelineState + #if !os(tvOS) /// Render pipeline used for rendering translucent world geometry. private var transparencyRenderPipelineState: MTLRenderPipelineState /// Render pipeline used for compositing translucent geometry onto the screen buffer. private var compositingRenderPipelineState: MTLRenderPipelineState + #endif /// The device used for rendering. private var device: MTLDevice @@ -48,9 +50,11 @@ public final class WorldRenderer: Renderer { /// A buffer containing the light map (updated each frame). private var lightMapBuffer: MTLBuffer? + #if !os(tvOS) /// The depth stencil state used for order independent transparency (which requires read-only /// depth). private let readOnlyDepthState: MTLDepthStencilState + #endif /// The depth stencil state used when order independent transparency is disabled. private let depthState: MTLDepthStencilState @@ -106,6 +110,7 @@ public final class WorldRenderer: Renderer { blendingEnabled: true ) + #if !os(tvOS) // Create OIT pipeline transparencyRenderPipelineState = try MetalUtil.makeRenderPipelineState( device: device, @@ -145,8 +150,10 @@ public final class WorldRenderer: Renderer { // Create the depth state used for order independent transparency readOnlyDepthState = try MetalUtil.createDepthState(device: device, readOnly: true) + #endif // Create the regular depth state. + // TODO: Is this meant to be read only? I would assume not depthState = try MetalUtil.createDepthState(device: device, readOnly: true) // Create entity renderer @@ -163,7 +170,7 @@ public final class WorldRenderer: Renderer { // TODO: Improve storage mode selection #if os(macOS) let storageMode = MTLResourceOptions.storageModeManaged - #elseif os(iOS) + #elseif os(iOS) || os(tvOS) let storageMode = MTLResourceOptions.storageModeShared #else #error("Unsupported platform") @@ -330,12 +337,16 @@ public final class WorldRenderer: Renderer { profiler.pop() // Setup render pass for encoding translucent geometry after entity rendering pass - if client.configuration.render.enableOrderIndependentTransparency { - encoder.setRenderPipelineState(transparencyRenderPipelineState) - encoder.setDepthStencilState(readOnlyDepthState) - } else { + #if os(tvOS) encoder.setRenderPipelineState(renderPipelineState) - } + #else + if client.configuration.render.enableOrderIndependentTransparency { + encoder.setRenderPipelineState(transparencyRenderPipelineState) + encoder.setDepthStencilState(readOnlyDepthState) + } else { + encoder.setRenderPipelineState(renderPipelineState) + } + #endif encoder.setVertexBuffer(texturePalette.textureStatesBuffer, offset: 0, index: 3) @@ -357,11 +368,13 @@ public final class WorldRenderer: Renderer { // Composite translucent geometry onto the screen buffer. No vertices need to be supplied, the // shader has the screen's corners hardcoded for simplicity. - if client.configuration.render.enableOrderIndependentTransparency { - encoder.setRenderPipelineState(compositingRenderPipelineState) - encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) - encoder.setDepthStencilState(depthState) - } + #if !os(tvOS) + if client.configuration.render.enableOrderIndependentTransparency { + encoder.setRenderPipelineState(compositingRenderPipelineState) + encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) + encoder.setDepthStencilState(depthState) + } + #endif profiler.pop() } diff --git a/Sources/Core/Sources/Chat/LegacyFormattedText/LegacyFormattedText.swift b/Sources/Core/Sources/Chat/LegacyFormattedText/LegacyFormattedText.swift index 8de9bc48..18f9a9ab 100644 --- a/Sources/Core/Sources/Chat/LegacyFormattedText/LegacyFormattedText.swift +++ b/Sources/Core/Sources/Chat/LegacyFormattedText/LegacyFormattedText.swift @@ -1,8 +1,8 @@ import Foundation -#if os(macOS) +#if canImport(AppKit) import AppKit -#elseif os(iOS) +#elseif canImport(UIKit) import CoreGraphics import UIKit #endif diff --git a/Sources/Core/Sources/RenderError.swift b/Sources/Core/Sources/RenderError.swift index 3b02d172..54fde0bf 100644 --- a/Sources/Core/Sources/RenderError.swift +++ b/Sources/Core/Sources/RenderError.swift @@ -22,7 +22,7 @@ public enum RenderError: LocalizedError { /// Failed to create the depth stencil state for the world renderer. case failedToCreateWorldDepthStencilState /// Failed to create the render pipeline state for the entity renderer. - case failedToCreateEntityRenderPipelineState(Error) + case failedToCreateEntityRenderPipelineState(Error, label: String) /// Failed to create the depth stencil state for the entity renderer. case failedToCreateEntityDepthStencilState /// Failed to create the block texture array. @@ -84,10 +84,11 @@ public enum RenderError: LocalizedError { """ case .failedToCreateWorldDepthStencilState: return "Failed to create the depth stencil state for the entity renderer." - case .failedToCreateEntityRenderPipelineState(let error): + case .failedToCreateEntityRenderPipelineState(let error, let label): return """ Failed to create the render pipeline state for the entity renderer. Reason: \(error.localizedDescription) + Label: \(label) """ case .failedToCreateEntityDepthStencilState: return " Failed to create the depth stencil state for the entity renderer." diff --git a/Sources/Core/Sources/Util/ColorUtil.swift b/Sources/Core/Sources/Util/ColorUtil.swift index fe44d889..0b7b9ecf 100644 --- a/Sources/Core/Sources/Util/ColorUtil.swift +++ b/Sources/Core/Sources/Util/ColorUtil.swift @@ -2,7 +2,7 @@ enum ColorUtil {} // TODO: Move ColorUtil to Client target (instead of Core) because it's not cross-platform -#if os(macOS) +#if canImport(AppKit) import AppKit extension NSColor { @@ -42,7 +42,7 @@ extension ColorUtil { return NSColor(hexString: hexString) } } -#elseif os(iOS) +#elseif canImport(UIKit) import UIKit extension UIColor { diff --git a/Sources/Core/Sources/Util/CustomJSONDecoder.swift b/Sources/Core/Sources/Util/CustomJSONDecoder.swift index ba400fcf..0070c454 100644 --- a/Sources/Core/Sources/Util/CustomJSONDecoder.swift +++ b/Sources/Core/Sources/Util/CustomJSONDecoder.swift @@ -1,21 +1,19 @@ import Foundation -#if !os(Linux) +#if canImport(ZippyJSON) import ZippyJSON #endif public struct CustomJSONDecoder { - #if !os(Linux) + #if canImport(ZippyJSON) public var keyDecodingStrategy: ZippyJSONDecoder.KeyDecodingStrategy = ZippyJSONDecoder.KeyDecodingStrategy.useDefaultKeys #else public var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = JSONDecoder.KeyDecodingStrategy.useDefaultKeys #endif - public init() { - // Empty initialiser because we do not want the keyDecodingStrategy to be an initialiser parameter - } + public init() {} public func decode(_ type: T.Type, from data: Data) throws -> T { - #if !os(Linux) + #if canImport(ZippyJSON) let decoder = ZippyJSONDecoder() #else let decoder = JSONDecoder() diff --git a/Sources/Core/Sources/Util/FontUtil.swift b/Sources/Core/Sources/Util/FontUtil.swift index c1951574..23f82048 100644 --- a/Sources/Core/Sources/Util/FontUtil.swift +++ b/Sources/Core/Sources/Util/FontUtil.swift @@ -2,7 +2,7 @@ public enum FontUtil {} // TODO: Move to Delta Client because it's not cross-platform -#if os(macOS) +#if canImport(AppKit) import AppKit extension NSFont { @@ -29,7 +29,7 @@ extension FontUtil { return NSFont.systemFontSize(for: size) } } -#elseif os(iOS) +#elseif canImport(UIKit) import UIKit extension UIFont { From fb9eec672c0fba62935ebb5f0a6953b7b42d45f5 Mon Sep 17 00:00:00 2001 From: stackotter Date: Mon, 15 Apr 2024 00:22:29 +1000 Subject: [PATCH 09/84] Fix focusing of in-game menu buttons for tvOS and implement basics of VideoSettingsView for tvOS (which doesn't have any built-in Slider view) --- Sources/Client/Views/Play/InGameMenu.swift | 42 +++++++++++---- .../Client/Views/Play/JoinServerAndThen.swift | 7 ++- .../Client/Views/Settings/SettingsView.swift | 5 +- .../Views/Settings/VideoSettingsView.swift | 52 ++++++++++++++----- 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/Sources/Client/Views/Play/InGameMenu.swift b/Sources/Client/Views/Play/InGameMenu.swift index 4db9895b..70300890 100644 --- a/Sources/Client/Views/Play/InGameMenu.swift +++ b/Sources/Client/Views/Play/InGameMenu.swift @@ -6,15 +6,38 @@ struct InGameMenu: View { case settings } - #if os(tvOS) - @Namespace var focusNamespace - #endif - @EnvironmentObject var appState: StateWrapper @Binding var presented: Bool @State var state: InGameMenuState = .menu + @FocusState var focusState: FocusElements? + + func moveFocus(_ direction: MoveCommandDirection) { + if let focusState = focusState { + let step: Int + switch direction { + case .down: + step = 1 + case .up: + step = -1 + case .left, .right: + return + } + let count = FocusElements.allCases.count + // Add an extra count before taking the modulo cause Swift's mod operator isn't + // the real mathematical modulo. + let index = (focusState.rawValue + step + count) % count + self.focusState = FocusElements.allCases[index] + } + } + + enum FocusElements: Int, CaseIterable { + case backToGame + case settings + case disconnect + } + init(presented: Binding) { _presented = presented } @@ -31,19 +54,20 @@ struct InGameMenu: View { } #if !os(tvOS) .keyboardShortcut(.escape, modifiers: []) - #else - .prefersDefaultFocus(in: focusNamespace) #endif + .focused($focusState, equals: .backToGame) .buttonStyle(PrimaryButtonStyle()) Button("Settings") { state = .settings } + .focused($focusState, equals: .settings) .buttonStyle(SecondaryButtonStyle()) Button("Disconnect") { appState.update(to: .serverList) } + .focused($focusState, equals: .disconnect) .buttonStyle(SecondaryButtonStyle()) } #if !os(tvOS) @@ -57,10 +81,10 @@ struct InGameMenu: View { } .frame(width: geometry.size.width, height: geometry.size.height) .background(Color.black.opacity(0.702), alignment: .center) - #if os(tvOS) + .onAppear { + focusState = .backToGame + } .focusSection() - .focusScope(focusNamespace) - #endif } } } diff --git a/Sources/Client/Views/Play/JoinServerAndThen.swift b/Sources/Client/Views/Play/JoinServerAndThen.swift index d485d52b..2984b4e6 100644 --- a/Sources/Client/Views/Play/JoinServerAndThen.swift +++ b/Sources/Client/Views/Play/JoinServerAndThen.swift @@ -80,12 +80,17 @@ struct JoinServerAndThen: View { HStack { ProgressView(value: Double(received) / Double(total)) Text("\(received) of \(total)") - }.frame(maxWidth: 200) + } + #if !os(tvOS) + .frame(maxWidth: 200) + #endif } Button("Cancel", action: cancel) .buttonStyle(SecondaryButtonStyle()) + #if !os(tvOS) .frame(width: 150) + #endif case .joined: if let client = client { content(client) diff --git a/Sources/Client/Views/Settings/SettingsView.swift b/Sources/Client/Views/Settings/SettingsView.swift index e7d73ee9..c82d785d 100644 --- a/Sources/Client/Views/Settings/SettingsView.swift +++ b/Sources/Client/Views/Settings/SettingsView.swift @@ -24,7 +24,7 @@ struct SettingsView: View { self.isInGame = isInGame self.done = done - #if os(iOS) + #if os(iOS) || os(tvOS) // On iOS, the navigation isn't a split view, so we should show the settings page selection // view first instead of auto-selecting the first page. self._currentPage = State(initialValue: landingPage) @@ -36,13 +36,14 @@ struct SettingsView: View { var body: some View { NavigationView { List { - #if !os(tvOS) NavigationLink( "Video", destination: VideoSettingsView().padding(), tag: SettingsState.video, selection: $currentPage ) + + #if !os(tvOS) NavigationLink( "Controls", destination: ControlsSettingsView().padding(), diff --git a/Sources/Client/Views/Settings/VideoSettingsView.swift b/Sources/Client/Views/Settings/VideoSettingsView.swift index dfa2ddf0..af97f905 100644 --- a/Sources/Client/Views/Settings/VideoSettingsView.swift +++ b/Sources/Client/Views/Settings/VideoSettingsView.swift @@ -1,7 +1,6 @@ import SwiftUI import DeltaCore -#if !os(tvOS) struct VideoSettingsView: View { @EnvironmentObject var managedConfig: ManagedConfig @@ -9,7 +8,9 @@ struct VideoSettingsView: View { ScrollView { HStack { Text("Render distance: \(managedConfig.render.renderDistance)") - Spacer() + #if os(tvOS) + ProgressView(value: Double(managedConfig.render.renderDistance) / 32) + #else Slider( value: Binding { Float(managedConfig.render.renderDistance) @@ -20,8 +21,23 @@ struct VideoSettingsView: View { step: 1 ) .frame(width: 220) + #endif + } + #if os(tvOS) + .focusable() + .onMoveCommand { direction in + switch direction { + case .left: + managedConfig.render.renderDistance -= 1 + case .right: + managedConfig.render.renderDistance += 1 + default: + break + } } + #endif + #if !os(tvOS) HStack { Text("FOV: \(Int(managedConfig.render.fovY.rounded()))") Spacer() @@ -35,6 +51,7 @@ struct VideoSettingsView: View { ) .frame(width: 220) } + #endif HStack { Text("Render mode") @@ -49,23 +66,30 @@ struct VideoSettingsView: View { #elseif os(iOS) || os(tvOS) .pickerStyle(DefaultPickerStyle()) #endif + #if !os(tvOS) .frame(width: 220) + #endif } - HStack { - Text("Order independent transparency") - Spacer() - Toggle( - "Order independent transparency", - isOn: $managedConfig.render.enableOrderIndependentTransparency - ) - .labelsHidden() - .toggleStyle(.switch) - .frame(width: 220) - } + // Order independent transparency doesn't work on tvOS yet (our implementation uses a Metal + // feature which isn't supported on tvOS). + #if !os(tvOS) + HStack { + Text("Order independent transparency") + Spacer() + Toggle( + "Order independent transparency", + isOn: $managedConfig.render.enableOrderIndependentTransparency + ) + .labelsHidden() + .toggleStyle(.switch) + .frame(width: 220) + } + #endif } + #if !os(tvOS) .frame(width: 450) + #endif .navigationTitle("Video") } } -#endif From 9d6de3ea195c56a668744e7277e5e0d8ef182516 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 16 Apr 2024 00:24:52 +1000 Subject: [PATCH 10/84] Change app identifier back to dev.stackotter.delta-client (whoops) --- Bundler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bundler.toml b/Bundler.toml index cb5d2a04..0f0f125f 100644 --- a/Bundler.toml +++ b/Bundler.toml @@ -1,7 +1,7 @@ [apps.DeltaClient] product = 'DeltaClient' version = 'v0.1.0-alpha.1' -bundle_identifier = 'dev.stackotter.TVHelloWorld' +bundle_identifier = 'dev.stackotter.delta-client' category = 'public.app-category.games' minimum_macos_version = '11.0' icon = 'AppIcon.icns' From 706e71a70701abff39afb88ed698e06797470d26 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 16 Apr 2024 00:44:32 +1000 Subject: [PATCH 11/84] Fix macOS compilation errors (related to SwiftUI focus code which is unavailable on our min macOS version) --- Package.resolved | 20 +------------------- Package.swift | 2 +- Sources/Client/Views/Play/InGameMenu.swift | 15 ++++++++++++--- Sources/Core/Package.swift | 2 +- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8906cc5c..8bd73b80 100644 --- a/Package.resolved +++ b/Package.resolved @@ -172,24 +172,6 @@ "version": null } }, - { - "package": "Swift Grammar", - "repositoryURL": "https://github.com/tayloraswift/swift-grammar", - "state": { - "branch": null, - "revision": "163ce5d4d88db7d94f2f4ca1cabcb2ae65af8af7", - "version": "0.3.3" - } - }, - { - "package": "swift-hash", - "repositoryURL": "https://github.com/tayloraswift/swift-hash", - "state": { - "branch": null, - "revision": "c7ba0cde5eb63042c2196b02b65a770101c1ac11", - "version": "0.5.0" - } - }, { "package": "SwiftImage", "repositoryURL": "https://github.com/stackotter/swift-image.git", @@ -258,7 +240,7 @@ "repositoryURL": "https://github.com/stackotter/swift-png", "state": { "branch": null, - "revision": "dee856ec2cad5a91060ace4729db7e6d747572b3", + "revision": "b68a5662ef9887c8f375854720b3621f772bf8c5", "version": null } }, diff --git a/Package.swift b/Package.swift index dc4eedfd..a037ab90 100644 --- a/Package.swift +++ b/Package.swift @@ -62,7 +62,7 @@ targets.append(.executableTarget( name: "DeltaClient", dependencies: [ "DynamicShim", - // .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), + .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), .product(name: "ArgumentParser", package: "swift-argument-parser") ], path: "Sources/Client" diff --git a/Sources/Client/Views/Play/InGameMenu.swift b/Sources/Client/Views/Play/InGameMenu.swift index 70300890..1f025ba2 100644 --- a/Sources/Client/Views/Play/InGameMenu.swift +++ b/Sources/Client/Views/Play/InGameMenu.swift @@ -11,6 +11,7 @@ struct InGameMenu: View { @Binding var presented: Bool @State var state: InGameMenuState = .menu + #if os(tvOS) @FocusState var focusState: FocusElements? func moveFocus(_ direction: MoveCommandDirection) { @@ -37,6 +38,7 @@ struct InGameMenu: View { case settings case disconnect } + #endif init(presented: Binding) { _presented = presented @@ -52,23 +54,28 @@ struct InGameMenu: View { Button("Back to game") { presented = false } - #if !os(tvOS) - .keyboardShortcut(.escape, modifiers: []) - #endif + #if os(tvOS) .focused($focusState, equals: .backToGame) .buttonStyle(PrimaryButtonStyle()) + #else + .keyboardShortcut(.escape, modifiers: []) + #endif Button("Settings") { state = .settings } + #if os(tvOS) .focused($focusState, equals: .settings) .buttonStyle(SecondaryButtonStyle()) + #endif Button("Disconnect") { appState.update(to: .serverList) } + #if os(tvOS) .focused($focusState, equals: .disconnect) .buttonStyle(SecondaryButtonStyle()) + #endif } #if !os(tvOS) .frame(width: 200) @@ -81,10 +88,12 @@ struct InGameMenu: View { } .frame(width: geometry.size.width, height: geometry.size.height) .background(Color.black.opacity(0.702), alignment: .center) + #if os(tvOS) .onAppear { focusState = .backToGame } .focusSection() + #endif } } } diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 803816db..95bcf4ae 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -86,7 +86,7 @@ let package = Package( .package(url: "https://github.com/fourplusone/swift-package-zlib", from: "1.2.11"), .package(url: "https://github.com/stackotter/swift-image.git", branch: "master"), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), - .package(url: "https://github.com/stackotter/swift-png", revision: "dee856ec2cad5a91060ace4729db7e6d747572b3"), + .package(url: "https://github.com/stackotter/swift-png", revision: "b68a5662ef9887c8f375854720b3621f772bf8c5"), .package(url: "https://github.com/stackotter/ASN1Parser", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.6.0"), .package(url: "https://github.com/Kitura/SwiftyRequest.git", from: "3.1.0"), From fcf597fb2c8d9ed84e62a5fa76e867e89f1d4b22 Mon Sep 17 00:00:00 2001 From: Plasma Date: Fri, 24 May 2024 19:33:25 -0400 Subject: [PATCH 12/84] Fix horizontal flight speed (#191, #193) --- .../ECS/Systems/PlayerAccelerationSystem.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift index befe6f0f..a0ae42df 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift @@ -105,7 +105,9 @@ public struct PlayerAccelerationSystem: System { world, entityAttributes[.movementSpeed].value, sprinting.isSprinting, - onGround.onGround + onGround.onGround, + flying.isFlying, + playerAttributes.flyingSpeed ) impulse *= speed @@ -152,7 +154,9 @@ public struct PlayerAccelerationSystem: System { _ world: World, _ movementSpeed: Double, _ isSprinting: Bool, - _ onGround: Bool + _ onGround: Bool, + _ isFlying: Bool, + _ flightSpeed: Float ) -> Double { var speed: Double if onGround { @@ -166,6 +170,11 @@ public struct PlayerAccelerationSystem: System { let slipperiness = block.material.slipperiness speed = movementSpeed * 0.216 / (slipperiness * slipperiness * slipperiness) + } else if isFlying { + speed = Double(flightSpeed) + if isSprinting { + speed *= 2 + } } else { speed = 0.02 if isSprinting { From 018ce0ca1036d4cb8f3fd882a8dca2bf6edfb307 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 23 May 2024 10:09:23 +1000 Subject: [PATCH 13/84] Update Xcode version in Contributing.md --- Contributing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Contributing.md b/Contributing.md index 23dd06e9..4454b1a7 100644 --- a/Contributing.md +++ b/Contributing.md @@ -34,9 +34,9 @@ If your contributions follow these guidelines, they'll be much more likely to ge ### Delta Client -**Important**: Only Xcode 14 is supported, Xcode 12 builds don't work because Delta Client uses new +**Important**: Only Xcode 14+ is supported, Xcode 12 builds don't work because Delta Client uses new automatic `Codable` conformance and there are some weird discrepancies between Xcode 12's swift -compiler and Xcode 14's swift compiler. Xcode 13 isn't supported because it causes some weird memory +compiler and Xcode 14+'s swift compiler. Xcode 13 isn't supported because it causes some weird memory corruption issues. [Delta Client](https://github.com/stackotter/delta-client) uses the From 8acd39bc9d2e8aae85d251a510ac288a31ace1da Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 23 May 2024 10:15:18 +1000 Subject: [PATCH 14/84] Fix in-game menu button styles (accidentally restricted the styles to tvOS when restricting focus modifiers) --- Sources/Client/Views/Play/InGameMenu.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Client/Views/Play/InGameMenu.swift b/Sources/Client/Views/Play/InGameMenu.swift index 1f025ba2..f01a60a6 100644 --- a/Sources/Client/Views/Play/InGameMenu.swift +++ b/Sources/Client/Views/Play/InGameMenu.swift @@ -56,8 +56,8 @@ struct InGameMenu: View { } #if os(tvOS) .focused($focusState, equals: .backToGame) - .buttonStyle(PrimaryButtonStyle()) #else + .buttonStyle(PrimaryButtonStyle()) .keyboardShortcut(.escape, modifiers: []) #endif @@ -66,16 +66,16 @@ struct InGameMenu: View { } #if os(tvOS) .focused($focusState, equals: .settings) - .buttonStyle(SecondaryButtonStyle()) #endif + .buttonStyle(SecondaryButtonStyle()) Button("Disconnect") { appState.update(to: .serverList) } #if os(tvOS) .focused($focusState, equals: .disconnect) - .buttonStyle(SecondaryButtonStyle()) #endif + .buttonStyle(SecondaryButtonStyle()) } #if !os(tvOS) .frame(width: 200) From 52d5c5d5bb300cfe3b7b4a9a6848bb67e54e88e7 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 09:35:35 +1000 Subject: [PATCH 15/84] Fix vertical flying speed (now matches Vanilla) --- Sources/Core/Sources/ECS/Systems/PlayerFlightSystem.swift | 6 ++---- .../Core/Sources/ECS/Systems/PlayerPositionSystem.swift | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerFlightSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerFlightSystem.swift index 52323c76..b54dfb06 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerFlightSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerFlightSystem.swift @@ -36,12 +36,10 @@ public struct PlayerFlightSystem: System { let jumpPressed = inputState.inputs.contains(.jump) if sneakPressed != jumpPressed { if sneakPressed { - velocity.y = Double(-attributes.flyingSpeed * 3) + velocity.y -= Double(attributes.flyingSpeed * 3) } else { - velocity.y = Double(attributes.flyingSpeed * 3) + velocity.y += Double(attributes.flyingSpeed * 3) } - } else if !sneakPressed && !jumpPressed { - velocity.y = 0 } } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerPositionSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerPositionSystem.swift index 960284b6..2f4be1c1 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerPositionSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerPositionSystem.swift @@ -5,10 +5,11 @@ public struct PlayerPositionSystem: System { var family = nexus.family( requiresAll: EntityPosition.self, EntityVelocity.self, + EntityFlying.self, ClientPlayerEntity.self ).makeIterator() - guard let (position, velocity, _) = family.next() else { + guard let (position, velocity, flying, _) = family.next() else { log.error("PlayerPositionSystem failed to get player to tick") return } @@ -17,5 +18,9 @@ public struct PlayerPositionSystem: System { position.x = MathUtil.clamp(position.x, -29_999_999, 29_999_999) position.z = MathUtil.clamp(position.z, -29_999_999, 29_999_999) + + if flying.isFlying { + velocity.y *= 0.6 + } } } From 25b0b3e9f04c9e0474fb6fd64789ede0f5534ecc Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 11:28:38 +1000 Subject: [PATCH 16/84] Fix typo in Microsoft auth implementation (somehow still worked anyway?) --- Sources/Core/Sources/Account/Microsoft/MicrosoftAPI.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Core/Sources/Account/Microsoft/MicrosoftAPI.swift b/Sources/Core/Sources/Account/Microsoft/MicrosoftAPI.swift index 48ed6173..ae447677 100644 --- a/Sources/Core/Sources/Account/Microsoft/MicrosoftAPI.swift +++ b/Sources/Core/Sources/Account/Microsoft/MicrosoftAPI.swift @@ -97,7 +97,7 @@ public enum MicrosoftAPI { let (_, data) = try await RequestUtil.performFormRequest( url: authenticationURL, body: [ - "tenant": "concumers", + "tenant": "consumers", "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "client_id": clientId, "device_code": deviceCode @@ -330,7 +330,8 @@ public enum MicrosoftAPI { /// - Returns: The user's Minecraft access token. private static func getMinecraftAccessToken(_ xstsToken: String, _ xboxLiveToken: XboxLiveToken) async throws -> MinecraftAccessToken { let payload = MinecraftXboxAuthenticationRequest( - identityToken: "XBL3.0 x=\(xboxLiveToken.userHash);\(xstsToken)") + identityToken: "XBL3.0 x=\(xboxLiveToken.userHash);\(xstsToken)" + ) let (_, data) = try await RequestUtil.performJSONRequest( url: minecraftXboxAuthenticationURL, From 13555c7a8d516ae3af83aa4758c1c6f495d2a873 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 12:04:31 +1000 Subject: [PATCH 17/84] Release inputs when chat or inventory gets opened, and reset acceleration to zero (fixes #192) --- Sources/Core/Sources/ECS/Singles/InputState.swift | 1 + .../ECS/Systems/PlayerAccelerationSystem.swift | 12 ++++++++---- .../Core/Sources/ECS/Systems/PlayerInputSystem.swift | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/Core/Sources/ECS/Singles/InputState.swift b/Sources/Core/Sources/ECS/Singles/InputState.swift index 512ac646..273c4059 100644 --- a/Sources/Core/Sources/ECS/Singles/InputState.swift +++ b/Sources/Core/Sources/ECS/Singles/InputState.swift @@ -68,6 +68,7 @@ public final class InputState: SingleComponent { } for input in inputs { + print("Releasing \(input)") newlyReleased.append(KeyReleaseEvent(key: nil, input: input)) } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift index a0ae42df..71315afa 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerAccelerationSystem.swift @@ -15,10 +15,6 @@ public struct PlayerAccelerationSystem: System { public func update(_ nexus: Nexus, _ world: World) { let guiState = nexus.single(GUIStateStorage.self).component - guard guiState.movementAllowed else { - return - } - var family = nexus.family( requiresAll: EntityNutrition.self, EntityFlying.self, @@ -52,6 +48,14 @@ public struct PlayerAccelerationSystem: System { return } + // This should just act as an optimization, movement inputs shouldn't get here + // in the first place when movement isn't allowed so this function would just + // zero the acceleration anyway. + guard guiState.movementAllowed else { + acceleration.vector = .zero + return + } + let inputState = nexus.single(InputState.self).component let inputs = Self.addControllerMovement(inputState.inputs, inputState.leftThumbstick) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 0ff829c4..9572f1ba 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -55,6 +55,7 @@ public final class PlayerInputSystem: System { guiState.showDebugScreen = !guiState.showDebugScreen case .toggleInventory: guiState.showInventory = !guiState.showInventory + inputState.releaseAll() eventBus.dispatch(ReleaseCursorEvent()) case .slot1: inventory.selectedHotbarSlot = 0 @@ -219,6 +220,10 @@ public final class PlayerInputSystem: System { } } else if event.input == .openChat { guiState.messageInput = "" + // TODO: Refactor input handling to be a bit more declarative so that something like + // issue #192 is less likely to happen again. Besides, this input handling code is + // pretty spaghetti and could do with a makeover anyway. + inputState.releaseAll() eventBus.dispatch(ReleaseCursorEvent()) } else if event.key == .forwardSlash { guiState.messageInput = "/" From 872f9925242dbd14e6e515f54da5ef00b8760dbe Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 13:54:10 +1000 Subject: [PATCH 18/84] Release cursor and inputs when chat is opened via '/' key --- Sources/Core/Sources/ECS/Singles/InputState.swift | 1 - .../Sources/ECS/Systems/PlayerFrictionSystem.swift | 1 + .../Core/Sources/ECS/Systems/PlayerInputSystem.swift | 2 ++ Sources/Core/Sources/Game.swift | 10 ++++++++-- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/Core/Sources/ECS/Singles/InputState.swift b/Sources/Core/Sources/ECS/Singles/InputState.swift index 273c4059..512ac646 100644 --- a/Sources/Core/Sources/ECS/Singles/InputState.swift +++ b/Sources/Core/Sources/ECS/Singles/InputState.swift @@ -68,7 +68,6 @@ public final class InputState: SingleComponent { } for input in inputs { - print("Releasing \(input)") newlyReleased.append(KeyReleaseEvent(key: nil, input: input)) } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift index e7385c61..8e6d36c5 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerFrictionSystem.swift @@ -23,6 +23,7 @@ public struct PlayerFrictionSystem: System { } velocity.x *= multiplier + velocity.y *= 0.98 velocity.z *= multiplier } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 9572f1ba..d29c5092 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -227,6 +227,8 @@ public final class PlayerInputSystem: System { eventBus.dispatch(ReleaseCursorEvent()) } else if event.key == .forwardSlash { guiState.messageInput = "/" + inputState.releaseAll() + eventBus.dispatch(ReleaseCursorEvent()) } // Suppress inputs while the user is typing. diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 4d7264cb..04895545 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -165,6 +165,12 @@ public final class Game: @unchecked Sendable { inputState.moveRightThumbstick(x, y) } + public func accessInputState(acquireLock: Bool = true, action: (InputState) -> R) -> R { + if acquireLock { nexusLock.acquireWriteLock() } + defer { if acquireLock { nexusLock.unlock() } } + return action(inputState) + } + /// Gets a copy of the current GUI state. /// - Returns: A copy of the current GUI state. public func guiState() -> GUIState { @@ -176,10 +182,10 @@ public final class Game: @unchecked Sendable { /// Mutates the GUI state using a provided action. /// - acquireLock: If `false`, a nexus lock will not be acquired. Use with caution. /// - action: Action to run on GUI state. - public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) -> Void) { + public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) -> R) -> R { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } - action(&_guiState.inner) + return action(&_guiState.inner) } // MARK: Entity From 7a86a9e7867fb6451d8ae6ce30e78be3ad51aa41 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 14:28:35 +1000 Subject: [PATCH 19/84] Update InputView to convert mouse position to true pixels before handing them to InputState, and propagate scalingFactor GUI.update --- Sources/Client/Views/Play/InputView.swift | 7 ++++- Sources/Core/Renderer/GUI/GUI.swift | 27 ++++++++++++------- Sources/Core/Renderer/GUI/GUIRenderer.swift | 22 ++++++++------- .../Core/Sources/ECS/Singles/InputState.swift | 1 + 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/Sources/Client/Views/Play/InputView.swift b/Sources/Client/Views/Play/InputView.swift index 0673806e..56496c22 100644 --- a/Sources/Client/Views/Play/InputView.swift +++ b/Sources/Client/Views/Play/InputView.swift @@ -1,5 +1,6 @@ import SwiftUI import DeltaCore +import DeltaRenderer struct InputView: View { @EnvironmentObject var modal: Modal @@ -103,7 +104,11 @@ struct InputView: View { let viewFrame = geometry.frame(in: .global) let x = (NSEvent.mouseLocation.x - window.frame.minX) - viewFrame.minX let y = window.frame.maxY - NSEvent.mouseLocation.y - viewFrame.minY - return (Float(x), Float(y)) + + // AppKit gives us the position scaled by the screen's scaling factor, so we + // adjust it back to get the position in terms of true pixels. + let scalingFactor = CGFloat(GUIRenderer.screenScalingFactor()) + return (Float(x * scalingFactor), Float(y * scalingFactor)) } #endif diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift index d3c31d3f..bd8f6d09 100644 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ b/Sources/Core/Renderer/GUI/GUI.swift @@ -89,9 +89,12 @@ struct GUI { ) } - mutating func update(_ screenSize: Vec2i) -> GUIGroupElement { + mutating func update( + effectiveDrawableSize: Vec2i, + scalingFactor: Float + ) -> GUIGroupElement { let state = client.game.guiState() - var root = GUIGroupElement(screenSize) + var root = GUIGroupElement(effectiveDrawableSize) guard state.showHUD else { return root @@ -112,10 +115,10 @@ struct GUI { } // Chat - chat(&root, state.chat.messages, state.messageInput, state.messageInputCursorIndex, screenSize) + chat(&root, state.chat.messages, state.messageInput, state.messageInputCursorIndex, effectiveDrawableSize) if state.showInventory { - inventory(&root, screenSize) + inventory(&root, effectiveDrawableSize) } return root @@ -123,12 +126,12 @@ struct GUI { func inventory( _ parentGroup: inout GUIGroupElement, - _ screenSize: Vec2i + _ effectiveDrawableSize: Vec2i ) { // TODO: Figure out the exact overlay opacity that vanilla uses parentGroup.add( GUIRectangle( - size: screenSize, + size: effectiveDrawableSize, color: [0, 0, 0, 0.702] ), .center @@ -180,7 +183,7 @@ struct GUI { _ messages: Deque, _ messageInput: String?, _ messageInputCursorIndex: String.Index, - _ screenSize: Vec2i + _ effectiveDrawableSize: Vec2i ) { let chatIsOpen = messageInput != nil @@ -244,7 +247,7 @@ struct GUI { if let messageInput = messageInput { parentGroup.add(GUITextInput( content: messageInput, - width: screenSize.x - 4, + width: effectiveDrawableSize.x - 4, cursorIndex: messageInputCursorIndex ), .bottom(2), .left(2)) } @@ -427,10 +430,14 @@ struct GUI { } mutating func meshes( - effectiveDrawableSize: Vec2i + drawableSize: Vec2i, + scalingFactor: Float ) throws -> [GUIElementMesh] { profiler.push(.updateContent) - let root = update(effectiveDrawableSize) + let root = update( + effectiveDrawableSize: Vec2i(Vec2f(drawableSize) / scalingFactor), + scalingFactor: scalingFactor + ) profiler.pop() profiler.push(.createMeshes) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 73a44efd..5081f6e7 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -8,9 +8,10 @@ import UIKit /// The renderer for the GUI (chat, f3, scoreboard etc.). public final class GUIRenderer: Renderer { + static let scale: Float = 2 + var device: MTLDevice var font: Font - var scale: Float = 2 var uniformsBuffer: MTLBuffer var pipelineState: MTLRenderPipelineState var gui: GUI @@ -68,10 +69,10 @@ public final class GUIRenderer: Renderer { let drawableSize = view.drawableSize let width = Float(drawableSize.width) let height = Float(drawableSize.height) - let scale = Self.adjustScale(scale) + let scalingFactor = Self.scale * Self.screenScalingFactor() // Adjust scale per screen scale factor - var uniforms = createUniforms(width, height, scale) + var uniforms = createUniforms(width, height, scalingFactor) if uniforms != previousUniforms || true { uniformsBuffer.contents().copyMemory(from: &uniforms, byteCount: MemoryLayout.size) previousUniforms = uniforms @@ -80,7 +81,8 @@ public final class GUIRenderer: Renderer { // Create meshes let meshes = try gui.meshes( - effectiveDrawableSize: Vec2i(Int(width / scale), Int(height / scale)) + drawableSize: Vec2i(Int(width), Int(height)), + scalingFactor: scalingFactor ) profiler.push(.encode) @@ -205,16 +207,18 @@ public final class GUIRenderer: Renderer { mesh.size &+= Vec2i(position) } - static func adjustScale(_ scale: Float) -> Float { - // Adjust scale per screen scale factor + /// Gets the scaling factor of the screen that Delta Client's currently getting rendered for. + public static func screenScalingFactor() -> Float { + // Higher density displays have higher scaling factors to keep content a similar real world + // size across screens. #if canImport(AppKit) - let screenScaleFactor = Float(NSApp.windows.first?.screen?.backingScaleFactor ?? 1) + let screenScalingFactor = Float(NSApp.windows.first?.screen?.backingScaleFactor ?? 1) #elseif canImport(UIKit) - let screenScaleFactor = Float(UIScreen.main.scale) + let screenScalingFactor = Float(UIScreen.main.scale) #else #error("Unsupported platform, unknown screen scale factor") #endif - return screenScaleFactor * scale + return screenScalingFactor } func createUniforms(_ width: Float, _ height: Float, _ scale: Float) -> GUIUniforms { diff --git a/Sources/Core/Sources/ECS/Singles/InputState.swift b/Sources/Core/Sources/ECS/Singles/InputState.swift index 512ac646..e3df3c22 100644 --- a/Sources/Core/Sources/ECS/Singles/InputState.swift +++ b/Sources/Core/Sources/ECS/Singles/InputState.swift @@ -20,6 +20,7 @@ public final class InputState: SingleComponent { public private(set) var inputs: Set = [] /// The current absolute mouse position relative to the play area's top left corner. + /// Measured in true pixels (not scaled down by the screen's scaling factor). public private(set) var mousePosition: Vec2f = Vec2f(0, 0) /// The mouse delta since the last call to ``resetMouseDelta()``. public private(set) var mouseDelta: Vec2f = Vec2f(0, 0) From 17c1b35d844b04601c88b12134e3f704dd37c3de Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 25 May 2024 17:20:39 +1000 Subject: [PATCH 20/84] Initial GUI system reimplementation (in anticipation of adding mouse interactivity) --- Sources/Core/Renderer/GUI/GUI.swift | 449 ------------------ .../GUI/GUIElement/GUIColoredString.swift | 23 - .../Renderer/GUI/GUIElement/GUIElement.swift | 3 - .../GUI/GUIElement/GUIGroupElement.swift | 46 -- .../GUI/GUIElement/GUIInventoryItem.swift | 82 ---- .../GUI/GUIElement/GUIInventorySlot.swift | 32 -- .../Renderer/GUI/GUIElement/GUIList.swift | 88 ---- .../Renderer/GUI/GUIElement/GUIListItem.swift | 4 - .../GUI/GUIElement/GUIRectangle.swift | 19 - .../Renderer/GUI/GUIElement/GUIStatBar.swift | 52 -- .../GUI/GUIElement/GUITextInput.swift | 59 --- .../Renderer/GUI/GUIElement/GUIXPBar.swift | 53 --- .../GUI/GUIElement/String+GUIElement.swift | 9 - .../Renderer/GUI/GUIFixedSizeElement.swift | 5 - Sources/Core/Renderer/GUI/GUIRenderer.swift | 99 +++- .../Renderer/GUI/HorizontalConstraint.swift | 7 - .../Renderer/GUI/VerticalConstraint.swift | 7 - Sources/Core/Renderer/RenderCoordinator.swift | 2 +- .../GUI/Constraints.swift | 14 +- Sources/Core/Sources/GUI/GUIBuilder.swift | 6 + Sources/Core/Sources/GUI/GUIElement.swift | 248 ++++++++++ .../GUI}/GUISprite.swift | 8 +- .../GUI}/GUISpriteDescriptor.swift | 19 +- .../Sources/GUI/HorizontalConstraint.swift | 7 + .../GUI/HorizontalOffset.swift | 4 +- Sources/Core/Sources/GUI/InGameGUI.swift | 17 + .../Core/Sources/GUI/VerticalConstraint.swift | 7 + .../GUI/VerticalOffset.swift | 4 +- 28 files changed, 393 insertions(+), 980 deletions(-) delete mode 100644 Sources/Core/Renderer/GUI/GUI.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIColoredString.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIElement.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIInventoryItem.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIList.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIListItem.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIRectangle.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIStatBar.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUITextInput.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/GUIXPBar.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIElement/String+GUIElement.swift delete mode 100644 Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift delete mode 100644 Sources/Core/Renderer/GUI/HorizontalConstraint.swift delete mode 100644 Sources/Core/Renderer/GUI/VerticalConstraint.swift rename Sources/Core/{Renderer => Sources}/GUI/Constraints.swift (64%) create mode 100644 Sources/Core/Sources/GUI/GUIBuilder.swift create mode 100644 Sources/Core/Sources/GUI/GUIElement.swift rename Sources/Core/{Renderer/GUI/GUIElement => Sources/GUI}/GUISprite.swift (88%) rename Sources/Core/{Renderer/GUI/GUIElement => Sources/GUI}/GUISpriteDescriptor.swift (62%) create mode 100644 Sources/Core/Sources/GUI/HorizontalConstraint.swift rename Sources/Core/{Renderer => Sources}/GUI/HorizontalOffset.swift (80%) create mode 100644 Sources/Core/Sources/GUI/InGameGUI.swift create mode 100644 Sources/Core/Sources/GUI/VerticalConstraint.swift rename Sources/Core/{Renderer => Sources}/GUI/VerticalOffset.swift (80%) diff --git a/Sources/Core/Renderer/GUI/GUI.swift b/Sources/Core/Renderer/GUI/GUI.swift deleted file mode 100644 index bd8f6d09..00000000 --- a/Sources/Core/Renderer/GUI/GUI.swift +++ /dev/null @@ -1,449 +0,0 @@ -import Metal -import Collections -import FirebladeMath -import DeltaCore -import SwiftCPUDetect - -struct GUI { - /// The number of seconds until messages should be hidden from the regular GUI. - static let messageHideDelay: Double = 10 - /// The maximum number of messages displayed in the regular GUI. - static let maximumDisplayedMessages = 10 - /// The width of the chat history. - static let chatHistoryWidth = 330 - /// The width of indent to use when wrapping chat messages. - static let chatWrapIndent = 4 - - /// The system's CPU display name. - static let cpuName = HWInfo.CPU.name() - /// The system's CPU architecture. - static let cpuArch = CpuArchitecture.current()?.rawValue - /// The system's total memory. - static let totalMem = (HWInfo.ramAmount() ?? 0) / (1024 * 1024 * 1024) - /// A string containing information about the system's default GPU. - static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() - - var client: Client - var renderStatistics = RenderStatistics(gpuCountersEnabled: false) - var fpsUpdateInterval = 0.4 - var lastFPSUpdate: CFAbsoluteTime = 0 - var savedRenderStatistics = RenderStatistics(gpuCountersEnabled: false) - var context: GUIContext - var profiler: Profiler - - init( - client: Client, - device: MTLDevice, - commandQueue: MTLCommandQueue, - profiler: Profiler - ) throws { - self.client = client - self.profiler = profiler - - let resources = client.resourcePack.vanillaResources - let font = resources.fontPalette.defaultFont - let fontArrayTexture = try font.createArrayTexture( - device: device, - commandQueue: commandQueue - ) - fontArrayTexture.label = "fontArrayTexture" - - let guiTexturePalette = try GUITexturePalette(resources.guiTexturePalette) - let guiArrayTexture = try MetalTexturePalette.createArrayTexture( - for: resources.guiTexturePalette, - device: device, - commandQueue: commandQueue, - includeAnimations: false - ) - guiArrayTexture.label = "guiArrayTexture" - - let itemTexturePalette = resources.itemTexturePalette - let itemArrayTexture = try MetalTexturePalette.createArrayTexture( - for: resources.itemTexturePalette, - device: device, - commandQueue: commandQueue, - includeAnimations: false - ) - itemArrayTexture.label = "itemArrayTexture" - - let blockTexturePalette = resources.blockTexturePalette - let blockArrayTexture = try MetalTexturePalette.createArrayTexture( - for: resources.blockTexturePalette, - device: device, - commandQueue: commandQueue, - includeAnimations: false - ) - blockArrayTexture.label = "blockArrayTexture" - - context = GUIContext( - font: font, - fontArrayTexture: fontArrayTexture, - guiTexturePalette: guiTexturePalette, - guiArrayTexture: guiArrayTexture, - itemTexturePalette: itemTexturePalette, - itemArrayTexture: itemArrayTexture, - itemModelPalette: resources.itemModelPalette, - blockArrayTexture: blockArrayTexture, - blockModelPalette: resources.blockModelPalette, - blockTexturePalette: blockTexturePalette - ) - } - - mutating func update( - effectiveDrawableSize: Vec2i, - scalingFactor: Float - ) -> GUIGroupElement { - let state = client.game.guiState() - var root = GUIGroupElement(effectiveDrawableSize) - - guard state.showHUD else { - return root - } - - // TODO: Crosshair should be visible in spectator mode when able to interact with an entity - if client.game.currentGamemode() != .spectator { - // Hot bar area (hot bar, health, food, etc.) - hotbarArea(&root) - - // Render crosshair - root.add(GUISprite.crossHair, .center) - } - - // Debug screen - if state.showDebugScreen { - debugScreen(&root) - } - - // Chat - chat(&root, state.chat.messages, state.messageInput, state.messageInputCursorIndex, effectiveDrawableSize) - - if state.showInventory { - inventory(&root, effectiveDrawableSize) - } - - return root - } - - func inventory( - _ parentGroup: inout GUIGroupElement, - _ effectiveDrawableSize: Vec2i - ) { - // TODO: Figure out the exact overlay opacity that vanilla uses - parentGroup.add( - GUIRectangle( - size: effectiveDrawableSize, - color: [0, 0, 0, 0.702] - ), - .center - ) - - var group = GUIGroupElement([GUISprite.inventory.descriptor.size.x, GUISprite.inventory.descriptor.size.y]) - group.add(GUISprite.inventory, .center) - - let (armorSlots, offHand, craftingArea, craftingResult, mainArea, hotbar) = client.game.accessPlayer { player in - ( - player.inventory.armorSlots, - player.inventory.offHand, - player.inventory.craftingArea, - player.inventory.craftingResult, - player.inventory.mainArea, - player.inventory.hotbar - ) - } - - for (y, slot) in armorSlots.enumerated() { - group.add(GUIInventorySlot(slot: slot), .top(18 * y + 8), .left(8)) - } - - group.add(GUIInventorySlot(slot: offHand), .top(62), .left(77)) - - for (y, row) in craftingArea.enumerated() { - for (x, slot) in row.enumerated() { - group.add(GUIInventorySlot(slot: slot), .top(18 * y + 18), .left(18 * x + 98)) - } - } - - group.add(GUIInventorySlot(slot: craftingResult), .top(28), .left(154)) - - for (y, row) in mainArea.enumerated() { - for (x, slot) in row.enumerated() { - group.add(GUIInventorySlot(slot: slot), .top(18 * y + 84), .left(18 * x + 8)) - } - } - - for (x, slot) in hotbar.enumerated() { - group.add(GUIInventorySlot(slot: slot), .top(142), .left(18 * x + 8)) - } - - parentGroup.add(group, .center) - } - - func chat( - _ parentGroup: inout GUIGroupElement, - _ messages: Deque, - _ messageInput: String?, - _ messageInputCursorIndex: String.Index, - _ effectiveDrawableSize: Vec2i - ) { - let chatIsOpen = messageInput != nil - - let font = client.resourcePack.vanillaResources.fontPalette.defaultFont - let builder = TextMeshBuilder(font: font) - let threshold = CFAbsoluteTimeGetCurrent() - Self.messageHideDelay - var chatLines: [(text: String, indent: Bool)] = [] - for message in messages.reversed() { - if !chatIsOpen && message.timeReceived < threshold { - break - } - - let text = message.content.toText(with: client.resourcePack.getDefaultLocale()) - let wrappedLines: [String] - wrappedLines = builder.wrap( - text, - maximumWidth: Self.chatHistoryWidth - 2, - indent: Self.chatWrapIndent - ) - - var done = false - for (i, line) in wrappedLines.enumerated().reversed() { - if chatLines.count >= Self.maximumDisplayedMessages { - done = true - break - } - - chatLines.append((text: line, indent: i != 0)) - } - - if done { - break - } - } - - if !chatLines.isEmpty { - var chat = GUIList(rowHeight: 9) - - for (chatLine, indent) in chatLines.reversed() { - if indent { - var group = GUIGroupElement([Self.chatHistoryWidth - 1, chat.rowHeight]) - group.add(chatLine, .top(0), .left(Self.chatWrapIndent)) - chat.add(group) - } else { - chat.add(chatLine) - } - } - - parentGroup.add(GUIRectangle( - size: [Self.chatHistoryWidth, chatLines.count * chat.rowHeight], - color: [0, 0, 0, 0.5] - ), .bottom(40), .left(0)) - - parentGroup.add( - chat, - .bottom(40), - .left(2) - ) - } - - if let messageInput = messageInput { - parentGroup.add(GUITextInput( - content: messageInput, - width: effectiveDrawableSize.x - 4, - cursorIndex: messageInputCursorIndex - ), .bottom(2), .left(2)) - } - } - - func hotbarArea(_ parentGroup: inout GUIGroupElement) { - var group = GUIGroupElement([184, 40]) - var gamemode: Gamemode = .adventure - var health: Float = 0 - var food: Int = 0 - var selectedSlot: Int = 0 - var xpBarProgress: Float = 0 - var xpLevel: Int = 0 - var hotbarSlots: [Slot] = [] - client.game.accessPlayer { player in - gamemode = player.gamemode.gamemode - health = player.health.health - food = player.nutrition.food - selectedSlot = player.inventory.selectedHotbarSlot - xpBarProgress = player.experience.experienceBarProgress - xpLevel = player.experience.experienceLevel - hotbarSlots = player.inventory.hotbar - } - - stats( - &group, - gamemode: gamemode, - health: health, - food: food, - xpBarProgress: xpBarProgress, - xpLevel: xpLevel - ) - hotbar(&group, selectedSlot: selectedSlot, slots: hotbarSlots) - - parentGroup.add(group, .bottom(-1), .center) - } - - func hotbar(_ group: inout GUIGroupElement, selectedSlot: Int, slots: [Slot]) { - group.add(GUISprite.hotbar, .bottom(1), .center) - group.add(GUISprite.selectedHotbarSlot, .bottom(0), .left(20 * selectedSlot)) - - for (i, slot) in slots.enumerated() { - group.add(GUIInventorySlot(slot: slot), .bottom(2), .left(20 * i + 4)) - } - } - - func stats( - _ group: inout GUIGroupElement, - gamemode: Gamemode, - health: Float, - food: Int, - xpBarProgress: Float, - xpLevel: Int - ) { - if gamemode.hasHealth { - // Health - group.add( - GUIStatBar( - value: Int(health.rounded()), - fullIcon: .fullHeart, - halfIcon: .halfHeart, - outlineIcon: .heartOutline - ), - .top(0), - .left(1) - ) - - // Hunger - group.add( - GUIStatBar( - value: food, - fullIcon: .fullFood, - halfIcon: .halfFood, - outlineIcon: .foodOutline, - reversed: true - ), - .top(0), - .right(1) - ) - - // XP bar - group.add( - GUIXPBar(level: xpLevel, progress: xpBarProgress), - .top(4), - .center - ) - } - } - - mutating func debugScreen(_ root: inout GUIGroupElement) { - // Fetch relevant player properties - var blockPosition = BlockPosition(x: 0, y: 0, z: 0) - var chunkSectionPosition = ChunkSectionPosition(sectionX: 0, sectionY: 0, sectionZ: 0) - var position: Vec3d = .zero - var pitch: Float = 0 - var yaw: Float = 0 - var heading: Direction = .north - var gamemode: Gamemode = .adventure - client.game.accessPlayer { player in - position = player.position.vector - blockPosition = player.position.blockUnderneath - chunkSectionPosition = player.position.chunkSection - pitch = MathUtil.degrees(from: player.rotation.pitch) - yaw = MathUtil.degrees(from: player.rotation.yaw) - heading = player.rotation.heading - gamemode = player.gamemode.gamemode - } - blockPosition.y += 1 - - // Slow down updating of render stats to be easier to read - if CFAbsoluteTimeGetCurrent() - lastFPSUpdate > fpsUpdateInterval { - lastFPSUpdate = CFAbsoluteTimeGetCurrent() - savedRenderStatistics = renderStatistics - } - let renderStatistics = savedRenderStatistics - - // Version - var leftList = GUIList(rowHeight: 9, renderRowBackground: true) - leftList.add("Minecraft \(Constants.versionString) (Delta Client)") - - // FPS - var theoreticalFPSString = "" - if let theoreticalFPS = renderStatistics.averageTheoreticalFPS { - theoreticalFPSString = " (\(theoreticalFPS) theoretical)" - } - let cpuTimeString = String(format: "%.02f", renderStatistics.averageCPUTime * 1000.0) - var gpuTimeString = "" - if let gpuTime = renderStatistics.averageGPUTime { - gpuTimeString = String(format: ", %.02fms gpu", gpuTime) - } - let fpsString = String(format: "%.00f", renderStatistics.averageFPS) - leftList.add("\(fpsString) fps\(theoreticalFPSString) (\(cpuTimeString)ms cpu\(gpuTimeString))") - - // Dimension - leftList.add("Dimension: \(client.game.world.dimension.identifier)") - leftList.add(spacer: 6) - - // Position - let x = String(format: "%.02f", position.x) - let y = String(format: "%.02f", position.y) - let z = String(format: "%.02f", position.z) - leftList.add("XYZ: \(x) / \(y) / \(z)") - - // Block under feet - leftList.add("Block: \(blockPosition.x) \(blockPosition.y) \(blockPosition.z)") - - // Chunk section and relative position - let relativePosition = blockPosition.relativeToChunkSection - let relativePositionString = "\(relativePosition.x) \(relativePosition.y) \(relativePosition.z)" - let chunkSectionString = "\(chunkSectionPosition.sectionX) \(chunkSectionPosition.sectionY) \(chunkSectionPosition.sectionZ)" - leftList.add("Chunk: \(relativePositionString) in \(chunkSectionString)") - - // Heading and rotation - let yawString = String(format: "%.01f", yaw) - let pitchString = String(format: "%.01f", pitch) - leftList.add("Facing: \(heading) (Towards \(heading.isPositive ? "positive" : "negative") \(heading.axis)) (\(yawString) / \(pitchString))") - - // Lighting (at foot level) - var lightPosition = blockPosition - lightPosition.y += 1 - let skyLightLevel = client.game.world.getSkyLightLevel(at: lightPosition) - let blockLightLevel = client.game.world.getBlockLightLevel(at: lightPosition) - leftList.add("Light: \(skyLightLevel) sky, \(blockLightLevel) block") - - // Biome - let biome = client.game.world.getBiome(at: blockPosition) - leftList.add("Biome: \(biome?.identifier.description ?? "not loaded")") - - // Gamemode - leftList.add("Gamemode: \(gamemode.string)") - - // System information - var rightList = GUIList(rowHeight: 9, renderRowBackground: true, alignment: .right) - rightList.add("CPU: \(Self.cpuName ?? "unknown") (\(Self.cpuArch ?? "n/a"))") - rightList.add("Total mem: \(Self.totalMem)GB") - rightList.add("GPU: \(Self.gpuInfo ?? "unknown")") - - root.add(leftList, .position(2, 3)) - root.add(rightList, .top(3), .right(2)) - } - - mutating func meshes( - drawableSize: Vec2i, - scalingFactor: Float - ) throws -> [GUIElementMesh] { - profiler.push(.updateContent) - let root = update( - effectiveDrawableSize: Vec2i(Vec2f(drawableSize) / scalingFactor), - scalingFactor: scalingFactor - ) - profiler.pop() - - profiler.push(.createMeshes) - let meshes = try root.meshes(context: context) - profiler.pop() - - return meshes - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIColoredString.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIColoredString.swift deleted file mode 100644 index 662f7ee8..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIColoredString.swift +++ /dev/null @@ -1,23 +0,0 @@ -import FirebladeMath - -struct GUIColoredString: GUIElement { - var text: String - var color: Vec4f - var outlineColor: Vec4f? - - init(_ text: String, _ color: Vec4f, outlineColor: Vec4f? = nil) { - self.text = text - self.color = color - self.outlineColor = outlineColor - } - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - let builder = TextMeshBuilder(font: context.font) - return try builder.build( - text, - fontArrayTexture: context.fontArrayTexture, - color: color, - outlineColor: outlineColor - ).map { [$0] } ?? [] - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIElement.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIElement.swift deleted file mode 100644 index de618bf1..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIElement.swift +++ /dev/null @@ -1,3 +0,0 @@ -protocol GUIElement { - func meshes(context: GUIContext) throws -> [GUIElementMesh] -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift deleted file mode 100644 index f71916e6..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIGroupElement.swift +++ /dev/null @@ -1,46 +0,0 @@ -import FirebladeMath - -struct GUIGroupElement: GUIFixedSizeElement { - var size: Vec2i - var children: [(GUIElement, Constraints)] - - init(_ size: Vec2i) { - self.size = size - children = [] - } - - mutating func add(_ element: GUIElement, _ constraints: Constraints) { - children.append((element, constraints)) - } - - mutating func add( - _ element: GUIElement, - _ vertical: VerticalConstraint, - _ horizontal: HorizontalConstraint - ) { - children.append((element, Constraints(vertical, horizontal))) - } - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - var meshes: [GUIElementMesh] = [] - for (element, constraints) in children { - var elementMeshes = try element.meshes(context: context) - - let elementSize: Vec2i - if let group = element as? GUIFixedSizeElement { - elementSize = group.size - } else { - elementSize = elementMeshes.size() - } - - let offset = constraints.solve(innerSize: elementSize, outerSize: size) - for i in 0.. [GUIElementMesh] { - guard let model = context.itemModelPalette.model(for: itemId) else { - throw GUIRendererError.invalidItemId(itemId) - } - - switch model { - case let .layered(textures, _): - return textures.map { texture in - switch texture { - case let .block(index): - return GUIElementMesh(slice: index, texture: context.blockArrayTexture) - case let .item(index): - return GUIElementMesh(slice: index, texture: context.itemArrayTexture) - } - } - case let .blockModel(modelId): - guard let model = context.blockModelPalette.model(for: modelId, at: nil) else { - log.warning("Missing block model of id \(modelId) (for item)") - return [] - } - - // Get the block's transformation assuming that each block model part has the same - // associated gui transformation (I don't see why this wouldn't always be true). - var transformation: Mat4x4f - if let transformsIndex = model.parts.first?.displayTransformsIndex { - transformation = context.blockModelPalette.displayTransforms[transformsIndex].gui - } else { - transformation = MatrixUtil.identity - } - - transformation *= MatrixUtil.translationMatrix([-0.5, -0.5, -0.5]) - * MatrixUtil.rotationMatrix(x: .pi) - * MatrixUtil.rotationMatrix(y: -.pi / 4) - * MatrixUtil.rotationMatrix(x: -.pi / 6) - - var geometry = Geometry() - var translucentGeometry = SortableMeshElement() - BlockMeshBuilder( - model: model, - position: BlockPosition(x: 0, y: 0, z: 0), - modelToWorld: transformation * MatrixUtil.scalingMatrix(9.76), - culledFaces: [], - lightLevel: LightLevel(sky: 15, block: 15), - neighbourLightLevels: [:], - tintColor: [1, 1, 1], - blockTexturePalette: context.blockTexturePalette - ).build(into: &geometry, translucentGeometry: &translucentGeometry) - - var vertices: [GUIVertex] = [] - vertices.reserveCapacity(geometry.vertices.count) - for vertex in geometry.vertices { - vertices.append(GUIVertex( - position: [vertex.x, vertex.y], - uv: [vertex.u, vertex.v], - tint: [vertex.r, vertex.g, vertex.b, 1], - textureIndex: vertex.textureIndex - )) - } - - // TODO: Handle translucent block items - - var mesh = GUIElementMesh( - size: [16, 16], - arrayTexture: context.blockArrayTexture, - vertices: .flatArray(vertices) - ) - mesh.position = [8, 8] - return [mesh] - case .empty, .entity: - return [] - } - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift deleted file mode 100644 index f421bdbd..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIInventorySlot.swift +++ /dev/null @@ -1,32 +0,0 @@ -import FirebladeMath -import DeltaCore - -struct GUIInventorySlot: GUIElement, GUIFixedSizeElement { - var slot: Slot - - let size = Vec2i(17, 18) - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - guard let stack = slot.stack else { - return [] - } - - // Starts in the upper left corner of the slot and extends 1 pixel further to the - // right and downwards (due to the placement of the count text). - var group = GUIGroupElement(Vec2i(17, 18)) - group.add(GUIInventoryItem(itemId: stack.itemId), .position(0, 0)) - - if stack.count != 1 { - // Drop shadow for the count - group.add( - GUIColoredString(String(stack.count), [62, 62, 62, 255] / 255), - .bottom(0), - .right(0) - ) - - group.add(String(stack.count), .bottom(1), .right(1)) - } - - return try group.meshes(context: context) - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift deleted file mode 100644 index 45013f14..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIList.swift +++ /dev/null @@ -1,88 +0,0 @@ -import FirebladeMath - -struct GUIList: GUIElement { - var items: [GUIListItem] - var rowHeight: Int - var renderRowBackground: Bool - var alignment: Alignment - - enum Alignment { - case left - case right - } - - init(rowHeight: Int, renderRowBackground: Bool = false, alignment: Alignment = .left) { - items = [] - self.rowHeight = rowHeight - self.renderRowBackground = renderRowBackground - self.alignment = alignment - } - - mutating func add(_ element: GUIElement) { - items.append(.element(element)) - } - - mutating func add(spacer height: Int) { - items.append(.spacer(height)) - } - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - var bgMeshes: [GUIElementMesh] = [] - var meshes: [GUIElementMesh] = [] - var processedItems: [(item: GUIListItem, meshes: [GUIElementMesh], size: Vec2i)] = [] - var maxWidth = 0 - var currentY = 0 - - // Cache meshes while finding widest element to prevent generating them again. - for item in items { - switch item { - case .element(let element): - let elementMeshes = try element.meshes(context: context) - let size = elementMeshes.size() - if size.x > maxWidth { - maxWidth = size.x - } - - processedItems.append((item: item, meshes: elementMeshes, size: size)) - case .spacer: - processedItems.append((item: item, meshes: [], size: [0, 0])) - } - } - - for (i, (item, _, size)) in processedItems.enumerated() { - switch item { - case .element: - switch alignment { - case .left: - processedItems[i].meshes.translate(amount: [0, currentY]) - case .right: - processedItems[i].meshes.translate(amount: [maxWidth - size.x, currentY]) - } - - if renderRowBackground { - let bgSize: Vec2i = [size.x + 2, rowHeight] - var bg = GUIRectangle( - size: bgSize, - color: [0x50, 0x50, 0x50, 0x90] / 255 - ).meshes(context: context) - - switch alignment { - case .left: - bg.translate(amount: [-1, currentY - 1]) - case .right: - bg.translate(amount: [maxWidth - size.x - 1, currentY - 1]) - } - - bgMeshes.append(contentsOf: bg) - } - - meshes.append(contentsOf: processedItems[i].meshes) - currentY += rowHeight - case .spacer(let height): - currentY += height - } - } - - return bgMeshes + meshes - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIListItem.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIListItem.swift deleted file mode 100644 index 961ff4c7..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIListItem.swift +++ /dev/null @@ -1,4 +0,0 @@ -enum GUIListItem { - case element(GUIElement) - case spacer(Int) -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIRectangle.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIRectangle.swift deleted file mode 100644 index 5e96904e..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIRectangle.swift +++ /dev/null @@ -1,19 +0,0 @@ -import FirebladeMath - -/// A solid colored rectangle. -struct GUIRectangle: GUIElement { - var size: Vec2i - var color: Vec4f - - func meshes(context: GUIContext) -> [GUIElementMesh] { - return [GUIElementMesh( - size: size, - arrayTexture: nil, - quads: [GUIQuad( - position: .zero, - size: Vec2f(size), - color: color - )] - )] - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIStatBar.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIStatBar.swift deleted file mode 100644 index 2cc2956c..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIStatBar.swift +++ /dev/null @@ -1,52 +0,0 @@ -/// An icon-based stat bar such as the health bar or hunger bar. -struct GUIStatBar: GUIElement { - /// Stat value out of 20 (1 unit is a half icon). - var value: Int - /// The full icon (e.g. full heart). - var fullIcon: GUISprite - /// The half icon (e.g. half heart). - var halfIcon: GUISprite - /// The outline icon (displayed behind each spot an icon could go). - var outlineIcon: GUISprite - /// Constructor for horizontal constraint. - var reversed = false - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - var mesh = GUIElementMesh( - size: [81, 9], - arrayTexture: context.guiArrayTexture, - vertices: .empty - ) - - func add(_ sprite: GUISprite, at x: Int) { - mesh.vertices.append(contentsOf: .tuples([GUIQuad( - for: sprite.descriptor, - guiTexturePalette: context.guiTexturePalette, - guiArrayTexture: context.guiArrayTexture, - position: [x, 0] - ).toVertexTuple()])) - } - - let fullIconCount = value / 2 - let hasHalfIcon = value % 2 == 1 - for i in 0..<10 { - // Position - var x = i * 8 - if reversed { - x = mesh.size.x - x - outlineIcon.descriptor.size.x - } - - // Outline - add(outlineIcon, at: x) - - // Full and half icons - if i < fullIconCount { - add(fullIcon, at: x) - } else if hasHalfIcon && i == fullIconCount { - add(halfIcon, at: x) - } - } - - return [mesh] - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUITextInput.swift b/Sources/Core/Renderer/GUI/GUIElement/GUITextInput.swift deleted file mode 100644 index 37dd3d8d..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUITextInput.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -struct GUITextInput: GUIElement { - var content: String - var width: Int - var cursorIndex: String.Index - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - // Background - let background = GUIRectangle( - size: [width, 11], - color: [0, 0, 0, 0.5] - ).meshes(context: context) - - // Message - let messageBeforeCursor = String(content.prefix(upTo: cursorIndex)) - let messageAfterCursor = String(content.suffix(from: cursorIndex)) - var messageMeshBeforeCursor = try messageBeforeCursor.meshes(context: context) - var messageMeshAfterCursor = try messageAfterCursor.meshes(context: context) - - if messageMeshBeforeCursor.size().x != 0 { - messageMeshBeforeCursor.translate(amount: [2, 2]) - messageMeshAfterCursor.translate(amount: [3 + messageMeshBeforeCursor.size().x, 2]) - } else { - messageMeshBeforeCursor.translate(amount: [0, 2]) - messageMeshAfterCursor.translate(amount: [2, 2]) - } - var cursor: [GUIElementMesh] = [] - // When at the end of the message, use an underscore cursor. Otherwise, use a vertical bar cursor - if Int(CFAbsoluteTimeGetCurrent() * 10/3) % 2 == 1 { - if messageMeshAfterCursor.size().x == 0 { - cursor = try "_".meshes(context: context) - let messageWidth = messageMeshBeforeCursor.size().x + messageMeshAfterCursor.size().x - var xOffset: Int - if messageWidth != 0 { - xOffset = messageWidth + 4 - } else { - xOffset = 2 - } - cursor.translate(amount: [xOffset, 2]) - } else { - let cursorWidth = 1 - let cursorHeight = 10 - cursor = GUIRectangle( - size: [cursorWidth, cursorHeight], - color: [1, 1, 1, 1] - ).meshes(context: context) - var xOffset: Int - if messageMeshBeforeCursor.size().x == 0 { - xOffset = messageMeshBeforeCursor.size().x + 2 - } else { - xOffset = messageMeshBeforeCursor.size().x + 3 - } - cursor.translate(amount: [xOffset, 1]) - } - } - return background + messageMeshBeforeCursor + messageMeshAfterCursor + cursor - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUIXPBar.swift b/Sources/Core/Renderer/GUI/GUIElement/GUIXPBar.swift deleted file mode 100644 index cd882497..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/GUIXPBar.swift +++ /dev/null @@ -1,53 +0,0 @@ -import FirebladeMath - -struct GUIXPBar: GUIElement { - static let background = GUISprite.xpBarBackground - static let foreground = GUISprite.xpBarForeground - static let textColor: Vec4f = [126, 252, 31, 255] / 255 - - var level: Int - var progress: Float - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - var spriteMesh = GUIElementMesh( - size: [ - Self.background.descriptor.size.x, - Self.background.descriptor.size.y - ], - arrayTexture: context.guiArrayTexture, - vertices: .empty - ) - spriteMesh.position = [0, 6] - - func add(_ sprite: GUISpriteDescriptor, at position: Vec2i) { - spriteMesh.vertices.append(contentsOf: .tuples([GUIQuad( - for: sprite, - guiTexturePalette: context.guiTexturePalette, - guiArrayTexture: context.guiArrayTexture, - position: position - ).toVertexTuple()])) - } - - var foreground = Self.foreground.descriptor - foreground.size.x = Int(Float(foreground.size.x) * progress) - - add(Self.background.descriptor, at: [0, 0]) - add(foreground, at: [0, 0]) - - var textMeshes: [GUIElementMesh] = [] - if level > 0 { - textMeshes = try GUIColoredString( - String(level), - Self.textColor, - outlineColor: [0, 0, 0, 1] - ).meshes(context: context) - - for (i, var mesh) in textMeshes.enumerated() { - mesh.position = [(spriteMesh.size.x - mesh.size.x) / 2, 0] - textMeshes[i] = mesh - } - } - - return [spriteMesh] + textMeshes - } -} diff --git a/Sources/Core/Renderer/GUI/GUIElement/String+GUIElement.swift b/Sources/Core/Renderer/GUI/GUIElement/String+GUIElement.swift deleted file mode 100644 index e5ad56e3..00000000 --- a/Sources/Core/Renderer/GUI/GUIElement/String+GUIElement.swift +++ /dev/null @@ -1,9 +0,0 @@ -extension String: GUIElement { - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - let builder = TextMeshBuilder(font: context.font) - return try builder.build( - self, - fontArrayTexture: context.fontArrayTexture - ).map { [$0] } ?? [] - } -} diff --git a/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift b/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift deleted file mode 100644 index f41399d7..00000000 --- a/Sources/Core/Renderer/GUI/GUIFixedSizeElement.swift +++ /dev/null @@ -1,5 +0,0 @@ -import FirebladeMath - -protocol GUIFixedSizeElement: GUIElement { - var size: Vec2i { get } -} diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 5081f6e7..68f9f23d 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -14,10 +14,21 @@ public final class GUIRenderer: Renderer { var font: Font var uniformsBuffer: MTLBuffer var pipelineState: MTLRenderPipelineState - var gui: GUI var profiler: Profiler var previousUniforms: GUIUniforms? + var gui: InGameGUI + + var fontArrayTexture: MTLTexture + var guiTexturePalette: GUITexturePalette + var guiArrayTexture: MTLTexture + var itemTexturePalette: TexturePalette + var itemArrayTexture: MTLTexture + var itemModelPalette: ItemModelPalette + var blockArrayTexture: MTLTexture + var blockModelPalette: BlockModelPalette + var blockTexturePalette: TexturePalette + var cache: [GUIElementMesh] = [] public init( @@ -32,6 +43,43 @@ public final class GUIRenderer: Renderer { // Create array texture font = client.resourcePack.vanillaResources.fontPalette.defaultFont + let resources = client.resourcePack.vanillaResources + let font = resources.fontPalette.defaultFont + fontArrayTexture = try font.createArrayTexture( + device: device, + commandQueue: commandQueue + ) + fontArrayTexture.label = "fontArrayTexture" + + guiTexturePalette = try GUITexturePalette(resources.guiTexturePalette) + guiArrayTexture = try MetalTexturePalette.createArrayTexture( + for: resources.guiTexturePalette, + device: device, + commandQueue: commandQueue, + includeAnimations: false + ) + guiArrayTexture.label = "guiArrayTexture" + + itemTexturePalette = resources.itemTexturePalette + itemArrayTexture = try MetalTexturePalette.createArrayTexture( + for: resources.itemTexturePalette, + device: device, + commandQueue: commandQueue, + includeAnimations: false + ) + itemArrayTexture.label = "itemArrayTexture" + itemModelPalette = resources.itemModelPalette + + blockTexturePalette = resources.blockTexturePalette + blockArrayTexture = try MetalTexturePalette.createArrayTexture( + for: resources.blockTexturePalette, + device: device, + commandQueue: commandQueue, + includeAnimations: false + ) + blockArrayTexture.label = "blockArrayTexture" + blockModelPalette = resources.blockModelPalette + // Create uniforms buffer uniformsBuffer = try MetalUtil.makeBuffer( device, @@ -49,12 +97,7 @@ public final class GUIRenderer: Renderer { blendingEnabled: true ) - gui = try GUI( - client: client, - device: device, - commandQueue: commandQueue, - profiler: profiler - ) + gui = InGameGUI() } public func render( @@ -80,11 +123,16 @@ public final class GUIRenderer: Renderer { profiler.pop() // Create meshes - let meshes = try gui.meshes( - drawableSize: Vec2i(Int(width), Int(height)), - scalingFactor: scalingFactor + let renderable = gui.body.resolveConstraints( + availableSize: Vec2i( + Int(width / scalingFactor), + Int(height / scalingFactor) + ), + font: font ) + let meshes = try meshes(for: renderable) + profiler.push(.encode) // Set vertex buffers encoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 0) @@ -114,6 +162,37 @@ public final class GUIRenderer: Renderer { profiler.pop() } + func meshes(for renderable: GUIElement.GUIRenderable) throws -> [GUIElementMesh] { + switch renderable.content { + case let .text(wrappedLines, hangingIndent): + let builder = TextMeshBuilder(font: font) + var meshes = try wrappedLines.compactMap { (line: String) in + do { + return try builder.build(line, fontArrayTexture: fontArrayTexture) + } catch let error as LocalizedError { + throw error + .with("Text", line) + } catch { + throw error + } + } + for i in meshes.indices where i != 0 { + meshes[i].position.x += hangingIndent + } + return meshes + case let .sprite(descriptor): + return try [GUIElementMesh( + sprite: descriptor, + guiTexturePalette: guiTexturePalette, + guiArrayTexture: guiArrayTexture + )] + case nil, .clickable: + var meshes = try renderable.children.map(\.0).flatMap(meshes) + meshes.translate(amount: renderable.relativePosition) + return meshes + } + } + static func optimizeMeshes(_ meshes: [GUIElementMesh]) throws -> [GUIElementMesh] { var textureToIndex: [String: Int] = [:] var boxes: [[(position: Vec2i, size: Vec2i)]] = [] diff --git a/Sources/Core/Renderer/GUI/HorizontalConstraint.swift b/Sources/Core/Renderer/GUI/HorizontalConstraint.swift deleted file mode 100644 index 4bb28d7c..00000000 --- a/Sources/Core/Renderer/GUI/HorizontalConstraint.swift +++ /dev/null @@ -1,7 +0,0 @@ -enum HorizontalConstraint { - case left(Int) - case center(HorizontalOffset?) - case right(Int) - - static let center = Self.center(nil) -} diff --git a/Sources/Core/Renderer/GUI/VerticalConstraint.swift b/Sources/Core/Renderer/GUI/VerticalConstraint.swift deleted file mode 100644 index 57cae5ac..00000000 --- a/Sources/Core/Renderer/GUI/VerticalConstraint.swift +++ /dev/null @@ -1,7 +0,0 @@ -enum VerticalConstraint { - case top(Int) - case center(VerticalOffset?) - case bottom(Int) - - static let center = Self.center(nil) -} diff --git a/Sources/Core/Renderer/RenderCoordinator.swift b/Sources/Core/Renderer/RenderCoordinator.swift index 4873a8fe..94f01d48 100644 --- a/Sources/Core/Renderer/RenderCoordinator.swift +++ b/Sources/Core/Renderer/RenderCoordinator.swift @@ -362,7 +362,7 @@ public final class RenderCoordinator: NSObject, MTKViewDelegate { ) // Update statistics in gui - guiRenderer.gui.renderStatistics = statistics + // guiRenderer.gui.renderStatistics = statistics commandBuffer.commit() profiler.pop() diff --git a/Sources/Core/Renderer/GUI/Constraints.swift b/Sources/Core/Sources/GUI/Constraints.swift similarity index 64% rename from Sources/Core/Renderer/GUI/Constraints.swift rename to Sources/Core/Sources/GUI/Constraints.swift index 654c81dc..df584582 100644 --- a/Sources/Core/Renderer/GUI/Constraints.swift +++ b/Sources/Core/Sources/GUI/Constraints.swift @@ -1,21 +1,21 @@ import FirebladeMath -struct Constraints { - var vertical: VerticalConstraint - var horizontal: HorizontalConstraint +public struct Constraints { + public var vertical: VerticalConstraint + public var horizontal: HorizontalConstraint - init(_ vertical: VerticalConstraint, _ horizontal: HorizontalConstraint) { + public init(_ vertical: VerticalConstraint, _ horizontal: HorizontalConstraint) { self.vertical = vertical self.horizontal = horizontal } - static let center = Constraints(.center, .center) + public static let center = Constraints(.center, .center) - static func position(_ x: Int, _ y: Int) -> Constraints { + public static func position(_ x: Int, _ y: Int) -> Constraints { return Constraints(.top(y), .left(x)) } - func solve(innerSize: Vec2i, outerSize: Vec2i) -> Vec2i { + public func solve(innerSize: Vec2i, outerSize: Vec2i) -> Vec2i { let x: Int switch horizontal { case .left(let distance): diff --git a/Sources/Core/Sources/GUI/GUIBuilder.swift b/Sources/Core/Sources/GUI/GUIBuilder.swift new file mode 100644 index 00000000..1028e24b --- /dev/null +++ b/Sources/Core/Sources/GUI/GUIBuilder.swift @@ -0,0 +1,6 @@ +@resultBuilder +public struct GUIBuilder { + public static func buildBlock(_ elements: GUIElement...) -> [GUIElement] { + elements + } +} diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift new file mode 100644 index 00000000..eb47d0cd --- /dev/null +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -0,0 +1,248 @@ +public indirect enum GUIElement { + case text(_ content: String, wrap: Bool = false) + case clickable(_ element: GUIElement, action: () -> Void) + case sprite(GUISprite) + case customSprite(GUISpriteDescriptor) + /// Stacks elements in the y direction. Aligns elements to the top left by default. + case list(spacing: Int, elements: [GUIElement]) + /// Stacks elements in the z direction. Non-positioned elements default to the top-left corner. + /// Elements appear on top of the elements that come before them. + case stack(elements: [GUIElement]) + case positioned(element: GUIElement, constraints: Constraints) + case sized(element: GUIElement, size: Vec2i) + + public static let textWrapIndent: Int = 4 + public static let lineSpacing: Int = 1 + + public static func list(spacing: Int, @GUIBuilder elements: () -> [GUIElement]) -> GUIElement { + .list(spacing: spacing, elements: elements()) + } + + public static func stack(@GUIBuilder elements: () -> [GUIElement]) -> GUIElement { + .stack(elements: elements()) + } + + public func centered() -> GUIElement { + .positioned(element: self, constraints: .center) + } + + public func positionInParent(_ x: Int, _ y: Int) -> GUIElement { + .positioned(element: self, constraints: .position(x, y)) + } + + public func sized(_ x: Int, _ y: Int) -> GUIElement { + .sized(element: self, size: Vec2i(x, y)) + } + + public struct GUIRenderable { + public var relativePosition: Vec2i + public var size: Vec2i + public var content: Content? + public var children: [(GUIRenderable, GUIElement)] + + public enum Content { + case text(wrappedLines: [String], hangingIndent: Int) + case clickable(action: () -> Void) + case sprite(GUISpriteDescriptor) + } + } + + public func resolveConstraints( + availableSize: Vec2i, + font: Font + ) -> GUIRenderable { + let relativePosition: Vec2i + let size: Vec2i + let content: GUIRenderable.Content? + let children: [(GUIRenderable, GUIElement)] + switch self { + case let .text(text, wrap): + // Wrap the lines, but if wrapping is disabled wrap to a width of Int.max (so that we can + // still compute the width of the line). + let lines = Self.wrap( + text, + maximumWidth: wrap ? availableSize.x : .max, + indent: Self.textWrapIndent, + font: font + ) + relativePosition = .zero + size = Vec2i( + lines.map(\.width).max() ?? 0, + lines.count * Font.defaultCharacterHeight + (lines.count - 1) * Self.lineSpacing + ) + content = .text( + wrappedLines: lines.map(\.line), + hangingIndent: Self.textWrapIndent + ) + children = [] + case let .clickable(label, action): + let child = label.resolveConstraints( + availableSize: availableSize, + font: font + ) + relativePosition = .zero + size = child.size + content = .clickable(action: action) + children = [(child, label)] + case let .sprite(sprite): + let descriptor = sprite.descriptor + relativePosition = .zero + size = descriptor.size + content = .sprite(descriptor) + children = [] + case let .customSprite(descriptor): + relativePosition = .zero + size = descriptor.size + content = .sprite(descriptor) + children = [] + case let .list(spacing, elements): + var availableSize = availableSize + var childPosition = Vec2i(0, 0) + children = elements.map { element in + var renderable = element.resolveConstraints( + availableSize: availableSize, + font: font + ) + renderable.relativePosition.y += childPosition.y + + let rowHeight = renderable.size.y + spacing + childPosition.y += rowHeight + availableSize.y -= rowHeight + + return (renderable, element) + } + relativePosition = .zero + let width = children.map(\.0.size.x).max() ?? 0 + let height = elements.isEmpty ? 0 : childPosition.y - spacing + size = Vec2i(width, height) + content = nil + case let .stack(elements): + children = elements.map { element in + let renderable = element.resolveConstraints( + availableSize: availableSize, + font: font + ) + return (renderable, element) + } + size = Vec2i( + children.map(\.0.size.x).max() ?? 0, + children.map(\.0.size.y).max() ?? 0 + ) + relativePosition = .zero + content = nil + case let .positioned(element, constraints): + let child = element.resolveConstraints( + availableSize: availableSize, + font: font + ) + children = [(child, element)] + relativePosition = constraints.solve( + innerSize: child.size, + outerSize: availableSize + ) + size = child.size + content = nil + case let .sized(element, specifiedSize): + let child = element.resolveConstraints( + availableSize: specifiedSize, + font: font + ) + children = [(child, element)] + relativePosition = .zero + size = specifiedSize + content = nil + } + + return GUIRenderable( + relativePosition: relativePosition, + size: size, + content: content, + children: children + ) + } + + /// `indent` must be less than `maximumWidth` and `maximumWidth` must greater than the width of + /// each individual character in the string. + static func wrap(_ text: String, maximumWidth: Int, indent: Int, font: Font) -> [(line: String, width: Int)] { + assert(indent < maximumWidth, "indent must be smaller than maximumWidth") + + if text == "" { + return [(line: "", width: 0)] + } + + var wrapIndex: String.Index? = nil + var latestSpace: String.Index? = nil + var width = 0 + for i in text.indices { + let character = text[i] + guard let descriptor = Self.descriptor(for: character, from: font) else { + continue + } + + assert( + descriptor.renderedWidth < maximumWidth, + "maximumWidth must be greater than every individual character in the string" + ) + + // Compute the width with the current character included + var nextWidth = width + descriptor.renderedWidth + if i != text.startIndex { + nextWidth += 1 // character spacing + } + + // TODO: wrap on other characters such as '-' as well + if character == " " { + latestSpace = i + } + + // Break before the current character if it'd bring the text over the maximum width + if nextWidth > maximumWidth { + if let spaceIndex = latestSpace { + wrapIndex = spaceIndex + } else { + wrapIndex = i + } + break + } else { + width = nextWidth + } + } + + var lines: [(line: String, width: Int)] = [] + if let wrapIndex = wrapIndex { + lines = [ + (line: String(text[text.startIndex.. CharacterDescriptor? { + if let descriptor = font.descriptor(for: character) { + return descriptor + } else if let descriptor = font.descriptor(for: "�") { + return descriptor + } else { + log.warning("Failed to replace invalid character '\(character)' with placeholder '�'.") + return nil + } + } +} diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUISprite.swift b/Sources/Core/Sources/GUI/GUISprite.swift similarity index 88% rename from Sources/Core/Renderer/GUI/GUIElement/GUISprite.swift rename to Sources/Core/Sources/GUI/GUISprite.swift index 33885806..98f21707 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUISprite.swift +++ b/Sources/Core/Sources/GUI/GUISprite.swift @@ -1,5 +1,5 @@ /// A sprite in the GUI texture palette. -enum GUISprite: GUIElement { +public enum GUISprite { case heartOutline case fullHeart case halfHeart @@ -17,7 +17,7 @@ enum GUISprite: GUIElement { case inventory /// The descriptor for the sprite. - var descriptor: GUISpriteDescriptor { + public var descriptor: GUISpriteDescriptor { switch self { case .heartOutline: return .icon(0, 0) @@ -51,8 +51,4 @@ enum GUISprite: GUIElement { return GUISpriteDescriptor(slice: .inventory, position: [0, 0], size: [176, 166]) } } - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - return try descriptor.meshes(context: context) - } } diff --git a/Sources/Core/Renderer/GUI/GUIElement/GUISpriteDescriptor.swift b/Sources/Core/Sources/GUI/GUISpriteDescriptor.swift similarity index 62% rename from Sources/Core/Renderer/GUI/GUIElement/GUISpriteDescriptor.swift rename to Sources/Core/Sources/GUI/GUISpriteDescriptor.swift index 1111e607..64722d97 100644 --- a/Sources/Core/Renderer/GUI/GUIElement/GUISpriteDescriptor.swift +++ b/Sources/Core/Sources/GUI/GUISpriteDescriptor.swift @@ -1,14 +1,13 @@ import FirebladeMath -import DeltaCore /// Describes how to render a specific sprite from a ``GUITexturePalette``. -struct GUISpriteDescriptor: GUIElement { +public struct GUISpriteDescriptor { /// The slice containing the sprite. - var slice: GUITextureSlice + public var slice: GUITextureSlice /// The position of the sprite in the texture. Origin is at the top left. - var position: Vec2i + public var position: Vec2i /// The size of the sprite. - var size: Vec2i + public var size: Vec2i /// Creates the descriptor for the specified icon. Icons start 16 pixels from the left of the /// texture and are arranged as a grid of 9x9 icons. @@ -16,19 +15,11 @@ struct GUISpriteDescriptor: GUIElement { /// - xIndex: The horizontal index of the sprite. /// - yIndex: The vertical index of the sprite. /// - Returns: A sprite descriptor for the icon. - static func icon(_ xIndex: Int, _ yIndex: Int) -> GUISpriteDescriptor { + public static func icon(_ xIndex: Int, _ yIndex: Int) -> GUISpriteDescriptor { return GUISpriteDescriptor( slice: .icons, position: [xIndex * 9 + 16, yIndex * 9], size: [9, 9] ) } - - func meshes(context: GUIContext) throws -> [GUIElementMesh] { - return try [GUIElementMesh( - sprite: self, - guiTexturePalette: context.guiTexturePalette, - guiArrayTexture: context.guiArrayTexture - )] - } } diff --git a/Sources/Core/Sources/GUI/HorizontalConstraint.swift b/Sources/Core/Sources/GUI/HorizontalConstraint.swift new file mode 100644 index 00000000..c2eb36af --- /dev/null +++ b/Sources/Core/Sources/GUI/HorizontalConstraint.swift @@ -0,0 +1,7 @@ +public enum HorizontalConstraint { + case left(Int) + case center(HorizontalOffset?) + case right(Int) + + public static let center = Self.center(nil) +} diff --git a/Sources/Core/Renderer/GUI/HorizontalOffset.swift b/Sources/Core/Sources/GUI/HorizontalOffset.swift similarity index 80% rename from Sources/Core/Renderer/GUI/HorizontalOffset.swift rename to Sources/Core/Sources/GUI/HorizontalOffset.swift index 2a4bf288..b4f7d9c9 100644 --- a/Sources/Core/Renderer/GUI/HorizontalOffset.swift +++ b/Sources/Core/Sources/GUI/HorizontalOffset.swift @@ -1,9 +1,9 @@ -enum HorizontalOffset { +public enum HorizontalOffset { case left(Int) case right(Int) /// The offset value, negative for up, positive for down. - var value: Int { + public var value: Int { switch self { case .left(let offset): return -offset diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift new file mode 100644 index 00000000..6166a433 --- /dev/null +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -0,0 +1,17 @@ +public struct InGameGUI { + public init() {} + + public var body: GUIElement = GUIElement.stack { + GUIElement.list(spacing: 2) { + GUIElement.text("Hello, world!") + + GUIElement.clickable(.text("Press me")) { + print("Button pressed") + } + } + .centered() + + GUIElement.text("Top left") + .positionInParent(0, 0) + } +} diff --git a/Sources/Core/Sources/GUI/VerticalConstraint.swift b/Sources/Core/Sources/GUI/VerticalConstraint.swift new file mode 100644 index 00000000..b2a584de --- /dev/null +++ b/Sources/Core/Sources/GUI/VerticalConstraint.swift @@ -0,0 +1,7 @@ +public enum VerticalConstraint { + case top(Int) + case center(VerticalOffset?) + case bottom(Int) + + public static let center = Self.center(nil) +} diff --git a/Sources/Core/Renderer/GUI/VerticalOffset.swift b/Sources/Core/Sources/GUI/VerticalOffset.swift similarity index 80% rename from Sources/Core/Renderer/GUI/VerticalOffset.swift rename to Sources/Core/Sources/GUI/VerticalOffset.swift index c41b332d..bf74e548 100644 --- a/Sources/Core/Renderer/GUI/VerticalOffset.swift +++ b/Sources/Core/Sources/GUI/VerticalOffset.swift @@ -1,9 +1,9 @@ -enum VerticalOffset { +public enum VerticalOffset { case up(Int) case down(Int) /// The offset value, negative for up, positive for down. - var value: Int { + public var value: Int { switch self { case .up(let offset): return -offset From 5d39ec53ac29dee45d2acf7e09f88a4ec6146fe5 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 26 May 2024 12:57:09 +1000 Subject: [PATCH 21/84] Move InGameGUI instance into property of Game, get mouse interactions working in general, implement padding/backgrounds, and fix deadlock in PlayerInputSystem --- Sources/Core/Package.swift | 5 +- .../Core/Renderer/GUI/GUIElementMesh.swift | 15 ++ Sources/Core/Renderer/GUI/GUIRenderer.swift | 45 +++--- Sources/Core/Sources/Client.swift | 14 +- .../Core/Sources/ECS/Singles/InputState.swift | 10 +- .../ECS/Systems/PlayerInputSystem.swift | 26 +++- Sources/Core/Sources/GUI/GUIBuilder.swift | 16 ++ Sources/Core/Sources/GUI/GUIElement.swift | 142 +++++++++++++++--- Sources/Core/Sources/GUI/InGameGUI.swift | 64 ++++++-- Sources/Core/Sources/GUIState.swift | 27 +++- Sources/Core/Sources/Game.swift | 62 +++++++- .../ChatMessageClientboundPacket.swift | 4 +- Sources/Core/Sources/Util/Box.swift | 2 + Sources/Core/Sources/Util/ReadWriteLock.swift | 34 ++++- Sources/Core/Sources/Util/TickScheduler.swift | 2 +- 15 files changed, 392 insertions(+), 76 deletions(-) diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 95bcf4ae..793329be 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -2,6 +2,8 @@ import PackageDescription +let debugLocks = false + // MARK: Products var productTargets = ["DeltaCore", "DeltaLogger"] @@ -34,7 +36,8 @@ var targets: [Target] = [ .product(name: "SwiftImage", package: "swift-image"), .product(name: "PNG", package: "swift-png") ], - path: "Sources" + path: "Sources", + swiftSettings: debugLocks ? [.define("DEBUG_LOCKS")] : [] ), .target( diff --git a/Sources/Core/Renderer/GUI/GUIElementMesh.swift b/Sources/Core/Renderer/GUI/GUIElementMesh.swift index 2ae95edd..8da64d8a 100644 --- a/Sources/Core/Renderer/GUI/GUIElementMesh.swift +++ b/Sources/Core/Renderer/GUI/GUIElementMesh.swift @@ -90,6 +90,21 @@ struct GUIElementMesh { ) } + init( + size: Vec2i, + color: Vec4f + ) { + self.init( + size: size, + arrayTexture: nil, + quads: [GUIQuad( + position: .zero, + size: Vec2f(size), + color: color + )] + ) + } + /// Renders the mesh. Expects ``GUIUniforms`` to be bound at vertex buffer index 1. Also expects /// pipeline state to be set to ``GUIRenderer/pipelineState``. mutating func render( diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 68f9f23d..070c970c 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -17,7 +17,7 @@ public final class GUIRenderer: Renderer { var profiler: Profiler var previousUniforms: GUIUniforms? - var gui: InGameGUI + var client: Client var fontArrayTexture: MTLTexture var guiTexturePalette: GUITexturePalette @@ -37,6 +37,7 @@ public final class GUIRenderer: Renderer { commandQueue: MTLCommandQueue, profiler: Profiler ) throws { + self.client = client self.device = device self.profiler = profiler @@ -96,8 +97,6 @@ public final class GUIRenderer: Renderer { fragmentFunction: try MetalUtil.loadFunction("guiFragment", from: library), blendingEnabled: true ) - - gui = InGameGUI() } public func render( @@ -123,14 +122,18 @@ public final class GUIRenderer: Renderer { profiler.pop() // Create meshes - let renderable = gui.body.resolveConstraints( - availableSize: Vec2i( - Int(width / scalingFactor), - Int(height / scalingFactor) - ), - font: font + let effectiveDrawableSize = Vec2i( + Int(width / scalingFactor), + Int(height / scalingFactor) ) + client.game.mutateGUIState { guiState in + guiState.drawableSize = effectiveDrawableSize + guiState.drawableScalingFactor = scalingFactor + } + + let renderable = client.game.compileGUI(withFont: font) + let meshes = try meshes(for: renderable) profiler.push(.encode) @@ -163,34 +166,40 @@ public final class GUIRenderer: Renderer { } func meshes(for renderable: GUIElement.GUIRenderable) throws -> [GUIElementMesh] { + var meshes: [GUIElementMesh] switch renderable.content { case let .text(wrappedLines, hangingIndent): let builder = TextMeshBuilder(font: font) - var meshes = try wrappedLines.compactMap { (line: String) in + meshes = try wrappedLines.compactMap { (line: String) in do { return try builder.build(line, fontArrayTexture: fontArrayTexture) } catch let error as LocalizedError { throw error .with("Text", line) - } catch { - throw error } } for i in meshes.indices where i != 0 { meshes[i].position.x += hangingIndent + meshes[i].position.y += Font.defaultCharacterHeight + 1 } - return meshes case let .sprite(descriptor): - return try [GUIElementMesh( + meshes = try [GUIElementMesh( sprite: descriptor, guiTexturePalette: guiTexturePalette, guiArrayTexture: guiArrayTexture )] - case nil, .clickable: - var meshes = try renderable.children.map(\.0).flatMap(meshes) - meshes.translate(amount: renderable.relativePosition) - return meshes + case nil, .clickable, .background: + if case let .background(color) = renderable.content { + meshes = [ + GUIElementMesh(size: renderable.size, color: color) + ] + } else { + meshes = [] + } + meshes += try renderable.children.flatMap(meshes(for:)) } + meshes.translate(amount: renderable.relativePosition) + return meshes } static func optimizeMeshes(_ meshes: [GUIElementMesh]) throws -> [GUIElementMesh] { diff --git a/Sources/Core/Sources/Client.swift b/Sources/Core/Sources/Client.swift index 6e63f17e..aaee7cae 100644 --- a/Sources/Core/Sources/Client.swift +++ b/Sources/Core/Sources/Client.swift @@ -31,7 +31,11 @@ public final class Client: @unchecked Sendable { public init(resourcePack: ResourcePack, configuration: ClientConfiguration) { self.resourcePack = resourcePack self.configuration = configuration - game = Game(eventBus: eventBus, configuration: configuration) + game = Game( + eventBus: eventBus, + configuration: configuration, + font: resourcePack.vanillaResources.fontPalette.defaultFont + ) } deinit { @@ -54,7 +58,13 @@ public final class Client: @unchecked Sendable { guard let self = self else { return } self.handlePacket(packet) } - game = Game(eventBus: eventBus, configuration: configuration, connection: connection) + game.stopTickScheduler() + game = Game( + eventBus: eventBus, + configuration: configuration, + connection: connection, + font: resourcePack.vanillaResources.fontPalette.defaultFont + ) hasFinishedDownloadingTerrain = false try connection.login(username: account.username) self.connection = connection diff --git a/Sources/Core/Sources/ECS/Singles/InputState.swift b/Sources/Core/Sources/ECS/Singles/InputState.swift index e3df3c22..4f45d677 100644 --- a/Sources/Core/Sources/ECS/Singles/InputState.swift +++ b/Sources/Core/Sources/ECS/Singles/InputState.swift @@ -62,8 +62,10 @@ public final class InputState: SingleComponent { newlyReleased.append(KeyReleaseEvent(key: key, input: input)) } - /// Releases all inputs. - public func releaseAll() { + /// Releases all inputs. Doesn't clear ``newlyPressed``, so if used when disabling + /// a certain set of (or all) inputs, wait to call this until after all relevant handlers + /// know to ignore said inputs. + public func releaseAll(clearNewlyPressed: Bool = true) { for key in keys { newlyReleased.append(KeyReleaseEvent(key: key, input: nil)) } @@ -71,8 +73,6 @@ public final class InputState: SingleComponent { for input in inputs { newlyReleased.append(KeyReleaseEvent(key: nil, input: input)) } - - newlyPressed = [] } /// Clears ``newlyPressed`` and ``newlyReleased``. @@ -120,7 +120,7 @@ public final class InputState: SingleComponent { /// Ticks the input state by flushing ``newlyPressed`` into ``keys`` and ``inputs``, and clearing /// ``newlyReleased``. Also emits events to the given ``EventBus``. func tick(_ isInputSuppressed: [Bool], _ eventBus: EventBus, _ configuration: ClientConfiguration) { - assert(isInputSuppressed.count == newlyPressed.count, "`isInputSuppressed` should be the same length as `newlyPressed`") + precondition(isInputSuppressed.count == newlyPressed.count, "`isInputSuppressed` should be the same length as `newlyPressed`") ticksSinceForwardsPressed += 1 ticksSinceJumpPressed += 1 diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index d29c5092..eec2e6dc 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -10,12 +10,21 @@ public final class PlayerInputSystem: System { weak var game: Game? var eventBus: EventBus let configuration: ClientConfiguration + let font: Font - public init(_ connection: ServerConnection?, _ game: Game, _ eventBus: EventBus, _ configuration: ClientConfiguration) { + // TODO: Font should be internal to the GUI (which should probably be stored in the nexus) + public init( + _ connection: ServerConnection?, + _ game: Game, + _ eventBus: EventBus, + _ configuration: ClientConfiguration, + _ font: Font + ) { self.connection = connection self.game = game self.eventBus = eventBus self.configuration = configuration + self.font = font } public func update(_ nexus: Nexus, _ world: World) throws { @@ -40,10 +49,19 @@ public final class PlayerInputSystem: System { let inputState = nexus.single(InputState.self).component let guiState = nexus.single(GUIStateStorage.self).component + let mousePosition = Vec2i(inputState.mousePosition) / 2 + // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) + let gui = game.compileGUI(withFont: font, guiState: guiState) + // Handle non-movement inputs var isInputSuppressed: [Bool] = [] for event in inputState.newlyPressed { - let suppressInput = try handleChat(event, inputState, guiState) || handleInventory(event, guiState) + var suppressInput = try handleChat(event, inputState, guiState) || handleInventory(event, guiState) + + // TODO: Formalize 'mouse interactions are allowed', seems a bit hacky this way + if event.key == .leftMouseButton && !guiState.movementAllowed { + suppressInput = gui.handleClick(at: mousePosition) + } if !suppressInput { switch event.input { @@ -192,7 +210,7 @@ public final class PlayerInputSystem: System { if let content = NSPasteboard.general.string(forType: .string) { newCharacters = Array(content) } - } else if message.utf8.count < GUIState.maximumMessageLength { + } else if message.utf8.count < InGameGUI.maximumMessageLength { newCharacters = event.characters } #else @@ -208,7 +226,7 @@ public final class PlayerInputSystem: System { // TODO: Make this check less restrictive, it's currently over-cautious continue } - guard character.utf8.count + message.utf8.count <= GUIState.maximumMessageLength else { + guard character.utf8.count + message.utf8.count <= InGameGUI.maximumMessageLength else { break } diff --git a/Sources/Core/Sources/GUI/GUIBuilder.swift b/Sources/Core/Sources/GUI/GUIBuilder.swift index 1028e24b..d0c95769 100644 --- a/Sources/Core/Sources/GUI/GUIBuilder.swift +++ b/Sources/Core/Sources/GUI/GUIBuilder.swift @@ -3,4 +3,20 @@ public struct GUIBuilder { public static func buildBlock(_ elements: GUIElement...) -> [GUIElement] { elements } + + public static func buildEither(first component: [GUIElement]) -> GUIElement { + .stack(elements: component) + } + + public static func buildEither(second component: [GUIElement]) -> GUIElement { + .stack(elements: component) + } + + public static func buildOptional(_ component: [GUIElement]?) -> GUIElement { + if let component = component { + .stack(elements: component) + } else { + .spacer(width: 0, height: 0) + } + } } diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index eb47d0cd..4089011a 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -9,7 +9,10 @@ public indirect enum GUIElement { /// Elements appear on top of the elements that come before them. case stack(elements: [GUIElement]) case positioned(element: GUIElement, constraints: Constraints) - case sized(element: GUIElement, size: Vec2i) + case sized(element: GUIElement, width: Int?, height: Int?) + case spacer(width: Int, height: Int) + /// Wraps an element with a background. + case container(background: Vec4f, padding: Int, element: GUIElement) public static let textWrapIndent: Int = 4 public static let lineSpacing: Int = 1 @@ -22,7 +25,7 @@ public indirect enum GUIElement { .stack(elements: elements()) } - public func centered() -> GUIElement { + public func center() -> GUIElement { .positioned(element: self, constraints: .center) } @@ -30,20 +33,86 @@ public indirect enum GUIElement { .positioned(element: self, constraints: .position(x, y)) } - public func sized(_ x: Int, _ y: Int) -> GUIElement { - .sized(element: self, size: Vec2i(x, y)) + public func constraints( + _ verticalConstraint: VerticalConstraint, + _ horizontalConstraint: HorizontalConstraint + ) -> GUIElement { + .positioned(element: self, constraints: Constraints(verticalConstraint, horizontalConstraint)) + } + + public func constraints(_ constraints: Constraints) -> GUIElement { + .positioned(element: self, constraints: constraints) + } + + /// `nil` indicates to use the natural width/height (the default). + public func size(_ width: Int?, _ height: Int?) -> GUIElement { + .sized(element: self, width: width, height: height) + } + + public func padding(_ padding: Int) -> GUIElement { + .container(background: .zero, padding: padding, element: self) + } + + public func background(_ color: Vec4f) -> GUIElement { + // Sometimes we can just update the element instead of adding another layer. + switch self { + case let .container(background, padding, element): + if background == .zero { + return .container(background: color, padding: padding, element: element) + } + default: + break + } + return .container(background: color, padding: 0, element: self) } public struct GUIRenderable { public var relativePosition: Vec2i public var size: Vec2i public var content: Content? - public var children: [(GUIRenderable, GUIElement)] + public var children: [GUIRenderable] public enum Content { case text(wrappedLines: [String], hangingIndent: Int) case clickable(action: () -> Void) case sprite(GUISpriteDescriptor) + /// Fills the renderable with the given background color. Goes behind + /// any children that the renderable may have. + case background(Vec4f) + } + + // Returns true if the click was handled by the renderable or any of its children. + public func handleClick(at position: Vec2i) -> Bool { + guard Self.isHit(position, inBoxAt: relativePosition, ofSize: size) else { + return false + } + + switch content { + case let .clickable(action): + action() + return true + case .text, .sprite, .background, nil: + break + } + + let relativeClickPosition = position &- relativePosition + for renderable in children.reversed() { + if renderable.handleClick(at: relativeClickPosition) { + return true + } + } + + return false + } + + private static func isHit( + _ position: Vec2i, + inBoxAt upperLeft: Vec2i, + ofSize size: Vec2i + ) -> Bool { + return + position.x > upperLeft.x && position.x < (upperLeft.x + size.x) + && position.y > upperLeft.y && position.y < (upperLeft.y + size.y) } } @@ -54,7 +123,7 @@ public indirect enum GUIElement { let relativePosition: Vec2i let size: Vec2i let content: GUIRenderable.Content? - let children: [(GUIRenderable, GUIElement)] + let children: [GUIRenderable] switch self { case let .text(text, wrap): // Wrap the lines, but if wrapping is disabled wrap to a width of Int.max (so that we can @@ -83,7 +152,7 @@ public indirect enum GUIElement { relativePosition = .zero size = child.size content = .clickable(action: action) - children = [(child, label)] + children = [child] case let .sprite(sprite): let descriptor = sprite.descriptor relativePosition = .zero @@ -109,24 +178,27 @@ public indirect enum GUIElement { childPosition.y += rowHeight availableSize.y -= rowHeight - return (renderable, element) + return renderable } relativePosition = .zero - let width = children.map(\.0.size.x).max() ?? 0 + let width = children.map(\.size.x).max() ?? 0 let height = elements.isEmpty ? 0 : childPosition.y - spacing size = Vec2i(width, height) content = nil case let .stack(elements): children = elements.map { element in - let renderable = element.resolveConstraints( + element.resolveConstraints( availableSize: availableSize, font: font ) - return (renderable, element) } size = Vec2i( - children.map(\.0.size.x).max() ?? 0, - children.map(\.0.size.y).max() ?? 0 + children.map { renderable in + renderable.size.x + renderable.relativePosition.x + }.max() ?? 0, + children.map { renderable in + renderable.size.y + renderable.relativePosition.y + }.max() ?? 0 ) relativePosition = .zero content = nil @@ -135,22 +207,51 @@ public indirect enum GUIElement { availableSize: availableSize, font: font ) - children = [(child, element)] + children = [child] relativePosition = constraints.solve( innerSize: child.size, outerSize: availableSize ) size = child.size content = nil - case let .sized(element, specifiedSize): + case let .sized(element, width, height): let child = element.resolveConstraints( - availableSize: specifiedSize, + availableSize: Vec2i( + width ?? availableSize.x, + height ?? availableSize.y + ), font: font ) - children = [(child, element)] + children = [child] + relativePosition = .zero + size = Vec2i( + width ?? child.size.x, + height ?? child.size.y + ) + content = nil + case let .spacer(width, height): + children = [] relativePosition = .zero - size = specifiedSize + size = Vec2i(width, height) content = nil + case let .container(background, padding, element): + var child = element.resolveConstraints( + availableSize: availableSize &- Vec2i(repeating: padding * 2), + font: font + ) + child.relativePosition &+= Vec2i(repeating: padding) + children = [child] + relativePosition = .zero + size = Vec2i( + min(availableSize.x, child.size.x + padding * 2), + min(availableSize.y, child.size.y + padding * 2) + ) + + if background.w != 0 { + content = .background(background) + } else { + content = nil + } } return GUIRenderable( @@ -195,8 +296,9 @@ public indirect enum GUIElement { latestSpace = i } - // Break before the current character if it'd bring the text over the maximum width - if nextWidth > maximumWidth { + // Break before the current character if it'd bring the text over the maximum width. + // If it's the first character, never wrap because otherwise we enter an infinite loop. + if nextWidth > maximumWidth && i != text.startIndex { if let spaceIndex = latestSpace { wrapIndex = spaceIndex } else { diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 6166a433..c1cc2203 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -1,17 +1,61 @@ -public struct InGameGUI { +import SwiftCPUDetect + +public class InGameGUI { + // TODO: Figure out why anything greater than 252 breaks the protocol. Anything less than 256 should work afaict + public static let maximumMessageLength = 252 + + /// The number of seconds until messages should be hidden from the regular GUI. + static let messageHideDelay: Double = 10 + /// The maximum number of messages displayed in the regular GUI. + static let maximumDisplayedMessages = 10 + /// The width of the chat history. + static let chatHistoryWidth = 330 + + /// The system's CPU display name. + static let cpuName = HWInfo.CPU.name() + /// The system's CPU architecture. + static let cpuArch = CpuArchitecture.current()?.rawValue + /// The system's total memory. + static let totalMem = (HWInfo.ramAmount() ?? 0) / (1024 * 1024 * 1024) + /// A string containing information about the system's default GPU. + static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() + + public var count: Int = 0 + public init() {} - public var body: GUIElement = GUIElement.stack { - GUIElement.list(spacing: 2) { - GUIElement.text("Hello, world!") + public func content(game: Game, state: GUIStateStorage) -> GUIElement { + GUIElement.stack { + if let messageInput = state.messageInput { + GUIElement.list(spacing: 2) { + GUIElement.list( + spacing: 2, + elements: state.chat.messages.map { _ in + GUIElement.text("message", wrap: true) + } + ) + .size(Self.chatHistoryWidth, nil) - GUIElement.clickable(.text("Press me")) { - print("Button pressed") + GUIElement.text(messageInput) + } + .constraints(.bottom(2), .left(2)) } - } - .centered() - GUIElement.text("Top left") - .positionInParent(0, 0) + GUIElement.list(spacing: 2) { + GUIElement.clickable(.text("Decrement")) { + self.count -= 1 + } + + GUIElement.clickable(.text("Increment")) { + self.count += 1 + } + } + .padding(2) + .background(Vec4f(1, 0, 1, 0.5)) + .center() + + GUIElement.text("Count: \(count)") + .positionInParent(0, 0) + } } } diff --git a/Sources/Core/Sources/GUIState.swift b/Sources/Core/Sources/GUIState.swift index 9f860a53..b10ae70f 100644 --- a/Sources/Core/Sources/GUIState.swift +++ b/Sources/Core/Sources/GUIState.swift @@ -1,20 +1,40 @@ public struct GUIState { - // TODO: Figure out why anything greater than 252 breaks the protocol. Anything less than 256 should work afaict - public static let maximumMessageLength = 252 - + /// Whether the HUD (health, hotbar, hunger, etc.) is visible or not. public var showHUD = true + /// Whether the debug screen is visible or not. public var showDebugScreen = false + /// Whether the inventory is open or not. public var showInventory = false + + /// The chat history including messages received from other players. public var chat = Chat() + /// The current contents of the chat message input. Non-`nil` if and only if chat is open. public var messageInput: String? + /// The message that the user was composing before they used the up arrow to replace it with + /// a historical message. Allows users to return to their message by hitting the down arrow a + /// sufficient number of times. public var stashedMessageInput: String? + /// All messages that the player has sent. public var playerMessageHistory: [String] = [] + /// The index of the currently selected historical message (changed by using the up + /// and down arrow keys while composing a chat message). Indexes into ``playerMessageHistory``. public var currentMessageIndex: Int? + /// A small default size would cause more text wrapping and slow down game + /// ticks (even if not rendering). Haven't measured, just a hunch. Renderers + /// must set this value to their drawable size to ensure that mouse interaction + /// functions correctly. + public var drawableSize = Vec2i(2000, 2000) + /// The scaling factor from true pixels to drawable pixels. Should be set by + /// renderers so that mouse coordinates can be correctly converted to drawable + /// coordinates by GUI code. + public var drawableScalingFactor: Float = 1 + /// The cursor position in the message input. 0 is the end of the message, /// and the maximum value is the beginning of the message. public var messageInputCursor: Int = 0 + /// The chat input field cursor as an index into ``messageInput``. public var messageInputCursorIndex: String.Index { if let messageInput = messageInput { return messageInput.index(messageInput.endIndex, offsetBy: -messageInputCursor) @@ -23,6 +43,7 @@ public struct GUIState { } } + /// Whether chat is open or not. public var showChat: Bool { return messageInput != nil } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 04895545..0432ede9 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -42,8 +42,13 @@ public final class Game: @unchecked Sendable { // MARK: Private properties + #if DEBUG_LOCKS + /// A locked for managing safe access of ``nexus``. + public let nexusLock = ReadWriteLock() + #else /// A locked for managing safe access of ``nexus``. private let nexusLock = ReadWriteLock() + #endif /// The container for the game's entities. Strictly only contains what Minecraft counts as /// entities. Doesn't include block entities. private let nexus = Nexus() @@ -51,13 +56,19 @@ public final class Game: @unchecked Sendable { private var player: Player /// The current input state (keyboard and mouse). private let inputState: InputState + + /// A lock for managing safe access of ``gui``. + private let guiLock = ReadWriteLock() /// The current GUI state (f3 screen, inventory, etc). + private let gui: InGameGUI + /// Storage for the current GUI state. Protected by ``nexusLock`` since it's stored in the + /// nexus. private let _guiState: GUIStateStorage // MARK: Init /// Creates a game with default properties. Creates the player. Starts the tick loop. - public init(eventBus: EventBus, configuration: ClientConfiguration, connection: ServerConnection? = nil) { + public init(eventBus: EventBus, configuration: ClientConfiguration, connection: ServerConnection? = nil, font: Font) { self.eventBus = eventBus world = World(eventBus: eventBus) @@ -65,6 +76,7 @@ public final class Game: @unchecked Sendable { tickScheduler = TickScheduler(nexus, nexusLock: nexusLock, world) inputState = nexus.single(InputState.self).component + gui = InGameGUI() _guiState = nexus.single(GUIStateStorage.self).component player = Player() @@ -78,7 +90,11 @@ public final class Game: @unchecked Sendable { tickScheduler.addSystem(PlayerClimbSystem()) tickScheduler.addSystem(PlayerGravitySystem()) tickScheduler.addSystem(PlayerSmoothingSystem()) - tickScheduler.addSystem(PlayerInputSystem(connection, self, eventBus, configuration)) + // TODO: Make sure that font gets updated when resource pack gets updated, will likely + // require significant refactoring if we wanna do it right (as in not just hacking it + // together for the specific case of PlayerInputSystem); proper resource pack propagation + // will probably take quite a bit of work. + tickScheduler.addSystem(PlayerInputSystem(connection, self, eventBus, configuration, font)) tickScheduler.addSystem(PlayerFlightSystem()) tickScheduler.addSystem(PlayerAccelerationSystem()) tickScheduler.addSystem(PlayerJumpSystem()) @@ -179,13 +195,38 @@ public final class Game: @unchecked Sendable { return _guiState.inner } - /// Mutates the GUI state using a provided action. - /// - acquireLock: If `false`, a nexus lock will not be acquired. Use with caution. - /// - action: Action to run on GUI state. - public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) -> R) -> R { + /// Handles a received chat message. + public func receiveChatMessage(acquireLock: Bool = true, _ message: ChatMessage) { + if acquireLock { nexusLock.acquireWriteLock() } + defer { if acquireLock { nexusLock.unlock() } } + _guiState.chat.add(message) + } + + public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) throws -> R) rethrows -> R { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } - return action(&_guiState.inner) + return try action(&_guiState.inner) + } + + /// Compile the in-game GUI to a renderable. + /// - acquireGUILock: If `false`, a GUI lock will not be acquired. Use with caution. + /// - acquireNexusLock: If `false`, a GUI lock will not be acquired (otherwise a nexus lock will be + /// acquired if guiState isn't supplied). Use with caution. + /// - guiState: Avoids the need for this function to call out to the nexus redundantly if the caller already + /// has a reference to the gui state. + public func compileGUI(acquireGUILock: Bool = true, acquireNexusLock: Bool = true, withFont font: Font, guiState: GUIStateStorage? = nil) -> GUIElement.GUIRenderable { + if acquireGUILock { guiLock.acquireWriteLock() } + defer { if acquireGUILock { guiLock.unlock() } } + var state: GUIStateStorage + if let guiState = guiState { + state = guiState + } else { + if acquireNexusLock { nexusLock.acquireWriteLock() } + state = nexus.single(GUIStateStorage.self).component + } + defer { if acquireNexusLock && guiState == nil { nexusLock.unlock() } } + return gui.content(game: self, state: state) + .resolveConstraints(availableSize: state.drawableSize, font: font) } // MARK: Entity @@ -340,7 +381,7 @@ public final class Game: @unchecked Sendable { return nil } - /// Gets current gamemode of the player + /// Gets current gamemode of the player. public func currentGamemode() -> Gamemode? { var gamemode: Gamemode? = nil accessPlayer { player in @@ -374,4 +415,9 @@ public final class Game: @unchecked Sendable { self.world = newWorld tickScheduler.setWorld(to: newWorld) } + + /// Stops the tick scheduler. + public func stopTickScheduler() { + tickScheduler.cancel() + } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChatMessageClientboundPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChatMessageClientboundPacket.swift index 77d82cd5..3f86d86d 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChatMessageClientboundPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChatMessageClientboundPacket.swift @@ -17,9 +17,7 @@ public struct ChatMessageClientboundPacket: ClientboundEntityPacket { let locale = client.resourcePack.getDefaultLocale() let message = ChatMessage(content: content, sender: sender) - client.game.mutateGUIState(acquireLock: false) { guiState in - guiState.chat.add(message) - } + client.game.receiveChatMessage(acquireLock: false, message) client.eventBus.dispatch(ChatMessageReceivedEvent(message: message)) diff --git a/Sources/Core/Sources/Util/Box.swift b/Sources/Core/Sources/Util/Box.swift index 681ca529..c0899f49 100644 --- a/Sources/Core/Sources/Util/Box.swift +++ b/Sources/Core/Sources/Util/Box.swift @@ -1,3 +1,5 @@ +import FirebladeECS + public class Box { public var value: T diff --git a/Sources/Core/Sources/Util/ReadWriteLock.swift b/Sources/Core/Sources/Util/ReadWriteLock.swift index fd3d10fd..7a3f54f8 100644 --- a/Sources/Core/Sources/Util/ReadWriteLock.swift +++ b/Sources/Core/Sources/Util/ReadWriteLock.swift @@ -1,3 +1,5 @@ +import Atomics + #if canImport(Darwin) import Darwin #elseif canImport(Glibc) @@ -6,15 +8,27 @@ import Glibc #error("Unsupported platform for ReadWriteLock") #endif +// TODO: Figure out why removing the `guiState` parameter to `compileGUI` in `PlayerInputSystem` doesn't +// cause a deadlock every tick (it causes crashes but only in specific conditions). It should cause a +// deadlock no matter what cause not passing the `guiState` parameter causes `compileGUI` to acquire a +// nexus lock. After some debugging it seemed like somehow the lock was successfully getting to a lockCount +// of 2 even though the base nexus lock held by tick scheduler each tick is a write lock (and so is the +// lock acquired by compileGUI). That just seems like it shouldn't be possible at all??? + /// A wrapper around the rwlock C api (`pthread_rwlock_t`). /// /// Add the `DEBUG_LOCKS` custom flag to enable the `lastLockedBy` property which keeps track of the -/// latest code to acquire the lock. This is very useful for debugging deadlocks. +/// latest code to acquire the lock, and the `lockCount`/`lastFullyReleasedBy` properties which +/// track the total number of locks held on the lock and the code which last unlocked the lock +/// and brought it to a `lockCount` of 0. These are very useful for debugging deadlocks and double unlocks. public final class ReadWriteLock { private var lock = pthread_rwlock_t() + #if DEBUG_LOCKS public var lastLockedBy: String? + public var lastFullyReleasedBy: String? + public var lockCount = ManagedAtomic(0) private var stringLock = pthread_rwlock_t() #endif @@ -38,6 +52,7 @@ public final class ReadWriteLock { @inline(never) public func acquireReadLock(file: String = #file, line: Int = #line, column: Int = #column) { pthread_rwlock_rdlock(&lock) + lockCount.wrappingIncrement(ordering: .relaxed) pthread_rwlock_wrlock(&stringLock) lastLockedBy = "\(file):\(line):\(column)" @@ -55,6 +70,7 @@ public final class ReadWriteLock { @inline(never) public func acquireWriteLock(file: String = #file, line: Int = #line, column: Int = #column) { pthread_rwlock_wrlock(&lock) + lockCount.wrappingIncrement(ordering: .relaxed) pthread_rwlock_wrlock(&stringLock) lastLockedBy = "\(file):\(line):\(column)" @@ -67,8 +83,24 @@ public final class ReadWriteLock { } #endif + #if DEBUG_LOCKS + /// Unlock the lock. + public func unlock(file: String = #file, line: Int = #line, column: Int = #column) { + let count = lockCount.wrappingDecrementThenLoad(ordering: .relaxed) + + pthread_rwlock_wrlock(&stringLock) + precondition(count >= 0, "Detected unbalanced unlock of ReadWriteLock, unlocked by: \(file):\(line):\(column), last unlocked by: \(lastFullyReleasedBy ?? "no one")") + if count == 0 { + lastFullyReleasedBy = "\(file):\(line):\(column)" + } + pthread_rwlock_unlock(&stringLock) + + pthread_rwlock_unlock(&lock) + } + #else /// Unlock the lock. public func unlock() { pthread_rwlock_unlock(&lock) } + #endif } diff --git a/Sources/Core/Sources/Util/TickScheduler.swift b/Sources/Core/Sources/Util/TickScheduler.swift index 36ac5639..b112d2b3 100644 --- a/Sources/Core/Sources/Util/TickScheduler.swift +++ b/Sources/Core/Sources/Util/TickScheduler.swift @@ -153,7 +153,6 @@ public final class TickScheduler: @unchecked Sendable { // The nexus lock must be held for the duration of the tick because it is used elsewhere as a // lock to prevent thread safety issue related to modifying dependencies of the tick method. nexusLock.acquireWriteLock() - defer { nexusLock.unlock() } worldLock.acquireReadLock() let world = world @@ -169,6 +168,7 @@ public final class TickScheduler: @unchecked Sendable { } tickNumber += 1 + nexusLock.unlock() } #if canImport(Darwin) From 12e0fd85bb6a6e85a3b27dd770351cf1e301a861 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 26 May 2024 12:59:14 +1000 Subject: [PATCH 22/84] Update mouse interaction handling to use correct scaling factor instead of the previously hardcoded scaling factor (whoops) --- Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index eec2e6dc..27c8113c 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -49,7 +49,7 @@ public final class PlayerInputSystem: System { let inputState = nexus.single(InputState.self).component let guiState = nexus.single(GUIStateStorage.self).component - let mousePosition = Vec2i(inputState.mousePosition) / 2 + let mousePosition = Vec2i(inputState.mousePosition / guiState.drawableScalingFactor) // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) let gui = game.compileGUI(withFont: font, guiState: guiState) From 0edb9a242c1e0544066a6ebcb6bc166366b2be19 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 26 May 2024 13:31:35 +1000 Subject: [PATCH 23/84] Fix deadlock in compileGUI (bad lock nesting order) and implement horizontal lists for GUI Also made a more convenient 'onClick' element method to make things easier to compose without nesting --- Sources/Core/Sources/GUI/GUIElement.swift | 43 ++++++++++++++++------- Sources/Core/Sources/GUI/InGameGUI.swift | 26 ++++++++------ Sources/Core/Sources/Game.swift | 9 +++-- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 4089011a..af54f03e 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -1,10 +1,15 @@ public indirect enum GUIElement { + public enum Direction { + case vertical + case horizontal + } + case text(_ content: String, wrap: Bool = false) case clickable(_ element: GUIElement, action: () -> Void) case sprite(GUISprite) case customSprite(GUISpriteDescriptor) - /// Stacks elements in the y direction. Aligns elements to the top left by default. - case list(spacing: Int, elements: [GUIElement]) + /// Stacks elements in the specified direction. Aligns elements to the top left by default. + case list(direction: Direction = .vertical, spacing: Int, elements: [GUIElement]) /// Stacks elements in the z direction. Non-positioned elements default to the top-left corner. /// Elements appear on top of the elements that come before them. case stack(elements: [GUIElement]) @@ -17,8 +22,11 @@ public indirect enum GUIElement { public static let textWrapIndent: Int = 4 public static let lineSpacing: Int = 1 - public static func list(spacing: Int, @GUIBuilder elements: () -> [GUIElement]) -> GUIElement { - .list(spacing: spacing, elements: elements()) + public static func list( + direction: Direction = .vertical, + spacing: Int, @GUIBuilder elements: () -> [GUIElement] + ) -> GUIElement { + .list(direction: direction, spacing: spacing, elements: elements()) } public static func stack(@GUIBuilder elements: () -> [GUIElement]) -> GUIElement { @@ -66,6 +74,10 @@ public indirect enum GUIElement { return .container(background: color, padding: 0, element: self) } + public func onClick(_ action: @escaping () -> Void) -> GUIElement { + .clickable(self, action: action) + } + public struct GUIRenderable { public var relativePosition: Vec2i public var size: Vec2i @@ -164,26 +176,33 @@ public indirect enum GUIElement { size = descriptor.size content = .sprite(descriptor) children = [] - case let .list(spacing, elements): + case let .list(direction, spacing, elements): var availableSize = availableSize var childPosition = Vec2i(0, 0) + let axisComponent = direction == .vertical ? 1 : 0 children = elements.map { element in var renderable = element.resolveConstraints( availableSize: availableSize, font: font ) - renderable.relativePosition.y += childPosition.y + renderable.relativePosition[axisComponent] += childPosition[axisComponent] - let rowHeight = renderable.size.y + spacing - childPosition.y += rowHeight - availableSize.y -= rowHeight + let rowSize = renderable.size[axisComponent] + spacing + childPosition[axisComponent] += rowSize + availableSize[axisComponent] -= rowSize return renderable } relativePosition = .zero - let width = children.map(\.size.x).max() ?? 0 - let height = elements.isEmpty ? 0 : childPosition.y - spacing - size = Vec2i(width, height) + let lengthAlongAxis = elements.isEmpty ? 0 : childPosition[axisComponent] - spacing + switch direction { + case .vertical: + let width = children.map(\.size.x).max() ?? 0 + size = Vec2i(width, lengthAlongAxis) + case .horizontal: + let height = children.map(\.size.y).max() ?? 0 + size = Vec2i(lengthAlongAxis, height) + } content = nil case let .stack(elements): children = elements.map { element in diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index c1cc2203..d3188deb 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -41,18 +41,24 @@ public class InGameGUI { .constraints(.bottom(2), .left(2)) } - GUIElement.list(spacing: 2) { - GUIElement.clickable(.text("Decrement")) { - self.count -= 1 - } + GUIElement.list(direction: .horizontal, spacing: 2) { + GUIElement.text("Decrement") + .padding(10) + .background(Vec4f(1, 0, 1, 0.5)) + .onClick { + self.count -= 1 + } - GUIElement.clickable(.text("Increment")) { - self.count += 1 - } + GUIElement.spacer(width: 10, height: 0) + + GUIElement.text("Increment") + .padding(10) + .background(Vec4f(1, 0, 1, 0.5)) + .onClick { + self.count += 1 + } } - .padding(2) - .background(Vec4f(1, 0, 1, 0.5)) - .center() + .constraints(.bottom(10), .center) GUIElement.text("Count: \(count)") .positionInParent(0, 0) diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 0432ede9..1a00e742 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -215,8 +215,11 @@ public final class Game: @unchecked Sendable { /// - guiState: Avoids the need for this function to call out to the nexus redundantly if the caller already /// has a reference to the gui state. public func compileGUI(acquireGUILock: Bool = true, acquireNexusLock: Bool = true, withFont font: Font, guiState: GUIStateStorage? = nil) -> GUIElement.GUIRenderable { - if acquireGUILock { guiLock.acquireWriteLock() } - defer { if acquireGUILock { guiLock.unlock() } } + // Acquire the nexus lock first as that's the one that threads can be sitting inside of with `Game.accessNexus`. + // If we get the GUI lock first then the renderer can be waiting for the nexus lock while PlayerInputSystem is + // sitting with a nexus lock and waiting for a gui lock. + // TODO: Formalize the idea of keeping a consistent 'topological' ordering for locks throughout the project. + // I think that would prevent this class of deadlocks. var state: GUIStateStorage if let guiState = guiState { state = guiState @@ -224,6 +227,8 @@ public final class Game: @unchecked Sendable { if acquireNexusLock { nexusLock.acquireWriteLock() } state = nexus.single(GUIStateStorage.self).component } + if acquireGUILock { guiLock.acquireWriteLock() } + defer { if acquireGUILock { guiLock.unlock() } } defer { if acquireNexusLock && guiState == nil { nexusLock.unlock() } } return gui.content(game: self, state: state) .resolveConstraints(availableSize: state.drawableSize, font: font) From d42a307c3994678748e819e1a222f168ebf0d229 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 26 May 2024 18:15:56 +1000 Subject: [PATCH 24/84] Reimplement hotbar area rendering in new GUI system (excluding item rendering) --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 8 +- Sources/Core/Sources/GUI/GUIBuilder.swift | 16 +- Sources/Core/Sources/GUI/GUIElement.swift | 56 +++++- Sources/Core/Sources/GUI/InGameGUI.swift | 182 ++++++++++++++++---- 4 files changed, 210 insertions(+), 52 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 070c970c..de3efdd6 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -168,11 +168,15 @@ public final class GUIRenderer: Renderer { func meshes(for renderable: GUIElement.GUIRenderable) throws -> [GUIElementMesh] { var meshes: [GUIElementMesh] switch renderable.content { - case let .text(wrappedLines, hangingIndent): + case let .text(wrappedLines, hangingIndent, color): let builder = TextMeshBuilder(font: font) meshes = try wrappedLines.compactMap { (line: String) in do { - return try builder.build(line, fontArrayTexture: fontArrayTexture) + return try builder.build( + line, + fontArrayTexture: fontArrayTexture, + color: color + ) } catch let error as LocalizedError { throw error .with("Text", line) diff --git a/Sources/Core/Sources/GUI/GUIBuilder.swift b/Sources/Core/Sources/GUI/GUIBuilder.swift index d0c95769..2aadd5ea 100644 --- a/Sources/Core/Sources/GUI/GUIBuilder.swift +++ b/Sources/Core/Sources/GUI/GUIBuilder.swift @@ -1,20 +1,20 @@ @resultBuilder public struct GUIBuilder { - public static func buildBlock(_ elements: GUIElement...) -> [GUIElement] { - elements + public static func buildBlock(_ elements: GUIElement...) -> GUIElement { + .list(spacing: 0, elements: elements) } - public static func buildEither(first component: [GUIElement]) -> GUIElement { - .stack(elements: component) + public static func buildEither(first component: GUIElement) -> GUIElement { + component } - public static func buildEither(second component: [GUIElement]) -> GUIElement { - .stack(elements: component) + public static func buildEither(second component: GUIElement) -> GUIElement { + component } - public static func buildOptional(_ component: [GUIElement]?) -> GUIElement { + public static func buildOptional(_ component: GUIElement?) -> GUIElement { if let component = component { - .stack(elements: component) + component } else { .spacer(width: 0, height: 0) } diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index af54f03e..2b4a8726 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -4,7 +4,7 @@ public indirect enum GUIElement { case horizontal } - case text(_ content: String, wrap: Bool = false) + case text(_ content: String, wrap: Bool = false, color: Vec4f = Vec4f(1, 1, 1, 1)) case clickable(_ element: GUIElement, action: () -> Void) case sprite(GUISprite) case customSprite(GUISpriteDescriptor) @@ -18,19 +18,42 @@ public indirect enum GUIElement { case spacer(width: Int, height: Int) /// Wraps an element with a background. case container(background: Vec4f, padding: Int, element: GUIElement) + case floating(element: GUIElement) + + public var children: [GUIElement] { + switch self { + case let .list(_, _, elements), let .stack(elements): + return elements + case let .clickable(element, _), let .positioned(element, _), let .sized(element, _, _), let .container(_, _, element), let .floating(element): + return [element] + case .text, .sprite, .customSprite, .spacer: + return [] + } + } public static let textWrapIndent: Int = 4 public static let lineSpacing: Int = 1 public static func list( direction: Direction = .vertical, - spacing: Int, @GUIBuilder elements: () -> [GUIElement] + spacing: Int, + @GUIBuilder elements: () -> GUIElement ) -> GUIElement { - .list(direction: direction, spacing: spacing, elements: elements()) + .list(direction: direction, spacing: spacing, elements: elements().children) } - public static func stack(@GUIBuilder elements: () -> [GUIElement]) -> GUIElement { - .stack(elements: elements()) + public static func forEach( + in values: S, + direction: Direction = .vertical, + spacing: Int, + @GUIBuilder element: (S.Element) -> GUIElement + ) -> GUIElement { + let elements = values.map(element) + return .list(direction: direction, spacing: spacing, elements: elements) + } + + public static func stack(@GUIBuilder elements: () -> GUIElement) -> GUIElement { + .stack(elements: elements().children) } public func center() -> GUIElement { @@ -78,6 +101,10 @@ public indirect enum GUIElement { .clickable(self, action: action) } + public func float() -> GUIElement { + .floating(element: self) + } + public struct GUIRenderable { public var relativePosition: Vec2i public var size: Vec2i @@ -85,7 +112,7 @@ public indirect enum GUIElement { public var children: [GUIRenderable] public enum Content { - case text(wrappedLines: [String], hangingIndent: Int) + case text(wrappedLines: [String], hangingIndent: Int, color: Vec4f) case clickable(action: () -> Void) case sprite(GUISpriteDescriptor) /// Fills the renderable with the given background color. Goes behind @@ -137,7 +164,7 @@ public indirect enum GUIElement { let content: GUIRenderable.Content? let children: [GUIRenderable] switch self { - case let .text(text, wrap): + case let .text(text, wrap, color): // Wrap the lines, but if wrapping is disabled wrap to a width of Int.max (so that we can // still compute the width of the line). let lines = Self.wrap( @@ -153,7 +180,8 @@ public indirect enum GUIElement { ) content = .text( wrappedLines: lines.map(\.line), - hangingIndent: Self.textWrapIndent + hangingIndent: Self.textWrapIndent, + color: color ) children = [] case let .clickable(label, action): @@ -271,6 +299,18 @@ public indirect enum GUIElement { } else { content = nil } + case let .floating(element): + let child = element.resolveConstraints( + availableSize: Vec2i( + .max, + .max + ), + font: font + ) + children = [child] + relativePosition = .zero + size = .zero + content = nil } return GUIRenderable( diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index d3188deb..d93ee310 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -20,48 +20,162 @@ public class InGameGUI { /// A string containing information about the system's default GPU. static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() - public var count: Int = 0 + static let xpLevelTextColor = Vec4f(126, 252, 31, 255) / 255 + static let xpLevelTextOutlineColor = [0, 0, 0, 1] public init() {} public func content(game: Game, state: GUIStateStorage) -> GUIElement { - GUIElement.stack { - if let messageInput = state.messageInput { - GUIElement.list(spacing: 2) { - GUIElement.list( - spacing: 2, - elements: state.chat.messages.map { _ in - GUIElement.text("message", wrap: true) - } - ) - .size(Self.chatHistoryWidth, nil) - - GUIElement.text(messageInput) - } - .constraints(.bottom(2), .left(2)) + let gamemode = game.accessPlayer(acquireLock: false) { player in + player.gamemode.gamemode + } + + return GUIElement.stack { + GUIElement.sprite(.crossHair) + .center() + + if gamemode != .spectator { + hotbarArea(game: game, gamemode: gamemode) } + } + } + + /// The hotbar (and nearby stats if in a gamemode with health). + public func hotbarArea(game: Game, gamemode: Gamemode) -> GUIElement { + var health: Float = 0 + var food: Int = 0 + var selectedSlot: Int = 0 + var xpBarProgress: Float = 0 + var xpLevel: Int = 0 + var hotbarSlots: [Slot] = [] + game.accessPlayer(acquireLock: false) { player in + health = player.health.health + food = player.nutrition.food + selectedSlot = player.inventory.selectedHotbarSlot + xpBarProgress = player.experience.experienceBarProgress + xpLevel = player.experience.experienceLevel + hotbarSlots = player.inventory.hotbar + } - GUIElement.list(direction: .horizontal, spacing: 2) { - GUIElement.text("Decrement") - .padding(10) - .background(Vec4f(1, 0, 1, 0.5)) - .onClick { - self.count -= 1 - } - - GUIElement.spacer(width: 10, height: 0) - - GUIElement.text("Increment") - .padding(10) - .background(Vec4f(1, 0, 1, 0.5)) - .onClick { - self.count += 1 - } + return GUIElement.list(spacing: 0) { + if gamemode.hasHealth { + stats(health: health, food: food, xpBarProgress: xpBarProgress, xpLevel: xpLevel) } - .constraints(.bottom(10), .center) - GUIElement.text("Count: \(count)") - .positionInParent(0, 0) + hotbar(slots: hotbarSlots, selectedSlot: selectedSlot) + } + .size(GUISprite.hotbar.descriptor.size.x + 2, nil) + .constraints(.bottom(-1), .center) + } + + public func hotbar(slots: [Slot], selectedSlot: Int) -> GUIElement { + return GUIElement.stack { + GUIElement.sprite(.hotbar) + .padding(1) + GUIElement.sprite(.selectedHotbarSlot) + .positionInParent(selectedSlot * 20, 0) + } + } + + public enum ReadingDirection { + case leftToRight + case rightToLeft + } + + public func stats( + health: Float, + food: Int, + xpBarProgress: Float, + xpLevel: Int + ) -> GUIElement { + GUIElement.list(spacing: 0) { + GUIElement.stack { + discreteMeter( + Int(health.rounded()), + fullIcon: .fullHeart, + halfIcon: .halfHeart, + outlineIcon: .heartOutline + ) + + discreteMeter( + food, + fullIcon: .fullFood, + halfIcon: .halfFood, + outlineIcon: .foodOutline, + direction: .rightToLeft + ) + .constraints(.top(0), .right(0)) + } + .size(GUISprite.hotbar.descriptor.size.x, nil) + .constraints(.top(0), .center) + + GUIElement.stack { + continuousMeter( + xpBarProgress, + background: .xpBarBackground, + foreground: .xpBarForeground + ) + .constraints(.top(0), .center) + + outlinedText("\(xpLevel)", textColor: Self.xpLevelTextColor) + .constraints(.top(-7), .center) + } + .padding(1) + .constraints(.top(0), .center) + } + } + + public func outlinedText( + _ text: String, + textColor: Vec4f, + outlineColor: Vec4f = Vec4f(0, 0, 0, 1) + ) -> GUIElement { + let outlineText = GUIElement.text(text, color: outlineColor) + return GUIElement.stack { + outlineText.constraints(.top(0), .left(1)) + outlineText.constraints(.top(1), .left(0)) + outlineText.constraints(.top(1), .left(2)) + outlineText.constraints(.top(2), .left(1)) + GUIElement.text(text, color: textColor) + .constraints(.top(1), .left(1)) + } + } + + public func discreteMeter( + _ value: Int, + fullIcon: GUISprite, + halfIcon: GUISprite, + outlineIcon: GUISprite, + direction: ReadingDirection = .leftToRight + ) -> GUIElement { + let fullIconCount = value / 2 + let hasHalfIcon = value % 2 == 0 + var range = Array(0..<10) + if direction == .rightToLeft { + range = range.reversed() + } + return GUIElement.forEach(in: range, direction: .horizontal, spacing: -1) { i in + GUIElement.stack { + GUIElement.sprite(outlineIcon) + if i < fullIconCount { + GUIElement.sprite(fullIcon) + } else if hasHalfIcon && i == fullIconCount { + GUIElement.sprite(halfIcon) + } + } + } + } + + public func continuousMeter( + _ value: Float, + background: GUISprite, + foreground:GUISprite + ) -> GUIElement { + var croppedForeground = foreground.descriptor + croppedForeground.size.x = Int(Float(croppedForeground.size.x) * value) + return GUIElement.stack { + GUIElement.sprite(background) + GUIElement.customSprite(croppedForeground) } } } From b3966133dbf0a2e6ec8740344846af626cbde380 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 26 May 2024 19:03:36 +1000 Subject: [PATCH 25/84] Add support for item models to the new GUI system and render hot bar items --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 75 +++++++++++++++++++++ Sources/Core/Sources/GUI/GUIElement.swift | 25 +++++-- Sources/Core/Sources/GUI/InGameGUI.swift | 36 +++++++++- 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index de3efdd6..652875b6 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -192,6 +192,8 @@ public final class GUIRenderer: Renderer { guiTexturePalette: guiTexturePalette, guiArrayTexture: guiArrayTexture )] + case let .item(itemId): + meshes = try self.meshes(forItemWithId: itemId) case nil, .clickable, .background: if case let .background(color) = renderable.content { meshes = [ @@ -206,6 +208,79 @@ public final class GUIRenderer: Renderer { return meshes } + func meshes(forItemWithId itemId: Int) throws -> [GUIElementMesh] { + guard let model = itemModelPalette.model(for: itemId) else { + throw GUIRendererError.invalidItemId(itemId) + } + + switch model { + case let .layered(textures, _): + return textures.map { texture in + switch texture { + case let .block(index): + return GUIElementMesh(slice: index, texture: blockArrayTexture) + case let .item(index): + return GUIElementMesh(slice: index, texture: itemArrayTexture) + } + } + case let .blockModel(modelId): + guard let model = blockModelPalette.model(for: modelId, at: nil) else { + log.warning("Missing block model of id \(modelId) (for item)") + return [] + } + + // Get the block's transformation assuming that each block model part has the same + // associated gui transformation (I don't see why this wouldn't always be true). + var transformation: Mat4x4f + if let transformsIndex = model.parts.first?.displayTransformsIndex { + transformation = blockModelPalette.displayTransforms[transformsIndex].gui + } else { + transformation = MatrixUtil.identity + } + + transformation *= MatrixUtil.translationMatrix([-0.5, -0.5, -0.5]) + * MatrixUtil.rotationMatrix(x: .pi) + * MatrixUtil.rotationMatrix(y: -.pi / 4) + * MatrixUtil.rotationMatrix(x: -.pi / 6) + + var geometry = Geometry() + var translucentGeometry = SortableMeshElement() + BlockMeshBuilder( + model: model, + position: BlockPosition(x: 0, y: 0, z: 0), + modelToWorld: transformation * MatrixUtil.scalingMatrix(9.76), + culledFaces: [], + lightLevel: LightLevel(sky: 15, block: 15), + neighbourLightLevels: [:], + tintColor: [1, 1, 1], + blockTexturePalette: blockTexturePalette + ).build(into: &geometry, translucentGeometry: &translucentGeometry) + + var vertices: [GUIVertex] = [] + vertices.reserveCapacity(geometry.vertices.count) + for vertex in geometry.vertices { + vertices.append(GUIVertex( + position: [vertex.x, vertex.y], + uv: [vertex.u, vertex.v], + tint: [vertex.r, vertex.g, vertex.b, 1], + textureIndex: vertex.textureIndex + )) + } + + // TODO: Handle translucent block items + + var mesh = GUIElementMesh( + size: [16, 16], + arrayTexture: blockArrayTexture, + vertices: .flatArray(vertices) + ) + mesh.position = [8, 8] + return [mesh] + case .empty, .entity: + return [] + } + } + static func optimizeMeshes(_ meshes: [GUIElementMesh]) throws -> [GUIElementMesh] { var textureToIndex: [String: Int] = [:] var boxes: [[(position: Vec2i, size: Vec2i)]] = [] diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 2b4a8726..2640158a 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -19,6 +19,7 @@ public indirect enum GUIElement { /// Wraps an element with a background. case container(background: Vec4f, padding: Int, element: GUIElement) case floating(element: GUIElement) + case item(id: Int) public var children: [GUIElement] { switch self { @@ -26,7 +27,7 @@ public indirect enum GUIElement { return elements case let .clickable(element, _), let .positioned(element, _), let .sized(element, _, _), let .container(_, _, element), let .floating(element): return [element] - case .text, .sprite, .customSprite, .spacer: + case .text, .sprite, .customSprite, .spacer, .item: return [] } } @@ -101,6 +102,15 @@ public indirect enum GUIElement { .clickable(self, action: action) } + /// Sets an element's apparent size to zero so that it doesn't partake in layout. + /// It will still get put exactly where it otherwise would, but for example if the + /// element is in a list, all following elements will be positioned as if the element + /// doesn't exist (except double spacing where the element would've been). + /// + /// Any constraints placed on an element after it has been floated will act as if the + /// element is of zero size. This probably isn't the best and could be fixed without + /// too much effort (by making element rendering logic understand floating instead of + /// just pretending the size is zero). public func float() -> GUIElement { .floating(element: self) } @@ -118,6 +128,7 @@ public indirect enum GUIElement { /// Fills the renderable with the given background color. Goes behind /// any children that the renderable may have. case background(Vec4f) + case item(id: Int) } // Returns true if the click was handled by the renderable or any of its children. @@ -130,7 +141,7 @@ public indirect enum GUIElement { case let .clickable(action): action() return true - case .text, .sprite, .background, nil: + case .text, .sprite, .background, .item, nil: break } @@ -301,16 +312,18 @@ public indirect enum GUIElement { } case let .floating(element): let child = element.resolveConstraints( - availableSize: Vec2i( - .max, - .max - ), + availableSize: availableSize, font: font ) children = [child] relativePosition = .zero size = .zero content = nil + case let .item(id): + children = [] + relativePosition = .zero + size = Vec2i(16, 16) + content = .item(id: id) } return GUIRenderable( diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index d93ee310..f0bac20d 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -21,7 +21,6 @@ public class InGameGUI { static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() static let xpLevelTextColor = Vec4f(126, 252, 31, 255) / 255 - static let xpLevelTextOutlineColor = [0, 0, 0, 1] public init() {} @@ -69,11 +68,44 @@ public class InGameGUI { } public func hotbar(slots: [Slot], selectedSlot: Int) -> GUIElement { - return GUIElement.stack { + GUIElement.stack { GUIElement.sprite(.hotbar) .padding(1) GUIElement.sprite(.selectedHotbarSlot) .positionInParent(selectedSlot * 20, 0) + + GUIElement.forEach(in: slots, direction: .horizontal, spacing: 4) { slot in + inventorySlot(slot) + } + .positionInParent(4, 4) + } + } + + public func inventorySlot(_ slot: Slot) -> GUIElement { + // TODO: Make if blocks layout transparent (their children should be treated as children of the parent block) + if let stack = slot.stack { + return GUIElement.stack { + GUIElement.item(id: stack.itemId) + + textWithShadow("\(stack.count)", shadowColor: Vec4f(62, 62, 62, 255) / 255) + .constraints(.bottom(-2), .right(-1)) + .float() + } + .size(16, 16) + } else { + return GUIElement.spacer(width: 16, height: 16) + } + } + + public func textWithShadow( + _ text: String, + textColor: Vec4f = Vec4f(1, 1, 1, 1), + shadowColor: Vec4f + ) -> GUIElement { + GUIElement.stack { + GUIElement.text(text, color: shadowColor) + .positionInParent(1, 1) + GUIElement.text(text, color: textColor) } } From f61fcde8943f1346aa1dd00b69f2ed7ae22ac8ab Mon Sep 17 00:00:00 2001 From: stackotter Date: Mon, 27 May 2024 00:17:09 +1000 Subject: [PATCH 26/84] Reimplement chat rendering in new GUI system (pixel perfect except for message text shadows) --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 4 +- Sources/Core/Sources/Client.swift | 6 +- .../ECS/Systems/PlayerInputSystem.swift | 8 +- Sources/Core/Sources/GUI/GUIElement.swift | 151 +++++++++++++++--- Sources/Core/Sources/GUI/InGameGUI.swift | 103 ++++++++++-- Sources/Core/Sources/Game.swift | 22 ++- 6 files changed, 254 insertions(+), 40 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 652875b6..34995c7a 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -12,6 +12,7 @@ public final class GUIRenderer: Renderer { var device: MTLDevice var font: Font + var locale: MinecraftLocale var uniformsBuffer: MTLBuffer var pipelineState: MTLRenderPipelineState var profiler: Profiler @@ -43,6 +44,7 @@ public final class GUIRenderer: Renderer { // Create array texture font = client.resourcePack.vanillaResources.fontPalette.defaultFont + locale = client.resourcePack.getDefaultLocale() let resources = client.resourcePack.vanillaResources let font = resources.fontPalette.defaultFont @@ -132,7 +134,7 @@ public final class GUIRenderer: Renderer { guiState.drawableScalingFactor = scalingFactor } - let renderable = client.game.compileGUI(withFont: font) + let renderable = client.game.compileGUI(withFont: font, locale: locale) let meshes = try meshes(for: renderable) diff --git a/Sources/Core/Sources/Client.swift b/Sources/Core/Sources/Client.swift index aaee7cae..c20bc72c 100644 --- a/Sources/Core/Sources/Client.swift +++ b/Sources/Core/Sources/Client.swift @@ -34,7 +34,8 @@ public final class Client: @unchecked Sendable { game = Game( eventBus: eventBus, configuration: configuration, - font: resourcePack.vanillaResources.fontPalette.defaultFont + font: resourcePack.vanillaResources.fontPalette.defaultFont, + locale: resourcePack.getDefaultLocale() ) } @@ -63,7 +64,8 @@ public final class Client: @unchecked Sendable { eventBus: eventBus, configuration: configuration, connection: connection, - font: resourcePack.vanillaResources.fontPalette.defaultFont + font: resourcePack.vanillaResources.fontPalette.defaultFont, + locale: resourcePack.getDefaultLocale() ) hasFinishedDownloadingTerrain = false try connection.login(username: account.username) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 27c8113c..41d7ea16 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -11,6 +11,7 @@ public final class PlayerInputSystem: System { var eventBus: EventBus let configuration: ClientConfiguration let font: Font + let locale: MinecraftLocale // TODO: Font should be internal to the GUI (which should probably be stored in the nexus) public init( @@ -18,13 +19,15 @@ public final class PlayerInputSystem: System { _ game: Game, _ eventBus: EventBus, _ configuration: ClientConfiguration, - _ font: Font + _ font: Font, + _ locale: MinecraftLocale ) { self.connection = connection self.game = game self.eventBus = eventBus self.configuration = configuration self.font = font + self.locale = locale } public func update(_ nexus: Nexus, _ world: World) throws { @@ -51,7 +54,7 @@ public final class PlayerInputSystem: System { let mousePosition = Vec2i(inputState.mousePosition / guiState.drawableScalingFactor) // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) - let gui = game.compileGUI(withFont: font, guiState: guiState) + let gui = game.compileGUI(withFont: font, locale: locale, guiState: guiState) // Handle non-movement inputs var isInputSuppressed: [Bool] = [] @@ -168,6 +171,7 @@ public final class PlayerInputSystem: System { eventBus.dispatch(CaptureCursorEvent()) return true } else if event.key == .escape { + guiState.messageInputCursor = 0 guiState.messageInput = nil guiState.currentMessageIndex = nil eventBus.dispatch(CaptureCursorEvent()) diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 2640158a..2df02bb4 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -1,10 +1,87 @@ +// TODO: Update container related modifier methods to avoid unnecessary nesting where possible, +// e.g. `.expand().padding(2)` should only result in a single container being added instead of +// two levels of nested containers. public indirect enum GUIElement { public enum Direction { case vertical case horizontal } + public struct DirectionSet: ExpressibleByArrayLiteral { + public var horizontal: Bool + public var vertical: Bool + + public static let both: Self = [.horizontal, .vertical] + public static let neither: Self = [] + public static let horizontal: Self = [.horizontal] + public static let vertical: Self = [.vertical] + + public init(arrayLiteral elements: Direction...) { + vertical = elements.contains(.vertical) + horizontal = elements.contains(.horizontal) + } + } + + public enum Edge { + case top + case bottom + case left + case right + } + + public struct EdgeSet: ExpressibleByArrayLiteral { + public var edges: Set + + public static let top: Self = [.top] + public static let bottom: Self = [.bottom] + public static let left: Self = [.left] + public static let right: Self = [.right] + public static let vertical: Self = [.top, .bottom] + public static let horizontal: Self = [.left, .right] + public static let all: Self = [.top, .bottom, .left, .right] + + public init(arrayLiteral elements: Edge...) { + edges = Set(elements) + } + + public func contains(_ edge: Edge) -> Bool { + edges.contains(edge) + } + } + + public struct Padding { + public var top: Int + public var bottom: Int + public var left: Int + public var right: Int + + /// The total padding along each axis. + public var axisTotals: Vec2i { + Vec2i( + left + right, + top + bottom + ) + } + + public static let zero: Self = Padding(top: 0, bottom: 0, left: 0, right: 0) + + public init(top: Int, bottom: Int, left: Int, right: Int) { + self.top = top + self.bottom = bottom + self.left = left + self.right = right + } + + public init(edges: EdgeSet, amount: Int) { + self.top = edges.contains(.top) ? amount : 0 + self.bottom = edges.contains(.bottom) ? amount : 0 + self.left = edges.contains(.left) ? amount : 0 + self.right = edges.contains(.right) ? amount : 0 + } + } + case text(_ content: String, wrap: Bool = false, color: Vec4f = Vec4f(1, 1, 1, 1)) + case message(_ message: ChatMessage, wrap: Bool = true) case clickable(_ element: GUIElement, action: () -> Void) case sprite(GUISprite) case customSprite(GUISpriteDescriptor) @@ -17,7 +94,7 @@ public indirect enum GUIElement { case sized(element: GUIElement, width: Int?, height: Int?) case spacer(width: Int, height: Int) /// Wraps an element with a background. - case container(background: Vec4f, padding: Int, element: GUIElement) + case container(background: Vec4f, padding: Padding, element: GUIElement, expandDirections: DirectionSet = .neither) case floating(element: GUIElement) case item(id: Int) @@ -25,9 +102,11 @@ public indirect enum GUIElement { switch self { case let .list(_, _, elements), let .stack(elements): return elements - case let .clickable(element, _), let .positioned(element, _), let .sized(element, _, _), let .container(_, _, element), let .floating(element): + case let .clickable(element, _), let .positioned(element, _), + let .sized(element, _, _), let .container(_, _, element, _), + let .floating(element): return [element] - case .text, .sprite, .customSprite, .spacer, .item: + case .text, .message, .sprite, .customSprite, .spacer, .item: return [] } } @@ -81,21 +160,34 @@ public indirect enum GUIElement { .sized(element: self, width: width, height: height) } - public func padding(_ padding: Int) -> GUIElement { - .container(background: .zero, padding: padding, element: self) + public func padding(_ amount: Int) -> GUIElement { + self.padding(.all, amount) + } + + public func padding(_ edges: EdgeSet, _ amount: Int) -> GUIElement { + .container(background: .zero, padding: Padding(edges: edges, amount: amount), element: self) } public func background(_ color: Vec4f) -> GUIElement { // Sometimes we can just update the element instead of adding another layer. switch self { - case let .container(background, padding, element): + case let .container(background, padding, element, expandDirections): if background == .zero { - return .container(background: color, padding: padding, element: element) + return .container( + background: color, + padding: padding, + element: element, + expandDirections: expandDirections + ) } default: break } - return .container(background: color, padding: 0, element: self) + return .container(background: color, padding: .zero, element: self) + } + + public func expand(_ directions: DirectionSet = .both) -> GUIElement { + return .container(background: .zero, padding: .zero, element: self, expandDirections: directions) } public func onClick(_ action: @escaping () -> Void) -> GUIElement { @@ -168,7 +260,8 @@ public indirect enum GUIElement { public func resolveConstraints( availableSize: Vec2i, - font: Font + font: Font, + locale: MinecraftLocale ) -> GUIRenderable { let relativePosition: Vec2i let size: Vec2i @@ -195,10 +288,15 @@ public indirect enum GUIElement { color: color ) children = [] + case let .message(message, wrap): + let text = message.content.toText(with: locale) + return GUIElement.text(text, wrap: wrap) + .resolveConstraints(availableSize: availableSize, font: font, locale: locale) case let .clickable(label, action): let child = label.resolveConstraints( availableSize: availableSize, - font: font + font: font, + locale: locale ) relativePosition = .zero size = child.size @@ -222,7 +320,8 @@ public indirect enum GUIElement { children = elements.map { element in var renderable = element.resolveConstraints( availableSize: availableSize, - font: font + font: font, + locale: locale ) renderable.relativePosition[axisComponent] += childPosition[axisComponent] @@ -247,7 +346,8 @@ public indirect enum GUIElement { children = elements.map { element in element.resolveConstraints( availableSize: availableSize, - font: font + font: font, + locale: locale ) } size = Vec2i( @@ -263,7 +363,8 @@ public indirect enum GUIElement { case let .positioned(element, constraints): let child = element.resolveConstraints( availableSize: availableSize, - font: font + font: font, + locale: locale ) children = [child] relativePosition = constraints.solve( @@ -278,7 +379,8 @@ public indirect enum GUIElement { width ?? availableSize.x, height ?? availableSize.y ), - font: font + font: font, + locale: locale ) children = [child] relativePosition = .zero @@ -292,17 +394,23 @@ public indirect enum GUIElement { relativePosition = .zero size = Vec2i(width, height) content = nil - case let .container(background, padding, element): + case let .container(background, padding, element, expandDirections): + let paddingAxisTotals = padding.axisTotals var child = element.resolveConstraints( - availableSize: availableSize &- Vec2i(repeating: padding * 2), - font: font + availableSize: availableSize &- paddingAxisTotals, + font: font, + locale: locale ) - child.relativePosition &+= Vec2i(repeating: padding) + child.relativePosition &+= Vec2i(padding.left, padding.top) children = [child] relativePosition = .zero size = Vec2i( - min(availableSize.x, child.size.x + padding * 2), - min(availableSize.y, child.size.y + padding * 2) + expandDirections.horizontal + ? availableSize.x + : min(availableSize.x, child.size.x + paddingAxisTotals.x), + expandDirections.vertical + ? availableSize.y + : min(availableSize.y, child.size.y + paddingAxisTotals.y) ) if background.w != 0 { @@ -313,7 +421,8 @@ public indirect enum GUIElement { case let .floating(element): let child = element.resolveConstraints( availableSize: availableSize, - font: font + font: font, + locale: locale ) children = [child] relativePosition = .zero diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index f0bac20d..f1c72d31 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -1,4 +1,6 @@ import SwiftCPUDetect +import CoreFoundation +import Collections public class InGameGUI { // TODO: Figure out why anything greater than 252 breaks the protocol. Anything less than 256 should work afaict @@ -29,14 +31,91 @@ public class InGameGUI { player.gamemode.gamemode } + if state.showHUD { + return GUIElement.stack { + GUIElement.sprite(.crossHair) + .center() + + if gamemode != .spectator { + hotbarArea(game: game, gamemode: gamemode) + } + + chat(state: state) + } + } else { + return GUIElement.spacer(width: 0, height: 0) + } + } + + public func chat(state: GUIStateStorage) -> GUIElement { + // TODO: Implement scrollable chat history. + + // Limit number of messages shown. + let index = max( + state.chat.messages.startIndex, + state.chat.messages.endIndex - Self.maximumDisplayedMessages + ) + let latestMessages = state.chat.messages[index...SubSequence + if state.showChat { + visibleMessages = latestMessages + } else { + let lastVisibleIndex = latestMessages.lastIndex { message in + message.timeReceived < threshold + }?.advanced(by: 1) ?? latestMessages.startIndex + visibleMessages = latestMessages[lastVisibleIndex.. GUIElement { + let messageBeforeCursor = String(content.prefix(upTo: cursorIndex)) + let messageAfterCursor = String(content.suffix(from: cursorIndex)) + + return GUIElement.list(direction: .horizontal, spacing: 0) { + textWithShadow(messageBeforeCursor) - if gamemode != .spectator { - hotbarArea(game: game, gamemode: gamemode) + if Int(CFAbsoluteTimeGetCurrent() * 10/3) % 2 == 1 { + if messageAfterCursor.isEmpty { + textWithShadow("_") + .positionInParent(messageBeforeCursor.isEmpty ? 0 : 1, 0) + } else { + GUIElement.spacer(width: 1, height: 11) + .background(Vec4f(1, 1, 1, 1)) + .positionInParent(0, -1) + .float() + } } + + textWithShadow(messageAfterCursor) } + .size(nil, Font.defaultCharacterHeight + 1) + .expand(.horizontal) + .padding([.top, .left, .right], 2) + .padding(.bottom, 1) + .background(Vec4f(0, 0, 0, 0.5)) } /// The hotbar (and nearby stats if in a gamemode with health). @@ -87,7 +166,7 @@ public class InGameGUI { return GUIElement.stack { GUIElement.item(id: stack.itemId) - textWithShadow("\(stack.count)", shadowColor: Vec4f(62, 62, 62, 255) / 255) + textWithShadow("\(stack.count)") .constraints(.bottom(-2), .right(-1)) .float() } @@ -100,12 +179,16 @@ public class InGameGUI { public func textWithShadow( _ text: String, textColor: Vec4f = Vec4f(1, 1, 1, 1), - shadowColor: Vec4f + shadowColor: Vec4f = Vec4f(62, 62, 62, 255) / 255 ) -> GUIElement { - GUIElement.stack { - GUIElement.text(text, color: shadowColor) - .positionInParent(1, 1) - GUIElement.text(text, color: textColor) + if !text.isEmpty { + return GUIElement.stack { + GUIElement.text(text, color: shadowColor) + .positionInParent(1, 1) + GUIElement.text(text, color: textColor) + } + } else { + return GUIElement.spacer(width: 0, height: 0) } } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 1a00e742..1fed9bf0 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -68,7 +68,13 @@ public final class Game: @unchecked Sendable { // MARK: Init /// Creates a game with default properties. Creates the player. Starts the tick loop. - public init(eventBus: EventBus, configuration: ClientConfiguration, connection: ServerConnection? = nil, font: Font) { + public init( + eventBus: EventBus, + configuration: ClientConfiguration, + connection: ServerConnection? = nil, + font: Font, + locale: MinecraftLocale + ) { self.eventBus = eventBus world = World(eventBus: eventBus) @@ -94,7 +100,7 @@ public final class Game: @unchecked Sendable { // require significant refactoring if we wanna do it right (as in not just hacking it // together for the specific case of PlayerInputSystem); proper resource pack propagation // will probably take quite a bit of work. - tickScheduler.addSystem(PlayerInputSystem(connection, self, eventBus, configuration, font)) + tickScheduler.addSystem(PlayerInputSystem(connection, self, eventBus, configuration, font, locale)) tickScheduler.addSystem(PlayerFlightSystem()) tickScheduler.addSystem(PlayerAccelerationSystem()) tickScheduler.addSystem(PlayerJumpSystem()) @@ -212,9 +218,17 @@ public final class Game: @unchecked Sendable { /// - acquireGUILock: If `false`, a GUI lock will not be acquired. Use with caution. /// - acquireNexusLock: If `false`, a GUI lock will not be acquired (otherwise a nexus lock will be /// acquired if guiState isn't supplied). Use with caution. + /// - font: Font to use when rendering, used to compute text sizing and wrapping. + /// - locale: Locale used to resolve chat message content. /// - guiState: Avoids the need for this function to call out to the nexus redundantly if the caller already /// has a reference to the gui state. - public func compileGUI(acquireGUILock: Bool = true, acquireNexusLock: Bool = true, withFont font: Font, guiState: GUIStateStorage? = nil) -> GUIElement.GUIRenderable { + public func compileGUI( + acquireGUILock: Bool = true, + acquireNexusLock: Bool = true, + withFont font: Font, + locale: MinecraftLocale, + guiState: GUIStateStorage? = nil + ) -> GUIElement.GUIRenderable { // Acquire the nexus lock first as that's the one that threads can be sitting inside of with `Game.accessNexus`. // If we get the GUI lock first then the renderer can be waiting for the nexus lock while PlayerInputSystem is // sitting with a nexus lock and waiting for a gui lock. @@ -231,7 +245,7 @@ public final class Game: @unchecked Sendable { defer { if acquireGUILock { guiLock.unlock() } } defer { if acquireNexusLock && guiState == nil { nexusLock.unlock() } } return gui.content(game: self, state: state) - .resolveConstraints(availableSize: state.drawableSize, font: font) + .resolveConstraints(availableSize: state.drawableSize, font: font, locale: locale) } // MARK: Entity From dc13c79bf4e04af6ce8a27df0061bbb910f3be04 Mon Sep 17 00:00:00 2001 From: stackotter Date: Mon, 27 May 2024 11:36:42 +1000 Subject: [PATCH 27/84] Reimplement debug screen rendering in new GUI system (seems like a pixel perfect match to vanilla in terms of layout) Just missing some fields --- Sources/Client/Views/Play/PlayView.swift | 1 + Sources/Core/Renderer/RenderCoordinator.swift | 2 +- Sources/Core/Sources/GUI/InGameGUI.swift | 133 +++++++++++++++++- .../GUI}/RenderStatistics.swift | 2 +- Sources/Core/Sources/GUIState.swift | 23 +++ Sources/Core/Sources/Game.swift | 8 ++ .../Sources/Util/FunctionalProgramming.swift | 4 + 7 files changed, 166 insertions(+), 7 deletions(-) rename Sources/Core/{Renderer => Sources/GUI}/RenderStatistics.swift (97%) create mode 100644 Sources/Core/Sources/Util/FunctionalProgramming.swift diff --git a/Sources/Client/Views/Play/PlayView.swift b/Sources/Client/Views/Play/PlayView.swift index 9d10df71..42e66408 100644 --- a/Sources/Client/Views/Play/PlayView.swift +++ b/Sources/Client/Views/Play/PlayView.swift @@ -43,6 +43,7 @@ struct PlayView: View { InGameMenu(presented: $inGameMenuPresented) } + .padding(.top, 1) } var allPlayersChoseControllers: Bool { diff --git a/Sources/Core/Renderer/RenderCoordinator.swift b/Sources/Core/Renderer/RenderCoordinator.swift index 94f01d48..f5247cf1 100644 --- a/Sources/Core/Renderer/RenderCoordinator.swift +++ b/Sources/Core/Renderer/RenderCoordinator.swift @@ -362,7 +362,7 @@ public final class RenderCoordinator: NSObject, MTKViewDelegate { ) // Update statistics in gui - // guiRenderer.gui.renderStatistics = statistics + client.game.updateRenderStatistics(to: statistics) commandBuffer.commit() profiler.pop() diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index f1c72d31..75c7e97b 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -2,6 +2,7 @@ import SwiftCPUDetect import CoreFoundation import Collections +/// Never acquires nexus locks. public class InGameGUI { // TODO: Figure out why anything greater than 252 breaks the protocol. Anything less than 256 should work afaict public static let maximumMessageLength = 252 @@ -23,9 +24,11 @@ public class InGameGUI { static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() static let xpLevelTextColor = Vec4f(126, 252, 31, 255) / 255 + static let debugScreenRowBackgroundColor = Vec4f(80, 80, 80, 144) / 255 public init() {} + /// Gets the GUI's content. Doesn't acquire any locks. public func content(game: Game, state: GUIStateStorage) -> GUIElement { let gamemode = game.accessPlayer(acquireLock: false) { player in player.gamemode.gamemode @@ -33,11 +36,17 @@ public class InGameGUI { if state.showHUD { return GUIElement.stack { - GUIElement.sprite(.crossHair) - .center() - if gamemode != .spectator { - hotbarArea(game: game, gamemode: gamemode) + GUIElement.stack { + hotbarArea(game: game, gamemode: gamemode) + + GUIElement.sprite(.crossHair) + .center() + } + } + + if state.showDebugScreen { + debugScreen(game: game, state: state) } chat(state: state) @@ -73,6 +82,7 @@ public class InGameGUI { return GUIElement.stack { if !visibleMessages.isEmpty { GUIElement.forEach(in: visibleMessages, spacing: 1) { message in + // TODO: Chat message text shadows GUIElement.message(message, wrap: true) } .constraints(.top(0), .left(1)) @@ -118,7 +128,8 @@ public class InGameGUI { .background(Vec4f(0, 0, 0, 0.5)) } - /// The hotbar (and nearby stats if in a gamemode with health). + /// Gets the contents of the hotbar (and nearby stats if in a gamemode with health). + /// Doesn't acquire a nexus lock. public func hotbarArea(game: Game, gamemode: Gamemode) -> GUIElement { var health: Float = 0 var food: Int = 0 @@ -240,6 +251,118 @@ public class InGameGUI { } } + /// Gets the contents of the debug screen, doesn't acquire a nexus lock. + public func debugScreen(game: Game, state: GUIStateStorage) -> GUIElement { + var blockPosition = BlockPosition(x: 0, y: 0, z: 0) + var chunkSectionPosition = ChunkSectionPosition(sectionX: 0, sectionY: 0, sectionZ: 0) + var position: Vec3d = .zero + var pitch: Float = 0 + var yaw: Float = 0 + var heading: Direction = .north + var gamemode: Gamemode = .adventure + game.accessPlayer(acquireLock: false) { player in + position = player.position.vector + blockPosition = player.position.blockUnderneath + chunkSectionPosition = player.position.chunkSection + pitch = MathUtil.degrees(from: player.rotation.pitch) + yaw = MathUtil.degrees(from: player.rotation.yaw) + heading = player.rotation.heading + gamemode = player.gamemode.gamemode + } + blockPosition.y += 1 + + let x = String(format: "%.06f", position.x).prefix(7) + let y = String(format: "%.06f", position.y).prefix(7) + let z = String(format: "%.06f", position.z).prefix(7) + + let relativePosition = blockPosition.relativeToChunkSection + let relativePositionString = "\(relativePosition.x) \(relativePosition.y) \(relativePosition.z)" + let chunkSectionString = "\(chunkSectionPosition.sectionX) \(chunkSectionPosition.sectionY) \(chunkSectionPosition.sectionZ)" + + let yawString = String(format: "%.01f", yaw) + let pitchString = String(format: "%.01f", pitch) + let axisHeading = "\(heading.isPositive ? "positive" : "negative") \(heading.axis)" + + var lightPosition = blockPosition + lightPosition.y += 1 + let skyLightLevel = game.world.getSkyLightLevel(at: lightPosition) + let blockLightLevel = game.world.getBlockLightLevel(at: lightPosition) + + let biome = game.world.getBiome(at: blockPosition) + + let leftSections: [[String]] = [ + [ + "Minecraft \(Constants.versionString) (Delta Client)", + renderStatisticsString(state.inner.debouncedRenderStatistics()), + "Dimension: \(game.world.dimension.identifier)", + ], + [ + "XYZ: \(x) / \(y) / \(z)", + // Block under feet + "Block: \(blockPosition.x) \(blockPosition.y) \(blockPosition.z)", + "Chunk: \(relativePositionString) in \(chunkSectionString)", + "Facing: \(heading) (Towards \(axisHeading)) (\(yawString) / \(pitchString))", + // Lighting (at foot level) + "Light: \(skyLightLevel) sky, \(blockLightLevel) block", + "Biome: \(biome?.identifier.description ?? "not loaded")", + "Gamemode: \(gamemode.string)" + ] + ] + + let rightSections: [[String]] = [ + [ + "CPU: \(Self.cpuName ?? "unknown") (\(Self.cpuArch ?? "n/a"))", + "Total mem: \(Self.totalMem)GB", + "GPU: \(Self.gpuInfo ?? "unknown")" + ] + ] + + return GUIElement.stack { + debugScreenList(leftSections, side: .left) + debugScreenList(rightSections, side: .right) + } + } + + public enum Alignment { + case left + case right + } + + public func debugScreenList(_ sections: [[String]], side: Alignment) -> GUIElement { + GUIElement.forEach(in: sections, spacing: 6) { section in + GUIElement.forEach(in: section, spacing: 0) { line in + GUIElement.text(line) + .padding([.left, .top], 1) + .padding([.right], 2) + .background(Self.debugScreenRowBackgroundColor) + .constraints(.top(0), side == .left ? .left(0) : .right(0)) + } + .padding(1) + } + } + + /// Converts the given render statistics into the format required by the debug screen; + /// + /// ``` + /// XX fps (XX.XX theoretical) (XX.XXms cpu, XX.XXms gpu) + /// ``` + /// + /// Theoretical FPS and GPU time are only included if being collected. + public func renderStatisticsString(_ renderStatistics: RenderStatistics) -> String { + let theoreticalFPSString = renderStatistics.averageTheoreticalFPS.map { theoreticalFPS in + "(\(theoreticalFPS) theoretical)" + } + let gpuTimeString = renderStatistics.averageGPUTime.map { gpuTime in + String(format: "%.02fms gpu", gpuTime * 1000) + } + let cpuTimeString = String(format: "%.02fms cpu", renderStatistics.averageCPUTime * 1000) + let fpsString = String(format: "%.00f fps", renderStatistics.averageFPS) + + let timingsString = [cpuTimeString, gpuTimeString].compactMap(identity).joined(separator: ", ") + + return [fpsString, theoreticalFPSString, "(\(timingsString))"].compactMap(identity).joined(separator: " ") + } + public func outlinedText( _ text: String, textColor: Vec4f, diff --git a/Sources/Core/Renderer/RenderStatistics.swift b/Sources/Core/Sources/GUI/RenderStatistics.swift similarity index 97% rename from Sources/Core/Renderer/RenderStatistics.swift rename to Sources/Core/Sources/GUI/RenderStatistics.swift index 77877b03..e26709ff 100644 --- a/Sources/Core/Renderer/RenderStatistics.swift +++ b/Sources/Core/Sources/GUI/RenderStatistics.swift @@ -49,7 +49,7 @@ public struct RenderStatistics { // MARK: Init /// Creates a new group of render statistics. - public init(gpuCountersEnabled: Bool) { + public init(gpuCountersEnabled: Bool = false) { self.gpuCountersEnabled = gpuCountersEnabled } diff --git a/Sources/Core/Sources/GUIState.swift b/Sources/Core/Sources/GUIState.swift index b10ae70f..f72241e4 100644 --- a/Sources/Core/Sources/GUIState.swift +++ b/Sources/Core/Sources/GUIState.swift @@ -1,4 +1,10 @@ +import CoreFoundation + public struct GUIState { + /// The interval between render statistics updates in the GUI. Used by + /// ``GUIState/debouncedRenderStatistics()``. + public static let renderStatisticsUpdateInterval = 0.4 + /// Whether the HUD (health, hotbar, hunger, etc.) is visible or not. public var showHUD = true /// Whether the debug screen is visible or not. @@ -34,6 +40,13 @@ public struct GUIState { /// and the maximum value is the beginning of the message. public var messageInputCursor: Int = 0 + /// Rendering statistics to display to the user. + public var renderStatistics = RenderStatistics() + /// The render statistics last returned by `debouncedRenderStatistics`. + private var debouncedRenderStatisticsStorage = RenderStatistics() + /// The last time that the debounced render statistics were updated at. + private var lastDebouncedRenderStatisticsUpdate: CFAbsoluteTime = 0 + /// The chat input field cursor as an index into ``messageInput``. public var messageInputCursorIndex: String.Index { if let messageInput = messageInput { @@ -53,4 +66,14 @@ public struct GUIState { public var movementAllowed: Bool { return !showChat && !showInventory } + + /// The render statistics but only updated every `Self.renderStatisticsUpdateInterval`. + public mutating func debouncedRenderStatistics() -> RenderStatistics { + let time = CFAbsoluteTimeGetCurrent() + if time > lastDebouncedRenderStatisticsUpdate + Self.renderStatisticsUpdateInterval { + debouncedRenderStatisticsStorage = renderStatistics + lastDebouncedRenderStatisticsUpdate = time + } + return debouncedRenderStatisticsStorage + } } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 1fed9bf0..7578c6cc 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -208,12 +208,20 @@ public final class Game: @unchecked Sendable { _guiState.chat.add(message) } + /// Mutates the GUI state with a given action. public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) throws -> R) rethrows -> R { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } return try action(&_guiState.inner) } + /// Updates the GUI's render statistics. + public func updateRenderStatistics(acquireLock: Bool = true, to statistics: RenderStatistics) { + mutateGUIState(acquireLock: false) { state in + state.renderStatistics = statistics + } + } + /// Compile the in-game GUI to a renderable. /// - acquireGUILock: If `false`, a GUI lock will not be acquired. Use with caution. /// - acquireNexusLock: If `false`, a GUI lock will not be acquired (otherwise a nexus lock will be diff --git a/Sources/Core/Sources/Util/FunctionalProgramming.swift b/Sources/Core/Sources/Util/FunctionalProgramming.swift new file mode 100644 index 00000000..21e3ecf6 --- /dev/null +++ b/Sources/Core/Sources/Util/FunctionalProgramming.swift @@ -0,0 +1,4 @@ +/// The identity function. +func identity(_ value: T) -> T { + value +} From 24e2f561df8bddf5b7f6115e9d16a69d1d242cd4 Mon Sep 17 00:00:00 2001 From: stackotter Date: Mon, 27 May 2024 19:32:43 +1000 Subject: [PATCH 28/84] Implement inventory GUI with basic mouse interaction, item stacks can be moved! --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 7 +- .../ECS/Components/PlayerInventory.swift | 120 ++++++++++-------- Sources/Core/Sources/GUI/GUIElement.swift | 8 ++ Sources/Core/Sources/GUI/InGameGUI.swift | 96 +++++++++++--- Sources/Core/Sources/GUIState.swift | 3 + .../Sources/Util/FunctionalProgramming.swift | 6 + 6 files changed, 165 insertions(+), 75 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 34995c7a..65068c0d 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -244,13 +244,15 @@ public final class GUIRenderer: Renderer { * MatrixUtil.rotationMatrix(x: .pi) * MatrixUtil.rotationMatrix(y: -.pi / 4) * MatrixUtil.rotationMatrix(x: -.pi / 6) + * MatrixUtil.scalingMatrix(9.76) + * MatrixUtil.translationMatrix([8, 8, 8]) var geometry = Geometry() var translucentGeometry = SortableMeshElement() BlockMeshBuilder( model: model, position: BlockPosition(x: 0, y: 0, z: 0), - modelToWorld: transformation * MatrixUtil.scalingMatrix(9.76), + modelToWorld: transformation, culledFaces: [], lightLevel: LightLevel(sky: 15, block: 15), neighbourLightLevels: [:], @@ -270,13 +272,12 @@ public final class GUIRenderer: Renderer { } // TODO: Handle translucent block items - var mesh = GUIElementMesh( size: [16, 16], arrayTexture: blockArrayTexture, vertices: .flatArray(vertices) ) - mesh.position = [8, 8] + mesh.position = [0, 0] return [mesh] case .empty, .entity: return [] diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index c7bcb659..b288bf81 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -6,70 +6,62 @@ public class PlayerInventory: Component { public static let slotCount = 46 /// The player's inventory's window id. public static let windowId = 0 - /// The index of the first hotbar slot. - public static let hotbarSlotStartIndex = 36 - /// The index of the last hotbar slot. - public static let hotbarSlotEndIndex = 44 - - /// The index of the first slot of the main inventory area (the 3 by 9 grid). - public static let mainAreaStartIndex = 9 - /// The width of the main area. - public static let mainAreaWidth = 9 - /// The height of the main area. - public static let mainAreaHeight = 3 - - /// The index of the first slot of the inventory's crafting area. - public static let craftingAreaStartIndex = 1 - /// The width of the inventory's crafting area. - public static let craftingAreaWidth = 2 - /// The height of the inventory's crafting area. - public static let craftingAreaHeight = 2 - /// The index of the crafting result slot. - public static let craftingResultIndex = 0 - - /// The index of the first armor slot. - public static let armorSlotsStartIndex = 5 - /// The number of armor slots. - public static let armorSlotsCount = 4 + /// The index of the crafting result slot. + public static let craftingResultIndex = Area.craftingResult.startIndex /// The index of the player's off-hand slot. - public static let offHandIndex = 45 + public static let offHandIndex = Area.offHand.startIndex /// The inventory's contents. public var slots: [Slot] /// The player's currently selected hotbar slot. public var selectedHotbarSlot: Int - /// The inventory's hotbar. - public var hotbar: [Slot] { - return Array(slots[Self.hotbarSlotStartIndex...Self.hotbarSlotEndIndex]) - } - /// The rows of the main 3 by 9 area of the inventory. - public var mainArea: [[Slot]] { - var rows: [[Slot]] = [] - for y in 0.. [[Slot]] { + var rows: [[Slot]] = [] + for y in 0.. GUIElement { + .positioned(element: self, constraints: .position(position.x, position.y)) + } + public func constraints( _ verticalConstraint: VerticalConstraint, _ horizontalConstraint: HorizontalConstraint @@ -160,6 +164,10 @@ public indirect enum GUIElement { .sized(element: self, width: width, height: height) } + public func size(_ size: Vec2i) -> GUIElement { + .sized(element: self, width: size.x, height: size.y) + } + public func padding(_ amount: Int) -> GUIElement { self.padding(.all, amount) } diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 75c7e97b..cac4d616 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -50,6 +50,10 @@ public class InGameGUI { } chat(state: state) + + if state.showInventory { + inventory(game: game, state: state) + } } } else { return GUIElement.spacer(width: 0, height: 0) @@ -131,20 +135,17 @@ public class InGameGUI { /// Gets the contents of the hotbar (and nearby stats if in a gamemode with health). /// Doesn't acquire a nexus lock. public func hotbarArea(game: Game, gamemode: Gamemode) -> GUIElement { - var health: Float = 0 - var food: Int = 0 - var selectedSlot: Int = 0 - var xpBarProgress: Float = 0 - var xpLevel: Int = 0 - var hotbarSlots: [Slot] = [] - game.accessPlayer(acquireLock: false) { player in - health = player.health.health - food = player.nutrition.food - selectedSlot = player.inventory.selectedHotbarSlot - xpBarProgress = player.experience.experienceBarProgress - xpLevel = player.experience.experienceLevel - hotbarSlots = player.inventory.hotbar - } + let (health, food, selectedSlot, xpBarProgress, xpLevel, hotbarSlots) = + game.accessPlayer(acquireLock: false) { player in + ( + player.health.health, + player.nutrition.food, + player.inventory.selectedHotbarSlot, + player.experience.experienceBarProgress, + player.experience.experienceLevel, + player.inventory.hotbar + ) + } return GUIElement.list(spacing: 0) { if gamemode.hasHealth { @@ -171,15 +172,76 @@ public class InGameGUI { } } + public func inventory(game: Game, state: GUIStateStorage) -> GUIElement { + let inventory = game.accessPlayer(acquireLock: false) { player in + player.inventory + } + + let mousePosition = game.accessInputState(acquireLock: false) { inputState in + Vec2i(inputState.mousePosition / state.drawableScalingFactor) + } + + return GUIElement.stack { + GUIElement.stack { + GUIElement.sprite(.inventory) + + inventoryGrid(inventory, state, area: .armor) + .positionInParent(8, 8) + + inventoryGrid(inventory, state, area: .offHand) + .positionInParent(77, 62) + + inventoryGrid(inventory, state, area: .craftingInput) + .positionInParent(98, 18) + + inventoryGrid(inventory, state, area: .craftingResult) + .positionInParent(154, 28) + + inventoryGrid(inventory, state, area: .main) + .positionInParent(8, 84) + + inventoryGrid(inventory, state, area: .hotbar) + .positionInParent(8, 142) + } + .size(GUISprite.inventory.descriptor.size) + .center() + .expand() + .background(Vec4f(0, 0, 0, 0.702)) + + if let mouseItemStack = state.mouseItemStack { + inventorySlot(Slot(mouseItemStack)) + .positionInParent(mousePosition &- Vec2i(8, 8)) + } + } + } + + public func inventoryGrid( + _ inventory: PlayerInventory, + _ state: GUIStateStorage, + area: PlayerInventory.Area + ) -> GUIElement { + GUIElement.forEach(in: 0.. GUIElement { // TODO: Make if blocks layout transparent (their children should be treated as children of the parent block) if let stack = slot.stack { return GUIElement.stack { GUIElement.item(id: stack.itemId) - textWithShadow("\(stack.count)") - .constraints(.bottom(-2), .right(-1)) - .float() + if stack.count != 1 { + textWithShadow("\(stack.count)") + .constraints(.bottom(-2), .right(-1)) + .float() + } } .size(16, 16) } else { diff --git a/Sources/Core/Sources/GUIState.swift b/Sources/Core/Sources/GUIState.swift index f72241e4..1e104102 100644 --- a/Sources/Core/Sources/GUIState.swift +++ b/Sources/Core/Sources/GUIState.swift @@ -47,6 +47,9 @@ public struct GUIState { /// The last time that the debounced render statistics were updated at. private var lastDebouncedRenderStatisticsUpdate: CFAbsoluteTime = 0 + /// The item stack currently being moved by the mouse. + public var mouseItemStack: ItemStack? + /// The chat input field cursor as an index into ``messageInput``. public var messageInputCursorIndex: String.Index { if let messageInput = messageInput { diff --git a/Sources/Core/Sources/Util/FunctionalProgramming.swift b/Sources/Core/Sources/Util/FunctionalProgramming.swift index 21e3ecf6..2a5124f9 100644 --- a/Sources/Core/Sources/Util/FunctionalProgramming.swift +++ b/Sources/Core/Sources/Util/FunctionalProgramming.swift @@ -2,3 +2,9 @@ func identity(_ value: T) -> T { value } + +func swap(_ left: inout T, _ right: inout T) { + let temp = left + left = right + right = temp +} From dc5544917a9ad8700447f005f5abf707b4b436e8 Mon Sep 17 00:00:00 2001 From: stackotter Date: Mon, 27 May 2024 22:21:08 +1000 Subject: [PATCH 29/84] Send ClickWindowPacket when player moves items in inventory, and CloseWindowPacket when inventory gets closed --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 2 +- .../ECS/Systems/PlayerInputSystem.swift | 9 ++- Sources/Core/Sources/GUI/InGameGUI.swift | 29 ++++--- Sources/Core/Sources/Game.swift | 4 +- .../Play/Serverbound/ClickWindowPacket.swift | 81 +++++++++++++++++-- 5 files changed, 107 insertions(+), 18 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 65068c0d..e4abb5c4 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -134,7 +134,7 @@ public final class GUIRenderer: Renderer { guiState.drawableScalingFactor = scalingFactor } - let renderable = client.game.compileGUI(withFont: font, locale: locale) + let renderable = client.game.compileGUI(withFont: font, locale: locale, connection: nil) let meshes = try meshes(for: renderable) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 41d7ea16..afb749f9 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -54,7 +54,7 @@ public final class PlayerInputSystem: System { let mousePosition = Vec2i(inputState.mousePosition / guiState.drawableScalingFactor) // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) - let gui = game.compileGUI(withFont: font, locale: locale, guiState: guiState) + let gui = game.compileGUI(withFont: font, locale: locale, connection: connection, guiState: guiState) // Handle non-movement inputs var isInputSuppressed: [Bool] = [] @@ -76,6 +76,13 @@ public final class PlayerInputSystem: System { 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 connection?.sendPacket(CloseWindowServerboundPacket(windowId: UInt8(PlayerInventory.windowId))) + } inputState.releaseAll() eventBus.dispatch(ReleaseCursorEvent()) case .slot1: diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index cac4d616..e87cacaf 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -29,7 +29,7 @@ public class InGameGUI { public init() {} /// Gets the GUI's content. Doesn't acquire any locks. - public func content(game: Game, state: GUIStateStorage) -> GUIElement { + public func content(game: Game, connection: ServerConnection?, state: GUIStateStorage) -> GUIElement { let gamemode = game.accessPlayer(acquireLock: false) { player in player.gamemode.gamemode } @@ -52,7 +52,7 @@ public class InGameGUI { chat(state: state) if state.showInventory { - inventory(game: game, state: state) + inventory(game: game, connection: connection, state: state) } } } else { @@ -172,7 +172,7 @@ public class InGameGUI { } } - public func inventory(game: Game, state: GUIStateStorage) -> GUIElement { + public func inventory(game: Game, connection: ServerConnection?, state: GUIStateStorage) -> GUIElement { let inventory = game.accessPlayer(acquireLock: false) { player in player.inventory } @@ -185,22 +185,22 @@ public class InGameGUI { GUIElement.stack { GUIElement.sprite(.inventory) - inventoryGrid(inventory, state, area: .armor) + inventoryGrid(inventory, connection, state, area: .armor) .positionInParent(8, 8) - inventoryGrid(inventory, state, area: .offHand) + inventoryGrid(inventory, connection, state, area: .offHand) .positionInParent(77, 62) - inventoryGrid(inventory, state, area: .craftingInput) + inventoryGrid(inventory, connection, state, area: .craftingInput) .positionInParent(98, 18) - inventoryGrid(inventory, state, area: .craftingResult) + inventoryGrid(inventory, connection, state, area: .craftingResult) .positionInParent(154, 28) - inventoryGrid(inventory, state, area: .main) + inventoryGrid(inventory, connection, state, area: .main) .positionInParent(8, 84) - inventoryGrid(inventory, state, area: .hotbar) + inventoryGrid(inventory, connection, state, area: .hotbar) .positionInParent(8, 142) } .size(GUISprite.inventory.descriptor.size) @@ -217,6 +217,7 @@ public class InGameGUI { public func inventoryGrid( _ inventory: PlayerInventory, + _ connection: ServerConnection?, _ state: GUIStateStorage, area: PlayerInventory.Area ) -> GUIElement { @@ -226,6 +227,16 @@ public class InGameGUI { inventorySlot(inventory.slots[index]) .onClick { swap(&inventory.slots[index].stack, &state.mouseItemStack) + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(PlayerInventory.windowId), + actionId: 0, + action: .leftClick(slot: Int16(index)), + clickedItem: Slot(state.mouseItemStack) + )) + } catch { + log.warning("Failed to send click window packet for inventory interaction: \(error)") + } } } } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 7578c6cc..3d2c94f4 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -226,6 +226,7 @@ public final class Game: @unchecked Sendable { /// - acquireGUILock: If `false`, a GUI lock will not be acquired. Use with caution. /// - acquireNexusLock: If `false`, a GUI lock will not be acquired (otherwise a nexus lock will be /// acquired if guiState isn't supplied). Use with caution. + /// - connection: Used to notify the server of window interactions and related operations. /// - font: Font to use when rendering, used to compute text sizing and wrapping. /// - locale: Locale used to resolve chat message content. /// - guiState: Avoids the need for this function to call out to the nexus redundantly if the caller already @@ -235,6 +236,7 @@ public final class Game: @unchecked Sendable { acquireNexusLock: Bool = true, withFont font: Font, locale: MinecraftLocale, + connection: ServerConnection?, guiState: GUIStateStorage? = nil ) -> GUIElement.GUIRenderable { // Acquire the nexus lock first as that's the one that threads can be sitting inside of with `Game.accessNexus`. @@ -252,7 +254,7 @@ public final class Game: @unchecked Sendable { if acquireGUILock { guiLock.acquireWriteLock() } defer { if acquireGUILock { guiLock.unlock() } } defer { if acquireNexusLock && guiState == nil { nexusLock.unlock() } } - return gui.content(game: self, state: state) + return gui.content(game: self, connection: connection, state: state) .resolveConstraints(availableSize: state.drawableSize, font: font, locale: locale) } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift index 1e511058..19c905b2 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift @@ -4,17 +4,86 @@ public struct ClickWindowPacket: ServerboundPacket { public static let id: Int = 0x09 public var windowId: UInt8 - public var slot: Int16 - public var button: Int8 - public var actionNumber: Int16 - public var mode: Int32 + /// A unique id for the action, used by the server when sending confirmation packets. + /// Vanilla computes this from per-window counters. + public var actionId: Int16 + public var action: Action public var clickedItem: Slot + public enum Action { + case leftClick(slot: Int16) + case rightClick(slot: Int16) + case shiftLeftClick(slot: Int16) + case shiftRightClick(slot: Int16) + case numberKey(slot: Int16, number: Int8) + case middleClick(slot: Int16) + case drop(slot: Int16) + case controlDrop(slot: Int16) + case leftClickOutsideInventory + case rightClickOutsideInventory + case startLeftDrag + case startRightDrag + case startMiddleDrag + case addLeftDragSlot(slot: Int16) + case addRightDragSlot(slot: Int16) + case addMiddleDragSlot(slot: Int16) + case endLeftDrag + case endRightDrag + case endMiddleDrag + case doubleClick(slot: Int16) + + var rawValue: (mode: Int32, button: Int8, slot: Int16?) { + switch self { + case let .leftClick(slot): + return (0, 0, slot) + case let .rightClick(slot): + return (0, 1, slot) + case let .shiftLeftClick(slot): + return (1, 0, slot) + case let .shiftRightClick(slot): + return (1, 1, slot) + case let .numberKey(slot, number): + return (2, number, slot) + case let .middleClick(slot): + return (3, 2, slot) + case let .drop(slot): + return (4, 0, slot) + case let .controlDrop(slot): + return (4, 1, slot) + case .leftClickOutsideInventory: + return (4, 0, nil) + case .rightClickOutsideInventory: + return (4, 1, nil) + case .startLeftDrag: + return (5, 0, nil) + case .startRightDrag: + return (5, 4, nil) + case .startMiddleDrag: + return (5, 8, nil) + case let .addLeftDragSlot(slot): + return (5, 1, slot) + case let .addRightDragSlot(slot): + return (5, 5, slot) + case let .addMiddleDragSlot(slot): + return (5, 9, slot) + case .endLeftDrag: + return (5, 2, nil) + case .endRightDrag: + return (5, 6, nil) + case .endMiddleDrag: + return (5, 10, nil) + case let .doubleClick(slot): + return (6, 0, slot) + } + } + } + public func writePayload(to writer: inout PacketWriter) { writer.writeUnsignedByte(windowId) - writer.writeShort(slot) + let (mode, button, slot) = action.rawValue + writer.writeShort(slot ?? -999) writer.writeByte(button) - writer.writeShort(actionNumber) + writer.writeShort(actionId) writer.writeVarInt(mode) writer.writeSlot(clickedItem) } From 6590d403d60c4c4c1e5714c244dcefd74864d96a Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 01:20:32 +1000 Subject: [PATCH 30/84] Implement dropping items from the inventory (and sending relevant packets to the server) --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 2 +- .../ECS/Components/PlayerInventory.swift | 4 + .../ECS/Systems/PlayerInputSystem.swift | 12 ++- Sources/Core/Sources/GUI/GUIElement.swift | 65 +++++++++++--- Sources/Core/Sources/GUI/InGameGUI.swift | 86 ++++++++++++++++--- Sources/Core/Sources/Input/Input.swift | 3 + Sources/Core/Sources/Input/Key.swift | 20 +++++ Sources/Core/Sources/Input/Keymap.swift | 1 + .../Play/Serverbound/ClickWindowPacket.swift | 16 ++-- 9 files changed, 177 insertions(+), 32 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index e4abb5c4..dcac7789 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -196,7 +196,7 @@ public final class GUIRenderer: Renderer { )] case let .item(itemId): meshes = try self.meshes(forItemWithId: itemId) - case nil, .clickable, .background: + case nil, .interactable, .background: if case let .background(color) = renderable.content { meshes = [ GUIElementMesh(size: renderable.size, color: color) diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index b288bf81..af703e20 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -17,6 +17,10 @@ public class PlayerInventory: Component { /// The player's currently selected hotbar slot. public var selectedHotbarSlot: Int + /// The action id to use for the next action performed on the inventory (used when sending + /// ``ClickWindowPacket``). + var nextActionId = 0 + public struct Area { public var startIndex: Int public var width: Int diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index afb749f9..1a639598 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -59,11 +59,15 @@ public final class PlayerInputSystem: System { // Handle non-movement inputs var isInputSuppressed: [Bool] = [] for event in inputState.newlyPressed { - var suppressInput = try handleChat(event, inputState, guiState) || handleInventory(event, guiState) + var suppressInput = false - // TODO: Formalize 'mouse interactions are allowed', seems a bit hacky this way - if event.key == .leftMouseButton && !guiState.movementAllowed { - suppressInput = gui.handleClick(at: mousePosition) + // TODO: Formalize 'mouse targeted interactions are allowed', seems a bit hacky this way + if !suppressInput && !guiState.movementAllowed { + suppressInput = gui.handleInteraction(.press(event), at: mousePosition) + } + + if !suppressInput { + suppressInput = try handleChat(event, inputState, guiState) || handleInventory(event, guiState) } if !suppressInput { diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 0e74ca09..06ea4db6 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -82,7 +82,7 @@ public indirect enum GUIElement { case text(_ content: String, wrap: Bool = false, color: Vec4f = Vec4f(1, 1, 1, 1)) case message(_ message: ChatMessage, wrap: Bool = true) - case clickable(_ element: GUIElement, action: () -> Void) + case interactable(_ element: GUIElement, handleInteraction: (Interaction) -> Bool) case sprite(GUISprite) case customSprite(GUISpriteDescriptor) /// Stacks elements in the specified direction. Aligns elements to the top left by default. @@ -102,7 +102,7 @@ public indirect enum GUIElement { switch self { case let .list(_, _, elements), let .stack(elements): return elements - case let .clickable(element, _), let .positioned(element, _), + case let .interactable(element, _), let .positioned(element, _), let .sized(element, _, _), let .container(_, _, element, _), let .floating(element): return [element] @@ -199,7 +199,44 @@ public indirect enum GUIElement { } public func onClick(_ action: @escaping () -> Void) -> GUIElement { - .clickable(self, action: action) + onHoverKeyPress(matching: .leftMouseButton, action) + } + + public func onRightClick(_ action: @escaping () -> Void) -> GUIElement { + onHoverKeyPress(matching: .rightMouseButton, action) + } + + public func onHoverKeyPress(matching key: Key, _ action: @escaping () -> Void) -> GUIElement { + .interactable( + self, + handleInteraction: { interaction in + switch interaction { + case let .press(event): + if event.key == key { + action() + return true + } else { + return false + } + case .release: + return false + } + } + ) + } + + public func onHoverKeyPress(_ action: @escaping (KeyPressEvent) -> Bool) -> GUIElement { + .interactable( + self, + handleInteraction: { interaction in + switch interaction { + case let .press(event): + return action(event) + case .release: + return false + } + } + ) } /// Sets an element's apparent size to zero so that it doesn't partake in layout. @@ -215,6 +252,11 @@ public indirect enum GUIElement { .floating(element: self) } + public enum Interaction { + case press(KeyPressEvent) + case release(KeyReleaseEvent) + } + public struct GUIRenderable { public var relativePosition: Vec2i public var size: Vec2i @@ -223,7 +265,7 @@ public indirect enum GUIElement { public enum Content { case text(wrappedLines: [String], hangingIndent: Int, color: Vec4f) - case clickable(action: () -> Void) + case interactable(handleInteraction: (Interaction) -> Bool) case sprite(GUISpriteDescriptor) /// Fills the renderable with the given background color. Goes behind /// any children that the renderable may have. @@ -232,22 +274,23 @@ public indirect enum GUIElement { } // Returns true if the click was handled by the renderable or any of its children. - public func handleClick(at position: Vec2i) -> Bool { + public func handleInteraction(_ interaction: Interaction, at position: Vec2i) -> Bool { guard Self.isHit(position, inBoxAt: relativePosition, ofSize: size) else { return false } switch content { - case let .clickable(action): - action() - return true + case let .interactable(handleInteraction): + if handleInteraction(interaction) { + return true + } case .text, .sprite, .background, .item, nil: break } let relativeClickPosition = position &- relativePosition for renderable in children.reversed() { - if renderable.handleClick(at: relativeClickPosition) { + if renderable.handleInteraction(interaction, at: relativeClickPosition) { return true } } @@ -300,7 +343,7 @@ public indirect enum GUIElement { let text = message.content.toText(with: locale) return GUIElement.text(text, wrap: wrap) .resolveConstraints(availableSize: availableSize, font: font, locale: locale) - case let .clickable(label, action): + case let .interactable(label, handleInteraction): let child = label.resolveConstraints( availableSize: availableSize, font: font, @@ -308,7 +351,7 @@ public indirect enum GUIElement { ) relativePosition = .zero size = child.size - content = .clickable(action: action) + content = .interactable(handleInteraction: handleInteraction) children = [child] case let .sprite(sprite): let descriptor = sprite.descriptor diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index e87cacaf..b0277ec6 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -182,31 +182,46 @@ public class InGameGUI { } return GUIElement.stack { + GUIElement.spacer(width: 0, height: 0) + .expand() + .background(Vec4f(0, 0, 0, 0.702)) + .onClick { + if let stack = state.mouseItemStack { + Self.dropItem(slot: nil, wholeStack: true, mouseItemStack: &state.mouseItemStack, inventory, connection) + } + } + .onRightClick { + // TODO: Figure out why the server is respecting this (pretty certain that we're sending + // the ClickWindowPacket with the `dropStack(slot: nil)` action, which should be correct??) + if let stack = state.mouseItemStack { + Self.dropItem(slot: nil, wholeStack: false, mouseItemStack: &state.mouseItemStack, inventory, connection) + } + } + GUIElement.stack { - GUIElement.sprite(.inventory) + // Has a dummy click handler to block clicks within the inventory from propagating to the background + GUIElement.sprite(.inventory).onClick {} - inventoryGrid(inventory, connection, state, area: .armor) + inventoryGrid(inventory, game, connection, state, area: .armor) .positionInParent(8, 8) - inventoryGrid(inventory, connection, state, area: .offHand) + inventoryGrid(inventory, game, connection, state, area: .offHand) .positionInParent(77, 62) - inventoryGrid(inventory, connection, state, area: .craftingInput) + inventoryGrid(inventory, game, connection, state, area: .craftingInput) .positionInParent(98, 18) - inventoryGrid(inventory, connection, state, area: .craftingResult) + inventoryGrid(inventory, game, connection, state, area: .craftingResult) .positionInParent(154, 28) - inventoryGrid(inventory, connection, state, area: .main) + inventoryGrid(inventory, game, connection, state, area: .main) .positionInParent(8, 84) - inventoryGrid(inventory, connection, state, area: .hotbar) + inventoryGrid(inventory, game, connection, state, area: .hotbar) .positionInParent(8, 142) } .size(GUISprite.inventory.descriptor.size) .center() - .expand() - .background(Vec4f(0, 0, 0, 0.702)) if let mouseItemStack = state.mouseItemStack { inventorySlot(Slot(mouseItemStack)) @@ -217,6 +232,7 @@ public class InGameGUI { public func inventoryGrid( _ inventory: PlayerInventory, + _ game: Game, _ connection: ServerConnection?, _ state: GUIStateStorage, area: PlayerInventory.Area @@ -235,13 +251,63 @@ public class InGameGUI { clickedItem: Slot(state.mouseItemStack) )) } catch { - log.warning("Failed to send click window packet for inventory interaction: \(error)") + log.warning("Failed to send click window packet for inventory left click: \(error)") + } + } + .onHoverKeyPress { event in + guard event.input == .dropItem else { + return false + } + + guard inventory.slots[index].stack?.count ?? 0 != 0 else { + return true } + + let inputState = game.accessInputState(acquireLock: false, action: identity) + let wholeStack = inputState.keys.contains(where: \.isControl) + Self.dropItem(slot: index, wholeStack: wholeStack, mouseItemStack: &state.mouseItemStack, inventory, connection) + + return true } } } } + public static func dropItem( + slot: Int?, + wholeStack: Bool, + mouseItemStack: inout ItemStack?, + _ inventory: PlayerInventory, + _ connection: ServerConnection? + ) { + let clickedItem = slot.map { inventory.slots[$0] } ?? Slot(mouseItemStack) + + let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 + if let index = slot { + inventory.slots[index].stack?.count -= dropCount + if inventory.slots[index].stack?.count == 0 { + inventory.slots[index].stack = nil + } + } else { + mouseItemStack?.count -= dropCount + if mouseItemStack?.count == 0 { + mouseItemStack = nil + } + } + + let index = slot.map(Int16.init) + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(PlayerInventory.windowId), + actionId: 0, + action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for item drop: \(error)") + } + } + public func inventorySlot(_ slot: Slot) -> GUIElement { // TODO: Make if blocks layout transparent (their children should be treated as children of the parent block) if let stack = slot.stack { diff --git a/Sources/Core/Sources/Input/Input.swift b/Sources/Core/Sources/Input/Input.swift index 6e5f5719..a0c1fca3 100644 --- a/Sources/Core/Sources/Input/Input.swift +++ b/Sources/Core/Sources/Input/Input.swift @@ -18,6 +18,7 @@ public enum Input: String, Codable, CaseIterable { case toggleInventory case changePerspective case performGPUFrameCapture + case dropItem case slot1 case slot2 case slot3 @@ -67,6 +68,8 @@ public enum Input: String, Codable, CaseIterable { return "Change Perspective" case .performGPUFrameCapture: return "Perform GPU trace" + case .dropItem: + return "Drop Selected Item" case .slot1: return "Slot 1" case .slot2: diff --git a/Sources/Core/Sources/Input/Key.swift b/Sources/Core/Sources/Input/Key.swift index 718d1bc3..767a322f 100644 --- a/Sources/Core/Sources/Input/Key.swift +++ b/Sources/Core/Sources/Input/Key.swift @@ -128,6 +128,26 @@ public enum Key: CustomStringConvertible, Hashable { case otherMouseButton(Int) + /// Whether the key is a control key. + public var isControl: Bool { + self == .leftControl || self == .rightControl + } + + /// Whether the key is a command key. + public var isCommand: Bool { + self == .leftCommand || self == .rightCommand + } + + /// Whether the key is a shift key. + public var isShift: Bool { + self == .leftShift || self == .rightShift + } + + /// Whether the key is an option key. + public var isOption: Bool { + self == .leftOption || self == .rightOption + } + /// The key's display name. public var description: String { name.display diff --git a/Sources/Core/Sources/Input/Keymap.swift b/Sources/Core/Sources/Input/Keymap.swift index 45159a17..489471c0 100644 --- a/Sources/Core/Sources/Input/Keymap.swift +++ b/Sources/Core/Sources/Input/Keymap.swift @@ -17,6 +17,7 @@ public struct Keymap { .toggleDebugHUD: .f3, .toggleInventory: .e, .changePerspective: .f5, + .dropItem: .q, .slot1: .one, .slot2: .two, .slot3: .three, diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift index 19c905b2..03553aca 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift @@ -17,8 +17,10 @@ public struct ClickWindowPacket: ServerboundPacket { case shiftRightClick(slot: Int16) case numberKey(slot: Int16, number: Int8) case middleClick(slot: Int16) - case drop(slot: Int16) - case controlDrop(slot: Int16) + /// If slot is nil, drop item from the stack currently getting moved by the mouse. + case dropOne(slot: Int16?) + /// If slot is nil, drop the stack currently getting moved by the mouse. + case dropStack(slot: Int16?) case leftClickOutsideInventory case rightClickOutsideInventory case startLeftDrag @@ -46,10 +48,12 @@ public struct ClickWindowPacket: ServerboundPacket { return (2, number, slot) case let .middleClick(slot): return (3, 2, slot) - case let .drop(slot): - return (4, 0, slot) - case let .controlDrop(slot): - return (4, 1, slot) + case let .dropOne(slot): + // -1 indicates that the slot attached to the mouse cursor is to be used + return (4, 0, slot ?? -1) + case let .dropStack(slot): + // -1 indicates that the slot attached to the mouse cursor is to be used + return (4, 1, slot ?? -1) case .leftClickOutsideInventory: return (4, 0, nil) case .rightClickOutsideInventory: From 08102569caf0095dc6c3ec07f0ff90f9028c19a9 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 11:35:23 +1000 Subject: [PATCH 31/84] Implement inventory slot right clicking (for taking half, putting one or swapping depending on context) --- Sources/Core/Sources/GUI/InGameGUI.swift | 67 +++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index b0277ec6..67791bb4 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -242,7 +242,28 @@ public class InGameGUI { let index = area.startIndex + y * area.width + x inventorySlot(inventory.slots[index]) .onClick { - swap(&inventory.slots[index].stack, &state.mouseItemStack) + if var slotStack = inventory.slots[index].stack, + var mouseStack = state.mouseItemStack, + slotStack.itemId == mouseStack.itemId + { + guard + let item = RegistryStore.shared.itemRegistry.item(withId: slotStack.itemId) + else { + log.warning("Failed to get maximum stack size for item with id '\(slotStack.itemId)'") + return + } + let total = slotStack.count + mouseStack.count + slotStack.count = min(total, item.maximumStackSize) + inventory.slots[index].stack = slotStack + if slotStack.count == total { + state.mouseItemStack = nil + } else { + mouseStack.count = total - slotStack.count + state.mouseItemStack = mouseStack + } + } else { + swap(&inventory.slots[index].stack, &state.mouseItemStack) + } do { try connection?.sendPacket(ClickWindowPacket( windowId: UInt8(PlayerInventory.windowId), @@ -254,6 +275,50 @@ public class InGameGUI { log.warning("Failed to send click window packet for inventory left click: \(error)") } } + .onRightClick { + if var stack = inventory.slots[index].stack, state.mouseItemStack == nil { + let total = stack.count + var takenStack = stack + stack.count = total / 2 + takenStack.count = total - stack.count + state.mouseItemStack = takenStack + if stack.count == 0 { + inventory.slots[index].stack = nil + } else { + inventory.slots[index].stack = stack + } + } else if var stack = state.mouseItemStack, inventory.slots[index].stack == nil { + stack.count -= 1 + inventory.slots[index].stack = ItemStack(itemId: stack.itemId, itemCount: 1) + if stack.count == 0 { + state.mouseItemStack = nil + } else { + state.mouseItemStack = stack + } + } else if let slotStack = inventory.slots[index].stack, + let mouseStack = state.mouseItemStack, + slotStack.itemId == mouseStack.itemId + { + inventory.slots[index].stack?.count += 1 + state.mouseItemStack?.count -= 1 + if state.mouseItemStack?.count == 0 { + state.mouseItemStack = nil + } + } else { + swap(&inventory.slots[index].stack, &state.mouseItemStack) + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(PlayerInventory.windowId), + actionId: 0, + action: .rightClick(slot: Int16(index)), + clickedItem: Slot(state.mouseItemStack) + )) + } catch { + log.warning("Failed to send click window packet for inventory right click: \(error)") + } + } .onHoverKeyPress { event in guard event.input == .dropItem else { return false From a1d708750039661a9edb8affec941f6ffb46a26b Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 12:03:56 +1000 Subject: [PATCH 32/84] Implement hotbar slot key shortcuts for swapping inventory items --- Sources/Core/Sources/GUI/InGameGUI.swift | 37 ++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 67791bb4..252d3c26 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -186,14 +186,14 @@ public class InGameGUI { .expand() .background(Vec4f(0, 0, 0, 0.702)) .onClick { - if let stack = state.mouseItemStack { + if state.mouseItemStack != nil { Self.dropItem(slot: nil, wholeStack: true, mouseItemStack: &state.mouseItemStack, inventory, connection) } } .onRightClick { - // TODO: Figure out why the server is respecting this (pretty certain that we're sending + // TODO: Figure out why the server isn't respecting this (pretty certain that we're sending // the ClickWindowPacket with the `dropStack(slot: nil)` action, which should be correct??) - if let stack = state.mouseItemStack { + if state.mouseItemStack != nil { Self.dropItem(slot: nil, wholeStack: false, mouseItemStack: &state.mouseItemStack, inventory, connection) } } @@ -242,6 +242,7 @@ public class InGameGUI { let index = area.startIndex + y * area.width + x inventorySlot(inventory.slots[index]) .onClick { + let clickedItem = inventory.slots[index] if var slotStack = inventory.slots[index].stack, var mouseStack = state.mouseItemStack, slotStack.itemId == mouseStack.itemId @@ -269,13 +270,14 @@ public class InGameGUI { windowId: UInt8(PlayerInventory.windowId), actionId: 0, action: .leftClick(slot: Int16(index)), - clickedItem: Slot(state.mouseItemStack) + clickedItem: clickedItem )) } catch { log.warning("Failed to send click window packet for inventory left click: \(error)") } } .onRightClick { + let clickedItem = inventory.slots[index] if var stack = inventory.slots[index].stack, state.mouseItemStack == nil { let total = stack.count var takenStack = stack @@ -313,7 +315,7 @@ public class InGameGUI { windowId: UInt8(PlayerInventory.windowId), actionId: 0, action: .rightClick(slot: Int16(index)), - clickedItem: Slot(state.mouseItemStack) + clickedItem: clickedItem )) } catch { log.warning("Failed to send click window packet for inventory right click: \(error)") @@ -332,6 +334,31 @@ public class InGameGUI { let wholeStack = inputState.keys.contains(where: \.isControl) Self.dropItem(slot: index, wholeStack: wholeStack, mouseItemStack: &state.mouseItemStack, inventory, connection) + return true + } + .onHoverKeyPress { event in + let slotInputs: [Input] = [.slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9] + guard let input = event.input, let hotBarSlot = slotInputs.firstIndex(of: input) else { + return false + } + + let clickedItem = inventory.slots[index] + let hotBarSlotIndex = PlayerInventory.Area.hotbar.startIndex + hotBarSlot + if hotBarSlotIndex != index { + inventory.slots.swapAt(index, hotBarSlotIndex) + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(PlayerInventory.windowId), + actionId: 0, + action: .numberKey(slot: Int16(index), number: Int8(hotBarSlot)), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for inventory right click: \(error)") + } + return true } } From ff18fbabbfbdb15e90f1213a0dbac83f85481270 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 12:17:04 +1000 Subject: [PATCH 33/84] Implement input handling for dropping hotbar items while not in inventory --- .../ECS/Systems/PlayerInputSystem.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 1a639598..57f4aed2 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -111,6 +111,29 @@ public final class PlayerInputSystem: System { inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 1) % 9 case .previousSlot: inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 8) % 9 + case .dropItem: + let slotIndex = PlayerInventory.Area.hotbar.startIndex + inventory.selectedHotbarSlot + guard var stack = inventory.slots[slotIndex].stack else { + break + } + let clickedItem = stack + stack.count -= 1 + if stack.count == 0 { + inventory.slots[slotIndex].stack = nil + } else { + inventory.slots[slotIndex].stack = stack + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(PlayerInventory.windowId), + actionId: 0, + action: .dropOne(slot: Int16(slotIndex)), + clickedItem: inventory.slots[slotIndex] + )) + } catch { + log.warning("Failed to send packet for dropping item: \(error)") + } case .place, .destroy: if inventory.hotbar[inventory.selectedHotbarSlot].stack != nil { try connection?.sendPacket(UseItemPacket(hand: .mainHand)) From 53d7115e31ff9adf0245d7cbf5d56316e50d07a9 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 19:22:41 +1000 Subject: [PATCH 34/84] Update inventory GUI code to be generic enough to handle other sorts of windows too --- .../ECS/Components/PlayerInventory.swift | 96 +++++----- .../ECS/Systems/PlayerInputSystem.swift | 8 +- Sources/Core/Sources/GUI/InGameGUI.swift | 170 +++++++++++------- Sources/Core/Sources/GUI/WindowArea.swift | 12 ++ Sources/Core/Sources/GUI/WindowType.swift | 40 +++++ Sources/Core/Sources/Util/ArrayBinding.swift | 29 +++ Sources/Core/Sources/Util/Dictionary.swift | 7 + 7 files changed, 245 insertions(+), 117 deletions(-) create mode 100644 Sources/Core/Sources/GUI/WindowArea.swift create mode 100644 Sources/Core/Sources/GUI/WindowType.swift create mode 100644 Sources/Core/Sources/Util/ArrayBinding.swift diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index af703e20..217775d1 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -8,9 +8,9 @@ public class PlayerInventory: Component { public static let windowId = 0 /// The index of the crafting result slot. - public static let craftingResultIndex = Area.craftingResult.startIndex + public static let craftingResultIndex = craftingResultArea.startIndex /// The index of the player's off-hand slot. - public static let offHandIndex = Area.offHand.startIndex + public static let offHandIndex = offHandArea.startIndex /// The inventory's contents. public var slots: [Slot] @@ -21,51 +21,51 @@ public class PlayerInventory: Component { /// ``ClickWindowPacket``). var nextActionId = 0 - public struct Area { - public var startIndex: Int - public var width: Int - public var height: Int - - public static let main = Area( - startIndex: 9, - width: 9, - height: 3 - ) - - public static let hotbar = Area( - startIndex: 36, - width: 9, - height: 1 - ) - - public static let craftingInput = Area( - startIndex: 1, - width: 2, - height: 2 - ) - - public static let craftingResult = Area( - startIndex: 0, - width: 1, - height: 1 - ) - - public static let armor = Area( - startIndex: 5, - width: 1, - height: 4 - ) - - public static let offHand = Area( - startIndex: 45, - width: 1, - height: 1 - ) - } + public static let mainArea = WindowArea( + startIndex: 9, + width: 9, + height: 3, + position: Vec2i(8, 84) + ) + + public static let hotbarArea = WindowArea( + startIndex: 36, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + + public static let craftingInputArea = WindowArea( + startIndex: 1, + width: 2, + height: 2, + position: Vec2i(98, 18) + ) + + public static let craftingResultArea = WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(154, 28) + ) + + public static let armorArea = WindowArea( + startIndex: 5, + width: 1, + height: 4, + position: Vec2i(8, 8) + ) + + public static let offHandArea = WindowArea( + startIndex: 45, + width: 1, + height: 1, + position: Vec2i(77, 62) + ) /// The inventory's hotbar. public var hotbar: [Slot] { - return slots(for: .hotbar)[0] + return slots(for: Self.hotbarArea)[0] } /// The result slot of the inventory's crafting area. @@ -75,7 +75,7 @@ public class PlayerInventory: Component { /// The armor slots. public var armorSlots: [Slot] { - return slots(for: .armor).map { row in + return slots(for: Self.armorArea).map { row in row[0] } } @@ -98,9 +98,9 @@ public class PlayerInventory: Component { } /// Gets the slots associated with a particular area of the inventory. - /// - Returns: The rows of the area, e.g. ``Area/hotbar`` results in a single row, and - /// ``Area/armor`` results in 4 rows containing 1 element each. - public func slots(for area: Area) -> [[Slot]] { + /// - Returns: The rows of the area, e.g. ``PlayerInventory/hotbarArea`` results in a single row, and + /// ``PlayerInventory/armorArea`` results in 4 rows containing 1 element each. + public func slots(for area: WindowArea) -> [[Slot]] { var rows: [[Slot]] = [] for y in 0.. GUIElement { - let gamemode = game.accessPlayer(acquireLock: false) { player in - player.gamemode.gamemode + let (gamemode, inventory) = game.accessPlayer(acquireLock: false) { player in + (player.gamemode.gamemode, player.inventory) } if state.showHUD { @@ -52,7 +52,21 @@ public class InGameGUI { chat(state: state) if state.showInventory { - inventory(game: game, connection: connection, state: state) + window( + type: .inventory, + windowId: PlayerInventory.windowId, + slots: ArrayBinding( + get: { index in + inventory.slots[index] + }, + set: { index, newValue in + inventory.slots[index] = newValue + } + ), + game: game, + connection: connection, + state: state + ) } } } else { @@ -172,11 +186,14 @@ public class InGameGUI { } } - public func inventory(game: Game, connection: ServerConnection?, state: GUIStateStorage) -> GUIElement { - let inventory = game.accessPlayer(acquireLock: false) { player in - player.inventory - } - + public func window( + type windowType: WindowType, + windowId: Int, + slots: ArrayBinding, + game: Game, + connection: ServerConnection?, + state: GUIStateStorage + ) -> GUIElement { let mousePosition = game.accessInputState(acquireLock: false) { inputState in Vec2i(inputState.mousePosition / state.drawableScalingFactor) } @@ -187,40 +204,48 @@ public class InGameGUI { .background(Vec4f(0, 0, 0, 0.702)) .onClick { if state.mouseItemStack != nil { - Self.dropItem(slot: nil, wholeStack: true, mouseItemStack: &state.mouseItemStack, inventory, connection) + Self.dropItem( + slot: nil, + wholeStack: true, + mouseItemStack: &state.mouseItemStack, + windowId: windowId, + slots: slots, + connection: connection + ) } } .onRightClick { // TODO: Figure out why the server isn't respecting this (pretty certain that we're sending // the ClickWindowPacket with the `dropStack(slot: nil)` action, which should be correct??) if state.mouseItemStack != nil { - Self.dropItem(slot: nil, wholeStack: false, mouseItemStack: &state.mouseItemStack, inventory, connection) + Self.dropItem( + slot: nil, + wholeStack: false, + mouseItemStack: &state.mouseItemStack, + windowId: windowId, + slots: slots, + connection: connection + ) } } GUIElement.stack { // Has a dummy click handler to block clicks within the inventory from propagating to the background - GUIElement.sprite(.inventory).onClick {} - - inventoryGrid(inventory, game, connection, state, area: .armor) - .positionInParent(8, 8) - - inventoryGrid(inventory, game, connection, state, area: .offHand) - .positionInParent(77, 62) - - inventoryGrid(inventory, game, connection, state, area: .craftingInput) - .positionInParent(98, 18) - - inventoryGrid(inventory, game, connection, state, area: .craftingResult) - .positionInParent(154, 28) - - inventoryGrid(inventory, game, connection, state, area: .main) - .positionInParent(8, 84) - - inventoryGrid(inventory, game, connection, state, area: .hotbar) - .positionInParent(8, 142) + GUIElement.sprite(windowType.texture).onClick {} + + GUIElement.stack(elements: windowType.areas.map { area in + windowArea( + area, + slots, + windowId: windowId, + windowType: windowType, + game: game, + connection: connection, + state: state + ) + }) } - .size(GUISprite.inventory.descriptor.size) + .size(windowType.texture.descriptor.size) .center() if let mouseItemStack = state.mouseItemStack { @@ -230,20 +255,22 @@ public class InGameGUI { } } - public func inventoryGrid( - _ inventory: PlayerInventory, - _ game: Game, - _ connection: ServerConnection?, - _ state: GUIStateStorage, - area: PlayerInventory.Area + public func windowArea( + _ area: WindowArea, + _ slots: ArrayBinding, + windowId: Int, + windowType: WindowType, + game: Game, + connection: ServerConnection?, + state: GUIStateStorage ) -> GUIElement { GUIElement.forEach(in: 0.., + connection: ServerConnection? ) { - let clickedItem = slot.map { inventory.slots[$0] } ?? Slot(mouseItemStack) + let clickedItem = slot.map { slots[$0] } ?? Slot(mouseItemStack) let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 if let index = slot { - inventory.slots[index].stack?.count -= dropCount - if inventory.slots[index].stack?.count == 0 { - inventory.slots[index].stack = nil + slots[index].stack?.count -= dropCount + if slots[index].stack?.count == 0 { + slots[index].stack = nil } } else { mouseItemStack?.count -= dropCount @@ -390,7 +430,7 @@ public class InGameGUI { let index = slot.map(Int16.init) do { try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(PlayerInventory.windowId), + windowId: UInt8(windowId), actionId: 0, action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), clickedItem: clickedItem diff --git a/Sources/Core/Sources/GUI/WindowArea.swift b/Sources/Core/Sources/GUI/WindowArea.swift new file mode 100644 index 00000000..2964b633 --- /dev/null +++ b/Sources/Core/Sources/GUI/WindowArea.swift @@ -0,0 +1,12 @@ +/// An area of a GUI window; a grid of slots. Only handles areas where the rows +/// are stored one after another in the window's slot array. +public struct WindowArea { + /// Index of the first slot in the area. + public var startIndex: Int + /// Number of slots wide. + public var width: Int + /// Number of slots high. + public var height: Int + /// The position of the area within its window. + public var position: Vec2i +} diff --git a/Sources/Core/Sources/GUI/WindowType.swift b/Sources/Core/Sources/GUI/WindowType.swift new file mode 100644 index 00000000..a0906e0d --- /dev/null +++ b/Sources/Core/Sources/GUI/WindowType.swift @@ -0,0 +1,40 @@ +/// A type of GUI window (e.g. inventory, crafting table, chest, etc). Defines the layout +/// and which slots go where. +public struct WindowType { + public var id: Id + public var identifier: Identifier + public var texture: GUISprite + public var slotCount: Int + public var areas: [WindowArea] + + public enum Id: Hashable, Equatable { + /// Vanilla minecraft doesn't have the inventory window type in its registry + /// cause it gets special treatment, so we just give it its own category of id. + case inventory + case vanilla(Int) + } + + /// The player's inventory. + public static let inventory = WindowType( + id: .inventory, + identifier: Identifier(namespace: "minecraft", name: "inventory"), + texture: .inventory, + slotCount: 46, + areas: [ + PlayerInventory.mainArea, + PlayerInventory.hotbarArea, + PlayerInventory.craftingInputArea, + PlayerInventory.craftingResultArea, + PlayerInventory.armorArea, + PlayerInventory.offHandArea + ] + ) + + /// The window types understood by vanilla. + public static let types = [Id: Self]( + values: [ + inventory, + ], + keyedBy: \.id + ) +} diff --git a/Sources/Core/Sources/Util/ArrayBinding.swift b/Sources/Core/Sources/Util/ArrayBinding.swift new file mode 100644 index 00000000..4331ed47 --- /dev/null +++ b/Sources/Core/Sources/Util/ArrayBinding.swift @@ -0,0 +1,29 @@ +/// Similar idea to SwiftUI bindings, but for arrays. Allows prolonged mutable access to an array. +/// Made it a class so that you're not forced to unnecessarily always store the binding as a `var` +/// just cause you want to assign to it. That would be unnecessary because there's no actual mutation +/// happening to the binding and by nature it's meant to be an opaque two way binding. +public class ArrayBinding { + public let getElement: (_ index: Int) -> Element + public let setElement: (_ index: Int, _ value: Element) -> Void + + public init(get getElement: @escaping (Int) -> Element, set setElement: @escaping (Int, Element) -> Void) { + self.getElement = getElement + self.setElement = setElement + } + + public subscript(_ index: Int) -> Element { + get { + getElement(index) + } + set { + setElement(index, newValue) + } + } + + public func swapAt(_ firstIndex: Int, _ secondIndex: Int) { + let first = self[firstIndex] + let second = self[secondIndex] + self[firstIndex] = second + self[secondIndex] = first + } +} diff --git a/Sources/Core/Sources/Util/Dictionary.swift b/Sources/Core/Sources/Util/Dictionary.swift index e323f3cc..98b5e3a4 100644 --- a/Sources/Core/Sources/Util/Dictionary.swift +++ b/Sources/Core/Sources/Util/Dictionary.swift @@ -1,6 +1,13 @@ import Foundation extension Dictionary { + init(values: [Value], keyedBy key: KeyPath) { + self.init() + for value in values { + self[value[keyPath: key]] = value + } + } + mutating func mutatingEach(_ update: (Key, inout Value) -> Void) { for key in keys { update(key, &(self[key]!)) From 37bfb62713f7611264e49a0cf19c3d9563e80bb2 Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 28 May 2024 23:48:23 +1000 Subject: [PATCH 35/84] Support non-inventory windows with accompanying packets; just crafting tables and single-chests for now --- .../ECS/Components/PlayerInventory.swift | 63 +++++------ .../ECS/Systems/PlayerInputSystem.swift | 37 +++++- Sources/Core/Sources/GUI/GUISprite.swift | 12 ++ Sources/Core/Sources/GUI/InGameGUI.swift | 107 ++++++++---------- Sources/Core/Sources/GUI/Window.swift | 48 ++++++++ Sources/Core/Sources/GUI/WindowType.swift | 71 +++++++++++- Sources/Core/Sources/GUIState.swift | 8 +- .../CloseWindowClientboundPacket.swift | 18 +++ .../Play/Clientbound/OpenWindowPacket.swift | 28 ++++- .../Play/Clientbound/SetSlotPacket.swift | 10 +- .../Play/Clientbound/WindowItemsPacket.swift | 33 ++++-- .../Resources/GUI/GUITextureSlice.swift | 10 ++ 12 files changed, 326 insertions(+), 119 deletions(-) create mode 100644 Sources/Core/Sources/GUI/Window.swift diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index 217775d1..8c475641 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -1,6 +1,7 @@ import FirebladeECS -/// A component storing the player's inventory. +/// A component storing the player's inventory. Simply wraps a ``Window`` with some +/// inventory-specific properties and helper methods. public class PlayerInventory: Component { /// The number of slots in a player inventory (including armor and off hand). public static let slotCount = 46 @@ -12,15 +13,6 @@ public class PlayerInventory: Component { /// The index of the player's off-hand slot. public static let offHandIndex = offHandArea.startIndex - /// The inventory's contents. - public var slots: [Slot] - /// The player's currently selected hotbar slot. - public var selectedHotbarSlot: Int - - /// The action id to use for the next action performed on the inventory (used when sending - /// ``ClickWindowPacket``). - var nextActionId = 0 - public static let mainArea = WindowArea( startIndex: 9, width: 9, @@ -63,53 +55,54 @@ public class PlayerInventory: Component { position: Vec2i(77, 62) ) - /// The inventory's hotbar. + /// The inventory's window; contains the underlying slots. + public var window: Window + /// The player's currently selected hotbar slot. + public var selectedHotbarSlot: Int + + /// The inventory's main 3 row 9 column area. + public var mainArea: [[Slot]] { + window.slots(for: Self.mainArea) + } + + /// The inventory's crafting input slots. + public var craftingInputs: [[Slot]] { + window.slots(for: Self.craftingInputArea) + } + + // TODO: Choose a casing for hotbar and stick to it (hotbar vs hotBar) + /// The inventory's hotbar slots. public var hotbar: [Slot] { - return slots(for: Self.hotbarArea)[0] + window.slots(for: Self.hotbarArea)[0] } /// The result slot of the inventory's crafting area. public var craftingResult: Slot { - return slots[Self.craftingResultIndex] + window.slots[Self.craftingResultIndex] } /// The armor slots. public var armorSlots: [Slot] { - return slots(for: Self.armorArea).map { row in + window.slots(for: Self.armorArea).map { row in row[0] } } /// The off-hand slot. public var offHand: Slot { - return slots[Self.offHandIndex] + window.slots[Self.offHandIndex] } /// 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``. public init(slots: [Slot]? = nil, selectedHotbarSlot: Int = 0) { - if let count = slots?.count { - assert(count == Self.slotCount) - } + window = Window( + id: Self.windowId, + type: .inventory, + slots: slots + ) - self.slots = slots ?? Array(repeating: Slot(), count: Self.slotCount) self.selectedHotbarSlot = selectedHotbarSlot } - - /// Gets the slots associated with a particular area of the inventory. - /// - Returns: The rows of the area, e.g. ``PlayerInventory/hotbarArea`` results in a single row, and - /// ``PlayerInventory/armorArea`` results in 4 rows containing 1 element each. - public func slots(for area: WindowArea) -> [[Slot]] { - var rows: [[Slot]] = [] - for y in 0.. Bool { + guard let window = guiState.window else { + return false + } + + if event.key == .escape || event.input == .toggleInventory { + eventBus.dispatch(CaptureCursorEvent()) + guiState.window = nil + try? connection?.sendPacket(CloseWindowServerboundPacket( + windowId: UInt8(window.id) + )) + } + + return true + } + /// Updates the direction which the player is looking. /// - Parameters: /// - inputState: The current input state. diff --git a/Sources/Core/Sources/GUI/GUISprite.swift b/Sources/Core/Sources/GUI/GUISprite.swift index 98f21707..ec5cf31d 100644 --- a/Sources/Core/Sources/GUI/GUISprite.swift +++ b/Sources/Core/Sources/GUI/GUISprite.swift @@ -15,6 +15,12 @@ public enum GUISprite { case xpBarBackground case xpBarForeground case inventory + case craftingTable + /// If positioned directly above ``GUISprite/singleChestBottomHalf`` it forms + /// the background for a single chest window. The way the texture is made forces + /// these to be separate sprites. + case singleChestTopHalf + case singleChestBottomHalf /// The descriptor for the sprite. public var descriptor: GUISpriteDescriptor { @@ -49,6 +55,12 @@ public enum GUISprite { return GUISpriteDescriptor(slice: .icons, position: [0, 69], size: [182, 5]) case .inventory: return GUISpriteDescriptor(slice: .inventory, position: [0, 0], size: [176, 166]) + case .craftingTable: + return GUISpriteDescriptor(slice: .craftingTable, position: [0, 0], size: [176, 166]) + case .singleChestTopHalf: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 71]) + case .singleChestBottomHalf: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 125], size: [176, 97]) } } } diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 7474394b..bdc48a74 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -53,16 +53,14 @@ public class InGameGUI { if state.showInventory { window( - type: .inventory, - windowId: PlayerInventory.windowId, - slots: ArrayBinding( - get: { index in - inventory.slots[index] - }, - set: { index, newValue in - inventory.slots[index] = newValue - } - ), + window: inventory.window, + game: game, + connection: connection, + state: state + ) + } else if let window = state.window { + self.window( + window: window, game: game, connection: connection, state: state @@ -187,9 +185,7 @@ public class InGameGUI { } public func window( - type windowType: WindowType, - windowId: Int, - slots: ArrayBinding, + window: Window, game: Game, connection: ServerConnection?, state: GUIStateStorage @@ -208,8 +204,7 @@ public class InGameGUI { slot: nil, wholeStack: true, mouseItemStack: &state.mouseItemStack, - windowId: windowId, - slots: slots, + window: window, connection: connection ) } @@ -222,8 +217,7 @@ public class InGameGUI { slot: nil, wholeStack: false, mouseItemStack: &state.mouseItemStack, - windowId: windowId, - slots: slots, + window: window, connection: connection ) } @@ -231,21 +225,18 @@ public class InGameGUI { GUIElement.stack { // Has a dummy click handler to block clicks within the inventory from propagating to the background - GUIElement.sprite(windowType.texture).onClick {} + window.type.background.onClick {} - GUIElement.stack(elements: windowType.areas.map { area in + GUIElement.stack(elements: window.type.areas.map { area in windowArea( area, - slots, - windowId: windowId, - windowType: windowType, + window, game: game, connection: connection, state: state ) }) } - .size(windowType.texture.descriptor.size) .center() if let mouseItemStack = state.mouseItemStack { @@ -257,9 +248,7 @@ public class InGameGUI { public func windowArea( _ area: WindowArea, - _ slots: ArrayBinding, - windowId: Int, - windowType: WindowType, + _ window: Window, game: Game, connection: ServerConnection?, state: GUIStateStorage @@ -267,10 +256,10 @@ public class InGameGUI { GUIElement.forEach(in: 0.., + window: Window, connection: ServerConnection? ) { - let clickedItem = slot.map { slots[$0] } ?? Slot(mouseItemStack) + let clickedItem = slot.map { window.slots[$0] } ?? Slot(mouseItemStack) let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 if let index = slot { - slots[index].stack?.count -= dropCount - if slots[index].stack?.count == 0 { - slots[index].stack = nil + window.slots[index].stack?.count -= dropCount + if window.slots[index].stack?.count == 0 { + window.slots[index].stack = nil } } else { mouseItemStack?.count -= dropCount @@ -430,8 +417,8 @@ public class InGameGUI { let index = slot.map(Int16.init) do { try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(windowId), - actionId: 0, + windowId: UInt8(window.id), + actionId: Int16(window.generateActionId()), action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), clickedItem: clickedItem )) diff --git a/Sources/Core/Sources/GUI/Window.swift b/Sources/Core/Sources/GUI/Window.swift new file mode 100644 index 00000000..c4a3311a --- /dev/null +++ b/Sources/Core/Sources/GUI/Window.swift @@ -0,0 +1,48 @@ +/// The slots behind a GUI window. Only a `class` because of the way it gets consumed by +/// ``InGameGUI``, it gets too unergonomic short of wrapping it in a ``Box`` (which +/// gets a little cumbersome sometimes, purely because we have to give inventory +/// special treatment while also wanting to work generically over mutable references to +/// windows). +public class Window { + public var id: Int + public var type: WindowType + public var slots: [Slot] + + /// The action id to use for the next action performed on the inventory (used when sending + /// ``ClickWindowPacket``). + private var nextActionId = 0 + + public init(id: Int, type: WindowType, slots: [Slot]? = nil) { + if let count = slots?.count { + precondition(count == type.slotCount) + } + + self.id = id + self.type = type + self.slots = slots ?? Array(repeating: Slot(), count: type.slotCount) + self.nextActionId = 0 + } + + /// Returns a unique window action id (counts up from 0 like vanilla does). + public func generateActionId() -> Int { + let id = nextActionId + nextActionId += 1 + return id + } + + /// Gets the slots associated with a particular area of the window. + /// - Returns: The rows of the area, e.g. ``PlayerInventory/hotbarArea`` results in a single row, and + /// ``PlayerInventory/armorArea`` results in 4 rows containing 1 element each. + public func slots(for area: WindowArea) -> [[Slot]] { + var rows: [[Slot]] = [] + for y in 0.. Date: Wed, 29 May 2024 00:50:41 +1000 Subject: [PATCH 36/84] Move window interaction handling code into Window type to streamline GUI code and allow reuse from PlayerInputSystem --- Readme.md | 8 +- Roadmap.md | 4 +- .../ECS/Systems/PlayerInputSystem.swift | 68 ++---- Sources/Core/Sources/GUI/InGameGUI.swift | 183 +--------------- Sources/Core/Sources/GUI/Window.swift | 201 +++++++++++++++++- .../CloseWindowClientboundPacket.swift | 7 +- 6 files changed, 235 insertions(+), 236 deletions(-) diff --git a/Readme.md b/Readme.md index 4ee18e5c..f39946d1 100644 --- a/Readme.md +++ b/Readme.md @@ -46,7 +46,7 @@ To view application logs, click `View > Logs` in the menu bar while Delta Client ### Building from source -To build Delta Client you'll first need to install Xcode 14 and the latest version of [Swift Bundler](https://github.com/stackotter/swift-bundler). Please note that using Xcode 13 is ok but you may run into some weird memory corruption issues, so test with Xcode 14 before assuming that it's a Delta Client bug. Once you've installed the requirements, run the following commands in terminal; +To build Delta Client you'll first need to install Xcode 14+ and the latest version of [Swift Bundler](https://github.com/stackotter/swift-bundler). Please note that using Xcode 13 is ok but you may run into some weird memory corruption issues, so test with Xcode 14+ before assuming that it's a Delta Client bug. Once you've installed the requirements, run the following commands in terminal; ```sh # Clone Delta Client @@ -111,9 +111,9 @@ Not every version will be perfectly supported but I will try and have the most p - [x] Health, hunger and experience - [x] Hotbar - [ ] Inventory - - [ ] Basic inventory - - [ ] Basic crafting - - [ ] Inventory actions + - [x] Basic inventory + - [x] Basic crafting + - [x] Inventory actions - [ ] Using recipe blocks (like crafting tables and stuff) - [ ] Creative inventory - [ ] Sound diff --git a/Roadmap.md b/Roadmap.md index 3fbfe6bb..660f9953 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -148,6 +148,6 @@ The demo versions were just proof-of-concepts. The alpha versions will still be - [x] hunger - [x] xp - [ ] bubbles -- [ ] item rendering +- [x] item rendering - [x] hotbar -- [ ] basic inventory (just for viewing) +- [x] basic inventory (just for viewing) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index d49a5952..17f4353b 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -53,8 +53,6 @@ public final class PlayerInputSystem: System { let guiState = nexus.single(GUIStateStorage.self).component let mousePosition = Vec2i(inputState.mousePosition / guiState.drawableScalingFactor) - // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) - let gui = game.compileGUI(withFont: font, locale: locale, connection: connection, guiState: guiState) // Handle non-movement inputs var isInputSuppressed: [Bool] = [] @@ -63,15 +61,23 @@ public final class PlayerInputSystem: System { // TODO: Formalize 'mouse targeted interactions are allowed', seems a bit hacky this way if !suppressInput && !guiState.movementAllowed { + // Recompute the gui everytime that we get it to handle a new interaction because e.g. if previous + // interactions within the same tick have closed the inventory and opened the chat, then the old + // gui would still be handling the input for the inventory. That's a bit contrived, but I'm sure + // other edge cases are possible, and recomputing the GUI is relatively cheap (and multiple inputs + // within a single tick should be uncommon anyway). + + // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) + let gui = game.compileGUI(withFont: font, locale: locale, connection: connection, guiState: guiState) suppressInput = gui.handleInteraction(.press(event), at: mousePosition) } if !suppressInput { - suppressInput = try handleChat(event, inputState, guiState) || handleInventory(event, guiState) + suppressInput = try handleChat(event, inputState, guiState) } if !suppressInput { - suppressInput = try handleWindow(event, guiState, connection) + suppressInput = try handleWindow(event, guiState, eventBus, connection) } if !suppressInput { @@ -89,10 +95,11 @@ public final class PlayerInputSystem: System { // 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 connection?.sendPacket(CloseWindowServerboundPacket(windowId: UInt8(PlayerInventory.windowId))) + try inventory.window.close(mouseStack: &guiState.mouseItemStack, eventBus: eventBus, connection: connection) + } else { + inputState.releaseAll() + eventBus.dispatch(ReleaseCursorEvent()) } - inputState.releaseAll() - eventBus.dispatch(ReleaseCursorEvent()) case .slot1: inventory.selectedHotbarSlot = 0 case .slot2: @@ -117,27 +124,7 @@ public final class PlayerInputSystem: System { inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 8) % 9 case .dropItem: let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot - let clickedSlot = inventory.window.slots[slotIndex] - guard var stack = clickedSlot.stack else { - break - } - stack.count -= 1 - if stack.count == 0 { - inventory.window.slots[slotIndex].stack = nil - } else { - inventory.window.slots[slotIndex].stack = stack - } - - do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(PlayerInventory.windowId), - actionId: 0, - action: .dropOne(slot: Int16(slotIndex)), - clickedItem: clickedSlot - )) - } catch { - log.warning("Failed to send packet for dropping item: \(error)") - } + inventory.window.dropItem(slotIndex, connection: connection) case .place, .destroy: if inventory.hotbar[inventory.selectedHotbarSlot].stack != nil { try connection?.sendPacket(UseItemPacket(hand: .mainHand)) @@ -295,39 +282,20 @@ 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, - _ guiState: GUIStateStorage - ) -> Bool { - guard guiState.showInventory else { - return false - } - - if event.key == .escape || event.input == .toggleInventory { - eventBus.dispatch(CaptureCursorEvent()) - guiState.showInventory = false - } - - return true - } - /// - Returns: Whether to suppress the input associated with the event or not. private func handleWindow( _ event: KeyPressEvent, _ guiState: GUIStateStorage, + _ eventBus: EventBus, _ connection: ServerConnection? - ) -> Bool { + ) throws -> Bool { guard let window = guiState.window else { return false } if event.key == .escape || event.input == .toggleInventory { - eventBus.dispatch(CaptureCursorEvent()) + try window.close(mouseStack: &guiState.mouseItemStack, eventBus: eventBus, connection: connection) guiState.window = nil - try? connection?.sendPacket(CloseWindowServerboundPacket( - windowId: UInt8(window.id) - )) } return true diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index bdc48a74..d6c8d59f 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -199,28 +199,12 @@ public class InGameGUI { .expand() .background(Vec4f(0, 0, 0, 0.702)) .onClick { - if state.mouseItemStack != nil { - Self.dropItem( - slot: nil, - wholeStack: true, - mouseItemStack: &state.mouseItemStack, - window: window, - connection: connection - ) - } + window.dropStackFromMouse(&state.mouseItemStack, connection: connection) } .onRightClick { // TODO: Figure out why the server isn't respecting this (pretty certain that we're sending // the ClickWindowPacket with the `dropStack(slot: nil)` action, which should be correct??) - if state.mouseItemStack != nil { - Self.dropItem( - slot: nil, - wholeStack: false, - mouseItemStack: &state.mouseItemStack, - window: window, - connection: connection - ) - } + window.dropItemFromMouse(&state.mouseItemStack, connection: connection) } GUIElement.stack { @@ -258,175 +242,26 @@ public class InGameGUI { let index = area.startIndex + y * area.width + x inventorySlot(window.slots[index]) .onClick { - let clickedItem = window.slots[index] - if var slotStack = window.slots[index].stack, - var mouseStack = state.mouseItemStack, - slotStack.itemId == mouseStack.itemId - { - guard - let item = RegistryStore.shared.itemRegistry.item(withId: slotStack.itemId) - else { - log.warning("Failed to get maximum stack size for item with id '\(slotStack.itemId)'") - return - } - let total = slotStack.count + mouseStack.count - slotStack.count = min(total, item.maximumStackSize) - window.slots[index].stack = slotStack - if slotStack.count == total { - state.mouseItemStack = nil - } else { - mouseStack.count = total - slotStack.count - state.mouseItemStack = mouseStack - } - } else { - swap(&window.slots[index].stack, &state.mouseItemStack) - } - do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(window.id), - actionId: Int16(window.generateActionId()), - action: .leftClick(slot: Int16(index)), - clickedItem: clickedItem - )) - } catch { - log.warning("Failed to send click window packet for inventory left click: \(error)") - } + window.leftClick(index, mouseStack: &state.mouseItemStack, connection: connection) } .onRightClick { - let clickedItem = window.slots[index] - if var stack = window.slots[index].stack, state.mouseItemStack == nil { - let total = stack.count - var takenStack = stack - stack.count = total / 2 - takenStack.count = total - stack.count - state.mouseItemStack = takenStack - if stack.count == 0 { - window.slots[index].stack = nil - } else { - window.slots[index].stack = stack - } - } else if var stack = state.mouseItemStack, window.slots[index].stack == nil { - stack.count -= 1 - window.slots[index].stack = ItemStack(itemId: stack.itemId, itemCount: 1) - if stack.count == 0 { - state.mouseItemStack = nil - } else { - state.mouseItemStack = stack - } - } else if let slotStack = window.slots[index].stack, - let mouseStack = state.mouseItemStack, - slotStack.itemId == mouseStack.itemId - { - window.slots[index].stack?.count += 1 - state.mouseItemStack?.count -= 1 - if state.mouseItemStack?.count == 0 { - state.mouseItemStack = nil - } - } else { - swap(&window.slots[index].stack, &state.mouseItemStack) - } - - do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(window.id), - actionId: Int16(window.generateActionId()), - action: .rightClick(slot: Int16(index)), - clickedItem: clickedItem - )) - } catch { - log.warning("Failed to send click window packet for inventory right click: \(error)") - } + window.rightClick(index, mouseStack: &state.mouseItemStack, connection: connection) } .onHoverKeyPress { event in - guard event.input == .dropItem else { - return false - } - - guard window.slots[index].stack?.count ?? 0 != 0 else { - return true - } - let inputState = game.accessInputState(acquireLock: false, action: identity) - let wholeStack = inputState.keys.contains(where: \.isControl) - Self.dropItem( - slot: index, - wholeStack: wholeStack, - mouseItemStack: &state.mouseItemStack, - window: window, + return window.pressKey( + over: index, + event: event, + mouseStack: &state.mouseItemStack, + inputState: inputState, connection: connection ) - - return true - } - .onHoverKeyPress { event in - guard window.type.id == .inventory else { - return false - } - - let slotInputs: [Input] = [.slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9] - guard let input = event.input, let hotBarSlot = slotInputs.firstIndex(of: input) else { - return false - } - - let clickedItem = window.slots[index] - let hotBarSlotIndex = PlayerInventory.hotbarArea.startIndex + hotBarSlot - if hotBarSlotIndex != index { - window.slots.swapAt(index, hotBarSlotIndex) - } - - do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(window.id), - actionId: Int16(window.generateActionId()), - action: .numberKey(slot: Int16(index), number: Int8(hotBarSlot)), - clickedItem: clickedItem - )) - } catch { - log.warning("Failed to send click window packet for inventory right click: \(error)") - } - - return true } } } .positionInParent(area.position) } - public static func dropItem( - slot: Int?, - wholeStack: Bool, - mouseItemStack: inout ItemStack?, - window: Window, - connection: ServerConnection? - ) { - let clickedItem = slot.map { window.slots[$0] } ?? Slot(mouseItemStack) - - let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 - if let index = slot { - window.slots[index].stack?.count -= dropCount - if window.slots[index].stack?.count == 0 { - window.slots[index].stack = nil - } - } else { - mouseItemStack?.count -= dropCount - if mouseItemStack?.count == 0 { - mouseItemStack = nil - } - } - - let index = slot.map(Int16.init) - do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(window.id), - actionId: Int16(window.generateActionId()), - action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), - clickedItem: clickedItem - )) - } catch { - log.warning("Failed to send click window packet for item drop: \(error)") - } - } - public func inventorySlot(_ slot: Slot) -> GUIElement { // TODO: Make if blocks layout transparent (their children should be treated as children of the parent block) if let stack = slot.stack { diff --git a/Sources/Core/Sources/GUI/Window.swift b/Sources/Core/Sources/GUI/Window.swift index c4a3311a..ec12429c 100644 --- a/Sources/Core/Sources/GUI/Window.swift +++ b/Sources/Core/Sources/GUI/Window.swift @@ -24,10 +24,10 @@ public class Window { } /// Returns a unique window action id (counts up from 0 like vanilla does). - public func generateActionId() -> Int { + private func generateActionId() -> Int16 { let id = nextActionId nextActionId += 1 - return id + return Int16(id) } /// Gets the slots associated with a particular area of the window. @@ -45,4 +45,201 @@ public class Window { } return rows } + + public func leftClick(_ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection?) { + let clickedItem = slots[slotIndex] + if var slotStack = slots[slotIndex].stack, + var mouseStackCopy = mouseStack, + slotStack.itemId == mouseStackCopy.itemId + { + guard + let item = RegistryStore.shared.itemRegistry.item(withId: slotStack.itemId) + else { + log.warning("Failed to get maximum stack size for item with id '\(slotStack.itemId)'") + return + } + let total = slotStack.count + mouseStackCopy.count + slotStack.count = min(total, item.maximumStackSize) + slots[slotIndex].stack = slotStack + if slotStack.count == total { + mouseStack = nil + } else { + mouseStackCopy.count = total - slotStack.count + mouseStack = mouseStackCopy + } + } else { + swap(&slots[slotIndex].stack, &mouseStack) + } + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .leftClick(slot: Int16(slotIndex)), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for inventory left click: \(error)") + } + } + + public func rightClick(_ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection?) { + let clickedItem = slots[slotIndex] + if var stack = slots[slotIndex].stack, mouseStack == nil { + let total = stack.count + var takenStack = stack + stack.count = total / 2 + takenStack.count = total - stack.count + mouseStack = takenStack + if stack.count == 0 { + slots[slotIndex].stack = nil + } else { + slots[slotIndex].stack = stack + } + } else if var stack = mouseStack, slots[slotIndex].stack == nil { + stack.count -= 1 + slots[slotIndex].stack = ItemStack(itemId: stack.itemId, itemCount: 1) + if stack.count == 0 { + mouseStack = nil + } else { + mouseStack = stack + } + } else if let slotStack = slots[slotIndex].stack, + slotStack.itemId == mouseStack?.itemId + { + slots[slotIndex].stack?.count += 1 + mouseStack?.count -= 1 + if mouseStack?.count == 0 { + mouseStack = nil + } + } else { + swap(&slots[slotIndex].stack, &mouseStack) + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .rightClick(slot: Int16(slotIndex)), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for inventory right click: \(error)") + } + } + + /// - Returns: `true` if the event was handled, otherwise `false` (indicating that the next relevant GUI + /// element should given the event, and so on). + public func pressKey( + over slotIndex: Int, + event: KeyPressEvent, + mouseStack: inout ItemStack?, + inputState: InputState, + connection: ServerConnection? + ) -> Bool { + guard let input = event.input else { + return false + } + + let slotInputs: [Input] = [.slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9] + + if input == .dropItem { + guard mouseStack == nil else { + return true + } + + guard slots[slotIndex].stack?.count ?? 0 != 0 else { + return true + } + + let dropWholeStack = inputState.keys.contains(where: \.isControl) + if dropWholeStack { + dropStack(slotIndex, connection: connection) + } else { + dropItem(slotIndex, connection: connection) + } + } else if let hotBarSlot = slotInputs.firstIndex(of: input) { + guard mouseStack == nil else { + return true + } + + let clickedItem = slots[slotIndex] + let hotBarSlotslotIndex = PlayerInventory.hotbarArea.startIndex + hotBarSlot + if hotBarSlotslotIndex != slotIndex { + slots.swapAt(slotIndex, hotBarSlotslotIndex) + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .numberKey(slot: Int16(slotIndex), number: Int8(hotBarSlot)), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for inventory right click: \(error)") + } + } else { + return false + } + + return true + } + + public func close(mouseStack: inout ItemStack?, eventBus: EventBus, connection: ServerConnection?) throws { + mouseStack = nil + eventBus.dispatch(CaptureCursorEvent()) + try connection?.sendPacket(CloseWindowServerboundPacket(windowId: UInt8(id))) + } + + public func dropItem(_ slotIndex: Int, connection: ServerConnection?) { + var dummy: ItemStack? = nil + drop(slotIndex: slotIndex, wholeStack: false, mouseItemStack: &dummy, connection: connection) + } + + public func dropStack(_ slotIndex: Int, connection: ServerConnection?) { + var dummy: ItemStack? = nil + drop(slotIndex: slotIndex, wholeStack: true, mouseItemStack: &dummy, connection: connection) + } + + public func dropItemFromMouse(_ mouseStack: inout ItemStack?, connection: ServerConnection?) { + drop(slotIndex: nil, wholeStack: false, mouseItemStack: &mouseStack, connection: connection) + } + + public func dropStackFromMouse(_ mouseStack: inout ItemStack?, connection: ServerConnection?) { + drop(slotIndex: nil, wholeStack: true, mouseItemStack: &mouseStack, connection: connection) + } + + private func drop( + slotIndex: Int?, + wholeStack: Bool, + mouseItemStack: inout ItemStack?, + connection: ServerConnection? + ) { + let clickedItem = slotIndex.map { slots[$0] } ?? Slot(mouseItemStack) + + let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 + if let index = slotIndex { + slots[index].stack?.count -= dropCount + if slots[index].stack?.count == 0 { + slots[index].stack = nil + } + } else { + mouseItemStack?.count -= dropCount + if mouseItemStack?.count == 0 { + mouseItemStack = nil + } + } + + let index = slotIndex.map(Int16.init) + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), + clickedItem: clickedItem + )) + } catch { + log.warning("Failed to send click window packet for item drop: \(error)") + } + } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/CloseWindowClientboundPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/CloseWindowClientboundPacket.swift index 339a8c69..d2520d15 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/CloseWindowClientboundPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/CloseWindowClientboundPacket.swift @@ -10,7 +10,7 @@ public struct CloseWindowClientboundPacket: ClientboundPacket { } public func handle(for client: Client) throws { - client.game.mutateGUIState { guiState in + try client.game.mutateGUIState { guiState in guard let window = guiState.window else { log.warning("Received CloseWindowClientboundPacket with no open window (window id: \(windowId))") return @@ -21,9 +21,8 @@ public struct CloseWindowClientboundPacket: ClientboundPacket { return } - guiState.window = nil - - client.eventBus.dispatch(CaptureCursorEvent()) + // Connection is set to nil since we don't need to send a packet (we just received one) + try window.close(mouseStack: &guiState.mouseItemStack, eventBus: client.eventBus, connection: nil) } } } From d4591f8228a8f88b455ec2195d2d175f2f0e1d83 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 29 May 2024 01:03:07 +1000 Subject: [PATCH 37/84] Revert accidental usage of if expressions feature to fix GitHub Actions workflows --- Sources/Core/Sources/GUI/GUIBuilder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Core/Sources/GUI/GUIBuilder.swift b/Sources/Core/Sources/GUI/GUIBuilder.swift index 2aadd5ea..019d5ebf 100644 --- a/Sources/Core/Sources/GUI/GUIBuilder.swift +++ b/Sources/Core/Sources/GUI/GUIBuilder.swift @@ -14,9 +14,9 @@ public struct GUIBuilder { public static func buildOptional(_ component: GUIElement?) -> GUIElement { if let component = component { - component + return component } else { - .spacer(width: 0, height: 0) + return .spacer(width: 0, height: 0) } } } From 9b6bf31a8317164baf89c6dcc526206a839d3415 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 29 May 2024 09:35:11 +1000 Subject: [PATCH 38/84] Fix Linux builds (hopefully...) --- .../ECS/Systems/PlayerInputSystem.swift | 2 +- Sources/Core/Sources/GUI/InGameGUI.swift | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 17f4353b..c558d571 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -243,7 +243,7 @@ public final class PlayerInputSystem: System { newCharacters = event.characters } #else - if message.utf8.count < GUIState.maximumMessageLength { + if message.utf8.count < InGameGUI.maximumMessageLength { newCharacters = event.characters } #endif diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index d6c8d59f..8641f18b 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -14,6 +14,7 @@ public class InGameGUI { /// The width of the chat history. static let chatHistoryWidth = 330 + #if os(macOS) /// The system's CPU display name. static let cpuName = HWInfo.CPU.name() /// The system's CPU architecture. @@ -22,6 +23,7 @@ public class InGameGUI { static let totalMem = (HWInfo.ramAmount() ?? 0) / (1024 * 1024 * 1024) /// A string containing information about the system's default GPU. static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() + #endif static let xpLevelTextColor = Vec4f(126, 252, 31, 255) / 255 static let debugScreenRowBackgroundColor = Vec4f(80, 80, 80, 144) / 255 @@ -402,13 +404,17 @@ public class InGameGUI { ] ] - let rightSections: [[String]] = [ - [ - "CPU: \(Self.cpuName ?? "unknown") (\(Self.cpuArch ?? "n/a"))", - "Total mem: \(Self.totalMem)GB", - "GPU: \(Self.gpuInfo ?? "unknown")" + #if os(macOS) + let rightSections: [[String]] = [ + [ + "CPU: \(Self.cpuName ?? "unknown") (\(Self.cpuArch ?? "n/a"))", + "Total mem: \(Self.totalMem)GB", + "GPU: \(Self.gpuInfo ?? "unknown")" + ] ] - ] + #else + let rightSections: [[String]] = [] + #endif return GUIElement.stack { debugScreenList(leftSections, side: .left) From a4bb7cb731791c8a08297bf27b99ca998357a4e6 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 29 May 2024 12:09:08 +1000 Subject: [PATCH 39/84] Load armor and tool properties from pixlyzer item registry --- .../BlockRegistry+BinaryCacheable.swift | 6 +- .../ECS/Components/PlayerInventory.swift | 15 ++ .../Systems/PlayerAccelerationSystem.swift | 2 +- .../ECS/Systems/PlayerFrictionSystem.swift | 2 +- .../ECS/Systems/PlayerInputSystem.swift | 44 ++++-- .../ECS/Systems/PlayerJumpSystem.swift | 2 +- .../ECS/Systems/PlayerVelocitySystem.swift | 2 +- .../Core/Sources/Registry/Block/Block.swift | 13 +- Sources/Core/Sources/Registry/Item/Item.swift | 119 ++++++++++++++ .../Registry/Pixlyzer/PixlyzerBlock.swift | 5 +- .../Registry/Pixlyzer/PixlyzerFormatter.swift | 2 +- .../Registry/Pixlyzer/PixlyzerItem.swift | 146 +++++++++++++++++- 12 files changed, 332 insertions(+), 26 deletions(-) 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 ?? [] + )) + } } } From 2654bd15ffa2a13ba68663428c4f44a8396050db Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 29 May 2024 22:41:10 +1000 Subject: [PATCH 40/84] Fix entity rendering (broken by skybox-related camera uniforms refactoring) --- .../Core/Renderer/Shader/EntityShaders.metal | 24 ++++++++++--------- .../Core/Renderer/World/WorldRenderer.swift | 2 ++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/Core/Renderer/Shader/EntityShaders.metal b/Sources/Core/Renderer/Shader/EntityShaders.metal index c357c4f1..4aca979f 100644 --- a/Sources/Core/Renderer/Shader/EntityShaders.metal +++ b/Sources/Core/Renderer/Shader/EntityShaders.metal @@ -1,7 +1,9 @@ #include +#include "ChunkTypes.metal" + using namespace metal; -struct Vertex { +struct EntityVertex { float x; float y; float z; @@ -10,30 +12,30 @@ struct Vertex { float b; }; -struct RasterizerData { +struct EntityRasterizerData { float4 position [[position]]; float4 color; }; -struct Uniforms { +struct EntityUniforms { float4x4 transformation; }; -vertex RasterizerData entityVertexShader(constant Vertex *vertices [[buffer(0)]], - constant Uniforms &uniforms [[buffer(1)]], - constant Uniforms *instanceUniforms [[buffer(2)]], +vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [[buffer(0)]], + constant CameraUniforms &cameraUniforms [[buffer(1)]], + constant EntityUniforms *instanceUniforms [[buffer(2)]], uint vertexId [[vertex_id]], uint instanceId [[instance_id]]) { - Vertex in = vertices[vertexId]; - RasterizerData out; + EntityVertex in = vertices[vertexId]; + EntityRasterizerData out; - out.position = float4(in.x, in.y, in.z, 1.0) * instanceUniforms[instanceId].transformation * uniforms.transformation; - out.color = float4(in.r, in.g, in.b, 1); + out.position = float4(in.x, in.y, in.z, 1.0) * instanceUniforms[instanceId].transformation * cameraUniforms.framing * cameraUniforms.projection; + out.color = float4(in.r, in.g, in.b, 1.0); return out; } -fragment float4 entityFragmentShader(RasterizerData in [[stage_in]], +fragment float4 entityFragmentShader(EntityRasterizerData in [[stage_in]], texture2d_array textureArray [[texture(0)]]) { return in.color; } diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 5a6213aa..52299187 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -70,6 +70,8 @@ public final class WorldRenderer: Renderer { commandQueue: MTLCommandQueue, profiler: Profiler ) throws { + print(client.resourcePack.vanillaResources.blockTexturePalette.texture(for: Identifier(namespace: "minecraft", name: "block/destroy_stage_0"))) + self.client = client self.device = device self.commandQueue = commandQueue From bc67f7dd86e5d66e84ea820604e7f4eefc8fe26b Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 30 May 2024 14:09:34 +1000 Subject: [PATCH 41/84] Implement survival mode block breaking (have to reimplement creative breaking and clean up overlay rendering implementation; inefficient) --- .../Core/Renderer/Mesh/BlockMeshBuilder.swift | 3 +- .../Core/Renderer/Shader/ChunkShaders.metal | 2 +- Sources/Core/Renderer/World/BlockVertex.swift | 26 ++-- .../Core/Renderer/World/WorldRenderer.swift | 86 ++++++++++- .../Core/Sources/Datatypes/Identifier.swift | 18 ++- .../Systems/PlayerBlockBreakingSystem.swift | 135 ++++++++++++++++++ .../ECS/Systems/PlayerInputSystem.swift | 22 +-- Sources/Core/Sources/Game.swift | 1 + Sources/Core/Sources/Registry/Item/Item.swift | 58 +++++++- .../Registry/Pixlyzer/PixlyzerItem.swift | 2 +- .../Core/Sources/World/BreakingBlock.swift | 25 ++++ Sources/Core/Sources/World/World.swift | 64 +++++++++ 12 files changed, 407 insertions(+), 35 deletions(-) create mode 100644 Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift create mode 100644 Sources/Core/Sources/World/BreakingBlock.swift diff --git a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift index 219426a3..fed690d4 100644 --- a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift @@ -28,7 +28,8 @@ struct BlockMeshBuilder { translucentGeometry = Self.mergeTranslucentGeometry( translucentGeometryParts, position: position - ) } + ) + } func buildPart( _ part: BlockModelPart, diff --git a/Sources/Core/Renderer/Shader/ChunkShaders.metal b/Sources/Core/Renderer/Shader/ChunkShaders.metal index 37e150b9..7b3de5c0 100644 --- a/Sources/Core/Renderer/Shader/ChunkShaders.metal +++ b/Sources/Core/Renderer/Shader/ChunkShaders.metal @@ -53,7 +53,7 @@ fragment float4 chunkFragmentShader(RasterizerData in [[stage_in]], } // Discard transparent fragments - if (in.isTransparent && color.w < 0.33) { + if (in.isTransparent && color.a < 0.33) { discard_fragment(); } diff --git a/Sources/Core/Renderer/World/BlockVertex.swift b/Sources/Core/Renderer/World/BlockVertex.swift index 68db5abd..556b3b74 100644 --- a/Sources/Core/Renderer/World/BlockVertex.swift +++ b/Sources/Core/Renderer/World/BlockVertex.swift @@ -2,17 +2,17 @@ import Foundation /// The vertex format used by the chunk block shader. public struct BlockVertex { - let x: Float - let y: Float - let z: Float - let u: Float - let v: Float - let r: Float - let g: Float - let b: Float - let a: Float - let skyLightLevel: UInt8 - let blockLightLevel: UInt8 - let textureIndex: UInt16 - let isTransparent: Bool + var x: Float + var y: Float + var z: Float + var u: Float + var v: Float + var r: Float + var g: Float + var b: Float + var a: Float + var skyLightLevel: UInt8 + var blockLightLevel: UInt8 + var textureIndex: UInt16 + var isTransparent: Bool } diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 52299187..e4f1fc26 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -61,6 +61,8 @@ public final class WorldRenderer: Renderer { /// The buffer for the uniforms used to render distance fog. private let fogUniformsBuffer: MTLBuffer + private let destroyOverlayRenderPipelineState: MTLRenderPipelineState + // MARK: Init /// Creates a new world renderer. @@ -70,8 +72,6 @@ public final class WorldRenderer: Renderer { commandQueue: MTLCommandQueue, profiler: Profiler ) throws { - print(client.resourcePack.vanillaResources.blockTexturePalette.texture(for: Identifier(namespace: "minecraft", name: "block/destroy_stage_0"))) - self.client = client self.device = device self.commandQueue = commandQueue @@ -112,6 +112,20 @@ public final class WorldRenderer: Renderer { blendingEnabled: true ) + destroyOverlayRenderPipelineState = try MetalUtil.makeRenderPipelineState( + device: device, + label: "WorldRenderer.destroyOverlayPipeline", + vertexFunction: vertexFunction, + fragmentFunction: fragmentFunction, + blendingEnabled: true, + editDescriptor: { descriptor in + descriptor.colorAttachments[0].sourceRGBBlendFactor = .destinationColor + descriptor.colorAttachments[0].sourceAlphaBlendFactor = .one + descriptor.colorAttachments[0].destinationRGBBlendFactor = .sourceColor + descriptor.colorAttachments[0].destinationAlphaBlendFactor = .zero + } + ) + #if !os(tvOS) // Create OIT pipeline transparencyRenderPipelineState = try MetalUtil.makeRenderPipelineState( @@ -327,6 +341,74 @@ public final class WorldRenderer: Renderer { profiler.pop() } + for breakingBlock in client.game.world.getBreakingBlocks() { + guard let stage = breakingBlock.stage else { + continue + } + let block = client.game.world.getBlock(at: breakingBlock.position) + if var model = resources.blockModelPalette.model(for: block.id, at: breakingBlock.position) { + let textureId = client.resourcePack.vanillaResources.blockTexturePalette.textureIndex(for: Identifier(namespace: "minecraft", name: "block/destroy_stage_\(stage)"))! + for (i, part) in model.parts.enumerated() { + for (j, element) in part.elements.enumerated() { + model.parts[i].elements[j].shade = false + for k in 0...stride * geometry.vertices.count) + guard let indexBuffer = device.makeBuffer(bytes: &geometry.indices, length: MemoryLayout.stride * geometry.indices.count) else { + // No geometry to render + continue + } + + encoder.setRenderPipelineState(destroyOverlayRenderPipelineState) + encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + encoder.setVertexBuffer(identityUniformsBuffer, offset: 0, index: 2) + + encoder.drawIndexedPrimitives( + type: .triangle, + indexCount: geometry.indices.count, + indexType: .uint32, + indexBuffer: indexBuffer, + indexBufferOffset: 0 + ) + } + } + // Entities are rendered before translucent geometry for correct alpha blending behaviour. profiler.push(.entities) try entityRenderer.render( diff --git a/Sources/Core/Sources/Datatypes/Identifier.swift b/Sources/Core/Sources/Datatypes/Identifier.swift index 585279d5..7f104c07 100644 --- a/Sources/Core/Sources/Datatypes/Identifier.swift +++ b/Sources/Core/Sources/Datatypes/Identifier.swift @@ -52,15 +52,27 @@ public struct Identifier: Hashable, Equatable, Codable, CustomStringConvertible // MARK: Init - /// Creates an identifier with the given name (and namespace if specified). The namespace defaults to 'minecraft'. + /// Creates an identifier with the given name and namespace. /// - Parameters: - /// - namespace: The namespace for the identifier. Defaults to `"minecraft"`. + /// - namespace: The namespace for the identifier. /// - name: The name for the identifier. - public init(namespace: String = "minecraft", name: String) { + public init(namespace: String, name: String) { self.namespace = namespace self.name = name } + /// Creates an identifier with the given name and the `"minecraft"` namespace. + /// + /// This is separate from ``Identifier/init(namespace:name:)`` so that it can be used it expressions + /// such as `["dirt", "wood"].map(Identifier.init(name:))` (which a single init with a default argument + /// wouldn't enable). + /// - Parameters: + /// - name: The name for the identifier. + public init(name: String) { + self.namespace = "minecraft" + self.name = name + } + /// Creates an identifier from the given string. Throws if the string is not a valid identifier. /// - Parameter string: String of the form `"namespace:name"` or `"name"`. public init(_ string: String) throws { diff --git a/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift new file mode 100644 index 00000000..1058765b --- /dev/null +++ b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift @@ -0,0 +1,135 @@ +import FirebladeECS + +public final class PlayerBlockBreakingSystem: System { + var connection: ServerConnection? + weak var game: Game? + var lastDestroyCompletionTick: Int? + + public init(_ connection: ServerConnection?, _ game: Game) { + self.connection = connection + self.game = game + } + + public func update(_ nexus: Nexus, _ world: World) throws { + guard let game = game else { + return + } + + // TODO: Figure out why obsidian mining speed is so wrong + // TODO: Cancel digging when hotbar slot changes + + var family = nexus.family( + requiresAll: EntityRotation.self, + PlayerInventory.self, + EntityCamera.self, + PlayerGamemode.self, + PlayerAttributes.self, + EntityId.self, + ClientPlayerEntity.self + ).makeIterator() + + guard let (rotation, inventory, camera, gamemode, attributes, playerEntityId, _) = family.next() else { + log.error("PlayerInputSystem failed to get player to tick") + return + } + + let guiState = nexus.single(GUIStateStorage.self).component + + guard guiState.movementAllowed else { + return + } + + // 5tick delay between successfully breaking a block and starting to break the next one. + if let completionTick = lastDestroyCompletionTick, game.tickScheduler.tickNumber - completionTick <= 5 { + return + } + + let inputState = nexus.single(InputState.self).component + + guard let (position, cursor, face, distance) = game.targetedBlock(acquireLock: false) else { + if let block = world.getBreakingBlocks().first(where: { $0.perpetratorEntityId == playerEntityId.id }) { + world.endBlockBreaking(for: playerEntityId.id) + + try self.connection?.sendPacket(PlayerDiggingPacket( + status: .cancelledDigging, + location: block.position, + face: .up // TODO: Figure out what value to use in this situation + )) + } + return + } + + let notifyServer = { status in + try self.connection?.sendPacket(PlayerDiggingPacket( + status: status, + location: position, + face: face + )) + } + + let newlyReleased = inputState.newlyReleased.contains(where: { $0.input == .destroy }) + if newlyReleased { + world.endBlockBreaking(for: playerEntityId.id) + try notifyServer(.cancelledDigging) + } + + // Technically possible to release and press again within the same tick. + if inputState.newlyPressed.contains(where: { $0.input == .destroy }) { + world.startBreakingBlock(at: position, for: playerEntityId.id) + try notifyServer(.startedDigging) + } else if inputState.inputs.contains(.destroy) && !newlyReleased { + let ourBreakingBlocks = world.getBreakingBlocks().filter { block in + block.perpetratorEntityId == playerEntityId.id + } + + var alreadyMiningTargetedBlock = false + for block in ourBreakingBlocks { + if block.position != position { + world.endBlockBreaking(at: block.position) + try notifyServer(.cancelledDigging) + } else { + alreadyMiningTargetedBlock = true + } + } + + if alreadyMiningTargetedBlock { + // TODO: This may be off by one tick, worth double checking + let block = world.getBlock(at: position) + let heldItem = inventory.mainHandItem + world.addBreakingProgress(Self.computeDestroyProgressDelta(block, heldItem), toBlockAt: position) + let progress = world.getBlockBreakingProgress(at: position) ?? 0 + if progress >= 1 { + world.endBlockBreaking(for: playerEntityId.id) + world.setBlockId(at: position, to: 0) + try notifyServer(.finishedDigging) + lastDestroyCompletionTick = game.tickScheduler.tickNumber + } + } else { + world.startBreakingBlock(at: position, for: playerEntityId.id) + try notifyServer(.startedDigging) + } + } + } + + public static func computeDestroyProgressDelta(_ block: Block, _ heldItem: Item?) -> Double { + let isBamboo = block.className == "BambooBlock" || block.className == "BambooSaplingBlock" + let holdingSword = heldItem?.properties?.toolProperties?.kind == .sword + if isBamboo && holdingSword { + return 1 + } else { + let hardness = block.physicalMaterial.hardness + + // TODO: Sentinel values are gross, we could probably just make hardness an optional + // (don't have to copy vanilla). I would do that right now but we need cache versioning + // first cause I'm pretty sure that change is subtle enough that it might just crash cache + // loading instead of forcing it to fail and retry from pixlyzer. + guard hardness != -1 else { + return 0 + } + + let defaultSpeed = block.physicalMaterial.requiresTool ? 0.01 : (1 / 30) + let toolSpeed = heldItem?.properties?.toolProperties?.destroySpeedMultiplier(for: block) ?? defaultSpeed + return toolSpeed / hardness + } + } +} diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 6fe327ee..0426d992 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -143,16 +143,20 @@ public final class PlayerInputSystem: System { cursorPositionZ: cursor.z, insideBlock: distance < 0 )) - } else if event.input == .destroy && attributes.canInstantBreak { - guard let (position, _, face, _) = game.targetedBlock(acquireLock: false) else { - break - } + } else if event.input == .destroy { + if attributes.canInstantBreak { + guard let (position, _, face, _) = game.targetedBlock(acquireLock: false) else { + break + } + + try connection?.sendPacket(PlayerDiggingPacket( + status: .startedDigging, + location: position, + face: face + )) + } else { - try connection?.sendPacket(PlayerDiggingPacket( - status: .startedDigging, - location: position, - face: face - )) + } } default: break diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 3d2c94f4..ae1f06ed 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -96,6 +96,7 @@ public final class Game: @unchecked Sendable { tickScheduler.addSystem(PlayerClimbSystem()) tickScheduler.addSystem(PlayerGravitySystem()) tickScheduler.addSystem(PlayerSmoothingSystem()) + tickScheduler.addSystem(PlayerBlockBreakingSystem(connection, self)) // TODO: Make sure that font gets updated when resource pack gets updated, will likely // require significant refactoring if we wanna do it right (as in not just hacking it // together for the specific case of PlayerInputSystem); proper resource pack propagation diff --git a/Sources/Core/Sources/Registry/Item/Item.swift b/Sources/Core/Sources/Registry/Item/Item.swift index 7fa8a138..2cf2d753 100644 --- a/Sources/Core/Sources/Registry/Item/Item.swift +++ b/Sources/Core/Sources/Registry/Item/Item.swift @@ -26,6 +26,20 @@ public struct Item: Codable { public enum Properties: Codable { case armor(ArmorProperties) case tool(ToolProperties) + + public var armorProperties: ArmorProperties? { + guard case let .armor(properties) = self else { + return nil + } + return properties + } + + public var toolProperties: ToolProperties? { + guard case let .tool(properties) = self else { + return nil + } + return properties + } } public struct ArmorProperties: Codable { @@ -59,7 +73,7 @@ public struct Item: Codable { public struct ToolProperties: Codable { public var uses: Int - public var level: Int + public var level: Level public var speed: Double public var attackDamage: Double public var attackDamageBonus: Double @@ -78,9 +92,21 @@ public struct Item: Codable { /// blocks at a time. public var effectiveMaterials: [Identifier] + public enum Level: Int, Codable, Equatable, Comparable { + case woodOrGold = 0 + case stone = 1 + case iron = 2 + case diamond = 3 + case netherite = 4 + + public static func < (lhs: Level, rhs: Level) -> Bool { + lhs.rawValue < rhs.rawValue + } + } + public init( uses: Int, - level: Int, + level: Level, speed: Double, attackDamage: Double, attackDamageBonus: Double, @@ -102,9 +128,31 @@ public struct Item: Codable { self.effectiveMaterials = effectiveMaterials } - public func isEffective(on block: Block) -> Bool { - effectiveMaterials.contains(block.vanillaMaterialIdentifier) - || mineableBlocks.contains(block.id) + public func destroySpeedMultiplier(for block: Block) -> Double { + if effectiveMaterials.contains(block.vanillaMaterialIdentifier) { + return speed * (1 / 30) + } + + switch kind { + case .sword: + let swordSemiEffectiveMaterials = ["plant", "replaceable_plant"].map(Identifier.init(name:)) + if block.className == "CobwebBlock" { + return 0.15 + } else if swordSemiEffectiveMaterials.contains(block.vanillaMaterialIdentifier) { + return 0.015 + } else { + return 0.01 + } + case .pickaxe, .shovel, .hoe, .axe: + let isCorrectTool = + effectiveMaterials.contains(block.vanillaMaterialIdentifier) + || mineableBlocks.contains(block.id) + if isCorrectTool { + return speed * (1 / 30) + } else { + return block.physicalMaterial.requiresTool ? 0.01 : (1 / 30) + } + } } public enum ToolKind: String, Codable { diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift index 175244e2..183340c3 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerItem.swift @@ -47,7 +47,7 @@ public struct PixlyzerItem: Decodable { public var speed: Double? public var attackDamage: Double? public var attackDamageBonus: Double? - public var level: Int? + public var level: Item.ToolProperties.Level? public var enchantmentValue: Int? public var diggableBlocks: [Int]? public var strippableBlocks: [String: Int]? diff --git a/Sources/Core/Sources/World/BreakingBlock.swift b/Sources/Core/Sources/World/BreakingBlock.swift new file mode 100644 index 00000000..c1a7a822 --- /dev/null +++ b/Sources/Core/Sources/World/BreakingBlock.swift @@ -0,0 +1,25 @@ +/// Represents a block getting broken. Includes the stage of the breaking animation, +/// the entity breaking the block (the perpetrator), and the position. +public struct BreakingBlock { + public var position: BlockPosition + public var perpetratorEntityId: Int + public var progress: Double + + /// `nil` if the animation hasn't started yet. Otherwise an integer in the range `0...9`. + public var stage: Int? { + guard progress >= 0 else { + return nil + } + + guard progress <= 1 else { + return 9 + } + + let stage = Int(progress * 10) - 1 + if stage < 0 { + return nil + } else { + return stage + } + } +} diff --git a/Sources/Core/Sources/World/World.swift b/Sources/Core/Sources/World/World.swift index 7104936b..011db462 100644 --- a/Sources/Core/Sources/World/World.swift +++ b/Sources/Core/Sources/World/World.swift @@ -71,6 +71,11 @@ public class World { /// Not thread safe. Use `eventBus`. private var _eventBus: EventBus + /// Lock for protecting access to ``breakingBlocks``. + private var blockBreakingLock = ReadWriteLock() + /// All blocks currently getting broken. + private var breakingBlocks: [BreakingBlock] = [] + // MARK: Init /// Create an empty world. @@ -692,6 +697,65 @@ public class World { return unlitChunks[position] != nil } + // MARK: Block breaking + + public func startBreakingBlock(at position: BlockPosition, for entityId: Int) { + blockBreakingLock.acquireWriteLock() + defer { blockBreakingLock.unlock() } + for block in breakingBlocks { + if block.position == position { + // TODO: Figure out what to do in this situation + return + } + } + breakingBlocks.append( + BreakingBlock( + position: position, + perpetratorEntityId: entityId, + progress: 0 + ) + ) + } + + /// Does nothing if the specified block isn't getting broken. + public func addBreakingProgress(_ progress: Double, toBlockAt position: BlockPosition) { + blockBreakingLock.acquireWriteLock() + defer { blockBreakingLock.unlock() } + for (i, block) in breakingBlocks.enumerated() where block.position == position { + breakingBlocks[i].progress += progress + } + } + + public func getBreakingBlocks() -> [BreakingBlock] { + blockBreakingLock.acquireReadLock() + defer { blockBreakingLock.unlock() } + return breakingBlocks + } + + public func getBlockBreakingProgress(at position: BlockPosition) -> Double? { + blockBreakingLock.acquireReadLock() + defer { blockBreakingLock.unlock() } + return breakingBlocks.first { block in + block.position == position + }?.progress + } + + public func endBlockBreaking(at position: BlockPosition) { + blockBreakingLock.acquireWriteLock() + defer { blockBreakingLock.unlock() } + breakingBlocks = breakingBlocks.filter { block in + block.position != position + } + } + + public func endBlockBreaking(for entityId: Int) { + blockBreakingLock.acquireWriteLock() + defer { blockBreakingLock.unlock() } + breakingBlocks = breakingBlocks.filter { block in + block.perpetratorEntityId != entityId + } + } + // MARK: Helper /// Gets whether the a position is in a loaded chunk or not. From 7117b24c0ddd328486f5d2db9b58e698bee98001 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 30 May 2024 16:26:40 +1000 Subject: [PATCH 42/84] Fix entity velocity (entity spawn packets without velocity should still include a velocity component; just set to zero), and add basic entity gravity --- .../ECS/Systems/EntityMovementSystem.swift | 3 ++ .../Play/Clientbound/SpawnEntityPacket.swift | 40 ++++++------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index 93bf5b2a..dea4ac25 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -14,6 +14,9 @@ public struct EntityMovementSystem: System { for (position, velocity, onGround) in physicsEntities { if onGround.onGround { velocity.vector.y = 0 + } else { + velocity.vector.y *= 0.98 + velocity.vector.y -= 0.04 } position.move(by: velocity.vector) 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 3f6b459e..b0e809ed 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift @@ -34,34 +34,18 @@ public struct SpawnEntityPacket: ClientboundPacket { return } - // TODO: implement if statements for component builder - if let velocity = velocity { - client.game.createEntity(id: entityId) { - NonLivingEntity() - EntityKindId(type) - EntityId(entityId) - ObjectUUID(objectUUID) - ObjectData(data) - EntityHitBox(width: entityKind.width, height: entityKind.height) - EntityOnGround(true) - EntityPosition(position) - EntityVelocity(velocity) - EntityRotation(pitch: pitch, yaw: yaw) - EntityAttributes() - } - } else { - client.game.createEntity(id: entityId) { - NonLivingEntity() - EntityKindId(type) - EntityId(entityId) - ObjectUUID(objectUUID) - ObjectData(data) - EntityHitBox(width: entityKind.width, height: entityKind.height) - EntityOnGround(true) - EntityPosition(position) - EntityRotation(pitch: pitch, yaw: yaw) - EntityAttributes() - } + client.game.createEntity(id: entityId) { + NonLivingEntity() + EntityKindId(type) + EntityId(entityId) + ObjectUUID(objectUUID) + ObjectData(data) + EntityHitBox(width: entityKind.width, height: entityKind.height) + EntityOnGround(false) + EntityPosition(position) + EntityVelocity(velocity ?? .zero) + EntityRotation(pitch: pitch, yaw: yaw) + EntityAttributes() } } } From 85545e8b27b5b4b24acb010de620d7aaf518a5a0 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 30 May 2024 16:52:08 +1000 Subject: [PATCH 43/84] Fix non-survival block breaking/interaction and fix mining speed of tool-requiring blocks (e.g. obsidian) --- Sources/Core/Sources/Datatypes/Gamemode.swift | 20 ++++++++++++++++- .../Systems/PlayerBlockBreakingSystem.swift | 22 ++++++++++++++----- .../ECS/Systems/PlayerInputSystem.swift | 19 +++++----------- Sources/Core/Sources/Registry/Item/Item.swift | 10 ++++----- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/Sources/Core/Sources/Datatypes/Gamemode.swift b/Sources/Core/Sources/Datatypes/Gamemode.swift index b3d8016d..ce4603cf 100644 --- a/Sources/Core/Sources/Datatypes/Gamemode.swift +++ b/Sources/Core/Sources/Datatypes/Gamemode.swift @@ -26,7 +26,25 @@ public enum Gamemode: Int8 { return false } } - + + public var canBreakBlocks: Bool { + switch self { + case .survival, .creative: + return true + case .adventure, .spectator: + return false + } + } + + public var canPlaceBlocks: Bool { + switch self { + case .survival, .creative: + return true + case .adventure, .spectator: + return false + } + } + /// The lowercase string representation of the gamemode. public var string: String { switch self { diff --git a/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift index 1058765b..22321d96 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift @@ -15,24 +15,25 @@ public final class PlayerBlockBreakingSystem: System { return } - // TODO: Figure out why obsidian mining speed is so wrong // TODO: Cancel digging when hotbar slot changes var family = nexus.family( - requiresAll: EntityRotation.self, - PlayerInventory.self, - EntityCamera.self, + requiresAll: PlayerInventory.self, PlayerGamemode.self, PlayerAttributes.self, EntityId.self, ClientPlayerEntity.self ).makeIterator() - guard let (rotation, inventory, camera, gamemode, attributes, playerEntityId, _) = family.next() else { + guard let (inventory, gamemode, attributes, playerEntityId, _) = family.next() else { log.error("PlayerInputSystem failed to get player to tick") return } + guard gamemode.gamemode.canPlaceBlocks else { + return + } + let guiState = nexus.single(GUIStateStorage.self).component guard guiState.movementAllowed else { @@ -67,6 +68,13 @@ public final class PlayerBlockBreakingSystem: System { )) } + guard !attributes.canInstantBreak else { + if inputState.newlyPressed.contains(where: { $0.input == .destroy }) { + try notifyServer(.startedDigging) + } + return + } + let newlyReleased = inputState.newlyReleased.contains(where: { $0.input == .destroy }) if newlyReleased { world.endBlockBreaking(for: playerEntityId.id) @@ -118,6 +126,7 @@ public final class PlayerBlockBreakingSystem: System { return 1 } else { let hardness = block.physicalMaterial.hardness + print("Hardness:", hardness) // TODO: Sentinel values are gross, we could probably just make hardness an optional // (don't have to copy vanilla). I would do that right now but we need cache versioning @@ -129,6 +138,9 @@ public final class PlayerBlockBreakingSystem: System { let defaultSpeed = block.physicalMaterial.requiresTool ? 0.01 : (1 / 30) let toolSpeed = heldItem?.properties?.toolProperties?.destroySpeedMultiplier(for: block) ?? defaultSpeed + print("Default speed:", defaultSpeed) + print("Tool speed:", toolSpeed) + print("") return toolSpeed / hardness } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 0426d992..6ce8286b 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -123,13 +123,15 @@ public final class PlayerInputSystem: System { let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot inventory.window.dropItem(slotIndex, connection: connection) case .place, .destroy: + // Block breaking is handled by ``PlayerBlockBreakingSystem``, this just handles hand animation and + // other non breaking things for the `.destroy` input (e.g. attacking) if inventory.hotbar[inventory.selectedHotbarSlot].stack != nil { try connection?.sendPacket(UseItemPacket(hand: .mainHand)) } else { try connection?.sendPacket(AnimationServerboundPacket(hand: .mainHand)) } - if event.input == .place && gamemode.gamemode != .spectator { + if event.input == .place { guard let (position, cursor, face, distance) = game.targetedBlock(acquireLock: false) else { break } @@ -143,19 +145,10 @@ public final class PlayerInputSystem: System { cursorPositionZ: cursor.z, insideBlock: distance < 0 )) - } else if event.input == .destroy { - if attributes.canInstantBreak { - guard let (position, _, face, _) = game.targetedBlock(acquireLock: false) else { - break - } - - try connection?.sendPacket(PlayerDiggingPacket( - status: .startedDigging, - location: position, - face: face - )) - } else { + if gamemode.gamemode.canPlaceBlocks { + // TODO: Predict the result of block placement so that we're not relying on the server + // (quite noticeable latency) } } default: diff --git a/Sources/Core/Sources/Registry/Item/Item.swift b/Sources/Core/Sources/Registry/Item/Item.swift index 2cf2d753..9ca184bf 100644 --- a/Sources/Core/Sources/Registry/Item/Item.swift +++ b/Sources/Core/Sources/Registry/Item/Item.swift @@ -129,10 +129,6 @@ public struct Item: Codable { } public func destroySpeedMultiplier(for block: Block) -> Double { - if effectiveMaterials.contains(block.vanillaMaterialIdentifier) { - return speed * (1 / 30) - } - switch kind { case .sword: let swordSemiEffectiveMaterials = ["plant", "replaceable_plant"].map(Identifier.init(name:)) @@ -146,7 +142,11 @@ public struct Item: Codable { case .pickaxe, .shovel, .hoe, .axe: let isCorrectTool = effectiveMaterials.contains(block.vanillaMaterialIdentifier) - || mineableBlocks.contains(block.id) + || mineableBlocks.contains(block.vanillaParentBlockId) + print("Is effective material:", effectiveMaterials.contains(block.vanillaMaterialIdentifier)) + print("Is mineable by tool:", mineableBlocks.contains(block.id)) + print("Requires tool:", block.physicalMaterial.requiresTool) + print("Speed:", speed) if isCorrectTool { return speed * (1 / 30) } else { From bd8a02c062fa85c0e3c97cc351ebb56e13287dad Mon Sep 17 00:00:00 2001 From: stackotter Date: Fri, 31 May 2024 01:19:14 +1000 Subject: [PATCH 44/84] Implement entity attacking and interaction (can right click villagers for trading window etc; the window types just aren't implemented yet) --- .../Core/Renderer/World/WorldRenderer.swift | 3 +- .../Systems/PlayerBlockBreakingSystem.swift | 10 +- .../ECS/Systems/PlayerInputSystem.swift | 75 +++++++++---- Sources/Core/Sources/Game.swift | 104 ++++++++++++++++-- .../Serverbound/InteractEntityPacket.swift | 2 +- Sources/Core/Sources/Registry/Item/Item.swift | 4 - .../Sources/Util/FunctionalProgramming.swift | 6 + Sources/Core/Sources/World/Targeted.swift | 31 ++++++ Sources/Core/Sources/World/Thing.swift | 5 + 9 files changed, 198 insertions(+), 42 deletions(-) create mode 100644 Sources/Core/Sources/World/Targeted.swift create mode 100644 Sources/Core/Sources/World/Thing.swift diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index e4f1fc26..b32b4486 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -300,7 +300,8 @@ public final class WorldRenderer: Renderer { if client.game.currentGamemode() != .spectator { // Render selected block outline profiler.push(.encodeBlockOutline) - if let (targetedBlockPosition, _, _, _) = client.game.targetedBlock() { + if let targetedBlock = client.game.targetedBlock() { + let targetedBlockPosition = targetedBlock.target var indices: [UInt32] = [] var vertices: [BlockVertex] = [] let block = client.game.world.getBlock(at: targetedBlockPosition) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift index 22321d96..01ba097b 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerBlockBreakingSystem.swift @@ -47,7 +47,7 @@ public final class PlayerBlockBreakingSystem: System { let inputState = nexus.single(InputState.self).component - guard let (position, cursor, face, distance) = game.targetedBlock(acquireLock: false) else { + guard let targetedBlock = game.targetedBlock(acquireLock: false) else { if let block = world.getBreakingBlocks().first(where: { $0.perpetratorEntityId == playerEntityId.id }) { world.endBlockBreaking(for: playerEntityId.id) @@ -60,11 +60,13 @@ public final class PlayerBlockBreakingSystem: System { return } + let position = targetedBlock.target + let notifyServer = { status in try self.connection?.sendPacket(PlayerDiggingPacket( status: status, location: position, - face: face + face: targetedBlock.face )) } @@ -126,7 +128,6 @@ public final class PlayerBlockBreakingSystem: System { return 1 } else { let hardness = block.physicalMaterial.hardness - print("Hardness:", hardness) // TODO: Sentinel values are gross, we could probably just make hardness an optional // (don't have to copy vanilla). I would do that right now but we need cache versioning @@ -138,9 +139,6 @@ public final class PlayerBlockBreakingSystem: System { let defaultSpeed = block.physicalMaterial.requiresTool ? 0.01 : (1 / 30) let toolSpeed = heldItem?.properties?.toolProperties?.destroySpeedMultiplier(for: block) ?? defaultSpeed - print("Default speed:", defaultSpeed) - print("Tool speed:", toolSpeed) - print("") return toolSpeed / hardness } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 6ce8286b..fdf8fa39 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -41,10 +41,12 @@ public final class PlayerInputSystem: System { EntityCamera.self, PlayerGamemode.self, PlayerAttributes.self, + EntitySneaking.self, + EntityId.self, ClientPlayerEntity.self ).makeIterator() - guard let (rotation, inventory, camera, gamemode, attributes, _) = family.next() else { + guard let (rotation, inventory, camera, gamemode, attributes, sneaking, playerEntityId, _) = family.next() else { log.error("PlayerInputSystem failed to get player to tick") return } @@ -122,35 +124,62 @@ public final class PlayerInputSystem: System { case .dropItem: let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot inventory.window.dropItem(slotIndex, connection: connection) - case .place, .destroy: + case .place: // Block breaking is handled by ``PlayerBlockBreakingSystem``, this just handles hand animation and // other non breaking things for the `.destroy` input (e.g. attacking) if inventory.hotbar[inventory.selectedHotbarSlot].stack != nil { try connection?.sendPacket(UseItemPacket(hand: .mainHand)) - } else { - try connection?.sendPacket(AnimationServerboundPacket(hand: .mainHand)) } - if event.input == .place { - guard let (position, cursor, face, distance) = game.targetedBlock(acquireLock: false) else { - break - } - - try connection?.sendPacket(PlayerBlockPlacementPacket( - hand: .mainHand, - location: position, - face: face, - cursorPositionX: cursor.x, - cursorPositionY: cursor.y, - cursorPositionZ: cursor.z, - insideBlock: distance < 0 - )) - - if gamemode.gamemode.canPlaceBlocks { - // TODO: Predict the result of block placement so that we're not relying on the server - // (quite noticeable latency) - } + guard let targetedThing = game.targetedThing(acquireLock: false) else { + break } + + switch targetedThing.target { + case let .block(blockPosition): + let cursor = targetedThing.cursor + try connection?.sendPacket(PlayerBlockPlacementPacket( + hand: .mainHand, + location: blockPosition, + face: targetedThing.face, + cursorPositionX: cursor.x, + cursorPositionY: cursor.y, + cursorPositionZ: cursor.z, + insideBlock: targetedThing.distance < 0 + )) + + if gamemode.gamemode.canPlaceBlocks { + // TODO: Predict the result of block placement so that we're not relying on the server + // (quite noticeable latency) + } + case let .entity(entityId): + let targetedPosition = targetedThing.targetedPosition + try connection?.sendPacket(InteractEntityPacket( + entityId: Int32(entityId), + interaction: .interactAt( + targetX: targetedPosition.x, + targetY: targetedPosition.y, + targetZ: targetedPosition.z, + hand: .mainHand, + isSneaking: sneaking.isSneaking + ) + )) + try connection?.sendPacket(InteractEntityPacket( + entityId: Int32(entityId), + interaction: .interact(hand: .mainHand, isSneaking: sneaking.isSneaking) + )) + } + case .destroy: + try connection?.sendPacket(AnimationServerboundPacket(hand: .mainHand)) + + guard let targetedEntity = game.targetedEntity(acquireLock: false) else { + break + } + + try connection?.sendPacket(InteractEntityPacket( + entityId: Int32(targetedEntity.target), + interaction: .attack(isSneaking: sneaking.isSneaking) + )) default: break } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index ae1f06ed..4d4dca57 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -380,9 +380,9 @@ public final class Game: @unchecked Sendable { /// Gets the position of the block currently targeted by the player. /// - Parameters: /// - acquireLock: If `false`, no locks are acquired. Only use if you know what you're doing. - public func targetedBlock(acquireLock: Bool = true) -> (block: BlockPosition, cursor: Vec3f, face: Direction, distance: Float)? { // swiftlint:disable:this large_tuple + public func targetedBlockIgnoringEntities(acquireLock: Bool = true) -> Targeted? { if acquireLock { - nexusLock.acquireWriteLock() + nexusLock.acquireReadLock() } let ray = player.ray @@ -400,17 +400,107 @@ public final class Game: @unchecked Sendable { break } - var cursor = ray.direction * distance + ray.origin - cursor.x = cursor.x.truncatingRemainder(dividingBy: 1) - cursor.y = cursor.y.truncatingRemainder(dividingBy: 1) - cursor.z = cursor.z.truncatingRemainder(dividingBy: 1) - return (position, cursor, face, distance) + let targetedPosition = ray.direction * distance + ray.origin + return Targeted( + target: position, + distance: distance, + face: face, + targetedPosition: targetedPosition + ) } } return nil } + public func targetedBlock(acquireLock: Bool = true) -> Targeted? { + guard let targetedThing = targetedThing(acquireLock: acquireLock) else { + return nil + } + + guard case let .block(position) = targetedThing.target else { + return nil + } + + return targetedThing.map(constant(position)) + } + + // TODO: Make a value type for entity ids so that this doesn't return a targeted integer (just feels confusing). + /// - Returns: The id of the entity targeted by the player, if any. + public func targetedEntityIgnoringBlocks(acquireLock: Bool = true) -> Targeted? { + if acquireLock { nexusLock.acquireReadLock() } + defer { if acquireLock { nexusLock.unlock() } } + + let playerPosition = player.position.vector + let playerRay = player.ray + + let family = nexus.family( + requiresAll: EntityId.self, + EntityPosition.self, + EntityHitBox.self, + excludesAll: ClientPlayerEntity.self + ) + + var candidate: Targeted? + for (id, position, hitbox) in family { + guard (playerPosition - position.vector).magnitude < 4 else { + continue + } + + guard let (distance, face) = hitbox.aabb(at: position.vector).intersectionDistanceAndFace(with: playerRay) else { + continue + } + + let newCandidate = Targeted( + target: id.id, + distance: distance, + face: face, + targetedPosition: playerRay.direction * distance + playerRay.origin + ) + + if let currentCandidate = candidate { + if distance < currentCandidate.distance { + candidate = newCandidate + } + } else { + candidate = newCandidate + } + } + + return candidate + } + + public func targetedEntity(acquireLock: Bool = true) -> Targeted? { + guard let targetedThing = targetedThing(acquireLock: acquireLock) else { + return nil + } + + guard case let .entity(id) = targetedThing.target else { + return nil + } + + return targetedThing.map(constant(id)) + } + + /// - Returns: The closest thing targeted by the player. + public func targetedThing(acquireLock: Bool = true) -> Targeted? { + let targetedBlock = targetedBlockIgnoringEntities(acquireLock: acquireLock) + let targetedEntity = targetedEntityIgnoringBlocks(acquireLock: acquireLock) + if let block = targetedBlock, let entity = targetedEntity { + if block.distance < entity.distance { + return block.map(Thing.block) + } else { + return entity.map(Thing.entity) + } + } else if let block = targetedBlock { + return block.map(Thing.block) + } else if let entity = targetedEntity { + return entity.map(Thing.entity) + } else { + return nil + } + } + /// Gets current gamemode of the player. public func currentGamemode() -> Gamemode? { var gamemode: Gamemode? = nil diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/InteractEntityPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/InteractEntityPacket.swift index 8849db5f..d68da2fa 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/InteractEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/InteractEntityPacket.swift @@ -20,7 +20,7 @@ public struct InteractEntityPacket: ServerboundPacket { writer.writeVarInt(hand.rawValue) writer.writeBool(isSneaking) case let .attack(isSneaking: isSneaking): - writer.writeVarInt(1) // interact + writer.writeVarInt(1) // attack writer.writeBool(isSneaking) case let .interactAt(targetX: targetX, targetY: targetY, targetZ: targetZ, hand: hand, isSneaking: isSneaking): writer.writeVarInt(2) // interact at diff --git a/Sources/Core/Sources/Registry/Item/Item.swift b/Sources/Core/Sources/Registry/Item/Item.swift index 9ca184bf..6d1b9300 100644 --- a/Sources/Core/Sources/Registry/Item/Item.swift +++ b/Sources/Core/Sources/Registry/Item/Item.swift @@ -143,10 +143,6 @@ public struct Item: Codable { let isCorrectTool = effectiveMaterials.contains(block.vanillaMaterialIdentifier) || mineableBlocks.contains(block.vanillaParentBlockId) - print("Is effective material:", effectiveMaterials.contains(block.vanillaMaterialIdentifier)) - print("Is mineable by tool:", mineableBlocks.contains(block.id)) - print("Requires tool:", block.physicalMaterial.requiresTool) - print("Speed:", speed) if isCorrectTool { return speed * (1 / 30) } else { diff --git a/Sources/Core/Sources/Util/FunctionalProgramming.swift b/Sources/Core/Sources/Util/FunctionalProgramming.swift index 2a5124f9..2b605bec 100644 --- a/Sources/Core/Sources/Util/FunctionalProgramming.swift +++ b/Sources/Core/Sources/Util/FunctionalProgramming.swift @@ -8,3 +8,9 @@ func swap(_ left: inout T, _ right: inout T) { left = right right = temp } + +func constant(_ value: B) -> (A) -> B { + { _ in + value + } +} diff --git a/Sources/Core/Sources/World/Targeted.swift b/Sources/Core/Sources/World/Targeted.swift new file mode 100644 index 00000000..19911c1f --- /dev/null +++ b/Sources/Core/Sources/World/Targeted.swift @@ -0,0 +1,31 @@ +/// Something targeted by the player. Often a ``Block``, ``Entity`` or ``Thing``. +public struct Targeted { + /// The underlying thing getting targeted. + public var target: T + /// The distance from the player to the point of intersection. + public var distance: Float + /// The face of the bounding box which the player's ray intersects with. + public var face: Direction + /// The position at which the player's ray intersects the thing's bounding box. + public var targetedPosition: Vec3f + + /// The targeted position relative to the block that it's in. All coordinates will be in + /// the range `0...1`. + public var cursor: Vec3f { + var cursor = targetedPosition + cursor.x = cursor.x.truncatingRemainder(dividingBy: 1) + cursor.y = cursor.y.truncatingRemainder(dividingBy: 1) + cursor.z = cursor.z.truncatingRemainder(dividingBy: 1) + return cursor + } + + /// Maps the targeted value, useful for changing the representation of the wrapped target. + public func map(_ mapTarget: (T) -> U) -> Targeted { + Targeted( + target: mapTarget(target), + distance: distance, + face: face, + targetedPosition: targetedPosition + ) + } +} diff --git a/Sources/Core/Sources/World/Thing.swift b/Sources/Core/Sources/World/Thing.swift new file mode 100644 index 00000000..60e8f462 --- /dev/null +++ b/Sources/Core/Sources/World/Thing.swift @@ -0,0 +1,5 @@ +/// A block or an entity. Starting to run out of ambiguous nouns! +public enum Thing { + case block(position: BlockPosition) + case entity(id: Int) +} From 177c4b3394a11a1175d293a9741e2a01e9faa9cf Mon Sep 17 00:00:00 2001 From: Plasma Date: Fri, 31 May 2024 01:13:48 -0400 Subject: [PATCH 45/84] Switch to swift-async-dns-resolver for DNS resolution (#197) * Switch DNS package * Fix formatting * Fix package version (Xcode messed it up) * Remove redundant code * Switch Resolver * Fix SRV --- Package.resolved | 73 ++++++++----------- Package.swift | 2 +- .../Client/Views/Play/JoinServerAndThen.swift | 2 +- .../Views/ServerList/ServerListView.swift | 4 +- Sources/Core/Package.swift | 4 +- Sources/Core/Sources/Client.swift | 4 +- .../Sources/Network/LANServerEnumerator.swift | 4 +- .../Sources/Network/ServerConnection.swift | 27 ++++--- Sources/Core/Sources/Server/Ping/Pinger.swift | 48 ++++++------ 9 files changed, 84 insertions(+), 84 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8bd73b80..191d6eae 100644 --- a/Package.resolved +++ b/Package.resolved @@ -51,17 +51,8 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", "state": { "branch": null, - "revision": "32f641cf24fc7abc1c591a2025e9f2f572648b0f", - "version": "1.7.2" - } - }, - { - "package": "DNS", - "repositoryURL": "https://github.com/Bouke/DNS.git", - "state": { - "branch": null, - "revision": "78bbd1589890a90b202d11d5f9e1297050cf0eb2", - "version": "1.2.0" + "revision": "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version": "1.8.2" } }, { @@ -87,8 +78,8 @@ "repositoryURL": "https://github.com/michaeleisel/JJLISO8601DateFormatter", "state": { "branch": null, - "revision": "de422afd9a47b72703c30a81423c478337191390", - "version": "0.1.6" + "revision": "ed1d996123688bade6e895aa49595f0d862900e7", + "version": "0.1.7" } }, { @@ -132,8 +123,17 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version": "1.2.2" + "revision": "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version": "1.4.0" + } + }, + { + "package": "swift-async-dns-resolver", + "repositoryURL": "https://github.com/apple/swift-async-dns-resolver.git", + "state": { + "branch": null, + "revision": "08c07ff31a745ee5e522ac10132fb4949834d925", + "version": "0.4.0" } }, { @@ -141,8 +141,8 @@ "repositoryURL": "https://github.com/apple/swift-atomics.git", "state": { "branch": null, - "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version": "1.1.0" + "revision": "cd142fd2f64be2100422d658e7411e39489da985", + "version": "1.2.0" } }, { @@ -150,8 +150,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version": "0.14.1" + "revision": "8d712376c99fc0267aa0e41fea732babe365270a", + "version": "1.3.3" } }, { @@ -167,7 +167,7 @@ "package": "swift-cross-ui", "repositoryURL": "https://github.com/stackotter/swift-cross-ui", "state": { - "branch": "main", + "branch": null, "revision": "e4491a59449dec572c1d0d2c214f35825dbc34aa", "version": null } @@ -186,8 +186,8 @@ "repositoryURL": "https://github.com/apple/swift-log.git", "state": { "branch": null, - "revision": "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version": "1.5.3" + "revision": "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version": "1.5.4" } }, { @@ -231,8 +231,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-parsing", "state": { "branch": null, - "revision": "27c941bbd22a4bbc53005a15a0440443fd892f70", - "version": "0.12.1" + "revision": "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version": "0.13.0" } }, { @@ -245,21 +245,12 @@ } }, { - "package": "Resolver", - "repositoryURL": "https://github.com/seznam/swift-resolver", - "state": { - "branch": null, - "revision": "cfb7d326bc4c89a48439303d758b375a8faae784", - "version": "0.3.0" - } - }, - { - "package": "UniSocket", - "repositoryURL": "https://github.com/seznam/swift-unisocket", + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", "state": { "branch": null, - "revision": "1785e432fb8497265a38712cdb9584c429ca3f96", - "version": "0.14.0" + "revision": "303e5c5c36d6a558407d364878df131c3546fad8", + "version": "510.0.2" } }, { @@ -303,8 +294,8 @@ "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, - "revision": "50843cbb8551db836adec2290bb4bc6bac5c1865", - "version": "0.9.0" + "revision": "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version": "1.1.2" } }, { @@ -312,8 +303,8 @@ "repositoryURL": "https://github.com/weichsel/ZIPFoundation.git", "state": { "branch": null, - "revision": "43ec568034b3731101dbf7670765d671c30f54f3", - "version": "0.9.16" + "revision": "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version": "0.9.19" } }, { diff --git a/Package.swift b/Package.swift index a037ab90..9a5f7d2f 100644 --- a/Package.swift +++ b/Package.swift @@ -79,7 +79,7 @@ let package = Package( .package(name: "DeltaCore", path: "Sources/Core"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), .package(url: "https://github.com/stackotter/SwordRPC", .revision("3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed")), - .package(url: "https://github.com/stackotter/swift-cross-ui", branch: "main") + .package(url: "https://github.com/stackotter/swift-cross-ui", revision: "e4491a59449dec572c1d0d2c214f35825dbc34aa") ], targets: targets ) diff --git a/Sources/Client/Views/Play/JoinServerAndThen.swift b/Sources/Client/Views/Play/JoinServerAndThen.swift index 2984b4e6..21d8f4a3 100644 --- a/Sources/Client/Views/Play/JoinServerAndThen.swift +++ b/Sources/Client/Views/Play/JoinServerAndThen.swift @@ -223,7 +223,7 @@ struct JoinServerAndThen: View { } do { - try client.joinServer( + try await client.joinServer( describedBy: descriptor, with: refreshedAccount ) diff --git a/Sources/Client/Views/ServerList/ServerListView.swift b/Sources/Client/Views/ServerList/ServerListView.swift index 8b65d3f0..ce2b25dd 100644 --- a/Sources/Client/Views/ServerList/ServerListView.swift +++ b/Sources/Client/Views/ServerList/ServerListView.swift @@ -145,7 +145,9 @@ struct ServerListView: View { /// Ping all servers and clear discovered LAN servers. func refresh() { for pinger in pingers { - try? pinger.ping() + Task { + try? await pinger.ping() + } } lanServerEnumerator?.clear() diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 793329be..3c88bc35 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -31,7 +31,7 @@ var targets: [Target] = [ .product(name: "Collections", package: "swift-collections"), .product(name: "OrderedCollections", package: "swift-collections"), .product(name: "FirebladeMath", package: "fireblade-math"), - .product(name: "Resolver", package: "swift-resolver"), + .product(name: "AsyncDNSResolver", package: "swift-async-dns-resolver"), .product(name: "Z", package: "swift-package-zlib"), .product(name: "SwiftImage", package: "swift-image"), .product(name: "PNG", package: "swift-png") @@ -85,7 +85,7 @@ let package = Package( .package(url: "https://github.com/michaeleisel/ZippyJSON", from: "1.2.4"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.12.1"), .package(url: "https://github.com/stackotter/fireblade-math.git", branch: "matrix2x2"), - .package(url: "https://github.com/seznam/swift-resolver", from: "0.3.0"), + .package(url: "https://github.com/apple/swift-async-dns-resolver.git", from: "0.4.0"), .package(url: "https://github.com/fourplusone/swift-package-zlib", from: "1.2.11"), .package(url: "https://github.com/stackotter/swift-image.git", branch: "master"), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), diff --git a/Sources/Core/Sources/Client.swift b/Sources/Core/Sources/Client.swift index c20bc72c..b6b0288b 100644 --- a/Sources/Core/Sources/Client.swift +++ b/Sources/Core/Sources/Client.swift @@ -47,11 +47,11 @@ public final class Client: @unchecked Sendable { // MARK: Connection lifecycle /// Join the specified server. Throws if the packets fail to send. - public func joinServer(describedBy descriptor: ServerDescriptor, with account: Account) throws { + public func joinServer(describedBy descriptor: ServerDescriptor, with account: Account) async throws { self.account = account // Create a connection to the server - let connection = try ServerConnection( + let connection = try await ServerConnection( descriptor: descriptor, eventBus: eventBus ) diff --git a/Sources/Core/Sources/Network/LANServerEnumerator.swift b/Sources/Core/Sources/Network/LANServerEnumerator.swift index 0bbcbc48..e5cadc46 100644 --- a/Sources/Core/Sources/Network/LANServerEnumerator.swift +++ b/Sources/Core/Sources/Network/LANServerEnumerator.swift @@ -190,7 +190,9 @@ public class LANServerEnumerator: ObservableObject { // Ping the server let pinger = Pinger(server) - try? pinger.ping() + Task { + try? await pinger.ping() + } servers.append(server) ThreadUtil.runInMain { diff --git a/Sources/Core/Sources/Network/ServerConnection.swift b/Sources/Core/Sources/Network/ServerConnection.swift index 2c25fde9..32d104a2 100644 --- a/Sources/Core/Sources/Network/ServerConnection.swift +++ b/Sources/Core/Sources/Network/ServerConnection.swift @@ -1,9 +1,10 @@ import Foundation -import Resolver +import AsyncDNSResolver public enum ServerConnectionError: LocalizedError { case invalidPacketId(Int) case failedToResolveHostname(hostname: String, Error?) + case failedToCreateDNSResolver(Error?) public var errorDescription: String? { switch self { @@ -15,6 +16,8 @@ public enum ServerConnectionError: LocalizedError { Hostname: \(hostname) Reason: \(error?.localizedDescription ?? "unknown") """ + case .failedToCreateDNSResolver(let error): + return "Failed to create DNS Resolver: \(error?.localizedDescription ?? "unknown")" } } } @@ -44,8 +47,8 @@ public class ServerConnection { // MARK: Init /// Create a new connection to the specified server. - public init(descriptor: ServerDescriptor, eventBus: EventBus? = nil) throws { - let (ipAddress, port) = try ServerConnection.resolve(descriptor) + public init(descriptor: ServerDescriptor, eventBus: EventBus? = nil) async throws { + let (ipAddress, port) = try await ServerConnection.resolve(descriptor) host = descriptor.host self.ipAddress = host @@ -125,7 +128,7 @@ public class ServerConnection { } /// Resolves a server descriptor into an IP and a port. - public static func resolve(_ server: ServerDescriptor) throws -> (String, UInt16) { + public static func resolve(_ server: ServerDescriptor) async throws -> (String, UInt16) { do { // We only care about IPv4 var isIp: Bool @@ -147,20 +150,26 @@ public class ServerConnection { } // If `host` is an ip already, no need to perform DNS lookups - let resolver = Resolver(timeout: 10) + let resolver: AsyncDNSResolver + + do { + resolver = try AsyncDNSResolver(try CAresDNSResolver()) + } catch { + throw ServerConnectionError.failedToCreateDNSResolver(error) + } // Check for SRV records if no port is specified if server.port == nil { - let records = try? resolver.discover("_minecraft._tcp.\(server.host)") + let records = try? await resolver.querySRV(name: "_minecraft._tcp.\(server.host)") if let record = records?.first { - return (record.address, record.port.map(UInt16.init) ?? server.port ?? 25565) + return (record.host, record.port ?? server.port ?? 25565) } } // Check for regular records - let records = try resolver.resolve(server.host) + let records = try await resolver.queryA(name: server.host) if let record = records.first { - return (record.address, server.port ?? 25565) + return (record.address.address, server.port ?? 25565) } throw ServerConnectionError.failedToResolveHostname(hostname: server.host, nil) diff --git a/Sources/Core/Sources/Server/Ping/Pinger.swift b/Sources/Core/Sources/Server/Ping/Pinger.swift index d1102781..6e89a627 100644 --- a/Sources/Core/Sources/Server/Ping/Pinger.swift +++ b/Sources/Core/Sources/Server/Ping/Pinger.swift @@ -13,14 +13,13 @@ public class Pinger: ObservableObject { public var shouldPing = false public var isConnecting = false - private let queue: DispatchQueue - // MARK: Init public init(_ descriptor: ServerDescriptor) { self.descriptor = descriptor - queue = DispatchQueue(label: "pinger-\(descriptor.name)") - connect() + Task { + await connect() + } } // MARK: Interface @@ -29,47 +28,44 @@ public class Pinger: ObservableObject { switch event { case let event as ConnectionFailedEvent: ThreadUtil.runInMain { - response = Result.failure(PingError.connectionFailed(event.networkError)) + self.response = Result.failure(PingError.connectionFailed(event.networkError)) } default: break } } - public func ping() throws { + public func ping() async throws { if let connection = connection { ThreadUtil.runInMain { - response = nil + self.response = nil } try connection.ping() shouldPing = false } else if !isConnecting { shouldPing = true - connect() + await connect() } else { shouldPing = true } } - private func connect() { + private func connect() async { isConnecting = true - // DNS resolution sometimes takes a while so we do that in parallel - queue.async { - do { - let connection = try ServerConnection(descriptor: self.descriptor) - connection.setPacketHandler(self.handlePacket) - connection.eventBus.registerHandler(self.handleNetworkEvent) - self.connection = connection - self.isConnecting = false - if self.shouldPing { - try? self.ping() - } - } catch { - self.isConnecting = false - log.trace("Failed to create server connection") - ThreadUtil.runInMain { - self.response = Result.failure(.connectionFailed(error)) - } + do { + let connection = try await ServerConnection(descriptor: self.descriptor) + connection.setPacketHandler(self.handlePacket) + connection.eventBus.registerHandler(self.handleNetworkEvent) + self.connection = connection + self.isConnecting = false + if self.shouldPing { + try? await self.ping() + } + } catch { + self.isConnecting = false + log.trace("Failed to create server connection") + ThreadUtil.runInMain { + self.response = Result.failure(.connectionFailed(error)) } } } From ab8a3d498e60130918073c8790c1813dd57a71d7 Mon Sep 17 00:00:00 2001 From: stackotter Date: Fri, 31 May 2024 15:51:21 +1000 Subject: [PATCH 46/84] Update block breaking overlay to match vanilla better and render block breaking overlay when other players are breaking blocks too --- .../Core/Renderer/World/WorldRenderer.swift | 6 +-- .../BlockBreakAnimationPacket.swift | 4 ++ Sources/Core/Sources/World/World.swift | 47 ++++++++++++++++++- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index b32b4486..7ae0ee07 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -359,16 +359,14 @@ public final class WorldRenderer: Renderer { } } model.textureType = .transparent - // No clue why light level 12 is the right one, vanilla seems to use light level 15 here but that - // just doesn't work for us at all (way too bright). let lightLevel = LightLevel( - sky: 12, + sky: 15, block: 0 ) var neighbourLightLevels: [Direction: LightLevel] = [:] for direction in Direction.allDirections { neighbourLightLevels[direction] = LightLevel( - sky: 12, + sky: 15, block: 0 ) } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockBreakAnimationPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockBreakAnimationPacket.swift index 935d4dee..8e4cfd45 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockBreakAnimationPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockBreakAnimationPacket.swift @@ -12,4 +12,8 @@ public struct BlockBreakAnimationPacket: ClientboundPacket { location = try packetReader.readBlockPosition() destroyStage = try packetReader.readByte() } + + public func handle(for client: Client) throws { + client.game.world.setBlockBreakingStage(at: location, to: Int(destroyStage), for: entityId) + } } diff --git a/Sources/Core/Sources/World/World.swift b/Sources/Core/Sources/World/World.swift index 011db462..9751fdd4 100644 --- a/Sources/Core/Sources/World/World.swift +++ b/Sources/Core/Sources/World/World.swift @@ -388,6 +388,14 @@ public class World { chunk.setBlockId(at: position.relativeToChunk, to: state) lightingEngine.updateLighting(at: position, in: self) + blockBreakingLock.acquireWriteLock() + // Use removeAll instead of filter to minimize cost in the case that the set block + // wasn't associated with a breaking block (probably the most likely case?) + breakingBlocks.removeAll { block in + block.position == position + } + blockBreakingLock.unlock() + eventBus.dispatch(Event.SingleBlockUpdate( position: position, newState: state @@ -409,12 +417,13 @@ public class World { _ updates: [Event.SingleBlockUpdate], inChunkAt chunkPosition: ChunkPosition? = nil ) { + let positions = updates.map(\.position) if let chunkPosition = chunkPosition { if let chunk = chunk(at: chunkPosition) { for update in updates { chunk.setBlockId(at: update.position.relativeToChunk, to: update.newState) } - lightingEngine.updateLighting(at: updates.map(\.position), in: self) + lightingEngine.updateLighting(at: positions, in: self) } else { log.warning("Cannot handle multi-block change in non-existent chunk, chunkPosition=\(chunkPosition)") return @@ -428,8 +437,16 @@ public class World { return } } - lightingEngine.updateLighting(at: updates.map(\.position), in: self) + lightingEngine.updateLighting(at: positions, in: self) + } + + blockBreakingLock.acquireWriteLock() + // Use removeAll instead of filter to minimize cost in the case that the set block + // wasn't associated with a breaking block (probably the most likely case?) + breakingBlocks.removeAll { block in + positions.contains(block.position) } + blockBreakingLock.unlock() eventBus.dispatch(Event.MultiBlockUpdate(updates: updates)) } @@ -717,6 +734,32 @@ public class World { ) } + /// Sets the block breaking progress of a given block to a value corresponding to a specific + /// stage of the block breaking animation. If the block is not already getting broken, it gets + /// added to the list of breaking blocks. + public func setBlockBreakingStage(at position: BlockPosition, to stage: Int, for entityId: Int) { + blockBreakingLock.acquireWriteLock() + defer { blockBreakingLock.unlock() } + + let progress = Double(clamp(stage + 1, min: 0, max: 10)) / 10 + + for (i, block) in breakingBlocks.enumerated() { + if block.position == position { + breakingBlocks[i].progress = progress + breakingBlocks[i].perpetratorEntityId = entityId + return + } + } + + breakingBlocks.append( + BreakingBlock( + position: position, + perpetratorEntityId: entityId, + progress: progress + ) + ) + } + /// Does nothing if the specified block isn't getting broken. public func addBreakingProgress(_ progress: Double, toBlockAt position: BlockPosition) { blockBreakingLock.acquireWriteLock() From 9989c96d45d67497d5278daccf2e1d787e406664 Mon Sep 17 00:00:00 2001 From: stackotter Date: Fri, 31 May 2024 22:04:17 +1000 Subject: [PATCH 47/84] Redo entity movement packet handling and entity movement system, much closer to vanilla now (minimal drift and jittering) The main change was making non-velocity packets trigger 3-tick lerps. --- .../Sources/ECS/Components/EntityKindId.swift | 4 ++ .../ECS/Components/EntityLerpState.swift | 69 +++++++++++++++++++ .../ECS/Systems/EntityMovementSystem.swift | 35 +++++++++- .../ECS/Systems/PlayerInputSystem.swift | 4 +- Sources/Core/Sources/Game.swift | 6 +- .../Clientbound/EntityHeadLookPacket.swift | 2 + .../EntityPositionAndRotationPacket.swift | 35 ++++++---- .../Clientbound/EntityPositionPacket.swift | 30 +++++--- .../Clientbound/EntityRotationPacket.swift | 27 +++++--- .../Clientbound/EntityTeleportPacket.swift | 28 ++++---- .../Play/Clientbound/SpawnEntityPacket.swift | 1 + .../Clientbound/SpawnLivingEntityPacket.swift | 1 + .../Play/Clientbound/SpawnPlayerPacket.swift | 1 + .../Sources/Network/ServerConnection.swift | 11 ++- .../Sources/Registry/Entity/EntityKind.swift | 16 ++++- .../Registry/Pixlyzer/PixlyzerEntity.swift | 13 ++-- .../Registry/Pixlyzer/PixlyzerFormatter.swift | 39 +++++++++-- 17 files changed, 254 insertions(+), 68 deletions(-) create mode 100644 Sources/Core/Sources/ECS/Components/EntityLerpState.swift diff --git a/Sources/Core/Sources/ECS/Components/EntityKindId.swift b/Sources/Core/Sources/ECS/Components/EntityKindId.swift index 56dcea00..68515554 100644 --- a/Sources/Core/Sources/ECS/Components/EntityKindId.swift +++ b/Sources/Core/Sources/ECS/Components/EntityKindId.swift @@ -3,6 +3,10 @@ import FirebladeECS /// A component storing the id of an entity's kind. public class EntityKindId: Component { public var id: Int + + public var entityKind: EntityKind? { + RegistryStore.shared.entityRegistry.entity(withId: id) + } public init(_ id: Int) { self.id = id diff --git a/Sources/Core/Sources/ECS/Components/EntityLerpState.swift b/Sources/Core/Sources/ECS/Components/EntityLerpState.swift new file mode 100644 index 00000000..f7cf0ebb --- /dev/null +++ b/Sources/Core/Sources/ECS/Components/EntityLerpState.swift @@ -0,0 +1,69 @@ +import Foundation +import CoreFoundation +import FirebladeECS +import FirebladeMath + +/// A component storing the lerp (if any) that an entity is currently undergoing; lerp is short +/// for linear interpolation. +public class EntityLerpState: Component { + public var currentLerp: Lerp? + + public struct Lerp { + public var targetPosition: Vec3d + public var targetPitch: Float + public var targetYaw: Float + public var ticksRemaining: Int + + public init( + targetPosition: Vec3d, + targetPitch: Float, + targetYaw: Float, + ticksRemaining: Int + ) { + self.targetPosition = targetPosition + self.targetPitch = targetPitch + self.targetYaw = targetYaw + self.ticksRemaining = ticksRemaining + } + } + + public init() {} + + /// Initiate a lerp to the given position and rotation. + /// - Parameters: + /// - position: Target position. + /// - pitch: Target pitch. + /// - yaw: Target yaw. + /// - duration: Lerp duration in ticks. + public func lerp(to position: Vec3d, pitch: Float, yaw: Float, duration: Int) { + currentLerp = Lerp( + targetPosition: position, + targetPitch: pitch, + targetYaw: yaw, + ticksRemaining: duration + ) + } + + /// Ticks an entities current lerp returning the entity's new position, pitch, and yaw. If there's no current + /// lerp, then `nil` is returned. + public func tick(position: Vec3d, pitch: Float, yaw: Float) -> (position: Vec3d, pitch: Float, yaw: Float)? { + guard var lerp = currentLerp else { + return nil + } + + let progress = 1 / Double(lerp.ticksRemaining) + + lerp.ticksRemaining -= 1 + if lerp.ticksRemaining == 0 { + currentLerp = nil + } else { + currentLerp = lerp + } + + return ( + MathUtil.lerp(from: position, to: lerp.targetPosition, progress: progress), + MathUtil.lerp(from: pitch, to: lerp.targetPitch, progress: Float(progress)), + MathUtil.lerp(from: yaw, to: lerp.targetYaw, progress: Float(progress)) + ) + } +} diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index dea4ac25..7c1c52c0 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -7,16 +7,45 @@ public struct EntityMovementSystem: System { let physicsEntities = nexus.family( requiresAll: EntityPosition.self, EntityVelocity.self, + EntityRotation.self, + EntityLerpState.self, + EntityKindId.self, EntityOnGround.self, excludesAll: ClientPlayerEntity.self ) - for (position, velocity, onGround) in physicsEntities { + for (position, velocity, rotation, lerpState, kind, onGround) in physicsEntities { + guard let kind = RegistryStore.shared.entityRegistry.entity(withId: kind.id) else { + log.warning("Unknown entity kind '\(kind.id)'") + continue + } + + if let (newPosition, newPitch, newYaw) = lerpState.tick(position: position.vector, pitch: rotation.pitch, yaw: rotation.yaw) { + position.vector = newPosition + rotation.pitch = newPitch + rotation.yaw = newYaw + return + } + + velocity.vector *= 0.98 + if onGround.onGround { velocity.vector.y = 0 } else { - velocity.vector.y *= 0.98 - velocity.vector.y -= 0.04 + if kind.identifier == Identifier(name: "item") { + velocity.vector.y *= 0.98 + velocity.vector.y -= 0.04 + } + } + + if abs(velocity.vector.x) < 0.003 { + velocity.vector.x = 0 + } + if abs(velocity.vector.y) < 0.003 { + velocity.vector.y = 0 + } + if abs(velocity.vector.z) < 0.003 { + velocity.vector.z = 0 } position.move(by: velocity.vector) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index fdf8fa39..692e505d 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -40,13 +40,11 @@ public final class PlayerInputSystem: System { PlayerInventory.self, EntityCamera.self, PlayerGamemode.self, - PlayerAttributes.self, EntitySneaking.self, - EntityId.self, ClientPlayerEntity.self ).makeIterator() - guard let (rotation, inventory, camera, gamemode, attributes, sneaking, playerEntityId, _) = family.next() else { + guard let (rotation, inventory, camera, gamemode, sneaking, _) = family.next() else { log.error("PlayerInputSystem failed to get player to tick") return } diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 4d4dca57..0bea7b45 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -287,9 +287,9 @@ 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, action: (Entity) -> Void) { - nexusLock.acquireWriteLock() - defer { nexusLock.unlock() } + public func accessEntity(id: Int, acquireLock: Bool = true, action: (Entity) -> Void) { + if acquireLock { nexusLock.acquireWriteLock() } + defer { if acquireLock { nexusLock.unlock() } } if let identifier = entityIdToEntityIdentifier[id] { action(nexus.entity(from: identifier)) diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityHeadLookPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityHeadLookPacket.swift index 63950866..75a9546f 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityHeadLookPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityHeadLookPacket.swift @@ -13,6 +13,8 @@ public struct EntityHeadLookPacket: ClientboundEntityPacket { /// Should only be called if a nexus write lock is already acquired. public func handle(for client: Client) throws { + // TODO: Lerp entity head rotation (with a lerp duration of 3 ticks) + // Would be best to implement by modifying EntityLerpState client.game.accessComponent(entityId: entityId, EntityHeadYaw.self, acquireLock: false) { component in component.yaw = headYaw } 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 6dff4862..19c27fde 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionAndRotationPacket.swift @@ -33,24 +33,29 @@ public struct EntityPositionAndRotationPacket: ClientboundEntityPacket { let x = Double(deltaX) / 4096 let y = Double(deltaY) / 4096 let z = Double(deltaZ) / 4096 + let relativePosition = Vec3d(x, y, z) + + client.game.accessEntity(id: entityId, acquireLock: false) { entity in + guard + let position = entity.get(component: EntityPosition.self), + let lerpState = entity.get(component: EntityLerpState.self), + let velocity = entity.get(component: EntityVelocity.self), + let kind = entity.get(component: EntityKindId.self)?.entityKind, + let onGroundComponent = entity.get(component: EntityOnGround.self) + else { + return + } - client.game.accessComponent(entityId: entityId, EntityPosition.self, acquireLock: false) { position in - position.move(by: Vec3d(x, y, z)) - } - - client.game.accessComponent(entityId: entityId, EntityRotation.self, acquireLock: false) { rotation in - rotation.pitch = pitch - rotation.yaw = yaw - } + velocity.vector = .zero - client.game.accessComponent(entityId: entityId, EntityOnGround.self, acquireLock: false) { onGroundComponent in + let currentTargetPosition = lerpState.currentLerp?.targetPosition ?? position.vector onGroundComponent.onGround = onGround - } - - if onGround { - client.game.accessComponent(entityId: entityId, EntityVelocity.self, acquireLock: false) { velocity in - velocity.y = 0 - } + lerpState.lerp( + to: currentTargetPosition + relativePosition, + pitch: pitch, + yaw: yaw, + duration: kind.defaultLerpDuration + ) } } } 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 70d522a4..753512d1 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift @@ -30,16 +30,30 @@ public struct EntityPositionPacket: ClientboundEntityPacket { let z = Double(deltaZ) / 4096 let relativePosition = Vec3d(x, y, z) - client.game.accessComponent(entityId: entityId, EntityPosition.self, acquireLock: false) { position in - position.move(by: relativePosition) - } - - client.game.accessComponent(entityId: entityId, EntityOnGround.self, acquireLock: false) { onGroundComponent in - onGroundComponent.onGround = onGround - } + client.game.accessEntity(id: entityId, acquireLock: false) { entity in + guard + let position = entity.get(component: EntityPosition.self), + let rotation = entity.get(component: EntityRotation.self), + let lerpState = entity.get(component: EntityLerpState.self), + let velocity = entity.get(component: EntityVelocity.self), + let kind = entity.get(component: EntityKindId.self)?.entityKind, + let onGroundComponent = entity.get(component: EntityOnGround.self) + else { + return + } - client.game.accessComponent(entityId: entityId, EntityVelocity.self, acquireLock: false) { velocity in velocity.vector = .zero + + // TODO: When lerping for a minecart, the velocity should get set to the relative + // position too. + let currentTargetPosition = lerpState.currentLerp?.targetPosition ?? position.vector + onGroundComponent.onGround = onGround + lerpState.lerp( + to: currentTargetPosition + relativePosition, + pitch: rotation.pitch, + yaw: rotation.yaw, + duration: kind.defaultLerpDuration + ) } } } 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 eaff1b2d..cdad976d 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityRotationPacket.swift @@ -20,19 +20,24 @@ public struct EntityRotationPacket: ClientboundEntityPacket { /// Should only be called if a nexus write lock is already acquired. public func handle(for client: Client) throws { - client.game.accessComponent(entityId: entityId, EntityRotation.self, acquireLock: false) { rotation in - rotation.pitch = pitch - rotation.yaw = yaw - } + client.game.accessEntity(id: entityId, acquireLock: false) { entity in + guard + let position = entity.get(component: EntityPosition.self), + let lerpState = entity.get(component: EntityLerpState.self), + let kind = entity.get(component: EntityKindId.self)?.entityKind, + let onGroundComponent = entity.get(component: EntityOnGround.self) + else { + return + } - client.game.accessComponent(entityId: entityId, EntityOnGround.self, acquireLock: false) { onGroundComponent in + let currentTargetPosition = lerpState.currentLerp?.targetPosition ?? position.vector onGroundComponent.onGround = onGround - } - - if onGround { - client.game.accessComponent(entityId: entityId, EntityVelocity.self, acquireLock: false) { velocity in - velocity.y = 0 - } + lerpState.lerp( + to: currentTargetPosition, + pitch: pitch, + yaw: yaw, + duration: kind.defaultLerpDuration + ) } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityTeleportPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityTeleportPacket.swift index 0f61d282..d0e9a3c0 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityTeleportPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityTeleportPacket.swift @@ -24,21 +24,25 @@ public struct EntityTeleportPacket: ClientboundEntityPacket { /// Should only be called if a nexus write lock is already acquired. public func handle(for client: Client) throws { - client.game.accessComponent(entityId: entityId, EntityPosition.self, acquireLock: false) { positionComponent in - positionComponent.move(to: position) - } + client.game.accessEntity(id: entityId, acquireLock: false) { entity in + guard + let lerpState = entity.get(component: EntityLerpState.self), + let velocity = entity.get(component: EntityVelocity.self), + let kind = entity.get(component: EntityKindId.self)?.entityKind, + let onGroundComponent = entity.get(component: EntityOnGround.self) + else { + return + } - client.game.accessComponent(entityId: entityId, EntityRotation.self, acquireLock: false) { rotation in - rotation.pitch = pitch - rotation.yaw = yaw - } + velocity.vector = .zero - client.game.accessComponent(entityId: entityId, EntityOnGround.self, acquireLock: false) { onGroundComponent in onGroundComponent.onGround = onGround - } - - client.game.accessComponent(entityId: entityId, EntityVelocity.self, acquireLock: false) { velocity in - velocity.vector = Vec3d.zero + lerpState.lerp( + to: position, + pitch: pitch, + yaw: yaw, + duration: kind.defaultLerpDuration + ) } } } 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 b0e809ed..0cead80d 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift @@ -45,6 +45,7 @@ public struct SpawnEntityPacket: ClientboundPacket { EntityPosition(position) EntityVelocity(velocity ?? .zero) EntityRotation(pitch: pitch, yaw: yaw) + EntityLerpState() EntityAttributes() } } 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 d797ea87..9f918b23 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift @@ -40,6 +40,7 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { EntityHitBox(width: entity.width, height: entity.height) EntityRotation(pitch: pitch, yaw: yaw) EntityHeadYaw(headYaw) + EntityLerpState() EntityAttributes() } } 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 fa66b887..be7fd97d 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift @@ -35,6 +35,7 @@ public struct SpawnPlayerPacket: ClientboundPacket { EntityPosition(position) EntityVelocity(0, 0, 0) EntityRotation(pitch: pitch, yaw: yaw) + EntityLerpState() EntityAttributes() } } diff --git a/Sources/Core/Sources/Network/ServerConnection.swift b/Sources/Core/Sources/Network/ServerConnection.swift index 32d104a2..cfef73ed 100644 --- a/Sources/Core/Sources/Network/ServerConnection.swift +++ b/Sources/Core/Sources/Network/ServerConnection.swift @@ -153,7 +153,7 @@ public class ServerConnection { let resolver: AsyncDNSResolver do { - resolver = try AsyncDNSResolver(try CAresDNSResolver()) + resolver = AsyncDNSResolver(try CAresDNSResolver()) } catch { throw ServerConnectionError.failedToCreateDNSResolver(error) } @@ -162,7 +162,7 @@ public class ServerConnection { if server.port == nil { let records = try? await resolver.querySRV(name: "_minecraft._tcp.\(server.host)") if let record = records?.first { - return (record.host, record.port ?? server.port ?? 25565) + return (record.host, record.port) } } @@ -207,7 +207,12 @@ public class ServerConnection { /// Sends a handshake with the goal of transitioning to the given state (either status or login). public func handshake(nextState: HandshakePacket.NextState) throws { - let handshake = HandshakePacket(protocolVersion: Constants.protocolVersion, serverAddr: host, serverPort: Int(port), nextState: nextState) + let handshake = HandshakePacket( + protocolVersion: Constants.protocolVersion, + serverAddr: host, + serverPort: Int(port), + nextState: nextState + ) try sendPacket(handshake) state = (nextState == .login) ? .login : .status } diff --git a/Sources/Core/Sources/Registry/Entity/EntityKind.swift b/Sources/Core/Sources/Registry/Entity/EntityKind.swift index b5a50b16..18ca7baa 100644 --- a/Sources/Core/Sources/Registry/Entity/EntityKind.swift +++ b/Sources/Core/Sources/Registry/Entity/EntityKind.swift @@ -10,6 +10,18 @@ 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. + public var isLiving: Bool + + /// The default duration of position/rotation linear interpolation (measured in ticks) + /// to use for this kind of entity. + public var defaultLerpDuration: Int { + if identifier == Identifier(name: "item") { + return 1 + } else { + return 3 + } + } /// Creates a new entity kind with the given properties. public init( @@ -17,12 +29,14 @@ public struct EntityKind: Codable { id: Int, width: Float, height: Float, - attributes: [EntityAttributeKey: Float] + attributes: [EntityAttributeKey: Float], + isLiving: Bool ) { self.identifier = identifier self.id = id self.width = width self.height = height self.attributes = attributes + self.isLiving = isLiving } } diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift index cc43afa1..d148c375 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerEntity.swift @@ -17,24 +17,27 @@ public struct PixlyzerEntity: Decodable { } public extension EntityKind { - init?(_ pixlyzerEntity: PixlyzerEntity, identifier: Identifier) { + /// 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 { guard let id = pixlyzerEntity.id else { return nil } self.id = id self.identifier = identifier + self.isLiving = isLiving width = pixlyzerEntity.width ?? 0 height = pixlyzerEntity.height ?? 0 attributes = [:] for (attribute, value) in pixlyzerEntity.attributes ?? [:] { - if let attribute = EntityAttributeKey(rawValue: attribute) { - attributes[attribute] = value - } else { - log.warning("Unknown entity attribute in pixlyzer registry: '\(attribute)'") + guard let attribute = EntityAttributeKey(rawValue: attribute) else { + throw PixlyzerError.unknownEntityAttribute(attribute) } + + attributes[attribute] = value } } } diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift index 8db5ec39..a955842e 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift @@ -11,25 +11,36 @@ public enum PixlyzerError: LocalizedError { case invalidUTF8BlockName(String) /// Failed to get the water fluid from the fluid registry. case failedToGetWaterFluid + /// Unknown entity attribute found in Pixlyzer entity registry. + case unknownEntityAttribute(String) + /// Expected an entity with the given name to be present in the Pixlyzer entity registry. + case missingEntity(String) public var errorDescription: String? { switch self { - case .missingBlockId(let id): + case let .missingBlockId(id): return "The block with id: \(id) is missing." - case .invalidAABBVertexLength(let length): + case let .invalidAABBVertexLength(length): return """ An AABB's vertex is of invalid length. length: \(length) """ case .entityRegistryMissingPlayer: return "The entity registry does not contain the player entity." - case .invalidUTF8BlockName(let blockName): + case let .invalidUTF8BlockName(blockName): return """ 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) + """ + case let .missingEntity(name): + return "Expected entity kind '\(name)' to be present in Pixlyzer entity registry." } } } @@ -154,7 +165,27 @@ public enum PixlyzerFormatter { var entities: [Int: EntityKind] = [:] for (identifier, pixlyzerEntity) in pixlyzerEntities { if let identifier = try? Identifier(identifier) { - if let entity = EntityKind(pixlyzerEntity, identifier: identifier) { + var isLiving = false + var parent = pixlyzerEntity.parent + 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 + } + } + + // 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) { entities[entity.id] = entity } } From 1f513f7166c69b6331e3e7106bc79bbe184a9324 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 1 Jun 2024 00:04:19 +1000 Subject: [PATCH 48/84] Handle mouse slot when handling SetSlotPacket, and fix packets sent by item dropping code Had some misconceptions about which packets should be sent for the different types of item/stack dropping from the inventory (which led to certain ways of dropping items just getting rejected by the server, e.g. right clicking with a stack outside the inventory --- .../ECS/Systems/PlayerInputSystem.swift | 2 +- Sources/Core/Sources/GUI/InGameGUI.swift | 8 +- Sources/Core/Sources/GUI/Window.swift | 86 +++++++++++-------- .../Play/Clientbound/SetSlotPacket.swift | 18 +++- .../WindowConfirmationClientboundPacket.swift | 1 + .../Play/Serverbound/ClickWindowPacket.swift | 20 ++--- 6 files changed, 81 insertions(+), 54 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 692e505d..81387e21 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -121,7 +121,7 @@ public final class PlayerInputSystem: System { inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 8) % 9 case .dropItem: let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot - inventory.window.dropItem(slotIndex, connection: connection) + inventory.window.dropItemFromSlot(slotIndex, mouseItemStack: nil, connection: connection) case .place: // Block breaking is handled by ``PlayerBlockBreakingSystem``, this just handles hand animation and // other non breaking things for the `.destroy` input (e.g. attacking) diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 8641f18b..efa79f59 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -204,14 +204,14 @@ public class InGameGUI { window.dropStackFromMouse(&state.mouseItemStack, connection: connection) } .onRightClick { - // TODO: Figure out why the server isn't respecting this (pretty certain that we're sending - // the ClickWindowPacket with the `dropStack(slot: nil)` action, which should be correct??) window.dropItemFromMouse(&state.mouseItemStack, connection: connection) } GUIElement.stack { - // Has a dummy click handler to block clicks within the inventory from propagating to the background - window.type.background.onClick {} + // Has a dummy click handler to prevent clicks within the inventory from propagating to the background + window.type.background.onHoverKeyPress { event in + return event.key == .leftMouseButton || event.key == .rightMouseButton + } GUIElement.stack(elements: window.type.areas.map { area in windowArea( diff --git a/Sources/Core/Sources/GUI/Window.swift b/Sources/Core/Sources/GUI/Window.swift index ec12429c..c8f1d126 100644 --- a/Sources/Core/Sources/GUI/Window.swift +++ b/Sources/Core/Sources/GUI/Window.swift @@ -143,19 +143,11 @@ public class Window { let slotInputs: [Input] = [.slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9] if input == .dropItem { - guard mouseStack == nil else { - return true - } - - guard slots[slotIndex].stack?.count ?? 0 != 0 else { - return true - } - let dropWholeStack = inputState.keys.contains(where: \.isControl) if dropWholeStack { - dropStack(slotIndex, connection: connection) + dropStackFromSlot(slotIndex, mouseItemStack: mouseStack, connection: connection) } else { - dropItem(slotIndex, connection: connection) + dropItemFromSlot(slotIndex, mouseItemStack: mouseStack, connection: connection) } } else if let hotBarSlot = slotInputs.firstIndex(of: input) { guard mouseStack == nil else { @@ -191,52 +183,76 @@ public class Window { try connection?.sendPacket(CloseWindowServerboundPacket(windowId: UInt8(id))) } - public func dropItem(_ slotIndex: Int, connection: ServerConnection?) { - var dummy: ItemStack? = nil - drop(slotIndex: slotIndex, wholeStack: false, mouseItemStack: &dummy, connection: connection) + public func dropItemFromSlot(_ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection?) { + dropFromSlot(slotIndex, wholeStack: false, mouseItemStack: mouseItemStack, connection: connection) } - public func dropStack(_ slotIndex: Int, connection: ServerConnection?) { - var dummy: ItemStack? = nil - drop(slotIndex: slotIndex, wholeStack: true, mouseItemStack: &dummy, connection: connection) + public func dropStackFromSlot(_ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection?) { + dropFromSlot(slotIndex, wholeStack: true, mouseItemStack: mouseItemStack, connection: connection) } public func dropItemFromMouse(_ mouseStack: inout ItemStack?, connection: ServerConnection?) { - drop(slotIndex: nil, wholeStack: false, mouseItemStack: &mouseStack, connection: connection) + dropFromMouse(wholeStack: false, mouseItemStack: &mouseStack, connection: connection) } public func dropStackFromMouse(_ mouseStack: inout ItemStack?, connection: ServerConnection?) { - drop(slotIndex: nil, wholeStack: true, mouseItemStack: &mouseStack, connection: connection) + dropFromMouse(wholeStack: true, mouseItemStack: &mouseStack, connection: connection) } - private func drop( - slotIndex: Int?, + public func dropFromMouse( wholeStack: Bool, - mouseItemStack: inout ItemStack?, + mouseItemStack mouseStack: inout ItemStack?, connection: ServerConnection? ) { - let clickedItem = slotIndex.map { slots[$0] } ?? Slot(mouseItemStack) - - let dropCount = wholeStack ? clickedItem.stack?.count ?? 0 : 1 - if let index = slotIndex { - slots[index].stack?.count -= dropCount - if slots[index].stack?.count == 0 { - slots[index].stack = nil + let slot = Slot(mouseStack) + if wholeStack { + mouseStack = nil + } else if var stack = mouseStack { + stack.count -= 1 + if stack.count == 0 { + mouseStack = nil + } else { + mouseStack = stack } - } else { - mouseItemStack?.count -= dropCount - if mouseItemStack?.count == 0 { - mouseItemStack = nil + } + + do { + try connection?.sendPacket(ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: wholeStack ? .leftClick(slot: nil) : .rightClick(slot: nil), + clickedItem: slot + )) + } catch { + log.warning("Failed to send click window packet for item drop: \(error)") + } + } + + private func dropFromSlot( + _ slotIndex: Int, + wholeStack: Bool, + mouseItemStack: ItemStack?, + connection: ServerConnection? + ) { + if mouseItemStack == nil { + if wholeStack { + slots[slotIndex].stack = nil + } else if var stack = slots[slotIndex].stack { + stack.count -= 1 + if stack.count == 0 { + slots[slotIndex].stack = nil + } else { + slots[slotIndex].stack = stack + } } } - let index = slotIndex.map(Int16.init) do { try connection?.sendPacket(ClickWindowPacket( windowId: UInt8(id), actionId: generateActionId(), - action: wholeStack ? .dropStack(slot: index) : .dropOne(slot: index), - clickedItem: clickedItem + action: wholeStack ? .dropStack(slot: Int16(slotIndex)) : .dropOne(slot: Int16(slotIndex)), + clickedItem: Slot(ItemStack(itemId: -1, itemCount: 1)) )) } catch { log.warning("Failed to send click window packet for item drop: \(error)") diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SetSlotPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SetSlotPacket.swift index 8a72a454..cc42e55a 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SetSlotPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SetSlotPacket.swift @@ -3,6 +3,10 @@ import Foundation public struct SetSlotPacket: ClientboundPacket { public static let id: Int = 0x16 + public static let mouseSlot = -1 + public static let mouseWindowId = -1 + public static let inventoryNoAnimationWindowId = -2 + public var windowId: Int8 public var slot: Int16 public var slotData: Slot @@ -17,8 +21,18 @@ public struct SetSlotPacket: ClientboundPacket { let slot = Int(slot) let windowId = Int(windowId) + if slot == Self.mouseSlot && windowId == Self.mouseWindowId { + client.game.mutateGUIState { guiState in + guiState.mouseItemStack = slotData.stack + } + return + } + // Only player inventory is handled at the moment - guard windowId == PlayerInventory.windowId || windowId == -2 else { + guard + windowId == PlayerInventory.windowId + || windowId == Self.inventoryNoAnimationWindowId + else { return } @@ -27,13 +41,13 @@ public struct SetSlotPacket: ClientboundPacket { throw ClientboundPacketError.invalidInventorySlotIndex(slot, windowId: windowId) } + // TODO: Figure out why this fails when joining servers like Hypixel // If window id is 0, only hotbar slots can be sent (and should be animated) // if windowId == 0 { // guard slot >= PlayerInventory.hotbarSlotStartIndex && slot <= PlayerInventory.hotbarSlotEndIndex else { // throw ClientboundPacketError.invalidInventorySlotIndex(slot, window: windowId) // } // } - // TODO: Figure out why this fails when joining servers like Hypixel client.game.accessPlayer { player in if Int(windowId) == player.inventory.window.id { diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/WindowConfirmationClientboundPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/WindowConfirmationClientboundPacket.swift index a6226207..404d3a5b 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/WindowConfirmationClientboundPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/WindowConfirmationClientboundPacket.swift @@ -14,6 +14,7 @@ public struct WindowConfirmationClientboundPacket: ClientboundPacket { } public func handle(for client: Client) throws { + // TODO: Should this return false when the `accepted` (from the server) is false? try client.sendPacket(WindowConfirmationServerboundPacket( windowId: windowId, actionNumber: actionNumber, diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift index 03553aca..489618c9 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Serverbound/ClickWindowPacket.swift @@ -11,16 +11,14 @@ public struct ClickWindowPacket: ServerboundPacket { public var clickedItem: Slot public enum Action { - case leftClick(slot: Int16) - case rightClick(slot: Int16) + case leftClick(slot: Int16?) + case rightClick(slot: Int16?) case shiftLeftClick(slot: Int16) case shiftRightClick(slot: Int16) case numberKey(slot: Int16, number: Int8) case middleClick(slot: Int16) - /// If slot is nil, drop item from the stack currently getting moved by the mouse. - case dropOne(slot: Int16?) - /// If slot is nil, drop the stack currently getting moved by the mouse. - case dropStack(slot: Int16?) + case dropOne(slot: Int16) + case dropStack(slot: Int16) case leftClickOutsideInventory case rightClickOutsideInventory case startLeftDrag @@ -37,9 +35,9 @@ public struct ClickWindowPacket: ServerboundPacket { var rawValue: (mode: Int32, button: Int8, slot: Int16?) { switch self { case let .leftClick(slot): - return (0, 0, slot) + return (0, 0, slot ?? -999) case let .rightClick(slot): - return (0, 1, slot) + return (0, 1, slot ?? -999) case let .shiftLeftClick(slot): return (1, 0, slot) case let .shiftRightClick(slot): @@ -49,11 +47,9 @@ public struct ClickWindowPacket: ServerboundPacket { case let .middleClick(slot): return (3, 2, slot) case let .dropOne(slot): - // -1 indicates that the slot attached to the mouse cursor is to be used - return (4, 0, slot ?? -1) + return (4, 0, slot) case let .dropStack(slot): - // -1 indicates that the slot attached to the mouse cursor is to be used - return (4, 1, slot ?? -1) + return (4, 1, slot) case .leftClickOutsideInventory: return (4, 0, nil) case .rightClickOutsideInventory: From 109dc6a7e32c59858ea8dbd412b177a70aabf1f4 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 1 Jun 2024 01:56:26 +1000 Subject: [PATCH 49/84] Implement boss bar packet handling and GUI --- Readme.md | 7 +- Sources/Core/Sources/GUI/BossBar.swift | 94 +++++++++++++++++++ Sources/Core/Sources/GUI/GUIElement.swift | 4 +- Sources/Core/Sources/GUI/GUISprite.swift | 59 ++++++++++++ Sources/Core/Sources/GUI/InGameGUI.swift | 23 ++++- Sources/Core/Sources/GUIState.swift | 3 + .../Protocol/Packets/ClientboundPacket.swift | 40 +++++++- .../Play/Clientbound/BossBarPacket.swift | 93 +++++++++++++++--- 8 files changed, 298 insertions(+), 25 deletions(-) create mode 100644 Sources/Core/Sources/GUI/BossBar.swift diff --git a/Readme.md b/Readme.md index f39946d1..000c8e16 100644 --- a/Readme.md +++ b/Readme.md @@ -106,8 +106,9 @@ Not every version will be perfectly supported but I will try and have the most p - [ ] GUI - [x] Chat - [x] F3-style stuff - - [ ] Bossbars + - [x] Bossbars - [ ] Scoreboard + - [ ] Tab menu - [x] Health, hunger and experience - [x] Hotbar - [ ] Inventory @@ -124,9 +125,9 @@ Not every version will be perfectly supported but I will try and have the most p - [x] Collision system - [ ] Interaction - [x] Block placing - - [ ] Block breaking + - [x] Block breaking - [ ] Block entity interaction - - [ ] Entity interaction + - [x] Entity interaction - [ ] Particles - [ ] Basic particle system - [ ] Block break particles diff --git a/Sources/Core/Sources/GUI/BossBar.swift b/Sources/Core/Sources/GUI/BossBar.swift new file mode 100644 index 00000000..beb41c42 --- /dev/null +++ b/Sources/Core/Sources/GUI/BossBar.swift @@ -0,0 +1,94 @@ +import Foundation + +public struct BossBar { + public var id: UUID + public var title: ChatComponent + /// The boss's health as a value from 0 to 1. + public var health: Float + public var color: Color + public var style: Style + public var flags: Flags + + public enum Color: Int { + case pink = 0 + case blue = 1 + case red = 2 + case green = 3 + case yellow = 4 + case purple = 5 + case white = 6 + + /// The background and foreground sprites used when rendering a boss bar of + /// this color. + public var sprites: (background: GUISprite, foreground: GUISprite) { + switch self { + case .pink: + return (.pinkBossBarBackground, .pinkBossBarForeground) + case .blue: + return (.blueBossBarBackground, .blueBossBarForeground) + case .red: + return (.redBossBarBackground, .redBossBarForeground) + case .green: + return (.greenBossBarBackground, .greenBossBarForeground) + case .yellow: + return (.yellowBossBarBackground, .yellowBossBarForeground) + case .purple: + return (.purpleBossBarBackground, .purpleBossBarForeground) + case .white: + return (.whiteBossBarBackground, .whiteBossBarForeground) + } + } + } + + public enum Style: Int { + case noNotches = 0 + case sixNotches = 1 + case tenNotches = 2 + case twelveNotches = 3 + case twentyNotches = 4 + + /// The bar overlay sprite used when rendering a boss bar of this style. + public var overlay: GUISprite { + switch self { + case .noNotches: + return .bossBarNoNotchOverlay + case .sixNotches: + return .bossBarSixNotchOverlay + case .tenNotches: + return .bossBarTenNotchOverlay + case .twelveNotches: + return .bossBarTwelveNotchOverlay + case .twentyNotches: + return .bossBarTwentyNotchOverlay + } + } + } + + public struct Flags { + public var darkenSky: Bool + public var createFog: Bool + public var isEnderDragonHealthBar: Bool + + public init(darkenSky: Bool, createFog: Bool, isEnderDragonHealthBar: Bool) { + self.darkenSky = darkenSky + self.createFog = createFog + self.isEnderDragonHealthBar = isEnderDragonHealthBar + } + } + + public init( + id: UUID, + title: ChatComponent, + health: Float, + color: BossBar.Color, + style: BossBar.Style, + flags: BossBar.Flags + ) { + self.id = id + self.title = title + self.health = health + self.color = color + self.style = style + self.flags = flags + } +} diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 06ea4db6..9ebfab39 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -81,7 +81,7 @@ public indirect enum GUIElement { } case text(_ content: String, wrap: Bool = false, color: Vec4f = Vec4f(1, 1, 1, 1)) - case message(_ message: ChatMessage, wrap: Bool = true) + case message(_ message: ChatComponent, wrap: Bool = true) case interactable(_ element: GUIElement, handleInteraction: (Interaction) -> Bool) case sprite(GUISprite) case customSprite(GUISpriteDescriptor) @@ -340,7 +340,7 @@ public indirect enum GUIElement { ) children = [] case let .message(message, wrap): - let text = message.content.toText(with: locale) + let text = message.toText(with: locale) return GUIElement.text(text, wrap: wrap) .resolveConstraints(availableSize: availableSize, font: font, locale: locale) case let .interactable(label, handleInteraction): diff --git a/Sources/Core/Sources/GUI/GUISprite.swift b/Sources/Core/Sources/GUI/GUISprite.swift index ec5cf31d..bd1ab2e3 100644 --- a/Sources/Core/Sources/GUI/GUISprite.swift +++ b/Sources/Core/Sources/GUI/GUISprite.swift @@ -22,6 +22,27 @@ public enum GUISprite { case singleChestTopHalf case singleChestBottomHalf + case pinkBossBarBackground + case pinkBossBarForeground + case blueBossBarBackground + case blueBossBarForeground + case redBossBarBackground + case redBossBarForeground + case greenBossBarBackground + case greenBossBarForeground + case yellowBossBarBackground + case yellowBossBarForeground + case purpleBossBarBackground + case purpleBossBarForeground + case whiteBossBarBackground + case whiteBossBarForeground + + case bossBarNoNotchOverlay + case bossBarSixNotchOverlay + case bossBarTenNotchOverlay + case bossBarTwelveNotchOverlay + case bossBarTwentyNotchOverlay + /// The descriptor for the sprite. public var descriptor: GUISpriteDescriptor { switch self { @@ -61,6 +82,44 @@ public enum GUISprite { return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 71]) case .singleChestBottomHalf: return GUISpriteDescriptor(slice: .genericContainer, position: [0, 125], size: [176, 97]) + case .pinkBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 0], size: [182, 5]) + case .pinkBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 5], size: [182, 5]) + case .blueBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 10], size: [182, 5]) + case .blueBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 15], size: [182, 5]) + case .redBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 20], size: [182, 5]) + case .redBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 25], size: [182, 5]) + case .greenBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 30], size: [182, 5]) + case .greenBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 35], size: [182, 5]) + case .yellowBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 40], size: [182, 5]) + case .yellowBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 45], size: [182, 5]) + case .purpleBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 50], size: [182, 5]) + case .purpleBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 55], size: [182, 5]) + case .whiteBossBarBackground: + return GUISpriteDescriptor(slice: .bars, position: [0, 60], size: [182, 5]) + case .whiteBossBarForeground: + return GUISpriteDescriptor(slice: .bars, position: [0, 65], size: [182, 5]) + case .bossBarNoNotchOverlay: + return GUISpriteDescriptor(slice: .bars, position: [0, 70], size: [182, 5]) + case .bossBarSixNotchOverlay: + return GUISpriteDescriptor(slice: .bars, position: [0, 80], size: [182, 5]) + case .bossBarTenNotchOverlay: + return GUISpriteDescriptor(slice: .bars, position: [0, 90], size: [182, 5]) + case .bossBarTwelveNotchOverlay: + return GUISpriteDescriptor(slice: .bars, position: [0, 100], size: [182, 5]) + case .bossBarTwentyNotchOverlay: + return GUISpriteDescriptor(slice: .bars, position: [0, 110], size: [182, 5]) } } } diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index efa79f59..3d44e229 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -47,6 +47,11 @@ public class InGameGUI { } } + GUIElement.forEach(in: state.bossBars, spacing: 3) { bossBar in + self.bossBar(bossBar) + } + .constraints(.top(2), .center) + if state.showDebugScreen { debugScreen(game: game, state: state) } @@ -74,6 +79,22 @@ public class InGameGUI { } } + public func bossBar(_ bossBar: BossBar) -> GUIElement { + let (background, foreground) = bossBar.color.sprites + return GUIElement.list(spacing: 1) { + GUIElement.message(bossBar.title, wrap: false) + .constraints(.top(0), .center) + + GUIElement.stack { + continuousMeter(bossBar.health, background: background, foreground: foreground) + // TODO: Render both the background and foreground overlays separately (instead of assuming + // that they're both the same like they are in the vanilla resource pack) + GUIElement.sprite(bossBar.style.overlay) + } + } + .size(GUISprite.xpBarBackground.descriptor.size.x, nil) + } + public func chat(state: GUIStateStorage) -> GUIElement { // TODO: Implement scrollable chat history. @@ -101,7 +122,7 @@ public class InGameGUI { if !visibleMessages.isEmpty { GUIElement.forEach(in: visibleMessages, spacing: 1) { message in // TODO: Chat message text shadows - GUIElement.message(message, wrap: true) + GUIElement.message(message.content, wrap: true) } .constraints(.top(0), .left(1)) .padding(1) diff --git a/Sources/Core/Sources/GUIState.swift b/Sources/Core/Sources/GUIState.swift index faeb5e21..0b333818 100644 --- a/Sources/Core/Sources/GUIState.swift +++ b/Sources/Core/Sources/GUIState.swift @@ -56,6 +56,9 @@ public struct GUIState { /// The item stack currently being moved by the mouse. public var mouseItemStack: ItemStack? + /// Boss bars currently visible to the player. + public var bossBars: [BossBar] = [] + /// The chat input field cursor as an index into ``messageInput``. public var messageInputCursorIndex: String.Index { if let messageInput = messageInput { diff --git a/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift index 7aea0b26..a6845d19 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/ClientboundPacket.swift @@ -9,12 +9,17 @@ public enum ClientboundPacketError: LocalizedError { case invalidInventorySlotIndex(Int, windowId: Int) case invalidChangeGameStateReasonRawValue(ChangeGameStatePacket.Reason.RawValue) case invalidDimension(Identifier) + case invalidBossBarActionId(Int) + case invalidBossBarColorId(Int) + case invalidBossBarStyleId(Int) + case duplicateBossBar(UUID) + case noSuchBossBar(UUID) public var errorDescription: String? { switch self { case .invalidDifficulty: return "Invalid difficulty." - case .invalidGamemode(let rawValue): + case let .invalidGamemode(rawValue): return """ Invalid gamemode. Raw value: \(rawValue) @@ -23,27 +28,52 @@ public enum ClientboundPacketError: LocalizedError { return "Invalid server Id." case .invalidJSONString: return "Invalid JSON string." - case .invalidInventorySlotCount(let slotCount): + case let .invalidInventorySlotCount(slotCount): return """ Invalid inventory slot count. Slot count: \(slotCount) """ - case .invalidInventorySlotIndex(let slotIndex, let windowId): + case let .invalidInventorySlotIndex(slotIndex, windowId): return """ Invalid inventory slot index. Slot index: \(slotIndex) Window Id: \(windowId) """ - case .invalidChangeGameStateReasonRawValue(let rawValue): + case let .invalidChangeGameStateReasonRawValue(rawValue): return """ Invalid change game state reason. Raw value: \(rawValue) """ - case .invalidDimension(let identifier): + case let .invalidDimension(identifier): return """ Invalid dimension. Identifier: \(identifier) """ + case let .invalidBossBarActionId(actionId): + return """ + Invalid boss bar action id. + Id: \(actionId) + """ + case let .invalidBossBarColorId(colorId): + return """ + Invalid boss bar color id. + Id: \(colorId) + """ + case let .invalidBossBarStyleId(styleId): + return """ + Invalid boss bar style id. + Id: \(styleId) + """ + case let .duplicateBossBar(uuid): + return """ + Received duplicate boss bar. + UUID: \(uuid.uuidString) + """ + case let .noSuchBossBar(uuid): + return """ + Received update for non-existent boss bar. + UUID: \(uuid) + """ } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BossBarPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BossBarPacket.swift index 176bf49d..270a2f47 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BossBarPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BossBarPacket.swift @@ -5,28 +5,28 @@ public struct BossBarPacket: ClientboundPacket { public var uuid: UUID public var action: BossBarAction + public enum BossBarAction { - case add(title: ChatComponent, health: Float, color: Int, division: Int, flags: UInt8) + case add(title: ChatComponent, health: Float, color: BossBar.Color, style: BossBar.Style, flags: BossBar.Flags) case remove case updateHealth(health: Float) case updateTitle(title: ChatComponent) - case updateStyle(color: Int, division: Int) - case updateFlags(flags: UInt8) + case updateStyle(color: BossBar.Color, style:BossBar.Style) + case updateFlags(flags: BossBar.Flags) } public init(from packetReader: inout PacketReader) throws { uuid = try packetReader.readUUID() let actionId = try packetReader.readVarInt() - switch actionId { case 0: let title = try packetReader.readChat() let health = try packetReader.readFloat() - let color = try packetReader.readVarInt() - let division = try packetReader.readVarInt() - let flags = try packetReader.readUnsignedByte() - action = .add(title: title, health: health, color: color, division: division, flags: flags) + let color = try Self.readColor(from: &packetReader) + let style = try Self.readStyle(from: &packetReader) + let flags = try Self.readFlags(from: &packetReader) + action = .add(title: title, health: health, color: color, style: style, flags: flags) case 1: action = .remove case 2: @@ -36,15 +36,80 @@ public struct BossBarPacket: ClientboundPacket { let title = try packetReader.readChat() action = .updateTitle(title: title) case 4: - let color = try packetReader.readVarInt() - let division = try packetReader.readVarInt() - action = .updateStyle(color: color, division: division) + let color = try Self.readColor(from: &packetReader) + let style = try Self.readStyle(from: &packetReader) + action = .updateStyle(color: color, style: style) case 5: - let flags = try packetReader.readUnsignedByte() + let flags = try Self.readFlags(from: &packetReader) action = .updateFlags(flags: flags) default: - log.warning("invalid boss bar action id") - action = .remove + throw ClientboundPacketError.invalidBossBarActionId(actionId) + } + } + + public static func readColor(from packetReader: inout PacketReader) throws -> BossBar.Color { + let colorId = try packetReader.readVarInt() + guard let color = BossBar.Color(rawValue: colorId) else { + throw ClientboundPacketError.invalidBossBarColorId(colorId) + } + return color + } + + public static func readStyle(from packetReader: inout PacketReader) throws -> BossBar.Style { + let styleId = try packetReader.readVarInt() + guard let style = BossBar.Style(rawValue: styleId) else { + throw ClientboundPacketError.invalidBossBarStyleId(styleId) + } + return style + } + + public static func readFlags(from packetReader: inout PacketReader) throws -> BossBar.Flags { + let bitField = try packetReader.readUnsignedByte() + return BossBar.Flags( + darkenSky: bitField & 1 == 1, + createFog: bitField & 4 == 4, + isEnderDragonHealthBar: bitField & 2 == 2 + ) + } + + public func handle(for client: Client) throws { + try client.game.mutateGUIState { guiState in + for (i, var bar) in guiState.bossBars.enumerated() where bar.id == uuid { + switch action { + case .remove: + guiState.bossBars.remove(at: i) + return + case let .updateHealth(health): + bar.health = health + case let .updateTitle(title): + bar.title = title + case let .updateStyle(color, style): + bar.color = color + bar.style = style + case let .updateFlags(flags): + bar.flags = flags + case .add: + throw ClientboundPacketError.duplicateBossBar(uuid) + } + guiState.bossBars[i] = bar + return + } + + switch action { + case let .add(title, health, color, style, flags): + guiState.bossBars.append( + BossBar( + id: uuid, + title: title, + health: health, + color: color, + style: style, + flags: flags + ) + ) + default: + throw ClientboundPacketError.noSuchBossBar(uuid) + } } } } From 08632530dd1d024f2b63a0f90dd2bf59a91aff40 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 1 Jun 2024 13:14:15 +1000 Subject: [PATCH 50/84] Implement player tab list GUI overlay --- Readme.md | 6 +- Sources/Core/Sources/GUI/GUIElement.swift | 112 ++++++++++++++++--- Sources/Core/Sources/GUI/GUISprite.swift | 6 + Sources/Core/Sources/GUI/InGameGUI.swift | 48 ++++++++ Sources/Core/Sources/Player/PlayerInfo.swift | 32 ++++++ 5 files changed, 187 insertions(+), 17 deletions(-) diff --git a/Readme.md b/Readme.md index 000c8e16..ee7746f7 100644 --- a/Readme.md +++ b/Readme.md @@ -108,7 +108,7 @@ Not every version will be perfectly supported but I will try and have the most p - [x] F3-style stuff - [x] Bossbars - [ ] Scoreboard - - [ ] Tab menu + - [x] Tab list - [x] Health, hunger and experience - [x] Hotbar - [ ] Inventory @@ -123,10 +123,10 @@ Not every version will be perfectly supported but I will try and have the most p - [x] Physics loop - [x] Input system - [x] Collision system -- [ ] Interaction +- [x] Interaction - [x] Block placing - [x] Block breaking - - [ ] Block entity interaction + - [x] Block entity interaction - [x] Entity interaction - [ ] Particles - [ ] Basic particle system diff --git a/Sources/Core/Sources/GUI/GUIElement.swift b/Sources/Core/Sources/GUI/GUIElement.swift index 9ebfab39..f0473c13 100644 --- a/Sources/Core/Sources/GUI/GUIElement.swift +++ b/Sources/Core/Sources/GUI/GUIElement.swift @@ -7,7 +7,7 @@ public indirect enum GUIElement { case horizontal } - public struct DirectionSet: ExpressibleByArrayLiteral { + public struct DirectionSet: ExpressibleByArrayLiteral, Equatable { public var horizontal: Bool public var vertical: Bool @@ -94,7 +94,13 @@ public indirect enum GUIElement { case sized(element: GUIElement, width: Int?, height: Int?) case spacer(width: Int, height: Int) /// Wraps an element with a background. - case container(background: Vec4f, padding: Padding, element: GUIElement, expandDirections: DirectionSet = .neither) + case container( + element: GUIElement, + background: Vec4f?, + padding: Padding, + paddingColor: Vec4f?, + expandDirections: DirectionSet = .neither + ) case floating(element: GUIElement) case item(id: Int) @@ -103,7 +109,7 @@ public indirect enum GUIElement { case let .list(_, _, elements), let .stack(elements): return elements case let .interactable(element, _), let .positioned(element, _), - let .sized(element, _, _), let .container(_, _, element, _), + let .sized(element, _, _), let .container(element, _, _, _, _), let .floating(element): return [element] case .text, .message, .sprite, .customSprite, .spacer, .item: @@ -173,29 +179,47 @@ public indirect enum GUIElement { } public func padding(_ edges: EdgeSet, _ amount: Int) -> GUIElement { - .container(background: .zero, padding: Padding(edges: edges, amount: amount), element: self) + .container(element: self, background: nil, padding: Padding(edges: edges, amount: amount), paddingColor: nil) + } + + public func border(_ amount: Int, _ color: Vec4f) -> GUIElement { + self.border(.all, amount, color) + } + + public func border(_ edges: EdgeSet, _ amount: Int, _ color: Vec4f) -> GUIElement { + .container(element: self, background: nil, padding: Padding(edges: edges, amount: amount), paddingColor: color) } public func background(_ color: Vec4f) -> GUIElement { // Sometimes we can just update the element instead of adding another layer. switch self { - case let .container(background, padding, element, expandDirections): - if background == .zero { + case let .container(element, background, padding, paddingColor, expandDirections): + if background == nil { return .container( + element: element, background: color, padding: padding, - element: element, + paddingColor: paddingColor, expandDirections: expandDirections ) } default: break } - return .container(background: color, padding: .zero, element: self) + return .container(element: self, background: color, padding: .zero, paddingColor: nil) } public func expand(_ directions: DirectionSet = .both) -> GUIElement { - return .container(background: .zero, padding: .zero, element: self, expandDirections: directions) + if case let .container(element, background, padding, paddingColor, .neither) = self { + return .container( + element: element, + background: background, + padding: padding, + paddingColor: paddingColor, + expandDirections: directions + ) + } + return .container(element: self, background: .zero, padding: .zero, paddingColor: nil, expandDirections: directions) } public func onClick(_ action: @escaping () -> Void) -> GUIElement { @@ -368,6 +392,7 @@ public indirect enum GUIElement { var availableSize = availableSize var childPosition = Vec2i(0, 0) let axisComponent = direction == .vertical ? 1 : 0 + children = elements.map { element in var renderable = element.resolveConstraints( availableSize: availableSize, @@ -382,7 +407,9 @@ public indirect enum GUIElement { return renderable } + relativePosition = .zero + let lengthAlongAxis = elements.isEmpty ? 0 : childPosition[axisComponent] - spacing switch direction { case .vertical: @@ -392,6 +419,7 @@ public indirect enum GUIElement { let height = children.map(\.size.y).max() ?? 0 size = Vec2i(lengthAlongAxis, height) } + content = nil case let .stack(elements): children = elements.map { element in @@ -445,7 +473,7 @@ public indirect enum GUIElement { relativePosition = .zero size = Vec2i(width, height) content = nil - case let .container(background, padding, element, expandDirections): + case let .container(element, background, padding, paddingColor, expandDirections): let paddingAxisTotals = padding.axisTotals var child = element.resolveConstraints( availableSize: availableSize &- paddingAxisTotals, @@ -453,7 +481,6 @@ public indirect enum GUIElement { locale: locale ) child.relativePosition &+= Vec2i(padding.left, padding.top) - children = [child] relativePosition = .zero size = Vec2i( expandDirections.horizontal @@ -464,10 +491,67 @@ public indirect enum GUIElement { : min(availableSize.y, child.size.y + paddingAxisTotals.y) ) - if background.w != 0 { - content = .background(background) - } else { + let expandedChildSize = size &- paddingAxisTotals + + if let paddingColor = paddingColor { + // Something feels kinda dark about this variable name... + var borderChildren: [GUIRenderable] = [] + + // https://open.spotify.com/track/5hM5arv9KDbCHS0k9uqwjr?si=58df9b2231e848b8 + func addBorderLine(position: Vec2i, size: Vec2i) { + borderChildren.append(GUIRenderable( + relativePosition: position, + size: size, + content: .background(paddingColor), + children: [] + )) + } + + if padding.left != 0 { + addBorderLine( + position: [0, 0], + size: [padding.left, expandedChildSize.y + paddingAxisTotals.y] + ) + } + if padding.right != 0 { + addBorderLine( + position: [padding.left + expandedChildSize.x, 0], + size: [padding.right, expandedChildSize.y + paddingAxisTotals.y] + ) + } + if padding.top != 0 { + addBorderLine( + position: [padding.left, 0], + size: [expandedChildSize.x, padding.top] + ) + } + if padding.bottom != 0 { + addBorderLine( + position: [padding.left, padding.top + expandedChildSize.y], + size: [expandedChildSize.x, padding.bottom] + ) + } + + let backgroundChild: GUIRenderable? + if let background = background { + backgroundChild = GUIRenderable( + relativePosition: Vec2i(padding.left, padding.top), + size: child.size, + content: .background(background), + children: [] + ) + } else { + backgroundChild = nil + } + + children = borderChildren + [ + backgroundChild, + child + ].compactMap(identity) content = nil + } else { + children = [child] + content = background.map(GUIRenderable.Content.background) } case let .floating(element): let child = element.resolveConstraints( diff --git a/Sources/Core/Sources/GUI/GUISprite.swift b/Sources/Core/Sources/GUI/GUISprite.swift index bd1ab2e3..b7877a2e 100644 --- a/Sources/Core/Sources/GUI/GUISprite.swift +++ b/Sources/Core/Sources/GUI/GUISprite.swift @@ -43,6 +43,9 @@ public enum GUISprite { case bossBarTwelveNotchOverlay case bossBarTwentyNotchOverlay + /// The sprite for a connection strength in the range `0...5`. + case playerConnectionStrength(PlayerInfo.ConnectionStrength) + /// The descriptor for the sprite. public var descriptor: GUISpriteDescriptor { switch self { @@ -120,6 +123,9 @@ public enum GUISprite { return GUISpriteDescriptor(slice: .bars, position: [0, 100], size: [182, 5]) case .bossBarTwentyNotchOverlay: return GUISpriteDescriptor(slice: .bars, position: [0, 110], size: [182, 5]) + case let .playerConnectionStrength(strength): + let y = 16 + (5 - strength.rawValue) * 8 + return GUISpriteDescriptor(slice: .icons, position: [0, y], size: [10, 7]) } } } diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 3d44e229..a6ee8c82 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -36,6 +36,8 @@ public class InGameGUI { (player.gamemode.gamemode, player.inventory) } + let inputState = game.accessInputState(acquireLock: false, action: identity) + if state.showHUD { return GUIElement.stack { if gamemode != .spectator { @@ -52,6 +54,11 @@ public class InGameGUI { } .constraints(.top(2), .center) + if state.movementAllowed && inputState.keys.contains(.tab) { + tabList(game.tabList) + .constraints(.top(8), .center) + } + if state.showDebugScreen { debugScreen(game: game, state: state) } @@ -95,6 +102,47 @@ public class InGameGUI { .size(GUISprite.xpBarBackground.descriptor.size.x, nil) } + public func tabList(_ tabList: TabList) -> GUIElement { + // TODO: Resolve chat component content when building ui instead of when resolving it + // (just too tricky to do stuff without knowing the chat component's content) + // TODO: Handle teams (changes sorting I think) + // TODO: Spectator players should go first, then sort by display name (with no-display-name players + // coming first?) then sort by player name + let sortedPlayers = tabList.players.values.sorted { left, right in + // TODO: Sort by the name that's gonna be displayed (displayName ?? name), + // requires the above TODO to be resolved first + left.name < right.name + } + + let borderColor = Vec4f(0, 0, 0, 0.8) + + // TODO: Render borders between rows (harder than it sounds lol, will require more advanced layout + // controls in GUIElement). + return GUIElement.list(direction: .horizontal, spacing: 0) { + GUIElement.forEach(in: sortedPlayers, spacing: 0) { player in + // TODO: Add text shadow when that's supported for chat components + // TODO: Render spectator mode player names italic + GUIElement.list(direction: .horizontal, spacing: 2) { + if let displayName = player.displayName { + GUIElement.message(displayName) + } else { + textWithShadow(player.name) + } + } + } + .padding(.bottom, -1) + .background(Vec4f(0, 0, 0, 0.5)) + + GUIElement.forEach(in: sortedPlayers, spacing: 1) { player in + GUIElement.sprite(.playerConnectionStrength(player.connectionStrength)) + .padding([.top, .right], 1) + .padding(.left, 2) + } + .background(Vec4f(0, 0, 0, 0.5)) + } + .border([.top, .left, .bottom], 1, borderColor) + } + public func chat(state: GUIStateStorage) -> GUIElement { // TODO: Implement scrollable chat history. diff --git a/Sources/Core/Sources/Player/PlayerInfo.swift b/Sources/Core/Sources/Player/PlayerInfo.swift index e4eec677..b33edf92 100644 --- a/Sources/Core/Sources/Player/PlayerInfo.swift +++ b/Sources/Core/Sources/Player/PlayerInfo.swift @@ -5,6 +5,38 @@ public struct PlayerInfo { public var name: String public var properties: [PlayerProperty] public var gamemode: Gamemode? + /// The player's ping measured in milliseconds public var ping: Int public var displayName: ChatComponent? + + /// The player's connection strength measured in bars (as displayed in the tab list). + public var connectionStrength: ConnectionStrength { + if ping < 0 { + return .noBars + } else if ping < 150 { + return .fiveBars + } else if ping < 300 { + return .fourBars + } else if ping < 600 { + return .threeBars + } else if ping < 1000 { + return .fourBars + } else { + return .fiveBars + } + } + + public enum ConnectionStrength: Int { + case noBars + case oneBar + case twoBars + case threeBars + case fourBars + case fiveBars + + /// The number of bars of connection in the range `0...5`. + var bars: Int { + rawValue + } + } } From 68fb39f6da2e07955e99f82990940459f920bdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?furby=E2=84=A2?= Date: Sun, 2 Jun 2024 00:25:48 +0000 Subject: [PATCH 51/84] Migrate to Swift Bundler v2 (#200) * migrate bundler toml to v2 * fix version/commit hash in short version string. --- Bundler.toml | 10 ++++++---- build.sh | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Bundler.toml b/Bundler.toml index 0f0f125f..0ce2795f 100644 --- a/Bundler.toml +++ b/Bundler.toml @@ -1,12 +1,14 @@ +format_version = 2 + [apps.DeltaClient] product = 'DeltaClient' version = 'v0.1.0-alpha.1' -bundle_identifier = 'dev.stackotter.delta-client' +identifier = 'dev.stackotter.delta-client' category = 'public.app-category.games' -minimum_macos_version = '11.0' icon = 'AppIcon.icns' -[apps.DeltaClient.extra_plist_entries] +[apps.DeltaClient.plist] # Append the current commit hash to the user-facing version string -CFBundleShortVersionString = "{VERSION}, commit: {COMMIT_HASH}" +CFBundleShortVersionString = "$(VERSION), commit: $(COMMIT_HASH)" GCSupportsControllerUserInteraction = "True" +MetalCaptureEnabled = true diff --git a/build.sh b/build.sh index bc82d858..01d18b7b 100644 --- a/build.sh +++ b/build.sh @@ -1,3 +1,2 @@ #!/bin/sh swift bundler bundle -c release -o . -plutil -insert MetalCaptureEnabled -bool YES DeltaClient.app/Contents/Info.plist From d127fa9a520379cf14bea41c780e26dc6210278c Mon Sep 17 00:00:00 2001 From: stackotter Date: Tue, 4 Jun 2024 11:51:22 +1000 Subject: [PATCH 52/84] Implement entity model downloading, loading and rendering (no textures yet) and fix attacking entities Attacks were always processed with no cooldown cause we sent a swing arm packet before attacking which was incorrect and caused the attack cooldown/strength to reset immediately before processing the attack --- .swift-format | 7 + Sources/Client/Modal.swift | 2 +- .../Client/Views/Startup/LoadAndThen.swift | 16 +- .../Core/Renderer/Entity/EntityRenderer.swift | 125 ++++------ Sources/Core/Renderer/GUI/GUIRenderer.swift | 62 +++-- .../Core/Renderer/Mesh/BlockMeshBuilder.swift | 39 +-- .../Core/Renderer/Mesh/ChunkSectionMesh.swift | 10 +- .../Mesh/ChunkSectionMeshBuilder.swift | 46 ++-- .../Renderer/Mesh/EntityMeshBuilder.swift | 88 +++++++ .../Core/Renderer/Mesh/FluidMeshBuilder.swift | 64 ++--- Sources/Core/Renderer/Mesh/Geometry.swift | 6 +- Sources/Core/Renderer/Mesh/Mesh.swift | 82 ++++--- Sources/Core/Renderer/Mesh/SortableMesh.swift | 7 +- .../Renderer/Mesh/SortableMeshElement.swift | 4 +- .../{Sources => Renderer}/RenderError.swift | 56 ++--- .../Core/Renderer/Shader/EntityShaders.metal | 10 +- Sources/Core/Renderer/Util/MetalUtil.swift | 34 ++- .../Core/Renderer/World/WorldRenderer.swift | 189 +++++++------- .../ECS/Systems/PlayerInputSystem.swift | 115 +++++---- Sources/Core/Sources/GUI/InGameGUI.swift | 140 ++++++----- Sources/Core/Sources/Game.swift | 37 +-- .../Clientbound/DestroyEntitiesPacket.swift | 9 +- .../Play/Clientbound/EntityStatusPacket.swift | 23 +- Sources/Core/Sources/Network/Socket.swift | 25 +- .../Model/Block/JSON/JSONBlockModel.swift | 6 +- .../Model/Entity/EntityModelPalette.swift | 92 +++++++ .../Model/Entity/JSON/JSONEntityModel.swift | 62 +++++ .../Core/Sources/Resources/ResourcePack.swift | 232 +++++------------- .../Core/Sources/Resources/Resources.swift | 212 +++++++++++++++- Sources/Core/Sources/Util/Either.swift | 27 ++ 30 files changed, 1160 insertions(+), 667 deletions(-) create mode 100644 .swift-format create mode 100644 Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift rename Sources/Core/{Sources => Renderer}/RenderError.swift (79%) create mode 100644 Sources/Core/Sources/Resources/Model/Entity/EntityModelPalette.swift create mode 100644 Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift create mode 100644 Sources/Core/Sources/Util/Either.swift diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..6191d8d8 --- /dev/null +++ b/.swift-format @@ -0,0 +1,7 @@ +{ + "version": 1, + "indentation": { + "spaces": 2 + }, + "indentSwitchCaseLabels": true +} diff --git a/Sources/Client/Modal.swift b/Sources/Client/Modal.swift index 70f031fc..05b5714d 100644 --- a/Sources/Client/Modal.swift +++ b/Sources/Client/Modal.swift @@ -1,5 +1,5 @@ -import SwiftUI import DeltaCore +import SwiftUI class Modal: ObservableObject { enum Content { diff --git a/Sources/Client/Views/Startup/LoadAndThen.swift b/Sources/Client/Views/Startup/LoadAndThen.swift index 366c0de5..3717d6e8 100644 --- a/Sources/Client/Views/Startup/LoadAndThen.swift +++ b/Sources/Client/Views/Startup/LoadAndThen.swift @@ -1,6 +1,6 @@ -import SwiftUI import Combine import DeltaCore +import SwiftUI struct LoadResult { var managedConfig: ManagedConfig @@ -108,7 +108,12 @@ struct LoadAndThen: View { modal.error("Error occurred during plugin loading, no plugins will be available: \(error)") } - try startup.perform(.downloadAssets, if: !FileSystem.directoryExists(storage.assetDirectory)) { progress in + // This check encompasses both missing entity models and missing assets. + let invalidateAssets = !FileSystem.directoryExists( + storage.assetDirectory.appendingPathComponent("minecraft/models/entity") + ) + try startup.perform(.downloadAssets, if: invalidateAssets) { progress in + try? FileManager.default.removeItem(at: storage.assetDirectory) try ResourcePack.downloadVanillaAssets( forVersion: Constants.versionString, to: storage.assetDirectory, @@ -124,8 +129,11 @@ struct LoadAndThen: View { // Load resource pack and cache it if necessary let resourcePack = try startup.perform(.loadResourcePacks) { let packCache = storage.cache(forResourcePackNamed: "vanilla") - let resourcePack = try ResourcePack.load(from: storage.assetDirectory, cacheDirectory: packCache) - if !FileSystem.directoryExists(packCache) { + let resourcePack = try ResourcePack.load( + from: storage.assetDirectory, + cacheDirectory: invalidateAssets ? nil : packCache + ) + if !FileSystem.directoryExists(packCache) || invalidateAssets { do { try resourcePack.cache(to: packCache) } catch { diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index 59c8e8b8..d3888bd5 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -1,8 +1,8 @@ -import Foundation +import DeltaCore import FirebladeECS import FirebladeMath +import Foundation import MetalKit -import DeltaCore /// Renders all entities in the world the client is currently connected to. public struct EntityRenderer: Renderer { @@ -90,19 +90,22 @@ public struct EntityRenderer: Renderer { } // Get all renderable entities - var entityUniforms: [EntityUniforms] = [] + var geometry = Geometry() client.game.accessNexus { nexus in // If the player is in first person view we don't render them profiler.push(.getEntities) - let entities: Family> + let entities: Family> if isFirstPerson { entities = nexus.family( requiresAll: EntityPosition.self, - EntityHitBox.self, + EntityKindId.self, excludesAll: ClientPlayerEntity.self ) } else { - entities = nexus.family(requiresAll: EntityPosition.self, EntityHitBox.self) + entities = nexus.family( + requiresAll: EntityPosition.self, + EntityKindId.self + ) } profiler.pop() @@ -111,87 +114,49 @@ public struct EntityRenderer: Renderer { // Create uniforms for each entity profiler.push(.createUniforms) - for (position, hitBox) in entities { - let aabb = hitBox.aabb(at: position.smoothVector) - let position = aabb.position - let size = aabb.size - + for (position, kindId) in entities { // Don't render entities that are outside of the render distance - let chunkPosition = EntityPosition(position).chunk + let chunkPosition = position.chunk if !chunkPosition.isWithinRenderDistance(renderDistance, of: cameraChunk) { continue } - let scale: Mat4x4f = MatrixUtil.scalingMatrix(Vec3f(size)) - let translation: Mat4x4f = MatrixUtil.translationMatrix(Vec3f(position)) - let uniforms = EntityUniforms(transformation: scale * translation) - entityUniforms.append(uniforms) + guard var kindIdentifier = kindId.entityKind?.identifier else { + log.warning("Unknown entity kind '\(kindId.id)'") + continue + } + + if kindIdentifier == Identifier(name: "ender_dragon") { + kindIdentifier = Identifier(name: "dragon") + } + + guard + let model = client.resourcePack.vanillaResources.entityModelPalette.models[kindIdentifier] + else { + // TODO: Re-enable missing model warning once item model rendering is implemented + log.warning("Missing model for entity kind with identifier '\(kindIdentifier)'") + continue + } + + let builder = EntityMeshBuilder(model: model, position: Vec3f(position.smoothVector)) + builder.build(into: &geometry) } profiler.pop() } - guard !entityUniforms.isEmpty else { + guard !geometry.isEmpty else { return } - // Create buffer for instance uniforms. If the current buffer is big enough, use it unless it is - // more than 64 entities too big. The maximum size limit is imposed so that the buffer isn't too - // much bigger than necessary. New buffers are always created with room for 32 more entities so - // that a new buffer isn't created each time an entity is added. - let minimumBufferSize = entityUniforms.count * MemoryLayout.stride - let maximumBufferSize = minimumBufferSize + 64 * MemoryLayout.stride - var instanceUniformsBuffer: MTLBuffer - - profiler.push(.getBuffer) - if let buffer = self.instanceUniformsBuffer, buffer.length >= minimumBufferSize, buffer.length <= maximumBufferSize { - buffer.contents().copyMemory(from: &entityUniforms, byteCount: minimumBufferSize) - instanceUniformsBuffer = buffer - } else { - log.trace("Creating new instance uniforms buffer") - instanceUniformsBuffer = try MetalUtil.makeBuffer( - device, - length: minimumBufferSize + MemoryLayout.stride * 32, - options: .storageModeShared, - label: "entityInstanceUniforms" - ) - instanceUniformsBuffer.contents().copyMemory( - from: &entityUniforms, - byteCount: minimumBufferSize - ) - } - profiler.pop() - - self.instanceUniformsBuffer = instanceUniformsBuffer - - // Render all the hitboxes using instancing - profiler.push(.encode) encoder.setRenderPipelineState(renderPipelineState) - encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) - encoder.setVertexBuffer(instanceUniformsBuffer, offset: 0, index: 2) - - encoder.drawIndexedPrimitives( - type: .triangle, - indexCount: indexCount, - indexType: .uint32, - indexBuffer: indexBuffer, - indexBufferOffset: 0, - instanceCount: entityUniforms.count - ) - profiler.pop() - // A hack to solve https://bugs.swift.org/browse/SR-15613 - // If this isn't done, `entityUniforms` gets freed somewhere around the line with `var - // instanceUniformsBuffer: MTLBuffer` in release builds - use(entityUniforms) + // TODO: Update profiler measurements + var mesh = Mesh(geometry, uniforms: ()) + try mesh.render(into: encoder, with: device, commandQueue: commandQueue) } - /// A hack used to solve https://bugs.swift.org/browse/SR-15613 - @inline(never) - @_optimize(none) - private func use(_ thing: Any) {} - /// Creates a coloured and shaded cube to be rendered using instancing as entities' hitboxes. - private static func createHitBoxGeometry(color: DeltaCore.RGBColor) -> (vertices: [EntityVertex], indices: [UInt32]) { + private static func createHitBoxGeometry(color: DeltaCore.RGBColor) -> Geometry { var vertices: [EntityVertex] = [] var indices: [UInt32] = [] @@ -199,14 +164,16 @@ public struct EntityRenderer: Renderer { let faceVertices = CubeGeometry.faceVertices[direction.rawValue] for position in faceVertices { let color = color.floatVector * CubeGeometry.shades[direction.rawValue] - vertices.append(EntityVertex( - x: position.x, - y: position.y, - z: position.z, - r: color.x, - g: color.y, - b: color.z - )) + vertices.append( + EntityVertex( + x: position.x, + y: position.y, + z: position.z, + r: color.x, + g: color.y, + b: color.z + ) + ) } let offset = UInt32(indices.count / 6 * 4) @@ -215,6 +182,6 @@ public struct EntityRenderer: Renderer { } } - return (vertices: vertices, indices: indices) + return Geometry(vertices: vertices, indices: indices) } } diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index dcac7789..f5b60d44 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -1,9 +1,9 @@ -import MetalKit -import FirebladeMath import DeltaCore +import FirebladeMath +import MetalKit #if canImport(UIKit) -import UIKit + import UIKit #endif /// The renderer for the GUI (chat, f3, scoreboard etc.). @@ -118,7 +118,10 @@ public final class GUIRenderer: Renderer { // Adjust scale per screen scale factor var uniforms = createUniforms(width, height, scalingFactor) if uniforms != previousUniforms || true { - uniformsBuffer.contents().copyMemory(from: &uniforms, byteCount: MemoryLayout.size) + uniformsBuffer.contents().copyMemory( + from: &uniforms, + byteCount: MemoryLayout.size + ) previousUniforms = uniforms } profiler.pop() @@ -180,8 +183,7 @@ public final class GUIRenderer: Renderer { color: color ) } catch let error as LocalizedError { - throw error - .with("Text", line) + throw error.with("Text", line) } } for i in meshes.indices where i != 0 { @@ -189,11 +191,13 @@ public final class GUIRenderer: Renderer { meshes[i].position.y += Font.defaultCharacterHeight + 1 } case let .sprite(descriptor): - meshes = try [GUIElementMesh( - sprite: descriptor, - guiTexturePalette: guiTexturePalette, - guiArrayTexture: guiArrayTexture - )] + meshes = try [ + GUIElementMesh( + sprite: descriptor, + guiTexturePalette: guiTexturePalette, + guiArrayTexture: guiArrayTexture + ) + ] case let .item(itemId): meshes = try self.meshes(forItemWithId: itemId) case nil, .interactable, .background: @@ -240,14 +244,15 @@ public final class GUIRenderer: Renderer { transformation = MatrixUtil.identity } - transformation *= MatrixUtil.translationMatrix([-0.5, -0.5, -0.5]) + transformation *= + MatrixUtil.translationMatrix([-0.5, -0.5, -0.5]) * MatrixUtil.rotationMatrix(x: .pi) * MatrixUtil.rotationMatrix(y: -.pi / 4) * MatrixUtil.rotationMatrix(x: -.pi / 6) * MatrixUtil.scalingMatrix(9.76) * MatrixUtil.translationMatrix([8, 8, 8]) - var geometry = Geometry() + var geometry = Geometry() var translucentGeometry = SortableMeshElement() BlockMeshBuilder( model: model, @@ -263,12 +268,14 @@ public final class GUIRenderer: Renderer { var vertices: [GUIVertex] = [] vertices.reserveCapacity(geometry.vertices.count) for vertex in geometry.vertices { - vertices.append(GUIVertex( - position: [vertex.x, vertex.y], - uv: [vertex.u, vertex.v], - tint: [vertex.r, vertex.g, vertex.b, 1], - textureIndex: vertex.textureIndex - )) + vertices.append( + GUIVertex( + position: [vertex.x, vertex.y], + uv: [vertex.u, vertex.v], + tint: [vertex.r, vertex.g, vertex.b, 1], + textureIndex: vertex.textureIndex + ) + ) } // TODO: Handle translucent block items @@ -334,7 +341,10 @@ public final class GUIRenderer: Renderer { } static func doIntersect(_ mesh: GUIElementMesh, _ other: GUIElementMesh) -> Bool { - doIntersect((position: mesh.position, size: mesh.size), (position: other.position, size: other.size)) + doIntersect( + (position: mesh.position, size: mesh.size), + (position: other.position, size: other.size) + ) } static func doIntersect( @@ -346,8 +356,8 @@ public final class GUIRenderer: Renderer { let pos2 = other.position let size2 = other.size - let overlapsX = abs((pos1.x + size1.x/2) - (pos2.x + size2.x/2)) * 2 < (size1.x + size2.x) - let overlapsY = abs((pos1.y + size1.y/2) - (pos2.y + size2.y/2)) * 2 < (size1.y + size2.y) + let overlapsX = abs((pos1.x + size1.x / 2) - (pos2.x + size2.x / 2)) * 2 < (size1.x + size2.x) + let overlapsY = abs((pos1.y + size1.y / 2) - (pos2.y + size2.y / 2)) * 2 < (size1.y + size2.y) return overlapsX && overlapsY } @@ -382,11 +392,11 @@ public final class GUIRenderer: Renderer { // Higher density displays have higher scaling factors to keep content a similar real world // size across screens. #if canImport(AppKit) - let screenScalingFactor = Float(NSApp.windows.first?.screen?.backingScaleFactor ?? 1) + let screenScalingFactor = Float(NSApp.windows.first?.screen?.backingScaleFactor ?? 1) #elseif canImport(UIKit) - let screenScalingFactor = Float(UIScreen.main.scale) + let screenScalingFactor = Float(UIScreen.main.scale) #else - #error("Unsupported platform, unknown screen scale factor") + #error("Unsupported platform, unknown screen scale factor") #endif return screenScalingFactor } @@ -395,7 +405,7 @@ public final class GUIRenderer: Renderer { let transformation = Mat3x3f([ [2 / width, 0, -1], [0, -2 / height, 1], - [0, 0, 1] + [0, 0, 1], ]) return GUIUniforms(screenSpaceToNormalized: transformation, scale: scale) } diff --git a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift index fed690d4..5d5bd89f 100644 --- a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift @@ -1,5 +1,5 @@ -import FirebladeMath import DeltaCore +import FirebladeMath /// Builds the mesh for a single block. struct BlockMeshBuilder { @@ -8,15 +8,15 @@ struct BlockMeshBuilder { let modelToWorld: Mat4x4f let culledFaces: DirectionSet let lightLevel: LightLevel - let neighbourLightLevels: [Direction: LightLevel] // TODO: Convert to array for faster access + let neighbourLightLevels: [Direction: LightLevel] // TODO: Convert to array for faster access let tintColor: Vec3f - let blockTexturePalette: TexturePalette // TODO: Remove when texture type is baked into block models + let blockTexturePalette: TexturePalette // TODO: Remove when texture type is baked into block models func build( - into geometry: inout Geometry, + into geometry: inout Geometry, translucentGeometry: inout SortableMeshElement ) { - var translucentGeometryParts: [(size: Float, geometry: Geometry)] = [] + var translucentGeometryParts: [(size: Float, geometry: Geometry)] = [] for part in model.parts { buildPart( part, @@ -33,11 +33,11 @@ struct BlockMeshBuilder { func buildPart( _ part: BlockModelPart, - into geometry: inout Geometry, - translucentGeometry: inout [(size: Float, geometry: Geometry)] + into geometry: inout Geometry, + translucentGeometry: inout [(size: Float, geometry: Geometry)] ) { for element in part.elements { - var elementTranslucentGeometry = Geometry() + var elementTranslucentGeometry = Geometry() buildElement( element, into: &geometry, @@ -57,8 +57,8 @@ struct BlockMeshBuilder { func buildElement( _ element: BlockModelElement, - into geometry: inout Geometry, - translucentGeometry: inout Geometry + into geometry: inout Geometry, + translucentGeometry: inout Geometry ) { let vertexToWorld = element.transformation * modelToWorld for face in element.faces { @@ -85,8 +85,8 @@ struct BlockMeshBuilder { func buildFace( _ face: BlockModelFace, transformedBy vertexToWorld: Mat4x4f, - into geometry: inout Geometry, - translucentGeometry: inout Geometry, + into geometry: inout Geometry, + translucentGeometry: inout Geometry, faceLightLevel: LightLevel, shouldShade: Bool ) { @@ -116,13 +116,13 @@ struct BlockMeshBuilder { func buildFace( _ face: BlockModelFace, transformedBy vertexToWorld: Mat4x4f, - into geometry: inout Geometry, + into geometry: inout Geometry, faceLightLevel: LightLevel, shouldShade: Bool, textureType: TextureType ) { // Add face winding - let offset = UInt32(geometry.vertices.count) // The index of the first vertex of this face + let offset = UInt32(geometry.vertices.count) // The index of the first vertex of this face for index in CubeGeometry.faceWinding { geometry.indices.append(index &+ offset) } @@ -172,10 +172,10 @@ struct BlockMeshBuilder { /// into a single element to add to the final mesh to reduce sorting calculations while /// rendering. private static func mergeTranslucentGeometry( - _ geometries: [(size: Float, geometry: Geometry)], + _ geometries: [(size: Float, geometry: Geometry)], position: BlockPosition ) -> SortableMeshElement { - var geometries = geometries // TODO: This may cause an unnecessary copy + var geometries = geometries // TODO: This may cause an unnecessary copy geometries.sort { first, second in return second.size > first.size } @@ -196,9 +196,10 @@ struct BlockMeshBuilder { for (_, geometry) in geometries { let startingIndex = UInt32(vertices.count) vertices.append(contentsOf: geometry.vertices) - indices.append(contentsOf: geometry.indices.map { index in - return index + startingIndex - }) + indices.append( + contentsOf: geometry.indices.map { index in + return index + startingIndex + }) } let geometry = Geometry(vertices: vertices, indices: indices) diff --git a/Sources/Core/Renderer/Mesh/ChunkSectionMesh.swift b/Sources/Core/Renderer/Mesh/ChunkSectionMesh.swift index 2b478367..dc082e56 100644 --- a/Sources/Core/Renderer/Mesh/ChunkSectionMesh.swift +++ b/Sources/Core/Renderer/Mesh/ChunkSectionMesh.swift @@ -1,11 +1,11 @@ +import FirebladeMath import Foundation import MetalKit -import FirebladeMath /// A renderable mesh of a chunk section. public struct ChunkSectionMesh { /// The mesh containing transparent and opaque blocks only. Doesn't need sorting. - public var transparentAndOpaqueMesh: Mesh + public var transparentAndOpaqueMesh: Mesh /// The mesh containing translucent blocks. Requires sorting when the player moves (clever stuff is done to minimise sorts in ``WorldRenderer``). public var translucentMesh: SortableMesh /// Whether the mesh contains fluids or not. @@ -17,8 +17,7 @@ public struct ChunkSectionMesh { /// Create a new chunk section mesh. public init(_ uniforms: ChunkUniforms) { - transparentAndOpaqueMesh = Mesh() - transparentAndOpaqueMesh.uniforms = uniforms + transparentAndOpaqueMesh = Mesh(uniforms: uniforms) translucentMesh = SortableMesh(uniforms: uniforms) } @@ -38,7 +37,8 @@ public struct ChunkSectionMesh { device: MTLDevice, commandQueue: MTLCommandQueue ) throws { - try transparentAndOpaqueMesh.render(into: renderEncoder, with: device, commandQueue: commandQueue) + try transparentAndOpaqueMesh.render( + into: renderEncoder, with: device, commandQueue: commandQueue) } /// Encode the render commands for translucent mesh of this chunk section. diff --git a/Sources/Core/Renderer/Mesh/ChunkSectionMeshBuilder.swift b/Sources/Core/Renderer/Mesh/ChunkSectionMeshBuilder.swift index 948997c1..c55695fe 100644 --- a/Sources/Core/Renderer/Mesh/ChunkSectionMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/ChunkSectionMeshBuilder.swift @@ -1,17 +1,18 @@ +import DeltaCore +import FirebladeMath import Foundation import MetalKit -import SwiftUI // TODO: why??? -import FirebladeMath -import DeltaCore /// Builds renderable meshes from chunk sections. /// /// Assumes that all relevant chunks have already been locked. -public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date +public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date /// A lookup to quickly convert block index to block position. private static let indexToPosition = generateIndexLookup() /// A lookup tp quickly get the block indices for blocks' neighbours. - private static let blockNeighboursLookup = (0..() for blockIndex in 0.., translucentMesh: inout SortableMesh, indexToNeighbours: [[BlockNeighbour]], containsFluids: inout Bool @@ -125,7 +126,9 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date // Get block guard let block = RegistryStore.shared.blockRegistry.block(withId: blockId) else { - log.warning("Skipping block with non-existent state id \(blockId), failed to get block information") + log.warning( + "Skipping block with non-existent state id \(blockId), failed to get block information" + ) return } @@ -157,7 +160,10 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date let culledFaces = getCullingNeighbours(at: position, blockId: blockId, neighbours: neighbours) // Return early if there can't possibly be any visible faces - if blockModel.cullableFaces == DirectionSet.all && culledFaces == DirectionSet.all && blockModel.nonCullableFaces.isEmpty { + if blockModel.cullableFaces == DirectionSet.all + && culledFaces == DirectionSet.all + && blockModel.nonCullableFaces.isEmpty + { return } @@ -185,7 +191,10 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date at: positionRelativeToChunkSection, inSectionAt: sectionPosition.sectionY ) - let neighbourLightLevels = getNeighbouringLightLevels(neighbours: neighbours, visibleFaces: visibleFaces) + let neighbourLightLevels = getNeighbouringLightLevels( + neighbours: neighbours, + visibleFaces: visibleFaces + ) // Get tint color guard let biome = chunk.biome(at: position.relativeToChunk, acquireLock: false) else { @@ -198,7 +207,9 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date // Create model to world transformation matrix let offset = block.getModelOffset(at: position) - let modelToWorld = MatrixUtil.translationMatrix(positionRelativeToChunkSection.floatVector + offset) + let modelToWorld = MatrixUtil.translationMatrix( + positionRelativeToChunkSection.floatVector + offset + ) // Add block model to mesh addBlockModel( @@ -216,7 +227,7 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date private func addBlockModel( _ model: BlockModel, - to transparentAndOpaqueGeometry: inout Geometry, + to transparentAndOpaqueGeometry: inout Geometry, translucentMesh: inout SortableMesh, position: BlockPosition, modelToWorld: Mat4x4f, @@ -280,7 +291,8 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date neighbouringBlocks[direction] = neighbourBlock } - let lightLevel = chunk + let lightLevel = + chunk .getLighting(acquireLock: false) .getLightLevel(atIndex: blockIndex, inSectionAt: sectionPosition.sectionY) let neighbouringLightLevels = getNeighbouringLightLevels( @@ -399,7 +411,9 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date for (direction, neighbourBlockId) in neighbouringBlocks where neighbourBlockId != 0 { // We assume that block model variants always have the same culling faces as eachother, so // no position is passed to getModel. - guard let blockModel = resources.blockModelPalette.model(for: neighbourBlockId, at: nil) else { + guard + let blockModel = resources.blockModelPalette.model(for: neighbourBlockId, at: nil) + else { log.debug("Skipping neighbour with no block models.") continue } @@ -408,7 +422,9 @@ public struct ChunkSectionMeshBuilder { // TODO: Bring docs up to date if blockModel.cullingFaces.contains(direction.opposite) || culledByOwnKind { cullingNeighbours.insert(direction) } else if let fluid = fluid { - guard let neighbourBlock = RegistryStore.shared.blockRegistry.block(withId: neighbourBlockId) else { + guard + let neighbourBlock = RegistryStore.shared.blockRegistry.block(withId: neighbourBlockId) + else { continue } diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift new file mode 100644 index 00000000..fb1c1dd3 --- /dev/null +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -0,0 +1,88 @@ +import DeltaCore + +public struct EntityMeshBuilder { + let model: JSONEntityModel + let position: Vec3f + + static let colors: [Vec3f] = [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1], + [1, 1, 0], + [1, 0, 1], + [0, 1, 1], + [0, 0, 0], + [1, 1, 1], + ] + + func build(into geometry: inout Geometry) { + for (index, submodel) in model.models.enumerated() { + buildSubmodel(submodel, index: index, into: &geometry) + } + } + + func buildSubmodel( + _ submodel: JSONEntityModel.Submodel, + index: Int, + into geometry: inout Geometry + ) { + for box in submodel.boxes ?? [] { + buildBox( + box, + color: index < Self.colors.count ? Self.colors[index] : [0.5, 0.5, 0.5], + into: &geometry + ) + } + + for (nestedIndex, nestedSubmodel) in (submodel.submodels ?? []).enumerated() { + buildSubmodel(nestedSubmodel, index: nestedIndex, into: &geometry) + } + } + + func buildBox( + _ box: JSONEntityModel.Box, + color: Vec3f, + into geometry: inout Geometry + ) { + var boxPosition = Vec3f( + box.coordinates[0], + box.coordinates[1], + box.coordinates[2] + ) + var boxSize = Vec3f( + box.coordinates[3], + box.coordinates[4], + box.coordinates[5] + ) + + if let additionalSize = box.sizeAdd { + let growth = Vec3f(repeating: additionalSize) + boxPosition -= growth + boxSize += 2 * growth + } + + boxPosition = boxPosition / 16 + position + boxSize /= 16 + for direction in Direction.allDirections { + // The index of the first vertex of this face + let offset = UInt32(geometry.vertices.count) + for index in CubeGeometry.faceWinding { + geometry.indices.append(index &+ offset) + } + + let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] + for vertexPosition in faceVertexPositions { + let position = vertexPosition * boxSize + boxPosition + let vertex = EntityVertex( + x: position.x, + y: position.y, + z: position.z, + r: color.x, + g: color.y, + b: color.z + ) + geometry.vertices.append(vertex) + } + } + } +} diff --git a/Sources/Core/Renderer/Mesh/FluidMeshBuilder.swift b/Sources/Core/Renderer/Mesh/FluidMeshBuilder.swift index d4332558..fcfe01c6 100644 --- a/Sources/Core/Renderer/Mesh/FluidMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/FluidMeshBuilder.swift @@ -1,14 +1,14 @@ -import FirebladeMath import DeltaCore +import FirebladeMath /// Builds the fluid mesh for a block. -struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in vanilla +struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in vanilla /// The UVs for the top of a fluid when flowing. static let flowingUVs: [Vec2f] = [ [0.75, 0.25], [0.25, 0.25], [0.25, 0.75], - [0.75, 0.75] + [0.75, 0.75], ] /// The UVs for the top of a fluid when still. @@ -16,7 +16,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v [1, 0], [0, 0], [0, 1], - [1, 1] + [1, 1], ] /// The component directions of the direction to each corner. The index of each is used as the @@ -25,7 +25,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v [.north, .east], [.north, .west], [.south, .west], - [.south, .east] + [.south, .east], ] /// Maps directions to the indices of the corners connected to that edge. @@ -33,11 +33,12 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v .north: [0, 1], .west: [1, 2], .south: [2, 3], - .east: [3, 0] + .east: [3, 0], ] let position: BlockPosition - let blockIndex: Int // Gets index as well as position to avoid duplicate calculation + // Take index as well as position to avoid duplicate calculation + let blockIndex: Int let block: Block let fluid: Fluid let chunk: Chunk @@ -59,10 +60,12 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v var tint = Vec3f(1, 1, 1) if block.fluidState?.fluid.identifier.name == "water" { - guard let tintColor = chunk.biome( - at: position.relativeToChunk, - acquireLock: false - )?.waterColor.floatVector else { + guard + let tintColor = chunk.biome( + at: position.relativeToChunk, + acquireLock: false + )?.waterColor.floatVector + else { // TODO: use a fallback color instead log.warning("Failed to get water tint") return @@ -71,7 +74,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v } let heights = calculateHeights() - let isFlowing = Set(heights).count > 1 // If the corners aren't all the same height, it's flowing + let isFlowing = Set(heights).count > 1 // If the corners aren't all the same height, it's flowing let topCornerPositions = calculatePositions(heights) // Get textures @@ -119,7 +122,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v let shade = CubeGeometry.shades[direction.rawValue] let tint = tint * shade - var geometry = Geometry() + var geometry = Geometry() switch direction { case .up: buildTopFace( @@ -153,15 +156,17 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v geometry.indices.append(contentsOf: CubeGeometry.faceWinding) - translucentMesh.add(SortableMeshElement( - geometry: geometry, - centerPosition: position.floatVector + Vec3f(0.5, 0.5, 0.5) - )) + translucentMesh.add( + SortableMeshElement( + geometry: geometry, + centerPosition: position.floatVector + Vec3f(0.5, 0.5, 0.5) + ) + ) } } private func buildTopFace( - into geometry: inout Geometry, + into geometry: inout Geometry, isFlowing: Bool, heights: [Float], topCornerPositions: [Vec3f], @@ -181,7 +186,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v // Rotate corner positions so that the lowest and the opposite from the lowest are on both triangles positions = [] for i in 0..<4 { - positions.append(topCornerPositions[(i + lowestCornerIndex) & 0x3]) // & 0x3 performs mod 4 + positions.append(topCornerPositions[(i + lowestCornerIndex) & 0x3]) // & 0x3 performs mod 4 } } else { positions = topCornerPositions @@ -194,7 +199,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v private func buildSideFace( _ direction: Direction, - into geometry: inout Geometry, + into geometry: inout Geometry, heights: [Float], topCornerPositions: [Vec3f], basePosition: Vec3f, @@ -212,14 +217,14 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v var positions = cornerIndices.map { topCornerPositions[$0] } let offsets = cornerIndices.map { Self.cornerToDirections[$0] }.reversed() for offset in offsets { - positions.append(basePosition + offset[0].vector/2 + offset[1].vector/2) + positions.append(basePosition + offset[0].vector / 2 + offset[1].vector / 2) } addVertices(to: &geometry, at: positions, uvs: uvs, texture: flowingTexture, tint: tint) } private func buildBottomFace( - into geometry: inout Geometry, + into geometry: inout Geometry, basePosition: Vec3f, topCornerPositions: [Vec3f], stillTexture: UInt16, @@ -229,7 +234,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v var positions: [Vec3f] = [] positions.reserveCapacity(4) for i in 0..<4 { - var position = topCornerPositions[(i - 1) & 0x3] // & 0x3 is mod 4 + var position = topCornerPositions[(i - 1) & 0x3] // & 0x3 is mod 4 position.y = basePosition.y positions.append(position) } @@ -276,7 +281,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v let positions: [BlockPosition] = [ position + xOffset, position + zOffset, - position + xOffset + zOffset + position + xOffset + zOffset, ] // Get the highest fluid level around the corner @@ -321,7 +326,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v private func countLowestCorners(_ heights: [Float]) -> (count: Int, index: Int) { var lowestCornerHeight: Float = 1 var lowestCornerIndex = 0 - var lowestCornersCount = 0 // The number of corners at the lowest height + var lowestCornersCount = 0 // The number of corners at the lowest height for (index, height) in heights.enumerated() { if height < lowestCornerHeight { lowestCornersCount = 1 @@ -341,7 +346,9 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v } else if lowestCornersCount == 3 { // If there are three lowest corners, take the last (when going anticlockwise) let nextCornerIndex = (lowestCornerIndex + 1) & 0x3 - if heights[previousCornerIndex] == lowestCornerHeight && heights[nextCornerIndex] == lowestCornerHeight { + if heights[previousCornerIndex] == lowestCornerHeight + && heights[nextCornerIndex] == lowestCornerHeight + { lowestCornerIndex = nextCornerIndex } else if heights[nextCornerIndex] == lowestCornerHeight { lowestCornerIndex = (lowestCornerIndex + 2) & 0x3 @@ -356,7 +363,8 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v // Rotate UVs 45 degrees if flowing diagonally if lowestCornersCount == 1 || lowestCornersCount == 3 { - let uvRotation = MatrixUtil.rotationMatrix2d(lowestCornersCount == 1 ? Float.pi / 4 : 3 * Float.pi / 4) + let uvRotation = MatrixUtil.rotationMatrix2d( + lowestCornersCount == 1 ? Float.pi / 4 : 3 * Float.pi / 4) let center = Vec2f(repeating: 0.5) for (index, uv) in uvs.enumerated() { uvs[index] = (uv - center) * uvRotation + center @@ -367,7 +375,7 @@ struct FluidMeshBuilder { // TODO: Make fluid meshes look more like they do in v } private func addVertices( - to geometry: inout Geometry, + to geometry: inout Geometry, at positions: Positions, uvs: [Vec2f], texture: UInt16, diff --git a/Sources/Core/Renderer/Mesh/Geometry.swift b/Sources/Core/Renderer/Mesh/Geometry.swift index 9a15ba0c..502700fb 100644 --- a/Sources/Core/Renderer/Mesh/Geometry.swift +++ b/Sources/Core/Renderer/Mesh/Geometry.swift @@ -1,9 +1,9 @@ import Foundation /// The simplest representation of renderable geometry data. Just vertices and vertex winding. -public struct Geometry { +public struct Geometry { /// Vertex data. - var vertices: [BlockVertex] = [] + var vertices: [Vertex] = [] /// Vertex windings. var indices: [UInt32] = [] @@ -11,7 +11,7 @@ public struct Geometry { return vertices.isEmpty || indices.isEmpty } - public init(vertices: [BlockVertex] = [], indices: [UInt32] = []) { + public init(vertices: [Vertex] = [], indices: [UInt32] = []) { self.vertices = vertices self.indices = indices } diff --git a/Sources/Core/Renderer/Mesh/Mesh.swift b/Sources/Core/Renderer/Mesh/Mesh.swift index 3e915677..90c63ce3 100644 --- a/Sources/Core/Renderer/Mesh/Mesh.swift +++ b/Sources/Core/Renderer/Mesh/Mesh.swift @@ -13,13 +13,13 @@ public enum MeshError: LocalizedError { } /// Holds and renders geometry data. -public struct Mesh { +public struct Mesh { /// The vertices in the mesh. - public var vertices: [BlockVertex] = [] + public var vertices: [Vertex] /// The vertex windings. - public var indices: [UInt32] = [] - /// The mesh's model to world transformation matrix. - public var uniforms = ChunkUniforms() + public var indices: [UInt32] + /// The mesh's uniforms. + public var uniforms: Uniforms /// A GPU buffer containing the vertices. public var vertexBuffer: MTLBuffer? @@ -40,19 +40,20 @@ public struct Mesh { return vertices.isEmpty || indices.isEmpty } - /// Create a new empty mesh. - public init() {} - /// Create a new populated mesh. - public init(vertices: [BlockVertex], indices: [UInt32], uniforms: ChunkUniforms) { + public init(vertices: [Vertex], indices: [UInt32], uniforms: Uniforms) { self.vertices = vertices self.indices = indices self.uniforms = uniforms } /// Create a new mesh with geometry. - public init(_ geometry: Geometry, uniforms: ChunkUniforms) { - self.init(vertices: geometry.vertices, indices: geometry.indices, uniforms: uniforms) + public init(_ geometry: Geometry? = nil, uniforms: Uniforms) { + self.init( + vertices: geometry?.vertices ?? [], + indices: geometry?.indices ?? [], + uniforms: uniforms + ) } /// Encodes the draw commands to render this mesh into a render encoder. Creates buffers if necessary. @@ -71,29 +72,38 @@ public struct Mesh { // Get buffers. If the buffer is valid and not nil, it is used. If the buffer is invalid and not nil, // it is repopulated with the new data (if big enough, otherwise a new buffer is created). If the // buffer is nil, a new one is created. - let vertexBuffer = try ((vertexBufferIsValid ? vertexBuffer : nil) ?? MetalUtil.createPrivateBuffer( - labelled: "vertexBuffer", - containing: vertices, - reusing: vertexBuffer, - device: device, - commandQueue: commandQueue - )) - - let indexBuffer = try ((indexBufferIsValid ? indexBuffer : nil) ?? MetalUtil.createPrivateBuffer( - labelled: "indexBuffer", - containing: indices, - reusing: indexBuffer, - device: device, - commandQueue: commandQueue - )) - - let uniformsBuffer = try ((uniformsBufferIsValid ? uniformsBuffer : nil) ?? MetalUtil.createPrivateBuffer( - labelled: "uniformsBuffer", - containing: [uniforms], - reusing: uniformsBuffer, - device: device, - commandQueue: commandQueue - )) + let vertexBuffer = + try + ((vertexBufferIsValid ? vertexBuffer : nil) + ?? MetalUtil.createPrivateBuffer( + labelled: "vertexBuffer", + containing: vertices, + reusing: vertexBuffer, + device: device, + commandQueue: commandQueue + )) + + let indexBuffer = + try + ((indexBufferIsValid ? indexBuffer : nil) + ?? MetalUtil.createPrivateBuffer( + labelled: "indexBuffer", + containing: indices, + reusing: indexBuffer, + device: device, + commandQueue: commandQueue + )) + + let uniformsBuffer = + try + ((uniformsBufferIsValid ? uniformsBuffer : nil) + ?? MetalUtil.createPrivateBuffer( + labelled: "uniformsBuffer", + containing: [uniforms], + reusing: uniformsBuffer, + device: device, + commandQueue: commandQueue + )) // Update cached buffers. Unnecessary assignments won't affect performance because `MTLBuffer`s // are just descriptors, not the actual data @@ -127,7 +137,9 @@ public struct Mesh { /// - keepVertexBuffer: If `true`, the vertex buffer is not invalidated. /// - keepIndexBuffer: If `true`, the index buffer is not invalidated. /// - keepUniformsBuffer: If `true`, the uniforms buffer is not invalidated. - public mutating func invalidateBuffers(keepVertexBuffer: Bool = false, keepIndexBuffer: Bool = false, keepUniformsBuffer: Bool = false) { + public mutating func invalidateBuffers( + keepVertexBuffer: Bool = false, keepIndexBuffer: Bool = false, keepUniformsBuffer: Bool = false + ) { vertexBufferIsValid = keepVertexBuffer indexBufferIsValid = keepIndexBuffer uniformsBufferIsValid = keepUniformsBuffer diff --git a/Sources/Core/Renderer/Mesh/SortableMesh.swift b/Sources/Core/Renderer/Mesh/SortableMesh.swift index 50ef153b..2b2a1b28 100644 --- a/Sources/Core/Renderer/Mesh/SortableMesh.swift +++ b/Sources/Core/Renderer/Mesh/SortableMesh.swift @@ -1,6 +1,6 @@ +import FirebladeMath import Foundation import MetalKit -import FirebladeMath /// A mesh that can be sorted after the initial preparation. /// @@ -15,7 +15,7 @@ public struct SortableMesh { } /// The mesh that is updated each time this mesh is sorted. - public var underlyingMesh: Mesh + public var underlyingMesh: Mesh /// Creates a new sortable mesh. /// - Parameters: @@ -23,8 +23,7 @@ public struct SortableMesh { /// - uniforms: The mesh's uniforms. public init(_ elements: [SortableMeshElement] = [], uniforms: ChunkUniforms) { self.elements = elements - underlyingMesh = Mesh() - underlyingMesh.uniforms = uniforms + underlyingMesh = Mesh(uniforms: uniforms) } /// Removes all elements from the mesh. diff --git a/Sources/Core/Renderer/Mesh/SortableMeshElement.swift b/Sources/Core/Renderer/Mesh/SortableMeshElement.swift index 3642f8f6..7753908e 100644 --- a/Sources/Core/Renderer/Mesh/SortableMeshElement.swift +++ b/Sources/Core/Renderer/Mesh/SortableMeshElement.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation /// An element of a ``SortableMesh``. public struct SortableMeshElement { @@ -34,7 +34,7 @@ public struct SortableMeshElement { /// - Parameters: /// - geometry: The element's geometry /// - centerPosition: The position of the center of the element. - public init(id: Int = 0, geometry: Geometry, centerPosition: Vec3f) { + public init(id: Int = 0, geometry: Geometry, centerPosition: Vec3f) { self.init( id: id, vertices: geometry.vertices, diff --git a/Sources/Core/Sources/RenderError.swift b/Sources/Core/Renderer/RenderError.swift similarity index 79% rename from Sources/Core/Sources/RenderError.swift rename to Sources/Core/Renderer/RenderError.swift index 54fde0bf..8fd2d8fd 100644 --- a/Sources/Core/Sources/RenderError.swift +++ b/Sources/Core/Renderer/RenderError.swift @@ -1,7 +1,6 @@ +import DeltaCore import Foundation -// TODO: Move to Renderer target once possible - public enum RenderError: LocalizedError { /// Failed to create a metal array texture. case failedToCreateArrayTexture @@ -21,8 +20,8 @@ public enum RenderError: LocalizedError { case failedToCreateWorldRenderPipelineState(Error) /// Failed to create the depth stencil state for the world renderer. case failedToCreateWorldDepthStencilState - /// Failed to create the render pipeline state for the entity renderer. - case failedToCreateEntityRenderPipelineState(Error, label: String) + /// Failed to create the render pipeline state for a renderer. + case failedToCreateRenderPipelineState(Error, label: String) /// Failed to create the depth stencil state for the entity renderer. case failedToCreateEntityDepthStencilState /// Failed to create the block texture array. @@ -70,33 +69,33 @@ public enum RenderError: LocalizedError { return "Failed to find default.metallib in the bundle." case .failedToCreateMetallib(let error): return """ - Failed to create a metal library from `default.metallib`. - Reason: \(error.localizedDescription) - """ + Failed to create a metal library from `default.metallib`. + Reason: \(error.localizedDescription) + """ case .failedToLoadShaders: return "Failed to load the shaders from the metallib." case .failedtoCreateWorldUniformBuffers: return "Failed to create the buffers that hold the world uniforms." case .failedToCreateWorldRenderPipelineState(let error): return """ - Failed to create the render pipeline state for the world renderer. - Reason: \(error.localizedDescription) - """ + Failed to create the render pipeline state for the world renderer. + Reason: \(error.localizedDescription) + """ case .failedToCreateWorldDepthStencilState: return "Failed to create the depth stencil state for the entity renderer." - case .failedToCreateEntityRenderPipelineState(let error, let label): + case .failedToCreateRenderPipelineState(let error, let label): return """ - Failed to create the render pipeline state for the entity renderer. - Reason: \(error.localizedDescription) - Label: \(label) - """ + Failed to create render pipeline state. + Reason: \(error.localizedDescription) + Label: \(label) + """ case .failedToCreateEntityDepthStencilState: return " Failed to create the depth stencil state for the entity renderer." case .failedToCreateBlockTextureArray(let error): return """ - Failed to create the block texture array. - Reason: \(error.localizedDescription) - """ + Failed to create the block texture array. + Reason: \(error.localizedDescription) + """ case .failedToCreateRenderEncoder: return "Failed to create the render encoder." case .failedToCreateCommandBuffer: @@ -108,26 +107,27 @@ public enum RenderError: LocalizedError { case .failedToGetCurrentRenderPassDescriptor: return "Failed to get the current render pass descriptor for a frame." case .failedToCreateTimerEvent: - return "Failed to get the specified counter set (it is likely not supported by the selected device)." + return + "Failed to get the specified counter set (it is likely not supported by the selected device)." case .failedToGetCounterSet(let rawValue): return """ - Failed to get the specified counter set (it is likely not supported by the selected device). - Raw value: \(rawValue) - """ + Failed to get the specified counter set (it is likely not supported by the selected device). + Raw value: \(rawValue) + """ case .failedToMakeCounterSampleBuffer(let error): return """ - Failed to create the buffer used for sampling GPU counters. - Reason: \(error.localizedDescription) - """ + Failed to create the buffer used for sampling GPU counters. + Reason: \(error.localizedDescription) + """ case .failedToSampleCounters: return "Failed to sample the GPU counters used to calculate FPS." case .gpuTraceNotSupported: return "The current device does not support capturing a gpu trace and outputting to a file." case .failedToStartCapture(let error): return """ - Failed to start GPU frame capture. - Reason: \(error.localizedDescription) - """ + Failed to start GPU frame capture. + Reason: \(error.localizedDescription) + """ case .failedToCreateBlitCommandEncoder: return "Failed to create blit command encoder." case .missingTexture(let identifier): diff --git a/Sources/Core/Renderer/Shader/EntityShaders.metal b/Sources/Core/Renderer/Shader/EntityShaders.metal index 4aca979f..7c1250be 100644 --- a/Sources/Core/Renderer/Shader/EntityShaders.metal +++ b/Sources/Core/Renderer/Shader/EntityShaders.metal @@ -17,19 +17,13 @@ struct EntityRasterizerData { float4 color; }; -struct EntityUniforms { - float4x4 transformation; -}; - vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [[buffer(0)]], constant CameraUniforms &cameraUniforms [[buffer(1)]], - constant EntityUniforms *instanceUniforms [[buffer(2)]], - uint vertexId [[vertex_id]], - uint instanceId [[instance_id]]) { + uint vertexId [[vertex_id]]) { EntityVertex in = vertices[vertexId]; EntityRasterizerData out; - out.position = float4(in.x, in.y, in.z, 1.0) * instanceUniforms[instanceId].transformation * cameraUniforms.framing * cameraUniforms.projection; + out.position = float4(in.x, in.y, in.z, 1.0) * cameraUniforms.framing * cameraUniforms.projection; out.color = float4(in.r, in.g, in.b, 1.0); return out; diff --git a/Sources/Core/Renderer/Util/MetalUtil.swift b/Sources/Core/Renderer/Util/MetalUtil.swift index 86f50227..335650c4 100644 --- a/Sources/Core/Renderer/Util/MetalUtil.swift +++ b/Sources/Core/Renderer/Util/MetalUtil.swift @@ -1,5 +1,4 @@ import Metal -import DeltaCore // TODO: remove this import once RenderError is in the Renderer target public enum MetalUtil { /// Makes a render pipeline state with the given properties. @@ -40,8 +39,7 @@ public enum MetalUtil { do { return try device.makeRenderPipelineState(descriptor: descriptor) } catch { - // TODO: Update error name - throw RenderError.failedToCreateEntityRenderPipelineState(error, label: label) + throw RenderError.failedToCreateRenderPipelineState(error, label: label) } } @@ -50,11 +48,11 @@ public enum MetalUtil { /// The default library is at `DeltaClient.app/Contents/Resources/DeltaCore_DeltaCore.bundle/Resources/default.metallib`. public static func loadDefaultLibrary(_ device: MTLDevice) throws -> MTLLibrary { #if os(macOS) - let bundlePath = "Contents/Resources/DeltaCore_DeltaRenderer.bundle" + let bundlePath = "Contents/Resources/DeltaCore_DeltaRenderer.bundle" #elseif os(iOS) || os(tvOS) - let bundlePath = "DeltaCore_DeltaRenderer.bundle" + let bundlePath = "DeltaCore_DeltaRenderer.bundle" #else - #error("Unsupported platform, unknown DeltaCore bundle location") + #error("Unsupported platform, unknown DeltaCore bundle location") #endif guard let bundle = Bundle(url: Bundle.main.bundleURL.appendingPathComponent(bundlePath)) else { @@ -170,7 +168,10 @@ public enum MetalUtil { /// - device: Device to create the state with. /// - readOnly: If `true`, the depth texture will not be written to. /// - Returns: A depth stencil state. - public static func createDepthState(device: MTLDevice, readOnly: Bool = false) throws -> MTLDepthStencilState { + public static func createDepthState( + device: MTLDevice, + readOnly: Bool = false + ) throws -> MTLDepthStencilState { let depthDescriptor = MTLDepthStencilDescriptor() depthDescriptor.depthCompareFunction = .lessEqual depthDescriptor.isDepthWriteEnabled = !readOnly @@ -235,22 +236,31 @@ public enum MetalUtil { device: MTLDevice, commandQueue: MTLCommandQueue ) throws -> MTLBuffer { - precondition(existingBuffer?.storageMode == .private || existingBuffer == nil, "existingBuffer must have a storageMode of private") + precondition( + existingBuffer?.storageMode == .private || existingBuffer == nil, + "existingBuffer must have a storageMode of private" + ) // First copy the array to a scratch buffer (accessible from both CPU and GPU) let bufferSize = MemoryLayout.stride * items.count - guard let sharedBuffer = device.makeBuffer(bytes: items, length: bufferSize, options: [.storageModeShared]) else { + guard + let sharedBuffer = device.makeBuffer( + bytes: items, + length: bufferSize, + options: [.storageModeShared] + ) + else { throw RenderError.failedToCreateBuffer(label: label) } // Create a private buffer (only accessible from GPU) or reuse the existing buffer if possible let privateBuffer: MTLBuffer if let existingBuffer = existingBuffer, existingBuffer.length >= bufferSize { -// log.trace("Reusing existing metal \(label)") privateBuffer = existingBuffer } else { -// log.trace("Creating new metal \(label)") - guard let buffer = device.makeBuffer(length: bufferSize, options: [.storageModePrivate]) else { + guard + let buffer = device.makeBuffer(length: bufferSize, options: [.storageModePrivate]) + else { throw RenderError.failedToCreateBuffer(label: label) } privateBuffer = buffer diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 7ae0ee07..63bc0954 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -1,7 +1,7 @@ +import DeltaCore +import FirebladeMath import Foundation import MetalKit -import FirebladeMath -import DeltaCore /// A renderer that renders a `World` along with its associated entities (from `Game.nexus`). public final class WorldRenderer: Renderer { @@ -13,10 +13,10 @@ public final class WorldRenderer: Renderer { /// Render pipeline used for rendering world geometry. private var renderPipelineState: MTLRenderPipelineState #if !os(tvOS) - /// Render pipeline used for rendering translucent world geometry. - private var transparencyRenderPipelineState: MTLRenderPipelineState - /// Render pipeline used for compositing translucent geometry onto the screen buffer. - private var compositingRenderPipelineState: MTLRenderPipelineState + /// Render pipeline used for rendering translucent world geometry. + private var transparencyRenderPipelineState: MTLRenderPipelineState + /// Render pipeline used for compositing translucent geometry onto the screen buffer. + private var compositingRenderPipelineState: MTLRenderPipelineState #endif /// The device used for rendering. @@ -51,9 +51,9 @@ public final class WorldRenderer: Renderer { private var lightMapBuffer: MTLBuffer? #if !os(tvOS) - /// The depth stencil state used for order independent transparency (which requires read-only - /// depth). - private let readOnlyDepthState: MTLDepthStencilState + /// The depth stencil state used for order independent transparency (which requires read-only + /// depth). + private let readOnlyDepthState: MTLDepthStencilState #endif /// The depth stencil state used when order independent transparency is disabled. private let depthState: MTLDepthStencilState @@ -81,7 +81,8 @@ public final class WorldRenderer: Renderer { let library = try MetalUtil.loadDefaultLibrary(device) let vertexFunction = try MetalUtil.loadFunction("chunkVertexShader", from: library) let fragmentFunction = try MetalUtil.loadFunction("chunkFragmentShader", from: library) - let transparentFragmentFunction = try MetalUtil.loadFunction("chunkOITFragmentShader", from: library) + let transparentFragmentFunction = try MetalUtil.loadFunction( + "chunkOITFragmentShader", from: library) let transparentCompositingVertexFunction = try MetalUtil.loadFunction( "chunkOITCompositingVertexShader", from: library @@ -127,45 +128,45 @@ public final class WorldRenderer: Renderer { ) #if !os(tvOS) - // Create OIT pipeline - transparencyRenderPipelineState = try MetalUtil.makeRenderPipelineState( - device: device, - label: "WorldRenderer.oit", - vertexFunction: vertexFunction, - fragmentFunction: transparentFragmentFunction, - blendingEnabled: true, - editDescriptor: { (descriptor: MTLRenderPipelineDescriptor) in - // Accumulation texture - descriptor.colorAttachments[1].isBlendingEnabled = true - descriptor.colorAttachments[1].rgbBlendOperation = .add - descriptor.colorAttachments[1].alphaBlendOperation = .add - descriptor.colorAttachments[1].sourceRGBBlendFactor = .one - descriptor.colorAttachments[1].sourceAlphaBlendFactor = .one - descriptor.colorAttachments[1].destinationRGBBlendFactor = .one - descriptor.colorAttachments[1].destinationAlphaBlendFactor = .one - - // Revealage texture - descriptor.colorAttachments[2].isBlendingEnabled = true - descriptor.colorAttachments[2].rgbBlendOperation = .add - descriptor.colorAttachments[2].alphaBlendOperation = .add - descriptor.colorAttachments[2].sourceRGBBlendFactor = .zero - descriptor.colorAttachments[2].sourceAlphaBlendFactor = .zero - descriptor.colorAttachments[2].destinationRGBBlendFactor = .oneMinusSourceColor - descriptor.colorAttachments[2].destinationAlphaBlendFactor = .oneMinusSourceAlpha - } - ) + // Create OIT pipeline + transparencyRenderPipelineState = try MetalUtil.makeRenderPipelineState( + device: device, + label: "WorldRenderer.oit", + vertexFunction: vertexFunction, + fragmentFunction: transparentFragmentFunction, + blendingEnabled: true, + editDescriptor: { (descriptor: MTLRenderPipelineDescriptor) in + // Accumulation texture + descriptor.colorAttachments[1].isBlendingEnabled = true + descriptor.colorAttachments[1].rgbBlendOperation = .add + descriptor.colorAttachments[1].alphaBlendOperation = .add + descriptor.colorAttachments[1].sourceRGBBlendFactor = .one + descriptor.colorAttachments[1].sourceAlphaBlendFactor = .one + descriptor.colorAttachments[1].destinationRGBBlendFactor = .one + descriptor.colorAttachments[1].destinationAlphaBlendFactor = .one + + // Revealage texture + descriptor.colorAttachments[2].isBlendingEnabled = true + descriptor.colorAttachments[2].rgbBlendOperation = .add + descriptor.colorAttachments[2].alphaBlendOperation = .add + descriptor.colorAttachments[2].sourceRGBBlendFactor = .zero + descriptor.colorAttachments[2].sourceAlphaBlendFactor = .zero + descriptor.colorAttachments[2].destinationRGBBlendFactor = .oneMinusSourceColor + descriptor.colorAttachments[2].destinationAlphaBlendFactor = .oneMinusSourceAlpha + } + ) - // Create OIT compositing pipeline - compositingRenderPipelineState = try MetalUtil.makeRenderPipelineState( - device: device, - label: "WorldRenderer.compositing", - vertexFunction: transparentCompositingVertexFunction, - fragmentFunction: transparentCompositingFragmentFunction, - blendingEnabled: true - ) + // Create OIT compositing pipeline + compositingRenderPipelineState = try MetalUtil.makeRenderPipelineState( + device: device, + label: "WorldRenderer.compositing", + vertexFunction: transparentCompositingVertexFunction, + fragmentFunction: transparentCompositingFragmentFunction, + blendingEnabled: true + ) - // Create the depth state used for order independent transparency - readOnlyDepthState = try MetalUtil.createDepthState(device: device, readOnly: true) + // Create the depth state used for order independent transparency + readOnlyDepthState = try MetalUtil.createDepthState(device: device, readOnly: true) #endif // Create the regular depth state. @@ -200,9 +201,10 @@ public final class WorldRenderer: Renderer { options: storageMode ) - let maxOutlinePartCount = RegistryStore.shared.blockRegistry.blocks.map { block in - return block.shape.outlineShape.aabbs.count - }.max() ?? 1 + let maxOutlinePartCount = + RegistryStore.shared.blockRegistry.blocks.map { block in + return block.shape.outlineShape.aabbs.count + }.max() ?? 1 let geometry = Self.generateOutlineGeometry(position: .zero, size: [1, 1, 1], baseIndex: 0) @@ -348,7 +350,8 @@ public final class WorldRenderer: Renderer { } let block = client.game.world.getBlock(at: breakingBlock.position) if var model = resources.blockModelPalette.model(for: block.id, at: breakingBlock.position) { - let textureId = client.resourcePack.vanillaResources.blockTexturePalette.textureIndex(for: Identifier(namespace: "minecraft", name: "block/destroy_stage_\(stage)"))! + let textureId = client.resourcePack.vanillaResources.blockTexturePalette.textureIndex( + for: Identifier(namespace: "minecraft", name: "block/destroy_stage_\(stage)"))! for (i, part) in model.parts.enumerated() { for (j, element) in part.elements.enumerated() { model.parts[i].elements[j].shade = false @@ -382,14 +385,19 @@ public final class WorldRenderer: Renderer { tintColor: Vec3f(repeating: 0), blockTexturePalette: resources.blockTexturePalette ) - var dummyGeometry = Geometry() + var dummyGeometry = Geometry() var geometry = SortableMeshElement() builder.build(into: &dummyGeometry, translucentGeometry: &geometry) for i in 0...stride * geometry.vertices.count) - guard let indexBuffer = device.makeBuffer(bytes: &geometry.indices, length: MemoryLayout.stride * geometry.indices.count) else { + let vertexBuffer = device.makeBuffer( + bytes: &geometry.vertices, + length: MemoryLayout.stride * geometry.vertices.count) + guard + let indexBuffer = device.makeBuffer( + bytes: &geometry.indices, length: MemoryLayout.stride * geometry.indices.count) + else { // No geometry to render continue } @@ -449,9 +457,9 @@ public final class WorldRenderer: Renderer { ) } - // Composite translucent geometry onto the screen buffer. No vertices need to be supplied, the - // shader has the screen's corners hardcoded for simplicity. #if !os(tvOS) + // Composite translucent geometry onto the screen buffer. No vertices need to be supplied, the + // shader has the screen's corners hardcoded for simplicity. if client.configuration.render.enableOrderIndependentTransparency { encoder.setRenderPipelineState(compositingRenderPipelineState) encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6) @@ -483,13 +491,15 @@ public final class WorldRenderer: Renderer { worldMesh.updateSections(at: Array(affectedSections)) case let event as World.Event.SingleBlockUpdate: - let affectedSections = worldMesh.sectionsAffectedBySectionUpdate(at: event.position.chunkSection) + let affectedSections = worldMesh.sectionsAffectedBySectionUpdate( + at: event.position.chunkSection) worldMesh.updateSections(at: Array(affectedSections)) case let event as World.Event.MultiBlockUpdate: var affectedSections: Set = [] for update in event.updates { - affectedSections.formUnion(worldMesh.sectionsAffectedBySectionUpdate(at: update.position.chunkSection)) + affectedSections.formUnion( + worldMesh.sectionsAffectedBySectionUpdate(at: update.position.chunkSection)) } worldMesh.updateSections(at: Array(affectedSections)) @@ -552,7 +562,7 @@ public final class WorldRenderer: Renderer { position: Vec3f, size: Vec3f, baseIndex: UInt32 - ) -> Geometry { + ) -> Geometry { let thickness: Float = 0.004 let padding: Float = -thickness + 0.001 @@ -566,12 +576,13 @@ public final class WorldRenderer: Renderer { position *= size / 2 + Vec3f(padding + thickness / 2, 0, padding + thickness / 2) position += Vec3f(size.x - thickness, 0, size.z - thickness) / 2 position.y -= padding - boxes.append(( - position: position, - size: [thickness, size.component(along: .y) + padding * 2, thickness], - axis: .y, - faces: [side, adjacentSide] - )) + boxes.append( + ( + position: position, + size: [thickness, size.component(along: .y) + padding * 2, thickness], + axis: .y, + faces: [side, adjacentSide] + )) // Create the edges above and below this side for direction: Direction in [.up, .down] { @@ -601,12 +612,13 @@ public final class WorldRenderer: Renderer { if adjacentSide.axis != .x { faces.append(contentsOf: [adjacentSide, adjacentSide.opposite]) } - boxes.append(( - position: position, - size: (Vec3f(1, 1, 1) - edgeDirection) * thickness + edge, - axis: adjacentSide.axis, - faces: faces - )) + boxes.append( + ( + position: position, + size: (Vec3f(1, 1, 1) - edgeDirection) * thickness + edge, + axis: adjacentSide.axis, + faces: faces + )) } } @@ -625,24 +637,27 @@ public final class WorldRenderer: Renderer { blockOutlineIndices.append(contentsOf: winding) blockOutlineIndices.append(contentsOf: winding.reversed()) - let transformation = MatrixUtil.scalingMatrix(box.size) * MatrixUtil.translationMatrix(box.position) * translation + let transformation = + MatrixUtil.scalingMatrix(box.size) * MatrixUtil.translationMatrix(box.position) + * translation for vertex in CubeGeometry.faceVertices[face.rawValue] { let vertexPosition = (Vec4f(vertex, 1) * transformation).xyz - blockOutlineVertices.append(BlockVertex( - x: vertexPosition.x, - y: vertexPosition.y, - z: vertexPosition.z, - u: 0, - v: 0, - r: 0, - g: 0, - b: 0, - a: 0.6, - skyLightLevel: UInt8(LightLevel.maximumLightLevel), - blockLightLevel: 0, - textureIndex: UInt16.max, - isTransparent: false - )) + blockOutlineVertices.append( + BlockVertex( + x: vertexPosition.x, + y: vertexPosition.y, + z: vertexPosition.z, + u: 0, + v: 0, + r: 0, + g: 0, + b: 0, + a: 0.6, + skyLightLevel: UInt8(LightLevel.maximumLightLevel), + blockLightLevel: 0, + textureIndex: UInt16.max, + isTransparent: false + )) } } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 81387e21..5230eccb 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -68,7 +68,12 @@ public final class PlayerInputSystem: System { // within a single tick should be uncommon anyway). // Be careful not to acquire a nexus lock here (passing the guiState parameter ensures this) - let gui = game.compileGUI(withFont: font, locale: locale, connection: connection, guiState: guiState) + let gui = game.compileGUI( + withFont: font, + locale: locale, + connection: connection, + guiState: guiState + ) suppressInput = gui.handleInteraction(.press(event), at: mousePosition) } @@ -121,7 +126,11 @@ public final class PlayerInputSystem: System { inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 8) % 9 case .dropItem: let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot - inventory.window.dropItemFromSlot(slotIndex, mouseItemStack: nil, connection: connection) + inventory.window.dropItemFromSlot( + slotIndex, + mouseItemStack: nil, + connection: connection + ) case .place: // Block breaking is handled by ``PlayerBlockBreakingSystem``, this just handles hand animation and // other non breaking things for the `.destroy` input (e.g. attacking) @@ -136,15 +145,17 @@ public final class PlayerInputSystem: System { switch targetedThing.target { case let .block(blockPosition): let cursor = targetedThing.cursor - try connection?.sendPacket(PlayerBlockPlacementPacket( - hand: .mainHand, - location: blockPosition, - face: targetedThing.face, - cursorPositionX: cursor.x, - cursorPositionY: cursor.y, - cursorPositionZ: cursor.z, - insideBlock: targetedThing.distance < 0 - )) + try connection?.sendPacket( + PlayerBlockPlacementPacket( + hand: .mainHand, + location: blockPosition, + face: targetedThing.face, + cursorPositionX: cursor.x, + cursorPositionY: cursor.y, + cursorPositionZ: cursor.z, + insideBlock: targetedThing.distance < 0 + ) + ) if gamemode.gamemode.canPlaceBlocks { // TODO: Predict the result of block placement so that we're not relying on the server @@ -152,32 +163,37 @@ public final class PlayerInputSystem: System { } case let .entity(entityId): let targetedPosition = targetedThing.targetedPosition - try connection?.sendPacket(InteractEntityPacket( - entityId: Int32(entityId), - interaction: .interactAt( - targetX: targetedPosition.x, - targetY: targetedPosition.y, - targetZ: targetedPosition.z, - hand: .mainHand, - isSneaking: sneaking.isSneaking + try connection?.sendPacket( + InteractEntityPacket( + entityId: Int32(entityId), + interaction: .interactAt( + targetX: targetedPosition.x, + targetY: targetedPosition.y, + targetZ: targetedPosition.z, + hand: .mainHand, + isSneaking: sneaking.isSneaking + ) ) - )) - try connection?.sendPacket(InteractEntityPacket( - entityId: Int32(entityId), - interaction: .interact(hand: .mainHand, isSneaking: sneaking.isSneaking) - )) + ) + try connection?.sendPacket( + InteractEntityPacket( + entityId: Int32(entityId), + interaction: .interact(hand: .mainHand, isSneaking: sneaking.isSneaking) + ) + ) } case .destroy: - try connection?.sendPacket(AnimationServerboundPacket(hand: .mainHand)) - guard let targetedEntity = game.targetedEntity(acquireLock: false) else { + try connection?.sendPacket(AnimationServerboundPacket(hand: .mainHand)) break } - try connection?.sendPacket(InteractEntityPacket( - entityId: Int32(targetedEntity.target), - interaction: .attack(isSneaking: sneaking.isSneaking) - )) + try connection?.sendPacket( + InteractEntityPacket( + entityId: Int32(targetedEntity.target), + interaction: .attack(isSneaking: sneaking.isSneaking) + ) + ) default: break } @@ -249,24 +265,28 @@ public final class PlayerInputSystem: System { guiState.messageInput = guiState.stashedMessageInput ?? "" } } - } else if event.key == .leftArrow && guiState.messageInput?.count ?? 0 > guiState.messageInputCursor { + } else if event.key == .leftArrow + && guiState.messageInput?.count ?? 0 > guiState.messageInputCursor + { guiState.messageInputCursor += 1 } else if event.key == .rightArrow && guiState.messageInputCursor > 0 { guiState.messageInputCursor -= 1 } else { #if os(macOS) - if event.key == .v && !inputState.keys.intersection([.leftCommand, .rightCommand]).isEmpty { - // Handle paste keyboard shortcut - if let content = NSPasteboard.general.string(forType: .string) { - newCharacters = Array(content) + if event.key == .v + && !inputState.keys.intersection([.leftCommand, .rightCommand]).isEmpty + { + // Handle paste keyboard shortcut + if let content = NSPasteboard.general.string(forType: .string) { + newCharacters = Array(content) + } + } else if message.utf8.count < InGameGUI.maximumMessageLength { + newCharacters = event.characters } - } else if message.utf8.count < InGameGUI.maximumMessageLength { - newCharacters = event.characters - } #else - if message.utf8.count < InGameGUI.maximumMessageLength { - newCharacters = event.characters - } + if message.utf8.count < InGameGUI.maximumMessageLength { + newCharacters = event.characters + } #endif // Ensure that the message doesn't exceed 256 bytes (including if multi-byte characters are entered). @@ -320,13 +340,16 @@ public final class PlayerInputSystem: System { // 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) + 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( @@ -340,7 +363,11 @@ public final class PlayerInputSystem: System { } if event.key == .escape || event.input == .toggleInventory { - try window.close(mouseStack: &guiState.mouseItemStack, eventBus: eventBus, connection: connection) + try window.close( + mouseStack: &guiState.mouseItemStack, + eventBus: eventBus, + connection: connection + ) guiState.window = nil } diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index a6ee8c82..3a15d7b4 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -1,6 +1,6 @@ -import SwiftCPUDetect -import CoreFoundation import Collections +import CoreFoundation +import SwiftCPUDetect /// Never acquires nexus locks. public class InGameGUI { @@ -15,14 +15,14 @@ public class InGameGUI { static let chatHistoryWidth = 330 #if os(macOS) - /// The system's CPU display name. - static let cpuName = HWInfo.CPU.name() - /// The system's CPU architecture. - static let cpuArch = CpuArchitecture.current()?.rawValue - /// The system's total memory. - static let totalMem = (HWInfo.ramAmount() ?? 0) / (1024 * 1024 * 1024) - /// A string containing information about the system's default GPU. - static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() + /// The system's CPU display name. + static let cpuName = HWInfo.CPU.name() + /// The system's CPU architecture. + static let cpuArch = CpuArchitecture.current()?.rawValue + /// The system's total memory. + static let totalMem = (HWInfo.ramAmount() ?? 0) / (1024 * 1024 * 1024) + /// A string containing information about the system's default GPU. + static let gpuInfo = GPUDetection.mainMetalGPU()?.infoString() #endif static let xpLevelTextColor = Vec4f(126, 252, 31, 255) / 255 @@ -31,9 +31,13 @@ public class InGameGUI { public init() {} /// Gets the GUI's content. Doesn't acquire any locks. - public func content(game: Game, connection: ServerConnection?, state: GUIStateStorage) -> GUIElement { - let (gamemode, inventory) = game.accessPlayer(acquireLock: false) { player in - (player.gamemode.gamemode, player.inventory) + public func content( + game: Game, + connection: ServerConnection?, + state: GUIStateStorage + ) -> GUIElement { + let (gamemode, inventory, perspective) = game.accessPlayer(acquireLock: false) { player in + (player.gamemode.gamemode, player.inventory, player.camera.perspective) } let inputState = game.accessInputState(acquireLock: false, action: identity) @@ -44,15 +48,17 @@ public class InGameGUI { GUIElement.stack { hotbarArea(game: game, gamemode: gamemode) - GUIElement.sprite(.crossHair) - .center() + if perspective == .firstPerson { + GUIElement.sprite(.crossHair) + .center() + } } } GUIElement.forEach(in: state.bossBars, spacing: 3) { bossBar in self.bossBar(bossBar) } - .constraints(.top(2), .center) + .constraints(.top(2), .center) if state.movementAllowed && inputState.keys.contains(.tab) { tabList(game.tabList) @@ -99,7 +105,7 @@ public class InGameGUI { GUIElement.sprite(bossBar.style.overlay) } } - .size(GUISprite.xpBarBackground.descriptor.size.x, nil) + .size(GUISprite.xpBarBackground.descriptor.size.x, nil) } public func tabList(_ tabList: TabList) -> GUIElement { @@ -130,17 +136,17 @@ public class InGameGUI { } } } - .padding(.bottom, -1) - .background(Vec4f(0, 0, 0, 0.5)) + .padding(.bottom, -1) + .background(Vec4f(0, 0, 0, 0.5)) GUIElement.forEach(in: sortedPlayers, spacing: 1) { player in GUIElement.sprite(.playerConnectionStrength(player.connectionStrength)) .padding([.top, .right], 1) .padding(.left, 2) } - .background(Vec4f(0, 0, 0, 0.5)) + .background(Vec4f(0, 0, 0, 0.5)) } - .border([.top, .left, .bottom], 1, borderColor) + .border([.top, .left, .bottom], 1, borderColor) } public func chat(state: GUIStateStorage) -> GUIElement { @@ -160,9 +166,10 @@ public class InGameGUI { if state.showChat { visibleMessages = latestMessages } else { - let lastVisibleIndex = latestMessages.lastIndex { message in - message.timeReceived < threshold - }?.advanced(by: 1) ?? latestMessages.startIndex + let lastVisibleIndex = + latestMessages.lastIndex { message in + message.timeReceived < threshold + }?.advanced(by: 1) ?? latestMessages.startIndex visibleMessages = latestMessages[lastVisibleIndex.. GUIElement { @@ -251,7 +258,7 @@ public class InGameGUI { GUIElement.forEach(in: slots, direction: .horizontal, spacing: 4) { slot in inventorySlot(slot) } - .positionInParent(4, 4) + .positionInParent(4, 4) } } @@ -282,17 +289,19 @@ public class InGameGUI { return event.key == .leftMouseButton || event.key == .rightMouseButton } - GUIElement.stack(elements: window.type.areas.map { area in - windowArea( - area, - window, - game: game, - connection: connection, - state: state - ) - }) + GUIElement.stack( + elements: window.type.areas.map { area in + windowArea( + area, + window, + game: game, + connection: connection, + state: state + ) + } + ) } - .center() + .center() if let mouseItemStack = state.mouseItemStack { inventorySlot(Slot(mouseItemStack)) @@ -330,7 +339,7 @@ public class InGameGUI { } } } - .positionInParent(area.position) + .positionInParent(area.position) } public func inventorySlot(_ slot: Slot) -> GUIElement { @@ -345,7 +354,7 @@ public class InGameGUI { .float() } } - .size(16, 16) + .size(16, 16) } else { return GUIElement.spacer(width: 16, height: 16) } @@ -394,10 +403,10 @@ public class InGameGUI { outlineIcon: .foodOutline, direction: .rightToLeft ) - .constraints(.top(0), .right(0)) + .constraints(.top(0), .right(0)) } - .size(GUISprite.hotbar.descriptor.size.x, nil) - .constraints(.top(0), .center) + .size(GUISprite.hotbar.descriptor.size.x, nil) + .constraints(.top(0), .center) GUIElement.stack { continuousMeter( @@ -405,13 +414,13 @@ public class InGameGUI { background: .xpBarBackground, foreground: .xpBarForeground ) - .constraints(.top(0), .center) + .constraints(.top(0), .center) outlinedText("\(xpLevel)", textColor: Self.xpLevelTextColor) .constraints(.top(-7), .center) } - .padding(1) - .constraints(.top(0), .center) + .padding(1) + .constraints(.top(0), .center) } } @@ -441,7 +450,8 @@ public class InGameGUI { let relativePosition = blockPosition.relativeToChunkSection let relativePositionString = "\(relativePosition.x) \(relativePosition.y) \(relativePosition.z)" - let chunkSectionString = "\(chunkSectionPosition.sectionX) \(chunkSectionPosition.sectionY) \(chunkSectionPosition.sectionZ)" + let chunkSectionString = + "\(chunkSectionPosition.sectionX) \(chunkSectionPosition.sectionY) \(chunkSectionPosition.sectionZ)" let yawString = String(format: "%.01f", yaw) let pitchString = String(format: "%.01f", pitch) @@ -469,8 +479,8 @@ public class InGameGUI { // Lighting (at foot level) "Light: \(skyLightLevel) sky, \(blockLightLevel) block", "Biome: \(biome?.identifier.description ?? "not loaded")", - "Gamemode: \(gamemode.string)" - ] + "Gamemode: \(gamemode.string)", + ], ] #if os(macOS) @@ -478,7 +488,7 @@ public class InGameGUI { [ "CPU: \(Self.cpuName ?? "unknown") (\(Self.cpuArch ?? "n/a"))", "Total mem: \(Self.totalMem)GB", - "GPU: \(Self.gpuInfo ?? "unknown")" + "GPU: \(Self.gpuInfo ?? "unknown")", ] ] #else @@ -505,7 +515,7 @@ public class InGameGUI { .background(Self.debugScreenRowBackgroundColor) .constraints(.top(0), side == .left ? .left(0) : .right(0)) } - .padding(1) + .padding(1) } } @@ -527,8 +537,10 @@ public class InGameGUI { let fpsString = String(format: "%.00f fps", renderStatistics.averageFPS) let timingsString = [cpuTimeString, gpuTimeString].compactMap(identity).joined(separator: ", ") - - return [fpsString, theoreticalFPSString, "(\(timingsString))"].compactMap(identity).joined(separator: " ") + + return [fpsString, theoreticalFPSString, "(\(timingsString))"] + .compactMap(identity) + .joined(separator: " ") } public func outlinedText( @@ -575,7 +587,7 @@ public class InGameGUI { public func continuousMeter( _ value: Float, background: GUISprite, - foreground:GUISprite + foreground: GUISprite ) -> GUIElement { var croppedForeground = foreground.descriptor croppedForeground.size.x = Int(Float(croppedForeground.size.x) * value) diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 0bea7b45..d132a4f3 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeECS +import Foundation /// Stores all of the game data such as entities, chunks and chat messages. public final class Game: @unchecked Sendable { @@ -43,11 +43,11 @@ public final class Game: @unchecked Sendable { // MARK: Private properties #if DEBUG_LOCKS - /// A locked for managing safe access of ``nexus``. - public let nexusLock = ReadWriteLock() + /// A locked for managing safe access of ``nexus``. + public let nexusLock = ReadWriteLock() #else - /// A locked for managing safe access of ``nexus``. - private let nexusLock = ReadWriteLock() + /// A locked for managing safe access of ``nexus``. + private let nexusLock = ReadWriteLock() #endif /// The container for the game's entities. Strictly only contains what Minecraft counts as /// entities. Doesn't include block entities. @@ -101,7 +101,8 @@ public final class Game: @unchecked Sendable { // require significant refactoring if we wanna do it right (as in not just hacking it // together for the specific case of PlayerInputSystem); proper resource pack propagation // will probably take quite a bit of work. - tickScheduler.addSystem(PlayerInputSystem(connection, self, eventBus, configuration, font, locale)) + tickScheduler.addSystem( + PlayerInputSystem(connection, self, eventBus, configuration, font, locale)) tickScheduler.addSystem(PlayerFlightSystem()) tickScheduler.addSystem(PlayerAccelerationSystem()) tickScheduler.addSystem(PlayerJumpSystem()) @@ -129,7 +130,7 @@ public final class Game: @unchecked Sendable { /// - key: The pressed key if any. /// - input: The pressed input if any. /// - characters: The characters typed by the pressed key. - public func press(key: Key?, input: Input?, characters: [Character] = []) { // swiftlint:disable:this cyclomatic_complexity + public func press(key: Key?, input: Input?, characters: [Character] = []) { // swiftlint:disable:this cyclomatic_complexity nexusLock.acquireWriteLock() defer { nexusLock.unlock() } inputState.press(key: key, input: input, characters: characters) @@ -210,7 +211,9 @@ public final class Game: @unchecked Sendable { } /// Mutates the GUI state with a given action. - public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) throws -> R) rethrows -> R { + public func mutateGUIState(acquireLock: Bool = true, action: (inout GUIState) throws -> R) + rethrows -> R + { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } return try action(&_guiState.inner) @@ -302,7 +305,9 @@ public final class Game: @unchecked Sendable { /// - componentType: The type of component to access. /// - acquireLock: If `false`, no lock is acquired. Only use if you know what you're doing. /// - action: The action to perform on the component if the entity exists and contains that component. - public func accessComponent(entityId: Int, _ componentType: T.Type, acquireLock: Bool = true, action: (T) -> Void) { + public func accessComponent( + entityId: Int, _ componentType: T.Type, acquireLock: Bool = true, action: (T) -> Void + ) { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } @@ -318,9 +323,9 @@ public final class Game: @unchecked Sendable { /// Removes the entity with the given vanilla id from the game if it exists. /// - Parameter id: The id of the entity to remove. - public func removeEntity(id: Int) { - nexusLock.acquireWriteLock() - defer { nexusLock.unlock() } + public func removeEntity(acquireLock: Bool = true, id: Int) { + if acquireLock { nexusLock.acquireWriteLock() } + defer { if acquireLock { nexusLock.unlock() } } if let identifier = entityIdToEntityIdentifier[id] { nexus.destroy(entityId: identifier) @@ -356,7 +361,8 @@ public final class Game: @unchecked Sendable { /// - Parameters: /// - acquireLock: If `false`, no lock is acquired. Only use if you know what you're doing. /// - action: The action to perform on the player. - public func accessPlayer(acquireLock: Bool = true, action: (Player) throws -> T) rethrows -> T { + public func accessPlayer(acquireLock: Bool = true, action: (Player) throws -> T) rethrows -> T + { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } @@ -447,7 +453,10 @@ public final class Game: @unchecked Sendable { continue } - guard let (distance, face) = hitbox.aabb(at: position.vector).intersectionDistanceAndFace(with: playerRay) else { + guard + let (distance, face) = hitbox.aabb(at: position.vector).intersectionDistanceAndFace( + with: playerRay) + else { continue } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/DestroyEntitiesPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/DestroyEntitiesPacket.swift index 7f39c306..85ae4a9c 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/DestroyEntitiesPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/DestroyEntitiesPacket.swift @@ -1,7 +1,7 @@ /// Removes entities from the game (either dead or disconnected or outside render distance). -public struct DestroyEntitiesPacket: ClientboundPacket { +public struct DestroyEntitiesPacket: ClientboundEntityPacket { public static let id: Int = 0x37 - + public var entityIds: [Int] public init(from packetReader: inout PacketReader) throws { @@ -12,10 +12,11 @@ public struct DestroyEntitiesPacket: ClientboundPacket { entityIds.append(entityId) } } - + + /// Should only be called if a nexus lock has already been acquired. public func handle(for client: Client) throws { for entityId in entityIds { - client.game.removeEntity(id: entityId) + client.game.removeEntity(acquireLock: false, id: entityId) } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift index af21df0c..01f369b1 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift @@ -1,13 +1,26 @@ import Foundation -public struct EntityStatusPacket: ClientboundPacket { +public struct EntityStatusPacket: ClientboundEntityPacket { public static let id: Int = 0x1b - + public var entityId: Int - public var status: Int8 - + public var status: Status? + + // TODO: Add other statuses + public enum Status: Int8 { + case death = 3 + } + public init(from packetReader: inout PacketReader) throws { entityId = try packetReader.readInt() - status = try packetReader.readByte() + status = Status(rawValue: try packetReader.readByte()) + } + + /// Should only be called if a nexus lock has already been acquired. + public func handle(for client: Client) throws { + if status == .death { + // TODO: Play a death animation instead of instantly removing entities on death + client.game.removeEntity(acquireLock: false, id: entityId) + } } } diff --git a/Sources/Core/Sources/Network/Socket.swift b/Sources/Core/Sources/Network/Socket.swift index daea4a37..fde90526 100644 --- a/Sources/Core/Sources/Network/Socket.swift +++ b/Sources/Core/Sources/Network/Socket.swift @@ -1,7 +1,8 @@ +import Foundation + #if canImport(WinSDK) -import WinSDK.WinSock2 + import WinSDK.WinSock2 #endif -import Foundation /// A socket that can connect to internet and Unix sockets. /// @@ -114,7 +115,8 @@ public struct Socket: Sendable, Hashable { /// - type: The type of socket to create. /// - Throws: An error is thrown if the socket could not be created. public init(_ addressFamily: AddressFamily, _ type: SocketType) throws { - let descriptor = FileDescriptor(rawValue: Socket.socket(addressFamily.rawValue, type.rawValue, 0)) + let descriptor = FileDescriptor( + rawValue: Socket.socket(addressFamily.rawValue, type.rawValue, 0)) guard descriptor != .invalid else { throw SocketError.actionFailed("create") } @@ -147,8 +149,18 @@ public struct Socket: Sendable, Hashable { public func setValue(_ value: O.Value, for option: O) throws { var value = option.makeSocketValue(from: value) let length = socklen_t(MemoryLayout.size) - guard Socket.setsockopt(file.rawValue, option.level, option.name, &value, length) >= 0 else { - throw SocketError.actionFailed("set option") + try withUnsafePointer(to: &value) { valuePointer in + guard + Socket.setsockopt( + file.rawValue, + option.level, + option.name, + UnsafeRawPointer(valuePointer), + length + ) >= 0 + else { + throw SocketError.actionFailed("set option") + } } } @@ -242,7 +254,8 @@ public struct Socket: Sendable, Hashable { static func makeInAddr(fromIP4 address: String) throws -> in_addr { var addr = in_addr() guard address.withCString({ Socket.inet_pton(AF_INET, $0, &addr) }) == 1 else { - throw SocketError.actionFailed("convert ipv4 address to in_addr (pton, address: '\(address)')") + throw SocketError.actionFailed( + "convert ipv4 address to in_addr (pton, address: '\(address)')") } return addr } diff --git a/Sources/Core/Sources/Resources/Model/Block/JSON/JSONBlockModel.swift b/Sources/Core/Sources/Resources/Model/Block/JSON/JSONBlockModel.swift index fc187034..913ff827 100644 --- a/Sources/Core/Sources/Resources/Model/Block/JSON/JSONBlockModel.swift +++ b/Sources/Core/Sources/Resources/Model/Block/JSON/JSONBlockModel.swift @@ -46,7 +46,8 @@ extension JSONBlockModel { let files = try FileManager.default.contentsOfDirectory( at: directory, includingPropertiesForKeys: nil, - options: .skipsSubdirectoryDescendants) + options: .skipsSubdirectoryDescendants + ) // All file reading operations are performed at once which is best for performance apparently // The models are combined into one big JSON object which should also minimise losses from @@ -83,7 +84,8 @@ extension JSONBlockModel { // swiftlint:enable force_unwrapping // Load JSON - let models: [String: JSONBlockModel] = try CustomJSONDecoder().decode([String: JSONBlockModel].self, from: json) + let models: [String: JSONBlockModel] = try CustomJSONDecoder() + .decode([String: JSONBlockModel].self, from: json) // Convert from [String: JSONBlockModel] to [Identifier: JSONBlockModel] var identifiedModels: [Identifier: JSONBlockModel] = [:] diff --git a/Sources/Core/Sources/Resources/Model/Entity/EntityModelPalette.swift b/Sources/Core/Sources/Resources/Model/Entity/EntityModelPalette.swift new file mode 100644 index 00000000..1b29e220 --- /dev/null +++ b/Sources/Core/Sources/Resources/Model/Entity/EntityModelPalette.swift @@ -0,0 +1,92 @@ +import Foundation + +public enum EntityModelPaletteError: LocalizedError { + case failedToDownloadJSONEntityModelPack(Error) + case failedToUnzipJSONEntityModelPack(Error) + case failedToDeserializeJSONEntityModel(URL, Error) + case failedToCopyJSONEntityModels(sourceDirectory: URL, destinationDirectory: URL, Error) + + public var errorDescription: String? { + switch self { + case let .failedToDownloadJSONEntityModelPack(error): + return "Failed to download default JSON Entity Model pack (\(error))." + case let .failedToUnzipJSONEntityModelPack(error): + return "Failed to unzip default JSON Entity Model pack (\(error))." + case let .failedToDeserializeJSONEntityModel(file, error): + return """ + Failed to deserialize JSON entity model (\(error)). + File: \(file) + """ + case let .failedToCopyJSONEntityModels(sourceDirectory, destinationDirectory, error): + return """ + Failed to copy JSON entity models (\(error)). + Source directory: \(sourceDirectory) + Destination directory: \(destinationDirectory) + """ + } + } +} + +public struct EntityModelPalette { + // swiftlint:disable force_unwrapping + static let jsonEntityModelPackDownloadURL = URL( + string: "https://www.curseforge.com/api/v1/mods/360910/files/3268537/download" + )! + + public var models: [Identifier: JSONEntityModel] = [:] + + /// Creates an empty entity model palette. + public init(models: [Identifier: JSONEntityModel] = [:]) { + self.models = models + } + + /// Loads the JSON Entity Models contained in a specified directory. + public static func load(from directory: URL, namespace: String) throws -> Self { + let models = try JSONEntityModel.loadModels(from: directory, namespace: namespace) + return EntityModelPalette(models: models) + } + + /// Downloads the required JSON Entity Model files to the specified directory. Downloads them + /// from the Template CEM project on CurseForge. + public static func downloadJSONEntityModels(to directory: URL) throws { + let temporaryDirectory = FileManager.default.temporaryDirectory + let packZipFile = temporaryDirectory.appendingPathComponent("json_entity_models.zip") + do { + let data = try RequestUtil.data(contentsOf: Self.jsonEntityModelPackDownloadURL) + try data.write(to: packZipFile) + } catch { + throw EntityModelPaletteError.failedToDownloadJSONEntityModelPack(error) + } + + let packDirectory = temporaryDirectory.appendingPathComponent("json_entity_models") + try? FileManager.default.removeItem(at: packDirectory) + do { + try FileManager.default.unzipItem(at: packZipFile, to: packDirectory, skipCRC32: true) + } catch { + throw EntityModelPaletteError.failedToUnzipJSONEntityModelPack(error) + } + + let entityModelsDirectory = packDirectory.appendingPathComponent( + "assets/minecraft/optifine/cem" + ) + do { + let files = try FileManager.default.contentsOfDirectory( + at: entityModelsDirectory, + includingPropertiesForKeys: nil, + options: [.skipsSubdirectoryDescendants] + ) + for file in files where file.pathExtension == "jem" { + try FileManager.default.copyItem( + at: file, + to: directory.appendingPathComponent(file.lastPathComponent) + ) + } + } catch { + throw EntityModelPaletteError.failedToCopyJSONEntityModels( + sourceDirectory: entityModelsDirectory, + destinationDirectory: directory, + error + ) + } + } +} diff --git a/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift b/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift new file mode 100644 index 00000000..ef944008 --- /dev/null +++ b/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift @@ -0,0 +1,62 @@ +import Foundation + +/// An entity model represented in the standard JSON Entity Model format (from `.jem` files). +public struct JSONEntityModel: Codable { + public var textureSize: Vec2f + public var models: [Submodel] + + public struct Submodel: Codable { + public var part: String? + public var id: String? + public var invertAxis: String? + public var mirrorTexture: String? + public var translate: Vec3f? + public var rotate: Vec3f? + public var boxes: [Box]? + public var submodels: [Submodel]? + public var animations: [[String: Either]]? + } + + public struct Box: Codable { + public var coordinates: [Float] + public var textureOffset: Vec2i? + public var uvNorth: Vec4i? + public var uvEast: Vec4i? + public var uvSouth: Vec4i? + public var uvWest: Vec4i? + public var uvUp: Vec4i? + public var uvDown: Vec4i? + public var sizeAdd: Float? + } + + /// Loads all JSON Entity Models from + public static func loadModels( + from directory: URL, + namespace: String + ) throws -> [Identifier: JSONEntityModel] { + let files = try FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: nil, + options: .skipsSubdirectoryDescendants + ) + + var models: [Identifier: JSONEntityModel] = [:] + for file in files where file.pathExtension == "jem" { + var identifier = Identifier( + namespace: namespace, + name: file.deletingPathExtension().lastPathComponent + ) + + let model: JSONEntityModel + do { + let data = try Data(contentsOf: file) + model = try CustomJSONDecoder().decode(JSONEntityModel.self, from: data) + } catch { + throw EntityModelPaletteError.failedToDeserializeJSONEntityModel(file, error) + } + models[identifier] = model + } + + return models + } +} diff --git a/Sources/Core/Sources/Resources/ResourcePack.swift b/Sources/Core/Sources/Resources/ResourcePack.swift index 105b4795..3747de6e 100644 --- a/Sources/Core/Sources/Resources/ResourcePack.swift +++ b/Sources/Core/Sources/Resources/ResourcePack.swift @@ -30,6 +30,9 @@ enum ResourcePackError: LocalizedError { case versionManifestFailure /// Failed to decode the versions manifest. case versionsManifestFailure + /// Failed to download default JSON entity models (downloaded separately from the default + /// vanilla resources). + case failedToDownloadJSONEntityModels var errorDescription: String? { switch self { @@ -42,17 +45,18 @@ enum ResourcePackError: LocalizedError { case .failedToEnumerateNamespaces: return "Failed to figure out what namespaces are included in this resource pack." case .failedToLoadTexture(let identifier): - return "Failed to convert an image into a texture with identifier: `\(identifier.description)`." + return + "Failed to convert an image into a texture with identifier: `\(identifier.description)`." case .failedToReadTextureImage(let url): return """ - Failed to read the image for the given texture from a file. - File URL: \(url.absoluteString) - """ + Failed to read the image for the given texture from a file. + File URL: \(url.absoluteString) + """ case .failedToCreateImageProvider(let url): return """ - Failed to create a `CGDataProvider` for the given image file. - File URL: \(url.absoluteString) - """ + Failed to create a `CGDataProvider` for the given image file. + File URL: \(url.absoluteString) + """ case .clientJarDownloadFailure: return "Failed to download the specified client jar." case .clientJarExtractionFailure: @@ -67,10 +71,14 @@ enum ResourcePackError: LocalizedError { return "Failed to decode a version manifest." case .versionsManifestFailure: return "Failed to decode the versions manifest." + case .failedToDownloadJSONEntityModels: + return "Failed to download default JSON entity models." } } } +// TODO: Version resource pack caches so that we can easily invalidate outdated caches +// when the format changes. /// A resource pack. public struct ResourcePack { /// The metadata of languages contained within this resource pack. @@ -132,14 +140,20 @@ public struct ResourcePack { let mcMeta = try readPackMCMeta(at: mcMetaFile) // Read resources from present namespaces - guard let contents = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: []) else { + guard + let contents = try? FileManager.default.contentsOfDirectory( + at: directory, + includingPropertiesForKeys: nil, + options: [] + ) + else { throw ResourcePackError.failedToEnumerateNamespaces } var namespacedResources: [String: ResourcePack.Resources] = [:] for directory in contents where FileManager.default.directoryExists(at: directory) { let namespace = directory.lastPathComponent - let resources = try loadResources( + let resources = try Resources.load( from: directory, inNamespace: namespace, cacheDirectory: cacheDirectory?.appendingPathComponent(namespace) @@ -154,149 +168,6 @@ public struct ResourcePack { ) } - public static func loadCachedResources( - cacheDirectory: URL - ) throws -> ResourcePack.Resources { - var resources = Resources() - resources.blockTexturePalette = try TexturePalette.loadCached(from: cacheDirectory.appendingPathComponent("BlockTexturePalette.bin")) - resources.itemTexturePalette = try TexturePalette.loadCached(from: cacheDirectory.appendingPathComponent("ItemTexturePalette.bin")) - resources.guiTexturePalette = try TexturePalette.loadCached(from: cacheDirectory.appendingPathComponent("GUITexturePalette.bin")) - resources.environmentTexturePalette = try TexturePalette.loadCached(from: cacheDirectory.appendingPathComponent("EnvironmentTexturePalette.bin")) - resources.blockModelPalette = try BlockModelPalette.loadCached(fromDirectory: cacheDirectory) - resources.itemModelPalette = try ItemModelPalette.loadCached(fromDirectory: cacheDirectory) - resources.fontPalette = try FontPalette.loadCached(fromDirectory: cacheDirectory) - return resources - } - - /// Loads the resources in the given directory and gives them the specified namespace. - public static func loadResources( - from directory: URL, - inNamespace namespace: String, - cacheDirectory: URL? - ) throws -> ResourcePack.Resources { - log.debug("Loading resources from '\(namespace)' namespace") - - var resources = Resources() - var loadedCachedResources = false - if let cacheDirectory = cacheDirectory, FileManager.default.directoryExists(at: cacheDirectory) { - do { - resources = try loadCachedResources(cacheDirectory: cacheDirectory) - loadedCachedResources = true - } catch { - do { - try FileManager.default.removeItem(at: cacheDirectory.deletingLastPathComponent()) - } catch { - log.warning("Failed to delete invalid resource caches") - } - } - } - - let textureDirectory = directory.appendingPathComponent("textures") - let modelDirectory = directory.appendingPathComponent("models") - - // Load biome colors - let colorMapDirectory = textureDirectory.appendingPathComponent("colormap") - if FileManager.default.directoryExists(at: colorMapDirectory) { - log.debug("Loading biome colors") - resources.biomeColors = try BiomeColors(from: colorMapDirectory) - } - - // Load locales - let localeDirectory = directory.appendingPathComponent("lang") - if FileManager.default.directoryExists(at: localeDirectory) { - log.debug("Loading locales") - let contents = try FileManager.default.contentsOfDirectory(at: localeDirectory, includingPropertiesForKeys: nil) - for file in contents where file.pathExtension == "json" { - let locale = try MinecraftLocale(localeFile: file) - resources.locales[file.deletingPathExtension().lastPathComponent] = locale - } - } - - if !loadedCachedResources { - log.debug("Loading textures") - - // Load block textures - let blockTextureDirectory = textureDirectory.appendingPathComponent("block") - if FileManager.default.directoryExists(at: blockTextureDirectory) { - resources.blockTexturePalette = try TexturePalette.load( - from: blockTextureDirectory, - inNamespace: namespace, - withType: "block" - ) - } - - /// Load item textures - let itemTextureDirectory = textureDirectory.appendingPathComponent("item") - if FileManager.default.directoryExists(at: itemTextureDirectory) { - resources.itemTexturePalette = try TexturePalette.load( - from: itemTextureDirectory, - inNamespace: namespace, - withType: "item" - ) - } - - // Load GUI textures - let guiTextureDirectory = textureDirectory.appendingPathComponent("gui") - if FileManager.default.directoryExists(at: guiTextureDirectory) { - resources.guiTexturePalette = try TexturePalette.load( - from: guiTextureDirectory, - inNamespace: namespace, - withType: "gui", - recursive: true, - isAnimated: false - ) - } - - // Load GUI textures - let environmentTextureDirectory = textureDirectory.appendingPathComponent("environment") - if FileManager.default.directoryExists(at: environmentTextureDirectory) { - resources.environmentTexturePalette = try TexturePalette.load( - from: environmentTextureDirectory, - inNamespace: namespace, - withType: "environment", - isAnimated: false - ) - } - - // Load block models - let blockModelDirectory = modelDirectory.appendingPathComponent("block") - if FileManager.default.directoryExists(at: blockModelDirectory) { - log.debug("Loading block models") - resources.blockModelPalette = try BlockModelPalette.load( - from: blockModelDirectory, - namespace: namespace, - blockTexturePalette: resources.blockTexturePalette - ) - } - - // Load item models - let itemModelDirectory = modelDirectory.appendingPathComponent("item") - if FileManager.default.directoryExists(at: itemModelDirectory) { - log.debug("Loading item models") - resources.itemModelPalette = try ItemModelPalette.load( - from: itemModelDirectory, - itemTexturePalette: resources.itemTexturePalette, - blockTexturePalette: resources.blockTexturePalette, - blockModelPalette: resources.blockModelPalette, - namespace: namespace - ) - } - - // Load fonts - let fontDirectory = directory.appendingPathComponent("font") - if FileManager.default.directoryExists(at: fontDirectory) { - log.debug("Loading fonts") - resources.fontPalette = try FontPalette.load( - from: fontDirectory, - namespaceDirectory: directory, - textureDirectory: textureDirectory - ) - } - } - - return resources - } - /// Reads a pack.mcmeta file. public static func readPackMCMeta(at mcMetaFile: URL) throws -> ResourcePack.PackMCMeta { let mcMeta: ResourcePack.PackMCMeta @@ -317,20 +188,13 @@ public struct ResourcePack { for (namespace, resources) in resources { log.debug("Caching resources from '\(namespace)' namespace") let cacheDirectory = directory.appendingPathComponent(namespace) - try FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil) - - // Cache models - try resources.blockModelPalette.cache(toDirectory: cacheDirectory) - try resources.itemModelPalette.cache(toDirectory: cacheDirectory) - - // Cache textures - try resources.blockTexturePalette.cache(to: cacheDirectory.appendingPathComponent("BlockTexturePalette.bin")) - try resources.itemTexturePalette.cache(to: cacheDirectory.appendingPathComponent("ItemTexturePalette.bin")) - try resources.guiTexturePalette.cache(to: cacheDirectory.appendingPathComponent("GUITexturePalette.bin")) - try resources.environmentTexturePalette.cache(to: cacheDirectory.appendingPathComponent("EnvironmentTexturePalette.bin")) + try FileManager.default.createDirectory( + at: cacheDirectory, + withIntermediateDirectories: true, + attributes: nil + ) - // Cache fonts - try resources.fontPalette.cache(toDirectory: cacheDirectory) + try resources.cache(to: cacheDirectory) } } @@ -338,7 +202,12 @@ public struct ResourcePack { /// Tasks to be exectued during the `downloadVanillaAssets` process public enum DownloadStep: CaseIterable, TaskStep { - case fetchManifest, downloadJar, extractJar, copyingAssets, creatingMcmeta + case fetchManifest + case downloadJar + case extractJar + case copyingAssets + case downloadEntityModels + case creatingMcmeta public var relativeDuration: Double { 1 } @@ -348,6 +217,7 @@ public struct ResourcePack { case .downloadJar: return "Downloading client jar" case .extractJar: return "Extracting client jar" case .copyingAssets: return "Copying assets" + case .downloadEntityModels: return "Download entity models" case .creatingMcmeta: return "Creating pack.mcmeta" } } @@ -377,10 +247,16 @@ public struct ResourcePack { // Extract the contents of the client jar (jar files are just zip archives) progress?.update(to: .extractJar) - let extractedClientJarDirectory = temporaryDirectory.appendingPathComponent("client", isDirectory: true) + let extractedClientJarDirectory = + temporaryDirectory + .appendingPathComponent("client", isDirectory: true) try? FileManager.default.removeItem(at: extractedClientJarDirectory) do { - try FileManager.default.unzipItem(at: clientJarTempFile, to: extractedClientJarDirectory, skipCRC32: true) + try FileManager.default.unzipItem( + at: clientJarTempFile, + to: extractedClientJarDirectory, + skipCRC32: true + ) } catch { throw ResourcePackError.clientJarExtractionFailure.becauseOf(error) } @@ -390,11 +266,25 @@ public struct ResourcePack { do { try FileManager.default.copyItem( at: extractedClientJarDirectory.appendingPathComponent("assets"), - to: directory) + to: directory + ) } catch { throw ResourcePackError.assetCopyFailure.becauseOf(error) } + // Download default JSON entity models + progress?.update(to: .downloadEntityModels) + do { + let entityModelsDirectory = directory.appendingPathComponent("minecraft/models/entity") + try FileManager.default.createDirectory( + at: entityModelsDirectory, + withIntermediateDirectories: false + ) + try EntityModelPalette.downloadJSONEntityModels(to: entityModelsDirectory) + } catch { + throw ResourcePackError.failedToDownloadJSONEntityModels.becauseOf(error) + } + // Create a default pack.mcmeta for it progress?.update(to: .creatingMcmeta) let contents = #"{"pack": {"pack_format": 5, "description": "The default vanilla assets"}}"# @@ -411,7 +301,9 @@ public struct ResourcePack { /// Get the manifest describing all versions. private static func getVersionsManifest() throws -> VersionsManifest { - let versionsManifestURL = URL(string: "https://launchermeta.mojang.com/mc/game/version_manifest.json")! + let versionsManifestURL = URL( + string: "https://launchermeta.mojang.com/mc/game/version_manifest.json" + )! let versionsManifest: VersionsManifest do { diff --git a/Sources/Core/Sources/Resources/Resources.swift b/Sources/Core/Sources/Resources/Resources.swift index d0666c75..c95e9413 100644 --- a/Sources/Core/Sources/Resources/Resources.swift +++ b/Sources/Core/Sources/Resources/Resources.swift @@ -1,10 +1,13 @@ import Foundation -// TODO: move resource loading code to here - extension ResourcePack { /// A namespace of resources. public struct Resources { + public static let blockTextureCacheFileName = "BlockTexturePalette.bin" + public static let itemTextureCacheFileName = "ItemTexturePalette.bin" + public static let entityTextureCacheFileName = "EntityTexturePalette.bin" + public static let guiTextureCacheFileName = "GUITexturePalette.bin" + public static let environmentTextureCacheFileName = "EnvironmentTexturePalette.bin" /// The palette holding block textures. public var blockTexturePalette = TexturePalette() /// The palette holding block models. @@ -13,6 +16,10 @@ extension ResourcePack { public var itemTexturePalette = TexturePalette() /// The palette holding item models. public var itemModelPalette = ItemModelPalette() + /// The palette holding entity textures. + public var entityTexturePalette = TexturePalette() + /// The palette holding entity models. + public var entityModelPalette = EntityModelPalette() /// The GUI texture palette. public var guiTexturePalette = TexturePalette() /// The environment texture palette (containing textures for the sun and moon etc.). @@ -26,5 +33,206 @@ extension ResourcePack { /// Creates a new empty namespace of resources. public init() {} + + public func cache(to cacheDirectory: URL) throws { + // Cache models + try blockModelPalette.cache(toDirectory: cacheDirectory) + try itemModelPalette.cache(toDirectory: cacheDirectory) + + // Cache textures + try blockTexturePalette.cache( + to: cacheDirectory.appendingPathComponent(Self.blockTextureCacheFileName) + ) + try itemTexturePalette.cache( + to: cacheDirectory.appendingPathComponent(Self.itemTextureCacheFileName) + ) + try entityTexturePalette.cache( + to: cacheDirectory.appendingPathComponent(Self.entityTextureCacheFileName) + ) + try guiTexturePalette.cache( + to: cacheDirectory.appendingPathComponent(Self.guiTextureCacheFileName) + ) + try environmentTexturePalette.cache( + to: cacheDirectory.appendingPathComponent(Self.environmentTextureCacheFileName) + ) + + // Cache fonts + try fontPalette.cache(toDirectory: cacheDirectory) + } + + public static func loadCached(from cacheDirectory: URL) throws -> ResourcePack.Resources { + var resources = Resources() + resources.blockTexturePalette = try TexturePalette.loadCached( + from: cacheDirectory.appendingPathComponent(Self.blockTextureCacheFileName) + ) + resources.itemTexturePalette = try TexturePalette.loadCached( + from: cacheDirectory.appendingPathComponent(Self.itemTextureCacheFileName) + ) + resources.entityTexturePalette = try TexturePalette.loadCached( + from: cacheDirectory.appendingPathComponent(Self.entityTextureCacheFileName) + ) + resources.guiTexturePalette = try TexturePalette.loadCached( + from: cacheDirectory.appendingPathComponent(Self.guiTextureCacheFileName) + ) + resources.environmentTexturePalette = try TexturePalette.loadCached( + from: cacheDirectory.appendingPathComponent(Self.environmentTextureCacheFileName) + ) + resources.blockModelPalette = try BlockModelPalette.loadCached(fromDirectory: cacheDirectory) + resources.itemModelPalette = try ItemModelPalette.loadCached(fromDirectory: cacheDirectory) + resources.fontPalette = try FontPalette.loadCached(fromDirectory: cacheDirectory) + return resources + } + + /// Loads the resources in the given directory and gives them the specified namespace. + public static func load( + from directory: URL, + inNamespace namespace: String, + cacheDirectory: URL? + ) throws -> ResourcePack.Resources { + log.debug("Loading resources from '\(namespace)' namespace") + + var resources = Resources() + var loadedCachedResources = false + if let cacheDirectory = cacheDirectory, + FileManager.default.directoryExists(at: cacheDirectory) + { + do { + resources = try loadCached(from: cacheDirectory) + loadedCachedResources = true + } catch { + do { + try FileManager.default.removeItem(at: cacheDirectory.deletingLastPathComponent()) + } catch { + log.warning("Failed to delete invalid resource caches") + } + } + } + + let textureDirectory = directory.appendingPathComponent("textures") + let modelDirectory = directory.appendingPathComponent("models") + + // Load biome colors + let colorMapDirectory = textureDirectory.appendingPathComponent("colormap") + if FileManager.default.directoryExists(at: colorMapDirectory) { + log.debug("Loading biome colors") + resources.biomeColors = try BiomeColors(from: colorMapDirectory) + } + + // Load locales + let localeDirectory = directory.appendingPathComponent("lang") + if FileManager.default.directoryExists(at: localeDirectory) { + log.debug("Loading locales") + let contents = try FileManager.default.contentsOfDirectory( + at: localeDirectory, includingPropertiesForKeys: nil) + for file in contents where file.pathExtension == "json" { + let locale = try MinecraftLocale(localeFile: file) + resources.locales[file.deletingPathExtension().lastPathComponent] = locale + } + } + + // Load entity models + let entityModelDirectory = modelDirectory.appendingPathComponent("entity") + if FileManager.default.directoryExists(at: entityModelDirectory) { + log.debug("Loading entity models") + resources.entityModelPalette = try EntityModelPalette.load( + from: entityModelDirectory, + namespace: namespace + ) + } + + if !loadedCachedResources { + log.debug("Loading textures") + + // Load block textures + let blockTextureDirectory = textureDirectory.appendingPathComponent("block") + if FileManager.default.directoryExists(at: blockTextureDirectory) { + resources.blockTexturePalette = try TexturePalette.load( + from: blockTextureDirectory, + inNamespace: namespace, + withType: "block" + ) + } + + /// Load item textures + let itemTextureDirectory = textureDirectory.appendingPathComponent("item") + if FileManager.default.directoryExists(at: itemTextureDirectory) { + resources.itemTexturePalette = try TexturePalette.load( + from: itemTextureDirectory, + inNamespace: namespace, + withType: "item" + ) + } + + /// Load entity textures + // let entityTextureDirectory = textureDirectory.appendingPathComponent("entity") + // if FileManager.default.directoryExists(at: entityTextureDirectory) { + // resources.entityTexturePalette = try TexturePalette.load( + // from: entityTextureDirectory, + // inNamespace: namespace, + // withType: "entity" + // ) + // } + + // Load GUI textures + let guiTextureDirectory = textureDirectory.appendingPathComponent("gui") + if FileManager.default.directoryExists(at: guiTextureDirectory) { + resources.guiTexturePalette = try TexturePalette.load( + from: guiTextureDirectory, + inNamespace: namespace, + withType: "gui", + recursive: true, + isAnimated: false + ) + } + + // Load GUI textures + let environmentTextureDirectory = textureDirectory.appendingPathComponent("environment") + if FileManager.default.directoryExists(at: environmentTextureDirectory) { + resources.environmentTexturePalette = try TexturePalette.load( + from: environmentTextureDirectory, + inNamespace: namespace, + withType: "environment", + isAnimated: false + ) + } + + // Load block models + let blockModelDirectory = modelDirectory.appendingPathComponent("block") + if FileManager.default.directoryExists(at: blockModelDirectory) { + log.debug("Loading block models") + resources.blockModelPalette = try BlockModelPalette.load( + from: blockModelDirectory, + namespace: namespace, + blockTexturePalette: resources.blockTexturePalette + ) + } + + // Load item models + let itemModelDirectory = modelDirectory.appendingPathComponent("item") + if FileManager.default.directoryExists(at: itemModelDirectory) { + log.debug("Loading item models") + resources.itemModelPalette = try ItemModelPalette.load( + from: itemModelDirectory, + itemTexturePalette: resources.itemTexturePalette, + blockTexturePalette: resources.blockTexturePalette, + blockModelPalette: resources.blockModelPalette, + namespace: namespace + ) + } + + // Load fonts + let fontDirectory = directory.appendingPathComponent("font") + if FileManager.default.directoryExists(at: fontDirectory) { + log.debug("Loading fonts") + resources.fontPalette = try FontPalette.load( + from: fontDirectory, + namespaceDirectory: directory, + textureDirectory: textureDirectory + ) + } + } + + return resources + } } } diff --git a/Sources/Core/Sources/Util/Either.swift b/Sources/Core/Sources/Util/Either.swift new file mode 100644 index 00000000..09bdd110 --- /dev/null +++ b/Sources/Core/Sources/Util/Either.swift @@ -0,0 +1,27 @@ +import Foundation + +public enum Either { + case left(Left) + case right(Right) +} + +extension Either: Decodable where Left: Decodable, Right: Decodable { + public init(from decoder: Decoder) throws { + do { + self = .left(try Left(from: decoder)) + } catch { + self = .right(try Right(from: decoder)) + } + } +} + +extension Either: Encodable where Left: Encodable, Right: Encodable { + public func encode(to encoder: Encoder) throws { + switch self { + case let .left(value): + try value.encode(to: encoder) + case let .right(value): + try value.encode(to: encoder) + } + } +} From 3da91618dc28e2201f2302574edf3bc764734a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?furby=E2=84=A2?= Date: Wed, 5 Jun 2024 08:40:54 +0000 Subject: [PATCH 53/84] Migrate to Swift Bundler v2 and fix compilation errors on linux (#201) * migrate bundler toml to v2 * fix version/commit hash in short version string. * get things minimally working. * cleanup needless case & formatting. * Add TODO for account refreshing, formatting. --- Package.resolved | 9 ---- Sources/ClientGtk/DeltaClientApp.swift | 12 +++--- Sources/ClientGtk/GameView.swift | 20 +++++---- .../Settings/MicrosoftLoginView.swift | 14 +------ Sources/ClientGtk/Storage/ConfigManager.swift | 26 ++++++------ .../ClientGtk/Storage/StorageManager.swift | 2 +- Sources/Core/Package.resolved | 41 +++++-------------- 7 files changed, 45 insertions(+), 79 deletions(-) diff --git a/Package.resolved b/Package.resolved index 191d6eae..590f44fb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -244,15 +244,6 @@ "version": null } }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax.git", - "state": { - "branch": null, - "revision": "303e5c5c36d6a558407d364878df131c3546fad8", - "version": "510.0.2" - } - }, { "package": "SwiftCPUDetect", "repositoryURL": "https://github.com/JWhitmore1/SwiftCPUDetect", diff --git a/Sources/ClientGtk/DeltaClientApp.swift b/Sources/ClientGtk/DeltaClientApp.swift index dfddc0d3..59558d37 100644 --- a/Sources/ClientGtk/DeltaClientApp.swift +++ b/Sources/ClientGtk/DeltaClientApp.swift @@ -36,16 +36,16 @@ struct DeltaClientApp: App { // Download vanilla assets if they haven't already been downloaded if !StorageManager.directoryExists(at: assetsDirectory) { loading("Downloading assets") - try ResourcePack.downloadVanillaAssets(forVersion: Constants.versionString, to: assetsDirectory) { progress, message in - loading(message) - } + try ResourcePack.downloadVanillaAssets( + forVersion: Constants.versionString, + to: assetsDirectory, + progress: nil + ) } // Load registries loading("Loading registries") - try RegistryStore.populateShared(registryDirectory) { progress, message in - loading(message) - } + try RegistryStore.populateShared(registryDirectory, progress: nil) // Load resource pack and cache it if necessary loading("Loading resource pack") diff --git a/Sources/ClientGtk/GameView.swift b/Sources/ClientGtk/GameView.swift index cb108f92..6cba8662 100644 --- a/Sources/ClientGtk/GameView.swift +++ b/Sources/ClientGtk/GameView.swift @@ -22,16 +22,18 @@ class GameViewState: Observable { self.handleClientEvent(event) } - do { - // TODO: Use structured concurrency to get join server to wait until login is finished so that - // errors can be handled inline - if let account = ConfigManager.default.config.selectedAccount { - try client.joinServer(describedBy: server, with: account) - } else { - state = .error("Please select an account") + // TODO: Use structured concurrency to get join server to wait until login is finished so that + // errors can be handled inline + if let account = ConfigManager.default.config.selectedAccount { + Task { + do { + try await client.joinServer(describedBy: server, with: account) + } catch { + state = .error("Failed to join server: \(error.localizedDescription)") + } } - } catch { - state = .error("Failed to join server: \(error.localizedDescription)") + } else { + state = .error("Please select an account") } } diff --git a/Sources/ClientGtk/Settings/MicrosoftLoginView.swift b/Sources/ClientGtk/Settings/MicrosoftLoginView.swift index 68045049..64d5d95a 100644 --- a/Sources/ClientGtk/Settings/MicrosoftLoginView.swift +++ b/Sources/ClientGtk/Settings/MicrosoftLoginView.swift @@ -60,21 +60,11 @@ struct MicrosoftLoginView: View { let accessToken = try await MicrosoftAPI.getMicrosoftAccessToken(response.deviceCode) account = try await MicrosoftAPI.getMinecraftAccount(accessToken) } catch { - guard case let .failedToGetXSTSToken(MicrosoftAPIError.xstsAuthenticationFailed(xstsError)) = error as? MicrosoftAPIError else { + guard let msoftError = (error as? MicrosoftAPIError)?.errorDescription else { state.state = .error("Failed to authenticate Microsoft account: \(error)") return } - - // TODO: Add localized descriptions to all authentication related errors - // XSTS errors are the most common so they get nice user-friendly errors - switch xstsError.code { - case 2148916233: // No Xbox Live account - state.state = .error("This Microsoft account does not have an attached Xbox Live account (\(xstsError.redirect))") - case 2148916238: // Child account - state.state = .error("Child accounts must first be added to a family (\(xstsError.redirect))") - default: - state.state = .error("Failed to get XSTS token: \(error)") - } + state.state = .error("Failed to authenticate Microsoft account: \(msoftError)") return } diff --git a/Sources/ClientGtk/Storage/ConfigManager.swift b/Sources/ClientGtk/Storage/ConfigManager.swift index b732f2c7..d5541b6c 100644 --- a/Sources/ClientGtk/Storage/ConfigManager.swift +++ b/Sources/ClientGtk/Storage/ConfigManager.swift @@ -108,22 +108,24 @@ public final class ConfigManager { try? commitConfig() } + // TODO: This is not used anywhere, do we need to add support + // for refreshing accounts? /// Refreshes the currently selected account and returns it. /// - Returns: The currently selected account after refreshing it. - public func getRefreshedAccount() async throws -> Account { - guard let account = config.selectedAccount else { - throw ConfigError.noAccountSelected - } + // public func getRefreshedAccount() async throws -> Account { + // guard let account = config.selectedAccount else { + // throw ConfigError.noAccountSelected + // } - do { - try await config.accounts[account.id]?.refreshIfExpired(withClientToken: config.clientToken) - } catch { - throw ConfigError.accountRefreshFailed(error) - } + // do { + // try await config.accounts[account.id]?.refreshIfExpired(withClientToken: config.clientToken) + // } catch { + // throw ConfigError.accountRefreshFailed(error) + // } - try commitConfig() - return account - } + // try commitConfig() + // return account + // } /// Updates the config and writes it to the config file. /// - Parameter config: The config to write. diff --git a/Sources/ClientGtk/Storage/StorageManager.swift b/Sources/ClientGtk/Storage/StorageManager.swift index b90c6d74..b397e2c4 100644 --- a/Sources/ClientGtk/Storage/StorageManager.swift +++ b/Sources/ClientGtk/Storage/StorageManager.swift @@ -31,7 +31,7 @@ final class StorageManager { try? FileManager.default.removeItem(at: storageDirectory) try Self.createDirectory(at: storageDirectory) } catch { - DeltaClientApp.fatal("Failed to create storage directory") + // DeltaClientApp.fatal("Failed to create storage directory") } } } diff --git a/Sources/Core/Package.resolved b/Sources/Core/Package.resolved index 6d7cacc3..1c86d025 100644 --- a/Sources/Core/Package.resolved +++ b/Sources/Core/Package.resolved @@ -45,15 +45,6 @@ "version" : "1.7.2" } }, - { - "identity" : "dns", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Bouke/DNS.git", - "state" : { - "revision" : "78bbd1589890a90b202d11d5f9e1297050cf0eb2", - "version" : "1.2.0" - } - }, { "identity" : "ecs", "kind" : "remoteSourceControl", @@ -117,6 +108,15 @@ "version" : "4.0.1" } }, + { + "identity" : "swift-async-dns-resolver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-dns-resolver.git", + "state" : { + "revision" : "08c07ff31a745ee5e522ac10132fb4949834d925", + "version" : "0.4.0" + } + }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -210,28 +210,9 @@ { "identity" : "swift-png", "kind" : "remoteSourceControl", - "location" : "https://github.com/kelvin13/swift-png", + "location" : "https://github.com/stackotter/swift-png", "state" : { - "revision" : "075dfb248ae327822635370e9d4f94a5d3fe93b2", - "version" : "4.0.2" - } - }, - { - "identity" : "swift-resolver", - "kind" : "remoteSourceControl", - "location" : "https://github.com/seznam/swift-resolver", - "state" : { - "revision" : "cfb7d326bc4c89a48439303d758b375a8faae784", - "version" : "0.3.0" - } - }, - { - "identity" : "swift-unisocket", - "kind" : "remoteSourceControl", - "location" : "https://github.com/seznam/swift-unisocket", - "state" : { - "revision" : "1785e432fb8497265a38712cdb9584c429ca3f96", - "version" : "0.14.0" + "revision" : "b68a5662ef9887c8f375854720b3621f772bf8c5" } }, { From 6ed09419aab1c5194e3be197dfc6028a48571ffe Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 5 Jun 2024 19:23:45 +1000 Subject: [PATCH 54/84] Clean up DeltaClientGtk and update to newer SwiftCrossUI version (the latest non-5.9 version) --- Package.resolved | 695 ++++++++++-------- Package.swift | 53 +- Sources/ClientGtk/ChatView.swift | 4 +- Sources/ClientGtk/DeltaClientApp.swift | 52 +- Sources/ClientGtk/GameView.swift | 12 +- Sources/ClientGtk/ServerListView.swift | 10 +- .../Settings/AccountInspectionView.swift | 14 +- .../ClientGtk/Settings/AccountListView.swift | 4 +- .../Settings/MicrosoftLoginView.swift | 16 +- .../ClientGtk/Settings/OfflineLoginView.swift | 6 +- Sources/ClientGtk/Settings/SettingsView.swift | 11 +- Sources/ClientGtk/Storage/ConfigManager.swift | 59 +- .../ClientGtk/Storage/StorageManager.swift | 11 +- Sources/Core/Package.swift | 46 +- 14 files changed, 529 insertions(+), 464 deletions(-) diff --git a/Package.resolved b/Package.resolved index 590f44fb..95732fc6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,322 +1,377 @@ { - "object": { - "pins": [ - { - "package": "ASN1Parser", - "repositoryURL": "https://github.com/stackotter/ASN1Parser", - "state": { - "branch": "main", - "revision": "f92a5b26f5c92d38ae858a5a77048e9af82331a3", - "version": null - } - }, - { - "package": "async-http-client", - "repositoryURL": "https://github.com/swift-server/async-http-client.git", - "state": { - "branch": null, - "revision": "037b70291941fe43de668066eb6fb802c5e181d2", - "version": "1.1.1" - } - }, - { - "package": "BigInt", - "repositoryURL": "https://github.com/attaswift/BigInt.git", - "state": { - "branch": null, - "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version": "5.3.0" - } - }, - { - "package": "Socket", - "repositoryURL": "https://github.com/stackotter/BlueSocket.git", - "state": { - "branch": "master", - "revision": "31e92ab743a9ffc2a4a6e7e2f1043f5fe1d97e80", - "version": null - } - }, - { - "package": "CircuitBreaker", - "repositoryURL": "https://github.com/Kitura/CircuitBreaker.git", - "state": { - "branch": null, - "revision": "bd4255762e48cc3748a448d197f1297a4ba705f7", - "version": "5.1.0" - } - }, - { - "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", - "state": { - "branch": null, - "revision": "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version": "1.8.2" - } - }, - { - "package": "FirebladeECS", - "repositoryURL": "https://github.com/stackotter/ecs.git", - "state": { - "branch": "master", - "revision": "c7660bcd24e31ef2fc3457f56a2bf4a58c3ad6ee", - "version": null - } - }, - { - "package": "FirebladeMath", - "repositoryURL": "https://github.com/stackotter/fireblade-math.git", - "state": { - "branch": "matrix2x2", - "revision": "750239647673b07457bea80b39438a6db52198f1", - "version": null - } - }, - { - "package": "JJLISO8601DateFormatter", - "repositoryURL": "https://github.com/michaeleisel/JJLISO8601DateFormatter", - "state": { - "branch": null, - "revision": "ed1d996123688bade6e895aa49595f0d862900e7", - "version": "0.1.7" - } - }, - { - "package": "LoggerAPI", - "repositoryURL": "https://github.com/Kitura/LoggerAPI.git", - "state": { - "branch": null, - "revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", - "version": "1.9.200" - } - }, - { - "package": "OpenCombine", - "repositoryURL": "https://github.com/OpenCombine/OpenCombine.git", - "state": { - "branch": null, - "revision": "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", - "version": "0.14.0" - } - }, - { - "package": "Puppy", - "repositoryURL": "https://github.com/sushichop/Puppy", - "state": { - "branch": null, - "revision": "b5af02a72a5a1f92a68e6eceee19cac804067ad9", - "version": "0.7.0" - } - }, - { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow", - "state": { - "branch": null, - "revision": "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", - "version": "4.0.1" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser", - "state": { - "branch": null, - "revision": "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version": "1.4.0" - } - }, - { - "package": "swift-async-dns-resolver", - "repositoryURL": "https://github.com/apple/swift-async-dns-resolver.git", - "state": { - "branch": null, - "revision": "08c07ff31a745ee5e522ac10132fb4949834d925", - "version": "0.4.0" - } - }, - { - "package": "swift-atomics", - "repositoryURL": "https://github.com/apple/swift-atomics.git", - "state": { - "branch": null, - "revision": "cd142fd2f64be2100422d658e7411e39489da985", - "version": "1.2.0" - } - }, - { - "package": "swift-case-paths", - "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", - "state": { - "branch": null, - "revision": "8d712376c99fc0267aa0e41fea732babe365270a", - "version": "1.3.3" - } - }, - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections.git", - "state": { - "branch": null, - "revision": "9d8719c8bebdc79740b6969c912ac706eb721d7a", - "version": "0.0.7" - } - }, - { - "package": "swift-cross-ui", - "repositoryURL": "https://github.com/stackotter/swift-cross-ui", - "state": { - "branch": null, - "revision": "e4491a59449dec572c1d0d2c214f35825dbc34aa", - "version": null - } - }, - { - "package": "SwiftImage", - "repositoryURL": "https://github.com/stackotter/swift-image.git", - "state": { - "branch": "master", - "revision": "7160e2d8799f8b182498ab699aa695cb66cf4ea6", - "version": null - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version": "1.5.4" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "b4e0a274f7f34210e97e2f2c50ab02a10b549250", - "version": "2.41.1" - } - }, - { - "package": "swift-nio-extras", - "repositoryURL": "https://github.com/apple/swift-nio-extras.git", - "state": { - "branch": null, - "revision": "6c84d247754ad77487a6f0694273b89b83efd056", - "version": "1.14.0" - } - }, - { - "package": "swift-nio-ssl", - "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", - "state": { - "branch": null, - "revision": "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", - "version": "2.22.1" - } - }, - { - "package": "swift-package-zlib", - "repositoryURL": "https://github.com/fourplusone/swift-package-zlib", - "state": { - "branch": null, - "revision": "03ecd41814d8929362f7439529f9682536a8de13", - "version": "1.2.11" - } - }, - { - "package": "swift-parsing", - "repositoryURL": "https://github.com/pointfreeco/swift-parsing", - "state": { - "branch": null, - "revision": "a0e7d73f462c1c38c59dc40a3969ac40cea42950", - "version": "0.13.0" - } - }, - { - "package": "swift-png", - "repositoryURL": "https://github.com/stackotter/swift-png", - "state": { - "branch": null, - "revision": "b68a5662ef9887c8f375854720b3621f772bf8c5", - "version": null - } - }, - { - "package": "SwiftCPUDetect", - "repositoryURL": "https://github.com/JWhitmore1/SwiftCPUDetect", - "state": { - "branch": "main", - "revision": "5ca694c6ad7eef1199d69463fa956c24c202465f", - "version": null - } - }, - { - "package": "SwiftPackagesBase", - "repositoryURL": "https://github.com/ITzTravelInTime/SwiftPackagesBase", - "state": { - "branch": null, - "revision": "79477622b5dbacc3722d485c5060c46a90740016", - "version": "0.0.23" - } - }, - { - "package": "SwiftyRequest", - "repositoryURL": "https://github.com/Kitura/SwiftyRequest.git", - "state": { - "branch": null, - "revision": "2c543777a5088bed811503a68551a4b4eceac198", - "version": "3.2.200" - } - }, - { - "package": "SwordRPC", - "repositoryURL": "https://github.com/stackotter/SwordRPC", - "state": { - "branch": null, - "revision": "3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed", - "version": null - } - }, - { - "package": "xctest-dynamic-overlay", - "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", - "state": { - "branch": null, - "revision": "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version": "1.1.2" - } - }, - { - "package": "ZIPFoundation", - "repositoryURL": "https://github.com/weichsel/ZIPFoundation.git", - "state": { - "branch": null, - "revision": "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", - "version": "0.9.19" - } - }, - { - "package": "ZippyJSON", - "repositoryURL": "https://github.com/michaeleisel/ZippyJSON", - "state": { - "branch": null, - "revision": "c4ab804780b64979f19268619dfa563b6be58f7d", - "version": "1.2.10" - } - }, - { - "package": "ZippyJSONCFamily", - "repositoryURL": "https://github.com/michaeleisel/ZippyJSONCFamily", - "state": { - "branch": null, - "revision": "8abdd7a5e943afe68e7b03fdaa63b21c042a3893", - "version": "1.2.9" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "asn1parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/ASN1Parser", + "state" : { + "branch" : "main", + "revision" : "f92a5b26f5c92d38ae858a5a77048e9af82331a3" + } + }, + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "037b70291941fe43de668066eb6fb802c5e181d2", + "version" : "1.1.1" + } + }, + { + "identity" : "asyncextensions", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lhoward/AsyncExtensions", + "state" : { + "branch" : "linux", + "revision" : "0d96d3550ef94f83c2f300021f9985e4fb44f7af" + } + }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "bluesocket", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/BlueSocket.git", + "state" : { + "branch" : "master", + "revision" : "31e92ab743a9ffc2a4a6e7e2f1043f5fe1d97e80" + } + }, + { + "identity" : "circuitbreaker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/CircuitBreaker.git", + "state" : { + "revision" : "bd4255762e48cc3748a448d197f1297a4ba705f7", + "version" : "5.1.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift", + "state" : { + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" + } + }, + { + "identity" : "ecs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/ecs.git", + "state" : { + "branch" : "master", + "revision" : "c7660bcd24e31ef2fc3457f56a2bf4a58c3ad6ee" + } + }, + { + "identity" : "fireblade-math", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/fireblade-math.git", + "state" : { + "branch" : "matrix2x2", + "revision" : "750239647673b07457bea80b39438a6db52198f1" + } + }, + { + "identity" : "jjliso8601dateformatter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/michaeleisel/JJLISO8601DateFormatter", + "state" : { + "revision" : "ed1d996123688bade6e895aa49595f0d862900e7", + "version" : "0.1.7" + } + }, + { + "identity" : "loggerapi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/LoggerAPI.git", + "state" : { + "revision" : "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", + "version" : "1.9.200" + } + }, + { + "identity" : "lvglswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PADL/LVGLSwift", + "state" : { + "revision" : "19c19a942153b50d61486faf1d0d45daf79e7be5" + } + }, + { + "identity" : "opencombine", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OpenCombine/OpenCombine.git", + "state" : { + "revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version" : "0.14.0" + } + }, + { + "identity" : "puppy", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sushichop/Puppy", + "state" : { + "revision" : "b5af02a72a5a1f92a68e6eceee19cac804067ad9", + "version" : "0.7.0" + } + }, + { + "identity" : "qlift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Longhanks/qlift", + "state" : { + "revision" : "ddab1f1ecc113ad4f8e05d2999c2734cdf706210" + } + }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", + "version" : "4.0.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-async-dns-resolver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-dns-resolver.git", + "state" : { + "revision" : "08c07ff31a745ee5e522ac10132fb4949834d925", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" + } + }, + { + "identity" : "swift-cross-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-cross-ui", + "state" : { + "revision" : "21410866cb8241f322633bed83018acb74840e87" + } + }, + { + "identity" : "swift-image", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-image.git", + "state" : { + "branch" : "master", + "revision" : "7160e2d8799f8b182498ab699aa695cb66cf4ea6" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250", + "version" : "2.41.1" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "6c84d247754ad77487a6f0694273b89b83efd056", + "version" : "1.14.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", + "version" : "2.22.1" + } + }, + { + "identity" : "swift-package-zlib", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fourplusone/swift-package-zlib", + "state" : { + "revision" : "03ecd41814d8929362f7439529f9682536a8de13", + "version" : "1.2.11" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing", + "state" : { + "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", + "version" : "0.13.0" + } + }, + { + "identity" : "swift-png", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-png", + "state" : { + "revision" : "b68a5662ef9887c8f375854720b3621f772bf8c5" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" + } + }, + { + "identity" : "swiftcpudetect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JWhitmore1/SwiftCPUDetect", + "state" : { + "branch" : "main", + "revision" : "5ca694c6ad7eef1199d69463fa956c24c202465f" + } + }, + { + "identity" : "swiftpackagesbase", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ITzTravelInTime/SwiftPackagesBase", + "state" : { + "revision" : "79477622b5dbacc3722d485c5060c46a90740016", + "version" : "0.0.23" + } + }, + { + "identity" : "swiftterm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/SwiftTerm.git", + "state" : { + "revision" : "e2b431dbf73f775fb4807a33e4572ffd3dc6933a", + "version" : "1.2.5" + } + }, + { + "identity" : "swiftyrequest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/SwiftyRequest.git", + "state" : { + "revision" : "2c543777a5088bed811503a68551a4b4eceac198", + "version" : "3.2.200" + } + }, + { + "identity" : "swordrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/SwordRPC", + "state" : { + "revision" : "3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed" + } + }, + { + "identity" : "termkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/TermKit", + "state" : { + "revision" : "163afa64f1257a0c026cc83ed8bc47a5f8fc9704" + } + }, + { + "identity" : "textbufferkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/migueldeicaza/TextBufferKit.git", + "state" : { + "revision" : "7f3ed5b1d7288de34ad853544d802647be11cfcf", + "version" : "0.3.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", + "version" : "1.1.2" + } + }, + { + "identity" : "zipfoundation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/weichsel/ZIPFoundation.git", + "state" : { + "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", + "version" : "0.9.19" + } + }, + { + "identity" : "zippyjson", + "kind" : "remoteSourceControl", + "location" : "https://github.com/michaeleisel/ZippyJSON", + "state" : { + "revision" : "c4ab804780b64979f19268619dfa563b6be58f7d", + "version" : "1.2.10" + } + }, + { + "identity" : "zippyjsoncfamily", + "kind" : "remoteSourceControl", + "location" : "https://github.com/michaeleisel/ZippyJSONCFamily", + "state" : { + "revision" : "8abdd7a5e943afe68e7b03fdaa63b21c042a3893", + "version" : "1.2.9" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 9a5f7d2f..e4f816be 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.6 import PackageDescription @@ -20,14 +20,16 @@ var products: [Product] = [ .library( name: "StaticShim", targets: ["StaticShim"] - ) + ), ] #if canImport(Darwin) -products.append(.executable( - name: "DeltaClient", - targets: ["DeltaClient"] -)) + products.append( + .executable( + name: "DeltaClient", + targets: ["DeltaClient"] + ) + ) #endif var targets: [Target] = [ @@ -35,7 +37,8 @@ var targets: [Target] = [ name: "DeltaClientGtk", dependencies: [ .product(name: "DeltaCore", package: "DeltaCore"), - .product(name: "SwiftCrossUI", package: "swift-cross-ui") + .product(name: "SwiftCrossUI", package: "swift-cross-ui"), + .product(name: "GtkBackend", package: "swift-cross-ui"), ], path: "Sources/ClientGtk" ), @@ -43,7 +46,7 @@ var targets: [Target] = [ .target( name: "DynamicShim", dependencies: [ - .product(name: "DeltaCore", package: "DeltaCore"), + .product(name: "DeltaCore", package: "DeltaCore") ], path: "Sources/Exporters/DynamicShim" ), @@ -51,22 +54,24 @@ var targets: [Target] = [ .target( name: "StaticShim", dependencies: [ - .product(name: "StaticDeltaCore", package: "DeltaCore"), + .product(name: "StaticDeltaCore", package: "DeltaCore") ], path: "Sources/Exporters/StaticShim" - ) + ), ] #if canImport(Darwin) -targets.append(.executableTarget( - name: "DeltaClient", - dependencies: [ - "DynamicShim", - .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), - .product(name: "ArgumentParser", package: "swift-argument-parser") - ], - path: "Sources/Client" -)) + targets.append( + .executableTarget( + name: "DeltaClient", + dependencies: [ + "DynamicShim", + .product(name: "SwordRPC", package: "SwordRPC", condition: .when(platforms: [.macOS])), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources/Client" + ) + ) #endif let package = Package( @@ -78,8 +83,14 @@ let package = Package( // In short, the dependencies for DeltaCore can be found in Sources/Core/Package.swift .package(name: "DeltaCore", path: "Sources/Core"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), - .package(url: "https://github.com/stackotter/SwordRPC", .revision("3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed")), - .package(url: "https://github.com/stackotter/swift-cross-ui", revision: "e4491a59449dec572c1d0d2c214f35825dbc34aa") + .package( + url: "https://github.com/stackotter/SwordRPC", + revision: "3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed" + ), + .package( + url: "https://github.com/stackotter/swift-cross-ui", + revision: "21410866cb8241f322633bed83018acb74840e87" + ), ], targets: targets ) diff --git a/Sources/ClientGtk/ChatView.swift b/Sources/ClientGtk/ChatView.swift index cd99ae7c..62247b58 100644 --- a/Sources/ClientGtk/ChatView.swift +++ b/Sources/ClientGtk/ChatView.swift @@ -1,5 +1,5 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI class ChatViewState: Observable { @Observed var error: String? @@ -28,7 +28,7 @@ struct ChatView: View { } } - var body: some ViewContent { + var body: some View { if let error = state.error { Text(error) } diff --git a/Sources/ClientGtk/DeltaClientApp.swift b/Sources/ClientGtk/DeltaClientApp.swift index 59558d37..83b3b8d8 100644 --- a/Sources/ClientGtk/DeltaClientApp.swift +++ b/Sources/ClientGtk/DeltaClientApp.swift @@ -1,7 +1,8 @@ +import DeltaCore import Dispatch import Foundation +import GtkBackend import SwiftCrossUI -import DeltaCore @main struct DeltaClientApp: App { @@ -18,7 +19,6 @@ struct DeltaClientApp: App { } var identifier = "dev.stackotter.DeltaClientApp" - var windowProperties = WindowProperties(title: "Delta Client", defaultSize: .init(400, 200)) var state = StateStorage() @@ -27,7 +27,7 @@ struct DeltaClientApp: App { } func load() { - DispatchQueue(label: "loading").async { + DispatchQueue(label: "loading").asyncAfter(deadline: .now().advanced(by: .milliseconds(100))) { let assetsDirectory = URL(fileURLWithPath: "assets") let registryDirectory = URL(fileURLWithPath: "registry") let cacheDirectory = URL(fileURLWithPath: "cache") @@ -77,28 +77,32 @@ struct DeltaClientApp: App { self.state.state = .loading(message: message) } - var body: some ViewContent { - VStack { - switch state.state { - case .loading(let message): - Text(message) - case .selectServer: - ServerListView { server in - if let resourcePack = state.resourcePack { - state.state = .play(server, resourcePack) + var body: some Scene { + WindowGroup("Delta Client") { + VStack { + switch state.state { + case .loading(let message): + Text(message) + case .selectServer: + ServerListView { server in + if let resourcePack = state.resourcePack { + state.state = .play(server, resourcePack) + } + } openSettings: { + state.state = .settings } - } openSettings: { - state.state = .settings - } - case .settings: - SettingsView { - state.state = .selectServer - } - case .play(let server, let resourcePack): - GameView(server, resourcePack) { - state.state = .selectServer - } + case .settings: + SettingsView { + state.state = .selectServer + } + case .play(let server, let resourcePack): + GameView(server, resourcePack) { + state.state = .selectServer + } + } } - }.padding(10) + .padding(10) + } + .defaultSize(width: 400, height: 200) } } diff --git a/Sources/ClientGtk/GameView.swift b/Sources/ClientGtk/GameView.swift index 6cba8662..81b70634 100644 --- a/Sources/ClientGtk/GameView.swift +++ b/Sources/ClientGtk/GameView.swift @@ -1,6 +1,6 @@ +import DeltaCore import Dispatch import SwiftCrossUI -import DeltaCore class GameViewState: Observable { enum State { @@ -16,7 +16,8 @@ class GameViewState: Observable { init(_ server: ServerDescriptor, _ resourcePack: ResourcePack) { self.server = server - client = Client(resourcePack: resourcePack, configuration: ConfigManager.default.coreConfiguration) + client = Client( + resourcePack: resourcePack, configuration: ConfigManager.default.coreConfiguration) client.eventBus.registerHandler { [weak self] event in guard let self = self else { return } self.handleClientEvent(event) @@ -72,12 +73,15 @@ struct GameView: View { var completionHandler: () -> Void - init(_ server: ServerDescriptor, _ resourcePack: ResourcePack, _ completionHandler: @escaping () -> Void) { + init( + _ server: ServerDescriptor, _ resourcePack: ResourcePack, + _ completionHandler: @escaping () -> Void + ) { state = GameViewState(server, resourcePack) self.completionHandler = completionHandler } - var body: some ViewContent { + var body: some View { switch state.state { case .error(let message): Text(message) diff --git a/Sources/ClientGtk/ServerListView.swift b/Sources/ClientGtk/ServerListView.swift index d1a6ebda..6d2d5c54 100644 --- a/Sources/ClientGtk/ServerListView.swift +++ b/Sources/ClientGtk/ServerListView.swift @@ -1,5 +1,5 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI indirect enum DetailState { case server(_ index: Int) @@ -23,7 +23,7 @@ struct ServerListView: View { var state = ServerListViewState() - var body: some ViewContent { + var body: some View { NavigationSplitView { VStack { Button("Add server") { @@ -56,7 +56,7 @@ struct ServerListView: View { editingView(index) case .error(let message, let returnState): Text("Error: \(message)") - Button ("Back") { + Button("Back") { state.detailState = returnState } } @@ -89,7 +89,7 @@ struct ServerListView: View { Text("Add server") TextField("Name", state.$name) TextField("Address", state.$address) - + Button("Add") { do { let (host, port) = try Self.parseAddress(state.address) @@ -136,7 +136,7 @@ struct ServerListView: View { state.name = "" state.address = "" - if (state.servers.count > 0) { + if state.servers.count > 0 { state.detailState = .server(0) } else { state.detailState = .adding diff --git a/Sources/ClientGtk/Settings/AccountInspectionView.swift b/Sources/ClientGtk/Settings/AccountInspectionView.swift index 12202dfc..baa815bd 100644 --- a/Sources/ClientGtk/Settings/AccountInspectionView.swift +++ b/Sources/ClientGtk/Settings/AccountInspectionView.swift @@ -1,23 +1,25 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI struct AccountInspectorView: View { var account: Account var selectAccount: () -> Void var removeAccount: () -> Void - - public init(account: Account, selectAccount: @escaping () -> Void, removeAccount: @escaping () -> Void) { + + public init( + account: Account, selectAccount: @escaping () -> Void, removeAccount: @escaping () -> Void + ) { self.account = account self.selectAccount = selectAccount self.removeAccount = removeAccount } - - var body: some ViewContent { + + var body: some View { VStack { Text("Username: \(account.username)") Text("Type: \(account.type)") - + Button("Select account") { selectAccount() } diff --git a/Sources/ClientGtk/Settings/AccountListView.swift b/Sources/ClientGtk/Settings/AccountListView.swift index 920fb714..a034aed9 100644 --- a/Sources/ClientGtk/Settings/AccountListView.swift +++ b/Sources/ClientGtk/Settings/AccountListView.swift @@ -1,12 +1,12 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI struct AccountListView: View { var inspectAccount: (Account) -> Void var offlineLogin: () -> Void var microsoftLogin: () -> Void - var body: some ViewContent { + var body: some View { VStack { Button("Add offline account") { offlineLogin() diff --git a/Sources/ClientGtk/Settings/MicrosoftLoginView.swift b/Sources/ClientGtk/Settings/MicrosoftLoginView.swift index 64d5d95a..6750bbcd 100644 --- a/Sources/ClientGtk/Settings/MicrosoftLoginView.swift +++ b/Sources/ClientGtk/Settings/MicrosoftLoginView.swift @@ -1,5 +1,5 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI enum MicrosoftState { case authorizingDevice @@ -22,8 +22,8 @@ struct MicrosoftLoginView: View { self.completionHandler = completionHandler authorizeDevice() } - - var body: some ViewContent { + + var body: some View { VStack { switch state.state { case .authorizingDevice: @@ -38,7 +38,7 @@ struct MicrosoftLoginView: View { Text("Authenticating...") case .error(let message): Text(message) - } + } } } @@ -60,11 +60,9 @@ struct MicrosoftLoginView: View { let accessToken = try await MicrosoftAPI.getMicrosoftAccessToken(response.deviceCode) account = try await MicrosoftAPI.getMinecraftAccount(accessToken) } catch { - guard let msoftError = (error as? MicrosoftAPIError)?.errorDescription else { - state.state = .error("Failed to authenticate Microsoft account: \(error)") - return - } - state.state = .error("Failed to authenticate Microsoft account: \(msoftError)") + state.state = .error( + "Failed to authenticate Microsoft account: \(error.localizedDescription)" + ) return } diff --git a/Sources/ClientGtk/Settings/OfflineLoginView.swift b/Sources/ClientGtk/Settings/OfflineLoginView.swift index 77dcfdea..795d906f 100644 --- a/Sources/ClientGtk/Settings/OfflineLoginView.swift +++ b/Sources/ClientGtk/Settings/OfflineLoginView.swift @@ -1,5 +1,5 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI class OfflineLoginViewState: Observable { @Observed var username = "" @@ -10,8 +10,8 @@ struct OfflineLoginView: View { var completionHandler: (Account) -> Void var state = OfflineLoginViewState() - - var body: some ViewContent { + + var body: some View { VStack { TextField("Username", state.$username) diff --git a/Sources/ClientGtk/Settings/SettingsView.swift b/Sources/ClientGtk/Settings/SettingsView.swift index 773727a8..31cd79fa 100644 --- a/Sources/ClientGtk/Settings/SettingsView.swift +++ b/Sources/ClientGtk/Settings/SettingsView.swift @@ -1,5 +1,5 @@ -import SwiftCrossUI import DeltaCore +import SwiftCrossUI enum SettingsPage { case accountList @@ -16,8 +16,8 @@ struct SettingsView: View { var returnToServerList: () -> Void var state = SettingsViewState() - - var body: some ViewContent { + + var body: some View { NavigationSplitView { VStack { Button("Accounts") { @@ -32,7 +32,7 @@ struct SettingsView: View { VStack { switch state.page { case .accountList: - AccountListView() { account in + AccountListView { account in state.page = .inspectAccount(account) } offlineLogin: { state.page = .offlineLogin @@ -49,7 +49,8 @@ struct SettingsView: View { if ConfigManager.default.config.selectedAccountId == account.id { ConfigManager.default.setAccounts(Array(accounts.values), selected: nil) } else { - ConfigManager.default.setAccounts(Array(accounts.values), selected: ConfigManager.default.config.selectedAccountId) + ConfigManager.default.setAccounts( + Array(accounts.values), selected: ConfigManager.default.config.selectedAccountId) } state.page = .accountList } diff --git a/Sources/ClientGtk/Storage/ConfigManager.swift b/Sources/ClientGtk/Storage/ConfigManager.swift index d5541b6c..b3deb6de 100644 --- a/Sources/ClientGtk/Storage/ConfigManager.swift +++ b/Sources/ClientGtk/Storage/ConfigManager.swift @@ -1,10 +1,12 @@ -import Foundation import DeltaCore +import Foundation /// Manages the config stored in a config file. public final class ConfigManager { /// The manager for the default config file. - public static var `default` = ConfigManager(for: StorageManager.default.absoluteFromRelative("config.json")) + public static var `default` = ConfigManager( + for: StorageManager.default.absoluteFromRelative("config.json") + ) /// The current config (thread-safe). public private(set) var config: Config { @@ -22,7 +24,7 @@ public final class ConfigManager { /// The implementation of ClientConfiguration that allows DeltaCore to access required config values. let coreConfiguration: CoreConfiguration - + /// The non-threadsafe storage for ``config``. private var _config: Config /// The file to store config in. @@ -47,7 +49,7 @@ public final class ConfigManager { FileManager.default.createFile(atPath: configFile.path, contents: data, attributes: nil) } catch { // TODO: Proper error handling for ConfigManager - // DeltaClientApp.fatal("Failed to encode config: \(error)") + fatalError("Failed to encode config: \(error)") } return } @@ -66,33 +68,33 @@ public final class ConfigManager { data = try JSONEncoder().encode(_config) FileManager.default.createFile(atPath: configFile.path, contents: data, attributes: nil) } catch { - // DeltaClientApp.fatal("Failed to encode config: \(error)") + fatalError("Failed to encode config: \(error)") } } coreConfiguration = CoreConfiguration(_config) } - + /// Commits the given account to the config file. /// - Parameters: /// - account: The account to add. /// - shouldSelect: Whether to select the account or not. public func addAccount(_ account: Account, shouldSelect: Bool = false) { config.accounts[account.id] = account - + if shouldSelect { config.selectedAccountId = account.id } - + try? commitConfig() } - + /// Selects the given account. /// - Parameter id: The id of the account as received from the authentication servers (or generated from the username if offline). public func selectAccount(_ id: String?) { config.selectedAccountId = id try? commitConfig() } - + /// Commits the given array of user accounts to the config file replacing any existing accounts. /// - Parameters: /// - accounts: The user's accounts. @@ -102,44 +104,25 @@ public final class ConfigManager { for account in accounts { config.accounts[account.id] = account } - + config.selectedAccountId = selected - + try? commitConfig() } - - // TODO: This is not used anywhere, do we need to add support - // for refreshing accounts? - /// Refreshes the currently selected account and returns it. - /// - Returns: The currently selected account after refreshing it. - // public func getRefreshedAccount() async throws -> Account { - // guard let account = config.selectedAccount else { - // throw ConfigError.noAccountSelected - // } - - // do { - // try await config.accounts[account.id]?.refreshIfExpired(withClientToken: config.clientToken) - // } catch { - // throw ConfigError.accountRefreshFailed(error) - // } - - // try commitConfig() - // return account - // } /// Updates the config and writes it to the config file. /// - Parameter config: The config to write. /// - Parameter saveToFile: Whether to write to the file or just update internal references. public func setConfig(to config: Config, saveToFile: Bool = true) { self.config = config - + do { try commitConfig(saveToFile: saveToFile) } catch { log.error("Failed to write config to file: \(error)") } } - + /// Resets the configuration to defaults. public func resetConfig() throws { log.info("Resetting config.json") @@ -165,24 +148,24 @@ public final class ConfigManager { /// This is passed to the Client constructor. class CoreConfiguration: ClientConfiguration { var config: Config - + init(_ config: Config) { self.config = config } public var render: RenderConfiguration { - get { return config.render } + return config.render } public var keymap: Keymap { - get { return config.keymap } + return config.keymap } public var toggleSprint: Bool { - get { return config.toggleSprint } + return config.toggleSprint } public var toggleSneak: Bool { - get { return config.toggleSneak } + return config.toggleSneak } } diff --git a/Sources/ClientGtk/Storage/StorageManager.swift b/Sources/ClientGtk/Storage/StorageManager.swift index b397e2c4..539bf85e 100644 --- a/Sources/ClientGtk/Storage/StorageManager.swift +++ b/Sources/ClientGtk/Storage/StorageManager.swift @@ -11,11 +11,14 @@ final class StorageManager { public var cacheDirectory: URL private init() { - if let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { + if let applicationSupport = FileManager.default.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ).first { storageDirectory = applicationSupport.appendingPathComponent("delta-client") } else { log.warning("Failed to get application support directory, using temporary directory instead") - let fallback = FileManager.default.temporaryDirectory.appendingPathComponent("delta-client.fallback") + let fallback = FileManager.default.temporaryDirectory.appendingPathComponent( + "delta-client.fallback") storageDirectory = fallback } @@ -25,13 +28,13 @@ final class StorageManager { log.trace("Using \(storageDirectory.path) as storage directory") - if (!Self.directoryExists(at: storageDirectory)) { + if !Self.directoryExists(at: storageDirectory) { do { log.info("Creating storage directory") try? FileManager.default.removeItem(at: storageDirectory) try Self.createDirectory(at: storageDirectory) } catch { - // DeltaClientApp.fatal("Failed to create storage directory") + fatalError("Failed to create storage directory") } } } diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 3c88bc35..24b614da 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -8,7 +8,7 @@ let debugLocks = false var productTargets = ["DeltaCore", "DeltaLogger"] #if canImport(Metal) -productTargets.append("DeltaRenderer") + productTargets.append("DeltaRenderer") #endif // MARK: Targets @@ -23,10 +23,12 @@ var targets: [Target] = [ "CryptoSwift", "SwiftCPUDetect", .product(name: "FirebladeECS", package: "ecs"), - .product(name: "SwiftyRequest", package: "SwiftyRequest", condition: .when(platforms: [.linux])), + .product( + name: "SwiftyRequest", package: "SwiftyRequest", condition: .when(platforms: [.linux])), .product(name: "OpenCombine", package: "OpenCombine", condition: .when(platforms: [.linux])), .product(name: "Atomics", package: "swift-atomics"), - .product(name: "ZippyJSON", package: "ZippyJSON", condition: .when(platforms: [.macOS, .iOS, .tvOS])), + .product( + name: "ZippyJSON", package: "ZippyJSON", condition: .when(platforms: [.macOS, .iOS, .tvOS])), .product(name: "Parsing", package: "swift-parsing"), .product(name: "Collections", package: "swift-collections"), .product(name: "OrderedCollections", package: "swift-collections"), @@ -34,7 +36,7 @@ var targets: [Target] = [ .product(name: "AsyncDNSResolver", package: "swift-async-dns-resolver"), .product(name: "Z", package: "swift-package-zlib"), .product(name: "SwiftImage", package: "swift-image"), - .product(name: "PNG", package: "swift-png") + .product(name: "PNG", package: "swift-png"), ], path: "Sources", swiftSettings: debugLocks ? [.define("DEBUG_LOCKS")] : [] @@ -44,7 +46,7 @@ var targets: [Target] = [ name: "DeltaLogger", dependencies: [ "Puppy", - "Rainbow" + "Rainbow", ], path: "Logger" ), @@ -52,22 +54,22 @@ var targets: [Target] = [ .testTarget( name: "DeltaCoreUnitTests", dependencies: ["DeltaCore"] - ) + ), ] #if canImport(Metal) -targets.append( - .target( - name: "DeltaRenderer", - dependencies: [ - "DeltaCore" - ], - path: "Renderer", - resources: [ - .process("Shader/") - ] + targets.append( + .target( + name: "DeltaRenderer", + dependencies: [ + "DeltaCore" + ], + path: "Renderer", + resources: [ + .process("Shader/") + ] + ) ) -) #endif let package = Package( @@ -75,11 +77,11 @@ let package = Package( platforms: [.macOS(.v11)], products: [ .library(name: "DeltaCore", type: .dynamic, targets: productTargets), - .library(name: "StaticDeltaCore", type: .static, targets: productTargets) + .library(name: "StaticDeltaCore", type: .static, targets: productTargets), ], dependencies: [ .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0"), - .package(url: "https://github.com/apple/swift-collections.git", from: "0.0.7"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package(url: "https://github.com/stackotter/ecs.git", branch: "master"), .package(url: "https://github.com/michaeleisel/ZippyJSON", from: "1.2.4"), @@ -89,13 +91,15 @@ let package = Package( .package(url: "https://github.com/fourplusone/swift-package-zlib", from: "1.2.11"), .package(url: "https://github.com/stackotter/swift-image.git", branch: "master"), .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), - .package(url: "https://github.com/stackotter/swift-png", revision: "b68a5662ef9887c8f375854720b3621f772bf8c5"), + .package( + url: "https://github.com/stackotter/swift-png", + revision: "b68a5662ef9887c8f375854720b3621f772bf8c5"), .package(url: "https://github.com/stackotter/ASN1Parser", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.6.0"), .package(url: "https://github.com/Kitura/SwiftyRequest.git", from: "3.1.0"), .package(url: "https://github.com/JWhitmore1/SwiftCPUDetect", branch: "main"), .package(url: "https://github.com/sushichop/Puppy", from: "0.6.0"), - .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.1") + .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.1"), ], targets: targets ) From ac6fc4b6e33279dd744285e6087b8e0b74baed1c Mon Sep 17 00:00:00 2001 From: Plasma Date: Wed, 5 Jun 2024 05:39:45 -0400 Subject: [PATCH 55/84] Implement interfaces of various GUI windows (double chests, anvil, etc) and fix NBT/missing packet crashes (#196) * Implement double chests * Implement Generic_9x4 and Generic_9x5 * Implement generic_9x1 * Fix generic_9x1's positioning and implement generic_9x2 * Refactor names, fix some formatting. * Remove forgotten comment * Fix NBT related crash * Add missing packet * Prepare for other interfaces * Implement anvil ui * Implement dropper & beacon * Implement furnace, blast furnace, smoker * Implement beacon --------- Co-authored-by: stackotter --- .../Sources/Datatypes/NBT/NBTCompound.swift | 2 +- Sources/Core/Sources/GUI/GUISprite.swift | 46 ++- Sources/Core/Sources/GUI/WindowType.swift | 380 +++++++++++++++++- .../Network/Protocol/PacketRegistry.swift | 3 +- .../Resources/GUI/GUITextureSlice.swift | 80 ++++ .../Sources/World/Dimension/Dimension.swift | 18 +- 6 files changed, 508 insertions(+), 21 deletions(-) diff --git a/Sources/Core/Sources/Datatypes/NBT/NBTCompound.swift b/Sources/Core/Sources/Datatypes/NBT/NBTCompound.swift index da9b9c9e..346c5dab 100644 --- a/Sources/Core/Sources/Datatypes/NBT/NBTCompound.swift +++ b/Sources/Core/Sources/Datatypes/NBT/NBTCompound.swift @@ -138,7 +138,7 @@ extension NBT { case .end: break case .byte: - value = try buffer.readByte() + value = try buffer.readSignedByte() case .short: value = try buffer.readSignedShort(endianness: .big) case .int: diff --git a/Sources/Core/Sources/GUI/GUISprite.swift b/Sources/Core/Sources/GUI/GUISprite.swift index b7877a2e..0ecceb31 100644 --- a/Sources/Core/Sources/GUI/GUISprite.swift +++ b/Sources/Core/Sources/GUI/GUISprite.swift @@ -16,11 +16,23 @@ public enum GUISprite { case xpBarForeground case inventory case craftingTable - /// If positioned directly above ``GUISprite/singleChestBottomHalf`` it forms + case furnace + case blastFurnace + case smoker + case anvil + case dispenser + case beacon + + /// If positioned directly above ``GUISprite/genericInventory`` it forms /// the background for a single chest window. The way the texture is made forces /// these to be separate sprites. - case singleChestTopHalf - case singleChestBottomHalf + case genericInventory // Inventory for most interfaces, its a part of the sprite + case generic9x1 + case generic9x2 + case generic9x3 + case generic9x4 + case generic9x5 + case generic9x6 case pinkBossBarBackground case pinkBossBarForeground @@ -81,10 +93,32 @@ public enum GUISprite { return GUISpriteDescriptor(slice: .inventory, position: [0, 0], size: [176, 166]) case .craftingTable: return GUISpriteDescriptor(slice: .craftingTable, position: [0, 0], size: [176, 166]) - case .singleChestTopHalf: - return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 71]) - case .singleChestBottomHalf: + case .furnace: + return GUISpriteDescriptor(slice: .furnace, position: [0, 0], size: [176, 166]) + case .blastFurnace: + return GUISpriteDescriptor(slice: .blastFurnace, position: [0, 0], size: [176, 166]) + case .smoker: + return GUISpriteDescriptor(slice: .smoker, position: [0, 0], size: [176, 166]) + case .anvil: + return GUISpriteDescriptor(slice: .anvil, position: [0, 0], size: [176, 166]) + case .dispenser: + return GUISpriteDescriptor(slice: .dispenser, position: [0, 0], size: [176, 166]) + case .beacon: + return GUISpriteDescriptor(slice: .beacon, position: [0,0], size: [229, 218]) + case .genericInventory: return GUISpriteDescriptor(slice: .genericContainer, position: [0, 125], size: [176, 97]) + case .generic9x1: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 35]) + case .generic9x2: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 53]) + case .generic9x3: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 71]) + case .generic9x4: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 89]) + case .generic9x5: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 107]) + case .generic9x6: + return GUISpriteDescriptor(slice: .genericContainer, position: [0, 0], size: [176, 222]) case .pinkBossBarBackground: return GUISpriteDescriptor(slice: .bars, position: [0, 0], size: [182, 5]) case .pinkBossBarForeground: diff --git a/Sources/Core/Sources/GUI/WindowType.swift b/Sources/Core/Sources/GUI/WindowType.swift index 8781cf67..58cc71b9 100644 --- a/Sources/Core/Sources/GUI/WindowType.swift +++ b/Sources/Core/Sources/GUI/WindowType.swift @@ -61,12 +61,284 @@ public struct WindowType { ] ) - public static let chest = WindowType( + public static let anvil = WindowType( + id: .vanilla(7), + identifier: Identifier(namespace: "minecraft", name: "crafting"), + background: .sprite(.anvil), + slotCount: 39, + areas:[ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(27, 47) + ), + WindowArea( + startIndex: 1, + width: 1, + height: 1, + position: Vec2i(76, 47) + ), + WindowArea( + startIndex: 2, + width: 1, + height: 1, + position: Vec2i(134, 47) + ), + WindowArea( + startIndex: 3, + width: 9, + height: 3, + position: Vec2i(8, 84) + ), + WindowArea( + startIndex: 30, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + ] + ) + + public static let furnace = WindowType( + id: .vanilla(13), + identifier: Identifier(namespace: "minecraft", name: "furnace"), + background: .sprite(.furnace), + slotCount: 39, + areas: [ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(56, 17) + ), + WindowArea( + startIndex: 1, + width: 1, + height: 1, + position: Vec2i(56, 53) + ), + WindowArea( + startIndex: 2, + width: 1, + height: 1, + position: Vec2i(112, 31) + ), + WindowArea( + startIndex: 3, + width: 9, + height: 3, + position: Vec2i(8, 84) + ), + WindowArea( + startIndex: 30, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + ] + ) + + public static let blastFurnace = WindowType( + id: .vanilla(9), + identifier: Identifier(namespace: "minecraft", name: "blast_furnace"), + background: .sprite(.blastFurnace), + slotCount: 39, + areas: [ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(56, 17) + ), + WindowArea( + startIndex: 1, + width: 1, + height: 1, + position: Vec2i(56, 53) + ), + WindowArea( + startIndex: 2, + width: 1, + height: 1, + position: Vec2i(112, 31) + ), + WindowArea( + startIndex: 3, + width: 9, + height: 3, + position: Vec2i(8, 84) + ), + WindowArea( + startIndex: 30, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + ] + ) + + public static let smoker = WindowType( + id: .vanilla(21), + identifier: Identifier(namespace: "minecraft", name: "smoker"), + background: .sprite(.smoker), + slotCount: 39, + areas: [ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(56, 17) + ), + WindowArea( + startIndex: 1, + width: 1, + height: 1, + position: Vec2i(56, 53) + ), + WindowArea( + startIndex: 2, + width: 1, + height: 1, + position: Vec2i(112, 31) + ), + WindowArea( + startIndex: 3, + width: 9, + height: 3, + position: Vec2i(8, 84) + ), + WindowArea( + startIndex: 30, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + ] + ) + + public static let beacon = WindowType( + id: .vanilla(8), + identifier: Identifier(namespace: "minecraft", name: "beacon"), + background: .sprite(.beacon), + slotCount: 37, + areas: [ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(136, 110) + ), + WindowArea( + startIndex: 1, + width: 9, + height: 3, + position: Vec2i(36, 137) + ), + WindowArea( + startIndex: 28, + width: 9, + height: 1, + position: Vec2i(36, 196) + ) + ] + ) + + // Dispenser & dropper + public static let generic3x3 = WindowType( + id: .vanilla(6), + identifier: Identifier(namespace: "minecraft", name: "generic_3x3"), + background: .sprite(.dispenser), + slotCount: 45, + areas: [ + WindowArea( + startIndex: 0, + width: 3, + height: 3, + position: Vec2i(62, 17) + ), + WindowArea( + startIndex: 9, + width: 9, + height: 3, + position: Vec2i(8, 84) + ), + WindowArea( + startIndex: 36, + width: 9, + height: 1, + position: Vec2i(8, 142) + ) + ] + ) + + // Generic window types + public static let generic9x1 = WindowType( + id: .vanilla(0), + identifier: Identifier(namespace: "minecraft", name: "generic_9x1"), + background: GUIElement.list(spacing: 0) { + GUIElement.sprite(.generic9x1) + GUIElement.sprite(.genericInventory) + }, + slotCount: 45, + areas: [ + WindowArea( + startIndex: 0, + width: 9, + height: 1, + position: Vec2i(8, 18) + ), + WindowArea( + startIndex: 9, + width: 9, + height: 3, + position: Vec2i(8, 50) + ), + WindowArea( + startIndex: 36, + width: 9, + height: 1, + position: Vec2i(8, 108) + ) + ] + ) + + public static let generic9x2 = WindowType( + id: .vanilla(1), + identifier: Identifier(namespace: "minecraft", name: "generic_9x2"), + background: GUIElement.list(spacing: 0) { + GUIElement.sprite(.generic9x2) + GUIElement.sprite(.genericInventory) + }, + slotCount: 54, + areas: [ + WindowArea( + startIndex: 0, + width: 9, + height: 2, + position: Vec2i(8, 18) + ), + WindowArea( + startIndex: 18, + width: 9, + height: 3, + position: Vec2i(8, 68) + ), + WindowArea( + startIndex: 45, + width: 9, + height: 1, + position: Vec2i(8, 126) + ) + ] + ) + + public static let generic9x3 = WindowType( id: .vanilla(2), identifier: Identifier(namespace: "minecraft", name: "generic_9x3"), background: GUIElement.list(spacing: 0) { - GUIElement.sprite(.singleChestTopHalf) - GUIElement.sprite(.singleChestBottomHalf) + GUIElement.sprite(.generic9x3) + GUIElement.sprite(.genericInventory) }, slotCount: 63, areas: [ @@ -91,12 +363,112 @@ public struct WindowType { ] ) + public static let generic9x4 = WindowType( + id: .vanilla(3), + identifier: Identifier(namespace: "minecraft", name: "generic_9x4"), + background: GUIElement.list(spacing: 0) { + GUIElement.sprite(.generic9x4) + GUIElement.sprite(.genericInventory) + }, + slotCount: 72, + areas: [ + WindowArea( + startIndex: 0, + width: 9, + height: 4, + position: Vec2i(8, 18) + ), + WindowArea( + startIndex: 36, + width: 9, + height: 3, + position: Vec2i(8, 104) + ), + WindowArea( + startIndex: 63, + width: 9, + height: 1, + position: Vec2i(8, 162) + ) + ] + ) + + public static let generic9x5 = WindowType( + id: .vanilla(4), + identifier: Identifier(namespace: "minecraft", name:"generic_9x5"), + background: GUIElement.list(spacing: 0) { + GUIElement.sprite(.generic9x5) + GUIElement.sprite(.genericInventory) + }, + slotCount: 81, + areas: [ + WindowArea( + startIndex: 0, + width: 9, + height: 5, + position: Vec2i(8, 18) + ), + WindowArea( + startIndex: 45, + width: 9, + height: 3, + position: Vec2i(8, 122) + ), + WindowArea( + startIndex: 72, + width: 9, + height: 1, + position: Vec2i(8, 180) + ) + ] + ) + + public static let generic9x6 = WindowType( + id: .vanilla(5), + identifier: Identifier(namespace: "minecraft", name: "generic_9x6"), + background: GUIElement.list(spacing: 0) { + GUIElement.sprite(.generic9x6) + }, + slotCount: 90, + areas: [ + WindowArea( + startIndex: 0, + width: 9, + height: 6, + position: Vec2i(8, 18) + ), + WindowArea( + startIndex: 54, + width: 9, + height: 3, + position: Vec2i(8, 140) + ), + WindowArea( + startIndex: 81, + width: 9, + height: 1, + position: Vec2i(8, 198) + ), + ] + ) + /// The window types understood by vanilla. public static let types = [Id: Self]( values: [ inventory, craftingTable, - chest + anvil, + furnace, + blastFurnace, + smoker, + beacon, + generic9x1, + generic9x2, + generic9x3, + generic9x4, + generic9x5, + generic9x6, + generic3x3 ], keyedBy: \.id ) diff --git a/Sources/Core/Sources/Network/Protocol/PacketRegistry.swift b/Sources/Core/Sources/Network/Protocol/PacketRegistry.swift index 3e59046d..f8ed63cd 100644 --- a/Sources/Core/Sources/Network/Protocol/PacketRegistry.swift +++ b/Sources/Core/Sources/Network/Protocol/PacketRegistry.swift @@ -126,7 +126,8 @@ public struct PacketRegistry { EntityAttributesPacket.self, EntityEffectPacket.self, DeclareRecipesPacket.self, - TagsPacket.self + TagsPacket.self, + WindowPropertyPacket.self ], toState: .play) return registry } diff --git a/Sources/Core/Sources/Resources/GUI/GUITextureSlice.swift b/Sources/Core/Sources/Resources/GUI/GUITextureSlice.swift index 028c209b..f8a1bfc7 100644 --- a/Sources/Core/Sources/Resources/GUI/GUITextureSlice.swift +++ b/Sources/Core/Sources/Resources/GUI/GUITextureSlice.swift @@ -6,6 +6,22 @@ public enum GUITextureSlice: Int, CaseIterable { case inventory case craftingTable case genericContainer + case dispenser // also covers dropper + case anvil + case beacon + case blastFurnace + case brewingStand + case enchantingTable + case furnace + case grindstone + case hopper + case loom + case merchant // Villagers & wandering traders + case shulkerBox + case smithingTable + case smoker + case cartographyTable + case stonecutter /// The path of the slice's underlying texture in a resource pack's textures directory. public var path: String { @@ -22,6 +38,38 @@ public enum GUITextureSlice: Int, CaseIterable { return "container/crafting_table.png" case .genericContainer: return "container/generic_54.png" + case .dispenser: + return "container/dispenser.png" + case .anvil: + return "container/anvil.png" + case .beacon: + return "container/beacon.png" + case .blastFurnace: + return "container/blast_furnace.png" + case .brewingStand: + return "container/brewing_stand.png" + case .enchantingTable: + return "container/enchanting_table.png" + case .furnace: + return "container/furnace.png" + case .grindstone: + return "container/grindstone.png" + case .hopper: + return "container/hopper.png" + case .loom: + return "container/loom.png" + case .merchant: + return "container/villager2.png" + case .shulkerBox: + return "container/shulker_box.png" + case .smithingTable: + return "container/smithing.png" + case .smoker: + return "container/smoker.png" + case .cartographyTable: + return "container/cartography_table.png" + case .stonecutter: + return "container/stonecutter.png" } } @@ -40,6 +88,38 @@ public enum GUITextureSlice: Int, CaseIterable { return Identifier(name: "gui/container/crafting_table") case .genericContainer: return Identifier(name: "gui/container/generic_54") + case .dispenser: + return Identifier(name: "gui/container/dispenser") + case .anvil: + return Identifier(name: "gui/container/anvil") + case .beacon: + return Identifier(name: "gui/container/beacon") + case .blastFurnace: + return Identifier(name: "gui/container/blast_furnace") + case .brewingStand: + return Identifier(name: "gui/container/brewing_stand") + case .enchantingTable: + return Identifier(name: "gui/container/enchanting_table") + case .furnace: + return Identifier(name: "gui/container/furnace") + case .grindstone: + return Identifier(name: "gui/container/grindstone") + case .hopper: + return Identifier(name: "gui/container/hopper") + case .loom: + return Identifier(name: "gui/container/loom") + case .merchant: + return Identifier(name: "gui/container/villager2") + case .shulkerBox: + return Identifier(name: "gui/container/shulker_box") + case .smithingTable: + return Identifier(name: "gui/container/smithing") + case .smoker: + return Identifier(name: "gui/container/smoker") + case .cartographyTable: + return Identifier(name: "gui/container/cartography_table") + case .stonecutter: + return Identifier(name: "gui/container/stonecutter") } } } diff --git a/Sources/Core/Sources/World/Dimension/Dimension.swift b/Sources/Core/Sources/World/Dimension/Dimension.swift index 03adc8ba..89a61ede 100644 --- a/Sources/Core/Sources/World/Dimension/Dimension.swift +++ b/Sources/Core/Sources/World/Dimension/Dimension.swift @@ -89,23 +89,23 @@ public struct Dimension { let fixedTime: Int64? = try? compound.get("fixed_time") self.fixedTime = fixedTime.map(Int.init) - let isNatural: UInt8 = try compound.get("natural") + let isNatural: Int8 = try compound.get("natural") self.isNatural = isNatural == 1 - let hasCeiling: UInt8 = try compound.get("has_ceiling") + let hasCeiling: Int8 = try compound.get("has_ceiling") self.hasCeiling = hasCeiling == 1 - let hasSkyLight: UInt8 = try compound.get("has_skylight") + let hasSkyLight: Int8 = try compound.get("has_skylight") self.hasSkyLight = hasSkyLight == 1 - let shrunk: UInt8 = try compound.get("shrunk") + let shrunk: Int8 = try compound.get("shrunk") self.shrunk = shrunk == 1 - let ultrawarm: UInt8 = try compound.get("ultrawarm") + let ultrawarm: Int8 = try compound.get("ultrawarm") self.ultrawarm = ultrawarm == 1 - let hasRaids: UInt8 = try compound.get("has_raids") + let hasRaids: Int8 = try compound.get("has_raids") self.hasRaids = hasRaids == 1 - let respawnAnchorWorks: UInt8 = try compound.get("respawn_anchor_works") + let respawnAnchorWorks: Int8 = try compound.get("respawn_anchor_works") self.respawnAnchorWorks = respawnAnchorWorks == 1 - let bedWorks: UInt8 = try compound.get("bed_works") + let bedWorks: Int8 = try compound.get("bed_works") self.bedWorks = bedWorks == 1 - let piglinSafe: UInt8 = try compound.get("piglin_safe") + let piglinSafe: Int8 = try compound.get("piglin_safe") self.piglinSafe = piglinSafe == 1 let logicalHeight: Int32 = try compound.get("logical_height") self.logicalHeight = Int(logicalHeight) From 58ad0fbdd4c351b150c86a4577c3b25da99d7585 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 5 Jun 2024 20:40:52 +1000 Subject: [PATCH 56/84] Implement entity model part rotation (fixes rendering of pigs, villagers, etc) --- .../Core/Renderer/Mesh/BlockMeshBuilder.swift | 3 ++- .../Renderer/Mesh/EntityMeshBuilder.swift | 25 ++++++++++++++++--- .../Model/Entity/JSON/JSONEntityModel.swift | 4 ++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift index 5d5bd89f..0f1bdc2a 100644 --- a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift @@ -199,7 +199,8 @@ struct BlockMeshBuilder { indices.append( contentsOf: geometry.indices.map { index in return index + startingIndex - }) + } + ) } let geometry = Geometry(vertices: vertices, indices: indices) diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index fb1c1dd3..b403a80e 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -24,24 +24,40 @@ public struct EntityMeshBuilder { func buildSubmodel( _ submodel: JSONEntityModel.Submodel, index: Int, + transformation: Mat4x4f = MatrixUtil.identity, into geometry: inout Geometry ) { + var transformation = transformation + if let rotation = submodel.rotate { + let translation = submodel.translate ?? .zero + transformation = + MatrixUtil.rotationMatrix(-MathUtil.radians(from: rotation)) + * MatrixUtil.translationMatrix(translation) + } + for box in submodel.boxes ?? [] { buildBox( box, color: index < Self.colors.count ? Self.colors[index] : [0.5, 0.5, 0.5], + transformation: transformation, into: &geometry ) } for (nestedIndex, nestedSubmodel) in (submodel.submodels ?? []).enumerated() { - buildSubmodel(nestedSubmodel, index: nestedIndex, into: &geometry) + buildSubmodel( + nestedSubmodel, + index: nestedIndex, + transformation: transformation, + into: &geometry + ) } } func buildBox( _ box: JSONEntityModel.Box, color: Vec3f, + transformation: Mat4x4f, into geometry: inout Geometry ) { var boxPosition = Vec3f( @@ -61,8 +77,6 @@ public struct EntityMeshBuilder { boxSize += 2 * growth } - boxPosition = boxPosition / 16 + position - boxSize /= 16 for direction in Direction.allDirections { // The index of the first vertex of this face let offset = UInt32(geometry.vertices.count) @@ -72,7 +86,10 @@ public struct EntityMeshBuilder { let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] for vertexPosition in faceVertexPositions { - let position = vertexPosition * boxSize + boxPosition + var position = vertexPosition * boxSize + boxPosition + position = (Vec4f(position, 1) * transformation).xyz + position /= 16 + position += self.position let vertex = EntityVertex( x: position.x, y: position.y, diff --git a/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift b/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift index ef944008..4cccd8be 100644 --- a/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift +++ b/Sources/Core/Sources/Resources/Model/Entity/JSON/JSONEntityModel.swift @@ -10,7 +10,9 @@ public struct JSONEntityModel: Codable { public var id: String? public var invertAxis: String? public var mirrorTexture: String? + /// Translation to apply post-rotation (ignored if ``rotate`` is `nil`). public var translate: Vec3f? + /// Rotation in degrees. public var rotate: Vec3f? public var boxes: [Box]? public var submodels: [Submodel]? @@ -42,7 +44,7 @@ public struct JSONEntityModel: Codable { var models: [Identifier: JSONEntityModel] = [:] for file in files where file.pathExtension == "jem" { - var identifier = Identifier( + let identifier = Identifier( namespace: namespace, name: file.deletingPathExtension().lastPathComponent ) From b3c49f75879c08047cc879d089cb7a2f0a8f15d5 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 5 Jun 2024 23:28:03 +1000 Subject: [PATCH 57/84] Don't drop item if user is pressing cmd+q, and show targeted entity id in f3 screen (+temp disable entity velocity packets) Entity velocity packets were mucking up the entity physics system (causing drift in certain situations) so I've temporarily disabled handling them. Also updated renderer to avoid spamming warnings every frame for missing entity models (stores a set of known-missing models) --- Sources/Core/Renderer/Entity/EntityRenderer.swift | 9 +++++++-- Sources/Core/Sources/Client.swift | 5 ++++- .../Core/Sources/ECS/Systems/PlayerInputSystem.swift | 11 +++++++++++ Sources/Core/Sources/GUI/InGameGUI.swift | 6 ++++++ Sources/Core/Sources/Input/Key.swift | 6 +++--- .../Play/Clientbound/EntityVelocityPacket.swift | 12 +++++++++--- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index d3888bd5..3a306d23 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -27,6 +27,9 @@ public struct EntityRenderer: Renderer { /// The command queue used to perform operations outside of the main render loop. private var commandQueue: MTLCommandQueue + /// Missing entity models that have already had warnings printed (used to avoid spamming warnings every frame). + private var missingModels: Set = [] + private var profiler: Profiler /// Creates a new entity renderer. @@ -133,8 +136,10 @@ public struct EntityRenderer: Renderer { guard let model = client.resourcePack.vanillaResources.entityModelPalette.models[kindIdentifier] else { - // TODO: Re-enable missing model warning once item model rendering is implemented - log.warning("Missing model for entity kind with identifier '\(kindIdentifier)'") + if !missingModels.contains(kindIdentifier) { + log.warning("Missing model for entity kind with identifier '\(kindIdentifier)'") + missingModels.insert(kindIdentifier) + } continue } diff --git a/Sources/Core/Sources/Client.swift b/Sources/Core/Sources/Client.swift index b6b0288b..3a082a1f 100644 --- a/Sources/Core/Sources/Client.swift +++ b/Sources/Core/Sources/Client.swift @@ -47,7 +47,10 @@ public final class Client: @unchecked Sendable { // MARK: Connection lifecycle /// Join the specified server. Throws if the packets fail to send. - public func joinServer(describedBy descriptor: ServerDescriptor, with account: Account) async throws { + public func joinServer( + describedBy descriptor: ServerDescriptor, + with account: Account + ) async throws { self.account = account // Create a connection to the server diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 5230eccb..1c6b9297 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -125,6 +125,17 @@ public final class PlayerInputSystem: System { case .previousSlot: inventory.selectedHotbarSlot = (inventory.selectedHotbarSlot + 8) % 9 case .dropItem: + // TODO: Implement a similar check on other desktop platforms (Linux, Windows) + #if os(macOS) + let isQuitKeyboardShortcut = + event.key == .q + && (inputState.keys.contains(where: \.isCommand) + || inputState.newlyPressed.contains { $0.key?.isCommand == true }) + guard !isQuitKeyboardShortcut else { + break + } + #endif + let slotIndex = PlayerInventory.hotbarArea.startIndex + inventory.selectedHotbarSlot inventory.window.dropItemFromSlot( slotIndex, diff --git a/Sources/Core/Sources/GUI/InGameGUI.swift b/Sources/Core/Sources/GUI/InGameGUI.swift index 3a15d7b4..49ee5fb7 100644 --- a/Sources/Core/Sources/GUI/InGameGUI.swift +++ b/Sources/Core/Sources/GUI/InGameGUI.swift @@ -464,6 +464,8 @@ public class InGameGUI { let biome = game.world.getBiome(at: blockPosition) + let targetedEntity = game.targetedEntity() + let leftSections: [[String]] = [ [ "Minecraft \(Constants.versionString) (Delta Client)", @@ -481,6 +483,10 @@ public class InGameGUI { "Biome: \(biome?.identifier.description ?? "not loaded")", "Gamemode: \(gamemode.string)", ], + // Custom Delta Client info + [ + targetedEntity.map { "Targeted entity: \($0.target)" } + ].compactMap(identity), ] #if os(macOS) diff --git a/Sources/Core/Sources/Input/Key.swift b/Sources/Core/Sources/Input/Key.swift index 767a322f..516dae2e 100644 --- a/Sources/Core/Sources/Input/Key.swift +++ b/Sources/Core/Sources/Input/Key.swift @@ -1,6 +1,6 @@ import Foundation -public enum Key: CustomStringConvertible, Hashable { +public enum Key: CustomStringConvertible, Hashable { case leftShift case rightShift case leftControl @@ -394,7 +394,7 @@ public enum Key: CustomStringConvertible, Hashable { 0x7B: .leftArrow, 0x7C: .rightArrow, 0x7D: .downArrow, - 0x7E: .upArrow + 0x7E: .upArrow, ] } @@ -539,7 +539,7 @@ extension Key: RawRepresentable { "leftMouseButton": .leftMouseButton, "rightMouseButton": .rightMouseButton, "scrollUp": .scrollUp, - "scrollDown": .scrollDown + "scrollDown": .scrollDown, ] } 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 ddffdbc7..a52b2cf6 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation public struct EntityVelocityPacket: ClientboundEntityPacket { public static let id: Int = 0x46 @@ -16,8 +16,14 @@ public struct EntityVelocityPacket: ClientboundEntityPacket { /// Should only be called if a nexus write lock is already acquired. public func handle(for client: Client) throws { - client.game.accessComponent(entityId: entityId, EntityVelocity.self, acquireLock: false) { velocityComponent in - velocityComponent.vector = velocity + client.game.accessComponent( + entityId: entityId, + EntityVelocity.self, + acquireLock: false + ) { velocityComponent in + // 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 } } } From c61c74a90253d89320ad5f4c3b1b8dae0ac504a5 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 01:48:08 +1000 Subject: [PATCH 58/84] Add texture support to entity renderer --- .../Core/Renderer/Entity/EntityRenderer.swift | 21 +++- .../Core/Renderer/Entity/EntityVertex.swift | 40 ++++++- .../Renderer/Mesh/EntityMeshBuilder.swift | 107 +++++++++++++++++- .../Core/Renderer/Shader/EntityShaders.metal | 20 +++- Sources/Core/Sources/Datatypes/Axis.swift | 12 ++ .../Core/Sources/Resources/Resources.swift | 18 +-- 6 files changed, 196 insertions(+), 22 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index 3a306d23..78abca5d 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -20,6 +20,8 @@ public struct EntityRenderer: Renderer { /// The number of indices in ``indexBuffer``. private var indexCount: Int + private var entityTexturePalette: MetalTexturePalette + /// The client that entities will be renderer for. private var client: Client /// The device that will be used to render. @@ -77,6 +79,12 @@ public struct EntityRenderer: Renderer { options: .storageModeShared, label: "entityHitBoxIndices" ) + + entityTexturePalette = try MetalTexturePalette( + palette: client.resourcePack.vanillaResources.entityTexturePalette, + device: device, + commandQueue: commandQueue + ) } /// Renders all entity hit boxes using instancing. @@ -143,7 +151,12 @@ public struct EntityRenderer: Renderer { continue } - let builder = EntityMeshBuilder(model: model, position: Vec3f(position.smoothVector)) + let builder = EntityMeshBuilder( + entityKind: kindIdentifier, + model: model, + position: Vec3f(position.smoothVector), + texturePalette: entityTexturePalette + ) builder.build(into: &geometry) } profiler.pop() @@ -154,6 +167,7 @@ public struct EntityRenderer: Renderer { } encoder.setRenderPipelineState(renderPipelineState) + encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0) // TODO: Update profiler measurements var mesh = Mesh(geometry, uniforms: ()) @@ -176,7 +190,10 @@ public struct EntityRenderer: Renderer { z: position.z, r: color.x, g: color.y, - b: color.z + b: color.z, + u: 0, + v: 0, + textureIndex: nil ) ) } diff --git a/Sources/Core/Renderer/Entity/EntityVertex.swift b/Sources/Core/Renderer/Entity/EntityVertex.swift index f24be78f..e73d7050 100644 --- a/Sources/Core/Renderer/Entity/EntityVertex.swift +++ b/Sources/Core/Renderer/Entity/EntityVertex.swift @@ -1,9 +1,37 @@ /// The vertex format used by the entity shader. public struct EntityVertex { - let x: Float - let y: Float - let z: Float - let r: Float - let g: Float - let b: Float + public let x: Float + public let y: Float + public let z: Float + public let r: Float + public let g: Float + public let b: Float + public let u: Float + public let v: Float + /// ``UInt16/max`` indicates that no texture is to be used. I would usually use + /// an optional to model that, but this type needs to be compatible with C as we + /// pass it off to the shaders for rendering. + public let textureIndex: UInt16 + + public init( + x: Float, + y: Float, + z: Float, + r: Float, + g: Float, + b: Float, + u: Float, + v: Float, + textureIndex: UInt16? + ) { + self.x = x + self.y = y + self.z = z + self.r = r + self.g = g + self.b = b + self.u = u + self.v = v + self.textureIndex = textureIndex ?? .max + } } diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index b403a80e..75fbaad7 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -1,8 +1,11 @@ +import CoreFoundation import DeltaCore public struct EntityMeshBuilder { + let entityKind: Identifier let model: JSONEntityModel let position: Vec3f + let texturePalette: MetalTexturePalette static let colors: [Vec3f] = [ [1, 0, 0], @@ -16,14 +19,37 @@ public struct EntityMeshBuilder { ] func build(into geometry: inout Geometry) { + let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ + Identifier(name: "player"): Identifier(name: "entity/steve"), + Identifier(name: "dragon"): Identifier(name: "entity/enderdragon/dragon"), + ] + + let texture: Int? + if let identifier = hardcodedTextureIdentifiers[entityKind] { + texture = texturePalette.textureIndex(for: identifier) + } else { + let textureIdentifier = Identifier( + namespace: entityKind.namespace, + name: "entity/\(entityKind.name)" + ) + let nestedTextureIdentifier = Identifier( + namespace: entityKind.namespace, + name: "entity/\(entityKind.name)/\(entityKind.name)" + ) + texture = + texturePalette.textureIndex(for: textureIdentifier) + ?? texturePalette.textureIndex(for: nestedTextureIdentifier) + } + for (index, submodel) in model.models.enumerated() { - buildSubmodel(submodel, index: index, into: &geometry) + buildSubmodel(submodel, index: index, textureIndex: texture, into: &geometry) } } func buildSubmodel( _ submodel: JSONEntityModel.Submodel, index: Int, + textureIndex: Int?, transformation: Mat4x4f = MatrixUtil.identity, into geometry: inout Geometry ) { @@ -40,6 +66,13 @@ public struct EntityMeshBuilder { box, color: index < Self.colors.count ? Self.colors[index] : [0.5, 0.5, 0.5], transformation: transformation, + textureIndex: textureIndex, + // We already invert 'y' and 'z' + invertedAxes: [ + submodel.invertAxis?.contains("x") == true, + submodel.invertAxis?.contains("y") != true, + submodel.invertAxis?.contains("z") != true, + ], into: &geometry ) } @@ -48,6 +81,7 @@ public struct EntityMeshBuilder { buildSubmodel( nestedSubmodel, index: nestedIndex, + textureIndex: textureIndex, transformation: transformation, into: &geometry ) @@ -58,6 +92,8 @@ public struct EntityMeshBuilder { _ box: JSONEntityModel.Box, color: Vec3f, transformation: Mat4x4f, + textureIndex: Int?, + invertedAxes: [Bool], into geometry: inout Geometry ) { var boxPosition = Vec3f( @@ -71,6 +107,9 @@ public struct EntityMeshBuilder { box.coordinates[5] ) + let textureOffset = Vec2f(box.textureOffset ?? .zero) + + let baseBoxSize = boxSize if let additionalSize = box.sizeAdd { let growth = Vec3f(repeating: additionalSize) boxPosition -= growth @@ -84,8 +123,63 @@ public struct EntityMeshBuilder { geometry.indices.append(index &+ offset) } + var uvOrigin: Vec2f + var uvSize: Vec2f + let verticalAxis: Axis + let horizontalAxis: Axis + switch direction { + case .east: + uvOrigin = textureOffset + Vec2f(0, baseBoxSize.z) + uvSize = Vec2f(baseBoxSize.z, baseBoxSize.y) + verticalAxis = .y + horizontalAxis = .z + case .north: + uvOrigin = textureOffset + Vec2f(baseBoxSize.z, baseBoxSize.z) + uvSize = Vec2f(baseBoxSize.x, baseBoxSize.y) + verticalAxis = .y + horizontalAxis = .x + case .west: + uvOrigin = textureOffset + Vec2f(baseBoxSize.z + baseBoxSize.x, baseBoxSize.z) + uvSize = Vec2f(baseBoxSize.z, baseBoxSize.y) + verticalAxis = .y + horizontalAxis = .z + case .south: + uvOrigin = textureOffset + Vec2f(baseBoxSize.z * 2 + baseBoxSize.x, baseBoxSize.z) + uvSize = Vec2f(baseBoxSize.x, baseBoxSize.y) + verticalAxis = .y + horizontalAxis = .x + case .up: + uvOrigin = textureOffset + Vec2f(baseBoxSize.z, 0) + uvSize = Vec2f(baseBoxSize.x, baseBoxSize.z) + verticalAxis = .z + horizontalAxis = .x + case .down: + uvOrigin = textureOffset + Vec2f(baseBoxSize.z + baseBoxSize.x, 0) + uvSize = Vec2f(baseBoxSize.x, baseBoxSize.z) + verticalAxis = .z + horizontalAxis = .x + } + + if invertedAxes[horizontalAxis.index] { + uvOrigin.x += uvSize.x + uvSize.x *= -1 + } + if invertedAxes[verticalAxis.index] { + uvOrigin.y += uvSize.y + uvSize.y *= -1 + } + + let uvs = [ + uvOrigin, + uvOrigin + Vec2f(0, uvSize.y), + uvOrigin + Vec2f(uvSize.x, uvSize.y), + uvOrigin + Vec2f(uvSize.x, 0), + ].map { + $0 / Vec2f(Float(texturePalette.palette.width), Float(texturePalette.palette.height)) + } + let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] - for vertexPosition in faceVertexPositions { + for (uv, vertexPosition) in zip(uvs, faceVertexPositions) { var position = vertexPosition * boxSize + boxPosition position = (Vec4f(position, 1) * transformation).xyz position /= 16 @@ -94,9 +188,12 @@ public struct EntityMeshBuilder { x: position.x, y: position.y, z: position.z, - r: color.x, - g: color.y, - b: color.z + r: textureIndex == nil ? color.x : 1, + g: textureIndex == nil ? color.y : 1, + b: textureIndex == nil ? color.z : 1, + u: uv.x, + v: uv.y, + textureIndex: textureIndex.map(UInt16.init) ) geometry.vertices.append(vertex) } diff --git a/Sources/Core/Renderer/Shader/EntityShaders.metal b/Sources/Core/Renderer/Shader/EntityShaders.metal index 7c1250be..5dab3176 100644 --- a/Sources/Core/Renderer/Shader/EntityShaders.metal +++ b/Sources/Core/Renderer/Shader/EntityShaders.metal @@ -10,11 +10,16 @@ struct EntityVertex { float r; float g; float b; + float u; + float v; + uint16_t textureIndex; }; struct EntityRasterizerData { float4 position [[position]]; float4 color; + float2 uv; + uint16_t textureIndex; }; vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [[buffer(0)]], @@ -25,11 +30,24 @@ vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [ out.position = float4(in.x, in.y, in.z, 1.0) * cameraUniforms.framing * cameraUniforms.projection; out.color = float4(in.r, in.g, in.b, 1.0); + out.uv = float2(in.u, in.v); + out.textureIndex = in.textureIndex; return out; } +constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest, mip_filter::linear); + fragment float4 entityFragmentShader(EntityRasterizerData in [[stage_in]], texture2d_array textureArray [[texture(0)]]) { - return in.color; + float4 color; + if (in.textureIndex == 65535) { + color = in.color; + } else { + color = textureArray.sample(textureSampler, in.uv, in.textureIndex); + } + if (color.a < 0.3) { + discard_fragment(); + } + return color; } diff --git a/Sources/Core/Sources/Datatypes/Axis.swift b/Sources/Core/Sources/Datatypes/Axis.swift index b8606a69..c8374b68 100644 --- a/Sources/Core/Sources/Datatypes/Axis.swift +++ b/Sources/Core/Sources/Datatypes/Axis.swift @@ -29,4 +29,16 @@ public enum Axis: CaseIterable { return .north } } + + /// The conventional indices assigned to the axis, i.e. x -> 0, y -> 1, z -> 2 + public var index: Int { + switch self { + case .x: + return 0 + case .y: + return 1 + case .z: + return 2 + } + } } diff --git a/Sources/Core/Sources/Resources/Resources.swift b/Sources/Core/Sources/Resources/Resources.swift index c95e9413..cad33911 100644 --- a/Sources/Core/Sources/Resources/Resources.swift +++ b/Sources/Core/Sources/Resources/Resources.swift @@ -164,14 +164,16 @@ extension ResourcePack { } /// Load entity textures - // let entityTextureDirectory = textureDirectory.appendingPathComponent("entity") - // if FileManager.default.directoryExists(at: entityTextureDirectory) { - // resources.entityTexturePalette = try TexturePalette.load( - // from: entityTextureDirectory, - // inNamespace: namespace, - // withType: "entity" - // ) - // } + let entityTextureDirectory = textureDirectory.appendingPathComponent("entity") + if FileManager.default.directoryExists(at: entityTextureDirectory) { + resources.entityTexturePalette = try TexturePalette.load( + from: entityTextureDirectory, + inNamespace: namespace, + withType: "entity", + recursive: true, + isAnimated: false + ) + } // Load GUI textures let guiTextureDirectory = textureDirectory.appendingPathComponent("gui") From e2b178f8d44cf6316acf40343f00adf8ff14880c Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 02:50:42 +1000 Subject: [PATCH 59/84] Support entity rotation in EntityMeshBuilder --- .../Core/Renderer/Entity/EntityRenderer.swift | 8 +++++-- .../Renderer/Mesh/EntityMeshBuilder.swift | 23 +++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index 78abca5d..da838112 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -105,16 +105,18 @@ public struct EntityRenderer: Renderer { client.game.accessNexus { nexus in // If the player is in first person view we don't render them profiler.push(.getEntities) - let entities: Family> + let entities: Family> if isFirstPerson { entities = nexus.family( requiresAll: EntityPosition.self, + EntityRotation.self, EntityKindId.self, excludesAll: ClientPlayerEntity.self ) } else { entities = nexus.family( requiresAll: EntityPosition.self, + EntityRotation.self, EntityKindId.self ) } @@ -125,7 +127,7 @@ public struct EntityRenderer: Renderer { // Create uniforms for each entity profiler.push(.createUniforms) - for (position, kindId) in entities { + for (position, rotation, kindId) in entities { // Don't render entities that are outside of the render distance let chunkPosition = position.chunk if !chunkPosition.isWithinRenderDistance(renderDistance, of: cameraChunk) { @@ -155,6 +157,8 @@ public struct EntityRenderer: Renderer { entityKind: kindIdentifier, model: model, position: Vec3f(position.smoothVector), + pitch: rotation.smoothPitch, + yaw: rotation.smoothYaw, texturePalette: entityTexturePalette ) builder.build(into: &geometry) diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index 75fbaad7..a75a2688 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -2,9 +2,19 @@ import CoreFoundation import DeltaCore public struct EntityMeshBuilder { + /// Associates entity kinds with hardcoded entity texture identifiers. Used to manually + /// instruct Delta Client where to find certain textures that aren't in the standard + /// locations. + static let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ + Identifier(name: "player"): Identifier(name: "entity/steve"), + Identifier(name: "dragon"): Identifier(name: "entity/enderdragon/dragon"), + ] + let entityKind: Identifier let model: JSONEntityModel let position: Vec3f + let pitch: Float + let yaw: Float let texturePalette: MetalTexturePalette static let colors: [Vec3f] = [ @@ -19,15 +29,12 @@ public struct EntityMeshBuilder { ] func build(into geometry: inout Geometry) { - let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ - Identifier(name: "player"): Identifier(name: "entity/steve"), - Identifier(name: "dragon"): Identifier(name: "entity/enderdragon/dragon"), - ] - let texture: Int? - if let identifier = hardcodedTextureIdentifiers[entityKind] { + if let identifier = Self.hardcodedTextureIdentifiers[entityKind] { texture = texturePalette.textureIndex(for: identifier) } else { + // Entity textures can be in all sorts of structures so we just have a few + // educated guesses for now. let textureIdentifier = Identifier( namespace: entityKind.namespace, name: "entity/\(entityKind.name)" @@ -181,7 +188,9 @@ public struct EntityMeshBuilder { let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] for (uv, vertexPosition) in zip(uvs, faceVertexPositions) { var position = vertexPosition * boxSize + boxPosition - position = (Vec4f(position, 1) * transformation).xyz + position = + (Vec4f(position, 1) * transformation * MatrixUtil.rotationMatrix(yaw + .pi, around: .y)) + .xyz position /= 16 position += self.position let vertex = EntityVertex( From 1f907d582270da6b52a960f066e6aff32f97eb16 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 02:51:23 +1000 Subject: [PATCH 60/84] Fix EntityMovementSystem (accidentally early exited loop whenever a lerping entity was reached, causing subsequent entities to never get updated) --- .../ECS/Components/EntityLerpState.swift | 26 ++++++++++++++++--- .../ECS/Systems/EntityMovementSystem.swift | 9 ++++--- Sources/Core/Sources/Game.swift | 3 ++- .../Clientbound/EntityPositionPacket.swift | 9 ++++--- .../Clientbound/EntityVelocityPacket.swift | 2 +- .../Play/Clientbound/SpawnEntityPacket.swift | 2 +- .../Clientbound/SpawnLivingEntityPacket.swift | 4 +-- .../Play/Clientbound/SpawnPlayerPacket.swift | 2 +- 8 files changed, 41 insertions(+), 16 deletions(-) diff --git a/Sources/Core/Sources/ECS/Components/EntityLerpState.swift b/Sources/Core/Sources/ECS/Components/EntityLerpState.swift index f7cf0ebb..66a47e9b 100644 --- a/Sources/Core/Sources/ECS/Components/EntityLerpState.swift +++ b/Sources/Core/Sources/ECS/Components/EntityLerpState.swift @@ -1,7 +1,7 @@ -import Foundation import CoreFoundation import FirebladeECS import FirebladeMath +import Foundation /// A component storing the lerp (if any) that an entity is currently undergoing; lerp is short /// for linear interpolation. @@ -46,8 +46,17 @@ public class EntityLerpState: Component { /// Ticks an entities current lerp returning the entity's new position, pitch, and yaw. If there's no current /// lerp, then `nil` is returned. - public func tick(position: Vec3d, pitch: Float, yaw: Float) -> (position: Vec3d, pitch: Float, yaw: Float)? { - guard var lerp = currentLerp else { + public func tick( + position: Vec3d, + pitch: Float, + yaw: Float + ) -> ( + position: Vec3d, + pitch: Float, + yaw: Float + )? { + guard var lerp = currentLerp, lerp.ticksRemaining > 0 else { + currentLerp = nil return nil } @@ -60,10 +69,19 @@ public class EntityLerpState: Component { currentLerp = lerp } + let targetYaw: Float + if yaw - lerp.targetYaw > .pi { + targetYaw = lerp.targetYaw + 2 * .pi + } else if yaw - lerp.targetYaw < -.pi { + targetYaw = lerp.targetYaw - 2 * .pi + } else { + targetYaw = lerp.targetYaw + } + return ( MathUtil.lerp(from: position, to: lerp.targetPosition, progress: progress), MathUtil.lerp(from: pitch, to: lerp.targetPitch, progress: Float(progress)), - MathUtil.lerp(from: yaw, to: lerp.targetYaw, progress: Float(progress)) + MathUtil.lerp(from: yaw, to: targetYaw, progress: Float(progress)) ) } } diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index 7c1c52c0..11eab7db 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -1,7 +1,8 @@ import FirebladeECS /// Updates the position of each entity according to its velocity (excluding the player, -/// because velocity for the player is handled by the ``PlayerVelocitySystem-30ewl``). +/// because velocity for the player is handled by the ``PlayerVelocitySystem``). Also handles +/// position/rotation lerping. public struct EntityMovementSystem: System { public func update(_ nexus: Nexus, _ world: World) { let physicsEntities = nexus.family( @@ -20,11 +21,13 @@ public struct EntityMovementSystem: System { continue } - if let (newPosition, newPitch, newYaw) = lerpState.tick(position: position.vector, pitch: rotation.pitch, yaw: rotation.yaw) { + if let (newPosition, newPitch, newYaw) = lerpState.tick( + position: position.vector, pitch: rotation.pitch, yaw: rotation.yaw) + { position.vector = newPosition rotation.pitch = newPitch rotation.yaw = newYaw - return + continue } velocity.vector *= 0.98 diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index d132a4f3..659eda54 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -102,7 +102,8 @@ public final class Game: @unchecked Sendable { // together for the specific case of PlayerInputSystem); proper resource pack propagation // will probably take quite a bit of work. tickScheduler.addSystem( - PlayerInputSystem(connection, self, eventBus, configuration, font, locale)) + PlayerInputSystem(connection, self, eventBus, configuration, font, locale) + ) tickScheduler.addSystem(PlayerFlightSystem()) tickScheduler.addSystem(PlayerAccelerationSystem()) tickScheduler.addSystem(PlayerJumpSystem()) 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 753512d1..e6a4b197 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityPositionPacket.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation public struct EntityPositionPacket: ClientboundEntityPacket { public static let id: Int = 0x28 @@ -39,6 +39,9 @@ public struct EntityPositionPacket: 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 EntityPositionPacket" + ) return } @@ -50,8 +53,8 @@ public struct EntityPositionPacket: ClientboundEntityPacket { onGroundComponent.onGround = onGround lerpState.lerp( to: currentTargetPosition + relativePosition, - pitch: rotation.pitch, - yaw: rotation.yaw, + pitch: lerpState.currentLerp?.targetPitch ?? rotation.pitch, + yaw: lerpState.currentLerp?.targetYaw ?? rotation.yaw, duration: kind.defaultLerpDuration ) } 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 a52b2cf6..02411bb0 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityVelocityPacket.swift @@ -23,7 +23,7 @@ public struct EntityVelocityPacket: ClientboundEntityPacket { ) { velocityComponent in // 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 + 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 0cead80d..f6614224 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift @@ -1,6 +1,6 @@ -import Foundation import FirebladeECS import FirebladeMath +import Foundation public struct SpawnEntityPacket: ClientboundPacket { public static let id: Int = 0x00 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 9f918b23..9a926a56 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation public struct SpawnLivingEntityPacket: ClientboundPacket { public static let id: Int = 0x02 @@ -30,7 +30,7 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { } client.game.createEntity(id: entityId) { - LivingEntity() // Mark it as a living entity + LivingEntity() // Mark it as a living entity EntityKindId(type) EntityId(entityId) EntityUUID(entityUUID) 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 be7fd97d..0f5b06f0 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation public struct SpawnPlayerPacket: ClientboundPacket { public static let id: Int = 0x04 From 44199469c6795f73232e0dbbb1dd4dffc05e51dc Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 11:06:45 +1000 Subject: [PATCH 61/84] Render entity hitbox when model is missing (at the moment: items, dragon's breath, and more) --- .../Core/Renderer/Entity/EntityRenderer.swift | 28 ++++--------- .../Renderer/Mesh/EntityMeshBuilder.swift | 41 ++++++++++++++++++- .../ECS/Systems/EntityMovementSystem.swift | 6 ++- 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index da838112..4828e99c 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -7,7 +7,7 @@ import MetalKit /// Renders all entities in the world the client is currently connected to. public struct EntityRenderer: Renderer { /// The color to render hit boxes as. Defaults to 0xe3c28d (light cream colour). - public var hitBoxColor = DeltaCore.RGBColor(hexCode: 0xe3c28d) + public static let hitBoxColor = DeltaCore.RGBColor(hexCode: 0xe3c28d) /// The render pipeline state for rendering entities. Does not have blending enabled. private var renderPipelineState: MTLRenderPipelineState @@ -29,9 +29,6 @@ public struct EntityRenderer: Renderer { /// The command queue used to perform operations outside of the main render loop. private var commandQueue: MTLCommandQueue - /// Missing entity models that have already had warnings printed (used to avoid spamming warnings every frame). - private var missingModels: Set = [] - private var profiler: Profiler /// Creates a new entity renderer. @@ -61,7 +58,7 @@ public struct EntityRenderer: Renderer { ) // Create hitbox geometry (hitboxes are rendered using instancing) - var geometry = Self.createHitBoxGeometry(color: hitBoxColor) + var geometry = Self.createHitBoxGeometry(color: Self.hitBoxColor) indexCount = geometry.indices.count vertexBuffer = try MetalUtil.makeBuffer( @@ -105,11 +102,12 @@ public struct EntityRenderer: Renderer { client.game.accessNexus { nexus in // If the player is in first person view we don't render them profiler.push(.getEntities) - let entities: Family> + let entities: Family> if isFirstPerson { entities = nexus.family( requiresAll: EntityPosition.self, EntityRotation.self, + EntityHitBox.self, EntityKindId.self, excludesAll: ClientPlayerEntity.self ) @@ -117,6 +115,7 @@ public struct EntityRenderer: Renderer { entities = nexus.family( requiresAll: EntityPosition.self, EntityRotation.self, + EntityHitBox.self, EntityKindId.self ) } @@ -127,7 +126,7 @@ public struct EntityRenderer: Renderer { // Create uniforms for each entity profiler.push(.createUniforms) - for (position, rotation, kindId) in entities { + for (position, rotation, hitbox, kindId) in entities { // Don't render entities that are outside of the render distance let chunkPosition = position.chunk if !chunkPosition.isWithinRenderDistance(renderDistance, of: cameraChunk) { @@ -143,23 +142,14 @@ public struct EntityRenderer: Renderer { kindIdentifier = Identifier(name: "dragon") } - guard - let model = client.resourcePack.vanillaResources.entityModelPalette.models[kindIdentifier] - else { - if !missingModels.contains(kindIdentifier) { - log.warning("Missing model for entity kind with identifier '\(kindIdentifier)'") - missingModels.insert(kindIdentifier) - } - continue - } - let builder = EntityMeshBuilder( entityKind: kindIdentifier, - model: model, position: Vec3f(position.smoothVector), pitch: rotation.smoothPitch, yaw: rotation.smoothYaw, - texturePalette: entityTexturePalette + entityModelPalette: client.resourcePack.vanillaResources.entityModelPalette, + texturePalette: entityTexturePalette, + hitbox: hitbox.aabb(at: position.smoothVector) ) builder.build(into: &geometry) } diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index a75a2688..9d41bf24 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -11,11 +11,12 @@ public struct EntityMeshBuilder { ] let entityKind: Identifier - let model: JSONEntityModel let position: Vec3f let pitch: Float let yaw: Float + let entityModelPalette: EntityModelPalette let texturePalette: MetalTexturePalette + let hitbox: AxisAlignedBoundingBox static let colors: [Vec3f] = [ [1, 0, 0], @@ -29,6 +30,44 @@ public struct EntityMeshBuilder { ] func build(into geometry: inout Geometry) { + if let model = entityModelPalette.models[entityKind] { + buildModel(model, into: &geometry) + } else { + buildAABB(hitbox, into: &geometry) + } + } + + func buildAABB(_ aabb: AxisAlignedBoundingBox, into geometry: inout Geometry) { + let transformation = + MatrixUtil.scalingMatrix(Vec3f(aabb.size)) + * MatrixUtil.translationMatrix(Vec3f(aabb.position)) + for direction in Direction.allDirections { + let offset = UInt32(geometry.vertices.count) + for index in CubeGeometry.faceWinding { + geometry.indices.append(index &+ offset) + } + + let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] + for vertexPosition in faceVertexPositions { + let position = (Vec4f(vertexPosition, 1) * transformation).xyz + let color = EntityRenderer.hitBoxColor.floatVector + let vertex = EntityVertex( + x: position.x, + y: position.y, + z: position.z, + r: color.x, + g: color.y, + b: color.z, + u: 0, + v: 0, + textureIndex: nil + ) + geometry.vertices.append(vertex) + } + } + } + + func buildModel(_ model: JSONEntityModel, into geometry: inout Geometry) { let texture: Int? if let identifier = Self.hardcodedTextureIdentifiers[entityKind] { texture = texturePalette.textureIndex(for: identifier) diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index 11eab7db..bbc03795 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -22,8 +22,10 @@ public struct EntityMovementSystem: System { } if let (newPosition, newPitch, newYaw) = lerpState.tick( - position: position.vector, pitch: rotation.pitch, yaw: rotation.yaw) - { + position: position.vector, + pitch: rotation.pitch, + yaw: rotation.yaw + ) { position.vector = newPosition rotation.pitch = newPitch rotation.yaw = newYaw From a7e5faf6577898b1a1e02221e42a7746dd514a58 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 11:43:40 +1000 Subject: [PATCH 62/84] Remove all entities (except for the player) when switching dimensions, and implement instant respawning (no respawn gui yet) --- Sources/Core/Sources/Game.swift | 11 ++++++++++ .../Clientbound/ChangeGameStatePacket.swift | 1 + .../Play/Clientbound/EntityStatusPacket.swift | 6 +++-- .../Play/Clientbound/RespawnPacket.swift | 22 +++++++++++++------ .../Play/Clientbound/UpdateHealthPacket.swift | 13 ++++++----- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 659eda54..70f4fd03 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -544,6 +544,17 @@ public final class Game: @unchecked Sendable { // TODO: Make this threadsafe self.world = newWorld tickScheduler.setWorld(to: newWorld) + + nexusLock.acquireWriteLock() + defer { nexusLock.unlock() } + + entityIdToEntityIdentifier = entityIdToEntityIdentifier.filter { (id, identifier) in + let isClientPlayer = id == player.entityId.id + if !isClientPlayer { + nexus.destroy(entityId: identifier) + } + return isClientPlayer + } } /// Stops the tick scheduler. diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift index e49177c8..379765ad 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift @@ -31,6 +31,7 @@ public struct ChangeGameStatePacket: ClientboundPacket { } public func handle(for client: Client) throws { + print("Received ChangeGameStatePacket with reason \(reason)") switch reason { case .changeGamemode: let rawValue = Int8(value) diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift index 01f369b1..c2aa3ca3 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityStatusPacket.swift @@ -19,8 +19,10 @@ public struct EntityStatusPacket: ClientboundEntityPacket { /// Should only be called if a nexus lock has already been acquired. public func handle(for client: Client) throws { if status == .death { - // TODO: Play a death animation instead of instantly removing entities on death - client.game.removeEntity(acquireLock: false, id: entityId) + if entityId != client.game.accessPlayer(acquireLock: false, action: \.entityId.id) { + // TODO: Play a death animation instead of instantly removing entities on death + client.game.removeEntity(acquireLock: false, id: entityId) + } } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift index 225ff641..7c5062c0 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift @@ -7,9 +7,9 @@ public enum RespawnPacketError: LocalizedError { switch self { case .invalidPreviousGamemodeRawValue(let rawValue): return """ - Invalid previous gamemode raw value. - Raw value: \(rawValue) - """ + Invalid previous gamemode raw value. + Raw value: \(rawValue) + """ } } } @@ -52,13 +52,17 @@ public struct RespawnPacket: ClientboundPacket { isDebug = try packetReader.readBool() isFlat = try packetReader.readBool() - copyMetadata = try packetReader.readBool() // TODO: not used yet + copyMetadata = try packetReader.readBool() // TODO: not used yet } public func handle(for client: Client) throws { - guard let currentDimension = client.game.dimensions.first(where: { dimension in - return dimension.identifier == currentDimensionIdentifier - }) else { + print("Received RespawnPacket") + + guard + let currentDimension = client.game.dimensions.first(where: { dimension in + return dimension.identifier == currentDimensionIdentifier + }) + else { throw ClientboundPacketError.invalidDimension(currentDimensionIdentifier) } @@ -81,6 +85,10 @@ public struct RespawnPacket: ClientboundPacket { client.game.accessPlayer { player in player.gamemode.gamemode = gamemode player.playerAttributes.previousGamemode = previousGamemode + + // Reset player physics + player.acceleration.vector = .zero + player.velocity.vector = .zero } // TODO: get auto respawn working diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/UpdateHealthPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/UpdateHealthPacket.swift index 073775ba..c3543755 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/UpdateHealthPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/UpdateHealthPacket.swift @@ -2,27 +2,28 @@ import Foundation public struct UpdateHealthPacket: ClientboundPacket { public static let id: Int = 0x49 - + public var health: Float public var food: Int public var foodSaturation: Float public init(from packetReader: inout PacketReader) throws { health = try packetReader.readFloat() - food = try packetReader.readVarInt() + food = try packetReader.readVarInt() foodSaturation = try packetReader.readFloat() } - + public func handle(for client: Client) throws { client.game.accessPlayer { player in player.health.health = health player.nutrition.food = food player.nutrition.saturation = foodSaturation } - + if health <= 0 { - // TODO: Handle death - log.debug("Died") + // TODO: Implement respawn screen (shown when world has respawnScreenEnabled), for now + // we just instantly respawn. + try client.sendPacket(ClientStatusPacket(action: .performRespawn)) } } } From 7e5d7afcd62a185f03119fd89f072e94b589af4c Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 6 Jun 2024 12:43:26 +1000 Subject: [PATCH 63/84] Add 'kind' property to window areas to give certain areas meaning (e.g. crafting result). Prevent invalid interactions with armor slots and crafting result slots --- Readme.md | 4 +- .../ECS/Components/PlayerInventory.swift | 9 +- Sources/Core/Sources/GUI/Window.swift | 168 +++++++--- Sources/Core/Sources/GUI/WindowArea.swift | 53 +++ Sources/Core/Sources/GUI/WindowType.swift | 308 +++++++++--------- Sources/Core/Sources/Registry/Item/Item.swift | 19 +- 6 files changed, 341 insertions(+), 220 deletions(-) diff --git a/Readme.md b/Readme.md index ee7746f7..92910b75 100644 --- a/Readme.md +++ b/Readme.md @@ -99,10 +99,10 @@ Not every version will be perfectly supported but I will try and have the most p - [x] [Sky box](https://github.com/stackotter/delta-client/pull/188) - [ ] Entities - [x] Basic entity rendering (just coloured cubes) - - [ ] Render entity models - - [ ] Entity animations + - [x] Render entity models - [ ] Block entities (e.g. chests) - [ ] Item entities + - [ ] Entity animations - [ ] GUI - [x] Chat - [x] F3-style stuff diff --git a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift index 45b54b08..bcb7b5a8 100644 --- a/Sources/Core/Sources/ECS/Components/PlayerInventory.swift +++ b/Sources/Core/Sources/ECS/Components/PlayerInventory.swift @@ -31,21 +31,24 @@ public class PlayerInventory: Component { startIndex: 1, width: 2, height: 2, - position: Vec2i(98, 18) + position: Vec2i(98, 18), + kind: .smallCraftingRecipeInput ) public static let craftingResultArea = WindowArea( startIndex: 0, width: 1, height: 1, - position: Vec2i(154, 28) + position: Vec2i(154, 28), + kind: .recipeResult ) public static let armorArea = WindowArea( startIndex: 5, width: 1, height: 4, - position: Vec2i(8, 8) + position: Vec2i(8, 8), + kind: .armor ) public static let offHandArea = WindowArea( diff --git a/Sources/Core/Sources/GUI/Window.swift b/Sources/Core/Sources/GUI/Window.swift index c8f1d126..59b31f86 100644 --- a/Sources/Core/Sources/GUI/Window.swift +++ b/Sources/Core/Sources/GUI/Window.swift @@ -46,11 +46,31 @@ public class Window { return rows } - public func leftClick(_ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection?) { + /// Gets the window area corresponding to the given slot and the position of the slot in the area, if any. + public func area(containing slotIndex: Int) -> (area: WindowArea, position: Vec2i)? { + for area in type.areas { + guard let position = area.position(ofWindowSlot: slotIndex) else { + continue + } + return (area, position) + } + return nil + } + + public func leftClick( + _ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection? + ) { + guard let (area, slotPosition) = area(containing: slotIndex) else { + log.warning( + "No area of window of type '\(type.identifier)' contains the slot with index '\(slotIndex)'" + ) + return + } + let clickedItem = slots[slotIndex] if var slotStack = slots[slotIndex].stack, - var mouseStackCopy = mouseStack, - slotStack.itemId == mouseStackCopy.itemId + var mouseStackCopy = mouseStack, + slotStack.itemId == mouseStackCopy.itemId { guard let item = RegistryStore.shared.itemRegistry.item(withId: slotStack.itemId) @@ -58,31 +78,64 @@ public class Window { log.warning("Failed to get maximum stack size for item with id '\(slotStack.itemId)'") return } - let total = slotStack.count + mouseStackCopy.count - slotStack.count = min(total, item.maximumStackSize) - slots[slotIndex].stack = slotStack - if slotStack.count == total { - mouseStack = nil + + // If clicking on a recipe result, take result if possible. + if area.kind == .recipeResult { + // Only take if we can take the whole result + if slotStack.count <= item.maximumStackSize - mouseStackCopy.count { + slots[slotIndex].stack = nil + mouseStackCopy.count += slotStack.count + mouseStack = mouseStackCopy + } } else { - mouseStackCopy.count = total - slotStack.count - mouseStack = mouseStackCopy + let total = slotStack.count + mouseStackCopy.count + slotStack.count = min(total, item.maximumStackSize) + slots[slotIndex].stack = slotStack + if slotStack.count == total { + mouseStack = nil + } else { + mouseStackCopy.count = total - slotStack.count + mouseStack = mouseStackCopy + } + } + } else if area.kind != .recipeResult { + if area.kind == .armor, let mouseStackCopy = mouseStack { + guard + let item = RegistryStore.shared.itemRegistry.item(withId: mouseStackCopy.itemId) + else { + log.warning("Failed to get armor properties for item with id '\(mouseStackCopy.itemId)'") + return + } + + // TODO: Allow heads and carved pumpkings to be warn (should be easy, just need an exhaustive + // list). + // Ensure that armor of the correct kind (boots etc) can be put in an armor slot + let isValid = item.properties?.armorProperties?.equipmentSlot.index == slotPosition.y + if isValid { + swap(&slots[slotIndex].stack, &mouseStack) + } + } else { + swap(&slots[slotIndex].stack, &mouseStack) } - } else { - swap(&slots[slotIndex].stack, &mouseStack) } + do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(id), - actionId: generateActionId(), - action: .leftClick(slot: Int16(slotIndex)), - clickedItem: clickedItem - )) + try connection?.sendPacket( + ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .leftClick(slot: Int16(slotIndex)), + clickedItem: clickedItem + ) + ) } catch { log.warning("Failed to send click window packet for inventory left click: \(error)") } } - public func rightClick(_ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection?) { + public func rightClick( + _ slotIndex: Int, mouseStack: inout ItemStack?, connection: ServerConnection? + ) { let clickedItem = slots[slotIndex] if var stack = slots[slotIndex].stack, mouseStack == nil { let total = stack.count @@ -104,7 +157,7 @@ public class Window { mouseStack = stack } } else if let slotStack = slots[slotIndex].stack, - slotStack.itemId == mouseStack?.itemId + slotStack.itemId == mouseStack?.itemId { slots[slotIndex].stack?.count += 1 mouseStack?.count -= 1 @@ -116,12 +169,13 @@ public class Window { } do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(id), - actionId: generateActionId(), - action: .rightClick(slot: Int16(slotIndex)), - clickedItem: clickedItem - )) + try connection?.sendPacket( + ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .rightClick(slot: Int16(slotIndex)), + clickedItem: clickedItem + )) } catch { log.warning("Failed to send click window packet for inventory right click: \(error)") } @@ -140,7 +194,9 @@ public class Window { return false } - let slotInputs: [Input] = [.slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9] + let slotInputs: [Input] = [ + .slot1, .slot2, .slot3, .slot4, .slot5, .slot6, .slot7, .slot8, .slot9, + ] if input == .dropItem { let dropWholeStack = inputState.keys.contains(where: \.isControl) @@ -161,12 +217,13 @@ public class Window { } do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(id), - actionId: generateActionId(), - action: .numberKey(slot: Int16(slotIndex), number: Int8(hotBarSlot)), - clickedItem: clickedItem - )) + try connection?.sendPacket( + ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: .numberKey(slot: Int16(slotIndex), number: Int8(hotBarSlot)), + clickedItem: clickedItem + )) } catch { log.warning("Failed to send click window packet for inventory right click: \(error)") } @@ -177,18 +234,26 @@ public class Window { return true } - public func close(mouseStack: inout ItemStack?, eventBus: EventBus, connection: ServerConnection?) throws { + public func close(mouseStack: inout ItemStack?, eventBus: EventBus, connection: ServerConnection?) + throws + { mouseStack = nil eventBus.dispatch(CaptureCursorEvent()) try connection?.sendPacket(CloseWindowServerboundPacket(windowId: UInt8(id))) } - public func dropItemFromSlot(_ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection?) { - dropFromSlot(slotIndex, wholeStack: false, mouseItemStack: mouseItemStack, connection: connection) + public func dropItemFromSlot( + _ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection? + ) { + dropFromSlot( + slotIndex, wholeStack: false, mouseItemStack: mouseItemStack, connection: connection) } - public func dropStackFromSlot(_ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection?) { - dropFromSlot(slotIndex, wholeStack: true, mouseItemStack: mouseItemStack, connection: connection) + public func dropStackFromSlot( + _ slotIndex: Int, mouseItemStack: ItemStack?, connection: ServerConnection? + ) { + dropFromSlot( + slotIndex, wholeStack: true, mouseItemStack: mouseItemStack, connection: connection) } public func dropItemFromMouse(_ mouseStack: inout ItemStack?, connection: ServerConnection?) { @@ -217,12 +282,13 @@ public class Window { } do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(id), - actionId: generateActionId(), - action: wholeStack ? .leftClick(slot: nil) : .rightClick(slot: nil), - clickedItem: slot - )) + try connection?.sendPacket( + ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: wholeStack ? .leftClick(slot: nil) : .rightClick(slot: nil), + clickedItem: slot + )) } catch { log.warning("Failed to send click window packet for item drop: \(error)") } @@ -248,12 +314,14 @@ public class Window { } do { - try connection?.sendPacket(ClickWindowPacket( - windowId: UInt8(id), - actionId: generateActionId(), - action: wholeStack ? .dropStack(slot: Int16(slotIndex)) : .dropOne(slot: Int16(slotIndex)), - clickedItem: Slot(ItemStack(itemId: -1, itemCount: 1)) - )) + try connection?.sendPacket( + ClickWindowPacket( + windowId: UInt8(id), + actionId: generateActionId(), + action: wholeStack + ? .dropStack(slot: Int16(slotIndex)) : .dropOne(slot: Int16(slotIndex)), + clickedItem: Slot(ItemStack(itemId: -1, itemCount: 1)) + )) } catch { log.warning("Failed to send click window packet for item drop: \(error)") } diff --git a/Sources/Core/Sources/GUI/WindowArea.swift b/Sources/Core/Sources/GUI/WindowArea.swift index 2964b633..8fd88660 100644 --- a/Sources/Core/Sources/GUI/WindowArea.swift +++ b/Sources/Core/Sources/GUI/WindowArea.swift @@ -9,4 +9,57 @@ public struct WindowArea { public var height: Int /// The position of the area within its window. public var position: Vec2i + /// The kind of window area (determines its behaviour). + public var kind: Kind? + + public init( + startIndex: Int, + width: Int, + height: Int, + position: Vec2i, + kind: WindowArea.Kind? = nil + ) { + self.startIndex = startIndex + self.width = width + self.height = height + self.position = position + self.kind = kind + } + + /// Gets the position of a slot (given as an index in the window's slots array) + /// if it lies within this area. + public func position(ofWindowSlot slotIndex: Int) -> Vec2i? { + let endIndex = startIndex + width * height + if slotIndex >= startIndex && slotIndex < endIndex { + let position = Vec2i( + (slotIndex - startIndex) % width, + (slotIndex - startIndex) / width + ) + return position + } + return nil + } + + public enum Kind { + /// A 9x3 area synced with the player's inventory. + case inventorySynced + /// A 9x1 area synced with the player's hotbar. + case hotbarSynced + /// A full 3x3 crafting recipe input area. + case fullCraftingRecipeInput + /// A small 2x2 crafting recipe input area. + case smallCraftingRecipeInput + /// A 1x1 heat recipe (e.g. furnace recipe) input area. + case heatRecipeInput + /// A 1x1 recipe result output area. + case recipeResult + /// A 1x1 heat recipe fuel input area (e.g. furnace fuel slot). + case heatRecipeFuel + /// The 1x1 anvil input slot on the left. + case firstAnvilInput + /// The 1x1 anvil input slot on the right. + case secondAnvilInput + /// The 1x4 armor area in the player inventory. + case armor + } } diff --git a/Sources/Core/Sources/GUI/WindowType.swift b/Sources/Core/Sources/GUI/WindowType.swift index 58cc71b9..4697684b 100644 --- a/Sources/Core/Sources/GUI/WindowType.swift +++ b/Sources/Core/Sources/GUI/WindowType.swift @@ -12,6 +12,27 @@ public struct WindowType { case vanilla(Int) } + /// The window types understood by vanilla. + public static let types = [Id: Self]( + values: [ + inventory, + craftingTable, + anvil, + furnace, + blastFurnace, + smoker, + beacon, + generic9x1, + generic9x2, + generic9x3, + generic9x4, + generic9x5, + generic9x6, + generic3x3, + ], + keyedBy: \.id + ) + /// The player's inventory. public static let inventory = WindowType( id: .inventory, @@ -24,10 +45,11 @@ public struct WindowType { PlayerInventory.craftingInputArea, PlayerInventory.craftingResultArea, PlayerInventory.armorArea, - PlayerInventory.offHandArea + PlayerInventory.offHandArea, ] ) + /// A 3x3 crafting table. public static let craftingTable = WindowType( id: .vanilla(11), identifier: Identifier(namespace: "minecraft", name: "crafting"), @@ -38,213 +60,175 @@ public struct WindowType { startIndex: 0, width: 1, height: 1, - position: Vec2i(124, 35) + position: Vec2i(124, 35), + kind: .recipeResult ), WindowArea( startIndex: 1, width: 3, height: 3, - position: Vec2i(30, 17) + position: Vec2i(30, 17), + kind: .fullCraftingRecipeInput ), WindowArea( startIndex: 10, width: 9, height: 3, - position: Vec2i(8, 84) + position: Vec2i(8, 84), + kind: .inventorySynced ), WindowArea( startIndex: 37, width: 9, height: 1, - position: Vec2i(8, 142) + position: Vec2i(8, 142), + kind: .hotbarSynced ), ] ) + /// An anvil. public static let anvil = WindowType( id: .vanilla(7), identifier: Identifier(namespace: "minecraft", name: "crafting"), background: .sprite(.anvil), slotCount: 39, - areas:[ + areas: [ WindowArea( startIndex: 0, width: 1, height: 1, - position: Vec2i(27, 47) + position: Vec2i(27, 47), + kind: .firstAnvilInput ), WindowArea( startIndex: 1, width: 1, height: 1, - position: Vec2i(76, 47) + position: Vec2i(76, 47), + kind: .secondAnvilInput ), WindowArea( startIndex: 2, width: 1, height: 1, - position: Vec2i(134, 47) + position: Vec2i(134, 47), + kind: .recipeResult ), WindowArea( startIndex: 3, width: 9, height: 3, - position: Vec2i(8, 84) + position: Vec2i(8, 84), + kind: .inventorySynced ), WindowArea( startIndex: 30, width: 9, height: 1, - position: Vec2i(8, 142) - ) + position: Vec2i(8, 142), + kind: .hotbarSynced + ), ] ) + /// The areas of a heat recipe window (e.g. furnace, smoker, etc). + public static let heatRecipeWindowAreas: [WindowArea] = [ + WindowArea( + startIndex: 0, + width: 1, + height: 1, + position: Vec2i(56, 17), + kind: .heatRecipeInput + ), + WindowArea( + startIndex: 1, + width: 1, + height: 1, + position: Vec2i(56, 53), + kind: .heatRecipeFuel + ), + WindowArea( + startIndex: 2, + width: 1, + height: 1, + position: Vec2i(112, 31), + kind: .recipeResult + ), + WindowArea( + startIndex: 3, + width: 9, + height: 3, + position: Vec2i(8, 84), + kind: .inventorySynced + ), + WindowArea( + startIndex: 30, + width: 9, + height: 1, + position: Vec2i(8, 142), + kind: .hotbarSynced + ), + ] + + /// A regular furnace. public static let furnace = WindowType( id: .vanilla(13), identifier: Identifier(namespace: "minecraft", name: "furnace"), background: .sprite(.furnace), slotCount: 39, - areas: [ - WindowArea( - startIndex: 0, - width: 1, - height: 1, - position: Vec2i(56, 17) - ), - WindowArea( - startIndex: 1, - width: 1, - height: 1, - position: Vec2i(56, 53) - ), - WindowArea( - startIndex: 2, - width: 1, - height: 1, - position: Vec2i(112, 31) - ), - WindowArea( - startIndex: 3, - width: 9, - height: 3, - position: Vec2i(8, 84) - ), - WindowArea( - startIndex: 30, - width: 9, - height: 1, - position: Vec2i(8, 142) - ) - ] + areas: heatRecipeWindowAreas ) + /// A blast furnace. public static let blastFurnace = WindowType( id: .vanilla(9), identifier: Identifier(namespace: "minecraft", name: "blast_furnace"), background: .sprite(.blastFurnace), slotCount: 39, - areas: [ - WindowArea( - startIndex: 0, - width: 1, - height: 1, - position: Vec2i(56, 17) - ), - WindowArea( - startIndex: 1, - width: 1, - height: 1, - position: Vec2i(56, 53) - ), - WindowArea( - startIndex: 2, - width: 1, - height: 1, - position: Vec2i(112, 31) - ), - WindowArea( - startIndex: 3, - width: 9, - height: 3, - position: Vec2i(8, 84) - ), - WindowArea( - startIndex: 30, - width: 9, - height: 1, - position: Vec2i(8, 142) - ) - ] + areas: heatRecipeWindowAreas ) + /// A smoker. public static let smoker = WindowType( id: .vanilla(21), identifier: Identifier(namespace: "minecraft", name: "smoker"), background: .sprite(.smoker), slotCount: 39, + areas: heatRecipeWindowAreas + ) + + /// A beacon block interface. + public static let beacon = WindowType( + id: .vanilla(8), + identifier: Identifier(namespace: "minecraft", name: "beacon"), + background: .sprite(.beacon), + slotCount: 37, areas: [ WindowArea( startIndex: 0, width: 1, height: 1, - position: Vec2i(56, 17) + position: Vec2i(136, 110) ), WindowArea( startIndex: 1, - width: 1, - height: 1, - position: Vec2i(56, 53) - ), - WindowArea( - startIndex: 2, - width: 1, - height: 1, - position: Vec2i(112, 31) - ), - WindowArea( - startIndex: 3, width: 9, height: 3, - position: Vec2i(8, 84) + position: Vec2i(36, 137), + kind: .inventorySynced ), WindowArea( - startIndex: 30, + startIndex: 28, width: 9, height: 1, - position: Vec2i(8, 142) - ) + position: Vec2i(36, 196), + kind: .hotbarSynced + ), ] ) - public static let beacon = WindowType( - id: .vanilla(8), - identifier: Identifier(namespace: "minecraft", name: "beacon"), - background: .sprite(.beacon), - slotCount: 37, - areas: [ - WindowArea( - startIndex: 0, - width: 1, - height: 1, - position: Vec2i(136, 110) - ), - WindowArea( - startIndex: 1, - width: 9, - height: 3, - position: Vec2i(36, 137) - ), - WindowArea( - startIndex: 28, - width: 9, - height: 1, - position: Vec2i(36, 196) - ) - ] - ) - - // Dispenser & dropper + // A dispenser or dropper. public static let generic3x3 = WindowType( id: .vanilla(6), identifier: Identifier(namespace: "minecraft", name: "generic_3x3"), @@ -261,18 +245,20 @@ public struct WindowType { startIndex: 9, width: 9, height: 3, - position: Vec2i(8, 84) + position: Vec2i(8, 84), + kind: .inventorySynced ), WindowArea( startIndex: 36, width: 9, height: 1, - position: Vec2i(8, 142) - ) + position: Vec2i(8, 142), + kind: .hotbarSynced + ), ] ) - // Generic window types + /// A 1-row container. public static let generic9x1 = WindowType( id: .vanilla(0), identifier: Identifier(namespace: "minecraft", name: "generic_9x1"), @@ -292,17 +278,20 @@ public struct WindowType { startIndex: 9, width: 9, height: 3, - position: Vec2i(8, 50) + position: Vec2i(8, 50), + kind: .inventorySynced ), WindowArea( startIndex: 36, width: 9, height: 1, - position: Vec2i(8, 108) - ) + position: Vec2i(8, 108), + kind: .hotbarSynced + ), ] ) + /// A 2-row container. public static let generic9x2 = WindowType( id: .vanilla(1), identifier: Identifier(namespace: "minecraft", name: "generic_9x2"), @@ -322,17 +311,20 @@ public struct WindowType { startIndex: 18, width: 9, height: 3, - position: Vec2i(8, 68) + position: Vec2i(8, 68), + kind: .inventorySynced ), WindowArea( startIndex: 45, width: 9, height: 1, - position: Vec2i(8, 126) - ) + position: Vec2i(8, 126), + kind: .hotbarSynced + ), ] ) + /// A 3-row container (e.g. a single chest). public static let generic9x3 = WindowType( id: .vanilla(2), identifier: Identifier(namespace: "minecraft", name: "generic_9x3"), @@ -352,17 +344,20 @@ public struct WindowType { startIndex: 27, width: 9, height: 3, - position: Vec2i(8, 86) + position: Vec2i(8, 86), + kind: .inventorySynced ), WindowArea( startIndex: 54, width: 9, height: 1, - position: Vec2i(8, 144) + position: Vec2i(8, 144), + kind: .hotbarSynced ), ] ) + /// A 4-row container. public static let generic9x4 = WindowType( id: .vanilla(3), identifier: Identifier(namespace: "minecraft", name: "generic_9x4"), @@ -382,20 +377,23 @@ public struct WindowType { startIndex: 36, width: 9, height: 3, - position: Vec2i(8, 104) + position: Vec2i(8, 104), + kind: .inventorySynced ), WindowArea( startIndex: 63, width: 9, height: 1, - position: Vec2i(8, 162) - ) + position: Vec2i(8, 162), + kind: .hotbarSynced + ), ] ) + /// A 4-row container. public static let generic9x5 = WindowType( id: .vanilla(4), - identifier: Identifier(namespace: "minecraft", name:"generic_9x5"), + identifier: Identifier(namespace: "minecraft", name: "generic_9x5"), background: GUIElement.list(spacing: 0) { GUIElement.sprite(.generic9x5) GUIElement.sprite(.genericInventory) @@ -412,17 +410,20 @@ public struct WindowType { startIndex: 45, width: 9, height: 3, - position: Vec2i(8, 122) + position: Vec2i(8, 122), + kind: .inventorySynced ), WindowArea( startIndex: 72, width: 9, height: 1, - position: Vec2i(8, 180) - ) + position: Vec2i(8, 180), + kind: .hotbarSynced + ), ] ) + /// A 6-row container (e.g. a double chest). public static let generic9x6 = WindowType( id: .vanilla(5), identifier: Identifier(namespace: "minecraft", name: "generic_9x6"), @@ -441,35 +442,16 @@ public struct WindowType { startIndex: 54, width: 9, height: 3, - position: Vec2i(8, 140) + position: Vec2i(8, 140), + kind: .inventorySynced ), WindowArea( startIndex: 81, width: 9, height: 1, - position: Vec2i(8, 198) + position: Vec2i(8, 198), + kind: .hotbarSynced ), ] ) - - /// The window types understood by vanilla. - public static let types = [Id: Self]( - values: [ - inventory, - craftingTable, - anvil, - furnace, - blastFurnace, - smoker, - beacon, - generic9x1, - generic9x2, - generic9x3, - generic9x4, - generic9x5, - generic9x6, - generic3x3 - ], - keyedBy: \.id - ) } diff --git a/Sources/Core/Sources/Registry/Item/Item.swift b/Sources/Core/Sources/Registry/Item/Item.swift index 6d1b9300..aa326066 100644 --- a/Sources/Core/Sources/Registry/Item/Item.swift +++ b/Sources/Core/Sources/Registry/Item/Item.swift @@ -68,6 +68,20 @@ public struct Item: Codable { case chest case legs case feet + + /// The index of the slot corresponding + public var index: Int { + switch self { + case .head: + return 0 + case .chest: + return 1 + case .legs: + return 2 + case .feet: + return 3 + } + } } } @@ -112,7 +126,7 @@ public struct Item: Codable { attackDamageBonus: Double, enchantmentValue: Int, mineableBlocks: [Int], - blockInteractions: [Int : Int], + blockInteractions: [Int: Int], kind: Item.ToolProperties.ToolKind, effectiveMaterials: [Identifier] ) { @@ -131,7 +145,8 @@ public struct Item: Codable { public func destroySpeedMultiplier(for block: Block) -> Double { switch kind { case .sword: - let swordSemiEffectiveMaterials = ["plant", "replaceable_plant"].map(Identifier.init(name:)) + let swordSemiEffectiveMaterials = ["plant", "replaceable_plant"].map( + Identifier.init(name:)) if block.className == "CobwebBlock" { return 0.15 } else if swordSemiEffectiveMaterials.contains(block.vanillaMaterialIdentifier) { From dc62c995eafb537c1c38630026c3c9254303304d Mon Sep 17 00:00:00 2001 From: stackotter Date: Fri, 7 Jun 2024 18:57:45 +1000 Subject: [PATCH 64/84] Parse whole EntityMetadataPacket and use metadata to avoid applying velocity to NoAI entities --- .../ECS/Components/EntityAttributes.swift | 3 +- .../ECS/Components/EntityMetadata.swift | 12 + .../ECS/Systems/EntityMovementSystem.swift | 7 +- Sources/Core/Sources/Game.swift | 8 +- .../Protocol/Packets/ClientboundPacket.swift | 86 ++++--- .../Protocol/Packets/PacketReader.swift | 33 ++- .../Protocol/Packets/PacketReaderError.swift | 1 + .../Clientbound/EntityMetadataPacket.swift | 214 +++++++++++++++++- .../EntityPositionAndRotationPacket.swift | 5 +- .../Clientbound/EntityPositionPacket.swift | 2 +- .../Clientbound/EntityRotationPacket.swift | 3 + .../Clientbound/EntityVelocityPacket.swift | 1 + .../Play/Clientbound/SpawnEntityPacket.swift | 1 + .../Clientbound/SpawnLivingEntityPacket.swift | 1 + .../Play/Clientbound/SpawnPlayerPacket.swift | 1 + Sources/Core/Sources/Player/Player.swift | 16 +- .../Sources/Registry/Entity/EntityKind.swift | 11 +- .../Registry/Pixlyzer/PixlyzerEntity.swift | 15 +- .../Registry/Pixlyzer/PixlyzerFormatter.swift | 98 +++++--- 19 files changed, 421 insertions(+), 97 deletions(-) create mode 100644 Sources/Core/Sources/ECS/Components/EntityMetadata.swift 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) From 61885c4b01c02ab9ec3e5e021f51894eb2bd4337 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 02:28:08 +1000 Subject: [PATCH 65/84] Implement dragon subhitboxes (kind of working, kind of not, will need entity animations to work fully) --- .../Core/Renderer/Entity/EntityRenderer.swift | 3 +- .../Renderer/Mesh/EntityMeshBuilder.swift | 25 +++-- .../ECS/Components/EnderDragonParts.swift | 98 +++++++++++++++++++ .../ECS/Systems/PlayerInputSystem.swift | 35 ++++++- Sources/Core/Sources/Game.swift | 27 ++--- .../Clientbound/SpawnLivingEntityPacket.swift | 49 +++++++--- .../Sources/Registry/Entity/EntityKind.swift | 6 ++ 7 files changed, 207 insertions(+), 36 deletions(-) create mode 100644 Sources/Core/Sources/ECS/Components/EnderDragonParts.swift diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index 4828e99c..7162373d 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -126,7 +126,7 @@ public struct EntityRenderer: Renderer { // Create uniforms for each entity profiler.push(.createUniforms) - for (position, rotation, hitbox, kindId) in entities { + for (entity, position, rotation, hitbox, kindId) in entities.entityAndComponents { // Don't render entities that are outside of the render distance let chunkPosition = position.chunk if !chunkPosition.isWithinRenderDistance(renderDistance, of: cameraChunk) { @@ -143,6 +143,7 @@ public struct EntityRenderer: Renderer { } let builder = EntityMeshBuilder( + entity: entity, entityKind: kindIdentifier, position: Vec3f(position.smoothVector), pitch: rotation.smoothPitch, diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index 9d41bf24..54046e18 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -1,22 +1,24 @@ import CoreFoundation import DeltaCore +import FirebladeECS public struct EntityMeshBuilder { /// Associates entity kinds with hardcoded entity texture identifiers. Used to manually /// instruct Delta Client where to find certain textures that aren't in the standard /// locations. - static let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ + public static let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ Identifier(name: "player"): Identifier(name: "entity/steve"), Identifier(name: "dragon"): Identifier(name: "entity/enderdragon/dragon"), ] - let entityKind: Identifier - let position: Vec3f - let pitch: Float - let yaw: Float - let entityModelPalette: EntityModelPalette - let texturePalette: MetalTexturePalette - let hitbox: AxisAlignedBoundingBox + public let entity: Entity + public let entityKind: Identifier + public let position: Vec3f + public let pitch: Float + public let yaw: Float + public let entityModelPalette: EntityModelPalette + public let texturePalette: MetalTexturePalette + public let hitbox: AxisAlignedBoundingBox static let colors: [Vec3f] = [ [1, 0, 0], @@ -35,6 +37,13 @@ public struct EntityMeshBuilder { } else { buildAABB(hitbox, into: &geometry) } + + if let dragonParts = entity.get(component: EnderDragonParts.self) { + for part in dragonParts.parts { + let aabb = part.aabb(withParentPosition: Vec3d(position)) + buildAABB(aabb, into: &geometry) + } + } } func buildAABB(_ aabb: AxisAlignedBoundingBox, into geometry: inout Geometry) { diff --git a/Sources/Core/Sources/ECS/Components/EnderDragonParts.swift b/Sources/Core/Sources/ECS/Components/EnderDragonParts.swift new file mode 100644 index 00000000..f9c116b8 --- /dev/null +++ b/Sources/Core/Sources/ECS/Components/EnderDragonParts.swift @@ -0,0 +1,98 @@ +import FirebladeECS + +public class EnderDragonParts: Component { + public var head = Part( + .head, + size: Vec2d(1, 1), + entityIdOffset: 1, + relativePosition: Vec3d(-0.5, 0, 1) + ) + public var neck = Part( + .neck, + size: Vec2d(3, 3), + entityIdOffset: 2, + relativePosition: Vec3d(-1.5, -1, -2) + ) + public var body = Part( + .body, + size: Vec2d(5, 3), + entityIdOffset: 3, + relativePosition: Vec3d(-2.5, -1, -4) + ) + // TODO: Verify the entity id offsets of these + public var upperTail = Part( + .tail, + size: Vec2d(2, 2), + entityIdOffset: 4, + relativePosition: Vec3d(-1, -0.5, -5) + ) + public var midTail = Part( + .tail, + size: Vec2d(2, 2), + entityIdOffset: 5, + relativePosition: Vec3d(-1, -0.5, -6) + ) + public var lowerTail = Part( + .tail, + size: Vec2d(2, 2), + entityIdOffset: 6, + relativePosition: Vec3d(-1, -0.5, -7) + ) + public var leftWing = Part( + .wing, + size: Vec2d(4, 2), + entityIdOffset: 7, + relativePosition: Vec3d(2, 0, -4) + ) + public var rightWing = Part( + .wing, + size: Vec2d(4, 2), + entityIdOffset: 8, + relativePosition: Vec3d(-6, 0, -4) + ) + + /// All parts. + public var parts: [Part] { + [head, neck, body, upperTail, midTail, lowerTail, leftWing, rightWing] + } + + public init() {} + + public struct Part { + /// Group that this part is a member of (e.g. tail). + public var group: Group + /// The size of the part's hitbox as a width (x and z) and a height (y). + public var size: Vec2d + /// The offset of this part's entity id from the parent entity's id. + public var entityIdOffset: Int + /// Position relative to parent entity. + public var relativePosition: Vec3d + + public enum Group { + case head + case neck + case body + case tail + case wing + } + + public init( + _ group: Group, + size: Vec2d, + entityIdOffset: Int, + relativePosition: Vec3d + ) { + self.group = group + self.size = size + self.entityIdOffset = entityIdOffset + self.relativePosition = relativePosition + } + + public func aabb(withParentPosition parentPosition: Vec3d) -> AxisAlignedBoundingBox { + AxisAlignedBoundingBox( + position: parentPosition + relativePosition, + size: Vec3d(size.x, size.y, size.x) + ) + } + } +} diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 1c6b9297..2df35c48 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -199,9 +199,42 @@ public final class PlayerInputSystem: System { break } + var entityId = targetedEntity.target + if let dragonParts = game.accessComponent( + entityId: targetedEntity.target, + EnderDragonParts.self, + acquireLock: false, + action: identity + ) { + guard + let dragonPosition = game.accessComponent( + entityId: targetedEntity.target, + EntityPosition.self, + acquireLock: false, + action: identity + ) + else { + log.warning("Ender dragon missing position") + break + } + + let ray = game.accessPlayer(acquireLock: false, action: \.ray) + // TODO: Don't hardcode reach + var currentDistance: Float = 4 + for part in dragonParts.parts { + let aabb = part.aabb(withParentPosition: dragonPosition.vector) + if let (distance, _) = aabb.intersectionDistanceAndFace(with: ray), + distance < currentDistance + { + entityId = targetedEntity.target + part.entityIdOffset + currentDistance = distance + } + } + } + try connection?.sendPacket( InteractEntityPacket( - entityId: Int32(targetedEntity.target), + entityId: Int32(entityId), interaction: .attack(isSneaking: sneaking.isSneaking) ) ) diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index aa65434c..4af466d9 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -291,16 +291,18 @@ 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( + public func accessEntity( id: Int, acquireLock: Bool = true, - action: (Entity) throws -> Void - ) rethrows { + action: (Entity) throws -> R + ) rethrows -> R? { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } if let identifier = entityIdToEntityIdentifier[id] { - try action(nexus.entity(from: identifier)) + return try action(nexus.entity(from: identifier)) + } else { + return nil } } @@ -310,9 +312,12 @@ public final class Game: @unchecked Sendable { /// - componentType: The type of component to access. /// - acquireLock: If `false`, no lock is acquired. Only use if you know what you're doing. /// - action: The action to perform on the component if the entity exists and contains that component. - public func accessComponent( - entityId: Int, _ componentType: T.Type, acquireLock: Bool = true, action: (T) -> Void - ) { + public func accessComponent( + entityId: Int, + _ componentType: T.Type, + acquireLock: Bool = true, + action: (T) throws -> R + ) rethrows -> R? { if acquireLock { nexusLock.acquireWriteLock() } defer { if acquireLock { nexusLock.unlock() } } @@ -320,10 +325,10 @@ public final class Game: @unchecked Sendable { let identifier = entityIdToEntityIdentifier[entityId], let component = nexus.entity(from: identifier).get(component: T.self) else { - return + return nil } - action(component) + return try action(component) } /// Removes the entity with the given vanilla id from the game if it exists. @@ -459,8 +464,8 @@ public final class Game: @unchecked Sendable { } guard - let (distance, face) = hitbox.aabb(at: position.vector).intersectionDistanceAndFace( - with: playerRay) + let (distance, face) = hitbox.aabb(at: position.vector) + .intersectionDistanceAndFace(with: playerRay) else { continue } 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 24962080..053aeab4 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift @@ -24,25 +24,44 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { } public func handle(for client: Client) { - guard let entity = RegistryStore.shared.entityRegistry.entity(withId: type) else { + guard let entityKind = RegistryStore.shared.entityRegistry.entity(withId: type) else { log.warning("Entity spawned with invalid type id: \(type)") return } - client.game.createEntity(id: entityId) { - LivingEntity() // Mark it as a living entity - EntityKindId(type) - EntityId(entityId) - EntityUUID(entityUUID) - EntityOnGround(true) - EntityPosition(position) - EntityVelocity(velocity) - EntityHitBox(width: entity.width, height: entity.height) - EntityRotation(pitch: pitch, yaw: yaw) - EntityHeadYaw(headYaw) - EntityLerpState() - EntityAttributes() - EntityMetadata() + if entityKind.isEnderDragon { + client.game.createEntity(id: entityId) { + LivingEntity() // Mark it as a living entity + EntityKindId(type) + EntityId(entityId) + EntityUUID(entityUUID) + EntityOnGround(true) + EntityPosition(position) + EntityVelocity(velocity) + EntityHitBox(width: entityKind.width, height: entityKind.height) + EntityRotation(pitch: pitch, yaw: yaw) + EntityHeadYaw(headYaw) + EntityLerpState() + EntityAttributes() + EntityMetadata() + EnderDragonParts() + } + } else { + client.game.createEntity(id: entityId) { + LivingEntity() // Mark it as a living entity + EntityKindId(type) + EntityId(entityId) + EntityUUID(entityUUID) + EntityOnGround(true) + EntityPosition(position) + EntityVelocity(velocity) + EntityHitBox(width: entityKind.width, height: entityKind.height) + EntityRotation(pitch: pitch, yaw: yaw) + EntityHeadYaw(headYaw) + EntityLerpState() + EntityAttributes() + EntityMetadata() + } } } } diff --git a/Sources/Core/Sources/Registry/Entity/EntityKind.swift b/Sources/Core/Sources/Registry/Entity/EntityKind.swift index b4f00b9c..b95cbf09 100644 --- a/Sources/Core/Sources/Registry/Entity/EntityKind.swift +++ b/Sources/Core/Sources/Registry/Entity/EntityKind.swift @@ -28,6 +28,12 @@ public struct EntityKind: Codable { } } + /// Whether this entity kind is the ender dragon or not (purely a convenience property, + /// just checks the identifier). + public var isEnderDragon: Bool { + identifier == Identifier(name: "ender_dragon") + } + /// Creates a new entity kind with the given properties. public init( identifier: Identifier, From 92764c725d1ed45a8f59088ad54e2a4218dfc91e Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 10:05:17 +1000 Subject: [PATCH 66/84] Fix entity hitbox intersection (could target entities behind the player before) --- .../Sources/ECS/Systems/PlayerInputSystem.swift | 6 ++++-- Sources/Core/Sources/Game.swift | 16 +++++++++++----- .../Sources/Physics/AxisAlignedBoundingBox.swift | 15 ++++++++++----- Sources/Core/Sources/Player/Player.swift | 3 +++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index 2df35c48..bc95a55f 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -219,8 +219,7 @@ public final class PlayerInputSystem: System { } let ray = game.accessPlayer(acquireLock: false, action: \.ray) - // TODO: Don't hardcode reach - var currentDistance: Float = 4 + var currentDistance: Float = Player.attackReach for part in dragonParts.parts { let aabb = part.aabb(withParentPosition: dragonPosition.vector) if let (distance, _) = aabb.intersectionDistanceAndFace(with: ray), @@ -230,6 +229,9 @@ public final class PlayerInputSystem: System { currentDistance = distance } } + + print(entityId, targetedEntity.target) + print(currentDistance) } try connection?.sendPacket( diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 4af466d9..01028370 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -409,10 +409,11 @@ public final class Game: @unchecked Sendable { for position in VoxelRay(along: ray, count: 7) { let block = world.getBlock(at: position, acquireLock: acquireLock) + print(position, block.identifier) let boundingBox = block.shape.outlineShape.offset(by: position.doubleVector) if let (distance, face) = boundingBox.intersectionDistanceAndFace(with: ray) { - // TODO: Don't hardcode reach here - guard distance <= 6 else { + print(distance) + guard distance <= Player.buildingReach else { break } @@ -459,13 +460,16 @@ public final class Game: @unchecked Sendable { var candidate: Targeted? for (id, position, hitbox) in family { - guard (playerPosition - position.vector).magnitude < 4 else { + // Should be big enough radius not to accidentally exclude big entities such as the Ender Dragon? + guard (playerPosition - position.vector).magnitude < 12 else { continue } + let aabb = hitbox.aabb(at: position.vector) guard - let (distance, face) = hitbox.aabb(at: position.vector) - .intersectionDistanceAndFace(with: playerRay) + let (distance, face) = aabb.intersectionDistanceAndFace(with: playerRay), + distance >= 0 || aabb.contains(Vec3d(playerRay.origin)), + distance <= Player.attackReach else { continue } @@ -486,6 +490,8 @@ public final class Game: @unchecked Sendable { } } + print(candidate) + return candidate } diff --git a/Sources/Core/Sources/Physics/AxisAlignedBoundingBox.swift b/Sources/Core/Sources/Physics/AxisAlignedBoundingBox.swift index dbd3359e..b863abac 100644 --- a/Sources/Core/Sources/Physics/AxisAlignedBoundingBox.swift +++ b/Sources/Core/Sources/Physics/AxisAlignedBoundingBox.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation /// An axis aligned bounding box used for efficient collisions and visibility checks. public struct AxisAlignedBoundingBox: Codable { @@ -184,10 +184,15 @@ public struct AxisAlignedBoundingBox: Codable { let otherMinimum = other.minimum let otherMaximum = other.maximum - return ( - minimum.x <= otherMaximum.x && maximum.x >= otherMinimum.x && - minimum.y <= otherMaximum.y && maximum.y >= otherMinimum.y && - minimum.z <= otherMaximum.z && maximum.z >= otherMinimum.z) + return + (minimum.x <= otherMaximum.x && maximum.x >= otherMinimum.x && minimum.y <= otherMaximum.y + && maximum.y >= otherMinimum.y && minimum.z <= otherMaximum.z && maximum.z >= otherMinimum.z) + } + + /// Checks whether the AABB contains a given point. + public func contains(_ point: Vec3d) -> Bool { + return minimum.x <= point.x && minimum.y <= point.y && minimum.z <= point.z + && point.x <= maximum.x && point.y <= maximum.y && point.z <= maximum.z } /// Checks whether the AABB intersects with the given AABB. diff --git a/Sources/Core/Sources/Player/Player.swift b/Sources/Core/Sources/Player/Player.swift index e1e3686f..05ffd350 100644 --- a/Sources/Core/Sources/Player/Player.swift +++ b/Sources/Core/Sources/Player/Player.swift @@ -6,6 +6,9 @@ import Foundation /// /// Please note that all components are classes. public struct Player { + public static let attackReach: Float = 3 + public static let buildingReach: Float = 4.5 + /// The component storing the player's entity id. public private(set) var entityId: EntityId /// The component storing whether the player is on the ground/swimming or not. From fc64383c093bcb61f021a41ddae7b00c9efd8ec9 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 12:36:01 +1000 Subject: [PATCH 67/84] Render entity item entities (e.g. chest item entities), and restructure entity metadata to clean things up a bit --- .../Core/Renderer/Entity/EntityRenderer.swift | 12 +++- .../Renderer/Mesh/EntityMeshBuilder.swift | 68 +++++++++++++++++-- .../ECS/Components/EntityMetadata.swift | 51 ++++++++++++-- .../ECS/Systems/EntityMovementSystem.swift | 2 +- .../ECS/Systems/PlayerInputSystem.swift | 3 - Sources/Core/Sources/Game.swift | 4 -- .../Clientbound/ChangeGameStatePacket.swift | 1 - .../Clientbound/EntityMetadataPacket.swift | 49 +++++++------ .../Play/Clientbound/RespawnPacket.swift | 2 - .../Play/Clientbound/SpawnEntityPacket.swift | 2 +- .../Clientbound/SpawnLivingEntityPacket.swift | 4 +- .../Play/Clientbound/SpawnPlayerPacket.swift | 2 +- Sources/Core/Sources/Player/Player.swift | 2 +- .../Registry/Pixlyzer/PixlyzerFormatter.swift | 14 ++-- 14 files changed, 162 insertions(+), 54 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index 7162373d..b46ce8df 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -22,6 +22,10 @@ public struct EntityRenderer: Renderer { private var entityTexturePalette: MetalTexturePalette + private var entityModelPalette: EntityModelPalette + private var itemModelPalette: ItemModelPalette + private var blockModelPalette: BlockModelPalette + /// The client that entities will be renderer for. private var client: Client /// The device that will be used to render. @@ -82,6 +86,10 @@ public struct EntityRenderer: Renderer { device: device, commandQueue: commandQueue ) + + entityModelPalette = client.resourcePack.vanillaResources.entityModelPalette + itemModelPalette = client.resourcePack.vanillaResources.itemModelPalette + blockModelPalette = client.resourcePack.vanillaResources.blockModelPalette } /// Renders all entity hit boxes using instancing. @@ -148,7 +156,9 @@ public struct EntityRenderer: Renderer { position: Vec3f(position.smoothVector), pitch: rotation.smoothPitch, yaw: rotation.smoothYaw, - entityModelPalette: client.resourcePack.vanillaResources.entityModelPalette, + entityModelPalette: entityModelPalette, + itemModelPalette: itemModelPalette, + blockModelPalette: blockModelPalette, texturePalette: entityTexturePalette, hitbox: hitbox.aabb(at: position.smoothVector) ) diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index 54046e18..a37c182c 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -1,6 +1,7 @@ import CoreFoundation import DeltaCore import FirebladeECS +import Foundation public struct EntityMeshBuilder { /// Associates entity kinds with hardcoded entity texture identifiers. Used to manually @@ -9,6 +10,7 @@ public struct EntityMeshBuilder { public static let hardcodedTextureIdentifiers: [Identifier: Identifier] = [ Identifier(name: "player"): Identifier(name: "entity/steve"), Identifier(name: "dragon"): Identifier(name: "entity/enderdragon/dragon"), + Identifier(name: "chest"): Identifier(name: "entity/chest/normal"), ] public let entity: Entity @@ -17,6 +19,8 @@ public struct EntityMeshBuilder { public let pitch: Float public let yaw: Float public let entityModelPalette: EntityModelPalette + public let itemModelPalette: ItemModelPalette + public let blockModelPalette: BlockModelPalette public let texturePalette: MetalTexturePalette public let hitbox: AxisAlignedBoundingBox @@ -34,6 +38,43 @@ public struct EntityMeshBuilder { func build(into geometry: inout Geometry) { if let model = entityModelPalette.models[entityKind] { buildModel(model, into: &geometry) + } else if let itemMetadata = entity.get(component: EntityMetadata.self)?.itemMetadata, + let itemStack = itemMetadata.slot.stack, + let itemModel = itemModelPalette.model(for: itemStack.itemId) + { + switch itemModel { + case let .entity(identifier, transforms): + // Remove identifier prefix (entity model palette doesn't have any `item/` or `entity/` prefixes). + var entityIdentifier = identifier + entityIdentifier.name = entityIdentifier.name.replacingOccurrences(of: "item/", with: "") + + guard let entityModel = entityModelPalette.models[entityIdentifier] else { + log.warning("Missing entity model for entity with '\(entityIdentifier)' (as item)") + return + } + + var transformation = transforms.ground + let time = CFAbsoluteTimeGetCurrent() * TickScheduler.defaultTicksPerSecond + let phaseOffset = Double(itemMetadata.bobbingPhaseOffset) + let bob = Float(Foundation.sin(time / 10 + phaseOffset)) + let scaleY = (transformation * Vec4f(0, 1, 0, 0)).magnitude + let verticalOffset = bob + 0.25 * scaleY + transformation *= MatrixUtil.translationMatrix(Vec3f(0, verticalOffset, 0)) + transformation *= MatrixUtil.rotationMatrix( + y: -Float((time / 20 + phaseOffset).remainder(dividingBy: 2 * .pi)) + ) + + buildModel( + entityModel, + textureIdentifier: entityIdentifier, + transformation: transformation, + into: &geometry + ) + case .blockModel, .layered: + buildAABB(hitbox, into: &geometry) + case .empty, .blockModel, .layered: + break + } } else { buildAABB(hitbox, into: &geometry) } @@ -76,20 +117,26 @@ public struct EntityMeshBuilder { } } - func buildModel(_ model: JSONEntityModel, into geometry: inout Geometry) { + func buildModel( + _ model: JSONEntityModel, + textureIdentifier: Identifier? = nil, + transformation: Mat4x4f = MatrixUtil.identity, + into geometry: inout Geometry + ) { + let baseTextureIdentifier = textureIdentifier ?? entityKind let texture: Int? - if let identifier = Self.hardcodedTextureIdentifiers[entityKind] { + if let identifier = Self.hardcodedTextureIdentifiers[baseTextureIdentifier] { texture = texturePalette.textureIndex(for: identifier) } else { // Entity textures can be in all sorts of structures so we just have a few // educated guesses for now. let textureIdentifier = Identifier( - namespace: entityKind.namespace, - name: "entity/\(entityKind.name)" + namespace: baseTextureIdentifier.namespace, + name: "entity/\(baseTextureIdentifier.name)" ) let nestedTextureIdentifier = Identifier( - namespace: entityKind.namespace, - name: "entity/\(entityKind.name)/\(entityKind.name)" + namespace: baseTextureIdentifier.namespace, + name: "entity/\(baseTextureIdentifier.name)/\(baseTextureIdentifier.name)" ) texture = texturePalette.textureIndex(for: textureIdentifier) @@ -97,7 +144,13 @@ public struct EntityMeshBuilder { } for (index, submodel) in model.models.enumerated() { - buildSubmodel(submodel, index: index, textureIndex: texture, into: &geometry) + buildSubmodel( + submodel, + index: index, + textureIndex: texture, + transformation: transformation, + into: &geometry + ) } } @@ -114,6 +167,7 @@ public struct EntityMeshBuilder { transformation = MatrixUtil.rotationMatrix(-MathUtil.radians(from: rotation)) * MatrixUtil.translationMatrix(translation) + * transformation } for box in submodel.boxes ?? [] { diff --git a/Sources/Core/Sources/ECS/Components/EntityMetadata.swift b/Sources/Core/Sources/ECS/Components/EntityMetadata.swift index 58916370..56fc8cf1 100644 --- a/Sources/Core/Sources/ECS/Components/EntityMetadata.swift +++ b/Sources/Core/Sources/ECS/Components/EntityMetadata.swift @@ -4,9 +4,52 @@ import FirebladeECS /// 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 + /// Metadata specific to a certain kind of entity. + public var specializedMetadata: SpecializedMetadata? - public init() {} + public var itemMetadata: ItemMetadata? { + switch specializedMetadata { + case let .item(metadata): return metadata + default: return nil + } + } + + public var mobMetadata: MobMetadata? { + switch specializedMetadata { + case let .mob(metadata): return metadata + default: return nil + } + } + + public enum SpecializedMetadata { + case item(ItemMetadata) + case mob(MobMetadata) + } + + public struct ItemMetadata { + public var slot = Slot() + /// The phase (in the periodic motion sense) of the item entity's bobbing animation. + /// Not part of the vanilla entity metadata, this is just a sensible place to store + /// this item entity property. + public var bobbingPhaseOffset: Float + + public init() { + bobbingPhaseOffset = Float.random(in: 0...1) * 2 * .pi + } + } + + public struct MobMetadata { + /// 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 + } + + /// Creates a + public init(inheritanceChain: [String]) { + if inheritanceChain.contains("ItemEntity") { + specializedMetadata = .item(ItemMetadata()) + } else if inheritanceChain.contains("MobEntity") { + specializedMetadata = .mob(MobMetadata()) + } + } } diff --git a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift index 31d29720..30f03dc4 100644 --- a/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/EntityMovementSystem.swift @@ -54,7 +54,7 @@ public struct EntityMovementSystem: System { velocity.vector.z = 0 } - if !metadata.noAI { + if metadata.mobMetadata?.noAI != true { position.move(by: velocity.vector) } } diff --git a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift index bc95a55f..15d8e6ca 100644 --- a/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift +++ b/Sources/Core/Sources/ECS/Systems/PlayerInputSystem.swift @@ -229,9 +229,6 @@ public final class PlayerInputSystem: System { currentDistance = distance } } - - print(entityId, targetedEntity.target) - print(currentDistance) } try connection?.sendPacket( diff --git a/Sources/Core/Sources/Game.swift b/Sources/Core/Sources/Game.swift index 01028370..abc05bc1 100644 --- a/Sources/Core/Sources/Game.swift +++ b/Sources/Core/Sources/Game.swift @@ -409,10 +409,8 @@ public final class Game: @unchecked Sendable { for position in VoxelRay(along: ray, count: 7) { let block = world.getBlock(at: position, acquireLock: acquireLock) - print(position, block.identifier) let boundingBox = block.shape.outlineShape.offset(by: position.doubleVector) if let (distance, face) = boundingBox.intersectionDistanceAndFace(with: ray) { - print(distance) guard distance <= Player.buildingReach else { break } @@ -490,8 +488,6 @@ public final class Game: @unchecked Sendable { } } - print(candidate) - return candidate } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift index 379765ad..e49177c8 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/ChangeGameStatePacket.swift @@ -31,7 +31,6 @@ public struct ChangeGameStatePacket: ClientboundPacket { } public func handle(for client: Client) throws { - print("Received ChangeGameStatePacket with reason \(reason)") switch reason { case .changeGamemode: let rawValue = Int8(value) 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 53882cdc..f6f003d0 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/EntityMetadataPacket.swift @@ -191,30 +191,41 @@ public struct EntityMetadataPacket: ClientboundPacket { 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 { + guard let metadataComponent = entity.get(component: EntityMetadata.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 + switch metadataComponent.specializedMetadata { + case var .mob(mobMetadata): + if entry.index == 14 { + guard case let .byte(flags) = entry.value else { + throw ClientboundPacketError.incorrectEntityMetadataDatatype( + property: "Mob.noAI", + expectedType: "byte", + value: entry.value + ) + } + + mobMetadata.noAI = flags & 0x01 == 0x01 + metadataComponent.specializedMetadata = .mob(mobMetadata) + } + case var .item(itemMetadata): + if entry.index == 7 { + guard case let .slot(slot) = entry.value else { + throw ClientboundPacketError.incorrectEntityMetadataDatatype( + property: "Item.slot", + expectedType: "slot", + value: entry.value + ) + } + + itemMetadata.slot = slot + metadataComponent.specializedMetadata = .item(itemMetadata) + } + case nil: + break } } } diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift index 7c5062c0..c71e0172 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/RespawnPacket.swift @@ -56,8 +56,6 @@ public struct RespawnPacket: ClientboundPacket { } public func handle(for client: Client) throws { - print("Received RespawnPacket") - guard let currentDimension = client.game.dimensions.first(where: { dimension in return dimension.identifier == currentDimensionIdentifier 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 eff98d50..740cd51b 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnEntityPacket.swift @@ -47,7 +47,7 @@ public struct SpawnEntityPacket: ClientboundPacket { EntityRotation(pitch: pitch, yaw: yaw) EntityLerpState() EntityAttributes() - EntityMetadata() + EntityMetadata(inheritanceChain: entityKind.inheritanceChain) } } } 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 053aeab4..ff5ca8be 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnLivingEntityPacket.swift @@ -43,7 +43,7 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { EntityHeadYaw(headYaw) EntityLerpState() EntityAttributes() - EntityMetadata() + EntityMetadata(inheritanceChain: entityKind.inheritanceChain) EnderDragonParts() } } else { @@ -60,7 +60,7 @@ public struct SpawnLivingEntityPacket: ClientboundPacket { EntityHeadYaw(headYaw) EntityLerpState() EntityAttributes() - EntityMetadata() + EntityMetadata(inheritanceChain: entityKind.inheritanceChain) } } } 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 0fd3ffec..77334d33 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/SpawnPlayerPacket.swift @@ -37,7 +37,7 @@ public struct SpawnPlayerPacket: ClientboundPacket { EntityRotation(pitch: pitch, yaw: yaw) EntityLerpState() EntityAttributes() - EntityMetadata() + EntityMetadata(inheritanceChain: playerEntity.inheritanceChain) } } } diff --git a/Sources/Core/Sources/Player/Player.swift b/Sources/Core/Sources/Player/Player.swift index 05ffd350..e7e18add 100644 --- a/Sources/Core/Sources/Player/Player.swift +++ b/Sources/Core/Sources/Player/Player.swift @@ -77,7 +77,7 @@ public struct Player { nutrition = EntityNutrition() playerAttributes = PlayerAttributes() entityAttributes = EntityAttributes() - entityMetadata = EntityMetadata() + entityMetadata = EntityMetadata(inheritanceChain: playerEntity.inheritanceChain) camera = EntityCamera() gamemode = PlayerGamemode() inventory = PlayerInventory() diff --git a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift index 1527016f..fafd6d64 100644 --- a/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift +++ b/Sources/Core/Sources/Registry/Pixlyzer/PixlyzerFormatter.swift @@ -164,9 +164,9 @@ public enum PixlyzerFormatter { ) } - 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) @@ -184,7 +184,7 @@ public enum PixlyzerFormatter { for (identifier, pixlyzerEntity) in pixlyzerEntities { if let identifier = try? Identifier(identifier) { var parent = pixlyzerEntity.parent - var inheritanceChain: [String] = [] + var inheritanceChain: [String] = [pixlyzerEntity.class].compactMap(identity) while let currentParent = parent { inheritanceChain.append(currentParent) @@ -299,9 +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) From 849d40784c0b03a92c433c39ec01d747a21f299d Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 15:22:06 +1000 Subject: [PATCH 68/84] Render block item entities and fix item entity bobbing to match vanilla better --- Sources/Core/Renderer/ChunkUniforms.swift | 3 +- .../Core/Renderer/Entity/EntityRenderer.swift | 67 ++++++++++--- .../Renderer/Mesh/EntityMeshBuilder.swift | 96 ++++++++++++++----- .../Core/Renderer/World/WorldRenderer.swift | 11 ++- .../Sources/Datatypes/BlockPosition.swift | 13 +-- .../Model/JSON/JSONModelTransform.swift | 6 +- 6 files changed, 146 insertions(+), 50 deletions(-) diff --git a/Sources/Core/Renderer/ChunkUniforms.swift b/Sources/Core/Renderer/ChunkUniforms.swift index 09dd694f..fc667b23 100644 --- a/Sources/Core/Renderer/ChunkUniforms.swift +++ b/Sources/Core/Renderer/ChunkUniforms.swift @@ -1,3 +1,4 @@ +import DeltaCore import FirebladeMath public struct ChunkUniforms { @@ -10,6 +11,6 @@ public struct ChunkUniforms { } public init() { - transformation = Mat4x4f(diagonal: 1) + transformation = MatrixUtil.identity } } diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index b46ce8df..d1e77662 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -11,6 +11,8 @@ public struct EntityRenderer: Renderer { /// The render pipeline state for rendering entities. Does not have blending enabled. private var renderPipelineState: MTLRenderPipelineState + /// The render pipeline state for rendering block entities and block item entities. + private var blockRenderPipelineState: MTLRenderPipelineState /// The buffer containing the uniforms for all rendered entities. private var instanceUniformsBuffer: MTLBuffer? /// The buffer containing the hit box vertices. They form a basic cube and instanced rendering is used to render the cube once for each entity. @@ -21,6 +23,7 @@ public struct EntityRenderer: Renderer { private var indexCount: Int private var entityTexturePalette: MetalTexturePalette + private var blockTexturePalette: MetalTexturePalette private var entityModelPalette: EntityModelPalette private var itemModelPalette: ItemModelPalette @@ -40,27 +43,43 @@ public struct EntityRenderer: Renderer { client: Client, device: MTLDevice, commandQueue: MTLCommandQueue, - profiler: Profiler + profiler: Profiler, + blockTexturePalette: MetalTexturePalette ) throws { self.client = client self.device = device self.commandQueue = commandQueue self.profiler = profiler + self.blockTexturePalette = blockTexturePalette // Load library + // TODO: Avoid loading library again and again let library = try MetalUtil.loadDefaultLibrary(device) let vertexFunction = try MetalUtil.loadFunction("entityVertexShader", from: library) let fragmentFunction = try MetalUtil.loadFunction("entityFragmentShader", from: library) + let blockVertexFunction = try MetalUtil.loadFunction("chunkVertexShader", from: library) + let blockFragmentFunction = try MetalUtil.loadFunction("chunkFragmentShader", from: library) // Create render pipeline state renderPipelineState = try MetalUtil.makeRenderPipelineState( device: device, - label: "dev.stackotter.delta-client.EntityRenderer", + label: "EntityRenderer.renderPipelineState", vertexFunction: vertexFunction, fragmentFunction: fragmentFunction, blendingEnabled: false ) + // TODO: Consider supporting OIT here too? Probably not of much use cause most block item + // entities aren't translucent, and there should never be many instances of them since + // item entities merge. + blockRenderPipelineState = try MetalUtil.makeRenderPipelineState( + device: device, + label: "EntityRenderer.blockRenderPipelineState", + vertexFunction: blockVertexFunction, + fragmentFunction: blockFragmentFunction, + blendingEnabled: true + ) + // Create hitbox geometry (hitboxes are rendered using instancing) var geometry = Self.createHitBoxGeometry(color: Self.hitBoxColor) indexCount = geometry.indices.count @@ -107,6 +126,8 @@ public struct EntityRenderer: Renderer { // Get all renderable entities var geometry = Geometry() + var blockGeometry = Geometry() + var translucentBlockGeometry = SortableMesh(uniforms: ChunkUniforms()) client.game.accessNexus { nexus in // If the player is in first person view we don't render them profiler.push(.getEntities) @@ -159,24 +180,48 @@ public struct EntityRenderer: Renderer { entityModelPalette: entityModelPalette, itemModelPalette: itemModelPalette, blockModelPalette: blockModelPalette, - texturePalette: entityTexturePalette, + entityTexturePalette: entityTexturePalette, + blockTexturePalette: blockTexturePalette, hitbox: hitbox.aabb(at: position.smoothVector) ) - builder.build(into: &geometry) + builder.build( + into: &geometry, + blockGeometry: &blockGeometry, + translucentBlockGeometry: &translucentBlockGeometry + ) } profiler.pop() } - guard !geometry.isEmpty else { - return + if !geometry.isEmpty { + encoder.setRenderPipelineState(renderPipelineState) + encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0) + + // TODO: Update profiler measurements + var mesh = Mesh(geometry, uniforms: ()) + try mesh.render(into: encoder, with: device, commandQueue: commandQueue) } - encoder.setRenderPipelineState(renderPipelineState) - encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0) + if !blockGeometry.isEmpty || !translucentBlockGeometry.isEmpty { + encoder.setRenderPipelineState(blockRenderPipelineState) + encoder.setVertexBuffer(blockTexturePalette.textureStatesBuffer, offset: 0, index: 3) + encoder.setFragmentTexture(blockTexturePalette.arrayTexture, index: 0) - // TODO: Update profiler measurements - var mesh = Mesh(geometry, uniforms: ()) - try mesh.render(into: encoder, with: device, commandQueue: commandQueue) + if !blockGeometry.isEmpty { + var blockMesh = Mesh(blockGeometry, uniforms: ChunkUniforms()) + try blockMesh.render(into: encoder, with: device, commandQueue: commandQueue) + } + + if !translucentBlockGeometry.isEmpty { + try translucentBlockGeometry.render( + viewedFrom: camera.position, + sort: true, + encoder: encoder, + device: device, + commandQueue: commandQueue + ) + } + } } /// Creates a coloured and shaded cube to be rendered using instancing as entities' hitboxes. diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index a37c182c..4d15905a 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -21,7 +21,8 @@ public struct EntityMeshBuilder { public let entityModelPalette: EntityModelPalette public let itemModelPalette: ItemModelPalette public let blockModelPalette: BlockModelPalette - public let texturePalette: MetalTexturePalette + public let entityTexturePalette: MetalTexturePalette + public let blockTexturePalette: MetalTexturePalette public let hitbox: AxisAlignedBoundingBox static let colors: [Vec3f] = [ @@ -35,13 +36,30 @@ public struct EntityMeshBuilder { [1, 1, 1], ] - func build(into geometry: inout Geometry) { + // TODO: Propagate all warnings as errors and then handle them and emit them as warnings in EntityRenderer instead + /// `blockGeometry` and `translucentBlockGeometry` are used to render block entities and block item entities. + func build( + into geometry: inout Geometry, + blockGeometry: inout Geometry, + translucentBlockGeometry: inout SortableMesh + ) { if let model = entityModelPalette.models[entityKind] { buildModel(model, into: &geometry) } else if let itemMetadata = entity.get(component: EntityMetadata.self)?.itemMetadata, let itemStack = itemMetadata.slot.stack, let itemModel = itemModelPalette.model(for: itemStack.itemId) { + // TODO: Figure out why these bobbing constants and hardcoded translations are so weird + // (they're even still slightly off vanilla, there must be a different order of transformations + // that makes these numbers nice or something). + let time = CFAbsoluteTimeGetCurrent() * TickScheduler.defaultTicksPerSecond + let phaseOffset = Double(itemMetadata.bobbingPhaseOffset) + let verticalOffset = Float(Foundation.sin(time / 10 + phaseOffset)) / 8 * 3 + let spinAngle = -Float((time / 20 + phaseOffset).remainder(dividingBy: 2 * .pi)) + let bob = + MatrixUtil.translationMatrix(Vec3f(0, verticalOffset, 0)) + * MatrixUtil.rotationMatrix(y: spinAngle) + switch itemModel { case let .entity(identifier, transforms): // Remove identifier prefix (entity model palette doesn't have any `item/` or `entity/` prefixes). @@ -53,26 +71,51 @@ public struct EntityMeshBuilder { return } - var transformation = transforms.ground - let time = CFAbsoluteTimeGetCurrent() * TickScheduler.defaultTicksPerSecond - let phaseOffset = Double(itemMetadata.bobbingPhaseOffset) - let bob = Float(Foundation.sin(time / 10 + phaseOffset)) - let scaleY = (transformation * Vec4f(0, 1, 0, 0)).magnitude - let verticalOffset = bob + 0.25 * scaleY - transformation *= MatrixUtil.translationMatrix(Vec3f(0, verticalOffset, 0)) - transformation *= MatrixUtil.rotationMatrix( - y: -Float((time / 20 + phaseOffset).remainder(dividingBy: 2 * .pi)) - ) - + let transformation = + bob * transforms.ground * MatrixUtil.translationMatrix(Vec3f(0, 11.0 / 64, 0)) buildModel( entityModel, textureIdentifier: entityIdentifier, transformation: transformation, into: &geometry ) - case .blockModel, .layered: + case let .blockModel(id): + guard let blockModel = blockModelPalette.model(for: id, at: nil) else { + log.warning( + "Missing block model for item entity (block id: \(id), item id: \(itemStack.itemId))" + ) + return + } + + // TODO: Don't just use dummy lighting + var neighbourLightLevels: [Direction: LightLevel] = [:] + for direction in Direction.allDirections { + neighbourLightLevels[direction] = LightLevel(sky: 15, block: 0) + } + + let transformation = + MatrixUtil.translationMatrix(Vec3f(-0.5, 0, -0.5)) + * bob + * MatrixUtil.scalingMatrix(0.25) + * MatrixUtil.translationMatrix(Vec3f(0, 7.0 / 32.0, 0)) + * MatrixUtil.rotationMatrix(y: yaw + .pi) + * MatrixUtil.translationMatrix(position) + let builder = BlockMeshBuilder( + model: blockModel, + position: .zero, + modelToWorld: transformation, + culledFaces: [], + lightLevel: LightLevel(sky: 15, block: 0), + neighbourLightLevels: [:], + tintColor: Vec3f(1, 1, 1), + blockTexturePalette: blockTexturePalette.palette + ) + var translucentElement = SortableMeshElement() + builder.build(into: &blockGeometry, translucentGeometry: &translucentElement) + translucentBlockGeometry.add(translucentElement) + case .layered: buildAABB(hitbox, into: &geometry) - case .empty, .blockModel, .layered: + case .empty: break } } else { @@ -117,6 +160,7 @@ public struct EntityMeshBuilder { } } + /// The unit of `transformation` is blocks. func buildModel( _ model: JSONEntityModel, textureIdentifier: Identifier? = nil, @@ -126,7 +170,7 @@ public struct EntityMeshBuilder { let baseTextureIdentifier = textureIdentifier ?? entityKind let texture: Int? if let identifier = Self.hardcodedTextureIdentifiers[baseTextureIdentifier] { - texture = texturePalette.textureIndex(for: identifier) + texture = entityTexturePalette.textureIndex(for: identifier) } else { // Entity textures can be in all sorts of structures so we just have a few // educated guesses for now. @@ -139,8 +183,8 @@ public struct EntityMeshBuilder { name: "entity/\(baseTextureIdentifier.name)/\(baseTextureIdentifier.name)" ) texture = - texturePalette.textureIndex(for: textureIdentifier) - ?? texturePalette.textureIndex(for: nestedTextureIdentifier) + entityTexturePalette.textureIndex(for: textureIdentifier) + ?? entityTexturePalette.textureIndex(for: nestedTextureIdentifier) } for (index, submodel) in model.models.enumerated() { @@ -154,6 +198,7 @@ public struct EntityMeshBuilder { } } + /// The unit of `transformation` is blocks. func buildSubmodel( _ submodel: JSONEntityModel.Submodel, index: Int, @@ -166,7 +211,7 @@ public struct EntityMeshBuilder { let translation = submodel.translate ?? .zero transformation = MatrixUtil.rotationMatrix(-MathUtil.radians(from: rotation)) - * MatrixUtil.translationMatrix(translation) + * MatrixUtil.translationMatrix(translation / 16) * transformation } @@ -197,6 +242,7 @@ public struct EntityMeshBuilder { } } + /// The unit of `transformation` is 16 units per block. func buildBox( _ box: JSONEntityModel.Box, color: Vec3f, @@ -278,22 +324,26 @@ public struct EntityMeshBuilder { uvSize.y *= -1 } + let textureSize = Vec2f( + Float(entityTexturePalette.palette.width), + Float(entityTexturePalette.palette.height) + ) let uvs = [ uvOrigin, uvOrigin + Vec2f(0, uvSize.y), uvOrigin + Vec2f(uvSize.x, uvSize.y), uvOrigin + Vec2f(uvSize.x, 0), - ].map { - $0 / Vec2f(Float(texturePalette.palette.width), Float(texturePalette.palette.height)) + ].map { pixelUV in + pixelUV / textureSize } let faceVertexPositions = CubeGeometry.faceVertices[direction.rawValue] for (uv, vertexPosition) in zip(uvs, faceVertexPositions) { var position = vertexPosition * boxSize + boxPosition + position /= 16 position = - (Vec4f(position, 1) * transformation * MatrixUtil.rotationMatrix(yaw + .pi, around: .y)) + (Vec4f(position, 1) * transformation * MatrixUtil.rotationMatrix(y: yaw + .pi)) .xyz - position /= 16 position += self.position let vertex = EntityVertex( x: position.x, diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 63bc0954..7202b38a 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -82,7 +82,9 @@ public final class WorldRenderer: Renderer { let vertexFunction = try MetalUtil.loadFunction("chunkVertexShader", from: library) let fragmentFunction = try MetalUtil.loadFunction("chunkFragmentShader", from: library) let transparentFragmentFunction = try MetalUtil.loadFunction( - "chunkOITFragmentShader", from: library) + "chunkOITFragmentShader", + from: library + ) let transparentCompositingVertexFunction = try MetalUtil.loadFunction( "chunkOITCompositingVertexShader", from: library @@ -107,7 +109,7 @@ public final class WorldRenderer: Renderer { // Create opaque pipeline (which also handles translucent geometry when OIT is disabled) renderPipelineState = try MetalUtil.makeRenderPipelineState( device: device, - label: "WorldRenderer.mainPipeline", + label: "WorldRenderer.renderPipelineState", vertexFunction: vertexFunction, fragmentFunction: fragmentFunction, blendingEnabled: true @@ -115,7 +117,7 @@ public final class WorldRenderer: Renderer { destroyOverlayRenderPipelineState = try MetalUtil.makeRenderPipelineState( device: device, - label: "WorldRenderer.destroyOverlayPipeline", + label: "WorldRenderer.destroyOverlayRenderPipelineState", vertexFunction: vertexFunction, fragmentFunction: fragmentFunction, blendingEnabled: true, @@ -178,7 +180,8 @@ public final class WorldRenderer: Renderer { client: client, device: device, commandQueue: commandQueue, - profiler: profiler + profiler: profiler, + blockTexturePalette: texturePalette ) // Create world mesh diff --git a/Sources/Core/Sources/Datatypes/BlockPosition.swift b/Sources/Core/Sources/Datatypes/BlockPosition.swift index a2879c6b..11c0203f 100644 --- a/Sources/Core/Sources/Datatypes/BlockPosition.swift +++ b/Sources/Core/Sources/Datatypes/BlockPosition.swift @@ -1,9 +1,10 @@ -import Foundation import FirebladeMath +import Foundation /// A block position. public struct BlockPosition { - // MARK: Public properties + /// The origin. + public static let zero = BlockPosition(x: 0, y: 0, z: 0) /// The x component. public var x: Int @@ -14,14 +15,14 @@ public struct BlockPosition { /// The position of the ``Chunk`` this position is in public var chunk: ChunkPosition { - let chunkX = x >> 4 // divides by 16 and rounds down + let chunkX = x >> 4 // divides by 16 and rounds down let chunkZ = z >> 4 return ChunkPosition(chunkX: chunkX, chunkZ: chunkZ) } /// The position of the ``Chunk/Section`` this position is in public var chunkSection: ChunkSectionPosition { - let sectionX = x >> 4 // divides by 16 and rounds down + let sectionX = x >> 4 // divides by 16 and rounds down let sectionY = y >> 4 let sectionZ = z >> 4 return ChunkSectionPosition(sectionX: sectionX, sectionY: sectionY, sectionZ: sectionZ) @@ -97,8 +98,6 @@ public struct BlockPosition { } } - // MARK: Init - /// Create a new block position. /// /// Coordinates are not validated. @@ -112,8 +111,6 @@ public struct BlockPosition { self.z = z } - // MARK: Public methods - /// Component-wise addition of two block positions. public static func + (lhs: BlockPosition, rhs: Vec3i) -> BlockPosition { return BlockPosition(x: lhs.x &+ rhs.x, y: lhs.y &+ rhs.y, z: lhs.z &+ rhs.z) diff --git a/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift b/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift index 1e587af5..e6213d00 100644 --- a/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift +++ b/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift @@ -1,12 +1,12 @@ -import Foundation import FirebladeMath +import Foundation /// Transformation that can be applied to a model, as read from JSON files in resource packs. /// Translation is applied before rotation. struct JSONModelTransform: Codable { /// The rotation (should be `[x, y, z]`). var rotation: [Double]? - /// The translation (should be `[x, y, z]`). Clamp to between -80 and 80. + /// The translation (should be `[x, y, z]`). Clamp to between -80 and 80. Measured in 16ths of a block. var translation: [Double]? /// The scale (should be `[x, y, z]`). Maximum 4. var scale: [Double]? @@ -17,7 +17,7 @@ struct JSONModelTransform: Codable { if let translation = self.translation { var translationVector = try MathUtil.vectorFloat3(from: translation) - translationVector = MathUtil.clamp(translationVector, min: -80, max: 80) + translationVector = MathUtil.clamp(translationVector, min: -80, max: 80) / 16 matrix *= MatrixUtil.translationMatrix(translationVector) } From 25ecc657b30f63e2fbc685c058652d8a26713e13 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 16:17:01 +1000 Subject: [PATCH 69/84] Implement translucent block item rendering for inventory/hotbar (previously not supported) --- Sources/Core/Renderer/GUI/GUIRenderer.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index f5b60d44..a0cf6e46 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -267,7 +267,7 @@ public final class GUIRenderer: Renderer { var vertices: [GUIVertex] = [] vertices.reserveCapacity(geometry.vertices.count) - for vertex in geometry.vertices { + for vertex in geometry.vertices + translucentGeometry.vertices { vertices.append( GUIVertex( position: [vertex.x, vertex.y], @@ -278,7 +278,6 @@ public final class GUIRenderer: Renderer { ) } - // TODO: Handle translucent block items var mesh = GUIElementMesh( size: [16, 16], arrayTexture: blockArrayTexture, From 1a99af57f5bfdb160a4fe72fda271027a6f46339 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sat, 8 Jun 2024 23:32:36 +1000 Subject: [PATCH 70/84] Implement block entity item rendering for inventory (chests etc) --- .../Core/Renderer/Entity/EntityRenderer.swift | 13 ++-- Sources/Core/Renderer/GUI/GUIRenderer.swift | 74 ++++++++++++++++++- .../Renderer/Mesh/EntityMeshBuilder.swift | 25 ++++--- .../JSON/JSONModelDisplayTransforms.swift | 1 + .../Model/JSON/JSONModelTransform.swift | 6 +- 5 files changed, 99 insertions(+), 20 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index d1e77662..ef972d5a 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -171,7 +171,8 @@ public struct EntityRenderer: Renderer { kindIdentifier = Identifier(name: "dragon") } - let builder = EntityMeshBuilder( + var translucentBlockElement = SortableMeshElement() + EntityMeshBuilder( entity: entity, entityKind: kindIdentifier, position: Vec3f(position.smoothVector), @@ -180,15 +181,15 @@ public struct EntityRenderer: Renderer { entityModelPalette: entityModelPalette, itemModelPalette: itemModelPalette, blockModelPalette: blockModelPalette, - entityTexturePalette: entityTexturePalette, - blockTexturePalette: blockTexturePalette, + entityTexturePalette: entityTexturePalette.palette, + blockTexturePalette: blockTexturePalette.palette, hitbox: hitbox.aabb(at: position.smoothVector) - ) - builder.build( + ).build( into: &geometry, blockGeometry: &blockGeometry, - translucentBlockGeometry: &translucentBlockGeometry + translucentBlockGeometry: &translucentBlockElement ) + translucentBlockGeometry.add(translucentBlockElement) } profiler.pop() } diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index a0cf6e46..223bf436 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -29,6 +29,9 @@ public final class GUIRenderer: Renderer { var blockArrayTexture: MTLTexture var blockModelPalette: BlockModelPalette var blockTexturePalette: TexturePalette + var entityArrayTexture: MTLTexture + var entityTexturePalette: TexturePalette + var entityModelPalette: EntityModelPalette var cache: [GUIElementMesh] = [] @@ -83,6 +86,16 @@ public final class GUIRenderer: Renderer { blockArrayTexture.label = "blockArrayTexture" blockModelPalette = resources.blockModelPalette + entityTexturePalette = resources.entityTexturePalette + entityArrayTexture = try MetalTexturePalette.createArrayTexture( + for: resources.entityTexturePalette, + device: device, + commandQueue: commandQueue, + includeAnimations: false + ) + entityArrayTexture.label = "entityArrayTexture" + entityModelPalette = resources.entityModelPalette + // Create uniforms buffer uniformsBuffer = try MetalUtil.makeBuffer( device, @@ -235,6 +248,8 @@ public final class GUIRenderer: Renderer { return [] } + // TODO: Use this assumption to just lift all the display transforms when loading the + // block model palette. // Get the block's transformation assuming that each block model part has the same // associated gui transformation (I don't see why this wouldn't always be true). var transformation: Mat4x4f @@ -285,7 +300,64 @@ public final class GUIRenderer: Renderer { ) mesh.position = [0, 0] return [mesh] - case .empty, .entity: + case let .entity(identifier, transforms): + var entityIdentifier = identifier + entityIdentifier.name = entityIdentifier.name.replacingOccurrences(of: "item/", with: "") + + // Dummy meshes, we don't handle rendering inventory entities which themselves + // contain block item entities cause that doesn't happen (famous last words...) + var blockGeometry = Geometry() + var translucentBlockGeometry = SortableMeshElement() + + var geometry = Geometry() + EntityMeshBuilder( + entity: nil, + entityKind: entityIdentifier, + position: .zero, + pitch: 0, + yaw: 0, + entityModelPalette: entityModelPalette, + itemModelPalette: itemModelPalette, + blockModelPalette: blockModelPalette, + entityTexturePalette: entityTexturePalette, + blockTexturePalette: blockTexturePalette, + hitbox: AxisAlignedBoundingBox(position: .zero, size: Vec3d(1, 1, 1)) + ).build( + into: &geometry, + blockGeometry: &blockGeometry, + translucentBlockGeometry: &translucentBlockGeometry + ) + + let transformation: Mat4x4f = + MatrixUtil.translationMatrix(Vec3f(0, -0.5, 0)) + * MatrixUtil.rotationMatrix(y: .pi / 2) + * transforms.gui + * MatrixUtil.scalingMatrix(Vec3f(-1, -1, 1)) + * MatrixUtil.scalingMatrix(16) + * MatrixUtil.translationMatrix([8, 8, 0]) + + var vertices: [GUIVertex] = [] + vertices.reserveCapacity(geometry.vertices.count) + for vertex in geometry.vertices { + let position = (Vec4f(vertex.x, vertex.y, vertex.z, 1) * transformation).xyz + vertices.append( + GUIVertex( + position: [position.x, position.y], + uv: [vertex.u, vertex.v], + tint: [vertex.r, vertex.g, vertex.b, 1], + textureIndex: vertex.textureIndex + ) + ) + } + + var mesh = GUIElementMesh( + size: [16, 16], + arrayTexture: entityArrayTexture, + vertices: .flatArray(vertices) + ) + mesh.position = [0, 0] + return [mesh] + case .empty: return [] } } diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index 4d15905a..f3403443 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -13,7 +13,9 @@ public struct EntityMeshBuilder { Identifier(name: "chest"): Identifier(name: "entity/chest/normal"), ] - public let entity: Entity + /// Used to get extra metadata for rendering item entities and the Ender Dragon. Not required if just + /// rendering an entity for things such as block entity items. + public let entity: Entity? public let entityKind: Identifier public let position: Vec3f public let pitch: Float @@ -21,8 +23,8 @@ public struct EntityMeshBuilder { public let entityModelPalette: EntityModelPalette public let itemModelPalette: ItemModelPalette public let blockModelPalette: BlockModelPalette - public let entityTexturePalette: MetalTexturePalette - public let blockTexturePalette: MetalTexturePalette + public let entityTexturePalette: TexturePalette + public let blockTexturePalette: TexturePalette public let hitbox: AxisAlignedBoundingBox static let colors: [Vec3f] = [ @@ -41,11 +43,11 @@ public struct EntityMeshBuilder { func build( into geometry: inout Geometry, blockGeometry: inout Geometry, - translucentBlockGeometry: inout SortableMesh + translucentBlockGeometry: inout SortableMeshElement ) { if let model = entityModelPalette.models[entityKind] { buildModel(model, into: &geometry) - } else if let itemMetadata = entity.get(component: EntityMetadata.self)?.itemMetadata, + } else if let itemMetadata = entity?.get(component: EntityMetadata.self)?.itemMetadata, let itemStack = itemMetadata.slot.stack, let itemModel = itemModelPalette.model(for: itemStack.itemId) { @@ -93,6 +95,7 @@ public struct EntityMeshBuilder { neighbourLightLevels[direction] = LightLevel(sky: 15, block: 0) } + // TODO: Try using the transformation code from the GUIRenderer and see if that cleans things up a bit. let transformation = MatrixUtil.translationMatrix(Vec3f(-0.5, 0, -0.5)) * bob @@ -108,11 +111,9 @@ public struct EntityMeshBuilder { lightLevel: LightLevel(sky: 15, block: 0), neighbourLightLevels: [:], tintColor: Vec3f(1, 1, 1), - blockTexturePalette: blockTexturePalette.palette + blockTexturePalette: blockTexturePalette ) - var translucentElement = SortableMeshElement() - builder.build(into: &blockGeometry, translucentGeometry: &translucentElement) - translucentBlockGeometry.add(translucentElement) + builder.build(into: &blockGeometry, translucentGeometry: &translucentBlockGeometry) case .layered: buildAABB(hitbox, into: &geometry) case .empty: @@ -122,7 +123,7 @@ public struct EntityMeshBuilder { buildAABB(hitbox, into: &geometry) } - if let dragonParts = entity.get(component: EnderDragonParts.self) { + if let dragonParts = entity?.get(component: EnderDragonParts.self) { for part in dragonParts.parts { let aabb = part.aabb(withParentPosition: Vec3d(position)) buildAABB(aabb, into: &geometry) @@ -325,8 +326,8 @@ public struct EntityMeshBuilder { } let textureSize = Vec2f( - Float(entityTexturePalette.palette.width), - Float(entityTexturePalette.palette.height) + Float(entityTexturePalette.width), + Float(entityTexturePalette.height) ) let uvs = [ uvOrigin, diff --git a/Sources/Core/Sources/Resources/Model/JSON/JSONModelDisplayTransforms.swift b/Sources/Core/Sources/Resources/Model/JSON/JSONModelDisplayTransforms.swift index 7a55b949..262f2011 100644 --- a/Sources/Core/Sources/Resources/Model/JSON/JSONModelDisplayTransforms.swift +++ b/Sources/Core/Sources/Resources/Model/JSON/JSONModelDisplayTransforms.swift @@ -30,6 +30,7 @@ struct JSONModelDisplayTransforms: Codable { case fixed } + /// Child takes precedence. func merge(withChild child: JSONModelDisplayTransforms) -> JSONModelDisplayTransforms { let thirdPersonRightHand = child.thirdPersonRightHand ?? thirdPersonRightHand let thirdPersonLeftHand = child.thirdPersonLeftHand ?? thirdPersonLeftHand diff --git a/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift b/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift index e6213d00..65a8f719 100644 --- a/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift +++ b/Sources/Core/Sources/Resources/Model/JSON/JSONModelTransform.swift @@ -24,7 +24,11 @@ struct JSONModelTransform: Codable { if let rotation = self.rotation { var rotationVector = try MathUtil.vectorFloat3(from: rotation) rotationVector = MathUtil.radians(from: rotationVector) - matrix *= MatrixUtil.rotationMatrix(rotationVector) + // matrix *= MatrixUtil.rotationMatrix(rotationVector) + matrix *= + MatrixUtil.rotationMatrix(z: rotationVector.z) + * MatrixUtil.rotationMatrix(y: rotationVector.y) + * MatrixUtil.rotationMatrix(x: rotationVector.x) } if let scale = self.scale { From 93060cd9dae27493bbcd6559a46e205914b8279a Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 9 Jun 2024 00:46:11 +1000 Subject: [PATCH 71/84] Implement block entity rotation --- .../Core/Renderer/Entity/EntityRenderer.swift | 102 +++++++++++++++--- .../Core/Renderer/RenderingMeasurement.swift | 5 +- .../Core/Renderer/World/WorldRenderer.swift | 5 +- .../Sources/Datatypes/BlockPosition.swift | 6 +- .../Core/Sources/Datatypes/Direction.swift | 9 +- .../Clientbound/BlockEntityDataPacket.swift | 8 +- 6 files changed, 108 insertions(+), 27 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index ef972d5a..bcafc404 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -38,6 +38,9 @@ public struct EntityRenderer: Renderer { private var profiler: Profiler + /// Should get updated each frame via `setVisibleChunks`. + private var visibleChunks: Set = [] + /// Creates a new entity renderer. public init( client: Client, @@ -154,7 +157,7 @@ public struct EntityRenderer: Renderer { let cameraChunk = camera.entityPosition.chunk // Create uniforms for each entity - profiler.push(.createUniforms) + profiler.push(.createRegularEntityMeshes) for (entity, position, rotation, hitbox, kindId) in entities.entityAndComponents { // Don't render entities that are outside of the render distance let chunkPosition = position.chunk @@ -171,34 +174,53 @@ public struct EntityRenderer: Renderer { kindIdentifier = Identifier(name: "dragon") } - var translucentBlockElement = SortableMeshElement() - EntityMeshBuilder( + buildEntityMesh( entity: entity, - entityKind: kindIdentifier, + entityKindIdentifier: kindIdentifier, position: Vec3f(position.smoothVector), pitch: rotation.smoothPitch, yaw: rotation.smoothYaw, - entityModelPalette: entityModelPalette, - itemModelPalette: itemModelPalette, - blockModelPalette: blockModelPalette, - entityTexturePalette: entityTexturePalette.palette, - blockTexturePalette: blockTexturePalette.palette, - hitbox: hitbox.aabb(at: position.smoothVector) - ).build( + hitbox: hitbox.aabb(at: position.smoothVector), into: &geometry, blockGeometry: &blockGeometry, - translucentBlockGeometry: &translucentBlockElement + translucentBlockGeometry: &translucentBlockGeometry ) - translucentBlockGeometry.add(translucentBlockElement) + } + profiler.pop() + + profiler.push(.createBlockEntityMeshes) + for chunkPosition in visibleChunks { + guard let chunk = client.game.world.chunk(at: chunkPosition) else { + continue + } + + for blockEntity in chunk.getBlockEntities() { + let position = blockEntity.position.floatVector + Vec3f(0.5, 0, 0.5) + + let block = chunk.getBlock(at: blockEntity.position.relativeToChunk) + let direction = block.stateProperties.facing ?? .south + + buildEntityMesh( + entity: nil, + entityKindIdentifier: blockEntity.identifier, + position: position, + pitch: 0, + yaw: Self.blockEntityYaw(toFace: direction), + hitbox: AxisAlignedBoundingBox(position: .zero, size: Vec3d(1, 1, 1)), + into: &geometry, + blockGeometry: &blockGeometry, + translucentBlockGeometry: &translucentBlockGeometry + ) + } } profiler.pop() } + profiler.push(.encodeEntities) if !geometry.isEmpty { encoder.setRenderPipelineState(renderPipelineState) encoder.setFragmentTexture(entityTexturePalette.arrayTexture, index: 0) - // TODO: Update profiler measurements var mesh = Mesh(geometry, uniforms: ()) try mesh.render(into: encoder, with: device, commandQueue: commandQueue) } @@ -223,6 +245,58 @@ public struct EntityRenderer: Renderer { ) } } + profiler.pop() + } + + private func buildEntityMesh( + entity: Entity? = nil, + entityKindIdentifier: Identifier, + position: Vec3f, + pitch: Float, + yaw: Float, + hitbox: AxisAlignedBoundingBox, + into geometry: inout Geometry, + blockGeometry: inout Geometry, + translucentBlockGeometry: inout SortableMesh + ) { + var translucentBlockElement = SortableMeshElement() + EntityMeshBuilder( + entity: entity, + entityKind: entityKindIdentifier, + position: position, + pitch: pitch, + yaw: yaw, + entityModelPalette: entityModelPalette, + itemModelPalette: itemModelPalette, + blockModelPalette: blockModelPalette, + entityTexturePalette: entityTexturePalette.palette, + blockTexturePalette: blockTexturePalette.palette, + hitbox: hitbox + ).build( + into: &geometry, + blockGeometry: &blockGeometry, + translucentBlockGeometry: &translucentBlockElement + ) + translucentBlockGeometry.add(translucentBlockElement) + } + + /// Computes the yaw required for a block entity to face a given direction. + private static func blockEntityYaw(toFace direction: Direction) -> Float { + switch direction { + case .south, .up, .down: + return 0 + case .west: + return .pi / 2 + case .north: + return .pi + case .east: + return -.pi / 2 + } + } + + /// Sets the chunks that block entities should be rendered from. + public mutating func setVisibleChunks(_ visibleChunks: Set) { + self.visibleChunks = visibleChunks } /// Creates a coloured and shaded cube to be rendered using instancing as entities' hitboxes. diff --git a/Sources/Core/Renderer/RenderingMeasurement.swift b/Sources/Core/Renderer/RenderingMeasurement.swift index fc085c6a..3d015ffb 100644 --- a/Sources/Core/Renderer/RenderingMeasurement.swift +++ b/Sources/Core/Renderer/RenderingMeasurement.swift @@ -16,8 +16,9 @@ public enum RenderingMeasurement: String, Hashable { case entities case getEntities - case createUniforms - case getBuffer + case createRegularEntityMeshes + case createBlockEntityMeshes + case encodeEntities case gui case updateUniforms diff --git a/Sources/Core/Renderer/World/WorldRenderer.swift b/Sources/Core/Renderer/World/WorldRenderer.swift index 7202b38a..7e99e66d 100644 --- a/Sources/Core/Renderer/World/WorldRenderer.swift +++ b/Sources/Core/Renderer/World/WorldRenderer.swift @@ -293,7 +293,9 @@ public final class WorldRenderer: Renderer { // Render transparent and opaque geometry profiler.push(.encodeOpaque) - try worldMesh.mutateVisibleMeshes { _, mesh in + var visibleChunks: Set = [] + try worldMesh.mutateVisibleMeshes { position, mesh in + visibleChunks.insert(position.chunk) try mesh.renderTransparentAndOpaque( renderEncoder: encoder, device: device, @@ -421,6 +423,7 @@ public final class WorldRenderer: Renderer { // Entities are rendered before translucent geometry for correct alpha blending behaviour. profiler.push(.entities) + entityRenderer.setVisibleChunks(visibleChunks) try entityRenderer.render( view: view, encoder: encoder, diff --git a/Sources/Core/Sources/Datatypes/BlockPosition.swift b/Sources/Core/Sources/Datatypes/BlockPosition.swift index 11c0203f..8e6c4d04 100644 --- a/Sources/Core/Sources/Datatypes/BlockPosition.swift +++ b/Sources/Core/Sources/Datatypes/BlockPosition.swift @@ -48,7 +48,8 @@ public struct BlockPosition { return Vec3f( Float(x), Float(y), - Float(z)) + Float(z) + ) } /// This position as a vector of doubles. @@ -56,7 +57,8 @@ public struct BlockPosition { return Vec3d( Double(x), Double(y), - Double(z)) + Double(z) + ) } /// This position as a vector of ints. diff --git a/Sources/Core/Sources/Datatypes/Direction.swift b/Sources/Core/Sources/Datatypes/Direction.swift index 6b769bd0..ec76b3f7 100644 --- a/Sources/Core/Sources/Datatypes/Direction.swift +++ b/Sources/Core/Sources/Datatypes/Direction.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation /// A direction enum where the raw value is the same as in some of the Minecraft packets. public enum Direction: Int, CustomStringConvertible { @@ -17,7 +17,7 @@ public enum Direction: Int, CustomStringConvertible { .east, .west, .up, - .down + .down, ] /// All directions excluding up and down. @@ -25,7 +25,7 @@ public enum Direction: Int, CustomStringConvertible { .north, .east, .south, - .west + .west, ] public var description: String { @@ -151,7 +151,8 @@ public enum Direction: Int, CustomStringConvertible { let loops: [Axis: [Direction]] = [ .x: [.up, .north, .down, .south], .y: [.north, .east, .south, .west], - .z: [.up, .east, .down, .west]] + .z: [.up, .east, .down, .west], + ] switch self { case referenceDirection, referenceDirection.opposite: return self diff --git a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockEntityDataPacket.swift b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockEntityDataPacket.swift index 32286fcd..5b52d1cf 100644 --- a/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockEntityDataPacket.swift +++ b/Sources/Core/Sources/Network/Protocol/Packets/Play/Clientbound/BlockEntityDataPacket.swift @@ -2,13 +2,13 @@ import Foundation public struct BlockEntityDataPacket: ClientboundPacket { public static let id: Int = 0x09 - - public var location: BlockPosition + + public var position: BlockPosition public var action: UInt8 public var nbtData: NBT.Compound - + public init(from packetReader: inout PacketReader) throws { - location = try packetReader.readBlockPosition() + position = try packetReader.readBlockPosition() action = try packetReader.readUnsignedByte() nbtData = try packetReader.readNBTCompound() } From cea734fffde313089357b8009c50e3a1cb899b18 Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 9 Jun 2024 02:04:48 +1000 Subject: [PATCH 72/84] Implement entity lighting (just simple flat lighting that respects sky and block light levels, no normals yet) --- .../Core/Renderer/Entity/EntityRenderer.swift | 67 ++----------------- .../Core/Renderer/Entity/EntityVertex.swift | 24 ++++--- Sources/Core/Renderer/GUI/GUIRenderer.swift | 3 +- .../Core/Renderer/Mesh/BlockMeshBuilder.swift | 2 +- .../Renderer/Mesh/EntityMeshBuilder.swift | 31 ++++++--- .../Core/Renderer/Shader/EntityShaders.metal | 18 ++++- Sources/Core/Sources/World/Chunk/Chunk.swift | 36 +++++++--- .../Core/Sources/World/Light/LightLevel.swift | 22 +++--- Sources/Core/Sources/World/World.swift | 56 ++++++++++------ 9 files changed, 137 insertions(+), 122 deletions(-) diff --git a/Sources/Core/Renderer/Entity/EntityRenderer.swift b/Sources/Core/Renderer/Entity/EntityRenderer.swift index bcafc404..94eb987e 100644 --- a/Sources/Core/Renderer/Entity/EntityRenderer.swift +++ b/Sources/Core/Renderer/Entity/EntityRenderer.swift @@ -15,12 +15,6 @@ public struct EntityRenderer: Renderer { private var blockRenderPipelineState: MTLRenderPipelineState /// The buffer containing the uniforms for all rendered entities. private var instanceUniformsBuffer: MTLBuffer? - /// The buffer containing the hit box vertices. They form a basic cube and instanced rendering is used to render the cube once for each entity. - private var vertexBuffer: MTLBuffer - /// The buffer containing the index windings for the template hit box (see ``vertexBuffer``. - private var indexBuffer: MTLBuffer - /// The number of indices in ``indexBuffer``. - private var indexCount: Int private var entityTexturePalette: MetalTexturePalette private var blockTexturePalette: MetalTexturePalette @@ -83,26 +77,6 @@ public struct EntityRenderer: Renderer { blendingEnabled: true ) - // Create hitbox geometry (hitboxes are rendered using instancing) - var geometry = Self.createHitBoxGeometry(color: Self.hitBoxColor) - indexCount = geometry.indices.count - - vertexBuffer = try MetalUtil.makeBuffer( - device, - bytes: &geometry.vertices, - length: geometry.vertices.count * MemoryLayout.stride, - options: .storageModeShared, - label: "entityHitBoxVertices" - ) - - indexBuffer = try MetalUtil.makeBuffer( - device, - bytes: &geometry.indices, - length: geometry.indices.count * MemoryLayout.stride, - options: .storageModeShared, - label: "entityHitBoxIndices" - ) - entityTexturePalette = try MetalTexturePalette( palette: client.resourcePack.vanillaResources.entityTexturePalette, device: device, @@ -174,6 +148,7 @@ public struct EntityRenderer: Renderer { kindIdentifier = Identifier(name: "dragon") } + let lightLevel = client.game.world.getLightLevel(at: position.block) buildEntityMesh( entity: entity, entityKindIdentifier: kindIdentifier, @@ -181,6 +156,7 @@ public struct EntityRenderer: Renderer { pitch: rotation.smoothPitch, yaw: rotation.smoothYaw, hitbox: hitbox.aabb(at: position.smoothVector), + lightLevel: lightLevel, into: &geometry, blockGeometry: &blockGeometry, translucentBlockGeometry: &translucentBlockGeometry @@ -200,6 +176,7 @@ public struct EntityRenderer: Renderer { let block = chunk.getBlock(at: blockEntity.position.relativeToChunk) let direction = block.stateProperties.facing ?? .south + let lightLevel = client.game.world.getLightLevel(at: blockEntity.position) buildEntityMesh( entity: nil, entityKindIdentifier: blockEntity.identifier, @@ -207,6 +184,7 @@ public struct EntityRenderer: Renderer { pitch: 0, yaw: Self.blockEntityYaw(toFace: direction), hitbox: AxisAlignedBoundingBox(position: .zero, size: Vec3d(1, 1, 1)), + lightLevel: lightLevel, into: &geometry, blockGeometry: &blockGeometry, translucentBlockGeometry: &translucentBlockGeometry @@ -255,6 +233,7 @@ public struct EntityRenderer: Renderer { pitch: Float, yaw: Float, hitbox: AxisAlignedBoundingBox, + lightLevel: LightLevel, into geometry: inout Geometry, blockGeometry: inout Geometry, translucentBlockGeometry: inout SortableMesh @@ -271,7 +250,8 @@ public struct EntityRenderer: Renderer { blockModelPalette: blockModelPalette, entityTexturePalette: entityTexturePalette.palette, blockTexturePalette: blockTexturePalette.palette, - hitbox: hitbox + hitbox: hitbox, + lightLevel: lightLevel ).build( into: &geometry, blockGeometry: &blockGeometry, @@ -298,37 +278,4 @@ public struct EntityRenderer: Renderer { public mutating func setVisibleChunks(_ visibleChunks: Set) { self.visibleChunks = visibleChunks } - - /// Creates a coloured and shaded cube to be rendered using instancing as entities' hitboxes. - private static func createHitBoxGeometry(color: DeltaCore.RGBColor) -> Geometry { - var vertices: [EntityVertex] = [] - var indices: [UInt32] = [] - - for direction in Direction.allDirections { - let faceVertices = CubeGeometry.faceVertices[direction.rawValue] - for position in faceVertices { - let color = color.floatVector * CubeGeometry.shades[direction.rawValue] - vertices.append( - EntityVertex( - x: position.x, - y: position.y, - z: position.z, - r: color.x, - g: color.y, - b: color.z, - u: 0, - v: 0, - textureIndex: nil - ) - ) - } - - let offset = UInt32(indices.count / 6 * 4) - for value in CubeGeometry.faceWinding { - indices.append(value + offset) - } - } - - return Geometry(vertices: vertices, indices: indices) - } } diff --git a/Sources/Core/Renderer/Entity/EntityVertex.swift b/Sources/Core/Renderer/Entity/EntityVertex.swift index e73d7050..c793601e 100644 --- a/Sources/Core/Renderer/Entity/EntityVertex.swift +++ b/Sources/Core/Renderer/Entity/EntityVertex.swift @@ -1,17 +1,19 @@ /// The vertex format used by the entity shader. public struct EntityVertex { - public let x: Float - public let y: Float - public let z: Float - public let r: Float - public let g: Float - public let b: Float - public let u: Float - public let v: Float + public var x: Float + public var y: Float + public var z: Float + public var r: Float + public var g: Float + public var b: Float + public var u: Float + public var v: Float + public var skyLightLevel: UInt8 + public var blockLightLevel: UInt8 /// ``UInt16/max`` indicates that no texture is to be used. I would usually use /// an optional to model that, but this type needs to be compatible with C as we /// pass it off to the shaders for rendering. - public let textureIndex: UInt16 + public var textureIndex: UInt16 public init( x: Float, @@ -22,6 +24,8 @@ public struct EntityVertex { b: Float, u: Float, v: Float, + skyLightLevel: UInt8, + blockLightLevel: UInt8, textureIndex: UInt16? ) { self.x = x @@ -32,6 +36,8 @@ public struct EntityVertex { self.b = b self.u = u self.v = v + self.skyLightLevel = skyLightLevel + self.blockLightLevel = blockLightLevel self.textureIndex = textureIndex ?? .max } } diff --git a/Sources/Core/Renderer/GUI/GUIRenderer.swift b/Sources/Core/Renderer/GUI/GUIRenderer.swift index 223bf436..ce5d739b 100644 --- a/Sources/Core/Renderer/GUI/GUIRenderer.swift +++ b/Sources/Core/Renderer/GUI/GUIRenderer.swift @@ -321,7 +321,8 @@ public final class GUIRenderer: Renderer { blockModelPalette: blockModelPalette, entityTexturePalette: entityTexturePalette, blockTexturePalette: blockTexturePalette, - hitbox: AxisAlignedBoundingBox(position: .zero, size: Vec3d(1, 1, 1)) + hitbox: AxisAlignedBoundingBox(position: .zero, size: Vec3d(1, 1, 1)), + lightLevel: .default // Doesn't matter cause the GUI doesn't use light levels ).build( into: &geometry, blockGeometry: &blockGeometry, diff --git a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift index 0f1bdc2a..3396da16 100644 --- a/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/BlockMeshBuilder.swift @@ -67,7 +67,7 @@ struct BlockMeshBuilder { } let faceLightLevel = LightLevel.max( - neighbourLightLevels[face.actualDirection] ?? LightLevel(), + neighbourLightLevels[face.actualDirection] ?? .default, lightLevel ) diff --git a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift index f3403443..a42af656 100644 --- a/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift +++ b/Sources/Core/Renderer/Mesh/EntityMeshBuilder.swift @@ -26,6 +26,7 @@ public struct EntityMeshBuilder { public let entityTexturePalette: TexturePalette public let blockTexturePalette: TexturePalette public let hitbox: AxisAlignedBoundingBox + public let lightLevel: LightLevel static let colors: [Vec3f] = [ [1, 0, 0], @@ -47,10 +48,20 @@ public struct EntityMeshBuilder { ) { if let model = entityModelPalette.models[entityKind] { buildModel(model, into: &geometry) - } else if let itemMetadata = entity?.get(component: EntityMetadata.self)?.itemMetadata, - let itemStack = itemMetadata.slot.stack, - let itemModel = itemModelPalette.model(for: itemStack.itemId) - { + } else if let itemMetadata = entity?.get(component: EntityMetadata.self)?.itemMetadata { + guard let itemStack = itemMetadata.slot.stack else { + // If there's no stack, then we're still waiting for the server to send + // the item's metadata so don't render anything yet (to avoid seeing the + // 'missing entity' hitbox for a split second whenever a new item entity + // spawns in). + return + } + + guard let itemModel = itemModelPalette.model(for: itemStack.itemId) else { + buildAABB(hitbox, into: &geometry) + return + } + // TODO: Figure out why these bobbing constants and hardcoded translations are so weird // (they're even still slightly off vanilla, there must be a different order of transformations // that makes these numbers nice or something). @@ -89,10 +100,9 @@ public struct EntityMeshBuilder { return } - // TODO: Don't just use dummy lighting var neighbourLightLevels: [Direction: LightLevel] = [:] for direction in Direction.allDirections { - neighbourLightLevels[direction] = LightLevel(sky: 15, block: 0) + neighbourLightLevels[direction] = lightLevel } // TODO: Try using the transformation code from the GUIRenderer and see if that cleans things up a bit. @@ -103,13 +113,14 @@ public struct EntityMeshBuilder { * MatrixUtil.translationMatrix(Vec3f(0, 7.0 / 32.0, 0)) * MatrixUtil.rotationMatrix(y: yaw + .pi) * MatrixUtil.translationMatrix(position) + let builder = BlockMeshBuilder( model: blockModel, position: .zero, modelToWorld: transformation, culledFaces: [], - lightLevel: LightLevel(sky: 15, block: 0), - neighbourLightLevels: [:], + lightLevel: lightLevel, + neighbourLightLevels: neighbourLightLevels, tintColor: Vec3f(1, 1, 1), blockTexturePalette: blockTexturePalette ) @@ -154,6 +165,8 @@ public struct EntityMeshBuilder { b: color.z, u: 0, v: 0, + skyLightLevel: UInt8(lightLevel.sky), + blockLightLevel: UInt8(lightLevel.block), textureIndex: nil ) geometry.vertices.append(vertex) @@ -355,6 +368,8 @@ public struct EntityMeshBuilder { b: textureIndex == nil ? color.z : 1, u: uv.x, v: uv.y, + skyLightLevel: UInt8(lightLevel.sky), + blockLightLevel: UInt8(lightLevel.block), textureIndex: textureIndex.map(UInt16.init) ) geometry.vertices.append(vertex) diff --git a/Sources/Core/Renderer/Shader/EntityShaders.metal b/Sources/Core/Renderer/Shader/EntityShaders.metal index 5dab3176..59b17106 100644 --- a/Sources/Core/Renderer/Shader/EntityShaders.metal +++ b/Sources/Core/Renderer/Shader/EntityShaders.metal @@ -12,6 +12,8 @@ struct EntityVertex { float b; float u; float v; + uint8_t skyLightLevel; + uint8_t blockLightLevel; uint16_t textureIndex; }; @@ -19,6 +21,8 @@ struct EntityRasterizerData { float4 position [[position]]; float4 color; float2 uv; + uint8_t skyLightLevel; + uint8_t blockLightLevel; uint16_t textureIndex; }; @@ -32,6 +36,8 @@ vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [ out.color = float4(in.r, in.g, in.b, 1.0); out.uv = float2(in.u, in.v); out.textureIndex = in.textureIndex; + out.skyLightLevel = in.skyLightLevel; + out.blockLightLevel = in.blockLightLevel; return out; } @@ -39,7 +45,8 @@ vertex EntityRasterizerData entityVertexShader(constant EntityVertex *vertices [ constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest, mip_filter::linear); fragment float4 entityFragmentShader(EntityRasterizerData in [[stage_in]], - texture2d_array textureArray [[texture(0)]]) { + texture2d_array textureArray [[texture(0)]], + constant uint8_t *lightMap [[buffer(0)]]) { float4 color; if (in.textureIndex == 65535) { color = in.color; @@ -49,5 +56,14 @@ fragment float4 entityFragmentShader(EntityRasterizerData in [[stage_in]], if (color.a < 0.3) { discard_fragment(); } + + int index = in.skyLightLevel * 16 + in.blockLightLevel; + float4 brightness; + brightness.r = (float)lightMap[index * 4]; + brightness.g = (float)lightMap[index * 4 + 1]; + brightness.b = (float)lightMap[index * 4 + 2]; + brightness.a = 255; + color *= brightness / 255.0; + return color; } diff --git a/Sources/Core/Sources/World/Chunk/Chunk.swift b/Sources/Core/Sources/World/Chunk/Chunk.swift index 4ec7b47a..1aa9c64e 100644 --- a/Sources/Core/Sources/World/Chunk/Chunk.swift +++ b/Sources/Core/Sources/World/Chunk/Chunk.swift @@ -75,7 +75,10 @@ public final class Chunk { /// - biomeIds: The biomes of the chunk in 4x4x4 blocks. Indexed in the same order as blocks. (Index is block index divided by 4). /// - lighting: Lighting data for the chunk /// - heightMap: Information about the highest blocks in each column of the chunk. - public init(sections: [Chunk.Section], blockEntities: [BlockEntity], biomeIds: [UInt8], lighting: ChunkLighting? = nil, heightMap: HeightMap) { + public init( + sections: [Chunk.Section], blockEntities: [BlockEntity], biomeIds: [UInt8], + lighting: ChunkLighting? = nil, heightMap: HeightMap + ) { self.sections = sections self.blockEntities = blockEntities self.biomeIds = biomeIds @@ -120,7 +123,9 @@ public final class Chunk { /// - Returns: Block id of block. Returns 0 (air) if `index` is invalid (outside chunk). public func getBlockId(at index: Int, acquireLock: Bool = true) -> Int { if !Self.isValidBlockIndex(index) { - log.warning("Invalid block index passed to Chunk.getBlockStateId(at:), index=\(index), returning block id 0 (air)") + log.warning( + "Invalid block index passed to Chunk.getBlockStateId(at:), index=\(index), returning block id 0 (air)" + ) return 0 } @@ -327,7 +332,9 @@ public final class Chunk { /// - position: Position of block. /// - level: The new block light level. /// - acquireLock: Whether to acquire a lock or not. Only set to false if you know what you're doing. See ``Chunk``. - public func setBlockLightLevel(at position: BlockPosition, to level: Int, acquireLock: Bool = true) { + public func setBlockLightLevel( + at position: BlockPosition, to level: Int, acquireLock: Bool = true + ) { if acquireLock { lock.acquireWriteLock() } defer { if acquireLock { lock.unlock() } } @@ -351,13 +358,27 @@ public final class Chunk { /// - position: Position of block. /// - level: The new sky light level. /// - acquireLock: Whether to acquire a lock or not. Only set to false if you know what you're doing. See ``Chunk``. - public func setSkyLightLevel(at position: BlockPosition, to level: Int, acquireLock: Bool = true) { + public func setSkyLightLevel(at position: BlockPosition, to level: Int, acquireLock: Bool = true) + { if acquireLock { lock.acquireWriteLock() } defer { if acquireLock { lock.unlock() } } lighting.setSkyLightLevel(at: position, to: level) } + // TODO: Make method naming consistent (chunk lighting uses `getLightLevel` but chunk uses `lightLevel`). + /// Returns the block and sky light levels for the given block. + /// - Parameters: + /// - position: Position of block. + /// - acquireLock: Position of block. + /// - Returns: Whether to acquire a lock or not. Only set to false if you know what you're doing. See ``Chunk``. + public func lightLevel(at position: BlockPosition, acquireLock: Bool = true) -> LightLevel { + if acquireLock { lock.acquireReadLock() } + defer { if acquireLock { lock.unlock() } } + + return lighting.getLightLevel(at: position) + } + /// Updates the chunk's lighting with data received from the server. /// - Parameters: /// - data: Data received from the server. @@ -464,9 +485,8 @@ public final class Chunk { /// - Returns: `true` if the block position is contained within the a chunk. private static func isValidBlockPosition(_ position: BlockPosition) -> Bool { - return ( - position.x < Chunk.width && position.x >= 0 && - position.z < Chunk.depth && position.z >= 0 && - position.y < Chunk.height && position.y >= 0) + return + (position.x < Chunk.width && position.x >= 0 && position.z < Chunk.depth && position.z >= 0 + && position.y < Chunk.height && position.y >= 0) } } diff --git a/Sources/Core/Sources/World/Light/LightLevel.swift b/Sources/Core/Sources/World/Light/LightLevel.swift index 2315139b..9cc23950 100644 --- a/Sources/Core/Sources/World/Light/LightLevel.swift +++ b/Sources/Core/Sources/World/Light/LightLevel.swift @@ -3,19 +3,21 @@ import Foundation /// A light level. Includes both the block light and sky light level. public struct LightLevel { /// The sky light level used for unloaded chunks. - public static var defaultSkyLightLevel = 0 + public static let defaultSkyLightLevel = 0 /// The block light level used for unloaded chunks. - public static var defaultBlockLightLevel = 0 + public static let defaultBlockLightLevel = 0 /// The maximum light level. - public static var maximumLightLevel = 15 + public static let maximumLightLevel = 15 /// The number of light levels. - public static var levelCount = maximumLightLevel + 1 - + public static let levelCount = maximumLightLevel + 1 + /// The default light level used for uninitialized chunks etc. + public static let `default` = Self(sky: defaultSkyLightLevel, block: defaultBlockLightLevel) + /// The sky light level. public var sky: Int /// The block light level. public var block: Int - + /// Creates a new light level value. /// - Parameters: /// - sky: The sky light level. @@ -24,13 +26,7 @@ public struct LightLevel { self.sky = sky self.block = block } - - /// Creates the default light level. - public init() { - sky = Self.defaultSkyLightLevel - block = Self.defaultBlockLightLevel - } - + /// Gets the highest sky light level and block light level from two light levels. /// - Parameters: /// - a: The first light level. diff --git a/Sources/Core/Sources/World/World.swift b/Sources/Core/Sources/World/World.swift index 9751fdd4..e6ce3658 100644 --- a/Sources/Core/Sources/World/World.swift +++ b/Sources/Core/Sources/World/World.swift @@ -1,5 +1,5 @@ -import Foundation import FirebladeMath +import Foundation import Logging /// Represents a Minecraft world. Completely thread-safe. @@ -152,8 +152,9 @@ public class World { // TODO: Avoid the force unwrap. Possibly by updating the BiomeRegistry to ensure that // a plains biome is always present to use as a default (perhaps as a defaultBiome // property). - let biome = getBiome(at: position) ?? - RegistryStore.shared.biomeRegistry.biome(for: Identifier(name: "plains"))! + let biome = + getBiome(at: position) ?? RegistryStore.shared.biomeRegistry.biome( + for: Identifier(name: "plains"))! let skyColor = biome.skyColor.floatVector let skyBrightness = getSkyBrightness() @@ -208,7 +209,8 @@ public class World { let position = ray.origin let blockPosition = BlockPosition(x: Int(position.x), y: Int(position.y), z: Int(position.z)) - let biome = getBiome(at: blockPosition) + let biome = + getBiome(at: blockPosition) ?? RegistryStore.shared.biomeRegistry.biome(for: Identifier(name: "plains"))! let fluidOnEyes = getFluidState(at: position, acquireLock: acquireLock) @@ -264,11 +266,11 @@ public class World { } } - // As the player nears the + // As the player nears the let voidFadeStart: Float = isFlat ? 1 : 32 if position.y < voidFadeStart { let amount = max(0, position.y / voidFadeStart) - fogColor *= amount * amount + fogColor *= amount * amount } return fogColor @@ -302,7 +304,7 @@ public class World { // TODO: Calculate density as per reverse engineering document return Fog(color: fogColor, style: .exponential(density: 0.05)) } - + // TODO: If player has blindness, the fog starts at 5/4 and ends at 5, lerping up to // starting at renderDistance/4 and ending at renderDistance over the last second of blindness @@ -396,10 +398,11 @@ public class World { } blockBreakingLock.unlock() - eventBus.dispatch(Event.SingleBlockUpdate( - position: position, - newState: state - )) + eventBus.dispatch( + Event.SingleBlockUpdate( + position: position, + newState: state + )) } else { log.warning("Cannot set block in non-existent chunk, chunkPosition=\(position.chunk)") } @@ -425,7 +428,8 @@ public class World { } lightingEngine.updateLighting(at: positions, in: self) } else { - log.warning("Cannot handle multi-block change in non-existent chunk, chunkPosition=\(chunkPosition)") + log.warning( + "Cannot handle multi-block change in non-existent chunk, chunkPosition=\(chunkPosition)") return } } else { @@ -433,7 +437,9 @@ public class World { if let chunk = chunk(at: update.position.chunk) { chunk.setBlockId(at: update.position.relativeToChunk, to: update.newState) } else { - log.warning("Cannot handle multi-block change in non-existent chunk, chunkPosition=\(update.position.chunk)") + log.warning( + "Cannot handle multi-block change in non-existent chunk, chunkPosition=\(update.position.chunk)" + ) return } } @@ -515,7 +521,6 @@ public class World { /// Sets the block light level of a block. Does not propagate the change and does not verify the level is valid. /// /// If `position` is in a chunk that isn't loaded or is above y=255 or below y=0, nothing happens. - /// /// - Parameters: /// - position: A block position relative to the world. /// - level: The new block light level. Should be from 0 to 15 inclusive. Not validated. @@ -526,7 +531,6 @@ public class World { } /// Gets the block light level for the given block. - /// /// - Parameter position: Position of block. /// - Returns: The block light level of the block. If the given position isn't loaded, ``LightLevel/defaultBlockLightLevel`` is returned. public func getBlockLightLevel(at position: BlockPosition) -> Int { @@ -540,7 +544,6 @@ public class World { /// Sets the sky light level of a block. Does not propagate the change and does not verify the level is valid. /// /// If `position` is in a chunk that isn't loaded or is above y=255 or below y=0, nothing happens. - /// /// - Parameters: /// - position: A block position relative to the world. /// - level: The new sky light level. Should be from 0 to 15 inclusive. Not validated. @@ -551,7 +554,6 @@ public class World { } /// Gets the sky light level for the given block. - /// /// - Parameter position: Position of block. /// - Returns: The sky light level of the block. If the given position isn't loaded, ``LightLevel/defaultSkyLightLevel`` is returned. public func getSkyLightLevel(at position: BlockPosition) -> Int { @@ -562,6 +564,17 @@ public class World { } } + /// Gets the block and sky light levels for the given block. + /// - Parameter position: Position of block. + /// - Returns: The light levels of the block. If the given position isn't loaded, ``LightLevel/default`` is returned. + public func getLightLevel(at position: BlockPosition) -> LightLevel { + if let chunk = chunk(at: position.chunk) { + return chunk.lightLevel(at: position.relativeToChunk) + } else { + return .default + } + } + /// Updates a chunk's lighting with lighting data received from the server. /// - Parameters: /// - position: Position of chunk to update. @@ -590,10 +603,11 @@ public class World { terrainLock.unlock() } - eventBus.dispatch(Event.UpdateChunkLighting( - position: position, - data: data - )) + eventBus.dispatch( + Event.UpdateChunkLighting( + position: position, + data: data + )) } // MARK: Biomes From 1652ea560b177a6ee482e33574aac08716532bc0 Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 22 Aug 2024 02:04:51 +1000 Subject: [PATCH 73/84] Update delta-core dependency path to start with ./ (hopefully fixes an SPM issue) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index e4f816be..7d059759 100644 --- a/Package.swift +++ b/Package.swift @@ -81,7 +81,7 @@ let package = Package( dependencies: [ // See Notes/PluginSystem.md for more details on the architecture of the project in regards to dependencies, targets and linking // In short, the dependencies for DeltaCore can be found in Sources/Core/Package.swift - .package(name: "DeltaCore", path: "Sources/Core"), + .package(name: "DeltaCore", path: "./Sources/Core"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), .package( url: "https://github.com/stackotter/SwordRPC", From 160c74917aa66f71e64b3f2c5e4c4d2c0ac986ac Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:09:27 +1000 Subject: [PATCH 74/84] Bump SwiftCrossUI version --- Notes/GUIRendering.md | 2 +- Package.resolved | 193 ++++++++++++++++--------- Package.swift | 2 +- Sources/ClientGtk/DeltaClientApp.swift | 2 +- Sources/Core/Package.swift | 2 +- 5 files changed, 129 insertions(+), 72 deletions(-) diff --git a/Notes/GUIRendering.md b/Notes/GUIRendering.md index 61ce62a2..62b27a4a 100644 --- a/Notes/GUIRendering.md +++ b/Notes/GUIRendering.md @@ -25,7 +25,7 @@ relatively consistent. This improvement gave a 2.36x reduction in encode time which is pretty great. -This improvement isn't foolproof, it should be conservative to always retain ordering when required, +This improvement isn't foolproof, it should be conservative enough to always retain ordering when required, but it doesn't seem to always combine meshes when they can be combined. This can be worked around by refactoring mesh generation to group by array texture when possible. I had to do this with `GUIList`'s row background generation (by putting all the backgrounds first) and it worked a charm. diff --git a/Package.resolved b/Package.resolved index 95732fc6..8f0f6725 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,22 +18,13 @@ "version" : "1.1.1" } }, - { - "identity" : "asyncextensions", - "kind" : "remoteSourceControl", - "location" : "https://github.com/lhoward/AsyncExtensions", - "state" : { - "branch" : "linux", - "revision" : "0d96d3550ef94f83c2f300021f9985e4fb44f7af" - } - }, { "identity" : "bigint", "kind" : "remoteSourceControl", "location" : "https://github.com/attaswift/BigInt.git", "state" : { - "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version" : "5.3.0" + "revision" : "114343a705df4725dfe7ab8a2a326b8883cfd79c", + "version" : "5.5.1" } }, { @@ -86,25 +77,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/JJLISO8601DateFormatter", "state" : { - "revision" : "ed1d996123688bade6e895aa49595f0d862900e7", - "version" : "0.1.7" + "revision" : "741a9e45db01148a8ac60c5e12f7c978181a22d3", + "version" : "0.1.8" } }, { - "identity" : "loggerapi", + "identity" : "jpeg", "kind" : "remoteSourceControl", - "location" : "https://github.com/Kitura/LoggerAPI.git", + "location" : "https://github.com/stackotter/jpeg", "state" : { - "revision" : "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", - "version" : "1.9.200" + "revision" : "a27e47f49479993b2541bc5f5c95d9354ed567a0", + "version" : "1.0.2" } }, { - "identity" : "lvglswift", + "identity" : "libpng", "kind" : "remoteSourceControl", - "location" : "https://github.com/PADL/LVGLSwift", + "location" : "https://github.com/the-swift-collective/libpng", "state" : { - "revision" : "19c19a942153b50d61486faf1d0d45daf79e7be5" + "revision" : "0eff23aca92a086b7892831f5cb4f58e15be9449", + "version" : "1.6.45" + } + }, + { + "identity" : "loggerapi", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kitura/LoggerAPI.git", + "state" : { + "revision" : "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", + "version" : "1.9.200" } }, { @@ -125,21 +126,13 @@ "version" : "0.7.0" } }, - { - "identity" : "qlift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Longhanks/qlift", - "state" : { - "revision" : "ddab1f1ecc113ad4f8e05d2999c2734cdf706210" - } - }, { "identity" : "rainbow", "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Rainbow", "state" : { - "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", - "version" : "4.0.1" + "revision" : "0c627a4f8a39ef37eadec1ceec02e4a7f55561ac", + "version" : "4.1.0" } }, { @@ -174,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "8d712376c99fc0267aa0e41fea732babe365270a", - "version" : "1.3.3" + "revision" : "e7039aaa4d9cf386fa8324a89f258c3f2c54d751", + "version" : "1.6.0" } }, { @@ -192,7 +185,16 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-cross-ui", "state" : { - "revision" : "21410866cb8241f322633bed83018acb74840e87" + "revision" : "20a6e6f4423edda14040fbd012f23d99a4b17823" + } + }, + { + "identity" : "swift-cwinrt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thebrowsercompany/swift-cwinrt", + "state" : { + "branch" : "main", + "revision" : "ef09b9dd58b97b29f444bbe28362de5bafaaaf04" } }, { @@ -204,6 +206,24 @@ "revision" : "7160e2d8799f8b182498ab699aa695cb66cf4ea6" } }, + { + "identity" : "swift-image-formats", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-image-formats", + "state" : { + "revision" : "05a0169a2a5e9365a058e9aa13da5937be6e2586", + "version" : "0.3.1" + } + }, + { + "identity" : "swift-libwebp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-libwebp", + "state" : { + "revision" : "61dc3787c764022ad2f5ab4f9994a569afe86f9f", + "version" : "0.2.0" + } + }, { "identity" : "swift-log", "kind" : "remoteSourceControl", @@ -213,13 +233,22 @@ "version" : "1.5.4" } }, + { + "identity" : "swift-macro-toolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stackotter/swift-macro-toolkit", + "state" : { + "revision" : "e706aa98bc28f82677923f7b8f560bba6f90fac2", + "version" : "0.6.0" + } + }, { "identity" : "swift-nio", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250", - "version" : "2.41.1" + "revision" : "ba72f31e11275fc5bf060c966cf6c1f36842a291", + "version" : "2.79.0" } }, { @@ -236,8 +265,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", - "version" : "2.22.1" + "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", + "version" : "2.29.0" } }, { @@ -254,8 +283,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-parsing", "state" : { - "revision" : "a0e7d73f462c1c38c59dc40a3969ac40cea42950", - "version" : "0.13.0" + "revision" : "3432cb81164dd3d69a75d0d63205be5fbae2c34b", + "version" : "0.14.1" } }, { @@ -271,69 +300,88 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", - "version" : "510.0.2" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { - "identity" : "swiftcpudetect", + "identity" : "swift-system", "kind" : "remoteSourceControl", - "location" : "https://github.com/JWhitmore1/SwiftCPUDetect", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", + "version" : "1.4.2" + } + }, + { + "identity" : "swift-uwp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/thebrowsercompany/swift-uwp", "state" : { "branch" : "main", - "revision" : "5ca694c6ad7eef1199d69463fa956c24c202465f" + "revision" : "e373f4ffb128cba831cc2f83324320b287b7c1b1" } }, { - "identity" : "swiftpackagesbase", + "identity" : "swift-windowsappsdk", "kind" : "remoteSourceControl", - "location" : "https://github.com/ITzTravelInTime/SwiftPackagesBase", + "location" : "https://github.com/wabiverse/swift-windowsappsdk", "state" : { - "revision" : "79477622b5dbacc3722d485c5060c46a90740016", - "version" : "0.0.23" + "branch" : "main", + "revision" : "9a92d35fd4734a10c9f396f48acf209a9ae406cc" } }, { - "identity" : "swiftterm", + "identity" : "swift-windowsfoundation", "kind" : "remoteSourceControl", - "location" : "https://github.com/migueldeicaza/SwiftTerm.git", + "location" : "https://github.com/thebrowsercompany/swift-windowsfoundation", "state" : { - "revision" : "e2b431dbf73f775fb4807a33e4572ffd3dc6933a", - "version" : "1.2.5" + "branch" : "main", + "revision" : "ba7a8b5000ed3f9f077000d1a31f2a0b19907657" } }, { - "identity" : "swiftyrequest", + "identity" : "swift-winui", "kind" : "remoteSourceControl", - "location" : "https://github.com/Kitura/SwiftyRequest.git", + "location" : "https://github.com/wabiverse/swift-winui", "state" : { - "revision" : "2c543777a5088bed811503a68551a4b4eceac198", - "version" : "3.2.200" + "branch" : "main", + "revision" : "c7a5cf4881479132c2d00e63b84a711e87670856" } }, { - "identity" : "swordrpc", + "identity" : "swiftcpudetect", "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/SwordRPC", + "location" : "https://github.com/JWhitmore1/SwiftCPUDetect", "state" : { - "revision" : "3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed" + "branch" : "main", + "revision" : "5ca694c6ad7eef1199d69463fa956c24c202465f" } }, { - "identity" : "termkit", + "identity" : "swiftpackagesbase", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ITzTravelInTime/SwiftPackagesBase", + "state" : { + "revision" : "79477622b5dbacc3722d485c5060c46a90740016", + "version" : "0.0.23" + } + }, + { + "identity" : "swiftyrequest", "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/TermKit", + "location" : "https://github.com/Kitura/SwiftyRequest.git", "state" : { - "revision" : "163afa64f1257a0c026cc83ed8bc47a5f8fc9704" + "revision" : "2c543777a5088bed811503a68551a4b4eceac198", + "version" : "3.2.200" } }, { - "identity" : "textbufferkit", + "identity" : "swordrpc", "kind" : "remoteSourceControl", - "location" : "https://github.com/migueldeicaza/TextBufferKit.git", + "location" : "https://github.com/stackotter/SwordRPC", "state" : { - "revision" : "7f3ed5b1d7288de34ad853544d802647be11cfcf", - "version" : "0.3.0" + "revision" : "3ddf125eeb3d83cb17a6e4cda685f9c80e0d4bed" } }, { @@ -341,8 +389,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2", - "version" : "1.1.2" + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" } }, { @@ -371,6 +419,15 @@ "revision" : "8abdd7a5e943afe68e7b03fdaa63b21c042a3893", "version" : "1.2.9" } + }, + { + "identity" : "zlib", + "kind" : "remoteSourceControl", + "location" : "https://github.com/the-swift-collective/zlib.git", + "state" : { + "revision" : "f1d153b90420f9fcc6ef916cd67ea96f0e68d137", + "version" : "1.3.2" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 7d059759..0cf56f20 100644 --- a/Package.swift +++ b/Package.swift @@ -89,7 +89,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-cross-ui", - revision: "21410866cb8241f322633bed83018acb74840e87" + revision: "20a6e6f4423edda14040fbd012f23d99a4b17823" ), ], targets: targets diff --git a/Sources/ClientGtk/DeltaClientApp.swift b/Sources/ClientGtk/DeltaClientApp.swift index 83b3b8d8..5c570dd9 100644 --- a/Sources/ClientGtk/DeltaClientApp.swift +++ b/Sources/ClientGtk/DeltaClientApp.swift @@ -1,7 +1,7 @@ +import DefaultBackend import DeltaCore import Dispatch import Foundation -import GtkBackend import SwiftCrossUI @main diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 24b614da..8ab174fb 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -74,7 +74,7 @@ var targets: [Target] = [ let package = Package( name: "DeltaCore", - platforms: [.macOS(.v11)], + platforms: [.macOS(.v11), .iOS(.v14)], products: [ .library(name: "DeltaCore", type: .dynamic, targets: productTargets), .library(name: "StaticDeltaCore", type: .static, targets: productTargets), From 38d088bf5b2d83e38b04bac9d524d535a37aec7f Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:15:56 +1000 Subject: [PATCH 75/84] CI: Bump workflows to newer software (fixes Linux error + macOS infinite queue) --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 67a553ed..a15f4536 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,14 +7,14 @@ on: jobs: build-macos: - runs-on: macOS-12 + runs-on: macOS-15 steps: - name: Checkout uses: actions/checkout@v2 - name: List Xcodes run: ls /Applications - - name: Force Xcode 14.2 - run: sudo xcode-select -switch /Applications/Xcode_14.2.app + - name: Force Xcode 16.4 + run: sudo xcode-select -switch /Applications/Xcode_16.4.app - name: Version run: swift --version - name: Download swift-bundler @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Swift - uses: swift-actions/setup-swift@v1 + uses: swift-actions/setup-swift@v3 with: swift-version: 5.7 - name: Checkout From fe32d6fa2ab11758933160860fccdfcdbd9e3fca Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:19:51 +1000 Subject: [PATCH 76/84] CI: Fix workflows more (bump more action versions) --- .github/workflows/build.yml | 8 ++++---- .github/workflows/swiftlint.yml | 2 +- .github/workflows/test.yml | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a15f4536..cb2c9dd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: macOS-15 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: List Xcodes run: ls /Applications - name: Force Xcode 16.4 @@ -29,7 +29,7 @@ jobs: - name: Zip .app run: zip -r DeltaClient.zip DeltaClient.app - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v7 with: name: DeltaClient path: ./DeltaClient.zip @@ -37,11 +37,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Swift - uses: swift-actions/setup-swift@v3 + uses: swift-actions/setup-swift@v2 with: swift-version: 5.7 - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Build run: | cd Sources/Core diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 39c548c1..d343dbee 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -10,7 +10,7 @@ jobs: runs-on: macOS-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v6 - name: Lint run: | URL="https://github.com/realm/SwiftLint/releases/download/0.50.3/portable_swiftlint.zip" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 588a3969..93ee61b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,12 @@ on: jobs: test-macos: - runs-on: macOS-12 + runs-on: macOS-15 steps: - name: Checkout - uses: actions/checkout@v2 - - name: Force Xcode 14.2 - run: sudo xcode-select -switch /Applications/Xcode_14.2.app + uses: actions/checkout@v6 + - name: Force Xcode 16.4 + run: sudo xcode-select -switch /Applications/Xcode_16.4.app - name: Version run: swift --version - name: Test @@ -23,11 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Swift - uses: swift-actions/setup-swift@v1 + uses: swift-actions/setup-swift@v2 with: swift-version: 5.7 - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Test run: | cd Sources/Core From 4f531c428a462a5a8077a27599dd6d2a44a32d88 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:30:14 +1000 Subject: [PATCH 77/84] CI: Switch away from universal builds for macOS (broken) and switch Linux Swift actions (broken) --- .github/workflows/build.yml | 31 ++++++++++++++++++++++--------- .github/workflows/test.yml | 4 ++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb2c9dd1..f1613d94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,24 +22,37 @@ jobs: curl -o swift-bundler -L https://github.com/stackotter/swift-bundler/releases/download/v2.0.4/swift-bundler chmod +x ./swift-bundler cp ./swift-bundler /usr/local/bin - - name: Build + - name: Build arm64 + run: | + ./swift-bundler bundle -c release -o . + plutil -insert MetalCaptureEnabled -bool YES DeltaClient.app/Contents/Info.plist + mv DeltaClient.app DeltaClient-arm64.app + - name: Build x86 run: | - ./swift-bundler bundle -c release -o . -u + ./swift-bundler bundle -c release -o . --arch x86_64 plutil -insert MetalCaptureEnabled -bool YES DeltaClient.app/Contents/Info.plist - - name: Zip .app - run: zip -r DeltaClient.zip DeltaClient.app - - name: Upload artifact + mv DeltaClient.app DeltaClient-x86_64.app + - name: Zip .app (arm64) + run: zip -r DeltaClient-arm64.zip DeltaClient-arm64.app + - name: Zip .app (x86_64) + run: zip -r DeltaClient-x86_64.zip DeltaClient-x86_64.app + - name: Upload artifact (arm64) + uses: actions/upload-artifact@v7 + with: + name: DeltaClient-arm64 + path: ./DeltaClient-arm64.zip + - name: Upload artifact (x86_64) uses: actions/upload-artifact@v7 with: - name: DeltaClient - path: ./DeltaClient.zip + name: DeltaClient-x86_64 + path: ./DeltaClient-x86_64.zip build-linux: runs-on: ubuntu-latest steps: - name: Setup Swift - uses: swift-actions/setup-swift@v2 + uses: SwiftyLab/setup-swift@latest with: - swift-version: 5.7 + swift-version: "5.7" - name: Checkout uses: actions/checkout@v6 - name: Build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93ee61b0..da3920ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup Swift - uses: swift-actions/setup-swift@v2 + uses: SwiftyLab/setup-swift@latest with: - swift-version: 5.7 + swift-version: "5.7" - name: Checkout uses: actions/checkout@v6 - name: Test From 62a36b7a135ecd3d7e36d3b52460f965c6b7d622 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:34:34 +1000 Subject: [PATCH 78/84] CI: Bump weichsel/ZipFoundation version to fix Linux compilation --- Package.resolved | 4 ++-- Sources/Core/Package.resolved | 8 ++++---- Sources/Core/Package.swift | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8f0f6725..952e999e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -398,8 +398,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation.git", "state" : { - "revision" : "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0", - "version" : "0.9.19" + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" } }, { diff --git a/Sources/Core/Package.resolved b/Sources/Core/Package.resolved index 1c86d025..3b8edd85 100644 --- a/Sources/Core/Package.resolved +++ b/Sources/Core/Package.resolved @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "9d8719c8bebdc79740b6969c912ac706eb721d7a", - "version" : "0.0.7" + "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", + "version" : "1.4.1" } }, { @@ -256,8 +256,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/weichsel/ZIPFoundation.git", "state" : { - "revision" : "43ec568034b3731101dbf7670765d671c30f54f3", - "version" : "0.9.16" + "revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d", + "version" : "0.9.20" } }, { diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index 8ab174fb..f46b42d2 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -80,7 +80,7 @@ let package = Package( .library(name: "StaticDeltaCore", type: .static, targets: productTargets), ], dependencies: [ - .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.0"), + .package(url: "https://github.com/weichsel/ZIPFoundation.git", from: "0.9.20"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.0.3"), .package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"), .package(url: "https://github.com/stackotter/ecs.git", branch: "master"), @@ -93,7 +93,8 @@ let package = Package( .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0"), .package( url: "https://github.com/stackotter/swift-png", - revision: "b68a5662ef9887c8f375854720b3621f772bf8c5"), + revision: "b68a5662ef9887c8f375854720b3621f772bf8c5" + ), .package(url: "https://github.com/stackotter/ASN1Parser", branch: "main"), .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.6.0"), .package(url: "https://github.com/Kitura/SwiftyRequest.git", from: "3.1.0"), From 65d562f5cda4f926c8a5c82d835d2b9dea91ede5 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:42:48 +1000 Subject: [PATCH 79/84] Bump sushichop/Puppy version to fix Linux compilation --- Package.resolved | 8 ++++---- Sources/Core/Package.resolved | 8 ++++---- Sources/Core/Package.swift | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Package.resolved b/Package.resolved index 952e999e..3053a5f1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sushichop/Puppy", "state" : { - "revision" : "b5af02a72a5a1f92a68e6eceee19cac804067ad9", - "version" : "0.7.0" + "revision" : "80ebe6ab25fb7050904647cb96f6ad7bee77bad6", + "version" : "0.9.0" } }, { @@ -229,8 +229,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" } }, { diff --git a/Sources/Core/Package.resolved b/Sources/Core/Package.resolved index 3b8edd85..8fe5a610 100644 --- a/Sources/Core/Package.resolved +++ b/Sources/Core/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sushichop/Puppy", "state" : { - "revision" : "b5af02a72a5a1f92a68e6eceee19cac804067ad9", - "version" : "0.7.0" + "revision" : "80ebe6ab25fb7050904647cb96f6ad7bee77bad6", + "version" : "0.9.0" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" } }, { diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index f46b42d2..da842cf0 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -99,7 +99,7 @@ let package = Package( .package(url: "https://github.com/krzyzanowskim/CryptoSwift", from: "1.6.0"), .package(url: "https://github.com/Kitura/SwiftyRequest.git", from: "3.1.0"), .package(url: "https://github.com/JWhitmore1/SwiftCPUDetect", branch: "main"), - .package(url: "https://github.com/sushichop/Puppy", from: "0.6.0"), + .package(url: "https://github.com/sushichop/Puppy", .upToNextMinor(from: "0.9.0")), .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.1"), ], targets: targets From 4029ce38f470f05a237fc2836ba33d6db097615f Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:54:47 +1000 Subject: [PATCH 80/84] CI: Bump swift-bundler version and Swift tools versions to fix compilation --- .github/workflows/build.yml | 31 ++++++++++++++++++++----------- Package.swift | 2 +- Sources/Core/Package.swift | 2 +- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1613d94..ed5e9fdc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,21 +17,30 @@ jobs: run: sudo xcode-select -switch /Applications/Xcode_16.4.app - name: Version run: swift --version - - name: Download swift-bundler + + - name: Cache swift-bundler + id: cache-swift-bundler + uses: actions/cache@v5 + with: + path: sbun + key: ${{ runner.os }}-swift-bundler + - name: Build swift-bundler + if: steps.cache-swift-bundler.outputs.cache-hit != 'true' run: | - curl -o swift-bundler -L https://github.com/stackotter/swift-bundler/releases/download/v2.0.4/swift-bundler - chmod +x ./swift-bundler - cp ./swift-bundler /usr/local/bin + git clone https://github.com/moreSwift/swift-bundler + cd swift-bundler + git checkout 6d72c4f442cc2c57c2559f3df21b3777b1d0a917 + swift build --product swift-bundler + cp .build/debug/swift-bundler ../sbun + - name: Build arm64 run: | - ./swift-bundler bundle -c release -o . - plutil -insert MetalCaptureEnabled -bool YES DeltaClient.app/Contents/Info.plist - mv DeltaClient.app DeltaClient-arm64.app + ./sbun bundle -c release --arch arm64 + mv "$(./sbun bundle --show-bundle-path)" DeltaClient-arm64.app - name: Build x86 run: | - ./swift-bundler bundle -c release -o . --arch x86_64 - plutil -insert MetalCaptureEnabled -bool YES DeltaClient.app/Contents/Info.plist - mv DeltaClient.app DeltaClient-x86_64.app + ./sbun bundle -c release --arch x86_64 + mv "$(./sbun bundle --show-bundle-path)" DeltaClient-x86_64.app - name: Zip .app (arm64) run: zip -r DeltaClient-arm64.zip DeltaClient-arm64.app - name: Zip .app (x86_64) @@ -52,7 +61,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "5.7" + swift-version: "5.9" - name: Checkout uses: actions/checkout@v6 - name: Build diff --git a/Package.swift b/Package.swift index 0cf56f20..848a2069 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.9 import PackageDescription diff --git a/Sources/Core/Package.swift b/Sources/Core/Package.swift index da842cf0..de2ee5c6 100644 --- a/Sources/Core/Package.swift +++ b/Sources/Core/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.9 import PackageDescription From e8f1696cd75757dce0d9bf6971c0231c996ec8f4 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 10:56:11 +1000 Subject: [PATCH 81/84] CI: Bump test.yml Swift version to match package Swift version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da3920ef..f69fe53a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "5.7" + swift-version: "5.9" - name: Checkout uses: actions/checkout@v6 - name: Test From 82ddcd618c8579efc7aa9e15ba5a5cbc4c48454e Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 11:03:40 +1000 Subject: [PATCH 82/84] CI: Bump Linux workflow jobs to Swift 6.0 (work around 5.9 break related to libc nullability annotations) --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed5e9fdc..e7aaaf90 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "5.9" + swift-version: "6.0" - name: Checkout uses: actions/checkout@v6 - name: Build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f69fe53a..eebe0392 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "5.9" + swift-version: "6.0" - name: Checkout uses: actions/checkout@v6 - name: Test From 4edcf28e9b6cc908d41b29be8361816a1a6a2c25 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 11:16:09 +1000 Subject: [PATCH 83/84] CI: Bump Linux workflow jobs to Swift 6.3 (6.0 wasn't enough) --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7aaaf90..33589379 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.0" + swift-version: "6.2" - name: Checkout uses: actions/checkout@v6 - name: Build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eebe0392..41a27344 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.0" + swift-version: "6.2" - name: Checkout uses: actions/checkout@v6 - name: Test From 83ff6c38264935bb2fdf15eafd5d032b6b6b22f4 Mon Sep 17 00:00:00 2001 From: stackotter Date: Wed, 15 Apr 2026 11:28:14 +1000 Subject: [PATCH 84/84] CI: Run 'swift package update' in hopes to fix remaining Linux errors --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 2 +- Package.resolved | 237 +++++++++++++++++++++++++--------- Package.swift | 2 +- Sources/Core/Package.resolved | 164 +++++++++++++++++++---- 5 files changed, 312 insertions(+), 95 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33589379..ed5e9fdc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.2" + swift-version: "5.9" - name: Checkout uses: actions/checkout@v6 - name: Build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41a27344..f69fe53a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest with: - swift-version: "6.2" + swift-version: "5.9" - name: Checkout uses: actions/checkout@v6 - name: Test diff --git a/Package.resolved b/Package.resolved index 3053a5f1..b1d2b9c1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/attaswift/BigInt.git", "state" : { - "revision" : "114343a705df4725dfe7ab8a2a326b8883cfd79c", - "version" : "5.5.1" + "revision" : "e07e00fa1fd435143a2dcf8b7eec9a7710b2fdfe", + "version" : "5.7.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift", "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" + "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f", + "version" : "1.9.0" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/JJLISO8601DateFormatter", "state" : { - "revision" : "741a9e45db01148a8ac60c5e12f7c978181a22d3", - "version" : "0.1.8" + "revision" : "50d5ea26ffd3f82e3db8516d939d80b745e168cf", + "version" : "0.2.0" } }, { @@ -99,6 +99,15 @@ "version" : "1.6.45" } }, + { + "identity" : "libwebp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/the-swift-collective/libwebp", + "state" : { + "revision" : "5f745a17b9a5c2a4283f17c2cde4517610ab5f99", + "version" : "1.4.1" + } + }, { "identity" : "loggerapi", "kind" : "remoteSourceControl", @@ -131,8 +140,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Rainbow", "state" : { - "revision" : "0c627a4f8a39ef37eadec1ceec02e4a7f55561ac", - "version" : "4.1.0" + "revision" : "cdf146ae671b2624917648b61c908d1244b98ca1", + "version" : "4.2.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" } }, { @@ -140,8 +158,26 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" + "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b", + "version" : "1.7.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "9f542610331815e29cc3821d3b6f488db8715517", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272", + "version" : "1.1.3" } }, { @@ -149,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-dns-resolver.git", "state" : { - "revision" : "08c07ff31a745ee5e522ac10132fb4949834d925", - "version" : "0.4.0" + "revision" : "36eedf108f9eda4feeaf47f3dfce657a88ef19aa", + "version" : "0.6.0" } }, { @@ -158,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -167,8 +203,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "e7039aaa4d9cf386fa8324a89f258c3f2c54d751", - "version" : "1.6.0" + "revision" : "206cbce3882b4de9aee19ce62ac5b7306cadd45b", + "version" : "1.7.3" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" } }, { @@ -176,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "6675bc0ff86e61436e615df6fc5174e043e57924", + "version" : "1.4.1" } }, { @@ -185,16 +230,44 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-cross-ui", "state" : { - "revision" : "20a6e6f4423edda14040fbd012f23d99a4b17823" + "revision" : "835c9ea90a3b1d2dc20436eb2c87b00037b4eb9c", + "version" : "0.4.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "bb4ba815dab96d4edc1e0b86d7b9acf9ff973a84", + "version" : "4.3.1" } }, { "identity" : "swift-cwinrt", "kind" : "remoteSourceControl", - "location" : "https://github.com/thebrowsercompany/swift-cwinrt", + "location" : "https://github.com/moreSwift/swift-cwinrt", "state" : { - "branch" : "main", - "revision" : "ef09b9dd58b97b29f444bbe28362de5bafaaaf04" + "revision" : "601287a2a89e8e56e8aa53782b6054db8be0bce8", + "version" : "0.1.2" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" } }, { @@ -211,17 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-image-formats", "state" : { - "revision" : "05a0169a2a5e9365a058e9aa13da5937be6e2586", - "version" : "0.3.1" - } - }, - { - "identity" : "swift-libwebp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stackotter/swift-libwebp", - "state" : { - "revision" : "61dc3787c764022ad2f5ab4f9994a569afe86f9f", - "version" : "0.2.0" + "revision" : "2e5dc1ead747afab9fd517d81316d961969e3610", + "version" : "0.3.3" } }, { @@ -238,8 +302,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-macro-toolkit", "state" : { - "revision" : "e706aa98bc28f82677923f7b8f560bba6f90fac2", - "version" : "0.6.0" + "revision" : "d319bcc2586f7dbc6a09c05792105078263f1ed3", + "version" : "0.7.2" + } + }, + { + "identity" : "swift-mutex", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swhitty/swift-mutex", + "state" : { + "revision" : "1770152df756b54c28ef1787df1e957d93cc62d5", + "version" : "0.0.6" } }, { @@ -247,8 +320,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "ba72f31e11275fc5bf060c966cf6c1f36842a291", - "version" : "2.79.0" + "revision" : "558f24a4647193b5a0e2104031b71c55d31ff83a", + "version" : "2.97.1" } }, { @@ -256,8 +329,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "6c84d247754ad77487a6f0694273b89b83efd056", - "version" : "1.14.0" + "revision" : "abcf5312eb8ed2fb11916078aef7c46b06f20813", + "version" : "1.33.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "6d8d596f0a9bfebb925733003731fe2d749b7e02", + "version" : "1.42.0" } }, { @@ -265,8 +347,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", - "version" : "2.29.0" + "revision" : "df9c3406028e3297246e6e7081977a167318b692", + "version" : "2.36.1" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" } }, { @@ -295,13 +386,22 @@ "revision" : "b68a5662ef9887c8f375854720b3621f772bf8c5" } }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", + "version" : "2.11.0" + } + }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } }, { @@ -309,44 +409,53 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", - "version" : "1.4.2" + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" } }, { "identity" : "swift-uwp", "kind" : "remoteSourceControl", - "location" : "https://github.com/thebrowsercompany/swift-uwp", + "location" : "https://github.com/moreSwift/swift-uwp", "state" : { - "branch" : "main", - "revision" : "e373f4ffb128cba831cc2f83324320b287b7c1b1" + "revision" : "a0a86467514eaa63272eae848c70d49e6874ede4", + "version" : "0.1.0" + } + }, + { + "identity" : "swift-webview2core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/moreSwift/swift-webview2core", + "state" : { + "revision" : "a8ed52a57f6d81ee06c8db692f8ee0431174379a", + "version" : "0.1.0" } }, { "identity" : "swift-windowsappsdk", "kind" : "remoteSourceControl", - "location" : "https://github.com/wabiverse/swift-windowsappsdk", + "location" : "https://github.com/moreSwift/swift-windowsappsdk", "state" : { - "branch" : "main", - "revision" : "9a92d35fd4734a10c9f396f48acf209a9ae406cc" + "revision" : "3c06cb36da9711e24a76c35861e0b97702448015", + "version" : "0.1.1" } }, { "identity" : "swift-windowsfoundation", "kind" : "remoteSourceControl", - "location" : "https://github.com/thebrowsercompany/swift-windowsfoundation", + "location" : "https://github.com/moreSwift/swift-windowsfoundation", "state" : { - "branch" : "main", - "revision" : "ba7a8b5000ed3f9f077000d1a31f2a0b19907657" + "revision" : "94d56a5aea472672bc5d041c091f6cdf72e3cc44", + "version" : "0.1.0" } }, { "identity" : "swift-winui", "kind" : "remoteSourceControl", - "location" : "https://github.com/wabiverse/swift-winui", + "location" : "https://github.com/moreSwift/swift-winui", "state" : { - "branch" : "main", - "revision" : "c7a5cf4881479132c2d00e63b84a711e87670856" + "revision" : "73d5d19c39c523c92355027dd13aee445fc627a7", + "version" : "0.1.1" } }, { @@ -389,8 +498,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", - "version" : "1.5.2" + "revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad", + "version" : "1.9.0" } }, { @@ -407,8 +516,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/ZippyJSON", "state" : { - "revision" : "c4ab804780b64979f19268619dfa563b6be58f7d", - "version" : "1.2.10" + "revision" : "ddbbc024ba5a826f9676035a0a090a0bc2d40755", + "version" : "1.2.15" } }, { @@ -416,8 +525,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/ZippyJSONCFamily", "state" : { - "revision" : "8abdd7a5e943afe68e7b03fdaa63b21c042a3893", - "version" : "1.2.9" + "revision" : "c1c0f88977359ea85b81e128b2d988e8250dfdae", + "version" : "1.2.14" } }, { diff --git a/Package.swift b/Package.swift index 848a2069..88e9a947 100644 --- a/Package.swift +++ b/Package.swift @@ -89,7 +89,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-cross-ui", - revision: "20a6e6f4423edda14040fbd012f23d99a4b17823" + .upToNextMinor(from: "0.4.0") ), ], targets: targets diff --git a/Sources/Core/Package.resolved b/Sources/Core/Package.resolved index 8fe5a610..924b3b8c 100644 --- a/Sources/Core/Package.resolved +++ b/Sources/Core/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/attaswift/BigInt.git", "state" : { - "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", - "version" : "5.3.0" + "revision" : "e07e00fa1fd435143a2dcf8b7eec9a7710b2fdfe", + "version" : "5.7.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift", "state" : { - "revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", - "version" : "1.7.2" + "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f", + "version" : "1.9.0" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/JJLISO8601DateFormatter", "state" : { - "revision" : "de422afd9a47b72703c30a81423c478337191390", - "version" : "0.1.6" + "revision" : "50d5ea26ffd3f82e3db8516d939d80b745e168cf", + "version" : "0.2.0" } }, { @@ -104,8 +104,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Rainbow", "state" : { - "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", - "version" : "4.0.1" + "revision" : "cdf146ae671b2624917648b61c908d1244b98ca1", + "version" : "4.2.1" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "9f542610331815e29cc3821d3b6f488db8715517", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272", + "version" : "1.1.3" } }, { @@ -113,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-dns-resolver.git", "state" : { - "revision" : "08c07ff31a745ee5e522ac10132fb4949834d925", - "version" : "0.4.0" + "revision" : "36eedf108f9eda4feeaf47f3dfce657a88ef19aa", + "version" : "0.6.0" } }, { @@ -122,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "6c89474e62719ddcc1e9614989fff2f68208fe10", - "version" : "1.1.0" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -131,8 +158,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "fc45e7b2cfece9dd80b5a45e6469ffe67fe67984", - "version" : "0.14.1" + "revision" : "206cbce3882b4de9aee19ce62ac5b7306cadd45b", + "version" : "1.7.3" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" } }, { @@ -144,6 +180,33 @@ "version" : "1.4.1" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "bb4ba815dab96d4edc1e0b86d7b9acf9ff973a84", + "version" : "4.3.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, { "identity" : "swift-image", "kind" : "remoteSourceControl", @@ -167,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250", - "version" : "2.41.1" + "revision" : "558f24a4647193b5a0e2104031b71c55d31ff83a", + "version" : "2.97.1" } }, { @@ -176,8 +239,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "6c84d247754ad77487a6f0694273b89b83efd056", - "version" : "1.14.0" + "revision" : "abcf5312eb8ed2fb11916078aef7c46b06f20813", + "version" : "1.33.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "6d8d596f0a9bfebb925733003731fe2d749b7e02", + "version" : "1.42.0" } }, { @@ -185,8 +257,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "ba7c0d7f82affc518147ea61d240330bf7f7ea9b", - "version" : "2.22.1" + "revision" : "df9c3406028e3297246e6e7081977a167318b692", + "version" : "2.36.1" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" } }, { @@ -203,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-parsing", "state" : { - "revision" : "27c941bbd22a4bbc53005a15a0440443fd892f70", - "version" : "0.12.1" + "revision" : "3432cb81164dd3d69a75d0d63205be5fbae2c34b", + "version" : "0.14.1" } }, { @@ -215,6 +296,33 @@ "revision" : "b68a5662ef9887c8f375854720b3621f772bf8c5" } }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", + "version" : "2.11.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "2b59c0c741e9184ab057fd22950b491076d42e91", + "version" : "603.0.0" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" + } + }, { "identity" : "swiftcpudetect", "kind" : "remoteSourceControl", @@ -247,8 +355,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "4af50b38daf0037cfbab15514a241224c3f62f98", - "version" : "0.8.5" + "revision" : "dfd70507def84cb5fb821278448a262c6ff2bbad", + "version" : "1.9.0" } }, { @@ -265,8 +373,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/ZippyJSON", "state" : { - "revision" : "c4ab804780b64979f19268619dfa563b6be58f7d", - "version" : "1.2.10" + "revision" : "ddbbc024ba5a826f9676035a0a090a0bc2d40755", + "version" : "1.2.15" } }, { @@ -274,8 +382,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/michaeleisel/ZippyJSONCFamily", "state" : { - "revision" : "8abdd7a5e943afe68e7b03fdaa63b21c042a3893", - "version" : "1.2.9" + "revision" : "c1c0f88977359ea85b81e128b2d988e8250dfdae", + "version" : "1.2.14" } } ],