using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Networking;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("0.0.0.0")]
namespace Pix.FatalFall;
[BepInPlugin("Pix.FatalFall", "Fatal Fall", "0.5.10")]
public sealed class FatalFallPlugin : BaseUnityPlugin
{
[CompilerGenerated]
private sealed class <ClientEnsureSeRoutine>d__47 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public FatalFallPlugin <>4__this;
private int <i>5__1;
private ObjectDB <odb>5__2;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <ClientEnsureSeRoutine>d__47(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<odb>5__2 = null;
<>1__state = -2;
}
private bool MoveNext()
{
//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
//IL_00bf: Expected O, but got Unknown
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<i>5__1 = 0;
break;
case 1:
<>1__state = -1;
<i>5__1++;
break;
}
if (<i>5__1 < 20)
{
try
{
<odb>5__2 = ObjectDB.instance;
if ((Object)(object)<odb>5__2 != (Object)null)
{
EnsureBleedIconLoaded((Object)(object)BleedIconSprite == (Object)null);
Patches.EnsureBleedRegistered(<odb>5__2, forceIconOverwrite: true);
if ((Object)(object)BleedPrefab != (Object)null && (Object)(object)BleedPrefab.m_icon != (Object)null)
{
Dbg("ClientEnsureSeRoutine: Bleed SE ready (prefab+icon).");
return false;
}
}
<odb>5__2 = null;
}
catch
{
}
<>2__current = (object)new WaitForSeconds(1f);
<>1__state = 1;
return true;
}
Dbg("ClientEnsureSeRoutine: Bleed SE not confirmed ready after retries (prefab/icon missing).");
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
[CompilerGenerated]
private sealed class <LoadHeartbeatOgg>d__43 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public FatalFallPlugin <>4__this;
private string <path>5__1;
private string <uri>5__2;
private UnityWebRequest <req>5__3;
private string <dllDir>5__4;
private Exception <e>5__5;
private Exception <e>5__6;
private AudioClip <clip>5__7;
private Exception <e>5__8;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <LoadHeartbeatOgg>d__43(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<path>5__1 = null;
<uri>5__2 = null;
<req>5__3 = null;
<dllDir>5__4 = null;
<e>5__5 = null;
<e>5__6 = null;
<clip>5__7 = null;
<e>5__8 = null;
<>1__state = -2;
}
private bool MoveNext()
{
//IL_0170: Unknown result type (might be due to invalid IL or missing references)
//IL_0176: Invalid comparison between Unknown and I4
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<path>5__1 = null;
<uri>5__2 = null;
try
{
<dllDir>5__4 = Path.GetDirectoryName(((BaseUnityPlugin)<>4__this).Info.Location);
<path>5__1 = Path.Combine(<dllDir>5__4 ?? ".", _soundFileName.Value ?? "heartbeat.ogg");
if (!File.Exists(<path>5__1))
{
Dbg("Sound file not found: " + <path>5__1);
return false;
}
<uri>5__2 = new Uri(<path>5__1).AbsoluteUri;
<dllDir>5__4 = null;
}
catch (Exception ex)
{
<e>5__5 = ex;
Dbg("OGG path exception: " + <e>5__5.GetType().Name);
return false;
}
<req>5__3 = null;
try
{
<req>5__3 = UnityWebRequestMultimedia.GetAudioClip(<uri>5__2, (AudioType)14);
}
catch (Exception ex)
{
<e>5__6 = ex;
Dbg("OGG request create exception: " + <e>5__6.GetType().Name);
return false;
}
<>2__current = <req>5__3.SendWebRequest();
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
try
{
if ((int)<req>5__3.result != 1)
{
Dbg("OGG load failed: " + <req>5__3.error);
return false;
}
<clip>5__7 = DownloadHandlerAudioClip.GetContent(<req>5__3);
if ((Object)(object)<clip>5__7 == (Object)null)
{
Dbg("OGG load failed: clip was null.");
return false;
}
HeartbeatClip = <clip>5__7;
Dbg($"OGG loaded len={<clip>5__7.length:0.00}s from={Path.GetFileName(<path>5__1)}");
<clip>5__7 = null;
}
catch (Exception ex)
{
<e>5__8 = ex;
Dbg("OGG parse exception: " + <e>5__8.GetType().Name);
}
finally
{
<req>5__3.Dispose();
}
return false;
}
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
public const string PluginGuid = "Pix.FatalFall";
public const string PluginName = "Fatal Fall";
public const string PluginVersion = "0.5.10";
internal static FatalFallPlugin Instance;
internal static ManualLogSource LogSrc;
private static ConfigEntry<bool> _enabled;
private static ConfigEntry<float> _fallMultiplier;
private static ConfigEntry<bool> _borderlineEnabled;
private static ConfigEntry<float> _borderlineWindowHp;
private static ConfigEntry<float> _leaveHp;
private static ConfigEntry<float> _maxOverkillToSave;
private static ConfigEntry<bool> _borderlineAllDamage;
private static ConfigEntry<float> _borderlineCooldownSeconds;
private static ConfigEntry<bool> _borderlineSkipIfBubble;
private static ConfigEntry<bool> _bleedEnabled;
private static ConfigEntry<float> _bleedDuration;
private static ConfigEntry<float> _bleedDps;
private static ConfigEntry<float> _bleedMinHp;
private static ConfigEntry<bool> _bleedSkipIfBubble;
private static readonly int[] _bubbleSeHashes = new int[5]
{
StableHash.GetStableHashCode("SE_Shield"),
StableHash.GetStableHashCode("SE_StaffShield"),
StableHash.GetStableHashCode("SE_StaffShield_Bubble"),
StableHash.GetStableHashCode("SE_StaffShield_Protection"),
StableHash.GetStableHashCode("SE_StaffShieldProtection")
};
private static ConfigEntry<bool> _popupEnabled;
private static ConfigEntry<string> _popupLines;
private static ConfigEntry<string> _popupLinesGeneric;
private static ConfigEntry<bool> _soundEnabled;
private static ConfigEntry<string> _soundFileName;
private static ConfigEntry<bool> _staggerEnabled;
private static ConfigEntry<float> _staggerStrength;
private static ConfigEntry<bool> _debug;
private static ConfigEntry<bool> _uncapEnabled;
private static ConfigEntry<float> _uncapStartHeight;
private static ConfigEntry<float> _uncapDamagePerMeter;
private static ConfigEntry<float> _uncapMaxExtraDamage;
private static ConfigEntry<float> _uncapRequireRecentFallHitWindow;
internal const string BleedSeName = "SE_PixFatalFall_Bleed";
internal static readonly int BleedSeHash = StableHash.GetStableHashCode("SE_PixFatalFall_Bleed");
internal static Sprite BleedIconSprite;
internal static AudioClip HeartbeatClip;
internal static StatusEffect BleedPrefab;
private static GameObject _audioGo;
private static AudioSource _audioSource;
private Harmony _harmony;
private void Awake()
{
//IL_03f9: Unknown result type (might be due to invalid IL or missing references)
//IL_0403: Expected O, but got Unknown
Instance = this;
LogSrc = ((BaseUnityPlugin)this).Logger;
_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable the mod.");
_fallMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("General", "FallDamageMultiplier", 1f, "Multiplier applied to fall damage before borderline logic.");
_debug = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DebugLogging", false, "Enable detailed logs.");
_borderlineEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Borderline", "Enabled", true, "Enable borderline survival behavior.");
_borderlineWindowHp = ((BaseUnityPlugin)this).Config.Bind<float>("Borderline", "BorderlineWindowHp", 13f, "If (incomingDamage - currentHP) <= this window, the mod may save you.");
_leaveHp = ((BaseUnityPlugin)this).Config.Bind<float>("Borderline", "LeaveHp", 1f, "HP to leave the character at when saved.");
_maxOverkillToSave = ((BaseUnityPlugin)this).Config.Bind<float>("Borderline", "MaxOverkillToSave", 50f, "If (incomingDamage - currentHP) > this, do NOT save (die normally).");
_borderlineAllDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Borderline", "ApplyToAllDamage", true, "If true, borderline save can trigger on any near-fatal hit (mobs, DoT, etc). If false, only fall damage can trigger it.");
_borderlineCooldownSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Borderline", "CooldownSeconds", 60f, "Borderline cannot trigger again for this many seconds after a save (prevents chain-proc cheese).");
_borderlineSkipIfBubble = ((BaseUnityPlugin)this).Config.Bind<bool>("Borderline", "SkipIfStaffBubbleActive", true, "If true, borderline save logic will not arm while a Staff of Protection bubble/shield status is active. Prevents false procs caused by raw damage being evaluated before bubble absorption.");
_popupEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "PopupEnabled", true, "Show a center-screen message on borderline saves.");
_popupLines = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "PopupLines_Fall", "That fall was nearly fatal.|Your bones shake in despair.|You barely survive.|The ground almost claims you.", "Pipe-separated popup lines for FALL borderline saves.");
_popupLinesGeneric = ((BaseUnityPlugin)this).Config.Bind<string>("UI", "PopupLines_Generic", "That was nearly fatal.|You barely survive.|Death brushes past you.|You stagger but stand.", "Pipe-separated popup lines for NON-FALL borderline saves.");
_soundEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Sound", "Enabled", true, "Play a sound on borderline saves.");
_soundFileName = ((BaseUnityPlugin)this).Config.Bind<string>("Sound", "OggFileName", "heartbeat.ogg", "OGG filename placed next to the plugin DLL.");
_bleedEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Bleed", "Enabled", true, "Apply a bleed status effect after borderline saves.");
_bleedDuration = ((BaseUnityPlugin)this).Config.Bind<float>("Bleed", "DurationSeconds", 45f, "Bleed duration in seconds.");
_bleedDps = ((BaseUnityPlugin)this).Config.Bind<float>("Bleed", "Dps", 1.25f, "Bleed damage per second.");
_bleedMinHp = ((BaseUnityPlugin)this).Config.Bind<float>("Bleed", "MinHp", 1f, "Bleed will not reduce the character below this HP.");
_bleedSkipIfBubble = ((BaseUnityPlugin)this).Config.Bind<bool>("Bleed", "SkipIfStaffBubbleActive", true, "If true, bleed will not be applied while a Staff of Protection bubble/shield status is active (best-effort detection).");
_staggerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Stagger", "Enabled", true, "Apply a stagger on borderline saves.");
_staggerStrength = ((BaseUnityPlugin)this).Config.Bind<float>("Stagger", "Strength", 1f, "Stagger strength scalar.");
_uncapEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Uncap", "Enabled", false, "If true, allows falls to exceed vanilla's effective cap by adding extra landing damage on touchdown (safe, server-authoritative).");
_uncapStartHeight = ((BaseUnityPlugin)this).Config.Bind<float>("Uncap", "StartHeightMeters", 16f, "Extra damage starts after this many meters of fall height (approx).");
_uncapDamagePerMeter = ((BaseUnityPlugin)this).Config.Bind<float>("Uncap", "ExtraDamagePerMeter", 20f, "Extra damage added per meter beyond StartHeightMeters.");
_uncapMaxExtraDamage = ((BaseUnityPlugin)this).Config.Bind<float>("Uncap", "MaxExtraDamage", 500f, "Hard clamp for added extra landing damage to prevent runaway values.");
_uncapRequireRecentFallHitWindow = ((BaseUnityPlugin)this).Config.Bind<float>("Uncap", "RequireRecentFallHitSeconds", 0.35f, "Safety: extra landing damage only applies if we observed a real fall Hit within this many seconds.");
DamageTypesScaler.Init();
HitIntrospection.Init();
EnsureBleedIconLoaded(forceReload: false);
if (!Application.isBatchMode)
{
EnsureAudioSource();
((MonoBehaviour)this).StartCoroutine(LoadHeartbeatOgg());
((MonoBehaviour)this).StartCoroutine(ClientEnsureSeRoutine());
}
_harmony = new Harmony("Pix.FatalFall");
_harmony.PatchAll(typeof(Patches));
_harmony.PatchAll(typeof(FallTrackerPatches));
Dbg(string.Format("Boot {0} | BleedSE='{1}' hash={2}", "0.5.10", "SE_PixFatalFall_Bleed", BleedSeHash));
}
private static void EnsureAudioSource()
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_0021: Expected O, but got Unknown
if (!((Object)(object)_audioSource != (Object)null))
{
_audioGo = new GameObject("PixFatalFallAudio");
Object.DontDestroyOnLoad((Object)(object)_audioGo);
_audioSource = _audioGo.AddComponent<AudioSource>();
_audioSource.playOnAwake = false;
_audioSource.loop = false;
_audioSource.spatialBlend = 0f;
_audioSource.volume = 1f;
}
}
[IteratorStateMachine(typeof(<LoadHeartbeatOgg>d__43))]
private IEnumerator LoadHeartbeatOgg()
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <LoadHeartbeatOgg>d__43(0)
{
<>4__this = this
};
}
private static string GetDllDirSafe()
{
try
{
if ((Object)(object)Instance != (Object)null && ((BaseUnityPlugin)Instance).Info != null && !string.IsNullOrEmpty(((BaseUnityPlugin)Instance).Info.Location))
{
return Path.GetDirectoryName(((BaseUnityPlugin)Instance).Info.Location);
}
}
catch
{
}
try
{
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
catch
{
return null;
}
}
internal static void EnsureBleedIconLoaded(bool forceReload)
{
//IL_005c: Unknown result type (might be due to invalid IL or missing references)
//IL_0063: Expected O, but got Unknown
//IL_008f: 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)
if (!forceReload && (Object)(object)BleedIconSprite != (Object)null)
{
return;
}
try
{
string dllDirSafe = GetDllDirSafe();
if (!string.IsNullOrEmpty(dllDirSafe))
{
string text = Path.Combine(dllDirSafe, "bleed.png");
if (File.Exists(text))
{
byte[] array = File.ReadAllBytes(text);
Texture2D val = new Texture2D(2, 2, (TextureFormat)4, false);
if (ImageConversion.LoadImage(val, array))
{
BleedIconSprite = Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f));
Dbg($"bleed.png loaded OK ({((Texture)val).width}x{((Texture)val).height}) from='{dllDirSafe}'");
return;
}
Dbg("bleed.png exists but failed to decode from='" + dllDirSafe + "'");
}
else
{
Dbg("bleed.png not found next to dll: '" + text + "'");
}
}
}
catch (Exception ex)
{
Dbg("bleed.png load exception: " + ex.GetType().Name);
}
BleedIconSprite = TryGetFallbackIcon();
}
private static Sprite TryGetFallbackIcon()
{
try
{
ObjectDB instance = ObjectDB.instance;
if ((Object)(object)instance == (Object)null)
{
return null;
}
StatusEffect statusEffect = instance.GetStatusEffect(StableHash.GetStableHashCode("Wet"));
if ((Object)(object)statusEffect != (Object)null && (Object)(object)statusEffect.m_icon != (Object)null)
{
return statusEffect.m_icon;
}
StatusEffect statusEffect2 = instance.GetStatusEffect(StableHash.GetStableHashCode("SE_FeatherFall"));
if ((Object)(object)statusEffect2 != (Object)null && (Object)(object)statusEffect2.m_icon != (Object)null)
{
return statusEffect2.m_icon;
}
}
catch
{
}
return null;
}
[IteratorStateMachine(typeof(<ClientEnsureSeRoutine>d__47))]
private IEnumerator ClientEnsureSeRoutine()
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <ClientEnsureSeRoutine>d__47(0)
{
<>4__this = this
};
}
internal static bool DebugEnabled()
{
try
{
return _debug != null && _debug.Value;
}
catch
{
return false;
}
}
internal static void Dbg(string msg)
{
if (DebugEnabled() && LogSrc != null)
{
LogSrc.LogInfo((object)("[Pix.FatalFall] " + msg));
}
}
internal static float Now()
{
try
{
if ((Object)(object)ZNet.instance != (Object)null)
{
return (float)ZNet.instance.GetTimeSeconds();
}
}
catch
{
}
return Time.realtimeSinceStartup;
}
internal static bool IsEnabled()
{
return _enabled.Value;
}
internal static float GetMul()
{
return Mathf.Max(0f, _fallMultiplier.Value);
}
internal static bool BorderlineEnabled()
{
return _borderlineEnabled.Value;
}
internal static bool BorderlineAllDamage()
{
return _borderlineAllDamage.Value;
}
internal static float BorderlineWindowHp()
{
return Mathf.Max(0f, _borderlineWindowHp.Value);
}
internal static float LeaveHp()
{
return Mathf.Clamp(_leaveHp.Value, 0f, 9999f);
}
internal static float MaxOverkillToSave()
{
return Mathf.Max(0f, _maxOverkillToSave.Value);
}
internal static float BorderlineCooldownSeconds()
{
return Mathf.Clamp(_borderlineCooldownSeconds.Value, 0f, 9999f);
}
internal static bool BorderlineSkipIfBubble()
{
return _borderlineSkipIfBubble != null && _borderlineSkipIfBubble.Value;
}
internal static bool PopupEnabled()
{
return _popupEnabled.Value;
}
internal static string[] PopupChoicesFall()
{
string text = _popupLines.Value ?? "";
string[] array = text.Split(new char[1] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (array.Length == 0)
{
return new string[1] { "That fall was nearly fatal." };
}
return array;
}
internal static string[] PopupChoicesGeneric()
{
string text = _popupLinesGeneric.Value ?? "";
string[] array = text.Split(new char[1] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (array.Length == 0)
{
return new string[1] { "That was nearly fatal." };
}
return array;
}
internal static bool SoundEnabled()
{
return _soundEnabled.Value;
}
internal static bool BleedEnabled()
{
return _bleedEnabled.Value;
}
internal static float BleedDuration()
{
return Mathf.Clamp(_bleedDuration.Value, 0f, 9999f);
}
internal static float BleedDps()
{
return Mathf.Clamp(_bleedDps.Value, 0f, 9999f);
}
internal static float BleedMinHp()
{
return Mathf.Clamp(_bleedMinHp.Value, 0f, 9999f);
}
internal static bool BleedSkipIfBubble()
{
return _bleedSkipIfBubble.Value;
}
internal static bool StaggerEnabled()
{
return _staggerEnabled.Value;
}
internal static float StaggerStrength()
{
return Mathf.Clamp(_staggerStrength.Value, 0f, 10f);
}
internal static bool UncapEnabled()
{
return _uncapEnabled.Value;
}
internal static float UncapStartHeight()
{
return Mathf.Clamp(_uncapStartHeight.Value, 0f, 9999f);
}
internal static float UncapDamagePerMeter()
{
return Mathf.Clamp(_uncapDamagePerMeter.Value, 0f, 9999f);
}
internal static float UncapMaxExtraDamage()
{
return Mathf.Clamp(_uncapMaxExtraDamage.Value, 0f, 99999f);
}
internal static float UncapRequireRecentFallHitWindow()
{
return Mathf.Clamp(_uncapRequireRecentFallHitWindow.Value, 0.05f, 5f);
}
internal static void PlayHeartbeat()
{
if (!Application.isBatchMode && SoundEnabled() && !((Object)(object)HeartbeatClip == (Object)null))
{
if ((Object)(object)_audioSource == (Object)null)
{
EnsureAudioSource();
}
if (!((Object)(object)_audioSource == (Object)null))
{
_audioSource.PlayOneShot(HeartbeatClip);
}
}
}
internal static bool HasStaffBubble(Character ch)
{
if ((Object)(object)ch == (Object)null)
{
return false;
}
try
{
SEMan sEMan = ch.GetSEMan();
if (sEMan == null)
{
return false;
}
for (int i = 0; i < _bubbleSeHashes.Length; i++)
{
if (sEMan.HaveStatusEffect(_bubbleSeHashes[i]))
{
return true;
}
}
}
catch
{
}
return false;
}
}
internal static class StableHash
{
public static int GetStableHashCode(string str)
{
int num = 5381;
int num2 = num;
for (int i = 0; i < str.Length && str[i] != 0; i += 2)
{
num = ((num << 5) + num) ^ str[i];
if (i == str.Length - 1 || str[i + 1] == '\0')
{
break;
}
num2 = ((num2 << 5) + num2) ^ str[i + 1];
}
return num + num2 * 1566083941;
}
}
internal static class HitIntrospection
{
private static bool _inited;
private static FieldInfo _fallField;
private static FieldInfo _hitTypeField;
public static void Init()
{
if (!_inited)
{
_inited = true;
try
{
_fallField = typeof(DamageTypes).GetField("m_fall", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
catch
{
_fallField = null;
}
try
{
_hitTypeField = typeof(HitData).GetField("m_hitType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
catch
{
_hitTypeField = null;
}
FatalFallPlugin.Dbg($"Introspection fallFieldFound={_fallField != null} hitTypeFieldFound={_hitTypeField != null}");
}
}
public static bool IsFallHit(HitData hit)
{
if (hit == null)
{
return false;
}
float fallDamage = GetFallDamage(hit);
if (fallDamage > 0.0001f)
{
return true;
}
try
{
if (_hitTypeField != null)
{
object value = _hitTypeField.GetValue(hit);
if (value != null)
{
string text = value.ToString();
if (!string.IsNullOrEmpty(text) && text.IndexOf("Fall", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
}
}
catch
{
}
return false;
}
public static float GetFallDamage(HitData hit)
{
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
if (hit == null)
{
return 0f;
}
try
{
if (_fallField != null)
{
object value = _fallField.GetValue(hit.m_damage);
if (value is float)
{
float result = (float)value;
if (true)
{
return result;
}
}
}
}
catch
{
}
return 0f;
}
public static void SetFallDamage(ref HitData hit, float value)
{
//IL_0020: 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_0040: 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)
if (hit == null)
{
return;
}
try
{
if (_fallField != null)
{
DamageTypes damage = hit.m_damage;
_fallField.SetValueDirect(__makeref(damage), value);
hit.m_damage = damage;
}
}
catch
{
}
}
public static float SumAllDamage(HitData hit)
{
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
if (hit == null)
{
return 0f;
}
return DamageTypesScaler.Sum(hit.m_damage);
}
}
internal static class DamageTypesScaler
{
private static FieldInfo[] _floatFields;
private static bool _inited;
public static void Init()
{
if (_inited)
{
return;
}
_inited = true;
try
{
Type typeFromHandle = typeof(DamageTypes);
FieldInfo[] fields = typeFromHandle.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
List<FieldInfo> list = new List<FieldInfo>();
FieldInfo[] array = fields;
foreach (FieldInfo fieldInfo in array)
{
if (fieldInfo.FieldType == typeof(float))
{
list.Add(fieldInfo);
}
}
_floatFields = list.ToArray();
FatalFallPlugin.Dbg($"DamageTypesScaler floatFields={_floatFields.Length}");
}
catch
{
_floatFields = Array.Empty<FieldInfo>();
}
}
public static void Scale(ref DamageTypes dmg, float factor)
{
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
if (_floatFields == null || _floatFields.Length == 0)
{
return;
}
for (int i = 0; i < _floatFields.Length; i++)
{
FieldInfo fieldInfo = _floatFields[i];
float num = (float)fieldInfo.GetValue(dmg);
if (Math.Abs(num) > 0.0001f)
{
fieldInfo.SetValueDirect(__makeref(dmg), num * factor);
}
}
}
public static float Sum(DamageTypes dmg)
{
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
float num = 0f;
if (_floatFields == null || _floatFields.Length == 0)
{
return num;
}
for (int i = 0; i < _floatFields.Length; i++)
{
try
{
FieldInfo fieldInfo = _floatFields[i];
num += (float)fieldInfo.GetValue(dmg);
}
catch
{
}
}
return num;
}
}
public sealed class SE_PixFatalFall_Bleed : StatusEffect
{
private float _tick;
private float _dps;
private float _minHp;
public override void Setup(Character character)
{
((StatusEffect)this).Setup(character);
_tick = 0f;
_dps = FatalFallPlugin.BleedDps();
_minHp = FatalFallPlugin.BleedMinHp();
}
public override void UpdateStatusEffect(float dt)
{
//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
//IL_00b4: Expected O, but got Unknown
//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_00cd: Unknown result type (might be due to invalid IL or missing references)
//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
((StatusEffect)this).UpdateStatusEffect(dt);
if ((Object)(object)base.m_character == (Object)null || _dps <= 0f)
{
return;
}
_tick += dt;
if (_tick < 1f)
{
return;
}
_tick -= 1f;
try
{
float health = base.m_character.GetHealth();
float num = Mathf.Min(_dps, Mathf.Max(0f, health - _minHp));
if (!(num <= 0.0001f))
{
HitData val = new HitData();
val.m_point = ((Component)base.m_character).transform.position;
val.m_dir = Vector3.zero;
val.m_damage.m_blunt = num;
base.m_character.Damage(val);
}
}
catch
{
}
}
}
[HarmonyPatch]
internal static class Patches
{
private struct BorderlineState
{
public bool Armed;
public bool IsFall;
}
private static MethodInfo _staggerVec3;
private static bool _staggerResolved;
private static readonly Dictionary<int, float> _borderlineNextAllowed = new Dictionary<int, float>(128);
[HarmonyPatch(typeof(ObjectDB), "Awake")]
[HarmonyPostfix]
private static void ObjectDB_Awake_Postfix(ObjectDB __instance)
{
EnsureBleedRegistered(__instance, forceIconOverwrite: true);
}
[HarmonyPatch(typeof(ObjectDB), "CopyOtherDB")]
[HarmonyPostfix]
private static void ObjectDB_CopyOtherDB_Postfix(ObjectDB __instance)
{
EnsureBleedRegistered(__instance, forceIconOverwrite: true);
}
internal static void EnsureBleedRegistered(ObjectDB odb, bool forceIconOverwrite)
{
if ((Object)(object)odb == (Object)null)
{
return;
}
try
{
FatalFallPlugin.EnsureBleedIconLoaded(forceReload: false);
StatusEffect statusEffect = odb.GetStatusEffect(FatalFallPlugin.BleedSeHash);
if ((Object)(object)statusEffect != (Object)null)
{
statusEffect.m_ttl = FatalFallPlugin.BleedDuration();
if (forceIconOverwrite && (Object)(object)FatalFallPlugin.BleedIconSprite != (Object)null)
{
statusEffect.m_icon = FatalFallPlugin.BleedIconSprite;
}
FatalFallPlugin.BleedPrefab = statusEffect;
return;
}
SE_PixFatalFall_Bleed sE_PixFatalFall_Bleed = ScriptableObject.CreateInstance<SE_PixFatalFall_Bleed>();
((Object)sE_PixFatalFall_Bleed).name = "SE_PixFatalFall_Bleed";
((StatusEffect)sE_PixFatalFall_Bleed).m_name = "Bleeding";
((StatusEffect)sE_PixFatalFall_Bleed).m_tooltip = "You are bleeding.";
((StatusEffect)sE_PixFatalFall_Bleed).m_ttl = FatalFallPlugin.BleedDuration();
if ((Object)(object)FatalFallPlugin.BleedIconSprite != (Object)null)
{
((StatusEffect)sE_PixFatalFall_Bleed).m_icon = FatalFallPlugin.BleedIconSprite;
}
odb.m_StatusEffects.Add((StatusEffect)(object)sE_PixFatalFall_Bleed);
FatalFallPlugin.BleedPrefab = (StatusEffect)(object)sE_PixFatalFall_Bleed;
FatalFallPlugin.Dbg($"Bleed StatusEffect registered. iconNull={(Object)(object)((StatusEffect)sE_PixFatalFall_Bleed).m_icon == (Object)null} listCount={odb.m_StatusEffects.Count}");
}
catch
{
}
}
private static bool CanBorderlineFire(Character ch)
{
if ((Object)(object)ch == (Object)null)
{
return false;
}
float num = FatalFallPlugin.Now();
int instanceID = ((Object)ch).GetInstanceID();
if (_borderlineNextAllowed.TryGetValue(instanceID, out var value) && num < value)
{
return false;
}
return true;
}
private static void ArmBorderlineCooldown(Character ch)
{
if (!((Object)(object)ch == (Object)null))
{
float num = FatalFallPlugin.BorderlineCooldownSeconds();
if (!(num <= 0f))
{
float num2 = FatalFallPlugin.Now();
int instanceID = ((Object)ch).GetInstanceID();
_borderlineNextAllowed[instanceID] = num2 + num;
}
}
}
[HarmonyPatch(typeof(Character), "RPC_Damage")]
[HarmonyPrefix]
private static void Character_RPC_Damage_Prefix(Character __instance, ref HitData hit, ref BorderlineState __state)
{
//IL_01f0: Unknown result type (might be due to invalid IL or missing references)
//IL_01f5: Unknown result type (might be due to invalid IL or missing references)
//IL_0203: Unknown result type (might be due to invalid IL or missing references)
//IL_0205: 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_00d1: 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)
__state = default(BorderlineState);
if (!FatalFallPlugin.IsEnabled() || (Object)(object)__instance == (Object)null || hit == null)
{
return;
}
bool flag = (__state.IsFall = HitIntrospection.IsFallHit(hit));
if (!flag && !FatalFallPlugin.BorderlineAllDamage())
{
return;
}
if (FatalFallPlugin.BorderlineSkipIfBubble() && FatalFallPlugin.HasStaffBubble(__instance))
{
if (FatalFallPlugin.DebugEnabled())
{
FatalFallPlugin.Dbg("Borderline skipped due to Staff bubble active (prevents raw-damage false proc).");
}
return;
}
if (flag)
{
float mul = FatalFallPlugin.GetMul();
if (Math.Abs(mul - 1f) > 0.0001f)
{
try
{
DamageTypes dmg = hit.m_damage;
DamageTypesScaler.Scale(ref dmg, mul);
hit.m_damage = dmg;
float fallDamage = HitIntrospection.GetFallDamage(hit);
if (fallDamage > 0.0001f)
{
float value = fallDamage * mul;
HitIntrospection.SetFallDamage(ref hit, value);
}
}
catch
{
}
}
}
if (!FatalFallPlugin.BorderlineEnabled() || !CanBorderlineFire(__instance))
{
return;
}
float health = __instance.GetHealth();
float maxHealth = __instance.GetMaxHealth();
float num = HitIntrospection.SumAllDamage(hit);
float num2 = num - health;
if (num2 <= 0f || num2 > FatalFallPlugin.MaxOverkillToSave() || num2 > FatalFallPlugin.BorderlineWindowHp())
{
return;
}
float num3 = Mathf.Clamp(FatalFallPlugin.LeaveHp(), 0f, maxHealth);
float num4 = Mathf.Max(0f, health - num3);
if (num4 <= 0f)
{
return;
}
float num5 = num4 / num;
num5 = Mathf.Clamp01(num5);
if (num5 >= 0.9999f)
{
return;
}
try
{
DamageTypes dmg2 = hit.m_damage;
DamageTypesScaler.Scale(ref dmg2, num5);
hit.m_damage = dmg2;
if (flag)
{
float fallDamage2 = HitIntrospection.GetFallDamage(hit);
if (fallDamage2 > 0.0001f)
{
float value2 = fallDamage2 * num5;
HitIntrospection.SetFallDamage(ref hit, value2);
}
}
__state.Armed = true;
ArmBorderlineCooldown(__instance);
if (FatalFallPlugin.DebugEnabled())
{
FatalFallPlugin.Dbg($"Borderline armed isFall={flag} hp={health:0.00} incoming={num:0.00} overkill={num2:0.00} capFactor={num5:0.000}");
}
}
catch
{
__state = default(BorderlineState);
}
}
[HarmonyPatch(typeof(Character), "RPC_Damage")]
[HarmonyPostfix]
private static void Character_RPC_Damage_Postfix(Character __instance, HitData hit, BorderlineState __state)
{
if (!__state.Armed || !FatalFallPlugin.IsEnabled() || (Object)(object)__instance == (Object)null)
{
return;
}
if (FatalFallPlugin.PopupEnabled() && __instance.IsPlayer())
{
try
{
string[] array = (__state.IsFall ? FatalFallPlugin.PopupChoicesFall() : FatalFallPlugin.PopupChoicesGeneric());
string text = array[Random.Range(0, array.Length)];
MessageHud instance = MessageHud.instance;
if (instance != null)
{
instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false);
}
}
catch
{
}
}
if (__instance.IsPlayer())
{
FatalFallPlugin.PlayHeartbeat();
}
if (FatalFallPlugin.StaggerEnabled())
{
TryStagger(__instance);
}
if (FatalFallPlugin.BleedEnabled())
{
TryApplyBleedGuaranteedVisible(__instance);
}
}
private static void TryApplyBleedGuaranteedVisible(Character ch)
{
try
{
if (FatalFallPlugin.BleedSkipIfBubble() && FatalFallPlugin.HasStaffBubble(ch))
{
return;
}
SEMan sEMan = ch.GetSEMan();
if (sEMan != null)
{
EnsureBleedRegistered(ObjectDB.instance, forceIconOverwrite: true);
StatusEffect bleedPrefab = FatalFallPlugin.BleedPrefab;
if (!((Object)(object)bleedPrefab == (Object)null))
{
sEMan.RemoveStatusEffect(FatalFallPlugin.BleedSeHash, true);
sEMan.AddStatusEffect(bleedPrefab, true, 0, 0f);
}
}
}
catch
{
}
}
private static void TryStagger(Character ch)
{
//IL_009b: 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_0057: 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_0064: 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_007e: 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)
try
{
if (!_staggerResolved)
{
_staggerResolved = true;
_staggerVec3 = typeof(Character).GetMethod("Stagger", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(Vector3) }, null);
}
if (_staggerVec3 == null)
{
return;
}
Vector3 val = Vector3.back;
try
{
Vector3 forward = ((Component)ch).transform.forward;
if (((Vector3)(ref forward)).sqrMagnitude > 0.0001f)
{
val = -forward;
}
}
catch
{
}
_staggerVec3.Invoke(ch, new object[1] { val * FatalFallPlugin.StaggerStrength() });
}
catch
{
}
}
}
internal static class FallTracker
{
private sealed class State
{
public float maxY;
public bool wasGrounded;
public float lastFallHitTime;
public float lastFallVanillaDamage;
}
private static readonly Dictionary<int, State> _states = new Dictionary<int, State>(256);
[ThreadStatic]
private static bool _applyingExtra;
private static State Get(Character ch)
{
//IL_002a: Unknown result type (might be due to invalid IL or missing references)
int instanceID = ((Object)ch).GetInstanceID();
if (!_states.TryGetValue(instanceID, out var value))
{
value = new State();
value.maxY = ((Component)ch).transform.position.y;
value.wasGrounded = true;
value.lastFallHitTime = -999f;
value.lastFallVanillaDamage = 0f;
_states[instanceID] = value;
}
return value;
}
public static void NoteFallHit(Character ch, HitData hit)
{
if (!((Object)(object)ch == (Object)null) && hit != null)
{
State state = Get(ch);
state.lastFallHitTime = FatalFallPlugin.Now();
float num = HitIntrospection.GetFallDamage(hit);
if (num <= 0.0001f)
{
num = HitIntrospection.SumAllDamage(hit);
}
state.lastFallVanillaDamage = num;
}
}
public static void OnGroundContactTick(Character ch)
{
//IL_005f: 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_01ac: Expected O, but got Unknown
//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_01c0: Unknown result type (might be due to invalid IL or missing references)
//IL_01c5: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)ch == (Object)null || !FatalFallPlugin.IsEnabled() || !FatalFallPlugin.UncapEnabled())
{
return;
}
State state = Get(ch);
bool flag = false;
try
{
flag = ch.IsOnGround();
}
catch
{
flag = false;
}
float num = 0f;
try
{
num = ((Component)ch).transform.position.y;
}
catch
{
num = 0f;
}
if (!flag)
{
if (num > state.maxY)
{
state.maxY = num;
}
state.wasGrounded = false;
}
else
{
if (!state.wasGrounded)
{
state.wasGrounded = true;
float num2 = FatalFallPlugin.Now();
float num3 = FatalFallPlugin.UncapRequireRecentFallHitWindow();
if (num2 - state.lastFallHitTime > num3)
{
state.maxY = num;
return;
}
float num4 = state.maxY - num;
state.maxY = num;
if (num4 < 0.5f || num4 > 250f)
{
return;
}
float num5 = FatalFallPlugin.UncapStartHeight();
float num6 = FatalFallPlugin.UncapDamagePerMeter();
if (num6 <= 0f)
{
return;
}
float num7 = Mathf.Max(0f, num4 - num5) * num6;
float num8 = FatalFallPlugin.UncapMaxExtraDamage();
num7 = Mathf.Clamp(num7, 0f, num8);
if (num7 <= 0.0001f || _applyingExtra)
{
return;
}
try
{
_applyingExtra = true;
HitData val = new HitData();
val.m_point = ((Component)ch).transform.position;
val.m_dir = Vector3.zero;
val.m_damage.m_blunt = num7;
ch.Damage(val);
FatalFallPlugin.Dbg($"Uncap landing: fallHeight={num4:0.0} extra={num7:0.0} recentFallHit={num2 - state.lastFallHitTime:0.000}s");
return;
}
catch
{
return;
}
finally
{
_applyingExtra = false;
}
}
state.maxY = num;
}
}
}
[HarmonyPatch]
internal static class FallTrackerPatches
{
[HarmonyPatch(typeof(Character), "RPC_Damage")]
[HarmonyPostfix]
private static void Character_RPC_Damage_Postfix_FallNote(Character __instance, HitData hit)
{
if (FatalFallPlugin.IsEnabled() && !((Object)(object)__instance == (Object)null) && hit != null && FatalFallPlugin.UncapEnabled() && HitIntrospection.IsFallHit(hit))
{
FallTracker.NoteFallHit(__instance, hit);
}
}
[HarmonyPatch(typeof(Character), "UpdateGroundContact")]
[HarmonyPostfix]
private static void Character_UpdateGroundContact_Postfix(Character __instance)
{
if (FatalFallPlugin.IsEnabled())
{
FallTracker.OnGroundContactTick(__instance);
}
}
}