Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of TidyDrops v1.0.0
plugins/TidyDrops.dll
Decompiled 2 months agousing 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, "Писать в лог причину решения по каждому предмету."); } } }