Decompiled source of ValheimSaveShare v1.0.6

plugins/ValheimSaveShare.dll

Decompiled a month ago
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("ValheimSaveShare")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.6.0")]
[assembly: AssemblyInformationalVersion("1.0.6+d3f2aab63efbf9442e5983f5e047d4e63a6ba486")]
[assembly: AssemblyProduct("ValheimSaveShare")]
[assembly: AssemblyTitle("ValheimSaveShare")]
[assembly: AssemblyVersion("1.0.6.0")]
namespace ValheimSaveShare;

internal class GitHubException : Exception
{
	public readonly HttpStatusCode? Status;

	public GitHubException(string message, HttpStatusCode? status)
		: base(message)
	{
		Status = status;
	}
}
internal class UserException : Exception
{
	public UserException(string message)
		: base(message)
	{
	}
}
internal class GitHubItem
{
	public string Name;

	public string Path;

	public string Type;

	public long Size;
}
internal class GitHubClient
{
	private const string ApiBase = "https://api.github.com";

	private const string RawBase = "https://raw.githubusercontent.com";

	private readonly HttpClient _http;

	private readonly bool _hasToken;

	public GitHubClient(string token, string proxy = null)
	{
		_hasToken = !string.IsNullOrEmpty(token);
		HttpClientHandler httpClientHandler = new HttpClientHandler();
		if (!string.IsNullOrEmpty(proxy))
		{
			try
			{
				httpClientHandler.Proxy = new WebProxy(proxy);
				httpClientHandler.UseProxy = true;
				SaveSharePlugin.Log.LogInfo((object)("GitHubClient using proxy: " + proxy));
			}
			catch (Exception ex)
			{
				SaveSharePlugin.Log.LogWarning((object)("invalid proxy config, using direct connection: " + ex.Message));
			}
		}
		_http = new HttpClient(httpClientHandler);
		_http.Timeout = TimeSpan.FromMinutes(10.0);
		_http.DefaultRequestHeaders.UserAgent.ParseAdd("ValheimSaveShare/1.0");
		_http.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
		if (_hasToken)
		{
			_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
		}
	}

	public string GetDefaultBranch(string owner, string repo, CancellationToken ct)
	{
		JObject json = GetJson("https://api.github.com/repos/" + owner + "/" + repo, ct);
		string text = ((json == null) ? null : ((object)json["default_branch"])?.ToString());
		if (!string.IsNullOrEmpty(text))
		{
			return text;
		}
		return "main";
	}

	public List<GitHubItem> ListDirectory(string owner, string repo, string path, string branch, CancellationToken ct)
	{
		string text = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + ShareUrl.EscapePath(path ?? "") + "?per_page=100";
		if (!string.IsNullOrEmpty(branch))
		{
			text = text + "&ref=" + Uri.EscapeDataString(branch);
		}
		JArray obj = JArray.Parse(Send(HttpMethod.Get, text, ct).Content.ReadAsStringAsync().GetAwaiter().GetResult());
		List<GitHubItem> list = new List<GitHubItem>();
		foreach (JToken item in obj)
		{
			list.Add(new GitHubItem
			{
				Name = item.Value<string>((object)"name"),
				Path = item.Value<string>((object)"path"),
				Type = item.Value<string>((object)"type"),
				Size = item.Value<long?>((object)"size").GetValueOrDefault()
			});
		}
		return list;
	}

	public byte[] TryDownloadFile(string owner, string repo, string path, string branch, Action<long, long> progress, CancellationToken ct)
	{
		try
		{
			return DownloadFile(owner, repo, path, branch, progress, ct);
		}
		catch (GitHubException ex) when (ex.Status == HttpStatusCode.NotFound)
		{
			return null;
		}
	}

	public byte[] DownloadFile(string owner, string repo, string path, string branch, Action<long, long> progress, CancellationToken ct)
	{
		string url = "https://raw.githubusercontent.com/" + owner + "/" + repo + "/" + branch + "/" + ShareUrl.EscapePath(path);
		try
		{
			return GetWithProgress(url, progress, ct);
		}
		catch (GitHubException ex) when (ex.Status == HttpStatusCode.NotFound && _hasToken)
		{
			string requestUri = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + ShareUrl.EscapePath(path) + "?ref=" + Uri.EscapeDataString(branch);
			HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
			httpRequestMessage.Headers.Accept.Clear();
			httpRequestMessage.Headers.Accept.ParseAdd("application/vnd.github.raw");
			return GetWithProgress(httpRequestMessage, progress, ct);
		}
	}

	public string TryGetFileSha(string owner, string repo, string path, string branch, CancellationToken ct)
	{
		string url = "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + ShareUrl.EscapePath(path) + "?ref=" + Uri.EscapeDataString(branch);
		try
		{
			JObject json = GetJson(url, ct);
			return (json == null) ? null : ((object)json["sha"])?.ToString();
		}
		catch (GitHubException ex) when (ex.Status == HttpStatusCode.NotFound)
		{
			return null;
		}
	}

	public void UploadFile(string owner, string repo, string path, string branch, byte[] data, string message, CancellationToken ct)
	{
		//IL_0010: Unknown result type (might be due to invalid IL or missing references)
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		//IL_0039: Unknown result type (might be due to invalid IL or missing references)
		//IL_0051: Expected O, but got Unknown
		int num = 0;
		while (true)
		{
			string text = TryGetFileSha(owner, repo, path, branch, ct);
			JObject val = new JObject
			{
				["message"] = JToken.op_Implicit(message),
				["branch"] = JToken.op_Implicit(branch),
				["content"] = JToken.op_Implicit(Convert.ToBase64String(data))
			};
			if (!string.IsNullOrEmpty(text))
			{
				val["sha"] = JToken.op_Implicit(text);
			}
			HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Put, "https://api.github.com/repos/" + owner + "/" + repo + "/contents/" + ShareUrl.EscapePath(path))
			{
				Content = new StringContent(((object)val).ToString(), Encoding.UTF8, "application/json")
			};
			try
			{
				Send(req, ct);
				break;
			}
			catch (GitHubException ex) when ((ex.Status == HttpStatusCode.Conflict || ex.Status == HttpStatusCode.UnprocessableEntity) && num == 0)
			{
			}
			num++;
		}
	}

	private JObject GetJson(string url, CancellationToken ct)
	{
		return JObject.Parse(Send(HttpMethod.Get, url, ct).Content.ReadAsStringAsync().GetAwaiter().GetResult());
	}

	private HttpResponseMessage Send(HttpMethod method, string url, CancellationToken ct)
	{
		return Send(new HttpRequestMessage(method, url), ct);
	}

	private HttpResponseMessage Send(HttpRequestMessage req, CancellationToken ct)
	{
		HttpResponseMessage result;
		try
		{
			result = _http.SendAsync(req, ct).GetAwaiter().GetResult();
		}
		catch (OperationCanceledException)
		{
			throw;
		}
		catch (Exception ex2)
		{
			throw new GitHubException("网络错误:" + ex2.Message, null);
		}
		if (result.StatusCode >= HttpStatusCode.MultipleChoices)
		{
			string body = "";
			try
			{
				body = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
			}
			catch
			{
			}
			string message = ExtractMessage(body) ?? $"HTTP {(int)result.StatusCode} {result.StatusCode}";
			result.Dispose();
			throw new GitHubException(message, result.StatusCode);
		}
		return result;
	}

	private byte[] GetWithProgress(string url, Action<long, long> progress, CancellationToken ct)
	{
		return GetWithProgress(new HttpRequestMessage(HttpMethod.Get, url), progress, ct);
	}

	private byte[] GetWithProgress(HttpRequestMessage req, Action<long, long> progress, CancellationToken ct)
	{
		HttpResponseMessage result;
		try
		{
			result = _http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct).GetAwaiter().GetResult();
		}
		catch (OperationCanceledException)
		{
			throw;
		}
		catch (Exception ex2)
		{
			throw new GitHubException("网络错误:" + ex2.Message, null);
		}
		if (result.StatusCode >= HttpStatusCode.MultipleChoices)
		{
			string body = "";
			try
			{
				body = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
			}
			catch
			{
			}
			string message = ExtractMessage(body) ?? $"HTTP {(int)result.StatusCode} {result.StatusCode}";
			long num = (long)result.StatusCode;
			result.Dispose();
			throw new GitHubException(message, (HttpStatusCode)num);
		}
		long arg = result.Content.Headers.ContentLength ?? (-1);
		using (result)
		{
			using Stream stream = result.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
			using MemoryStream memoryStream = new MemoryStream();
			byte[] array = new byte[65536];
			long num2 = 0L;
			int num3;
			while ((num3 = stream.Read(array, 0, array.Length)) > 0)
			{
				ct.ThrowIfCancellationRequested();
				memoryStream.Write(array, 0, num3);
				num2 += num3;
				progress?.Invoke(num2, arg);
			}
			return memoryStream.ToArray();
		}
	}

	private static string ExtractMessage(string body)
	{
		try
		{
			string text = ((JToken)JObject.Parse(body)).Value<string>((object)"message");
			return string.IsNullOrEmpty(text) ? null : text;
		}
		catch
		{
			return null;
		}
	}
}
[BepInPlugin("SuperVikingDepartment.ValheimSaveShare", "ValheimSaveShare", "1.0.6")]
public class SaveSharePlugin : BaseUnityPlugin
{
	public const string PluginGuid = "SuperVikingDepartment.ValheimSaveShare";

	public const string PluginName = "ValheimSaveShare";

	public const string PluginVersion = "1.0.6";

	internal static ManualLogSource Log;

	internal static SaveSharePlugin Instance;

	internal static ConfigEntry<string> ConfigRepo;

	internal static ConfigEntry<string> ConfigBranch;

	internal static ConfigEntry<string> ConfigPathPrefix;

	internal static ConfigEntry<string> ConfigProxy;

	private readonly ConcurrentQueue<Action> _mainThreadQueue = new ConcurrentQueue<Action>();

