Decompiled source of SmartSave v1.3.0

plugins/SmartSave.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Splatform;
using Steamworks;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = "")]
[assembly: AssemblyCompany("SmartSave")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.3.0.0")]
[assembly: AssemblyInformationalVersion("1.3.0+67a939df1af9073ce5b2bbe87128cb7042e2d404")]
[assembly: AssemblyProduct("SmartSave")]
[assembly: AssemblyTitle("SmartSave")]
[assembly: AssemblyVersion("1.3.0.0")]
namespace SmartSave;

public enum BackgroundWriteMode
{
	Off,
	CloudOnly
}
internal static class CloudWriter
{
	private sealed class Job
	{
		public string Fch;

		public string New;

		public string Old;

		public byte[] Data;

		public object[] BackupArgs;

		public readonly Stopwatch Sw = Stopwatch.StartNew();

		public double TNew;

		public double TRead;

		public double TOld;

		public double TFch;

		public double TVerify;

		public long Last;
	}

	private enum CallKind
	{
		None,
		Write,
		Read
	}

	private static bool _disabled;

	private static Job _pending;

	private static Job _job;

	private static string _capturedPath;

	private static byte[] _captured;

	private static Job _queuedThisSave;

	private static CallResult<RemoteStorageFileWriteAsyncComplete_t> _writeResult;

	private static CallResult<RemoteStorageFileReadAsyncComplete_t> _readResult;

	private static Action<bool> _onWrite;

	private static Action<byte[]> _onRead;

	private static byte[] _readBuffer;

	private static SteamAPICall_t _call = SteamAPICall_t.Invalid;

	private static CallKind _kind;

	private static string _callWhat;

	private static readonly Stopwatch CallSw = new Stopwatch();

	private static int _callFrames;

	private static int _completedSeenFrames;

	private static SteamAPICall_t _abandoned = SteamAPICall_t.Invalid;

	private const int WaitMs = 1000;

	private const int WaitShutdownMs = 3000;

	private static readonly MethodInfo ConsiderAutoBackup = AccessTools.Method(typeof(ZNet), "ConsiderAutoBackup", (Type[])null, (Type[])null);

	private static readonly MethodInfo Mount = AccessTools.Method(typeof(FileHelpers), "Mount", (Type[])null, (Type[])null);

	private static readonly MethodInfo Unmount = AccessTools.Method(typeof(FileHelpers), "Unmount", (Type[])null, (Type[])null);

	internal static bool Active { get; private set; }

	internal static bool Bypass { get; private set; }

	internal static bool Busy
	{
		get
		{
			if (_job == null)
			{
				return _pending != null;
			}
			return true;
		}
	}

	internal static double LastWaitMs { get; private set; }

	internal static void OnSaveStart(PlayerProfile profile)
	{
		LastWaitMs = 0.0;
		if (_pending != null)
		{
			Log("a newer save came before the background write started; the newer one replaces it");
			_pending = null;
		}
		Game instance = Game.instance;
		Wait("a new save", ((Object)(object)instance != (Object)null && instance.IsShuttingDown()) ? 3000 : 1000);
		_capturedPath = null;
		_captured = null;
		_queuedThisSave = null;
		Active = Eligible(profile);
	}

	internal static void OnSaveEnd()
	{
		Active = false;
		if (_captured != null && _queuedThisSave == null)
		{
			Plugin.Log.LogWarning((object)"background cloud write: the save stopped before the file was handed over; the game reported it");
		}
		_capturedPath = null;
		_captured = null;
		_queuedThisSave = null;
	}

	private static bool Eligible(PlayerProfile profile)
	{
		//IL_0026: Unknown result type (might be due to invalid IL or missing references)
		if (_disabled || !Plugin.Enabled.Value || Plugin.BackgroundWrite.Value != BackgroundWriteMode.CloudOnly)
		{
			return false;
		}
		if (profile == null || !FileSourceHelper.IsCloud(profile.m_fileSource))
		{
			return false;
		}
		Game instance = Game.instance;
		if ((Object)(object)instance == (Object)null || instance.IsShuttingDown() || (Object)(object)Player.m_localPlayer == (Object)null)
		{
			return false;
		}
		if (!SteamManager.Initialized && !SteamManager2.Initialized)
		{
			return false;
		}
		return true;
	}

	internal static bool TryCapture(string path, byte[] data)
	{
		if (!Active || path == null || data == null || data.Length == 0)
		{
			return false;
		}
		if (!path.EndsWith(".fch.new", StringComparison.OrdinalIgnoreCase))
		{
			return false;
		}
		_capturedPath = path;
		_captured = (byte[])data.Clone();
		return true;
	}

	internal static bool TryReplace(string saveFile, string newFile, string oldFile)
	{
		if (!Active || _captured == null || !string.Equals(newFile, _capturedPath, StringComparison.Ordinal))
		{
			return false;
		}
		Job obj = new Job
		{
			Fch = saveFile,
			New = newFile,
			Old = oldFile,
			Data = _captured
		};
		_captured = null;
		_pending = obj;
		_queuedThisSave = obj;
		return true;
	}

	internal static bool TryDeferBackup(object[] args)
	{
		if (Bypass || !Active || _queuedThisSave == null)
		{
			return false;
		}
		_queuedThisSave.BackupArgs = (object[])args.Clone();
		return true;
	}

	private static void EnsureCallResults()
	{
		if (_writeResult == null)
		{
			_writeResult = CallResult<RemoteStorageFileWriteAsyncComplete_t>.Create((APIDispatchDelegate<RemoteStorageFileWriteAsyncComplete_t>)OnWriteComplete);
		}
		if (_readResult == null)
		{
			_readResult = CallResult<RemoteStorageFileReadAsyncComplete_t>.Create((APIDispatchDelegate<RemoteStorageFileReadAsyncComplete_t>)OnReadComplete);
		}
	}

	internal static void Tick()
	{
		if (_pending != null && _job == null && !Active)
		{
			Job pending = _pending;
			_pending = null;
			try
			{
				Start(pending);
			}
			catch (Exception ex)
			{
				_job = pending;
				Fail("starting (" + ex.GetType().Name + ": " + ex.Message + ")");
			}
		}
		if (_kind != CallKind.None)
		{
			Poll();
		}
		CheckAbandoned();
	}

	private static void Start(Job job)
	{
		EnsureCallResults();
		_job = job;
		Trace($"background cloud write started ({job.Data.Length / 1024} KB, {job.Sw.Elapsed.TotalMilliseconds:F0} ms after the save)");
		job.Last = job.Sw.ElapsedTicks;
		WriteAsync(job.New, job.Data, delegate(bool ok)
		{
			if (!ok)
			{
				Fail("writing " + job.New);
			}
			else
			{
				job.TNew = Lap(job);
				if (SteamRemoteStorage.FileExists(job.Fch))
				{
					ReadCurrentForOld(job);
				}
				else
				{
					WriteMain(job);
				}
			}
		});
	}

	private static void ReadCurrentForOld(Job job)
	{
		ReadAsync(job.Fch, delegate(byte[] buf)
		{
			if (buf == null)
			{
				Fail("reading " + job.Fch);
			}
			else
			{
				job.TRead = Lap(job);
				if (buf.Length == 0)
				{
					WriteMain(job);
				}
				else
				{
					WriteAsync(job.Old, buf, delegate(bool ok)
					{
						if (!ok)
						{
							Fail("writing " + job.Old);
						}
						else
						{
							job.TOld = Lap(job);
							WriteMain(job);
						}
					});
				}
			}
		});
	}

	private static void WriteMain(Job job)
	{
		WriteAsync(job.Fch, job.Data, delegate(bool ok)
		{
			if (!ok)
			{
				Fail("writing " + job.Fch);
			}
			else
			{
				job.TFch = Lap(job);
				ReadAsync(job.Fch, delegate(byte[] back)
				{
					if (back == null || !Same(back, job.Data))
					{
						Fail("checking " + job.Fch + " (read-back differs)");
					}
					else
					{
						job.TVerify = Lap(job);
						SteamRemoteStorage.FileDelete(job.New);
						Done(job);
					}
				});
			}
		});
	}

	private static void ReadAsync(string path, Action<byte[]> then)
	{
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0031: 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)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Unknown result type (might be due to invalid IL or missing references)
		//IL_0095: Unknown result type (might be due to invalid IL or missing references)
		int fileSize = SteamRemoteStorage.GetFileSize(path);
		if (fileSize <= 0)
		{
			then(new byte[0]);
			return;
		}
		_readBuffer = new byte[fileSize];
		_onRead = then;
		SteamAPICall_t val = SteamRemoteStorage.FileReadAsync(path, 0u, (uint)fileSize);
		if (val == SteamAPICall_t.Invalid)
		{
			_onRead = null;
			_readBuffer = null;
			Log("→ read " + path + ": Steam refused the request");
			then(null);
		}
		else
		{
			Track(val, CallKind.Read, "read " + path);
			_readResult = CallResult<RemoteStorageFileReadAsyncComplete_t>.Create((APIDispatchDelegate<RemoteStorageFileReadAsyncComplete_t>)OnReadComplete);
			_readResult.Set(val, (APIDispatchDelegate<RemoteStorageFileReadAsyncComplete_t>)null);
		}
	}

	private static void OnReadComplete(RemoteStorageFileReadAsyncComplete_t r, bool ioFailure)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		ReadDone(r, ioFailure, "callback");
	}

	private static void ReadDone(RemoteStorageFileReadAsyncComplete_t r, bool ioFailure, string via)
	{
		//IL_000a: 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_0035: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_003c: Invalid comparison between Unknown and I4
		//IL_0041: 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_0048: 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_006c: Unknown result type (might be due to invalid IL or missing references)
		if (_kind != CallKind.Read)
		{
			return;
		}
		Answered(via, r.m_eResult, ioFailure);
		Action<byte[]> onRead = _onRead;
		byte[] array = _readBuffer;
		_onRead = null;
		_readBuffer = null;
		if (onRead == null)
		{
			return;
		}
		if (ioFailure || (int)r.m_eResult != 1 || array == null || !SteamRemoteStorage.FileReadAsyncComplete(r.m_hFileReadAsync, array, r.m_cubRead))
		{
			onRead(null);
			return;
		}
		if (r.m_cubRead != array.Length)
		{
			Array.Resize(ref array, (int)r.m_cubRead);
		}
		onRead(array);
	}

	private static void WriteAsync(string path, byte[] data, Action<bool> then)
	{
		//IL_000b: 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)
		//IL_0011: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0041: 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)
		_onWrite = then;
		SteamAPICall_t val = SteamRemoteStorage.FileWriteAsync(path, data, (uint)data.Length);
		if (val == SteamAPICall_t.Invalid)
		{
			_onWrite = null;
			Log("→ write " + path + ": Steam refused the request");
			then(obj: false);
		}
		else
		{
			Track(val, CallKind.Write, $"write {path} ({data.Length / 1024} KB)");
			_writeResult = CallResult<RemoteStorageFileWriteAsyncComplete_t>.Create((APIDispatchDelegate<RemoteStorageFileWriteAsyncComplete_t>)OnWriteComplete);
			_writeResult.Set(val, (APIDispatchDelegate<RemoteStorageFileWriteAsyncComplete_t>)null);
		}
	}

	private static void OnWriteComplete(RemoteStorageFileWriteAsyncComplete_t r, bool ioFailure)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		WriteDone(r, ioFailure, "callback");
	}

	private static void WriteDone(RemoteStorageFileWriteAsyncComplete_t r, bool ioFailure, string via)
	{
		//IL_000a: 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_0029: 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_0030: Invalid comparison between Unknown and I4
		if (_kind == CallKind.Write)
		{
			Answered(via, r.m_eResult, ioFailure);
			Action<bool> onWrite = _onWrite;
			_onWrite = null;
			onWrite?.Invoke(!ioFailure && (int)r.m_eResult == 1);
		}
	}

	private static void Track(SteamAPICall_t call, CallKind kind, string what)
	{
		//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_002e: Unknown result type (might be due to invalid IL or missing references)
		_call = call;
		_kind = kind;
		_callWhat = what;
		_callFrames = 0;
		_completedSeenFrames = 0;
		CallSw.Restart();
		Trace($"→ {what} (request {call.m_SteamAPICall})");
	}

	private static void Answered(string via, EResult result, bool ioFailure)
	{
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_0069: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		Trace(string.Format("← {0}: {1}{2} after {3:F0} ms, {4} frames, via {5}", _callWhat, result, ioFailure ? ", I/O failure" : "", CallSw.Elapsed.TotalMilliseconds, _callFrames, via));
		_kind = CallKind.None;
		_call = SteamAPICall_t.Invalid;
	}

	private static void Poll()
	{
		//IL_000c: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_00be: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Unknown result type (might be due to invalid IL or missing references)
		_callFrames++;
		bool failed = default(bool);
		if (!SteamUtils.IsAPICallCompleted(_call, ref failed) || ++_completedSeenFrames < 30)
		{
			return;
		}
		Log("Steam reports " + _callWhat + " as finished, but the callback did not deliver it; asking Steam for the result");
		if (_kind == CallKind.Write)
		{
			_writeResult.Cancel();
			if (TryGetResult<RemoteStorageFileWriteAsyncComplete_t>(_call, out RemoteStorageFileWriteAsyncComplete_t result, out failed))
			{
				WriteDone(result, failed, "asking Steam");
				return;
			}
			Log("Steam gave no result for " + _callWhat);
			_kind = CallKind.None;
			Fail("getting the result of " + _callWhat);
		}
		else
		{
			_readResult.Cancel();
			if (TryGetResult<RemoteStorageFileReadAsyncComplete_t>(_call, out RemoteStorageFileReadAsyncComplete_t result2, out failed))
			{
				ReadDone(result2, failed, "asking Steam");
				return;
			}
			Log("Steam gave no result for " + _callWhat);
			_kind = CallKind.None;
			Fail("getting the result of " + _callWhat);
		}
	}

	private static bool TryGetResult<T>(SteamAPICall_t call, out T result, out bool failed) where T : struct
	{
		//IL_0040: Unknown result type (might be due to invalid IL or missing references)
		result = default(T);
		failed = true;
		int num = Marshal.SizeOf(typeof(T));
		int num2 = (int)typeof(T).GetField("k_iCallback").GetRawConstantValue();
		IntPtr intPtr = Marshal.AllocHGlobal(num);
		try
		{
			if (!SteamUtils.GetAPICallResult(call, intPtr, num, num2, ref failed))
			{
				return false;
			}
			result = (T)Marshal.PtrToStructure(intPtr, typeof(T));
			return true;
		}
		finally
		{
			Marshal.FreeHGlobal(intPtr);
		}
	}

	private static void CheckAbandoned()
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0005: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Unknown result type (might be due to invalid IL or missing references)
		//IL_0026: Unknown result type (might be due to invalid IL or missing references)
		bool flag = default(bool);
		if (!(_abandoned == SteamAPICall_t.Invalid) && SteamUtils.IsAPICallCompleted(_abandoned, ref flag))
		{
			_abandoned = SteamAPICall_t.Invalid;
			Log("a given-up background write finished late; the game saves once more so the newest data is written last");
			SaveScheduler.Request(Reason.Repair, Prio.High, wantLogout: false);
		}
	}

	private static void Trace(string text)
	{
		if (Plugin.Profiling.Value)
		{
			Plugin.Log.LogInfo((object)("cloud: " + text));
		}
	}

	private static void Log(string text)
	{
		if (Plugin.LogSaves.Value)
		{
			Plugin.Log.LogInfo((object)("cloud: " + text));
		}
	}

	private static void Done(Job job)
	{
		_job = null;
		SaveSystem.InvalidateCache();
		RunDeferredBackup(job);
		if (Plugin.LogSaves.Value)
		{
			Plugin.Log.LogInfo((object)($"background cloud write done in {job.Sw.Elapsed.TotalMilliseconds:F0} ms " + $"(.new {job.TNew:F0}, read {job.TRead:F0}, .old {job.TOld:F0}, .fch {job.TFch:F0}, check {job.TVerify:F0} ms; {job.Data.Length / 1024} KB, verified)"));
		}
	}

	private static void Fail(string what)
	{
		//IL_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		//IL_000d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_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)
		Job job = _job;
		if (_kind != CallKind.None && _call != SteamAPICall_t.Invalid)
		{
			_abandoned = _call;
		}
		_kind = CallKind.None;
		_call = SteamAPICall_t.Invalid;
		try
		{
			_writeResult?.Cancel();
			_readResult?.Cancel();
		}
		catch
		{
		}
		_job = null;
		_onWrite = null;
		_onRead = null;
		_readBuffer = null;
		Disable("Steam Cloud error while " + what);
		if (job != null)
		{
			try
			{
				string text = SaveSystem.GetCharacterFolderPath((FileSource)2) + Path.GetFileNameWithoutExtension(job.Fch) + "_backup_cloud-" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".fch";
				Directory.CreateDirectory(Path.GetDirectoryName(text));
				File.WriteAllBytes(text, job.Data);
				SaveSystem.InvalidateCache();
				ZLog.LogError((object)("Cloud save to location \"" + job.Fch + "\" failed! Saved as local backup \"" + text + "\". Use the \"Manage saves\" menu to restore this backup."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogError((object)("background cloud write: could not write the local backup either: " + ex.Message));
			}
			Game instance = Game.instance;
			if ((Object)(object)instance != (Object)null && !instance.IsShuttingDown() && (Object)(object)Player.m_localPlayer != (Object)null)
			{
				SaveScheduler.Request(Reason.Repair, Prio.High, wantLogout: false);
			}
		}
	}

	private static void RunDeferredBackup(Job job)
	{
		if (job.BackupArgs == null || ConsiderAutoBackup == null)
		{
			return;
		}
		bool flag = false;
		Bypass = true;
		try
		{
			flag = Mount != null && (bool)Mount.Invoke(null, new object[1] { Enum.ToObject(Mount.GetParameters()[0].ParameterType, 3) });
			ConsiderAutoBackup.Invoke(null, job.BackupArgs);
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("background cloud write: auto-backup check failed: " + (ex.InnerException?.Message ?? ex.Message)));
		}
		finally
		{
			Bypass = false;
			if (flag && Unmount != null)
			{
				Unmount.Invoke(null, new object[1] { Enum.ToObject(Unmount.GetParameters()[0].ParameterType, 1) });
			}
		}
	}

	internal static void Wait(string why, int timeoutMs)
	{
		if (_job == null)
		{
			return;
		}
		Stopwatch stopwatch = Stopwatch.StartNew();
		while (_job != null && stopwatch.ElapsedMilliseconds < timeoutMs)
		{
			SteamAPI.RunCallbacks();
			if (_job != null && _kind != CallKind.None)
			{
				Poll();
			}
			if (_job != null)
			{
				Thread.Sleep(1);
			}
		}
		LastWaitMs = stopwatch.Elapsed.TotalMilliseconds;
		if (_job != null)
		{
			Fail(string.Format("waiting for the previous write (no answer from Steam in {0:F0} s, stuck at: {1})", (double)timeoutMs / 1000.0, _callWhat ?? "?"));
		}
		else
		{
			Log($"{why} waited {LastWaitMs:F0} ms for the previous background write");
		}
	}

	private static void WriteSync(string path, byte[] data)
	{
		try
		{
			SteamRemoteStorage.FileWrite(path, data, data.Length);
		}
		catch (Exception ex)
		{
			Plugin.Log.LogError((object)("background cloud write: direct write failed: " + ex.Message));
		}
	}

	private static bool Same(byte[] a, byte[] b)
	{
		if (a.Length != b.Length)
		{
			return false;
		}
		for (int i = 0; i < a.Length; i++)
		{
			if (a[i] != b[i])
			{
				return false;
			}
		}
		return true;
	}

	private static double Lap(Job job)
	{
		long elapsedTicks = job.Sw.ElapsedTicks;
		double result = (double)(elapsedTicks - job.Last) * 1000.0 / (double)Stopwatch.Frequency;
		job.Last = elapsedTicks;
		return result;
	}

	private static void Disable(string why)
	{
		if (!_disabled)
		{
			_disabled = true;
			Plugin.Log.LogWarning((object)("background cloud write switched off until restart, using the game's own cloud save: " + why));
		}
	}
}
internal static class FastMapData
{
	private enum State
	{
		Unverified,
		Verified,
		Disabled
	}

