Decompiled source of NativeBackup v0.1.1

NativeBackup.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Microsoft.Win32;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
[assembly: AssemblyCompany("NativeBackup")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("A simple backup valheim mod")]
[assembly: AssemblyFileVersion("0.1.1.0")]
[assembly: AssemblyInformationalVersion("0.1.1+d4f9f3bb7c02247faff8e1b72c55fa167c6c79e2")]
[assembly: AssemblyProduct("NativeBackup")]
[assembly: AssemblyTitle("NativeBackup")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.1.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace NativeBackup
{
	public static class BackupCoordinator
	{
		public enum BackupStartResult
		{
			Started,
			AlreadyRunning,
			CooldownActive
		}

		private const int MinimumBackupIntervalSeconds = 5;

		private static readonly object _backupGate = new object();

		private static int _backupInProgress;

		private static long _lastBackupStartTicks;

		public static bool IsBackupInProgress => Volatile.Read(ref _backupInProgress) == 1;

		public static bool IsCooldownActive => IsWithinCooldown(DateTime.UtcNow.Ticks);

		public static BackupStartResult TryStartBackup(string targetWorld, string targetCharacter)
		{
			long ticks = DateTime.UtcNow.Ticks;
			lock (_backupGate)
			{
				if (Volatile.Read(ref _backupInProgress) == 1)
				{
					return BackupStartResult.AlreadyRunning;
				}
				if (IsWithinCooldown(ticks))
				{
					return BackupStartResult.CooldownActive;
				}
				_backupInProgress = 1;
				_lastBackupStartTicks = ticks;
			}
			Task.Run(delegate
			{
				try
				{
					BackupManager.PerformFullBackup(targetWorld, targetCharacter);
				}
				catch (Exception ex)
				{
					ManualLogSource log = NativeBackupPlugin.Log;
					if (log != null)
					{
						log.LogError((object)ex);
					}
					NativeBackupPlugin.QueueUIMessage("Backup failed unexpectedly.");
				}
				finally
				{
					lock (_backupGate)
					{
						Volatile.Write(ref _backupInProgress, 0);
					}
				}
			});
			return BackupStartResult.Started;
		}

		private static bool IsWithinCooldown(long nowTicks)
		{
			long num = Volatile.Read(ref _lastBackupStartTicks);
			if (num == 0L)
			{
				return false;
			}
			return nowTicks - num < TimeSpan.FromSeconds(5.0).Ticks;
		}
	}
	public static class BackupManager
	{
		public enum BackupSaveType
		{
			Unknown,
			Character,
			World
		}

		public struct BackupArchiveInfo
		{
			public string TargetName;

			public string SourceCategory;

			public string ArchivePath;

			public BackupSaveType SaveType;

			public DateTime CreatedAt;
		}

		public struct BackupTargetInfo
		{
			public string TargetName;

			public string LatestBackupPath;

			public string SourceCategory;

			public BackupSaveType SaveType;

			public DateTime CreatedAt;
		}

		public struct BackupMetrics
		{
			public int Count;

			public long Bytes;
		}

		private sealed class SaveWriteProbe
		{
			public string Label;

			public string Path;

			public DateTime BaselineWriteUtc;
		}

		private const int SaveSyncTimeoutMs = 10000;

		private const int SavePollIntervalMs = 100;

		private const int SaveSettleDelayMs = 1000;

		private const int SaveWriteConfirmationTimeoutMs = 2500;

		public static string GetBackupRootDirectory()
		{
			string text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "Low", "IronGate", "Valheim", "NativeBackup");
			if (!Directory.Exists(text))
			{
				Directory.CreateDirectory(text);
			}
			return text;
		}

		public static void PerformFullBackup(string targetWorld = null, string targetCharacter = null)
		{
			bool flag = !string.IsNullOrEmpty(targetWorld) || !string.IsNullOrEmpty(targetCharacter);
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				string text = "Backup unavailable in this scene.";
				NativeBackupPlugin.QueueUIMessage(text);
				NativeBackupPlugin.Log.LogWarning((object)text);
				return;
			}
			Stopwatch stopwatch = Stopwatch.StartNew();
			NativeBackupPlugin.SetBackupIndicatorActive(active: true);
			try
			{
				if (!TrySyncLiveStateBeforeBackup(targetWorld, targetCharacter))
				{
					string text2 = $"Backup canceled ({stopwatch.Elapsed.TotalSeconds:0.0}s): could not confirm current save state.";
					NativeBackupPlugin.QueueUIMessage(text2);
					NativeBackupPlugin.Log.LogWarning((object)text2);
					return;
				}
				List<string> list = new List<string>();
				if (!string.IsNullOrEmpty(targetWorld) && TryCreateNativeBackup(targetWorld, (SaveDataType)0))
				{
					list.Add(DescribeNativeTarget(BackupSaveType.World, targetWorld));
				}
				if (!string.IsNullOrEmpty(targetCharacter) && TryCreateNativeBackup(targetCharacter, (SaveDataType)1))
				{
					list.Add(DescribeNativeTarget(BackupSaveType.Character, targetCharacter));
				}
				if (!flag && list.Count == 0)
				{
					if (ZNet.instance.IsServer())
					{
						string worldName = ZNet.instance.GetWorldName();
						if (!string.IsNullOrEmpty(worldName) && TryCreateNativeBackup(worldName, (SaveDataType)0))
						{
							list.Add(DescribeNativeTarget(BackupSaveType.World, worldName));
						}
					}
					string currentCharacterSaveName = GetCurrentCharacterSaveName();
					if (!string.IsNullOrEmpty(currentCharacterSaveName) && TryCreateNativeBackup(currentCharacterSaveName, (SaveDataType)1))
					{
						list.Add(DescribeNativeTarget(BackupSaveType.Character, currentCharacterSaveName));
					}
				}
				if (list.Count > 0)
				{
					string arg = string.Join(" and ", list.Distinct().ToArray());
					string text3 = $"Backup complete ({stopwatch.Elapsed.TotalSeconds:0.0}s): {arg}.";
					NativeBackupPlugin.QueueUIMessage(text3);
					NativeBackupPlugin.Log.LogInfo((object)text3);
				}
				else
				{
					string arg2 = (flag ? "requested target unavailable" : "no eligible save target found");
					string text4 = $"Backup failed ({stopwatch.Elapsed.TotalSeconds:0.0}s): {arg2}.";
					NativeBackupPlugin.QueueUIMessage(text4);
					NativeBackupPlugin.Log.LogWarning((object)text4);
				}
			}
			finally
			{
				NativeBackupPlugin.SetBackupIndicatorActive(active: false);
			}
		}

		private static bool TrySyncLiveStateBeforeBackup(string targetWorld, string targetCharacter)
		{
			try
			{
				if ((Object)(object)ZNet.instance == (Object)null)
				{
					NativeBackupPlugin.Log.LogWarning((object)"Could not sync save because ZNet is unavailable.");
					return false;
				}
				List<SaveWriteProbe> probes = CollectSaveWriteProbes(targetWorld, targetCharacter);
				float baselineStartTime = 0f;
				float baselineDoneTime = 0f;
				if (!NativeBackupPlugin.TryInvokeOnMainThread(delegate
				{
					baselineStartTime = ZNet.instance.SaveStartTime;
					baselineDoneTime = ZNet.instance.SaveDoneTime;
				}))
				{
					NativeBackupPlugin.Log.LogWarning((object)"Could not read baseline save timestamp before triggering save.");
					return false;
				}
				string saveTriggerRoute = null;
				bool nativeSaveTriggered = false;
				if (!NativeBackupPlugin.TryInvokeOnMainThread(delegate
				{
					nativeSaveTriggered = TryTriggerNativeSaveLikeMenuButton(out saveTriggerRoute);
				}) || !nativeSaveTriggered)
				{
					NativeBackupPlugin.Log.LogWarning((object)"Failed to trigger native Save-button flow before backup.");
					return false;
				}
				NativeBackupPlugin.Log.LogDebug((object)("Triggered native save via " + saveTriggerRoute + "."));
				DateTime dateTime = DateTime.UtcNow.AddMilliseconds(10000.0);
				bool flag = false;
				while (DateTime.UtcNow < dateTime)
				{
					float currentStartTime = baselineStartTime;
					float currentDoneTime = baselineDoneTime;
					if (!NativeBackupPlugin.TryInvokeOnMainThread(delegate
					{
						currentStartTime = ZNet.instance.SaveStartTime;
						currentDoneTime = ZNet.instance.SaveDoneTime;
					}))
					{
						NativeBackupPlugin.Log.LogWarning((object)"Could not read save completion timestamp from ZNet.");
						return false;
					}
					if (!flag && currentStartTime > baselineStartTime)
					{
						flag = true;
					}
					if (flag && currentDoneTime > baselineDoneTime && currentDoneTime >= currentStartTime)
					{
						Thread.Sleep(1000);
						if (!WaitForProbeWrites(probes))
						{
							NativeBackupPlugin.Log.LogWarning((object)"Save synchronization completed but file writes were not confirmed in time.");
							return false;
						}
						return true;
					}
					Thread.Sleep(100);
				}
				NativeBackupPlugin.Log.LogWarning((object)$"Timed out waiting for ZNet save completion after {10000} ms.");
				return false;
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogWarning((object)("Failed while syncing live save state before backup: " + ex.Message));
				return false;
			}
		}

		private static List<SaveWriteProbe> CollectSaveWriteProbes(string targetWorld, string targetCharacter)
		{
			List<SaveWriteProbe> list = new List<SaveWriteProbe>();
			if (!string.IsNullOrEmpty(targetWorld))
			{
				TryAddSaveWriteProbe(targetWorld, (SaveDataType)0, "world", list);
			}
			if (!string.IsNullOrEmpty(targetCharacter))
			{
				TryAddSaveWriteProbe(targetCharacter, (SaveDataType)1, "character", list);
			}
			return list;
		}

		private static bool TryTriggerNativeSaveLikeMenuButton(out string triggerRoute)
		{
			triggerRoute = "unknown route";
			Menu val = Object.FindAnyObjectByType<Menu>();
			if ((Object)(object)val == (Object)null)
			{
				triggerRoute = "menu unavailable";
				return false;
			}
			MethodInfo methodInfo = AccessTools.Method(typeof(Menu), "OnManualSave", Type.EmptyTypes, (Type[])null);
			if (methodInfo != null)
			{
				try
				{
					methodInfo.Invoke(val, null);
					triggerRoute = "Menu.OnManualSave()";
					return true;
				}
				catch (Exception ex)
				{
					NativeBackupPlugin.Log.LogWarning((object)("Native save method 'OnManualSave' failed: " + ex.Message));
				}
			}
			Button val2 = FindNativeSaveButton(val);
			if ((Object)(object)val2 != (Object)null)
			{
				((UnityEvent)val2.onClick).Invoke();
				triggerRoute = "Menu Save button onClick";
				return true;
			}
			triggerRoute = "no native save method or button found";
			return false;
		}

		private static Button FindNativeSaveButton(Menu menu)
		{
			if ((Object)(object)menu == (Object)null || (Object)(object)menu.m_menuDialog == (Object)null)
			{
				return null;
			}
			Transform transform = ((Component)menu.m_menuDialog).transform;
			Button[] componentsInChildren = ((Component)(transform.Find("MenuEntries") ?? transform.Find("menu") ?? transform.Find("MENU") ?? transform.Find("MenuContainer") ?? transform)).GetComponentsInChildren<Button>(true);
			foreach (Button val in componentsInChildren)
			{
				if (!((Object)(object)val == (Object)null))
				{
					if (string.Equals(((Object)val).name, "Save", StringComparison.OrdinalIgnoreCase) || string.Equals(((Object)val).name, "ButtonSave", StringComparison.OrdinalIgnoreCase))
					{
						return val;
					}
					TMP_Text componentInChildren = ((Component)val).GetComponentInChildren<TMP_Text>(true);
					if (string.Equals(((Object)(object)componentInChildren != (Object)null && componentInChildren.text != null) ? componentInChildren.text.Trim() : string.Empty, "Save", StringComparison.OrdinalIgnoreCase))
					{
						return val;
					}
				}
			}
			return null;
		}

		private static void TryAddSaveWriteProbe(string saveName, SaveDataType saveDataType, string labelPrefix, List<SaveWriteProbe> probes)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				SaveWithBackups val = default(SaveWithBackups);
				if (SaveSystem.TryGetSaveByName(saveName, saveDataType, ref val) && val != null && val.PrimaryFile != null)
				{
					string pathPrimary = val.PrimaryFile.PathPrimary;
					if (!string.IsNullOrEmpty(pathPrimary) && File.Exists(pathPrimary))
					{
						probes.Add(new SaveWriteProbe
						{
							Label = labelPrefix + " '" + saveName + "'",
							Path = pathPrimary,
							BaselineWriteUtc = File.GetLastWriteTimeUtc(pathPrimary)
						});
					}
				}
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogDebug((object)("Failed to collect save write probe for " + labelPrefix + " '" + saveName + "': " + ex.Message));
			}
		}

		private static bool WaitForProbeWrites(List<SaveWriteProbe> probes)
		{
			if (probes == null || probes.Count == 0)
			{
				return true;
			}
			DateTime dateTime = DateTime.UtcNow.AddMilliseconds(2500.0);
			while (DateTime.UtcNow < dateTime)
			{
				bool flag = true;
				foreach (SaveWriteProbe probe in probes)
				{
					if (!File.Exists(probe.Path))
					{
						flag = false;
						break;
					}
					if (File.GetLastWriteTimeUtc(probe.Path) <= probe.BaselineWriteUtc)
					{
						flag = false;
						break;
					}
				}
				if (flag)
				{
					return true;
				}
				Thread.Sleep(100);
			}
			return false;
		}

		private static bool TryCreateNativeBackup(string saveName, SaveDataType saveDataType)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				SaveWithBackups val = default(SaveWithBackups);
				if (!SaveSystem.TryGetSaveByName(saveName, saveDataType, ref val) || val == null)
				{
					NativeBackupPlugin.Log.LogWarning((object)$"Native backup skipped because save '{saveName}' was not found for type {saveDataType}.");
					return false;
				}
				SaveFile primaryFile = val.PrimaryFile;
				if (primaryFile == null)
				{
					NativeBackupPlugin.Log.LogWarning((object)("Native backup skipped because no primary file exists for '" + saveName + "'."));
					return false;
				}
				bool num = InvokeMoveToBackup(primaryFile, DateTime.Now);
				if (num)
				{
					NativeBackupPlugin.Log.LogDebug((object)("Native backup created for " + DescribeNativeTarget(((int)saveDataType != 0) ? BackupSaveType.Character : BackupSaveType.World, saveName)));
					PruneNativeBackupsForTarget(primaryFile);
				}
				else
				{
					NativeBackupPlugin.Log.LogWarning((object)("Native backup call did not create a backup entry for " + DescribeNativeTarget(((int)saveDataType != 0) ? BackupSaveType.Character : BackupSaveType.World, saveName) + "."));
				}
				return num;
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogError((object)("Native backup failed for " + saveName + ": " + ex.Message));
				return false;
			}
		}

		private static string DescribeNativeTarget(BackupSaveType saveType, string saveName)
		{
			if (string.IsNullOrEmpty(saveName))
			{
				if (saveType != BackupSaveType.World)
				{
					return "character";
				}
				return "world";
			}
			if (saveType != BackupSaveType.World)
			{
				return "character '" + saveName + "'";
			}
			return "world '" + saveName + "'";
		}

		private static void PruneNativeBackupsForTarget(SaveFile primaryFile)
		{
			try
			{
				int num = ((NativeBackupPlugin.MaxBackupsPerSave != null) ? NativeBackupPlugin.MaxBackupsPerSave.Value : 0);
				if (num <= 0 || primaryFile == null)
				{
					return;
				}
				string pathPrimary = primaryFile.PathPrimary;
				if (string.IsNullOrEmpty(pathPrimary))
				{
					return;
				}
				string directoryName = Path.GetDirectoryName(pathPrimary);
				if (string.IsNullOrEmpty(directoryName))
				{
					return;
				}
				string path2 = Path.Combine(directoryName, "backups");
				if (!Directory.Exists(path2))
				{
					return;
				}
				string fileName = primaryFile.FileName;
				if (string.IsNullOrEmpty(fileName))
				{
					fileName = Path.GetFileName(pathPrimary);
				}
				if (string.IsNullOrEmpty(fileName))
				{
					return;
				}
				List<FileInfo> list = (from path in Directory.GetFiles(path2, fileName + "*")
					select new FileInfo(path) into file
					orderby file.CreationTimeUtc descending
					select file).ToList();
				if (list.Count <= num)
				{
					return;
				}
				for (int i = num; i < list.Count; i++)
				{
					try
					{
						list[i].Delete();
						NativeBackupPlugin.Log.LogDebug((object)("Pruned native backup: " + list[i].Name));
					}
					catch (Exception ex)
					{
						NativeBackupPlugin.Log.LogWarning((object)("Failed to prune native backup " + list[i].Name + ": " + ex.Message));
					}
				}
			}
			catch (Exception ex2)
			{
				NativeBackupPlugin.Log.LogWarning((object)("Native backup pruning failed: " + ex2.Message));
			}
		}

		private static bool InvokeMoveToBackup(SaveFile saveFile, DateTime now)
		{
			try
			{
				MethodInfo method = typeof(SaveSystem).GetMethod("MoveToBackup", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				if (method == null)
				{
					NativeBackupPlugin.Log.LogWarning((object)"Could not find native SaveSystem.MoveToBackup method.");
					return false;
				}
				object obj = method.Invoke(null, new object[2] { saveFile, now });
				return obj is bool && (bool)obj;
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogError((object)("Reflection call to MoveToBackup failed: " + ex.Message));
				return false;
			}
		}

		public static string GetCurrentCharacterSaveName()
		{
			try
			{
				if ((Object)(object)Game.instance != (Object)null)
				{
					PlayerProfile playerProfile = Game.instance.GetPlayerProfile();
					if (playerProfile != null)
					{
						string filename = playerProfile.GetFilename();
						if (!string.IsNullOrEmpty(filename))
						{
							return filename;
						}
					}
				}
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogWarning((object)("Failed to resolve canonical character save name: " + ex.Message));
			}
			if ((Object)(object)Player.m_localPlayer != (Object)null)
			{
				return Player.m_localPlayer.GetPlayerName();
			}
			return null;
		}

		public static List<string> GetValheimSaveDirectories()
		{
			List<string> list = new List<string>();
			string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "Low", "IronGate", "Valheim");
			string[] array = new string[4] { "characters", "characters_local", "worlds", "worlds_local" };
			foreach (string path2 in array)
			{
				string text = Path.Combine(path, path2);
				if (Directory.Exists(text))
				{
					list.Add(text);
				}
			}
			try
			{
				string text2 = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Valve\\Steam", "InstallPath", null) as string;
				if (string.IsNullOrEmpty(text2))
				{
					text2 = Registry.GetValue("HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Steam", "InstallPath", null) as string;
				}
				if (!string.IsNullOrEmpty(text2))
				{
					string path3 = Path.Combine(text2, "userdata");
					if (Directory.Exists(path3))
					{
						array = Directory.GetDirectories(path3);
						foreach (string path4 in array)
						{
							string text3 = Path.Combine(path4, "892970", "remote", "characters");
							string text4 = Path.Combine(path4, "892970", "remote", "worlds");
							if (Directory.Exists(text3))
							{
								list.Add(text3);
							}
							if (Directory.Exists(text4))
							{
								list.Add(text4);
							}
						}
					}
				}
			}
			catch (Exception ex)
			{
				NativeBackupPlugin.Log.LogWarning((object)("Failed to read Steam path from registry: " + ex.Message));
			}
			return list;
		}

		public static List<string> GetAllAvailableBackups()
		{
			List<string> list = new List<string>();
			string backupRootDirectory = GetBackupRootDirectory();
			if (!Directory.Exists(backupRootDirectory))
			{
				return list;
			}
			string[] directories = Directory.GetDirectories(backupRootDirectory);
			foreach (string path in directories)
			{
				list.AddRange(Directory.GetFiles(path, "*.zip"));
			}
			return list;
		}

		public static List<BackupTargetInfo> GetLatestBackupTargets()
		{
			Dictionary<string, BackupTargetInfo> dictionary = new Dictionary<string, BackupTargetInfo>(StringComparer.OrdinalIgnoreCase);
			foreach (BackupArchiveInfo allBackupArchive in GetAllBackupArchives())
			{
				string targetName = allBackupArchive.TargetName;
				if (!string.IsNullOrEmpty(targetName))
				{
					string key = BuildTargetKey(allBackupArchive.SaveType, targetName);
					if (!dictionary.TryGetValue(key, out var value) || allBackupArchive.CreatedAt > value.CreatedAt)
					{
						dictionary[key] = new BackupTargetInfo
						{
							TargetName = targetName,
							LatestBackupPath = allBackupArchive.ArchivePath,
							SourceCategory = allBackupArchive.SourceCategory,
							SaveType = allBackupArchive.SaveType,
							CreatedAt = allBackupArchive.CreatedAt
						};
					}
				}
			}
			return dictionary.Values.OrderByDescending((BackupTargetInfo entry) => entry.CreatedAt).ToList();
		}

		public static List<BackupArchiveInfo> GetAllBackupArchives()
		{
			List<BackupArchiveInfo> list = new List<BackupArchiveInfo>();
			string backupRootDirectory = GetBackupRootDirectory();
			if (!Directory.Exists(backupRootDirectory))
			{
				return list;
			}
			string[] directories = Directory.GetDirectories(backupRootDirectory);
			foreach (string path in directories)
			{
				string name = new DirectoryInfo(path).Name;
				string[] files = Directory.GetFiles(path, "*.zip");
				for (int j = 0; j < files.Length; j++)
				{
					if (TryCreateBackupArchiveInfo(files[j], name, out var archiveInfo))
					{
						list.Add(archiveInfo);
					}
				}
			}
			return list.OrderByDescending((BackupArchiveInfo archive) => archive.CreatedAt).ToList();
		}

		public static bool TryCreateBackupArchiveInfo(string archivePath, string sourceCategory, out BackupArchiveInfo archiveInfo)
		{
			archiveInfo = default(BackupArchiveInfo);
			if (string.IsNullOrEmpty(archivePath) || !File.Exists(archivePath))
			{
				return false;
			}
			string targetNameFromBackupFile = GetTargetNameFromBackupFile(archivePath);
			if (string.IsNullOrEmpty(targetNameFromBackupFile))
			{
				return false;
			}
			archiveInfo = new BackupArchiveInfo
			{
				TargetName = targetNameFromBackupFile,
				SourceCategory = sourceCategory,
				ArchivePath = archivePath,
				SaveType = GetSaveTypeFromCategory(sourceCategory),
				CreatedAt = File.GetCreationTime(archivePath)
			};
			return true;
		}

		public static string GetTargetNameFromBackupFile(string backupPath)
		{
			if (string.IsNullOrEmpty(backupPath))
			{
				return null;
			}
			string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(backupPath);
			if (string.IsNullOrEmpty(fileNameWithoutExtension))
			{
				return null;
			}
			Match match = Regex.Match(fileNameWithoutExtension, "^\\d{8}_\\d{6}-(.+)$");
			if (match.Success)
			{
				return match.Groups[1].Value.Trim();
			}
			int num = fileNameWithoutExtension.IndexOf('-');
			if (num >= 0 && num < fileNameWithoutExtension.Length - 1)
			{
				return fileNameWithoutExtension.Substring(num + 1).Trim();
			}
			return fileNameWithoutExtension.Trim();
		}

		public static BackupSaveType GetSaveTypeFromCategory(string sourceCategory)
		{
			if (string.IsNullOrEmpty(sourceCategory))
			{
				return BackupSaveType.Unknown;
			}
			string text = sourceCategory.ToLowerInvariant();
			if (text.Contains("character"))
			{
				return BackupSaveType.Character;
			}
			if (text.Contains("world"))
			{
				return BackupSaveType.World;
			}
			return BackupSaveType.Unknown;
		}

		private static string BuildTargetKey(BackupSaveType saveType, string targetName)
		{
			return $"{saveType}:{targetName}";
		}
	}
	[BepInPlugin("com.aloncifer.nativebackup", "NativeBackup", "0.1.1")]
	public class NativeBackupPlugin : BaseUnityPlugin
	{
		public const string PluginGUID = "com.aloncifer.nativebackup";

		public const string PluginName = "NativeBackup";

		public const string PluginVersion = "0.1.1";

		private Harmony _harmony;

		public static NativeBackupPlugin Instance;

		private static readonly ConcurrentQueue<string> _uiMessageQueue = new ConcurrentQueue<string>();

		private static readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();

		private static int _backupIndicatorActive;

		private static Texture2D _backupIndicatorTexture;

		private static bool _iconLoadInProgress;

		private static bool _iconLoadCompleted;

		private static int _nativeFallbackActive;

		private static readonly string[] _indicatorMethodNames = new string[4] { "ShowSavingIcon", "SetSavingIcon", "SetSavingIndicator", "SetSaving" };

		private static readonly string[] _indicatorEnableMethodNames = new string[3] { "ShowSavingIcon", "ShowSaving", "StartSavingIcon" };

		private static readonly string[] _indicatorDisableMethodNames = new string[3] { "HideSavingIcon", "HideSaving", "StopSavingIcon" };

		private static readonly string[] _indicatorFieldNames = new string[3] { "m_showSavingIcon", "m_showSaving", "m_saving" };

		public static ManualLogSource Log;

		public static ConfigEntry<int> BackupIntervalMinutes;

		public static ConfigEntry<int> MaxBackupsPerSave;

		private float _timeSinceLastBackup;

		private float _commandCheckTimer;

		public static void QueueUIMessage(string msg)
		{
			_uiMessageQueue.Enqueue(msg);
		}

		public static void SetBackupIndicatorActive(bool active)
		{
			Interlocked.Exchange(ref _backupIndicatorActive, active ? 1 : 0);
			_mainThreadActions.Enqueue(delegate
			{
				try
				{
					if (active)
					{
						if (!TryEnsureBackupIndicatorIconLoaded() && _iconLoadCompleted && Volatile.Read(ref _nativeFallbackActive) == 0 && TrySetNativeSavingIndicator(active: true))
						{
							Interlocked.Exchange(ref _nativeFallbackActive, 1);
						}
					}
					else if (Volatile.Read(ref _nativeFallbackActive) == 1 && !IsNativeSaveStillRunning())
					{
						TrySetNativeSavingIndicator(active: false);
						Interlocked.Exchange(ref _nativeFallbackActive, 0);
					}
				}
				catch (Exception ex)
				{
					ManualLogSource log = Log;
					if (log != null)
					{
						log.LogDebug((object)("Failed to toggle native saving indicator: " + ex.Message));
					}
				}
			});
		}

		public static bool TryInvokeOnMainThread(Action action, int timeoutMs = 3000)
		{
			if (action == null || (Object)(object)Instance == (Object)null)
			{
				return false;
			}
			Exception actionException = null;
			ManualResetEventSlim completed = new ManualResetEventSlim(initialState: false);
			try
			{
				_mainThreadActions.Enqueue(delegate
				{
					try
					{
						action();
					}
					catch (Exception ex)
					{
						actionException = ex;
					}
					finally
					{
						completed.Set();
					}
				});
				if (!completed.Wait(timeoutMs))
				{
					return false;
				}
			}
			finally
			{
				if (completed != null)
				{
					((IDisposable)completed).Dispose();
				}
			}
			if (actionException != null)
			{
				ManualLogSource log = Log;
				if (log != null)
				{
					log.LogWarning((object)("Main-thread action failed: " + actionException.Message));
				}
				return false;
			}
			return true;
		}

		private void Awake()
		{
			//IL_0057: Unknown result type (might be due to invalid IL or missing references)
			//IL_0061: Expected O, but got Unknown
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			BackupIntervalMinutes = ((BaseUnityPlugin)this).Config.Bind<int>("General", "BackupIntervalMinutes", 0, "Time in minutes between automatic backups. Set to 0 to disable automatic backups.");
			MaxBackupsPerSave = ((BaseUnityPlugin)this).Config.Bind<int>("General", "MaxBackupsPerSave", 5, "Maximum number of native backups to keep per save.");
			_harmony = new Harmony("com.aloncifer.nativebackup");
			_harmony.PatchAll(Assembly.GetExecutingAssembly());
			StartIconLoadIfNeeded();
			((BaseUnityPlugin)this).Logger.LogInfo((object)"NativeBackup v0.1.1 loaded!");
		}

		private void Update()
		{
			_commandCheckTimer += Time.deltaTime;
			if (_commandCheckTimer > 2f)
			{
				_commandCheckTimer = 0f;
				if (RestoreCommandLogic.IsBackupCommandMissing())
				{
					RestoreCommandLogic.RegisterCommands();
				}
			}
			string result;
			while (_uiMessageQueue.TryDequeue(out result))
			{
				if ((Object)(object)MessageHud.instance != (Object)null)
				{
					MessageHud.instance.ShowMessage((MessageType)1, result, 0, (Sprite)null, false);
				}
			}
			List<Action> list = new List<Action>();
			Action result2;
			while (_mainThreadActions.TryDequeue(out result2))
			{
				list.Add(result2);
			}
			foreach (Action item in list)
			{
				item?.Invoke();
			}
			if (BackupIntervalMinutes.Value <= 0 || (Object)(object)ZNet.instance == (Object)null)
			{
				return;
			}
			_timeSinceLastBackup += Time.deltaTime;
			float num = (float)BackupIntervalMinutes.Value * 60f;
			if (_timeSinceLastBackup >= num)
			{
				_timeSinceLastBackup = 0f;
				string text = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? ZNet.instance.GetWorldName() : null);
				string currentCharacterSaveName = BackupManager.GetCurrentCharacterSaveName();
				switch (BackupCoordinator.TryStartBackup(text, currentCharacterSaveName))
				{
				case BackupCoordinator.BackupStartResult.Started:
					((BaseUnityPlugin)this).Logger.LogDebug((object)("Scheduled backup started for world='" + text + "', character='" + currentCharacterSaveName + "'."));
					break;
				case BackupCoordinator.BackupStartResult.CooldownActive:
					((BaseUnityPlugin)this).Logger.LogDebug((object)"Skipped scheduled backup due to cooldown.");
					break;
				default:
					((BaseUnityPlugin)this).Logger.LogDebug((object)"Skipped scheduled backup because another backup is running.");
					break;
				}
			}
		}

		private void OnDestroy()
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			if ((Object)(object)_backupIndicatorTexture != (Object)null)
			{
				Object.Destroy((Object)(object)_backupIndicatorTexture);
				_backupIndicatorTexture = null;
			}
		}

		private void OnGUI()
		{
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0074: Unknown result type (might be due to invalid IL or missing references)
			//IL_0092: Unknown result type (might be due to invalid IL or missing references)
			if (Volatile.Read(ref _backupIndicatorActive) != 1)
			{
				return;
			}
			if (!TryEnsureBackupIndicatorIconLoaded())
			{
				if (_iconLoadCompleted && Volatile.Read(ref _nativeFallbackActive) == 0 && TrySetNativeSavingIndicator(active: true))
				{
					Interlocked.Exchange(ref _nativeFallbackActive, 1);
				}
			}
			else
			{
				float num = 0.35f + 0.65f * Mathf.Abs(Mathf.Sin(Time.unscaledTime * 5f));
				Color color = GUI.color;
				GUI.color = new Color(1f, 1f, 1f, num);
				GUI.DrawTexture(new Rect(18f, 18f, 40f, 40f), (Texture)(object)_backupIndicatorTexture, (ScaleMode)2, true);
				GUI.color = color;
			}
		}

		private static bool TryEnsureBackupIndicatorIconLoaded()
		{
			if ((Object)(object)_backupIndicatorTexture != (Object)null)
			{
				return true;
			}
			StartIconLoadIfNeeded();
			return false;
		}

		private static void StartIconLoadIfNeeded()
		{
			if (!((Object)(object)Instance == (Object)null) && !((Object)(object)_backupIndicatorTexture != (Object)null) && !_iconLoadCompleted && !_iconLoadInProgress)
			{
				((MonoBehaviour)Instance).StartCoroutine(Instance.LoadBackupIndicatorIconCoroutine());
			}
		}

		private IEnumerator LoadBackupIndicatorIconCoroutine()
		{
			_iconLoadInProgress = true;
			foreach (string path in GetIconCandidatePaths())
			{
				if (string.IsNullOrEmpty(path) || !File.Exists(path))
				{
					continue;
				}
				string text = Path.GetFullPath(path).Replace("\\", "/");
				UnityWebRequest request = UnityWebRequestTexture.GetTexture("file:///" + text, false);
				try
				{
					yield return request.SendWebRequest();
					if ((int)request.result != 1)
					{
						continue;
					}
					Texture2D content = DownloadHandlerTexture.GetContent(request);
					if ((Object)(object)content != (Object)null)
					{
						((Texture)content).wrapMode = (TextureWrapMode)1;
						((Texture)content).filterMode = (FilterMode)1;
						_backupIndicatorTexture = content;
						_iconLoadInProgress = false;
						_iconLoadCompleted = true;
						ManualLogSource log = Log;
						if (log != null)
						{
							log.LogDebug((object)("Loaded backup indicator icon from '" + path + "'."));
						}
						yield break;
					}
				}
				finally
				{
					((IDisposable)request)?.Dispose();
				}
			}
			_iconLoadInProgress = false;
			_iconLoadCompleted = true;
			ManualLogSource log2 = Log;
			if (log2 != null)
			{
				log2.LogDebug((object)"Backup indicator icon load failed; native indicator fallback will be used.");
			}
		}

		private static IEnumerable<string> GetIconCandidatePaths()
		{
			string assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			if (!string.IsNullOrEmpty(assemblyDir))
			{
				yield return Path.Combine(assemblyDir, "icon.png");
				yield return Path.Combine(assemblyDir, "NativeBackup", "icon.png");
			}
			yield return Path.Combine(Paths.PluginPath, "icon.png");
			yield return Path.Combine(Paths.PluginPath, "NativeBackup", "icon.png");
			yield return Path.Combine(Paths.BepInExRootPath, "icon.png");
			yield return Path.Combine(Environment.CurrentDirectory, "icon.png");
		}

		private static bool IsNativeSaveStillRunning()
		{
			if ((Object)(object)ZNet.instance == (Object)null)
			{
				return false;
			}
			return ZNet.instance.SaveStartTime > ZNet.instance.SaveDoneTime;
		}

		private static bool TrySetNativeSavingIndicator(bool active)
		{
			object[] array = new object[2]
			{
				MessageHud.instance,
				Hud.instance
			};
			foreach (object obj in array)
			{
				if (obj == null)
				{
					continue;
				}
				Type type = obj.GetType();
				string[] indicatorMethodNames = _indicatorMethodNames;
				foreach (string text in indicatorMethodNames)
				{
					MethodInfo methodInfo = AccessTools.Method(type, text, new Type[1] { typeof(bool) }, (Type[])null);
					if (methodInfo != null)
					{
						methodInfo.Invoke(obj, new object[1] { active });
						return true;
					}
				}
				indicatorMethodNames = (active ? _indicatorEnableMethodNames : _indicatorDisableMethodNames);
				foreach (string text2 in indicatorMethodNames)
				{
					MethodInfo methodInfo2 = AccessTools.Method(type, text2, Type.EmptyTypes, (Type[])null);
					if (methodInfo2 != null)
					{
						methodInfo2.Invoke(obj, null);
						return true;
					}
				}
				indicatorMethodNames = _indicatorFieldNames;
				foreach (string text3 in indicatorMethodNames)
				{
					FieldInfo fieldInfo = AccessTools.Field(type, text3);
					if (fieldInfo != null && fieldInfo.FieldType == typeof(bool))
					{
						fieldInfo.SetValue(obj, active);
						return true;
					}
				}
			}
			return false;
		}
	}
	public static class RestoreCommandLogic
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static ConsoleEvent <>9__3_0;

			public static ConsoleEvent <>9__3_1;

			public static ConsoleEvent <>9__3_2;

			public static Func<string, DateTime> <>9__7_0;

			public static Func<string, DateTime> <>9__9_1;

			internal void <RegisterCommands>b__3_0(ConsoleEventArgs args)
			{
				HandleRestoreCommand(args.Context, args.Args);
			}

			internal void <RegisterCommands>b__3_1(ConsoleEventArgs args)
			{
				HandleBackupCommand(args.Context, args.Args);
			}

			internal void <RegisterCommands>b__3_2(ConsoleEventArgs args)
			{
				HandleListCommand(args.Context);
			}

			internal DateTime <HandleListCommand>b__7_0(string f)
			{
				return File.GetCreationTime(f);
			}

			internal DateTime <TryRestoreLatestBackup>b__9_1(string b)
			{
				return File.GetCreationTime(b);
			}
		}

		private static FieldInfo _terminalCommandsField;

		public static bool IsBackupCommandMissing()
		{
			return !CommandExists("sb.backup");
		}

		private static bool CommandExists(string cmd)
		{
			try
			{
				if (_terminalCommandsField == null)
				{
					_terminalCommandsField = typeof(Terminal).GetField("commands", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
				}
				if (_terminalCommandsField != null)
				{
					return _terminalCommandsField.GetValue(null) is IDictionary dictionary && dictionary.Contains(cmd);
				}
			}
			catch
			{
			}
			return true;
		}

		public static void RegisterCommands()
		{
			//IL_0040: 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)
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected O, but got Unknown
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Expected O, but got Unknown
			//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Expected O, but got Unknown
			bool flag = false;
			if (!CommandExists("sb.restore"))
			{
				object obj = <>c.<>9__3_0;
				if (obj == null)
				{
					ConsoleEvent val = delegate(ConsoleEventArgs args)
					{
						HandleRestoreCommand(args.Context, args.Args);
					};
					<>c.<>9__3_0 = val;
					obj = (object)val;
				}
				new ConsoleCommand("sb.restore", "Restores a backup.", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
				flag = true;
			}
			if (!CommandExists("sb.backup"))
			{
				object obj2 = <>c.<>9__3_1;
				if (obj2 == null)
				{
					ConsoleEvent val2 = delegate(ConsoleEventArgs args)
					{
						HandleBackupCommand(args.Context, args.Args);
					};
					<>c.<>9__3_1 = val2;
					obj2 = (object)val2;
				}
				new ConsoleCommand("sb.backup", "Triggers a backup.", (ConsoleEvent)obj2, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
				flag = true;
			}
			if (!CommandExists("sb.list"))
			{
				object obj3 = <>c.<>9__3_2;
				if (obj3 == null)
				{
					ConsoleEvent val3 = delegate(ConsoleEventArgs args)
					{
						HandleListCommand(args.Context);
					};
					<>c.<>9__3_2 = val3;
					obj3 = (object)val3;
				}
				new ConsoleCommand("sb.list", "Lists backups.", (ConsoleEvent)obj3, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false);
				flag = true;
			}
			if (flag)
			{
				NativeBackupPlugin.Log.LogInfo((object)"sb. Commands forcefully registered into Terminal dictionary.");
			}
		}

		private static void HandleBackupCommand(Terminal context, string[] args)
		{
			string text = ((args.Length >= 2) ? args[1].ToLower() : "both");
			string currentCharacterSaveName = BackupManager.GetCurrentCharacterSaveName();
			string text2 = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? ZNet.instance.GetWorldName() : null);
			if (text == "char")
			{
				if (string.IsNullOrEmpty(currentCharacterSaveName))
				{
					context.AddString("ERROR: No active character found.");
				}
				else
				{
					StartBackupFromConsole(context, null, currentCharacterSaveName, "Backup started: character '" + currentCharacterSaveName + "'.");
				}
			}
			else if (text == "world")
			{
				if (string.IsNullOrEmpty(text2))
				{
					context.AddString("ERROR: You can only backup a world if you are actively hosting it locally.");
				}
				else
				{
					StartBackupFromConsole(context, text2, null, "Backup started: world '" + text2 + "'.");
				}
			}
			else if (string.IsNullOrEmpty(currentCharacterSaveName) && string.IsNullOrEmpty(text2))
			{
				context.AddString("ERROR: No active world or character found.");
			}
			else
			{
				string text3 = DescribeTarget(text2, currentCharacterSaveName);
				StartBackupFromConsole(context, text2, currentCharacterSaveName, "Backup started: " + text3 + ".");
			}
		}

		private static void StartBackupFromConsole(Terminal context, string worldName, string characterName, string startMessage)
		{
			switch (BackupCoordinator.TryStartBackup(worldName, characterName))
			{
			case BackupCoordinator.BackupStartResult.Started:
				context.AddString(startMessage);
				break;
			case BackupCoordinator.BackupStartResult.CooldownActive:
				context.AddString("Backup on cooldown.");
				break;
			default:
				context.AddString("Backup already running.");
				break;
			}
		}

		private static string DescribeTarget(string worldName, string characterName)
		{
			if (!string.IsNullOrEmpty(worldName) && !string.IsNullOrEmpty(characterName))
			{
				return "world '" + worldName + "' and character '" + characterName + "'";
			}
			if (!string.IsNullOrEmpty(worldName))
			{
				return "world '" + worldName + "'";
			}
			if (!string.IsNullOrEmpty(characterName))
			{
				return "character '" + characterName + "'";
			}
			return "the current save targets";
		}

		private static void HandleListCommand(Terminal context)
		{
			List<string> allAvailableBackups = BackupManager.GetAllAvailableBackups();
			if (allAvailableBackups.Count == 0)
			{
				context.AddString("No backups found.");
				return;
			}
			int num = Math.Min(15, allAvailableBackups.Count);
			context.AddString($"Found {allAvailableBackups.Count} backups. Showing last {num}:");
			foreach (string item in allAvailableBackups.OrderByDescending((string f) => File.GetCreationTime(f)).Take(num).ToList())
			{
				context.AddString("- " + Path.GetFileName(item));
			}
		}

		private static void HandleRestoreCommand(Terminal context, string[] args)
		{
			if (args.Length < 2)
			{
				context.AddString("Usage: sb.restore <SaveName>");
			}
			else
			{
				TryRestoreLatestBackup(args[1], ((Object)(object)context != (Object)null) ? new Action<string>(context.AddString) : null);
			}
		}

		public static bool TryRestoreLatestBackup(string saveName, Action<string> reportMessage)
		{
			if ((Object)(object)ZNet.instance != (Object)null || (Object)(object)Player.m_localPlayer != (Object)null)
			{
				Emit(reportMessage, "ERROR: Restoring while actively loaded into a world is extremely dangerous and can corrupt your game! Please return to the Main Menu to restore.");
				return false;
			}
			List<string> list = (from b in BackupManager.GetAllAvailableBackups()
				where string.Equals(BackupManager.GetTargetNameFromBackupFile(b), saveName, StringComparison.OrdinalIgnoreCase)
				orderby File.GetCreationTime(b) descending
				select b).ToList();
			if (list.Count == 0)
			{
				Emit(reportMessage, "No backups found for target: " + saveName);
				return false;
			}
			string text = list.First();
			Emit(reportMessage, "Found latest backup: " + Path.GetFileName(text));
			try
			{
				string text2 = FindOriginalSaveDirectory(saveName);
				if (string.IsNullOrEmpty(text2))
				{
					Emit(reportMessage, "Could not locate original save file location in Steam/Local. You may need to manually extract this zip from: \n" + text);
					return false;
				}
				Emit(reportMessage, "Original location found: " + text2);
				foreach (string item in (from f in Directory.GetFiles(text2)
					where Path.GetFileNameWithoutExtension(f) == saveName && !f.EndsWith(".old") && !f.EndsWith(".zip")
					select f).ToList())
				{
					string text3 = item + ".old";
					if (File.Exists(text3))
					{
						File.Delete(text3);
					}
					File.Move(item, text3);
					Emit(reportMessage, "Renamed current active save to " + Path.GetFileName(text3));
				}
				using (ZipArchive zipArchive = ZipFile.OpenRead(text))
				{
					foreach (ZipArchiveEntry entry in zipArchive.Entries)
					{
						string destinationFileName = Path.Combine(text2, entry.FullName);
						entry.ExtractToFile(destinationFileName, overwrite: true);
						Emit(reportMessage, "Extracted: " + entry.FullName);
					}
				}
				Emit(reportMessage, "Restore complete! Please restart your game session or reload from Main Menu if necessary.");
				return true;
			}
			catch (Exception ex)
			{
				Emit(reportMessage, "Error during restore: " + ex.Message);
				NativeBackupPlugin.Log.LogError((object)ex);
				return false;
			}
		}

		public static bool TryRestoreLatestBackup(string saveName, Terminal context)
		{
			return TryRestoreLatestBackup(saveName, ((Object)(object)context != (Object)null) ? new Action<string>(context.AddString) : null);
		}

		public static List<BackupManager.BackupTargetInfo> GetAvailableRestoreTargets()
		{
			return BackupManager.GetLatestBackupTargets();
		}

		private static void Emit(Action<string> reportMessage, string message)
		{
			if (reportMessage != null)
			{
				reportMessage(message);
			}
			else
			{
				NativeBackupPlugin.Log.LogInfo((object)message);
			}
		}

		private static string FindOriginalSaveDirectory(string saveName)
		{
			foreach (string valheimSaveDirectory in BackupManager.GetValheimSaveDirectories())
			{
				if (Directory.Exists(valheimSaveDirectory) && Directory.GetFiles(valheimSaveDirectory).Any((string f) => Path.GetFileNameWithoutExtension(f) == saveName && !f.EndsWith(".old") && !f.EndsWith(".zip")))
				{
					return valheimSaveDirectory;
				}
			}
			return null;
		}
	}
	public sealed class BackupButtonStateController : MonoBehaviour
	{
		private Button _button;

		public void Initialize(Button button)
		{
			_button = button;
			RefreshState();
		}

		private void Update()
		{
			RefreshState();
		}

		private void RefreshState()
		{
			if (!((Object)(object)_button == (Object)null))
			{
				((Selectable)_button).interactable = !BackupCoordinator.IsBackupInProgress && !BackupCoordinator.IsCooldownActive;
			}
		}
	}
	[HarmonyPatch(typeof(Menu))]
	public static class MenuPatch
	{
		[Serializable]
		[CompilerGenerated]
		private sealed class <>c
		{
			public static readonly <>c <>9 = new <>c();

			public static UnityAction <>9__0_0;

			internal void <Start_Postfix>b__0_0()
			{
				string text = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? ZNet.instance.GetWorldName() : null);
				string currentCharacterSaveName = BackupManager.GetCurrentCharacterSaveName();
				object msg;
				switch (BackupCoordinator.TryStartBackup(text, currentCharacterSaveName))
				{
				case BackupCoordinator.BackupStartResult.Started:
					NativeBackupPlugin.Log.LogDebug((object)("Manual UI backup triggered for world='" + text + "', character='" + currentCharacterSaveName + "'."));
					return;
				default:
					msg = "Backup already running.";
					break;
				case BackupCoordinator.BackupStartResult.CooldownActive:
					msg = "Backup on cooldown.";
					break;
				}
				NativeBackupPlugin.QueueUIMessage((string)msg);
			}
		}

		[HarmonyPatch("Start")]
		[HarmonyPostfix]
		public static void Start_Postfix(Menu __instance)
		{
			//IL_01ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b8: Expected O, but got Unknown
			//IL_01d3: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.m_menuDialog == (Object)null)
			{
				return;
			}
			Transform val = ((Component)__instance.m_menuDialog).transform.Find("MenuEntries");
			if ((Object)(object)val == (Object)null)
			{
				val = ((Component)__instance.m_menuDialog).transform.Find("menu") ?? ((Component)__instance.m_menuDialog).transform.Find("MENU") ?? ((Component)__instance.m_menuDialog).transform.Find("MenuContainer");
			}
			if (!((Object)(object)val != (Object)null))
			{
				return;
			}
			Transform val2 = FindButton(val, "Settings", "ButtonSettings");
			Transform val3 = FindButton(val, "Save", "ButtonSave");
			if (!((Object)(object)val2 != (Object)null))
			{
				return;
			}
			NativeBackupPlugin.Log.LogInfo((object)"Injecting Backup button under Save.");
			GameObject val4 = Object.Instantiate<GameObject>(((Component)val2).gameObject, val);
			((Object)val4).name = "BackupGame";
			if ((Object)(object)val3 != (Object)null)
			{
				val4.transform.SetSiblingIndex(val3.GetSiblingIndex() + 1);
			}
			else
			{
				val4.transform.SetSiblingIndex(val2.GetSiblingIndex() + 1);
			}
			TMP_Text componentInChildren = val4.GetComponentInChildren<TMP_Text>();
			if ((Object)(object)componentInChildren != (Object)null)
			{
				componentInChildren.text = "Backup";
			}
			Button component = val4.GetComponent<Button>();
			if (!((Object)(object)component != (Object)null))
			{
				return;
			}
			for (int i = 0; i < ((UnityEventBase)component.onClick).GetPersistentEventCount(); i++)
			{
				((UnityEventBase)component.onClick).SetPersistentListenerState(i, (UnityEventCallState)0);
			}
			((UnityEventBase)component.onClick).RemoveAllListeners();
			ButtonClickedEvent onClick = component.onClick;
			object obj = <>c.<>9__0_0;
			if (obj == null)
			{
				UnityAction val5 = delegate
				{
					string text = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? ZNet.instance.GetWorldName() : null);
					string currentCharacterSaveName = BackupManager.GetCurrentCharacterSaveName();
					object msg;
					switch (BackupCoordinator.TryStartBackup(text, currentCharacterSaveName))
					{
					case BackupCoordinator.BackupStartResult.Started:
						NativeBackupPlugin.Log.LogDebug((object)("Manual UI backup triggered for world='" + text + "', character='" + currentCharacterSaveName + "'."));
						return;
					default:
						msg = "Backup already running.";
						break;
					case BackupCoordinator.BackupStartResult.CooldownActive:
						msg = "Backup on cooldown.";
						break;
					}
					NativeBackupPlugin.QueueUIMessage((string)msg);
				};
				<>c.<>9__0_0 = val5;
				obj = (object)val5;
			}
			((UnityEvent)onClick).AddListener((UnityAction)obj);
			Button component2 = ((Component)val2).GetComponent<Button>();
			if ((Object)(object)component2 != (Object)null)
			{
				((Selectable)component).navigation = ((Selectable)component2).navigation;
			}
			val4.AddComponent<BackupButtonStateController>().Initialize(component);
		}

		private static Transform FindButton(Transform root, params string[] labels)
		{
			Button[] componentsInChildren = ((Component)root).GetComponentsInChildren<Button>(true);
			foreach (Button button in componentsInChildren)
			{
				if ((Object)(object)button == (Object)null)
				{
					continue;
				}
				if (labels.Any((string label) => string.Equals(((Object)button).name, label, StringComparison.OrdinalIgnoreCase)))
				{
					return ((Component)button).transform;
				}
				TMP_Text componentInChildren = ((Component)button).GetComponentInChildren<TMP_Text>(true);
				if ((Object)(object)componentInChildren != (Object)null)
				{
					string value = ((componentInChildren.text != null) ? componentInChildren.text.Trim() : string.Empty);
					if (labels.Any((string label) => string.Equals(value, label, StringComparison.OrdinalIgnoreCase)))
					{
						return ((Component)button).transform;
					}
				}
			}
			return root.Find(labels.FirstOrDefault());
		}
	}
}