Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ZoneSavior v1.0.1
ZoneSavior.dll
Decompiled 6 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using Splatform; using TMPro; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.UI; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.ObjectPool; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("ZoneSavior")] [assembly: AssemblyDescription("Dedicated-server zone archiving, zone bundle restore, zone UI, and per-zone WearNTear limits for Valheim.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("ZoneSavior")] [assembly: AssemblyCopyright("Copyright 2026 sighsorry")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("B276E8F6-7F9F-4A0F-8E7D-757383FF7CB1")] [assembly: AssemblyFileVersion("1.0.1")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList<T> : IEnumerable, ICollection, IList, IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T>, IList<T> { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator<T> { object IEnumerator.Current => _item; T IEnumerator<T>.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection<T>.Count => 1; T IReadOnlyList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection<T>.Count => 1; bool ICollection<T>.IsReadOnly => true; T IList<T>.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer<T>.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer<T>.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return new Enumerator(_item); } void ICollection<T>.Add(T item) { throw new NotSupportedException(); } void ICollection<T>.Clear() { throw new NotSupportedException(); } bool ICollection<T>.Contains(T item) { return EqualityComparer<T>.Default.Equals(_item, item); } void ICollection<T>.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection<T>.Remove(T item) { throw new NotSupportedException(); } int IList<T>.IndexOf(T item) { if (!EqualityComparer<T>.Default.Equals(_item, item)) { return -1; } return 0; } void IList<T>.Insert(int index, T item) { throw new NotSupportedException(); } void IList<T>.RemoveAt(int index) { throw new NotSupportedException(); } } namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class ExtensionMarkerAttribute : Attribute { private readonly string <Name>k__BackingField; public string Name => <Name>k__BackingField; public ExtensionMarkerAttribute(string name) { <Name>k__BackingField = name; } } } namespace ZoneSavior { internal static class AutoArchiveCommands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__HandleCommand; public static Action<long, ZPackage> <1>__RPC_HandleRequest; public static Action<long, ZPackage> <2>__RPC_HandleResult; public static Func<char, bool> <3>__IsDigit; } [CompilerGenerated] private sealed class <>c__DisplayClass24_0 { public ZoneBundleCommandResult result; internal void <ExecuteRestoreAsync>b__0(ZoneBundleCommandResult value) { result = value; } } [CompilerGenerated] private sealed class <ExecuteRestoreAsync>d__24 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public string tag; public Action<AutoArchiveCommandResult> onComplete; private <>c__DisplayClass24_0 <>8__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ExecuteRestoreAsync>d__24(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass24_0(); <>8__1.result = ZoneBundleCommandResult.Fail("Archive restore failed before it started."); <>2__current = ZoneBundleCommands.RestoreTagToOriginalZonesAsync(tag, delegate(ZoneBundleCommandResult value) { <>8__1.result = value; }); <>1__state = 1; return true; case 1: <>1__state = -1; onComplete(<>8__1.result.Success ? AutoArchiveCommandResult.Ok(new <>z__ReadOnlySingleElementList<string>(<>8__1.result.Message)) : AutoArchiveCommandResult.Fail(<>8__1.result.Message)); return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string ScanCommand = "zs_archive_scan"; private const string StatusCommand = "zs_archive_status"; private const string PlayerCommand = "zs_archive_player"; private const string ListCommand = "zs_archive_list"; private const string MarkSeenCommand = "zs_archive_mark_seen"; private const string IgnorePlayerCommand = "zs_archive_ignore_player"; private const string RestoreCommand = "zs_archive_restore"; private const string ScheduleCommand = "zs_archive_schedule"; private const string DebugZoneCommand = "zs_archive_debug_zone"; private const string RequestRpcName = "sighsorry.ZoneSavior_AutoArchiveCommandRequest"; private const string ResultRpcName = "sighsorry.ZoneSavior_AutoArchiveCommandResult"; private static readonly Regex ZoneSpecPattern = new Regex("^\\s*\\(\\s*(-?\\d+)\\s*,\\s*(-?\\d+)\\s*\\)\\s*$", RegexOptions.Compiled); private static ManualLogSource _logger = null; private static bool _initialized; private static readonly ZoneRpcRegistrar RpcRegistrar = new ZoneRpcRegistrar(); public static void Initialize(ManualLogSource logger) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_002e: 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_0039: Expected O, but got Unknown //IL_0076: 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) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_0096: 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_00a1: Expected O, but got Unknown //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Expected O, but got Unknown //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown //IL_0146: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Expected O, but got Unknown //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Expected O, but got Unknown //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_01a5: Expected O, but got Unknown //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Expected O, but got Unknown if (!_initialized) { _initialized = true; _logger = logger; object obj = <>O.<0>__HandleCommand; if (obj == null) { ConsoleEvent val = HandleCommand; <>O.<0>__HandleCommand = val; obj = (object)val; } new ConsoleCommand("zs_archive_scan", "[dry|save|reset] - Runs an auto archive scan.", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj2 = <>O.<0>__HandleCommand; if (obj2 == null) { ConsoleEvent val2 = HandleCommand; <>O.<0>__HandleCommand = val2; obj2 = (object)val2; } new ConsoleCommand("zs_archive_status", "- Prints auto archive activity status.", (ConsoleEvent)obj2, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj3 = <>O.<0>__HandleCommand; if (obj3 == null) { ConsoleEvent val3 = HandleCommand; <>O.<0>__HandleCommand = val3; obj3 = (object)val3; } new ConsoleCommand("zs_archive_player", "steamID [dry|save|reset] - Runs an auto archive scan filtered to one Steam owner.", (ConsoleEvent)obj3, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj4 = <>O.<0>__HandleCommand; if (obj4 == null) { ConsoleEvent val4 = HandleCommand; <>O.<0>__HandleCommand = val4; obj4 = (object)val4; } new ConsoleCommand("zs_archive_list", "- Lists recent auto archive runs.", (ConsoleEvent)obj4, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj5 = <>O.<0>__HandleCommand; if (obj5 == null) { ConsoleEvent val5 = HandleCommand; <>O.<0>__HandleCommand = val5; obj5 = (object)val5; } new ConsoleCommand("zs_archive_mark_seen", "playerID - Marks a playerID as seen now.", (ConsoleEvent)obj5, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj6 = <>O.<0>__HandleCommand; if (obj6 == null) { ConsoleEvent val6 = HandleCommand; <>O.<0>__HandleCommand = val6; obj6 = (object)val6; } new ConsoleCommand("zs_archive_ignore_player", "playerID [on|off] - Protects or unprotects a playerID from auto archive.", (ConsoleEvent)obj6, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj7 = <>O.<0>__HandleCommand; if (obj7 == null) { ConsoleEvent val7 = HandleCommand; <>O.<0>__HandleCommand = val7; obj7 = (object)val7; } new ConsoleCommand("zs_archive_restore", "tag - Restores an archived tag to its original zones.", (ConsoleEvent)obj7, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj8 = <>O.<0>__HandleCommand; if (obj8 == null) { ConsoleEvent val8 = HandleCommand; <>O.<0>__HandleCommand = val8; obj8 = (object)val8; } new ConsoleCommand("zs_archive_schedule", "[status|now|clear|last yyyy-MM-dd HH:mm|next yyyy-MM-dd HH:mm] - Shows or adjusts the automatic archive scan schedule anchor. Local server time is used unless the date includes Z or an offset.", (ConsoleEvent)obj8, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); object obj9 = <>O.<0>__HandleCommand; if (obj9 == null) { ConsoleEvent val9 = HandleCommand; <>O.<0>__HandleCommand = val9; obj9 = (object)val9; } new ConsoleCommand("zs_archive_debug_zone", "(x,z) - Writes a YAML report explaining auto archive eligibility for one zone.", (ConsoleEvent)obj9, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); RegisterRpcs(); } } internal static void RegisterRpcs() { RpcRegistrar.EnsureRegistered(delegate(ZRoutedRpc routedRpc) { routedRpc.Register<ZPackage>("sighsorry.ZoneSavior_AutoArchiveCommandRequest", (Action<long, ZPackage>)RPC_HandleRequest); routedRpc.Register<ZPackage>("sighsorry.ZoneSavior_AutoArchiveCommandResult", (Action<long, ZPackage>)RPC_HandleResult); }); } private static void HandleCommand(ConsoleEventArgs args) { EnsureCommandReady(); DispatchRequest(new AutoArchiveCommandRequest { Command = ((args.Args.Length != 0) ? args.Args[0] : ""), Args = args.Args.ToList() }, args.Context); } private static void DispatchRequest(AutoArchiveCommandRequest request, Terminal? context) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown Terminal context2 = context; if (ZNet.instance.IsServer()) { StartRequest(request, delegate(AutoArchiveCommandResult result) { ShowResult(result, context2); }); return; } RegisterRpcs(); ZPackage val = new ZPackage(); val.Write(ZoneBundleSerialization.Serialize(request)); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "sighsorry.ZoneSavior_AutoArchiveCommandRequest", new object[1] { val }); Terminal obj = context2; if (obj != null) { obj.AddString(request.Command + " request sent to server."); } } private static void RPC_HandleRequest(long sender, ZPackage package) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } try { if (!IsAuthorizedSender(sender)) { SendResult(sender, AutoArchiveCommandResult.Fail("Admin only.")); return; } StartRequest(ZoneBundleSerialization.Deserialize<AutoArchiveCommandRequest>(package.ReadString()), delegate(AutoArchiveCommandResult result) { SendResult(sender, result); }); } catch (Exception ex) { _logger.LogError((object)$"Auto archive command RPC failed: {ex}"); SendResult(sender, AutoArchiveCommandResult.Fail(ex.Message)); } } private static void SendResult(long target, AutoArchiveCommandResult result) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { ZPackage val = new ZPackage(); val.Write(ZoneBundleSerialization.Serialize(result)); ZRoutedRpc.instance.InvokeRoutedRPC(target, "sighsorry.ZoneSavior_AutoArchiveCommandResult", new object[1] { val }); } } private static void RPC_HandleResult(long sender, ZPackage package) { if (!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) { ShowResult(ZoneBundleSerialization.Deserialize<AutoArchiveCommandResult>(package.ReadString()), (Terminal?)(object)Console.instance); } } private static void StartRequest(AutoArchiveCommandRequest request, Action<AutoArchiveCommandResult> onComplete) { if ((Object)(object)ZoneSaviorPlugin.Instance != (Object)null && TryGetRestoreTag(request, out string tag, out AutoArchiveCommandResult validationResult)) { if (validationResult != null) { onComplete(validationResult); } else { ((MonoBehaviour)ZoneSaviorPlugin.Instance).StartCoroutine(ExecuteRestoreAsync(tag, onComplete)); } } else { onComplete(ExecuteRequest(request)); } } private static bool TryGetRestoreTag(AutoArchiveCommandRequest request, out string tag, out AutoArchiveCommandResult? validationResult) { tag = ""; validationResult = null; string text = request.Command; string[] array = request.Args?.ToArray() ?? Array.Empty<string>(); if (string.IsNullOrWhiteSpace(text) && array.Length != 0) { text = array[0]; } if (!string.Equals(text, "zs_archive_restore", StringComparison.OrdinalIgnoreCase)) { return false; } if (array.Length < 2) { validationResult = AutoArchiveCommandResult.Fail("Syntax: zs_archive_restore tag"); return true; } tag = array[1]; return true; } [IteratorStateMachine(typeof(<ExecuteRestoreAsync>d__24))] private static IEnumerator ExecuteRestoreAsync(string tag, Action<AutoArchiveCommandResult> onComplete) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ExecuteRestoreAsync>d__24(0) { tag = tag, onComplete = onComplete }; } private static AutoArchiveCommandResult ExecuteRequest(AutoArchiveCommandRequest request) { List<string> messages = new List<string>(); try { string text = request.Command; string[] array = request.Args?.ToArray() ?? Array.Empty<string>(); if (string.IsNullOrWhiteSpace(text) && array.Length != 0) { text = array[0]; } switch (text) { case "zs_archive_scan": ExecuteScan(array, messages); break; case "zs_archive_status": ExecuteStatus(messages); break; case "zs_archive_player": ExecutePlayer(array, messages); break; case "zs_archive_list": ExecuteList(messages); break; case "zs_archive_mark_seen": ExecuteMarkSeen(array, messages); break; case "zs_archive_ignore_player": ExecuteIgnorePlayer(array, messages); break; case "zs_archive_restore": ExecuteRestore(array, messages); break; case "zs_archive_schedule": ExecuteSchedule(array, messages); break; case "zs_archive_debug_zone": ExecuteDebugZone(array, messages); break; default: throw new InvalidOperationException("Unsupported auto archive command '" + text + "'."); } return AutoArchiveCommandResult.Ok(messages); } catch (Exception ex) { _logger.LogError((object)$"Auto archive command '{request.Command}' failed: {ex}"); return AutoArchiveCommandResult.Fail(ex.Message); } } private static void ExecuteScan(string[] args, List<string> messages) { ParseMode(args.Skip(1), out var dryRun, out var reset); if (!AutoArchiveService.QueueManualScan(dryRun, reset)) { messages.Add("Auto archive scan could not be started. World may not be ready or another scan is running."); } else { messages.Add("Auto archive scan started."); } } private static void ExecuteStatus(List<string> messages) { AutoArchiveState state = AutoArchiveStore.State; int num = state.Players.Sum((PlayerActivityRecord player) => player.PlayerIds.Count); string arg = (AutoArchiveConfig.Enabled ? $"enabled, interval: {AutoArchiveConfig.ScanIntervalMinutes} minute(s), next auto scan: {FormatDate(GetNextAutoScanUtc(state.LastAutoScanUtc))}" : "disabled (Scan Interval Minutes = 0)"); messages.Add($"Auto archive automatic scan: {arg}, dry run: {AutoArchiveConfig.DryRun}, reset after save: {AutoArchiveConfig.ResetAfterSave}."); messages.Add($"Players: {state.Players.Count} platform record(s), {num} playerID(s), ignored: {state.IgnoredPlayerIds.Count}."); messages.Add($"Runs: {state.Runs.Count}, last scan: {FormatDate(state.LastScanUtc)}, last auto scan: {FormatDate(state.LastAutoScanUtc)}."); messages.Add("Activity file: " + AutoArchiveStore.FilePath); } private static void ExecutePlayer(string[] args, List<string> messages) { if (args.Length < 2) { throw new InvalidOperationException("Syntax: zs_archive_player steamID [dry|save|reset]"); } string targetLabel; List<long> targetPlayerIds = ResolveTargetPlayerIds(args[1], out targetLabel); ParseMode(args.Skip(2), out var dryRun, out var reset); if (!AutoArchiveService.QueueManualScan(dryRun, reset, targetPlayerIds)) { messages.Add("Auto archive player scan could not be started. World may not be ready or another scan is running."); } else { messages.Add("Auto archive scan started for " + targetLabel + "."); } } private static void ExecuteList(List<string> messages) { List<ArchiveRunRecord> list = AutoArchiveStore.State.Runs.OrderByDescending((ArchiveRunRecord run) => run.CreatedUtc).Take(10).ToList(); if (list.Count == 0) { messages.Add("No auto archive runs recorded."); return; } foreach (ArchiveRunRecord item in list) { messages.Add($"{item.RunId}: dry={item.DryRun}, reset={item.ResetAfterSave}, candidates={item.CandidateZones}, processed={item.ProcessedZones}, clusters={item.Clusters.Count}"); foreach (ArchiveClusterRecord item2 in item.Clusters.Take(5)) { string text = (string.IsNullOrWhiteSpace(item2.Tag) ? "-" : item2.Tag); messages.Add($" {item2.Status} {text}: zones={item2.Zones.Count}, pieces={item2.PieceCount}, reason={item2.Reason}"); } } } private static void ExecuteMarkSeen(string[] args, List<string> messages) { if (args.Length < 2 || !long.TryParse(args[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { throw new InvalidOperationException("Syntax: zs_archive_mark_seen playerID"); } AutoArchiveStore.RecordManualPlayerSeen(result, DateTime.UtcNow); AutoArchiveStore.Flush(force: true); messages.Add($"Marked playerID {result} as seen now."); } private static void ExecuteIgnorePlayer(string[] args, List<string> messages) { if (args.Length < 2 || !long.TryParse(args[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { throw new InvalidOperationException("Syntax: zs_archive_ignore_player playerID [on|off]"); } bool flag = args.Length < 3 || !string.Equals(args[2], "off", StringComparison.OrdinalIgnoreCase); AutoArchiveStore.SetIgnored(result, flag); AutoArchiveStore.Flush(force: true); messages.Add(flag ? $"playerID {result} is now protected from auto archive." : $"playerID {result} is no longer ignored."); } private static void ExecuteRestore(string[] args, List<string> messages) { if (args.Length < 2) { throw new InvalidOperationException("Syntax: zs_archive_restore tag"); } ZoneBundleCommandResult zoneBundleCommandResult = ZoneBundleCommands.RestoreTagToOriginalZones(args[1]); messages.Add(zoneBundleCommandResult.Message); } private static void ExecuteSchedule(string[] args, List<string> messages) { if (args.Length < 2 || string.Equals(args[1], "status", StringComparison.OrdinalIgnoreCase)) { messages.Add(BuildScheduleMessage()); return; } string a = args[1]; if (string.Equals(a, "now", StringComparison.OrdinalIgnoreCase)) { AutoArchiveStore.SetLastAutoScanUtc(DateTime.UtcNow); AutoArchiveStore.Flush(force: true); messages.Add(BuildScheduleMessage()); return; } if (string.Equals(a, "clear", StringComparison.OrdinalIgnoreCase)) { AutoArchiveStore.SetLastAutoScanUtc(DateTime.MinValue); AutoArchiveStore.Flush(force: true); messages.Add(BuildScheduleMessage()); return; } if (args.Length < 3) { throw new InvalidOperationException("Syntax: zs_archive_schedule [status|now|clear|last yyyy-MM-dd HH:mm|next yyyy-MM-dd HH:mm]"); } DateTime dateTime = ParseScheduleDate(args.Skip(2)); if (string.Equals(a, "last", StringComparison.OrdinalIgnoreCase)) { AutoArchiveStore.SetLastAutoScanUtc(dateTime); AutoArchiveStore.Flush(force: true); messages.Add(BuildScheduleMessage()); return; } if (string.Equals(a, "next", StringComparison.OrdinalIgnoreCase)) { if (!AutoArchiveConfig.Enabled) { throw new InvalidOperationException("Automatic auto archive scans are disabled because Scan Interval Minutes is 0."); } AutoArchiveStore.SetLastAutoScanUtc(dateTime - TimeSpan.FromMinutes(AutoArchiveConfig.ScanIntervalMinutes)); AutoArchiveStore.Flush(force: true); messages.Add(BuildScheduleMessage()); return; } throw new InvalidOperationException("Syntax: zs_archive_schedule [status|now|clear|last yyyy-MM-dd HH:mm|next yyyy-MM-dd HH:mm]"); } private static void ExecuteDebugZone(string[] args, List<string> messages) { //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) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0029: 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) Vector2i val = ParseZoneSpec(args.Skip(1)); AutoArchiveZoneDebugReport autoArchiveZoneDebugReport = BuildZoneDebugReport(val); string text = WriteZoneDebugReport(autoArchiveZoneDebugReport); messages.Add($"Archive debug zone ({val.x},{val.y}): zdo={autoArchiveZoneDebugReport.Summary.TotalZdos}, candidatePieces={autoArchiveZoneDebugReport.Summary.AutoArchiveCandidatePieces}, creators={autoArchiveZoneDebugReport.Summary.CandidateCreators}, wouldCandidate={autoArchiveZoneDebugReport.Summary.WouldBeCandidateZone}."); messages.Add("Reason: " + autoArchiveZoneDebugReport.Summary.Reason); messages.Add("Wrote YAML: " + text); } private static string BuildScheduleMessage() { DateTime lastAutoScanUtc = AutoArchiveStore.State.LastAutoScanUtc; if (!AutoArchiveConfig.Enabled) { return "Auto archive schedule: disabled (Scan Interval Minutes = 0), last auto scan=" + FormatDate(lastAutoScanUtc) + "."; } return $"Auto archive schedule: interval={AutoArchiveConfig.ScanIntervalMinutes} minute(s), last auto scan={FormatDate(lastAutoScanUtc)}, next auto scan={FormatDate(GetNextAutoScanUtc(lastAutoScanUtc))}."; } private static AutoArchiveZoneDebugReport BuildZoneDebugReport(Vector2i zone) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) if (ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null || (Object)(object)ZoneSystem.instance == (Object)null) { throw new InvalidOperationException("World ZDO systems are not ready."); } List<ZDO> list = new List<ZDO>(); ZDOMan.instance.FindObjects(zone, list); DateTime utcNow = DateTime.UtcNow; AutoArchiveZoneDebugReport autoArchiveZoneDebugReport = new AutoArchiveZoneDebugReport(); ZNet instance = ZNet.instance; autoArchiveZoneDebugReport.World = ((instance != null) ? instance.GetWorldName() : null) ?? "unknown"; autoArchiveZoneDebugReport.CreatedAt = ZoneSaviorTimestamp.Format(utcNow); autoArchiveZoneDebugReport.Zone = new ZoneBundleZone { X = zone.x, Z = zone.y }; autoArchiveZoneDebugReport.Settings = new AutoArchiveZoneDebugSettings { DryRun = AutoArchiveConfig.DryRun, ResetAfterSave = AutoArchiveConfig.ResetAfterSave, InactiveDays = AutoArchiveConfig.InactiveDays, UnknownOwnerGraceDays = AutoArchiveConfig.UnknownOwnerGraceDays, MinimumPiecesPerCluster = AutoArchiveConfig.MinimumPiecesPerCluster, SmallClusterAction = AutoArchiveConfig.SmallClusterAction.ToString(), MaxZonesPerRun = AutoArchiveConfig.MaxZonesPerRun }; AutoArchiveZoneDebugReport autoArchiveZoneDebugReport2 = autoArchiveZoneDebugReport; HashSet<ZDOID> hashSet = new HashSet<ZDOID>(); foreach (ZDO item in list) { if (item != null && item.IsValid() && hashSet.Add(item.m_uid)) { AutoArchiveZoneDebugObject autoArchiveZoneDebugObject = BuildZoneDebugObject(zone, item); autoArchiveZoneDebugReport2.Objects.Add(autoArchiveZoneDebugObject); AddExclusionCounts(autoArchiveZoneDebugReport2.ExclusionCounts, autoArchiveZoneDebugObject.ExclusionReasons); } } List<long> list2 = (from playerId in (from entry in autoArchiveZoneDebugReport2.Objects where entry.AutoArchiveCandidatePiece select entry.CreatorPlayerId into playerId where playerId != 0 select playerId).Distinct() orderby playerId select playerId).ToList(); autoArchiveZoneDebugReport2.Creators = list2.Select((long playerId) => BuildCreatorDebug(playerId, utcNow)).ToList(); bool flag = autoArchiveZoneDebugReport2.Creators.Count > 0 && autoArchiveZoneDebugReport2.Creators.All((AutoArchiveZoneDebugCreator creator) => creator.Eligible); autoArchiveZoneDebugReport2.Summary = new AutoArchiveZoneDebugSummary { TotalZdos = autoArchiveZoneDebugReport2.Objects.Count, InRequestedZone = autoArchiveZoneDebugReport2.Objects.Count((AutoArchiveZoneDebugObject entry) => entry.InRequestedZone), WearNTear = autoArchiveZoneDebugReport2.Objects.Count((AutoArchiveZoneDebugObject entry) => entry.HasWearNTear), PlayerBuildRecipe = autoArchiveZoneDebugReport2.Objects.Count((AutoArchiveZoneDebugObject entry) => entry.HasBuildRecipe), AutoArchiveCandidatePieces = autoArchiveZoneDebugReport2.Objects.Count((AutoArchiveZoneDebugObject entry) => entry.AutoArchiveCandidatePiece), CandidateCreators = list2.Count, ObjectDbReady = ((Object)(object)ObjectDB.instance != (Object)null), WouldBeCandidateZone = (autoArchiveZoneDebugReport2.Objects.Any((AutoArchiveZoneDebugObject entry) => entry.AutoArchiveCandidatePiece) && flag) }; autoArchiveZoneDebugReport2.Summary.Reason = BuildDebugSummaryReason(autoArchiveZoneDebugReport2, flag); return autoArchiveZoneDebugReport2; } private static AutoArchiveZoneDebugObject BuildZoneDebugObject(Vector2i requestedZone, ZDO zdo) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //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_000d: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c7: Unknown result type (might be due to invalid IL or missing references) Vector3 position = zdo.GetPosition(); Vector2i zone = ZoneSystem.GetZone(position); GameObject prefab = ZNetScene.instance.GetPrefab(zdo.GetPrefab()); bool flag = (Object)(object)prefab != (Object)null && Object.op_Implicit((Object)(object)prefab); bool flag2 = flag && (Object)(object)prefab.GetComponent<ZNetView>() != (Object)null; bool flag3 = flag && (Object)(object)prefab.GetComponent<WearNTear>() != (Object)null; bool hasPiece = flag && (Object)(object)prefab.GetComponent<Piece>() != (Object)null; bool flag4 = flag && ZoneBlueprintCommands.HasBuildRecipe(prefab); bool flag5 = zone.x == requestedZone.x && zone.y == requestedZone.y; long @long = zdo.GetLong(ZDOVars.s_creator, 0L); List<string> list = new List<string>(); if (!flag5) { list.Add("outside_requested_zone"); } if (!flag) { list.Add("prefab_missing"); } if (flag && !flag2) { list.Add("no_znetview"); } if (!flag3) { list.Add("not_wearntear"); } if (@long == 0L) { list.Add("creatorless_or_missing_creator"); } if (flag3 && !flag4) { list.Add("no_player_build_recipe_or_resource_cost"); } bool flag6 = flag5 && @long != 0 && flag3 && flag4; if (flag6) { list.Clear(); } AutoArchiveZoneDebugObject autoArchiveZoneDebugObject = new AutoArchiveZoneDebugObject(); autoArchiveZoneDebugObject.ZdoId = ((object)(ZDOID)(ref zdo.m_uid)).ToString(); autoArchiveZoneDebugObject.PrefabHash = zdo.GetPrefab(); autoArchiveZoneDebugObject.Prefab = (flag ? Utils.GetPrefabName(prefab) : ""); autoArchiveZoneDebugObject.Position = new float[3] { Round(position.x), Round(position.y), Round(position.z) }; autoArchiveZoneDebugObject.ObjectZone = new ZoneBundleZone { X = zone.x, Z = zone.y }; autoArchiveZoneDebugObject.CreatorPlayerId = @long; autoArchiveZoneDebugObject.CreatorName = zdo.GetString(ZDOVars.s_creatorName, ""); autoArchiveZoneDebugObject.HasPrefab = flag; autoArchiveZoneDebugObject.HasZNetView = flag2; autoArchiveZoneDebugObject.HasWearNTear = flag3; autoArchiveZoneDebugObject.HasPiece = hasPiece; autoArchiveZoneDebugObject.HasBuildRecipe = flag4; autoArchiveZoneDebugObject.InRequestedZone = flag5; autoArchiveZoneDebugObject.AutoArchiveCandidatePiece = flag6; autoArchiveZoneDebugObject.ExclusionReasons = list; return autoArchiveZoneDebugObject; } private static AutoArchiveZoneDebugCreator BuildCreatorDebug(long playerId, DateTime utcNow) { AutoArchiveCreatorEligibility autoArchiveCreatorEligibility = AutoArchiveStore.EvaluateCreatorArchiveEligibility(playerId, utcNow, AutoArchiveConfig.InactiveDays, AutoArchiveConfig.UnknownOwnerGraceDays, recordUnknownPlayer: false); return new AutoArchiveZoneDebugCreator { PlayerId = autoArchiveCreatorEligibility.PlayerId, PlatformId = autoArchiveCreatorEligibility.PlatformId, Names = autoArchiveCreatorEligibility.Names, RecordedInActivity = autoArchiveCreatorEligibility.RecordedInActivity, UnknownActivityRecord = autoArchiveCreatorEligibility.UnknownActivityRecord, Ignored = autoArchiveCreatorEligibility.Ignored, Protected = autoArchiveCreatorEligibility.Protected, Eligible = autoArchiveCreatorEligibility.Eligible, Reason = autoArchiveCreatorEligibility.Reason }; } private static string BuildDebugSummaryReason(AutoArchiveZoneDebugReport report, bool allCreatorsEligible) { if (report.Summary.AutoArchiveCandidatePieces == 0) { return "No WearNTear with a non-zero creator and a registered player build recipe was found in this zone."; } if (!allCreatorsEligible) { string text = string.Join("; ", from creator in report.Creators where !creator.Eligible select creator.Reason); return "At least one candidate creator is not archive-eligible: " + text; } if (report.Summary.AutoArchiveCandidatePieces < AutoArchiveConfig.MinimumPiecesPerCluster) { return $"Zone has candidate pieces, but this single-zone piece count is below Minimum Pieces Per Cluster ({report.Summary.AutoArchiveCandidatePieces}/{AutoArchiveConfig.MinimumPiecesPerCluster}). Cluster adjacency may still change the final action."; } return "This zone would be an auto archive candidate before cluster adjacency and max-zones-per-run checks."; } private static string WriteZoneDebugReport(AutoArchiveZoneDebugReport report) { string text = Path.Combine(ZoneSaviorPlugin.DataStorageFullPath, "Diagnostics"); Directory.CreateDirectory(text); string path = string.Format("archive_debug_zone_{0}_{1}_{2}.yml", report.Zone.X, report.Zone.Z, DateTime.UtcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture)); string text2 = Path.Combine(text, path); File.WriteAllText(text2, ZoneBundleSerialization.Serialize(report)); return text2; } private static Vector2i ParseZoneSpec(IEnumerable<string> values) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) string input = string.Join(" ", values).Trim(); Match match = ZoneSpecPattern.Match(input); if (!match.Success || !int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { throw new InvalidOperationException("Syntax: zs_archive_debug_zone (x,z)"); } return new Vector2i(result, result2); } private static void AddExclusionCounts(Dictionary<string, int> counts, IEnumerable<string> reasons) { foreach (string reason in reasons) { counts.TryGetValue(reason, out var value); counts[reason] = value + 1; } } private static float Round(float value) { return Mathf.Round(value * 1000f) / 1000f; } private static void ParseMode(IEnumerable<string> args, out bool? dryRun, out bool? reset) { dryRun = null; reset = null; foreach (string arg in args) { if (string.Equals(arg, "dry", StringComparison.OrdinalIgnoreCase) || string.Equals(arg, "dry-run", StringComparison.OrdinalIgnoreCase)) { dryRun = true; reset = false; continue; } if (string.Equals(arg, "save", StringComparison.OrdinalIgnoreCase)) { dryRun = false; reset = false; continue; } if (string.Equals(arg, "reset", StringComparison.OrdinalIgnoreCase)) { dryRun = false; reset = true; continue; } throw new InvalidOperationException("Unknown archive mode '" + arg + "'. Use dry, save, or reset."); } } private static List<long> ResolveTargetPlayerIds(string target, out string targetLabel) { if (AutoArchiveStore.TryGetPlayerIdsBySteamId(target, out List<long> playerIds, out string normalizedSteamId)) { targetLabel = "steamID " + normalizedSteamId + " (playerID " + string.Join(", ", playerIds) + ")"; return playerIds; } if (IsLikelySteamId(target)) { throw new InvalidOperationException("No known playerID is linked to SteamID " + target + ". The player must have joined while ZoneSavior activity tracking was active."); } throw new InvalidOperationException("Syntax: zs_archive_player steamID [dry|save|reset]"); } private static bool IsLikelySteamId(string target) { if (string.IsNullOrWhiteSpace(target)) { return false; } string text = target.Trim(); if (text.StartsWith("steam:", StringComparison.OrdinalIgnoreCase)) { text = text.Substring("steam:".Length); } return new string(text.Where(char.IsDigit).ToArray()).Length >= 15; } private static void EnsureCommandReady() { if ((Object)(object)ZNet.instance == (Object)null) { throw new InvalidOperationException("World is not ready."); } if (!ZNet.instance.IsServer() && ZRoutedRpc.instance == null) { throw new InvalidOperationException("Server RPC is not ready."); } if (!ZNet.instance.IsServer() || (ZNet.instance.IsServer() && (Object)(object)Player.m_localPlayer == (Object)null) || ZNet.instance.LocalPlayerIsAdminOrHost()) { return; } throw new InvalidOperationException("Admin only."); } private static bool IsAuthorizedSender(long sender) { ZNetPeer peer = ZNet.instance.GetPeer(sender); object obj; if (peer == null) { obj = null; } else { ZRpc rpc = peer.m_rpc; if (rpc == null) { obj = null; } else { ISocket socket = rpc.m_socket; obj = ((socket != null) ? socket.GetHostName() : null); } } if (obj == null) { obj = ""; } string text = (string)obj; if (text.Length > 0) { return ZNet.instance.IsAdmin(text); } return false; } private static void ShowResult(AutoArchiveCommandResult result, Terminal? terminal) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) MessageType val = (MessageType)(result.Success ? 1 : 2); foreach (string item in (result.Messages.Count > 0) ? result.Messages : new List<string>(1) { result.Success ? "Done." : "Command failed." }) { _logger.LogInfo((object)item); if (terminal != null) { terminal.AddString(item); } if ((Object)(object)Player.m_localPlayer != (Object)null) { ((Character)Player.m_localPlayer).Message(val, item, 0, (Sprite)null); } } } private static string FormatDate(DateTime value) { if (!(value == DateTime.MinValue)) { return ZoneSaviorTimestamp.Format(value); } return "-"; } private static DateTime GetNextAutoScanUtc(DateTime lastAutoScanUtc) { if (AutoArchiveConfig.Enabled && !(lastAutoScanUtc == DateTime.MinValue)) { return DateTime.SpecifyKind(lastAutoScanUtc, DateTimeKind.Utc) + TimeSpan.FromMinutes(AutoArchiveConfig.ScanIntervalMinutes); } return DateTime.MinValue; } private static DateTime ParseScheduleDate(IEnumerable<string> values) { string text = string.Join(" ", values).Trim(); if (string.IsNullOrWhiteSpace(text)) { throw new InvalidOperationException("Missing schedule date."); } DateTimeStyles styles = ((text.EndsWith("Z", StringComparison.OrdinalIgnoreCase) || HasTimezoneOffset(text)) ? (DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal) : (DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal)); if (!DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out var result) && !DateTime.TryParse(text, CultureInfo.CurrentCulture, styles, out result)) { throw new InvalidOperationException("Invalid schedule date '" + text + "'. Use local server time like 2026-05-02 15:00, or UTC like 2026-05-02T06:00:00Z."); } return DateTime.SpecifyKind(result, DateTimeKind.Utc); } private static bool HasTimezoneOffset(string value) { string text = value.Trim(); if (text.Length < 6) { return false; } int num = text.Length - 6; char c = text[num]; if ((c == '+' || c == '-') && char.IsDigit(text[num + 1]) && char.IsDigit(text[num + 2]) && text[num + 3] == ':' && char.IsDigit(text[num + 4])) { return char.IsDigit(text[num + 5]); } return false; } } internal sealed class AutoArchiveCommandRequest { public string Command { get; set; } = ""; public List<string> Args { get; set; } = new List<string>(); } internal sealed class AutoArchiveCommandResult { public bool Success { get; set; } public List<string> Messages { get; set; } = new List<string>(); public static AutoArchiveCommandResult Ok(IEnumerable<string> messages) { return new AutoArchiveCommandResult { Success = true, Messages = messages.ToList() }; } public static AutoArchiveCommandResult Fail(string message) { return new AutoArchiveCommandResult { Success = false, Messages = new List<string>(1) { message } }; } } internal enum AutoArchiveSmallClusterAction { Skip, SaveAnyway, ResetWithoutSave } internal sealed class AutoArchiveState { public int Version { get; set; } = 1; [YamlIgnore] public DateTime LastScanUtc { get; set; } = DateTime.MinValue; public string LastScanAt { get { return ZoneSaviorTimestamp.Format(LastScanUtc); } set { LastScanUtc = ZoneSaviorTimestamp.ParseUtc(value); } } [YamlIgnore] public DateTime LastAutoScanUtc { get; set; } = DateTime.MinValue; public string LastAutoScanAt { get { return ZoneSaviorTimestamp.Format(LastAutoScanUtc); } set { LastAutoScanUtc = ZoneSaviorTimestamp.ParseUtc(value); } } public List<PlayerActivityRecord> Players { get; set; } = new List<PlayerActivityRecord>(); public List<ArchiveRunRecord> Runs { get; set; } = new List<ArchiveRunRecord>(); public List<long> IgnoredPlayerIds { get; set; } = new List<long>(); } internal sealed class PlayerActivityRecord { public string PlatformId { get; set; } = ""; public List<long> PlayerIds { get; set; } = new List<long>(); public List<string> Names { get; set; } = new List<string>(); [YamlIgnore] public DateTime FirstSeenUtc { get; set; } = DateTime.MinValue; public string FirstSeenAt { get { return ZoneSaviorTimestamp.Format(FirstSeenUtc); } set { FirstSeenUtc = ZoneSaviorTimestamp.ParseUtc(value); } } [YamlIgnore] public DateTime LastSeenUtc { get; set; } = DateTime.MinValue; public string LastSeenAt { get { return ZoneSaviorTimestamp.Format(LastSeenUtc); } set { LastSeenUtc = ZoneSaviorTimestamp.ParseUtc(value); } } } internal sealed class ArchiveRunRecord { public string RunId { get; set; } = ""; [YamlIgnore] public DateTime CreatedUtc { get; set; } = DateTime.MinValue; public string CreatedAt { get { return ZoneSaviorTimestamp.Format(CreatedUtc); } set { CreatedUtc = ZoneSaviorTimestamp.ParseUtc(value); } } public bool Manual { get; set; } public bool DryRun { get; set; } public bool ResetAfterSave { get; set; } public List<long> TargetPlayerIds { get; set; } = new List<long>(); public int ScannedZdos { get; set; } public int StructureZdos { get; set; } public int CandidateZones { get; set; } public int ProcessedZones { get; set; } public List<ArchiveClusterRecord> Clusters { get; set; } = new List<ArchiveClusterRecord>(); public List<string> Messages { get; set; } = new List<string>(); } internal sealed class ArchiveClusterRecord { public string Tag { get; set; } = ""; public string Status { get; set; } = ""; public string Reason { get; set; } = ""; public int PieceCount { get; set; } public int TerrainLoaded { get; set; } public int TerrainCaptured { get; set; } public List<long> Creators { get; set; } = new List<long>(); public List<ZoneBundleZone> Zones { get; set; } = new List<ZoneBundleZone>(); } internal sealed class AutoArchiveScanOptions { public bool Manual { get; set; } public bool DryRun { get; set; } public bool ResetAfterSave { get; set; } public List<long> TargetPlayerIds { get; set; } = new List<long>(); } internal sealed class AutoArchiveCreatorEligibility { public long PlayerId { get; set; } public string PlatformId { get; set; } = ""; public List<string> Names { get; set; } = new List<string>(); public bool RecordedInActivity { get; set; } public bool UnknownActivityRecord { get; set; } public bool Ignored { get; set; } public bool Protected { get; set; } public bool Eligible { get; set; } public string Reason { get; set; } = ""; } internal sealed class ZoneBundleArchiveResult { public bool Success { get; set; } public string Message { get; set; } = ""; public string Tag { get; set; } = ""; public string ManifestPath { get; set; } = ""; public int ZoneCount { get; set; } public int EntryCount { get; set; } public int MonsterCount { get; set; } public int TerrainLoaded { get; set; } public int TerrainCaptured { get; set; } } internal sealed class ZoneBundleResetResult { public bool Success { get; set; } public string Message { get; set; } = ""; public int ZoneCount { get; set; } public int RemovedCount { get; set; } public int RemainingWearNTearCount { get; set; } } internal sealed class AutoArchiveZoneDebugReport { public int Version { get; set; } = 1; public string World { get; set; } = ""; public string CreatedAt { get; set; } = ""; public ZoneBundleZone Zone { get; set; } = new ZoneBundleZone(); public AutoArchiveZoneDebugSettings Settings { get; set; } = new AutoArchiveZoneDebugSettings(); public AutoArchiveZoneDebugSummary Summary { get; set; } = new AutoArchiveZoneDebugSummary(); public List<AutoArchiveZoneDebugCreator> Creators { get; set; } = new List<AutoArchiveZoneDebugCreator>(); public Dictionary<string, int> ExclusionCounts { get; set; } = new Dictionary<string, int>(); public List<AutoArchiveZoneDebugObject> Objects { get; set; } = new List<AutoArchiveZoneDebugObject>(); } internal sealed class AutoArchiveZoneDebugSettings { public bool DryRun { get; set; } public bool ResetAfterSave { get; set; } public int InactiveDays { get; set; } public int UnknownOwnerGraceDays { get; set; } public int MinimumPiecesPerCluster { get; set; } public string SmallClusterAction { get; set; } = ""; public int MaxZonesPerRun { get; set; } } internal sealed class AutoArchiveZoneDebugSummary { public int TotalZdos { get; set; } public int InRequestedZone { get; set; } public int WearNTear { get; set; } public int PlayerBuildRecipe { get; set; } public int AutoArchiveCandidatePieces { get; set; } public int CandidateCreators { get; set; } public bool ObjectDbReady { get; set; } public bool WouldBeCandidateZone { get; set; } public string Reason { get; set; } = ""; } internal sealed class AutoArchiveZoneDebugCreator { public long PlayerId { get; set; } public string PlatformId { get; set; } = ""; public List<string> Names { get; set; } = new List<string>(); public bool RecordedInActivity { get; set; } public bool UnknownActivityRecord { get; set; } public bool Ignored { get; set; } public bool Protected { get; set; } public bool Eligible { get; set; } public string Reason { get; set; } = ""; } internal sealed class AutoArchiveZoneDebugObject { public string ZdoId { get; set; } = ""; public int PrefabHash { get; set; } public string Prefab { get; set; } = ""; public float[] Position { get; set; } = new float[3]; public ZoneBundleZone ObjectZone { get; set; } = new ZoneBundleZone(); public long CreatorPlayerId { get; set; } public string CreatorName { get; set; } = ""; public bool HasPrefab { get; set; } public bool HasZNetView { get; set; } public bool HasWearNTear { get; set; } public bool HasPiece { get; set; } public bool HasBuildRecipe { get; set; } public bool InRequestedZone { get; set; } public bool AutoArchiveCandidatePiece { get; set; } public List<string> ExclusionReasons { get; set; } = new List<string>(); } internal static class AutoArchiveScanner { private sealed class AutoArchiveZoneInfo { public Vector2i Zone { get; } public int PieceCount { get; set; } public HashSet<long> Creators { get; } = new HashSet<long>(); public Dictionary<long, int> CreatorPieceCounts { get; } = new Dictionary<long, int>(); public AutoArchiveZoneInfo(Vector2i zone) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) Zone = zone; } public void AddCreator(long creator) { PieceCount++; Creators.Add(creator); CreatorPieceCounts.TryGetValue(creator, out var value); CreatorPieceCounts[creator] = value + 1; } } [CompilerGenerated] private sealed class <>c__DisplayClass3_0 { public ZoneBundleArchiveResult saveResult; internal void <Run>b__4(ZoneBundleArchiveResult result) { saveResult = result; } } [CompilerGenerated] private sealed class <>c__DisplayClass3_1 { public ZoneBundleResetResult resetResult; internal void <Run>b__5(ZoneBundleResetResult result) { resetResult = result; } } [CompilerGenerated] private sealed class <Run>d__3 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public AutoArchiveScanOptions options; public Action<ArchiveRunRecord> onComplete; private <>c__DisplayClass3_0 <>8__1; private <>c__DisplayClass3_1 <>8__2; private DateTime <utcNow>5__2; private ArchiveRunRecord <run>5__3; private Dictionary<Vector2i, AutoArchiveZoneInfo> <zoneInfos>5__4; private Dictionary<Vector2i, AutoArchiveZoneInfo> <candidates>5__5; private int <processedZones>5__6; private int <clusterIndex>5__7; private IEnumerator<List<Vector2i>> <>7__wrap7; private List<Vector2i> <cluster>5__9; private ArchiveClusterRecord <record>5__10; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <Run>d__3(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || (uint)(num - 2) <= 5u) { try { } finally { <>m__Finally1(); } } <>8__1 = null; <>8__2 = null; <run>5__3 = null; <zoneInfos>5__4 = null; <candidates>5__5 = null; <>7__wrap7 = null; <cluster>5__9 = null; <record>5__10 = null; <>1__state = -2; } private bool MoveNext() { try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <utcNow>5__2 = DateTime.UtcNow; <run>5__3 = new ArchiveRunRecord { RunId = <utcNow>5__2.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture), CreatedUtc = <utcNow>5__2, Manual = options.Manual, DryRun = options.DryRun, ResetAfterSave = options.ResetAfterSave, TargetPlayerIds = options.TargetPlayerIds.ToList() }; if (!IsWorldReady()) { <run>5__3.Messages.Add("World is not ready."); onComplete(<run>5__3); return false; } PlayerActivityTracker.TrackOnlinePlayers(<utcNow>5__2); <zoneInfos>5__4 = new Dictionary<Vector2i, AutoArchiveZoneInfo>(); <>2__current = ScanZdosBySector(<utcNow>5__2, <zoneInfos>5__4, <run>5__3); <>1__state = 1; return true; case 1: { <>1__state = -1; <candidates>5__5 = BuildCandidateZones(<zoneInfos>5__4, <utcNow>5__2, options.TargetPlayerIds, <run>5__3); <run>5__3.CandidateZones = <candidates>5__5.Count; List<List<Vector2i>> source = BuildClusters(<candidates>5__5); <processedZones>5__6 = 0; <clusterIndex>5__7 = 0; <>7__wrap7 = (from cluster in source orderby cluster.Min((Vector2i zone) => zone.x), cluster.Min((Vector2i zone) => zone.y) select cluster).GetEnumerator(); <>1__state = -3; break; } case 2: <>1__state = -3; break; case 3: <>1__state = -3; if (<>8__1.saveResult == null) { <record>5__10.Status = "failed"; <record>5__10.Reason = "save failed: archive coroutine did not return a result"; <run>5__3.Clusters.Add(<record>5__10); _logger.LogError((object)("Auto archive save failed for tag '" + <record>5__10.Tag + "': archive coroutine did not return a result.")); <>2__current = null; <>1__state = 4; return true; } <record>5__10.TerrainLoaded = <>8__1.saveResult.TerrainLoaded; <record>5__10.TerrainCaptured = <>8__1.saveResult.TerrainCaptured; if (!<>8__1.saveResult.Success) { <record>5__10.Status = "failed"; <record>5__10.Reason = <>8__1.saveResult.Message; <run>5__3.Clusters.Add(<record>5__10); break; } <record>5__10.Status = "saved"; <record>5__10.Reason = <>8__1.saveResult.Message; if (options.ResetAfterSave) { if (!AutoArchiveConfig.RequireLoadedTerrainForReset || <>8__1.saveResult.TerrainCaptured >= <cluster>5__9.Count) { <>8__2 = new <>c__DisplayClass3_1(); <>8__2.resetResult = null; <>2__current = ZoneBundleCommands.ResetGeneratedZonesAsync(<cluster>5__9, delegate(ZoneBundleResetResult result) { <>8__2.resetResult = result; }); <>1__state = 5; return true; } <record>5__10.Status = "saved-reset-skipped"; <record>5__10.Reason = $"saved, but reset skipped because usable terrain contacts were captured for {<>8__1.saveResult.TerrainCaptured}/{<cluster>5__9.Count} zones"; } goto IL_08c2; case 4: <>1__state = -3; break; case 5: <>1__state = -3; if (<>8__2.resetResult == null) { <record>5__10.Status = "saved-reset-failed"; <record>5__10.Reason = "reset failed: reset coroutine did not return a result"; <run>5__3.Clusters.Add(<record>5__10); _logger.LogError((object)("Auto archive reset failed for tag '" + <record>5__10.Tag + "': reset coroutine did not return a result.")); <>2__current = null; <>1__state = 6; return true; } <record>5__10.Status = (<>8__2.resetResult.Success ? "reset" : "saved-reset-failed"); <record>5__10.Reason = <>8__2.resetResult.Message; <>8__2 = null; goto IL_08c2; case 6: <>1__state = -3; break; case 7: { <>1__state = -3; <>8__1 = null; <record>5__10 = null; <cluster>5__9 = null; break; } IL_08c2: <run>5__3.Clusters.Add(<record>5__10); <processedZones>5__6 += <cluster>5__9.Count; <>2__current = null; <>1__state = 7; return true; } while (<>7__wrap7.MoveNext()) { <cluster>5__9 = <>7__wrap7.Current; <>8__1 = new <>c__DisplayClass3_0(); <clusterIndex>5__7++; <record>5__10 = BuildClusterRecord(<cluster>5__9, <candidates>5__5); bool flag = <record>5__10.PieceCount < AutoArchiveConfig.MinimumPiecesPerCluster; if (flag && AutoArchiveConfig.SmallClusterAction == AutoArchiveSmallClusterAction.Skip) { <record>5__10.Status = "skipped"; <record>5__10.Reason = $"piece count {<record>5__10.PieceCount} is below minimum {AutoArchiveConfig.MinimumPiecesPerCluster}"; <run>5__3.Clusters.Add(<record>5__10); continue; } if (<processedZones>5__6 + <cluster>5__9.Count > AutoArchiveConfig.MaxZonesPerRun) { <record>5__10.Status = "skipped"; <record>5__10.Reason = $"max zones per run would be exceeded ({<processedZones>5__6 + <cluster>5__9.Count}/{AutoArchiveConfig.MaxZonesPerRun})"; <run>5__3.Clusters.Add(<record>5__10); continue; } if (!AllCreatorsEligible(<record>5__10.Creators, <utcNow>5__2, out string reason)) { <record>5__10.Status = "skipped"; <record>5__10.Reason = reason; <run>5__3.Clusters.Add(<record>5__10); continue; } if (flag && AutoArchiveConfig.SmallClusterAction == AutoArchiveSmallClusterAction.ResetWithoutSave) { if (!options.ResetAfterSave) { <record>5__10.Status = "skipped"; <record>5__10.Reason = $"piece count {<record>5__10.PieceCount} is below minimum {AutoArchiveConfig.MinimumPiecesPerCluster}; reset mode is not enabled"; <run>5__3.Clusters.Add(<record>5__10); continue; } if (options.DryRun) { <record>5__10.Status = "dry-run-reset-without-save"; <record>5__10.Reason = $"small inactive cluster would be reset without saving ({<record>5__10.PieceCount}/{AutoArchiveConfig.MinimumPiecesPerCluster} pieces)"; <run>5__3.Clusters.Add(<record>5__10); <processedZones>5__6 += <cluster>5__9.Count; continue; } ZoneBundleResetResult zoneBundleResetResult = ZoneBundleCommands.ResetGeneratedZones(<cluster>5__9); <record>5__10.Status = (zoneBundleResetResult.Success ? "reset-without-save" : "reset-without-save-failed"); <record>5__10.Reason = zoneBundleResetResult.Message; <run>5__3.Clusters.Add(<record>5__10); <processedZones>5__6 += <cluster>5__9.Count; <>2__current = null; <>1__state = 2; return true; } <record>5__10.Tag = ZoneBundleCommands.MakeUniqueAutoArchiveTag(BuildTag(<clusterIndex>5__7, <cluster>5__9, <candidates>5__5)); if (options.DryRun) { <record>5__10.Status = "dry-run"; <record>5__10.Reason = "candidate only; dry run is enabled"; <run>5__3.Clusters.Add(<record>5__10); <processedZones>5__6 += <cluster>5__9.Count; continue; } <>8__1.saveResult = null; <>2__current = ZoneBundleCommands.SaveZonesAsync(<cluster>5__9, <record>5__10.Tag, delegate(ZoneBundleArchiveResult result) { <>8__1.saveResult = result; }); <>1__state = 3; return true; } <>m__Finally1(); <>7__wrap7 = null; <run>5__3.ProcessedZones = <processedZones>5__6; onComplete(<run>5__3); return false; } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<>7__wrap7 != null) { <>7__wrap7.Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } [CompilerGenerated] private sealed class <ScanZdosBySector>d__5 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public DateTime utcNow; public Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos; public ArchiveRunRecord run; private int <batchSize>5__2; private int <processedSinceYield>5__3; private List<ZDO>[] <sectors>5__4; private int <sectorIndex>5__5; private List<ZDO> <sector>5__6; private int <objectIndex>5__7; private List<List<ZDO>>.Enumerator <>7__wrap7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <ScanZdosBySector>d__5(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 2) { try { } finally { <>m__Finally1(); } } <sectors>5__4 = null; <sector>5__6 = null; <>7__wrap7 = default(List<List<ZDO>>.Enumerator); <>1__state = -2; } private bool MoveNext() { try { List<List<ZDO>> list; switch (<>1__state) { default: return false; case 0: <>1__state = -1; <batchSize>5__2 = Math.Max(1, AutoArchiveConfig.ScannerBatchSize); <processedSinceYield>5__3 = 0; <sectors>5__4 = ZDOMan.instance.m_objectsBySector; <sectorIndex>5__5 = 0; goto IL_0138; case 1: <>1__state = -1; goto IL_00fb; case 2: { <>1__state = -3; goto IL_022d; } IL_0138: if (<sectors>5__4 != null && <sectorIndex>5__5 < <sectors>5__4.Length) { <sector>5__6 = <sectors>5__4[<sectorIndex>5__5]; if (<sector>5__6 != null && <sector>5__6.Count != 0) { <objectIndex>5__7 = 0; goto IL_010b; } goto IL_0128; } list = ZDOMan.instance.m_objectsByOutsideSector?.Values.ToList() ?? new List<List<ZDO>>(); <>7__wrap7 = list.GetEnumerator(); <>1__state = -3; goto IL_025a; IL_00fb: <objectIndex>5__7++; goto IL_010b; IL_0128: <sectorIndex>5__5++; goto IL_0138; IL_025a: do { if (<>7__wrap7.MoveNext()) { <sector>5__6 = <>7__wrap7.Current; continue; } <>m__Finally1(); <>7__wrap7 = default(List<List<ZDO>>.Enumerator); return false; } while (<sector>5__6 == null || <sector>5__6.Count == 0); <sectorIndex>5__5 = 0; goto IL_023d; IL_010b: if (<objectIndex>5__7 < <sector>5__6.Count) { ProcessScanZdo(<sector>5__6[<objectIndex>5__7], utcNow, zoneInfos, run); <processedSinceYield>5__3++; if (<processedSinceYield>5__3 >= <batchSize>5__2) { <processedSinceYield>5__3 = 0; <>2__current = null; <>1__state = 1; return true; } goto IL_00fb; } <sector>5__6 = null; goto IL_0128; IL_022d: <sectorIndex>5__5++; goto IL_023d; IL_023d: if (<sectorIndex>5__5 < <sector>5__6.Count) { ProcessScanZdo(<sector>5__6[<sectorIndex>5__5], utcNow, zoneInfos, run); <processedSinceYield>5__3++; if (<processedSinceYield>5__3 >= <batchSize>5__2) { <processedSinceYield>5__3 = 0; <>2__current = null; <>1__state = 2; return true; } goto IL_022d; } <sector>5__6 = null; goto IL_025a; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; ((IDisposable)<>7__wrap7).Dispose(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static readonly Vector2i[] NeighborOffsets = (Vector2i[])(object)new Vector2i[8] { new Vector2i(-1, -1), new Vector2i(0, -1), new Vector2i(1, -1), new Vector2i(-1, 0), new Vector2i(1, 0), new Vector2i(-1, 1), new Vector2i(0, 1), new Vector2i(1, 1) }; private static ManualLogSource _logger = null; public static void Initialize(ManualLogSource logger) { _logger = logger; } [IteratorStateMachine(typeof(<Run>d__3))] public static IEnumerator Run(AutoArchiveScanOptions options, Action<ArchiveRunRecord> onComplete) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <Run>d__3(0) { options = options, onComplete = onComplete }; } private static bool TryReadPlayerStructure(ZDO zdo, DateTime utcNow, out Vector2i zone, out long creator) { //IL_0001: 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) //IL_0088: 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) zone = default(Vector2i); creator = 0L; if (zdo == null || !zdo.IsValid()) { return false; } creator = zdo.GetLong(ZDOVars.s_creator, 0L); if (creator == 0L) { return false; } GameObject prefab = ZNetScene.instance.GetPrefab(zdo.GetPrefab()); if ((Object)(object)prefab == (Object)null || !Object.op_Implicit((Object)(object)prefab) || (Object)(object)prefab.GetComponent<WearNTear>() == (Object)null || !ZoneBlueprintCommands.HasBuildRecipe(prefab)) { return false; } string @string = zdo.GetString(ZDOVars.s_creatorName, ""); AutoArchiveStore.RecordUnknownPlayer(creator, utcNow, @string); zone = ZoneSystem.GetZone(zdo.GetPosition()); return true; } [IteratorStateMachine(typeof(<ScanZdosBySector>d__5))] private static IEnumerator ScanZdosBySector(DateTime utcNow, Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, ArchiveRunRecord run) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <ScanZdosBySector>d__5(0) { utcNow = utcNow, zoneInfos = zoneInfos, run = run }; } private static void ProcessScanZdo(ZDO zdo, DateTime utcNow, Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, ArchiveRunRecord run) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: 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) run.ScannedZdos++; if (TryReadPlayerStructure(zdo, utcNow, out var zone, out var creator)) { run.StructureZdos++; if (!zoneInfos.TryGetValue(zone, out AutoArchiveZoneInfo value)) { value = (zoneInfos[zone] = new AutoArchiveZoneInfo(zone)); } value.AddCreator(creator); } } private static Dictionary<Vector2i, AutoArchiveZoneInfo> BuildCandidateZones(Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, DateTime utcNow, IReadOnlyCollection<long> targetPlayerIds, ArchiveRunRecord run) { //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) Dictionary<Vector2i, AutoArchiveZoneInfo> dictionary = new Dictionary<Vector2i, AutoArchiveZoneInfo>(); foreach (AutoArchiveZoneInfo value in zoneInfos.Values) { if (targetPlayerIds.Count <= 0 || value.Creators.Any(((IEnumerable<long>)targetPlayerIds).Contains<long>)) { if (!AllCreatorsEligible(value.Creators, utcNow, out string reason)) { run.Messages.Add($"Skipped zone ({value.Zone.x},{value.Zone.y}): {reason}"); } else { dictionary[value.Zone] = value; } } } return dictionary; } private static List<List<Vector2i>> BuildClusters(Dictionary<Vector2i, AutoArchiveZoneInfo> candidates) { //IL_0018: 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_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: 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_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0076: 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_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) List<List<Vector2i>> list = new List<List<Vector2i>>(); HashSet<Vector2i> hashSet = candidates.Keys.ToHashSet(); Vector2i item2 = default(Vector2i); while (hashSet.Count > 0) { Vector2i item = hashSet.First(); hashSet.Remove(item); List<Vector2i> list2 = new List<Vector2i>(); Queue<Vector2i> queue = new Queue<Vector2i>(); queue.Enqueue(item); while (queue.Count > 0) { Vector2i val = queue.Dequeue(); list2.Add(val); Vector2i[] neighborOffsets = NeighborOffsets; foreach (Vector2i val2 in neighborOffsets) { ((Vector2i)(ref item2))..ctor(val.x + val2.x, val.y + val2.y); if (hashSet.Contains(item2)) { hashSet.Remove(item2); queue.Enqueue(item2); } } } list.Add(list2); } return list; } private static ArchiveClusterRecord BuildClusterRecord(List<Vector2i> cluster, Dictionary<Vector2i, AutoArchiveZoneInfo> candidates) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) ArchiveClusterRecord archiveClusterRecord = new ArchiveClusterRecord { Zones = (from zone in cluster orderby zone.y, zone.x select new ZoneBundleZone { X = zone.x, Z = zone.y }).ToList() }; HashSet<long> hashSet = new HashSet<long>(); foreach (Vector2i item in cluster) { AutoArchiveZoneInfo autoArchiveZoneInfo = candidates[item]; archiveClusterRecord.PieceCount += autoArchiveZoneInfo.PieceCount; foreach (long creator in autoArchiveZoneInfo.Creators) { hashSet.Add(creator); } } archiveClusterRecord.Creators = hashSet.OrderBy((long id) => id).ToList(); return archiveClusterRecord; } private static bool AllCreatorsEligible(IEnumerable<long> creators, DateTime utcNow, out string reason) { foreach (long creator in creators) { if (!AutoArchiveStore.IsCreatorArchiveEligible(creator, utcNow, AutoArchiveConfig.InactiveDays, AutoArchiveConfig.UnknownOwnerGraceDays, out reason)) { return false; } } reason = ""; return true; } private static string BuildTag(int index, List<Vector2i> cluster, Dictionary<Vector2i, AutoArchiveZoneInfo> candidates) { string arg = BuildOwnerSegment(BuildCreatorPieceCounts(cluster, candidates)); return $"auto_{arg}_c{index:D3}"; } private static Dictionary<long, int> BuildCreatorPieceCounts(List<Vector2i> cluster, Dictionary<Vector2i, AutoArchiveZoneInfo> candidates) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) Dictionary<long, int> dictionary = new Dictionary<long, int>(); foreach (Vector2i item in cluster) { foreach (KeyValuePair<long, int> creatorPieceCount in candidates[item].CreatorPieceCounts) { dictionary.TryGetValue(creatorPieceCount.Key, out var value); dictionary[creatorPieceCount.Key] = value + creatorPieceCount.Value; } } return dictionary; } private static string BuildOwnerSegment(IReadOnlyDictionary<long, int> creatorPieceCounts) { IReadOnlyDictionary<long, int> creatorPieceCounts2 = creatorPieceCounts; List<long> list = (from creator in creatorPieceCounts2.Keys.Where((long creator) => creator != 0).Distinct() orderby creator select creator).ToList(); if (list.Count == 0) { return "unknown"; } int value; string text = BuildOwnerToken((from owner in list orderby creatorPieceCounts2.TryGetValue(owner, out value) ? value : 0 descending, owner select owner).First()); if (list.Count == 1) { return text; } return $"{text}_plus{list.Count - 1}_{BuildOwnerHash(list)}"; } private static string BuildOwnerToken(long playerId) { string text = ResolvePlayerName(playerId); string text2 = ResolveSteamId(playerId); if (!string.Equals(text2, "unknown", StringComparison.Ordinal)) { return text + "_s" + text2; } return text; } private static string ResolvePlayerName(long playerId) { if (!AutoArchiveStore.TryGetPlayerRecord(playerId, out PlayerActivityRecord record)) { return "unknown"; } return SanitizeTagToken(record.Names.LastOrDefault((string candidate) => !string.IsNullOrWhiteSpace(candidate) && !string.Equals(candidate, "unknown", StringComparison.OrdinalIgnoreCase) && !string.Equals(candidate, "manual", StringComparison.OrdinalIgnoreCase)) ?? "unknown"); } private static string ResolveSteamId(long playerId) { if (!AutoArchiveStore.TryGetPlayerRecord(playerId, out PlayerActivityRecord record) || string.IsNullOrWhiteSpace(record.PlatformId) || !record.PlatformId.StartsWith("steam:", StringComparison.OrdinalIgnoreCase)) { return "unknown"; } string source = record.PlatformId.Substring("steam:".Length); string text = new string(source.Where(char.IsDigit).ToArray()); if (text.Length >= 15) { return text; } string text2 = new string(source.Where((char character) => char.IsLetterOrDigit(character) || character == '-' || character == '_').ToArray()); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } return "unknown"; } private static string BuildOwnerHash(IEnumerable<long> ownerIds) { uint num = 2166136261u; foreach (long ownerId in ownerIds) { byte[] bytes = BitConverter.GetBytes(ownerId); foreach (byte b in bytes) { num ^= b; num *= 16777619; } } return num.ToString("x8", CultureInfo.InvariantCulture); } private static string SanitizeTagToken(string value) { if (string.IsNullOrWhiteSpace(value)) { return "unknown"; } char[] invalidChars = Path.GetInvalidFileNameChars(); string text = new string(value.Select(delegate(char character) { if (invalidChars.Contains(character)) { return '_'; } if (char.IsWhiteSpace(character)) { return '_'; } return (!char.IsLetterOrDigit(character) && character != '-' && character != '_') ? '_' : character; }).ToArray()); text = text.Trim(new char[1] { '_' }); while (text.Contains("__")) { text = text.Replace("__", "_"); } if (text.Length > 32) { text = text.Substring(0, 32).Trim(new char[1] { '_' }); } if (!string.IsNullOrWhiteSpace(text)) { return text; } return "unknown"; } private static bool IsWorldReady() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && ZDOMan.instance != null && (Object)(object)ZNetScene.instance != (Object)null) { return (Object)(object)ZoneSystem.instance != (Object)null; } return false; } private static string FormatDate(DateTime value) { if (!(value == DateTime.MinValue)) { return value.ToString("O", CultureInfo.InvariantCulture); } return "-"; } } internal static class AutoArchiveService { [CompilerGenerated] private sealed class <>c__DisplayClass13_0 { public ArchiveRunRecord completed; internal void <RunScan>b__0(ArchiveRunRecord run) { completed = run; } } [CompilerGenerated] private sealed class <RunScan>d__13 : IEnumerator<object>, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public AutoArchiveScanOptions options; private <>c__DisplayClass13_0 <>8__1; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <RunScan>d__13(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <>1__state = -2; } private bool MoveNext() { switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass13_0(); <>8__1.completed = null; <>2__current = AutoArchiveScanner.Run(options, delegate(ArchiveRunRecord run) { <>8__1.completed = run; }); <>1__state = 1; return true; case 1: <>1__state = -1; if (<>8__1.completed != null) { AutoArchiveStore.RecordRun(<>8__1.completed); AutoArchiveStore.Flush(force: true); _logger.LogInfo((object)$"Auto archive scan finished: {<>8__1.completed.CandidateZones} candidate zone(s), {<>8__1.completed.ProcessedZones} processed zone(s), {<>8__1.completed.Clusters.Count} cluster record(s)."); } _scanRunning = false; _scanCoroutine = null; return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static readonly TimeSpan TrackInterval = TimeSpan.FromMinutes(10.0); private static readonly TimeSpan FlushInterval = TimeSpan.FromMinutes(5.0); private static ManualLogSource _logger = null; private static bool _initialized; private static bool _scanRunning; private static DateTime _lastTrackUtc = DateTime.MinValue; private static DateTime _lastFlushUtc = DateTime.MinValue; private static Coroutine? _scanCoroutine; public static void Initialize(ManualLogSource logger) { if (!_initialized) { _initialized = true; _logger = logger; PlayerActivityTracker.Initialize(logger); AutoArchiveScanner.Initialize(logger); } } public static void Update() { if (!IsServerReady()) { return; } DateTime utcNow = DateTime.UtcNow; if (utcNow - _lastTrackUtc >= TrackInterval) { _lastTrackUtc = utcNow; PlayerActivityTracker.TrackOnlinePlayers(utcNow); } if (utcNow - _lastFlushUtc >= FlushInterval) { _lastFlushUtc = utcNow; AutoArchiveStore.Flush(); } if (AutoArchiveConfig.Enabled && !_scanRunning) { TimeSpan timeSpan = TimeSpan.FromMinutes(AutoArchiveConfig.ScanIntervalMinutes); DateTime dateTime = DateTime.SpecifyKind(AutoArchiveStore.State.LastAutoScanUtc, DateTimeKind.Utc); if (!(dateTime != DateTime.MinValue) || !(utcNow - dateTime < timeSpan)) { QueueScan(new AutoArchiveScanOptions { Manual = false, DryRun = AutoArchiveConfig.DryRun, ResetAfterSave = AutoArchiveConfig.ResetAfterSave }); } } } public static bool QueueManualScan(bool? dryRunOverride = null, bool? resetAfterSaveOverride = null, IEnumerable<long>? targetPlayerIds = null) { if (!IsServerReady() || _scanRunning) { return false; } QueueScan(new AutoArchiveScanOptions { Manual = true, DryRun = (dryRunOverride ?? AutoArchiveConfig.DryRun), ResetAfterSave = (resetAfterSaveOverride ?? AutoArchiveConfig.ResetAfterSave), TargetPlayerIds = ((targetPlayerIds == null) ? new List<long>() : new List<long>(targetPlayerIds)) }); return true; } public static void Shutdown() { if (_scanCoroutine != null && (Object)(object)ZoneSaviorPlugin.Instance != (Object)null) { ((MonoBehaviour)ZoneSaviorPlugin.Instance).StopCoroutine(_scanCoroutine); _scanCoroutine = null; } _scanRunning = false; } private static void QueueScan(AutoArchiveScanOptions options) { _scanRunning = true; _logger.LogInfo((object)$"Starting auto archive scan (manual: {options.Manual}, dry run: {options.DryRun}, reset: {options.ResetAfterSave}, targets: {options.TargetPlayerIds.Count})."); _scanCoroutine = ((MonoBehaviour)ZoneSaviorPlugin.Instance).StartCoroutine(RunScan(options)); } [IteratorStateMachine(typeof(<RunScan>d__13))] private static IEnumerator RunScan(AutoArchiveScanOptions options) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <RunScan>d__13(0) { options = options }; } private static bool IsServerReady() { if ((Object)(object)ZoneSaviorPlugin.Instance != (Object)null && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && ZDOMan.instance != null && (Object)(object)ZNetScene.instance != (Object)null) { return (Object)(object)ZoneSystem.instance != (Object)null; } return false; } } internal static class AutoArchiveStore { private const string UnknownPlatformPrefix = "unknown:"; private const string ManualPlatformPrefix = "manual:"; private const int MaxRunHistory = 50; private static readonly ISerializer Serializer = new SerializerBuilder().WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); private static readonly IDeserializer Deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().WithNamingConvention(UnderscoredNamingConvention.Instance).Build(); private static readonly object Sync = new object(); private static ManualLogSource _logger = null; private static bool _initialized; private static bool _dirty; private static DateTime _lastFlushUtc = DateTime.MinValue; private static readonly Dictionary<string, PlayerActivityRecord> PlayersByPlatform = new Dictionary<string, PlayerActivityRecord>(StringComparer.Ordinal); private static readonly Dictionary<long, PlayerActivityRecord> PlayersById = new Dictionary<long, PlayerActivityRecord>(); public static string FilePath => Path.Combine(ZoneSaviorPlugin.DataStorageFullPath, "activity.yml"); public static AutoArchiveState State { get; private set; } = new AutoArchiveState(); public static void Initialize(ManualLogSource logger) { if (!_initialized) { _initialized = true; _logger = logger; Load(); } } public static void Load() { lock (Sync) { if (!File.Exists(FilePath)) { State = new AutoArchiveState(); RebuildIndexes(); _dirty = true; Flush(force: true); return; } try { string input = File.ReadAllText(FilePath); State = Deserializer.Deserialize<AutoArchiveState>(input) ?? new AutoArchiveState(); AutoArchiveState state = State; if (state.Players == null) { List<PlayerActivityRecord> list2 = (state.Players = new List<PlayerActivityRecord>()); } state = State; if (state.Runs == null) { List<ArchiveRunRecord> list4 = (state.Runs = new List<ArchiveRunRecord>()); } state = State; if (state.IgnoredPlayerIds == null) { List<long> list6 = (state.IgnoredPlayerIds = new List<long>()); } RebuildIndexes(); _dirty = false; } catch (Exception arg) { _logger.LogError((object)$"Failed to load auto archive activity file, using a blank state: {arg}"); State = new AutoArchiveState(); RebuildIndexes(); _dirty = true; } } } public static void Flush(bool force = false) { lock (Sync) { if (_dirty || force) { DateTime utcNow = DateTime.UtcNow; if (force || !(utcNow - _lastFlushUtc < TimeSpan.FromSeconds(60.0))) { Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); File.WriteAllText(FilePath, Serializer.Serialize(State)); _lastFlushUtc = utcNow; _dirty = false; } } } } public static void RecordPlayerSeen(string platformId, long playerId, string name, DateTime utcNow) { if (string.IsNullOrWhiteSpace(platformId)) { platformId = ((playerId != 0L) ? UnknownPlatformId(playerId) : "unknown"); } lock (Sync) { PlayerActivityRecord value; PlayerActivityRecord playerActivityRecord = (PlayersByPlatform.TryGetValue(platformId, out value) ? value : null); if (playerActivityRecord == null) { playerActivityRecord = new PlayerActivityRecord { PlatformId = platformId, FirstSeenUtc = utcNow }; State.Players.Add(playerActivityRecord); IndexPlayer(playerActivityRecord); } if (playerActivityRecord.FirstSeenUtc == DateTime.MinValue) { playerActivityRecord.FirstSeenUtc = utcNow; } playerActivityRecord.LastSeenUtc = utcNow; AddDistinct(playerActivityRecord.Names, name); if (playerId != 0L) { AddDistinct(playerActivityRecord.PlayerIds, playerId); PlayersById[playerId] = playerActivityRecord; MergePlayerIdRecords(playerActivityRecord, playerId); } _dirty = true; } } public static void RecordManualPlayerSeen(long playerId, DateTime utcNow) { lock (Sync) { PlayerActivityRecord value; PlayerActivityRecord playerActivityRecord = (PlayersById.TryGetValue(playerId, out value) ? value : new PlayerActivityRecord { PlatformId = ManualPlatformId(playerId), FirstSeenUtc = utcNow, PlayerIds = new List<long>(1) { playerId } }); if (!State.Players.Contains(playerActivityRecord)) { State.Players.Add(playerActivityRecord); } if (IsUnknown(playerActivityRecord)) { playerActivityRecord.PlatformId = ManualPlatformId(playerId); } if (playerActivityRecord.FirstSeenUtc == DateTime.MinValue) { playerActivityRecord.FirstSeenUtc = utcNow; } playerActivityRecord.LastSeenUtc = utcNow; AddDistinct(playerActivityRecord.Names, "manual"); RebuildPlayerIndexes(); _dirty = true; } } public static void RecordUnknownPlayer(long playerId, DateTime utcNow, string observedName = "") { if (playerId == 0L) { return; } lock (Sync) { if (PlayersById.TryGetValue(playerId, out PlayerActivityRecord value)) { if (AddObservedName(value, observedName)) { _dirty = true; } } else { GetOrCreateUnknownPlayer(playerId, utcNow, observedName); _dirty = true; } } } public static bool TryGetPlayerRecord(long playerId, out PlayerActivityRecord record) { lock (Sync) { record = (PlayersById.TryGetValue(playerId, out PlayerActivityRecord value) ? value : null); return record != null; } } public static bool TryGetPlayerIdsBySteamId(string steamId, out List<long> playerIds, out string normalizedSteamId) { normalizedSteamId = NormalizeSteamId(steamId); playerIds = new List<long>(); if (string.IsNullOrWhiteSpace(normalizedSteamId)) { return false; } lock (Sync) { foreach (PlayerActivityRecord player in State.Players) { if (!TryNormalizeSteamPlatformId(player.PlatformId, out string steamId2) || !string.Equals(steamId2, normalizedSteamId, StringComparison.Ordinal)) { continue; } foreach (long item in player.PlayerIds.Where((long id) => id != 0)) { AddDistinct(playerIds, item); } } } return playerIds.Count > 0; } public static bool IsIgnored(long playerId) { lock (Sync) { return State.IgnoredPlayerIds.Contains(playerId); } } public static void SetIgnored(long playerId, bool ignored) { lock (Sync) { if (ignored) { AddDistinct(State.IgnoredPlayerIds, playerId); } else { State.IgnoredPlayerIds.Remove(playerId); } _dirty = true; } } public static AutoArchiveCreatorEligibility EvaluateCreatorArchiveEligibility(long playerId, DateTime utcNow, int inactiveDays, int unknownGraceDays, bool recordUnknownPlayer, string observedName = "") { AutoArchiveCreatorEligibility autoArchiveCreatorEligibility = new AutoArchiveCreatorEligibility { PlayerId = playerId, Ignored = (playerId != 0L && IsIgnored(playerId)) }; if (playerId == 0L) { autoArchiveCreatorEligibility.Reason = "creatorless"; return autoArchiveCreatorEligibility; } if (ZoneLimitConfiguration.IsArchiveProtected(playerId, out string reason)) { autoArchiveCreatorEligibility.Protected = true; autoArchiveCreatorEligibility.Reason = reason; return autoArchiveCreatorEligibility; } if (autoArchiveCreatorEligibility.Ignored) { autoArchiveCreatorEligibility.Reason = $"player {playerId} is ignored"; return autoArchiveCreatorEligibility; } if (!TryGetPlayerRecord(playerId, out PlayerActivityRecord record)) { autoArchiveCreatorEligibility.PlatformId = UnknownPlatformId(playerId); autoArchiveCreatorEligibility.Names = (string.IsNullOrWhiteSpace(observedName) ? new List<string>(1) { "not_recorded_in_activity" } : new List<string>(1) { observedName.Trim() }); autoArchiveCreatorEligibility.UnknownActivityRecord = true; if (recordUnknownPlayer) { RecordUnknownPlayer(playerId, utcNow, observedName); autoArchiveCreatorEligibility.Reason = $"player {playerId} was first discovered by scanner"; } else { autoArchiveCreatorEligibility.Reason = $"player {playerId} is not recorded in activity"; } return autoArchiveCreatorEligibility; } autoArchiveCreatorEligibility.RecordedInActivity = true; autoArchiveCreatorEligibility.PlatformId = record.PlatformId; autoArchiveCreatorEligibility.Names = record.Names?.ToList() ?? new List<string>(); bool flag2 = (autoArchiveCreatorEligibility.UnknownActivityRecord = IsUnknown(record)); DateTime dateTime = (flag2 ? record.FirstSeenUtc : record.LastSeenUtc); int num = (flag2 ? unknownGraceDays : inactiveDays); if (dateTime == DateTime.MinValue) { autoArchiveCreatorEligibility.Reason = $"player {playerId} has no usable activity timestamp"; return autoArchiveCreatorEligibility; } TimeSpan timeSpan = utcNow - DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); if (timeSpan.TotalDays < (double)num) { autoArchiveCreatorEligibility.Reason = (flag2 ? $"unknown player {playerId} first seen {timeSpan.TotalDays:F1}/{num} days ago" : $"player {playerId} last seen {timeSpan.TotalDays:F1}/{num} days ago"); return autoArchiveCreatorEligibility; } autoArchiveCreatorEligibility.Eligible = true; autoArchiveCreatorEligibility.Reason = (flag2 ? $"unknown player {playerId} grace expired" : $"player {playerId} inactive for {timeSpan.TotalDays:F1} days"); return autoArchiveCreatorEligibility; } public static bool IsCreatorArchiveEligible(long playerId, DateTime utcNow, int inactiveDays, int unknownGraceDays, out string reason) { AutoArchiveCreatorEligibility autoArchiveCreatorEligibility = EvaluateCreatorArchiveEligibility(playerId, utcNow, inactiveDays, unknownGraceDays, recordUnknownPlayer: true); reason = autoArchiveCreatorEligibility.Reason; return autoArchiveCreatorEligibility.Eligible; } public static void RecordRun(ArchiveRunRecord run) { lock (Sync) { State.LastScanUtc = run.CreatedUtc; if (!run.Manual) { State.LastAutoScanUtc = run.CreatedUtc; } State.Runs.Add(run); if (State.Runs.Count > 50) { State.Runs.RemoveRange(0, State.Runs.Count - 50); } _dirty = true; } } public static void SetLastAutoScanUtc(DateTime value) { lock (Sync) { State.LastAutoScanUtc = ((value == DateTime.MinValue) ? DateTime.MinValue : DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc)); _dirty = true; } } private static PlayerActivityRecord GetOrCreateUnknownPlayer(long playerId, DateTime utcNow, string observedName = "") { string text = UnknownPlatformId(playerId); if (PlayersByPlatform.TryGetValue(text, out PlayerActivityRecord value)) { AddObservedName(value, observedName); return value; } List<string> list = new List<string>(); AddObservedName(list, observedName); if (list.Count == 0) { list.Add("unknown"); } value = new PlayerActivityRecord { PlatformId = text, FirstSeenUtc = utcNow, LastSeenUtc = DateTime.MinValue, PlayerIds = new List<long>(1) { playerId }, Names = list }; State.Players.Add(value); IndexPlayer(value); return value; } private static bool AddObservedName(PlayerActivityRecord record, string observedName) { return AddObservedName(record.Names, observedName); } private static bool AddObservedName(List<string> names, string observedName) { if (string.IsNullOrWhiteSpace(observedName) || string.Equals(observedName, "unknown", StringComparison.OrdinalIgnoreCase)) { return false; } string item = observedName.Trim(); if (names.Contains(item)) { return false; } names.Add(item); return true; } private static void MergePlayerIdRecords(PlayerActivityRecord target, long playerId) { PlayerActivityRecord target2 = target; foreach (PlayerActivityRecord item in State.Players.Where((PlayerActivityRecord player) => player != target2 && player.PlayerIds.Contains(playerId)).ToList()) { foreach (long playerId2 in item.PlayerIds) { AddDistinct(target2.PlayerIds, playerId2); } foreach (string name in item.Names) { AddDistinct(target2.Names, name); } if (target2.FirstSeenUtc == DateTime.MinValue || item.FirstSeenUtc < target2.FirstSeenUtc) { target2.FirstSeenUtc = item.FirstSeenUtc; } State.Players.Remove(item); } RebuildPlayerIndexes(); } private static void RebuildIndexes() { RebuildPlayerIndexes(); } private static void RebuildPlayerIndexes() {