	private sealed class Snapshot
	{
		public int Size;

		public int Len;

		public int[] A;

		public int[] B;

		public byte[] Tail;

		public bool SameAs(Snapshot o)
		{
			if (o != null && Size == o.Size && Len == o.Len && Same(A, o.A) && Same(B, o.B))
			{
				return Same(Tail, o.Tail);
			}
			return false;
		}
	}

	private const int MapVersion = 8;

	private static State _state = State.Unverified;

	private static readonly FieldRef<Minimap, BitArray> Explored = AccessTools.FieldRefAccess<Minimap, BitArray>("m_explored");

	private static readonly FieldRef<Minimap, BitArray> ExploredOthers = AccessTools.FieldRefAccess<Minimap, BitArray>("m_exploredOthers");

	private static readonly FieldRef<Minimap, List<PinData>> Pins = AccessTools.FieldRefAccess<Minimap, List<PinData>>("m_pins");

	private static readonly object Lock = new object();

	private static Snapshot _cachedSnap;

	private static byte[] _cachedResult;

	private static volatile bool _workerBusy;

	private static float _nextPrewarm;

	internal static bool Bypass { get; private set; }

	internal static string LastPath { get; set; } = "";

	internal static bool Enabled
	{
		get
		{
			if (Plugin.FastMapSave.Value)
			{
				return _state != State.Disabled;
			}
			return false;
		}
	}

