Decompiled source of TidyDrops v1.0.0

plugins/TidyDrops.dll

Decompiled 2 months ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("demit")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Drop cleanup for Valheim that respects item age, EpicLoot rarity and player bases.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("TidyDrops")]
[assembly: AssemblyTitle("TidyDrops")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace TidyDrops
{
	internal static class DropSweeper
	{
		private static string[] _neverDeleteCache = new string[0];

		private static string _neverDeleteRaw;

		public static void RunOnce()
		{
			TidyDropsConfig cfg = TidyDropsPlugin.Cfg;
			ManualLogSource log = TidyDropsPlugin.Log;
			RefreshNeverDelete(cfg.NeverDelete.Value);
			DateTime now = DateTime.Now;
			Stopwatch stopwatch = Stopwatch.StartNew();
			IList<ItemDrop> allDrops = GameBridge.GetAllDrops();
			if (allDrops == null || allDrops.Count == 0)
			{
				if (cfg.Verbose.Value)
				{
					log.LogInfo((object)$"{now:HH:mm:ss} — предметов на земле нет, проход пропущен.");
				}
				return;
			}
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			int value = cfg.MaxDeletionsPerPass.Value;
			if (cfg.Verbose.Value)
			{
				log.LogInfo((object)$"{now:HH:mm:ss} — проход начат, в списке {allDrops.Count} предмет(ов).");
			}
			foreach (ItemDrop item in allDrops)
			{
				if (value > 0 && num2 >= value)
				{
					log.LogInfo((object)$"{DateTime.Now:HH:mm:ss} достигнут предел MaxDeletionsPerPass ({value}), остальное — в следующий проход.");
					break;
				}
				if ((Object)(object)item == (Object)null)
				{
					continue;
				}
				num++;
				if (ShouldKeep(item, cfg, out var reason))
				{
					num3++;
					if (cfg.Verbose.Value && reason != null)
					{
						log.LogInfo((object)$"{DateTime.Now:HH:mm:ss} оставляю {Describe(item)}: {reason}");
					}
				}
				else if (cfg.DryRun.Value)
				{
					num2++;
					log.LogInfo((object)$"{DateTime.Now:HH:mm:ss} [DryRun] удалил бы {Describe(item)}");
				}
				else if (TryDestroy(item))
				{
					num2++;
					if (cfg.Verbose.Value)
					{
						log.LogInfo((object)$"{DateTime.Now:HH:mm:ss} удаляю {Describe(item)}");
					}
				}
			}
			stopwatch.Stop();
			if (num2 > 0 || cfg.Verbose.Value)
			{
				DateTime dateTime = DateTime.Now.AddSeconds(Math.Max(10f, cfg.CleanupInterval.Value));
				string arg = (cfg.DryRun.Value ? " [DryRun, ничего не удалено]" : string.Empty);
				log.LogInfo((object)($"{now:HH:mm:ss} — проход завершён за {stopwatch.ElapsedMilliseconds} мс{arg}: " + $"просмотрено {num}, удалено {num2}, оставлено {num3}. " + $"Следующий примерно в {dateTime:HH:mm:ss}."));
			}
		}

		private static bool ShouldKeep(ItemDrop drop, TidyDropsConfig cfg, out string reason)
		{
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: Invalid comparison between Unknown and I4
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0115: Unknown result type (might be due to invalid IL or missing references)
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			reason = null;
			if ((Object)(object)((Component)drop).transform.parent != (Object)null)
			{
				reason = "не лежит на земле";
				return true;
			}
			ItemData itemData = drop.m_itemData;
			if (itemData == null || itemData.m_shared == null)
			{
				reason = "нет ItemData";
				return true;
			}
			ZNetView component = ((Component)drop).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsValid())
			{
				reason = "ZNetView невалиден";
				return true;
			}
			string prefabName = GetPrefabName(drop);
			if (_neverDeleteCache.Length != 0 && _neverDeleteCache.Any((string n) => string.Equals(n, prefabName, StringComparison.OrdinalIgnoreCase)))
			{
				reason = "в списке NeverDelete";
				return true;
			}
			if (cfg.ProtectTrophies.Value && (int)itemData.m_shared.m_itemType == 13)
			{
				reason = "трофей";
				return true;
			}
			if (EpicLootBridge.ShouldKeep(itemData, cfg.MinimumRarityToKeep.Value, cfg.KeepAllMagicItems.Value, out var reason2))
			{
				reason = reason2 ?? "магический предмет";
				return true;
			}
			Vector3 position = ((Component)drop).transform.position;
			if (cfg.ProtectInsideBase.Value && GameBridge.IsInsidePlayerBase(position))
			{
				reason = "внутри базы";
				return true;
			}
			if (GameBridge.IsPlayerNear(position, cfg.PlayerSafeRadius.Value))
			{
				reason = "рядом игрок";
				return true;
			}
			if (!GameBridge.TryGetAgeSeconds(drop, component, out var seconds))
			{
				reason = "возраст неизвестен";
				return true;
			}
			if (seconds < (double)cfg.MinItemAgeSeconds.Value)
			{
				reason = $"слишком свежий ({seconds:0}s < {cfg.MinItemAgeSeconds.Value}s)";
				return true;
			}
			return false;
		}

		private static bool TryDestroy(ItemDrop drop)
		{
			try
			{
				ZNetView component = ((Component)drop).GetComponent<ZNetView>();
				if ((Object)(object)component == (Object)null || !component.IsValid())
				{
					return false;
				}
				if (!component.IsOwner())
				{
					component.ClaimOwnership();
				}
				if (!component.IsOwner())
				{
					return false;
				}
				component.Destroy();
				return true;
			}
			catch (Exception ex)
			{
				TidyDropsPlugin.Log.LogWarning((object)("Не смог удалить " + Describe(drop) + ": " + ex.Message));
				return false;
			}
		}

		private static string GetPrefabName(ItemDrop drop)
		{
			GameObject val = drop.m_itemData?.m_dropPrefab;
			if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(((Object)val).name))
			{
				return ((Object)val).name;
			}
			string name = ((Object)((Component)drop).gameObject).name;
			int num = name.IndexOf("(Clone)", StringComparison.Ordinal);
			if (num < 0)
			{
				return name;
			}
			return name.Substring(0, num);
		}

		private static string Describe(ItemDrop drop)
		{
			int num = ((drop.m_itemData != null) ? drop.m_itemData.m_stack : 0);
			return $"{GetPrefabName(drop)} x{num}";
		}

		private static void RefreshNeverDelete(string raw)
		{
			if (!(raw == _neverDeleteRaw))
			{
				_neverDeleteRaw = raw;
				_neverDeleteCache = (from s in (raw ?? string.Empty).Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
					select s.Trim() into s
					where s.Length > 0
					select s).ToArray();
			}
		}
	}
	internal static class EpicLootBridge
	{
		private const string AssemblyName = "EpicLoot";

		private static bool _probed;

		private static MethodInfo _hasRarity;

		private static MethodInfo _getRarity;

		private static Type _rarityEnum;

		private static bool _warnedBadConfig;

		public static bool Available { get; private set; }

		public static bool PresentButUnreadable { get; private set; }

		private static void EnsureProbed()
		{
			if (_probed)
			{
				return;
			}
			_probed = true;
			Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly a) => string.Equals(a.GetName().Name, "EpicLoot", StringComparison.OrdinalIgnoreCase));
			if (assembly == null)
			{
				TidyDropsPlugin.Log.LogInfo((object)"EpicLoot не обнаружен — фильтр по редкости не используется.");
				return;
			}
			try
			{
				Type type = assembly.GetType("EpicLoot.ItemDataExtensions", throwOnError: false);
				_rarityEnum = assembly.GetType("EpicLoot.ItemRarity", throwOnError: false);
				if (type != null && _rarityEnum != null)
				{
					Type typeFromHandle = typeof(ItemData);
					_hasRarity = type.GetMethod("HasRarity", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { typeFromHandle }, null);
					_getRarity = type.GetMethod("GetRarity", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { typeFromHandle }, null);
				}
			}
			catch (Exception ex)
			{
				TidyDropsPlugin.Log.LogWarning((object)("Не удалось прочитать API EpicLoot: " + ex.Message));
			}
			if (_hasRarity != null && _getRarity != null && _rarityEnum != null)
			{
				Available = true;
				TidyDropsPlugin.Log.LogInfo((object)("EpicLoot подключён. Известные редкости: " + string.Join(", ", Enum.GetNames(_rarityEnum)) + "."));
			}
			else
			{
				PresentButUnreadable = true;
				TidyDropsPlugin.Log.LogWarning((object)"EpicLoot найден, но его API не распознано. Магические предметы удаляться не будут.");
			}
		}

		public static bool ShouldKeep(ItemData item, string minRarityName, bool keepAll, out string reason)
		{
			reason = null;
			EnsureProbed();
			if (PresentButUnreadable)
			{
				reason = "EpicLoot есть, API не читается";
				return true;
			}
			if (!Available || item == null)
			{
				return false;
			}
			bool flag;
			try
			{
				flag = (bool)_hasRarity.Invoke(null, new object[1] { item });
			}
			catch
			{
				reason = "HasRarity упал";
				return true;
			}
			if (!flag)
			{
				return false;
			}
			if (keepAll)
			{
				reason = "KeepAllMagicItems";
				return true;
			}
			if (!string.IsNullOrEmpty(minRarityName) && minRarityName.Trim().Equals("None", StringComparison.OrdinalIgnoreCase))
			{
				return false;
			}
			int num;
			try
			{
				num = Convert.ToInt32(Enum.Parse(_rarityEnum, minRarityName.Trim(), ignoreCase: true));
			}
			catch
			{
				if (!_warnedBadConfig)
				{
					_warnedBadConfig = true;
					TidyDropsPlugin.Log.LogWarning((object)("MinimumRarityToKeep = '" + minRarityName + "' не распознано. Допустимо: None, " + string.Join(", ", Enum.GetNames(_rarityEnum)) + ". Пока значение не исправлено, магические предметы не удаляются."));
				}
				reason = "редкость в конфиге не распознана";
				return true;
			}
			int num2;
			try
			{
				num2 = Convert.ToInt32(_getRarity.Invoke(null, new object[1] { item }));
			}
			catch
			{
				reason = "GetRarity упал";
				return true;
			}
			if (num2 >= num)
			{
				reason = "редкость " + Enum.GetName(_rarityEnum, num2) + " >= " + Enum.GetName(_rarityEnum, num);
				return true;
			}
			return false;
		}
	}
	internal static class GameBridge
	{
		private static bool _init;

		private static FieldInfo _instancesField;

		private static MethodInfo _timeSinceSpawn;

		private static void EnsureInit()
		{
			if (!_init)
			{
				_init = true;
				_instancesField = typeof(ItemDrop).GetField("s_instances", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				if (_instancesField != null && !typeof(List<ItemDrop>).IsAssignableFrom(_instancesField.FieldType))
				{
					_instancesField = null;
				}
				_timeSinceSpawn = typeof(ItemDrop).GetMethod("GetTimeSinceSpawned", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
				if (_instancesField == null)
				{
					TidyDropsPlugin.Log.LogWarning((object)"ItemDrop.s_instances не найден — перехожу на FindObjectsOfType. Работает, но медленнее.");
				}
				if (_timeSinceSpawn == null)
				{
					TidyDropsPlugin.Log.LogWarning((object)"ItemDrop.GetTimeSinceSpawned не найден — возраст буду считать напрямую из ZDO.");
				}
			}
		}

		public static IList<ItemDrop> GetAllDrops()
		{
			EnsureInit();
			if (_instancesField != null && _instancesField.GetValue(null) is List<ItemDrop> list)
			{
				return list.ToArray();
			}
			return Object.FindObjectsOfType<ItemDrop>();
		}

		public static bool TryGetAgeSeconds(ItemDrop drop, ZNetView nview, out double seconds)
		{
			EnsureInit();
			seconds = 0.0;
			if (_timeSinceSpawn != null)
			{
				try
				{
					seconds = Convert.ToDouble(_timeSinceSpawn.Invoke(drop, null));
					return true;
				}
				catch
				{
				}
			}
			if ((Object)(object)nview == (Object)null || !nview.IsValid())
			{
				return false;
			}
			ZDO zDO = nview.GetZDO();
			if (zDO == null)
			{
				return false;
			}
			long num = zDO.GetLong(ZDOVars.s_spawnTime, 0L);
			if (num <= 0)
			{
				return false;
			}
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return false;
			}
			try
			{
				seconds = (instance.GetTime() - new DateTime(num)).TotalSeconds;
			}
			catch (ArgumentOutOfRangeException)
			{
				return false;
			}
			return seconds >= 0.0;
		}

		public static bool IsInsidePlayerBase(Vector3 position)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			return (Object)(object)EffectArea.IsPointInsideArea(position, (Type)4, 0f) != (Object)null;
		}

		public static bool IsPlayerNear(Vector3 position, float radius)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			if (radius <= 0f)
			{
				return false;
			}
			return Player.IsPlayerInRange(position, radius);
		}
	}
	[BepInPlugin("dev.demit.tidydrops", "TidyDrops", "1.0.0")]
	[BepInProcess("valheim.exe")]
	public sealed class TidyDropsPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "dev.demit.tidydrops";

		public const string PluginName = "TidyDrops";

		public const string PluginVersion = "1.0.0";

		private Coroutine _loop;

		internal static ManualLogSource Log { get; private set; }

		internal static TidyDropsConfig Cfg { get; private set; }

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			Cfg = new TidyDropsConfig(((BaseUnityPlugin)this).Config);
			_loop = ((MonoBehaviour)this).StartCoroutine(CleanupLoop());
			((BaseUnityPlugin)this).Logger.LogInfo((object)("TidyDrops 1.0.0 загружен. " + $"Интервал {Cfg.CleanupInterval.Value:0}s, минимальный возраст {Cfg.MinItemAgeSeconds.Value}s."));
		}

		private void OnDestroy()
		{
			if (_loop != null)
			{
				((MonoBehaviour)this).StopCoroutine(_loop);
				_loop = null;
			}
		}

		private IEnumerator CleanupLoop()
		{
			yield return (object)new WaitForSeconds(30f);
			while (true)
			{
				float num = Mathf.Max(10f, Cfg.CleanupInterval.Value);
				yield return (object)new WaitForSeconds(num);
				if (Cfg.Enabled.Value && IsAuthority())
				{
					try
					{
						DropSweeper.RunOnce();
					}
					catch (Exception arg)
					{
						((BaseUnityPlugin)this).Logger.LogError((object)$"Проход очистки упал, пропускаю: {arg}");
					}
				}
			}
		}

		private static bool IsAuthority()
		{
			ZNet instance = ZNet.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return false;
			}
			if ((Object)(object)ZNetScene.instance == (Object)null)
			{
				return false;
			}
			return instance.IsServer();
		}
	}
	internal sealed class TidyDropsConfig
	{
		public readonly ConfigEntry<bool> Enabled;

		public readonly ConfigEntry<float> CleanupInterval;

		public readonly ConfigEntry<int> MinItemAgeSeconds;

		public readonly ConfigEntry<int> MaxDeletionsPerPass;

		public readonly ConfigEntry<bool> ProtectTrophies;

		public readonly ConfigEntry<bool> ProtectInsideBase;

		public readonly ConfigEntry<float> PlayerSafeRadius;

		public readonly ConfigEntry<string> NeverDelete;

		public readonly ConfigEntry<string> MinimumRarityToKeep;

		public readonly ConfigEntry<bool> KeepAllMagicItems;

		public readonly ConfigEntry<bool> DryRun;

		public readonly ConfigEntry<bool> Verbose;

		public TidyDropsConfig(ConfigFile cfg)
		{
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Expected O, but got Unknown
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Expected O, but got Unknown
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c0: Expected O, but got Unknown
			//IL_0127: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Expected O, but got Unknown
			Enabled = cfg.Bind<bool>("General", "Enabled", true, "Полностью включает или выключает очистку.");
			CleanupInterval = cfg.Bind<float>("General", "CleanupInterval", 600f, new ConfigDescription("Секунд между проходами очистки. Мод НИКОГДА не меняет это значение сам.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 7200f), Array.Empty<object>()));
			MinItemAgeSeconds = cfg.Bind<int>("General", "MinItemAgeToDelete", 1200, new ConfigDescription("Предмет удаляется, только если пролежал дольше этого времени. Возраст берётся из ZDO-ключа spawntime — того же, которым пользуется сама игра.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 86400), Array.Empty<object>()));
			MaxDeletionsPerPass = cfg.Bind<int>("General", "MaxDeletionsPerPass", 200, new ConfigDescription("Предохранитель: не больше стольких предметов за один проход. 0 — без ограничения.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 5000), Array.Empty<object>()));
			ProtectTrophies = cfg.Bind<bool>("Filters", "ProtectTrophies", true, "Не удалять трофеи. Проверяется по ItemType.Trophy, поэтому трофеи из любых модов защищены автоматически — список имён вести не нужно.");
			ProtectInsideBase = cfg.Bind<bool>("Filters", "ProtectInsideBase", true, "Не удалять предметы внутри базы игрока (зона действия костра/верстака).");
			PlayerSafeRadius = cfg.Bind<float>("Filters", "PlayerSafeRadius", 30f, new ConfigDescription("Не удалять предметы, если игрок ближе этого расстояния. 0 — выключить проверку.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 200f), Array.Empty<object>()));
			NeverDelete = cfg.Bind<string>("Filters", "NeverDelete", "Coins,Amber,AmberPearl,Ruby,SilverNecklace", "Имена префабов через запятую, которые не удалять никогда. Регистр не важен.");
			MinimumRarityToKeep = cfg.Bind<string>("EpicLoot", "MinimumRarityToKeep", "Magic", "Магические предметы этой редкости и выше не удаляются. Допустимые значения читаются из самого EpicLoot (сейчас: Magic, Rare, Epic, Legendary, Mythic, Ancient). Значение None отключает защиту по редкости. Если значение не распознано — мод не удаляет НИ ОДИН магический предмет и пишет предупреждение.");
			KeepAllMagicItems = cfg.Bind<bool>("EpicLoot", "KeepAllMagicItems", false, "Если true — любой магический предмет неприкосновенен, MinimumRarityToKeep игнорируется.");
			DryRun = cfg.Bind<bool>("Debug", "DryRun", false, "Только считать и писать в лог, ничего не удалять. Полезно для проверки настроек.");
			Verbose = cfg.Bind<bool>("Debug", "Verbose", false, "Писать в лог причину решения по каждому предмету.");
		}
	}
}