Decompiled source of Cloudward v0.1.1

Cloudward/Cloudward.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Cloudward.Core;
using Cloudward.Lock;
using ForgeKit;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("Cloudward")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+d3fbef8726f47d3953b79ef11c8e75d2f5805c9a")]
[assembly: AssemblyProduct("Cloudward")]
[assembly: AssemblyTitle("Cloudward")]
[assembly: AssemblyMetadata("BuildStamp", "d3fbef8 2026-07-24")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
namespace Cloudward
{
	[BepInPlugin("cobalt.cloudward", "Cloudward", "0.1.1")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BaseUnityPlugin
	{
		public const string GUID = "cobalt.cloudward";

		public const string NAME = "Cloudward";

		public const string VERSION = "0.1.1";

		internal static ManualLogSource Log;

		internal static SyncCoordinator Coordinator;

		internal static int OwnPid;

		public static ConfigEntry<bool> Enable;

		public static ConfigEntry<string> MountPath;

		public static ConfigEntry<string> MarkerFileName;

		public static ConfigEntry<bool> AutoCreateMarker;

		public static ConfigEntry<MountDownPolicy> OnMountDown;

		public static ConfigEntry<LockHeldPolicy> OnLockHeld;

		public static ConfigEntry<ForkResolution> ForkPolicy;

		public static ConfigEntry<int> HeartbeatSeconds;

		public static ConfigEntry<int> StaleSeconds;

		public static ConfigEntry<string> DeviceName;

		private CommandRegistry _commands;

		private VerbHost _verbs;

		private CommandChannel _channel;

		private float _lastHeartbeat;

		internal void Awake()
		{
			//IL_01a3: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ad: Expected O, but got Unknown
			//IL_01b2: Unknown result type (might be due to invalid IL or missing references)
			Log = ((BaseUnityPlugin)this).Logger;
			try
			{
				OwnPid = Process.GetCurrentProcess().Id;
			}
			catch
			{
				OwnPid = 0;
			}
			Enable = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "Enable", false, "Master switch. When false, saves stay in the local game folder (vanilla behavior).");
			MountPath = ((BaseUnityPlugin)this).Config.Bind<string>("Sync", "MountPath", "", "Root of the mounted/shared directory to sync saves through (e.g. /mnt/nas/outward or Z:\\outward). Empty = inert. The game's SaveGames/<SteamID> tail is preserved under it, so the same Steam account on every device lands in one shared folder.");
			MarkerFileName = ((BaseUnityPlugin)this).Config.Bind<string>("Sync", "MarkerFileName", ".outward-sync-root", "Sentinel file that must exist in MountPath for the share to count as mounted. An unmounted mountpoint is an empty dir with no marker, so this stops writes landing on local disk. With AutoCreateMarker=true (default) Cloudward creates it for you; otherwise stamp it once with the 'syncmarker' verb (or touch it by hand).");
			AutoCreateMarker = ((BaseUnityPlugin)this).Config.Bind<bool>("Sync", "AutoCreateMarker", true, "Create the mount marker automatically so config-only setup works (just set MountPath — no 'syncmarker' step). On the FIRST run for a mount path the marker is created and remembered; on a later launch when the mount is down (marker absent because the share isn't mounted) Cloudward reads that as 'disconnected → play offline' and does NOT re-stamp the placeholder. A share that already holds real data (a SaveGames/ folder or a ledger) always gets its marker restored. Set false for the strict old behavior: the marker must already exist ('syncmarker' required).");
			OnMountDown = ((BaseUnityPlugin)this).Config.Bind<MountDownPolicy>("Sync", "OnMountDown", MountDownPolicy.FallbackLocal, "What to do if the mount isn't live at launch. FallbackLocal = use local saves + warn; RefuseAndLog = same, but log an error (louder for headless sessions).");
			OnLockHeld = ((BaseUnityPlugin)this).Config.Bind<LockHeldPolicy>("Sync", "OnLockHeld", (LockHeldPolicy)0, "What to do about PUSHING when another device holds a LIVE lock (pulls always happen). FallbackLocal = defer pushes, accumulate locally, sync when it frees (safe default); ForceTake = push anyway (only if you know the other device is dead). ReadOnly behaves like FallbackLocal here (there is no redirect in the always-local model).");
			ForkPolicy = ((BaseUnityPlugin)this).Config.Bind<ForkResolution>("Sync", "ForkResolution", (ForkResolution)0, "When the SAME character diverged on two devices (both played it offline from a common point), how to resolve it automatically. The version that isn't kept live is always backed up to .cloudward-forks/ (never silently discarded). NewestWins = keep whichever device has the most recent IN-GAME save (default — no prompt, just works); ThisDevice = always keep this device's version; OtherDevice = always take the other device's version; KeepBoth = keep this device's live and preserve the other (v1: a restorable backup; a true second save slot is planned for v2); AskMe = leave it untouched and wait for a manual 'syncresolve local|share|both' decision.");
			HeartbeatSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Lock", "HeartbeatSeconds", 30, "How often the held lock's heartbeat is refreshed (and the mount re-checked).");
			StaleSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Lock", "StaleSeconds", 180, "A lock whose heartbeat is older than this is considered dead (crash recovery) and is reclaimable by any device. Should be several heartbeats.");
			DeviceName = ((BaseUnityPlugin)this).Config.Bind<string>("Lock", "DeviceName", SafeMachineName(), "This device's name in the lock file. Must be UNIQUE per device — own-device locks are reclaimed instantly, so two devices sharing a name would never block each other.");
			Coordinator = new SyncCoordinator(Log);
			RegisterVerbs();
			_channel = new CommandChannel("Cloudward_cmd.txt", Log, _commands, 0.5f, true, false);
			new Harmony("cobalt.cloudward").PatchAll();
			Log.LogMessage((object)string.Format("{0} {1} loaded (Enable={2}, device={3}).", "Cloudward", "0.1.1", Enable.Value, DeviceName.Value));
		}

		internal void Update()
		{
			_channel.Tick();
			Coordinator?.PumpMainThread();
			int num = Mathf.Max(1, HeartbeatSeconds.Value);
			if (Time.unscaledTime - _lastHeartbeat >= (float)num)
			{
				_lastHeartbeat = Time.unscaledTime;
				Coordinator?.Heartbeat();
			}
		}

		private static string SafeMachineName()
		{
			try
			{
				return Environment.MachineName;
			}
			catch
			{
				return "device";
			}
		}

		private void RegisterVerbs()
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Expected O, but got Unknown
			_commands = new CommandRegistry(Log);
			_verbs = new VerbHost(_commands, Log, (Func<Character>)(() => (Character)null));
			_verbs.Register("selftest", "Run the Cloudward self-test ([SELFTEST] PASS/FAIL ... DONE).", (Action<VerbContext>)delegate
			{
				SelfTest();
			}, "[Cloudward]", false, true, false, (string)null);
			_verbs.Register("syncstatus", "Dump sync state: mode, local/share paths, lock holder, pending forks.", (Action<VerbContext>)delegate
			{
				Status();
			}, "[Cloudward]", false, true, false, (string)null);
			_verbs.Register("syncnow", "Force a reconcile + push now (push-only; pulls apply at next launch).", (Action<VerbContext>)delegate
			{
				Coordinator?.SyncNow();
			}, "[Cloudward]", false, true, false, (string)null);
			_verbs.Register("syncresolve", "Resolve a paused fork: 'syncresolve <local|share|both> [uid]'.", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				Coordinator?.ResolveFork(ctx.Arg(1), ctx.Arg(2));
			}, "[Cloudward]", false, true, false, (string)null);
			_verbs.Register("synclock", "Show the current lock ('synclock'); push+release ours ('synclock release').", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				if (ctx.Arg(1) == "release")
				{
					Coordinator?.PushAndReleaseAtQuit();
					Log.LogMessage((object)"[Cloudward] lock released on request.");
				}
				else
				{
					Log.LogMessage((object)("[Cloudward] lock holder: " + (Coordinator?.CurrentHolder() ?? "(none/unreadable)") + "."));
				}
			}, "[Cloudward]", false, true, false, (string)null);
			_verbs.Register("syncmarker", "Manually stamp the mount-liveness marker into MountPath (not required with AutoCreateMarker=true).", (Action<VerbContext>)delegate
			{
				StampMarker();
			}, "[Cloudward]", false, true, false, (string)null);
		}

		private void Status()
		{
			SyncCoordinator coordinator = Coordinator;
			Log.LogMessage((object)$"[Cloudward] mode={coordinator?.Mode} holdingLock={coordinator?.HoldingLock}");
			Log.LogMessage((object)("[Cloudward]   local (game writes here) = " + (coordinator?.LocalRoot ?? SaveManager.GetSavePath())));
			Log.LogMessage((object)("[Cloudward]   share replica            = " + (coordinator?.TargetPath ?? "(none)")));
			string text = (MountPath.Value ?? "").Trim();
			if (text.Length > 0)
			{
				Log.LogMessage((object)string.Format("[Cloudward]   mount '{0}' live={1} holder={2}", text, coordinator?.LastMountLive, coordinator?.CurrentHolder() ?? "(none)"));
			}
			IReadOnlyList<string> readOnlyList = coordinator?.PendingForks();
			if (readOnlyList != null && readOnlyList.Count > 0)
			{
				Log.LogMessage((object)("[Cloudward]   PENDING FORKS: " + string.Join(", ", readOnlyList) + " — run 'syncresolve'."));
			}
		}

		private void StampMarker()
		{
			string text = (MountPath.Value ?? "").Trim();
			if (text.Length == 0)
			{
				Log.LogWarning((object)"[Cloudward] set MountPath first.");
				return;
			}
			try
			{
				Directory.CreateDirectory(text);
				string text2 = Path.Combine(text, MarkerFileName.Value);
				File.WriteAllText(text2, "Cloudward mount marker — do not delete.\n");
				Log.LogMessage((object)("[Cloudward] wrote marker " + text2 + ". This share is now recognized as mounted."));
			}
			catch (Exception ex)
			{
				Log.LogError((object)("[Cloudward] could not write marker into '" + text + "' (" + ex.GetType().Name + ": " + ex.Message + ")."));
			}
		}

		private void SelfTest()
		{
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Expected O, but got Unknown
			//IL_00c7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00eb: Expected O, but got Unknown
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ec: Invalid comparison between Unknown and I4
			SelfTestHarness val = new SelfTestHarness(Log);
			val.Begin("Cloudward");
			val.Check("logger wired", Log != null);
			val.Check("coordinator built", Coordinator != null);
			val.Check("config bound", Enable != null && MountPath != null && OnLockHeld != null);
			val.Check("no cross-mod keybind conflicts", !Keybinds.HasConflicts());
			val.Check("mount guard: no marker => down", !MountGuard.IsLive(false, true));
			val.Check("path rebase keeps account tail", PathRebase.Target("/x/SaveGames/ACCT", "/mnt/nas") == "/mnt/nas/SaveGames/ACCT");
			val.Check("own-device lock reclaims", (int)LockLease.Decide(DateTime.UtcNow, new LockStamp(DeviceName.Value, 1, DateTime.UtcNow), DeviceName.Value, OwnPid, StaleSeconds.Value, (LockHeldPolicy)0) == 0);
			val.Done();
		}
	}
	public enum MountDownPolicy
	{
		FallbackLocal,
		RefuseAndLog
	}
	internal enum SyncMode
	{
		Inactive,
		Offline,
		Online
	}
	internal sealed class SyncCoordinator
	{
		private readonly ManualLogSource _log;

		private bool _bootDone;

		private volatile SyncMode _mode;

		private volatile bool _holdingLock;

		private volatile bool _needsRefresh;

		private volatile bool _lastMountLive;

		private int _busy;

		private SessionLock _lock;

		private LocalSyncState _state;

		public SyncMode Mode => _mode;

		public bool HoldingLock => _holdingLock;

		public bool LastMountLive => _lastMountLive;

		public string LocalRoot { get; private set; }

		public string TargetPath { get; private set; }

		private string LedgerPath => Path.Combine(TargetPath, ".cloudward-ledger");

		private string ForksBackupRoot => Path.Combine(TargetPath, ".cloudward-forks");

		private static string StatePath => Path.Combine(Paths.ConfigPath, "cloudward_state.txt");

		public SyncCoordinator(ManualLogSource log)
		{
			_log = log;
		}

		private static string CharDir(string root, string uid)
		{
			return Path.Combine(root, "Save_" + uid);
		}

		private bool RunInBackground(string tag, Action work, bool quietIfBusy = false)
		{
			if (Interlocked.CompareExchange(ref _busy, 1, 0) != 0)
			{
				if (!quietIfBusy)
				{
					_log.LogMessage((object)("[CLOUDWARD] a sync is already running — skipping " + tag + "."));
				}
				return false;
			}
			Task.Run(delegate
			{
				try
				{
					work();
				}
				catch (Exception arg)
				{
					_log.LogError((object)$"[CLOUDWARD] {tag} failed: {arg}");
				}
				finally
				{
					Interlocked.Exchange(ref _busy, 0);
				}
			});
			return true;
		}

		public void PumpMainThread()
		{
			if (!_needsRefresh)
			{
				return;
			}
			_needsRefresh = false;
			try
			{
				SaveManager.Instance.Reset();
				SaveManager.Instance.RetrieveCharacterSaves();
				_log.LogMessage((object)"[CLOUDWARD] character list refreshed after sync.");
			}
			catch (Exception arg)
			{
				_log.LogError((object)$"[CLOUDWARD] character-list refresh failed: {arg}");
			}
		}

		public void TryReconcileAtBoot()
		{
			if (_bootDone)
			{
				return;
			}
			_bootDone = true;
			if (!Plugin.Enable.Value)
			{
				_log.LogMessage((object)"[CLOUDWARD] disabled; local saves only.");
				_mode = SyncMode.Inactive;
				return;
			}
			string mountRoot = (Plugin.MountPath.Value ?? "").Trim();
			if (mountRoot.Length == 0)
			{
				_log.LogMessage((object)"[CLOUDWARD] set [Sync] MountPath to enable; local saves only.");
				_mode = SyncMode.Inactive;
				return;
			}
			LocalRoot = SaveManager.GetSavePath();
			_state = LoadState();
			_mode = SyncMode.Offline;
			RunInBackground("boot reconcile", delegate
			{
				if (!EnsureMountLive(mountRoot))
				{
					string text = "[CLOUDWARD] mount not live (marker '" + Plugin.MarkerFileName.Value + "' missing at '" + mountRoot + "'); playing OFFLINE — will sync on reconnect.";
					if (Plugin.OnMountDown.Value == MountDownPolicy.RefuseAndLog)
					{
						_log.LogError((object)text);
					}
					else
					{
						_log.LogWarning((object)text);
					}
					_mode = SyncMode.Offline;
				}
				else if (!SetupTarget(mountRoot))
				{
					_mode = SyncMode.Offline;
				}
				else
				{
					AcquireLockForPush();
					DoReconcile(pullAllowed: true);
					_mode = SyncMode.Online;
				}
			});
		}

		private bool SetupTarget(string mountRoot)
		{
			TargetPath = PathRebase.Target(LocalRoot, mountRoot);
			try
			{
				Directory.CreateDirectory(TargetPath);
				return true;
			}
			catch (Exception ex)
			{
				_log.LogError((object)("[CLOUDWARD] cannot open target '" + TargetPath + "' (" + ex.Message + "); OFFLINE."));
				return false;
			}
		}

		private void AcquireLockForPush()
		{
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_004b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Invalid comparison between Unknown and I4
			_lock = new SessionLock(TargetPath, Plugin.DeviceName.Value, Plugin.OwnPid, _log);
			LeaseDecision val = _lock.Decide(DateTime.UtcNow, Plugin.StaleSeconds.Value, Plugin.OnLockHeld.Value);
			if ((int)val == 0 || (int)Plugin.OnLockHeld.Value == 2)
			{
				_lock.Write(DateTime.UtcNow);
				_holdingLock = true;
				return;
			}
			_holdingLock = false;
			ManualLogSource log = _log;
			LockStamp obj = _lock.Read();
			log.LogWarning((object)("[CLOUDWARD] share in use on '" + (((obj != null) ? obj.Device : null) ?? "another device") + "'; will PULL but defer pushes until it frees (like offline)."));
		}

		private void DoReconcile(bool pullAllowed)
		{
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c4: Invalid comparison between Unknown and I4
			//IL_0195: Unknown result type (might be due to invalid IL or missing references)
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_019c: Unknown result type (might be due to invalid IL or missing references)
			//IL_01af: Expected I4, but got Unknown
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0116: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Expected O, but got Unknown
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			//IL_0241: Expected O, but got Unknown
			if (SaveManager.Instance.SaveInProgress)
			{
				return;
			}
			SyncLedger val = LoadLedger();
			Dictionary<string, CharTree> dictionary = TreeScanner.Scan(LocalRoot);
			Dictionary<string, CharTree> dictionary2 = TreeScanner.Scan(TargetPath);
			bool flag = false;
			bool flag2 = false;
			ForkResolution value = Plugin.ForkPolicy.Value;
			bool flag3 = ForkResolve.IsAuto(value);
			foreach (string item in dictionary.Keys.Union(dictionary2.Keys).ToList())
			{
				bool flag4 = _state.GetFork(item) != null;
				if (flag4 && !flag3)
				{
					continue;
				}
				ReconcilePlan val2 = Reconciler.Plan(item, Tree(dictionary, item), Tree(dictionary2, item), val.Get(item), _state.Get(item));
				if ((int)val2.Action == 3)
				{
					if (flag3)
					{
						(bool, bool) tuple = AutoResolve(item, val2, dictionary, dictionary2, val, value);
						flag |= tuple.Item1;
						flag2 |= tuple.Item2;
					}
					else if (!flag4)
					{
						_state.SetFork(item, new ForkRecord(val2.LocalHead, val2.ShareHead));
						_log.LogError((object)("[CLOUDWARD] " + item + ": FORK — sync paused (local head " + val2.LocalHead + " vs share " + val2.ShareHead + "). Run 'syncresolve local|share|both " + item + "'."));
					}
					continue;
				}
				if (flag4)
				{
					_state.ClearFork(item);
				}
				SyncAction action = val2.Action;
				switch ((int)action)
				{
				case 1:
					if (pullAllowed && ExecPull(item, val2))
					{
						flag = true;
					}
					break;
				case 2:
					if (_holdingLock)
					{
						flag2 |= ExecPush(item, val2, val);
					}
					else
					{
						_log.LogMessage((object)("[CLOUDWARD] " + item + ": local ahead but lock held elsewhere — deferring push."));
					}
					break;
				case 0:
				{
					LedgerEntry val3 = val.Get(item);
					if (val3 != null && val2.NewHead.Length > 0)
					{
						_state.Set(item, new LocalCharState(val3.Gen, val2.NewHead));
					}
					break;
				}
				}
			}
			SaveState();
			if (flag2)
			{
				SaveLedger(val);
			}
			if (flag)
			{
				_needsRefresh = true;
			}
		}

		private (bool pulled, bool ledgerDirty) AutoResolve(string uid, ReconcilePlan plan, Dictionary<string, CharTree> local, Dictionary<string, CharTree> share, SyncLedger ledger, ForkResolution policy)
		{
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_015d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Invalid comparison between Unknown and I4
			//IL_010a: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Expected O, but got Unknown
			CharTree val = Tree(local, uid);
			CharTree val2 = Tree(share, uid);
			if (ForkResolve.LocalWins(policy, Reconciler.LocalIsNewer(plan.LocalHead, plan.ShareHead)) ?? true)
			{
				if (!_holdingLock)
				{
					if (_state.GetFork(uid) == null)
					{
						_state.SetFork(uid, new ForkRecord(plan.LocalHead, plan.ShareHead));
					}
					_log.LogMessage((object)$"[CLOUDWARD] {uid}: fork ({policy}, keeping local) — deferring resolve until the lock is held.");
					return (pulled: false, ledgerDirty: false);
				}
				string text = BackupLineage(uid, TargetPath, val2, "share");
				PushWins(uid, val, share, ledger);
				_state.ClearFork(uid);
				if ((int)policy == 3)
				{
					_log.LogMessage((object)("[CLOUDWARD] " + uid + ": fork resolved (KeepBoth) — local kept live; the other version is preserved at " + text + " (v1: a restorable backup, not yet a separate save slot)."));
				}
				else
				{
					_log.LogMessage((object)$"[CLOUDWARD] {uid}: fork auto-resolved ({policy}) — kept LOCAL; other version backed up at {text}.");
				}
				return (pulled: false, ledgerDirty: true);
			}
			string arg = BackupLineage(uid, LocalRoot, val, "local");
			PullWins(uid, val2, local, ledger);
			_state.ClearFork(uid);
			_log.LogMessage((object)$"[CLOUDWARD] {uid}: fork auto-resolved ({policy}) — kept SHARE; other version backed up at {arg}.");
			return (pulled: true, ledgerDirty: false);
		}

		public void Heartbeat()
		{
			if (_mode == SyncMode.Inactive || TargetPath == null)
			{
				return;
			}
			RunInBackground("heartbeat", delegate
			{
				string text = (Plugin.MountPath.Value ?? "").Trim();
				bool flag = text.Length > 0 && EnsureMountLive(text);
				if (_mode == SyncMode.Online && !flag)
				{
					_log.LogWarning((object)"[CLOUDWARD] mount went away — playing OFFLINE, will sync on reconnect.");
					_mode = SyncMode.Offline;
					_holdingLock = false;
				}
				else if (_mode == SyncMode.Offline && flag)
				{
					_log.LogMessage((object)"[CLOUDWARD] mount is back — pushing deferred local changes (pulls apply next launch).");
					AcquireLockForPush();
					_mode = SyncMode.Online;
					DoReconcile(pullAllowed: false);
				}
				else if (_mode == SyncMode.Online)
				{
					if (!_holdingLock)
					{
						AcquireLockForPush();
					}
					if (_holdingLock)
					{
						_lock?.Write(DateTime.UtcNow);
						DoReconcile(pullAllowed: false);
					}
				}
			}, quietIfBusy: true);
		}

		public void PushAndReleaseAtQuit()
		{
			try
			{
				if (_mode == SyncMode.Online && _holdingLock)
				{
					DoReconcile(pullAllowed: false);
				}
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] quit push failed (" + ex.Message + ")."));
			}
			if (_holdingLock && _lock != null)
			{
				_lock.ReleaseIfOwn();
				_log.LogMessage((object)"[CLOUDWARD] released the lock.");
			}
			_holdingLock = false;
		}

		public void ResolveFork(string choice, string uid)
		{
			if (_state == null)
			{
				_log.LogWarning((object)"[CLOUDWARD] sync not active.");
				return;
			}
			if (uid == null)
			{
				List<string> list = _state.PendingForks.ToList();
				if (list.Count != 1)
				{
					_log.LogWarning((object)("[CLOUDWARD] specify a uid — pending forks: " + string.Join(", ", list)));
					return;
				}
				uid = list[0];
			}
			if (_state.GetFork(uid) == null)
			{
				_log.LogWarning((object)("[CLOUDWARD] no pending fork for " + uid + "."));
				return;
			}
			if (choice != "local" && choice != "share" && choice != "both")
			{
				_log.LogWarning((object)"[CLOUDWARD] syncresolve <local|share|both> [uid].");
				return;
			}
			string target = uid;
			RunInBackground("resolve " + choice + " " + target, delegate
			{
				if (TargetPath == null || !EnsureMountLive((Plugin.MountPath.Value ?? "").Trim()))
				{
					_log.LogWarning((object)"[CLOUDWARD] mount not live; can't resolve now.");
				}
				else
				{
					SyncLedger ledger = LoadLedger();
					Dictionary<string, CharTree> dictionary = TreeScanner.Scan(LocalRoot);
					Dictionary<string, CharTree> dictionary2 = TreeScanner.Scan(TargetPath);
					CharTree val = Tree(dictionary, target);
					CharTree val2 = Tree(dictionary2, target);
					bool flag = false;
					switch (choice)
					{
					case "local":
						BackupLineage(target, TargetPath, val2, "share");
						PushWins(target, val, dictionary2, ledger);
						break;
					case "share":
						BackupLineage(target, LocalRoot, val, "local");
						PullWins(target, val2, dictionary, ledger);
						flag = true;
						break;
					case "both":
					{
						string text = BackupLineage(target, TargetPath, val2, "share");
						PushWins(target, val, dictionary2, ledger);
						_log.LogMessage((object)("[CLOUDWARD] 'both': local kept live; the other version is preserved at " + text + " (restore by copying its snapshot folders into a Save_<uid> dir)."));
						break;
					}
					}
					_state.ClearFork(target);
					SaveState();
					SaveLedger(ledger);
					if (flag)
					{
						_needsRefresh = true;
					}
					_log.LogMessage((object)("[CLOUDWARD] fork on " + target + " resolved: " + choice + "."));
				}
			});
		}

		public void SyncNow()
		{
			if (_mode == SyncMode.Inactive)
			{
				_log.LogMessage((object)"[CLOUDWARD] inactive.");
				return;
			}
			RunInBackground("syncnow", delegate
			{
				string mountRoot = (Plugin.MountPath.Value ?? "").Trim();
				if (!EnsureMountLive(mountRoot))
				{
					_log.LogWarning((object)"[CLOUDWARD] mount not live.");
				}
				else
				{
					if (_mode == SyncMode.Offline)
					{
						AcquireLockForPush();
						_mode = SyncMode.Online;
					}
					else if (!_holdingLock)
					{
						AcquireLockForPush();
					}
					DoReconcile(pullAllowed: false);
					_log.LogMessage((object)"[CLOUDWARD] syncnow done (push-only; pulls apply at next launch).");
				}
			});
		}

		private void PushWins(string uid, CharTree local, Dictionary<string, CharTree> share, SyncLedger ledger)
		{
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Expected O, but got Unknown
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Expected O, but got Unknown
			LedgerEntry obj = ledger.Get(uid);
			int num = ((obj != null) ? obj.Gen : 0) + 1;
			SafeCopy(CharDir(LocalRoot, uid), CharDir(TargetPath, uid), MissingList(local, Tree(share, uid)));
			SnapshotCopier.PruneTo(CharDir(TargetPath, uid), 15);
			ledger.Set(uid, new LedgerEntry(num, local.Head, Plugin.DeviceName.Value));
			_state.Set(uid, new LocalCharState(num, local.Head));
		}

		private void PullWins(string uid, CharTree share, Dictionary<string, CharTree> local, SyncLedger ledger)
		{
			//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Expected O, but got Unknown
			LedgerEntry obj = ledger.Get(uid);
			int num = ((obj != null) ? obj.Gen : 0);
			CharTree val = Tree(local, uid);
			SafeCopy(CharDir(TargetPath, uid), CharDir(LocalRoot, uid), MissingList(share, val));
			if (val != null)
			{
				HashSet<string> shareSet = new HashSet<string>(share.Snapshots, StringComparer.Ordinal);
				List<string> list = val.Snapshots.Where((string s) => !shareSet.Contains(s)).ToList();
				SnapshotCopier.RemoveSnapshots(CharDir(LocalRoot, uid), (IEnumerable<string>)list);
			}
			SnapshotCopier.PruneTo(CharDir(LocalRoot, uid), 15);
			_state.Set(uid, new LocalCharState(num, share.Head));
		}

		private string BackupLineage(string uid, string srcRoot, CharTree tree, string side)
		{
			if (tree.IsEmpty)
			{
				return "(nothing)";
			}
			string text = Path.Combine(ForksBackupRoot, uid + "-" + side + "-" + tree.Head);
			try
			{
				SafeCopy(CharDir(srcRoot, uid), text, tree.Snapshots);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] backup of " + side + " lineage failed (" + ex.Message + ")."));
			}
			return text;
		}

		private bool ExecPull(string uid, ReconcilePlan plan)
		{
			//IL_004a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Expected O, but got Unknown
			try
			{
				SafeCopy(CharDir(TargetPath, uid), CharDir(LocalRoot, uid), plan.CopyTimestamps);
				SnapshotCopier.PruneTo(CharDir(LocalRoot, uid), 15);
				_state.Set(uid, new LocalCharState(plan.NewGen, plan.NewHead));
				_log.LogMessage((object)$"[CLOUDWARD] {uid}: pulled {plan.CopyTimestamps.Count} snapshot(s) <- share.");
				return true;
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] " + uid + ": pull failed (" + ex.Message + ")."));
				return false;
			}
		}

		private bool ExecPush(string uid, ReconcilePlan plan, SyncLedger ledger)
		{
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0059: Expected O, but got Unknown
			//IL_006c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0076: Expected O, but got Unknown
			try
			{
				SafeCopy(CharDir(LocalRoot, uid), CharDir(TargetPath, uid), plan.CopyTimestamps);
				SnapshotCopier.PruneTo(CharDir(TargetPath, uid), 15);
				ledger.Set(uid, new LedgerEntry(plan.NewGen, plan.NewHead, Plugin.DeviceName.Value));
				_state.Set(uid, new LocalCharState(plan.NewGen, plan.NewHead));
				_log.LogMessage((object)$"[CLOUDWARD] {uid}: pushed {plan.CopyTimestamps.Count} snapshot(s) -> share (gen {plan.NewGen}).");
				return true;
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] " + uid + ": push failed (" + ex.Message + ")."));
				return false;
			}
		}

		private static void SafeCopy(string srcCharDir, string dstCharDir, IEnumerable<string> timestamps)
		{
			SnapshotCopier.CopySnapshots(srcCharDir, dstCharDir, timestamps);
		}

		private static IReadOnlyList<string> MissingList(CharTree from, CharTree to)
		{
			HashSet<string> have = new HashSet<string>(to.Snapshots, StringComparer.Ordinal);
			return from.Snapshots.Where((string s) => !have.Contains(s)).ToList();
		}

		private static CharTree Tree(Dictionary<string, CharTree> map, string uid)
		{
			if (!map.TryGetValue(uid, out var value))
			{
				return null;
			}
			return value;
		}

		private LocalSyncState LoadState()
		{
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Expected O, but got Unknown
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				return (LocalSyncState)(File.Exists(StatePath) ? ((object)LocalSyncState.Parse(File.ReadAllText(StatePath))) : ((object)new LocalSyncState()));
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] state read failed (" + ex.Message + ")."));
				return new LocalSyncState();
			}
		}

		private void SaveState()
		{
			try
			{
				File.WriteAllText(StatePath, _state.Serialize());
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] state write failed (" + ex.Message + ")."));
			}
		}

		private SyncLedger LoadLedger()
		{
			//IL_0048: Unknown result type (might be due to invalid IL or missing references)
			//IL_004e: Expected O, but got Unknown
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				return (SyncLedger)(File.Exists(LedgerPath) ? ((object)SyncLedger.Parse(File.ReadAllText(LedgerPath))) : ((object)new SyncLedger()));
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] ledger read failed (" + ex.Message + ")."));
				return new SyncLedger();
			}
		}

		private void SaveLedger(SyncLedger ledger)
		{
			try
			{
				Directory.CreateDirectory(TargetPath);
				string text = LedgerPath + ".tmp";
				File.WriteAllText(text, ledger.Serialize());
				if (File.Exists(LedgerPath))
				{
					File.Delete(LedgerPath);
				}
				File.Move(text, LedgerPath);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] ledger write failed (" + ex.Message + ")."));
			}
		}

		public bool EnsureMountLive(string mountRoot)
		{
			//IL_0064: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Unknown result type (might be due to invalid IL or missing references)
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			//IL_006f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Invalid comparison between Unknown and I4
			if (string.IsNullOrEmpty(mountRoot))
			{
				_lastMountLive = false;
				return false;
			}
			bool flag;
			try
			{
				flag = File.Exists(Path.Combine(mountRoot, Plugin.MarkerFileName.Value));
			}
			catch
			{
				flag = false;
			}
			LocalSyncState state = _state;
			bool flag2 = state != null && state.IsBootstrapped(mountRoot);
			bool flag3 = ShareHasData(mountRoot);
			bool flag4 = !Plugin.AutoCreateMarker.Value;
			MountDecision val = MountGuard.Decide(flag, flag2, flag3, DriveReady(mountRoot), flag4);
			if ((int)val != 0)
			{
				if ((int)val == 2)
				{
					bool flag5 = CreateMarker(mountRoot, flag3);
					if (flag5 && _state != null && !_state.IsBootstrapped(mountRoot))
					{
						_state.MarkBootstrapped(mountRoot);
						SaveState();
					}
					_lastMountLive = flag5;
					return flag5;
				}
				_lastMountLive = false;
				return false;
			}
			_lastMountLive = true;
			return true;
		}

		private static bool ShareHasData(string mountRoot)
		{
			try
			{
				return Directory.Exists(Path.Combine(mountRoot, "SaveGames")) || File.Exists(Path.Combine(mountRoot, ".cloudward-ledger"));
			}
			catch
			{
				return false;
			}
		}

		private bool CreateMarker(string mountRoot, bool recovery)
		{
			try
			{
				Directory.CreateDirectory(mountRoot);
				string text = Path.Combine(mountRoot, Plugin.MarkerFileName.Value);
				File.WriteAllText(text, "Cloudward mount marker — do not delete.\n");
				_log.LogMessage((object)("[CLOUDWARD] auto-created mount marker at " + text + (recovery ? " (share has data — restored a missing marker)." : " (first run for this mount path).")));
				return true;
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] could not auto-create marker in '" + mountRoot + "' (" + ex.Message + "); OFFLINE."));
				return false;
			}
		}

		public IReadOnlyList<string> PendingForks()
		{
			LocalSyncState state = _state;
			return ((state != null) ? state.PendingForks.ToList() : null) ?? new List<string>();
		}

		public string CurrentHolder()
		{
			SessionLock sessionLock = _lock;
			if (sessionLock == null)
			{
				return null;
			}
			LockStamp obj = sessionLock.Read();
			if (obj == null)
			{
				return null;
			}
			return obj.Device;
		}

		private static bool DriveReady(string mountRoot)
		{
			try
			{
				if (mountRoot.Length >= 2 && mountRoot[1] == ':' && char.IsLetter(mountRoot[0]))
				{
					string text = mountRoot.Substring(0, 2);
					char directorySeparatorChar = Path.DirectorySeparatorChar;
					return new DriveInfo(text + directorySeparatorChar).IsReady;
				}
			}
			catch
			{
			}
			return true;
		}
	}
}
namespace Cloudward.Patches
{
	[HarmonyPatch(typeof(NetworkLevelLoader), "OnApplicationQuit")]
	internal static class AppQuit
	{
		private static void Postfix()
		{
			Plugin.Coordinator?.PushAndReleaseAtQuit();
		}
	}
	[HarmonyPatch(typeof(SaveManager), "Init")]
	internal static class SaveManagerInit
	{
		private static void Postfix()
		{
			Plugin.Coordinator?.TryReconcileAtBoot();
		}
	}
}
namespace Cloudward.Lock
{
	internal sealed class SessionLock
	{
		public const string LockFileName = ".owner.lock";

