Decompiled source of SoulboundNoKill v0.1.0

SoulboundNoKill.dll

Decompiled 19 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using RoR2;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("SoulboundNoKill")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("SoulboundNoKill")]
[assembly: AssemblyTitle("SoulboundNoKill")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace SoulboundNoKill;

[BepInPlugin("tesv0.SoulboundNoKill", "SoulboundNoKill", "0.1.0")]
public class SoulboundNoKillPlugin : BaseUnityPlugin
{
	public const string Guid = "tesv0.SoulboundNoKill";

	public const string Name = "SoulboundNoKill";

	public const string Version = "0.1.0";

	internal static ManualLogSource Log;

	internal static ConfigEntry<bool> Enabled;

	internal static ConfigEntry<string> SceneMatch;

	internal static ConfigEntry<float> FallDepth;

	internal static ConfigEntry<float> InvulnSeconds;

	internal static ConfigEntry<bool> InterceptMapZones;

	internal static ConfigEntry<bool> InterceptSuicide;

	internal static ConfigEntry<bool> InterceptEnvironmentDamage;

	internal static ConfigEntry<bool> InterceptTrueKill;

	internal static ConfigEntry<bool> DebugLogging;

	private static readonly Dictionary<CharacterBody, Vector3> lastSafePosition = new Dictionary<CharacterBody, Vector3>();

	private static readonly Dictionary<CharacterBody, float> lastRescueTime = new Dictionary<CharacterBody, float>();

	private static bool inTargetScene;

	private static bool Active
	{
		get
		{
			if (Enabled.Value && inTargetScene)
			{
				return NetworkServer.active;
			}
			return false;
		}
	}

	private void Awake()
	{
		//IL_013c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0141: Unknown result type (might be due to invalid IL or missing references)
		//IL_015b: Expected O, but got Unknown
		//IL_015b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0175: Expected O, but got Unknown
		//IL_0175: Unknown result type (might be due to invalid IL or missing references)
		//IL_018f: Expected O, but got Unknown
		//IL_01a8: Expected O, but got Unknown
		Log = ((BaseUnityPlugin)this).Logger;
		Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Turn the mod on or off.");
		SceneMatch = ((BaseUnityPlugin)this).Config.Bind<string>("General", "SceneMatch", "soulbound", "Comma-separated, case-insensitive substrings. The mod is active on a stage whose internal name, name token or English name contains any of them. The stage name is written to the log on every stage load.");
		FallDepth = ((BaseUnityPlugin)this).Config.Bind<float>("Detection", "FallDepth", 8f, "A death only counts as a pit death if the player is this many meters below the last ground they stood on. Protects normal deaths (enemies, timers) from being cancelled.");
		InvulnSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Rescue", "InvulnerabilitySeconds", 2f, "Seconds of invulnerability after being put back on the ground.");
		InterceptMapZones = ((BaseUnityPlugin)this).Config.Bind<bool>("Detection", "InterceptKillZones", true, "Intercept map trigger zones of a type other than the vanilla OutOfBounds/KickOutPlayers (a new kill zone type) and treat them as out of bounds.");
		InterceptSuicide = ((BaseUnityPlugin)this).Config.Bind<bool>("Detection", "InterceptSuicide", true, "Intercept HealthComponent.Suicide on a falling player.");
		InterceptEnvironmentDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Detection", "InterceptEnvironmentDamage", true, "Intercept lethal damage that does not come from an enemy (no attacker body) on a falling player.");
		InterceptTrueKill = ((BaseUnityPlugin)this).Config.Bind<bool>("Detection", "InterceptTrueKill", true, "Intercept CharacterMaster.TrueKill on a falling player.");
		DebugLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "VerboseLogging", true, "Log every player death in the target stage with its details. Send this log to the mod author if the pit still kills you.");
		Harmony val = new Harmony("tesv0.SoulboundNoKill");
		TryPatch(val, typeof(MapZone), "TryZoneStart", "MapZoneTryZoneStartPrefix");
		TryPatch(val, typeof(HealthComponent), "Suicide", "SuicidePrefix");
		TryPatch(val, typeof(HealthComponent), "TakeDamage", "TakeDamagePrefix");
		TryPatch(val, typeof(CharacterMaster), "TrueKill", "TrueKillPrefix");
		SceneCatalog.onMostRecentSceneDefChanged += OnSceneChanged;
	}

	private static void TryPatch(Harmony harmony, Type type, string methodName, string prefixName)
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0018: Expected O, but got Unknown
		HarmonyMethod val = new HarmonyMethod(typeof(SoulboundNoKillPlugin).GetMethod(prefixName, BindingFlags.Static | BindingFlags.NonPublic));
		int num = 0;
		MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
		foreach (MethodInfo methodInfo in methods)
		{
			if (!(methodInfo.Name != methodName))
			{
				try
				{
					harmony.Patch((MethodBase)methodInfo, val, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
					num++;
				}
				catch (Exception ex)
				{
					Log.LogWarning((object)("Could not patch " + type.Name + "." + methodName + ": " + ex.Message));
				}
			}
		}
		if (num == 0)
		{
			Log.LogWarning((object)(type.Name + "." + methodName + " not found; that detection path is off."));
		}
	}

	private static void OnSceneChanged(SceneDef sceneDef)
	{
		lastSafePosition.Clear();
		lastRescueTime.Clear();
		inTargetScene = false;
		if (!Object.op_Implicit((Object)(object)sceneDef))
		{
			return;
		}
		string text = "";
		try
		{
			text = Language.GetString(sceneDef.nameToken, "en") ?? "";
		}
		catch
		{
		}
		string text2 = (sceneDef.cachedName + "|" + sceneDef.nameToken + "|" + text).ToLowerInvariant();
		string[] array = SceneMatch.Value.Split(',');
		for (int i = 0; i < array.Length; i++)
		{
			string text3 = array[i].Trim().ToLowerInvariant();
			if (text3.Length > 0 && text2.Contains(text3))
			{
				inTargetScene = true;
			}
		}
		Log.LogInfo((object)("Stage loaded: name='" + sceneDef.cachedName + "' token='" + sceneDef.nameToken + "' english='" + text + "' -> mod " + (inTargetScene ? "ACTIVE" : "idle")));
	}

	private void FixedUpdate()
	{
		//IL_0069: Unknown result type (might be due to invalid IL or missing references)
		if (!Active)
		{
			return;
		}
		foreach (PlayerCharacterMasterController instance in PlayerCharacterMasterController.instances)
		{
			CharacterBody val = ((Object.op_Implicit((Object)(object)instance) && Object.op_Implicit((Object)(object)instance.master)) ? instance.master.GetBody() : null);
			if (Object.op_Implicit((Object)(object)val) && Object.op_Implicit((Object)(object)val.characterMotor) && val.characterMotor.isGrounded)
			{
				lastSafePosition[val] = val.footPosition;
			}
		}
	}

	private static bool IsPlayer(CharacterBody body)
	{
		if (Object.op_Implicit((Object)(object)body) && Object.op_Implicit((Object)(object)body.master))
		{
			return Object.op_Implicit((Object)(object)body.master.playerCharacterMasterController);
		}
		return false;
	}

	private static bool IsFallingIntoPit(CharacterBody body)
	{
		//IL_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		if (Object.op_Implicit((Object)(object)body.characterMotor) && body.characterMotor.isGrounded)
		{
			return false;
		}
		if (!lastSafePosition.TryGetValue(body, out var value))
		{
			return false;
		}
		return body.footPosition.y < value.y - FallDepth.Value;
	}

	private static void LogDeath(string source, CharacterBody body, string details)
	{
		//IL_0057: 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)
		if (DebugLogging.Value)
		{
			lastSafePosition.TryGetValue(body, out var value);
			bool flag = Object.op_Implicit((Object)(object)body.characterMotor) && body.characterMotor.isGrounded;
			Log.LogInfo((object)$"[{source}] player={body.GetDisplayName()} pos={body.footPosition} lastGround={value} grounded={flag} {details}");
		}
	}

	private static bool Rescue(CharacterBody body, string source)
	{
		//IL_003f: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
		//IL_0097: Unknown result type (might be due to invalid IL or missing references)
		//IL_009c: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
		if (lastRescueTime.TryGetValue(body, out var value) && Time.time - value < 0.5f)
		{
			return true;
		}
		lastRescueTime[body] = Time.time;
		if (!lastSafePosition.TryGetValue(body, out var value2))
		{
			Vector3? val = TeleportHelper.FindSafeTeleportDestination(body.footPosition, body, RoR2Application.rng);
			if (!val.HasValue)
			{
				Log.LogWarning((object)("[" + source + "] No safe ground found for " + body.GetDisplayName() + "; letting the death happen."));
				return false;
			}
			value2 = val.Value;
		}
		if (Object.op_Implicit((Object)(object)body.characterMotor))
		{
			body.characterMotor.velocity = Vector3.zero;
		}
		TeleportHelper.TeleportBody(body, value2);
		if (InvulnSeconds.Value > 0f)
		{
			body.AddTimedBuff(Buffs.HiddenInvincibility, InvulnSeconds.Value);
		}
		Log.LogInfo((object)$"[{source}] Saved {body.GetDisplayName()} from the pit, moved to {value2}.");
		return true;
	}

	private static bool MapZoneTryZoneStartPrefix(MapZone __instance, Collider other)
	{
		if (!Active || !InterceptMapZones.Value || !Object.op_Implicit((Object)(object)other))
		{
			return true;
		}
		string text = ((object)Unsafe.As<ZoneType, ZoneType>(ref __instance.zoneType)/*cast due to .constrained prefix*/).ToString();
		if (text == "OutOfBounds" || text == "KickOutPlayers")
		{
			return true;
		}
		CharacterBody component = ((Component)other).GetComponent<CharacterBody>();
		if (!IsPlayer(component))
		{
			return true;
		}
		LogDeath("MapZone", component, "zoneType=" + text + " zone='" + ((Object)__instance).name + "'");
		return !Rescue(component, "MapZone");
	}

	private static bool SuicidePrefix(HealthComponent __instance)
	{
		if (!Active || !InterceptSuicide.Value)
		{
			return true;
		}
		CharacterBody body = __instance.body;
		if (!IsPlayer(body))
		{
			return true;
		}
		LogDeath("Suicide", body, "");
		if (!IsFallingIntoPit(body))
		{
			return true;
		}
		return !Rescue(body, "Suicide");
	}

	private static bool TakeDamagePrefix(HealthComponent __instance, DamageInfo damageInfo)
	{
		//IL_0073: Unknown result type (might be due to invalid IL or missing references)
		if (!Active || !InterceptEnvironmentDamage.Value || damageInfo == null)
		{
			return true;
		}
		CharacterBody body = __instance.body;
		if (!IsPlayer(body) || !__instance.alive)
		{
			return true;
		}
		if (damageInfo.damage < __instance.combinedHealth)
		{
			return true;
		}
		CharacterBody obj = (Object.op_Implicit((Object)(object)damageInfo.attacker) ? damageInfo.attacker.GetComponent<CharacterBody>() : null);
		LogDeath("TakeDamage", body, string.Format("damage={0} type={1} attacker='{2}' ", damageInfo.damage, damageInfo.damageType, Object.op_Implicit((Object)(object)damageInfo.attacker) ? ((Object)damageInfo.attacker).name : "none") + "inflictor='" + (Object.op_Implicit((Object)(object)damageInfo.inflictor) ? ((Object)damageInfo.inflictor).name : "none") + "'");
		if (Object.op_Implicit((Object)(object)obj))
		{
			return true;
		}
		if (!IsFallingIntoPit(body))
		{
			return true;
		}
		return !Rescue(body, "TakeDamage");
	}

	private static bool TrueKillPrefix(CharacterMaster __instance)
	{
		if (!Active || !InterceptTrueKill.Value)
		{
			return true;
		}
		CharacterBody body = __instance.GetBody();
		if (!IsPlayer(body))
		{
			return true;
		}
		LogDeath("TrueKill", body, "");
		if (!IsFallingIntoPit(body))
		{
			return true;
		}
		return !Rescue(body, "TrueKill");
	}
}