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.3.0
ZoneSavior.dll
Decompiled 5 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.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using Splatform; using TMPro; using UnityEngine; using UnityEngine.EventSystems; 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.3.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.3.0.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] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace 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; } private const string ScanCommand = "zs_scan"; private const string StatusCommand = "zs_status"; private const string DebugZoneCommand = "zs_debugzone"; 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_0043: 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_0078: 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_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Expected O, but got Unknown //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: 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_scan", "[steamID] [dry|save|reset] - Runs an auto archive scan, optionally filtered to one Steam owner.", (ConsoleEvent)obj, false, 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_status", "- Writes a YAML report with recent auto archive runs.", (ConsoleEvent)obj2, false, 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_debugzone", "(x,z) - Writes a YAML report explaining auto archive eligibility for one zone.", (ConsoleEvent)obj3, false, 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_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (ZNet.instance.IsServer()) { ShowResult(ExecuteRequest(request), context); return; } RegisterRpcs(); ZPackage val = new ZPackage(); val.Write(ZoneBundleSerialization.Serialize(request)); ZRoutedRpc.instance.InvokeRoutedRPC("sighsorry.ZoneSavior_AutoArchiveCommandRequest", new object[1] { val }); if (context != null) { context.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 (!ZoneBundleCommands.IsAuthorizedSender(sender)) { SendResult(sender, AutoArchiveCommandResult.Fail("Admin only.")); return; } AutoArchiveCommandRequest request = ZoneBundleSerialization.Deserialize<AutoArchiveCommandRequest>(package.ReadString()); SendResult(sender, ExecuteRequest(request)); } 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 (!ZoneRpcRegistrar.IsServerSender(sender)) { return; } try { ShowResult(ZoneBundleSerialization.Deserialize<AutoArchiveCommandResult>(package.ReadString()), (Terminal?)(object)Console.instance); } catch (Exception ex) { _logger.LogWarning((object)("Failed to read auto archive command result: " + ex.Message)); } } 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_scan": ExecuteScan(array, messages); break; case "zs_status": ExecuteStatus(messages); break; case "zs_debugzone": 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) { IEnumerable<string> args2 = args.Skip(1); List<long> list = null; string targetLabel = ""; if (args.Length > 1 && !IsArchiveModeToken(args[1])) { list = ResolveTargetPlayerIds(args[1], out targetLabel); args2 = args.Skip(2); } ParseMode(args2, out var dryRun, out var reset); if (!((list == null) ? AutoArchiveService.QueueManualScan(dryRun, reset) : AutoArchiveService.QueueManualScan(dryRun, reset, list))) { messages.Add("Auto archive scan could not be started. World may not be ready or another scan is running."); } else { messages.Add((list == null) ? "Auto archive scan started." : ("Auto archive scan started for " + targetLabel + ".")); } } private static void ExecuteStatus(List<string> messages) { AutoArchiveStatusReport autoArchiveStatusReport = BuildStatusReport(); string text = WriteStatusReport(autoArchiveStatusReport); messages.Add($"Archive status report: {autoArchiveStatusReport.Runs.Count}/{autoArchiveStatusReport.TotalRuns} run(s)."); if (autoArchiveStatusReport.Runs.Count > 0) { ArchiveRunRecord archiveRunRecord = autoArchiveStatusReport.Runs[0]; messages.Add($"Latest: {archiveRunRecord.RunId}, dry={archiveRunRecord.DryRun}, reset={archiveRunRecord.ResetAfterSave}, candidates={archiveRunRecord.CandidateZones}, processed={archiveRunRecord.ProcessedZones}, clusters={archiveRunRecord.Clusters.Count}."); } messages.Add("Wrote YAML: " + text); } private static AutoArchiveStatusReport BuildStatusReport() { List<ArchiveRunRecord> runs = AutoArchiveStore.State.Runs.OrderByDescending((ArchiveRunRecord run) => run.CreatedUtc).ToList(); AutoArchiveStatusReport autoArchiveStatusReport = new AutoArchiveStatusReport(); ZNet instance = ZNet.instance; autoArchiveStatusReport.World = ((instance != null) ? instance.GetWorldName() : null) ?? "unknown"; autoArchiveStatusReport.CreatedAt = ZoneSaviorTimestamp.Format(DateTime.UtcNow); autoArchiveStatusReport.TotalRuns = AutoArchiveStore.State.Runs.Count; autoArchiveStatusReport.Runs = runs; return autoArchiveStatusReport; } private static string WriteStatusReport(AutoArchiveStatusReport report) { string text = Path.Combine(ZoneSaviorPlugin.DataStorageFullPath, "Diagnostics"); Directory.CreateDirectory(text); string path = "archive_status_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture) + ".yml"; string text2 = Path.Combine(text, path); File.WriteAllText(text2, ZoneBundleSerialization.Serialize(report)); return text2; } 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_0012: 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_003c: Unknown result type (might be due to invalid IL or missing references) Vector2i val = ParseZoneSpec(args.Skip(1)); ZoneSaviorBuildRecipeRules.RefreshIndex(); 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}, resetCandidate={autoArchiveZoneDebugReport.Summary.WouldBeCandidateZone}, mayBeBackupOnly={autoArchiveZoneDebugReport.Summary.MayBeBackupOnlyZone}."); messages.Add("Reason: " + autoArchiveZoneDebugReport.Summary.Reason); messages.Add("Wrote YAML: " + text); } private static AutoArchiveZoneDebugReport BuildZoneDebugReport(Vector2i zone) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0104: 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>(); ZoneSaviorZones.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 = ZoneSaviorZones.ToModel(zone); autoArchiveZoneDebugReport.Settings = new AutoArchiveZoneDebugSettings { DryRun = AutoArchiveConfig.DryRun, ResetAfterSave = AutoArchiveConfig.ResetAfterSave, InactiveDays = AutoArchiveConfig.InactiveDays, MinimumPiecesPerCluster = AutoArchiveConfig.MinimumPiecesPerCluster, 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) => AutoArchiveStore.EvaluateCreatorArchiveEligibility(playerId, utcNow, AutoArchiveConfig.InactiveDays, recordUnknownPlayer: false)).ToList(); HashSet<long> onlineCreatorIds = AutoArchiveScanner.GetOnlineCreatorIds(); foreach (AutoArchiveCreatorEligibility creator in autoArchiveZoneDebugReport2.Creators) { if (onlineCreatorIds.Contains(creator.PlayerId)) { creator.Eligible = false; creator.Reason = $"player {creator.PlayerId} is online"; } } bool flag = autoArchiveZoneDebugReport2.Creators.Count > 0 && autoArchiveZoneDebugReport2.Creators.All((AutoArchiveCreatorEligibility 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), MayBeBackupOnlyZone = (!flag && autoArchiveZoneDebugReport2.Creators.Any((AutoArchiveCreatorEligibility creator) => creator.Eligible)) }; 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_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) ZoneStructureInfo zoneStructureInfo = ZoneStructureClassifier.Inspect(zdo, requestedZone); AutoArchiveZoneDebugObject autoArchiveZoneDebugObject = new AutoArchiveZoneDebugObject(); autoArchiveZoneDebugObject.ZdoId = zoneStructureInfo.ZdoId; autoArchiveZoneDebugObject.PrefabHash = zoneStructureInfo.PrefabHash; autoArchiveZoneDebugObject.Prefab = zoneStructureInfo.Prefab; autoArchiveZoneDebugObject.Position = new float[3] { Round(zoneStructureInfo.Position.x), Round(zoneStructureInfo.Position.y), Round(zoneStructureInfo.Position.z) }; autoArchiveZoneDebugObject.ObjectZone = ZoneSaviorZones.ToModel(zoneStructureInfo.ObjectZone); autoArchiveZoneDebugObject.CreatorPlayerId = zoneStructureInfo.CreatorPlayerId; autoArchiveZoneDebugObject.CreatorName = zoneStructureInfo.CreatorName; autoArchiveZoneDebugObject.HasPrefab = zoneStructureInfo.HasPrefab; autoArchiveZoneDebugObject.HasZNetView = zoneStructureInfo.HasZNetView; autoArchiveZoneDebugObject.HasWearNTear = zoneStructureInfo.HasWearNTear; autoArchiveZoneDebugObject.HasPiece = zoneStructureInfo.HasPiece; autoArchiveZoneDebugObject.HasBuildRecipe = zoneStructureInfo.HasBuildRecipe; autoArchiveZoneDebugObject.InRequestedZone = zoneStructureInfo.InRequestedZone; autoArchiveZoneDebugObject.AutoArchiveCandidatePiece = zoneStructureInfo.AutoArchiveCandidatePiece; autoArchiveZoneDebugObject.ExclusionReasons = zoneStructureInfo.ExclusionReasons; return autoArchiveZoneDebugObject; } 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); if (report.Summary.MayBeBackupOnlyZone) { return "This mixed-owner zone can be saved without reset when it connects to an eligible core through shared eligible creators. Protected creators: " + text; } 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_debugzone (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 (IsArchiveModeToken(arg, "dry") || IsArchiveModeToken(arg, "dry-run")) { dryRun = true; reset = false; continue; } if (IsArchiveModeToken(arg, "save")) { dryRun = false; reset = false; continue; } if (IsArchiveModeToken(arg, "reset")) { dryRun = false; reset = true; continue; } throw new InvalidOperationException("Unknown archive mode '" + arg + "'. Use dry, save, or reset."); } } private static bool IsArchiveModeToken(string arg) { if (!IsArchiveModeToken(arg, "dry") && !IsArchiveModeToken(arg, "dry-run") && !IsArchiveModeToken(arg, "save")) { return IsArchiveModeToken(arg, "reset"); } return true; } private static bool IsArchiveModeToken(string arg, string expected) { return string.Equals(arg, expected, StringComparison.OrdinalIgnoreCase); } 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 (ZoneSaviorSteamIds.LooksLikeSteamId(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_scan [steamID] [dry|save|reset]"); } 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 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, 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 sealed class AutoArchiveState { public const int CurrentVersion = 1; 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>(); } 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 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 AutoArchiveStatusReport { public int Version { get; set; } = 1; public string World { get; set; } = ""; public string CreatedAt { get; set; } = ""; public int TotalRuns { get; set; } public List<ArchiveRunRecord> Runs { get; set; } = new List<ArchiveRunRecord>(); } 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 EligiblePieceCount { 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>(); public List<ZoneBundleZone> ResetZones { get; set; } = new List<ZoneBundleZone>(); public List<ZoneBundleZone> BackupOnlyZones { get; set; } = new List<ZoneBundleZone>(); public List<ZoneBundleZone> ResetCompletedZones { get; set; } = new List<ZoneBundleZone>(); public List<ZoneBundleZone> ResetSkippedZones { 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 Protected { get; set; } public bool Eligible { get; set; } public string Reason { 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<AutoArchiveCreatorEligibility> Creators { get; set; } = new List<AutoArchiveCreatorEligibility>(); 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 MinimumPiecesPerCluster { 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 bool MayBeBackupOnlyZone { 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; } } private sealed class AutoArchivePlan { public List<Vector2i> Zones { get; } public List<Vector2i> ResetZones { get; } public AutoArchivePlan(List<Vector2i> zones, List<Vector2i> resetZones) { Zones = zones; ResetZones = resetZones; } } 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; } public static IEnumerator Run(AutoArchiveScanOptions options, Action<ArchiveRunRecord> onComplete) { DateTime utcNow = DateTime.UtcNow; ArchiveRunRecord run = new ArchiveRunRecord { RunId = utcNow.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture), CreatedUtc = utcNow, Manual = options.Manual, DryRun = options.DryRun, ResetAfterSave = options.ResetAfterSave, TargetPlayerIds = options.TargetPlayerIds.ToList() }; if (!IsWorldReady()) { run.Messages.Add("World is not ready."); onComplete(run); yield break; } ZoneSaviorBuildRecipeRules.RefreshIndex(); PlayerActivityTracker.TrackOnlinePlayers(utcNow); Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos = new Dictionary<Vector2i, AutoArchiveZoneInfo>(); yield return ScanZdosBySector(utcNow, zoneInfos, run); HashSet<long> targetPlayerIds = options.TargetPlayerIds.ToHashSet(); bool isTargetOverride = targetPlayerIds.Count > 0; HashSet<long> eligibleCreators = FindEligibleCreators(zoneInfos, utcNow, targetPlayerIds); List<AutoArchivePlan> list = BuildArchivePlans(zoneInfos, eligibleCreators, isTargetOverride && !options.ResetAfterSave); run.CandidateZones = list.Sum((AutoArchivePlan autoArchivePlan) => autoArchivePlan.Zones.Count); int processedZones = 0; int clusterIndex = 0; foreach (AutoArchivePlan plan in list) { List<Vector2i> cluster = plan.Zones; clusterIndex++; ArchiveClusterRecord record = BuildClusterRecord(cluster, zoneInfos); record.ResetZones = plan.ResetZones.Select(ZoneSaviorZones.ToModel).ToList(); record.BackupOnlyZones = cluster.Except(plan.ResetZones).Select(ZoneSaviorZones.ToModel).ToList(); record.EligiblePieceCount = cluster.Sum((Vector2i zone) => zoneInfos[zone].CreatorPieceCounts.Where((KeyValuePair<long, int> pair) => eligibleCreators.Contains(pair.Key)).Sum((KeyValuePair<long, int> pair) => pair.Value)); bool flag = !isTargetOverride && record.EligiblePieceCount < AutoArchiveConfig.MinimumPiecesPerCluster; if (flag && (!options.ResetAfterSave || record.BackupOnlyZones.Count > 0)) { record.Status = "skipped"; record.Reason = $"eligible piece count {record.EligiblePieceCount} is below minimum {AutoArchiveConfig.MinimumPiecesPerCluster}" + ((record.BackupOnlyZones.Count > 0) ? "; mixed-owner archives are never reset without saving" : ""); run.Clusters.Add(record); continue; } if (!isTargetOverride && !AllCreatorsEligible(record.Creators.Where(eligibleCreators.Contains), DateTime.UtcNow, out string reason)) { record.Status = "skipped"; record.Reason = reason; run.Clusters.Add(record); continue; } if (processedZones + cluster.Count > AutoArchiveConfig.MaxZonesPerRun) { record.Status = "skipped"; record.Reason = $"max zones per run would be exceeded ({processedZones + cluster.Count}/{AutoArchiveConfig.MaxZonesPerRun})"; run.Clusters.Add(record); continue; } processedZones += cluster.Count; if (flag) { if (options.DryRun) { record.Status = "dry-run-reset-without-save"; record.Reason = $"small inactive cluster would be reset without saving ({record.PieceCount}/{AutoArchiveConfig.MinimumPiecesPerCluster} pieces)"; run.Clusters.Add(record); continue; } ZoneBundleResetResult resetOnlyResult = null; string reason2; yield return ZoneBundleCommands.ResetGeneratedZonesAsync(cluster, delegate(ZoneBundleResetResult result) { resetOnlyResult = result; }, (Vector2i zone) => CanResetZone(zone, eligibleCreators, targetPlayerIds, out reason2)); if (resetOnlyResult == null) { record.Status = "reset-without-save-failed"; record.Reason = "reset failed: reset coroutine did not return a result"; run.Clusters.Add(record); _logger.LogError((object)"Auto archive small-cluster reset failed: reset coroutine did not return a result."); yield return null; continue; } record.Status = (resetOnlyResult.Success ? "reset-without-save" : "reset-without-save-failed"); record.Reason = resetOnlyResult.Message; if (resetOnlyResult.Success) { record.ResetCompletedZones = cluster.Select(ZoneSaviorZones.ToModel).ToList(); } run.Clusters.Add(record); yield return null; continue; } record.Tag = ZoneBundleCommands.MakeUniqueAutoArchiveTag(BuildTag(clusterIndex, cluster, zoneInfos, eligibleCreators)); if (options.DryRun) { record.Status = "dry-run"; record.Reason = $"Would save {cluster.Count} zone(s), keep {record.BackupOnlyZones.Count} mixed-owner zone(s), " + $"and reset {(options.ResetAfterSave ? plan.ResetZones.Count : 0)} zone(s); dry run is enabled."; run.Clusters.Add(record); continue; } ZoneBundleArchiveResult saveResult = null; Dictionary<Vector2i, Dictionary<ZDOID, string>> resetSnapshots = new Dictionary<Vector2i, Dictionary<ZDOID, string>>(); Exception ex = null; if (options.ResetAfterSave) { try { resetSnapshots = plan.ResetZones.ToDictionary((Vector2i zone) => zone, CaptureResetObjects); } catch (Exception ex2) { ex = ex2; } } if (ex != null) { record.Status = "skipped"; record.Reason = "Could not verify all player-owned reset objects: " + ex.Message; run.Clusters.Add(record); continue; } Dictionary<Vector2i, HashSet<ZDOID>> capturedIds = plan.ResetZones.ToDictionary((Vector2i zone) => zone, (Vector2i _) => new HashSet<ZDOID>()); yield return ZoneBundleCommands.SaveZonesAsync(cluster, record.Tag, delegate(ZoneBundleArchiveResult result) { saveResult = result; }, 0L, (plan.ResetZones.Count > 0) ? plan.ResetZones : null, options.ResetAfterSave, options.ResetAfterSave ? new Func<Vector2i, ZDO, bool>(VerifyCapturedObject) : null); if (saveResult == null) { record.Status = "failed"; record.Reason = "save failed: archive coroutine did not return a result"; run.Clusters.Add(record); _logger.LogError((object)("Auto archive save failed for tag '" + record.Tag + "': archive coroutine did not return a result.")); yield return null; continue; } record.TerrainLoaded = saveResult.TerrainLoaded; record.TerrainCaptured = saveResult.TerrainCaptured; if (!saveResult.Success) { record.Status = "failed"; record.Reason = saveResult.Message; run.Clusters.Add(record); continue; } record.Status = "saved"; record.Reason = saveResult.Message; if (options.ResetAfterSave) { yield return ResetSavedZones(plan, record, zoneInfos, eligibleCreators, targetPlayerIds, resetSnapshots, capturedIds); } run.Clusters.Add(record); yield return null; bool VerifyCapturedObject(Vector2i zone, ZDO zdo) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_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) if (!options.ResetAfterSave || !resetSnapshots.TryGetValue(zone, out Dictionary<ZDOID, string> value) || (zdo.GetLong(ZDOVars.s_creator, 0L) == 0L && !zdo.GetBool(ZDOVars.s_tamed, false))) { return true; } if (!value.TryGetValue(zdo.m_uid, out var value2) || value2 != ZoneBundleCommands.GetAutoArchiveResetFingerprint(zdo)) { return false; } capturedIds[zone].Add(zdo.m_uid); return true; } } run.ProcessedZones = processedZones; onComplete(run); } private static IEnumerator ResetSavedZones(AutoArchivePlan plan, ArchiveClusterRecord record, Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, HashSet<long> eligibleCreators, HashSet<long> targetPlayerIds, Dictionary<Vector2i, Dictionary<ZDOID, string>> snapshots, Dictionary<Vector2i, HashSet<ZDOID>> capturedIds) { List<string> messages = new List<string>(); foreach (Vector2i resetZone in plan.ResetZones) { Vector2i zone = resetZone; string reason = ""; HashSet<ZDOID> destroyedIds = new HashSet<ZDOID>(); bool mutationStarted = false; if (!CanReset()) { record.ResetSkippedZones.Add(ZoneSaviorZones.ToModel(zone)); messages.Add($"Kept ({zone.x},{zone.y}): {reason}."); continue; } ZoneBundleResetResult resetResult = null; Exception resetError = null; yield return ZoneSaviorCoroutines.RunSafely(ZoneBundleCommands.ResetGeneratedZonesAsync((IEnumerable<Vector2i>)(object)new Vector2i[1] { zone }, delegate(ZoneBundleResetResult value) { resetResult = value; }, (Vector2i _) => CanReset(), destroyedIds, BeforeFirstMutation), delegate(Exception? ex3) { resetError = ex3; }); if (resetError != null || resetResult == null || !resetResult.Success) { if (!mutationStarted) { record.ResetSkippedZones.Add(ZoneSaviorZones.ToModel(zone)); messages.Add($"Kept ({zone.x},{zone.y}) before any mutation: " + (resetError?.Message ?? resetResult?.Message ?? "Reset returned no result.") + " " + reason); continue; } record.Status = "saved-reset-failed"; record.Reason = $"Reset incomplete at ({zone.x},{zone.y}); pending marker retained. " + (resetError?.Message ?? resetResult?.Message ?? "Reset returned no result.") + (string.IsNullOrWhiteSpace(reason) ? "" : (" " + reason)); _logger.LogWarning((object)record.Reason); yield break; } Exception ex = null; try { ZoneBundleStore.MarkResetCompleted(record.Tag, zone); } catch (Exception ex2) { ex = ex2; } if (ex != null) { record.Status = "saved-reset-failed"; record.Reason = $"Reset finished at ({zone.x},{zone.y}), but completion could not be recorded; pending marker retained: {ex.Message}"; yield break; } record.ResetCompletedZones.Add(ZoneSaviorZones.ToModel(zone)); messages.Add(resetResult.Message); void BeforeFirstMutation() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) ZoneBundleStore.MarkResetStarted(record.Tag, zone); mutationStarted = true; } bool CanReset() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: 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_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) try { if (snapshots[zone].Keys.Any((ZDOID id) => !capturedIds[zone].Contains(id))) { reason = "Not all player-owned objects were captured in the archive"; return false; } if (targetPlayerIds.Count == 0 && !AllCreatorsEligible(zoneInfos[zone].Creators, DateTime.UtcNow, out reason)) { return false; } if (!CanResetZone(zone, eligibleCreators, targetPlayerIds, out reason)) { return false; } return ResetSnapshotMatches(snapshots[zone], CaptureResetObjects(zone), destroyedIds, out reason); } catch (Exception ex3) { reason = "Could not verify reset objects: " + ex3.Message; return false; } } } record.Status = ((record.ResetSkippedZones.Count == 0) ? "reset" : ((record.ResetCompletedZones.Count > 0) ? "saved-partial-reset" : "saved-reset-skipped")); record.Reason = $"Saved {plan.Zones.Count} zone(s), reset {record.ResetCompletedZones.Count}, " + $"kept {record.BackupOnlyZones.Count} backup-only zone(s). " + string.Join(" ", messages); } private static bool CanResetZone(Vector2i zone, HashSet<long> plannedCreators, HashSet<long> targetPlayerIds, out string reason) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (!IsWorldReady()) { reason = "World is no longer ready"; return false; } PlayerActivityTracker.TrackOnlinePlayers(DateTime.UtcNow); List<ZDO> list = new List<ZDO>(); ZoneSaviorZones.FindObjects(zone, list); HashSet<long> hashSet = new HashSet<long>(); foreach (ZDO item in list) { if (!item.IsValid()) { continue; } long num = item.GetLong(ZDOVars.s_creator, 0L); if (num != 0L) { if (!plannedCreators.Contains(num) || (targetPlayerIds.Count > 0 && !targetPlayerIds.Contains(num))) { reason = $"creator {num} is outside the approved reset owners"; return false; } hashSet.Add(num); } } reason = ""; if (targetPlayerIds.Count <= 0) { return AllCreatorsEligible(hashSet, DateTime.UtcNow, out reason); } return true; } private static Dictionary<ZDOID, string> CaptureResetObjects(Vector2i zone) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) List<ZDO> list = new List<ZDO>(); ZoneSaviorZones.FindObjects(zone, list); Dictionary<ZDOID, string> dictionary = new Dictionary<ZDOID, string>(); foreach (ZDO item in list) { if (item.IsValid() && (item.GetLong(ZDOVars.s_creator, 0L) != 0L || item.GetBool(ZDOVars.s_tamed, false))) { dictionary[item.m_uid] = ZoneBundleCommands.GetAutoArchiveResetFingerprint(item); } } return dictionary; } private static bool ResetSnapshotMatches(Dictionary<ZDOID, string> saved, Dictionary<ZDOID, string> current, ISet<ZDOID> destroyedIds, out string reason) { if (saved.Keys.Any((ZDOID id) => !current.ContainsKey(id) && !destroyedIds.Contains(id)) || current.Any<KeyValuePair<ZDOID, string>>((KeyValuePair<ZDOID, string> pair) => !saved.TryGetValue(pair.Key, out string value) || value != pair.Value)) { reason = "player-owned objects or their saved contents changed during the archive operation"; return false; } reason = ""; return true; } internal static HashSet<long> GetOnlineCreatorIds() { //IL_0068: Unknown result type (might be due to invalid IL or missing references) HashSet<long> hashSet = new HashSet<long>(); if ((Object)(object)Player.m_localPlayer != (Object)null) { hashSet.Add(Player.m_localPlayer.GetPlayerID()); } if ((Object)(object)ZNet.instance != (Object)null && ZDOMan.instance != null) { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null && !((ZDOID)(ref peer.m_characterID)).IsNone()) { ZDO zDO = ZDOMan.instance.GetZDO(peer.m_characterID); long num = ((zDO != null) ? zDO.GetLong(ZDOVars.s_playerID, 0L) : 0); if (num != 0L) { hashSet.Add(num); } } } } long[] array = hashSet.ToArray(); for (int i = 0; i < array.Length; i++) { if (AutoArchiveStore.TryGetPlayerRecord(array[i], out PlayerActivityRecord record)) { hashSet.UnionWith(record.PlayerIds.Where((long creator) => creator != 0)); } } return hashSet; } 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) zone = default(Vector2i); creator = 0L; if (!ZoneStructureClassifier.TryGetAutoArchiveCandidate(zdo, out zone, out creator, out string creatorName)) { return false; } AutoArchiveStore.RecordUnknownPlayer(creator, utcNow, creatorName); return true; } private static IEnumerator ScanZdosBySector(DateTime utcNow, Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, ArchiveRunRecord run) { int batchSize = Math.Max(1, AutoArchiveConfig.ScannerBatchSize); int processedSinceYield = 0; List<ZDO>[] sectors = ZoneSaviorGameAccess.Sectors.Invoke(ZDOMan.instance); int sectorIndex = 0; while (sectors != null && sectorIndex < sectors.Length) { List<ZDO> sector = sectors[sectorIndex]; if (sector != null && sector.Count != 0) { for (int objectIndex = 0; objectIndex < sector.Count; objectIndex++) { ProcessScanZdo(sector[objectIndex], utcNow, zoneInfos, run); processedSinceYield++; if (processedSinceYield >= batchSize) { processedSinceYield = 0; yield return null; } } } sectorIndex++; } List<List<ZDO>> list = ZDOMan.instance.GetPortals().Values.ToList(); foreach (List<ZDO> sector in list) { if (sector == null || sector.Count == 0) { continue; } for (sectorIndex = 0; sectorIndex < sector.Count; sectorIndex++) { ProcessScanZdo(sector[sectorIndex], utcNow, zoneInfos, run); processedSinceYield++; if (processedSinceYield >= batchSize) { processedSinceYield = 0; yield return null; } } } } 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 HashSet<long> FindEligibleCreators(Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, DateTime utcNow, HashSet<long> targetPlayerIds) { HashSet<long> online = GetOnlineCreatorIds(); string reason; return (from creator in zoneInfos.Values.SelectMany((AutoArchiveZoneInfo info) => info.Creators).Distinct() where (targetPlayerIds.Count <= 0) ? (!online.Contains(creator) && AutoArchiveStore.IsCreatorArchiveEligible(creator, utcNow, AutoArchiveConfig.InactiveDays, out reason)) : targetPlayerIds.Contains(creator) select creator).ToHashSet(); } private static List<AutoArchivePlan> BuildArchivePlans(Dictionary<Vector2i, AutoArchiveZoneInfo> zoneInfos, HashSet<long> eligibleCreators, bool allowMixedOnly) { //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Unknown result type (might be due to invalid IL or missing references) //IL_013e: 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) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Unknown result type (might be due to invalid IL or missing references) List<AutoArchivePlan> list = new List<AutoArchivePlan>(); HashSet<Vector2i> hashSet = (from info in zoneInfos.Values where info.Creators.Any(eligibleCreators.Contains) select info.Zone).ToHashSet(); HashSet<Vector2i> hashSet2 = hashSet.Where((Vector2i zone) => zoneInfos[zone].Creators.All(eligibleCreators.Contains)).ToHashSet(); while (hashSet.Count > 0) { Vector2i item = (from zone in hashSet orderby zone.x, zone.y select zone).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 neighbor = new Vector2i(val.x + val2.x, val.y + val2.y); if (hashSet.Contains(neighbor) && ((hashSet2.Contains(val) && hashSet2.Contains(neighbor)) || zoneInfos[val].Creators.Any((long creator) => eligibleCreators.Contains(creator) && zoneInfos[neighbor].Creators.Contains(creator)))) { hashSet.Remove(neighbor); queue.Enqueue(neighbor); } } } List<Vector2i> list3 = (from zone in list2 orderby zone.y, zone.x select zone).ToList(); List<Vector2i> list4 = list3.Where(hashSet2.Contains).ToList(); if (list4.Count > 0 || allowMixedOnly) { list.Add(new AutoArchivePlan(list3, list4)); } } return (from plan in list orderby plan.Zones.Min((Vector2i zone) => zone.x), plan.Zones.Min((Vector2i zone) => zone.y) select plan).ToList(); } private static ArchiveClusterRecord BuildClusterRecord(List<Vector2i> cluster, Dictionary<Vector2i, AutoArchiveZoneInfo> candidates) { //IL_008b: 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_0092: 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 zone).Select(ZoneSaviorZones.ToModel).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) { HashSet<long> onlineCreatorIds = GetOnlineCreatorIds(); foreach (long creator in creators) { if (onlineCreatorIds.Contains(creator)) { reason = $"player {creator} is online"; return false; } if (!AutoArchiveStore.IsCreatorArchiveEligible(creator, utcNow, AutoArchiveConfig.InactiveDays, out reason)) { return false; } } reason = ""; return true; } private static string BuildTag(int index, List<Vector2i> cluster, Dictionary<Vector2i, AutoArchiveZoneInfo> candidates, HashSet<long> eligibleCreators) { Dictionary<long, int> dictionary = BuildCreatorPieceCounts(cluster, candidates); foreach (long item in dictionary.Keys.Where((long creator) => !eligibleCreators.Contains(creator)).ToList()) { dictionary.Remove(item); } string arg = BuildOwnerSegment(dictionary); 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) { List<long> list = (from creator in creatorPieceCounts.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 creatorPieceCounts.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 ZoneSaviorPaths.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 text = ZoneSaviorSteamIds.Normalize(record.PlatformId); if (!string.IsNullOrWhiteSpace(text)) { return text; } string text2 = new string((from character in record.PlatformId.Substring("steam:".Length) where char.IsLetterOrDigit(character) || character == '-' || character == '_' select 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 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; } } internal static class AutoArchiveService { 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; private static int _scanOperationToken; public static bool IsScanRunning => _scanRunning; 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; } return QueueScan(new AutoArchiveScanOptions { Manual = true, DryRun = (dryRunOverride ?? AutoArchiveConfig.DryRun), ResetAfterSave = (resetAfterSaveOverride ?? AutoArchiveConfig.ResetAfterSave), TargetPlayerIds = ((targetPlayerIds == null) ? new List<long>() : new List<long>(targetPlayerIds)) }); } public static void Shutdown() { if (_scanCoroutine != null && (Object)(object)ZoneSaviorPlugin.Instance != (Object)null) { ((MonoBehaviour)ZoneSaviorPlugin.Instance).StopCoroutine(_scanCoroutine); _scanCoroutine = null; } ZoneBundleCommands.EndOperation(_scanOperationToken); _scanOperationToken = 0; _scanRunning = false; } private static bool QueueScan(AutoArchiveScanOptions options) { if (!ZoneBundleCommands.TryBeginOperation("auto archive scan", out int token, out string _)) { return false; } _scanOperationToken = token; _scanRunning = true; _logger.LogInfo((object)$"Starting auto archive scan (manual: {options.Manual}, dry run: {options.DryRun}, reset: {options.ResetAfterSave}, targets: {options.TargetPlayerIds.Count})."); try { _scanCoroutine = ((MonoBehaviour)ZoneSaviorPlugin.Instance).StartCoroutine(RunScan(options, token)); if (_scanCoroutine == null) { throw new InvalidOperationException("Unity did not start the auto archive coroutine."); } return true; } catch (Exception ex) { ZoneBundleCommands.EndOperation(token); _scanOperationToken = 0; _scanRunning = false; _scanCoroutine = null; _logger.LogError((object)$"Auto archive scan could not start: {ex}"); try { AutoArchiveStore.RecordRun(CreateFailedRun(options, DateTime.UtcNow, "Auto archive scan could not start: " + ex.Message)); AutoArchiveStore.Flush(force: true); } catch (Exception arg) { _logger.LogError((object)$"Failed to record auto archive start failure: {arg}"); } return false; } } private static IEnumerator RunScan(AutoArchiveScanOptions options, int operationToken) { ArchiveRunRecord completed = null; Exception failure = null; try { yield return ZoneSaviorCoroutines.RunSafely(AutoArchiveScanner.Run(options, delegate(ArchiveRunRecord run) { completed = run; }), delegate(Exception? exception) { failure = exception; }); if (failure != null || completed == null) { DateTime utcNow = DateTime.UtcNow; string text = ((failure != null) ? ("Auto archive scan failed: " + failure.Message) : "Auto archive scanner did not return a result."); completed = CreateFailedRun(options, utcNow, text); _logger.LogError((object)((failure != null) ? $"Auto archive scan failed unexpectedly: {failure}" : text)); } AutoArchiveStore.RecordRun(completed); AutoArchiveStore.Flush(force: true); _logger.LogInfo((object)$"Auto archive scan finished: {completed.CandidateZones} candidate zone(s), {completed.ProcessedZones} processed zone(s), {completed.Clusters.Count} cluster record(s)."); } finally { ZoneBundleCommands.EndOperation(operationToken); if (_scanOperationToken == operationToken) { _scanOperationToken = 0; } _scanRunning = false; _scanCoroutine = null; } } private static ArchiveRunRecord CreateFailedRun(AutoArchiveScanOptions options, DateTime failedAt, string failureMessage) { return new ArchiveRunRecord { RunId = failedAt.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture), CreatedUtc = failedAt, Manual = options.Manual, DryRun = options.DryRun, ResetAfterSave = options.ResetAfterSave, TargetPlayerIds = new List<long>(options.TargetPlayerIds), Messages = new List<string>(1) { failureMessage } }; } 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 int MaxRunHistory = 50; private static readonly TimeSpan InternalWriteReloadGrace = TimeSpan.FromSeconds(2.0); 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 DateTime _lastInternalWriteUtc = 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)) { ReplaceState(new AutoArchiveState()); _dirty = true; Flush(force: true); return; } try { if (!TryReadStateFromDisk(out AutoArchiveState state, out string error)) { throw new InvalidDataException(error); } ReplaceState(state); _dirty = false; } catch (Exception arg) { BackupInvalidActivityFile(); _logger.LogError((object)$"Failed to load auto archive activity file, using a blank state: {arg}"); ReplaceState(new AutoArchiveState()); _dirty = true; } } } private static void BackupInvalidActivityFile() { try { string? directoryName = Path.GetDirectoryName(FilePath); string text = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture); string text2 = Path.Combine(directoryName, "activity.invalid." + text + ".yml"); File.Copy(FilePath, text2, overwrite: false); _logger.LogWarning((object)("Backed up invalid auto archive activity data to '" + text2 + "'.")); } catch (Exception ex) { _logger.LogWarning((object)("Failed to back up invalid auto archive activity data: " + ex.Message)); } } public static void Flush(bool force = false) { lock (Sync) { if (_dirty || force) { DateTime utcNow = DateTime.UtcNow; if (force || !(utcNow - _lastFlushUtc < TimeSpan.FromSeconds(60.0))) { ZoneSaviorFiles.WriteAllTextAtomic(FilePath, Serializer.Serialize(State)); _lastFlushUtc = utcNow; _lastInternalWriteUtc = utcNow; _dirty = false; } } } } public static bool TryReloadFromDiskIfSafe(bool scanRunning, out string message) { lock (Sync) { if (_dirty) { message = "Activity reload skipped: runtime activity state has unsaved changes."; return false; } if (scanRunning) { message = "Activity reload skipped: auto archive scan is running."; return false; } if (DateTime.UtcNow - _lastInternalWriteUtc < InternalWriteReloadGrace) { message = "Activity reload skipped: change was caused by a recent ZoneSavior activity flush."; return false; } if (!File.Exists(FilePath)) { message = "Activity reload skipped: activity.yml does not exist."; return false; } if (!TryReadStateFromDisk(out AutoArchiveState state, out string error)) { message = "Activity reload skipped: " + error; return false; } ReplaceState(state); _dirty = false; message = "Activity reload complete."; return true; } } 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 }; State.Players.Add(playerActivityRecord); IndexPlayer(playerActivityRecord); } playerActivityRecord.LastSeenUtc = utcNow; AddDistinct(playerActivityRecord.Names, name); if (playerId != 0L) { PlayerActivityRecord value2; PlayerActivityRecord playerActivityRecord2 = (PlayersById.TryGetValue(playerId, out value2) ? value2 : null); AddDistinct(playerActivityRecord.PlayerIds, playerId); PlayersById[playerId] = playerActivityRecord; if (playerActivityRecord2 != null && playerActivityRecord2 != playerActivityRecord) { MergePlayerRecords(playerActivityRecord, playerActivityRecord2); 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 (value.LastSeenUtc == DateTime.MinValue) { value.LastSeenUtc = utcNow; _dirty = true; } 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; } } internal static bool TryResolveLastKnownPlayerName(long playerId, out string name) { name = ""; if (playerId == 0L) { return false; } lock (Sync) { if (!PlayersById.TryGetValue(playerId, out PlayerActivityRecord value) || value.Names == null) { return false; } for (int num = value.Names.Count - 1; num >= 0; num--) { string text = value.Names[num]?.Trim() ?? ""; if (text.Length != 0 && !string.Equals(text, "unknown", StringComparison.OrdinalIgnoreCase) && !string.Equals(text, "manual", StringComparison.OrdinalIgnoreCase)) { name = text; return true; } } } return false; } public static bool TryGetPlayerIdsBySteamId(string steamId, out List<long> playerIds, out string normalizedSteamId) { normalizedSteamId = ZoneSaviorSteamIds.Normalize(steamId); playerIds = new List<long>(); if (string.IsNullOrWhiteSpace(normalizedSteamId)) { return false; } lock (Sync) { foreach (PlayerActivityRecord player in State.Players) { if (!ZoneSaviorSteamIds.TryNormalizePlatformId(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 AutoArchiveCreatorEligibility EvaluateCreatorArchiveEligibility(long playerId, DateTime utcNow, int inactiveDays, bool recordUnknownPlayer, string observedName = "") { AutoArchiveCreatorEligibility autoArchiveCreatorEligibility = new AutoArchiveCreatorEligibility { PlayerId = playerId }; if (playerId == 0L) { autoArchiveCreatorEligibility.Reason = "creatorless"; return autoArchiveCreatorEligibility; } PlayerActivityRecord record; bool flag = TryGetPlayerRecord(playerId, out record); string platformId = (flag ? record.PlatformId : ""); IEnumerable<string> playerNames = (flag ? record.Names.ToList() : new List<string>()); if (ZoneLimitConfiguration.IsArchiveProtected(playerId, platformId, playerNames, out string reason)) { autoArchiveCreatorEligibility.Protected = true; autoArchiveCreatorEligibility.Reason = reason; return autoArchiveCreatorEligibility; } if (!flag) { 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 lastSeenUtc = record.LastSeenUtc; if (lastSeenUtc == DateTime.MinValue) { autoArchiveCreatorEligibility.Reason = $"player {playerId} has no usable activity timestamp"; return autoArchiveCreatorEligibility; } TimeSpan timeSpan = utcNow - DateTime.SpecifyKind(lastSeenUtc, DateTimeKind.Utc); if (timeSpan.TotalDays < (double)inactiveDays) { autoArchiveCreatorEligibility.Reason = (flag2 ? $"unknown player {playerId} discovered {timeSpan.TotalDays:F1}/{inactiveDays} days ago" : $"player {playerId} last seen {timeSpan.TotalDays:F1}/{inactiveDays} days ago"); return autoArchiveCreatorEligibility; } autoArchiveCreatorEligibility.Eligible = true; autoArchiveCreatorEligibility.Reason = (flag2 ? $"unknown player {playerId} inactive since discovery for {timeSpan.TotalDays:F1} days" : $"player {playerId} inactive for {timeSpan.TotalDays:F1} days"); return autoArchiveCreatorEligibility; } public static bool IsCreatorArchiveEligible(long playerId, DateTime utcNow, int inactiveDays, out string reason) { AutoArchiveCreatorEligibility autoArchiveCreatorEligibility = EvaluateCreatorArchiveEligibility(playerId, utcNow, inactiveDays, 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; } } 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, LastSeenUtc = utcNow, 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 MergePlayerRecords(PlayerActivityRecord target, PlayerActivityRecord duplicate) { MergePlayerRecordContents(target, duplicate); State.Players.Remove(duplicate); } private static void MergePlayerRecordContents(PlayerActivityRecord target, PlayerActivityRecord duplicate) { foreach (long playerId in duplicate.PlayerIds) { AddDistinct(target.PlayerIds, playerId); } foreach (string name in duplicate.Names) { AddDistinct(target.Names, name); } if (duplicate.LastSeenUtc > target.LastSeenUtc) { target.LastSeenUtc = duplicate.LastSeenUtc; } } private static void RebuildPlayerIndexes() { BuildPlayerIndexes(State, out Dictionary<string, PlayerActivityRecord> playersByPlatform, out Dictionary<long, PlayerActivityRecord> playersById); ReplaceIndexes(playersByPlatform, playersById); } private static void ReplaceState(AutoArchiveState state) { NormalizePlayerRecords(state); BuildPlayerIndexes(state, out Dictionary<string, PlayerActivityRecord> playersByPlatform, out Dictionary<long, PlayerActivityRecord> playersById); State = state; ReplaceIndexes(playersByPlatform, playersById); } private static void NormalizePlayerRecords(AutoArchiveState state) { bool flag; do { flag = false; for (int i = 0; i < state.Players.Count; i++) { if (flag) { break; } PlayerActivityRecord playerActivityRecord = state.Players[i]; for (int j = i + 1; j < state.Players.Count; j++) { PlayerActivityRecord duplicate = state.Players[j]; if (playerActivityRecord.PlayerIds.Any((long id) => id != 0L && duplicate.PlayerIds.Contains(id))) { bool flag2 = GetPlatformIdentityPriority(duplicate) > GetPlatformIdentityPriority(playerActivityRecord); PlayerActivityRecord target = (flag2 ? duplicate : playerActivityRecord); PlayerActivityRecord duplicate2 = (flag2 ? playerActivityRecord : duplicate); MergePlayerRecordContents(target, duplicate2); state.Players.RemoveAt(flag2 ? i : j); flag = true; break; } } } } while (flag); } private static void BuildPlayerIndexes(AutoArchiveState state, out Dictionary<string, PlayerActivityRecord> playersByPlatform, out Dictionary<long, PlayerActivityRecord> playersById) { playersByPlatform = new Dictionary<string, PlayerActivityRecord>(StringComparer.Ordinal); playersById = new Dictionary<long, PlayerActivityRecord>(); foreach (PlayerActivityRecord player in state.Players) { if (!string.IsNullOrWhiteSpace(player.PlatformId)) { playersByPlatform[player.PlatformId] = player; } foreach (long playerId in player.PlayerIds) { if (playerId != 0L) { playersById[playerId] = player; } } } } private static void ReplaceIndexes(Dictionary<string, PlayerActivityRecord> playersByPlatform, Dictionary<long, PlayerActivityRecord> playersById) { PlayersByPlatform.Clear(); foreach (KeyValuePair<string, PlayerActivityRecord> item in playersByPlatform) { PlayersByPlatform[item.Key] = item.Value; } PlayersById.Clear(); foreach (KeyValuePair<long, PlayerActivityRecord> item2 in playersById) { PlayersById[item2.Key] = item2.Value; } } private static void IndexPlayer(PlayerActivityRecord record) { if (!string.IsNullOrWhiteSpace(record.PlatformId)) { PlayersByPlatform[record.PlatformId] = record; } foreach (long playerId in record.PlayerIds) { if (playerId != 0L) { PlayersById[playerId] = record; } } } private static string UnknownPlatformId(long playerId) { return "unknown:" + playerId; } private static bool IsUnknown(PlayerActivityRecord record) { return record.PlatformId.StartsWith("unknown:", StringComparison.Ordinal); } private static int GetPlatformIdentityPriority(PlayerActivityRecord record) { if (ZoneSaviorSteamIds.TryNormalizePlatformId(record.PlatformId, out string _)) { return 2; } if (!string.IsNullOrWhiteSpace(record.PlatformId) && !IsUnknown(record)) { return 1; } return 0; } private static bool TryReadStateFromDisk(out AutoArchiveState state, out string error) { state = new AutoArchiveState(); error = ""; try { string input = File.ReadAllText(FilePath); state = Deserializer.Deserialize<AutoArchiveState>(input) ?? new AutoArchiveState(); ValidateState(state); return true; } catch (Exception ex) { error = ex.Message; return false; } } private static void ValidateState(AutoArchiveState state) { if (state.Version != 1) { throw new InvalidDataException($"Unsupported activity version {state.Version}."); } if (state.Players == null || state.Runs == null) { throw new InvalidDataException("Activity players or runs list is missing."); } for (int i = 0; i < state.Players.Count; i++) { PlayerActivityRecord playerActivityRecord = state.Players[i]; if (playerActivityRecord == null || playerActivityRecord.PlatformId == null || playerActivityRecord.PlayerIds == null || playerActivityRecord.Names == null) { throw new InvalidDataException($"Activity player record {i} is invalid."); } } for (int j = 0; j < state.Runs.Count; j++) { ArchiveRunRecord archiveRunRecord = state.Runs[j]; if (archiveRunRecord == null || archiveRunRecord.TargetPlayerIds == null || archiveRunRecord.Clusters == null || archiveRunRecord.Messages == null) { throw new InvalidDataException($"Activity run record {j} is invalid."); } for (int k = 0; k < archiveRunRecord.Clusters.Count; k++) { ArchiveClusterRecord archiveClusterRecord = archiveRunRecord.Clusters[k]; if (archiveClusterRecord == null || archiveClusterRecord.Creators == null || archiveClusterRecord.Zones == null || archiveClusterRecord.Zones.Any((ZoneBundleZone zone) => zone == null) || archiveClusterRecord.ResetZones == null || archiveClusterRecord.ResetZones.Any((ZoneBundleZone zone) => zone == null) || archiveClusterRecord.BackupOnlyZones == null || archiveClusterRecord.BackupOnlyZones.Any((ZoneBundleZone zone) => zone == null) || archiveClusterRecord.ResetCompletedZones == null || archiveClusterRecord.ResetCompletedZones.Any((ZoneBundleZone zone) => zone == null) || archiveClusterRecord.ResetSkippedZones == null || archiveClusterRecord.ResetSkippedZones.Any((ZoneBundleZone zone) => zone == null)) { throw new InvalidDataException($"Activity run {j} cluster {k} is invalid."); } } } } private static void AddDistinct<T>(List<T> values, T value) { if (value != null && !values.Contains(value)) { values.Add(value); } } } public static class ZoneSaviorCreatorNameLookupProtocol { public const int Version = 1; public const int RequestBytes = 28; public const int MaxResponseBytes = 512; public const int MaxNameCharacters = 128; public const int StatusSuccess = 0; public const int StatusUnsupportedVersion = 1; public const int StatusMalformedRequest = 2; public const int StatusUnauthorized = 3; public const int StatusRateLimited = 4; public const int StatusWorldMismatch = 5; public const int StatusServerError = 6; public const string RequestRpcName = "sighsorry.ZoneSavior_CreatorNameLookupRequest"; public const string ResponseRpcName = "sighsorry.ZoneSavior_CreatorNameLookupResponse"; } internal static class CreatorNameLookupRpc { private sealed class RequestRateWindow { internal DateTime StartedUtc { get; } internal int RequestCount { get; set; } internal RequestRateWindow(DateTime startedUtc, int requestCount) { StartedUtc = startedUtc; RequestCount = requestCount; } } private const int MaxRequestsPerWindow = 8; private static readonly TimeSpan RateWindowDuration = TimeSpan.FromSeconds(5.0); private static readonly Dictionary<long, RequestRateWindow> RequestWindows = new Dictionary<long, RequestRateWindow>(); private static ZRoutedRpc? _registeredRpc; internal static void Register() { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && instance2 != null && _registeredRpc != instance2) { _registeredRpc = instance2; RequestWindows.Clear(); instance2.Register<ZPackage>("sighsorry.ZoneSavior_CreatorNameLookupRequest", (Action<long, ZPackage>)RPC_HandleRequest); } } internal static void ClearRateLimits() { RequestWindows.Clear(); } private static void RPC_HandleRequest(long sender, ZPackage package) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || ZRoutedRpc.instance == null || !TryReadRequest(package, out var version, out var requestId, out var worldUid, out var playerId)) { return; } bool flag; try { flag = IsAuthorizedSender(sender); } catch { return; } if (!flag) { SendResponse(sender, requestId, worldUid, 3, playerId, found: false, ""); return; } if (!TryConsumeRequest(sender, DateTime.UtcNow)) { SendResponse(sender, requestId, worldUid, 4, playerId, found: false, ""); return; } if (requestId == 0L || playerId == 0L) { SendResponse(sender, requestId, worldUid, 2, playerId, found: false, ""); return; } if (version != 1) { SendResponse(sender, requestId, worldUid, 1, playerId, found: false, ""); return; } long worldUID; try { worldUID = instance.GetWorldUID(); } catch { SendResponse(sender, requestId, worldUid, 6, playerId, found: false, ""); return; } if (worldUid != worldUID) { SendResponse(sender, requestId, worldUid, 5, playerId, found: false, ""); return; } try { string name; string text = (AutoArchiveStore.TryResolveLastKnownPlayerName(playerId, out name) ? NormalizeName(name) : ""); SendResponse(sender, requestId, worldUid, 0, playerId, text.Length > 0, text); } catch (Exception ex) { ZoneSaviorPlugin.ZoneSaviorLogger.LogWarning((object)("Creator name lookup failed: " + ex.Message)); SendResponse(sender, requestId, worldUid, 6, playerId, found: false, ""); } } private static bool TryReadRequest(ZPackage package, out int version, out long requestId, out long worldUid, out long playerId) { version = 0; requestId = 0L; worldUid = 0L; playerId = 0L; if (package == null || package.Size() != 28) { return false; } try { version = package.ReadInt(); requestId = package.ReadLong(); worldUid = package.ReadLong(); playerId = package.ReadLong(); return package.GetPos() == package.Size(); } catch { return false; } } private static bool IsAuthorizedSender(long sender) { if (!((Object)(object)Player.m_localPlayer != (Object)null) || !((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer() || ZDOMan.instance == null || sender != ZDOMan.GetSessionID()) { return ZoneBundleCommands.IsAuthorizedSender(sender); } return true; } private static bool TryConsumeRequest(long sender, DateTime utcNow) { if (!RequestWindows.TryGetValue(sender, out RequestRateWindow value) || utcNow - value.StartedUtc >= RateWindowDuration) { RequestWindows[sender] = new RequestRateWindow(utcNow, 1); PruneExpiredWindows(utcNow); return true; } if (value.RequestCount >= 8) { return false; } value.RequestCount++; return true; } private static void PruneExpiredWindows(DateTime utcNow) { if (RequestWindows.Count <= 32) { return; } List<long> list = new List<long>(); foreach (KeyValuePair<long, RequestRateWindow> requestWindow in RequestWindows) { if (utcNow - requestWindow.Value.StartedUtc >= RateWindowDuration) { list.Add(requestWindow.Key); } } foreach (long item in list) { RequestWindows.Remove(item); } } private static void SendResponse(long target, long requestId, long worldUid, int status, long playerId, bool found, string name) { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || target == 0L) { return; } ZPackage val = CreateResponse(requestId, worldUid, status, playerId, found, name); if (val.Size() > 512) { val = CreateResponse(requestId, worldUid, 6, playerId, found: false, ""); } try { instance.InvokeRoutedRPC(target, "sighsorry.ZoneSavior_CreatorNameLookupResponse", new object[1] { val }); } catch (Exception ex) { ZoneSaviorPlugin.ZoneSaviorLogger.LogDebug((object)("Could not send creator name lookup response: " + ex.Message)); } } private static ZPackage CreateResponse(long requestId, long worldUid, int status, long playerId, bool found, string name) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: 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_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(requestId); val.Write(worldUid); val.Write(status); val.Write(playerId); val.Write(found); val.Write(name); return val; } private static string NormalizeName(string value) { string text = value?.Trim() ?? ""; if (text.Length == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(Math.Min(text.Length, 128)); bool flag = false; string text2 = text; foreach (char c in text2) { char c2 = ((char.IsControl(c) || char.IsWhiteSpace(c)) ? ' ' : c); if (!(c2 == ' ' && flag)) { if (stringBuilder.Length >= 128) { break; } stringBuilder.Append(c2); flag = c2 == ' '; } } if (stringBuilder.Length > 0 && char.IsHighSurrogate(stringBuilder[stringBuilder.Length - 1])) { stringBuilder.Length--; } return stringBuilder.ToString().Trim(); } } internal sealed class BuildCounterHud : MonoBehaviour { private const float FadeDuration = 0.15f; private static BuildCounterHud? _instance; private CanvasGroup? _canvasGroup; private TextMeshProUGUI? _text; private float _hideAt = float.MinValue; public static void ShowCount(int count, int limit) { if (!((Object)(object)Hud.instance == (Object)null)) { EnsureInstance(); _instance?.Show($"{count}/{limit}"); } } private static void EnsureInstance() { if ((Object)(object)Hud.instance == (Object)null) { return; } if ((Object)(object)_instance != (Object)null && Object.op_Implicit((Object)(object)_instance)) { _instance.EnsureElements(); return; } _instance = ((Component)Hud.instance).GetComponent<BuildCounterHud>(); if ((Object)(object)_instance == (Object)null) { _instance = ((Component)Hud.instance).gameObject.AddComponent<BuildCounterHud>(); } _instance.EnsureElements(); } private void Update() { if ((Object)(object)_canvasGroup == (Object)null) { return; } if (Time.unscaledTime <= _hideAt) { _canvasGroup.alpha = 1f; return; } _canvasGroup.alpha = Mathf.MoveTowards(_canvasGroup.alpha, 0f, Time.unscaledDeltaTime / 0.15f); if (_canvasGroup.alpha <= 0f && (Object)(object)_text != (Object)null) { ((TMP_Text)_text).text = string.Empty; } } private void Show(string value) { if (!((Object)(object)_text == (Object)null) && !((Object)(object)_canvasGroup == (Object)null)) { ((TMP_Text)_text).text = value; _canvasGroup.alpha = 1f; _hideAt = Time.unscaledTime + ClientConfig.CounterVisibleSeconds; } } private void EnsureElements() { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Expected O, but got Unknown //IL_009a: 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) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing