using System.Buffers.Binary; namespace TravelEar.Core; /// /// Dissonance packet types, copied verbatim from Dissonance's MessageTypes (values confirmed /// against the game's DissonanceVoip.dll). /// public enum DissonanceMessageType : byte { ClientState = 1, VoiceData = 2, TextData = 3, HandshakeRequest = 4, HandshakeResponse = 5, ErrorWrongSession = 6, ServerRelayReliable = 7, ServerRelayUnreliable = 8, DeltaChannelState = 9, RemoveClient = 10, HandshakeP2P = 11, } /// Why rejected a packet. public enum DissonanceFrameError { None = 0, /// Shorter than the fixed header. TooShort, /// First 16 bits are not . BadMagic, /// A valid Dissonance packet, but not . NotVoiceData, /// The flags byte has its MSB clear; Dissonance always sets it. BadFlags, /// The channel list or payload runs past the end of the packet. Truncated, /// Bytes remain after the payload. TrailingBytes, } /// One entry of a VoiceData channel list: 16-bit channel bitfield then 16-bit channel id. public readonly record struct DissonanceChannel(ushort Bitfield, ushort Id); /// /// A parsed Dissonance VoiceData packet: the exact bytes the game sends to peers /// (Outbound Voice). Layout follows the documented network protocol /// (https://placeholder-software.co.uk/dissonance/docs/Reference/Networking/Network-Protocol.html) /// and Dissonance's PacketWriter.WriteVoiceData, big-endian throughout: /// /// u16 magic 0x8BC7 | u8 type (=2) | u32 session | u16 sender | u8 flags (0x80 | channelSession) /// | u16 sequence | u16 channelCount | { u16 bitfield, u16 id } * channelCount | u16 payloadLength | payload /// /// public readonly struct DissonanceFrame { public const ushort Magic = 0x8BC7; /// Bytes before the channel list: magic, type, session, sender, flags, sequence, channel count. public const int FixedHeaderSize = 2 + 1 + 4 + 2 + 1 + 2 + 2; public uint SessionId { get; } public ushort SenderId { get; } /// The 7-bit wrapping counter that increments each time the sender restarts its channel session. public byte ChannelSession { get; } public ushort Sequence { get; } public IReadOnlyList Channels { get; } /// The Opus frame, as a slice of the packet passed to (no copy). public ReadOnlyMemory Payload { get; } private DissonanceFrame(uint sessionId, ushort senderId, byte channelSession, ushort sequence, DissonanceChannel[] channels, ReadOnlyMemory payload) { SessionId = sessionId; SenderId = senderId; ChannelSession = channelSession; Sequence = sequence; Channels = channels; Payload = payload; } /// True if starts with the Dissonance magic number. public static bool HasMagic(ReadOnlySpan packet) => packet.Length >= 2 && BinaryPrimitives.ReadUInt16BigEndian(packet) == Magic; /// Reads the packet type without parsing the rest. False if too short or no magic. public static bool TryGetMessageType(ReadOnlySpan packet, out DissonanceMessageType type) { type = 0; if (packet.Length < 3 || !HasMagic(packet)) return false; type = (DissonanceMessageType)packet[2]; return true; } // [impl->REQ-VOICE-OUTBOUND-TAP] /// /// Parses a complete VoiceData packet. Rejects anything that is not exactly one well-formed /// VoiceData frame so the tap never feeds garbage to the decoder. /// public static bool TryParse(ReadOnlyMemory packet, out DissonanceFrame frame, out DissonanceFrameError error) { frame = default; var span = packet.Span; if (span.Length >= 2 && !HasMagic(span)) { error = DissonanceFrameError.BadMagic; return false; } if (span.Length < FixedHeaderSize) { error = DissonanceFrameError.TooShort; return false; } if ((DissonanceMessageType)span[2] != DissonanceMessageType.VoiceData) { error = DissonanceFrameError.NotVoiceData; return false; } var offset = 3; var session = BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset)); offset += 4; var sender = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; var flags = span[offset]; offset += 1; if ((flags & 0x80) == 0) { error = DissonanceFrameError.BadFlags; return false; } var sequence = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; var channelCount = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; if (span.Length < offset + channelCount * 4 + 2) { error = DissonanceFrameError.Truncated; return false; } var channels = channelCount == 0 ? Array.Empty() : new DissonanceChannel[channelCount]; for (var i = 0; i < channelCount; i++) { var bitfield = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; var id = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; channels[i] = new DissonanceChannel(bitfield, id); } var payloadLength = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(offset)); offset += 2; if (span.Length < offset + payloadLength) { error = DissonanceFrameError.Truncated; return false; } if (span.Length != offset + payloadLength) { error = DissonanceFrameError.TrailingBytes; return false; } frame = new DissonanceFrame(session, sender, (byte)(flags & 0x7F), sequence, channels, packet.Slice(offset, payloadLength)); error = DissonanceFrameError.None; return true; } }