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 _bloodStates = new Dictionary(); private static readonly HashSet _knownNoBloodInstanceIds = new HashSet(); private static readonly Dictionary _networkFloats = new Dictionary(); private static readonly Dictionary _localTrackedAmount = new Dictionary(); private static readonly Dictionary _lastSeenNetworkValue = new Dictionary(); private static readonly HashSet _loggedNoNetworkEntity = new HashSet(); 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.op_Implicit(((Component)root).GetComponentsInChildren(true)); int num = 0; List list = new List(); Renderer[] array2 = array; foreach (Renderer val in array2) { if ((Object)(object)val == (Object)null) { continue; } Material[] array3 = Il2CppArrayBase.op_Implicit((Il2CppArrayBase)(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.op_Implicit((Il2CppArrayBase)(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.op_Implicit(((Component)root).GetComponentsInChildren(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 componentsInChildren = ((Component)rigRoot).GetComponentsInChildren(Il2CppType.From(type), true); int num = 0; foreach (Component item in (Il2CppArrayBase)(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 toRemove = null; foreach (Material mat in new List(_bloodStates.Keys)) { if ((Object)(object)mat == (Object)null) { if (toRemove == null) { toRemove = new List(); } 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(); } 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(); if ((Object)(object)val == (Object)null) { val = ((Component)root).gameObject.AddComponent(); 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.op_Implicit(Object.FindObjectsOfType()); AIBrain[] array = aiBrains; foreach (AIBrain brain in array) { if (!((Object)(object)brain == (Object)null)) { EnsureRpcFloat(((Component)brain).transform.root, logDiscovery: false); } } PlayerDamageReceiver[] receivers = Il2CppArrayBase.op_Implicit(Object.FindObjectsOfType()); PlayerDamageReceiver[] array2 = receivers; foreach (PlayerDamageReceiver r in array2) { if (!((Object)(object)r == (Object)null)) { EnsureRpcFloat(((Component)r).transform.root, logDiscovery: false); } } foreach (KeyValuePair kvp in new List>(_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(true)) { if (((Object)componentsInChild).name.Contains(nameContains)) { return componentsInChild; } } } catch (Exception ex) { MelonLogger.Warning("[KF2 Blood] FindChildByNameContains failed: " + ex.Message); } return null; } }