using System;
using System.Collections.Concurrent;
using System.Threading;
using HarmonyLib;
using System.Reflection;
namespace TravelEar;
///
/// The main mixer's exposed floats the renderer needs but cannot read: a Harmony postfix on
/// AudioMixer.SetFloat(string, float) that remembers the game's own writes.
///
/// AudioMixer.GetFloat(string, out float) is stripped from this game and comes back
/// through Il2CppInterop's unstripping with Unity 6's managed body, which pins the name as an
/// Il2CppSystem.ReadOnlySpan<char> whose GetPinnableReference is stripped too:
/// every call throws MissingMethodException (M3 run 3). The native
/// GetFloat_Injected icall is not registered in GameAssembly.dll either, so there is no
/// read path at all. SetFloat(string, float) is a real IL2CPP method the game calls, so
/// its detour sees every value the game writes.
///
///
/// Until the game writes a watched parameter its value is unknown; the reader reports that and
/// the caller assumes the mixer asset's nominal 0 dB, logged once, rather than bypassing the
/// stage for a parameter the game may only touch on a settings change.
///
///
[HarmonyPatch]
internal static class MixerFloats
{
/// The Master Wet return bus volume, dB. The environment reverb's return level.
public const string MasterWet = "MasterWet";
private static readonly ConcurrentDictionary Values = new();
private const int FirstWriteLogCap = 60;
private static int _firstWriteLogs;
/// Called on every successful write (main thread) while a calibration capture is on.
public static volatile Action Observer;
/// How many distinct exposed floats the game has written since load.
public static int DistinctNames => Values.Count;
/// The last value the game wrote for an exposed float, if it has written one.
public static bool TryGet(string name, out float value) => Values.TryGetValue(name, out value);
private static int _masterWetWritten;
private static float _masterWetDb;
private static long _masterWetWrites;
/// Number of MasterWet writes the game has made since load.
public static long MasterWetWrites => Interlocked.Read(ref _masterWetWrites);
///
/// The last MasterWet value the game wrote. False (and 0 dB) while the game has not
/// written it yet; the caller decides what to assume and says so.
///
public static bool TryGetMasterWet(out float db)
{
if (Volatile.Read(ref _masterWetWritten) != 0)
{
db = Volatile.Read(ref _masterWetDb);
return true;
}
db = 0f;
return false;
}
[HarmonyTargetMethod]
private static MethodBase TargetMethod() => GameSymbols.AudioMixerSetFloat;
// [impl->REQ-MIXER-RESYNTH]
[HarmonyPostfix]
private static void Postfix(string name, float value, bool __result)
{
try
{
if (!__result || name is null) return;
// Every distinct name is logged on its first write (capped): the map of what the
// game drives on its mixer, e.g. the red-bell zone effects (M4).
if (!Values.ContainsKey(name) && _firstWriteLogs < FirstWriteLogCap)
{
_firstWriteLogs++;
Plugin.Logger.LogInfo($"Mixer floats: first write of '{name}' = {value:F2} (#{_firstWriteLogs}).");
}
Values[name] = value;
Observer?.Invoke(name, value);
if (name != MasterWet) return;
Volatile.Write(ref _masterWetDb, value);
Volatile.Write(ref _masterWetWritten, 1);
Interlocked.Increment(ref _masterWetWrites);
}
catch (Exception)
{
// Never disturb the game's mixer writes.
}
}
}