	internal static bool Verified => _state == State.Verified;

	internal static byte[] TryGet(Minimap minimap)
	{
		try
		{
			Snapshot snapshot = TakeSnapshot(minimap);
			lock (Lock)
			{
				if (_cachedResult != null && snapshot.SameAs(_cachedSnap))
				{
					LastPath = "cache";
					return (byte[])_cachedResult.Clone();
				}
			}
			byte[] array = Wrap(Utils.Compress(Payload(snapshot)));
			Store(snapshot, array);
			LastPath = "fast";
			ZLog.Log((object)("Minimap: compressed mapData (SmartSave fast build) => " + array.Length + " bytes"));
			return (byte[])array.Clone();
		}
		catch (Exception ex)
		{
			Disable("error: " + ex.GetType().Name + ": " + ex.Message);
			return null;
		}
	}

	internal static void VerifyAtSave(Minimap minimap, byte[] vanilla)
	{
		if (_state != State.Unverified)
		{
			return;
		}
		try
		{
			Check(TakeSnapshot(minimap), vanilla, "first save");
		}
		catch (Exception ex)
		{
			Disable("check failed: " + ex.GetType().Name + ": " + ex.Message);
		}
	}

	internal static void VerifyAtJoin(Minimap minimap)
	{
		if (_state != State.Unverified || !Plugin.FastMapSave.Value)
		{
			return;
		}
		try
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			Snapshot snap = TakeSnapshot(minimap);
			Bypass = true;
			byte[] vanilla;
			try
			{
				vanilla = (byte[])AccessTools.Method(typeof(Minimap), "GetMapData", (Type[])null, (Type[])null).Invoke(minimap, null);
			}
			finally
			{
				Bypass = false;
			}
			Check(snap, vanilla, $"joining the world, {stopwatch.ElapsedMilliseconds} ms");
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("fast map save check on joining failed (" + ex.GetType().Name + ": " + ex.Message + "), checking at the first save instead"));
		}
	}

	private static void Check(Snapshot snap, byte[] vanilla, string when)
	{
		//IL_000f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0014: Unknown result type (might be due to invalid IL or missing references)
		if (vanilla == null)
		{
			Disable("the game returned no map data");
			return;
		}
		ZPackage val = new ZPackage(vanilla);
		int num = val.ReadInt();
		byte[] array = val.ReadCompressedPackage().GetArray();
		if (num == 8 && Same(array, Payload(snap)))
		{
			_state = State.Verified;
			Store(snap, (byte[])vanilla.Clone());
			Plugin.Log.LogInfo((object)("fast map save checked (" + when + "): same result as the game, now in use"));
		}
		else
		{
			Disable($"result differs from the game's (map version {num}; another mod may change the map data)");
		}
	}

	internal static void Tick()
	{
		float value = Plugin.MapPrewarmSec.Value;
		if (value <= 0f || !Enabled || !Verified || _workerBusy || Time.realtimeSinceStartup < _nextPrewarm)
		{
			return;
		}
		_nextPrewarm = Time.realtimeSinceStartup + value;
		Minimap instance = Minimap.instance;
		if ((Object)(object)instance == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null)
		{
			return;
		}
		try
		{
			Snapshot snap = TakeSnapshot(instance);
			lock (Lock)
			{
				if (_cachedResult != null && snap.SameAs(_cachedSnap))
				{
					return;
				}
			}
			_workerBusy = true;
			Thread thread = new Thread((ThreadStart)delegate
			{
				try
				{
					Store(snap, Wrap(Utils.Compress(Payload(snap))));
				}
				catch
				{
				}
				finally
				{
					_workerBusy = false;
				}
			});
			thread.IsBackground = true;
			thread.Name = "SmartSave map prewarm";
			thread.Priority = ThreadPriority.BelowNormal;
			thread.Start();
		}
		catch (Exception ex)
		{
			_workerBusy = false;
			Plugin.Log.LogWarning((object)("map prewarm skipped: " + ex.Message));
		}
	}

	private static Snapshot TakeSnapshot(Minimap minimap)
	{
		BitArray bitArray = Explored.Invoke(minimap);
		BitArray bitArray2 = ExploredOthers.Invoke(minimap);
		int length = bitArray.Length;
		if (bitArray2 == null || bitArray2.Length < length)
		{
			throw new InvalidOperationException("explored grids differ in size");
		}
		return new Snapshot
		{
			Size = minimap.m_textureSize,
			Len = length,
			A = Words(bitArray, length),
			B = Words(bitArray2, length),
			Tail = PinsPart(minimap)
		};
	}

	private static int[] Words(BitArray bits, int n)
	{
		int[] array = new int[(bits.Length + 31) / 32];
		bits.CopyTo(array, 0);
		int[] array2 = new int[(n + 31) / 32];
		Array.Copy(array, array2, array2.Length);
		int num = n & 0x1F;
		if (num != 0)
		{
			array2[^1] &= (1 << num) - 1;
		}
		return array2;
	}

	private static byte[] PinsPart(Minimap minimap)
	{
		//IL_0000: Unknown result type (might be due to invalid IL or missing references)
		//IL_0006: Expected O, but got Unknown
		//IL_007b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0088: Unknown result type (might be due to invalid IL or missing references)
		//IL_0092: Expected I4, but got Unknown
		ZPackage val = new ZPackage();
		List<PinData> list = Pins.Invoke(minimap);
		int num = 0;
		foreach (PinData item in list)
		{
			if (item.m_save)
			{
				num++;
			}
		}
		val.Write(num);
		foreach (PinData item2 in list)
		{
			if (item2.m_save)
			{
				val.Write(item2.m_name);
				val.Write(item2.m_pos);
				val.Write((int)item2.m_type);
				val.Write(item2.m_checked);
				val.Write(item2.m_ownerID);
				val.Write(((object)Unsafe.As<PlatformUserID, PlatformUserID>(ref item2.m_author)/*cast due to .constrained prefix*/).ToString());
			}
		}
		val.Write(ZNet.instance.IsReferencePositionPublic());
		return val.GetArray();
	}

	private static byte[] Payload(Snapshot s)
	{
		byte[] array = new byte[4 + 2 * s.Len + s.Tail.Length];
		array[0] = (byte)s.Size;
		array[1] = (byte)(s.Size >> 8);
		array[2] = (byte)(s.Size >> 16);
		array[3] = (byte)(s.Size >> 24);
		int k = 4;
		Expand(s.A, s.Len, array, ref k);
		Expand(s.B, s.Len, array, ref k);
		Buffer.BlockCopy(s.Tail, 0, array, k, s.Tail.Length);
		return array;
	}

	private static void Expand(int[] words, int n, byte[] dst, ref int k)
	{
		for (int i = 0; i < n; i++)
		{
			dst[k++] = (byte)((words[i >> 5] >> i) & 1);
		}
	}

	private static byte[] Wrap(byte[] compressed)
	{
		using MemoryStream memoryStream = new MemoryStream(compressed.Length + 8);
		using BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
		binaryWriter.Write(8);
		binaryWriter.Write(compressed.Length);
		binaryWriter.Write(compressed);
		binaryWriter.Flush();
		return memoryStream.ToArray();
	}

	private static void Store(Snapshot snap, byte[] result)
	{
		lock (Lock)
		{
			_cachedSnap = snap;
			_cachedResult = result;
		}
	}

	private static bool Same(int[] a, int[] b)
	{
		if (a == null || b == null || a.Length != b.Length)
		{
			return false;
		}
		for (int i = 0; i < a.Length; i++)
		{
			if (a[i] != b[i])
			{
				return false;
			}
		}
		return true;
	}

	private static bool Same(byte[] a, byte[] b)
	{
		if (a == null || b == null || a.Length != b.Length)
		{
			return false;
		}
		for (int i = 0; i < a.Length; i++)
		{
			if (a[i] != b[i])
			{
				return false;
			}
		}
		return true;
	}

	private static void Disable(string why)
	{
		_state = State.Disabled;
		lock (Lock)
		{
			_cachedSnap = null;
			_cachedResult = null;
		}
		Plugin.Log.LogWarning((object)("fast map save switched off, using the game's own map save: " + why));
	}
}
internal static class Patches
{
	[HarmonyPatch(typeof(Game), "SavePlayerProfile")]
	private static class Game_SavePlayerProfile
	{
		private static bool Prefix(Game __instance, bool isFromRpc)
		{
			if (!isFromRpc && TimerSave.Running && SaveScheduler.RecentFullSave(out var ago))
			{
				__instance.m_saveTimer = 0f;
				SaveScheduler.SkippedNow = true;
				if (Plugin.LogSaves.Value)
				{
					Plugin.Log.LogInfo((object)$"skipped the game's timer save: a full save ran {ago:F0} s ago");
				}
				return false;
			}
			SaveScheduler.SkippedNow = false;
			SaveScheduler.OnProfileSaveStart();
			return true;
		}