	private void Awake()
	{
		//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
		Instance = this;
		Log = ((BaseUnityPlugin)this).Logger;
		ConfigRepo = ((BaseUnityPlugin)this).Config.Bind<string>("GitHub", "Repo", "", "上传用的 GitHub 仓库,格式 owner/repo,例如 MyName/valheim-shared-worlds。\n还没有仓库?先到 github.com/new 新建一个空仓库(Public 公开 = 朋友无需 Token 即可下载;Private 私有 = 下载者也需配置 Token)。\n仓库不需要预先放任何文件,本模组会自动创建 worlds/<世界名>/ 目录结构。\nGitHub repository (owner/repo) used as the upload target for shared saves.");
		ConfigBranch = ((BaseUnityPlugin)this).Config.Bind<string>("GitHub", "Branch", "", "上传目标分支。留空则自动使用仓库默认分支。Target branch; empty = repo default branch.");
		ConfigPathPrefix = ((BaseUnityPlugin)this).Config.Bind<string>("GitHub", "PathPrefix", "worlds", "仓库内存放共享存档的根目录。Repo folder that holds shared saves.");
		ConfigProxy = ((BaseUnityPlugin)this).Config.Bind<string>("GitHub", "Proxy", "", "可选 HTTP 代理,例如 http://127.0.0.1:7890(Clash/SingBox 等本地代理端口)。留空 = 直连。\n直连 api.github.com 困难或上传/下载卡住时,请填写你的本地代理端口。\nOptional HTTP proxy for GitHub access, e.g. http://127.0.0.1:7890. Empty = direct.\nGitHub Token 存放在 %AppData%\\ValheimSaveShare\\token.dat,不在本 cfg。\n(GitHub Token lives in %AppData%\\ValheimSaveShare\\token.dat, not in this cfg.)");
		((BaseUnityPlugin)this).Config.Save();
		TokenStore.EnsureTemplate();
		new Harmony("SuperVikingDepartment.ValheimSaveShare").PatchAll(typeof(FejdStartupSetupGuiPatch));
		SharedSaveRegistry.Load();
		Log.LogInfo((object)"ValheimSaveShare 1.0.6 loaded");
	}

	private void Update()
	{
		Action result;
		while (_mainThreadQueue.TryDequeue(out result))
		{
			try
			{
				result();
			}
			catch (Exception ex)
			{
				Log.LogError((object)("main-thread task failed: " + ex));
			}
		}
	}

	internal static void Post(Action action)
	{
		if ((Object)(object)Instance != (Object)null)
		{
			Instance._mainThreadQueue.Enqueue(action);
		}
	}
}
internal static class SaveShareService
{
	[Serializable]
	[CompilerGenerated]
	private sealed class <>c
	{
		public static readonly <>c <>9 = new <>c();

		public static Func<GitHubItem, bool> <>9__9_5;

		public static Func<GitHubItem, string> <>9__9_6;

		public static Func<string, string> <>9__9_7;

		public static Func<GitHubItem, bool> <>9__9_1;

		public static Func<GitHubItem, ShareFileInfo> <>9__9_2;

		public static Func<ShareFileInfo, long> <>9__9_3;

		public static Action<long, long> <>9__10_5;

		public static Func<(string Rel, byte[] Data, long Size), bool> <>9__11_0;

		public static Func<(string Rel, byte[] Data, long Size), long> <>9__11_1;

		public static Func<(string Rel, byte[] Data, long Size), ShareFileInfo> <>9__12_3;

		public static RetrieveFromStringSource <>9__14_0;

		public static RetrieveFromStringSource <>9__14_1;

		public static RetrieveFromBoolSource <>9__14_2;

		public static PopupButtonCallback <>9__14_3;

		public static Action <>9__16_0;

		internal bool <BeginDownload>b__9_5(GitHubItem i)
		{
			return i.Type == "dir";
		}

		internal string <BeginDownload>b__9_6(GitHubItem i)
		{
			return i.Path;
		}

		internal string <BeginDownload>b__9_7(string d)
		{
			return "· " + ShareUrl.LastSegment(d);
		}

		internal bool <BeginDownload>b__9_1(GitHubItem i)
		{
			if (i.Type == "file")
			{
				return !i.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase);
			}
			return false;
		}

		internal ShareFileInfo <BeginDownload>b__9_2(GitHubItem i)
		{
			return new ShareFileInfo
			{
				Path = i.Name,
				Size = i.Size
			};
		}

		internal long <BeginDownload>b__9_3(ShareFileInfo f)
		{
			return f.Size;
		}

		internal void <ConfirmDownload>b__10_5(long cur, long tot)
		{
			SetStatus(L("下载中  " + FmtMB(cur) + " / " + FmtMB((tot > 0) ? tot : 0) + " MB", "Downloading  " + FmtMB(cur) + " / " + FmtMB((tot > 0) ? tot : 0) + " MB"));
		}

		internal bool <BeginUpload>b__11_0((string Rel, byte[] Data, long Size) f)
		{
			return f.Size > 94371840;
		}

		internal long <BeginUpload>b__11_1((string Rel, byte[] Data, long Size) f)
		{
			return f.Size;
		}

		internal ShareFileInfo <RunUploadTask>b__12_3((string Rel, byte[] Data, long Size) f)
		{
			ShareFileInfo shareFileInfo = new ShareFileInfo();
			(shareFileInfo.Path, _, shareFileInfo.Size) = f;
			return shareFileInfo;
		}

		internal string <OpenTaskPopup>b__14_0()
		{
			return L("Valheim 存档共享", "Valheim Save Share");
		}

		internal string <OpenTaskPopup>b__14_1()
		{
			return _status;
		}

		internal bool <OpenTaskPopup>b__14_2()
		{
			if (!_done)
			{
				return _cancelled;
			}
			return true;
		}

		internal void <OpenTaskPopup>b__14_3()
		{
			_cancelled = true;
			try
			{
				_cts?.Cancel();
			}
			catch
			{
			}
		}

