- Add `AppDistribution` enum to `AppInfo.swift` that detects simulator,
development, TestFlight, Ad Hoc, and App Store builds at runtime
- Display distribution type in the settings footer version label
- Bump `remoteFormEvent` server method ID from 20 to 21 (comment range
updated to 9..20 accordingly)
This fixes the client to work with the game server again and allows it to determine the install
method.
- New "Announce Background Messages" toggle in Accessibility section (off by default) — VoiceOver announces messages from non-active buffers when enabled
- Rename "Other Buffers" to "Background Buffer Sounds" in Sounds section
- Add footer descriptions to Save Debug Buffer, Background Buffer Sounds, Status Haptics, and Announce Background Messages
Persistence:
- SavedMessage SwiftData model with index on [buffer, timestamp]
- @Query in ChatBufferView/DMListView/DMConversationView — SwiftData is the single source of truth, no in-memory dictionary
- Settings: Save Messages toggle, Save Debug Buffer toggle, Delete After picker (1d/1w/1m/1y/never), Delete All with confirmation dialog
- Expired and debug messages purged on launch based on settings
- Say messages go to debug buffer only (not all buffers)
Reactive side effects:
- Sound, haptic, and VoiceOver announcement all fire from ChatBufferView's onChange(of: messages.count)
- Sound plays for active buffer or background buffers if enabled
- Haptic and announcement fire on active buffer only, current session only (I'll add a toggle to announce other buffers next)
- Status haptic for Say messages stays imperative in model (global event, not per-buffer)
- Removed trigger counters and lastAnnouncement from ConnectionManager
DM conversation fix:
- Track DM recipient via lastDMRecipientId; tag outgoing echoes with partner ID in characterId
- Conversation view filters on characterId == partnerId (shows only that conversation)
Other:
- modelContext set in ContentView.onAppear (available for all screens)
- Instant scroll (no animation), respects Reduce Motion
- 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
Sounds:
- Add chat sounds (global, map, DM) converted from game client ogg to caf
- Audio category .ambient (mixes with other audio, respects silent switch)
- Per-buffer sound toggles and background buffer sound option in Settings
Messages:
- Card-style message rows with .regularMaterial background, bumped fonts (content .title3, sender .headline)
- Context menu and VoiceOver actions: DM and Show Profile on other players' messages
- Generate local UUIDs for message identity (server ID field is not unique, caused ForEach to collapse messages)
Own-message detection:
- Capture player entity ID from LoadInstance packet (Key 1) on game join
- Hide DM/Profile actions on own messages via senderId comparison
Crash fix:
- Guard all UInt64 timestamp subtractions against underflow (retransmitPending, ping, disconnect timeout)
This is an iOS chat client for Kirandur, a multiplayer RPG. Reimplements the LiteNetLib UDP protocol in Swift for reliable ordered messaging. Renders server-driven UI via a RemoteForms widget system for login, character creation and more inside the client.
Uses msgpack-swift for serialization, Keychain for credentials, and targets iOS 18.6+/Xcode 26+.
No message persistence yet. No auto-reconnect. Game-specific packets (movement, combat, etc.) are silently ignored.
No profile viewing yet.
Assisted by Claude Code (Opus 4.6)