using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSLZ.Marrow;
using Il2CppSLZ.Marrow.AI;
using Il2CppSLZ.Marrow.Combat;
using Il2CppSLZ.Marrow.PuppetMasta;
using KF2BloodMod;
using LabFusion.Marrow.Integration;
using MelonLoader;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: MelonInfo(typeof(BloodMod), "KF2 Blood", "1.0.0", "happyknight15", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: MelonOptionalDependencies(new string[] { "LabFusion" })]
[assembly: AssemblyTitle("KF2bloodmod")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("KF2bloodmod")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("e374828c-cc96-435d-9d85-2e9d32ab35a7")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace KF2BloodMod;
public class BloodMod : MelonMod
{
private class BloodState
{
public float gloss;
public float lastHitTime;
public float amountAtLastHit;
}
public static class TakeDamagePostfix
{
public static void Postfix(SubBehaviourHealth __instance, int m, Attack attack)
{
//IL_00ce: 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_00d3: Unknown result type (might be due to invalid IL or missing references)
//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
try
{
if (__instance == null || attack == null)
{
return;
}
BehaviourBase behaviour = ((SubBehaviourBase)__instance).behaviour;
if ((Object)(object)behaviour == (Object)null)
{
return;
}
RegisterLocalHit(((Component)behaviour).transform.root, ((Object)((Component)behaviour).transform.root).name);
if (!((Object)(object)attack.proxy != (Object)null) || !((Object)(object)attack.proxy.root != (Object)null))
{
return;
}
Transform transform = attack.proxy.root.transform;
Transform root = transform.root;
if ((Object)(object)root != (Object)(object)((Component)behaviour).transform.root)
{
Transform val = FindChildByNameContains(root, "Head");
Vector3 val2 = (((Object)(object)val != (Object)null) ? val.position : transform.position);
float num = Vector3.Distance(val2, ((Component)behaviour).transform.position);
if (num <= 3f)
{
ApplyBloodToRenderersUnder(root, ((Object)root).name);
ApplyBloodToHeldWeapons(root);
}
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood] TakeDamagePostfix failed: " + ex.Message);
}
}
}
public static class PlayerReceiveAttackPostfix
{
public static void Postfix(PlayerDamageReceiver __instance, Attack attack)
{
//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
//IL_00af: Unknown result type (might be due to invalid IL or missing references)
//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
//IL_00df: Unknown result type (might be due to invalid IL or missing references)
//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
try
{
if ((Object)(object)__instance == (Object)null || attack == null)
{
return;
}
Transform root = ((Component)__instance).transform.root;
RegisterLocalHit(root, ((Object)root).name);
if (!((Object)(object)attack.proxy != (Object)null) || !((Object)(object)attack.proxy.root != (Object)null))
{
return;
}
Transform transform = attack.proxy.root.transform;
Transform root2 = transform.root;
if ((Object)(object)root2 != (Object)(object)root)
{
Transform val = FindChildByNameContains(root2, "Head");
Transform val2 = FindChildByNameContains(root, "Head");
Vector3 val3 = (((Object)(object)val != (Object)null) ? val.position : transform.position);
Vector3 val4 = (((Object)(object)val2 != (Object)null) ? val2.position : ((Component)__instance).transform.position);
float num = Vector3.Distance(val3, val4);
if (num <= 3f)
{
ApplyBloodToRenderersUnder(root2, ((Object)root2).name);
ApplyBloodToHeldWeapons(root2);
}
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood] PlayerReceiveAttackPostfix failed: " + ex.Message);
}
}
}
private const float AttackerBloodMaxDistance = 3f;
private const float BloodAmountPerHit = 0.9f;
private const float BloodAmountCap = 0.9f;
private const float FreshBloodGloss = 0.9f;
private const float DriedBloodGloss = 0.15f;
private const float BloodDryRatePerSecond = 0.05f;
private const float BloodDissolveSeconds = 60f;
private static Harmony _harmony;
private static readonly Dictionary<Material, BloodState> _bloodStates = new Dictionary<Material, BloodState>();
private static readonly HashSet<int> _knownNoBloodInstanceIds = new HashSet<int>();
private static readonly Dictionary<int, RPCFloat> _networkFloats = new Dictionary<int, RPCFloat>();
private static readonly Dictionary<int, float> _localTrackedAmount = new Dictionary<int, float>();
private static readonly Dictionary<int, float> _lastSeenNetworkValue = new Dictionary<int, float>();
private static readonly HashSet<int> _loggedNoNetworkEntity = new HashSet<int>();
public override void OnInitializeMelon()
{
MelonLogger.Msg("KF2 Blood (standalone) Initialized!");
ApplyBloodDamagePatch();
MelonCoroutines.Start(BloodNetworkWatcherRoutine());
}
private static void ApplyBloodToRenderersUnder(Transform root, string label)
{
if ((Object)(object)root == (Object)null)
{
return;
}
int instanceID = ((Object)root).GetInstanceID();
if (_knownNoBloodInstanceIds.Contains(instanceID))
{
return;
}
try
{
Renderer[] array = Il2CppArrayBase<Renderer>.op_Implicit(((Component)root).GetComponentsInChildren<Renderer>(true));
int num = 0;
List<string> list = new List<string>();
Renderer[] array2 = array;
foreach (Renderer val in array2)
{
if ((Object)(object)val == (Object)null)
{
continue;
}
Material[] array3 = Il2CppArrayBase<Material>.op_Implicit((Il2CppArrayBase<Material>)(object)val.sharedMaterials);
Material[] array4 = null;
for (int j = 0; j < array3.Length; j++)
{
Material val2 = array3[j];
list.Add($"{((Object)val).name}[{j}]:" + (((Object)(object)val2 != (Object)null) ? ((Object)val2.shader).name : "NULL material"));
if (!((Object)(object)val2 == (Object)null) && val2.HasProperty("_BloodAmount"))
{
if (array4 == null)
{
array4 = Il2CppArrayBase<Material>.op_Implicit((Il2CppArrayBase<Material>)(object)val.materials);
}
Material val3 = array4[j];
float num2 = val3.GetFloat("_BloodAmount");
float num3 = Mathf.Min(0.9f, num2 + 0.9f);
val3.SetFloat("_BloodAmount", num3);
if (val3.HasProperty("_BloodGloss"))
{
val3.SetFloat("_BloodGloss", 0.9f);
}
_bloodStates[val3] = new BloodState
{
gloss = 0.9f,
lastHitTime = Time.time,
amountAtLastHit = num3
};
num++;
}
}
}
if (num > 0)
{
MelonLogger.Msg($"[KF2 Blood] Applied blood to {num} renderer(s) on '{label}'.");
return;
}
MelonLogger.Msg(string.Format("[KF2 Blood Diag] No blood-capable renderer found under '{0}' ({1} renderer(s) checked): {2}.", label, array.Length, string.Join(", ", list)));
_knownNoBloodInstanceIds.Add(instanceID);
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood] ApplyBloodToRenderersUnder('" + label + "') failed: " + ex.Message);
}
}
private static void ApplyAbsoluteBloodAmount(Transform root, float amount, string label)
{
if ((Object)(object)root == (Object)null)
{
return;
}
try
{
float num = Mathf.Clamp(amount, 0f, 0.9f);
Renderer[] array = Il2CppArrayBase<Renderer>.op_Implicit(((Component)root).GetComponentsInChildren<Renderer>(true));
int num2 = 0;
Renderer[] array2 = array;
foreach (Renderer val in array2)
{
if ((Object)(object)val == (Object)null)
{
continue;
}
Material sharedMaterial = val.sharedMaterial;
if (!((Object)(object)sharedMaterial == (Object)null) && sharedMaterial.HasProperty("_BloodAmount"))
{
Material material = val.material;
material.SetFloat("_BloodAmount", num);
if (material.HasProperty("_BloodGloss"))
{
material.SetFloat("_BloodGloss", 0.9f);
}
_bloodStates[material] = new BloodState
{
gloss = 0.9f,
lastHitTime = Time.time,
amountAtLastHit = num
};
num2++;
}
}
if (num2 > 0)
{
MelonLogger.Msg($"[KF2 Blood Sync] Applied synced blood amount {num:0.###} to {num2} renderer(s) on '{label}'.");
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood Sync] ApplyAbsoluteBloodAmount('" + label + "') failed: " + ex.Message);
}
}
private static void ApplyBloodToHeldWeapons(Transform rigRoot)
{
if ((Object)(object)rigRoot == (Object)null)
{
return;
}
try
{
Type type = FindTypeByName("Hand");
if (type == null)
{
MelonLogger.Warning("[KF2 Blood Diag] Hand type not found - can't locate held weapons.");
return;
}
MethodInfo method = type.GetMethod("HasAttachedObject", BindingFlags.Instance | BindingFlags.Public);
MethodInfo method2 = type.GetMethod("get_m_CurrentAttachedGO", BindingFlags.Instance | BindingFlags.Public);
if (method == null || method2 == null)
{
MelonLogger.Warning("[KF2 Blood Diag] HasAttachedObject/get_m_CurrentAttachedGO not found on Hand.");
return;
}
Il2CppReferenceArray<Component> componentsInChildren = ((Component)rigRoot).GetComponentsInChildren(Il2CppType.From(type), true);
int num = 0;
foreach (Component item in (Il2CppArrayBase<Component>)(object)componentsInChildren)
{
Component val = item;
if ((Object)(object)val == (Object)null)
{
continue;
}
num++;
bool flag = false;
try
{
flag = (bool)(method.Invoke(val, null) ?? ((object)false));
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood Diag] HasAttachedObject invoke failed: " + ex.Message);
continue;
}
if (flag)
{
GameObject val2 = null;
try
{
object? obj = method2.Invoke(val, null);
val2 = (GameObject)((obj is GameObject) ? obj : null);
}
catch (Exception ex2)
{
MelonLogger.Warning("[KF2 Blood Diag] get_m_CurrentAttachedGO invoke failed: " + ex2.Message);
continue;
}
if ((Object)(object)val2 != (Object)null)
{
Transform root = val2.transform.root;
MelonLogger.Msg("[KF2 Blood Diag] Hand has attached object: '" + GetFullPath(val2.transform) + "', weapon root='" + GetFullPath(root) + "'.");
ApplyBloodToRenderersUnder(root, ((Object)root).name);
}
}
}
if (num == 0)
{
MelonLogger.Msg("[KF2 Blood Diag] No Hand components found under '" + ((Object)rigRoot).name + "'.");
}
}
catch (Exception ex3)
{
MelonLogger.Warning("[KF2 Blood] ApplyBloodToHeldWeapons failed: " + ex3.Message);
}
}
private static IEnumerator BloodDryOverTimeRoutine()
{
WaitForSeconds tickWait = new WaitForSeconds(0.5f);
while (true)
{
yield return tickWait;
if (_bloodStates.Count == 0)
{
continue;
}
List<Material> toRemove = null;
foreach (Material mat in new List<Material>(_bloodStates.Keys))
{
if ((Object)(object)mat == (Object)null)
{
if (toRemove == null)
{
toRemove = new List<Material>();
}
toRemove.Add(mat);
continue;
}
BloodState state = _bloodStates[mat];
state.gloss = Mathf.Max(0.15f, state.gloss - 0.025f);
if (mat.HasProperty("_BloodGloss"))
{
mat.SetFloat("_BloodGloss", state.gloss);
}
float elapsed = Time.time - state.lastHitTime;
if (elapsed >= 60f)
{
mat.SetFloat("_BloodAmount", 0f);
if (toRemove == null)
{
toRemove = new List<Material>();
}
toRemove.Add(mat);
}
else
{
float t = elapsed / 60f;
mat.SetFloat("_BloodAmount", Mathf.Lerp(state.amountAtLastHit, 0f, t));
}
}
if (toRemove == null)
{
continue;
}
foreach (Material mat2 in toRemove)
{
_bloodStates.Remove(mat2);
}
}
}
private static RPCFloat EnsureRpcFloat(Transform root, bool logDiscovery)
{
if ((Object)(object)root == (Object)null)
{
return null;
}
int instanceID = ((Object)root).GetInstanceID();
if (_networkFloats.TryGetValue(instanceID, out var value))
{
return value;
}
RPCFloat val = null;
try
{
val = ((Component)root).GetComponent<RPCFloat>();
if ((Object)(object)val == (Object)null)
{
val = ((Component)root).gameObject.AddComponent<RPCFloat>();
if (logDiscovery)
{
MelonLogger.Msg("[KF2 Blood Sync] Added RPCFloat to '" + ((Object)root).name + "'.");
}
}
else if (logDiscovery)
{
MelonLogger.Msg("[KF2 Blood Sync] Found existing RPCFloat already on '" + ((Object)root).name + "'.");
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood Sync] EnsureRpcFloat failed on '" + ((Object)root).name + "': " + ex.Message + " (LabFusion may not be loaded, or this object can't take the component - blood will stay local-only here).");
return null;
}
_networkFloats[instanceID] = val;
try
{
bool hasNetworkEntity = ((RPCVariable)val).HasNetworkEntity;
MelonLogger.Msg($"[KF2 Blood Sync] '{((Object)root).name}' RPCFloat.HasNetworkEntity={hasNetworkEntity}.");
if (!hasNetworkEntity && _loggedNoNetworkEntity.Add(instanceID))
{
MelonLogger.Warning("[KF2 Blood Sync] '" + ((Object)root).name + "' has no NetworkEntity - this object's blood will NOT sync across clients (local-only fallback).");
}
}
catch (Exception ex2)
{
MelonLogger.Warning("[KF2 Blood Sync] Reading HasNetworkEntity failed on '" + ((Object)root).name + "': " + ex2.Message);
}
return val;
}
private static void RegisterLocalHit(Transform root, string label)
{
if ((Object)(object)root == (Object)null)
{
return;
}
ApplyBloodToRenderersUnder(root, label);
int instanceID = ((Object)root).GetInstanceID();
float value;
float num = (_localTrackedAmount.TryGetValue(instanceID, out value) ? value : 0f);
float num2 = Mathf.Min(0.9f, num + 0.9f);
_localTrackedAmount[instanceID] = num2;
RPCFloat val = EnsureRpcFloat(root, logDiscovery: true);
if ((Object)(object)val == (Object)null)
{
return;
}
try
{
if (((RPCVariable)val).HasNetworkEntity)
{
bool flag = val.SetValue(num2);
MelonLogger.Msg($"[KF2 Blood Sync] SetValue({num2:0.###}) on '{((Object)root).name}' -> success={flag}.");
_lastSeenNetworkValue[instanceID] = num2;
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood Sync] SetValue failed on '" + ((Object)root).name + "': " + ex.Message);
}
}
private static IEnumerator BloodNetworkWatcherRoutine()
{
WaitForSeconds pollWait = new WaitForSeconds(0.5f);
while (true)
{
yield return pollWait;
try
{
AIBrain[] aiBrains = Il2CppArrayBase<AIBrain>.op_Implicit(Object.FindObjectsOfType<AIBrain>());
AIBrain[] array = aiBrains;
foreach (AIBrain brain in array)
{
if (!((Object)(object)brain == (Object)null))
{
EnsureRpcFloat(((Component)brain).transform.root, logDiscovery: false);
}
}
PlayerDamageReceiver[] receivers = Il2CppArrayBase<PlayerDamageReceiver>.op_Implicit(Object.FindObjectsOfType<PlayerDamageReceiver>());
PlayerDamageReceiver[] array2 = receivers;
foreach (PlayerDamageReceiver r in array2)
{
if (!((Object)(object)r == (Object)null))
{
EnsureRpcFloat(((Component)r).transform.root, logDiscovery: false);
}
}
foreach (KeyValuePair<int, RPCFloat> kvp in new List<KeyValuePair<int, RPCFloat>>(_networkFloats))
{
int id = kvp.Key;
RPCFloat rf = kvp.Value;
if (!((Object)(object)rf == (Object)null))
{
float latest;
try
{
latest = rf.GetLatestValue();
}
catch (Exception ex)
{
Exception ex2 = ex;
MelonLogger.Warning($"[KF2 Blood Sync] GetLatestValue failed (id={id}): {ex2.Message}");
continue;
}
float ls;
float lastSeen = (_lastSeenNetworkValue.TryGetValue(id, out ls) ? ls : (-1f));
if (!Mathf.Approximately(latest, lastSeen))
{
_lastSeenNetworkValue[id] = latest;
_localTrackedAmount[id] = latest;
Transform root = ((Component)rf).transform;
MelonLogger.Msg($"[KF2 Blood Sync] '{((Object)root).name}' received new synced blood value: {latest:0.###}.");
ApplyAbsoluteBloodAmount(root, latest, ((Object)root).name);
}
}
}
}
catch (Exception ex)
{
Exception ex3 = ex;
MelonLogger.Warning("[KF2 Blood Sync] BloodNetworkWatcherRoutine error: " + ex3.Message);
}
}
}
private static void ApplyBloodDamagePatch()
{
//IL_003a: Unknown result type (might be due to invalid IL or missing references)
//IL_0044: Expected O, but got Unknown
//IL_0061: Unknown result type (might be due to invalid IL or missing references)
//IL_006e: Expected O, but got Unknown
//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
//IL_00da: Expected O, but got Unknown
try
{
MethodInfo method = typeof(SubBehaviourHealth).GetMethod("TakeDamage", BindingFlags.Instance | BindingFlags.Public);
if (method == null)
{
MelonLogger.Warning("[KF2 Blood] TakeDamage method not found on SubBehaviourHealth - blood-on-hit won't work.");
return;
}
_harmony = new Harmony("KF2BloodMod.Standalone");
MethodInfo method2 = typeof(TakeDamagePostfix).GetMethod("Postfix");
_harmony.Patch((MethodBase)method, (HarmonyMethod)null, new HarmonyMethod(method2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
MelonLogger.Msg("[KF2 Blood] Harmony patch applied to SubBehaviourHealth.TakeDamage - shader-driven blood-on-hit active.");
MethodInfo method3 = typeof(PlayerDamageReceiver).GetMethod("ReceiveAttack", BindingFlags.Instance | BindingFlags.Public);
if (method3 == null)
{
MelonLogger.Warning("[KF2 Blood] ReceiveAttack method not found on PlayerDamageReceiver - player blood-on-hit won't work.");
}
else
{
MethodInfo method4 = typeof(PlayerReceiveAttackPostfix).GetMethod("Postfix");
_harmony.Patch((MethodBase)method3, (HarmonyMethod)null, new HarmonyMethod(method4), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
MelonLogger.Msg("[KF2 Blood] Harmony patch applied to PlayerDamageReceiver.ReceiveAttack - player blood-on-hit active.");
}
MelonCoroutines.Start(BloodDryOverTimeRoutine());
}
catch (Exception arg)
{
MelonLogger.Error($"[KF2 Blood] ApplyBloodDamagePatch failed: {arg}");
}
}
private static Type FindTypeByName(string name)
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch
{
continue;
}
Type[] array = types;
foreach (Type type in array)
{
if (type != null && type.Name == name)
{
return type;
}
}
}
return null;
}
private static string GetFullPath(Transform t)
{
string text = ((Object)t).name;
Transform parent = t.parent;
while ((Object)(object)parent != (Object)null)
{
text = ((Object)parent).name + "/" + text;
parent = parent.parent;
}
return text;
}
private static Transform FindChildByNameContains(Transform root, string nameContains)
{
if ((Object)(object)root == (Object)null)
{
return null;
}
try
{
foreach (Transform componentsInChild in ((Component)root).GetComponentsInChildren<Transform>(true))
{
if (((Object)componentsInChild).name.Contains(nameContains))
{
return componentsInChild;
}
}
}
catch (Exception ex)
{
MelonLogger.Warning("[KF2 Blood] FindChildByNameContains failed: " + ex.Message);
}
return null;
}
}