		internal void <OnCancelled>b__16_0()
		{
			CloseTaskPopup();
			Warn(L("已取消", "Cancelled"), L("操作已取消,本地文件未受影响。", "Operation cancelled; local files untouched."));
		}
	}

	[CompilerGenerated]
	private sealed class <>c__DisplayClass9_0
	{
		public ParsedShareUrl parsed;

		public string prefix;

		public string gameVersion;

		internal void <BeginDownload>b__0()
		{
			try
			{
				<>c__DisplayClass9_1 CS$<>8__locals38 = new <>c__DisplayClass9_1
				{
					CS$<>8__locals1 = this,
					branch = parsed.Branch
				};
				if (string.IsNullOrEmpty(CS$<>8__locals38.branch))
				{
					CS$<>8__locals38.branch = SaveSharePlugin.ConfigBranch.Value?.Trim();
				}
				if (string.IsNullOrEmpty(CS$<>8__locals38.branch))
				{
					SetStatus(L("正在连接 GitHub…", "Connecting to GitHub…"));
					CS$<>8__locals38.branch = GH.GetDefaultBranch(parsed.Owner, parsed.Repo, Token());
				}
				CS$<>8__locals38.worldPath = ShareUrl.NormalizeWorldPath(parsed.Path, prefix);
				if (string.IsNullOrEmpty(CS$<>8__locals38.worldPath) || CS$<>8__locals38.worldPath == prefix || CS$<>8__locals38.worldPath == prefix + "/")
				{
					SetStatus(L("正在获取共享存档列表…", "Listing shared saves…"));
					List<string> list = (from i in GH.ListDirectory(parsed.Owner, parsed.Repo, prefix, CS$<>8__locals38.branch, Token())
						where i.Type == "dir"
						select i.Path).ToList();
					if (list.Count != 1)
					{
						if (list.Count == 0)
						{
							throw new UserException(L("该仓库 " + prefix + "/ 下没有共享存档。", "No shared saves under " + prefix + "/ in this repo."));
						}
						string text = string.Join("\n", list.Select((string d) => "· " + ShareUrl.LastSegment(d)));
						throw new UserException(L("该仓库包含多个共享存档,请复制具体世界文件夹的链接:\n" + text, "This repo contains multiple shared saves, paste the link of a specific world folder:\n" + text));
					}
					CS$<>8__locals38.worldPath = list[0];
				}
				SetStatus(L("正在读取存档信息…", "Reading save info…"));
				ShareManifest shareManifest = null;
				byte[] array = GH.TryDownloadFile(parsed.Owner, parsed.Repo, CS$<>8__locals38.worldPath + "/manifest.json", CS$<>8__locals38.branch, null, Token());
				if (array != null)
				{
					try
					{
						shareManifest = JsonConvert.DeserializeObject<ShareManifest>(Encoding.UTF8.GetString(array));
					}
					catch (Exception ex)
					{
						SaveSharePlugin.Log.LogWarning((object)("manifest parse failed: " + ex.Message));
					}
				}
				List<ShareFileInfo> list2 = shareManifest?.Files;
				if (list2 != null && list2.Count > 0)
				{
					CS$<>8__locals38.files = shareManifest.Files;
				}
				else
				{
					CS$<>8__locals38.files = (from i in GH.ListDirectory(parsed.Owner, parsed.Repo, CS$<>8__locals38.worldPath, CS$<>8__locals38.branch, Token())
						where i.Type == "file" && !i.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)
						select new ShareFileInfo
						{
							Path = i.Name,
							Size = i.Size
						}).ToList();
				}
				if (CS$<>8__locals38.files.Count == 0)
				{
					throw new UserException(L("该地址下没有找到存档文件(缺少 .fwl2)。", "No save files (.fwl2) found at this address."));
				}
				CS$<>8__locals38.worldName = SanitizeName(shareManifest?.Name ?? ShareUrl.LastSegment(CS$<>8__locals38.worldPath));
				CS$<>8__locals38.seed = shareManifest?.Seed ?? "?";
				CS$<>8__locals38.saveGameVersion = shareManifest?.GameVersion ?? "";
				CS$<>8__locals38.total = CS$<>8__locals38.files.Sum((ShareFileInfo f) => f.Size);
				CS$<>8__locals38.url = ShareUrl.BuildWorldUrl(parsed.Owner, parsed.Repo, CS$<>8__locals38.branch, CS$<>8__locals38.worldPath);
				SaveSharePlugin.Post(delegate
				{
					ConfirmDownload(CS$<>8__locals38.CS$<>8__locals1.parsed, CS$<>8__locals38.branch, CS$<>8__locals38.worldPath, CS$<>8__locals38.worldName, CS$<>8__locals38.seed, CS$<>8__locals38.saveGameVersion, CS$<>8__locals38.CS$<>8__locals1.gameVersion, CS$<>8__locals38.files, CS$<>8__locals38.total, CS$<>8__locals38.url);
				});
			}
			catch (OperationCanceledException)
			{
				OnCancelled();
			}
			catch (Exception ex3)
			{
				<>c__DisplayClass9_2 CS$<>8__locals39 = new <>c__DisplayClass9_2();
				Exception e = ex3;
				CS$<>8__locals39.e = e;
				SaveSharePlugin.Post(delegate
				{
					CloseTaskPopup();
					ShowError(L("下载失败", "Download failed"), CS$<>8__locals39.e);
				});
			}
		}
	}

	[CompilerGenerated]
	private sealed class <>c__DisplayClass9_1
	{
		public string branch;

		public string worldPath;

		public string worldName;

		public string seed;

		public string saveGameVersion;

		public List<ShareFileInfo> files;

		public long total;

		public string url;

		public <>c__DisplayClass9_0 CS$<>8__locals1;

		internal void <BeginDownload>b__4()
		{
			ConfirmDownload(CS$<>8__locals1.parsed, branch, worldPath, worldName, seed, saveGameVersion, CS$<>8__locals1.gameVersion, files, total, url);
		}
	}

	[CompilerGenerated]
	private sealed class <>c__DisplayClass9_2
	{
		public Exception e;

		internal void <BeginDownload>b__8()
		{
			CloseTaskPopup();
			ShowError(L("下载失败", "Download failed"), e);
		}
	}

	private static GitHubClient _gh;

	private static volatile string _status = "";

	private static volatile bool _done;

	private static volatile bool _cancelled;

	private static bool _taskOpen;

	private static CancellationTokenSource _cts;

	private static readonly string[] ShareExtensions = new string[7] { ".fwl2", ".db2", ".fwl", ".db", ".ok", ".chunks", ".chunk" };

	private static GitHubClient GH => _gh ?? (_gh = new GitHubClient(TokenStore.Load(), SaveSharePlugin.ConfigProxy.Value?.Trim()));

	public static void BeginDownload(string rawUrl)
	{
		if (string.IsNullOrWhiteSpace(rawUrl))
		{
			return;
		}
		if (!ShareUrl.TryParse(rawUrl, out var parsed, out var error))
		{
			Warn(L("链接无效", "Invalid URL"), error);
			return;
		}
		string gameVersion = Version.GetVersionString(false);
		string prefix = (SaveSharePlugin.ConfigPathPrefix.Value ?? "worlds").Trim('/');
		<>c__DisplayClass9_0 CS$<>8__locals8;
		RunBackground(delegate
		{
			try
			{
				if (string.IsNullOrEmpty((string?)(object)CS$<>8__locals8))
				{
					CS$<>8__locals8 = (<>c__DisplayClass9_0)(object)SaveSharePlugin.ConfigBranch.Value?.Trim();
				}
				if (string.IsNullOrEmpty((string?)(object)CS$<>8__locals8))
				{
					SetStatus(L("正在连接 GitHub…", "Connecting to GitHub…"));
					CS$<>8__locals8 = (<>c__DisplayClass9_0)(object)GH.GetDefaultBranch(parsed.Owner, parsed.Repo, Token());
				}
				string worldPath = ShareUrl.NormalizeWorldPath(parsed.Path, prefix);
				if (string.IsNullOrEmpty(worldPath) || worldPath == prefix || worldPath == prefix + "/")
				{
					SetStatus(L("正在获取共享存档列表…", "Listing shared saves…"));
					List<string> list = (from i in GH.ListDirectory(parsed.Owner, parsed.Repo, prefix, (string)(object)CS$<>8__locals8, Token())
						where i.Type == "dir"
						select i.Path).ToList();
					if (list.Count != 1)
					{
						if (list.Count == 0)
						{
							throw new UserException(L("该仓库 " + prefix + "/ 下没有共享存档。", "No shared saves under " + prefix + "/ in this repo."));
						}
						string text = string.Join("\n", list.Select((string d) => "· " + ShareUrl.LastSegment(d)));
						throw new UserException(L("该仓库包含多个共享存档,请复制具体世界文件夹的链接:\n" + text, "This repo contains multiple shared saves, paste the link of a specific world folder:\n" + text));
					}
					worldPath = list[0];
				}
				SetStatus(L("正在读取存档信息…", "Reading save info…"));
				ShareManifest shareManifest = null;
				byte[] array = GH.TryDownloadFile(parsed.Owner, parsed.Repo, worldPath + "/manifest.json", (string)(object)CS$<>8__locals8, null, Token());
				if (array != null)
				{
					try
					{
						shareManifest = JsonConvert.DeserializeObject<ShareManifest>(Encoding.UTF8.GetString(array));
					}
					catch (Exception ex)
					{
						SaveSharePlugin.Log.LogWarning((object)("manifest parse failed: " + ex.Message));
					}
				}
				List<ShareFileInfo> list2 = shareManifest?.Files;
				List<ShareFileInfo> files;
				if (list2 != null && list2.Count > 0)
				{
					files = shareManifest.Files;
				}
				else
				{
					files = (from i in GH.ListDirectory(parsed.Owner, parsed.Repo, worldPath, (string)(object)CS$<>8__locals8, Token())
						where i.Type == "file" && !i.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)
						select new ShareFileInfo
						{
							Path = i.Name,
							Size = i.Size
						}).ToList();
				}
				if (files.Count == 0)
				{
					throw new UserException(L("该地址下没有找到存档文件(缺少 .fwl2)。", "No save files (.fwl2) found at this address."));
				}
				string worldName = SanitizeName(shareManifest?.Name ?? ShareUrl.LastSegment(worldPath));
				string seed = shareManifest?.Seed ?? "?";
				string saveGameVersion = shareManifest?.GameVersion ?? "";
				long total = files.Sum((ShareFileInfo f) => f.Size);
				string url = ShareUrl.BuildWorldUrl(parsed.Owner, parsed.Repo, (string)(object)CS$<>8__locals8, worldPath);
				SaveSharePlugin.Post(delegate
				{
					ConfirmDownload(parsed, (string)(object)CS$<>8__locals8, worldPath, worldName, seed, saveGameVersion, gameVersion, files, total, url);
				});
			}
			catch (OperationCanceledException)
			{
				OnCancelled();
			}
			catch (Exception ex3)
			{
				Exception ex4 = ex3;
				Exception e = ex4;
				SaveSharePlugin.Post(delegate
				{
					CloseTaskPopup();
					ShowError(L("下载失败", "Download failed"), e);
				});
			}
		});
	}

	private static void ConfirmDownload(ParsedShareUrl parsed, string branch, string worldPath, string worldName, string seed, string saveGameVersion, string gameVersion, List<ShareFileInfo> files, long total, string url)
	{
		//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
		//IL_0101: Expected O, but got Unknown
		//IL_0101: Expected O, but got Unknown
		//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
		//IL_0106: Expected O, but got Unknown
		if (!string.IsNullOrEmpty(saveGameVersion) && saveGameVersion != gameVersion)
		{
			UnifiedPopup.Push((PopupBase)new YesNoPopup(L("游戏版本不同", "Game version differs"), L("该存档由游戏 " + saveGameVersion + " 保存,当前游戏为 " + gameVersion + "。\n旧版本世界通常可以正常加载(进游戏时会自动升级),仍要下载吗?", "This save was made with game " + saveGameVersion + ", current game is " + gameVersion + ".\nOld worlds usually load fine (auto-upgraded in game). Download anyway?"), new PopupButtonCallback(ConfirmCore), new PopupButtonCallback(UnifiedPopup.Pop), false, false));
		}
		else
		{
			ConfirmCore();
		}
		void ConfirmCore()
		{
			//IL_0039: 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)
			//IL_003f: 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_0044: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d3: 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_00eb: Expected O, but got Unknown
			//IL_00eb: Expected O, but got Unknown
			//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f0: Expected O, but got Unknown
			bool flag = false;
			SaveWithBackups val = null;
			try
			{
				flag = SaveSystem.TryGetSaveByName(worldName, (SaveDataType)0, ref val) && !val.IsDeleted;
			}
			catch
			{
			}
			if (flag)
			{
				SaveFile primaryFile = val.PrimaryFile;
				FileSource val2 = (FileSource)((primaryFile == null) ? 1 : ((int)primaryFile.m_source));
				if ((val2 & 4) != 0 || (val2 & 2) == 0)
				{
					Warn(L("无法覆盖", "Cannot overwrite"), L("本地已有同名世界「" + worldName + "」,但它存在 Steam 云存档(或老格式存档)中,暂不支持自动备份覆盖。\n请先在游戏内删除或改名该存档,再重新下载。", "A Steam-cloud (or legacy) world named \"" + worldName + "\" already exists.\nCloud saves cannot be auto-backed-up here; delete or rename it in game first, then retry."));
				}
				else
				{
					UnifiedPopup.Push((PopupBase)new YesNoPopup(L("本地已存在同名存档", "A local save with this name exists"), L("本地已有世界「" + worldName + "」。\n继续会把旧存档打包为 zip 备份后覆盖,确定吗?", "World \"" + worldName + "\" already exists locally.\nContinue = zip-backup the old one, then overwrite. Proceed?"), new PopupButtonCallback(StartDownload), new PopupButtonCallback(UnifiedPopup.Pop), false, false));
				}
			}
			else
			{
				StartDownload();
			}
		}
		void RunDownloadTask()
		{
			OpenTaskPopup();
			string worldDirLocal = SaveSystem.GetWorldsSaveRootPath((FileSource)2);
			RunBackground(delegate
			{
				try
				{
					string text = worldDirLocal + "/" + worldName;
					if (Directory.Exists(text))
					{
						SetStatus(L("正在备份旧存档…", "Backing up old save…"));
						BackupWorldDir(text, worldName);
					}
					Directory.CreateDirectory(text);
					byte[] array = GH.TryDownloadFile(parsed.Owner, parsed.Repo, worldPath + "/manifest.json", branch, null, Token());
					bool flag = false;
					if (array != null)
					{
						try
						{
							flag = JsonConvert.DeserializeObject<ShareManifest>(Encoding.UTF8.GetString(array))?.Format == "zip";
						}
						catch
						{
						}
					}
					if (flag)
					{
						SetStatus(L("正在下载 world.zip…", "Downloading world.zip…"));
						byte[] buffer = GH.DownloadFile(parsed.Owner, parsed.Repo, worldPath + "/world.zip", branch, delegate(long cur, long tot)
						{
							SetStatus(L("下载中  " + FmtMB(cur) + " / " + FmtMB((tot > 0) ? tot : 0) + " MB", "Downloading  " + FmtMB(cur) + " / " + FmtMB((tot > 0) ? tot : 0) + " MB"));
						}, Token());
						SetStatus(L("正在解包存档文件…", "Extracting save files…"));
						using MemoryStream stream = new MemoryStream(buffer);
						using ZipArchive zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
						foreach (ZipArchiveEntry entry in zipArchive.Entries)
						{
							if (!string.IsNullOrEmpty(entry.Name))
							{
								string text2 = Path.Combine(text, entry.FullName.Replace('/', Path.DirectorySeparatorChar));
								Directory.CreateDirectory(Path.GetDirectoryName(text2) ?? text);
								entry.ExtractToFile(text2, overwrite: true);
							}
						}
					}
					else
					{
						int num = 0;
						foreach (ShareFileInfo f in files)
						{
							Token().ThrowIfCancellationRequested();
							string rel = (f.Path ?? "").Replace('\\', '/').TrimStart('/');
							int idx = num;
							SetStatus($"({idx + 1}/{files.Count}) {rel}  0 MB / {FmtMB(f.Size)}");
							byte[] bytes = GH.DownloadFile(parsed.Owner, parsed.Repo, worldPath + "/" + rel, branch, delegate(long cur, long tot)
							{
								SetStatus($"({idx + 1}/{files.Count}) {rel}  {FmtMB(cur)} / {FmtMB((tot > 0) ? tot : f.Size)}");
							}, Token());
							string text3 = Path.Combine(text, rel.Replace('/', Path.DirectorySeparatorChar));
							Directory.CreateDirectory(Path.GetDirectoryName(text3) ?? text);
							string text4 = text3 + ".download";
							File.WriteAllBytes(text4, bytes);
							if (File.Exists(text3))
							{
								File.Delete(text3);
							}
							File.Move(text4, text3);
							num++;
						}
					}
					SaveSharePlugin.Post(delegate
					{
						_done = true;
						CloseTaskPopup();
						try
						{
							SharedSaveRegistry.AddOrUpdate(worldName, url, saveGameVersion);
							SharedSaveRegistry.Save();
							RefreshWorldList();
							SaveShareUI.Refresh();
							Warn(L("下载完成", "Download complete"), L("共享存档「" + worldName + "」已加入世界列表,可在「共享存档」页签中选中并开始游戏。", "Shared save \"" + worldName + "\" added to the world list. Select it in the Shared Saves tab and start."));
						}
						catch (Exception e2)
						{
							ShowError(L("下载失败", "Download failed"), e2);
						}
					});
				}
				catch (OperationCanceledException)
				{
					OnCancelled();
				}
				catch (Exception ex2)
				{
					Exception ex3 = ex2;
					Exception e = ex3;
					SaveSharePlugin.Post(delegate
					{
						CloseTaskPopup();
						ShowError(L("下载失败", "Download failed"), e);
					});
				}
			});
		}
		void StartDownload()
		{
			//IL_00a1: 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_00b9: Expected O, but got Unknown
			//IL_00b9: Expected O, but got Unknown
			//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00be: Expected O, but got Unknown
			UnifiedPopup.Push((PopupBase)new YesNoPopup(L("确认下载", "Confirm download"), L($"世界:{worldName}\n种子:{seed}\n文件:{files.Count} 个(约 {FmtMB(total)} MB)\n下载到本地后可在共享列表中开始游戏。", $"World: {worldName}\nSeed: {seed}\nFiles: {files.Count} (~{FmtMB(total)} MB)\nAfter download it appears in the Shared Saves list."), new PopupButtonCallback(RunDownloadTask), new PopupButtonCallback(UnifiedPopup.Pop), false, false));
		}
	}

	public static void BeginUpload()
	{
		//IL_0217: Unknown result type (might be due to invalid IL or missing references)
		//IL_021d: Unknown result type (might be due to invalid IL or missing references)
		//IL_021f: Invalid comparison between Unknown and I4
		//IL_01fd: Unknown result type (might be due to invalid IL or missing references)
		//IL_052f: Unknown result type (might be due to invalid IL or missing references)
		//IL_053b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0547: Expected O, but got Unknown
		//IL_0547: Expected O, but got Unknown
		//IL_0542: Unknown result type (might be due to invalid IL or missing references)
		//IL_054c: Expected O, but got Unknown
		FejdStartup instance = FejdStartup.instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		World value = Traverse.Create((object)instance).Field("m_world").GetValue<World>();
		if (value == null)
		{
			Warn(L("共享存档", "Share save"), L("请先在列表中选择一个世界。", "Select a world in the list first."));
			return;
		}
		string obj = SaveSharePlugin.ConfigRepo.Value?.Trim() ?? "";
		string value2 = TokenStore.LoadUserToken();
		string[] array = obj.Split('/');
		if (string.IsNullOrEmpty(obj) || array.Length != 2 || string.IsNullOrEmpty(array[0]) || string.IsNullOrEmpty(array[1]))
		{
			Warn(L("尚未配置 GitHub 仓库", "GitHub repo not configured"), L("上传需要先在 BepInEx/config/SuperVikingDepartment.ValheimSaveShare.cfg 中配置:\n[GitHub] Repo = 你的用户名/仓库名(没有就先去 github.com 新建一个空仓库)\n并在 %AppData%\\ValheimSaveShare\\token.dat 里填入你的 Personal Access Token(需该仓库 Contents 读写权限)", "Configure in BepInEx/config/SuperVikingDepartment.ValheimSaveShare.cfg:\n[GitHub] Repo = yourname/your-repo (create an empty repo on github.com first)\nand put your Personal Access Token (Contents read/write for that repo) in\n%AppData%\\ValheimSaveShare\\token.dat"));
			return;
		}
		if (string.IsNullOrEmpty(value2))
		{
			Warn(L("尚未配置 GitHub Token", "GitHub token not configured"), L("上传需要 Token。请打开文件:%AppData%\\ValheimSaveShare\\token.dat\n(资源管理器地址栏直接粘贴上面路径即可打开;首次运行游戏时已自动生成带说明的模板)\n按文件内说明生成 Fine-grained token(只勾选你共享仓库的 Contents: Read and write),\n粘贴到说明下方单独一行并保存,然后重试上传(无需重启游戏)。", "A token is required. Open the file: %AppData%\\ValheimSaveShare\\token.dat\n(paste the path into Explorer's address bar; a commented template is created on first launch)\nCreate a fine-grained PAT per the instructions inside (Contents: Read and write on your\nshare repo only), paste it on its own line below the block, save, then retry (no restart needed)."));
			return;
		}
		string owner = array[0];
		string repoName = array[1];
		SaveWithBackups val = default(SaveWithBackups);
		if (!SaveSystem.TryGetSaveByName(value.m_name, (SaveDataType)0, ref val) || val.IsDeleted || val.PrimaryFile == null)
		{
			Warn(L("共享存档", "Share save"), L("无法读取该世界的存档文件(可能已损坏)。", "Cannot read this world's save files (may be corrupt)."));
			return;
		}
		List<(string Rel, byte[] Data, long Size)> fileSet = new List<(string, byte[], long)>();
		string text = "/" + value.m_name + "/";
		string[] allPaths = val.PrimaryFile.AllPaths;
		foreach (string text2 in allPaths)
		{
			string text3 = text2.Replace('\\', '/').TrimStart('/');
			string value3 = Path.GetExtension(text3).ToLowerInvariant();
			if (ShareExtensions.Contains(value3))
			{
				int num = text3.LastIndexOf(text, StringComparison.OrdinalIgnoreCase);
				string text4 = ((num >= 0) ? text3.Substring(num + text.Length) : Path.GetFileName(text3));
				byte[] array2;
				try
				{
					array2 = ReadSaveFileBytes(text2, val.PrimaryFile.m_source);
				}
				catch (Exception ex)
				{
					bool flag = (val.PrimaryFile.m_source & 4) > 0;
					Warn(L("共享存档", "Share save"), L("读取存档文件失败:" + text4 + "\n" + ex.Message + (flag ? "\n(该世界是 Steam 云存档,读取依赖 Steam 云存储,请确认 Steam 正常运行且云同步可用。)" : ""), "Failed to read save file " + text4 + ": " + ex.Message + (flag ? " (Steam cloud save — make sure Steam cloud storage is available.)" : "")));
					return;
				}
				fileSet.Add((text4, array2, array2.LongLength));
			}
		}
		if (fileSet.Count == 0)
		{
			Warn(L("共享存档", "Share save"), L("没有找到可共享的存档文件。", "No shareable save files found."));
			return;
		}
		(string, byte[], long) tuple = fileSet.FirstOrDefault(((string Rel, byte[] Data, long Size) f) => f.Size > 94371840);
		if (tuple.Item1 != null)
		{
			Warn(L("文件过大", "File too large"), L("文件 " + tuple.Item1 + " 超过 90 MB,GitHub Contents API 不支持。\n建议改用 GitHub Release 上传(后续版本支持)。", tuple.Item1 + " exceeds 90 MB (GitHub Contents API limit).\nUse a GitHub Release instead (supported in a later version)."));
			return;
		}
		long bytes = fileSet.Sum(((string Rel, byte[] Data, long Size) f) => f.Size);
		string worldNameLocal = value.m_name;
		string seedLocal = value.m_seedName;
		int worldGenVersion = value.m_worldGenVersion;
		string gameVersion = Version.GetVersionString(false);
		UnifiedPopup.Push((PopupBase)new YesNoPopup(L("共享存档到 GitHub", "Share save to GitHub"), L("把世界「" + worldNameLocal + "」上传到仓库 " + owner + "/" + repoName + "?\n" + $"路径:{SaveSharePlugin.ConfigPathPrefix.Value}/{worldNameLocal}/({fileSet.Count} 个文件,约 {FmtMB(bytes)} MB)\n" + "已存在的旧版本会被覆盖(保留提交历史)。", "Upload world \"" + worldNameLocal + "\" to " + owner + "/" + repoName + "?\n" + $"Path: {SaveSharePlugin.ConfigPathPrefix.Value}/{worldNameLocal}/ ({fileSet.Count} files, ~{FmtMB(bytes)} MB)\n" + "Existing versions are overwritten (git history keeps old commits)."), (PopupButtonCallback)delegate
		{
			RunUploadTask(owner, repoName, worldNameLocal, seedLocal, worldGenVersion, gameVersion, fileSet);
		}, new PopupButtonCallback(UnifiedPopup.Pop), false, false));
	}

	private static void RunUploadTask(string owner, string repoName, string worldName, string seed, int worldGenVersion, string gameVersion, List<(string Rel, byte[] Data, long Size)> files)
	{
		string prefix = (SaveSharePlugin.ConfigPathPrefix.Value ?? "worlds").Trim('/');
		OpenTaskPopup();
		RunBackground(delegate
		{
			try
			{
				string text = SaveSharePlugin.ConfigBranch.Value?.Trim();
				if (string.IsNullOrEmpty(text))
				{
					SetStatus(L("正在连接 GitHub…", "Connecting to GitHub…"));
					text = GH.GetDefaultBranch(owner, repoName, Token());
				}
				SetStatus(L($"正在打包 {files.Count} 个文件…", $"Packing {files.Count} files…"));
				byte[] zipData;
				using (MemoryStream memoryStream = new MemoryStream())
				{
					using (ZipArchive zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Create, leaveOpen: true))
					{
						foreach (var file in files)
						{
							using Stream stream = zipArchive.CreateEntry(file.Rel, CompressionLevel.Optimal).Open();
							stream.Write(file.Data, 0, file.Data.Length);
						}
					}
					zipData = memoryStream.ToArray();
				}
				if (zipData.LongLength > 94371840)
				{
					throw new UserException(L("打包后 " + FmtMB(zipData.Length) + " MB 超过 GitHub Contents API 的 100 MB 限制,无法上传。\n(分块存档的世界可能非常大,后续版本考虑改走 GitHub Release。)", "Packed size " + FmtMB(zipData.Length) + " MB exceeds the 100 MB GitHub Contents API limit.\n(Chunked worlds can be huge; a GitHub Release path may come later.)"));
				}
				SetStatus(L("正在上传(" + FmtMB(zipData.Length) + " MB,上传期间没有逐字节进度,请耐心等待)…", "Uploading (" + FmtMB(zipData.Length) + " MB; no byte-level progress during upload, please wait)…"));
				DateTime startTicks = DateTime.UtcNow;
				using (new Timer(delegate
				{
					SetStatus(L($"正在上传({FmtMB(zipData.Length)} MB,已用 {(DateTime.UtcNow - startTicks).TotalSeconds / 10.0 * 10.0:0} 秒)…", $"Uploading ({FmtMB(zipData.Length)} MB, {(DateTime.UtcNow - startTicks).TotalSeconds / 10.0 * 10.0:0}s elapsed)…"));
				}, null, 10000, 10000))
				{
					GH.UploadFile(owner, repoName, prefix + "/" + worldName + "/world.zip", text, zipData, $"ValheimSaveShare: update world '{worldName}' ({gameVersion}, {files.Count} files)", Token());
				}
				SetStatus(L("正在上传存档信息…", "Uploading manifest…"));
				ShareManifest shareManifest = new ShareManifest
				{
					Name = worldName,
					Seed = seed,
					GameVersion = gameVersion,
					WorldGenVersion = worldGenVersion,
					SavedAt = DateTime.Now.ToString("s"),
					Uploader = TrySteamName(),
					Tool = "ValheimSaveShare 1.0.6",
					Format = "zip",
					Files = files.Select(delegate((string Rel, byte[] Data, long Size) f)
					{
						ShareFileInfo shareFileInfo = new ShareFileInfo();
						(shareFileInfo.Path, _, shareFileInfo.Size) = f;
						return shareFileInfo;
					}).ToList()
				};
				byte[] bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject((object)shareManifest, (Formatting)1));
				GH.UploadFile(owner, repoName, prefix + "/" + worldName + "/manifest.json", text, bytes, "ValheimSaveShare: update manifest for '" + worldName + "'", Token());
				string url = ShareUrl.BuildWorldUrl(owner, repoName, text, prefix + "/" + worldName);
				SaveSharePlugin.Post(delegate
				{
					_done = true;
					CloseTaskPopup();
					try
					{
						SharedSaveRegistry.AddOrUpdate(worldName, url, gameVersion);
						SharedSaveRegistry.Save();
						SaveShareUI.Refresh();
						GUIUtility.systemCopyBuffer = url;
						Warn(L("共享成功", "Shared"), L("世界「" + worldName + "」已上传。\n分享链接已自动复制到剪贴板,点「确认」可再次复制:\n" + url, "World \"" + worldName + "\" uploaded.\nShare link copied to clipboard; press OK to copy again:\n" + url), delegate
						{
							GUIUtility.systemCopyBuffer = url;
						});
					}
					catch (Exception e2)
					{
						ShowError(L("共享失败", "Share failed"), e2);
					}
				});
			}
			catch (OperationCanceledException)
			{
				OnCancelled();
			}
			catch (Exception ex2)
			{
				Exception ex3 = ex2;
				Exception e = ex3;
				SaveSharePlugin.Post(delegate
				{
					CloseTaskPopup();
					ShowError(L("共享失败", "Share failed"), e);
				});
			}
		});
	}

	public static void RemoveRegistryEntry(string worldName)
	{
		SharedSaveRegistry.Remove(worldName);
		SharedSaveRegistry.Save();
		SaveShareUI.Refresh();
	}

	private static void OpenTaskPopup()
	{
		//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_004b: Expected O, but got Unknown
		//IL_005f: 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_006a: Expected O, but got Unknown
		//IL_007e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0083: Unknown result type (might be due to invalid IL or missing references)
		//IL_0089: Expected O, but got Unknown
		//IL_00a8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b2: Expected O, but got Unknown
		//IL_009d: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a8: Expected O, but got Unknown
		_cts = new CancellationTokenSource();
		_done = false;
		_cancelled = false;
		_taskOpen = true;
		_status = "";
		object obj = <>c.<>9__14_0;
		if (obj == null)
		{
			RetrieveFromStringSource val = () => L("Valheim 存档共享", "Valheim Save Share");
			<>c.<>9__14_0 = val;
			obj = (object)val;
		}
		object obj2 = <>c.<>9__14_1;
		if (obj2 == null)
		{
			RetrieveFromStringSource val2 = () => _status;
			<>c.<>9__14_1 = val2;
			obj2 = (object)val2;
		}
		object obj3 = <>c.<>9__14_2;
		if (obj3 == null)
		{
			RetrieveFromBoolSource val3 = () => _done || _cancelled;
			<>c.<>9__14_2 = val3;
			obj3 = (object)val3;
		}
		object obj4 = <>c.<>9__14_3;
		if (obj4 == null)
		{
			PopupButtonCallback val4 = delegate
			{
				_cancelled = true;
				try
				{
					_cts?.Cancel();
				}
				catch
				{
				}
			};
			<>c.<>9__14_3 = val4;
			obj4 = (object)val4;
		}
		UnifiedPopup.Push((PopupBase)new CancelableTaskPopup((RetrieveFromStringSource)obj, (RetrieveFromStringSource)obj2, (RetrieveFromBoolSource)obj3, (PopupButtonCallback)obj4));
	}

	private static void CloseTaskPopup()
	{
		if (_taskOpen)
		{
			_taskOpen = false;
			if (UnifiedPopup.IsVisible())
			{
				UnifiedPopup.Pop();
			}
		}
	}

	private static void OnCancelled()
	{
		_cancelled = true;
		SaveSharePlugin.Post(delegate
		{
			CloseTaskPopup();
			Warn(L("已取消", "Cancelled"), L("操作已取消,本地文件未受影响。", "Operation cancelled; local files untouched."));
		});
	}

	private static CancellationToken Token()
	{
		return _cts?.Token ?? CancellationToken.None;
	}

	private static void RunBackground(Action action)
	{
		Task.Run(action);
	}

	private static void SetStatus(string text)
	{
		_status = text;
	}

	private static void RefreshWorldList()
	{
		try
		{
			SaveSystem.InvalidateCache((SaveDataType)0);
			FejdStartup instance = FejdStartup.instance;
			AccessTools.Method(typeof(FejdStartup), "UpdateWorldList", (Type[])null, (Type[])null)?.Invoke(instance, new object[1] { true });
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("refresh world list failed: " + ex));
		}
	}

	private static byte[] ReadSaveFileBytes(string path, FileSource source)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0003: Unknown result type (might be due to invalid IL or missing references)
		//IL_0009: Expected O, but got Unknown
		FileReader val = new FileReader(path, source, (FileHelperType)0);
		try
		{
			Stream stream = ((val.m_binary != null) ? val.m_binary.BaseStream : val.m_stream.BaseStream);
			using MemoryStream memoryStream = new MemoryStream();
			stream.CopyTo(memoryStream);
			return memoryStream.ToArray();
		}
		finally
		{
			val.Dispose();
		}
	}

	private static void BackupWorldDir(string worldDir, string worldName)
	{
		string fullName = Directory.GetParent(worldDir).FullName;
		string text = DateTime.Now.ToString("yyyyMMdd-HHmmss");
		using (FileStream stream = new FileStream(Path.Combine(fullName, worldName + "_backup_" + text + ".zip"), FileMode.CreateNew))
		{
			using ZipArchive destination = new ZipArchive(stream, ZipArchiveMode.Create);
			string[] files = Directory.GetFiles(worldDir, "*", SearchOption.AllDirectories);
			foreach (string text2 in files)
			{
				string entryName = text2.Substring(worldDir.Length).TrimStart('\\', '/').Replace('\\', '/');
				destination.CreateEntryFromFile(text2, entryName, CompressionLevel.Optimal);
			}
		}
		Directory.Delete(worldDir, recursive: true);
	}

	private static string SanitizeName(string name)
	{
		if (string.IsNullOrEmpty(name))
		{
			return "SharedWorld";
		}
		char[] invalidFileNameChars = Path.GetInvalidFileNameChars();
		foreach (char oldChar in invalidFileNameChars)
		{
			name = name.Replace(oldChar, '_');
		}
		return name.Trim();
	}

	private static string TrySteamName()
	{
		try
		{
			return SteamFriends.GetPersonaName();
		}
		catch
		{
			return "";
		}
	}

	private static string FmtMB(long bytes)
	{
		return ((float)bytes / 1048576f).ToString("0.##");
	}

	internal static string L(string zh, string en)
	{
		try
		{
			Localization instance = Localization.instance;
			switch ((instance != null) ? instance.GetSelectedLanguage() : null)
			{
			case "Chinese":
			case "ChineseSimplified":
			case "zhCN":
				return zh;
			}
		}
		catch
		{
		}
		return en;
	}

	private static void Warn(string title, string text, Action onOk = null)
	{
		//IL_0016: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Expected O, but got Unknown
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0026: Expected O, but got Unknown
		UnifiedPopup.Push((PopupBase)new WarningPopup(title, text, (PopupButtonCallback)delegate
		{
			onOk?.Invoke();
			UnifiedPopup.Pop();
		}, false));
	}

	internal static void ShowError(string title, Exception e)
	{
		string text = ((e is UserException || e is GitHubException) ? e.Message : e.ToString());
		if (e is GitHubException ex && ex.Status == HttpStatusCode.Unauthorized)
		{
			text = text + "\n\n" + L("提示:GitHub 拒绝了 Token(无效或已过期)。请到 GitHub → Settings → Developer settings →\nFine-grained tokens 检查/重新生成,并按模组的 Token 配置说明重新填写。", "Hint: GitHub rejected the token (invalid or expired). Regenerate it under Settings →\nDeveloper settings → Fine-grained tokens and re-enter it per the mod's token instructions.");
		}
		else if (e is GitHubException ex2 && ex2.Status == HttpStatusCode.Forbidden && text != null && text.Contains("not accessible"))
		{
			text = text + "\n\n" + L("提示:这是 Token 权限问题。请到 GitHub → Settings → Developer settings → 你的 fine-grained token,\n确认 Repository access 勾选了共享仓库、且 Permissions → Contents = Read and write\n(经典 token 则需要勾选 repo 整个 scope),生成新 token 粘贴到\n%AppData%\\ValheimSaveShare\\token.dat(覆盖原 token 行)后再试。", "Hint: token permission issue. Edit your fine-grained token: select the share repo under\nRepository access and set Permissions → Contents = Read and write (classic tokens need the\nrepo scope), then generate a new token and paste it into\n%AppData%\\ValheimSaveShare\\token.dat (replacing the old token line).");
		}
		SaveSharePlugin.Log.LogError((object)(title + ": " + e));
		Warn(title, text);
	}
}
[HarmonyPatch(typeof(FejdStartup), "SetupGui")]
internal static class FejdStartupSetupGuiPatch
{
	[HarmonyPostfix]
	private static void Postfix()
	{
		try
		{
			SaveShareUI.Build();
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogError((object)("SaveShare UI build failed: " + ex));
		}
	}
}
internal static class SaveShareUI
{
	private class RowEntry
	{
		public SharedSaveEntry Registry;

