Decompiled source of FreshWorld v1.2.0

FreshWorld.dll

Decompiled 3 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using FreshWorld.API;
using FreshWorld.Backend;
using FreshWorld.Commands;
using FreshWorld.Configuration;
using FreshWorld.Core;
using FreshWorld.Engine;
using FreshWorld.Runtime;
using HarmonyLib;
using JetBrains.Annotations;
using Microsoft.CodeAnalysis;
using ServerSync;
using SoftReferenceableAssets;
using TMPro;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyTitle("FreshWorld")]
[assembly: AssemblyProduct("FreshWorld")]
[assembly: AssemblyCompany("sighsorry")]
[assembly: AssemblyFileVersion("1.2.0")]
[assembly: AssemblyInformationalVersion("1.2.0")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyVersion("1.2.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 FreshWorld
{
	[BepInPlugin("sighsorry.FreshWorld", "FreshWorld", "1.2.0")]
	public sealed class FreshWorldPlugin : BaseUnityPlugin
	{
		private sealed class ManualRequest
		{
			internal readonly ScheduledRun Run;

			internal readonly CommandRequestContext Context;

			internal readonly RunOptions Options;

			internal string? LastWait;

			internal ManualRequest(ScheduledRun run, CommandRequestContext context, RunOptions options)
			{
				Run = run;
				Context = context;
				Options = options;
			}
		}

		public const string Author = "sighsorry";

		public const string ModName = "FreshWorld";

		public const string ModVersion = "1.2.0";

		public const string ModGUID = "sighsorry.FreshWorld";

		public const string PluginGuid = "sighsorry.FreshWorld";

		public const string PluginName = "FreshWorld";

		public const string PluginVersion = "1.2.0";

		private const float SchedulePollIntervalSeconds = 1f;

		internal static FreshWorldPlugin? Instance;

		private FreshWorldConfig configuration;

		private ConfigSynchronization? configSync;

		private PathOfValheimAzurite? povAzurite;

		private RuntimeSettings? settings;

		private Harmony? harmony;

		private Harmony? commandHarmony;

		private FileSystemWatcher? watcher;

		private int reloadRequested;

		private int configValuesChanged;

		private bool supported;

		private bool compatibilityDrained;

		private bool sessionFaulted;

		private ZNet? host;

		private long? worldUid;

		private FreshWorldScheduler? scheduler;

		private GuardedCoroutine? runner;

		private IDisposable? backendLease;

		private ScheduleClock lastClock;

		private float attachedAt;

		private float nextPoll;

		private float nextCheckpoint;

		private float nextReleaseSweep;

		private ManualRequest? pendingManual;

		private string? activeRunId;

		private bool activePovRepair;

		private string progress = "Idle.";

		private void Awake()
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Expected O, but got Unknown
			Instance = this;
			MaintenanceCoordination.Initialize(CoordinationReady, PrepareMaintenance, () => GameWorld.HasPendingZoneCleanup);
			try
			{
				EmptyChunkSaveCompatibility.Reset();
				configuration = new FreshWorldConfig(((BaseUnityPlugin)this).Config);
				harmony = new Harmony("sighsorry.FreshWorld");
				commandHarmony = new Harmony("sighsorry.FreshWorld.Commands");
				foreach (Type item in from type in PatchTypes()
					where type.Namespace == "FreshWorld.Commands"
					select type)
				{
					commandHarmony.PatchAll(item);
				}
				FreshWorldCommands.Register(HandleCommand, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
			}
			catch (Exception ex)
			{
				CleanupPlugin();
				((BaseUnityPlugin)this).Logger.LogError((object)("FreshWorld initialization failed; maintenance is disabled. " + ex));
			}
		}

		private static Type[] PatchTypes()
		{
			return typeof(FreshWorldPlugin).Assembly.GetTypes().Where(delegate(Type type)
			{
				if (!(type.Namespace == "FreshWorld"))
				{
					string? text = type.Namespace;
					if (text == null || !text.StartsWith("FreshWorld.", StringComparison.Ordinal))
					{
						return false;
					}
				}
				return type.IsDefined(typeof(HarmonyPatch), inherit: false);
			}).ToArray();
		}

		private void Start()
		{
			if (Instance != this || harmony == null || EmptyChunkSaveCompatibility.Check() == EmptyChunkSaveCompatibility.Strategy.Unsupported)
			{
				return;
			}
			try
			{
				foreach (Type item in from type in PatchTypes()
					where type.Namespace != "FreshWorld.Commands"
					select type)
				{
					harmony.PatchAll(item);
				}
				configSync = new ConfigSynchronization(((BaseUnityPlugin)this).Config, configuration, harmony, delegate
				{
					Interlocked.Exchange(ref configValuesChanged, 1);
				}, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
				povAzurite = PathOfValheimAzurite.TryCreate(delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
				});
				ReloadSettings(reloadFile: false);
				watcher = new FileSystemWatcher(Path.GetDirectoryName(((BaseUnityPlugin)this).Config.ConfigFilePath), Path.GetFileName(((BaseUnityPlugin)this).Config.ConfigFilePath));
				watcher.Changed += OnConfigChanged;
				watcher.Created += OnConfigChanged;
				watcher.Renamed += OnConfigChanged;
				watcher.EnableRaisingEvents = true;
				supported = true;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("FreshWorld ready. Maintenance runs only on the world host; configure " + ((BaseUnityPlugin)this).Config.ConfigFilePath));
			}
			catch (Exception ex)
			{
				CleanupPlugin();
				((BaseUnityPlugin)this).Logger.LogError((object)("FreshWorld initialization failed; maintenance is disabled. " + ex));
			}
		}

		internal static void LogSaveCompatibility(string message, bool error)
		{
			if (Instance != null)
			{
				if (error)
				{
					((BaseUnityPlugin)Instance).Logger.LogError((object)message);
				}
				else
				{
					((BaseUnityPlugin)Instance).Logger.LogInfo((object)message);
				}
			}
		}

		private void OnConfigChanged(object sender, FileSystemEventArgs args)
		{
			Interlocked.Exchange(ref reloadRequested, 1);
		}

		private void ReloadSettings(bool reloadFile)
		{
			try
			{
				if (reloadFile)
				{
					configSync.ReloadFile();
					Interlocked.Exchange(ref configValuesChanged, 0);
				}
				RuntimeSettings runtimeSettings = configuration.Capture();
				try
				{
					scheduler?.Reconfigure(runtimeSettings.Schedule, ReadClock());
				}
				catch (Exception ex)
				{
					sessionFaulted = true;
					CloseScheduler("The updated schedule could not be persisted.");
					((BaseUnityPlugin)this).Logger.LogError((object)("Could not update the recorded schedule; maintenance is disabled for this world session. " + ex));
					return;
				}
				settings = runtimeSettings;
				povAzurite?.Configure(runtimeSettings.PathOfValheimAzuriteVegetationEnabled);
				if (reloadFile)
				{
					configSync?.Publish(0L);
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)$"Configuration applied: automatic={runtimeSettings.AutomaticEnabled}, schedule={runtimeSettings.Schedule.Mode}, timezone={runtimeSettings.Schedule.TimeZone.Id}, safeZones={runtimeSettings.Options.ZoneSafeZones}/{runtimeSettings.Options.VegetationSafeZones}/{runtimeSettings.Options.LocationSafeZones} (zones/resources/locations).");
				nextPoll = 0f;
			}
			catch (Exception ex2)
			{
				settings = null;
				TryCleanup(delegate
				{
					povAzurite?.Configure(enable: false);
				}, "Could not stop PoV azurite vegetation after invalid configuration.");
				CloseScheduler("The host configuration is invalid.");
				((BaseUnityPlugin)this).Logger.LogError((object)("Invalid FreshWorld configuration; new maintenance is disabled until the cfg is corrected. " + ex2.Message));
			}
		}

		private void ApplyPendingSettings()
		{
			if (runner == null && !MaintenanceGate.IsActive)
			{
				bool flag = Interlocked.Exchange(ref reloadRequested, 0) != 0;
				bool flag2 = Interlocked.Exchange(ref configValuesChanged, 0) != 0;
				if (flag || flag2)
				{
					ReloadSettings(flag);
				}
			}
		}

		private static bool IsHost(ZNet? net)
		{
			if ((Object)(object)net != (Object)null && net.IsServer() && !net.HaveStopped)
			{
				return ZNet.World != null;
			}
			return false;
		}

		private static bool WorldReady()
		{
			if ((Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.LocationsGenerated && ZDOMan.instance != null && (Object)(object)ZNetScene.instance != (Object)null && WorldGenerator.instance != null && (Object)(object)DungeonDB.instance != (Object)null && (Object)(object)Game.instance != (Object)null && (Object)(object)EnvMan.instance != (Object)null)
			{
				return !ZNet.m_loadError;
			}
			return false;
		}

		private ScheduleClock ReadClock()
		{
			if (!IsHost(host) || (Object)(object)EnvMan.instance == (Object)null)
			{
				return new ScheduleClock(DateTimeOffset.UtcNow, lastClock.GameDay);
			}
			long dayLengthSec = EnvMan.instance.m_dayLengthSec;
			if (dayLengthSec <= 0)
			{
				throw new InvalidOperationException("The game day length must be positive.");
			}
			return new ScheduleClock(DateTimeOffset.UtcNow, Math.Max(0.0, host.GetTimeSeconds() / (double)dayLengthSec));
		}

		private void Update()
		{
			if (!supported)
			{
				return;
			}
			try
			{
				ZNet instance = ZNet.instance;
				long? num = (IsHost(instance) ? new long?(instance.GetWorldUID()) : ((long?)null));
				if (worldUid.HasValue && (host != instance || worldUid != num || !IsHost(instance)))
				{
					StopSession();
				}
				if (EmptyChunkSaveCompatibility.DisabledReason != null)
				{
					DrainDisabledMaintenance();
					return;
				}
				ApplyPendingSettings();
				if (!IsHost(instance) || !WorldReady())
				{
					runner?.Dispose();
					runner = null;
					CancelPendingManual("The hosting world is no longer ready.");
					return;
				}
				float realtimeSinceStartup = Time.realtimeSinceStartup;
				if (realtimeSinceStartup >= nextReleaseSweep)
				{
					int num2 = GameWorld.ProcessDeferredReleases();
					if (num2 > 0)
					{
						((BaseUnityPlugin)this).Logger.LogInfo((object)$"Cleared {num2} deferred maintenance zone release(s).");
					}
					nextReleaseSweep = realtimeSinceStartup + 1f;
				}
				AttachHost(instance);
				if (runner != null)
				{
					GuardedCoroutine guardedCoroutine = runner;
					if (!guardedCoroutine.MoveNext() && runner == guardedCoroutine)
					{
						runner = null;
					}
				}
				ApplyPendingSettings();
				if (settings == null || sessionFaulted)
				{
					return;
				}
				lastClock = ReadClock();
				EnsureScheduler();
				if (pendingManual != null)
				{
					if (!IsCurrentRequest(pendingManual.Context))
					{
						CancelPendingManual("The requester disconnected, changed session, or lost administrator authority.");
					}
					else if (runner == null)
					{
						TryDispatch(pendingManual.Run);
					}
				}
				if (EmptyChunkSaveCompatibility.DisabledReason == null && !(realtimeSinceStartup < nextPoll))
				{
					nextPoll = realtimeSinceStartup + 1f;
					ScheduledRun due = scheduler.GetDue(lastClock);
					if (realtimeSinceStartup >= nextCheckpoint)
					{
						scheduler.Checkpoint(lastClock);
						nextCheckpoint = realtimeSinceStartup + 60f;
					}
					if (runner == null && due != null)
					{
						TryDispatch(due);
					}
				}
			}
			catch (Exception ex)
			{
				sessionFaulted = true;
				runner?.Dispose();
				runner = null;
				((BaseUnityPlugin)this).Logger.LogError((object)("FreshWorld stopped maintenance for this world session after an error. " + ex));
				CloseScheduler("The host stopped maintenance after an error.");
			}
		}

		private void AttachHost(ZNet current)
		{
			long worldUID = current.GetWorldUID();
			if (worldUid.HasValue && (host != current || worldUid != worldUID))
			{
				StopSession();
			}
			if (!((Object)(object)host != (Object)null))
			{
				host = current;
				worldUid = worldUID;
				attachedAt = Time.realtimeSinceStartup;
				nextCheckpoint = attachedAt + 60f;
				sessionFaulted = false;
			}
		}

		private void EnsureScheduler()
		{
			if (scheduler == null)
			{
				string text = worldUid.Value.ToString(CultureInfo.InvariantCulture);
				scheduler = FreshWorldScheduler.Open(settings.Schedule, new JsonWorldStateStore(Path.Combine(Paths.ConfigPath, "FreshWorld", "state")), text, lastClock);
				ScheduledRun pendingRun = scheduler.PendingRun;
				if (pendingRun != null && pendingRun.IsManual)
				{
					scheduler.CancelPendingManualRun(pendingRun.Id, lastClock, "The manual request belonged to an earlier host session.");
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Cancelled an earlier session's unstarted manual request: " + pendingRun.Id));
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Scheduler attached to world " + host.GetWorldName() + " (" + text + ")."));
				MaintenanceCoordination.Attach(text);
			}
		}

		private bool IsCurrentRequest(CommandRequestContext context)
		{
			if (context.IsAuthorizedNow && context.Network == ZNet.instance && IsHost(context.Network))
			{
				return context.Network.GetWorldUID() == context.WorldUid;
			}
			return false;
		}

		internal void HandleCommand(FreshWorldCommandAction action, CommandRequestContext context)
		{
			try
			{
				if (!IsCurrentRequest(context))
				{
					context.Reply("Request rejected: this host session or your authority is no longer valid.");
					return;
				}
				if (!Enum.IsDefined(typeof(FreshWorldCommandAction), action))
				{
					context.Reply("Invalid command action.");
					return;
				}
				if (EmptyChunkSaveCompatibility.DisabledReason != null)
				{
					context.Reply(DisabledStatus());
					return;
				}
				if (!supported)
				{
					context.Reply("FreshWorld initialization is not complete; maintenance is unavailable.");
					return;
				}
				if (!WorldReady())
				{
					context.Reply("Request rejected: the world is not ready.");
					return;
				}
				switch (action)
				{
				case FreshWorldCommandAction.Pov:
					StartPovRepair(context);
					return;
				case FreshWorldCommandAction.Invasions:
					try
					{
						foreach (string item in JotunInvasionDiagnostics.Describe())
						{
							((BaseUnityPlugin)this).Logger.LogInfo((object)item);
							context.Reply(item);
						}
						return;
					}
					catch (Exception ex)
					{
						((BaseUnityPlugin)this).Logger.LogWarning((object)("Jotun invasion inspection failed: " + ex));
						context.Reply("Invasion inspection failed; see the host log. Maintenance state is unchanged.");
						return;
					}
				}
				AttachHost(context.Network);
				if (action == FreshWorldCommandAction.Status)
				{
					context.Reply(StatusMessage());
					return;
				}
				if (runner != null || activeRunId != null)
				{
					context.Reply("Maintenance is already running: " + (activePovRepair ? "PoV altar repair" : activeRunId) + ". " + progress);
					return;
				}
				if (MaintenanceCoordination.HasExternalLease)
				{
					context.Reply("Another mod holds the maintenance lease. Try again after it finishes.");
					return;
				}
				if (pendingManual != null)
				{
					if (IsCurrentRequest(pendingManual.Context))
					{
						context.Reply("A manual request is already pending: " + pendingManual.Run.Id);
						return;
					}
					CancelPendingManual("The earlier request lost its authority or session.");
				}
				Interlocked.Exchange(ref reloadRequested, 0);
				ReloadSettings(reloadFile: true);
				if (settings == null)
				{
					context.Reply("Request rejected: the host cfg is invalid; see the host log.");
					return;
				}
				if (sessionFaulted)
				{
					context.Reply("Request rejected: maintenance is faulted for this world session; see the host log.");
					return;
				}
				if (!IsCurrentRequest(context))
				{
					context.Reply("Request rejected: authority changed before admission.");
					return;
				}
				lastClock = ReadClock();
				EnsureScheduler();
				ScheduledRun due = scheduler.GetDue(lastClock);
				if (due != null)
				{
					context.Reply("A scheduled maintenance is already pending: " + due.Id);
					return;
				}
				ScheduledRun scheduledRun = scheduler.RequestManualRun(lastClock);
				pendingManual = new ManualRequest(scheduledRun, context, settings.Options);
				context.Reply("Accepted " + scheduledRun.Id + ". Host cfg: zones=" + settings.Options.ZonesEnabled + ", vegetation=" + settings.Options.VegetationEnabled + ", locations=" + settings.Options.LocationsEnabled + ", safeZones=" + settings.Options.ZoneSafeZones + "/" + settings.Options.VegetationSafeZones + "/" + settings.Options.LocationSafeZones + " (zones/resources/locations).");
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Manual request accepted from " + context.Actor + ": " + scheduledRun.Id));
				TryDispatch(scheduledRun);
				nextPoll = 0f;
			}
			catch (Exception ex2)
			{
				sessionFaulted = true;
				context.Reply("Request failed; see the host log. New maintenance is disabled for this session.");
				((BaseUnityPlugin)this).Logger.LogError((object)("FreshWorld command admission failed: " + ex2));
				CloseScheduler("Command admission failed.");
			}
		}

		private void StartPovRepair(CommandRequestContext context)
		{
			if (runner != null || activeRunId != null || pendingManual != null || scheduler?.PendingRun != null || !MaintenanceGate.IsAvailable || GameWorld.HasPendingMaintenanceCleanup)
			{
				context.Reply("PoV altar repair is unavailable while maintenance is running, pending, or cleaning up.");
				return;
			}
			AttachHost(context.Network);
			if (sessionFaulted || settings == null)
			{
				context.Reply("PoV altar repair is unavailable: maintenance is faulted or the host cfg is invalid.");
				return;
			}
			if (Time.realtimeSinceStartup - attachedAt < settings.StartupDelaySeconds || Time.timeScale <= 0f || context.Network.IsSaving())
			{
				context.Reply("PoV altar repair is unavailable during startup, pause, or a world save; try again afterward.");
				return;
			}
			if (EmptyChunkSaveCompatibility.Check() == EmptyChunkSaveCompatibility.Strategy.Unsupported)
			{
				DrainDisabledMaintenance();
				context.Reply(DisabledStatus());
				return;
			}
			ZoneSystem activeZones = ZoneSystem.instance;
			ZDOMan activeManager = ZDOMan.instance;
			ZNetScene activeScene = ZNetScene.instance;
			WorldGenerator activeGenerator = WorldGenerator.instance;
			try
			{
				RequireAuthority();
				PathOfValheimRepair repair = new PathOfValheimRepair(RequireAuthority, delegate(string message)
				{
					progress = message;
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
					context.Reply(message);
				});
				backendLease = AcquireMaintenanceLease();
				activePovRepair = true;
				progress = "Checking recorded PoV altars.";
				Exception failure = null;
				runner = new GuardedCoroutine(repair.Run(), CanContinue, delegate(Exception error)
				{
					failure = error;
					((BaseUnityPlugin)this).Logger.LogError((object)("PoV altar repair failed: " + error));
				}, delegate(bool success)
				{
					try
					{
						string text = (success ? "Completed. " : ("Stopped: " + (failure?.Message ?? "authority or world session changed") + ". ")) + repair.Summary + (success ? " Changes use the game's normal saves." : " Completed changes remain; no automatic retry.");
						progress = text;
						if (success)
						{
							((BaseUnityPlugin)this).Logger.LogInfo((object)text);
						}
						else
						{
							((BaseUnityPlugin)this).Logger.LogWarning((object)text);
						}
						context.Reply(text);
					}
					finally
					{
						backendLease?.Dispose();
						backendLease = null;
						activePovRepair = false;
					}
				});
				context.Reply("Starting PoV altar repair. Only missing Warden and Power altars with recorded coordinates will be restored.");
			}
			catch (Exception ex)
			{
				runner?.Dispose();
				runner = null;
				backendLease?.Dispose();
				backendLease = null;
				activePovRepair = false;
				((BaseUnityPlugin)this).Logger.LogWarning((object)("PoV altar repair could not start: " + ex));
				context.Reply("PoV altar repair could not start: " + ex.Message);
			}
			bool CanContinue()
			{
				if (EmptyChunkSaveCompatibility.DisabledReason == null && IsCurrentRequest(context) && WorldReady() && activeZones == ZoneSystem.instance && activeManager == ZDOMan.instance && activeScene == ZNetScene.instance)
				{
					return activeGenerator == WorldGenerator.instance;
				}
				return false;
			}
			void RequireAuthority()
			{
				if (!CanContinue())
				{
					throw new InvalidOperationException("PoV repair lost its authority, world session, or compatibility contract.");
				}
			}
		}

		private string StatusMessage()
		{
			if (EmptyChunkSaveCompatibility.DisabledReason != null)
			{
				return DisabledStatus();
			}
			if (sessionFaulted)
			{
				return "Maintenance is faulted for this world session. " + progress;
			}
			if (runner != null)
			{
				return "Running " + (activePovRepair ? "PoV altar repair" : activeRunId) + ": " + progress;
			}
			if (MaintenanceCoordination.HasExternalLease)
			{
				return "Another mod holds the maintenance lease. Automatic resets and configuration application are waiting for its completion or verified cleanup.";
			}
			if (pendingManual != null)
			{
				return "Pending " + pendingManual.Run.Id + ": " + (pendingManual.LastWait ?? "waiting to start");
			}
			if (settings == null)
			{
				return "The host cfg is invalid; new maintenance is disabled.";
			}
			ScheduledRun scheduledRun = scheduler?.PendingRun;
			if (scheduledRun != null)
			{
				return "Scheduled maintenance pending: " + scheduledRun.Id;
			}
			RunRecord runRecord = scheduler?.Attempts.LastOrDefault();
			return string.Concat("Automatic=" + settings.AutomaticEnabled + "; manual commands available; Mode=" + settings.Schedule.Mode.ToString() + "; zones=" + settings.Options.ZonesEnabled + ", resources=" + settings.Options.VegetationEnabled + ", locations=" + settings.Options.LocationsEnabled + "; safeZones=" + settings.Options.ZoneSafeZones + "/" + settings.Options.VegetationSafeZones + "/" + settings.Options.LocationSafeZones + " (zones/resources/locations). ", (runRecord == null) ? "Idle. No maintenance has run in this world's recorded history." : ("Idle. Last run: " + runRecord.Status.ToString() + " (" + runRecord.Run.Id + ")."));
		}

		private void TryDispatch(ScheduledRun run)
		{
			if (runner != null || settings == null || sessionFaulted)
			{
				return;
			}
			if (EmptyChunkSaveCompatibility.Check() == EmptyChunkSaveCompatibility.Strategy.Unsupported)
			{
				DrainDisabledMaintenance();
			}
			else
			{
				if (!run.IsManual && !settings.AutomaticEnabled)
				{
					return;
				}
				RunOptions options = settings.Options;
				ManualRequest manualRequest = pendingManual;
				if (run.IsManual)
				{
					if (manualRequest == null || manualRequest.Run.Id != run.Id || !IsCurrentRequest(manualRequest.Context))
					{
						if (manualRequest != null && manualRequest.Run.Id == run.Id)
						{
							CancelPendingManual("The requester is no longer authorized in this session.");
						}
						else
						{
							scheduler.CancelPendingManualRun(run.Id, lastClock, "No authenticated request exists in this session.");
						}
						return;
					}
					options = manualRequest.Options;
				}
				string text = null;
				if (!WorldReady() || !IsHost(host))
				{
					text = "world readiness";
				}
				else if (Time.realtimeSinceStartup - attachedAt < settings.StartupDelaySeconds)
				{
					text = "startup delay";
				}
				else if (Time.timeScale <= 0f)
				{
					text = "the host to unpause";
				}
				else if (host.IsSaving())
				{
					text = "the current world save";
				}
				else if (!MaintenanceGate.IsAvailable)
				{
					text = "another maintenance operation";
				}
				if (text != null)
				{
					if (manualRequest != null && manualRequest.LastWait != text)
					{
						manualRequest.LastWait = text;
						manualRequest.Context.Reply("Waiting for " + text + ".");
					}
				}
				else
				{
					StartRun(run, options);
				}
			}
		}

		private void StartRun(ScheduledRun run, RunOptions options)
		{
			FreshWorldScheduler activeScheduler = scheduler;
			ZNet activeHost = host;
			long? activeWorldUid = worldUid;
			CommandRequestContext requester = ((pendingManual?.Run.Id == run.Id) ? pendingManual.Context : null);
			backendLease = AcquireMaintenanceLease();
			try
			{
				activeScheduler.BeginRun(run, lastClock);
				pendingManual = null;
				activeRunId = run.Id;
				progress = "Starting.";
				Exception failure = null;
				MaintenancePipeline pipeline = new MaintenancePipeline(options, run.IncludeVegetation, delegate(string message)
				{
					progress = message;
					((BaseUnityPlugin)this).Logger.LogInfo((object)message);
					requester?.Reply(message);
				}, delegate(string message)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)message);
					requester?.Reply(message);
				}, run.IsManual ? 100 : options.AutomaticResetPercent, run.Id);
				if (run.IsManual)
				{
					requester?.Reply("Manual reset: processing all eligible zones. AutomaticResetPercent does not apply.");
				}
				runner = new GuardedCoroutine(pipeline.Run(), CanContinue, delegate(Exception error)
				{
					failure = failure ?? error;
					((BaseUnityPlugin)this).Logger.LogError((object)("Maintenance error: " + error));
				}, delegate(bool success)
				{
					try
					{
						if (success)
						{
							activeScheduler.CompleteRun(run.Id, ReadClock());
							string planningExclusionSummary = pipeline.PlanningExclusionSummary;
							string text = ((planningExclusionSummary == null) ? "Completed" : "Partially completed");
							if (planningExclusionSummary == null)
							{
								((BaseUnityPlugin)this).Logger.LogInfo((object)("Maintenance completed: " + run.Id));
							}
							else
							{
								((BaseUnityPlugin)this).Logger.LogWarning((object)("Maintenance partially completed: " + run.Id + "; " + planningExclusionSummary));
							}
							progress = text + ".";
							requester?.Reply(text + " " + run.Id + ". " + ((planningExclusionSummary == null) ? "" : (planningExclusionSummary + " ")) + "World changes use the game's normal saves; no extra save was requested.");
						}
						else
						{
							activeScheduler.FailRun(run.Id, ReadClock(), failure?.Message ?? "World session ended or maintenance was cancelled.");
							((BaseUnityPlugin)this).Logger.LogWarning((object)("Maintenance stopped; completed world changes remain. This run will not retry automatically: " + run.Id));
							progress = "Stopped: " + (failure?.Message ?? "world session ended");
							requester?.Reply("Failed or interrupted " + run.Id + ": " + (failure?.Message ?? "world session ended") + ". Applied changes remain; this run will not retry automatically.");
						}
					}
					catch (Exception ex)
					{
						sessionFaulted = true;
						((BaseUnityPlugin)this).Logger.LogError((object)("Could not persist the maintenance result; further runs disabled for this session. " + ex));
						requester?.Reply("Maintenance ended, but its result could not be recorded. Further runs are disabled; see the host log.");
					}
					finally
					{
						backendLease?.Dispose();
						backendLease = null;
						activeRunId = null;
					}
				});
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Maintenance starting: " + run.Id + "; vegetation=" + run.IncludeVegetation));
				requester?.Reply("Starting " + run.Id + ".");
				bool CanContinue()
				{
					if (EmptyChunkSaveCompatibility.DisabledReason != null)
					{
						if (failure == null)
						{
							failure = new InvalidOperationException(DisabledStatus());
						}
						return false;
					}
					if (requester != null && !IsCurrentRequest(requester))
					{
						if (failure == null)
						{
							failure = new InvalidOperationException("The requester's connection, session, or authority is no longer valid.");
						}
						return false;
					}
					if (ZNet.instance == activeHost && IsHost(activeHost) && activeHost.GetWorldUID() == activeWorldUid)
					{
						return WorldReady();
					}
					return false;
				}
			}
			catch
			{
				backendLease?.Dispose();
				backendLease = null;
				activeRunId = null;
				requester?.Reply("Maintenance could not start; see the host log.");
				throw;
			}
		}

		private void CancelPendingManual(string reason)
		{
			ManualRequest manualRequest = pendingManual;
			if (manualRequest != null)
			{
				pendingManual = null;
				try
				{
					scheduler?.CancelPendingManualRun(manualRequest.Run.Id, ReadClock(), reason);
				}
				catch (Exception ex)
				{
					sessionFaulted = true;
					((BaseUnityPlugin)this).Logger.LogError((object)("Could not persist cancellation of the manual request: " + ex));
				}
				manualRequest.Context.Reply("Cancelled " + manualRequest.Run.Id + " before execution: " + reason);
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Manual request cancelled before execution: " + manualRequest.Run.Id + "; " + reason));
			}
		}

		private void CloseScheduler(string reason = "The scheduler was closed.")
		{
			CancelPendingManual(reason);
			FreshWorldScheduler freshWorldScheduler = scheduler;
			scheduler = null;
			if (freshWorldScheduler == null)
			{
				return;
			}
			try
			{
				freshWorldScheduler.Dispose();
			}
			catch (Exception ex)
			{
				sessionFaulted = true;
				((BaseUnityPlugin)this).Logger.LogError((object)("Failed to close world state. " + ex));
			}
		}

		private IDisposable AcquireMaintenanceLease()
		{
			IDisposable disposable = MaintenanceGate.Acquire();
			try
			{
				PrepareMaintenance();
				return disposable;
			}
			catch
			{
				disposable.Dispose();
				throw;
			}
		}

		private bool CoordinationReady()
		{
			if (supported && settings != null && !sessionFaulted && scheduler != null && EmptyChunkSaveCompatibility.DisabledReason == null && ZNet.instance == host && IsHost(host) && host.GetWorldUID() == worldUid)
			{
				return WorldReady();
			}
			return false;
		}

		private void PrepareMaintenance()
		{
			if (EmptyChunkSaveCompatibility.Check() == EmptyChunkSaveCompatibility.Strategy.Unsupported)
			{
				throw new InvalidOperationException(DisabledStatus());
			}
			povAzurite?.BeforeMaintenance();
		}

		internal void StopSession()
		{
			runner?.Dispose();
			runner = null;
			backendLease?.Dispose();
			backendLease = null;
			MaintenanceCoordination.CloseSession();
			TryCleanup(delegate
			{
				povAzurite?.EndSession();
			}, "Could not discard the PoV azurite scan at session end.");
			CloseScheduler("The hosting world session ended.");
			host = null;
			worldUid = null;
			nextPoll = 0f;
			nextReleaseSweep = 0f;
			sessionFaulted = false;
			activeRunId = null;
			activePovRepair = false;
			progress = "Idle.";
		}

		private void OnDestroy()
		{
			CleanupPlugin();
		}

		private static string DisabledStatus()
		{
			return "FreshWorld disabled: " + EmptyChunkSaveCompatibility.DisabledReason + " Maintenance and FreshWorld save changes are disabled. World saves are not blocked by FreshWorld. Restart after resolving the mod combination.";
		}

		private void DrainDisabledMaintenance()
		{
			configSync?.Suspend();
			if (MaintenanceCoordination.HasExternalLease)
			{
				return;
			}
			if (runner != null)
			{
				GuardedCoroutine guardedCoroutine = runner;
				if (!guardedCoroutine.MoveNext() && runner == guardedCoroutine)
				{
					runner = null;
				}
			}
			if (runner != null || compatibilityDrained)
			{
				return;
			}
			CloseScheduler("FreshWorld save compatibility is disabled.");
			if (IsHost(ZNet.instance) && (Object)(object)ZoneSystem.instance != (Object)null && ZDOMan.instance != null)
			{
				GameWorld.ProcessDeferredReleases();
				if (GameWorld.HasPendingMaintenanceCleanup)
				{
					return;
				}
			}
			DisposeWatcher();
			ConfigSynchronization? configSynchronization = configSync;
			if (configSynchronization == null || !configSynchronization.HasPendingLogin)
			{
				configSync?.Dispose();
				configSync = null;
				povAzurite?.Dispose();
				povAzurite = null;
				Harmony? obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
				compatibilityDrained = true;
				((BaseUnityPlugin)this).Logger.LogWarning((object)"FreshWorld maintenance cleanup finished; operational patches removed. Only authenticated command diagnostics remain.");
			}
		}

		private void CleanupPlugin()
		{
			supported = false;
			try
			{
				TryCleanup(DisposeWatcher, "Could not stop configuration watching.");
				TryCleanup(delegate
				{
					configSync?.Dispose();
					configSync = null;
				}, "Could not stop configuration synchronization.");
				TryCleanup(StopSession, "Could not stop the active world session.");
				TryCleanup(delegate
				{
					povAzurite?.Dispose();
					povAzurite = null;
				}, "Could not stop PoV azurite compatibility.");
				TryCleanup(FreshWorldCommands.Unregister, "Could not unregister the console command.");
				TryCleanup(delegate
				{
					Harmony? obj = harmony;
					if (obj != null)
					{
						obj.UnpatchSelf();
					}
				}, "Could not remove Harmony patches.");
				TryCleanup(delegate
				{
					Harmony? obj = commandHarmony;
					if (obj != null)
					{
						obj.UnpatchSelf();
					}
				}, "Could not remove command authentication patches.");
			}
			finally
			{
				MaintenanceCoordination.Shutdown();
				Instance = null;
			}
		}

		private void DisposeWatcher()
		{
			FileSystemWatcher fileSystemWatcher = watcher;
			watcher = null;
			if (fileSystemWatcher != null)
			{
				fileSystemWatcher.Changed -= OnConfigChanged;
				fileSystemWatcher.Created -= OnConfigChanged;
				fileSystemWatcher.Renamed -= OnConfigChanged;
				fileSystemWatcher.Dispose();
			}
		}

		private void TryCleanup(Action cleanup, string message)
		{
			try
			{
				cleanup();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)(message + " " + ex));
			}
		}
	}
	[HarmonyPatch(typeof(ZNet), "Shutdown", new Type[] { typeof(bool) })]
	internal static class ShutdownPatch
	{
		[HarmonyPrefix]
		private static void Prefix()
		{
			FreshWorldPlugin.Instance?.StopSession();
		}
	}
	[HarmonyPatch(typeof(ZNet), "ShutdownWithoutSave", new Type[] { typeof(bool) })]
	internal static class ShutdownWithoutSavePatch
	{
		[HarmonyPrefix]
		private static void Prefix()
		{
			FreshWorldPlugin.Instance?.StopSession();
		}
	}
}
namespace FreshWorld.Core
{
	public sealed class FreshWorldScheduler : IDisposable
	{
		private readonly object gate = new object();

