using System.Reflection;
using System.Text;
using Dissonance;
using HarmonyLib;
namespace TravelEar;
///
/// The "peers receive" signal for the transmit gate (M2 T0, M2-PLAN question 1, settled by
/// runs 1-2), read on the main thread each tick. Peers receive while the comms are not muted and
/// either a room other than the game's always-open Echo room is open (the token rooms:
/// radio, megaphone, GhostRoom once its channel opens) or any voice-activation
/// VoiceBroadcastTrigger reports its VAD speaking.
///
/// Why the VAD flag and not the open channels alone: the game's "Self Echo" trigger holds the
/// Echo room open in Open mode, so the encoder runs continuously while unmuted and says nothing
/// about peers; the peer-facing channels are voice-activation triggers (GhostRoom and the
/// proximity trigger) whose channel opens on VAD, and in a solo session that channel never opens
/// (no peer, no collider) while the VAD flag still flips with speech. The open-room test stays
/// as the complement for the Open-mode token rooms, which transmit without VAD.
///
///
/// Triggers are collected by a postfix on their Start, which runs for every trigger in
/// the scene (Object.FindObjectOfType is stripped from this build). Any failure to read
/// the signal reports false and the caller fails open (renders
/// everything), never closed: a broken read must not silence Local Voice (docs/KNOWN-HAZARDS.md,
/// no partial fidelity means no silent degradation either way).
///
///
[HarmonyPatch]
internal static class TransmitSignal
{
/// The game's always-open self-monitoring room; frames sent only there reach no peer.
public const string EchoRoom = "Echo";
private static readonly List Triggers = new(); // main thread only
private static readonly StringBuilder Rooms = new();
private static readonly StringBuilder Transmitting = new();
public readonly struct Sample
{
public Sample(bool available, bool peersReceive, bool muted, bool vadSpeaking, string rooms, string triggersTransmitting)
{
Available = available;
PeersReceive = peersReceive;
Muted = muted;
VadSpeaking = vadSpeaking;
RoomsText = rooms;
TriggersText = triggersTransmitting;
}
/// False until the world and its DissonanceComms exist, or when the read failed.
public bool Available { get; }
/// Not muted, and a token room is open or a voice-activation trigger hears speech.
public bool PeersReceive { get; }
public bool Muted { get; }
public bool VadSpeaking { get; }
public string RoomsText { get; }
public string TriggersText { get; }
/// One line for the state-change log; equal strings mean an unchanged state.
public string Describe() =>
Available
? $"muted {Muted}, vad {VadSpeaking}, rooms=[{RoomsText}], transmitting=[{TriggersText}] -> gate {(PeersReceive ? "OPEN" : "closed")}"
: "unavailable (no DissonanceComms yet) -> gate OPEN (fail open)";
}
[HarmonyTargetMethod]
private static MethodBase TargetMethod() => GameSymbols.VoiceBroadcastTriggerStart;
[HarmonyPostfix]
private static void TrackTrigger(VoiceBroadcastTrigger __instance)
{
try
{
var pointer = __instance.Pointer;
foreach (var known in Triggers)
if (known.Pointer == pointer) return;
Triggers.Add(__instance);
}
catch (Exception e)
{
Plugin.Logger.LogWarning($"Transmit signal: could not track a VoiceBroadcastTrigger: {e.Message}");
}
}
///
/// Main thread. The channel fade of the game's voice-activation triggers, from their own
/// VolumeFaderSettings: the longest fade-out among them and that trigger's fade-in.
/// False until a voice-activation trigger has started. Throws on an interop failure (the
/// caller logs once and keeps the hard gate).
///
public static bool TryReadFade(out double fadeInMs, out double fadeOutMs, out string source)
{
fadeInMs = 0;
fadeOutMs = -1;
source = null;
foreach (var trigger in Triggers)
{
if (trigger is null || trigger.Pointer == IntPtr.Zero) continue;
if (trigger.Mode != CommActivationMode.VoiceActivation) continue;
var settings = trigger._activationFaderSettings;
if (settings is null) continue;
var outMs = settings._fadeOutTicks / 10000.0; // TimeSpan ticks
if (outMs <= fadeOutMs) continue;
fadeOutMs = outMs;
fadeInMs = settings._fadeInTicks / 10000.0;
var name = trigger.RoomName;
source = string.IsNullOrEmpty(name) ? "(unnamed)" : name;
}
return fadeOutMs >= 0;
}
/// Main thread. Never throws; a failed read returns an unavailable sample and the error.
public static Sample Read(out Exception error)
{
error = null;
try
{
var comms = WorldManager.instance?.dissonanceComms;
if (comms is null) return default;
var muted = comms.IsMuted;
Rooms.Clear();
var tokenRoomOpen = false;
var roomChannels = comms.RoomChannels?._openChannelsBySubId;
if (roomChannels is not null)
{
var rooms = roomChannels.Values.GetEnumerator();
while (rooms.MoveNext())
{
var name = rooms.Current._roomId.Name;
if (Rooms.Length > 0) Rooms.Append(", ");
Rooms.Append(name);
if (!string.Equals(name, EchoRoom, StringComparison.Ordinal)) tokenRoomOpen = true;
}
}
Transmitting.Clear();
var vadSpeaking = false;
for (var i = Triggers.Count - 1; i >= 0; i--)
{
var trigger = Triggers[i];
if (trigger is null || trigger.Pointer == IntPtr.Zero)
{
Triggers.RemoveAt(i);
continue;
}
if (trigger.Mode == CommActivationMode.VoiceActivation && trigger._isVadSpeaking) vadSpeaking = true;
if (!trigger.IsTransmitting) continue;
if (Transmitting.Length > 0) Transmitting.Append(", ");
Transmitting.Append(trigger.RoomName).Append('(').Append(trigger.Mode).Append(')');
}
var peersReceive = !muted && (tokenRoomOpen || vadSpeaking);
return new Sample(true, peersReceive, muted, vadSpeaking, Rooms.ToString(), Transmitting.ToString());
}
catch (Exception e)
{
error = e;
return default;
}
}
}