Decompiled source of Cloudward v0.2.0

patchers/Cloudward.Preload/Cloudward.Core.dll

Decompiled 3 weeks 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 System.Security.Cryptography;
using System.Text;
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("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+83254ebc1e0f377c73c2296a9412cdb0c1c00f84")]
[assembly: AssemblyProduct("Cloudward.Core")]
[assembly: AssemblyTitle("Cloudward.Core")]
[assembly: InternalsVisibleTo("Cloudward.Tests")]
[assembly: AssemblyMetadata("BuildStamp", "83254ebc 2026-08-28")]
[assembly: AssemblyVersion("0.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[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 sealed class CfgDoc
	{
		private static readonly byte[] Bom = new byte[3] { 239, 187, 191 };

		private static readonly byte[] DefaultPrefix = Encoding.ASCII.GetBytes("# Default value:");

		private static readonly Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

		private readonly byte[] _raw;

		private readonly byte[] _bom;

		private readonly byte[] _eol;

		private readonly List<byte[]> _lines;

		private readonly Dictionary<CfgKey, int> _index = new Dictionary<CfgKey, int>();

		private readonly Dictionary<CfgKey, string> _defaults = new Dictionary<CfgKey, string>();

		public IReadOnlyList<CfgKey> Keys => (from kv in _index
			orderby kv.Value
			select kv.Key).ToList();

		public byte[] Raw => _raw;

		public CfgDoc(byte[] raw)
		{
			_raw = raw ?? Array.Empty<byte>();
			_bom = (StartsWith(_raw, Bom) ? Bom : Array.Empty<byte>());
			byte[] array = new byte[_raw.Length - _bom.Length];
			Array.Copy(_raw, _bom.Length, array, 0, array.Length);
			_eol = ((!Contains(array, 13, 10)) ? new byte[1] { 10 } : new byte[2] { 13, 10 });
			_lines = SplitOn(array, _eol);
			string text = string.Empty;
			string text2 = null;
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			for (int i = 0; i < _lines.Count; i++)
			{
				byte[] array2 = _lines[i];
				string text3 = MatchSection(array2);
				if (text3 != null)
				{
					text = text3;
					if (!hashSet.Add(text))
					{
						throw new InvalidDataException("section [" + text + "] appears twice — malformed cfg, refusing");
					}
					text2 = null;
					continue;
				}
				string text4 = MatchDefault(array2);
				if (text4 != null)
				{
					text2 = text4;
				}
				else
				{
					if ((array2.Length != 0 && array2[0] == 35) || IsBlank(array2))
					{
						continue;
					}
					if (TrySplitSetting(array2, out int _, out string key))
					{
						CfgKey key2 = new CfgKey(text, key);
						_index[key2] = i;
						if (text2 != null)
						{
							_defaults[key2] = text2;
						}
					}
					text2 = null;
				}
			}
		}

		public static CfgDoc FromText(string text)
		{
			return new CfgDoc(Utf8.GetBytes(text ?? string.Empty));
		}

		public string? Default(CfgKey key)
		{
			if (!_defaults.TryGetValue(key, out string value))
			{
				return null;
			}
			return value;
		}

		public bool Contains(CfgKey key)
		{
			return _index.ContainsKey(key);
		}

		public string? Get(CfgKey key)
		{
			if (!_index.TryGetValue(key, out var value))
			{
				return null;
			}
			if (!TrySplitSetting(_lines[value], out int preLen, out string _))
			{
				return null;
			}
			return Utf8.GetString(_lines[value], preLen, _lines[value].Length - preLen);
		}

		public bool Set(CfgKey key, string value)
		{
			if (!_index.TryGetValue(key, out var value2))
			{
				return false;
			}
			byte[] array = _lines[value2];
			if (!TrySplitSetting(array, out int preLen, out string _))
			{
				return false;
			}
			byte[] bytes = Utf8.GetBytes(value ?? string.Empty);
			byte[] array2 = new byte[preLen + bytes.Length];
			Array.Copy(array, 0, array2, 0, preLen);
			Array.Copy(bytes, 0, array2, preLen, bytes.Length);
			_lines[value2] = array2;
			return true;
		}

		public byte[] ToBytes()
		{
			int num = _bom.Length;
			for (int i = 0; i < _lines.Count; i++)
			{
				num += _lines[i].Length + ((i > 0) ? _eol.Length : 0);
			}
			byte[] array = new byte[num];
			int num2 = 0;
			Array.Copy(_bom, 0, array, num2, _bom.Length);
			num2 += _bom.Length;
			for (int j = 0; j < _lines.Count; j++)
			{
				if (j > 0)
				{
					Array.Copy(_eol, 0, array, num2, _eol.Length);
					num2 += _eol.Length;
				}
				Array.Copy(_lines[j], 0, array, num2, _lines[j].Length);
				num2 += _lines[j].Length;
			}
			return array;
		}

		public bool Changed()
		{
			return !BytesEqual(ToBytes(), _raw);
		}

		private static string? MatchSection(byte[] line)
		{
			if (line.Length < 3 || line[0] != 91)
			{
				return null;
			}
			int num = line.Length;
			while (num > 0 && IsSpace(line[num - 1]))
			{
				num--;
			}
			if (num < 3 || line[num - 1] != 93)
			{
				return null;
			}
			return Utf8.GetString(line, 1, num - 2);
		}

		private static string? MatchDefault(byte[] line)
		{
			if (!StartsWith(line, DefaultPrefix))
			{
				return null;
			}
			int num = DefaultPrefix.Length;
			if (num < line.Length && line[num] == 32)
			{
				num++;
			}
			return Utf8.GetString(line, num, line.Length - num);
		}

		private static bool TrySplitSetting(byte[] line, out int preLen, out string key)
		{
			preLen = 0;
			key = string.Empty;
			if (line.Length == 0)
			{
				return false;
			}
			byte b = line[0];
			if (b == 35 || b == 91 || b == 13 || b == 10 || b == 61)
			{
				return false;
			}
			int num = Array.IndexOf(line, (byte)61);
			if (num <= 0)
			{
				return false;
			}
			preLen = num + 1;
			if (preLen < line.Length && (line[preLen] == 32 || line[preLen] == 9))
			{
				preLen++;
			}
			key = Utf8.GetString(line, 0, num).Trim();
			return key.Length > 0;
		}

		private static bool IsSpace(byte b)
		{
			if (b != 32 && b != 9 && b != 13)
			{
				return b == 10;
			}
			return true;
		}

		private static bool IsBlank(byte[] line)
		{
			for (int i = 0; i < line.Length; i++)
			{
				if (!IsSpace(line[i]))
				{
					return false;
				}
			}
			return true;
		}

		private static bool StartsWith(byte[] data, byte[] prefix)
		{
			if (data.Length < prefix.Length)
			{
				return false;
			}
			for (int i = 0; i < prefix.Length; i++)
			{
				if (data[i] != prefix[i])
				{
					return false;
				}
			}
			return true;
		}

		private static bool Contains(byte[] data, byte a, byte b)
		{
			for (int i = 0; i + 1 < data.Length; i++)
			{
				if (data[i] == a && data[i + 1] == b)
				{
					return true;
				}
			}
			return false;
		}

		private static bool BytesEqual(byte[] x, byte[] y)
		{
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}

		private static List<byte[]> SplitOn(byte[] data, byte[] sep)
		{
			List<byte[]> list = new List<byte[]>();
			int num = 0;
			for (int i = 0; i + sep.Length <= data.Length; i++)
			{
				bool flag = true;
				for (int j = 0; j < sep.Length; j++)
				{
					if (data[i + j] != sep[j])
					{
						flag = false;
						break;
					}
				}
				if (flag)
				{
					list.Add(Slice(data, num, i - num));
					i += sep.Length - 1;
					num = i + 1;
				}
			}
			list.Add(Slice(data, num, data.Length - num));
			return list;
		}

		private static byte[] Slice(byte[] data, int offset, int count)
		{
			byte[] array = new byte[count];
			Array.Copy(data, offset, array, 0, count);
			return array;
		}
	}
	public readonly struct CfgKey : IEquatable<CfgKey>, IComparable<CfgKey>
	{
		public string Section { get; }

		public string Key { get; }

		public CfgKey(string section, string key)
		{
			Section = section ?? string.Empty;
			Key = key ?? string.Empty;
		}

		public bool Equals(CfgKey other)
		{
			if (string.Equals(Section, other.Section, StringComparison.Ordinal))
			{
				return string.Equals(Key, other.Key, StringComparison.Ordinal);
			}
			return false;
		}

		public override bool Equals(object? obj)
		{
			if (obj is CfgKey other)
			{
				return Equals(other);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return (Section.GetHashCode() * 397) ^ Key.GetHashCode();
		}

		public int CompareTo(CfgKey other)
		{
			int num = string.CompareOrdinal(Section, other.Section);
			if (num == 0)
			{
				return string.CompareOrdinal(Key, other.Key);
			}
			return num;
		}

		public override string ToString()
		{
			return "[" + Section + "] " + Key;
		}
	}
	public sealed class CfgMergeResult
	{
		public byte[] Before { get; }

		public byte[] After { get; }

		public IReadOnlyList<string> Changes { get; }

		public IReadOnlyList<string> Skipped { get; }

		public bool Dirty => !SequenceEqual(Before, After);

		public CfgMergeResult(byte[] before, byte[] after, IReadOnlyList<string> changes, IReadOnlyList<string> skipped)
		{
			Before = before;
			After = after;
			Changes = changes;
			Skipped = skipped;
		}

		private static bool SequenceEqual(byte[] x, byte[] y)
		{
			if (x == y)
			{
				return true;
			}
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}
	}
	public static class CfgMerge
	{
		public static CfgMergeResult Apply(byte[] raw, IReadOnlyDictionary<CfgKey, string> overlay)
		{
			CfgDoc cfgDoc = new CfgDoc(raw);
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			foreach (KeyValuePair<CfgKey, string> item in (overlay ?? new Dictionary<CfgKey, string>()).OrderBy<KeyValuePair<CfgKey, string>, CfgKey>((KeyValuePair<CfgKey, string> kv) => kv.Key))
			{
				string text = cfgDoc.Get(item.Key);
				if (text == null)
				{
					list2.Add($"{item.Key} — not bound in target (host hasn't launched this build?)");
				}
				else if (!string.Equals(text, item.Value, StringComparison.Ordinal) && cfgDoc.Set(item.Key, item.Value))
				{
					list.Add($"{item.Key}: \"{text}\" -> \"{item.Value}\"");
				}
			}
			byte[] array = cfgDoc.ToBytes();
			if (list.Count == 0 && !BytesEqual(array, cfgDoc.Raw))
			{
				throw new InvalidOperationException("no-op merge must be byte-identical — CfgDoc round-trip is lossy, refusing to write");
			}
			return new CfgMergeResult(raw, array, list, list2);
		}

		public static IReadOnlyDictionary<CfgKey, string> Extract(byte[] raw, IEnumerable<CfgKey> allowed)
		{
			CfgDoc cfgDoc = new CfgDoc(raw);
			Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
			foreach (CfgKey item in allowed ?? Array.Empty<CfgKey>())
			{
				string text = cfgDoc.Get(item);
				if (text != null)
				{
					dictionary[item] = text;
				}
			}
			return dictionary;
		}

		private static bool BytesEqual(byte[] x, byte[] y)
		{
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}
	}
	public sealed class CfgOverlay
	{
		public const string FileSuffix = ".cfg.overlay";

		public string Guid { get; }

		public IReadOnlyDictionary<CfgKey, string> Values { get; }

		public int Count => Values.Count;

		public CfgOverlay(string guid, IReadOnlyDictionary<CfgKey, string> values)
		{
			Guid = guid ?? string.Empty;
			Values = values ?? new Dictionary<CfgKey, string>();
		}

		public bool Allows(CfgKey key)
		{
			return Values.ContainsKey(key);
		}

		public static string GuidFromFileName(string fileName)
		{
			string fileName2 = Path.GetFileName(fileName ?? string.Empty);
			if (!fileName2.EndsWith(".cfg.overlay", StringComparison.OrdinalIgnoreCase))
			{
				return fileName2;
			}
			return fileName2.Substring(0, fileName2.Length - ".cfg.overlay".Length);
		}

		public static CfgOverlay Parse(string guid, string? text)
		{
			Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
			if (string.IsNullOrEmpty(text))
			{
				return new CfgOverlay(guid, dictionary);
			}
			string text2 = string.Empty;
			string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text3 = array[i].Trim();
				if (text3.Length == 0 || text3[0] == '#')
				{
					continue;
				}
				if (text3[0] == '[' && text3[text3.Length - 1] == ']')
				{
					text2 = text3.Substring(1, text3.Length - 2);
					Refuse(text2, "section");
					continue;
				}
				int num = text3.IndexOf('=');
				if (num >= 0)
				{
					string text4 = text3.Substring(0, num).Trim();
					Refuse(text4, "key");
					string text5 = text3.Substring(num + 1);
					if (text5.StartsWith(" ", StringComparison.Ordinal))
					{
						text5 = text5.Substring(1);
					}
					dictionary[new CfgKey(text2, text4)] = text5;
				}
			}
			return new CfgOverlay(guid, dictionary);
		}

		public string Serialize(string header)
		{
			List<string> list = new List<string>
			{
				(header ?? string.Empty).TrimEnd(Array.Empty<char>()),
				string.Empty
			};
			foreach (string section in Values.Keys.Select((CfgKey k) => k.Section).Distinct().OrderBy<string, string>((string s) => s, StringComparer.Ordinal))
			{
				list.Add("[" + section + "]");
				foreach (KeyValuePair<CfgKey, string> item in from kv in Values
					where string.Equals(kv.Key.Section, section, StringComparison.Ordinal)
					orderby kv.Key
					select kv)
				{
					list.Add(item.Key.Key + " = " + item.Value);
				}
				list.Add(string.Empty);
			}
			return string.Join("\n", list) + "\n";
		}

		private static void Refuse(string token, string what)
		{
			if (token.IndexOf('*') >= 0 || token.IndexOf('?') >= 0)
			{
				throw new InvalidDataException("overlay " + what + " \"" + token + "\" contains a wildcard. Overlays name individual keys; there is deliberately no way to say \"everything\" — see config/README.md.");
			}
		}
	}
	public sealed class CfgValueSet
	{
		public sealed class Row
		{
			public string Guid { get; }

			public CfgKey Key { get; }

			public string Value { get; }

			public string RelPath => Guid + ".cfg/" + Key.Section + "/" + Key.Key;

			public Row(string guid, CfgKey key, string value)
			{
				Guid = guid ?? string.Empty;
				Key = key;
				Value = value ?? string.Empty;
			}
		}

		public IReadOnlyList<Row> Rows { get; }

		public static CfgValueSet Empty => new CfgValueSet(Array.Empty<Row>());

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

		public CfgValueSet(IEnumerable<Row> rows)
		{
			Rows = (from g in (rows ?? Array.Empty<Row>()).Where((Row r) => r != null && r.Guid.Length > 0).GroupBy<Row, string>((Row r) => r.RelPath, StringComparer.Ordinal)
				select g.First()).OrderBy<Row, string>((Row r) => r.RelPath, StringComparer.Ordinal).ToList();
		}

		public PayloadManifest ToManifest()
		{
			return new PayloadManifest(PayloadTier.Config, Rows.Select((Row r) => new PayloadEntry(r.RelPath, AtomicFile.Utf8NoBom.GetByteCount(r.Value), PayloadHasher.HashBytes(AtomicFile.Utf8NoBom.GetBytes(r.Value)))));
		}

		public string Serialize()
		{
			return string.Join("\n", Rows.Select((Row r) => Escape(r.Guid) + "|" + Escape(r.Key.Section) + "|" + Escape(r.Key.Key) + "|" + Escape(r.Value)));
		}

		public static CfgValueSet Parse(string? text)
		{
			List<Row> list = new List<Row>();
			if (string.IsNullOrEmpty(text))
			{
				return new CfgValueSet(list);
			}
			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 && text2[0] != '#')
				{
					string[] array2 = text2.Split(new char[1] { '|' });
					if (array2.Length >= 4)
					{
						list.Add(new Row(Unescape(array2[0]), new CfgKey(Unescape(array2[1]), Unescape(array2[2])), Unescape(array2[3])));
					}
				}
			}
			return new CfgValueSet(list);
		}

		public IReadOnlyDictionary<string, IReadOnlyDictionary<CfgKey, string>> ByGuid()
		{
			Dictionary<string, IReadOnlyDictionary<CfgKey, string>> dictionary = new Dictionary<string, IReadOnlyDictionary<CfgKey, string>>(StringComparer.OrdinalIgnoreCase);
			foreach (IGrouping<string, Row> item in Rows.GroupBy<Row, string>((Row r) => r.Guid, StringComparer.OrdinalIgnoreCase))
			{
				Dictionary<CfgKey, string> dictionary2 = new Dictionary<CfgKey, string>();
				foreach (Row item2 in item)
				{
					dictionary2[item2.Key] = item2.Value;
				}
				dictionary[item.Key] = dictionary2;
			}
			return dictionary;
		}

		public static CfgValueSet Extract(IEnumerable<CfgOverlay> overlays, Func<string, byte[]?> readCfg)
		{
			IReadOnlyList<string> unreadable;
			return Extract(overlays, readCfg, out unreadable);
		}

		public static CfgValueSet Extract(IEnumerable<CfgOverlay> overlays, Func<string, byte[]?> readCfg, out IReadOnlyList<string> unreadable)
		{
			List<Row> list = new List<Row>();
			List<string> list2 = new List<string>();
			foreach (CfgOverlay item in overlays ?? Array.Empty<CfgOverlay>())
			{
				byte[] array;
				try
				{
					array = readCfg(item.Guid);
				}
				catch
				{
					list2.Add(item.Guid + ".cfg (read failed)");
					continue;
				}
				if (array == null || array.Length == 0)
				{
					continue;
				}
				IReadOnlyDictionary<CfgKey, string> readOnlyDictionary;
				try
				{
					readOnlyDictionary = CfgMerge.Extract(array, item.Values.Keys);
				}
				catch
				{
					list2.Add(item.Guid + ".cfg (parse failed)");
					continue;
				}
				foreach (KeyValuePair<CfgKey, string> item2 in readOnlyDictionary)
				{
					list.Add(new Row(item.Guid, item2.Key, item2.Value));
				}
			}
			unreadable = list2;
			return new CfgValueSet(list);
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public enum ForkResolution
	{
		NewestWins,
		ThisDevice,
		OtherDevice,
		KeepBoth,
		AskMe
	}
	public enum ForkGate
	{
		AutoResolve,
		DeferMidSession,
		PauseForManual,
		AlreadyPaused
	}
	public static class ForkResolve
	{
		public static ForkGate Gate(bool autoPolicy, bool mayMutateLocal, bool alreadyPending)
		{
			if (!autoPolicy)
			{
				if (!alreadyPending)
				{
					return ForkGate.PauseForManual;
				}
				return ForkGate.AlreadyPaused;
			}
			if (!mayMutateLocal)
			{
				return ForkGate.DeferMidSession;
			}
			return ForkGate.AutoResolve;
		}

		public static bool DivergenceExists(CharTree? local, CharTree? share)
		{
			if (local != null && !local.IsEmpty && share != null)
			{
				return !share.IsEmpty;
			}
			return false;
		}

		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 static class JoinRaceGate
	{
		public enum PullDecision
		{
			Pull,
			HoldAndRetry,
			DeferToNextLaunch
		}

		public const float DefaultHoldBudgetSeconds = 60f;

		public static PullDecision Decide(bool mayMutateLocal, bool savesPinned, bool characterEverInUse, float heldSeconds, float holdBudgetSeconds = 60f)
		{
			if (!mayMutateLocal)
			{
				return PullDecision.DeferToNextLaunch;
			}
			if (!savesPinned)
			{
				return PullDecision.Pull;
			}
			if (characterEverInUse)
			{
				return PullDecision.DeferToNextLaunch;
			}
			if (heldSeconds > holdBudgetSeconds)
			{
				return PullDecision.DeferToNextLaunch;
			}
			return PullDecision.HoldAndRetry;
		}
	}
	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 object _gate = new object();

		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 IReadOnlyList<string> PendingForks
		{
			get
			{
				lock (_gate)
				{
					return _forks.Keys.ToList();
				}
			}
		}

		public bool HasForks
		{
			get
			{
				lock (_gate)
				{
					return _forks.Count > 0;
				}
			}
		}

		public LocalCharState? Get(string uid)
		{
			lock (_gate)
			{
				LocalCharState value;
				return _state.TryGetValue(uid, out value) ? value : null;
			}
		}

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

		public ForkRecord? GetFork(string uid)
		{
			lock (_gate)
			{
				ForkRecord value;
				return _forks.TryGetValue(uid, out value) ? value : null;
			}
		}

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

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

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

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

		public string Serialize()
		{
			lock (_gate)
			{
				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
	}
	public static class LockLease
	{
		public static LeaseDecision Decide(DateTime nowUtc, LockStamp? existing, string ownDevice, int ownPid, int staleSeconds, LockHeldPolicy policy, Func<int, bool>? isPidAlive = null)
		{
			if (existing == null)
			{
				return LeaseDecision.Take;
			}
			if (string.Equals(existing.Device, ownDevice, StringComparison.OrdinalIgnoreCase) && (existing.Pid == ownPid || isPidAlive == null || !isPidAlive(existing.Pid)))
			{
				return LeaseDecision.Take;
			}
			if ((nowUtc - existing.HeartbeatUtc).TotalSeconds > (double)staleSeconds)
			{
				return LeaseDecision.Take;
			}
			return Held(policy);
		}

		private static LeaseDecision Held(LockHeldPolicy policy)
		{
			if (policy != LockHeldPolicy.ForceTake)
			{
				return LeaseDecision.FallbackLocal;
			}
			return LeaseDecision.Take;
		}
	}
	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, bool mountpointConfirmed = false)
		{
			if (!driveReady)
			{
				return MountDecision.Offline;
			}
			if (markerExists)
			{
				return MountDecision.Live;
			}
			if (requireExistingMarker)
			{
				return MountDecision.Offline;
			}
			if (shareHasData)
			{
				return MountDecision.CreateMarkerThenLive;
			}
			if (alreadyBootstrapped)
			{
				return MountDecision.Offline;
			}
			if (!mountpointConfirmed)
			{
				return MountDecision.Offline;
			}
			return MountDecision.CreateMarkerThenLive;
		}

		public static string? WineUnixPath(string? path)
		{
			if (path == null || path.Length < 2)
			{
				return null;
			}
			if ((path[0] != 'Z' && path[0] != 'z') || path[1] != ':')
			{
				return null;
			}
			if (path.Length == 2)
			{
				return "/";
			}
			if (path[2] != '/' && path[2] != '\\')
			{
				return null;
			}
			return path.Substring(2).Replace('\\', '/');
		}

		public static bool TryCoveringMount(string? fullPath, IEnumerable<string>? procMountsLines, out string mountpoint, out string fsType)
		{
			mountpoint = "";
			fsType = "";
			if (string.IsNullOrEmpty(fullPath) || procMountsLines == null)
			{
				return false;
			}
			string text = fullPath.TrimEnd(new char[1] { '/' });
			if (text.Length == 0)
			{
				text = "/";
			}
			int num = -1;
			foreach (string procMountsLine in procMountsLines)
			{
				if (string.IsNullOrEmpty(procMountsLine))
				{
					continue;
				}
				string[] array = procMountsLine.Split(new char[1] { ' ' });
				if (array.Length >= 3)
				{
					string text2 = array[1].Replace("\\040", " ").Replace("\\011", "\t").TrimEnd(new char[1] { '/' });
					if ((text2.Length == 0 || string.Equals(text, text2, StringComparison.Ordinal) || text.StartsWith(text2 + "/", StringComparison.Ordinal)) && text2.Length >= num)
					{
						num = text2.Length;
						mountpoint = ((text2.Length == 0) ? "/" : text2);
						fsType = array[2];
					}
				}
			}
			return num >= 0;
		}

		public static bool IsRemoteFsType(string? fsType)
		{
			if (string.IsNullOrEmpty(fsType))
			{
				return false;
			}
			string text = fsType.ToLowerInvariant();
			if (text.StartsWith("fuse.", StringComparison.Ordinal))
			{
				text = text.Substring(5);
			}
			if (!text.StartsWith("nfs", StringComparison.Ordinal) && !text.StartsWith("smb", StringComparison.Ordinal) && !text.StartsWith("ceph", StringComparison.Ordinal) && !text.StartsWith("davfs", StringComparison.Ordinal))
			{
				switch (text)
				{
				default:
					return text == "afs";
				case "cifs":
				case "9p":
				case "glusterfs":
				case "sshfs":
				case "rclone":
					break;
				}
			}
			return true;
		}

		public static bool IsUnderNonRootMount(string? fullPath, IEnumerable<string>? procMountsLines)
		{
			if (string.IsNullOrEmpty(fullPath) || procMountsLines == null)
			{
				return false;
			}
			string text = fullPath.TrimEnd(new char[1] { '/' });
			if (text.Length == 0)
			{
				return false;
			}
			foreach (string procMountsLine in procMountsLines)
			{
				if (string.IsNullOrEmpty(procMountsLine))
				{
					continue;
				}
				string[] array = procMountsLine.Split(new char[1] { ' ' });
				if (array.Length >= 2)
				{
					string text2 = array[1].Replace("\\040", " ").Replace("\\011", "\t").TrimEnd(new char[1] { '/' });
					if (text2.Length != 0 && (string.Equals(text, text2, StringComparison.Ordinal) || text.StartsWith(text2 + "/", StringComparison.Ordinal)))
					{
						return true;
					}
				}
			}
			return false;
		}
	}
	public static class OpWatchdog
	{
		public enum Verdict
		{
			Quiet,
			WarnStalled,
			NoteRecovered
		}

		public const double DefaultWarnAfterSeconds = 60.0;

		public static Verdict Check(bool running, bool alreadyWarned, double heldSeconds, double warnAfterSeconds)
		{
			if (running)
			{
				if (alreadyWarned || !(heldSeconds >= warnAfterSeconds))
				{
					return Verdict.Quiet;
				}
				return Verdict.WarnStalled;
			}
			if (!alreadyWarned)
			{
				return Verdict.Quiet;
			}
			return Verdict.NoteRecovered;
		}
	}
	public static class PathRebase
	{
		public const string SaveBase = "SaveGames";

		public const string PayloadBase = ".cloudward-payload";

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

		public static string PayloadRoot(string mountRoot)
		{
			return TrimTrailingSeparators(mountRoot) + "/.cloudward-payload";
		}

		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 StagedRuling
	{
		StillStaged,
		Applied,
		NotApplied,
		Ambiguous
	}
	public sealed class ApplyReceipt
	{
		public const string RelPath = "BepInEx/cloudward-apply-receipt.txt";

		public ApplyOutcome Outcome { get; }

		public int OpsFailed { get; }

		public IReadOnlyDictionary<PayloadTier, int> Generations { get; }

		public string ApplierVersion { get; }

		public string Utc { get; }

		public bool PartiallyApplied
		{
			get
			{
				if (Outcome == ApplyOutcome.Applied)
				{
					return OpsFailed > 0;
				}
				return false;
			}
		}

		public static string PathFor(string gameRoot)
		{
			return Path.Combine(gameRoot ?? string.Empty, PayloadScope.ToNative("BepInEx/cloudward-apply-receipt.txt"));
		}

		public ApplyReceipt(ApplyOutcome outcome, int opsFailed, IReadOnlyDictionary<PayloadTier, int>? generations, string? applierVersion, string? utc = null)
		{
			Outcome = outcome;
			OpsFailed = opsFailed;
			Generations = generations ?? new Dictionary<PayloadTier, int>();
			ApplierVersion = applierVersion ?? string.Empty;
			Utc = utc ?? DateTime.UtcNow.ToString("o");
		}

		public static StagedRuling Judge(ApplyReceipt? receipt, bool stagingStillPresent)
		{
			if (stagingStillPresent)
			{
				return StagedRuling.StillStaged;
			}
			if (receipt == null)
			{
				return StagedRuling.Ambiguous;
			}
			if (receipt.Outcome != ApplyOutcome.Applied || receipt.OpsFailed != 0)
			{
				return StagedRuling.NotApplied;
			}
			return StagedRuling.Applied;
		}

		public static StagedRuling Judge(ApplyReceipt? receipt, bool stagingStillPresent, PayloadTier tier, int stagedGen)
		{
			StagedRuling stagedRuling = Judge(receipt, stagingStillPresent);
			if (stagedRuling == StagedRuling.Applied && receipt.Generations.TryGetValue(tier, out var value) && value != stagedGen)
			{
				return StagedRuling.Ambiguous;
			}
			return stagedRuling;
		}

		public string Serialize()
		{
			List<string> list = new List<string>
			{
				"O|" + Outcome,
				"F|" + OpsFailed.ToString(CultureInfo.InvariantCulture),
				"A|" + LineCodec.Escape(ApplierVersion),
				"U|" + LineCodec.Escape(Utc)
			};
			foreach (KeyValuePair<PayloadTier, int> generation in Generations)
			{
				list.Add("G|" + PayloadTiers.Name(generation.Key) + "|" + generation.Value.ToString(CultureInfo.InvariantCulture));
			}
			return string.Join("\n", list);
		}

		public static ApplyReceipt? Parse(string? text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			ApplyOutcome? applyOutcome = null;
			int result = 0;
			Dictionary<PayloadTier, int> dictionary = new Dictionary<PayloadTier, int>();
			string applierVersion = string.Empty;
			string utc = string.Empty;
			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;
				}
				switch (array2[0])
				{
				case "O":
				{
					if (Enum.TryParse<ApplyOutcome>(array2[1], out var result3))
					{
						applyOutcome = result3;
					}
					break;
				}
				case "F":
					int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
					break;
				case "A":
					applierVersion = LineCodec.Unescape(array2[1]);
					break;
				case "U":
					utc = LineCodec.Unescape(array2[1]);
					break;
				case "G":
				{
					if (array2.Length >= 3 && PayloadTiers.TryParse(array2[1], out var tier) && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						dictionary[tier] = result2;
					}
					break;
				}
				}
			}
			if (applyOutcome.HasValue)
			{
				return new ApplyReceipt(applyOutcome.Value, result, dictionary, applierVersion, utc);
			}
			return null;
		}

		public void WriteTo(string gameRoot)
		{
			try
			{
				AtomicFile.WriteAllText(PathFor(gameRoot), Serialize());
			}
			catch
			{
			}
		}

		public static ApplyReceipt? Consume(string gameRoot)
		{
			string path = PathFor(gameRoot);
			try
			{
				if (!File.Exists(path))
				{
					return null;
				}
				ApplyReceipt result = Parse(File.ReadAllText(path));
				try
				{
					File.Delete(path);
				}
				catch
				{
				}
				return result;
			}
			catch
			{
				return null;
			}
		}
	}
	public static class AtomicFile
	{
		public static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

		public static string ShareTempSuffix(string device, int pid)
		{
			return ".tmp." + SanitizeForFileName(device) + "." + pid.ToString(CultureInfo.InvariantCulture);
		}

		public static void WriteAllText(string path, string content, string? tempSuffix = null)
		{
			WriteAllBytes(path, Utf8NoBom.GetBytes(content ?? string.Empty), tempSuffix);
		}

		public static void WriteAllBytes(string path, byte[] bytes, string? tempSuffix = null)
		{
			string directoryName = Path.GetDirectoryName(path);
			if (!string.IsNullOrEmpty(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			string text = path + (string.IsNullOrEmpty(tempSuffix) ? ".tmp" : tempSuffix);
			try
			{
				File.WriteAllBytes(text, bytes ?? Array.Empty<byte>());
				Swap(text, path);
			}
			catch
			{
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch
				{
				}
				throw;
			}
		}

		public static void Swap(string tmp, string path)
		{
			if (!File.Exists(path))
			{
				File.Move(tmp, path);
				return;
			}
			try
			{
				File.Replace(tmp, path, null);
			}
			catch (PlatformNotSupportedException)
			{
				MoveAsideThenMove(tmp, path);
			}
			catch (IOException)
			{
				MoveAsideThenMove(tmp, path);
			}
		}

		internal static void MoveAsideThenMove(string tmp, string path)
		{
			string text = tmp + ".aside";
			try
			{
				if (File.Exists(text))
				{
					File.Delete(text);
				}
			}
			catch
			{
			}
			File.Move(path, text);
			try
			{
				File.Move(tmp, path);
			}
			catch
			{
				try
				{
					if (!File.Exists(path))
					{
						File.Move(text, path);
					}
				}
				catch
				{
				}
				throw;
			}
			try
			{
				File.Delete(text);
			}
			catch
			{
			}
		}

		public static string SanitizeForFileName(string? value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "unknown";
			}
			StringBuilder stringBuilder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				stringBuilder.Append((char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_');
			}
			string text = stringBuilder.ToString();
			if (text.Length != 0)
			{
				return text;
			}
			return "unknown";
		}
	}
	public static class Fnv1a
	{
		public static string HashLines(IEnumerable<string> lines)
		{
			List<string> list = new List<string>(lines ?? Array.Empty<string>());
			list.Sort(StringComparer.Ordinal);
			uint num = 2166136261u;
			foreach (string item in list)
			{
				if (item != null)
				{
					string text = item;
					foreach (char c in text)
					{
						num ^= c;
						num *= 16777619;
					}
					num ^= 0xA;
					num *= 16777619;
				}
			}
			return num.ToString("x8");
		}
	}
	public static class LineCodec
	{
		public static string Escape(string? s)
		{
			return (s ?? string.Empty).Replace("%", "%25").Replace("|", "%7C");
		}

		public static string Unescape(string? s)
		{
			return (s ?? string.Empty).Replace("%7C", "|").Replace("%25", "%");
		}
	}
	public enum ApplyOutcome
	{
		Nothing,
		Disabled,
		Held,
		Refused,
		Applied
	}
	public sealed class ApplyResult
	{
		public ApplyOutcome Outcome { get; }

		public int FilesCopied { get; }

		public int FilesDeleted { get; }

		public int CfgKeysChanged { get; }

		public IReadOnlyList<string> Messages { get; }

		public int OpsFailed { get; }

		public bool PartiallyApplied
		{
			get
			{
				if (Outcome == ApplyOutcome.Applied)
				{
					return OpsFailed > 0;
				}
				return false;
			}
		}

		public ApplyResult(ApplyOutcome outcome, int copied, int deleted, int cfgChanged, IReadOnlyList<string> messages, int opsFailed = 0)
		{
			Outcome = outcome;
			FilesCopied = copied;
			FilesDeleted = deleted;
			CfgKeysChanged = cfgChanged;
			Messages = messages;
			OpsFailed = opsFailed;
		}

		public static ApplyResult Simple(ApplyOutcome outcome, params string[] messages)
		{
			return new ApplyResult(outcome, 0, 0, 0, messages);
		}
	}
	public static class PayloadApply
	{
		public const string KillSwitchFileName = ".cloudward-disable";

		public const string TypeLoaderCache = "BepInEx/cache";

		public const string HoldFileName = "hold";

		public static bool KillSwitchPresent(string gameRoot)
		{
			return File.Exists(Path.Combine(gameRoot ?? string.Empty, ".cloudward-disable"));
		}

		public static bool HeldForManualApply(string stagingRoot)
		{
			return File.Exists(Path.Combine(stagingRoot ?? string.Empty, "hold"));
		}

		public static ApplyResult Execute(string gameRoot, string stagingRoot, string? backupDir, Action<string>? log = null)
		{
			IReadOnlyDictionary<PayloadTier, int> planGens = null;
			try
			{
				if (KillSwitchPresent(gameRoot))
				{
					return ApplyResult.Simple(ApplyOutcome.Disabled, ".cloudward-disable present — payload apply disabled, staging left in place");
				}
				string path = Path.Combine(stagingRoot, "stage.plan");
				if (!Directory.Exists(stagingRoot) || !File.Exists(path))
				{
					return ApplyResult.Simple(ApplyOutcome.Nothing);
				}
				if (HeldForManualApply(stagingRoot))
				{
					return ApplyResult.Simple(ApplyOutcome.Held, "a payload is staged but held ([Payload] ApplyMode=StageOnly) — run the 'payloadapply' verb, then restart, to install it");
				}
				StagePlan stagePlan;
				try
				{
					stagePlan = StagePlan.Parse(File.ReadAllText(path));
				}
				catch (Exception ex)
				{
					stagePlan = null;
					Say("plan unreadable: " + ex.Message);
				}
				if (stagePlan == null)
				{
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Refused, 0);
					return ApplyResult.Simple(ApplyOutcome.Refused, "staged plan was unreadable or unsupported — discarded, install untouched");
				}
				planGens = stagePlan.Generations;
				if (stagePlan.IsEmpty)
				{
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Nothing, 0);
					return ApplyResult.Simple(ApplyOutcome.Nothing, "staged plan was empty");
				}
				string path2 = Path.Combine(stagingRoot, "files");
				List<string> list = new List<string>();
				foreach (FileOp item in stagePlan.FileOps.Where((FileOp o) => o.Kind == FileOpKind.Copy))
				{
					string path3 = Path.Combine(path2, PayloadScope.ToNative(item.RelPath));
					if (!File.Exists(path3))
					{
						list.Add("missing staged file: " + item.RelPath);
						continue;
					}
					string a;
					try
					{
						a = PayloadHasher.HashFile(path3);
					}
					catch (Exception ex2)
					{
						list.Add("unreadable staged file " + item.RelPath + ": " + ex2.Message);
						continue;
					}
					if (!string.Equals(a, item.Sha256, StringComparison.Ordinal))
					{
						list.Add("sha256 mismatch: " + item.RelPath);
					}
				}
				if (list.Count > 0)
				{
					foreach (string item2 in list.Take(20))
					{
						Say("  " + item2);
					}
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Refused, 0);
					return new ApplyResult(ApplyOutcome.Refused, 0, 0, 0, new string[1] { $"staged payload failed verification ({list.Count} problem(s)) — discarded, install untouched" }.Concat(list.Take(20)).ToList());
				}
				int num = 0;
				int num2 = 0;
				int num3 = 0;
				int num4 = 0;
				List<string> list2 = new List<string>();
				foreach (FileOp fileOp in stagePlan.FileOps)
				{
					string text = Path.Combine(gameRoot, PayloadScope.ToNative(fileOp.RelPath));
					try
					{
						Backup(backupDir, fileOp.RelPath, text);
						if (fileOp.Kind == FileOpKind.Copy)
						{
							string sourceFileName = Path.Combine(path2, PayloadScope.ToNative(fileOp.RelPath));
							string directoryName = Path.GetDirectoryName(text);
							if (!string.IsNullOrEmpty(directoryName))
							{
								Directory.CreateDirectory(directoryName);
							}
							string text2 = text + ".cloudward-new";
							File.Copy(sourceFileName, text2, overwrite: true);
							AtomicFile.Swap(text2, text);
							num++;
						}
						else if (File.Exists(text))
						{
							File.Delete(text);
							num2++;
						}
					}
					catch (Exception ex3)
					{
						num4++;
						list2.Add("failed " + fileOp.Kind.ToString() + " " + fileOp.RelPath + ": " + ex3.Message);
					}
				}
				foreach (IGrouping<string, CfgOp> item3 in stagePlan.CfgOps.GroupBy<CfgOp, string>((CfgOp o) => o.Guid, StringComparer.OrdinalIgnoreCase))
				{
					string text3 = Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/config/" + item3.Key + ".cfg"));
					if (!File.Exists(text3))
					{
						list2.Add("cfg absent, skipped: " + item3.Key);
						continue;
					}
					try
					{
						Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
						foreach (CfgOp item4 in item3)
						{
							dictionary[item4.Key] = item4.Value;
						}
						CfgMergeResult cfgMergeResult = CfgMerge.Apply(File.ReadAllBytes(text3), dictionary);
						if (cfgMergeResult.Skipped.Count > 0)
						{
							list2.Add($"cfg {item3.Key}: {cfgMergeResult.Skipped.Count} key(s) skipped — " + string.Join("; ", cfgMergeResult.Skipped.Take(3)) + ((cfgMergeResult.Skipped.Count > 3) ? "; …" : string.Empty));
						}
						if (!cfgMergeResult.Dirty)
						{
							continue;
						}
						Backup(backupDir, "BepInEx/config/" + item3.Key + ".cfg", text3);
						AtomicFile.WriteAllBytes(text3, cfgMergeResult.After);
						num3 += cfgMergeResult.Changes.Count;
						foreach (string change in cfgMergeResult.Changes)
						{
							list2.Add("cfg " + item3.Key + " " + change);
						}
					}
					catch (Exception ex4)
					{
						num4++;
						list2.Add("failed cfg " + item3.Key + ": " + ex4.Message);
					}
				}
				if (num > 0 || num2 > 0)
				{
					try
					{
						string path4 = Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/cache"));
						if (Directory.Exists(path4))
						{
							Directory.Delete(path4, recursive: true);
							list2.Add("cleared BepInEx/cache");
						}
					}
					catch (Exception ex5)
					{
						list2.Add("could not clear BepInEx/cache: " + ex5.Message);
					}
				}
				Discard(stagingRoot);
				PruneEmptyDirs(Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/plugins")));
				if (num4 > 0)
				{
					list2.Insert(0, $"PARTIALLY APPLIED — {num4} op(s) failed AFTER verification. The install " + "is now a mix of old and new files; re-run the sync (or restore from the backup dir) before trusting this launch.");
				}
				list2.Insert(0, $"applied staged payload: {num} copied, {num2} deleted, {num3} cfg key(s)" + ((num4 > 0) ? $", {num4} FAILED" : ""));
				foreach (string item5 in list2.Take(40))
				{
					Say("  " + item5);
				}
				Receipt(ApplyOutcome.Applied, num4);
				return new ApplyResult(ApplyOutcome.Applied, num, num2, num3, list2, num4);
			}
			catch (Exception ex6)
			{
				try
				{
					Discard(stagingRoot);
				}
				catch
				{
				}
				try
				{
					Receipt(ApplyOutcome.Refused, 0);
				}
				catch
				{
				}
				return ApplyResult.Simple(ApplyOutcome.Refused, "payload apply aborted: " + ex6.Message);
			}
			void Receipt(ApplyOutcome outcome, int opsFailedCount)
			{
				new ApplyReceipt(outcome, opsFailedCount, planGens, ApplierVersion()).WriteTo(gameRoot);
			}
			void Say(string m)
			{
				try
				{
					log?.Invoke(m);
				}
				catch
				{
				}
			}
		}

		public static string ApplierVersion()
		{
			try
			{
				return typeof(PayloadApply).Assembly.GetName().Version?.ToString() ?? "?";
			}
			catch
			{
				return "?";
			}
		}

		private static void Backup(string? backupDir, string relPath, string source)
		{
			if (string.IsNullOrEmpty(backupDir) || !File.Exists(source))
			{
				return;
			}
			try
			{
				string text = Path.Combine(backupDir, PayloadScope.ToNative(relPath));
				string directoryName = Path.GetDirectoryName(text);
				if (!string.IsNullOrEmpty(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				File.Copy(source, text, overwrite: true);
			}
			catch
			{
			}
		}

		private static void Discard(string stagingRoot)
		{
			try
			{
				if (Directory.Exists(stagingRoot))
				{
					Directory.Delete(stagingRoot, recursive: true);
				}
			}
			catch
			{
			}
		}

		private static void PruneEmptyDirs(string root)
		{
			if (!Directory.Exists(root))
			{
				return;
			}
			try
			{
				string[] directories = Directory.GetDirectories(root);
				foreach (string text in directories)
				{
					PruneEmptyDirs(text);
					if (Directory.GetFileSystemEntries(text).Length == 0)
					{
						Directory.Delete(text);
					}
				}
			}
			catch
			{
			}
		}
	}
	public static class PayloadBackupName
	{
		public static string Compose(PayloadTier tier, int gen, string? stamp)
		{
			return PayloadTiers.Name(tier) + "-gen" + gen.ToString(CultureInfo.InvariantCulture) + "-" + AtomicFile.SanitizeForFileName(stamp);
		}

		public static string Prefix(PayloadTier tier)
		{
			return PayloadTiers.Name(tier) + "-gen";
		}

		public static bool BelongsTo(string? name, PayloadTier tier)
		{
			if (!string.IsNullOrEmpty(name))
			{
				return name.StartsWith(Prefix(tier), StringComparison.Ordinal);
			}
			return false;
		}

		public static bool TryParse(string? name, PayloadTier tier, out int gen, out string stamp)
		{
			gen = -1;
			stamp = string.Empty;
			if (!BelongsTo(name, tier))
			{
				return false;
			}
			string text = name.Substring(Prefix(tier).Length);
			int num = text.IndexOf('-');
			if (!int.TryParse((num < 0) ? text : text.Substring(0, num), NumberStyles.None, CultureInfo.InvariantCulture, out var result))
			{
				return false;
			}
			gen = result;
			stamp = ((num < 0) ? string.Empty : text.Substring(num + 1));
			return true;
		}

		public static List<string> OrderOldestFirst(IEnumerable<string>? names, PayloadTier tier)
		{
			List<string> list = new List<string>(names ?? Array.Empty<string>());
			list.Sort((string a, string b) => Compare(a, b, tier));
			return list;
		}

		public static List<string> OrderNewestFirst(IEnumerable<string>? names, PayloadTier tier)
		{
			List<string> list = OrderOldestFirst(names, tier);
			list.Reverse();
			return list;
		}

		private static int Compare(string a, string b, PayloadTier tier)
		{
			TryParse(a, tier, out int gen, out string stamp);
			TryParse(b, tier, out int gen2, out string stamp2);
			if (gen != gen2)
			{
				return gen.CompareTo(gen2);
			}
			int num = string.CompareOrdinal(stamp, stamp2);
			if (num == 0)
			{
				return string.CompareOrdinal(a ?? string.Empty, b ?? string.Empty);
			}
			return num;
		}
	}
	public enum PayloadConflictPolicy
	{
		Skip,
		PreferLocal,
		PreferShare
	}
	public enum PayloadResolution
	{
		Local,
		Share
	}
	public static class PayloadConflictResolve
	{
		public static bool IsAuto(PayloadConflictPolicy policy)
		{
			return policy != PayloadConflictPolicy.Skip;
		}

		public static PayloadResolution Winner(PayloadConflictPolicy policy)
		{
			if (policy != PayloadConflictPolicy.PreferLocal)
			{
				return PayloadResolution.Share;
			}
			return PayloadResolution.Local;
		}

		public static bool Gate(PayloadConflictPolicy policy, bool atBoot, bool alreadyPending)
		{
			if (IsAuto(policy) && atBoot)
			{
				return !alreadyPending;
			}
			return false;
		}

		public static bool TryParseResolution(string? text, out PayloadResolution resolution)
		{
			resolution = PayloadResolution.Local;
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			string a = text.Trim();
			if (string.Equals(a, "local", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "this", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "mine", StringComparison.OrdinalIgnoreCase))
			{
				resolution = PayloadResolution.Local;
				return true;
			}
			if (string.Equals(a, "share", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "remote", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "other", StringComparison.OrdinalIgnoreCase))
			{
				resolution = PayloadResolution.Share;
				return true;
			}
			return false;
		}
	}
	public sealed class PayloadDiff
	{
		public IReadOnlyList<PayloadEntry> Added { get; }

		public IReadOnlyList<PayloadEntry> Changed { get; }

		public IReadOnlyList<string> Removed { get; }

		public bool IsEmpty
		{
			get
			{
				if (Added.Count == 0 && Changed.Count == 0)
				{
					return Removed.Count == 0;
				}
				return false;
			}
		}

		public int TouchedCount => Added.Count + Changed.Count + Removed.Count;

		public long TransferBytes => Added.Sum((PayloadEntry e) => e.Size) + Changed.Sum((PayloadEntry e) => e.Size);

		private PayloadDiff(IReadOnlyList<PayloadEntry> added, IReadOnlyList<PayloadEntry> changed, IReadOnlyList<string> removed)
		{
			Added = added;
			Changed = changed;
			Removed = removed;
		}

		public static PayloadDiff Between(PayloadManifest? from, PayloadManifest? to)
		{
			Dictionary<string, PayloadEntry> dictionary = (from?.Entries ?? Array.Empty<PayloadEntry>()).ToDictionary<PayloadEntry, string, PayloadEntry>((PayloadEntry e) => e.RelPath, (PayloadEntry e) => e, StringComparer.Ordinal);
			Dictionary<string, PayloadEntry> want = (to?.Entries ?? Array.Empty<PayloadEntry>()).ToDictionary<PayloadEntry, string, PayloadEntry>((PayloadEntry e) => e.RelPath, (PayloadEntry e) => e, StringComparer.Ordinal);
			List<PayloadEntry> list = new List<PayloadEntry>();
			List<PayloadEntry> list2 = new List<PayloadEntry>();
			foreach (PayloadEntry item in want.Values.OrderBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal))
			{
				if (!dictionary.TryGetValue(item.RelPath, out var value))
				{
					list.Add(item);
				}
				else if (!string.Equals(value.Sha256, item.Sha256, StringComparison.Ordinal))
				{
					list2.Add(item);
				}
			}
			List<string> removed = dictionary.Keys.Where((string k) => !want.ContainsKey(k)).OrderBy<string, string>((string k) => k, StringComparer.Ordinal).ToList();
			return new PayloadDiff(list, list2, removed);
		}

		public IEnumerable<string> Describe()
		{
			return Added.Select((PayloadEntry e) => "  + " + e.RelPath).Concat(Changed.Select((PayloadEntry e) => "  ~ " + e.RelPath)).Concat(Removed.Select((string p) => "  - " + p));
		}
	}
	public static class PayloadGuard
	{
		public const int AbsoluteFloor = 5;

		public const int DefaultMaxDeletePercent = 25;

		public static bool IsMassDelete(int previousCount, int deleteCount, int maxDeletePercent, out string why)
		{
			why = string.Empty;
			int num = Math.Max(0, Math.Min(100, maxDeletePercent));
			if (num >= 100)
			{
				return false;
			}
			if (deleteCount <= 5)
			{
				return false;
			}
			if (previousCount <= 0)
			{
				return false;
			}
			if ((long)deleteCount * 100L <= (long)previousCount * (long)num)
			{
				return false;
			}
			why = $"the plan deletes {deleteCount} of {previousCount} file(s) " + $"(> {num}% and > {5} files). This is far more likely a broken scan than a real " + "uninstall. If it IS deliberate, set [Payload] MaxDeletePercent=100 for one pass.";
			return true;
		}
	}
	public sealed class ScanResult
	{
		public PayloadManifest Manifest { get; }

		public bool TierCacheHit { get; }

		public int FilesHashed { get; }

		public int FilesReused { get; }

		public long BytesHashed { get; }

		public IReadOnlyList<string> Unreadable { get; }

		public bool IsComplete => Unreadable.Count == 0;

		public string Summary => (TierCacheHit ? "prefilter hit (0 files hashed)" : $"{FilesHashed} hashed / {FilesReused} reused ({BytesHashed / 1024} KiB read)") + (IsComplete ? string.Empty : $", {Unreadable.Count} UNREADABLE");

		public ScanResult(PayloadManifest manifest, bool tierCacheHit, int hashed, int reused, long bytesHashed, IReadOnlyList<string>? unreadable = null)
		{
			Manifest = manifest;
			TierCacheHit = tierCacheHit;
			FilesHashed = hashed;
			FilesReused = reused;
			BytesHashed = bytesHashed;
			Unreadable = unreadable ?? Array.Empty<string>();
		}
	}
	public static class PayloadHasher
	{
		public static string HashFile(string path)
		{
			using SHA256 sHA = SHA256.Create();
			using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 65536);
			return ToHex(sHA.ComputeHash(inputStream));
		}

		public static string HashBytes(byte[] bytes)
		{
			using SHA256 sHA = SHA256.Create();
			return ToHex(sHA.ComputeHash(bytes ?? Array.Empty<byte>()));
		}

		public static IReadOnlyList<FileStat> Walk(string root, PayloadScope scope, ICollection<string>? unreadable = null)
		{
			List<FileStat> list = new List<FileStat>();
			if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
			{
				return list;
			}
			string fullPath = Path.GetFullPath(root);
			foreach (string item in SafeEnumerate(fullPath, fullPath, scope, unreadable))
			{
				string text;
				try
				{
					text = PayloadScope.Normalize(Path.GetFullPath(item).Substring(fullPath.Length));
				}
				catch
				{
					unreadable?.Add(item);
					continue;
				}
				if (text.Length == 0 || scope.IsExcluded(text))
				{
					continue;
				}
				try
				{
					FileInfo fileInfo = new FileInfo(item);
					if (fileInfo.Exists)
					{
						list.Add(new FileStat(text, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks));
					}
				}
				catch
				{
					unreadable?.Add(text);
				}
			}
			return list.OrderBy<FileStat, string>((FileStat s) => s.RelPath, StringComparer.Ordinal).ToList();
		}

		public static ScanResult Scan(PayloadTier tier, string root, IReadOnlyList<FileStat> stats, PayloadScanCache cache, IReadOnlyDictionary<string, string>? modVersions = null, bool force = false, IReadOnlyList<string>? walkUnreadable = null)
		{
			string statSignature = PayloadScanCache.StatSignature(stats);
			bool flag = walkUnreadable != null && walkUnreadable.Count > 0;
			if (!force && !flag && cache.TryReuseTier(tier, statSignature, out string fingerprint))
			{
				List<PayloadEntry> list = new List<PayloadEntry>(stats.Count);
				bool flag2 = true;
				foreach (FileStat stat in stats)
				{
					if (!cache.TryReuse(tier, stat, out string sha))
					{
						flag2 = false;
						break;
					}
					list.Add(new PayloadEntry(stat.RelPath, stat.Size, sha));
				}
				if (flag2)
				{
					PayloadManifest payloadManifest = new PayloadManifest(tier, list, modVersions);
					if (string.Equals(payloadManifest.Fingerprint, fingerprint, StringComparison.Ordinal))
					{
						return new ScanResult(payloadManifest, tierCacheHit: true, 0, list.Count, 0L);
					}
				}
			}
			List<PayloadEntry> list2 = new List<PayloadEntry>(stats.Count);
			List<string> list3 = null;
			int num = 0;
			int num2 = 0;
			long num3 = 0L;
			foreach (FileStat stat2 in stats)
			{
				string sha3;
				if (!force && cache.TryReuse(tier, stat2, out string sha2))
				{
					sha3 = sha2;
					num2++;
				}
				else
				{
					try
					{
						sha3 = HashFile(Path.Combine(root, stat2.RelPath.Replace('/', Path.DirectorySeparatorChar)));
					}
					catch
					{
						(list3 ?? (list3 = new List<string>())).Add(stat2.RelPath);
						continue;
					}
					num++;
					num3 += stat2.Size;
					cache.Record(tier, stat2, sha3);
				}
				list2.Add(new PayloadEntry(stat2.RelPath, stat2.Size, sha3));
			}
			if (flag)
			{
				(list3 ?? (list3 = new List<string>())).AddRange(walkUnreadable);
			}
			PayloadManifest payloadManifest2 = new PayloadManifest(tier, list2, modVersions);
			if (list3 == null)
			{
				cache.RecordTier(tier, statSignature, payloadManifest2.Fingerprint, stats);
			}
			return new ScanResult(payloadManifest2, tierCacheHit: false, num, num2, num3, list3);
		}

		private static IEnumerable<string> SafeEnumerate(string root, string rootFull, PayloadScope? scope, ICollection<string>? unreadable)
		{
			Stack<string> pending = new Stack<string>();
			pending.Push(root);
			while (pending.Count > 0)
			{
				string text = pending.Pop();
				string[] files;
				string[] dirs;
				try
				{
					files = Directory.GetFiles(text);
					dirs = Directory.GetDirectories(text);
				}
				catch
				{
					if (unreadable != null)
					{
						string item;
						try
						{
							item = PayloadScope.Normalize(Path.GetFullPath(text).Substring(rootFull.Length)) + "/**";
						}
						catch
						{
							item = text + "/**";
						}
						unreadable.Add(item);
					}
					continue;
				}
				string[] array = files;
				for (int i = 0; i < array.Length; i++)
				{
					yield return array[i];
				}
				string[] array2 = dirs;
				foreach (string text2 in array2)
				{
					if (scope == null || !IsExcludedDir(text2, rootFull, scope))
					{
						pending.Push(text2);
					}
				}
			}
		}

		private static bool IsExcludedDir(string dir, string rootFull, PayloadScope scope)
		{
			try
			{
				string text = PayloadScope.Normalize(Path.GetFullPath(dir).Substring(rootFull.Length));
				return text.Length > 0 && scope.IsExcluded(text);
			}
			catch
			{
				return false;
			}
		}

		private static string ToHex(byte[] hash)
		{
			char[] array = new char[hash.Length * 2];
			for (int i = 0; i < hash.Length; i++)
			{
				array[i * 2] = "0123456789abcdef"[hash[i] >> 4];
				array[i * 2 + 1] = "0123456789abcdef"[hash[i] & 0xF];
			}
			return new string(array);
		}
	}
	public sealed class PayloadLedgerEntry
	{
		public int Gen { get; }

		public string Fingerprint { get; }

		public string Device { get; }

		public string PublishedUtc { get; }

		public PayloadLedgerEntry(int gen, string fingerprint, string device, string publishedUtc)
		{
			Gen = gen;
			Fingerprint = fingerprint ?? string.Empty;
			Device = device ?? string.Empty;
			PublishedUtc = publishedUtc ?? string.Empty;
		}
	}
	public sealed class PayloadLedger
	{
		private readonly Dictionary<PayloadTier, PayloadLedgerEntry> _entries = new Dictionary<PayloadTier, PayloadLedgerEntry>();

		public PayloadLedgerEntry? Get(PayloadTier tier)
		{
			if (!_entries.TryGetValue(tier, out PayloadLedgerEntry value))
			{
				return null;
			}
			return value;
		}

		public void Set(PayloadTier tier, PayloadLedgerEntry entry)
		{
			_entries[tier] = entry;
		}

		public string Serialize()
		{
			return string.Join("\n", PayloadTiers.All.Where((PayloadTier t) => _entries.ContainsKey(t)).Select(delegate(PayloadTier t)
			{
				PayloadLedgerEntry payloadLedgerEntry = _entries[t];
				return PayloadTiers.Name(t) + "|" + payloadLedgerEntry.Gen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(payloadLedgerEntry.Fingerprint) + "|" + Escape(payloadLedgerEntry.Device) + "|" + Escape(payloadLedgerEntry.PublishedUtc);
			}));
		}

		public static PayloadLedger Parse(string? text)
		{
			PayloadLedger payloadLedger = new PayloadLedger();
			if (string.IsNullOrEmpty(text))
			{
				return payloadLedger;
			}
			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 && PayloadTiers.TryParse(array2[0], out var tier) && int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						payloadLedger._entries[tier] = new PayloadLedgerEntry(result, Unescape(array2[2]), Unescape(array2[3]), (array2.Length >= 5) ? Unescape(array2[4]) : string.Empty);
					}
				}
			}
			return payloadLedger;
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public sealed class PayloadEntry
	{
		public string RelPath { get; }

		public long Size { get; }

		public string Sha256 { get; }

		public PayloadEntry(string relPath, long size, string sha256)
		{
			RelPath = PayloadScope.Normalize(relPath);
			Size = size;
			Sha256 = (sha256 ?? string.Empty).ToLowerInvariant();
		}
	}
	public sealed class PayloadManifest
	{
		public const int FormatVersion = 1;

		public PayloadTier Tier { get; }

		public IReadOnlyList<PayloadEntry> Entries { get; }

		public IReadOnlyDictionary<string, string> ModVersions { get; }

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

		public long TotalBytes => Entries.Sum((PayloadEntry e) => e.Size);

		public string Fingerprint => Fnv1a.HashLines(Entries.Select((PayloadEntry e) => e.RelPath + "|" + e.Sha256));

		public PayloadManifest(PayloadTier tier, IEnumerable<PayloadEntry> entries, IReadOnlyDictionary<string, string>? modVersions = null)
		{
			Tier = tier;
			Entries = (from g in (entries ?? Array.Empty<PayloadEntry>()).Where((PayloadEntry e) => e != null && e.RelPath.Length > 0).GroupBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal)
				select g.First()).OrderBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal).ToList();
			ModVersions = modVersions ?? new Dictionary<string, string>(StringComparer.Ordinal);
		}

		public static PayloadManifest Empty(PayloadTier tier)
		{
			return new PayloadManifest(tier, Array.Empty<PayloadEntry>());
		}

		public PayloadEntry? Get(string relPath)
		{
			string norm = PayloadScope.Normalize(relPath);
			return Entries.FirstOrDefault((PayloadEntry e) => string.Equals(e.RelPath, norm, StringComparison.Ordinal));
		}

		public string Serialize()
		{
			List<string> list = new List<string> { "V|" + 1.ToString(CultureInfo.InvariantCulture) + "|" + PayloadTiers.Name(Tier) };
			foreach (KeyValuePair<string, string> item in ModVersions.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> k) => k.Key, StringComparer.Ordinal))
			{
				list.Add("M|" + Escape(item.Key) + "|" + Escape(item.Value));
			}
			foreach (PayloadEntry entry in Entries)
			{
				list.Add("F|" + Escape(entry.RelPath) + "|" + entry.Size.ToString(CultureInfo.InvariantCulture) + "|" + entry.Sha256);
			}
			return string.Join("\n", list);
		}

		public static PayloadManifest? Parse(string? text, PayloadTier fallbackTier)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			PayloadTier tier = fallbackTier;
			List<PayloadEntry> list = new List<PayloadEntry>();
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			bool flag = false;
			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 || text2[0] == '#')
				{
					continue;
				}
				string[] array2 = text2.Split(new char[1] { '|' });
				if (array2.Length < 2)
				{
					continue;
				}
				switch (array2[0])
				{
				case "V":
				{
					if (!int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						return null;
					}
					if (result2 > 1)
					{
						return null;
					}
					if (array2.Length >= 3 && PayloadTiers.TryParse(array2[2], out var tier2))
					{
						tier = tier2;
					}
					flag = true;
					break;
				}
				case "M":
					if (array2.Length >= 3)
					{
						dictionary[Unescape(array2[1])] = Unescape(array2[2]);
					}
					break;
				case "F":
				{
					if (array2.Length >= 4 && long.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						list.Add(new PayloadEntry(Unescape(array2[1]), result, array2[3]));
					}
					break;
				}
				}
			}
			if (!flag)
			{
				return null;
			}
			return new PayloadManifest(tier, list, dictionary);
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public enum PayloadAction
	{
		UpToDate,
		Pull,
		Push,
		Conflict,
		ShareEmpty
	}
	public sealed class PayloadPlan
	{
		public PayloadTier Tier { get; }

		public PayloadAction Action { get; }

		public int NewGen { get; }

		public string LocalFingerprint { get; }

		public string ShareFingerprint { get; }

		public string Reason { get; }

		public PayloadPlan(PayloadTier tier, PayloadAction action, int newGen, string localFingerprint, string shareFingerprint, string reason)
		{
			Tier = tier;
			Action = action;
			NewGen = newGen;
			LocalFingerprint = localFingerprint ?? string.Empty;
			ShareFingerprint = shareFingerprint ?? string.Empty;
			Reason = reason ?? string.Empty;
		}
	}
	public static class PayloadReconciler
	{
		public static PayloadPlan Plan(PayloadTier tier, string? localFingerprint, bool localScanned, PayloadManifest? shareManifest, PayloadLedgerEntry? ledger, PayloadTierState? state)
		{
			string text = localFingerprint ?? string.Empty;
			string text2 = shareManifest?.Fingerprint ?? string.Empty;
			int num = ledger?.Gen ?? 0;
			int num2 = state?.BasedOnGen ?? 0;
			if (shareManifest == null || shareManifest.IsEmpty || num <= 0 || text2.Length <= 0)
			{
				if (!localScanned || text.Length == 0)
				{
					return Make(tier, PayloadAction.ShareEmpty, num, text, text2, "share has no published payload and this device has nothing scanned");
				}
				return Make(tier, PayloadAction.Push, Math.Max(num, num2) + 1, text, text2, "first publish — share has no payload for this tier");
			}
			if (!localScanned)
			{
				return Make(tier, PayloadAction.ShareEmpty, num, text, text2, "local tier could not be scanned — refusing to act");
			}
			if (string.Equals(text, text2, StringComparison.Ordinal))
			{
				return Make(tier, PayloadAction.UpToDate, Math.Max(num, num2), text, text2, "fingerprints match");
			}
			if (state == null)
			{
				return Make(tier, PayloadAction.Conflict, num, text, text2, "no lineage record and both sides hold a payload");
			}
			bool flag = !string.Equals(text, state.LastPushedFingerprint, StringComparison.Ordinal);
			bool flag2 = num > num2;
			if (flag && !flag2)
			{
				return Make(tier, PayloadAction.Push, num2 + 1, text, text2, "this device changed since its last publish");
			}
			if (!flag && flag2)
			{
				return Make(tier, PayloadAction.Pull, num, text, text2, "share advanced to gen " + num + " and this device is unchanged");
			}
			if (!flag && !flag2)
			{
				if (num < num2)
				{
					return Make(tier, PayloadAction.Conflict, num, text, text2, "ledger generation went backwards (share gen " + num + " < basedOn " + num2 + ")");
				}
				if (tier == PayloadTier.Config)
				{
					return Make(tier, PayloadAction.UpToDate, Math.Max(num, num2), text, text2, "converged — this device reproduces its last sync and the share is unchanged");
				}
				return Make(tier, PayloadAction.Conflict, num, text, text2, "share fingerprint changed without a generation bump");
			}
			return Make(tier, PayloadAction.Conflict, num, text, text2, "both this device and the share changed");
		}

		private static PayloadPlan Make(PayloadTier tier, PayloadAction action, int gen, string localFp, string shareFp, string reason)
		{
			return new PayloadPlan(tier, action, gen, localFp, shareFp, reason);
		}
	}
	public sealed class FileStat
	{
		public string RelPath { get; }

		public long Size { get; }

		public long MtimeTicks { get; }

		public FileStat(string relPath, long size, long mtimeTicks)
		{
			RelPath = PayloadScope.Normalize(relPath);
			Size = size;
			MtimeTicks = mtimeTicks;
		}
	}
	public sealed class PayloadScanCache
	{
		private sealed class Row
		{
			public long Size;

			public long MtimeTicks;

			public string Sha256 = string.Empty;
		}

		private readonly object _gate = new object();

		private readonly Dictionary<string, Row> _rows = new Dictionary<string, Row>(StringComparer.Ordinal);

		private readonly Dictionary<PayloadTier, string> _statSig = new Dictionary<PayloadTier, string>();

		private readonly Dictionary<PayloadTier, string> _fingerprint = new Dictionary<PayloadTier, string>();

		public static string StatSignature(IEnumerable<FileStat> stats)
		{
			return Fnv1a.HashLines((stats ?? Array.Empty<FileStat>()).Select((FileStat s) => s.RelPath + "|" + s.Size.ToString(CultureInfo.InvariantCulture) + "|" + s.MtimeTicks.ToString(CultureInfo.InvariantCulture)));
		}

		public bool TryReuseTier(PayloadTier tier, string statSignature, out string fingerprint)
		{
			lock (_gate)
			{
				fingerprint = string.Empty;
				if (!_statSig.TryGetValue(tier, out string value) || !string.Equals(value, statSignature, StringComparison.Ordinal))
				{
					return false;
				}
				if (!_fingerprint.TryGetValue(tier, out string value2) || value2.Length == 0)
				{
					return false;
				}
				fingerprint = value2;
				return true;
			}
		}

		public bool TryReuse(PayloadTier tier, FileStat stat, out string sha256)
		{
			lock (_gate)
			{
				sha256 = string.Empty;
				if (!_rows.TryGetValue(Key(tier, stat.RelPath), out Row value))
				{
					return false;
				}
				if (value.Size != stat.Size || value.MtimeTicks != stat.MtimeTicks)
				{
					return false;
				}
				if (value.Sha256.Length == 0)
				{
					return false;
				}
				sha256 = value.Sha256;
				return true;
			}
		}

		public void Record(PayloadTier tier, FileStat stat, string sha256)
		{
			lock (_gate)
			{
				_rows[Key(tier, stat.RelPath)] = new Row
				{
					Size = stat.Size,
					MtimeTicks = stat.MtimeTicks,
					Sha256 = (sha256 ?? string.Empty).ToLowerInvariant()
				};
			}
		}

		public void RecordTier(PayloadTier tier, string statSignature, string fingerprint, IEnumerable<FileStat> present)
		{
			lock (_gate)
			{
				_statSig[tier] = statSignature ?? string.Empty;
				_fingerprint[tier] = fingerprint ?? string.Empty;
				HashSet<string> keep = new HashSet<string>((present ?? Array.Empty<FileStat>()).Select((FileStat s) => Key(tier, s.RelPath)), StringComparer.Ordinal);
				string prefix = PayloadTiers.Name(tier) + "|";
				foreach (string item in _rows.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.Ordinal) && !keep.Contains(k)).ToList())
				{
					_rows.Remove(item);
				}
			}
		}

		public void Invalidate(PayloadTier tier)
		{
			lock (_gate)
			{
				_statSig.Remove(tier);
				_fingerprint.Remove(tier);
				string prefix = PayloadTiers.Name(tier) + "|";
				foreach (string item in _rows.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.Ordinal)).ToList())
				{
					_rows.Remove(item);
				}
			}
		}

		public string Serialize()
		{
			lock (_gate)
			{
				List<string> list = new List<string>();
				PayloadTier[] all = PayloadTiers.All;
				foreach (PayloadTier payloadTier in all)
				{
					if (_statSig.TryGetValue(payloadTier, out string value))
					{
						_fingerprint.TryGetValue(payloadTier, out string value2);
						list.Add("T|" + PayloadTiers.Name(payloadTier) + "|" + value + "|" + (value2 ?? string.Empty));
					}
				}
				foreach (KeyValuePair<string, Row> item in _rows.OrderBy<KeyValuePair<string, Row>, string>((KeyValuePair<string, Row> k) => k.Key, StringComparer.Ordinal))
				{
					int num = item.Key.IndexOf('|');
					string text = item.Key.Substring(0, num);
					string s = item.Key.Substring(num + 1);
					list.Add("R|" + text + "|" + Escape(s) + "|" + item.Value.Size.ToString(CultureInfo.InvariantCulture) + "|" + item.Value.MtimeTicks.ToString(CultureInfo.InvariantCulture) + "|" + item.Value.Sha256);
				}
				return string.Join("\n", list);
			}
		}

		public static PayloadScanCache Parse(string? text)
		{
			PayloadScanCache payloadScanCache = new PayloadScanCache();
			if (string.IsNullOrEmpty(text))
			{
				return payloadScanCache;
			}
			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 >= 3 && PayloadTiers.TryParse(array2[1], out var tier))
				{
					long result;
					long result2;
					if (array2[0] == "T")
					{
						payloadScanCache._statSig[tier] = array2[2];
						payloadScanCache._fingerprint[tier] = ((array2.Length >= 4) ? array2[3] : string.Empty);
					}
					else if (array2[0] == "R" && array2.Length >= 6 && long.TryParse(array2[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out result) && long.TryParse(array2[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
					{
						payloadScanCache._rows[Key(tier, Unescape(array2[2]))] = new Row
						{
							Size = result,
							MtimeTicks = result2,
							Sha256 = array2[5]
						};
					}
				}
			}
			return payloadScanCache;
		}

		private static string Key(PayloadTier tier, string relPath)
		{
			return PayloadTiers.Name(tier) + "|" + relPath;
		}

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

		private static string Unescape(string s)
		{
			return s.Replace("%7C", "|");
		}
	}
	public sealed class PayloadScope
	{
		public static readonly string[] ExcludedNames = new string[20]
		{
			"*.cfg", "*.log", "*_cmd.txt", "bw_pets_*", "bw_registries", "ck_expeditions*", "bw_expeditions*", "*.bak*", "SaveGames*", "OptionSettings*",
			"*Keymappings*", "cloudward_state.txt", "cloudward_payload_state.txt", "cloudward_payload_cache.txt", ".cloudward-*", "cloudward-staged", ".owner.lock", ".payload-ledger", ".sync-backups", ".cloudward-disable"
		};

		public static readonly string[] ExcludedPaths = new string[4] { "BepInEx/config", "BepInEx/cache", "BepInEx/cloudward-staged", "SaveGames" };

		private readonly string[] _extra;

		public IReadOnlyList<string> ActivePatterns => ExcludedNames.Concat(_extra).ToList();

		public PayloadScope(IEnumerable<string>? extra = null)
		{
			_extra = (from p in extra ?? Array.Empty<string>()
				select (p ?? string.Empty).Trim() into p
				where p.Length > 0
				select p).ToArray();
		}

		public static PayloadScope FromConfig(string? extraExcludes)
		{
			return new PayloadScope((extraExcludes ?? string.Empty).Split(new char[1] { ';' }));
		}

		public bool IsExcluded(string? relPath)
		{
			string text = Normalize(relPath);
			if (text.Length == 0)
			{
				return true;
			}
			string[] excludedPaths = ExcludedPaths;
			foreach (string text2 in excludedPaths)
			{
				if (text.Equals(text2, StringComparison.OrdinalIgnoreCase) || text.StartsWith(text2 + "/", StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			excludedPaths = text.Split(new char[1] { '/' });
			foreach (string text3 in excludedPaths)
			{
				if (text3.Length == 0)
				{
					continue;
				}
				string[] excludedNames = ExcludedNames;
				for (int j = 0; j < excludedNames.Length; j++)
				{
					if (GlobMatch(excludedNames[j], text3))
					{
						return true;
					}
				}
				excludedNames = _extra;
				for (int j = 0; j < excludedNames.Length; j++)
				{
					if (GlobMatch(excludedNames[j], text3))
					{
						return true;
					}
				}
			}
			return false;
		}

		public IReadOnlyList<string> Filter(IEnumerable<string> relPaths)
		{
			return (from p in (relPaths ?? Array.Empty<string>()).Select(Normalize)
				where p.Length > 0 && !IsExcluded(p)
				select p).OrderBy<string, string>((string p) => p, StringComparer.Ordinal).ToList();
		}

		public static string Normalize(string? relPath)
		{
			return (relPath ?? string.Empty).Replace('\\', '/').Trim(new char[1] { '/' });
		}

		public static string ToNative(string? relPath)
		{
			return (relPath ?? string.Empty).Replace('/', Path.DirectorySeparatorChar);
		}

		public static bool GlobMatch(string pattern, string text)
		{
			int i = 0;
			int num = 0;
			int num2 = -1;
			int num3 = 0;
			while (num < text.Length)
			{
				if (i < pattern.Length && (pattern[i] == '?' || Same(pattern[i], text[num])))
				{
					i++;
					num++;
					continue;
				}
				if (i < pattern.Length && pattern[i] == '*')
				{
					num2 = i++;
					num3 = num;
					continue;
				}
				if (num2 >= 0)
				{
					i = num2 + 1;
					num = ++num3;
					continue;
				}
				return false;
			}
			for (; i < pattern.Length && pattern[i] == '*'; i++)
			{
			}
			return i == pattern.Length;
		}

		private static bool Same(char a, char b)
		{
			return char.ToUpperInvariant(a) == char.ToUpperInvariant(b);
		}
	}
	public enum FileOpKind
	{
		Copy,
		Delete
	}
	public sealed class FileOp
	{
		public FileOpKind Kind { get; }

		public string RelPath { get; }

		public string Sha256 { get; }

		public FileOp(FileOpKind kind, string relPath, string sha256)
		{
			Kind = kind;
			RelPath = PayloadScope.Normalize(relPath);
			Sha256 = (sha256 ?? string.Empty).ToLowerInvariant();
		}
	}
	public sealed class CfgOp
	{
		public string Guid { get; }

		public CfgKey Key { get; }

		public string Value { get; }

		public string CfgRelPath => "BepInEx/config/" + Guid + ".cfg";

		public CfgOp(string guid, CfgKey key, string value)
		{
			Guid = guid ?? string.Empty;
			Key = key;
			Value = value ?? string.Empty;
		}
	}
	public sealed class StagePlan
	{
		public const int FormatVersion = 1;

		public const string FilesDir = "files";

		public const string PlanFileName = "stage.plan";

		public const string PluginsRoot = "BepInEx/plugins";

		public const string ConfigRoot = "BepInEx/config";

		public IReadOnlyList<PayloadTier> Tiers { get; }

		public IReadOnlyList<FileOp> FileOps { get; }

		public IReadOnlyList<CfgOp> CfgOps { get; }

		public IReadOnlyDictionary<PayloadTier, int> Generations { get; }

		public bool IsEmpty
		{
			get
			{
				if (FileOps.Count == 0)
				{
					return CfgOps.Count == 0;
				}
				return false;
			}
		}

		public StagePlan(IEnumerable<PayloadTier> tiers, IEnumerable<FileOp> fileOps, IEnumerable<CfgOp> cfgOps, IReadOnlyDictionary<PayloadTier, int>? generations = null)
		{
			Tiers = (from t in (tiers ?? Array.Empty<PayloadTier>()).Distinct()
				orderby (int)t
				select t).ToList();
			FileOps = (fileOps ?? Array.Empty<FileOp>()).OrderBy<FileOp, string>((FileOp o) => o.RelPath, StringComparer.Ordinal).ToList();
			CfgOps = (cfgOps ?? Array.Empty<CfgOp>()).OrderBy<CfgOp, string>((CfgOp o) => o.Guid, StringComparer.Ordinal).ThenBy((CfgOp o) => o.Key).ToList();
			Generations = generations ?? new Dictionary<PayloadTier, int>();
			Validate();
		}

		private void Validate()
		{
			PayloadScope payloadScope = new PayloadScope();
			foreach (FileOp fileOp in FileOps)
			{
				if (fileOp.RelPath.Length == 0)
				{
					throw new InvalidDataException("stage op with an empty path");
				}
				if (fileOp.RelPath.Contains(".."))
				{
					throw new InvalidDataException("stage op escapes the tree: " + fileOp.RelPath);
				}
				if (!fileOp.RelPath.StartsWith("BepInEx/plugins/", StringComparison.OrdinalIgnoreCase))
				{
					throw new InvalidDataException("stage op outside BepInEx/plugins/: " + fileOp.RelPath);
				}
				string relPath = fileOp.RelPath.Substring("BepInEx/plugins".Length + 1);
				if (payloadScope.IsExcluded(relPath))
				{
					throw new InvalidDataException("stage op names an excluded path: " + fileOp.RelPath);
				}
				if (fileOp.Kind == FileOpKind.Copy && fileOp.Sha256.Length != 64)
				{
					throw new InvalidDataException("copy op without a usable sha256: " + fileOp.RelPath);
				}
			}
			foreach (CfgOp cfgOp in CfgOps)
			{
				if (cfgOp.Guid.Length == 0 || cfgOp.Guid.IndexOfAny(new char[3] { '/', '\\', '.' }) == 0)
				{
					throw new InvalidDataException("cfg op with a bad guid: \"" + cfgOp.Guid + "\"");
				}
				if (cfgOp.Guid.Contains("..") || cfgOp.Guid.IndexOf('/') >= 0 || cfgOp.Guid.IndexOf('\\') >= 0)
				{
					throw new InvalidDataException("cfg op escapes the config dir: \"" + cfgOp.Guid + "\"");
				}
			}
		}

		public string Serialize()
		{
			List<string> list = new List<string> { "V|" + 1.ToString(CultureInfo.InvariantCulture) };
			foreach (PayloadTier tier in Tiers)
			{
				Generations.TryGetValue(tier, out var value);
				list.Add("T|" + PayloadTiers.Name(tier) + "|" + value.ToString(CultureInfo.InvariantCulture));
			}
			foreach (FileOp fileOp in FileOps)
			{
				list.Add(((fileOp.Kind == FileOpKind.Copy) ? "C|" : "D|") + Escape(fileOp.RelPath) + ((fileOp.Kind == FileOpKind.Copy) ? ("|" + fileOp.Sha256) : string.Empty));
			}
			foreach (CfgOp cfgOp in CfgOps)
			{
				list.Add("K|" + Escape(cfgOp.Guid) + "|" + Escape(cfgOp.Key.Section) + "|" + Escape(cfgOp.Key.Key) + "|" + Escape(cfgOp.Value));
			}
			return string.Join("\n", list);
		}

		public static StagePlan? Parse(string? text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			List<PayloadTier> list = new List<PayloadTier>();
			Dictionary<PayloadTier, int> dictionary = new Dictionary<PayloadTier, int>();
			List<FileOp> list2 = new List<FileOp>();
			List<CfgOp> list3 = new List<CfgOp>();
			bool flag = false;
			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 || text2[0] == '#')
				{
					continue;
				}
				string[] array2 = text2.Split(new char[1] { '|' });
				if (array2.Length < 2)
				{
					continue;
				}
				switch (array2[0])
				{
				case "V":
				{
					if (!int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						return null;
					}
					if (result2 > 1)
					{
						return null;
					}
					flag = true;
					break;
				}
				case "T":
				{
					if (!PayloadTiers.TryParse(array2[1], out var tier))
					{
						return null;
					}
					list.Add(tier);
					if (array2.Length >= 3 && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						dictionary[tier] = result;
					}
					break;
				}
				case "C":
					if (array2.Length < 3)
					{
						return null;
					}
					list2.Add(new FileOp(FileOpKind.Copy, Unescape(array2[1]), array2[2]));
					break;
				case "D":
					list2.Add(new FileOp(FileOpKind.Delete, Unescape(array2[1]), string.Empty));
					break;
				case "K":
					if (array2.Length < 5)
					{
						return null;
					}
					list3.Add(new CfgOp(Unescape(array2[1]), new CfgKey(Unescape(array2[2]), Unescape(array2[3])), Unescape(array2[4])));
					break;
				default:
					return null;
				}
			}
			if (!flag)
			{
				return null;
			}
			try
			{
				return new StagePlan(list, list2, list3, dictionary);
			}
			catch (InvalidDataException)
			{
				return null;
			}
		}

		public static IReadOnlyList<FileOp> PluginOps(PayloadDiff diff)
		{
			List<FileOp> list = new List<FileOp>();
			foreach (PayloadEntry item in diff.Added.Concat(diff.Changed))
			{
				list.Add(new FileOp(FileOpKind.Copy, "BepInEx/plugins/" + item.RelPath, item.Sha256));
			}
			foreach (string item2 in diff.Removed)
			{
				list.Add(new FileOp(FileOpKind.Delete, "BepInEx/plugins/" + item2, string.Empty));
			}
			return list;
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public sealed class PayloadTierState
	{
		public int BasedOnGen { get; }

		public string LastPushedFingerprint { get; }

		public PayloadTierState(int basedOnGen, string lastPushedFingerprint)
		{
			BasedOnGen = basedOnGen;
			LastPushedFingerprint = lastPushedFingerprint ?? string.Empty;
		}
	}
	public sealed class PayloadConflictRecord
	{
		public string LocalFingerprint { get; }

		public string ShareFingerprint { get; }

		public int ShareGen { get; }

		public PayloadConflictRecord(string localFingerprint, string shareFingerprint, int shareGen)
		{
			LocalFingerprint = localFingerprint ?? string.Empty;
			ShareFingerprint = shareFingerprint ?? string.Empty;
			ShareGen = shareGen;
		}
	}
	public sealed class PayloadState
	{
		private readonly object _gate = new object();

		private readonly Dictionary<PayloadTier, PayloadTierState> _state = new Dictionary<PayloadTier, PayloadTierState>();

		private readonly Dictionary<PayloadTier, PayloadConflictRecord> _conflicts = new Dictionary<PayloadTier, PayloadConflictRecord>();

		private readonly Dictionary<PayloadTier, PayloadTierState> _staged = new Dictionary<PayloadTier, PayloadTierState>();

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

		public IReadOnlyList<PayloadTier> PendingConflicts
		{
			get
			{
				lock (_gate)
				{
					return _conflicts.Keys.ToList();
				}
			}
		}

		public bool HasConflicts
		{
			get
			{
				lock (_gate)
				{
					return _conflicts.Count > 0;
				}
			}
		}

		public IReadOnlyList<PayloadTier> Staged
		{
			get
			{
				lock (_gate)
				{
					return _staged.Keys.ToList();
				}
			}
		}

		public PayloadTierState? Get(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadTierState value;
				return _state.TryGetValue(t, out value) ? value : null;
			}
		}

		public void Set(PayloadTier t, PayloadTierState s)
		{
			lock (_gate)
			{
				_state[t] = s;
			}
		}

		public PayloadConflictRecord? GetConflict(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadConflictRecord value;
				return _conflicts.TryGetValue(t, out value) ? value : null;
			}
		}

		public void SetConflict(PayloadTier t, PayloadConflictRecord c)
		{
			lock (_gate)
			{
				_conflicts[t] = c;
			}
		}

		public void ClearConflict(PayloadTier t)
		{
			lock (_gate)
			{
				_conflicts.Remove(t);
			}
		}

		public PayloadTierState? GetStaged(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadTierState value;
				return _staged.TryGetValue(t, out value) ? value : null;
			}
		}

		public void SetStaged(PayloadTier t, PayloadTierState s)
		{
			lock (_gate)
			{
				_staged[t] = s;
			}
		}

		public void ClearStaged(PayloadTier t)
		{
			lock (_gate)
			{
				_staged.Remove(t);
			}
		}

		public bool IsIntroduced(string payloadRoot)
		{
			lock (_gate)
			{
				return _introduced.Contains(payloadRoot ?? string.Empty);
			}
		}

		public void MarkIntroduced(string payloadRoot)
		{
			lock (_gate)
			{
				_introduced.Add(payloadRoot ?? string.Empty);
			}
		}

		public string Serialize()
		{
			lock (_gate)
			{
				List<string> list = new List<string>();
				PayloadTier[] all = PayloadTiers.All;
				foreach (PayloadTier payloadTier in all)
				{
					if (_state.TryGetValue(payloadTier, out PayloadTierState value))
					{
						list.Add("S|" + PayloadTiers.Name(payloadTier) + "|" + value.BasedOnGen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(value.LastPushedFingerprint));
					}
					if (_conflicts.TryGetValue(payloadTier, out PayloadConflictRecord value2))
					{
						list.Add("C|" + PayloadTiers.Name(payloadTier) + "|" + Escape(value2.LocalFingerprint) + "|" + Escape(value2.ShareFingerprint) + "|" + value2.ShareGen.ToString(CultureInfo.InvariantCulture));
					}
					if (_staged.TryGetValue(payloadTier, out PayloadTierState value3))
					{
						list.Add("G|" + PayloadTiers.Name(payloadTier) + "|" + Escape(value3.LastPushedFingerprint) + "|" + value3.BasedOnGen.ToString(CultureInfo.InvariantCulture));
					}
				}
				foreach (string item in _introduced.OrderBy<string, string>((string k) => k, StringComparer.Ordinal))
				{
					list.Add("I|" + Escape(item));
				}
				return string.Join("\n", list);
			}
		}

		public static PayloadState Parse(string? text)
		{
			PayloadState payloadState = new PayloadState();
			if (string.IsNullOrEmpty(text))
			{
				return payloadState;
			}
			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;
				}
				PayloadTier tier;
				if (array2[0] == "I")
				{
					payloadState._introduced.Add(Unescape(array2[1]));
				}
				else if (PayloadTiers.TryParse(array2[1], out tier))
				{
					int result2;
					int result3;
					if (array2[0] == "S" && array2.Length >= 4 && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						payloadState._state[tier] = new PayloadTierState(result, Unescape(array2[3]));
					}
					else if (array2[0] == "C" && array2.Length >= 5 && int.TryParse(array2[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
					{
						payloadState._conflicts[tier] = new PayloadConflictRecord(Unescape(array2[2]), Unescape(array2[3]), result2);
					}
					else if (array2[0] == "G" && array2.Length >= 4 && int.TryParse(array2[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out result3))
					{
						payloadState._staged[tier] = new PayloadTierState(result3, Unescape(array2[2]));
					}
				}
			}
			return payloadState;
		}

		private static string Escape(string v)
		{
			return LineCodec.Escape(v);
		}

		private static string Unescape(string v)
		{
			return LineCodec.Unescape(v);
		}
	}
	public static class PayloadStore
	{
		public const string LedgerFileName = ".payload-ledger";

		public const string LockFileName = ".owner.lock";

		public const string ManifestDir = "manifest";

		public const string TreeDir = "tree";

		public const string DevicesDir = "devices";

		public const string BackupDir = ".payload-forks";

		public const int HeartbeatEveryFiles = 25;

		public static string LedgerPath(string payloadRoot)
		{
			return Path.Combine(payloadRoot, ".payload-ledger");
		}

		public static string ManifestPath(string payloadRoot, PayloadTier tier)
		{
			return Path.Combine(payloadRoot, "manifest", PayloadTiers.Name(tier) + ".manifest");
		}

		public static string TreePath(string payloadRoot, PayloadTier tier)
		{
			return Path.Combine(payloadRoot, "tree", PayloadTiers.Name(tier));
		}

		public static string DevicePath(string payloadRoot, string device)
		{
			return Path.Combine(payloadRoot, "devices", AtomicFile.SanitizeForFileName(device) + ".json");
		}

		public static PayloadLedger ReadLedger(string payloadRoot, Action<string>? warn = null)
		{
			string text = LedgerPath(payloadRoot);
			try
			{
				return File.Exists(text) ? PayloadLedger.Parse(File.ReadAllText(text)) : new PayloadLedger();
			}
			catch (Exception ex)
			{
				Warn(warn, "payload ledger at '" + text + "' is unreadable (" + ex.GetType().Name + ": " + ex.Message + ") — reading the share as EMPTY, which means this device will try to republish its whole payload. Check the mount before letting that happen.");
				return new PayloadLedger();
			}
		}

		public static PayloadManifest? ReadManifest(string payloadRoot, PayloadTier tier, Action<string>? warn = null)
		{
			string text = ManifestPath(payloadRoot, tier);
			try
			{
				if (!File.Exists(text))
				{
					return null;
				}
				PayloadManifest payloadManifest = PayloadManifest.Parse(File.ReadAllText(text), tier);
				if (payloadManifest == null)
				{
					Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest at '" + text + "' could not be parsed (written by a newer Cloudward?) — this device will act as if the share published nothing.");
				}
				return payloadManifest;
			}
			catch (Exception ex)
			{
				Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest at '" + text + "' is unreadable (" + ex.GetType().Name + ": " + ex.Message + ") — this device will act as if the share published nothing.");
				return null;
			}
		}

		private static void Warn(Action<string>? warn, string message)
		{
			try
			{
				warn?.Invoke(message);
			}
			catch
			{
			}
		}

		public static void PublishTree(string payloadRoot, PayloadTier tier, string localRoot, PayloadManifest manifest, string tempSuffix, string? backupPath, Action? heartbeat = null)
		{
			string text = TreePath(payloadRoot, tier);
			string text2 = text + tempSuffix;
			try
			{
				if (Directory.Exists(text2))
				{
					Directory.Delete(text2, recursive: true);
				}
				Directory.CreateDirectory(text2);
				List<string> list = null;
				int num = 0;
				foreach (PayloadEntry entry in manifest.Entries)
				{
					if (heartbeat != null && num++ % 25 == 0)
					{
						try
						{
							heartbeat();
						}
						catch
						{
						}
					}
					string text3 = Path.Combine(localRoot, PayloadScope.ToNative(entry.RelPath));
					if (!File.Exists(text3))
					{
						(list ?? (list = new List<string>())).Add(entry.RelPath);
						continue;
					}
					string text4 = Path.Combine(text2, PayloadScope.ToNative(entry.RelPath));
					string directoryName = Path.GetDirectoryName(text4);
					if (!string.IsNullOrEmpty(directoryName))
					{
						Directory.CreateDirectory(directoryName);
					}
					File.Copy(text3, text4, overwrite: true);
				}
				if (list != null)
				{
					throw new FileNotFoundException($"{list.Count} file(s) named by the {PayloadTiers.Name(tier)} manifest vanished between " + "the scan and the publish — publish ABORTED so the share keeps a whole payload (first: " + list[0] + ")");
				}
				SwapDirIntoPlace(text2, text, backupPath);
			}
			catch
			{
				try
				{
					if (Directory.Exists(text2))
					{
						Directory.Delete(text2, recursive: true);
					}
				}
				catch
				{
				}
				try
				{
					if (!Directory.Exists(text) && !string.IsNullOrEmpty(backupPath) && Directory.Exists(backupPath))
					{
						MoveOrCopy(backupPath, text);
					}
				}
				catch
				{
				}
				throw;
			}
			WriteManifest(payloadRoot, tier, manifest, tempSuffix);
		}

		public static bool RepairTreeFromBackup(string payloadRoot, PayloadTier tier, Action<string>? warn = null, string? tempSuffix = null)
		{
			string text = TreePath(payloadRoot, tier) + (string.IsNullOrEmpty(tempSuffix) ? ".tmp-repair" : (tempSuffix + ".repair"));
			try
			{
				string path = ManifestPath(payloadRoot, tier);
				string text2 = TreePath(payloadRoot, tier);
				if (!File.Exists(path) || Directory.Exists(text2))
				{
					return false;
				}
				IReadOnlyList<string> readOnlyList = ListBackups(payloadRoot, tier);
				if (readOnlyList.Count == 0)
				{
					Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest names a tree that DOES NOT EXIST and no backup is available — an interrupted publish; peers cannot fetch this tier until somebody publishes again.");
					return false;
				}
				if (Directory.Exists(text))
				{
					Directory.Delete(text, recursive: true);
				}
				CopyDir(Path.Combine(payloadRoot, ".payload-forks", readOnlyList[0]), text);
				if (Directory.Exists(text2))
				{
					try
					{
						Directory.Delete(text, recursive: true);
					}
					catch
					{
					}
					return false;
				}
				Directory.Move(text, text2);
				Warn(warn, "the share's " + PayloadTiers.Name(tier) + " tree was MISSING under a live manifest (interrupted publish) — restored from backup '" + readOnlyList[0] + "'.");
				return true;
			}
			catch (Exception ex)
			{
				try
				{
					if (Directory.Exists(text))
					{
						Directory.Delete(text, recursive: true);
					}
				}
				catch
				{
				}
				Warn(warn, "boot repair of the " + PayloadTiers.Name(tier) + " tree failed (" + ex.GetType().Name + ": " + ex.Message + ").");
				return false;
			}
		}

		public static void WriteManifest(string payloadRoot, PayloadTier tier, PayloadManifest manifest, string tempSuffix)
		{
			string path = ManifestPath(payloadRoot, tier);
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			AtomicFile.WriteAllText(path, manifest.Serialize(), tempSuffix);
		}

		public static void WriteLedger(string payloadRoot, PayloadLedger ledger, string tempSuffix)
		{
			Directory.CreateDirectory(payloadRoot);
			AtomicFile.WriteAllText(LedgerPath(payloadRoot), ledger.Serialize(), tempSuffix);
		}

		public static string ConfigValuesPath(string payloadRoot)
		{
			return Path.Combine(TreePath(payloadRoot, PayloadTier.Config), "values.txt");
		}

		public static void PublishConfigValues(string payloadRoot, CfgValueSet values, PayloadManifest manifest, string tempSuffix)
		{
			string path = ConfigValuesPath(payloadRoot);
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			AtomicFile.WriteAllText(path, values.Serialize(), tempSuffix);
			WriteManifest(payloadRoot, PayloadTier.Config, manifest, tempSuffix);
		}

		public static CfgValueSet ReadConfigValues(string payloadRoot)
		{
			try
			{
				string path = ConfigValuesPath(payloadRoot);
				return File.Exists(path) ? CfgValueSet.Parse(File.ReadAllText(path)) : CfgValueSet.Empty;
			}
			catch
			{
				return CfgValueSet.Empty;
			}
		}

		public static int FetchFiles(string payloadRoot

patchers/Cloudward.Preload/Cloudward.Preload.dll

Decompiled 3 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using BepInEx;
using BepInEx.Logging;
using Cloudward.Core;
using Microsoft.CodeAnalysis;
using Mono.Cecil;

[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.Preload")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+83254ebc1e0f377c73c2296a9412cdb0c1c00f84")]
[assembly: AssemblyProduct("Cloudward.Preload")]
[assembly: AssemblyTitle("Cloudward.Preload")]
[assembly: AssemblyMetadata("BuildStamp", "83254ebc 2026-08-28")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
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;
		}
	}
}
namespace Cloudward.Preload
{
	public static class Patcher
	{
		public static IEnumerable<string> TargetDLLs => Array.Empty<string>();

		public static void Patch(ref AssemblyDefinition assembly)
		{
		}

		public static void Initialize()
		{
			//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ca: Expected I4, but got Unknown
			ManualLogSource log = null;
			try
			{
				log = Logger.CreateLogSource("Cloudward.Preload");
				string text = GameRoot();
				string text2 = Path.Combine(Paths.BepInExRootPath, "cloudward-staged");
				if (!Directory.Exists(text2))
				{
					return;
				}
				if (PayloadApply.KillSwitchPresent(text))
				{
					log.LogWarning((object)"'.cloudward-disable' present — staged payload NOT applied.");
					return;
				}
				string text3 = Path.Combine(Paths.BepInExRootPath, "cloudward-backup", DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ"));
				log.LogMessage((object)"applying staged payload before chainload…");
				ApplyResult val = PayloadApply.Execute(text, text2, text3, (Action<string>)delegate(string m)
				{
					log.LogMessage((object)m);
				});
				ApplyOutcome outcome = val.Outcome;
				switch ((int)outcome)
				{
				case 4:
					if (val.PartiallyApplied)
					{
						log.LogError((object)($"payload PARTIALLY applied: {val.FilesCopied} copied, {val.FilesDeleted} " + $"deleted, {val.CfgKeysChanged} cfg key(s), {val.OpsFailed} FAILED. " + "Backup: " + text3 + " | " + string.Join(" | ", ToArray(val.Messages))));
					}
					else
					{
						log.LogMessage((object)($"payload applied: {val.FilesCopied} copied, {val.FilesDeleted} deleted, " + $"{val.CfgKeysChanged} cfg key(s). Backup: {text3}"));
					}
					break;
				case 3:
					log.LogError((object)("staged payload REFUSED — the install was not modified. " + string.Join(" | ", ToArray(val.Messages))));
					break;
				default:
					log.LogWarning((object)string.Join(" | ", ToArray(val.Messages)));
					break;
				case 0:
					break;
				}
				PruneBackups(Path.Combine(Paths.BepInExRootPath, "cloudward-backup"), 3);
			}
			catch (Exception ex)
			{
				try
				{
					ManualLogSource obj = log;
					if (obj != null)
					{
						obj.LogError((object)("Cloudward preload patcher failed (install untouched): " + ex));
					}
				}
				catch
				{
				}
			}
		}

		public static void Finish()
		{
		}

		private static string GameRoot()
		{
			try
			{
				DirectoryInfo parent = Directory.GetParent(Paths.BepInExRootPath);
				if (parent != null)
				{
					return parent.FullName;
				}
			}
			catch
			{
			}
			return Paths.GameRootPath;
		}

		private static void PruneBackups(string root, int keep)
		{
			try
			{
				if (!Directory.Exists(root))
				{
					return;
				}
				string[] directories = Directory.GetDirectories(root);
				Array.Sort(directories, (IComparer<string>?)StringComparer.Ordinal);
				for (int i = 0; i < directories.Length - keep; i++)
				{
					try
					{
						Directory.Delete(directories[i], recursive: true);
					}
					catch
					{
					}
				}
			}
			catch
			{
			}
		}

		private static string[] ToArray(IReadOnlyList<string> list)
		{
			string[] array = new string[list.Count];
			for (int i = 0; i < list.Count; i++)
			{
				array[i] = list[i];
			}
			return array;
		}
	}
}

plugins/Cloudward.Core.dll

Decompiled 3 weeks 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 System.Security.Cryptography;
using System.Text;
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("0.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+83254ebc1e0f377c73c2296a9412cdb0c1c00f84")]
[assembly: AssemblyProduct("Cloudward.Core")]
[assembly: AssemblyTitle("Cloudward.Core")]
[assembly: InternalsVisibleTo("Cloudward.Tests")]
[assembly: AssemblyMetadata("BuildStamp", "83254ebc 2026-08-28")]
[assembly: AssemblyVersion("0.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[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 sealed class CfgDoc
	{
		private static readonly byte[] Bom = new byte[3] { 239, 187, 191 };

		private static readonly byte[] DefaultPrefix = Encoding.ASCII.GetBytes("# Default value:");

		private static readonly Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

		private readonly byte[] _raw;

		private readonly byte[] _bom;

		private readonly byte[] _eol;

		private readonly List<byte[]> _lines;

		private readonly Dictionary<CfgKey, int> _index = new Dictionary<CfgKey, int>();

		private readonly Dictionary<CfgKey, string> _defaults = new Dictionary<CfgKey, string>();

		public IReadOnlyList<CfgKey> Keys => (from kv in _index
			orderby kv.Value
			select kv.Key).ToList();

		public byte[] Raw => _raw;

		public CfgDoc(byte[] raw)
		{
			_raw = raw ?? Array.Empty<byte>();
			_bom = (StartsWith(_raw, Bom) ? Bom : Array.Empty<byte>());
			byte[] array = new byte[_raw.Length - _bom.Length];
			Array.Copy(_raw, _bom.Length, array, 0, array.Length);
			_eol = ((!Contains(array, 13, 10)) ? new byte[1] { 10 } : new byte[2] { 13, 10 });
			_lines = SplitOn(array, _eol);
			string text = string.Empty;
			string text2 = null;
			HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal);
			for (int i = 0; i < _lines.Count; i++)
			{
				byte[] array2 = _lines[i];
				string text3 = MatchSection(array2);
				if (text3 != null)
				{
					text = text3;
					if (!hashSet.Add(text))
					{
						throw new InvalidDataException("section [" + text + "] appears twice — malformed cfg, refusing");
					}
					text2 = null;
					continue;
				}
				string text4 = MatchDefault(array2);
				if (text4 != null)
				{
					text2 = text4;
				}
				else
				{
					if ((array2.Length != 0 && array2[0] == 35) || IsBlank(array2))
					{
						continue;
					}
					if (TrySplitSetting(array2, out int _, out string key))
					{
						CfgKey key2 = new CfgKey(text, key);
						_index[key2] = i;
						if (text2 != null)
						{
							_defaults[key2] = text2;
						}
					}
					text2 = null;
				}
			}
		}

		public static CfgDoc FromText(string text)
		{
			return new CfgDoc(Utf8.GetBytes(text ?? string.Empty));
		}

		public string? Default(CfgKey key)
		{
			if (!_defaults.TryGetValue(key, out string value))
			{
				return null;
			}
			return value;
		}

		public bool Contains(CfgKey key)
		{
			return _index.ContainsKey(key);
		}

		public string? Get(CfgKey key)
		{
			if (!_index.TryGetValue(key, out var value))
			{
				return null;
			}
			if (!TrySplitSetting(_lines[value], out int preLen, out string _))
			{
				return null;
			}
			return Utf8.GetString(_lines[value], preLen, _lines[value].Length - preLen);
		}

		public bool Set(CfgKey key, string value)
		{
			if (!_index.TryGetValue(key, out var value2))
			{
				return false;
			}
			byte[] array = _lines[value2];
			if (!TrySplitSetting(array, out int preLen, out string _))
			{
				return false;
			}
			byte[] bytes = Utf8.GetBytes(value ?? string.Empty);
			byte[] array2 = new byte[preLen + bytes.Length];
			Array.Copy(array, 0, array2, 0, preLen);
			Array.Copy(bytes, 0, array2, preLen, bytes.Length);
			_lines[value2] = array2;
			return true;
		}

		public byte[] ToBytes()
		{
			int num = _bom.Length;
			for (int i = 0; i < _lines.Count; i++)
			{
				num += _lines[i].Length + ((i > 0) ? _eol.Length : 0);
			}
			byte[] array = new byte[num];
			int num2 = 0;
			Array.Copy(_bom, 0, array, num2, _bom.Length);
			num2 += _bom.Length;
			for (int j = 0; j < _lines.Count; j++)
			{
				if (j > 0)
				{
					Array.Copy(_eol, 0, array, num2, _eol.Length);
					num2 += _eol.Length;
				}
				Array.Copy(_lines[j], 0, array, num2, _lines[j].Length);
				num2 += _lines[j].Length;
			}
			return array;
		}

		public bool Changed()
		{
			return !BytesEqual(ToBytes(), _raw);
		}

		private static string? MatchSection(byte[] line)
		{
			if (line.Length < 3 || line[0] != 91)
			{
				return null;
			}
			int num = line.Length;
			while (num > 0 && IsSpace(line[num - 1]))
			{
				num--;
			}
			if (num < 3 || line[num - 1] != 93)
			{
				return null;
			}
			return Utf8.GetString(line, 1, num - 2);
		}

		private static string? MatchDefault(byte[] line)
		{
			if (!StartsWith(line, DefaultPrefix))
			{
				return null;
			}
			int num = DefaultPrefix.Length;
			if (num < line.Length && line[num] == 32)
			{
				num++;
			}
			return Utf8.GetString(line, num, line.Length - num);
		}

		private static bool TrySplitSetting(byte[] line, out int preLen, out string key)
		{
			preLen = 0;
			key = string.Empty;
			if (line.Length == 0)
			{
				return false;
			}
			byte b = line[0];
			if (b == 35 || b == 91 || b == 13 || b == 10 || b == 61)
			{
				return false;
			}
			int num = Array.IndexOf(line, (byte)61);
			if (num <= 0)
			{
				return false;
			}
			preLen = num + 1;
			if (preLen < line.Length && (line[preLen] == 32 || line[preLen] == 9))
			{
				preLen++;
			}
			key = Utf8.GetString(line, 0, num).Trim();
			return key.Length > 0;
		}

		private static bool IsSpace(byte b)
		{
			if (b != 32 && b != 9 && b != 13)
			{
				return b == 10;
			}
			return true;
		}

		private static bool IsBlank(byte[] line)
		{
			for (int i = 0; i < line.Length; i++)
			{
				if (!IsSpace(line[i]))
				{
					return false;
				}
			}
			return true;
		}

		private static bool StartsWith(byte[] data, byte[] prefix)
		{
			if (data.Length < prefix.Length)
			{
				return false;
			}
			for (int i = 0; i < prefix.Length; i++)
			{
				if (data[i] != prefix[i])
				{
					return false;
				}
			}
			return true;
		}

		private static bool Contains(byte[] data, byte a, byte b)
		{
			for (int i = 0; i + 1 < data.Length; i++)
			{
				if (data[i] == a && data[i + 1] == b)
				{
					return true;
				}
			}
			return false;
		}

		private static bool BytesEqual(byte[] x, byte[] y)
		{
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}

		private static List<byte[]> SplitOn(byte[] data, byte[] sep)
		{
			List<byte[]> list = new List<byte[]>();
			int num = 0;
			for (int i = 0; i + sep.Length <= data.Length; i++)
			{
				bool flag = true;
				for (int j = 0; j < sep.Length; j++)
				{
					if (data[i + j] != sep[j])
					{
						flag = false;
						break;
					}
				}
				if (flag)
				{
					list.Add(Slice(data, num, i - num));
					i += sep.Length - 1;
					num = i + 1;
				}
			}
			list.Add(Slice(data, num, data.Length - num));
			return list;
		}

		private static byte[] Slice(byte[] data, int offset, int count)
		{
			byte[] array = new byte[count];
			Array.Copy(data, offset, array, 0, count);
			return array;
		}
	}
	public readonly struct CfgKey : IEquatable<CfgKey>, IComparable<CfgKey>
	{
		public string Section { get; }

		public string Key { get; }

		public CfgKey(string section, string key)
		{
			Section = section ?? string.Empty;
			Key = key ?? string.Empty;
		}

		public bool Equals(CfgKey other)
		{
			if (string.Equals(Section, other.Section, StringComparison.Ordinal))
			{
				return string.Equals(Key, other.Key, StringComparison.Ordinal);
			}
			return false;
		}

		public override bool Equals(object? obj)
		{
			if (obj is CfgKey other)
			{
				return Equals(other);
			}
			return false;
		}

		public override int GetHashCode()
		{
			return (Section.GetHashCode() * 397) ^ Key.GetHashCode();
		}

		public int CompareTo(CfgKey other)
		{
			int num = string.CompareOrdinal(Section, other.Section);
			if (num == 0)
			{
				return string.CompareOrdinal(Key, other.Key);
			}
			return num;
		}

		public override string ToString()
		{
			return "[" + Section + "] " + Key;
		}
	}
	public sealed class CfgMergeResult
	{
		public byte[] Before { get; }

		public byte[] After { get; }

		public IReadOnlyList<string> Changes { get; }

		public IReadOnlyList<string> Skipped { get; }

		public bool Dirty => !SequenceEqual(Before, After);

		public CfgMergeResult(byte[] before, byte[] after, IReadOnlyList<string> changes, IReadOnlyList<string> skipped)
		{
			Before = before;
			After = after;
			Changes = changes;
			Skipped = skipped;
		}

		private static bool SequenceEqual(byte[] x, byte[] y)
		{
			if (x == y)
			{
				return true;
			}
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}
	}
	public static class CfgMerge
	{
		public static CfgMergeResult Apply(byte[] raw, IReadOnlyDictionary<CfgKey, string> overlay)
		{
			CfgDoc cfgDoc = new CfgDoc(raw);
			List<string> list = new List<string>();
			List<string> list2 = new List<string>();
			foreach (KeyValuePair<CfgKey, string> item in (overlay ?? new Dictionary<CfgKey, string>()).OrderBy<KeyValuePair<CfgKey, string>, CfgKey>((KeyValuePair<CfgKey, string> kv) => kv.Key))
			{
				string text = cfgDoc.Get(item.Key);
				if (text == null)
				{
					list2.Add($"{item.Key} — not bound in target (host hasn't launched this build?)");
				}
				else if (!string.Equals(text, item.Value, StringComparison.Ordinal) && cfgDoc.Set(item.Key, item.Value))
				{
					list.Add($"{item.Key}: \"{text}\" -> \"{item.Value}\"");
				}
			}
			byte[] array = cfgDoc.ToBytes();
			if (list.Count == 0 && !BytesEqual(array, cfgDoc.Raw))
			{
				throw new InvalidOperationException("no-op merge must be byte-identical — CfgDoc round-trip is lossy, refusing to write");
			}
			return new CfgMergeResult(raw, array, list, list2);
		}

		public static IReadOnlyDictionary<CfgKey, string> Extract(byte[] raw, IEnumerable<CfgKey> allowed)
		{
			CfgDoc cfgDoc = new CfgDoc(raw);
			Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
			foreach (CfgKey item in allowed ?? Array.Empty<CfgKey>())
			{
				string text = cfgDoc.Get(item);
				if (text != null)
				{
					dictionary[item] = text;
				}
			}
			return dictionary;
		}

		private static bool BytesEqual(byte[] x, byte[] y)
		{
			if (x.Length != y.Length)
			{
				return false;
			}
			for (int i = 0; i < x.Length; i++)
			{
				if (x[i] != y[i])
				{
					return false;
				}
			}
			return true;
		}
	}
	public sealed class CfgOverlay
	{
		public const string FileSuffix = ".cfg.overlay";

		public string Guid { get; }

		public IReadOnlyDictionary<CfgKey, string> Values { get; }

		public int Count => Values.Count;

		public CfgOverlay(string guid, IReadOnlyDictionary<CfgKey, string> values)
		{
			Guid = guid ?? string.Empty;
			Values = values ?? new Dictionary<CfgKey, string>();
		}

		public bool Allows(CfgKey key)
		{
			return Values.ContainsKey(key);
		}

		public static string GuidFromFileName(string fileName)
		{
			string fileName2 = Path.GetFileName(fileName ?? string.Empty);
			if (!fileName2.EndsWith(".cfg.overlay", StringComparison.OrdinalIgnoreCase))
			{
				return fileName2;
			}
			return fileName2.Substring(0, fileName2.Length - ".cfg.overlay".Length);
		}

		public static CfgOverlay Parse(string guid, string? text)
		{
			Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
			if (string.IsNullOrEmpty(text))
			{
				return new CfgOverlay(guid, dictionary);
			}
			string text2 = string.Empty;
			string[] array = text.Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
			for (int i = 0; i < array.Length; i++)
			{
				string text3 = array[i].Trim();
				if (text3.Length == 0 || text3[0] == '#')
				{
					continue;
				}
				if (text3[0] == '[' && text3[text3.Length - 1] == ']')
				{
					text2 = text3.Substring(1, text3.Length - 2);
					Refuse(text2, "section");
					continue;
				}
				int num = text3.IndexOf('=');
				if (num >= 0)
				{
					string text4 = text3.Substring(0, num).Trim();
					Refuse(text4, "key");
					string text5 = text3.Substring(num + 1);
					if (text5.StartsWith(" ", StringComparison.Ordinal))
					{
						text5 = text5.Substring(1);
					}
					dictionary[new CfgKey(text2, text4)] = text5;
				}
			}
			return new CfgOverlay(guid, dictionary);
		}

		public string Serialize(string header)
		{
			List<string> list = new List<string>
			{
				(header ?? string.Empty).TrimEnd(Array.Empty<char>()),
				string.Empty
			};
			foreach (string section in Values.Keys.Select((CfgKey k) => k.Section).Distinct().OrderBy<string, string>((string s) => s, StringComparer.Ordinal))
			{
				list.Add("[" + section + "]");
				foreach (KeyValuePair<CfgKey, string> item in from kv in Values
					where string.Equals(kv.Key.Section, section, StringComparison.Ordinal)
					orderby kv.Key
					select kv)
				{
					list.Add(item.Key.Key + " = " + item.Value);
				}
				list.Add(string.Empty);
			}
			return string.Join("\n", list) + "\n";
		}

		private static void Refuse(string token, string what)
		{
			if (token.IndexOf('*') >= 0 || token.IndexOf('?') >= 0)
			{
				throw new InvalidDataException("overlay " + what + " \"" + token + "\" contains a wildcard. Overlays name individual keys; there is deliberately no way to say \"everything\" — see config/README.md.");
			}
		}
	}
	public sealed class CfgValueSet
	{
		public sealed class Row
		{
			public string Guid { get; }

			public CfgKey Key { get; }

			public string Value { get; }

			public string RelPath => Guid + ".cfg/" + Key.Section + "/" + Key.Key;

			public Row(string guid, CfgKey key, string value)
			{
				Guid = guid ?? string.Empty;
				Key = key;
				Value = value ?? string.Empty;
			}
		}

		public IReadOnlyList<Row> Rows { get; }

		public static CfgValueSet Empty => new CfgValueSet(Array.Empty<Row>());

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

		public CfgValueSet(IEnumerable<Row> rows)
		{
			Rows = (from g in (rows ?? Array.Empty<Row>()).Where((Row r) => r != null && r.Guid.Length > 0).GroupBy<Row, string>((Row r) => r.RelPath, StringComparer.Ordinal)
				select g.First()).OrderBy<Row, string>((Row r) => r.RelPath, StringComparer.Ordinal).ToList();
		}

		public PayloadManifest ToManifest()
		{
			return new PayloadManifest(PayloadTier.Config, Rows.Select((Row r) => new PayloadEntry(r.RelPath, AtomicFile.Utf8NoBom.GetByteCount(r.Value), PayloadHasher.HashBytes(AtomicFile.Utf8NoBom.GetBytes(r.Value)))));
		}

		public string Serialize()
		{
			return string.Join("\n", Rows.Select((Row r) => Escape(r.Guid) + "|" + Escape(r.Key.Section) + "|" + Escape(r.Key.Key) + "|" + Escape(r.Value)));
		}

		public static CfgValueSet Parse(string? text)
		{
			List<Row> list = new List<Row>();
			if (string.IsNullOrEmpty(text))
			{
				return new CfgValueSet(list);
			}
			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 && text2[0] != '#')
				{
					string[] array2 = text2.Split(new char[1] { '|' });
					if (array2.Length >= 4)
					{
						list.Add(new Row(Unescape(array2[0]), new CfgKey(Unescape(array2[1]), Unescape(array2[2])), Unescape(array2[3])));
					}
				}
			}
			return new CfgValueSet(list);
		}

		public IReadOnlyDictionary<string, IReadOnlyDictionary<CfgKey, string>> ByGuid()
		{
			Dictionary<string, IReadOnlyDictionary<CfgKey, string>> dictionary = new Dictionary<string, IReadOnlyDictionary<CfgKey, string>>(StringComparer.OrdinalIgnoreCase);
			foreach (IGrouping<string, Row> item in Rows.GroupBy<Row, string>((Row r) => r.Guid, StringComparer.OrdinalIgnoreCase))
			{
				Dictionary<CfgKey, string> dictionary2 = new Dictionary<CfgKey, string>();
				foreach (Row item2 in item)
				{
					dictionary2[item2.Key] = item2.Value;
				}
				dictionary[item.Key] = dictionary2;
			}
			return dictionary;
		}

		public static CfgValueSet Extract(IEnumerable<CfgOverlay> overlays, Func<string, byte[]?> readCfg)
		{
			IReadOnlyList<string> unreadable;
			return Extract(overlays, readCfg, out unreadable);
		}

		public static CfgValueSet Extract(IEnumerable<CfgOverlay> overlays, Func<string, byte[]?> readCfg, out IReadOnlyList<string> unreadable)
		{
			List<Row> list = new List<Row>();
			List<string> list2 = new List<string>();
			foreach (CfgOverlay item in overlays ?? Array.Empty<CfgOverlay>())
			{
				byte[] array;
				try
				{
					array = readCfg(item.Guid);
				}
				catch
				{
					list2.Add(item.Guid + ".cfg (read failed)");
					continue;
				}
				if (array == null || array.Length == 0)
				{
					continue;
				}
				IReadOnlyDictionary<CfgKey, string> readOnlyDictionary;
				try
				{
					readOnlyDictionary = CfgMerge.Extract(array, item.Values.Keys);
				}
				catch
				{
					list2.Add(item.Guid + ".cfg (parse failed)");
					continue;
				}
				foreach (KeyValuePair<CfgKey, string> item2 in readOnlyDictionary)
				{
					list.Add(new Row(item.Guid, item2.Key, item2.Value));
				}
			}
			unreadable = list2;
			return new CfgValueSet(list);
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public enum ForkResolution
	{
		NewestWins,
		ThisDevice,
		OtherDevice,
		KeepBoth,
		AskMe
	}
	public enum ForkGate
	{
		AutoResolve,
		DeferMidSession,
		PauseForManual,
		AlreadyPaused
	}
	public static class ForkResolve
	{
		public static ForkGate Gate(bool autoPolicy, bool mayMutateLocal, bool alreadyPending)
		{
			if (!autoPolicy)
			{
				if (!alreadyPending)
				{
					return ForkGate.PauseForManual;
				}
				return ForkGate.AlreadyPaused;
			}
			if (!mayMutateLocal)
			{
				return ForkGate.DeferMidSession;
			}
			return ForkGate.AutoResolve;
		}

		public static bool DivergenceExists(CharTree? local, CharTree? share)
		{
			if (local != null && !local.IsEmpty && share != null)
			{
				return !share.IsEmpty;
			}
			return false;
		}

		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 static class JoinRaceGate
	{
		public enum PullDecision
		{
			Pull,
			HoldAndRetry,
			DeferToNextLaunch
		}

		public const float DefaultHoldBudgetSeconds = 60f;

		public static PullDecision Decide(bool mayMutateLocal, bool savesPinned, bool characterEverInUse, float heldSeconds, float holdBudgetSeconds = 60f)
		{
			if (!mayMutateLocal)
			{
				return PullDecision.DeferToNextLaunch;
			}
			if (!savesPinned)
			{
				return PullDecision.Pull;
			}
			if (characterEverInUse)
			{
				return PullDecision.DeferToNextLaunch;
			}
			if (heldSeconds > holdBudgetSeconds)
			{
				return PullDecision.DeferToNextLaunch;
			}
			return PullDecision.HoldAndRetry;
		}
	}
	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 object _gate = new object();

		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 IReadOnlyList<string> PendingForks
		{
			get
			{
				lock (_gate)
				{
					return _forks.Keys.ToList();
				}
			}
		}

		public bool HasForks
		{
			get
			{
				lock (_gate)
				{
					return _forks.Count > 0;
				}
			}
		}

		public LocalCharState? Get(string uid)
		{
			lock (_gate)
			{
				LocalCharState value;
				return _state.TryGetValue(uid, out value) ? value : null;
			}
		}

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

		public ForkRecord? GetFork(string uid)
		{
			lock (_gate)
			{
				ForkRecord value;
				return _forks.TryGetValue(uid, out value) ? value : null;
			}
		}

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

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

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

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

		public string Serialize()
		{
			lock (_gate)
			{
				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
	}
	public static class LockLease
	{
		public static LeaseDecision Decide(DateTime nowUtc, LockStamp? existing, string ownDevice, int ownPid, int staleSeconds, LockHeldPolicy policy, Func<int, bool>? isPidAlive = null)
		{
			if (existing == null)
			{
				return LeaseDecision.Take;
			}
			if (string.Equals(existing.Device, ownDevice, StringComparison.OrdinalIgnoreCase) && (existing.Pid == ownPid || isPidAlive == null || !isPidAlive(existing.Pid)))
			{
				return LeaseDecision.Take;
			}
			if ((nowUtc - existing.HeartbeatUtc).TotalSeconds > (double)staleSeconds)
			{
				return LeaseDecision.Take;
			}
			return Held(policy);
		}

		private static LeaseDecision Held(LockHeldPolicy policy)
		{
			if (policy != LockHeldPolicy.ForceTake)
			{
				return LeaseDecision.FallbackLocal;
			}
			return LeaseDecision.Take;
		}
	}
	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, bool mountpointConfirmed = false)
		{
			if (!driveReady)
			{
				return MountDecision.Offline;
			}
			if (markerExists)
			{
				return MountDecision.Live;
			}
			if (requireExistingMarker)
			{
				return MountDecision.Offline;
			}
			if (shareHasData)
			{
				return MountDecision.CreateMarkerThenLive;
			}
			if (alreadyBootstrapped)
			{
				return MountDecision.Offline;
			}
			if (!mountpointConfirmed)
			{
				return MountDecision.Offline;
			}
			return MountDecision.CreateMarkerThenLive;
		}

		public static string? WineUnixPath(string? path)
		{
			if (path == null || path.Length < 2)
			{
				return null;
			}
			if ((path[0] != 'Z' && path[0] != 'z') || path[1] != ':')
			{
				return null;
			}
			if (path.Length == 2)
			{
				return "/";
			}
			if (path[2] != '/' && path[2] != '\\')
			{
				return null;
			}
			return path.Substring(2).Replace('\\', '/');
		}

		public static bool TryCoveringMount(string? fullPath, IEnumerable<string>? procMountsLines, out string mountpoint, out string fsType)
		{
			mountpoint = "";
			fsType = "";
			if (string.IsNullOrEmpty(fullPath) || procMountsLines == null)
			{
				return false;
			}
			string text = fullPath.TrimEnd(new char[1] { '/' });
			if (text.Length == 0)
			{
				text = "/";
			}
			int num = -1;
			foreach (string procMountsLine in procMountsLines)
			{
				if (string.IsNullOrEmpty(procMountsLine))
				{
					continue;
				}
				string[] array = procMountsLine.Split(new char[1] { ' ' });
				if (array.Length >= 3)
				{
					string text2 = array[1].Replace("\\040", " ").Replace("\\011", "\t").TrimEnd(new char[1] { '/' });
					if ((text2.Length == 0 || string.Equals(text, text2, StringComparison.Ordinal) || text.StartsWith(text2 + "/", StringComparison.Ordinal)) && text2.Length >= num)
					{
						num = text2.Length;
						mountpoint = ((text2.Length == 0) ? "/" : text2);
						fsType = array[2];
					}
				}
			}
			return num >= 0;
		}

		public static bool IsRemoteFsType(string? fsType)
		{
			if (string.IsNullOrEmpty(fsType))
			{
				return false;
			}
			string text = fsType.ToLowerInvariant();
			if (text.StartsWith("fuse.", StringComparison.Ordinal))
			{
				text = text.Substring(5);
			}
			if (!text.StartsWith("nfs", StringComparison.Ordinal) && !text.StartsWith("smb", StringComparison.Ordinal) && !text.StartsWith("ceph", StringComparison.Ordinal) && !text.StartsWith("davfs", StringComparison.Ordinal))
			{
				switch (text)
				{
				default:
					return text == "afs";
				case "cifs":
				case "9p":
				case "glusterfs":
				case "sshfs":
				case "rclone":
					break;
				}
			}
			return true;
		}

		public static bool IsUnderNonRootMount(string? fullPath, IEnumerable<string>? procMountsLines)
		{
			if (string.IsNullOrEmpty(fullPath) || procMountsLines == null)
			{
				return false;
			}
			string text = fullPath.TrimEnd(new char[1] { '/' });
			if (text.Length == 0)
			{
				return false;
			}
			foreach (string procMountsLine in procMountsLines)
			{
				if (string.IsNullOrEmpty(procMountsLine))
				{
					continue;
				}
				string[] array = procMountsLine.Split(new char[1] { ' ' });
				if (array.Length >= 2)
				{
					string text2 = array[1].Replace("\\040", " ").Replace("\\011", "\t").TrimEnd(new char[1] { '/' });
					if (text2.Length != 0 && (string.Equals(text, text2, StringComparison.Ordinal) || text.StartsWith(text2 + "/", StringComparison.Ordinal)))
					{
						return true;
					}
				}
			}
			return false;
		}
	}
	public static class OpWatchdog
	{
		public enum Verdict
		{
			Quiet,
			WarnStalled,
			NoteRecovered
		}

		public const double DefaultWarnAfterSeconds = 60.0;

		public static Verdict Check(bool running, bool alreadyWarned, double heldSeconds, double warnAfterSeconds)
		{
			if (running)
			{
				if (alreadyWarned || !(heldSeconds >= warnAfterSeconds))
				{
					return Verdict.Quiet;
				}
				return Verdict.WarnStalled;
			}
			if (!alreadyWarned)
			{
				return Verdict.Quiet;
			}
			return Verdict.NoteRecovered;
		}
	}
	public static class PathRebase
	{
		public const string SaveBase = "SaveGames";

		public const string PayloadBase = ".cloudward-payload";

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

		public static string PayloadRoot(string mountRoot)
		{
			return TrimTrailingSeparators(mountRoot) + "/.cloudward-payload";
		}

		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 StagedRuling
	{
		StillStaged,
		Applied,
		NotApplied,
		Ambiguous
	}
	public sealed class ApplyReceipt
	{
		public const string RelPath = "BepInEx/cloudward-apply-receipt.txt";

		public ApplyOutcome Outcome { get; }

		public int OpsFailed { get; }

		public IReadOnlyDictionary<PayloadTier, int> Generations { get; }

		public string ApplierVersion { get; }

		public string Utc { get; }

		public bool PartiallyApplied
		{
			get
			{
				if (Outcome == ApplyOutcome.Applied)
				{
					return OpsFailed > 0;
				}
				return false;
			}
		}

		public static string PathFor(string gameRoot)
		{
			return Path.Combine(gameRoot ?? string.Empty, PayloadScope.ToNative("BepInEx/cloudward-apply-receipt.txt"));
		}

		public ApplyReceipt(ApplyOutcome outcome, int opsFailed, IReadOnlyDictionary<PayloadTier, int>? generations, string? applierVersion, string? utc = null)
		{
			Outcome = outcome;
			OpsFailed = opsFailed;
			Generations = generations ?? new Dictionary<PayloadTier, int>();
			ApplierVersion = applierVersion ?? string.Empty;
			Utc = utc ?? DateTime.UtcNow.ToString("o");
		}

		public static StagedRuling Judge(ApplyReceipt? receipt, bool stagingStillPresent)
		{
			if (stagingStillPresent)
			{
				return StagedRuling.StillStaged;
			}
			if (receipt == null)
			{
				return StagedRuling.Ambiguous;
			}
			if (receipt.Outcome != ApplyOutcome.Applied || receipt.OpsFailed != 0)
			{
				return StagedRuling.NotApplied;
			}
			return StagedRuling.Applied;
		}

		public static StagedRuling Judge(ApplyReceipt? receipt, bool stagingStillPresent, PayloadTier tier, int stagedGen)
		{
			StagedRuling stagedRuling = Judge(receipt, stagingStillPresent);
			if (stagedRuling == StagedRuling.Applied && receipt.Generations.TryGetValue(tier, out var value) && value != stagedGen)
			{
				return StagedRuling.Ambiguous;
			}
			return stagedRuling;
		}

		public string Serialize()
		{
			List<string> list = new List<string>
			{
				"O|" + Outcome,
				"F|" + OpsFailed.ToString(CultureInfo.InvariantCulture),
				"A|" + LineCodec.Escape(ApplierVersion),
				"U|" + LineCodec.Escape(Utc)
			};
			foreach (KeyValuePair<PayloadTier, int> generation in Generations)
			{
				list.Add("G|" + PayloadTiers.Name(generation.Key) + "|" + generation.Value.ToString(CultureInfo.InvariantCulture));
			}
			return string.Join("\n", list);
		}

		public static ApplyReceipt? Parse(string? text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			ApplyOutcome? applyOutcome = null;
			int result = 0;
			Dictionary<PayloadTier, int> dictionary = new Dictionary<PayloadTier, int>();
			string applierVersion = string.Empty;
			string utc = string.Empty;
			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;
				}
				switch (array2[0])
				{
				case "O":
				{
					if (Enum.TryParse<ApplyOutcome>(array2[1], out var result3))
					{
						applyOutcome = result3;
					}
					break;
				}
				case "F":
					int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
					break;
				case "A":
					applierVersion = LineCodec.Unescape(array2[1]);
					break;
				case "U":
					utc = LineCodec.Unescape(array2[1]);
					break;
				case "G":
				{
					if (array2.Length >= 3 && PayloadTiers.TryParse(array2[1], out var tier) && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						dictionary[tier] = result2;
					}
					break;
				}
				}
			}
			if (applyOutcome.HasValue)
			{
				return new ApplyReceipt(applyOutcome.Value, result, dictionary, applierVersion, utc);
			}
			return null;
		}

		public void WriteTo(string gameRoot)
		{
			try
			{
				AtomicFile.WriteAllText(PathFor(gameRoot), Serialize());
			}
			catch
			{
			}
		}

		public static ApplyReceipt? Consume(string gameRoot)
		{
			string path = PathFor(gameRoot);
			try
			{
				if (!File.Exists(path))
				{
					return null;
				}
				ApplyReceipt result = Parse(File.ReadAllText(path));
				try
				{
					File.Delete(path);
				}
				catch
				{
				}
				return result;
			}
			catch
			{
				return null;
			}
		}
	}
	public static class AtomicFile
	{
		public static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

		public static string ShareTempSuffix(string device, int pid)
		{
			return ".tmp." + SanitizeForFileName(device) + "." + pid.ToString(CultureInfo.InvariantCulture);
		}

		public static void WriteAllText(string path, string content, string? tempSuffix = null)
		{
			WriteAllBytes(path, Utf8NoBom.GetBytes(content ?? string.Empty), tempSuffix);
		}

		public static void WriteAllBytes(string path, byte[] bytes, string? tempSuffix = null)
		{
			string directoryName = Path.GetDirectoryName(path);
			if (!string.IsNullOrEmpty(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			string text = path + (string.IsNullOrEmpty(tempSuffix) ? ".tmp" : tempSuffix);
			try
			{
				File.WriteAllBytes(text, bytes ?? Array.Empty<byte>());
				Swap(text, path);
			}
			catch
			{
				try
				{
					if (File.Exists(text))
					{
						File.Delete(text);
					}
				}
				catch
				{
				}
				throw;
			}
		}

		public static void Swap(string tmp, string path)
		{
			if (!File.Exists(path))
			{
				File.Move(tmp, path);
				return;
			}
			try
			{
				File.Replace(tmp, path, null);
			}
			catch (PlatformNotSupportedException)
			{
				MoveAsideThenMove(tmp, path);
			}
			catch (IOException)
			{
				MoveAsideThenMove(tmp, path);
			}
		}

		internal static void MoveAsideThenMove(string tmp, string path)
		{
			string text = tmp + ".aside";
			try
			{
				if (File.Exists(text))
				{
					File.Delete(text);
				}
			}
			catch
			{
			}
			File.Move(path, text);
			try
			{
				File.Move(tmp, path);
			}
			catch
			{
				try
				{
					if (!File.Exists(path))
					{
						File.Move(text, path);
					}
				}
				catch
				{
				}
				throw;
			}
			try
			{
				File.Delete(text);
			}
			catch
			{
			}
		}

		public static string SanitizeForFileName(string? value)
		{
			if (string.IsNullOrEmpty(value))
			{
				return "unknown";
			}
			StringBuilder stringBuilder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				stringBuilder.Append((char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_');
			}
			string text = stringBuilder.ToString();
			if (text.Length != 0)
			{
				return text;
			}
			return "unknown";
		}
	}
	public static class Fnv1a
	{
		public static string HashLines(IEnumerable<string> lines)
		{
			List<string> list = new List<string>(lines ?? Array.Empty<string>());
			list.Sort(StringComparer.Ordinal);
			uint num = 2166136261u;
			foreach (string item in list)
			{
				if (item != null)
				{
					string text = item;
					foreach (char c in text)
					{
						num ^= c;
						num *= 16777619;
					}
					num ^= 0xA;
					num *= 16777619;
				}
			}
			return num.ToString("x8");
		}
	}
	public static class LineCodec
	{
		public static string Escape(string? s)
		{
			return (s ?? string.Empty).Replace("%", "%25").Replace("|", "%7C");
		}

		public static string Unescape(string? s)
		{
			return (s ?? string.Empty).Replace("%7C", "|").Replace("%25", "%");
		}
	}
	public enum ApplyOutcome
	{
		Nothing,
		Disabled,
		Held,
		Refused,
		Applied
	}
	public sealed class ApplyResult
	{
		public ApplyOutcome Outcome { get; }

		public int FilesCopied { get; }

		public int FilesDeleted { get; }

		public int CfgKeysChanged { get; }

		public IReadOnlyList<string> Messages { get; }

		public int OpsFailed { get; }

		public bool PartiallyApplied
		{
			get
			{
				if (Outcome == ApplyOutcome.Applied)
				{
					return OpsFailed > 0;
				}
				return false;
			}
		}

		public ApplyResult(ApplyOutcome outcome, int copied, int deleted, int cfgChanged, IReadOnlyList<string> messages, int opsFailed = 0)
		{
			Outcome = outcome;
			FilesCopied = copied;
			FilesDeleted = deleted;
			CfgKeysChanged = cfgChanged;
			Messages = messages;
			OpsFailed = opsFailed;
		}

		public static ApplyResult Simple(ApplyOutcome outcome, params string[] messages)
		{
			return new ApplyResult(outcome, 0, 0, 0, messages);
		}
	}
	public static class PayloadApply
	{
		public const string KillSwitchFileName = ".cloudward-disable";

		public const string TypeLoaderCache = "BepInEx/cache";

		public const string HoldFileName = "hold";

		public static bool KillSwitchPresent(string gameRoot)
		{
			return File.Exists(Path.Combine(gameRoot ?? string.Empty, ".cloudward-disable"));
		}

		public static bool HeldForManualApply(string stagingRoot)
		{
			return File.Exists(Path.Combine(stagingRoot ?? string.Empty, "hold"));
		}

		public static ApplyResult Execute(string gameRoot, string stagingRoot, string? backupDir, Action<string>? log = null)
		{
			IReadOnlyDictionary<PayloadTier, int> planGens = null;
			try
			{
				if (KillSwitchPresent(gameRoot))
				{
					return ApplyResult.Simple(ApplyOutcome.Disabled, ".cloudward-disable present — payload apply disabled, staging left in place");
				}
				string path = Path.Combine(stagingRoot, "stage.plan");
				if (!Directory.Exists(stagingRoot) || !File.Exists(path))
				{
					return ApplyResult.Simple(ApplyOutcome.Nothing);
				}
				if (HeldForManualApply(stagingRoot))
				{
					return ApplyResult.Simple(ApplyOutcome.Held, "a payload is staged but held ([Payload] ApplyMode=StageOnly) — run the 'payloadapply' verb, then restart, to install it");
				}
				StagePlan stagePlan;
				try
				{
					stagePlan = StagePlan.Parse(File.ReadAllText(path));
				}
				catch (Exception ex)
				{
					stagePlan = null;
					Say("plan unreadable: " + ex.Message);
				}
				if (stagePlan == null)
				{
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Refused, 0);
					return ApplyResult.Simple(ApplyOutcome.Refused, "staged plan was unreadable or unsupported — discarded, install untouched");
				}
				planGens = stagePlan.Generations;
				if (stagePlan.IsEmpty)
				{
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Nothing, 0);
					return ApplyResult.Simple(ApplyOutcome.Nothing, "staged plan was empty");
				}
				string path2 = Path.Combine(stagingRoot, "files");
				List<string> list = new List<string>();
				foreach (FileOp item in stagePlan.FileOps.Where((FileOp o) => o.Kind == FileOpKind.Copy))
				{
					string path3 = Path.Combine(path2, PayloadScope.ToNative(item.RelPath));
					if (!File.Exists(path3))
					{
						list.Add("missing staged file: " + item.RelPath);
						continue;
					}
					string a;
					try
					{
						a = PayloadHasher.HashFile(path3);
					}
					catch (Exception ex2)
					{
						list.Add("unreadable staged file " + item.RelPath + ": " + ex2.Message);
						continue;
					}
					if (!string.Equals(a, item.Sha256, StringComparison.Ordinal))
					{
						list.Add("sha256 mismatch: " + item.RelPath);
					}
				}
				if (list.Count > 0)
				{
					foreach (string item2 in list.Take(20))
					{
						Say("  " + item2);
					}
					Discard(stagingRoot);
					Receipt(ApplyOutcome.Refused, 0);
					return new ApplyResult(ApplyOutcome.Refused, 0, 0, 0, new string[1] { $"staged payload failed verification ({list.Count} problem(s)) — discarded, install untouched" }.Concat(list.Take(20)).ToList());
				}
				int num = 0;
				int num2 = 0;
				int num3 = 0;
				int num4 = 0;
				List<string> list2 = new List<string>();
				foreach (FileOp fileOp in stagePlan.FileOps)
				{
					string text = Path.Combine(gameRoot, PayloadScope.ToNative(fileOp.RelPath));
					try
					{
						Backup(backupDir, fileOp.RelPath, text);
						if (fileOp.Kind == FileOpKind.Copy)
						{
							string sourceFileName = Path.Combine(path2, PayloadScope.ToNative(fileOp.RelPath));
							string directoryName = Path.GetDirectoryName(text);
							if (!string.IsNullOrEmpty(directoryName))
							{
								Directory.CreateDirectory(directoryName);
							}
							string text2 = text + ".cloudward-new";
							File.Copy(sourceFileName, text2, overwrite: true);
							AtomicFile.Swap(text2, text);
							num++;
						}
						else if (File.Exists(text))
						{
							File.Delete(text);
							num2++;
						}
					}
					catch (Exception ex3)
					{
						num4++;
						list2.Add("failed " + fileOp.Kind.ToString() + " " + fileOp.RelPath + ": " + ex3.Message);
					}
				}
				foreach (IGrouping<string, CfgOp> item3 in stagePlan.CfgOps.GroupBy<CfgOp, string>((CfgOp o) => o.Guid, StringComparer.OrdinalIgnoreCase))
				{
					string text3 = Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/config/" + item3.Key + ".cfg"));
					if (!File.Exists(text3))
					{
						list2.Add("cfg absent, skipped: " + item3.Key);
						continue;
					}
					try
					{
						Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string>();
						foreach (CfgOp item4 in item3)
						{
							dictionary[item4.Key] = item4.Value;
						}
						CfgMergeResult cfgMergeResult = CfgMerge.Apply(File.ReadAllBytes(text3), dictionary);
						if (cfgMergeResult.Skipped.Count > 0)
						{
							list2.Add($"cfg {item3.Key}: {cfgMergeResult.Skipped.Count} key(s) skipped — " + string.Join("; ", cfgMergeResult.Skipped.Take(3)) + ((cfgMergeResult.Skipped.Count > 3) ? "; …" : string.Empty));
						}
						if (!cfgMergeResult.Dirty)
						{
							continue;
						}
						Backup(backupDir, "BepInEx/config/" + item3.Key + ".cfg", text3);
						AtomicFile.WriteAllBytes(text3, cfgMergeResult.After);
						num3 += cfgMergeResult.Changes.Count;
						foreach (string change in cfgMergeResult.Changes)
						{
							list2.Add("cfg " + item3.Key + " " + change);
						}
					}
					catch (Exception ex4)
					{
						num4++;
						list2.Add("failed cfg " + item3.Key + ": " + ex4.Message);
					}
				}
				if (num > 0 || num2 > 0)
				{
					try
					{
						string path4 = Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/cache"));
						if (Directory.Exists(path4))
						{
							Directory.Delete(path4, recursive: true);
							list2.Add("cleared BepInEx/cache");
						}
					}
					catch (Exception ex5)
					{
						list2.Add("could not clear BepInEx/cache: " + ex5.Message);
					}
				}
				Discard(stagingRoot);
				PruneEmptyDirs(Path.Combine(gameRoot, PayloadScope.ToNative("BepInEx/plugins")));
				if (num4 > 0)
				{
					list2.Insert(0, $"PARTIALLY APPLIED — {num4} op(s) failed AFTER verification. The install " + "is now a mix of old and new files; re-run the sync (or restore from the backup dir) before trusting this launch.");
				}
				list2.Insert(0, $"applied staged payload: {num} copied, {num2} deleted, {num3} cfg key(s)" + ((num4 > 0) ? $", {num4} FAILED" : ""));
				foreach (string item5 in list2.Take(40))
				{
					Say("  " + item5);
				}
				Receipt(ApplyOutcome.Applied, num4);
				return new ApplyResult(ApplyOutcome.Applied, num, num2, num3, list2, num4);
			}
			catch (Exception ex6)
			{
				try
				{
					Discard(stagingRoot);
				}
				catch
				{
				}
				try
				{
					Receipt(ApplyOutcome.Refused, 0);
				}
				catch
				{
				}
				return ApplyResult.Simple(ApplyOutcome.Refused, "payload apply aborted: " + ex6.Message);
			}
			void Receipt(ApplyOutcome outcome, int opsFailedCount)
			{
				new ApplyReceipt(outcome, opsFailedCount, planGens, ApplierVersion()).WriteTo(gameRoot);
			}
			void Say(string m)
			{
				try
				{
					log?.Invoke(m);
				}
				catch
				{
				}
			}
		}

		public static string ApplierVersion()
		{
			try
			{
				return typeof(PayloadApply).Assembly.GetName().Version?.ToString() ?? "?";
			}
			catch
			{
				return "?";
			}
		}

		private static void Backup(string? backupDir, string relPath, string source)
		{
			if (string.IsNullOrEmpty(backupDir) || !File.Exists(source))
			{
				return;
			}
			try
			{
				string text = Path.Combine(backupDir, PayloadScope.ToNative(relPath));
				string directoryName = Path.GetDirectoryName(text);
				if (!string.IsNullOrEmpty(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				File.Copy(source, text, overwrite: true);
			}
			catch
			{
			}
		}

		private static void Discard(string stagingRoot)
		{
			try
			{
				if (Directory.Exists(stagingRoot))
				{
					Directory.Delete(stagingRoot, recursive: true);
				}
			}
			catch
			{
			}
		}

		private static void PruneEmptyDirs(string root)
		{
			if (!Directory.Exists(root))
			{
				return;
			}
			try
			{
				string[] directories = Directory.GetDirectories(root);
				foreach (string text in directories)
				{
					PruneEmptyDirs(text);
					if (Directory.GetFileSystemEntries(text).Length == 0)
					{
						Directory.Delete(text);
					}
				}
			}
			catch
			{
			}
		}
	}
	public static class PayloadBackupName
	{
		public static string Compose(PayloadTier tier, int gen, string? stamp)
		{
			return PayloadTiers.Name(tier) + "-gen" + gen.ToString(CultureInfo.InvariantCulture) + "-" + AtomicFile.SanitizeForFileName(stamp);
		}

		public static string Prefix(PayloadTier tier)
		{
			return PayloadTiers.Name(tier) + "-gen";
		}

		public static bool BelongsTo(string? name, PayloadTier tier)
		{
			if (!string.IsNullOrEmpty(name))
			{
				return name.StartsWith(Prefix(tier), StringComparison.Ordinal);
			}
			return false;
		}

		public static bool TryParse(string? name, PayloadTier tier, out int gen, out string stamp)
		{
			gen = -1;
			stamp = string.Empty;
			if (!BelongsTo(name, tier))
			{
				return false;
			}
			string text = name.Substring(Prefix(tier).Length);
			int num = text.IndexOf('-');
			if (!int.TryParse((num < 0) ? text : text.Substring(0, num), NumberStyles.None, CultureInfo.InvariantCulture, out var result))
			{
				return false;
			}
			gen = result;
			stamp = ((num < 0) ? string.Empty : text.Substring(num + 1));
			return true;
		}

		public static List<string> OrderOldestFirst(IEnumerable<string>? names, PayloadTier tier)
		{
			List<string> list = new List<string>(names ?? Array.Empty<string>());
			list.Sort((string a, string b) => Compare(a, b, tier));
			return list;
		}

		public static List<string> OrderNewestFirst(IEnumerable<string>? names, PayloadTier tier)
		{
			List<string> list = OrderOldestFirst(names, tier);
			list.Reverse();
			return list;
		}

		private static int Compare(string a, string b, PayloadTier tier)
		{
			TryParse(a, tier, out int gen, out string stamp);
			TryParse(b, tier, out int gen2, out string stamp2);
			if (gen != gen2)
			{
				return gen.CompareTo(gen2);
			}
			int num = string.CompareOrdinal(stamp, stamp2);
			if (num == 0)
			{
				return string.CompareOrdinal(a ?? string.Empty, b ?? string.Empty);
			}
			return num;
		}
	}
	public enum PayloadConflictPolicy
	{
		Skip,
		PreferLocal,
		PreferShare
	}
	public enum PayloadResolution
	{
		Local,
		Share
	}
	public static class PayloadConflictResolve
	{
		public static bool IsAuto(PayloadConflictPolicy policy)
		{
			return policy != PayloadConflictPolicy.Skip;
		}

		public static PayloadResolution Winner(PayloadConflictPolicy policy)
		{
			if (policy != PayloadConflictPolicy.PreferLocal)
			{
				return PayloadResolution.Share;
			}
			return PayloadResolution.Local;
		}

		public static bool Gate(PayloadConflictPolicy policy, bool atBoot, bool alreadyPending)
		{
			if (IsAuto(policy) && atBoot)
			{
				return !alreadyPending;
			}
			return false;
		}

		public static bool TryParseResolution(string? text, out PayloadResolution resolution)
		{
			resolution = PayloadResolution.Local;
			if (string.IsNullOrEmpty(text))
			{
				return false;
			}
			string a = text.Trim();
			if (string.Equals(a, "local", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "this", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "mine", StringComparison.OrdinalIgnoreCase))
			{
				resolution = PayloadResolution.Local;
				return true;
			}
			if (string.Equals(a, "share", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "remote", StringComparison.OrdinalIgnoreCase) || string.Equals(a, "other", StringComparison.OrdinalIgnoreCase))
			{
				resolution = PayloadResolution.Share;
				return true;
			}
			return false;
		}
	}
	public sealed class PayloadDiff
	{
		public IReadOnlyList<PayloadEntry> Added { get; }

		public IReadOnlyList<PayloadEntry> Changed { get; }

		public IReadOnlyList<string> Removed { get; }

		public bool IsEmpty
		{
			get
			{
				if (Added.Count == 0 && Changed.Count == 0)
				{
					return Removed.Count == 0;
				}
				return false;
			}
		}

		public int TouchedCount => Added.Count + Changed.Count + Removed.Count;

		public long TransferBytes => Added.Sum((PayloadEntry e) => e.Size) + Changed.Sum((PayloadEntry e) => e.Size);

		private PayloadDiff(IReadOnlyList<PayloadEntry> added, IReadOnlyList<PayloadEntry> changed, IReadOnlyList<string> removed)
		{
			Added = added;
			Changed = changed;
			Removed = removed;
		}

		public static PayloadDiff Between(PayloadManifest? from, PayloadManifest? to)
		{
			Dictionary<string, PayloadEntry> dictionary = (from?.Entries ?? Array.Empty<PayloadEntry>()).ToDictionary<PayloadEntry, string, PayloadEntry>((PayloadEntry e) => e.RelPath, (PayloadEntry e) => e, StringComparer.Ordinal);
			Dictionary<string, PayloadEntry> want = (to?.Entries ?? Array.Empty<PayloadEntry>()).ToDictionary<PayloadEntry, string, PayloadEntry>((PayloadEntry e) => e.RelPath, (PayloadEntry e) => e, StringComparer.Ordinal);
			List<PayloadEntry> list = new List<PayloadEntry>();
			List<PayloadEntry> list2 = new List<PayloadEntry>();
			foreach (PayloadEntry item in want.Values.OrderBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal))
			{
				if (!dictionary.TryGetValue(item.RelPath, out var value))
				{
					list.Add(item);
				}
				else if (!string.Equals(value.Sha256, item.Sha256, StringComparison.Ordinal))
				{
					list2.Add(item);
				}
			}
			List<string> removed = dictionary.Keys.Where((string k) => !want.ContainsKey(k)).OrderBy<string, string>((string k) => k, StringComparer.Ordinal).ToList();
			return new PayloadDiff(list, list2, removed);
		}

		public IEnumerable<string> Describe()
		{
			return Added.Select((PayloadEntry e) => "  + " + e.RelPath).Concat(Changed.Select((PayloadEntry e) => "  ~ " + e.RelPath)).Concat(Removed.Select((string p) => "  - " + p));
		}
	}
	public static class PayloadGuard
	{
		public const int AbsoluteFloor = 5;

		public const int DefaultMaxDeletePercent = 25;

		public static bool IsMassDelete(int previousCount, int deleteCount, int maxDeletePercent, out string why)
		{
			why = string.Empty;
			int num = Math.Max(0, Math.Min(100, maxDeletePercent));
			if (num >= 100)
			{
				return false;
			}
			if (deleteCount <= 5)
			{
				return false;
			}
			if (previousCount <= 0)
			{
				return false;
			}
			if ((long)deleteCount * 100L <= (long)previousCount * (long)num)
			{
				return false;
			}
			why = $"the plan deletes {deleteCount} of {previousCount} file(s) " + $"(> {num}% and > {5} files). This is far more likely a broken scan than a real " + "uninstall. If it IS deliberate, set [Payload] MaxDeletePercent=100 for one pass.";
			return true;
		}
	}
	public sealed class ScanResult
	{
		public PayloadManifest Manifest { get; }

		public bool TierCacheHit { get; }

		public int FilesHashed { get; }

		public int FilesReused { get; }

		public long BytesHashed { get; }

		public IReadOnlyList<string> Unreadable { get; }

		public bool IsComplete => Unreadable.Count == 0;

		public string Summary => (TierCacheHit ? "prefilter hit (0 files hashed)" : $"{FilesHashed} hashed / {FilesReused} reused ({BytesHashed / 1024} KiB read)") + (IsComplete ? string.Empty : $", {Unreadable.Count} UNREADABLE");

		public ScanResult(PayloadManifest manifest, bool tierCacheHit, int hashed, int reused, long bytesHashed, IReadOnlyList<string>? unreadable = null)
		{
			Manifest = manifest;
			TierCacheHit = tierCacheHit;
			FilesHashed = hashed;
			FilesReused = reused;
			BytesHashed = bytesHashed;
			Unreadable = unreadable ?? Array.Empty<string>();
		}
	}
	public static class PayloadHasher
	{
		public static string HashFile(string path)
		{
			using SHA256 sHA = SHA256.Create();
			using FileStream inputStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 65536);
			return ToHex(sHA.ComputeHash(inputStream));
		}

		public static string HashBytes(byte[] bytes)
		{
			using SHA256 sHA = SHA256.Create();
			return ToHex(sHA.ComputeHash(bytes ?? Array.Empty<byte>()));
		}

		public static IReadOnlyList<FileStat> Walk(string root, PayloadScope scope, ICollection<string>? unreadable = null)
		{
			List<FileStat> list = new List<FileStat>();
			if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
			{
				return list;
			}
			string fullPath = Path.GetFullPath(root);
			foreach (string item in SafeEnumerate(fullPath, fullPath, scope, unreadable))
			{
				string text;
				try
				{
					text = PayloadScope.Normalize(Path.GetFullPath(item).Substring(fullPath.Length));
				}
				catch
				{
					unreadable?.Add(item);
					continue;
				}
				if (text.Length == 0 || scope.IsExcluded(text))
				{
					continue;
				}
				try
				{
					FileInfo fileInfo = new FileInfo(item);
					if (fileInfo.Exists)
					{
						list.Add(new FileStat(text, fileInfo.Length, fileInfo.LastWriteTimeUtc.Ticks));
					}
				}
				catch
				{
					unreadable?.Add(text);
				}
			}
			return list.OrderBy<FileStat, string>((FileStat s) => s.RelPath, StringComparer.Ordinal).ToList();
		}

		public static ScanResult Scan(PayloadTier tier, string root, IReadOnlyList<FileStat> stats, PayloadScanCache cache, IReadOnlyDictionary<string, string>? modVersions = null, bool force = false, IReadOnlyList<string>? walkUnreadable = null)
		{
			string statSignature = PayloadScanCache.StatSignature(stats);
			bool flag = walkUnreadable != null && walkUnreadable.Count > 0;
			if (!force && !flag && cache.TryReuseTier(tier, statSignature, out string fingerprint))
			{
				List<PayloadEntry> list = new List<PayloadEntry>(stats.Count);
				bool flag2 = true;
				foreach (FileStat stat in stats)
				{
					if (!cache.TryReuse(tier, stat, out string sha))
					{
						flag2 = false;
						break;
					}
					list.Add(new PayloadEntry(stat.RelPath, stat.Size, sha));
				}
				if (flag2)
				{
					PayloadManifest payloadManifest = new PayloadManifest(tier, list, modVersions);
					if (string.Equals(payloadManifest.Fingerprint, fingerprint, StringComparison.Ordinal))
					{
						return new ScanResult(payloadManifest, tierCacheHit: true, 0, list.Count, 0L);
					}
				}
			}
			List<PayloadEntry> list2 = new List<PayloadEntry>(stats.Count);
			List<string> list3 = null;
			int num = 0;
			int num2 = 0;
			long num3 = 0L;
			foreach (FileStat stat2 in stats)
			{
				string sha3;
				if (!force && cache.TryReuse(tier, stat2, out string sha2))
				{
					sha3 = sha2;
					num2++;
				}
				else
				{
					try
					{
						sha3 = HashFile(Path.Combine(root, stat2.RelPath.Replace('/', Path.DirectorySeparatorChar)));
					}
					catch
					{
						(list3 ?? (list3 = new List<string>())).Add(stat2.RelPath);
						continue;
					}
					num++;
					num3 += stat2.Size;
					cache.Record(tier, stat2, sha3);
				}
				list2.Add(new PayloadEntry(stat2.RelPath, stat2.Size, sha3));
			}
			if (flag)
			{
				(list3 ?? (list3 = new List<string>())).AddRange(walkUnreadable);
			}
			PayloadManifest payloadManifest2 = new PayloadManifest(tier, list2, modVersions);
			if (list3 == null)
			{
				cache.RecordTier(tier, statSignature, payloadManifest2.Fingerprint, stats);
			}
			return new ScanResult(payloadManifest2, tierCacheHit: false, num, num2, num3, list3);
		}

		private static IEnumerable<string> SafeEnumerate(string root, string rootFull, PayloadScope? scope, ICollection<string>? unreadable)
		{
			Stack<string> pending = new Stack<string>();
			pending.Push(root);
			while (pending.Count > 0)
			{
				string text = pending.Pop();
				string[] files;
				string[] dirs;
				try
				{
					files = Directory.GetFiles(text);
					dirs = Directory.GetDirectories(text);
				}
				catch
				{
					if (unreadable != null)
					{
						string item;
						try
						{
							item = PayloadScope.Normalize(Path.GetFullPath(text).Substring(rootFull.Length)) + "/**";
						}
						catch
						{
							item = text + "/**";
						}
						unreadable.Add(item);
					}
					continue;
				}
				string[] array = files;
				for (int i = 0; i < array.Length; i++)
				{
					yield return array[i];
				}
				string[] array2 = dirs;
				foreach (string text2 in array2)
				{
					if (scope == null || !IsExcludedDir(text2, rootFull, scope))
					{
						pending.Push(text2);
					}
				}
			}
		}

		private static bool IsExcludedDir(string dir, string rootFull, PayloadScope scope)
		{
			try
			{
				string text = PayloadScope.Normalize(Path.GetFullPath(dir).Substring(rootFull.Length));
				return text.Length > 0 && scope.IsExcluded(text);
			}
			catch
			{
				return false;
			}
		}

		private static string ToHex(byte[] hash)
		{
			char[] array = new char[hash.Length * 2];
			for (int i = 0; i < hash.Length; i++)
			{
				array[i * 2] = "0123456789abcdef"[hash[i] >> 4];
				array[i * 2 + 1] = "0123456789abcdef"[hash[i] & 0xF];
			}
			return new string(array);
		}
	}
	public sealed class PayloadLedgerEntry
	{
		public int Gen { get; }

		public string Fingerprint { get; }

		public string Device { get; }

		public string PublishedUtc { get; }

		public PayloadLedgerEntry(int gen, string fingerprint, string device, string publishedUtc)
		{
			Gen = gen;
			Fingerprint = fingerprint ?? string.Empty;
			Device = device ?? string.Empty;
			PublishedUtc = publishedUtc ?? string.Empty;
		}
	}
	public sealed class PayloadLedger
	{
		private readonly Dictionary<PayloadTier, PayloadLedgerEntry> _entries = new Dictionary<PayloadTier, PayloadLedgerEntry>();

		public PayloadLedgerEntry? Get(PayloadTier tier)
		{
			if (!_entries.TryGetValue(tier, out PayloadLedgerEntry value))
			{
				return null;
			}
			return value;
		}

		public void Set(PayloadTier tier, PayloadLedgerEntry entry)
		{
			_entries[tier] = entry;
		}

		public string Serialize()
		{
			return string.Join("\n", PayloadTiers.All.Where((PayloadTier t) => _entries.ContainsKey(t)).Select(delegate(PayloadTier t)
			{
				PayloadLedgerEntry payloadLedgerEntry = _entries[t];
				return PayloadTiers.Name(t) + "|" + payloadLedgerEntry.Gen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(payloadLedgerEntry.Fingerprint) + "|" + Escape(payloadLedgerEntry.Device) + "|" + Escape(payloadLedgerEntry.PublishedUtc);
			}));
		}

		public static PayloadLedger Parse(string? text)
		{
			PayloadLedger payloadLedger = new PayloadLedger();
			if (string.IsNullOrEmpty(text))
			{
				return payloadLedger;
			}
			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 && PayloadTiers.TryParse(array2[0], out var tier) && int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						payloadLedger._entries[tier] = new PayloadLedgerEntry(result, Unescape(array2[2]), Unescape(array2[3]), (array2.Length >= 5) ? Unescape(array2[4]) : string.Empty);
					}
				}
			}
			return payloadLedger;
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public sealed class PayloadEntry
	{
		public string RelPath { get; }

		public long Size { get; }

		public string Sha256 { get; }

		public PayloadEntry(string relPath, long size, string sha256)
		{
			RelPath = PayloadScope.Normalize(relPath);
			Size = size;
			Sha256 = (sha256 ?? string.Empty).ToLowerInvariant();
		}
	}
	public sealed class PayloadManifest
	{
		public const int FormatVersion = 1;

		public PayloadTier Tier { get; }

		public IReadOnlyList<PayloadEntry> Entries { get; }

		public IReadOnlyDictionary<string, string> ModVersions { get; }

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

		public long TotalBytes => Entries.Sum((PayloadEntry e) => e.Size);

		public string Fingerprint => Fnv1a.HashLines(Entries.Select((PayloadEntry e) => e.RelPath + "|" + e.Sha256));

		public PayloadManifest(PayloadTier tier, IEnumerable<PayloadEntry> entries, IReadOnlyDictionary<string, string>? modVersions = null)
		{
			Tier = tier;
			Entries = (from g in (entries ?? Array.Empty<PayloadEntry>()).Where((PayloadEntry e) => e != null && e.RelPath.Length > 0).GroupBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal)
				select g.First()).OrderBy<PayloadEntry, string>((PayloadEntry e) => e.RelPath, StringComparer.Ordinal).ToList();
			ModVersions = modVersions ?? new Dictionary<string, string>(StringComparer.Ordinal);
		}

		public static PayloadManifest Empty(PayloadTier tier)
		{
			return new PayloadManifest(tier, Array.Empty<PayloadEntry>());
		}

		public PayloadEntry? Get(string relPath)
		{
			string norm = PayloadScope.Normalize(relPath);
			return Entries.FirstOrDefault((PayloadEntry e) => string.Equals(e.RelPath, norm, StringComparison.Ordinal));
		}

		public string Serialize()
		{
			List<string> list = new List<string> { "V|" + 1.ToString(CultureInfo.InvariantCulture) + "|" + PayloadTiers.Name(Tier) };
			foreach (KeyValuePair<string, string> item in ModVersions.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> k) => k.Key, StringComparer.Ordinal))
			{
				list.Add("M|" + Escape(item.Key) + "|" + Escape(item.Value));
			}
			foreach (PayloadEntry entry in Entries)
			{
				list.Add("F|" + Escape(entry.RelPath) + "|" + entry.Size.ToString(CultureInfo.InvariantCulture) + "|" + entry.Sha256);
			}
			return string.Join("\n", list);
		}

		public static PayloadManifest? Parse(string? text, PayloadTier fallbackTier)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			PayloadTier tier = fallbackTier;
			List<PayloadEntry> list = new List<PayloadEntry>();
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			bool flag = false;
			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 || text2[0] == '#')
				{
					continue;
				}
				string[] array2 = text2.Split(new char[1] { '|' });
				if (array2.Length < 2)
				{
					continue;
				}
				switch (array2[0])
				{
				case "V":
				{
					if (!int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						return null;
					}
					if (result2 > 1)
					{
						return null;
					}
					if (array2.Length >= 3 && PayloadTiers.TryParse(array2[2], out var tier2))
					{
						tier = tier2;
					}
					flag = true;
					break;
				}
				case "M":
					if (array2.Length >= 3)
					{
						dictionary[Unescape(array2[1])] = Unescape(array2[2]);
					}
					break;
				case "F":
				{
					if (array2.Length >= 4 && long.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						list.Add(new PayloadEntry(Unescape(array2[1]), result, array2[3]));
					}
					break;
				}
				}
			}
			if (!flag)
			{
				return null;
			}
			return new PayloadManifest(tier, list, dictionary);
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public enum PayloadAction
	{
		UpToDate,
		Pull,
		Push,
		Conflict,
		ShareEmpty
	}
	public sealed class PayloadPlan
	{
		public PayloadTier Tier { get; }

		public PayloadAction Action { get; }

		public int NewGen { get; }

		public string LocalFingerprint { get; }

		public string ShareFingerprint { get; }

		public string Reason { get; }

		public PayloadPlan(PayloadTier tier, PayloadAction action, int newGen, string localFingerprint, string shareFingerprint, string reason)
		{
			Tier = tier;
			Action = action;
			NewGen = newGen;
			LocalFingerprint = localFingerprint ?? string.Empty;
			ShareFingerprint = shareFingerprint ?? string.Empty;
			Reason = reason ?? string.Empty;
		}
	}
	public static class PayloadReconciler
	{
		public static PayloadPlan Plan(PayloadTier tier, string? localFingerprint, bool localScanned, PayloadManifest? shareManifest, PayloadLedgerEntry? ledger, PayloadTierState? state)
		{
			string text = localFingerprint ?? string.Empty;
			string text2 = shareManifest?.Fingerprint ?? string.Empty;
			int num = ledger?.Gen ?? 0;
			int num2 = state?.BasedOnGen ?? 0;
			if (shareManifest == null || shareManifest.IsEmpty || num <= 0 || text2.Length <= 0)
			{
				if (!localScanned || text.Length == 0)
				{
					return Make(tier, PayloadAction.ShareEmpty, num, text, text2, "share has no published payload and this device has nothing scanned");
				}
				return Make(tier, PayloadAction.Push, Math.Max(num, num2) + 1, text, text2, "first publish — share has no payload for this tier");
			}
			if (!localScanned)
			{
				return Make(tier, PayloadAction.ShareEmpty, num, text, text2, "local tier could not be scanned — refusing to act");
			}
			if (string.Equals(text, text2, StringComparison.Ordinal))
			{
				return Make(tier, PayloadAction.UpToDate, Math.Max(num, num2), text, text2, "fingerprints match");
			}
			if (state == null)
			{
				return Make(tier, PayloadAction.Conflict, num, text, text2, "no lineage record and both sides hold a payload");
			}
			bool flag = !string.Equals(text, state.LastPushedFingerprint, StringComparison.Ordinal);
			bool flag2 = num > num2;
			if (flag && !flag2)
			{
				return Make(tier, PayloadAction.Push, num2 + 1, text, text2, "this device changed since its last publish");
			}
			if (!flag && flag2)
			{
				return Make(tier, PayloadAction.Pull, num, text, text2, "share advanced to gen " + num + " and this device is unchanged");
			}
			if (!flag && !flag2)
			{
				if (num < num2)
				{
					return Make(tier, PayloadAction.Conflict, num, text, text2, "ledger generation went backwards (share gen " + num + " < basedOn " + num2 + ")");
				}
				if (tier == PayloadTier.Config)
				{
					return Make(tier, PayloadAction.UpToDate, Math.Max(num, num2), text, text2, "converged — this device reproduces its last sync and the share is unchanged");
				}
				return Make(tier, PayloadAction.Conflict, num, text, text2, "share fingerprint changed without a generation bump");
			}
			return Make(tier, PayloadAction.Conflict, num, text, text2, "both this device and the share changed");
		}

		private static PayloadPlan Make(PayloadTier tier, PayloadAction action, int gen, string localFp, string shareFp, string reason)
		{
			return new PayloadPlan(tier, action, gen, localFp, shareFp, reason);
		}
	}
	public sealed class FileStat
	{
		public string RelPath { get; }

		public long Size { get; }

		public long MtimeTicks { get; }

		public FileStat(string relPath, long size, long mtimeTicks)
		{
			RelPath = PayloadScope.Normalize(relPath);
			Size = size;
			MtimeTicks = mtimeTicks;
		}
	}
	public sealed class PayloadScanCache
	{
		private sealed class Row
		{
			public long Size;

			public long MtimeTicks;

			public string Sha256 = string.Empty;
		}

		private readonly object _gate = new object();

		private readonly Dictionary<string, Row> _rows = new Dictionary<string, Row>(StringComparer.Ordinal);

		private readonly Dictionary<PayloadTier, string> _statSig = new Dictionary<PayloadTier, string>();

		private readonly Dictionary<PayloadTier, string> _fingerprint = new Dictionary<PayloadTier, string>();

		public static string StatSignature(IEnumerable<FileStat> stats)
		{
			return Fnv1a.HashLines((stats ?? Array.Empty<FileStat>()).Select((FileStat s) => s.RelPath + "|" + s.Size.ToString(CultureInfo.InvariantCulture) + "|" + s.MtimeTicks.ToString(CultureInfo.InvariantCulture)));
		}

		public bool TryReuseTier(PayloadTier tier, string statSignature, out string fingerprint)
		{
			lock (_gate)
			{
				fingerprint = string.Empty;
				if (!_statSig.TryGetValue(tier, out string value) || !string.Equals(value, statSignature, StringComparison.Ordinal))
				{
					return false;
				}
				if (!_fingerprint.TryGetValue(tier, out string value2) || value2.Length == 0)
				{
					return false;
				}
				fingerprint = value2;
				return true;
			}
		}

		public bool TryReuse(PayloadTier tier, FileStat stat, out string sha256)
		{
			lock (_gate)
			{
				sha256 = string.Empty;
				if (!_rows.TryGetValue(Key(tier, stat.RelPath), out Row value))
				{
					return false;
				}
				if (value.Size != stat.Size || value.MtimeTicks != stat.MtimeTicks)
				{
					return false;
				}
				if (value.Sha256.Length == 0)
				{
					return false;
				}
				sha256 = value.Sha256;
				return true;
			}
		}

		public void Record(PayloadTier tier, FileStat stat, string sha256)
		{
			lock (_gate)
			{
				_rows[Key(tier, stat.RelPath)] = new Row
				{
					Size = stat.Size,
					MtimeTicks = stat.MtimeTicks,
					Sha256 = (sha256 ?? string.Empty).ToLowerInvariant()
				};
			}
		}

		public void RecordTier(PayloadTier tier, string statSignature, string fingerprint, IEnumerable<FileStat> present)
		{
			lock (_gate)
			{
				_statSig[tier] = statSignature ?? string.Empty;
				_fingerprint[tier] = fingerprint ?? string.Empty;
				HashSet<string> keep = new HashSet<string>((present ?? Array.Empty<FileStat>()).Select((FileStat s) => Key(tier, s.RelPath)), StringComparer.Ordinal);
				string prefix = PayloadTiers.Name(tier) + "|";
				foreach (string item in _rows.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.Ordinal) && !keep.Contains(k)).ToList())
				{
					_rows.Remove(item);
				}
			}
		}

		public void Invalidate(PayloadTier tier)
		{
			lock (_gate)
			{
				_statSig.Remove(tier);
				_fingerprint.Remove(tier);
				string prefix = PayloadTiers.Name(tier) + "|";
				foreach (string item in _rows.Keys.Where((string k) => k.StartsWith(prefix, StringComparison.Ordinal)).ToList())
				{
					_rows.Remove(item);
				}
			}
		}

		public string Serialize()
		{
			lock (_gate)
			{
				List<string> list = new List<string>();
				PayloadTier[] all = PayloadTiers.All;
				foreach (PayloadTier payloadTier in all)
				{
					if (_statSig.TryGetValue(payloadTier, out string value))
					{
						_fingerprint.TryGetValue(payloadTier, out string value2);
						list.Add("T|" + PayloadTiers.Name(payloadTier) + "|" + value + "|" + (value2 ?? string.Empty));
					}
				}
				foreach (KeyValuePair<string, Row> item in _rows.OrderBy<KeyValuePair<string, Row>, string>((KeyValuePair<string, Row> k) => k.Key, StringComparer.Ordinal))
				{
					int num = item.Key.IndexOf('|');
					string text = item.Key.Substring(0, num);
					string s = item.Key.Substring(num + 1);
					list.Add("R|" + text + "|" + Escape(s) + "|" + item.Value.Size.ToString(CultureInfo.InvariantCulture) + "|" + item.Value.MtimeTicks.ToString(CultureInfo.InvariantCulture) + "|" + item.Value.Sha256);
				}
				return string.Join("\n", list);
			}
		}

		public static PayloadScanCache Parse(string? text)
		{
			PayloadScanCache payloadScanCache = new PayloadScanCache();
			if (string.IsNullOrEmpty(text))
			{
				return payloadScanCache;
			}
			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 >= 3 && PayloadTiers.TryParse(array2[1], out var tier))
				{
					long result;
					long result2;
					if (array2[0] == "T")
					{
						payloadScanCache._statSig[tier] = array2[2];
						payloadScanCache._fingerprint[tier] = ((array2.Length >= 4) ? array2[3] : string.Empty);
					}
					else if (array2[0] == "R" && array2.Length >= 6 && long.TryParse(array2[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out result) && long.TryParse(array2[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
					{
						payloadScanCache._rows[Key(tier, Unescape(array2[2]))] = new Row
						{
							Size = result,
							MtimeTicks = result2,
							Sha256 = array2[5]
						};
					}
				}
			}
			return payloadScanCache;
		}

		private static string Key(PayloadTier tier, string relPath)
		{
			return PayloadTiers.Name(tier) + "|" + relPath;
		}

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

		private static string Unescape(string s)
		{
			return s.Replace("%7C", "|");
		}
	}
	public sealed class PayloadScope
	{
		public static readonly string[] ExcludedNames = new string[20]
		{
			"*.cfg", "*.log", "*_cmd.txt", "bw_pets_*", "bw_registries", "ck_expeditions*", "bw_expeditions*", "*.bak*", "SaveGames*", "OptionSettings*",
			"*Keymappings*", "cloudward_state.txt", "cloudward_payload_state.txt", "cloudward_payload_cache.txt", ".cloudward-*", "cloudward-staged", ".owner.lock", ".payload-ledger", ".sync-backups", ".cloudward-disable"
		};

		public static readonly string[] ExcludedPaths = new string[4] { "BepInEx/config", "BepInEx/cache", "BepInEx/cloudward-staged", "SaveGames" };

		private readonly string[] _extra;

		public IReadOnlyList<string> ActivePatterns => ExcludedNames.Concat(_extra).ToList();

		public PayloadScope(IEnumerable<string>? extra = null)
		{
			_extra = (from p in extra ?? Array.Empty<string>()
				select (p ?? string.Empty).Trim() into p
				where p.Length > 0
				select p).ToArray();
		}

		public static PayloadScope FromConfig(string? extraExcludes)
		{
			return new PayloadScope((extraExcludes ?? string.Empty).Split(new char[1] { ';' }));
		}

		public bool IsExcluded(string? relPath)
		{
			string text = Normalize(relPath);
			if (text.Length == 0)
			{
				return true;
			}
			string[] excludedPaths = ExcludedPaths;
			foreach (string text2 in excludedPaths)
			{
				if (text.Equals(text2, StringComparison.OrdinalIgnoreCase) || text.StartsWith(text2 + "/", StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			excludedPaths = text.Split(new char[1] { '/' });
			foreach (string text3 in excludedPaths)
			{
				if (text3.Length == 0)
				{
					continue;
				}
				string[] excludedNames = ExcludedNames;
				for (int j = 0; j < excludedNames.Length; j++)
				{
					if (GlobMatch(excludedNames[j], text3))
					{
						return true;
					}
				}
				excludedNames = _extra;
				for (int j = 0; j < excludedNames.Length; j++)
				{
					if (GlobMatch(excludedNames[j], text3))
					{
						return true;
					}
				}
			}
			return false;
		}

		public IReadOnlyList<string> Filter(IEnumerable<string> relPaths)
		{
			return (from p in (relPaths ?? Array.Empty<string>()).Select(Normalize)
				where p.Length > 0 && !IsExcluded(p)
				select p).OrderBy<string, string>((string p) => p, StringComparer.Ordinal).ToList();
		}

		public static string Normalize(string? relPath)
		{
			return (relPath ?? string.Empty).Replace('\\', '/').Trim(new char[1] { '/' });
		}

		public static string ToNative(string? relPath)
		{
			return (relPath ?? string.Empty).Replace('/', Path.DirectorySeparatorChar);
		}

		public static bool GlobMatch(string pattern, string text)
		{
			int i = 0;
			int num = 0;
			int num2 = -1;
			int num3 = 0;
			while (num < text.Length)
			{
				if (i < pattern.Length && (pattern[i] == '?' || Same(pattern[i], text[num])))
				{
					i++;
					num++;
					continue;
				}
				if (i < pattern.Length && pattern[i] == '*')
				{
					num2 = i++;
					num3 = num;
					continue;
				}
				if (num2 >= 0)
				{
					i = num2 + 1;
					num = ++num3;
					continue;
				}
				return false;
			}
			for (; i < pattern.Length && pattern[i] == '*'; i++)
			{
			}
			return i == pattern.Length;
		}

		private static bool Same(char a, char b)
		{
			return char.ToUpperInvariant(a) == char.ToUpperInvariant(b);
		}
	}
	public enum FileOpKind
	{
		Copy,
		Delete
	}
	public sealed class FileOp
	{
		public FileOpKind Kind { get; }

		public string RelPath { get; }

		public string Sha256 { get; }

		public FileOp(FileOpKind kind, string relPath, string sha256)
		{
			Kind = kind;
			RelPath = PayloadScope.Normalize(relPath);
			Sha256 = (sha256 ?? string.Empty).ToLowerInvariant();
		}
	}
	public sealed class CfgOp
	{
		public string Guid { get; }

		public CfgKey Key { get; }

		public string Value { get; }

		public string CfgRelPath => "BepInEx/config/" + Guid + ".cfg";

		public CfgOp(string guid, CfgKey key, string value)
		{
			Guid = guid ?? string.Empty;
			Key = key;
			Value = value ?? string.Empty;
		}
	}
	public sealed class StagePlan
	{
		public const int FormatVersion = 1;

		public const string FilesDir = "files";

		public const string PlanFileName = "stage.plan";

		public const string PluginsRoot = "BepInEx/plugins";

		public const string ConfigRoot = "BepInEx/config";

		public IReadOnlyList<PayloadTier> Tiers { get; }

		public IReadOnlyList<FileOp> FileOps { get; }

		public IReadOnlyList<CfgOp> CfgOps { get; }

		public IReadOnlyDictionary<PayloadTier, int> Generations { get; }

		public bool IsEmpty
		{
			get
			{
				if (FileOps.Count == 0)
				{
					return CfgOps.Count == 0;
				}
				return false;
			}
		}

		public StagePlan(IEnumerable<PayloadTier> tiers, IEnumerable<FileOp> fileOps, IEnumerable<CfgOp> cfgOps, IReadOnlyDictionary<PayloadTier, int>? generations = null)
		{
			Tiers = (from t in (tiers ?? Array.Empty<PayloadTier>()).Distinct()
				orderby (int)t
				select t).ToList();
			FileOps = (fileOps ?? Array.Empty<FileOp>()).OrderBy<FileOp, string>((FileOp o) => o.RelPath, StringComparer.Ordinal).ToList();
			CfgOps = (cfgOps ?? Array.Empty<CfgOp>()).OrderBy<CfgOp, string>((CfgOp o) => o.Guid, StringComparer.Ordinal).ThenBy((CfgOp o) => o.Key).ToList();
			Generations = generations ?? new Dictionary<PayloadTier, int>();
			Validate();
		}

		private void Validate()
		{
			PayloadScope payloadScope = new PayloadScope();
			foreach (FileOp fileOp in FileOps)
			{
				if (fileOp.RelPath.Length == 0)
				{
					throw new InvalidDataException("stage op with an empty path");
				}
				if (fileOp.RelPath.Contains(".."))
				{
					throw new InvalidDataException("stage op escapes the tree: " + fileOp.RelPath);
				}
				if (!fileOp.RelPath.StartsWith("BepInEx/plugins/", StringComparison.OrdinalIgnoreCase))
				{
					throw new InvalidDataException("stage op outside BepInEx/plugins/: " + fileOp.RelPath);
				}
				string relPath = fileOp.RelPath.Substring("BepInEx/plugins".Length + 1);
				if (payloadScope.IsExcluded(relPath))
				{
					throw new InvalidDataException("stage op names an excluded path: " + fileOp.RelPath);
				}
				if (fileOp.Kind == FileOpKind.Copy && fileOp.Sha256.Length != 64)
				{
					throw new InvalidDataException("copy op without a usable sha256: " + fileOp.RelPath);
				}
			}
			foreach (CfgOp cfgOp in CfgOps)
			{
				if (cfgOp.Guid.Length == 0 || cfgOp.Guid.IndexOfAny(new char[3] { '/', '\\', '.' }) == 0)
				{
					throw new InvalidDataException("cfg op with a bad guid: \"" + cfgOp.Guid + "\"");
				}
				if (cfgOp.Guid.Contains("..") || cfgOp.Guid.IndexOf('/') >= 0 || cfgOp.Guid.IndexOf('\\') >= 0)
				{
					throw new InvalidDataException("cfg op escapes the config dir: \"" + cfgOp.Guid + "\"");
				}
			}
		}

		public string Serialize()
		{
			List<string> list = new List<string> { "V|" + 1.ToString(CultureInfo.InvariantCulture) };
			foreach (PayloadTier tier in Tiers)
			{
				Generations.TryGetValue(tier, out var value);
				list.Add("T|" + PayloadTiers.Name(tier) + "|" + value.ToString(CultureInfo.InvariantCulture));
			}
			foreach (FileOp fileOp in FileOps)
			{
				list.Add(((fileOp.Kind == FileOpKind.Copy) ? "C|" : "D|") + Escape(fileOp.RelPath) + ((fileOp.Kind == FileOpKind.Copy) ? ("|" + fileOp.Sha256) : string.Empty));
			}
			foreach (CfgOp cfgOp in CfgOps)
			{
				list.Add("K|" + Escape(cfgOp.Guid) + "|" + Escape(cfgOp.Key.Section) + "|" + Escape(cfgOp.Key.Key) + "|" + Escape(cfgOp.Value));
			}
			return string.Join("\n", list);
		}

		public static StagePlan? Parse(string? text)
		{
			if (string.IsNullOrEmpty(text))
			{
				return null;
			}
			List<PayloadTier> list = new List<PayloadTier>();
			Dictionary<PayloadTier, int> dictionary = new Dictionary<PayloadTier, int>();
			List<FileOp> list2 = new List<FileOp>();
			List<CfgOp> list3 = new List<CfgOp>();
			bool flag = false;
			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 || text2[0] == '#')
				{
					continue;
				}
				string[] array2 = text2.Split(new char[1] { '|' });
				if (array2.Length < 2)
				{
					continue;
				}
				switch (array2[0])
				{
				case "V":
				{
					if (!int.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2))
					{
						return null;
					}
					if (result2 > 1)
					{
						return null;
					}
					flag = true;
					break;
				}
				case "T":
				{
					if (!PayloadTiers.TryParse(array2[1], out var tier))
					{
						return null;
					}
					list.Add(tier);
					if (array2.Length >= 3 && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						dictionary[tier] = result;
					}
					break;
				}
				case "C":
					if (array2.Length < 3)
					{
						return null;
					}
					list2.Add(new FileOp(FileOpKind.Copy, Unescape(array2[1]), array2[2]));
					break;
				case "D":
					list2.Add(new FileOp(FileOpKind.Delete, Unescape(array2[1]), string.Empty));
					break;
				case "K":
					if (array2.Length < 5)
					{
						return null;
					}
					list3.Add(new CfgOp(Unescape(array2[1]), new CfgKey(Unescape(array2[2]), Unescape(array2[3])), Unescape(array2[4])));
					break;
				default:
					return null;
				}
			}
			if (!flag)
			{
				return null;
			}
			try
			{
				return new StagePlan(list, list2, list3, dictionary);
			}
			catch (InvalidDataException)
			{
				return null;
			}
		}

		public static IReadOnlyList<FileOp> PluginOps(PayloadDiff diff)
		{
			List<FileOp> list = new List<FileOp>();
			foreach (PayloadEntry item in diff.Added.Concat(diff.Changed))
			{
				list.Add(new FileOp(FileOpKind.Copy, "BepInEx/plugins/" + item.RelPath, item.Sha256));
			}
			foreach (string item2 in diff.Removed)
			{
				list.Add(new FileOp(FileOpKind.Delete, "BepInEx/plugins/" + item2, string.Empty));
			}
			return list;
		}

		private static string Escape(string s)
		{
			return LineCodec.Escape(s);
		}

		private static string Unescape(string s)
		{
			return LineCodec.Unescape(s);
		}
	}
	public sealed class PayloadTierState
	{
		public int BasedOnGen { get; }

		public string LastPushedFingerprint { get; }

		public PayloadTierState(int basedOnGen, string lastPushedFingerprint)
		{
			BasedOnGen = basedOnGen;
			LastPushedFingerprint = lastPushedFingerprint ?? string.Empty;
		}
	}
	public sealed class PayloadConflictRecord
	{
		public string LocalFingerprint { get; }

		public string ShareFingerprint { get; }

		public int ShareGen { get; }

		public PayloadConflictRecord(string localFingerprint, string shareFingerprint, int shareGen)
		{
			LocalFingerprint = localFingerprint ?? string.Empty;
			ShareFingerprint = shareFingerprint ?? string.Empty;
			ShareGen = shareGen;
		}
	}
	public sealed class PayloadState
	{
		private readonly object _gate = new object();

		private readonly Dictionary<PayloadTier, PayloadTierState> _state = new Dictionary<PayloadTier, PayloadTierState>();

		private readonly Dictionary<PayloadTier, PayloadConflictRecord> _conflicts = new Dictionary<PayloadTier, PayloadConflictRecord>();

		private readonly Dictionary<PayloadTier, PayloadTierState> _staged = new Dictionary<PayloadTier, PayloadTierState>();

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

		public IReadOnlyList<PayloadTier> PendingConflicts
		{
			get
			{
				lock (_gate)
				{
					return _conflicts.Keys.ToList();
				}
			}
		}

		public bool HasConflicts
		{
			get
			{
				lock (_gate)
				{
					return _conflicts.Count > 0;
				}
			}
		}

		public IReadOnlyList<PayloadTier> Staged
		{
			get
			{
				lock (_gate)
				{
					return _staged.Keys.ToList();
				}
			}
		}

		public PayloadTierState? Get(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadTierState value;
				return _state.TryGetValue(t, out value) ? value : null;
			}
		}

		public void Set(PayloadTier t, PayloadTierState s)
		{
			lock (_gate)
			{
				_state[t] = s;
			}
		}

		public PayloadConflictRecord? GetConflict(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadConflictRecord value;
				return _conflicts.TryGetValue(t, out value) ? value : null;
			}
		}

		public void SetConflict(PayloadTier t, PayloadConflictRecord c)
		{
			lock (_gate)
			{
				_conflicts[t] = c;
			}
		}

		public void ClearConflict(PayloadTier t)
		{
			lock (_gate)
			{
				_conflicts.Remove(t);
			}
		}

		public PayloadTierState? GetStaged(PayloadTier t)
		{
			lock (_gate)
			{
				PayloadTierState value;
				return _staged.TryGetValue(t, out value) ? value : null;
			}
		}

		public void SetStaged(PayloadTier t, PayloadTierState s)
		{
			lock (_gate)
			{
				_staged[t] = s;
			}
		}

		public void ClearStaged(PayloadTier t)
		{
			lock (_gate)
			{
				_staged.Remove(t);
			}
		}

		public bool IsIntroduced(string payloadRoot)
		{
			lock (_gate)
			{
				return _introduced.Contains(payloadRoot ?? string.Empty);
			}
		}

		public void MarkIntroduced(string payloadRoot)
		{
			lock (_gate)
			{
				_introduced.Add(payloadRoot ?? string.Empty);
			}
		}

		public string Serialize()
		{
			lock (_gate)
			{
				List<string> list = new List<string>();
				PayloadTier[] all = PayloadTiers.All;
				foreach (PayloadTier payloadTier in all)
				{
					if (_state.TryGetValue(payloadTier, out PayloadTierState value))
					{
						list.Add("S|" + PayloadTiers.Name(payloadTier) + "|" + value.BasedOnGen.ToString(CultureInfo.InvariantCulture) + "|" + Escape(value.LastPushedFingerprint));
					}
					if (_conflicts.TryGetValue(payloadTier, out PayloadConflictRecord value2))
					{
						list.Add("C|" + PayloadTiers.Name(payloadTier) + "|" + Escape(value2.LocalFingerprint) + "|" + Escape(value2.ShareFingerprint) + "|" + value2.ShareGen.ToString(CultureInfo.InvariantCulture));
					}
					if (_staged.TryGetValue(payloadTier, out PayloadTierState value3))
					{
						list.Add("G|" + PayloadTiers.Name(payloadTier) + "|" + Escape(value3.LastPushedFingerprint) + "|" + value3.BasedOnGen.ToString(CultureInfo.InvariantCulture));
					}
				}
				foreach (string item in _introduced.OrderBy<string, string>((string k) => k, StringComparer.Ordinal))
				{
					list.Add("I|" + Escape(item));
				}
				return string.Join("\n", list);
			}
		}

		public static PayloadState Parse(string? text)
		{
			PayloadState payloadState = new PayloadState();
			if (string.IsNullOrEmpty(text))
			{
				return payloadState;
			}
			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;
				}
				PayloadTier tier;
				if (array2[0] == "I")
				{
					payloadState._introduced.Add(Unescape(array2[1]));
				}
				else if (PayloadTiers.TryParse(array2[1], out tier))
				{
					int result2;
					int result3;
					if (array2[0] == "S" && array2.Length >= 4 && int.TryParse(array2[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
					{
						payloadState._state[tier] = new PayloadTierState(result, Unescape(array2[3]));
					}
					else if (array2[0] == "C" && array2.Length >= 5 && int.TryParse(array2[4], NumberStyles.Integer, CultureInfo.InvariantCulture, out result2))
					{
						payloadState._conflicts[tier] = new PayloadConflictRecord(Unescape(array2[2]), Unescape(array2[3]), result2);
					}
					else if (array2[0] == "G" && array2.Length >= 4 && int.TryParse(array2[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out result3))
					{
						payloadState._staged[tier] = new PayloadTierState(result3, Unescape(array2[2]));
					}
				}
			}
			return payloadState;
		}

		private static string Escape(string v)
		{
			return LineCodec.Escape(v);
		}

		private static string Unescape(string v)
		{
			return LineCodec.Unescape(v);
		}
	}
	public static class PayloadStore
	{
		public const string LedgerFileName = ".payload-ledger";

		public const string LockFileName = ".owner.lock";

		public const string ManifestDir = "manifest";

		public const string TreeDir = "tree";

		public const string DevicesDir = "devices";

		public const string BackupDir = ".payload-forks";

		public const int HeartbeatEveryFiles = 25;

		public static string LedgerPath(string payloadRoot)
		{
			return Path.Combine(payloadRoot, ".payload-ledger");
		}

		public static string ManifestPath(string payloadRoot, PayloadTier tier)
		{
			return Path.Combine(payloadRoot, "manifest", PayloadTiers.Name(tier) + ".manifest");
		}

		public static string TreePath(string payloadRoot, PayloadTier tier)
		{
			return Path.Combine(payloadRoot, "tree", PayloadTiers.Name(tier));
		}

		public static string DevicePath(string payloadRoot, string device)
		{
			return Path.Combine(payloadRoot, "devices", AtomicFile.SanitizeForFileName(device) + ".json");
		}

		public static PayloadLedger ReadLedger(string payloadRoot, Action<string>? warn = null)
		{
			string text = LedgerPath(payloadRoot);
			try
			{
				return File.Exists(text) ? PayloadLedger.Parse(File.ReadAllText(text)) : new PayloadLedger();
			}
			catch (Exception ex)
			{
				Warn(warn, "payload ledger at '" + text + "' is unreadable (" + ex.GetType().Name + ": " + ex.Message + ") — reading the share as EMPTY, which means this device will try to republish its whole payload. Check the mount before letting that happen.");
				return new PayloadLedger();
			}
		}

		public static PayloadManifest? ReadManifest(string payloadRoot, PayloadTier tier, Action<string>? warn = null)
		{
			string text = ManifestPath(payloadRoot, tier);
			try
			{
				if (!File.Exists(text))
				{
					return null;
				}
				PayloadManifest payloadManifest = PayloadManifest.Parse(File.ReadAllText(text), tier);
				if (payloadManifest == null)
				{
					Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest at '" + text + "' could not be parsed (written by a newer Cloudward?) — this device will act as if the share published nothing.");
				}
				return payloadManifest;
			}
			catch (Exception ex)
			{
				Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest at '" + text + "' is unreadable (" + ex.GetType().Name + ": " + ex.Message + ") — this device will act as if the share published nothing.");
				return null;
			}
		}

		private static void Warn(Action<string>? warn, string message)
		{
			try
			{
				warn?.Invoke(message);
			}
			catch
			{
			}
		}

		public static void PublishTree(string payloadRoot, PayloadTier tier, string localRoot, PayloadManifest manifest, string tempSuffix, string? backupPath, Action? heartbeat = null)
		{
			string text = TreePath(payloadRoot, tier);
			string text2 = text + tempSuffix;
			try
			{
				if (Directory.Exists(text2))
				{
					Directory.Delete(text2, recursive: true);
				}
				Directory.CreateDirectory(text2);
				List<string> list = null;
				int num = 0;
				foreach (PayloadEntry entry in manifest.Entries)
				{
					if (heartbeat != null && num++ % 25 == 0)
					{
						try
						{
							heartbeat();
						}
						catch
						{
						}
					}
					string text3 = Path.Combine(localRoot, PayloadScope.ToNative(entry.RelPath));
					if (!File.Exists(text3))
					{
						(list ?? (list = new List<string>())).Add(entry.RelPath);
						continue;
					}
					string text4 = Path.Combine(text2, PayloadScope.ToNative(entry.RelPath));
					string directoryName = Path.GetDirectoryName(text4);
					if (!string.IsNullOrEmpty(directoryName))
					{
						Directory.CreateDirectory(directoryName);
					}
					File.Copy(text3, text4, overwrite: true);
				}
				if (list != null)
				{
					throw new FileNotFoundException($"{list.Count} file(s) named by the {PayloadTiers.Name(tier)} manifest vanished between " + "the scan and the publish — publish ABORTED so the share keeps a whole payload (first: " + list[0] + ")");
				}
				SwapDirIntoPlace(text2, text, backupPath);
			}
			catch
			{
				try
				{
					if (Directory.Exists(text2))
					{
						Directory.Delete(text2, recursive: true);
					}
				}
				catch
				{
				}
				try
				{
					if (!Directory.Exists(text) && !string.IsNullOrEmpty(backupPath) && Directory.Exists(backupPath))
					{
						MoveOrCopy(backupPath, text);
					}
				}
				catch
				{
				}
				throw;
			}
			WriteManifest(payloadRoot, tier, manifest, tempSuffix);
		}

		public static bool RepairTreeFromBackup(string payloadRoot, PayloadTier tier, Action<string>? warn = null, string? tempSuffix = null)
		{
			string text = TreePath(payloadRoot, tier) + (string.IsNullOrEmpty(tempSuffix) ? ".tmp-repair" : (tempSuffix + ".repair"));
			try
			{
				string path = ManifestPath(payloadRoot, tier);
				string text2 = TreePath(payloadRoot, tier);
				if (!File.Exists(path) || Directory.Exists(text2))
				{
					return false;
				}
				IReadOnlyList<string> readOnlyList = ListBackups(payloadRoot, tier);
				if (readOnlyList.Count == 0)
				{
					Warn(warn, "the share's " + PayloadTiers.Name(tier) + " manifest names a tree that DOES NOT EXIST and no backup is available — an interrupted publish; peers cannot fetch this tier until somebody publishes again.");
					return false;
				}
				if (Directory.Exists(text))
				{
					Directory.Delete(text, recursive: true);
				}
				CopyDir(Path.Combine(payloadRoot, ".payload-forks", readOnlyList[0]), text);
				if (Directory.Exists(text2))
				{
					try
					{
						Directory.Delete(text, recursive: true);
					}
					catch
					{
					}
					return false;
				}
				Directory.Move(text, text2);
				Warn(warn, "the share's " + PayloadTiers.Name(tier) + " tree was MISSING under a live manifest (interrupted publish) — restored from backup '" + readOnlyList[0] + "'.");
				return true;
			}
			catch (Exception ex)
			{
				try
				{
					if (Directory.Exists(text))
					{
						Directory.Delete(text, recursive: true);
					}
				}
				catch
				{
				}
				Warn(warn, "boot repair of the " + PayloadTiers.Name(tier) + " tree failed (" + ex.GetType().Name + ": " + ex.Message + ").");
				return false;
			}
		}

		public static void WriteManifest(string payloadRoot, PayloadTier tier, PayloadManifest manifest, string tempSuffix)
		{
			string path = ManifestPath(payloadRoot, tier);
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			AtomicFile.WriteAllText(path, manifest.Serialize(), tempSuffix);
		}

		public static void WriteLedger(string payloadRoot, PayloadLedger ledger, string tempSuffix)
		{
			Directory.CreateDirectory(payloadRoot);
			AtomicFile.WriteAllText(LedgerPath(payloadRoot), ledger.Serialize(), tempSuffix);
		}

		public static string ConfigValuesPath(string payloadRoot)
		{
			return Path.Combine(TreePath(payloadRoot, PayloadTier.Config), "values.txt");
		}

		public static void PublishConfigValues(string payloadRoot, CfgValueSet values, PayloadManifest manifest, string tempSuffix)
		{
			string path = ConfigValuesPath(payloadRoot);
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			AtomicFile.WriteAllText(path, values.Serialize(), tempSuffix);
			WriteManifest(payloadRoot, PayloadTier.Config, manifest, tempSuffix);
		}

		public static CfgValueSet ReadConfigValues(string payloadRoot)
		{
			try
			{
				string path = ConfigValuesPath(payloadRoot);
				return File.Exists(path) ? CfgValueSet.Parse(File.ReadAllText(path)) : CfgValueSet.Empty;
			}
			catch
			{
				return CfgValueSet.Empty;
			}
		}

		public static int FetchFiles(string payloadRoot

plugins/Cloudward.dll

Decompiled 3 weeks 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 System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using BepInEx.Logging;
using Cloudward.Core;
using Cloudward.Lock;
using ForgeKit;
using HarmonyLib;
using Microsoft.CodeAnalysis;
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.2.0.0")]
[assembly: AssemblyInformationalVersion("0.2.0+83254ebc1e0f377c73c2296a9412cdb0c1c00f84")]
[assembly: AssemblyProduct("Cloudward")]
[assembly: AssemblyTitle("Cloudward")]
[assembly: AssemblyMetadata("BuildStamp", "83254ebc 2026-08-28")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
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;
		}
	}
}
namespace Cloudward
{
	public enum PayloadRole
	{
		Both,
		Publisher,
		Subscriber
	}
	public enum PayloadApplyMode
	{
		Auto,
		StageOnly
	}
	internal sealed class PayloadCoordinator
	{
		private sealed class PendingStage
		{
			public PayloadTier Tier;

			public PayloadManifest Local;

			public PayloadManifest Share;

			public PayloadPlan Plan;
		}

		private readonly ManualLogSource _log;

		private readonly SyncCoordinator _saves;

		private int _busy;

		private PayloadState _state;

		private PayloadScanCache _cache;

		private SessionLock _lock;

		private volatile bool _bootDone;

		private volatile string _payloadRoot;

		private volatile string _lastSummary = "(not yet scanned)";

		private const int QuitJoinBudgetMs = 15000;

		private volatile object _quitDeadlineBox;

		public string PayloadRoot => _payloadRoot;

		public string LastSummary => _lastSummary;

		private static string Tag => "[CLOUDWARD/PAYLOAD]";

		private DateTime? _quitDeadlineUtc
		{
			get
			{
				return (DateTime?)_quitDeadlineBox;
			}
			set
			{
				_quitDeadlineBox = value;
			}
		}

		public PayloadCoordinator(ManualLogSource log, SyncCoordinator saves)
		{
			_log = log;
			_saves = saves;
		}

		public void TryReconcileAtBoot()
		{
			if (_bootDone)
			{
				return;
			}
			_bootDone = true;
			if (!Enabled(out var why))
			{
				_log.LogMessage((object)(Tag + " inactive — " + why + "."));
				return;
			}
			_state = LoadState();
			_cache = LoadCache();
			ReportStagedOutcome();
			RunInBackground("boot payload reconcile", delegate
			{
				Reconcile(atBoot: true, force: false, null);
			});
		}

		public void ReconcileNow(bool force, PayloadTier? only, bool pushOnly = false, bool stageOnly = false)
		{
			if (!Enabled(out var why))
			{
				_log.LogMessage((object)(Tag + " inactive — " + why + "."));
				return;
			}
			_state = _state ?? LoadState();
			_cache = _cache ?? LoadCache();
			RunInBackground("payload " + (pushOnly ? "push" : (stageOnly ? "stage" : "scan")), delegate
			{
				Reconcile(atBoot: false, force, only, pushOnly, stageOnly);
			});
		}

		private bool Enabled(out string why)
		{
			why = null;
			if (!Plugin.Enable.Value)
			{
				why = "[Sync] Enable is false";
				return false;
			}
			if (!Plugin.PayloadEnable.Value)
			{
				why = "[Payload] Enable is false";
				return false;
			}
			if (string.IsNullOrEmpty((Plugin.MountPath.Value ?? "").Trim()))
			{
				why = "[Sync] MountPath is empty";
				return false;
			}
			if (!Plugin.PayloadSyncPlugins.Value && !Plugin.PayloadSyncConfig.Value)
			{
				why = "both tiers are disabled";
				return false;
			}
			string gameRoot = PayloadScopeResolver.GameRoot;
			if (PayloadApply.KillSwitchPresent(gameRoot))
			{
				why = "'.cloudward-disable' is present in the game root — delete it to re-enable";
				return false;
			}
			if (PayloadScopeResolver.LooksLikeIl2Cpp(gameRoot))
			{
				why = "this looks like an IL2CPP install (GameAssembly.dll / il2cpp_data present)";
				return false;
			}
			return true;
		}

		private void Reconcile(bool atBoot, bool force, PayloadTier? only, bool pushOnly = false, bool stageOnly = false)
		{
			//IL_01e2: Unknown result type (might be due to invalid IL or missing references)
			//IL_021f: Unknown result type (might be due to invalid IL or missing references)
			//IL_017f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0198: Unknown result type (might be due to invalid IL or missing references)
			//IL_018c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0191: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a8: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			string text = (Plugin.MountPath.Value ?? "").Trim();
			if (!_saves.EnsureMountLive(text))
			{
				_lastSummary = "mount not live — nothing scanned";
				_log.LogMessage((object)(Tag + " mount not live; payload sync idle this session."));
				return;
			}
			_payloadRoot = PathRebase.PayloadRoot(text);
			_lock = _lock ?? new SessionLock(_payloadRoot, Plugin.DeviceName.Value, Plugin.OwnPid, _log);
			bool flag = _state.IsIntroduced(_payloadRoot);
			bool flag2 = !flag;
			PayloadScope scope = PayloadScope.FromConfig(Plugin.PayloadExtraExcludes.Value);
			PayloadLedger ledger = PayloadStore.ReadLedger(_payloadRoot, (Action<string>)delegate(string m)
			{
				_log.LogWarning((object)(Tag + " " + m));
			});
			if (flag && AnyTierLooksTorn())
			{
				if (AcquireLock())
				{
					try
					{
						PayloadTier[] all = PayloadTiers.All;
						foreach (PayloadTier val in all)
						{
							PayloadStore.RepairTreeFromBackup(_payloadRoot, val, (Action<string>)delegate(string m)
							{
								_log.LogWarning((object)(Tag + " " + m));
							}, AtomicFile.ShareTempSuffix(Plugin.DeviceName.Value, Plugin.OwnPid));
						}
					}
					finally
					{
						ReleaseLock();
					}
				}
				else
				{
					_log.LogMessage((object)(Tag + " a tier looks torn (manifest without tree) but another device holds the payload lock — likely a publish in flight; skipping the boot repair this pass."));
				}
			}
			List<string> list = new List<string>();
			List<PendingStage> list2 = new List<PendingStage>();
			PayloadTier[] all2 = PayloadTiers.All;
			foreach (PayloadTier val2 in all2)
			{
				if ((!only.HasValue || only.Value == val2) && TierEnabled(val2))
				{
					try
					{
						list.Add(ReconcileTier(val2, scope, ledger, atBoot, force, flag2, pushOnly, stageOnly, list2));
					}
					catch (Exception ex)
					{
						_log.LogError((object)(Tag + " " + PayloadTiers.Name(val2) + ": " + ex.GetType().Name + ": " + ex.Message));
						list.Add(PayloadTiers.Name(val2) + "=error");
					}
				}
			}
			if (list2.Count > 0)
			{
				try
				{
					list.Add(WriteStaging(list2));
				}
				catch (Exception ex2)
				{
					_log.LogError((object)(Tag + " staging failed, nothing was staged: " + ex2.GetType().Name + ": " + ex2.Message));
					try
					{
						if (Directory.Exists(PayloadScopeResolver.StagingRoot))
						{
							Directory.Delete(PayloadScopeResolver.StagingRoot, recursive: true);
						}
					}
					catch
					{
					}
					foreach (PendingStage item in list2)
					{
						_state.ClearStaged(item.Tier);
					}
					list.Add("staging=failed");
				}
			}
			if (flag2)
			{
				_state.MarkIntroduced(_payloadRoot);
				_log.LogMessage((object)(Tag + " FIRST CONTACT with this share — detection only, nothing was changed. The plan above is what the next launch will do. Set [Payload] Enable=false, or create '.cloudward-disable' in the game root, to stop it."));
			}
			SaveState();
			SaveCache();
			PublishCensus();
			_lastSummary = string.Join(" · ", list);
			_log.LogMessage((object)(Tag + " " + _lastSummary));
		}

		private static bool TierEnabled(PayloadTier tier)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			if ((int)tier != 0)
			{
				return Plugin.PayloadSyncConfig.Value;
			}
			return Plugin.PayloadSyncPlugins.Value;
		}

		private string ReconcileTier(PayloadTier tier, PayloadScope scope, PayloadLedger ledger, bool atBoot, bool force, bool detectOnly, bool pushOnly, bool stageOnly, List<PendingStage> pending)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_01dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_0201: Unknown result type (might be due to invalid IL or missing references)
			//IL_0234: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a4: Unknown result type (might be due to invalid IL or missing references)
			//IL_02a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ab: Unknown result type (might be due to invalid IL or missing references)
			//IL_02c6: Expected I4, but got Unknown
			//IL_0387: Unknown result type (might be due to invalid IL or missing references)
			//IL_036c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0314: Unknown result type (might be due to invalid IL or missing references)
			//IL_031a: Invalid comparison between Unknown and I4
			//IL_0327: Unknown result type (might be due to invalid IL or missing references)
			string text = PayloadTiers.Name(tier);
			CfgValueSet val = null;
			bool flag = true;
			PayloadManifest val3;
			string text2;
			if ((int)tier == 0)
			{
				string pluginsRoot = PayloadScopeResolver.PluginsRoot;
				if (!Directory.Exists(pluginsRoot))
				{
					return text + "=no local root";
				}
				List<string> list = new List<string>();
				IReadOnlyList<FileStat> readOnlyList = PayloadHasher.Walk(pluginsRoot, scope, (ICollection<string>)list);
				ScanResult val2 = PayloadHasher.Scan(tier, pluginsRoot, readOnlyList, _cache, PayloadScopeResolver.LoadedModVersions(), force, (IReadOnlyList<string>)list);
				val3 = val2.Manifest;
				text2 = val2.Summary;
				flag = val2.IsComplete;
				if (!flag)
				{
					_log.LogWarning((object)($"{Tag} {text}: {val2.Unreadable.Count} entr(ies) under '{pluginsRoot}' could not be read " + "(first: " + val2.Unreadable[0] + "). This device's manifest is INCOMPLETE, so it will not be published — a manifest missing a file tells every other device to DELETE it. Adoption is unaffected; the next pass re-scans."));
				}
			}
			else
			{
				IReadOnlyList<CfgOverlay> readOnlyList2 = PayloadScopeResolver.LoadOverlays(delegate(string m)
				{
					_log.LogWarning((object)(Tag + " " + m));
				});
				if (readOnlyList2.Count == 0)
				{
					return text + "=no overlays shipped (nothing is declared uniform)";
				}
				IReadOnlyList<string> readOnlyList3 = default(IReadOnlyList<string>);
				val = CfgValueSet.Extract((IEnumerable<CfgOverlay>)readOnlyList2, (Func<string, byte[]>)PayloadScopeResolver.ReadCfg, ref readOnlyList3);
				val3 = val.ToManifest();
				text2 = $"{val.Rows.Count} allowlisted key(s) from {readOnlyList2.Count} overlay(s)";
				if (readOnlyList3.Count > 0)
				{
					flag = false;
					text2 += $", {readOnlyList3.Count} cfg UNREADABLE";
					_log.LogWarning((object)($"{Tag} {text}: {readOnlyList3.Count} cfg file(s) could not be read or parsed " + "(first: " + readOnlyList3[0] + "). The value set is INCOMPLETE, so it will not be published. Adoption is unaffected; the next pass re-extracts."));
				}
			}
			PayloadManifest val4 = PayloadStore.ReadManifest(_payloadRoot, tier, (Action<string>)delegate(string m)
			{
				_log.LogWarning((object)(Tag + " " + m));
			});
			PayloadLedgerEntry val5 = ledger.Get(tier);
			PayloadTierState val6 = _state.Get(tier);
			PayloadPlan val7 = PayloadReconciler.Plan(tier, val3.Fingerprint, true, val4, val5, val6);
			string text3 = $"{text}={val7.Action} ({text2})";
			_log.LogMessage((object)($"{Tag} {text}: {val7.Action} — {val7.Reason}. local={Short(val7.LocalFingerprint)} " + $"share={Short(val7.ShareFingerprint)}@gen{((val5 != null) ? val5.Gen : 0)} [{text2}]"));
			if (detectOnly)
			{
				return text3 + " (detect-only)";
			}
			PayloadAction action = val7.Action;
			switch ((int)action)
			{
			case 4:
				if (val3.IsEmpty)
				{
					goto case 0;
				}
				goto case 2;
			case 0:
				return text3;
			case 2:
				if (stageOnly)
				{
					return text3 + " (push skipped)";
				}
				if (Plugin.PayloadRoleValue.Value == PayloadRole.Subscriber)
				{
					return text3 + " (Role=Subscriber — refusing to write the share)";
				}
				if (!flag)
				{
					return text3 + " (push refused — scan incomplete)";
				}
				if ((int)val7.Action != 2)
				{
					return text3;
				}
				return text3 + " → " + Push(tier, val3, val, val4, val7);
			case 1:
				if (pushOnly)
				{
					return text3 + " (pull deferred to boot)";
				}
				if (Plugin.PayloadRoleValue.Value == PayloadRole.Publisher)
				{
					return text3 + " (Role=Publisher — not adopting)";
				}
				return text3 + " → " + Queue(tier, val3, val4, val7, pending);
			case 3:
				return text3 + " → " + HandleConflict(tier, val3, val, val4, val7, atBoot, pending, flag);
			default:
				return text3;
			}
		}

		private string Push(PayloadTier tier, PayloadManifest local, CfgValueSet configValues, PayloadManifest share, PayloadPlan plan)
		{
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0151: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
			//IL_020e: Unknown result type (might be due to invalid IL or missing references)
			//IL_021f: Unknown result type (might be due to invalid IL or missing references)
			//IL_018e: Unknown result type (might be due to invalid IL or missing references)
			//IL_022a: Unknown result type (might be due to invalid IL or missing references)
			//IL_025a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0285: Unknown result type (might be due to invalid IL or missing references)
			//IL_028f: Expected O, but got Unknown
			//IL_02a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b3: Unknown result type (might be due to invalid IL or missing references)
			//IL_02bd: Expected O, but got Unknown
			//IL_02c3: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d1: Unknown result type (might be due to invalid IL or missing references)
			long num = (long)Math.Max(1, Plugin.PayloadMaxMegabytes.Value) * 1024L * 1024;
			if (local.TotalBytes > num)
			{
				_log.LogWarning((object)(Tag + " refusing to publish " + PayloadTiers.Name(tier) + ": " + $"{local.TotalBytes / 1048576} MiB exceeds [Payload] MaxPayloadMegabytes " + $"({Plugin.PayloadMaxMegabytes.Value}). This usually means a mispointed root."));
				return "refused (too large)";
			}
			if ((int)tier == 0 && share != null)
			{
				int count = PayloadDiff.Between(share, local).Removed.Count;
				string text = default(string);
				if (PayloadGuard.IsMassDelete(share.Entries.Count, count, Plugin.PayloadMaxDeletePercent.Value, ref text))
				{
					_log.LogError((object)(Tag + " " + PayloadTiers.Name(tier) + ": PUSH REFUSED — " + text + " Nothing was written."));
					return "refused (mass delete — see log)";
				}
			}
			if (!AcquireLock())
			{
				return "deferred (another device holds the payload lock)";
			}
			try
			{
				PayloadLedger val = PayloadStore.ReadLedger(_payloadRoot, (Action<string>)delegate(string m)
				{
					_log.LogWarning((object)(Tag + " " + m));
				});
				PayloadLedgerEntry obj = val.Get(tier);
				int num2 = ((obj != null) ? obj.Gen : 0);
				if (num2 >= plan.NewGen)
				{
					_log.LogWarning((object)(Tag + " " + PayloadTiers.Name(tier) + ": PUSH ABANDONED — another device published " + $"gen{num2} while this pass was planning gen{plan.NewGen}. " + "Nothing was written; the next pass will reconcile against the new payload."));
					return "deferred (share advanced during planning)";
				}
				string text2 = AtomicFile.ShareTempSuffix(Plugin.DeviceName.Value, Plugin.OwnPid);
				string text3 = DateTime.UtcNow.ToString("yyyyMMddTHHmmssZ");
				string text4 = PayloadStore.BackupPath(_payloadRoot, tier, plan.NewGen, text3);
				if ((int)tier == 0)
				{
					PayloadStore.PublishTree(_payloadRoot, tier, PayloadScopeResolver.PluginsRoot, local, text2, text4, (Action)delegate
					{
						DateTime? quitDeadlineUtc = _quitDeadlineUtc;
						if (quitDeadlineUtc.HasValue && DateTime.UtcNow > quitDeadlineUtc.Value)
						{
							throw new TimeoutException($"quit-push budget ({15}s) exceeded mid-publish — aborted " + "cleanly (share untouched: temp tree discarded, previous tree restored). The boot push replays it.");
						}
						_lock?.Refresh(DateTime.UtcNow);
					});
				}
				else
				{
					PayloadStore.PublishConfigValues(_payloadRoot, configValues, local, text2);
				}
				val.Set(tier, new PayloadLedgerEntry(plan.NewGen, local.Fingerprint, Plugin.DeviceName.Value, DateTime.UtcNow.ToString("o")));
				PayloadStore.WriteLedger(_payloadRoot, val, text2);
				_state.Set(tier, new PayloadTierState(plan.NewGen, local.Fingerprint));
				_state.ClearConflict(tier);
				PayloadStore.PruneBackups(_payloadRoot, tier, Math.Max(0, Plugin.PayloadKeepBackups.Value));
				return $"published gen{plan.NewGen} ({local.Entries.Count} file(s), {local.TotalBytes / 1024} KiB)";
			}
			finally
			{
				ReleaseLock();
			}
		}

		private string Queue(PayloadTier tier, PayloadManifest local, PayloadManifest share, PayloadPlan plan, List<PendingStage> pending)
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0088: Unknown result type (might be due to invalid IL or missing references)
			//IL_003e: Unknown result type (might be due to invalid IL or missing references)
			string text = ReadPublisherLoaderFingerprint(tier);
			string text2 = PayloadScopeResolver.LoaderFingerprint(PayloadScopeResolver.GameRoot);
			if (text != null && !string.Equals(text, text2, StringComparison.Ordinal))
			{
				_log.LogWarning((object)(Tag + " NOT adopting " + PayloadTiers.Name(tier) + ": the publishing device's BepInEx loader differs from this one's (theirs=" + Short(text) + " ours=" + Short(text2) + "). Bring the loaders into line with the installer first — Cloudward cannot update it."));
				return "refused (loader mismatch)";
			}
			pending.Add(new PendingStage
			{
				Tier = tier,
				Local = local,
				Share = share,
				Plan = plan
			});
			return $"queued for staging at gen{plan.NewGen}";
		}

		private string WriteStaging(List<PendingStage> pending)
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_00df: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Unknown result type (might be due to invalid IL or missing references)
			//IL_0120: Unknown result type (might be due to invalid IL or missing references)
			//IL_025d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0264: Expected O, but got Unknown
			//IL_01da: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f0: Expected O, but got Unknown
			//IL_02be: Unknown result type (might be due to invalid IL or missing references)
			//IL_02d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_02f9: Expected O, but got Unknown
			string stagingRoot = PayloadScopeResolver.StagingRoot;
			try
			{
				if (Directory.Exists(stagingRoot))
				{
					Directory.Delete(stagingRoot, recursive: true);
				}
			}
			catch
			{
			}
			List<FileOp> list = new List<FileOp>();
			List<CfgOp> list2 = new List<CfgOp>();
			Dictionary<PayloadTier, int> dictionary = new Dictionary<PayloadTier, int>();
			HashSet<PayloadTier> refused = new HashSet<PayloadTier>();
			string text = default(string);
			foreach (PendingStage item in pending)
			{
				dictionary[item.Tier] = item.Plan.NewGen;
				if ((int)item.Tier == 0)
				{
					PayloadDiff val = PayloadDiff.Between(item.Local, item.Share);
					if (PayloadGuard.IsMassDelete(item.Local.Entries.Count, val.Removed.Count, Plugin.PayloadMaxDeletePercent.Value, ref text))
					{
						_log.LogError((object)(Tag + " " + PayloadTiers.Name(item.Tier) + ": ADOPTION REFUSED — " + text + " Nothing was staged for this tier."));
						refused.Add(item.Tier);
						dictionary.Remove(item.Tier);
						continue;
					}
					IReadOnlyList<FileOp> readOnlyList = StagePlan.PluginOps(val);
					PayloadStore.FetchFiles(_payloadRoot, item.Tier, (IEnumerable<FileOp>)readOnlyList, stagingRoot);
					list.AddRange(readOnlyList);
					foreach (string item2 in val.Describe().Take(30))
					{
						_log.LogMessage((object)(Tag + " " + item2));
					}
					continue;
				}
				foreach (Row row in PayloadStore.ReadConfigValues(_payloadRoot).Rows)
				{
					list2.Add(new CfgOp(row.Guid, row.Key, row.Value));
				}
			}
			StagePlan val2 = new StagePlan(from p in pending
				where !refused.Contains(p.Tier)
				select p.Tier, (IEnumerable<FileOp>)list, (IEnumerable<CfgOp>)list2, (IReadOnlyDictionary<PayloadTier, int>)dictionary);
			if (val2.IsEmpty)
			{
				try
				{
					if (Directory.Exists(stagingRoot))
					{
						Directory.Delete(stagingRoot, recursive: true);
					}
				}
				catch
				{
				}
				if (refused.Count <= 0)
				{
					return "staging=nothing to do";
				}
				return "staging=refused (mass delete)";
			}
			PayloadStore.WriteStagePlan(stagingRoot, val2);
			foreach (PendingStage item3 in pending)
			{
				if (!refused.Contains(item3.Tier))
				{
					_state.SetStaged(item3.Tier, new PayloadTierState(item3.Plan.NewGen, item3.Plan.ShareFingerprint));
				}
			}
			if (Plugin.PayloadApplyModeValue.Value == PayloadApplyMode.StageOnly)
			{
				File.WriteAllText(Path.Combine(stagingRoot, "hold"), "ApplyMode=StageOnly. Delete this file (or run the 'payloadapply' verb) and restart to install.\n");
				_log.LogMessage((object)(Tag + " staged and HELD ([Payload] ApplyMode=StageOnly) — run 'payloadapply' then restart, or delete " + Path.Combine(stagingRoot, "hold")));
			}
			else
			{
				_log.LogMessage((object)(Tag + " staged for the next launch — restart Outward to apply."));
			}
			return $"staging={list.Count} file op(s), {list2.Count} cfg key(s) across " + string.Join("+", pending.Select((PendingStage p) => PayloadTiers.Name(p.Tier)));
		}

		private string HandleConflict(PayloadTier tier, PayloadManifest local, CfgValueSet configValues, PayloadManifest share, PayloadPlan plan, bool atBoot, List<PendingStage> pending, bool scanComplete = true)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: 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_0036: Expected O, but got Unknown
			//IL_003b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0040: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: 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_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_009e: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c6: Expected O, but got Unknown
			bool flag = _state.GetConflict(tier) != null;
			_state.SetConflict(tier, new PayloadConflictRecord(plan.LocalFingerprint, plan.ShareFingerprint, plan.NewGen));
			PayloadConflictPolicy value = Plugin.PayloadConflictPolicyValue.Value;
			if (!PayloadConflictResolve.Gate(value, atBoot, flag))
			{
				_log.LogWarning((object)(Tag + " " + PayloadTiers.Name(tier) + " DIVERGED — this device and the share both changed. Nothing was touched. Resolve with 'payloadresolve local' or 'payloadresolve share'."));
				return "conflict (waiting for payloadresolve)";
			}
			if ((int)PayloadConflictResolve.Winner(value) != 0)
			{
				return Queue(tier, local, share, plan, pending);
			}
			if (!scanComplete)
			{
				return "conflict (auto-resolve to local refused — scan incomplete)";
			}
			return Push(tier, local, configValues, share, new PayloadPlan(tier, (PayloadAction)2, plan.NewGen + 1, plan.LocalFingerprint, plan.ShareFingerprint, "auto-resolved to local"));
		}

		public void Resolve(string sideArg, string tierArg)
		{
			//IL_0077: Unknown result type (might be due to invalid IL or missing references)
			if (!Enabled(out var why))
			{
				_log.LogMessage((object)(Tag + " inactive — " + why + "."));
				return;
			}
			PayloadResolution side = default(PayloadResolution);
			if (!PayloadConflictResolve.TryParseResolution(sideArg, ref side))
			{
				_log.LogWarning((object)(Tag + " usage: payloadresolve <local|share> [plugins|config]"));
				return;
			}
			PayloadTier value = default(PayloadTier);
			PayloadTier? only = (PayloadTiers.TryParse(tierArg, ref value) ? new PayloadTier?(value) : ((PayloadTier?)null));
			if (_state == null || !_state.HasConflicts)
			{
				_log.LogMessage((object)(Tag + " no conflicts recorded."));
				return;
			}
			RunInBackground("payload resolve", delegate
			{
				//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
				//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
				//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
				//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
				//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
				//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
				//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
				//IL_0270: Unknown result type (might be due to invalid IL or missing references)
				//IL_0125: Unknown result type (might be due to invalid IL or missing references)
				//IL_0112: Unknown result type (might be due to invalid IL or missing references)
				//IL_012f: Expected O, but got Unknown
				//IL_013a: Unknown result type (might be due to invalid IL or missing references)
				//IL_0156: Unknown result type (might be due to invalid IL or missing references)
				//IL_015e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0178: Unknown result type (might be due to invalid IL or missing references)
				string text = (Plugin.MountPath.Value ?? "").Trim();
				if (!_saves.EnsureMountLive(text))
				{
					_log.LogWarning((object)(Tag + " mount not live."));
				}
				else
				{
					_payloadRoot = PathRebase.PayloadRoot(text);
					PayloadLedger ledger = PayloadStore.ReadLedger(_payloadRoot, (Action<string>)null);
					PayloadScope scope = PayloadScope.FromConfig(Plugin.PayloadExtraExcludes.Value);
					List<PendingStage> list = new List<PendingStage>();
					foreach (PayloadTier item in _state.PendingConflicts.ToList())
					{
						if (!only.HasValue || only.Value == item)
						{
							PayloadConflictRecord conflict = _state.GetConflict(item);
							if (conflict != null)
							{
								_state.Set(item, ((int)side == 0) ? new PayloadTierState(conflict.ShareGen, "resolved-take-local") : new PayloadTierState(conflict.ShareGen - 1, conflict.LocalFingerprint));
								_state.ClearConflict(item);
								_log.LogMessage((object)$"{Tag} {PayloadTiers.Name(item)}: resolving in favour of {side}.");
								try
								{
									ReconcileTier(item, scope, ledger, atBoot: false, force: false, detectOnly: false, pushOnly: false, stageOnly: false, list);
								}
								catch (Exception ex)
								{
									_log.LogError((object)(Tag + " resolve failed: " + ex.Message));
								}
							}
						}
					}
					if (list.Count > 0)
					{
						try
						{
							_log.LogMessage((object)(Tag + " " + WriteStaging(list)));
						}
						catch (Exception ex2)
						{
							_log.LogError((object)(Tag + " staging failed, nothing was staged: " + ex2.Message));
							try
							{
								if (Directory.Exists(PayloadScopeResolver.StagingRoot))
								{
									Directory.Delete(PayloadScopeResolver.StagingRoot, recursive: true);
								}
							}
							catch
							{
							}
							foreach (PendingStage item2 in list)
							{
								_state.ClearStaged(item2.Tier);
							}
						}
					}
					SaveState();
				}
			});
		}

		public void Rollback(string tierArg, string indexArg)
		{
			//IL_0062: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Invalid comparison between Unknown and I4
			if (!Enabled(out var why))
			{
				_log.LogMessage((object)(Tag + " inactive — " + why + "."));
				return;
			}
			PayloadTier tier = default(PayloadTier);
			if (!PayloadTiers.TryParse(tierArg, ref tier))
			{
				_log.LogWarning((object)(Tag + " usage: payloadrollback plugins [n]  (n=0 is the newest)"));
				return;
			}
			if ((int)tier == 1)
			{
				_log.LogWarning((object)(Tag + " the config tier has no rollback — config publishes carry no backups (they transport overlay VALUES, not files). Fix the value in the overlay/cfg and 'payloadpush config', or use 'payloadresolve'."));
				return;
			}
			int result;
			int index = (int.TryParse(indexArg, out result) ? result : 0);
			RunInBackground("payload rollback", delegate
			{
				//IL_006d: Unknown result type (might be due to invalid IL or missing references)
				//IL_0096: Unknown result type (might be due to invalid IL or missing references)
				//IL_01c6: Unknown result type (might be due to invalid IL or missing references)
				//IL_01f7: Unknown result type (might be due to invalid IL or missing references)
				//IL_021f: Unknown result type (might be due to invalid IL or missing references)
				//IL_022f: Unknown result type (might be due to invalid IL or missing references)
				//IL_025a: Unknown result type (might be due to invalid IL or missing references)
				//IL_0264: Expected O, but got Unknown
				//IL_029a: Unknown result type (might be due to invalid IL or missing references)
				string text = (Plugin.MountPath.Value ?? "").Trim();
				if (!_saves.EnsureMountLive(text))
				{
					_log.LogWarning((object)(Tag + " mount not live."));
				}
				else
				{
					_payloadRoot = PathRebase.PayloadRoot(text);
					IReadOnlyList<string> readOnlyList = PayloadStore.ListBackups(_payloadRoot, tier);
					if (readOnlyList.Count == 0)
					{
						_log.LogWarning((object)(Tag + " no backups for " + PayloadTiers.Name(tier) + "."));
					}
					else if (index < 0 || index >= readOnlyList.Count)
					{
						_log.LogWarning((object)$"{Tag} backup {index} out of range; available:");
						for (int i = 0; i < readOnlyList.Count; i++)
						{
							_log.LogMessage((object)$"{Tag}   [{i}] {readOnlyList[i]}");
						}
					}
					else
					{
						_lock = _lock ?? new SessionLock(_payloadRoot, Plugin.DeviceName.Value, Plugin.OwnPid, _log);
						if (AcquireLock())
						{
							try
							{
								string text2 = AtomicFile.ShareTempSuffix(Plugin.DeviceName.Value, Plugin.OwnPid);
								PayloadManifest val = PayloadStore.RestoreFromBackup(_payloadRoot, tier, readOnlyList[index], text2);
								PayloadLedger val2 = PayloadStore.ReadLedger(_payloadRoot, (Action<string>)null);
								PayloadLedgerEntry obj = val2.Get(tier);
								int num = ((obj != null) ? obj.Gen : 0) + 1;
								PayloadStore.WriteManifest(_payloadRoot, tier, val, text2);
								val2.Set(tier, new PayloadLedgerEntry(num, val.Fingerprint, Plugin.DeviceName.Value, DateTime.UtcNow.ToString("o")));
								PayloadStore.WriteLedger(_payloadRoot, val2, text2);
								_log.LogMessage((object)($"{Tag} rolled {PayloadTiers.Name(tier)} back to '{readOnlyList[index]}' and republished as gen{num}. " + "Every device will adopt it on its next boot."));
								return;
							}
							finally
							{
								ReleaseLock();
							}
						}
						_log.LogWarning((object)(Tag + " another device holds the payload lock."));
					}
				}
			});
		}

		public void ReleaseHold()
		{
			string stagingRoot = PayloadScopeResolver.StagingRoot;
			string path = Path.Combine(stagingRoot, "hold");
			if (!Directory.Exists(stagingRoot) || !File.Exists(Path.Combine(stagingRoot, "stage.plan")))
			{
				_log.LogMessage((object)(Tag + " nothing is staged."));
				return;
			}
			if (!File.Exists(path))
			{
				_log.LogMessage((object)(Tag + " a payload is staged and not held — restart Outward to apply it."));
				return;
			}
			try
			{
				File.Delete(path);
				_log.LogMessage((object)(Tag + " hold released — restart Outward to install the staged payload."));
			}
			catch (Exception ex)
			{
				_log.LogError((object)(Tag + " could not release the hold (" + ex.Message + ")."));
			}
		}

		public void Status()
		{
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_014b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0152: Unknown result type (might be due to invalid IL or missing references)
			//IL_0160: Unknown result type (might be due to invalid IL or missing references)
			//IL_016e: Unknown result type (might be due to invalid IL or missing references)
			//IL_018e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0237: Unknown result type (might be due to invalid IL or missing references)
			//IL_0241: Unknown result type (might be due to invalid IL or missing references)
			//IL_0272: Unknown result type (might be due to invalid IL or missing references)
			_log.LogMessage((object)(Tag + " enabled=" + (Enabled(out var why) ? "yes" : ("no — " + why))));
			_log.LogMessage((object)($"{Tag}   role={Plugin.PayloadRoleValue.Value} apply={Plugin.PayloadApplyModeValue.Value} " + $"onConflict={Plugin.PayloadConflictPolicyValue.Value} tiers=" + (Plugin.PayloadSyncPlugins.Value ? "plugins " : "") + (Plugin.PayloadSyncConfig.Value ? "config" : "")));
			_log.LogMessage((object)(Tag + "   payload root = " + (_payloadRoot ?? "(mount not resolved yet)")));
			_log.LogMessage((object)(Tag + "   last pass: " + _lastSummary));
			_log.LogMessage((object)(Tag + "   loader fingerprint = " + Short(PayloadScopeResolver.LoaderFingerprint(PayloadScopeResolver.GameRoot))));
			if (_state == null)
			{
				return;
			}
			PayloadTier[] all = PayloadTiers.All;
			foreach (PayloadTier val in all)
			{
				PayloadTierState val2 = _state.Get(val);
				PayloadTierState staged = _state.GetStaged(val);
				PayloadConflictRecord conflict = _state.GetConflict(val);
				_log.LogMessage((object)($"{Tag}   {PayloadTiers.Name(val)}: basedOnGen={((val2 != null) ? val2.BasedOnGen : 0)} " + "lastPushed=" + Short((val2 != null) ? val2.LastPushedFingerprint : null) + ((staged != null) ? $" STAGED gen{staged.BasedOnGen} (restart to apply)" : "") + ((conflict != null) ? " CONFLICT — run 'payloadresolve'" : "")));
			}
			if (_payloadRoot == null)
			{
				return;
			}
			PayloadTier[] all2 = PayloadTiers.All;
			foreach (PayloadTier val3 in all2)
			{
				IReadOnlyList<string> readOnlyList = PayloadStore.ListBackups(_payloadRoot, val3);
				if (readOnlyList.Count > 0)
				{
					_log.LogMessage((object)(Tag + "   " + PayloadTiers.Name(val3) + " backups: " + string.Join(", ", readOnlyList.Take(5))));
				}
			}
		}

		private void ReportStagedOutcome()
		{
			//IL_0302: Unknown result type (might be due to invalid IL or missing references)
			//IL_030d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0317: Expected O, but got Unknown
			//IL_031d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0334: Unknown result type (might be due to invalid IL or missing references)
			//IL_0033: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d4: Invalid comparison between Unknown and I4
			//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00df: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
			//IL_016f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f1: Invalid comparison between Unknown and I4
			//IL_01ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ed: Invalid comparison between Unknown and I4
			//IL_0285: Unknown result type (might be due to invalid IL or missing references)
			//IL_0288: Invalid comparison between Unknown and I4
			//IL_01cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_0193: Unknown result type (might be due to invalid IL or missing references)
			//IL_0114: Unknown result type (might be due to invalid IL or missing references)
			//IL_0131: Unknown result type (might be due to invalid IL or missing references)
			//IL_02cb: Unknown result type (might be due to invalid IL or missing references)
			//IL_02ce: Invalid comparison between Unknown and I4
			//IL_029a: Unknown result type (might be due to invalid IL or missing references)
			//IL_02b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0200: Unknown result type (might be due to invalid IL or missing references)
			//IL_0243: Unknown result type (might be due to invalid IL or missing references)
			//IL_0279: Unknown result type (might be due to invalid IL or missing references)
			IReadOnlyList<PayloadTier> staged = _state.Staged;
			if (staged.Count == 0)
			{
				return;
			}
			bool flag = Directory.Exists(PayloadScopeResolver.StagingRoot);
			ApplyReceipt val = (flag ? null : ApplyReceipt.Consume(PayloadScopeResolver.GameRoot));
			StagedRuling val2 = ApplyReceipt.Judge(val, flag);
			string text = PayloadApply.ApplierVersion();
			if (val != null && val.ApplierVersion.Length > 0 && !string.Equals(val.ApplierVersion, text, StringComparison.Ordinal))
			{
				_log.LogWarning((object)(Tag + " VERSION SKEW: the preloader patcher applied with Cloudward.Core " + val.ApplierVersion + " but this plugin runs " + text + " — BepInEx/patchers/ and BepInEx/plugins/ carry different Cloudward builds; redeploy both."));
			}
			foreach (PayloadTier item in staged)
			{
				PayloadTierState staged2 = _state.GetStaged(item);
				if (staged2 == null)
				{
					continue;
				}
				StagedRuling val3 = ApplyReceipt.Judge(val, (int)val2 == 0, item, staged2.BasedOnGen);
				if (val3 != val2 && (int)val3 == 3 && val != null)
				{
					_log.LogWarning((object)(Tag + " " + PayloadTiers.Name(item) + ": the apply receipt reports " + $"gen{(val.Generations.TryGetValue(item, out var value) ? value : 0)} but " + $"gen{staged2.BasedOnGen} was staged — treating the receipt as STALE."));
				}
				if ((int)val3 == 0)
				{
					if (PayloadApply.HeldForManualApply(PayloadScopeResolver.StagingRoot))
					{
						_log.LogMessage((object)($"{Tag} {PayloadTiers.Name(item)} gen{staged2.BasedOnGen} is staged and HELD " + "([Payload] ApplyMode=StageOnly) — run 'payloadapply' then restart."));
					}
					else
					{
						_log.LogWarning((object)(Tag + " " + PayloadTiers.Name(item) + " is STILL STAGED after a restart — the preloader patcher did not run. Check that BepInEx/patchers/Cloudward.Preload.dll exists (deploy.sh and package-bundle.sh both install it there)."));
					}
					continue;
				}
				if ((int)val3 == 2)
				{
					string text2 = (val.PartiallyApplied ? $"PARTIALLY applied ({val.OpsFailed} op(s) failed after verification) — the install is a MIX" : $"{val.Outcome} — the install was not modified");
					_log.LogError((object)($"{Tag} {PayloadTiers.Name(item)}: staged payload gen{staged2.BasedOnGen} was {text2}. " + "NOT recording it as applied; the next pass will re-adopt from the share (see the preloader's log lines above for why)."));
					_state.ClearStaged(item);
					continue;
				}
				if ((int)val3 == 3)
				{
					_log.LogError((object)(Tag + " " + PayloadTiers.Name(item) + ": the staging tree is gone but the applier left no receipt vouching for THIS stage — deleted out of band, a stale receipt, or BepInEx/patchers/ holds an OLD Cloudward.Preload build that predates apply receipts. NOT recording the payload as applied; the next pass re-checks against the share. Redeploy the patcher if it is old."));
					_state.ClearStaged(item);
					continue;
				}
				string text3 = staged2.LastPushedFingerprint;
				if ((int)item == 1)
				{
					try
					{
						IReadOnlyList<CfgOverlay> readOnlyList = PayloadScopeResolver.LoadOverlays(null);
						text3 = CfgValueSet.Extract((IEnumerable<CfgOverlay>)readOnlyList, (Func<string, byte[]>)PayloadScopeResolver.ReadCfg).ToManifest().Fingerprint;
					}
					catch
					{
					}
				}
				_state.Set(item, new PayloadTierState(staged2.BasedOnGen, text3));
				_state.ClearStaged(item);
				_log.LogMessage((object)$"{Tag} {PayloadTiers.Name(item)}: staged payload gen{staged2.BasedOnGen} was applied at preload.");
			}
			SaveState();
		}

		private void PublishCensus()
		{
			try
			{
				IReadOnlyDictionary<string, string> source = PayloadScopeResolver.LoadedModVersions();
				string text = string.Join(",\n    ", from kv in source.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> kv) => kv.Key, StringComparer.Ordinal)
					select "\"" + kv.Key + "\": \"" + kv.Value + "\"");
				string text2 = "{\n  \"device\": \"" + Plugin.DeviceName.Value + "\",\n" + $"  \"utc\": \"{DateTime.UtcNow:o}\",\n" + "  \"cloudward\": \"0.2.0\",\n  \"loaderFingerprint\": \"" + PayloadScopeResolver.LoaderFingerprint(PayloadScopeResolver.GameRoot) + "\",\n  \"mods\": {\n    " + text + "\n  }\n}\n";
				AtomicFile.WriteAllText(PayloadStore.DevicePath(_payloadRoot, Plugin.DeviceName.Value), text2, AtomicFile.ShareTempSuffix(Plugin.DeviceName.Value, Plugin.OwnPid));
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " census write failed (" + ex.Message + ")."));
			}
		}

		private string ReadPublisherLoaderFingerprint(PayloadTier tier)
		{
			//IL_0000: 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)
			string text = PayloadTiers.Name(tier);
			string text2 = null;
			try
			{
				PayloadLedger val = PayloadStore.ReadLedger(_payloadRoot, (Action<string>)null);
				PayloadLedgerEntry obj = val.Get(tier);
				string text3 = ((obj != null) ? obj.Device : null);
				if (string.IsNullOrEmpty(text3))
				{
					_log.LogMessage((object)(Tag + " " + text + ": the ledger names no publishing device, so the loader precondition cannot be checked for this adoption."));
					return null;
				}
				text2 = PayloadStore.DevicePath(_payloadRoot, text3);
				if (!File.Exists(text2))
				{
					_log.LogWarning((object)(Tag + " " + text + ": no census row for publisher '" + text3 + "' at '" + text2 + "' — adopting WITHOUT the loader-match precondition."));
					return null;
				}
				string[] array = File.ReadAllLines(text2);
				foreach (string text4 in array)
				{
					int num = text4.IndexOf("\"loaderFingerprint\"", StringComparison.Ordinal);
					if (num >= 0)
					{
						string[] array2 = text4.Split(new char[1] { '"' });
						if (array2.Length >= 4)
						{
							return array2[3];
						}
					}
				}
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " " + text + ": could not read the publisher census at '" + (text2 ?? "(unresolved)") + "' (" + ex.GetType().Name + ": " + ex.Message + ") — adopting WITHOUT the loader-match precondition."));
			}
			return null;
		}

		private bool AnyTierLooksTorn()
		{
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				PayloadTier[] all = PayloadTiers.All;
				foreach (PayloadTier val in all)
				{
					if (File.Exists(PayloadStore.ManifestPath(_payloadRoot, val)) && !Directory.Exists(PayloadStore.TreePath(_payloadRoot, val)))
					{
						return true;
					}
				}
			}
			catch
			{
			}
			return false;
		}

		private bool AcquireLock()
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_002b: 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)
			LeaseDecision val = _lock.Decide(DateTime.UtcNow, Math.Max(30, Plugin.StaleSeconds.Value), Plugin.OnLockHeld.Value);
			if ((int)val != 0)
			{
				return false;
			}
			return _lock.Acquire(DateTime.UtcNow);
		}

		private void ReleaseLock()
		{
			try
			{
				_lock?.ReleaseIfOwn();
			}
			catch
			{
			}
		}

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

		private PayloadState LoadState()
		{
			//IL_0049: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				string statePath = PayloadScopeResolver.StatePath;
				return (PayloadState)(File.Exists(statePath) ? ((object)PayloadState.Parse(File.ReadAllText(statePath))) : ((object)new PayloadState()));
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " state read failed (" + ex.Message + ")."));
				return new PayloadState();
			}
		}

		private void SaveState()
		{
			try
			{
				AtomicFile.WriteAllText(PayloadScopeResolver.StatePath, _state.Serialize(), (string)null);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " state write failed (" + ex.Message + ")."));
			}
		}

		private PayloadScanCache LoadCache()
		{
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Expected O, but got Unknown
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				string cachePath = PayloadScopeResolver.CachePath;
				return (PayloadScanCache)(File.Exists(cachePath) ? ((object)PayloadScanCache.Parse(File.ReadAllText(cachePath))) : ((object)new PayloadScanCache()));
			}
			catch
			{
				return new PayloadScanCache();
			}
		}

		private void SaveCache()
		{
			try
			{
				AtomicFile.WriteAllText(PayloadScopeResolver.CachePath, _cache.Serialize(), (string)null);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " cache write failed (" + ex.Message + ")."));
			}
		}

		public void InvalidateCache(PayloadTier? tier)
		{
			//IL_0023: 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_002f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			_cache = _cache ?? LoadCache();
			PayloadTier[] all = PayloadTiers.All;
			foreach (PayloadTier val in all)
			{
				if (!tier.HasValue || tier.Value == val)
				{
					_cache.Invalidate(val);
				}
			}
		}

		public void PushAtQuitSync()
		{
			if (!Enabled(out var why))
			{
				_log.LogMessage((object)(Tag + " quit push skipped — " + why + "."));
				return;
			}
			_state = _state ?? LoadState();
			_cache = _cache ?? LoadCache();
			Stopwatch stopwatch = Stopwatch.StartNew();
			while (Interlocked.CompareExchange(ref _busy, 1, 0) != 0)
			{
				if (stopwatch.ElapsedMilliseconds > 15000)
				{
					_log.LogWarning((object)(Tag + " quit push skipped — a payload op is still running after " + $"{15}s (wedged mount?). The boot push replays it."));
					return;
				}
				Thread.Sleep(25);
			}
			try
			{
				_quitDeadlineUtc = DateTime.UtcNow.AddMilliseconds(Math.Max(1000L, 15000 - stopwatch.ElapsedMilliseconds));
				Reconcile(atBoot: false, force: false, null, pushOnly: true);
			}
			catch (Exception ex)
			{
				_log.LogWarning((object)(Tag + " quit push failed (" + ex.Message + ")."));
			}
			finally
			{
				_quitDeadlineUtc = null;
				Interlocked.Exchange(ref _busy, 0);
			}
		}

		private static string Short(string fingerprint)
		{
			if (!string.IsNullOrEmpty(fingerprint))
			{
				if (fingerprint.Length > 12)
				{
					return fingerprint.Substring(0, 12);
				}
				return fingerprint;
			}
			return "(none)";
		}
	}
	internal static class PayloadScopeResolver
	{
		public static string PluginsRoot => Paths.PluginPath;

		public static string GameRoot => Directory.GetParent(Paths.BepInExRootPath)?.FullName ?? Paths.GameRootPath;

		public static string StagingRoot => Path.Combine(Paths.BepInExRootPath, "cloudward-staged");

		public static string BackupRoot => Path.Combine(Paths.BepInExRootPath, "cloudward-backup");

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

		public static string CachePath => Path.Combine(Paths.ConfigPath, "cloudward_payload_cache.txt");

		public static string OverlayDir => Path.Combine(PluginsRoot, "Cloudward", "overlays");

		public static IReadOnlyList<CfgOverlay> LoadOverlays(Action<string> warn)
		{
			List<CfgOverlay> list = new List<CfgOverlay>();
			try
			{
				if (!Directory.Exists(OverlayDir))
				{
					return list;
				}
				foreach (string item in Directory.GetFiles(OverlayDir, "*.cfg.overlay").OrderBy<string, string>((string f) => f, StringComparer.Ordinal))
				{
					try
					{
						CfgOverlay val = CfgOverlay.Parse(CfgOverlay.GuidFromFileName(item), File.ReadAllText(item));
						if (val.Count > 0)
						{
							list.Add(val);
						}
					}
					catch (Exception ex)
					{
						warn?.Invoke("overlay '" + Path.GetFileName(item) + "' refused (" + ex.Message + "); its keys will NOT sync.");
					}
				}
			}
			catch (Exception ex2)
			{
				warn?.Invoke("could not read overlays from '" + OverlayDir + "' (" + ex2.Message + ").");
			}
			return list;
		}

		public static byte[] ReadCfg(string guid)
		{
			string path = Path.Combine(Paths.ConfigPath, guid + ".cfg");
			if (!File.Exists(path))
			{
				return null;
			}
			return File.ReadAllBytes(path);
		}

		public static IReadOnlyDictionary<string, string> LoadedModVersions()
		{
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
			try
			{
				foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos)
				{
					string key = pluginInfo.Key;
					PluginInfo value = pluginInfo.Value;
					object obj;
					if (value == null)
					{
						obj = null;
					}
					else
					{
						BepInPlugin metadata = value.Metadata;
						obj = ((metadata == null) ? null : metadata.Version?.ToString());
					}
					if (obj == null)
					{
						obj = "?";
					}
					dictionary[key] = (string)obj;
				}
			}
			catch
			{
			}
			return dictionary;
		}

		public static bool LooksLikeIl2Cpp(string gameRoot)
		{
			try
			{
				if (File.Exists(Path.Combine(gameRoot, "GameAssembly.dll")))
				{
					return true;
				}
				return Directory.GetDirectories(gameRoot, "*_Data").Any((string d) => Directory.Exists(Path.Combine(d, "il2cpp_data")));
			}
			catch
			{
				return false;
			}
		}

		public static string LoaderFingerprint(string gameRoot)
		{
			List<string> lines = new List<string>();
			Add("winhttp.dll");
			Add("doorstop_config.ini");
			Add("BepInEx/core");
			Add("mono_fix");
			return Fnv1a.HashLines((IEnumerable<string>)lines);
			void Add(string rel)
			{
				string text = Path.Combine(gameRoot, rel.Replace('/', Path.DirectorySeparatorChar));
				try
				{
					if (File.Exists(text))
					{
						lines.Add(rel + "|" + PayloadHasher.HashFile(text));
					}
					else if (Directory.Exists(text))
					{
						foreach (string item in Directory.GetFiles(text, "*.dll").OrderBy<string, string>((string f) => f, StringComparer.Ordinal))
						{
							lines.Add(rel + "/" + Path.GetFileName(item) + "|" + PayloadHasher.HashFile(item));
						}
						return;
					}
				}
				catch
				{
				}
			}
		}
	}
	[BepInPlugin("cobalt.cloudward", "Cloudward", "0.2.0")]
	[BepInDependency("cobalt.forgekit", "0.4.10")]
	public class Plugin : BaseUnityPlugin
	{
		public const string GUID = "cobalt.cloudward";

		public const string NAME = "Cloudward";

		public const string VERSION = "0.2.0";

		internal static ManualLogSource Log;

		internal static SyncCoordinator Coordinator;

		internal static PayloadCoordinator Payload;

		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;

		public static ConfigEntry<bool> PayloadEnable;

		public static ConfigEntry<bool> PayloadSyncPlugins;

		public static ConfigEntry<bool> PayloadSyncConfig;

		public static ConfigEntry<PayloadRole> PayloadRoleValue;

		public static ConfigEntry<PayloadApplyMode> PayloadApplyModeValue;

		public static ConfigEntry<PayloadConflictPolicy> PayloadConflictPolicyValue;

		public static ConfigEntry<bool> PayloadPushAtQuit;

		public static ConfigEntry<string> PayloadExtraExcludes;

		public static ConfigEntry<int> PayloadMaxMegabytes;

		public static ConfigEntry<int> PayloadKeepBackups;

		public static ConfigEntry<int> PayloadMaxDeletePercent;

		private CommandRegistry _commands;

		private VerbHost _verbs;

		private CommandChannel _channel;

		private float _lastHeartbeat;

		[MethodImpl(MethodImplOptions.NoInlining)]
		private static void DeclareKitContracts()
		{
			KitContract.Declare("Cloudward", "cobalt.forgekit", "0.4.10");
		}

		private void TryDeclareKitContracts()
		{
			try
			{
				DeclareKitContracts();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("[CONTRACT] kit handshake unavailable (" + ex.GetType().Name + ") — is ForgeKit older than this mod?"));
			}
		}

		internal void Awake()
		{
			//IL_01db: Unknown result type (might be due to invalid IL or missing references)
			//IL_01e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01eb: Unknown result type (might be due to invalid IL or missing references)
			//IL_01f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_0201: Unknown result type (might be due to invalid IL or missing references)
			//IL_0218: Expected O, but got Unknown
			//IL_0213: Unknown result type (might be due to invalid IL or missing references)
			//IL_021d: Expected O, but got Unknown
			//IL_0240: Unknown result type (might be due to invalid IL or missing references)
			//IL_0246: Expected O, but got Unknown
			TryDeclareKitContracts();
			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.");
			BindPayloadConfig();
			Coordinator = new SyncCoordinator(Log);
			Payload = new PayloadCoordinator(Log, Coordinator);
			if (Enable.Value)
			{
				((Component)this).gameObject.AddComponent<SyncProgressUi>();
			}
			RegisterVerbs();
			_channel = new CommandChannel("Cloudward_cmd.txt", Log, _commands, 0.5f, true, true, new CatalogInfo
			{
				ModGuid = "cobalt.cloudward",
				ModName = "Cloudward",
				ModVersion = "0.2.0",
				ConfigSource = () => ((BaseUnityPlugin)this).Config
			});
			CommonVerbs.RegisterConfigVerbs(_commands, Log, (Func<ConfigFile>)(() => ((BaseUnityPlugin)this).Config), (Action)null, true);
			Harmony val = new Harmony("cobalt.cloudward");
			val.PatchAll();
			int num = 0;
			foreach (MethodBase patchedMethod in val.GetPatchedMethods())
			{
				num++;
			}
			if (num == 2)
			{
				Log.LogMessage((object)$"[CLOUDWARD] patch ledger: applied={num}/{2} (SaveManager.Init boot hook, OnApplicationQuit push).");
			}
			else
			{
				Log.LogWarning((object)$"[CLOUDWARD] patch ledger: applied={num}/{2} — a missing patch means boot sync and/or the quit push will SILENTLY not run.");
			}
			Log.LogMessage((object)string.Format("{0} {1} loaded (Enable={2}, device={3}).", "Cloudward", "0.2.0", Enable.Value, DeviceName.Value));
		}

		internal void Update()
		{
			_channel.Tick();
			Coordinator?.SampleGameState();
			Coordinator?.PumpMainThread();
			Coordinator?.RetryHeldBootPull();
			Coordinator?.WatchdogTick();
			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 BindPayloadConfig()
		{
			PayloadEnable = ((BaseUnityPlugin)this).Config.Bind<bool>("Payload", "Enable", false, "Sync mod folders and mod configuration across your devices, alongside saves. Requires [Sync] Enable and a MountPath. OFF by default deliberately: this rewrites BepInEx/plugins, so it must be a decision, not a side effect of turning save sync on. The first boot against a new share only REPORTS what it would do; it acts from the second boot onward.");
			PayloadSyncPlugins = ((BaseUnityPlugin)this).Config.Bind<bool>("Payload", "SyncPlugins", true, "Sync the BepInEx/plugins tree. Whole folders, never a *.dll glob (a glob drops the data mods ship beside their DLL). Configs, saves, logs, pet files and command channels can never ride this tier — that exclusion is in code, not policy.");
			PayloadSyncConfig = ((BaseUnityPlugin)this).Config.Bind<bool>("Payload", "SyncConfig", true, "Sync the config KEYS named in plugins/Cloudward/overlays/*.cfg.overlay — and only those. Everything else in your .cfg files is host-local and its bytes are never rewritten, so MountPath, DeviceName, keybinds, resolution and joystick GUIDs cannot travel.");
			PayloadRoleValue = ((BaseUnityPlugin)this).Config.Bind<PayloadRole>("Payload", "Role", PayloadRole.Both, "Both = publish and adopt. Publisher = never adopt another device's payload. Subscriber = never write the share (set this on someone else's machine).");
			PayloadApplyModeValue = ((BaseUnityPlugin)this).Config.Bind<PayloadApplyMode>("Payload", "ApplyMode", PayloadApplyMode.Auto, "Auto = a fetched payload installs itself at the next launch (a BepInEx preloader patcher applies it before plugins load — nothing can be applied to a RUNNING session). StageOnly = fetch it and print the command to finish by hand.");
			PayloadConflictPolicyValue = ((BaseUnityPlugin)this).Config.Bind<PayloadConflictPolicy>("Payload", "OnConflict", (PayloadConflictPolicy)0, "Both this device and the share changed since the common generation. Skip = touch nothing and wait for 'payloadresolve' (default: unlike a save fork there is no newest-wins guess worth automating, because a wrong pick installs the wrong CODE). PreferLocal / PreferShare resolve automatically at boot. The losing side is always backed up first.");
			PayloadPushAtQuit = ((BaseUnityPlugin)this).Config.Bind<bool>("Payload", "PushAtQuit", false, "Also publish at quit, catching a mid-session deploy. Off by default: the quit push has a 15s budget and a plugins tree runs to tens of megabytes over a network mount. The boot push is the workhorse.");
			PayloadExtraExcludes = ((BaseUnityPlugin)this).Config.Bind<string>("Payload", "ExtraExcludes", "", "Semicolon-separated extra glob patterns to keep off the share (e.g. '*.pdb;scratch_*'). Additive only — the built-in exclusions cannot be removed by any config edit.");
			PayloadMaxMegabytes = ((BaseUnityPlugin)this).Config.Bind<int>("Payload", "MaxPayloadMegabytes", 2048, "Refuse to publish a tier larger than this. A tier that suddenly exceeds it almost always means a mispointed root rather than a real change.");
			PayloadKeepBackups = ((BaseUnityPlugin)this).Config.Bind<int>("Payload", "KeepBackups", 3, "How many pre-replace backups to keep per tier under .cloudward-payload/.payload-forks/. These are what 'payloadrollback' restores from.");
			PayloadMaxDeletePercent = ((BaseUnityPlugin)this).Config.Bind<int>("Payload", "MaxDeletePercent", 25, "The mass-delete bound: refuse to publish OR adopt a plugins-tier plan that would delete more than this percentage of the previous manifest's files (and more than " + $"{5} files). A plan like that is far more likely a broken scan than " + "a deliberate uninstall wave. Set 100 for one pass to perform a genuine mass-uninstall.");
		}

		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, (ArgSpec[])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, (ArgSpec[])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, (ArgSpec[])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, (ArgSpec[])null);
			_verbs.Register("synclock", "synclock [release] — show the current lock (bare); push+release ours ('synclock release').", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				if (ctx.Arg(1) == "release")
				{
					bool flag = Coordinator?.PushAndReleaseAtQuit() ?? false;
					Log.LogMessage((object)(flag ? "[Cloudward] pushed + released the lock on request." : "[Cloudward] nothing released — not holding the lock (or the push was skipped; see warnings above)."));
				}
				else
				{
					Log.LogMessage((object)("[Cloudward] lock holder: " + (Coordinator?.CurrentHolder() ?? "(none/unreadable)") + "."));
				}
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])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, (ArgSpec[])null);
			_verbs.Register("payloadstatus", "Dump payload state: tiers, generations, staged/conflict, backups, loader fingerprint.", (Action<VerbContext>)delegate
			{
				Payload?.Status();
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadscan", "Re-detect from cold, ignoring the size+mtime prefilter: 'payloadscan [plugins|config]'.", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				PayloadTier? val = ParseTier(ctx.Arg(1));
				Payload?.InvalidateCache(val);
				Payload?.ReconcileNow(force: true, val);
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadpush", "Publish this device's payload now: 'payloadpush [plugins|config]'.", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				Payload?.ReconcileNow(force: false, ParseTier(ctx.Arg(1)), pushOnly: true);
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadstage", "Fetch the share's payload for the next launch: 'payloadstage [plugins|config]'.", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				Payload?.ReconcileNow(force: false, ParseTier(ctx.Arg(1)), pushOnly: false, stageOnly: true);
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadresolve", "Resolve a payload divergence: 'payloadresolve <local|share> [plugins|config]'.", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				Payload?.Resolve(ctx.Arg(1), ctx.Arg(2));
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadrollback", "Restore the plugins tier on the share from a backup: 'payloadrollback plugins [n]' (0 = newest; config publishes carry no backups).", (Action<VerbContext>)delegate(VerbContext ctx)
			{
				Payload?.Rollback(ctx.Arg(1), ctx.Arg(2));
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
			_verbs.Register("payloadapply", "Release a payload held by ApplyMode=StageOnly so the next launch installs it.", (Action<VerbContext>)delegate
			{
				Payload?.ReleaseHold();
			}, "[Cloudward]", false, true, false, (string)null, (ArgSpec[])null);
		}

		private static PayloadTier? ParseTier(string arg)
		{
			//IL_0014: Unknown result type (might be due to invalid IL or missing references)
			PayloadTier value = default(PayloadTier);
			if (!PayloadTiers.TryParse(arg, ref value))
			{
				return null;
			}
			return value;
		}

		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 static bool NoOpCfgMergeIsByteIdentical()
		{
			//IL_001f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				byte[] bytes = Encoding.UTF8.GetBytes("[S]\r\nK = v\r\n# Default value: 1\r\nOther = 2\r\n");
				Dictionary<CfgKey, string> dictionary = new Dictionary<CfgKey, string> { [new CfgKey("S", "K")] = "v" };
				CfgMergeResult val = CfgMerge.Apply(bytes, (IReadOnlyDictionary<CfgKey, string>)dictionary);
				return !val.Dirty && val.After.Length == bytes.Length;
			}
			catch
			{
				return false;
			}
		}

		private static bool StageOpsAreConfined()
		{
			//IL_001d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Expected O, but got Unknown
			//IL_0029: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				new StagePlan((IEnumerable<PayloadTier>)(object)new PayloadTier[1], (IEnumerable<FileOp>)(object)new FileOp[1]
				{
					new FileOp((FileOpKind)0, "SaveGames/evil", new string('a', 64))
				}, (IEnumerable<CfgOp>)Array.Empty<CfgOp>(), (IReadOnlyDictionary<PayloadTier, int>)null);
				return false;
			}
			catch (InvalidDataException)
			{
				return true;
			}
			catch
			{
				return false;
			}
		}

		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_00ec: Expected O, but got Unknown
			//IL_00e7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ed: Invalid comparison between Unknown and I4
			//IL_0172: Unknown result type (might be due to invalid IL or missing references)
			//IL_0178: 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, (Func<int, bool>)null) == 0);
			PayloadScope val2 = PayloadScope.FromConfig(PayloadExtraExcludes.Value);
			val.Check("payload gate bars configs + command channels", val2.IsExcluded("cobalt.beastwhispering.cfg") && val2.IsExcluded("bw_cmd.txt") && val2.IsExcluded("bw_pets_hero.txt") && val2.IsExcluded("SaveGames/x"));
			val.Check("payload gate still passes a mod tree", !val2.IsExcluded("Beastwhispering/SideLoader/Items/PetChow.xml"));
			val.Check("an empty share never pulls", (int)PayloadReconciler.Plan((PayloadTier)0, "local-fp", true, (PayloadManifest)null, (PayloadLedgerEntry)null, (PayloadTierState)null).Action != 1);
			val.Check("a no-op cfg merge is byte-identical", NoOpCfgMergeIsByteIdentical());
			val.Check("payload root is not account-scoped", PathRebase.PayloadRoot("/mnt/nas") == "/mnt/nas/.cloudward-payload");
			val.Check("stage ops cannot escape their tier", StageOpsAreConfined());
			val.Done();
		}
	}
	public enum MountDownPolicy
	{
		FallbackLocal,
		RefuseAndLog
	}
	internal enum SyncMode
	{
		Inactive,
		Offline,
		Online
	}
	internal readonly struct ReconcileSummary
	{
		public readonly bool Skipped;

		public readonly int Pulled;

		public readonly int Pushed;

		public readonly int UpToDate;

		public readonly int Forks;

		public readonly int Held;

		public static ReconcileSummary SkippedSaveInProgress => new ReconcileSummary(skipped: true, 0, 0, 0, 0, 0);

		public ReconcileSummary(bool skipped, int pulled, int pushed, int upToDate, int forks, int held)
		{
			Skipped = skipped;
			Pulled = pulled;
			Pushed = pushed;
			UpToDate = upToDate;
			Forks = forks;
			Held = held;
		}

		public override string ToString()
		{
			return $"pulled={Pulled} pushed={Pushed} up-to-date={UpToDate} forks={Forks} held={Held}";
		}
	}
	internal readonly struct SyncUiState
	{
		public readonly bool Active;

		public readonly string Phase;

		public readonly int Done;

		public readonly int Total;

		public readonly bool HadSummary;

		public readonly double ActiveForSeconds;

		public readonly double SinceDoneSeconds;

		public SyncUiState(bool active, string phase, int done, int total, bool hadSummary, double activeForSeconds, double sinceDoneSeconds)
		{
			Active = active;
			Phase = phase ?? "";
			Done = done;
			Total = total;
			HadSummary = hadSummary;
			ActiveForSeconds = activeForSeconds;
			SinceDoneSeconds = sinceDoneSeconds;
		}
	}
	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 volatile bool _ambiguousMountAdvised;

		private volatile bool _shareFsProbed;

		private int _busy;

		private volatile string _busyTag;

		private long _busyStartUtcTicks;

		private bool _stallWarned;

		private volatile bool _savesPinned;

		private volatile string _pinReason = "";

		private volatile bool _charEverInUse;

		private volatile bool _bootPullHeld;

		private bool _refreshHoldLogged;

		private bool _heldPullLogged;

		private float _holdStartUnscaled = -1f;

		private volatile bool _uiActive;

		private volatile string _uiPhase = "";

		private volatile int _uiDone;

		private volatile int _uiTotal;

		private volatile bool _uiHadSummary;

		private long _uiStartUtcTicks;

		private long _uiDoneUtcTicks;

		private SessionLock _lock;

		private LocalSyncState _state;

		private const int QuitJoinBudgetMs = 15000;

		private readonly object _stateGate = new object();

		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");

		private void UiBegin(string phase)
		{
			_uiDone = 0;
			_uiTotal = 0;
			_uiHadSummary = false;
			_uiPhase = phase;
			Interlocked.Exchange(ref _uiStartUtcTicks, DateTime.UtcNow.Ticks);
			Interlocked.Exchange(ref _uiDoneUtcTicks, 0L);
			_uiActive = true;
		}

		private void UiPhase(string phase)
		{
			if (_uiActive)
			{
				_uiPhase = phase;
			}
		}

		private void UiStep(int done, int total)
		{
			if (_uiActive)
			{
				_uiDone = done;
				_uiTotal = total;
				_uiPhase = SyncProgress.Counting(done, total);
			}
		}

		private void UiEnd(string summary)
		{
			if (_uiActive)
			{
				_uiHadSummary = summary != null;
				if (summary != null)
				{
					_uiPhase = summary;
					_uiDone = 1;
					_uiTotal = 1;
				}
				Interlocked.Exchange(ref _uiDoneUtcTicks, DateTime.UtcNow.Ticks);
				_uiActive = false;
			}
		}

		internal SyncUiState ReadUiState()
		{
			long ticks = DateTime.UtcNow.Ticks;
			long num = Interlocked.Read(in _uiStartUtcTicks);
			long num2 = Interlocked.Read(in _uiDoneUtcTicks);
			double activeForSeconds = ((num > 0) ? ((double)(ticks - num) / 10000000.0) : 0.0);
			double sinceDoneSeconds = ((num2 > 0) ? ((double)(ticks - num2) / 10000000.0) : double.MaxValue);
			return new SyncUiState(_uiActive, _uiPhase, _uiDone, _uiTotal, _uiHadSummary, activeForSeconds, sinceDoneSeconds);
		}

		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;
			}
			Interlocked.Exchange(ref _busyStartUtcTicks, DateTime.UtcNow.Ticks);
			_busyTag = tag;
			Task.Run(delegate
			{
				try
				{
					work();
				}
				catch (Exception arg)
				{
					_log.LogError((object)$"[CLOUDWARD] {tag} failed: {arg}");
				}
				finally
				{
					_busyTag = null;
					Interlocked.Exchange(ref _busy, 0);
				}
			});
			return true;
		}

		public void WatchdogTick()
		{
			//IL_0051: Unknown result type (might be due to invalid IL or missing references)
			//IL_0056: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005b: Invalid comparison between Unknown and I4
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0060: Invalid comparison between Unknown and I4
			string busyTag = _busyTag;
			bool flag = busyTag != null;
			double num = (flag ? (DateTime.UtcNow - new DateTime(Interlocked.Read(in _busyStartUtcTicks), DateTimeKind.Utc)).TotalSeconds : 0.0);
			Verdict val = OpWatchdog.Check(flag, _stallWarned, num, 60.0);
			if ((int)val != 1)
			{
				if ((int)val == 2)
				{
					_stallWarned = false;
					_log.LogMessage((object)"[CLOUDWARD] the long-running background op finished — sync is operational again.");
				}
			}
			else
			{
				_stallWarned = true;
				_log.LogWarning((object)($"[CLOUDWARD] background op '{busyTag}' still running after {num:0}s — either a very " + "large first sync or a HUNG mount. All sync (pulls, pushes, heartbeats) is blocked until it returns; this session's saves stay local-only if it never does."));
			}
		}

		public void SampleGameState()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Invalid comparison between Unknown and I4
			//IL_000b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			bool savesPinned;
			bool flag;
			string text;
			try
			{
				Signal val = SamplePinSignal();
				savesPinned = (int)val > 0;
				flag = SavePin.IsSticky(val);
				text = SavePin.Describe(val);
			}
			catch (Exception ex)
			{
				savesPinned = true;
				flag = false;
				text = "pin sample threw (" + ex.GetType().Name + ") — assuming pinned";
			}
			_savesPinned = savesPinned;
			_pinReason = text ?? "";
			if (flag)
			{
				_charEverInUse = true;
			}
		}

		private static Signal SamplePinSignal()
		{
			//IL_0079: Unknown result type (might be due to invalid IL or missing references)
			SaveManager instance = SaveManager.Instance;
			NetworkLevelLoader instance2 = NetworkLevelLoader.Instance;
			bool flag = false;
			if (instance != null && instance.CharacterSaves != null)
			{
				foreach (CharacterSaveInstanceHolder characterSafe in instance.CharacterSaves)
				{
					if (characterSafe != null && instance.IsCharInUse(characterSafe.CharacterUID))
					{
						flag = true;
						break;
					}
				}
			}
			return SavePin.Evaluate(instance != null && instance.SaveInProgress, (Object)(object)instance2 != (Object)null && instance2.IsLoadSequenceStarted, flag);
		}

		private static bool SavesPinnedNow(out string why)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_000e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Invalid comparison between Unknown and I4
			Signal val = SamplePinSignal();
			why = SavePin.Describe(val);
			return (int)val > 0;
		}

		public void PumpMainThread()
		{
			if (!_needsRefresh)
			{
				return;
			}
			if (SavesPinnedNow(out var why))
			{
				if (!_refreshHoldLogged)
				{
					_refreshHoldLogged = true;
					_log.LogMessage((object)("[CLOUDWARD] character-list refresh HELD (" + why + ") — will apply when the pin lifts (V-JOINRACE)."));
				}
				return;
			}
			_refreshHoldLogged = false;
			_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 RetryHeldBootPull()
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: 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_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Invalid comparison between Unknown and I4
			if (!_bootPullHeld)
			{
				return;
			}
			if (_holdStartUnscaled < 0f)
			{
				_holdStartUnscaled = Time.unscaledTime;
			}
			float held = Time.unscaledTime - _holdStartUnscaled;
			PullDecision val = JoinRaceGate.Decide(true, _savesPinned, _charEverInUse, held, 60f);
			if ((int)val != 0)
			{
				if ((int)val == 2)
				{
					_bootPullHeld = false;
					_log.LogWarning((object)("[CLOUDWARD] boot pull DEFERRED to next launch — " + (_charEverInUse ? "a character was already selected (the pin never lifts in-session)." : $"the pin did not lift within {60f:0}s.") + " Nothing on disk was changed."));
				}
			}
			else if (RunInBackground("held boot pull", delegate
			{
				UiBegin("checking share…");
				try
				{
					string text = (Plugin.MountPath.Value ?? "").Trim();
					if (text.Length != 0 && EnsureMountLive(text))
					{
						ReconcileSummary reconcileSummary = DoReconcile(mayMutateLocal: true, announce: true);
						if (!reconcileSummary.Skipped)
						{
							UiEnd(SyncProgress.Summary(reconcileSummary.Pulled, reconcileSummary.Pushed));
						}
						_log.LogMessage((object)$"[CLOUDWARD] held boot pull applied after {held:0.0}s (join window cleared): {reconcileSummary}.");
					}
				}
				finally
				{
					UiEnd(null);
				}
			}, quietIfBusy: true))
			{
				_bootPullHeld = false;
			}
		}

		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;
			_log.LogMessage((object)("[CLOUDWARD] boot reconcile queued: local='" + LocalRoot + "' mount='" + mountRoot + "' (background)."));
			RunInBackground("boot reconcile", delegate
			{
				UiBegin("checking share…");
				try
				{
					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();
						ReconcileSummary reconcileSummary = DoReconcile(mayMutateLocal: true, announce: true);
						_mode = SyncMode.Online;
						if (!reconcileSummary.Skipped)
						{
							UiEnd(SyncProgress.Summary(reconcileSummary.Pulled, reconcileSummary.Pushed));
						}
						_log.LogMessage((object)(reconcileSummary.Skipped ? "[CLOUDWARD] boot reconcile done: SKIPPED (a game save was in progress) — pushes resume on the heartbeat; pulls at next launch." : $"[CLOUDWARD] boot reconcile done: {reconcileSummary} (mode=Online)."));
					}
				}
				finally
				{
					UiEnd(null);
				}
			});
		}

		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."));
				TargetPath = null;
				return false;
			}
		}

		private bool EnsureTarget(string mountRoot)
		{
			if (TargetPath != null)
			{
				return true;
			}
			if (!SetupTarget(mountRoot))
			{
				return false;
			}
			_log.LogMessage((object)("[CLOUDWARD] mount came up after boot — target '" + TargetPath + "' opened."));
			return true;
		}

		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);
			bool flag = (int)val == 0 || (int)Plugin.OnLockHeld.Value == 2;
			_holdingLock = flag && _lock.Acquire(DateTime.UtcNow);
			if (!_holdingLock)
			{
				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 ReconcileSummary DoReconcile(bool mayMutateLocal, bool announce = false)
		{
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_005d: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0104: Unknown result type (might be due to invalid IL or missing references)
			//IL_010a: Invalid comparison between Unknown and I4
			//IL_011a: Unknown result type (might be due to invalid IL or missing references)
			//IL_011f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0121: Unknown result type (might be due to invalid IL or missing references)
			//IL_0138: Expected I4, but got Unknown
			//IL_0295: Unknown result type (might be due to invalid IL or missing references)
			//IL_029a: Unknown result type (might be due to invalid IL or missing references)
			//IL_029c: Unknown result type (might be due to invalid IL or missing references)
			//IL_02af: Expected I4, but got Unknown
			//IL_0145: Unknown result type (might be due to invalid IL or missing references)
			//IL_01fa: Unknown result type (might be due to invalid IL or missing references)
			//IL_0204: Expected O, but got Unknown
			//IL_0186: Unknown result type (might be due to invalid IL or missing references)
			//IL_0190: Expected O, but got Unknown
			//IL_03e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_03ef: Expected O, but got Unknown
			if (SaveManager.Instance.SaveInProgress)
			{
				return ReconcileSummary.SkippedSaveInProgress;
			}
			if (announce)
			{
				UiPhase("scanning saves…");
			}
			SyncLedger val = LoadLedger();
			Dictionary<string, CharTree> dictionary = TreeScanner.Scan(LocalRoot);
			Dictionary<string, CharTree> dictionary2 = TreeScanner.Scan(TargetPath);
			bool flag = false;
			bool flag2 = false;
			int num = 0;
			int num2 = 0;
			int num3 = 0;
			int num4 = 0;
			int num5 = 0;
			ForkResolution value = Plugin.ForkPolicy.Value;
			bool flag3 = ForkResolve.IsAuto(value);
			List<string> list = dictionary.Keys.Union(dictionary2.Keys).ToList();
			int num6 = 0;
			if (announce)
			{
				UiStep(0, list.Count);
			}
			foreach (string item in list)
			{
				if (announce)
				{
					UiStep(++num6, list.Count);
				}
				bool flag4 = _state.GetFork(item) != null;
				ReconcilePlan val2 = Reconciler.Plan(item, Tree(dictionary, item), Tree(dictionary2, item), val.Get(item), _state.Get(item));
				if ((int)val2.Action == 3)
				{
					num4++;
					ForkGate val3 = ForkResolve.Gate(flag3, mayMutateLocal, flag4);
					switch ((int)val3)
					{
					case 0:
					{
						(bool, bool) tuple = AutoResolve(item, val2, dictionary, dictionary2, val, value);
						flag |= tuple.Item1;
						flag2 |= tuple.Item2;
						break;
					}
					case 1:
						if (!flag4)
						{
							_state.SetFork(item, new ForkRecord(val2.LocalHead, val2.ShareHead));
							_log.LogWarning((object)("[CLOUDWARD] " + item + ": FORK detected mid-session (local " + val2.LocalHead + " vs share " + val2.ShareHead + ") — auto-resolve deferred to next boot (can't change the live save set)."));
						}
						break;
					case 2:
						_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 + "'."));
						break;
					}
					continue;
				}
				if (flag4)
				{
					_state.ClearFork(item);
					_log.LogMessage((object)("[CLOUDWARD] " + item + ": recorded fork no longer diverges — cleared; resuming normal sync."));
				}
				SyncAction action = val2.Action;
				switch ((int)action)
				{
				case 1:
					if (mayMutateLocal && _savesPinned)
					{
						_bootPullHeld = true;
						num5++;
						if (!_heldPullLogged)
						{
							_heldPullLogged = true;
							_log.LogWarning((object)("[CLOUDWARD] " + item + ": pull HELD — " + _pinReason + ". The live save set is pinned " + $"(V-JOINRACE); retrying for up to {60f:0}s pre-selection, " + "else deferred to next launch."));
						}
					}
					else if (mayMutateLocal && ExecPull(item, val2))
					{
						flag = true;
						num++;
					}
					break;
				case 2:
					if (_holdingLock)
					{
						if (ExecPush(item, val2, val))
						{
							flag2 = true;
							num2++;
						}
					}
					else
					{
						_log.LogMessage((object)("[CLOUDWARD] " + item + ": local ahead but lock held elsewhere — deferring push."));
					}
					break;
				case 0:
				{
					num3++;
					LedgerEntry val4 = val.Get(item);
					if (val4 != null && val2.NewHead.Length > 0)
					{
						_state.Set(item, new LocalCharState(val4.Gen, val2.NewHead));
					}
					break;
				}
				}
			}
			SaveState();
			if (flag2)
			{
				SaveLedger(val);
			}
			if (flag)
			{
				_needsRefresh = true;
			}
			return new ReconcileSummary(skipped: false, num, num2, num3, num4, num5);
		}

		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_00c0: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c3: Invalid comparison between Unknown and I4
			//IL_0231: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			//IL_015a: Expected O, but got Unknown
			//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
			//IL_01e0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_01d4: 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);
			}
			if (_savesPinned)
			{
				if (_state.GetFork(uid) == null)
				{
					_state.SetFork(uid, new ForkRecord(plan.LocalHead, plan.ShareHead));
				}
				_log.LogWarning((object)("[CLOUDWARD] " + uid + ": fork resolve (keeping share) HELD — " + _pinReason + " (V-JOINRACE); recorded pending, re-processed when the save set unpins or at next launch."));
				return (pulled: false, ledgerDirty: false);
			}
			if (!_holdingLock)
			{
				if (_state.GetFork(uid) == null)
				{
					_state.SetFork(uid, new ForkRecord(plan.LocalHead, plan.ShareHead));
				}
				_log.LogMessage((object)$"[CLOUDWARD] {uid}: fork ({policy}, keeping share) — deferring resolve until the lock is held.");
				return (pulled: false, ledgerDirty: false);
			}
			string arg = BackupLineage(uid, LocalRoot, val, "local");
			PullWins(uid, val2, local, ledger);
			_state.ClearFork(uid);