		private static void Postfix(bool setLogoutPoint, bool __runOriginal)
		{
			if (__runOriginal)
			{
				SaveScheduler.OnProfileSaved(setLogoutPoint);
			}
		}
	}

	[HarmonyPatch(typeof(PlayerProfile), "SavePlayerToDisk")]
	private static class PlayerProfile_SavePlayerToDisk
	{
		private static void Prefix(PlayerProfile __instance)
		{
			CloudWriter.OnSaveStart(__instance);
		}

		private static Exception Finalizer(Exception __exception)
		{
			CloudWriter.OnSaveEnd();
			return __exception;
		}
	}

	[HarmonyPatch(typeof(FileHelpers), "CloudFileWriteInChunks")]
	private static class FileHelpers_CloudFileWriteInChunks
	{
		private static bool Prefix(string pchFile, byte[] data, ref bool __result)
		{
			if (!CloudWriter.TryCapture(pchFile, data))
			{
				return true;
			}
			__result = true;
			return false;
		}
	}

	[HarmonyPatch(typeof(FileHelpers), "ReplaceOldFile")]
	private static class FileHelpers_ReplaceOldFile
	{
		private static bool Prefix(string saveFile, string newFile, string oldFile)
		{
			return !CloudWriter.TryReplace(saveFile, newFile, oldFile);
		}
	}

	[HarmonyPatch(typeof(ZNet), "ConsiderAutoBackup")]
	private static class ZNet_ConsiderAutoBackup
	{
		private static bool Prefix(object[] __args, ref bool __result)
		{
			if (!CloudWriter.TryDeferBackup(__args))
			{
				return true;
			}
			__result = true;
			return false;
		}
	}

	[HarmonyPatch(typeof(Game), "UpdateSaving")]
	private static class TimerSave
	{
		internal static bool Running;

		private static void Prefix()
		{
			Running = true;
		}

		private static Exception Finalizer(Exception __exception)
		{
			Running = false;
			return __exception;
		}
	}

	[HarmonyPatch(typeof(Minimap), "SaveMapData")]
	private static class Minimap_SaveMapData
	{
		private static bool Prefix()
		{
			if (SaveScheduler.SkipMapNow)
			{
				return false;
			}
			SaveScheduler.OnMapWritten();
			return true;
		}
	}

	[HarmonyPatch(typeof(Minimap), "GetMapData")]
	private static class Minimap_GetMapData
	{
		private static bool Prefix(Minimap __instance, ref byte[] __result, out bool __state)
		{
			__state = false;
			if (FastMapData.Bypass)
			{
				return true;
			}
			FastMapData.LastPath = "game";
			if (!FastMapData.Enabled)
			{
				return true;
			}
			if (!FastMapData.Verified)
			{
				__state = true;
				return true;
			}
			byte[] array = FastMapData.TryGet(__instance);
			if (array == null)
			{
				return true;
			}
			__result = array;
			return false;
		}

		private static void Postfix(Minimap __instance, byte[] __result, bool __state)
		{
			if (__state)
			{
				FastMapData.VerifyAtSave(__instance, __result);
			}
		}
	}

	[HarmonyPatch(typeof(Minimap), "LoadMapData")]
	private static class Minimap_LoadMapData
	{
		private static void Postfix(Minimap __instance)
		{
			FastMapData.VerifyAtJoin(__instance);
		}
	}

	[HarmonyPatch(typeof(ZNet), "RPC_SavePlayerProfile")]
	private static class ZNet_RPC_SavePlayerProfile
	{
		private static bool Prefix()
		{
			if (!Plugin.Enabled.Value)
			{
				return true;
			}
			if (SaveScheduler.RecentFullSave(out var ago))
			{
				if (Plugin.LogSaves.Value)
				{
					Plugin.Log.LogInfo((object)$"skipped the server's save request: a full save ran {ago:F0} s ago");
				}
				return false;
			}
			if (!Plugin.SafeServerPushSave.Value)
			{
				return true;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null || (Object)(object)Game.instance == (Object)null || SafetyState.IsLogoutSafe(localPlayer))
			{
				return true;
			}
			if (Plugin.LogSaves.Value)
			{
				Plugin.Log.LogInfo((object)"server save request while not safe: saving without moving the logout point");
			}
			Game.instance.SavePlayerProfile(false, true);
			return false;
		}
	}

	[HarmonyPatch(typeof(InventoryGui), "DoCrafting")]
	private static class InventoryGui_DoCrafting
	{
		private static void Postfix(Player player)
		{
			if (IsLocal((Character)(object)player))
			{
				SaveScheduler.OnCrafted();
			}
		}
	}

	[HarmonyPatch(typeof(InventoryGui), "RepairOneItem")]
	private static class InventoryGui_RepairOneItem
	{
		private static void Postfix()
		{
			SaveScheduler.OnCrafted();
		}
	}

	[HarmonyPatch(typeof(Inventory), "Changed")]
	private static class Inventory_Changed
	{
		private static void Postfix(Inventory __instance)
		{
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer != (Object)null && __instance == ((Humanoid)localPlayer).GetInventory())
			{
				SaveScheduler.OnInventoryChanged();
			}
		}
	}

	[HarmonyPatch(typeof(TombStone), "OnTakeAllSuccess")]
	private static class TombStone_OnTakeAllSuccess
	{
		private static void Postfix()
		{
			if (Plugin.Enabled.Value && Plugin.SaveOnTombstone.Value)
			{
				SaveScheduler.Request(Reason.Tombstone, Prio.High, wantLogout: true);
			}
		}
	}

	[HarmonyPatch(typeof(Player), "TeleportTo")]
	private static class Player_TeleportTo
	{
		private static void Prefix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance))
			{
				SaveScheduler.BeforeTeleport(__instance);
			}
		}
	}

	[HarmonyPatch(typeof(Player), "OnSkillLevelup")]
	private static class Player_OnSkillLevelup
	{
		private static void Postfix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance) && Plugin.SaveOnSkillLevelUp.Value)
			{
				SaveScheduler.Request(Reason.SkillUp, Prio.Low, wantLogout: true);
			}
		}
	}

	[HarmonyPatch(typeof(Player), "AddKnownRecipe")]
	private static class Player_AddKnownRecipe
	{
		private static void Postfix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance) && Plugin.SaveOnNewRecipe.Value)
			{
				SaveScheduler.Request(Reason.Recipe, Prio.Low, wantLogout: true);
			}
		}
	}

	[HarmonyPatch(typeof(Player), "AddKnownPiece")]
	private static class Player_AddKnownPiece
	{
		private static void Postfix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance) && Plugin.SaveOnNewRecipe.Value)
			{
				SaveScheduler.Request(Reason.Recipe, Prio.Low, wantLogout: true);
			}
		}
	}

	[HarmonyPatch(typeof(Player), "SetGuardianPower")]
	private static class Player_SetGuardianPower
	{
		private static void Postfix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance) && Plugin.SaveOnBossKill.Value)
			{
				SaveScheduler.Request(Reason.Boss, Prio.Normal, wantLogout: true);
			}
		}
	}

	[HarmonyPatch(typeof(Character), "OnDeath")]
	private static class Character_OnDeath
	{
		private static void Postfix(Character __instance)
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.SaveOnBossKill.Value && !((Object)(object)__instance == (Object)null) && __instance.IsBoss())
			{
				Player localPlayer = Player.m_localPlayer;
				if (!((Object)(object)localPlayer == (Object)null) && Vector3.Distance(((Component)localPlayer).transform.position, ((Component)__instance).transform.position) < 100f)
				{
					SaveScheduler.Request(Reason.Boss, Prio.Normal, wantLogout: true, 5f);
				}
			}
		}
	}

	[HarmonyPatch(typeof(Player), "OnDamaged")]
	private static class Player_OnDamaged
	{
		private static void Postfix(Player __instance)
		{
			if (IsLocal((Character)(object)__instance))
			{
				SafetyState.MarkCombatAction();
			}
		}
	}

	private static bool IsLocal(Character c)
	{
		if ((Object)(object)c != (Object)null)
		{
			return (Object)(object)c == (Object)(object)Player.m_localPlayer;
		}
		return false;
	}
}
[BepInPlugin("nfp007.SmartSave", "SmartSave", "1.3.0")]
public class Plugin : BaseUnityPlugin
{
	public const string Guid = "nfp007.SmartSave";

