using System; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using BepInEx; using FishNet; using FishNet.Managing.Server; using FishNet.Managing.Timing; using HarmonyLib; using UnityEngine; [assembly: AssemblyDescription("Freezes gameplay while the native pause menu is open in strict singleplayer sessions only.")] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyCompany("evansvl")] [assembly: CompilationRelaxations(8)] [assembly: AssemblyVersion("0.0.0.0")] namespace HowToFish.SingleplayerPause; [BepInPlugin("community.howtofish.singleplayerpause", "Singleplayer Pause", "0.1.10")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "community.howtofish.singleplayerpause"; public const string PluginName = "Singleplayer Pause"; public const string PluginVersion = "0.1.10"; private const float MinimumSafePauseDelay = 0.08f; private static readonly FieldInfo HasCoolDownField = AccessTools.Field(typeof(Weapon), "_hasCoolDown"); private static readonly FieldInfo IsReloadingField = AccessTools.Field(typeof(Weapon), "_isReloading"); private static readonly FieldInfo QueueReloadField = AccessTools.Field(typeof(Weapon), "_queueReload"); private static readonly FieldInfo QueuedShootField = AccessTools.Field(typeof(Weapon), "_queuedShoot"); private static readonly FieldInfo DisabledBeforeShootAnimField = AccessTools.Field(typeof(Weapon), "_disabledBeforeShootAnim"); private static readonly FieldInfo HoldingFireInputField = AccessTools.Field(typeof(Weapon), "_holdingFireInput"); private static readonly FieldInfo HandHoldRightField = AccessTools.Field(typeof(PlayerHands), "_curHandHoldAmountRight"); private static readonly FieldInfo HandHoldLeftField = AccessTools.Field(typeof(PlayerHands), "_curHandHoldAmountLeft"); private static readonly FieldInfo InHandHolderField = AccessTools.Field(typeof(Item), "_inHandHolder"); private static readonly FieldInfo OutOfHandHolderField = AccessTools.Field(typeof(Item), "_outOfHandHolder"); private static readonly FieldInfo RodLineField = AccessTools.Field(typeof(FishingRod), "_line"); private static readonly FieldInfo RodDisabledField = AccessTools.Field(typeof(FishingRod), "_isDisabled"); private static Plugin _instance; private bool _ownsFreeze; private bool _awaitingPositiveDeltaAfterResume; private float _restoreTimeScale = 1f; private float _pauseDetectedAt = -1f; private bool _loggedWeaponDelay; private Tool _lastRepairedTool; private float _nextDiagnosticAt; private string _lastDiagnosticState; private Harmony _harmony; private void Awake() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown _instance = this; _harmony = new Harmony("community.howtofish.singleplayerpause"); PatchPrefix(typeof(TimeManager), "IncreaseTick", "IncreaseTickPrefix"); PatchPrefix(typeof(PlayerToolMovement), "LateUpdate", "PlayerToolMovementLateUpdatePrefix"); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Loaded. Strict-singleplayer pause freezes Unity time and FishNet simulation ticks while guarding held-tool sway from zero-delta-time updates."); } private void PatchPrefix(Type targetType, string targetName, string hookName) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(targetType, targetName, (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(TimeManagerHooks), hookName, (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { throw new MissingMethodException(targetType.FullName, targetName); } _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private void Update() { if (PauseManager.IsPaused && IsStrictSingleplayerWorld()) { if (_ownsFreeze) { Freeze(); return; } if (_pauseDetectedAt < 0f) { _pauseDetectedAt = Time.realtimeSinceStartup; LogRuntimeState("pause-request", force: true); } if (Time.realtimeSinceStartup - _pauseDetectedAt >= 0.08f && !HasActiveHeldToolTransition()) { Freeze(); } return; } _pauseDetectedAt = -1f; _loggedWeaponDelay = false; _lastRepairedTool = null; if (_ownsFreeze) { LogRuntimeState("pause-release", force: true); } _lastDiagnosticState = null; Restore(); if (_awaitingPositiveDeltaAfterResume && Time.deltaTime > 1E-06f) { _awaitingPositiveDeltaAfterResume = false; } } private bool HasActiveHeldToolTransition() { Player localPlayer = Player.LocalPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)localPlayer.Holding == (Object)null) { return false; } Item heldItem = localPlayer.Holding.HeldItem; Tool val = (Tool)(object)((heldItem is Tool) ? heldItem : null); PlayerToolMovement toolMovement = localPlayer.ToolMovement; if ((Object)(object)toolMovement != (Object)null) { Tool currentTool = toolMovement.CurrentTool; if ((Object)(object)currentTool != (Object)(object)val || ((Object)(object)currentTool != (Object)null && toolMovement.HoldPercent < 0.999f)) { LogToolDelay(); return true; } } Weapon val2 = (Weapon)(object)((val is Weapon) ? val : null); if ((Object)(object)val2 == (Object)null) { return false; } bool flag = ReadBool(HasCoolDownField, val2) || ReadBool(IsReloadingField, val2) || ReadBool(QueueReloadField, val2) || ReadBool(QueuedShootField, val2) || ReadBool(DisabledBeforeShootAnimField, val2) || ReadBool(HoldingFireInputField, val2); if (flag) { LogToolDelay(); } return flag; } private void LogToolDelay() { if (!_loggedWeaponDelay) { _loggedWeaponDelay = true; ((BaseUnityPlugin)this).Logger.LogDebug((object)"Pause freeze delayed until the held tool finishes its equip, shot, or reload transition."); } } private static bool ReadBool(FieldInfo field, Weapon weapon) { if (field != null) { return (bool)field.GetValue(weapon); } return false; } private static bool IsStrictSingleplayerWorld() { if (MainMenuManager.IsInMenu) { return false; } if ((Object)(object)ConnectionManager.Instance == (Object)null || ConnectionManager.IsUsingSteam) { return false; } if (!InstanceFinder.IsHostStarted) { return false; } ServerManager serverManager = InstanceFinder.ServerManager; if ((Object)(object)serverManager == (Object)null || !serverManager.Started || serverManager.Clients == null) { return false; } if (serverManager.Clients.Count != 1) { return false; } return (Object)(object)Player.LocalPlayer != (Object)null; } private void Freeze() { if (!_ownsFreeze) { _restoreTimeScale = Time.timeScale; _ownsFreeze = true; _loggedWeaponDelay = false; ((BaseUnityPlugin)this).Logger.LogDebug((object)"Singleplayer pause freeze acquired."); } if (!Mathf.Approximately(Time.timeScale, 0f)) { Time.timeScale = 0f; } MaintainHeldToolPresentation(); LogRuntimeState("paused", force: false); } private void MaintainHeldToolPresentation() { Player localPlayer = Player.LocalPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)localPlayer.Holding == (Object)null) { return; } Item heldItem = localPlayer.Holding.HeldItem; Tool val = (Tool)(object)((heldItem is Tool) ? heldItem : null); if (!((Object)(object)val == (Object)null) && !((Object)(object)val.HandsMesh == (Object)null)) { bool flag = !val.HandsMesh.enabled; if (val.TryActivateAnimatedHands(localPlayer) && flag && (Object)(object)_lastRepairedTool != (Object)(object)val) { _lastRepairedTool = val; ((BaseUnityPlugin)this).Logger.LogInfo((object)("Restored paused animated-hands renderer for held tool " + ((object)val).GetType().Name + ".")); } } } private void LogRuntimeState(string phase, bool force) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (force || !(realtimeSinceStartup < _nextDiagnosticAt)) { _nextDiagnosticAt = realtimeSinceStartup + 0.25f; string text = DescribeRuntimeState(); if (force || !string.Equals(text, _lastDiagnosticState, StringComparison.Ordinal)) { _lastDiagnosticState = text; ((BaseUnityPlugin)this).Logger.LogInfo((object)("STATE " + phase + " | " + text)); } } } private static string DescribeRuntimeState() { //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { return "player=null timeScale=" + Time.timeScale; } PlayerHolding holding = localPlayer.Holding; Item val = (((Object)(object)holding == (Object)null) ? null : holding.HeldItem); Tool val2 = (Tool)(object)((val is Tool) ? val : null); PlayerToolMovement toolMovement = localPlayer.ToolMovement; Tool val3 = (((Object)(object)toolMovement == (Object)null) ? null : toolMovement.CurrentTool); StringBuilder stringBuilder = new StringBuilder(384); stringBuilder.Append("timeScale=").Append(Time.timeScale); stringBuilder.Append(" deltaTime=").Append(Time.deltaTime); stringBuilder.Append(" paused=").Append(PauseManager.IsPaused); stringBuilder.Append(" afk=").Append(localPlayer.IsAfk); stringBuilder.Append(" held=").Append(ObjectLabel((Object)(object)val)); stringBuilder.Append(" currentTool=").Append(ObjectLabel((Object)(object)val3)); stringBuilder.Append(" sameTool=").Append((Object)(object)val2 == (Object)(object)val3); if ((Object)(object)val != (Object)null) { stringBuilder.Append(" holderLocal=").Append((Object)(object)val.Holder == (Object)(object)localPlayer); stringBuilder.Append(" syncedHolderLocal=").Append((Object)(object)val.SyncedHolder == (Object)(object)localPlayer); stringBuilder.Append(" inventory=").Append(val.IsInInventory); stringBuilder.Append(" active=").Append(((Component)val).gameObject.activeInHierarchy); stringBuilder.Append(" pos=").Append(VectorLabel(((Component)val).transform.position)); AppendGameObjectState(stringBuilder, " inHand", InHandHolderField, val); AppendGameObjectState(stringBuilder, " outHand", OutOfHandHolderField, val); } if ((Object)(object)toolMovement != (Object)null) { stringBuilder.Append(" holdPercent=").Append(toolMovement.HoldPercent.ToString("0.000")); } if ((Object)(object)val2 != (Object)null) { Renderer handsMesh = val2.HandsMesh; stringBuilder.Append(" handsMesh=").Append(((Object)(object)handsMesh == (Object)null) ? "null" : (handsMesh.enabled + "/" + ((Component)handsMesh).gameObject.activeInHierarchy)); stringBuilder.Append(" swayPos=").Append(((Object)(object)val2.SwayTransform == (Object)null) ? "null" : VectorLabel(val2.SwayTransform.position)); } PlayerHands hands = localPlayer.Hands; if ((Object)(object)hands != (Object)null) { stringBuilder.Append(" handR=").Append(ReadFloat(HandHoldRightField, hands).ToString("0.000")); stringBuilder.Append(" handL=").Append(ReadFloat(HandHoldLeftField, hands).ToString("0.000")); } FishingRod val4 = (FishingRod)(object)((val2 is FishingRod) ? val2 : null); if ((Object)(object)val4 != (Object)null) { LineRenderer val5 = (LineRenderer)((RodLineField == null) ? null : /*isinst with value type is only supported in some contexts*/); stringBuilder.Append(" rodDisabled=").Append(ReadObjectBool(RodDisabledField, val4)); stringBuilder.Append(" line=").Append(((Object)(object)val5 == (Object)null) ? "null" : (((Renderer)val5).enabled + "/" + ((Component)val5).gameObject.activeInHierarchy)); } return stringBuilder.ToString(); } private static string ObjectLabel(Object value) { if (!(value == (Object)null)) { return ((object)value).GetType().Name + "#" + value.GetInstanceID(); } return "null"; } private static string VectorLabel(Vector3 value) { return value.x.ToString("0.00") + "," + value.y.ToString("0.00") + "," + value.z.ToString("0.00"); } private static float ReadFloat(FieldInfo field, object instance) { if (!(field == null) && instance != null) { return (float)field.GetValue(instance); } return float.NaN; } private static bool ReadObjectBool(FieldInfo field, object instance) { if (field != null && instance != null) { return (bool)field.GetValue(instance); } return false; } private static void AppendGameObjectState(StringBuilder value, string label, FieldInfo field, Item item) { GameObject val = (GameObject)((field == null) ? null : /*isinst with value type is only supported in some contexts*/); value.Append(label).Append('=').Append(((Object)(object)val == (Object)null) ? "null" : (val.activeSelf + "/" + val.activeInHierarchy)); } private void Restore() { if (_ownsFreeze) { if (Mathf.Approximately(Time.timeScale, 0f)) { Time.timeScale = _restoreTimeScale; } _ownsFreeze = false; _awaitingPositiveDeltaAfterResume = true; ((BaseUnityPlugin)this).Logger.LogDebug((object)"Singleplayer pause freeze released."); } } internal static bool AllowSimulationTick() { Plugin instance = _instance; if ((Object)(object)instance == (Object)null) { return true; } return !instance._ownsFreeze; } internal static bool AllowHeldToolFrameUpdate() { Plugin instance = _instance; if (!((Object)(object)instance == (Object)null)) { if (!instance._ownsFreeze) { return !instance._awaitingPositiveDeltaAfterResume; } return false; } return true; } private void OnDisable() { _pauseDetectedAt = -1f; Restore(); _awaitingPositiveDeltaAfterResume = false; } private void OnDestroy() { Restore(); _awaitingPositiveDeltaAfterResume = false; if (_harmony != null) { _harmony.UnpatchSelf(); } if ((Object)(object)_instance == (Object)(object)this) { _instance = null; } } private void OnApplicationQuit() { Restore(); } } internal static class TimeManagerHooks { internal static bool IncreaseTickPrefix() { return Plugin.AllowSimulationTick(); } internal static bool PlayerToolMovementLateUpdatePrefix() { return Plugin.AllowHeldToolFrameUpdate(); } }