Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of NativeBackup v0.1.1
NativeBackup.dll
Decompiled a month agousing 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()); } } }