using System; using System.Collections; 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.InteropServices; using System.Runtime.Versioning; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Jotunn; using Jotunn.Entities; using Jotunn.Extensions; using Jotunn.Managers; using Jotunn.Utils; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("PvpOverhaul")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PvpOverhaul")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("e3243d22-4307-4008-ba36-9f326008cde5")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] namespace PvpOverhaul; internal class BountySystem { private class KillRecord { public string KillerId; public string VictimId; public DateTime TimestampUtc; } public class BountyState { public string PlayerId; public DateTime StartUtc; public DateTime EndUtc; public int Tier; public Vector3 LastKnownPosition; } public class LastHit { public string VictimName; public string AttackerName; public float Damage; public float Time; } [CompilerGenerated] private sealed class d__51 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Player victim; public BountySystem <>4__this; private string 5__1; private long 5__2; private int 5__3; private LastHit 5__4; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__51(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; 5__4 = null; <>1__state = -2; } private bool MoveNext() { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; if ((Object)(object)victim == (Object)null) { return false; } 5__1 = victim.GetPlayerName(); 5__2 = victim.GetPlayerID(); 5__3 = 0; break; case 1: <>1__state = -1; if ((Object)(object)victim == (Object)null) { return false; } 5__4 = <>4__this.ResolveLastHit(5__2); if (5__3 == 0 || ((Character)victim).IsDead() || ((Character)victim).GetHealth() <= 0f) { Logger.LogInfo((object)($"[PvPOverhaul BOUNTY] DelayedDeathCheck {5__3} victim={5__1} " + $"health={((Character)victim).GetHealth()} dead={((Character)victim).IsDead()} " + "lastHit=" + ((5__4 != null) ? 5__4.AttackerName : "NULL"))); } if (((Character)victim).GetHealth() <= 0f || ((Character)victim).IsDead()) { <>4__this.TryReportDeath(victim); return false; } 5__4 = null; 5__3++; break; } if (5__3 < 30) { <>2__current = (object)new WaitForSeconds(0.1f); <>1__state = 1; return true; } 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 readonly List _kills = new List(); private readonly Dictionary _activeBounties = new Dictionary(); private readonly Dictionary _lastHits = new Dictionary(); private readonly Dictionary _lastHitsByName = new Dictionary(); private readonly Dictionary _reportedDeaths = new Dictionary(); private readonly Dictionary _ignoredDeaths = new Dictionary(); private bool _loadedPersistence; private float _nextTimerTick; private float _nextPositionSync; private float _nextFullSync; public static BountySystem Instance { get; } = new BountySystem(); private string SaveDir => Path.Combine(Paths.ConfigPath, "PvPOverhaul"); private string SaveFile => Path.Combine(SaveDir, "bounties.txt"); private string LogDir => Path.Combine(Paths.ConfigPath, "PvPOverhaul", "Logs"); private TimeSpan KillWindow => TimeSpan.FromMinutes(PvPOverhaul.BountyKillWindowMinutes.Value); private TimeSpan BountyDuration => TimeSpan.FromMinutes(PvPOverhaul.BountyDurationMinutes.Value); public void Update() { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!_loadedPersistence) { _loadedPersistence = true; LoadBounties(); } if (Time.time >= _nextTimerTick) { _nextTimerTick = Time.time + 1f; UpdateTimers(); } if (Time.time >= _nextPositionSync) { _nextPositionSync = Time.time + 10f; SyncBountyPositions(); } if (Time.time >= _nextFullSync) { _nextFullSync = Time.time + 30f; BroadcastAllBounties(); } } } private void WriteServerLog(string message) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { Directory.CreateDirectory(LogDir); string path = Path.Combine(LogDir, $"{DateTime.UtcNow:yyyy-MM-dd}.log"); string text = $"[{DateTime.UtcNow:HH:mm:ss} UTC] {message}"; File.AppendAllText(path, text + Environment.NewLine); } } private bool IsPlayerOnline(string playerName) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null) { return false; } foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { if (player.m_name == playerName) { return true; } } return false; } private static string FormatMessage(string template, string killer = "", string victim = "", string player = "", int tier = 0, int duration = 0, int amount = 0, string item = "") { return template.Replace("{killer}", killer).Replace("{victim}", victim).Replace("{player}", player) .Replace("{tier}", tier.ToString()) .Replace("{duration}", duration.ToString()) .Replace("{amount}", amount.ToString()) .Replace("{item}", item); } public void RegisterKill(string killerName, string victimName) { if (string.IsNullOrWhiteSpace(killerName) || string.IsNullOrWhiteSpace(victimName) || killerName == victimName) { return; } DateTime now = DateTime.UtcNow; BountyState bounty = GetBounty(victimName); ClearBounty(victimName, killed: true); _kills.RemoveAll((KillRecord x) => x.KillerId == victimName); SaveBounties(); if (bounty != null) { int num = Math.Max(1, bounty.Tier) * PvPOverhaul.BountyRewardAmountPerTier.Value; string value = PvPOverhaul.BountyRewardItem.Value; BountyRpc.SendRewardToPlayer(killerName, value, num); _kills.RemoveAll((KillRecord x) => x.KillerId == killerName); string message = FormatMessage(PvPOverhaul.MsgBountyReward.Value, killerName, victimName, "", 0, 0, num, value); BountyRpc.BroadcastCenterMessage(message); WriteServerLog($"REWARD | {killerName} claimed bounty on {victimName} | tier={bounty.Tier} | {num}x {value}"); } _kills.Add(new KillRecord { KillerId = killerName, VictimId = victimName, TimestampUtc = now }); CleanupOldKills(now); int num2 = _kills.Count((KillRecord x) => x.KillerId == killerName && now - x.TimestampUtc <= KillWindow); Logger.LogInfo((object)$"[PvPOverhaul SERVER] Kill registered: {killerName} -> {victimName} | kills={num2}"); WriteServerLog($"KILL | {killerName} killed {victimName} | kills={num2}"); BountyRpc.BroadcastCenterMessage(FormatMessage(PvPOverhaul.MsgKillBroadcast.Value, killerName, victimName)); if (bounty == null && num2 >= PvPOverhaul.BountyTriggerKillCount.Value) { ActivateOrRefreshBounty(killerName, num2, now); } } private int GetTierFromKills(int kills) { return (int)Math.Floor(Math.Sqrt(kills)); } private void ActivateOrRefreshBounty(string playerName, int killCount, DateTime now) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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_00de: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) int tierFromKills = GetTierFromKills(killCount); DateTime dateTime = now.Add(BountyDuration); Vector3 playerPosition = GetPlayerPosition(playerName); if (!_activeBounties.TryGetValue(playerName, out var value)) { value = new BountyState { PlayerId = playerName, StartUtc = now, EndUtc = dateTime, Tier = tierFromKills, LastKnownPosition = playerPosition }; _activeBounties[playerName] = value; Logger.LogInfo((object)("[PvPOverhaul SERVER] BOUNTY ACTIVATED: " + playerName)); WriteServerLog($"BOUNTY_ACTIVATED | {playerName} | tier={tierFromKills} | end={dateTime:o}"); BountyRpc.BroadcastCenterMessage(FormatMessage(PvPOverhaul.MsgBountyActivated.Value, "", "", playerName, tierFromKills)); } else { value.EndUtc = dateTime; value.Tier = tierFromKills; if (playerPosition != Vector3.zero) { value.LastKnownPosition = playerPosition; } Logger.LogInfo((object)("[PvPOverhaul SERVER] BOUNTY REFRESHED: " + playerName)); WriteServerLog($"BOUNTY_REFRESHED | {playerName} | tier={tierFromKills} | end={dateTime:o}"); BountyRpc.BroadcastCenterMessage(FormatMessage(PvPOverhaul.MsgBountyRefreshed.Value, "", "", playerName, tierFromKills, PvPOverhaul.BountyDurationMinutes.Value)); } SaveBounties(); if (IsPlayerOnline(playerName)) { BountyRpc.BroadcastBountyCircle(playerName, active: true, value.LastKnownPosition, tierFromKills, dateTime); } } public void UpdateBountyPositionFromClient(string playerName, Vector3 position) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (_activeBounties.TryGetValue(playerName, out var value) && !(position == Vector3.zero)) { value.LastKnownPosition = position; SaveBounties(); BountyRpc.BroadcastBountyCircle(playerName, active: true, position, value.Tier, value.EndUtc); } } public void ClearBounty(string playerName, bool killed) { //IL_00a6: Unknown result type (might be due to invalid IL or missing references) if (_activeBounties.Remove(playerName)) { Logger.LogInfo((object)("[PvPOverhaul SERVER] Bounty cleared: " + playerName)); WriteServerLog($"BOUNTY_CLEARED | {playerName} | killed={killed}"); if (killed) { BountyRpc.BroadcastCenterMessage(FormatMessage(PvPOverhaul.MsgBountyCleared.Value, "", "", playerName)); } else { BountyRpc.BroadcastCenterMessage(FormatMessage(PvPOverhaul.MsgBountyExpired.Value, "", "", playerName)); } SaveBounties(); BountyRpc.BroadcastBountyCircle(playerName, active: false, Vector3.zero, 0, DateTime.UtcNow); } } public void ResetPlayer(string playerName) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) _activeBounties.Remove(playerName); _kills.RemoveAll((KillRecord x) => x.KillerId == playerName || x.VictimId == playerName); Logger.LogInfo((object)("[PvPOverhaul SERVER] Full reset: " + playerName)); WriteServerLog("ADMIN_RESET | " + playerName); SaveBounties(); BountyRpc.BroadcastBountyCircle(playerName, active: false, Vector3.zero, 0, DateTime.UtcNow); } public BountyState GetBounty(string playerName) { if (!_activeBounties.TryGetValue(playerName, out var value)) { return null; } if (DateTime.UtcNow >= value.EndUtc) { ClearBounty(playerName, killed: false); return null; } return value; } private void UpdateTimers() { DateTime now = DateTime.UtcNow; foreach (string item in (from x in _activeBounties where now >= x.Value.EndUtc select x.Key).ToList()) { ClearBounty(item, killed: false); } } private void SyncBountyPositions() { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) DateTime utcNow = DateTime.UtcNow; foreach (BountyState item in _activeBounties.Values.ToList()) { if (!(utcNow >= item.EndUtc)) { if (!IsPlayerOnline(item.PlayerId)) { BountyRpc.BroadcastBountyCircle(item.PlayerId, active: false, Vector3.zero, item.Tier, item.EndUtc); } else { BountyRpc.BroadcastBountyCircle(item.PlayerId, active: true, item.LastKnownPosition, item.Tier, item.EndUtc); } } } } private void BroadcastAllBounties() { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) DateTime utcNow = DateTime.UtcNow; foreach (BountyState item in _activeBounties.Values.ToList()) { if (!(utcNow >= item.EndUtc)) { if (!IsPlayerOnline(item.PlayerId)) { BountyRpc.BroadcastBountyCircle(item.PlayerId, active: false, Vector3.zero, item.Tier, item.EndUtc); } else { BountyRpc.BroadcastBountyCircle(item.PlayerId, active: true, item.LastKnownPosition, item.Tier, item.EndUtc); } } } } private Vector3 GetPlayerPosition(string playerName) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) foreach (Player allPlayer in Player.GetAllPlayers()) { if (allPlayer.GetPlayerName() == playerName) { return ((Component)allPlayer).transform.position; } } return Vector3.zero; } private void CleanupOldKills(DateTime now) { _kills.RemoveAll((KillRecord x) => now - x.TimestampUtc > KillWindow); } public void SetLastHit(long victimId, string victimName, string attackerName, float damage) { LastHit value = new LastHit { VictimName = victimName, AttackerName = attackerName, Damage = damage, Time = Time.time }; _lastHits[victimId] = value; if (!string.IsNullOrWhiteSpace(victimName)) { _lastHitsByName[victimName] = value; } Logger.LogInfo((object)$"[PvPOverhaul BOUNTY] LastHit set: victim={victimName} id={victimId} attacker={attackerName} dmg={damage}"); } public LastHit ResolveLastHit(long victimId) { if (!_lastHits.TryGetValue(victimId, out var value)) { return null; } if (Time.time - value.Time > 10f) { return null; } return value; } private LastHit ResolveLastHit(Player victim) { if ((Object)(object)victim == (Object)null) { return null; } long playerID = victim.GetPlayerID(); LastHit value = ResolveLastHit(playerID); if (value != null) { return value; } string playerName = victim.GetPlayerName(); if (string.IsNullOrWhiteSpace(playerName)) { return null; } if (!_lastHitsByName.TryGetValue(playerName, out value)) { return null; } if (Time.time - value.Time > 10f) { return null; } return value; } public void TryReportDeath(Player victim) { if ((Object)(object)victim == (Object)null) { return; } if (victim.m_godMode) { Logger.LogInfo((object)("[PvPOverhaul BOUNTY] Ignore death: godmode victim=" + victim.GetPlayerName())); return; } if (((Character)victim).GetHealth() > 0f && !((Character)victim).IsDead()) { Logger.LogInfo((object)$"[PvPOverhaul BOUNTY] Ignore death report: victim not dead yet {victim.GetPlayerName()} health={((Character)victim).GetHealth()} dead={((Character)victim).IsDead()}"); return; } if (RecentlyIgnoredDeath(victim)) { Logger.LogInfo((object)("[PvPOverhaul BOUNTY] Ignore death: recently ignored victim=" + victim.GetPlayerName())); return; } long playerID = victim.GetPlayerID(); if (!_reportedDeaths.TryGetValue(playerID, out var value) || !(Time.time - value < 10f)) { LastHit lastHit = ResolveLastHit(victim); Logger.LogInfo((object)($"[PvPOverhaul BOUNTY] TryReportDeath victim={victim.GetPlayerName()} id={playerID} " + "lastHit=" + ((lastHit != null) ? lastHit.AttackerName : "NULL") + " " + $"health={((Character)victim).GetHealth()} dead={((Character)victim).IsDead()}")); if (lastHit != null && !string.IsNullOrWhiteSpace(lastHit.AttackerName) && !(lastHit.AttackerName == victim.GetPlayerName())) { _reportedDeaths[playerID] = Time.time; Logger.LogInfo((object)("[PvPOverhaul CLIENT] Reporting REAL kill to server: " + lastHit.AttackerName + " -> " + victim.GetPlayerName())); BountyRpc.SendKillToServer(lastHit.AttackerName, victim.GetPlayerName()); } } } public void MarkIgnoredDeath(Player victim) { if (!((Object)(object)victim == (Object)null)) { _ignoredDeaths[victim.GetPlayerID()] = Time.time; } } private bool RecentlyIgnoredDeath(Player victim) { if ((Object)(object)victim == (Object)null) { return false; } if (!_ignoredDeaths.TryGetValue(victim.GetPlayerID(), out var value)) { return false; } return Time.time - value < 1f; } private void SaveBounties() { Directory.CreateDirectory(SaveDir); List list = new List(); foreach (BountyState value in _activeBounties.Values) { list.Add(string.Join("|", value.PlayerId, value.StartUtc.Ticks, value.EndUtc.Ticks, value.Tier, value.LastKnownPosition.x.ToString(CultureInfo.InvariantCulture), value.LastKnownPosition.y.ToString(CultureInfo.InvariantCulture), value.LastKnownPosition.z.ToString(CultureInfo.InvariantCulture))); } File.WriteAllLines(SaveFile, list); } private void LoadBounties() { //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) if (!File.Exists(SaveFile)) { return; } DateTime utcNow = DateTime.UtcNow; string[] array = File.ReadAllLines(SaveFile); foreach (string text in array) { string[] array2 = text.Split(new char[1] { '|' }); if (array2.Length < 7) { continue; } string text2 = array2[0]; if (!long.TryParse(array2[1], out var result) || !long.TryParse(array2[2], out var result2) || !int.TryParse(array2[3], out var result3) || !float.TryParse(array2[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result4) || !float.TryParse(array2[5], NumberStyles.Float, CultureInfo.InvariantCulture, out var result5) || !float.TryParse(array2[6], NumberStyles.Float, CultureInfo.InvariantCulture, out var result6)) { continue; } DateTime dateTime = new DateTime(result2, DateTimeKind.Utc); if (!(utcNow >= dateTime)) { BountyState bountyState = new BountyState { PlayerId = text2, StartUtc = new DateTime(result, DateTimeKind.Utc), EndUtc = dateTime, Tier = result3, LastKnownPosition = new Vector3(result4, result5, result6) }; _activeBounties[text2] = bountyState; Logger.LogInfo((object)("[PvPOverhaul SERVER] Loaded persistent bounty: " + text2)); WriteServerLog($"BOUNTY_LOADED | {text2} | tier={result3} | end={dateTime:o}"); if (IsPlayerOnline(text2)) { BountyRpc.BroadcastBountyCircle(text2, active: true, bountyState.LastKnownPosition, result3, dateTime); } } } SaveBounties(); } public void ScheduleDeathCheck(Player victim) { if (!((Object)(object)victim == (Object)null) && !((Object)(object)PvPOverhaul.Instance == (Object)null)) { ((MonoBehaviour)PvPOverhaul.Instance).StartCoroutine(DelayedDeathCheck(victim)); } } [IteratorStateMachine(typeof(d__51))] private IEnumerator DelayedDeathCheck(Player victim) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__51(0) { <>4__this = this, victim = victim }; } } internal static class BountyRpc { private const string RpcRegisterKill = "PvpModes_RegisterKill"; private const string RpcBroadcastMessage = "PvpModes_BroadcastMessage"; private const string RpcBountyCircle = "PvpModes_BountyCircle"; private const string RpcBountyPosition = "PvpModes_BountyPosition"; private const string RpcBountyStatusRequest = "PvpModes_BountyStatusRequest"; private const string RpcBountyStatusResponse = "PvpModes_BountyStatusResponse"; private const string RpcBountyResetRequest = "PvpModes_BountyResetRequest"; private const string RpcBountyResetResponse = "PvpModes_BountyResetResponse"; private const string RpcBountyReward = "PvpModes_BountyReward"; private static bool _registered; private static ZRoutedRpc _registeredInstance; private static string _pendingRewardItem; private static int _pendingRewardAmount; public static void Update() { EnsureRegistered(); } public static bool EnsureRegistered() { if (ZRoutedRpc.instance == null) { _registered = false; _registeredInstance = null; return false; } if (_registered && _registeredInstance == ZRoutedRpc.instance) { return true; } _registered = false; _registeredInstance = ZRoutedRpc.instance; ZRoutedRpc.instance.Register("PvpModes_RegisterKill", (Action)RPC_RegisterKill); ZRoutedRpc.instance.Register("PvpModes_BroadcastMessage", (Action)RPC_BroadcastMessage); ZRoutedRpc.instance.Register("PvpModes_BountyCircle", (Action)RPC_BountyCircle); ZRoutedRpc.instance.Register("PvpModes_BountyPosition", (Action)RPC_BountyPosition); ZRoutedRpc.instance.Register("PvpModes_BountyStatusRequest", (Action)RPC_BountyStatusRequest); ZRoutedRpc.instance.Register("PvpModes_BountyStatusResponse", (Action)RPC_BountyStatusResponse); ZRoutedRpc.instance.Register("PvpModes_BountyResetRequest", (Action)RPC_BountyResetRequest); ZRoutedRpc.instance.Register("PvpModes_BountyResetResponse", (Action)RPC_BountyResetResponse); ZRoutedRpc.instance.Register("PvpModes_BountyReward", (Action)RPC_BountyReward); _registered = true; Logger.LogInfo((object)"[PvPOverhaul] RPC registered successfully"); return true; } public static void RequestBountyStatus(string playerName) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!EnsureRegistered()) { Console.instance.Print("[PvPOverhaul] RPC not ready"); return; } ZPackage val = new ZPackage(); val.Write(playerName); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { SendBountyStatusResponse(ZRoutedRpc.instance.GetServerPeerID(), playerName); return; } ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PvpModes_BountyStatusRequest", new object[1] { val }); } public static void RequestBountyReset(string playerName) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (!EnsureRegistered()) { Console.instance.Print("[PvPOverhaul] RPC not ready"); return; } ZPackage val = new ZPackage(); val.Write(playerName); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BountySystem.Instance.ResetPlayer(playerName); Console.instance.Print("Reset bounty for " + playerName); } else { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PvpModes_BountyResetRequest", new object[1] { val }); } } private static void RPC_BountyStatusRequest(long sender, ZPackage pkg) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { string text = pkg.ReadString(); ZPackage val = BuildBountyStatusResponse(text); Logger.LogInfo((object)$"[PvPOverhaul SERVER] Bounty status request: {text} sender={sender}"); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "PvpModes_BountyStatusResponse", new object[1] { val }); } } private static void RPC_BountyStatusResponse(long sender, ZPackage pkg) { string text = pkg.ReadString(); string text2; if (!pkg.ReadBool()) { text2 = "No bounty for " + text; } else { int num = pkg.ReadInt(); long ticks = pkg.ReadLong(); TimeSpan timeSpan = new DateTime(ticks, DateTimeKind.Utc) - DateTime.UtcNow; if (timeSpan < TimeSpan.Zero) { timeSpan = TimeSpan.Zero; } text2 = $"{text} → Tier {num} | Time left: {timeSpan:hh\\:mm\\:ss}"; } Console.instance.Print(text2); if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, text2, 0, (Sprite)null, false); } } private static void RPC_BountyResetRequest(long sender, ZPackage pkg) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { string text = pkg.ReadString(); BountySystem.Instance.ResetPlayer(text); ZPackage val = new ZPackage(); val.Write(text); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "PvpModes_BountyResetResponse", new object[1] { val }); } } private static void RPC_BountyResetResponse(long sender, ZPackage pkg) { string text = pkg.ReadString(); Console.instance.Print("Reset bounty for " + text); } private static ZPackage BuildBountyStatusResponse(string playerName) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); BountySystem.BountyState bounty = BountySystem.Instance.GetBounty(playerName); val.Write(playerName); val.Write(bounty != null); if (bounty != null) { val.Write(bounty.Tier); val.Write(bounty.EndUtc.Ticks); } return val; } private static void SendBountyStatusResponse(long targetPeerId, string playerName) { ZPackage val = BuildBountyStatusResponse(playerName); ZRoutedRpc.instance.InvokeRoutedRPC(targetPeerId, "PvpModes_BountyStatusResponse", new object[1] { val }); } public static void SendKillToServer(string killerName, string victimName) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown if (EnsureRegistered()) { ZPackage val = new ZPackage(); val.Write(killerName); val.Write(victimName); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BountySystem.Instance.RegisterKill(killerName, victimName); return; } ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PvpModes_RegisterKill", new object[1] { val }); } } public static void SendBountyPositionToServer(string playerName, Vector3 position) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_003e: Unknown result type (might be due to invalid IL or missing references) if (EnsureRegistered() && (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer())) { ZPackage val = new ZPackage(); val.Write(playerName); val.Write(position); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PvpModes_BountyPosition", new object[1] { val }); } } public static void BroadcastCenterMessage(string message) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown if (EnsureRegistered()) { ZPackage val = new ZPackage(); val.Write(message); Logger.LogInfo((object)("[PvPOverhaul SERVER] Broadcast message: " + message)); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "PvpModes_BroadcastMessage", new object[1] { val }); } } public static void BroadcastBountyCircle(string playerName, bool active, Vector3 position, int tier, DateTime endUtc) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (EnsureRegistered()) { ZPackage val = new ZPackage(); val.Write(playerName); val.Write(active); val.Write(position); val.Write(tier); val.Write(endUtc.Ticks); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "PvpModes_BountyCircle", new object[1] { val }); } } private static void RPC_RegisterKill(long sender, ZPackage pkg) { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); Logger.LogInfo((object)("[PvPOverhaul SERVER] Kill RPC received: " + text + " -> " + text2)); BountySystem.Instance.RegisterKill(text, text2); } } private static void RPC_BountyPosition(long sender, ZPackage pkg) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: 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)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { string playerName = pkg.ReadString(); Vector3 position = pkg.ReadVector3(); BountySystem.Instance.UpdateBountyPositionFromClient(playerName, position); } } private static void RPC_BroadcastMessage(long sender, ZPackage pkg) { string text = pkg.ReadString(); if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false); } } private static void RPC_BountyCircle(long sender, ZPackage pkg) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) string playerName = pkg.ReadString(); bool active = pkg.ReadBool(); Vector3 position = pkg.ReadVector3(); int tier = pkg.ReadInt(); long ticks = pkg.ReadLong(); DateTime endUtc = new DateTime(ticks, DateTimeKind.Utc); BountyStatusEffectController.SetLocalBountyStatus(playerName, active, endUtc); BountyMapCircle.Set(playerName, active, position, tier, endUtc); } public static void SendRewardToPlayer(string playerName, string itemPrefabName, int amount) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Expected O, but got Unknown if (EnsureRegistered()) { ZPackage val = new ZPackage(); val.Write(playerName); val.Write(itemPrefabName); val.Write(amount); Logger.LogInfo((object)$"[PvPOverhaul SERVER] Sending reward RPC to {playerName}: {amount}x {itemPrefabName}"); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "PvpModes_BountyReward", new object[1] { val }); } } private static void RPC_BountyReward(long sender, ZPackage pkg) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); int num = pkg.ReadInt(); Logger.LogInfo((object)$"[PvPOverhaul CLIENT] Reward RPC received for {text}: {num}x {text2}"); if (!((Object)(object)Player.m_localPlayer == (Object)null) && !(Player.m_localPlayer.GetPlayerName() != text)) { Logger.LogInfo((object)("[PvPOverhaul CLIENT] Reward accepted by local player " + Player.m_localPlayer.GetPlayerName())); _pendingRewardItem = text2; _pendingRewardAmount += num; TryGivePendingReward(); } } public static void UpdateReward() { TryGivePendingReward(); } private static void TryGivePendingReward() { if (string.IsNullOrWhiteSpace(_pendingRewardItem) || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ObjectDB.instance == (Object)null) { return; } GameObject itemPrefab = ObjectDB.instance.GetItemPrefab(_pendingRewardItem); if ((Object)(object)itemPrefab == (Object)null) { Logger.LogWarning((object)("[PvPOverhaul] Reward prefab not found yet: " + _pendingRewardItem)); return; } ((Humanoid)Player.m_localPlayer).GetInventory().AddItem(itemPrefab, _pendingRewardAmount); if ((Object)(object)MessageHud.instance != (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, $"Récompense de prime : {_pendingRewardAmount}x {_pendingRewardItem}", 0, (Sprite)null, false); } _pendingRewardItem = null; _pendingRewardAmount = 0; } } internal static class BountyMapCircle { private class CircleState { public PinData Pin; public bool Active; public int Tier; public DateTime EndUtc; public float NextPositionSend; } private static readonly Dictionary _circles = new Dictionary(); private const float CircleWorldSize = 300f; public static void Update() { //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) DateTime utcNow = DateTime.UtcNow; foreach (KeyValuePair item in _circles.ToList()) { if (item.Value.EndUtc <= utcNow) { Remove(item.Key); } } if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } string playerName = Player.m_localPlayer.GetPlayerName(); if (_circles.TryGetValue(playerName, out var value) && value.Active && !(Time.time < value.NextPositionSend)) { value.NextPositionSend = Time.time + 10f; Vector3 position = ((Component)Player.m_localPlayer).transform.position; if (value.Pin != null) { value.Pin.m_pos = position; value.Pin.m_name = "RECHERCHÉ: " + playerName; SetPinWorldSize(value.Pin, 300f); } BountyRpc.SendBountyPositionToServer(playerName, position); } } public static void Set(string playerName, bool active, Vector3 position, int tier, DateTime endUtc) { //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)Minimap.instance == (Object)null) { return; } bool flag = (Object)(object)Player.m_localPlayer != (Object)null && Player.m_localPlayer.GetPlayerName() == playerName; if (!active) { Remove(playerName); return; } if (flag) { if (_circles.TryGetValue(playerName, out var value)) { if (value.Pin != null) { Minimap.instance.RemovePin(value.Pin); } value.Pin = null; value.Active = true; value.Tier = tier; value.EndUtc = endUtc; } else { _circles[playerName] = new CircleState { Pin = null, Active = true, Tier = tier, EndUtc = endUtc, NextPositionSend = 0f }; } return; } string text = "RECHERCHÉ: " + playerName; if (_circles.TryGetValue(playerName, out var value2)) { value2.Active = true; value2.Tier = tier; value2.EndUtc = endUtc; if (value2.Pin != null) { Minimap.instance.RemovePin(value2.Pin); } PinData pin = Minimap.instance.AddPin(position, (PinType)13, text, false, false, 0L, default(PlatformUserID)); SetPinWorldSize(pin, 300f); value2.Pin = pin; } else { PinData pin2 = Minimap.instance.AddPin(position, (PinType)13, text, false, false, 0L, default(PlatformUserID)); SetPinWorldSize(pin2, 300f); _circles[playerName] = new CircleState { Pin = pin2, Active = true, Tier = tier, EndUtc = endUtc, NextPositionSend = 0f }; } } private static void Remove(string playerName) { if (_circles.TryGetValue(playerName, out var value)) { if ((Object)(object)Minimap.instance != (Object)null && value.Pin != null) { Minimap.instance.RemovePin(value.Pin); } _circles.Remove(playerName); } } private static void SetPinWorldSize(PinData pin, float size) { FieldInfo field = typeof(PinData).GetField("m_worldSize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { field.SetValue(pin, size); } } } internal static class BountyStatusEffectController { private const string EffectName = "SE_PvpModes_Bounty"; private static readonly int EffectHash = StringExtensionMethods.GetStableHashCode("SE_PvpModes_Bounty"); private static DateTime _currentEndUtc; private static SE_Stats _effectPrefab; private static bool _registered; private static bool _pendingActive; private static DateTime _pendingEndUtc; private static string _pendingPlayerName; public static void UpdateRegistration() { if (_registered || (Object)(object)ObjectDB.instance == (Object)null) { return; } StatusEffect statusEffect = ObjectDB.instance.GetStatusEffect(EffectHash); if ((Object)(object)statusEffect != (Object)null) { _effectPrefab = (SE_Stats)(object)((statusEffect is SE_Stats) ? statusEffect : null); _registered = true; return; } SE_Stats val = (_effectPrefab = ScriptableObject.CreateInstance()); ((Object)val).name = "SE_PvpModes_Bounty"; ((StatusEffect)val).m_name = "Recherché"; ((StatusEffect)val).m_tooltip = "Votre position approximative est révélée aux autres joueurs."; ((StatusEffect)val).m_ttl = 60f; Sprite sprite = GetSprite("SoftDeath"); if ((Object)(object)sprite != (Object)null) { ((StatusEffect)val).m_icon = sprite; } else { Logger.LogWarning((object)"[PvPOverhaul] SoftDeath icon not found"); } ObjectDB.instance.m_StatusEffects.Add((StatusEffect)(object)val); _registered = true; Logger.LogInfo((object)"[PvPOverhaul] Bounty StatusEffect registered"); if (!string.IsNullOrEmpty(_pendingPlayerName)) { string pendingPlayerName = _pendingPlayerName; bool pendingActive = _pendingActive; DateTime pendingEndUtc = _pendingEndUtc; _pendingPlayerName = null; SetLocalBountyStatus(pendingPlayerName, pendingActive, pendingEndUtc); } } public static void SetLocalBountyStatus(string playerName, bool active, DateTime endUtc) { if (!_registered || (Object)(object)Player.m_localPlayer == (Object)null) { _pendingPlayerName = playerName; _pendingActive = active; _pendingEndUtc = endUtc; } UpdateRegistration(); if (!_registered || (Object)(object)Player.m_localPlayer == (Object)null || Player.m_localPlayer.GetPlayerName() != playerName) { return; } SEMan sEMan = ((Character)Player.m_localPlayer).GetSEMan(); if (!active) { sEMan.RemoveStatusEffect(EffectHash, false); return; } float num = (float)(endUtc - DateTime.UtcNow).TotalSeconds; if (num <= 0f) { sEMan.RemoveStatusEffect(EffectHash, false); return; } bool flag = sEMan.HaveStatusEffect(EffectHash); if (!(_currentEndUtc == endUtc && flag)) { _currentEndUtc = endUtc; if (flag) { sEMan.RemoveStatusEffect(EffectHash, false); } if ((Object)(object)_effectPrefab != (Object)null) { ((StatusEffect)_effectPrefab).m_ttl = num; } StatusEffect val = (((Object)(object)_effectPrefab != (Object)null) ? sEMan.AddStatusEffect((StatusEffect)(object)_effectPrefab, true, 0, 0f) : sEMan.AddStatusEffect(EffectHash, true, 0, 0f)); if ((Object)(object)val != (Object)null) { val.m_ttl = num; val.m_time = 0f; } } } private static Sprite GetSprite(string name) { Sprite[] array = Resources.FindObjectsOfTypeAll(); foreach (Sprite val in array) { if (((Object)val).name == name) { return val; } } return null; } } internal class BountyStatusCommand : ConsoleCommand { public override string Name => "pvp_bounty"; public override string Help => "Affiche la bounty d'un joueur. Usage: pvp_bounty nom"; public override void Run(string[] args) { if (args.Length < 1) { Console.instance.Print("Usage: pvp_bounty nom"); } else if (!SynchronizationManager.Instance.PlayerIsAdmin) { Console.instance.Print("Only server admins can use this command."); } else { BountyRpc.RequestBountyStatus(args[0]); } } } internal class BountyResetCommand : ConsoleCommand { public override string Name => "pvp_resetbounty"; public override string Help => "Reset la bounty et les kills d'un joueur. Usage: pvp_resetbounty nom"; public override void Run(string[] args) { if (args.Length < 1) { Console.instance.Print("Usage: pvp_resetbounty nom"); } else if (!SynchronizationManager.Instance.PlayerIsAdmin) { Console.instance.Print("Only server admins can use this command."); } else { BountyRpc.RequestBountyReset(args[0]); } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathBountyPatch { private static void Postfix(Player __instance) { if (!((Object)(object)__instance == (Object)null)) { Logger.LogInfo((object)("[PvPOverhaul BOUNTY] Player.OnDeath detected: " + __instance.GetPlayerName())); BountySystem.Instance.TryReportDeath(__instance); } } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class CharacterDeathBountyPatch { private static void Postfix(Character __instance) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null) { Logger.LogInfo((object)("[PvPOverhaul BOUNTY] Character.OnDeath detected: " + val.GetPlayerName())); BountySystem.Instance.TryReportDeath(val); } } } [HarmonyPatch] internal static class PvPTweaksCombatFilterPatch { private static readonly int CombatHash = StringExtensionMethods.GetStableHashCode("SE_Combat"); private static readonly Dictionary RecentRealPvpHit = new Dictionary(); [HarmonyPatch(typeof(Character), "Damage")] [HarmonyPrefix] [HarmonyPriority(800)] private static void CharacterDamagePrefix(Character __instance, HitData hit) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && hit != null) { Character attacker = hit.GetAttacker(); Player val2 = (Player)(object)((attacker is Player) ? attacker : null); if (val2 != null && !((Object)(object)val2 == (Object)(object)val)) { float time = Time.time; RecentRealPvpHit[val.GetPlayerID()] = time; RecentRealPvpHit[val2.GetPlayerID()] = time; } } } [HarmonyPatch(typeof(SEMan), "AddStatusEffect", new Type[] { typeof(int), typeof(bool), typeof(int), typeof(float) })] [HarmonyPrefix] [HarmonyPriority(800)] private static bool AddStatusEffectHashPrefix(SEMan __instance, int nameHash) { if (nameHash != CombatHash) { return true; } return AllowCombatStatus(__instance); } [HarmonyPatch(typeof(SEMan), "AddStatusEffect", new Type[] { typeof(StatusEffect), typeof(bool), typeof(int), typeof(float) })] [HarmonyPrefix] [HarmonyPriority(800)] private static bool AddStatusEffectObjectPrefix(SEMan __instance, StatusEffect statusEffect) { if ((Object)(object)statusEffect == (Object)null) { return true; } if (statusEffect.NameHash() != CombatHash) { return true; } return AllowCombatStatus(__instance); } private static bool AllowCombatStatus(SEMan seMan) { if (seMan == null) { return true; } Character character = seMan.m_character; Player val = (Player)(object)((character is Player) ? character : null); if (val == null) { return true; } long playerID = val.GetPlayerID(); if (RecentRealPvpHit.TryGetValue(playerID, out var value) && Time.time - value <= 2f) { return true; } if (BountySystem.Instance.ResolveLastHit(playerID) != null) { return true; } Logger.LogInfo((object)("[PvPOverhaul Compat] Blocked PvPTweaks SE_Combat on " + val.GetPlayerName() + " | no real PvP hit")); return false; } } [HarmonyPatch(typeof(Character), "Damage")] internal static class PvPDamagePatch { private class MagicBurstWindow { public float StartTime; public float Damage; } internal class PendingBlockLeak { public float ExpectedDamage; public float HealthBefore; public float Time; public DamageTypes LeakDamage; public float BlockLeakPercent; } internal static readonly Dictionary PendingLeaks = new Dictionary(); private static readonly Dictionary MagicBurstWindows = new Dictionary(); private static readonly Dictionary PendingBountyAttackers = new Dictionary(); [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(Character __instance, HitData hit) { //IL_014a: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) if (!PvPOverhaul.EnablePvpDamagePatch.Value) { return; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null || hit == null) { return; } if (IsTherzieBleedTick(val, hit)) { ScaleDamage(hit, PvPOverhaul.TherzieBleedNerf.Value); Logger.LogInfo((object)$"[PvPOverhaul PvPDMG] Therzie bleed nerfed on {val.GetPlayerName()} | dmg={hit.GetTotalDamage()}"); return; } Character attacker = hit.GetAttacker(); Player val2 = (Player)(object)((attacker is Player) ? attacker : null); if ((Object)(object)val2 == (Object)null) { val2 = TryResolveRecentPvpAttacker(val); } if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val)) { PendingBountyAttackers[val.GetPlayerID()] = val2.GetPlayerName(); float totalDamage = hit.GetTotalDamage(); if (totalDamage > 0f && !val.m_godMode) { BountySystem.Instance.SetLastHit(val.GetPlayerID(), val.GetPlayerName(), val2.GetPlayerName(), totalDamage); } float health = ((Character)val).GetHealth(); NormalizePvpDamage(val, val2, hit); float totalDamage2 = hit.GetTotalDamage(); if (hit.m_blockable && totalDamage2 > 0f && ((Character)val).IsBlocking()) { DamageTypes damage = hit.m_damage; float num = (IsTwoHandedMeleeWeapon(((Humanoid)val2).GetCurrentWeapon()) ? PvPOverhaul.PvpTwoHandedBlockedDamageLeakPercent.Value : PvPOverhaul.PvpBlockedDamageLeakPercent.Value); ScaleDamageTypes(ref damage, num); PendingLeaks[val.GetPlayerID()] = new PendingBlockLeak { ExpectedDamage = totalDamage2, HealthBefore = health, LeakDamage = damage, Time = Time.time, BlockLeakPercent = num }; } } } private static Player TryResolveRecentPvpAttacker(Player victim) { if ((Object)(object)victim == (Object)null) { return null; } BountySystem.LastHit lastHit = BountySystem.Instance.ResolveLastHit(victim.GetPlayerID()); if (lastHit == null) { return null; } foreach (Player allPlayer in Player.GetAllPlayers()) { if ((Object)(object)allPlayer != (Object)null && allPlayer.GetPlayerName() == lastHit.AttackerName) { return allPlayer; } } return null; } [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Character __instance, HitData hit) { Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null) { return; } long playerID = val.GetPlayerID(); if (PendingBountyAttackers.TryGetValue(playerID, out var value)) { PendingBountyAttackers.Remove(playerID); if (!string.IsNullOrWhiteSpace(value) && !val.m_godMode && !(value == val.GetPlayerName())) { Logger.LogInfo((object)$"[PvPOverhaul BOUNTY] PvP hit completed, scheduling death check: {value} -> {val.GetPlayerName()} health={((Character)val).GetHealth()} dead={((Character)val).IsDead()}"); BountySystem.Instance.ScheduleDeathCheck(val); } } } private static void NormalizePvpDamage(Player victim, Player attacker, HitData hit) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Invalid comparison between Unknown and I4 float maxHealth = ((Character)victim).GetMaxHealth(); if (maxHealth <= 0f) { return; } float totalDamage = hit.GetTotalDamage(); if (totalDamage <= 0f) { return; } bool flag = IsLikelyDotTick(hit); bool flag2 = IsLikelyMagicBurstFragment(hit); bool flag3 = (int)hit.m_skill == 9 && IsLikelyMagicSecondary(hit); float value = PvPOverhaul.PvpMaxDirectHitPercentHp.Value; value *= GetWeaponPvpMultiplier(attacker); float num = (flag ? (maxHealth * PvPOverhaul.PvpMaxDotTickPercentHp.Value) : (maxHealth * value)); if (flag2) { num = Mathf.Min(num, maxHealth * PvPOverhaul.PvpMaxMagicMultiHitPercentHp.Value); } if (flag3) { num = Mathf.Min(num, maxHealth * PvPOverhaul.PvpMaxMagicSecondaryPercentHp.Value); } if (flag2) { string key = $"{victim.GetPlayerID()}:{attacker.GetPlayerID()}"; float num2 = Mathf.Max(0.05f, PvPOverhaul.PvpMagicBurstWindowSeconds.Value); float num3 = maxHealth * PvPOverhaul.PvpMaxMagicBurstPercentHp.Value; if (!MagicBurstWindows.TryGetValue(key, out var value2) || Time.time - value2.StartTime > num2) { value2 = new MagicBurstWindow { StartTime = Time.time, Damage = 0f }; MagicBurstWindows[key] = value2; } float num4 = num3 - value2.Damage; if (num4 <= 0f) { ZeroDamage(hit); return; } num = Mathf.Min(num, num4); } if (num <= 0f) { ZeroDamage(hit); return; } if (totalDamage > num) { float scale = num / totalDamage; ScaleDamage(hit, scale); } ApplyElementalPvpMultipliers(hit); ApplyTherzieSpecialNerfs(victim, attacker, hit); if (!flag && (!hit.m_blockable || !((Character)victim).IsBlocking())) { ConvertPvpDamageToTrueDamage(hit); } if (flag2) { string key2 = $"{victim.GetPlayerID()}:{attacker.GetPlayerID()}"; if (MagicBurstWindows.TryGetValue(key2, out var value3)) { value3.Damage += hit.GetTotalDamage(); } } } private static void ConvertPvpDamageToTrueDamage(HitData hit) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (hit != null) { float totalDamage = hit.GetTotalDamage(); if (!(totalDamage <= 0f)) { hit.m_damage = new DamageTypes { m_damage = totalDamage }; } } } private static void ApplyElementalPvpMultipliers(HitData hit) { if (hit != null) { hit.m_damage.m_lightning *= PvPOverhaul.PvpLightningDamageMultiplier.Value; } } private static void ApplyTherzieSpecialNerfs(Player victim, Player attacker, HitData hit) { if (hit != null) { if (IsTherzieBleedTick(victim, hit)) { ScaleDamage(hit, PvPOverhaul.TherzieBleedNerf.Value); } else if (IsTherzieDualWeapon(attacker)) { ScaleDamage(hit, PvPOverhaul.TherzieDualWeapNerf.Value); } } } private static bool IsTherzieDualWeapon(Player attacker) { if ((Object)(object)attacker == (Object)null) { return false; } ItemData currentWeapon = ((Humanoid)attacker).GetCurrentWeapon(); if (currentWeapon == null || currentWeapon.m_shared == null) { return false; } string text = ""; if ((Object)(object)currentWeapon.m_dropPrefab != (Object)null) { text = text + ((Object)currentWeapon.m_dropPrefab).name + " "; } text += currentWeapon.m_shared.m_name; return text.IndexOf("Dual", StringComparison.OrdinalIgnoreCase) >= 0; } private static bool IsTherzieBleedTick(Player victim, HitData hit) { if ((Object)(object)victim == (Object)null || hit == null) { return false; } if ((Object)(object)hit.GetAttacker() != (Object)null) { return false; } if (hit.GetTotalDamage() <= 0f) { return false; } SEMan sEMan = ((Character)victim).GetSEMan(); if (sEMan == null) { return false; } foreach (StatusEffect statusEffect in sEMan.GetStatusEffects()) { if (!((Object)(object)statusEffect == (Object)null)) { string text = ((Object)statusEffect).name ?? ""; string text2 = statusEffect.m_name ?? ""; string text3 = ((object)statusEffect).GetType().FullName ?? ""; if (text.StartsWith("SE_Warfare", StringComparison.OrdinalIgnoreCase)) { return true; } if (text.StartsWith("SE_WarfareFireAndIce", StringComparison.OrdinalIgnoreCase)) { return true; } if (text3.IndexOf("SE_Warfare", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (text3.IndexOf("SE_WarfareFireAndIce", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (text.IndexOf("Warfare_Bleeding", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } if (text2.IndexOf("Bleeding", StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } } return false; } private static float GetWeaponPvpMultiplier(Player attacker) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected I4, but got Unknown //IL_00fd: Unknown result type (might be due to invalid IL or missing references) ItemData currentWeapon = ((Humanoid)attacker).GetCurrentWeapon(); if (currentWeapon == null || currentWeapon.m_shared == null) { return 1f; } SkillType skillType = currentWeapon.m_shared.m_skillType; SkillType val = skillType; float num = (val - 1) switch { 0 => PvPOverhaul.PvpSwordDamageMultiplier.Value, 6 => PvPOverhaul.PvpAxeDamageMultiplier.Value, 2 => PvPOverhaul.PvpClubDamageMultiplier.Value, 1 => PvPOverhaul.PvpKnifeDamageMultiplier.Value, 4 => PvPOverhaul.PvpSpearDamageMultiplier.Value, 3 => PvPOverhaul.PvpPolearmDamageMultiplier.Value, 10 => PvPOverhaul.PvpUnarmedDamageMultiplier.Value, 7 => PvPOverhaul.PvpBowDamageMultiplier.Value, 13 => PvPOverhaul.PvpCrossbowDamageMultiplier.Value, _ => 1f, }; if (IsMeleeSkill(currentWeapon.m_shared.m_skillType) && IsSecondaryAttack(attacker, currentWeapon)) { num *= PvPOverhaul.PvpMeleeSecondaryDamageMultiplier.Value; } if (IsTwoHandedMeleeWeapon(currentWeapon)) { num *= PvPOverhaul.PvpTwoHandedDamageMultiplier.Value; } return num; } private static bool IsMeleeSkill(SkillType skill) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Invalid comparison between Unknown and I4 //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Invalid comparison between Unknown and I4 //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Invalid comparison between Unknown and I4 return (int)skill == 1 || (int)skill == 7 || (int)skill == 3 || (int)skill == 2 || (int)skill == 5 || (int)skill == 4 || (int)skill == 11; } private static bool IsTwoHandedMeleeWeapon(ItemData weapon) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Invalid comparison between Unknown and I4 //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Invalid comparison between Unknown and I4 if (weapon == null || weapon.m_shared == null) { return false; } if (!IsMeleeSkill(weapon.m_shared.m_skillType)) { return false; } return (int)weapon.m_shared.m_itemType == 14 || (int)weapon.m_shared.m_itemType == 22; } private static bool IsSecondaryAttack(Player attacker, ItemData weapon) { if ((Object)(object)attacker == (Object)null || weapon?.m_shared == null) { return false; } Attack currentAttack = ((Humanoid)attacker).m_currentAttack; Attack secondaryAttack = weapon.m_shared.m_secondaryAttack; if (currentAttack == null || secondaryAttack == null) { return false; } if (currentAttack == secondaryAttack) { return true; } return currentAttack.m_attackAnimation == secondaryAttack.m_attackAnimation || currentAttack.m_attackChainLevels == secondaryAttack.m_attackChainLevels || Math.Abs(currentAttack.m_damageMultiplier - secondaryAttack.m_damageMultiplier) < 0.01f; } private static bool IsLikelyMagicBurstFragment(HitData hit) { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Invalid comparison between Unknown and I4 //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Invalid comparison between Unknown and I4 if (hit == null) { return false; } float totalDamage = hit.GetTotalDamage(); if (totalDamage <= 0f || totalDamage > 100f) { return false; } if ((int)hit.m_skill != 9 && (int)hit.m_skill != 10) { return false; } int num = 0; if (hit.m_damage.m_damage > 0f) { num++; } if (hit.m_damage.m_blunt > 0f) { num++; } if (hit.m_damage.m_slash > 0f) { num++; } if (hit.m_damage.m_pierce > 0f) { num++; } if (hit.m_damage.m_chop > 0f) { num++; } if (hit.m_damage.m_pickaxe > 0f) { num++; } if (hit.m_damage.m_fire > 0f) { num++; } if (hit.m_damage.m_frost > 0f) { num++; } if (hit.m_damage.m_lightning > 0f) { num++; } if (hit.m_damage.m_poison > 0f) { num++; } if (hit.m_damage.m_spirit > 0f) { num++; } return num <= 3; } private static bool IsLikelyMagicSecondary(HitData hit) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Invalid comparison between Unknown and I4 if ((int)hit.m_skill != 9) { return false; } float totalDamage = hit.GetTotalDamage(); if (totalDamage <= 0f) { return false; } return hit.m_damage.m_fire >= 50f || hit.m_damage.m_frost >= 50f || hit.m_damage.m_lightning >= 50f || hit.m_damage.m_poison >= 50f || hit.m_damage.m_spirit >= 50f || totalDamage >= 100f; } private static bool IsLikelyDotTick(HitData hit) { if (!(hit.m_damage.m_fire > 0f) && !(hit.m_damage.m_poison > 0f) && !(hit.m_damage.m_spirit > 0f)) { return false; } bool flag = hit.m_damage.m_damage > 0f || hit.m_damage.m_blunt > 0f || hit.m_damage.m_slash > 0f || hit.m_damage.m_pierce > 0f || hit.m_damage.m_chop > 0f || hit.m_damage.m_pickaxe > 0f || hit.m_damage.m_frost > 0f || hit.m_damage.m_lightning > 0f; return !flag; } private static void ScaleDamage(HitData hit, float scale) { hit.m_damage.m_damage *= scale; hit.m_damage.m_blunt *= scale; hit.m_damage.m_slash *= scale; hit.m_damage.m_pierce *= scale; hit.m_damage.m_chop *= scale; hit.m_damage.m_pickaxe *= scale; hit.m_damage.m_fire *= scale; hit.m_damage.m_frost *= scale; hit.m_damage.m_lightning *= scale; hit.m_damage.m_poison *= scale; hit.m_damage.m_spirit *= scale; } private static void ScaleDamageTypes(ref DamageTypes damage, float scale) { damage.m_damage *= scale; damage.m_blunt *= scale; damage.m_slash *= scale; damage.m_pierce *= scale; damage.m_chop *= scale; damage.m_pickaxe *= scale; damage.m_fire *= scale; damage.m_frost *= scale; damage.m_lightning *= scale; damage.m_poison *= scale; damage.m_spirit *= scale; } private static void ZeroDamage(HitData hit) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) hit.m_damage = default(DamageTypes); } } [HarmonyPatch(typeof(Character), "Heal", new Type[] { typeof(float), typeof(bool) })] internal static class PvpCombatNoHealingPatch { private static bool Prefix(Character __instance, ref float hp) { if (!PvPOverhaul.DisableHealthRegenInPvpBattle.Value) { return true; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null) { return true; } if (!IsInPvpCombat(val)) { return true; } hp = 0f; return false; } private static bool IsInPvpCombat(Player player) { SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null && sEMan.HaveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_Combat"))) { return true; } if ((Object)(object)((Character)player).m_nview != (Object)null && ((Character)player).m_nview.IsValid()) { ZDO zDO = ((Character)player).m_nview.GetZDO(); if (zDO != null && zDO.GetBool("VPT_PlayerInCombat", false)) { return true; } } return false; } } [HarmonyPatch(typeof(SE_Shield), "OnDamaged")] internal static class PvPShieldBreakOnDamagedPatch { private static bool Prefix(SE_Shield __instance, HitData hit, Character attacker) { //IL_00f4: Unknown result type (might be due to invalid IL or missing references) if (!PvPOverhaul.BreakShieldOnPvpHit.Value) { return true; } Character obj = ((StatusEffect)(__instance?)).m_character; Player val = (Player)(object)((obj is Player) ? obj : null); if (val == null) { return true; } if (hit == null || hit.GetTotalDamage() <= 0f) { return true; } Character val2 = (((Object)(object)attacker != (Object)null) ? attacker : hit.GetAttacker()); Player val3 = (Player)(object)((val2 is Player) ? val2 : null); if (val3 == null) { return true; } if ((Object)(object)val3 == (Object)(object)val) { return true; } __instance.m_absorbDamage = 0f; __instance.m_absorbDamagePerSkillLevel = 0f; SEMan sEMan = ((Character)val).GetSEMan(); if (sEMan != null) { sEMan.RemoveStatusEffect(((StatusEffect)__instance).NameHash(), true); } BountySystem.Instance.MarkIgnoredDeath(val); if (PvPOverhaul.ShieldBreakConsumesHit.Value) { hit.m_damage = default(DamageTypes); } return false; } } [HarmonyPatch(typeof(Character), "Damage")] internal static class PvPBlockLeakPatch { private static bool _applyingLeak; private static void Postfix(Character __instance, HitData hit) { //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Expected O, but got Unknown if (_applyingLeak) { return; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val == null) { return; } long playerID = val.GetPlayerID(); if (!PvPDamagePatch.PendingLeaks.TryGetValue(playerID, out var value)) { return; } PvPDamagePatch.PendingLeaks.Remove(playerID); if (Time.time - value.Time > 1f) { return; } float health = ((Character)val).GetHealth(); float num = Mathf.Max(0f, value.HealthBefore - health); if (num >= value.ExpectedDamage * 0.8f) { return; } float blockLeakPercent = value.BlockLeakPercent; float num2 = value.ExpectedDamage * blockLeakPercent; if (!(num >= num2)) { float num3 = num2 - num; if (!(num3 <= 0f)) { HitData val2 = new HitData { m_damage = new DamageTypes { m_damage = num3 }, m_point = ((Character)val).GetCenterPoint(), m_dir = Vector3.zero, m_blockable = false, m_dodgeable = false, m_skill = hit.m_skill, m_hitType = hit.m_hitType }; _applyingLeak = true; ((Character)val).Damage(val2); _applyingLeak = false; } } } } [HarmonyPatch(typeof(Aoe), "OnHit")] internal static class DisablePvpAoePatch { private static bool Prefix(Aoe __instance, Collider collider) { if (!PvPOverhaul.DisablePvpAoeSplash.Value) { return true; } if ((Object)(object)__instance == (Object)null || (Object)(object)collider == (Object)null) { return true; } if (((DamageTypes)(ref __instance.m_damage)).GetTotalDamage() <= 0f) { return true; } Player componentInParent = ((Component)collider).GetComponentInParent(); if ((Object)(object)componentInParent == (Object)null) { return true; } Character owner = __instance.m_owner; Player val = (Player)(object)((owner is Player) ? owner : null); if (val == null) { return true; } if ((Object)(object)val == (Object)(object)componentInParent) { return true; } return false; } } [BepInPlugin("dzk.pvpoverhaul", "PvpOverhaul", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] internal class PvPOverhaul : BaseUnityPlugin { public const string PluginGUID = "dzk.pvpoverhaul"; public const string PluginName = "PvpOverhaul"; public const string PluginVersion = "1.0.0"; public static PvPOverhaul Instance; public static CustomLocalization Localization; public static ConfigEntry BountyTriggerKillCount; public static ConfigEntry BountyKillWindowMinutes; public static ConfigEntry BountyDurationMinutes; public static ConfigEntry BountyRewardItem; public static ConfigEntry BountyRewardAmountPerTier; public static ConfigEntry MsgKillBroadcast; public static ConfigEntry MsgBountyActivated; public static ConfigEntry MsgBountyRefreshed; public static ConfigEntry MsgBountyCleared; public static ConfigEntry MsgBountyExpired; public static ConfigEntry MsgBountyReward; public static ConfigEntry EnablePvpDamagePatch; public static ConfigEntry PvpMaxDirectHitPercentHp; public static ConfigEntry PvpMaxDotTickPercentHp; public static ConfigEntry BreakShieldOnPvpHit; public static ConfigEntry ShieldBreakConsumesHit; public static ConfigEntry ShieldStatusEffectNames; public static ConfigEntry DisableHealthRegenInPvpBattle; public static ConfigEntry PvpBattleStatusEffectNames; public static ConfigEntry PvpBlockedDamageLeakPercent; public static ConfigEntry PvpMaxMagicMultiHitPercentHp; public static ConfigEntry DisablePvpAoeSplash; public static ConfigEntry PvpMaxMagicBurstPercentHp; public static ConfigEntry PvpMagicBurstWindowSeconds; public static ConfigEntry PvpMaxMagicSecondaryPercentHp; public static ConfigEntry PvpMeleeDamageMultiplier; public static ConfigEntry PvpMeleeSecondaryDamageMultiplier; public static ConfigEntry PvpSwordDamageMultiplier; public static ConfigEntry PvpAxeDamageMultiplier; public static ConfigEntry PvpClubDamageMultiplier; public static ConfigEntry PvpKnifeDamageMultiplier; public static ConfigEntry PvpSpearDamageMultiplier; public static ConfigEntry PvpPolearmDamageMultiplier; public static ConfigEntry PvpUnarmedDamageMultiplier; public static ConfigEntry PvpBowDamageMultiplier; public static ConfigEntry PvpCrossbowDamageMultiplier; public static ConfigEntry PvpLightningDamageMultiplier; public static ConfigEntry PvpTwoHandedDamageMultiplier; public static ConfigEntry PvpTwoHandedBlockedDamageLeakPercent; public static ConfigEntry TherzieDualWeapNerf; public static ConfigEntry TherzieBleedNerf; public static ConfigEntry DisableSkillLossOnPvpDeath; private FileSystemWatcher _configWatcher; private DateTime _lastConfigReloadUtc; private void Awake() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown Instance = this; Localization = LocalizationManager.Instance.GetLocalization(); SetupConfig(); SetupConfigWatcher(); Logger.LogInfo((object)"PvpOverhaul is loading..."); Harmony val = new Harmony("dzk.pvpoverhaul"); val.PatchAll(); PvpSkillLossPatch.PatchFortifySkillsRedux(val); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new BountyStatusCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new BountyResetCommand()); SynchronizationManager.OnConfigurationSynchronized += delegate(object _, ConfigurationSynchronizationEventArgs args) { Logger.LogInfo((object)(args.InitialSynchronization ? "[PvPOverhaul] Initial server config sync received" : "[PvPOverhaul] Server config sync received")); }; Logger.LogInfo((object)"PvpOverhaul loaded successfully."); } private void Update() { BountyRpc.Update(); BountySystem.Instance.Update(); BountyMapCircle.Update(); BountyStatusEffectController.UpdateRegistration(); BountyRpc.UpdateReward(); } private void SetupConfig() { BountyTriggerKillCount = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Bounty", "TriggerKillCount", 3, "Number of player kills required to activate a bounty.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); BountyKillWindowMinutes = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Bounty", "KillWindowMinutes", 180, "Time window in minutes during which kills are counted for bounty activation.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); BountyDurationMinutes = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Bounty", "DurationMinutes", 60, "How long the bounty stays active, in minutes.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); BountyRewardItem = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Bounty", "RewardItem", "Coins", "Prefab name of the item rewarded when killing a bounty player.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); BountyRewardAmountPerTier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Bounty", "RewardAmountPerTier", 100, "Reward amount per bounty tier.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgKillBroadcast = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "KillBroadcast", "{killer} a tué {victim}", "Broadcast when a player kills another player.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgBountyActivated = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "BountyActivated", "{player} est maintenant recherché ! Tier {tier}", "Broadcast when a bounty starts.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgBountyRefreshed = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "BountyRefreshed", "{player} reste recherché ! Tier {tier} | Timer remis à {duration} min.", "Broadcast when bounty timer refreshes.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgBountyCleared = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "BountyCleared", "{player} n'est plus recherché.", "Broadcast when a bounty is cleared.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgBountyExpired = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "BountyExpired", "La prime de {player} a expiré.", "Broadcast when a bounty expires.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); MsgBountyReward = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "Messages", "BountyReward", "{killer} a éliminé le joueur recherché {victim} et gagne {amount}x {item} !", "Broadcast when a bounty is claimed.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); EnablePvpDamagePatch = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "EnablePvpDamagePatch", true, "Enable global PvP damage normalization.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMaxDirectHitPercentHp = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MaxDirectHitPercentHp", 0.08f, "Maximum PvP direct hit damage as percent of victim max HP.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMaxDotTickPercentHp = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MaxDotTickPercentHp", 0.015f, "Maximum PvP fire/poison/spirit tick damage as percent of victim max HP.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); BreakShieldOnPvpHit = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Shields", "BreakShieldOnPvpHit", true, "If true, PvP hits instantly remove configured shield status effects.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); ShieldBreakConsumesHit = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Shields", "ShieldBreakConsumesHit", true, "If true, the hit that breaks a shield deals 0 damage.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); ShieldStatusEffectNames = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Shields", "ShieldStatusEffectNames", "Staff_shield,SE_StaffShield,SE_Shield,Shield,Bubble,Barrier,Protection", "Comma-separated status effect names/hashes to remove on PvP hit.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); DisableHealthRegenInPvpBattle = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Battle", "DisableHealthRegenInPvpBattle", true, "If true, health regen is set to 0 while the player has the PvP battle status.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpBattleStatusEffectNames = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Battle", "BattleStatusEffectNames", "SE_Combat", "Comma-separated status effect names used by Valheim PvP Tweaks.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpBlockedDamageLeakPercent = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "BlockedDamageLeakPercent", 0.25f, "Minimum percent of normalized PvP damage that still goes through block.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMaxMagicMultiHitPercentHp = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MaxMagicMultiHitPercentHp", 0.015f, "Maximum damage per hit for magic multi-hit projectiles, as percent of victim max HP.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); DisablePvpAoeSplash = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "DisablePvpAoeSplash", true, "If true, damaging AoE effects owned by players do not damage other players.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMaxMagicBurstPercentHp = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MaxMagicBurstPercentHp", 0.08f, "Maximum total PvP damage from magic multi-hit burst per attacker/victim over a short window.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMagicBurstWindowSeconds = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MagicBurstWindowSeconds", 0.2f, "Short time window used to group instant magic fragment hits from the same attacker to the same victim.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMaxMagicSecondaryPercentHp = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MaxMagicSecondaryPercentHp", 0.02f, "Maximum PvP damage for magic secondary attacks, as percent of victim max HP.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMeleeDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MeleeDamageMultiplier", 1.25f, "Multiplier applied to melee PvP direct hit cap.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpMeleeSecondaryDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "MeleeSecondaryDamageMultiplier", 1.5f, "Multiplier applied to melee secondary attack PvP damage cap.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpSwordDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Swords", 1.25f, "PvP damage cap multiplier for swords.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpAxeDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Axes", 1.25f, "PvP damage cap multiplier for axes.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpClubDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Clubs", 1.25f, "PvP damage cap multiplier for clubs.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpKnifeDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Knives", 0.9f, "PvP damage cap multiplier for knives.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpSpearDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Spears", 1f, "PvP damage cap multiplier for spears.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpPolearmDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Polearms", 1.15f, "PvP damage cap multiplier for polearms.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpUnarmedDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Unarmed", 1f, "PvP damage cap multiplier for unarmed.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpBowDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Bows", 1f, "PvP damage cap multiplier for bows.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpCrossbowDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Weapon Multipliers", "Crossbows", 1.25f, "PvP damage cap multiplier for crossbows.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpLightningDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "LightningDamageMultiplier", 0.25f, "Multiplier applied to PvP lightning damage after normalization. 0.25 = -75%.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpTwoHandedDamageMultiplier = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "TwoHandedDamageMultiplier", 1.25f, "Extra PvP damage cap multiplier for two-handed melee weapons.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); PvpTwoHandedBlockedDamageLeakPercent = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Damage", "TwoHandedBlockedDamageLeakPercent", 0.75f, "Minimum percent of normalized two-handed PvP damage that still goes through block.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); TherzieDualWeapNerf = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Therzie Balance", "TherzieDualWeapNerf", 0.5f, "Multiplier applied to PvP damage from Therzie dual weapons detected by prefab/name containing 'Dual'. 0.50 = -50%.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); TherzieBleedNerf = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Therzie Balance", "TherzieSENerf", 0.25f, "Multiplier applied to Therzie SE that proc on 4th or 5th. 0.25 = -75%.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); DisableSkillLossOnPvpDeath = ConfigFileExtensions.BindConfig(((BaseUnityPlugin)this).Config, "PvP Death", "DisableSkillLossOnPvpDeath", true, "If true, PvP deaths restore all skill levels after death, overriding vanilla and other mods.", true, (int?)null, (AcceptableValueBase)null, (Action)null, (ConfigurationManagerAttributes)null); } private void SetupConfigWatcher() { string configFilePath = ((BaseUnityPlugin)this).Config.ConfigFilePath; string directoryName = Path.GetDirectoryName(configFilePath); string fileName = Path.GetFileName(configFilePath); if (!string.IsNullOrWhiteSpace(directoryName) && !string.IsNullOrWhiteSpace(fileName)) { _configWatcher = new FileSystemWatcher(directoryName, fileName) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite), EnableRaisingEvents = true }; _configWatcher.Changed += OnConfigFileChanged; _configWatcher.Created += OnConfigFileChanged; _configWatcher.Renamed += OnConfigFileChanged; Logger.LogInfo((object)"[PvPOverhaul] Config watcher enabled"); } } private void OnConfigFileChanged(object sender, FileSystemEventArgs e) { DateTime utcNow = DateTime.UtcNow; if ((utcNow - _lastConfigReloadUtc).TotalMilliseconds < 500.0) { return; } _lastConfigReloadUtc = utcNow; try { ((BaseUnityPlugin)this).Config.Reload(); Logger.LogInfo((object)"[PvPOverhaul] Config reloaded"); } catch (Exception arg) { Logger.LogWarning((object)$"[PvPOverhaul] Failed to reload config: {arg}"); } } private void OnDestroy() { if (_configWatcher != null) { _configWatcher.Changed -= OnConfigFileChanged; _configWatcher.Created -= OnConfigFileChanged; _configWatcher.Renamed -= OnConfigFileChanged; _configWatcher.Dispose(); _configWatcher = null; } } } internal static class PvpSkillLossPatch { private class SkillSnapshot { public float LastPvpHitTime; public readonly Dictionary Skills = new Dictionary(); public readonly Dictionary FortifySkills = new Dictionary(); } private class SkillData { public float Level; public float Accumulator; } private class FortifyData { public float Level; public float Accumulator; } [CompilerGenerated] private sealed class d__11 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Player player; private long 5__1; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__11(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>1__state = -2; } private bool MoveNext() { //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = null; <>1__state = 1; return true; case 1: <>1__state = -1; RestoreSnapshot(player); <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 2; return true; case 2: <>1__state = -1; RestoreSnapshot(player); if ((Object)(object)player != (Object)null) { 5__1 = player.GetPlayerID(); Snapshots.Remove(5__1); DelayedRestoreRunning.Remove(5__1); } 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 const float SnapshotDuration = 30f; private static readonly Dictionary Snapshots = new Dictionary(); private static readonly HashSet DelayedRestoreRunning = new HashSet(); internal static bool ShouldProtectSkills(Player player) { if (!PvPOverhaul.DisableSkillLossOnPvpDeath.Value) { return false; } if ((Object)(object)player == (Object)null || (Object)(object)player.m_skills == (Object)null) { return false; } if (player.m_godMode) { return false; } if (Snapshots.TryGetValue(player.GetPlayerID(), out var value) && Time.time - value.LastPvpHitTime <= 30f) { return true; } if (IsInPvpCombat(player)) { return true; } if (BountySystem.Instance.ResolveLastHit(player.GetPlayerID()) != null) { return true; } return false; } private static bool IsInPvpCombat(Player player) { if ((Object)(object)player == (Object)null) { return false; } SEMan sEMan = ((Character)player).GetSEMan(); if (sEMan != null && sEMan.HaveStatusEffect(StringExtensionMethods.GetStableHashCode("SE_Combat"))) { return true; } if ((Object)(object)((Character)player).m_nview != (Object)null && ((Character)player).m_nview.IsValid()) { ZDO zDO = ((Character)player).m_nview.GetZDO(); if (zDO != null && zDO.GetBool("VPT_PlayerInCombat", false)) { return true; } } return false; } internal static void ForceSaveSnapshot(Player player) { //IL_0097: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || (Object)(object)player.m_skills == (Object)null) { return; } long playerID = player.GetPlayerID(); if (Snapshots.TryGetValue(playerID, out var value)) { value.LastPvpHitTime = Time.time; return; } SkillSnapshot skillSnapshot = new SkillSnapshot { LastPvpHitTime = Time.time }; foreach (KeyValuePair skillDatum in player.m_skills.m_skillData) { if (skillDatum.Value != null) { skillSnapshot.Skills[skillDatum.Key] = new SkillData { Level = skillDatum.Value.m_level, Accumulator = skillDatum.Value.m_accumulator }; } } SnapshotFortifySkills(skillSnapshot); Snapshots[playerID] = skillSnapshot; Logger.LogInfo((object)("[PvPOverhaul] PvP skill snapshot saved for " + player.GetPlayerName())); } internal static void RestoreSnapshot(Player player) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || (Object)(object)player.m_skills == (Object)null) { return; } long playerID = player.GetPlayerID(); if (!Snapshots.TryGetValue(playerID, out var value)) { return; } if (Time.time - value.LastPvpHitTime > 30f) { Snapshots.Remove(playerID); return; } foreach (KeyValuePair skill2 in value.Skills) { Skill skill = player.m_skills.GetSkill(skill2.Key); if (skill != null) { skill.m_level = skill2.Value.Level; skill.m_accumulator = skill2.Value.Accumulator; } } RestoreFortifySkills(value); Logger.LogInfo((object)("[PvPOverhaul] PvP death skills restored for " + player.GetPlayerName())); } internal static void StartDelayedRestore(Player player) { if (!((Object)(object)PvPOverhaul.Instance == (Object)null) && !((Object)(object)player == (Object)null)) { long playerID = player.GetPlayerID(); if (Snapshots.ContainsKey(playerID) && !DelayedRestoreRunning.Contains(playerID)) { DelayedRestoreRunning.Add(playerID); ((MonoBehaviour)PvPOverhaul.Instance).StartCoroutine(DelayedRestore(player)); } } } [IteratorStateMachine(typeof(d__11))] private static IEnumerator DelayedRestore(Player player) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__11(0) { player = player }; } private static void SnapshotFortifySkills(SkillSnapshot snapshot) { try { Type type = AccessTools.TypeByName("FortifySkillsRedux.FortifySkillData"); if (type == null) { return; } object obj = AccessTools.Field(type, "s_FortifySkills")?.GetValue(null); if (obj == null) { return; } foreach (object item in (IEnumerable)obj) { object obj2 = item.GetType().GetProperty("Key")?.GetValue(item, null); object obj3 = item.GetType().GetProperty("Value")?.GetValue(item, null); if (obj2 != null && obj3 != null) { FieldInfo fieldInfo = AccessTools.Field(obj3.GetType(), "FortifyLevel"); FieldInfo fieldInfo2 = AccessTools.Field(obj3.GetType(), "FortifyAccumulator"); if (!(fieldInfo == null) && !(fieldInfo2 == null)) { snapshot.FortifySkills[obj2] = new FortifyData { Level = Convert.ToSingle(fieldInfo.GetValue(obj3)), Accumulator = Convert.ToSingle(fieldInfo2.GetValue(obj3)) }; } } } } catch (Exception ex) { Logger.LogWarning((object)("[PvPOverhaul] Fortify snapshot failed: " + ex.Message)); } } private static void RestoreFortifySkills(SkillSnapshot snapshot) { try { Type type = AccessTools.TypeByName("FortifySkillsRedux.FortifySkillData"); if (type == null) { return; } object obj = AccessTools.Field(type, "s_FortifySkills")?.GetValue(null); if (obj == null) { return; } foreach (object item in (IEnumerable)obj) { object obj2 = item.GetType().GetProperty("Key")?.GetValue(item, null); object obj3 = item.GetType().GetProperty("Value")?.GetValue(item, null); if (obj2 != null && obj3 != null && snapshot.FortifySkills.TryGetValue(obj2, out var value)) { FieldInfo fieldInfo = AccessTools.Field(obj3.GetType(), "FortifyLevel"); FieldInfo fieldInfo2 = AccessTools.Field(obj3.GetType(), "FortifyAccumulator"); fieldInfo?.SetValue(obj3, value.Level); fieldInfo2?.SetValue(obj3, value.Accumulator); } } } catch (Exception ex) { Logger.LogWarning((object)("[PvPOverhaul] Fortify restore failed: " + ex.Message)); } } internal static void PatchFortifySkillsRedux(Harmony harmony) { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetTypes().FirstOrDefault((Type t) => t.FullName == "FortifySkillsRedux.Patches.OnDeathPatches"); if (type != null) { break; } } catch { } } if (type == null) { Logger.LogWarning((object)"[PvPOverhaul] FortifySkillsRedux OnDeathPatches type not found."); return; } MethodInfo methodInfo = AccessTools.DeclaredMethod(type, "OnDeathFinalizer", (Type[])null, (Type[])null); if (methodInfo == null) { Logger.LogWarning((object)"[PvPOverhaul] FortifySkillsRedux OnDeathFinalizer method not found."); return; } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(PvpSkillLossPatch), "FortifyOnDeathFinalizerPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Logger.LogInfo((object)"[PvPOverhaul] FortifySkillsRedux death finalizer patched."); } private static bool FortifyOnDeathFinalizerPrefix(Player __instance) { if (!ShouldProtectSkills(__instance)) { return true; } ForceFortifyLevelsToCurrentSkills(__instance); RestoreSnapshot(__instance); StartDelayedRestore(__instance); Logger.LogInfo((object)("[PvPOverhaul] FortifySkillsRedux death finalizer BLOCKED for PvP death: " + __instance.GetPlayerName())); return false; } private static void FortifyOnDeathFinalizerPostfix(Player __instance) { if (ShouldProtectSkills(__instance)) { RestoreSnapshot(__instance); StartDelayedRestore(__instance); } } private static void ForceFortifyLevelsToCurrentSkills(Player player) { //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)player == (Object)null || (Object)(object)player.m_skills == (Object)null) { return; } try { Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetTypes().FirstOrDefault((Type t) => t.FullName == "FortifySkillsRedux.FortifySkillData"); if (type != null) { break; } } catch { } } if (type == null) { return; } object obj2 = AccessTools.Field(type, "s_FortifySkills")?.GetValue(null); if (obj2 == null) { return; } foreach (object item in (IEnumerable)obj2) { object obj3 = item.GetType().GetProperty("Key")?.GetValue(item, null); object obj4 = item.GetType().GetProperty("Value")?.GetValue(item, null); if (obj3 != null && obj4 != null) { SkillType val = (SkillType)obj3; Skill skill = player.m_skills.GetSkill(val); if (skill != null) { FieldInfo fieldInfo = AccessTools.Field(obj4.GetType(), "FortifyLevel"); FieldInfo fieldInfo2 = AccessTools.Field(obj4.GetType(), "FortifyAccumulator"); fieldInfo?.SetValue(obj4, skill.m_level); fieldInfo2?.SetValue(obj4, skill.m_accumulator); } } } Logger.LogInfo((object)("[PvPOverhaul] Fortify levels forced to current skill levels for " + player.GetPlayerName())); } catch (Exception ex) { Logger.LogWarning((object)("[PvPOverhaul] Failed to force Fortify levels: " + ex.Message)); } } } [HarmonyPatch(typeof(Character), "Damage")] internal static class PvpSkillSnapshotOnDamagePatch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(Character __instance, HitData hit) { if (!PvPOverhaul.DisableSkillLossOnPvpDeath.Value) { return; } Player val = (Player)(object)((__instance is Player) ? __instance : null); if (val != null && hit != null) { Character attacker = hit.GetAttacker(); Player val2 = (Player)(object)((attacker is Player) ? attacker : null); if (val2 != null && !((Object)(object)val2 == (Object)(object)val)) { PvpSkillLossPatch.ForceSaveSnapshot(val); } } } } [HarmonyPatch(typeof(Skills), "OnDeath")] internal static class PvpSkillsOnDeathPatch { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(Skills __instance) { Player player = __instance.m_player; if (!PvpSkillLossPatch.ShouldProtectSkills(player)) { return true; } Logger.LogInfo((object)("[PvPOverhaul] Blocked vanilla Skills.OnDeath for PvP death: " + player.GetPlayerName())); return false; } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PvpPlayerOnDeathSkillRestorePatch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(Player __instance) { if (PvpSkillLossPatch.ShouldProtectSkills(__instance)) { PvpSkillLossPatch.ForceSaveSnapshot(__instance); } } [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Player __instance) { if (PvpSkillLossPatch.ShouldProtectSkills(__instance)) { PvpSkillLossPatch.RestoreSnapshot(__instance); PvpSkillLossPatch.StartDelayedRestore(__instance); } } [HarmonyFinalizer] [HarmonyPriority(0)] private static void Finalizer(Player __instance) { if (PvpSkillLossPatch.ShouldProtectSkills(__instance)) { PvpSkillLossPatch.RestoreSnapshot(__instance); PvpSkillLossPatch.StartDelayedRestore(__instance); } } }