Decompiled source of WorldRenewal v0.1.0

plugins/WorldRenewal.dll

Decompiled a week ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using WorldRenewal.Core;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("WorldRenewal")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+d0bbbb5788adc3c1a83f8182b25e876accae748e")]
[assembly: AssemblyProduct("WorldRenewal")]
[assembly: AssemblyTitle("WorldRenewal")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[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 WorldRenewal
{
	[BepInPlugin("sansara.WorldRenewal", "World Renewal", "0.1.0")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string Guid = "sansara.WorldRenewal";

		public const string Name = "World Renewal";

		public const string Version = "0.1.0";

		internal static ManualLogSource Log;

		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<bool> DryRun;

		internal static ConfigEntry<float> IdleDays;

		internal static ConfigEntry<float> SpawnProtectRadius;

		internal static ConfigEntry<int> CheckMinutes;

		internal static ConfigEntry<int> MaxZonesPerPass;

		internal static ConfigEntry<int> TouchRadius;

		internal static ConfigEntry<string> Biomes;

		private float _nextTouch;

		private float _nextPass;

		private float _nextSave;

		private bool _started;

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Учёт посещений и восстановление зон.");
			DryRun = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "DryRun", true, "Пробный режим: только журнал «восстановил бы зону …», мир не меняется. false — восстанавливать по-настоящему.");
			IdleDays = ((BaseUnityPlugin)this).Config.Bind<float>("General", "IdleDays", 7f, "Сколько дней (реальных) зона и её соседи должны простоять без игроков рядом.");
			CheckMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("General", "CheckMinutes", 10, "Как часто искать зоны для восстановления, минуты.");
			MaxZonesPerPass = ((BaseUnityPlugin)this).Config.Bind<int>("General", "MaxZonesPerPass", 10, "Сколько зон восстанавливать за один проход.");
			SpawnProtectRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Protection", "SpawnProtectRadius", 300f, "Радиус защиты вокруг стартового алтаря, м.");
			TouchRadius = ((BaseUnityPlugin)this).Config.Bind<int>("Visits", "TouchRadiusZones", 2, "Сколько зон вокруг игрока считаются посещёнными (сервер держит загруженными около двух).");
			Biomes = ((BaseUnityPlugin)this).Config.Bind<string>("Protection", "Biomes", "Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, AshLands, DeepNorth", "Какие биомы восстанавливать: All или имена через запятую (Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, AshLands, DeepNorth, Ocean).");
			Log.LogInfo((object)"World Renewal 0.1.0 загружен.");
		}

		private void Update()
		{
			if (!Enabled.Value || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null || (Object)(object)ZoneSystem.instance == (Object)null || WorldGenerator.instance == null || !ZoneSystem.instance.LocationsGenerated)
			{
				return;
			}
			try
			{
				if (!_started)
				{
					_started = true;
					Visits.Load();
					_nextPass = Time.time + 60f;
					_nextSave = Time.time + 300f;
				}
				if (Time.time >= _nextTouch)
				{
					_nextTouch = Time.time + 10f;
					World.TouchPlayers();
				}
				if (Time.time >= _nextPass)
				{
					_nextPass = Time.time + (float)Mathf.Max(1, CheckMinutes.Value) * 60f;
					World.Pass();
				}
				if (Time.time >= _nextSave)
				{
					_nextSave = Time.time + 300f;
					Visits.Save();
				}
			}
			catch (Exception ex)
			{
				Log.LogError((object)("Обновление мира: ошибка: " + ex));
			}
		}

		private void OnDestroy()
		{
			if (_started)
			{
				Visits.Save();
			}
		}

		internal static HashSet<Biome> BiomeFilter()
		{
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			string text = Biomes.Value.Trim();
			if (text.Equals("All", StringComparison.OrdinalIgnoreCase) || text == "")
			{
				return null;
			}
			HashSet<Biome> hashSet = new HashSet<Biome>();
			string[] array = text.Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string text2 in array)
			{
				if (Enum.TryParse<Biome>(text2.Trim(), ignoreCase: true, out Biome result))
				{
					hashSet.Add(result);
				}
				else
				{
					Log.LogWarning((object)("Обновление мира: неизвестный биом «" + text2 + "» в Biomes — пропущен."));
				}
			}
			return hashSet;
		}
	}
	internal static class Visits
	{
		private static string _path;

		public static VisitBook Book { get; private set; } = new VisitBook();

		public static void Load()
		{
			string text = Path.Combine(Paths.ConfigPath, "sansara.WorldRenewal");
			Directory.CreateDirectory(text);
			string text2 = string.Concat(ZNet.instance.GetWorldName().Split(Path.GetInvalidFileNameChars()));
			_path = Path.Combine(text, text2 + ".visits");
			double timeSeconds = ZNet.instance.GetTimeSeconds();
			if (File.Exists(_path))
			{
				Book = VisitBook.Parse(File.ReadAllText(_path), out var badLines);
				Plugin.Log.LogInfo((object)($"Обновление мира: посещений зон в учёте {Book.Count}" + ((badLines > 0) ? $", испорченных строк пропущено {badLines}." : ".")));
				return;
			}
			Book = new VisitBook();
			int num = Book.AddUnknown(World.GeneratedZones(), timeSeconds);
			Plugin.Log.LogInfo((object)$"Обновление мира: первый запуск — {num} сгенерированных зон считаются посещёнными сейчас.");
			Save();
		}

		public static void Save()
		{
			if (_path != null && Book.Changed)
			{
				string text = _path + ".tmp";
				File.WriteAllText(text, Book.Serialize());
				if (File.Exists(_path))
				{
					File.Delete(_path);
				}
				File.Move(text, _path);
				Book.Changed = false;
			}
		}
	}
	internal static class World
	{
		private enum Kind
		{
			Other,
			Piece,
			Tombstone,
			ItemStand,
			Player,
			Character
		}

		private struct TrialBlock
		{
			public int X;

			public int Y;

			public int Radius;
		}

		private static readonly Dictionary<int, Kind> Kinds = new Dictionary<int, Kind>();

		private static HashSet<Vector2s> _generated;

		private static List<ZDO>[] _bySector;

		private static bool _reported;

		private static HashSet<Vector2s> Generated => _generated ?? (_generated = (HashSet<Vector2s>)AccessTools.Field(typeof(ZoneSystem), "m_generatedZones").GetValue(ZoneSystem.instance));

		public static IEnumerable<ZoneKey> GeneratedZones()
		{
			return Generated.Select((Vector2s z) => new ZoneKey(z.x, z.y)).ToList();
		}

		public static void TouchPlayers()
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: 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_005b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			double timeSeconds = ZNet.instance.GetTimeSeconds();
			foreach (ZNetPeer peer in ZNet.instance.GetPeers())
			{
				ZDO val = (((ZDOID)(ref peer.m_characterID)).IsNone() ? null : ZDOMan.instance.GetZDO(peer.m_characterID));
				if (val != null)
				{
					Vector2s zone = ZoneSystem.GetZone(val.GetPosition());
					Visits.Book.Touch(new ZoneKey(zone.x, zone.y), Mathf.Max(0, Plugin.TouchRadius.Value), timeSeconds);
				}
			}
		}

		public static void Pass()
		{
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0083: Unknown result type (might be due to invalid IL or missing references)
			double timeSeconds = ZNet.instance.GetTimeSeconds();
			List<ZoneKey> list = GeneratedZones().ToList();
			int num = Visits.Book.AddUnknown(list, timeSeconds);
			HashSet<Biome> biomes = Plugin.BiomeFilter();
			Vector3? spawn = StartTemple();
			List<TrialBlock> trialBlocks = MinePortalBlocks();
			if (!_reported)
			{
				_reported = true;
				ManualLogSource log = Plugin.Log;
				object arg;
				if (!spawn.HasValue)
				{
					arg = "не найден";
				}
				else
				{
					Vector3 value = spawn.Value;
					arg = ((Vector3)(ref value)).ToString("F0");
				}
				log.LogInfo((object)($"Обновление мира: стартовый алтарь {arg} (защита {Plugin.SpawnProtectRadius.Value:0} м), " + string.Format("мест Mine Portal {0}, биомы {1}, срок {2:0.##} дн., {3}.", trialBlocks.Count, Plugin.Biomes.Value, Plugin.IdleDays.Value, Plugin.DryRun.Value ? "пробный режим" : "боевой режим")));
			}
			int protectedZones = 0;
			List<ZoneKey> list2 = RenewalPicker.Pick(list, Visits.Book, Excluded, delegate(ZoneKey z)
			{
				bool flag = IsProtected(z);
				protectedZones += (flag ? 1 : 0);
				return flag;
			}, timeSeconds, (double)Plugin.IdleDays.Value * 86400.0, Plugin.MaxZonesPerPass.Value);
			bool value2 = Plugin.DryRun.Value;
			int num2 = 0;
			foreach (ZoneKey item in list2)
			{
				Renew(item, value2, timeSeconds, !value2 || num2++ < 20);
			}
			if (value2 && list2.Count > 20)
			{
				Plugin.Log.LogInfo((object)$"Обновление мира: … и ещё {list2.Count - 20} зон.");
			}
			if (list2.Count > 0 || num > 0)
			{
				Plugin.Log.LogInfo((object)($"Обновление мира: проход — зон в мире {list.Count}, новых в учёте {num}, защищённых встречено {protectedZones}, " + (value2 ? $"восстановил бы {list2.Count} (пробный режим)." : $"восстановлено {list2.Count}.")));
			}
			bool Excluded(ZoneKey z)
			{
				//IL_0025: Unknown result type (might be due to invalid IL or missing references)
				//IL_002a: Unknown result type (might be due to invalid IL or missing references)
				//IL_002f: 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_0036: Unknown result type (might be due to invalid IL or missing references)
				//IL_003c: Unknown result type (might be due to invalid IL or missing references)
				//IL_0041: Unknown result type (might be due to invalid IL or missing references)
				//IL_005f: Unknown result type (might be due to invalid IL or missing references)
				//IL_0066: Unknown result type (might be due to invalid IL or missing references)
				//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
				//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
				Vector3 zonePos = ZoneSystem.GetZonePos(new Vector2s((short)z.X, (short)z.Y));
				Vector2 val = new Vector2(zonePos.x, zonePos.z);
				if (((Vector2)(ref val)).magnitude > 9936f)
				{
					return true;
				}
				if (spawn.HasValue && Utils.DistanceXZ(zonePos, spawn.Value) < Plugin.SpawnProtectRadius.Value)
				{
					return true;
				}
				if (trialBlocks.Any((TrialBlock t) => Mathf.Abs(t.X - z.X) <= t.Radius && Mathf.Abs(t.Y - z.Y) <= t.Radius))
				{
					return true;
				}
				if (biomes != null)
				{
					return !biomes.Contains(WorldGenerator.instance.GetBiome(zonePos));
				}
				return false;
			}
		}

		private static bool IsProtected(ZoneKey zone)
		{
			foreach (ZDO item in ZoneObjects(zone))
			{
				switch (KindOf(item.GetPrefab()))
				{
				case Kind.Piece:
					if (item.GetLong(ZDOVars.s_creator, 0L) == 0L)
					{
						break;
					}
					goto case Kind.Tombstone;
				case Kind.Tombstone:
				case Kind.Player:
					return true;
				case Kind.ItemStand:
					if (item.GetInt(ZDOVars.s_item, 0) != 0)
					{
						return true;
					}
					break;
				case Kind.Character:
					if (item.GetBool(ZDOVars.s_tamed, false))
					{
						return true;
					}
					break;
				}
			}
			return false;
		}

		private static void Renew(ZoneKey zone, bool dry, double now, bool log)
		{
			//IL_001e: 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_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			//IL_007e: Unknown result type (might be due to invalid IL or missing references)
			//IL_012f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0140: Unknown result type (might be due to invalid IL or missing references)
			//IL_015c: Unknown result type (might be due to invalid IL or missing references)
			//IL_015d: Unknown result type (might be due to invalid IL or missing references)
			Vector2s val = default(Vector2s);
			((Vector2s)(ref val))..ctor((short)zone.X, (short)zone.Y);
			List<ZDO> list = ZoneObjects(zone);
			Vector3 zonePos = ZoneSystem.GetZonePos(val);
			LocationInstance value;
			string text = ((!ZoneSystem.instance.m_locationInstances.TryGetValue(val, out value)) ? "—" : (value.m_location?.m_prefab.Name ?? "?"));
			string text2 = $"{zone} ({WorldGenerator.instance.GetBiome(zonePos)}, локация {text}, объектов {list.Count})";
			if (dry)
			{
				if (log)
				{
					Plugin.Log.LogInfo((object)("Обновление мира: восстановил бы зону " + text2 + "."));
				}
				return;
			}
			foreach (ZDO item in list.Where((ZDO z) => KindOf(z.GetPrefab()) != Kind.Player))
			{
				item.SetOwner(ZDOMan.GetSessionID());
				ZDOMan.instance.DestroyZDO(item);
			}
			Generated.Remove(val);
			if (ZoneSystem.instance.m_locationInstances.TryGetValue(val, out var value2))
			{
				value2.m_placed = false;
				ZoneSystem.instance.m_locationInstances[val] = value2;
			}
			Visits.Book.Set(zone, now);
			Plugin.Log.LogInfo((object)("Обновление мира: восстановлена зона " + text2 + "."));
		}

		private static List<ZDO> ZoneObjects(ZoneKey zone)
		{
			//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)
			if (_bySector == null)
			{
				_bySector = (List<ZDO>[])AccessTools.Field(typeof(ZDOMan), "m_objectsBySector").GetValue(ZDOMan.instance);
			}
			uint sector = ZoneSystem.SectorToIndex(new Vector2s((short)zone.X, (short)zone.Y)).Sector;
			List<ZDO> list = ((sector < _bySector.Length) ? _bySector[sector] : null);
			if (list == null)
			{
				return new List<ZDO>();
			}
			return new List<ZDO>(list);
		}

		private static Kind KindOf(int hash)
		{
			if (!Kinds.TryGetValue(hash, out var value))
			{
				GameObject prefab = ZNetScene.instance.GetPrefab(hash);
				value = ((!((Object)(object)prefab == (Object)null)) ? (((Object)(object)prefab.GetComponent<Player>() != (Object)null) ? Kind.Player : (((Object)(object)prefab.GetComponent<TombStone>() != (Object)null) ? Kind.Tombstone : (((Object)(object)prefab.GetComponent<ItemStand>() != (Object)null && (Object)(object)prefab.GetComponent<Piece>() == (Object)null) ? Kind.ItemStand : (((Object)(object)prefab.GetComponent<Piece>() != (Object)null) ? Kind.Piece : (((Object)(object)prefab.GetComponent<Character>() != (Object)null) ? Kind.Character : Kind.Other))))) : Kind.Other);
				Kinds[hash] = value;
			}
			return value;
		}

		private static Vector3? StartTemple()
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			foreach (KeyValuePair<Vector2s, LocationInstance> locationInstance in ZoneSystem.instance.m_locationInstances)
			{
				if (locationInstance.Value.m_location?.m_prefab.Name == "StartTemple")
				{
					return locationInstance.Value.m_position;
				}
			}
			return null;
		}

		private static List<TrialBlock> MinePortalBlocks()
		{
			List<TrialBlock> list = new List<TrialBlock>();
			string text = string.Concat(ZNet.instance.GetWorldName().Split(Path.GetInvalidFileNameChars()));
			string path = Path.Combine(Paths.ConfigPath, "sansara.MinePortal", text + ".slots.txt");
			if (!File.Exists(path))
			{
				return list;
			}
			string[] array = File.ReadAllLines(path);
			for (int i = 0; i < array.Length; i++)
			{
				string[] array2 = array[i].Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				if (array2.Length == 3 && int.TryParse(array2[1], out var result) && int.TryParse(array2[2], out var result2))
				{
					list.Add(new TrialBlock
					{
						X = result,
						Y = result2,
						Radius = 6
					});
				}
			}
			return list;
		}
	}
}
namespace WorldRenewal.Core
{
	public struct ZoneKey : IEquatable<ZoneKey>
	{
		public int X { get; }

