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());
}
}
}