using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Dissonance; using Dissonance.Integrations.Unity_NFGO; using EnhancedSpectator.Config; using EnhancedSpectator.Features; using EnhancedSpectator.Features.FloatingHead; using EnhancedSpectator.Features.ModelInspection; using EnhancedSpectator.Features.PlayerStateSync; using EnhancedSpectator.Features.Spectator; using EnhancedSpectator.Features.SpectatorPresence; using EnhancedSpectator.Features.VoiceActivity; using EnhancedSpectator.Features.VoiceDiagnostics; using EnhancedSpectator.Features.VoiceRouting; using EnhancedSpectator.GameInterop; using EnhancedSpectator.Logging; using EnhancedSpectator.Networking; using EnhancedSpectator.Patching; using EnhancedSpectator.Runtime; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Steamworks; using Steamworks.Data; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.Audio; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Rendering; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("EnhancedSpectator")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+12c7eaf60f9d1e976f15c11fdf911b1e41c5140e")] [assembly: AssemblyProduct("EnhancedSpectator")] [assembly: AssemblyTitle("EnhancedSpectator")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace EnhancedSpectator { [BepInPlugin("Auuueser.EnhancedSpectator", "Enhanced Spectator", "0.2.0")] public sealed class Plugin : BaseUnityPlugin { private FeatureBootstrapper? _featureBootstrapper; private PatchBootstrapper? _patchBootstrapper; private bool _applicationQuitting; private bool _shutdownComplete; private void Awake() { RuntimeConnectionState.Reset(); ModLog.Initialize(((BaseUnityPlugin)this).Logger); ModLog.Info("Enhanced Spectator 0.2.0 starting."); Application.quitting += OnApplicationQuitting; EnhancedSpectatorConfig enhancedSpectatorConfig = EnhancedSpectatorConfig.Bind(((BaseUnityPlugin)this).Config); ModLog.SetDebugEnabled(enhancedSpectatorConfig.EnableDebugLogging.Value); if (enhancedSpectatorConfig.EnableDebugLogging.Value) { PluginDiagnostics.LogPluginBinaryHash(((BaseUnityPlugin)this).Info.Location); } _featureBootstrapper = new FeatureBootstrapper(enhancedSpectatorConfig); _featureBootstrapper.Initialize(); _patchBootstrapper = new PatchBootstrapper(); _patchBootstrapper.Register(); EnhancedSpectatorRuntimeDriver.Install(_featureBootstrapper, Shutdown); ModLog.Info("Local spectator freecam initialized."); } private void OnDestroy() { if (!_applicationQuitting) { EnhancedSpectatorRuntimeDriver.EnsureInstalled(); ModLog.Warning("Plugin component destroyed before application quit; preserving runtime driver and patches."); } else { Shutdown(); } } private void OnApplicationQuitting() { _applicationQuitting = true; RuntimeConnectionState.MarkApplicationQuitting(); Shutdown(); } private void Shutdown() { if (!_shutdownComplete) { _shutdownComplete = true; RuntimeConnectionState.MarkPluginShuttingDown(); Application.quitting -= OnApplicationQuitting; EnhancedSpectatorRuntimeDriver.BeginShutdown(); _patchBootstrapper?.Dispose(); _featureBootstrapper?.Dispose(); ModLog.Info("Local spectator freecam shut down."); } } } public static class PluginMetadata { public const string Guid = "Auuueser.EnhancedSpectator"; public const string Name = "Enhanced Spectator"; public const string Version = "0.2.0"; } } namespace EnhancedSpectator.Runtime { public sealed class EnhancedSpectatorRuntimeDriver : MonoBehaviour { [CompilerGenerated] private static class <>O { public static UnityAction <0>__OnSceneLoaded; public static UnityAction <1>__OnSceneUnloaded; public static CameraCallback <2>__OnCameraPreCull; public static Action <3>__OnBeginCameraRendering; } private static FeatureBootstrapper? _featureBootstrapper; private static Action? _shutdown; private static EnhancedSpectatorRuntimeDriver? _instance; private static bool _applicationQuitting; private static bool _sceneHookRegistered; private static int _lastCameraTickFrame = -1; private static int _lastCameraTickInstanceId = int.MinValue; private bool _intentionalDestroy; public static void Install(FeatureBootstrapper featureBootstrapper, Action shutdown) { //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Expected O, but got Unknown //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Expected O, but got Unknown _featureBootstrapper = featureBootstrapper ?? throw new ArgumentNullException("featureBootstrapper"); _shutdown = shutdown ?? throw new ArgumentNullException("shutdown"); _applicationQuitting = false; if (!_sceneHookRegistered) { SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; CameraCallback onPreCull = Camera.onPreCull; object obj = <>O.<2>__OnCameraPreCull; if (obj == null) { CameraCallback val = OnCameraPreCull; <>O.<2>__OnCameraPreCull = val; obj = (object)val; } Camera.onPreCull = (CameraCallback)Delegate.Combine((Delegate?)(object)onPreCull, (Delegate?)obj); RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; _sceneHookRegistered = true; } EnsureInstalled(); } public static void EnsureInstalled() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown if (!_applicationQuitting && _featureBootstrapper != null && !((Object)(object)_instance != (Object)null)) { GameObject val = new GameObject("Enhanced Spectator Runtime"); Object.DontDestroyOnLoad((Object)val); _instance = val.AddComponent(); } } public static void BeginShutdown() { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Expected O, but got Unknown _applicationQuitting = true; RuntimeConnectionState.MarkPluginShuttingDown(); if (_sceneHookRegistered) { SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneUnloaded -= OnSceneUnloaded; CameraCallback onPreCull = Camera.onPreCull; object obj = <>O.<2>__OnCameraPreCull; if (obj == null) { CameraCallback val = OnCameraPreCull; <>O.<2>__OnCameraPreCull = val; obj = (object)val; } Camera.onPreCull = (CameraCallback)Delegate.Remove((Delegate?)(object)onPreCull, (Delegate?)obj); RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; _sceneHookRegistered = false; } if ((Object)(object)_instance != (Object)null) { _instance._intentionalDestroy = true; Object.Destroy((Object)(object)((Component)_instance).gameObject); _instance = null; } _featureBootstrapper = null; _shutdown = null; _lastCameraTickFrame = -1; _lastCameraTickInstanceId = int.MinValue; } private void Update() { _featureBootstrapper?.Tick(); } private void LateUpdate() { _featureBootstrapper?.LateTick(); } private void OnGUI() { _featureBootstrapper?.GuiTick(); } private void OnApplicationQuit() { _applicationQuitting = true; RuntimeConnectionState.MarkApplicationQuitting(); _shutdown?.Invoke(); _shutdown = null; } private void OnDestroy() { if ((Object)(object)_instance == (Object)(object)this) { _instance = null; } if (!_applicationQuitting && !_intentionalDestroy) { ModLog.Warning("Runtime driver destroyed before application quit; it will be recreated on the next plugin or scene lifecycle event."); } } private static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { RuntimeConnectionState.MarkSceneTransition(); EnsureInstalled(); } private static void OnSceneUnloaded(Scene scene) { RuntimeConnectionState.MarkSceneTransition(); } private static void OnCameraPreCull(Camera camera) { TryCameraPreCullTick(camera); } private static void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) { TryCameraPreCullTick(camera); } private static void TryCameraPreCullTick(Camera camera) { if (_featureBootstrapper != null && !((Object)(object)camera == (Object)null)) { int frameCount = Time.frameCount; int instanceID = ((Object)camera).GetInstanceID(); if (_lastCameraTickFrame != frameCount || _lastCameraTickInstanceId != instanceID) { _lastCameraTickFrame = frameCount; _lastCameraTickInstanceId = instanceID; _featureBootstrapper.CameraPreCullTick(camera); } } } } public interface IRuntimeCameraPreCullTickable { void CameraPreCullTick(Camera camera); } public interface IRuntimeGuiTickable { void GuiTick(); } public interface IRuntimeLateTickable { void LateTick(); } public interface IRuntimeTickable { void Tick(); } public static class RuntimeConnectionState { private const float SceneTransitionUnsafeSeconds = 0.5f; private static bool _applicationQuitting; private static bool _pluginShuttingDown; private static float _unsafeUntilRealtime; public static void Reset() { _applicationQuitting = false; _pluginShuttingDown = false; _unsafeUntilRealtime = 0f; } public static void MarkApplicationQuitting() { _applicationQuitting = true; MarkUnsafeWindow(); } public static void MarkPluginShuttingDown() { _pluginShuttingDown = true; MarkUnsafeWindow(); } public static void MarkSceneTransition() { MarkUnsafeWindow(); } public static bool CanUseModNetworking(out string reason) { if (IsLifecycleUnsafe(out reason)) { return false; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { reason = "NetworkManager unavailable"; return false; } if (singleton.ShutdownInProgress) { reason = "NetworkManager shutdown in progress"; return false; } if (!singleton.IsListening) { reason = "NetworkManager is not listening"; return false; } if (!singleton.IsClient && !singleton.IsHost) { reason = "not connected as client or host"; return false; } if (!singleton.IsConnectedClient && !singleton.IsHost) { reason = "local client is not connected"; return false; } if (singleton.CustomMessagingManager == null) { reason = "CustomMessagingManager unavailable"; return false; } reason = string.Empty; return true; } public static bool CanRunLocalDiagnostics(out string reason) { if (IsLifecycleUnsafe(out reason)) { return false; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null && singleton.ShutdownInProgress) { reason = "NetworkManager shutdown in progress"; return false; } reason = string.Empty; return true; } public static bool CanRepairVanillaPlayerState(out string reason) { if (IsLifecycleUnsafe(out reason)) { return false; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null && singleton.ShutdownInProgress) { reason = "NetworkManager shutdown in progress"; return false; } reason = string.Empty; return true; } public static bool ShouldSkipVanillaSpectatorTargetSwitch(out string reason) { if (IsLifecycleUnsafe(out reason)) { return true; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { reason = "NetworkManager unavailable"; return true; } if (singleton.ShutdownInProgress) { reason = "NetworkManager shutdown in progress"; return true; } if (!singleton.IsListening || (!singleton.IsClient && !singleton.IsHost)) { reason = "network is disconnected"; return true; } reason = string.Empty; return false; } private static bool IsLifecycleUnsafe(out string reason) { if (_applicationQuitting) { reason = "application is quitting"; return true; } if (_pluginShuttingDown) { reason = "plugin is shutting down"; return true; } if (Time.realtimeSinceStartup < _unsafeUntilRealtime) { reason = "scene transition unsafe window"; return true; } reason = string.Empty; return false; } private static void MarkUnsafeWindow() { float num = Time.realtimeSinceStartup + 0.5f; if (num > _unsafeUntilRealtime) { _unsafeUntilRealtime = num; } } } } namespace EnhancedSpectator.Patching { public interface IPatchModule { void Register(Harmony harmony); void Unregister(Harmony harmony); } public sealed class PatchBootstrapper : IDisposable { private readonly Harmony _harmony = new Harmony("Auuueser.EnhancedSpectator"); private readonly List _modules = new List { new SpectatorLifecyclePatchModule() }; private bool _registered; public void Register() { if (_registered) { return; } foreach (IPatchModule module in _modules) { module.Register(_harmony); } _registered = true; ModLog.Debug("Patch modules registered."); } public void Dispose() { if (_registered) { for (int num = _modules.Count - 1; num >= 0; num--) { _modules[num].Unregister(_harmony); } _harmony.UnpatchSelf(); _registered = false; ModLog.Debug("Patch modules unregistered."); } } } public sealed class SpectatorLifecyclePatchModule : IPatchModule { [HarmonyPatch(typeof(PlayerControllerB), "KillPlayer", new Type[] { typeof(Vector3), typeof(bool), typeof(CauseOfDeath), typeof(int), typeof(Vector3), typeof(bool) })] private static class PlayerKillPatch { private static void Postfix(PlayerControllerB __instance) { if (IsLocalPlayerOrOwner(__instance)) { SpectatorLifecycleEvents.Raise(SpectatorLifecycleEventKind.PlayerDied); } } } [HarmonyPatch(typeof(PlayerControllerB), "SetSpectatedPlayerEffects", new Type[] { typeof(bool) })] private static class SpectatedPlayerEffectsPatch { private static void Postfix(PlayerControllerB __instance) { if (IsLocalSpectatorContext(__instance)) { SpectatorLifecycleEvents.Raise(SpectatorLifecycleEventKind.SpectatedPlayerEffectsApplied); } } } [HarmonyPatch(typeof(StartOfRound), "SwitchCamera", new Type[] { typeof(Camera) })] private static class SwitchCameraPatch { private static void Postfix() { SpectatorLifecycleEvents.Raise(SpectatorLifecycleEventKind.CameraSwitched); } } [HarmonyPatch(typeof(StartOfRound), "SetSpectateCameraToGameOverMode", new Type[] { typeof(bool), typeof(PlayerControllerB) })] private static class GameOverSpectateModePatch { private static void Postfix() { SpectatorLifecycleEvents.Raise(SpectatorLifecycleEventKind.GameOverOverrideChanged); } } [HarmonyPatch(typeof(StartOfRound), "ReviveDeadPlayers")] private static class ReviveDeadPlayersPatch { private static void Postfix() { SpectatorLifecycleEvents.Raise(SpectatorLifecycleEventKind.Revived); } } [HarmonyPatch(typeof(StartOfRound), "OnPlayerConnectedClientRpc", new Type[] { typeof(ulong), typeof(int), typeof(ulong[]), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(bool) })] private static class PlayerConnectedPatch { private static int _nextDebugFrame; private static void Postfix() { RestoreLocalDeadSpectatorStateAfterPlayerJoin(); } private static void RestoreLocalDeadSpectatorStateAfterPlayerJoin() { if (!RuntimeConnectionState.CanRepairVanillaPlayerState(out string reason)) { LogDebug("Skipped local spectator state restore after player connect: " + reason + "."); return; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return; } PlayerControllerB localPlayerController = instance.localPlayerController; if (!((Object)(object)localPlayerController == (Object)null) && IsLocalPlayerOrOwner(localPlayerController) && localPlayerController.isPlayerDead && localPlayerController.isPlayerControlled) { localPlayerController.isPlayerControlled = false; Camera spectateCamera = instance.spectateCamera; if ((Object)(object)spectateCamera != (Object)null && (Object)(object)instance.activeCamera != (Object)(object)spectateCamera) { instance.SwitchCamera(spectateCamera); } LogDebug("Restored local dead spectator state after vanilla player-connect sync."); } } private static void LogDebug(string message) { if (Time.frameCount >= _nextDebugFrame) { _nextDebugFrame = Time.frameCount + 120; ModLog.Debug(message); } } } [HarmonyPatch(typeof(PlayerControllerB), "SpectateNextPlayer", new Type[] { typeof(bool) })] private static class SpectateNextPlayerPatch { private static int _nextSuppressDebugFrame; private static int _nextUnsafeDebugFrame; private static bool Prefix(PlayerControllerB __instance) { if (!TryGetLocalDeadSpectatorContext(__instance, out string reason)) { if (reason.Length > 0 && Time.frameCount >= _nextUnsafeDebugFrame) { _nextUnsafeDebugFrame = Time.frameCount + 120; ModLog.Debug("Vanilla spectator target switch suppression skipped: " + reason + "."); } return true; } if (RuntimeConnectionState.ShouldSkipVanillaSpectatorTargetSwitch(out string reason2)) { if (Time.frameCount >= _nextUnsafeDebugFrame) { _nextUnsafeDebugFrame = Time.frameCount + 120; ModLog.Debug("Skipped vanilla spectator target switch during unsafe lifecycle: " + reason2 + "."); } return false; } if (!TryGetLocalSpectatorContext(__instance, out string reason3)) { if (reason3.Length > 0 && Time.frameCount >= _nextUnsafeDebugFrame) { _nextUnsafeDebugFrame = Time.frameCount + 120; ModLog.Debug("Vanilla spectator target switch suppression skipped: " + reason3 + "."); } return true; } if (SpectatorVanillaInputGuard.ShouldSuppressTargetSwitchInput(out string reason4)) { if (Time.frameCount >= _nextSuppressDebugFrame) { _nextSuppressDebugFrame = Time.frameCount + 120; ModLog.Debug("Suppressed vanilla spectator target switch while " + ((reason4.Length > 0) ? reason4 : "quick menu is open") + "."); } return false; } return true; } private static bool TryGetLocalDeadSpectatorContext(PlayerControllerB player, out string reason) { reason = string.Empty; if ((Object)(object)player == (Object)null) { reason = "patch instance unavailable"; return false; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { reason = "round unavailable"; return false; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { reason = "local player unavailable"; return false; } if (!IsLocalPlayerOrOwner(player, localPlayerController)) { return false; } if (!localPlayerController.isPlayerDead) { return false; } return true; } private static bool TryGetLocalSpectatorContext(PlayerControllerB player, out string reason) { reason = string.Empty; if ((Object)(object)player == (Object)null) { reason = "patch instance unavailable"; return false; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { reason = "round unavailable"; return false; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { reason = "local player unavailable"; return false; } if (!IsLocalPlayerOrOwner(player, localPlayerController)) { return false; } if (!localPlayerController.isPlayerDead) { return false; } if ((Object)(object)instance.spectateCamera == (Object)null || (Object)(object)localPlayerController.spectatedPlayerScript == (Object)null) { reason = "spectator camera or target unavailable"; return false; } return true; } } [HarmonyPatch(typeof(PlayerControllerB), "Interact_performed", new Type[] { typeof(CallbackContext) })] private static class InteractPerformedPatch { private static int _nextSuppressDebugFrame; private static bool Prefix(PlayerControllerB __instance) { if (!ShouldSuppressQuickMenuGameplayInput(__instance, out string reason)) { return true; } LogQuickMenuGameplaySuppression("interact", reason, ref _nextSuppressDebugFrame); return false; } } [HarmonyPatch(typeof(PlayerControllerB), "ActivateItem_performed", new Type[] { typeof(CallbackContext) })] private static class ActivateItemPerformedPatch { private static int _nextSuppressDebugFrame; private static bool Prefix(PlayerControllerB __instance) { if (!ShouldSuppressQuickMenuGameplayInput(__instance, out string reason)) { return true; } LogQuickMenuGameplaySuppression("activate item", reason, ref _nextSuppressDebugFrame); return false; } } public void Register(Harmony harmony) { harmony.CreateClassProcessor(typeof(PlayerKillPatch)).Patch(); harmony.CreateClassProcessor(typeof(SpectatedPlayerEffectsPatch)).Patch(); harmony.CreateClassProcessor(typeof(SwitchCameraPatch)).Patch(); harmony.CreateClassProcessor(typeof(GameOverSpectateModePatch)).Patch(); harmony.CreateClassProcessor(typeof(ReviveDeadPlayersPatch)).Patch(); harmony.CreateClassProcessor(typeof(PlayerConnectedPatch)).Patch(); harmony.CreateClassProcessor(typeof(SpectateNextPlayerPatch)).Patch(); harmony.CreateClassProcessor(typeof(InteractPerformedPatch)).Patch(); harmony.CreateClassProcessor(typeof(ActivateItemPerformedPatch)).Patch(); } public void Unregister(Harmony harmony) { } private static bool IsLocalSpectatorContext(PlayerControllerB player) { if (IsLocalPlayerOrOwner(player)) { return true; } PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer != (Object)null && localPlayer.isPlayerDead) { return (Object)(object)localPlayer.spectatedPlayerScript == (Object)(object)player; } return false; } private static bool IsLocalPlayerOrOwner(PlayerControllerB player) { if ((Object)(object)player == (Object)null) { return false; } if ((Object)(object)GetLocalPlayer() == (Object)(object)player) { return true; } if (!((NetworkBehaviour)player).IsOwner) { return ((NetworkBehaviour)player).IsLocalPlayer; } return true; } private static bool IsLocalPlayerOrOwner(PlayerControllerB player, PlayerControllerB localPlayer) { if ((Object)(object)player == (Object)null) { return false; } if ((Object)(object)localPlayer == (Object)(object)player) { return true; } if (!((NetworkBehaviour)player).IsOwner) { return ((NetworkBehaviour)player).IsLocalPlayer; } return true; } private static PlayerControllerB? GetLocalPlayer() { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.localPlayerController; } private static bool ShouldSuppressQuickMenuGameplayInput(PlayerControllerB player, out string reason) { reason = string.Empty; if (!SpectatorVanillaInputGuard.ShouldSuppressGameplayInteractInput() && !IsLocalQuickMenuOpen()) { return false; } PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer == (Object)null) { reason = "local player unavailable"; return false; } if (!IsLocalPlayerOrOwner(player, localPlayer)) { return false; } reason = "quick menu is open"; return true; } private static bool IsLocalQuickMenuOpen() { PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer != (Object)null && (Object)(object)localPlayer.quickMenuManager != (Object)null) { return localPlayer.quickMenuManager.isMenuOpen; } return false; } private static void LogQuickMenuGameplaySuppression(string inputName, string reason, ref int nextSuppressDebugFrame) { if (Time.frameCount >= nextSuppressDebugFrame) { nextSuppressDebugFrame = Time.frameCount + 120; ModLog.Debug("Suppressed local " + inputName + " input while " + reason + "."); } } } } namespace EnhancedSpectator.Networking { public sealed class EnhancedSpectatorNetworkService : IEnhancedSpectatorNetworkService { private const float CapabilityStableDelaySeconds = 0.35f; private const int CapabilityStableDelayFrames = 3; private const float CompatiblePeerProbeTimeoutSeconds = 2.5f; private const float CapabilityProbeRetryIntervalSeconds = 2.5f; private const float SpectatorPoseHeartbeatIntervalSeconds = 0.5f; private readonly EnhancedSpectatorConfig _config; private readonly ISpectatorTargetStateProvider _spectatorTargetStateProvider; private readonly ISpectatorPoseStateProvider _spectatorPoseStateProvider; private readonly IPeerIdentityStateProvider _peerIdentityStateProvider; private readonly IVoiceActivityProvider _voiceActivityProvider; private readonly IModNetworkTransport _transport; private readonly INetworkRuntimeState _runtimeState; private readonly RemotePeerRegistry _peerRegistry; private readonly RemoteSpectatorTargetRegistry _remoteTargetRegistry; private readonly RemoteSpectatorPoseRegistry _remotePoseRegistry; private readonly RemotePeerIdentityRegistry _remoteIdentityRegistry; private readonly RemoteVoiceActivityRegistry _remoteVoiceActivityRegistry; private readonly VoiceActivityDebugLimiter _voiceDebugLimiter; private SpectatorTargetState? _lastObservedTargetState; private SpectatorTargetState? _lastSentTargetState; private SpectatorTargetState? _pendingTargetState; private SpectatorPoseState? _lastObservedPoseState; private SpectatorPoseState? _lastSentPoseState; private SpectatorPoseState? _pendingPoseState; private bool _pendingPoseRefresh; private VoiceActivityState? _lastObservedVoiceActivityState; private VoiceActivityState? _lastSentVoiceActivityState; private VoiceActivityState? _pendingVoiceActivityState; private PeerIdentityState? _lastSentIdentityState; private bool _pendingVoiceActivityRefresh; private bool _initialized; private bool _networkAvailable; private bool _targetSyncReady; private bool _hasCompatibleModPeer; private bool _noCompatiblePeerLocalOnly; private bool _capabilitySent; private float _capabilityProbeSentRealtime; private ulong? _lastLocalClientId; private float _nextTargetSyncTime; private float _nextPoseSyncTime; private float _nextVoiceActivitySyncTime; private float _nextTargetSampleTime; private float _nextPoseSampleTime; private float _nextPoseRefreshTime; private float _nextVoiceActivitySampleTime; private float _nextVoiceActivityRefreshTime; private float _nextPeerPruneTime; private float _transportRegisteredRealtime; private int _transportRegisteredFrame; private int _nextLifecycleDebugFrame; private int _nextCapabilityDelayDebugFrame; private string? _lastDegradationReason; private string? _lastTargetSyncWaitReason; private NetworkLifecycleState _lifecycleState; public bool IsNetworkAvailable => _networkAvailable; public bool IsTargetSyncEnabled => _targetSyncReady; public bool HasCompatibleModPeer => _hasCompatibleModPeer; public NetworkLifecycleState LifecycleState => _lifecycleState; public int RemotePeerIdentityRevision => _remoteIdentityRegistry.Revision; public int RemoteSpectatorTargetRevision => _remoteTargetRegistry.Revision; public EnhancedSpectatorNetworkService(EnhancedSpectatorConfig config, SpectatorModule spectatorModule) { EnhancedSpectatorConfig config2 = config; this..ctor(config2, spectatorModule, spectatorModule, spectatorModule, new LethalCompanyVoiceActivityProvider(), new UnityNetcodeMessagingTransport(() => config2.DebugNetworkMessages.Value), UnityNetworkRuntimeState.Instance); } public EnhancedSpectatorNetworkService(EnhancedSpectatorConfig config, ISpectatorTargetStateProvider spectatorTargetStateProvider, ISpectatorPoseStateProvider spectatorPoseStateProvider, IPeerIdentityStateProvider peerIdentityStateProvider, IVoiceActivityProvider voiceActivityProvider, IModNetworkTransport transport) : this(config, spectatorTargetStateProvider, spectatorPoseStateProvider, peerIdentityStateProvider, voiceActivityProvider, transport, UnityNetworkRuntimeState.Instance) { } public EnhancedSpectatorNetworkService(EnhancedSpectatorConfig config, ISpectatorTargetStateProvider spectatorTargetStateProvider, ISpectatorPoseStateProvider spectatorPoseStateProvider, IPeerIdentityStateProvider peerIdentityStateProvider, IVoiceActivityProvider voiceActivityProvider, IModNetworkTransport transport, INetworkRuntimeState runtimeState) { _peerRegistry = new RemotePeerRegistry(); _remoteTargetRegistry = new RemoteSpectatorTargetRegistry(); _remotePoseRegistry = new RemoteSpectatorPoseRegistry(); _remoteIdentityRegistry = new RemotePeerIdentityRegistry(); _remoteVoiceActivityRegistry = new RemoteVoiceActivityRegistry(); _voiceDebugLimiter = new VoiceActivityDebugLimiter(); _capabilityProbeSentRealtime = -1f; _transportRegisteredFrame = -1; base..ctor(); _config = config ?? throw new ArgumentNullException("config"); _spectatorTargetStateProvider = spectatorTargetStateProvider ?? throw new ArgumentNullException("spectatorTargetStateProvider"); _spectatorPoseStateProvider = spectatorPoseStateProvider ?? throw new ArgumentNullException("spectatorPoseStateProvider"); _peerIdentityStateProvider = peerIdentityStateProvider ?? throw new ArgumentNullException("peerIdentityStateProvider"); _voiceActivityProvider = voiceActivityProvider ?? throw new ArgumentNullException("voiceActivityProvider"); _transport = transport ?? throw new ArgumentNullException("transport"); _runtimeState = runtimeState ?? throw new ArgumentNullException("runtimeState"); } public void Initialize() { if (!_initialized) { _initialized = true; _lifecycleState = NetworkLifecycleState.LocalOnly; ModLog.Debug("Networking service initialized."); } } public void Tick() { if (!_initialized) { return; } try { string reason; if (!_config.EnableNetworking.Value) { Degrade("networking config disabled"); } else if (!_runtimeState.CanUseModNetworking(out reason)) { StopNetworkingForLifecycle(reason); } else if (EnsureTransportRegistered()) { _networkAvailable = true; _lifecycleState = NetworkLifecycleState.TransportRegistered; RegisterLocalCapability(); PruneDisconnectedPeers(); SendLocalCapabilityIfNeeded(); UpdateTargetSyncReadiness(); UpdateCompatiblePeerProbeState(); if (!NetworkCompatibilityPolicy.ShouldRunBusinessSync(_lifecycleState, _targetSyncReady)) { ClearBusinessSyncState(); return; } UpdateAndSendLocalPeerIdentity(); UpdateLocalSpectatorTargetIfDue(); UpdateLocalSpectatorPoseIfDue(); UpdateLocalVoiceActivityIfDue(); UpdateTargetSyncReadiness(); TrySendPendingTargetState(); TrySendPendingPoseState(); TrySendPendingVoiceActivityState(); } } catch (Exception ex) { Degrade("networking exception: " + ex.GetType().Name); ModLog.Error($"Enhanced Spectator networking failed and degraded to local-only mode: {ex}"); } } public void Dispose() { _transport.Dispose(); ClearNetworkState(); _initialized = false; _lifecycleState = NetworkLifecycleState.Disposed; ModLog.Debug("Networking service disposed."); } public bool TryGetPeerCapability(ulong clientId, out ModPeerCapability capability) { return _peerRegistry.TryGetCapability(clientId, out capability); } public bool TryGetRemoteSpectatorTarget(ulong clientId, out SpectatorTargetState state) { return _remoteTargetRegistry.TryGet(clientId, out state); } public bool TryGetRemoteSpectatorPose(ulong clientId, out SpectatorPoseState state) { return _remotePoseRegistry.TryGet(clientId, out state); } public bool TryGetRemotePeerIdentity(ulong clientId, out PeerIdentityState state) { return _remoteIdentityRegistry.TryGet(clientId, out state); } public bool TryGetRemoteVoiceActivity(ulong clientId, out VoiceActivityState state) { if (!_remoteVoiceActivityRegistry.TryGet(clientId, out state, out var receivedAtTicks)) { return false; } long ticks = TimeSpan.FromSeconds(Mathf.Max(0f, _config.VoiceActivityStaleSeconds.Value)).Ticks; if (VoiceActivitySyncRules.IsFresh(receivedAtTicks, _runtimeState.UtcNowTicks, ticks)) { return true; } DebugVoice($"Remote voice activity expired: peer={clientId}."); _remoteVoiceActivityRegistry.Remove(clientId); state = VoiceActivityState.NoData; return false; } public IReadOnlyList GetRemoteSpectatorTargets() { return _remoteTargetRegistry.GetSnapshot().AsReadOnly(); } public void CopyRemoteSpectatorTargetsTo(List destination) { if (destination == null) { throw new ArgumentNullException("destination"); } _remoteTargetRegistry.CopySnapshotTo(destination); } public IReadOnlyList GetRemoteSpectatorPoses() { return _remotePoseRegistry.GetSnapshot().AsReadOnly(); } public IReadOnlyList GetRemotePeerIdentities() { return _remoteIdentityRegistry.GetSnapshot().AsReadOnly(); } public void CopyRemotePeerIdentitiesTo(List destination) { if (destination == null) { throw new ArgumentNullException("destination"); } _remoteIdentityRegistry.CopySnapshotTo(destination); } public IReadOnlyList GetRemoteVoiceActivities() { return _remoteVoiceActivityRegistry.GetSnapshot().AsReadOnly(); } public IReadOnlyList GetKnownModdedPeers() { return _peerRegistry.GetCapabilitiesSnapshot().AsReadOnly(); } private bool EnsureTransportRegistered() { if (_transport.IsRegistered) { if (!_transport.IsNetworkAvailable) { Degrade("custom messaging transport no longer available"); return false; } return true; } if (!_transport.TryRegister(OnCapabilityReceived, OnSpectatorTargetReceived, OnSpectatorPoseReceived, OnPeerIdentityReceived, OnVoiceActivityReceived, out string reason)) { Degrade(reason); return false; } _capabilitySent = false; _capabilityProbeSentRealtime = -1f; _noCompatiblePeerLocalOnly = false; _lastLocalClientId = _transport.LocalClientId; _transportRegisteredFrame = _runtimeState.FrameCount; _transportRegisteredRealtime = _runtimeState.RealtimeSinceStartup; _lastDegradationReason = null; Debug("Custom messaging transport registered."); return true; } private void RegisterLocalCapability() { ulong localClientId = _transport.LocalClientId; if (_lastLocalClientId.HasValue && _lastLocalClientId.Value != localClientId) { _peerRegistry.Clear(); _remoteTargetRegistry.Clear(); _remotePoseRegistry.Clear(); _remoteIdentityRegistry.Clear(); _remoteVoiceActivityRegistry.Clear(); _lastObservedTargetState = null; _lastSentTargetState = null; _pendingTargetState = null; _lastObservedPoseState = null; _lastSentPoseState = null; _pendingPoseState = null; _pendingPoseRefresh = false; _lastObservedVoiceActivityState = null; _lastSentVoiceActivityState = null; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; _lastSentIdentityState = null; _nextTargetSyncTime = 0f; _nextPoseSyncTime = 0f; _nextVoiceActivitySyncTime = 0f; _nextTargetSampleTime = 0f; _nextPoseSampleTime = 0f; _nextPoseRefreshTime = 0f; _nextVoiceActivitySampleTime = 0f; _nextVoiceActivityRefreshTime = 0f; _capabilitySent = false; _capabilityProbeSentRealtime = -1f; _noCompatiblePeerLocalOnly = false; _transportRegisteredFrame = _runtimeState.FrameCount; _transportRegisteredRealtime = _runtimeState.RealtimeSinceStartup; Debug($"Local Netcode client id changed from {_lastLocalClientId.Value} to {localClientId}; reset network state."); } _lastLocalClientId = localClientId; _peerRegistry.RegisterLocal(CreateLocalCapability(localClientId)); } private void SendLocalCapabilityIfNeeded() { if (!_config.EnableCapabilityHandshake.Value) { _capabilitySent = false; } else { if (_capabilitySent && !NetworkCompatibilityPolicy.ShouldRetryCapabilityProbe(_targetSyncReady, _capabilitySent, _capabilityProbeSentRealtime, _runtimeState.RealtimeSinceStartup, 2.5f)) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { StopNetworkingForLifecycle(reason); return; } if (!IsTransportStableForCapability(out string reason2)) { DebugCapabilityDelay(reason2); return; } ModPeerCapability modPeerCapability = CreateLocalCapability(_transport.LocalClientId); _peerRegistry.RegisterLocal(modPeerCapability); if (_transport.SendCapability(modPeerCapability, null, out string reason3)) { _capabilitySent = true; _capabilityProbeSentRealtime = _runtimeState.RealtimeSinceStartup; _noCompatiblePeerLocalOnly = false; _lifecycleState = NetworkLifecycleState.TransportRegistered; Debug($"Capability sent: client={modPeerCapability.ClientId}, targetSync={modPeerCapability.SupportsSpectatorTargetSync}, voiceSync={modPeerCapability.SupportsVoiceActivitySync}, voiceRoute={modPeerCapability.SupportsSpectatorVoiceToTarget}."); } else { Degrade("capability send failed: " + reason3); } } } private bool IsTransportStableForCapability(out string reason) { int num = _runtimeState.FrameCount - _transportRegisteredFrame; if (_transportRegisteredFrame < 0 || num < 3) { reason = $"transport registered {Mathf.Max(0, num)} frame(s) ago"; return false; } float num2 = _runtimeState.RealtimeSinceStartup - _transportRegisteredRealtime; if (num2 < 0.35f) { reason = $"transport registered {num2:0.00}s ago"; return false; } reason = string.Empty; return true; } private void UpdateAndSendLocalPeerIdentity() { if (!_capabilitySent || !_targetSyncReady || !_peerIdentityStateProvider.TryGetLocalPeerIdentity(out PeerIdentityState state) || string.IsNullOrWhiteSpace(state.DisplayName) || IdentityEquals(_lastSentIdentityState, state)) { return; } List spectatorTargetSyncPeerIds = _peerRegistry.GetSpectatorTargetSyncPeerIds(_transport.LocalClientId); if (spectatorTargetSyncPeerIds.Count != 0) { if (_transport.SendPeerIdentity(state, spectatorTargetSyncPeerIds, out string reason)) { _lastSentIdentityState = state; Debug($"Peer identity sent: client={state.ClientId}, slot={state.PlayerSlotId}, name={state.DisplayName}, voiceName={FormatVoiceName(state.VoicePlayerName)}."); } else { Degrade("peer identity send failed: " + reason); } } } private void PruneDisconnectedPeers(bool force = false) { if (!force && _runtimeState.UnscaledTime < _nextPeerPruneTime) { return; } _nextPeerPruneTime = _runtimeState.UnscaledTime + 1f; if (!_lastLocalClientId.HasValue) { return; } foreach (ulong remotePeerId in _peerRegistry.GetRemotePeerIds(_lastLocalClientId.Value)) { if ((_transport.IsHost || !_peerRegistry.IsRelayedPeer(remotePeerId)) && !_transport.IsPeerConnected(remotePeerId)) { RelayDisconnectedPeerCleanup(remotePeerId); _peerRegistry.Remove(remotePeerId); _remoteTargetRegistry.Remove(remotePeerId); _remotePoseRegistry.Remove(remotePeerId); _remoteIdentityRegistry.Remove(remotePeerId); _remoteVoiceActivityRegistry.Remove(remotePeerId); Debug($"Removed disconnected network peer state: peer={remotePeerId}."); } } } private void UpdateTargetSyncReadiness() { string text = null; if (!_config.EnableCapabilityHandshake.Value) { text = "capability handshake disabled"; } else if (!_config.EnableSpectatorTargetSync.Value) { text = "spectator target sync disabled"; } else if (!_transport.IsRegistered) { text = "custom messaging transport not registered"; } else if (!_peerRegistry.HasSpectatorTargetSyncPeer(_transport.LocalClientId)) { text = "waiting for compatible remote peer capability"; } _targetSyncReady = text == null; _hasCompatibleModPeer = _targetSyncReady; if (_config.DebugNetworkMessages.Value && _lastTargetSyncWaitReason != text) { _lastTargetSyncWaitReason = text; if (text == null) { ModLog.Debug("Spectator target sync is ready."); } else { ModLog.Debug("Spectator target sync is not ready: " + text + "."); } } } private void UpdateCompatiblePeerProbeState() { _lifecycleState = NetworkCompatibilityPolicy.ResolveLifecycleState(_targetSyncReady, _capabilitySent, _capabilityProbeSentRealtime, _runtimeState.RealtimeSinceStartup, 2.5f); _noCompatiblePeerLocalOnly = _lifecycleState == NetworkLifecycleState.NoCompatiblePeerLocalOnly; if (_noCompatiblePeerLocalOnly && _lastDegradationReason != "no compatible Enhanced Spectator peer capability received") { _lastDegradationReason = "no compatible Enhanced Spectator peer capability received"; Debug("Networking remains local-only because no compatible Enhanced Spectator peer answered the capability probe."); } } private void ClearBusinessSyncState() { _lastObservedTargetState = null; _lastSentTargetState = null; _pendingTargetState = null; _lastObservedPoseState = null; _lastSentPoseState = null; _pendingPoseState = null; _pendingPoseRefresh = false; _lastObservedVoiceActivityState = null; _lastSentVoiceActivityState = null; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; _lastSentIdentityState = null; _nextTargetSampleTime = 0f; _nextPoseSampleTime = 0f; _nextPoseRefreshTime = 0f; _nextVoiceActivitySampleTime = 0f; } private void UpdateLocalSpectatorTargetIfDue() { if (!_config.EnableSpectatorTargetSync.Value) { _lastObservedTargetState = null; _lastSentTargetState = null; _pendingTargetState = null; } else if (NetworkSyncSamplingRules.ShouldSample(_lastObservedTargetState != null, _runtimeState.UnscaledTime, _nextTargetSampleTime)) { UpdateLocalSpectatorTarget(); _nextTargetSampleTime = NetworkSyncSamplingRules.ResolveNextSampleTime(_runtimeState.UnscaledTime, 0.1f); } } private void UpdateLocalSpectatorTarget() { if (!_spectatorTargetStateProvider.TryGetCurrentSpectatorTarget(out SpectatorTargetState state)) { state = new SpectatorTargetState(isSpectating: false, _transport.LocalClientId, 0uL, null, null, _runtimeState.UtcNowTicks); } if (_lastObservedTargetState == null || !_lastObservedTargetState.Equals(state)) { _lastObservedTargetState = state; if (_lastSentTargetState != null && _lastSentTargetState.Equals(state)) { _pendingTargetState = null; } else { _pendingTargetState = state; } Debug($"Observed spectator target change: spectating={state.IsSpectating}, localClient={state.LocalClientId}, localSlot={state.LocalPlayerSlotId}, targetClient={FormatNullable(state.TargetClientId)}, targetSlot={FormatNullable(state.TargetPlayerSlotId)}."); } } private void UpdateLocalSpectatorPoseIfDue() { if (!_config.EnableSpectatorPoseSync.Value) { _lastObservedPoseState = null; _lastSentPoseState = null; _pendingPoseState = null; _pendingPoseRefresh = false; } else if (NetworkSyncSamplingRules.ShouldSample(_lastObservedPoseState != null, _runtimeState.UnscaledTime, _nextPoseSampleTime)) { UpdateLocalSpectatorPose(); _nextPoseSampleTime = NetworkSyncSamplingRules.ResolveNextSampleTime(_runtimeState.UnscaledTime, GetPoseSyncInterval()); } } private void TrySendPendingTargetState() { if (!_targetSyncReady || _pendingTargetState == null) { return; } string reason; if (!_transport.IsRegistered || !_transport.IsNetworkAvailable) { StopNetworkingForLifecycle("custom messaging transport unavailable before target send"); } else if (!_runtimeState.CanUseModNetworking(out reason)) { StopNetworkingForLifecycle(reason); } else if (_lastSentTargetState != null && _lastSentTargetState.Equals(_pendingTargetState)) { _pendingTargetState = null; } else { if (_runtimeState.UnscaledTime < _nextTargetSyncTime) { return; } List spectatorTargetSyncPeerIds = _peerRegistry.GetSpectatorTargetSyncPeerIds(_transport.LocalClientId); if (spectatorTargetSyncPeerIds.Count == 0) { UpdateTargetSyncReadiness(); return; } SpectatorTargetSyncMessage spectatorTargetSyncMessage = new SpectatorTargetSyncMessage(1, _pendingTargetState, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorTarget(spectatorTargetSyncMessage, spectatorTargetSyncPeerIds, out string reason2)) { _lastSentTargetState = _pendingTargetState; _pendingTargetState = null; _nextTargetSyncTime = _runtimeState.UnscaledTime + 0.1f; Debug($"Spectator target sent to {spectatorTargetSyncPeerIds.Count} peer(s): spectating={spectatorTargetSyncMessage.State.IsSpectating}, targetClient={FormatNullable(spectatorTargetSyncMessage.State.TargetClientId)}, targetSlot={FormatNullable(spectatorTargetSyncMessage.State.TargetPlayerSlotId)}."); } else { Degrade("spectator target send failed: " + reason2); } } } private void UpdateLocalSpectatorPose() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) if (!_config.EnableSpectatorPoseSync.Value) { _lastObservedPoseState = null; _lastSentPoseState = null; _pendingPoseState = null; _pendingPoseRefresh = false; return; } if (!_spectatorPoseStateProvider.TryGetCurrentSpectatorPose(out SpectatorPoseState state)) { state = new SpectatorPoseState(isSpectating: false, _transport.LocalClientId, 0uL, null, null, Vector3.zero, Quaternion.identity, _runtimeState.UtcNowTicks); } if (_lastObservedPoseState != null && _lastObservedPoseState.ApproximatelyEquals(state)) { if (NetworkSyncSamplingRules.ShouldRefreshUnchangedState(state.IsSpectating, _lastSentPoseState != null && _lastSentPoseState.IsSpectating, _runtimeState.UnscaledTime, _nextPoseRefreshTime)) { _pendingPoseState = state; _pendingPoseRefresh = true; } return; } _lastObservedPoseState = state; if (_lastSentPoseState != null && _lastSentPoseState.ApproximatelyEquals(state)) { _pendingPoseState = null; _pendingPoseRefresh = false; } else { _pendingPoseState = state; _pendingPoseRefresh = false; } if (IsPoseDebugEnabled()) { ModLog.Debug($"Observed spectator pose change: spectating={state.IsSpectating}, localClient={state.LocalClientId}, targetClient={FormatNullable(state.TargetClientId)}, position={FormatVector(state.Position)}."); } } private void UpdateLocalVoiceActivityIfDue() { if (!_config.EnableVoiceActivitySync.Value) { _lastObservedVoiceActivityState = null; _lastSentVoiceActivityState = null; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; } else if (NetworkSyncSamplingRules.ShouldSample(_lastObservedVoiceActivityState != null, _runtimeState.UnscaledTime, _nextVoiceActivitySampleTime)) { UpdateLocalVoiceActivity(); _nextVoiceActivitySampleTime = NetworkSyncSamplingRules.ResolveNextSampleTime(_runtimeState.UnscaledTime, GetVoiceActivitySyncInterval()); } } private void TrySendPendingPoseState() { //IL_018a: Unknown result type (might be due to invalid IL or missing references) if (!_targetSyncReady || !_config.EnableSpectatorPoseSync.Value || _pendingPoseState == null) { return; } string reason; if (!_transport.IsRegistered || !_transport.IsNetworkAvailable) { StopNetworkingForLifecycle("custom messaging transport unavailable before pose send"); } else if (!_runtimeState.CanUseModNetworking(out reason)) { StopNetworkingForLifecycle(reason); } else if (!_pendingPoseRefresh && _lastSentPoseState != null && _lastSentPoseState.ApproximatelyEquals(_pendingPoseState)) { _pendingPoseState = null; } else { if (_runtimeState.UnscaledTime < _nextPoseSyncTime) { return; } List spectatorTargetSyncPeerIds = _peerRegistry.GetSpectatorTargetSyncPeerIds(_transport.LocalClientId); if (spectatorTargetSyncPeerIds.Count == 0) { UpdateTargetSyncReadiness(); return; } SpectatorPoseSyncMessage spectatorPoseSyncMessage = new SpectatorPoseSyncMessage(1, _pendingPoseState, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorPose(spectatorPoseSyncMessage, spectatorTargetSyncPeerIds, out string reason2)) { _lastSentPoseState = _pendingPoseState; _pendingPoseState = null; _pendingPoseRefresh = false; _nextPoseSyncTime = _runtimeState.UnscaledTime + GetPoseSyncInterval(); _nextPoseRefreshTime = _runtimeState.UnscaledTime + GetPoseHeartbeatInterval(); if (IsPoseDebugEnabled()) { ModLog.Debug($"Spectator pose sent to {spectatorTargetSyncPeerIds.Count} peer(s): spectating={spectatorPoseSyncMessage.State.IsSpectating}, targetClient={FormatNullable(spectatorPoseSyncMessage.State.TargetClientId)}, position={FormatVector(spectatorPoseSyncMessage.State.Position)}."); } } else { Degrade("spectator pose send failed: " + reason2); } } } private void UpdateLocalVoiceActivity() { if (!_config.EnableVoiceActivitySync.Value) { _lastObservedVoiceActivityState = null; _lastSentVoiceActivityState = null; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; } else { if (!TryGetLocalVoiceIdentity(out var clientId, out var slotId)) { return; } if (!_voiceActivityProvider.TryGetVoiceActivity(clientId, slotId, out VoiceActivityState state) || !state.HasData) { state = new VoiceActivityState(hasData: false, isSpeaking: false, 0f, 0f, clientId, slotId, _runtimeState.UtcNowTicks); } if (_lastObservedVoiceActivityState == null && !state.HasData) { _lastObservedVoiceActivityState = state; return; } if (VoiceActivitySyncRules.ApproximatelyEquals(_lastObservedVoiceActivityState, state)) { if (ShouldRefreshVoiceActivity(state)) { _pendingVoiceActivityState = state; _pendingVoiceActivityRefresh = true; } return; } _lastObservedVoiceActivityState = state; if (VoiceActivitySyncRules.ApproximatelyEquals(_lastSentVoiceActivityState, state)) { _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; } else { _pendingVoiceActivityState = state; _pendingVoiceActivityRefresh = false; } DebugObservedVoiceActivity(state); } } private void TrySendPendingVoiceActivityState() { if (!_targetSyncReady || !_config.EnableVoiceActivitySync.Value || _pendingVoiceActivityState == null) { return; } string reason; if (!_transport.IsRegistered || !_transport.IsNetworkAvailable) { StopNetworkingForLifecycle("custom messaging transport unavailable before voice activity send"); } else if (!_runtimeState.CanUseModNetworking(out reason)) { StopNetworkingForLifecycle(reason); } else if (!_pendingVoiceActivityRefresh && VoiceActivitySyncRules.ApproximatelyEquals(_lastSentVoiceActivityState, _pendingVoiceActivityState)) { _pendingVoiceActivityState = null; } else if (!_pendingVoiceActivityState.HasData && (_lastSentVoiceActivityState == null || !_lastSentVoiceActivityState.HasData)) { _pendingVoiceActivityState = null; } else { if (_runtimeState.UnscaledTime < _nextVoiceActivitySyncTime) { return; } List voiceActivitySyncPeerIds = _peerRegistry.GetVoiceActivitySyncPeerIds(_transport.LocalClientId); if (voiceActivitySyncPeerIds.Count == 0) { UpdateTargetSyncReadiness(); return; } VoiceActivitySyncMessage voiceActivitySyncMessage = new VoiceActivitySyncMessage(1, _pendingVoiceActivityState, _runtimeState.UtcNowTicks); if (_transport.SendVoiceActivity(voiceActivitySyncMessage, voiceActivitySyncPeerIds, out string reason2)) { _lastSentVoiceActivityState = _pendingVoiceActivityState; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; _nextVoiceActivitySyncTime = _runtimeState.UnscaledTime + GetVoiceActivitySyncInterval(); _nextVoiceActivityRefreshTime = _runtimeState.UnscaledTime + GetVoiceActivityRefreshInterval(); DebugSentVoiceActivity(voiceActivitySyncMessage.State, voiceActivitySyncPeerIds.Count); } else { Degrade("voice activity send failed: " + reason2); } } } private void OnCapabilityReceived(ulong senderClientId, ModPeerCapability capability) { if (!_initialized || !_config.EnableNetworking.Value || !_config.EnableCapabilityHandshake.Value) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { Debug($"Dropped capability from sender={senderClientId}: {reason}."); return; } if (capability.ClientId == _transport.LocalClientId) { _peerRegistry.RegisterLocal(CreateLocalCapability(_transport.LocalClientId)); return; } if (_transport.IsHost && capability.ClientId != senderClientId) { Debug($"Dropped capability with mismatched sender and peer id: sender={senderClientId}, peer={capability.ClientId}."); return; } if (!_transport.IsHost && senderClientId != capability.ClientId && senderClientId != _transport.ServerClientId) { Debug($"Dropped relayed capability from non-server sender={senderClientId}, peer={capability.ClientId}."); return; } bool flag = !_transport.IsHost && senderClientId != capability.ClientId; if (!ModPeerCapabilityRules.SupportsCurrentSpectatorTargetSync(capability)) { RemoveRemotePeerState(capability.ClientId, $"capability no longer compatible: sender={senderClientId}, targetSync={capability.SupportsSpectatorTargetSync}, voiceSync={capability.SupportsVoiceActivitySync}, voiceRoute={capability.SupportsSpectatorVoiceToTarget}, relayed={flag}"); return; } bool flag2 = _peerRegistry.RegisterRemote(capability, flag); if (flag2) { _noCompatiblePeerLocalOnly = false; _lifecycleState = NetworkLifecycleState.TransportRegistered; } Debug($"Capability received from sender={senderClientId}, peer={capability.ClientId}, protocol={capability.ProtocolVersion}, targetSync={capability.SupportsSpectatorTargetSync}, voiceSync={capability.SupportsVoiceActivitySync}, voiceRoute={capability.SupportsSpectatorVoiceToTarget}, compatible={flag2}, relayed={flag}."); if (flag2 && _transport.IsHost) { ModPeerCapability capability2 = CreateLocalCapability(_transport.LocalClientId); _peerRegistry.RegisterLocal(capability2); if (_transport.SendCapability(capability2, new ulong[1] { capability.ClientId }, out string reason2)) { Debug($"Capability reply sent to peer={capability.ClientId}."); } else { Debug($"Capability reply to peer={capability.ClientId} failed: {reason2}."); } SendLocalPeerIdentityToRecipients(new ulong[1] { capability.ClientId }); SendLocalSpectatorStateToPeer(capability.ClientId); PruneDisconnectedPeers(force: true); RelayCapabilityToCompatiblePeers(capability); RelayKnownCapabilitiesToPeer(capability.ClientId); RelayKnownSpectatorStatesToPeer(capability.ClientId); RelayKnownPeerIdentitiesToPeer(capability.ClientId); RelayKnownVoiceActivitiesToPeer(capability.ClientId); } } private void RemoveRemotePeerState(ulong peerId, string reason) { if (!_transport.IsRegistered || peerId != _transport.LocalClientId) { ModPeerCapability capability; SpectatorTargetState state; SpectatorPoseState state2; PeerIdentityState state3; VoiceActivityState state4; bool num = _peerRegistry.TryGetCapability(peerId, out capability) || _remoteTargetRegistry.TryGet(peerId, out state) || _remotePoseRegistry.TryGet(peerId, out state2) || _remoteIdentityRegistry.TryGet(peerId, out state3) || _remoteVoiceActivityRegistry.TryGet(peerId, out state4); _peerRegistry.Remove(peerId); _remoteTargetRegistry.Remove(peerId); _remotePoseRegistry.Remove(peerId); _remoteIdentityRegistry.Remove(peerId); _remoteVoiceActivityRegistry.Remove(peerId); if (num) { Debug($"Removed remote peer state for peer={peerId}: {reason}."); } else { Debug($"Ignored incompatible remote peer capability for peer={peerId}: {reason}."); } } } private void OnSpectatorTargetReceived(ulong senderClientId, SpectatorTargetState state) { if (!_initialized || !_config.EnableNetworking.Value || !_config.EnableSpectatorTargetSync.Value) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { Debug($"Dropped spectator target from sender={senderClientId}: {reason}."); } else if (state.LocalClientId != _transport.LocalClientId) { if (!HostRelayPlanner.CanAcceptSpectatorState(_transport.IsHost, _transport.LocalClientId, _transport.ServerClientId, senderClientId, state.LocalClientId, _peerRegistry, out string reason2)) { Debug($"Dropped spectator target from sender={senderClientId}, peer={state.LocalClientId}: {reason2}."); return; } _remoteTargetRegistry.Update(state); Debug($"Spectator target received from peer={state.LocalClientId}: spectating={state.IsSpectating}, targetClient={FormatNullable(state.TargetClientId)}, targetSlot={FormatNullable(state.TargetPlayerSlotId)}."); RelaySpectatorTargetState(senderClientId, state); } } private void OnSpectatorPoseReceived(ulong senderClientId, SpectatorPoseState state) { //IL_010e: Unknown result type (might be due to invalid IL or missing references) if (!_initialized || !_config.EnableNetworking.Value || !_config.EnableSpectatorPoseSync.Value) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { Debug($"Dropped spectator pose from sender={senderClientId}: {reason}."); } else { if (state.LocalClientId == _transport.LocalClientId) { return; } if (!HostRelayPlanner.CanAcceptSpectatorState(_transport.IsHost, _transport.LocalClientId, _transport.ServerClientId, senderClientId, state.LocalClientId, _peerRegistry, out string reason2)) { Debug($"Dropped spectator pose from sender={senderClientId}, peer={state.LocalClientId}: {reason2}."); return; } _remotePoseRegistry.Update(state); if (IsPoseDebugEnabled()) { ModLog.Debug($"Spectator pose received from peer={state.LocalClientId}: spectating={state.IsSpectating}, targetClient={FormatNullable(state.TargetClientId)}, position={FormatVector(state.Position)}."); } RelaySpectatorPoseState(senderClientId, state); } } private void OnPeerIdentityReceived(ulong senderClientId, PeerIdentityState state) { if (!_initialized || !_config.EnableNetworking.Value || !_config.EnableCapabilityHandshake.Value) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { Debug($"Dropped peer identity from sender={senderClientId}: {reason}."); } else if (state.ClientId != _transport.LocalClientId) { if (!HostRelayPlanner.CanAcceptSpectatorState(_transport.IsHost, _transport.LocalClientId, _transport.ServerClientId, senderClientId, state.ClientId, _peerRegistry, out string reason2)) { Debug($"Dropped peer identity from sender={senderClientId}, peer={state.ClientId}: {reason2}."); return; } _remoteIdentityRegistry.Update(state); Debug($"Peer identity received from peer={state.ClientId}: slot={state.PlayerSlotId}, name={state.DisplayName}, voiceName={FormatVoiceName(state.VoicePlayerName)}."); RelayPeerIdentity(senderClientId, state); } } private void OnVoiceActivityReceived(ulong senderClientId, VoiceActivityState state) { if (!_initialized || !_config.EnableNetworking.Value || !_config.EnableVoiceActivitySync.Value) { return; } if (!_runtimeState.CanUseModNetworking(out string reason)) { DebugVoice($"Dropped voice activity from sender={senderClientId}: {reason}."); } else if (state.ClientId != _transport.LocalClientId) { if (!HostRelayPlanner.CanAcceptSpectatorState(_transport.IsHost, _transport.LocalClientId, _transport.ServerClientId, senderClientId, state.ClientId, _peerRegistry, out string reason2)) { DebugVoice($"Dropped voice activity from sender={senderClientId}, peer={state.ClientId}: {reason2}."); return; } _remoteVoiceActivityRegistry.Update(state, _runtimeState.UtcNowTicks); DebugReceivedVoiceActivity(senderClientId, state); RelayVoiceActivityState(senderClientId, state); } } private void RelayCapabilityToCompatiblePeers(ModPeerCapability capability) { if (!_transport.IsHost || !_config.EnableHostRelay.Value) { return; } IReadOnlyList relayRecipients = HostRelayPlanner.GetRelayRecipients(_peerRegistry, _transport.LocalClientId, capability.ClientId); if (relayRecipients.Count != 0) { if (_transport.SendCapability(capability, relayRecipients, out string reason)) { Debug($"Relayed capability for peer={capability.ClientId} to {relayRecipients.Count} peer(s)."); } else { Debug($"Capability relay for peer={capability.ClientId} failed: {reason}."); } } } private void RelayKnownCapabilitiesToPeer(ulong recipientClientId) { if (!_transport.IsHost || !_config.EnableHostRelay.Value) { return; } foreach (ModPeerCapability item in _peerRegistry.GetCapabilitiesSnapshot()) { if (item.ClientId != _transport.LocalClientId && item.ClientId != recipientClientId && item.HandshakeComplete && item.ProtocolVersion == 1 && item.SupportsCapabilityHandshake && item.SupportsSpectatorTargetSync) { if (_transport.SendCapability(item, new ulong[1] { recipientClientId }, out string reason)) { Debug($"Relayed known capability for peer={item.ClientId} to peer={recipientClientId}."); } else { Debug($"Known capability relay for peer={item.ClientId} to peer={recipientClientId} failed: {reason}."); } } } } private void RelayKnownSpectatorStatesToPeer(ulong recipientClientId) { if (!_transport.IsHost || !_config.EnableHostRelay.Value) { return; } foreach (SpectatorTargetState item in _remoteTargetRegistry.GetSnapshot()) { if (item.LocalClientId == _transport.LocalClientId || item.LocalClientId == recipientClientId) { continue; } if (!IsConnectedForKnownState(item.LocalClientId)) { Debug($"Skipped known spectator target relay for peer={item.LocalClientId} to peer={recipientClientId}: origin is not connected."); continue; } if (item.TargetClientId.HasValue && !IsConnectedForKnownState(item.TargetClientId.Value)) { Debug($"Skipped known spectator target relay for peer={item.LocalClientId} to peer={recipientClientId}: target={item.TargetClientId.Value} is not connected."); continue; } SpectatorTargetSyncMessage message = new SpectatorTargetSyncMessage(1, item, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorTarget(message, new ulong[1] { recipientClientId }, out string reason)) { Debug($"Relayed known spectator target for peer={item.LocalClientId} to peer={recipientClientId}."); } else { Debug($"Known spectator target relay for peer={item.LocalClientId} to peer={recipientClientId} failed: {reason}."); } } if (!_config.EnableSpectatorPoseSync.Value) { return; } foreach (SpectatorPoseState item2 in _remotePoseRegistry.GetSnapshot()) { if (item2.LocalClientId == _transport.LocalClientId || item2.LocalClientId == recipientClientId) { continue; } if (!IsConnectedForKnownState(item2.LocalClientId)) { DebugPose($"Skipped known spectator pose relay for peer={item2.LocalClientId} to peer={recipientClientId}: origin is not connected."); continue; } if (item2.TargetClientId.HasValue && !IsConnectedForKnownState(item2.TargetClientId.Value)) { DebugPose($"Skipped known spectator pose relay for peer={item2.LocalClientId} to peer={recipientClientId}: target={item2.TargetClientId.Value} is not connected."); continue; } if (!HasMatchingStoredTarget(item2)) { DebugPose($"Skipped known spectator pose relay for peer={item2.LocalClientId} to peer={recipientClientId}: no matching active target."); continue; } SpectatorPoseSyncMessage message2 = new SpectatorPoseSyncMessage(1, item2, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorPose(message2, new ulong[1] { recipientClientId }, out string reason2)) { DebugPose($"Relayed known spectator pose for peer={item2.LocalClientId} to peer={recipientClientId}."); } else { DebugPose($"Known spectator pose relay for peer={item2.LocalClientId} to peer={recipientClientId} failed: {reason2}."); } } } private bool HasMatchingStoredPose(SpectatorTargetState targetState) { if (_remotePoseRegistry.TryGet(targetState.LocalClientId, out SpectatorPoseState state) && state.IsSpectating && state.TargetClientId == targetState.TargetClientId) { return state.TargetPlayerSlotId == targetState.TargetPlayerSlotId; } return false; } private bool HasMatchingStoredTarget(SpectatorPoseState poseState) { if (_remoteTargetRegistry.TryGet(poseState.LocalClientId, out SpectatorTargetState state) && state.IsSpectating && state.TargetClientId == poseState.TargetClientId) { return state.TargetPlayerSlotId == poseState.TargetPlayerSlotId; } return false; } private bool IsConnectedForKnownState(ulong clientId) { if (clientId != _transport.LocalClientId) { return _transport.IsPeerConnected(clientId); } return true; } private bool IsConnectedForRelayState(ulong originClientId, bool isSpectating, ulong? targetClientId, out string reason) { if (!IsConnectedForKnownState(originClientId)) { reason = "origin is not connected"; return false; } if (isSpectating && targetClientId.HasValue && !IsConnectedForKnownState(targetClientId.Value)) { reason = $"target={targetClientId.Value} is not connected"; return false; } reason = string.Empty; return true; } private void RelaySpectatorTargetState(ulong senderClientId, SpectatorTargetState state) { if (!_transport.IsHost || !_config.EnableHostRelay.Value || senderClientId != state.LocalClientId) { return; } if (!IsConnectedForRelayState(state.LocalClientId, state.IsSpectating, state.TargetClientId, out string reason)) { Debug($"Skipped spectator target relay from peer={state.LocalClientId}: {reason}."); return; } IReadOnlyList relayRecipients = HostRelayPlanner.GetRelayRecipients(_peerRegistry, _transport.LocalClientId, state.LocalClientId); if (relayRecipients.Count != 0) { SpectatorTargetSyncMessage message = new SpectatorTargetSyncMessage(1, state, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorTarget(message, relayRecipients, out string reason2)) { Debug($"Relayed spectator target from peer={state.LocalClientId} to {relayRecipients.Count} peer(s)."); } else { Debug($"Spectator target relay from peer={state.LocalClientId} failed: {reason2}."); } } } private void RelaySpectatorPoseState(ulong senderClientId, SpectatorPoseState state) { if (!_transport.IsHost || !_config.EnableHostRelay.Value || senderClientId != state.LocalClientId) { return; } if (!IsConnectedForRelayState(state.LocalClientId, state.IsSpectating, state.TargetClientId, out string reason)) { DebugPose($"Skipped spectator pose relay from peer={state.LocalClientId}: {reason}."); return; } IReadOnlyList relayRecipients = HostRelayPlanner.GetRelayRecipients(_peerRegistry, _transport.LocalClientId, state.LocalClientId); if (relayRecipients.Count != 0) { SpectatorPoseSyncMessage message = new SpectatorPoseSyncMessage(1, state, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorPose(message, relayRecipients, out string reason2)) { DebugPose($"Relayed spectator pose from peer={state.LocalClientId} to {relayRecipients.Count} peer(s)."); } else { DebugPose($"Spectator pose relay from peer={state.LocalClientId} failed: {reason2}."); } } } private void RelayVoiceActivityState(ulong senderClientId, VoiceActivityState state) { if (!_transport.IsHost || !_config.EnableHostRelay.Value || senderClientId != state.ClientId) { return; } if (!IsConnectedForKnownState(state.ClientId)) { DebugVoice($"Skipped voice activity relay from peer={state.ClientId}: origin is not connected."); return; } IReadOnlyList voiceActivityRelayRecipients = HostRelayPlanner.GetVoiceActivityRelayRecipients(_peerRegistry, _transport.LocalClientId, state.ClientId); if (voiceActivityRelayRecipients.Count != 0) { VoiceActivitySyncMessage message = new VoiceActivitySyncMessage(1, state, _runtimeState.UtcNowTicks); if (_transport.SendVoiceActivity(message, voiceActivityRelayRecipients, out string reason)) { DebugRelayedVoiceActivity(state, voiceActivityRelayRecipients.Count); } else { DebugVoice($"Voice activity relay from peer={state.ClientId} failed: {reason}."); } } } private void RelayDisconnectedPeerCleanup(ulong peerId) { //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_021d: Unknown result type (might be due to invalid IL or missing references) if (!_transport.IsHost || !_config.EnableHostRelay.Value) { return; } IReadOnlyList relayRecipients = HostRelayPlanner.GetRelayRecipients(_peerRegistry, _transport.LocalClientId, peerId); if (relayRecipients.Count == 0) { return; } ModPeerCapability capability = new ModPeerCapability(peerId, 1, supportsCapabilityHandshake: false, supportsSpectatorTargetSync: false, handshakeComplete: false, _runtimeState.UtcNowTicks); if (_transport.SendCapability(capability, relayRecipients, out string reason)) { Debug($"Relayed disconnected peer capability cleanup for peer={peerId} to {relayRecipients.Count} peer(s)."); } else { Debug($"Disconnected peer capability cleanup relay for peer={peerId} failed: {reason}."); } SpectatorTargetState state = new SpectatorTargetState(isSpectating: false, peerId, 0uL, null, null, _runtimeState.UtcNowTicks); SpectatorTargetSyncMessage message = new SpectatorTargetSyncMessage(1, state, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorTarget(message, relayRecipients, out string reason2)) { Debug($"Relayed disconnected peer target cleanup for peer={peerId} to {relayRecipients.Count} peer(s)."); } else { Debug($"Disconnected peer target cleanup relay for peer={peerId} failed: {reason2}."); } if (_config.EnableVoiceActivitySync.Value) { IReadOnlyList voiceActivityRelayRecipients = HostRelayPlanner.GetVoiceActivityRelayRecipients(_peerRegistry, _transport.LocalClientId, peerId); VoiceActivityState state2 = new VoiceActivityState(hasData: false, isSpeaking: false, 0f, 0f, peerId, 0uL, _runtimeState.UtcNowTicks); VoiceActivitySyncMessage message2 = new VoiceActivitySyncMessage(1, state2, _runtimeState.UtcNowTicks); if (voiceActivityRelayRecipients.Count > 0) { if (_transport.SendVoiceActivity(message2, voiceActivityRelayRecipients, out string reason3)) { DebugVoice($"Relayed disconnected peer voice activity cleanup for peer={peerId} to {voiceActivityRelayRecipients.Count} peer(s)."); } else { DebugVoice($"Disconnected peer voice activity cleanup relay for peer={peerId} failed: {reason3}."); } } } if (_config.EnableSpectatorPoseSync.Value) { SpectatorPoseState state3 = new SpectatorPoseState(isSpectating: false, peerId, 0uL, null, null, Vector3.zero, Quaternion.identity, _runtimeState.UtcNowTicks); SpectatorPoseSyncMessage message3 = new SpectatorPoseSyncMessage(1, state3, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorPose(message3, relayRecipients, out string reason4)) { DebugPose($"Relayed disconnected peer pose cleanup for peer={peerId} to {relayRecipients.Count} peer(s)."); } else { DebugPose($"Disconnected peer pose cleanup relay for peer={peerId} failed: {reason4}."); } } } private void SendLocalPeerIdentityToRecipients(IEnumerable recipients) { if (_peerIdentityStateProvider.TryGetLocalPeerIdentity(out PeerIdentityState state) && !string.IsNullOrWhiteSpace(state.DisplayName)) { if (_transport.SendPeerIdentity(state, recipients, out string reason)) { _lastSentIdentityState = state; Debug($"Peer identity reply sent: client={state.ClientId}, slot={state.PlayerSlotId}, name={state.DisplayName}, voiceName={FormatVoiceName(state.VoicePlayerName)}."); } else { Debug("Peer identity reply failed: " + reason + "."); } } } private void SendLocalSpectatorStateToPeer(ulong recipientClientId) { if (!_config.EnableSpectatorTargetSync.Value || !_spectatorTargetStateProvider.TryGetCurrentSpectatorTarget(out SpectatorTargetState state) || !state.IsSpectating) { return; } if (!IsConnectedForRelayState(state.LocalClientId, state.IsSpectating, state.TargetClientId, out string reason)) { Debug($"Skipped local spectator target replay to peer={recipientClientId}: {reason}."); return; } SpectatorTargetSyncMessage message = new SpectatorTargetSyncMessage(1, state, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorTarget(message, new ulong[1] { recipientClientId }, out string reason2)) { Debug($"Replayed local spectator target to peer={recipientClientId}."); } else { Debug($"Local spectator target replay to peer={recipientClientId} failed: {reason2}."); } if (_config.EnableSpectatorPoseSync.Value && _spectatorPoseStateProvider.TryGetCurrentSpectatorPose(out SpectatorPoseState state2) && state2.IsSpectating && HasMatchingLocalTarget(state, state2)) { SpectatorPoseSyncMessage message2 = new SpectatorPoseSyncMessage(1, state2, _runtimeState.UtcNowTicks); if (_transport.SendSpectatorPose(message2, new ulong[1] { recipientClientId }, out string reason3)) { DebugPose($"Replayed local spectator pose to peer={recipientClientId}."); } else { DebugPose($"Local spectator pose replay to peer={recipientClientId} failed: {reason3}."); } } } private static bool HasMatchingLocalTarget(SpectatorTargetState targetState, SpectatorPoseState poseState) { if (targetState.IsSpectating && poseState.TargetClientId == targetState.TargetClientId) { return poseState.TargetPlayerSlotId == targetState.TargetPlayerSlotId; } return false; } private void RelayPeerIdentity(ulong senderClientId, PeerIdentityState state) { if (!_transport.IsHost || !_config.EnableHostRelay.Value || senderClientId != state.ClientId) { return; } IReadOnlyList relayRecipients = HostRelayPlanner.GetRelayRecipients(_peerRegistry, _transport.LocalClientId, state.ClientId); if (relayRecipients.Count != 0) { if (_transport.SendPeerIdentity(state, relayRecipients, out string reason)) { Debug($"Relayed peer identity from peer={state.ClientId} to {relayRecipients.Count} peer(s)."); } else { Debug($"Peer identity relay from peer={state.ClientId} failed: {reason}."); } } } private void RelayKnownPeerIdentitiesToPeer(ulong recipientClientId) { if (!_transport.IsHost || !_config.EnableHostRelay.Value) { return; } foreach (PeerIdentityState item in _remoteIdentityRegistry.GetSnapshot()) { if (item.ClientId != _transport.LocalClientId && item.ClientId != recipientClientId) { if (_transport.SendPeerIdentity(item, new ulong[1] { recipientClientId }, out string reason)) { Debug($"Relayed known peer identity for peer={item.ClientId} to peer={recipientClientId}."); } else { Debug($"Known peer identity relay for peer={item.ClientId} to peer={recipientClientId} failed: {reason}."); } } } } private void RelayKnownVoiceActivitiesToPeer(ulong recipientClientId) { if (!_transport.IsHost || !_config.EnableHostRelay.Value || !_config.EnableVoiceActivitySync.Value || !_peerRegistry.TryGetCapability(recipientClientId, out ModPeerCapability capability) || !ModPeerCapabilityRules.SupportsCurrentVoiceActivitySync(capability)) { return; } foreach (VoiceActivityState item in _remoteVoiceActivityRegistry.GetSnapshot()) { if (item.ClientId == _transport.LocalClientId || item.ClientId == recipientClientId) { continue; } if (!IsConnectedForKnownState(item.ClientId)) { DebugVoice($"Skipped known voice activity relay for peer={item.ClientId} to peer={recipientClientId}: origin is not connected."); continue; } VoiceActivitySyncMessage message = new VoiceActivitySyncMessage(1, item, _runtimeState.UtcNowTicks); if (_transport.SendVoiceActivity(message, new ulong[1] { recipientClientId }, out string reason)) { DebugVoice($"Relayed known voice activity for peer={item.ClientId} to peer={recipientClientId}."); } else { DebugVoice($"Known voice activity relay for peer={item.ClientId} to peer={recipientClientId} failed: {reason}."); } } } private void Degrade(string reason) { _transport.Unregister(); ClearNetworkState(); _lifecycleState = NetworkLifecycleState.LocalOnly; if (!(_lastDegradationReason == reason)) { _lastDegradationReason = reason; Debug("Networking degraded to local-only mode: " + reason + "."); } } private void StopNetworkingForLifecycle(string reason) { if (_transport.IsRegistered) { _transport.Unregister(); } ClearNetworkState(); _lifecycleState = NetworkLifecycleState.LocalOnly; if (!(_lastDegradationReason == reason)) { _lastDegradationReason = reason; if (_runtimeState.FrameCount >= _nextLifecycleDebugFrame) { _nextLifecycleDebugFrame = _runtimeState.FrameCount + 120; Debug("Networking stopped because shutdown/disconnect was detected: " + reason + "."); } } } private void ClearNetworkState() { _peerRegistry.Clear(); _remoteTargetRegistry.Clear(); _remotePoseRegistry.Clear(); _remoteIdentityRegistry.Clear(); _remoteVoiceActivityRegistry.Clear(); _lastObservedTargetState = null; _lastSentTargetState = null; _pendingTargetState = null; _lastObservedPoseState = null; _lastSentPoseState = null; _pendingPoseState = null; _pendingPoseRefresh = false; _lastObservedVoiceActivityState = null; _lastSentVoiceActivityState = null; _pendingVoiceActivityState = null; _pendingVoiceActivityRefresh = false; _lastSentIdentityState = null; _networkAvailable = false; _targetSyncReady = false; _hasCompatibleModPeer = false; _noCompatiblePeerLocalOnly = false; _capabilitySent = false; _capabilityProbeSentRealtime = -1f; _lastLocalClientId = null; _nextPeerPruneTime = 0f; _transportRegisteredRealtime = 0f; _transportRegisteredFrame = -1; _lastTargetSyncWaitReason = null; _nextTargetSyncTime = 0f; _nextPoseSyncTime = 0f; _nextVoiceActivitySyncTime = 0f; _nextTargetSampleTime = 0f; _nextPoseSampleTime = 0f; _nextPoseRefreshTime = 0f; _nextVoiceActivitySampleTime = 0f; _nextVoiceActivityRefreshTime = 0f; _voiceDebugLimiter.Clear(); } private ModPeerCapability CreateLocalCapability(ulong localClientId) { return new ModPeerCapability(localClientId, 1, _config.EnableCapabilityHandshake.Value, _config.EnableSpectatorTargetSync.Value, _config.EnableCapabilityHandshake.Value, _runtimeState.UtcNowTicks, _config.EnableVoiceActivitySync.Value, _config.EnableSpectatorVoiceToTarget.Value); } private void Debug(string message) { if (_config.DebugNetworkMessages.Value) { ModLog.Debug(message); } } private void DebugPose(string message) { if (IsPoseDebugEnabled()) { ModLog.Debug(message); } } private bool IsPoseDebugEnabled() { if (_config.DebugNetworkMessages.Value) { return _config.DebugPoseMessages.Value; } return false; } private float GetPoseSyncInterval() { return Mathf.Max(0.02f, _config.SpectatorPoseSyncInterval.Value); } private float GetPoseHeartbeatInterval() { return Mathf.Max(0.5f, GetPoseSyncInterval() * 5f); } private float GetVoiceActivitySyncInterval() { return NetworkSyncSamplingRules.ResolveVoiceActivitySyncInterval(_config.VoiceActivitySyncInterval.Value); } private float GetVoiceActivityRefreshInterval() { return Mathf.Clamp(Mathf.Max(0.1f, _config.VoiceActivityStaleSeconds.Value) * 0.5f, 0.1f, 0.25f); } private bool ShouldRefreshVoiceActivity(VoiceActivityState state) { if (!state.HasData || (!state.IsSpeaking && state.Amplitude <= 0f)) { return false; } return _runtimeState.UnscaledTime >= _nextVoiceActivityRefreshTime; } private bool TryGetLocalVoiceIdentity(out ulong clientId, out ulong slotId) { if (_peerIdentityStateProvider.TryGetLocalPeerIdentity(out PeerIdentityState state)) { clientId = state.ClientId; slotId = state.PlayerSlotId; return true; } if (_transport.IsRegistered) { clientId = _transport.LocalClientId; slotId = 0uL; return true; } clientId = 0uL; slotId = 0uL; return false; } private void DebugVoice(string message) { if (IsVoiceDebugEnabled()) { ModLog.Debug(message); } } private void DebugObservedVoiceActivity(VoiceActivityState state) { if (ShouldLogVoiceActivity("observed", state.ClientId, state, isRelayed: false)) { ModLog.Debug($"Observed local voice activity change: hasData={state.HasData}, speaking={state.IsSpeaking}, amplitude={state.Amplitude:0.00}, client={state.ClientId}, slot={state.SlotId}."); } } private void DebugSentVoiceActivity(VoiceActivityState state, int recipientCount) { if (ShouldLogVoiceActivity("sent", state.ClientId, state, isRelayed: false)) { ModLog.Debug($"Voice activity sent to {recipientCount} peer(s): hasData={state.HasData}, speaking={state.IsSpeaking}, amplitude={state.Amplitude:0.00}."); } } private void DebugReceivedVoiceActivity(ulong senderClientId, VoiceActivityState state) { bool flag = senderClientId != state.ClientId; if (ShouldLogVoiceActivity("received", state.ClientId, state, flag)) { ModLog.Debug($"Voice activity received from peer={state.ClientId}: hasData={state.HasData}, speaking={state.IsSpeaking}, amplitude={state.Amplitude:0.00}, relayed={flag}."); } } private void DebugRelayedVoiceActivity(VoiceActivityState state, int recipientCount) { if (ShouldLogVoiceActivity("relayed", state.ClientId, state, isRelayed: true)) { ModLog.Debug($"Relayed voice activity from peer={state.ClientId} to {recipientCount} peer(s)."); } } private bool ShouldLogVoiceActivity(string category, ulong peerId, VoiceActivityState state, bool isRelayed) { if (IsVoiceDebugEnabled()) { return _voiceDebugLimiter.ShouldLog(category, peerId, _runtimeState.FrameCount, state, isRelayed); } return false; } private bool IsVoiceDebugEnabled() { if (_config.DebugNetworkMessages.Value) { return _config.DebugVoiceActivitySync.Value; } return false; } private void DebugCapabilityDelay(string reason) { if (_config.DebugNetworkMessages.Value && _runtimeState.FrameCount >= _nextCapabilityDelayDebugFrame) { _nextCapabilityDelayDebugFrame = _runtimeState.FrameCount + 120; ModLog.Debug("Capability delayed because network is not stable yet: " + reason + "."); } } private static string FormatNullable(ulong? value) { if (!value.HasValue) { return "none"; } return value.Value.ToString(); } private static string FormatVector(Vector3 value) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({value.x:0.00}, {value.y:0.00}, {value.z:0.00})"; } private static string FormatVoiceName(string value) { if (!string.IsNullOrWhiteSpace(value)) { return "present"; } return "none"; } private static bool IdentityEquals(PeerIdentityState? left, PeerIdentityState right) { if (left != null && left.ClientId == right.ClientId && left.PlayerSlotId == right.PlayerSlotId && string.Equals(left.DisplayName, right.DisplayName, StringComparison.Ordinal)) { return string.Equals(left.VoicePlayerName, right.VoicePlayerName, StringComparison.Ordinal); } return false; } } public static class HostRelayPlanner { public static IReadOnlyList GetRelayRecipients(RemotePeerRegistry registry, ulong hostClientId, ulong originClientId) { List spectatorTargetSyncPeerIds = registry.GetSpectatorTargetSyncPeerIds(hostClientId); for (int num = spectatorTargetSyncPeerIds.Count - 1; num >= 0; num--) { ulong num2 = spectatorTargetSyncPeerIds[num]; if (num2 == hostClientId || num2 == originClientId) { spectatorTargetSyncPeerIds.RemoveAt(num); } } return spectatorTargetSyncPeerIds; } public static IReadOnlyList GetVoiceActivityRelayRecipients(RemotePeerRegistry registry, ulong hostClientId, ulong originClientId) { List voiceActivitySyncPeerIds = registry.GetVoiceActivitySyncPeerIds(hostClientId); for (int num = voiceActivitySyncPeerIds.Count - 1; num >= 0; num--) { ulong num2 = voiceActivitySyncPeerIds[num]; if (num2 == hostClientId || num2 == originClientId) { voiceActivitySyncPeerIds.RemoveAt(num); } } return voiceActivitySyncPeerIds; } public static bool CanAcceptSpectatorState(bool isHost, ulong localClientId, ulong serverClientId, ulong senderClientId, ulong originClientId, RemotePeerRegistry registry, out string reason) { if (originClientId == localClientId) { reason = "origin is local client"; return false; } if (!registry.IsSpectatorTargetSyncPeer(originClientId)) { reason = "origin is not a compatible spectator sync peer"; return false; } if (isHost) { if (senderClientId == originClientId) { reason = string.Empty; return true; } reason = "host rejected state where sender does not match origin"; return false; } if (senderClientId == originClientId && !registry.IsRelayedPeer(originClientId)) { reason = string.Empty; return true; } if (senderClientId == serverClientId && registry.IsRelayedPeer(originClientId)) { reason = string.Empty; return true; } reason = "client rejected non-host relayed foreign origin"; return false; } } public interface IEnhancedSpectatorNetworkService { bool IsNetworkAvailable { get; } bool IsTargetSyncEnabled { get; } bool HasCompatibleModPeer { get; } NetworkLifecycleState LifecycleState { get; } int RemotePeerIdentityRevision { get; } int RemoteSpectatorTargetRevision { get; } void Initialize(); void Tick(); void Dispose(); bool TryGetPeerCapability(ulong clientId, out ModPeerCapability capability); bool TryGetRemoteSpectatorTarget(ulong clientId, out SpectatorTargetState state); bool TryGetRemoteSpectatorPose(ulong clientId, out SpectatorPoseState state); bool TryGetRemotePeerIdentity(ulong clientId, out PeerIdentityState state); bool TryGetRemoteVoiceActivity(ulong clientId, out VoiceActivityState state); IReadOnlyList GetRemoteSpectatorTargets(); void CopyRemoteSpectatorTargetsTo(List destination); IReadOnlyList GetRemoteSpectatorPoses(); IReadOnlyList GetRemotePeerIdentities(); void CopyRemotePeerIdentitiesTo(List destination); IReadOnlyList GetRemoteVoiceActivities(); IReadOnlyList GetKnownModdedPeers(); } public interface IModNetworkTransport : IDisposable { bool IsRegistered { get; } bool IsNetworkAvailable { get; } bool IsHost { get; } ulong LocalClientId { get; } ulong ServerClientId { get; } bool IsPeerConnected(ulong clientId); bool TryRegister(Action capabilityReceived, Action spectatorTargetReceived, Action spectatorPoseReceived, Action peerIdentityReceived, Action voiceActivityReceived, out string reason); void Unregister(); bool SendCapability(ModPeerCapability capability, IEnumerable? recipients, out string reason); bool SendSpectatorTarget(SpectatorTargetSyncMessage message, IEnumerable recipients, out string reason); bool SendSpectatorPose(SpectatorPoseSyncMessage message, IEnumerable recipients, out string reason); bool SendPeerIdentity(PeerIdentityState state, IEnumerable? recipients, out string reason); bool SendVoiceActivity(VoiceActivitySyncMessage message, IEnumerable recipients, out string reason); } public interface INetworkRuntimeState { int FrameCount { get; } float RealtimeSinceStartup { get; } float UnscaledTime { get; } long UtcNowTicks { get; } bool CanUseModNetworking(out string reason); } public sealed class UnityNetworkRuntimeState : INetworkRuntimeState { public static UnityNetworkRuntimeState Instance { get; } = new UnityNetworkRuntimeState(); public int FrameCount => Time.frameCount; public float RealtimeSinceStartup => Time.realtimeSinceStartup; public float UnscaledTime => Time.unscaledTime; public long UtcNowTicks => DateTime.UtcNow.Ticks; private UnityNetworkRuntimeState() { } public bool CanUseModNetworking(out string reason) { return RuntimeConnectionState.CanUseModNetworking(out reason); } } public interface IPeerIdentityStateProvider { bool TryGetLocalPeerIdentity(out PeerIdentityState state); } public static class ModNetworkConstants { public const int ProtocolVersion = 1; public const string CapabilityMessageName = "EnhancedSpectator.Capability.V1"; public const string SpectatorTargetMessageName = "EnhancedSpectator.SpectatorTarget.V1"; public const string SpectatorPoseMessageName = "EnhancedSpectator.SpectatorPose.V1"; public const string PeerIdentityMessageName = "EnhancedSpectator.PeerIdentity.V1"; public const string VoiceActivityMessageName = "EnhancedSpectator.VoiceActivity.V1"; public const double TargetSyncMinIntervalSeconds = 0.1; public const double VoiceActivitySyncMinIntervalSeconds = 0.066; } public static class ModNetworkSerializer { public static int CapabilityMessageSize => FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); public static int SpectatorTargetMessageSize => FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); public static int SpectatorPoseMessageSize => FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() * 7 + FastBufferWriter.GetWriteSize(); public static int PeerIdentityMessageSize => FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); public static int VoiceActivityMessageSize => FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); public static void WriteCapability(ref FastBufferWriter writer, ModPeerCapability capability) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) int protocolVersion = capability.ProtocolVersion; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref protocolVersion, default(ForPrimitives)); ulong clientId = capability.ClientId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref clientId, default(ForPrimitives)); bool supportsCapabilityHandshake = capability.SupportsCapabilityHandshake; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref supportsCapabilityHandshake, default(ForPrimitives)); supportsCapabilityHandshake = capability.SupportsSpectatorTargetSync; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref supportsCapabilityHandshake, default(ForPrimitives)); long lastSeenTicks = capability.LastSeenTicks; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref lastSeenTicks, default(ForPrimitives)); supportsCapabilityHandshake = capability.SupportsVoiceActivitySync; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref supportsCapabilityHandshake, default(ForPrimitives)); supportsCapabilityHandshake = capability.SupportsSpectatorVoiceToTarget; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref supportsCapabilityHandshake, default(ForPrimitives)); } public static bool TryReadCapability(ref FastBufferReader reader, out ModPeerCapability capability, out string reason) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) capability = null; reason = string.Empty; try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num, default(ForPrimitives)); if (num != 1) { reason = $"unsupported protocol version {num}"; return false; } ulong clientId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref clientId, default(ForPrimitives)); bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag, default(ForPrimitives)); bool supportsSpectatorTargetSync = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref supportsSpectatorTargetSync, default(ForPrimitives)); long lastSeenTicks = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe(ref lastSeenTicks, default(ForPrimitives)); bool supportsVoiceActivitySync = false; bool supportsSpectatorVoiceToTarget = false; try { ((FastBufferReader)(ref reader)).ReadValueSafe(ref supportsVoiceActivitySync, default(ForPrimitives)); } catch { supportsVoiceActivitySync = false; } try { ((FastBufferReader)(ref reader)).ReadValueSafe(ref supportsSpectatorVoiceToTarget, default(ForPrimitives)); } catch { supportsSpectatorVoiceToTarget = false; } capability = new ModPeerCapability(clientId, num, flag, supportsSpectatorTargetSync, flag, lastSeenTicks, supportsVoiceActivitySync, supportsSpectatorVoiceToTarget); return true; } catch (Exception ex) { reason = "capability read failed: " + ex.GetType().Name; return false; } } public static void WriteSpectatorTarget(ref FastBufferWriter writer, SpectatorTargetSyncMessage message) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) SpectatorTargetState state = message.State; bool hasValue = state.TargetClientId.HasValue; bool hasValue2 = state.TargetPlayerSlotId.HasValue; ulong valueOrDefault = state.TargetClientId.GetValueOrDefault(); ulong valueOrDefault2 = state.TargetPlayerSlotId.GetValueOrDefault(); int protocolVersion = message.ProtocolVersion; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref protocolVersion, default(ForPrimitives)); bool isSpectating = state.IsSpectating; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref isSpectating, default(ForPrimitives)); ulong localClientId = state.LocalClientId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref localClientId, default(ForPrimitives)); localClientId = state.LocalPlayerSlotId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref localClientId, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasValue, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref valueOrDefault, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasValue2, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref valueOrDefault2, default(ForPrimitives)); long timestampTicks = state.TimestampTicks; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref timestampTicks, default(ForPrimitives)); } public static bool TryReadSpectatorTarget(ref FastBufferReader reader, out SpectatorTargetState state, out string reason) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) state = null; reason = string.Empty; try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num, default(ForPrimitives)); if (num != 1) { reason = $"unsupported protocol version {num}"; return false; } bool isSpectating = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref isSpectating, default(ForPrimitives)); ulong localClientId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref localClientId, default(ForPrimitives)); ulong localPlayerSlotId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref localPlayerSlotId, default(ForPrimitives)); bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag, default(ForPrimitives)); ulong value = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref value, default(ForPrimitives)); bool flag2 = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag2, default(ForPrimitives)); ulong value2 = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref value2, default(ForPrimitives)); long timestampTicks = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe(ref timestampTicks, default(ForPrimitives)); state = new SpectatorTargetState(isSpectating, localClientId, localPlayerSlotId, flag ? new ulong?(value) : null, flag2 ? new ulong?(value2) : null, timestampTicks); return true; } catch (Exception ex) { reason = "spectator target read failed: " + ex.GetType().Name; return false; } } public static void WriteSpectatorPose(ref FastBufferWriter writer, SpectatorPoseSyncMessage message) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) SpectatorPoseState state = message.State; bool hasValue = state.TargetClientId.HasValue; bool hasValue2 = state.TargetPlayerSlotId.HasValue; ulong valueOrDefault = state.TargetClientId.GetValueOrDefault(); ulong valueOrDefault2 = state.TargetPlayerSlotId.GetValueOrDefault(); int protocolVersion = message.ProtocolVersion; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref protocolVersion, default(ForPrimitives)); bool isSpectating = state.IsSpectating; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref isSpectating, default(ForPrimitives)); ulong localClientId = state.LocalClientId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref localClientId, default(ForPrimitives)); localClientId = state.LocalPlayerSlotId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref localClientId, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasValue, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref valueOrDefault, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasValue2, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref valueOrDefault2, default(ForPrimitives)); Vector3 position = state.Position; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref position.x, default(ForPrimitives)); position = state.Position; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref position.y, default(ForPrimitives)); position = state.Position; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref position.z, default(ForPrimitives)); Quaternion rotation = state.Rotation; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref rotation.x, default(ForPrimitives)); rotation = state.Rotation; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref rotation.y, default(ForPrimitives)); rotation = state.Rotation; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref rotation.z, default(ForPrimitives)); rotation = state.Rotation; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref rotation.w, default(ForPrimitives)); long timestampTicks = state.TimestampTicks; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref timestampTicks, default(ForPrimitives)); } public static bool TryReadSpectatorPose(ref FastBufferReader reader, out SpectatorPoseState state, out string reason) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_018d: Unknown result type (might be due to invalid IL or missing references) state = null; reason = string.Empty; try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num, default(ForPrimitives)); if (num != 1) { reason = $"unsupported protocol version {num}"; return false; } bool isSpectating = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref isSpectating, default(ForPrimitives)); ulong localClientId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref localClientId, default(ForPrimitives)); ulong localPlayerSlotId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref localPlayerSlotId, default(ForPrimitives)); bool flag = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag, default(ForPrimitives)); ulong value = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref value, default(ForPrimitives)); bool flag2 = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref flag2, default(ForPrimitives)); ulong value2 = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref value2, default(ForPrimitives)); float num2 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num2, default(ForPrimitives)); float num3 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num3, default(ForPrimitives)); float num4 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num4, default(ForPrimitives)); float num5 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num5, default(ForPrimitives)); float num6 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num6, default(ForPrimitives)); float num7 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num7, default(ForPrimitives)); float num8 = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num8, default(ForPrimitives)); long timestampTicks = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe(ref timestampTicks, default(ForPrimitives)); state = new SpectatorPoseState(isSpectating, localClientId, localPlayerSlotId, flag ? new ulong?(value) : null, flag2 ? new ulong?(value2) : null, new Vector3(num2, num3, num4), new Quaternion(num5, num6, num7, num8), timestampTicks); return true; } catch (Exception ex) { reason = "spectator pose read failed: " + ex.GetType().Name; return false; } } public static void WritePeerIdentity(ref FastBufferWriter writer, PeerIdentityState state) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) FixedString64Bytes val = FixedString64Bytes.op_Implicit(state.DisplayName ?? string.Empty); FixedString64Bytes val2 = FixedString64Bytes.op_Implicit(state.VoicePlayerName ?? string.Empty); int num = 1; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref num, default(ForPrimitives)); ulong clientId = state.ClientId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref clientId, default(ForPrimitives)); clientId = state.PlayerSlotId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref clientId, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref val, default(ForFixedStrings)); long timestampTicks = state.TimestampTicks; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref timestampTicks, default(ForPrimitives)); ((FastBufferWriter)(ref writer)).WriteValueSafe(ref val2, default(ForFixedStrings)); } public static bool TryReadPeerIdentity(ref FastBufferReader reader, out PeerIdentityState state, out string reason) { //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) state = null; reason = string.Empty; try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num, default(ForPrimitives)); if (num != 1) { reason = $"unsupported protocol version {num}"; return false; } ulong clientId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref clientId, default(ForPrimitives)); ulong playerSlotId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref playerSlotId, default(ForPrimitives)); FixedString64Bytes val = default(FixedString64Bytes); ((FastBufferReader)(ref reader)).ReadValueSafe(ref val, default(ForFixedStrings)); long timestampTicks = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe(ref timestampTicks, default(ForPrimitives)); FixedString64Bytes val2 = default(FixedString64Bytes); try { ((FastBufferReader)(ref reader)).ReadValueSafe(ref val2, default(ForFixedStrings)); } catch { val2 = default(FixedString64Bytes); } state = new PeerIdentityState(clientId, playerSlotId, ((object)(FixedString64Bytes)(ref val)).ToString(), ((object)(FixedString64Bytes)(ref val2)).ToString(), timestampTicks); return true; } catch (Exception ex) { reason = "peer identity read failed: " + ex.GetType().Name; return false; } } public static void WriteVoiceActivity(ref FastBufferWriter writer, VoiceActivitySyncMessage message) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) VoiceActivityState state = message.State; int protocolVersion = message.ProtocolVersion; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref protocolVersion, default(ForPrimitives)); bool hasData = state.HasData; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasData, default(ForPrimitives)); hasData = state.IsSpeaking; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref hasData, default(ForPrimitives)); ulong clientId = state.ClientId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref clientId, default(ForPrimitives)); clientId = state.SlotId; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref clientId, default(ForPrimitives)); float amplitude = state.Amplitude; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref amplitude, default(ForPrimitives)); amplitude = state.Volume; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref amplitude, default(ForPrimitives)); long timestampTicks = state.TimestampTicks; ((FastBufferWriter)(ref writer)).WriteValueSafe(ref timestampTicks, default(ForPrimitives)); } public static bool TryReadVoiceActivity(ref FastBufferReader reader, out VoiceActivityState state, out string reason) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) state = VoiceActivityState.NoData; reason = string.Empty; try { int num = default(int); ((FastBufferReader)(ref reader)).ReadValueSafe(ref num, default(ForPrimitives)); if (num != 1) { reason = $"unsupported protocol version {num}"; return false; } bool hasData = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref hasData, default(ForPrimitives)); bool isSpeaking = default(bool); ((FastBufferReader)(ref reader)).ReadValueSafe(ref isSpeaking, default(ForPrimitives)); ulong clientId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref clientId, default(ForPrimitives)); ulong slotId = default(ulong); ((FastBufferReader)(ref reader)).ReadValueSafe(ref slotId, default(ForPrimitives)); float amplitude = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref amplitude, default(ForPrimitives)); float volume = default(float); ((FastBufferReader)(ref reader)).ReadValueSafe(ref volume, default(ForPrimitives)); long timestampTicks = default(long); ((FastBufferReader)(ref reader)).ReadValueSafe(ref timestampTicks, default(ForPrimitives)); state = new VoiceActivityState(hasData, isSpeaking, amplitude, volume, clientId, slotId, timestampTicks); return true; } catch (Exception ex) { reason = "voice activity read failed: " + ex.GetType().Name; return false; } } } public sealed class ModPeerCapability { public ulong ClientId { get; } public int ProtocolVersion { get; } public bool SupportsCapabilityHandshake { get; } public bool SupportsSpectatorTargetSync { get; } public bool SupportsVoiceActivitySync { get; } public bool SupportsSpectatorVoiceToTarget { get; } public bool HandshakeComplete { get; } public long LastSeenTicks { get; } public ModPeerCapability(ulong clientId, int protocolVersion, bool supportsCapabilityHandshake, bool supportsSpectatorTargetSync, bool handshakeComplete, long lastSeenTicks, bool supportsVoiceActivitySync = false, bool supportsSpectatorVoiceToTarget = false) { ClientId = clientId; ProtocolVersion = protocolVersion; SupportsCapabilityHandshake = supportsCapabilityHandshake; SupportsSpectatorTargetSync = supportsSpectatorTargetSync; HandshakeComplete = handshakeComplete; LastSeenTicks = lastSeenTicks; SupportsVoiceActivitySync = supportsVoiceActivitySync; SupportsSpectatorVoiceToTarget = supportsSpectatorVoiceToTarget; } } public static class ModPeerCapabilityRules { public static bool SupportsCurrentSpectatorTargetSync(ModPeerCapability capability) { if (capability.ProtocolVersion == 1 && capability.SupportsCapabilityHandshake && capability.SupportsSpectatorTargetSync) { return capability.HandshakeComplete; } return false; } public static bool SupportsCurrentVoiceActivitySync(ModPeerCapability capability) { if (SupportsCurrentSpectatorTargetSync(capability)) { return capability.SupportsVoiceActivitySync; } return false; } public static bool SupportsCurrentSpectatorVoiceToTarget(ModPeerCapability capability) { if (SupportsCurrentSpectatorTargetSync(capability)) { return capability.SupportsSpectatorVoiceToTarget; } return false; } } public static class NetworkCompatibilityPolicy { public static NetworkLifecycleState ResolveLifecycleState(bool targetSyncReady, bool capabilitySent, float capabilityProbeSentRealtime, float currentRealtime, float noCompatiblePeerTimeoutSeconds) { if (targetSyncReady) { return NetworkLifecycleState.TransportRegistered; } if (capabilitySent && capabilityProbeSentRealtime >= 0f && currentRealtime - capabilityProbeSentRealtime >= noCompatiblePeerTimeoutSeconds) { return NetworkLifecycleState.NoCompatiblePeerLocalOnly; } return NetworkLifecycleState.TransportRegistered; } public static bool ShouldRunBusinessSync(NetworkLifecycleState lifecycleState, bool targetSyncReady) { if (targetSyncReady) { return lifecycleState == NetworkLifecycleState.TransportRegistered; } return false; } public static bool ShouldRetryCapabilityProbe(bool targetSyncReady, bool capabilitySent, float capabilityProbeSentRealtime, float currentRealtime, float retryIntervalSeconds) { if (!targetSyncReady && capabilitySent && capabilityProbeSentRealtime >= 0f && retryIntervalSeconds > 0f) { return currentRealtime - capabilityProbeSentRealtime >= retryIntervalSeconds; } return false; } } public sealed class NetworkingModule : IFeatureModule, IDisposable, IRuntimeTickable { private readonly IEnhancedSpectatorNetworkService _networkService; private bool _initialized; public NetworkingModule(IEnhancedSpectatorNetworkService networkService) { _networkService = networkService ?? throw new ArgumentNullException("networkService"); } public void Initialize() { if (!_initialized) { _networkService.Initialize(); _initialized = true; ModLog.Debug("Networking module initialized."); } } public void Tick() { if (_initialized) { _networkService.Tick(); } } public void Dispose() { if (_initialized) { _networkService.Dispose(); _initialized = false; ModLog.Debug("Networking module disposed."); } } } public enum NetworkLifecycleState { Unavailable, LocalOnly, TransportRegistered, NoCompatiblePeerLocalOnly, Disposed } public static class NetworkSyncSamplingRules { public static bool ShouldSample(bool hasObservedState, float now, float nextSampleTime) { if (hasObservedState) { return now >= nextSampleTime; } return true; } public static float ResolveNextSampleTime(float now, float intervalSeconds) { return now + Mathf.Max(0f, intervalSeconds); } public static float ResolveVoiceActivitySyncInterval(float configuredSeconds) { return Mathf.Max(0.066f, configuredSeconds); } public static bool ShouldRefreshUnchangedState(bool stateIsActive, bool hasSentState, float now, float nextRefreshTime) { if (stateIsActive && hasSentState) { return now >= nextRefreshTime; } return false; } } public sealed class PeerIdentityState { public ulong ClientId { get; } public ulong PlayerSlotId { get; } public string DisplayName { get; } public string VoicePlayerName { get; } public long TimestampTicks { get; } public PeerIdentityState(ulong clientId, ulong playerSlotId, string displayName, long timestampTicks) : this(clientId, playerSlotId, displayName, string.Empty, timestampTicks) { } public PeerIdentityState(ulong clientId, ulong playerSlotId, string displayName, string voicePlayerName, long timestampTicks) { ClientId = clientId; PlayerSlotId = playerSlotId; DisplayName = displayName; VoicePlayerName = voicePlayerName; TimestampTicks = timestampTicks; } } public sealed class RemotePeerIdentityRegistry { private readonly Dictionary _identities = new Dictionary(); public int Revision { get; private set; } public void Update(PeerIdentityState state) { bool flag = true; if (_identities.TryGetValue(state.ClientId, out PeerIdentityState value)) { if (state.TimestampTicks < value.TimestampTicks) { return; } if (string.IsNullOrWhiteSpace(state.VoicePlayerName) && !string.IsNullOrWhiteSpace(value.VoicePlayerName)) { state = new PeerIdentityState(state.ClientId, state.PlayerSlotId, state.DisplayName, value.VoicePlayerName, state.TimestampTicks); } flag = !IdentityEquals(value, state); } _identities[state.ClientId] = state; if (flag) { Revision++; } } public bool TryGet(ulong clientId, out PeerIdentityState state) { return _identities.TryGetValue(clientId, out state); } public List GetSnapshot() { return new List(_identities.Values); } public void CopySnapshotTo(List destination) { destination.Clear(); foreach (PeerIdentityState value in _identities.Values) { destination.Add(value); } } public void Remove(ulong clientId) { if (_identities.Remove(clientId)) { Revision++; } } public void Clear() { if (_identities.Count != 0) { _identities.Clear(); Revision++; } } private static bool IdentityEquals(PeerIdentityState left, PeerIdentityState right) { if (left.ClientId == right.ClientId && left.PlayerSlotId == right.PlayerSlotId && string.Equals(left.DisplayName, right.DisplayName, StringComparison.Ordinal)) { return string.Equals(left.VoicePlayerName, right.VoicePlayerName, StringComparison.Ordinal); } return false; } } public sealed class RemotePeerRegistry { private readonly Dictionary _capabilities = new Dictionary(); private readonly HashSet _relayedPeerIds = new HashSet(); public void RegisterLocal(ModPeerCapability capability) { _capabilities[capability.ClientId] = capability; _relayedPeerIds.Remove(capability.ClientId); } public bool RegisterRemote(ModPeerCapability capability) { return RegisterRemote(capability, isRelayed: false); } public bool RegisterRemote(ModPeerCapability capability, bool isRelayed) { ModPeerCapability modPeerCapability = NormalizeRemoteCapability(capability); _capabilities[modPeerCapability.ClientId] = modPeerCapability; if (isRelayed) { _relayedPeerIds.Add(modPeerCapability.ClientId); } else { _relayedPeerIds.Remove(modPeerCapability.ClientId); } return modPeerCapability.HandshakeComplete; } public bool TryGetCapability(ulong clientId, out ModPeerCapability capability) { return _capabilities.TryGetValue(clientId, out capability); } public List GetCapabilitiesSnapshot() { return new List(_capabilities.Values); } public List GetSpectatorTargetSyncPeerIds(ulong localClientId) { return GetSpectatorTargetSyncPeerIds(localClientId, includeRelayed: false); } public List GetSpectatorTargetSyncPeerIds(ulong localClientId, bool includeRelayed) { return GetPeerIds(localClientId, includeRelayed, ModPeerCapabilityRules.SupportsCurrentSpectatorTargetSync); } public List GetVoiceActivitySyncPeerIds(ulong localClientId) { return GetVoiceActivitySyncPeerIds(localClientId, includeRelayed: false); } public List GetVoiceActivitySyncPeerIds(ulong localClientId, bool includeRelayed) { return GetPeerIds(localClientId, includeRelayed, ModPeerCapabilityRules.SupportsCurrentVoiceActivitySync); } public bool HasSpectatorTargetSyncPeer(ulong localClientId) { foreach (KeyValuePair capability in _capabilities) { if (capability.Key != localClientId && ModPeerCapabilityRules.SupportsCurrentSpectatorTargetSync(capability.Value)) { return true; } } return false; } public bool IsSpectatorTargetSyncPeer(ulong clientId) { if (_capabilities.TryGetValue(clientId, out ModPeerCapability value)) { return ModPeerCapabilityRules.SupportsCurrentSpectatorTargetSync(value); } return false; } public bool IsRelayedPeer(ulong clientId) { return _relayedPeerIds.Contains(clientId); } public List GetRemotePeerIds(ulong localClientId) { List list = new List(); foreach (ulong key in _capabilities.Keys) { if (key != localClientId) { list.Add(key); } } return list; } public void Remove(ulong clientId) { _capabilities.Remove(clientId); _relayedPeerIds.Remove(clientId); } public void Clear() { _capabilities.Clear(); _relayedPeerIds.Clear(); } private static ModPeerCapability NormalizeRemoteCapability(ModPeerCapability capability) { bool handshakeComplete = capability.ProtocolVersion == 1 && capability.SupportsCapabilityHandshake; return new ModPeerCapability(capability.ClientId, capability.ProtocolVersion, capability.SupportsCapabilityHandshake, capability.SupportsSpectatorTargetSync, handshakeComplete, capability.LastSeenTicks, capability.SupportsVoiceActivitySync, capability.SupportsSpectatorVoiceToTarget); } private List GetPeerIds(ulong localClientId, bool includeRelayed, Func predicate) { List list = new List(); foreach (KeyValuePair capability in _capabilities) { if (capability.Key != localClientId && (includeRelayed || !_relayedPeerIds.Contains(capability.Key)) && predicate(capability.Value)) { list.Add(capability.Key); } } return list; } } public sealed class RemoteSpectatorPoseRegistry { private readonly Dictionary _poses = new Dictionary(); public void Update(SpectatorPoseState state) { if (!state.IsSpectating) { _poses.Remove(state.LocalClientId); } else { _poses[state.LocalClientId] = state; } } public bool TryGet(ulong clientId, out SpectatorPoseState state) { if (_poses.TryGetValue(clientId, out SpectatorPoseState value)) { state = value; return true; } state = null; return false; } public List GetSnapshot() { List list = new List(); foreach (SpectatorPoseState value in _poses.Values) { list.Add(value); } return list; } public void Remove(ulong clientId) { _poses.Remove(clientId); } public void Clear() { _poses.Clear(); } } public sealed class RemoteSpectatorTargetRegistry { private readonly Dictionary _targets = new Dictionary(); public int Revision { get; private set; } public void Update(SpectatorTargetState state) { SpectatorTargetState value; bool num = !_targets.TryGetValue(state.LocalClientId, out value) || !value.Equals(state); _targets[state.LocalClientId] = state; if (num) { Revision++; } } public bool TryGet(ulong clientId, out SpectatorTargetState state) { return _targets.TryGetValue(clientId, out state); } public List GetSnapshot() { return new List(_targets.Values); } public void CopySnapshotTo(List destination) { destination.Clear(); foreach (SpectatorTargetState value in _targets.Values) { destination.Add(value); } } public void Remove(ulong clientId) { if (_targets.Remove(clientId)) { Revision++; } } public void Clear() { if (_targets.Count != 0) { _targets.Clear(); Revision++; } } } public sealed class RemoteVoiceActivityRegistry { private sealed class Entry { public VoiceActivityState State { get; } public long ReceivedAtTicks { get; } public Entry(VoiceActivityState state, long receivedAtTicks) { State = state; ReceivedAtTicks = receivedAtTicks; } } private readonly Dictionary _states = new Dictionary(); public void Update(VoiceActivityState state) { Update(state, DateTime.UtcNow.Ticks); } public void Update(VoiceActivityState state, long receivedAtTicks) { if (!_states.TryGetValue(state.ClientId, out Entry value) || value.State.TimestampTicks <= state.TimestampTicks) { if (!state.HasData) { _states.Remove(state.ClientId); } else { _states[state.ClientId] = new Entry(state, receivedAtTicks); } } } public bool TryGet(ulong clientId, out VoiceActivityState state) { if (TryGet(clientId, out state, out var _)) { return true; } state = VoiceActivityState.NoData; return false; } public bool TryGet(ulong clientId, out VoiceActivityState state, out long receivedAtTicks) { if (_states.TryGetValue(clientId, out Entry value)) { state = value.State; receivedAtTicks = value.ReceivedAtTicks; return true; } state = VoiceActivityState.NoData; receivedAtTicks = 0L; return false; } public List GetSnapshot() { List list = new List(_states.Count); foreach (Entry value in _states.Values) { list.Add(value.State); } return list; } public void Remove(ulong clientId) { _states.Remove(clientId); } public void Clear() { _states.Clear(); } } public sealed class SpectatorPoseState { private const float PositionEpsilonSqr = 0.0004f; private const float RotationDotEpsilon = 0.9995f; public bool IsSpectating { get; } public ulong LocalClientId { get; } public ulong LocalPlayerSlotId { get; } public ulong? TargetClientId { get; } public ulong? TargetPlayerSlotId { get; } public Vector3 Position { get; } public Quaternion Rotation { get; } public long TimestampTicks { get; } public SpectatorPoseState(bool isSpectating, ulong localClientId, ulong localPlayerSlotId, ulong? targetClientId, ulong? targetPlayerSlotId, Vector3 position, Quaternion rotation, long timestampTicks) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) IsSpectating = isSpectating; LocalClientId = localClientId; LocalPlayerSlotId = localPlayerSlotId; TargetClientId = targetClientId; TargetPlayerSlotId = targetPlayerSlotId; Position = position; Rotation = rotation; TimestampTicks = timestampTicks; } public bool ApproximatelyEquals(SpectatorPoseState? other) { //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) if (other == null) { return false; } if (IsSpectating != other.IsSpectating || LocalClientId != other.LocalClientId || LocalPlayerSlotId != other.LocalPlayerSlotId || TargetClientId != other.TargetClientId || TargetPlayerSlotId != other.TargetPlayerSlotId) { return false; } Vector3 val = Position - other.Position; float sqrMagnitude = ((Vector3)(ref val)).sqrMagnitude; float num = Mathf.Abs(Quaternion.Dot(Rotation, other.Rotation)); if (sqrMagnitude <= 0.0004f) { return num >= 0.9995f; } return false; } } public sealed class SpectatorPoseSyncMessage { public int ProtocolVersion { get; } public SpectatorPoseState State { get; } public long TimestampTicks { get; } public SpectatorPoseSyncMessage(int protocolVersion, SpectatorPoseState state, long timestampTicks) { ProtocolVersion = protocolVersion; State = state; TimestampTicks = timestampTicks; } } public sealed class SpectatorTargetState : IEquatable { public bool IsSpectating { get; } public ulong LocalClientId { get; } public ulong LocalPlayerSlotId { get; } public ulong? TargetClientId { get; } public ulong? TargetPlayerSlotId { get; } public long TimestampTicks { get; } public SpectatorTargetState(bool isSpectating, ulong localClientId, ulong localPlayerSlotId, ulong? targetClientId, ulong? targetPlayerSlotId, long timestampTicks) { IsSpectating = isSpectating; LocalClientId = localClientId; LocalPlayerSlotId = localPlayerSlotId; TargetClientId = targetClientId; TargetPlayerSlotId = targetPlayerSlotId; TimestampTicks = timestampTicks; } public bool Equals(SpectatorTargetState? other) { if (other != null && IsSpectating == other.IsSpectating && LocalClientId == other.LocalClientId && LocalPlayerSlotId == other.LocalPlayerSlotId && TargetClientId == other.TargetClientId) { return TargetPlayerSlotId == other.TargetPlayerSlotId; } return false; } public override bool Equals(object? obj) { return Equals(obj as SpectatorTargetState); } public override int GetHashCode() { return ((((((((IsSpectating ? 17 : 31) * 397) ^ LocalClientId.GetHashCode()) * 397) ^ LocalPlayerSlotId.GetHashCode()) * 397) ^ TargetClientId.GetHashCode()) * 397) ^ TargetPlayerSlotId.GetHashCode(); } } public sealed class SpectatorTargetSyncMessage { public int ProtocolVersion { get; } public SpectatorTargetState State { get; } public long MessageTicks { get; } public SpectatorTargetSyncMessage(int protocolVersion, SpectatorTargetState state, long messageTicks) { ProtocolVersion = protocolVersion; State = state ?? throw new ArgumentNullException("state"); MessageTicks = messageTicks; } } public sealed class UnityNetcodeMessagingTransport : IModNetworkTransport, IDisposable { private readonly Func _debugEnabled; private NetworkManager? _networkManager; private CustomMessagingManager? _customMessagingManager; private Action? _capabilityReceived; private Action? _spectatorTargetReceived; private Action? _spectatorPoseReceived; private Action? _peerIdentityReceived; private Action? _voiceActivityReceived; private bool _registered; public bool IsRegistered => _registered; public bool IsNetworkAvailable { get { NetworkManager singleton = NetworkManager.Singleton; if (RuntimeConnectionState.CanUseModNetworking(out string _) && (Object)(object)singleton != (Object)null && singleton.CustomMessagingManager != null && (!_registered || _networkManager == singleton)) { if (_registered) { return _customMessagingManager == singleton.CustomMessagingManager; } return true; } return false; } } public bool IsHost { get { if ((Object)(object)NetworkManager.Singleton != (Object)null) { return NetworkManager.Singleton.IsHost; } return false; } } public ulong LocalClientId { get { if (!((Object)(object)NetworkManager.Singleton != (Object)null)) { return 0uL; } return NetworkManager.Singleton.LocalClientId; } } public ulong ServerClientId => 0uL; public UnityNetcodeMessagingTransport(Func debugEnabled) { _debugEnabled = debugEnabled ?? throw new ArgumentNullException("debugEnabled"); } public bool IsPeerConnected(ulong clientId) { NetworkManager val = _networkManager ?? NetworkManager.Singleton; if (!RuntimeConnectionState.CanUseModNetworking(out string _) || (Object)(object)val == (Object)null) { return false; } if (_registered && val != NetworkManager.Singleton) { return false; } if (clientId == val.LocalClientId) { return true; } if (!val.IsHost) { return clientId == 0; } foreach (ulong connectedClientsId in val.ConnectedClientsIds) { if (connectedClientsId == clientId) { return true; } } return false; } public bool TryRegister(Action capabilityReceived, Action spectatorTargetReceived, Action spectatorPoseReceived, Action peerIdentityReceived, Action voiceActivityReceived, out string reason) { //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Expected O, but got Unknown //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Expected O, but got Unknown //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Expected O, but got Unknown //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Expected O, but got Unknown reason = string.Empty; if (_registered) { return true; } if (!RuntimeConnectionState.CanUseModNetworking(out reason)) { return false; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { reason = "NetworkManager unavailable"; return false; } if (!singleton.IsClient && !singleton.IsHost) { reason = "not connected as client or host"; return false; } CustomMessagingManager customMessagingManager = singleton.CustomMessagingManager; if (customMessagingManager == null) { reason = "CustomMessagingManager unavailable"; return false; } _networkManager = singleton; _customMessagingManager = customMessagingManager; _capabilityReceived = capabilityReceived ?? throw new ArgumentNullException("capabilityReceived"); _spectatorTargetReceived = spectatorTargetReceived ?? throw new ArgumentNullException("spectatorTargetReceived"); _spectatorPoseReceived = spectatorPoseReceived ?? throw new ArgumentNullException("spectatorPoseReceived"); _peerIdentityReceived = peerIdentityReceived ?? throw new ArgumentNullException("peerIdentityReceived"); _voiceActivityReceived = voiceActivityReceived ?? throw new ArgumentNullException("voiceActivityReceived"); try { customMessagingManager.RegisterNamedMessageHandler("EnhancedSpectator.Capability.V1", new HandleNamedMessageDelegate(HandleCapabilityMessage)); customMessagingManager.RegisterNamedMessageHandler("EnhancedSpectator.SpectatorTarget.V1", new HandleNamedMessageDelegate(HandleSpectatorTargetMessage)); customMessagingManager.RegisterNamedMessageHandler("EnhancedSpectator.SpectatorPose.V1", new HandleNamedMessageDelegate(HandleSpectatorPoseMessage)); customMessagingManager.RegisterNamedMessageHandler("EnhancedSpectator.PeerIdentity.V1", new HandleNamedMessageDelegate(HandlePeerIdentityMessage)); customMessagingManager.RegisterNamedMessageHandler("EnhancedSpectator.VoiceActivity.V1", new HandleNamedMessageDelegate(HandleVoiceActivityMessage)); _registered = true; Debug("Registered Enhanced Spectator named message handlers."); return true; } catch (Exception ex) { reason = "handler registration failed: " + ex.GetType().Name; Unregister(); return false; } } public void Unregister() { CustomMessagingManager customMessagingManager = _customMessagingManager; if (customMessagingManager != null) { try { customMessagingManager.UnregisterNamedMessageHandler("EnhancedSpectator.Capability.V1"); customMessagingManager.UnregisterNamedMessageHandler("EnhancedSpectator.SpectatorTarget.V1"); customMessagingManager.UnregisterNamedMessageHandler("EnhancedSpectator.SpectatorPose.V1"); customMessagingManager.UnregisterNamedMessageHandler("EnhancedSpectator.PeerIdentity.V1"); customMessagingManager.UnregisterNamedMessageHandler("EnhancedSpectator.VoiceActivity.V1"); Debug("Unregistered Enhanced Spectator named message handlers."); } catch (Exception ex) { Debug("Named message handler unregister failed: " + ex.GetType().Name + "."); } } _registered = false; _networkManager = null; _customMessagingManager = null; _capabilityReceived = null; _spectatorTargetReceived = null; _spectatorPoseReceived = null; _peerIdentityReceived = null; _voiceActivityReceived = null; } public bool SendCapability(ModPeerCapability capability, IEnumerable? recipients, out string reason) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out reason)) { return false; } FastBufferWriter writer = default(FastBufferWriter); ((FastBufferWriter)(ref writer))..ctor(ModNetworkSerializer.CapabilityMessageSize, (Allocator)2, -1); try { ModNetworkSerializer.WriteCapability(ref writer, capability); if (recipients == null) { if (manager.IsHost) { customMessagingManager.SendNamedMessageToAll("EnhancedSpectator.Capability.V1", writer, (NetworkDelivery)3); } else { customMessagingManager.SendNamedMessage("EnhancedSpectator.Capability.V1", 0uL, writer, (NetworkDelivery)3); } return true; } bool flag = false; foreach (ulong recipient in recipients) { if (recipient != manager.LocalClientId) { customMessagingManager.SendNamedMessage("EnhancedSpectator.Capability.V1", recipient, writer, (NetworkDelivery)3); flag = true; } } if (!flag) { reason = "no capability recipients"; } return flag; } catch (Exception ex) { reason = "capability send failed: " + ex.GetType().Name; return false; } finally { ((FastBufferWriter)(ref writer)).Dispose(); } } public bool SendSpectatorTarget(SpectatorTargetSyncMessage message, IEnumerable recipients, out string reason) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out reason)) { return false; } FastBufferWriter writer = default(FastBufferWriter); ((FastBufferWriter)(ref writer))..ctor(ModNetworkSerializer.SpectatorTargetMessageSize, (Allocator)2, -1); try { ModNetworkSerializer.WriteSpectatorTarget(ref writer, message); bool flag = false; foreach (ulong recipient in recipients) { if (recipient != manager.LocalClientId) { customMessagingManager.SendNamedMessage("EnhancedSpectator.SpectatorTarget.V1", recipient, writer, (NetworkDelivery)3); flag = true; } } if (!flag) { reason = "no spectator target recipients"; } return flag; } catch (Exception ex) { reason = "spectator target send failed: " + ex.GetType().Name; return false; } finally { ((FastBufferWriter)(ref writer)).Dispose(); } } public bool SendSpectatorPose(SpectatorPoseSyncMessage message, IEnumerable recipients, out string reason) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out reason)) { return false; } FastBufferWriter writer = default(FastBufferWriter); ((FastBufferWriter)(ref writer))..ctor(ModNetworkSerializer.SpectatorPoseMessageSize, (Allocator)2, -1); try { ModNetworkSerializer.WriteSpectatorPose(ref writer, message); bool flag = false; foreach (ulong recipient in recipients) { if (recipient != manager.LocalClientId) { customMessagingManager.SendNamedMessage("EnhancedSpectator.SpectatorPose.V1", recipient, writer, (NetworkDelivery)1); flag = true; } } if (!flag) { reason = "no spectator pose recipients"; } return flag; } catch (Exception ex) { reason = "spectator pose send failed: " + ex.GetType().Name; return false; } finally { ((FastBufferWriter)(ref writer)).Dispose(); } } public bool SendPeerIdentity(PeerIdentityState state, IEnumerable? recipients, out string reason) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out reason)) { return false; } FastBufferWriter writer = default(FastBufferWriter); ((FastBufferWriter)(ref writer))..ctor(ModNetworkSerializer.PeerIdentityMessageSize, (Allocator)2, -1); try { ModNetworkSerializer.WritePeerIdentity(ref writer, state); if (recipients == null) { if (manager.IsHost) { customMessagingManager.SendNamedMessageToAll("EnhancedSpectator.PeerIdentity.V1", writer, (NetworkDelivery)3); } else { customMessagingManager.SendNamedMessage("EnhancedSpectator.PeerIdentity.V1", 0uL, writer, (NetworkDelivery)3); } return true; } bool flag = false; foreach (ulong recipient in recipients) { if (recipient != manager.LocalClientId) { customMessagingManager.SendNamedMessage("EnhancedSpectator.PeerIdentity.V1", recipient, writer, (NetworkDelivery)3); flag = true; } } if (!flag) { reason = "no peer identity recipients"; } return flag; } catch (Exception ex) { reason = "peer identity send failed: " + ex.GetType().Name; return false; } finally { ((FastBufferWriter)(ref writer)).Dispose(); } } public bool SendVoiceActivity(VoiceActivitySyncMessage message, IEnumerable recipients, out string reason) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out reason)) { return false; } FastBufferWriter writer = default(FastBufferWriter); ((FastBufferWriter)(ref writer))..ctor(ModNetworkSerializer.VoiceActivityMessageSize, (Allocator)2, -1); try { ModNetworkSerializer.WriteVoiceActivity(ref writer, message); bool flag = false; foreach (ulong recipient in recipients) { if (recipient != manager.LocalClientId) { customMessagingManager.SendNamedMessage("EnhancedSpectator.VoiceActivity.V1", recipient, writer, (NetworkDelivery)1); flag = true; } } if (!flag) { reason = "no voice activity recipients"; } return flag; } catch (Exception ex) { reason = "voice activity send failed: " + ex.GetType().Name; return false; } finally { ((FastBufferWriter)(ref writer)).Dispose(); } } public void Dispose() { Unregister(); } private bool TryGetMessagingManager(out NetworkManager manager, out CustomMessagingManager customMessagingManager, out string reason) { manager = null; customMessagingManager = null; reason = string.Empty; if (!RuntimeConnectionState.CanUseModNetworking(out reason)) { return false; } if (!_registered || (Object)(object)_networkManager == (Object)null || _customMessagingManager == null) { reason = "transport is not registered"; return false; } if (_networkManager != NetworkManager.Singleton) { reason = "NetworkManager instance changed"; return false; } if (_customMessagingManager != _networkManager.CustomMessagingManager) { reason = "CustomMessagingManager instance changed"; return false; } if (!_networkManager.IsClient && !_networkManager.IsHost) { reason = "network is no longer connected"; return false; } manager = _networkManager; customMessagingManager = _customMessagingManager; return true; } private void HandleCapabilityMessage(ulong senderClientId, FastBufferReader messagePayload) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { Debug($"Dropped capability message from {senderClientId}: {reason}."); return; } FastBufferReader reader = messagePayload; if (!ModNetworkSerializer.TryReadCapability(ref reader, out ModPeerCapability capability, out string reason2)) { Debug($"Dropped capability message from {senderClientId}: {reason2}."); } else { _capabilityReceived?.Invoke(senderClientId, capability); } } catch (Exception ex) { Debug($"Capability handler failed for sender {senderClientId}: {ex.GetType().Name}."); } } private void HandleSpectatorTargetMessage(ulong senderClientId, FastBufferReader messagePayload) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { Debug($"Dropped spectator target message from {senderClientId}: {reason}."); return; } FastBufferReader reader = messagePayload; if (!ModNetworkSerializer.TryReadSpectatorTarget(ref reader, out SpectatorTargetState state, out string reason2)) { Debug($"Dropped spectator target message from {senderClientId}: {reason2}."); } else { _spectatorTargetReceived?.Invoke(senderClientId, state); } } catch (Exception ex) { Debug($"Spectator target handler failed for sender {senderClientId}: {ex.GetType().Name}."); } } private void HandleSpectatorPoseMessage(ulong senderClientId, FastBufferReader messagePayload) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { Debug($"Dropped spectator pose message from {senderClientId}: {reason}."); return; } FastBufferReader reader = messagePayload; if (!ModNetworkSerializer.TryReadSpectatorPose(ref reader, out SpectatorPoseState state, out string reason2)) { Debug($"Dropped spectator pose message from {senderClientId}: {reason2}."); } else { _spectatorPoseReceived?.Invoke(senderClientId, state); } } catch (Exception ex) { Debug($"Spectator pose handler failed for sender {senderClientId}: {ex.GetType().Name}."); } } private void HandlePeerIdentityMessage(ulong senderClientId, FastBufferReader messagePayload) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { Debug($"Dropped peer identity message from {senderClientId}: {reason}."); return; } FastBufferReader reader = messagePayload; if (!ModNetworkSerializer.TryReadPeerIdentity(ref reader, out PeerIdentityState state, out string reason2)) { Debug($"Dropped peer identity message from {senderClientId}: {reason2}."); } else { _peerIdentityReceived?.Invoke(senderClientId, state); } } catch (Exception ex) { Debug($"Peer identity handler failed for sender {senderClientId}: {ex.GetType().Name}."); } } private void HandleVoiceActivityMessage(ulong senderClientId, FastBufferReader messagePayload) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { Debug($"Dropped voice activity message from {senderClientId}: {reason}."); return; } FastBufferReader reader = messagePayload; if (!ModNetworkSerializer.TryReadVoiceActivity(ref reader, out VoiceActivityState state, out string reason2)) { Debug($"Dropped voice activity message from {senderClientId}: {reason2}."); } else { _voiceActivityReceived?.Invoke(senderClientId, state); } } catch (Exception ex) { Debug($"Voice activity handler failed for sender {senderClientId}: {ex.GetType().Name}."); } } private void Debug(string message) { if (_debugEnabled()) { ModLog.Debug(message); } } } public sealed class VoiceActivityDebugLimiter { private readonly struct DebugKey : IEquatable { public string Category { get; } public ulong PeerId { get; } public DebugKey(string category, ulong peerId) { Category = category ?? string.Empty; PeerId = peerId; } public bool Equals(DebugKey other) { if (PeerId == other.PeerId) { return string.Equals(Category, other.Category, StringComparison.Ordinal); } return false; } public override bool Equals(object? obj) { if (obj is DebugKey other) { return Equals(other); } return false; } public override int GetHashCode() { return (((Category != null) ? Category.GetHashCode() : 0) * 397) ^ PeerId.GetHashCode(); } } private readonly struct LastDebugState { public int Frame { get; } public bool HasData { get; } public bool IsSpeaking { get; } public float Amplitude { get; } public float Volume { get; } public bool IsRelayed { get; } public LastDebugState(int frame, VoiceActivityState state, bool isRelayed) { Frame = frame; HasData = state.HasData; IsSpeaking = state.IsSpeaking; Amplitude = state.Amplitude; Volume = state.Volume; IsRelayed = isRelayed; } public bool IsMeaningfullyDifferent(LastDebugState other, int minimumIntervalFrames) { if (other.Frame - Frame < minimumIntervalFrames && HasData == other.HasData && IsSpeaking == other.IsSpeaking && IsRelayed == other.IsRelayed && !(MathF.Abs(Amplitude - other.Amplitude) >= 0.2f)) { return MathF.Abs(Volume - other.Volume) >= 0.2f; } return true; } } private const float AmplitudeDeltaThreshold = 0.2f; private const float VolumeDeltaThreshold = 0.2f; private readonly Dictionary _lastStates = new Dictionary(); private readonly int _minimumIntervalFrames; public VoiceActivityDebugLimiter(int minimumIntervalFrames = 180) { _minimumIntervalFrames = Math.Max(1, minimumIntervalFrames); } public bool ShouldLog(string category, ulong peerId, int frame, VoiceActivityState state, bool isRelayed) { DebugKey key = new DebugKey(category, peerId); LastDebugState lastDebugState = new LastDebugState(frame, state, isRelayed); if (!_lastStates.TryGetValue(key, out var value) || value.IsMeaningfullyDifferent(lastDebugState, _minimumIntervalFrames)) { _lastStates[key] = lastDebugState; return true; } return false; } public void Clear() { _lastStates.Clear(); } } public sealed class VoiceActivitySyncMessage { public int ProtocolVersion { get; } public VoiceActivityState State { get; } public long TimestampTicks { get; } public VoiceActivitySyncMessage(int protocolVersion, VoiceActivityState state, long timestampTicks) { ProtocolVersion = protocolVersion; State = state; TimestampTicks = timestampTicks; } } public static class VoiceActivitySyncRules { private const float AmplitudeEpsilon = 0.02f; private const float VolumeEpsilon = 0.02f; public static bool ApproximatelyEquals(VoiceActivityState? left, VoiceActivityState? right) { if (left == right) { return true; } if (left == null || right == null) { return false; } if (left.HasData == right.HasData && left.IsSpeaking == right.IsSpeaking && left.ClientId == right.ClientId && left.SlotId == right.SlotId && Math.Abs(left.Amplitude - right.Amplitude) <= 0.02f) { return Math.Abs(left.Volume - right.Volume) <= 0.02f; } return false; } public static bool IsFresh(long receivedAtTicks, long nowTicks, long staleTicks) { if (receivedAtTicks <= 0) { return false; } if (staleTicks <= 0) { return true; } long num = nowTicks - receivedAtTicks; if (num >= 0) { return num <= staleTicks; } return false; } } } namespace EnhancedSpectator.Logging { public static class ModLog { private static ManualLogSource? _source; private static bool _debugEnabled; public static bool IsDebugEnabled => _debugEnabled; private static ManualLogSource Source => _source ?? throw new InvalidOperationException("ModLog has not been initialized."); public static void Initialize(ManualLogSource source) { _source = source; } public static void SetDebugEnabled(bool enabled) { _debugEnabled = enabled; } public static void Debug(string message) { if (_debugEnabled) { Source.LogInfo((object)("[Debug] " + message)); } } public static void Info(string message) { Source.LogInfo((object)message); } public static void Warning(string message) { Source.LogWarning((object)message); } public static void Error(string message) { Source.LogError((object)message); } } public static class PluginDiagnostics { public static void LogPluginBinaryHash(string? pluginPath) { if (string.IsNullOrWhiteSpace(pluginPath)) { ModLog.Debug("Plugin DLL path unavailable; cannot compute SHA256."); return; } try { if (!File.Exists(pluginPath)) { ModLog.Debug("Plugin DLL path does not exist; cannot compute SHA256: " + pluginPath); return; } using FileStream inputStream = File.OpenRead(pluginPath); using SHA256 sHA = SHA256.Create(); byte[] bytes = sHA.ComputeHash(inputStream); ModLog.Debug("Plugin binary SHA256: " + ToHex(bytes) + "; path=" + pluginPath); } catch (Exception ex) { ModLog.Debug("Plugin DLL SHA256 calculation failed: " + ex.GetType().Name + "."); } } private static string ToHex(byte[] bytes) { char[] array = new char[bytes.Length * 2]; for (int i = 0; i < bytes.Length; i++) { byte b = bytes[i]; array[i * 2] = GetHexValue(b >> 4); array[i * 2 + 1] = GetHexValue(b & 0xF); } return new string(array); } private static char GetHexValue(int value) { return (char)((value < 10) ? (value + 48) : (value - 10 + 65)); } } } namespace EnhancedSpectator.GameInterop { public static class ConnectedPlayerStateRepairRules { public static bool ShouldRestoreConnectedAliveControl(bool isLocalPlayer, bool isCurrentlyConnected, bool isPlayerControlled, bool isPlayerDead) { if (!isLocalPlayer && isCurrentlyConnected && !isPlayerControlled) { return !isPlayerDead; } return false; } public static bool ShouldClearDisconnectedMidGame(bool isLocalPlayer, bool isCurrentlyConnected, bool disconnectedMidGame) { return !isLocalPlayer && isCurrentlyConnected && disconnectedMidGame; } public static bool ShouldUseVanillaFallbackDisplayName(bool updatePlayerNames, bool hasModIdentityDisplayName, string? currentDisplayName) { if (updatePlayerNames && !hasModIdentityDisplayName && !string.IsNullOrWhiteSpace(currentDisplayName)) { return PlayerDisplayNameRules.IsGenericPlayerNumber(currentDisplayName); } return false; } } public static class GameInteropNotes { public const string Summary = "Game interop is adapter-only and uses confirmed Lethal Company members through direct publicized access."; } public readonly struct GameSpectatorSnapshot { public static GameSpectatorSnapshot Unavailable { get; } = new GameSpectatorSnapshot(hasRound: false, hasLocalPlayer: false, isLocalPlayerDead: false, hasBegunSpectating: false, hasSpectatedTarget: false, isGameOverOverride: false, isSpectateCameraActive: false, null, null, null, null, null, null); public bool HasRound { get; } public bool HasLocalPlayer { get; } public bool IsLocalPlayerDead { get; } public bool HasBegunSpectating { get; } public bool HasSpectatedTarget { get; } public bool IsGameOverOverride { get; } public bool IsSpectateCameraActive { get; } public Camera? SpectateCamera { get; } public Transform? Anchor { get; } public ulong? LocalPlayerSlotId { get; } public ulong? LocalPlayerActualClientId { get; } public ulong? SpectatedPlayerSlotId { get; } public ulong? SpectatedPlayerActualClientId { get; } public GameSpectatorSnapshot(bool hasRound, bool hasLocalPlayer, bool isLocalPlayerDead, bool hasBegunSpectating, bool hasSpectatedTarget, bool isGameOverOverride, bool isSpectateCameraActive, Camera? spectateCamera, Transform? anchor, ulong? localPlayerSlotId, ulong? localPlayerActualClientId, ulong? spectatedPlayerSlotId, ulong? spectatedPlayerActualClientId) { HasRound = hasRound; HasLocalPlayer = hasLocalPlayer; IsLocalPlayerDead = isLocalPlayerDead; HasBegunSpectating = hasBegunSpectating; HasSpectatedTarget = hasSpectatedTarget; IsGameOverOverride = isGameOverOverride; IsSpectateCameraActive = isSpectateCameraActive; SpectateCamera = spectateCamera; Anchor = anchor; LocalPlayerSlotId = localPlayerSlotId; LocalPlayerActualClientId = localPlayerActualClientId; SpectatedPlayerSlotId = spectatedPlayerSlotId; SpectatedPlayerActualClientId = spectatedPlayerActualClientId; } } public interface IConnectedPlayerStateRepairAdapter { int RepairConnectedPlayerState(IReadOnlyList peerIdentities, IReadOnlyList remoteSpectatorTargets, bool updatePlayerNames, bool updateQuickMenu, bool debug, out string summary); } public interface IGameDeadBodyHeadSourceInspectionAdapter { bool TryGetDeadBodyHeadSourceInspectionSnapshots(int maxTransformDepth, bool includeRendererBounds, bool includeMaterials, out IReadOnlyList snapshots); } public interface IGameDetachedHeadVisualSourceAdapter { bool TryGetDetachedHeadVisualTemplate(out Transform? source); } public interface IGamePlayerModelInspectionAdapter { bool TryGetPlayerModelInspectionSnapshots(bool includeLocalPlayer, bool includeRemotePlayers, int maxTransformDepth, bool includeRendererBounds, bool includeMaterials, out IReadOnlyList snapshots); } public interface IGameSpectatorAdapter { SpectatorState ReadSpectatorState(); bool TryGetLocalSpectatorSnapshot(out GameSpectatorSnapshot snapshot); bool IsLocalPlayerSpectating(); bool TryGetSpectatedPlayerAnchor(out Transform? anchor); bool TryGetSpectateCamera(out Camera? camera); bool TryGetActiveCamera(out Camera? camera); bool IsGameOverSpectateOverrideActive(); bool IsLocalQuickMenuOpen(); bool IsValidSpectateTarget(object? target); bool TryGetSpectatedPlayerId(out ulong slotId, out ulong actualClientId); bool TryGetLocalPlayerIdentity(out ulong clientId, out ulong slotId); bool TryGetPlayerDisplayName(ulong clientId, ulong slotId, out string displayName); bool TryGetLocalVoicePlayerName(out string voicePlayerName); bool TryGetLocalPlayerHeadPoint(out Transform? anchor); bool TryGetLocalPlayerHeadAnchor(out Transform? anchor); bool TryGetLocalPlayerHeadAnchorPosition(out Vector3 position); } internal sealed class NoopGameSpectatorAdapter : IGameSpectatorAdapter { public static NoopGameSpectatorAdapter Instance { get; } = new NoopGameSpectatorAdapter(); private NoopGameSpectatorAdapter() { } public SpectatorState ReadSpectatorState() { return SpectatorState.Unavailable; } public bool TryGetLocalSpectatorSnapshot(out GameSpectatorSnapshot snapshot) { snapshot = GameSpectatorSnapshot.Unavailable; return false; } public bool IsLocalPlayerSpectating() { return false; } public bool TryGetSpectatedPlayerAnchor(out Transform? anchor) { anchor = null; return false; } public bool TryGetSpectateCamera(out Camera? camera) { camera = null; return false; } public bool TryGetActiveCamera(out Camera? camera) { camera = null; return false; } public bool IsGameOverSpectateOverrideActive() { return false; } public bool IsLocalQuickMenuOpen() { return false; } public bool IsValidSpectateTarget(object? target) { return false; } public bool TryGetSpectatedPlayerId(out ulong slotId, out ulong actualClientId) { slotId = 0uL; actualClientId = 0uL; return false; } public bool TryGetLocalPlayerIdentity(out ulong clientId, out ulong slotId) { clientId = 0uL; slotId = 0uL; return false; } public bool TryGetPlayerDisplayName(ulong clientId, ulong slotId, out string displayName) { displayName = string.Empty; return false; } public bool TryGetLocalVoicePlayerName(out string voicePlayerName) { voicePlayerName = string.Empty; return false; } public bool TryGetLocalPlayerHeadPoint(out Transform? anchor) { anchor = null; return false; } public bool TryGetLocalPlayerHeadAnchor(out Transform? anchor) { anchor = null; return false; } public bool TryGetLocalPlayerHeadAnchorPosition(out Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) position = Vector3.zero; return false; } } public interface IGameSpectatorTargetSwitchAdapter { bool TryGetDisconnectTargetSwitchContext(out SpectatorDisconnectTargetSwitchContext context); bool TrySwitchToNextSpectatorTarget(); } internal sealed class NoopGameSpectatorTargetSwitchAdapter : IGameSpectatorTargetSwitchAdapter { public static NoopGameSpectatorTargetSwitchAdapter Instance { get; } = new NoopGameSpectatorTargetSwitchAdapter(); private NoopGameSpectatorTargetSwitchAdapter() { } public bool TryGetDisconnectTargetSwitchContext(out SpectatorDisconnectTargetSwitchContext context) { context = default(SpectatorDisconnectTargetSwitchContext); return false; } public bool TrySwitchToNextSpectatorTarget() { return false; } } public interface IGameSpectatorVoiceRoutingAdapter { bool TryGetLocalVoiceReceiverState(out bool hasLocalPlayer, out bool isLocalPlayerDead, out ulong localClientId, out ulong localPlayerSlotId); bool TryApplySpectatorVoiceRoute(ulong spectatorClientId, ulong spectatorSlotId, SpectatorPoseState? poseState, SpectatorVoicePlaybackSettings settings, out string reason); void ClearSpectatorVoiceRoute(ulong spectatorClientId, ulong spectatorSlotId); void ClearCachedVoiceRouteLookups(); } public interface IGameVoiceActivityAdapter { bool TryGetVoiceActivity(ulong clientId, ulong slotId, out VoiceActivityState state); } public interface IGameVoiceDiagnosticsAdapter { bool TryGetVoiceDiagnosticsSnapshot(bool includeLocalPlayer, bool includeRemotePlayers, bool includeAudioSourceDiagnostics, bool includeWalkieDiagnostics, out VoiceDiagnosticsSnapshot snapshot); } public sealed class LethalCompanyConnectedPlayerStateRepairAdapter : IConnectedPlayerStateRepairAdapter { public int RepairConnectedPlayerState(IReadOnlyList peerIdentities, IReadOnlyList remoteSpectatorTargets, bool updatePlayerNames, bool updateQuickMenu, bool debug, out string summary) { summary = string.Empty; StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.ClientPlayerList == null || instance.allPlayerScripts == null) { summary = "StartOfRound player state unavailable"; return 0; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { summary = "NetworkManager unavailable"; return 0; } ulong localClientId = singleton.LocalClientId; QuickMenuManager quickMenuManager = Object.FindObjectOfType(); bool[] array = new bool[instance.allPlayerScripts.Length]; int num = 0; if (peerIdentities != null) { for (int i = 0; i < peerIdentities.Count; i++) { PeerIdentityState peerIdentityState = peerIdentities[i]; if (peerIdentityState.ClientId == localClientId) { continue; } if (!IsClientCurrentlyConnected(singleton, peerIdentityState.ClientId)) { if (debug) { ModLog.Debug($"Skipping connected-player repair for client={peerIdentityState.ClientId}; client is not currently connected."); } } else { if (!instance.ClientPlayerList.TryGetValue(peerIdentityState.ClientId, out var value) || value < 0 || value >= instance.allPlayerScripts.Length) { continue; } if (peerIdentityState.PlayerSlotId != (ulong)value) { if (debug) { ModLog.Debug($"Skipping connected-player repair for client={peerIdentityState.ClientId}; identity slot={peerIdentityState.PlayerSlotId}, vanilla slot={value}."); } continue; } PlayerControllerB val = instance.allPlayerScripts[value]; if (!((Object)(object)val == (Object)null)) { array[value] = true; if (RepairOneConnectedPlayerSlot(instance, singleton, quickMenuManager, val, value, peerIdentityState.ClientId, peerIdentityState, remoteSpectatorTargets, updatePlayerNames, updateQuickMenu, debug)) { num++; } } } } } foreach (KeyValuePair clientPlayer in instance.ClientPlayerList) { ulong key = clientPlayer.Key; int value2 = clientPlayer.Value; if (key != localClientId && value2 >= 0 && value2 < instance.allPlayerScripts.Length && !array[value2] && IsClientCurrentlyConnected(singleton, key)) { PlayerControllerB val2 = instance.allPlayerScripts[value2]; if (!((Object)(object)val2 == (Object)null) && RepairOneConnectedPlayerSlot(instance, singleton, quickMenuManager, val2, value2, key, null, remoteSpectatorTargets, updatePlayerNames, updateQuickMenu, debug)) { num++; } } } if (num > 0) { summary = $"repaired {num} connected player slot(s)"; } return num; } private static bool RepairOneConnectedPlayerSlot(StartOfRound round, NetworkManager networkManager, QuickMenuManager? quickMenuManager, PlayerControllerB player, int slot, ulong clientId, PeerIdentityState? identity, IReadOnlyList? remoteSpectatorTargets, bool updatePlayerNames, bool updateQuickMenu, bool debug) { SpectatorTargetState spectatorTargetState; bool flag = TryGetRemoteSpectatorTarget(remoteSpectatorTargets, clientId, out spectatorTargetState); bool flag2 = flag && spectatorTargetState != null && spectatorTargetState.IsSpectating; bool flag3 = false; if (flag2) { if (!player.isPlayerDead) { player.isPlayerDead = true; flag3 = true; } if (player.isPlayerControlled) { player.isPlayerControlled = false; flag3 = true; } } else if (flag && player.isPlayerDead) { player.isPlayerDead = false; if (!player.isPlayerControlled) { player.isPlayerControlled = true; } flag3 = true; } else if (ConnectedPlayerStateRepairRules.ShouldRestoreConnectedAliveControl(clientId == networkManager.LocalClientId, isCurrentlyConnected: true, player.isPlayerControlled, player.isPlayerDead)) { player.isPlayerControlled = true; flag3 = true; } if (ConnectedPlayerStateRepairRules.ShouldClearDisconnectedMidGame(clientId == networkManager.LocalClientId, isCurrentlyConnected: true, player.disconnectedMidGame)) { player.disconnectedMidGame = false; flag3 = true; } if (!flag2 && RepairAliveConnectedPlayerVisualState(round, slot, player)) { flag3 = true; } if (player.actualClientId != clientId) { player.actualClientId = clientId; flag3 = true; } if (player.playerClientId != (ulong)slot) { player.playerClientId = (ulong)slot; flag3 = true; } string displayName = string.Empty; bool flag4 = identity != null && PlayerDisplayNameRules.TryNormalize(identity.DisplayName, out displayName); string displayName2; ulong steamId; if (updatePlayerNames && flag4) { if (!StringComparer.Ordinal.Equals(player.playerUsername, displayName)) { player.playerUsername = displayName; flag3 = true; } } else if (ConnectedPlayerStateRepairRules.ShouldUseVanillaFallbackDisplayName(updatePlayerNames, flag4, player.playerUsername) && TryResolveVanillaFallbackDisplayName(slot, player, out displayName2, out steamId)) { if (!StringComparer.Ordinal.Equals(player.playerUsername, displayName2)) { player.playerUsername = displayName2; flag3 = true; } displayName = displayName2; if (steamId != 0L && player.playerSteamId != steamId) { player.playerSteamId = steamId; flag3 = true; } } if (updateQuickMenu && (Object)(object)quickMenuManager != (Object)null) { string text = ((!string.IsNullOrWhiteSpace(displayName)) ? displayName : player.playerUsername); quickMenuManager.AddUserToPlayerList(player.playerSteamId, text, slot); } if (flag3 && debug) { string text2 = ((identity != null) ? "mod-identity" : "vanilla-client-list"); ModLog.Debug($"Repaired vanilla connected player slot: source={text2}, client={clientId}, slot={slot}, name={player.playerUsername}, controlled={player.isPlayerControlled}, dead={player.isPlayerDead}, disconnected={player.disconnectedMidGame}, setPositionOfDeadPlayer={player.setPositionOfDeadPlayer}, modelHidden={IsPlayerModelHidden(player)}, targetStateKnown={flag}, spectator={flag2}."); } return flag3; } private static bool TryGetRemoteSpectatorTarget(IReadOnlyList? remoteSpectatorTargets, ulong clientId, out SpectatorTargetState? spectatorTargetState) { spectatorTargetState = null; if (remoteSpectatorTargets == null) { return false; } for (int i = 0; i < remoteSpectatorTargets.Count; i++) { SpectatorTargetState spectatorTargetState2 = remoteSpectatorTargets[i]; if (spectatorTargetState2.LocalClientId == clientId) { spectatorTargetState = spectatorTargetState2; return true; } } return false; } private static bool RepairAliveConnectedPlayerVisualState(StartOfRound round, int slot, PlayerControllerB player) { bool flag = false; if (player.setPositionOfDeadPlayer) { player.setPositionOfDeadPlayer = false; flag = true; } if (IsPlayerModelHidden(player) && round.allPlayerObjects != null && slot >= 0 && slot < round.allPlayerObjects.Length && (Object)(object)round.allPlayerObjects[slot] != (Object)null) { player.DisablePlayerModel(round.allPlayerObjects[slot], true, true); flag = true; } if (flag && (Object)(object)player.playerBodyAnimator != (Object)null) { player.playerBodyAnimator.SetBool("Limp", false); } return flag; } private static bool IsPlayerModelHidden(PlayerControllerB player) { if ((!((Object)(object)player.thisPlayerModel != (Object)null) || ((Renderer)player.thisPlayerModel).enabled) && (!((Object)(object)player.thisPlayerModelLOD1 != (Object)null) || ((Renderer)player.thisPlayerModelLOD1).enabled)) { if ((Object)(object)player.thisPlayerModelLOD2 != (Object)null) { return !((Renderer)player.thisPlayerModelLOD2).enabled; } return false; } return true; } private static bool IsClientCurrentlyConnected(NetworkManager networkManager, ulong clientId) { if (clientId == networkManager.LocalClientId) { return true; } IReadOnlyList connectedClientsIds = networkManager.ConnectedClientsIds; for (int i = 0; i < connectedClientsIds.Count; i++) { if (connectedClientsIds[i] == clientId) { return true; } } return false; } private static bool TryResolveVanillaFallbackDisplayName(int slot, PlayerControllerB player, out string displayName, out ulong steamId) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) displayName = string.Empty; steamId = player.playerSteamId; if (steamId == 0L) { steamId = (TryGetLobbySteamIdForSlot(slot, out var steamId2) ? steamId2 : 0); } if (steamId == 0L) { return false; } Friend val = new Friend(SteamId.op_Implicit(steamId)); return PlayerDisplayNameRules.TryNormalize(NormalizeSteamFriendName(((Friend)(ref val)).Name), out displayName); } private static bool TryGetLobbySteamIdForSlot(int slot, out ulong steamId) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) steamId = 0uL; GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || instance.disableSteam || slot < 0) { return false; } if (instance.steamIdsInLobby != null && slot < instance.steamIdsInLobby.Count) { steamId = SteamId.op_Implicit(instance.steamIdsInLobby[slot]); if (steamId != 0L) { return true; } } if (!instance.currentLobby.HasValue) { return false; } int num = 0; Lobby value = instance.currentLobby.Value; foreach (Friend member in ((Lobby)(ref value)).Members) { if (num == slot) { steamId = SteamId.op_Implicit(member.Id); return steamId != 0; } num++; } return false; } private static string NormalizeSteamFriendName(string friendName) { if (string.IsNullOrWhiteSpace(friendName)) { return "Nameless"; } char[] array = new char[friendName.Length]; int num = 0; foreach (char c in friendName) { if (char.IsLetter(c) || char.IsDigit(c) || c == '_' || c == '.') { array[num] = c; num++; } } if (num == 0) { return "Nameless"; } string text = new string(array, 0, num); if (text.Length > 2) { return text; } return text + "0"; } } public sealed class LethalCompanyDeadBodyHeadSourceInspectionAdapter : IGameDeadBodyHeadSourceInspectionAdapter { public bool TryGetDeadBodyHeadSourceInspectionSnapshots(int maxTransformDepth, bool includeRendererBounds, bool includeMaterials, out IReadOnlyList snapshots) { List list = new List(); snapshots = list.AsReadOnly(); StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null) { return false; } PlayerControllerB localPlayerController = instance.localPlayerController; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val.deadBody == (Object)null)) { bool isLocalPlayer = (Object)(object)localPlayerController != (Object)null && (Object)(object)val == (Object)(object)localPlayerController; list.Add(BuildSnapshot(val, isLocalPlayer, Mathf.Max(0, maxTransformDepth), includeRendererBounds, includeMaterials)); } } return list.Count > 0; } private static DeadBodyHeadSourceInspectionSnapshot BuildSnapshot(PlayerControllerB player, bool isLocalPlayer, int maxTransformDepth, bool includeRendererBounds, bool includeMaterials) { DeadBodyInfo deadBody = player.deadBody; Transform transform = ((Component)deadBody).transform; Transform detachedHeadObject = deadBody.detachedHeadObject; if ((Object)(object)detachedHeadObject == (Object)null) { return new DeadBodyHeadSourceInspectionSnapshot(player.playerClientId, player.actualClientId, string.IsNullOrWhiteSpace(player.playerUsername) ? "null" : player.playerUsername, isLocalPlayer, player.isPlayerDead, player.isPlayerControlled, SafeName(((Component)player).transform), GetPath(null, transform), deadBody.deactivated, deadBody.detachedHead, (deadBody.bodyParts != null) ? deadBody.bodyParts.Length : 0, GetFirstBodyPartPath(deadBody), hasDetachedHeadObject: false, "null", "null", "null", detachedHeadActiveSelf: false, detachedHeadActiveInHierarchy: false, 0, 0, 0, 0, new List().AsReadOnly(), new List().AsReadOnly()); } return new DeadBodyHeadSourceInspectionSnapshot(player.playerClientId, player.actualClientId, string.IsNullOrWhiteSpace(player.playerUsername) ? "null" : player.playerUsername, isLocalPlayer, player.isPlayerDead, player.isPlayerControlled, SafeName(((Component)player).transform), GetPath(null, transform), deadBody.deactivated, deadBody.detachedHead, (deadBody.bodyParts != null) ? deadBody.bodyParts.Length : 0, GetFirstBodyPartPath(deadBody), hasDetachedHeadObject: true, GetPath(null, detachedHeadObject), SafeName(detachedHeadObject), GetPath(null, detachedHeadObject.parent), ((Component)detachedHeadObject).gameObject.activeSelf, ((Component)detachedHeadObject).gameObject.activeInHierarchy, ((Component)detachedHeadObject).GetComponentsInChildren(true).Length, ((Component)detachedHeadObject).GetComponentsInChildren(true).Length, ((Component)detachedHeadObject).GetComponentsInChildren(true).Length, ((Component)detachedHeadObject).GetComponentsInChildren(true).Length, CollectTransforms(detachedHeadObject, maxTransformDepth).AsReadOnly(), CollectRenderers(detachedHeadObject, includeRendererBounds, includeMaterials).AsReadOnly()); } private static string GetFirstBodyPartPath(DeadBodyInfo deadBody) { if (deadBody.bodyParts == null || deadBody.bodyParts.Length == 0 || (Object)(object)deadBody.bodyParts[0] == (Object)null) { return "null"; } return GetPath(null, ((Component)deadBody.bodyParts[0]).transform); } private static List CollectTransforms(Transform root, int maxTransformDepth) { List list = new List(); VisitTransform(root, root, 0, maxTransformDepth, list); return list; } private static void VisitTransform(Transform root, Transform current, int depth, int maxDepth, List transforms) { if ((Object)(object)current == (Object)null || depth > maxDepth) { return; } transforms.Add(new TransformInspectionInfo(GetPath(root, current), SafeName(current), depth)); if (depth == maxDepth) { return; } for (int i = 0; i < current.childCount; i++) { Transform child = current.GetChild(i); if ((Object)(object)child != (Object)null) { VisitTransform(root, child, depth + 1, maxDepth, transforms); } } } private static List CollectRenderers(Transform root, bool includeRendererBounds, bool includeMaterials) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) List list = new List(); Renderer[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); foreach (Renderer val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { string rendererType = "Renderer"; Mesh val2 = null; int bonesCount = 0; string rootBoneName = "null"; SkinnedMeshRenderer val3 = (SkinnedMeshRenderer)(object)((val is SkinnedMeshRenderer) ? val : null); if (val3 != null) { rendererType = "SkinnedMeshRenderer"; val2 = val3.sharedMesh; bonesCount = ((val3.bones != null) ? val3.bones.Length : 0); rootBoneName = (((Object)(object)val3.rootBone != (Object)null) ? ((Object)val3.rootBone).name : "null"); } else if (val is MeshRenderer) { rendererType = "MeshRenderer"; MeshFilter component = ((Component)val).GetComponent(); val2 = (((Object)(object)component != (Object)null) ? component.sharedMesh : null); } Vector3? obj; Bounds bounds; if (!includeRendererBounds) { obj = null; } else { bounds = val.bounds; obj = ((Bounds)(ref bounds)).center; } Vector3? boundsCenter = obj; Vector3? obj2; if (!includeRendererBounds) { obj2 = null; } else { bounds = val.bounds; obj2 = ((Bounds)(ref bounds)).size; } Vector3? boundsSize = obj2; list.Add(new DeadBodyHeadRendererInspectionInfo(GetPath(root, ((Component)val).transform), SafeName(((Component)val).transform), rendererType, val.enabled, val.forceRenderingOff, ((Object)(object)val2 != (Object)null) ? ((Object)val2).name : "null", ((Object)(object)val2 != (Object)null) ? val2.subMeshCount : 0, bonesCount, rootBoneName, boundsCenter, boundsSize, (val.sharedMaterials != null) ? val.sharedMaterials.Length : 0, includeMaterials ? ((IReadOnlyList)GetMaterialNames(val).AsReadOnly()) : ((IReadOnlyList)new List().AsReadOnly()))); } } return list; } private static List GetMaterialNames(Renderer renderer) { List list = new List(); Material[] sharedMaterials = renderer.sharedMaterials; if (sharedMaterials == null) { return list; } Material[] array = sharedMaterials; foreach (Material val in array) { list.Add(((Object)(object)val != (Object)null) ? ((Object)val).name : "null"); } return list; } private static string GetPath(Transform? root, Transform? target) { if ((Object)(object)target == (Object)null) { return "null"; } List list = new List(); Transform val = target; while ((Object)(object)val != (Object)null) { list.Insert(0, SafeName(val)); if ((Object)(object)root != (Object)null && (Object)(object)val == (Object)(object)root) { return string.Join("/", list); } val = val.parent; } return string.Join("/", list); } private static string SafeName(Transform? transform) { if (!((Object)(object)transform != (Object)null)) { return "null"; } return ((Object)transform).name; } } public sealed class LethalCompanyDetachedHeadVisualSourceAdapter : IGameDetachedHeadVisualSourceAdapter { private const int GhostGirlDeathAnimationIndex = 1; public bool TryGetDetachedHeadVisualTemplate(out Transform? source) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.playerRagdolls == null || instance.playerRagdolls.Count <= 1) { source = null; return false; } return TryGetSource(instance.playerRagdolls[1], out source); } private static bool TryGetSource(GameObject ragdollPrefab, out Transform? source) { if ((Object)(object)ragdollPrefab == (Object)null) { source = null; return false; } DeadBodyInfo val = ragdollPrefab.GetComponent(); if ((Object)(object)val == (Object)null) { val = ragdollPrefab.GetComponentInChildren(true); } return TryGetSource(val, out source); } private static bool TryGetSource(DeadBodyInfo deadBody, out Transform? source) { if ((Object)(object)deadBody == (Object)null || !deadBody.detachedHead || (Object)(object)deadBody.detachedHeadObject == (Object)null || !HasSupportedVisualSource(deadBody.detachedHeadObject)) { source = null; return false; } source = deadBody.detachedHeadObject; return true; } private static bool HasSupportedVisualSource(Transform source) { MeshRenderer componentInChildren = ((Component)source).GetComponentInChildren(true); if ((Object)(object)componentInChildren == (Object)null) { return false; } MeshFilter component = ((Component)componentInChildren).GetComponent(); if ((Object)(object)component != (Object)null) { return (Object)(object)component.sharedMesh != (Object)null; } return false; } } public sealed class LethalCompanyPlayerModelInspectionAdapter : IGamePlayerModelInspectionAdapter { private static readonly string[] HeadRelatedNameTokens = new string[9] { "head", "skull", "neck", "spine", "visor", "helmet", "costume", "metarig", "rig" }; public bool TryGetPlayerModelInspectionSnapshots(bool includeLocalPlayer, bool includeRemotePlayers, int maxTransformDepth, bool includeRendererBounds, bool includeMaterials, out IReadOnlyList snapshots) { List list = new List(); snapshots = list.AsReadOnly(); StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null) { return false; } PlayerControllerB localPlayerController = instance.localPlayerController; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null)) { bool flag = (Object)(object)localPlayerController != (Object)null && (Object)(object)val == (Object)(object)localPlayerController; if ((!flag || includeLocalPlayer) && (flag || includeRemotePlayers)) { list.Add(BuildSnapshot(val, flag, Mathf.Max(0, maxTransformDepth), includeRendererBounds, includeMaterials)); } } } return list.Count > 0; } private static PlayerModelInspectionSnapshot BuildSnapshot(PlayerControllerB player, bool isLocalPlayer, int maxTransformDepth, bool includeRendererBounds, bool includeMaterials) { Transform transform = ((Component)player).transform; return new PlayerModelInspectionSnapshot(player.playerClientId, player.actualClientId, player.isPlayerDead, player.isPlayerControlled, isLocalPlayer, SafeName(transform), GetPath(transform, player.thisPlayerBody), GetPath(transform, player.meshContainer), GetPath(transform, player.lowerSpine), GetPath(transform, player.upperSpine), GetPath(transform, player.playerGlobalHead), GetPath(transform, player.playerEye), GetPath(transform, player.headCostumeContainer), GetPath(transform, player.headCostumeContainerLocal), (Object)(object)player.thisPlayerModel != (Object)null, (Object)(object)player.thisPlayerModelLOD1 != (Object)null, (Object)(object)player.thisPlayerModelLOD2 != (Object)null, (Object)(object)player.thisPlayerModelArms != (Object)null, CollectHeadRelatedTransforms(transform, maxTransformDepth).AsReadOnly(), CollectSkinnedMeshRenderers(transform, includeRendererBounds, includeMaterials).AsReadOnly()); } private static List CollectHeadRelatedTransforms(Transform root, int maxTransformDepth) { List list = new List(); VisitTransform(root, root, 0, maxTransformDepth, list); return list; } private static void VisitTransform(Transform root, Transform current, int depth, int maxDepth, List transforms) { if ((Object)(object)current == (Object)null || depth > maxDepth) { return; } if (IsHeadRelatedName(((Object)current).name)) { transforms.Add(new TransformInspectionInfo(GetPath(root, current), SafeName(current), depth)); } if (depth == maxDepth) { return; } for (int i = 0; i < current.childCount; i++) { Transform child = current.GetChild(i); if ((Object)(object)child != (Object)null) { VisitTransform(root, child, depth + 1, maxDepth, transforms); } } } private static List CollectSkinnedMeshRenderers(Transform root, bool includeRendererBounds, bool includeMaterials) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if ((Object)(object)root == (Object)null) { return list; } SkinnedMeshRenderer[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); foreach (SkinnedMeshRenderer val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { Mesh sharedMesh = val.sharedMesh; Transform rootBone = val.rootBone; Vector3? obj; Bounds bounds; if (!includeRendererBounds) { obj = null; } else { bounds = ((Renderer)val).bounds; obj = ((Bounds)(ref bounds)).center; } Vector3? boundsCenter = obj; Vector3? obj2; if (!includeRendererBounds) { obj2 = null; } else { bounds = ((Renderer)val).bounds; obj2 = ((Bounds)(ref bounds)).size; } Vector3? boundsSize = obj2; list.Add(new RendererInspectionInfo(GetPath(root, ((Component)val).transform), SafeName(((Component)val).transform), ((Renderer)val).enabled, ((Object)(object)sharedMesh != (Object)null) ? ((Object)sharedMesh).name : "null", ((Object)(object)sharedMesh != (Object)null) ? sharedMesh.subMeshCount : 0, (val.bones != null) ? val.bones.Length : 0, ((Object)(object)rootBone != (Object)null) ? ((Object)rootBone).name : "null", boundsCenter, boundsSize, (((Renderer)val).sharedMaterials != null) ? ((Renderer)val).sharedMaterials.Length : 0, includeMaterials ? ((IReadOnlyList)GetMaterialNames((Renderer)(object)val).AsReadOnly()) : ((IReadOnlyList)new List().AsReadOnly()))); } } return list; } private static List GetMaterialNames(Renderer renderer) { List list = new List(); Material[] sharedMaterials = renderer.sharedMaterials; if (sharedMaterials == null) { return list; } Material[] array = sharedMaterials; foreach (Material val in array) { list.Add(((Object)(object)val != (Object)null) ? ((Object)val).name : "null"); } return list; } private static bool IsHeadRelatedName(string name) { if (string.IsNullOrEmpty(name)) { return false; } string[] headRelatedNameTokens = HeadRelatedNameTokens; foreach (string value in headRelatedNameTokens) { if (name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } private static string GetPath(Transform root, Transform? target) { if ((Object)(object)root == (Object)null || (Object)(object)target == (Object)null) { return "null"; } List list = new List(); Transform val = target; while ((Object)(object)val != (Object)null) { list.Insert(0, SafeName(val)); if ((Object)(object)val == (Object)(object)root) { return string.Join("/", list); } val = val.parent; } return string.Join("/", list); } private static string SafeName(Transform? transform) { if (!((Object)(object)transform != (Object)null)) { return "null"; } return ((Object)transform).name; } } public sealed class LethalCompanySpectatorAdapter : IGameSpectatorAdapter { public SpectatorState ReadSpectatorState() { if (!TryGetLocalSpectatorSnapshot(out var snapshot)) { return SpectatorState.Unavailable; } string status = (snapshot.IsLocalPlayerDead ? "Local player is in spectator-compatible dead state." : "Local player is not dead."); return new SpectatorState(isAvailable: true, status, snapshot.IsLocalPlayerDead); } public bool TryGetLocalSpectatorSnapshot(out GameSpectatorSnapshot snapshot) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { snapshot = GameSpectatorSnapshot.Unavailable; return false; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { bool isSpectateCameraActive = IsSpectateCameraActive(instance); snapshot = new GameSpectatorSnapshot(hasRound: true, hasLocalPlayer: false, isLocalPlayerDead: false, hasBegunSpectating: false, hasSpectatedTarget: false, instance.overrideSpectateCamera, isSpectateCameraActive, instance.spectateCamera, null, null, null, null, null); return true; } PlayerControllerB spectatedPlayerScript = localPlayerController.spectatedPlayerScript; bool flag = IsValidSpectateTarget(spectatedPlayerScript); Transform anchor = (flag ? ResolveAnchor(spectatedPlayerScript) : null); ulong? spectatedPlayerSlotId = (flag ? new ulong?(spectatedPlayerScript.playerClientId) : null); ulong? spectatedPlayerActualClientId = (flag ? new ulong?(spectatedPlayerScript.actualClientId) : null); bool isSpectateCameraActive2 = IsSpectateCameraActive(instance); snapshot = new GameSpectatorSnapshot(hasRound: true, hasLocalPlayer: true, localPlayerController.isPlayerDead, localPlayerController.hasBegunSpectating, flag, instance.overrideSpectateCamera, isSpectateCameraActive2, instance.spectateCamera, anchor, localPlayerController.playerClientId, localPlayerController.actualClientId, spectatedPlayerSlotId, spectatedPlayerActualClientId); return true; } public bool IsLocalPlayerSpectating() { if (TryGetLocalSpectatorSnapshot(out var snapshot) && snapshot.HasRound && snapshot.HasLocalPlayer && snapshot.IsLocalPlayerDead) { return (Object)(object)snapshot.SpectateCamera != (Object)null; } return false; } public bool TryGetSpectatedPlayerAnchor(out Transform? anchor) { if (TryGetLocalSpectatorSnapshot(out var snapshot) && (Object)(object)snapshot.Anchor != (Object)null) { anchor = snapshot.Anchor; return true; } anchor = null; return false; } public bool TryGetSpectateCamera(out Camera? camera) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.spectateCamera != (Object)null) { camera = instance.spectateCamera; return true; } camera = null; return false; } public bool TryGetActiveCamera(out Camera? camera) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.activeCamera != (Object)null) { camera = instance.activeCamera; return true; } PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer != (Object)null && (Object)(object)localPlayer.gameplayCamera != (Object)null) { camera = localPlayer.gameplayCamera; return true; } camera = null; return false; } public bool IsGameOverSpectateOverrideActive() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null) { return instance.overrideSpectateCamera; } return false; } public bool IsLocalQuickMenuOpen() { PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer == (Object)null || (Object)(object)localPlayer.quickMenuManager == (Object)null) { return false; } return localPlayer.quickMenuManager.isMenuOpen; } public bool IsValidSpectateTarget(object? target) { PlayerControllerB player = (PlayerControllerB)((target is PlayerControllerB) ? target : null); return LethalCompanySpectatorTargetRules.IsValidSpectateTarget(StartOfRound.Instance, player); } public bool TryGetSpectatedPlayerId(out ulong slotId, out ulong actualClientId) { if (TryGetLocalSpectatorSnapshot(out var snapshot) && snapshot.SpectatedPlayerSlotId.HasValue && snapshot.SpectatedPlayerActualClientId.HasValue) { slotId = snapshot.SpectatedPlayerSlotId.Value; actualClientId = snapshot.SpectatedPlayerActualClientId.Value; return true; } slotId = 0uL; actualClientId = 0uL; return false; } public bool TryGetLocalPlayerIdentity(out ulong clientId, out ulong slotId) { PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer != (Object)null) { clientId = localPlayer.actualClientId; slotId = localPlayer.playerClientId; return true; } clientId = 0uL; slotId = 0uL; return false; } public bool TryGetPlayerDisplayName(ulong clientId, ulong slotId, out string displayName) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null) { displayName = string.Empty; return false; } bool flag = false; for (int i = 0; i < instance.allPlayerScripts.Length; i++) { PlayerControllerB val = instance.allPlayerScripts[i]; if (!((Object)(object)val == (Object)null) && val.actualClientId == clientId) { flag = true; if (PlayerDisplayNameRules.TryNormalize(val.playerUsername, out displayName)) { return true; } } } if (flag) { displayName = string.Empty; return false; } for (int j = 0; j < instance.allPlayerScripts.Length; j++) { PlayerControllerB val2 = instance.allPlayerScripts[j]; if (!((Object)(object)val2 == (Object)null) && val2.playerClientId == slotId && PlayerDisplayNameRules.TryNormalize(val2.playerUsername, out displayName)) { return true; } } displayName = string.Empty; return false; } public bool TryGetLocalVoicePlayerName(out string voicePlayerName) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.voiceChatModule != (Object)null && !string.IsNullOrWhiteSpace(instance.voiceChatModule.LocalPlayerName)) { voicePlayerName = instance.voiceChatModule.LocalPlayerName.Trim(); return true; } voicePlayerName = string.Empty; return false; } public bool TryGetLocalPlayerHeadPoint(out Transform? anchor) { PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer == (Object)null) { anchor = null; return false; } if (TryFindHeadPoint(localPlayer.playerGlobalHead, out anchor)) { return true; } anchor = null; return false; } public bool TryGetLocalPlayerHeadAnchor(out Transform? anchor) { PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer == (Object)null) { anchor = null; return false; } if (TryGetLocalPlayerHeadPoint(out anchor)) { return true; } if ((Object)(object)localPlayer.playerGlobalHead != (Object)null) { anchor = localPlayer.playerGlobalHead; return true; } if ((Object)(object)localPlayer.headCostumeContainer != (Object)null) { anchor = localPlayer.headCostumeContainer; return true; } anchor = ((Component)localPlayer).transform; return (Object)(object)anchor != (Object)null; } public bool TryGetLocalPlayerHeadAnchorPosition(out Vector3 position) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB localPlayer = GetLocalPlayer(); if ((Object)(object)localPlayer == (Object)null) { position = Vector3.zero; return false; } if (TryGetLocalPlayerHeadAnchor(out Transform anchor) && (Object)(object)anchor != (Object)null) { position = (((Object)(object)anchor == (Object)(object)((Component)localPlayer).transform) ? (anchor.position + Vector3.up) : anchor.position); return true; } position = (((Object)(object)((Component)localPlayer).transform != (Object)null) ? (((Component)localPlayer).transform.position + Vector3.up) : Vector3.zero); return (Object)(object)((Component)localPlayer).transform != (Object)null; } private static Transform? ResolveAnchor(PlayerControllerB player) { Transform transform = ((Component)player).transform; return (Transform?)(SpectatorTargetAnchorSelectionRules.Resolve((Object)(object)transform != (Object)null, (Object)(object)player.lowerSpine != (Object)null, (Object)(object)player.playerGlobalHead != (Object)null) switch { SpectatorTargetAnchorSource.PlayerRoot => transform, SpectatorTargetAnchorSource.LowerSpine => player.lowerSpine, SpectatorTargetAnchorSource.GlobalHead => player.playerGlobalHead, _ => null, }); } private static PlayerControllerB? GetLocalPlayer() { StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.localPlayerController; } private static bool TryFindHeadPoint(Transform? root, out Transform? headPoint) { if ((Object)(object)root == (Object)null) { headPoint = null; return false; } if (((Object)root).name == "HeadPoint") { headPoint = root; return true; } for (int i = 0; i < root.childCount; i++) { if (TryFindHeadPoint(root.GetChild(i), out headPoint)) { return true; } } headPoint = null; return false; } private static bool IsSpectateCameraActive(StartOfRound round) { if ((Object)(object)round.spectateCamera != (Object)null) { return (Object)(object)round.activeCamera == (Object)(object)round.spectateCamera; } return false; } } internal static class LethalCompanySpectatorTargetRules { public static bool IsValidSpectateTarget(StartOfRound round, PlayerControllerB? player) { if ((Object)(object)player == (Object)null || !player.isPlayerControlled || player.isPlayerDead || player.disconnectedMidGame) { return false; } if ((Object)(object)round == (Object)null || round.ClientPlayerList == null || round.allPlayerScripts == null) { return false; } if (!round.ClientPlayerList.TryGetValue(player.actualClientId, out var value) || value < 0 || value >= round.allPlayerScripts.Length || (Object)(object)round.allPlayerScripts[value] != (Object)(object)player) { return false; } return player.playerClientId == (ulong)value; } public static bool IsDisconnectedTarget(StartOfRound round, PlayerControllerB? player) { if ((Object)(object)player == (Object)null) { return false; } if ((Object)(object)round == (Object)null || round.ClientPlayerList == null) { return SpectatorDisconnectTargetSwitchRules.IsDisconnectLikeInvalidTarget(player.disconnectedMidGame, player.isPlayerControlled, player.isPlayerDead, removedFromClientMap: false); } int value; bool removedFromClientMap = !round.ClientPlayerList.TryGetValue(player.actualClientId, out value) || value < 0 || round.allPlayerScripts == null || value >= round.allPlayerScripts.Length || (Object)(object)round.allPlayerScripts[value] != (Object)(object)player; return SpectatorDisconnectTargetSwitchRules.IsDisconnectLikeInvalidTarget(player.disconnectedMidGame, player.isPlayerControlled, player.isPlayerDead, removedFromClientMap); } } public sealed class LethalCompanySpectatorTargetSwitchAdapter : IGameSpectatorTargetSwitchAdapter { public bool TryGetDisconnectTargetSwitchContext(out SpectatorDisconnectTargetSwitchContext context) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { context = default(SpectatorDisconnectTargetSwitchContext); return false; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { context = default(SpectatorDisconnectTargetSwitchContext); return false; } PlayerControllerB spectatedPlayerScript = localPlayerController.spectatedPlayerScript; bool flag = (Object)(object)spectatedPlayerScript != (Object)null; bool currentTargetIsValid = flag && LethalCompanySpectatorTargetRules.IsValidSpectateTarget(instance, spectatedPlayerScript); bool currentTargetDisconnected = flag && LethalCompanySpectatorTargetRules.IsDisconnectedTarget(instance, spectatedPlayerScript); context = new SpectatorDisconnectTargetSwitchContext(localPlayerController.isPlayerDead, (Object)(object)instance.spectateCamera != (Object)null, instance.overrideSpectateCamera, flag, currentTargetIsValid, currentTargetDisconnected, HasReplacementTarget(instance, localPlayerController, spectatedPlayerScript)); return true; } public bool TrySwitchToNextSpectatorTarget() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null || !localPlayerController.isPlayerDead) { return false; } PlayerControllerB spectatedPlayerScript = localPlayerController.spectatedPlayerScript; SpectatorVanillaInputGuard.BeginInternalTargetSwitch(); try { localPlayerController.SpectateNextPlayer(false); } finally { SpectatorVanillaInputGuard.EndInternalTargetSwitch(); } PlayerControllerB spectatedPlayerScript2 = localPlayerController.spectatedPlayerScript; return SpectatorDisconnectTargetSwitchResultRules.DidSwitchToValidTarget(targetSwitchInvoked: true, (Object)(object)spectatedPlayerScript2 != (Object)null && spectatedPlayerScript2 != spectatedPlayerScript, LethalCompanySpectatorTargetRules.IsValidSpectateTarget(instance, spectatedPlayerScript2)); } private static bool HasReplacementTarget(StartOfRound round, PlayerControllerB localPlayer, PlayerControllerB? currentTarget) { if (round.allPlayerScripts == null) { return false; } for (int i = 0; i < round.allPlayerScripts.Length; i++) { PlayerControllerB val = round.allPlayerScripts[i]; if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)localPlayer) && !((Object)(object)val == (Object)(object)currentTarget) && LethalCompanySpectatorTargetRules.IsValidSpectateTarget(round, val)) { return true; } } return false; } } public sealed class LethalCompanySpectatorVoiceRoutingAdapter : IGameSpectatorVoiceRoutingAdapter { private readonly struct PlaybackSnapshot { public float Volume { get; } public float SpatialBlend { get; } public float PanStereo { get; } public bool Set2D { get; } public Vector3 Position { get; } public PlaybackSnapshot(float volume, float spatialBlend, float panStereo, bool set2D, Vector3 position) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) Volume = volume; SpatialBlend = spatialBlend; PanStereo = panStereo; Set2D = set2D; Position = position; } } private readonly struct VoiceListenerFrame { public Vector3 DesiredPosition { get; } public Quaternion DesiredRotation { get; } public Vector3 ActualPosition { get; } public Quaternion ActualRotation { get; } public bool IsRemapped { get { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) Vector3 val = DesiredPosition - ActualPosition; if (!(((Vector3)(ref val)).sqrMagnitude > 0.0001f)) { return Quaternion.Dot(DesiredRotation, ActualRotation) < 0.9995f; } return true; } } public VoiceListenerFrame(Vector3 desiredPosition, Quaternion desiredRotation, Vector3 actualPosition, Quaternion actualRotation) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) DesiredPosition = desiredPosition; DesiredRotation = desiredRotation; ActualPosition = actualPosition; ActualRotation = actualRotation; } } private readonly Dictionary _snapshots = new Dictionary(); private readonly Dictionary _nextPlaybackResolveTime = new Dictionary(); private readonly SpectatorVoicePlayerLookupCache _playerLookupCache = new SpectatorVoicePlayerLookupCache(); private readonly SpectatorVoiceRouteDiagnosticLimiter _diagnosticLimiter = new SpectatorVoiceRouteDiagnosticLimiter(); private readonly HashSet _fallbackBindingLogged = new HashSet(); private readonly IEnhancedSpectatorNetworkService? _networkService; private readonly Func _debugEnabled; private PlayerControllerB[]? _cachedPlayerScripts; private int _playerLookupCacheFrame = -1; public LethalCompanySpectatorVoiceRoutingAdapter(IEnhancedSpectatorNetworkService? networkService = null, Func? debugEnabled = null) { _networkService = networkService; _debugEnabled = debugEnabled ?? ((Func)(() => false)); } public bool TryGetLocalVoiceReceiverState(out bool hasLocalPlayer, out bool isLocalPlayerDead, out ulong localClientId, out ulong localPlayerSlotId) { PlayerControllerB val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.localPlayerController : null); hasLocalPlayer = (Object)(object)val != (Object)null; isLocalPlayerDead = (Object)(object)val != (Object)null && val.isPlayerDead; localClientId = (((Object)(object)val != (Object)null) ? val.actualClientId : 0); localPlayerSlotId = (((Object)(object)val != (Object)null) ? val.playerClientId : 0); return hasLocalPlayer; } public bool TryApplySpectatorVoiceRoute(ulong spectatorClientId, ulong spectatorSlotId, SpectatorPoseState? poseState, SpectatorVoicePlaybackSettings settings, out string reason) { //IL_02c5: Unknown result type (might be due to invalid IL or missing references) //IL_02cc: Unknown result type (might be due to invalid IL or missing references) //IL_02dc: Unknown result type (might be due to invalid IL or missing references) //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Unknown result type (might be due to invalid IL or missing references) //IL_024e: Unknown result type (might be due to invalid IL or missing references) //IL_0251: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.localPlayerController == (Object)null) { reason = "round or local player unavailable"; return false; } if ((Object)(object)instance.voiceChatModule != (Object)null && instance.voiceChatModule.IsDeafened) { reason = "local player is deafened"; return false; } PlayerControllerB val = FindPlayer(instance, spectatorClientId, spectatorSlotId); if ((Object)(object)val == (Object)null) { reason = "spectator player object unavailable"; return false; } if ((Object)(object)val == (Object)(object)instance.localPlayerController) { reason = "spectator is local player"; return false; } if (!val.isPlayerDead) { reason = "spectator is not dead"; return false; } PeerIdentityState identity = TryGetRemotePeerIdentity(spectatorClientId); if (!EnsureVoicePlayback(instance, val, identity)) { reason = "voice playback objects unavailable"; return false; } if (val.voicePlayerState.IsLocallyMuted) { reason = "spectator is locally muted"; return false; } if (settings.UseRemotePosePosition) { if (poseState == null || !poseState.IsSpectating) { if (!settings.FallbackTo2DWhenPoseMissing) { reason = "remote spectator pose unavailable"; return false; } CaptureSnapshotIfNeeded(val); VoiceListenerFrame voiceListenerFrame = ResolveVoiceListenerFrame(instance); Apply2DPlayback(val, Mathf.Clamp01(settings.Volume)); MaybeLogRouteApply(spectatorClientId, poseAvailable: false, fallbackTo2D: true, ((Component)val.currentVoiceChatAudioSource).transform.position, voiceListenerFrame.ActualPosition, ((Component)val.currentVoiceChatAudioSource).transform.position, voiceListenerFrame.DesiredPosition, 0f, val.voicePlayerState.Volume, val.currentVoiceChatAudioSource.spatialBlend, val.currentVoiceChatIngameSettings.set2D, remapped: false); return true; } CaptureSnapshotIfNeeded(val); Vector3 position = poseState.Position; VoiceListenerFrame voiceListenerFrame2 = ResolveVoiceListenerFrame(instance); Vector3 val2 = SpectatorVoiceSpatializationRules.ResolvePlaybackSourcePosition(position, voiceListenerFrame2.DesiredPosition, voiceListenerFrame2.DesiredRotation, voiceListenerFrame2.ActualPosition, voiceListenerFrame2.ActualRotation); float distance = Vector3.Distance(voiceListenerFrame2.ActualPosition, val2); ((Component)val.currentVoiceChatAudioSource).transform.position = val2; val.currentVoiceChatAudioSource.spatialBlend = 1f; val.currentVoiceChatAudioSource.panStereo = 0f; val.currentVoiceChatIngameSettings.set2D = false; val.voicePlayerState.Volume = SpectatorVoiceDistanceAttenuation.CalculateVolume(settings.Volume, settings.EnableDistanceAttenuation, distance, settings.MinDistance, settings.MaxDistance, settings.RolloffPower, settings.MinimumVolume); MaybeLogRouteApply(spectatorClientId, poseAvailable: true, fallbackTo2D: false, val2, voiceListenerFrame2.ActualPosition, position, voiceListenerFrame2.DesiredPosition, distance, val.voicePlayerState.Volume, val.currentVoiceChatAudioSource.spatialBlend, val.currentVoiceChatIngameSettings.set2D, voiceListenerFrame2.IsRemapped); } else { CaptureSnapshotIfNeeded(val); VoiceListenerFrame voiceListenerFrame3 = ResolveVoiceListenerFrame(instance); Apply2DPlayback(val, Mathf.Clamp01(settings.Volume)); MaybeLogRouteApply(spectatorClientId, poseState?.IsSpectating ?? false, fallbackTo2D: false, ((Component)val.currentVoiceChatAudioSource).transform.position, voiceListenerFrame3.ActualPosition, ((Component)val.currentVoiceChatAudioSource).transform.position, voiceListenerFrame3.DesiredPosition, 0f, val.voicePlayerState.Volume, val.currentVoiceChatAudioSource.spatialBlend, val.currentVoiceChatIngameSettings.set2D, remapped: false); } return true; } public void ClearSpectatorVoiceRoute(ulong spectatorClientId, ulong spectatorSlotId) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.localPlayerController == (Object)null) { return; } PlayerControllerB val = FindPlayer(instance, spectatorClientId, spectatorSlotId); if ((Object)(object)val == (Object)null || (Object)(object)val == (Object)(object)instance.localPlayerController) { return; } PeerIdentityState identity = TryGetRemotePeerIdentity(spectatorClientId); if (EnsureVoicePlayback(instance, val, identity)) { if (_snapshots.TryGetValue(val.actualClientId, out var value) || _snapshots.TryGetValue(val.playerClientId, out value)) { RestoreSnapshot(val, value); _snapshots.Remove(val.actualClientId); _snapshots.Remove(val.playerClientId); _diagnosticLimiter.Clear(val.actualClientId); _diagnosticLimiter.Clear(val.playerClientId); _fallbackBindingLogged.Remove(val.actualClientId); _fallbackBindingLogged.Remove(val.playerClientId); _nextPlaybackResolveTime.Remove(val.actualClientId); _nextPlaybackResolveTime.Remove(val.playerClientId); } else if (val.isPlayerDead && !instance.localPlayerController.isPlayerDead) { val.voicePlayerState.Volume = 0f; val.currentVoiceChatAudioSource.spatialBlend = 1f; val.currentVoiceChatIngameSettings.set2D = false; _diagnosticLimiter.Clear(spectatorClientId); _diagnosticLimiter.Clear(spectatorSlotId); _fallbackBindingLogged.Remove(spectatorClientId); _fallbackBindingLogged.Remove(spectatorSlotId); _nextPlaybackResolveTime.Remove(spectatorClientId); _nextPlaybackResolveTime.Remove(spectatorSlotId); } } } public void ClearCachedVoiceRouteLookups() { ClearPlayerLookupCache(); } private bool EnsureVoicePlayback(StartOfRound round, PlayerControllerB player, PeerIdentityState? identity) { if (player.voicePlayerState != null && (Object)(object)player.currentVoiceChatAudioSource != (Object)null && (Object)(object)player.currentVoiceChatIngameSettings != (Object)null) { _nextPlaybackResolveTime.Remove(player.actualClientId); return true; } if (_nextPlaybackResolveTime.TryGetValue(player.actualClientId, out var value) && Time.unscaledTime < value) { return false; } round.RefreshPlayerVoicePlaybackObjects(); if (player.voicePlayerState != null && (Object)(object)player.currentVoiceChatAudioSource != (Object)null && (Object)(object)player.currentVoiceChatIngameSettings != (Object)null) { _nextPlaybackResolveTime.Remove(player.actualClientId); return true; } if (TryBindVoicePlaybackFromIdentity(round, player, identity)) { _nextPlaybackResolveTime.Remove(player.actualClientId); return true; } _nextPlaybackResolveTime[player.actualClientId] = Time.unscaledTime + 0.5f; return false; } private PeerIdentityState? TryGetRemotePeerIdentity(ulong clientId) { if (_networkService != null && _networkService.TryGetRemotePeerIdentity(clientId, out PeerIdentityState state)) { return state; } return null; } private bool TryBindVoicePlaybackFromIdentity(StartOfRound round, PlayerControllerB player, PeerIdentityState? identity) { List list = BuildVoicePlayerNameCandidates(player, identity); if (list.Count == 0) { return false; } PlayerVoiceIngameSettings[] array = Object.FindObjectsOfType(true); foreach (PlayerVoiceIngameSettings val in array) { if ((Object)(object)val == (Object)null) { continue; } val.FindPlayerIfNull(); if (IsMatchingVoicePlayback(val, list)) { VoicePlayerState val2 = (VoicePlayerState)((val._playerState != null) ? ((object)val._playerState) : ((object)TryFindVoicePlayerState(round, list))); AudioSource voiceAudio = val.voiceAudio; if (val2 != null && !((Object)(object)voiceAudio == (Object)null) && !IsVoicePlaybackAlreadyAssigned(round, player, val, voiceAudio, val2)) { player.voicePlayerState = val2; player.currentVoiceChatAudioSource = voiceAudio; player.currentVoiceChatIngameSettings = val; val._playerState = val2; ApplyVoiceMixer(player, voiceAudio); MaybeLogFallbackBinding(player, val, list[0]); return true; } } } return false; } private static List BuildVoicePlayerNameCandidates(PlayerControllerB player, PeerIdentityState? identity) { List list = new List(); AddCandidate(list, (identity != null) ? identity.VoicePlayerName : string.Empty); AddCandidate(list, TryGetNfgoPlayerId(player)); AddCandidate(list, (player.voicePlayerState != null) ? player.voicePlayerState.Name : string.Empty); return list; } private static void AddCandidate(List candidates, string? value) { if (string.IsNullOrWhiteSpace(value)) { return; } string text = value.Trim(); foreach (string candidate in candidates) { if (string.Equals(candidate, text, StringComparison.Ordinal)) { return; } } candidates.Add(text); } private static string TryGetNfgoPlayerId(PlayerControllerB player) { NfgoPlayer val = (((Object)(object)((Component)player).gameObject != (Object)null) ? ((Component)player).gameObject.GetComponentInChildren() : null); if (!((Object)(object)val != (Object)null)) { return string.Empty; } return val.PlayerId; } private static bool IsMatchingVoicePlayback(PlayerVoiceIngameSettings setting, List candidateNames) { string a = ((setting._playerState != null) ? setting._playerState.Name : string.Empty); string a2 = (((Object)(object)setting._playbackComponent != (Object)null) ? setting._playbackComponent.PlayerName : string.Empty); foreach (string candidateName in candidateNames) { if (string.Equals(a, candidateName, StringComparison.Ordinal) || string.Equals(a2, candidateName, StringComparison.Ordinal)) { return true; } } return false; } private static VoicePlayerState? TryFindVoicePlayerState(StartOfRound round, List candidateNames) { if ((Object)(object)round.voiceChatModule == (Object)null) { return null; } foreach (string candidateName in candidateNames) { VoicePlayerState val = round.voiceChatModule.FindPlayer(candidateName); if (val != null) { return val; } } return null; } private static bool IsVoicePlaybackAlreadyAssigned(StartOfRound round, PlayerControllerB targetPlayer, PlayerVoiceIngameSettings setting, AudioSource voiceAudio, VoicePlayerState voiceState) { if (round.allPlayerScripts == null) { return false; } PlayerControllerB[] allPlayerScripts = round.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && !((Object)(object)val == (Object)(object)targetPlayer) && ((Object)(object)val.currentVoiceChatIngameSettings == (Object)(object)setting || (Object)(object)val.currentVoiceChatAudioSource == (Object)(object)voiceAudio || val.voicePlayerState == voiceState)) { return true; } } return false; } private static void ApplyVoiceMixer(PlayerControllerB player, AudioSource voiceAudio) { SoundManager instance = SoundManager.Instance; if (!((Object)(object)instance == (Object)null) && instance.playerVoiceMixers != null && player.playerClientId < (ulong)instance.playerVoiceMixers.Length) { voiceAudio.outputAudioMixerGroup = instance.playerVoiceMixers[(uint)player.playerClientId]; } } private void MaybeLogFallbackBinding(PlayerControllerB player, PlayerVoiceIngameSettings setting, string matchedVoiceName) { if (_debugEnabled() && _fallbackBindingLogged.Add(player.actualClientId)) { string value = (((Object)(object)setting._playbackComponent != (Object)null) ? setting._playbackComponent.PlayerName : "none"); string value2 = ((setting._playerState != null) ? setting._playerState.Name : "none"); ModLog.Debug($"Spectator voice playback fallback bound: client={player.actualClientId}, slot={player.playerClientId}, matchedVoiceName={FormatVoiceNameForLog(matchedVoiceName)}, playbackName={FormatVoiceNameForLog(value)}, voiceStateName={FormatVoiceNameForLog(value2)}."); } } private static string FormatVoiceNameForLog(string value) { if (!string.IsNullOrWhiteSpace(value)) { return "present"; } return "none"; } private PlayerControllerB? FindPlayer(StartOfRound round, ulong clientId, ulong slotId) { PlayerControllerB[] allPlayerScripts = round.allPlayerScripts; if (allPlayerScripts == null) { ClearPlayerLookupCache(); return null; } int frameCount = Time.frameCount; if (_playerLookupCacheFrame != frameCount || _cachedPlayerScripts != allPlayerScripts) { RebuildPlayerLookupCache(allPlayerScripts, frameCount); } if (!_playerLookupCache.TryGet(clientId, slotId, out PlayerControllerB player)) { return null; } return player; } private void RebuildPlayerLookupCache(PlayerControllerB[] players, int frame) { _playerLookupCache.Clear(); _cachedPlayerScripts = players; _playerLookupCacheFrame = frame; foreach (PlayerControllerB val in players) { if (!((Object)(object)val == (Object)null)) { _playerLookupCache.Store(val, val.actualClientId, val.playerClientId); } } } private void ClearPlayerLookupCache() { _playerLookupCache.Clear(); _cachedPlayerScripts = null; _playerLookupCacheFrame = -1; } private static VoiceListenerFrame ResolveVoiceListenerFrame(StartOfRound round) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) Transform val = TryGetDesiredListenerTransform(round); Transform val2 = TryGetActualAudioListenerTransform(round); if ((Object)(object)val == (Object)null && (Object)(object)val2 == (Object)null) { PlayerControllerB localPlayerController = round.localPlayerController; Vector3 val3 = (((Object)(object)localPlayerController != (Object)null) ? ((Component)localPlayerController).transform.position : Vector3.zero); Quaternion val4 = (((Object)(object)localPlayerController != (Object)null) ? ((Component)localPlayerController).transform.rotation : Quaternion.identity); return new VoiceListenerFrame(val3, val4, val3, val4); } if (val == null) { val = val2; } if (val2 == null) { val2 = val; } return new VoiceListenerFrame(val.position, val.rotation, val2.position, val2.rotation); } private static Transform? TryGetDesiredListenerTransform(StartOfRound round) { PlayerControllerB localPlayerController = round.localPlayerController; if ((Object)(object)localPlayerController != (Object)null && localPlayerController.isPlayerDead && (Object)(object)round.spectateCamera != (Object)null && ((Behaviour)round.spectateCamera).enabled) { return ((Component)round.spectateCamera).transform; } Camera val = (((Object)(object)round.activeCamera != (Object)null) ? round.activeCamera : Camera.main); if (!((Object)(object)val != (Object)null)) { if (!((Object)(object)localPlayerController != (Object)null)) { return null; } return ((Component)localPlayerController).transform; } return ((Component)val).transform; } private static Transform? TryGetActualAudioListenerTransform(StartOfRound round) { if ((Object)(object)round.audioListener != (Object)null && ((Behaviour)round.audioListener).enabled) { return ((Component)round.audioListener).transform; } PlayerControllerB localPlayerController = round.localPlayerController; if ((Object)(object)localPlayerController != (Object)null && (Object)(object)localPlayerController.activeAudioListener != (Object)null && ((Behaviour)localPlayerController.activeAudioListener).enabled) { return ((Component)localPlayerController.activeAudioListener).transform; } return null; } private static void Apply2DPlayback(PlayerControllerB spectator, float volume) { spectator.voicePlayerState.Volume = volume; spectator.currentVoiceChatAudioSource.spatialBlend = 0f; spectator.currentVoiceChatAudioSource.panStereo = 0f; spectator.currentVoiceChatIngameSettings.set2D = true; } private void CaptureSnapshotIfNeeded(PlayerControllerB spectator) { //IL_0054: Unknown result type (might be due to invalid IL or missing references) ulong actualClientId = spectator.actualClientId; if (!_snapshots.ContainsKey(actualClientId)) { _snapshots[actualClientId] = new PlaybackSnapshot(spectator.voicePlayerState.Volume, spectator.currentVoiceChatAudioSource.spatialBlend, spectator.currentVoiceChatAudioSource.panStereo, spectator.currentVoiceChatIngameSettings.set2D, ((Component)spectator.currentVoiceChatAudioSource).transform.position); } } private void MaybeLogRouteApply(ulong spectatorClientId, bool poseAvailable, bool fallbackTo2D, Vector3 sourcePosition, Vector3 listenerPosition, Vector3 remotePosePosition, Vector3 desiredListenerPosition, float distance, float finalVolume, float spatialBlend, bool set2D, bool remapped) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) if (_debugEnabled() && _diagnosticLimiter.ShouldLog(spectatorClientId, Time.frameCount, poseAvailable, fallbackTo2D, distance, finalVolume, spatialBlend, set2D)) { string text = ((spatialBlend >= 0.5f && !set2D) ? "3D" : "2D"); ModLog.Debug($"Spectator voice route apply: spectatorClient={spectatorClientId}, mode={text}, poseAvailable={poseAvailable}, fallbackTo2D={fallbackTo2D}, source={FormatVector(sourcePosition)}, listener={FormatVector(listenerPosition)}, remoteSource={FormatVector(remotePosePosition)}, desiredListener={FormatVector(desiredListenerPosition)}, remapped={remapped}, distance={distance:F2}, finalVolume={finalVolume:F2}, spatialBlend={spatialBlend:F2}, set2D={set2D}."); } } private static string FormatVector(Vector3 value) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({value.x:F2}, {value.y:F2}, {value.z:F2})"; } private static void RestoreSnapshot(PlayerControllerB spectator, PlaybackSnapshot snapshot) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) if (spectator.voicePlayerState != null) { spectator.voicePlayerState.Volume = snapshot.Volume; } if ((Object)(object)spectator.currentVoiceChatAudioSource != (Object)null) { spectator.currentVoiceChatAudioSource.spatialBlend = snapshot.SpatialBlend; spectator.currentVoiceChatAudioSource.panStereo = snapshot.PanStereo; ((Component)spectator.currentVoiceChatAudioSource).transform.position = snapshot.Position; } if ((Object)(object)spectator.currentVoiceChatIngameSettings != (Object)null) { spectator.currentVoiceChatIngameSettings.set2D = snapshot.Set2D; } } } public sealed class LethalCompanyVoiceActivityAdapter : IGameVoiceActivityAdapter { public bool TryGetVoiceActivity(ulong clientId, ulong slotId, out VoiceActivityState state) { PlayerControllerB val = FindPlayer(clientId, slotId); if ((Object)(object)val == (Object)null) { state = VoiceActivityState.NoData; return false; } if (IsLocalPlayer(val) && TryGetLocalVoiceActivity(val, out state)) { return true; } VoicePlayerState voicePlayerState = val.voicePlayerState; if (voicePlayerState == null) { state = VoiceActivityState.NoData; return false; } bool isSpeaking = voicePlayerState.IsSpeaking && !voicePlayerState.IsLocallyMuted; state = new VoiceActivityState(hasData: true, isSpeaking, voicePlayerState.Amplitude, voicePlayerState.Volume, val.actualClientId, val.playerClientId, DateTime.UtcNow.Ticks); return true; } private static bool TryGetLocalVoiceActivity(PlayerControllerB player, out VoiceActivityState state) { state = VoiceActivityState.NoData; StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.voiceChatModule == (Object)null) { return false; } string localPlayerName = instance.voiceChatModule.LocalPlayerName; if (string.IsNullOrWhiteSpace(localPlayerName)) { return false; } VoicePlayerState val = instance.voiceChatModule.FindPlayer(localPlayerName); if (val == null) { return false; } bool isSpeaking = val.IsSpeaking && !instance.voiceChatModule.IsMuted; state = new VoiceActivityState(hasData: true, isSpeaking, val.Amplitude, val.Volume, player.actualClientId, player.playerClientId, DateTime.UtcNow.Ticks); return true; } private static bool IsLocalPlayer(PlayerControllerB player) { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance != (Object)null) { return (Object)(object)instance.localPlayerController == (Object)(object)player; } return false; } private static PlayerControllerB? FindPlayer(ulong clientId, ulong slotId) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.allPlayerScripts == null) { return null; } PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && val.actualClientId == clientId) { return val; } } allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if (!((Object)(object)val2 == (Object)null) && val2.playerClientId == slotId) { return val2; } } return null; } } public sealed class LethalCompanyVoiceDiagnosticsAdapter : IGameVoiceDiagnosticsAdapter { public bool TryGetVoiceDiagnosticsSnapshot(bool includeLocalPlayer, bool includeRemotePlayers, bool includeAudioSourceDiagnostics, bool includeWalkieDiagnostics, out VoiceDiagnosticsSnapshot snapshot) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { snapshot = EmptySnapshot(hasRound: false); return false; } PlayerControllerB localPlayerController = instance.localPlayerController; DissonanceComms voiceChatModule = instance.voiceChatModule; bool flag = (Object)(object)localPlayerController != (Object)null; ulong localClientId = 0uL; ulong localPlayerSlotId = 0uL; bool isLocalPlayerDead = false; bool isLocalPlayerSpectating = false; PlayerControllerB val = null; if ((Object)(object)localPlayerController != (Object)null) { localClientId = localPlayerController.actualClientId; localPlayerSlotId = localPlayerController.playerClientId; isLocalPlayerDead = localPlayerController.isPlayerDead; isLocalPlayerSpectating = localPlayerController.hasBegunSpectating; val = localPlayerController.spectatedPlayerScript; } List list = new List(); if (instance.allPlayerScripts != null) { PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if (!((Object)(object)val2 == (Object)null)) { bool flag2 = flag && (Object)(object)val2 == (Object)(object)localPlayerController; if ((!flag2 || includeLocalPlayer) && (flag2 || includeRemotePlayers)) { list.Add(BuildPlayerSnapshot(val2, flag2, (Object)(object)val != (Object)null && (Object)(object)val2 == (Object)(object)val, includeAudioSourceDiagnostics, includeWalkieDiagnostics)); } } } } snapshot = new VoiceDiagnosticsSnapshot(hasRound: true, flag, (Object)(object)voiceChatModule != (Object)null, ((Object)(object)voiceChatModule != (Object)null) ? SafeString(voiceChatModule.LocalPlayerName) : string.Empty, (Object)(object)voiceChatModule != (Object)null && voiceChatModule.IsMuted, (Object)(object)voiceChatModule != (Object)null && voiceChatModule.IsDeafened, localClientId, localPlayerSlotId, isLocalPlayerDead, isLocalPlayerSpectating, ((Object)(object)val != (Object)null) ? new ulong?(val.actualClientId) : null, ((Object)(object)val != (Object)null) ? new ulong?(val.playerClientId) : null, includeAudioSourceDiagnostics, includeWalkieDiagnostics, list.AsReadOnly(), DateTime.UtcNow.Ticks); return true; } private static VoiceDiagnosticsSnapshot EmptySnapshot(bool hasRound) { return new VoiceDiagnosticsSnapshot(hasRound, hasLocalPlayer: false, hasVoiceChatModule: false, string.Empty, voiceChatMuted: false, voiceChatDeafened: false, 0uL, 0uL, isLocalPlayerDead: false, isLocalPlayerSpectating: false, null, null, includeAudioSourceDiagnostics: false, includeWalkieDiagnostics: false, Array.Empty(), DateTime.UtcNow.Ticks); } private static PlayerVoiceDiagnosticsSnapshot BuildPlayerSnapshot(PlayerControllerB player, bool isLocal, bool isSpectatedTarget, bool includeAudioSourceDiagnostics, bool includeWalkieDiagnostics) { VoicePlayerState voicePlayerState = player.voicePlayerState; AudioSource val = (includeAudioSourceDiagnostics ? player.currentVoiceChatAudioSource : null); PlayerVoiceIngameSettings val2 = (includeAudioSourceDiagnostics ? player.currentVoiceChatIngameSettings : null); AudioMixerGroup val3 = (((Object)(object)val != (Object)null) ? val.outputAudioMixerGroup : null); return new PlayerVoiceDiagnosticsSnapshot(player.playerClientId, player.actualClientId, SafeString(player.playerUsername), (voicePlayerState != null) ? SafeString(voicePlayerState.Name) : string.Empty, isLocal, isSpectatedTarget, player.isPlayerControlled, player.isPlayerDead, voicePlayerState != null, voicePlayerState != null && voicePlayerState.IsConnected, voicePlayerState != null && voicePlayerState.IsSpeaking, voicePlayerState != null && voicePlayerState.IsLocallyMuted, (voicePlayerState != null) ? voicePlayerState.Amplitude : 0f, (voicePlayerState != null) ? voicePlayerState.Volume : 0f, (Object)(object)val != (Object)null, (Object)(object)val != (Object)null && val.isPlaying, (Object)(object)val != (Object)null && val.mute, ((Object)(object)val != (Object)null) ? val.volume : 0f, ((Object)(object)val != (Object)null) ? val.spatialBlend : 0f, ((Object)(object)val3 != (Object)null) ? SafeString(((Object)val3).name) : string.Empty, (Object)(object)val2 != (Object)null, (Object)(object)val2 != (Object)null && val2.set2D, ((Object)(object)val2 != (Object)null && (Object)(object)val2._playbackComponent != (Object)null) ? SafeString(val2._playbackComponent.PlayerName) : string.Empty, includeWalkieDiagnostics && player.holdingWalkieTalkie, includeWalkieDiagnostics && player.speakingToWalkieTalkie, includeWalkieDiagnostics && player.voiceMuffledByEnemy); } private static string SafeString(string? value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } return string.Empty; } } public static class PlayerDisplayNameRules { private const string PlayerNumberPrefix = "Player#"; public static bool TryNormalize(string? playerUsername, out string displayName) { if (string.IsNullOrWhiteSpace(playerUsername)) { displayName = string.Empty; return false; } string text = playerUsername.Trim(); if (IsGenericPlayerNumber(text)) { displayName = string.Empty; return false; } displayName = text; return displayName.Length > 0; } public static bool IsGenericPlayerNumber(string displayName) { if (string.IsNullOrWhiteSpace(displayName)) { return false; } string text = RemoveWhitespace(displayName); if (!text.StartsWith("Player#", StringComparison.OrdinalIgnoreCase) || text.Length == "Player#".Length) { return false; } for (int i = "Player#".Length; i < text.Length; i++) { if (!char.IsDigit(text[i])) { return false; } } return true; } private static string RemoveWhitespace(string value) { char[] array = new char[value.Length]; int num = 0; foreach (char c in value) { if (!char.IsWhiteSpace(c)) { array[num] = c; num++; } } return new string(array, 0, num); } } public readonly struct SpectatorDisconnectTargetSwitchContext { public bool IsLocalPlayerDead { get; } public bool HasSpectateCamera { get; } public bool GameOverOverrideActive { get; } public bool HasCurrentTarget { get; } public bool CurrentTargetIsValid { get; } public bool CurrentTargetDisconnected { get; } public bool HasReplacementTarget { get; } public SpectatorDisconnectTargetSwitchContext(bool isLocalPlayerDead, bool hasSpectateCamera, bool gameOverOverrideActive, bool hasCurrentTarget, bool currentTargetIsValid, bool currentTargetDisconnected, bool hasReplacementTarget) { IsLocalPlayerDead = isLocalPlayerDead; HasSpectateCamera = hasSpectateCamera; GameOverOverrideActive = gameOverOverrideActive; HasCurrentTarget = hasCurrentTarget; CurrentTargetIsValid = currentTargetIsValid; CurrentTargetDisconnected = currentTargetDisconnected; HasReplacementTarget = hasReplacementTarget; } } public enum SpectatorTargetAnchorSource { None, PlayerRoot, LowerSpine, GlobalHead } public static class SpectatorTargetAnchorSelectionRules { public static SpectatorTargetAnchorSource Resolve(bool hasPlayerRoot, bool hasLowerSpine, bool hasGlobalHead) { if (hasPlayerRoot) { return SpectatorTargetAnchorSource.PlayerRoot; } if (hasLowerSpine) { return SpectatorTargetAnchorSource.LowerSpine; } if (hasGlobalHead) { return SpectatorTargetAnchorSource.GlobalHead; } return SpectatorTargetAnchorSource.None; } } } namespace EnhancedSpectator.Features { public sealed class FeatureBootstrapper : IDisposable { private readonly List _features = new List(); private readonly FeatureRuntimeDispatchLists _runtimeDispatchLists = new FeatureRuntimeDispatchLists(); private bool _initialized; public FeatureBootstrapper(EnhancedSpectatorConfig config) { EnhancedSpectatorConfig config2 = config; base..ctor(); if (config2.EnableSpectatorModule.Value) { IGameSpectatorAdapter gameSpectatorAdapter = new LethalCompanySpectatorAdapter(); SpectatorFreecamSettings freecamSettings = new SpectatorFreecamSettings(config2); SpectatorModule spectatorModule = new SpectatorModule(gameSpectatorAdapter, freecamSettings); _features.Add(spectatorModule); _runtimeDispatchLists.AddTickable(spectatorModule); _runtimeDispatchLists.AddLateTickable(spectatorModule); _runtimeDispatchLists.AddCameraPreCullTickable(spectatorModule); SpectatorDisconnectTargetSwitchService spectatorDisconnectTargetSwitchService = new SpectatorDisconnectTargetSwitchService(new LethalCompanySpectatorTargetSwitchAdapter()); _features.Add(spectatorDisconnectTargetSwitchService); _runtimeDispatchLists.AddTickable(spectatorDisconnectTargetSwitchService); if (config2.EnableNetworking.Value) { LethalCompanyVoiceActivityProvider voiceActivityProvider = new LethalCompanyVoiceActivityProvider(); EnhancedSpectatorNetworkService networkService = new EnhancedSpectatorNetworkService(config2, spectatorModule, spectatorModule, spectatorModule, voiceActivityProvider, new UnityNetcodeMessagingTransport(() => config2.DebugNetworkMessages.Value)); NetworkingModule networkingModule = new NetworkingModule(networkService); _features.Add(networkingModule); _runtimeDispatchLists.AddTickable(networkingModule); ConnectedPlayerStateRepairModule connectedPlayerStateRepairModule = new ConnectedPlayerStateRepairModule(config2, networkService, new LethalCompanyConnectedPlayerStateRepairAdapter()); _features.Add(connectedPlayerStateRepairModule); _runtimeDispatchLists.AddTickable(connectedPlayerStateRepairModule); SpectatorPresenceService spectatorPresenceService = new SpectatorPresenceService(config2, gameSpectatorAdapter, networkService); SpectatorPresenceModule spectatorPresenceModule = new SpectatorPresenceModule(spectatorPresenceService); _features.Add(spectatorPresenceModule); _runtimeDispatchLists.AddTickable(spectatorPresenceModule); SpectatorVoiceRoutingModule spectatorVoiceRoutingModule = new SpectatorVoiceRoutingModule(new SpectatorVoiceRoutingService(config2, networkService, new LethalCompanySpectatorVoiceRoutingAdapter(networkService, () => ModLog.IsDebugEnabled && config2.EnableDebugLogging.Value && config2.DebugSpectatorVoiceRouting.Value))); _features.Add(spectatorVoiceRoutingModule); _runtimeDispatchLists.AddLateTickable(spectatorVoiceRoutingModule); if (config2.EnableFloatingHeadVisuals.Value) { FloatingHeadModule floatingHeadModule = new FloatingHeadModule(new FloatingHeadVisualService(config2, spectatorPresenceService, voiceActivityProvider, networkService, new LethalCompanyDetachedHeadVisualSourceAdapter(), new FloatingHeadPlacementService(gameSpectatorAdapter), new PlaceholderHeadVisualFactory())); _features.Add(floatingHeadModule); _runtimeDispatchLists.AddLateTickable(floatingHeadModule); _runtimeDispatchLists.AddCameraPreCullTickable(floatingHeadModule); _runtimeDispatchLists.AddGuiTickable(floatingHeadModule); } } } if (config2.EnableModelInspection.Value) { ModelInspectionModule modelInspectionModule = new ModelInspectionModule(config2, new PlayerModelInspectionService(config2, new LethalCompanyPlayerModelInspectionAdapter())); _features.Add(modelInspectionModule); _runtimeDispatchLists.AddTickable(modelInspectionModule); } if (config2.EnableRuntimeHeadSourceInspection.Value) { DeadBodyHeadSourceInspectionModule deadBodyHeadSourceInspectionModule = new DeadBodyHeadSourceInspectionModule(config2, new DeadBodyHeadSourceInspectionService(config2, new LethalCompanyDeadBodyHeadSourceInspectionAdapter())); _features.Add(deadBodyHeadSourceInspectionModule); _runtimeDispatchLists.AddTickable(deadBodyHeadSourceInspectionModule); } if (config2.EnableVoiceDiagnostics.Value) { VoiceDiagnosticsModule voiceDiagnosticsModule = new VoiceDiagnosticsModule(config2, new VoiceDiagnosticsService(config2, new LethalCompanyVoiceDiagnosticsAdapter())); _features.Add(voiceDiagnosticsModule); _runtimeDispatchLists.AddTickable(voiceDiagnosticsModule); } } public void Initialize() { if (_initialized) { return; } foreach (IFeatureModule feature in _features) { feature.Initialize(); } _initialized = true; ModLog.Debug("Feature modules initialized."); } public void Tick() { if (_initialized) { _runtimeDispatchLists.TickAll(); } } public void LateTick() { if (_initialized) { _runtimeDispatchLists.LateTickAll(); } } public void CameraPreCullTick(Camera camera) { if (_initialized) { _runtimeDispatchLists.CameraPreCullTickAll(camera); } } public void GuiTick() { if (_initialized) { _runtimeDispatchLists.GuiTickAll(); } } public void Dispose() { if (_initialized) { for (int num = _features.Count - 1; num >= 0; num--) { _features[num].Dispose(); } _initialized = false; ModLog.Debug("Feature modules disposed."); } } } public sealed class FeatureRuntimeDispatchLists { private readonly List _tickables = new List(); private readonly List _lateTickables = new List(); private readonly List _cameraPreCullTickables = new List(); private readonly List _guiTickables = new List(); public IReadOnlyList Tickables => _tickables; public IReadOnlyList LateTickables => _lateTickables; public IReadOnlyList CameraPreCullTickables => _cameraPreCullTickables; public IReadOnlyList GuiTickables => _guiTickables; public void AddTickable(IRuntimeTickable tickable) { if (tickable == null) { throw new ArgumentNullException("tickable"); } _tickables.Add(tickable); } public void AddLateTickable(IRuntimeLateTickable lateTickable) { if (lateTickable == null) { throw new ArgumentNullException("lateTickable"); } _lateTickables.Add(lateTickable); } public void AddCameraPreCullTickable(IRuntimeCameraPreCullTickable cameraPreCullTickable) { if (cameraPreCullTickable == null) { throw new ArgumentNullException("cameraPreCullTickable"); } _cameraPreCullTickables.Add(cameraPreCullTickable); } public void AddGuiTickable(IRuntimeGuiTickable guiTickable) { if (guiTickable == null) { throw new ArgumentNullException("guiTickable"); } _guiTickables.Add(guiTickable); } public void TickAll() { for (int i = 0; i < _tickables.Count; i++) { _tickables[i].Tick(); } } public void LateTickAll() { for (int i = 0; i < _lateTickables.Count; i++) { _lateTickables[i].LateTick(); } } public void CameraPreCullTickAll(Camera camera) { for (int i = 0; i < _cameraPreCullTickables.Count; i++) { _cameraPreCullTickables[i].CameraPreCullTick(camera); } } public void GuiTickAll() { for (int i = 0; i < _guiTickables.Count; i++) { _guiTickables[i].GuiTick(); } } } public interface IFeatureModule : IDisposable { void Initialize(); } } namespace EnhancedSpectator.Features.VoiceRouting { public static class SpectatorVoiceDistanceAttenuation { public static float CalculateVolume(float baseVolume, bool attenuationEnabled, float distance, float minDistance, float maxDistance, float rolloffPower, float minimumVolume) { float num = Clamp01(baseVolume); if (!attenuationEnabled) { return num; } float num2 = MathF.Max(0f, minDistance); float num3 = MathF.Max(num2 + 0.01f, maxDistance); float num4 = MathF.Max(0f, distance); float y = MathF.Max(0.01f, rolloffPower); float num5 = Clamp01(minimumVolume); if (num4 <= num2) { return num; } if (num4 >= num3) { return num * num5; } float t = MathF.Pow((num4 - num2) / (num3 - num2), y); float num6 = Lerp(1f, num5, t); return num * num6; } private static float Clamp01(float value) { if (value <= 0f) { return 0f; } if (!(value >= 1f)) { return value; } return 1f; } private static float Lerp(float from, float to, float t) { return from + (to - from) * Clamp01(t); } } public readonly struct SpectatorVoicePlaybackSettings { public float Volume { get; } public bool UseRemotePosePosition { get; } public bool EnableDistanceAttenuation { get; } public float MinDistance { get; } public float MaxDistance { get; } public float RolloffPower { get; } public float MinimumVolume { get; } public bool FallbackTo2DWhenPoseMissing { get; } public SpectatorVoicePlaybackSettings(float volume, bool useRemotePosePosition, bool enableDistanceAttenuation, float minDistance, float maxDistance, float rolloffPower, float minimumVolume, bool fallbackTo2DWhenPoseMissing) { Volume = volume; UseRemotePosePosition = useRemotePosePosition; EnableDistanceAttenuation = enableDistanceAttenuation; MinDistance = minDistance; MaxDistance = maxDistance; RolloffPower = rolloffPower; MinimumVolume = minimumVolume; FallbackTo2DWhenPoseMissing = fallbackTo2DWhenPoseMissing; } } public sealed class SpectatorVoicePlayerLookupCache where TPlayer : class { private readonly Dictionary _byClientId = new Dictionary(); private readonly Dictionary _bySlotId = new Dictionary(); public void Store(TPlayer player, ulong clientId, ulong slotId) { if (player == null) { throw new ArgumentNullException("player"); } if (!_byClientId.ContainsKey(clientId)) { _byClientId.Add(clientId, player); } if (!_bySlotId.ContainsKey(slotId)) { _bySlotId.Add(slotId, player); } } public bool TryGet(ulong clientId, ulong slotId, out TPlayer player) { if (_byClientId.TryGetValue(clientId, out var value)) { player = value; return true; } if (_bySlotId.TryGetValue(slotId, out var value2)) { player = value2; return true; } player = null; return false; } public void Clear() { _byClientId.Clear(); _bySlotId.Clear(); } } public sealed class SpectatorVoiceRouteDiagnosticLimiter { private readonly struct LastDiagnosticState { public int Frame { get; } public bool PoseAvailable { get; } public bool FallbackTo2D { get; } public float Distance { get; } public float FinalVolume { get; } public float SpatialBlend { get; } public bool Set2D { get; } public LastDiagnosticState(int frame, bool poseAvailable, bool fallbackTo2D, float distance, float finalVolume, float spatialBlend, bool set2D) { Frame = frame; PoseAvailable = poseAvailable; FallbackTo2D = fallbackTo2D; Distance = distance; FinalVolume = finalVolume; SpatialBlend = spatialBlend; Set2D = set2D; } public bool IsMeaningfullyDifferent(LastDiagnosticState other, int minimumIntervalFrames) { if (other.Frame - Frame < minimumIntervalFrames && PoseAvailable == other.PoseAvailable && FallbackTo2D == other.FallbackTo2D && Set2D == other.Set2D && !(MathF.Abs(SpatialBlend - other.SpatialBlend) >= 0.05f) && !(MathF.Abs(FinalVolume - other.FinalVolume) >= 0.1f)) { return MathF.Abs(Distance - other.Distance) >= 2f; } return true; } } private const float DistanceDeltaThreshold = 2f; private const float VolumeDeltaThreshold = 0.1f; private const float SpatialBlendDeltaThreshold = 0.05f; private readonly Dictionary _lastStates = new Dictionary(); private readonly int _minimumIntervalFrames; public SpectatorVoiceRouteDiagnosticLimiter(int minimumIntervalFrames = 120) { _minimumIntervalFrames = Math.Max(1, minimumIntervalFrames); } public bool ShouldLog(ulong spectatorClientId, int frame, bool poseAvailable, bool fallbackTo2D, float distance, float finalVolume, float spatialBlend, bool set2D) { LastDiagnosticState lastDiagnosticState = new LastDiagnosticState(frame, poseAvailable, fallbackTo2D, distance, finalVolume, spatialBlend, set2D); if (!_lastStates.TryGetValue(spectatorClientId, out var value) || value.IsMeaningfullyDifferent(lastDiagnosticState, _minimumIntervalFrames)) { _lastStates[spectatorClientId] = lastDiagnosticState; return true; } return false; } public void Clear(ulong spectatorClientId) { _lastStates.Remove(spectatorClientId); } } public sealed class SpectatorVoiceRoutingModule : IFeatureModule, IDisposable, IRuntimeLateTickable { private readonly SpectatorVoiceRoutingService _service; private bool _initialized; public SpectatorVoiceRoutingModule(SpectatorVoiceRoutingService service) { _service = service ?? throw new ArgumentNullException("service"); } public void Initialize() { _initialized = true; } public void LateTick() { if (_initialized) { _service.LateTick(); } } public void Dispose() { if (_initialized) { _service.Dispose(); _initialized = false; } } } public static class SpectatorVoiceRoutingRules { public static bool ShouldRouteToLocalTarget(bool featureEnabled, bool hasLocalPlayer, bool isLocalPlayerDead, RemoteSpectatorInfo spectator) { if (featureEnabled && hasLocalPlayer && !isLocalPlayerDead) { return spectator.IsWatchingLocalPlayer; } return false; } public static bool ShouldRouteToLocalPlayer(bool featureEnabled, bool hasLocalPlayer, bool isLocalPlayerDead, bool isRemoteSpectating, bool isWatchingLocalPlayer, SpectatorVoiceAudienceMode audienceMode) { if (!featureEnabled || !hasLocalPlayer || !isRemoteSpectating) { return false; } return audienceMode switch { SpectatorVoiceAudienceMode.WatchedTargetOnly => isWatchingLocalPlayer, SpectatorVoiceAudienceMode.AllModdedPlayers => true, SpectatorVoiceAudienceMode.AliveModdedPlayersOnly => !isLocalPlayerDead, SpectatorVoiceAudienceMode.DeadModdedPlayersOnly => isLocalPlayerDead, _ => false, }; } } public sealed class SpectatorVoiceRoutingService : IDisposable { private readonly struct RouteSkipDiagnostic { public int Frame { get; } public string Reason { get; } public RouteSkipDiagnostic(int frame, string reason) { Frame = frame; Reason = reason; } } private const int RouteSkipDebugIntervalFrames = 120; private readonly EnhancedSpectatorConfig _config; private readonly IEnhancedSpectatorNetworkService _networkService; private readonly IGameSpectatorVoiceRoutingAdapter _adapter; private readonly HashSet _activeRoutes = new HashSet(); private readonly Dictionary _activeSlots = new Dictionary(); private readonly HashSet _desiredRoutes = new HashSet(); private readonly List _routesToClear = new List(); private readonly List _remoteTargets = new List(); private readonly Dictionary _lastRouteSkips = new Dictionary(); private bool _disposed; public SpectatorVoiceRoutingService(EnhancedSpectatorConfig config, IEnhancedSpectatorNetworkService networkService, IGameSpectatorVoiceRoutingAdapter adapter) { _config = config ?? throw new ArgumentNullException("config"); _networkService = networkService ?? throw new ArgumentNullException("networkService"); _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public void LateTick() { if (_disposed) { return; } if (!_config.EnableSpectatorVoiceToTarget.Value || !RuntimeConnectionState.CanUseModNetworking(out string _) || !_adapter.TryGetLocalVoiceReceiverState(out var hasLocalPlayer, out var isLocalPlayerDead, out var localClientId, out var localPlayerSlotId)) { ClearAllRoutes(); return; } if (!hasLocalPlayer || !_networkService.IsNetworkAvailable || !_networkService.IsTargetSyncEnabled) { ClearAllRoutes(); return; } _desiredRoutes.Clear(); _networkService.CopyRemoteSpectatorTargetsTo(_remoteTargets); if (_remoteTargets.Count == 0) { ClearRoutesNotIn(_desiredRoutes); return; } SpectatorVoiceAudienceMode value = _config.SpectatorVoiceAudienceMode.Value; SpectatorVoicePlaybackSettings settings = default(SpectatorVoicePlaybackSettings); bool flag = false; foreach (SpectatorTargetState remoteTarget in _remoteTargets) { if (remoteTarget.LocalClientId == localClientId) { continue; } bool isWatchingLocalPlayer = RemoteSpectatorVisibilityRules.IsWatchingLocalPlayer(remoteTarget, localClientId, localPlayerSlotId); if (!SpectatorVoiceRoutingRules.ShouldRouteToLocalPlayer(featureEnabled: true, hasLocalPlayer, isLocalPlayerDead, remoteTarget.IsSpectating, isWatchingLocalPlayer, value)) { continue; } if (!IsRemoteVoiceRoutingPeer(remoteTarget.LocalClientId)) { DebugRouteSkipped(remoteTarget.LocalClientId, "remote peer did not advertise spectator voice routing capability"); continue; } SpectatorPoseState poseState = TryGetMatchingPose(remoteTarget); if (!flag) { settings = CreatePlaybackSettings(); flag = true; } if (_adapter.TryApplySpectatorVoiceRoute(remoteTarget.LocalClientId, remoteTarget.LocalPlayerSlotId, poseState, settings, out string reason2)) { _desiredRoutes.Add(remoteTarget.LocalClientId); _activeSlots[remoteTarget.LocalClientId] = remoteTarget.LocalPlayerSlotId; _lastRouteSkips.Remove(remoteTarget.LocalClientId); if (_activeRoutes.Add(remoteTarget.LocalClientId)) { DebugRouteEnabled(remoteTarget, value); } } else { DebugRouteSkipped(remoteTarget.LocalClientId, reason2); if (_activeRoutes.Contains(remoteTarget.LocalClientId)) { ClearRoute(remoteTarget.LocalClientId, reason2); } } } ClearRoutesNotIn(_desiredRoutes); } public void Dispose() { if (!_disposed) { ClearAllRoutes(); _disposed = true; } } private void ClearRoutesNotIn(HashSet desiredRoutes) { _routesToClear.Clear(); foreach (ulong activeRoute in _activeRoutes) { if (!desiredRoutes.Contains(activeRoute)) { _routesToClear.Add(activeRoute); } } foreach (ulong item in _routesToClear) { ClearRoute(item, "presence lost"); } _routesToClear.Clear(); } private void ClearAllRoutes() { if (_activeRoutes.Count == 0) { _activeSlots.Clear(); _lastRouteSkips.Clear(); _remoteTargets.Clear(); _adapter.ClearCachedVoiceRouteLookups(); return; } _routesToClear.Clear(); _routesToClear.AddRange(_activeRoutes); foreach (ulong item in _routesToClear) { ClearRoute(item, "routing disabled or lifecycle unavailable"); } _routesToClear.Clear(); _activeSlots.Clear(); _lastRouteSkips.Clear(); _remoteTargets.Clear(); _adapter.ClearCachedVoiceRouteLookups(); } private void ClearRoute(ulong clientId, string reason) { if (_activeRoutes.Remove(clientId)) { ulong value; ulong spectatorSlotId = (_activeSlots.TryGetValue(clientId, out value) ? value : clientId); _activeSlots.Remove(clientId); _adapter.ClearSpectatorVoiceRoute(clientId, spectatorSlotId); _lastRouteSkips.Remove(clientId); if (_activeRoutes.Count == 0) { _adapter.ClearCachedVoiceRouteLookups(); } DebugRouteCleared(clientId, reason); } } private bool ShouldLogDebug() { if (ModLog.IsDebugEnabled && _config.EnableDebugLogging.Value) { return _config.DebugSpectatorVoiceRouting.Value; } return false; } private void DebugRouteEnabled(SpectatorTargetState remoteTarget, SpectatorVoiceAudienceMode audienceMode) { if (ShouldLogDebug()) { ModLog.Debug($"Spectator voice route enabled: spectatorClient={remoteTarget.LocalClientId}, spectatorSlot={remoteTarget.LocalPlayerSlotId}, audienceMode={audienceMode}."); } } private void DebugRouteCleared(ulong clientId, string reason) { if (ShouldLogDebug()) { ModLog.Debug($"Spectator voice route cleared: spectatorClient={clientId}, reason={reason}."); } } private void DebugRouteSkipped(ulong clientId, string reason) { if (ShouldLogDebug()) { int frameCount = Time.frameCount; if (!_lastRouteSkips.TryGetValue(clientId, out var value) || !(value.Reason == reason) || frameCount - value.Frame >= 120) { _lastRouteSkips[clientId] = new RouteSkipDiagnostic(frameCount, reason); ModLog.Debug($"Spectator voice route skipped: spectatorClient={clientId}, reason={reason}."); } } } private bool IsRemoteVoiceRoutingPeer(ulong clientId) { if (_networkService.TryGetPeerCapability(clientId, out ModPeerCapability capability)) { return ModPeerCapabilityRules.SupportsCurrentSpectatorVoiceToTarget(capability); } return false; } private SpectatorVoicePlaybackSettings CreatePlaybackSettings() { return new SpectatorVoicePlaybackSettings(Mathf.Clamp01(_config.SpectatorVoiceToTargetVolume.Value), _config.SpectatorVoiceUseRemotePosePosition.Value, _config.SpectatorVoiceEnableDistanceAttenuation.Value, _config.SpectatorVoiceMinDistance.Value, _config.SpectatorVoiceMaxDistance.Value, _config.SpectatorVoiceRolloffPower.Value, _config.SpectatorVoiceMinimumVolume.Value, _config.SpectatorVoiceFallbackTo2DWhenPoseMissing.Value); } private SpectatorPoseState? TryGetMatchingPose(SpectatorTargetState remoteTarget) { if (!_networkService.TryGetRemoteSpectatorPose(remoteTarget.LocalClientId, out SpectatorPoseState state)) { return null; } if (!state.IsSpectating || state.TargetClientId != remoteTarget.TargetClientId || state.TargetPlayerSlotId != remoteTarget.TargetPlayerSlotId) { return null; } return state; } } public static class SpectatorVoiceSpatializationRules { public static Vector3 ResolvePlaybackSourcePosition(Vector3 remotePosePosition, Vector3 desiredListenerPosition, Quaternion desiredListenerRotation, Vector3 actualListenerPosition, Quaternion actualListenerRotation) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) Vector3 value = RotateByQuaternion(remotePosePosition - desiredListenerPosition, Inverse(desiredListenerRotation)); return actualListenerPosition + RotateByQuaternion(value, actualListenerRotation); } private static Quaternion Inverse(Quaternion value) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) float num = value.x * value.x + value.y * value.y + value.z * value.z + value.w * value.w; if (num <= 1E-06f) { return Quaternion.identity; } float num2 = 1f / num; return new Quaternion((0f - value.x) * num2, (0f - value.y) * num2, (0f - value.z) * num2, value.w * num2); } private static Vector3 RotateByQuaternion(Vector3 value, Quaternion rotation) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) float num = rotation.x * 2f; float num2 = rotation.y * 2f; float num3 = rotation.z * 2f; float num4 = rotation.x * num; float num5 = rotation.y * num2; float num6 = rotation.z * num3; float num7 = rotation.x * num2; float num8 = rotation.x * num3; float num9 = rotation.y * num3; float num10 = rotation.w * num; float num11 = rotation.w * num2; float num12 = rotation.w * num3; return new Vector3((1f - (num5 + num6)) * value.x + (num7 - num12) * value.y + (num8 + num11) * value.z, (num7 + num12) * value.x + (1f - (num4 + num6)) * value.y + (num9 - num10) * value.z, (num8 - num11) * value.x + (num9 + num10) * value.y + (1f - (num4 + num5)) * value.z); } } } namespace EnhancedSpectator.Features.VoiceDiagnostics { public sealed class PlayerVoiceDiagnosticsSnapshot { public ulong PlayerClientId { get; } public ulong ActualClientId { get; } public string PlayerName { get; } public string VoicePlayerName { get; } public bool IsLocalPlayer { get; } public bool IsSpectatedTarget { get; } public bool IsPlayerControlled { get; } public bool IsPlayerDead { get; } public bool HasVoicePlayerState { get; } public bool VoicePlayerIsConnected { get; } public bool VoicePlayerIsSpeaking { get; } public bool VoicePlayerIsLocallyMuted { get; } public float VoiceAmplitude { get; } public float VoiceVolume { get; } public bool HasCurrentVoiceAudioSource { get; } public bool VoiceAudioIsPlaying { get; } public bool VoiceAudioMuted { get; } public float VoiceAudioVolume { get; } public float VoiceAudioSpatialBlend { get; } public string VoiceAudioMixerName { get; } public bool HasCurrentVoiceIngameSettings { get; } public bool VoiceIngameSettingsSet2D { get; } public string VoicePlaybackPlayerName { get; } public bool HoldingWalkieTalkie { get; } public bool SpeakingToWalkieTalkie { get; } public bool VoiceMuffledByEnemy { get; } public PlayerVoiceDiagnosticsSnapshot(ulong playerClientId, ulong actualClientId, string playerName, string voicePlayerName, bool isLocalPlayer, bool isSpectatedTarget, bool isPlayerControlled, bool isPlayerDead, bool hasVoicePlayerState, bool voicePlayerIsConnected, bool voicePlayerIsSpeaking, bool voicePlayerIsLocallyMuted, float voiceAmplitude, float voiceVolume, bool hasCurrentVoiceAudioSource, bool voiceAudioIsPlaying, bool voiceAudioMuted, float voiceAudioVolume, float voiceAudioSpatialBlend, string voiceAudioMixerName, bool hasCurrentVoiceIngameSettings, bool voiceIngameSettingsSet2D, string voicePlaybackPlayerName, bool holdingWalkieTalkie, bool speakingToWalkieTalkie, bool voiceMuffledByEnemy) { PlayerClientId = playerClientId; ActualClientId = actualClientId; PlayerName = playerName; VoicePlayerName = voicePlayerName; IsLocalPlayer = isLocalPlayer; IsSpectatedTarget = isSpectatedTarget; IsPlayerControlled = isPlayerControlled; IsPlayerDead = isPlayerDead; HasVoicePlayerState = hasVoicePlayerState; VoicePlayerIsConnected = voicePlayerIsConnected; VoicePlayerIsSpeaking = voicePlayerIsSpeaking; VoicePlayerIsLocallyMuted = voicePlayerIsLocallyMuted; VoiceAmplitude = voiceAmplitude; VoiceVolume = voiceVolume; HasCurrentVoiceAudioSource = hasCurrentVoiceAudioSource; VoiceAudioIsPlaying = voiceAudioIsPlaying; VoiceAudioMuted = voiceAudioMuted; VoiceAudioVolume = voiceAudioVolume; VoiceAudioSpatialBlend = voiceAudioSpatialBlend; VoiceAudioMixerName = voiceAudioMixerName; HasCurrentVoiceIngameSettings = hasCurrentVoiceIngameSettings; VoiceIngameSettingsSet2D = voiceIngameSettingsSet2D; VoicePlaybackPlayerName = voicePlaybackPlayerName; HoldingWalkieTalkie = holdingWalkieTalkie; SpeakingToWalkieTalkie = speakingToWalkieTalkie; VoiceMuffledByEnemy = voiceMuffledByEnemy; } } public sealed class VoiceDiagnosticsModule : IFeatureModule, IDisposable, IRuntimeTickable { private readonly EnhancedSpectatorConfig _config; private readonly VoiceDiagnosticsService _diagnosticsService; private bool _initialized; public VoiceDiagnosticsModule(EnhancedSpectatorConfig config, VoiceDiagnosticsService diagnosticsService) { _config = config ?? throw new ArgumentNullException("config"); _diagnosticsService = diagnosticsService ?? throw new ArgumentNullException("diagnosticsService"); } public void Initialize() { if (!_initialized) { _initialized = true; ModLog.Debug("Voice diagnostics module initialized."); } } public void Tick() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) if (_initialized && _config.EnableVoiceDiagnostics.Value && SpectatorInputService.IsKeyPressedThisFrame(_config.VoiceDiagnosticsKey.Value)) { ModLog.Info($"Enhanced Spectator voice diagnostics key pressed: {_config.VoiceDiagnosticsKey.Value}."); _diagnosticsService.InspectOnce(); } } public void Dispose() { if (_initialized) { _initialized = false; ModLog.Debug("Voice diagnostics module disposed."); } } } public static class VoiceDiagnosticsReportFormatter { public static string Build(VoiceDiagnosticsSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Enhanced Spectator voice diagnostics"); stringBuilder.AppendLine($"hasRound={snapshot.HasRound}, hasLocalPlayer={snapshot.HasLocalPlayer}, hasVoiceChatModule={snapshot.HasVoiceChatModule}, localVoiceName={FormatString(snapshot.LocalDissonancePlayerName)}, voiceMuted={snapshot.VoiceChatMuted}, voiceDeafened={snapshot.VoiceChatDeafened}"); stringBuilder.AppendLine($"localClientId={snapshot.LocalClientId}, localSlot={snapshot.LocalPlayerSlotId}, localDead={snapshot.IsLocalPlayerDead}, localSpectating={snapshot.IsLocalPlayerSpectating}, spectatedTargetClient={FormatNullable(snapshot.SpectatedTargetClientId)}, spectatedTargetSlot={FormatNullable(snapshot.SpectatedTargetPlayerSlotId)}"); stringBuilder.AppendLine($"players={snapshot.Players.Count}, includeAudio={snapshot.IncludeAudioSourceDiagnostics}, includeWalkie={snapshot.IncludeWalkieDiagnostics}, timestampTicks={snapshot.TimestampTicks}"); if (snapshot.Players.Count == 0) { stringBuilder.AppendLine("no player voice rows captured"); } foreach (PlayerVoiceDiagnosticsSnapshot player in snapshot.Players) { AppendPlayer(stringBuilder, snapshot, player); } return stringBuilder.ToString(); } private static void AppendPlayer(StringBuilder builder, VoiceDiagnosticsSnapshot snapshot, PlayerVoiceDiagnosticsSnapshot player) { builder.AppendLine($"player slot={player.PlayerClientId} client={player.ActualClientId} name={FormatString(player.PlayerName)} local={player.IsLocalPlayer} target={player.IsSpectatedTarget} controlled={player.IsPlayerControlled} dead={player.IsPlayerDead}"); builder.AppendLine($" voice present={player.HasVoicePlayerState} connected={player.VoicePlayerIsConnected} speaking={player.VoicePlayerIsSpeaking} muted={player.VoicePlayerIsLocallyMuted} amplitude={player.VoiceAmplitude:0.00} volume={player.VoiceVolume:0.00} voiceName={FormatString(player.VoicePlayerName)}"); if (snapshot.IncludeAudioSourceDiagnostics) { builder.AppendLine($" audio present={player.HasCurrentVoiceAudioSource} playing={player.VoiceAudioIsPlaying} muted={player.VoiceAudioMuted} volume={player.VoiceAudioVolume:0.00} spatialBlend={player.VoiceAudioSpatialBlend:0.00} mixer={FormatString(player.VoiceAudioMixerName)} ingameSettings={player.HasCurrentVoiceIngameSettings} set2D={player.VoiceIngameSettingsSet2D} playbackName={FormatString(player.VoicePlaybackPlayerName)}"); } if (snapshot.IncludeWalkieDiagnostics) { builder.AppendLine($" walkie holding={player.HoldingWalkieTalkie} speaking={player.SpeakingToWalkieTalkie} muffled={player.VoiceMuffledByEnemy}"); } } private static string FormatNullable(ulong? value) { if (!value.HasValue) { return "none"; } return value.Value.ToString(); } private static string FormatString(string value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } return "none"; } } public sealed class VoiceDiagnosticsService { private readonly EnhancedSpectatorConfig _config; private readonly IGameVoiceDiagnosticsAdapter _adapter; public VoiceDiagnosticsService(EnhancedSpectatorConfig config, IGameVoiceDiagnosticsAdapter adapter) { _config = config ?? throw new ArgumentNullException("config"); _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public void InspectOnce() { try { if (!RuntimeConnectionState.CanRunLocalDiagnostics(out string reason)) { ModLog.Info("Enhanced Spectator voice diagnostics skipped because runtime state is unsafe: " + reason + "."); return; } bool value = _config.LogLocalVoiceStateOnKey.Value; bool value2 = _config.LogRemoteVoiceStatesOnKey.Value; VoiceDiagnosticsSnapshot snapshot; if (!value && !value2) { ModLog.Info("Enhanced Spectator voice diagnostics skipped: local and remote voice logging are both disabled."); } else if (!_adapter.TryGetVoiceDiagnosticsSnapshot(value, value2, _config.IncludeVoiceAudioSourceDetails.Value, _config.IncludeWalkieVoiceDiagnostics.Value, out snapshot)) { ModLog.Info("Enhanced Spectator voice diagnostics found no current voice state."); } else { ModLog.Info(VoiceDiagnosticsReportFormatter.Build(snapshot)); } } catch (Exception arg) { ModLog.Error($"Enhanced Spectator voice diagnostics failed: {arg}"); } } } public sealed class VoiceDiagnosticsSnapshot { public bool HasRound { get; } public bool HasLocalPlayer { get; } public bool HasVoiceChatModule { get; } public string LocalDissonancePlayerName { get; } public bool VoiceChatMuted { get; } public bool VoiceChatDeafened { get; } public ulong LocalClientId { get; } public ulong LocalPlayerSlotId { get; } public bool IsLocalPlayerDead { get; } public bool IsLocalPlayerSpectating { get; } public ulong? SpectatedTargetClientId { get; } public ulong? SpectatedTargetPlayerSlotId { get; } public bool IncludeAudioSourceDiagnostics { get; } public bool IncludeWalkieDiagnostics { get; } public IReadOnlyList Players { get; } public long TimestampTicks { get; } public VoiceDiagnosticsSnapshot(bool hasRound, bool hasLocalPlayer, bool hasVoiceChatModule, string localDissonancePlayerName, bool voiceChatMuted, bool voiceChatDeafened, ulong localClientId, ulong localPlayerSlotId, bool isLocalPlayerDead, bool isLocalPlayerSpectating, ulong? spectatedTargetClientId, ulong? spectatedTargetPlayerSlotId, bool includeAudioSourceDiagnostics, bool includeWalkieDiagnostics, IReadOnlyList players, long timestampTicks) { HasRound = hasRound; HasLocalPlayer = hasLocalPlayer; HasVoiceChatModule = hasVoiceChatModule; LocalDissonancePlayerName = localDissonancePlayerName; VoiceChatMuted = voiceChatMuted; VoiceChatDeafened = voiceChatDeafened; LocalClientId = localClientId; LocalPlayerSlotId = localPlayerSlotId; IsLocalPlayerDead = isLocalPlayerDead; IsLocalPlayerSpectating = isLocalPlayerSpectating; SpectatedTargetClientId = spectatedTargetClientId; SpectatedTargetPlayerSlotId = spectatedTargetPlayerSlotId; IncludeAudioSourceDiagnostics = includeAudioSourceDiagnostics; IncludeWalkieDiagnostics = includeWalkieDiagnostics; Players = players; TimestampTicks = timestampTicks; } } } namespace EnhancedSpectator.Features.VoiceActivity { public interface IVoiceActivityProvider { bool TryGetVoiceActivity(ulong clientId, ulong slotId, out VoiceActivityState state); } public sealed class LethalCompanyVoiceActivityProvider : IVoiceActivityProvider { private readonly IGameVoiceActivityAdapter _adapter; public LethalCompanyVoiceActivityProvider() : this(new LethalCompanyVoiceActivityAdapter()) { } public LethalCompanyVoiceActivityProvider(IGameVoiceActivityAdapter adapter) { _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public bool TryGetVoiceActivity(ulong clientId, ulong slotId, out VoiceActivityState state) { return _adapter.TryGetVoiceActivity(clientId, slotId, out state); } } public sealed class NoopVoiceActivityProvider : IVoiceActivityProvider { public static NoopVoiceActivityProvider Instance { get; } = new NoopVoiceActivityProvider(); private NoopVoiceActivityProvider() { } public bool TryGetVoiceActivity(ulong clientId, ulong slotId, out VoiceActivityState state) { state = VoiceActivityState.NoData; return false; } } public sealed class VoiceActivityState { public static VoiceActivityState NoData { get; } = new VoiceActivityState(hasData: false, isSpeaking: false, 0f, 0f, 0uL, 0uL, 0L); public bool HasData { get; } public bool IsSpeaking { get; } public float Amplitude { get; } public float Volume { get; } public ulong ClientId { get; } public ulong SlotId { get; } public long TimestampTicks { get; } public VoiceActivityState(bool hasData, bool isSpeaking, float amplitude, float volume, ulong clientId, ulong slotId, long timestampTicks) { HasData = hasData; IsSpeaking = isSpeaking; Amplitude = Mathf.Clamp01(amplitude); Volume = Mathf.Clamp01(volume); ClientId = clientId; SlotId = slotId; TimestampTicks = timestampTicks; } } } namespace EnhancedSpectator.Features.Spectator { public interface ISpectatorPoseStateProvider { bool TryGetCurrentSpectatorPose(out SpectatorPoseState state); } public interface ISpectatorStateService { SpectatorState Current { get; } void Refresh(); } public interface ISpectatorTargetStateProvider { bool TryGetCurrentSpectatorTarget(out SpectatorTargetState state); } public sealed class SpectatorAnchorService { private SpectatorAnchorTargetIdentity _targetIdentity; private bool _hasTargetIdentity; public bool TryUpdate(GameSpectatorSnapshot snapshot, out Transform? anchor, out bool targetChanged) { anchor = snapshot.Anchor; targetChanged = false; if ((Object)(object)anchor == (Object)null || !snapshot.HasSpectatedTarget) { Clear(); return false; } if (!SpectatorAnchorTargetIdentity.TryCreate(snapshot, ((Object)anchor).GetInstanceID(), out var identity)) { Clear(); return false; } targetChanged = _hasTargetIdentity && !_targetIdentity.Equals(identity); _targetIdentity = identity; _hasTargetIdentity = true; return true; } public void Clear() { _targetIdentity = default(SpectatorAnchorTargetIdentity); _hasTargetIdentity = false; } } public readonly struct SpectatorAnchorTargetIdentity : IEquatable { private const byte NoneKind = 0; private const byte SlotKind = 1; private const byte SlotAndClientKind = 2; private const byte AnchorKind = 3; private readonly byte _kind; private readonly ulong _slotId; private readonly ulong _actualClientId; private readonly int _anchorInstanceId; private SpectatorAnchorTargetIdentity(byte kind, ulong slotId, ulong actualClientId, int anchorInstanceId) { _kind = kind; _slotId = slotId; _actualClientId = actualClientId; _anchorInstanceId = anchorInstanceId; } public static bool TryCreate(GameSpectatorSnapshot snapshot, int anchorInstanceId, out SpectatorAnchorTargetIdentity identity) { if (!snapshot.HasSpectatedTarget) { identity = default(SpectatorAnchorTargetIdentity); return false; } if (snapshot.SpectatedPlayerSlotId.HasValue) { ulong value = snapshot.SpectatedPlayerSlotId.Value; if (snapshot.SpectatedPlayerActualClientId.HasValue) { identity = new SpectatorAnchorTargetIdentity(2, value, snapshot.SpectatedPlayerActualClientId.Value, 0); return true; } identity = new SpectatorAnchorTargetIdentity(1, value, 0uL, 0); return true; } identity = new SpectatorAnchorTargetIdentity(3, 0uL, 0uL, anchorInstanceId); return true; } public bool Equals(SpectatorAnchorTargetIdentity other) { if (_kind == other._kind && _slotId == other._slotId && _actualClientId == other._actualClientId) { return _anchorInstanceId == other._anchorInstanceId; } return false; } public override bool Equals(object? obj) { if (obj is SpectatorAnchorTargetIdentity other) { return Equals(other); } return false; } public override int GetHashCode() { return (((((_kind * 397) ^ _slotId.GetHashCode()) * 397) ^ _actualClientId.GetHashCode()) * 397) ^ _anchorInstanceId; } } public sealed class SpectatorCameraState { public bool IsActive { get; internal set; } public bool UserEnabled { get; internal set; } public Vector3 Offset { get; internal set; } public Quaternion Rotation { get; internal set; } = Quaternion.identity; public bool HasWorldPose { get; internal set; } public Vector3 WorldPosition { get; internal set; } public ulong? TargetSlotId { get; internal set; } public ulong? TargetActualClientId { get; internal set; } } public static class SpectatorDisconnectTargetSwitchResultRules { public static bool DidSwitchToValidTarget(bool targetSwitchInvoked, bool targetChanged, bool newTargetIsValid) { return targetSwitchInvoked && targetChanged && newTargetIsValid; } } public static class SpectatorDisconnectTargetSwitchRules { public static bool ShouldAutoSwitch(bool isLocalPlayerDead, bool hasSpectateCamera, bool gameOverOverrideActive, bool hasCurrentTarget, bool currentTargetIsValid, bool currentTargetDisconnected, bool hasReplacementTarget) { return isLocalPlayerDead && hasSpectateCamera && !gameOverOverrideActive && hasCurrentTarget && !currentTargetIsValid && currentTargetDisconnected && hasReplacementTarget; } public static bool IsDisconnectLikeInvalidTarget(bool disconnectedMidGame, bool isPlayerControlled, bool isPlayerDead, bool removedFromClientMap) { if (!(disconnectedMidGame || removedFromClientMap)) { if (!isPlayerControlled) { return !isPlayerDead; } return false; } return true; } } public sealed class SpectatorDisconnectTargetSwitchService : IFeatureModule, IDisposable, IRuntimeTickable { private const float TargetValidationIntervalSeconds = 0.25f; private readonly IGameSpectatorTargetSwitchAdapter _adapter; private NetworkManager? _subscribedNetworkManager; private bool _initialized; private bool _pendingDisconnectSwitch; private float _nextTargetValidationTime; public SpectatorDisconnectTargetSwitchService(IGameSpectatorTargetSwitchAdapter adapter) { _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public void Initialize() { if (!_initialized) { SpectatorLifecycleEvents.Changed += OnSpectatorLifecycleChanged; RefreshDisconnectSubscription(); _initialized = true; } } public void Tick() { if (_initialized) { RefreshDisconnectSubscription(); if (_pendingDisconnectSwitch || !(Time.unscaledTime < _nextTargetValidationTime)) { _pendingDisconnectSwitch = false; _nextTargetValidationTime = Time.unscaledTime + 0.25f; TrySwitchAwayFromInvalidTarget(); } } } public void Dispose() { if (_initialized) { SpectatorLifecycleEvents.Changed -= OnSpectatorLifecycleChanged; UnsubscribeDisconnectCallback(); _pendingDisconnectSwitch = false; _nextTargetValidationTime = 0f; _initialized = false; } } private void OnSpectatorLifecycleChanged(SpectatorLifecycleEventKind kind) { if (kind == SpectatorLifecycleEventKind.Revived) { _pendingDisconnectSwitch = false; } } private void RefreshDisconnectSubscription() { NetworkManager singleton = NetworkManager.Singleton; if (singleton != _subscribedNetworkManager) { UnsubscribeDisconnectCallback(); if (!((Object)(object)singleton == (Object)null)) { singleton.OnClientDisconnectCallback += OnClientDisconnected; _subscribedNetworkManager = singleton; } } } private void UnsubscribeDisconnectCallback() { if (!((Object)(object)_subscribedNetworkManager == (Object)null)) { _subscribedNetworkManager.OnClientDisconnectCallback -= OnClientDisconnected; _subscribedNetworkManager = null; } } private void OnClientDisconnected(ulong clientId) { _pendingDisconnectSwitch = true; } private void TrySwitchAwayFromInvalidTarget() { SpectatorDisconnectTargetSwitchContext context; if (RuntimeConnectionState.ShouldSkipVanillaSpectatorTargetSwitch(out string reason)) { if (ModLog.IsDebugEnabled) { ModLog.Debug("Skipped disconnect spectator target auto-switch: " + reason + "."); } } else if (_adapter.TryGetDisconnectTargetSwitchContext(out context) && SpectatorDisconnectTargetSwitchRules.ShouldAutoSwitch(context.IsLocalPlayerDead, context.HasSpectateCamera, context.GameOverOverrideActive, context.HasCurrentTarget, context.CurrentTargetIsValid, context.CurrentTargetDisconnected, context.HasReplacementTarget) && _adapter.TrySwitchToNextSpectatorTarget()) { ModLog.Debug("Switched spectator target after watched player disconnected."); } } } public sealed class SpectatorFreecamController { private const float MaxPitch = 85f; private const float MinPitch = -85f; private const int TargetSwitchRecoveryFrames = 4; private const int CameraInactiveRecoveryFrames = 3; private readonly IGameSpectatorAdapter _adapter; private readonly SpectatorSnapshotCache _snapshotCache; private readonly SpectatorAnchorService _anchorService; private readonly SpectatorInputService _inputService; private readonly SpectatorFreecamSettings _settings; private readonly SpectatorCameraState _state = new SpectatorCameraState(); private bool _wasSpectating; private bool _hasPose; private bool _recenterRequested; private float _yaw; private float _pitch; private Vector3 _smoothVelocity; private Vector3 _smoothedPosition; private bool _hasSmoothedPosition; private int _lastSmoothingFrame = -1; private int _lastPreCullApplyFrame = -1; private int _nextApplyDebugFrame; private int _nextMenuBlockDebugFrame; private int _targetSwitchGraceUntilFrame = -1; private int _cameraInactiveGraceUntilFrame = -1; private int _nextEligibilityDebugFrame; private SpectatorFreecamIneligibleReason _lastEligibilityDebugReason; private bool _cameraInactiveGraceStarted; public SpectatorCameraState State => _state; public SpectatorFreecamController(IGameSpectatorAdapter adapter, SpectatorSnapshotCache snapshotCache, SpectatorAnchorService anchorService, SpectatorInputService inputService, SpectatorFreecamSettings settings) { _adapter = adapter; _snapshotCache = snapshotCache ?? throw new ArgumentNullException("snapshotCache"); _anchorService = anchorService; _inputService = inputService; _settings = settings; _state.UserEnabled = settings.FreecamDefaultOn; } public void NotifyLifecycleEvent(SpectatorLifecycleEventKind kind) { switch (kind) { case SpectatorLifecycleEventKind.PlayerDied: case SpectatorLifecycleEventKind.CameraSwitched: _hasPose = false; _cameraInactiveGraceUntilFrame = SpectatorFreecamRecoveryPolicy.ExtendGraceUntilFrame(Time.frameCount, 3); break; case SpectatorLifecycleEventKind.SpectatedPlayerEffectsApplied: BeginTargetSwitchRecoveryWindow(); if (_settings.RecenterOnTargetSwitch) { _recenterRequested = true; } break; case SpectatorLifecycleEventKind.GameOverOverrideChanged: if (_settings.DisableDuringGameOverOverride && _adapter.IsGameOverSpectateOverrideActive()) { Deactivate(clearAnchor: false); } break; case SpectatorLifecycleEventKind.Revived: ResetForNonSpectator(); break; } } public void Tick() { try { if (!TryGetEligibleSnapshot(out var _)) { SpectatorVanillaInputGuard.Clear(); return; } bool flag = _adapter.IsLocalQuickMenuOpen(); UpdateVanillaInputGuard(_state.UserEnabled, flag); if (flag) { LogQuickMenuInputBlocked(); return; } if (_inputService.ResetToVanillaPressed) { _state.UserEnabled = false; Deactivate(clearAnchor: false); ModLog.Info("Enhanced spectator freecam disabled until toggled again."); return; } if (_inputService.ToggleFreecamPressed) { _state.UserEnabled = !_state.UserEnabled; _hasPose = false; _recenterRequested = _state.UserEnabled; ModLog.Info(_state.UserEnabled ? "Enhanced spectator freecam enabled." : "Enhanced spectator freecam disabled."); } if (_inputService.RecenterPressed) { _recenterRequested = true; } } catch (Exception ex) { DisableAfterFailure(ex); } } public void LateTick() { try { if (!TryGetEligibleSnapshot(out var snapshot)) { return; } if (!_state.UserEnabled) { Deactivate(clearAnchor: false); return; } if (!_anchorService.TryUpdate(snapshot, out Transform anchor, out bool targetChanged) || (Object)(object)anchor == (Object)null) { if (!TrySoftPause(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot)) { DeactivateWithReason(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot, clearAnchor: true); } return; } Camera spectateCamera = snapshot.SpectateCamera; if (!_hasPose) { InitializePoseFromCamera(spectateCamera, anchor); } if (_recenterRequested || (targetChanged && _settings.RecenterOnTargetSwitch)) { Recenter(spectateCamera, anchor); _recenterRequested = false; } bool flag = _adapter.IsLocalQuickMenuOpen(); UpdateVanillaInputGuard(_state.UserEnabled, flag); if (flag) { LogQuickMenuInputBlocked(); } else { ApplyInput(); } ApplyCameraTransform(spectateCamera, anchor); UpdateState(snapshot); _state.IsActive = true; } catch (Exception ex) { DisableAfterFailure(ex); } } public void CameraPreCullTick(Camera renderingCamera) { try { if ((Object)(object)renderingCamera == (Object)null || !_state.UserEnabled || !_hasPose || !TryGetEligibleSnapshot(out var snapshot)) { return; } Camera spectateCamera = snapshot.SpectateCamera; if ((Object)(object)spectateCamera == (Object)null || (Object)(object)renderingCamera != (Object)(object)spectateCamera || _lastPreCullApplyFrame == Time.frameCount) { return; } if (!_anchorService.TryUpdate(snapshot, out Transform anchor, out bool targetChanged) || (Object)(object)anchor == (Object)null) { if (!TrySoftPause(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot)) { DeactivateWithReason(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot, clearAnchor: true); } return; } if (targetChanged && _settings.RecenterOnTargetSwitch) { Recenter(spectateCamera, anchor); } ApplyCameraTransform(spectateCamera, anchor); _lastPreCullApplyFrame = Time.frameCount; UpdateState(snapshot); } catch (Exception ex) { DisableAfterFailure(ex); } } private bool TryGetEligibleSnapshot(out GameSpectatorSnapshot snapshot) { if (!_settings.EnableEnhancedSpectator || !_settings.EnableFreecam) { DeactivateWithReason(SpectatorFreecamIneligibleReason.FeatureDisabled, GameSpectatorSnapshot.Unavailable, clearAnchor: false); snapshot = GameSpectatorSnapshot.Unavailable; return false; } if (!RuntimeConnectionState.CanRunLocalDiagnostics(out string _)) { snapshot = GameSpectatorSnapshot.Unavailable; if (!TrySoftPause(SpectatorFreecamIneligibleReason.LifecycleUnsafe, snapshot)) { DeactivateWithReason(SpectatorFreecamIneligibleReason.LifecycleUnsafe, snapshot, clearAnchor: false); } return false; } if (!_snapshotCache.TryGetCurrentFrameSnapshot(out snapshot)) { ResetForNonSpectator(); return false; } if (!snapshot.HasRound || !snapshot.HasLocalPlayer || !snapshot.IsLocalPlayerDead) { ResetForNonSpectator(); return false; } if (!_wasSpectating) { EnterSpectatorState(); } if ((Object)(object)snapshot.SpectateCamera == (Object)null || (Object)(object)snapshot.Anchor == (Object)null || !snapshot.HasSpectatedTarget) { if (!TrySoftPause(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot)) { DeactivateWithReason(SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget, snapshot, clearAnchor: true); } return false; } if (!snapshot.IsSpectateCameraActive) { BeginCameraInactiveRecoveryWindow(); if (!TrySoftPause(SpectatorFreecamIneligibleReason.SpectateCameraInactive, snapshot)) { DeactivateWithReason(SpectatorFreecamIneligibleReason.SpectateCameraInactive, snapshot, clearAnchor: false); } return false; } if (snapshot.IsGameOverOverride && _settings.DisableDuringGameOverOverride) { DeactivateWithReason(SpectatorFreecamIneligibleReason.GameOverOverride, snapshot, clearAnchor: false); return false; } _targetSwitchGraceUntilFrame = -1; _cameraInactiveGraceUntilFrame = -1; _cameraInactiveGraceStarted = false; _lastEligibilityDebugReason = SpectatorFreecamIneligibleReason.None; return true; } private void EnterSpectatorState() { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) _wasSpectating = true; _state.UserEnabled = _settings.FreecamDefaultOn; _state.IsActive = false; _hasPose = false; _recenterRequested = _settings.FreecamDefaultOn; _smoothVelocity = Vector3.zero; _hasSmoothedPosition = false; _lastSmoothingFrame = -1; _lastPreCullApplyFrame = -1; _targetSwitchGraceUntilFrame = -1; _cameraInactiveGraceUntilFrame = -1; _cameraInactiveGraceStarted = false; ModLog.Debug("Entered local spectator state."); } private void ResetForNonSpectator() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) bool num = _wasSpectating || _hasPose || _state.IsActive || _state.TargetSlotId.HasValue || _state.TargetActualClientId.HasValue; _wasSpectating = false; _hasPose = false; _recenterRequested = false; _smoothVelocity = Vector3.zero; _hasSmoothedPosition = false; _lastSmoothingFrame = -1; _lastPreCullApplyFrame = -1; _targetSwitchGraceUntilFrame = -1; _cameraInactiveGraceUntilFrame = -1; _cameraInactiveGraceStarted = false; SpectatorVanillaInputGuard.Clear(); _anchorService.Clear(); _state.IsActive = false; _state.UserEnabled = _settings.FreecamDefaultOn; _state.TargetSlotId = null; _state.TargetActualClientId = null; _state.Offset = Vector3.zero; _state.Rotation = Quaternion.identity; _state.WorldPosition = Vector3.zero; _state.HasWorldPose = false; if (num) { ModLog.Debug("Reset local spectator freecam state."); } } private void Deactivate(bool clearAnchor) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) bool isActive = _state.IsActive; _state.IsActive = false; _smoothVelocity = Vector3.zero; _lastSmoothingFrame = -1; SpectatorVanillaInputGuard.Clear(); if (clearAnchor) { _anchorService.Clear(); _hasPose = false; _hasSmoothedPosition = false; _targetSwitchGraceUntilFrame = -1; _cameraInactiveGraceStarted = false; _state.TargetSlotId = null; _state.TargetActualClientId = null; _state.WorldPosition = Vector3.zero; _state.HasWorldPose = false; } if (isActive) { ModLog.Debug(clearAnchor ? "Deactivated local spectator freecam and cleared anchor." : "Deactivated local spectator freecam."); } } private void BeginTargetSwitchRecoveryWindow() { _targetSwitchGraceUntilFrame = SpectatorFreecamRecoveryPolicy.ExtendGraceUntilFrame(Time.frameCount, 4); } private void BeginCameraInactiveRecoveryWindow() { if (_hasPose && !_cameraInactiveGraceStarted) { _cameraInactiveGraceStarted = true; _cameraInactiveGraceUntilFrame = SpectatorFreecamRecoveryPolicy.ExtendGraceUntilFrame(Time.frameCount, 3); } } private bool TrySoftPause(SpectatorFreecamIneligibleReason reason, GameSpectatorSnapshot snapshot) { if (SpectatorFreecamRecoveryPolicy.GetIneligibleAction(reason, Time.frameCount, _hasPose, _targetSwitchGraceUntilFrame, _cameraInactiveGraceUntilFrame) != SpectatorFreecamRecoveryAction.SoftPausePreservePose) { return false; } SoftPauseWithReason(reason, snapshot); return true; } private void SoftPauseWithReason(SpectatorFreecamIneligibleReason reason, GameSpectatorSnapshot snapshot) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) _state.IsActive = false; _smoothVelocity = Vector3.zero; _lastSmoothingFrame = -1; SpectatorVanillaInputGuard.Clear(); LogEligibilityBlocked(reason, snapshot, softPaused: true); } private void DeactivateWithReason(SpectatorFreecamIneligibleReason reason, GameSpectatorSnapshot snapshot, bool clearAnchor) { LogEligibilityBlocked(reason, snapshot, softPaused: false); Deactivate(clearAnchor); } private void LogEligibilityBlocked(SpectatorFreecamIneligibleReason reason, GameSpectatorSnapshot snapshot, bool softPaused) { if (ModLog.IsDebugEnabled && (Time.frameCount >= _nextEligibilityDebugFrame || reason != _lastEligibilityDebugReason)) { _lastEligibilityDebugReason = reason; _nextEligibilityDebugFrame = Time.frameCount + 60; ModLog.Debug(string.Format("Local spectator freecam {0} reason={1} ", softPaused ? "soft-paused" : "inactive", reason) + $"frame={Time.frameCount} hasPose={_hasPose} userEnabled={_state.UserEnabled} " + $"hasTarget={snapshot.HasSpectatedTarget} hasCamera={(Object)(object)snapshot.SpectateCamera != (Object)null} " + $"hasAnchor={(Object)(object)snapshot.Anchor != (Object)null} activeCamera={snapshot.IsSpectateCameraActive} " + "targetSlot=" + (snapshot.SpectatedPlayerSlotId?.ToString() ?? "none") + " targetClient=" + (snapshot.SpectatedPlayerActualClientId?.ToString() ?? "none")); } } private void InitializePoseFromCamera(Camera camera, Transform anchor) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) _state.Offset = ((Component)camera).transform.position - anchor.position; ClampOffset(); SetYawPitchFromRotation(((Component)camera).transform.rotation); _state.Rotation = Quaternion.Euler(_pitch, _yaw, 0f); _smoothVelocity = Vector3.zero; _smoothedPosition = ((Component)camera).transform.position; _hasSmoothedPosition = true; _lastSmoothingFrame = -1; _hasPose = true; } private void Recenter(Camera camera, Transform anchor) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) float freecamRadius = _settings.FreecamRadius; float num = ((freecamRadius > 0.01f) ? Mathf.Min(4f, freecamRadius * 0.6f) : 0f); float num2 = ((freecamRadius > 0.01f) ? Mathf.Min(1.2f, freecamRadius * 0.3f) : 0f); Vector3 val = ResolveRecenterDirection(camera, anchor); _state.Offset = val * num + Vector3.up * num2; ClampOffset(); Vector3 val2 = anchor.position + _state.Offset; Vector3 val3 = anchor.position + Vector3.up * Mathf.Min(0.6f, freecamRadius * 0.2f) - val2; if (((Vector3)(ref val3)).sqrMagnitude > 0.0001f) { SetYawPitchFromRotation(Quaternion.LookRotation(((Vector3)(ref val3)).normalized, Vector3.up)); } _state.Rotation = Quaternion.Euler(_pitch, _yaw, 0f); _smoothVelocity = Vector3.zero; _smoothedPosition = anchor.position + _state.Offset; _hasSmoothedPosition = true; _lastSmoothingFrame = -1; _hasPose = true; } private static Vector3 ResolveRecenterDirection(Camera camera, Transform anchor) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) if (TryGetHorizontalDirection(((Component)camera).transform.position - anchor.position, out var direction)) { return direction; } if (TryGetHorizontalDirection(-((Component)camera).transform.forward, out direction)) { return direction; } if (TryGetHorizontalDirection(-anchor.forward, out direction)) { return direction; } return Vector3.back; } private static bool TryGetHorizontalDirection(Vector3 source, out Vector3 direction) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) source.y = 0f; float magnitude = ((Vector3)(ref source)).magnitude; if (magnitude > 0.0001f) { direction = source / magnitude; return true; } direction = Vector3.zero; return false; } private void ApplyInput() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) Vector2 val = _inputService.ReadLookDelta(); _yaw += val.x * _settings.FreecamLookSensitivity; _pitch = Mathf.Clamp(_pitch - val.y * _settings.FreecamLookSensitivity, -85f, 85f); Quaternion val2 = Quaternion.Euler(_pitch, _yaw, 0f); Vector3 val3 = _inputService.ReadMoveInput(); if (((Vector3)(ref val3)).sqrMagnitude > 0f) { float num = 1f; if (_inputService.FastMoveHeld) { num *= _settings.FreecamFastMoveMultiplier; } if (_inputService.SlowMoveHeld) { num *= _settings.FreecamSlowMoveMultiplier; } float num2 = _settings.FreecamMoveSpeed * num; Vector3 val4 = val2 * Vector3.right * val3.x + Vector3.up * val3.y + val2 * Vector3.forward * val3.z; SpectatorCameraState state = _state; state.Offset += val4 * num2 * Time.unscaledDeltaTime; ClampOffset(); } _state.Rotation = val2; } private void ApplyCameraTransform(Camera camera, Transform anchor) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) Vector3 val = anchor.position + _state.Offset; Transform transform = ((Component)camera).transform; if (Time.frameCount != _lastSmoothingFrame) { if (_settings.FreecamSmoothTime > 0f && _state.IsActive && _hasSmoothedPosition) { _smoothedPosition = Vector3.SmoothDamp(_smoothedPosition, val, ref _smoothVelocity, _settings.FreecamSmoothTime, float.PositiveInfinity, Time.unscaledDeltaTime); } else { _smoothedPosition = val; _hasSmoothedPosition = true; _smoothVelocity = Vector3.zero; } _lastSmoothingFrame = Time.frameCount; } transform.position = _smoothedPosition; transform.rotation = _state.Rotation; _state.IsActive = true; _state.WorldPosition = _smoothedPosition; _state.HasWorldPose = true; if (ModLog.IsDebugEnabled && Time.frameCount >= _nextApplyDebugFrame) { _nextApplyDebugFrame = Time.frameCount + 120; ModLog.Debug($"Applied freecam camera frame={Time.frameCount} camera={((Object)camera).name} offset={_state.Offset} target={val} rendered={_smoothedPosition}"); } } private void ClampOffset() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) if (!_settings.ClampCameraToRadius) { return; } float freecamRadius = _settings.FreecamRadius; if (freecamRadius <= 0f) { _state.Offset = Vector3.zero; return; } Vector3 offset = _state.Offset; float magnitude = ((Vector3)(ref offset)).magnitude; if (magnitude > freecamRadius) { _state.Offset = _state.Offset / magnitude * freecamRadius; } } private void UpdateState(GameSpectatorSnapshot snapshot) { _state.TargetSlotId = snapshot.SpectatedPlayerSlotId; _state.TargetActualClientId = snapshot.SpectatedPlayerActualClientId; } private void SetYawPitchFromRotation(Quaternion rotation) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) Vector3 eulerAngles = ((Quaternion)(ref rotation)).eulerAngles; _yaw = eulerAngles.y; _pitch = NormalizePitch(eulerAngles.x); _pitch = Mathf.Clamp(_pitch, -85f, 85f); } private static float NormalizePitch(float pitch) { if (!(pitch > 180f)) { return pitch; } return pitch - 360f; } private void DisableAfterFailure(Exception ex) { ModLog.Error($"Enhanced spectator freecam failed and will fall back to vanilla camera: {ex}"); _state.UserEnabled = false; Deactivate(clearAnchor: true); } private void UpdateVanillaInputGuard(bool enabled) { UpdateVanillaInputGuard(enabled, quickMenuBlocksInput: false); } private void UpdateVanillaInputGuard(bool enabled, bool quickMenuBlocksInput) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) SpectatorVanillaInputGuard.Update(enabled, _settings.AscendKey, _settings.DescendKey, quickMenuBlocksInput); } private void LogQuickMenuInputBlocked() { if (ModLog.IsDebugEnabled && Time.frameCount >= _nextMenuBlockDebugFrame) { _nextMenuBlockDebugFrame = Time.frameCount + 120; ModLog.Debug("Local spectator freecam input is paused while the quick menu is open."); } } } public enum SpectatorFreecamIneligibleReason { None, FeatureDisabled, NonSpectator, MissingCameraAnchorOrTarget, SpectateCameraInactive, GameOverOverride, LifecycleUnsafe } public enum SpectatorFreecamRecoveryAction { Continue, SoftPausePreservePose, DeactivatePreservePose, DeactivateClearPose, ResetForNonSpectator } public static class SpectatorFreecamRecoveryPolicy { public static SpectatorFreecamRecoveryAction GetIneligibleAction(SpectatorFreecamIneligibleReason reason, int currentFrame, bool hasPose, int targetSwitchGraceUntilFrame, int cameraInactiveGraceUntilFrame) { switch (reason) { case SpectatorFreecamIneligibleReason.None: return SpectatorFreecamRecoveryAction.Continue; case SpectatorFreecamIneligibleReason.NonSpectator: return SpectatorFreecamRecoveryAction.ResetForNonSpectator; case SpectatorFreecamIneligibleReason.MissingCameraAnchorOrTarget: if (!hasPose || !IsWithinGrace(currentFrame, targetSwitchGraceUntilFrame)) { return SpectatorFreecamRecoveryAction.DeactivateClearPose; } return SpectatorFreecamRecoveryAction.SoftPausePreservePose; case SpectatorFreecamIneligibleReason.SpectateCameraInactive: if (!hasPose || !IsWithinGrace(currentFrame, cameraInactiveGraceUntilFrame)) { return SpectatorFreecamRecoveryAction.DeactivatePreservePose; } return SpectatorFreecamRecoveryAction.SoftPausePreservePose; case SpectatorFreecamIneligibleReason.LifecycleUnsafe: if (!hasPose) { return SpectatorFreecamRecoveryAction.DeactivatePreservePose; } return SpectatorFreecamRecoveryAction.SoftPausePreservePose; default: return SpectatorFreecamRecoveryAction.DeactivatePreservePose; } } public static int ExtendGraceUntilFrame(int currentFrame, int graceFrames) { return currentFrame + ((graceFrames >= 0) ? graceFrames : 0); } private static bool IsWithinGrace(int currentFrame, int graceUntilFrame) { if (graceUntilFrame >= 0) { return currentFrame <= graceUntilFrame; } return false; } } public sealed class SpectatorFreecamSettings { private readonly EnhancedSpectatorConfig _config; public bool EnableEnhancedSpectator => _config.EnableEnhancedSpectator.Value; public bool EnableFreecam => _config.EnableFreecam.Value; public bool FreecamDefaultOn => _config.FreecamDefaultOn.Value; public float FreecamRadius => Mathf.Max(0f, _config.FreecamRadius.Value); public float FreecamMoveSpeed => Mathf.Max(0f, _config.FreecamMoveSpeed.Value); public float FreecamFastMoveMultiplier => Mathf.Max(0f, _config.FreecamFastMoveMultiplier.Value); public float FreecamSlowMoveMultiplier => Mathf.Max(0f, _config.FreecamSlowMoveMultiplier.Value); public float FreecamLookSensitivity => Mathf.Max(0f, _config.FreecamLookSensitivity.Value); public float FreecamSmoothTime => Mathf.Max(0f, _config.FreecamSmoothTime.Value); public bool ClampCameraToRadius => _config.ClampCameraToRadius.Value; public bool RecenterOnTargetSwitch => _config.RecenterOnTargetSwitch.Value; public bool DisableDuringGameOverOverride => _config.DisableDuringGameOverOverride.Value; public KeyCode ToggleFreecamKey => _config.ToggleFreecamKey.Value; public KeyCode RecenterKey => _config.RecenterKey.Value; public KeyCode ResetToVanillaViewKey => _config.ResetToVanillaViewKey.Value; public KeyCode FastMoveKey => _config.FastMoveKey.Value; public KeyCode SlowMoveKey => _config.SlowMoveKey.Value; public KeyCode AscendKey => _config.AscendKey.Value; public KeyCode DescendKey => _config.DescendKey.Value; public SpectatorFreecamSettings(EnhancedSpectatorConfig config) { _config = config; } } public readonly struct SpectatorInputKeyResolution { public static SpectatorInputKeyResolution Unmapped => new SpectatorInputKeyResolution(isMapped: false, (Key)0); public bool IsMapped { get; } public Key InputKey { get; } public SpectatorInputKeyResolution(bool isMapped, Key inputKey) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) IsMapped = isMapped; InputKey = inputKey; } } public sealed class SpectatorInputKeyCache { private readonly Func _resolve; private KeyCode _keyCode; private SpectatorInputKeyResolution _resolution; private bool _hasResolution; public SpectatorInputKeyCache() : this(SpectatorInputKeyMappings.Resolve) { } public SpectatorInputKeyCache(Func resolve) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) _resolve = resolve ?? throw new ArgumentNullException("resolve"); _keyCode = (KeyCode)0; _resolution = SpectatorInputKeyResolution.Unmapped; } public bool TryResolve(KeyCode keyCode, out Key inputKey) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Expected I4, but got Unknown if (!_hasResolution || _keyCode != keyCode) { _keyCode = keyCode; _resolution = _resolve(keyCode); _hasResolution = true; } inputKey = (Key)(int)_resolution.InputKey; return _resolution.IsMapped; } public void Clear() { //IL_0002: Unknown result type (might be due to invalid IL or missing references) _keyCode = (KeyCode)0; _resolution = SpectatorInputKeyResolution.Unmapped; _hasResolution = false; } } public static class SpectatorInputKeyMappings { public static SpectatorInputKeyResolution Resolve(KeyCode keyCode) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Expected I4, but got Unknown //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_02af: Unknown result type (might be due to invalid IL or missing references) //IL_0562: Unknown result type (might be due to invalid IL or missing references) //IL_02b6: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Unknown result type (might be due to invalid IL or missing references) //IL_02c4: Unknown result type (might be due to invalid IL or missing references) //IL_02cc: Unknown result type (might be due to invalid IL or missing references) //IL_02d3: Unknown result type (might be due to invalid IL or missing references) //IL_02db: Unknown result type (might be due to invalid IL or missing references) //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_02eb: Unknown result type (might be due to invalid IL or missing references) //IL_02f3: Unknown result type (might be due to invalid IL or missing references) //IL_02fb: Unknown result type (might be due to invalid IL or missing references) //IL_0303: Unknown result type (might be due to invalid IL or missing references) //IL_030b: Unknown result type (might be due to invalid IL or missing references) //IL_0313: Unknown result type (might be due to invalid IL or missing references) //IL_031b: Unknown result type (might be due to invalid IL or missing references) //IL_0323: Unknown result type (might be due to invalid IL or missing references) //IL_032b: Unknown result type (might be due to invalid IL or missing references) //IL_0332: Unknown result type (might be due to invalid IL or missing references) //IL_033a: Unknown result type (might be due to invalid IL or missing references) //IL_0342: Unknown result type (might be due to invalid IL or missing references) //IL_034a: Unknown result type (might be due to invalid IL or missing references) //IL_0352: Unknown result type (might be due to invalid IL or missing references) //IL_0359: Unknown result type (might be due to invalid IL or missing references) //IL_0361: Unknown result type (might be due to invalid IL or missing references) //IL_0369: Unknown result type (might be due to invalid IL or missing references) //IL_0371: Unknown result type (might be due to invalid IL or missing references) //IL_0379: Unknown result type (might be due to invalid IL or missing references) //IL_0381: Unknown result type (might be due to invalid IL or missing references) //IL_0389: Unknown result type (might be due to invalid IL or missing references) //IL_0391: Unknown result type (might be due to invalid IL or missing references) //IL_0399: Unknown result type (might be due to invalid IL or missing references) //IL_03a1: Unknown result type (might be due to invalid IL or missing references) //IL_03a9: Unknown result type (might be due to invalid IL or missing references) //IL_03b1: Unknown result type (might be due to invalid IL or missing references) //IL_03b9: Unknown result type (might be due to invalid IL or missing references) //IL_03c1: Unknown result type (might be due to invalid IL or missing references) //IL_03c9: Unknown result type (might be due to invalid IL or missing references) //IL_03d1: Unknown result type (might be due to invalid IL or missing references) //IL_03d9: Unknown result type (might be due to invalid IL or missing references) //IL_03e1: Unknown result type (might be due to invalid IL or missing references) //IL_03e9: Unknown result type (might be due to invalid IL or missing references) //IL_03f1: Unknown result type (might be due to invalid IL or missing references) //IL_03f9: Unknown result type (might be due to invalid IL or missing references) //IL_0401: Unknown result type (might be due to invalid IL or missing references) //IL_0409: Unknown result type (might be due to invalid IL or missing references) //IL_0411: Unknown result type (might be due to invalid IL or missing references) //IL_0419: Unknown result type (might be due to invalid IL or missing references) //IL_0421: Unknown result type (might be due to invalid IL or missing references) //IL_0429: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01ad: Invalid comparison between Unknown and I4 //IL_0299: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 //IL_0563: Unknown result type (might be due to invalid IL or missing references) //IL_0564: Unknown result type (might be due to invalid IL or missing references) //IL_0565: Unknown result type (might be due to invalid IL or missing references) //IL_0431: Unknown result type (might be due to invalid IL or missing references) //IL_01b2: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Expected I4, but got Unknown //IL_02a0: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 //IL_0569: Unknown result type (might be due to invalid IL or missing references) //IL_0439: Unknown result type (might be due to invalid IL or missing references) //IL_0441: Unknown result type (might be due to invalid IL or missing references) //IL_0449: Unknown result type (might be due to invalid IL or missing references) //IL_0451: Unknown result type (might be due to invalid IL or missing references) //IL_0459: Unknown result type (might be due to invalid IL or missing references) //IL_0461: Unknown result type (might be due to invalid IL or missing references) //IL_0469: Unknown result type (might be due to invalid IL or missing references) //IL_0471: Unknown result type (might be due to invalid IL or missing references) //IL_0479: Unknown result type (might be due to invalid IL or missing references) //IL_0481: Unknown result type (might be due to invalid IL or missing references) //IL_0489: Unknown result type (might be due to invalid IL or missing references) //IL_0491: Unknown result type (might be due to invalid IL or missing references) //IL_0499: Unknown result type (might be due to invalid IL or missing references) //IL_04a1: Unknown result type (might be due to invalid IL or missing references) //IL_04a9: Unknown result type (might be due to invalid IL or missing references) //IL_04b1: Unknown result type (might be due to invalid IL or missing references) //IL_04b9: Unknown result type (might be due to invalid IL or missing references) //IL_04c1: Unknown result type (might be due to invalid IL or missing references) //IL_04c9: Unknown result type (might be due to invalid IL or missing references) //IL_04d1: Unknown result type (might be due to invalid IL or missing references) //IL_04d9: Unknown result type (might be due to invalid IL or missing references) //IL_04e1: Unknown result type (might be due to invalid IL or missing references) //IL_04e6: Unknown result type (might be due to invalid IL or missing references) //IL_04eb: Unknown result type (might be due to invalid IL or missing references) //IL_04f0: Unknown result type (might be due to invalid IL or missing references) //IL_04f5: Unknown result type (might be due to invalid IL or missing references) //IL_04fa: Unknown result type (might be due to invalid IL or missing references) //IL_04ff: Unknown result type (might be due to invalid IL or missing references) //IL_0504: Unknown result type (might be due to invalid IL or missing references) //IL_0509: Unknown result type (might be due to invalid IL or missing references) //IL_050e: Unknown result type (might be due to invalid IL or missing references) //IL_0513: Unknown result type (might be due to invalid IL or missing references) //IL_0518: Unknown result type (might be due to invalid IL or missing references) //IL_051d: Unknown result type (might be due to invalid IL or missing references) //IL_0522: Unknown result type (might be due to invalid IL or missing references) //IL_0527: Unknown result type (might be due to invalid IL or missing references) //IL_052c: Unknown result type (might be due to invalid IL or missing references) //IL_0531: Unknown result type (might be due to invalid IL or missing references) //IL_0536: Unknown result type (might be due to invalid IL or missing references) //IL_053b: Unknown result type (might be due to invalid IL or missing references) //IL_0540: Unknown result type (might be due to invalid IL or missing references) //IL_0545: Unknown result type (might be due to invalid IL or missing references) //IL_054a: Unknown result type (might be due to invalid IL or missing references) //IL_054f: Unknown result type (might be due to invalid IL or missing references) //IL_0554: Unknown result type (might be due to invalid IL or missing references) //IL_0559: Unknown result type (might be due to invalid IL or missing references) //IL_055e: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Unknown result type (might be due to invalid IL or missing references) Key val; if ((int)keyCode <= 13) { if ((int)keyCode != 8) { if ((int)keyCode != 9) { if ((int)keyCode != 13) { goto IL_0561; } val = (Key)2; } else { val = (Key)3; } } else { val = (Key)65; } } else { switch (keyCode - 27) { case 0: goto IL_02ad; case 5: goto IL_02b5; case 12: goto IL_02bc; case 17: goto IL_02c3; case 18: goto IL_02ca; case 19: goto IL_02d2; case 20: goto IL_02d9; case 21: goto IL_02e1; case 22: goto IL_02e9; case 23: goto IL_02f1; case 24: goto IL_02f9; case 25: goto IL_0301; case 26: goto IL_0309; case 27: goto IL_0311; case 28: goto IL_0319; case 29: goto IL_0321; case 30: goto IL_0329; case 32: goto IL_0331; case 34: goto IL_0338; case 64: goto IL_0340; case 65: goto IL_0348; case 66: goto IL_0350; case 69: goto IL_0358; case 70: goto IL_035f; case 71: goto IL_0367; case 72: goto IL_036f; case 73: goto IL_0377; case 74: goto IL_037f; case 75: goto IL_0387; case 76: goto IL_038f; case 77: goto IL_0397; case 78: goto IL_039f; case 79: goto IL_03a7; case 80: goto IL_03af; case 81: goto IL_03b7; case 82: goto IL_03bf; case 83: goto IL_03c7; case 84: goto IL_03cf; case 85: goto IL_03d7; case 86: goto IL_03df; case 87: goto IL_03e7; case 88: goto IL_03ef; case 89: goto IL_03f7; case 90: goto IL_03ff; case 91: goto IL_0407; case 92: goto IL_040f; case 93: goto IL_0417; case 94: goto IL_041f; case 95: goto IL_0427; case 1: case 2: case 3: case 4: case 6: case 7: case 8: case 9: case 10: case 11: case 13: case 14: case 15: case 16: case 31: case 33: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: case 48: case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: case 58: case 59: case 60: case 61: case 62: case 63: case 67: case 68: goto IL_0561; } if ((int)keyCode != 127) { switch (keyCode - 256) { case 0: break; case 1: goto IL_043f; case 2: goto IL_0447; case 3: goto IL_044f; case 4: goto IL_0457; case 5: goto IL_045f; case 6: goto IL_0467; case 7: goto IL_046f; case 8: goto IL_0477; case 9: goto IL_047f; case 10: goto IL_0487; case 11: goto IL_048f; case 12: goto IL_0497; case 13: goto IL_049f; case 14: goto IL_04a7; case 15: goto IL_04af; case 16: goto IL_04b7; case 17: goto IL_04bf; case 18: goto IL_04c7; case 19: goto IL_04cf; case 20: goto IL_04d7; case 21: goto IL_04df; case 22: goto IL_04e4; case 23: goto IL_04e9; case 24: goto IL_04ee; case 25: goto IL_04f3; case 26: goto IL_04f8; case 27: goto IL_04fd; case 28: goto IL_0502; case 29: goto IL_0507; case 30: goto IL_050c; case 31: goto IL_0511; case 32: goto IL_0516; case 33: goto IL_051b; case 34: goto IL_0520; case 35: goto IL_0525; case 36: goto IL_052a; case 37: goto IL_052f; case 44: goto IL_0534; case 45: goto IL_0539; case 46: goto IL_053e; case 47: goto IL_0543; case 48: goto IL_0548; case 49: goto IL_054d; case 50: goto IL_0552; case 51: goto IL_0557; case 52: goto IL_055c; default: goto IL_0561; } val = (Key)84; } else { val = (Key)71; } } goto IL_0563; IL_02e9: val = (Key)41; goto IL_0563; IL_02d9: val = (Key)9; goto IL_0563; IL_02ca: val = (Key)13; goto IL_0563; IL_02bc: val = (Key)5; goto IL_0563; IL_02c3: val = (Key)7; goto IL_0563; IL_02d2: val = (Key)8; goto IL_0563; IL_02e1: val = (Key)50; goto IL_0563; IL_02b5: val = (Key)1; goto IL_0563; IL_0561: val = (Key)0; goto IL_0563; IL_055c: val = (Key)53; goto IL_0563; IL_0557: val = (Key)54; goto IL_0563; IL_0552: val = (Key)55; goto IL_0563; IL_054d: val = (Key)56; goto IL_0563; IL_0548: val = (Key)51; goto IL_0563; IL_0543: val = (Key)52; goto IL_0563; IL_053e: val = (Key)75; goto IL_0563; IL_0539: val = (Key)72; goto IL_0563; IL_0534: val = (Key)73; goto IL_0563; IL_052f: val = (Key)105; goto IL_0563; IL_052a: val = (Key)104; goto IL_0563; IL_0525: val = (Key)103; goto IL_0563; IL_0520: val = (Key)102; goto IL_0563; IL_051b: val = (Key)101; goto IL_0563; IL_0516: val = (Key)100; goto IL_0563; IL_0511: val = (Key)99; goto IL_0563; IL_050c: val = (Key)98; goto IL_0563; IL_0507: val = (Key)97; goto IL_0563; IL_0502: val = (Key)96; goto IL_0563; IL_04fd: val = (Key)95; goto IL_0563; IL_04f8: val = (Key)94; goto IL_0563; IL_04f3: val = (Key)66; goto IL_0563; IL_04ee: val = (Key)67; goto IL_0563; IL_04e9: val = (Key)69; goto IL_0563; IL_04e4: val = (Key)68; goto IL_0563; IL_04df: val = (Key)70; goto IL_0563; IL_04d7: val = (Key)61; goto IL_0563; IL_04cf: val = (Key)62; goto IL_0563; IL_04c7: val = (Key)64; goto IL_0563; IL_04bf: val = (Key)63; goto IL_0563; IL_04b7: val = (Key)83; goto IL_0563; IL_04af: val = (Key)77; goto IL_0563; IL_04a7: val = (Key)80; goto IL_0563; IL_049f: val = (Key)81; goto IL_0563; IL_0497: val = (Key)79; goto IL_0563; IL_048f: val = (Key)78; goto IL_0563; IL_0487: val = (Key)82; goto IL_0563; IL_047f: val = (Key)93; goto IL_0563; IL_0477: val = (Key)92; goto IL_0563; IL_046f: val = (Key)91; goto IL_0563; IL_0467: val = (Key)90; goto IL_0563; IL_045f: val = (Key)89; goto IL_0563; IL_0457: val = (Key)88; goto IL_0563; IL_044f: val = (Key)87; goto IL_0563; IL_0447: val = (Key)86; goto IL_0563; IL_043f: val = (Key)85; goto IL_0563; IL_0563: Key val2 = val; if ((int)val2 != 0) { return new SpectatorInputKeyResolution(isMapped: true, val2); } return SpectatorInputKeyResolution.Unmapped; IL_02ad: val = (Key)60; goto IL_0563; IL_0427: val = (Key)40; goto IL_0563; IL_041f: val = (Key)39; goto IL_0563; IL_0417: val = (Key)38; goto IL_0563; IL_040f: val = (Key)37; goto IL_0563; IL_0407: val = (Key)36; goto IL_0563; IL_03ff: val = (Key)35; goto IL_0563; IL_03f7: val = (Key)34; goto IL_0563; IL_03ef: val = (Key)33; goto IL_0563; IL_03e7: val = (Key)32; goto IL_0563; IL_03df: val = (Key)31; goto IL_0563; IL_03d7: val = (Key)30; goto IL_0563; IL_03cf: val = (Key)29; goto IL_0563; IL_03c7: val = (Key)28; goto IL_0563; IL_03bf: val = (Key)27; goto IL_0563; IL_03b7: val = (Key)26; goto IL_0563; IL_03af: val = (Key)25; goto IL_0563; IL_03a7: val = (Key)24; goto IL_0563; IL_039f: val = (Key)23; goto IL_0563; IL_0397: val = (Key)22; goto IL_0563; IL_038f: val = (Key)21; goto IL_0563; IL_0387: val = (Key)20; goto IL_0563; IL_037f: val = (Key)19; goto IL_0563; IL_0377: val = (Key)18; goto IL_0563; IL_036f: val = (Key)17; goto IL_0563; IL_0367: val = (Key)16; goto IL_0563; IL_035f: val = (Key)15; goto IL_0563; IL_0358: val = (Key)4; goto IL_0563; IL_0350: val = (Key)12; goto IL_0563; IL_0348: val = (Key)10; goto IL_0563; IL_0340: val = (Key)11; goto IL_0563; IL_0338: val = (Key)14; goto IL_0563; IL_0331: val = (Key)6; goto IL_0563; IL_0329: val = (Key)49; goto IL_0563; IL_0321: val = (Key)48; goto IL_0563; IL_0319: val = (Key)47; goto IL_0563; IL_0311: val = (Key)46; goto IL_0563; IL_0309: val = (Key)45; goto IL_0563; IL_0301: val = (Key)44; goto IL_0563; IL_02f9: val = (Key)43; goto IL_0563; IL_02f1: val = (Key)42; goto IL_0563; } public static bool TryResolve(KeyCode keyCode, out Key inputKey) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected I4, but got Unknown SpectatorInputKeyResolution spectatorInputKeyResolution = Resolve(keyCode); inputKey = (Key)(int)spectatorInputKeyResolution.InputKey; return spectatorInputKeyResolution.IsMapped; } } public sealed class SpectatorInputService { private const float MouseDeltaScale = 0.05f; private readonly SpectatorFreecamSettings _settings; private readonly SpectatorInputKeyCache _toggleFreecamKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _recenterKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _resetToVanillaViewKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _fastMoveKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _slowMoveKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _ascendKey = new SpectatorInputKeyCache(); private readonly SpectatorInputKeyCache _descendKey = new SpectatorInputKeyCache(); public bool ToggleFreecamPressed => GetConfiguredKeyDown(_toggleFreecamKey, _settings.ToggleFreecamKey); public bool RecenterPressed => GetConfiguredKeyDown(_recenterKey, _settings.RecenterKey); public bool ResetToVanillaPressed => GetConfiguredKeyDown(_resetToVanillaViewKey, _settings.ResetToVanillaViewKey); public bool FastMoveHeld => GetConfiguredKey(_fastMoveKey, _settings.FastMoveKey); public bool SlowMoveHeld => GetConfiguredKey(_slowMoveKey, _settings.SlowMoveKey); public bool AscendHeld => GetConfiguredKey(_ascendKey, _settings.AscendKey); public bool DescendHeld => GetConfiguredKey(_descendKey, _settings.DescendKey); public SpectatorInputService(SpectatorFreecamSettings settings) { _settings = settings; } public Vector3 ReadMoveInput() { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current == null) { return Vector3.zero; } float num = 0f; float num2 = 0f; float num3 = 0f; if (IsPressed(current, (Key)15)) { num -= 1f; } if (IsPressed(current, (Key)18)) { num += 1f; } if (DescendHeld) { num2 -= 1f; } if (AscendHeld) { num2 += 1f; } if (IsPressed(current, (Key)33)) { num3 -= 1f; } if (IsPressed(current, (Key)37)) { num3 += 1f; } Vector3 result = default(Vector3); ((Vector3)(ref result))..ctor(num, num2, num3); if (!(((Vector3)(ref result)).sqrMagnitude > 1f)) { return result; } return ((Vector3)(ref result)).normalized; } public Vector2 ReadLookDelta() { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) Mouse current = Mouse.current; if (current != null) { return ((InputControl)(object)((Pointer)current).delta).ReadValue() * 0.05f; } return Vector2.zero; } public static bool IsKeyHeld(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return GetKey(key); } public static bool IsKeyPressedThisFrame(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return GetKeyDown(key); } private static bool GetKey(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (!TryGetInputSystemKey(key, out var inputKey)) { return false; } Keyboard current = Keyboard.current; if (current != null) { return IsPressed(current, inputKey); } return false; } private static bool GetKeyDown(KeyCode key) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (!TryGetInputSystemKey(key, out var inputKey)) { return false; } Keyboard current = Keyboard.current; if (current != null) { return WasPressedThisFrame(current, inputKey); } return false; } private static bool GetConfiguredKey(SpectatorInputKeyCache cache, KeyCode key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (!cache.TryResolve(key, out var inputKey)) { return false; } Keyboard current = Keyboard.current; if (current != null) { return IsPressed(current, inputKey); } return false; } private static bool GetConfiguredKeyDown(SpectatorInputKeyCache cache, KeyCode key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) if (!cache.TryResolve(key, out var inputKey)) { return false; } Keyboard current = Keyboard.current; if (current != null) { return WasPressedThisFrame(current, inputKey); } return false; } private static bool IsPressed(Keyboard keyboard, Key key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) KeyControl val = keyboard[key]; if (val != null) { return ((ButtonControl)val).isPressed; } return false; } private static bool WasPressedThisFrame(Keyboard keyboard, Key key) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) KeyControl val = keyboard[key]; if (val != null) { return ((ButtonControl)val).wasPressedThisFrame; } return false; } private static bool TryGetInputSystemKey(KeyCode keyCode, out Key inputKey) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return SpectatorInputKeyMappings.TryResolve(keyCode, out inputKey); } } public enum SpectatorLifecycleEventKind { PlayerDied, SpectatedPlayerEffectsApplied, CameraSwitched, GameOverOverrideChanged, Revived } public static class SpectatorLifecycleEvents { public static event Action? Changed; public static void Raise(SpectatorLifecycleEventKind kind) { try { SpectatorLifecycleEvents.Changed?.Invoke(kind); } catch (Exception arg) { ModLog.Error($"Spectator lifecycle subscriber failed for {kind}: {arg}"); } } } public sealed class SpectatorModule : IFeatureModule, IDisposable, ISpectatorStateService, ISpectatorTargetStateProvider, ISpectatorPoseStateProvider, IPeerIdentityStateProvider, IRuntimeTickable, IRuntimeLateTickable, IRuntimeCameraPreCullTickable { private readonly IGameSpectatorAdapter _gameSpectatorAdapter; private readonly SpectatorSnapshotCache _snapshotCache; private readonly SpectatorFreecamController _freecamController; private bool _initialized; public SpectatorState Current { get; private set; } public SpectatorModule(IGameSpectatorAdapter gameSpectatorAdapter, SpectatorFreecamSettings freecamSettings) { _gameSpectatorAdapter = gameSpectatorAdapter ?? throw new ArgumentNullException("gameSpectatorAdapter"); SpectatorFreecamSettings settings = freecamSettings ?? throw new ArgumentNullException("freecamSettings"); _snapshotCache = new SpectatorSnapshotCache(_gameSpectatorAdapter); SpectatorAnchorService anchorService = new SpectatorAnchorService(); SpectatorInputService inputService = new SpectatorInputService(settings); _freecamController = new SpectatorFreecamController(_gameSpectatorAdapter, _snapshotCache, anchorService, inputService, settings); Current = SpectatorState.Unavailable; } public void Initialize() { if (!_initialized) { Current = _gameSpectatorAdapter.ReadSpectatorState(); SpectatorLifecycleEvents.Changed += OnSpectatorLifecycleChanged; _initialized = true; ModLog.Debug("Spectator freecam module initialized."); } } public void Refresh() { if (_initialized) { if (_snapshotCache.TryGetCurrentFrameSnapshot(out var snapshot)) { Current = new SpectatorState(isAvailable: true, snapshot.IsLocalPlayerDead ? "Local spectator state is available." : "Local player is not spectating.", snapshot.IsLocalPlayerDead, _freecamController.State.IsActive); } else { Current = SpectatorState.Unavailable; } } } public bool TryGetCurrentSpectatorTarget(out SpectatorTargetState state) { if (_initialized && _snapshotCache.TryGetCurrentFrameSnapshot(out var snapshot) && snapshot.HasRound && snapshot.HasLocalPlayer && snapshot.LocalPlayerSlotId.HasValue && snapshot.LocalPlayerActualClientId.HasValue) { bool isLocalPlayerDead = snapshot.IsLocalPlayerDead; state = new SpectatorTargetState(isLocalPlayerDead, snapshot.LocalPlayerActualClientId.Value, snapshot.LocalPlayerSlotId.Value, (isLocalPlayerDead && snapshot.HasSpectatedTarget) ? snapshot.SpectatedPlayerActualClientId : null, (isLocalPlayerDead && snapshot.HasSpectatedTarget) ? snapshot.SpectatedPlayerSlotId : null, DateTime.UtcNow.Ticks); return true; } state = new SpectatorTargetState(isSpectating: false, 0uL, 0uL, null, null, DateTime.UtcNow.Ticks); return false; } public bool TryGetCurrentSpectatorPose(out SpectatorPoseState state) { //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) if (_initialized && _snapshotCache.TryGetCurrentFrameSnapshot(out var snapshot) && snapshot.HasRound && snapshot.HasLocalPlayer && snapshot.LocalPlayerSlotId.HasValue && snapshot.LocalPlayerActualClientId.HasValue) { bool flag = SpectatorPoseSourceRules.ShouldUseFreecamPose(_freecamController.State.IsActive, _freecamController.State.HasWorldPose); bool useVanillaSpectatorPose = !flag && SpectatorPoseSourceRules.ShouldUseVanillaSpectatorPose((Object)(object)snapshot.SpectateCamera != (Object)null, snapshot.IsSpectateCameraActive); bool flag2 = SpectatorPoseSourceRules.ShouldPublishSpectatorPose(snapshot.IsLocalPlayerDead, snapshot.HasSpectatedTarget, flag, useVanillaSpectatorPose); Vector3 position = Vector3.zero; Quaternion rotation = Quaternion.identity; if (flag2 && flag) { position = _freecamController.State.WorldPosition; rotation = _freecamController.State.Rotation; } else if (flag2 && (Object)(object)snapshot.SpectateCamera != (Object)null) { Transform transform = ((Component)snapshot.SpectateCamera).transform; position = transform.position; rotation = transform.rotation; } state = new SpectatorPoseState(flag2, snapshot.LocalPlayerActualClientId.Value, snapshot.LocalPlayerSlotId.Value, flag2 ? snapshot.SpectatedPlayerActualClientId : null, flag2 ? snapshot.SpectatedPlayerSlotId : null, position, rotation, DateTime.UtcNow.Ticks); return true; } state = new SpectatorPoseState(isSpectating: false, 0uL, 0uL, null, null, Vector3.zero, Quaternion.identity, DateTime.UtcNow.Ticks); return false; } public bool TryGetLocalPeerIdentity(out PeerIdentityState state) { if (_initialized && _gameSpectatorAdapter.TryGetLocalPlayerIdentity(out var clientId, out var slotId) && _gameSpectatorAdapter.TryGetPlayerDisplayName(clientId, slotId, out string displayName)) { string voicePlayerName; string voicePlayerName2 = (_gameSpectatorAdapter.TryGetLocalVoicePlayerName(out voicePlayerName) ? voicePlayerName : string.Empty); state = new PeerIdentityState(clientId, slotId, displayName, voicePlayerName2, DateTime.UtcNow.Ticks); return true; } state = new PeerIdentityState(0uL, 0uL, string.Empty, DateTime.UtcNow.Ticks); return false; } public void Tick() { if (_initialized) { _freecamController.Tick(); } } public void LateTick() { if (_initialized) { _freecamController.LateTick(); Refresh(); } } public void CameraPreCullTick(Camera camera) { if (_initialized) { _freecamController.CameraPreCullTick(camera); } } public void Dispose() { if (_initialized) { _initialized = false; SpectatorLifecycleEvents.Changed -= OnSpectatorLifecycleChanged; Current = SpectatorState.Unavailable; _snapshotCache.Clear(); ModLog.Debug("Spectator freecam module disposed."); } } private void OnSpectatorLifecycleChanged(SpectatorLifecycleEventKind kind) { _snapshotCache.Clear(); _freecamController.NotifyLifecycleEvent(kind); } } public static class SpectatorPoseSourceRules { public static bool ShouldUseFreecamPose(bool freecamActive, bool freecamHasWorldPose) { return freecamActive && freecamHasWorldPose; } public static bool ShouldUseVanillaSpectatorPose(bool hasSpectateCamera, bool isSpectateCameraActive) { return hasSpectateCamera && isSpectateCameraActive; } public static bool ShouldPublishSpectatorPose(bool isLocalPlayerDead, bool hasSpectatedTarget, bool useFreecamPose, bool useVanillaSpectatorPose) { if (isLocalPlayerDead && hasSpectatedTarget) { return useFreecamPose || useVanillaSpectatorPose; } return false; } } public sealed class SpectatorSnapshotCache { private readonly IGameSpectatorAdapter _adapter; private int _cachedFrame = -1; private bool _cachedResult; private GameSpectatorSnapshot _cachedSnapshot; public SpectatorSnapshotCache(IGameSpectatorAdapter adapter) { _adapter = adapter ?? throw new ArgumentNullException("adapter"); _cachedSnapshot = GameSpectatorSnapshot.Unavailable; } public bool TryGetCurrentFrameSnapshot(out GameSpectatorSnapshot snapshot) { return TryGetSnapshotForFrame(Time.frameCount, out snapshot); } public bool TryGetSnapshotForFrame(int frame, out GameSpectatorSnapshot snapshot) { if (_cachedFrame == frame) { snapshot = _cachedSnapshot; return _cachedResult; } GameSpectatorSnapshot snapshot2; bool flag = _adapter.TryGetLocalSpectatorSnapshot(out snapshot2); _cachedFrame = frame; _cachedResult = flag; _cachedSnapshot = snapshot2; snapshot = snapshot2; return flag; } public void Clear() { _cachedFrame = -1; _cachedResult = false; _cachedSnapshot = GameSpectatorSnapshot.Unavailable; } } public sealed class SpectatorState { public static SpectatorState Unavailable { get; } = new SpectatorState(isAvailable: false, "Game spectator state is not wired yet."); public bool IsAvailable { get; } public bool IsLocalPlayerSpectating { get; } public bool IsFreecamActive { get; } public string? Status { get; } public SpectatorState(bool isAvailable, string? status, bool isLocalPlayerSpectating = false, bool isFreecamActive = false) { IsAvailable = isAvailable; Status = status; IsLocalPlayerSpectating = isLocalPlayerSpectating; IsFreecamActive = isFreecamActive; } } public static class SpectatorVanillaInputGuard { private static bool _freecamWantsVerticalInput; private static KeyCode _ascendKey; private static KeyCode _descendKey; private static bool _quickMenuBlocksInput; private static int _internalTargetSwitchDepth; public static void Update(bool freecamEnabled, KeyCode ascendKey, KeyCode descendKey, bool quickMenuBlocksInput) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) _freecamWantsVerticalInput = freecamEnabled; _ascendKey = ascendKey; _descendKey = descendKey; _quickMenuBlocksInput = quickMenuBlocksInput; } public static void Clear() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) _freecamWantsVerticalInput = false; _ascendKey = (KeyCode)0; _descendKey = (KeyCode)0; _quickMenuBlocksInput = false; _internalTargetSwitchDepth = 0; } public static void BeginInternalTargetSwitch() { _internalTargetSwitchDepth++; } public static void EndInternalTargetSwitch() { if (_internalTargetSwitchDepth > 0) { _internalTargetSwitchDepth--; } } public static bool ShouldSuppressTargetSwitchInput(out string reason) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) bool ascendKeyHeld = false; bool descendKeyHeld = false; if (_freecamWantsVerticalInput && _internalTargetSwitchDepth <= 0) { ascendKeyHeld = SpectatorInputService.IsKeyHeld(_ascendKey); descendKeyHeld = SpectatorInputService.IsKeyHeld(_descendKey); } return SpectatorVanillaInputGuardRules.ShouldSuppressTargetSwitchInput(_internalTargetSwitchDepth > 0, _freecamWantsVerticalInput, ascendKeyHeld, descendKeyHeld, out reason); } public static bool ShouldSuppressGameplayInteractInput() { return _quickMenuBlocksInput; } } public static class SpectatorVanillaInputGuardRules { public static bool ShouldSuppressTargetSwitchInput(bool internalTargetSwitchInProgress, bool freecamWantsVerticalInput, bool ascendKeyHeld, bool descendKeyHeld, out string reason) { if (internalTargetSwitchInProgress) { reason = string.Empty; return false; } if (freecamWantsVerticalInput && (ascendKeyHeld || descendKeyHeld)) { reason = "freecam vertical movement is held"; return true; } reason = string.Empty; return false; } } } namespace EnhancedSpectator.Features.SpectatorPresence { public interface ISpectatorPresenceProvider { LocalSpectatorPresenceState Current { get; } } public sealed class LocalSpectatorPresenceState { public bool HasLocalPlayer { get; } public IReadOnlyList RemoteSpectators { get; } public static LocalSpectatorPresenceState Empty { get; } = new LocalSpectatorPresenceState(hasLocalPlayer: false, new List().AsReadOnly()); public LocalSpectatorPresenceState(bool hasLocalPlayer, IReadOnlyList remoteSpectators) { HasLocalPlayer = hasLocalPlayer; RemoteSpectators = remoteSpectators; } } public sealed class RemoteSpectatorInfo : IEquatable { public ulong SpectatorClientId { get; } public ulong SpectatorSlotId { get; } public bool IsWatchingLocalPlayer { get; } public long LastObservedTicks { get; } public SpectatorPoseState? PoseState { get; } public RemoteSpectatorInfo(ulong spectatorClientId, ulong spectatorSlotId, bool isWatchingLocalPlayer, long lastObservedTicks, SpectatorPoseState? poseState) { SpectatorClientId = spectatorClientId; SpectatorSlotId = spectatorSlotId; IsWatchingLocalPlayer = isWatchingLocalPlayer; LastObservedTicks = lastObservedTicks; PoseState = poseState; } public bool Equals(RemoteSpectatorInfo? other) { if (other != null && SpectatorClientId == other.SpectatorClientId) { return SpectatorSlotId == other.SpectatorSlotId; } return false; } public override bool Equals(object? obj) { return Equals(obj as RemoteSpectatorInfo); } public override int GetHashCode() { return (SpectatorClientId.GetHashCode() * 397) ^ SpectatorSlotId.GetHashCode(); } } public static class RemoteSpectatorVisibilityRules { public static bool IsWatchingLocalPlayer(SpectatorTargetState remoteTarget, ulong localClientId, ulong localPlayerSlotId) { if (!remoteTarget.IsSpectating || remoteTarget.LocalClientId == localClientId) { return false; } if (remoteTarget.TargetClientId.HasValue) { return remoteTarget.TargetClientId.Value == localClientId; } if (remoteTarget.TargetPlayerSlotId.HasValue) { return remoteTarget.TargetPlayerSlotId.Value == localPlayerSlotId; } return false; } public static bool ShouldShowRemoteSpectator(SpectatorTargetState remoteTarget, bool isWatchingLocalPlayer, bool hasMatchingPose, bool localPlayerIsDeadOrSpectating, bool showRemoteSpectators, bool showOnlySpectatorsWatchingMe, bool showDeadSpectatorsToAlivePlayers, bool showDeadSpectatorsToDeadPlayers) { if (!remoteTarget.IsSpectating || !showRemoteSpectators || !hasMatchingPose) { return false; } if (showOnlySpectatorsWatchingMe && !isWatchingLocalPlayer) { return false; } if (localPlayerIsDeadOrSpectating) { return showDeadSpectatorsToDeadPlayers; } return showDeadSpectatorsToAlivePlayers; } } public sealed class SpectatorPresenceModule : IFeatureModule, IDisposable, IRuntimeTickable { private readonly SpectatorPresenceService _presenceService; private bool _initialized; public SpectatorPresenceModule(SpectatorPresenceService presenceService) { _presenceService = presenceService ?? throw new ArgumentNullException("presenceService"); } public void Initialize() { if (!_initialized) { _initialized = true; ModLog.Debug("Spectator presence debug module initialized."); } } public void Tick() { if (_initialized) { _presenceService.Tick(); } } public void Dispose() { if (_initialized) { _presenceService.Clear(); _initialized = false; ModLog.Debug("Spectator presence debug module disposed."); } } } public sealed class SpectatorPresenceService : ISpectatorPresenceProvider { private readonly EnhancedSpectatorConfig _config; private readonly IGameSpectatorAdapter _gameSpectatorAdapter; private readonly IEnhancedSpectatorNetworkService _networkService; private readonly Dictionary _currentSpectators = new Dictionary(); private readonly Dictionary _nextSpectators = new Dictionary(); private readonly List _candidateSpectators = new List(); private readonly List _publishedSpectators = new List(); private readonly List _remoteTargetScratch = new List(); private readonly LocalSpectatorPresenceState _activePresenceState; public LocalSpectatorPresenceState Current { get; private set; } public SpectatorPresenceService(EnhancedSpectatorConfig config, IGameSpectatorAdapter gameSpectatorAdapter, IEnhancedSpectatorNetworkService networkService) { _config = config ?? throw new ArgumentNullException("config"); _gameSpectatorAdapter = gameSpectatorAdapter ?? throw new ArgumentNullException("gameSpectatorAdapter"); _networkService = networkService ?? throw new ArgumentNullException("networkService"); _activePresenceState = new LocalSpectatorPresenceState(hasLocalPlayer: true, _publishedSpectators); Current = LocalSpectatorPresenceState.Empty; } public void Tick() { if (!_config.EnableSpectatorPresenceDebug.Value) { ClearPresenceState(logStops: false); return; } if (!RuntimeConnectionState.CanUseModNetworking(out string _) || !_networkService.IsNetworkAvailable || !_networkService.IsTargetSyncEnabled) { ClearPresenceState(logStops: false); return; } if (!TryGetLocalContext(out var localClientId, out var localPlayerSlotId, out var localPlayerIsDeadOrSpectating)) { ClearPresenceState(logStops: false); return; } _networkService.CopyRemoteSpectatorTargetsTo(_remoteTargetScratch); BuildRemoteSpectatorSet(localClientId, localPlayerSlotId, localPlayerIsDeadOrSpectating, _remoteTargetScratch); PublishPresenceChanges(); } public void Clear() { ClearPresenceState(logStops: false); } private bool TryGetLocalContext(out ulong localClientId, out ulong localPlayerSlotId, out bool localPlayerIsDeadOrSpectating) { if (_gameSpectatorAdapter.TryGetLocalSpectatorSnapshot(out var snapshot) && snapshot.HasRound && snapshot.HasLocalPlayer && snapshot.LocalPlayerActualClientId.HasValue && snapshot.LocalPlayerSlotId.HasValue) { localClientId = snapshot.LocalPlayerActualClientId.Value; localPlayerSlotId = snapshot.LocalPlayerSlotId.Value; localPlayerIsDeadOrSpectating = snapshot.IsLocalPlayerDead || snapshot.HasBegunSpectating; return true; } localClientId = 0uL; localPlayerSlotId = 0uL; localPlayerIsDeadOrSpectating = false; return false; } private void BuildRemoteSpectatorSet(ulong localClientId, ulong localPlayerSlotId, bool localPlayerIsDeadOrSpectating, IReadOnlyList remoteTargets) { _nextSpectators.Clear(); _candidateSpectators.Clear(); int num = Math.Max(0, _config.MaxFloatingHeadsVisible.Value); if (num == 0) { return; } foreach (SpectatorTargetState remoteTarget in remoteTargets) { bool isWatchingLocalPlayer = RemoteSpectatorVisibilityRules.IsWatchingLocalPlayer(remoteTarget, localClientId, localPlayerSlotId); SpectatorPoseState spectatorPoseState = TryGetMatchingPose(remoteTarget); if (RemoteSpectatorVisibilityRules.ShouldShowRemoteSpectator(remoteTarget, isWatchingLocalPlayer, spectatorPoseState != null, localPlayerIsDeadOrSpectating, _config.ShowRemoteSpectators.Value, _config.ShowOnlySpectatorsWatchingMe.Value, _config.ShowDeadSpectatorsToAlivePlayers.Value, _config.ShowDeadSpectatorsToDeadPlayers.Value)) { _candidateSpectators.Add(new RemoteSpectatorInfo(remoteTarget.LocalClientId, remoteTarget.LocalPlayerSlotId, isWatchingLocalPlayer, remoteTarget.TimestampTicks, spectatorPoseState)); } } _candidateSpectators.Sort((RemoteSpectatorInfo left, RemoteSpectatorInfo right) => left.SpectatorClientId.CompareTo(right.SpectatorClientId)); int num2 = Math.Min(num, _candidateSpectators.Count); for (int i = 0; i < num2; i++) { RemoteSpectatorInfo remoteSpectatorInfo = _candidateSpectators[i]; _nextSpectators[remoteSpectatorInfo.SpectatorClientId] = remoteSpectatorInfo; } } private SpectatorPoseState? TryGetMatchingPose(SpectatorTargetState remoteTarget) { if (!_networkService.TryGetRemoteSpectatorPose(remoteTarget.LocalClientId, out SpectatorPoseState state)) { return null; } if (!state.IsSpectating || state.TargetClientId != remoteTarget.TargetClientId || state.TargetPlayerSlotId != remoteTarget.TargetPlayerSlotId) { return null; } return state; } private void PublishPresenceChanges() { if (_config.DebugLogPresenceChanges.Value) { foreach (RemoteSpectatorInfo value in _nextSpectators.Values) { if (!_currentSpectators.ContainsKey(value.SpectatorClientId)) { LogSpectatorStarted(value); } } foreach (RemoteSpectatorInfo value2 in _currentSpectators.Values) { if (!_nextSpectators.ContainsKey(value2.SpectatorClientId)) { LogSpectatorStopped(value2); } } } _currentSpectators.Clear(); _publishedSpectators.Clear(); foreach (KeyValuePair nextSpectator in _nextSpectators) { _currentSpectators[nextSpectator.Key] = nextSpectator.Value; _publishedSpectators.Add(nextSpectator.Value); } Current = _activePresenceState; } private void ClearPresenceState(bool logStops) { if (logStops && _config.DebugLogPresenceChanges.Value) { foreach (RemoteSpectatorInfo value in _currentSpectators.Values) { ModLog.Debug(value.IsWatchingLocalPlayer ? $"Remote spectator stopped watching local player: spectatorClient={value.SpectatorClientId}, spectatorSlot={value.SpectatorSlotId}." : $"Remote spectator no longer visible: spectatorClient={value.SpectatorClientId}, spectatorSlot={value.SpectatorSlotId}."); } } _currentSpectators.Clear(); _nextSpectators.Clear(); _candidateSpectators.Clear(); _publishedSpectators.Clear(); _remoteTargetScratch.Clear(); Current = LocalSpectatorPresenceState.Empty; } private static void LogSpectatorStarted(RemoteSpectatorInfo spectator) { ModLog.Debug(spectator.IsWatchingLocalPlayer ? $"Remote spectator started watching local player: spectatorClient={spectator.SpectatorClientId}, spectatorSlot={spectator.SpectatorSlotId}." : $"Remote spectator became visible while spectating: spectatorClient={spectator.SpectatorClientId}, spectatorSlot={spectator.SpectatorSlotId}."); } private static void LogSpectatorStopped(RemoteSpectatorInfo spectator) { ModLog.Debug(spectator.IsWatchingLocalPlayer ? $"Remote spectator stopped watching local player: spectatorClient={spectator.SpectatorClientId}, spectatorSlot={spectator.SpectatorSlotId}." : $"Remote spectator no longer visible: spectatorClient={spectator.SpectatorClientId}, spectatorSlot={spectator.SpectatorSlotId}."); } } } namespace EnhancedSpectator.Features.PlayerStateSync { public sealed class ConnectedPlayerStateRepairModule : IFeatureModule, IDisposable, IRuntimeTickable { private const float RepairIntervalSeconds = 0.5f; private const float IdleFallbackRepairIntervalSeconds = 5f; private readonly EnhancedSpectatorConfig _config; private readonly IEnhancedSpectatorNetworkService _networkService; private readonly IConnectedPlayerStateRepairAdapter _repairAdapter; private readonly List _identityScratch = new List(); private readonly List _targetScratch = new List(); private float _nextRepairTime; private float _nextIdleFallbackRepairTime; private int _lastIdentityRevision = -1; private int _lastTargetRevision = -1; private bool _repeatAfterRepair; private bool _initialized; public ConnectedPlayerStateRepairModule(EnhancedSpectatorConfig config, IEnhancedSpectatorNetworkService networkService, IConnectedPlayerStateRepairAdapter repairAdapter) { _config = config ?? throw new ArgumentNullException("config"); _networkService = networkService ?? throw new ArgumentNullException("networkService"); _repairAdapter = repairAdapter ?? throw new ArgumentNullException("repairAdapter"); } public void Initialize() { _initialized = true; _nextRepairTime = 0f; _nextIdleFallbackRepairTime = 0f; _lastIdentityRevision = -1; _lastTargetRevision = -1; _repeatAfterRepair = false; } public void Tick() { if (!_initialized) { return; } if (!_config.RepairVanillaConnectedPlayerState.Value) { ResetSchedule(); return; } float unscaledTime = Time.unscaledTime; int remotePeerIdentityRevision = _networkService.RemotePeerIdentityRevision; int remoteSpectatorTargetRevision = _networkService.RemoteSpectatorTargetRevision; if (ConnectedPlayerStateRepairScheduleRules.ShouldRunRepair(_initialized, enabled: true, unscaledTime, _nextRepairTime, _lastIdentityRevision, remotePeerIdentityRevision, _lastTargetRevision, remoteSpectatorTargetRevision, _nextIdleFallbackRepairTime, _repeatAfterRepair)) { _nextRepairTime = unscaledTime + 0.5f; if (RuntimeConnectionState.CanRepairVanillaPlayerState(out string reason)) { _networkService.CopyRemotePeerIdentitiesTo(_identityScratch); _networkService.CopyRemoteSpectatorTargetsTo(_targetScratch); int num = _repairAdapter.RepairConnectedPlayerState(_identityScratch, _targetScratch, _config.RepairVanillaPlayerNames.Value, updateQuickMenu: true, _config.DebugPlayerStateRepair.Value, out reason); _lastIdentityRevision = remotePeerIdentityRevision; _lastTargetRevision = remoteSpectatorTargetRevision; _repeatAfterRepair = num > 0; _nextIdleFallbackRepairTime = unscaledTime + ((num > 0) ? 0.5f : 5f); } } } public void Dispose() { _initialized = false; ResetSchedule(); } private void ResetSchedule() { _nextRepairTime = 0f; _nextIdleFallbackRepairTime = 0f; _lastIdentityRevision = -1; _lastTargetRevision = -1; _repeatAfterRepair = false; } } public static class ConnectedPlayerStateRepairScheduleRules { public static bool ShouldRunRepair(bool initialized, bool enabled, float now, float nextRepairTime, int lastIdentityRevision, int currentIdentityRevision, int lastTargetRevision, int currentTargetRevision, float nextIdleFallbackRepairTime, bool repeatAfterRepair) { if (!initialized || !enabled || now < nextRepairTime) { return false; } if (!repeatAfterRepair && lastIdentityRevision >= 0 && lastTargetRevision >= 0 && lastIdentityRevision == currentIdentityRevision && lastTargetRevision == currentTargetRevision) { return now >= nextIdleFallbackRepairTime; } return true; } } } namespace EnhancedSpectator.Features.ModelInspection { public sealed class DeadBodyHeadRendererInspectionInfo { public string Path { get; } public string Name { get; } public string RendererType { get; } public bool Enabled { get; } public bool ForceRenderingOff { get; } public string SharedMeshName { get; } public int SubMeshCount { get; } public int BonesCount { get; } public string RootBoneName { get; } public Vector3? BoundsCenter { get; } public Vector3? BoundsSize { get; } public int MaterialSlotCount { get; } public IReadOnlyList MaterialNames { get; } public DeadBodyHeadRendererInspectionInfo(string path, string name, string rendererType, bool enabled, bool forceRenderingOff, string sharedMeshName, int subMeshCount, int bonesCount, string rootBoneName, Vector3? boundsCenter, Vector3? boundsSize, int materialSlotCount, IReadOnlyList materialNames) { Path = path; Name = name; RendererType = rendererType; Enabled = enabled; ForceRenderingOff = forceRenderingOff; SharedMeshName = sharedMeshName; SubMeshCount = subMeshCount; BonesCount = bonesCount; RootBoneName = rootBoneName; BoundsCenter = boundsCenter; BoundsSize = boundsSize; MaterialSlotCount = materialSlotCount; MaterialNames = materialNames; } } public sealed class DeadBodyHeadSourceInspectionModule : IFeatureModule, IDisposable, IRuntimeTickable { private readonly EnhancedSpectatorConfig _config; private readonly DeadBodyHeadSourceInspectionService _inspectionService; private bool _initialized; public DeadBodyHeadSourceInspectionModule(EnhancedSpectatorConfig config, DeadBodyHeadSourceInspectionService inspectionService) { _config = config ?? throw new ArgumentNullException("config"); _inspectionService = inspectionService ?? throw new ArgumentNullException("inspectionService"); } public void Initialize() { if (!_initialized) { _initialized = true; ModLog.Debug("Head source inspection module initialized."); } } public void Tick() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (_initialized && _config.EnableRuntimeHeadSourceInspection.Value && SpectatorInputService.IsKeyPressedThisFrame(_config.RuntimeHeadSourceInspectionKey.Value)) { _inspectionService.InspectOnce(); } } public void Dispose() { if (_initialized) { _initialized = false; ModLog.Debug("Head source inspection module disposed."); } } } public sealed class DeadBodyHeadSourceInspectionService { private readonly EnhancedSpectatorConfig _config; private readonly IGameDeadBodyHeadSourceInspectionAdapter _adapter; public DeadBodyHeadSourceInspectionService(EnhancedSpectatorConfig config, IGameDeadBodyHeadSourceInspectionAdapter adapter) { _config = config ?? throw new ArgumentNullException("config"); _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public void InspectOnce() { try { IReadOnlyList snapshots; if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { ModLog.Debug("Head source inspection skipped because runtime state is unsafe: " + reason + "."); } else if (!_adapter.TryGetDeadBodyHeadSourceInspectionSnapshots(_config.RuntimeHeadSourceMaxTransformDepth.Value, _config.RuntimeHeadSourceIncludeRendererBounds.Value, _config.RuntimeHeadSourceIncludeMaterials.Value, out snapshots)) { ModLog.Info("Enhanced Spectator head source inspection found no current player dead bodies."); } else { ModLog.Info(BuildInspectionLog(snapshots)); } } catch (Exception arg) { ModLog.Error($"Enhanced Spectator head source inspection failed: {arg}"); } } private string BuildInspectionLog(IReadOnlyList snapshots) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Enhanced Spectator runtime head source inspection"); stringBuilder.AppendLine($"SnapshotCount={snapshots.Count}"); stringBuilder.AppendLine($"MaxTransformDepth={_config.RuntimeHeadSourceMaxTransformDepth.Value}"); stringBuilder.AppendLine($"IncludeRendererBounds={_config.RuntimeHeadSourceIncludeRendererBounds.Value}"); stringBuilder.AppendLine($"IncludeMaterials={_config.RuntimeHeadSourceIncludeMaterials.Value}"); foreach (DeadBodyHeadSourceInspectionSnapshot snapshot in snapshots) { AppendSnapshot(stringBuilder, snapshot); } return stringBuilder.ToString(); } private static void AppendSnapshot(StringBuilder builder, DeadBodyHeadSourceInspectionSnapshot snapshot) { //IL_02e9: Unknown result type (might be due to invalid IL or missing references) //IL_0302: Unknown result type (might be due to invalid IL or missing references) builder.AppendLine("--- DeadBody Head Source ---"); builder.AppendLine($"playerClientId={snapshot.PlayerClientId}, actualClientId={snapshot.ActualClientId}, name={snapshot.PlayerUsername}, local={snapshot.IsLocalPlayer}, controlled={snapshot.IsPlayerControlled}, dead={snapshot.IsPlayerDead}, root={snapshot.PlayerRootName}"); builder.AppendLine($"deadBody={snapshot.DeadBodyPath}, deactivated={snapshot.DeadBodyDeactivated}"); builder.AppendLine($"detachedHead={snapshot.DetachedHead}, hasDetachedHeadObject={snapshot.HasDetachedHeadObject}, detachedHeadObject={snapshot.DetachedHeadObjectPath}"); builder.AppendLine($"detachedHeadName={snapshot.DetachedHeadObjectName}, parent={snapshot.DetachedHeadParentPath}, activeSelf={snapshot.DetachedHeadActiveSelf}, activeInHierarchy={snapshot.DetachedHeadActiveInHierarchy}"); builder.AppendLine($"bodyParts={snapshot.BodyPartsCount}, bodyParts[0]={snapshot.FirstBodyPartPath}"); builder.AppendLine($"components: rigidbodies={snapshot.RigidbodyCount}, colliders={snapshot.ColliderCount}, audioSources={snapshot.AudioSourceCount}, networkObjects={snapshot.NetworkObjectCount}"); builder.AppendLine($"detachedHeadTransforms={snapshot.DetachedHeadTransforms.Count}"); foreach (TransformInspectionInfo detachedHeadTransform in snapshot.DetachedHeadTransforms) { builder.AppendLine($" transform depth={detachedHeadTransform.Depth} name={detachedHeadTransform.Name} path={detachedHeadTransform.Path}"); } builder.AppendLine($"detachedHeadRenderers={snapshot.DetachedHeadRenderers.Count}"); foreach (DeadBodyHeadRendererInspectionInfo detachedHeadRenderer in snapshot.DetachedHeadRenderers) { builder.Append($" renderer type={detachedHeadRenderer.RendererType} name={detachedHeadRenderer.Name} path={detachedHeadRenderer.Path} enabled={detachedHeadRenderer.Enabled} forceOff={detachedHeadRenderer.ForceRenderingOff} mesh={detachedHeadRenderer.SharedMeshName} subMeshes={detachedHeadRenderer.SubMeshCount} bones={detachedHeadRenderer.BonesCount} rootBone={detachedHeadRenderer.RootBoneName} materialSlots={detachedHeadRenderer.MaterialSlotCount}"); if (detachedHeadRenderer.BoundsCenter.HasValue && detachedHeadRenderer.BoundsSize.HasValue) { builder.Append(" boundsCenter=" + FormatVector(detachedHeadRenderer.BoundsCenter.Value) + " boundsSize=" + FormatVector(detachedHeadRenderer.BoundsSize.Value)); } if (detachedHeadRenderer.MaterialNames.Count > 0) { builder.Append(" materials=[" + string.Join(", ", detachedHeadRenderer.MaterialNames) + "]"); } builder.AppendLine(); } } private static string FormatVector(Vector3 vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x:0.00}, {vector.y:0.00}, {vector.z:0.00})"; } } public sealed class DeadBodyHeadSourceInspectionSnapshot { public ulong PlayerClientId { get; } public ulong ActualClientId { get; } public string PlayerUsername { get; } public bool IsLocalPlayer { get; } public bool IsPlayerDead { get; } public bool IsPlayerControlled { get; } public string PlayerRootName { get; } public string DeadBodyPath { get; } public bool DeadBodyDeactivated { get; } public bool DetachedHead { get; } public int BodyPartsCount { get; } public string FirstBodyPartPath { get; } public bool HasDetachedHeadObject { get; } public string DetachedHeadObjectPath { get; } public string DetachedHeadObjectName { get; } public string DetachedHeadParentPath { get; } public bool DetachedHeadActiveSelf { get; } public bool DetachedHeadActiveInHierarchy { get; } public int RigidbodyCount { get; } public int ColliderCount { get; } public int AudioSourceCount { get; } public int NetworkObjectCount { get; } public IReadOnlyList DetachedHeadTransforms { get; } public IReadOnlyList DetachedHeadRenderers { get; } public DeadBodyHeadSourceInspectionSnapshot(ulong playerClientId, ulong actualClientId, string playerUsername, bool isLocalPlayer, bool isPlayerDead, bool isPlayerControlled, string playerRootName, string deadBodyPath, bool deadBodyDeactivated, bool detachedHead, int bodyPartsCount, string firstBodyPartPath, bool hasDetachedHeadObject, string detachedHeadObjectPath, string detachedHeadObjectName, string detachedHeadParentPath, bool detachedHeadActiveSelf, bool detachedHeadActiveInHierarchy, int rigidbodyCount, int colliderCount, int audioSourceCount, int networkObjectCount, IReadOnlyList detachedHeadTransforms, IReadOnlyList detachedHeadRenderers) { PlayerClientId = playerClientId; ActualClientId = actualClientId; PlayerUsername = playerUsername; IsLocalPlayer = isLocalPlayer; IsPlayerDead = isPlayerDead; IsPlayerControlled = isPlayerControlled; PlayerRootName = playerRootName; DeadBodyPath = deadBodyPath; DeadBodyDeactivated = deadBodyDeactivated; DetachedHead = detachedHead; BodyPartsCount = bodyPartsCount; FirstBodyPartPath = firstBodyPartPath; HasDetachedHeadObject = hasDetachedHeadObject; DetachedHeadObjectPath = detachedHeadObjectPath; DetachedHeadObjectName = detachedHeadObjectName; DetachedHeadParentPath = detachedHeadParentPath; DetachedHeadActiveSelf = detachedHeadActiveSelf; DetachedHeadActiveInHierarchy = detachedHeadActiveInHierarchy; RigidbodyCount = rigidbodyCount; ColliderCount = colliderCount; AudioSourceCount = audioSourceCount; NetworkObjectCount = networkObjectCount; DetachedHeadTransforms = detachedHeadTransforms; DetachedHeadRenderers = detachedHeadRenderers; } } public sealed class ModelInspectionModule : IFeatureModule, IDisposable, IRuntimeTickable { private readonly EnhancedSpectatorConfig _config; private readonly PlayerModelInspectionService _inspectionService; private bool _initialized; public ModelInspectionModule(EnhancedSpectatorConfig config, PlayerModelInspectionService inspectionService) { _config = config ?? throw new ArgumentNullException("config"); _inspectionService = inspectionService ?? throw new ArgumentNullException("inspectionService"); } public void Initialize() { if (!_initialized) { _initialized = true; ModLog.Debug("Model inspection module initialized."); } } public void Tick() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (_initialized && _config.EnableModelInspection.Value && SpectatorInputService.IsKeyPressedThisFrame(_config.ModelInspectionKey.Value)) { _inspectionService.InspectOnce(); } } public void Dispose() { if (_initialized) { _initialized = false; ModLog.Debug("Model inspection module disposed."); } } } public sealed class PlayerModelInspectionService { private readonly EnhancedSpectatorConfig _config; private readonly IGamePlayerModelInspectionAdapter _adapter; public PlayerModelInspectionService(EnhancedSpectatorConfig config, IGamePlayerModelInspectionAdapter adapter) { _config = config ?? throw new ArgumentNullException("config"); _adapter = adapter ?? throw new ArgumentNullException("adapter"); } public void InspectOnce() { try { if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { ModLog.Debug("Model inspection skipped because runtime state is unsafe: " + reason + "."); return; } bool value = _config.LogLocalPlayerModelOnKey.Value; bool value2 = _config.LogRemotePlayerModelsOnKey.Value; IReadOnlyList snapshots; if (!value && !value2) { ModLog.Info("Enhanced Spectator model inspection skipped: local and remote player logging are both disabled."); } else if (!_adapter.TryGetPlayerModelInspectionSnapshots(value, value2, _config.MaxTransformDepth.Value, _config.IncludeRendererBounds.Value, _config.IncludeMaterials.Value, out snapshots)) { ModLog.Info("Enhanced Spectator model inspection found no current player model snapshots."); } else { ModLog.Info(BuildInspectionLog(snapshots)); } } catch (Exception arg) { ModLog.Error($"Enhanced Spectator model inspection failed: {arg}"); } } private string BuildInspectionLog(IReadOnlyList snapshots) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("Enhanced Spectator player model inspection"); stringBuilder.AppendLine($"SnapshotCount={snapshots.Count}"); stringBuilder.AppendLine($"MaxTransformDepth={_config.MaxTransformDepth.Value}"); stringBuilder.AppendLine($"IncludeRendererBounds={_config.IncludeRendererBounds.Value}"); stringBuilder.AppendLine($"IncludeMaterials={_config.IncludeMaterials.Value}"); foreach (PlayerModelInspectionSnapshot snapshot in snapshots) { AppendSnapshot(stringBuilder, snapshot); } return stringBuilder.ToString(); } private static void AppendSnapshot(StringBuilder builder, PlayerModelInspectionSnapshot snapshot) { //IL_02c9: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: Unknown result type (might be due to invalid IL or missing references) builder.AppendLine("--- Player ---"); builder.AppendLine($"playerClientId={snapshot.PlayerClientId}, actualClientId={snapshot.ActualClientId}, local={snapshot.IsLocalPlayer}, controlled={snapshot.IsPlayerControlled}, dead={snapshot.IsPlayerDead}, root={snapshot.RootName}"); builder.AppendLine("thisPlayerBody=" + snapshot.ThisPlayerBodyPath); builder.AppendLine("meshContainer=" + snapshot.MeshContainerPath); builder.AppendLine("lowerSpine=" + snapshot.LowerSpinePath); builder.AppendLine("upperSpine=" + snapshot.UpperSpinePath); builder.AppendLine("playerGlobalHead=" + snapshot.PlayerGlobalHeadPath); builder.AppendLine("playerEye=" + snapshot.PlayerEyePath); builder.AppendLine("headCostumeContainer=" + snapshot.HeadCostumeContainerPath); builder.AppendLine("headCostumeContainerLocal=" + snapshot.HeadCostumeContainerLocalPath); builder.AppendLine($"renderersExist: thisPlayerModel={snapshot.HasThisPlayerModel}, LOD1={snapshot.HasThisPlayerModelLod1}, LOD2={snapshot.HasThisPlayerModelLod2}, arms={snapshot.HasThisPlayerModelArms}"); builder.AppendLine($"headRelatedTransforms={snapshot.HeadRelatedTransforms.Count}"); foreach (TransformInspectionInfo headRelatedTransform in snapshot.HeadRelatedTransforms) { builder.AppendLine($" transform depth={headRelatedTransform.Depth} name={headRelatedTransform.Name} path={headRelatedTransform.Path}"); } builder.AppendLine($"skinnedMeshRenderers={snapshot.SkinnedMeshRenderers.Count}"); foreach (RendererInspectionInfo skinnedMeshRenderer in snapshot.SkinnedMeshRenderers) { builder.Append($" renderer name={skinnedMeshRenderer.Name} path={skinnedMeshRenderer.Path} enabled={skinnedMeshRenderer.Enabled} mesh={skinnedMeshRenderer.SharedMeshName} subMeshes={skinnedMeshRenderer.SubMeshCount} bones={skinnedMeshRenderer.BonesCount} rootBone={skinnedMeshRenderer.RootBoneName} materialSlots={skinnedMeshRenderer.MaterialSlotCount}"); if (skinnedMeshRenderer.BoundsCenter.HasValue && skinnedMeshRenderer.BoundsSize.HasValue) { builder.Append(" boundsCenter=" + FormatVector(skinnedMeshRenderer.BoundsCenter.Value) + " boundsSize=" + FormatVector(skinnedMeshRenderer.BoundsSize.Value)); } if (skinnedMeshRenderer.MaterialNames.Count > 0) { builder.Append(" materials=[" + string.Join(", ", skinnedMeshRenderer.MaterialNames) + "]"); } builder.AppendLine(); } } private static string FormatVector(Vector3 vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x:0.00}, {vector.y:0.00}, {vector.z:0.00})"; } } public sealed class PlayerModelInspectionSnapshot { public ulong PlayerClientId { get; } public ulong ActualClientId { get; } public bool IsPlayerDead { get; } public bool IsPlayerControlled { get; } public bool IsLocalPlayer { get; } public string RootName { get; } public string ThisPlayerBodyPath { get; } public string MeshContainerPath { get; } public string LowerSpinePath { get; } public string UpperSpinePath { get; } public string PlayerGlobalHeadPath { get; } public string PlayerEyePath { get; } public string HeadCostumeContainerPath { get; } public string HeadCostumeContainerLocalPath { get; } public bool HasThisPlayerModel { get; } public bool HasThisPlayerModelLod1 { get; } public bool HasThisPlayerModelLod2 { get; } public bool HasThisPlayerModelArms { get; } public IReadOnlyList HeadRelatedTransforms { get; } public IReadOnlyList SkinnedMeshRenderers { get; } public PlayerModelInspectionSnapshot(ulong playerClientId, ulong actualClientId, bool isPlayerDead, bool isPlayerControlled, bool isLocalPlayer, string rootName, string thisPlayerBodyPath, string meshContainerPath, string lowerSpinePath, string upperSpinePath, string playerGlobalHeadPath, string playerEyePath, string headCostumeContainerPath, string headCostumeContainerLocalPath, bool hasThisPlayerModel, bool hasThisPlayerModelLod1, bool hasThisPlayerModelLod2, bool hasThisPlayerModelArms, IReadOnlyList headRelatedTransforms, IReadOnlyList skinnedMeshRenderers) { PlayerClientId = playerClientId; ActualClientId = actualClientId; IsPlayerDead = isPlayerDead; IsPlayerControlled = isPlayerControlled; IsLocalPlayer = isLocalPlayer; RootName = rootName; ThisPlayerBodyPath = thisPlayerBodyPath; MeshContainerPath = meshContainerPath; LowerSpinePath = lowerSpinePath; UpperSpinePath = upperSpinePath; PlayerGlobalHeadPath = playerGlobalHeadPath; PlayerEyePath = playerEyePath; HeadCostumeContainerPath = headCostumeContainerPath; HeadCostumeContainerLocalPath = headCostumeContainerLocalPath; HasThisPlayerModel = hasThisPlayerModel; HasThisPlayerModelLod1 = hasThisPlayerModelLod1; HasThisPlayerModelLod2 = hasThisPlayerModelLod2; HasThisPlayerModelArms = hasThisPlayerModelArms; HeadRelatedTransforms = headRelatedTransforms; SkinnedMeshRenderers = skinnedMeshRenderers; } } public sealed class RendererInspectionInfo { public string Path { get; } public string Name { get; } public bool Enabled { get; } public string SharedMeshName { get; } public int SubMeshCount { get; } public int BonesCount { get; } public string RootBoneName { get; } public Vector3? BoundsCenter { get; } public Vector3? BoundsSize { get; } public int MaterialSlotCount { get; } public IReadOnlyList MaterialNames { get; } public RendererInspectionInfo(string path, string name, bool enabled, string sharedMeshName, int subMeshCount, int bonesCount, string rootBoneName, Vector3? boundsCenter, Vector3? boundsSize, int materialSlotCount, IReadOnlyList materialNames) { Path = path; Name = name; Enabled = enabled; SharedMeshName = sharedMeshName; SubMeshCount = subMeshCount; BonesCount = bonesCount; RootBoneName = rootBoneName; BoundsCenter = boundsCenter; BoundsSize = boundsSize; MaterialSlotCount = materialSlotCount; MaterialNames = materialNames; } } public sealed class TransformInspectionInfo { public string Path { get; } public string Name { get; } public int Depth { get; } public TransformInspectionInfo(string path, string name, int depth) { Path = path; Name = name; Depth = depth; } } } namespace EnhancedSpectator.Features.FloatingHead { public static class DetachedHeadVisualSourceRules { public static bool ShouldUseRuntimeDetachedHead(bool useRuntimeDetachedHeadVisuals, bool hasRuntimeDetachedHeadTemplate) { return useRuntimeDetachedHeadVisuals && hasRuntimeDetachedHeadTemplate; } public static bool TryResolveVisualSourceKind(bool enablePlaceholderVisuals, bool useRuntimeDetachedHeadVisuals, bool hasRuntimeDetachedHeadTemplate, bool fallbackToPlaceholderWhenDetachedHeadUnavailable, out FloatingHeadVisualSourceKind sourceKind) { if (ShouldUseRuntimeDetachedHead(useRuntimeDetachedHeadVisuals, hasRuntimeDetachedHeadTemplate)) { sourceKind = FloatingHeadVisualSourceKind.RuntimeDetachedHead; return true; } if (!enablePlaceholderVisuals) { sourceKind = FloatingHeadVisualSourceKind.Placeholder; return false; } if (!useRuntimeDetachedHeadVisuals || fallbackToPlaceholderWhenDetachedHeadUnavailable) { sourceKind = FloatingHeadVisualSourceKind.Placeholder; return true; } sourceKind = FloatingHeadVisualSourceKind.Placeholder; return false; } } public static class FloatingHeadFrameUpdateRules { public static bool ShouldRunFullVisualUpdate(int currentFrame, int lastFullUpdateFrame, bool hasCachedVisualState) { if (hasCachedVisualState) { return lastFullUpdateFrame != currentFrame; } return true; } } public sealed class FloatingHeadModule : IFeatureModule, IDisposable, IRuntimeLateTickable, IRuntimeCameraPreCullTickable, IRuntimeGuiTickable { private readonly FloatingHeadVisualService _visualService; private bool _initialized; public FloatingHeadModule(FloatingHeadVisualService visualService) { _visualService = visualService ?? throw new ArgumentNullException("visualService"); } public void Initialize() { if (!_initialized) { _initialized = true; ModLog.Debug("Floating-head placeholder module initialized."); } } public void LateTick() { if (_initialized) { _visualService.LateTick(); } } public void CameraPreCullTick(Camera camera) { if (_initialized) { _visualService.CameraPreCullTick(camera); } } public void GuiTick() { if (_initialized) { _visualService.GuiTick(); } } public void Dispose() { if (_initialized) { _visualService.Dispose(); _initialized = false; ModLog.Debug("Floating-head placeholder module disposed."); } } } public sealed class FloatingHeadPlacementService { private readonly IGameSpectatorAdapter _gameSpectatorAdapter; public FloatingHeadPlacementService(IGameSpectatorAdapter gameSpectatorAdapter) { _gameSpectatorAdapter = gameSpectatorAdapter ?? throw new ArgumentNullException("gameSpectatorAdapter"); } public bool TryGetAnchorPosition(out Vector3 position) { return _gameSpectatorAdapter.TryGetLocalPlayerHeadAnchorPosition(out position); } public bool TryGetPose(Vector3 anchorPosition, int visualIndex, int visualCount, float ringRadius, float heightOffset, bool useCameraVisiblePlacement, float cameraForwardOffset, bool faceCamera, Camera? renderingCamera, out Vector3 position, out Quaternion rotation) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) int num = Mathf.Max(1, visualCount); float num2 = Mathf.Max(0f, ringRadius); rotation = Quaternion.identity; Camera camera; Camera val = renderingCamera ?? (TryGetFacingCamera(out camera) ? camera : null); if (useCameraVisiblePlacement && (Object)(object)val != (Object)null) { position = GetCameraVisiblePosition(anchorPosition, val, visualIndex, num, num2, heightOffset, Mathf.Max(0.2f, cameraForwardOffset)); } else { float num3 = MathF.PI * 2f * (float)visualIndex / (float)num; Vector3 val2 = default(Vector3); ((Vector3)(ref val2))..ctor(Mathf.Cos(num3) * num2, heightOffset, Mathf.Sin(num3) * num2); position = anchorPosition + val2; } if (faceCamera && (Object)(object)val != (Object)null) { Vector3 val3 = ((Component)val).transform.position - position; if (((Vector3)(ref val3)).sqrMagnitude > 0.0001f) { rotation = Quaternion.LookRotation(((Vector3)(ref val3)).normalized, Vector3.up); } } return true; } public bool TryGetActiveCamera(out Camera? camera) { if (_gameSpectatorAdapter.TryGetActiveCamera(out camera) && (Object)(object)camera != (Object)null) { return true; } camera = Camera.main; if ((Object)(object)camera != (Object)null) { return true; } camera = Camera.current; return (Object)(object)camera != (Object)null; } public bool TryGetPlayerDisplayName(ulong clientId, ulong slotId, out string displayName) { return _gameSpectatorAdapter.TryGetPlayerDisplayName(clientId, slotId, out displayName); } private bool TryGetFacingCamera(out Camera? camera) { return TryGetActiveCamera(out camera); } private static Vector3 GetCameraVisiblePosition(Vector3 anchorPosition, Camera camera, int visualIndex, int visualCount, float ringRadius, float heightOffset, float cameraForwardOffset) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) Transform transform = ((Component)camera).transform; Vector3 val = transform.forward; Vector3 val2; if (!(((Vector3)(ref val)).sqrMagnitude > 0.0001f)) { val2 = Vector3.forward; } else { val = transform.forward; val2 = ((Vector3)(ref val)).normalized; } Vector3 val3 = val2; val = transform.right; Vector3 val4; if (!(((Vector3)(ref val)).sqrMagnitude > 0.0001f)) { val4 = Vector3.right; } else { val = transform.right; val4 = ((Vector3)(ref val)).normalized; } Vector3 val5 = val4; val = transform.up; Vector3 val6; if (!(((Vector3)(ref val)).sqrMagnitude > 0.0001f)) { val6 = Vector3.up; } else { val = transform.up; val6 = ((Vector3)(ref val)).normalized; } Vector3 val7 = val6; float num = (float)(visualCount - 1) * 0.5f; float num2 = ((float)visualIndex - num) * Mathf.Max(0.2f, ringRadius); float num3 = heightOffset + Mathf.Abs(num2) * 0.15f; float num4 = Mathf.Max(camera.nearClipPlane + 0.35f, cameraForwardOffset); return transform.position + val3 * num4 + val5 * num2 + val7 * num3; } } public static class FloatingHeadRotationRules { public const float DefaultRuntimeDetachedHeadPitchOffsetDegrees = -90f; public const float DefaultRuntimeDetachedHeadYawOffsetDegrees = 360f; public const float DefaultRuntimeDetachedHeadRollOffsetDegrees = 0f; public static bool ShouldFaceLocalCamera(FloatingHeadVisualSourceKind sourceKind, bool faceCameraConfig) { if (faceCameraConfig) { return sourceKind != FloatingHeadVisualSourceKind.RuntimeDetachedHead; } return false; } public static Quaternion ApplyRuntimeDetachedHeadOffset(Quaternion remoteRotation, float pitchOffsetDegrees, float yawOffsetDegrees, float rollOffsetDegrees) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) return remoteRotation * Quaternion.Euler(pitchOffsetDegrees, yawOffsetDegrees, rollOffsetDegrees); } } public sealed class FloatingHeadVisual : IDisposable { private readonly GameObject _gameObject; private readonly Material? _material; private readonly Mesh? _runtimeMesh; private readonly MeshRenderer? _meshRenderer; private readonly MeshFilter? _meshFilter; private readonly NameTagVisual? _nameTag; private readonly Color _baseColor; private readonly float _baseAlpha; private readonly bool _colliderRemoved; private Vector3 _smoothedPosition; private Vector3 _positionVelocity; private Quaternion _smoothedRotation = Quaternion.identity; private float _smoothedScale; private float _scaleVelocity; private float _smoothedVoiceLevel; private float _voiceVelocity; private int _lastSmoothingFrame = -1; private int _lastVoiceSmoothingFrame = -1; private int _currentLayer = -1; private bool _hasPose; private bool _disposed; public ulong SpectatorClientId { get; } public ulong SpectatorSlotId { get; } public FloatingHeadVisualSourceKind SourceKind { get; } public string ShaderName { get { if (!((Object)(object)_material != (Object)null) || !((Object)(object)_material.shader != (Object)null)) { return "none"; } return ((Object)_material.shader).name; } } public Color BaseColor => _baseColor; public int MaterialRenderQueue { get { if (!((Object)(object)_material != (Object)null)) { return -1; } return _material.renderQueue; } } public bool HasMeshRenderer => (Object)(object)_meshRenderer != (Object)null; public bool HasMeshFilter => (Object)(object)_meshFilter != (Object)null; public string MeshName { get { Mesh val = (((Object)(object)_meshFilter != (Object)null) ? _meshFilter.sharedMesh : null); if (!((Object)(object)val != (Object)null)) { return "none"; } return ((Object)val).name; } } public bool ColliderRemoved => _colliderRemoved; public bool HasNameTag => _nameTag != null; public string NameTagText { get { if (_nameTag == null) { return string.Empty; } return _nameTag.Text; } } public bool RendererEnabled { get { if ((Object)(object)_meshRenderer != (Object)null) { return ((Renderer)_meshRenderer).enabled; } return false; } } public bool ForceRenderingOff { get { if ((Object)(object)_meshRenderer != (Object)null) { return ((Renderer)_meshRenderer).forceRenderingOff; } return false; } } public bool ActiveSelf { get { if ((Object)(object)_gameObject != (Object)null) { return _gameObject.activeSelf; } return false; } } public bool ActiveInHierarchy { get { if ((Object)(object)_gameObject != (Object)null) { return _gameObject.activeInHierarchy; } return false; } } public int Layer { get { if (!((Object)(object)_gameObject != (Object)null)) { return -1; } return _gameObject.layer; } } public bool RootActiveInHierarchy { get { if ((Object)(object)_gameObject == (Object)null || (Object)(object)_gameObject.transform.parent == (Object)null) { return false; } return ((Component)_gameObject.transform.parent).gameObject.activeInHierarchy; } } public FloatingHeadVisualState State { get; private set; } public float CurrentScale { get { if (!_hasPose) { return 1f; } return Mathf.Max(0.01f, _smoothedScale); } } public FloatingHeadVisual(ulong spectatorClientId, ulong spectatorSlotId, FloatingHeadVisualSourceKind sourceKind, GameObject gameObject, Material? material, Mesh? runtimeMesh, Color baseColor, float baseAlpha, MeshRenderer? meshRenderer, MeshFilter? meshFilter, bool colliderRemoved, NameTagVisual? nameTag) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) SpectatorClientId = spectatorClientId; SpectatorSlotId = spectatorSlotId; SourceKind = sourceKind; _gameObject = gameObject ?? throw new ArgumentNullException("gameObject"); _material = material; _runtimeMesh = runtimeMesh; _meshRenderer = meshRenderer; _meshFilter = meshFilter; _nameTag = nameTag; _baseColor = baseColor; _baseAlpha = Mathf.Clamp01(baseAlpha); _colliderRemoved = colliderRemoved; State = new FloatingHeadVisualState(spectatorClientId, spectatorSlotId, isVisible: false, Vector3.zero); } public float UpdateVoiceLevel(float targetLevel, float smoothTime) { return UpdateVoiceLevel(targetLevel, smoothTime, smoothTime); } public float UpdateVoiceLevel(float targetLevel, float attackSmoothTime, float releaseSmoothTime) { if (_disposed) { return 0f; } float num = Mathf.Clamp01(targetLevel); float num2 = FloatingHeadVoiceScaleRules.ResolveVoiceSmoothTime(_smoothedVoiceLevel, num, attackSmoothTime, releaseSmoothTime); if (num2 <= 0f) { _smoothedVoiceLevel = num; _voiceVelocity = 0f; _lastVoiceSmoothingFrame = Time.frameCount; } else if (_lastVoiceSmoothingFrame != Time.frameCount) { _smoothedVoiceLevel = Mathf.SmoothDamp(_smoothedVoiceLevel, num, ref _voiceVelocity, num2, float.PositiveInfinity, Mathf.Max(Time.deltaTime, 0.0001f)); _lastVoiceSmoothingFrame = Time.frameCount; } ApplyMaterialVoiceLevel(_smoothedVoiceLevel); return _smoothedVoiceLevel; } public void ApplyPose(Vector3 position, Quaternion rotation, float scale) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) ApplyPose(position, rotation, scale, 0f); } public void ApplyPose(Vector3 position, Quaternion rotation, float scale, float smoothTime) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) ApplyPose(position, rotation, scale, smoothTime, smoothTime); } public void ApplyPose(Vector3 position, Quaternion rotation, float scale, float smoothTime, float scaleSmoothTime) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Unknown result type (might be due to invalid IL or missing references) //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) if (_disposed || (Object)(object)_gameObject == (Object)null) { return; } Vector3 val = position; Quaternion val2 = rotation; float num = Mathf.Max(0f, smoothTime); float num2 = Mathf.Max(0f, scaleSmoothTime); if (!_hasPose || num <= 0f) { _smoothedPosition = position; _positionVelocity = Vector3.zero; _smoothedRotation = rotation; _smoothedScale = Mathf.Max(0.01f, scale); _scaleVelocity = 0f; _hasPose = true; _lastSmoothingFrame = Time.frameCount; } else { if (_lastSmoothingFrame != Time.frameCount) { float num3 = Mathf.Max(Time.deltaTime, 0.0001f); _smoothedPosition = Vector3.SmoothDamp(_smoothedPosition, position, ref _positionVelocity, num, float.PositiveInfinity, num3); float num4 = 1f - Mathf.Exp((0f - num3) / Mathf.Max(0.0001f, num)); _smoothedRotation = Quaternion.Slerp(_smoothedRotation, rotation, num4); if (num2 <= 0f) { _smoothedScale = Mathf.Max(0.01f, scale); _scaleVelocity = 0f; } else { _smoothedScale = Mathf.SmoothDamp(_smoothedScale, Mathf.Max(0.01f, scale), ref _scaleVelocity, num2, float.PositiveInfinity, num3); } _lastSmoothingFrame = Time.frameCount; } val = _smoothedPosition; val2 = _smoothedRotation; scale = _smoothedScale; } Transform transform = _gameObject.transform; transform.SetPositionAndRotation(val, val2); transform.localScale = Vector3.one * Mathf.Max(0.01f, scale); SetVisible(visible: true); State = new FloatingHeadVisualState(SpectatorClientId, SpectatorSlotId, isVisible: true, val); _nameTag?.ApplyPose(val, val2, null); } public void UpdateNameTag(Camera? camera) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) if (!_disposed && _nameTag != null && !((Object)(object)_gameObject == (Object)null) && State.IsVisible) { _nameTag.ApplyPose(State.Position, _gameObject.transform.rotation, camera); } } public bool TrySetNameTagText(string text) { if (_disposed || _nameTag == null) { return false; } return _nameTag.SetText(text); } public void SetLayer(int layer) { if (!_disposed && !((Object)(object)_gameObject == (Object)null) && layer >= 0 && layer <= 31 && _currentLayer != layer) { SetLayerRecursive(_gameObject.transform, layer); _currentLayer = layer; _nameTag?.SetLayer(layer); } } public void SetVisible(bool visible) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (!_disposed && !((Object)(object)_gameObject == (Object)null)) { if (_gameObject.activeSelf != visible) { _gameObject.SetActive(visible); } _nameTag?.SetVisible(visible); State = new FloatingHeadVisualState(SpectatorClientId, SpectatorSlotId, visible, _gameObject.transform.position); } } public void Dispose() { if (!_disposed) { _disposed = true; if ((Object)(object)_gameObject != (Object)null) { Object.Destroy((Object)(object)_gameObject); } if ((Object)(object)_material != (Object)null) { Object.Destroy((Object)(object)_material); } if ((Object)(object)_runtimeMesh != (Object)null) { Object.Destroy((Object)(object)_runtimeMesh); } _nameTag?.Dispose(); } } private void ApplyMaterialVoiceLevel(float voiceLevel) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_material == (Object)null)) { float num = 1f + voiceLevel * 0.65f; Color color = _baseColor * num; color.a = _baseAlpha; SetMaterialColor("_BaseColor", color); SetMaterialColor("_UnlitColor", color); SetMaterialColor("_Color", color); SetMaterialColor("_EmissionColor", color); SetMaterialColor("_EmissiveColor", color); } } private void SetMaterialColor(string propertyName, Color color) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_material != (Object)null && _material.HasProperty(propertyName)) { _material.SetColor(propertyName, color); } } private static void SetLayerRecursive(Transform transform, int layer) { if (((Component)transform).gameObject.layer != layer) { ((Component)transform).gameObject.layer = layer; } for (int i = 0; i < transform.childCount; i++) { Transform child = transform.GetChild(i); if ((Object)(object)child != (Object)null) { SetLayerRecursive(child, layer); } } } } public sealed class FloatingHeadVisualService : IDisposable { private readonly struct NameTagTextCacheEntry { public ulong SpectatorSlotId { get; } public string Text { get; } public NameTagTextCacheEntry(ulong spectatorSlotId, string text) { SpectatorSlotId = spectatorSlotId; Text = text; } } private readonly EnhancedSpectatorConfig _config; private readonly ISpectatorPresenceProvider _presenceProvider; private readonly IVoiceActivityProvider _voiceActivityProvider; private readonly IEnhancedSpectatorNetworkService? _networkService; private readonly IGameDetachedHeadVisualSourceAdapter _detachedHeadVisualSourceAdapter; private readonly FloatingHeadPlacementService _placementService; private readonly PlaceholderHeadVisualFactory _visualFactory; private readonly Dictionary _visuals = new Dictionary(); private readonly HashSet _activeSpectatorIds = new HashSet(); private readonly List _staleVisualIds = new List(); private readonly List _sortedSpectators = new List(); private readonly HashSet _poseLoggedSpectators = new HashSet(); private readonly HashSet _remotePoseLoggedSpectators = new HashSet(); private readonly HashSet _screenFallbackLoggedSpectators = new HashSet(); private readonly HashSet _voiceDataLoggedSpectators = new HashSet(); private readonly HashSet _voiceDataActiveSpectators = new HashSet(); private readonly HashSet _visualSkipLoggedSpectators = new HashSet(); private readonly Dictionary _nameTagTextCache = new Dictionary(); private Texture2D? _screenMarkerTexture; private Transform? _detachedHeadTemplateSource; private Vector3 _lastAnchorPosition; private float _lastPresenceSeenUnscaledTime = float.NegativeInfinity; private int _lastFullUpdateFrame = -1; private bool _anchorLostLogged; private bool _disposed; private bool _disabledDueToError; private bool _voiceProviderDisabled; private bool _presenceGraceLogged; private bool _layerWarningLogged; private bool _hasCachedVisualState; private int _cachedNameTagIdentityRevision = int.MinValue; private bool _cachedNameTagUseGamePlayerNames; private bool _cachedNameTagUseFallbackIds; public FloatingHeadVisualService(EnhancedSpectatorConfig config, ISpectatorPresenceProvider presenceProvider, IVoiceActivityProvider voiceActivityProvider, IEnhancedSpectatorNetworkService? networkService, IGameDetachedHeadVisualSourceAdapter detachedHeadVisualSourceAdapter, FloatingHeadPlacementService placementService, PlaceholderHeadVisualFactory visualFactory) { _config = config ?? throw new ArgumentNullException("config"); _presenceProvider = presenceProvider ?? throw new ArgumentNullException("presenceProvider"); _voiceActivityProvider = voiceActivityProvider ?? throw new ArgumentNullException("voiceActivityProvider"); _networkService = networkService; _detachedHeadVisualSourceAdapter = detachedHeadVisualSourceAdapter ?? throw new ArgumentNullException("detachedHeadVisualSourceAdapter"); _placementService = placementService ?? throw new ArgumentNullException("placementService"); _visualFactory = visualFactory ?? throw new ArgumentNullException("visualFactory"); } public void LateTick() { if (_disposed || _disabledDueToError) { return; } try { if (FloatingHeadFrameUpdateRules.ShouldRunFullVisualUpdate(Time.frameCount, _lastFullUpdateFrame, _hasCachedVisualState)) { TickCore(null, "LateUpdate", _config.DebugVisualLifecycle.Value); } } catch (Exception arg) { ModLog.Error($"Floating-head placeholder visual failed and was disabled: {arg}"); DestroyAll("exception"); _disabledDueToError = true; } } public void CameraPreCullTick(Camera camera) { if (_disposed || _disabledDueToError || !IsRenderableGameCamera(camera)) { return; } try { if (FloatingHeadFrameUpdateRules.ShouldRunFullVisualUpdate(Time.frameCount, _lastFullUpdateFrame, _hasCachedVisualState)) { TickCore(camera, "PreCull", logPose: true); } else { UpdateCameraOnlyPose(camera, "PreCull", logPose: true); } } catch (Exception arg) { ModLog.Error($"Floating-head placeholder visual pre-cull update failed and was disabled: {arg}"); DestroyAll("pre-cull exception"); _disabledDueToError = true; } } public void GuiTick() { //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Invalid comparison between Unknown and I4 if (_disposed || _disabledDueToError || !_config.EnableScreenFallbackVisual.Value || !_config.EnableFloatingHeadVisuals.Value || (!_config.EnablePlaceholderVisuals.Value && !_config.UseRuntimeDetachedHeadVisuals.Value) || Event.current == null || (int)Event.current.type != 7 || !RuntimeConnectionState.CanUseModNetworking(out string _) || !_placementService.TryGetActiveCamera(out Camera camera) || (Object)(object)camera == (Object)null) { return; } foreach (FloatingHeadVisual value in _visuals.Values) { DrawScreenFallback(camera, value); } } public void Clear() { DestroyAll("clear"); } public void Dispose() { if (!_disposed) { DestroyAll("dispose"); if ((Object)(object)_screenMarkerTexture != (Object)null) { Object.Destroy((Object)(object)_screenMarkerTexture); _screenMarkerTexture = null; } _visualFactory.Dispose(); _disposed = true; } } private void TickCore(Camera? renderingCamera, string poseSource, bool logPose) { //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) if (!_config.EnableFloatingHeadVisuals.Value || (!_config.EnablePlaceholderVisuals.Value && !_config.UseRuntimeDetachedHeadVisuals.Value)) { DestroyAll("disabled by config"); return; } if (!RuntimeConnectionState.CanUseModNetworking(out string reason)) { DestroyAll("network/lifecycle unavailable: " + reason); return; } LocalSpectatorPresenceState current = _presenceProvider.Current; if (!current.HasLocalPlayer || current.RemoteSpectators.Count == 0) { if (!ShouldHoldPresenceGrace()) { ClearForPresenceLost(); } return; } _lastPresenceSeenUnscaledTime = Time.unscaledTime; _presenceGraceLogged = false; if (!_placementService.TryGetAnchorPosition(out var position)) { if (!_anchorLostLogged) { LogDebug("Floating-head placeholder anchor lost; clearing visuals."); _anchorLostLogged = true; } DestroyAll("anchor lost"); } else { _anchorLostLogged = false; IReadOnlyList spectators = SortSpectators(current.RemoteSpectators); _lastAnchorPosition = position; _hasCachedVisualState = true; _lastFullUpdateFrame = Time.frameCount; SyncVisuals(spectators); UpdateVisualPoses(position, spectators, renderingCamera, poseSource, logPose, updateDynamicState: true); } } private void UpdateCameraOnlyPose(Camera renderingCamera, string poseSource, bool logPose) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (_hasCachedVisualState && _visuals.Count != 0 && _sortedSpectators.Count != 0) { UpdateVisualPoses(_lastAnchorPosition, _sortedSpectators, renderingCamera, poseSource, logPose, updateDynamicState: false); } } private void ClearForPresenceLost() { InvalidateCachedVisualState(); if (_config.DestroyOnPresenceLost.Value) { DestroyAll("presence lost"); return; } foreach (FloatingHeadVisual value in _visuals.Values) { value.SetVisible(visible: false); } } private bool ShouldHoldPresenceGrace() { if (_visuals.Count == 0) { return false; } float num = Mathf.Max(0f, _config.PresenceLostGraceSeconds.Value); if (num <= 0f) { return false; } float num2 = Time.unscaledTime - _lastPresenceSeenUnscaledTime; if (num2 < 0f || num2 > num) { return false; } if (!_presenceGraceLogged) { LogDebug($"Floating-head placeholder presence temporarily empty; holding visuals for {num:0.00}s grace window."); _presenceGraceLogged = true; } return true; } private void SyncVisuals(IReadOnlyList spectators) { _activeSpectatorIds.Clear(); Transform source; bool flag = TryGetRuntimeDetachedHeadTemplate(out source); foreach (RemoteSpectatorInfo spectator in spectators) { _activeSpectatorIds.Add(spectator.SpectatorClientId); FloatingHeadVisualSourceKind sourceKind; bool flag2 = DetachedHeadVisualSourceRules.TryResolveVisualSourceKind(_config.EnablePlaceholderVisuals.Value, _config.UseRuntimeDetachedHeadVisuals.Value, flag, _config.FallbackToPlaceholderWhenDetachedHeadUnavailable.Value, out sourceKind); if (_visuals.TryGetValue(spectator.SpectatorClientId, out FloatingHeadVisual value)) { if (!flag2) { RemoveVisual(spectator.SpectatorClientId, "visual source unavailable"); LogVisualSkippedOnce(spectator, flag); continue; } if (value.SourceKind == sourceKind) { continue; } RemoveVisual(spectator.SpectatorClientId, $"visual source changed from {value.SourceKind} to {sourceKind}"); } if (!flag2) { LogVisualSkippedOnce(spectator, flag); } else if (!_visuals.ContainsKey(spectator.SpectatorClientId)) { FloatingHeadVisual floatingHeadVisual = CreateVisual(spectator, sourceKind, source); if (floatingHeadVisual == null) { LogVisualSkippedOnce(spectator, flag); continue; } _visuals[spectator.SpectatorClientId] = floatingHeadVisual; _visualSkipLoggedSpectators.Remove(spectator.SpectatorClientId); LogDebug($"Floating-head visual created: spectatorClient={spectator.SpectatorClientId}, spectatorSlot={spectator.SpectatorSlotId}, source={floatingHeadVisual.SourceKind}, style={_config.VisualStyle.Value}, {GetCreationDebugInfo(floatingHeadVisual)}."); } } _staleVisualIds.Clear(); foreach (ulong key in _visuals.Keys) { if (!_activeSpectatorIds.Contains(key)) { _staleVisualIds.Add(key); } } foreach (ulong staleVisualId in _staleVisualIds) { RemoveVisual(staleVisualId, "presence lost"); } _staleVisualIds.Clear(); } private void UpdateVisualPoses(Vector3 anchorPosition, IReadOnlyList spectators, Camera? renderingCamera, string poseSource, bool logPose, bool updateDynamicState) { //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) int count = spectators.Count; for (int i = 0; i < count; i++) { RemoteSpectatorInfo remoteSpectatorInfo = spectators[i]; if (!_visuals.TryGetValue(remoteSpectatorInfo.SpectatorClientId, out FloatingHeadVisual value)) { continue; } float scale = (updateDynamicState ? CalculateVisualScale(remoteSpectatorInfo, value) : value.CurrentScale); if (updateDynamicState) { UpdateNameTagText(remoteSpectatorInfo, value); } Vector3 position2; Quaternion rotation2; if (TryGetRemoteSpectatorPose(remoteSpectatorInfo, value.SourceKind, renderingCamera, out var position, out var rotation, out var rawPosition, out var usedVisibleProxy)) { int num = ResolveVisibleLayer(renderingCamera); value.SetLayer(num); value.ApplyPose(position, rotation, scale, Mathf.Max(0f, _config.RemotePoseSmoothTime.Value), 0f); value.UpdateNameTag(renderingCamera); if (logPose) { LogFirstRemotePose(remoteSpectatorInfo.SpectatorClientId, position, rawPosition, anchorPosition, value, num, renderingCamera, usedVisibleProxy ? (poseSource + ":RemotePoseVisibleProxy") : (poseSource + ":RemotePose"), usedVisibleProxy, rotation); } } else if (_placementService.TryGetPose(anchorPosition, i, count, _config.FloatingHeadRingRadius.Value, _config.FloatingHeadHeightOffset.Value, _config.UseCameraVisiblePlacement.Value, _config.CameraForwardOffset.Value, _config.FloatingHeadFaceCamera.Value, renderingCamera, out position2, out rotation2)) { int num2 = ResolveVisibleLayer(renderingCamera); value.SetLayer(num2); value.ApplyPose(position2, rotation2, scale); value.UpdateNameTag(renderingCamera); if (logPose) { LogFirstPose(remoteSpectatorInfo.SpectatorClientId, position2, anchorPosition, value, num2, renderingCamera, poseSource); } } } } private float CalculateVisualScale(RemoteSpectatorInfo spectator, FloatingHeadVisual visual) { float num = 0f; num = FloatingHeadVoiceScaleRules.ResolveTargetVoiceLevel(TryGetVoiceActivity(spectator, out VoiceActivityState state) && state.HasData, state.IsSpeaking, state.Amplitude, _config.MinimumSpeakingVoiceLevel.Value); float voiceLevel = visual.UpdateVoiceLevel(num, Mathf.Max(0f, _config.VoiceAttackSmoothTime.Value), Mathf.Max(0f, _config.VoiceReleaseSmoothTime.Value)); float pulse = 0.5f + 0.5f * Mathf.Sin(Time.time * Mathf.Max(0f, _config.SpeakingPulseSpeed.Value)); float num2 = FloatingHeadVoiceScaleRules.ResolveScaleMultiplier(_config.SilenceScaleMultiplier.Value, _config.SpeakingScaleMultiplier.Value, voiceLevel, _config.PulseWhenSpeaking.Value, _config.SpeakingPulseAmount.Value, pulse); float num3 = ((visual.SourceKind == FloatingHeadVisualSourceKind.RuntimeDetachedHead) ? _config.RuntimeDetachedHeadScale.Value : ((_config.VisualStyle.Value == FloatingHeadVisualStyle.Sphere) ? _config.PlaceholderScale.Value : _config.BillboardSize.Value)); return Mathf.Max(0.01f, num3 * num2); } private FloatingHeadVisual? CreateVisual(RemoteSpectatorInfo spectator, FloatingHeadVisualSourceKind sourceKind, Transform? detachedHeadSource) { bool canCache; string text = FormatNameTagText(spectator, out canCache); if (canCache) { _nameTagTextCache[spectator.SpectatorClientId] = new NameTagTextCacheEntry(spectator.SpectatorSlotId, text); } if (sourceKind == FloatingHeadVisualSourceKind.RuntimeDetachedHead && (Object)(object)detachedHeadSource != (Object)null) { return _visualFactory.CreateFromDetachedHead(spectator, detachedHeadSource, _config.RuntimeDetachedHeadScale.Value, _config.ShowNameTags.Value, _config.NameTagScale.Value, _config.NameTagHeightOffset.Value, _config.NameTagMaxDistance.Value, text); } if (sourceKind != 0) { return null; } return _visualFactory.Create(spectator, _config.PlaceholderScale.Value, _config.VisualStyle.Value, _config.BillboardSize.Value, _config.BaseAlpha.Value, _config.UseUnlitMaterial.Value, _config.EnableDepthTest.Value, _config.ShowNameTags.Value, _config.NameTagScale.Value, _config.NameTagHeightOffset.Value, _config.NameTagMaxDistance.Value, text); } private void LogVisualSkippedOnce(RemoteSpectatorInfo spectator, bool hasDetachedHeadTemplate) { if (_visualSkipLoggedSpectators.Add(spectator.SpectatorClientId)) { LogDebug($"Floating-head visual skipped: spectatorClient={spectator.SpectatorClientId}, detachedHeadEnabled={_config.UseRuntimeDetachedHeadVisuals.Value}, hasDetachedHeadTemplate={hasDetachedHeadTemplate}, placeholderEnabled={_config.EnablePlaceholderVisuals.Value}, fallbackEnabled={_config.FallbackToPlaceholderWhenDetachedHeadUnavailable.Value}."); } } private bool TryGetRuntimeDetachedHeadTemplate(out Transform? source) { if (!_config.UseRuntimeDetachedHeadVisuals.Value) { _detachedHeadTemplateSource = null; source = null; return false; } if ((Object)(object)_detachedHeadTemplateSource != (Object)null) { source = _detachedHeadTemplateSource; return true; } try { if (_detachedHeadVisualSourceAdapter.TryGetDetachedHeadVisualTemplate(out source) && (Object)(object)source != (Object)null) { _detachedHeadTemplateSource = source; LogDebug("Runtime detached-head visual template resolved: source=" + ((Object)source).name + "."); return true; } source = null; return false; } catch (Exception ex) { source = null; _detachedHeadTemplateSource = null; LogDebug("Runtime detached-head visual template lookup failed: " + ex.GetType().Name + "."); return false; } } private bool TryGetVoiceActivity(RemoteSpectatorInfo spectator, out VoiceActivityState state) { state = VoiceActivityState.NoData; if (TryGetSyncedVoiceActivity(spectator, out state)) { return true; } if (!FloatingHeadVoiceActivityRules.ShouldUseLocalFallback(_config.EnableVoiceActivitySync.Value, _networkService != null)) { LogVoiceDataLost(spectator.SpectatorClientId); return false; } if (_voiceProviderDisabled) { return false; } try { if (!_voiceActivityProvider.TryGetVoiceActivity(spectator.SpectatorClientId, spectator.SpectatorSlotId, out state) || !state.HasData) { LogVoiceDataLost(spectator.SpectatorClientId); return false; } LogVoiceDataDetected(spectator.SpectatorClientId); return true; } catch (Exception ex) { _voiceProviderDisabled = true; LogDebug("Voice activity provider failed and was disabled for this session: " + ex.GetType().Name + "."); state = VoiceActivityState.NoData; return false; } } private bool TryGetSyncedVoiceActivity(RemoteSpectatorInfo spectator, out VoiceActivityState state) { state = VoiceActivityState.NoData; if (!_config.EnableVoiceActivitySync.Value || _networkService == null) { return false; } if (_networkService.TryGetRemoteVoiceActivity(spectator.SpectatorClientId, out state) && state.HasData && state.ClientId == spectator.SpectatorClientId && state.SlotId == spectator.SpectatorSlotId) { LogVoiceDataDetected(spectator.SpectatorClientId); return true; } if (_networkService.TryGetPeerCapability(spectator.SpectatorClientId, out ModPeerCapability capability) && ModPeerCapabilityRules.SupportsCurrentVoiceActivitySync(capability)) { state = new VoiceActivityState(hasData: true, isSpeaking: false, 0f, 0f, spectator.SpectatorClientId, spectator.SpectatorSlotId, DateTime.UtcNow.Ticks); LogVoiceDataDetected(spectator.SpectatorClientId); return true; } return false; } private bool TryGetRemoteSpectatorPose(RemoteSpectatorInfo spectator, FloatingHeadVisualSourceKind sourceKind, Camera? renderingCamera, out Vector3 position, out Quaternion rotation, out Vector3 rawPosition, out bool usedVisibleProxy) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) if (spectator.PoseState == null || !spectator.PoseState.IsSpectating) { position = Vector3.zero; rotation = Quaternion.identity; rawPosition = Vector3.zero; usedVisibleProxy = false; return false; } rawPosition = spectator.PoseState.Position; position = rawPosition; rotation = spectator.PoseState.Rotation; if (sourceKind == FloatingHeadVisualSourceKind.RuntimeDetachedHead) { rotation = FloatingHeadRotationRules.ApplyRuntimeDetachedHeadOffset(rotation, _config.RuntimeDetachedHeadPitchOffset.Value, _config.RuntimeDetachedHeadYawOffset.Value, _config.RuntimeDetachedHeadRollOffset.Value); } Camera camera = renderingCamera; if ((Object)(object)camera == (Object)null) { _placementService.TryGetActiveCamera(out camera); } usedVisibleProxy = _config.KeepRemotePoseInView.Value && (Object)(object)camera != (Object)null && TryGetVisibleProxyPosition(rawPosition, camera, out position); if (FloatingHeadRotationRules.ShouldFaceLocalCamera(sourceKind, _config.FloatingHeadFaceCamera.Value) && (Object)(object)camera != (Object)null) { Vector3 val = ((Component)camera).transform.position - position; if (((Vector3)(ref val)).sqrMagnitude > 0.0001f) { rotation = Quaternion.LookRotation(((Vector3)(ref val)).normalized, Vector3.up); } } return true; } private bool TryGetVisibleProxyPosition(Vector3 remotePosition, Camera camera, out Vector3 proxyPosition) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) proxyPosition = remotePosition; Vector3 val = camera.WorldToViewportPoint(remotePosition); if (val.z > camera.nearClipPlane + 0.05f && val.x >= 0.08f && val.x <= 0.92f && val.y >= 0.12f && val.y <= 0.88f) { return false; } float num = Mathf.Max(camera.nearClipPlane + 0.35f, _config.RemotePoseVisibleProxyDistance.Value); Transform transform = ((Component)camera).transform; Vector2 up = default(Vector2); if (val.z > camera.nearClipPlane + 0.05f) { ((Vector2)(ref up))..ctor(val.x - 0.5f, val.y - 0.5f); } else { Vector3 val2 = transform.InverseTransformPoint(remotePosition); ((Vector2)(ref up))..ctor(val2.x, val2.y); } if (((Vector2)(ref up)).sqrMagnitude < 0.0001f) { up = Vector2.up; } else { ((Vector2)(ref up)).Normalize(); } Vector2 val3 = ProjectDirectionToViewportEdge(up, 0.08f, 0.92f, 0.12f, 0.88f); Vector3 val4 = default(Vector3); ((Vector3)(ref val4))..ctor(val3.x, val3.y, num); proxyPosition = camera.ViewportToWorldPoint(val4); return true; } private static Vector2 ProjectDirectionToViewportEdge(Vector2 direction, float minX, float maxX, float minY, float maxY) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) float num = float.PositiveInfinity; float num2 = float.PositiveInfinity; if (direction.x > 0.0001f) { num = (maxX - 0.5f) / direction.x; } else if (direction.x < -0.0001f) { num = (minX - 0.5f) / direction.x; } if (direction.y > 0.0001f) { num2 = (maxY - 0.5f) / direction.y; } else if (direction.y < -0.0001f) { num2 = (minY - 0.5f) / direction.y; } float num3 = Mathf.Min(num, num2); if (float.IsInfinity(num3) || num3 <= 0f) { num3 = 0f; } return new Vector2(Mathf.Clamp(0.5f + direction.x * num3, minX, maxX), Mathf.Clamp(0.5f + direction.y * num3, minY, maxY)); } private void LogFirstPose(ulong spectatorClientId, Vector3 position, Vector3 anchorPosition, FloatingHeadVisual visual, int visualLayer, Camera? renderingCamera, string poseSource) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if (_config.DebugVisualLifecycle.Value && _poseLoggedSpectators.Add(spectatorClientId)) { string text = TryGetCameraDebugInfo(renderingCamera); string text2 = TryGetPoseVisibilityDebugInfo(renderingCamera, visual, position, visualLayer, usedVisibleProxy: false); LogDebug($"Floating-head placeholder pose applied: spectatorClient={spectatorClientId}, source={poseSource}, position={FormatVector(position)}, anchor={FormatVector(anchorPosition)}, layer={visualLayer}, cameraVisiblePlacement={_config.UseCameraVisiblePlacement.Value}, {text}, {text2}."); } } private void LogFirstRemotePose(ulong spectatorClientId, Vector3 position, Vector3 rawPosition, Vector3 anchorPosition, FloatingHeadVisual visual, int visualLayer, Camera? renderingCamera, string poseSource, bool usedVisibleProxy, Quaternion rotation) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) string item = $"{spectatorClientId}:{poseSource}"; if (_config.DebugVisualLifecycle.Value && _remotePoseLoggedSpectators.Add(item)) { string text = TryGetCameraDebugInfo(renderingCamera); string text2 = TryGetPoseVisibilityDebugInfo(renderingCamera, visual, position, visualLayer, usedVisibleProxy); LogDebug($"Floating-head placeholder remote pose applied: spectatorClient={spectatorClientId}, source={poseSource}, position={FormatVector(position)}, rawPosition={FormatVector(rawPosition)}, rotationEuler={FormatVector(((Quaternion)(ref rotation)).eulerAngles)}, visibleProxy={usedVisibleProxy}, anchor={FormatVector(anchorPosition)}, layer={visualLayer}, {text}, {text2}."); } } private void RemoveVisual(ulong spectatorClientId, string reason) { if (_visuals.TryGetValue(spectatorClientId, out FloatingHeadVisual value)) { _visuals.Remove(spectatorClientId); _poseLoggedSpectators.Remove(spectatorClientId); string logKeyPrefix = $"{spectatorClientId}:"; _remotePoseLoggedSpectators.RemoveWhere((string key) => key.StartsWith(logKeyPrefix, StringComparison.Ordinal)); _screenFallbackLoggedSpectators.Remove(spectatorClientId); _voiceDataLoggedSpectators.Remove(spectatorClientId); _voiceDataActiveSpectators.Remove(spectatorClientId); _visualSkipLoggedSpectators.Remove(spectatorClientId); _nameTagTextCache.Remove(spectatorClientId); value.Dispose(); LogDebug($"Floating-head placeholder visual destroyed: spectatorClient={spectatorClientId}, reason={reason}."); } } private void DestroyAll(string reason) { if (_visuals.Count == 0) { InvalidateCachedVisualState(); return; } foreach (ulong item in new List(_visuals.Keys)) { _visuals[item].Dispose(); LogDebug($"Floating-head placeholder visual destroyed: spectatorClient={item}, reason={reason}."); } _visuals.Clear(); _poseLoggedSpectators.Clear(); _remotePoseLoggedSpectators.Clear(); _screenFallbackLoggedSpectators.Clear(); _voiceDataLoggedSpectators.Clear(); _voiceDataActiveSpectators.Clear(); _visualSkipLoggedSpectators.Clear(); _nameTagTextCache.Clear(); InvalidateCachedVisualState(); } private void InvalidateCachedVisualState() { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) _hasCachedVisualState = false; _lastFullUpdateFrame = -1; _lastAnchorPosition = Vector3.zero; _sortedSpectators.Clear(); } private void DrawScreenFallback(Camera camera, FloatingHeadVisual visual) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_01a9: Unknown result type (might be due to invalid IL or missing references) //IL_01cd: Unknown result type (might be due to invalid IL or missing references) if (visual.State.IsVisible) { float num = Mathf.Clamp(_config.ScreenFallbackSize.Value, 8f, 96f); float num2 = num * 0.5f; Vector3 val = camera.WorldToScreenPoint(visual.State.Position); bool flag = val.z <= camera.nearClipPlane; Vector2 val2 = ResolveScreenFallbackPosition(camera, visual.State.Position, val, num2, flag); float x = val2.x; float y = val2.y; Rect val3 = default(Rect); ((Rect)(ref val3))..ctor(x - num2, y - num2, num, num); Texture2D orCreateScreenMarkerTexture = GetOrCreateScreenMarkerTexture(); Color color = GUI.color; Color baseColor = visual.BaseColor; baseColor.a = Mathf.Clamp01(Mathf.Max(baseColor.a, _config.BaseAlpha.Value)); GUI.color = baseColor; GUI.DrawTexture(val3, (Texture)(object)orCreateScreenMarkerTexture, (ScaleMode)0, true); GUI.color = color; if (_config.DebugVisualLifecycle.Value && _screenFallbackLoggedSpectators.Add(visual.SpectatorClientId)) { Vector3 val4 = camera.WorldToViewportPoint(visual.State.Position); LogDebug($"Floating-head screen fallback active: spectatorClient={visual.SpectatorClientId}, screen=({x:0}, {y:0}), rawScreen=({val.x:0}, {val.y:0}, {val.z:0.00}), viewport=({val4.x:0.00}, {val4.y:0.00}, {val4.z:0.00}), behindCamera={flag}, world={FormatVector(visual.State.Position)}, camera={((Object)camera).name}."); } } } private static Vector2 ResolveScreenFallbackPosition(Camera camera, Vector3 worldPosition, Vector3 screenPosition, float halfSize, bool behindCamera) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_017e: Unknown result type (might be due to invalid IL or missing references) if (!behindCamera) { return new Vector2(Mathf.Clamp(screenPosition.x, halfSize, (float)Screen.width - halfSize), Mathf.Clamp((float)Screen.height - screenPosition.y, halfSize, (float)Screen.height - halfSize)); } Vector3 val = ((Component)camera).transform.InverseTransformPoint(worldPosition); Vector2 up = default(Vector2); ((Vector2)(ref up))..ctor(val.x, val.y); if (((Vector2)(ref up)).sqrMagnitude < 0.0001f) { up = Vector2.up; } else { ((Vector2)(ref up)).Normalize(); } float num = (float)Screen.width * 0.5f; float num2 = (float)Screen.height * 0.5f; float num3 = ((up.x >= 0f) ? ((float)Screen.width - halfSize) : halfSize); float num4 = ((up.y >= 0f) ? halfSize : ((float)Screen.height - halfSize)); float num5 = ((Mathf.Abs(up.x) > 0.0001f) ? ((num3 - num) / up.x) : float.PositiveInfinity); float num6 = ((Mathf.Abs(up.y) > 0.0001f) ? ((num4 - num2) / (0f - up.y)) : float.PositiveInfinity); float num7 = Mathf.Min(Mathf.Abs(num5), Mathf.Abs(num6)); if (float.IsInfinity(num7) || num7 <= 0f) { num7 = (float)Mathf.Min(Screen.width, Screen.height) * 0.25f; } return new Vector2(Mathf.Clamp(num + up.x * num7, halfSize, (float)Screen.width - halfSize), Mathf.Clamp(num2 - up.y * num7, halfSize, (float)Screen.height - halfSize)); } private Texture2D GetOrCreateScreenMarkerTexture() { //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Expected O, but got Unknown //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_screenMarkerTexture != (Object)null) { return _screenMarkerTexture; } Color[] array = (Color[])(object)new Color[1024]; for (int i = 0; i < 32; i++) { for (int j = 0; j < 32; j++) { float num = (float)j - 15.5f; float num2 = (float)i - 15.5f; float num3 = Mathf.Sqrt(num * num + num2 * num2); float num4 = Mathf.Clamp01(15f - num3); array[i * 32 + j] = new Color(1f, 1f, 1f, num4); } } _screenMarkerTexture = new Texture2D(32, 32, (TextureFormat)5, false) { name = "Enhanced Spectator Screen Marker", hideFlags = (HideFlags)61 }; _screenMarkerTexture.SetPixels(array); _screenMarkerTexture.Apply(false, true); return _screenMarkerTexture; } private void LogVoiceDataDetected(ulong spectatorClientId) { _voiceDataActiveSpectators.Add(spectatorClientId); if (_config.DebugVisualLifecycle.Value && _voiceDataLoggedSpectators.Add(spectatorClientId)) { LogDebug($"Voice activity data detected for placeholder spectatorClient={spectatorClientId}."); } } private void LogVoiceDataLost(ulong spectatorClientId) { if (_voiceDataActiveSpectators.Remove(spectatorClientId) && _config.DebugVisualLifecycle.Value) { LogDebug($"Voice activity data lost for placeholder spectatorClient={spectatorClientId}."); } } private void LogDebug(string message) { if (_config.DebugVisualLifecycle.Value) { ModLog.Debug(message); } } private static string GetCreationDebugInfo(FloatingHeadVisual visual) { return $"source={visual.SourceKind}, shader={visual.ShaderName}, materialRenderQueue={visual.MaterialRenderQueue}, rendererEnabled={visual.RendererEnabled}, forceRenderingOff={visual.ForceRenderingOff}, activeSelf={visual.ActiveSelf}, activeInHierarchy={visual.ActiveInHierarchy}, rootActive={visual.RootActiveInHierarchy}, layer={visual.Layer}, hasMeshRenderer={visual.HasMeshRenderer}, hasMeshFilter={visual.HasMeshFilter}, mesh={visual.MeshName}, colliderRemoved={visual.ColliderRemoved}, hasNameTag={visual.HasNameTag}"; } private IReadOnlyList SortSpectators(IReadOnlyList spectators) { _sortedSpectators.Clear(); for (int i = 0; i < spectators.Count; i++) { _sortedSpectators.Add(spectators[i]); } _sortedSpectators.Sort((RemoteSpectatorInfo left, RemoteSpectatorInfo right) => left.SpectatorClientId.CompareTo(right.SpectatorClientId)); return _sortedSpectators; } private void UpdateNameTagText(RemoteSpectatorInfo spectator, FloatingHeadVisual visual) { if (!_config.ShowNameTags.Value) { return; } RefreshNameTagTextCache(); string text; if (_nameTagTextCache.TryGetValue(spectator.SpectatorClientId, out var value) && value.SpectatorSlotId == spectator.SpectatorSlotId) { text = value.Text; } else { text = FormatNameTagText(spectator, out var canCache); if (canCache) { _nameTagTextCache[spectator.SpectatorClientId] = new NameTagTextCacheEntry(spectator.SpectatorSlotId, text); } } if (visual.TrySetNameTagText(text) && _config.DebugNameTagLifecycle.Value) { ModLog.Debug($"Floating-head name tag updated: spectatorClient={spectator.SpectatorClientId}, text={text.Replace('\n', ' ')}."); } } private void RefreshNameTagTextCache() { int num = ((_networkService != null) ? _networkService.RemotePeerIdentityRevision : (-1)); bool value = _config.NameTagUseGamePlayerNames.Value; bool value2 = _config.NameTagUseFallbackIds.Value; if (NameTagTextCacheRules.ShouldClear(_cachedNameTagIdentityRevision, num, _cachedNameTagUseGamePlayerNames, value, _cachedNameTagUseFallbackIds, value2)) { _nameTagTextCache.Clear(); _cachedNameTagIdentityRevision = num; _cachedNameTagUseGamePlayerNames = value; _cachedNameTagUseFallbackIds = value2; } } private string FormatNameTagText(RemoteSpectatorInfo spectator, out bool canCache) { if (_config.NameTagUseGamePlayerNames.Value && TryGetSyncedDisplayName(spectator.SpectatorClientId, out string displayName)) { canCache = true; return displayName; } if (_config.NameTagUseGamePlayerNames.Value && _placementService.TryGetPlayerDisplayName(spectator.SpectatorClientId, spectator.SpectatorSlotId, out string displayName2) && !string.IsNullOrWhiteSpace(displayName2)) { canCache = false; return displayName2.Trim(); } if (_config.NameTagUseFallbackIds.Value) { canCache = true; return $"Client {spectator.SpectatorClientId}\nSlot {spectator.SpectatorSlotId}"; } canCache = true; return "Spectator"; } private bool TryGetSyncedDisplayName(ulong spectatorClientId, out string displayName) { if (_networkService != null && _networkService.TryGetRemotePeerIdentity(spectatorClientId, out PeerIdentityState state) && PlayerDisplayNameRules.TryNormalize(state.DisplayName, out displayName)) { return true; } displayName = string.Empty; return false; } private int ResolveVisibleLayer(Camera? renderingCamera) { Camera camera = renderingCamera; if ((Object)(object)camera == (Object)null && (!_placementService.TryGetActiveCamera(out camera) || (Object)(object)camera == (Object)null)) { return 0; } if (IsLayerVisible(camera, 0)) { return 0; } if (!_layerWarningLogged) { LogDebug($"Floating-head default layer is not visible to camera={((Object)camera).name}; cullingMask={camera.cullingMask}. Searching for a visible layer."); _layerWarningLogged = true; } for (int i = 0; i < 32; i++) { if (IsLayerVisible(camera, i)) { return i; } } LogDebug("Floating-head could not find any visible layer for camera=" + ((Object)camera).name + "; falling back to Default layer."); return 0; } private string TryGetCameraDebugInfo(Camera? renderingCamera) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) Camera camera = renderingCamera; if ((Object)(object)camera == (Object)null && (!_placementService.TryGetActiveCamera(out camera) || (Object)(object)camera == (Object)null)) { return "camera=none"; } return $"camera={((Object)camera).name}, cameraType={camera.cameraType}, cameraEnabled={((Behaviour)camera).enabled}, cameraActive={((Component)camera).gameObject.activeInHierarchy}, cameraPosition={FormatVector(((Component)camera).transform.position)}, cameraForward={FormatVector(((Component)camera).transform.forward)}, nearClip={camera.nearClipPlane:0.00}, cullingMask={camera.cullingMask}"; } private string TryGetPoseVisibilityDebugInfo(Camera? renderingCamera, FloatingHeadVisual visual, Vector3 position, int selectedLayer, bool usedVisibleProxy) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) Camera camera = renderingCamera; if ((Object)(object)camera == (Object)null && (!_placementService.TryGetActiveCamera(out camera) || (Object)(object)camera == (Object)null)) { return $"screenFallbackEnabled={_config.EnableScreenFallbackVisual.Value}, visibleProxy={usedVisibleProxy}, poseVisibilityCamera=none"; } Vector3 val = camera.WorldToScreenPoint(position); Vector3 val2 = camera.WorldToViewportPoint(position); float num = Vector3.Distance(((Component)camera).transform.position, position); bool flag = selectedLayer >= 0 && selectedLayer < 32 && IsLayerVisible(camera, selectedLayer); bool flag2 = IsLayerVisible(camera, 0); return $"screen=({val.x:0}, {val.y:0}, {val.z:0.00}), viewport=({val2.x:0.00}, {val2.y:0.00}, {val2.z:0.00}), distance={num:0.00}, z={val.z:0.00}, nearClip={camera.nearClipPlane:0.00}, selectedLayerVisible={flag}, defaultLayerVisible={flag2}, screenFallbackEnabled={_config.EnableScreenFallbackVisual.Value}, visibleProxy={usedVisibleProxy}, visualActiveSelf={visual.ActiveSelf}, visualActiveInHierarchy={visual.ActiveInHierarchy}, rootActive={visual.RootActiveInHierarchy}, materialRenderQueue={visual.MaterialRenderQueue}, rendererEnabled={visual.RendererEnabled}, forceRenderingOff={visual.ForceRenderingOff}"; } private static bool IsLayerVisible(Camera camera, int layer) { return (camera.cullingMask & (1 << layer)) != 0; } private static bool IsRenderableGameCamera(Camera? camera) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Invalid comparison between Unknown and I4 if ((Object)(object)camera != (Object)null && ((Behaviour)camera).enabled && ((Component)camera).gameObject.activeInHierarchy) { return (int)camera.cameraType == 1; } return false; } private static string FormatVector(Vector3 vector) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({vector.x:0.00}, {vector.y:0.00}, {vector.z:0.00})"; } } public enum FloatingHeadVisualSourceKind { Placeholder, RuntimeDetachedHead } public readonly struct FloatingHeadVisualState { public ulong SpectatorClientId { get; } public ulong SpectatorSlotId { get; } public bool IsVisible { get; } public Vector3 Position { get; } public FloatingHeadVisualState(ulong spectatorClientId, ulong spectatorSlotId, bool isVisible, Vector3 position) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) SpectatorClientId = spectatorClientId; SpectatorSlotId = spectatorSlotId; IsVisible = isVisible; Position = position; } } public enum FloatingHeadVisualStyle { Sphere, Billboard, Ring } public static class FloatingHeadVoiceActivityRules { public static bool ShouldUseLocalFallback(bool voiceActivitySyncEnabled, bool hasNetworkService) { if (voiceActivitySyncEnabled) { return !hasNetworkService; } return true; } } public static class FloatingHeadVoiceScaleRules { public const float DefaultMinimumSpeakingVoiceLevel = 0.35f; public const float DefaultSpeakingPulseAmount = 0.32f; public const float DefaultVoiceAttackSmoothTime = 0.005f; public const float DefaultVoiceReleaseSmoothTime = 0.008f; public static float ResolveTargetVoiceLevel(bool hasVoiceData, bool isSpeaking, float amplitude, float minimumSpeakingVoiceLevel) { if (!hasVoiceData) { return 0f; } if (!isSpeaking) { return 0f; } float num = Clamp01(amplitude); if (num > 0f) { return num; } return Clamp01(minimumSpeakingVoiceLevel); } public static float ResolveScaleMultiplier(float silenceScaleMultiplier, float speakingScaleMultiplier, float voiceLevel, bool pulseWhenSpeaking, float speakingPulseAmount, float pulse01) { float num = Clamp01(voiceLevel); float num2 = Lerp(Math.Max(0.01f, silenceScaleMultiplier), Math.Max(0.01f, speakingScaleMultiplier), num); if (pulseWhenSpeaking && num > 0.01f) { num2 *= 1f + num * Math.Max(0f, speakingPulseAmount) * Clamp01(pulse01); } return Math.Max(0.01f, num2); } public static float ResolveVoiceSmoothTime(float currentLevel, float targetLevel, float attackSmoothTime, float releaseSmoothTime) { if (!(targetLevel > currentLevel)) { return Math.Max(0f, releaseSmoothTime); } return Math.Max(0f, attackSmoothTime); } private static float Clamp01(float value) { if (value <= 0f) { return 0f; } if (!(value >= 1f)) { return value; } return 1f; } private static float Lerp(float from, float to, float t) { return from + (to - from) * Clamp01(t); } } public static class NameTagTextCacheRules { public static bool ShouldClear(int lastIdentityRevision, int currentIdentityRevision, bool lastUseGamePlayerNames, bool currentUseGamePlayerNames, bool lastUseFallbackIds, bool currentUseFallbackIds) { if (lastIdentityRevision == currentIdentityRevision && lastUseGamePlayerNames == currentUseGamePlayerNames) { return lastUseFallbackIds != currentUseFallbackIds; } return true; } } public sealed class NameTagVisual : IDisposable { private readonly GameObject _gameObject; private readonly TextMesh _textMesh; private readonly MeshRenderer _renderer; private readonly float _heightOffset; private readonly float _maxDistance; private bool _disposed; public bool ActiveInHierarchy { get { if ((Object)(object)_gameObject != (Object)null) { return _gameObject.activeInHierarchy; } return false; } } public string Text { get { if (!((Object)(object)_textMesh != (Object)null)) { return string.Empty; } return _textMesh.text; } } public NameTagVisual(GameObject gameObject, TextMesh textMesh, MeshRenderer renderer, float heightOffset, float maxDistance) { _gameObject = gameObject ?? throw new ArgumentNullException("gameObject"); _textMesh = textMesh ?? throw new ArgumentNullException("textMesh"); _renderer = renderer ?? throw new ArgumentNullException("renderer"); _heightOffset = Mathf.Max(0f, heightOffset); _maxDistance = Mathf.Max(0f, maxDistance); } public bool SetText(string text) { if (_disposed || (Object)(object)_textMesh == (Object)null || _textMesh.text == text) { return false; } _textMesh.text = text; return true; } public void ApplyPose(Vector3 markerPosition, Quaternion fallbackRotation, Camera? camera) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Unknown result type (might be due to invalid IL or missing references) if (!_disposed && !((Object)(object)_gameObject == (Object)null)) { Vector3 val = markerPosition + Vector3.up * _heightOffset; bool visible = true; Quaternion val2 = fallbackRotation; if ((Object)(object)camera != (Object)null) { Vector3 val3 = ((Component)camera).transform.position - val; float magnitude = ((Vector3)(ref val3)).magnitude; visible = _maxDistance <= 0f || magnitude <= _maxDistance; val2 = Quaternion.LookRotation(((Component)camera).transform.rotation * Vector3.forward, ((Component)camera).transform.rotation * Vector3.up); } _gameObject.transform.SetPositionAndRotation(val, val2); SetVisible(visible); } } public void SetLayer(int layer) { if (!_disposed && !((Object)(object)_gameObject == (Object)null) && layer >= 0 && layer <= 31 && _gameObject.layer != layer) { _gameObject.layer = layer; } } public void SetVisible(bool visible) { if (!_disposed && !((Object)(object)_gameObject == (Object)null) && _gameObject.activeSelf != visible) { _gameObject.SetActive(visible); } } public void Dispose() { if (!_disposed) { _disposed = true; if ((Object)(object)_gameObject != (Object)null) { Object.Destroy((Object)(object)_gameObject); } } } public static NameTagVisual Create(Transform root, string text, Color color, float scale, float heightOffset, float maxDistance) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown GameObject val = new GameObject("Enhanced Spectator Name Tag"); val.SetActive(false); val.transform.SetParent(root, false); TextMesh val2 = val.AddComponent(); val2.text = text; val2.anchor = (TextAnchor)4; val2.alignment = (TextAlignment)1; val2.fontSize = 64; val2.characterSize = Mathf.Max(0.005f, scale); val2.richText = false; val2.color = color; MeshRenderer component = val.GetComponent(); ((Renderer)component).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)component).receiveShadows = false; ((Renderer)component).allowOcclusionWhenDynamic = false; ((Renderer)component).sortingOrder = 32767; return new NameTagVisual(val, val2, component, heightOffset, maxDistance); } } public sealed class PlaceholderHeadVisualFactory : IDisposable { private const string RootName = "Enhanced Spectator Visuals"; private GameObject? _root; public FloatingHeadVisual Create(RemoteSpectatorInfo spectator, float sphereScale, FloatingHeadVisualStyle style, float billboardSize, float baseAlpha, bool useUnlitMaterial, bool enableDepthTest, bool showNameTag, float nameTagScale, float nameTagHeightOffset, float nameTagMaxDistance, string nameTagText) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) GameObject orCreateRoot = GetOrCreateRoot(); Mesh runtimeMesh; GameObject val = CreateVisualObject(style, out runtimeMesh); ((Object)val).name = $"Enhanced Spectator Placeholder {spectator.SpectatorClientId}"; val.SetActive(false); val.transform.SetParent(orCreateRoot.transform, false); float num = ((style == FloatingHeadVisualStyle.Sphere) ? sphereScale : billboardSize); val.transform.localScale = Vector3.one * Mathf.Max(0.01f, num); Collider component = val.GetComponent(); bool colliderRemoved = (Object)(object)component != (Object)null; if ((Object)(object)component != (Object)null) { Object.DestroyImmediate((Object)(object)component); } MeshRenderer component2 = val.GetComponent(); MeshFilter component3 = val.GetComponent(); Color val2 = ColorForSpectator(spectator.SpectatorClientId, Mathf.Clamp01(baseAlpha)); Material val3 = CreateRuntimeMaterial(spectator.SpectatorClientId, ((Object)(object)component2 != (Object)null) ? ((Renderer)component2).sharedMaterial : null, val2, useUnlitMaterial, enableDepthTest); if ((Object)(object)component2 != (Object)null) { ((Renderer)component2).enabled = true; ((Renderer)component2).forceRenderingOff = false; ((Renderer)component2).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)component2).sharedMaterial = val3; ((Renderer)component2).receiveShadows = false; ((Renderer)component2).allowOcclusionWhenDynamic = false; ((Renderer)component2).sortingOrder = 32767; } NameTagVisual nameTag = (showNameTag ? NameTagVisual.Create(orCreateRoot.transform, nameTagText, Color.white, nameTagScale, nameTagHeightOffset, nameTagMaxDistance) : null); return new FloatingHeadVisual(spectator.SpectatorClientId, spectator.SpectatorSlotId, FloatingHeadVisualSourceKind.Placeholder, val, val3, runtimeMesh, val2, Mathf.Clamp01(baseAlpha), component2, component3, colliderRemoved, nameTag); } public FloatingHeadVisual CreateFromDetachedHead(RemoteSpectatorInfo spectator, Transform source, float visualScale, bool showNameTag, float nameTagScale, float nameTagHeightOffset, float nameTagMaxDistance, string nameTagText) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)source == (Object)null) { throw new ArgumentNullException("source"); } if (!TryGetDetachedHeadRendererSource(source, out MeshFilter meshFilter, out MeshRenderer meshRenderer)) { throw new InvalidOperationException("Detached-head source does not contain a supported MeshFilter and MeshRenderer."); } GameObject orCreateRoot = GetOrCreateRoot(); GameObject val = new GameObject($"Enhanced Spectator Detached Head {spectator.SpectatorClientId}"); MeshFilter val2 = val.AddComponent(); MeshRenderer val3 = val.AddComponent(); val2.sharedMesh = meshFilter.sharedMesh; ((Renderer)val3).sharedMaterials = ((Renderer)meshRenderer).sharedMaterials; ((Object)val).name = $"Enhanced Spectator Detached Head {spectator.SpectatorClientId}"; val.SetActive(false); val.transform.SetParent(orCreateRoot.transform, false); val.transform.localScale = Vector3.one * Mathf.Max(0.01f, visualScale); ((Renderer)val3).enabled = true; ((Renderer)val3).forceRenderingOff = false; ((Renderer)val3).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)val3).receiveShadows = false; ((Renderer)val3).allowOcclusionWhenDynamic = false; NameTagVisual nameTag = (showNameTag ? NameTagVisual.Create(orCreateRoot.transform, nameTagText, Color.white, nameTagScale, nameTagHeightOffset, nameTagMaxDistance) : null); return new FloatingHeadVisual(spectator.SpectatorClientId, spectator.SpectatorSlotId, FloatingHeadVisualSourceKind.RuntimeDetachedHead, val, null, null, Color.white, 1f, val3, val2, colliderRemoved: false, nameTag); } public void Dispose() { if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); _root = null; } } private GameObject GetOrCreateRoot() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown if ((Object)(object)_root != (Object)null) { return _root; } _root = new GameObject("Enhanced Spectator Visuals"); Object.DontDestroyOnLoad((Object)(object)_root); return _root; } private static GameObject CreateVisualObject(FloatingHeadVisualStyle style, out Mesh? runtimeMesh) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown runtimeMesh = null; if (style == FloatingHeadVisualStyle.Sphere) { return GameObject.CreatePrimitive((PrimitiveType)0); } GameObject val = new GameObject($"Enhanced Spectator {style}"); MeshFilter val2 = val.AddComponent(); val.AddComponent(); runtimeMesh = ((style == FloatingHeadVisualStyle.Ring) ? CreateRingMesh() : CreateBillboardMesh()); val2.sharedMesh = runtimeMesh; return val; } private static Material CreateRuntimeMaterial(ulong spectatorClientId, Material? sourceMaterial, Color color, bool useUnlitMaterial, bool enableDepthTest) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown //IL_004f: Unknown result type (might be due to invalid IL or missing references) Shader val = FindPlaceholderShader(useUnlitMaterial, enableDepthTest); Material val2; if ((Object)(object)val != (Object)null) { val2 = new Material(val); } else { if (!((Object)(object)sourceMaterial != (Object)null)) { throw new InvalidOperationException("No placeholder shader or source material is available."); } val2 = new Material(sourceMaterial); } ((Object)val2).name = $"Enhanced Spectator Placeholder Material {spectatorClientId}"; ApplyMaterialColor(val2, color); ConfigureMaterial(val2, enableDepthTest); return val2; } private static Shader? FindPlaceholderShader(bool useUnlitMaterial, bool enableDepthTest) { string[] array = ((!useUnlitMaterial) ? new string[7] { "HDRP/Lit", "Universal Render Pipeline/Lit", "Standard", "HDRP/Unlit", "Universal Render Pipeline/Unlit", "Unlit/Color", "Sprites/Default" } : new string[7] { "HDRP/Unlit", "HDRP/Lit", "Universal Render Pipeline/Unlit", "Universal Render Pipeline/Lit", "Unlit/Color", "Sprites/Default", "Standard" }); for (int i = 0; i < array.Length; i++) { Shader val = Shader.Find(array[i]); if ((Object)(object)val != (Object)null) { return val; } } return null; } private static void ApplyMaterialColor(Material material, Color color) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) SetMaterialColor(material, "_BaseColor", color); SetMaterialColor(material, "_UnlitColor", color); SetMaterialColor(material, "_Color", color); SetMaterialColor(material, "_EmissionColor", color); SetMaterialColor(material, "_EmissiveColor", color); SetMaterialColor(material, "_TintColor", color); } private static void SetMaterialColor(Material material, string propertyName, Color color) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) if (material.HasProperty(propertyName)) { material.SetColor(propertyName, color); if (propertyName.Contains("Emission") || propertyName.Contains("Emissive")) { material.EnableKeyword("_EMISSION"); } } } private static void ConfigureMaterial(Material material, bool enableDepthTest) { material.renderQueue = (enableDepthTest ? 3000 : 5000); if (material.HasProperty("_SrcBlend")) { material.SetFloat("_SrcBlend", 5f); } if (material.HasProperty("_DstBlend")) { material.SetFloat("_DstBlend", 10f); } if (material.HasProperty("_ZTest")) { material.SetFloat("_ZTest", enableDepthTest ? 4f : 8f); } if (material.HasProperty("_ZWrite")) { material.SetFloat("_ZWrite", 0f); } if (material.HasProperty("_SurfaceType")) { material.SetFloat("_SurfaceType", 1f); } if (material.HasProperty("_BlendMode")) { material.SetFloat("_BlendMode", 0f); } if (material.HasProperty("_Cull")) { material.SetFloat("_Cull", 0f); } if (material.HasProperty("_CullMode")) { material.SetFloat("_CullMode", 0f); } if (material.HasProperty("_TransparentCullMode")) { material.SetFloat("_TransparentCullMode", 0f); } if (material.HasProperty("_DoubleSidedEnable")) { material.SetFloat("_DoubleSidedEnable", 1f); } material.EnableKeyword("_DOUBLESIDED_ON"); material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); material.EnableKeyword("_ALPHABLEND_ON"); } private static Mesh CreateBillboardMesh() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) Mesh val = new Mesh(); ((Object)val).name = "Enhanced Spectator Runtime Billboard"; val.vertices = (Vector3[])(object)new Vector3[4] { new Vector3(-0.5f, -0.5f, 0f), new Vector3(0.5f, -0.5f, 0f), new Vector3(-0.5f, 0.5f, 0f), new Vector3(0.5f, 0.5f, 0f) }; val.uv = (Vector2[])(object)new Vector2[4] { new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(0f, 1f), new Vector2(1f, 1f) }; val.triangles = new int[6] { 0, 1, 2, 2, 1, 3 }; val.RecalculateBounds(); val.RecalculateNormals(); return val; } private static Mesh CreateRingMesh() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Expected O, but got Unknown float num = 0.29f; Vector3[] array = (Vector3[])(object)new Vector3[64]; int[] array2 = new int[192]; for (int i = 0; i < 32; i++) { float num2 = MathF.PI * 2f * (float)i / 32f; float num3 = Mathf.Sin(num2); float num4 = Mathf.Cos(num2); array[i * 2] = new Vector3(num4 * 0.5f, num3 * 0.5f, 0f); array[i * 2 + 1] = new Vector3(num4 * num, num3 * num, 0f); int num5 = (i + 1) % 32; int num6 = i * 6; int num7 = i * 2; int num8 = num7 + 1; int num9 = num5 * 2; int num10 = num9 + 1; array2[num6] = num7; array2[num6 + 1] = num9; array2[num6 + 2] = num8; array2[num6 + 3] = num8; array2[num6 + 4] = num9; array2[num6 + 5] = num10; } Mesh val = new Mesh { name = "Enhanced Spectator Runtime Ring", vertices = array, triangles = array2 }; val.RecalculateBounds(); val.RecalculateNormals(); return val; } private static Color ColorForSpectator(ulong spectatorClientId, float alpha) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) Color result = Color.HSVToRGB(((float)spectatorClientId * 0.173f + 0.52f) % 1f, 0.65f, 1f); result.a = alpha; return result; } private static bool TryGetDetachedHeadRendererSource(Transform source, out MeshFilter? meshFilter, out MeshRenderer? meshRenderer) { meshRenderer = ((Component)source).GetComponentInChildren(true); if ((Object)(object)meshRenderer == (Object)null) { meshFilter = null; return false; } meshFilter = ((Component)meshRenderer).GetComponent(); if ((Object)(object)meshFilter == (Object)null || (Object)(object)meshFilter.sharedMesh == (Object)null) { meshFilter = null; meshRenderer = null; return false; } return true; } } } namespace EnhancedSpectator.Config { public sealed class EnhancedSpectatorConfig { public ConfigEntry EnableSpectatorModule { get; } public ConfigEntry EnableEnhancedSpectator { get; } public ConfigEntry EnableFreecam { get; } public ConfigEntry FreecamDefaultOn { get; } public ConfigEntry FreecamRadius { get; } public ConfigEntry FreecamMoveSpeed { get; } public ConfigEntry FreecamFastMoveMultiplier { get; } public ConfigEntry FreecamSlowMoveMultiplier { get; } public ConfigEntry FreecamLookSensitivity { get; } public ConfigEntry FreecamSmoothTime { get; } public ConfigEntry ClampCameraToRadius { get; } public ConfigEntry RecenterOnTargetSwitch { get; } public ConfigEntry DisableDuringGameOverOverride { get; } public ConfigEntry ToggleFreecamKey { get; } public ConfigEntry RecenterKey { get; } public ConfigEntry ResetToVanillaViewKey { get; } public ConfigEntry FastMoveKey { get; } public ConfigEntry SlowMoveKey { get; } public ConfigEntry AscendKey { get; } public ConfigEntry DescendKey { get; } public ConfigEntry EnableDebugLogging { get; } public ConfigEntry EnableNetworking { get; } public ConfigEntry EnableCapabilityHandshake { get; } public ConfigEntry EnableSpectatorTargetSync { get; } public ConfigEntry EnableSpectatorPoseSync { get; } public ConfigEntry EnableHostRelay { get; } public ConfigEntry SpectatorPoseSyncInterval { get; } public ConfigEntry EnableVoiceActivitySync { get; } public ConfigEntry VoiceActivitySyncInterval { get; } public ConfigEntry VoiceActivityStaleSeconds { get; } public ConfigEntry DebugVoiceActivitySync { get; } public ConfigEntry EnableSpectatorVoiceToTarget { get; } public ConfigEntry SpectatorVoiceAudienceMode { get; } public ConfigEntry SpectatorVoiceToTargetVolume { get; } public ConfigEntry SpectatorVoiceUseRemotePosePosition { get; } public ConfigEntry SpectatorVoiceEnableDistanceAttenuation { get; } public ConfigEntry SpectatorVoiceMinDistance { get; } public ConfigEntry SpectatorVoiceMaxDistance { get; } public ConfigEntry SpectatorVoiceRolloffPower { get; } public ConfigEntry SpectatorVoiceMinimumVolume { get; } public ConfigEntry SpectatorVoiceFallbackTo2DWhenPoseMissing { get; } public ConfigEntry DebugSpectatorVoiceRouting { get; } public ConfigEntry RepairVanillaConnectedPlayerState { get; } public ConfigEntry RepairVanillaPlayerNames { get; } public ConfigEntry DebugPlayerStateRepair { get; } public ConfigEntry DebugNetworkMessages { get; } public ConfigEntry DebugPoseMessages { get; } public ConfigEntry EnableSpectatorPresenceDebug { get; } public ConfigEntry DebugLogPresenceChanges { get; } public ConfigEntry EnableModelInspection { get; } public ConfigEntry LogLocalPlayerModelOnKey { get; } public ConfigEntry LogRemotePlayerModelsOnKey { get; } public ConfigEntry ModelInspectionKey { get; } public ConfigEntry IncludeRendererBounds { get; } public ConfigEntry IncludeMaterials { get; } public ConfigEntry MaxTransformDepth { get; } public ConfigEntry EnableRuntimeHeadSourceInspection { get; } public ConfigEntry RuntimeHeadSourceInspectionKey { get; } public ConfigEntry RuntimeHeadSourceIncludeRendererBounds { get; } public ConfigEntry RuntimeHeadSourceIncludeMaterials { get; } public ConfigEntry RuntimeHeadSourceMaxTransformDepth { get; } public ConfigEntry EnableVoiceDiagnostics { get; } public ConfigEntry VoiceDiagnosticsKey { get; } public ConfigEntry LogLocalVoiceStateOnKey { get; } public ConfigEntry LogRemoteVoiceStatesOnKey { get; } public ConfigEntry IncludeVoiceAudioSourceDetails { get; } public ConfigEntry IncludeWalkieVoiceDiagnostics { get; } public ConfigEntry EnableFloatingHeadVisuals { get; } public ConfigEntry EnablePlaceholderVisuals { get; } public ConfigEntry UseRuntimeDetachedHeadVisuals { get; } public ConfigEntry RuntimeDetachedHeadScale { get; } public ConfigEntry RuntimeDetachedHeadPitchOffset { get; } public ConfigEntry RuntimeDetachedHeadYawOffset { get; } public ConfigEntry RuntimeDetachedHeadRollOffset { get; } public ConfigEntry FallbackToPlaceholderWhenDetachedHeadUnavailable { get; } public ConfigEntry ShowRemoteSpectators { get; } public ConfigEntry ShowOnlySpectatorsWatchingMe { get; } public ConfigEntry ShowDeadSpectatorsToAlivePlayers { get; } public ConfigEntry ShowDeadSpectatorsToDeadPlayers { get; } public ConfigEntry MaxFloatingHeadsVisible { get; } public ConfigEntry VisualStyle { get; } public ConfigEntry PlaceholderScale { get; } public ConfigEntry BillboardSize { get; } public ConfigEntry BaseAlpha { get; } public ConfigEntry UseUnlitMaterial { get; } public ConfigEntry EnableDepthTest { get; } public ConfigEntry FloatingHeadRingRadius { get; } public ConfigEntry FloatingHeadHeightOffset { get; } public ConfigEntry UseCameraVisiblePlacement { get; } public ConfigEntry CameraForwardOffset { get; } public ConfigEntry RemotePoseSmoothTime { get; } public ConfigEntry KeepRemotePoseInView { get; } public ConfigEntry RemotePoseVisibleProxyDistance { get; } public ConfigEntry EnableScreenFallbackVisual { get; } public ConfigEntry ScreenFallbackSize { get; } public ConfigEntry PresenceLostGraceSeconds { get; } public ConfigEntry FloatingHeadFaceCamera { get; } public ConfigEntry PulseWhenSpeaking { get; } public ConfigEntry SpeakingScaleMultiplier { get; } public ConfigEntry SpeakingPulseSpeed { get; } public ConfigEntry MinimumSpeakingVoiceLevel { get; } public ConfigEntry SpeakingPulseAmount { get; } public ConfigEntry VoiceAttackSmoothTime { get; } public ConfigEntry VoiceReleaseSmoothTime { get; } public ConfigEntry SilenceScaleMultiplier { get; } public ConfigEntry AmplitudeSmoothing { get; } public ConfigEntry DestroyOnPresenceLost { get; } public ConfigEntry DebugVisualLifecycle { get; } public ConfigEntry ShowNameTags { get; } public ConfigEntry NameTagScale { get; } public ConfigEntry NameTagHeightOffset { get; } public ConfigEntry NameTagMaxDistance { get; } public ConfigEntry NameTagUseGamePlayerNames { get; } public ConfigEntry NameTagUseFallbackIds { get; } public ConfigEntry DebugNameTagLifecycle { get; } private EnhancedSpectatorConfig(ConfigEntry enableSpectatorModule, ConfigEntry enableEnhancedSpectator, ConfigEntry enableFreecam, ConfigEntry freecamDefaultOn, ConfigEntry freecamRadius, ConfigEntry freecamMoveSpeed, ConfigEntry freecamFastMoveMultiplier, ConfigEntry freecamSlowMoveMultiplier, ConfigEntry freecamLookSensitivity, ConfigEntry freecamSmoothTime, ConfigEntry clampCameraToRadius, ConfigEntry recenterOnTargetSwitch, ConfigEntry disableDuringGameOverOverride, ConfigEntry toggleFreecamKey, ConfigEntry recenterKey, ConfigEntry resetToVanillaViewKey, ConfigEntry fastMoveKey, ConfigEntry slowMoveKey, ConfigEntry ascendKey, ConfigEntry descendKey, ConfigEntry enableDebugLogging, ConfigEntry enableNetworking, ConfigEntry enableCapabilityHandshake, ConfigEntry enableSpectatorTargetSync, ConfigEntry enableSpectatorPoseSync, ConfigEntry enableHostRelay, ConfigEntry spectatorPoseSyncInterval, ConfigEntry enableVoiceActivitySync, ConfigEntry voiceActivitySyncInterval, ConfigEntry voiceActivityStaleSeconds, ConfigEntry debugVoiceActivitySync, ConfigEntry enableSpectatorVoiceToTarget, ConfigEntry spectatorVoiceAudienceMode, ConfigEntry spectatorVoiceToTargetVolume, ConfigEntry spectatorVoiceUseRemotePosePosition, ConfigEntry spectatorVoiceEnableDistanceAttenuation, ConfigEntry spectatorVoiceMinDistance, ConfigEntry spectatorVoiceMaxDistance, ConfigEntry spectatorVoiceRolloffPower, ConfigEntry spectatorVoiceMinimumVolume, ConfigEntry spectatorVoiceFallbackTo2DWhenPoseMissing, ConfigEntry debugSpectatorVoiceRouting, ConfigEntry repairVanillaConnectedPlayerState, ConfigEntry repairVanillaPlayerNames, ConfigEntry debugPlayerStateRepair, ConfigEntry debugNetworkMessages, ConfigEntry debugPoseMessages, ConfigEntry enableSpectatorPresenceDebug, ConfigEntry debugLogPresenceChanges, ConfigEntry enableModelInspection, ConfigEntry logLocalPlayerModelOnKey, ConfigEntry logRemotePlayerModelsOnKey, ConfigEntry modelInspectionKey, ConfigEntry includeRendererBounds, ConfigEntry includeMaterials, ConfigEntry maxTransformDepth, ConfigEntry enableRuntimeHeadSourceInspection, ConfigEntry runtimeHeadSourceInspectionKey, ConfigEntry runtimeHeadSourceIncludeRendererBounds, ConfigEntry runtimeHeadSourceIncludeMaterials, ConfigEntry runtimeHeadSourceMaxTransformDepth, ConfigEntry enableVoiceDiagnostics, ConfigEntry voiceDiagnosticsKey, ConfigEntry logLocalVoiceStateOnKey, ConfigEntry logRemoteVoiceStatesOnKey, ConfigEntry includeVoiceAudioSourceDetails, ConfigEntry includeWalkieVoiceDiagnostics, ConfigEntry enableFloatingHeadVisuals, ConfigEntry enablePlaceholderVisuals, ConfigEntry useRuntimeDetachedHeadVisuals, ConfigEntry runtimeDetachedHeadScale, ConfigEntry runtimeDetachedHeadPitchOffset, ConfigEntry runtimeDetachedHeadYawOffset, ConfigEntry runtimeDetachedHeadRollOffset, ConfigEntry fallbackToPlaceholderWhenDetachedHeadUnavailable, ConfigEntry showRemoteSpectators, ConfigEntry showOnlySpectatorsWatchingMe, ConfigEntry showDeadSpectatorsToAlivePlayers, ConfigEntry showDeadSpectatorsToDeadPlayers, ConfigEntry maxFloatingHeadsVisible, ConfigEntry visualStyle, ConfigEntry placeholderScale, ConfigEntry billboardSize, ConfigEntry baseAlpha, ConfigEntry useUnlitMaterial, ConfigEntry enableDepthTest, ConfigEntry floatingHeadRingRadius, ConfigEntry floatingHeadHeightOffset, ConfigEntry useCameraVisiblePlacement, ConfigEntry cameraForwardOffset, ConfigEntry remotePoseSmoothTime, ConfigEntry keepRemotePoseInView, ConfigEntry remotePoseVisibleProxyDistance, ConfigEntry enableScreenFallbackVisual, ConfigEntry screenFallbackSize, ConfigEntry presenceLostGraceSeconds, ConfigEntry floatingHeadFaceCamera, ConfigEntry pulseWhenSpeaking, ConfigEntry speakingScaleMultiplier, ConfigEntry speakingPulseSpeed, ConfigEntry minimumSpeakingVoiceLevel, ConfigEntry speakingPulseAmount, ConfigEntry voiceAttackSmoothTime, ConfigEntry voiceReleaseSmoothTime, ConfigEntry silenceScaleMultiplier, ConfigEntry amplitudeSmoothing, ConfigEntry destroyOnPresenceLost, ConfigEntry debugVisualLifecycle, ConfigEntry showNameTags, ConfigEntry nameTagScale, ConfigEntry nameTagHeightOffset, ConfigEntry nameTagMaxDistance, ConfigEntry nameTagUseGamePlayerNames, ConfigEntry nameTagUseFallbackIds, ConfigEntry debugNameTagLifecycle) { EnableSpectatorModule = enableSpectatorModule; EnableEnhancedSpectator = enableEnhancedSpectator; EnableFreecam = enableFreecam; FreecamDefaultOn = freecamDefaultOn; FreecamRadius = freecamRadius; FreecamMoveSpeed = freecamMoveSpeed; FreecamFastMoveMultiplier = freecamFastMoveMultiplier; FreecamSlowMoveMultiplier = freecamSlowMoveMultiplier; FreecamLookSensitivity = freecamLookSensitivity; FreecamSmoothTime = freecamSmoothTime; ClampCameraToRadius = clampCameraToRadius; RecenterOnTargetSwitch = recenterOnTargetSwitch; DisableDuringGameOverOverride = disableDuringGameOverOverride; ToggleFreecamKey = toggleFreecamKey; RecenterKey = recenterKey; ResetToVanillaViewKey = resetToVanillaViewKey; FastMoveKey = fastMoveKey; SlowMoveKey = slowMoveKey; AscendKey = ascendKey; DescendKey = descendKey; EnableDebugLogging = enableDebugLogging; EnableNetworking = enableNetworking; EnableCapabilityHandshake = enableCapabilityHandshake; EnableSpectatorTargetSync = enableSpectatorTargetSync; EnableSpectatorPoseSync = enableSpectatorPoseSync; EnableHostRelay = enableHostRelay; SpectatorPoseSyncInterval = spectatorPoseSyncInterval; EnableVoiceActivitySync = enableVoiceActivitySync; VoiceActivitySyncInterval = voiceActivitySyncInterval; VoiceActivityStaleSeconds = voiceActivityStaleSeconds; DebugVoiceActivitySync = debugVoiceActivitySync; EnableSpectatorVoiceToTarget = enableSpectatorVoiceToTarget; SpectatorVoiceAudienceMode = spectatorVoiceAudienceMode; SpectatorVoiceToTargetVolume = spectatorVoiceToTargetVolume; SpectatorVoiceUseRemotePosePosition = spectatorVoiceUseRemotePosePosition; SpectatorVoiceEnableDistanceAttenuation = spectatorVoiceEnableDistanceAttenuation; SpectatorVoiceMinDistance = spectatorVoiceMinDistance; SpectatorVoiceMaxDistance = spectatorVoiceMaxDistance; SpectatorVoiceRolloffPower = spectatorVoiceRolloffPower; SpectatorVoiceMinimumVolume = spectatorVoiceMinimumVolume; SpectatorVoiceFallbackTo2DWhenPoseMissing = spectatorVoiceFallbackTo2DWhenPoseMissing; DebugSpectatorVoiceRouting = debugSpectatorVoiceRouting; RepairVanillaConnectedPlayerState = repairVanillaConnectedPlayerState; RepairVanillaPlayerNames = repairVanillaPlayerNames; DebugPlayerStateRepair = debugPlayerStateRepair; DebugNetworkMessages = debugNetworkMessages; DebugPoseMessages = debugPoseMessages; EnableSpectatorPresenceDebug = enableSpectatorPresenceDebug; DebugLogPresenceChanges = debugLogPresenceChanges; EnableModelInspection = enableModelInspection; LogLocalPlayerModelOnKey = logLocalPlayerModelOnKey; LogRemotePlayerModelsOnKey = logRemotePlayerModelsOnKey; ModelInspectionKey = modelInspectionKey; IncludeRendererBounds = includeRendererBounds; IncludeMaterials = includeMaterials; MaxTransformDepth = maxTransformDepth; EnableRuntimeHeadSourceInspection = enableRuntimeHeadSourceInspection; RuntimeHeadSourceInspectionKey = runtimeHeadSourceInspectionKey; RuntimeHeadSourceIncludeRendererBounds = runtimeHeadSourceIncludeRendererBounds; RuntimeHeadSourceIncludeMaterials = runtimeHeadSourceIncludeMaterials; RuntimeHeadSourceMaxTransformDepth = runtimeHeadSourceMaxTransformDepth; EnableVoiceDiagnostics = enableVoiceDiagnostics; VoiceDiagnosticsKey = voiceDiagnosticsKey; LogLocalVoiceStateOnKey = logLocalVoiceStateOnKey; LogRemoteVoiceStatesOnKey = logRemoteVoiceStatesOnKey; IncludeVoiceAudioSourceDetails = includeVoiceAudioSourceDetails; IncludeWalkieVoiceDiagnostics = includeWalkieVoiceDiagnostics; EnableFloatingHeadVisuals = enableFloatingHeadVisuals; EnablePlaceholderVisuals = enablePlaceholderVisuals; UseRuntimeDetachedHeadVisuals = useRuntimeDetachedHeadVisuals; RuntimeDetachedHeadScale = runtimeDetachedHeadScale; RuntimeDetachedHeadPitchOffset = runtimeDetachedHeadPitchOffset; RuntimeDetachedHeadYawOffset = runtimeDetachedHeadYawOffset; RuntimeDetachedHeadRollOffset = runtimeDetachedHeadRollOffset; FallbackToPlaceholderWhenDetachedHeadUnavailable = fallbackToPlaceholderWhenDetachedHeadUnavailable; ShowRemoteSpectators = showRemoteSpectators; ShowOnlySpectatorsWatchingMe = showOnlySpectatorsWatchingMe; ShowDeadSpectatorsToAlivePlayers = showDeadSpectatorsToAlivePlayers; ShowDeadSpectatorsToDeadPlayers = showDeadSpectatorsToDeadPlayers; MaxFloatingHeadsVisible = maxFloatingHeadsVisible; VisualStyle = visualStyle; PlaceholderScale = placeholderScale; BillboardSize = billboardSize; BaseAlpha = baseAlpha; UseUnlitMaterial = useUnlitMaterial; EnableDepthTest = enableDepthTest; FloatingHeadRingRadius = floatingHeadRingRadius; FloatingHeadHeightOffset = floatingHeadHeightOffset; UseCameraVisiblePlacement = useCameraVisiblePlacement; CameraForwardOffset = cameraForwardOffset; RemotePoseSmoothTime = remotePoseSmoothTime; KeepRemotePoseInView = keepRemotePoseInView; RemotePoseVisibleProxyDistance = remotePoseVisibleProxyDistance; EnableScreenFallbackVisual = enableScreenFallbackVisual; ScreenFallbackSize = screenFallbackSize; PresenceLostGraceSeconds = presenceLostGraceSeconds; FloatingHeadFaceCamera = floatingHeadFaceCamera; PulseWhenSpeaking = pulseWhenSpeaking; SpeakingScaleMultiplier = speakingScaleMultiplier; SpeakingPulseSpeed = speakingPulseSpeed; MinimumSpeakingVoiceLevel = minimumSpeakingVoiceLevel; SpeakingPulseAmount = speakingPulseAmount; VoiceAttackSmoothTime = voiceAttackSmoothTime; VoiceReleaseSmoothTime = voiceReleaseSmoothTime; SilenceScaleMultiplier = silenceScaleMultiplier; AmplitudeSmoothing = amplitudeSmoothing; DestroyOnPresenceLost = destroyOnPresenceLost; DebugVisualLifecycle = debugVisualLifecycle; ShowNameTags = showNameTags; NameTagScale = nameTagScale; NameTagHeightOffset = nameTagHeightOffset; NameTagMaxDistance = nameTagMaxDistance; NameTagUseGamePlayerNames = nameTagUseGamePlayerNames; NameTagUseFallbackIds = nameTagUseFallbackIds; DebugNameTagLifecycle = debugNameTagLifecycle; } public static EnhancedSpectatorConfig Bind(ConfigFile config) { ConfigEntry enableSpectatorModule = config.Bind("Features", "EnableSpectatorModule", true, "Loads the spectator feature module."); ConfigEntry enableEnhancedSpectator = config.Bind("Spectator.Freecam", "EnableEnhancedSpectator", true, "Enables all enhanced spectator behavior."); ConfigEntry enableFreecam = config.Bind("Spectator.Freecam", "EnableFreecam", true, "Enables local spectator freecam behavior."); ConfigEntry freecamDefaultOn = config.Bind("Spectator.Freecam", "FreecamDefaultOn", true, "Automatically enables freecam after entering vanilla spectator state."); ConfigEntry freecamRadius = config.Bind("Spectator.Freecam", "FreecamRadius", 8f, "Maximum freecam offset radius from the current target anchor."); ConfigEntry freecamMoveSpeed = config.Bind("Spectator.Freecam", "FreecamMoveSpeed", 4f, "Base freecam movement speed in units per second."); ConfigEntry freecamFastMoveMultiplier = config.Bind("Spectator.Freecam", "FreecamFastMoveMultiplier", 2.5f, "Movement multiplier while the fast movement key is held."); ConfigEntry freecamSlowMoveMultiplier = config.Bind("Spectator.Freecam", "FreecamSlowMoveMultiplier", 0.35f, "Movement multiplier while the slow movement key is held."); ConfigEntry freecamLookSensitivity = config.Bind("Spectator.Freecam", "FreecamLookSensitivity", 1f, "Mouse look sensitivity multiplier."); ConfigEntry freecamSmoothTime = config.Bind("Spectator.Freecam", "FreecamSmoothTime", 0.04f, "Smooth damp time for camera position. Set to 0 to disable smoothing."); ConfigEntry clampCameraToRadius = config.Bind("Spectator.Freecam", "ClampCameraToRadius", true, "Clamps freecam offset to FreecamRadius."); ConfigEntry recenterOnTargetSwitch = config.Bind("Spectator.Freecam", "RecenterOnTargetSwitch", true, "Recenters freecam when vanilla switches the spectated target."); ConfigEntry disableDuringGameOverOverride = config.Bind("Spectator.Freecam", "DisableDuringGameOverOverride", true, "Disables enhanced freecam while vanilla game-over spectator camera override is active."); ConfigEntry toggleFreecamKey = config.Bind("Spectator.Freecam.Keys", "ToggleFreecamKey", (KeyCode)287, "Toggles enhanced freecam while spectating."); ConfigEntry recenterKey = config.Bind("Spectator.Freecam.Keys", "RecenterKey", (KeyCode)114, "Recenters enhanced freecam around the current spectated target."); ConfigEntry resetToVanillaViewKey = config.Bind("Spectator.Freecam.Keys", "ResetToVanillaViewKey", (KeyCode)288, "Disables enhanced freecam and returns to vanilla spectator camera until toggled again."); ConfigEntry fastMoveKey = config.Bind("Spectator.Freecam.Keys", "FastMoveKey", (KeyCode)304, "Fast movement modifier key."); ConfigEntry slowMoveKey = config.Bind("Spectator.Freecam.Keys", "SlowMoveKey", (KeyCode)308, "Slow movement modifier key."); ConfigEntry ascendKey = config.Bind("Spectator.Freecam.Keys", "AscendKey", (KeyCode)32, "Moves the freecam upward while held."); ConfigEntry descendKey = config.Bind("Spectator.Freecam.Keys", "DescendKey", (KeyCode)306, "Moves the freecam downward while held."); ConfigEntry enableDebugLogging = config.Bind("Logging", "EnableDebugLogging", false, "Enables verbose Enhanced Spectator debug logs."); ConfigEntry enableNetworking = config.Bind("Networking", "EnableNetworking", true, "Enables Enhanced Spectator mod-owned networking modules."); ConfigEntry enableCapabilityHandshake = config.Bind("Networking", "EnableCapabilityHandshake", true, "Enables mod capability handshake messages over Unity Netcode custom messaging."); ConfigEntry enableSpectatorTargetSync = config.Bind("Networking", "EnableSpectatorTargetSync", true, "Enables handshake-gated spectator target state synchronization."); ConfigEntry enableSpectatorPoseSync = config.Bind("Networking", "EnableSpectatorPoseSync", true, "Enables handshake-gated spectator camera pose synchronization for placeholder visuals."); ConfigEntry enableHostRelay = config.Bind("Networking", "EnableHostRelay", true, "Enables host-mediated relay of compatible client spectator target and pose state to other modded clients. Required for Client A -> Client B visibility in three-player rooms."); ConfigEntry spectatorPoseSyncInterval = config.Bind("Networking", "SpectatorPoseSyncInterval", 0.1f, "Minimum seconds between spectator camera pose messages. Higher values reduce traffic; lower values track movement more closely."); ConfigEntry enableVoiceActivitySync = config.Bind("Networking", "EnableVoiceActivitySync", true, "Enables visual-only voice activity synchronization so remote floating heads can scale from the speaker's local microphone amplitude. This does not forward voice audio."); ConfigEntry voiceActivitySyncInterval = config.Bind("Networking", "VoiceActivitySyncInterval", 0.066f, "Minimum seconds between voice activity visual messages. Lower values react faster but send more network metadata."); ConfigEntry voiceActivityStaleSeconds = config.Bind("Networking", "VoiceActivityStaleSeconds", 0.5f, "Seconds before a received voice activity visual state is considered stale. This prevents dropped silence packets from leaving a remote head enlarged."); ConfigEntry debugVoiceActivitySync = config.Bind("Networking", "DebugVoiceActivitySync", false, "Logs voice activity sync send/receive/relay diagnostics. Keep disabled during normal testing."); ConfigEntry enableSpectatorVoiceToTarget = config.Bind("VoiceRouting", "EnableSpectatorVoiceToTarget", true, "Enables modded players to hear remote dead spectators according to SpectatorVoiceAudienceMode. Only applies between peers that advertised Enhanced Spectator voice-routing support."); ConfigEntry spectatorVoiceAudienceMode = config.Bind("VoiceRouting", "SpectatorVoiceAudienceMode", EnhancedSpectator.Config.SpectatorVoiceAudienceMode.AllModdedPlayers, "Controls who can hear routed dead spectator voice: WatchedTargetOnly, AllModdedPlayers, AliveModdedPlayersOnly, or DeadModdedPlayersOnly."); ConfigEntry spectatorVoiceToTargetVolume = config.Bind("VoiceRouting", "SpectatorVoiceToTargetVolume", 1f, "Local playback volume for routed spectator voice. This writes only local Dissonance playback volume for eligible dead spectators."); ConfigEntry spectatorVoiceUseRemotePosePosition = config.Bind("VoiceRouting", "SpectatorVoiceUseRemotePosePosition", true, "Positions routed spectator voice at the synced spectator camera pose when available. Disable to force safer 2D local playback."); ConfigEntry spectatorVoiceEnableDistanceAttenuation = config.Bind("VoiceRouting", "SpectatorVoiceEnableDistanceAttenuation", true, "Reduces routed spectator voice volume by distance from the synced spectator camera pose. Requires SpectatorVoiceUseRemotePosePosition=true and pose sync."); ConfigEntry spectatorVoiceMinDistance = config.Bind("VoiceRouting", "SpectatorVoiceMinDistance", 2f, "Distance in meters that keeps routed spectator voice at full configured volume."); ConfigEntry spectatorVoiceMaxDistance = config.Bind("VoiceRouting", "SpectatorVoiceMaxDistance", 18f, "Distance in meters where routed spectator voice reaches SpectatorVoiceMinimumVolume."); ConfigEntry spectatorVoiceRolloffPower = config.Bind("VoiceRouting", "SpectatorVoiceRolloffPower", 1.25f, "Distance attenuation curve. 1 is linear; higher values keep near voices louder and fade more near the max distance."); ConfigEntry spectatorVoiceMinimumVolume = config.Bind("VoiceRouting", "SpectatorVoiceMinimumVolume", 0f, "Minimum routed spectator voice volume multiplier at or beyond SpectatorVoiceMaxDistance."); ConfigEntry spectatorVoiceFallbackTo2DWhenPoseMissing = config.Bind("VoiceRouting", "SpectatorVoiceFallbackTo2DWhenPoseMissing", false, "Falls back to 2D routed spectator voice when synced pose data is temporarily unavailable instead of dropping voice entirely. Disabled by default so relayed listeners do not hear stale global voice when pose sync is missing."); ConfigEntry debugSpectatorVoiceRouting = config.Bind("VoiceRouting", "DebugSpectatorVoiceRouting", false, "Logs spectator voice route enable/clear diagnostics. Requires Logging.EnableDebugLogging=true."); ConfigEntry repairVanillaConnectedPlayerState = config.Bind("Networking", "RepairVanillaConnectedPlayerState", true, "Repairs late vanilla connected-player controlled flags using vanilla ClientPlayerList. This also runs in local-only sessions when the host is unmodded, fixing cases where another connected client is missing from the ESC player list or vanilla spectator target list."); ConfigEntry repairVanillaPlayerNames = config.Bind("Networking", "RepairVanillaPlayerNames", true, "Applies synced Enhanced Spectator peer names, or a vanilla Steam lobby fallback when no mod peer identity is available, to repaired player scripts and the ESC player list when vanilla still reports generic Player # names."); ConfigEntry debugPlayerStateRepair = config.Bind("Networking", "DebugPlayerStateRepair", false, "Logs each vanilla connected-player state repair. Requires Logging.EnableDebugLogging for debug output."); ConfigEntry debugNetworkMessages = config.Bind("Networking", "DebugNetworkMessages", false, "Enables verbose network message diagnostics."); ConfigEntry debugPoseMessages = config.Bind("Networking", "DebugPoseMessages", false, "Logs high-frequency spectator pose observe/send/receive diagnostics. Keep disabled during normal testing."); ConfigEntry enableSpectatorPresenceDebug = config.Bind("Presence", "EnableSpectatorPresenceDebug", true, "Enables debug-only inference of remote spectators watching the local player."); ConfigEntry debugLogPresenceChanges = config.Bind("Presence", "DebugLogPresenceChanges", false, "Logs when a remote spectator starts or stops watching the local player. Requires Logging.EnableDebugLogging."); ConfigEntry enableModelInspection = config.Bind("ModelInspection", "EnableModelInspection", false, "Enables key-triggered runtime player model hierarchy inspection."); ConfigEntry logLocalPlayerModelOnKey = config.Bind("ModelInspection", "LogLocalPlayerModelOnKey", true, "Logs local player model information when the inspection key is pressed."); ConfigEntry logRemotePlayerModelsOnKey = config.Bind("ModelInspection", "LogRemotePlayerModelsOnKey", true, "Logs remote player model information when the inspection key is pressed."); ConfigEntry modelInspectionKey = config.Bind("ModelInspection", "InspectionKey", (KeyCode)289, "Runs one runtime player model hierarchy inspection pass."); ConfigEntry includeRendererBounds = config.Bind("ModelInspection", "IncludeRendererBounds", true, "Includes renderer world bounds in inspection logs."); ConfigEntry includeMaterials = config.Bind("ModelInspection", "IncludeMaterials", false, "Includes material names in inspection logs. Disabled by default to keep logs smaller."); ConfigEntry maxTransformDepth = config.Bind("ModelInspection", "MaxTransformDepth", 8, "Maximum transform depth scanned below each player root."); ConfigEntry enableRuntimeHeadSourceInspection = config.Bind("HeadSourceInspection", "EnableRuntimeHeadSourceInspection", false, "Enables key-triggered runtime inspection of dead-body detached-head source candidates."); ConfigEntry runtimeHeadSourceInspectionKey = config.Bind("HeadSourceInspection", "InspectionKey", (KeyCode)291, "Runs one runtime detached-head source inspection pass."); ConfigEntry runtimeHeadSourceIncludeRendererBounds = config.Bind("HeadSourceInspection", "IncludeRendererBounds", true, "Includes detached-head renderer world bounds in inspection logs."); ConfigEntry runtimeHeadSourceIncludeMaterials = config.Bind("HeadSourceInspection", "IncludeMaterials", false, "Includes detached-head material names in inspection logs. Disabled by default to keep logs smaller."); ConfigEntry runtimeHeadSourceMaxTransformDepth = config.Bind("HeadSourceInspection", "MaxTransformDepth", 6, "Maximum transform depth scanned below each detached-head object."); ConfigEntry enableVoiceDiagnostics = config.Bind("VoiceDiagnostics", "EnableVoiceDiagnostics", false, "Enables key-triggered read-only voice diagnostics. This does not route, forward, or modify voice."); ConfigEntry voiceDiagnosticsKey = config.Bind("VoiceDiagnostics", "InspectionKey", (KeyCode)292, "Runs one voice diagnostics log pass."); ConfigEntry logLocalVoiceStateOnKey = config.Bind("VoiceDiagnostics", "LogLocalVoiceStateOnKey", true, "Logs local player voice state when the diagnostics key is pressed."); ConfigEntry logRemoteVoiceStatesOnKey = config.Bind("VoiceDiagnostics", "LogRemoteVoiceStatesOnKey", true, "Logs remote player voice states when the diagnostics key is pressed."); ConfigEntry includeVoiceAudioSourceDetails = config.Bind("VoiceDiagnostics", "IncludeAudioSourceDetails", true, "Includes mapped AudioSource playback details in voice diagnostics logs."); ConfigEntry includeWalkieVoiceDiagnostics = config.Bind("VoiceDiagnostics", "IncludeWalkieDiagnostics", true, "Includes walkie-talkie voice flags in voice diagnostics logs."); ConfigEntry enableFloatingHeadVisuals = config.Bind("FloatingHead", "EnableFloatingHeadVisuals", true, "Enables local placeholder visuals for remote modded spectators watching the local player."); ConfigEntry enablePlaceholderVisuals = config.Bind("FloatingHead", "EnablePlaceholderVisuals", true, "Creates simple runtime placeholder visuals instead of real head mesh clones."); ConfigEntry useRuntimeDetachedHeadVisuals = config.Bind("FloatingHead", "UseRuntimeDetachedHeadVisuals", true, "Uses the loaded ghost-girl ragdoll detached-head template as a runtime-only marker source when available. Placeholder visuals remain the fallback when the runtime source is unavailable."); ConfigEntry runtimeDetachedHeadScale = config.Bind("FloatingHead", "RuntimeDetachedHeadScale", 0.35f, "World scale multiplier applied to runtime detached-head visual clones."); ConfigEntry runtimeDetachedHeadPitchOffset = config.Bind("FloatingHead", "RuntimeDetachedHeadPitchOffset", -90f, "Pitch correction in degrees applied after the remote spectator camera rotation for runtime detached-head visuals. Default matches the calibrated detached-head template orientation."); ConfigEntry runtimeDetachedHeadYawOffset = config.Bind("FloatingHead", "RuntimeDetachedHeadYawOffset", 360f, "Yaw correction in degrees applied after the remote spectator camera rotation for runtime detached-head visuals. Default matches the calibrated detached-head template orientation."); ConfigEntry runtimeDetachedHeadRollOffset = config.Bind("FloatingHead", "RuntimeDetachedHeadRollOffset", 0f, "Roll correction in degrees applied after the remote spectator camera rotation for runtime detached-head visuals. Default matches the calibrated detached-head template orientation."); ConfigEntry fallbackToPlaceholderWhenDetachedHeadUnavailable = config.Bind("FloatingHead", "FallbackToPlaceholderWhenDetachedHeadUnavailable", true, "Falls back to placeholder visuals when runtime detached-head source data is unavailable."); ConfigEntry showRemoteSpectators = config.Bind("FloatingHead", "ShowRemoteSpectators", true, "Shows remote modded players whenever they are in spectator state and a remote pose is available."); ConfigEntry showOnlySpectatorsWatchingMe = config.Bind("FloatingHead", "ShowOnlySpectatorsWatchingMe", false, "When enabled, only shows remote spectators whose current target is the local player. When disabled, all remote spectators can be shown."); ConfigEntry showDeadSpectatorsToAlivePlayers = config.Bind("FloatingHead", "ShowDeadSpectatorsToAlivePlayers", true, "Allows living local players to see remote spectator placeholders."); ConfigEntry showDeadSpectatorsToDeadPlayers = config.Bind("FloatingHead", "ShowDeadSpectatorsToDeadPlayers", true, "Allows dead or spectating local players to see remote spectator placeholders."); ConfigEntry maxFloatingHeadsVisible = config.Bind("FloatingHead", "MaxFloatingHeadsVisible", 8, "Maximum number of remote spectator placeholders visible at once. Set to 0 to hide all placeholders."); ConfigEntry visualStyle = config.Bind("FloatingHead", "VisualStyle", FloatingHeadVisualStyle.Sphere, "Runtime-only placeholder style: Sphere, Billboard, or Ring. Sphere is the default world-space marker style."); ConfigEntry placeholderScale = config.Bind("FloatingHead", "PlaceholderScale", 0.18f, "World scale for each floating-head placeholder sphere."); ConfigEntry billboardSize = config.Bind("FloatingHead", "BillboardSize", 0.22f, "World size for billboard and ring placeholder styles."); ConfigEntry baseAlpha = config.Bind("FloatingHead", "BaseAlpha", 1f, "Placeholder material alpha where the runtime shader supports transparency."); ConfigEntry useUnlitMaterial = config.Bind("FloatingHead", "UseUnlitMaterial", true, "Prefers an unlit runtime material for placeholder visibility."); ConfigEntry enableDepthTest = config.Bind("FloatingHead", "EnableDepthTest", true, "Keeps normal depth testing for placeholders when the runtime shader supports it."); ConfigEntry floatingHeadRingRadius = config.Bind("FloatingHead", "RingRadius", 0.45f, "Horizontal radius around the local head anchor used to distribute multiple placeholders."); ConfigEntry floatingHeadHeightOffset = config.Bind("FloatingHead", "HeightOffset", 0.25f, "Vertical offset above the local head anchor for placeholder visuals."); ConfigEntry useCameraVisiblePlacement = config.Bind("FloatingHead", "UseCameraVisiblePlacement", false, "Places fallback placeholders in front of the active camera. Diagnostic only; disabled by default so remote freecam poses stay in world space."); ConfigEntry cameraForwardOffset = config.Bind("FloatingHead", "CameraForwardOffset", 1.15f, "Forward distance from the active camera when camera-visible placement is enabled."); ConfigEntry remotePoseSmoothTime = config.Bind("FloatingHead", "RemotePoseSmoothTime", 0.08f, "Smooth damp time for remote spectator placeholder movement. Set to 0 to snap to received poses."); ConfigEntry keepRemotePoseInView = config.Bind("FloatingHead", "KeepRemotePoseInView", false, "Projects remote spectator pose markers into the local camera view edge when the true remote pose is behind or outside the current view. Diagnostic only; disabled by default for strict world-space freecam following."); ConfigEntry remotePoseVisibleProxyDistance = config.Bind("FloatingHead", "RemotePoseVisibleProxyDistance", 1.35f, "Camera-forward distance for visible proxy placement when a remote spectator pose is outside the local camera view."); ConfigEntry enableScreenFallbackVisual = config.Bind("FloatingHead", "EnableScreenFallbackVisual", false, "Draws a runtime IMGUI fallback marker at the placeholder screen position when 3D marker rendering is unreliable. Intended for diagnostics only."); ConfigEntry screenFallbackSize = config.Bind("FloatingHead", "ScreenFallbackSize", 48f, "Screen fallback marker diameter in pixels."); ConfigEntry presenceLostGraceSeconds = config.Bind("FloatingHead", "PresenceLostGraceSeconds", 0.3f, "Keeps existing placeholder visuals alive briefly through transient empty presence frames. Disconnect and shutdown still clear immediately."); ConfigEntry floatingHeadFaceCamera = config.Bind("FloatingHead", "FaceCamera", true, "Rotates placeholder visuals toward the local camera when possible."); ConfigEntry pulseWhenSpeaking = config.Bind("FloatingHead", "PulseWhenSpeaking", true, "Pulses placeholder scale when local voice activity data says the remote spectator is speaking."); ConfigEntry speakingScaleMultiplier = config.Bind("FloatingHead", "SpeakingScaleMultiplier", 1.65f, "Maximum visual scale multiplier while the represented spectator is speaking at high observed amplitude."); ConfigEntry speakingPulseSpeed = config.Bind("FloatingHead", "SpeakingPulseSpeed", 8f, "Speed of the speaking visual pulse."); ConfigEntry minimumSpeakingVoiceLevel = config.Bind("FloatingHead", "MinimumSpeakingVoiceLevel", 0.35f, "Normalized fallback voice level used only when IsSpeaking is true and amplitude is unavailable or zero. Positive amplitude values are preserved so quiet syllables do not stay fully enlarged."); ConfigEntry speakingPulseAmount = config.Bind("FloatingHead", "SpeakingPulseAmount", 0.32f, "Extra scale pulse amount applied while the represented spectator is speaking."); ConfigEntry voiceAttackSmoothTime = config.Bind("FloatingHead", "VoiceAttackSmoothTime", 0.005f, "Smooth time used when voice activity starts. Lower values make the head enlarge faster."); ConfigEntry voiceReleaseSmoothTime = config.Bind("FloatingHead", "VoiceReleaseSmoothTime", 0.008f, "Smooth time used when voice activity stops. Lower values make the head shrink back faster."); ConfigEntry silenceScaleMultiplier = config.Bind("FloatingHead", "SilenceScaleMultiplier", 1f, "Visual scale multiplier while silent or when no voice activity data is available."); ConfigEntry amplitudeSmoothing = config.Bind("FloatingHead", "AmplitudeSmoothing", 0.08f, "Legacy voice activity smooth time retained for config compatibility. Current visual response uses VoiceAttackSmoothTime and VoiceReleaseSmoothTime."); ConfigEntry destroyOnPresenceLost = config.Bind("FloatingHead", "DestroyOnPresenceLost", true, "Destroys placeholder visuals when remote spectator presence is lost."); ConfigEntry debugVisualLifecycle = config.Bind("FloatingHead", "DebugVisualLifecycle", false, "Logs placeholder visual creation, destruction, and anchor loss diagnostics."); ConfigEntry showNameTags = config.Bind("NameTag", "ShowNameTags", true, "Shows runtime-only fallback identity labels above floating-head placeholders."); ConfigEntry nameTagScale = config.Bind("NameTag", "NameTagScale", 0.035f, "World-space character size for floating-head name tags."); ConfigEntry nameTagHeightOffset = config.Bind("NameTag", "NameTagHeightOffset", 0.78f, "Vertical world offset above each floating-head placeholder."); ConfigEntry nameTagMaxDistance = config.Bind("NameTag", "NameTagMaxDistance", 35f, "Maximum camera distance for rendering name tags. Set to 0 to disable distance culling."); ConfigEntry nameTagUseGamePlayerNames = config.Bind("NameTag", "NameTagUseGamePlayerNames", true, "Uses the confirmed PlayerControllerB.playerUsername display name when available."); ConfigEntry nameTagUseFallbackIds = config.Bind("NameTag", "NameTagUseFallbackIds", true, "Falls back to Client/Slot identifiers when the in-game player name is unavailable."); ConfigEntry debugNameTagLifecycle = config.Bind("NameTag", "DebugNameTagLifecycle", false, "Reserved for verbose name tag diagnostics."); return new EnhancedSpectatorConfig(enableSpectatorModule, enableEnhancedSpectator, enableFreecam, freecamDefaultOn, freecamRadius, freecamMoveSpeed, freecamFastMoveMultiplier, freecamSlowMoveMultiplier, freecamLookSensitivity, freecamSmoothTime, clampCameraToRadius, recenterOnTargetSwitch, disableDuringGameOverOverride, toggleFreecamKey, recenterKey, resetToVanillaViewKey, fastMoveKey, slowMoveKey, ascendKey, descendKey, enableDebugLogging, enableNetworking, enableCapabilityHandshake, enableSpectatorTargetSync, enableSpectatorPoseSync, enableHostRelay, spectatorPoseSyncInterval, enableVoiceActivitySync, voiceActivitySyncInterval, voiceActivityStaleSeconds, debugVoiceActivitySync, enableSpectatorVoiceToTarget, spectatorVoiceAudienceMode, spectatorVoiceToTargetVolume, spectatorVoiceUseRemotePosePosition, spectatorVoiceEnableDistanceAttenuation, spectatorVoiceMinDistance, spectatorVoiceMaxDistance, spectatorVoiceRolloffPower, spectatorVoiceMinimumVolume, spectatorVoiceFallbackTo2DWhenPoseMissing, debugSpectatorVoiceRouting, repairVanillaConnectedPlayerState, repairVanillaPlayerNames, debugPlayerStateRepair, debugNetworkMessages, debugPoseMessages, enableSpectatorPresenceDebug, debugLogPresenceChanges, enableModelInspection, logLocalPlayerModelOnKey, logRemotePlayerModelsOnKey, modelInspectionKey, includeRendererBounds, includeMaterials, maxTransformDepth, enableRuntimeHeadSourceInspection, runtimeHeadSourceInspectionKey, runtimeHeadSourceIncludeRendererBounds, runtimeHeadSourceIncludeMaterials, runtimeHeadSourceMaxTransformDepth, enableVoiceDiagnostics, voiceDiagnosticsKey, logLocalVoiceStateOnKey, logRemoteVoiceStatesOnKey, includeVoiceAudioSourceDetails, includeWalkieVoiceDiagnostics, enableFloatingHeadVisuals, enablePlaceholderVisuals, useRuntimeDetachedHeadVisuals, runtimeDetachedHeadScale, runtimeDetachedHeadPitchOffset, runtimeDetachedHeadYawOffset, runtimeDetachedHeadRollOffset, fallbackToPlaceholderWhenDetachedHeadUnavailable, showRemoteSpectators, showOnlySpectatorsWatchingMe, showDeadSpectatorsToAlivePlayers, showDeadSpectatorsToDeadPlayers, maxFloatingHeadsVisible, visualStyle, placeholderScale, billboardSize, baseAlpha, useUnlitMaterial, enableDepthTest, floatingHeadRingRadius, floatingHeadHeightOffset, useCameraVisiblePlacement, cameraForwardOffset, remotePoseSmoothTime, keepRemotePoseInView, remotePoseVisibleProxyDistance, enableScreenFallbackVisual, screenFallbackSize, presenceLostGraceSeconds, floatingHeadFaceCamera, pulseWhenSpeaking, speakingScaleMultiplier, speakingPulseSpeed, minimumSpeakingVoiceLevel, speakingPulseAmount, voiceAttackSmoothTime, voiceReleaseSmoothTime, silenceScaleMultiplier, amplitudeSmoothing, destroyOnPresenceLost, debugVisualLifecycle, showNameTags, nameTagScale, nameTagHeightOffset, nameTagMaxDistance, nameTagUseGamePlayerNames, nameTagUseFallbackIds, debugNameTagLifecycle); } } public enum SpectatorVoiceAudienceMode { WatchedTargetOnly, AllModdedPlayers, AliveModdedPlayersOnly, DeadModdedPlayersOnly } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { public IgnoresAccessChecksToAttribute(string assemblyName) { } } }