Decompiled source of ServerModBootstrap v2.1.0

BepInEx/patchers/XomNghienBootstrap.dll

Decompiled 8 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using Microsoft.CodeAnalysis;
using Mono.Cecil;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: InternalsVisibleTo("XomNghien.Bootstrap.Tests")]
[assembly: InternalsVisibleTo("XomNghienRuntimeUpdater")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("XomNghienBootstrap")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("2.1.0.0")]
[assembly: AssemblyInformationalVersion("2.1.0+cede9045c60d8f4cbe5376fd902eda1df65f5555")]
[assembly: AssemblyProduct("XomNghienBootstrap")]
[assembly: AssemblyTitle("XomNghienBootstrap")]
[assembly: AssemblyVersion("2.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace XomNghien.Bootstrap
{
	internal static class AtomicFile
	{
		public static void Replace(string temporary, string target)
		{
			if (File.Exists(target))
			{
				string text = target + ".bak";
				if (File.Exists(text))
				{
					File.Delete(text);
				}
				File.Replace(temporary, target, text);
				File.Delete(text);
			}
			else
			{
				File.Move(temporary, target);
			}
		}
	}
	internal static class BootstrapLog
	{
		private static string? _path;

		public static void Initialize(string stateRoot)
		{
			Directory.CreateDirectory(stateRoot);
			_path = Path.Combine(stateRoot, "bootstrap.log");
		}

		public static void Info(string message)
		{
			Write("INFO", message);
		}

		public static void Error(string message, Exception error)
		{
			Write("ERROR", message + Environment.NewLine + error);
		}

		private static void Write(string level, string message)
		{
			string text = $"[{DateTimeOffset.UtcNow:O}] [{level}] {message}";
			Console.WriteLine("[ServerModBootstrap] " + text);
			try
			{
				if (_path != null)
				{
					File.AppendAllText(_path, text + Environment.NewLine);
				}
			}
			catch
			{
			}
		}
	}
	public static class BootstrapPatcher
	{
		public static IEnumerable<string> TargetDLLs => Array.Empty<string>();

		public static void Initialize()
		{
			try
			{
				BootstrapSynchronizer.Run();
				ClearRestartMarker();
			}
			catch (Exception error)
			{
				BootstrapLog.Error("Synchronization failed; keeping the last-known-good installation", error);
			}
		}

		public static void Patch(AssemblyDefinition assembly)
		{
		}

		private static void ClearRestartMarker()
		{
			string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
			string text = ((directoryName == null) ? null : Directory.GetParent(directoryName)?.FullName);
			if (text != null)
			{
				string path = Path.Combine(text, "xom-bootstrap", "restart-required");
				if (File.Exists(path))
				{
					File.Delete(path);
				}
			}
		}
	}
	internal sealed class BootstrapSettings
	{
		public string ManifestUrl { get; private set; } = "";

		public int RequestTimeoutSeconds { get; private set; } = 45;

		public bool HasManifestUrl => ManifestUrl.Length > 0;

		public static BootstrapSettings Load(string path)
		{
			BootstrapSettings bootstrapSettings = new BootstrapSettings();
			if (!File.Exists(path))
			{
				return bootstrapSettings;
			}
			Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
			string[] array = File.ReadAllLines(path);
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim();
				if (text.Length != 0 && !text.StartsWith("#", StringComparison.Ordinal) && !text.StartsWith(";", StringComparison.Ordinal))
				{
					int num = text.IndexOf('=');
					if (num > 0)
					{
						dictionary[text.Substring(0, num).Trim()] = text.Substring(num + 1).Trim();
					}
				}
			}
			if (dictionary.TryGetValue("ManifestUrl", out var value))
			{
				bootstrapSettings.ManifestUrl = value;
			}
			if (dictionary.TryGetValue("RequestTimeoutSeconds", out var value2) && int.TryParse(value2, NumberStyles.None, CultureInfo.InvariantCulture, out var result))
			{
				bootstrapSettings.RequestTimeoutSeconds = Math.Max(10, Math.Min(120, result));
			}
			bootstrapSettings.Validate();
			return bootstrapSettings;
		}

		private void Validate()
		{
			if (HasManifestUrl && (!Uri.TryCreate(ManifestUrl, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps))
			{
				throw new InvalidDataException("ManifestUrl must be an absolute HTTPS URL");
			}
		}
	}
	public static class BootstrapSynchronizer
	{
		private sealed class BootstrapContext
		{
			public string BepInExRoot { get; }

			public string StateRoot { get; }

			public string StatePath { get; }

			public string LastManifestPath { get; }

			public string PendingManifestPath { get; }

			public BootstrapSettings Settings { get; }

			public BootstrapContext(string bepinExRoot, string stateRoot, string statePath, string lastManifestPath, string pendingManifestPath, BootstrapSettings settings)
			{
				BepInExRoot = bepinExRoot;
				StateRoot = stateRoot;
				StatePath = statePath;
				LastManifestPath = lastManifestPath;
				PendingManifestPath = pendingManifestPath;
				Settings = settings;
			}
		}

		private enum ConfigAudience
		{
			Server,
			Client
		}

		private const long MaximumArchiveBytes = 524288000L;

		private const int MaximumManifestBytes = 8388608;

		private static readonly object SynchronizeLock = new object();

		public static SynchronizationResult Run()
		{
			lock (SynchronizeLock)
			{
				BootstrapContext context = CreateContext();
				ApplyPendingLocked(context);
				return RunLocked(context);
			}
		}

		private static SynchronizationResult RunLocked(BootstrapContext context)
		{
			ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
			BootstrapState bootstrapState = LoadState(context.StatePath);
			if (!context.Settings.HasManifestUrl)
			{
				BootstrapLog.Info("No ManifestUrl is configured; waiting for a server-relayed manifest");
				return SynchronizationResult.Unchanged(bootstrapState.Revision);
			}
			BootstrapLog.Info("Checking configured manifest for managed mod and config updates");
			string previousRevision = (LocalStateIsHealthy(context.BepInExRoot, bootstrapState) ? bootstrapState.Revision : "");
			byte[] array = DownloadManifest(context.Settings, previousRevision);
			if (array == null)
			{
				BootstrapLog.Info("Revision " + ShortRevision(bootstrapState.Revision) + " is already current");
				return SynchronizationResult.Unchanged(bootstrapState.Revision);
			}
			return ApplyManifestLocked(context, bootstrapState, array, ConfigAudience.Server);
		}

		public static SynchronizationResult StageRelayedManifest(string manifestJson)
		{
			if (manifestJson == null)
			{
				throw new ArgumentNullException("manifestJson");
			}
			byte[] bytes = Encoding.UTF8.GetBytes(manifestJson);
			if (bytes.Length > 8388608)
			{
				throw new InvalidDataException("Relayed manifest exceeds the 8 MiB limit");
			}
			lock (SynchronizeLock)
			{
				BootstrapContext bootstrapContext = CreateContext();
				return StageManifestLocked(bootstrapContext, LoadState(bootstrapContext.StatePath), bytes, ConfigAudience.Client);
			}
		}

		public static SynchronizationResult StageConfiguredUpdate()
		{
			lock (SynchronizeLock)
			{
				ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
				BootstrapContext bootstrapContext = CreateContext();
				BootstrapState bootstrapState = LoadState(bootstrapContext.StatePath);
				if (!bootstrapContext.Settings.HasManifestUrl)
				{
					return SynchronizationResult.Unchanged(bootstrapState.Revision);
				}
				string previousRevision = (LocalStateIsHealthy(bootstrapContext.BepInExRoot, bootstrapState) ? bootstrapState.Revision : "");
				byte[] array = DownloadManifest(bootstrapContext.Settings, previousRevision);
				return (array == null) ? SynchronizationResult.Unchanged(bootstrapState.Revision) : StageManifestLocked(bootstrapContext, bootstrapState, array, ConfigAudience.Server);
			}
		}

		public static string? ReadRelayManifest()
		{
			BootstrapContext bootstrapContext = CreateContext();
			if (!bootstrapContext.Settings.HasManifestUrl || !File.Exists(bootstrapContext.LastManifestPath))
			{
				return null;
			}
			if (new FileInfo(bootstrapContext.LastManifestPath).Length > 8388608)
			{
				throw new InvalidDataException("Manifest exceeds the 8 MiB relay limit");
			}
			return CreateRelayManifest(File.ReadAllText(bootstrapContext.LastManifestPath, Encoding.UTF8));
		}

		internal static string CreateRelayManifest(string manifestJson)
		{
			BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(Encoding.UTF8.GetBytes(manifestJson));
			if (bootstrapManifest.SchemaVersion >= 2)
			{
				bootstrapManifest.Configs = ApplicableConfigs(bootstrapManifest.Configs, ConfigAudience.Client).ToList();
				bootstrapManifest.Revision = bootstrapManifest.ClientRevision;
			}
			byte[] array = Json.Write(bootstrapManifest);
			if (array.Length > 8388608)
			{
				throw new InvalidDataException("Relayed manifest exceeds the 8 MiB limit");
			}
			return Encoding.UTF8.GetString(array);
		}

		private static SynchronizationResult ApplyManifestLocked(BootstrapContext context, BootstrapState previous, byte[] manifestBytes, ConfigAudience audience)
		{
			BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(manifestBytes);
			string manifestId = ManifestIdentity(bootstrapManifest);
			ValidateManifest(bootstrapManifest, manifestId, previous);
			List<ManifestConfig> list = ApplicableConfigs(bootstrapManifest.Configs, audience).ToList();
			if (string.Equals(previous.Revision, bootstrapManifest.Revision, StringComparison.Ordinal) && LocalStateIsHealthy(context.BepInExRoot, previous))
			{
				previous.ManifestId = manifestId;
				previous.GeneratedAt = bootstrapManifest.GeneratedAt;
				Json.WriteFile(context.StatePath, previous);
				WriteLastManifest(context.LastManifestPath, manifestBytes);
				BootstrapLog.Info("Revision " + ShortRevision(bootstrapManifest.Revision) + " is already installed");
				return SynchronizationResult.Unchanged(bootstrapManifest.Revision);
			}
			bool flag = PackageSetsDiffer(previous.Packages, bootstrapManifest.Packages.Select((ManifestPackage package) => package.Coordinate));
			bool flag2 = ManagedConfigsDiffer(previous, list);
			if (!flag && !flag2 && LocalStateIsHealthy(context.BepInExRoot, previous))
			{
				previous.ManifestId = manifestId;
				previous.Revision = bootstrapManifest.Revision;
				previous.GeneratedAt = bootstrapManifest.GeneratedAt;
				Json.WriteFile(context.StatePath, previous);
				WriteLastManifest(context.LastManifestPath, manifestBytes);
				BootstrapLog.Info("Accepted relay-only revision " + ShortRevision(bootstrapManifest.Revision) + " without changing local files");
				return SynchronizationResult.Applied(bootstrapManifest.Revision, packagesChanged: false, configsChanged: false);
			}
			string text = Path.Combine(context.StateRoot, "staging-" + Guid.NewGuid().ToString("N"));
			string text2 = Path.Combine(text, "plugins");
			string text3 = Path.Combine(text, "defaults");
			Directory.CreateDirectory(text2);
			Directory.CreateDirectory(text3);
			try
			{
				Dictionary<string, string> owners = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
				foreach (ManifestPackage package in bootstrapManifest.Packages)
				{
					PackageInstaller.Extract(GetPackageArchive(context.Settings, context.StateRoot, package), package, text2, text3, owners);
				}
				Apply(context.BepInExRoot, text2, text3, bootstrapManifest, list, manifestId, previous, context.StatePath);
				WriteLastManifest(context.LastManifestPath, manifestBytes);
				BootstrapLog.Info($"Installed revision {ShortRevision(bootstrapManifest.Revision)} with {bootstrapManifest.Packages.Count} packages and {list.Count} managed configs");
				return SynchronizationResult.Applied(bootstrapManifest.Revision, flag, flag2);
			}
			finally
			{
				TryDeleteDirectory(text);
			}
		}

		private static SynchronizationResult StageManifestLocked(BootstrapContext context, BootstrapState previous, byte[] manifestBytes, ConfigAudience audience)
		{
			BootstrapManifest bootstrapManifest = Json.Read<BootstrapManifest>(manifestBytes);
			string text = ManifestIdentity(bootstrapManifest);
			ValidateManifest(bootstrapManifest, text, previous);
			if (string.Equals(previous.Revision, bootstrapManifest.Revision, StringComparison.Ordinal) && string.Equals(previous.ManifestId, text, StringComparison.Ordinal) && LocalStateIsHealthy(context.BepInExRoot, previous))
			{
				return SynchronizationResult.Unchanged(bootstrapManifest.Revision);
			}
			foreach (ManifestPackage package in bootstrapManifest.Packages)
			{
				GetPackageArchive(context.Settings, context.StateRoot, package);
			}
			if (!PackageSetsDiffer(previous.Packages, bootstrapManifest.Packages.Select((ManifestPackage package) => package.Coordinate)))
			{
				return ApplyManifestLocked(context, previous, manifestBytes, audience);
			}
			string text2 = context.PendingManifestPath + ".new";
			File.WriteAllBytes(text2, manifestBytes);
			AtomicFile.Replace(text2, context.PendingManifestPath);
			BootstrapLog.Info("Staged revision " + ShortRevision(bootstrapManifest.Revision) + " for the next process start");
			return SynchronizationResult.Applied(bootstrapManifest.Revision, packagesChanged: true, configsChanged: true);
		}

		private static void ApplyPendingLocked(BootstrapContext context)
		{
			if (File.Exists(context.PendingManifestPath))
			{
				BootstrapLog.Info("Applying the pending managed mod revision before plugin loading");
				byte[] manifestBytes = File.ReadAllBytes(context.PendingManifestPath);
				ConfigAudience audience = ((!context.Settings.HasManifestUrl) ? ConfigAudience.Client : ConfigAudience.Server);
				ApplyManifestLocked(context, LoadState(context.StatePath), manifestBytes, audience);
				File.Delete(context.PendingManifestPath);
			}
		}

		private static void WriteLastManifest(string path, byte[] manifestBytes)
		{
			string text = path + ".new";
			File.WriteAllBytes(text, manifestBytes);
			AtomicFile.Replace(text, path);
		}

		private static BootstrapContext CreateContext()
		{
			string obj = Directory.GetParent(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Bootstrap assembly has no directory"))?.FullName ?? throw new InvalidOperationException("Cannot find BepInEx root");
			string path = Path.Combine(obj, "config", "ServerModBootstrap");
			string text = Path.Combine(obj, "xom-bootstrap");
			BootstrapLog.Initialize(text);
			return new BootstrapContext(obj, text, Path.Combine(text, "state.json"), Path.Combine(text, "last-manifest.json"), Path.Combine(text, "pending-manifest.json"), BootstrapSettings.Load(Path.Combine(path, "bootstrap.cfg")));
		}

		private static BootstrapState LoadState(string statePath)
		{
			if (!File.Exists(statePath))
			{
				return new BootstrapState();
			}
			try
			{
				return Json.ReadFile<BootstrapState>(statePath);
			}
			catch (Exception error)
			{
				BootstrapLog.Error("Ignoring corrupt local bootstrap state", error);
				return new BootstrapState();
			}
		}

		private static byte[]? DownloadManifest(BootstrapSettings settings, string previousRevision)
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			HttpClient val = CreateClient(settings.RequestTimeoutSeconds);
			try
			{
				HttpRequestMessage val2 = new HttpRequestMessage(HttpMethod.Get, settings.ManifestUrl);
				try
				{
					if (!string.IsNullOrWhiteSpace(previousRevision))
					{
						((HttpHeaders)val2.Headers).TryAddWithoutValidation("If-None-Match", "\"" + previousRevision + "\"");
					}
					HttpResponseMessage result = val.SendAsync(val2, (HttpCompletionOption)1).GetAwaiter().GetResult();
					try
					{
						if (result.StatusCode == HttpStatusCode.NotModified)
						{
							return null;
						}
						result.EnsureSuccessStatusCode();
						if (result.Content.Headers.ContentLength > 8388608)
						{
							throw new InvalidDataException("Manifest exceeds the 8 MiB limit");
						}
						using Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
						using MemoryStream memoryStream = new MemoryStream();
						byte[] array = new byte[81920];
						int num;
						while ((num = stream.Read(array, 0, array.Length)) > 0)
						{
							if (memoryStream.Length + num > 8388608)
							{
								throw new InvalidDataException("Manifest exceeds the 8 MiB limit");
							}
							memoryStream.Write(array, 0, num);
						}
						return memoryStream.ToArray();
					}
					finally
					{
						((IDisposable)result)?.Dispose();
					}
				}
				finally
				{
					((IDisposable)val2)?.Dispose();
				}
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}

		private static string GetPackageArchive(BootstrapSettings settings, string stateRoot, ManifestPackage package)
		{
			ValidatePackage(package);
			string text = Path.Combine(stateRoot, "cache");
			Directory.CreateDirectory(text);
			string path = Hex(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(package.Coordinate))) + ".zip";
			string text2 = Path.Combine(text, path);
			if (File.Exists(text2))
			{
				try
				{
					PackageInstaller.ValidateArchive(text2, package);
					return text2;
				}
				catch
				{
					File.Delete(text2);
				}
			}
			BootstrapLog.Info("Downloading " + package.Coordinate);
			HttpClient val = CreateClient(settings.RequestTimeoutSeconds);
			try
			{
				HttpResponseMessage result = val.GetAsync(package.DownloadUrl, (HttpCompletionOption)1).GetAwaiter().GetResult();
				try
				{
					result.EnsureSuccessStatusCode();
					if (result.Content.Headers.ContentLength > 524288000)
					{
						throw new InvalidDataException(package.Coordinate + " exceeds the 500 MiB archive limit");
					}
					string text3 = text2 + ".download";
					using (Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult())
					{
						using FileStream fileStream = new FileStream(text3, FileMode.Create, FileAccess.Write, FileShare.None);
						byte[] array = new byte[81920];
						long num = 0L;
						int num2;
						while ((num2 = stream.Read(array, 0, array.Length)) > 0)
						{
							num += num2;
							if (num > 524288000)
							{
								throw new InvalidDataException(package.Coordinate + " exceeds the 500 MiB archive limit");
							}
							fileStream.Write(array, 0, num2);
						}
					}
					try
					{
						PackageInstaller.ValidateArchive(text3, package);
						AtomicFile.Replace(text3, text2);
						return text2;
					}
					catch
					{
						if (File.Exists(text3))
						{
							File.Delete(text3);
						}
						throw;
					}
				}
				finally
				{
					((IDisposable)result)?.Dispose();
				}
			}
			finally
			{
				((IDisposable)val)?.Dispose();
			}
		}

		private static HttpClient CreateClient(int timeoutSeconds)
		{
			//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_0012: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Expected O, but got Unknown
			HttpClient val = new HttpClient
			{
				Timeout = TimeSpan.FromSeconds(timeoutSeconds)
			};
			val.DefaultRequestHeaders.UserAgent.ParseAdd("ServerModBootstrap/2.1");
			return val;
		}

		private static void Apply(string bepinexRoot, string stagedPlugins, string stagedDefaults, BootstrapManifest manifest, IReadOnlyCollection<ManifestConfig> applicableConfigs, string manifestId, BootstrapState previous, string statePath)
		{
			string text = Path.Combine(bepinexRoot, "plugins");
			string text2 = Path.Combine(text, "XomNghienManaged");
			string text3 = Path.Combine(text, "XomNghienManaged.backup");
			Directory.CreateDirectory(text);
			TryDeleteDirectory(text3);
			if (Directory.Exists(text2))
			{
				Directory.Move(text2, text3);
			}
			Dictionary<string, byte[]> backups = new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);
			try
			{
				Directory.Move(stagedPlugins, text2);
				ApplyPackageDefaults(bepinexRoot, stagedDefaults);
				ApplyManagedConfigs(bepinexRoot, applicableConfigs, previous.ManagedConfigs, backups);
				Json.WriteFile(statePath, new BootstrapState
				{
					ManifestId = manifestId,
					Revision = manifest.Revision,
					GeneratedAt = manifest.GeneratedAt,
					Packages = manifest.Packages.Select((ManifestPackage package) => package.Coordinate).ToList(),
					ManagedConfigs = applicableConfigs.Select((ManifestConfig config) => config.Path).ToList(),
					ManagedConfigHashes = applicableConfigs.ToDictionary<ManifestConfig, string, string>((ManifestConfig config) => config.Path, (ManifestConfig config) => config.Sha256, StringComparer.OrdinalIgnoreCase)
				});
				TryDeleteDirectory(text3);
			}
			catch
			{
				TryDeleteDirectory(text2);
				if (Directory.Exists(text3))
				{
					Directory.Move(text3, text2);
				}
				RestoreConfigs(bepinexRoot, backups);
				throw;
			}
		}

		private static void ApplyPackageDefaults(string bepinexRoot, string stagedDefaults)
		{
			if (!Directory.Exists(stagedDefaults))
			{
				return;
			}
			string[] files = Directory.GetFiles(stagedDefaults, "*", SearchOption.AllDirectories);
			foreach (string text in files)
			{
				string relative = RelativePath(stagedDefaults, text);
				string text2 = SafeConfigTarget(bepinexRoot, relative);
				if (!File.Exists(text2))
				{
					Directory.CreateDirectory(Path.GetDirectoryName(text2));
					File.Copy(text, text2);
				}
			}
		}

		private static void ApplyManagedConfigs(string bepinexRoot, IEnumerable<ManifestConfig> configs, IEnumerable<string> previousPaths, IDictionary<string, byte[]?> backups)
		{
			HashSet<string> nextPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (ManifestConfig config in configs)
			{
				string text = SafeConfigTarget(bepinexRoot, config.Path);
				nextPaths.Add(config.Path);
				BackupOnce(text, backups);
				byte[] array = Convert.FromBase64String(config.ContentBase64);
				if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(array)), config.Sha256))
				{
					throw new InvalidDataException("Managed config hash mismatch for " + config.Path);
				}
				Directory.CreateDirectory(Path.GetDirectoryName(text));
				string text2 = text + ".xn-new";
				File.WriteAllBytes(text2, array);
				AtomicFile.Replace(text2, text);
			}
			foreach (string item in previousPaths.Where((string path) => !nextPaths.Contains(path)))
			{
				string text3 = SafeConfigTarget(bepinexRoot, item);
				BackupOnce(text3, backups);
				if (File.Exists(text3))
				{
					File.Delete(text3);
				}
			}
		}

		private static void BackupOnce(string target, IDictionary<string, byte[]?> backups)
		{
			if (!backups.ContainsKey(target))
			{
				backups[target] = (File.Exists(target) ? File.ReadAllBytes(target) : null);
			}
		}

		private static void RestoreConfigs(string bepinexRoot, IDictionary<string, byte[]?> backups)
		{
			foreach (KeyValuePair<string, byte[]> backup in backups)
			{
				string key = backup.Key;
				string text = Path.Combine(bepinexRoot, "config");
				char directorySeparatorChar = Path.DirectorySeparatorChar;
				if (!key.StartsWith(text + directorySeparatorChar, StringComparison.OrdinalIgnoreCase))
				{
					continue;
				}
				if (backup.Value == null)
				{
					if (File.Exists(backup.Key))
					{
						File.Delete(backup.Key);
					}
				}
				else
				{
					Directory.CreateDirectory(Path.GetDirectoryName(backup.Key));
					File.WriteAllBytes(backup.Key, backup.Value);
				}
			}
		}

		private static bool ConfigsAreCurrent(string bepinexRoot, IEnumerable<ManifestConfig> configs)
		{
			foreach (ManifestConfig config in configs)
			{
				string path = SafeConfigTarget(bepinexRoot, config.Path);
				if (!File.Exists(path))
				{
					return false;
				}
				using FileStream inputStream = File.OpenRead(path);
				if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(inputStream)), config.Sha256))
				{
					return false;
				}
			}
			return true;
		}

		private static bool LocalStateIsHealthy(string bepinexRoot, BootstrapState state)
		{
			if (string.IsNullOrWhiteSpace(state.Revision) || !Directory.Exists(Path.Combine(bepinexRoot, "plugins", "XomNghienManaged")) || state.ManagedConfigHashes == null || state.ManagedConfigHashes.Count != state.ManagedConfigs.Count)
			{
				return false;
			}
			foreach (KeyValuePair<string, string> managedConfigHash in state.ManagedConfigHashes)
			{
				string path = SafeConfigTarget(bepinexRoot, managedConfigHash.Key);
				if (!File.Exists(path))
				{
					return false;
				}
				using FileStream inputStream = File.OpenRead(path);
				if (!FixedTimeEquals(Hex(SHA256.Create().ComputeHash(inputStream)), managedConfigHash.Value))
				{
					return false;
				}
			}
			return true;
		}

		internal static string SafeConfigTarget(string bepinexRoot, string relative)
		{
			string text = relative.Replace('\\', '/');
			if (text.Length == 0 || text.StartsWith("/", StringComparison.Ordinal) || text.Split(new char[1] { '/' }).Any((string part) => part.Length == 0 || part == "." || part == ".." || part.EndsWith(".", StringComparison.Ordinal) || part.EndsWith(" ", StringComparison.Ordinal) || part.IndexOfAny(new char[7] { '<', '>', ':', '"', '|', '?', '*' }) >= 0 || part.Any((char character) => character < ' ')))
			{
				throw new InvalidDataException("Unsafe managed config path: " + relative);
			}
			string fullPath = Path.GetFullPath(Path.Combine(bepinexRoot, "config"));
			string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, text.Replace('/', Path.DirectorySeparatorChar)));
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			if (!fullPath2.StartsWith(fullPath + directorySeparatorChar, StringComparison.OrdinalIgnoreCase))
			{
				throw new InvalidDataException("Unsafe managed config path: " + relative);
			}
			return fullPath2;
		}

		private static void ValidateManifest(BootstrapManifest manifest, string manifestId, BootstrapState previous)
		{
			if (manifest.SchemaVersion != 1 && manifest.SchemaVersion != 2)
			{
				throw new InvalidDataException("Unsupported bootstrap manifest schema");
			}
			if (manifestId.Length == 0 || manifestId.Length > 200)
			{
				throw new InvalidDataException("Bootstrap manifest identity is invalid");
			}
			if (manifest.Revision.Length != 64 || !manifest.Revision.All(IsHex))
			{
				throw new InvalidDataException("Bootstrap revision is invalid");
			}
			if (manifest.SchemaVersion >= 2 && (manifest.ClientRevision.Length != 64 || !manifest.ClientRevision.All(IsHex)))
			{
				throw new InvalidDataException("Bootstrap client revision is invalid");
			}
			if (!DateTimeOffset.TryParse(manifest.GeneratedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var result))
			{
				throw new InvalidDataException("Bootstrap manifest timestamp is invalid");
			}
			if (result > DateTimeOffset.UtcNow.AddMinutes(10.0))
			{
				throw new InvalidDataException("Bootstrap manifest timestamp is in the future");
			}
			if (string.Equals(previous.ManifestId, manifestId, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(previous.GeneratedAt) && DateTimeOffset.TryParse(previous.GeneratedAt, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var result2) && result < result2)
			{
				throw new InvalidDataException("Bootstrap manifest is older than the installed manifest");
			}
			if (manifest.Packages.Count > 500)
			{
				throw new InvalidDataException("Bootstrap manifest contains too many packages");
			}
			if (manifest.Configs.Count > 100)
			{
				throw new InvalidDataException("Bootstrap manifest contains too many configs");
			}
			HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (ManifestPackage package in manifest.Packages)
			{
				if (!hashSet.Add(package.Coordinate))
				{
					throw new InvalidDataException("Duplicate package " + package.Coordinate);
				}
			}
			HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			HashSet<string> hashSet3 = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			foreach (ManifestConfig config in manifest.Configs)
			{
				if (manifest.SchemaVersion >= 2 && !IsConfigTarget(config.Target))
				{
					throw new InvalidDataException("Invalid config target for " + config.Path);
				}
				if (AppliesTo(config, ConfigAudience.Server) && !hashSet2.Add(config.Path))
				{
					throw new InvalidDataException("Duplicate server config " + config.Path);
				}
				if (AppliesTo(config, ConfigAudience.Client) && !hashSet3.Add(config.Path))
				{
					throw new InvalidDataException("Duplicate client config " + config.Path);
				}
			}
		}

		private static IEnumerable<ManifestConfig> ApplicableConfigs(IEnumerable<ManifestConfig> configs, ConfigAudience audience)
		{
			foreach (ManifestConfig config in configs)
			{
				if (AppliesTo(config, audience))
				{
					yield return config;
				}
			}
		}

		private static bool AppliesTo(ManifestConfig config, ConfigAudience audience)
		{
			string text = (string.IsNullOrWhiteSpace(config.Target) ? "both" : config.Target.Trim().ToLowerInvariant());
			if (!(text == "both") && (audience != ConfigAudience.Server || !(text == "server")))
			{
				if (audience == ConfigAudience.Client)
				{
					return text == "client";
				}
				return false;
			}
			return true;
		}

		private static bool IsConfigTarget(string target)
		{
			string text = (string.IsNullOrWhiteSpace(target) ? "both" : target.Trim().ToLowerInvariant());
			if (!(text == "server") && !(text == "client"))
			{
				return text == "both";
			}
			return true;
		}

		private static string ManifestIdentity(BootstrapManifest manifest)
		{
			if (string.IsNullOrWhiteSpace(manifest.ManifestId))
			{
				return manifest.ServerId.Trim();
			}
			return manifest.ManifestId.Trim();
		}

		private static void ValidatePackage(ManifestPackage package)
		{
			if (package.Coordinate != package.Namespace + "-" + package.PackageName + "-" + package.VersionNumber)
			{
				throw new InvalidDataException("Package coordinate fields disagree");
			}
			if (!Uri.TryCreate(package.DownloadUrl, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps || (!result.Host.Equals("thunderstore.io", StringComparison.OrdinalIgnoreCase) && !result.Host.EndsWith(".thunderstore.io", StringComparison.OrdinalIgnoreCase)))
			{
				throw new InvalidDataException("Package download URL is not trusted");
			}
			if (package.FileSize > 524288000)
			{
				throw new InvalidDataException(package.Coordinate + " exceeds the package limit");
			}
		}

		internal static string RelativePath(string root, string path)
		{
			return Uri.UnescapeDataString(new Uri(AppendSeparator(Path.GetFullPath(root))).MakeRelativeUri(new Uri(Path.GetFullPath(path))).ToString()).Replace('/', Path.DirectorySeparatorChar);
		}

		private static string AppendSeparator(string path)
		{
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			if (!path.EndsWith(directorySeparatorChar.ToString(), StringComparison.Ordinal))
			{
				directorySeparatorChar = Path.DirectorySeparatorChar;
				return path + directorySeparatorChar;
			}
			return path;
		}

		private static string ShortRevision(string revision)
		{
			return revision.Substring(0, Math.Min(12, revision.Length));
		}

		private static bool IsHex(char value)
		{
			if (value < '0' || value > '9')
			{
				if (value >= 'a')
				{
					return value <= 'f';
				}
				return false;
			}
			return true;
		}

		private static string Hex(byte[] bytes)
		{
			return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
		}

		private static bool FixedTimeEquals(string left, string right)
		{
			if (left.Length != right.Length)
			{
				return false;
			}
			int num = 0;
			for (int i = 0; i < left.Length; i++)
			{
				num |= left[i] ^ right[i];
			}
			return num == 0;
		}

		internal static bool PackageSetsDiffer(IEnumerable<string> previous, IEnumerable<string> current)
		{
			return !new HashSet<string>(previous, StringComparer.OrdinalIgnoreCase).SetEquals(current);
		}

		private static bool ManagedConfigsDiffer(BootstrapState previous, IReadOnlyCollection<ManifestConfig> current)
		{
			if (previous.ManagedConfigHashes == null || previous.ManagedConfigHashes.Count != current.Count)
			{
				return true;
			}
			foreach (ManifestConfig item in current)
			{
				if (!previous.ManagedConfigHashes.TryGetValue(item.Path, out string value) || !string.Equals(value, item.Sha256, StringComparison.OrdinalIgnoreCase))
				{
					return true;
				}
			}
			return false;
		}

		private static void TryDeleteDirectory(string path)
		{
			try
			{
				if (Directory.Exists(path))
				{
					Directory.Delete(path, recursive: true);
				}
			}
			catch (Exception)
			{
			}
		}
	}
	public sealed class SynchronizationResult
	{
		public string Revision { get; }

		public bool Changed { get; }

		public bool PackagesChanged { get; }

		public bool ConfigsChanged { get; }

		private SynchronizationResult(string revision, bool changed, bool packagesChanged, bool configsChanged)
		{
			Revision = revision;
			Changed = changed;
			PackagesChanged = packagesChanged;
			ConfigsChanged = configsChanged;
		}

		internal static SynchronizationResult Unchanged(string revision)
		{
			return new SynchronizationResult(revision, changed: false, packagesChanged: false, configsChanged: false);
		}

		internal static SynchronizationResult Applied(string revision, bool packagesChanged, bool configsChanged)
		{
			return new SynchronizationResult(revision, changed: true, packagesChanged, configsChanged);
		}
	}
	internal static class Json
	{
		public static T Read<T>(byte[] bytes)
		{
			//IL_0012: Unknown result type (might be due to invalid IL or missing references)
			using MemoryStream memoryStream = new MemoryStream(bytes, writable: false);
			return (T)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).ReadObject((Stream)memoryStream);
		}

		public static T ReadFile<T>(string path)
		{
			return Read<T>(File.ReadAllBytes(path));
		}

		public static byte[] Write<T>(T value)
		{
			//IL_0010: Unknown result type (might be due to invalid IL or missing references)
			using MemoryStream memoryStream = new MemoryStream();
			((XmlObjectSerializer)new DataContractJsonSerializer(typeof(T))).WriteObject((Stream)memoryStream, (object)value);
			return memoryStream.ToArray();
		}

		public static void WriteFile<T>(string path, T value)
		{
			string text = path + ".tmp";
			Directory.CreateDirectory(Path.GetDirectoryName(path));
			File.WriteAllBytes(text, Write(value));
			AtomicFile.Replace(text, path);
		}
	}
	[DataContract]
	internal sealed class BootstrapManifest
	{
		[DataMember(Name = "schemaVersion", IsRequired = true)]
		public int SchemaVersion { get; set; }

		[DataMember(Name = "manifestId", EmitDefaultValue = false)]
		public string ManifestId { get; set; } = "";

		[DataMember(Name = "serverId", EmitDefaultValue = false)]
		public string ServerId { get; set; } = "";

		[DataMember(Name = "revision", IsRequired = true)]
		public string Revision { get; set; } = "";

		[DataMember(Name = "clientRevision", EmitDefaultValue = false)]
		public string ClientRevision { get; set; } = "";

		[DataMember(Name = "generatedAt", IsRequired = true)]
		public string GeneratedAt { get; set; } = "";

		[DataMember(Name = "packages", IsRequired = true)]
		public List<ManifestPackage> Packages { get; set; } = new List<ManifestPackage>();

		[DataMember(Name = "configs", IsRequired = true)]
		public List<ManifestConfig> Configs { get; set; } = new List<ManifestConfig>();
	}
	[DataContract]
	internal sealed class ManifestPackage
	{
		[DataMember(Name = "coordinate", IsRequired = true)]
		public string Coordinate { get; set; } = "";

		[DataMember(Name = "namespace", IsRequired = true)]
		public string Namespace { get; set; } = "";

		[DataMember(Name = "packageName", IsRequired = true)]
		public string PackageName { get; set; } = "";

		[DataMember(Name = "versionNumber", IsRequired = true)]
		public string VersionNumber { get; set; } = "";

		[DataMember(Name = "downloadUrl", IsRequired = true)]
		public string DownloadUrl { get; set; } = "";

		[DataMember(Name = "fileSize")]
		public long? FileSize { get; set; }

		[DataMember(Name = "dependencies", IsRequired = true)]
		public List<string> Dependencies { get; set; } = new List<string>();
	}
	[DataContract]
	internal sealed class ManifestConfig
	{
		[DataMember(Name = "path", IsRequired = true)]
		public string Path { get; set; } = "";

		[DataMember(Name = "sha256", IsRequired = true)]
		public string Sha256 { get; set; } = "";

		[DataMember(Name = "contentBase64", IsRequired = true)]
		public string ContentBase64 { get; set; } = "";

		[DataMember(Name = "target", EmitDefaultValue = false)]
		public string Target { get; set; } = "";
	}
	[DataContract]
	internal sealed class BootstrapState
	{
		[DataMember(Name = "manifestId", EmitDefaultValue = false)]
		public string ManifestId { get; set; } = "";

		[DataMember(Name = "revision", IsRequired = true)]
		public string Revision { get; set; } = "";

		[DataMember(Name = "generatedAt")]
		public string GeneratedAt { get; set; } = "";

		[DataMember(Name = "packages", IsRequired = true)]
		public List<string> Packages { get; set; } = new List<string>();

		[DataMember(Name = "managedConfigs", IsRequired = true)]
		public List<string> ManagedConfigs { get; set; } = new List<string>();

		[DataMember(Name = "managedConfigHashes", EmitDefaultValue = false)]
		public Dictionary<string, string> ManagedConfigHashes { get; set; } = new Dictionary<string, string>();
	}
	[DataContract]
	internal sealed class PackageManifest
	{
		[DataMember(Name = "name", IsRequired = true)]
		public string Name { get; set; } = "";

		[DataMember(Name = "version_number", IsRequired = true)]
		public string VersionNumber { get; set; } = "";
	}
	internal static class PackageInstaller
	{
		private enum InstallKind
		{
			Plugin,
			ConfigDefault,
			RejectEarlyLoader
		}

		private sealed class InstallRoute
		{
			public InstallKind Kind { get; }

			public string RelativePath { get; }

			public InstallRoute(InstallKind kind, string relativePath)
			{
				Kind = kind;
				RelativePath = relativePath;
			}
		}

		private const int MaximumEntries = 20000;

		private const long MaximumExpandedBytes = 2147483648L;

		private static readonly HashSet<string> Metadata = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "manifest.json", "README.md", "CHANGELOG.md", "icon.png" };

		public static void ValidateArchive(string archivePath, ManifestPackage package)
		{
			using ZipArchive zipArchive = ZipFile.OpenRead(archivePath);
			if (zipArchive.Entries.Count > 20000)
			{
				throw new InvalidDataException("Package contains too many files");
			}
			foreach (ZipArchiveEntry entry in zipArchive.Entries)
			{
				ValidateEntry(entry);
			}
			using Stream stream = (zipArchive.Entries.FirstOrDefault((ZipArchiveEntry entry) => entry.FullName.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)) ?? throw new InvalidDataException(package.Coordinate + " has no root manifest.json")).Open();
			using MemoryStream memoryStream = new MemoryStream();
			stream.CopyTo(memoryStream);
			PackageManifest packageManifest = Json.Read<PackageManifest>(memoryStream.ToArray());
			if (!string.Equals(packageManifest.Name, package.PackageName, StringComparison.OrdinalIgnoreCase) || !string.Equals(packageManifest.VersionNumber, package.VersionNumber, StringComparison.Ordinal))
			{
				throw new InvalidDataException(package.Coordinate + " archive identity does not match its manifest");
			}
		}

		public static void Extract(string archivePath, ManifestPackage package, string pluginsRoot, string defaultsRoot, IDictionary<string, string> owners)
		{
			ValidateArchive(archivePath, package);
			using ZipArchive zipArchive = ZipFile.OpenRead(archivePath);
			long num = 0L;
			foreach (ZipArchiveEntry entry in zipArchive.Entries)
			{
				ValidateEntry(entry);
				num += entry.Length;
				if (num > 2147483648u)
				{
					throw new InvalidDataException(package.Coordinate + " exceeds the expanded package limit");
				}
				if (entry.FullName.EndsWith("/", StringComparison.Ordinal))
				{
					continue;
				}
				string text = StripLoaderWrapper(entry.FullName).Replace('\\', '/');
				if (!text.Contains("/") && Metadata.Contains(text))
				{
					continue;
				}
				InstallRoute installRoute = Route(text, package);
				if (installRoute.Kind == InstallKind.RejectEarlyLoader)
				{
					throw new InvalidDataException(package.Coordinate + " contains BepInEx core, patcher, or monomod files and cannot be managed by this bootstrap");
				}
				string root = ((installRoute.Kind == InstallKind.ConfigDefault) ? defaultsRoot : pluginsRoot);
				string text2 = installRoute.RelativePath.Replace('\\', '/');
				string key = installRoute.Kind.ToString() + ":" + text2;
				if (owners.TryGetValue(key, out string value))
				{
					throw new InvalidDataException(value + " and " + package.Coordinate + " both install " + text2);
				}
				owners[key] = package.Coordinate;
				string path = SafeOutput(root, installRoute.RelativePath);
				Directory.CreateDirectory(Path.GetDirectoryName(path));
				using Stream stream = entry.Open();
				using FileStream destination = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
				stream.CopyTo(destination);
			}
		}

		private static InstallRoute Route(string rawPath, ManifestPackage package)
		{
			List<string> list = rawPath.Split(new char[1] { '/' }, StringSplitOptions.RemoveEmptyEntries).ToList();
			if (list.Count > 0 && list[0].Equals("BepInEx", StringComparison.OrdinalIgnoreCase))
			{
				list.RemoveAt(0);
			}
			if (list.Count == 0)
			{
				return new InstallRoute(InstallKind.Plugin, PackageDirectory(package));
			}
			string text = list[0];
			if (text.Equals("core", StringComparison.OrdinalIgnoreCase) || text.Equals("patchers", StringComparison.OrdinalIgnoreCase) || text.Equals("monomod", StringComparison.OrdinalIgnoreCase))
			{
				return new InstallRoute(InstallKind.RejectEarlyLoader, string.Join("/", list));
			}
			if (text.Equals("config", StringComparison.OrdinalIgnoreCase))
			{
				return new InstallRoute(InstallKind.ConfigDefault, string.Join("/", list.Skip(1)));
			}
			if (text.Equals("plugins", StringComparison.OrdinalIgnoreCase))
			{
				list.RemoveAt(0);
			}
			return new InstallRoute(InstallKind.Plugin, PackageDirectory(package) + "/" + string.Join("/", list));
		}

		private static string PackageDirectory(ManifestPackage package)
		{
			return package.Namespace + "-" + package.PackageName;
		}

		private static string StripLoaderWrapper(string path)
		{
			if (!path.StartsWith("BepInExPack_Valheim/", StringComparison.OrdinalIgnoreCase))
			{
				return path;
			}
			return path.Substring("BepInExPack_Valheim/".Length);
		}

		private static void ValidateEntry(ZipArchiveEntry entry)
		{
			string text = entry.FullName.Replace('\\', '/');
			if (text.StartsWith("/", StringComparison.Ordinal) || text.Split(new char[1] { '/' }).Any((string part) => part == ".." || part.Contains(":")))
			{
				throw new InvalidDataException("Package archive contains an unsafe path");
			}
			if (((entry.ExternalAttributes >> 16) & 0xF000) == 40960)
			{
				throw new InvalidDataException("Package archive contains a symbolic link");
			}
		}

		private static string SafeOutput(string root, string relative)
		{
			string fullPath = Path.GetFullPath(root);
			string fullPath2 = Path.GetFullPath(Path.Combine(fullPath, relative.Replace('/', Path.DirectorySeparatorChar)));
			char directorySeparatorChar = Path.DirectorySeparatorChar;
			if (!fullPath2.StartsWith(fullPath + directorySeparatorChar, StringComparison.OrdinalIgnoreCase))
			{
				throw new InvalidDataException("Package archive escaped its managed root");
			}
			return fullPath2;
		}
	}
	internal static class RpcReflectionBridge
	{
		private const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

		public static void RegisterString(object rpc, string name, Action<object, string> handler)
		{
			MethodInfo methodInfo = ((from method in rpc.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
				where method.Name == "Register" && method.IsGenericMethodDefinition
				where method.GetGenericArguments().Length == 1
				select method).FirstOrDefault(delegate(MethodInfo method)
			{
				ParameterInfo[] parameters2 = method.GetParameters();
				return parameters2.Length == 2 && parameters2[0].ParameterType == typeof(string);
			}) ?? throw new MissingMethodException(rpc.GetType().FullName, "Register<T>(string, callback)")).MakeGenericMethod(typeof(string));
			Type parameterType = methodInfo.GetParameters()[1].ParameterType;
			ParameterInfo[] parameters = (parameterType.GetMethod("Invoke") ?? throw new InvalidOperationException("RPC callback type is not a delegate")).GetParameters();
			if (parameters.Length != 2 || parameters[1].ParameterType != typeof(string))
			{
				throw new InvalidOperationException("Valheim string RPC callback signature is unsupported");
			}
			ParameterExpression parameterExpression = Expression.Parameter(parameters[0].ParameterType, "rpc");
			ParameterExpression parameterExpression2 = Expression.Parameter(typeof(string), "payload");
			InvocationExpression body = Expression.Invoke(Expression.Constant(handler), Expression.Convert(parameterExpression, typeof(object)), parameterExpression2);
			Delegate obj = Expression.Lambda(parameterType, body, parameterExpression, parameterExpression2).Compile();
			methodInfo.Invoke(rpc, new object[2] { name, obj });
		}

		public static void InvokeString(object rpc, string name, string payload)
		{
			(rpc.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(delegate(MethodInfo method)
			{
				if (method.Name != "Invoke")
				{
					return false;
				}
				ParameterInfo[] parameters = method.GetParameters();
				return parameters.Length == 2 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == typeof(object[]);
			}) ?? throw new MissingMethodException(rpc.GetType().FullName, "Invoke(string, object[])")).Invoke(rpc, new object[2]
			{
				name,
				new object[1] { payload }
			});
		}
	}
}

