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;
}
}