		private ScheduleSettings settings;

		private readonly JsonWorldStateStore.Lease lease;

		private WorldState state;

		private bool disposed;

		private bool faulted;

		public string WorldId => state.WorldId;

		public IReadOnlyList<RunRecord> Attempts
		{
			get
			{
				lock (gate)
				{
					return state.Attempts.Select((RunRecord a) => a.Copy()).ToArray();
				}
			}
		}

		public RunRecord? ActiveRun
		{
			get
			{
				lock (gate)
				{
					return state.Attempts.FirstOrDefault((RunRecord a) => a.Status == RunStatus.Running)?.Copy();
				}
			}
		}

		public ScheduledRun? PendingRun
		{
			get
			{
				lock (gate)
				{
					EnsureUsable();
					return state.Pending;
				}
			}
		}

		private FreshWorldScheduler(ScheduleSettings settings, JsonWorldStateStore.Lease lease, WorldState state)
		{
			this.settings = settings;
			this.lease = lease;
			this.state = state;
		}

		public static FreshWorldScheduler Open(ScheduleSettings settings, JsonWorldStateStore store, string worldId, ScheduleClock now)
		{
			if (settings == null)
			{
				throw new ArgumentNullException("settings");
			}
			if (store == null)
			{
				throw new ArgumentNullException("store");
			}
			JsonWorldStateStore.Lease lease = store.Acquire(worldId);
			try
			{
				WorldState worldState = lease.Read(worldId);
				if (worldState == null)
				{
					worldState = NewState(settings, worldId, now);
				}
				else
				{
					foreach (RunRecord item in worldState.Attempts.Where((RunRecord a) => a.Status == RunStatus.Running))
					{
						item.Finish(RunStatus.Interrupted, now.UtcNow, "The previous host session ended before completion. This run will not be retried automatically.");
					}
					if (worldState.Fingerprint != settings.Fingerprint)
					{
						ResetAutomaticSchedule(worldState, settings, now);
					}
					else if (!settings.RunMissedOnWorldStart)
					{
						Advance(worldState, now);
					}
				}
				if (!settings.AutomaticEnabled)
				{
					ScheduledRun? pending = worldState.Pending;
					if (pending == null || !pending.IsManual)
					{
						worldState.Pending = null;
					}
				}
				lease.Write(worldState);
				return new FreshWorldScheduler(settings, lease, worldState);
			}
			catch
			{
				lease.Dispose();
				throw;
			}
		}

