Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimSaveShare v1.0.6
plugins/ValheimSaveShare.dll
Decompiled a month agousing 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; } }