		public World Local;

		public string Name => Registry?.Name;
	}

	[Serializable]
	[CompilerGenerated]
	private sealed class <>c
	{
		public static readonly <>c <>9 = new <>c();

		public static ValidateTextEntry <>9__17_0;

		public static RetrieveTextEntryResult <>9__17_1;

		public static UnityAction <>9__23_0;

		internal bool <OnAddClicked>b__17_0(string _)
		{
			return true;
		}

		internal void <OnAddClicked>b__17_1(string url)
		{
			UnifiedPopup.Pop();
			SaveShareService.BeginDownload(url);
		}

		internal void <CreateUploadButton>b__23_0()
		{
			try
			{
				SaveShareService.BeginUpload();
			}
			catch (Exception e)
			{
				SaveShareService.ShowError(SaveShareService.L("共享失败", "Share failed"), e);
			}
		}
	}

	private static FejdStartup _builtFor;

	private static TabHandler _tabs;

	private static int _ourTabIndex = -1;

	private static RectTransform _listRoot;

	private static GameObject _rowTemplate;

	private static float _rowStep = 28f;

	private static float _baseHeight = 220f;

	private static readonly List<GameObject> Rows = new List<GameObject>();

	private static readonly List<RowEntry> Entries = new List<RowEntry>();

	private static int _selected = -1;