		public int Y { get; }

		public ZoneKey(int x, int y)
		{
			X = x;
			Y = y;
		}

		public bool Equals(ZoneKey other)
		{
			if (X == other.X)
			{
				return Y == other.Y;
			}
			return false;
		}

		public override bool Equals(object obj)
		{
			if (obj is ZoneKey other)
			{
				return Equals(other);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return (X * 73856093) ^ (Y * 19349663);
		}

		public override string ToString()
		{
			return X.ToString(CultureInfo.InvariantCulture) + "," + Y.ToString(CultureInfo.InvariantCulture);
		}

		public IEnumerable<ZoneKey> Block()
		{
			for (int dx = -1; dx <= 1; dx++)
			{
				for (int dy = -1; dy <= 1; dy++)
				{
					yield return new ZoneKey(X + dx, Y + dy);
				}
			}
		}
	}
	public sealed class VisitBook
	{
		private readonly Dictionary<ZoneKey, double> _last = new Dictionary<ZoneKey, double>();

		public int Count => _last.Count;

		public bool Changed { get; set; }

		public bool TryGet(ZoneKey zone, out double time)
		{
			return _last.TryGetValue(zone, out time);
		}

		public void Set(ZoneKey zone, double time)
		{
			_last[zone] = time;
			Changed = true;
		}

