using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Threading; using System.Threading.Tasks; using AsyncSave.Config; using AsyncSave.Patches; using AsyncSave.Profile; using AsyncSave.Snapshot; using AsyncSave.Util; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("AsyncSave")] [assembly: AssemblyDescription("Zero-allocation asynchronous world save for Valheim")] [assembly: AssemblyProduct("AsyncSave")] [assembly: ComVisible(false)] [assembly: Guid("A7C1B3D2-4E5F-4A6B-8C7D-9E0F1A2B3C4D")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 AsyncSave { [BepInPlugin("MidnightsFX.AsyncSave", "AsyncSave", "0.5.0")] [BepInIncompatibility("org.bepinex.plugins.smoothsave")] public class AsyncSavePlugin : BaseUnityPlugin { public const string PluginGUID = "MidnightsFX.AsyncSave"; public const string PluginName = "AsyncSave"; public const string PluginVersion = "0.5.0"; private Harmony _harmony; public void Awake() { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Expected O, but got Unknown Log.Source = ((BaseUnityPlugin)this).Logger; Settings.Bind(((BaseUnityPlugin)this).Config); if (!BitConverter.IsLittleEndian) { Settings.BlockWorldSave(); Settings.BlockCharacterSave(); Log.Error("AsyncSave: big-endian runtime detected. Async save disabled - the snapshot serializer would produce an unloadable world file."); } _ = Utils.persistantDataPath; if (!ProfileFormat.Verify()) { Settings.BlockCharacterSave(); } SaveIo.Start(); _harmony = new Harmony("MidnightsFX.AsyncSave"); _harmony.PatchAll(Assembly.GetExecutingAssembly()); VerifyRevisionHooks(); Log.Info("AsyncSave 0.5.0 loaded."); } private void VerifyRevisionHooks() { string missing = null; NoteIfUnhooked(AccessTools.DeclaredMethod(typeof(ZDO), "IncreaseDataRevision", (Type[])null, (Type[])null), "ZDO.IncreaseDataRevision", ref missing); NoteIfUnhooked(AccessTools.DeclaredMethod(typeof(ZDO), "Deserialize", (Type[])null, (Type[])null), "ZDO.Deserialize", ref missing); if (missing != null) { Settings.ForceFullScanReconcile(); Log.Error("AsyncSave: the ZDO revision hook on " + missing + " is not attached. Dirty reconciliation cannot see mid-snapshot mutations without it and would save them one revision stale, so 'Reconcile mutations' has been forced to FullScan (correct, but it walks the whole world every save). This means either the game renamed the method or the mod was built without Patches/ZdoRevisionPatches.cs."); } } private static void NoteIfUnhooked(MethodBase target, string label, ref string missing) { bool flag = false; Patches val = ((target == null) ? null : Harmony.GetPatchInfo(target)); if (val != null) { foreach (Patch postfix in val.Postfixes) { if (!(postfix.owner != "MidnightsFX.AsyncSave")) { flag = true; break; } } } if (!flag) { missing = ((missing == null) ? label : (missing + " and " + label)); } } public void Update() { ProfileSave.Pump(); DeadZdoPruner.Pump(); } public void OnDestroy() { ProfileSave.DrainToDisk(); SaveIo.Stop(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } namespace AsyncSave.Util { internal static class Log { internal static ManualLogSource Source; public static void Info(string msg) { ManualLogSource source = Source; if (source != null) { source.LogInfo((object)msg); } } public static void Warn(string msg) { ManualLogSource source = Source; if (source != null) { source.LogWarning((object)msg); } } public static void Error(string msg) { ManualLogSource source = Source; if (source != null) { source.LogError((object)msg); } } public static void Debug(string msg) { ManualLogSource source = Source; if (source != null) { source.LogDebug((object)msg); } } } } namespace AsyncSave.Snapshot { internal static class ChunkPartition { private static readonly int[] s_count0 = new int[4096]; private static readonly int[] s_count1 = new int[1024]; private static readonly int[] s_count2 = new int[256]; private static readonly int[] s_count3 = new int[64]; private const int MaxZdosPerBiggerChunk = 100000; private const int Blocked = -100000000; public static void Compute(ZDOMan zdoMan, List selected, out int totalChunkFiles, out int selectedZdoCount) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) selected.Clear(); List[] objectsBySector = zdoMan.m_objectsBySector; int[] array = s_count0; int[] array2 = s_count1; int[] array3 = s_count2; int[] array4 = s_count3; Array.Clear(array, 0, array.Length); Array.Clear(array2, 0, array2.Length); Array.Clear(array3, 0, array3.Length); Array.Clear(array4, 0, array4.Length); int num = 0; for (uint num2 = 0u; num2 < 512; num2 += 8) { for (uint num3 = 0u; num3 < 512; num3 += 8) { int num4 = 0; for (uint num5 = 0u; num5 < 8; num5++) { for (uint num6 = 0u; num6 < 8; num6++) { List list = objectsBySector[ZoneSystem.IndicesToIndex(num3 + num6, num2 + num5).Sector]; if (list != null) { num4 += list.Count; } } } array[num] = num4; num++; } } Coarsen(zdoMan, 64u, 1, array, array2); Coarsen(zdoMan, 32u, 2, array2, array3); Coarsen(zdoMan, 16u, 3, array3, array4); totalChunkFiles = NonEmpty(array) + NonEmpty(array2) + NonEmpty(array3) + NonEmpty(array4); HashSet dirty = zdoMan.m_dirtyChunks[0]; long num7 = 0L; num7 += Select(64, 0, array, dirty, selected); num7 += Select(32, 1, array2, dirty, selected); num7 += Select(16, 2, array3, dirty, selected); num7 += Select(8, 3, array4, dirty, selected); selectedZdoCount = (int)((num7 > int.MaxValue) ? int.MaxValue : num7); } private static void Coarsen(ZDOMan zdoMan, uint size, byte chunkSize, int[] fine, int[] coarse) { byte b = (byte)(chunkSize - 1); uint num = 64 / size; ChunkSaveMapping chunkSaveMapping = zdoMan.m_chunkSaveMapping; int num2 = 0; if (chunkSaveMapping != null) { for (uint num3 = 0u; num3 < size; num3 += 2) { for (uint num4 = 0u; num4 < size; num4 += 2) { uint num5 = num4 * num; uint num6 = num3 * num; if (chunkSaveMapping.GetNumZDOs(num5, num6, b) + chunkSaveMapping.GetNumZDOs(num5 + num, num6, b) + chunkSaveMapping.GetNumZDOs(num5, num6 + num, b) + chunkSaveMapping.GetNumZDOs(num5 + num, num6 + num, b) > 0) { coarse[num2] = -100000000; } num2++; } } } num2 = 0; for (uint num7 = 0u; num7 < size; num7 += 2) { uint num8 = num7 * size; for (uint num9 = 0u; num9 < size; num9 += 2) { if (coarse[num2] >= 0) { int num10 = fine[num8 + num9] + fine[1 + num8 + num9] + fine[size + num8 + num9] + fine[size + 1 + num8 + num9]; if (num9 != 0 && num7 != 0 && num10 > 0 && num10 < 100000) { coarse[num2] = num10; fine[num8 + num9] = 0; fine[num8 + num9 + 1] = 0; fine[num8 + size + num9] = 0; fine[num8 + size + num9 + 1] = 0; } else { coarse[num2] = -100000000; } } num2++; } } } private static long Select(int size, byte chunkSize, int[] counts, HashSet dirty, List selected) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) int num = 64 / size; long num2 = 0L; for (int i = 0; i < counts.Length; i++) { if (counts[i] > 0) { int num3 = i % size * num; uint num4 = (uint)(i / size * num); ChunkIndex item = ZoneSystem.ChunkIndexFromXY((uint)num3, num4, chunkSize); if (dirty.Contains(item)) { selected.Add(item); num2 += counts[i]; } } } return num2; } private static int NonEmpty(int[] counts) { int num = 0; for (int i = 0; i < counts.Length; i++) { if (counts[i] > 0) { num++; } } return num; } public static void ZoneBounds(ChunkIndex index, out int x0, out int y0, out int span) { //IL_0000: 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) (int, int) zoneFromChunk = ZoneSystem.GetZoneFromChunk(index); x0 = zoneFromChunk.Item1; y0 = zoneFromChunk.Item2; span = 8 * (1 << (int)index.m_chunkSize); } } internal struct SliceBudget { private const int PollMask = 31; private const float Alpha = 0.25f; private const float MinBudgetMs = 0.25f; private const float DefaultTargetFrameMs = 16.666666f; private const int MaxPerSlice = 50000; private long _budgetTicks; private int _maxPerSlice; private int _inSlice; private long _sliceStart; private long _worstTicks; private int _blocks; private bool _adaptive; private float _maxBudgetMs; private float _headroom; private float _capTargetMs; private float _targetFrameMs; private float _baseFrameEmaMs; private float _lastSliceMs; private float _lastBudgetMs; private float _ticksPerMs; public float WorstBlockMs => (float)_worstTicks / _ticksPerMs; public int Blocks => _blocks; public float BaseFrameMs { get { if (!(_baseFrameEmaMs < 0f)) { return _baseFrameEmaMs; } return 0f; } } public float TargetFrameMs => _targetFrameMs; public float CapTargetMs => _capTargetMs; public float LastBudgetMs => _lastBudgetMs; public static SliceBudget Create(int budgetMs, bool adaptive, float headroom, bool unsliced) { long num = (unsliced ? long.MaxValue : ((long)((double)Stopwatch.Frequency * ((double)budgetMs / 1000.0)))); if (num < 1) { num = 1L; } float num2 = DetectTargetFrameMs(); return new SliceBudget { _budgetTicks = num, _maxPerSlice = (unsliced ? int.MaxValue : 50000), _sliceStart = Stopwatch.GetTimestamp(), _adaptive = (adaptive && !unsliced), _maxBudgetMs = budgetMs, _headroom = headroom, _capTargetMs = num2, _targetFrameMs = num2, _baseFrameEmaMs = -1f, _lastBudgetMs = budgetMs, _ticksPerMs = (float)Stopwatch.Frequency / 1000f }; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Step() { int num = ++_inSlice; if (num >= _maxPerSlice) { return true; } if ((num & 0x1F) != 0) { return false; } return Stopwatch.GetTimestamp() - _sliceStart >= _budgetTicks; } public void EndSlice() { long num = Stopwatch.GetTimestamp() - _sliceStart; if (num > _worstTicks) { _worstTicks = num; } _lastSliceMs = (float)num / _ticksPerMs; _blocks++; } public void BeginSlice() { if (_adaptive) { float num = Time.unscaledDeltaTime * 1000f - _lastSliceMs; if (num < 0f) { num = 0f; } _baseFrameEmaMs = ((_baseFrameEmaMs < 0f) ? num : (_baseFrameEmaMs + (num - _baseFrameEmaMs) * 0.25f)); float num2 = _baseFrameEmaMs * (1f + _headroom); if (num2 > _capTargetMs) { num2 = _capTargetMs; } _targetFrameMs = num2; float num3 = num2 - _baseFrameEmaMs; if (num3 > _maxBudgetMs) { num3 = _maxBudgetMs; } if (num3 < 0.25f) { num3 = 0.25f; } _lastBudgetMs = num3; _budgetTicks = (long)(num3 * _ticksPerMs); if (_budgetTicks < 1) { _budgetTicks = 1L; } } _inSlice = 0; _sliceStart = Stopwatch.GetTimestamp(); } private static float DetectTargetFrameMs() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) int targetFrameRate = Application.targetFrameRate; if (targetFrameRate > 0) { return 1000f / (float)targetFrameRate; } int vSyncCount = QualitySettings.vSyncCount; if (vSyncCount > 0) { Resolution currentResolution = Screen.currentResolution; RefreshRate refreshRateRatio = ((Resolution)(ref currentResolution)).refreshRateRatio; double value = ((RefreshRate)(ref refreshRateRatio)).value; if (value > 1.0) { return (float)(1000.0 * (double)vSyncCount / value); } } return 16.666666f; } } internal sealed class SnapshotBuffer { public const int BlockBytes = 67108864; public byte[] Data; public int Position; public int Block; private readonly List _blocks = new List(); private long _committed; public long TotalBytes => _committed + Position; public long Capacity { get { long num = 0L; for (int i = 0; i < _blocks.Count; i++) { num += _blocks[i].Length; } return num; } } public int BlockCount => _blocks.Count; public SnapshotBuffer(int initialCapacity) { _blocks.Add(new byte[initialCapacity]); Data = _blocks[0]; } public byte[] BlockAt(int index) { return _blocks[index]; } public void Reset() { Block = 0; Position = 0; _committed = 0L; Data = _blocks[0]; } public void Release(int capacity) { _blocks.Clear(); _blocks.Add(new byte[capacity]); Reset(); } public void EnsureCapacity(long bytes) { if (Position != 0 || Block != 0) { return; } if (bytes <= 67108864) { if (_blocks[0].Length < bytes) { _blocks[0] = new byte[(int)bytes]; } } else { if (_blocks[0].Length < 67108864) { _blocks[0] = new byte[67108864]; } int num = (int)((bytes + 67108864 - 1) / 67108864); while (_blocks.Count < num) { _blocks.Add(new byte[67108864]); } } Data = _blocks[0]; } public void EnsureFree(int additional) { if ((long)Position + (long)additional <= Data.Length) { return; } _committed += Position; Block++; int num = ((additional > 67108864) ? additional : 67108864); if (Block < _blocks.Count) { if (_blocks[Block].Length < num) { _blocks[Block] = new byte[num]; } } else { _blocks.Add(new byte[num]); } Data = _blocks[Block]; Position = 0; } public void Seek(int block, int offset) { Block = block; Position = offset; Data = _blocks[block]; } } internal static class SnapshotCoroutine { private static Coroutine _current; private static float _triggerFrameMs; private const int MaxCatchUpRounds = 4; private static readonly HashSet _portalPrefabs = new HashSet(); public static Task Start(ZDOMan zdoMan) { long timestamp = Stopwatch.GetTimestamp(); TaskCompletionSource taskCompletionSource = (SnapshotState.Task = new TaskCompletionSource()); SnapshotState.ResetForNewSnapshot(); SnapshotState.InProgress = true; _current = ((MonoBehaviour)ZNet.instance).StartCoroutine(Guarded(Run(zdoMan, taskCompletionSource), taskCompletionSource)); _triggerFrameMs = (float)(Stopwatch.GetTimestamp() - timestamp) * 1000f / (float)Stopwatch.Frequency; return taskCompletionSource.Task; } private static IEnumerator Guarded(IEnumerator walk, TaskCompletionSource tcs) { while (true) { object current; try { if (!walk.MoveNext()) { break; } current = walk.Current; goto IL_007c; } catch (Exception arg) { Log.Error($"AsyncSave: snapshot walk threw, falling back to the vanilla save path. {arg}"); SnapshotState.InProgress = false; SnapshotState.ReadyForWrite = false; ZoneSnapshot.Reset(); RestoreVanillaPrepare(ZDOMan.instance); tcs.TrySetResult(result: true); _current = null; yield break; } IL_007c: yield return current; } if (!tcs.Task.IsCompleted) { tcs.TrySetResult(result: true); } } private static void RestoreVanillaPrepare(ZDOMan zdoMan) { try { RollbackSaveState(zdoMan); zdoMan.PrepareSave(); ZoneSystem.instance.PrepareSave(); RandEventSystem.instance.PrepareSave(); PersistentEventSystem.instance.PrepareSave(); } catch (Exception arg) { Log.Error($"AsyncSave: could not hand the save back to vanilla's PrepareSave. {arg}"); } } private static void RollbackSaveState(ZDOMan zdoMan) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) if (zdoMan != null && (int)zdoMan.m_currentSaveState != 0) { HashSet[] dirtyChunks = zdoMan.m_dirtyChunks; dirtyChunks[0].UnionWith(dirtyChunks[1]); dirtyChunks[1].Clear(); bool[] dirtyPortalObjects = zdoMan.m_dirtyPortalObjects; ref bool reference = ref dirtyPortalObjects[0]; reference |= dirtyPortalObjects[1]; dirtyPortalObjects[1] = false; zdoMan.m_currentSaveState = (SaveState)0; zdoMan.m_endSave = null; } } public static void Abort(string reason) { if (SnapshotState.InProgress || SnapshotState.ReadyForWrite || ZoneSnapshot.Ready || !SnapshotState.Task.Task.IsCompleted) { SnapshotState.InProgress = false; SnapshotState.ReadyForWrite = false; ZoneSnapshot.Reset(); RollbackSaveState(ZDOMan.instance); TaskCompletionSource task = SnapshotState.Task; if (!task.Task.IsCompleted) { task.TrySetResult(result: false); } if (_current != null) { ((MonoBehaviour)ZNet.instance).StopCoroutine(_current); _current = null; } Log.Info("AsyncSave snapshot aborted: " + reason); } } private static IEnumerator Run(ZDOMan zdoMan, TaskCompletionSource tcs) { yield return null; LogLevel log = Settings.Logging.Value; bool timings = Settings.LogTimings; Stopwatch totalStopwatch = Stopwatch.StartNew(); bool verify = false; int value = Settings.SliceBudgetMs.Value; bool value2 = Settings.AdaptiveBudget.Value; float headroom = (float)Settings.FrameHeadroomPercent.Value / 100f; ReconcileMode reconMode = Settings.Reconcile; SliceBudget slice = SliceBudget.Create(value, value2, headroom, verify); ZDOExtraData.RegenerateConnectionHashData(); long connMs = totalStopwatch.ElapsedMilliseconds; List selected = SnapshotState.Selected; ChunkPartition.Compute(zdoMan, selected, out var totalChunkFiles, out var selectedZdoCount); bool writePortalChunk = zdoMan.m_dirtyPortalObjects[zdoMan.m_currentSaveState] && zdoMan.m_portalObjects.Count > 0; SnapshotState.TotalChunkFiles = totalChunkFiles + (writePortalChunk ? 1 : 0); SaveData val = new SaveData(); List>> objectsByChunk = val.m_objectsByChunk; for (int i = 0; i < selected.Count; i++) { objectsByChunk.Add(new Tuple>(selected[i], new List())); } if (writePortalChunk) { objectsByChunk.Add(new Tuple>(ZoneSystem.ChunkPortal, new List())); } val.m_numFiles = SnapshotState.TotalChunkFiles + 4; val.m_dirtyFiles = objectsByChunk.Count + 4; zdoMan.m_saveData = val; zdoMan.m_currentSaveState = (SaveState)1; int serialized = 0; int expectedZdos = EstimateZdoCount(zdoMan, selectedZdoCount, writePortalChunk); SnapshotState.Visited.EnsureCapacity(expectedZdos); if (!verify) { slice.EndSlice(); yield return null; slice.BeginSlice(); } SnapshotState.EnsureEntryCapacity(expectedZdos); if (!verify) { slice.EndSlice(); yield return null; slice.BeginSlice(); } long bytes = (long)((double)expectedZdos * (double)SnapshotState.BytesPerZdoEma * 1.15); SnapshotState.Buffer.EnsureCapacity(bytes); if (!verify) { slice.EndSlice(); yield return null; slice.BeginSlice(); } long presizeMs = totalStopwatch.ElapsedMilliseconds - connMs; _portalPrefabs.Clear(); List portalPrefabHash = Game.instance.PortalPrefabHash; for (int j = 0; j < portalPrefabHash.Count; j++) { _portalPrefabs.Add(portalPrefabHash[j]); } List scratch = SnapshotState.SectorScratch; List[] sectors = zdoMan.m_objectsBySector; int c = 0; int y0; int span; while (c < selected.Count) { int slot = SnapshotState.BeginChunk(selected[c]); ChunkPartition.ZoneBounds(selected[c], out var x0, out y0, out span); int num; for (int zy = y0; zy < y0 + span; zy = num) { for (int zx = x0; zx < x0 + span; zx = num) { List list = sectors[ZoneSystem.SectorToIndex(zx, zy).Sector]; if (list != null && list.Count != 0) { scratch.Clear(); scratch.AddRange(list); int i2 = 0; while (i2 < scratch.Count) { i2 = SerializeRange(scratch, i2, ref slice, ref serialized); if (i2 < scratch.Count) { slice.EndSlice(); yield return null; slice.BeginSlice(); } } } num = zx + 1; } num = zy + 1; } SnapshotState.EndChunk(slot); num = c + 1; c = num; } long chunkMs = totalStopwatch.ElapsedMilliseconds; int chunkBlocks = slice.Blocks; int portalSlot = -1; if (writePortalChunk) { portalSlot = SnapshotState.BeginChunk(ZoneSystem.ChunkPortal); scratch.Clear(); foreach (KeyValuePair> portalObject in zdoMan.m_portalObjects) { List value3 = portalObject.Value; if (value3 != null) { scratch.AddRange(value3); } } c = 0; while (c < scratch.Count) { c = SerializePortalRange(scratch, c, ref slice, ref serialized); if (c < scratch.Count) { slice.EndSlice(); yield return null; slice.BeginSlice(); } } SnapshotState.EndChunk(portalSlot); } scratch.Clear(); long reconStartMs = totalStopwatch.ElapsedMilliseconds; int reconStartBlocks = slice.Blocks; int reconciledMutations = 0; int appendedAdds = 0; int dirtyExamined = 0; int catchUpExamined = 0; List dirtyScratch; if (reconMode == ReconcileMode.Dirty || verify) { HashSet dirty = SnapshotState.PendingDirty; dirtyScratch = SnapshotState.DirtyScratch; c = 0; while (c <= 4 && dirty.Count > 0) { dirtyScratch.Clear(); dirtyScratch.AddRange(dirty); dirty.Clear(); dirtyExamined += dirtyScratch.Count; if (c == 4) { catchUpExamined = dirtyScratch.Count; } span = 0; while (span < dirtyScratch.Count) { span = ReconcileDirtyRange(zdoMan, dirtyScratch, span, ref slice, ref reconciledMutations); if (span < dirtyScratch.Count) { slice.EndSlice(); yield return null; slice.BeginSlice(); } } int num = c + 1; c = num; } dirtyScratch.Clear(); } if (reconMode == ReconcileMode.FullScan || verify) { c = reconciledMutations; span = SnapshotState.EntryCount; y0 = 0; while (y0 < span) { y0 = ReconcileRange(zdoMan, y0, ref slice, ref reconciledMutations); if (y0 < span) { slice.EndSlice(); yield return null; slice.BeginSlice(); } } if (verify) { _ = reconciledMutations; _ = c; } } dirtyScratch = SnapshotState.PendingAdds; y0 = 0; span = 0; while (span < 4 && y0 < dirtyScratch.Count) { c = dirtyScratch.Count; while (y0 < c) { y0 = AppendAddsRange(zdoMan, y0, c, portalSlot, ref slice, ref appendedAdds); if (y0 < c) { slice.EndSlice(); yield return null; slice.BeginSlice(); } } int num = span + 1; span = num; } int num2 = dirtyScratch.Count - y0; SnapshotState.Overflow.Sort(); long num3 = totalStopwatch.ElapsedMilliseconds - reconStartMs; int num4 = slice.Blocks - reconStartBlocks + 1; long elapsedMilliseconds = totalStopwatch.ElapsedMilliseconds; ZoneSnapshot.Capture(ZoneSystem.instance); long num5 = totalStopwatch.ElapsedMilliseconds - elapsedMilliseconds; RandEventSystem.instance.PrepareSave(); PersistentEventSystem.instance.PrepareSave(); ZNet.instance.m_saveThreadStartTime = Time.realtimeSinceStartup; slice.EndSlice(); totalStopwatch.Stop(); SnapshotState.InProgress = false; SnapshotState.ReadyForWrite = true; ZoneSnapshot.Ready = true; SnapshotState.RecordBytesPerZdo(SnapshotState.Buffer.TotalBytes, serialized); _current = null; tcs.TrySetResult(result: true); if (num2 > 0) { Log.Warn($"AsyncSave: {num2} sector entries arrived faster than the adds pass " + $"could drain them and were left unexamined after {4} rounds. Most " + "will be objects already in this save; a few may be one save behind. Raise the slice budget if this recurs."); } if (timings) { Log.Info($"AsyncSave phases: connHash={connMs}ms presize={presizeMs}ms " + $"chunkWalk={chunkMs - connMs - presizeMs}ms/{chunkBlocks}blocks " + $"portals={reconStartMs - chunkMs}ms " + $"reconcile+adds={num3}ms/{num4}blocks (mode={reconMode} dirty={dirtyExamined} catchUp={catchUpExamined}) " + $"zoneCapture={num5}ms (zones={ZoneSnapshot.ZoneCount} locations={ZoneSnapshot.LocationCount}) " + $"total={totalStopwatch.ElapsedMilliseconds}ms triggerFrame={_triggerFrameMs:0.0}ms " + $"worstBlock={slice.WorstBlockMs:0.00}ms blocks={slice.Blocks} " + $"target={slice.TargetFrameMs:0.0}ms cap={slice.CapTargetMs:0.0}ms " + $"budget={slice.LastBudgetMs:0.00}ms headroom={Settings.FrameHeadroomPercent.Value}% " + $"baseFrame={slice.BaseFrameMs:0.0}ms"); } int num6 = SnapshotState.EntryCount - SnapshotState.PendingRemoves.Count; if (log == LogLevel.Simple) { Log.Info($"AsyncSave snapshot: {num6} ZDOs across {SnapshotState.ChunkCount} dirty " + $"chunk(s) of {SnapshotState.TotalChunkFiles} in {totalStopwatch.ElapsedMilliseconds} ms " + $"over {slice.Blocks} frames (worst block: {slice.WorstBlockMs:0.00} ms)."); } else if (log >= LogLevel.Detailed) { Log.Info($"AsyncSave snapshot: serialized={serialized} reconciled={reconciledMutations} " + $"adds={appendedAdds} removes={SnapshotState.PendingRemoves.Count} kept={num6} " + $"chunks={SnapshotState.ChunkCount}/{SnapshotState.TotalChunkFiles} " + $"overflow={SnapshotState.Overflow.Count} " + $"buffer={SnapshotState.Buffer.TotalBytes} bytes cap={SnapshotState.Buffer.Capacity} " + $"bufBlocks={SnapshotState.Buffer.BlockCount} " + $"bytesPerZdo={SnapshotState.BytesPerZdoEma:0.0} " + $"total={totalStopwatch.ElapsedMilliseconds}ms recon={num3}ms " + $"blocks={slice.Blocks} worstBlock={slice.WorstBlockMs:0.00}ms"); } } private static int EstimateZdoCount(ZDOMan zdoMan, int selectedZdos, bool portals) { long num = selectedZdos; if (portals) { foreach (KeyValuePair> portalObject in zdoMan.m_portalObjects) { if (portalObject.Value != null) { num += portalObject.Value.Count; } } } num += 1024; if (num <= int.MaxValue) { return (int)num; } return int.MaxValue; } private static bool AppendEntry(ZDO live, VisitedSet visited, SnapshotBuffer buffer, ref SnapshotEntry[] entries, ref int entryCount) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) if (!visited.Add(live.m_uid, entryCount)) { return false; } int block; int offset; int length = ZdoSerializer.Write(live, buffer, out block, out offset); if (entryCount == entries.Length) { entries = SnapshotState.GrowEntries(entryCount); } entries[entryCount].Uid = live.m_uid; entries[entryCount].Block = block; entries[entryCount].Offset = offset; entries[entryCount].Length = length; entries[entryCount].DataRevision = live.DataRevision; entryCount++; return true; } private static int SerializeRange(List scratch, int from, ref SliceBudget slice, ref int serialized) { VisitedSet visited = SnapshotState.Visited; SnapshotBuffer buffer = SnapshotState.Buffer; SnapshotEntry[] entries = SnapshotState.Entries; HashSet portalPrefabs = _portalPrefabs; int count = scratch.Count; int num = serialized; int entryCount = SnapshotState.EntryCount; for (int i = from; i < count; i++) { if (slice.Step()) { serialized = num; SnapshotState.EntryCount = entryCount; return i; } ZDO val = scratch[i]; if (val.Persistent && !portalPrefabs.Contains(val.GetPrefab()) && AppendEntry(val, visited, buffer, ref entries, ref entryCount)) { num++; } } serialized = num; SnapshotState.EntryCount = entryCount; return count; } private static int SerializePortalRange(List scratch, int from, ref SliceBudget slice, ref int serialized) { VisitedSet visited = SnapshotState.Visited; SnapshotBuffer buffer = SnapshotState.Buffer; SnapshotEntry[] entries = SnapshotState.Entries; int count = scratch.Count; int num = serialized; int entryCount = SnapshotState.EntryCount; for (int i = from; i < count; i++) { if (slice.Step()) { serialized = num; SnapshotState.EntryCount = entryCount; return i; } if (AppendEntry(scratch[i], visited, buffer, ref entries, ref entryCount)) { num++; } } serialized = num; SnapshotState.EntryCount = entryCount; return count; } private static int AppendAddsRange(ZDOMan zdoMan, int from, int n, int portalSlot, ref SliceBudget slice, ref int appended) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005d: 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_0094: Unknown result type (might be due to invalid IL or missing references) List pendingAdds = SnapshotState.PendingAdds; HashSet pendingRemoves = SnapshotState.PendingRemoves; VisitedSet visited = SnapshotState.Visited; SnapshotBuffer buffer = SnapshotState.Buffer; SnapshotEntry[] entries = SnapshotState.Entries; HashSet portalPrefabs = _portalPrefabs; int num = appended; int entryCount = SnapshotState.EntryCount; for (int i = from; i < n; i++) { if (slice.Step()) { appended = num; SnapshotState.EntryCount = entryCount; return i; } ZDOID val = pendingAdds[i]; if (pendingRemoves.Contains(val)) { continue; } ZDO zDO = zdoMan.GetZDO(val); if (zDO == null) { continue; } int num2; if (portalPrefabs.Contains(zDO.GetPrefab())) { num2 = portalSlot; } else { if (!zDO.Persistent) { continue; } num2 = SnapshotState.SlotForSector(zDO.GetSectorIndex()); } if (num2 >= 0) { int entryIndex = entryCount; if (AppendEntry(zDO, visited, buffer, ref entries, ref entryCount)) { SnapshotState.AddOverflow(num2, entryIndex); num++; } } } appended = num; SnapshotState.EntryCount = entryCount; return n; } private static int ReconcileDirtyRange(ZDOMan zdoMan, List dirty, int from, ref SliceBudget slice, ref int reconciled) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) SnapshotEntry[] entries = SnapshotState.Entries; SnapshotBuffer buffer = SnapshotState.Buffer; VisitedSet visited = SnapshotState.Visited; HashSet pendingRemoves = SnapshotState.PendingRemoves; int count = dirty.Count; int num = reconciled; for (int i = from; i < count; i++) { if (slice.Step()) { reconciled = num; return i; } ZDOID val = dirty[i]; if ((pendingRemoves.Count > 0 && pendingRemoves.Contains(val)) || !visited.TryGetIndex(val, out var index)) { continue; } ZDO zDO = zdoMan.GetZDO(val); if (zDO != null) { SnapshotEntry e = entries[index]; if (zDO.DataRevision != e.DataRevision) { RewriteEntry(zDO, buffer, ref e); entries[index] = e; num++; } } } reconciled = num; return count; } private static int ReconcileRange(ZDOMan zdoMan, int from, ref SliceBudget slice, ref int reconciled) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) SnapshotEntry[] entries = SnapshotState.Entries; SnapshotBuffer buffer = SnapshotState.Buffer; int entryCount = SnapshotState.EntryCount; int num = reconciled; for (int i = from; i < entryCount; i++) { if (slice.Step()) { reconciled = num; return i; } SnapshotEntry e = entries[i]; ZDO zDO = zdoMan.GetZDO(e.Uid); if (zDO != null && zDO.DataRevision != e.DataRevision) { RewriteEntry(zDO, buffer, ref e); entries[i] = e; num++; } } reconciled = num; return entryCount; } private static void RewriteEntry(ZDO live, SnapshotBuffer buffer, ref SnapshotEntry e) { int block = buffer.Block; int position = buffer.Position; buffer.Seek(e.Block, e.Offset); int block2; int offset; int num = ZdoSerializer.Write(live, buffer, out block2, out offset, e.Length); buffer.Seek(block, position); if (num < 0) { e.Length = ZdoSerializer.Write(live, buffer, out block2, out offset); e.Block = block2; e.Offset = offset; } else { e.Length = num; } e.DataRevision = live.DataRevision; } } internal static class SnapshotState { private const int SeedBufferBytes = 4194304; private const int SeedEntries = 65536; private const int SeedVisited = 65536; private const int SeedChunks = 256; public static readonly SnapshotBuffer Buffer = new SnapshotBuffer(4194304); public static SnapshotEntry[] Entries = new SnapshotEntry[65536]; public static int EntryCount; public static ChunkRange[] ChunkRanges = new ChunkRange[256]; public static int ChunkCount; public static readonly Dictionary SelectedSlot = new Dictionary(); public static readonly List Overflow = new List(256); public static int TotalChunkFiles; public static readonly List PendingAdds = new List(1024); public static readonly HashSet PendingRemoves = new HashSet(ZdoIdComparer.Instance); public static readonly HashSet PendingDirty = new HashSet(ZdoIdComparer.Instance); public static readonly VisitedSet Visited = new VisitedSet(65536); public static readonly List SectorScratch = new List(4096); public static readonly List DirtyScratch = new List(1024); public static readonly List Selected = new List(256); public static volatile bool InProgress; public static volatile bool ReadyForWrite; public static TaskCompletionSource Task = CompletedSource(); public static float BytesPerZdoEma = 48f; private const int MaxEntries = 89478485; public static void RecordBytesPerZdo(long bytes, int count) { if (count > 0) { float num = (float)((double)bytes / (double)count); BytesPerZdoEma += (num - BytesPerZdoEma) * 0.5f; } } public static void SignalNoSnapshot() { Task = CompletedSource(); } private static TaskCompletionSource CompletedSource() { TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); taskCompletionSource.SetResult(result: true); return taskCompletionSource; } public static void ResetForNewSnapshot() { ReadyForWrite = false; ZoneSnapshot.Reset(); Buffer.Reset(); EntryCount = 0; ChunkCount = 0; TotalChunkFiles = 0; SelectedSlot.Clear(); Overflow.Clear(); Selected.Clear(); PendingAdds.Clear(); PendingRemoves.Clear(); PendingDirty.Clear(); Visited.Clear(); SectorScratch.Clear(); DirtyScratch.Clear(); } public static void ReleasePools() { Buffer.Release(4194304); Entries = new SnapshotEntry[65536]; EntryCount = 0; ChunkRanges = new ChunkRange[256]; ChunkCount = 0; Visited.Release(65536); ZoneSnapshot.ReleasePools(); SelectedSlot.Clear(); Overflow.Clear(); Overflow.TrimExcess(); Selected.Clear(); Selected.TrimExcess(); PendingAdds.Clear(); PendingAdds.TrimExcess(); PendingRemoves.Clear(); PendingDirty.Clear(); SectorScratch.Clear(); SectorScratch.TrimExcess(); DirtyScratch.Clear(); DirtyScratch.TrimExcess(); } public static void EnsureEntryCapacity(int expected) { if (expected > Entries.Length) { Entries = new SnapshotEntry[(expected > 89478485) ? 89478485 : expected]; } } public static SnapshotEntry[] GrowEntries(int count) { long num = (long)Entries.Length + (long)(Entries.Length >> 1) + 1024; if (num > 89478485) { num = 89478485L; } if (num <= count) { throw new InvalidOperationException($"AsyncSave: the snapshot needs more than {89478485} ZDO entries, which is as " + "many as one array can hold. Set 'World save' to false to use the vanilla save."); } SnapshotEntry[] array = new SnapshotEntry[num]; Array.Copy(Entries, array, count); Entries = array; return array; } public static int BeginChunk(ChunkIndex index) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) if (ChunkCount == ChunkRanges.Length) { ChunkRange[] array = new ChunkRange[ChunkRanges.Length * 2]; Array.Copy(ChunkRanges, array, ChunkCount); ChunkRanges = array; } int num = ChunkCount++; ChunkRanges[num].Index = index; ChunkRanges[num].Start = EntryCount; ChunkRanges[num].Count = 0; SelectedSlot[index] = num; return num; } public static void EndChunk(int slot) { ChunkRanges[slot].Count = EntryCount - ChunkRanges[slot].Start; } public static void AddOverflow(int slot, int entryIndex) { Overflow.Add(((long)slot << 32) | (uint)entryIndex); } public static int SlotForSector(SectorIndex sectorIndex) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) ChunkIndex zonesChunk = ZoneSystem.GetZonesChunk(sectorIndex); if (SelectedSlot.TryGetValue(zonesChunk, out var value)) { return value; } for (byte b = 1; b <= 3; b++) { ChunkIndex key = ZoneSystem.ChunkIndexFromIndexAndSize(zonesChunk, b); if (SelectedSlot.TryGetValue(key, out value)) { return value; } } return -1; } } internal struct ChunkRange { public ChunkIndex Index; public int Start; public int Count; } internal struct SnapshotEntry { public ZDOID Uid; public int Block; public int Offset; public int Length; public uint DataRevision; } internal sealed class ZdoIdComparer : IEqualityComparer { public static readonly ZdoIdComparer Instance = new ZdoIdComparer(); private ZdoIdComparer() { } public bool Equals(ZDOID a, ZDOID b) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) return a == b; } public int GetHashCode(ZDOID id) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return Hash(id); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Hash(ZDOID id) { return (int)((((ZDOID)(ref id)).ID * 397) ^ ((ZDOID)(ref id)).UserKey); } } internal sealed class VisitedSet { private struct Slot { public uint Stamp; public ZDOID Key; public int Index; } private const int SegShift = 22; private const int SegSlots = 4194304; private const int SegMask = 4194303; private const int MaxSlots = 1073741824; private Slot[][] _segs; private int _mask; private int _count; private int _growAt; private uint _stamp; public int Count => _count; public int Capacity => _mask + 1; public VisitedSet(int capacity) { Release(capacity); } public void Release(int capacity) { int num = 16; while (num < capacity && num < 1073741824) { num <<= 1; } Allocate(num); } public void Clear() { _count = 0; if (++_stamp == 0) { for (int i = 0; i < _segs.Length; i++) { Array.Clear(_segs[i], 0, _segs[i].Length); } _stamp = 1u; } } public void EnsureCapacity(int expected) { long num = (long)expected * 4L / 3 + 1024; if (num > _mask + 1) { long num2 = _mask + 1; while (num2 < num && num2 < 1073741824) { num2 <<= 1; } Allocate((int)num2); } } public bool Add(ZDOID uid, int index) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) uint stamp = _stamp; int num = Hash(uid) & _mask; while (true) { Slot[] array = _segs[num >> 22]; int num2 = num & 0x3FFFFF; if (array[num2].Stamp != stamp) { array[num2].Stamp = stamp; array[num2].Key = uid; array[num2].Index = index; if (++_count >= _growAt) { Grow(); } return true; } if (array[num2].Key == uid) { break; } num = (num + 1) & _mask; } return false; } public bool TryGetIndex(ZDOID uid, out int index) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) uint stamp = _stamp; int num = Hash(uid) & _mask; Slot[] array; int num2; while (true) { array = _segs[num >> 22]; num2 = num & 0x3FFFFF; if (array[num2].Stamp != stamp) { index = -1; return false; } if (array[num2].Key == uid) { break; } num = (num + 1) & _mask; } index = array[num2].Index; return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Hash(ZDOID uid) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return ZdoIdComparer.Hash(uid); } private void Allocate(int cap) { if (cap <= 4194304) { _segs = new Slot[1][]; _segs[0] = new Slot[cap]; } else { _segs = new Slot[cap / 4194304][]; for (int i = 0; i < _segs.Length; i++) { _segs[i] = new Slot[4194304]; } } _mask = cap - 1; _growAt = cap - (cap >> 2); _count = 0; _stamp = 1u; } private void Grow() { //IL_0075: 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) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) int num = _mask + 1; if (num >= 1073741824) { return; } Slot[][] segs = _segs; uint stamp = _stamp; Allocate(num << 1); _stamp = stamp; int mask = _mask; Slot[][] segs2 = _segs; int num2 = 0; foreach (Slot[] array in segs) { for (int j = 0; j < array.Length; j++) { if (array[j].Stamp == stamp) { ZDOID key = array[j].Key; int num3 = Hash(key) & mask; while (segs2[num3 >> 22][num3 & 0x3FFFFF].Stamp == stamp) { num3 = (num3 + 1) & mask; } Slot[] obj = segs2[num3 >> 22]; int num4 = num3 & 0x3FFFFF; obj[num4].Stamp = stamp; obj[num4].Key = key; obj[num4].Index = array[j].Index; num2++; } } } _count = num2; } } internal static class ZdoSerializer { public const int MinBytesPerZdo = 10; private const int FlagConn = 1; private const int FlagAnyExtra = 255; private const int FlagRotation = 4096; private const int FlagSmallPos = 8192; private const int DataFlagsTypeMask = 3; private const int DataFlagsPersistDistantMask = 12; public unsafe static int Write(ZDO zdo, SnapshotBuffer buf, out int block, out int offset, int maxAllowed = int.MaxValue) { //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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0046: 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_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Invalid comparison between Unknown and I4 //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected I4, but got Unknown //IL_0373: Unknown result type (might be due to invalid IL or missing references) //IL_037f: Unknown result type (might be due to invalid IL or missing references) //IL_038b: Unknown result type (might be due to invalid IL or missing references) //IL_0355: Unknown result type (might be due to invalid IL or missing references) //IL_0361: Unknown result type (might be due to invalid IL or missing references) //IL_03af: Unknown result type (might be due to invalid IL or missing references) //IL_03cd: Unknown result type (might be due to invalid IL or missing references) //IL_03d3: Expected I4, but got Unknown //IL_049a: Unknown result type (might be due to invalid IL or missing references) //IL_049f: Unknown result type (might be due to invalid IL or missing references) //IL_04a5: Unknown result type (might be due to invalid IL or missing references) //IL_04b1: Unknown result type (might be due to invalid IL or missing references) //IL_04be: Unknown result type (might be due to invalid IL or missing references) //IL_0527: Unknown result type (might be due to invalid IL or missing references) //IL_052c: Unknown result type (might be due to invalid IL or missing references) //IL_0532: Unknown result type (might be due to invalid IL or missing references) //IL_053e: Unknown result type (might be due to invalid IL or missing references) //IL_054b: Unknown result type (might be due to invalid IL or missing references) //IL_0558: Unknown result type (might be due to invalid IL or missing references) block = buf.Block; offset = buf.Position; ZDOID uid = zdo.m_uid; ZDOExtraData.s_floats.TryGetValue(uid, out var value); ZDOExtraData.s_vec3.TryGetValue(uid, out var value2); ZDOExtraData.s_quats.TryGetValue(uid, out var value3); ZDOExtraData.s_ints.TryGetValue(uid, out var value4); ZDOExtraData.s_longs.TryGetValue(uid, out var value5); ZDOExtraData.s_strings.TryGetValue(uid, out var value6); ZDOExtraData.s_byteArrays.TryGetValue(uid, out var value7); ZDOExtraData.s_connectionsHashData.TryGetValue(uid, out var value8); HashSet s_sessionOnly = ZDOExtraData.s_sessionOnly; bool flag = s_sessionOnly.Count > 0; int num = Kept(value, s_sessionOnly, flag); int num2 = Kept(value2, s_sessionOnly, flag); int num3 = Kept(value3, s_sessionOnly, flag); int num4 = Kept(value4, s_sessionOnly, flag); int num5 = Kept(value5, s_sessionOnly, flag); int num6 = Kept(value6, s_sessionOnly, flag); int num7 = Kept(value7, s_sessionOnly, flag); bool flag2 = value8 != null && (int)value8.m_type > 0; Vector3 rotation = zdo.m_rotation; bool flag3 = !Utils.CloseToZero(rotation); Vector3 position = zdo.m_position; ValueTuple valueTuple = Utils.SmallPosition(position); bool item = valueTuple.Item1; Vector2s item2 = valueTuple.Item2; int num8 = (int)zdo.m_dataFlags; int num9 = (int)((flag2 ? 1u : 0u) | (uint)((num > 0) ? 2 : 0) | (uint)((num2 > 0) ? 4 : 0) | (uint)((num3 > 0) ? 8 : 0) | (uint)((num4 > 0) ? 16 : 0) | (uint)((num5 > 0) ? 32 : 0) | (uint)((num6 > 0) ? 64 : 0) | (uint)((num7 > 0) ? 128 : 0) | (uint)((num8 & 0xC) << 6) | (uint)((num8 & 3) << 10) | (uint)(flag3 ? 4096 : 0)) | (item ? 8192 : 0); int num10 = 2 + (item ? 4 : 12) + 4 + (flag3 ? 4 : 0) + (flag2 ? 5 : 0) + ((num > 0) ? (2 + num * 8) : 0) + ((num2 > 0) ? (2 + num2 * 16) : 0) + ((num3 > 0) ? (2 + num3 * 20) : 0) + ((num4 > 0) ? (2 + num4 * 8) : 0) + ((num5 > 0) ? (2 + num5 * 12) : 0); if (num6 > 0) { num10 += 2 + num6 * 9; int[] keys = value6.m_keys; string[] values = value6.m_values; int count = value6.Count; for (int i = 0; i < count; i++) { if (!flag || !s_sessionOnly.Contains(keys[i])) { string text = values[i]; if (text != null) { num10 += text.Length * 3; } } } } if (num7 > 0) { num10 += 2 + num7 * 8; int[] keys2 = value7.m_keys; byte[][] values2 = value7.m_values; int count2 = value7.Count; for (int j = 0; j < count2; j++) { if (!flag || !s_sessionOnly.Contains(keys2[j])) { byte[] array = values2[j]; if (array != null) { num10 += array.Length; } } } } if (num10 > maxAllowed) { return -1; } buf.EnsureFree(num10); byte[] data = buf.Data; int position2 = buf.Position; block = buf.Block; offset = position2; fixed (byte* ptr = data) { byte* ptr2 = ptr + position2; *(ushort*)ptr2 = (ushort)num9; ptr2 += 2; if (item) { *(short*)ptr2 = item2.x; ((short*)ptr2)[1] = item2.y; ptr2 += 4; } else { *(float*)ptr2 = position.x; ((float*)ptr2)[1] = position.y; ((float*)ptr2)[2] = position.z; ptr2 += 12; } *(int*)ptr2 = zdo.m_prefab; ptr2 += 4; if (flag3) { ptr2 = WriteSmallRotation(ptr2, rotation); } if ((num9 & 0xFF) != 0) { if (flag2) { *ptr2 = (byte)(int)value8.m_type; ptr2++; *(int*)ptr2 = value8.m_hash; ptr2 += 4; } if (num > 0) { ptr2 = WriteNumItems(ptr2, num); int[] keys3 = value.m_keys; float[] values3 = value.m_values; int count3 = value.Count; for (int k = 0; k < count3; k++) { if (!flag || !s_sessionOnly.Contains(keys3[k])) { *(int*)ptr2 = keys3[k]; ((float*)ptr2)[1] = values3[k]; ptr2 += 8; } } } if (num2 > 0) { ptr2 = WriteNumItems(ptr2, num2); int[] keys4 = value2.m_keys; Vector3[] values4 = value2.m_values; int count4 = value2.Count; for (int l = 0; l < count4; l++) { if (!flag || !s_sessionOnly.Contains(keys4[l])) { *(int*)ptr2 = keys4[l]; Vector3 val = values4[l]; ((float*)ptr2)[1] = val.x; ((float*)ptr2)[2] = val.y; ((float*)ptr2)[3] = val.z; ptr2 += 16; } } } if (num3 > 0) { ptr2 = WriteNumItems(ptr2, num3); int[] keys5 = value3.m_keys; Quaternion[] values5 = value3.m_values; int count5 = value3.Count; for (int m = 0; m < count5; m++) { if (!flag || !s_sessionOnly.Contains(keys5[m])) { *(int*)ptr2 = keys5[m]; Quaternion val2 = values5[m]; ((float*)ptr2)[1] = val2.x; ((float*)ptr2)[2] = val2.y; ((float*)ptr2)[3] = val2.z; ((float*)ptr2)[4] = val2.w; ptr2 += 20; } } } if (num4 > 0) { ptr2 = WriteNumItems(ptr2, num4); int[] keys6 = value4.m_keys; int[] values6 = value4.m_values; int count6 = value4.Count; for (int n = 0; n < count6; n++) { if (!flag || !s_sessionOnly.Contains(keys6[n])) { *(int*)ptr2 = keys6[n]; ((int*)ptr2)[1] = values6[n]; ptr2 += 8; } } } if (num5 > 0) { ptr2 = WriteNumItems(ptr2, num5); int[] keys7 = value5.m_keys; long[] values7 = value5.m_values; int count7 = value5.Count; for (int num11 = 0; num11 < count7; num11++) { if (!flag || !s_sessionOnly.Contains(keys7[num11])) { *(int*)ptr2 = keys7[num11]; *(long*)(ptr2 + 4) = values7[num11]; ptr2 += 12; } } } if (num6 > 0) { ptr2 = WriteNumItems(ptr2, num6); int[] keys8 = value6.m_keys; string[] values8 = value6.m_values; int count8 = value6.Count; for (int num12 = 0; num12 < count8; num12++) { if (!flag || !s_sessionOnly.Contains(keys8[num12])) { *(int*)ptr2 = keys8[num12]; ptr2 += 4; ptr2 = WriteString(ptr2, values8[num12] ?? string.Empty); } } } if (num7 > 0) { ptr2 = WriteNumItems(ptr2, num7); int[] keys9 = value7.m_keys; byte[][] values9 = value7.m_values; int count9 = value7.Count; for (int num13 = 0; num13 < count9; num13++) { if (!flag || !s_sessionOnly.Contains(keys9[num13])) { *(int*)ptr2 = keys9[num13]; ptr2 += 4; byte[] array2 = values9[num13]; int num14 = (*(int*)ptr2 = ((array2 != null) ? array2.Length : 0)); ptr2 += 4; for (int num15 = 0; num15 < num14; num15++) { ptr2[num15] = array2[num15]; } ptr2 += num14; } } } } int num16 = (int)(ptr2 - (ptr + position2)); buf.Position = position2 + num16; return num16; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Kept(BinarySearchDictionary d, HashSet sessionOnly, bool anySession) { if (d == null) { return 0; } int count = d.Count; if (!anySession || count == 0) { return count; } int[] keys = d.m_keys; int num = 0; for (int i = 0; i < count; i++) { if (!sessionOnly.Contains(keys[i])) { num++; } } return num; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe static byte* WriteSmallRotation(byte* p, Vector3 v3) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) v3 *= 2f; uint num = (uint)v3.x; uint num2 = (uint)v3.y; uint num3 = (uint)v3.z; if ((num <= 1 || num >= 719) && (num3 <= 1 || num3 >= 719)) { num2 |= 0x8000; *(ushort*)p = (ushort)num2; return p + 2; } uint num4 = num | (num2 << 10) | (num3 << 20); *(ushort*)p = (ushort)(num4 >> 16); ((short*)p)[1] = (short)(ushort)num4; return p + 4; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private unsafe static byte* WriteNumItems(byte* p, int numItems) { if (numItems < 128) { *p = (byte)numItems; return p + 1; } *p = (byte)((numItems >> 8) | 0x80); p[1] = (byte)numItems; return p + 2; } private unsafe static byte* WriteString(byte* p, string s) { int length = s.Length; if (length == 0) { *p = 0; return p + 1; } fixed (char* ptr = s) { if (length < 128) { byte* ptr2 = p + 1; int i; for (i = 0; i < length; i++) { char c = ptr[i]; if (c >= '\u0080') { break; } ptr2[i] = (byte)c; } if (i == length) { *p = (byte)length; return p + 1 + length; } } int byteCount = Encoding.UTF8.GetByteCount(ptr, length); uint num; for (num = (uint)byteCount; num >= 128; num >>= 7) { *(p++) = (byte)(num | 0x80); } *(p++) = (byte)num; p += Encoding.UTF8.GetBytes(ptr, length, p, byteCount); return p; } } } internal static class ZoneSnapshot { public static Vector2s[] Zones = (Vector2s[])(object)new Vector2s[16384]; public static int ZoneCount; public static LocationInstance[] Locations = (LocationInstance[])(object)new LocationInstance[8192]; public static int LocationCount; public static string[] GlobalKeys = new string[64]; public static int GlobalKeyCount; public static string[] FilteredKeys = new string[64]; public static bool LocationsGenerated; public static volatile bool Ready; public static void Capture(ZoneSystem zs) { HashSet generatedZones = zs.m_generatedZones; Grow(ref Zones, generatedZones.Count); generatedZones.CopyTo(Zones, 0); ZoneCount = generatedZones.Count; Dictionary locationInstances = zs.m_locationInstances; Grow(ref Locations, locationInstances.Count); locationInstances.Values.CopyTo(Locations, 0); LocationCount = locationInstances.Count; HashSet globalKeys = zs.m_globalKeys; Grow(ref GlobalKeys, globalKeys.Count); globalKeys.CopyTo(GlobalKeys, 0); GlobalKeyCount = globalKeys.Count; Grow(ref FilteredKeys, globalKeys.Count); LocationsGenerated = zs.LocationsGenerated; } public static void Reset() { Ready = false; } public static void ReleasePools() { Ready = false; Zones = (Vector2s[])(object)new Vector2s[16384]; ZoneCount = 0; Locations = (LocationInstance[])(object)new LocationInstance[8192]; LocationCount = 0; GlobalKeys = new string[64]; GlobalKeyCount = 0; FilteredKeys = new string[64]; } private static void Grow(ref T[] arr, int need) { if (arr.Length < need) { int num; for (num = ((arr.Length < 16) ? 16 : arr.Length); num < need; num *= 2) { } arr = new T[num]; } } } } namespace AsyncSave.Profile { internal static class MapSnapshot { private static byte[] _payload = new byte[0]; private static int _payloadLength; private static int[] _exploredBits = new int[0]; private static int[] _exploredOthersBits = new int[0]; private static int _bitCount; private static int _textureSize; private static bool _publicPosition; private static readonly MemoryStream _pinBlock = new MemoryStream(1024); private static int _pinLength; private static readonly UTF8Encoding PinEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public static bool HasCapture { get; private set; } public static void Reset() { HasCapture = false; _payloadLength = 0; } public static void Capture(Minimap map) { //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected I4, but got Unknown int num = (_bitCount = map.m_explored.Length); _textureSize = map.m_textureSize; int num2 = (num + 31) / 32; if (_exploredBits.Length < num2) { _exploredBits = new int[num2]; } if (_exploredOthersBits.Length < num2) { _exploredOthersBits = new int[num2]; } map.m_explored.CopyTo(_exploredBits, 0); map.m_exploredOthers.CopyTo(_exploredOthersBits, 0); _pinBlock.SetLength(0L); _pinBlock.Position = 0L; using (BinaryWriter binaryWriter = new BinaryWriter(_pinBlock, PinEncoding, leaveOpen: true)) { List pins = map.m_pins; int num3 = 0; for (int i = 0; i < pins.Count; i++) { if (pins[i].m_save) { num3++; } } binaryWriter.Write(num3); for (int j = 0; j < pins.Count; j++) { PinData val = pins[j]; if (val.m_save) { binaryWriter.Write(val.m_name); binaryWriter.Write(val.m_pos.x); binaryWriter.Write(val.m_pos.y); binaryWriter.Write(val.m_pos.z); binaryWriter.Write((int)val.m_type); binaryWriter.Write(val.m_checked); binaryWriter.Write(val.m_ownerID); binaryWriter.Write(((object)Unsafe.As(ref val.m_author)/*cast due to .constrained prefix*/).ToString()); } } binaryWriter.Flush(); } _pinLength = (int)_pinBlock.Length; _publicPosition = ZNet.instance.IsReferencePositionPublic(); HasCapture = true; } public static byte[] Compress() { BuildPayload(); byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionLevel.Fastest)) { gZipStream.Write(_payload, 0, _payloadLength); } array = memoryStream.ToArray(); } byte[] array2 = new byte[8 + array.Length]; WriteInt(array2, 0, 8); WriteInt(array2, 4, array.Length); Buffer.BlockCopy(array, 0, array2, 8, array.Length); return array2; } private static void BuildPayload() { int bitCount = _bitCount; int num = 4 + bitCount + bitCount + _pinLength + 1; if (_payload.Length < num) { _payload = new byte[num]; } byte[] payload = _payload; int num2 = 0; WriteInt(payload, num2, _textureSize); num2 += 4; Expand(_exploredBits, payload, num2, bitCount); num2 += bitCount; Expand(_exploredOthersBits, payload, num2, bitCount); num2 += bitCount; Buffer.BlockCopy(_pinBlock.GetBuffer(), 0, payload, num2, _pinLength); num2 += _pinLength; payload[num2++] = (_publicPosition ? ((byte)1) : ((byte)0)); _payloadLength = num2; } private static void Expand(int[] bits, byte[] dest, int offset, int count) { int num = count >> 5; int num2 = offset; for (int i = 0; i < num; i++) { int num3 = bits[i]; for (int j = 0; j < 32; j++) { dest[num2 + j] = (byte)((num3 >> j) & 1); } num2 += 32; } int num4 = count & 0x1F; if (num4 != 0) { int num5 = bits[num]; for (int k = 0; k < num4; k++) { dest[num2 + k] = (byte)((num5 >> k) & 1); } } } private static void WriteInt(byte[] buf, int offset, int value) { buf[offset] = (byte)value; buf[offset + 1] = (byte)(value >> 8); buf[offset + 2] = (byte)(value >> 16); buf[offset + 3] = (byte)(value >> 24); } } internal static class ProfileFormat { public const int ProfileVersion = 46; public const int PlayerStatCount = 205; public const int DifficultySlots = 10; public const int EnemyStatArrays = 5; public const int GlobalKeyCount = 53; public const int MapVersion = 8; public static bool Verify() { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(PlayerProfile), "SavePlayerToDisk", (Type[])null, (Type[])null); if (methodInfo == null) { Log.Error("AsyncSave: PlayerProfile.SavePlayerToDisk not found. Async character save disabled."); return false; } MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(ZPackage), "Write", new Type[1] { typeof(int) }, (Type[])null); if (methodInfo2 == null) { Log.Error("AsyncSave: ZPackage.Write(int) not found. Async character save disabled."); return false; } MethodInfo methodInfo3 = AccessTools.DeclaredMethod(typeof(Minimap), "GetMapData", (Type[])null, (Type[])null); if (methodInfo3 == null) { Log.Error("AsyncSave: Minimap.GetMapData not found. Async character save disabled."); return false; } if (!FirstWriteIntLiterals(methodInfo, methodInfo2, 3, "PlayerProfile.SavePlayerToDisk", out var values)) { return false; } if (!FirstWriteIntLiterals(methodInfo3, methodInfo2, 1, "Minimap.GetMapData", out var values2)) { return false; } int num = values[0]; int num2 = values[1]; int num3 = values[2]; int num4 = values2[0]; if (num != 46 || num2 != 205 || num3 != 10) { Log.Error("AsyncSave: character save format looks different from what this build supports " + $"(found version={num} statCount={num2} difficultySlots={num3}, expected " + $"{46}/{205}/{10}). Async character save " + "disabled; vanilla character saving is untouched."); return false; } if (num4 != 8) { Log.Error($"AsyncSave: Minimap.GetMapData writes map version {num4}, expected " + $"{8}. The map serialization format changed. Async character save disabled."); return false; } return true; } private static bool FirstWriteIntLiterals(MethodBase method, MethodInfo writeInt, int count, string what, out int[] values) { values = null; List list; try { list = new List(PatchProcessor.GetOriginalInstructions(method, (ILGenerator)null)); } catch (Exception ex) { Log.Error("AsyncSave: could not read " + what + " IL (" + ex.Message + "). Async character save disabled."); return false; } int[] array = new int[count]; int num = 0; for (int i = 1; i < list.Count; i++) { if (num >= count) { break; } if (CodeInstructionExtensions.Calls(list[i], writeInt) && TryGetInt32(list[i - 1], out var value)) { array[num++] = value; } } if (num < count) { Log.Error($"AsyncSave: expected {count} leading ZPackage.Write(int) literals in {what} " + $"but found {num}. Async character save disabled."); return false; } values = array; return true; } private static bool TryGetInt32(CodeInstruction ins, out int value) { value = 0; OpCode opcode = ins.opcode; if (opcode == OpCodes.Ldc_I4) { value = (int)ins.operand; return true; } if (opcode == OpCodes.Ldc_I4_S) { value = Convert.ToInt32(ins.operand); return true; } if (opcode == OpCodes.Ldc_I4_0) { value = 0; return true; } if (opcode == OpCodes.Ldc_I4_1) { value = 1; return true; } if (opcode == OpCodes.Ldc_I4_2) { value = 2; return true; } if (opcode == OpCodes.Ldc_I4_3) { value = 3; return true; } if (opcode == OpCodes.Ldc_I4_4) { value = 4; return true; } if (opcode == OpCodes.Ldc_I4_5) { value = 5; return true; } if (opcode == OpCodes.Ldc_I4_6) { value = 6; return true; } if (opcode == OpCodes.Ldc_I4_7) { value = 7; return true; } if (opcode == OpCodes.Ldc_I4_8) { value = 8; return true; } return false; } } internal enum ProfileSaveStage { Idle, Compressing, ReadyToBuild, Writing, Done } internal static class ProfileSave { private static int _stage; public static bool AsyncRequested; public static bool ForceSync; public static PlayerProfile Profile; public static byte[] MapData; public static bool HadMapCapture; public static ProfileSaveStage Stage { get { return (ProfileSaveStage)Volatile.Read(in _stage); } set { Volatile.Write(ref _stage, (int)value); } } public static bool Active => Stage != ProfileSaveStage.Idle; public static bool Enabled { get { if (Settings.CharacterSaveOn) { return !ForceSync; } return false; } } public static void Reset() { Stage = ProfileSaveStage.Idle; Profile = null; MapData = null; HadMapCapture = false; MapSnapshot.Reset(); } public static void Pump() { switch (Stage) { case ProfileSaveStage.ReadyToBuild: BuildAndQueueWrite(); break; case ProfileSaveStage.Done: Finish(); break; } } private static void BuildAndQueueWrite() { PlayerProfile profile = Profile; if (profile == null) { Reset(); return; } if (HadMapCapture && MapData != null) { profile.SetMapData(MapData); MapData = null; } Stage = ProfileSaveStage.Writing; AsyncRequested = true; bool flag; try { flag = profile.Save(); } catch (Exception arg) { Log.Error($"AsyncSave: character save failed during package build: {arg}"); flag = false; } finally { AsyncRequested = false; } if (!flag) { Stage = ProfileSaveStage.Done; } } private static void Finish() { Reset(); PlayerProfile.SavingFinished?.Invoke(); } public static void DrainToDisk() { if (!Active) { SaveIo.Drain(); return; } Stopwatch stopwatch = Stopwatch.StartNew(); int i; for (i = 0; i < 10000; i++) { if (!Active) { break; } if (!SaveIo.Drain()) { break; } if (Stage == ProfileSaveStage.ReadyToBuild) { BuildAndQueueWrite(); continue; } if (Stage == ProfileSaveStage.Done) { Reset(); break; } if (!SaveIo.Busy && Stage == ProfileSaveStage.Writing) { break; } } bool num = i >= 10000; if (num) { Log.Error($"AsyncSave: character save did not settle after {i} drain rounds " + $"({stopwatch.ElapsedMilliseconds} ms, stage {Stage}). Abandoning it so shutdown can " + "continue - the character file may be a save behind."); } SaveIo.Drain(); Reset(); if (!num && Settings.Logging.Value != LogLevel.Off) { Log.Info($"AsyncSave: landed the in-flight character save in {stopwatch.ElapsedMilliseconds} ms " + $"over {i + 1} round(s) before shutdown."); } } } internal static class SaveIo { public static readonly object Gate = new object(); private static readonly Queue _queue = new Queue(); private static readonly object _queueLock = new object(); private static Thread _worker; private static bool _running; private static int _pending; private const int DrainTimeoutMs = 10000; public static bool Busy => Volatile.Read(in _pending) > 0; public static void Start() { if (_worker == null) { _running = true; _worker = new Thread(WorkerLoop) { Name = "AsyncSave.SaveIo", IsBackground = true }; _worker.Start(); } } public static void Stop() { if (_worker != null) { Drain(); lock (_queueLock) { _running = false; Monitor.PulseAll(_queueLock); } _worker.Join(5000); _worker = null; } } public static void Enqueue(Action job) { Interlocked.Increment(ref _pending); lock (_queueLock) { _queue.Enqueue(job); Monitor.Pulse(_queueLock); } } public static bool Drain() { if (Volatile.Read(in _pending) == 0) { return true; } Stopwatch stopwatch = Stopwatch.StartNew(); while (Volatile.Read(in _pending) > 0) { if (stopwatch.ElapsedMilliseconds >= 10000) { Log.Error($"AsyncSave: character save worker did not finish within {10000} ms " + $"({Volatile.Read(in _pending)} job(s) outstanding). Giving up rather than " + "hanging shutdown - the character file may be a save behind."); return false; } Thread.Sleep(1); } if (Settings.Logging.Value >= LogLevel.Detailed) { Log.Info($"AsyncSave: drained the character save worker in {stopwatch.ElapsedMilliseconds} ms."); } return true; } private static void WorkerLoop() { while (true) { Action action; lock (_queueLock) { while (_running && _queue.Count == 0) { Monitor.Wait(_queueLock); } if (!_running && _queue.Count == 0) { break; } action = _queue.Dequeue(); } try { lock (Gate) { action(); } } catch (Exception arg) { Log.Error($"AsyncSave: character save job failed: {arg}"); } finally { Interlocked.Decrement(ref _pending); } } } } } namespace AsyncSave.Patches { internal static class DeadZdoPruner { private struct Entry { public ZDOID Uid; public long Ticks; } [HarmonyPatch(typeof(ZDOMan), "HandleDestroyedZDO")] internal static class HandleDestroyedZdoPrunerPatch { [HarmonyPostfix] private static void Postfix(ZDOMan __instance, ZDOID uid) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (__instance.m_deadZDOs.TryGetValue(uid, out var value)) { Record(uid, value); } } } [HarmonyPatch(typeof(ZDOMan), "Load")] internal static class ZdoManLoadPrunerPatch { [HarmonyPostfix] private static void Postfix() { Clear(); } } [HarmonyPatch(typeof(ZDOMan), "LoadChunks")] internal static class ZdoManLoadChunksPrunerPatch { [HarmonyPostfix] private static void Postfix() { Clear(); } } [HarmonyPatch(typeof(ZDOMan), "ShutDown")] internal static class ZdoManShutDownPrunerPatch { [HarmonyPostfix] private static void Postfix() { Clear(); } } private static readonly Queue _queue = new Queue(); private const int MaxRemovalsPerPump = 2000; private const int MaxRemovalsDuringSnapshot = 256; private static bool _warnedBackwards; public static void Record(ZDOID uid, long ticks) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) _queue.Enqueue(new Entry { Uid = uid, Ticks = ticks }); } public static void Clear() { _queue.Clear(); _warnedBackwards = false; } public static void Pump() { //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) if (_queue.Count == 0 || !Settings.DeadZdoPruning.Value) { return; } ZDOMan instance = ZDOMan.instance; ZNet instance2 = ZNet.instance; if (instance == null || (Object)(object)instance2 == (Object)null) { return; } long ticks = instance2.GetTime().Ticks; long num = (long)Settings.DeadZdoTtlSeconds.Value * 10000000L; Dictionary deadZDOs = instance.m_deadZDOs; int num2 = (SnapshotState.InProgress ? 256 : 2000); int num3 = 0; int num4 = 0; while (_queue.Count > 0 && num4 < num2) { Entry entry = _queue.Peek(); long num5 = ticks - entry.Ticks; if (num5 < 0) { if (!_warnedBackwards) { _warnedBackwards = true; Log.Warn("AsyncSave: network time moved backwards, so dead-ZDO pruning is paused until it catches up. Harmless - this is vanilla behaviour - but the dead-ZDO table will grow in the meantime."); } return; } if (num5 <= num) { break; } _queue.Dequeue(); num4++; if (deadZDOs.TryGetValue(entry.Uid, out var value) && value == entry.Ticks) { deadZDOs.Remove(entry.Uid); num3++; } } if (num3 > 0 && Settings.LogTimings) { Log.Debug($"AsyncSave dead-ZDO prune: removed={num3} remaining={deadZDOs.Count} " + $"queued={_queue.Count} ttl={Settings.DeadZdoTtlSeconds.Value}s"); } } } [HarmonyPatch(typeof(ZDOMan), "SaveChunks")] internal static class SaveChunksPatch { private struct RunWriter { private readonly BinaryWriter _out; private readonly SnapshotBuffer _buffer; private int _block; private int _offset; private int _length; public long Written; public RunWriter(BinaryWriter output, SnapshotBuffer buffer) { _out = output; _buffer = buffer; _block = 0; _offset = 0; _length = 0; Written = 0L; } public void Add(SnapshotEntry e) { if (_length > 0 && e.Block == _block && e.Offset == _offset + _length) { _length += e.Length; return; } Flush(); _block = e.Block; _offset = e.Offset; _length = e.Length; } public void Break() { Flush(); } public void Flush() { if (_length != 0) { _out.Write(_buffer.BlockAt(_block), _offset, _length); Written += _length; _length = 0; } } } internal static int LastKeptCount; internal static long LastZdoBytes; internal static int LastChunkCount; internal static bool LastWasSnapshot; internal static void ResetWriteStats() { LastKeptCount = 0; LastZdoBytes = 0L; LastChunkCount = 0; LastWasSnapshot = false; } private static bool Prefix(ZDOMan __instance, string path, FileSource fileSource, ref bool __result) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (!SnapshotState.ReadyForWrite) { return true; } SnapshotState.ReadyForWrite = false; __result = WriteAll(__instance, path, fileSource); return false; } private static bool WriteAll(ZDOMan zdoMan, string path, FileSource fileSource) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) ChunkSaveMapping val = (zdoMan.m_chunkSaveMappingCurrent = zdoMan.m_chunkSaveMapping.Clone()); int chunkCount = SnapshotState.ChunkCount; ChunkRange[] chunkRanges = SnapshotState.ChunkRanges; for (int i = 0; i < chunkCount; i++) { val.CreateOrUpdate(chunkRanges[i].Index); } SnapshotEntry[] entries = SnapshotState.Entries; HashSet pendingRemoves = SnapshotState.PendingRemoves; List overflow = SnapshotState.Overflow; SnapshotBuffer buffer = SnapshotState.Buffer; bool anyRemoves = pendingRemoves.Count > 0; int num = 0; long num2 = 0L; int j = 0; bool flag = true; for (int k = 0; k < chunkCount; k++) { ChunkRange range = chunkRanges[k]; int ofStart = j; for (; j < overflow.Count && (int)(overflow[j] >> 32) == k; j++) { } int ofEnd = j; string chunkFilename = val.GetChunkFilename(range.Index); if (string.IsNullOrEmpty(chunkFilename)) { Log.Error($"AsyncSave: no filename for chunk {range.Index.Chunk} size " + $"{range.Index.m_chunkSize}. Aborting the world save so the partial " + "set of chunk files is rolled back rather than committed."); flag = false; break; } int num3 = CountKept(entries, pendingRemoves, anyRemoves, range, overflow, ofStart, ofEnd); flag = WriteChunk(path + chunkFilename, fileSource, entries, pendingRemoves, anyRemoves, buffer, range, overflow, ofStart, ofEnd, num3, out var written); val.Get(range.Index).m_numZDOs = num3; num += num3; num2 += written; if (!flag) { break; } } LastKeptCount = num; LastZdoBytes = num2; LastChunkCount = chunkCount; LastWasSnapshot = true; if (Settings.Logging.Value >= LogLevel.Detailed) { Log.Info($"AsyncSave wrote {num} ZDOs into {chunkCount} chunk file(s) " + $"({num2} bytes, {pendingRemoves.Count} skipped)."); } if (flag) { return val.Save(path, fileSource); } return false; } private static int CountKept(SnapshotEntry[] entries, HashSet removes, bool anyRemoves, ChunkRange range, List overflow, int ofStart, int ofEnd) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) int result = range.Count + (ofEnd - ofStart); if (!anyRemoves) { return result; } int num = 0; int num2 = range.Start + range.Count; for (int i = range.Start; i < num2; i++) { if (!removes.Contains(entries[i].Uid)) { num++; } } for (int j = ofStart; j < ofEnd; j++) { if (!removes.Contains(entries[(int)overflow[j]].Uid)) { num++; } } return num; } private static bool WriteChunk(string fullPath, FileSource fileSource, SnapshotEntry[] entries, HashSet removes, bool anyRemoves, SnapshotBuffer buffer, ChunkRange range, List overflow, int ofStart, int ofEnd, int kept, out long written) { //IL_0006: 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_00ab: Unknown result type (might be due to invalid IL or missing references) written = 0L; SaveWrite val = SaveFileHelper.CreateZPackageForWriting(fullPath, fileSource); ZPackage pkg = val.Pkg; pkg.Write((short)WorldVersion.Current); pkg.Write(kept); BinaryWriter writer = pkg.m_writer; RunWriter runWriter = new RunWriter(writer, buffer); int num = range.Start + range.Count; for (int i = range.Start; i < num; i++) { SnapshotEntry e = entries[i]; if (anyRemoves && removes.Contains(e.Uid)) { runWriter.Break(); } else { runWriter.Add(e); } } for (int j = ofStart; j < ofEnd; j++) { SnapshotEntry e2 = entries[(int)overflow[j]]; if (anyRemoves && removes.Contains(e2.Uid)) { runWriter.Break(); } else { runWriter.Add(e2); } } runWriter.Flush(); written = runWriter.Written; return val.Finish(); } } internal static class WorldVersion { public static readonly int Current = Resolve(); private static int Resolve() { int num = 41; try { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(Version), "c_WorldVersion"); if (fieldInfo == null) { return num; } int num2 = (int)fieldInfo.GetRawConstantValue(); if (num2 != num) { Log.Warn($"AsyncSave: the game's world save version is {num2}, but this build was " + $"compiled against {num}. Writing {num2}, which is what vanilla writes. " + "Update the mod if anything about the save format looks wrong."); } return num2; } catch (Exception ex) { Log.Warn("AsyncSave: could not read Version.c_WorldVersion (" + ex.Message + "); " + $"using the compiled value {num}."); return num; } } } [HarmonyPatch(typeof(Game), "SavePlayerProfile")] internal static class SavePlayerProfilePatch { private static bool Prefix(Game __instance, bool setLogoutPoint) { //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Invalid comparison between Unknown and I4 //IL_00e0: Unknown result type (might be due to invalid IL or missing references) if (!ProfileSave.Enabled) { return true; } PlayerProfile playerProfile = __instance.m_playerProfile; if (playerProfile == null) { return true; } if (ProfileSave.Active) { __instance.m_saveTimer = 0f; if (setLogoutPoint && Object.op_Implicit((Object)(object)Player.m_localPlayer)) { playerProfile.SaveLogoutPoint(); } Log.Debug("AsyncSave: dropping duplicate character save request."); return false; } Stopwatch stopwatch = (Settings.LogTimings ? Stopwatch.StartNew() : null); __instance.m_saveTimer = 0f; bool flag = false; if (Object.op_Implicit((Object)(object)Player.m_localPlayer)) { playerProfile.SavePlayerData(Player.m_localPlayer); Minimap instance = Minimap.instance; if (Object.op_Implicit((Object)(object)instance)) { MapSnapshot.Capture(instance); flag = true; } if (setLogoutPoint) { playerProfile.SaveLogoutPoint(); } } if ((int)playerProfile.m_fileSource == 4) { ulong num = 1048576uL; if (FileHelpers.FileExistsCloud(playerProfile.GetPath())) { num += FileHelpers.GetFileSize(playerProfile.GetPath(), (FileSource)4); } if (FileHelpers.OperationExceedsCloudCapacity(num * 3)) { string path = playerProfile.GetPath(); playerProfile.m_fileSource = (FileSource)2; string path2 = playerProfile.GetPath(); if (FileHelpers.FileExistsCloud(path)) { FileHelpers.FileCopyOutFromCloud(path, path2, true); } SaveSystem.InvalidateCache(); Log.Warn("The character save operation may exceed the cloud save quota and it has therefore been moved to local storage!"); } } ProfileSave.Profile = playerProfile; ProfileSave.HadMapCapture = flag; if (flag) { ProfileSave.Stage = ProfileSaveStage.Compressing; SaveIo.Enqueue(delegate { try { Stopwatch stopwatch2 = (Settings.LogTimings ? Stopwatch.StartNew() : null); ProfileSave.MapData = MapSnapshot.Compress(); if (stopwatch2 != null) { Log.Info($"AsyncSave timing: map compress (worker) {stopwatch2.ElapsedMilliseconds} ms, {ProfileSave.MapData.Length} bytes"); } } finally { ProfileSave.Stage = ProfileSaveStage.ReadyToBuild; } }); } else { ProfileSave.Stage = ProfileSaveStage.ReadyToBuild; } if (stopwatch != null) { Log.Info($"AsyncSave timing: character capture (main thread) {stopwatch.ElapsedMilliseconds} ms"); } return false; } } [HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")] internal static class SavePlayerToDiskPatch { private static bool Prefix(PlayerProfile __instance, ref bool __result) { if (!ProfileSave.AsyncRequested) { return true; } __result = Run(__instance); return false; } private static bool Run(PlayerProfile profile) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0108: Unknown result type (might be due to invalid IL or missing references) if (profile.m_filename == null) { return false; } PlayerProfile.SavingStarted?.Invoke(); DateTime now = DateTime.Now; bool mounted = false; if (FileHelpers.CloudStorageSupported) { mounted = FileHelpers.Mount((SaveDataAccess)3); if (!mounted) { throw new InvalidOperationException("Failed to mount!"); } } string saveFile; string oldFile; string newFile; byte[] array; try { bool flag = false; SaveSystem.PreSaveCloudChecksAndOperations(profile.m_filename, (SaveDataType)1, ref profile.m_fileSource, ref profile.m_createBackupBeforeSaving, ref flag, 1, (World)null); string characterFolderPath = SaveSystem.GetCharacterFolderPath(profile.m_fileSource); saveFile = characterFolderPath + profile.m_filename + ".fch"; oldFile = saveFile + ".old"; newFile = saveFile + ".new"; if (!Directory.Exists(characterFolderPath) && FileSourceHelper.IsNotCloud(profile.m_fileSource)) { Directory.CreateDirectory(characterFolderPath); } array = BuildPackage(profile); } catch { if (mounted) { FileHelpers.Unmount((UnmountMode)1); } throw; } FileSource fileSource = profile.m_fileSource; string filename = profile.m_filename; SaveIo.Enqueue(delegate { //IL_000d: Unknown result type (might be due to invalid IL or missing references) WriteToDisk(array, filename, fileSource, saveFile, oldFile, newFile, now, mounted); }); return true; } internal static byte[] BuildPackage(PlayerProfile profile) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_0312: Unknown result type (might be due to invalid IL or missing references) //IL_02f0: Unknown result type (might be due to invalid IL or missing references) ZPackage val = new ZPackage(); val.Write(46); val.Write(205); val.Write(10); PlayerStats[] playerStats = profile.m_playerStats; for (int i = 0; i < 10; i++) { PlayerStats val2 = playerStats[i]; for (int j = 0; j < 205; j++) { val.Write(val2.m_stats[(PlayerStatType)j]); } WriteStringFloatMap(val, val2.m_knownWorlds); WriteStringFloatMap(val, val2.m_knownWorldKeys); WriteStringFloatMap(val, val2.m_knownCommands); val.Write(5); for (int k = 0; k < 5; k++) { WriteStringFloatMap(val, val2.m_enemyStats[k]); } WriteStringFloatMap(val, val2.m_itemPickupStats); WriteStringFloatMap(val, val2.m_itemCraftStats); WriteStringFloatMap(val, val2.m_pickableStats); WriteStringFloatMap(val, val2.m_foodEatenStats); WriteStringFloatMap(val, val2.m_piecesPlacedStats); } val.Write(profile.m_firstSpawn); val.Write(profile.m_worldData.Count); foreach (KeyValuePair worldDatum in profile.m_worldData) { val.Write(worldDatum.Key); val.Write(worldDatum.Value.m_haveCustomSpawnPoint); val.Write(worldDatum.Value.m_spawnPoint); val.Write(worldDatum.Value.m_haveLogoutPoint); val.Write(worldDatum.Value.m_logoutPoint); val.Write(worldDatum.Value.m_haveDeathPoint); val.Write(worldDatum.Value.m_deathPoint); val.Write(worldDatum.Value.m_homePoint); val.Write(worldDatum.Value.m_mapData != null); if (worldDatum.Value.m_mapData != null) { val.Write(worldDatum.Value.m_mapData); } } val.Write(profile.m_playerName); val.Write(profile.m_playerID); val.Write(profile.m_startSeed); int num = (int)(DateTime.Now - profile.m_lastSaveLoad).TotalSeconds; profile.m_lastSaveLoad = DateTime.Now; val.Write(profile.m_usedCheats); val.Write(new DateTimeOffset(profile.m_dateCreated).ToUnixTimeSeconds()); if (Object.op_Implicit((Object)(object)ZNet.instance) && Object.op_Implicit((Object)(object)ZoneSystem.instance) && ZNet.World != null) { int currentAchievementDifficultyIndex = Achievements.GetCurrentAchievementDifficultyIndex(); string worldName = ZNet.instance.GetWorldName(); Utils.IncrementOrSet(playerStats[0].m_knownWorlds, worldName, (float)num); Utils.IncrementOrSet(playerStats[currentAchievementDifficultyIndex].m_knownWorlds, worldName, (float)num); string text2 = default(string); for (int l = 0; l < 53; l++) { string text = (ZoneSystem.instance.GetGlobalKey((GlobalKeys)l, ref text2) ? (((object)(GlobalKeys)l/*cast due to .constrained prefix*/).ToString().ToLower() + " " + text2) : (((object)(GlobalKeys)l/*cast due to .constrained prefix*/).ToString().ToLower() + " default")); Utils.IncrementOrSet(playerStats[0].m_knownWorldKeys, text, (float)num); Utils.IncrementOrSet(playerStats[currentAchievementDifficultyIndex].m_knownWorldKeys, text, (float)num); } } if (profile.m_playerData != null) { val.Write(true); val.Write(profile.m_playerData); } else { val.Write(false); } return val.GetArray(); } private static void WriteStringFloatMap(ZPackage pkg, Dictionary map) { pkg.Write(map.Count); foreach (KeyValuePair item in map) { pkg.Write(item.Key); pkg.Write(item.Value); } } private static void WriteToDisk(byte[] array, string filename, FileSource fileSource, string saveFile, string oldFile, string newFile, DateTime now, bool mounted) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Invalid comparison between Unknown and I4 //IL_0109: 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) try { Stopwatch stopwatch = (Settings.LogTimings ? Stopwatch.StartNew() : null); byte[] array2; using (SHA512 sHA = SHA512.Create()) { array2 = sHA.ComputeHash(array); } FileWriter val = new FileWriter(newFile, (CloudStorageFileGrouping)1, (FileHelperType)0, fileSource); val.m_binary.Write(array.Length); val.m_binary.Write(array); val.m_binary.Write(array2.Length); val.m_binary.Write(array2); val.Finish(); SaveSystem.InvalidateCache((SaveDataType)1); if ((int)val.Status != 2 && FileSourceHelper.IsCloud(fileSource)) { string text = SaveSystem.GetCharacterFolderPath((FileSource)2) + filename + "_backup_cloud-" + now.ToString(SaveSystem.s_defaultDateFormat) + ".fch"; val.DumpCloudWriteToLocalFile(text); SaveSystem.InvalidateCache((SaveDataType)1); Log.Error("Cloud save to location \"" + saveFile + "\" failed! Saved as local backup \"" + text + "\". Use the \"Manage saves\" menu to restore this backup."); } else { FileHelpers.ReplaceOldFile(saveFile, newFile, oldFile, (CloudStorageFileGrouping)1, fileSource); SaveSystem.InvalidateCache((SaveDataType)1); ZNet.ConsiderAutoBackup(filename, (SaveDataType)1, now); } if (stopwatch != null) { Log.Info($"AsyncSave timing: character write (worker) {stopwatch.ElapsedMilliseconds} ms, {array.Length} bytes"); } } finally { if (mounted) { FileHelpers.Unmount((UnmountMode)1); } ProfileSave.Stage = ProfileSaveStage.Done; } } } internal struct SaveWorldTiming { public bool Ran; public bool WasSync; public bool Sync; public long Start; } [HarmonyPatch(typeof(ZNet), "SaveWorld")] internal static class SaveWorldPatch { internal static bool TranspilerFailed; private const float MainThreadLogThresholdMs = 50f; private const float RetryAfterSeconds = 60f; private static bool Prefix(ZNet __instance, ref bool sync, out SaveWorldTiming __state) { __state = new SaveWorldTiming { Ran = false, WasSync = sync, Sync = sync, Start = Stopwatch.GetTimestamp() }; if (TranspilerFailed) { __state.Ran = true; return true; } if (sync && !ShouldForceSync(sync)) { sync = false; } if (!__state.WasSync && __instance.m_saveThread != null && __instance.m_saveThread.IsAlive) { float num = Game.m_saveInterval - 60f; if (num < 0f) { num = 0f; } if ((Object)(object)Game.instance != (Object)null) { Game.instance.m_saveTimer = num; } Log.Info("AsyncSave: previous world save is still writing to disk - skipping this " + $"autosave, retrying in {Game.m_saveInterval - num:0} s."); return false; } if (SnapshotState.Task.Task.IsCompleted && !SnapshotState.InProgress) { __state.Ran = true; __state.Sync = sync; return true; } if (ShouldForceSync(sync)) { SnapshotCoroutine.Abort(ProfileSave.ForceSync ? "preempted by shutdown save" : "preempted by sync manual save"); SnapshotState.SignalNoSnapshot(); __state.Ran = true; __state.Sync = sync; return true; } Log.Debug("AsyncSave: dropping duplicate async save request."); return false; } private static void Postfix(SaveWorldTiming __state) { if (!__state.Ran) { return; } LogLevel value = Settings.Logging.Value; if (value == LogLevel.Off) { return; } float num = (float)(Stopwatch.GetTimestamp() - __state.Start) * 1000f / (float)Stopwatch.Frequency; bool flag = __state.WasSync || num >= 50f; if (value != LogLevel.Simple || flag) { if (__state.Sync) { Log.Info($"AsyncSave: ZNet.SaveWorld blocked the main thread for {num:0.0} ms " + "(sync path: vanilla PrepareSave + save thread Join)."); } else if (__state.WasSync) { Log.Info($"AsyncSave: ZNet.SaveWorld returned in {num:0.0} ms " + "(manual save, downgraded to the async path - no Join)."); } else { Log.Info($"AsyncSave: ZNet.SaveWorld returned in {num:0.0} ms (async path)."); } } } private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator il) { //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Expected O, but got Unknown //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Expected O, but got Unknown //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0241: Expected O, but got Unknown //IL_024a: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Expected O, but got Unknown //IL_025e: Unknown result type (might be due to invalid IL or missing references) //IL_0264: Expected O, but got Unknown List list = new List(instructions); MethodInfo zdoPrepare = AccessTools.DeclaredMethod(typeof(ZDOMan), "PrepareSave", (Type[])null, (Type[])null); MethodInfo zonePrepare = AccessTools.DeclaredMethod(typeof(ZoneSystem), "PrepareSave", (Type[])null, (Type[])null); MethodInfo randPrepare = AccessTools.DeclaredMethod(typeof(RandEventSystem), "PrepareSave", (Type[])null, (Type[])null); MethodInfo persistPrepare = AccessTools.DeclaredMethod(typeof(PersistentEventSystem), "PrepareSave", (Type[])null, (Type[])null); MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(SaveWorldPatch), "AsyncSaveStart", (Type[])null, (Type[])null); int num = ((zdoPrepare == null) ? (-1) : list.FindIndex((CodeInstruction c) => CodeInstructionExtensions.Calls(c, zdoPrepare))); int num2 = ((num < 0 || zonePrepare == null) ? (-1) : list.FindIndex(num + 1, (CodeInstruction c) => CodeInstructionExtensions.Calls(c, zonePrepare))); int num3 = ((num2 < 0 || randPrepare == null) ? (-1) : list.FindIndex(num2 + 1, (CodeInstruction c) => CodeInstructionExtensions.Calls(c, randPrepare))); int num4 = ((num3 < 0 || persistPrepare == null) ? (-1) : list.FindIndex(num3 + 1, (CodeInstruction c) => CodeInstructionExtensions.Calls(c, persistPrepare))); bool flag = num >= 0 && num2 >= 0 && num3 >= 0 && num4 >= 0 && num4 + 1 < list.Count; if (flag) { for (int num5 = num; num5 <= num4; num5++) { if (list[num5].labels.Count > 0) { flag = false; break; } } } if (!flag) { TranspilerFailed = true; Settings.BlockWorldSave(); Log.Error("AsyncSave: could not locate the ZDOMan/ZoneSystem/RandEventSystem/PersistentEventSystem PrepareSave sequence in ZNet.SaveWorld. Async save is disabled; the vanilla save path is left completely intact. This usually means the game updated."); return list; } Label label = il.DefineLabel(); Label label2 = il.DefineLabel(); list[num4 + 1].labels.Add(label2); list.InsertRange(num4 + 1, (IEnumerable)(object)new CodeInstruction[2] { new CodeInstruction(OpCodes.Br, (object)label2), new CodeInstruction(OpCodes.Call, (object)methodInfo) { labels = { label } } }); MethodInfo methodInfo2 = AccessTools.DeclaredMethod(typeof(SaveWorldPatch), "ShouldForceSync", (Type[])null, (Type[])null); list.InsertRange(num, (IEnumerable)(object)new CodeInstruction[3] { new CodeInstruction(OpCodes.Ldarg_1, (object)null), new CodeInstruction(OpCodes.Call, (object)methodInfo2), new CodeInstruction(OpCodes.Brfalse, (object)label) }); return list; } public static bool ShouldForceSync(bool sync) { if (!sync) { return false; } if (ProfileSave.ForceSync) { return true; } if (!Settings.WorldSaveOn) { return true; } return !Settings.AsyncManualSave.Value; } public static void AsyncSaveStart(ZDOMan zdoMan) { if (Settings.WorldSaveOn) { SnapshotCoroutine.Start(zdoMan); return; } zdoMan.PrepareSave(); ZoneSystem.instance.PrepareSave(); RandEventSystem.instance.PrepareSave(); PersistentEventSystem.instance.PrepareSave(); SnapshotState.InProgress = false; SnapshotState.ReadyForWrite = false; ZoneSnapshot.Reset(); SnapshotState.SignalNoSnapshot(); } } internal struct WriteTiming { public bool GateHeld; public long WaitTicks; public long GateTicks; public long BodyStart; } [HarmonyPatch(typeof(ZNet), "SaveWorldThread")] internal static class SaveWorldThreadPatch { private static bool Prefix(out WriteTiming __state) { __state = default(WriteTiming); Task task = SnapshotState.Task.Task; long timestamp = Stopwatch.GetTimestamp(); task.Wait(); __state.WaitTicks = Stopwatch.GetTimestamp() - timestamp; if (!task.Result) { return false; } long timestamp2 = Stopwatch.GetTimestamp(); Monitor.Enter(SaveIo.Gate); __state.GateTicks = Stopwatch.GetTimestamp() - timestamp2; __state.GateHeld = true; SaveChunksPatch.ResetWriteStats(); __state.BodyStart = Stopwatch.GetTimestamp(); return true; } private static Exception Finalizer(WriteTiming __state) { if (!__state.GateHeld) { return null; } Monitor.Exit(SaveIo.Gate); LogWrite(__state); return null; } private static void LogWrite(WriteTiming state) { LogLevel value = Settings.Logging.Value; if (value != LogLevel.Off) { float num = 1000f / (float)Stopwatch.Frequency; float num2 = (float)(Stopwatch.GetTimestamp() - state.BodyStart) * num; float num3 = (float)state.WaitTicks * num; float num4 = (float)state.GateTicks * num; string arg = (SaveChunksPatch.LastWasSnapshot ? ($"{SaveChunksPatch.LastKeptCount} ZDOs, {(float)SaveChunksPatch.LastZdoBytes / 1048576f:0.0} MB " + $"across {SaveChunksPatch.LastChunkCount} chunk file(s)") : "vanilla path (no snapshot)"); if (value >= LogLevel.Detailed) { Log.Info($"AsyncSave world write: {num2:0} ms - {arg} " + $"(writer parked {num3:0} ms on the snapshot, {num4:0} ms on the character-save gate)."); } else { Log.Info($"AsyncSave world write: {num2:0} ms - {arg} " + $"(writer parked {num3:0} ms waiting for the snapshot)."); } } } } [HarmonyPatch(typeof(ZDOMan), "AddToSector")] internal static class AddToSectorPatch { [HarmonyPostfix] private static void Postfix(ZDO zdo) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (SnapshotState.InProgress && zdo != null) { SnapshotState.PendingAdds.Add(zdo.m_uid); } } } [HarmonyPatch(typeof(ZDOMan), "HandleDestroyedZDO")] internal static class HandleDestroyedZdoPatch { [HarmonyPostfix] private static void Postfix(ZDOID uid) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) if (SnapshotState.InProgress) { SnapshotState.PendingRemoves.Add(uid); } } } internal static class ShutdownPatches { [HarmonyPatch(typeof(Game), "Awake")] internal static class GameAwake { private static void Postfix() { ProfileSave.ForceSync = false; ProfileSave.Reset(); } } [HarmonyPatch(typeof(Game), "Shutdown")] internal static class GameShutdown { private static void Prefix() { ProfileSave.DrainToDisk(); ProfileSave.ForceSync = true; } } [HarmonyPatch(typeof(Game), "OnApplicationQuit")] internal static class GameOnApplicationQuit { private static void Prefix() { ProfileSave.DrainToDisk(); ProfileSave.ForceSync = true; } } } [HarmonyPatch(typeof(ZNet), "StopAll")] internal static class StopAllPatch { private static void Prefix(ZNet __instance) { if (!__instance.m_haveStoped) { ProfileSave.DrainToDisk(); if (SnapshotState.InProgress || SnapshotState.ReadyForWrite || ZoneSnapshot.Ready) { ProfileSave.ForceSync = true; __instance.SaveWorld(true); } } } private static void Postfix() { SnapshotState.ReleasePools(); } } internal static class ZdoRevisionPatches { [HarmonyPatch(typeof(ZDO), "IncreaseDataRevision")] internal static class IncreaseDataRevisionPatch { [HarmonyPrepare] private static bool Prepare() { return AccessTools.DeclaredMethod(typeof(ZDO), "IncreaseDataRevision", (Type[])null, (Type[])null) != null; } [HarmonyPostfix] private static void Postfix(ZDO __instance) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) if (SnapshotState.InProgress) { SnapshotState.PendingDirty.Add(__instance.m_uid); } } } [HarmonyPatch(typeof(ZDO), "Deserialize")] internal static class DeserializePatch { [HarmonyPrepare] private static bool Prepare() { return AccessTools.DeclaredMethod(typeof(ZDO), "Deserialize", (Type[])null, (Type[])null) != null; } [HarmonyPostfix] private static void Postfix(ZDO __instance) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) if (SnapshotState.InProgress) { SnapshotState.PendingDirty.Add(__instance.m_uid); } } } internal const string IncreaseDataRevisionName = "IncreaseDataRevision"; } [HarmonyPatch(typeof(ZoneSystem), "Save")] internal static class ZoneSavePatch { private static bool Prefix(ZoneSystem __instance, BinaryWriter binaryWriter) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Invalid comparison between Unknown and I4 if (!ZoneSnapshot.Ready) { if (__instance.m_tempGeneratedZonesSaveClone == null) { Log.Error("AsyncSave: ZoneSystem.Save reached with neither an AsyncSave capture nor vanilla's PrepareSave clones. World zone data will not be saved."); } return true; } ZPackage val = new ZPackage(); Vector2s[] zones = ZoneSnapshot.Zones; int zoneCount = ZoneSnapshot.ZoneCount; val.Write(zoneCount); for (int i = 0; i < zoneCount; i++) { val.Write(zones[i]); } val.Write(__instance.m_locationVersion); string[] globalKeys = ZoneSnapshot.GlobalKeys; int globalKeyCount = ZoneSnapshot.GlobalKeyCount; string[] filteredKeys = ZoneSnapshot.FilteredKeys; int num = 0; string text2 = default(string); GlobalKeys val2 = default(GlobalKeys); for (int j = 0; j < globalKeyCount; j++) { string text = globalKeys[j]; ZoneSystem.GetKeyValue(text, ref text2, ref val2); if ((int)val2 >= 41) { filteredKeys[num++] = text; } } val.Write(num); for (int k = 0; k < num; k++) { val.Write(filteredKeys[k]); } val.Write(ZoneSnapshot.LocationsGenerated); LocationInstance[] locations = ZoneSnapshot.Locations; int locationCount = ZoneSnapshot.LocationCount; val.Write(locationCount); for (int l = 0; l < locationCount; l++) { val.Write(StringExtensionMethods.GetStableHashCode(locations[l].m_location.m_prefabName)); val.Write(locations[l].m_position.x); val.Write(locations[l].m_position.y); val.Write(locations[l].m_position.z); val.Write(locations[l].m_placed); } byte[] compressed = val.GetCompressed(); binaryWriter.Write(compressed.Length); binaryWriter.Write(compressed); ZoneSnapshot.Ready = false; return false; } } } namespace AsyncSave.Config { internal enum LogLevel { Off, Simple, Detailed, Debug } internal enum ReconcileMode { Dirty, FullScan } internal static class Settings { public static ConfigEntry SaveIntervalSeconds; public static ConfigEntry Logging; public static ConfigEntry DeadZdoPruning; public static ConfigEntry DeadZdoTtlSeconds; public static ConfigEntry WorldSaveEnabled; public static ConfigEntry AsyncManualSave; public static ConfigEntry SliceBudgetMs; public static ConfigEntry AdaptiveBudget; public static ConfigEntry FrameHeadroomPercent; public static ConfigEntry DeltaReconciliation; public static ConfigEntry CharacterSaveEnabled; private const int VanillaSaveInterval = 1800; private static bool _worldSaveBlocked; private static bool _characterSaveBlocked; private static bool _reconcileForcedFullScan; public static bool WorldSaveOn { get { if (WorldSaveEnabled.Value) { return !_worldSaveBlocked; } return false; } } public static bool CharacterSaveOn { get { if (CharacterSaveEnabled.Value) { return !_characterSaveBlocked; } return false; } } public static ReconcileMode Reconcile { get { if (!_reconcileForcedFullScan) { return DeltaReconciliation.Value; } return ReconcileMode.FullScan; } } public static bool LogTimings => Logging.Value >= LogLevel.Debug; public static void BlockWorldSave() { _worldSaveBlocked = true; } public static void BlockCharacterSave() { _characterSaveBlocked = true; } public static void ForceFullScanReconcile() { _reconcileForcedFullScan = true; } public static void Bind(ConfigFile cfg) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Expected O, but got Unknown SaveIntervalSeconds = cfg.Bind("1 - General", "Save interval (seconds)", 1800, new ConfigDescription("How often the world auto-saves, in seconds. Vanilla default is 1800 (30 minutes). Values below 60 disable the pre-save warning message but the save itself still works. Applied immediately - no restart required. Left at 1800 this does not touch the interval at all, so another mod setting it still wins.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 86400), Array.Empty())); Logging = cfg.Bind("1 - General", "Logging", LogLevel.Simple, "Amount of information written to the log per save. Debug adds per-phase timings."); DeadZdoPruning = cfg.Bind("1 - General", "Prune dead ZDO records", true, "Expire the server's table of recently-destroyed ZDOs instead of letting it grow for the whole session. Vanilla adds an entry for every destroyed ZDO and only ever clears the table when a world loads, but reads it for one purpose: rejecting a create for a ZDO that died while a client's packet was in flight. On a busy large world that is millions of entries retained to answer a question about the last few seconds. Server-side only; a client never fills this table."); DeadZdoTtlSeconds = cfg.Bind("1 - General", "Dead ZDO record lifetime (seconds)", 60, new ConfigDescription("How long a destroyed ZDO stays in that table before being pruned. It only has to outlast a client's round-trip, so the default is already far more than needed. Raise it if you see destroyed objects reappearing on very high-latency clients.", (AcceptableValueBase)(object)new AcceptableValueRange(30, 3600), Array.Empty())); WorldSaveEnabled = cfg.Bind("2 - World Save", "Enabled", true, "Spread the world (ZDO) save across frames instead of stalling on it. Only ever runs on the server or a host - a pure client ignores this. Turning this off restores the vanilla save path completely. The mod also stands this down by itself, for the session only, if it cannot patch ZNet.SaveWorld or the host is big-endian - this setting is never rewritten on disk."); AsyncManualSave = cfg.Bind("2 - World Save", "Async manual saves", true, "Use the async save path for the in-game menu's Save button (which vanilla only takes in single-player, or on a host with no connected players). Turn this off to have the Save button block until the world is on disk. The /save console command is already asynchronous in vanilla and is unaffected either way. Shutdown, logout and quit always save synchronously regardless of this setting."); SliceBudgetMs = cfg.Bind("2 - World Save", "Slice budget (ms)", 2, new ConfigDescription("Upper bound on the main-thread time budget per snapshot slice. Lower = smoother frames, longer total snapshot time. With the adaptive budget on this is a ceiling, not a target - a slice only ever gets whatever the frame had left over.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 33), Array.Empty())); AdaptiveBudget = cfg.Bind("2 - World Save", "Adaptive budget", true, "Scale each slice to the frame time the game is not already using, instead of spending the full slice budget unconditionally. On frames that have already missed the target framerate the snapshot does almost nothing, so a save costs little when the game is struggling - at the price of a longer save window. Turn off for a fixed budget and the shortest possible save."); FrameHeadroomPercent = cfg.Bind("2 - World Save", "Frame headroom (%)", 25, new ConfigDescription("How much longer a frame is allowed to take while a save runs, as a percentage of what that frame costs without us. 25 means a 4 ms frame may become 5 ms. This is what keeps the cost proportional: budgeting against a flat 60 FPS target authorises a 4x frame-time increase on a machine already running at 230 FPS. Lower = smoother but longer saves. Ignored when the adaptive budget is off.", (AcceptableValueBase)(object)new AcceptableValueRange(5, 200), Array.Empty())); DeltaReconciliation = cfg.Bind("2 - World Save", "Reconcile mutations", ReconcileMode.Dirty, "Re-serialize any ZDO whose DataRevision changed during snapshot yields, so a save is not one revision stale for mutations that land mid-snapshot. Dirty re-serializes only what the revision hooks flagged and costs nothing on an idle world. FullScan re-checks every ZDO against live state - correct regardless of hook coverage, but it walks the whole world every save. The mod switches to FullScan by itself, for the session only, if the revision hooks are not attached."); CharacterSaveEnabled = cfg.Bind("3 - Character Save", "Enabled", true, "Move the character (.fch) save off the main thread: bulk-copy the minimap instead of writing 8 million bools one at a time, then compress, hash and write on a worker. Runs wherever there is a local player, and also covers a dedicated server's own profile write. Quitting always uses the untouched vanilla path. The mod stands this down by itself, for the session only, if the .fch layout is not the one this build was written against - this setting is never rewritten on disk."); ApplySaveInterval(); SaveIntervalSeconds.SettingChanged += delegate { ApplySaveInterval(); }; } private static void ApplySaveInterval() { if (SaveIntervalSeconds.Value != 1800) { Game.m_saveInterval = SaveIntervalSeconds.Value; } } } }