using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using BetterHeals.Config;
using BetterHeals.Patches;
using HarmonyLib;
using Photon.Pun;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("TeamHeals")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("TeamHeals")]
[assembly: AssemblyCopyright("Copyright © 2025")]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("8b363090-6f7a-451a-92a9-ed78838c26e0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace BetterHeals
{
[BepInPlugin("MrBytesized.REPO.BetterHeals", "Better Heals", "2.1.0")]
public class TeamHealsPlugin : BaseUnityPlugin
{
private const string mod_guid = "MrBytesized.REPO.BetterHeals";
private const string mod_name = "Better Heals";
private const string mod_version = "2.1.0";
private readonly Harmony harmony = new Harmony("MrBytesized.REPO.BetterHeals");
private static TeamHealsPlugin instance;
internal static ManualLogSource Log;
internal static readonly FieldRef<ItemHealthPack, ItemToggle> item_toggle_ref = AccessTools.FieldRefAccess<ItemHealthPack, ItemToggle>("itemToggle");
internal static readonly FieldRef<ItemToggle, int> player_photon_id_ref = AccessTools.FieldRefAccess<ItemToggle, int>("playerTogglePhotonID");
internal static readonly FieldRef<PlayerHealth, int> health_ref = AccessTools.FieldRefAccess<PlayerHealth, int>("health");
internal static readonly FieldRef<PlayerHealth, int> max_health_ref = AccessTools.FieldRefAccess<PlayerHealth, int>("maxHealth");
private (ConfigEntry<bool> configEntry, Action enablePatch, Action disablePatch, string description)[] patchArray;
private void Awake()
{
if ((Object)(object)instance == (Object)null)
{
instance = this;
}
Configuration.Init(((BaseUnityPlugin)this).Config);
Log = Logger.CreateLogSource("MrBytesized.REPO.BetterHeals");
Log.LogInfo((object)"Team Heals mod has been activated");
harmony.PatchAll(typeof(TeamHealsPlugin));
harmony.PatchAll(typeof(PunManagerPatch));
Log.LogInfo((object)"PunManager patch applied.");
harmony.PatchAll(typeof(ItemHealthPackPatch));
Log.LogInfo((object)"ItemHealthPack patch applied.");
patchArray = new(ConfigEntry<bool>, Action, Action, string)[5]
{
(Configuration.EnableCustomReviveHealthPatch, delegate
{
harmony.PatchAll(typeof(PlayerReviveHealthPatch));
}, delegate
{
harmony.Unpatch((MethodBase)AccessTools.Method(typeof(PlayerAvatar), "ReviveRPC", (Type[])null, (Type[])null), AccessTools.Method(typeof(PlayerReviveHealthPatch), "ReviveRPC_Postfix", (Type[])null, (Type[])null));
}, "Custom Revive Health"),
(Configuration.EnableExtractionHealPatch, delegate
{
harmony.PatchAll(typeof(ExtractionPointHealPatch));
}, delegate
{
harmony.Unpatch((MethodBase)AccessTools.Method(typeof(ExtractionPoint), "StateSet", (Type[])null, (Type[])null), AccessTools.Method(typeof(ExtractionPointHealPatch), "StateSet_Postfix", (Type[])null, (Type[])null));
}, "Extraction Heal"),
(Configuration.EnableHealthRegenPatch, delegate
{
harmony.PatchAll(typeof(HealthRegenPatch));
}, delegate
{
harmony.Unpatch((MethodBase)AccessTools.Method(typeof(LevelGenerator), "GenerateDone", (Type[])null, (Type[])null), AccessTools.Method(typeof(HealthRegenPatch), "Start_Postfix", (Type[])null, (Type[])null));
}, "Health Regeneration"),
(Configuration.EnableFullHealthAtStartPatch, delegate
{
harmony.PatchAll(typeof(FullHealthAtStartPatch));
}, delegate
{
harmony.Unpatch((MethodBase)AccessTools.Method(typeof(LevelGenerator), "GenerateDone", (Type[])null, (Type[])null), AccessTools.Method(typeof(FullHealthAtStartPatch), "GenerateDone_Postfix", (Type[])null, (Type[])null));
}, "Full Health At Start"),
(Configuration.EnableTeamHealthPackPatch, delegate
{
//IL_002d: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: Expected O, but got Unknown
harmony.Patch((MethodBase)AccessTools.Method(typeof(ItemHealthPack), "UsedRPC", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(ItemHealthPackPatch), "TeamHealthPackSync", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
}, delegate
{
harmony.Unpatch((MethodBase)AccessTools.Method(typeof(ItemHealthPack), "UsedRPC", (Type[])null, (Type[])null), AccessTools.Method(typeof(ItemHealthPackPatch), "TeamHealthPackSync", (Type[])null, (Type[])null));
}, "Health Pack Team Healing")
};
(ConfigEntry<bool>, Action, Action, string)[] array = patchArray;
for (int i = 0; i < array.Length; i++)
{
var (configEntry, enablePatch, disablePatch, description) = array[i];
UpdatePatchFromConfig(configEntry, enablePatch, disablePatch, description);
configEntry.SettingChanged += delegate
{
UpdatePatchFromConfig(configEntry, enablePatch, disablePatch, description);
};
}
}
private void UpdatePatchFromConfig(ConfigEntry<bool> configEntry, Action enablePatch, Action disablePatch, string description)
{
if (configEntry.Value)
{
enablePatch();
Log.LogInfo((object)(description + " patch enabled."));
}
else
{
disablePatch();
Log.LogInfo((object)(description + " patch disabled."));
}
}
}
}
namespace BetterHeals.Patches
{
internal static class ExtractionPointHealPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ExtractionPoint), "StateSet")]
private static void StateSet_Postfix(ExtractionPoint __instance, State newState)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0002: Invalid comparison between Unknown and I4
if ((int)newState != 7 || SemiFunc.RunIsShop() || !PhotonNetwork.IsMasterClient)
{
return;
}
int num = Configuration.ExtractionHealAmount.Value;
foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
{
if ((Object)(object)player != (Object)null && (Object)(object)player.playerHealth != (Object)null)
{
if (Configuration.EnableFullReviveHealth.Value)
{
int num2 = TeamHealsPlugin.health_ref.Invoke(player.playerHealth);
num = TeamHealsPlugin.max_health_ref.Invoke(player.playerHealth) - num2;
}
FieldInfo field = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
int num3 = ((!(field != null)) ? 1 : ((int)field.GetValue(player.playerHealth)));
FieldInfo field2 = typeof(PlayerAvatar).GetField("isDisabled", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
bool flag = field2 != null && (bool)field2.GetValue(player);
if (num3 > 0 && !flag)
{
player.playerHealth.HealOther(num, true);
}
}
}
TeamHealsPlugin.Log.LogInfo((object)$"All alive players healed to {num} after extraction.");
}
}
internal static class FullHealthAtStartPatch
{
[HarmonyPatch(typeof(LevelGenerator), "GenerateDone")]
[HarmonyPostfix]
private static void GenerateDone_Postfix()
{
//IL_008e: Unknown result type (might be due to invalid IL or missing references)
RunManager instance = RunManager.instance;
if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelMainMenu) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobbyMenu) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobby) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelShop) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelRecording) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelSplashScreen) && PhotonNetwork.IsMasterClient)
{
new GameObject("HealCoroutineRunner").AddComponent<HealRunner>();
}
}
}
public class HealRunner : MonoBehaviour
{
[CompilerGenerated]
private sealed class <HealAllPlayersToFull>d__1 : IEnumerator<object>, IDisposable, IEnumerator
{
private int <>1__state;
private object <>2__current;
public HealRunner <>4__this;
object IEnumerator<object>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <HealAllPlayersToFull>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
<>1__state = -2;
}
private bool MoveNext()
{
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Expected O, but got Unknown
int num = <>1__state;
HealRunner healRunner = <>4__this;
switch (num)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = (object)new WaitForSeconds(1f);
<>1__state = 1;
return true;
case 1:
{
<>1__state = -1;
TeamHealsPlugin.Log.LogInfo((object)"Proceeding to heal all players to full health.");
if ((Object)(object)GameDirector.instance == (Object)null || GameDirector.instance.PlayerList == null)
{
TeamHealsPlugin.Log.LogWarning((object)"GameDirector.instance or PlayerList is null.");
Object.Destroy((Object)(object)((Component)healRunner).gameObject);
return false;
}
List<PlayerAvatar> playerList = GameDirector.instance.PlayerList;
FieldInfo field = typeof(PlayerHealth).GetField("health", BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo field2 = typeof(PlayerHealth).GetField("maxHealth", BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null || field2 == null)
{
TeamHealsPlugin.Log.LogError((object)"Could not find health or maxHealth fields via reflection.");
Object.Destroy((Object)(object)((Component)healRunner).gameObject);
return false;
}
foreach (PlayerAvatar item in playerList)
{
if ((Object)(object)item != (Object)null && (Object)(object)item.playerHealth != (Object)null)
{
int num2 = (int)field.GetValue(item.playerHealth);
int num3 = (int)field2.GetValue(item.playerHealth);
int num4 = num3 - num2;
TeamHealsPlugin.Log.LogInfo((object)$"Player {item.photonView.Owner.NickName} current health: {num2}, max health: {num3}, calculated heal amount: {num4}");
if (num4 > 0)
{
item.playerHealth.HealOther(num4, true);
TeamHealsPlugin.Log.LogInfo((object)$"Healed player {item.photonView.Owner.NickName} to full health: {num3}");
}
}
}
Object.Destroy((Object)(object)((Component)healRunner).gameObject);
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();
}
}
private void Awake()
{
((MonoBehaviour)this).StartCoroutine(HealAllPlayersToFull());
}
[IteratorStateMachine(typeof(<HealAllPlayersToFull>d__1))]
private IEnumerator HealAllPlayersToFull()
{
//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
return new <HealAllPlayersToFull>d__1(0)
{
<>4__this = this
};
}
}
public class RegenUpdater : MonoBehaviour
{
private float updateThrottle;
public void Init()
{
}
private void Update()
{
RunManager instance = RunManager.instance;
if ((Object)(object)instance == (Object)null || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelMainMenu || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobbyMenu || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobby || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelShop || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelRecording || (Object)(object)instance.levelCurrent == (Object)(object)instance.levelSplashScreen || !Configuration.EnableHealthRegenPatch.Value)
{
return;
}
updateThrottle += Time.deltaTime;
if (updateThrottle < (float)Configuration.CustomRegenIntervalAmount.Value)
{
return;
}
updateThrottle = 0f;
int value = Configuration.HealthRegenAmount.Value;
foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
{
if ((Object)(object)player.playerHealth != (Object)null && TeamHealsPlugin.health_ref.Invoke(player.playerHealth) > 0)
{
player.playerHealth.HealOther(value, false);
}
}
TeamHealsPlugin.Log.LogInfo((object)$"Players healed by regeneration: {value}");
}
}
internal static class HealthRegenPatch
{
[HarmonyPatch(typeof(LevelGenerator), "GenerateDone")]
[HarmonyPostfix]
private static void Start_Postfix(PlayerController __instance)
{
RunManager instance = RunManager.instance;
if (!((Object)(object)instance == (Object)null) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelMainMenu) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobbyMenu) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelLobby) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelShop) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelRecording) && !((Object)(object)instance.levelCurrent == (Object)(object)instance.levelSplashScreen) && (!GameManager.Multiplayer() || PhotonNetwork.IsMasterClient) && (Object)(object)__instance != (Object)null)
{
GameObject gameObject = ((Component)__instance).gameObject;
if ((Object)(object)gameObject.GetComponent<RegenUpdater>() == (Object)null)
{
gameObject.AddComponent<RegenUpdater>().Init();
TeamHealsPlugin.Log.LogInfo((object)("RegenUpdater attached to " + ((Object)__instance).name + "."));
}
}
}
}
internal static class ItemHealthPackPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ItemHealthPack), "Start")]
private static void OverrideHealAmount(ItemHealthPack __instance)
{
float num = (float)Math.Round(Configuration.HealAmountMultiplier.Value, 4);
if (num == 1f)
{
TeamHealsPlugin.Log.LogInfo((object)"Heal amount multiplier is 1. No changes made to health pack heal amount.");
}
else
{
__instance.healAmount = (int)Math.Ceiling((float)__instance.healAmount * num);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(ItemAttributes), "Start")]
private static void OverrideHealthPackName(ItemAttributes __instance)
{
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_001d: Invalid comparison between Unknown and I4
if (!((Object)(object)__instance.item != (Object)null) || (int)__instance.item.itemType != 8)
{
return;
}
ItemHealthPack component = ((Component)__instance).GetComponent<ItemHealthPack>();
if ((Object)(object)component != (Object)null)
{
int healAmount = component.healAmount;
FieldInfo field = typeof(ItemAttributes).GetField("itemName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (field != null)
{
string text = (string)field.GetValue(__instance);
if (!string.IsNullOrEmpty(text))
{
string text2 = Regex.Replace(text, "\\(\\d+\\)", $"({healAmount})");
field.SetValue(__instance, text2);
TeamHealsPlugin.Log.LogInfo((object)("Health pack name changed from '" + text + "' to '" + text2 + "'"));
}
else
{
TeamHealsPlugin.Log.LogWarning((object)"itemName value is null or empty in ItemAttributes.Start postfix.");
}
}
else
{
TeamHealsPlugin.Log.LogWarning((object)"itemName field not found in ItemAttributes.");
}
}
else
{
TeamHealsPlugin.Log.LogWarning((object)"ItemHealthPack component not found on ItemAttributes.");
}
}
private static void TeamHealthPackSync(ItemHealthPack __instance)
{
ItemToggle val = TeamHealsPlugin.item_toggle_ref.Invoke(__instance);
PlayerAvatar val2 = SemiFunc.PlayerAvatarGetFromPhotonID(TeamHealsPlugin.player_photon_id_ref.Invoke(val));
List<PlayerAvatar> list = SemiFunc.PlayerGetAll();
foreach (PlayerAvatar item in list)
{
if (item.photonView.ViewID != val2.photonView.ViewID)
{
if (Configuration.EnableEqualSplitTeamHealth.Value)
{
item.playerHealth.HealOther(__instance.healAmount / list.Count, true);
}
else
{
item.playerHealth.HealOther(__instance.healAmount, true);
}
TeamHealsPlugin.Log.LogInfo((object)($"Healed player {item.photonView.ViewID} for {__instance.healAmount} HP " + $"(used by {val2.photonView.ViewID})"));
}
}
TeamHealsPlugin.Log.LogInfo((object)$"Health pack used by player {val2.photonView.ViewID} healed all teammates");
}
}
internal static class PlayerReviveHealthPatch
{
[HarmonyPatch(typeof(PlayerAvatar), "ReviveRPC")]
[HarmonyPostfix]
private static void ReviveRPC_Postfix(PlayerAvatar __instance)
{
int num = Configuration.CustomReviveHealthAmount.Value - 1;
if (__instance.photonView.IsMine)
{
if (Configuration.EnableFullReviveHealth.Value)
{
int num2 = TeamHealsPlugin.health_ref.Invoke(__instance.playerHealth);
num = TeamHealsPlugin.max_health_ref.Invoke(__instance.playerHealth) - num2;
}
__instance.playerHealth.HealOther(num, true);
TeamHealsPlugin.Log.LogInfo((object)$"Player revived with custom health: {num}");
}
}
}
[HarmonyPatch(typeof(PunManager), "ShopPopulateItemVolumes")]
internal class PunManagerPatch
{
private static readonly FieldRef<PunManager, ShopManager> shop_manager_ref = AccessTools.FieldRefAccess<PunManager, ShopManager>("shopManager");
public static bool Prefix(PunManager __instance)
{
List<Item> list = new HashSet<Item>(shop_manager_ref.Invoke(__instance).potentialItemHealthPacks).Distinct().ToList();
for (int i = 0; i < list.Count; i++)
{
string itemName = Regex.Replace(list[i].itemName, "\\d+", delegate(Match match)
{
int num = int.Parse(match.Value);
return ((int)Math.Ceiling((float)num * Configuration.HealAmountMultiplier.Value)).ToString();
});
list[i].itemName = itemName;
}
return true;
}
}
}
namespace BetterHeals.Config
{
internal class Configuration
{
public static ConfigEntry<bool> EnableFullHealthAtStartPatch;
public static ConfigEntry<bool> EnableTeamHealthPackPatch;
public static ConfigEntry<bool> EnableEqualSplitTeamHealth;
public static ConfigEntry<bool> EnableHealthRegenPatch;
public static ConfigEntry<int> HealthRegenAmount;
public static ConfigEntry<int> CustomRegenIntervalAmount;
public static ConfigEntry<float> HealAmountMultiplier;
public static ConfigEntry<bool> EnableCustomReviveHealthPatch;
public static ConfigEntry<int> CustomReviveHealthAmount;
public static ConfigEntry<bool> EnableFullReviveHealth;
public static ConfigEntry<bool> EnableExtractionHealPatch;
public static ConfigEntry<bool> EnableExtractionFullHeal;
public static ConfigEntry<int> ExtractionHealAmount;
public static void Init(ConfigFile config)
{
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
//IL_0033: Expected O, but got Unknown
//IL_008d: Unknown result type (might be due to invalid IL or missing references)
//IL_0097: Expected O, but got Unknown
//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
//IL_00c5: Expected O, but got Unknown
//IL_0122: Unknown result type (might be due to invalid IL or missing references)
//IL_012c: Expected O, but got Unknown
//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
//IL_01c6: Expected O, but got Unknown
HealAmountMultiplier = config.Bind<float>("General Health Settings", "HealthPackAmountMultiplier", 1f, new ConfigDescription("Multiplier applied to the health pack healing", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 10f), Array.Empty<object>()));
EnableFullHealthAtStartPatch = config.Bind<bool>("General Health Settings", "EnableFullHealthAtStart", false, "If enabled, players will start the match with full health");
EnableHealthRegenPatch = config.Bind<bool>("General Health Settings", "EnableHealthRegen", false, "If enabled, players will slowly regenerate health over time");
HealthRegenAmount = config.Bind<int>("General Health Settings", "HealthRegenAmount", 10, new ConfigDescription("The amount of health players will regenerate each interval if the Health Regeneration patch is enabled", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 20), Array.Empty<object>()));
CustomRegenIntervalAmount = config.Bind<int>("General Health Settings", "CustomRegenIntervalAmount", 60, new ConfigDescription("The interval in seconds at which players will regenerate health if the Health Regeneration patch is enabled", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 120), Array.Empty<object>()));
EnableExtractionHealPatch = config.Bind<bool>("General Health Settings", "EnableExtractionHeal", true, "Enable the patch that heals alive players after extraction is completed");
EnableExtractionFullHeal = config.Bind<bool>("General Health Settings", "ExtractionFullHeal", false, "If enabled, players will be healed to full health upon extraction regardless of the ExtractionHealAmount setting");
ExtractionHealAmount = config.Bind<int>("General Health Settings", "ExtractionHealAmount", 20, new ConfigDescription("The health that alive players receive after extraction is completed", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 200), Array.Empty<object>()));
EnableTeamHealthPackPatch = config.Bind<bool>("Team Health Settings", "EnableTeamHealthPack", true, "Enable the patch that allows health packs to heal all teammates");
EnableEqualSplitTeamHealth = config.Bind<bool>("Team Health Settings", "EnableEqualSplitTeamHealth", false, "If enabled, health packs will split their healing amount equally among all teammates");
EnableCustomReviveHealthPatch = config.Bind<bool>("Team Health Settings", "EnableCustomReviveHealth", true, "Enable the patch that sets a custom value to the health received when a player is revived");
EnableFullReviveHealth = config.Bind<bool>("Team Health Settings", "FullReviveHealth", false, "If enabled, players will be revived with full health regardless of the CustomReviveHealthAmount setting");
CustomReviveHealthAmount = config.Bind<int>("Team Health Settings", "CustomReviveHealthAmount", 20, new ConfigDescription("The health that a player receives upon revival", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 100), Array.Empty<object>()));
}
}
}