	public const string Name = "SmartSave";

	public const string Version = "1.3.0";

	internal static Plugin Instance;

	internal static ManualLogSource Log;

	internal static ConfigEntry<bool> Enabled;

	internal static ConfigEntry<bool> LogSaves;

	internal static ConfigEntry<bool> FastMapSave;

	internal static ConfigEntry<float> MapPrewarmSec;

	internal static ConfigEntry<float> SkipRepeatSaveSec;

	internal static ConfigEntry<BackgroundWriteMode> BackgroundWrite;

	internal static ConfigEntry<SaveMessageMode> SaveMessage;

	internal static ConfigEntry<float> MinSaveIntervalSec;

	internal static ConfigEntry<KeyboardShortcut> ManualSaveKey;

	internal static ConfigEntry<MapSaveMode> MapSave;

	internal static ConfigEntry<bool> SaveAfterCrafting;

	internal static ConfigEntry<bool> SaveOnChestClose;

	internal static ConfigEntry<bool> SaveOnTombstone;

	internal static ConfigEntry<bool> SaveBeforeTeleport;

	internal static ConfigEntry<bool> SaveAfterTeleport;

	internal static ConfigEntry<bool> SaveOnSkillLevelUp;

	internal static ConfigEntry<bool> SaveOnNewRecipe;

	internal static ConfigEntry<bool> SaveOnBossKill;

	internal static ConfigEntry<bool> SaveAfterCombat;

	internal static ConfigEntry<float> GuiCloseDelaySec;

	internal static ConfigEntry<float> GuiMaxDelaySec;

	internal static ConfigEntry<float> ArrivalDelaySec;

	internal static ConfigEntry<float> FallbackIntervalMin;

	internal static ConfigEntry<float> CombatCooldownSec;

	internal static ConfigEntry<float> CombatScanRadiusM;

	internal static ConfigEntry<bool> AllowProfileSaveInCombat;

	internal static ConfigEntry<bool> SafeServerPushSave;

	internal static ConfigEntry<bool> RequestWorldSaveAsAdmin;

	internal static ConfigEntry<float> WorldSaveMinIntervalMin;

	internal static ConfigEntry<bool> Profiling;

	private Harmony _harmony;

	private void Awake()
	{
		//IL_00af: Unknown result type (might be due to invalid IL or missing references)
		//IL_040d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0417: Expected O, but got Unknown
		Instance = this;
		Log = ((BaseUnityPlugin)this).Logger;
		Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Turn SmartSave on or off.");
		LogSaves = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "LogSaves", true, "Write a line to the BepInEx log for every save (with the reason).");
		SaveMessage = ((BaseUnityPlugin)this).Config.Bind<SaveMessageMode>("General", "SaveMessage", SaveMessageMode.Off, "Show a small \"Saved\" message top left after SmartSave saves.\n  Off       = no message (the manual save key always shows one).\n  Important = only after crafting, chest changes, the tombstone and a boss / forsaken power.\n  All       = after every SmartSave save.");
		MinSaveIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MinSaveIntervalSec", 15f, "Cooldown: the shortest gap in seconds between two less important saves (arrival, boss, end of combat, skill up,\nnew recipe, fallback), so a burst of events gives one save. It never triggers a save by itself.\nImportant saves (crafting, chests, tombstone, teleport, manual) ignore it.");
		ManualSaveKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "ManualSaveKey", new KeyboardShortcut((KeyCode)287, Array.Empty<KeyCode>()), "Key to save right now. Set to None to turn it off.");
		MapSave = ((BaseUnityPlugin)this).Config.Bind<MapSaveMode>("General", "MapSave", MapSaveMode.OnPortal, "When SmartSave's saves also write your explored map. The map is by far the slowest part of a save.\nMeasured on a normal PC (with FastMapSave on): the map part of a save takes under 1 ms when the map is reused\nand ~50 ms right after exploring. Without FastMapSave it takes ~200 ms. A whole save takes ~20 ms, with a local\ncharacter and with a Steam Cloud character (BackgroundWrite on).\n  OnPortal = only the save right before a teleport writes the map (the screen fades out anyway).\n  Always   = every SmartSave save writes the map: nothing of the map is ever lost, but every save freezes.\n  Never    = SmartSave never writes the map.\nThe game's own saves (server save interval, logout) and the manual save key always write the map.\nWithout the map, a crash can lose map exploration and new pins since the last save that wrote it. Never items.");
		FastMapSave = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "FastMapSave", true, "Build the explored map data much faster whenever it is saved (by SmartSave and by the game's own saves),\nand reuse it when your explored map and pins haven't changed since the last save (under 1 ms instead of ~50 ms).\nSafeguard: once per game start, when you join a world (usually behind the loading screen), the map data is built\nboth ways (SmartSave's and the game's) and compared byte by byte. Only if they are identical is the faster version\nused for the rest of the session. If they differ (after a game update, or because another mod changes the map data),\nSmartSave switches back to the game's own method and writes a warning to the log.\nIf another map mod misbehaves (pins or map not saved as expected), set this to false.");
		MapPrewarmSec = ((BaseUnityPlugin)this).Config.Bind<float>("General", "MapPrewarmSec", 60f, "With FastMapSave: every this many seconds, if your explored map or pins changed, SmartSave prepares the map data\nin the background (a ~1-2 ms copy in the game, the rest on another CPU core). The next save that writes the map then\ntakes ~1-2 ms for the map instead of ~50 ms. If nothing changed since the last save, the map is reused anyway.\n0 = off (the map is then prepared during the save itself).");
		BackgroundWrite = ((BaseUnityPlugin)this).Config.Bind<BackgroundWriteMode>("General", "BackgroundWrite", BackgroundWriteMode.CloudOnly, "Steam Cloud characters: the save is made in the frame as always (all data captured at once), but writing it to\nSteam Cloud happens in the background through Steam's own background write. A Steam Cloud save then freezes the\ngame much shorter. The game's safe order is kept (.new, .old, .fch). Quitting and logging out always wait for it.\nIf Steam reports an error, the save is kept as a local backup (like the game does) and this switches itself off.\n  CloudOnly = Steam Cloud characters only (local characters already save fast).\n  Off       = the game's own cloud save.");
		SkipRepeatSaveSec = ((BaseUnityPlugin)this).Config.Bind<float>("General", "SkipRepeatSaveSec", 30f, "Skip the game's own regular save (the server's save interval, or the game's 30-minute timer) if a full save\n(with the map and your position) already ran within this many seconds. It would write exactly the same again.\nSaves when quitting, logging out or sleeping are never skipped. 0 = off.");
		SaveAfterCrafting = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveAfterCrafting", true, "Save after crafting, upgrading or repairing, as soon as the crafting window is closed.");
		SaveOnChestClose = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveOnChestClose", true, "Save after closing a chest (or the inventory) - only if your inventory changed while it was open.");
		GuiCloseDelaySec = ((BaseUnityPlugin)this).Config.Bind<float>("Triggers", "GuiCloseDelaySec", 1.5f, "Seconds the inventory/crafting window must stay closed before the save runs. Going from chest to chest gives one save, not ten.");
		GuiMaxDelaySec = ((BaseUnityPlugin)this).Config.Bind<float>("Triggers", "GuiMaxDelaySec", 20f, "If the window stays open longer than this after a change, save anyway.");
		SaveOnTombstone = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveOnTombstone", true, "Save right after you take everything from your tombstone.");
		SaveBeforeTeleport = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveBeforeTeleport", true, "Save just before any teleport (portals, dungeon entrances, mods that teleport).");
		SaveAfterTeleport = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveAfterTeleport", true, "Save a few seconds after arriving from a teleport.");
		ArrivalDelaySec = ((BaseUnityPlugin)this).Config.Bind<float>("Triggers", "ArrivalDelaySec", 5f, "Seconds to wait after arriving before saving.");
		SaveOnSkillLevelUp = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveOnSkillLevelUp", true, "Save after a skill level up (when out of combat).");
		SaveOnNewRecipe = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveOnNewRecipe", true, "Save after learning a new recipe or building piece.");
		SaveOnBossKill = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveOnBossKill", true, "Save after a boss dies near you, or when you take a new forsaken power.");
		SaveAfterCombat = ((BaseUnityPlugin)this).Config.Bind<bool>("Triggers", "SaveAfterCombat", true, "If something waits to be saved, save as soon as the fight is over.");
		FallbackIntervalMin = ((BaseUnityPlugin)this).Config.Bind<float>("Triggers", "FallbackIntervalMin", 30f, "Save at the next safe moment if nothing saved the character for this many minutes. 0 = off.\n(On servers with -saveinterval the server already asks every client to save at that interval.)");
		CombatCooldownSec = ((BaseUnityPlugin)this).Config.Bind<float>("Combat", "CombatCooldownSec", 10f, "You count as 'in combat' for this many seconds after taking damage or attacking.");
		CombatScanRadiusM = ((BaseUnityPlugin)this).Config.Bind<float>("Combat", "CombatScanRadiusM", 30f, "An alerted enemy that targets you within this distance (metres) means 'in combat'.");
		AllowProfileSaveInCombat = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "AllowProfileSaveInCombat", true, "true = important saves (crafting, chests, tombstone, teleport) still save your inventory during combat, but your logout point is not moved.\nfalse = wait until the fight is over.");
		SafeServerPushSave = ((BaseUnityPlugin)this).Config.Bind<bool>("Combat", "SafeServerPushSave", true, "The server regularly tells every client to save (with -saveinterval). Vanilla then also moves your logout point, even mid-fight. true = during combat that save keeps your inventory but does not move the logout point.");
		RequestWorldSaveAsAdmin = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "RequestWorldSaveAsAdmin", false, "Only for server admins. After important saves, also ask the server to save the world.\nThis makes the game stutter for everybody. Keep it off unless your server crashes too.");
		WorldSaveMinIntervalMin = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "WorldSaveMinIntervalMin", 5f, "Minimum minutes between two world save requests.");
		Profiling = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "Profiling", false, "Measure every character save (SmartSave's and the game's own) and write the results to the BepInEx log\nand to BepInEx/SmartSave_profile.csv: total time, the slow parts (map, compression, file write, Steam Cloud),\nmemory and the frame times right after the save. Needs a game restart to change.");
		_harmony = new Harmony("nfp007.SmartSave");
		_harmony.PatchAll(typeof(Plugin).Assembly);
		SaveProfiler.Init(_harmony);
		Log.LogInfo((object)"SmartSave 1.3.0 loaded");
	}

	private void Update()
	{
		try
		{
			SaveProfiler.Tick();
			FastMapData.Tick();
			CloudWriter.Tick();
			SaveScheduler.Tick();
		}
		catch (Exception ex)
		{
			Log.LogError((object)ex);
		}
	}

	private void OnDestroy()
	{
		Harmony harmony = _harmony;
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
	}
}
internal static class SafetyState
{
	private static readonly Func<MusicMan, bool> MusicInCombat = MakeMusicInCombat();