		private readonly string _lockPath;

		private readonly string _device;

		private readonly int _pid;

		private readonly ManualLogSource _log;

		public string Device => _device;

		public SessionLock(string targetDir, string device, int pid, ManualLogSource log)
		{
			_lockPath = Path.Combine(targetDir, ".owner.lock");
			_device = device;
			_pid = pid;
			_log = log;
		}

		public LockStamp Read()
		{
			try
			{
				if (!File.Exists(_lockPath))
				{
					return null;
				}
				string text = File.ReadAllText(_lockPath);
				LockStamp val = default(LockStamp);
				return LockStamp.TryParse(text, ref val) ? val : null;
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] could not read lock (" + ex.GetType().Name + ": " + ex.Message + "); treating as absent."));
				return null;
			}
		}

		public LeaseDecision Decide(DateTime nowUtc, int staleSeconds, LockHeldPolicy policy)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			return LockLease.Decide(nowUtc, Read(), _device, _pid, staleSeconds, policy);
		}

		public bool Write(DateTime nowUtc)
		{
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				string directoryName = Path.GetDirectoryName(_lockPath);
				if (!string.IsNullOrEmpty(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				string text = _lockPath + ".tmp";
				File.WriteAllText(text, new LockStamp(_device, _pid, nowUtc).Serialize());
				if (File.Exists(_lockPath))
				{
					File.Delete(_lockPath);
				}
				File.Move(text, _lockPath);
				return true;
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] could not write lock (" + ex.GetType().Name + ": " + ex.Message + ")."));
				return false;
			}
		}

		public void ReleaseIfOwn()
		{
			try
			{
				LockStamp val = Read();
				if (val != null && !string.Equals(val.Device, _device, StringComparison.OrdinalIgnoreCase))
				{
					_log.LogMessage((object)("[CLOUDWARD] lock now held by " + val.Device + "; leaving it."));
				}
				else if (File.Exists(_lockPath))
				{
					File.Delete(_lockPath);
				}
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)("[CLOUDWARD] could not release lock (" + ex.GetType().Name + ": " + ex.Message + "); it will self-heal after the stale window."));
			}
		}
	}
}