	public static void Build()
	{
		//IL_016f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0179: Expected O, but got Unknown
		//IL_01f9: 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_0205: Unknown result type (might be due to invalid IL or missing references)
		//IL_0219: Unknown result type (might be due to invalid IL or missing references)
		//IL_021e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0319: Unknown result type (might be due to invalid IL or missing references)
		//IL_0323: Expected O, but got Unknown
		//IL_0489: Unknown result type (might be due to invalid IL or missing references)
		//IL_048e: Unknown result type (might be due to invalid IL or missing references)
		//IL_033f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0349: Expected O, but got Unknown
		//IL_0517: Unknown result type (might be due to invalid IL or missing references)
		//IL_051c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0524: Unknown result type (might be due to invalid IL or missing references)
		//IL_0536: Unknown result type (might be due to invalid IL or missing references)
		//IL_0542: Expected O, but got Unknown
		//IL_03c5: Unknown result type (might be due to invalid IL or missing references)
		//IL_03cf: Expected O, but got Unknown
		//IL_0375: Unknown result type (might be due to invalid IL or missing references)
		//IL_037a: Unknown result type (might be due to invalid IL or missing references)
		//IL_037d: Expected O, but got Unknown
		//IL_0382: Expected O, but got Unknown
		//IL_0408: Unknown result type (might be due to invalid IL or missing references)
		//IL_0412: Expected O, but got Unknown
		FejdStartup fejd = FejdStartup.instance;
		if ((Object)(object)fejd == (Object)null || (Object)(object)fejd.m_startGamePanel == (Object)null || (Object)(object)fejd.m_worldListRoot == (Object)null || (Object)(object)fejd.m_worldListElement == (Object)null || (Object)(object)_builtFor == (Object)(object)fejd)
		{
			return;
		}
		TabHandler component = ((Component)fejd.m_startGamePanel.transform.GetChild(0)).GetComponent<TabHandler>();
		if ((Object)(object)component == (Object)null || component.m_tabs == null || component.m_tabs.Count == 0 || (Object)(object)component.m_tabs[0].m_page == (Object)null || (Object)(object)component.m_tabs[0].m_button == (Object)null)
		{
			SaveSharePlugin.Log.LogWarning((object)"SaveShare: start panel TabHandler not found, UI skipped");
			return;
		}
		_builtFor = fejd;
		ResetCachedState();
		_rowStep = Mathf.Max(8f, fejd.m_worldListElementStep);
		_rowTemplate = fejd.m_worldListElement;
		Tab val = component.m_tabs[0];
		GameObject val2 = Object.Instantiate<GameObject>(((Component)val.m_button).gameObject, ((Component)val.m_button).transform.parent);
		((Object)val2).name = "SaveShareTabButton";
		Button component2 = val2.GetComponent<Button>();
		if ((Object)(object)component2 != (Object)null)
		{
			component2.onClick = new ButtonClickedEvent();
		}
		SetLabel(val2, SaveShareService.L("共享存档", "Shared Saves"));
		Transform transform = val2.transform;
		RectTransform val3 = (RectTransform)(object)((transform is RectTransform) ? transform : null);
		Rect rect;
		if ((Object)(object)((Component)((Component)val.m_button).transform.parent).GetComponent<HorizontalOrVerticalLayoutGroup>() == (Object)null && (Object)(object)val3 != (Object)null)
		{
			Transform transform2 = ((Component)component.m_tabs[component.m_tabs.Count - 1].m_button).transform;
			RectTransform val4 = (RectTransform)(object)((transform2 is RectTransform) ? transform2 : null);
			if ((Object)(object)val4 != (Object)null)
			{
				Vector2 anchoredPosition = val4.anchoredPosition;
				rect = val4.rect;
				val3.anchoredPosition = anchoredPosition + new Vector2(((Rect)(ref rect)).width + 4f, 0f);
			}
		}
		GameObject val5 = Object.Instantiate<GameObject>(((Component)val.m_page).gameObject, ((Transform)val.m_page).parent);
		((Object)val5).name = "SaveSharePage";
		val5.SetActive(false);
		if ((Object)(object)fejd.m_worldSourceInfoPanel != (Object)null)
		{
			Transform val6 = FindChild(val5.transform, ((Object)fejd.m_worldSourceInfoPanel).name);
			if ((Object)(object)val6 != (Object)null)
			{
				((Component)val6).gameObject.SetActive(false);
			}
		}
		if ((Object)(object)fejd.m_removeWorldDialog != (Object)null)
		{
			Transform val7 = FindChild(val5.transform, ((Object)fejd.m_removeWorldDialog).name);
			if ((Object)(object)val7 != (Object)null)
			{
				((Component)val7).gameObject.SetActive(false);
			}
		}
		Button[] componentsInChildren = val5.GetComponentsInChildren<Button>(true);
		UnityAction val9 = default(UnityAction);
		foreach (Button val8 in componentsInChildren)
		{
			if (IsRowTemplate(val8))
			{
				continue;
			}
			string text = PersistentMethods(val8);
			val8.onClick = new ButtonClickedEvent();
			if (text.Contains("OnWorldStart"))
			{
				((UnityEvent)val8.onClick).AddListener(new UnityAction(OnStartClicked));
			}
			else if (text.Contains("OnStartGameBack"))
			{
				ButtonClickedEvent onClick = val8.onClick;
				UnityAction obj = val9;
				if (obj == null)
				{
					UnityAction val10 = delegate
					{
						try
						{
							fejd.OnStartGameBack();
						}
						catch (Exception ex2)
						{
							SaveSharePlugin.Log.LogError((object)("back failed: " + ex2));
						}
					};
					UnityAction val11 = val10;
					val9 = val10;
					obj = val11;
				}
				((UnityEvent)onClick).AddListener(obj);
			}
			else if (text.Contains("OnWorldNew"))
			{
				SetLabel(((Component)val8).gameObject, SaveShareService.L("添加共享", "Add Shared"));
				((UnityEvent)val8.onClick).AddListener(new UnityAction(OnAddClicked));
			}
			else if (text.Contains("OnWorldRemove"))
			{
				SetLabel(((Component)val8).gameObject, SaveShareService.L("移除共享", "Remove Shared"));
				((UnityEvent)val8.onClick).AddListener(new UnityAction(OnRemoveClicked));
			}
			else
			{
				((Component)val8).gameObject.SetActive(false);
			}
		}
		Transform obj2 = FindChild(val5.transform, ((Object)fejd.m_worldListRoot).name);
		RectTransform val12 = (RectTransform)(object)((obj2 is RectTransform) ? obj2 : null);
		if ((Object)(object)val12 == (Object)null)
		{
			SaveSharePlugin.Log.LogError((object)"SaveShare: world list root not found in cloned page, UI skipped");
			Object.Destroy((Object)(object)val5);
			Object.Destroy((Object)(object)val2);
			_builtFor = null;
			return;
		}
		rect = val12.rect;
		_baseHeight = Mathf.Max(120f, ((Rect)(ref rect)).height);
		for (int num = ((Transform)val12).childCount - 1; num >= 0; num--)
		{
			Transform child = ((Transform)val12).GetChild(num);
			if ((Object)(object)_rowTemplate != (Object)null && ((Object)child).name == ((Object)_rowTemplate).name)
			{
				((Component)child).gameObject.SetActive(false);
			}
			else
			{
				Object.Destroy((Object)(object)((Component)child).gameObject);
			}
		}
		_tabs = component;
		_listRoot = val12;
		component.m_tabs.Add(new Tab
		{
			m_button = component2,
			m_page = (RectTransform)/*isinst with value type is only supported in some contexts*/,
			m_default = false
		});
		_ourTabIndex = component.m_tabs.Count - 1;
		try
		{
			component.ActiveTabChanged += OnActiveTabChanged;
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("ActiveTabChanged subscribe failed: " + ex.Message));
		}
		component.Init(false);
		CreateUploadButton(fejd, val);
		RebuildList();
		SaveSharePlugin.Log.LogInfo((object)$"SaveShare UI built (tab index {_ourTabIndex})");
	}

	public static void Refresh()
	{
		if ((Object)(object)_builtFor != (Object)null)
		{
			RebuildList();
		}
	}

	private static void ResetCachedState()
	{
		_tabs = null;
		_ourTabIndex = -1;
		_listRoot = null;
		_rowTemplate = null;
		Rows.Clear();
		Entries.Clear();
		_selected = -1;
	}

	private static void OnActiveTabChanged(int index)
	{
		if (index == _ourTabIndex)
		{
			RebuildList();
		}
	}

	private static void OnRowClicked(int index)
	{
		_selected = index;
		UpdateHighlights();
	}

	private static void OnStartClicked()
	{
		RowEntry rowEntry = SelectedEntry();
		if (rowEntry == null)
		{
			Warn(SaveShareService.L("请先在列表中选择一个共享存档。", "Select a shared save in the list first."));
			return;
		}
		if (rowEntry.Local == null)
		{
			Warn(SaveShareService.L("该共享存档还没有下载到本地(列表中显示“未下载”)。\n请通过「添加共享」输入 GitHub 链接下载。", "This shared save is not downloaded yet (shows \"Not downloaded\").\nUse Add Shared and paste its GitHub URL to download."));
			return;
		}
		FejdStartup instance = FejdStartup.instance;
		if (!((Object)(object)instance == (Object)null))
		{
			AccessTools.Field(typeof(FejdStartup), "m_world").SetValue(instance, rowEntry.Local);
			instance.OnWorldStart();
		}
	}

	private static void OnAddClicked()
	{
		//IL_002a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0048: Unknown result type (might be due to invalid IL or missing references)
		//IL_004e: Expected O, but got Unknown
		//IL_0073: Expected O, but got Unknown
		//IL_006e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0078: Expected O, but got Unknown
		//IL_0062: 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_006d: Expected O, but got Unknown
		string text = SaveShareService.L("添加共享存档", "Add shared save");
		string text2 = SaveShareService.L("输入 GitHub 分享链接(仓库名下存档的主页或世界文件夹链接均可):", "Paste a GitHub share link (repo home or a specific world folder):");
		PopupButtonCallback val = UnifiedPopup.Pop;
		object obj = <>c.<>9__17_0;
		if (obj == null)
		{
			ValidateTextEntry val2 = (string _) => true;
			<>c.<>9__17_0 = val2;
			obj = (object)val2;
		}
		object obj2 = <>c.<>9__17_1;
		if (obj2 == null)
		{
			RetrieveTextEntryResult val3 = delegate(string url)
			{
				UnifiedPopup.Pop();
				SaveShareService.BeginDownload(url);
			};
			<>c.<>9__17_1 = val3;
			obj2 = (object)val3;
		}
		UnifiedPopup.Push((PopupBase)new TextEntryPopup(text, text2, "https://github.com/user/repo/tree/main/worlds/MyWorld", val, (ValidateTextEntry)obj, (RetrieveTextEntryResult)obj2, false));
		UnifiedPopup.SetFocus();
	}

	private static void OnRemoveClicked()
	{
		//IL_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_0081: Unknown result type (might be due to invalid IL or missing references)
		//IL_008d: Expected O, but got Unknown
		//IL_008d: Expected O, but got Unknown
		//IL_0088: Unknown result type (might be due to invalid IL or missing references)
		//IL_0092: Expected O, but got Unknown
		RowEntry rowEntry = SelectedEntry();
		if (rowEntry == null)
		{
			Warn(SaveShareService.L("请先在列表中选择一个共享存档。", "Select a shared save in the list first."));
			return;
		}
		string name = rowEntry.Name;
		UnifiedPopup.Push((PopupBase)new YesNoPopup(SaveShareService.L("移除共享记录", "Remove shared entry"), SaveShareService.L("只从「共享存档」列表移除「" + name + "」的记录,不会删除本地世界文件。确定吗?", "Remove \"" + name + "\" from the Shared Saves list only? Local world files are kept."), (PopupButtonCallback)delegate
		{
			UnifiedPopup.Pop();
			SaveShareService.RemoveRegistryEntry(name);
			_selected = -1;
			RebuildList();
		}, new PopupButtonCallback(UnifiedPopup.Pop), false, false));
	}

	private static RowEntry SelectedEntry()
	{
		if (_selected < 0 || _selected >= Entries.Count)
		{
			return null;
		}
		return Entries[_selected];
	}

	private static void RebuildList()
	{
		//IL_017c: Unknown result type (might be due to invalid IL or missing references)
		//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
		//IL_0205: Expected O, but got Unknown
		//IL_0214: Unknown result type (might be due to invalid IL or missing references)
		//IL_021e: Expected O, but got Unknown
		//IL_028c: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)_listRoot == (Object)null || (Object)(object)_rowTemplate == (Object)null)
		{
			return;
		}
		_selected = -1;
		Entries.Clear();
		try
		{
			List<World> worldList = SaveSystem.GetWorldList();
			foreach (SharedSaveEntry reg in SharedSaveRegistry.All)
			{
				RowEntry rowEntry = new RowEntry
				{
					Registry = reg
				};
				rowEntry.Local = ((IEnumerable<World>)worldList).FirstOrDefault((Func<World, bool>)((World w) => string.Equals(w.m_name, reg.Name, StringComparison.OrdinalIgnoreCase)));
				Entries.Add(rowEntry);
			}
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("world list query failed: " + ex.Message));
		}
		foreach (GameObject row in Rows)
		{
			if ((Object)(object)row != (Object)null)
			{
				Object.Destroy((Object)(object)row);
			}
		}
		Rows.Clear();
		if (Entries.Count == 0)
		{
			GameObject val = Object.Instantiate<GameObject>(_rowTemplate, (Transform)(object)_listRoot);
			((Object)val).name = "SaveShareHintRow";
			SetRowTexts(val, SaveShareService.L("(还没有共享存档,点「添加共享」粘贴 GitHub 链接)", "(No shared saves yet — click Add Shared and paste a GitHub URL)"), "", keepSourcesHidden: false);
			Button component = val.GetComponent<Button>();
			if ((Object)(object)component != (Object)null)
			{
				((Selectable)component).interactable = false;
			}
			val.SetActive(true);
			Transform transform = val.transform;
			((RectTransform)((transform is RectTransform) ? transform : null)).anchoredPosition = Vector2.zero;
			Rows.Add(val);
		}
		else
		{
			for (int num = 0; num < Entries.Count; num++)
			{
				RowEntry rowEntry2 = Entries[num];
				GameObject val2 = Object.Instantiate<GameObject>(_rowTemplate, (Transform)(object)_listRoot);
				((Object)val2).name = "SaveShareRow" + num;
				int idx = num;
				Button component2 = val2.GetComponent<Button>();
				if ((Object)(object)component2 != (Object)null)
				{
					component2.onClick = new ButtonClickedEvent();
					((UnityEvent)component2.onClick).AddListener((UnityAction)delegate
					{
						OnRowClicked(idx);
					});
				}
				string text = ((rowEntry2.Local != null) ? rowEntry2.Local.m_seedName : SaveShareService.L("未下载", "Not downloaded"));
				SetRowTexts(val2, rowEntry2.Name ?? "?", text ?? "", keepSourcesHidden: true);
				val2.SetActive(true);
				Transform transform2 = val2.transform;
				((RectTransform)((transform2 is RectTransform) ? transform2 : null)).anchoredPosition = new Vector2(0f, (float)(-num) * _rowStep);
				Rows.Add(val2);
			}
		}
		_listRoot.SetSizeWithCurrentAnchors((Axis)1, Mathf.Max(_baseHeight, (float)Rows.Count * _rowStep));
		UpdateHighlights();
	}

	private static void UpdateHighlights()
	{
		for (int i = 0; i < Rows.Count; i++)
		{
			Transform val = (((Object)(object)Rows[i] != (Object)null) ? Rows[i].transform.Find("selected") : null);
			if ((Object)(object)val != (Object)null)
			{
				((Component)val).gameObject.SetActive(i == _selected);
			}
		}
	}

	private static void SetRowTexts(GameObject row, string name, string seed, bool keepSourcesHidden)
	{
		Transform obj = row.transform.Find("name");
		TMP_Text val = ((obj != null) ? ((Component)obj).GetComponent<TMP_Text>() : null);
		if ((Object)(object)val != (Object)null)
		{
			val.text = name ?? "";
		}
		Transform obj2 = row.transform.Find("seed");
		TMP_Text val2 = ((obj2 != null) ? ((Component)obj2).GetComponent<TMP_Text>() : null);
		if ((Object)(object)val2 != (Object)null)
		{
			val2.text = seed ?? "";
		}
		Transform obj3 = row.transform.Find("modifiers");
		TMP_Text val3 = ((obj3 != null) ? ((Component)obj3).GetComponent<TMP_Text>() : null);
		if ((Object)(object)val3 != (Object)null)
		{
			val3.text = "";
		}
		string[] array = new string[3] { "source_cloud", "source_local", "source_legacy" };
		foreach (string text in array)
		{
			Transform val4 = row.transform.Find(text);
			if ((Object)(object)val4 != (Object)null)
			{
				((Component)val4).gameObject.SetActive(false);
			}
		}
	}

	private static void CreateUploadButton(FejdStartup fejd, Tab tab0)
	{
		//IL_006b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0075: Expected O, but got Unknown
		//IL_008f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0094: Unknown result type (might be due to invalid IL or missing references)
		//IL_009a: Expected O, but got Unknown
		//IL_00f7: Unknown result type (might be due to invalid IL or missing references)
		//IL_0102: Unknown result type (might be due to invalid IL or missing references)
		//IL_0107: Unknown result type (might be due to invalid IL or missing references)
		//IL_0117: Unknown result type (might be due to invalid IL or missing references)
		//IL_011c: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			Button val = FindButtonByMethod(fejd.m_startGamePanel.transform, "OnStartGameBack", tab0.m_page);
			if ((Object)(object)val == (Object)null)
			{
				SaveSharePlugin.Log.LogWarning((object)"SaveShare: back button (OnStartGameBack) not found, upload button skipped");
				return;
			}
			GameObject obj = Object.Instantiate<GameObject>(((Component)val).gameObject, ((Component)val).transform.parent);
			((Object)obj).name = "SaveShareUploadButton";
			Button component = obj.GetComponent<Button>();
			if ((Object)(object)component != (Object)null)
			{
				component.onClick = new ButtonClickedEvent();
				ButtonClickedEvent onClick = component.onClick;
				object obj2 = <>c.<>9__23_0;
				if (obj2 == null)
				{
					UnityAction val2 = delegate
					{
						try
						{
							SaveShareService.BeginUpload();
						}
						catch (Exception e)
						{
							SaveShareService.ShowError(SaveShareService.L("共享失败", "Share failed"), e);
						}
					};
					<>c.<>9__23_0 = val2;
					obj2 = (object)val2;
				}
				((UnityEvent)onClick).AddListener((UnityAction)obj2);
			}
			SetLabel(obj, SaveShareService.L("共享存档", "Share Save"));
			Transform transform = ((Component)val).transform;
			RectTransform val3 = (RectTransform)(object)((transform is RectTransform) ? transform : null);
			Transform transform2 = obj.transform;
			RectTransform val4 = (RectTransform)(object)((transform2 is RectTransform) ? transform2 : null);
			if ((Object)(object)val3 != (Object)null && (Object)(object)val4 != (Object)null && (Object)(object)((Component)((Component)val).transform.parent).GetComponent<HorizontalOrVerticalLayoutGroup>() == (Object)null)
			{
				Vector2 anchoredPosition = val3.anchoredPosition;
				Rect rect = val3.rect;
				val4.anchoredPosition = anchoredPosition + new Vector2(0f, 0f - (((Rect)(ref rect)).height + 6f));
			}
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("upload button creation failed: " + ex));
		}
	}

	private static Button FindButtonByMethod(Transform root, string methodName, RectTransform preferUnder)
	{
		Button val = null;
		Button[] componentsInChildren = ((Component)root).GetComponentsInChildren<Button>(true);
		foreach (Button val2 in componentsInChildren)
		{
			if (PersistentMethods(val2).Contains(methodName))
			{
				if ((Object)(object)preferUnder != (Object)null && ((Component)val2).transform.IsChildOf((Transform)(object)preferUnder))
				{
					return val2;
				}
				if (val == null)
				{
					val = val2;
				}
			}
		}
		return val;
	}

	private static string PersistentMethods(Button b)
	{
		StringBuilder stringBuilder = new StringBuilder();
		int persistentEventCount = ((UnityEventBase)b.onClick).GetPersistentEventCount();
		for (int i = 0; i < persistentEventCount; i++)
		{
			stringBuilder.Append(((UnityEventBase)b.onClick).GetPersistentMethodName(i)).Append(';');
		}
		return stringBuilder.ToString();
	}

	private static bool IsRowTemplate(Button b)
	{
		if ((Object)(object)_rowTemplate != (Object)null)
		{
			if (!((Object)(object)((Component)b).gameObject == (Object)(object)_rowTemplate))
			{
				return ((Component)b).transform.IsChildOf(_rowTemplate.transform);
			}
			return true;
		}
		return false;
	}

	private static void SetLabel(GameObject go, string text)
	{
		if (!((Object)(object)go == (Object)null))
		{
			TMP_Text[] componentsInChildren = go.GetComponentsInChildren<TMP_Text>(true);
			for (int i = 0; i < componentsInChildren.Length; i++)
			{
				componentsInChildren[i].text = text;
			}
		}
	}

	private static Transform FindChild(Transform root, string name)
	{
		if ((Object)(object)root == (Object)null || string.IsNullOrEmpty(name) || ((Object)root).name == name)
		{
			return root;
		}
		for (int i = 0; i < root.childCount; i++)
		{
			Transform val = FindChild(root.GetChild(i), name);
			if ((Object)(object)val != (Object)null)
			{
				return val;
			}
		}
		return null;
	}

	private static void Warn(string text)
	{
		//IL_000d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0018: Expected O, but got Unknown
		//IL_0013: Unknown result type (might be due to invalid IL or missing references)
		//IL_001d: Expected O, but got Unknown
		UnifiedPopup.Push((PopupBase)new WarningPopup("Valheim Save Share", text, new PopupButtonCallback(UnifiedPopup.Pop), false));
	}
}
internal class SharedSaveEntry
{
	public string Name { get; set; }

