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 list = new List(); 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 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 CollectSaveWriteProbes(string targetWorld, string targetCharacter) { List list = new List(); 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(); 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