	private static float _lastCombatActionTime = -999f;

	private static float _nextScanTime;

	private static bool _enemyTargetsMe;

	private static Func<MusicMan, bool> MakeMusicInCombat()
	{
		try
		{
			MethodInfo methodInfo = AccessTools.Method(typeof(MusicMan), "InCombat", (Type[])null, (Type[])null);
			return (methodInfo == null) ? null : AccessTools.MethodDelegate<Func<MusicMan, bool>>(methodInfo, (object)null, true);
		}
		catch (Exception ex)
		{
			ManualLogSource log = Plugin.Log;
			if (log != null)
			{
				log.LogWarning((object)("MusicMan.InCombat not found, using own combat detection only: " + ex.Message));
			}
			return null;
		}
	}

	internal static void MarkCombatAction()
	{
		_lastCombatActionTime = Time.time;
	}

	internal static bool IsBlocked(Player p)
	{
		if ((Object)(object)p == (Object)null || (Object)(object)Game.instance == (Object)null || (Object)(object)ZNet.instance == (Object)null)
		{
			return true;
		}
		if (((Character)p).IsDead() || ((Character)p).IsTeleporting())
		{
			return true;
		}
		if (((Character)p).InCutscene())
		{
			return true;
		}
		return false;
	}

	internal static bool InCombat(Player p)
	{
		if ((Object)(object)p == (Object)null)
		{
			return false;
		}
		if (Time.time - _lastCombatActionTime < Plugin.CombatCooldownSec.Value)
		{
			return true;
		}
		if (((Character)p).InAttack() || ((Character)p).IsBlocking())
		{
			return true;
		}
		MusicMan instance = MusicMan.instance;
		if ((Object)(object)instance != (Object)null && MusicInCombat != null)
		{
			try
			{
				if (MusicInCombat(instance))
				{
					return true;
				}
			}
			catch
			{
			}
		}
		if (Time.time >= _nextScanTime)
		{
			_nextScanTime = Time.time + 0.5f;
			_enemyTargetsMe = ScanEnemies(p);
		}
		return _enemyTargetsMe;
	}

	private static bool ScanEnemies(Player p)
	{
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		//IL_0018: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: 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_005c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0061: Unknown result type (might be due to invalid IL or missing references)
		float value = Plugin.CombatScanRadiusM.Value;
		float num = value * value;
		Vector3 position = ((Component)p).transform.position;
		foreach (Character allCharacter in Character.GetAllCharacters())
		{
			if ((Object)(object)allCharacter == (Object)null || (Object)(object)allCharacter == (Object)(object)p || allCharacter.IsPlayer() || allCharacter.IsDead())
			{
				continue;
			}
			Vector3 val = ((Component)allCharacter).transform.position - position;
			if (!(((Vector3)(ref val)).sqrMagnitude > num))
			{
				BaseAI baseAI = allCharacter.GetBaseAI();
				if (!((Object)(object)baseAI == (Object)null) && baseAI.IsAlerted() && BaseAI.IsEnemy(allCharacter, (Character)(object)p) && (Object)(object)baseAI.GetTargetCreature() == (Object)(object)p)
				{
					return true;
				}
			}
		}
		return false;
	}

	internal static bool IsLogoutSafe(Player p)
	{
		if (IsBlocked(p) || InCombat(p))
		{
			return false;
		}
		if (!((Character)p).IsOnGround())
		{
			return false;
		}
		if (((Character)p).IsSwimming() || ((Character)p).IsAttached() || ((Character)p).IsAttachedToShip())
		{
			return false;
		}
		if (((Character)p).IsFlying())
		{
			return false;
		}
		return true;
	}

	internal static bool IsTyping()
	{
		if ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus())
		{
			return true;
		}
		if (Console.IsVisible() || TextInput.IsVisible() || Minimap.InTextInput())
		{
			return true;
		}
		return false;
	}
}
internal static class SaveProfiler
{
	private static readonly (string label, string type, string method)[] Targets = new(string, string, string)[15]
	{
		("loadHintPush", "SystemResourceManager", "PushIsInLoadingScreen"),
		("cloudMount", "FileHelpers", "Mount"),
		("playerData", "PlayerProfile", "SavePlayerData"),
		("mapTotal", "Minimap", "SaveMapData"),
		("mapBuild", "Minimap", "GetMapData"),
		("compress", "ZPackage", "WriteCompressed"),
		("toDiskTotal", "PlayerProfile", "SavePlayerToDisk"),
		("cloudChecks", "SaveSystem", "PreSaveCloudChecksAndOperations"),
		("hash", "ZPackage", "GenerateHash"),
		("writeFile", "FileWriter", "Finish"),
		("replaceFile", "FileHelpers", "ReplaceOldFile"),
		("backupCheck", "ZNet", "ConsiderAutoBackup"),
		("cacheInvalidate", "SaveSystem", "InvalidateCache"),
		("cloudUnmount", "FileHelpers", "Unmount"),
		("loadHintPop", "SystemResourceManager", "PopIsInLoadingScreen")
	};

	private static readonly string[] Columns = Array.ConvertAll(Targets, ((string label, string type, string method) t) => t.label);

	private static readonly Dictionary<MethodBase, string> Labels = new Dictionary<MethodBase, string>();

	private static readonly Dictionary<string, double> Parts = new Dictionary<string, double>();

	private static readonly double TickMs = 1000.0 / (double)Stopwatch.Frequency;

	private static int _depth;

	private static long _start;

	private static string _source;

	private static bool _logoutPoint;

	private static int _gc0;

	private static int _gc1;

	private static int _gc2;

	private static long _memBefore;

	private static int _saveNo;

	private static float _avgFrameMs = 16f;

	private static string _pendingLine;

	private static string _pendingRow;

	private static readonly List<float> _framesAfter = new List<float>();

	private const int FramesToWatch = 3;

	private static readonly Dictionary<string, int> Running = new Dictionary<string, int>();

	internal static bool Active { get; private set; }

	private static string CsvPath => Path.Combine(Paths.BepInExRootPath, "SmartSave_profile.csv");