BepInEx/plugins/ServerModBootstrap/XomNghienRuntimeUpdater.dll

Decompiled 8 hours ago
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;
using XomNghien.Bootstrap;

[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("XomNghienRuntimeUpdater")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("2.1.0.0")]
[assembly: AssemblyInformationalVersion("2.1.0+cede9045c60d8f4cbe5376fd902eda1df65f5555")]
[assembly: AssemblyProduct("XomNghienRuntimeUpdater")]
[assembly: AssemblyTitle("XomNghienRuntimeUpdater")]
[assembly: AssemblyVersion("2.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

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

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

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace XomNghien.RuntimeUpdater
{
	internal sealed class ManifestHandshake
	{
		private const string ManifestRpc = "ServerModBootstrap_Manifest_v1";

		private const BindingFlags AllMembers = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;

		private static ManifestHandshake? _instance;

		private readonly ManualLogSource _log;

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

		public void Install(Harmony harmony)
		{
			//IL_006d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0072: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Expected O, but got Unknown
			_instance = this;
			Type type = AccessTools.TypeByName("ZNet") ?? throw new TypeLoadException("Valheim ZNet type was not found");
			MethodInfo methodInfo = AccessTools.Method(type, "OnNewConnection", (Type[])null, (Type[])null) ?? throw new MissingMethodException(type.FullName, "OnNewConnection");
			MethodInfo methodInfo2 = AccessTools.Method(typeof(ManifestHandshake), "OnNewConnectionPostfix", (Type[])null, (Type[])null) ?? throw new MissingMethodException("OnNewConnectionPostfix");
			harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2)
			{
				priority = 800
			}, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			_log.LogInfo((object)"Installed the server manifest relay handshake");
		}

		private static void OnNewConnectionPostfix(object __instance, object __0)
		{
			_instance?.OnNewConnection(__instance, __0);
		}

		private void OnNewConnection(object znet, object peer)
		{
			try
			{
				object obj = peer.GetType().GetField("m_rpc", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(peer);
				if (obj == null)
				{
					return;
				}
				RpcReflectionBridge.RegisterString(obj, "ServerModBootstrap_Manifest_v1", (Action<object, string>)ReceiveManifest);
				if (IsServer(znet))
				{
					string text = BootstrapSynchronizer.ReadRelayManifest();
					if (string.IsNullOrWhiteSpace(text))
					{
						_log.LogWarning((object)"A client connected, but no validated manifest is available to relay");
						return;
					}
					RpcReflectionBridge.InvokeString(obj, "ServerModBootstrap_Manifest_v1", text);
					_log.LogInfo((object)$"Relayed the manifest to a connecting client ({text.Length} characters)");
				}
			}
			catch (Exception ex)
			{
				_log.LogError((object)("Manifest handshake failed: " + ex));
			}
		}

		private static void ReceiveManifest(object _, string manifest)
		{
			RuntimeUpdaterPlugin.Instance?.QueueRelayedManifest(manifest);
		}

		private static bool IsServer(object znet)
		{
			object obj = znet.GetType().GetMethod("IsServer", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null)?.Invoke(znet, null);
			if (obj is bool)
			{
				return (bool)obj;
			}
			FieldInfo field = znet.GetType().GetField("m_isServer", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
			obj = field?.GetValue(field.IsStatic ? null : znet);
			bool flag = default(bool);
			int num;
			if (obj is bool)
			{
				flag = (bool)obj;
				num = 1;
			}
			else
			{
				num = 0;
			}
			return (byte)((uint)num & (flag ? 1u : 0u)) != 0;
		}
	}
	[BepInPlugin("org.servermodbootstrap.runtime-updater", "Server Mod Bootstrap Runtime Updater", "2.1.0")]
	public sealed class RuntimeUpdaterPlugin : BaseUnityPlugin
	{
		public const string PluginGuid = "org.servermodbootstrap.runtime-updater";

		public const string PluginName = "Server Mod Bootstrap Runtime Updater";

		public const string PluginVersion = "2.1.0";

		private ConfigEntry<bool> _enabled;

		private ConfigEntry<int> _pollIntervalSeconds;

		private ConfigEntry<bool> _autoRestart;

		private ConfigEntry<int> _restartDelaySeconds;

		private ConfigEntry<bool> _restartForConfigChanges;

		private Task<SynchronizationResult>? _check;

		private Task<SynchronizationResult>? _relayedCheck;

		private readonly object _relayLock = new object();

		private string? _pendingManifest;

		private float _nextCheckAt;

		private float? _restartAt;

		private bool _restartRequested;

		private bool _isDedicatedServer;

		private bool _showRestartPrompt;

		private string _promptMessage = "Server mods were updated. Restart Valheim before reconnecting.";

		private Harmony? _harmony;

		internal static RuntimeUpdaterPlugin? Instance { get; private set; }

		private void Awake()
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Expected O, but got Unknown
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b3: Expected O, but got Unknown
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f4: Expected O, but got Unknown
			Instance = this;
			_enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Live updates", "Enabled", true, "Poll the server manifest while a dedicated server is running.");
			_pollIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Live updates", "PollIntervalSeconds", 60, new ConfigDescription("Seconds between manifest checks.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 3600), Array.Empty<object>()));
			_autoRestart = ((BaseUnityPlugin)this).Config.Bind<bool>("Restart", "AutoRestartForModChanges", true, "Save and quit after installing a changed plugin set. The server supervisor must restart the process.");
			_restartDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Restart", "DelaySeconds", 60, new ConfigDescription("Delay before saving and quitting after a mod change.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 1800), Array.Empty<object>()));
			_restartForConfigChanges = ((BaseUnityPlugin)this).Config.Bind<bool>("Restart", "RestartForConfigChanges", false, "Also restart after config-only changes. Leave false for mods such as AzuAntiCheat that watch their config files.");
			_isDedicatedServer = IsDedicatedServer();
			_harmony = new Harmony("org.servermodbootstrap.runtime-updater");
			try
			{
				new ManifestHandshake(((BaseUnityPlugin)this).Logger).Install(_harmony);
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Could not install the manifest handshake: " + ex));
			}
			if (!_isDedicatedServer)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Client manifest relay receiver is ready; no client configuration is required");
				return;
			}
			_nextCheckAt = Time.realtimeSinceStartup + (float)Math.Max(30, _pollIntervalSeconds.Value);
			((BaseUnityPlugin)this).Logger.LogInfo((object)$"Live server mod polling enabled every {Math.Max(30, _pollIntervalSeconds.Value)} seconds");
		}

		private void Update()
		{
			CompleteRelayedCheck();
			StartRelayedCheck();
			if (!_isDedicatedServer || !_enabled.Value || _restartRequested)
			{
				return;
			}
			if (_restartAt.HasValue)
			{
				if (Time.realtimeSinceStartup >= _restartAt.Value)
				{
					RestartServer();
				}
				return;
			}
			CompleteCheck();
			if (_check == null && Time.realtimeSinceStartup >= _nextCheckAt)
			{
				_nextCheckAt = Time.realtimeSinceStartup + (float)Math.Max(30, _pollIntervalSeconds.Value);
				_check = Task.Run((Func<SynchronizationResult>)BootstrapSynchronizer.StageConfiguredUpdate);
			}
		}

		internal void QueueRelayedManifest(string manifest)
		{
			if (!_isDedicatedServer && !string.IsNullOrWhiteSpace(manifest))
			{
				lock (_relayLock)
				{
					_pendingManifest = manifest;
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Received the server mod manifest; checking local managed mods");
			}
		}

		private void StartRelayedCheck()
		{
			if (_isDedicatedServer || _relayedCheck != null)
			{
				return;
			}
			string manifest;
			lock (_relayLock)
			{
				manifest = _pendingManifest;
				_pendingManifest = null;
			}
			if (manifest != null)
			{
				_relayedCheck = Task.Run(() => BootstrapSynchronizer.StageRelayedManifest(manifest));
			}
		}

		private void CompleteRelayedCheck()
		{
			if (_relayedCheck == null || !_relayedCheck.IsCompleted)
			{
				return;
			}
			Task<SynchronizationResult> relayedCheck = _relayedCheck;
			_relayedCheck = null;
			if (relayedCheck.IsFaulted)
			{
				string text = relayedCheck.Exception?.GetBaseException().Message ?? "Unknown manifest error";
				((BaseUnityPlugin)this).Logger.LogError((object)("The server manifest was rejected: " + text));
				DisconnectClient();
				ShowPrompt("The server's mod manifest was invalid. Connection stopped.\n\n" + text);
				return;
			}
			SynchronizationResult result = relayedCheck.Result;
			if (!result.Changed)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)"The installed managed mods already match the server");
				return;
			}
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Staged server revision " + ShortRevision(result.Revision) + "; Valheim must restart before reconnecting"));
			WriteRestartMarker(result);
			DisconnectClient();
			ShowPrompt("This server requires a different managed mod set.\n\nThe mods are downloaded and staged. Restart Valheim, then connect again.");
		}

		private void CompleteCheck()
		{
			if (_check == null || !_check.IsCompleted)
			{
				return;
			}
			Task<SynchronizationResult> check = _check;
			_check = null;
			if (check.IsFaulted)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Live mod synchronization failed: " + check.Exception?.GetBaseException()));
				return;
			}
			SynchronizationResult result = check.Result;
			if (!result.Changed)
			{
				return;
			}
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Installed managed revision " + ShortRevision(result.Revision)));
			if (result.PackagesChanged || (_restartForConfigChanges.Value && result.ConfigsChanged))
			{
				WriteRestartMarker(result);
				if (!_autoRestart.Value)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Managed mods changed. A server restart is required; automatic restart is disabled.");
					return;
				}
				_restartAt = Time.realtimeSinceStartup + (float)Math.Max(10, _restartDelaySeconds.Value);
				((BaseUnityPlugin)this).Logger.LogWarning((object)$"Managed mods changed. Saving and quitting for supervisor restart in {Math.Max(10, _restartDelaySeconds.Value)} seconds.");
			}
		}

		private void RestartServer()
		{
			_restartRequested = true;
			TrySaveWorld();
			((BaseUnityPlugin)this).Logger.LogWarning((object)"Quitting dedicated server so its supervisor can load the new managed mods.");
			Application.Quit();
		}

		private void DisconnectClient()
		{
			try
			{
				Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
					select assembly.GetType("Game", throwOnError: false)).FirstOrDefault((Type type2) => type2 != null);
				if (!(type == null))
				{
					object obj = type.GetField("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null) ?? type.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null);
					type.GetMethod("Logout", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2]
					{
						typeof(bool),
						typeof(bool)
					}, null)?.Invoke(obj, new object[2] { true, true });
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Could not disconnect after a managed mod update: " + ex.Message));
			}
		}

		private void ShowPrompt(string message)
		{
			_promptMessage = message;
			_showRestartPrompt = true;
		}

		private void OnGUI()
		{
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			if (_showRestartPrompt && !_isDedicatedServer)
			{
				Rect val = default(Rect);
				((Rect)(ref val))..ctor(((float)Screen.width - 520f) / 2f, ((float)Screen.height - 220f) / 2f, 520f, 220f);
				GUI.Box(val, "Server Mod Update");
				GUI.Label(new Rect(((Rect)(ref val)).x + 24f, ((Rect)(ref val)).y + 48f, 472f, 100f), _promptMessage);
				if (GUI.Button(new Rect(((Rect)(ref val)).x + 150f, ((Rect)(ref val)).y + 164f, 220f, 36f), "Quit Valheim now"))
				{
					Application.Quit();
				}
			}
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
			if (Instance == this)
			{
				Instance = null;
			}
		}

		private void TrySaveWorld()
		{
			try
			{
				Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
					select assembly.GetType("ZNet", throwOnError: false)).FirstOrDefault((Type type2) => type2 != null);
				if (type == null)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not find ZNet; quitting without an explicit pre-restart save.");
					return;
				}
				object obj = type.GetField("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null) ?? type.GetProperty("instance", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null, null);
				MethodInfo method = type.GetMethod("Save", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(bool) }, null);
				if (obj == null || method == null)
				{
					((BaseUnityPlugin)this).Logger.LogWarning((object)"Could not invoke ZNet.Save; Valheim's normal quit handling will be used.");
					return;
				}
				method.Invoke(obj, new object[1] { false });
				((BaseUnityPlugin)this).Logger.LogInfo((object)"Requested a world save before restart.");
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)("Pre-restart world save failed: " + ex));
			}
		}

		private static void WriteRestartMarker(SynchronizationResult result)
		{
			string text = Path.Combine(Paths.BepInExRootPath, "xom-bootstrap");
			Directory.CreateDirectory(text);
			File.WriteAllText(Path.Combine(text, "restart-required"), $"revision={result.Revision}{Environment.NewLine}createdAt={DateTimeOffset.UtcNow:O}{Environment.NewLine}");
		}

		private static string ShortRevision(string revision)
		{
			return revision.Substring(0, Math.Min(12, revision.Length));
		}

		private static bool IsDedicatedServer()
		{
			return Environment.GetCommandLineArgs().Any((string argument) => argument.Equals("-batchmode", StringComparison.OrdinalIgnoreCase));
		}
	}
}