Decompiled source of ItemIntegrity ServerOnly v0.2.0

ItemIntegrity.ServerOnly.dll

Decompiled 2 months ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using ItemIntegrity.Core;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ItemIntegrity.ServerOnly")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0-preview.1")]
[assembly: AssemblyProduct("ItemIntegrity.ServerOnly")]
[assembly: AssemblyTitle("ItemIntegrity.ServerOnly")]
[assembly: AssemblyVersion("0.2.0.0")]
namespace ItemIntegrity
{
	internal sealed class AuditStore
	{
		private readonly string _root = Path.Combine(Paths.ConfigPath, "ItemIntegrity.ServerOnly", "quarantine");

		private long _used;

		internal string Fault { get; private set; }

		internal AuditStore()
		{
			checked
			{
				try
				{
					Directory.CreateDirectory(_root);
					foreach (string item in Directory.EnumerateFiles(_root, "*", SearchOption.AllDirectories))
					{
						_used += new FileInfo(item).Length;
					}
				}
				catch (Exception ex)
				{
					Fault = ex.Message;
				}
			}
		}

		internal bool Snapshot(string kind, string context, byte[] bytes, int capMiB, out string path)
		{
			path = null;
			if (Fault != null)
			{
				return false;
			}
			try
			{
				if (bytes == null || bytes.Length > 2097152)
				{
					throw new IOException("Invalid backup payload size");
				}
				string path2 = DateTime.UtcNow.ToString("yyyyMMdd");
				string text = Path.Combine(_root, path2);
				string text2 = DateTime.UtcNow.ToString("HHmmssfff") + "-" + Guid.NewGuid().ToString("N");
				string text3 = Path.Combine(text, text2 + ".bin");
				string s = "{\"schema\":1,\"utc\":" + Quote(DateTime.UtcNow.ToString("O")) + ",\"kind\":" + Quote(kind) + ",\"context\":" + Quote(context) + ",\"sha256\":" + Quote(Hash(bytes)) + ",\"payload\":" + Quote(text2 + ".bin") + "}\n";
				byte[] bytes2 = Encoding.UTF8.GetBytes(s);
				if (_used + bytes.Length + bytes2.Length > (long)capMiB * 1024L * 1024)
				{
					throw new IOException("Quarantine size limit reached. Archive it and restart before further deletion.");
				}
				Directory.CreateDirectory(text);
				WriteNew(text3, bytes);
				WriteNew(Path.Combine(text, text2 + ".json"), bytes2);
				_used += bytes.Length + bytes2.Length;
				path = text3;
				return true;
			}
			catch (Exception ex)
			{
				Fault = ex.Message;
				return false;
			}
		}

		private static void WriteNew(string path, byte[] bytes)
		{
			using FileStream fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
			fileStream.Write(bytes, 0, bytes.Length);
			fileStream.Flush(flushToDisk: true);
		}

		internal static string Hash(byte[] bytes)
		{
			using SHA256 sHA = SHA256.Create();
			return BitConverter.ToString(sHA.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant();
		}

		internal static string Quote(string text)
		{
			StringBuilder stringBuilder = new StringBuilder("\"");
			string text2 = text ?? "";
			foreach (char c in text2)
			{
				if (c == '\\' || c == '"')
				{
					stringBuilder.Append('\\').Append(c);
				}
				else if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4"));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			return stringBuilder.Append('"').ToString();
		}
	}
	internal static class GameAccess
	{
		private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

		private static FieldInfo _objects;

		private static FieldInfo _containerRevision;

		private static FieldInfo _containerLoading;

		private static FieldInfo _containerView;

		private static FieldInfo _standView;

		private static FieldInfo _armorView;

		internal static void VerifyApi()
		{
			_objects = RequireField(typeof(ZDOMan), "m_objectsByID", typeof(Dictionary<ZDOID, ZDO>));
			_containerRevision = RequireField(typeof(Container), "m_lastRevision", typeof(uint));
			_containerLoading = RequireField(typeof(Container), "m_loading", typeof(bool));
			_containerView = RequireField(typeof(Container), "m_nview", typeof(ZNetView));
			_standView = RequireField(typeof(ItemStand), "m_nview", typeof(ZNetView));
			_armorView = RequireField(typeof(ArmorStand), "m_nview", typeof(ZNetView));
			int num = 0;
			foreach (object value in Enum.GetValues(typeof(Item)))
			{
				num = Math.Max(num, Convert.ToInt32(value));
			}
			if (num != 109)
			{
				throw new NotSupportedException("Game item format " + num + " is not the reviewed version 109");
			}
		}

		private static FieldInfo RequireField(Type type, string name, Type expected)
		{
			FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field == null || field.FieldType != expected)
			{
				throw new MissingFieldException(type.Name + "." + name);
			}
			return field;
		}

		internal static Dictionary<ZDOID, ZDO> Objects(ZDOMan man)
		{
			return (Dictionary<ZDOID, ZDO>)_objects.GetValue(man);
		}

		internal static ZNetView View(Container c)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Expected O, but got Unknown
			return (ZNetView)_containerView.GetValue(c);
		}

		internal static ZNetView View(ItemStand s)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Expected O, but got Unknown
			return (ZNetView)_standView.GetValue(s);
		}

		internal static ZNetView View(ArmorStand s)
		{
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Expected O, but got Unknown
			return (ZNetView)_armorView.GetValue(s);
		}

		internal static bool ContainerReady(Container c, ZDO z)
		{
			if ((bool)_containerLoading.GetValue(c))
			{
				return false;
			}
			if (!c.IsInUse())
			{
				return (uint)_containerRevision.GetValue(c) == z.DataRevision;
			}
			return true;
		}

		internal static int AttachedKey(int slot)
		{
			if (slot >= 0)
			{
				return StringExtensionMethods.GetStableHashCode(slot + "_item");
			}
			return ZDOVars.s_item;
		}

		internal static int ItemDataKey(int slot)
		{
			if (slot >= 0)
			{
				return StringExtensionMethods.GetStableHashCode(slot + "_itemData");
			}
			return ZDOVars.s_itemData;
		}

		internal static string PrefabName(int hash)
		{
			GameObject val = (((Object)(object)ObjectDB.instance != (Object)null) ? ObjectDB.instance.GetItemPrefab(hash) : null);
			if ((Object)(object)val == (Object)null && (Object)(object)ZNetScene.instance != (Object)null)
			{
				val = ZNetScene.instance.GetPrefab(hash);
			}
			if (!((Object)(object)val != (Object)null))
			{
				return "#" + hash;
			}
			return ((Object)val).name;
		}