	public string Url { get; set; }

	public string GameVersion { get; set; }

	public string AddedAt { get; set; }

	public string LastSyncAt { get; set; }
}
internal static class SharedSaveRegistry
{
	private class RegistryFile
	{
		public List<SharedSaveEntry> Worlds { get; set; } = new List<SharedSaveEntry>();
	}

	private static readonly List<SharedSaveEntry> Entries = new List<SharedSaveEntry>();

	public static IReadOnlyList<SharedSaveEntry> All => Entries;

	private static string FilePath => Path.Combine(Paths.ConfigPath, "ValheimSaveShare.shared.json");

	public static void Load()
	{
		Entries.Clear();
		try
		{
			if (File.Exists(FilePath))
			{
				RegistryFile registryFile = JsonConvert.DeserializeObject<RegistryFile>(File.ReadAllText(FilePath));
				if (registryFile?.Worlds != null)
				{
					Entries.AddRange(registryFile.Worlds);
				}
			}
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("failed to load shared save registry: " + ex.Message));
		}
	}

	public static void Save()
	{
		try
		{
			Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
			File.WriteAllText(FilePath, JsonConvert.SerializeObject((object)new RegistryFile
			{
				Worlds = Entries
			}, (Formatting)1));
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("failed to save shared save registry: " + ex.Message));
		}
	}

	public static SharedSaveEntry Find(string worldName)
	{
		return Entries.Find((SharedSaveEntry e) => string.Equals(e.Name, worldName, StringComparison.OrdinalIgnoreCase));
	}

	public static void AddOrUpdate(string worldName, string url, string gameVersion)
	{
		SharedSaveEntry sharedSaveEntry = Find(worldName);
		if (sharedSaveEntry == null)
		{
			sharedSaveEntry = new SharedSaveEntry
			{
				Name = worldName,
				AddedAt = DateTime.Now.ToString("s")
			};
			Entries.Add(sharedSaveEntry);
		}
		sharedSaveEntry.Url = url;
		sharedSaveEntry.GameVersion = gameVersion;
		sharedSaveEntry.LastSyncAt = DateTime.Now.ToString("s");
	}

	public static bool Remove(string worldName)
	{
		return Entries.RemoveAll((SharedSaveEntry e) => string.Equals(e.Name, worldName, StringComparison.OrdinalIgnoreCase)) > 0;
	}
}
internal class ShareFileInfo
{
	[JsonProperty("path")]
	public string Path { get; set; }

