Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

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

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

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

283 lines
9.1 KiB
C#

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; }
}
}