		internal static byte[] SaveItem(ItemData item)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			val.Write((byte)109);
			item.Save(val);
			return val.GetArray();
		}

		internal static byte[] SaveInventory(Inventory inventory)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Expected O, but got Unknown
			ZPackage val = new ZPackage();
			inventory.Save(val);
			return val.GetArray();
		}

		internal static void NativeCodecSelfTest()
		{
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0032: Expected O, but got Unknown
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: 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_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_008d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009a: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cb: Expected O, but got Unknown
			GameObject itemPrefab = ObjectDB.instance.GetItemPrefab("Wood");
			if ((Object)(object)itemPrefab == (Object)null)
			{
				throw new InvalidOperationException("ObjectDB Wood prefab not available");
			}
			Inventory val = new Inventory("ItemIntegrity codec self-test", (Sprite)null, 8, 4);
			for (int i = 0; i < 4; i++)
			{
				ItemData val2 = new ItemData
				{
					m_dropPrefab = itemPrefab,
					m_cheated = ((i & 1) != 0),
					m_stack = ((i == 0) ? 1 : 50),
					m_quality = ((i < 2) ? 1 : 3),
					m_variant = ((i >= 2) ? 7 : 0),
					m_durability = 123.45f,
					m_worldLevel = 2,
					m_gridPos = new Vector2i(i, 1),
					m_pickedUp = true,
					m_equipped = (i == 2),
					m_crafterID = ((i >= 2) ? 123456789 : 0),
					m_crafterName = "codec-é-木"
				};
				if (i == 3)
				{
					for (int j = 0; j < 130; j++)
					{
						val2.m_customData["key" + j] = "value-木-" + new string('x', j);
					}
				}
				ItemCodec.Record record = ItemCodec.ReadDrop(SaveItem(val2));
				if (record.Cheated != val2.m_cheated || record.Stack != val2.m_stack || record.Quality != val2.m_quality || record.PrefabHash != StringExtensionMethods.GetStableHashCode(((Object)itemPrefab).name))
				{
					throw new InvalidDataException("Native item codec self-test mismatch");
				}
				val.GetAllItems().Add(val2);
			}
			ItemCodec.InventoryData inventoryData = ItemCodec.ReadInventory(SaveInventory(val));
			if (inventoryData.Records.Count != 4)
			{
				throw new InvalidDataException("Native inventory count mismatch");
			}
			ItemCodec.InventoryData inventoryData2 = ItemCodec.ReadInventory(inventoryData.Without(new HashSet<int> { 1, 3 }));
			if (inventoryData2.Records.Count != 2 || inventoryData2.Records.Any((ItemCodec.Record r) => r.Cheated))
			{
				throw new InvalidDataException("Native inventory filter mismatch");
			}
		}
	}
	[HarmonyPatch(typeof(Terminal), "InitTerminal")]
	internal static class ConsolePatch
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__0_0;

			public static ConsoleEvent <>9__0_1;

			public static ConsoleEvent <>9__0_2;

			internal void <Postfix>b__0_0(ConsoleEventArgs a)
			{
				a.Context.AddString(((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.Status() : "Not initialized");
			}

			internal void <Postfix>b__0_1(ConsoleEventArgs a)
			{
				Plugin.Instance?.CommandScan(a.Context);
			}

			internal void <Postfix>b__0_2(ConsoleEventArgs a)
			{
				Plugin.Instance?.CommandReload(a.Context);
			}
		}

		private static void Postfix()
		{
			//IL_0033: 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_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0029: Expected O, but got Unknown
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0057: 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_0062: Expected O, but got Unknown
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0090: Unknown result type (might be due to invalid IL or missing references)
			//IL_0095: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Expected O, but got Unknown
			object obj = <>c.<>9__0_0;
			if (obj == null)
			{
				ConsoleEvent val = delegate(ConsoleEventArgs a)
				{
					a.Context.AddString(((Object)(object)Plugin.Instance != (Object)null) ? Plugin.Instance.Status() : "Not initialized");
				};
				<>c.<>9__0_0 = val;
				obj = (object)val;
			}
			new ConsoleCommand("itemintegrity_status", "Show server-only world cleaner state", (ConsoleEvent)obj, false, false, true, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
			object obj2 = <>c.<>9__0_1;
			if (obj2 == null)
			{
				ConsoleEvent val2 = delegate(ConsoleEventArgs a)
				{
					Plugin.Instance?.CommandScan(a.Context);
				};
				<>c.<>9__0_1 = val2;
				obj2 = (object)val2;
			}
			new ConsoleCommand("itemintegrity_scan", "Request a server-only world scan", (ConsoleEvent)obj2, false, false, true, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
			object obj3 = <>c.<>9__0_2;
			if (obj3 == null)
			{
				ConsoleEvent val3 = delegate(ConsoleEventArgs a)
				{
					Plugin.Instance?.CommandReload(a.Context);
				};
				<>c.<>9__0_2 = val3;
				obj3 = (object)val3;
			}
			new ConsoleCommand("itemintegrity_reload", "Reload server-only cleaner settings", (ConsoleEvent)obj3, false, false, true, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
		}
	}
	[BepInPlugin("local.valheim.itemintegrity.serveronly", "ItemIntegrity.ServerOnly", "0.2.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "local.valheim.itemintegrity.serveronly";

		public const string BuildVersion = "0.2.0";

		public const string Release = "0.2.0-preview.1 (source preview; staging required)";

		private Harmony _harmony;

		private AuditStore _audit;

		private WorldCleaner _world;

		private ServerLocalCleaner _local;

		private ZNet _net;

		private ZDOMan _man;

		private bool _apiOk;

		private bool _nativeOk;

		private bool _nativeAttempted;

		private bool _oldConflict;

		private double _nextLocal;

		private double _nextSummary;

		private double _nextChecks;

		private double _lastPeerSeen;

		private readonly Dictionary<string, double> _rate = new Dictionary<string, double>();

		internal static Plugin Instance { get; private set; }

		internal static double Now => Time.realtimeSinceStartup;

		internal static bool IsDedicatedServer
		{
			get
			{
				if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer())
				{
					return ZNet.instance.IsDedicated();
				}
				return false;
			}
		}

		internal static int ConnectedPeers
		{
			get
			{
				if (!((Object)(object)ZNet.instance != (Object)null))
				{
					return 0;
				}
				return ZNet.instance.GetPeers().Count;
			}
		}

		internal Settings Options { get; private set; }

		internal bool Operational
		{
			get
			{
				if (IsDedicatedServer && _apiOk && _nativeOk)
				{
					return !_oldConflict;
				}
				return false;
			}
		}

		internal Policy ServerPolicy { get; private set; } = new Policy
		{
			Enabled = false
		};

		internal double EmptySeconds
		{
			get
			{
				if (!IsDedicatedServer || ConnectedPeers != 0)
				{
					return 0.0;
				}
				return Math.Max(0.0, Now - _lastPeerSeen);
			}
		}

		internal bool StorageWritesAllowed
		{
			get
			{
				if (Operational && ConnectedPeers == 0)
				{
					return EmptySeconds >= 15.0;
				}
				return false;
			}
		}

		private void Awake()
		{
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			Instance = this;
			Options = new Settings(((BaseUnityPlugin)this).Config);
			_audit = new AuditStore();
			_local = new ServerLocalCleaner(this);
			_world = new WorldCleaner(this);
			try
			{
				GameAccess.VerifyApi();
				_harmony = new Harmony("local.valheim.itemintegrity.serveronly");
				_harmony.PatchAll(Assembly.GetExecutingAssembly());
				_apiOk = true;
			}
			catch (Exception ex)
			{
				_apiOk = false;
				Harmony harmony = _harmony;
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
				((BaseUnityPlugin)this).Logger.LogError((object)("[paused] API/patch validation failed. Nothing will be cleaned: " + ex));
			}
			Info("[startup] 0.2.0-preview.1 (source preview; staging required); dedicated-server only; no client component or custom client protocol; dryRun=" + Options.DryRun.Value);
			Info("[scope] Ground drops: online ZDO cleanup. Containers/stands: empty-server maintenance. Carried/equipped player inventories: NOT scanned or changed.");
			if (_audit.Fault != null)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("[backup-unavailable] deletion will be refused: " + _audit.Fault));
			}
		}

		private void Update()
		{
			try
			{
				Tick();
			}
			catch (Exception ex)
			{
				Warn("update", "[update-error] " + ex);
			}
		}

		private void Tick()
		{
			ZNet instance = ZNet.instance;
			ZDOMan instance2 = ZDOMan.instance;
			if (instance != _net || instance2 != _man)
			{
				ResetSession(instance, instance2);
			}
			if (!IsDedicatedServer)
			{
				return;
			}
			double now = Now;
			if (ConnectedPeers > 0)
			{
				_lastPeerSeen = now;
			}
			if (now >= _nextChecks)
			{
				_nextChecks = now + 1.0;
				_oldConflict = Chainloader.PluginInfos.ContainsKey("ageous27.nocheatitems") || Chainloader.PluginInfos.ContainsKey("local.valheim.itemintegrity");
				if (_oldConflict)
				{
					Warn("old-plugin", "[paused] Disable NoCheatItems and the earlier client-required ItemIntegrity before using this replacement.");
				}
				if (_apiOk && !_oldConflict && !_nativeAttempted && instance2 != null && (Object)(object)ZNetScene.instance != (Object)null && (Object)(object)ObjectDB.instance != (Object)null && (Object)(object)ObjectDB.instance.GetItemPrefab("Wood") != (Object)null)
				{
					_nativeAttempted = true;
					try
					{
						GameAccess.NativeCodecSelfTest();
						_nativeOk = true;
						Info("[native-codec PASS] version=109; real native item/inventory writer matched the checked decoder");
					}
					catch (Exception ex)
					{
						_nativeOk = false;
						((BaseUnityPlugin)this).Logger.LogError((object)("[paused] Native serialization check failed; deletion disabled: " + ex));
					}
				}
				ServerPolicy = Options.MakePolicy(Operational);
			}
			if (Operational && ServerPolicy.Enabled)
			{
				_world.Tick(ServerPolicy);
				if (now >= _nextLocal)
				{
					_nextLocal = now + (double)Options.SweepSeconds.Value;
					ServerLocalCleaner.Counts counts = _local.Sweep(ServerPolicy);
					if (counts.Inspected > 0)
					{
						Info("[server-local-world-sweep] inspected=" + counts.Inspected + " matched=" + counts.Matched + " removed=" + counts.Removed + " storageWritesAllowed=" + StorageWritesAllowed + " dryRun=" + ServerPolicy.DryRun);
					}
				}
			}
			if (now >= _nextSummary)
			{
				_nextSummary = now + 30.0;
				Info("[status] " + Status());
			}
		}

		private void ResetSession(ZNet net, ZDOMan man)
		{
			_world = new WorldCleaner(this);
			_net = net;
			_man = man;
			_nativeOk = false;
			_nativeAttempted = false;
			_nextLocal = (_nextSummary = (_nextChecks = 0.0));
			_lastPeerSeen = Now;
			ServerPolicy = new Policy
			{
				Enabled = false
			};
			_rate.Clear();
		}

		internal bool Backup(string kind, string context, byte[] bytes)
		{
			string path;
			bool num = _audit.Snapshot(kind, context, bytes, Options.QuarantineMiB.Value, out path);
			if (!num)
			{
				Warn("audit", "[deletion-blocked] backup failed: " + _audit.Fault);
			}
			return num;
		}

		internal void Info(string message)
		{
			((BaseUnityPlugin)this).Logger.LogInfo((object)message);
		}

		internal void Warn(string key, string message)
		{
			Throttled(key, message, warning: true);
		}

		internal void Note(string key, string message)
		{
			Throttled(key, message, warning: false);
		}

		private void Throttled(string key, string message, bool warning)
		{
			if (!_rate.TryGetValue(key, out var value) || !(Now < value))
			{
				if (_rate.Count >= 4096)
				{
					_rate.Clear();
				}
				_rate[key] = Now + 30.0;
				if (warning)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				}
				else
				{
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
				}
			}
		}

		internal static string Label(string value)
		{
			string text = (value ?? "").Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' ');
			if (text.Length <= 180)
			{
				return text;
			}
			return text.Substring(0, 180);
		}

		internal string Status()
		{
			string text = (((Object)(object)ZNet.instance == (Object)null) ? "menu" : (IsDedicatedServer ? "dedicated-server" : "inactive-non-dedicated-process"));
			return "release=0.2.0-preview.1 (source preview; staging required) role=" + text + " api=" + _apiOk + " nativeCodec=" + _nativeOk + " conflictingCleaner=" + _oldConflict + " enabled=" + ServerPolicy.Enabled + " dryRun=" + ServerPolicy.DryRun + " peers=" + ConnectedPeers + " emptySeconds=" + EmptySeconds.ToString("F1") + " storageWritesAllowed=" + StorageWritesAllowed + " playerBags=not-covered auditFault=" + Label(_audit?.Fault ?? "none") + " " + _world.Status;
		}

		internal void CommandScan(Terminal terminal)
		{
			if (!IsDedicatedServer)
			{
				terminal.AddString("This command only acts inside the dedicated-server process.");
				return;
			}
			_world.Request();
			_nextLocal = 0.0;
			terminal.AddString("World scan requested. It does not bypass dry-run, backups, or the empty-server storage gate. " + Status());
		}

		internal void CommandReload(Terminal terminal)
		{
			if (!IsDedicatedServer)
			{
				terminal.AddString("Run reload in the dedicated-server console.");
				return;
			}
			((BaseUnityPlugin)this).Config.Reload();
			ServerPolicy = Options.MakePolicy(Operational);
			_world = new WorldCleaner(this);
			_lastPeerSeen = Now;
			terminal.AddString("Server settings reloaded; stored-item grace reset. No client action required. " + Status());
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			if (Instance == this)
			{
				Instance = null;
			}
		}
	}
	internal sealed class ServerLocalCleaner
	{
		internal struct Counts
		{
			internal int Inspected;

			internal int Matched;

			internal int Removed;
		}

		private readonly Plugin _p;

		private bool _busy;

		internal ServerLocalCleaner(Plugin plugin)
		{
			_p = plugin;
		}

		internal unsafe Counts Sweep(Policy policy)
		{
			//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d0: Unknown result type (might be due to invalid IL or missing references)
			Counts counts = default(Counts);
			if (_busy || !Plugin.IsDedicatedServer || policy == null || !policy.Enabled || !_p.Operational)
			{
				return counts;
			}
			_busy = true;
			try
			{
				HashSet<Inventory> hashSet = new HashSet<Inventory>();
				if (policy.ScanContainers)
				{
					Container[] array = Object.FindObjectsByType<Container>((FindObjectsInactive)0, (FindObjectsSortMode)0);
					foreach (Container val in array)
					{
						if ((Object)(object)val == (Object)null)
						{
							continue;
						}
						try
						{
							ZNetView val2 = GameAccess.View(val);
							if (Owned(val2) && GameAccess.ContainerReady(val, val2.GetZDO()))
							{
								Inventory inventory = val.GetInventory();
								if (inventory != null && hashSet.Add(inventory))
								{
									string text = Plugin.Label(((Object)val).name);
									ZDOID uid = val2.GetZDO().m_uid;
									CleanInventory(inventory, val2, "container=" + text + " zdo=" + ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString(), policy, ref counts);
								}
							}
						}
						catch (Exception ex)
						{
							_p.Warn("local-container", "[local-scan-error] " + Plugin.Label(ex.Message));
						}
					}
				}
				if (policy.ScanStands)
				{
					ItemStand[] array2 = Object.FindObjectsByType<ItemStand>((FindObjectsInactive)0, (FindObjectsSortMode)0);
					foreach (ItemStand s in array2)
					{
						if ((Object)(object)s == (Object)null)
						{
							continue;
						}
						ZNetView val3 = GameAccess.View(s);
						if (Owned(val3))
						{
							CleanAttachment(val3.GetZDO(), -1, delegate
							{
								s.RPC_DestroyAttachment(ZNet.GetUID());
							}, policy, ref counts);
						}
					}
					ArmorStand[] array3 = Object.FindObjectsByType<ArmorStand>((FindObjectsInactive)0, (FindObjectsSortMode)0);
					foreach (ArmorStand s2 in array3)
					{
						if ((Object)(object)s2 == (Object)null || s2.m_slots == null)
						{
							continue;
						}
						ZNetView val4 = GameAccess.View(s2);
						if (!Owned(val4))
						{
							continue;
						}
						for (int num = 0; num < Math.Min(s2.m_slots.Count, 64); num++)
						{
							int slot = num;
							CleanAttachment(val4.GetZDO(), slot, delegate
							{
								s2.RPC_DestroyAttachment(ZNet.GetUID(), slot);
							}, policy, ref counts);
						}
					}
				}
			}
			finally
			{
				_busy = false;
			}
			return counts;
		}

		private static bool Owned(ZNetView view)
		{
			//IL_0032: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)view == (Object)null || !view.IsValid() || !view.IsOwner() || ZDOMan.instance == null)
			{
				return false;
			}
			ZDO zDO = view.GetZDO();
			if (zDO != null)
			{
				return ZDOMan.instance.GetZDO(zDO.m_uid) == zDO;
			}
			return false;
		}

		private void CleanInventory(Inventory inventory, ZNetView view, string context, Policy policy, ref Counts counts)
		{
			ItemData[] array = inventory.GetAllItems().ToArray();
			foreach (ItemData val in array)
			{
				if (!Owned(view))
				{
					break;
				}
				if (val == null || !inventory.ContainsItem(val))
				{
					continue;
				}
				counts.Inspected++;
				string text = (((Object)(object)val.m_dropPrefab != (Object)null) ? ((Object)val.m_dropPrefab).name : "<missing prefab>");
				string text2 = policy.Reason(val.m_cheated, text);
				if (text2 == null)
				{
					continue;
				}
				counts.Matched++;
				string text3 = context + " item=" + Plugin.Label(text) + " stack=" + val.m_stack + " reason=" + text2;
				if (policy.DryRun)
				{
					_p.Note("dry:" + context + ":" + text, "[dry-run server-local-container] " + text3);
					continue;
				}
				if (!_p.StorageWritesAllowed || counts.Removed >= _p.Options.ActionsPerSweep.Value)
				{
					_p.Note("defer-local:" + context, "[deferred stored-items] " + context + " waiting for an empty server and edit budget");
					continue;
				}
				try
				{
					byte[] array2 = GameAccess.SaveItem(val);
					ItemCodec.Record record = ItemCodec.ReadDrop(array2);
					if (record.Stack != val.m_stack || record.Quality != val.m_quality)
					{
						throw new InvalidDataException("Native serialization changed stack/quality; refusing lossy backup");
					}
					if (_p.Backup("inventory-item", text3, array2) && _p.StorageWritesAllowed && Owned(view) && inventory.ContainsItem(val) && policy.Reason(val.m_cheated, text) != null)
					{
						bool flag = inventory.RemoveItem(val);
						bool flag2 = !inventory.ContainsItem(val);
						if (flag && flag2)
						{
							counts.Removed++;
							_p.Info("[removed server-local-container] " + text3);
							continue;
						}
						_p.Warn("remove:" + context, "[remove-unconfirmed] " + text3 + " nativeResult=" + flag + " absent=" + flag2);
					}
				}
				catch (Exception ex)
				{
					_p.Warn("remove-error:" + context, "[remove-error] " + text3 + "; an observer may have run after mutation: " + Plugin.Label(ex.Message));
				}
			}
		}

		private unsafe void CleanAttachment(ZDO z, int slot, Action remove, Policy policy, ref Counts counts)
		{
			//IL_021d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0222: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_017a: Unknown result type (might be due to invalid IL or missing references)
			int num = GameAccess.AttachedKey(slot);
			int num2 = z.GetInt(num, 0);
			if (num2 == 0)
			{
				return;
			}
			counts.Inspected++;
			try
			{
				byte[] byteArray = z.GetByteArray(GameAccess.ItemDataKey(slot), (byte[])null);
				if (byteArray == null || byteArray.Length == 0)
				{
					return;
				}
				ItemCodec.Record record = ItemCodec.ReadDrop(byteArray);
				if (record.PrefabHash != 0 && record.PrefabHash != num2)
				{
					throw new InvalidDataException("Attachment hash mismatch");
				}
				string text = GameAccess.PrefabName(num2);
				string text2 = policy.Reason(record.Cheated, text);
				if (text2 == null)
				{
					return;
				}
				counts.Matched++;
				string[] obj = new string[8] { "stand=", null, null, null, null, null, null, null };
				ZDOID uid = z.m_uid;
				obj[1] = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
				obj[2] = " slot=";
				obj[3] = slot.ToString();
				obj[4] = " item=";
				obj[5] = Plugin.Label(text);
				obj[6] = " reason=";
				obj[7] = text2;
				string text3 = string.Concat(obj);
				if (policy.DryRun)
				{
					_p.Note("dry:" + text3, "[dry-run attachment] " + text3);
				}
				else if (_p.StorageWritesAllowed && counts.Removed < _p.Options.ActionsPerSweep.Value && _p.Backup("attachment", text3, byteArray) && _p.StorageWritesAllowed && z.IsOwner() && ZDOMan.instance.GetZDO(z.m_uid) == z && z.GetInt(num, 0) == num2 && byteArray.SequenceEqual(z.GetByteArray(GameAccess.ItemDataKey(slot), (byte[])null) ?? Array.Empty<byte>()))
				{
					remove();
					if (z.GetInt(num, 0) == 0)
					{
						counts.Removed++;
						_p.Info("[removed attachment] " + text3);
					}
					else
					{
						_p.Warn("stand:" + text3, "[remove-unconfirmed attachment] " + text3);
					}
				}
			}
			catch (Exception ex)
			{
				Plugin p = _p;
				ZDOID uid = z.m_uid;
				p.Warn("stand:" + ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString() + ":" + slot, "[attachment-skip] " + Plugin.Label(ex.Message));
			}
		}
	}
	internal sealed class Settings
	{
		internal readonly ConfigEntry<bool> Enabled;

		internal readonly ConfigEntry<bool> DryRun;

		internal readonly ConfigEntry<bool> RemoveFlagged;

		internal readonly ConfigEntry<bool> RemoveKnown;

		internal readonly ConfigEntry<bool> ScanDrops;

		internal readonly ConfigEntry<bool> ScanContainers;

		internal readonly ConfigEntry<bool> ScanStands;

		internal readonly ConfigEntry<string> ExtraPrefabs;

		internal readonly ConfigEntry<float> SweepSeconds;

		internal readonly ConfigEntry<float> FrameMilliseconds;

		internal readonly ConfigEntry<int> RecordsPerFrame;

		internal readonly ConfigEntry<int> ActionsPerSweep;

		internal readonly ConfigEntry<int> QuarantineMiB;

		internal Settings(ConfigFile c)
		{
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_011e: Expected O, but got Unknown
			//IL_014a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0154: Expected O, but got Unknown
			//IL_0183: Unknown result type (might be due to invalid IL or missing references)
			//IL_018d: Expected O, but got Unknown
			//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Expected O, but got Unknown
			//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f5: Expected O, but got Unknown
			Enabled = c.Bind<bool>("General", "Enabled", true, "Dedicated-server-only master switch. No client plugin is used.");
			DryRun = c.Bind<bool>("General", "DryRun", true, "SAFE DEFAULT: log matches without altering world data. Set false only after backups and staging validation.");
			RemoveFlagged = c.Bind<bool>("Detection", "RemoveFlaggedItems", true, "Remove records/items carrying the native cheated flag.");
			RemoveKnown = c.Bind<bool>("Detection", "RemoveKnownCheatPrefabs", true, "Remove SwordCheat and SledgeCheat.");
			ExtraPrefabs = c.Bind<string>("Detection", "ExtraPrefabNames", "", "Comma-separated exact prefab names. Deletes legitimate copies too when enabled. Maximum 4096 characters.");
			ScanDrops = c.Bind<bool>("Scan", "WorldDrops", true, "Dedicated-server ZDO scan of serialized ground items.");
			ScanContainers = c.Bind<bool>("Scan", "Containers", true, "Scan native container inventories. Removal waits until no peers are connected for at least 15 seconds; carried player inventory is not covered.");
			ScanStands = c.Bind<bool>("Scan", "ItemAndArmorStands", true, "Scan attached item flags. Removal waits until the server is empty for at least 15 seconds; the stand is retained.");
			SweepSeconds = c.Bind<float>("Server", "WorldSweepSeconds", 8f, new ConfigDescription("Delay between complete ZDO passes. Large worlds take longer than this to complete a pass.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 120f), Array.Empty<object>()));
			RecordsPerFrame = c.Bind<int>("Server", "RecordsPerFrame", 128, new ConfigDescription("ZDO scan batch limit. Snapshotting IDs is a separate main-thread operation.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(16, 4096), Array.Empty<object>()));
			FrameMilliseconds = c.Bind<float>("Server", "FrameBudgetMilliseconds", 2f, new ConfigDescription("Soft per-frame scan budget; one parse or disk write can exceed it.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 20f), Array.Empty<object>()));
			ActionsPerSweep = c.Bind<int>("Safety", "MaximumEditsPerWorldSweep", 100, new ConfigDescription("Maximum world-object edits per pass; excess matches wait for later passes.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 1000), Array.Empty<object>()));
			QuarantineMiB = c.Bind<int>("Safety", "MaximumQuarantineMiB", 256, new ConfigDescription("Local backup cap. Reaching it blocks deletion; nothing is automatically pruned.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(16, 8192), Array.Empty<object>()));
		}

		internal Policy MakePolicy(bool ready)
		{
			string text = ExtraPrefabs.Value ?? "";
			if (text.Length > 4096)
			{
				throw new InvalidOperationException("ExtraPrefabNames is too long");
			}
			return new Policy
			{
				Enabled = (ready && Enabled.Value),
				DryRun = DryRun.Value,
				RemoveFlagged = RemoveFlagged.Value,
				RemoveKnown = RemoveKnown.Value,
				ScanDrops = ScanDrops.Value,
				ScanContainers = ScanContainers.Value,
				ScanStands = ScanStands.Value,
				ExtraPrefabs = text
			};
		}
	}
	internal sealed class WorldCleaner
	{
		private sealed class Candidate
		{
			internal string Hash;

			internal long Owner;

			internal uint Revision;

			internal double Since;

			internal double Seen;
		}

		private sealed class Shape
		{
			internal bool Drop;

			internal bool Container;

			internal bool Stand;

			internal int ArmorSlots;

			internal string Name;
		}

		private readonly Plugin _p;

		private ZDOID[] _ids;

		private int _cursor;

		private int _visited;

		private int _drops;

		private int _inventories;

		private int _stands;

		private int _matches;

		private int _edits;

		private int _deferred;

		private int _skipped;

		private double _nextSweep;

		private readonly Dictionary<ZDOID, double> _pendingDrops = new Dictionary<ZDOID, double>();

		private readonly Dictionary<string, Candidate> _stable = new Dictionary<string, Candidate>();

		private readonly Dictionary<int, Shape> _shapes = new Dictionary<int, Shape>();

		internal string Status
		{
			get
			{
				string[] obj = new string[8]
				{
					"inPass=",
					(_ids != null).ToString(),
					" cursor=",
					_cursor.ToString(),
					"/",
					null,
					null,
					null
				};
				ZDOID[] ids = _ids;
				obj[5] = ((ids != null) ? ids.Length : 0).ToString();
				obj[6] = " pendingDropDeletes=";
				obj[7] = _pendingDrops.Count.ToString();
				return string.Concat(obj);
			}
		}

		internal WorldCleaner(Plugin p)
		{
			_p = p;
		}

		internal void Request()
		{
			if (_ids == null)
			{
				_nextSweep = 0.0;
			}
		}

		internal unsafe void Tick(Policy policy)
		{
			//IL_0225: Unknown result type (might be due to invalid IL or missing references)
			//IL_0227: Unknown result type (might be due to invalid IL or missing references)
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_0242: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			if (policy == null || !policy.Enabled || !Plugin.IsDedicatedServer || !_p.Operational || ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null)
			{
				return;
			}
			ConfirmDeletes();
			double now = Plugin.Now;
			if (_ids == null)
			{
				if (now < _nextSweep)
				{
					return;
				}
				Dictionary<ZDOID, ZDO> dictionary = GameAccess.Objects(ZDOMan.instance);
				if (dictionary.Count > 2000000)
				{
					_p.Warn("snapshot-cap", "[world-paused] more than two million ZDOs; snapshot safety cap reached");
					_nextSweep = now + 30.0;
					return;
				}
				_ids = (ZDOID[])(object)new ZDOID[dictionary.Count];
				dictionary.Keys.CopyTo(_ids, 0);
				_cursor = (_visited = (_drops = (_inventories = (_stands = (_matches = (_edits = (_deferred = (_skipped = 0))))))));
				string[] array = (from kv in _stable
					where now - kv.Value.Seen > (double)Math.Max(120f, _p.Options.SweepSeconds.Value * 4f)
					select kv.Key).ToArray();
				foreach (string key in array)
				{
					_stable.Remove(key);
				}
			}
			Stopwatch stopwatch = Stopwatch.StartNew();
			int num2 = 0;
			while (_cursor < _ids.Length && num2++ < _p.Options.RecordsPerFrame.Value && stopwatch.Elapsed.TotalMilliseconds < (double)_p.Options.FrameMilliseconds.Value)
			{
				ZDOID val = _ids[_cursor++];
				ZDO zDO = ZDOMan.instance.GetZDO(val);
				if (zDO != null)
				{
					_visited++;
					try
					{
						Inspect(zDO, policy);
					}
					catch (Exception ex)
					{
						_skipped++;
						Plugin p = _p;
						ZDOID val2 = val;
						string key2 = "world:" + ((object)(*(ZDOID*)(&val2))/*cast due to .constrained prefix*/).ToString();
						val2 = val;
						p.Warn(key2, "[world-skip] zdo=" + ((object)(*(ZDOID*)(&val2))/*cast due to .constrained prefix*/).ToString() + " reason=" + Plugin.Label(ex.Message));
					}
				}
			}
			if (_cursor == _ids.Length)
			{
				_p.Info("[world-sweep] zdos=" + _visited + " dropRecords=" + _drops + " inventories=" + _inventories + " standAttachments=" + _stands + " matchingStacks=" + _matches + " objectEdits=" + _edits + " deferred=" + _deferred + " skipped=" + _skipped + " storageWritesAllowed=" + _p.StorageWritesAllowed + " dryRun=" + policy.DryRun);
				_ids = null;
				_nextSweep = now + (double)_p.Options.SweepSeconds.Value;
			}
		}

		private Shape Classify(int hash)
		{
			if (_shapes.TryGetValue(hash, out var value))
			{
				return value;
			}
			GameObject prefab = ZNetScene.instance.GetPrefab(hash);
			if ((Object)(object)prefab == (Object)null)
			{
				return null;
			}
			ZNetView root = prefab.GetComponent<ZNetView>();
			if ((Object)(object)root == (Object)null)
			{
				return null;
			}
			bool container = prefab.GetComponentsInChildren<Container>(true).Any((Container c) => (Object)(object)(((Object)(object)c.m_rootObjectOverride != (Object)null) ? c.m_rootObjectOverride : ((Component)c).GetComponent<ZNetView>()) == (Object)(object)root);
			bool stand = prefab.GetComponentsInChildren<ItemStand>(true).Any((ItemStand s) => (Object)(object)(((Object)(object)s.m_netViewOverride != (Object)null) ? s.m_netViewOverride : ((Component)s).GetComponent<ZNetView>()) == (Object)(object)root);
			ArmorStand val = ((IEnumerable<ArmorStand>)prefab.GetComponentsInChildren<ArmorStand>(true)).FirstOrDefault((Func<ArmorStand, bool>)((ArmorStand s) => (Object)(object)(((Object)(object)s.m_netViewOverride != (Object)null) ? s.m_netViewOverride : ((Component)s).GetComponent<ZNetView>()) == (Object)(object)root));
			Shape shape = new Shape
			{
				Name = ((Object)prefab).name,
				Drop = ((Object)(object)prefab.GetComponent<ItemDrop>() != (Object)null),
				Container = container,
				Stand = stand,
				ArmorSlots = (((Object)(object)val != (Object)null && val.m_slots != null) ? Math.Min(val.m_slots.Count, 64) : 0)
			};
			if (_shapes.Count < 10000)
			{
				_shapes[hash] = shape;
			}
			return shape;
		}

		private void Inspect(ZDO z, Policy policy)
		{
			Shape shape = Classify(z.GetPrefab());
			if (shape == null)
			{
				_skipped++;
				return;
			}
			if (shape.Drop && policy.ScanDrops)
			{
				InspectDrop(z, shape, policy);
				return;
			}
			if (shape.Container && policy.ScanContainers)
			{
				InspectContainer(z, shape, policy);
			}
			if (policy.ScanStands)
			{
				if (shape.Stand)
				{
					InspectAttachment(z, -1, policy);
				}
				for (int i = 0; i < shape.ArmorSlots; i++)
				{
					InspectAttachment(z, i, policy);
				}
			}
		}

		private unsafe void InspectDrop(ZDO z, Shape shape, Policy policy)
		{
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0119: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Unknown result type (might be due to invalid IL or missing references)
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_018b: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_023d: Unknown result type (might be due to invalid IL or missing references)
			byte[] byteArray = z.GetByteArray(ZDOVars.s_itemData, (byte[])null);
			if (byteArray == null || byteArray.Length == 0)
			{
				return;
			}
			_drops++;
			ItemCodec.Record record = ItemCodec.ReadDrop(byteArray);
			if (record.PrefabHash != 0 && record.PrefabHash != z.GetPrefab())
			{
				throw new InvalidDataException("Drop prefab/record mismatch");
			}
			string text = policy.Reason(record.Cheated, shape.Name);
			if (text == null)
			{
				return;
			}
			_matches++;
			string[] obj = new string[8] { "zdo=", null, null, null, null, null, null, null };
			ZDOID uid = z.m_uid;
			obj[1] = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
			obj[2] = " prefab=";
			obj[3] = Plugin.Label(shape.Name);
			obj[4] = " stack=";
			obj[5] = record.Stack.ToString();
			obj[6] = " reason=";
			obj[7] = text;
			string text2 = string.Concat(obj);
			if (policy.DryRun)
			{
				Plugin p = _p;
				uid = z.m_uid;
				p.Note("dry-drop:" + ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString(), "[dry-run world-drop] " + text2);
				return;
			}
			if (_pendingDrops.ContainsKey(z.m_uid) || !HasBudget())
			{
				_deferred++;
				return;
			}
			uint dataRevision = z.DataRevision;
			long owner = z.GetOwner();
			if (!_p.Backup("world-drop", text2 + " owner=" + z.GetOwner() + " position=" + ((object)z.GetPosition()/*cast due to .constrained prefix*/).ToString(), byteArray))
			{
				_deferred++;
				return;
			}
			ZDOID uid2 = z.m_uid;
			if (ZDOMan.instance.GetZDO(uid2) != z || z.DataRevision != dataRevision || z.GetOwner() != owner || !byteArray.SequenceEqual(z.GetByteArray(ZDOVars.s_itemData, (byte[])null) ?? Array.Empty<byte>()))
			{
				_deferred++;
				return;
			}
			z.SetOwner(ZNet.GetUID());
			if (!z.IsOwner())
			{
				throw new InvalidOperationException("Server could not claim drop ownership");
			}
			ZDOMan.instance.DestroyZDO(z);
			_pendingDrops[uid2] = Plugin.Now;
			_edits++;
			_p.Info("[delete-requested world-drop] " + text2);
		}

		private unsafe void InspectContainer(ZDO z, Shape shape, Policy policy)
		{
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_0109: Unknown result type (might be due to invalid IL or missing references)
			//IL_010e: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0218: Unknown result type (might be due to invalid IL or missing references)
			//IL_0296: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b5: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ba: Unknown result type (might be due to invalid IL or missing references)
			byte[] byteArray = z.GetByteArray(ZDOVars.s_items, (byte[])null);
			if (byteArray == null || byteArray.Length == 0)
			{
				return;
			}
			_inventories++;
			ItemCodec.InventoryData inventoryData = ItemCodec.ReadInventory(byteArray);
			HashSet<int> hashSet = new HashSet<int>();
			for (int i = 0; i < inventoryData.Records.Count; i++)
			{
				ItemCodec.Record record = inventoryData.Records[i];
				if (policy.Reason(record.Cheated, GameAccess.PrefabName(record.PrefabHash)) != null)
				{
					hashSet.Add(i);
				}
			}
			if (hashSet.Count == 0)
			{
				return;
			}
			_matches += hashSet.Count;
			string[] obj = new string[6] { "zdo=", null, null, null, null, null };
			ZDOID uid = z.m_uid;
			obj[1] = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
			obj[2] = " container=";
			obj[3] = Plugin.Label(shape.Name);
			obj[4] = " matchingStacks=";
			obj[5] = hashSet.Count.ToString();
			string text = string.Concat(obj);
			if (policy.DryRun)
			{
				Plugin p = _p;
				uid = z.m_uid;
				p.Note("dry-container:" + ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString(), "[dry-run container] " + text);
				return;
			}
			if (!HasBudget() || !MayEditRecord(z, "inventory", byteArray))
			{
				_deferred++;
				return;
			}
			byte[] array = inventoryData.Without(hashSet);
			if (ItemCodec.ReadInventory(array).Records.Count != inventoryData.Records.Count - hashSet.Count)
			{
				throw new InvalidDataException("Filtered count mismatch");
			}
			uint dataRevision = z.DataRevision;
			long owner = z.GetOwner();
			if (!_p.Backup("container-inventory", text + " owner=" + owner + " position=" + ((object)z.GetPosition()/*cast due to .constrained prefix*/).ToString(), byteArray))
			{
				_deferred++;
				return;
			}
			if (!_p.StorageWritesAllowed || ZDOMan.instance.GetZDO(z.m_uid) != z || z.DataRevision != dataRevision || z.GetOwner() != owner || !byteArray.SequenceEqual(z.GetByteArray(ZDOVars.s_items, (byte[])null) ?? Array.Empty<byte>()))
			{
				_deferred++;
				return;
			}
			z.SetOwner(ZNet.GetUID());
			if (!z.IsOwner())
			{
				throw new InvalidOperationException("Cannot claim stable inventory record");
			}
			z.Set(ZDOVars.s_items, array);
			ZDOMan.instance.ForceSendZDO(z.m_uid);
			_edits++;
			Dictionary<string, Candidate> stable = _stable;
			uid = z.m_uid;
			stable.Remove(((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString() + ":inventory");
			_p.Info("[written container-record] " + text + " retainedStacks=" + (inventoryData.Records.Count - hashSet.Count) + " (server data updated; client/restart persistence requires integration verification)");
		}

		private unsafe void InspectAttachment(ZDO z, int slot, Policy policy)
		{
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0231: Unknown result type (might be due to invalid IL or missing references)
			//IL_0250: Unknown result type (might be due to invalid IL or missing references)
			//IL_0255: Unknown result type (might be due to invalid IL or missing references)
			int num = GameAccess.AttachedKey(slot);
			int num2 = z.GetInt(num, 0);
			if (num2 == 0)
			{
				return;
			}
			byte[] byteArray = z.GetByteArray(GameAccess.ItemDataKey(slot), (byte[])null);
			if (byteArray == null || byteArray.Length == 0)
			{
				return;
			}
			_stands++;
			ItemCodec.Record record = ItemCodec.ReadDrop(byteArray);
			if (record.PrefabHash != 0 && record.PrefabHash != num2)
			{
				throw new InvalidDataException("Attachment record mismatch");
			}
			string text = GameAccess.PrefabName(num2);
			string text2 = policy.Reason(record.Cheated, text);
			if (text2 == null)
			{
				return;
			}
			_matches++;
			string[] obj = new string[8] { "zdo=", null, null, null, null, null, null, null };
			ZDOID uid = z.m_uid;
			obj[1] = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
			obj[2] = " slot=";
			obj[3] = slot.ToString();
			obj[4] = " item=";
			obj[5] = Plugin.Label(text);
			obj[6] = " reason=";
			obj[7] = text2;
			string text3 = string.Concat(obj);
			if (policy.DryRun)
			{
				_p.Note("dry-stand:" + text3, "[dry-run attachment-record] " + text3);
				return;
			}
			if (!HasBudget() || !MayEditRecord(z, "slot" + slot, byteArray))
			{
				_deferred++;
				return;
			}
			uint dataRevision = z.DataRevision;
			long owner = z.GetOwner();
			if (!_p.Backup("stand-item", text3 + " owner=" + owner, byteArray))
			{
				_deferred++;
				return;
			}
			if (!_p.StorageWritesAllowed || ZDOMan.instance.GetZDO(z.m_uid) != z || z.DataRevision != dataRevision || z.GetOwner() != owner || z.GetInt(num, 0) != num2 || !byteArray.SequenceEqual(z.GetByteArray(GameAccess.ItemDataKey(slot), (byte[])null) ?? Array.Empty<byte>()))
			{
				_deferred++;
				return;
			}
			z.SetOwner(ZNet.GetUID());
			if (!z.IsOwner())
			{
				throw new InvalidOperationException("Cannot claim stable attachment");
			}
			z.Set(num, 0, false);
			z.Set(GameAccess.ItemDataKey(slot), Array.Empty<byte>());
			ZDOMan.instance.ForceSendZDO(z.m_uid);
			_edits++;
			Dictionary<string, Candidate> stable = _stable;
			uid = z.m_uid;
			stable.Remove(((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString() + ":slot" + slot);
			_p.Info("[written attachment-record] " + text3 + " (attachment cleared; stand retained)");
		}

		private unsafe bool MayEditRecord(ZDO z, string kind, byte[] bytes)
		{
			//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_003c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			long owner = z.GetOwner();
			uint dataRevision = z.DataRevision;
			bool flag = (Object)(object)ZNetScene.instance.FindInstance(z) != (Object)null;
			ZDOID uid;
			if (!_p.StorageWritesAllowed || flag)
			{
				Dictionary<string, Candidate> stable = _stable;
				uid = z.m_uid;
				stable.Remove(((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString() + ":" + kind);
				Plugin p = _p;
				uid = z.m_uid;
				string key = "defer:" + ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
				string[] obj = new string[9] { "[deferred stored-items] zdo=", null, null, null, null, null, null, null, null };
				uid = z.m_uid;
				obj[1] = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString();
				obj[2] = " peers=";
				obj[3] = Plugin.ConnectedPeers.ToString();
				obj[4] = " emptySeconds=";
				obj[5] = _p.EmptySeconds.ToString("F1");
				obj[6] = " localInstance=";
				obj[7] = flag.ToString();
				obj[8] = "; serialized maintenance waits for an empty server; live SERVER-owned components use their native inventory path";
				p.Note(key, string.Concat(obj));
				return false;
			}
			uid = z.m_uid;
			string key2 = ((object)(*(ZDOID*)(&uid))/*cast due to .constrained prefix*/).ToString() + ":" + kind;
			string text = AuditStore.Hash(bytes);
			double now = Plugin.Now;
			if (!_stable.TryGetValue(key2, out var value) || value.Hash != text || value.Owner != owner || value.Revision != dataRevision)
			{
				if (_stable.Count >= 20000 && !_stable.ContainsKey(key2))
				{
					return false;
				}
				_stable[key2] = new Candidate
				{
					Hash = text,
					Owner = owner,
					Revision = dataRevision,
					Since = now,
					Seen = now
				};
				return false;
			}
			value.Seen = now;
			return WorldWriteRules.MayRewrite(Plugin.ConnectedPeers, _p.EmptySeconds, flag, value.Revision, z.DataRevision, now - value.Since);
		}

		private bool HasBudget()
		{
			return _edits < _p.Options.ActionsPerSweep.Value;
		}

		private void ConfirmDeletes()
		{
			//IL_0022: 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)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bb: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
			KeyValuePair<ZDOID, double>[] array = _pendingDrops.ToArray();
			for (int i = 0; i < array.Length; i++)
			{
				KeyValuePair<ZDOID, double> keyValuePair = array[i];
				if (ZDOMan.instance.GetZDO(keyValuePair.Key) == null)
				{
					_p.Info("[delete-confirmed server-record] zdo=" + ((object)keyValuePair.Key/*cast due to .constrained prefix*/).ToString() + " absent from ZDOMan");
					_pendingDrops.Remove(keyValuePair.Key);
				}
				else if (Plugin.Now - keyValuePair.Value > 15.0)
				{
					_p.Warn("pending:" + ((object)keyValuePair.Key/*cast due to .constrained prefix*/).ToString(), "[delete-unconfirmed] zdo=" + ((object)keyValuePair.Key/*cast due to .constrained prefix*/).ToString() + " still present after 15s");
					_pendingDrops.Remove(keyValuePair.Key);
				}
			}
		}
	}
}
namespace ItemIntegrity.Core
{
	public static class ItemCodec
	{
		public sealed class Record
		{
			public int Offset;

			public int Length;

			public int PrefabHash;

			public int Stack;

			public int Quality;

			public bool Cheated;
		}

		public sealed class InventoryData
		{
			public readonly byte[] Original;

			public readonly List<Record> Records;

			internal InventoryData(byte[] original, List<Record> records)
			{
				Original = original;
				Records = records;
			}

			public byte[] Without(ISet<int> removedIndices)
			{
				if (removedIndices == null)
				{
					throw new ArgumentNullException("removedIndices");
				}
				foreach (int removedIndex in removedIndices)
				{
					if (removedIndex < 0 || removedIndex >= Records.Count)
					{
						throw new ArgumentOutOfRangeException("removedIndices");
					}
				}
				using MemoryStream memoryStream = new MemoryStream();
				using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true);
				binaryWriter.Write(109);
				binaryWriter.Write(checked((ushort)(Records.Count - removedIndices.Count)));
				for (int i = 0; i < Records.Count; i++)
				{
					if (!removedIndices.Contains(i))
					{
						Record record = Records[i];
						binaryWriter.Write(Original, record.Offset, record.Length);
					}
				}
				binaryWriter.Flush();
				return memoryStream.ToArray();
			}
		}

		private sealed class Reader
		{
			private readonly byte[] _bytes;

			internal int Position { get; private set; }

			internal Reader(byte[] bytes)
			{
				_bytes = bytes ?? throw new ArgumentNullException("bytes");
				if (bytes.Length > 2097152)
				{
					throw new InvalidDataException("Serialized data exceeds safety limit");
				}
			}

			private void Need(int n)
			{
				if (n < 0 || n > _bytes.Length - Position)
				{
					throw new EndOfStreamException("Truncated item data");
				}
			}

			internal byte Byte()
			{
				Need(1);
				return _bytes[Position++];
			}

			internal void Skip(int n)
			{
				Need(n);
				Position += n;
			}

			internal int UInt16()
			{
				Need(2);
				int result = _bytes[Position] | (_bytes[Position + 1] << 8);
				Position += 2;
				return result;
			}

			internal int Int32()
			{
				Need(4);
				int position = Position;
				Position += 4;
				return _bytes[position] | (_bytes[position + 1] << 8) | (_bytes[position + 2] << 16) | (_bytes[position + 3] << 24);
			}

			internal int NumItems()
			{
				int num = Byte();
				if ((num & 0x80) != 0)
				{
					return ((num & 0x7F) << 8) | Byte();
				}
				return num;
			}

			internal void String()
			{
				uint num = 0u;
				bool flag = false;
				for (int i = 0; i <= 28; i += 7)
				{
					byte b = Byte();
					if (i == 28 && (b & 0xF8) != 0)
					{
						throw new InvalidDataException("Invalid string length");
					}
					num |= (uint)((b & 0x7F) << i);
					if ((b & 0x80) == 0)
					{
						flag = true;
						break;
					}
				}
				if (!flag || num > 2097152)
				{
					throw new InvalidDataException("Oversized string");
				}
				Need((int)num);
				StrictUtf8.GetCharCount(_bytes, Position, (int)num);
				Position += (int)num;
			}

			internal void RequireEnd()
			{
				if (Position != _bytes.Length)
				{
					throw new InvalidDataException("Unknown trailing data; refusing to rewrite");
				}
			}
		}

		public const int SupportedVersion = 109;

		public const int MaxBytes = 2097152;

		public const int MaxItems = 4096;

		private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

		public static InventoryData ReadInventory(byte[] data)
		{
			Reader reader = new Reader(data);
			int num = reader.Int32();
			if (num != 109)
			{
				throw new InvalidDataException("Unsupported inventory format " + num);
			}
			int num2 = reader.UInt16();
			if (num2 > 4096)
			{
				throw new InvalidDataException("Inventory item count exceeds safety limit");
			}
			List<Record> list = new List<Record>(num2);
			for (int i = 0; i < num2; i++)
			{
				list.Add(ReadRecord(reader));
			}
			reader.RequireEnd();
			return new InventoryData(data, list);
		}

		public static Record ReadDrop(byte[] data)
		{
			Reader reader = new Reader(data);
			int num = reader.Byte();
			if (num != 109)
			{
				throw new InvalidDataException("Unsupported item format " + num);
			}
			Record result = ReadRecord(reader);
			reader.RequireEnd();
			return result;
		}

		private static Record ReadRecord(Reader r)
		{
			int position = r.Position;
			r.Skip(4);
			r.Skip(3);
			byte num = r.Byte();
			int num2 = (((num & 4) == 0) ? 1 : r.UInt16());
			int num3 = (((num & 8) == 0) ? 1 : r.UInt16());
			if ((num & 0x10) != 0)
			{
				r.Skip(4);
			}
			if ((num & 0x20) != 0)
			{
				r.Skip(8);
				r.String();
			}
			int prefabHash = (((num & 0x40) != 0) ? r.Int32() : 0);
			int num4 = (((num & 0x80) != 0) ? r.NumItems() : 0);
			for (int i = 0; i < num4; i++)
			{
				r.String();
				r.String();
			}
			byte b = r.Byte();
			if (num3 <= 0 || num2 <= 0)
			{
				throw new InvalidDataException("Invalid zero stack/quality; refusing to edit");
			}
			return new Record
			{
				Offset = position,
				Length = r.Position - position,
				PrefabHash = prefabHash,
				Stack = num3,
				Quality = num2,
				Cheated = ((b & 1) != 0)
			};
		}
	}
	public sealed class Policy
	{
		public bool Enabled = true;

		public bool DryRun = true;

		public bool RemoveFlagged = true;

		public bool RemoveKnown = true;

		public bool ScanDrops = true;

		public bool ScanContainers = true;

		public bool ScanStands = true;

		public string ExtraPrefabs = "";

		private string _lastExtras;

		private HashSet<string> _extraSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

		public string Reason(bool cheated, string prefab)
		{
			if (!Enabled)
			{
				return null;
			}
			if (RemoveFlagged && cheated)
			{
				return "cheated flag";
			}
			if (RemoveKnown && (string.Equals(prefab, "SwordCheat", StringComparison.OrdinalIgnoreCase) || string.Equals(prefab, "SledgeCheat", StringComparison.OrdinalIgnoreCase)))
			{
				return "debug prefab";
			}
			string text = ExtraPrefabs ?? "";
			if (!string.Equals(_lastExtras, text, StringComparison.Ordinal))
			{
				HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
				string[] array = text.Split(',');
				foreach (string text2 in array)
				{
					if (text2.Trim().Length > 0)
					{
						hashSet.Add(text2.Trim());
					}
				}
				_extraSet = hashSet;
				_lastExtras = text;
			}
			if (prefab == null || !_extraSet.Contains(prefab))
			{
				return null;
			}
			return "configured prefab";
		}
	}
	public static class WorldWriteRules
	{
		public const double EmptyServerGraceSeconds = 15.0;

		public const double StableRecordSeconds = 2.0;

		public static bool MayRewrite(int connectedPeers, double emptyServerSeconds, bool instantiatedLocally, uint expectedRevision, uint currentRevision, double unchangedSeconds)
		{
			if (connectedPeers == 0 && !double.IsNaN(emptyServerSeconds) && !double.IsInfinity(emptyServerSeconds) && emptyServerSeconds >= 15.0 && !instantiatedLocally && expectedRevision == currentRevision && !double.IsNaN(unchangedSeconds) && !double.IsInfinity(unchangedSeconds))
			{
				return unchangedSeconds >= 2.0;
			}
			return false;
		}
	}
}