		public void Reconfigure(ScheduleSettings nextSettings, ScheduleClock now)
		{
			if (nextSettings == null)
			{
				throw new ArgumentNullException("nextSettings");
			}
			lock (gate)
			{
				EnsureUsable();
				WorldState worldState = state.Copy();
				if (worldState.Fingerprint != nextSettings.Fingerprint)
				{
					ResetAutomaticSchedule(worldState, nextSettings, now);
				}
				if (!nextSettings.AutomaticEnabled)
				{
					ScheduledRun? pending = worldState.Pending;
					if (pending == null || !pending.IsManual)
					{
						worldState.Pending = null;
					}
				}
				if (worldState.Fingerprint != state.Fingerprint || worldState.Pending?.Id != state.Pending?.Id)
				{
					Persist(worldState);
				}
				settings = nextSettings;
			}
		}

		public ScheduledRun? GetDue(ScheduleClock now)
		{
			lock (gate)
			{
				EnsureUsable();
				WorldState worldState = state.Copy();
				ScheduledRun candidate = ((!settings.AutomaticEnabled) ? null : ((settings.Mode == ScheduleMode.DailyTimes) ? LatestDaily(worldState, now) : LatestGameDay(worldState, now)));
				if (!settings.AutomaticEnabled)
				{
					ScheduledRun? pending = worldState.Pending;
					if (pending == null || !pending.IsManual)
					{
						worldState.Pending = null;
					}
				}
				Advance(worldState, now);
				if (candidate != null)
				{
					ScheduledRun? pending2 = worldState.Pending;
					if ((pending2 == null || !pending2.IsManual) && !worldState.Attempts.Any((RunRecord a) => a.Run.Id == candidate.Id))
					{
						worldState.Pending = candidate;
					}
				}
				if (worldState.Pending?.Id != state.Pending?.Id)
				{
					Persist(worldState);
				}
				else
				{
					state = worldState;
				}
				return state.Attempts.Any((RunRecord a) => a.Status == RunStatus.Running) ? null : state.Pending;
			}
		}

