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();
}
}