Decompiled source of Kf2bloodmod v1.0.2

KF2bloodmod/Mods/KF2bloodmod.dll

Decompiled 15 hours ago
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;
	}
}