		public void BeginRun(ScheduledRun run, ScheduleClock now)
		{
			if (run == null)
			{
				throw new ArgumentNullException("run");
			}
			lock (gate)
			{
				EnsureUsable();
				if (state.Attempts.Any((RunRecord a) => a.Status == RunStatus.Running))
				{
					throw new InvalidOperationException("A FreshWorld run is already active.");
				}
				if (state.Pending == null || state.Pending.Id != run.Id || state.Attempts.Any((RunRecord a) => a.Run.Id == run.Id))
				{
					throw new InvalidOperationException("The run is no longer pending or has already been attempted.");
				}
				WorldState worldState = state.Copy();
				worldState.Attempts.Add(new RunRecord(worldState.Pending, now.UtcNow));
				worldState.Pending = null;
				while (worldState.Attempts.Count > 128)
				{
					worldState.Attempts.RemoveAt(0);
				}
				Persist(worldState);
			}
		}

		public ScheduledRun RequestManualRun(ScheduleClock now, bool includeVegetation = true)
		{
			lock (gate)
			{
				EnsureUsable();
				if (state.Attempts.Any((RunRecord a) => a.Status == RunStatus.Running))
				{
					throw new InvalidOperationException("A FreshWorld run is already active.");
				}
				if (state.Pending != null)
				{
					throw new InvalidOperationException("A FreshWorld run is already pending; a manual request cannot replace it.");
				}
				WorldState worldState = state.Copy();
				ScheduledRun result = (worldState.Pending = new ScheduledRun("manual:" + Guid.NewGuid().ToString("N"), now.UtcNow, now.GameDay, "Manual", includeVegetation));
				Persist(worldState);
				return result;
			}
		}

		public void CompleteRun(string runId, ScheduleClock now)
		{
			FinishRun(runId, now, RunStatus.Completed, "");
		}

		public void FailRun(string runId, ScheduleClock now, string reason)
		{
			FinishRun(runId, now, RunStatus.Failed, reason);
		}

		public bool CancelPendingManualRun(string runId, ScheduleClock now, string reason)
		{
			lock (gate)
			{
				EnsureUsable();
				if (state.Pending == null || state.Pending.Id != runId || !state.Pending.IsManual)
				{
					return false;
				}
				WorldState worldState = state.Copy();
				RunRecord runRecord = new RunRecord(worldState.Pending, now.UtcNow);
				runRecord.Finish(RunStatus.Interrupted, now.UtcNow, reason);
				worldState.Attempts.Add(runRecord);
				worldState.Pending = null;
				while (worldState.Attempts.Count > 128)
				{
					worldState.Attempts.RemoveAt(0);
				}
				Persist(worldState);
				return true;
			}
		}

		public void Checkpoint(ScheduleClock now)
		{
			lock (gate)
			{
				EnsureUsable();
				GetDue(now);
				Persist(state.Copy());
			}
		}

		private void FinishRun(string runId, ScheduleClock now, RunStatus status, string reason)
		{
			lock (gate)
			{
				EnsureUsable();
				WorldState worldState = state.Copy();
				(worldState.Attempts.SingleOrDefault((RunRecord a) => a.Run.Id == runId && a.Status == RunStatus.Running) ?? throw new InvalidOperationException("No matching active FreshWorld run exists.")).Finish(status, now.UtcNow, reason);
				Persist(worldState);
			}
		}