	internal static void Init(Harmony harmony)
	{
		//IL_0023: Unknown result type (might be due to invalid IL or missing references)
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		//IL_0034: Expected O, but got Unknown
		//IL_004a: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0057: Expected O, but got Unknown
		//IL_01bd: Unknown result type (might be due to invalid IL or missing references)
		//IL_01c2: Unknown result type (might be due to invalid IL or missing references)
		//IL_01e5: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ea: Unknown result type (might be due to invalid IL or missing references)
		//IL_01f7: Expected O, but got Unknown
		//IL_01f7: Expected O, but got Unknown
		if (!Plugin.Profiling.Value)
		{
			return;
		}
		HarmonyMethod val = new HarmonyMethod(AccessTools.Method(typeof(SaveProfiler), "PartPrefix", (Type[])null, (Type[])null))
		{
			priority = 800
		};
		HarmonyMethod val2 = new HarmonyMethod(AccessTools.Method(typeof(SaveProfiler), "PartPostfix", (Type[])null, (Type[])null))
		{
			priority = 0
		};
		int num = 0;
		(string, string, string)[] targets = Targets;
		for (int i = 0; i < targets.Length; i++)
		{
			(string, string, string) tuple = targets[i];
			string item = tuple.Item1;
			string item2 = tuple.Item2;
			string item3 = tuple.Item3;
			Type type = AccessTools.TypeByName(item2);
			if (type == null)
			{
				Plugin.Log.LogWarning((object)("profiler: type " + item2 + " not found"));
				continue;
			}
			foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type))
			{
				if (!(declaredMethod.Name != item3) && !declaredMethod.IsAbstract && !declaredMethod.IsGenericMethodDefinition)
				{
					try
					{
						harmony.Patch((MethodBase)declaredMethod, val, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
						Labels[declaredMethod] = item;
						num++;
					}
					catch (Exception ex)
					{
						Plugin.Log.LogWarning((object)("profiler: could not patch " + item2 + "." + item3 + ": " + ex.Message));
					}
				}
			}
		}
		MethodInfo methodInfo = AccessTools.Method(typeof(Game), "SavePlayerProfile", (Type[])null, (Type[])null);
		harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(SaveProfiler), "SavePrefix", (Type[])null, (Type[])null))
		{
			priority = 800
		}, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(AccessTools.Method(typeof(SaveProfiler), "SaveFinalizer", (Type[])null, (Type[])null))
		{
			priority = 0
		}, (HarmonyMethod)null);
		Active = true;
		Plugin.Log.LogInfo((object)$"profiler on: {num} methods measured, CSV: {CsvPath}");
	}

	private static void SavePrefix(bool setLogoutPoint, bool isFromRpc)
	{
		if (_depth++ <= 0)
		{
			FlushPending();
			Parts.Clear();
			Running.Clear();
			_logoutPoint = setLogoutPoint;
			_source = (SaveScheduler.SavingNow ? ("SmartSave:" + SaveScheduler.ReasonText(SaveScheduler.CurrentReason)) : (isFromRpc ? "game:serverPush" : "game"));
			_gc0 = GC.CollectionCount(0);
			_gc1 = GC.CollectionCount(1);
			_gc2 = GC.CollectionCount(2);
			_memBefore = GC.GetTotalMemory(forceFullCollection: false);
			_start = Stopwatch.GetTimestamp();
		}
	}

	private static Exception SaveFinalizer(Exception __exception)
	{
		if (--_depth > 0)
		{
			return __exception;
		}
		double num = (double)(Stopwatch.GetTimestamp() - _start) * TickMs;
		long num2 = (GC.GetTotalMemory(forceFullCollection: false) - _memBefore) / 1024;
		int num3 = GC.CollectionCount(0) - _gc0;
		int num4 = GC.CollectionCount(1) - _gc1;
		int num5 = GC.CollectionCount(2) - _gc2;
		_saveNo++;
		string text = "?";
		string text2 = "";
		try
		{
			Game instance = Game.instance;
			PlayerProfile val = ((instance != null) ? instance.GetPlayerProfile() : null);
			if (val != null)
			{
				text = ((object)Unsafe.As<FileSource, FileSource>(ref val.m_fileSource)/*cast due to .constrained prefix*/).ToString();
				string path = val.GetPath();
				if (!string.IsNullOrEmpty(path) && File.Exists(path))
				{
					text2 = (new FileInfo(path).Length / 1024).ToString();
				}
			}
		}
		catch
		{
		}
		if (SaveScheduler.SkippedNow)
		{
			_source += " (skipped)";
		}
		else
		{
			_source += (Parts.ContainsKey("mapBuild") ? (" map=" + FastMapData.LastPath) : " map=none");
		}
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append($"[profile #{_saveNo}] {_source} logoutPoint={_logoutPoint} total={num:F0} ms |");
		string[] columns = Columns;
		foreach (string text3 in columns)
		{
			if (Parts.TryGetValue(text3, out var value))
			{
				stringBuilder.Append($" {text3}={value:F1}");
			}
		}
		stringBuilder.Append(string.Format(" | storage={0} file={1} alloc={2} KB gc={3}/{4}/{5}", text, (text2 == "") ? "?" : (text2 + " KB"), num2, num3, num4, num5));
		if (__exception != null)
		{
			stringBuilder.Append(" | EXCEPTION " + __exception.GetType().Name);
		}
		StringBuilder stringBuilder2 = new StringBuilder();
		stringBuilder2.Append(FormattableString.Invariant($"{DateTime.Now:yyyy-MM-dd HH:mm:ss},{_saveNo},{_source},{_logoutPoint},{num:F1}"));
		columns = Columns;
		foreach (string key in columns)
		{
			stringBuilder2.Append(',').Append(Parts.TryGetValue(key, out var value2) ? value2.ToString("F1", CultureInfo.InvariantCulture) : "");
		}
		stringBuilder2.Append(FormattableString.Invariant($",{text},{text2},{num2},{num3},{num4},{num5},{_avgFrameMs:F1}"));
		_pendingLine = stringBuilder.ToString();
		_pendingRow = stringBuilder2.ToString();
		_framesAfter.Clear();
		return __exception;
	}

	private static void PartPrefix(MethodBase __originalMethod, out long __state)
	{
		__state = 0L;
		if (_depth != 0 && Labels.TryGetValue(__originalMethod, out var value))
		{
			Running.TryGetValue(value, out var value2);
			Running[value] = value2 + 1;
			if (value2 == 0)
			{
				__state = Stopwatch.GetTimestamp();
			}
		}
	}

	private static void PartPostfix(MethodBase __originalMethod, long __state)
	{
		if (_depth != 0 && Labels.TryGetValue(__originalMethod, out var value))
		{
			if (Running.TryGetValue(value, out var value2) && value2 > 0)
			{
				Running[value] = value2 - 1;
			}
			if (__state != 0L)
			{
				double num = (double)(Stopwatch.GetTimestamp() - __state) * TickMs;
				Parts[value] = (Parts.TryGetValue(value, out var value3) ? (value3 + num) : num);
			}
		}
	}

	internal static void Tick()
	{
		if (!Active)
		{
			return;
		}
		float num = Time.unscaledDeltaTime * 1000f;
		if (_pendingLine == null)
		{
			if (num < _avgFrameMs * 3f || _avgFrameMs <= 0f)
			{
				_avgFrameMs = _avgFrameMs * 0.95f + num * 0.05f;
			}
			return;
		}
		_framesAfter.Add(num);
		if (_framesAfter.Count >= 3)
		{
			FlushPending();
		}
	}

	private static void FlushPending()
	{
		if (_pendingLine == null)
		{
			return;
		}
		string arg = string.Join(" ", _framesAfter.ConvertAll((float f) => f.ToString("F0")));
		Plugin.Log.LogInfo((object)$"{_pendingLine} | frames after: {arg} ms (normal {_avgFrameMs:F0} ms)");
		try
		{
			bool flag = !File.Exists(CsvPath);
			using StreamWriter streamWriter = new StreamWriter(CsvPath, append: true);
			if (flag)
			{
				streamWriter.WriteLine("time,no,source,logoutPoint,totalMs," + string.Join(",", Columns) + ",storage,fileKb,allocKb,gc0,gc1,gc2,normalFrameMs,frameAfter1,frameAfter2,frameAfter3");
			}
			List<string> list = _framesAfter.ConvertAll((float x) => x.ToString("F1", CultureInfo.InvariantCulture));
			while (list.Count < 3)
			{
				list.Add("");
			}
			streamWriter.WriteLine(_pendingRow + "," + string.Join(",", list));
		}
		catch (Exception ex)
		{
			Plugin.Log.LogWarning((object)("profiler: could not write CSV: " + ex.Message));
		}
		_pendingLine = null;
		_pendingRow = null;
		_framesAfter.Clear();
	}
}
[Flags]
internal enum Reason
{
	None = 0,
	Craft = 1,
	Inventory = 2,
	Tombstone = 4,
	Teleport = 8,
	Arrival = 0x10,
	SkillUp = 0x20,
	Recipe = 0x40,
	Boss = 0x80,
	CombatEnd = 0x100,
	Fallback = 0x200,
	Manual = 0x400,
	Repair = 0x800
}
public enum MapSaveMode
{
	OnPortal,
	Always,
	Never
}
public enum SaveMessageMode
{
	Off,
	Important,
	All
}
internal enum Prio
{
	None,
	Low,
	Normal,
	High
}
internal static class SaveScheduler
{
	private static Reason _pending;

	private static Prio _prio;

	private static bool _wantLogout;

	private static float _pendingSince;

	private static float _notBefore;

	private static bool _guiDirty;

	private static Reason _guiReason;

	private static float _guiDirtySince;

	private static float _guiClosedAt = -1f;

	private static float _lastSaveTime;

	private static float _lastWorldSaveRequest = -9999f;

	private static bool _wasInCombat;

	private static bool _wasTeleporting;

	private static float _nextTick;

	private static bool _savingNow;

	private static bool _skipMapNow;

	private static bool _inSession;

	private static float _sessionStart;

	private const Reason LoginNoise = Reason.SkillUp | Reason.Recipe | Reason.Boss;

	private const float LoginQuietSec = 10f;

	private static bool _mapWritten;

	internal static bool SkippedNow;

	private static float _lastFullSave = -9999f;

	private const Reason ImportantForMessage = Reason.Craft | Reason.Inventory | Reason.Tombstone | Reason.Boss | Reason.Manual;

	internal static bool SavingNow => _savingNow;

	internal static bool SkipMapNow => _skipMapNow;

	internal static Reason CurrentReason { get; private set; }

	internal static void Request(Reason reason, Prio prio, bool wantLogout, float delaySec = 0f)
	{
		if (!Plugin.Enabled.Value)
		{
			return;
		}
		if ((reason & (Reason.SkillUp | Reason.Recipe | Reason.Boss)) != Reason.None && (!_inSession || Time.time - _sessionStart < 10f))
		{
			reason &= ~(Reason.SkillUp | Reason.Recipe | Reason.Boss);
			if (reason == Reason.None)
			{
				return;
			}
		}
		if (_pending == Reason.None)
		{
			_pendingSince = Time.time;
		}
		_pending |= reason;
		if (prio > _prio)
		{
			_prio = prio;
		}
		_wantLogout |= wantLogout;
		if (delaySec > 0f)
		{
			_notBefore = Mathf.Max(_notBefore, Time.time + delaySec);
		}
	}

