using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BoneLib; using BoneLib.BoneMenu; using ClimbAnythingWeaponFix; using HarmonyLib; using Il2CppSLZ.Marrow; using Il2CppSLZ.Marrow.Interaction; using MelonLoader; using MelonLoader.Preferences; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(WeaponFixMod), "ClimbAnythingWeaponFix", "1.0.2", "Cesar", null)] [assembly: MelonGame("Stress Level Zero", "BONELAB")] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("ClimbAnythingWeaponFix")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.2.0")] [assembly: AssemblyInformationalVersion("1.0.2+3d05db3eeca51d4ece554de84629e696d426294e")] [assembly: AssemblyProduct("ClimbAnythingWeaponFix")] [assembly: AssemblyTitle("ClimbAnythingWeaponFix")] [assembly: AssemblyVersion("1.0.2.0")] namespace ClimbAnythingWeaponFix; internal static class ClimbAnythingBridge { private static bool _looked; private static Harmony _harmony; private static object _melon; private static FieldInfo _leftStateField; private static FieldInfo _rightStateField; private static FieldInfo _handField; private static FieldInfo _climbPointField; private static FieldInfo _isClimbingField; private static MethodInfo _stopClimbing; private static int _faults; private static float _lastFaultAt = float.NegativeInfinity; private const int MaxFaults = 5; private const float FaultWindow = 10f; internal static bool Active { get; private set; } internal static void Initialize(Harmony harmony) { _harmony = harmony; if (!_looked) { MelonBase val = MelonBase.FindMelon("ClimbAnything", "Waspothegreat"); if (val != null) { _looked = true; TryBind(val); } } } internal static void Retry(Harmony harmony) { if (_harmony == null) { _harmony = harmony; } if (!_looked && _harmony != null) { _looked = true; MelonBase val = MelonBase.FindMelon("ClimbAnything", "Waspothegreat"); if (val == null) { Log.Msg("ClimbAnything is not installed — nothing to fix, staying out of the way."); } else { TryBind(val); } } } private static void TryBind(MelonBase melon) { //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown try { Type type = ((object)melon).GetType(); _leftStateField = type.GetField("_leftHandState", BindingFlags.Instance | BindingFlags.NonPublic); _rightStateField = type.GetField("_rightHandState", BindingFlags.Instance | BindingFlags.NonPublic); Type nestedType = type.GetNestedType("HandState", BindingFlags.Public | BindingFlags.NonPublic); _handField = nestedType?.GetField("Hand", BindingFlags.Instance | BindingFlags.Public); _climbPointField = nestedType?.GetField("ClimbPoint", BindingFlags.Instance | BindingFlags.Public); _isClimbingField = nestedType?.GetField("IsClimbing", BindingFlags.Instance | BindingFlags.Public); MethodInfo method = type.GetMethod("StartClimbing", BindingFlags.Instance | BindingFlags.NonPublic); _stopClimbing = type.GetMethod("StopClimbing", BindingFlags.Instance | BindingFlags.NonPublic); if (!ShapeIsSupported(method, nestedType)) { Log.Warning("ClimbAnything is installed but its internals don't match the supported layout (verified against 1.0.0 / package 1.0.1). This fix is disabled; ClimbAnything itself is untouched."); return; } _melon = melon; _harmony.Patch((MethodBase)method, new HarmonyMethod(typeof(ClimbAnythingBridge), "StartClimbingPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Active = true; MelonInfoAttribute info = melon.Info; Log.Msg("Patched ClimbAnything " + (((info != null) ? info.Version : null) ?? "?") + " — per hand: climbs on an occupied hand are blocked, and a climb needs a fresh grip press rather than a grip that was already held."); } catch (Exception ex) { Active = false; Log.Warning($"Could not patch ClimbAnything ({ex.GetType().Name}: {ex.Message}). " + "The fix is disabled; ClimbAnything itself is untouched."); } } private static bool ShapeIsSupported(MethodInfo startClimbing, Type handStateType) { if (handStateType == null || startClimbing == null || _stopClimbing == null) { return false; } if (_leftStateField == null || _rightStateField == null) { return false; } if (_handField == null || _climbPointField == null || _isClimbingField == null) { return false; } if (_handField.FieldType != typeof(Hand)) { return false; } if (_isClimbingField.FieldType != typeof(bool)) { return false; } if (!typeof(Object).IsAssignableFrom(_climbPointField.FieldType)) { return false; } ParameterInfo[] parameters = startClimbing.GetParameters(); if (parameters.Length != 2 || parameters[0].ParameterType != handStateType) { return false; } ParameterInfo[] parameters2 = _stopClimbing.GetParameters(); if (parameters2.Length != 1 || parameters2[0].ParameterType != handStateType) { return false; } return true; } private static bool StartClimbingPrefix(object __0, object __1) { if (!Active || (!Prefs.Enabled && !Prefs.RequireGripPress)) { return true; } try { object? value = _handField.GetValue(__0); Hand val = (Hand)((value is Hand) ? value : null); if ((Object)(object)val == (Object)null) { return true; } Collider target = (Collider)((__1 is Collider) ? __1 : null); if (Prefs.Enabled) { HandOccupancy.Reason reason = HandOccupancy.Evaluate(val); if (reason != HandOccupancy.Reason.Free) { Diagnostics.NoteBlocked(val, reason, target); return false; } } if (Prefs.RequireGripPress && GripIntent.Authorize(val) == GripPressTracker.Verdict.Stale) { Diagnostics.NoteStaleGrip(val, target); return false; } Diagnostics.NoteAllowed(val, target); return true; } catch (Exception e) { Fault(e); return true; } } internal static void Watchdog() { if (!Active || !Prefs.Enabled || _melon == null) { return; } try { CheckState(_leftStateField.GetValue(_melon)); CheckState(_rightStateField.GetValue(_melon)); } catch (Exception e) { Fault(e); } } private static void CheckState(object state) { if (state != null && (bool)_isClimbingField.GetValue(state) && !((Object)/*isinst with value type is only supported in some contexts*/ == (Object)null)) { object? value = _handField.GetValue(state); Hand hand = (Hand)((value is Hand) ? value : null); HandOccupancy.Reason reason = HandOccupancy.Attached(hand); if (reason != HandOccupancy.Reason.Free) { _stopClimbing.Invoke(_melon, new object[1] { state }); Diagnostics.NoteWatchdog(hand, reason); } } } private static void Fault(Exception e) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (realtimeSinceStartup - _lastFaultAt > 10f) { _faults = 0; } _lastFaultAt = realtimeSinceStartup; _faults++; if (_faults == 1) { Log.Warning("Error while guarding ClimbAnything: " + e.GetType().Name + ": " + e.Message); } if (_faults >= 5 && Active) { Active = false; Log.Warning("Disabling the ClimbAnything fixes after repeated errors. ClimbAnything continues to run exactly as it does unpatched."); } } } internal static class Diagnostics { private sealed class Episode { public bool Open; public int StartFrame; public float StartTime; public float PeakGrip; public int GameGrabbedFrame = -1; public int AttachedFrame = -1; public bool SawHover; public string HoverName; public bool Blocked; public bool GatePassed; public string AllowedTarget; public int Lines; } private const float ClimbAnythingGripThreshold = 0.01f; private static readonly Episode LeftEpisode = new Episode(); private static readonly Episode RightEpisode = new Episode(); private static bool _thresholdsLogged; private const int MaxLinesPerEpisode = 6; private static float _lastBlockedLogL = -99f; private static float _lastBlockedLogR = -99f; private static float _lastStaleGripLogL = -99f; private static float _lastStaleGripLogR = -99f; private static float _lastWatchdogLogL = -99f; private static float _lastWatchdogLogR = -99f; private const float BlockedLogInterval = 1f; private static bool Due(Hand hand, ref float left, ref float right) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 float realtimeSinceStartup = Time.realtimeSinceStartup; bool flag = (int)SideOf(hand) != 2; float num = (flag ? left : right); if (realtimeSinceStartup - num < 1f) { return false; } if (flag) { left = realtimeSinceStartup; } else { right = realtimeSinceStartup; } return true; } internal static void Tick() { if (!Prefs.Diagnostics) { return; } try { if (!Player.ControllersExist || (Object)(object)Player.RigManager == (Object)null) { Abandon(LeftEpisode); Abandon(RightEpisode); } else { LogThresholdsOnce(); Track(Player.LeftHand, Player.LeftController, LeftEpisode, "L"); Track(Player.RightHand, Player.RightController, RightEpisode, "R"); } } catch { } } private static void LogThresholdsOnce() { if (!_thresholdsLogged) { _thresholdsLogged = true; Log.Msg($"[diag] ClimbAnything treats grip > {0.01f:F3} as held. " + "Marrow does not attach until its own, much higher, grab threshold is crossed — the gap between the two is the window this mod closes. Watch the 'grab threshold crossed' line below for where it actually lands on this rig."); } } private static void Track(Hand hand, BaseController controller, Episode ep, string side) { if ((Object)(object)hand == (Object)null || (Object)(object)controller == (Object)null) { Abandon(ep); return; } float gripForce; bool flag; try { gripForce = controller.GetGripForce(); flag = controller.IsGrabbed(); } catch { Abandon(ep); return; } bool flag2 = gripForce > 0.01f; if (!ep.Open) { if (flag2) { ep.Open = true; ep.StartFrame = Time.frameCount; ep.StartTime = Time.realtimeSinceStartup; ep.PeakGrip = gripForce; ep.GameGrabbedFrame = -1; ep.AttachedFrame = -1; ep.SawHover = false; ep.HoverName = null; ep.Blocked = false; ep.GatePassed = false; ep.AllowedTarget = null; ep.Lines = 0; HandReciever val = SafeHover(hand); bool flag3 = HandOccupancy.IsPendingGrab(hand); if (flag3) { ep.SawHover = true; ep.HoverName = Name(val); } Emit(ep, $"[diag {side}] grip-press grip={gripForce:F3} trigger={SafeTrigger(hand):F2} gameGrabbed={flag} attached={HandOccupancy.Attached(hand)} hover={(((Object)(object)val == (Object)null) ? "none" : Name(val))} committedHover={flag3}"); } return; } if (gripForce > ep.PeakGrip) { ep.PeakGrip = gripForce; } if (!ep.SawHover) { HandReciever receiver = SafeHover(hand); if (HandOccupancy.IsPendingGrab(hand)) { ep.SawHover = true; ep.HoverName = Name(receiver); Emit(ep, $"[diag {side}] +{Frames(ep)}f hover committed -> {ep.HoverName} (grip={gripForce:F3})"); } } if (ep.GameGrabbedFrame < 0 && flag) { ep.GameGrabbedFrame = Time.frameCount; Emit(ep, $"[diag {side}] +{Frames(ep)}f Marrow grab threshold crossed (grip={gripForce:F3})"); } if (ep.AttachedFrame < 0 && HandOccupancy.Attached(hand) != HandOccupancy.Reason.Free) { ep.AttachedFrame = Time.frameCount; Emit(ep, $"[diag {side}] +{Frames(ep)}f ATTACHED ({HandOccupancy.Attached(hand)})"); } if (!flag2) { Close(ep, side); } } private static void Close(Episode ep, string side) { int num = ((ep.AttachedFrame < 0) ? (-1) : (ep.AttachedFrame - ep.StartFrame)); float value = (Time.realtimeSinceStartup - ep.StartTime) * 1000f; Log.Msg($"[diag {side}] episode end: peakGrip={ep.PeakGrip:F3} attachDelay={((num < 0) ? "never" : (num + "f"))} window={value:F0}ms hover={ep.HoverName ?? "none"} gateBlocked={ep.Blocked} gatePassed={ep.GatePassed}" + ((ep.GatePassed && ep.AllowedTarget != null) ? (" target=" + ep.AllowedTarget) : "")); ep.Open = false; } private static void Abandon(Episode ep) { ep.Open = false; } private static int Frames(Episode ep) { return Time.frameCount - ep.StartFrame; } private static void Emit(Episode ep, string line) { if (ep.Lines < 6) { ep.Lines++; Log.Msg(line); } } internal static void NoteBlocked(Hand hand, HandOccupancy.Reason reason, Collider target) { Episode episode = EpisodeFor(hand); if (episode != null) { episode.Blocked = true; } if (Prefs.Diagnostics && Due(hand, ref _lastBlockedLogL, ref _lastBlockedLogR)) { Log.Msg($"[diag {Side(hand)}] BLOCKED climb — {HandOccupancy.Describe(reason)}; would have anchored to '{Path(target)}'"); } } internal static void NoteStaleGrip(Hand hand, Collider target) { Episode episode = EpisodeFor(hand); if (episode != null) { episode.Blocked = true; } if (Prefs.Diagnostics && Due(hand, ref _lastStaleGripLogL, ref _lastStaleGripLogR)) { Log.Msg($"[diag {Side(hand)}] BLOCKED climb — {GripIntent.Describe(GripPressTracker.Verdict.Stale)}; would have anchored to '{Path(target)}'. Release grip and press again at the surface."); } } internal static void NoteAllowed(Hand hand, Collider target) { Episode episode = EpisodeFor(hand); if (episode != null && !episode.GatePassed) { episode.GatePassed = true; if (Prefs.Diagnostics) { episode.AllowedTarget = Path(target); } } } internal static void NoteWatchdog(Hand hand, HandOccupancy.Reason reason) { if (Due(hand, ref _lastWatchdogLogL, ref _lastWatchdogLogR)) { Log.Warning($"[{Side(hand)}] tore down a climb point on an occupied hand ({HandOccupancy.Describe(reason)})."); } } private static Episode EpisodeFor(Hand hand) { //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_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 Handedness val = SideOf(hand); if ((int)val != 1) { if ((int)val == 2) { return RightEpisode; } return null; } return LeftEpisode; } private static string Side(Hand hand) { //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_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 Handedness val = SideOf(hand); if ((int)val != 1) { if ((int)val == 2) { return "R"; } return "?"; } return "L"; } private static Handedness SideOf(Hand hand) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return HandOccupancy.SideOf(hand); } private static HandReciever SafeHover(Hand hand) { try { return hand.HoveringReceiver; } catch { return null; } } private static float SafeTrigger(Hand hand) { try { return hand.GetIndexTriggerAxis(); } catch { return -1f; } } private static string Name(HandReciever receiver) { try { return ((Object)(object)receiver == (Object)null) ? "none" : ((Object)receiver).name; } catch { return "?"; } } private static string Path(Collider collider) { try { if ((Object)(object)collider == (Object)null) { return "none"; } string text = ((Object)collider).name; Transform parent = ((Component)collider).transform.parent; for (int i = 0; i < 3; i++) { if (!((Object)(object)parent != (Object)null)) { break; } text = ((Object)parent).name + "/" + text; parent = parent.parent; } return text; } catch { return "?"; } } } internal static class GripIntent { internal const float DefaultWindowMs = 80f; internal const float MaxWindowMs = 500f; private static float Window => GripPressTracker.ClampWindowMs(Prefs.GripPressWindowMs, 80f, 500f) / 1000f; internal static GripPressTracker.Verdict Authorize(Hand hand) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) GripPressTracker gripPressTracker = GripWatch.TrackerFor(HandOccupancy.SideOf(hand)); if (gripPressTracker == null) { return GripPressTracker.Verdict.Unknown; } GripPressTracker.Verdict num = gripPressTracker.Check(Time.frameCount, Time.realtimeSinceStartup, Window); if (num == GripPressTracker.Verdict.Fresh) { gripPressTracker.Consume(); } return num; } internal static string Describe(GripPressTracker.Verdict verdict) { return verdict switch { GripPressTracker.Verdict.Fresh => "fresh grip press", GripPressTracker.Verdict.Stale => "no fresh grip press (grip was already held before this hand reached the surface)", _ => "grip state unreadable", }; } } internal sealed class GripPressTracker { internal enum Phase { Unknown, Released, Held } internal enum Verdict { Fresh, Stale, Unknown } private Phase _phase; private float _pressedAt = -1f; private int _pressedFrame = -1; private float _heldSince = -1f; private int _consumedFrame = -1; internal Phase State => _phase; internal int EdgeFrame => _pressedFrame; internal float EdgeTime => _pressedAt; internal bool CanRead => _phase != Phase.Unknown; internal float HeldFor(float now) { if (!(_heldSince < 0f)) { return now - _heldSince; } return -1f; } internal void Observe(bool readable, bool held, int frame, float now) { if (!readable) { _phase = Phase.Unknown; _heldSince = -1f; _pressedAt = -1f; _pressedFrame = -1; return; } if (!held) { _phase = Phase.Released; _heldSince = -1f; _pressedAt = -1f; _pressedFrame = -1; return; } if (_phase == Phase.Released) { _heldSince = now; _pressedAt = now; _pressedFrame = frame; } else if (_phase == Phase.Unknown) { _heldSince = now; _pressedAt = -1f; _pressedFrame = -1; } _phase = Phase.Held; } internal Verdict Check(int frame, float now, float window) { if (_phase == Phase.Unknown) { return Verdict.Unknown; } if (_pressedFrame < 0) { return Verdict.Stale; } if (_pressedFrame == _consumedFrame) { return Verdict.Stale; } if (frame == _pressedFrame) { return Verdict.Fresh; } float num = now - _pressedAt; if (!(num >= 0f) || !(num <= window)) { return Verdict.Stale; } return Verdict.Fresh; } internal void Consume() { _consumedFrame = _pressedFrame; } internal static float ClampWindowMs(float ms, float defaultMs, float maxMs) { if (float.IsNaN(ms)) { return defaultMs; } if (ms < 0f) { return 0f; } if (!(ms > maxMs)) { return ms; } return maxMs; } } internal static class GripWatch { internal const float PressThreshold = 0.01f; internal const float GrabWindow = 0.5f; private static readonly GripPressTracker Left = new GripPressTracker(); private static readonly GripPressTracker Right = new GripPressTracker(); private static int _leftSampledFrame = -1; private static int _rightSampledFrame = -1; internal static void Tick() { Sample((Handedness)1); Sample((Handedness)2); } internal static GripPressTracker TrackerFor(Handedness handedness) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_000a: 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_0012: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 if ((int)handedness != 1 && (int)handedness != 2) { return null; } Sample(handedness); if ((int)handedness != 1) { return Right; } return Left; } internal static float HeldFor(Handedness handedness) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return TrackerFor(handedness)?.HeldFor(Time.realtimeSinceStartup) ?? (-1f); } internal static bool WithinGrabWindow(Handedness handedness) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) float num = HeldFor(handedness); if (num >= 0f) { return num <= 0.5f; } return false; } private static void Sample(Handedness handedness) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Invalid comparison between Unknown and I4 //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Invalid comparison between Unknown and I4 //IL_001e: 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) int frameCount = Time.frameCount; if ((int)handedness == 1) { if (_leftSampledFrame != frameCount) { _leftSampledFrame = frameCount; Feed(Left, handedness, frameCount); } } else if ((int)handedness == 2 && _rightSampledFrame != frameCount) { _rightSampledFrame = frameCount; Feed(Right, handedness, frameCount); } } private static void Feed(GripPressTracker tracker, Handedness handedness, int frame) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) float realtimeSinceStartup = Time.realtimeSinceStartup; BaseController val = Controller(handedness); if ((Object)(object)val == (Object)null) { tracker.Observe(readable: false, held: false, frame, realtimeSinceStartup); return; } float gripForce; try { gripForce = val.GetGripForce(); } catch { tracker.Observe(readable: false, held: false, frame, realtimeSinceStartup); return; } tracker.Observe(readable: true, gripForce > 0.01f, frame, realtimeSinceStartup); } private static BaseController Controller(Handedness handedness) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 try { if (!Player.ControllersExist) { return null; } return ((int)handedness == 1) ? Player.LeftController : Player.RightController; } catch { return null; } } } internal static class HandOccupancy { internal enum Reason { Free, AttachedReceiver, AttachedObject, PendingGrab } internal static Reason Attached(Hand hand) { if ((Object)(object)hand == (Object)null) { return Reason.Free; } if ((Object)(object)hand.AttachedReceiver != (Object)null) { return Reason.AttachedReceiver; } if (hand.HasAttachedObject()) { return Reason.AttachedObject; } return Reason.Free; } internal static Reason Evaluate(Hand hand) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) Reason reason = Attached(hand); if (reason != Reason.Free) { return reason; } if (Prefs.BlockPendingGrab && GripWatch.WithinGrabWindow(SideOf(hand)) && IsPendingGrab(hand)) { return Reason.PendingGrab; } return Reason.Free; } internal static bool IsPendingGrab(Hand hand) { if ((Object)(object)hand == (Object)null) { return false; } try { HandReciever hoveringReceiver = hand.HoveringReceiver; if ((Object)(object)hoveringReceiver == (Object)null) { return false; } if (!hoveringReceiver.HasHost) { return false; } if (hoveringReceiver.IsStatic) { return false; } if (!hoveringReceiver.HasRigidbody) { return false; } return IsWithinReach(hand, hoveringReceiver); } catch { return false; } } private static bool IsWithinReach(Hand hand, HandReciever receiver) { //IL_0023: 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_0033: 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) float num = hand.HoverSphereRadius; if (num <= 0f) { num = 0.1f; } float num2 = num * 4f; Vector3 val = ((Component)receiver).transform.position - ((Component)hand).transform.position; return ((Vector3)(ref val)).sqrMagnitude <= num2 * num2; } internal static Handedness SideOf(Hand hand) { //IL_0017: 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_001a: 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) try { return (Handedness)((!((Object)(object)hand == (Object)null)) ? ((int)hand.handedness) : 0); } catch { return (Handedness)0; } } internal static string Describe(Reason reason) { return reason switch { Reason.AttachedReceiver => "hand.AttachedReceiver", Reason.AttachedObject => "hand.HasAttachedObject", Reason.PendingGrab => "hand.HoveringReceiver (dynamic, rigidbody-hosted, in reach)", _ => "free", }; } } internal static class Prefs { private static MelonPreferences_Category _category; private static MelonPreferences_Entry _enabled; private static MelonPreferences_Entry _blockPendingGrab; private static MelonPreferences_Entry _requireGripPress; private static MelonPreferences_Entry _gripPressWindowMs; private static MelonPreferences_Entry _diagnostics; internal static bool Enabled { get { return _enabled?.Value ?? true; } set { if (_enabled != null) { _enabled.Value = value; MelonPreferences_Category category = _category; if (category != null) { category.SaveToFile(false); } } } } internal static bool BlockPendingGrab { get { return _blockPendingGrab?.Value ?? true; } set { if (_blockPendingGrab != null) { _blockPendingGrab.Value = value; MelonPreferences_Category category = _category; if (category != null) { category.SaveToFile(false); } } } } internal static bool RequireGripPress { get { return _requireGripPress?.Value ?? true; } set { if (_requireGripPress != null) { _requireGripPress.Value = value; MelonPreferences_Category category = _category; if (category != null) { category.SaveToFile(false); } } } } internal static float GripPressWindowMs { get { return _gripPressWindowMs?.Value ?? 80f; } set { if (_gripPressWindowMs != null) { _gripPressWindowMs.Value = value; MelonPreferences_Category category = _category; if (category != null) { category.SaveToFile(false); } } } } internal static bool Diagnostics { get { return _diagnostics?.Value ?? false; } set { if (_diagnostics != null) { _diagnostics.Value = value; MelonPreferences_Category category = _category; if (category != null) { category.SaveToFile(false); } } } } internal static void Setup() { _category = MelonPreferences.CreateCategory("ClimbAnything Weapon Fix"); _enabled = _category.CreateEntry("Enabled", true, "Enabled", "Block ClimbAnything from starting a climb on a hand that is already grabbing something. This covers the weapon-grip launch only; the sticky-hand fix has its own switch, and ClimbAnything is fully unpatched only when both are off.", false, false, (ValueValidator)null, (string)null); _blockPendingGrab = _category.CreateEntry("BlockPendingGrab", true, "Block climbs on a pending grab", "Also refuse a climb when Marrow's hover pass has already selected a physical grabbable for that hand. This is what closes the window between the grip leaving zero and the grab completing. Turn it off to gate on a completed attach only.", false, false, (ValueValidator)null, (string)null); _requireGripPress = _category.CreateEntry("RequireGripPress", true, "Require an intentional grip press", "Only let ClimbAnything start a climb on a hand that has just pressed grip. ClimbAnything otherwise treats a held grip as a standing request to climb and re-scans every frame, so a grip squeezed in open air makes the hand sticky and it attaches to whatever wall it later touches. Turn this off to restore ClimbAnything's original hold-to-climb behaviour.", false, false, (ValueValidator)null, (string)null); _gripPressWindowMs = _category.CreateEntry("GripPressWindowMs", 80f, "Grip press window (ms)", "How long after pressing grip a climb may still start. Reaching for a wall and squeezing is one motion, so ClimbAnything can find the surface a frame or two after the press; this is the slack that covers it. Lower is stricter. 0 means the press and the surface must land on the same frame.", false, false, (ValueValidator)null, (string)null); _diagnostics = _category.CreateEntry("Diagnostics", false, "Diagnostic logging", "Log each grab attempt: grip force, trigger, hover state, and how many frames Marrow took to attach. Useful for comparing 'grip the weapon' against 'trigger first, then grip'.", false, false, (ValueValidator)null, (string)null); } } public class WeaponFixMod : MelonMod { private static Instance _logger; public override void OnInitializeMelon() { _logger = ((MelonBase)this).LoggerInstance; try { Prefs.Setup(); } catch (Exception ex) { Warn("Could not create preferences (" + ex.GetType().Name + "); running on defaults."); } ClimbAnythingBridge.Initialize(((MelonBase)this).HarmonyInstance); SetupMenu(); } public override void OnLateInitializeMelon() { ClimbAnythingBridge.Retry(((MelonBase)this).HarmonyInstance); } public override void OnUpdate() { GripWatch.Tick(); ClimbAnythingBridge.Watchdog(); Diagnostics.Tick(); } public override void OnFixedUpdate() { ClimbAnythingBridge.Watchdog(); } private static void SetupMenu() { //IL_000a: 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_0051: 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_00bb: 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) try { Page obj = Page.Root.CreatePage("ClimbAnything Weapon Fix", Color.cyan, 0, true); obj.CreateBool("Enabled", Color.cyan, Prefs.Enabled, (Action)delegate(bool value) { Prefs.Enabled = value; }); obj.CreateBool("Block pending grabs", Color.cyan, Prefs.BlockPendingGrab, (Action)delegate(bool value) { Prefs.BlockPendingGrab = value; }); obj.CreateBool("Require grip press", Color.cyan, Prefs.RequireGripPress, (Action)delegate(bool value) { Prefs.RequireGripPress = value; }); obj.CreateFloat("Grip press window (ms)", Color.cyan, Prefs.GripPressWindowMs, 10f, 0f, 500f, (Action)delegate(float value) { Prefs.GripPressWindowMs = value; }); obj.CreateBool("Diagnostic logging", Color.yellow, Prefs.Diagnostics, (Action)delegate(bool value) { Prefs.Diagnostics = value; }); } catch (Exception ex) { Log.Warning("Could not build the BoneMenu page (" + ex.GetType().Name + "); preferences still work from the config file."); } } internal static void Msg(string message) { Instance logger = _logger; if (logger != null) { logger.Msg(message); } } internal static void Warn(string message) { Instance logger = _logger; if (logger != null) { logger.Warning(message); } } } internal static class Log { internal static void Msg(string message) { WeaponFixMod.Msg(message); } internal static void Warning(string message) { WeaponFixMod.Warn(message); } }