初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class AnnouncementService
|
||||
{
|
||||
public static void Load()
|
||||
{
|
||||
if (States.System.AnnounceSolution > 1)
|
||||
return;
|
||||
|
||||
var showedAnnounced = States.Hint.ShowedAnnouncements
|
||||
.Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
|
||||
var showAnnounce = UpdateManager.remoteServer.GetAnnouncementList().Content
|
||||
.Where(x => !showedAnnounced.Contains(x.Id))
|
||||
.ToList();
|
||||
|
||||
ModBase.Log("[System] 需要展示的公告数量:" + showAnnounce.Count);
|
||||
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
foreach (var item in showAnnounce)
|
||||
{
|
||||
ModMain.MyMsgBox(item.Detail, item.Title,
|
||||
item.Btn1 is null ? "" : item.Btn1.Text,
|
||||
item.Btn2 is null ? "" : item.Btn2.Text,
|
||||
Lang.Text("Common.Action.Close"),
|
||||
button1Action: () =>
|
||||
{
|
||||
if (EventTypeMapper.TryParse(
|
||||
item.Btn1.Command, out var eventType))
|
||||
CustomEvent.Raise(eventType, item.Btn1.CommandParameter);
|
||||
},
|
||||
button2Action: () =>
|
||||
{
|
||||
if (EventTypeMapper.TryParse(
|
||||
item.Btn2.Command, out var eventType))
|
||||
CustomEvent.Raise(eventType, item.Btn2.CommandParameter);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
showedAnnounced.AddRange(showAnnounce.Select(x => x.Id));
|
||||
showedAnnounced = showedAnnounced.Distinct().ToList();
|
||||
States.Hint.ShowedAnnouncements = showedAnnounced.Join("|");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace PCL;
|
||||
|
||||
public enum UpdateChannel
|
||||
{
|
||||
stable,
|
||||
beta
|
||||
}
|
||||
|
||||
public enum UpdateArch
|
||||
{
|
||||
x64,
|
||||
arm64
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public interface IUpdateSource
|
||||
{
|
||||
string SourceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可用,根据本地情况判断
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool IsAvailable();
|
||||
|
||||
/// <summary>
|
||||
/// 确保最新版本
|
||||
/// </summary>
|
||||
/// <returns>True 表示更新成功,False 表示没有数据更新</returns>
|
||||
bool RefreshCache();
|
||||
|
||||
VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch);
|
||||
bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode);
|
||||
VersionAnnouncementDataModel GetAnnouncementList();
|
||||
List<ModLoader.LoaderBase> GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace PCL;
|
||||
|
||||
public static class UpdateEnums
|
||||
{
|
||||
public enum VersionStatus
|
||||
{
|
||||
Latest,
|
||||
NotLatest,
|
||||
Unknown
|
||||
}
|
||||
|
||||
public enum UpdateType
|
||||
{
|
||||
Silent = 0,
|
||||
PromptOnly = 1,
|
||||
DownloadAndPrompt = 2,
|
||||
UpdateNow = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.OS;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public static class UpdateManager
|
||||
{
|
||||
public static bool isUpdateWaitingRestart;
|
||||
|
||||
public static UpdatesWrapperModel remoteServer = new(new List<IUpdateSource>
|
||||
{
|
||||
new UpdatesMirrorChyanModel(),
|
||||
new UpdatesRandomModel(new[]
|
||||
{
|
||||
new UpdatesMinioModel("https://s3.pysio.online/pcl2-ce/", "Pysio"),
|
||||
new UpdatesMinioModel("https://staticassets.naids.com/resources/pclce/", "Naids")
|
||||
}),
|
||||
new UpdatesMinioModel("https://github.com/PCL-Community/PCL2_CE_Server/raw/main/", "GitHub")
|
||||
});
|
||||
|
||||
public static bool IsCurrentVersionBeta
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ModBase.versionBaseName.Contains("beta"))
|
||||
return true;
|
||||
return (int)Config.Update.UpdateChannel == 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static UpdateEnums.VersionStatus GetVersionStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsCurrentVersionBeta && (int)Config.Update.UpdateChannel != 1)
|
||||
{
|
||||
var isNewerThanStable = remoteServer.IsLatest(UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.versionBaseName),
|
||||
ModBase.versionCode);
|
||||
var isBetaLatest = remoteServer.IsLatest(UpdateChannel.beta,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.versionBaseName),
|
||||
ModBase.versionCode);
|
||||
return isNewerThanStable && isBetaLatest
|
||||
? UpdateEnums.VersionStatus.Latest
|
||||
: UpdateEnums.VersionStatus.NotLatest;
|
||||
}
|
||||
|
||||
return remoteServer.IsLatest(
|
||||
IsCurrentVersionBeta ? UpdateChannel.beta : UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.versionBaseName),
|
||||
ModBase.versionCode)
|
||||
? UpdateEnums.VersionStatus.Latest
|
||||
: UpdateEnums.VersionStatus.NotLatest;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(
|
||||
ex,
|
||||
Lang.Text("Update.Check.Failed"),
|
||||
ModBase.LogLevel.Hint,
|
||||
userSummary: Lang.Text("Update.Check.Failed"));
|
||||
return UpdateEnums.VersionStatus.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public static ModLoader.LoaderCombo<JsonObject> updateLoader;
|
||||
|
||||
public static void UpdateStart(UpdateEnums.UpdateType type, string receivedKey = null, bool forceValidated = false)
|
||||
{
|
||||
var dlTargetPath = ModBase.exePath + @"PCL\Plain Craft Launcher Community Edition.exe";
|
||||
ModBase.RunInNewThread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = remoteServer.GetLatestVersion(
|
||||
IsCurrentVersionBeta ? UpdateChannel.beta : UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64
|
||||
);
|
||||
|
||||
ModBase.WriteFile($"{ModBase.pathTemp}CEUpdateLog.md", version.Changelog);
|
||||
ModBase.Log($"[Update] 远程最新版本: {version.VersionName}, 当前版本: {ModBase.versionBaseName}");
|
||||
if (!(SemVer.Parse(version.VersionName) > SemVer.Parse(ModBase.versionBaseName)))
|
||||
return;
|
||||
if (type == UpdateEnums.UpdateType.PromptOnly)
|
||||
{
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
if (ModMain.MyMsgBox(
|
||||
Lang.Text("Update.Available", ModBase.versionBaseName, version.VersionName),
|
||||
Lang.Text("Update.Title"),
|
||||
Lang.Text("Update.Action"),
|
||||
Lang.Text("Common.Action.Cancel")
|
||||
) == 1)
|
||||
ModMain.frmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupUpdate);
|
||||
});
|
||||
return;
|
||||
// 构造步骤加载器
|
||||
}
|
||||
|
||||
var loaders = new List<ModLoader.LoaderBase>();
|
||||
// 下载
|
||||
loaders.AddRange(remoteServer.GetDownloadLoader(
|
||||
IsCurrentVersionBeta ? UpdateChannel.beta : UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, dlTargetPath));
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Update.Task.Check"), _ =>
|
||||
{
|
||||
var curHash = ModBase.GetFileSHA256(dlTargetPath);
|
||||
if ((curHash ?? "") != (version.Sha256 ?? ""))
|
||||
throw new Exception(Lang.Text("Update.Error.Sha256Mismatch", version.Sha256, curHash));
|
||||
}));
|
||||
if (type == UpdateEnums.UpdateType.UpdateNow)
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Update.Task.Install"), _ => UpdateRestart(true)));
|
||||
else if (type == UpdateEnums.UpdateType.Silent)
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Update.Task.Prepare"), _ => isUpdateWaitingRestart = true));
|
||||
else if (type == UpdateEnums.UpdateType.DownloadAndPrompt)
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Update.Task.ShowButton"), _ =>
|
||||
{
|
||||
isUpdateWaitingRestart = true;
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
ModMain.frmMain.BtnExtraUpdateRestart.ToolTip =
|
||||
Lang.Text("Main.Extra.UpdateRestart.ToolTipWithVersion", ModBase.versionBaseName, version.VersionName);
|
||||
ModMain.frmMain.BtnExtraUpdateRestart.ShowRefresh();
|
||||
ModMain.frmMain.BtnExtraUpdateRestart.Ribble();
|
||||
});
|
||||
})
|
||||
{
|
||||
show = false
|
||||
});
|
||||
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Update.Task.RefreshSettings"), _ =>
|
||||
{
|
||||
if (ModMain.frmSetupUpdate is not null)
|
||||
ModBase.RunInUi(() =>
|
||||
{
|
||||
ModMain.frmSetupUpdate.BtnUpdate.Text = Lang.Text("Update.Task.RestartInstall");
|
||||
ModMain.frmSetupUpdate.BtnUpdate.IsEnabled = true;
|
||||
});
|
||||
})
|
||||
{
|
||||
show = false
|
||||
});
|
||||
// 启动
|
||||
updateLoader = new ModLoader.LoaderCombo<JsonObject>(Lang.Text("Update.Title"), loaders);
|
||||
updateLoader.Start();
|
||||
if (type == UpdateEnums.UpdateType.UpdateNow)
|
||||
{
|
||||
ModLoader.LoaderTaskbarAdd(updateLoader);
|
||||
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
||||
ModMain.frmMain.BtnExtraDownload.Ribble();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "[Update] 获取启动器更新失败");
|
||||
if (type != UpdateEnums.UpdateType.Silent)
|
||||
HintService.Hint(Lang.Text("Update.Error.FetchFailed"), HintType.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void UpdateRestart(bool triggerRestartAndByEnd, bool triggerRestart = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileName = ModBase.exePath + @"PCL\Plain Craft Launcher Community Edition.exe";
|
||||
if (!File.Exists(fileName))
|
||||
{
|
||||
ModBase.Log("[System] 更新失败:未找到更新文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// id old new restart
|
||||
var text =
|
||||
$"update {Process.GetCurrentProcess().Id} \"{Basics.ExecutablePath}\" \"{fileName}\" {(triggerRestart ? "true" : "false")}";
|
||||
ModBase.Log("[System] 更新程序启动,参数:" + text);
|
||||
Process.Start(new ProcessStartInfo(fileName)
|
||||
{ WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, Arguments = text });
|
||||
if (triggerRestartAndByEnd)
|
||||
{
|
||||
ModMain.frmMain.EndProgram(false, true);
|
||||
ModBase.Log("[System] 已由于更新强制结束程序");
|
||||
}
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, "自动更新时触发 Win32 错误,疑似被拦截");
|
||||
ModMain.MyMsgBox(
|
||||
Lang.Text("Update.Error.UpdateBlockedMessage", ModBase.exePath),
|
||||
Lang.Text("Update.Error.UpdateBlocked"),
|
||||
Lang.Text("Common.Action.Confirm"),
|
||||
"",
|
||||
"",
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保 PathTemp 下的 Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
|
||||
/// 如果不是,则下载一个。
|
||||
/// </summary>
|
||||
internal static void DownloadLatestPCL(ModLoader.LoaderBase loaderToSyncProgress = null)
|
||||
{
|
||||
// 注意:如果要自行实现这个功能,请换用另一个文件路径,以免与官方版本冲突
|
||||
var latestPCLPath = Path.Combine(ModBase.pathTemp, "CE-Latest.exe");
|
||||
var target = remoteServer.GetLatestVersion(UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64);
|
||||
if (target is null)
|
||||
throw new Exception(Lang.Text("Update.Error.UnableToGetUpdate"));
|
||||
if (File.Exists(latestPCLPath) && (ModBase.GetFileSHA256(latestPCLPath) ?? "") == (target.Sha256 ?? ""))
|
||||
{
|
||||
ModBase.Log("[System] 最新版 PCL 已存在,跳过下载");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((ModBase.GetFileSHA256(Basics.ExecutablePath) ?? "") == (target.Sha256 ?? "")) // 正在使用的版本符合要求,直接拿来用
|
||||
{
|
||||
ModBase.CopyFile(Basics.ExecutablePath, latestPCLPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var loaders = remoteServer.GetDownloadLoader(UpdateChannel.stable,
|
||||
SystemInfo.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, latestPCLPath);
|
||||
var loader = new ModLoader.LoaderCombo<int>(Lang.Text("Update.Task.DownloadLatestStable"), loaders);
|
||||
loader.Start();
|
||||
loader.WaitForExit();
|
||||
}
|
||||
|
||||
public static ModLoader.LoaderTask<int, int> serverLoader =
|
||||
new(Lang.Text("Update.Service.PclCe"),
|
||||
_ => LoadOnlineInfo(),
|
||||
priority: ThreadPriority.BelowNormal);
|
||||
|
||||
private static void LoadOnlineInfo()
|
||||
{
|
||||
ScheduleBasedOnConfig();
|
||||
AnnouncementService.Load();
|
||||
}
|
||||
|
||||
private static void ScheduleBasedOnConfig()
|
||||
{
|
||||
switch (Config.Update.UpdateMode)
|
||||
{
|
||||
case LauncherAutoUpdateBehavior.DownloadAndInstall:
|
||||
ModBase.Log("[Update] 更新设置: 自动下载并安装更新");
|
||||
if (GetVersionStatus() != UpdateEnums.VersionStatus.Latest)
|
||||
UpdateStart(UpdateEnums.UpdateType.Silent);
|
||||
break;
|
||||
case LauncherAutoUpdateBehavior.DownloadAndAnnounce:
|
||||
ModBase.Log("[Update] 更新设置: 自动下载并提示更新");
|
||||
UpdateStart(UpdateEnums.UpdateType.DownloadAndPrompt);
|
||||
break;
|
||||
case LauncherAutoUpdateBehavior.AnnounceOnly:
|
||||
ModBase.Log("[Update] 更新设置: 提示更新");
|
||||
UpdateStart(UpdateEnums.UpdateType.PromptOnly);
|
||||
break;
|
||||
default:
|
||||
ModBase.Log("[Update] 更新设置: 不自动检查更新");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 展示社区版提示
|
||||
/// </summary>
|
||||
/// <param name="IsUpdate">是否为更新时启动</param>
|
||||
public static void ShowCEAnnounce()
|
||||
{
|
||||
ModMain.MyMsgBox(Lang.Text("Update.CommunityNotice.Body"),
|
||||
Lang.Text("Update.CommunityNotice.Title"),
|
||||
Lang.Text("Update.CommunityNotice.Confirm"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Core.Utils.Diff;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class UpdatesMinioModel : IUpdateSource // 社区自己的更新系统格式
|
||||
{
|
||||
private readonly string _baseUrl;
|
||||
|
||||
private Dictionary<string, string> _remoteCache;
|
||||
|
||||
public UpdatesMinioModel(string baseUrl, string name = "Minio")
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
SourceName = name;
|
||||
}
|
||||
|
||||
public string SourceName { get; set; }
|
||||
|
||||
public bool IsAvailable()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(_baseUrl);
|
||||
}
|
||||
|
||||
public bool RefreshCache()
|
||||
{
|
||||
// 先检查缓存
|
||||
var remoteCache =
|
||||
ModBase.GetJson(Requester.FetchString($"{_baseUrl}apiv2/cache.json", RequestParam.WithRetry));
|
||||
_remoteCache = remoteCache.ToObject<Dictionary<string, string>>();
|
||||
return true;
|
||||
}
|
||||
|
||||
public VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
if (_remoteCache is null)
|
||||
RefreshCache();
|
||||
// 确定版本通道名称
|
||||
return GetChannelInfo(channel, arch);
|
||||
}
|
||||
|
||||
public bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode)
|
||||
{
|
||||
if (_remoteCache is null)
|
||||
RefreshCache();
|
||||
var latestVersion = GetChannelInfo(channel, arch);
|
||||
return currentVersion >= SemVer.Parse(latestVersion.VersionName);
|
||||
}
|
||||
|
||||
public VersionAnnouncementDataModel GetAnnouncementList()
|
||||
{
|
||||
if (_remoteCache is null)
|
||||
RefreshCache();
|
||||
var deJsonData = GetRemoteInfoByName("announcement")?.ToObject<VersionAnnouncementDataModel>();
|
||||
if (deJsonData is null)
|
||||
throw new NullReferenceException("Can not get remote announcement info!");
|
||||
return deJsonData;
|
||||
}
|
||||
|
||||
public List<ModLoader.LoaderBase> GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output)
|
||||
{
|
||||
if (_remoteCache is null)
|
||||
RefreshCache();
|
||||
var loaders = new List<ModLoader.LoaderBase>();
|
||||
var patchUpdate = true;
|
||||
var tempPath = $@"{ModBase.pathTemp}Cache\Update\Download\";
|
||||
loaders.Add(new ModLoader.LoaderTask<int, List<DownloadFile>>(Lang.Text("Update.Task.GetVersionInfo"), load =>
|
||||
{
|
||||
var channelName = GetChannelName(channel, arch);
|
||||
var deJsonData = GetRemoteInfoByName($"updates-{channelName}", "updates/")
|
||||
?.ToObject<MinioUpdateModel>()
|
||||
?.Assets
|
||||
?.FirstOrDefault();
|
||||
if (deJsonData is null)
|
||||
throw new Exception("No assets can download!");
|
||||
var selfSha256 = ModBase.GetFileSHA256(Basics.ExecutablePath);
|
||||
var remoteUpdSha256 = deJsonData.Sha256;
|
||||
var patchFileName = $"{selfSha256}_{remoteUpdSha256}.patch";
|
||||
if (deJsonData.Patches.Contains(patchFileName))
|
||||
{
|
||||
patchUpdate = true;
|
||||
tempPath += patchFileName;
|
||||
load.output = new List<DownloadFile>
|
||||
{ new(new[] { $"{_baseUrl}static/patch/{patchFileName}" }, tempPath) };
|
||||
}
|
||||
else
|
||||
{
|
||||
patchUpdate = false;
|
||||
|
||||
tempPath += $"{deJsonData.Sha256}.bin";
|
||||
load.output = new List<DownloadFile> { new(RandomUtils.Shuffle(deJsonData.Downloads), tempPath) };
|
||||
}
|
||||
}));
|
||||
loaders.Add(new LoaderDownload(Lang.Text("Update.Task.DownloadFile"), new List<DownloadFile>()));
|
||||
loaders.Add(new ModLoader.LoaderTask<string, int>(Lang.Text("Update.Task.ApplyFile"), _ =>
|
||||
{
|
||||
if (patchUpdate)
|
||||
{
|
||||
var diff = new BsDiff();
|
||||
var newFile = diff
|
||||
.ApplyAsync(ModBase.ReadFileBytes(Basics.ExecutablePath), ModBase.ReadFileBytes(tempPath))
|
||||
.GetAwaiter().GetResult();
|
||||
ModBase.WriteFile(output, newFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var fs = new FileStream(tempPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
using (var zip = new ZipArchive(fs))
|
||||
{
|
||||
// 尝试找到目标条目
|
||||
var entry = zip.Entries
|
||||
.FirstOrDefault(x => x.Name.Contains("Plain Craft Launcher Community Edition.exe")) ?? zip
|
||||
.Entries
|
||||
.FirstOrDefault(x => x.Name.Contains("Plain Craft Launcher"));
|
||||
|
||||
entry ??= zip.Entries
|
||||
.FirstOrDefault(x => x.Name.Contains("Launcher"));
|
||||
|
||||
entry ??= zip.Entries
|
||||
.FirstOrDefault(x => x.Name.Contains(".exe"));
|
||||
|
||||
if (entry is null)
|
||||
throw new Exception(Lang.Text("Update.Error.FileNotFound"));
|
||||
|
||||
// 解压到指定文件(覆盖已存在文件)
|
||||
entry.ExtractToFile(output, true);
|
||||
}
|
||||
}
|
||||
}));
|
||||
return loaders;
|
||||
}
|
||||
|
||||
private VersionDataModel GetChannelInfo(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
var channelName = GetChannelName(channel, arch);
|
||||
var deJsonData = GetRemoteInfoByName($"updates-{channelName}", "updates/")?.ToObject<MinioUpdateModel>().Assets
|
||||
.FirstOrDefault();
|
||||
if (deJsonData is null)
|
||||
throw new NullReferenceException("Can not get remote update info!");
|
||||
return new VersionDataModel
|
||||
{
|
||||
VersionName = deJsonData.Version.Name,
|
||||
VersionCode = deJsonData.Version.Code,
|
||||
Sha256 = deJsonData.Sha256,
|
||||
Source = SourceName,
|
||||
Changelog = deJsonData.Changelog
|
||||
};
|
||||
}
|
||||
|
||||
private JsonNode GetRemoteInfoByName(string name, string path = "")
|
||||
{
|
||||
var localInfoFile = Path.Combine(ModBase.pathTemp, "Cache", "Update", $"{name}.json");
|
||||
JsonNode jsonData;
|
||||
if (IsCacheValid($"{name}.json", _remoteCache[name]))
|
||||
{
|
||||
jsonData = ModBase.GetJson(ModBase.ReadFile(localInfoFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
var response = HttpRequest.Create($"{_baseUrl}apiv2/{path}{name}.json")
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
var content = response.AsString();
|
||||
jsonData = ModBase.GetJson(content);
|
||||
ModBase.WriteFile(localInfoFile, content);
|
||||
}
|
||||
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存是否有效
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="hash"></param>
|
||||
/// <returns></returns>
|
||||
private bool IsCacheValid(string path, string hash)
|
||||
{
|
||||
var cacheFile = Path.Combine(ModBase.pathTemp, "Cache", "Update", path);
|
||||
var fileInfo = new FileInfo(cacheFile);
|
||||
return fileInfo.Exists && (DateTime.Now - fileInfo.LastWriteTime).TotalHours < 1 &&
|
||||
(ModBase.GetFileMD5(cacheFile) ?? "") == (hash ?? "");
|
||||
}
|
||||
|
||||
private string GetChannelName(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
var channelName = string.Empty;
|
||||
switch (channel)
|
||||
{
|
||||
case UpdateChannel.stable:
|
||||
{
|
||||
channelName += "sr";
|
||||
break;
|
||||
}
|
||||
case UpdateChannel.beta:
|
||||
{
|
||||
channelName += "fr";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
channelName += "sr";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (arch)
|
||||
{
|
||||
case UpdateArch.x64:
|
||||
{
|
||||
channelName += "x64";
|
||||
break;
|
||||
}
|
||||
case UpdateArch.arm64:
|
||||
{
|
||||
channelName += "arm64";
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
channelName += "x64";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return channelName;
|
||||
}
|
||||
|
||||
private class MinioUpdateModel
|
||||
{
|
||||
[JsonPropertyName("assets")]
|
||||
public List<MinioUpdateAsset> Assets { get; set; }
|
||||
}
|
||||
|
||||
private class MinioUpdateAsset
|
||||
{
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public MinioUpdateAssetVersionInfo Version { get; set; }
|
||||
|
||||
[JsonPropertyName("upd_time")]
|
||||
public string UpdTime { get; set; }
|
||||
|
||||
[JsonPropertyName("downloads")]
|
||||
public List<string> Downloads { get; set; }
|
||||
|
||||
[JsonPropertyName("patches")]
|
||||
public List<string> Patches { get; set; }
|
||||
|
||||
[JsonPropertyName("sha256")]
|
||||
public string Sha256 { get; set; }
|
||||
|
||||
[JsonPropertyName("changelog")]
|
||||
public string Changelog { get; set; }
|
||||
}
|
||||
|
||||
private class MinioUpdateAssetVersionInfo
|
||||
{
|
||||
[JsonPropertyName("channel")]
|
||||
public string Channel { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Net.Http;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
using PCL.Network;
|
||||
using PCL.Network.Loaders;
|
||||
using PCL.Core.IO.Net.Http;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class UpdatesMirrorChyanModel : IUpdateSource // Mirror 酱的更新格式
|
||||
{
|
||||
private const string mirrorChyanBaseUrl =
|
||||
"https://mirrorchyan.com/api/resources/{cid}/latest?cdk={cdk}&os=win&arch={arch}&channel={channel}";
|
||||
|
||||
private const string myCid = "PCL2-CE";
|
||||
public string SourceName { get; set; } = "MirrorChyan";
|
||||
|
||||
public bool IsAvailable()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(Config.Update.MirrorChyanKey);
|
||||
}
|
||||
|
||||
public VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
using (var response = HttpRequest.Create(GetUrl(channel, arch))
|
||||
.SendAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult())
|
||||
{
|
||||
var ret = (JsonObject)ModBase.GetJson(response.AsString());
|
||||
if ((int)ret["code"] != 0)
|
||||
throw new Exception("Mirror 酱获取数据不成功");
|
||||
var data = ret["data"];
|
||||
var upd_url = data["url"]?.ToString();
|
||||
if (data is not null && string.IsNullOrWhiteSpace(upd_url))
|
||||
throw new Exception("无效 CDK");
|
||||
return new VersionDataModel
|
||||
{
|
||||
Source = SourceName,
|
||||
VersionCode = (int)data["version_number"],
|
||||
VersionName = (string)data["version_name"],
|
||||
Sha256 = (string)data["sha256"],
|
||||
Changelog = (string)data["release_note"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public bool RefreshCache()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode)
|
||||
{
|
||||
var latest = GetLatestVersion(channel, arch);
|
||||
return currentVersion >= SemVer.Parse(latest.VersionName);
|
||||
}
|
||||
|
||||
public VersionAnnouncementDataModel GetAnnouncementList()
|
||||
{
|
||||
throw new Exception("Mirror 酱无公告系统");
|
||||
}
|
||||
|
||||
public List<ModLoader.LoaderBase> GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output)
|
||||
{
|
||||
var loaders = new List<ModLoader.LoaderBase>();
|
||||
loaders.Add(new ModLoader.LoaderTask<int, List<DownloadFile>>(Lang.Text("Update.Task.GetDownloadInfo"), load =>
|
||||
{
|
||||
var ret = (JsonObject)Requester.FetchJson(GetUrl(channel, arch), RequestParam.WithRetry);
|
||||
var dlUrl = ret["data"]["url"]?.ToString();
|
||||
if (dlUrl is null)
|
||||
throw new Exception("Mirror 酱下载源不可用");
|
||||
load.output = new List<DownloadFile> { new(new[] { dlUrl }, output) };
|
||||
}));
|
||||
loaders.Add(new LoaderDownload(Lang.Text("Update.Task.DownloadUpdateFile"), new List<DownloadFile>()));
|
||||
return loaders;
|
||||
}
|
||||
|
||||
private string GetUrl(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
var reqUrl = mirrorChyanBaseUrl;
|
||||
reqUrl = reqUrl.Replace("{cid}", myCid);
|
||||
reqUrl = reqUrl.Replace("{cdk}", Config.Update.MirrorChyanKey);
|
||||
reqUrl = reqUrl.Replace("{arch}", arch.ToString());
|
||||
reqUrl = reqUrl.Replace("{channel}", channel.ToString());
|
||||
return reqUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class UpdatesRandomModel : IUpdateSource // 社区自己的更新系统格式
|
||||
{
|
||||
private readonly int _randIndex;
|
||||
|
||||
private readonly IEnumerable<IUpdateSource> _sources;
|
||||
|
||||
public UpdatesRandomModel(IEnumerable<IUpdateSource> sources)
|
||||
{
|
||||
_sources = sources;
|
||||
var rand = new Random(DateTime.Now.Millisecond);
|
||||
_randIndex = rand.Next(0, _sources.Count() - 1);
|
||||
}
|
||||
|
||||
public string SourceName
|
||||
{
|
||||
get => _sources.ElementAt(_randIndex).SourceName;
|
||||
set => _sources.ElementAt(_randIndex).SourceName = value;
|
||||
}
|
||||
|
||||
public bool IsAvailable()
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).IsAvailable();
|
||||
}
|
||||
|
||||
public bool RefreshCache()
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).RefreshCache();
|
||||
}
|
||||
|
||||
public VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).GetLatestVersion(channel, arch);
|
||||
}
|
||||
|
||||
public bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode)
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).IsLatest(channel, arch, currentVersion, currentVersionCode);
|
||||
}
|
||||
|
||||
public VersionAnnouncementDataModel GetAnnouncementList()
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).GetAnnouncementList();
|
||||
}
|
||||
|
||||
public List<ModLoader.LoaderBase> GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output)
|
||||
{
|
||||
return _sources.ElementAt(_randIndex).GetDownloadLoader(channel, arch, output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class UpdatesWrapperModel : IUpdateSource
|
||||
{
|
||||
private readonly IEnumerable<IUpdateSource> _sources;
|
||||
private IUpdateSource _announcementSource;
|
||||
private IUpdateSource _versionSource;
|
||||
|
||||
public UpdatesWrapperModel(IEnumerable<IUpdateSource> sources)
|
||||
{
|
||||
_sources = sources;
|
||||
}
|
||||
|
||||
public string SourceName
|
||||
{
|
||||
get => _versionSource?.SourceName ?? "";
|
||||
set
|
||||
{
|
||||
if (_versionSource is null)
|
||||
return;
|
||||
_versionSource.SourceName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAvailable()
|
||||
{
|
||||
return _sources.Any(x => x.IsAvailable());
|
||||
}
|
||||
|
||||
public bool RefreshCache()
|
||||
{
|
||||
foreach (var item in _sources)
|
||||
try
|
||||
{
|
||||
item.RefreshCache();
|
||||
_versionSource = item;
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] {item.SourceName} 暂不可用");
|
||||
}
|
||||
|
||||
return _versionSource is not null;
|
||||
}
|
||||
|
||||
public VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch)
|
||||
{
|
||||
foreach (var item in _sources)
|
||||
try
|
||||
{
|
||||
if (_versionSource is not null)
|
||||
try
|
||||
{
|
||||
return _versionSource.GetLatestVersion(channel, arch);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] 缓存的版本源 {_versionSource.SourceName} 不可用");
|
||||
}
|
||||
|
||||
var ret = item.GetLatestVersion(channel, arch);
|
||||
_versionSource = item;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] {item.SourceName} 无法获取最新版本信息");
|
||||
}
|
||||
|
||||
ModBase.Log("[Update] 错误!所有的版本源都无法使用!");
|
||||
throw new Exception(Lang.Text("Update.Task.GetVersionInfoFailed"));
|
||||
}
|
||||
|
||||
public bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode)
|
||||
{
|
||||
foreach (var item in _sources)
|
||||
try
|
||||
{
|
||||
if (_versionSource is not null)
|
||||
try
|
||||
{
|
||||
return _versionSource.IsLatest(channel, arch, currentVersion, currentVersionCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] 缓存的版本源 {_versionSource.SourceName} 不可用");
|
||||
}
|
||||
|
||||
var ret = item.IsLatest(channel, arch, currentVersion, currentVersionCode);
|
||||
_versionSource = item;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] {item.SourceName} 无法获取最新版本信息");
|
||||
}
|
||||
|
||||
ModBase.Log("[Update] 错误!所有的版本源都无法使用!");
|
||||
throw new Exception(Lang.Text("Update.Task.GetVersionInfoFailed"));
|
||||
}
|
||||
|
||||
public VersionAnnouncementDataModel GetAnnouncementList()
|
||||
{
|
||||
foreach (var item in _sources)
|
||||
try
|
||||
{
|
||||
if (_announcementSource is not null)
|
||||
try
|
||||
{
|
||||
return _announcementSource.GetAnnouncementList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] 缓存的公告源 {_announcementSource.SourceName} 不可用");
|
||||
}
|
||||
|
||||
var ret = item.GetAnnouncementList();
|
||||
_announcementSource = item;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] {item.SourceName} 无法获取最新公告信息");
|
||||
}
|
||||
|
||||
ModBase.Log("[Update] 错误!所有的公告源都无法使用!");
|
||||
throw new Exception(Lang.Text("Update.Task.GetAnnouncementFailed"));
|
||||
}
|
||||
|
||||
public List<ModLoader.LoaderBase> GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output)
|
||||
{
|
||||
foreach (var item in _sources)
|
||||
try
|
||||
{
|
||||
if (_versionSource is not null)
|
||||
try
|
||||
{
|
||||
return _versionSource.GetDownloadLoader(channel, arch, output);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] 缓存的版本源 {_versionSource.SourceName} 不可用");
|
||||
}
|
||||
|
||||
var ret = item.GetDownloadLoader(channel, arch, output);
|
||||
_versionSource = item;
|
||||
return ret;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ModBase.Log(ex, $"[Update] {item.SourceName} 无法获取最新版本信息");
|
||||
}
|
||||
|
||||
ModBase.Log("[Update] 错误!所有的版本源都无法使用!");
|
||||
throw new Exception(Lang.Text("Update.Task.GetVersionInfoFailed"));
|
||||
}
|
||||
|
||||
public async Task<bool> IsLatestAsync(UpdateChannel channel, UpdateArch arch, SemVer currentVersion,
|
||||
int currentVersionCode)
|
||||
{
|
||||
return await Task.Run(() => IsLatest(channel, arch, currentVersion, currentVersionCode));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL;
|
||||
|
||||
public class VersionAnnouncementDataModel
|
||||
{
|
||||
[JsonPropertyName("content")]
|
||||
public List<VersionAnnouncementContentModel> Content { get; set; }
|
||||
}
|
||||
|
||||
public class VersionAnnouncementContentModel
|
||||
{
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonPropertyName("detail")]
|
||||
public string Detail { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonPropertyName("date")]
|
||||
public string Date { get; set; }
|
||||
|
||||
[JsonPropertyName("btn1")]
|
||||
public AnnouncementBtnInfoModel Btn1 { get; set; }
|
||||
|
||||
[JsonPropertyName("btn2")]
|
||||
public AnnouncementBtnInfoModel Btn2 { get; set; }
|
||||
}
|
||||
|
||||
public class AnnouncementBtnInfoModel
|
||||
{
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
|
||||
[JsonPropertyName("command")]
|
||||
public string Command { get; set; }
|
||||
|
||||
[JsonPropertyName("command_paramter")]
|
||||
public string CommandParameter { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace PCL;
|
||||
|
||||
public class VersionDataModel
|
||||
{
|
||||
public string Changelog { get; set; }
|
||||
public string Sha256 { get; set; }
|
||||
public string Source { get; set; }
|
||||
public int VersionCode { get; set; }
|
||||
public string VersionName { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user