	internal static void OnInventoryChanged()
	{
		if (Plugin.Enabled.Value && !_savingNow && InventoryGui.IsVisible() && Plugin.SaveOnChestClose.Value)
		{
			MarkGui(Reason.Inventory);
		}
	}

	internal static void OnCrafted()
	{
		if (Plugin.Enabled.Value && Plugin.SaveAfterCrafting.Value)
		{
			MarkGui(Reason.Craft);
		}
	}

	private static void MarkGui(Reason r)
	{
		if (!_guiDirty)
		{
			_guiDirtySince = Time.time;
		}
		_guiDirty = true;
		_guiReason |= r;
	}

	internal static void BeforeTeleport(Player p)
	{
		if (Plugin.Enabled.Value && Plugin.SaveBeforeTeleport.Value && !((Object)(object)p == (Object)null) && !((Character)p).IsDead() && !((Character)p).IsTeleporting() && !((Object)(object)Game.instance == (Object)null) && (!SafetyState.InCombat(p) || Plugin.AllowProfileSaveInCombat.Value))
		{
			FlushGui();
			bool setLogout = SafetyState.IsLogoutSafe(p);
			SaveNow(_pending | Reason.Teleport, setLogout);
		}
	}

	internal static void Tick()
	{
		//IL_0052: Unknown result type (might be due to invalid IL or missing references)
		if (!Plugin.Enabled.Value)
		{
			return;
		}
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null || (Object)(object)Game.instance == (Object)null)
		{
			ResetSession();
			return;
		}
		if (!_inSession)
		{
			_inSession = true;
			_sessionStart = Time.time;
		}
		if (!SafetyState.IsTyping() && IsDown(Plugin.ManualSaveKey.Value))
		{
			if (SafetyState.IsBlocked(localPlayer))
			{
				Message("SmartSave: can't save right now");
			}
			else
			{
				FlushGui();
				SaveNow(_pending | Reason.Manual, SafetyState.IsLogoutSafe(localPlayer), forceMessage: true);
			}
		}
		if (Time.time < _nextTick)
		{
			return;
		}
		_nextTick = Time.time + 0.25f;
		bool flag = ((Character)localPlayer).IsTeleporting();
		if (_wasTeleporting && !flag && Plugin.SaveAfterTeleport.Value)
		{
			Request(Reason.Arrival, Prio.Normal, wantLogout: true, Plugin.ArrivalDelaySec.Value);
		}
		_wasTeleporting = flag;
		if (_guiDirty)
		{
			bool num = InventoryGui.IsVisible();
			if (num)
			{
				_guiClosedAt = -1f;
			}
			else if (_guiClosedAt < 0f)
			{
				_guiClosedAt = Time.time;
			}
			bool num2 = !num && Time.time - _guiClosedAt >= Plugin.GuiCloseDelaySec.Value;
			bool flag2 = Time.time - _guiDirtySince >= Plugin.GuiMaxDelaySec.Value;
			if (num2 || flag2)
			{
				FlushGui();
			}
		}
		bool flag3 = SafetyState.InCombat(localPlayer);
		if (_wasInCombat && !flag3 && Plugin.SaveAfterCombat.Value && _wantLogout)
		{
			Request(Reason.CombatEnd, Prio.Normal, wantLogout: true);
		}
		_wasInCombat = flag3;
		float value = Plugin.FallbackIntervalMin.Value;
		if (value > 0f && Time.time - _lastSaveTime >= value * 60f && _pending == Reason.None)
		{
			Request(Reason.Fallback, Prio.Low, wantLogout: true);
		}
		TryRunPending(localPlayer, flag3);
	}

	private static void FlushGui()
	{
		if (_guiDirty)
		{
			Request(_guiReason, Prio.High, wantLogout: true);
			_guiDirty = false;
			_guiReason = Reason.None;
			_guiClosedAt = -1f;
		}
	}

	private static void TryRunPending(Player p, bool inCombat)
	{
		if (_pending == Reason.None || Time.time < _notBefore || SafetyState.IsBlocked(p))
		{
			return;
		}
		bool flag = _prio >= Prio.High;
		if (!flag && Time.time - _lastSaveTime < Plugin.MinSaveIntervalSec.Value)
		{
			return;
		}
		if (inCombat)
		{
			if (flag && Plugin.AllowProfileSaveInCombat.Value)
			{
				SaveNow(_pending, setLogout: false);
			}
			return;
		}
		bool flag2 = SafetyState.IsLogoutSafe(p);
		if (flag || !_wantLogout || flag2 || !(Time.time - _pendingSince < 60f))
		{
			SaveNow(_pending, _wantLogout && flag2);
		}
	}

	private static void SaveNow(Reason reason, bool setLogout, bool forceMessage = false)
	{
		Game instance = Game.instance;
		if (!((Object)(object)instance == (Object)null))
		{
			float saveTimer = instance.m_saveTimer;
			bool flag = IncludeMap(reason);
			CurrentReason = reason;
			_skipMapNow = !flag;
			_savingNow = true;
			try
			{
				instance.SavePlayerProfile(setLogout, false);
			}
			finally
			{
				_savingNow = false;
				_skipMapNow = false;
				instance.m_saveTimer = saveTimer;
			}
			if (Plugin.LogSaves.Value)
			{
				Plugin.Log.LogInfo((object)string.Format("saved (reason={0}, logoutPoint={1}, map={2})", ReasonText(reason), setLogout, flag ? FastMapData.LastPath : "skipped"));
			}
			if (forceMessage || WantsMessage(reason))
			{
				Message(setLogout ? "SmartSave: saved" : "SmartSave: saved (position not updated)");
			}
			MaybeRequestWorldSave(reason);
		}
	}

	internal static void OnProfileSaveStart()
	{
		_mapWritten = false;
	}

	internal static void OnMapWritten()
	{
		_mapWritten = true;
	}

	internal static bool RecentFullSave(out float ago)
	{
		ago = Time.realtimeSinceStartup - _lastFullSave;
		float value = Plugin.SkipRepeatSaveSec.Value;
		if (Plugin.Enabled.Value && value > 0f && ago < value)
		{
			return (Object)(object)Player.m_localPlayer != (Object)null;
		}
		return false;
	}

	internal static void OnProfileSaved(bool setLogoutPoint)
	{
		_lastSaveTime = Time.time;
		if (setLogoutPoint && _mapWritten)
		{
			_lastFullSave = Time.realtimeSinceStartup;
		}
		_pending = Reason.None;
		_prio = Prio.None;
		_notBefore = 0f;
		if (setLogoutPoint)
		{
			_wantLogout = false;
		}
		else if (!_wantLogout)
		{
			_wantLogout = true;
		}
	}

	private static void MaybeRequestWorldSave(Reason reason)
	{
		if (!Plugin.RequestWorldSaveAsAdmin.Value || (reason & (Reason.Craft | Reason.Inventory | Reason.Tombstone | Reason.Boss | Reason.Manual)) == 0 || Time.time - _lastWorldSaveRequest < Plugin.WorldSaveMinIntervalMin.Value * 60f)
		{
			return;
		}
		ZNet instance = ZNet.instance;
		if ((Object)(object)instance == (Object)null || instance.IsServer())
		{
			return;
		}
		ZRpc serverRPC = instance.GetServerRPC();
		if (serverRPC != null)
		{
			_lastWorldSaveRequest = Time.time;
			serverRPC.Invoke("Save", Array.Empty<object>());
			if (Plugin.LogSaves.Value)
			{
				Plugin.Log.LogInfo((object)"asked the server to save the world");
			}
		}
	}

	private static bool WantsMessage(Reason reason)
	{
		return Plugin.SaveMessage.Value switch
		{
			SaveMessageMode.All => true, 
			SaveMessageMode.Important => (reason & (Reason.Craft | Reason.Inventory | Reason.Tombstone | Reason.Boss | Reason.Manual)) != 0, 
			_ => false, 
		};
	}

	private static bool IncludeMap(Reason reason)
	{
		if ((reason & Reason.Manual) != Reason.None)
		{
			return true;
		}
		return Plugin.MapSave.Value switch
		{
			MapSaveMode.Always => true, 
			MapSaveMode.Never => false, 
			_ => (reason & Reason.Teleport) != 0, 
		};
	}

	internal static string ReasonText(Reason r)
	{
		return r.ToString().Replace(", ", "+");
	}

	private static void ResetSession()
	{
		_inSession = false;
		_pending = Reason.None;
		_prio = Prio.None;
		_notBefore = 0f;
		_wantLogout = false;
		_guiDirty = false;
		_guiReason = Reason.None;
		_guiClosedAt = -1f;
		_wasInCombat = false;
		_wasTeleporting = false;
		_lastSaveTime = Time.time;
	}

	private static bool IsDown(KeyboardShortcut k)
	{
		//IL_0002: 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_002c: Unknown result type (might be due to invalid IL or missing references)
		if ((int)((KeyboardShortcut)(ref k)).MainKey == 0)
		{
			return false;
		}
		if (!ZInput.GetKeyDown(((KeyboardShortcut)(ref k)).MainKey, false))
		{
			return false;
		}
		foreach (KeyCode modifier in ((KeyboardShortcut)(ref k)).Modifiers)
		{
			if (!ZInput.GetKey(modifier, false))
			{
				return false;
			}
		}
		return true;
	}

	private static void Message(string text)
	{
		if ((Object)(object)MessageHud.instance != (Object)null)
		{
			MessageHud.instance.ShowMessage((MessageType)1, text, 0, (Sprite)null, false, true);
		}
	}
}