Character switching, entity ID capture, and UI fixes
- Switch Character now works: send LeaveGame, then re-send LoginRequest with cached token to trigger account screen - Capture player entity ID from LoadInstance packet to identify own messages and enable LeaveGame - Fix MsgPackValue Decodable to handle MessagePack timestamp extension types (Date) - ListBox uses button-based rows instead of Picker (server expects click events, not selection changes) - Dynamic nav title per buffer tab (Kirandur Global Chat, Map Chat, etc.) - App icon on login screen via separate Logo image asset - AsyncStream buffering set to unbounded
This commit is contained in:
parent
f9c751a993
commit
26182ec8d0
10 changed files with 84 additions and 31 deletions
BIN
KDChat/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
BIN
KDChat/Assets.xcassets/AppIcon.appiconset/AppIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
|
|
@ -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"
|
||||
|
|
|
|||
12
KDChat/Assets.xcassets/Logo.imageset/Contents.json
vendored
Normal file
12
KDChat/Assets.xcassets/Logo.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Logo.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
KDChat/Assets.xcassets/Logo.imageset/Logo.png
vendored
Normal file
BIN
KDChat/Assets.xcassets/Logo.imageset/Logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,11 +252,16 @@ 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 {
|
||||
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()
|
||||
case .leaveGameUpdate:
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -6,27 +6,30 @@ struct ListBoxContent: View {
|
|||
@Environment(ConnectionManager.self) private var manager
|
||||
|
||||
var body: some View {
|
||||
Picker(widget.label, selection: Bindable(widget).selectedIndex) {
|
||||
if !widget.label.isEmpty {
|
||||
Text(widget.label)
|
||||
.font(.headline)
|
||||
}
|
||||
ForEach(widget.items.indices, id: \.self) { index in
|
||||
Text(widget.items[index]).tag(index)
|
||||
}
|
||||
}
|
||||
.disabled(!widget.enabled)
|
||||
.onChange(of: widget.selectedIndex) {
|
||||
sendSelection()
|
||||
}
|
||||
.onAppear {
|
||||
if widget.items.count == 1 {
|
||||
sendSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendSelection() {
|
||||
Button {
|
||||
widget.selectedIndex = index
|
||||
manager.sendFormEvent(
|
||||
formId: formId, widgetId: widget.id,
|
||||
eventType: .listBoxItemClicked,
|
||||
value: .uint(UInt64(widget.selectedIndex))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue