Decompiled source of TrophyPity v1.0.1

TrophyPity.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[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("TrophyPity")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("TrophyPity")]
[assembly: AssemblyTitle("TrophyPity")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace Gafteds.TrophyPity
{
	[BepInPlugin("com.gafteds.trophypity", "TrophyPity", "0.2.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(Character), "OnDeath")]
		private static class DeathPatch
		{
			private static void Prefix(Character __instance)
			{
				if ((Object)(object)Instance == (Object)null || !Instance.Enabled.Value || (Object)(object)__instance == (Object)null || __instance.IsPlayer())
				{
					return;
				}
				try
				{
					HitData val = (HitData)((LastHitField == null) ? null : /*isinst with value type is only supported in some contexts*/);
					Player val2 = (Player)((val == null) ? null : /*isinst with value type is only supported in some contexts*/);
					lock (Sync)
					{
						if ((Object)(object)val2 != (Object)null)
						{
							Killers[((Object)__instance).GetInstanceID()] = val2;
						}
					}
					Log("[Death] creature=" + ((Object)((Component)__instance).gameObject).name + " killer=" + (((Object)(object)val2 == (Object)null) ? "unattributed" : val2.GetPlayerName()));
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)Instance).Logger.LogWarning((object)("Attribution error: " + ex.Message));
				}
			}
		}

		[HarmonyPatch(typeof(CharacterDrop), "GenerateDropList")]
		private static class LootPatch
		{
			private static void Postfix(CharacterDrop __instance, List<KeyValuePair<GameObject, int>> __result)
			{
				if ((Object)(object)Instance == (Object)null || !Instance.Enabled.Value || (Object)(object)__instance == (Object)null || __result == null)
				{
					return;
				}
				try
				{
					Character component = ((Component)__instance).GetComponent<Character>();
					if ((Object)(object)component == (Object)null || component.IsPlayer() || IsBoss(component))
					{
						return;
					}
					ZNetView component2 = ((Component)component).GetComponent<ZNetView>();
					if ((Object)(object)component2 == (Object)null || !component2.IsValid() || !component2.IsOwner())
					{
						return;
					}
					int instanceID = ((Object)component).GetInstanceID();
					lock (Sync)
					{
						if (!Processed.Add(instanceID))
						{
							Log("[Skip] repeated loot generation " + ((Object)((Component)component).gameObject).name);
							return;
						}
						if (!Killers.TryGetValue(instanceID, out var value) || (Object)(object)value == (Object)null)
						{
							Log("[Skip] no reliable killing-blow player for " + ((Object)((Component)component).gameObject).name);
							return;
						}
						string worldKey = GetWorldKey();
						if (string.IsNullOrEmpty(worldKey))
						{
							Log("[Skip] world UID unavailable");
							return;
						}
						long playerID = value.GetPlayerID();
						if (playerID == 0L)
						{
							Log("[Skip] player ID unavailable");
							return;
						}
						EnsureLoaded(worldKey);
						bool flag = false;
						foreach (Drop drop in __instance.m_drops)
						{
							if (drop == null || (Object)(object)drop.m_prefab == (Object)null || !((Object)drop.m_prefab).name.StartsWith("Trophy", StringComparison.OrdinalIgnoreCase))
							{
								continue;
							}
							int num = ((Math.Abs(drop.m_chance - 0.05f) < 0.0001f) ? Instance.FivePercentKills.Value : ((Math.Abs(drop.m_chance - 0.1f) < 0.0001f) ? Instance.TenPercentKills.Value : 0));
							if (num >= 1 && !drop.m_levelMultiplier)
							{
								string key = playerID.ToString(CultureInfo.InvariantCulture) + "|" + ((Object)((Component)component).gameObject).name.Replace("(Clone)", "") + "|" + ((Object)drop.m_prefab).name;
								Counters.TryGetValue(key, out var value2);
								if (__result.Any((KeyValuePair<GameObject, int> x) => (Object)(object)x.Key == (Object)(object)drop.m_prefab && x.Value > 0))
								{
									Counters.Remove(key);
									flag = true;
									Log("[Natural] " + value.GetPlayerName() + " " + ((Object)drop.m_prefab).name + " reset from " + value2);
								}
								else if (value2 + 1 >= num)
								{
									__result.Add(new KeyValuePair<GameObject, int>(drop.m_prefab, 1));
									Counters.Remove(key);
									flag = true;
									Log("[Guaranteed] " + value.GetPlayerName() + " " + ((Object)drop.m_prefab).name + " at kill " + num);
								}
								else
								{
									Counters[key] = value2 + 1;
									flag = true;
									Log("[Progress] " + value.GetPlayerName() + " " + ((Object)drop.m_prefab).name + " " + (value2 + 1) + "/" + num);
								}
							}
						}
						if (flag)
						{
							Save(worldKey);
						}
						Killers.Remove(instanceID);
					}
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)Instance).Logger.LogError((object)("Pity loot error: " + ex));
				}
			}
		}

		internal static Plugin Instance;

		internal ConfigEntry<bool> Enabled;

		internal ConfigEntry<bool> Diagnostics;

		internal ConfigEntry<int> TenPercentKills;

		internal ConfigEntry<int> FivePercentKills;

		private Harmony harmony;

		private static readonly Dictionary<int, Player> Killers = new Dictionary<int, Player>();

		private static readonly HashSet<int> Processed = new HashSet<int>();

		private static readonly FieldInfo LastHitField = AccessTools.Field(typeof(Character), "m_lastHit");

		private static readonly FieldInfo BossField = AccessTools.Field(typeof(Character), "m_boss");

		private static readonly MethodInfo WorldUidMethod = AccessTools.Method(typeof(ZNet), "GetWorldUID", (Type[])null, (Type[])null);

		private static readonly Dictionary<string, int> Counters = new Dictionary<string, int>();

		private static string loadedWorld;

		private static readonly object Sync = new object();

		private void Awake()
		{
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Expected O, but got Unknown
			Instance = this;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("TrophyPity", "Enabled", true, "Enable pity rewards (local testing build; dedicated-server-only operation unverified).");
			Diagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("TrophyPity", "Diagnostics", true, "Log trophy rolls, counters, and guarantees.");
			TenPercentKills = ((BaseUnityPlugin)this).Config.Bind<int>("TrophyPity", "TenPercentKills", 12, "Kill threshold for exactly 10% trophies.");
			FivePercentKills = ((BaseUnityPlugin)this).Config.Bind<int>("TrophyPity", "FivePercentKills", 20, "Kill threshold for exactly 5% trophies.");
			harmony = new Harmony("com.gafteds.trophypity");
			harmony.PatchAll();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"TrophyPity v0.2.0 LOCAL TEST BUILD: pity enabled; server-only compatibility NOT verified.");
			if (LastHitField == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Cannot read Character.m_lastHit: unattributed kills will be skipped.");
			}
			if (WorldUidMethod == null)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Cannot read world UID: pity progress will be skipped to prevent cross-world mixing.");
			}
		}

		private void OnDestroy()
		{
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			lock (Sync)
			{
				Killers.Clear();
				Processed.Clear();
				Counters.Clear();
				loadedWorld = null;
			}
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		private static bool IsBoss(Character creature)
		{
			if (BossField != null)
			{
				object value = BossField.GetValue(creature);
				if (value is bool)
				{
					return (bool)value;
				}
			}
			string name = ((Object)((Component)creature).gameObject).name;
			return new string[7] { "Eikthyr", "gd_king", "Bonemass", "Dragon", "GoblinKing", "SeekerQueen", "Fader" }.Any((string s) => name.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0);
		}

		private static string GetWorldKey()
		{
			if ((Object)(object)ZNet.instance == (Object)null || WorldUidMethod == null)
			{
				return null;
			}
			try
			{
				object obj = WorldUidMethod.Invoke(ZNet.instance, null);
				return (obj == null) ? null : Convert.ToString(obj, CultureInfo.InvariantCulture);
			}
			catch
			{
				return null;
			}
		}

		private static string FileForWorld(string world)
		{
			string text = new string(world.Where((char c) => char.IsLetterOrDigit(c) || c == '_' || c == '-').ToArray());
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			return Path.Combine(Paths.ConfigPath, "Gafteds.TrophyPity." + text + ".progress.tsv");
		}

		private static void EnsureLoaded(string world)
		{
			if (loadedWorld == world)
			{
				return;
			}
			Counters.Clear();
			loadedWorld = world;
			string text = FileForWorld(world);
			if (text == null || !File.Exists(text))
			{
				return;
			}
			try
			{
				string[] array = File.ReadAllLines(text);
				for (int i = 0; i < array.Length; i++)
				{
					string[] array2 = array[i].Split('\t');
					if (array2.Length == 2 && int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result > 0)
					{
						string key = Encoding.UTF8.GetString(Convert.FromBase64String(array2[0]));
						Counters[key] = result;
					}
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)Instance).Logger.LogWarning((object)("Unable to load pity progress: " + ex.Message));
			}
		}

		private static void Save(string world)
		{
			string text = FileForWorld(world);
			if (text == null)
			{
				return;
			}
			string text2 = text + ".tmp";
			try
			{
				File.WriteAllLines(text2, Counters.Select((KeyValuePair<string, int> kv) => Convert.ToBase64String(Encoding.UTF8.GetBytes(kv.Key)) + "\t" + kv.Value.ToString(CultureInfo.InvariantCulture)).ToArray());
				if (File.Exists(text))
				{
					File.Delete(text);
				}
				File.Move(text2, text);
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)Instance).Logger.LogError((object)("Could not save pity progress: " + ex.Message));
			}
		}

		private static void Log(string message)
		{
			if ((Object)(object)Instance != (Object)null && Instance.Diagnostics.Value)
			{
				((BaseUnityPlugin)Instance).Logger.LogInfo((object)message);
			}
		}
	}
}