diff --git a/KDChat/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/KDChat/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000..e8faca4 Binary files /dev/null and b/KDChat/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ diff --git a/KDChat/Assets.xcassets/AppIcon.appiconset/Contents.json b/KDChat/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..87d4015 100644 --- a/KDChat/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/KDChat/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -23,6 +25,7 @@ "value" : "tinted" } ], + "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/KDChat/Assets.xcassets/Logo.imageset/Contents.json b/KDChat/Assets.xcassets/Logo.imageset/Contents.json new file mode 100644 index 0000000..1f8162a --- /dev/null +++ b/KDChat/Assets.xcassets/Logo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Logo.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/KDChat/Assets.xcassets/Logo.imageset/Logo.png b/KDChat/Assets.xcassets/Logo.imageset/Logo.png new file mode 100644 index 0000000..e8faca4 Binary files /dev/null and b/KDChat/Assets.xcassets/Logo.imageset/Logo.png differ diff --git a/KDChat/Networking/LiteNetLib.swift b/KDChat/Networking/LiteNetLib.swift index b3457ca..584f58f 100644 --- a/KDChat/Networking/LiteNetLib.swift +++ b/KDChat/Networking/LiteNetLib.swift @@ -156,7 +156,7 @@ actor LNLClient { let ackSize = (ws - 1) / 8 + 2 self.ackBitfield = Array(repeating: 0, count: ackSize) - let (stream, continuation) = AsyncStream.makeStream(of: Event.self) + let (stream, continuation) = AsyncStream.makeStream(of: Event.self, bufferingPolicy: .unbounded) self.events = stream self.eventContinuation = continuation } @@ -450,7 +450,8 @@ actor LNLClient { } incomingFragments.removeValue(forKey: fragmentId) lnlLog.info("Reassembled fragmented packet: \(fragment.totalParts) parts, \(assembled.count) bytes") - eventContinuation.yield(.received(assembled)) + let yieldResult = eventContinuation.yield(.received(assembled)) + lnlLog.info("Yield result for reassembled packet: \(String(describing: yieldResult))") } } else { let userData = Data(data[data.startIndex + LNL.channeledHeaderSize ..< data.endIndex]) diff --git a/KDChat/Networking/MessagePack.swift b/KDChat/Networking/MessagePack.swift index 07eebc6..5ea02aa 100644 --- a/KDChat/Networking/MessagePack.swift +++ b/KDChat/Networking/MessagePack.swift @@ -130,6 +130,8 @@ extension MsgPackValue: Decodable { if let v = try? c.decode(Double.self) { self = .double(v); return } if let v = try? c.decode(Float.self) { self = .float(v); return } if let v = try? c.decode(Data.self) { self = .binary(v); return } + // MessagePack ext types (e.g. timestamps) — decode as Date, store as string + if let v = try? c.decode(Date.self) { self = .string(v.description); return } throw DecodingError.dataCorrupted( DecodingError.Context(codingPath: decoder.codingPath, diff --git a/KDChat/Services/ConnectionManager.swift b/KDChat/Services/ConnectionManager.swift index 077c885..94a6a97 100644 --- a/KDChat/Services/ConnectionManager.swift +++ b/KDChat/Services/ConnectionManager.swift @@ -1,4 +1,5 @@ import Foundation +import MessagePack import SwiftUI import os @@ -120,6 +121,7 @@ final class ConnectionManager { self.handleConnected() } case .received(let data): + log.debug("Event: received \(data.count) bytes, method=\(data.count >= 4 ? String(data.withUnsafeBytes { Int32(littleEndian: $0.loadUnaligned(as: Int32.self)) }) : "?")") self.handlePacket(data) case .disconnected(let reason): self.handleDisconnect(reason: reason) @@ -250,10 +252,15 @@ final class ConnectionManager { guard let message = PacketDecoder.decode(ChatMessage.self, from: payload) else { return } handleAddMessage(message) case .loadInstance: - if let value = PacketDecoder.decodeDynamic(from: payload), - let entityId = value[1]?.stringValue { - localSenderId = entityId - log.info("Local entity ID: \(entityId)") + do { + let value = try MessagePackDecoder().decode(MsgPackValue.self, from: payload) + if let entityId = value[1]?.stringValue { + localSenderId = entityId + currentCharacterId = entityId + log.info("Local entity ID: \(entityId)") + } + } catch { + log.error("LoadInstance decode failed: \(error)") } case .requestGameplay: handleRequestGameplay() @@ -343,6 +350,12 @@ final class ConnectionManager { private func handleAddMessage(_ message: ChatMessage) { messages[message.buffer, default: []].append(message) + // Learn our own entity ID from echoed messages + if message.senderIsRecipient, let id = message.senderId { + if localSenderId == nil { localSenderId = id } + if currentCharacterId == nil { currentCharacterId = id } + } + messageFeedbackTrigger += 1 lastAnnouncement = "\(message.senderName): \(message.content)" playMessageSound(buffer: message.buffer) @@ -382,6 +395,7 @@ final class ConnectionManager { } } + // MARK: - Game State Handlers private func handleRequestGameplay() { @@ -392,9 +406,13 @@ final class ConnectionManager { private func handleLeaveGameUpdate(_ response: GenericResponse) { if response.success { - // Clear chat state, wait for new RemoteForms + // Clear chat state, re-login to get account screen resetState() + isLoggedIn = false screen = .forms + if let token = cachedToken { + loginWithToken(token) + } } } diff --git a/KDChat/Views/ChatView.swift b/KDChat/Views/ChatView.swift index 8ca1c74..db2dd45 100644 --- a/KDChat/Views/ChatView.swift +++ b/KDChat/Views/ChatView.swift @@ -26,7 +26,7 @@ struct ChatView: View { } } } - .navigationTitle("Kirandur Chat") + .navigationTitle(titleForBuffer(selectedBuffer)) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -66,6 +66,16 @@ struct ChatView: View { return buffer.canSend } + private func titleForBuffer(_ name: String) -> String { + switch name { + case "global": "Kirandur Global Chat" + case "map": "Kirandur Map Chat" + case "debug": "Kirandur Debug Output" + case "direct-messages": "Kirandur DMs" + default: "Kirandur \(name.capitalized)" + } + } + private func iconForBuffer(_ name: String) -> String { switch name { case "global": "globe" diff --git a/KDChat/Views/ListBoxContent.swift b/KDChat/Views/ListBoxContent.swift index 2925cce..88bc897 100644 --- a/KDChat/Views/ListBoxContent.swift +++ b/KDChat/Views/ListBoxContent.swift @@ -6,27 +6,30 @@ struct ListBoxContent: View { @Environment(ConnectionManager.self) private var manager var body: some View { - Picker(widget.label, selection: Bindable(widget).selectedIndex) { - ForEach(widget.items.indices, id: \.self) { index in - Text(widget.items[index]).tag(index) + if !widget.label.isEmpty { + Text(widget.label) + .font(.headline) + } + ForEach(widget.items.indices, id: \.self) { index in + Button { + widget.selectedIndex = index + manager.sendFormEvent( + formId: formId, widgetId: widget.id, + eventType: .listBoxItemClicked, + value: .uint(UInt64(index)) + ) + } label: { + HStack { + Text(widget.items[index]) + Spacer() + if index == widget.selectedIndex { + Image(systemName: "checkmark") + .foregroundStyle(.tint) + } + } } + .accessibilityAddTraits(index == widget.selectedIndex ? .isSelected : []) + .disabled(!widget.enabled) } - .disabled(!widget.enabled) - .onChange(of: widget.selectedIndex) { - sendSelection() - } - .onAppear { - if widget.items.count == 1 { - sendSelection() - } - } - } - - private func sendSelection() { - manager.sendFormEvent( - formId: formId, widgetId: widget.id, - eventType: .listBoxItemClicked, - value: .uint(UInt64(widget.selectedIndex)) - ) } } diff --git a/KDChat/Views/LoginView.swift b/KDChat/Views/LoginView.swift index 04ecf41..9129b30 100644 --- a/KDChat/Views/LoginView.swift +++ b/KDChat/Views/LoginView.swift @@ -15,9 +15,13 @@ struct LoginView: View { NavigationStack { Form { Section { - Text("Enter your email and password to log in.") - .font(.subheadline) - .foregroundStyle(.secondary) + Image(.logo) + .resizable() + .scaledToFit() + .frame(maxHeight: 120) + .clipShape(.rect(cornerRadius: 24)) + .frame(maxWidth: .infinity) + .accessibilityHidden(true) } Section("Credentials") {