using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("FaerieFlight")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.6.0")] [assembly: AssemblyInformationalVersion("0.2.6+f28d65039db0f6b3cd91fbb4364c06ba853f7391")] [assembly: AssemblyProduct("FaerieFlight")] [assembly: AssemblyTitle("FaerieFlight")] [assembly: AssemblyVersion("0.2.6.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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] [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] [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 FaerieFlight { [HarmonyPatch(typeof(FlashlightController), "Update")] internal static class FlashlightTumblePatch { private static readonly FieldRef IsTumblingRef = AccessTools.FieldRefAccess("isTumbling"); private static readonly FieldRef IsCrouchingRef = AccessTools.FieldRefAccess("isCrouching"); private static readonly FieldRef IsSlidingRef = AccessTools.FieldRefAccess("isSliding"); private static readonly FieldRef IsLocalRef = AccessTools.FieldRefAccess("isLocal"); private static readonly FieldRef ActiveRef = AccessTools.FieldRefAccess("active"); private static readonly FieldRef HideFlashlightRef = AccessTools.FieldRefAccess("hideFlashlight"); private static float _logAccum; [HarmonyPrefix] private static void Prefix(FlashlightController __instance, ref FlashState __state) { __state = default(FlashState); try { if (Plugin.Enabled.Value && Plugin.Flashlight.Value && !((Object)(object)RunManager.instance == (Object)null) && !SemiFunc.MenuLevel() && SemiFunc.RunIsLevel()) { PlayerAvatar playerAvatar = __instance.PlayerAvatar; if (!((Object)(object)playerAvatar == (Object)null) && IsLocalRef.Invoke(playerAvatar)) { __state.masked = true; __state.tumbling = IsTumblingRef.Invoke(playerAvatar); __state.crouching = IsCrouchingRef.Invoke(playerAvatar); __state.sliding = IsSlidingRef.Invoke(playerAvatar); IsTumblingRef.Invoke(playerAvatar) = false; IsCrouchingRef.Invoke(playerAvatar) = false; IsSlidingRef.Invoke(playerAvatar) = false; } } } catch (Exception arg) { Plugin.Log.LogError((object)$"FlashlightTumblePatch.Prefix: {arg}"); } } [HarmonyPostfix] private static void Postfix(FlashlightController __instance, FlashState __state) { if (!__state.masked) { return; } try { PlayerAvatar playerAvatar = __instance.PlayerAvatar; if ((Object)(object)playerAvatar != (Object)null) { IsTumblingRef.Invoke(playerAvatar) = __state.tumbling; IsCrouchingRef.Invoke(playerAvatar) = __state.crouching; IsSlidingRef.Invoke(playerAvatar) = __state.sliding; } _logAccum += Time.deltaTime; if (_logAccum >= 1f) { _logAccum = 0f; Plugin.Log.LogInfo((object)($"[FloatDiag] flashlight active={ActiveRef.Invoke(__instance)} " + $"LightActive={__instance.LightActive} hide={HideFlashlightRef.Invoke(__instance)} " + $"wasTumbling={__state.tumbling} wasCrouching={__state.crouching}")); } } catch (Exception arg) { Plugin.Log.LogError((object)$"FlashlightTumblePatch.Postfix: {arg}"); } } } internal struct FlashState { public bool masked; public bool tumbling; public bool crouching; public bool sliding; } public class FloatDriver : MonoBehaviour, IOnEventCallback { private const float AffectTime = 5f; private const float RefreshInterval = 4f; private const float PrefabScanCooldown = 2f; private const float PrefabScanSettleDelay = 1.5f; private const float GrabOverrideTime = 0.2f; private const float ClimbSuppressTime = 0.5f; private const float MissingEffectRebroadcastDelay = 0.25f; private const string StaffItemKey = "Item Staff Zero Gravity"; private const byte FloatEventCode = 117; private static readonly byte[] RepoReservedEventCodes = new byte[3] { 123, 124, 199 }; private static readonly FieldRef AvatarTumbleRef = AccessTools.FieldRefAccess("tumble"); private static readonly FieldRef AvatarDeadSetRef = AccessTools.FieldRefAccess("deadSet"); private static readonly FieldRef AvatarIsDisabledRef = AccessTools.FieldRefAccess("isDisabled"); private static readonly FieldRef AvatarIsTumblingRef = AccessTools.FieldRefAccess("isTumbling"); private static readonly FieldRef AvatarIsLocalRef = AccessTools.FieldRefAccess("isLocal"); private static readonly FieldRef TumblePhysGrabObjectRef = AccessTools.FieldRefAccess("physGrabObject"); private static readonly FieldRef GrabbedPhysGrabObjectRef = AccessTools.FieldRefAccess("grabbedPhysGrabObject"); private static readonly FieldRef LevelAnimationCompletedRef = AccessTools.FieldRefAccess("levelAnimationCompleted"); private static readonly FieldRef AffectTimerRef = AccessTools.FieldRefAccess("timer"); private static readonly FieldRef AffectTimerTotalRef = AccessTools.FieldRefAccess("timerTotal"); private GameObject? _affectPrefab; private bool _searched; private float _nextPrefabScan; private bool _photonCallbackRegistered; private readonly Dictionary _active = new Dictionary(); private float _nextBroadcast; private float _logAccum; private static bool ShouldFloat() { if ((Object)(object)RunManager.instance == (Object)null) { return false; } if (SemiFunc.MenuLevel()) { return false; } return SemiFunc.RunIsLevel(); } private static bool IsAlive(PlayerAvatar pa) { if (!AvatarDeadSetRef.Invoke(pa)) { return !AvatarIsDisabledRef.Invoke(pa); } return false; } private static bool LevelFullyLoaded() { LoadingUI instance = LoadingUI.instance; if ((Object)(object)instance != (Object)null) { return LevelAnimationCompletedRef.Invoke(instance); } return false; } private void OnEnable() { if (Array.IndexOf(RepoReservedEventCodes, (byte)117) >= 0) { Plugin.Log.LogError((object)($"FloatEventCode {(byte)117} collides with a REPO-reserved event (kick/ban); " + "networked float disabled to avoid kicking clients. Pick an unreserved code.")); ((Behaviour)this).enabled = false; } else { SceneManager.sceneLoaded += OnSceneLoaded; } } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; UnregisterPhotonCallback(); } private void RegisterPhotonCallback() { if (!_photonCallbackRegistered) { PhotonNetwork.AddCallbackTarget((object)this); _photonCallbackRegistered = true; } } private void UnregisterPhotonCallback() { if (_photonCallbackRegistered) { PhotonNetwork.RemoveCallbackTarget((object)this); _photonCallbackRegistered = false; } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { _affectPrefab = null; _searched = false; _active.Clear(); _nextBroadcast = 0f; _nextPrefabScan = Time.time + 1.5f; Plugin.Log.LogInfo((object)("[FloatDiag] scene '" + ((Scene)(ref scene)).name + "' loaded -> reset prefab + per-player state")); } private void Update() { try { bool flag = FloatGate.ShouldRunFloatWork(Plugin.Enabled.Value, ShouldFloat(), LevelFullyLoaded()); GameObject val = (flag ? GetAffectPrefab() : null); GameDirector instance = GameDirector.instance; if (!flag || (Object)(object)val == (Object)null || (Object)(object)instance == (Object)null) { UnregisterPhotonCallback(); if (_active.Count > 0) { DestroyAllEffects(); } return; } if (FloatGate.PhotonCallbackDesired(flag, SemiFunc.IsMultiplayer())) { RegisterPhotonCallback(); } MaintainEffects(); SuppressClimbWhileFloating(); if (SemiFunc.IsMasterClientOrSingleplayer()) { CompensateGrabStrength(instance); } if (!SemiFunc.IsMultiplayer()) { SpawnMissingLocal(val, instance); } else if (SemiFunc.IsMasterClientOrSingleplayer()) { float time = Time.time; _nextBroadcast = FloatGate.NextBroadcastTime(_nextBroadcast, time, AnyEligiblePlayerMissingEffect(instance), 0.25f); if (time >= _nextBroadcast) { _nextBroadcast = time + 4f; BroadcastRoster(instance); } } MaybeLog(instance); } catch (Exception arg) { Plugin.Log.LogError((object)$"FloatDriver.Update: {arg}"); } } private void MaintainEffects() { if (_active.Count == 0) { return; } List list = null; foreach (KeyValuePair item in _active) { PlayerAvatar key = item.Key; SemiAffect value = item.Value; if ((Object)(object)key == (Object)null || (Object)(object)value == (Object)null || !IsAlive(key)) { if ((Object)(object)value != (Object)null) { Object.Destroy((Object)(object)((Component)value).gameObject); } (list ?? (list = new List())).Add(item.Key); } else { AffectTimerRef.Invoke(value) = AffectTimerTotalRef.Invoke(value); } } if (list == null) { return; } foreach (PlayerAvatar item2 in list) { _active.Remove(item2); } } private void SuppressClimbWhileFloating() { foreach (KeyValuePair item in _active) { PlayerAvatar key = item.Key; if (!((Object)(object)key == (Object)null) && !((Object)(object)item.Value == (Object)null) && AvatarIsLocalRef.Invoke(key)) { if (FloatGate.ShouldSuppressClimb(Plugin.DisableWallClimbWhileFloating.Value, localEffectActive: true) && (Object)(object)key.physGrabber != (Object)null) { key.physGrabber.OverrideDisableSpecialGrabPowers(0.5f); } break; } } } private bool AnyEligiblePlayerMissingEffect(GameDirector director) { List playerList = director.PlayerList; for (int i = 0; i < playerList.Count; i++) { PlayerAvatar val = playerList[i]; if (!((Object)(object)val == (Object)null) && IsAlive(val)) { PlayerTumble val2 = AvatarTumbleRef.Invoke(val); if (!((Object)(object)val2 == (Object)null) && !((Object)(object)TumblePhysGrabObjectRef.Invoke(val2) == (Object)null) && (!_active.TryGetValue(val, out SemiAffect value) || (Object)(object)value == (Object)null)) { return true; } } } return false; } private void DestroyAllEffects() { foreach (KeyValuePair item in _active) { if ((Object)(object)item.Value != (Object)null) { Object.Destroy((Object)(object)((Component)item.Value).gameObject); } } _active.Clear(); } private void CompensateGrabStrength(GameDirector director) { if (!Plugin.RestoreGrabUpgrades.Value) { return; } List playerList = director.PlayerList; for (int i = 0; i < playerList.Count; i++) { PlayerAvatar val = playerList[i]; if ((Object)(object)val == (Object)null || (Object)(object)val.physGrabber == (Object)null) { continue; } PhysGrabObject val2 = GrabbedPhysGrabObjectRef.Invoke(val.physGrabber); if (!((Object)(object)val2 == (Object)null)) { SemiAffect value; float? num = FloatGate.CompensatedGrabStrength(_active.TryGetValue(val, out value) && (Object)(object)value != (Object)null, (Object)(object)((Component)val2).GetComponent() != (Object)null, val.physGrabber.grabStrength, Plugin.StrengthSoftening.Value); if (num.HasValue) { float valueOrDefault = num.GetValueOrDefault(); val2.OverrideGrabStrength(valueOrDefault, 0.2f); } } } } private void SpawnMissingLocal(GameObject prefab, GameDirector director) { //IL_0096: 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_00c5: 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) //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) List playerList = director.PlayerList; for (int i = 0; i < playerList.Count; i++) { PlayerAvatar val = playerList[i]; if ((Object)(object)val == (Object)null) { continue; } PlayerTumble val2 = AvatarTumbleRef.Invoke(val); PhysGrabObject val3 = (((Object)(object)val2 != (Object)null) ? TumblePhysGrabObjectRef.Invoke(val2) : null); if (FloatGate.CanSpawnEffect(Plugin.Enabled.Value, ShouldFloat(), IsAlive(val), (Object)(object)val2 != (Object)null, (Object)(object)val3 != (Object)null, _active.TryGetValue(val, out SemiAffect value) && (Object)(object)value != (Object)null)) { GameObject val4 = Object.Instantiate(prefab, ((Component)val).transform.position, Quaternion.identity); SemiAffect component = val4.GetComponent(); if ((Object)(object)component == (Object)null) { Object.Destroy((Object)(object)val4); continue; } component.direction = Vector3.up; component.positionOfOriginalAreaOfEffect = ((Component)val).transform.position; component.SetupSingleplayer(((Component)val).transform, val3, 5f, val); _active[val] = component; } } } private void BroadcastRoster(GameDirector director) { //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_007c: 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_008b: Expected O, but got Unknown if (!PhotonNetwork.InRoom) { return; } List playerList = director.PlayerList; List list = new List(playerList.Count); for (int i = 0; i < playerList.Count; i++) { PlayerAvatar val = playerList[i]; if (!((Object)(object)val == (Object)null) && IsAlive(val) && !((Object)(object)val.photonView == (Object)null)) { list.Add(val.photonView.ViewID); } } if (list.Count != 0) { PhotonNetwork.RaiseEvent((byte)117, (object)list.ToArray(), new RaiseEventOptions { Receivers = (ReceiverGroup)1 }, SendOptions.SendReliable); } } public void OnEvent(EventData photonEvent) { //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_014a: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) if (photonEvent.Code != 117) { return; } try { if (PhotonNetwork.MasterClient == null || photonEvent.Sender != PhotonNetwork.MasterClient.ActorNumber || !(photonEvent.CustomData is int[] array) || !FloatGate.ShouldRunFloatWork(Plugin.Enabled.Value, ShouldFloat(), LevelFullyLoaded())) { return; } bool value = Plugin.Enabled.Value; bool inFloatableLevel = ShouldFloat(); GameObject affectPrefab = GetAffectPrefab(); if ((Object)(object)affectPrefab == (Object)null) { return; } int[] array2 = array; foreach (int num in array2) { PlayerAvatar val = SemiFunc.PlayerAvatarGetFromPhotonID(num); if ((Object)(object)val == (Object)null) { continue; } PlayerTumble val2 = AvatarTumbleRef.Invoke(val); PhysGrabObject val3 = (((Object)(object)val2 != (Object)null) ? TumblePhysGrabObjectRef.Invoke(val2) : null); if (FloatGate.CanSpawnEffect(value, inFloatableLevel, IsAlive(val), (Object)(object)val2 != (Object)null, (Object)(object)val3 != (Object)null, _active.TryGetValue(val, out SemiAffect value2) && (Object)(object)value2 != (Object)null)) { GameObject val4 = Object.Instantiate(affectPrefab, ((Component)val).transform.position, Quaternion.identity); SemiAffect component = val4.GetComponent(); if ((Object)(object)component == (Object)null) { Object.Destroy((Object)(object)val4); continue; } component.direction = Vector3.up; component.positionOfOriginalAreaOfEffect = ((Component)val).transform.position; component.Setup(num, 5f); _active[val] = component; } } } catch (Exception arg) { Plugin.Log.LogError((object)$"FloatDriver.OnEvent: {arg}"); } } private GameObject? GetAffectPrefab() { if ((Object)(object)_affectPrefab != (Object)null) { return _affectPrefab; } if (!FloatGate.ShouldScanPrefab((Object)(object)_affectPrefab != (Object)null, Time.time, _nextPrefabScan)) { return null; } _nextPrefabScan = Time.time + 2f; StatsManager instance = StatsManager.instance; if ((Object)(object)instance == (Object)null || instance.itemDictionary == null) { return null; } GameObject val = null; int num = 0; Item value; bool flag = instance.itemDictionary.TryGetValue("Item Staff Zero Gravity", out value) && (Object)(object)(val = TryResolveAffect(value)) != (Object)null; if ((Object)(object)val == (Object)null) { foreach (Item value2 in instance.itemDictionary.Values) { if (!((Object)(object)value2 == (Object)null) && value2.prefab != null && (value2.itemName ?? "").ToLowerInvariant().Contains("gravity")) { num++; val = TryResolveAffect(value2); if ((Object)(object)val != (Object)null) { break; } } } } if (!_searched || (Object)(object)val != (Object)null) { _searched = true; string text = (flag ? "via staff key" : $"via item DB: gravityItems={num}"); Plugin.Log.LogInfo((object)("[FloatDiag] prefab " + text + " -> " + (((Object)(object)val != (Object)null) ? "FOUND" : "not yet"))); } _affectPrefab = val; return _affectPrefab; } private static PrefabRef? ExtractProjectileRef(GameObject itemGO) { ItemStaffZeroGravity component = itemGO.GetComponent(); if (!((Object)(object)component != (Object)null)) { return null; } return component.projectilePrefab; } private static GameObject? TryResolveAffect(Item? item) { if ((Object)(object)item == (Object)null || item.prefab == null) { return null; } GameObject prefab; try { prefab = ((PrefabRef)(object)item.prefab).Prefab; } catch { return null; } if ((Object)(object)prefab == (Object)null) { return null; } PrefabRef val = ExtractProjectileRef(prefab); if (val == null) { return null; } GameObject prefab2; try { prefab2 = ((PrefabRef)(object)val).Prefab; } catch { return null; } if ((Object)(object)prefab2 == (Object)null) { return null; } SemiAreaOfEffect componentInChildren = prefab2.GetComponentInChildren(true); if ((Object)(object)componentInChildren == (Object)null || componentInChildren.semiAffectPrefab == null) { return null; } GameObject prefab3; try { prefab3 = ((PrefabRef)(object)componentInChildren.semiAffectPrefab).Prefab; } catch { return null; } if (!((Object)(object)prefab3 != (Object)null) || !((Object)(object)prefab3.GetComponent() != (Object)null)) { return null; } return prefab3; } private void MaybeLog(GameDirector director) { _logAccum += Time.deltaTime; if (_logAccum < 1f) { return; } _logAccum = 0f; bool flag = SemiFunc.IsMasterClientOrSingleplayer(); List playerList = director.PlayerList; for (int i = 0; i < playerList.Count; i++) { PlayerAvatar val = playerList[i]; if (!((Object)(object)val == (Object)null)) { SemiAffect value; bool flag2 = _active.TryGetValue(val, out value) && (Object)(object)value != (Object)null; Plugin.Log.LogInfo((object)($"[FloatDiag] master={flag} p{i} local={AvatarIsLocalRef.Invoke(val)} " + $"alive={IsAlive(val)} tumbling={AvatarIsTumblingRef.Invoke(val)} effect={flag2}")); } } } } public static class FloatGate { public static bool CanSpawnEffect(bool modEnabled, bool inFloatableLevel, bool avatarAlive, bool tumbleLinked, bool physGrabObjectLinked, bool effectAlreadyActive) { if (modEnabled && inFloatableLevel && avatarAlive && tumbleLinked && physGrabObjectLinked) { return !effectAlreadyActive; } return false; } public static bool ShouldScanPrefab(bool prefabCached, float now, float nextScanAllowedAt) { if (!prefabCached) { return now >= nextScanAllowedAt; } return false; } public static bool ShouldRunFloatWork(bool modEnabled, bool inFloatableLevel, bool levelFullyLoaded) { return modEnabled && inFloatableLevel && levelFullyLoaded; } public static bool PhotonCallbackDesired(bool floatWorkActive, bool isMultiplayer) { return floatWorkActive && isMultiplayer; } public static bool ShouldSuppressClimb(bool disableClimbWhileFloating, bool localEffectActive) { return disableClimbWhileFloating && localEffectActive; } public static float NextBroadcastTime(float scheduledAt, float now, bool eligiblePlayerMissingEffect, float missingEffectDelay) { if (!eligiblePlayerMissingEffect) { return scheduledAt; } float num = now + missingEffectDelay; if (!(num < scheduledAt)) { return scheduledAt; } return num; } public static float? CompensatedGrabStrength(bool effectActive, bool grabbedIsEnemy, float upgradedStrength, float softening) { if (!effectActive || grabbedIsEnemy) { return null; } if (upgradedStrength <= 1f) { return null; } float num = ((softening < 0f) ? 0f : ((softening > 1f) ? 1f : softening)); float num2 = (float)Math.Sqrt(upgradedStrength); return upgradedStrength + (num2 - upgradedStrength) * num; } } [BepInPlugin("darkharasho.FaerieFlight", "FaerieFlight", "0.2.6")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; internal static ConfigEntry Enabled; internal static ConfigEntry Flashlight; internal static ConfigEntry RestoreGrabUpgrades; internal static ConfigEntry StrengthSoftening; internal static ConfigEntry DisableWallClimbWhileFloating; private void Awake() { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown //IL_00cd: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Master on/off switch for permanent floating. Best set on the host; the host drives the float for every player. (All players should run the mod.)"); Flashlight = ((BaseUnityPlugin)this).Config.Bind("General", "Flashlight", true, "Keep your flashlight on while floating. The game normally turns it off during tumble; since it's a dark game, this keeps it lit."); RestoreGrabUpgrades = ((BaseUnityPlugin)this).Config.Bind("General", "RestoreGrabUpgrades", true, "The game ignores Strength upgrades while a player is tumbling, so permanent float would leave them permanently dead. Restores them (softened) while floating. Host-side setting; grabbing enemies keeps vanilla behavior either way."); StrengthSoftening = ((BaseUnityPlugin)this).Config.Bind("General", "StrengthSoftening", 0.65f, new ConfigDescription("How much to soften restored Strength upgrades while floating: 0 = full upgrade strength, 1 = square-root curve. Floating grabbers take the grab's recoil, so full strength can fling you.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); DisableWallClimbWhileFloating = ((BaseUnityPlugin)this).Config.Bind("General", "DisableWallClimbWhileFloating", true, "The Tumble Climb upgrade turns any wall grab into a climb; while floating, the climb spring yanks you out of the air to the grab point (feels like being dragged down to the ground). Suppresses climbing while you float — wall grabs show the game's 'disabled' zap instead. Per-player setting; turn off to keep wall-climbing."); new Harmony("darkharasho.FaerieFlight").PatchAll(); ((Component)this).gameObject.AddComponent(); Log.LogInfo((object)"FaerieFlight v0.2.6 loaded."); } } public static class PluginInfo { public const string PLUGIN_GUID = "darkharasho.FaerieFlight"; public const string PLUGIN_NAME = "FaerieFlight"; public const string PLUGIN_VERSION = "0.2.6"; } }