Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of AsyncSave v0.5.0
plugins/AsyncSave.dll
Decompiled 13 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.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<ChunkIndex> selected, out int totalChunkFiles, out int selectedZdoCount) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) selected.Clear(); List<ZDO>[] 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<ZDO> 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<ChunkIndex> 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<ChunkIndex> dirty, List<ChunkIndex> 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<byte[]> _blocks = new List<byte[]>(); 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<int> _portalPrefabs = new HashSet<int>(); public static Task<bool> Start(ZDOMan zdoMan) { long timestamp = Stopwatch.GetTimestamp(); TaskCompletionSource<bool> taskCompletionSource = (SnapshotState.Task = new TaskCompletionSource<bool>()); 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<bool> 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<ChunkIndex>[] 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<bool> 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<bool> 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<ChunkIndex> 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<Tuple<ChunkIndex, List<ZDO>>> objectsByChunk = val.m_objectsByChunk; for (int i = 0; i < selected.Count; i++) { objectsByChunk.Add(new Tuple<ChunkIndex, List<ZDO>>(selected[i], new List<ZDO>())); } if (writePortalChunk) { objectsByChunk.Add(new Tuple<ChunkIndex, List<ZDO>>(ZoneSystem.ChunkPortal, new List<ZDO>())); } 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<int> portalPrefabHash = Game.instance.PortalPrefabHash; for (int j = 0; j < portalPrefabHash.Count; j++) { _portalPrefabs.Add(portalPrefabHash[j]); } List<ZDO> scratch = SnapshotState.SectorScratch; List<ZDO>[] 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<ZDO> 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<SectorIndex, List<ZDO>> portalObject in zdoMan.m_portalObjects) { List<ZDO> 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<ZDOID> dirtyScratch; if (reconMode == ReconcileMode.Dirty || verify) { HashSet<ZDOID> 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<SectorIndex, List<ZDO>> 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<ZDO> scratch, int from, ref SliceBudget slice, ref int serialized) { VisitedSet visited = SnapshotState.Visited; SnapshotBuffer buffer = SnapshotState.Buffer; SnapshotEntry[] entries = SnapshotState.Entries; HashSet<int> 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<ZDO> 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<ZDOID> pendingAdds = SnapshotState.PendingAdds; HashSet<ZDOID> pendingRemoves = SnapshotState.PendingRemoves; VisitedSet visited = SnapshotState.Visited; SnapshotBuffer buffer = SnapshotState.Buffer; SnapshotEntry[] entries = SnapshotState.Entries; HashSet<int> 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<ZDOID> 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<ZDOID> 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<ChunkIndex, int> SelectedSlot = new Dictionary<ChunkIndex, int>(); public static readonly List<long> Overflow = new List<long>(256); public static int TotalChunkFiles; public static readonly List<ZDOID> PendingAdds = new List<ZDOID>(1024); public static readonly HashSet<ZDOID> PendingRemoves = new HashSet<ZDOID>(ZdoIdComparer.Instance); public static readonly HashSet<ZDOID> PendingDirty = new HashSet<ZDOID>(ZdoIdComparer.Instance); public static readonly VisitedSet Visited = new VisitedSet(65536); public static readonly List<ZDO> SectorScratch = new List<ZDO>(4096); public static readonly List<ZDOID> DirtyScratch = new List<ZDOID>(1024); public static readonly List<ChunkIndex> Selected = new List<ChunkIndex>(256); public static volatile bool InProgress; public static volatile bool ReadyForWrite; public static TaskCompletionSource<bool> 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<bool> CompletedSource() { TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>(); 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<ZDOID> { 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<int> s_sessionOnly = ZDOExtraData.s_sessionOnly; bool flag = s_sessionOnly.Count > 0; int num = Kept<float>(value, s_sessionOnly, flag); int num2 = Kept<Vector3>(value2, s_sessionOnly, flag); int num3 = Kept<Quaternion>(value3, s_sessionOnly, flag); int num4 = Kept<int>(value4, s_sessionOnly, flag); int num5 = Kept<long>(value5, s_sessionOnly, flag); int num6 = Kept<string>(value6, s_sessionOnly, flag); int num7 = Kept<byte[]>(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<bool, Vector2s> 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<T>(BinarySearchDictionary<int, T> d, HashSet<int> 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<Vector2s> generatedZones = zs.m_generatedZones; Grow(ref Zones, generatedZones.Count); generatedZones.CopyTo(Zones, 0); ZoneCount = generatedZones.Count; Dictionary<Vector2s, LocationInstance> locationInstances = zs.m_locationInstances; Grow(ref Locations, locationInstances.Count); locationInstances.Values.CopyTo(Locations, 0); LocationCount = locationInstances.Count; HashSet<string> 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<T>(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<PinData> 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<PlatformUserID, PlatformUserID>(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<CodeInstruction> list; try { list = new List<CodeInstruction>(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<Action> _queue = new Queue<Action>(); 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<Entry> _queue = new Queue<Entry>(); 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<ZDOID, long> 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<ZDOID> pendingRemoves = SnapshotState.PendingRemoves; List<long> 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<ZDOID> removes, bool anyRemoves, ChunkRange range, List<long> 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<ZDOID> removes, bool anyRemoves, SnapshotBuffer buffer, ChunkRange range, List<long> 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<long, WorldPlayerData> 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<string>(playerStats[0].m_knownWorlds, worldName, (float)num); Utils.IncrementOrSet<string>(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<string>(playerStats[0].m_knownWorldKeys, text, (float)num); Utils.IncrementOrSet<string>(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<string, float> map) { pkg.Write(map.Count); foreach (KeyValuePair<string, float> 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<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> 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<CodeInstruction> list = new List<CodeInstruction>(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