using System.Diagnostics;
using System.Reflection;
using HarmonyLib;
namespace TravelEar;
///
/// The Outbound Voice tap: a Harmony postfix on Dissonance's OpusEncoder.Encode. Its
/// return value is the exact encoded frame the game goes on to send to peers, produced from the
/// mic audio after the game's capture preprocessing, so by CONTEXT.md it is Outbound
/// Voice. It fires whenever the local player transmits, whether or not any peer is listening.
///
/// Why not the network send (M1 spike S2, runs 1-5): a host with no listeners never builds a
/// VoiceData packet, and a postfix on the generic BaseClient<...>.SendVoiceData
/// installs but never fires under IL2CPP generic sharing. OpusEncoder.Encode is
/// non-generic and upstream of both. The VoiceData wire parser (TravelEar.Core.DissonanceFrame)
/// stays as tested reference code.
///
///
[HarmonyPatch]
internal static class OutboundVoiceTap
{
///
/// Raised on Dissonance's encoder thread for every encoded frame with a local sequence
/// number, a private copy of the Opus bytes, and the timestamp taken
/// as the postfix entered (the Offset capture point, REQ-OFFSET-MEASURE). Handlers must
/// be cheap and must not touch Unity objects.
///
public static event Action FrameEncoded;
public static long Frames;
public static int LastInputSamples;
private const int VerboseFrames = 3;
private const int SummaryEvery = 500;
[HarmonyTargetMethod]
private static MethodBase TargetMethod() => GameSymbols.OpusEncoderEncode;
// [impl->REQ-VOICE-OUTBOUND-TAP]
[HarmonyPostfix]
private static void Postfix(Il2CppSystem.ArraySegment samples, Il2CppSystem.ArraySegment __result)
{
try
{
var capturedAt = Stopwatch.GetTimestamp();
var bytes = Copy(__result);
LastInputSamples = samples.Count;
var n = (int)Interlocked.Increment(ref Frames);
if (n <= VerboseFrames)
Plugin.Logger.LogInfo($"Tap: frame #{n} in={samples.Count} samples out={bytes.Length} bytes toc=0x{(bytes.Length > 0 ? bytes[0] : 0):X2}");
else if (n % SummaryEvery == 0)
Plugin.Logger.LogInfo($"Tap: {n} frames encoded; last {bytes.Length} bytes");
FrameEncoded?.Invoke(n, bytes, capturedAt);
}
catch (Exception e)
{
// Never let the tap disturb the game's encode path.
if (Interlocked.Read(ref Frames) <= VerboseFrames)
Plugin.Logger.LogError($"Tap: postfix threw: {e}");
}
}
/// Copies the segment out of IL2CPP memory into a managed array.
private static byte[] Copy(Il2CppSystem.ArraySegment segment)
{
var array = segment.Array;
var count = segment.Count;
var offset = segment.Offset;
if (array is null || count <= 0) return Array.Empty();
var bytes = new byte[count];
for (var i = 0; i < count; i++) bytes[i] = array[offset + i];
return bytes;
}
}