	[JsonProperty("size")]
	public long Size { get; set; }
}
internal class ShareManifest
{
	[JsonProperty("name")]
	public string Name { get; set; }

	[JsonProperty("seed")]
	public string Seed { get; set; }

	[JsonProperty("gameVersion")]
	public string GameVersion { get; set; }

	[JsonProperty("worldGenVersion")]
	public int WorldGenVersion { get; set; }

	[JsonProperty("savedAt")]
	public string SavedAt { get; set; }

	[JsonProperty("uploader")]
	public string Uploader { get; set; }

	[JsonProperty("tool")]
	public string Tool { get; set; }

	[JsonProperty("format")]
	public string Format { get; set; }

	[JsonProperty("files")]
	public List<ShareFileInfo> Files { get; set; } = new List<ShareFileInfo>();
}
internal class ParsedShareUrl
{
	public string Owner;

	public string Repo;

	public string Branch;

	public string Path;

	public string RepoSlug => Owner + "/" + Repo;
}
internal static class ShareUrl
{
	public static bool TryParse(string input, out ParsedShareUrl parsed, out string error)
	{
		parsed = null;
		error = null;
		input = input?.Trim();
		if (string.IsNullOrEmpty(input))
		{
			error = "链接为空";
			return false;
		}
		if (!input.Contains("://"))
		{
			input = "https://" + input;
		}
		Uri uri;
		try
		{
			uri = new Uri(input);
		}
		catch
		{
			error = "无法解析的 URL:" + input;
			return false;
		}
		string text = uri.Host.ToLowerInvariant();
		string[] array = uri.AbsolutePath.Trim('/').Split('/').Select(Uri.UnescapeDataString)
			.ToArray();
		string queryValue = GetQueryValue(uri.Query, "ref");
		switch (text)
		{
		case "github.com":
		case "www.github.com":
			if (array.Length < 2 || string.IsNullOrEmpty(array[0]) || string.IsNullOrEmpty(array[1]))
			{
				error = "链接里缺少 owner/repo:" + input;
				return false;
			}
			parsed = new ParsedShareUrl
			{
				Owner = array[0],
				Repo = (array[1].EndsWith(".git") ? array[1].Substring(0, array[1].Length - 4) : array[1])
			};
			if (array.Length >= 4 && (array[2] == "tree" || array[2] == "blob"))
			{
				parsed.Branch = array[3];
				parsed.Path = string.Join("/", array.Skip(4));
			}
			return true;
		case "raw.githubusercontent.com":
			if (array.Length < 3)
			{
				error = "raw 链接缺少 owner/repo/branch:" + input;
				return false;
			}
			parsed = new ParsedShareUrl
			{
				Owner = array[0],
				Repo = array[1],
				Branch = array[2],
				Path = string.Join("/", array.Skip(3))
			};
			return true;
		case "api.github.com":
		{
			if (array.Length < 2)
			{
				error = "API 链接缺少 owner/repo:" + input;
				return false;
			}
			parsed = new ParsedShareUrl
			{
				Owner = array[0],
				Repo = array[1],
				Branch = queryValue
			};
			int num = Array.IndexOf(array, "contents");
			if (num >= 0)
			{
				parsed.Path = string.Join("/", array.Skip(num + 1));
			}
			return true;
		}
		default:
			error = "仅支持 github.com 链接,收到:" + text;
			return false;
		}
	}

