using System.Reflection; using BepInEx.Logging; using Dissonance; using Dissonance.Audio.Codecs; using Dissonance.Audio.Codecs.Opus; using Il2CppInterop.Runtime.InteropTypes.Arrays; using TravelEar.Core; namespace TravelEar; /// /// Every game class and method the mod touches, resolved once at load. Names are facts from the /// decompiled game (see AGENTS.md); if any is missing after a game update the whole mod stays /// off and says so in one log line, instead of running with whatever still binds. /// internal static class GameSymbols { /// OpusEncoder.Encode(ArraySegment<float>, ArraySegment<byte>): produces every Outbound Voice frame. public static MethodInfo OpusEncoderEncode { get; private set; } /// OpusDecoder.Decode(EncodedBuffer, ArraySegment<float>): the game's own decoder, used for the round trip. public static MethodInfo OpusDecoderDecode { get; private set; } /// LocalVoiceProvider.Start(): subscribes the provider to the mic; skipped for the mod-owned instance. public static MethodInfo LocalVoiceProviderStart { get; private set; } /// The provider's IMicrophoneSubscriber.ReceiveMicrophoneData proxy: how decoded PCM enters its ring. public static MethodInfo LocalVoiceProviderReceive { get; private set; } /// AudioFilterMixer.OnAudioFilterRead(float[], int): the end of the Filter Stage; the Tap sits here. public static MethodInfo AudioFilterMixerOnAudioFilterRead { get; private set; } /// VoiceBroadcastTrigger.Start(): where the transmit-signal probe collects the scene's triggers. public static MethodInfo VoiceBroadcastTriggerStart { get; private set; } /// AudioMixer.SetFloat(string, float): the game's writes to the main mixer's exposed floats (MasterWet); the only way to read them, see . public static MethodInfo AudioMixerSetFloat { get; private set; } public static bool IsBound { get; private set; } /// Unity's SFX Reverb parameter names in its documented order; AudioDynamicReverb exposes them as DSP_*, AudioBasicReverb bare, and the main mixer under the same names. public static readonly string[] ReverbParameterNames = { "DryLevel", "Room", "RoomHF", "RoomLF", "DecayTime", "DecayHFRatio", "Reflections", "ReflectDelay", "Reverb", "ReverbDelay", "HFReference", "LFReference", "Diffusion", "Density", }; // [impl->REQ-HAZARD-NO-PARTIAL-FIDELITY] /// /// Resolves all symbols. Returns false, having logged the complete list of misses, if any /// symbol is absent. Callers must not install any hook when this returns false. /// public static bool Bind(ManualLogSource log) { var missing = new SymbolBinder(); OpusEncoderEncode = Method(missing, typeof(OpusEncoder), "Encode", typeof(Il2CppSystem.ArraySegment), typeof(Il2CppSystem.ArraySegment)); OpusDecoderDecode = Method(missing, typeof(OpusDecoder), "Decode", typeof(EncodedBuffer), typeof(Il2CppSystem.ArraySegment)); LocalVoiceProviderStart = Method(missing, typeof(LocalVoiceProvider), "Start"); LocalVoiceProviderReceive = Method(missing, typeof(LocalVoiceProvider), "Dissonance_Audio_Capture_IMicrophoneSubscriber_ReceiveMicrophoneData", typeof(Il2CppSystem.ArraySegment), typeof(NAudio.Wave.WaveFormat)); AudioFilterMixerOnAudioFilterRead = Method(missing, typeof(AudioFilterMixer), "OnAudioFilterRead", typeof(Il2CppStructArray), typeof(int)); VoiceBroadcastTriggerStart = Method(missing, typeof(VoiceBroadcastTrigger), "Start"); AudioMixerSetFloat = Method(missing, typeof(UnityEngine.Audio.AudioMixer), "SetFloat", typeof(string), typeof(float)); // Fields and properties the renderer assigns or reads (interop exposes fields as properties). Property(missing, typeof(VoicePlayer), "Cue"); Property(missing, typeof(VoicePlayer), "LocalVoiceProvider"); Property(missing, typeof(VoicePlayer), "Volume"); Property(missing, typeof(VoicePlayer), "PlayerType"); Property(missing, typeof(VoicePlayer), "Controller"); Property(missing, typeof(AudioSourceController), "FilterMixer"); Property(missing, typeof(GlobalAudioEffects), "Instance"); Property(missing, typeof(GlobalAudioEffects), "VoiceCues"); Property(missing, typeof(AudioManager), "Instance"); Property(missing, typeof(AudioManager), "ListenerPosition"); Property(missing, typeof(LocalVoiceProvider), "CachedVoiceData"); Property(missing, typeof(LocalVoiceProvider), "CachedVoiceWriteHead"); // Transmit signal (M2 T0): comms mute, the open room channels and the broadcast triggers. Property(missing, typeof(WorldManager), "instance"); Property(missing, typeof(WorldManager), "dissonanceComms"); Property(missing, typeof(DissonanceComms), "IsMuted"); Property(missing, typeof(DissonanceComms), "RoomChannels"); Property(missing, typeof(RoomChannels), "_openChannelsBySubId"); Property(missing, typeof(RoomChannel), "_roomId"); Property(missing, typeof(RoomName), "Name"); Property(missing, typeof(VoiceBroadcastTrigger), "IsTransmitting"); Property(missing, typeof(VoiceBroadcastTrigger), "RoomName"); Property(missing, typeof(VoiceBroadcastTrigger), "Mode"); Property(missing, typeof(VoiceBroadcastTrigger), "_isVadSpeaking"); Property(missing, typeof(VoiceBroadcastTrigger), "_activationFaderSettings"); Property(missing, typeof(VolumeFaderSettings), "_fadeInTicks"); Property(missing, typeof(VolumeFaderSettings), "_fadeOutTicks"); // Mixer Stage inputs (M3 T1): the local player's fall state and synced outdoorness, the // listener's dynamic reverb (PlayerVoicePlaybackControl.Update's speaker/listener terms). Property(missing, typeof(WorldManager), "localPlayerCharacter"); Property(missing, typeof(PlayerCharacter), "faller"); Property(missing, typeof(PlayerCharacter), "playerNetworking"); Property(missing, typeof(PlayerFaller), "isInDanger"); Property(missing, typeof(PlayerNetworking), "outdoorness"); Property(missing, typeof(AudioManager), "AudioDynamicReverb"); Property(missing, typeof(AudioDynamicReverb), "Outdoorness"); // Megaphone state (M3 T2): the held prop's radio assigner and its broadcast state. Property(missing, typeof(PlayerCharacter), "hands"); Property(missing, typeof(PlayerHands), "heldProp"); Property(missing, typeof(Prop), "radioVoiceAssigner"); Property(missing, typeof(RadioVoiceAssigner), "_cachedVoiceType"); Property(missing, typeof(RadioVoiceAssigner), "isBroadcasting"); Property(missing, typeof(RadioVoiceAssigner), "latestBroadcastPlayer"); Property(missing, typeof(RadioVoiceAssigner), "roomName"); // Environment reverb (M3 T2b): the listener's SFX Reverb parameters as the game writes them // (Dynamic or Basic mode), the Master Wet return level and the voice slider. Property(missing, typeof(AudioManager), "AudioBasicReverb"); Property(missing, typeof(AudioDynamicReverb), "Bypass"); Property(missing, typeof(AudioDynamicReverb), "RoomSize"); Property(missing, typeof(AudioDynamicReverb), "ReverbTime"); Property(missing, typeof(AudioDynamicReverb), "Diffusion"); Property(missing, typeof(AudioBasicReverb), "Bypass"); Property(missing, typeof(PlayerCharacter), "speechless"); Property(missing, typeof(PlayerSpeechless), "speechlessness"); foreach (var name in ReverbParameterNames) { Property(missing, typeof(AudioDynamicReverb), "DSP_" + name); Property(missing, typeof(AudioBasicReverb), name); } Property(missing, typeof(GlobalAudioEffects), "Mixer"); Property(missing, typeof(GlobalAudioEffects), "VoiceNormalVol"); Property(missing, typeof(GlobalAudioEffects), "VoiceAudioSettingsVol"); // One verdict, one line: with any miss the mod stays off (docs/KNOWN-HAZARDS.md 3.1). IsBound = missing.Complete(out var report); if (IsBound) log.LogInfo(report); else log.LogError(report); return IsBound; } private const BindingFlags Any = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; // No lambdas here: a lambda returning a BCL reference type makes the compiler synthesize // NullableAttribute, which clashes with the Il2CppInterop proxies (see the csproj). private static MethodInfo Method(SymbolBinder binder, Type type, string name, params Type[] parameters) { MethodInfo method = null; try { method = type.GetMethod(name, Any, null, parameters, null); } catch (Exception) { /* ambiguous or unresolvable: a miss, never fatal here */ } binder.Require($"{type.FullName}.{name}({string.Join(", ", parameters.Select(p => p.Name))})", method != null); return method; } private static void Property(SymbolBinder binder, Type type, string name) { PropertyInfo property = null; try { property = type.GetProperty(name, Any); } catch (Exception) { /* ambiguous: a miss */ } binder.Require($"{type.FullName}.{name}", property != null); } }