		private ScheduledRun? LatestDaily(WorldState current, ScheduleClock now)
		{
			if (now.UtcNow.UtcDateTime.Ticks <= current.CursorUtcTicks)
			{
				return null;
			}
			DateTime date = TimeZoneInfo.ConvertTime(now.UtcNow, settings.TimeZone).Date;
			for (int i = 0; i < 3; i++)
			{
				DateTime dateTime = date.AddDays(-i);
				for (int num = settings.DailyTimes.Count - 1; num >= 0; num--)
				{
					TimeSpan timeSpan = settings.DailyTimes[num];
					DateTime dateTime2 = DateTime.SpecifyKind(dateTime.Add(timeSpan), DateTimeKind.Unspecified);
					if (!settings.TimeZone.IsInvalidTime(dateTime2))
					{
						TimeSpan offset = (settings.TimeZone.IsAmbiguousTime(dateTime2) ? settings.TimeZone.GetAmbiguousTimeOffsets(dateTime2).Max() : settings.TimeZone.GetUtcOffset(dateTime2));
						DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTime2, offset).ToUniversalTime();
						if (!(dateTimeOffset > now.UtcNow))
						{
							if (dateTimeOffset.UtcDateTime.Ticks <= current.CursorUtcTicks)
							{
								return null;
							}
							string text = ScheduleSettings.FormatTime(timeSpan);
							return new ScheduledRun(current.Fingerprint + ":daily:" + dateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) + ":" + text, dateTimeOffset, now.GameDay, text, settings.IncludeVegetation(timeSpan));
						}
					}
				}
			}
			return null;
		}

		private ScheduledRun? LatestGameDay(WorldState current, ScheduleClock now)
		{
			if (now.GameDay <= current.CursorGameDay)
			{
				return null;
			}
			double num = Math.Floor((now.GameDay - current.GameDayAnchor) / settings.GameDayInterval);
			double num2 = Math.Floor((current.CursorGameDay - current.GameDayAnchor) / settings.GameDayInterval);
			if (num < 1.0 || num <= num2)
			{
				return null;
			}
			double gameDay = current.GameDayAnchor + num * settings.GameDayInterval;
			return new ScheduledRun(current.Fingerprint + ":days:" + current.GameDayAnchor.ToString("R", CultureInfo.InvariantCulture) + ":" + num.ToString("0", CultureInfo.InvariantCulture), now.UtcNow, gameDay, "GameDays", vegetation: true);
		}

		private void Persist(WorldState next)
		{
			try
			{
				lease.Write(next);
				state = next;
			}
			catch
			{
				faulted = true;
				throw;
			}
		}

		private static void Advance(WorldState target, ScheduleClock clock)
		{
			target.CursorUtcTicks = Math.Max(target.CursorUtcTicks, clock.UtcNow.UtcDateTime.Ticks);
			target.CursorGameDay = Math.Max(target.CursorGameDay, clock.GameDay);
		}

		private static void ResetAutomaticSchedule(WorldState target, ScheduleSettings settings, ScheduleClock now)
		{
			target.Fingerprint = settings.Fingerprint;
			ScheduledRun? pending = target.Pending;
			if (pending == null || !pending.IsManual)
			{
				target.Pending = null;
			}
			target.GameDayAnchor = now.GameDay;
			target.CursorGameDay = now.GameDay;
			target.CursorUtcTicks = now.UtcNow.UtcDateTime.Ticks;
		}

		private static WorldState NewState(ScheduleSettings settings, string worldId, ScheduleClock now)
		{
			return new WorldState
			{
				WorldId = worldId,
				Fingerprint = settings.Fingerprint,
				CursorUtcTicks = now.UtcNow.UtcDateTime.Ticks,
				CursorGameDay = now.GameDay,
				GameDayAnchor = now.GameDay
			};
		}

		private void EnsureUsable()
		{
			if (disposed)
			{
				throw new ObjectDisposedException("FreshWorldScheduler");
			}
			if (faulted)
			{
				throw new InvalidOperationException("State persistence failed; this scheduler is disabled for the session.");
			}
		}

		public void Dispose()
		{
			lock (gate)
			{
				if (disposed)
				{
					return;
				}
				try
				{
					if (!faulted)
					{
						lease.Write(state);
					}
				}
				finally
				{
					disposed = true;
					lease.Dispose();
				}
			}
		}
	}
	public sealed class StateCorruptionException : IOException
	{
		public StateCorruptionException(string message)
			: base(message)
		{
		}

		public StateCorruptionException(string message, Exception inner)
			: base(message, inner)
		{
		}
	}
	public sealed class JsonWorldStateStore
	{
		internal sealed class Lease : IDisposable
		{
			private readonly string path;

			private readonly FileStream fileLock;

			private readonly DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(WorldState));

			public Lease(string path, FileStream fileLock)
			{
				this.path = path;
				this.fileLock = fileLock;
			}

			public WorldState? Read(string worldId)
			{
				if (!File.Exists(path))
				{
					return null;
				}
				try
				{
					using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
					if (fileStream.Length > 4194304)
					{
						throw new StateCorruptionException("FreshWorld state exceeds its size limit.");
					}
					WorldState obj = (serializer.ReadObject(fileStream) as WorldState) ?? throw new StateCorruptionException("FreshWorld state is empty.");
					obj.Validate(worldId);
					return obj;
				}
				catch (StateCorruptionException)
				{
					throw;
				}
				catch (Exception ex2) when (ex2 is SerializationException || ex2 is XmlException || ex2 is ArgumentException)
				{
					throw new StateCorruptionException("FreshWorld state is corrupt. Automatic reset is disabled; a stale backup is never restored automatically.", ex2);
				}
			}

			public void Write(WorldState state)
			{
				state.Validate(state.WorldId);
				string sourceFileName = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
				try
				{
					using (FileStream fileStream = new FileStream(sourceFileName, FileMode.CreateNew, FileAccess.Write, FileShare.None))
					{
						serializer.WriteObject(fileStream, state);
						fileStream.Flush(flushToDisk: true);
					}
					if (File.Exists(path))
					{
						File.Replace(sourceFileName, path, null);
					}
					else
					{
						File.Move(sourceFileName, path);
					}
				}
				finally
				{
					if (File.Exists(sourceFileName))
					{
						File.Delete(sourceFileName);
					}
				}
			}

			public void Dispose()
			{
				fileLock.Dispose();
			}
		}

		private readonly string directory;

		public JsonWorldStateStore(string directory)
		{
			if (string.IsNullOrWhiteSpace(directory))
			{
				throw new ArgumentException("A state directory is required.", "directory");
			}
			this.directory = Path.GetFullPath(directory);
		}

		public string GetStatePath(string worldId)
		{
			ValidateWorldId(worldId);
			return Path.Combine(directory, ScheduleSettings.Hash(worldId) + ".json");
		}

		internal Lease Acquire(string worldId)
		{
			string statePath = GetStatePath(worldId);
			Directory.CreateDirectory(directory);
			return new Lease(statePath, new FileStream(statePath + ".lock", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None));
		}

		private static void ValidateWorldId(string worldId)
		{
			if (string.IsNullOrWhiteSpace(worldId))
			{
				throw new ArgumentException("Use the persistent world UID, not its display name.", "worldId");
			}
		}
	}
	public enum ScheduleMode
	{
		DailyTimes,
		GameDays
	}
	public sealed class ScheduleSettings
	{
		private readonly HashSet<TimeSpan> vegetationTimes;

		public ScheduleMode Mode { get; }

		public bool AutomaticEnabled { get; }

		public IReadOnlyList<TimeSpan> DailyTimes { get; }

		public TimeZoneInfo TimeZone { get; }

		public double GameDayInterval { get; }

		public bool RunMissedOnWorldStart { get; }

		internal string Fingerprint { get; }

		public ScheduleSettings(ScheduleMode mode, string dailyTimesCsv, string timeZoneId, double gameDayInterval, string vegetationDailyTimesCsv = "*", bool runMissedOnWorldStart = false, bool automaticEnabled = true)
		{
			if (!Enum.IsDefined(typeof(ScheduleMode), mode))
			{
				throw new ArgumentOutOfRangeException("mode");
			}
			Mode = mode;
			AutomaticEnabled = automaticEnabled;
			bool flag = automaticEnabled && mode == ScheduleMode.DailyTimes;
			bool flag2 = automaticEnabled && mode == ScheduleMode.GameDays;
			if (flag2)
			{
				ValidateInterval(gameDayInterval);
			}
			GameDayInterval = (flag2 ? gameDayInterval : 24.0);
			RunMissedOnWorldStart = automaticEnabled && runMissedOnWorldStart;
			DailyTimes = Array.AsReadOnly(flag ? ParseTimes(dailyTimesCsv, allowEmpty: false) : Array.Empty<TimeSpan>());
			TimeZone = (flag ? ResolveTimeZone(timeZoneId) : TimeZoneInfo.Utc);
			vegetationTimes = (flag ? ParseVegetationTimes(DailyTimes, vegetationDailyTimesCsv) : new HashSet<TimeSpan>());
			string text = mode switch
			{
				ScheduleMode.GameDays => gameDayInterval.ToString("R", CultureInfo.InvariantCulture), 
				ScheduleMode.DailyTimes => string.Join("|", TimeZone.Id, string.Join(",", DailyTimes.Select(FormatTime)), string.Join(",", vegetationTimes.OrderBy((TimeSpan t) => t).Select(FormatTime))), 
				_ => "", 
			};
			Fingerprint = Hash(automaticEnabled ? ("active-v2|" + mode.ToString() + "|" + text) : "automatic-disabled-v1").Substring(0, 16);
		}

		internal bool IncludeVegetation(TimeSpan slot)
		{
			return vegetationTimes.Contains(slot);
		}

		internal static string FormatTime(TimeSpan time)
		{
			return time.ToString("hh\\:mm", CultureInfo.InvariantCulture);
		}

		internal static string Hash(string value)
		{
			using SHA256 sHA = SHA256.Create();
			return string.Concat(from b in sHA.ComputeHash(Encoding.UTF8.GetBytes(value))
				select b.ToString("x2", CultureInfo.InvariantCulture));
		}

		private static void ValidateInterval(double interval)
		{
			if (double.IsNaN(interval) || double.IsInfinity(interval) || interval <= 0.0)
			{
				throw new ArgumentOutOfRangeException("interval", "Game day interval must be finite and greater than zero.");
			}
		}

		private static TimeZoneInfo ResolveTimeZone(string timeZoneId)
		{
			if (!string.Equals(timeZoneId, "Local", StringComparison.OrdinalIgnoreCase))
			{
				return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
			}
			return TimeZoneInfo.Local;
		}

		private static HashSet<TimeSpan> ParseVegetationTimes(IReadOnlyList<TimeSpan> times, string text)
		{
			if (text == null)
			{
				throw new ArgumentNullException("text");
			}
			IReadOnlyList<TimeSpan> collection;
			if (!(text.Trim() == "*"))
			{
				IReadOnlyList<TimeSpan> readOnlyList = ParseTimes(text, allowEmpty: true);
				collection = readOnlyList;
			}
			else
			{
				collection = times;
			}
			HashSet<TimeSpan> hashSet = new HashSet<TimeSpan>(collection);
			if (hashSet.Any((TimeSpan t) => !times.Contains(t)))
			{
				throw new ArgumentException("Vegetation times must be a subset of daily run times, or '*'.", "text");
			}
			return hashSet;
		}

		private static TimeSpan[] ParseTimes(string text, bool allowEmpty)
		{
			if (text == null)
			{
				throw new ArgumentNullException("text");
			}
			if (allowEmpty && string.IsNullOrWhiteSpace(text))
			{
				return Array.Empty<TimeSpan>();
			}
			HashSet<TimeSpan> hashSet = new HashSet<TimeSpan>();
			string[] array = text.Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				if (!TimeSpan.TryParseExact(array[i].Trim(), "hh\\:mm", CultureInfo.InvariantCulture, out var result) || result.TotalHours >= 24.0)
				{
					throw new ArgumentException("Daily times must contain HH:mm values separated by commas.", "text");
				}
				if (!hashSet.Add(result))
				{
					throw new ArgumentException("Daily times cannot contain duplicates.", "text");
				}
			}
			if (hashSet.Count == 0)
			{
				throw new ArgumentException("At least one daily run time is required.", "text");
			}
			return hashSet.OrderBy((TimeSpan t) => t).ToArray();
		}
	}
	public readonly struct ScheduleClock
	{
		public DateTimeOffset UtcNow { get; }

		public double GameDay { get; }

		public ScheduleClock(DateTimeOffset utcNow, double gameDay)
		{
			if (double.IsNaN(gameDay) || double.IsInfinity(gameDay) || gameDay < 0.0)
			{
				throw new ArgumentOutOfRangeException("gameDay");
			}
			UtcNow = utcNow.ToUniversalTime();
			GameDay = gameDay;
		}
	}
	[DataContract]
	public sealed class ScheduledRun
	{
		internal const string ManualSlot = "Manual";

		[DataMember(Order = 2)]
		private long dueUtcTicks;

		[DataMember(Order = 1)]
		public string Id { get; private set; } = "";

		[DataMember(Order = 3)]
		public double GameDay { get; private set; }

		[DataMember(Order = 4)]
		public string Slot { get; private set; } = "";

		[DataMember(Order = 5)]
		public bool IncludeVegetation { get; private set; }

		public DateTimeOffset DueUtc => new DateTimeOffset(dueUtcTicks, TimeSpan.Zero);

		internal bool IsManual => string.Equals(Slot, "Manual", StringComparison.Ordinal);

		internal bool Valid
		{
			get
			{
				if (!string.IsNullOrWhiteSpace(Id) && dueUtcTicks > 0 && dueUtcTicks <= DateTime.MaxValue.Ticks && GameDay >= 0.0 && !double.IsInfinity(GameDay) && !double.IsNaN(GameDay))
				{
					return Slot != null;
				}
				return false;
			}
		}

		internal ScheduledRun(string id, DateTimeOffset dueUtc, double gameDay, string slot, bool vegetation)
		{
			Id = id;
			dueUtcTicks = dueUtc.UtcDateTime.Ticks;
			GameDay = gameDay;
			Slot = slot;
			IncludeVegetation = vegetation;
		}
	}
	public enum RunStatus
	{
		Running,
		Completed,
		Failed,
		Interrupted
	}
	[DataContract]
	public sealed class RunRecord
	{
		[DataMember(Order = 3)]
		private long startedUtcTicks;

		[DataMember(Order = 4)]
		private long finishedUtcTicks;

		[DataMember(Order = 1)]
		public ScheduledRun Run { get; private set; }

		[DataMember(Order = 2)]
		public RunStatus Status { get; internal set; }

		[DataMember(Order = 5)]
		public string Message { get; internal set; } = "";

		public DateTimeOffset StartedUtc => new DateTimeOffset(startedUtcTicks, TimeSpan.Zero);

		public DateTimeOffset? FinishedUtc
		{
			get
			{
				if (finishedUtcTicks != 0L)
				{
					return new DateTimeOffset(finishedUtcTicks, TimeSpan.Zero);
				}
				return null;
			}
		}

		internal bool Valid
		{
			get
			{
				if (Run != null && Run.Valid && Enum.IsDefined(typeof(RunStatus), Status) && startedUtcTicks > 0 && startedUtcTicks <= DateTime.MaxValue.Ticks && finishedUtcTicks >= 0 && finishedUtcTicks <= DateTime.MaxValue.Ticks && ((Status == RunStatus.Running) ? (finishedUtcTicks == 0) : (finishedUtcTicks > 0)))
				{
					return Message != null;
				}
				return false;
			}
		}

		internal RunRecord(ScheduledRun run, DateTimeOffset start)
		{
			Run = run;
			startedUtcTicks = start.UtcDateTime.Ticks;
			Status = RunStatus.Running;
		}

		internal void Finish(RunStatus status, DateTimeOffset time, string message)
		{
			Status = status;
			finishedUtcTicks = time.UtcDateTime.Ticks;
			Message = message ?? "";
		}

		internal RunRecord Copy()
		{
			return (RunRecord)MemberwiseClone();
		}
	}
	[DataContract]
	internal sealed class WorldState
	{
		[DataMember(Order = 1)]
		public int SchemaVersion = 1;

		[DataMember(Order = 2)]
		public string WorldId = "";

		[DataMember(Order = 3)]
		public string Fingerprint = "";

		[DataMember(Order = 4)]
		public long CursorUtcTicks;

		[DataMember(Order = 5)]
		public double CursorGameDay;

		[DataMember(Order = 6)]
		public double GameDayAnchor;

		[DataMember(Order = 7)]
		public ScheduledRun? Pending;

		[DataMember(Order = 8)]
		public List<RunRecord> Attempts = new List<RunRecord>();

		public WorldState Copy()
		{
			WorldState obj = (WorldState)MemberwiseClone();
			obj.Attempts = Attempts.Select((RunRecord a) => a.Copy()).ToList();
			return obj;
		}

		public void Validate(string expectedWorldId)
		{
			if (SchemaVersion != 1 || WorldId != expectedWorldId || string.IsNullOrWhiteSpace(Fingerprint) || CursorUtcTicks <= 0 || CursorUtcTicks > DateTime.MaxValue.Ticks || !FiniteNonnegative(CursorGameDay) || !FiniteNonnegative(GameDayAnchor) || GameDayAnchor > CursorGameDay || Attempts == null || Attempts.Any((RunRecord a) => a == null || !a.Valid) || Attempts.Select((RunRecord a) => a.Run.Id).Distinct().Count() != Attempts.Count || Attempts.Count((RunRecord a) => a.Status == RunStatus.Running) > 1 || (Pending != null && (!Pending.Valid || Attempts.Any((RunRecord a) => a.Run.Id == Pending.Id))))
			{
				throw new StateCorruptionException("FreshWorld state is inconsistent. Automatic reset is disabled until the state file is recovered.");
			}
		}

		private static bool FiniteNonnegative(double x)
		{
			if (x >= 0.0 && !double.IsNaN(x))
			{
				return !double.IsInfinity(x);
			}
			return false;
		}
	}
}
namespace FreshWorld.Runtime
{
	public sealed class GuardedCoroutine : IEnumerator, IDisposable
	{
		private readonly Stack<IEnumerator> _stack = new Stack<IEnumerator>();

		private readonly Func<bool> _canContinue;

		private readonly Action<Exception> _onError;

		private readonly Action<bool> _onFinished;

		private bool _finished;

		private object? _current;

		public object? Current => _current;

		public GuardedCoroutine(IEnumerator root, Func<bool> canContinue, Action<Exception> onError, Action<bool> onFinished)
		{
			if (root == null)
			{
				throw new ArgumentNullException("root");
			}
			_canContinue = canContinue ?? throw new ArgumentNullException("canContinue");
			_onError = onError ?? throw new ArgumentNullException("onError");
			_onFinished = onFinished ?? throw new ArgumentNullException("onFinished");
			_stack.Push(root);
		}

		public bool MoveNext()
		{
			if (_finished)
			{
				return false;
			}
			_current = null;
			try
			{
				while (_stack.Count > 0)
				{
					if (!_canContinue())
					{
						Finish(success: false);
						return false;
					}
					IEnumerator enumerator = _stack.Peek();
					if (!enumerator.MoveNext())
					{
						_stack.Pop();
						(enumerator as IDisposable)?.Dispose();
						continue;
					}
					object current = enumerator.Current;
					if (current is IEnumerator enumerator2)
					{
						foreach (IEnumerator item in _stack)
						{
							if (item == enumerator2)
							{
								throw new InvalidOperationException("A coroutine yielded an already active enumerator.");
							}
						}
						_stack.Push(enumerator2);
						continue;
					}
					_current = current;
					return true;
				}
				Finish(success: true);
				return false;
			}
			catch (Exception error)
			{
				Report(error);
				Finish(success: false);
				return false;
			}
		}

		public void Reset()
		{
			throw new NotSupportedException("Coroutines cannot be reset.");
		}

		public void Dispose()
		{
			Finish(success: false);
		}

		private void Finish(bool success)
		{
			if (_finished)
			{
				return;
			}
			_finished = true;
			_current = null;
			while (_stack.Count > 0)
			{
				IEnumerator enumerator = _stack.Pop();
				try
				{
					(enumerator as IDisposable)?.Dispose();
				}
				catch (Exception error)
				{
					success = false;
					Report(error);
				}
			}
			try
			{
				_onFinished(success);
			}
			catch (Exception error2)
			{
				Report(error2);
			}
		}

		private void Report(Exception error)
		{
			try
			{
				_onError(error);
			}
			catch (Exception)
			{
			}
		}
	}
}
namespace FreshWorld.Engine
{
	internal sealed class AlwaysProtectedObjects
	{
		private readonly HashSet<int> _prefabs;

		private readonly Func<ZDO, bool>? _additionalProtection;

		private readonly HashSet<Vector2s> _observed = new HashSet<Vector2s>();

		public AlwaysProtectedObjects(IEnumerable<string> prefabs, Func<ZDO, bool>? additionalProtection = null)
		{
			_prefabs = new HashSet<int>(prefabs.Select((string id) => StringExtensionMethods.GetStableHashCode(id)));
			_additionalProtection = additionalProtection;
		}