Cloudward/Cloudward.Core.dll

Decompiled 2 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("Cloudward.Core")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+d3fbef8726f47d3953b79ef11c8e75d2f5805c9a")]
[assembly: AssemblyProduct("Cloudward.Core")]
[assembly: AssemblyTitle("Cloudward.Core")]
[assembly: AssemblyMetadata("BuildStamp", "d3fbef8 2026-07-24")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[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 Cloudward.Core
{
	public enum ForkResolution
	{
		NewestWins,
		ThisDevice,
		OtherDevice,
		KeepBoth,
		AskMe
	}
	public static class ForkResolve
	{
		public static bool IsAuto(ForkResolution policy)
		{
			return policy != ForkResolution.AskMe;
		}

		public static bool? LocalWins(ForkResolution policy, bool localIsNewer)
		{
			return policy switch
			{
				ForkResolution.NewestWins => localIsNewer, 
				ForkResolution.ThisDevice => true, 
				ForkResolution.OtherDevice => false, 
				ForkResolution.KeepBoth => true, 
				ForkResolution.AskMe => null, 
				_ => localIsNewer, 
			};
		}
	}
	public sealed class LocalCharState
	{
		public int BasedOnGen { get; }

		public string LastPushedHead { get; }

		public LocalCharState(int basedOnGen, string lastPushedHead)
		{
			BasedOnGen = basedOnGen;
			LastPushedHead = lastPushedHead ?? string.Empty;
		}
	}
	public sealed class ForkRecord
	{
		public string LocalHead { get; }

		public string ShareHead { get; }

		public ForkRecord(string localHead, string shareHead)
		{
			LocalHead = localHead ?? string.Empty;
			ShareHead = shareHead ?? string.Empty;
		}
	}
	public sealed class LocalSyncState
	{
		private readonly Dictionary<string, LocalCharState> _state = new Dictionary<string, LocalCharState>(StringComparer.Ordinal);

		private readonly Dictionary<string, ForkRecord> _forks = new Dictionary<string, ForkRecord>(StringComparer.Ordinal);

		private readonly HashSet<string> _bootstrapped = new HashSet<string>(StringComparer.Ordinal);

		public IEnumerable<string> PendingForks => _forks.Keys;

		public bool HasForks => _forks.Count > 0;

		public LocalCharState? Get(string uid)
		{
			if (!_state.TryGetValue(uid, out LocalCharState value))
			{
				return null;
			}
			return value;
		}

		public void Set(string uid, LocalCharState s)
		{
			_state[uid] = s;
		}

		public ForkRecord? GetFork(string uid)
		{
			if (!_forks.TryGetValue(uid, out ForkRecord value))
			{
				return null;
			}
			return value;
		}

		public void SetFork(string uid, ForkRecord f)
		{
			_forks[uid] = f;
		}

		public void ClearFork(string uid)
		{
			_forks.Remove(uid);
		}

		public bool IsBootstrapped(string mountPath)
		{
			return _bootstrapped.Contains(mountPath ?? string.Empty);
		}

		public void MarkBootstrapped(string mountPath)
		{
			_bootstrapped.Add(mountPath ?? string.Empty);
		}

		public string Serialize()
		{
			List<string> list = new List<string>();
			foreach (KeyValuePair<string, LocalCharState> item in _state.OrderBy<KeyValuePair<string, LocalCharState>, string>((KeyValuePair<string, LocalCharState> k) => k.Key, StringComparer.Ordinal))
			{
				list.Add("S|" + Escape(item.Key) + "|" + item.Value.BasedOnGen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(item.Value.LastPushedHead));
			}
			foreach (KeyValuePair<string, ForkRecord> item2 in _forks.OrderBy<KeyValuePair<string, ForkRecord>, string>((KeyValuePair<string, ForkRecord> k) => k.Key, StringComparer.Ordinal))
			{
				list.Add("F|" + Escape(item2.Key) + "|" + Escape(item2.Value.LocalHead) + "|" + Escape(item2.Value.ShareHead));
			}
			foreach (string item3 in _bootstrapped.OrderBy<string, string>((string k) => k, StringComparer.Ordinal))
			{
				list.Add("B|" + Escape(item3));
			}
			return string.Join("\n", list);
		}

		public static LocalSyncState Parse(string text)
		{
			LocalSyncState localSyncState = new LocalSyncState();
			if (string.IsNullOrEmpty(text))
			{
				return localSyncState;
			}
			string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length == 0)
				{
					continue;
				}
				string[] array2 = text2.Split(new char[1] { '|' });
				if (array2.Length < 2)
				{
					continue;
				}
				if (array2[0] == "B")
				{
					localSyncState._bootstrapped.Add(Unescape(array2[1]));
				}
				else if (array2.Length >= 4)
				{
					if (array2[0] == "S" && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						localSyncState._state[Unescape(array2[1])] = new LocalCharState(result, Unescape(array2[3]));
					}
					else if (array2[0] == "F")
					{
						localSyncState._forks[Unescape(array2[1])] = new ForkRecord(Unescape(array2[2]), Unescape(array2[3]));
					}
				}
			}
			return localSyncState;
		}

		private static string Escape(string v)
		{
			return (v ?? string.Empty).Replace("|", "%7C");
		}

		private static string Unescape(string v)
		{
			return v.Replace("%7C", "|");
		}
	}
	public enum LockHeldPolicy
	{
		FallbackLocal,
		ReadOnly,
		ForceTake
	}
	public enum LeaseDecision
	{
		Take,
		FallbackLocal,
		ReadOnly
	}
	public static class LockLease
	{
		public static LeaseDecision Decide(DateTime nowUtc, LockStamp existing, string ownDevice, int ownPid, int staleSeconds, LockHeldPolicy policy)
		{
			if (existing == null)
			{
				return LeaseDecision.Take;
			}
			if (string.Equals(existing.Device, ownDevice, StringComparison.OrdinalIgnoreCase))
			{
				return LeaseDecision.Take;
			}
			if ((nowUtc - existing.HeartbeatUtc).TotalSeconds > (double)staleSeconds)
			{
				return LeaseDecision.Take;
			}
			return policy switch
			{
				LockHeldPolicy.ForceTake => LeaseDecision.Take, 
				LockHeldPolicy.ReadOnly => LeaseDecision.ReadOnly, 
				_ => LeaseDecision.FallbackLocal, 
			};
		}
	}
	public sealed class LockStamp
	{
		public string Device { get; }

		public int Pid { get; }

		public DateTime HeartbeatUtc { get; }

		public LockStamp(string device, int pid, DateTime heartbeatUtc)
		{
			Device = Sanitize(device);
			Pid = pid;
			HeartbeatUtc = heartbeatUtc.ToUniversalTime();
		}

		public string Serialize()
		{
			return Device + "\n" + Pid.ToString(CultureInfo.InvariantCulture) + "\n" + HeartbeatUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
		}

		public static bool TryParse(string text, out LockStamp? stamp)
		{
			stamp = null;
			if (string.IsNullOrWhiteSpace(text))
			{
				return false;
			}
			string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
			if (array.Length < 3)
			{
				return false;
			}
			string device = array[0].Trim();
			if (!int.TryParse(array[1].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
			{
				return false;
			}
			if (!DateTime.TryParse(array[2].Trim(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result2))
			{
				return false;
			}
			stamp = new LockStamp(device, result, result2);
			return true;
		}

		private static string Sanitize(string s)
		{
			if (!string.IsNullOrEmpty(s))
			{
				return s.Replace("\r", " ").Replace("\n", " ").Trim();
			}
			return string.Empty;
		}
	}
	public enum MountDecision
	{
		Live,
		Offline,
		CreateMarkerThenLive
	}
	public static class MountGuard
	{
		public static bool IsLive(bool markerExists, bool driveReady)
		{
			return markerExists && driveReady;
		}

		public static MountDecision Decide(bool markerExists, bool alreadyBootstrapped, bool shareHasData, bool driveReady, bool requireExistingMarker)
		{
			if (!driveReady)
			{
				return MountDecision.Offline;
			}
			if (markerExists)
			{
				return MountDecision.Live;
			}
			if (requireExistingMarker)
			{
				return MountDecision.Offline;
			}
			if (shareHasData)
			{
				return MountDecision.CreateMarkerThenLive;
			}
			if (!alreadyBootstrapped)
			{
				return MountDecision.CreateMarkerThenLive;
			}
			return MountDecision.Offline;
		}
	}
	public static class PathRebase
	{
		public const string SaveBase = "SaveGames";

		public static string Target(string originalSavePath, string mountRoot)
		{
			string text = LastElement(originalSavePath);
			return TrimTrailingSeparators(mountRoot) + "/SaveGames/" + text;
		}

		public static string LastElement(string path)
		{
			if (string.IsNullOrEmpty(path))
			{
				return string.Empty;
			}
			string text = TrimTrailingSeparators(path);
			int num = text.LastIndexOfAny(new char[2] { '/', '\\' });
			if (num >= 0)
			{
				return text.Substring(num + 1);
			}
			return text;
		}

		private static string TrimTrailingSeparators(string path)
		{
			if (!string.IsNullOrEmpty(path))
			{
				return path.TrimEnd('/', '\\');
			}
			return path;
		}
	}
	public enum SyncAction
	{
		UpToDate,
		Pull,
		Push,
		Fork
	}
	public sealed class ReconcilePlan
	{
		public string Uid { get; }

		public SyncAction Action { get; }

		public IReadOnlyList<string> CopyTimestamps { get; }

		public int NewGen { get; }

		public string NewHead { get; }

		public string LocalHead { get; }

		public string ShareHead { get; }

		public ReconcilePlan(string uid, SyncAction action, IReadOnlyList<string> copy, int newGen, string newHead, string localHead, string shareHead)
		{
			Uid = uid;
			Action = action;
			CopyTimestamps = copy ?? Array.Empty<string>();
			NewGen = newGen;
			NewHead = newHead ?? string.Empty;
			LocalHead = localHead ?? string.Empty;
			ShareHead = shareHead ?? string.Empty;
		}
	}
	public static class Reconciler
	{
		public static ReconcilePlan Plan(string uid, CharTree? local, CharTree? share, LedgerEntry? ledger, LocalCharState? state)
		{
			string text = local?.Head;
			string text2 = share?.Head;
			int num = ledger?.Gen ?? 0;
			int num2 = state?.BasedOnGen ?? 0;
			bool flag = !string.IsNullOrEmpty(text);
			bool flag2 = !string.IsNullOrEmpty(text2);
			if (!flag && !flag2)
			{
				return Make(uid, SyncAction.UpToDate, Array.Empty<string>(), num, "", text, text2);
			}
			if (flag && !flag2)
			{
				return Make(uid, SyncAction.Push, local.Snapshots, num + 1, text, text, text2);
			}
			if (!flag && flag2)
			{
				return Make(uid, SyncAction.Pull, share.Snapshots, num, text2, text, text2);
			}
			if (text == text2)
			{
				return Make(uid, SyncAction.UpToDate, Array.Empty<string>(), Math.Max(num, num2), text, text, text2);
			}
			if (state == null)
			{
				if (share.Snapshots.Contains(text))
				{
					return Make(uid, SyncAction.Pull, Missing(share, local), num, text2, text, text2);
				}
				if (local.Snapshots.Contains(text2))
				{
					return Make(uid, SyncAction.Push, Missing(local, share), num + 1, text, text, text2);
				}
				return Make(uid, SyncAction.Fork, Array.Empty<string>(), num, "", text, text2);
			}
			bool flag3 = text != state.LastPushedHead;
			bool flag4 = num > num2;
			if (flag3 && !flag4)
			{
				return Make(uid, SyncAction.Push, Missing(local, share), num2 + 1, text, text, text2);
			}
			if (!flag3 && flag4)
			{
				return Make(uid, SyncAction.Pull, Missing(share, local), num, text2, text, text2);
			}
			return Make(uid, SyncAction.Fork, Array.Empty<string>(), num, "", text, text2);
		}

		public static bool LocalIsNewer(string localHead, string shareHead)
		{
			return string.CompareOrdinal(localHead ?? string.Empty, shareHead ?? string.Empty) > 0;
		}

		private static IReadOnlyList<string> Missing(CharTree from, CharTree to)
		{
			HashSet<string> have = new HashSet<string>(to.Snapshots, StringComparer.Ordinal);
			return from.Snapshots.Where((string s) => !have.Contains(s)).ToList();
		}

		private static ReconcilePlan Make(string uid, SyncAction a, IReadOnlyList<string> copy, int newGen, string newHead, string? localHead, string? shareHead)
		{
			return new ReconcilePlan(uid, a, copy, newGen, newHead, localHead ?? "", shareHead ?? "");
		}
	}
	public sealed class CharTree
	{
		public string Uid { get; }

		public IReadOnlyList<string> Snapshots { get; }

		public string? Head
		{
			get
			{
				if (Snapshots.Count <= 0)
				{
					return null;
				}
				return Snapshots[Snapshots.Count - 1];
			}
		}

		public bool IsEmpty => Snapshots.Count == 0;

		public CharTree(string uid, IEnumerable<string> snapshots)
		{
			Uid = uid;
			Snapshots = snapshots.OrderBy<string, string>((string s) => s, StringComparer.Ordinal).ToList();
		}
	}
	public static class TreeScanner
	{
		public const string SavePrefix = "Save_";

		public const string ManifestName = "Manifest.txt";

		public const string DoneLine = "Done";

		public static Dictionary<string, CharTree> Scan(string saveRoot)
		{
			Dictionary<string, CharTree> dictionary = new Dictionary<string, CharTree>(StringComparer.Ordinal);
			if (string.IsNullOrEmpty(saveRoot) || !Directory.Exists(saveRoot))
			{
				return dictionary;
			}
			string[] directories = Directory.GetDirectories(saveRoot);
			foreach (string path in directories)
			{
				string text = LastSegment(path);
				if (!text.StartsWith("Save_", StringComparison.Ordinal))
				{
					continue;
				}
				string text2 = text.Substring("Save_".Length);
				List<string> list = new List<string>();
				string[] directories2 = Directory.GetDirectories(path);
				foreach (string text3 in directories2)
				{
					if (IsComplete(text3))
					{
						list.Add(LastSegment(text3));
					}
				}
				dictionary[text2] = new CharTree(text2, list);
			}
			return dictionary;
		}

		public static bool IsComplete(string snapshotDir)
		{
			try
			{
				string path = Path.Combine(snapshotDir, "Manifest.txt");
				if (!File.Exists(path))
				{
					return false;
				}
				string text = File.ReadAllLines(path).LastOrDefault((string l) => l.Trim().Length > 0);
				return text != null && text.Trim() == "Done";
			}
			catch
			{
				return false;
			}
		}

		private static string LastSegment(string path)
		{
			return Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
		}
	}
	public static class SnapshotCopier
	{
		public const int DefaultKeep = 15;

		public static int CopySnapshots(string srcCharDir, string dstCharDir, IEnumerable<string> timestamps)
		{
			Directory.CreateDirectory(dstCharDir);
			int num = 0;
			foreach (string timestamp in timestamps)
			{
				string text = Path.Combine(srcCharDir, timestamp);
				string text2 = Path.Combine(dstCharDir, timestamp);
				if (!Directory.Exists(text) || Directory.Exists(text2))
				{
					continue;
				}
				string text3 = text2 + ".tmp";
				try
				{
					if (Directory.Exists(text3))
					{
						Directory.Delete(text3, recursive: true);
					}
					CopyDir(text, text3);
					Directory.Move(text3, text2);
					num++;
				}
				catch
				{
					try
					{
						if (Directory.Exists(text3))
						{
							Directory.Delete(text3, recursive: true);
						}
					}
					catch
					{
					}
					throw;
				}
			}
			return num;
		}

		public static int RemoveSnapshots(string charDir, IEnumerable<string> timestamps)
		{
			int num = 0;
			foreach (string timestamp in timestamps)
			{
				try
				{
					string path = Path.Combine(charDir, timestamp);
					if (Directory.Exists(path))
					{
						Directory.Delete(path, recursive: true);
						num++;
					}
				}
				catch
				{
				}
			}
			return num;
		}

		public static int PruneTo(string charDir, int keep = 15)
		{
			if (!Directory.Exists(charDir))
			{
				return 0;
			}
			List<string> list = (from d in Directory.GetDirectories(charDir)
				select Path.GetFileName(d.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) into n
				where !n.EndsWith(".tmp", StringComparison.Ordinal)
				select n).OrderBy<string, string>((string n) => n, StringComparer.Ordinal).ToList();
			int num = 0;
			for (int num2 = 0; num2 < list.Count - keep; num2++)
			{
				try
				{
					Directory.Delete(Path.Combine(charDir, list[num2]), recursive: true);
					num++;
				}
				catch
				{
				}
			}
			return num;
		}

		private static void CopyDir(string src, string dst)
		{
			Directory.CreateDirectory(dst);
			string[] files = Directory.GetFiles(src);
			foreach (string text in files)
			{
				File.Copy(text, Path.Combine(dst, Path.GetFileName(text)), overwrite: true);
			}
			files = Directory.GetDirectories(src);
			foreach (string text2 in files)
			{
				CopyDir(text2, Path.Combine(dst, Path.GetFileName(text2.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))));
			}
		}
	}
	public sealed class LedgerEntry
	{
		public int Gen { get; }

		public string Head { get; }

		public string Device { get; }

		public LedgerEntry(int gen, string head, string device)
		{
			Gen = gen;
			Head = head ?? string.Empty;
			Device = device ?? string.Empty;
		}
	}
	public sealed class SyncLedger
	{
		private readonly Dictionary<string, LedgerEntry> _entries = new Dictionary<string, LedgerEntry>(StringComparer.Ordinal);

		public IEnumerable<string> Uids => _entries.Keys;

		public LedgerEntry? Get(string uid)
		{
			if (!_entries.TryGetValue(uid, out LedgerEntry value))
			{
				return null;
			}
			return value;
		}

		public void Set(string uid, LedgerEntry entry)
		{
			_entries[uid] = entry;
		}

		public string Serialize()
		{
			return string.Join("\n", from kv in _entries.OrderBy<KeyValuePair<string, LedgerEntry>, string>((KeyValuePair<string, LedgerEntry> kv) => kv.Key, StringComparer.Ordinal)
				select Escape(kv.Key) + "|" + kv.Value.Gen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(kv.Value.Head) + "|" + Escape(kv.Value.Device));
		}

		public static SyncLedger Parse(string text)
		{
			SyncLedger syncLedger = new SyncLedger();
			if (string.IsNullOrEmpty(text))
			{
				return syncLedger;
			}
			string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i].Trim();
				if (text2.Length != 0)
				{
					string[] array2 = text2.Split(new char[1] { '|' });
					if (array2.Length >= 4 && int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						syncLedger._entries[Unescape(array2[0])] = new LedgerEntry(result, Unescape(array2[2]), Unescape(array2[3]));
					}
				}
			}
			return syncLedger;
		}

		private static string Escape(string s)
		{
			return (s ?? string.Empty).Replace("|", "%7C");
		}

		private static string Unescape(string s)
		{
			return s.Replace("%7C", "|");
		}
	}
}