feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -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));
}
}
@@ -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; }
}