		public bool IsProtectedObject(ZDO zdo)
		{
			if (zdo.IsValid())
			{
				if (!_prefabs.Contains(zdo.GetPrefab()))
				{
					return _additionalProtection?.Invoke(zdo) ?? false;
				}
				return true;
			}
			return false;
		}

		public int Capture()
		{
			if (_prefabs.Count == 0 && _additionalProtection == null)
			{
				return 0;
			}
			foreach (ZDO item in GameWorld.AllZDOs())
			{
				Observe(item);
			}
			return _observed.Count;
		}

		public bool CanResetZone(Vector2s zone)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			if (_prefabs.Count == 0 && _additionalProtection == null)
			{
				return true;
			}
			if (_observed.Contains(zone))
			{
				return false;
			}
			foreach (ZDO zDO in GameWorld.GetZDOs(zone))
			{
				if (Observe(zDO))
				{
					return false;
				}
			}
			return true;
		}

		private bool Observe(ZDO zdo)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			if (!IsProtectedObject(zdo))
			{
				return false;
			}
			_observed.Add(ZoneSystem.GetZone(zdo.GetPosition()));
			return true;
		}
	}
	internal static class BaseProtection
	{
		private static HashSet<int> pieceBlacklist = new HashSet<int>();

		private static HashSet<int> unconditionalObjects = new HashSet<int>();

		private static HashSet<Vector2s> excluded = new HashSet<Vector2s>();

		private static DateTime calculatedAt = DateTime.MinValue;

		private static int lastSize = -1;

		public static void Configure(IEnumerable<string> blacklistedPieces, IEnumerable<string> alwaysProtectedObjects)
		{
			pieceBlacklist = new HashSet<int>(blacklistedPieces.Select((string id) => StringExtensionMethods.GetStableHashCode(id)));
			unconditionalObjects = new HashSet<int>(alwaysProtectedObjects.Select((string id) => StringExtensionMethods.GetStableHashCode(id)));
			InvalidateCache();
		}

		public static HashSet<Vector2s> GetExcluded(int size)
		{
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_0125: Unknown result type (might be due to invalid IL or missing references)
			//IL_012a: Unknown result type (might be due to invalid IL or missing references)
			//IL_012d: Unknown result type (might be due to invalid IL or missing references)
			//IL_013b: Unknown result type (might be due to invalid IL or missing references)
			//IL_019e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0152: Unknown result type (might be due to invalid IL or missing references)
			//IL_0181: Unknown result type (might be due to invalid IL or missing references)
			//IL_0169: Unknown result type (might be due to invalid IL or missing references)
			if (size < 0)
			{
				throw new ArgumentOutOfRangeException("size");
			}
			if (lastSize == size && DateTime.UtcNow - calculatedAt < TimeSpan.FromSeconds(10.0))
			{
				return excluded;
			}
			HashSet<Vector2s> hashSet = new HashSet<Vector2s>();
			ZNetScene scene;
			Dictionary<int, bool> playerPrefabs;
			if (size > 0)
			{
				scene = ZNetScene.instance;
				if ((Object)(object)scene == (Object)null)
				{
					throw new InvalidOperationException("Base protection requires the world's prefab registry.");
				}
				playerPrefabs = pieceBlacklist.ToDictionary((int result) => result, (int _) => false);
				HashSet<Vector2s> hashSet2 = new HashSet<Vector2s>();
				int num = size - 1;
				foreach (ZDO item in GameWorld.AllZDOs())
				{
					int prefab = item.GetPrefab();
					if (!unconditionalObjects.Contains(prefab) && (!IsPlayerPrefab(prefab) || item.GetLong(ZDOVars.s_creator, 0L) == 0L))
					{
						continue;
					}
					Vector2s zone = ZoneSystem.GetZone(item.GetPosition());
					if (!hashSet2.Add(zone))
					{
						continue;
					}
					for (int num2 = Math.Max(-32768, zone.x - num); num2 <= Math.Min(32767, zone.x + num); num2++)
					{
						for (int num3 = Math.Max(-32768, zone.y - num); num3 <= Math.Min(32767, zone.y + num); num3++)
						{
							hashSet.Add(new Vector2s(num2, num3));
						}
					}
				}
			}
			excluded = hashSet;
			lastSize = size;
			calculatedAt = DateTime.UtcNow;
			return excluded;
			bool IsPlayerPrefab(int num4)
			{
				if (playerPrefabs.TryGetValue(num4, out var value))
				{
					return value;
				}
				GameObject prefab2 = scene.GetPrefab(num4);
				return playerPrefabs[num4] = (Object)(object)prefab2 != (Object)null && (Object)(object)prefab2.GetComponent<Piece>() != (Object)null;
			}
		}

		public static void InvalidateCache()
		{
			calculatedAt = DateTime.MinValue;
			lastSize = -1;
			excluded = new HashSet<Vector2s>();
		}
	}
	internal static class EmptyChunkSaveCompatibility
	{
		internal enum Strategy
		{
			FreshWorld,
			UpgradeWorld,
			Unsupported
		}

		private const string UpgradeOwner = "upgrade_world";

		private static readonly (MethodInfo Target, string Type, string Method, bool Prefix)[] Targets = new(MethodInfo, string, string, bool)[3]
		{
			(Required(typeof(ZDOMan), "AddObjectsPerChunk", typeof(int), typeof(byte), typeof(int[]), typeof(List<Tuple<ChunkIndex, List<ZDO>>>).MakeByRefType()), "UpgradeWorld.FindEmptiedChunks", "Prefix", true),
			(Required(typeof(ChunkSaveMapping), "Clone"), "UpgradeWorld.DropEmptiedChunks", "Postfix", false),
			(Required(typeof(ZDOMan), "DeleteOldChunks", typeof(string), typeof(FileSource)), "UpgradeWorld.DeleteEmptiedChunkFiles", "Prefix", true)
		};

		private static string? lastReport;

		internal static string? DisabledReason { get; private set; }

		internal static void Reset()
		{
			DisabledReason = null;
			lastReport = null;
		}

		private static MethodInfo Required(Type type, string name, params Type[] arguments)
		{
			return AccessTools.DeclaredMethod(type, name, arguments, (Type[])null) ?? throw new MissingMethodException(type.FullName, name);
		}

		internal static Strategy Inspect(out string detail)
		{
			List<string> list = new List<string>();
			int num = 0;
			Assembly assembly = null;
			bool flag = true;
			(MethodInfo, string, string, bool)[] targets = Targets;
			for (int i = 0; i < targets.Length; i++)
			{
				(MethodInfo, string, string, bool) tuple = targets[i];
				Patches patchInfo = Harmony.GetPatchInfo((MethodBase)tuple.Item1);
				if (patchInfo == null)
				{
					continue;
				}
				int num2 = 0;
				int num3 = 0;
				(string, ReadOnlyCollection<Patch>)[] array = new(string, ReadOnlyCollection<Patch>)[4]
				{
					("Prefix", patchInfo.Prefixes),
					("Postfix", patchInfo.Postfixes),
					("Transpiler", patchInfo.Transpilers),
					("Finalizer", patchInfo.Finalizers)
				};
				for (int j = 0; j < array.Length; j++)
				{
					(string, ReadOnlyCollection<Patch>) tuple2 = array[j];
					foreach (Patch item in tuple2.Item2)
					{
						MethodInfo patchMethod = item.PatchMethod;
						Assembly assembly2 = patchMethod.DeclaringType?.Assembly;
						if (item.owner != "upgrade_world" && assembly2?.GetName().Name != "UpgradeWorld" && patchMethod.DeclaringType?.Namespace != "UpgradeWorld")
						{
							continue;
						}
						num2++;
						list.Add(tuple.Item1.DeclaringType.Name + "." + tuple.Item1.Name + ": " + tuple2.Item1 + " " + item.owner + "/" + string.Format("{0}.{1} [priority={2}; before={3}; ", patchMethod.DeclaringType?.FullName, patchMethod.Name, item.priority, string.Join(",", item.before)) + string.Format("after={0}; {1}; MVID={2}]", string.Join(",", item.after), assembly2?.FullName, patchMethod.Module.ModuleVersionId));
						if (item.owner == "upgrade_world" && patchMethod.DeclaringType?.FullName == tuple.Item2 && patchMethod.Name == tuple.Item3 && patchMethod.IsStatic && patchMethod.ReturnType == typeof(void) && tuple2.Item1 == (tuple.Item4 ? "Prefix" : "Postfix") && assembly2?.GetName().Name == "UpgradeWorld")
						{
							num3++;
							if (assembly == null)
							{
								assembly = assembly2;
							}
							else if ((object)assembly != assembly2)
							{
								flag = false;
							}
						}
					}
				}
				if (num2 == 1 && num3 == 1)
				{
					num++;
				}
			}
			detail = string.Join("; ", list);
			if (list.Count == 0)
			{
				return Strategy.FreshWorld;
			}
			if (list.Count != Targets.Length || num != Targets.Length || !flag || assembly == null)
			{
				return Strategy.Unsupported;
			}
			string detail2;
			bool num4 = UpgradeWorldSaveContract.Recognizes(assembly, out detail2);
			detail = detail + "; " + detail2;
			if (!num4)
			{
				return Strategy.Unsupported;
			}
			return Strategy.UpgradeWorld;
		}

		internal static Strategy Check()
		{
			if (DisabledReason != null)
			{
				return Strategy.Unsupported;
			}
			Strategy strategy;
			string detail;
			try
			{
				strategy = Inspect(out detail);
			}
			catch (Exception ex)
			{
				strategy = Strategy.Unsupported;
				detail = ex.GetType().Name + ": " + ex.Message;
			}
			if (strategy == Strategy.Unsupported)
			{
				DisabledReason = "UpgradeWorld empty-chunk contract was not recognized. " + detail;
			}
			string text = strategy.ToString() + ":" + detail;
			if (text != lastReport)
			{
				lastReport = text;
				FreshWorldPlugin.LogSaveCompatibility(strategy switch
				{
					Strategy.FreshWorld => "FreshWorld empty chunk compatibility: FreshWorld strategy active.", 
					Strategy.UpgradeWorld => "FreshWorld empty chunk compatibility: UpgradeWorld cleanup contract recognized; FreshWorld empty replacements are disabled.", 
					_ => "FreshWorld disabled: " + DisabledReason + " New maintenance and FreshWorld save changes are disabled. World saves have NOT been blocked by FreshWorld. External save behavior is not verified; resolve the combination and restart to re-enable FreshWorld.", 
				}, strategy == Strategy.Unsupported);
			}
			return strategy;
		}
	}
	[HarmonyPatch(typeof(ZDOMan), "GetSaveClonePerChunk", new Type[] { })]
	internal static class EmptyChunkSavePatch
	{
		private static readonly FieldInfo FileCount = FindCounter("m_numFiles");

		private static readonly FieldInfo DirtyFileCount = FindCounter("m_dirtyFiles");

		private static FieldInfo FindCounter(string name)
		{
			FieldInfo field = (typeof(ZDOMan).GetNestedType("SaveData", BindingFlags.NonPublic) ?? throw new TypeLoadException("ZDOMan.SaveData")).GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field == null || field.FieldType != typeof(int))
			{
				throw new MissingFieldException("ZDOMan.SaveData", name);
			}
			return field;
		}

		[HarmonyPostfix]
		internal static void Postfix(List<Tuple<ChunkIndex, List<ZDO>>> __result, ChunkSaveMapping ___m_chunkSaveMapping, HashSet<ChunkIndex>[] ___m_dirtyChunks, List<ZDO>[] ___m_objectsBySector, object ___m_saveData)
		{
			//IL_0037: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_009b: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b0: 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_00c6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			ZNet instance = ZNet.instance;
			if (instance == null || !instance.IsServer() || EmptyChunkSaveCompatibility.Check() != EmptyChunkSaveCompatibility.Strategy.FreshWorld)
			{
				return;
			}
			HashSet<ChunkIndex> scheduled = new HashSet<ChunkIndex>();
			HashSet<ChunkIndex> splitParents = new HashSet<ChunkIndex>();
			foreach (Tuple<ChunkIndex, List<ZDO>> item in __result)
			{
				Record(item.Item1, scheduled, splitParents);
			}
			int num = 0;
			foreach (KeyValuePair<ChunkIndex, ChunkInfo> chunk in ___m_chunkSaveMapping.Chunks)
			{
				ChunkIndex key = chunk.Key;
				ChunkInfo value = chunk.Value;
				if (value.SaveChunk && value.m_numZDOs > 0 && IsOrdinaryChunk(key) && ___m_dirtyChunks[0].Contains(key) && !Overlaps(key, scheduled, splitParents) && IsEmpty(key, ___m_objectsBySector))
				{
					__result.Add(Tuple.Create<ChunkIndex, List<ZDO>>(key, new List<ZDO>()));
					Record(key, scheduled, splitParents);
					num++;
				}
			}
			if (num != 0)
			{
				FileCount.SetValue(___m_saveData, (int)FileCount.GetValue(___m_saveData) + num);
				DirtyFileCount.SetValue(___m_saveData, (int)DirtyFileCount.GetValue(___m_saveData) + num);
			}
		}

		private static bool IsOrdinaryChunk(ChunkIndex chunk)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0027: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0042: Unknown result type (might be due to invalid IL or missing references)
			if (!((ChunkIndex)(ref chunk)).Equals(ZoneSystem.ChunkPortal) && chunk.m_chunkSize <= 3 && (chunk.Chunk & 0xFF) < 64 && chunk.Chunk >> 8 < 64)
			{
				ChunkIndex val = ZoneSystem.ChunkIndexFromIndexAndSize(chunk, chunk.m_chunkSize);
				return ((ChunkIndex)(ref val)).Equals(chunk);
			}
			return false;
		}

		private static void Record(ChunkIndex chunk, HashSet<ChunkIndex> scheduled, HashSet<ChunkIndex> splitParents)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Unknown result type (might be due to invalid IL or missing references)
			if (IsOrdinaryChunk(chunk))
			{
				scheduled.Add(chunk);
				for (int i = chunk.m_chunkSize + 1; i <= 3; i++)
				{
					splitParents.Add(ZoneSystem.ChunkIndexFromIndexAndSize(chunk, (byte)i));
				}
			}
		}

		private static bool Overlaps(ChunkIndex chunk, HashSet<ChunkIndex> scheduled, HashSet<ChunkIndex> splitParents)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: 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)
			if (scheduled.Contains(chunk) || splitParents.Contains(chunk))
			{
				return true;
			}
			for (int i = chunk.m_chunkSize + 1; i <= 3; i++)
			{
				if (scheduled.Contains(ZoneSystem.ChunkIndexFromIndexAndSize(chunk, (byte)i)))
				{
					return true;
				}
			}
			return false;
		}

		private static bool IsEmpty(ChunkIndex chunk, List<ZDO>[] sectors)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Unknown result type (might be due to invalid IL or missing references)
			ValueTuple<int, int> zoneFromChunk = ZoneSystem.GetZoneFromChunk(chunk);
			int item = zoneFromChunk.Item1;
			int item2 = zoneFromChunk.Item2;
			int num = 8 << (int)chunk.m_chunkSize;
			for (int i = item2; i < item2 + num; i++)
			{
				for (int j = item; j < item + num; j++)
				{
					List<ZDO> list = sectors[ZoneSystem.SectorToIndex(j, i).Sector];
					if (list != null && list.Count != 0)
					{
						return false;
					}
				}
			}
			return true;
		}
	}
	internal sealed class EpicLootProtection
	{
		private static readonly int SpawnController = StringExtensionMethods.GetStableHashCode("EL_SpawnController");

		private static readonly int TreasureBiome = StringExtensionMethods.GetStableHashCode("TreasureMapChest.Biome");

		private static readonly int TreasureFound = StringExtensionMethods.GetStableHashCode("TreasureMapChest.HasBeenFound");

		private static readonly int TreasureSpawn = StringExtensionMethods.GetStableHashCode("treasure_spawn");

		private static readonly int BountySpawn = StringExtensionMethods.GetStableHashCode("bount_spawn");

		private static readonly int BountyId = StringExtensionMethods.GetStableHashCode("BountyID");

		private static readonly int IsBounty = StringExtensionMethods.GetStableHashCode("isBounty");

		private static readonly int Placed = StringExtensionMethods.GetStableHashCode("placed");

		private readonly HashSet<Vector2s> _observed = new HashSet<Vector2s>();

		private readonly bool _protectTreasure;

		private readonly bool _protectBounties;

		internal EpicLootProtection(bool protectTreasure, bool protectBounties)
		{
			_protectTreasure = protectTreasure;
			_protectBounties = protectBounties;
		}

		internal static bool IsTreasureObject(ZDO zdo)
		{
			if (zdo.IsValid())
			{
				if (zdo.GetPrefab() == SpawnController && !zdo.GetBool(IsBounty, false) && !zdo.GetBool(Placed, false))
				{
					byte[] byteArray = zdo.GetByteArray(TreasureSpawn, (byte[])null);
					if (byteArray != null && byteArray.Length > 0)
					{
						return true;
					}
				}
				if (!string.IsNullOrEmpty(zdo.GetString(TreasureBiome, "")))
				{
					return !zdo.GetBool(TreasureFound, false);
				}
				return false;
			}
			return false;
		}

		internal static bool IsBountyObject(ZDO zdo)
		{
			if (zdo.IsValid())
			{
				if (zdo.GetPrefab() == SpawnController && zdo.GetBool(IsBounty, false) && !zdo.GetBool(Placed, false))
				{
					byte[] byteArray = zdo.GetByteArray(BountySpawn, (byte[])null);
					if (byteArray != null && byteArray.Length > 0)
					{
						return true;
					}
				}
				return !string.IsNullOrEmpty(zdo.GetString(BountyId, ""));
			}
			return false;
		}

		internal static bool IsProtectedObject(ZDO zdo, bool protectTreasure, bool protectBounties)
		{
			if (!protectTreasure || !IsTreasureObject(zdo))
			{
				if (protectBounties)
				{
					return IsBountyObject(zdo);
				}
				return false;
			}
			return true;
		}

		public int Capture()
		{
			if (!_protectTreasure && !_protectBounties)
			{
				return 0;
			}
			foreach (ZDO item in GameWorld.AllZDOs())
			{
				Observe(item);
			}
			return _observed.Count;
		}

		public bool CanResetZone(Vector2s zone)
		{
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (!_protectTreasure && !_protectBounties)
			{
				return true;
			}
			if (_observed.Contains(zone))
			{
				return false;
			}
			foreach (ZDO zDO in GameWorld.GetZDOs(zone))
			{
				if (Observe(zDO))
				{
					return false;
				}
			}
			return true;
		}

		private bool Observe(ZDO zdo)
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			if (!IsProtectedObject(zdo, _protectTreasure, _protectBounties))
			{
				return false;
			}
			_observed.Add(ZoneSystem.GetZone(zdo.GetPosition()));
			return true;
		}
	}
	internal static class GameWorld
	{
		internal sealed class ZoneDeletionPlan
		{
			private sealed class Entry
			{
				internal readonly ZDO Object;

				internal readonly ZDOID Id;

				internal readonly ZDOID Spawned;

				internal readonly int Prefab;

				internal readonly Vector3 Position;

				internal readonly bool PendingBefore;

				internal string DeletePath = "not-attempted";

				internal Entry(ZDO zdo, HashSet<ZDOID> pending)
				{
					//IL_001a: Unknown result type (might be due to invalid IL or missing references)
					//IL_001f: Unknown result type (might be due to invalid IL or missing references)
					//IL_0032: Unknown result type (might be due to invalid IL or missing references)
					//IL_0037: Unknown result type (might be due to invalid IL or missing references)
					//IL_003f: Unknown result type (might be due to invalid IL or missing references)
					//IL_0044: Unknown result type (might be due to invalid IL or missing references)
					//IL_004c: Unknown result type (might be due to invalid IL or missing references)
					Object = zdo;
					Id = zdo.m_uid;
					Prefab = zdo.GetPrefab();
					Position = zdo.GetPosition();
					Spawned = zdo.GetConnectionZDOID((ConnectionType)3);
					PendingBefore = pending.Contains(Id);
				}

				internal bool Matches(ZDO zdo)
				{
					//IL_000a: Unknown result type (might be due to invalid IL or missing references)
					//IL_0010: Unknown result type (might be due to invalid IL or missing references)
					//IL_002b: Unknown result type (might be due to invalid IL or missing references)
					//IL_0030: Unknown result type (might be due to invalid IL or missing references)
					//IL_0034: Unknown result type (might be due to invalid IL or missing references)
					//IL_0042: Unknown result type (might be due to invalid IL or missing references)
					//IL_0048: Unknown result type (might be due to invalid IL or missing references)
					if (Object == zdo && zdo.m_uid == Id && zdo.GetPrefab() == Prefab)
					{
						Vector3 position = zdo.GetPosition();
						if (((Vector3)(ref position)).Equals(Position))
						{
							return zdo.GetConnectionZDOID((ConnectionType)3) == Spawned;
						}
					}
					return false;
				}
			}

			private readonly HashSet<Vector2s> zones;

			private readonly bool epicLoot;

			private readonly bool bounties;

			private readonly AlwaysProtectedObjects? absolute;

			private readonly Dictionary<ZDOID, Entry> entries = new Dictionary<ZDOID, Entry>();

			private readonly DateTime plannedUtc = DateTime.UtcNow;

			private DateTime executionUtc;

			internal string Stage { get; private set; } = "Plan";

			internal ZoneDeletionPlan(HashSet<Vector2s> group, bool protectEpicLoot, bool protectBounties, AlwaysProtectedObjects? alwaysProtected)
			{
				zones = new HashSet<Vector2s>(group);
				epicLoot = protectEpicLoot;
				bounties = protectBounties;
				absolute = alwaysProtected;
			}

			internal void Capture()
			{
				//IL_0026: Unknown result type (might be due to invalid IL or missing references)
				HashSet<ZDOID> pending = new HashSet<ZDOID>(PendingDestruction.Invoke(ZDOMan.instance));
				foreach (Vector2s zone in zones)
				{
					foreach (ZDO zDO in GetZDOs(zone))
					{
						Capture(zDO, pending);
					}
				}
				Validate();
			}

			private void Capture(ZDO zdo, HashSet<ZDOID> pending)
			{
				//IL_000f: Unknown result type (might be due to invalid IL or missing references)
				//IL_0014: Unknown result type (might be due to invalid IL or missing references)
				//IL_0027: Unknown result type (might be due to invalid IL or missing references)
				//IL_0043: Unknown result type (might be due to invalid IL or missing references)
				//IL_004f: Unknown result type (might be due to invalid IL or missing references)
				//IL_0054: Unknown result type (might be due to invalid IL or missing references)
				//IL_0071: Unknown result type (might be due to invalid IL or missing references)
				if (zdo.IsValid() && zones.Contains(ZoneSystem.GetZone(zdo.GetPosition())) && !entries.ContainsKey(zdo.m_uid))
				{
					Entry entry = new Entry(zdo, pending);
					entries.Add(entry.Id, entry);
					if (entry.Spawned != ZDOID.None && ObjectsById.Invoke(ZDOMan.instance).TryGetValue(entry.Spawned, out var value))
					{
						Capture(value, pending);
					}
				}
			}

			internal bool Contains(ZDOID id)
			{
				//IL_0006: Unknown result type (might be due to invalid IL or missing references)
				return entries.ContainsKey(id);
			}

			internal bool Matches(ZDO zdo)
			{
				//IL_0007: Unknown result type (might be due to invalid IL or missing references)
				if (entries.TryGetValue(zdo.m_uid, out Entry value))
				{
					return value.Matches(zdo);
				}
				return false;
			}

			internal IEnumerable<ZDO> Remaining()
			{
				foreach (ZDOID key in entries.Keys)
				{
					if (ObjectsById.Invoke(ZDOMan.instance).TryGetValue(key, out var value) && value.IsValid())
					{
						yield return value;
					}
				}
			}

			internal void Validate()
			{
				//IL_006c: Unknown result type (might be due to invalid IL or missing references)
				//IL_00d3: Unknown result type (might be due to invalid IL or missing references)
				Stage = "Preflight";
				if (zones.Any(HasPendingZoneLoad))
				{
					throw new InvalidOperationException("A location zone group has a pending load before deletion.");
				}
				Dictionary<ZDOID, ZDO> dictionary = ObjectsById.Invoke(ZDOMan.instance);
				foreach (Entry value2 in entries.Values)
				{
					if (!dictionary.TryGetValue(value2.Id, out var value) || !value.IsValid() || !value2.Matches(value) || Protection(value) != "none")
					{
						throw new InvalidOperationException("A location zone group object changed or is protected before deletion.");
					}
				}
				foreach (Vector2s zone in zones)
				{
					foreach (ZDO zDO in GetZDOs(zone))
					{
						if (!Matches(zDO))
						{
							throw new InvalidOperationException("An unplanned object appeared in a location zone group before deletion.");
						}
					}
				}
			}

			internal void Delete()
			{
				//IL_0042: Unknown result type (might be due to invalid IL or missing references)
				Stage = "Delete";
				executionUtc = DateTime.UtcNow;
				foreach (Entry value2 in entries.Values)
				{
					if (ObjectsById.Invoke(ZDOMan.instance).TryGetValue(value2.Id, out var value) && value.IsValid())
					{
						if (!value2.Matches(value))
						{
							throw new InvalidOperationException("A planned object was replaced during location zone group deletion.");
						}
						RemoveZDO(value, null, epicLoot, bounties, absolute, WithinPlan, Deleting);
					}
				}
				Stage = "PostDelete";
				CompleteZoneDeletions(zones, epicLoot, bounties, absolute, this);
				void Deleting(ZDO zdo, string path)
				{
					//IL_002e: Unknown result type (might be due to invalid IL or missing references)
					if (!WithinPlan(zdo) || Protection(zdo) != "none")
					{
						throw new InvalidOperationException("A planned object changed during location zone group deletion.");
					}
					entries[zdo.m_uid].DeletePath = path;
				}
				bool WithinPlan(ZDO zdo)
				{
					//IL_0007: Unknown result type (might be due to invalid IL or missing references)
					//IL_000c: Unknown result type (might be due to invalid IL or missing references)
					if (zones.Contains(ZoneSystem.GetZone(zdo.GetPosition())))
					{
						return Matches(zdo);
					}
					return false;
				}
			}

			private string Protection(ZDO zdo)
			{
				if (IsPlayer(zdo))
				{
					return "player";
				}
				AlwaysProtectedObjects? alwaysProtectedObjects = absolute;
				if (alwaysProtectedObjects != null && alwaysProtectedObjects.IsProtectedObject(zdo))
				{
					return "absolute-or-PoV";
				}
				if (JotunInvasionProtection.IsProtectedObject(zdo))
				{
					return "jotun-objective";
				}
				if (EpicLootProtection.IsProtectedObject(zdo, epicLoot, bounties))
				{
					return "epicloot";
				}
				return "none";
			}

			internal void ReportFailure(Action<string> warn, string runId, Vector2s anchor, Exception error)
			{
				//IL_0028: Unknown result type (might be due to invalid IL or missing references)
				//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
				//IL_00fd: 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_0308: Unknown result type (might be due to invalid IL or missing references)
				//IL_0349: Unknown result type (might be due to invalid IL or missing references)
				//IL_03f8: Unknown result type (might be due to invalid IL or missing references)
				//IL_0416: Unknown result type (might be due to invalid IL or missing references)
				//IL_0425: Unknown result type (might be due to invalid IL or missing references)
				//IL_042a: Unknown result type (might be due to invalid IL or missing references)
				//IL_047f: Unknown result type (might be due to invalid IL or missing references)
				//IL_0484: Unknown result type (might be due to invalid IL or missing references)
				//IL_04ab: Unknown result type (might be due to invalid IL or missing references)
				//IL_03b9: Unknown result type (might be due to invalid IL or missing references)
				//IL_04fa: Unknown result type (might be due to invalid IL or missing references)
				try
				{
					warn($"Location group stopped: runId={runId}; phase={Stage}; anchor={anchor}; error={error.Message}");
					Dictionary<ZDOID, ZDO> manager = ObjectsById.Invoke(ZDOMan.instance);
					HashSet<ZDOID> pending = new HashSet<ZDOID>(PendingDestruction.Invoke(ZDOMan.instance));
					Dictionary<ZDOID, ZDO> dictionary = new Dictionary<ZDOID, ZDO>();
					foreach (ZDO item in zones.SelectMany(GetZDOs))
					{
						dictionary[item.m_uid] = item;
					}
					foreach (Entry value7 in entries.Values)
					{
						if (manager.TryGetValue(value7.Id, out ZDO value))
						{
							dictionary[value7.Id] = value;
						}
					}
					IEnumerable<string> values = from zone in zones.Where((Vector2s zone) => ZoneSystem.instance.m_locationInstances.ContainsKey(zone)).Take(8)
						select ZoneSystem.instance.m_locationInstances[zone].m_location?.m_prefab.Name ?? "unknown";
					warn("Location group state: locations=" + string.Join(",", values) + "; " + string.Format("sectors={0} [{1}]; omittedSectors={2}; plannedUtc={3:O}; executionUtc={4}; ", zones.Count, string.Join(",", zones.Take(16)), Math.Max(0, zones.Count - 16), plannedUtc, (executionUtc == default(DateTime)) ? "not-started" : executionUtc.ToString("O")) + $"planned={entries.Count}; residual={dictionary.Count}; plannedAbsentOrInvalid={entries.Keys.Count((ZDOID id) => !manager.TryGetValue(id, out Entry value6) || !value6.IsValid())}; detailLimit={8}");
					KeyValuePair<ZDOID, ZDO>[] array = dictionary.Where((KeyValuePair<ZDOID, ZDO> pair) => !entries.TryGetValue(pair.Key, out value6) || !value6.Matches(pair.Value) || Protection(pair.Value) != "none" || !pending.Contains(pair.Key)).ToArray();
					KeyValuePair<ZDOID, ZDO>[] array2 = ((array.Length != 0) ? array : dictionary.ToArray());
					foreach (KeyValuePair<ZDOID, ZDO> item2 in array2.Take(8))
					{
						ZDO value2 = item2.Value;
						entries.TryGetValue(item2.Key, out Entry value3);
						int prefab = value2.GetPrefab();
						GameObject prefab2 = ZNetScene.instance.GetPrefab(prefab);
						string text = ((prefab2 != null) ? ((Object)prefab2).name : null) ?? "unknown";
						ZDO value4;
						bool flag = manager.TryGetValue(item2.Key, out value4) && value4 == value2;
						ZNetView value5;
						bool flag2 = SceneInstances.Invoke(ZNetScene.instance).TryGetValue(value2, out value5) && (Object)(object)value5 != (Object)null;
						string text2 = Protection(value2);
						string text3 = ((value3 == null) ? "unplanned" : ((!value3.Matches(value2)) ? "changed" : ((text2 != "none") ? "protected" : ((!pending.Contains(item2.Key)) ? "not-queued" : "queued-native-deletion"))));
						warn($"Location group object: id={item2.Key}; prefab={text}; hash={prefab}; position={value2.GetPosition()}; zone={ZoneSystem.GetZone(value2.GetPosition())}; " + $"valid={value2.IsValid()}; dictionary={flag}; scene={flag2}; planned={value3 != null}; inScope={zones.Contains(ZoneSystem.GetZone(value2.GetPosition()))}; protection={text2}; exclusion={text3}; " + string.Format("spawned={0}; deletePath={1}; ", value2.GetConnectionZDOID((ConnectionType)3), value3?.DeletePath ?? "not-planned") + string.Format("pendingBefore={0}; pendingNow={1}; newSincePlan={2}", value3?.PendingBefore.ToString() ?? "unknown", pending.Contains(item2.Key), value3 == null));
					}
					if (array2.Length > 8)
					{
						warn($"Location group diagnostics: {array2.Length - 8} additional objects omitted.");
					}
				}
				catch
				{
				}
			}
		}

		[HarmonyPatch(typeof(ZDOMan), "SendDestroyed")]
		private static class DestroyBatchPatch
		{
			private const int MaxPerPacket = 10000;

			[HarmonyPrefix]
			private static bool Prefix(ZDOMan __instance)
			{
				//IL_001c: Unknown result type (might be due to invalid IL or missing references)
				//IL_0022: Expected O, but got Unknown
				//IL_0034: Unknown result type (might be due to invalid IL or missing references)
				List<ZDOID> list = PendingDestruction.Invoke(__instance);
				if (list.Count < 10000)
				{
					return true;
				}
				ZPackage val = new ZPackage();
				val.Write(10000);
				for (int i = 0; i < 10000; i++)
				{
					val.Write(list[i]);
				}
				list.RemoveRange(0, 10000);
				ZRoutedRpc.instance.InvokeRoutedRPC(0L, "DestroyZDO", new object[1] { val });
				return false;
			}
		}

		private static readonly FieldRef<ZoneSystem, HashSet<Vector2s>> GeneratedZones = AccessTools.FieldRefAccess<ZoneSystem, HashSet<Vector2s>>("m_generatedZones");

		private static readonly FieldRef<ZDOMan, Dictionary<ZDOID, ZDO>> ObjectsById = AccessTools.FieldRefAccess<ZDOMan, Dictionary<ZDOID, ZDO>>("m_objectsByID");

		private static readonly FieldRef<ZDOMan, List<ZDOID>> PendingDestruction = AccessTools.FieldRefAccess<ZDOMan, List<ZDOID>>("m_destroySendList");

		private static readonly Action<ZDOMan, ZDOID> HandleDestroyed = AccessTools.MethodDelegate<Action<ZDOMan, ZDOID>>(AccessTools.Method(typeof(ZDOMan), "HandleDestroyedZDO", (Type[])null, (Type[])null), (object)null, true);

		private static readonly FieldRef<ZNetScene, Dictionary<ZDO, ZNetView>> SceneInstances = AccessTools.FieldRefAccess<ZNetScene, Dictionary<ZDO, ZNetView>>("m_instances");

		private static readonly FieldRef<ZoneSystem, Dictionary<Vector2s, List<ZDO>>> LoadingObjects = AccessTools.FieldRefAccess<ZoneSystem, Dictionary<Vector2s, List<ZDO>>>("m_loadingObjectsInZones");

		private static readonly Func<ZoneSystem, Vector2s, bool> PokeLocalZone = AccessTools.MethodDelegate<Func<ZoneSystem, Vector2s, bool>>(AccessTools.Method(typeof(ZoneSystem), "PokeLocalZone", (Type[])null, (Type[])null), (object)null, true);

		private static readonly FieldInfo ZoneRoots = AccessTools.Field(typeof(ZoneSystem), "m_zones") ?? throw new MissingFieldException(typeof(ZoneSystem).FullName, "m_zones");

		private static readonly FieldInfo RootObject = AccessTools.Field(ZoneRoots.FieldType.GetGenericArguments()[1], "m_root") ?? throw new MissingFieldException("ZoneSystem.ZoneData", "m_root");

		private static readonly FieldInfo Heightmaps = AccessTools.Field(typeof(Heightmap), "s_heightmaps") ?? throw new MissingFieldException(typeof(Heightmap).FullName, "s_heightmaps");

		private static readonly FieldInfo HeightmapBuildData = AccessTools.Field(typeof(Heightmap), "m_buildData") ?? throw new MissingFieldException(typeof(Heightmap).FullName, "m_buildData");

		private static readonly int PlayerPrefab = StringExtensionMethods.GetStableHashCode("Player");

		private static readonly Dictionary<Vector2s, GameObject> OwnedLoads = new Dictionary<Vector2s, GameObject>();

		private static readonly HashSet<Vector2s> DeferredReleases = new HashSet<Vector2s>();

		private static ZoneSystem? _loadWorld;

		internal static bool HasPendingMaintenanceCleanup
		{
			get
			{
				EnsureLoadWorld();
				if (DeferredReleases.Count == 0 && OwnedLoads.Count == 0)
				{
					if (ZDOMan.instance != null)
					{
						return PendingDestruction.Invoke(ZDOMan.instance).Count != 0;
					}
					return false;
				}
				return true;
			}
		}

		internal static bool HasPendingZoneCleanup
		{
			get
			{
				EnsureLoadWorld();
				if (DeferredReleases.Count == 0)
				{
					return OwnedLoads.Count != 0;
				}
				return true;
			}
		}

		public static Vector2s[] GeneratedSnapshot(HashSet<Vector2s>? candidates = null)
		{
			IEnumerable<Vector2s> source = GeneratedZones.Invoke(ZoneSystem.instance);
			if (candidates != null)
			{
				source = source.Where(candidates.Contains);
			}
			return source.OrderBy((Vector2s zone) => (long)zone.x * (long)zone.x + (long)zone.y * (long)zone.y).ToArray();
		}

		public static HashSet<Vector2s> GeneratedSetSnapshot()
		{
			return new HashSet<Vector2s>(GeneratedZones.Invoke(ZoneSystem.instance));
		}

		public static bool IsGenerated(Vector2s zone)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			return GeneratedZones.Invoke(ZoneSystem.instance).Contains(zone);
		}

		public static bool HasPendingZoneLoad(Vector2s zone)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			return LoadingObjects.Invoke(ZoneSystem.instance).ContainsKey(zone);
		}

		public static List<ZDO> GetZDOs(Vector2s zone)
		{
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			List<ZDO> list = new List<ZDO>();
			ZDOMan.instance.FindSectorObjects(zone, new SimulationDistance(0, 0, false), list, (List<ZDO>)null);
			list.RemoveAll((ZDO zdo) => zdo == null || !zdo.IsValid() || ZoneSystem.GetZone(zdo.GetPosition()) != zone);
			return list;
		}

		public static IEnumerable<ZDO> AllZDOs()
		{
			return ObjectsById.Invoke(ZDOMan.instance).Values.ToArray();
		}

		internal static bool HasRemainingPrefabNearZone(Vector2s zone, int prefab, float border)
		{
			//IL_0002: Unknown result type (might be due to invalid IL or missing references)
			//IL_0003: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: 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_008d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Unkn