	public static string NormalizeWorldPath(string path, string prefix)
	{
		path = (path ?? "").Trim('/').Replace('\\', '/');
		if (path.EndsWith("/manifest.json", StringComparison.OrdinalIgnoreCase))
		{
			path = path.Substring(0, path.Length - "/manifest.json".Length);
		}
		if (path.EndsWith("/manifest.json/", StringComparison.OrdinalIgnoreCase))
		{
			path = path.Substring(0, path.Length - "/manifest.json/".Length);
		}
		return path;
	}

	public static string LastSegment(string path)
	{
		path = (path ?? "").Trim('/');
		int num = path.LastIndexOf('/');
		if (num < 0)
		{
			return path;
		}
		return path.Substring(num + 1);
	}

	public static string BuildWorldUrl(string owner, string repo, string branch, string worldPath)
	{
		return "https://github.com/" + owner + "/" + repo + "/tree/" + branch + "/" + EscapePath(worldPath);
	}

	public static string EscapePath(string path)
	{
		return string.Join("/", (from s in (path ?? "").Split('/')
			where s.Length > 0
			select s).Select(Uri.EscapeDataString));
	}

	private static string GetQueryValue(string query, string key)
	{
		if (string.IsNullOrEmpty(query))
		{
			return null;
		}
		string[] array = query.TrimStart('?').Split('&');
		foreach (string text in array)
		{
			int num = text.IndexOf('=');
			if (num > 0 && text.Substring(0, num) == key)
			{
				return Uri.UnescapeDataString(text.Substring(num + 1));
			}
		}
		return null;
	}
}
internal static class TokenStore
{
	public static string FilePath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ValheimSaveShare", "token.dat");

	public static string Load()
	{
		try
		{
			return File.Exists(FilePath) ? File.ReadAllText(FilePath, Encoding.UTF8).Trim() : "";
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("failed to read token file: " + ex.Message));
			return "";
		}
	}

	public static void Save(string token)
	{
		token = (token ?? "").Trim();
		Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
		string text = FilePath + ".tmp";
		File.WriteAllText(text, token, Encoding.UTF8);
		if (File.Exists(FilePath))
		{
			File.Delete(FilePath);
		}
		File.Move(text, FilePath);
	}

	public static void EnsureTemplate()
	{
		if (File.Exists(FilePath))
		{
			return;
		}
		try
		{
			Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
			File.WriteAllText(FilePath, "把 GitHub Personal Access Token 粘贴到本行下面单独一行(上传共享存档必需;下载公开仓库不需要)。\n生成步骤:github.com → 右上角头像 → Settings → 左侧最底部 Developer settings →\nPersonal access tokens → Fine-grained tokens → Generate new token:\n  Repository access 选 Only select repositories 并勾选你 cfg 里 Repo 填的仓库;\n  Permissions → Repository permissions → Contents 设为 Read and write(其余保持 No access);\n  Generate 后复制 github_pat_ 开头的完整字符串,粘贴到本说明下方单独一行,保存文件即可(无需重启)。\n换 token:直接覆盖粘贴;作废 token:到 GitHub token 管理页 Revoke。\n本文件在 %AppData% 下,请勿分享给他人。\nPaste your GitHub Personal Access Token on its own line below this block.\n", Encoding.UTF8);
		}
		catch (Exception ex)
		{
			SaveSharePlugin.Log.LogWarning((object)("failed to create token template: " + ex.Message));
		}
	}

	public static string LoadUserToken()
	{
		string text = Load();
		if (text.Length == 0 || text.Contains('\n'))
		{
			try
			{
				string[] array = File.ReadAllLines(FilePath);
				for (int i = 0; i < array.Length; i++)
				{
					string text2 = array[i].Trim();
					if (text2.StartsWith("github_pat_", StringComparison.OrdinalIgnoreCase) || text2.StartsWith("ghp_", StringComparison.OrdinalIgnoreCase))
					{
						return text2;
					}
				}
			}
			catch
			{
			}
			return "";
		}
		return text;
	}
}