KDChat/KDChat/Networking/LiteNetLib.swift
Blake Oliver f9c751a993
Chat sounds, message card UI, own-message detection, and crash fix
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)
2026-04-05 08:15:31 -06:00

788 lines
26 KiB
Swift

import Foundation
import Network
import os
private let lnlLog = Logger(subsystem: "dev.smoll.KDChat", category: "LiteNetLib")
// MARK: - Constants
private enum LNL {
static let protocolId: Int32 = 13
static let maxSequence: UInt16 = 32768
static let halfMaxSequence: Int = 16384
static let windowSize: Int = 64
static let channeledHeaderSize: Int = 4
static let reliableOrderedChannelId: UInt8 = 2 // channel 0, ReliableOrdered (0*4+2)
static let pingIntervalMs: Int = 1000
static let disconnectTimeoutMs: Int = 5000
static let resendBaseMs: Double = 25
static let resendRttMultiplier: Double = 2.1
static let updateIntervalMs: Int = 50
// .NET epoch offset: ticks between 0001-01-01 and 1970-01-01
static let dotNetEpochOffset: Double = 62_135_596_800
static var dotNetTicks: Int64 {
Int64((Date.now.timeIntervalSince1970 + dotNetEpochOffset) * 10_000_000)
}
}
// MARK: - Packet Property
private enum PacketProperty: UInt8 {
case unreliable = 0
case channeled = 1
case ack = 2
case ping = 3
case pong = 4
case connectRequest = 5
case connectAccept = 6
case disconnect = 7
case unconnectedMessage = 8
case mtuCheck = 9
case mtuOk = 10
case broadcast = 11
case merged = 12
case shutdownOk = 13
case peerNotFound = 14
case invalidProtocol = 15
case natMessage = 16
case empty = 17
}
// MARK: - Connection State
private enum ConnectionState {
case disconnected
case connecting
case connected
case shutdownRequested
}
// MARK: - Pending Packet
private struct PendingPacket {
var data: Data
var timestamp: UInt64
var isSent: Bool
}
// MARK: - Fragment Reassembly
private struct IncomingFragment {
let totalParts: Int
let channelId: UInt8
var parts: [Data?]
var receivedCount: Int
init(totalParts: Int, channelId: UInt8) {
self.totalParts = totalParts
self.channelId = channelId
self.parts = Array(repeating: nil, count: totalParts)
self.receivedCount = 0
}
}
// MARK: - Client Event
extension LNLClient {
enum Event: Sendable {
case connected
case disconnected(String?)
case received(Data)
}
}
// MARK: - LNLClient
/// Minimal LiteNetLib client supporting ReliableOrdered delivery over UDP.
actor LNLClient {
// NWConnection requires a DispatchQueue for its callback-based API
private let nwQueue = DispatchQueue(label: "com.kdchat.litenetlib.nw")
private var connection: NWConnection?
private var updateTask: Task<Void, Never>?
// Event stream for consumers
let events: AsyncStream<Event>
private let eventContinuation: AsyncStream<Event>.Continuation
// Connection state
private var state: ConnectionState = .disconnected
private var connectionTime: Int64 = 0
private var connectionNumber: UInt8 = 0
private var remotePeerId: Int32 = 0
private var localPeerId: Int32 = Int32.random(in: 1...Int32.max)
private var connectKey: String = ""
private var connectAttempts: Int = 0
private let maxConnectAttempts: Int = 10
private var lastConnectSendTime: UInt64 = 0
// Reliable ordered channel - send side
private var localSequence: UInt16 = 0
private var localWindowStart: UInt16 = 0
private var pendingPackets: [PendingPacket?]
private var outgoingQueue: [Data] = []
// Reliable ordered channel - receive side
private var remoteSequence: UInt16 = 0
private var remoteWindowStart: UInt16 = 0
private var receivedPackets: [Data?]
private var ackBitfield: [UInt8]
private var mustSendAcks: Bool = false
// Fragment reassembly
private var incomingFragments: [UInt16: IncomingFragment] = [:]
// Ping / keepalive
private var pingSequence: UInt16 = 0
private var lastPingSentTime: UInt64 = 0
private var lastPacketReceivedTime: UInt64 = 0
private var avgRtt: Double = 50
private var rttSum: Double = 0
private var rttCount: Int = 0
private var pingSentTimestamp: UInt64 = 0
// Resend delay (computed from RTT)
private var resendDelay: Double { LNL.resendBaseMs + avgRtt * LNL.resendRttMultiplier }
// Send stats
private var totalBytesSent: Int = 0
private var totalPacketsSent: Int = 0
init() {
let ws = LNL.windowSize
self.pendingPackets = Array(repeating: nil, count: ws)
self.receivedPackets = Array(repeating: nil, count: ws)
let ackSize = (ws - 1) / 8 + 2
self.ackBitfield = Array(repeating: 0, count: ackSize)
let (stream, continuation) = AsyncStream.makeStream(of: Event.self)
self.events = stream
self.eventContinuation = continuation
}
deinit {
eventContinuation.finish()
}
// MARK: - Public API
func connect(host: String, port: UInt16, key: String) {
guard state == .disconnected else {
lnlLog.warning("connect() called but state is not disconnected")
return
}
lnlLog.info("Connecting to \(host):\(port) with key '\(key)'")
connectKey = key
connectionTime = LNL.dotNetTicks
connectAttempts = 0
state = .connecting
let nwHost = NWEndpoint.Host(host)
guard let nwPort = NWEndpoint.Port(rawValue: port) else {
lnlLog.error("Invalid port: \(port)")
return
}
let conn = NWConnection(host: nwHost, port: nwPort, using: .udp)
connection = conn
conn.stateUpdateHandler = { [weak self] newState in
Task { [weak self] in await self?.handleNWState(newState) }
}
conn.pathUpdateHandler = { path in
lnlLog.info("Path status: \(String(describing: path.status)), isExpensive: \(path.isExpensive), isConstrained: \(path.isConstrained)")
}
conn.start(queue: nwQueue)
}
func send(data: Data) {
guard state == .connected else { return }
outgoingQueue.append(data)
}
func disconnect() {
performDisconnect(reason: "Client disconnected")
}
// MARK: - NWConnection State
private func handleNWState(_ newState: NWConnection.State) {
lnlLog.info("NWConnection state: \(String(describing: newState))")
switch newState {
case .ready:
if let path = connection?.currentPath {
lnlLog.info("UDP socket ready. Local: \(String(describing: path.localEndpoint)), remote: \(String(describing: path.remoteEndpoint))")
for iface in path.availableInterfaces {
lnlLog.info("Interface: \(iface.name) type: \(String(describing: iface.type))")
}
}
startReceiving()
startUpdateLoop()
sendConnectRequest()
case .failed(let error):
lnlLog.error("NWConnection failed: \(error.localizedDescription)")
performDisconnect(reason: "Connection failed: \(error.localizedDescription)")
case .cancelled:
lnlLog.info("NWConnection cancelled")
performDisconnect(reason: nil)
default:
break
}
}
// MARK: - Receive Loop
private func startReceiving() {
lnlLog.debug("Waiting for incoming UDP data...")
connection?.receiveMessage { [weak self] data, _, _, error in
Task { [weak self] in await self?.handleReceiveCallback(data: data, error: error) }
}
}
private func handleReceiveCallback(data: Data?, error: NWError?) {
if let error {
lnlLog.error("Receive error: \(error.localizedDescription)")
}
if let data, !data.isEmpty {
lnlLog.info("Received \(data.count) bytes: \(data.prefix(20).map { String(format: "%02x", $0) }.joined(separator: " "))\(data.count > 20 ? "..." : "")")
lastPacketReceivedTime = currentMs()
handleRawPacket(data)
} else if data == nil && error == nil {
lnlLog.warning("receiveMessage returned nil data with no error")
}
if error == nil && state != .disconnected {
startReceiving()
} else if state == .disconnected {
lnlLog.info("Not re-registering receive (disconnected)")
}
}
// MARK: - Update Loop
private func startUpdateLoop() {
updateTask = Task { [weak self] in
while !Task.isCancelled {
await self?.update()
try? await Task.sleep(for: .milliseconds(LNL.updateIntervalMs))
}
}
}
private func update() {
let now = currentMs()
switch state {
case .connecting:
if now >= lastConnectSendTime, now - lastConnectSendTime > 500 {
connectAttempts += 1
lnlLog.info("ConnectRequest attempt \(self.connectAttempts)/\(self.maxConnectAttempts)")
if connectAttempts > maxConnectAttempts {
lnlLog.error("Connection timed out after \(self.maxConnectAttempts) attempts")
performDisconnect(reason: "Connection timed out")
return
}
sendConnectRequest()
}
case .connected:
if now >= lastPacketReceivedTime, now - lastPacketReceivedTime > UInt64(LNL.disconnectTimeoutMs) {
performDisconnect(reason: "Connection timed out")
return
}
flushOutgoingQueue()
retransmitPending(now: now)
if mustSendAcks {
sendAck()
mustSendAcks = false
}
if now >= lastPingSentTime, now - lastPingSentTime > UInt64(LNL.pingIntervalMs) {
sendPing()
}
case .shutdownRequested, .disconnected:
break
}
}
// MARK: - Packet Handling
private func handleRawPacket(_ data: Data) {
guard !data.isEmpty else { return }
let byte0 = data[data.startIndex]
let property = byte0 & 0x1f
let connNum = (byte0 >> 5) & 0x03
guard let prop = PacketProperty(rawValue: property) else {
lnlLog.debug("Unknown packet property: \(property)")
return
}
lnlLog.debug("Received packet: \(String(describing: prop)), \(data.count) bytes")
if state == .connected && prop != .connectAccept && prop != .connectRequest {
if connNum != connectionNumber { return }
}
switch prop {
case .connectAccept: handleConnectAccept(data)
case .channeled: handleChanneled(data)
case .ack: handleAck(data)
case .ping: handlePing(data)
case .pong: handlePong(data)
case .disconnect: handleServerDisconnect(data)
case .merged: handleMerged(data)
case .shutdownOk: performDisconnect(reason: nil)
default: break
}
}
// MARK: - Connect Handshake
private func sendConnectRequest() {
lnlLog.debug("Sending ConnectRequest")
var w = DataWriter()
w.writeByte(PacketProperty.connectRequest.rawValue)
w.writeInt32LE(LNL.protocolId)
w.writeInt64LE(connectionTime)
w.writeInt32LE(localPeerId)
// Dummy IPv4 sockaddr (16 bytes, .NET SocketAddress format)
w.writeByte(16)
var addr = Data(count: 16)
addr[0] = 2 // AF_INET
w.writeData(addr)
// Connection key: ushort16 LE (byteCount + 1) + UTF-8
let keyBytes = Array(connectKey.utf8)
w.writeUInt16LE(UInt16(keyBytes.count + 1))
w.writeData(Data(keyBytes))
lastConnectSendTime = currentMs()
let packet = w.data
lnlLog.info("ConnectRequest packet (\(packet.count) bytes): \(packet.map { String(format: "%02x", $0) }.joined(separator: " "))")
sendRaw(packet)
}
private func handleConnectAccept(_ data: Data) {
guard state == .connecting else {
lnlLog.warning("ConnectAccept received but not in connecting state")
return
}
guard data.count >= 15 else {
lnlLog.error("ConnectAccept too short: \(data.count) bytes")
return
}
var r = DataReader(data: data, offset: 1)
guard let echoedTime = r.readInt64LE(),
echoedTime == connectionTime else {
lnlLog.error("ConnectAccept connectionTime mismatch")
return
}
connectionNumber = r.readByte() ?? 0
_ = r.readByte() // isReused
remotePeerId = r.readInt32LE() ?? 0
lnlLog.info("Connected! connNum=\(self.connectionNumber), remotePeerId=\(self.remotePeerId)")
state = .connected
lastPacketReceivedTime = currentMs()
lastPingSentTime = currentMs()
eventContinuation.yield(.connected)
}
// MARK: - Channeled (ReliableOrdered)
private func handleChanneled(_ data: Data) {
guard data.count >= LNL.channeledHeaderSize else { return }
var r = DataReader(data: data, offset: 1)
guard let seq = r.readUInt16LE() else { return }
guard let channelId = r.readByte() else { return }
guard channelId == LNL.reliableOrderedChannelId else { return }
// Pass the full packet (including fragment header if present) through
// the reliable ordered channel for sequencing and ACKs.
// Delivery/reassembly happens in deliverOrderedPacket.
let fullPayload = Data(data[data.startIndex ..< data.endIndex])
processReliableOrdered(sequence: seq, data: fullPayload)
}
/// Called when a reliable ordered packet is delivered in-sequence.
/// Handles fragment reassembly before yielding to the consumer.
private func deliverOrderedPacket(_ data: Data) {
guard data.count >= LNL.channeledHeaderSize else { return }
let isFragmented = (data[data.startIndex] & 0x80) != 0
if isFragmented {
guard data.count >= 10 else { return }
var r = DataReader(data: data, offset: 4) // skip channeled header
guard let fragmentId = r.readUInt16LE() else { return }
guard let fragmentPart = r.readUInt16LE() else { return }
guard let fragmentsTotal = r.readUInt16LE() else { return }
guard fragmentsTotal > 0, fragmentPart < fragmentsTotal else { return }
let channelId = data[data.startIndex + 3]
let fragmentData = Data(data[data.startIndex + 10 ..< data.endIndex])
if incomingFragments[fragmentId] == nil {
incomingFragments[fragmentId] = IncomingFragment(
totalParts: Int(fragmentsTotal), channelId: channelId
)
}
guard var fragment = incomingFragments[fragmentId],
fragment.channelId == channelId,
Int(fragmentPart) < fragment.totalParts,
fragment.parts[Int(fragmentPart)] == nil else { return }
fragment.parts[Int(fragmentPart)] = fragmentData
fragment.receivedCount += 1
incomingFragments[fragmentId] = fragment
if fragment.receivedCount == fragment.totalParts {
var assembled = Data()
for part in fragment.parts {
if let part { assembled.append(part) }
}
incomingFragments.removeValue(forKey: fragmentId)
lnlLog.info("Reassembled fragmented packet: \(fragment.totalParts) parts, \(assembled.count) bytes")
eventContinuation.yield(.received(assembled))
}
} else {
let userData = Data(data[data.startIndex + LNL.channeledHeaderSize ..< data.endIndex])
eventContinuation.yield(.received(userData))
}
}
private func processReliableOrdered(sequence seq: UInt16, data: Data) {
// Reject already-delivered sequences
let relateToDelivered = relativeSequence(seq, to: remoteSequence)
if relateToDelivered < 0 { return }
let relate = relativeSequence(seq, to: remoteWindowStart)
if relate >= LNL.windowSize {
let diff = relate - LNL.windowSize + 1
for _ in 0..<diff {
let idx = Int(remoteWindowStart) % LNL.windowSize
receivedPackets[idx] = nil
let bitIndex = Int(remoteWindowStart) % LNL.windowSize
ackBitfield[bitIndex / 8] &= ~(1 << (bitIndex % 8))
remoteWindowStart = (remoteWindowStart &+ 1) % LNL.maxSequence
}
}
let ackIdx = Int(seq) % LNL.windowSize
ackBitfield[ackIdx / 8] |= (1 << (ackIdx % 8))
mustSendAcks = true
if seq == remoteSequence {
deliverOrderedPacket(data)
remoteSequence = (remoteSequence &+ 1) % LNL.maxSequence
lnlLog.debug("Delivered seq \(seq), next expected: \(self.remoteSequence)")
while true {
let idx = Int(remoteSequence) % LNL.windowSize
guard let buffered = receivedPackets[idx] else { break }
receivedPackets[idx] = nil
deliverOrderedPacket(buffered)
remoteSequence = (remoteSequence &+ 1) % LNL.maxSequence
}
} else {
let idx = Int(seq) % LNL.windowSize
receivedPackets[idx] = data
lnlLog.warning("Buffered out-of-order seq \(seq), expecting \(self.remoteSequence)")
}
}
// MARK: - Send Reliable
private func flushOutgoingQueue() {
while !outgoingQueue.isEmpty {
let windowUsed = relativeSequence(localSequence, to: localWindowStart)
if windowUsed >= LNL.windowSize { break }
let userData = outgoingQueue.removeFirst()
let packet = buildChanneledPacket(sequence: localSequence, data: userData)
let idx = Int(localSequence) % LNL.windowSize
pendingPackets[idx] = PendingPacket(data: packet, timestamp: currentMs(), isSent: true)
localSequence = (localSequence &+ 1) % LNL.maxSequence
sendRaw(packet)
}
}
private func retransmitPending(now: UInt64) {
var seq = localWindowStart
while seq != localSequence {
let idx = Int(seq) % LNL.windowSize
if let pending = pendingPackets[idx] {
if now >= pending.timestamp, Double(now - pending.timestamp) >= resendDelay {
pendingPackets[idx]?.timestamp = now
sendRaw(pending.data)
}
}
seq = (seq &+ 1) % LNL.maxSequence
}
}
private func buildChanneledPacket(sequence: UInt16, data: Data) -> Data {
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.channeled.rawValue)
w.writeUInt16LE(sequence)
w.writeByte(LNL.reliableOrderedChannelId)
w.writeData(data)
return w.data
}
// MARK: - ACK
private func sendAck() {
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.ack.rawValue)
w.writeUInt16LE(remoteWindowStart)
w.writeByte(LNL.reliableOrderedChannelId)
w.writeData(Data(ackBitfield))
sendRaw(w.data)
}
private func handleAck(_ data: Data) {
guard data.count >= LNL.channeledHeaderSize else { return }
var r = DataReader(data: data, offset: 1)
guard let ackWindowStart = r.readUInt16LE() else { return }
guard let channelId = r.readByte(), channelId == LNL.reliableOrderedChannelId else { return }
let bitfieldStart = data.startIndex + LNL.channeledHeaderSize
let bitfieldData = data[bitfieldStart...]
var seq = localWindowStart
while seq != localSequence {
let relToAckStart = relativeSequence(seq, to: ackWindowStart)
if relToAckStart < 0 { break }
if relToAckStart >= LNL.windowSize { break }
let byteIdx = relToAckStart / 8
let bitIdx = relToAckStart % 8
if byteIdx < bitfieldData.count {
let acked = (bitfieldData[bitfieldStart + byteIdx] & (1 << bitIdx)) != 0
if acked {
let idx = Int(seq) % LNL.windowSize
pendingPackets[idx] = nil
if seq == localWindowStart {
localWindowStart = (localWindowStart &+ 1) % LNL.maxSequence
}
}
}
seq = (seq &+ 1) % LNL.maxSequence
}
while localWindowStart != localSequence {
let idx = Int(localWindowStart) % LNL.windowSize
if pendingPackets[idx] != nil { break }
localWindowStart = (localWindowStart &+ 1) % LNL.maxSequence
}
}
// MARK: - Ping / Pong
private func sendPing() {
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.ping.rawValue)
w.writeUInt16LE(pingSequence)
pingSequence = (pingSequence &+ 1) % LNL.maxSequence
pingSentTimestamp = currentMs()
lastPingSentTime = pingSentTimestamp
sendRaw(w.data)
}
private func handlePing(_ data: Data) {
guard data.count >= 3 else { return }
var r = DataReader(data: data, offset: 1)
guard let seq = r.readUInt16LE() else { return }
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.pong.rawValue)
w.writeUInt16LE(seq)
w.writeInt64LE(LNL.dotNetTicks)
sendRaw(w.data)
}
private func handlePong(_ data: Data) {
guard data.count >= 3 else { return }
let now = currentMs()
guard now >= pingSentTimestamp else { return }
let elapsed = Double(now - pingSentTimestamp)
rttSum += elapsed
rttCount += 1
avgRtt = rttSum / Double(rttCount)
if rttCount > 10 {
rttSum = avgRtt
rttCount = 1
}
}
// MARK: - Disconnect
private func handleServerDisconnect(_ data: Data) {
guard data.count >= 9 else { return }
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.shutdownOk.rawValue)
sendRaw(w.data)
performDisconnect(reason: "Server disconnected")
}
private func handleMerged(_ data: Data) {
var offset = 1
while offset + 2 <= data.count {
let sizeBytes = data[data.startIndex + offset ..< data.startIndex + offset + 2]
let size = Int(UInt16(littleEndian: sizeBytes.withUnsafeBytes { $0.loadUnaligned(as: UInt16.self) }))
offset += 2
guard offset + size <= data.count else { break }
let subPacket = Data(data[data.startIndex + offset ..< data.startIndex + offset + size])
offset += size
handleRawPacket(subPacket)
}
}
private func performDisconnect(reason: String?) {
guard state != .disconnected else { return }
lnlLog.info("Disconnecting: \(reason ?? "no reason")")
if state == .connected {
var w = DataWriter()
w.writeByte((connectionNumber << 5) | PacketProperty.disconnect.rawValue)
w.writeInt64LE(connectionTime)
sendRaw(w.data)
}
state = .disconnected
updateTask?.cancel()
updateTask = nil
connection?.cancel()
connection = nil
// Reset channel state
localSequence = 0
localWindowStart = 0
remoteSequence = 0
remoteWindowStart = 0
outgoingQueue.removeAll()
for i in 0..<LNL.windowSize {
pendingPackets[i] = nil
receivedPackets[i] = nil
}
for i in 0..<ackBitfield.count {
ackBitfield[i] = 0
}
incomingFragments.removeAll()
eventContinuation.yield(.disconnected(reason))
}
// MARK: - Raw Send
private func sendRaw(_ data: Data) {
guard let conn = connection else {
lnlLog.error("sendRaw called but connection is nil")
return
}
if conn.state != .ready {
lnlLog.warning("sendRaw called but NWConnection state is \(String(describing: conn.state))")
}
totalPacketsSent += 1
totalBytesSent += data.count
let pktNum = totalPacketsSent
conn.send(content: data, completion: .contentProcessed { error in
if let error {
lnlLog.error("Send #\(pktNum) failed: \(error.localizedDescription)")
} else {
lnlLog.debug("Send #\(pktNum) completed (\(data.count) bytes)")
}
})
}
// MARK: - Utility
private func currentMs() -> UInt64 {
UInt64(DispatchTime.now().uptimeNanoseconds / 1_000_000)
}
private func relativeSequence(_ number: UInt16, to expected: UInt16) -> Int {
let n = Int(number)
let e = Int(expected)
let max = Int(LNL.maxSequence)
return (n - e + max + LNL.halfMaxSequence) % max - LNL.halfMaxSequence
}
}
// MARK: - Binary Helpers
private struct DataWriter {
var data = Data()
mutating func writeByte(_ byte: UInt8) {
data.append(byte)
}
mutating func writeUInt16LE(_ value: UInt16) {
withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
}
mutating func writeInt32LE(_ value: Int32) {
withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
}
mutating func writeInt64LE(_ value: Int64) {
withUnsafeBytes(of: value.littleEndian) { data.append(contentsOf: $0) }
}
mutating func writeData(_ d: Data) {
data.append(d)
}
}
private struct DataReader {
let data: Data
var offset: Int
mutating func readByte() -> UInt8? {
let idx = data.startIndex + offset
guard idx < data.endIndex else { return nil }
offset += 1
return data[idx]
}
mutating func readUInt16LE() -> UInt16? {
let start = data.startIndex + offset
guard start + 2 <= data.endIndex else { return nil }
offset += 2
return data[start..<start+2].withUnsafeBytes {
UInt16(littleEndian: $0.loadUnaligned(as: UInt16.self))
}
}
mutating func readInt32LE() -> Int32? {
let start = data.startIndex + offset
guard start + 4 <= data.endIndex else { return nil }
offset += 4
return data[start..<start+4].withUnsafeBytes {
Int32(littleEndian: $0.loadUnaligned(as: Int32.self))
}
}
mutating func readInt64LE() -> Int64? {
let start = data.startIndex + offset
guard start + 8 <= data.endIndex else { return nil }
offset += 8
return data[start..<start+8].withUnsafeBytes {
Int64(littleEndian: $0.loadUnaligned(as: Int64.self))
}
}
}