		public void Touch(ZoneKey center, int radius, double now)
		{
			for (int i = -radius; i <= radius; i++)
			{
				for (int j = -radius; j <= radius; j++)
				{
					Set(new ZoneKey(center.X + i, center.Y + j), now);
				}
			}
		}

		public int AddUnknown(IEnumerable<ZoneKey> zones, double now)
		{
			int num = 0;
			foreach (ZoneKey zone in zones)
			{
				if (!_last.ContainsKey(zone))
				{
					_last[zone] = now;
					num++;
				}
			}
			Changed |= num > 0;
			return num;
		}

		public string Serialize()
		{
			StringBuilder stringBuilder = new StringBuilder();
			foreach (KeyValuePair<ZoneKey, double> item in from p in _last
				orderby p.Key.X, p.Key.Y
				select p)
			{
				stringBuilder.Append(item.Key.X.ToString(CultureInfo.InvariantCulture)).Append(' ').Append(item.Key.Y.ToString(CultureInfo.InvariantCulture))
					.Append(' ')
					.Append(item.Value.ToString("R", CultureInfo.InvariantCulture))
					.Append('\n');
			}
			return stringBuilder.ToString();
		}

		public static VisitBook Parse(string text, out int badLines)
		{
			VisitBook visitBook = new VisitBook();
			badLines = 0;
			string[] array = (text ?? "").Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length != 0)
				{
					string[] array2 = text2.Split(new char[1] { ' ' });
					if (array2.Length == 3 && int.TryParse(array2[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2) && double.TryParse(array2[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var result3))
					{
						visitBook._last[new ZoneKey(result, result2)] = result3;
					}
					else
					{
						badLines++;
					}
				}
			}
			return visitBook;
		}
	}
	public static class RenewalPicker
	{
		public static List<ZoneKey> Pick(IEnumerable<ZoneKey> generated, VisitBook book, Func<ZoneKey, bool> excluded, Func<ZoneKey, bool> isProtected, double now, double idleSeconds, int max)
		{
			HashSet<ZoneKey> gen = new HashSet<ZoneKey>(generated);
			Dictionary<ZoneKey, bool> protectedCache = new Dictionary<ZoneKey, bool>();
			List<ZoneKey> list = new List<ZoneKey>();
			if (max <= 0)
			{
				return list;
			}
			foreach (ZoneKey item in gen.Where((ZoneKey z) => !excluded(z)).OrderBy(LastVisit).ThenBy((ZoneKey z) => z.X)
				.ThenBy((ZoneKey z) => z.Y))
			{
				if (item.Block().All(Idle) && !item.Block().Any(Protected))
				{
					list.Add(item);
					if (list.Count >= max)
					{
						break;
					}
				}
			}
			return list;
			bool Idle(ZoneKey z)
			{
				if (book.TryGet(z, out var time))
				{
					return now - time >= idleSeconds;
				}
				return true;
			}
			double LastVisit(ZoneKey z)
			{
				if (!book.TryGet(z, out var time))
				{
					return double.MinValue;
				}
				return time;
			}
			bool Protected(ZoneKey z)
			{
				if (!gen.Contains(z))
				{
					return false;
				}
				if (!protectedCache.TryGetValue(z, out var value))
				{
					value = isProtected(z);
					protectedCache[z] = value;
				}
				return value;
			}
		}
	}
}