diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index a75d79fd8..35bd4dbe0 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -59,7 +59,7 @@ jobs: PCL_LOBBY_DEFAULT_SECRET: ${{ secrets.LOBBY_DEFAULT_SECRET }} PCL_GITHUB_SHA: ${{ github.sha }} run: | - dotnet publish "Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj" \ + dotnet publish "Plain Craft Launcher 2/Plain Craft Launcher 2.csproj" \ -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.architecture }} \ -p:DeleteExistingFiles=true -o ./artifact --no-self-contained diff --git a/PCL.Core.Test/PCL.Core.Test.csproj b/PCL.Core.Test/PCL.Core.Test.csproj index 4b4ddf7cb..bdb42018e 100644 --- a/PCL.Core.Test/PCL.Core.Test.csproj +++ b/PCL.Core.Test/PCL.Core.Test.csproj @@ -1,4 +1,4 @@ - + net8.0-windows Library @@ -44,9 +44,9 @@ - - - + + + diff --git a/PCL.Core/App/Basics.cs b/PCL.Core/App/Basics.cs index 702312941..062eadcfc 100644 --- a/PCL.Core/App/Basics.cs +++ b/PCL.Core/App/Basics.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text.Json; using System.Threading; +using System.Threading.Tasks; using System.Windows; using PCL.Core.Logging; using PCL.Core.Utils; @@ -155,6 +156,19 @@ public static void OpenPath(string path, string? workingDirectory = null) }; Process.Start(psi); } + + /// + /// 以默认程序打开 Uri + /// + /// Uri 地址 + public static void OpenUri(string uri) + { + var psi = new ProcessStartInfo(uri) + { + UseShellExecute = true, + }; + _ = Task.Run(() => Process.Start(psi)); + } #endregion #region 应用程序操作 diff --git a/PCL.Core/App/Configuration/ConfigItem.cs b/PCL.Core/App/Configuration/ConfigItem.cs index 5aeabf90d..85719b8d4 100644 --- a/PCL.Core/App/Configuration/ConfigItem.cs +++ b/PCL.Core/App/Configuration/ConfigItem.cs @@ -58,6 +58,10 @@ public ConfigItem(string key, TValue defaultValue, ConfigSource source) private ConfigValueCache _valueCache = new(); + /// + /// 指定是否启用缓存。
+ /// NOTE: 禁用缓存将造成一些功能(如自动监听内容更改)不按预期工作,请仅在真正需要的时候禁用。 + ///
public bool EnableCache { get; @@ -72,23 +76,24 @@ public bool EnableCache /// 处理看起来是新的值,并返回是否真的是新的。
/// 只有启用缓存时该方法才会生效,未启用缓存将始终直接返回 。 /// - private bool _ProcessNewCache(TValue newCache, object? argument) + private bool _ProcessNewCache(TValue newCache, object? argument, bool force = false) { if (!EnableCache) return true; - var existsOld = _valueCache.TryRead(out var oldCache, argument); - if (existsOld) + if (!force) { - // 判断一下是否是新值 - if (EqualityComparer.Default.Equals(oldCache, newCache)) return false; - // 对新缓存值执行准备工作 - if (newCache is INotifyPropertyChanged reactive) - reactive.PropertyChanged += (_, _) => OnContentChanged(); - else if (newCache is INotifyCollectionChanged reactiveCollection) - reactiveCollection.CollectionChanged += (_, _) => OnContentChanged(); + // 判断是否是新值 + var existsOld = _valueCache.TryRead(out var oldCache, argument); + if (existsOld && EqualityComparer.Default.Equals(oldCache, newCache)) return false; } + // 对新缓存值执行准备工作 + if (newCache is INotifyPropertyChanged reactive) + reactive.PropertyChanged += (_, _) => OnContentChanged(); + else if (newCache is INotifyCollectionChanged reactiveCollection) + reactiveCollection.CollectionChanged += (_, _) => OnContentChanged(); + // 写入缓存 _valueCache.Write(newCache, argument); return true; - void OnContentChanged() => SetValue(newCache, argument, true); + void OnContentChanged() => SetValue(newCache, argument, bypassCache: true); } /// @@ -127,9 +132,10 @@ public object GetValueNoType(object? argument = null) /// /// 用于设置的值 /// 上下文参数 - /// 跳过缓存检查,将造成一些功能不按预期工作,请仅在真正需要的时候指定该参数 + /// 强制将传入的值视为新值,不检查缓存,仅在 时生效 + /// 跳过缓存检查和写入,相当于对本次操作临时将 设为 /// 是否成功设置值,若成功则为 true - public bool SetValue(TValue value, object? argument = null, bool bypassCacheCheck = false) + public bool SetValue(TValue value, object? argument = null, bool forceNewValue = false, bool bypassCache = false) { var e = _TriggerEvent(ConfigEvent.Set, argument, value, isPreview: true); if (e != null) @@ -137,7 +143,8 @@ public bool SetValue(TValue value, object? argument = null, bool bypassCacheChec if (e.Cancelled) return false; if (e.NewValueReplacement != null) value = (TValue)e.NewValueReplacement; } - if (bypassCacheCheck || _ProcessNewCache(value, argument)) _Provider.SetValue(Key, value, argument); + if (bypassCache || _ProcessNewCache(value, argument, forceNewValue)) + _Provider.SetValue(Key, value, argument); _TriggerEvent(ConfigEvent.Set, argument, value, e: e, isPreview: false); return true; } @@ -157,9 +164,9 @@ public bool SetValueNoType(object value, object? argument = null) } } - public bool SetDefaultValue(object? argument = null) + public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null) { - return SetValue(DefaultValue, argument); + return SetValue(DefaultValue, argument, forceNewValue ?? IsDefault(argument)); } public bool Reset(object? argument = null) @@ -333,8 +340,9 @@ public event ConfigEventHandler Changed /// 将配置项的值设置为默认值,设置后 将返回 false。 /// /// 上下文参数 + /// 强制视为新值,不检查缓存,仅在 时生效 /// 是否成功设置值,若成功则为 true - public bool SetDefaultValue(object? argument = null); + public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null); /// /// 没有泛型的
diff --git a/PCL.Core/App/IoC/Lifecycle.cs b/PCL.Core/App/IoC/Lifecycle.cs index 0abcf54b0..edc1e41a4 100644 --- a/PCL.Core/App/IoC/Lifecycle.cs +++ b/PCL.Core/App/IoC/Lifecycle.cs @@ -240,10 +240,17 @@ private static void _RemoveRunningInstance(ILifecycleService service) removed?.MarkAsStopped(); } + private static readonly ConcurrentBag _StoppingServiceTasks = []; + + private static void _WaitStoppingServiceTasks() + { + Task.WaitAll(_StoppingServiceTasks.ToArray()); + } + private static void _StopService(ILifecycleService service, bool async, bool manual = false) { var name = _ServiceName(service, manual ? LifecycleState.Manual : CurrentState); - if (async) Task.Run(Stop); + if (async) _StoppingServiceTasks.Add(Task.Run(Stop)); else Stop().Wait(); return; @@ -326,7 +333,7 @@ private static void _UpdateLastException(ILifecycleService service, Exception ex private static void _RunCurrentExecutable(string? arguments) { - var fileName = Process.GetCurrentProcess().MainModule!.FileName; + var fileName = Environment.ProcessPath!; if (arguments == null) Process.Start(fileName); else Process.Start(fileName, arguments); } @@ -335,10 +342,15 @@ private static void _RunCurrentExecutable(string? arguments) private static string? _requestRestartArguments; private static ILifecycleService? _requestRestartService; + private static readonly object _ExitLock = new(); + private static void _Exit(int statusCode = 0) { - if (HasShutdownStarted) return; - HasShutdownStarted = true; + lock (_ExitLock) + { + if (HasShutdownStarted) return; + HasShutdownStarted = true; + } // 结束 Running 计时 if (_countRunningStart is { } start) { @@ -364,6 +376,7 @@ private static void _Exit(int statusCode = 0) // 执行停止流程 _StopService(service, service.SupportAsync); } + _WaitStoppingServiceTasks(); if (logService != null) { Context.Trace("退出过程已结束,正在停止日志服务"); diff --git a/PCL.Core/Link/Broadcast.cs b/PCL.Core/Link/BroadcastLocal.cs similarity index 95% rename from PCL.Core/Link/Broadcast.cs rename to PCL.Core/Link/BroadcastLocal.cs index 4442b7231..4e779549d 100644 --- a/PCL.Core/Link/Broadcast.cs +++ b/PCL.Core/Link/BroadcastLocal.cs @@ -8,7 +8,7 @@ namespace PCL.Core.Link; -public class Broadcast(string description, int localPort) : IDisposable +public class BroadcastLocal(string description, int localPort) : IDisposable { private Socket? _broadcastSocket; private CancellationTokenSource? _cts; @@ -80,6 +80,7 @@ public void Dispose() { Stop(); _cts?.Dispose(); + _broadcastSocket.SafeClose(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/PCL.Core/Link/Lobby/LobbyController.cs b/PCL.Core/Link/Lobby/LobbyController.cs index b89358414..6905b4d52 100644 --- a/PCL.Core/Link/Lobby/LobbyController.cs +++ b/PCL.Core/Link/Lobby/LobbyController.cs @@ -19,6 +19,7 @@ using static PCL.Core.Link.Lobby.LobbyInfoProvider; using static PCL.Core.Link.Natayark.NatayarkProfileManager; using LobbyType = PCL.Core.Link.Scaffolding.Client.Models.LobbyType; +using PCL.Core.Link.McPing; namespace PCL.Core.Link.Lobby; @@ -88,7 +89,7 @@ public sealed class LobbyController var tcpPortForForward = NetworkHelper.NewTcpPort(); McForward = new TcpForward(IPAddress.Loopback, tcpPortForForward, IPAddress.Loopback, localPort); - McBroadcast = new Broadcast($"§ePCL CE 大厅{desc}", tcpPortForForward); + McBroadcast = new BroadcastLocal($"§ePCL CE 大厅{desc}", tcpPortForForward); McForward.Start(); McBroadcast.Start(); @@ -159,7 +160,7 @@ public sealed class LobbyController ///
public static async Task IsHostInstanceAvailableAsync(int port) { - var ping = new McPing("127.0.0.1", port); + using var ping = McPingServiceFactory.CreateService("127.0.0.1", port); var info = await ping.PingAsync().ConfigureAwait(false); if (info != null) return true; diff --git a/PCL.Core/Link/Lobby/LobbyInfoProvider.cs b/PCL.Core/Link/Lobby/LobbyInfoProvider.cs index 5f8b0e990..9d0390f19 100644 --- a/PCL.Core/Link/Lobby/LobbyInfoProvider.cs +++ b/PCL.Core/Link/Lobby/LobbyInfoProvider.cs @@ -17,7 +17,7 @@ public static class LobbyInfoProvider public static bool RequiresRealName { get; set; } = true; public static int ProtocolVersion { get; set; } = 6; - public static Broadcast? McBroadcast { get; internal set; } + public static BroadcastLocal? McBroadcast { get; internal set; } public static TcpForward? McForward { get; internal set; } public class LobbyInfo diff --git a/PCL.Core/Link/Lobby/LobbyService.cs b/PCL.Core/Link/Lobby/LobbyService.cs index 6e7bbaef8..067500de1 100644 --- a/PCL.Core/Link/Lobby/LobbyService.cs +++ b/PCL.Core/Link/Lobby/LobbyService.cs @@ -16,6 +16,7 @@ using System.Windows; using PCL.Core.App.IoC; using PCL.Core.UI; +using PCL.Core.Link.McPing; namespace PCL.Core.Link.Lobby; @@ -204,7 +205,7 @@ public static async Task DiscoverWorldAsync() { if (!recordedPorts.TryAdd(info.Address.Port)) return; - using var pinger = new McPing(new IPEndPoint(IPAddress.Loopback, info.Address.Port)); + using var pinger = McPingServiceFactory.CreateService(new IPEndPoint(IPAddress.Loopback, info.Address.Port)); using var cts = new CancellationTokenSource(2000); try diff --git a/PCL.Core/Link/McPing/IMcPingService.cs b/PCL.Core/Link/McPing/IMcPingService.cs new file mode 100644 index 000000000..0a9655b96 --- /dev/null +++ b/PCL.Core/Link/McPing/IMcPingService.cs @@ -0,0 +1,35 @@ +using PCL.Core.Link.McPing.Model; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace PCL.Core.Link.McPing; + +/// +/// Minecraft服务器探测服务接口 +/// +public interface IMcPingService : IDisposable +{ + /// + /// 异步探测Minecraft服务器信息 + /// + /// 取消令牌 + /// 服务器探测结果,如果探测失败则返回null + Task PingAsync(CancellationToken cancellationToken = default); + + /// + /// 获取服务端点信息 + /// + IPEndPoint Endpoint { get; } + + /// + /// 获取主机地址 + /// + string Host { get; } + + /// + /// 获取超时时间(毫秒) + /// + int Timeout { get; } +} diff --git a/PCL.Core/Link/McPing/LegacyMcPingService.cs b/PCL.Core/Link/McPing/LegacyMcPingService.cs new file mode 100644 index 000000000..de3e3e5f5 --- /dev/null +++ b/PCL.Core/Link/McPing/LegacyMcPingService.cs @@ -0,0 +1,113 @@ +using PCL.Core.Link.McPing.Model; +using PCL.Core.Logging; +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace PCL.Core.Link.McPing; + +/// +/// 旧版Minecraft协议服务器探测服务实现 +/// 支持1.6及以下版本的服务器信息查询协议 +/// +public class LegacyMcPingService : IMcPingService +{ + private readonly IPEndPoint _endpoint; + private readonly string _host; + private const int DefaultTimeout = 10000; + private readonly int _timeout; + private bool _disposed; + + public IPEndPoint Endpoint => _endpoint; + public string Host => _host; + public int Timeout => _timeout; + + public LegacyMcPingService(IPEndPoint endpoint, int timeout = DefaultTimeout) + { + _endpoint = endpoint; + _host = _endpoint.Address.ToString(); + _timeout = timeout; + } + + public LegacyMcPingService(string ip, int port = 25565, int timeout = DefaultTimeout) + { + _endpoint = IPAddress.TryParse(ip, out var ipAddress) + ? new IPEndPoint(ipAddress, port) + : new IPEndPoint(Dns.GetHostAddresses(ip).First(), port); + _host = ip; + _timeout = timeout; + } + + /// + /// 执行旧版Minecraft协议的服务器探测 + /// + /// + /// + public async Task PingAsync(CancellationToken cancellationToken = default) + { + // TODO: 实现旧版协议的探测逻辑 + // 这里需要迁移原来McPing类中的PingOldAsync方法逻辑 + + using var so = new Socket(SocketType.Stream, ProtocolType.Tcp); + using var cts = new CancellationTokenSource(); + cts.CancelAfter(_timeout); + cts.Token.Register(() => + { + try + { + if (so.Connected) so.Close(); + } + catch (ObjectDisposedException) + { + /* Ignore */ + } + }); + + await so.ConnectAsync(_endpoint, cts.Token); + LogWrapper.Debug("LegacyMcPing", $"Connected to {_endpoint}"); + await using var stream = new NetworkStream(so, false); + + var queryPack = new byte[] { 0xfe, 0x01 }; + await stream.WriteAsync(queryPack.AsMemory(0, queryPack.Length), cts.Token); + var ms = new MemoryStream(); + await stream.CopyToAsync(ms, cts.Token); + so.Close(); + var retData = ms.ToArray(); + if (retData.Length < 21 || (retData.Length >= 21 && retData[0] != 0xff)) + { + LogWrapper.Info("McPing", $"Unknown response from {_endpoint}, ignore"); + return null; + } + + var retRep = Encoding.UTF8.GetString(retData); + try + { + var retPart = retRep.Split(["\0\0\0"], StringSplitOptions.None); + retPart = retPart + .Select(s => new string([.. s.Where((_, index) => index % 2 == 0)])) + .ToArray(); + if (retPart.Length < 6) + return null; + return new McPingResult(new McPingVersionResult(retPart[2], int.Parse(retPart[1])), + new McPingPlayerResult(int.Parse(retPart[5]), int.Parse(retPart[4]), []), retPart[3], string.Empty, 0, + new McPingModInfoResult(string.Empty, []), null); + } + catch (Exception e) + { + LogWrapper.Error(e, "McPing", $"Unable to serialize response from {_endpoint}"); + return null; + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + GC.SuppressFinalize(this); + } +} diff --git a/PCL.Core/Link/McPing.cs b/PCL.Core/Link/McPing/McPingService.cs similarity index 65% rename from PCL.Core/Link/McPing.cs rename to PCL.Core/Link/McPing/McPingService.cs index 7e890486e..c1cd4a25e 100644 --- a/PCL.Core/Link/McPing.cs +++ b/PCL.Core/Link/McPing/McPingService.cs @@ -1,3 +1,4 @@ +using PCL.Core.Link.McPing.Model; using PCL.Core.Logging; using PCL.Core.Utils; using System; @@ -13,23 +14,33 @@ using System.Threading; using System.Threading.Tasks; -namespace PCL.Core.Link; +namespace PCL.Core.Link.McPing; -public class McPing : IDisposable +/// +/// 现代Minecraft协议服务器探测服务实现 +/// 支持1.7+版本的服务器信息查询协议 +/// +public class McPingService : IMcPingService { private readonly IPEndPoint _endpoint; private readonly string _host; private const int DefaultTimeout = 10000; private readonly int _timeout; + private bool _disposed; + private const string ModuleName = "McPing"; + + public IPEndPoint Endpoint => _endpoint; + public string Host => _host; + public int Timeout => _timeout; - public McPing(IPEndPoint endpoint, int timeout = DefaultTimeout) + public McPingService(IPEndPoint endpoint, int timeout = DefaultTimeout) { _endpoint = endpoint; _host = _endpoint.Address.ToString(); _timeout = timeout; } - public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) + public McPingService(string ip, int port = 25565, int timeout = DefaultTimeout) { _endpoint = IPAddress.TryParse(ip, out var ipAddress) ? new IPEndPoint(ipAddress, port) @@ -39,10 +50,10 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) } /// - /// 执行一次 Mc 服务器信息 Ping + /// 执行现代Minecraft协议的服务器探测 /// + /// /// - /// 获取的结果出现字段缺失时 public async Task PingAsync(CancellationToken cancellationToken = default) { using var so = new Socket(SocketType.Stream, ProtocolType.Tcp); @@ -51,22 +62,23 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) try { - LogWrapper.Debug("McPing", $"Connecting to {_endpoint}"); + LogWrapper.Debug(ModuleName, $"Connecting to {_endpoint}"); await so.ConnectAsync(_endpoint.Address, _endpoint.Port, linkedCts.Token); } catch (OperationCanceledException) { - LogWrapper.Error(new TimeoutException("连接超时"), "McPing", $"Failed to connect to the {_endpoint}"); + LogWrapper.Error(new TimeoutException("连接超时"), ModuleName, $"Failed to connect to the {_endpoint}"); return null; } catch (Exception e) { - LogWrapper.Error(e, "McPing", $"Failed to connect to the {_endpoint}"); + LogWrapper.Error(e, ModuleName, $"Failed to connect to the {_endpoint}"); return null; } - LogWrapper.Debug("McPing", $"Connection established: {_endpoint}"); + LogWrapper.Debug(ModuleName, $"Connection established: {_endpoint}"); await using var stream = new NetworkStream(so, false); + var handshakePacket = _BuildHandshakePacket(_host, _endpoint.Port); var statusPacket = _BuildStatusRequestPacket(); @@ -75,17 +87,17 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) try { await stream.WriteAsync(handshakePacket, linkedCts.Token); - LogWrapper.Debug("McPing", $"Handshake sent, packet length: {handshakePacket.Length}"); + LogWrapper.Debug(ModuleName, $"Handshake sent, packet length: {handshakePacket.Length}"); await stream.WriteAsync(statusPacket, linkedCts.Token); - LogWrapper.Debug("McPing", $"Status sent, packet length: {statusPacket.Length}"); + LogWrapper.Debug(ModuleName, $"Status sent, packet length: {statusPacket.Length}"); var buffer = new byte[4096]; watcher.Start(); var totalLength = Convert.ToInt64(await VarIntHelper.ReadFromStreamAsync(stream, linkedCts.Token)); watcher.Stop(); - LogWrapper.Debug("McPing", $"Total length: {totalLength}"); + LogWrapper.Debug(ModuleName, $"Total length: {totalLength}"); long readLength = 0; while (readLength < totalLength) @@ -102,7 +114,7 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) } catch (Exception e) { - LogWrapper.Error(e, "McPing", $"Failed to communicate with {_endpoint}: {e.Message}"); + LogWrapper.Error(e, ModuleName, $"Failed to communicate with {_endpoint}: {e.Message}"); return null; } finally @@ -115,7 +127,7 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) var retBinary = res.ToArray(); var dataLength = Convert.ToInt32(VarIntHelper.Decode(retBinary.Skip(1).ToArray(), out var packDataHeaderLength)); - LogWrapper.Debug("McPing", $"ServerDataLength: {dataLength}"); + LogWrapper.Debug(ModuleName, $"ServerDataLength: {dataLength}"); if (dataLength > retBinary.Length) throw new Exception("The server data is too large"); var retCtx = Encoding.UTF8.GetString(retBinary.Skip(1 + packDataHeaderLength).Take(dataLength).ToArray()); @@ -127,88 +139,31 @@ public McPing(string ip, int port = 25565, int timeout = DefaultTimeout) jsonObject["favicon"] = "..."; } - LogWrapper.Debug("McPing", resJsonDebug.ToJsonString()); + LogWrapper.Debug(ModuleName, resJsonDebug.ToJsonString()); #endif - var versionNode = retJson["version"] ?? throw new NullReferenceException("服务器返回了错误的字段,缺失: version"); - var playersNode = retJson["players"] ?? new JsonObject(); - var descNode = _ConvertJNodeToMcString(retJson["description"] ?? new JsonObject()); - var modInfoNode = retJson["modinfo"]; - var ret = new McPingResult( - new McPingVersionResult( - versionNode["name"]?.ToString() ?? "未知服务端版本名", - Convert.ToInt32(versionNode["id"]?.ToString() ?? "-1")), - new McPingPlayerResult( - Convert.ToInt32(playersNode["max"]?.ToString() ?? "0"), - Convert.ToInt32(playersNode["online"]?.ToString() ?? "0"), - (playersNode["sample"]?.AsArray() ?? []).Select(x => - new McPingPlayerSampleResult(x!["name"]?.ToString() ?? "", x["id"]?.ToString() ?? "")).ToList()), - descNode, - retJson["favicon"]?.ToString() ?? string.Empty, - watcher.ElapsedMilliseconds, - modInfoNode is null - ? null - : new McPingModInfoResult( - modInfoNode["type"]?.ToString() ?? "未知服务端类型", - (modInfoNode["modList"]?.AsArray() ?? []) - .Where(x => x!.AsObject().TryGetPropertyValue("modid", out _)) - .Select(x => new McPingModInfoModResult( - x!["modid"]?.ToString() ?? string.Empty, - x["version"]?.ToString() ?? string.Empty)) - .ToList()) - ); - return ret; - } - - public async Task PingOldAsync() - { - using var so = new Socket(SocketType.Stream, ProtocolType.Tcp); - using var cts = new CancellationTokenSource(); - cts.CancelAfter(_timeout); - cts.Token.Register(() => - { - try - { - if (so.Connected) so.Close(); - } - catch (ObjectDisposedException) - { - /* Ignore */ - } - }); - await so.ConnectAsync(_endpoint, cts.Token); - LogWrapper.Debug("McPing", $"Connected to {_endpoint}"); - await using var stream = new NetworkStream(so, false); - var queryPack = new byte[] { 0xfe, 0x01 }; - await stream.WriteAsync(queryPack.AsMemory(0, queryPack.Length), cts.Token); - var ms = new MemoryStream(); - await stream.CopyToAsync(ms, cts.Token); - so.Close(); - var retData = ms.ToArray(); - if (retData.Length < 21 || (retData.Length >= 21 && retData[0] != 0xff)) + // 先处理Description字段,将其转换为字符串形式 + if (retJson["description"] is JsonObject descObj) { - LogWrapper.Info("McPing", $"Unknown response from {_endpoint}, ignore"); - return null; + retJson["description"] = _ConvertJNodeToMcString(descObj); } - var retRep = Encoding.UTF8.GetString(retData); - try - { - var retPart = retRep.Split(["\0\0\0"], StringSplitOptions.None); - retPart = retPart.Select(s => new string(s - .Where((_, index) => index % 2 == 0) - .ToArray())) - .ToArray(); - if (retPart.Length < 6) - return null; - return new McPingResult(new McPingVersionResult(retPart[2], int.Parse(retPart[1])), - new McPingPlayerResult(int.Parse(retPart[5]), int.Parse(retPart[4]), []), retPart[3], string.Empty, 0, - new McPingModInfoResult(string.Empty, [])); - } - catch (Exception e) + var response = JsonSerializer.Deserialize(retJson); + if (response?.Version == null) + throw new NullReferenceException("服务器返回了错误的字段,缺失: version"); + + response = response with { - LogWrapper.Error(e, "McPing", $"Unable to serialize response from {_endpoint}"); - return null; - } + Latency = watcher.ElapsedMilliseconds + }; + + return response; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + GC.SuppressFinalize(this); } /// @@ -301,13 +256,13 @@ private static string _ConvertJNodeToMcString(JsonNode? jsonNode) } default: { - LogWrapper.Warn("McPing", $"解析到无法处理的 Motd 内容({current.GetValueKind()}):{current}"); + LogWrapper.Warn(ModuleName, $"解析到无法处理的 Motd 内容({current.GetValueKind()}):{current}"); break; } } } - LogWrapper.Debug("McPing", $"处理 Motd 内容完成,结果:{result}"); + LogWrapper.Debug(ModuleName, $"处理 Motd 内容完成,结果:{result}"); return result.ToString(); } @@ -349,13 +304,4 @@ private static string _GetTextStyleString( if (color.StartsWith('#')) sb.Append(color); return sb.ToString(); } - - private bool _disposed; - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - GC.SuppressFinalize(this); - } } diff --git a/PCL.Core/Link/McPing/McPingServiceFactory.cs b/PCL.Core/Link/McPing/McPingServiceFactory.cs new file mode 100644 index 000000000..bfdf4ff6f --- /dev/null +++ b/PCL.Core/Link/McPing/McPingServiceFactory.cs @@ -0,0 +1,56 @@ +using System.Net; + +namespace PCL.Core.Link.McPing; + +/// +/// Minecraft服务器探测服务工厂 +/// 提供统一的服务创建接口 +/// +public static class McPingServiceFactory +{ + /// + /// 创建现代协议探测服务 + /// + /// 服务器端点 + /// 超时时间(毫秒) + /// IMcPingService实例 + public static IMcPingService CreateService(IPEndPoint endpoint, int timeout = 10000) + { + return new McPingService(endpoint, timeout); + } + + /// + /// 创建现代协议探测服务 + /// + /// 服务器IP地址 + /// 服务器端口 + /// 超时时间(毫秒) + /// IMcPingService实例 + public static IMcPingService CreateService(string ip, int port = 25565, int timeout = 10000) + { + return new McPingService(ip, port, timeout); + } + + /// + /// 创建旧版协议探测服务 + /// + /// 服务器端点 + /// 超时时间(毫秒) + /// IMcPingService实例 + public static IMcPingService CreateLegacyService(IPEndPoint endpoint, int timeout = 10000) + { + return new LegacyMcPingService(endpoint, timeout); + } + + /// + /// 创建旧版协议探测服务 + /// + /// 服务器IP地址 + /// 服务器端口 + /// 超时时间(毫秒) + /// IMcPingService实例 + public static IMcPingService CreateLegacyService(string ip, int port = 25565, int timeout = 10000) + { + return new LegacyMcPingService(ip, port, timeout); + } +} diff --git a/PCL.Core/Link/McPing/Model/McPingModInfoModResult.cs b/PCL.Core/Link/McPing/Model/McPingModInfoModResult.cs new file mode 100644 index 000000000..b15db4b4c --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingModInfoModResult.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingModInfoModResult( + [property: JsonPropertyName("modid")] string Id, + [property: JsonPropertyName("version")] string Version); \ No newline at end of file diff --git a/PCL.Core/Link/McPing/Model/McPingModInfoResult.cs b/PCL.Core/Link/McPing/Model/McPingModInfoResult.cs new file mode 100644 index 000000000..aafb73e9e --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingModInfoResult.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingModInfoResult( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("modList")] List ModList); \ No newline at end of file diff --git a/PCL.Core/Link/McPing/Model/McPingPlayerResult.cs b/PCL.Core/Link/McPing/Model/McPingPlayerResult.cs new file mode 100644 index 000000000..7673b6573 --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingPlayerResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingPlayerResult( + [property: JsonPropertyName("max")] int Max, + [property: JsonPropertyName("online")] int Online, + [property: JsonPropertyName("sample")] List? Samples); \ No newline at end of file diff --git a/PCL.Core/Link/McPing/Model/McPingPlayerSampleResult.cs b/PCL.Core/Link/McPing/Model/McPingPlayerSampleResult.cs new file mode 100644 index 000000000..2ed6f9c13 --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingPlayerSampleResult.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingPlayerSampleResult( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("id")] string Id); \ No newline at end of file diff --git a/PCL.Core/Link/McPing/Model/McPingResult.cs b/PCL.Core/Link/McPing/Model/McPingResult.cs new file mode 100644 index 000000000..7ad10d665 --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingResult.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingResult( + [property: JsonPropertyName("version")] McPingVersionResult Version, + [property: JsonPropertyName("players")] McPingPlayerResult Players, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("favicon")] string? Favicon, + [property: JsonPropertyName("latency")] long Latency, + [property: JsonPropertyName("modinfo")] McPingModInfoResult? ModInfo, + [property: JsonPropertyName("preventsChatReports")] bool? PreventsChatReports); \ No newline at end of file diff --git a/PCL.Core/Link/McPing/Model/McPingVersionResult.cs b/PCL.Core/Link/McPing/Model/McPingVersionResult.cs new file mode 100644 index 000000000..f7412dfee --- /dev/null +++ b/PCL.Core/Link/McPing/Model/McPingVersionResult.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.Link.McPing.Model; + +public record McPingVersionResult( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("protocol")] int Protocol); \ No newline at end of file diff --git a/PCL.Core/Link/McPingResult.cs b/PCL.Core/Link/McPingResult.cs deleted file mode 100644 index 1a0b7aec9..000000000 --- a/PCL.Core/Link/McPingResult.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; - -namespace PCL.Core.Link; - -public record McPingResult( - McPingVersionResult Version, - McPingPlayerResult Players, - string Description, - string Favicon, - long Latency, - McPingModInfoResult? ModInfo); - -public record McPingVersionResult( - string Name, - int Protocol); - -public record McPingPlayerResult( - int Max, - int Online, - List Samples); - -public record McPingPlayerSampleResult( - string Name, - string Id); - -public record McPingModInfoResult( - string Type, - List ModList); - -public record McPingModInfoModResult( - string Id, - string Version); \ No newline at end of file diff --git a/PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs b/PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs index 3ab17c58a..8bb0fbe10 100644 --- a/PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs +++ b/PCL.Core/Link/Scaffolding/EasyTier/EasyTierEntity.cs @@ -158,7 +158,7 @@ private async Task _BuildProcessAsync(bool asHost) EnableRaisingEvents = true, StartInfo = new ProcessStartInfo { - FileName = $"{EasyTierMetadata.EasyTierFilePath}\\easytier-core.exe", + FileName = Path.Combine(EasyTierMetadata.EasyTierFilePath, "easytier-core.exe"), WorkingDirectory = EasyTierMetadata.EasyTierFilePath, WindowStyle = ProcessWindowStyle.Hidden } @@ -206,7 +206,9 @@ private async Task _BuildProcessAsync(bool asHost) .Add("l", "udp://0.0.0.0:0"); } - foreach (var address in _fallbackNodeLinks) + foreach (var address in ETRelay.RelayList + .Select(static x => x.Url) + .Concat(_fallbackNodeLinks)) { args.Add("p", address); } diff --git a/PCL.Core/Logging/LogService.cs b/PCL.Core/Logging/LogService.cs index b36b6984a..ded59eb6a 100644 --- a/PCL.Core/Logging/LogService.cs +++ b/PCL.Core/Logging/LogService.cs @@ -36,11 +36,11 @@ public Task StartAsync() return Task.CompletedTask; } - public Task StopAsync() + public async Task StopAsync() { if (_wrapperRegistered) LogWrapper.OnLog -= _OnWrapperLog; - _logger?.Dispose(); - return Task.CompletedTask; + if (_logger != null) + await _logger.DisposeAsync(); } private static void _LogAction(ActionLevel level, string formatted, string plain, Exception? ex) diff --git a/PCL.Core/Logging/LogWrapper.cs b/PCL.Core/Logging/LogWrapper.cs index 48519ec2a..fdbdc0a6b 100644 --- a/PCL.Core/Logging/LogWrapper.cs +++ b/PCL.Core/Logging/LogWrapper.cs @@ -39,4 +39,10 @@ public static class LogWrapper public static void Trace(string msg) => Trace(null, msg); public static Logger CurrentLogger => LogService.Logger; + + private static readonly Lazy _LoggerFactory = new(static () => + { + return new LoggerFactoryAdapter(CurrentLogger); + }); + public static LoggerFactoryAdapter LoggerFactory => _LoggerFactory.Value; } diff --git a/PCL.Core/Logging/Logger.cs b/PCL.Core/Logging/Logger.cs index 3bc63da9a..bc971217e 100644 --- a/PCL.Core/Logging/Logger.cs +++ b/PCL.Core/Logging/Logger.cs @@ -12,13 +12,13 @@ namespace PCL.Core.Logging; -public sealed class Logger : IDisposable +public sealed class Logger : IAsyncDisposable { public Logger(LoggerConfiguration configuration) { Configuration = configuration; _CreateNewFile(); - _processingTask = _ProcessLogQueueAsync(_cancelToken.Token); + _processingTask = _ProcessLogQueueAsync(); } // Data stream private StreamWriter? _currentStream; @@ -33,7 +33,6 @@ public Logger(LoggerConfiguration configuration) { SingleReader = true }); - private readonly CancellationTokenSource _cancelToken = new(); public ReadOnlyCollection CurrentLogFiles => _files.AsReadOnly(); @@ -102,7 +101,7 @@ public void Log(string message) } } - private async Task _ProcessLogQueueAsync(CancellationToken token) + private async Task _ProcessLogQueueAsync() { const int maxBatchLines = 198; var writeTimeout = TimeSpan.FromMilliseconds(325); @@ -112,7 +111,7 @@ private async Task _ProcessLogQueueAsync(CancellationToken token) try { - while (!token.IsCancellationRequested) + while (!_disposed || _logChannel.Reader.TryPeek(out _)) { if (_logChannel.Reader.TryRead(out var message)) { @@ -136,24 +135,17 @@ private async Task _ProcessLogQueueAsync(CancellationToken token) { await DoRefreshAsync().ConfigureAwait(false); } - await Task.Delay(80, token).ConfigureAwait(false); - } - - if (_logChannel.Reader.Completion.IsCompleted) break; - - async Task DoRefreshAsync() - { - await _DoWriteAsync(batch).ConfigureAwait(false); - batch.Clear(); - lineCount = 0; - lastFlush = Stopwatch.GetTimestamp(); + await Task.Delay(80).ConfigureAwait(false); } } - } - catch (OperationCanceledException) - { - if (lineCount > 0) + + async Task DoRefreshAsync() + { await _DoWriteAsync(batch).ConfigureAwait(false); + batch.Clear(); + lineCount = 0; + lastFlush = Stopwatch.GetTimestamp(); + } } catch (Exception e) { @@ -172,26 +164,28 @@ private async Task _DoWriteAsync(StringBuilder ctx) _CreateNewFile(); } await _currentStream!.WriteAsync(ctx).ConfigureAwait(false); + await _currentStream.FlushAsync().ConfigureAwait(false); } catch (Exception e) { Console.WriteLine($"[{_GetTimeFormatted()}] [ERROR] An error occured while writing log file: {e.Message}"); + await File.AppendAllTextAsync(Path.Combine(Configuration.StoreFolder, "Error.log"), $"[{_GetTimeFormatted}] LogCycle Error: {e}\n"); throw; } } private bool _disposed; - public void Dispose() + public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; - _cancelToken.Cancel(); + _logChannel.Writer.Complete(); - _processingTask.Forget(); - _processingTask.ContinueWith(_ => - { - _currentStream?.Dispose(); - _currentFile?.Dispose(); - }).Forget(); + await _processingTask; + + if (_currentStream != null) + await _currentStream.DisposeAsync().ConfigureAwait(false); + if (_currentFile != null) + await _currentFile.DisposeAsync().ConfigureAwait(false); } } \ No newline at end of file diff --git a/PCL.Core/Logging/LoggerAdapter.cs b/PCL.Core/Logging/LoggerAdapter.cs new file mode 100644 index 000000000..4ad9990fa --- /dev/null +++ b/PCL.Core/Logging/LoggerAdapter.cs @@ -0,0 +1,137 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +namespace PCL.Core.Logging; + +/// +/// Microsoft.Extensions.Logging.ILogger 适配器 +/// 将现有的 Logger 包装为标准的 ILogger 接口实现 +/// +public class LoggerAdapter(Logger logger, string categoryName) : ILogger +{ + private readonly Logger _innerLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly string _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName)); + + private static readonly AsyncLocal> _ScopeStack = new(); + + IDisposable ILogger.BeginScope(TState state) + { + _ScopeStack.Value ??= new Stack(); + _ScopeStack.Value.Push(state); + return new ScopeDisposable(state); + } + +#pragma warning disable CS9113 // 参数未读。 + private class ScopeDisposable(object state) : IDisposable + { + private bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + if (_ScopeStack.Value is { Count: > 0 }) + { +#if DEBUG + var popped = _ScopeStack.Value.Pop(); + if (!ReferenceEquals(popped, state)) + { + throw new InvalidOperationException("Scope disposal order mismatch."); + } +#else + _ = _ScopeStack.Value.Pop(); +#endif + } + + _disposed = true; + } + } + + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel level) => true; + + public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + ArgumentNullException.ThrowIfNull(formatter); + + var originalMessage = formatter(state, exception); + var sb = new StringBuilder(); + + // 类别名称 + if (!string.IsNullOrEmpty(_categoryName)) + { + sb.Append('[').Append(_categoryName).Append("] "); + } + + // 事件 ID + if (eventId.Id != 0 || !string.IsNullOrEmpty(eventId.Name)) + { + sb.Append("[EventId:"); + if (!string.IsNullOrEmpty(eventId.Name)) + { + sb.Append(eventId.Id).Append(':').Append(eventId.Name); + } + else + { + sb.Append(eventId.Id); + } + sb.Append("] "); + } + + // 上下文信息 + var scopeContext = _BuildScopeContext(); + if (!string.IsNullOrEmpty(scopeContext)) + { + sb.Append('[').Append(_categoryName).Append("] "); + } + + sb.Append(originalMessage); + var finalMessage = sb.ToString(); + + // 日志级别 + switch (logLevel) + { + case Microsoft.Extensions.Logging.LogLevel.Trace: + _innerLogger.Trace(finalMessage); + break; + case Microsoft.Extensions.Logging.LogLevel.Debug: + _innerLogger.Debug(finalMessage); + break; + case Microsoft.Extensions.Logging.LogLevel.Information: + _innerLogger.Info(finalMessage); + break; + case Microsoft.Extensions.Logging.LogLevel.Warning: + _innerLogger.Warn(finalMessage); + break; + case Microsoft.Extensions.Logging.LogLevel.Error: + _innerLogger.Error(finalMessage); + break; + case Microsoft.Extensions.Logging.LogLevel.Critical: + _innerLogger.Fatal(finalMessage); + break; + } + + if (exception != null) + { + var exceptionMessage = $"Exception: {exception}"; + _innerLogger.Log($"[{_categoryName}] {exceptionMessage}"); + } + } + + private string _BuildScopeContext() + { + var stack = _ScopeStack.Value; + if (stack == null || stack.Count == 0) + return string.Empty; + + var scopes = stack.AsEnumerable().Reverse(); + return string.Join(" => ", scopes.Select(s => s.ToString())); + } +} diff --git a/PCL.Core/Logging/LoggerExtensions.cs b/PCL.Core/Logging/LoggerExtensions.cs new file mode 100644 index 000000000..33f650e07 --- /dev/null +++ b/PCL.Core/Logging/LoggerExtensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace PCL.Core.Logging; + +public static class LoggerExtensions +{ + /// + /// 创建 ILogger 实例 + /// + /// 现有的 Logger 实例 + /// 日志类别名称 + /// ILogger 实例 + public static ILogger CreateLogger(this Logger logger, string categoryName) + { + return new LoggerAdapter(logger, categoryName); + } + + /// + /// 创建 ILogger 工厂 + /// + /// 现有的 Logger 实例 + /// ILoggerFactory 实例 + public static ILoggerFactory CreateLoggerFactory(this Logger logger) + { + return new LoggerFactoryAdapter(logger); + } + + /// + /// 使用结构化日志记录的扩展方法 + /// + public static void LogInformation(this ILogger logger, string message, T0 arg0) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0); + } + + public static void LogInformation(this ILogger logger, string message, T0 arg0, T1 arg1) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1); + } + + public static void LogInformation(this ILogger logger, string message, T0 arg0, T1 arg1, T2 arg2) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, message, arg0, arg1, arg2); + } + + public static void LogWarning(this ILogger logger, string message, T0 arg0) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0); + } + + public static void LogWarning(this ILogger logger, string message, T0 arg0, T1 arg1) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Warning, message, arg0, arg1); + } + + public static void LogError(this ILogger logger, Exception? exception, string message, T0 arg0) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0); + } + + public static void LogError(this ILogger logger, Exception? exception, string message, T0 arg0, T1 arg1) + { + logger.Log(Microsoft.Extensions.Logging.LogLevel.Error, exception, message, arg0, arg1); + } + + /// + /// 条件日志记录扩展方法 + /// + public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, string message) + { + if (condition) + { + logger.Log(level, message); + } + } + + public static void LogIf(this ILogger logger, bool condition, Microsoft.Extensions.Logging.LogLevel level, Exception? exception, string message) + { + if (condition) + { + logger.Log(level, exception, message); + } + } + + /// + /// 性能计时日志记录 + /// + public static IDisposable LogPerformance(this ILogger logger, string operationName) + { + logger.LogInformation("开始执行: {OperationName}", operationName); + + return new PerformanceLoggerDisposable(logger, operationName); + } + + private class PerformanceLoggerDisposable(ILogger logger, string operationName) : IDisposable + { + private readonly long _startTime = Stopwatch.GetTimestamp(); + + public void Dispose() + { + var elapsed = Stopwatch.GetElapsedTime(_startTime); + logger.LogInformation("完成执行: {OperationName}, 耗时: {ElapsedMs}ms", operationName, elapsed.TotalMilliseconds); + } + } +} \ No newline at end of file diff --git a/PCL.Core/Logging/LoggerFactoryAdapter.cs b/PCL.Core/Logging/LoggerFactoryAdapter.cs new file mode 100644 index 000000000..5a610fca6 --- /dev/null +++ b/PCL.Core/Logging/LoggerFactoryAdapter.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace PCL.Core.Logging; + +/// +/// ILoggerFactory 实现,用于创建 LoggerAdapter 实例 +/// +public class LoggerFactoryAdapter(Logger logger) : ILoggerFactory +{ + private readonly Logger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly List _disposables = []; + + public void AddProvider(ILoggerProvider provider) + { + _disposables.Add(provider); + // 不需要实现,因为我们只有一个固定的 Logger + } + + public ILogger CreateLogger(string categoryName) + { + return new LoggerAdapter(_logger, categoryName); + } + + public void Dispose() + { + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + _disposables.Clear(); + } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebKeys.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebKeys.cs new file mode 100644 index 000000000..e04862ee9 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebKeys.cs @@ -0,0 +1,9 @@ +using Microsoft.IdentityModel.Tokens; +using System.Text.Json.Serialization; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken; + +public record JsonWebKeys +{ + [JsonPropertyName("keys")] public required JsonWebKey[] Keys; +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebToken.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebToken.cs new file mode 100644 index 000000000..dcb63fb15 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/JsonWebToken/JsonWebToken.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security; +using System.Text.Json; +using Microsoft.IdentityModel.Tokens; +using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken; + +/// +/// Json Web Token 类 +/// +/// JWT 令牌字符串 +/// OpenID 元数据 +public class JsonWebToken(string token, OpenIdMetadata meta) +{ + public delegate SecurityToken? TokenValidateCallback(OpenIdMetadata metadata, string token, JsonWebKey? key, string? clientId); + + /// + /// 安全令牌验证回调函数,默认验证签名、发行者、nbf 和 exp + /// + public TokenValidateCallback SecurityTokenValidateCallback { get; set; } = static (meta, token, key, clientId) => + { + try + { + var handler = new JwtSecurityTokenHandler(); + + var parameter = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = meta.Issuer, + ValidateAudience = !string.IsNullOrEmpty(clientId), + ValidAudience = clientId, + ValidateIssuerSigningKey = key != null, + IssuerSigningKey = key != null ? new JsonWebKeySet { Keys = { key } }.Keys[0] : null, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromSeconds(60) + }; + + handler.ValidateToken(token, parameter, out var secToken); + return secToken; + } + catch (Exception ex) + { + throw new SecurityException($"令牌验证失败:{ex.Message}", ex); + } + }; + + private bool _verified; + private JwtSecurityToken? _parsedToken; + private readonly JwtSecurityTokenHandler _tokenHandler = new(); + + /// + /// 解析令牌(不验证签名) + /// + /// 解析后的 JWT 令牌对象 + /// 令牌格式无效 + private JwtSecurityToken _ParseToken() + { + if (_parsedToken != null) + return _parsedToken; + + try + { + if (!_tokenHandler.CanReadToken(token)) + throw new SecurityException("无法读取令牌:格式无效"); + + _parsedToken = _tokenHandler.ReadJwtToken(token); + return _parsedToken; + } + catch (Exception ex) + { + throw new SecurityException($"令牌解析失败:{ex.Message}", ex); + } + } + + /// + /// 尝试读取 Token 中的字段 + /// + /// 是否允许在未验证的情况下读取字段,若为 false,当 Token 未验证时将抛出异常 + /// 声明值的目标类型 + /// 解析后的声明对象 + /// 未调用 VerifySignature() 且 allowUnverifyToken 为 false + /// 令牌中不存在 payload 数据 + public T? ReadTokenPayload(bool allowUnverifyToken = false) + { + if (!allowUnverifyToken && !_verified) + throw new SecurityException("不安全的令牌"); + + try + { + var jwtToken = _ParseToken(); + + if (jwtToken.Payload == null || jwtToken.Payload.Count == 0) + throw new InvalidOperationException("令牌 Payload 无效"); + + if (typeof(T).IsAssignableFrom(typeof(Dictionary))) + return (T)(object)jwtToken.Payload; + + if (typeof(T) == typeof(JwtPayload)) + return (T)(object)jwtToken.Payload; + + var payloadJson = JsonSerializer.Serialize(jwtToken.Payload); + var result = JsonSerializer.Deserialize(payloadJson); + + return result; + } + catch (SecurityException) + { + throw; + } + catch (Exception ex) + { + throw new SecurityException($"读取令牌 payload 失败:{ex.Message}", ex); + } + } + + /// + /// 读取 Token 头 + /// + /// 声明值的目标类型 + /// 解析后的头对象 + /// 令牌中不存在 header 数据 + public T? ReadTokenHeader() + { + try + { + var jwtToken = _ParseToken(); + + if (jwtToken.Header == null || jwtToken.Header.Count == 0) + throw new InvalidOperationException("令牌中不存在 header 数据"); + + if (typeof(T).IsAssignableFrom(typeof(Dictionary))) + return (T)(object)jwtToken.Header; + + if (typeof(T) == typeof(JwtHeader)) + return (T)(object)jwtToken.Header; + + var headerJson = JsonSerializer.Serialize(jwtToken.Header); + var result = JsonSerializer.Deserialize(headerJson); + + return result; + } + catch (Exception ex) + { + throw new SecurityException($"读取令牌 header 失败:{ex.Message}", ex); + } + } + + /// + /// 对 Token 进行签名验证
+ /// 默认情况下仅对签名、iss、nbf、exp 进行验证,如果需要更细粒度验证,请设置 + ///
+ /// 用于验证签名的 JSON Web Key + /// 预期的受众(audience),可选 + /// 验证成功返回 SecurityToken 对象,否则返回 null + public SecurityToken? VerifySignature(JsonWebKey key, string? clientId = null) + { + try + { + var result = SecurityTokenValidateCallback.Invoke(meta, token, key, clientId); + if (result != null) + _verified = true; + return result; + } + catch (Exception ex) + { + throw new SecurityException($"令牌签名验证失败:{ex.Message}", ex); + } + } + + /// + /// 重载方法,用于无参调用验证(仅验证基本声明) + /// + /// 验证成功返回 SecurityToken 对象,否则返回 null + public SecurityToken? VerifySignature() + { + try + { + var result = SecurityTokenValidateCallback.Invoke(meta, token, null, null); + if (result != null) + _verified = true; + return result; + } + catch (Exception ex) + { + throw new SecurityException($"令牌验证失败:{ex.Message}", ex); + } + } + + /// + /// 获取令牌的过期时间 + /// + /// 过期时间,若不存在则返回 null + public DateTime? GetExpirationTime() + { + try + { + var jwtToken = _ParseToken(); + return jwtToken.ValidTo != DateTime.MinValue ? jwtToken.ValidTo : null; + } + catch (Exception ex) + { + throw new SecurityException($"获取令牌过期时间失败:{ex.Message}", ex); + } + } + + /// + /// 获取令牌的签发时间 + /// + /// 签发时间,若不存在则返回 null + public DateTime? GetIssuedAtTime() + { + try + { + var jwtToken = _ParseToken(); + return jwtToken.ValidFrom != DateTime.MinValue ? jwtToken.ValidFrom : null; + } + catch (Exception ex) + { + throw new SecurityException($"获取令牌签发时间失败:{ex.Message}", ex); + } + } + + /// + /// 检查令牌是否已过期 + /// + /// 若已过期返回 true,否则返回 false + public bool IsExpired() + { + try + { + var expTime = GetExpirationTime(); + return expTime.HasValue && DateTime.UtcNow > expTime.Value; + } + catch + { + return true; // 如果无法解析,视为已过期 + } + } + + /// + /// 获取特定声明的值 + /// + /// 声明类型 + /// 是否允许在未验证的情况下读取 + /// 声明值,若不存在则返回 null + public string? GetClaimValue(string claimType, bool allowUnverifyToken = false) + { + try + { + var payload = ReadTokenPayload>(allowUnverifyToken); + return payload?.TryGetValue(claimType, out var value) ?? false ? value.ToString() : null; + } + catch (Exception ex) + { + throw new SecurityException($"获取声明值失败({claimType}):{ex.Message}", ex); + } + } + + /// + /// 获取原始令牌字符串 + /// + /// JWT 令牌字符串 + public string GetTokenString() => token; + + /// + /// 检查令牌验证状态 + /// + public bool IsVerified => _verified; +} diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdClient.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdClient.cs new file mode 100644 index 000000000..d03539a08 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdClient.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Documents; +using PCL.Core.Minecraft.IdentityModel.Extensions.Pkce; +using PCL.Core.Minecraft.IdentityModel.OAuth; +using PCL.Core.Utils.Exts; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; + +public class OpenIdClient(OpenIdOptions options):IOAuthClient +{ + private IOAuthClient? _client; + /// + /// 初始化并从网络加载 OpenId 配置 + /// + /// + /// + /// 当要求检查地址并不存在任何授权端点时,将触发此错误 + public async Task InitializeAsync(CancellationToken token,bool checkAddress = false) + { + var opt = await options.BuildOAuthOptionsAsync(token); + if (!checkAddress || opt.Meta.AuthorizeEndpoint.IsNullOrEmpty() || opt.Meta.DeviceEndpoint.IsNullOrEmpty()) + { + _client = options.EnablePkceSupport ? new PkceClient(opt) : new SimpleOAuthClient(opt); + return; + } + + throw new InvalidOperationException(); + } + /// + /// 获取授权代码流地址 + /// + /// 权限列表 + /// + /// 扩展数据 + /// + /// 未调用 + public string GetAuthorizeUrl(string[] scopes, string state,Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return _client.GetAuthorizeUrl(scopes, state, extData); + } + /// + /// 使用授权代码兑换 Token + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithCodeAsync(code, token, extData); + } + /// + /// 获取设备代码流代码对 + /// + /// + /// + /// + /// + /// + public async Task GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.GetCodePairAsync(scopes, token, extData); + } + /// + /// 发起一次验证,以检查认证是否成功 + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithDeviceAsync(data, token, extData); + } + /// + /// 进行一次刷新调用 + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithSilentAsync(data, token, extData); + } +} diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdMetaData.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdMetaData.cs new file mode 100644 index 000000000..151a5de0a --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdMetaData.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; + + + +public record OpenIdMetadata +{ + [JsonPropertyName("issuer")] + public required string Issuer { get; init; } + + [JsonPropertyName("authorization_endpoint")] + public string? AuthorizationEndpoint { get; init; } + + [JsonPropertyName("device_authorization_endpoint")] + public string? DeviceAuthorizationEndpoint { get; init; } + + [JsonPropertyName("token_endpoint")] + public required string TokenEndpoint { get; init; } + + [JsonPropertyName("userinfo_endpoint")] + public required string UserInfoEndpoint { get; init; } + + [JsonPropertyName("registration_endpoint")] + public string? RegistrationEndpoint { get; init; } + + [JsonPropertyName("jwks_uri")] + public required string JwksUri { get; init; } + + [JsonPropertyName("scopes_supported")] + public required IReadOnlyList ScopesSupported { get; init; } + + [JsonPropertyName("subject_types_supported")] + public required IReadOnlyList SubjectTypesSupported { get; init; } + + [JsonPropertyName("id_token_signing_alg_values_supported")] + public required IReadOnlyList IdTokenSigningAlgValuesSupported { get; init; } + + +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs new file mode 100644 index 000000000..e9b5f0b74 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/OpenId/OpenIdOptions.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; +using PCL.Core.Minecraft.IdentityModel.Extensions.JsonWebToken; +using PCL.Core.Minecraft.IdentityModel.OAuth; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; + +public record OpenIdOptions +{ + /// + /// OpenId Discovery 地址 + /// + public required string OpenIdDiscoveryAddress { get; set; } + /// + /// 客户端 ID(必须设置) + /// + public required string ClientId + { + get; + set; + } + + // 为了让 YggdrasilConnect Client 复用代码做的逻辑 + + /// + /// 是否只使用设备代码流授权 + /// + public bool OnlyDeviceAuthorize { get; set; } + /// + /// 回调 Uri + /// + public string? RedirectUri { get; set; } + /// + /// 发送 HTTP 请求时设置的请求头,仅适用于请求头(丢到 HttpRequestMessage 不会报错的那种) + /// + public Dictionary? Headers { get; set; } + /// + /// 是否启用 PKCE 支持,默认启用 + /// + public bool EnablePkceSupport { get; set; } = true; + /// + /// 获取 HttpClient,生命周期由调用方管理 + /// + public required Func GetClient { get; set; } + /// + /// OpenId 元数据,请勿自行设置此属性,而是应该调用 + /// + public OpenIdMetadata? Meta { get; internal set; } + + /// + /// 从互联网拉取 OpenID 配置信息 + /// + /// + public virtual async Task InitializeAsync(CancellationToken token) + { + using var request = new HttpRequestMessage(HttpMethod.Get, OpenIdDiscoveryAddress); + if (Headers is not null) + foreach (var kvp in Headers) + { + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + } + + var requestTask = GetClient.Invoke().SendAsync(request, token); + using var response = await requestTask; + var task = response.Content.ReadAsStringAsync(token); + Meta = JsonSerializer.Deserialize(await task); + } + /// + /// 获取 Json Web Key + /// + /// 密钥 ID + /// + /// + /// 未调用 + /// 找不到 Jwk 或 Jwk 配置无效 + public async Task GetSignatureKeyAsync(string kid,CancellationToken token) + { + if (Meta?.JwksUri is null) throw new InvalidOperationException(); + using var request = new HttpRequestMessage(HttpMethod.Get, Meta.JwksUri); + if (Headers is not null) + foreach (var kvp in Headers) + { + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + } + using var response = await GetClient.Invoke().SendAsync(request, token); + var result = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(token)); + return result?.Keys.Single(k => k.Kid == kid) + ?? throw new FormatException(); + } + /// + /// 构建 OAuth 客户端配置 + /// + /// + /// + /// 未调用 + public virtual async Task BuildOAuthOptionsAsync(CancellationToken token) + { + if (Meta is null) throw new InvalidOperationException(); + if(!OnlyDeviceAuthorize) ArgumentException.ThrowIfNullOrEmpty(RedirectUri); + return new OAuthClientOptions + { + GetClient = GetClient, + ClientId = ClientId, + RedirectUri = OnlyDeviceAuthorize ? string.Empty:RedirectUri!, + Meta = new EndpointMeta + { + AuthorizeEndpoint = Meta?.AuthorizationEndpoint??string.Empty, + DeviceEndpoint = Meta?.DeviceAuthorizationEndpoint??string.Empty, + TokenEndpoint = Meta!.TokenEndpoint, + } + }; + } + +} diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceChallengeOptions.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceChallengeOptions.cs new file mode 100644 index 000000000..20d39ac80 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceChallengeOptions.cs @@ -0,0 +1,7 @@ +namespace PCL.Core.Minecraft.IdentityModel.Extensions.Pkce; + +public enum PkceChallengeOptions +{ + Sha256, + PlainText +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceClient.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceClient.cs new file mode 100644 index 000000000..4ef2e8d38 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/Pkce/PkceClient.cs @@ -0,0 +1,88 @@ +using System; +using PCL.Core.Utils.Exts; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.Minecraft.IdentityModel.OAuth; +using PCL.Core.Utils.Hash; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.Pkce; + +/// +/// 带 PKCE 支持的客户端
+/// 此客户端并非线程安全,请勿在多个线程间共享示例 +///
+/// +public class PkceClient(OAuthClientOptions options):IOAuthClient +{ + private byte[] _ChallengeCode { get; set; } = new byte[32]; + private bool _isCallGetAuthorizeUrl; + /// + /// 设置验证方法,支持 PlainText 和 SHA256 + /// + public PkceChallengeOptions ChallengeMethod { get; private set; } = PkceChallengeOptions.Sha256; + private readonly SimpleOAuthClient _client = new(options); + /// + /// 获取授权地址 + /// + /// + /// + /// + /// + public string GetAuthorizeUrl(string[] scopes, string state, Dictionary? extData) + { + RandomNumberGenerator.Fill(_ChallengeCode); + extData ??= []; + extData["code_challenge"] = ChallengeMethod == PkceChallengeOptions.Sha256 + ? SHA256Provider.Instance.ComputeHash(_ChallengeCode).ToHexString() + : _ChallengeCode.FromBytesToB64UrlSafe(); + extData["code_challenge_method"] = ChallengeMethod == PkceChallengeOptions.Sha256 ? "S256":"plain"; + _isCallGetAuthorizeUrl = true; + return _client.GetAuthorizeUrl(scopes, state, extData); + } + /// + /// 使用授权代码兑换令牌 + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary? extData = null) + { + if (!_isCallGetAuthorizeUrl) throw new InvalidOperationException("Challenge code is invalid"); + var pkce = _ChallengeCode.FromBytesToB64UrlSafe(); + extData ??= []; + extData["code_verifier"] = pkce; + _isCallGetAuthorizeUrl = false; + return await _client.AuthorizeWithCodeAsync(code, token, extData); + } + /// + /// 获取代码对 + /// + /// + /// + /// + /// + public async Task GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary? extData = null) + { + return await _client.GetCodePairAsync(scopes, token, extData); + } + /// + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary? extData = null) + { + return await _client.AuthorizeWithDeviceAsync(data, token, extData); + } + + public async Task AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary? extData = null) + { + return await _client.AuthorizeWithSilentAsync(data, token, extData); + } +} diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilClient.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilClient.cs new file mode 100644 index 000000000..c072581a8 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilClient.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; +using PCL.Core.Minecraft.IdentityModel.OAuth; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.YggdrasilConnect; + +// Steven Qiu 说这东西完全就是 OpenId + 魔改了一部分,所以可以直接复用 OpenId 的逻辑 + +/// +/// +/// +public class YggdrasilClient:IOAuthClient +{ + + private OpenIdClient? _client; + + private YggdrasilOptions _options; + + public YggdrasilClient(YggdrasilOptions options) + { + _options = options; + } + /// + /// 初始化并拉取网络配置 + /// + /// 当无法获取 ClientId 时抛出,调用方应该设置 ClientId 并重新实例化 Client + /// + public async Task InitializeAsync(CancellationToken token) + { + _client = new OpenIdClient(_options); + await _client.InitializeAsync(token,true); + } + /// + /// 获取授权端点地址 + /// + /// + /// + /// + /// + /// 未调用 + public string GetAuthorizeUrl(string[] scopes, string state, Dictionary? extData) + { + if (_client is null) throw new InvalidOperationException(); + return _client.GetAuthorizeUrl(scopes, state, extData); + } + /// + /// 使用授权代码兑换令牌 + /// + /// + /// + /// + /// + /// 未调用 + public async Task AuthorizeWithCodeAsync(string code, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithCodeAsync(code, token, extData); + + } + /// + /// 获取代码对 + /// + /// + /// + /// + /// + /// 未调用 + public async Task GetCodePairAsync(string[] scopes, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.GetCodePairAsync(scopes, token, extData); + + } + /// + /// 发起一次请求验证用户授权状态 + /// + /// + /// + /// + /// + /// 未调用 + public async Task AuthorizeWithDeviceAsync(DeviceCodeData data, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithDeviceAsync(data, token, extData); + + } + /// + /// 刷新登录 + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithSilentAsync(AuthorizeResult data, CancellationToken token, Dictionary? extData = null) + { + if (_client is null) throw new InvalidOperationException(); + return await _client.AuthorizeWithSilentAsync(data, token, extData); + } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilConnectMetaData.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilConnectMetaData.cs new file mode 100644 index 000000000..539915432 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilConnectMetaData.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; +using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.YggdrasilConnect; + +public record YggdrasilConnectMetaData: OpenIdMetadata +{ + [JsonPropertyName("shared_client_id")] + public string? SharedClientId { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilOptions.cs b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilOptions.cs new file mode 100644 index 000000000..90dac453a --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Extensions/YggdrasilConnect/YggdrasilOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using PCL.Core.Minecraft.IdentityModel.Extensions.OpenId; +using PCL.Core.Minecraft.IdentityModel.OAuth; +using PCL.Core.Utils.Exts; + +namespace PCL.Core.Minecraft.IdentityModel.Extensions.YggdrasilConnect; + +public record YggdrasilOptions:OpenIdOptions +{ + private string[] _scopesRequired = ["openid", "Yggdrasil.PlayerProfiles.Select", "Yggdrasil.Server.Join"]; + + // 重写这个鬼方法是因为 Yggdrasil Connect 有要求( + + /// + /// 拉取 Yggdrasil 配置 + /// + /// + /// + public override async Task InitializeAsync(CancellationToken token) + { + using var request = new HttpRequestMessage(HttpMethod.Get, OpenIdDiscoveryAddress); + if (Headers is not null) + foreach (var kvp in Headers) + { + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + } + + using var response = await GetClient.Invoke().SendAsync(request, token); + Meta = JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(token)); + if (Meta is null) throw new InvalidOperationException(); + if (_scopesRequired.Except(Meta.ScopesSupported).Any()) throw new InvalidOperationException(); + } + /// + /// 构建 OAuth 客户端选项 + /// + /// + /// + /// 未调用 + /// + /// + public override async Task BuildOAuthOptionsAsync(CancellationToken token) + { + if (Meta is YggdrasilConnectMetaData meta) + { + var options = await base.BuildOAuthOptionsAsync(token); + if (!options.ClientId.IsNullOrEmpty()) return options; + if (meta is null) throw new InvalidOperationException(); + if (!meta.SharedClientId.IsNullOrEmpty()) + { + options.ClientId = meta.SharedClientId; + } + + throw new ArgumentException(); + } + + throw new InvalidCastException(); + } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/AuthorizeResult.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/AuthorizeResult.cs new file mode 100644 index 000000000..4b46d4508 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/AuthorizeResult.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; +using PCL.Core.Utils.Exts; + +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +public record AuthorizeResult +{ + public bool IsError => !Error.IsNullOrEmpty(); + /// + /// 错误类型 (e.g. invalid_request) + /// + [JsonPropertyName("error")] public string? Error { get; init; } + /// + /// 描述此错误的文本 + /// + [JsonPropertyName("error_description")] public string? ErrorDescription { get; init; } + + // 不用 SecureString,因为这东西依赖 DPAPI,不是最佳实践 + + /// + /// 访问令牌 + /// + [JsonPropertyName("access_token")] public string? AccessToken { get; init; } + /// + /// 刷新令牌 + /// + [JsonPropertyName("refresh_token")] public string? RefreshToken { get; init; } + /// + /// ID Token + /// + [JsonPropertyName("id_token")] public string? IdToken { get; init; } + /// + /// 令牌类型 + /// + [JsonPropertyName("token_type")] public string? TokenType { get; init; } + /// + /// 过期时间 + /// + [JsonPropertyName("expires_in")] public int? ExpiresIn { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/Client.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/Client.cs new file mode 100644 index 000000000..b9a9a83de --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/Client.cs @@ -0,0 +1,144 @@ +using System; +using System.Text; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; + +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +/// +/// OAuth 客户端实现,配合 Polly 食用效果更佳 +/// +/// OAuth 参数 +public sealed class SimpleOAuthClient(OAuthClientOptions options):IOAuthClient +{ + /// + /// 获取授权 Url + /// + /// 访问权限列表 + /// + /// + /// + public string GetAuthorizeUrl(string[] scopes,string state,Dictionary? extData = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(options.Meta.AuthorizeEndpoint); + var sb = new StringBuilder(); + sb.Append(options.Meta.AuthorizeEndpoint); + sb.Append($"?response_type=code&scope={Uri.EscapeDataString(string.Join(" ", scopes))}"); + sb.Append($"&redirect_uri={Uri.EscapeDataString(options.RedirectUri)}"); + sb.Append($"&client_id={options.ClientId}&state={state}"); + if (extData is null) return sb.ToString(); + foreach (var kvp in extData) + sb.Append($"&{kvp.Key}={Uri.EscapeDataString(kvp.Value)}"); + return sb.ToString(); + } + + /// + /// 使用授权代码获取令牌 + /// + /// 授权代码 + /// 附加属性,不应该包含必须参数和预定义字段 (e.g. client_id、grant_type) + /// + /// + public async Task AuthorizeWithCodeAsync( + string code,CancellationToken token,Dictionary? extData = null + ) + { + extData ??= new Dictionary(); + extData["client_id"] = options.ClientId; + extData["grant_type"] = "authorization_code"; + extData["code"] = code; + var client = options.GetClient.Invoke(); + using var content = new FormUrlEncodedContent(extData); + using var request = new HttpRequestMessage(HttpMethod.Post,options.Meta.TokenEndpoint); + request.Content = content; + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key,kvp.Value); + using var response = await client.SendAsync(request,token); + var result = await response.Content.ReadAsStringAsync(token); + return JsonSerializer.Deserialize(result); + } + /// + /// 获取设备代码对 + /// + /// + /// + /// + /// + public async Task GetCodePairAsync + (string[] scopes,CancellationToken token, Dictionary? extData = null) + { + ArgumentException.ThrowIfNullOrEmpty(options.Meta.DeviceEndpoint); + var client = options.GetClient.Invoke(); + extData ??= new Dictionary(); + extData["scope"] = string.Join(" ", scopes); + extData["client_id"] = options.ClientId; + using var request = new HttpRequestMessage(HttpMethod.Post, options.Meta.DeviceEndpoint); + var content = new FormUrlEncodedContent(extData); + request.Content = content; + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key,kvp.Value); + using var response = await client.SendAsync(request,token); + var result = await response.Content.ReadAsStringAsync(token); + return JsonSerializer.Deserialize(result); + } + /// + /// 验证用户授权状态
+ /// 注:此方法不会检查是否过去了 Interval 秒,请自行处理 + ///
+ /// + /// + /// + /// + /// + public async Task AuthorizeWithDeviceAsync + (DeviceCodeData data,CancellationToken token,Dictionary? extData = null) + { + if (data.IsError) throw new OperationCanceledException(data.ErrorDescription); + var client = options.GetClient.Invoke(); + extData ??= new Dictionary(); + extData["client_id"] = options.ClientId; + extData["grant_type"] = "urn:ietf:params:oauth:grant-type:device_code"; + extData["device_code"] = data.DeviceCode!; + using var request = new HttpRequestMessage(HttpMethod.Post,options.Meta.TokenEndpoint); + using var content = new FormUrlEncodedContent(extData); + request.Content = content; + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key,kvp.Value); + using var response = await client.SendAsync(request,token); + var result = await response.Content.ReadAsStringAsync(token); + return JsonSerializer.Deserialize(result); + } + /// + /// 刷新登录 + /// + /// + /// + /// + /// + /// + public async Task AuthorizeWithSilentAsync + (AuthorizeResult data,CancellationToken token,Dictionary? extData = null) + { + var client = options.GetClient.Invoke(); + if (data.IsError) throw new OperationCanceledException(data.ErrorDescription); + extData ??= new Dictionary(); + extData["refresh_token"] = data.RefreshToken!; + extData["grant_type"] = "refresh_token"; + extData["client_id"] = options.ClientId; + using var request = new HttpRequestMessage(HttpMethod.Post,options.Meta.TokenEndpoint); + using var content = new FormUrlEncodedContent(extData); + request.Content = content; + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key,kvp.Value); + using var response = await client.SendAsync(request,token); + var result = await response.Content.ReadAsStringAsync(token); + return JsonSerializer.Deserialize(result); + } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/ClientOptions.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/ClientOptions.cs new file mode 100644 index 000000000..24835ee20 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/ClientOptions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +public record OAuthClientOptions +{ + /// + /// 请求头 + /// + public Dictionary? Headers { get; set; } + /// + /// 端点数据 + /// + public required EndpointMeta Meta { get; set; } + public required Func GetClient { get; set; } + /// + /// 重定向 Uri + /// + public required string RedirectUri { get; set; } + /// + /// 客户端 ID + /// + public required string ClientId { get; set; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/DeviceCodeData.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/DeviceCodeData.cs new file mode 100644 index 000000000..0dd8b2f22 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/DeviceCodeData.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; +using PCL.Core.Utils.Exts; + +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +public record DeviceCodeData +{ + public bool IsError => !Error.IsNullOrEmpty(); + /// + /// 错误类型 + /// + [JsonPropertyName("error")] + public string? Error { get; init; } + /// + /// 错误描述 + /// + [JsonPropertyName("error_description")] + public string? ErrorDescription { get; init; } + /// + /// 用户授权码 + /// + [JsonPropertyName("user_code")] + public string? UserCode { get; init; } + /// + /// 设备授权码 + /// + [JsonPropertyName("device_code")] + public string? DeviceCode { get; init; } + /// + /// 验证 Uri + /// + [JsonPropertyName("verification_uri")] + public string? VerificationUri { get; init; } + /// + /// 验证 Uri (自动填充代码) + /// + [JsonPropertyName("verification_uri_complete")] + public string? VerificationUriComplete { get; init; } + /// + /// 轮询间隔 + /// + [JsonPropertyName("interval")] + public int? Interval { get; init; } + /// + /// 过期时间 + /// + [JsonPropertyName("expires_in")] + public int? ExpiresIn { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/EndpointMeta.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/EndpointMeta.cs new file mode 100644 index 000000000..c26c9ec8a --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/EndpointMeta.cs @@ -0,0 +1,17 @@ +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +public record EndpointMeta +{ + /// + /// 设备授权端点 + /// + public string? DeviceEndpoint { get; set; } + /// + /// 授权端点 + /// + public required string AuthorizeEndpoint { get; set; } + /// + /// 令牌端点 + /// + public required string TokenEndpoint { get; set; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/OAuth/IOAuthClient.cs b/PCL.Core/Minecraft/IdentityModel/OAuth/IOAuthClient.cs new file mode 100644 index 000000000..dd035367a --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/OAuth/IOAuthClient.cs @@ -0,0 +1,14 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace PCL.Core.Minecraft.IdentityModel.OAuth; + +public interface IOAuthClient +{ + public string GetAuthorizeUrl(string[] scopes,string state,Dictionary? extData); + public Task AuthorizeWithCodeAsync(string code,CancellationToken token,Dictionary? extData = null); + public Task GetCodePairAsync(string[] scopes,CancellationToken token, Dictionary? extData = null); + public Task AuthorizeWithDeviceAsync(DeviceCodeData data,CancellationToken token,Dictionary? extData = null); + public Task AuthorizeWithSilentAsync(AuthorizeResult data,CancellationToken token,Dictionary? extData = null); +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Agent.cs b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Agent.cs new file mode 100644 index 000000000..06ed1a007 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Agent.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil; + +/// +/// Yggdrasil Agent +/// +public record Agent +{ + [JsonPropertyName("name")] public string Name { get; init; } = "minecraft"; + [JsonPropertyName("version")] public int Version { get; init; } = 1; +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs new file mode 100644 index 000000000..13c6a0654 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Client.cs @@ -0,0 +1,152 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; + +namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil; + +/// +/// 提供 Yggdrasil 传统认证支持 +/// +/// 认证参数 +public sealed class YggdrasilLegacyClient(YggdrasilLegacyAuthenticateOptions options) +{ + /// + /// 异步向服务器发送一次登录请求 + /// + /// + /// 认证结果 + /// 用户名或密码无效 + public async Task AuthenticateAsync(CancellationToken token) + { + ArgumentException.ThrowIfNullOrEmpty(options.Username); + ArgumentException.ThrowIfNullOrEmpty(options.Password); + + var credential = new YggdrasilCredential + { + User = options.Username, + Password = options.Password, + }; + var address = $"{options.YggdrasilApiLocation}/authserver/authenticate"; + using var request = new HttpRequestMessage(HttpMethod.Post, address); + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + + using var content = + new StringContent(JsonSerializer.Serialize(credential), Encoding.UTF8, "application/json"); + request.Content = content; + using var response = await options.GetClient.Invoke().SendAsync(request,token); + return + JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(token)); + + } + /// + /// 异步向服务器发送一次刷新请求 + /// + /// + /// 如果需要选择角色,请填写此参数 + public async Task RefreshAsync(CancellationToken token,Profile? seleectedProfile) + { + ArgumentException.ThrowIfNullOrEmpty(options.AccessToken); + + var refreshData = new YggdrasilRefresh() + { + AccessToken = options.AccessToken + }; + if (seleectedProfile is not null) refreshData.SelectedProfile = seleectedProfile; + + var address = $"{options.YggdrasilApiLocation}/authserver/refresh"; + + using var request = new HttpRequestMessage(HttpMethod.Post, address); + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + using var content = new StringContent( + JsonSerializer.Serialize(refreshData), Encoding.UTF8, "application/json"); + request.Content = content; + using var response = await options.GetClient.Invoke().SendAsync(request, token); + return JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(token)); + } + /// + /// 异步向服务器发送一次验证请求 + /// + /// + /// + public async Task ValidateAsync(CancellationToken token) + { + ArgumentException.ThrowIfNullOrEmpty(options.AccessToken); + + var validateData = new YggdrasilRefresh() + { + AccessToken = options.AccessToken + }; + var address = $"{options.YggdrasilApiLocation}/authserver/invalidate"; + + using var request = new HttpRequestMessage(HttpMethod.Post, address); + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + + using var content = new StringContent( + JsonSerializer.Serialize(validateData), Encoding.UTF8, "application/json"); + request.Content = content; + using var response = await options.GetClient.Invoke().SendAsync(request, token); + return response.StatusCode == HttpStatusCode.NoContent; + } + + /// + /// 异步向服务器发送一次注销请求 + /// + /// + public async Task InvalidateAsync(CancellationToken token) + { + ArgumentException.ThrowIfNullOrEmpty(options.AccessToken); + + var validateData = new YggdrasilRefresh() + { + AccessToken = options.AccessToken + }; + var address = $"{options.YggdrasilApiLocation}/authserver/invalidate"; + + using var request = new HttpRequestMessage(HttpMethod.Post, address); + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + + using var content = new StringContent( + JsonSerializer.Serialize(validateData), Encoding.UTF8, "application/json"); + request.Content = content; + await options.GetClient.Invoke().SendAsync(request, token); + } + /// + /// 异步向服务器发送登出请求
+ /// 这会立刻注销所有会话,无论当前会话是否属于调用方 + ///
+ /// + /// + public async Task<(bool IsSuccess,string ErrorDescription)> SignOutAsync(CancellationToken token) + { + // 不想写 Model 了,就这样吧(趴 + var signoutData = new JsonObject + { + ["username"] = options.Username, + ["password"] = options.Password + }.ToJsonString(); + var address = $"{options.YggdrasilApiLocation}/authserver/signout"; + + using var request = new HttpRequestMessage(HttpMethod.Post, address); + if(options.Headers is not null) + foreach (var kvp in options.Headers) + _ = request.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value); + using var content = new StringContent(signoutData, Encoding.UTF8, "application/json"); + request.Content = content; + using var response = await options.GetClient.Invoke().SendAsync(request, token); + var data = JsonNode.Parse(await response.Content.ReadAsStringAsync(token)); + return (response.StatusCode == HttpStatusCode.NoContent, data?["errorMessage"]?.ToString()!); + } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Options.cs b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Options.cs new file mode 100644 index 000000000..90581a8c9 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Options.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil; + +public record YggdrasilLegacyAuthenticateOptions +{ + /// + /// API 基地址 (e.g. https://api.example.com/api/yggdrasil) + /// + public required string YggdrasilApiLocation { get; set; } + /// + /// 用户名 + /// + public string? Username { get; set; } + /// + /// 密码 + /// + public string? Password { get; set; } + /// + /// 访问令牌 + /// + public string? AccessToken { get; set; } + public required Func GetClient { get; set; } + /// + /// 请求头 + /// + public Dictionary? Headers { get; set; } +} diff --git a/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Profile.cs b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Profile.cs new file mode 100644 index 000000000..fe580ac96 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/Profile.cs @@ -0,0 +1,88 @@ +using System.Text.Json.Serialization; + +namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil; + + +public record Profile +{ + /// + /// UUID + /// + [JsonPropertyName("id")] public required string Id { get; init; } + /// + /// 档案名称 + /// + [JsonPropertyName("name")] public string? Name { get; init; } + /// + /// 属性信息 + /// + [JsonPropertyName("properties")] public PlayerProperty[]? Properties { get; init; } +} + +public record PlayerProperty +{ + /// + /// 属性名称 + /// + [JsonPropertyName("name")] public required string Name { get; init; } + /// + /// 属性值 + /// + [JsonPropertyName("value")] public required string Value { get; init; } + /// + /// 数字签名 + /// + [JsonPropertyName("signature")] public string? Signature { get; init; } +} + +public record PlayerTextureProperty +{ + /// + /// Unix 时间戳 + /// + [JsonPropertyName("timestamp")] public required long Timestamp { get; init; } + /// + /// 所有者的 UUID + /// + [JsonPropertyName("profileId")] public required string ProfileId { get; init; } + /// + /// 所有者名称 + /// + [JsonPropertyName("profileName")] public required string ProfileName { get; init; } + /// + /// 材质信息 + /// + [JsonPropertyName("textures")] public required PlayerTextures Textures { get; init; } +} + +public record PlayerTextures +{ + /// + /// 皮肤 + /// + [JsonPropertyName("skin")] public required PlayerTexture Skin { get; init; } + /// + /// 披风 + /// + [JsonPropertyName("cape")] public required PlayerTexture Cape { get; init; } +} + +public record PlayerTexture +{ + /// + /// 材质地址 + /// + [JsonPropertyName("Url")] public required string Url { get; init; } + /// + /// 元数据 + /// + [JsonPropertyName("metadata")] public required PlayerTextureMetadata Metadata { get; init; } +} + +public record PlayerTextureMetadata +{ + /// + /// 模型信息 (e.g. Steven -> default, Alex -> Slim) + /// + [JsonPropertyName("model")] public required string Model { get; init; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/IdentityModel/Yggdrasil/YggdrasilCredential.cs b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/YggdrasilCredential.cs new file mode 100644 index 000000000..415cab4a1 --- /dev/null +++ b/PCL.Core/Minecraft/IdentityModel/Yggdrasil/YggdrasilCredential.cs @@ -0,0 +1,50 @@ +using System.Text.Json.Serialization; +using PCL.Core.Link.Scaffolding.Client.Models; + +namespace PCL.Core.Minecraft.IdentityModel.Yggdrasil; + +public record YggdrasilCredential +{ + [JsonPropertyName("username")] public required string User { get; init; } + [JsonPropertyName("password")] public required string Password { get; init; } + [JsonPropertyName("agent")] public Agent Agent = new(); + [JsonPropertyName("requestUser")] public bool RequestUser { get; set; } +} + +public record YggdrasilAuthenticateResult +{ + /// + /// 错误类型 + /// + [JsonPropertyName("error")] public string? Error { get; init; } + /// + /// 错误消息 + /// + [JsonPropertyName("errorMessage")] public string? ErrorMessage { get; init; } + /// + /// 访问令牌 + /// + [JsonPropertyName("accessToken")] public string? AccessToken { get; init; } + /// + /// 客户端令牌,基本没用 + /// + [JsonPropertyName("clientToken")] public string? ClientToken { get; init; } + /// + /// 选择的档案 + /// + [JsonPropertyName("selectedProfile")] public Profile? SelectedProfile { get; init; } + /// + /// 可用档案 + /// + [JsonPropertyName("availableProfiles")] public required Profile[]? AvailableProfiles { get; init; } + /// + /// 用户信息 + /// + [JsonPropertyName("user")] public Profile? User; +} + +public record YggdrasilRefresh +{ + [JsonPropertyName("accessToken")] public required string AccessToken { get; set; } + [JsonPropertyName("selectedProfile")] public Profile? SelectedProfile { get; set; } +} \ No newline at end of file diff --git a/PCL.Core/Minecraft/Java/JavaBrandType.cs b/PCL.Core/Minecraft/Java/JavaBrandType.cs index 6be510641..251d9c124 100644 --- a/PCL.Core/Minecraft/Java/JavaBrandType.cs +++ b/PCL.Core/Minecraft/Java/JavaBrandType.cs @@ -3,9 +3,9 @@ public enum JavaBrandType { EclipseTemurin, - Bellsoft, - AzulZulu, - AmazonCorretto, + Liberica, + Zulu, + Corretto, Microsoft, IBMSemeru, Oracle, diff --git a/PCL.Core/Minecraft/Java/Parser/PeHeaderParser.cs b/PCL.Core/Minecraft/Java/Parser/PeHeaderParser.cs index 2257284bc..0b08a9f5f 100644 --- a/PCL.Core/Minecraft/Java/Parser/PeHeaderParser.cs +++ b/PCL.Core/Minecraft/Java/Parser/PeHeaderParser.cs @@ -13,10 +13,10 @@ public class PeHeaderParser : IJavaParser { ["Eclipse"] = JavaBrandType.EclipseTemurin, ["Temurin"] = JavaBrandType.EclipseTemurin, - ["Bellsoft"] = JavaBrandType.Bellsoft, + ["Bellsoft"] = JavaBrandType.Liberica, ["Microsoft"] = JavaBrandType.Microsoft, - ["Amazon"] = JavaBrandType.AmazonCorretto, - ["Azul"] = JavaBrandType.AzulZulu, + ["Amazon"] = JavaBrandType.Corretto, + ["Azul"] = JavaBrandType.Zulu, ["IBM"] = JavaBrandType.IBMSemeru, ["Oracle"] = JavaBrandType.Oracle, ["Tencent"] = JavaBrandType.TencentKona, diff --git a/PCL.Core/PCL.Core.csproj b/PCL.Core/PCL.Core.csproj index dc7ef4424..b8afc18c3 100644 --- a/PCL.Core/PCL.Core.csproj +++ b/PCL.Core/PCL.Core.csproj @@ -1,92 +1,97 @@ - - - - PCL.Core - PCL.Core - PCL Community 为 PCL 开发的启动器核心库 - PCL Community - PCL.Core - Copyright © PCL Community - 1.0.0.0 - 1.0.0.0 - - - Debug - AnyCPU - Debug;CI;Release;Beta - AnyCPU;x64;ARM64 - {A0C2209D-64FB-4C11-9459-8E86304B6F94} - PCL.Core - net8.0-windows - true - true - true - 14.0 - enable - prompt - 4 - bin\$(Configuration)-$(Platform)\ - $(Platform) - $(NoWarn);CS9113 - - - - true - full - false - DEBUG;TRACE - CI;TRACE - - - none - true - RELEASE;PUBLISH - BETA;PUBLISH - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - false - false - false - false - false - false - - - - - - - - - - - - + + + + PCL.Core + PCL.Core + PCL Community 为 PCL 开发的启动器核心库 + PCL Community + PCL.Core + Copyright © PCL Community + 1.0.0.0 + 1.0.0.0 + + + Debug + AnyCPU + Debug;CI;Release;Beta + AnyCPU;x64;ARM64 + {A0C2209D-64FB-4C11-9459-8E86304B6F94} + PCL.Core + net8.0-windows + true + true + true + 14.0 + enable + prompt + 4 + bin\$(Configuration)-$(Platform)\ + $(Platform) + $(NoWarn);CS9113 + + + + true + full + false + DEBUG;TRACE + CI;TRACE + + + none + true + RELEASE;PUBLISH + BETA;PUBLISH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + false + false + false + false + false + + + + + + + + + + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2.slnx b/Plain Craft Launcher 2.slnx index 815a16a64..cbe70b35c 100644 --- a/Plain Craft Launcher 2.slnx +++ b/Plain Craft Launcher 2.slnx @@ -12,6 +12,6 @@ - - - \ No newline at end of file + + + diff --git a/Plain Craft Launcher 2/Application.xaml b/Plain Craft Launcher 2/Application.xaml index 10b720815..54bfb546a 100644 --- a/Plain Craft Launcher 2/Application.xaml +++ b/Plain Craft Launcher 2/Application.xaml @@ -1,12 +1,12 @@ - + @@ -15,14 +15,14 @@ 0.0 0.7 Gaussian - + - - - - - - + + + + + + Resources/#PCL English, Microsoft YaHei UI @@ -99,43 +99,43 @@ - + - + - - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Application.xaml.cs b/Plain Craft Launcher 2/Application.xaml.cs new file mode 100644 index 000000000..b7d0dec51 --- /dev/null +++ b/Plain Craft Launcher 2/Application.xaml.cs @@ -0,0 +1,290 @@ +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.App.IoC; +using PCL.Core.Logging; +using PCL.Core.Utils; +using PCL.Core.Utils.OS; + +namespace PCL; + +public partial class Application +{ + public static readonly List ShowingTooltips = new(); + + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + ''' + ''' 用于开始程序时的一些测试。 + ''' + Private Sub Test() + Try + ModDevelop.Start() + Catch ex As Exception + Log(ex, "开发者模式测试出错", LogLevel.Msgbox) + End Try + End Sub + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + public Application() + { + // 注册生命周期事件 + Lifecycle.When(LifecycleState.Loaded, Application_Startup); + SessionEnding += Application_SessionEnding; + } + + // 开始 + private void Application_Startup() // (sender As Object, e As StartupEventArgs) Handles Me.Startup + { + try + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + // 创建自定义跟踪监听器,用于检测是否存在 Binding 失败 + PresentationTraceSources.DataBindingSource.Listeners.Add(new BindingErrorTraceListener()); + PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Error; + ModSecret.SecretOnApplicationStart(); + // 检查参数调用 + var args = Basics.CommandLineArguments; + if (args.Length > 0) + { + if (args[0] == "--gpu") + { + // 调整显卡设置 + try + { + ModMain.SetGPUPreference(args[1].Trim('"')); + Environment.Exit((int)ModBase.ProcessReturnValues.TaskDone); + } + catch (Exception ex) + { + Environment.Exit((int)ModBase.ProcessReturnValues.Fail); + } + } + else if (args[0].StartsWithF("--memory")) + { + // 内存优化 + var Ram = KernelInterop.GetAvailablePhysicalMemoryBytes(); + try + { + PageToolsTest.MemoryOptimizeInternal(false); + } + catch (Exception ex) + { + Interaction.MsgBox(ex.Message, MsgBoxStyle.Critical, "内存优化失败"); + Environment.Exit(-1); + } + + if (KernelInterop.GetAvailablePhysicalMemoryBytes() < Ram) // 避免 ULong 相减出现负数 + Environment.Exit(0); + else + Environment.Exit((int)((KernelInterop.GetAvailablePhysicalMemoryBytes() - Ram) / + 1024)); // 返回清理的内存量(K) + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + '制作更新包 + ElseIf args(0) = "--edit1" Then + ExeEdit(args(1), True) + Environment.Exit(ProcessReturnValues.TaskDone) + ElseIf args(0) = "--edit2" Then + ExeEdit(args(1), False) + Environment.Exit(ProcessReturnValues.TaskDone) + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + } + } + + // 初始化文件结构 + Directory.CreateDirectory(ModBase.ExePath + @"PCL\Pictures"); + Directory.CreateDirectory(ModBase.ExePath + @"PCL\Musics"); + Directory.CreateDirectory(ModBase.PathTemp + "Cache"); + Directory.CreateDirectory(ModBase.PathTemp + "Download"); + Directory.CreateDirectory(ModBase.PathAppdata); + /* TODO ERROR: Skipped IfDirectiveTrivia + #If False Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + '检测单例 + Dim ShouldWaitForExit As Boolean = args.Length > 0 AndAlso args(0) = "--wait" '要求等待已有的 PCL 退出 + Dim WaitRetryCount As Integer = 0 + WaitRetry: + Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher Community Edition ") + If WindowHwnd = IntPtr.Zero Then FindWindow(Nothing, "Plain Craft Launcher 2 Community Edition ") + If WindowHwnd <> IntPtr.Zero Then + If ShouldWaitForExit AndAlso WaitRetryCount < 20 Then '至多等待 10 秒 + WaitRetryCount += 1 + Thread.Sleep(500) + GoTo WaitRetry + End If + '将已有的 PCL 窗口拖出来 + ShowWindowToTop(WindowHwnd) + '播放提示音并退出 + Beep() + Environment.[Exit](ProcessReturnValues.Cancel) + End If + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ // 设置 ToolTipService 默认值 + ToolTipService.InitialShowDelayProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(300)); + ToolTipService.BetweenShowDelayProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(400)); + ToolTipService.ShowDurationProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(9999999)); + ToolTipService.PlacementProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(PlacementMode.Bottom)); + ToolTipService.HorizontalOffsetProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(8.0d)); + ToolTipService.VerticalOffsetProperty.OverrideMetadata(typeof(DependencyObject), + new FrameworkPropertyMetadata(4.0d)); + // 设置初始窗口 + if (Conversions.ToBoolean(Config.Preference.ShowStartupLogo)) + { + ModMain.FrmStart = new SplashScreen(@"Images\icon.ico"); + ModMain.FrmStart.Show(false, true); + } + + // 检测异常环境 + var problemList = new List(); + var currentOSVersion = KernelInterop.GetCurrentOSVersion(); + if (currentOSVersion.Build < 17763) + problemList.Add("- Windows 版本不满足推荐要求,推荐至少 Windows 10 1809,建议考虑升级 Windows 系统"); + if (ModBase.Is32BitSystem) + problemList.Add("- 当前系统为 32 位,不受 PCL 和新版 Minecraft 支持,非常建议重装为 64 位系统后再进行游戏"); + if (ModBase.ExePath.Contains(Path.GetTempPath()) || ModBase.ExePath.Contains(@"AppData\Local\Temp\")) + problemList.Add("- PCL 正在临时目录运行,请将 PCL 从压缩包中解压之后再使用,否则可能导致游戏存档或设置丢失"); + if (ModBase.ExePath.ContainsF("wechat_files", true) || ModBase.ExePath.ContainsF("WeChat Files", true) || + ModBase.ExePath.ContainsF("Tencent Files", true)) + problemList.Add("- PCL 正在 QQ、微信、TIM 等社交软件的下载目录运行,请考虑移动到其他位置,否则可能导致游戏存档或设置丢失"); + if (problemList.Count != 0) + ModMain.MyMsgBox( + "PCL CE 在启动时检测到环境问题:" + "\r\n" + "\r\n" + problemList.Join("\r\n") + + "\r\n" + "\r\n" + "不解决这些问题可能会导致部分功能无法正常工作……", "环境警告", "我知道了", IsWarn: true); + // 设置初始化 + ModBase.Setup.Load("SystemDebugMode"); + ModBase.Setup.Load("SystemDebugAnim"); + ModBase.Setup.Load("SystemHttpProxy"); + ModBase.Setup.Load("SystemHttpProxyCustomUsername"); + ModBase.Setup.Load("SystemHttpProxyType"); + ModBase.Setup.Load("ToolDownloadThread"); + ModBase.Setup.Load("ToolDownloadSpeed"); + ModBase.Setup.Load("UiFont"); + var updateBranchCfg = Config.Update.UpdateChannelConfig; + if (updateBranchCfg.IsDefault()) + updateBranchCfg.SetValue(ModBase.VersionBaseName.Contains("beta") + ? Core.App.UpdateChannel.Beta + : Core.App.UpdateChannel.Release); + // 删除旧日志 + for (var i = 1; i <= 5; i++) + { + var oldLogFile = $@"{ModBase.ExePath}PCL\Log-CE{i}.log"; + if (File.Exists(oldLogFile)) + File.Delete(oldLogFile); + } + + // 计时 + ModBase.Log("[Start] 第一阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick) + " ms"); + ModBase.ApplicationStartTick = TimeUtils.GetTimeTick(); + // 执行测试 + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + Test() + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + ModAnimation.AniControlEnabled += 1; + } + catch (Exception ex) + { + var FilePath = ModBase.ExePathWithName; + + Interaction.MsgBox( + ex + "\r\n" + "PCL 所在路径:" + (string.IsNullOrEmpty(FilePath) ? "获取失败" : FilePath), + MsgBoxStyle.Critical, "PCL 初始化错误"); + FormMain.EndProgramForce(ModBase.ProcessReturnValues.Exception); + } + } + + // 结束 + private void Application_SessionEnding(object sender, SessionEndingCancelEventArgs e) + { + ModMain.FrmMain.EndProgram(false); + } + +// Error handling for unhandled exceptions + private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + try + { + e.Handled = true; + if (ModBase.IsProgramEnded) return; + + ModBase.FeedbackInfo(); + + var detail = e.Exception.ToString(); + + // Automatic error analysis for environment issues + if (detail.Contains("System.Windows.Threading.Dispatcher.Invoke") || + detail.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") || + detail.Contains("未能加载文件或程序集")) + { + ModBase.OpenWebsite("https://get.dot.net/8"); + LogWrapper.Error(e.Exception, + "Your .NET Desktop Runtime is outdated or corrupted. Please reinstall .NET 8!"); + } + else + { + LogWrapper.Error(e.Exception, "An unexpected error occurred"); + } + } + catch + { + // Equivalent to On Error Resume Next for safety in the global handler + } + } + + // Win32 API declaration for DLL directory configuration + [DllImport("kernel32", EntryPoint = "SetDllDirectoryA", CharSet = CharSet.Ansi)] + private static extern bool SetDllDirectory(string lpPathName); + // 切换窗口 + + // 控件模板事件 + private void MyIconButton_Click(object sender, EventArgs e) + { + } + + private void TooltipLoaded(object sender, EventArgs e) + { + ShowingTooltips.Add((Border)sender); + } + + private void TooltipUnloaded(object sender, RoutedEventArgs e) + { + ShowingTooltips.Remove((Border)sender); + } + + // 自定义监听器类 + public class BindingErrorTraceListener : TraceListener + { + public override void Write(string message) + { + ModBase.Log($"警告,检测到 Binding 失败:{message}"); + } + + public override void WriteLine(string message) + { + ModBase.Log($"警告,检测到 Binding 失败:{message}"); + } + } +} diff --git a/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs b/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs new file mode 100644 index 000000000..8934977cd --- /dev/null +++ b/Plain Craft Launcher 2/Controls/AnimatedBackgroundGrid.cs @@ -0,0 +1,79 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class AnimatedBackgroundGrid : Grid +{ + public static readonly DependencyProperty BackgroundBrushProperty = DependencyProperty.Register("BackgroundBrush", + typeof(SolidColorBrush), typeof(AnimatedBackgroundGrid), + new PropertyMetadata(new SolidColorBrush(Color.FromArgb(0, 0, 0, 0)), _BackgroundBrushChanged)); + + private readonly DependencyProperty _animatableBrushProperty; + + public readonly int Uuid = ModBase.GetUuid(); + + private bool _isAnimating; + + public AnimatedBackgroundGrid(DependencyProperty brushDp) + { + _animatableBrushProperty = brushDp; + Loaded += (_, __) => Init(); + } + + public AnimatedBackgroundGrid() : this(BackgroundProperty) + { + } + + protected virtual FrameworkElement AnimatableElement => this; + + protected virtual SolidColorBrush AnimatableBrush + { + get => (SolidColorBrush)Background; + set => Background = value; + } + + protected bool IsAnimating + { + get => _isAnimating; + private set => _isAnimating = Conversions.ToBoolean(value); + } + + public SolidColorBrush BackgroundBrush + { + get => (SolidColorBrush)GetValue(BackgroundBrushProperty); + set => SetValue(BackgroundBrushProperty, value); + } + + private static void _BackgroundBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var grid = (AnimatedBackgroundGrid)d; + var brush = (SolidColorBrush)e.NewValue; + if (!(grid.IsLoaded & grid.IsVisible)) + { + grid.AnimatableBrush = brush; + return; + } + + grid.Dispatcher.BeginInvoke(new Func(async () => + { + grid.IsAnimating = true; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(grid.AnimatableElement, grid._animatableBrushProperty, + new ModBase.MyColor(brush) - grid.AnimatableBrush, 300) + }, "MyCard Theme " + grid.Uuid); + await Task.Delay(300); + grid.AnimatableBrush = brush; + grid.IsAnimating = false; + })); + } + + private void Init() + { + AnimatableBrush = BackgroundBrush; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs b/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs new file mode 100644 index 000000000..49d5e3c8e --- /dev/null +++ b/Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.cs @@ -0,0 +1,235 @@ +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Clipboard = System.Windows.Forms.Clipboard; + +// Author: uye (owner of the MaaAssistantArknights team) +// Original Source: MaaAssistantArknights project - https://github.com/MaaAssistantArknights/MaaAssistantArknights +// License: Apache License 2.0 (this file only) +// +// This file is based on work originally developed in the MaaAssistantArknights project, +// which is licensed under the GNU AGPL v3.0 only. +// +// As the original author of this code, I am re-licensing this specific file under +// the Apache License 2.0 for use in PCL2-CE. +// +// Description: +// Implements a WPF clipboard fix to handle OpenClipboard failures in TextBox, +// RichTextBox, and DataGrid, typically caused by focus issues or external hooks. +// +// Date: 2025-07-03 + +namespace PCL.Controls.Behaviors; + +public sealed class ClipboardInterceptor +{ + public static readonly DependencyProperty EnableSafeClipboardProperty = + DependencyProperty.RegisterAttached("EnableSafeClipboard", typeof(bool), typeof(ClipboardInterceptor), + new PropertyMetadata(false, OnEnableSafeClipboardChanged)); + + private ClipboardInterceptor() + { + } + + public static void SetEnableSafeClipboard(DependencyObject element, bool value) + { + element.SetValue(EnableSafeClipboardProperty, value); + } + + public static bool GetEnableSafeClipboard(DependencyObject element) + { + return Conversions.ToBoolean(element.GetValue(EnableSafeClipboardProperty)); + } + + private static void OnEnableSafeClipboardChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBox && Conversions.ToBoolean(e.NewValue)) + AddCommandBindingsToTextBox((TextBox)d); + else if (d is RichTextBox && Conversions.ToBoolean(e.NewValue)) + AddCommandBindingsToRichTextBox((RichTextBox)d); + else if (d is DataGrid && Conversions.ToBoolean(e.NewValue)) AddCommandBindingsToDataGrid((DataGrid)d); + } + + private static void AddCommandBindingsToTextBox(TextBox tb) + { + tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyTextBox)); + tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutTextBox)); + tb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteTextBox)); + } + + private static void AddCommandBindingsToRichTextBox(RichTextBox rtb) + { + rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyRichTextBox)); + rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, OnCutRichTextBox)); + rtb.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPasteRichTextBox)); + } + + private static void AddCommandBindingsToDataGrid(DataGrid dg) + { + dg.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopyDataGrid)); + } + + private static void OnCopyTextBox(object sender, ExecutedRoutedEventArgs e) + { + var tb = sender as TextBox; + if (tb is null || tb.SelectionLength <= 0) + return; + + try + { + Clipboard.Clear(); + Clipboard.SetDataObject(tb.SelectedText, true); + } + catch + { + } + + e.Handled = true; + } + + private static void OnCutTextBox(object sender, ExecutedRoutedEventArgs e) + { + var tb = sender as TextBox; + if (tb is null || tb.SelectionLength <= 0) + return; + + try + { + Clipboard.Clear(); + Clipboard.SetDataObject(tb.SelectedText, true); + } + catch + { + } + + tb.SelectedText = string.Empty; + e.Handled = true; + } + + private static void OnPasteTextBox(object sender, ExecutedRoutedEventArgs e) + { + var tb = sender as TextBox; + if (tb is null) + return; + + if (Clipboard.ContainsText()) + { + var pasteText = Clipboard.GetText(); + + if (!tb.AcceptsReturn) + pasteText = pasteText.Replace("\r\n", " ").Replace("\r", " ") + .Replace("\n", " "); + + var start = tb.SelectionStart; + + tb.SelectedText = pasteText; + tb.CaretIndex = start + pasteText.Length; + tb.SelectionLength = 0; + } + + e.Handled = true; + } + + private static void OnCopyRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + var rtb = sender as RichTextBox; + if (rtb is null) + return; + + var textRange = new TextRange(rtb.Selection.Start, rtb.Selection.End); + if (string.IsNullOrEmpty(textRange.Text)) + return; + + try + { + Clipboard.Clear(); + Clipboard.SetDataObject(textRange.Text, true); + } + catch + { + } + + e.Handled = true; + } + + private static void OnCutRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + var rtb = sender as RichTextBox; + if (rtb is null) + return; + + var selection = new TextRange(rtb.Selection.Start, rtb.Selection.End); + if (string.IsNullOrEmpty(selection.Text)) + return; + + try + { + Clipboard.Clear(); + Clipboard.SetDataObject(selection.Text, true); + } + catch + { + } + + selection.Text = string.Empty; + e.Handled = true; + } + + private static void OnPasteRichTextBox(object sender, ExecutedRoutedEventArgs e) + { + var rtb = sender as RichTextBox; + if (rtb is null) + return; + + if (!Clipboard.ContainsText()) + return; + + var pasteText = Clipboard.GetText(); + var selection = rtb.Selection; + + selection.Text = pasteText; + + var caretPos = selection.End; + rtb.CaretPosition = caretPos; + rtb.Selection.Select(caretPos, caretPos); + + e.Handled = true; + } + + private static void OnCopyDataGrid(object sender, ExecutedRoutedEventArgs e) + { + var dg = sender as DataGrid; + if (dg is null || dg.SelectedCells is null || dg.SelectedCells.Count == 0) + return; + + var sb = new StringBuilder(); + var rowGroups = dg.SelectedCells.GroupBy(c => c.Item); + + foreach (var row in rowGroups) + { + var rowText = string.Join(Constants.vbTab, row.Select(cell => + { + var tb = cell.Column.GetCellContent(cell.Item) as TextBlock; + return tb is not null ? tb.Text : ""; + })); + sb.AppendLine(rowText); + } + + var sbStr = sb.ToString().TrimEnd(ControlChars.Cr, ControlChars.Lf); + + try + { + Clipboard.Clear(); + Clipboard.SetDataObject(sbStr, true); + } + catch + { + } + + e.Handled = true; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs b/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs new file mode 100644 index 000000000..1af433fe3 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/Behaviors/LazyLoadBehavior.cs @@ -0,0 +1,75 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.Xaml.Behaviors; + +namespace PCL; + +internal static class LazyLoader +{ + public static void EnableLazyLoad(this FrameworkElement element, Action action) + { + var behavior = new LazyLoadBehavior(); + behavior.Action = action; + Interaction.GetBehaviors(element).Add(behavior); + } +} + +public class LazyLoadBehavior : Behavior +{ + public static readonly DependencyProperty ActionProperty = DependencyProperty.Register(nameof(Action), + typeof(Action), typeof(LazyLoadBehavior), new PropertyMetadata(null)); + + public Action Action + { + get => (Action)GetValue(ActionProperty); + set => SetValue(ActionProperty, value); + } + + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.LayoutUpdated += OnLayoutUpdated; + } + + protected override void OnDetaching() + { + AssociatedObject.LayoutUpdated -= OnLayoutUpdated; + base.OnDetaching(); + } + + private void OnLayoutUpdated(object sender, EventArgs e) + { + if (AssociatedObject.RenderSize.Width < double.Epsilon) + return; + if (!AssociatedObject.IsVisible) + return; + + var scrollViewer = FindParentScrollViewer(AssociatedObject); + if (scrollViewer is null) + return; + + var elementBounds = AssociatedObject.TransformToAncestor(scrollViewer) + .TransformBounds(new Rect(new Point(0d, 0d), AssociatedObject.RenderSize)); + var viewport = new Rect(0d, 0d, scrollViewer.ViewportWidth, scrollViewer.ViewportHeight); + + if (viewport.IntersectsWith(elementBounds)) + { + Action?.Invoke(); + // 仅执行一次 + AssociatedObject.LayoutUpdated -= OnLayoutUpdated; + } + } + + private ScrollViewer FindParentScrollViewer(DependencyObject d) + { + while (d is not null) + { + if (d is ScrollViewer) + return (ScrollViewer)d; + d = VisualTreeHelper.GetParent(d); + } + + return null; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/FontSelector.xaml b/Plain Craft Launcher 2/Controls/FontSelector.xaml index f66cdab6b..41d96da2c 100644 --- a/Plain Craft Launcher 2/Controls/FontSelector.xaml +++ b/Plain Craft Launcher 2/Controls/FontSelector.xaml @@ -1,24 +1,24 @@ - - + + FontFamily="{Binding Font}" /> - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs b/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs new file mode 100644 index 000000000..f8b3b277c --- /dev/null +++ b/Plain Craft Launcher 2/Controls/FontSelector.xaml.cs @@ -0,0 +1,169 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.Logging; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public partial class FontSelector +{ + public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e); + + public new static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", + typeof(string), typeof(FontSelector), new PropertyMetadata(null, OnTooltipChanged)); + + private bool _isInitializing; + private string _pendingFontTag; + + public FontSelector() + { + InitializeComponent(); + Loaded += FontSelector_Loaded; + ComboFont.SelectionChanged += ComboFont_SelectionChanged; + } + + + public new string Tooltip + { + get => Conversions.ToString(GetValue(TooltipProperty)); + set => SetValue(TooltipProperty, value); + } + + public ObservableCollection CustomFontCollection { get; } = new(); + + public string SelectedFontTag + { + get + { + if (ComboFont.SelectedItem is null) + return ""; + var selectedFont = ComboFont.SelectedItem as CustomFontProperties; + if (selectedFont is null) + return ""; + return selectedFont.Tag; + } + set + { + // 如果字体还在加载中,延迟设置 + if (CustomFontCollection.Count == 0 || + (CustomFontCollection.Count == 1 && CustomFontCollection[0].Name == "加载中...")) + { + _pendingFontTag = value; + return; + } + + _isInitializing = true; + + var targetSelection = CustomFontCollection.FirstOrDefault(i => (i.Tag ?? "") == (value ?? "")); + if (targetSelection is null) + ComboFont.SelectedIndex = 0; + else + ComboFont.SelectedItem = targetSelection; + + _isInitializing = false; + } + } + + public int SelectedIndex + { + get => ComboFont.SelectedIndex; + set => ComboFont.SelectedIndex = value; + } + + public new bool IsEnabled + { + get => ComboFont.IsEnabled; + set => ComboFont.IsEnabled = value; + } + + private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as FontSelector; + if (control is not null) control.ComboFont.ToolTip = e.NewValue; + } + + public event SelectionChangedEventHandler? SelectionChanged; + + private void FontSelector_Loaded(object sender, RoutedEventArgs e) + { + if (CustomFontCollection.Count == 0) LoadFonts(); + } + + private void LoadFonts() + { + Dispatcher.BeginInvoke(async () => + { + ComboFont.IsEnabled = false; + _isInitializing = true; + CustomFontCollection.Add(new CustomFontProperties { Name = "加载中..." }); + ComboFont.SelectedIndex = 0; + + var availableFonts = new List<(string Name, FontFamily Font)>(); + + await Task.Run(() => + { + foreach (var font in Fonts.SystemFontFamilies) + try + { + if (font.Source.StartsWith("Global ")) continue; + + foreach (var typeface in font.GetTypefaces()) + { + if (!typeface.TryGetGlyphTypeface(out var glyph)) + throw new NullReferenceException( + $"字形 {typeface.FaceNames.GetForCurrentUiCulture("(unknown)")} 无法加载"); + + _ = new GlyphTypeface(glyph.FontUri); + } + + availableFonts.Add((font.FamilyNames.GetForCurrentUiCulture(), font)); + } + catch (Exception ex) + { + LogWrapper.Error(ex, $"发现了一个无法加载的异常的字体:{font.Source}"); + } + + availableFonts.Sort((l, r) => string.Compare(l.Name, r.Name, StringComparison.Ordinal)); + }); + + CustomFontCollection.Clear(); + CustomFontCollection.Add(new CustomFontProperties + { + Name = "默认", + Font = new FontFamily(new Uri("pack://application:,,,/"), + "./Resources/#PCL English, Segoe UI, Microsoft YaHei UI"), + Tag = "" + }); + + foreach (var font in availableFonts) + CustomFontCollection.Add(new CustomFontProperties + { Name = font.Name, Font = font.Font, Tag = font.Font.Source }); + + ComboFont.IsEnabled = true; + + if (_pendingFontTag != null) + { + var pendingTag = _pendingFontTag; + _pendingFontTag = null; + SelectedFontTag = pendingTag; + } + + _isInitializing = false; + }); + } + + private void ComboFont_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!_isInitializing) SelectionChanged?.Invoke(sender, e); + } + + public class CustomFontProperties + { + public string Name { get; set; } + public FontFamily Font { get; set; } + public string Tag { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/IMyRadio.cs b/Plain Craft Launcher 2/Controls/IMyRadio.cs new file mode 100644 index 000000000..063906dfe --- /dev/null +++ b/Plain Craft Launcher 2/Controls/IMyRadio.cs @@ -0,0 +1,11 @@ +namespace PCL; + +public interface IMyRadio +{ + delegate void ChangedEventHandler(object sender, ModBase.RouteEventArgs e); + + delegate void CheckEventHandler(object sender, ModBase.RouteEventArgs e); + + event CheckEventHandler Check; + event ChangedEventHandler Changed; +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml index aefaa6fe8..b9cf8d22c 100644 --- a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml +++ b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml @@ -1,19 +1,20 @@ - - - - + + + - - + + - - + IsHitTestVisible="False" Width="300" Height="Auto" VerticalAlignment="Center" /> + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs new file mode 100644 index 000000000..854ff6f20 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.cs @@ -0,0 +1,100 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.Link.McPing; +using PCL.Core.Link.McPing.Model; +using PCL.Core.Minecraft; +using PCL.Core.UI; + +namespace PCL; + +public partial class MinecraftServer : Grid +{ + private const string FallbackImageUri = + "pack://application:,,,/Plain Craft Launcher 2;component/Images/Icons/DefaultServer.png"; + + private static readonly DependencyProperty AddressProperty = DependencyProperty.Register(nameof(Address), + typeof(string), typeof(MinecraftServer), new PropertyMetadata(string.Empty, OnAddressChanged)); + + public MinecraftServer() + { + InitializeComponent(); + } + + public string Address + { + get => Conversions.ToString(GetValue(AddressProperty)); + set => SetValue(AddressProperty, value); + } + + private static void OnAddressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var server = (MinecraftServer)d; + d.Dispatcher.BeginInvoke(new Func(() => server.UpdateServerInfoAsync(e.NewValue?.ToString()))); + } + + public async Task UpdateServerInfoAsync(string address) + { + if (address is null) + return; + address = address.Replace(":", ":"); + // 预先重置UI状态 + LabServerDesc.Foreground = Brushes.White; + LabServerDesc.Text = "查询中..."; + LabServerPlayer.Text = "-/-"; + LabServerPlayer.ToolTip = null; + ImageLoaderHelper.SetFallbackImage(ImgServerLogo, FallbackImageUri); + + try + { + // 获取可达地址(DNS解析) + var addr = await ServerAddressResolver.GetReachableAddressAsync(address); + + // Ping服务器 + using (var query = McPingServiceFactory.CreateService(addr.Ip, addr.Port)) + { + var ret = await query.PingAsync(); + + if (ret is null) throw new Exception("未返回服务器信息"); + + // 处理服务器图标 + await ImageLoaderHelper.SetServerLogoAsync(ret.Favicon, ImgServerLogo); + + // 更新UI + UpdateServerStatus(ret); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "[MinecraftServer] 信息查询失败"); + LabServerDesc.Text = $"无法连接: {ex.Message}"; + LabServerDesc.Foreground = Brushes.Red; + ImageLoaderHelper.SetFallbackImage(ImgServerLogo, FallbackImageUri); + } + } + + private void UpdateServerStatus(McPingResult ret) + { + // 延迟颜色判断 + var latencyColor = ret.Latency < 150 ? "a" : ret.Latency < 400 ? "6" : "c"; + + // 更新描述 + LabServerDesc.Text = "Minecraft 服务器"; + MotdRenderer.RenderMotd(ret.Description, false, 2, 14); + MotdRenderer.RenderCanvas(); + + // 更新玩家信息 + var playerText = $"{ret.Players.Online}/{ret.Players.Max}{"\r\n"}§{latencyColor}{ret.Latency}ms"; + ModStyle.MinecraftFormatter.SetColorfulTextLab(playerText, LabServerPlayer, false); + + // 玩家列表提示 + if (ret.Players.Samples.Any()) + { + LabServerPlayer.ToolTip = string.Join("\r\n", ret.Players.Samples.Select(x => x.Name)); + ToolTipService.SetPlacement(LabServerPlayer, PlacementMode.Mouse); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.vb b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.vb index 30ec1e132..9c0c523bb 100644 --- a/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MinecraftServer.xaml.vb @@ -1,5 +1,5 @@ -Imports System.Threading.Tasks -Imports PCL.Core.Link +Imports PCL.Core.Link.McPing +Imports PCL.Core.Link.McPing.Model Imports PCL.Core.Minecraft Imports PCL.Core.UI @@ -44,7 +44,7 @@ Class MinecraftServer Dim addr = Await ServerAddressResolver.GetReachableAddressAsync(address) ' Ping服务器 - Using query = New McPing(addr.Ip, addr.Port) + Using query = McPingServiceFactory.CreateService(addr.Ip, addr.Port) Dim ret = Await query.PingAsync() If ret Is Nothing Then @@ -71,12 +71,12 @@ Class MinecraftServer ' 更新描述 LabServerDesc.Text = "Minecraft 服务器" - MotdRenderer.RenderMotd(ret.Description, false, 2, 14) + MotdRenderer.RenderMotd(ret.Description, False, 2, 14) MotdRenderer.RenderCanvas() ' 更新玩家信息 Dim playerText = $"{ret.Players.Online}/{ret.Players.Max}{vbCrLf}§{latencyColor}{ret.Latency}ms" - MinecraftFormatter.SetColorfulTextLab(playerText, LabServerPlayer, false) + MinecraftFormatter.SetColorfulTextLab(playerText, LabServerPlayer, False) ' 玩家列表提示 If ret.Players.Samples?.Any() Then diff --git a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml index e187330e8..017037c46 100644 --- a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml +++ b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml @@ -1,19 +1,19 @@ - - - + + - - + + @@ -24,11 +24,13 @@ - - + + - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs new file mode 100644 index 000000000..8b8158d2d --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MinecraftServerQuery.xaml.cs @@ -0,0 +1,24 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace PCL; + +public partial class MinecraftServerQuery : Grid +{ + public MinecraftServerQuery() + { + InitializeComponent(); + BtnServerQuery.Click += BtnServerQuery_Click; + } + private void BtnServerQuery_Click(object sender, MouseButtonEventArgs e) + { + Dispatcher.BeginInvoke(new Func(() => ServerQueryAsync())); + } + + private async Task ServerQueryAsync() + { + await PanMcServer.UpdateServerInfoAsync(LabServerIp.Text); + ServerInfo.Visibility = Visibility.Visible; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyButton.xaml b/Plain Craft Launcher 2/Controls/MyButton.xaml index 52c44aea1..7ba584a10 100644 --- a/Plain Craft Launcher 2/Controls/MyButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyButton.xaml @@ -1,13 +1,19 @@ - - + + - + diff --git a/Plain Craft Launcher 2/Controls/MyButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyButton.xaml.cs new file mode 100644 index 000000000..5ec77b2ac --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyButton.xaml.cs @@ -0,0 +1,305 @@ +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyButton +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件 + + public enum ColorState + { + Normal = 0, + Highlight = 1, + Red = 2 + } + + // 自定义事件 + private const int AnimationColorIn = 100; + private const int AnimationColorOut = 200; + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyButton), new PropertyMetadata((sender, e) => + { + if (sender is not null) ((MyButton)sender).LabText.Text = Conversions.ToString(e.NewValue); + })); + + // 属性穿透 + public new static readonly DependencyProperty PaddingProperty = DependencyProperty.Register("Padding", + typeof(Thickness), typeof(MyButton), new PropertyMetadata((sender, e) => + { + if (sender is not null) ((dynamic)sender).PanFore.Padding = (Thickness)e.NewValue; + })); + + public static readonly DependencyProperty EventTypeProperty = + DependencyProperty.Register("EventType", typeof(string), typeof(MyButton), new PropertyMetadata(null)); + + public static readonly DependencyProperty EventDataProperty = + DependencyProperty.Register("EventData", typeof(string), typeof(MyButton), new PropertyMetadata(null)); + + private ColorState _ColorType = ColorState.Normal; // 配色方案 + + // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行) + private bool IsMouseDown; + + // 自定义属性 + public int Uuid = ModBase.GetUuid(); + + public MyButton() + { + InitializeComponent(); + + MouseEnter += RefreshColor; + MouseLeave += RefreshColor; + Loaded += RefreshColor; + IsEnabledChanged += (_, _) => RefreshColor(); + MouseLeftButtonUp += Button_MouseUp; + MouseLeftButtonDown += Button_MouseDown; + MouseEnter += (_, __) => Button_MouseEnter(); + MouseLeftButtonUp += (_, __) => Button_MouseUp(); + MouseLeave += (_, __) => Button_MouseLeave(); + } + + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } // 显示文本 + + public Thickness TextPadding + { + get => LabText.Padding; + set => LabText.Padding = value; + } + + public ColorState ColorType + { + get => _ColorType; + set + { + _ColorType = value; + RefreshColor(); + } + } + + public new Thickness Padding + { + get => PanFore.Padding; + set => PanFore.Padding = value; + } + + public Transform RealRenderTransform + { + get => PanFore.RenderTransform; + set => PanFore.RenderTransform = value; + } + + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + // 声明 + public event ClickEventHandler? Click; + + private void RefreshColor(object obj = null, object e = null) + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + if (IsEnabled) + switch (ColorType) + { + case ColorState.Normal: + { + if (IsMouseOver) + // 指向(Main 3) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush3", + AnimationColorIn) + }, "MyButton Color " + Uuid); + else + // 普通(Main 1) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush1", + AnimationColorOut) + }, "MyButton Color " + Uuid); + + break; + } + case ColorState.Highlight: + { + if (IsMouseOver) + // 指向(Main 3) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush3", + AnimationColorIn) + }, "MyButton Color " + Uuid); + else + // 高亮(Main 2) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrush2", + AnimationColorOut) + }, "MyButton Color " + Uuid); + + break; + } + case ColorState.Red: + { + if (IsMouseOver) + // 红色指向 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrushRedLight", + AnimationColorIn) + }, "MyButton Color " + Uuid); + else + // 红色 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, "ColorBrushRedDark", + AnimationColorOut) + }, "MyButton Color " + Uuid); + + break; + } + } + else + // 不可用(Gray 4) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanFore, BorderBrushProperty, + ModSecret.ColorGray4 - PanFore.BorderBrush, AnimationColorOut) + }, "MyButton Color " + Uuid); + } + else + { + ModAnimation.AniStop("MyButton Color " + Uuid); + if (IsEnabled) + switch (ColorType) + { + case ColorState.Normal: + { + if (IsMouseOver) + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush3"); + else + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush1"); + + break; + } + case ColorState.Highlight: + { + if (IsMouseOver) + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush3"); + else + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrush2"); + + break; + } + case ColorState.Red: + { + if (IsMouseOver) + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrushRedLight"); + else + PanFore.SetResourceReference(BorderBrushProperty, "ColorBrushRedDark"); + + break; + } + } + else + PanFore.BorderBrush = ModSecret.ColorGray4; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新按钮颜色出错"); + } + } + + // 实现自定义事件 + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + ModBase.Log("[Control] 按下按钮:" + Text); + Click?.Invoke(sender, e); + if (!string.IsNullOrEmpty(Conversions.ToString(Tag))) + if (Tag.ToString().StartsWithF("链接-") || Tag.ToString().StartsWithF("启动-")) + ModMain.Hint("主页自定义按钮语法已更新,且不再兼容老版本语法,请查看新的自定义示例!"); + + ModEvent.TryStartEvent(EventType, EventData); + } + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + IsMouseDown = true; + Focus(); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanFore, 0.955d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 80, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), + ModAnimation.AaScaleTransform(PanFore, -0.01d, 700, Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyButton Scale " + Uuid); + } + + private void Button_MouseEnter() + { + ModAnimation.AniStart( + ModAnimation.AaColor(PanFore, BackgroundProperty, + _ColorType == ColorState.Red ? "ColorBrushRedBack" : "ColorBrush7", AnimationColorIn), + "MyButton Background " + Uuid); + } + + private void Button_MouseUp() + { + if (!IsMouseDown) + return; + IsMouseDown = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 300, 10, + new ModAnimation.AniEaseOutFluent()) + }, "MyButton Scale " + Uuid); + } + + private void Button_MouseLeave() + { + ModAnimation.AniStart( + ModAnimation.AaColor(PanFore, BackgroundProperty, "ColorBrushHalfWhite", AnimationColorOut), + "MyButton Background " + Uuid); + if (!IsMouseDown) + return; + IsMouseDown = false; + ModAnimation.AniStart( + ModAnimation.AaScaleTransform(PanFore, 1d - ((ScaleTransform)PanFore.RenderTransform).ScaleX, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), "MyButton Scale " + Uuid); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyCard.cs b/Plain Craft Launcher 2/Controls/MyCard.cs new file mode 100644 index 000000000..431635593 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyCard.cs @@ -0,0 +1,504 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.UI.Controls; + +namespace PCL; + +public class MyCard : AnimatedBackgroundGrid +{ + // 动画 + private const double DropShadowIdleOpacity = 0.07d; + private const double DropShadowHoverOpacity = 0.4d; + + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register("Title", typeof(string), typeof(MyCard), new PropertyMetadata("")); + + private readonly BlurBorder MainBorder; + + // 控件 + private readonly Grid MainGrid; + private Path _MainSwap; + private TextBlock _MainTextBlock; + private bool IsLoad; + + // UI 建立 + public MyCard() : base(BlurBorder.BackgroundProperty) + { + MainChrome = new MyDropShadow + { + Margin = new Thickness(-3, -3, -3, -3 - ModBase.GetWPFSize(1d)), ShadowRadius = 3d, + Opacity = DropShadowIdleOpacity, CornerRadius = new CornerRadius(5d) + }; + MainChrome.SetResourceReference(MyDropShadow.ColorProperty, "ColorObject1"); + Children.Insert(0, MainChrome); + MainBorder = new BlurBorder { CornerRadius = new CornerRadius(5d), IsHitTestVisible = false }; + Children.Insert(1, MainBorder); + MainGrid = new Grid(); + Children.Add(MainGrid); + // 设置背景色 + SetResourceReference(BackgroundBrushProperty, "ColorBrushTransparentBackground"); + Loaded += (_, __) => Init(); + MouseEnter += MyCard_MouseEnter; + MouseLeave += MyCard_MouseLeave; + SizeChanged += MySizeChanged; + MouseLeftButtonDown += MyCard_MouseLeftButtonDown; + MouseLeftButtonUp += MyCard_MouseLeftButtonUp; + MouseLeave += MyCard_MouseLeave_Swap; + } + + public MyDropShadow MainChrome { get; } + + public UIElement BorderChild + { + get => MainBorder.Child; + set => MainBorder.Child = value; + } + + public TextBlock MainTextBlock + { + get + { + Init(); // 当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。 + return _MainTextBlock; + } + set => _MainTextBlock = value; + } + + public Path MainSwap + { + get + { + Init(); + return _MainSwap; + } + set => _MainSwap = value; + } + + // 属性 + public InlineCollection Inlines => MainTextBlock.Inlines; + + public CornerRadius CornerRadius + { + get => MainChrome.CornerRadius; + set + { + MainChrome.CornerRadius = value; + MainBorder.CornerRadius = value; + } + } + + public string Title + { + get => Conversions.ToString(GetValue(TitleProperty)); + set + { + SetValue(TitleProperty, value); + if (_MainTextBlock is not null) + MainTextBlock.Text = value; + } + } + + protected override SolidColorBrush AnimatableBrush + { + get => (SolidColorBrush)MainBorder.Background; + set => MainBorder.Background = value; + } + + protected override FrameworkElement AnimatableElement => MainBorder; + public bool HasMouseAnimation { get; set; } = true; + + private void Init() + { + if (IsLoad) + return; + IsLoad = true; + // AddHandler ThemeChanged, AddressOf _BackgroundBrushChanged '已在依赖属性中实现 + // 初次加载限定 + if (MainTextBlock is null) + { + MainTextBlock = new TextBlock + { + HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(15d, 12d, 0d, 0d), FontWeight = FontWeights.Bold, FontSize = 13d, + IsHitTestVisible = false + }; + MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1"); + MainTextBlock.SetBinding(TextBlock.TextProperty, + new Binding("Title") { Source = this, Mode = BindingMode.OneWay }); + MainGrid.Children.Add(MainTextBlock); + } + + if (CanSwap || SwapControl is not null) + { + if (SwapControl is null && Children.Count > 3) + SwapControl = Children[3]; + MainSwap = new Path + { + HorizontalAlignment = HorizontalAlignment.Right, Stretch = Stretch.Uniform, Height = 6d, Width = 10d, + VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0d, 17d, 16d, 0d), + Data = + (Geometry)new GeometryConverter().ConvertFromString("M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z"), + RenderTransform = new RotateTransform(180d), RenderTransformOrigin = new Point(0.5d, 0.5d) + }; + MainSwap.SetResourceReference(Shape.FillProperty, "ColorBrush1"); + MainGrid.Children.Add(MainSwap); + } + + // 改变默认的折叠 + if (IsSwapped && SwapControl is not null) + { + MainSwap.RenderTransform = new RotateTransform(SwapLogoRight ? 270 : 0); + ((dynamic)SwapControl).Visibility = Visibility.Collapsed; + // 取消由于高度变化被迫触发的高度动画 + var RawUseAnimation = UseAnimation; + UseAnimation = false; + Height = SwapedHeight; + ModAnimation.AniStop("MyCard Height " + Uuid); + IsHeightAnimating = false; + ModBase.RunInUi(() => UseAnimation = RawUseAnimation, true); + } + } + + // 已在依赖属性中实现 + // Private Sub Dispose() Handles Me.Unloaded + // If Parent Is Nothing Then + // RemoveHandler ThemeChanged, AddressOf _BackgroundBrushChanged + // End If + // End Sub + public void StackInstall() + { + var argstack = (StackPanel)SwapControl; + StackInstall(ref argstack, InstallMethod); + SwapControl = argstack; + TriggerForceResize(); + } + + public static void StackInstall(ref StackPanel stack, Action installMethod) + { + if (stack.Tag is null) + return; + try + { + installMethod(stack); + } + catch (Exception ex) + { + ModBase.Log(ex, "[MyCard] InstallMethod 调用失败"); + } + + stack.Children.Add(new FrameworkElement { Height = 18d }); // 下边距,同时适应折叠 + stack.Tag = null; + } + + private void MyCard_MouseEnter(object sender, MouseEventArgs e) + { + if (!HasMouseAnimation) + return; + var AniList = new List(); + if (!(MainTextBlock == null)) + AniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 90)); + if (!(MainSwap == null)) + AniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush2", 90)); + AniList.AddRange(new[] + { + ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject4", 90), + ModAnimation.AaOpacity(MainChrome, DropShadowHoverOpacity - MainChrome.Opacity, 90) + }); + if (Conversions.ToBoolean(!IsAnimating)) + ModAnimation.AniStart(AniList, "MyCard Mouse " + Uuid); + } + + private void MyCard_MouseLeave(object sender, MouseEventArgs e) + { + if (!HasMouseAnimation) + return; + var AniList = new List(); + if (!(MainTextBlock == null)) + AniList.Add(ModAnimation.AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 90)); + if (!(MainSwap == null)) + AniList.Add(ModAnimation.AaColor(MainSwap, Shape.FillProperty, "ColorBrush1", 90)); + AniList.AddRange(new[] + { + ModAnimation.AaColor(MainChrome, MyDropShadow.ColorProperty, "ColorObject1", 90), + ModAnimation.AaOpacity(MainChrome, DropShadowIdleOpacity - MainChrome.Opacity, 90) + }); + if (Conversions.ToBoolean(!IsAnimating)) + ModAnimation.AniStart(AniList, "MyCard Mouse " + Uuid); + } + + #region 高度改变动画 + + /// + /// 是否启用高度改变动画。 + /// + public bool UseAnimation { get; set; } = true; + + private bool IsHeightAnimating; + private double ActualUsedHeight; // 回滚实际高度(例如 NaN) + + private void MySizeChanged(object sender, SizeChangedEventArgs e) + { + if (!UseAnimation) + return; + var DeltaHeight = (IsSwapped ? SwapedHeight : e.NewSize.Height) - e.PreviousSize.Height; + // 卡片的进入时动画已被页面通用切换动画替代 + if (e.PreviousSize.Height == 0d || IsHeightAnimating || Math.Abs(DeltaHeight) < 1d || ActualHeight == 0d) + return; + StartHeightAnimation(DeltaHeight, e.PreviousSize.Height, false); + } + + /// + /// 启动卡片高度变化的动画效果 + /// 根据变化距离的大小采用不同的动画策略:短距离使用简单缓动,长距离使用分段动画 + /// + /// 高度变化量 + /// 之前的高度 + /// 是否为加载动画 + private void StartHeightAnimation(double Delta, double PreviousHeight, bool IsLoadAnimation) + { + if (IsHeightAnimating || ModMain.FrmMain is null) + return; // 避免 XAML 设计器出错 + + var AnimList = new List(); + var AbsDelta = Math.Abs(Delta); + + if (AbsDelta <= 800d) + { + // 短距离,直接使用 150ms 的缓动动画 + AnimList.Add(ModAnimation.AaHeight(this, Delta, 150, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong))); + } + else + { + var EaseLength = default(int); + int EaseTime; + int InitSpeed; // 到达缓动区前的初速度 + if (Delta < 0d && AbsDelta - EaseLength > 5000d * 0.1d) + { + // 收回距离过长 (>0.1s),强制以 100ms 完成匀速段,然后让减速段更长 + EaseLength = 200; + EaseTime = 150; + InitSpeed = (int)Math.Round((AbsDelta - EaseLength) / 0.1d); + } + else if (Delta > 0d && AbsDelta - EaseLength > 5000d * 0.6d) + { + // 展开距离过长 (>0.6s),以 5000 速度展示 300ms 匀速段,剩下的距离全部归入减速段 + InitSpeed = 5000; + EaseLength = (int)Math.Round(AbsDelta - InitSpeed * 0.3d); + EaseTime = 400; + } + else + { + // 中程,匀速地快速展开(或收回) + EaseLength = 150; + EaseTime = 200; + InitSpeed = 4000; + } + + // 匀速段 + AnimList.Add(ModAnimation.AaHeight(this, (AbsDelta - EaseLength) * Math.Sign(Delta), + (int)Math.Round((AbsDelta - EaseLength) / InitSpeed * 1000d))); + // 减速段 + AnimList.Add(ModAnimation.AaHeight(this, EaseLength * Math.Sign(Delta), EaseTime, + Ease: new ModAnimation.AniEaseOutFluentWithInitial(InitSpeed, EaseTime / 1000d, EaseLength), + After: true)); + } + + AnimList.Add(ModAnimation.AaCode(() => + { + IsHeightAnimating = false; + Height = ActualUsedHeight; + if (IsSwapped && SwapControl is not null) + ((dynamic)SwapControl).Visibility = Visibility.Collapsed; + }, After: true)); + ModAnimation.AniStart(AnimList, "MyCard Height " + Uuid); + IsHeightAnimating = true; + ActualUsedHeight = IsSwapped ? SwapedHeight : Height; + Height = PreviousHeight; + } + + /// + /// 通知 MyCard,控件内容已改变,需要中断动画并瞬间更新高度。 + /// + public void TriggerForceResize() + { + Height = IsSwapped ? SwapedHeight : double.NaN; + ModAnimation.AniStop("MyCard Height " + Uuid); + IsHeightAnimating = false; + } + + #endregion + + #region 折叠 + + // 若设置了 CanSwap,或 SwapControl 不为空,则判定为会进行折叠 + // 这是因为不能直接在 XAML 中设置 SwapControl + public object SwapControl; + public bool CanSwap { get; set; } = false; + + /// + /// 数据转为列表项的转换方法 + /// + /// + public Action InstallMethod { get; set; } + + /// + /// 是否已被折叠。 + /// + public bool IsSwapped + { + get => _IsSwapped; + set + { + if (_IsSwapped == value) + return; + _IsSwapped = value; + if (SwapControl is null) + return; + + // 当卡片展开时,如果SwapControl是StackPanel类型,则执行安装方法 + // 这通常用于动态添加内容到折叠卡片中 + if (!IsSwapped && SwapControl is StackPanel) + { + var argstack = (StackPanel)SwapControl; + StackInstall(ref argstack, InstallMethod); + SwapControl = argstack; + } + + // 若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行 + if (!IsLoaded) + return; + + // 更新控件的可见性和高度 + ((dynamic)SwapControl).Visibility = Visibility.Visible; + TriggerForceResize(); + + // 根据折叠状态旋转箭头图标 + // 折叠时箭头指向右侧或向上(根据SwapLogoRight设置),展开时指向下方 + ModAnimation.AniStart( + ModAnimation.AaRotateTransform(MainSwap, + (_IsSwapped ? SwapLogoRight ? 270 : 0 : 180) - ((RotateTransform)MainSwap.RenderTransform).Angle, + 250, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), + "MyCard Swap " + Uuid, true); + } + } + + private bool _IsSwapped; + + /// + /// 是否已被折叠。(已过时,请使用 IsSwapped) + /// + [Obsolete("请使用 IsSwapped 属性,IsSwaped 存在拼写错误")] + public bool IsSwaped + { + get => IsSwapped; + set => IsSwapped = value; + } + + public bool SwapLogoRight { get; set; } = false; + private bool IsMouseDown; + public event PreviewSwapEventHandler? PreviewSwap; + + public delegate void PreviewSwapEventHandler(object sender, ModBase.RouteEventArgs e); + + public event SwapEventHandler? Swap; + + public delegate void SwapEventHandler(object sender, ModBase.RouteEventArgs e); + + public const int SwapedHeight = 40; + + private void MyCard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + var Pos = Mouse.GetPosition(this).Y; + if (!IsSwapped && (SwapControl is null || Pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || + (Pos == 0d && !IsMouseDirectlyOver))) + return; // 检测点击位置;或已经不在可视树上的误判 + IsMouseDown = true; + } + + private void MyCard_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + IsMouseDown = false; + + var Pos = Mouse.GetPosition(this).Y; + if (!IsSwapped && (SwapControl is null || Pos > (IsSwapped ? SwapedHeight : SwapedHeight - 6) || + (Pos == 0d && !IsMouseDirectlyOver))) + return; // 检测点击位置;或已经不在可视树上的误判 + + var ee = new ModBase.RouteEventArgs(true); + PreviewSwap?.Invoke(this, ee); + if (ee.Handled) + { + IsMouseDown = false; + return; + } + + IsSwapped = !IsSwapped; + ModBase.Log("[Control] " + (IsSwapped ? "折叠卡片" : "展开卡片") + (Title is null ? "" : ":" + Title)); + Swap?.Invoke(this, ee); + } + + private void MyCard_MouseLeave_Swap(object sender, MouseEventArgs e) + { + IsMouseDown = false; + } + + #endregion +} + +public static partial class ModAnimation +{ + public static void AniDispose(MyCard Control, bool RemoveFromChildren, ParameterizedThreadStart CallBack = null) + { + if (Control.IsHitTestVisible) + { + Control.IsHitTestVisible = false; + AniStart(new[] + { + AaScaleTransform(Control, -0.08d, 200, Ease: new AniEaseInFluent()), + AaOpacity(Control, -1, 200, Ease: new AniEaseOutFluent()), + AaHeight(Control, -Control.ActualHeight, 150, 100, new AniEaseOutFluent()), + AaCode(() => + { + if (RemoveFromChildren) + { + if (Control.Parent is null) + return; + ((Panel)Control.Parent).Children.Remove(Control); + } + else + { + Control.Visibility = Visibility.Collapsed; + } + + if (CallBack is not null) + CallBack(Control); + }, After: true) + }, "MyCard Dispose " + Control.Uuid); + } + else + { + if (RemoveFromChildren) + { + if (Control.Parent is null) + return; + ((Panel)Control.Parent).Children.Remove(Control); + } + else + { + Control.Visibility = Visibility.Collapsed; + } + + if (CallBack is not null) + CallBack(Control); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml index 1813032a0..150c1bc6e 100644 --- a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml +++ b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml @@ -1,19 +1,30 @@ - - - + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="PCL.MyCheckBox" + FocusVisualStyle="{x:Null}" + MinWidth="20" x:Name="PanBack" UseLayoutRounding="False" SnapsToDevicePixels="False" MinHeight="20" + Background="{StaticResource ColorBrushSemiTransparent}" Focusable="True" d:DesignWidth="126.4" + d:DesignHeight="44.8"> + + + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs new file mode 100644 index 000000000..04f731ac3 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyCheckBox.xaml.cs @@ -0,0 +1,508 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyCheckBox +{ + public delegate void ChangeEventHandler(object sender, bool user); + + public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e); + + private const int AnimationTimeOfCheck = 150; // 勾选状态变更动画长度 + + // 指向动画 + + private const int AnimationTimeOfMouseIn = 100; + + private const int AnimationTimeOfMouseOut = 200; + + // 在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变 + public static readonly DependencyProperty CheckedProperty = DependencyProperty.Register("Checked", typeof(bool?), + typeof(MyCheckBox), new PropertyMetadata(false, (d, e) => + { + var obj = (MyCheckBox)d; + if (!obj.IsLoaded) obj.SyncUI(); + })); + + /// + /// 是否为三态复选框。 + /// + public static readonly DependencyProperty IsThreeStateProperty = + DependencyProperty.Register("IsThreeState", typeof(bool), typeof(MyCheckBox), new PropertyMetadata(false)); + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyCheckBox), new PropertyMetadata((sender, e) => + { + if (!(sender == null)) ((MyCheckBox)sender).LabText.Text = Conversions.ToString(e.NewValue); + })); + + private bool? _previousState = false; // 上一次的勾选状态 + private bool AllowMouseDown = true; + + // 点击事件 + + private bool MouseDowned; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyCheckBox() + { + InitializeComponent(); + + MouseLeftButtonUp += (_, __) => Checkbox_MouseUp(); + MouseLeftButtonDown += (_, __) => Checkbox_MouseDown(); + MouseLeave += (_, __) => Checkbox_MouseLeave(); + IsEnabledChanged += (_, __) => Checkbox_IsEnabledChanged(); + MouseEnter += (_, __) => Checkbox_MouseEnterAnimation(); + MouseLeave += (_, __) => Checkbox_MouseLeaveAnimation(); + } + + // 自定义属性 + public bool? Checked + { + get => (bool?)GetValue(CheckedProperty); + set => SetChecked(value, false); + } + + public bool IsThreeState + { + get => Conversions.ToBoolean(GetValue(IsThreeStateProperty)); + set => SetValue(IsThreeStateProperty, value); + } // 是否为三态复选框 + + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } // 内容 + + /// + /// 复选框勾选状态改变。 + /// + /// 是否为用户手动改变的勾选状态。 + public event ChangeEventHandler? Change; + + public event PreviewChangeEventHandler? PreviewChange; + + public void RaiseChange() + { + Change?.Invoke(this, false); + } // 使外部程序引发本控件的 Change 事件 + + /// + /// 手动设置 Checked 属性。 + /// + /// 新的 Checked 属性。 + /// 是否由用户引发。 + public void SetChecked(bool? value, bool user) + { + try + { + if (Checked is var arg1 && value.HasValue && arg1.HasValue && value.Value == arg1.Value) + return; + + // Preview 事件 + if ((!value.HasValue || value.Value) && user && value.HasValue) + { + var e = new ModBase.RouteEventArgs(user); + PreviewChange?.Invoke(this, e); + if (e.Handled) + { + MouseDowned = true; + Checkbox_MouseLeave(); + MouseDowned = false; + return; + } + } + + // 判断真实勾选状态 + var isChecked = GetFinalState(value, IsThreeState); + + _previousState = Checked; // 记录上一次的勾选状态 + SetValue(CheckedProperty, isChecked); + if (IsLoaded) + Change?.Invoke(this, user); + + // 更改动画 + SyncUI(); + } + catch (Exception ex) + { + ModBase.Log(ex, "设置 Checked 失败"); + } + } + + private void SyncUI() + { + if (ModAnimation.AniControlEnabled == 0 && IsLoaded) // 防止默认属性变更触发动画 + { + AllowMouseDown = false; + + var isChecked = GetFinalState(Checked, IsThreeState); + + switch (isChecked, _previousState) + { + case (true, false): + AniBackgroundScale(); + AniCheckShow(); + AniColorChecked(); + AniAllowMouseDown(); + break; + + case (true, null): + AniBackgroundScale(); + AniIndeterminateHide(); + AniCheckShow(); + AniColorChecked(); + AniAllowMouseDown(); + break; + + case (false, true): + AniBackgroundScale(); + AniCheckHide(); + AniColorUnchecked(); + AniAllowMouseDown(); + break; + + case (false, null): + AniBackgroundScale(); + AniIndeterminateHide(); + AniCheckHide(); + AniColorUnchecked(); + AniAllowMouseDown(); + break; + + case (null, true): + AniBackgroundScale(); + AniCheckHide(); + AniIndeterminateShow(); + AniColorUnchecked(); + AniAllowMouseDown(); + break; + + case (null, false): + AniBackgroundScale(); + AniIndeterminateShow(); + AniColorUnchecked(); + AniAllowMouseDown(); + break; + } + } + + // If Checked Then + // '由无变有 + // AniStart({ + // AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True), + // AaScaleTransform(ShapeCheck, 1 - CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack(AniEasePower.Weak)), + // AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True) + // }, "MyCheckBox Scale " & Uuid) + // AniStart({ + // AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush2"), "ColorBrushGray4"), AnimationTimeOfCheck) + // }, "MyCheckBox BorderColor " & Uuid) + // AniStart({ + // AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2) + // }, "MyCheckBox AllowMouseDown " & Uuid) + // Else + // '由有变无 + // AniStart({ + // AaScale(ShapeBorder, 12 - ShapeBorder.Width, AnimationTimeOfCheck, , New AniEaseOutFluent, , True), + // AaScaleTransform(ShapeCheck, -CType(ShapeCheck.RenderTransform, ScaleTransform).ScaleX, AnimationTimeOfCheck * 0.9, , New AniEaseInFluent(AniEasePower.Weak)), + // AaScale(ShapeBorder, 6, AnimationTimeOfCheck * 2, AnimationTimeOfCheck * 0.7, New AniEaseOutBack, , True) + // }, "MyCheckBox Scale " & Uuid) + // AniStart({ + // AaColor(ShapeBorder, Border.BorderBrushProperty, If(IsEnabled, If(IsMouseOver, "ColorBrush3", "ColorBrush1"), "ColorBrushGray4"), AnimationTimeOfCheck) + // }, "MyCheckBox BorderColor " & Uuid) + // AniStart({ + // AaCode(Sub() AllowMouseDown = True, AnimationTimeOfCheck * 2) + // }, "MyCheckBox AllowMouseDown " & Uuid) + // End If + else + { + // 不使用动画 + ModAnimation.AniStop("MyCheckBox Background Scale " + Uuid); + ModAnimation.AniStop("MyCheckBox Check Scale Show" + Uuid); + ModAnimation.AniStop("MyCheckBox Check Scale Hide" + Uuid); + ModAnimation.AniStop("MyCheckBox Indeterminate Scale Show" + Uuid); + ModAnimation.AniStop("MyCheckBox Indeterminate Scale Hide" + Uuid); + ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid); + ModAnimation.AniStop("MyCheckBox AllowMouseDown " + Uuid); + if (Checked == true) + { + ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 1d; + ((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 1d; + ShapeBorder.SetResourceReference(Border.BorderBrushProperty, + IsEnabled ? "ColorBrush2" : "ColorBrushGray4"); + } + else + { + ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX = 0d; + ((ScaleTransform)ShapeCheck.RenderTransform).ScaleY = 0d; + ShapeBorder.SetResourceReference(Border.BorderBrushProperty, + IsEnabled ? "ColorBrush1" : "ColorBrushGray4"); + } + } + } + + private void Checkbox_MouseUp() + { + if (!MouseDowned) + return; + ModBase.Log("[Control] 按下复选框(" + !Checked + "):" + Text); + MouseDowned = false; + if (IsThreeState) + { + switch (Checked) + { + case true: + SetChecked(null, true); + break; + case false: + SetChecked(true, true); + break; + case null: + SetChecked(false, true); + break; + } + + ModAnimation.AniStart( + ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), + "MyCheckBox Background " + Uuid); + return; + } + + SetChecked(!Checked, true); + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), + "MyCheckBox Background " + Uuid); + } + + private void Checkbox_MouseDown() + { + if (!AllowMouseDown) + return; + MouseDowned = true; + Focus(); + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushBg1", 100), + "MyCheckBox Background " + Uuid); + if (Checked == true) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + ModAnimation.AaScaleTransform(ShapeCheck, + 0.9d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, 1000, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)) + }, "MyCheckBox Scale " + Uuid); + else + ModAnimation.AniStart( + ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + "MyCheckBox Scale " + Uuid); + } + + private void Checkbox_MouseLeave() + { + if (!MouseDowned) + return; + MouseDowned = false; + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Border.BackgroundProperty, "ColorBrushHalfWhite", 100), + "MyCheckBox Background " + Uuid); + if (Checked == true) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, + 500, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)) + }, "MyCheckBox Scale " + Uuid); + else + ModAnimation.AniStart( + ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + "MyCheckBox Scale " + Uuid); + } + + private void Checkbox_IsEnabledChanged() + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + if (IsEnabled) + { + // 可用 + Checkbox_MouseLeaveAnimation(); + } + else + { + // 不可用 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, + ModSecret.ColorGray4 - ShapeBorder.BorderBrush, AnimationTimeOfMouseOut) + }, "MyCheckBox BorderColor " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + ModSecret.ColorGray4 - LabText.Foreground, AnimationTimeOfMouseOut) + }, "MyCheckBox TextColor " + Uuid); + } + } + else + { + // 无动画 + ModAnimation.AniStop("MyCheckBox TextColor " + Uuid); + ModAnimation.AniStop("MyCheckBox BorderColor " + Uuid); + LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4"); + ShapeBorder.SetResourceReference(Border.BorderBrushProperty, + IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4"); + } + } + + private void Checkbox_MouseEnterAnimation() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn) + }, "MyCheckBox TextColor " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, "ColorBrush3", AnimationTimeOfMouseIn) + }, "MyCheckBox BorderColor " + Uuid); + } + + private void Checkbox_MouseLeaveAnimation() + { + if (!IsEnabled) + return; // MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + IsEnabled ? "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfMouseOut) + }, "MyCheckBox TextColor " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, + IsEnabled ? Checked == true ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4", + AnimationTimeOfMouseOut) + }, "MyCheckBox BorderColor " + Uuid); + } + + // 动画 + private void AniBackgroundScale() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeBorder, 12d - ShapeBorder.Width, AnimationTimeOfCheck, + Ease: new ModAnimation.AniEaseOutFluent(), Absolute: true), + ModAnimation.AaScale(ShapeBorder, 6d, AnimationTimeOfCheck * 2, + (int)Math.Round(AnimationTimeOfCheck * 0.7d), new ModAnimation.AniEaseOutBack(), Absolute: true) + }, "MyCheckBox Background Scale " + Uuid); + } + + private void AniCheckShow() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeCheck, 1d - ((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, + AnimationTimeOfCheck * 2, (int)Math.Round(AnimationTimeOfCheck * 0.7d), + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)) + }, "MyCheckBox Check Scale Show" + Uuid); + } + + private void AniCheckHide() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeCheck, -((ScaleTransform)ShapeCheck.RenderTransform).ScaleX, + (int)Math.Round(AnimationTimeOfCheck * 0.9d), + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)) + }, "MyCheckBox Check Scale Hide" + Uuid); + } + + private void AniIndeterminateShow() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeIndeterminate, + 1d - ((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX, AnimationTimeOfCheck * 2, + (int)Math.Round(AnimationTimeOfCheck * 0.7d), + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)) + }, "MyCheckBox Indeterminate Scale Show" + Uuid); + } + + private void AniIndeterminateHide() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeIndeterminate, + -((ScaleTransform)ShapeIndeterminate.RenderTransform).ScaleX, + (int)Math.Round(AnimationTimeOfCheck * 0.9d), + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)) + }, "MyCheckBox Indeterminate Scale Hide" + Uuid); + } + + private void AniAllowMouseDown() + { + ModAnimation.AniStart(new[] { ModAnimation.AaCode(() => AllowMouseDown = true, AnimationTimeOfCheck * 2) }, + "MyCheckBox AllowMouseDown " + Uuid); + } + + private void AniColorChecked() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, + IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush2" : "ColorBrushGray4", AnimationTimeOfCheck) + }, "MyCheckBox BorderColor " + Uuid); + } + + private void AniColorUnchecked() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Border.BorderBrushProperty, + IsEnabled ? IsMouseOver ? "ColorBrush3" : "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfCheck) + }, "MyCheckBox BorderColor " + Uuid); + } + + private bool? GetFinalState(bool? value, bool isThreeState) + { + if (isThreeState) + { + // 三态复选框 + if (value.HasValue && value.Value) return true; + + if (value.HasValue && !value.Value) return false; + + return default; + // 空值表示未选中状态 + } + + // 二态复选框 + return value == true ? true : false; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyComboBox.cs b/Plain Craft Launcher 2/Controls/MyComboBox.cs new file mode 100644 index 000000000..a1e46b8ca --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyComboBox.cs @@ -0,0 +1,232 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class MyComboBox : ComboBox +{ + public delegate void TextChangedEventHandler(object sender, TextChangedEventArgs e); + + public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string), + typeof(MyComboBox), new PropertyMetadata("", (d, e) => + { + var c = (MyComboBox)d; + if (c.TextBox is not null) + c.TextBox.HintText = Conversions.ToString(e.NewValue); + })); + + private string _Text; + + // 鼠标按下接口 + private bool IsMouseDown; + + // 修复 WPF Bug:下拉框文本修改后,依然误认为还选择着此前的选项,导致再次点击该选项时内容不变 + private bool IsTextChanging; + private double RealWidth; // 由于下拉框 Popup 宽度与 Width 一致,故不能为 NaN(Auto) + private MyTextBox TextBox; + + // 基础 + public int Uuid = ModBase.GetUuid(); + + public MyComboBox() + { + _Text = SelectedItem?.ToString() ?? ""; + PreviewMouseLeftButtonDown += MyComboBox_PreviewMouseLeftButtonDown; + PreviewMouseLeftButtonUp += MyComboBox_PreviewMouseLeftButtonUp; + MouseLeave += MyComboBox_PreviewMouseLeftButtonUp; + IsEnabledChanged += (_, __) => RefreshColor(); + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + PreviewMouseLeftButtonDown += (_, __) => RefreshColor(); + PreviewMouseLeftButtonUp += (_, __) => RefreshColor(); + GotKeyboardFocus += (_, __) => RefreshColor(); + DropDownOpened += MyComboBox_DropDownOpened; + DropDownClosed += MyComboBox_DropDownClosed; + TextChanged += MyComboBox_TextChanged; + } + + public string HintText + { + get => Conversions.ToString(GetValue(HintTextProperty)); + set => SetValue(HintTextProperty, value); + } + + public new string Text + { + get + { + if (IsEditable) + { + if (TextBox is null) return _Text ?? ""; + + return TextBox.Text ?? ""; + } + + return (SelectedItem ?? "").ToString(); + } + set + { + if (IsEditable) + { + if (TextBox == null) + _Text = value; + else + TextBox.Text = value; + } + else + { + throw new NotSupportedException("该 ComboBox 不支持修改文本。"); + } + } + } + + public bool DropDownWidthSync { get; set; } = true; + + public ContentPresenter ContentPresenter => (ContentPresenter)Template.FindName("PART_Content", this); + public event TextChangedEventHandler? TextChanged; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + if (!IsEditable) + return; + try + { + TextBox = (MyTextBox)Template.FindName("PART_EditableTextBox", this); + TextBox.AddHandler(LostFocusEvent, new RoutedEventHandler((_, __) => RefreshColor())); + TextBox.ChangedEventList.Add((sender, e) => TextChanged?.Invoke(sender, (TextChangedEventArgs)e)); + TextBox.Tag = Tag; // 有时需要用文本框的 Tag 来写入设置 + if (string.IsNullOrEmpty(Text)) + TextBox.Text = _Text; + else + TextChanged?.Invoke(this, null); + if (HintText.Length > 0) + TextBox.HintText = HintText; + TextBox.SetResourceReference(TextBoxBase.CaretBrushProperty, "ColorBrushGray1"); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化可编辑文本框失败(" + (Name ?? "") + ")", ModBase.LogLevel.Feedback); + } + } + + private void MyComboBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + IsMouseDown = true; + } + + private void MyComboBox_PreviewMouseLeftButtonUp(object sender, EventArgs e) + { + IsMouseDown = false; + } + + // 指向动画 + public void RefreshColor() + { + // 判断当前颜色 + string ForeColorName; + string BackColorName; + int Time; + if (IsEnabled) + { + if (Conversions.ToBoolean(IsMouseDown || IsDropDownOpen || + (IsEditable && ((dynamic)Template.FindName("PART_EditableTextBox", this)) + .IsFocused))) + { + ForeColorName = "ColorBrush3"; + BackColorName = "ColorBrush7"; + Time = 10; + } + else if (IsMouseOver) + { + ForeColorName = "ColorBrush4"; + BackColorName = "ColorBrush7"; + Time = 100; + } + else + { + ForeColorName = "ColorBrushBg0"; + BackColorName = "ColorBrushHalfWhite"; + Time = 100; + } + } + else + { + ForeColorName = "ColorBrushGray5"; + BackColorName = "ColorBrushGray6"; + Time = 200; + } + + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, ForegroundProperty, ForeColorName, Time), + ModAnimation.AaColor(this, BackgroundProperty, BackColorName, Time) + }, "MyComboBox Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyComboBox Color " + Uuid); + SetResourceReference(ForegroundProperty, ForeColorName); + SetResourceReference(BackgroundProperty, BackColorName); + } + } + + private void MyComboBox_DropDownOpened(object sender, EventArgs e) + { + RealWidth = Width; + if (DropDownWidthSync) + Width = ActualWidth; + try + { + var popup = (Grid)Template.FindName("PanPopup", this); + popup.Opacity = ModMain.FrmMain.Opacity; + if (!DropDownWidthSync) + popup.MinWidth = ActualWidth; + } + catch (Exception ex) + { + ModBase.Log(ex, "设置下拉框属性失败", ModBase.LogLevel.Feedback); + } + } + + private void MyComboBox_DropDownClosed(object sender, EventArgs e) + { + Width = RealWidth; + } + + private void MyComboBox_TextChanged(object sender, TextChangedEventArgs e) + { + if (IsTextChanging || !IsEditable) + return; + if (SelectedItem is not null && (Text ?? "") != (SelectedItem.ToString() ?? "")) + { + var RawText = Text; + var RawSelectionStart = TextBox.SelectionStart; + IsTextChanging = true; + SelectedItem = null; + Text = RawText; + TextBox.SelectionStart = RawSelectionStart; + IsTextChanging = false; + } + } + + // 用于 ItemsSource 的自定义容器 + protected override DependencyObject GetContainerForItemOverride() + { + return new MyComboBoxItem(); + } + + protected override bool IsItemItsOwnContainerOverride(object item) + { + return item is MyComboBoxItem || base.IsItemItsOwnContainerOverride(item); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs b/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs new file mode 100644 index 000000000..4ae6b6cf8 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyComboBoxItem.cs @@ -0,0 +1,100 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace PCL; + +public class MyComboBoxItem : ComboBoxItem +{ + // 指向动画 + + private const int AnimationTimeIn = 100; + private const int AnimationTimeOut = 300; + private string BackColorName; + private double FontOpacity; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyComboBoxItem() + { + Style = (Style)FindResource("MyComboBoxItem"); + Unselected += (_, __) => RefreshColor(); + MouseMove += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + Selected += (_, __) => RefreshColor(); + IsEnabledChanged += (_, __) => RefreshColor(); + MouseLeftButtonUp += MyComboBoxItem_MouseLeftButtonUp; + } + + private void RefreshColor() + { + // 判断当前颜色 + string NewBackColorName; + double NewFontOpacity; + int Time; + if (IsSelected) + { + NewBackColorName = "ColorBrush6"; + NewFontOpacity = 1d; + Time = AnimationTimeIn; + } + else if (IsMouseOver) + { + NewBackColorName = "ColorBrush8"; + NewFontOpacity = 1d; + Time = AnimationTimeIn; + } + else if (IsEnabled) + { + NewBackColorName = "ColorBrushTransparent"; + NewFontOpacity = 1d; + Time = AnimationTimeOut; + } + else + { + NewBackColorName = "ColorBrushTransparent"; + NewFontOpacity = 0.4d; + Time = AnimationTimeOut; + } + + if ((BackColorName ?? "") == (NewBackColorName ?? "") && FontOpacity == NewFontOpacity) + return; + BackColorName = NewBackColorName; + FontOpacity = NewFontOpacity; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, BackgroundProperty, BackColorName, Time), + ModAnimation.AaOpacity(this, FontOpacity - Opacity, Time) + }, "ComboBoxItem Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("ComboBoxItem Color " + Uuid); + SetResourceReference(BackgroundProperty, BackColorName); + Opacity = FontOpacity; + } + } + + public override string ToString() + { + return Content.ToString(); + } + + public static implicit operator string(MyComboBoxItem Value) + { + return Value.Content.ToString(); + } + + private void MyComboBoxItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + ModBase.Log("[Control] 选择下拉列表项:" + ToString()); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyDropShadow.cs b/Plain Craft Launcher 2/Controls/MyDropShadow.cs new file mode 100644 index 000000000..878d60dfb --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyDropShadow.cs @@ -0,0 +1,374 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class MyDropShadow : Decorator +{ + public static readonly DependencyProperty ColorProperty = DependencyProperty.Register("Color", typeof(Color), + typeof(MyDropShadow), + new FrameworkPropertyMetadata(Color.FromArgb(0x71, 0x0, 0x0, 0x0), + FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes)); + + public static readonly DependencyProperty ShadowRadiusProperty = DependencyProperty.Register("ShadowRadius", + typeof(double), typeof(MyDropShadow), + new FrameworkPropertyMetadata(5d, FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes)); + + public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", + typeof(CornerRadius), typeof(MyDropShadow), + new FrameworkPropertyMetadata(new CornerRadius(), FrameworkPropertyMetadataOptions.AffectsRender, ClearBrushes), + IsCornerRadiusValid); + + private static Brush[] _commonBrushes; + private static CornerRadius _commonCornerRadius; + private static readonly object _resourceAccess = new(); + private Brush[] _brushes; + + /// + /// 阴影颜色。 + /// + public Color Color + { + get => (Color)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// 阴影模糊半径。 + /// + public double ShadowRadius + { + get => Conversions.ToDouble(GetValue(ShadowRadiusProperty)); + set => SetValue(ShadowRadiusProperty, value); + } + + /// + /// 圆角大小。 + /// + public CornerRadius CornerRadius + { + get => (CornerRadius)GetValue(CornerRadiusProperty); + set => SetValue(CornerRadiusProperty, value); + } + + private static bool IsCornerRadiusValid(object value) + { + var cr = (CornerRadius)value; + return !(cr.TopLeft < 0.0d || cr.TopRight < 0.0d || cr.BottomLeft < 0.0d || cr.BottomRight < 0.0d || + double.IsNaN(cr.TopLeft) || double.IsNaN(cr.TopRight) || double.IsNaN(cr.BottomLeft) || + double.IsNaN(cr.BottomRight) || double.IsInfinity(cr.TopLeft) || double.IsInfinity(cr.TopRight) || + double.IsInfinity(cr.BottomLeft) || double.IsInfinity(cr.BottomRight)); + } + + + // ======================================= + // 渲染 + // ======================================= + + + protected override void OnRender(DrawingContext drawingContext) + { + var cornerRadius = CornerRadius; + var shadowBounds = new Rect(0d, 0d, RenderSize.Width, RenderSize.Height); + var color = Color; + + if (shadowBounds.Width > 0d && shadowBounds.Height > 0d && color.A > 0) + { + var centerWidth = shadowBounds.Right - shadowBounds.Left - 2d * ShadowRadius; + var centerHeight = shadowBounds.Bottom - shadowBounds.Top - 2d * ShadowRadius; + var maxRadius = Math.Min(centerWidth * 0.5d, centerHeight * 0.5d); + cornerRadius.TopLeft = Math.Min(cornerRadius.TopLeft, maxRadius); + cornerRadius.TopRight = Math.Min(cornerRadius.TopRight, maxRadius); + cornerRadius.BottomLeft = Math.Min(cornerRadius.BottomLeft, maxRadius); + cornerRadius.BottomRight = Math.Min(cornerRadius.BottomRight, maxRadius); + var brushes = GetBrushes(color, cornerRadius); + var centerTop = shadowBounds.Top + ShadowRadius; + var centerLeft = shadowBounds.Left + ShadowRadius; + var centerRight = shadowBounds.Right - ShadowRadius; + var centerBottom = shadowBounds.Bottom - ShadowRadius; + var guidelineSetX = new[] + { + centerLeft, centerLeft + cornerRadius.TopLeft, centerRight - cornerRadius.TopRight, + centerLeft + cornerRadius.BottomLeft, centerRight - cornerRadius.BottomRight, centerRight + }; + var guidelineSetY = new[] + { + centerTop, centerTop + cornerRadius.TopLeft, centerTop + cornerRadius.TopRight, + centerBottom - cornerRadius.BottomLeft, centerBottom - cornerRadius.BottomRight, centerBottom + }; + drawingContext.PushGuidelineSet(new GuidelineSet(guidelineSetX, guidelineSetY)); + cornerRadius.TopLeft += ShadowRadius; + cornerRadius.TopRight += ShadowRadius; + cornerRadius.BottomLeft += ShadowRadius; + cornerRadius.BottomRight += ShadowRadius; + var topLeft = new Rect(shadowBounds.Left, shadowBounds.Top, cornerRadius.TopLeft, cornerRadius.TopLeft); + drawingContext.DrawRectangle(brushes[(int)Placement.TopLeft], null, topLeft); + var topWidth = guidelineSetX[2] - guidelineSetX[1]; + + if (topWidth > 0d) + { + var top = new Rect(guidelineSetX[1], shadowBounds.Top, topWidth, ShadowRadius); + drawingContext.DrawRectangle(brushes[(int)Placement.Top], null, top); + } + + var topRight = new Rect(guidelineSetX[2], shadowBounds.Top, cornerRadius.TopRight, cornerRadius.TopRight); + drawingContext.DrawRectangle(brushes[(int)Placement.TopRight], null, topRight); + var leftHeight = guidelineSetY[3] - guidelineSetY[1]; + + if (leftHeight > 0d) + { + var left = new Rect(shadowBounds.Left, guidelineSetY[1], ShadowRadius, leftHeight); + drawingContext.DrawRectangle(brushes[(int)Placement.Left], null, left); + } + + var rightHeight = guidelineSetY[4] - guidelineSetY[2]; + + if (rightHeight > 0d) + { + var right = new Rect(guidelineSetX[5], guidelineSetY[2], ShadowRadius, rightHeight); + drawingContext.DrawRectangle(brushes[(int)Placement.Right], null, right); + } + + var bottomLeft = new Rect(shadowBounds.Left, guidelineSetY[3], cornerRadius.BottomLeft, + cornerRadius.BottomLeft); + drawingContext.DrawRectangle(brushes[(int)Placement.BottomLeft], null, bottomLeft); + var bottomWidth = guidelineSetX[4] - guidelineSetX[3]; + + if (bottomWidth > 0d) + { + var bottom = new Rect(guidelineSetX[3], guidelineSetY[5], bottomWidth, ShadowRadius); + drawingContext.DrawRectangle(brushes[(int)Placement.Bottom], null, bottom); + } + + var bottomRight = new Rect(guidelineSetX[4], guidelineSetY[4], cornerRadius.BottomRight, + cornerRadius.BottomRight); + drawingContext.DrawRectangle(brushes[(int)Placement.BottomRight], null, bottomRight); + + if (cornerRadius.TopLeft == ShadowRadius && cornerRadius.TopLeft == cornerRadius.TopRight && + cornerRadius.TopLeft == cornerRadius.BottomLeft && cornerRadius.TopLeft == cornerRadius.BottomRight) + { + var center = new Rect(guidelineSetX[0], guidelineSetY[0], centerWidth, centerHeight); + drawingContext.DrawRectangle(brushes[(int)Placement.Center], null, center); + } + else + { + var figure = new PathFigure(); + + if (cornerRadius.TopLeft > ShadowRadius) + { + figure.StartPoint = new Point(guidelineSetX[1], guidelineSetY[0]); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[1], guidelineSetY[1]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[1]), true)); + } + else + { + figure.StartPoint = new Point(guidelineSetX[0], guidelineSetY[0]); + } + + if (cornerRadius.BottomLeft > ShadowRadius) + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[3]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[3]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[3], guidelineSetY[5]), true)); + } + else + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[0], guidelineSetY[5]), true)); + } + + if (cornerRadius.BottomRight > ShadowRadius) + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[5]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[4], guidelineSetY[4]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[4]), true)); + } + else + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[5]), true)); + } + + if (cornerRadius.TopRight > ShadowRadius) + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[2]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[2]), true)); + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[2], guidelineSetY[0]), true)); + } + else + { + figure.Segments.Add(new LineSegment(new Point(guidelineSetX[5], guidelineSetY[0]), true)); + } + + figure.IsClosed = true; + figure.Freeze(); + var geometry = new PathGeometry(); + geometry.Figures.Add(figure); + geometry.Freeze(); + drawingContext.DrawGeometry(brushes[(int)Placement.Center], null, geometry); + } + + drawingContext.Pop(); + } + } + + private static void ClearBrushes(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + ((MyDropShadow)o)._brushes = null; + } + + private GradientStopCollection CreateStops(Color c, double cornerRadius) + { + var gradientScale = 1d / (ShadowRadius + cornerRadius); + var gsc = new GradientStopCollection(); + var stopColor = c; + gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.1d + cornerRadius) * gradientScale)); + stopColor.A = (byte)Math.Round(0.74336d * c.A); + gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.3d + cornerRadius) * gradientScale)); + stopColor.A = (byte)Math.Round(0.38053d * c.A); + gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.5d + cornerRadius) * gradientScale)); + stopColor.A = (byte)Math.Round(0.12389d * c.A); + gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.7d + cornerRadius) * gradientScale)); + stopColor.A = (byte)Math.Round(0.02654d * c.A); + gsc.Add(new GradientStop(stopColor, (ShadowRadius * 0.9d + cornerRadius) * gradientScale)); + stopColor.A = 0; + gsc.Add(new GradientStop(stopColor, (ShadowRadius + cornerRadius) * gradientScale)); + gsc.Freeze(); + return gsc; + } + + private Brush[] CreateBrushes(Color c, CornerRadius cornerRadius) + { + var brushes = new Brush[9]; + brushes[(int)Placement.Center] = new SolidColorBrush(c); + brushes[(int)Placement.Center].Freeze(); + var sideStops = CreateStops(c, 0d); + var top = new LinearGradientBrush(sideStops, new Point(0d, 1d), new Point(0d, 0d)); + top.Freeze(); + brushes[(int)Placement.Top] = top; + var left = new LinearGradientBrush(sideStops, new Point(1d, 0d), new Point(0d, 0d)); + left.Freeze(); + brushes[(int)Placement.Left] = left; + var right = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(1d, 0d)); + right.Freeze(); + brushes[(int)Placement.Right] = right; + var bottom = new LinearGradientBrush(sideStops, new Point(0d, 0d), new Point(0d, 1d)); + bottom.Freeze(); + brushes[(int)Placement.Bottom] = bottom; + GradientStopCollection topLeftStops; + + if (cornerRadius.TopLeft == 0d) + topLeftStops = sideStops; + else + topLeftStops = CreateStops(c, cornerRadius.TopLeft); + + var topLeft = new RadialGradientBrush(topLeftStops) + { + RadiusX = 1d, + RadiusY = 1d, + Center = new Point(1d, 1d), + GradientOrigin = new Point(1d, 1d) + }; + topLeft.Freeze(); + brushes[(int)Placement.TopLeft] = topLeft; + GradientStopCollection topRightStops; + + if (cornerRadius.TopRight == 0d) + topRightStops = sideStops; + else if (cornerRadius.TopRight == cornerRadius.TopLeft) + topRightStops = topLeftStops; + else + topRightStops = CreateStops(c, cornerRadius.TopRight); + + var topRight = new RadialGradientBrush(topRightStops) + { + RadiusX = 1d, + RadiusY = 1d, + Center = new Point(0d, 1d), + GradientOrigin = new Point(0d, 1d) + }; + topRight.Freeze(); + brushes[(int)Placement.TopRight] = topRight; + GradientStopCollection bottomLeftStops; + + if (cornerRadius.BottomLeft == 0d) + bottomLeftStops = sideStops; + else if (cornerRadius.BottomLeft == cornerRadius.TopLeft) + bottomLeftStops = topLeftStops; + else if (cornerRadius.BottomLeft == cornerRadius.TopRight) + bottomLeftStops = topRightStops; + else + bottomLeftStops = CreateStops(c, cornerRadius.BottomLeft); + + var bottomLeft = new RadialGradientBrush(bottomLeftStops) + { + RadiusX = 1d, + RadiusY = 1d, + Center = new Point(1d, 0d), + GradientOrigin = new Point(1d, 0d) + }; + bottomLeft.Freeze(); + brushes[(int)Placement.BottomLeft] = bottomLeft; + GradientStopCollection bottomRightStops; + + if (cornerRadius.BottomRight == 0d) + bottomRightStops = sideStops; + else if (cornerRadius.BottomRight == cornerRadius.TopLeft) + bottomRightStops = topLeftStops; + else if (cornerRadius.BottomRight == cornerRadius.TopRight) + bottomRightStops = topRightStops; + else if (cornerRadius.BottomRight == cornerRadius.BottomLeft) + bottomRightStops = bottomLeftStops; + else + bottomRightStops = CreateStops(c, cornerRadius.BottomRight); + + var bottomRight = new RadialGradientBrush(bottomRightStops) + { + RadiusX = 1d, + RadiusY = 1d, + Center = new Point(0d, 0d), + GradientOrigin = new Point(0d, 0d) + }; + bottomRight.Freeze(); + brushes[(int)Placement.BottomRight] = bottomRight; + return brushes; + } + + private Brush[] GetBrushes(Color c, CornerRadius cornerRadius) + { + if (_commonBrushes is null) + lock (_resourceAccess) + { + if (_commonBrushes is null) + { + _commonBrushes = CreateBrushes(c, cornerRadius); + _commonCornerRadius = cornerRadius; + } + } + + if (c == ((SolidColorBrush)_commonBrushes[(int)Placement.Center]).Color && cornerRadius == _commonCornerRadius) + { + _brushes = null; + return _commonBrushes; + } + + if (_brushes is null) _brushes = CreateBrushes(c, cornerRadius); + + return _brushes; + } + + private enum Placement + { + TopLeft = 0, + Top = 1, + TopRight = 2, + Left = 3, + Center = 4, + Right = 5, + BottomLeft = 6, + Bottom = 7, + BottomRight = 8 + } +} + +// 参考自:https://referencesource.microsoft.com/#PresentationFramework.Aero/parent/Shared/Microsoft/Windows/Themes/SystemDropShadowChrome.cs,6d9c27d92a8128c1 \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml index f8a0d88ca..2dfd00789 100644 --- a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml @@ -1,30 +1,40 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d" + x:Class="PCL.MyExtraButton" + Width="40" RenderTransformOrigin="0.5,0.5" ToolTipService.Placement="Left" ToolTipService.VerticalOffset="16" + ToolTipService.HorizontalOffset="-8" Height="0"> - + - + - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs new file mode 100644 index 000000000..f8d6328da --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.cs @@ -0,0 +1,311 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class MyExtraButton +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件 + + public delegate void RightClickEventHandler(object sender, MouseButtonEventArgs e); + + public delegate bool ShowCheckDelegate(); + + // 自定义事件 + // 务必放在 IsMouseDown 更新之后 + private const int AnimationColorIn = 120; + private const int AnimationColorOut = 150; + private string _Logo = ""; + private double _LogoScale = 1d; + + // 进度条 + private double _Progress; + private bool _Show; + + // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行) + private bool IsLeftMouseHeld; + private bool IsRightMouseHeld; + public ShowCheckDelegate ShowCheck = null; + + // 自定义属性 + public int Uuid = ModBase.GetUuid(); + + public MyExtraButton() + { + Loaded += (_, _) => RefreshColor(); + IsEnabledChanged += (_, _) => RefreshColor(); + InitializeComponent(); + PanClick.MouseLeave += (_, _) => Button_MouseLeave(); + } + + public double Progress + { + get => _Progress; + set + { + if (_Progress == value) + return; + _Progress = value; + if (value < 0.0001d) + { + PanProgress.Visibility = Visibility.Collapsed; + } + else + { + PanProgress.Visibility = Visibility.Visible; + RectProgress.Rect = new Rect(0d, 40d * (1d - value), 40d, 40d * value); + } + } + } + + public string Logo + { + get => _Logo; + set + { + if ((value ?? "") == (_Logo ?? "")) + return; + _Logo = value; + Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value); + } + } + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (!(Path == null)) + Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + public bool Show + { + get => _Show; + set + { + if (_Show == value) + return; + _Show = value; + ModBase.RunInUi(() => + { + if (value) + { + // 有了 + Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(this, 0.3d - ((ScaleTransform)RenderTransform).ScaleX, 500, + 60, new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaScaleTransform(this, 0.7d, 500, 60, + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaHeight(this, 50d - Height, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyExtraButton MainScale " + Uuid); + } + else + { + // 没了 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaHeight(this, -Height, 400, 100, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => Visibility = Visibility.Collapsed, After: true) + }, "MyExtraButton MainScale " + Uuid); + } + + IsHitTestVisible = value; // 防止缩放动画中依然可以点进去 + }); + } + } + + public bool CanRightClick { get; set; } + + // 声明 + public event ClickEventHandler? Click; + public event RightClickEventHandler? RightClick; + + public void ShowRefresh() + { + if (ShowCheck is not null) + Show = ShowCheck(); + } + + // 触发点击事件 + private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e) + { + if (IsLeftMouseHeld) + { + ModBase.Log("[Control] 按下附加按钮" + + (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(ToolTip, "", false)) + ? "" + : ":" + ToolTip)); + Click?.Invoke(sender, e); + e.Handled = true; + Button_LeftMouseUp(); + } + } + + private void Button_RightMouseUp(object sender, MouseButtonEventArgs e) + { + if (IsRightMouseHeld) + { + ModBase.Log("[Control] 右键按下附加按钮" + + (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(ToolTip, "", false)) + ? "" + : ":" + ToolTip)); + RightClick?.Invoke(sender, e); + e.Handled = true; + Button_RightMouseUp(); + } + } + + private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e) + { + if (!IsLeftMouseHeld && !IsRightMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, + 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyExtraButton Scale " + Uuid); + IsLeftMouseHeld = true; + Focus(); + } + + private void Button_RightMouseDown(object sender, MouseButtonEventArgs e) + { + if (!CanRightClick) + return; + if (!IsLeftMouseHeld && !IsRightMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, + 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyExtraButton Scale " + Uuid); + IsRightMouseHeld = true; + Focus(); + } + + private void Button_LeftMouseUp() + { + if (!IsRightMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300, + Ease: new ModAnimation.AniEaseOutBack()) + }, "MyExtraButton Scale " + Uuid); + IsLeftMouseHeld = false; + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + private void Button_RightMouseUp() + { + if (!CanRightClick) + return; + if (!IsLeftMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300, + Ease: new ModAnimation.AniEaseOutBack()) + }, "MyExtraButton Scale " + Uuid); + IsRightMouseHeld = false; + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + private void Button_MouseLeave() + { + IsLeftMouseHeld = false; + IsRightMouseHeld = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyExtraButton Scale " + Uuid); + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + public void RefreshColor() + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + if (!IsEnabled) + // 禁用 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", AnimationColorIn), + "MyExtraButton Color " + Uuid); + else if (IsMouseOver) + // 指向 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", AnimationColorIn), + "MyExtraButton Color " + Uuid); + else + // 普通 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut), + "MyExtraButton Color " + Uuid); + } + + else + { + ModAnimation.AniStop("MyExtraButton Color " + Uuid); + if (!IsEnabled) + PanColor.SetResourceReference(BackgroundProperty, "ColorBrushGray4"); + else if (IsMouseOver) + PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4"); + else + PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新图标按钮颜色出错"); + } + } + + /// + /// 发出一圈波浪效果提示。 + /// + public void Ribble() + { + ModBase.RunInUi(() => + { + var Shape = new Border + { + CornerRadius = new CornerRadius(1000d), BorderThickness = new Thickness(0.001d), Opacity = 0.5d, + RenderTransformOrigin = new Point(0.5d, 0.5d), RenderTransform = new ScaleTransform() + }; + Shape.SetResourceReference(Border.BackgroundProperty, "ColorBrush5"); + PanScale.Children.Insert(0, Shape); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(Shape, 13d, 1000, + Ease: new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Strong, 0.3d)), + ModAnimation.AaOpacity(Shape, -Shape.Opacity, 1000), + ModAnimation.AaCode(() => PanScale.Children.Remove(Shape), After: true) + }, "ExtraButton Ribble " + ModBase.GetUuid()); + }); + } + + private void PanClick_MouseEvent(object sender, MouseEventArgs e) + { + RefreshColor(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml index 795a7bd9d..d09b57c6f 100644 --- a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml @@ -1,15 +1,15 @@  - + @@ -21,12 +21,14 @@ - + - diff --git a/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs new file mode 100644 index 000000000..d33330897 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyExtraTextButton.xaml.cs @@ -0,0 +1,231 @@ +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyExtraTextButton +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); // 自定义事件 + + // 自定义事件 + // 务必放在 IsMouseDown 更新之后 + private const int AnimationColorIn = 120; + private const int AnimationColorOut = 150; + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyExtraTextButton), new PropertyMetadata((sender, e) => + { + if (sender is not null) ((MyExtraTextButton)sender).LabText.Text = Conversions.ToString(e.NewValue); + })); + + private string _Logo = ""; + private double _LogoScale = 1d; + + // 动画 + private bool _Show; + + // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行) + private bool IsLeftMouseHeld; + + // 自定义属性 + public int Uuid = ModBase.GetUuid(); + + public MyExtraTextButton() + { + InitializeComponent(); + + Loaded += (_, __) => RefreshColor(); + IsEnabledChanged += (_, __) => RefreshColor(); + PanClick.MouseLeftButtonDown += Button_LeftMouseDown; + PanClick.MouseLeftButtonUp += Button_LeftMouseUp; + PanClick.MouseLeave += Button_MouseLeave; + PanClick.MouseRightButtonUp += Button_RightMouseUp; + PanClick.MouseEnter += (sender, e) => RefreshColor(); + } + + public string Logo + { + get => _Logo; + set + { + if ((value ?? "") == (_Logo ?? "")) + return; + _Logo = value; + Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value); + } + } + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (Path is not null) + Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + // 显示文本 + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set + { + if (value == null) return; + SetValue(TextProperty, value); + } + } + + public bool Show + { + get => _Show; + set + { + if (_Show == value) + return; + _Show = value; + ModBase.RunInUi(() => + { + if (value) + { + // 有了 + Opacity = 0d; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d - Opacity, 80, 50), + ModAnimation.AaScaleTransform(this, 0.15d - ((ScaleTransform)RenderTransform).ScaleX, 400, + 50, new ModAnimation.AniEaseOutBack()), + ModAnimation.AaScaleTransform(this, 0.85d, 160, 50, new ModAnimation.AniEaseOutFluent()) + }, "MyExtraTextButton MainScale " + Uuid); + } + else + { + // 没了 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, -Opacity, 50, 50), + ModAnimation.AaScaleTransform(this, -((ScaleTransform)RenderTransform).ScaleX, 100, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)) + }, "MyExtraTextButton MainScale " + Uuid); + } + + IsHitTestVisible = value; // 防止缩放动画中依然可以点进去 + }); + } + } + + // 声明 + public event ClickEventHandler? Click; + + // 触发点击事件 + private void Button_LeftMouseUp(object sender, MouseButtonEventArgs e) + { + if (IsLeftMouseHeld) + { + ModBase.Log("[Control] 按下附加图标按钮:" + Text); + Click?.Invoke(sender, e); + e.Handled = true; + Button_LeftMouseUp(); + } + } + + private void Button_LeftMouseDown(object sender, MouseButtonEventArgs e) + { + if (!IsLeftMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 0.85d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, + 800, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaScaleTransform(PanScale, -0.05d, 60, Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyExtraTextButton Scale " + Uuid); + IsLeftMouseHeld = true; + Focus(); + } + + private void Button_LeftMouseUp() + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300, + Ease: new ModAnimation.AniEaseOutBack()) + }, "MyExtraTextButton Scale " + Uuid); + IsLeftMouseHeld = false; + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + private void Button_RightMouseUp(object sender, MouseEventArgs e) + { + if (!IsLeftMouseHeld) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 300, + Ease: new ModAnimation.AniEaseOutBack()) + }, "MyExtraTextButton Scale " + Uuid); + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + private void Button_MouseLeave(object sender, MouseEventArgs e) + { + IsLeftMouseHeld = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanScale, 1d - ((ScaleTransform)PanScale.RenderTransform).ScaleX, 500, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyExtraTextButton Scale " + Uuid); + RefreshColor(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + public void RefreshColor() + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + if (!IsEnabled) + // 禁用 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrushGray4", AnimationColorIn), + "MyExtraTextButton Color " + Uuid); + else if (IsMouseOver) + // 指向 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush4", AnimationColorIn), + "MyExtraTextButton Color " + Uuid); + else + // 普通 + ModAnimation.AniStart( + ModAnimation.AaColor(PanColor, BackgroundProperty, "ColorBrush3", AnimationColorOut), + "MyExtraTextButton Color " + Uuid); + } + + else + { + ModAnimation.AniStop("MyExtraTextButton Color " + Uuid); + if (!IsEnabled) + PanColor.SetResourceReference(BackgroundProperty, "ColorBrushGray4"); + else if (IsMouseOver) + PanColor.SetResourceReference(BackgroundProperty, "ColorBrush4"); + else + PanColor.SetResourceReference(BackgroundProperty, "ColorBrush3"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新附加图标按钮颜色出错"); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyHint.xaml b/Plain Craft Launcher 2/Controls/MyHint.xaml index 6395c28ef..b4dff47e9 100644 --- a/Plain Craft Launcher 2/Controls/MyHint.xaml +++ b/Plain Craft Launcher 2/Controls/MyHint.xaml @@ -1,16 +1,21 @@ - + - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyHint.xaml.cs b/Plain Craft Launcher 2/Controls/MyHint.xaml.cs new file mode 100644 index 000000000..56b28b95f --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyHint.xaml.cs @@ -0,0 +1,273 @@ +using System.Windows; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Markup; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.UI.Theme; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyHint +{ + // 配色 + public enum Themes + { + Blue = 0, + Red = 1, + Yellow = 2 + } + + public static readonly DependencyProperty IsWarnProperty = DependencyProperty.Register("IsWarn", typeof(bool), + typeof(MyHint), + new PropertyMetadata(true, + (d, e) => + { + var f = (MyHint)d; + f.Theme = e.NewValue != null ? Themes.Red : Themes.Blue; + })); + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyHint), new PropertyMetadata("", (d, e) => + { + dynamic f = d; + f.LabText.Text = Conversions.ToString(e.NewValue); + })); + + public static readonly DependencyProperty EventTypeProperty = + DependencyProperty.Register("EventType", typeof(string), typeof(MyHint), new PropertyMetadata(null)); + + public static readonly DependencyProperty EventDataProperty = + DependencyProperty.Register("EventData", typeof(string), typeof(MyHint), new PropertyMetadata(null)); + + private Themes _ColorType = Themes.Red; + + // 触发点击事件 + private bool IsMouseDown; + public int Uuid = ModBase.GetUuid(); + + public MyHint() + { + InitializeComponent(); + UpdateUI(); + Loaded += (_, __) => UpdateUI(); + Loaded += MyHint_Loaded; + MouseLeftButtonUp += MyHint_MouseUp; + MouseLeftButtonDown += MyHint_MouseDown; + MouseLeave += (_, __) => MyHint_MouseLeave(); + Unloaded += (_, __) => Dispose(); + } + + // 边框 + public bool HasBorder + { + get => BorderThickness.Top > 0d; + set + { + if (value) + BorderThickness = new Thickness(3d, ModBase.GetWPFSize(1d), ModBase.GetWPFSize(1d), + ModBase.GetWPFSize(1d)); + else + BorderThickness = new Thickness(3d, 0d, 0d, 0d); + } + } + + public Themes Theme + { + get => _ColorType; + set + { + _ColorType = value; + UpdateUI(); + } + } + + [Obsolete("IsWarn 已过时。请换用 Theme 属性。")] + public bool IsWarn + { + get => Theme == Themes.Red; + set => Theme = value ? Themes.Red : Themes.Blue; + } + + // 文本 + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } + + // 关闭按钮 + public bool CanClose + { + get => BtnClose.Visibility == Visibility.Visible; + set => BtnClose.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + } + + public string RelativeSetup { get; set; } = ""; + + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + private void UpdateUI() + { + var hue = default(double); + switch (Theme) + { + case Themes.Blue: + { + hue = 210d; + break; + } + case Themes.Red: + { + hue = 355d; + break; + } + case Themes.Yellow: + { + hue = 40d; + break; + } + } + + var s = ThemeService.CurrentTone; + Background = new ModBase.MyColor().FromHSL2(hue, 90, s.L7 * 100); + BorderBrush = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100); + LabText.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100); + BtnClose.Foreground = new ModBase.MyColor().FromHSL2(hue, 90, s.L2 * 100); + } + + private void MyHint_Loaded(object sender, RoutedEventArgs e) + { + ThemeService.ColorModeChanged += (v, theme) => _ThemeChanged(v, theme); + if (CanClose && ModBase.Setup.Get(RelativeSetup) != null) + Visibility = Visibility.Collapsed; + } + + private void BtnClose_Click(object sender, EventArgs e) + { + ModBase.Setup.Set(RelativeSetup, true); + ModAnimation.AniDispose(this, false); + } + + private void MyHint_MouseUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + IsMouseDown = false; + ModBase.Log("[Control] 按下提示条" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name)); + e.Handled = true; + ModEvent.TryStartEvent(EventType, EventData); + } + + private void MyHint_MouseDown(object sender, MouseButtonEventArgs e) + { + IsMouseDown = true; + } + + private void MyHint_MouseLeave() + { + IsMouseDown = false; + } + + private void _ThemeChanged(bool isDarkMode, ColorTheme theme) + { + UpdateUI(); + } + + private void Dispose() + { + ThemeService.ColorModeChanged -= _ThemeChanged; + } + + // Private Sub SetStyle() + // If Type = HintType.Note Then + // If IsWarn Then + // BorderBrush = New MyColor("#CCFF4444") + // Gradient1.Color = New MyColor(CType(If(IsDarkMode, "#BBFF8888", "#BBFFBBBB"), String)) + // Gradient2.Color = New MyColor(CType(If(IsDarkMode, "#BBFF6666", "#BBFF8888"), String)) + // Path.Fill = New MyColor("#BF0000") + // LabText.Foreground = New MyColor("#BF0000") + // BtnClose.Foreground = New MyColor("#BF0000") + // Path.Data = (New GeometryConverter).ConvertFromString("F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z ") + // Return + // Else + // BorderBrush = New MyColor("#CC4D76FF") + // Gradient1.Color = New MyColor("#BBB0D0FF") + // Gradient2.Color = New MyColor("#BB9EBAFF") + // Path.Fill = New MyColor("#0062BF") + // LabText.Foreground = New MyColor("#0062BF") + // BtnClose.Foreground = New MyColor("#0062BF") + // Path.Data = (New GeometryConverter).ConvertFromString("F1M38,19C48.4934,19 57,27.5066 57,38 57,48.4934 48.4934,57 38,57 27.5066,57 19,48.4934 19,38 19,27.5066 27.5066,19 38,19z M33.25,33.25L33.25,36.4167 36.4166,36.4167 36.4166,47.5 33.25,47.5 33.25,50.6667 44.3333,50.6667 44.3333,47.5 41.1666,47.5 41.1666,36.4167 41.1666,33.25 33.25,33.25z M38.7917,25.3333C37.48,25.3333 36.4167,26.3967 36.4167,27.7083 36.4167,29.02 37.48,30.0833 38.7917,30.0833 40.1033,30.0833 41.1667,29.02 41.1667,27.7083 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333z") + // Return + // End If + // End If + + // Select Case Type + // Case HintType.Warning + // BorderBrush = New MyColor("#CCE69900") + // Gradient1.Color = New MyColor("#BBFFF4CE") + // Gradient2.Color = New MyColor("#BBFFF5CE") + // Path.Fill = New MyColor("#957500") + // LabText.Foreground = New MyColor("#957500") + // BtnClose.Foreground = New MyColor("#957500") + // Path.Data = (New GeometryConverter).ConvertFromString("F1 M 58.5832,55.4172L 17.4169,55.4171C 15.5619,53.5621 15.5619,50.5546 17.4168,48.6996L 35.201,15.8402C 37.056,13.9852 40.0635,13.9852 41.9185,15.8402L 58.5832,48.6997C 60.4382,50.5546 60.4382,53.5622 58.5832,55.4172 Z M 34.0417,25.7292L 36.0208,41.9584L 39.9791,41.9583L 41.9583,25.7292L 34.0417,25.7292 Z M 38,44.3333C 36.2511,44.3333 34.8333,45.7511 34.8333,47.5C 34.8333,49.2489 36.2511,50.6667 38,50.6667C 39.7489,50.6667 41.1666,49.2489 41.1666,47.5C 41.1666,45.7511 39.7489,44.3333 38,44.3333 Z ") + // Return + // Case HintType.Caution + // BorderBrush = New MyColor("#CCFF4444") + // Gradient1.Color = New MyColor(CType(If(IsDarkMode, "#BBFF8888", "#BBFFBBBB"), String)) + // Gradient2.Color = New MyColor(CType(If(IsDarkMode, "#BBFF6666", "#BBFF8888"), String)) + // Path.Fill = New MyColor("#BF0000") + // LabText.Foreground = New MyColor("#BF0000") + // BtnClose.Foreground = New MyColor("#BF0000") + // Path.Data = (New GeometryConverter).ConvertFromString("F1 M1024,1024z M0,0z M512,0C229.23,0 0,229.23 0,512 0,794.77 229.23,1024 512,1024 794.768,1024 1024,794.77 1024,512 1024,229.23 794.77,0 512,0z M746.76,656.252C754.568,664.06,754.566,676.724,746.762,684.536L684.534,746.76C676.726,754.568,664.064,754.574,656.248,746.762L512,602.51 367.75,746.76C359.94,754.572,347.276,754.568,339.466,746.76L277.24,684.536C269.43,676.728,269.428,664.064,277.24,656.252L421.492,512 277.242,367.75C269.432,359.942,269.432,347.276,277.242,339.466L339.468,277.242C347.278,269.43,359.942,269.432,367.752,277.242L512,421.49 656.252,277.24C664.058,269.428,676.722,269.43,684.534,277.24L746.76,339.464C754.566,347.276,754.568,359.938,746.76,367.748L602.51,512 746.76,656.252z") + // Return + // Case Else + // BorderBrush = New MyColor("#CC4D76FF") + // Gradient1.Color = New MyColor("#BBB0D0FF") + // Gradient2.Color = New MyColor("#BB9EBAFF") + // Path.Fill = New MyColor("#0062BF") + // LabText.Foreground = New MyColor("#0062BF") + // BtnClose.Foreground = New MyColor("#0062BF") + // Path.Data = (New GeometryConverter).ConvertFromString("F1M38,19C48.4934,19 57,27.5066 57,38 57,48.4934 48.4934,57 38,57 27.5066,57 19,48.4934 19,38 19,27.5066 27.5066,19 38,19z M33.25,33.25L33.25,36.4167 36.4166,36.4167 36.4166,47.5 33.25,47.5 33.25,50.6667 44.3333,50.6667 44.3333,47.5 41.1666,47.5 41.1666,36.4167 41.1666,33.25 33.25,33.25z M38.7917,25.3333C37.48,25.3333 36.4167,26.3967 36.4167,27.7083 36.4167,29.02 37.48,30.0833 38.7917,30.0833 40.1033,30.0833 41.1667,29.02 41.1667,27.7083 41.1667,26.3967 40.1033,25.3333 38.7917,25.3333z") + // Return + // End Select + // End Sub +} + +public static partial class ModAnimation +{ + public static void AniDispose(MyHint Control, bool RemoveFromChildren, ParameterizedThreadStart CallBack = null) + { + if (!Control.IsHitTestVisible) + return; + Control.IsHitTestVisible = false; + AniStart(new[] + { + AaScaleTransform(Control, -0.08d, 200, Ease: new AniEaseInFluent()), + AaOpacity(Control, -1, 200, Ease: new AniEaseOutFluent()), + AaHeight(Control, -Control.ActualHeight, 150, 100, new AniEaseOutFluent()), + AaCode(() => + { + if (RemoveFromChildren) + ((dynamic)Control.Parent).Children.Remove(Control); + else + Control.Visibility = Visibility.Collapsed; + if (CallBack is not null) + CallBack(Control); + }, After: true) + }, "MyCard Dispose " + Control.Uuid); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyIconButton.xaml b/Plain Craft Launcher 2/Controls/MyIconButton.xaml index 5b4d8a080..ff7f5f228 100644 --- a/Plain Craft Launcher 2/Controls/MyIconButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyIconButton.xaml @@ -1,8 +1,9 @@ - - + + @@ -12,4 +13,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs new file mode 100644 index 000000000..26094b4a6 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyIconButton.xaml.cs @@ -0,0 +1,352 @@ +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class MyIconButton +{ + public delegate void ClickEventHandler(object sender, EventArgs e); + + public enum Themes + { + Color, + White, + Black, + Red, + Custom + } + + // 自定义事件 + // 务必放在 IsMouseDown 更新之后 + private const int AnimationColorIn = 120; + private const int AnimationColorOut = 150; + + public static readonly DependencyProperty EventTypeProperty = + DependencyProperty.Register("EventType", typeof(string), typeof(MyIconButton), new PropertyMetadata(null)); + + public static readonly DependencyProperty EventDataProperty = + DependencyProperty.Register("EventData", typeof(string), typeof(MyIconButton), new PropertyMetadata(null)); + + private SolidColorBrush _Foreground = new(Color.FromRgb(128, 128, 128)); + + private double _LogoScale = 1d; + + // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行) + private bool IsMouseDown; + + // 自定义属性 + + public int Uuid = ModBase.GetUuid(); + + public MyIconButton() + { + InitializeComponent(); + + MouseLeftButtonUp += Button_MouseUp; + MouseLeftButtonDown += Button_MouseDown; + MouseLeftButtonUp += (_, __) => Button_MouseUp(); + MouseLeave += (_, __) => Button_MouseLeave(); + MouseEnter += (_, __) => RefreshAnim(); + MouseLeave += (_, __) => RefreshAnim(); + Loaded += (_, __) => RefreshAnim(); + } + + public string Logo + { + get => Path.Data.ToString(); + set + { + if (Path == null) return; + Path.Data = (Geometry)new GeometryConverter().ConvertFromString(value); + } + } + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (!(Path == null)) + Path.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + public Themes Theme { get; set; } = Themes.Color; + + public SolidColorBrush Foreground + { + get => _Foreground; + set + { + _Foreground = value; + ModAnimation.AniControlEnabled += 1; + RefreshAnim(); + ModAnimation.AniControlEnabled -= 1; + } + } + + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + // 自定义事件 + public event ClickEventHandler? Click; + + // 触发点击事件 + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + ModBase.Log("[Control] 按下图标按钮" + (string.IsNullOrEmpty(Name) ? "" : ":" + Name)); + Click?.Invoke(sender, e); + e.Handled = true; + Button_MouseUp(); + ModEvent.TryStartEvent(EventType, EventData); + } + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + IsMouseDown = true; + Focus(); + // 指向 + ModAnimation.AniStart( + ModAnimation.AaScaleTransform(PanBack, 0.8d - ((ScaleTransform)PanBack.RenderTransform).ScaleX, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + "MyIconButton Scale " + Uuid); + } + + private void Button_MouseUp() + { + if (IsMouseDown) + { + IsMouseDown = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanBack, 1.05d - ((ScaleTransform)PanBack.RenderTransform).ScaleX, + 250, Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaScaleTransform(PanBack, -0.05d, 250, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)) + }, "MyIconButton Scale " + Uuid); + } + + RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + private void Button_MouseLeave() + { + IsMouseDown = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(PanBack, 1d - ((ScaleTransform)PanBack.RenderTransform).ScaleX, 250, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyIconButton Scale " + Uuid); + RefreshAnim(); // 直接刷新颜色以判断是否已触发 MouseLeave + } + + public void RefreshAnim() + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + if (PanBack.Background is null) + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + if (Path.Fill is null) + switch (Theme) + { + case Themes.Red: + { + Path.Fill = new ModBase.MyColor(160d, 255d, 76d, 76d); + break; + } + case Themes.Black: + { + if (ModSecret.IsDarkMode) + Path.Fill = new ModBase.MyColor(160d, 255d, 255d, 255d); + else + Path.Fill = new ModBase.MyColor(160d, 0d, 0d, 0d); + + break; + } + case Themes.Custom: + { + Path.Fill = new ModBase.MyColor(160d, Foreground); + break; + } + } + + if (IsMouseOver) + { + // 指向 + var AnimList = new List(); + switch (Theme) + { + case Themes.Color: + { + AnimList.Add( + ModAnimation.AaColor(Path, Shape.FillProperty, "ColorBrush2", AnimationColorIn)); + break; + } + case Themes.White: + { + AnimList.Add(ModAnimation.AaColor(PanBack, BackgroundProperty, + new ModBase.MyColor(50d, 255d, 255d, 255d) - PanBack.Background, AnimationColorIn)); + break; + } + case Themes.Red: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + new ModBase.MyColor(255d, 76d, 76d) - Path.Fill, AnimationColorIn)); + break; + } + case Themes.Black: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + (ModSecret.IsDarkMode + ? new ModBase.MyColor(230d, 255d, 255d, 255d) + : new ModBase.MyColor(230d, 0d, 0d, 0d)) - Path.Fill, AnimationColorIn)); + break; + } + case Themes.Custom: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + new ModBase.MyColor(255d, Foreground) - Path.Fill, AnimationColorIn)); + break; + } + } + + ModAnimation.AniStart(AnimList, "MyIconButton Color " + Uuid); + } + else + { + // 普通 + var AnimList = new List(); + switch (Theme) + { + case Themes.Color: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, "ColorBrush4", + AnimationColorOut)); + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + break; + } + case Themes.White: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + new ModBase.MyColor(234d, 242d, 254d), AnimationColorOut)); + AnimList.Add(ModAnimation.AaColor(PanBack, BackgroundProperty, + new ModBase.MyColor(0d, 255d, 255d, 255d) - PanBack.Background, AnimationColorOut)); + break; + } + case Themes.Red: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + new ModBase.MyColor(160d, 255d, 76d, 76d) - Path.Fill, AnimationColorOut)); + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + break; + } + case Themes.Black: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + (ModSecret.IsDarkMode + ? new ModBase.MyColor(160d, 255d, 255d, 255d) + : new ModBase.MyColor(160d, 0d, 0d, 0d)) - Path.Fill, AnimationColorOut)); + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + break; + } + case Themes.Custom: + { + AnimList.Add(ModAnimation.AaColor(Path, Shape.FillProperty, + new ModBase.MyColor(160d, Foreground) - Path.Fill, AnimationColorOut)); + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + break; + } + } + + ModAnimation.AniStart(AnimList, "MyIconButton Color " + Uuid); + } + } + + else + { + ModAnimation.AniStop("MyIconButton Color " + Uuid); + switch (Theme) + { + case Themes.Color: + { + Path.SetResourceReference(Shape.FillProperty, "ColorBrush5"); + break; + } + case Themes.White: + { + Path.Fill = new ModBase.MyColor(234d, 242d, 254d); + break; + } + case Themes.Red: + { + Path.Fill = new ModBase.MyColor(160d, 255d, 76d, 76d); + break; + } + case Themes.Black: + { + if (ModSecret.IsDarkMode) + Path.Fill = new ModBase.MyColor(160d, 255d, 255d, 255d); + else + Path.Fill = new ModBase.MyColor(160d, 0d, 0d, 0d); + + break; + } + case Themes.Custom: + { + Path.Fill = new ModBase.MyColor(160d, Foreground); + break; + } + } + + PanBack.Background = new ModBase.MyColor(0d, 255d, 255d, 255d); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新图标按钮动画状态出错"); + } + } +} + +public static partial class ModAnimation +{ + public static void AniDispose(MyIconButton Control, bool RemoveFromChildren, + ParameterizedThreadStart CallBack = null) + { + if (!Control.IsHitTestVisible) + return; + Control.IsHitTestVisible = false; + AniStart(new[] + { + AaScaleTransform(Control, -1.5d, 200, Ease: new AniEaseInFluent()), + AaCode(() => + { + if (RemoveFromChildren) + ((dynamic)Control.Parent).Children.Remove(Control); + else + Control.Visibility = Visibility.Collapsed; + if (CallBack is not null) + CallBack(Control); + }, After: true) + }, "MyIconButton Dispose " + Control.Uuid); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml index abd786d14..5dcb78359 100644 --- a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml @@ -1,9 +1,12 @@ - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs new file mode 100644 index 000000000..b4b0e432c --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.cs @@ -0,0 +1,312 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyIconTextButton +{ + public delegate void ChangeEventHandler(object sender, bool raiseByMouse); + + public delegate void CheckEventHandler(object sender, bool raiseByMouse); + + public delegate void ClickEventHandler(object sender, ModBase.RouteEventArgs e); + + public enum ColorState + { + Black, + Highlight + } + + // 动画 + + private const int AnimationTimeOfMouseIn = 100; // 鼠标指向动画长度 + private const int AnimationTimeOfMouseOut = 150; // 鼠标移出动画长度 + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyIconTextButton), new PropertyMetadata((sender, e) => + { + if (!(sender == null)) ((MyIconTextButton)sender).LabText.Text = Conversions.ToString(e.NewValue); + })); + + public static readonly DependencyProperty ColorTypeProperty = DependencyProperty.Register("ColorType", + typeof(ColorState), typeof(MyIconTextButton), new PropertyMetadata(ColorState.Black)); + + public static readonly DependencyProperty EventTypeProperty = DependencyProperty.Register("EventType", + typeof(string), typeof(MyIconTextButton), new PropertyMetadata(null)); + + public static readonly DependencyProperty EventDataProperty = DependencyProperty.Register("EventData", + typeof(string), typeof(MyIconTextButton), new PropertyMetadata(null)); + + private double _LogoScale = 1d; + private bool IsMouseDown; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyIconTextButton() + { + InitializeComponent(); + + MouseLeftButtonUp += (_, _) => MyIconTextButton_MouseUp(); + MouseLeftButtonDown += (_, _) => MyIconTextButton_MouseDown(); + MouseLeave += (_, _) => MyIconTextButton_MouseLeave(); + MouseEnter += RefreshColor; + Loaded += RefreshColor; + IsEnabledChanged += (_, _) => RefreshColor(); + } + + // 自定义属性 + + public string Logo + { + get => ShapeLogo.Data.ToString(); + set + { + if (ShapeLogo == null) return; + ShapeLogo.Data = (Geometry)new GeometryConverter().ConvertFromString(value); + } + } + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (!(ShapeLogo == null)) + ShapeLogo.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } // 内容 + + public ColorState ColorType + { + get => (ColorState)Conversions.ToInteger(GetValue(ColorTypeProperty)); + set + { + if (ColorType == value) + return; + SetValue(ColorTypeProperty, value); + RefreshColor(); + } + } // 颜色类别 + + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + public event CheckEventHandler? Check; + public event ChangeEventHandler? Change; + + public void RaiseChange() + { + Change?.Invoke(this, false); + } // 使外部程序可以引发本控件的 Change 事件 + + // 点击事件 + + public event ClickEventHandler? Click; + + private void MyIconTextButton_MouseUp() + { + if (!IsMouseDown) + return; + ModBase.Log("[Control] 按下带图标按钮:" + Text); + IsMouseDown = false; + Click?.Invoke(this, new ModBase.RouteEventArgs(true)); + ModEvent.TryStartEvent(EventType, EventData); + RefreshColor(); + } + + private void MyIconTextButton_MouseDown() + { + IsMouseDown = true; + RefreshColor(); + } + + private void MyIconTextButton_MouseLeave() + { + IsMouseDown = false; + RefreshColor(); + } + + private void RefreshColor(object obj = null, object e = null) + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0 && + !false.Equals(e)) // 防止默认属性变更触发动画,若强制不执行动画,则 e 为 False + { + switch (ColorType) + { + case ColorState.Black: + { + if (IsMouseDown) + { + // 按下 + ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", 70), + "MyIconTextButton Color " + Uuid); + } + else if (IsMouseOver) + { + // 指向 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3", + AnimationTimeOfMouseIn), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", + AnimationTimeOfMouseIn) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn), + "MyIconTextButton Color " + Uuid); + } + else if (IsEnabled) + { + // 正常 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush1", + AnimationTimeOfMouseOut), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush1", + AnimationTimeOfMouseOut) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut), + "MyIconTextButton Color " + Uuid); + } + else + { + // 禁用 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrushGray5", 100), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut), + "MyIconTextButton Color " + Uuid); + } + + break; + } + case ColorState.Highlight: + { + if (IsMouseDown) + { + // 按下 + ModAnimation.AniStart(ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", 70), + "MyIconTextButton Color " + Uuid); + } + else if (IsMouseOver) + { + // 指向 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3", + AnimationTimeOfMouseIn), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", + AnimationTimeOfMouseIn) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn), + "MyIconTextButton Color " + Uuid); + } + else if (IsEnabled) + { + // 正常 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3", + AnimationTimeOfMouseOut), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", + AnimationTimeOfMouseOut) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut), + "MyIconTextButton Color " + Uuid); + } + else + { + // 禁用 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrushGray5", 100), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100) + }, "MyIconTextButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + ModSecret.ColorSemiTransparent - Background, AnimationTimeOfMouseOut), + "MyIconTextButton Color " + Uuid); + } + + break; + } + } + } + + else + { + // 不使用动画 + ModAnimation.AniStop("MyIconTextButton Checked " + Uuid); + ModAnimation.AniStop("MyIconTextButton Color " + Uuid); + switch (ColorType) + { + case ColorState.Black: + { + Background = ModSecret.ColorSemiTransparent; + ShapeLogo.SetResourceReference(Shape.FillProperty, + IsEnabled ? "ColorBrush1" : "ColorBrushGray5"); + LabText.SetResourceReference(TextBlock.ForegroundProperty, + IsEnabled ? "ColorBrush1" : "ColorBrushGray5"); + break; + } + case ColorState.Highlight: + { + Background = ModSecret.ColorSemiTransparent; + ShapeLogo.SetResourceReference(Shape.FillProperty, + IsEnabled ? "ColorBrush3" : "ColorBrushGray5"); + LabText.SetResourceReference(TextBlock.ForegroundProperty, + IsEnabled ? "ColorBrush3" : "ColorBrushGray5"); + break; + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新带图标按钮颜色出错"); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyImage.cs b/Plain Craft Launcher 2/Controls/MyImage.cs new file mode 100644 index 000000000..4fbb09808 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyImage.cs @@ -0,0 +1,238 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Utils; + +namespace PCL; + +public class MyImage : Image +{ + private string _ActualSource; + + public MyImage() + { + Initialized += (_, __) => Load(); + } + + /// + /// 实际被呈现的图片地址。 + /// + public string ActualSource + { + get => _ActualSource; + set + { + if (string.IsNullOrEmpty(value)) + value = null; + if ((_ActualSource ?? "") == (value ?? "")) + return; + _ActualSource = value; + Dispatcher.BeginInvoke(new Func(async () => + { + try + { + ImageSource bitmap = value is null ? null : await Task.Run(() => new MyBitmap(value)); + base.Source = bitmap; + } + catch (Exception ex) + { + ModBase.Log(ex, $"加载图片失败({value})"); + try + { + if (value.StartsWithF(ModBase.PathTemp) && File.Exists(value)) File.Delete(value); + } + catch + { + } + } + })); // 在这里先触发可能的文件读取,尽量避免在 UI 线程中读取文件 + // ignored + } + } + + private async void Load() // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868) + { + // 空 + if (Source is null) + { + ActualSource = null; + return; + } + + // 本地图片 + if (!Source.StartsWithF("http")) + { + ActualSource = Source; + return; + } + + // 从缓存加载网络图片 + var Url = Source; + var TempPath = GetTempPath(Url); + var TempFile = new FileInfo(TempPath); + var EnableCache = this.EnableCache; + if (EnableCache && TempFile.Exists) + { + ActualSource = TempPath; + if (DateTime.Now - TempFile.LastWriteTime < FileCacheExpiredTime) + return; // 无需刷新缓存 + } + + string TempDownloadingPath = null; + try + { + // 下载 + ActualSource = LoadingSource; // 显示加载中图片 + TempDownloadingPath = TempPath + RandomUtils.NextInt(0, 1000000); + Directory.CreateDirectory(ModBase.GetPathFromFullPath(TempPath)); // 重新实现下载,以避免携带 Header(#5072) + using (var fs = new FileStream(TempDownloadingPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read)) + { + using (var response = await HttpRequestBuilder.Create(Url, HttpMethod.Get) + .WithHttpVersionOption(HttpVersion.Version30).WithDefaultHeaderOption(false).SendAsync()) + { + if (response.IsSuccess) + { + using (var nfs = await response.AsStreamAsync()) + { + fs.SetLength(0L); + await nfs.CopyToAsync(fs); + } + } + else if (!string.IsNullOrWhiteSpace(FallbackSource)) + { + using (var fallbackResponse = await HttpRequestBuilder.Create(FallbackSource, HttpMethod.Get) + .WithHttpVersionOption(HttpVersion.Version30).WithDefaultHeaderOption(false) + .SendAsync(true)) + { + if (fallbackResponse.IsSuccess) + using (var fallbackNfs = await fallbackResponse.AsStreamAsync()) + { + fs.SetLength(0L); + await fallbackNfs.CopyToAsync(fs); + } + } + } + else + { + return; + } + } + } + + if ((Url ?? "") != (Source ?? "") && (Url ?? "") != (FallbackSource ?? "")) + { + // 已经更换了地址 + File.Delete(TempDownloadingPath); + } + else if (EnableCache) + { + // 保存缓存并显示 + if (File.Exists(TempPath)) + File.Delete(TempPath); + File.Move(TempDownloadingPath, TempPath, true); + ActualSource = TempPath; + } + else + { + // 直接显示 + ActualSource = TempDownloadingPath; + } + } + catch (Exception ex) + { + try + { + if (TempPath is not null && File.Exists(TempPath)) + File.Delete(TempPath); + if (TempDownloadingPath is not null && File.Exists(TempDownloadingPath)) + File.Delete(TempDownloadingPath); + } + catch + { + } + + // 更换备用地址 + ModBase.Log(ex, $"下载图片失败(Base = {Url}, Fallback = {FallbackSource})", ModBase.LogLevel.Developer); + // 从缓存加载网络图片 + TempPath = GetTempPath(Url); + TempFile = new FileInfo(TempPath); + if (EnableCache && TempFile.Exists) + { + ActualSource = TempPath; + if (DateTime.Now - TempFile.LastWriteTime < FileCacheExpiredTime) + return; // 无需刷新缓存 + } + } + } + + public static string GetTempPath(string Url) + { + return Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{ModBase.GetStringMD5(Url)}.png"); + } + + #region 公开属性 + + /// + /// 网络图片的缓存有效期。 + /// 在这个时间后,才会重新尝试下载图片。 + /// + public TimeSpan FileCacheExpiredTime = TimeSpan.FromDays(14d); + + /// + /// 是否允许将网络图片存储到本地用作缓存。 + /// + public bool EnableCache + { + get => Conversions.ToBoolean(GetValue(EnableCacheProperty)); + set => SetValue(EnableCacheProperty, value); + } + + public new static readonly DependencyProperty EnableCacheProperty = + DependencyProperty.Register("EnableCache", typeof(bool), typeof(MyImage), new PropertyMetadata(true)); + + /// + /// 与 Image 的 Source 类似。 + /// 若输入以 http 开头的字符串,则会尝试下载图片然后显示,图片会保存为本地缓存。 + /// 支持 WebP 格式的图片。 + /// + public new string Source // 覆写 Image 的 Source 属性 + { + get => _Source; + set + { + if (string.IsNullOrEmpty(value)) + value = null; + if ((_Source ?? "") == (value ?? "")) + return; + _Source = value; + if (!IsInitialized) + return; // 属性读取顺序修正:在完成 XAML 属性读取后再触发图片加载(#4868) + Load(); + } + } + + private string _Source = ""; + + public new static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(string), + typeof(MyImage), new PropertyMetadata((sender, e) => + { + if (sender is not null) ((MyImage)sender).Source = e.NewValue.ToString(); + })); + + /// + /// 当 Source 首次下载失败时,会从该备用地址加载图片。 + /// + public string FallbackSource { get; set; } + + /// + /// 正在下载网络图片时显示的本地图片。 + /// + public string LoadingSource { get; set; } = "pack://application:,,,/images/Icons/NoIcon.png"; + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyListItem.xaml b/Plain Craft Launcher 2/Controls/MyListItem.xaml index 19b54c455..278fd5b5b 100644 --- a/Plain Craft Launcher 2/Controls/MyListItem.xaml +++ b/Plain Craft Launcher 2/Controls/MyListItem.xaml @@ -1,17 +1,17 @@ - + - + - - + + @@ -22,7 +22,11 @@ - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs b/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs new file mode 100644 index 000000000..ff5ba70cc --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyListItem.xaml.cs @@ -0,0 +1,1010 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyListItem : IMyRadio +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); + + public delegate void LogoClickEventHandler(object sender, MouseButtonEventArgs e); + + public bool IsMouseOverAnimationEnabled = true; + + private string StateLast; + + public object tag { get; set; } + public event IMyRadio.CheckEventHandler? Check; + public event IMyRadio.ChangedEventHandler? Changed; + + public event ClickEventHandler? Click; + public event LogoClickEventHandler? LogoClick; + + public void RefreshColor(object sender, EventArgs e) + { + // 菜单虚拟化检测 + if (ContentHandler is not null) + { + ContentHandler.Invoke(this, e); + ContentHandler = null; + } + + // 判断当前颜色 + string StateNew; + int Time; + if (IsMouseDown && !(Type == CheckType.RadioBox && Checked)) + { + StateNew = "MouseDown"; + Time = 120; + } + else if (IsMouseOver && IsMouseOverAnimationEnabled) + { + StateNew = "MouseOver"; + Time = 120; + } + else + { + StateNew = "Idle"; + Time = 180; + } + + if ((StateLast ?? "") == (StateNew ?? "")) + return; + StateLast = StateNew; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + var Ani = new List(); + if (IsMouseOver && IsMouseOverAnimationEnabled) + { + if (ButtonStack is not null) + { + Ani.Add(ModAnimation.AaOpacity(ButtonStack, 1d - ButtonStack.Opacity, (int)Math.Round(Time * 0.7d), + (int)Math.Round(Time * 0.3d))); + Ani.Add(ModAnimation.AaDouble( + i => ColumnPaddingRight.Width = + new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)), + Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25) - ColumnPaddingRight.Width.Value, + (int)Math.Round(Time * 0.3d), (int)Math.Round(Time * 0.7d))); + } + + Ani.AddRange(new[] + { + ModAnimation.AaColor(RectBack, Border.BackgroundProperty, + IsMouseDown ? "ColorBrush6" : "ColorBrushBg1", Time), + ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, Time, + Ease: new ModAnimation.AniEaseOutFluent()) + }); + if (IsScaleAnimationEnabled) + { + Ani.Add(ModAnimation.AaScaleTransform(RectBack, + 1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(Time * 1.6d), + Ease: new ModAnimation.AniEaseOutFluent())); + if (IsMouseDown) + Ani.Add(ModAnimation.AaScaleTransform(this, 0.98d - ((ScaleTransform)RenderTransform).ScaleX, + (int)Math.Round(Time * 0.9d), Ease: new ModAnimation.AniEaseOutFluent())); + else + Ani.Add(ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, + (int)Math.Round(Time * 1.2d), Ease: new ModAnimation.AniEaseOutFluent())); + } + } + else + { + if (ButtonStack is not null) + { + Ani.Add(ModAnimation.AaOpacity(ButtonStack, -ButtonStack.Opacity, (int)Math.Round(Time * 0.4d))); + Ani.Add(ModAnimation.AaDouble( + i => ColumnPaddingRight.Width = + new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)), + MinPaddingRight - ColumnPaddingRight.Width.Value, (int)Math.Round(Time * 0.4d))); + } + + Ani.Add(ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, Time)); + if (IsScaleAnimationEnabled) + Ani.AddRange(new[] + { + ModAnimation.AaColor(RectBack, Border.BackgroundProperty, + IsMouseDown ? "ColorBrush6" : "ColorBrush7", Time), + ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, Time * 3, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaScaleTransform(RectBack, + 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, Time, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaScaleTransform(RectBack, -0.246d, 1, After: true) + }); + } + + ModAnimation.AniStart(Ani, "ListItem Color " + Uuid); + } + else + { + // 无动画 + if (IsMouseOver && IsMouseOverAnimationEnabled) + { + if (ButtonStack is not null) + { + ButtonStack.Opacity = 1d; + ColumnPaddingRight.Width = new GridLength(Math.Max(MinPaddingRight, 5 + Buttons.Count() * 25)); + } + + // 由于鼠标已经移入,所以直接实例化 RectBack + RectBack.Background = (Brush)ModSecret.AppResources["ColorBrushBg1"]; + RectBack.Opacity = 1d; + RectBack.RenderTransform = new ScaleTransform(1d, 1d); + RenderTransform = new ScaleTransform(1d, 1d); + } + else + { + if (ButtonStack is not null) + { + ButtonStack.Opacity = 0d; + ColumnPaddingRight.Width = new GridLength(MinPaddingRight); + } + + RenderTransform = new ScaleTransform(1d, 1d); + if (_RectBack is not null) + { + if (IsScaleAnimationEnabled) + RectBack.RenderTransform = new ScaleTransform(0.75d, 0.75d); + RectBack.Background = (Brush)ModSecret.AppResources["ColorBrush7"]; + RectBack.Opacity = 0d; + } + } + + ModAnimation.AniStop("ListItem Color " + Uuid); + } + } + + private void MyListItem_Loaded(object sender, RoutedEventArgs e) + { + if (Checked) + SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2"); + else + SetResourceReference(ForegroundProperty, "ColorBrush1"); + ColumnPaddingRight.Width = new GridLength(MinPaddingRight); + if (EventType == "打开帮助" && !(!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(Info))) // #3266 + try + { + var Unused = + new ModMain.HelpEntry(ModEvent.GetEventAbsoluteUrls(EventData, EventType)[0]).SetToListItem(this); + } + catch (Exception ex) + { + ModBase.Log(ex, "设置帮助 MyListItem 失败", ModBase.LogLevel.Msgbox); + EventType = null; + EventData = null; + } + } + + public override string ToString() + { + return Title; + } + + #region 后加载控件 + + // 指向背景 + private Border _RectBack; + + public Border RectBack + { + get + { + if (_RectBack is null) + { + var Rect = new Border + { + Name = "RectBack", + CornerRadius = new CornerRadius(IsScaleAnimationEnabled || Height > 40d ? 6 : 0), + RenderTransform = IsScaleAnimationEnabled ? new ScaleTransform(0.8d, 0.8d) : null, + RenderTransformOrigin = new Point(0.5d, 0.5d), + BorderThickness = new Thickness(ModBase.GetWPFSize(1d)), + SnapsToDevicePixels = true, + IsHitTestVisible = false, + Opacity = 0d + }; + Rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7"); + Rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6"); + SetColumnSpan(Rect, 999); + SetRowSpan(Rect, 999); + Children.Insert(0, Rect); + _RectBack = Rect; + // + } + + return _RectBack; + } + } + + // 按钮 + public FrameworkElement ButtonStack; + + // 图标 + public FrameworkElement PathLogo; + + // 勾选条 + public Border RectCheck; + + + /// + /// Tags 的存放 StackPanel + /// + public StackPanel _PanTags; + + public StackPanel PanTags + { + get + { + if (_PanTags is not null) + return _PanTags; + var NewStack = new StackPanel + { + IsHitTestVisible = false, + Orientation = Orientation.Horizontal, + VerticalAlignment = VerticalAlignment.Bottom, + Margin = new Thickness(3.5d, 0d, -3, 0d) + }; + SetColumn(NewStack, 3); + SetRow(NewStack, 2); + PanBack.Children.Add(NewStack); + _PanTags = NewStack; + return _PanTags; + } + } + + /// + /// 标签,可以传入 String 和 List(Of String) + /// + public object Tags + { + set + { + var list = new List(); + if (value is string) list = Conversions.ToString(value).Split("|").ToList(); + if (value is List) list = (List)value; + PanTags.Children.Clear(); + PanTags.Visibility = list.Any() ? Visibility.Visible : Visibility.Collapsed; + foreach (var TagText in list) + { + var NewTag = new Border + { + Background = new SolidColorBrush(Color.FromArgb(17, 0, 0, 0)), + Padding = new Thickness(3d, 1d, 3d, 1d), + CornerRadius = new CornerRadius(3d), + Margin = new Thickness(0d, 0d, 3d, 0d), + SnapsToDevicePixels = true, + UseLayoutRounding = false + }; + var TagTextBlock = new TextBlock + { + Text = TagText, + Foreground = new SolidColorBrush(Color.FromRgb(134, 134, 134)), + FontSize = 11d + }; + NewTag.Child = TagTextBlock; + PanTags.Children.Add(NewTag); + } + } + } + + // 副文本 + private TextBlock _LabInfo; + + public TextBlock LabInfo + { + get + { + if (_LabInfo is null) + { + var Lab = new TextBlock + { + Name = "LabInfo", + SnapsToDevicePixels = false, + UseLayoutRounding = false, + HorizontalAlignment = HorizontalAlignment.Left, + IsHitTestVisible = false, + TextTrimming = TextTrimming.CharacterEllipsis, + Visibility = Visibility.Collapsed, + FontSize = 12d, + Margin = new Thickness(4d, 0d, 0d, 0d), + Opacity = 0.6d + }; + SetColumn(Lab, 4); + SetRow(Lab, 2); + PanBack.Children.Add(Lab); + _LabInfo = Lab; + // + } + + return _LabInfo; + } + } + + #endregion + + #region 自定义属性 + + // Uuid + public int Uuid = ModBase.GetUuid(); + + /// + /// 是否启用缩放动画。 + /// + public bool IsScaleAnimationEnabled + { + get => _IsScaleAnimationEnabled; + set + { + _IsScaleAnimationEnabled = value; + if (_RectBack is not null) + RectBack.CornerRadius = new CornerRadius(value ? 6 : 0); + } + } + + private bool _IsScaleAnimationEnabled = true; + + // 边距 + public int PaddingLeft + { + get => (int)Math.Round(ColumnPaddingLeft.Width.Value); + set => ColumnPaddingLeft.Width = new GridLength(value); + } + + /// + /// 右边距的最小值。 + /// 在存在右侧按钮时,右边距会被自动设置为 5 + 按钮数 * 25。 + /// + public int MinPaddingRight { get; set; } = 4; + + // 按钮 + private IEnumerable _Buttons; + + public IEnumerable Buttons + { + get => _Buttons; + set + { + _Buttons = value; + // 没有特殊按钮,移除原 Stack + if (ButtonStack is not null) + { + Children.Remove(ButtonStack); + ButtonStack = null; + } + + // 添加新 Stack + switch (value.Count()) + { + case 0: + { + break; + } + // 没有按钮,不添加新的 + case 1: + { + // 只有一个按钮 + foreach (var Btn in value) + { + if (Btn.Height.Equals(double.NaN)) + Btn.Height = 25d; + if (Btn.Width.Equals(double.NaN)) + Btn.Width = 25d; + Btn.Opacity = 0d; + Btn.Margin = new Thickness(0d, 0d, 5d, 0d); + Btn.SnapsToDevicePixels = false; + Btn.HorizontalAlignment = HorizontalAlignment.Right; + Btn.VerticalAlignment = VerticalAlignment.Center; + Btn.SnapsToDevicePixels = false; + Btn.UseLayoutRounding = false; + SetColumnSpan(Btn, 10); + SetRowSpan(Btn, 10); + Children.Add(Btn); + ButtonStack = Btn; + } + + break; + } + + default: + { + // 有复数按钮,使用 StackPanel + ButtonStack = new StackPanel + { + Opacity = 0d, Margin = new Thickness(0d, 0d, 5d, 0d), SnapsToDevicePixels = false, + Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, UseLayoutRounding = false + }; + SetColumnSpan(ButtonStack, 10); + SetRowSpan(ButtonStack, 10); + // 构造按钮 + foreach (var Btn in value) + { + if (Btn.Height.Equals(double.NaN)) + Btn.Height = 25d; + if (Btn.Width.Equals(double.NaN)) + Btn.Width = 25d; + ((StackPanel)ButtonStack).Children.Add(Btn); + } + + Children.Add(ButtonStack); + break; + } + } + } + } + + // 标题 + public InlineCollection Inlines => LabTitle.Inlines; + + public string Title + { + get => Conversions.ToString(GetValue(TitleProperty)); + set => SetValue(TitleProperty, value.Replace("\r", "").Replace("\n", "")); + } + + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register("Title", typeof(string), typeof(MyListItem)); + + // 字号 + public double FontSize + { + get => Conversions.ToDouble(GetValue(FontSizeProperty)); + set => SetValue(FontSizeProperty, value); + } + + public static readonly DependencyProperty FontSizeProperty = + DependencyProperty.Register("FontSize", typeof(double), typeof(MyListItem), new PropertyMetadata(14d)); + + // 信息 + public string Info + { + get => Conversions.ToString(GetValue(InfoProperty)); + set + { + if ((Info ?? "") == (value ?? "")) + return; + value = value.Replace("\r", "").Replace("\n", ""); + SetValue(InfoProperty, value); + } + } + + public static readonly DependencyProperty InfoProperty = DependencyProperty.Register("Info", typeof(string), + typeof(MyListItem), new PropertyMetadata("", OnInfoChanged)); + + public MyListItem() + { + InitializeComponent(); + + SizeChanged += (_, __) => OnSizeChanged(); + PreviewMouseLeftButtonUp += Button_MouseUp; + PreviewMouseLeftButtonDown += Button_MouseDown; + MouseLeave += Button_MouseLeave; + PreviewMouseLeftButtonUp += Button_MouseLeave; + MouseEnter += RefreshColor; + MouseLeave += RefreshColor; + MouseLeftButtonDown += RefreshColor; + MouseLeftButtonUp += RefreshColor; + Loaded += MyListItem_Loaded; + } + + private static void OnInfoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (MyListItem)d; + var value = Conversions.ToString(e.NewValue); + control.LabInfo.Text = value; + control.LabInfo.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible; + } + + // 图片 + public string Logo + { + get => Conversions.ToString(GetValue(LogoProperty)); + set + { + if ((Logo ?? "") == (value ?? "")) + return; + SetValue(LogoProperty, value); + } + } + + public static readonly DependencyProperty LogoProperty = DependencyProperty.Register("Logo", typeof(string), + typeof(MyListItem), new PropertyMetadata("", OnLogoChanged)); + + private static void OnLogoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (MyListItem)d; + var value = Conversions.ToString(e.NewValue); + control.UpdateLogo(value); + } + + private void UpdateLogo(string _Logo) + { + // 删除旧 Logo + if (!(PathLogo == null)) + Children.Remove(PathLogo); + // 添加新 Logo + if (!string.IsNullOrEmpty(_Logo)) + { + if (_Logo.StartsWithF("http", true)) + { + // 网络图片 + PathLogo = new MyImage + { + Tag = this, + IsHitTestVisible = LogoClickable, + Source = _Logo, + RenderTransformOrigin = new Point(0.5d, 0.5d), + RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }, + SnapsToDevicePixels = true, + UseLayoutRounding = false + }; + RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.Linear); + } + else if (_Logo.EndsWithF(".png", true) || _Logo.EndsWithF(".jpg", true) || _Logo.EndsWithF(".webp", true)) + { + // 位图 + PathLogo = new Canvas + { + Tag = this, + IsHitTestVisible = LogoClickable, + Background = new MyBitmap(_Logo), + RenderTransformOrigin = new Point(0.5d, 0.5d), + RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }, + SnapsToDevicePixels = true, + UseLayoutRounding = false, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + if (_Logo.Contains(ModBase.PathTemp + @"Cache\Skin\Head") || + _Logo.Contains(ModBase.PathTemp + @"Cache\Cape")) + RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.NearestNeighbor); + else + RenderOptions.SetBitmapScalingMode(PathLogo, BitmapScalingMode.Linear); + } + else + { + // 矢量图 + PathLogo = new Path + { + Tag = this, + IsHitTestVisible = LogoClickable, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Stretch = Stretch.Uniform, + Data = (Geometry)new GeometryConverter().ConvertFromString(_Logo), + RenderTransformOrigin = new Point(0.5d, 0.5d), + RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }, + SnapsToDevicePixels = false, + UseLayoutRounding = false + }; + PathLogo.SetBinding(Shape.FillProperty, new Binding("Foreground") { Source = this }); + } + + SetColumn(PathLogo, 2); + SetRowSpan(PathLogo, 4); + OnSizeChanged(); // 设置边距 + Children.Add(PathLogo); + // 图标的点击事件 + if (LogoClickable) + { + PathLogo.MouseLeave += (sender, e) => IsLogoDown = false; + PathLogo.MouseLeftButtonDown += (sender, e) => IsLogoDown = true; + PathLogo.MouseLeftButtonUp += (sender, e) => + { + if (IsLogoDown) + { + IsLogoDown = false; + LogoClick?.Invoke(((FrameworkElement)sender).Tag, e); + } + }; + } + } + + // 改变行距 + ColumnLogo.Width = new GridLength((string.IsNullOrEmpty(_Logo) ? 0 : 34) + (Height < 40d ? 0 : 4)); + } + + private double _LogoScale = 1d; + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (!(PathLogo == null)) + PathLogo.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + // 图标的点击 + /// + /// 该 Logo 是否可用点击触发事件。需要在 Logo 属性之前设置。 + /// + public bool LogoClickable { get; set; } = false; + + private bool IsLogoDown; + + // 勾选选项 + public enum CheckType + { + None, + Clickable, + RadioBox, + CheckBox + } + + private CheckType _Type = CheckType.None; + + public CheckType Type + { + get => _Type; + set + { + if (_Type == value) + return; + _Type = value; + // 切换左栏大小 + ColumnCheck.Width = + new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6); + // 切换竖条控件 + if (_Type == CheckType.None || _Type == CheckType.Clickable) + { + // 移除竖条控件 + if (!(RectCheck == null)) + { + Children.Remove(RectCheck); + RectCheck = null; + } + + SetChecked(false, false, false); + } + // 添加竖条控件 + else if (RectCheck == null) + { + RectCheck = new Border + { + Width = 5d, + Height = Checked ? double.NaN : 0d, + CornerRadius = new CornerRadius(2d, 2d, 2d, 2d), + VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + UseLayoutRounding = false, + SnapsToDevicePixels = false, + Margin = Checked ? new Thickness(-1, 6d, 0d, 6d) : new Thickness(-1, 0d, 0d, 0d) + }; + RectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3"); + SetRowSpan(RectCheck, 4); + Children.Add(RectCheck); + } + } + } + + // 适应尺寸 + private void OnSizeChanged() + { + var _Logo = Logo; + ColumnCheck.Width = + new GridLength(_Type == CheckType.None || _Type == CheckType.Clickable ? Height < 40d ? 4 : 2 : 6); + ColumnLogo.Width = new GridLength((string.IsNullOrEmpty(_Logo) ? 0 : 34) + (Height < 40d ? 0 : 4)); + if (PathLogo is not null) + { + if (_Logo.EndsWithF(".png", true) || _Logo.EndsWithF(".jpg", true) || _Logo.EndsWithF(".webp", true)) + PathLogo.Margin = new Thickness(4d, 5d, 3d, 5d); + else + PathLogo.Margin = new Thickness(Height < 40d ? 6 : 8, 8d, Height < 40d ? 4 : 6, 8d); + } + + LabTitle.Margin = new Thickness(4d, 0d, 0d, Height < 40d ? 0 : 2); + } + + // 勾选状态 + private bool _Checked; + + public bool Checked + { + get => _Checked; + set => SetChecked(value, false, value != _Checked); // 仅在值发生变化时触发动画 (#4596) + } + + /// + /// 手动设置 Checked 属性。 + /// + /// 新的 Checked 属性。 + /// 是否由用户引发。 + /// 是否执行动画。 + public void SetChecked(bool value, bool user, bool anime) + { + try + { + // 自定义属性基础 + + var ChangedEventArgs = new ModBase.RouteEventArgs(user); + var RawValue = _Checked; + if (Type == CheckType.RadioBox) + { + if (IsInitialized && !(value == _Checked)) + { + _Checked = value; + Changed?.Invoke(this, ChangedEventArgs); + if (ChangedEventArgs.Handled) + { + _Checked = RawValue; + return; + } + } + + _Checked = value; + } + else + { + if (value == _Checked) + return; + _Checked = value; + if (IsInitialized) + { + Changed?.Invoke(this, ChangedEventArgs); + if (ChangedEventArgs.Handled) + { + _Checked = RawValue; + return; + } + } + } + + if (value) + { + var CheckEventArgs = new ModBase.RouteEventArgs(user); + Check?.Invoke(this, CheckEventArgs); + if (CheckEventArgs.Handled) + return; + } + + // 保证只有一个单选 ListItem 选中 + + if (Type == CheckType.RadioBox) + { + if (Parent == null) + return; + var RadioboxList = new List(); + var CheckedCount = 0; + // 收集控件列表与选中个数 + foreach (var ControlRaw in ((Panel)Parent).Children) + { + var Control = MyVirtualizingElement.TryInit((FrameworkElement)ControlRaw); + if (Control is MyListItem listItem && listItem.Type == CheckType.RadioBox) + { + RadioboxList.Add(listItem); + if (listItem.Checked) + CheckedCount += 1; + } + } + + // 判断选中情况 + switch (CheckedCount) + { + case 0: + { + // 没有任何单选框被选中,选择第一个 + RadioboxList[0].Checked = true; + break; + } + case var @case when @case > 1: + { + // 选中项目多于 1 个 + if (Checked) + { + // 如果本控件选中,则取消其他所有控件的选中 + foreach (var Control in RadioboxList) + if (Control.Checked && !Control.Equals(this)) + Control.Checked = false; + } + else + { + // 如果本控件未选中,则只保留第一个选中的控件 + var FirstChecked = false; + foreach (var Control in RadioboxList) + if (Control.Checked) + { + if (FirstChecked) + Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发 + else + FirstChecked = true; + } + } + + break; + } + } + } + + // 更改动画 + + if (IsLoaded && ModAnimation.AniControlEnabled == 0 && anime) // 防止默认属性变更触发动画 + { + var Anim = new List(); + if (Checked) + { + // 由无变有 + if (!(RectCheck == null)) + { + var Delta = 20; + Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.4d, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.6d, 300, + Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaOpacity(RectCheck, 1d - RectCheck.Opacity, 30)); + RectCheck.VerticalAlignment = VerticalAlignment.Center; + RectCheck.Margin = new Thickness(-1, 0d, 0d, 0d); + } + + Anim.Add(ModAnimation.AaColor(this, ForegroundProperty, + Height < 40d ? "ColorBrush3" : "ColorBrush2", 200)); + } + else + { + // 由有变无 + if (!(RectCheck == null)) + { + // Anim.Add(AaWidth(RectCheck, -RectCheck.Width, 120,, New AniEaseInFluent)) + Anim.Add(ModAnimation.AaHeight(RectCheck, -RectCheck.ActualHeight, 120, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaOpacity(RectCheck, -RectCheck.Opacity, 70, 40)); + RectCheck.VerticalAlignment = VerticalAlignment.Center; + } + + Anim.Add(ModAnimation.AaColor(this, ForegroundProperty, "ColorBrush1", 120)); + } + + ModAnimation.AniStart(Anim, "MyListItem Checked " + Uuid); + } + else + { + // 不使用动画 + ModAnimation.AniStop("MyListItem Checked " + Uuid); + if (Checked) + { + if (!(RectCheck == null)) + { + RectCheck.Height = double.NaN; + RectCheck.Margin = new Thickness(-1, 6d, 0d, 6d); + RectCheck.Opacity = 1d; + RectCheck.VerticalAlignment = VerticalAlignment.Stretch; + } + + SetResourceReference(ForegroundProperty, Height < 40d ? "ColorBrush3" : "ColorBrush2"); + } + else + { + if (!(RectCheck == null)) + { + RectCheck.Height = 0d; + RectCheck.Margin = new Thickness(-1, 0d, 0d, 0d); + RectCheck.Opacity = 0d; + RectCheck.VerticalAlignment = VerticalAlignment.Center; + } + + SetResourceReference(ForegroundProperty, "ColorBrush1"); + } + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "设置 Checked 失败"); + } + } + + // 前景色绑定 + public Brush Foreground + { + get => (Brush)GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly DependencyProperty ForegroundProperty = DependencyProperty.Register("Foreground", + typeof(Brush), typeof(MyListItem), new PropertyMetadata(ModSecret.AppResources["ColorBrush1"])); + + // 菜单与按钮绑定 + public Action ContentHandler { get; set; } + + #endregion + + #region 点击 + + // 触发点击事件 + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + Click?.Invoke(sender, e); + if (e.Handled) + return; + // 触发自定义事件 + if (!string.IsNullOrEmpty(EventType)) + { + ModEvent.TryStartEvent(EventType, EventData); + e.Handled = true; + } + + if (e.Handled) + return; + // 实际的单击处理 + switch (Type) + { + case CheckType.Clickable: + { + ModBase.Log("[Control] 按下单击列表项:" + Title); + break; + } + case CheckType.RadioBox: + { + ModBase.Log("[Control] 按下单选列表项:" + Title); + if (!Checked) + SetChecked(true, true, true); + break; + } + case CheckType.CheckBox: + { + ModBase.Log("[Control] 按下复选列表项(" + !Checked + "):" + Title); + SetChecked(!Checked, true, true); + break; + } + } + } + + // 鼠标点击判定 + private bool IsMouseDown; + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + if (IsMouseDirectlyOver && !(Type == CheckType.None)) + { + IsMouseDown = true; + if (ButtonStack is not null) + ButtonStack.IsHitTestVisible = false; + } + } + + private void Button_MouseLeave(object sender, object e) + { + IsMouseDown = false; + if (ButtonStack is not null) + ButtonStack.IsHitTestVisible = true; + } + + // 实现自定义事件 + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public static readonly DependencyProperty EventTypeProperty = + DependencyProperty.Register("EventType", typeof(string), typeof(MyListItem), new PropertyMetadata(null)); + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + public static readonly DependencyProperty EventDataProperty = + DependencyProperty.Register("EventData", typeof(string), typeof(MyListItem), new PropertyMetadata(null)); + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml b/Plain Craft Launcher 2/Controls/MyLoading.xaml index f819ef8cb..4215d8d2c 100644 --- a/Plain Craft Launcher 2/Controls/MyLoading.xaml +++ b/Plain Craft Launcher 2/Controls/MyLoading.xaml @@ -1,14 +1,17 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="PanBack" mc:Ignorable="d" + x:Class="PCL.MyLoading" + MinWidth="50" MinHeight="50" Background="{StaticResource ColorBrushTransparent}"> - - - - + + + + - + @@ -16,22 +19,30 @@ - + - + - + - + - + - + - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs b/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs new file mode 100644 index 000000000..742ec335d --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyLoading.xaml.cs @@ -0,0 +1,398 @@ +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; +using static PCL.MyLoading; + +namespace PCL; + +public partial class MyLoading +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); + + public delegate void IsErrorChangedEventHandler(object sender, bool isError); + + public delegate void StateChangedEventHandler(object sender, MyLoadingState newState, MyLoadingState oldState); + + private readonly int Uuid = ModBase.GetUuid(); + + public bool AutoRun { get; set; } = true; + + public event IsErrorChangedEventHandler? IsErrorChanged; + public event StateChangedEventHandler? StateChanged; + public event ClickEventHandler? Click; + + #region 颜色 + + public SolidColorBrush Foreground + { + get => (SolidColorBrush)GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static readonly DependencyProperty ForegroundProperty = + DependencyProperty.Register("Foreground", typeof(SolidColorBrush), typeof(MyLoading)); + + public MyLoading() + { + InitializeComponent(); + SetResourceReference(ForegroundProperty, "ColorBrush3"); + IsErrorChanged += (_, __) => RefreshText(); + Loaded += (_, __) => RefreshText(); + Loaded += (_, __) => InitState(); + Loaded += (_, __) => RefreshState(); + Unloaded += (_, __) => RefreshState(); + MouseLeftButtonUp += Button_MouseUp; + MouseLeftButtonDown += Button_MouseDown; + MouseLeave += Button_MouseLeave; + MouseLeftButtonUp += Button_MouseLeave; + } + + #endregion + + #region 文本 + + private bool _ShowProgress { get; set; } + + public bool ShowProgress + { + get => _ShowProgress; + set + { + if (_ShowProgress == value) + return; + _ShowProgress = value; + RefreshText(); + } + } + + private string _Text = "加载中"; + + public string Text + { + get => _Text; + set + { + _Text = value; + RefreshText(); + } + } + + private string _TextError = "加载失败"; + + public string TextError + { + get => _TextError; + set + { + _TextError = value; + RefreshText(); + } + } + + /// + /// 是否在使用 Loader 时使用 Loader 的错误输出来替换默认的错误文本显示。 + /// + public bool TextErrorInherit { get; set; } = true; + + private void RefreshText() + { + ModBase.RunInUi(() => + { + if (InnerState == MyLoadingState.Error) + { + if (TextErrorInherit && State.IsLoader) + { + var Ex = (Exception)((dynamic)State).Error; + if (Ex is null) + { + LabText.Text = "未知错误"; + } + else + { + while (Ex.InnerException is not null) Ex = Ex.InnerException; + LabText.Text = Conversions.ToString(ModBase.StrTrim(Ex.Message)); + if (new[] + { + "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称", "由于目标计算机积极拒绝", "操作已超时", "操作超时", "服务器超时", "连接超时" + }.Any(s => LabText.Text.Contains(s))) LabText.Text = "网络环境不佳,请稍后重试,或使用 VPN 以改善网络环境"; + } + } + else + { + LabText.Text = TextError; + } + } + else if (ShowProgress && State.IsLoader) + { + LabText.Text = Conversions.ToString(Operators.ConcatenateObject( + Operators.ConcatenateObject(Text + " - ", + Math.Floor(Operators.MultiplyObject(((dynamic)State).Progress, 100))), "%")); + } + else + { + LabText.Text = Text; + } + }); + } + + #endregion + + #region 状态改变 + + // 状态枚举 + public enum MyLoadingState + { + Unloaded = -1, + Run = 0, + Stop = 1, + Error = 2 + } + + // 用于外部改变的公开状态 + private ILoadingTrigger __State; + + private ILoadingTrigger _State + { + [MethodImpl(MethodImplOptions.Synchronized)] + get => __State; + + [MethodImpl(MethodImplOptions.Synchronized)] + set + { + if (__State != null) + { + __State.ProgressChanged -= (_, __) => RefreshText(); + __State.LoadingStateChanged -= (_, __) => RefreshState(); + } + + __State = value; + if (__State != null) + { + __State.ProgressChanged += (_, __) => RefreshText(); + __State.LoadingStateChanged += (_, __) => RefreshState(); + } + } + } + + public ILoadingTrigger State + { + get + { + InitState(); + return _State; + } + set + { + _State = value; + RefreshState(); + } + } + + private void InitState() + { + if (_State is null) + { + _State = new MyLoadingStateSimulator(); + if (AutoRun) + _State.LoadingState = MyLoadingState.Run; + } + } + + private void RefreshState() + { + if (_State.LoadingState == MyLoadingState.Run && !IsLoaded) + InnerState = MyLoadingState.Stop; + InnerState = _State.LoadingState; + OuterState = _State.LoadingState; + AniLoop(); + } + + // 用于引发外部事件的状态 + private MyLoadingState _OuterState { get; set; } = MyLoadingState.Unloaded; + + private MyLoadingState OuterState + { + get => _OuterState; + set + { + if (_OuterState == value) + return; + var OldValue = _OuterState; + _OuterState = value; + // 引发事件 + StateChanged?.Invoke(this, value, OldValue); + if (OldValue == MyLoadingState.Error != (value == MyLoadingState.Error)) + IsErrorChanged?.Invoke(this, value == MyLoadingState.Error); + } + } + + + // 用于引发内部动画事件的状态 + private MyLoadingState _InnerState { get; set; } = MyLoadingState.Unloaded; + + private MyLoadingState InnerState + { + get => _InnerState; + set + { + if (_InnerState == value) + return; + var OldValue = _InnerState; + _InnerState = value; + // 引发事件 + AniLoop(); + if (OldValue == MyLoadingState.Error != (value == MyLoadingState.Error)) + ErrorAnimation(this, value == MyLoadingState.Error); + } + } + + #endregion + + #region 动画 + + /// + /// 是否需要动画。 + /// + public bool HasAnimation { get; set; } = true; + + /// + /// 主动画循环是否正在运行中。 + /// + private bool IsLooping; + + private void AniLoop() + { + // 这坨循环代码也是老屎坑了,救救.jpg + if (!HasAnimation || IsLooping || !(InnerState == MyLoadingState.Run) || ModAnimation.AniSpeed > 10d || + !IsLoaded) + return; + IsLooping = true; + ErrorAnimationWaiting = true; + ModAnimation.AniStart(new[] + { + ModAnimation.AaRotateTransform(PathPickaxe, -20 - ((RotateTransform)PathPickaxe.RenderTransform).Angle, 350, + 250, new ModAnimation.AniEaseInBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaRotateTransform(PathPickaxe, 50d, 900, Ease: new ModAnimation.AniEaseOutFluent(), + After: true), + ModAnimation.AaRotateTransform(PathPickaxe, 25d, 900, + Ease: new ModAnimation.AniEaseOutElastic(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => + { + PathLeft.Opacity = 1d; + PathLeft.Margin = new Thickness(7d, 41d, 0d, 0d); + PathRight.Opacity = 1d; + PathRight.Margin = new Thickness(14d, 41d, 0d, 0d); + ErrorAnimationWaiting = false; + }), + ModAnimation.AaOpacity(PathLeft, -1, 100, 50), + ModAnimation.AaX(PathLeft, -5, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaY(PathLeft, -6, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaOpacity(PathRight, -1, 100, 50), + ModAnimation.AaX(PathRight, 5d, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaY(PathRight, -6, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => + { + IsLooping = false; + AniLoop(); + }, After: true) + }, "MyLoader Loop " + Uuid + "/" + ModBase.GetUuid()); + if (ShowProgress) + { + } + } + + /// + /// 镐子是否还没挥下去,要求错误动画等待。 + /// + private bool ErrorAnimationWaiting; + + private void ErrorAnimation(object sender, bool isError) + { + if (isError) + { + // 非错误变为错误 + var Wait = ErrorAnimationWaiting ? 400 : 0; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrushRedLight", 300), + ModAnimation.AaOpacity(PathError, 1d - PathError.Opacity, 100, 300 + Wait), + ModAnimation.AaScaleTransform(PathError, 1d - ((ScaleTransform)PathError.RenderTransform).ScaleX, + 400, 300 + Wait, new ModAnimation.AniEaseOutBack()) + }, "MyLoader Error " + Uuid); + } + else + { + // 错误变为非错误 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(PathError, -PathError.Opacity, 100), + ModAnimation.AaScaleTransform(PathError, 0.5d - ((ScaleTransform)PathError.RenderTransform).ScaleX, + 200), + ModAnimation.AaColor(PanBack, ForegroundProperty, "ColorBrush3", 300) + }, "MyLoader Error " + Uuid); + } + } + + #endregion + + #region 点击事件 + + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + Click?.Invoke(sender, e); + } + + private bool IsMouseDown; + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + // 鼠标点击判定(务必放在点击事件之后,以使得 Button_MouseUp 先于 Button_MouseLeave 执行) + IsMouseDown = true; + } + + private void Button_MouseLeave(object sender, object e) + { + IsMouseDown = false; + } + + #endregion +} + +public interface ILoadingTrigger +{ + delegate void LoadingStateChangedEventHandler(MyLoadingState NewState, MyLoadingState OldState); + + delegate void ProgressChangedEventHandler(double NewProgress, double OldProgress); + + bool IsLoader { get; } + MyLoadingState LoadingState { get; set; } + event LoadingStateChangedEventHandler? LoadingStateChanged; + event ProgressChangedEventHandler? ProgressChanged; +} + +public class MyLoadingStateSimulator : ILoadingTrigger +{ + private MyLoadingState _LoadingState { get; set; } = MyLoadingState.Unloaded; + + public MyLoadingState LoadingState + { + get => _LoadingState; + set + { + if (_LoadingState == value) + return; + var OldState = _LoadingState; + _LoadingState = value; + LoadingStateChanged?.Invoke(value, OldState); + } + } + + public bool IsLoader { get; } = false; + + public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged; + public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged; +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMenuItem.cs b/Plain Craft Launcher 2/Controls/MyMenuItem.cs new file mode 100644 index 000000000..84238fd80 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMenuItem.cs @@ -0,0 +1,92 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; + +namespace PCL; + +public class MyMenuItem : MenuItem +{ + // 指向动画 + + private const int AnimationTimeIn = 100; + private const int AnimationTimeOut = 200; + private string ColorName; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyMenuItem() + { + Loaded += MyMenuItem_Loaded; + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + IsEnabledChanged += (_, __) => RefreshColor(); + } + + private void MyMenuItem_Loaded(object sender, RoutedEventArgs e) + { + if (Icon is not null) + { + var IconControl = (Path)GetTemplateChild("Icon"); + if (IconControl is not null) + IconControl.Data = (Geometry)new GeometryConverter().ConvertFromString(Conversions.ToString(Icon)); + // 对父级设置透明度 + } + + ((ContextMenu)Parent).Opacity = Conversions.ToDouble( + Operators.AddObject(Operators.DivideObject(Config.Preference.Theme.WindowOpacity, 1000), 0.4d)); + } + + private void RefreshColor() + { + // 判断当前颜色 + string BackName; + string ForeName; + int Time; + if (!IsEnabled) + { + BackName = "ColorBrushTransparent"; + ForeName = "ColorBrushGray5"; + Time = AnimationTimeOut; + } + else if (IsMouseOver) + { + BackName = "ColorBrush6"; + ForeName = "ColorBrush2"; + Time = AnimationTimeIn; + } + else + { + BackName = "ColorBrushTransparent"; + ForeName = "ColorBrush1"; + Time = AnimationTimeOut; + } + + // 重复性验证 + if ((ColorName ?? "") == (BackName ?? "")) + return; + ColorName = BackName; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, BackgroundProperty, BackName, Time), + ModAnimation.AaColor(this, ForegroundProperty, ForeName, Time) + }, "MyMenuItem Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyMenuItem Color " + Uuid); + SetResourceReference(BackgroundProperty, BackName); + SetResourceReference(ForegroundProperty, ForeName); + } + } +} diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml index 6721b73ce..f0a783de9 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml @@ -1,8 +1,9 @@ - + @@ -11,7 +12,8 @@ - + @@ -22,18 +24,29 @@ - + - - + - - - - + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs new file mode 100644 index 000000000..4f3f43564 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.cs @@ -0,0 +1,148 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class MyMsgInput +{ + private readonly ModMain.MyMsgBoxConverter MyConverter; + private readonly int Uuid = ModBase.GetUuid(); + + public MyMsgInput(ModMain.MyMsgBoxConverter Converter) + { + try + { + InitializeComponent(); + Btn1.Name = Btn1.Name + ModBase.GetUuid(); + Btn2.Name = Btn2.Name + ModBase.GetUuid(); + MyConverter = Converter; + LabTitle.Text = Converter.Title; + LabText.Text = Converter.Text; + PanText.Visibility = string.IsNullOrEmpty(Converter.Text) ? Visibility.Collapsed : Visibility.Visible; + TextArea.Text = Conversions.ToString(Converter.Content); + TextArea.HintText = Converter.HintText; + TextArea.ValidateRules = Converter.ValidateRules; + Btn1.Text = Converter.Button1; + if (Converter.IsWarn) + { + Btn1.ColorType = MyButton.ColorState.Red; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); + } + + Btn2.Text = Converter.Button2; + Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible; + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + } + + catch (Exception ex) + { + ModBase.Log(ex, "输入弹窗初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += Load; + } + + private void Load(object sender, EventArgs e) + { + try + { + // UI 初始化 + if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) + Btn1.ColorType = MyButton.ColorState.Highlight; + TextArea.Focus(); + TextArea.SelectionStart = TextArea.Text.Length; + // 动画 + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (MyConverter.IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyMsgBox " + Uuid); + // 记录日志 + ModBase.Log("[Control] 输入弹窗:" + LabTitle.Text); + } + + catch (Exception ex) + { + ModBase.Log(ex, "输入弹窗加载失败", ModBase.LogLevel.Hint); + } + } + + private void Close() + { + // 结束线程阻塞 + MyConverter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + // 动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + if (!ModMain.WaitingMyMsgBox.Any()) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true) + }, "MyMsgBox " + Uuid); + } + + public void Btn1_Click(object sender, MouseButtonEventArgs e) + { + TextArea.Validate(); // #5773 + if (MyConverter.IsExited || !TextArea.IsValidated) + return; + MyConverter.IsExited = true; + MyConverter.Result = TextArea.Text; + Close(); + } + + public void Btn2_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + MyConverter.IsExited = true; + MyConverter.Result = null; + Close(); + } + + private void TextCaption_ValidateChanged(object sender, EventArgs e) + { + Btn1.IsEnabled = TextArea.IsValidated; + } + + private void Drag(object sender, MouseButtonEventArgs e) + { + try + { + if (e.LeftButton == MouseButtonState.Pressed) + if (e.GetPosition(ShapeLine).Y <= 2d) + ModMain.FrmMain.DragMove(); + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml index 502d01c2a..d34fd682f 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml @@ -1,9 +1,10 @@ - + @@ -12,7 +13,8 @@ - + @@ -22,18 +24,29 @@ - + - + + Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" /> - - - - + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs new file mode 100644 index 000000000..86a2784ee --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgMarkdown.xaml.cs @@ -0,0 +1,172 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using Microsoft.VisualBasic; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class MyMsgMarkdown +{ + private readonly ModMain.MyMsgBoxConverter MyConverter; + private readonly int Uuid = ModBase.GetUuid(); + + public MyMsgMarkdown(ModMain.MyMsgBoxConverter Converter) + { + try + { + InitializeComponent(); + Btn1.Name = Btn1.Name + ModBase.GetUuid(); + Btn2.Name = Btn2.Name + ModBase.GetUuid(); + Btn3.Name = Btn3.Name + ModBase.GetUuid(); + MyConverter = Converter; + LabTitle.Text = Converter.Title; + LabCaption.Markdown = Converter.Text; + DataContext = this; + Btn1.Text = Converter.Button1; + if (Converter.IsWarn) + { + Btn1.ColorType = MyButton.ColorState.Red; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); + } + + Btn2.Text = Converter.Button2; + Btn3.Text = Converter.Button3; + Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible; + Btn3.Visibility = string.IsNullOrEmpty(Converter.Button3) ? Visibility.Collapsed : Visibility.Visible; + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + } + + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += Load; + } + + private void Load(object sender, EventArgs e) + { + try + { + // UI 初始化 + if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) + Btn1.ColorType = MyButton.ColorState.Highlight; + Btn1.Focus(); + // 动画 + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (MyConverter.IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyMsgBox " + Uuid); + // 记录日志 + ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Markdown); + } + + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); + } + } + + private void Close() + { + // 结束线程阻塞 + if (MyConverter.ForceWait || !string.IsNullOrEmpty(MyConverter.Button2)) + MyConverter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + // 动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + if (!ModMain.WaitingMyMsgBox.Any()) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true) + }, "MyMsgBox " + Uuid); + } + + public void Btn1_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button1Action is not null) + { + MyConverter.Button1Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 1; + Close(); + } + } + + public void Btn2_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button2Action is not null) + { + MyConverter.Button2Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 2; + Close(); + } + } + + public void Btn3_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button3Action is not null) + { + MyConverter.Button3Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 3; + Close(); + } + } + + private void Drag(object? sender = null, MouseButtonEventArgs? e = null) + { + try + { + if (e.LeftButton == MouseButtonState.Pressed) + if (e.GetPosition(ShapeLine).Y <= 2d) + ModMain.FrmMain.DragMove(); + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml index cca194dfa..1ba30ccf8 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml @@ -1,8 +1,9 @@ - + @@ -11,7 +12,8 @@ - + @@ -21,15 +23,22 @@ - + - + - - - + + + diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs new file mode 100644 index 000000000..7824eacee --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.cs @@ -0,0 +1,173 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class MyMsgSelect +{ + private readonly ModMain.MyMsgBoxConverter MyConverter; + private readonly int Uuid = ModBase.GetUuid(); + + private int SelectedIndex = -1; + + public MyMsgSelect(ModMain.MyMsgBoxConverter Converter) + { + try + { + InitializeComponent(); + Btn1.Name = Btn1.Name + ModBase.GetUuid(); + Btn2.Name = Btn2.Name + ModBase.GetUuid(); + MyConverter = Converter; + LabTitle.Text = Converter.Title; + Btn1.Text = Converter.Button1; + if (Converter.IsWarn) + { + Btn1.ColorType = MyButton.ColorState.Red; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); + } + + Btn2.Text = Converter.Button2; + Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible; + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + // 添加选择控件 + Btn1.IsEnabled = false; + foreach (var rawContent in (IEnumerable)Converter.Content) + { + // 1. Initialize and get the actual element + // Note: We use a new variable because 'foreach' variables are read-only + var content = MyVirtualizingElement.TryInit((FrameworkElement)rawContent); + + // 2. Interface casting and event subscription + if (content is IMyRadio selection) + { + PanSelection.Children.Add((UIElement)selection); + selection.Check += (sender, e) => OnChecked((IMyRadio)sender, e); + + // 3. Property configuration based on specific type + if (selection is MyListItem listItem) + { + listItem.Type = MyListItem.CheckType.RadioBox; + listItem.MinHeight = 24.0; + } + else if (selection is MyRadioBox radioBox) + { + radioBox.MinHeight = 24.0; + } + } + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "选择弹窗初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += Load; + Btn1.Click += Btn1_Click; + Btn2.Click += Btn2_Click; + LabTitle.MouseLeftButtonDown += Drag; + PanBorder.MouseLeftButtonDown += Drag; + } + + private void Load(object sender, EventArgs e) + { + try + { + // UI 初始化 + if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) + Btn1.ColorType = MyButton.ColorState.Highlight; + // 动画 + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (MyConverter.IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyMsgBox " + Uuid); + // 记录日志 + ModBase.Log("[Control] 选择弹窗:" + LabTitle.Text); + } + + catch (Exception ex) + { + ModBase.Log(ex, "选择弹窗加载失败", ModBase.LogLevel.Hint); + } + } + + private void Close() + { + // 结束线程阻塞 + MyConverter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + // 动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + if (!ModMain.WaitingMyMsgBox.Any()) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true) + }, "MyMsgBox " + Uuid); + } + + public void Btn1_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited || SelectedIndex == -1) + return; + MyConverter.IsExited = true; + MyConverter.Result = SelectedIndex; + Close(); + } + + public void Btn2_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + MyConverter.IsExited = true; + MyConverter.Result = null; + Close(); + } + + private void OnChecked(IMyRadio sender, EventArgs e) + { + Btn1.IsEnabled = true; + SelectedIndex = PanSelection.Children.IndexOf((UIElement)sender); + } + + private void Drag(object sender, MouseButtonEventArgs e) + { + try + { + if (e.LeftButton == MouseButtonState.Pressed) + if (e.GetPosition(ShapeLine).Y <= 2d) + ModMain.FrmMain.DragMove(); + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml index 469f70954..ed540acd2 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml @@ -1,8 +1,9 @@ - + @@ -11,7 +12,8 @@ - + @@ -21,18 +23,30 @@ - + - - + - - - - + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs new file mode 100644 index 000000000..6a07fe81c --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs @@ -0,0 +1,171 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using Microsoft.VisualBasic; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class MyMsgText +{ + private readonly ModMain.MyMsgBoxConverter MyConverter; + private readonly int Uuid = ModBase.GetUuid(); + + public MyMsgText(ModMain.MyMsgBoxConverter Converter) + { + try + { + InitializeComponent(); + Btn1.Name = Btn1.Name + ModBase.GetUuid(); + Btn2.Name = Btn2.Name + ModBase.GetUuid(); + Btn3.Name = Btn3.Name + ModBase.GetUuid(); + MyConverter = Converter; + LabTitle.Text = Converter.Title; + LabCaption.Text = Converter.Text; + Btn1.Text = Converter.Button1; + if (Converter.IsWarn) + { + Btn1.ColorType = MyButton.ColorState.Red; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrushRedLight"); + } + + Btn2.Text = Converter.Button2; + Btn3.Text = Converter.Button3; + Btn2.Visibility = string.IsNullOrEmpty(Converter.Button2) ? Visibility.Collapsed : Visibility.Visible; + Btn3.Visibility = string.IsNullOrEmpty(Converter.Button3) ? Visibility.Collapsed : Visibility.Visible; + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + } + + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += Load; + } + + private void Load(object sender, RoutedEventArgs e) + { + try + { + // UI 初始化 + if (Btn2.IsVisible && !(Btn1.ColorType == MyButton.ColorState.Red)) + Btn1.ColorType = MyButton.ColorState.Highlight; + Btn1.Focus(); + // 动画 + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (MyConverter.IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyMsgBox " + Uuid); + // 记录日志 + ModBase.Log("[Control] 普通弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text); + } + + catch (Exception ex) + { + ModBase.Log(ex, "普通弹窗加载失败", ModBase.LogLevel.Hint); + } + } + + private void Close() + { + // 结束线程阻塞 + if (MyConverter.ForceWait || !string.IsNullOrEmpty(MyConverter.Button2)) + MyConverter.WaitFrame.Continue = false; + ComponentDispatcher.PopModal(); + // 动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + if (!ModMain.WaitingMyMsgBox.Any()) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true) + }, "MyMsgBox " + Uuid); + } + + public void Btn1_Click(object? sender = null, MouseButtonEventArgs? e = null) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button1Action is not null) + { + MyConverter.Button1Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 1; + Close(); + } + } + + public void Btn2_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button2Action is not null) + { + MyConverter.Button2Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 2; + Close(); + } + } + + public void Btn3_Click(object sender, MouseButtonEventArgs e) + { + if (MyConverter.IsExited) + return; + if (MyConverter.Button3Action is not null) + { + MyConverter.Button3Action(); + } + else + { + MyConverter.IsExited = true; + MyConverter.Result = 3; + Close(); + } + } + + private void Drag(object sender, MouseButtonEventArgs e) + { + try + { + if (e.LeftButton == MouseButtonState.Pressed) + if (e.GetPosition(ShapeLine).Y <= 2d) + ModMain.FrmMain.DragMove(); + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽移动失败", ModBase.LogLevel.Hint); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyPageLeft.cs b/Plain Craft Launcher 2/Controls/MyPageLeft.cs new file mode 100644 index 000000000..a645e17d1 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyPageLeft.cs @@ -0,0 +1,165 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace PCL; + +public class MyPageLeft : Grid +{ + public static DependencyProperty AnimatedControlProperty = + DependencyProperty.Register("AnimatedControl", typeof(FrameworkElement), typeof(MyPageLeft)); + + private readonly int Uuid = ModBase.GetUuid(); + + private bool _animatedControlNullWarned; + + // 执行逐个进入动画的控件 + public FrameworkElement AnimatedControl + { + get + { + var res = GetValue(AnimatedControlProperty); + if (res is null && !_animatedControlNullWarned) + { + _animatedControlNullWarned = true; + ModBase.Log($"[MyPageLeft] 获取到 AnimatedControl(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug); + } + + return (FrameworkElement)res; + } + set => SetValue(AnimatedControlProperty, value); + } + + public void TriggerShowAnimation() + { + if (AnimatedControl is null) + { + // 缩放动画 + if (!(RenderTransform is ScaleTransform)) + { + RenderTransform = new ScaleTransform(0.96d, 0.96d); + RenderTransformOrigin = new Point(0.5d, 0.5d); + } + + Opacity = 0d; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, + Ease: new ModAnimation.AniEaseOutBack((ModAnimation.AniEasePower)2)), + ModAnimation.AaOpacity(this, 1d, 100) + }, "PageLeft PageChange " + Uuid); + } + else + { + // 逐个进入动画 + var AniList = new List(); + var Id = 0; + var Delay = 0; + foreach (var ElementRaw in GetAllAnimControls(true)) + { + var Element = MyVirtualizingElement.TryInit(ElementRaw); + if (Element.Visibility == Visibility.Collapsed) + { + // 还原之前的隐藏动画可能导致的改变(#2436) + Element.Opacity = 1d; + Element.RenderTransform = new TranslateTransform(0d, 0d); + if (Element is MyListItem) + ((MyListItem)Element).IsMouseOverAnimationEnabled = true; + } + else + { + Element.Opacity = 0d; + Element.RenderTransform = new TranslateTransform(-25, 0d); + if (Element is MyListItem) + ((MyListItem)Element).IsMouseOverAnimationEnabled = false; + AniList.Add(ModAnimation.AaOpacity(Element, Element is TextBlock ? 0.6d : 1d, 100, Delay, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + AniList.Add(ModAnimation.AaTranslateX(Element, 5d, 200, Delay, + new ModAnimation.AniEaseOutFluent())); + AniList.Add(ModAnimation.AaTranslateX(Element, 20d, 300, Delay, + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))); + if (Element is MyListItem) + AniList.Add(ModAnimation.AaCode(() => + { + ((MyListItem)Element).IsMouseOverAnimationEnabled = true; + ((MyListItem)Element).RefreshColor(this, new EventArgs()); + }, Delay + 280)); + Delay += Math.Max(15 - Id, 7) * 2; + Id += 1; + } + } + + ModAnimation.AniStart(AniList, "PageLeft PageChange " + Uuid); + } + } + + public void TriggerHideAnimation() + { + if (AnimatedControl is null) + { + // 缩放动画 + if (!(RenderTransform is ScaleTransform)) + { + RenderTransform = new ScaleTransform(1d, 1d); + RenderTransformOrigin = new Point(0.5d, 0.5d); + } + + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(this, 0.95d - ((ScaleTransform)RenderTransform).ScaleX, 110, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaOpacity(this, -Opacity, 80, 30) + }, "PageLeft PageChange " + Uuid); + } + else + { + // 逐个退出动画 + var AniList = new List(); + var Id = 0; + var Controls = GetAllAnimControls(); + foreach (var Element in Controls) + { + AniList.Add(ModAnimation.AaOpacity(Element, -Element.Opacity, 50, + (int)Math.Round(70d / Controls.Count * Id))); + AniList.Add(ModAnimation.AaTranslateX(Element, -6, 50, (int)Math.Round(70d / Controls.Count * Id))); + Id += 1; + } + + ModAnimation.AniStart(AniList, "PageLeft PageChange " + Uuid); + } + } + + // 遍历获取所有需要生成动画的控件 + private List GetAllAnimControls(bool IgnoreInvisibility = false) + { + var AllControls = new List(); + GetAllAnimControls(AnimatedControl, ref AllControls, IgnoreInvisibility); + return AllControls; + } + + private void GetAllAnimControls(FrameworkElement Element, ref List AllControls, + bool IgnoreInvisibility) + { + if (!IgnoreInvisibility && Element.Visibility == Visibility.Collapsed) + return; + if (Element is MyTextButton) + AllControls.Add(Element); + else if (Element is MyListItem) + AllControls.Add(Element); + else if (Element is ContentControl) + GetAllAnimControls((FrameworkElement)((ContentControl)Element).Content, ref AllControls, + IgnoreInvisibility); + else if (Element is Panel) + foreach (FrameworkElement Element2 in ((Panel)Element).Children) + GetAllAnimControls(Element2, ref AllControls, IgnoreInvisibility); + else + AllControls.Add(Element); + } +} + +public interface IRefreshable +{ + void Refresh(); +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyPageRight.cs b/Plain Craft Launcher 2/Controls/MyPageRight.cs new file mode 100644 index 000000000..b4c4e1605 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyPageRight.cs @@ -0,0 +1,714 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; + +namespace PCL; + +public class MyPageRight : AdornerDecorator +{ + // 当前状态 + public enum PageStates + { + Empty, // 默认状态,页面全空 + LoaderWait, // 加载环初始等待 + LoaderEnter, // 加载环进入动画 + LoaderStayForce, // 加载环正常显示(强制等待) + LoaderStay, // 加载环正常显示 + LoaderExit, // 加载环退出动画 + ContentEnter, // 内容进入动画 + ContentStay, // 内容正常显示 + ContentExit, // 刷新导致的全部退出动画,或页面内容退出(子页面更改)导致的全部退出动画 + PageExit // 切换页面导致的全部退出动画 + } + + private static readonly object PanScrollProperty = + DependencyProperty.Register("PanScroll", typeof(MyScrollViewer), typeof(MyPageRight)); + + private PageStates _PageState = PageStates.Empty; + + private bool _panScrollNullWarned; + + public int PageUuid = ModBase.GetUuid(); + + // “返回顶部” 按钮检测的滚动区域 + public MyScrollViewer PanScroll + { + get + { + var res = GetValue((DependencyProperty)PanScrollProperty); + if (res is null && !_panScrollNullWarned) + { + _panScrollNullWarned = true; + ModBase.Log($"[MyPageRight] 获取到 PanScroll(来自 {Name}) 的值为 null", ModBase.LogLevel.Debug); + } + + return (MyScrollViewer)res; + } + set => SetValue((dynamic)PanScrollProperty, value); + } + + public PageStates PageState + { + get => _PageState; + set + { + if (_PageState == value) + return; + _PageState = value; + if (ModBase.ModeDebug) + ModBase.Log("[UI] 页面状态切换为 " + ModBase.GetStringFromEnum(value)); + } + } + + #region 加载器 + + private ModLoader.LoaderBase PageLoader; + private Func? PageLoaderInputInvoke; + private MyLoading? PageLoaderUi; + private FrameworkElement PanLoader; + private FrameworkElement PanContent; + private FrameworkElement? PanAlways; + private bool PageLoaderAutoRun; + + // 初始化 + /// + /// 表明页面存在需要在后台执行的加载器。 + /// + /// MyLoading 控件。 + /// MyLoading 控件对应的卡片。 + /// 加载结束后出现的内容容器。 + /// 无论是否在加载总是要显示的容器。可以为 Nothing。 + /// 在工作线程执行的加载器。 + /// 当加载器执行完成,在 UI 线程触发的 UI 初始化事件。 + public void PageLoaderInit(MyLoading LoaderUi, FrameworkElement PanLoader, FrameworkElement PanContent, + FrameworkElement? PanAlways, ModLoader.LoaderBase RealLoader, Action? FinishedInvoke = null, + Func? InputInvoke = null, bool AutoRun = true) + { + // 初始化参数 + this.PanLoader = PanLoader; + this.PanContent = PanContent; + this.PanAlways = PanAlways; + PageLoader = RealLoader; + PageLoaderUi = LoaderUi; + PageLoaderInputInvoke = InputInvoke; + PageLoaderAutoRun = AutoRun; + // 添加结束 Invoke + if (FinishedInvoke is not null) + RealLoader.PreviewFinish += _ => + { + while (PageState == PageStates.PageExit || PageState == PageStates.ContentExit) + Thread.Sleep(10); // 不在退出动画时执行 UI 线程操作,避免退出动画被重置 + ModBase.RunInUiWait(() => FinishedInvoke(RealLoader)); + Thread.Sleep(20); // 由于大量初始化控件会导致掉帧,延迟触发 State 改变事件 + }; + RealLoader.OnStateChangedUi += (Loader, NewState, OldState) => + ModBase.RunInUi(() => PageLoaderState(Loader, NewState, OldState)); + // 隐藏 UI + PanLoader.Visibility = Visibility.Collapsed; + PanContent.Visibility = Visibility.Collapsed; + PanAlways?.Visibility = Visibility.Collapsed; + // 初次运行加载器 + if (PageLoaderAutoRun) + { + if (PageLoader is ModLoader.LoaderTask task) + { + task.Start(task.StartGetInputNoType(null, PageLoaderInputInvoke)); + } + else + { + object? Input = null; + if (PageLoaderInputInvoke is not null) + Input = PageLoaderInputInvoke(); + PageLoader.Start(Input); + } + } + + if (PageLoader.State == ModBase.LoadState.Finished && FinishedInvoke is not null) + ModBase.RunInUiWait(() => FinishedInvoke(RealLoader)); // 加载器已提前完成,直接触发事件 + // 设置加载环 + PageLoaderUi.State = RealLoader; + PageLoaderUi.Click += (_, _) => + { + if (RealLoader.State == ModBase.LoadState.Failed) PageLoaderRestart(); + }; // 点击重试事件 + } + + // 重试 + public void PageLoaderRestart(object Input = null, bool IsForceRestart = true) // 由外部调用的重试 + { + if (!PageLoaderAutoRun) + return; + if (PageLoader.GetType().Name.StartsWithF("LoaderTask")) + { + PageLoader.Start(((dynamic)PageLoader).StartGetInputNoType(Input, PageLoaderInputInvoke), IsForceRestart); + } + else + { + if (Input is null && PageLoaderInputInvoke is not null) + Input = PageLoaderInputInvoke; + PageLoader.Start(Input, IsForceRestart); + } + } + + #endregion + + #region 事件 + + // 外部触发的事件 + /// + /// 需要切换到当前页面,并且原本的 Loaded 事件已执行完成。 + /// 需要根据加载器状态,从 Empty 切换到 ContentEnter、LoaderWait、LoaderEnter。 + /// + public void PageOnEnter() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnEnter"); + PageEnter?.Invoke(); + switch (PageState) + { + case PageStates.Empty: + { + if (PageLoader is null || PageLoader.State == ModBase.LoadState.Finished || + PageLoader.State == ModBase.LoadState.Waiting || PageLoader.State == ModBase.LoadState.Aborted) + { + // 如果加载器在进入页面时不启动(例如联机),那么在此时就会有 State = Waiting + PageState = PageStates.ContentEnter; + TriggerEnterAnimation(PanAlways, (FrameworkElement)(PanContent ?? Child)); + } + else if (PageLoader.State == ModBase.LoadState.Loading) + { + PageState = PageStates.LoaderWait; + ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderWaitFinished, 400), + "PageRight PageChange " + PageUuid); + } + else // PageLoader.State = LoadState.Failed + { + PageState = PageStates.LoaderEnter; + TriggerEnterAnimation(PanAlways, PanLoader); + } + + break; + } + case PageStates.ContentExit: + { + // 和上面的一样,但是不管 PanAlways + if (PageLoader is null || PageLoader.State == ModBase.LoadState.Finished || + PageLoader.State == ModBase.LoadState.Waiting || PageLoader.State == ModBase.LoadState.Aborted) + { + PageState = PageStates.ContentEnter; + TriggerEnterAnimation((FrameworkElement)(PanContent ?? Child)); + } + else if (PageLoader.State == ModBase.LoadState.Loading) + { + PageState = PageStates.LoaderWait; + ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderWaitFinished, 400), + "PageRight PageChange " + PageUuid); + } + else // PageLoader.State = LoadState.Failed + { + PageState = PageStates.LoaderEnter; + TriggerEnterAnimation(PanLoader); + } + + break; + } + case PageStates.ContentEnter: // 重复调用 PageOnEnter,直接忽略 + { + break; + } + + default: + { + throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + " 时触发了 PageOnEnter 事件。"); + } + } + } + + public event PageEnterEventHandler? PageEnter; + + public delegate void PageEnterEventHandler(); + + /// + /// 需要切换到其他页面。 + /// 需要立即切换至 PageExit 或 Empty。 + /// + public void PageOnExit() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnExit"); + PageExit?.Invoke(); + switch (PageState) + { + case PageStates.ContentEnter: + case PageStates.ContentStay: + { + PageState = PageStates.PageExit; + TriggerExitAnimation(PanAlways, (FrameworkElement)(PanContent ?? Child)); + break; + } + case PageStates.LoaderEnter: + case PageStates.LoaderStayForce: + case PageStates.LoaderStay: + { + PageState = PageStates.PageExit; + TriggerExitAnimation(PanAlways, PanLoader); + break; + } + case PageStates.LoaderWait: + { + PageState = PageStates.PageExit; + TriggerExitAnimation(PanAlways); + break; + } + case PageStates.LoaderExit: + case PageStates.ContentExit: + { + PageState = PageStates.PageExit; + if (PanAlways is not null) + TriggerExitAnimation(PanAlways, (FrameworkElement)(PanContent ?? Child)); + break; + } + case PageStates.PageExit: + case PageStates.Empty: + { + break; + } + } + } + + public event PageExitEventHandler? PageExit; + + public delegate void PageExitEventHandler(); + + /// + /// 即将切换到其他页面,需要强制完成页面状态清理。 + /// 需要立即切换至 Empty。 + /// + public void PageOnForceExit() + { + if (PageState == PageStates.Empty) + return; + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnForceExit"); + PageState = PageStates.Empty; + ModAnimation.AniStop("PageRight PageChange " + PageUuid); + // 由于动画会被强制中止,所以需要手动进行隐藏 + if (PageLoader is null && Child is not null) + { + Child.Visibility = Visibility.Collapsed; + } + else + { + PanContent.Visibility = Visibility.Collapsed; + PanLoader.Visibility = Visibility.Collapsed; + if (PanAlways is not null) + PanAlways.Visibility = Visibility.Collapsed; + } + } + + /// + /// PanContent 中的子页面改变,需要让当前内容退出,再显示新的内容。 + /// 需要在 PageEnter 事件确认要显示的子页面有哪些。 + /// + public void PageOnContentExit() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnContentExit"); + if (PageLoader is not null && PageLoader.State == ModBase.LoadState.Loading) + throw new Exception("在调用 PageOnContentExit 时,加载器不能为 Loading 状态"); + // Loading 的加载器可能触发进一步变化,难以预测会触发子页面的动画还是加载器完成的动画 + switch (PageState) + { + case PageStates.ContentEnter: + case PageStates.ContentStay: + { + PageState = PageStates.ContentExit; + TriggerExitAnimation(PanContent); + break; + } + case PageStates.LoaderExit: + { + PageState = PageStates.ContentExit; + break; + } + case PageStates.LoaderEnter: + case PageStates.LoaderStayForce: + case PageStates.LoaderStay: + { + PageState = PageStates.ContentExit; + TriggerExitAnimation(PanLoader); + break; + } + case PageStates.LoaderWait: + case PageStates.Empty: + { + PageOnEnter(); + break; + } + } + } + + // 内部触发的事件 + /// + /// 逐个进入动画已执行完成。 + /// 需要根据目前状态,从 ContentEnter 切换到 ContentStay,或从 LoaderEnter 切换到 LoaderStayForce。 + /// + private void PageOnEnterAnimationFinished() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnEnterAnimationFinished"); + switch (PageState) + { + case PageStates.ContentEnter: + { + PageState = PageStates.ContentStay; + break; + } + case PageStates.LoaderEnter: + { + PageState = PageStates.LoaderStayForce; + ModAnimation.AniStart(ModAnimation.AaCode(PageOnLoaderStayFinished, 400), + "PageRight PageChange " + PageUuid); + break; + } + + default: + { + throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + + " 时触发了 PageOnEnterAnimationFinished 事件。"); + } + } + } + + /// + /// 逐个退出动画已执行完成。 + /// 需要根据目前状态,从 AllExit 切换到 Empty,或从 LoaderExit 切换到 ContentEnter,或从 ContentExit 重新触发 PageOnEnter。 + /// + private void PageOnExitAnimationFinished() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnExitAnimationFinished"); + switch (PageState) + { + case PageStates.PageExit: + { + PageState = PageStates.Empty; + break; + } + case PageStates.ContentExit: + { + PageOnEnter(); + break; + } + case PageStates.LoaderExit: + { + PageState = PageStates.ContentEnter; + TriggerEnterAnimation(PanContent); + break; + } + + default: + { + throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + + " 时触发了 PageOnExitAnimationFinished 事件。"); + } + } + } + + /// + /// 加载环进入等待已结束。 + /// 需要从 LoaderWait 切换到 LoaderEnter。 + /// + private void PageOnLoaderWaitFinished() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnLoaderWaitFinished"); + switch (PageState) + { + case PageStates.LoaderWait: + { + PageState = PageStates.LoaderEnter; + if (PanAlways is not null && PanAlways.Visibility == Visibility.Collapsed) + TriggerEnterAnimation(PanAlways, PanLoader); + else + TriggerEnterAnimation(PanLoader); + + break; + } + + default: + { + throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + + " 时触发了 PageOnLoaderWaitFinished 事件。"); + } + } + } + + /// + /// 加载环展示等待已结束。 + /// 需要从 LoaderStayForce 切换到 LoaderStay 或 LoaderExit。 + /// + private void PageOnLoaderStayFinished() + { + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageOnLoaderStayFinished"); + switch (PageState) + { + case PageStates.LoaderStayForce: + { + if (PageLoader.State == ModBase.LoadState.Finished) + { + PageState = PageStates.LoaderExit; + TriggerExitAnimation(PanLoader); + } + else + { + PageState = PageStates.LoaderStay; + } + + break; + } + + default: + { + throw new Exception("在状态为 " + ModBase.GetStringFromEnum(PageState) + + " 时触发了 PageOnLoaderWaitFinished 事件。"); + } + } + } + + /// + /// 全局加载状态已改变。 + /// + private void PageLoaderState(object sender, ModBase.LoadState NewState, ModBase.LoadState OldState) + { + switch (NewState) + { + case ModBase.LoadState.Failed: + case ModBase.LoadState.Loading: + { + if (OldState == ModBase.LoadState.Failed || OldState == ModBase.LoadState.Loading) + return; + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageLoaderState (Start/Refresh)"); + // (重新)开始运行 + // 需要从部分状态切换到 ReloadExit + switch (PageState) + { + case PageStates.ContentEnter: + case PageStates.ContentStay: + { + PageState = PageStates.ContentExit; + TriggerExitAnimation(PanContent); + break; + } + case PageStates.LoaderExit: + { + PageState = PageStates.ContentExit; + break; + } + } + + break; + } + case ModBase.LoadState.Finished: + case ModBase.LoadState.Aborted: + case ModBase.LoadState.Waiting: + { + if (!(OldState == ModBase.LoadState.Failed || OldState == ModBase.LoadState.Loading)) + return; + if (ModBase.ModeDebug) + ModBase.Log("[UI] 已触发 PageLoaderState (Stop/Abort)"); + // 运行结束 + // 需要从 LoaderWait 切换到 ContentEnter,或从 LoaderStay 切换到 LoaderExit + switch (PageState) + { + case PageStates.LoaderWait: + { + PageState = PageStates.ContentEnter; + if (PanAlways is not null && PanAlways.Visibility == Visibility.Collapsed) + TriggerEnterAnimation(PanAlways, PanContent); + else + TriggerEnterAnimation(PanContent); + + break; + } + case PageStates.LoaderStay: + { + PageState = PageStates.LoaderExit; + TriggerExitAnimation(PanLoader); + break; + } + } + + break; + } + } + } + + #endregion + + #region 动画 + + // 逐个进入动画 + public void TriggerEnterAnimation(params FrameworkElement[] Elements) + { + var RealElements = Elements.Where(e => e is not null); + foreach (var Element in RealElements) + Element.Visibility = Visibility.Visible; // 页面均处于默认的隐藏状态 + var AniList = new List(); + var Delay = 0; + // 基础动画 + foreach (var Element in RealElements) + { + foreach (var Control in GetAllAnimControls(Element, true)) + { + // 还原被隐藏的卡片的消失动画 + Control.IsHitTestVisible = true; + if (Control.RenderTransform is not null && Control.RenderTransform is TranslateTransform) + Control.RenderTransform = null; + } + + foreach (var Control in GetAllAnimControls(Element)) + if (Control is MyExtraTextButton) + { + ((MyExtraTextButton)Control).Show = true; + } + else + { + Control.Opacity = 0d; + Control.RenderTransform = new TranslateTransform(0d, -16); + AniList.Add(ModAnimation.AaOpacity(Control, 1d, 100, Delay, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + AniList.Add(ModAnimation.AaTranslateY(Control, 5d, 250, Delay, + new ModAnimation.AniEaseOutFluent())); + AniList.Add(ModAnimation.AaTranslateY(Control, 11d, 350, Delay, new ModAnimation.AniEaseOutBack())); + Delay += 25; + } + } + + // 滚动条动画 + var Scroll = GetFirstScrollViewer(RealElements); + if (Scroll is not null) + { + if (!(Scroll.RenderTransform is TranslateTransform)) + Scroll.RenderTransform = new TranslateTransform(10d, 0d); + AniList.Add(ModAnimation.AaTranslateX(Scroll, -((TranslateTransform)Scroll.RenderTransform).X, 350, 0, + new ModAnimation.AniEaseOutFluent())); + } + + // 结束 + AniList.Add(ModAnimation.AaCode(() => PageOnEnterAnimationFinished(), After: true)); + ModAnimation.AniStart(AniList, "PageRight PageChange " + PageUuid, true); + } + + // 逐个退出动画 + public void TriggerExitAnimation(params FrameworkElement[] Elements) + { + var RealElements = Elements.Where(e => e is not null); + var AniList = new List(); + var Delay = 0; + foreach (var Element in RealElements) + foreach (var Control in GetAllAnimControls(Element)) + if (Control is MyExtraTextButton) + { + ((MyExtraTextButton)Control).Show = false; + } + else + { + Control.IsHitTestVisible = false; + AniList.Add(ModAnimation.AaOpacity(Control, -1, 70, Delay)); + AniList.Add(ModAnimation.AaTranslateY(Control, -6, 70, Delay)); + Delay += 15; + } + + // 滚动条动画 + var Scroll = GetFirstScrollViewer(RealElements); + if (Scroll is not null) + { + if (!(Scroll.RenderTransform is TranslateTransform)) + Scroll.RenderTransform = new TranslateTransform(); + AniList.Add(ModAnimation.AaTranslateX(Scroll, 10d - ((TranslateTransform)Scroll.RenderTransform).X, 90, 0, + new ModAnimation.AniEaseInFluent())); + } + + // 结束 + AniList.Add(ModAnimation.AaCode(() => + { + foreach (var Element in RealElements) + Element.Visibility = Visibility.Collapsed; + PageOnExitAnimationFinished(); + }, After: true)); + ModAnimation.AniStart(AniList, "PageRight PageChange " + PageUuid); + } + + /// + /// 禁用页面切换动画的控件列表。 + /// + public List DisabledPageAnimControls = new(); + + /// + /// 遍历获取所有需要生成动画的控件。 + /// + internal IEnumerable GetAllAnimControls(FrameworkElement Element, bool IgnoreInvisibility = false) + { + var AllControls = new List(); + _GetAllAnimControls(Element, ref AllControls, IgnoreInvisibility); + return AllControls.Except(DisabledPageAnimControls); + } + + private void _GetAllAnimControls(FrameworkElement Element, ref List AllControls, + bool IgnoreInvisibility) + { + if (!IgnoreInvisibility && Element.Visibility == Visibility.Collapsed) + return; + if (Element is MyCard || Element is MyHint || Element is MyExtraTextButton || Element is TextBlock || + Element is MyTextButton) + { + AllControls.Add(Element); + } + else if (Element is ContentControl) + { + var Content = ((ContentControl)Element).Content; + if (Content is not null && Content is FrameworkElement) + _GetAllAnimControls((FrameworkElement)Content, ref AllControls, IgnoreInvisibility); + } + else if (Element is Panel) + { + foreach (var Element2 in ((Panel)Element).Children) + if (Element2 is FrameworkElement) + _GetAllAnimControls((FrameworkElement)Element2, ref AllControls, IgnoreInvisibility); + } + } + + // 查找列表中的第一个滚动条 + private MyScrollBar GetFirstScrollViewer(IEnumerable Elements) + { + MyScrollViewer Viewer = null; + foreach (var Element in Elements) + { + if (Element is MyScrollViewer) + { + Viewer = (MyScrollViewer)Element; + goto FindViewer; + } + + foreach (var Control in LogicalTreeHelper.GetChildren(Element)) + if (Control is MyScrollViewer) + { + Viewer = (MyScrollViewer)Control; + goto FindViewer; + } + } + + return null; + FindViewer: ; + + if (Viewer.ComputedVerticalScrollBarVisibility != Visibility.Visible) + return null; + return Viewer.ScrollBar; + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyRadioBox.xaml b/Plain Craft Launcher 2/Controls/MyRadioBox.xaml index f56125167..fe76415a3 100644 --- a/Plain Craft Launcher 2/Controls/MyRadioBox.xaml +++ b/Plain Craft Launcher 2/Controls/MyRadioBox.xaml @@ -1,9 +1,15 @@ - - - - - + + + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.cs b/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.cs new file mode 100644 index 000000000..0db3d8ead --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyRadioBox.xaml.cs @@ -0,0 +1,368 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyRadioBox : IMyRadio +{ + public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e); + + public delegate void PreviewCheckEventHandler(object sender, ModBase.RouteEventArgs e); + + // 指向动画 + + private const int AnimationTimeOfMouseIn = 100; // 鼠标指向动画长度 + private const int AnimationTimeOfMouseOut = 200; // 鼠标指向动画长度 + + private const int AnimationTimeOfCheck = 150; // 勾选状态变更动画长度 + + // 在使用 XAML 设置 Checked 属性时,不会触发 Checked_Set 方法,所以需要在这里手动触发 UI 改变 + public static readonly DependencyProperty CheckedProperty = DependencyProperty.Register("Checked", typeof(bool), + typeof(MyRadioBox), new PropertyMetadata(false, (dRaw, e) => + { + var d = (MyRadioBox)dRaw; + if (!d.IsLoaded) d.SyncUI(); + })); + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyRadioBox), new PropertyMetadata((sender, e) => + { + if (sender is not null) ((MyRadioBox)sender).LabText.Text = Conversions.ToString(e.NewValue); + })); + + private bool AllowMouseDown = true; + + // 点击事件 + + private bool MouseDowned; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyRadioBox() + { + InitializeComponent(); + MouseLeftButtonUp += (_, __) => Radiobox_MouseUp(); + MouseLeftButtonDown += (_, __) => Radiobox_MouseDown(); + MouseLeave += (_, __) => Radiobox_MouseLeave(); + IsEnabledChanged += (_, __) => Radiobox_IsEnabledChanged(); + MouseEnter += (_, __) => Radiobox_MouseEnterAnimation(); + MouseLeave += (_, __) => Radiobox_MouseLeaveAnimation(); + } + + // 自定义属性 + public bool Checked + { + get => Conversions.ToBoolean(GetValue(CheckedProperty)); + set => SetChecked(value, false); + } + + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } // 内容 + + public event IMyRadio.CheckEventHandler? Check; + public event IMyRadio.ChangedEventHandler? Changed; + public event PreviewCheckEventHandler? PreviewCheck; + public event PreviewChangeEventHandler? PreviewChange; + + /// + /// 手动设置 Checked 属性。 + /// + /// 新的 Checked 属性。 + /// 是否由用户引发。 + public void SetChecked(bool value, bool user) + { + try + { + // Preview 事件 + if (value && user) + { + var e = new ModBase.RouteEventArgs(user); + PreviewCheck?.Invoke(this, e); + if (e.Handled) + { + Radiobox_MouseLeave(); + return; + } + } + + // 自定义属性基础 + var IsChanged = false; + if (IsLoaded && !(value == Checked)) + PreviewChange?.Invoke(this, new ModBase.RouteEventArgs(user)); + if (!(value == Checked)) + { + SetValue(CheckedProperty, value); + IsChanged = true; + } + + // 保证只有一个单选框选中 + if (Parent is null) + return; + var RadioboxList = new List(); + var CheckedCount = 0; + foreach (var Control in ((Panel)Parent).Children) // 收集控件列表与选中个数 + if (Control is MyRadioBox radioBox) + { + RadioboxList.Add(radioBox); + if (radioBox.Checked) + CheckedCount += 1; + } + + switch (CheckedCount) // 判断选中情况 + { + case 0: + { + // 没有任何单选框被选中,选择第一个 + RadioboxList[0].Checked = true; + break; + } + case var @case when @case > 1: + { + // 选中项目多于 1 个 + if (Checked) + { + // 如果本控件选中,则取消其他所有控件的选中 + foreach (var Control in RadioboxList) + if (Control.Checked && !Control.Equals(this)) + Control.Checked = false; + } + else + { + // 如果本控件未选中,则只保留第一个选中的控件 + var FirstChecked = false; + foreach (var Control in RadioboxList) + if (Control.Checked) + { + if (FirstChecked) + Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发 + else + FirstChecked = true; + } + } + + break; + } + } + + // 触发事件 + if (IsChanged) + { + if (Checked) + Check?.Invoke(this, new ModBase.RouteEventArgs(user)); + Changed?.Invoke(this, new ModBase.RouteEventArgs(user)); + } + + // 更改动画 + SyncUI(); + } + catch (Exception ex) + { + ModBase.Log(ex, "单选框勾选改变错误", ModBase.LogLevel.Hint); + } + } + + private void SyncUI() + { + if (ModAnimation.AniControlEnabled == 0 && IsLoaded) // 防止默认属性变更触发动画 + { + if (Checked) + { + // 由无变有 + if (ShapeDot.Opacity < 0.01d) + ShapeDot.Opacity = 1d; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeBorder, 10d - ShapeBorder.Width, AnimationTimeOfCheck, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak), Absolute: true), + ModAnimation.AaScale(ShapeBorder, 8d, AnimationTimeOfCheck * 2, + (int)Math.Round(AnimationTimeOfCheck * 0.6d), new ModAnimation.AniEaseOutBack(), + Absolute: true) + }, "MyRadioBox Border " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeDot, 9d - ShapeDot.Width, + (int)Math.Round(AnimationTimeOfCheck * 2.6d), + Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak), Absolute: true), + ModAnimation.AaOpacity(ShapeDot, 1d - ShapeDot.Opacity, + (int)Math.Round(AnimationTimeOfCheck * 0.5d), (int)Math.Round(AnimationTimeOfCheck * 0.6d)) + }, "MyRadioBox Dot " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, + IsMouseOver ? "ColorBrush3" : IsEnabled ? "ColorBrush2" : "ColorBrushGray4", + AnimationTimeOfCheck) + }, "MyRadioBox BorderColor " + Uuid); + } + else + { + // 由有变无 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width, AnimationTimeOfCheck, + Ease: new ModAnimation.AniEaseOutFluent(), Absolute: true) + }, "MyRadioBox Border " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScale(ShapeDot, -ShapeDot.Width, AnimationTimeOfCheck, + Ease: new ModAnimation.AniEaseInFluent(), Absolute: true), + ModAnimation.AaOpacity(ShapeDot, -ShapeDot.Opacity, + (int)Math.Round(AnimationTimeOfCheck * 0.5d), (int)Math.Round(AnimationTimeOfCheck * 0.2d)) + }, "MyRadioBox Dot " + Uuid); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, + IsMouseOver ? "ColorBrush3" : IsEnabled ? "ColorBrush1" : "ColorBrushGray4", + AnimationTimeOfCheck) + }, "MyRadioBox BorderColor " + Uuid); + } + } + else + { + // 不使用动画 + ModAnimation.AniStop("MyRadioBox Border " + Uuid); + ModAnimation.AniStop("MyRadioBox Dot " + Uuid); + ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid); + if (Checked) + { + ShapeDot.Width = 9d; + ShapeDot.Height = 9d; + ShapeDot.Opacity = 1d; + ShapeDot.Margin = new Thickness(5.5d, 0d, 0d, 0d); + ShapeBorder.SetResourceReference(Shape.StrokeProperty, IsEnabled ? "ColorBrush2" : "ColorBrushGray4"); + } + else + { + ShapeDot.Width = 0d; + ShapeDot.Height = 0d; + ShapeDot.Opacity = 0d; + ShapeDot.Margin = new Thickness(10d, 0d, 0d, 0d); + ShapeBorder.SetResourceReference(Shape.StrokeProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4"); + } + } + } + + private void Radiobox_MouseUp() + { + if (!MouseDowned) + return; + ModBase.Log("[Control] 按下单选框:" + Text); + SetChecked(true, true); + MouseDowned = false; + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushHalfWhite", 100), + "MyRadioBox Background " + Uuid); + } + + private void Radiobox_MouseDown() + { + MouseDowned = true; + Focus(); + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushBg1", 100), + "MyRadioBox Background " + Uuid); + if (!Checked) + ModAnimation.AniStart( + ModAnimation.AaScale(ShapeBorder, 16.5d - ShapeBorder.Width, 1000, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + "MyRadioBox Border " + Uuid); + } + + private void Radiobox_MouseLeave() + { + if (!MouseDowned) + return; + MouseDowned = false; + ModAnimation.AniStart(ModAnimation.AaColor(ShapeBorder, Shape.FillProperty, "ColorBrushHalfWhite", 100), + "MyRadioBox Background " + Uuid); + if (!Checked) + ModAnimation.AniStart( + ModAnimation.AaScale(ShapeBorder, 18d - ShapeBorder.Width, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong), Absolute: true), + "MyRadioBox Border " + Uuid); + } + + private void Radiobox_IsEnabledChanged() + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + if (IsEnabled) + { + // 可用 + Radiobox_MouseLeaveAnimation(); + } + else + { + // 不可用 + ModAnimation.AniStart( + ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, ModSecret.ColorGray4 - ShapeBorder.Stroke, + AnimationTimeOfMouseOut), "MyRadioBox BorderColor " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + ModSecret.ColorGray4 - LabText.Foreground, AnimationTimeOfMouseOut), + "MyRadioBox TextColor " + Uuid); + } + } + else + { + // 无动画 + ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid); + ModAnimation.AniStop("MyRadioBox TextColor " + Uuid); + LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4"); + ShapeBorder.SetResourceReference(Shape.StrokeProperty, + IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4"); + } + } + + private void Radiobox_MouseEnterAnimation() + { + ModAnimation.AniStart( + ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, "ColorBrush3", AnimationTimeOfMouseIn), + "MyRadioBox BorderColor " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn), + "MyRadioBox TextColor " + Uuid); + } + + private void Radiobox_MouseLeaveAnimation() + { + if (!IsEnabled) + return; // MouseLeave 比 IsEnabledChanged 后执行,所以如果自定义事件修改了 IsEnabled,将导致显示错误 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) + { + ModAnimation.AniStart( + ModAnimation.AaColor(ShapeBorder, Shape.StrokeProperty, + IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfMouseOut), + "MyRadioBox BorderColor " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + IsEnabled ? "ColorBrush1" : "ColorBrushGray4", AnimationTimeOfMouseOut), + "MyRadioBox TextColor " + Uuid); + } + else + { + ModAnimation.AniStop("MyRadioBox BorderColor " + Uuid); + ModAnimation.AniStop("MyRadioBox TextColor " + Uuid); + ShapeBorder.SetResourceReference(Shape.StrokeProperty, + IsEnabled ? Checked ? "ColorBrush2" : "ColorBrush1" : "ColorBrushGray4"); + LabText.SetResourceReference(TextBlock.ForegroundProperty, IsEnabled ? "ColorBrush1" : "ColorBrushGray4"); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyRadioButton.xaml b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml index aa42f53c6..1cb4df2ae 100644 --- a/Plain Craft Launcher 2/Controls/MyRadioButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml @@ -1,9 +1,12 @@ - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyRadioButton.xaml.cs b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml.cs new file mode 100644 index 000000000..cef94ddb4 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml.cs @@ -0,0 +1,435 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Markup; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("Inlines")] +public partial class MyRadioButton +{ + public delegate void ChangeEventHandler(MyRadioButton sender, bool raiseByMouse); + + public delegate void CheckEventHandler(MyRadioButton sender, bool raiseByMouse); + + public delegate void PreviewClickEventHandler(object sender, ModBase.RouteEventArgs e); + + public enum ColorState + { + White, + Highlight + } + + // 动画 + + private const int AnimationTimeOfMouseIn = 90; // 鼠标指向动画长度 + private const int AnimationTimeOfMouseOut = 150; // 鼠标移出动画长度 + private const int AnimationTimeOfCheck = 120; // 勾选状态变更动画长度 + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyRadioButton), new PropertyMetadata((sender, e) => + { + if (sender is MyRadioButton rb && rb.LabText != null) rb.LabText.Text = Conversions.ToString(e.NewValue); + })); + + private bool _Checked; // 是否选中 + private ColorState _ColorType = ColorState.White; + private double _LogoScale = 1d; + private bool IsMouseDown; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyRadioButton() + { + InitializeComponent(); + + Loaded += (_, __) => + { + if (LabText != null) + LabText.Text = Conversions.ToString(GetValue(TextProperty)); + }; + + MouseLeftButtonUp += (_, __) => Radiobox_MouseUp(); + MouseLeftButtonDown += (_, __) => Radiobox_MouseDown(); + MouseLeave += (_, __) => Radiobox_MouseLeave(); + MouseEnter += RefreshColor; + MouseLeave += RefreshColor; + Loaded += RefreshColor; + } + + // 自定义属性 + + public string Logo + { + get => ShapeLogo.Data.ToString(); + set + { + if (ShapeLogo == null) return; + ShapeLogo.Data = (Geometry)new GeometryConverter().ConvertFromString(value); + } + } + + public double LogoScale + { + get => _LogoScale; + set + { + _LogoScale = value; + if (!(ShapeLogo == null)) + ShapeLogo.RenderTransform = new ScaleTransform { ScaleX = LogoScale, ScaleY = LogoScale }; + } + } + + public bool Checked + { + get => _Checked; + set => SetChecked(value, false, true); + } + + public InlineCollection Inlines => LabText.Inlines; + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } // 内容 + + public ColorState ColorType + { + get => _ColorType; + set + { + _ColorType = value; + RefreshColor(); + } + } // 颜色类别 + + public event CheckEventHandler? Check; + public event ChangeEventHandler? Change; + + public void RaiseChange() + { + Change?.Invoke(this, false); + } // 使外部程序可以引发本控件的 Change 事件 + + /// + /// 手动设置 Checked 属性。 + /// + /// 新的 Checked 属性。 + /// 是否由用户引发。 + /// 是否执行动画。 + public void SetChecked(bool value, bool user, bool anime) + { + try + { + // 自定义属性基础 + + var IsChanged = false; + if (IsLoaded && !(value == _Checked)) + Change?.Invoke(this, user); + if (!(value == _Checked)) + { + _Checked = value; + IsChanged = true; + } + + // 保证只有一个单选框选中 + + if (Parent == null) + return; + var RadioboxList = new List(); + var CheckedCount = 0; + // 收集控件列表与选中个数 + foreach (var Control in ((Panel)Parent).Children) + if (Control is MyRadioButton radioButton) + { + RadioboxList.Add(radioButton); + if (radioButton.Checked) + CheckedCount += 1; + } + + // 判断选中情况 + switch (CheckedCount) + { + case 0: + { + // 没有任何单选框被选中,选择第一个 + RadioboxList[0].Checked = true; + break; + } + case var @case when @case > 1: + { + // 选中项目多于 1 个 + if (Checked) + { + // 如果本控件选中,则取消其他所有控件的选中 + foreach (var Control in RadioboxList) + if (Control.Checked && !Control.Equals(this)) + Control.Checked = false; + } + else + { + // 如果本控件未选中,则只保留第一个选中的控件 + var FirstChecked = false; + foreach (var Control in RadioboxList) + if (Control.Checked) + { + if (FirstChecked) + Control.Checked = false; // 修改 Checked 会自动触发 Change 事件,所以不用额外触发 + else + FirstChecked = true; + } + } + + break; + } + } + + // 更改动画 + + if (!IsChanged) + return; + RefreshColor(null, anime); + + // 触发事件 + if (Checked) + Check?.Invoke(this, user); + } + + catch (Exception ex) + { + ModBase.Log(ex, "单选按钮勾选改变错误", ModBase.LogLevel.Hint); + } + } + + // 点击事件 + + public event PreviewClickEventHandler? PreviewClick; + + private void Radiobox_MouseUp() + { + if (Checked) + return; + if (!IsMouseDown) + return; + ModBase.Log("[Control] 按下单选按钮:" + Text); + IsMouseDown = false; + var e = new ModBase.RouteEventArgs(true); + PreviewClick?.Invoke(this, e); + if (e.Handled) + return; + SetChecked(true, true, true); + } + + private void Radiobox_MouseDown() + { + if (Checked) + return; + IsMouseDown = true; + RefreshColor(); + } + + private void Radiobox_MouseLeave() + { + IsMouseDown = false; + } + + private void RefreshColor(object obj = null, object e = null) + { + try + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0 && + !false.Equals(e)) // 防止默认属性变更触发动画,若强制不执行动画,则 e 为 False + { + switch (ColorType) + { + case ColorState.White: + { + if (Checked) + { + // 勾选 + var color3 = new ModBase.MyColor(ModSecret.AppResources["ColorObject3"]); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, color3 - ShapeLogo.Fill, + AnimationTimeOfCheck), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + color3 - LabText.Foreground, AnimationTimeOfCheck) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + new ModBase.MyColor(255d, 255d, 255d) - Background, AnimationTimeOfCheck), + "MyRadioButton Color " + Uuid); + } + else if (IsMouseDown) + { + // 按下 + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + new ModBase.MyColor(120d, + new ModBase.MyColor(ModSecret.AppResources["ColorObject8"])) - Background, 60), + "MyRadioButton Color " + Uuid); + } + else if (IsMouseOver) + { + // 指向 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, + new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill, AnimationTimeOfMouseIn), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground, + AnimationTimeOfMouseIn) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + new ModBase.MyColor(50d, + new ModBase.MyColor(ModSecret.AppResources["ColorObject8"])) - Background, + AnimationTimeOfMouseIn), "MyRadioButton Color " + Uuid); + } + else + { + // 正常 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, + new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill, + AnimationTimeOfMouseOut), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground, + AnimationTimeOfMouseOut) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + new ModBase.MyColor(ModSecret.AppResources["ColorBrushSemiTransparent"]) - + Background, AnimationTimeOfMouseOut), "MyRadioButton Color " + Uuid); + } + + break; + } + case ColorState.Highlight: + { + if (Checked) + { + // 勾选 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, + new ModBase.MyColor(255d, 255d, 255d) - ShapeLogo.Fill, AnimationTimeOfCheck), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, + new ModBase.MyColor(255d, 255d, 255d) - LabText.Foreground, + AnimationTimeOfCheck) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush3", AnimationTimeOfCheck), + "MyRadioButton Color " + Uuid); + } + else if (IsMouseDown) + { + // 按下 + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush6", AnimationTimeOfMouseIn), + "MyRadioButton Color " + Uuid); + } + else if (IsMouseOver) + { + // 指向 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3", + AnimationTimeOfMouseIn), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", + AnimationTimeOfMouseIn) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, "ColorBrush7", AnimationTimeOfMouseIn), + "MyRadioButton Color " + Uuid); + } + else + { + // 正常 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(ShapeLogo, Shape.FillProperty, "ColorBrush3", + AnimationTimeOfMouseOut), + ModAnimation.AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", + AnimationTimeOfMouseOut) + }, "MyRadioButton Checked " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaColor(this, BackgroundProperty, + new ModBase.MyColor(ModSecret.AppResources["ColorBrushSemiTransparent"]) - + Background, AnimationTimeOfMouseOut), "MyRadioButton Color " + Uuid); + } + + break; + } + } + } + + else + { + // 不使用动画 + ModAnimation.AniStop("MyRadioButton Checked " + Uuid); + ModAnimation.AniStop("MyRadioButton Color " + Uuid); + switch (ColorType) + { + case ColorState.White: + { + if (Checked) + { + Background = new ModBase.MyColor(255d, 255d, 255d); + ShapeLogo.SetResourceReference(Shape.FillProperty, "ColorBrush3"); + LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3"); + } + else + { + Background = (Brush)ModSecret.AppResources["ColorBrushSemiTransparent"]; + ShapeLogo.Fill = new ModBase.MyColor(255d, 255d, 255d); + LabText.Foreground = new ModBase.MyColor(255d, 255d, 255d); + } + + break; + } + case ColorState.Highlight: + { + if (Checked) + { + SetResourceReference(BackgroundProperty, "ColorBrush3"); + ShapeLogo.Fill = new ModBase.MyColor(255d, 255d, 255d); + LabText.Foreground = new ModBase.MyColor(255d, 255d, 255d); + } + else + { + Background = (Brush)ModSecret.AppResources["ColorBrushSemiTransparent"]; + ShapeLogo.SetResourceReference(Shape.FillProperty, "ColorBrush3"); + LabText.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush3"); + } + + break; + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新单选按钮颜色出错"); + } + } + + public void RefreshMyRadioButtonColor() + { + RefreshColor(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyResizer.cs b/Plain Craft Launcher 2/Controls/MyResizer.cs new file mode 100644 index 000000000..97c35e46b --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyResizer.cs @@ -0,0 +1,399 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Threading; + +namespace PCL; + +public class MyResizer +{ + private static int workAreaMaxHeight = -1; + private readonly Dictionary downElements = new(); + + private readonly Dictionary leftElements = new(); + private readonly Dictionary rightElements = new(); + + private readonly Window target; + private readonly Dictionary upElements = new(); + + private HwndSource hs; + private bool resizeDown; + private bool resizeLeft; + + private bool resizeRight; + private bool resizeUp; + + private PointAPI startMousePoint; + private POINT startWindowLeftUpPoint; + private Size startWindowSize; + + public MyResizer(Window target) + { + this.target = target; + if (target == null) + throw new Exception("Invalid Window handle"); + target.SourceInitialized += MyMacClass_SourceInitialized; + } + + private void MyMacClass_SourceInitialized(object sender, EventArgs e) + { + hs = PresentationSource.FromVisual((Visual)sender) as HwndSource; + hs.AddHook(WndProc); + } + + private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) + { + if (msg == 36) + { + WmGetMinMaxInfo(hwnd, lParam); + handled = true; + } + + return 0; + } + + private static void WmGetMinMaxInfo(nint hwnd, nint lParam) + { + var mINMAXINFO = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO)); + var flags = 2; + var intPtr = MonitorFromWindow(hwnd, flags); + var flag = !intPtr.Equals(nint.Zero); + if (flag) + { + var mONITORINFO = new MONITORINFO(); + GetMonitorInfo(intPtr, mONITORINFO); + var rcWork = mONITORINFO.rcWork; + var rcMonitor = mONITORINFO.rcMonitor; + mINMAXINFO.ptMaxPosition.x = Math.Abs(rcWork.left - rcMonitor.left); + mINMAXINFO.ptMaxPosition.y = Math.Abs(rcWork.top - rcMonitor.top); + mINMAXINFO.ptMaxSize.y = Math.Abs(rcWork.bottom - rcWork.top); + workAreaMaxHeight = mINMAXINFO.ptMaxSize.y; + if (rcWork.Height == rcMonitor.Height) mINMAXINFO.ptMaxSize.y -= 2; + } + + Marshal.StructureToPtr(mINMAXINFO, lParam, true); + } + + [DllImport("user32")] + private static extern bool GetMonitorInfo(nint hMonitor, MONITORINFO lpmi); + + [DllImport("User32")] + private static extern nint MonitorFromWindow(nint handle, int flags); + + private void connectMouseHandlers(UIElement element) + { + element.MouseLeftButtonDown += element_MouseLeftButtonDown; + } + + public void addResizerRight(UIElement element) + { + connectMouseHandlers(element); + rightElements.TryAdd(element, 0); + } + + public void addResizerLeft(UIElement element) + { + connectMouseHandlers(element); + leftElements.TryAdd(element, 0); + } + + public void addResizerUp(UIElement element) + { + connectMouseHandlers(element); + upElements.TryAdd(element, 0); + } + + public void addResizerDown(UIElement element) + { + connectMouseHandlers(element); + downElements.TryAdd(element, 0); + } + + public void addResizerRightDown(UIElement element) + { + connectMouseHandlers(element); + rightElements.TryAdd(element, 0); + downElements.TryAdd(element, 0); + } + + public void addResizerLeftDown(UIElement element) + { + connectMouseHandlers(element); + leftElements.TryAdd(element, 0); + downElements.TryAdd(element, 0); + } + + public void addResizerRightUp(UIElement element) + { + connectMouseHandlers(element); + rightElements.TryAdd(element, 0); + upElements.TryAdd(element, 0); + } + + public void addResizerLeftUp(UIElement element) + { + connectMouseHandlers(element); + leftElements.TryAdd(element, 0); + upElements.TryAdd(element, 0); + } + + public void removeAllResizers() + { + leftElements.Clear(); + rightElements.Clear(); + upElements.Clear(); + downElements.Clear(); + } + + private void element_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + GetCursorPos(out startMousePoint); + startMousePoint.X = (int)Math.Round(ModBase.GetWPFSize(startMousePoint.X)); + startMousePoint.Y = (int)Math.Round(ModBase.GetWPFSize(startMousePoint.Y)); + startWindowSize = new Size(target.Width, target.Height); + startWindowLeftUpPoint = new POINT((int)Math.Round(target.Left), (int)Math.Round(target.Top)); + var key = (UIElement)sender; + if (leftElements.ContainsKey(key)) + resizeLeft = true; + if (rightElements.ContainsKey(key)) + resizeRight = true; + if (upElements.ContainsKey(key)) + resizeUp = true; + if (downElements.ContainsKey(key)) + resizeDown = true; + ModBase.RunInNewThread(updateSizeLoop, "窗口大小调整检测"); + } + + private void updateSizeLoop() + { + try + { + while (resizeDown || resizeLeft || resizeRight || resizeUp) + { + target.Dispatcher.Invoke(updateSize, DispatcherPriority.Render); + target.Dispatcher.Invoke(updateMouseDown, DispatcherPriority.Render); + Thread.Sleep(0); + } + } + catch + { + } + } + + private void updateSize() + { + PointAPI pointAPI = default; + GetCursorPos(out pointAPI); + pointAPI.X = (int)Math.Round(ModBase.GetWPFSize(pointAPI.X)); + pointAPI.Y = (int)Math.Round(ModBase.GetWPFSize(pointAPI.Y)); + try + { + double NewWidth = -1; + double NewHeight = -1; + double NewLeft = -10000; + double NewTop = -10000; + + if (resizeRight) + { + if (target.Width == target.MinWidth) + { + if (startMousePoint.X < pointAPI.X) + NewWidth = startWindowSize.Width - (startMousePoint.X - pointAPI.X); + } + else if (startWindowSize.Width - (startMousePoint.X - pointAPI.X) >= target.MinWidth) + { + NewWidth = startWindowSize.Width - (startMousePoint.X - pointAPI.X); + } + else + { + NewWidth = target.MinWidth; + } + } + + if (resizeDown) + { + if (target.Height == target.MinHeight) + { + if (startMousePoint.Y < pointAPI.Y) + { + if (workAreaMaxHeight > 0) + NewHeight = + startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + target.Top <= + workAreaMaxHeight + ? startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + : workAreaMaxHeight - target.Top; + else + NewHeight = startWindowSize.Height - (startMousePoint.Y - pointAPI.Y); + } + } + else if (startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) >= target.MinHeight) + { + if (workAreaMaxHeight > 0) + NewHeight = + startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + target.Top <= workAreaMaxHeight + ? startWindowSize.Height - (startMousePoint.Y - pointAPI.Y) + : workAreaMaxHeight - target.Top; + else + NewHeight = startWindowSize.Height - (startMousePoint.Y - pointAPI.Y); + } + else + { + NewHeight = target.MinHeight; + } + } + + if (resizeLeft) + { + if (target.Width == target.MinWidth) + { + if (startMousePoint.X > pointAPI.X) + { + NewWidth = startWindowSize.Width + startMousePoint.X - pointAPI.X; + NewLeft = startWindowLeftUpPoint.x - (startMousePoint.X - pointAPI.X); + } + else + { + NewWidth = target.MinWidth; + NewLeft = startWindowLeftUpPoint.x + startWindowSize.Width - target.Width; + } + } + else if (startWindowSize.Width + (startMousePoint.X - pointAPI.X) >= target.MinWidth) + { + NewWidth = startWindowSize.Width + (startMousePoint.X - pointAPI.X); + NewLeft = startWindowLeftUpPoint.x - (startMousePoint.X - pointAPI.X); + } + else + { + NewWidth = target.MinWidth; + NewLeft = startWindowLeftUpPoint.x + startWindowSize.Width - target.Width; + } + } + + if (resizeUp) + { + if (target.Height == target.MinHeight) + { + if (startMousePoint.Y > pointAPI.Y) + { + NewHeight = startWindowSize.Height + startMousePoint.Y - pointAPI.Y; + NewTop = startWindowLeftUpPoint.y - (startMousePoint.Y - pointAPI.Y); + } + else + { + NewHeight = target.MinHeight; + NewTop = startWindowLeftUpPoint.y + startWindowSize.Height - target.Height; + } + } + else if (startWindowSize.Height + (startMousePoint.Y - pointAPI.Y) >= target.MinHeight) + { + NewHeight = startWindowSize.Height + startMousePoint.Y - pointAPI.Y; + NewTop = startWindowLeftUpPoint.y - (startMousePoint.Y - pointAPI.Y); + } + else + { + NewHeight = target.MinHeight; + NewTop = startWindowLeftUpPoint.y + startWindowSize.Height - target.Height; + } + } + + if (NewWidth > 10d && Math.Abs(NewWidth - target.Width) > 0.7d) + target.Width = NewWidth; + if (NewHeight > 10d && Math.Abs(NewHeight - target.Height) > 0.7d) + target.Height = NewHeight; + if (NewLeft > -9999 && Math.Abs(NewLeft - target.Left) > 0.7d) + target.Left = NewLeft; + if (NewTop > -9999 && Math.Abs(NewTop - target.Top) > 0.7d) + target.Top = NewTop; + } + catch + { + } + } + + private void updateMouseDown() + { + var flag = (GetAsyncKeyState(0x1) & 0x8000) == 0; // 调用原生API判断鼠标是否抬起,如果使用WPF的API的话鼠标不在窗口上时不会更新状态 (#5655) + if (flag) + { + resizeRight = false; + resizeLeft = false; + resizeUp = false; + resizeDown = false; + } + } + + [DllImport("user32.dll")] + private static extern bool GetCursorPos(out PointAPI lpPoint); + + [DllImport("user32.dll")] + private static extern short GetAsyncKeyState(int vKey); + + private delegate void RefreshDelegate(); + + private struct POINT + { + public int x; + public int y; + + public POINT(int x, int y) + { + this.x = x; + this.y = y; + } + } + + private struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + } + + private struct RECT + { + public readonly int left; + public readonly int top; + public readonly int right; + public readonly int bottom; + public static RECT Empty = default; + + public int Width => Math.Abs(right - left); + + public int Height => bottom - top; + + public RECT(int left, int top, int right, int bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + public RECT(RECT rcSrc) + { + left = rcSrc.left; + top = rcSrc.top; + right = rcSrc.right; + bottom = rcSrc.bottom; + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private class MONITORINFO + { + public int cbSize = Marshal.SizeOf(typeof(MONITORINFO)); + public readonly RECT rcMonitor = default; + public readonly RECT rcWork = default; + public int dwFlags = 0; + } + + private struct PointAPI + { + public int X; + public int Y; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyScrollBar.cs b/Plain Craft Launcher 2/Controls/MyScrollBar.cs new file mode 100644 index 000000000..877ecd64b --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyScrollBar.cs @@ -0,0 +1,81 @@ +using System.Windows.Controls.Primitives; + +namespace PCL; + +public class MyScrollBar : ScrollBar +{ + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyScrollBar() + { + IsEnabledChanged += (_, __) => RefreshColor(); + GotMouseCapture += (_, __) => RefreshColor(); + LostMouseCapture += (_, __) => RefreshColor(); + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + IsVisibleChanged += (_, __) => RefreshColor(); + } + + // 指向动画 + + private void RefreshColor() + { + try + { + // 判断当前颜色 + double NewOpacity; + string NewColor; + int Time; + if (!IsVisible) + { + NewOpacity = 0d; + Time = 20; // 防止错误的尺寸判断导致闪烁 + NewColor = "ColorBrush4"; + } + else if (IsMouseCaptureWithin) + { + NewOpacity = 1d; + NewColor = "ColorBrush4"; + Time = 50; + } + else if (IsMouseOver) + { + NewOpacity = 0.9d; + NewColor = "ColorBrush3"; + Time = 130; + } + else + { + NewOpacity = 0.5d; + NewColor = "ColorBrush4"; + Time = 180; + } + + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, ForegroundProperty, NewColor, Time), + ModAnimation.AaOpacity(this, NewOpacity - Opacity, Time) + }, "MyScrollBar Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyScrollBar Color " + Uuid); + SetResourceReference(ForegroundProperty, NewColor); + Opacity = NewOpacity; + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "滚动条颜色改变出错"); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyScrollViewer.cs b/Plain Craft Launcher 2/Controls/MyScrollViewer.cs new file mode 100644 index 000000000..f390ca830 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyScrollViewer.cs @@ -0,0 +1,94 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class MyScrollViewer : ScrollViewer +{ + private readonly string TooltipHideId; + + + private double RealOffset; + + public MyScrollBar ScrollBar; + + public MyScrollViewer() + { + TooltipHideId = $"HideTooltip_{GetHashCode()}"; + PreviewMouseWheel += MyScrollViewer_PreviewMouseWheel; + ScrollChanged += MyScrollViewer_ScrollChanged; + IsVisibleChanged += MyScrollViewer_IsVisibleChanged; + Loaded += (_, __) => Load(); + PreviewGotKeyboardFocus += MyScrollViewer_PreviewGotKeyboardFocus; + } + + public double DeltaMult { get; set; } = 1d; + + private void MyScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (e.Delta == 0 || ScrollableHeight <= 0d) + return; + + var src = e.Source; + if (((dynamic)Content).TemplatedParent is null) + { + if (src is ComboBox) + { + if (((ComboBox)src).IsDropDownOpen) + return; + } + else if (src is TextBox) + { + if (((TextBox)src).AcceptsReturn) + return; + } + else if (src is ComboBoxItem || src is CheckBox) + { + return; + } + } + + e.Handled = true; + PerformVerticalOffsetDelta(-e.Delta); + + if (Application.ShowingTooltips.Count > 0) + foreach (var TooltipBorder in Application.ShowingTooltips) + // 建议:如果动画已经在执行,则不再重复触发 + ModAnimation.AniStart(ModAnimation.AaOpacity(TooltipBorder, -1, 100), TooltipHideId); + } + + public void PerformVerticalOffsetDelta(double Delta) + { + ModAnimation.AniStart(ModAnimation.AaDouble(AnimDelta => + { + RealOffset = ModBase.MathClamp(RealOffset + (double)AnimDelta, 0d, ExtentHeight - ActualHeight); + ScrollToVerticalOffset(RealOffset); + }, Delta * DeltaMult, 300, 0, new ModAnimation.AniEaseOutFluent((ModAnimation.AniEasePower)6), false)); + } + + private void MyScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) + { + RealOffset = VerticalOffset; + if (ModMain.FrmMain is not null && + (Conversions.ToBoolean(e.VerticalChange) || Conversions.ToBoolean(e.ViewportHeightChange))) + ModMain.FrmMain.BtnExtraBack.ShowRefresh(); + } + + private void MyScrollViewer_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + ModMain.FrmMain.BtnExtraBack.ShowRefresh(); + } + + private void Load() + { + ScrollBar = (MyScrollBar)GetTemplateChild("PART_VerticalScrollBar"); + } + + private void MyScrollViewer_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (e.NewFocus is not null && e.NewFocus is MySlider) + e.Handled = true; // #3854,阻止获得焦点时自动滚动 + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MySearchBox.xaml b/Plain Craft Launcher 2/Controls/MySearchBox.xaml index c78dee9dc..eca167aea 100644 --- a/Plain Craft Launcher 2/Controls/MySearchBox.xaml +++ b/Plain Craft Launcher 2/Controls/MySearchBox.xaml @@ -1,16 +1,26 @@ - + - - - - + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MySearchBox.xaml.cs b/Plain Craft Launcher 2/Controls/MySearchBox.xaml.cs new file mode 100644 index 000000000..3ef6fde39 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MySearchBox.xaml.cs @@ -0,0 +1,81 @@ +using Microsoft.VisualBasic.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace PCL; + +public partial class MySearchBox : MyCard +{ + public delegate void SearchEventHandler(object sender, EventArgs e); + + public delegate void TextChangedEventHandler(object sender, EventArgs e); + + public MySearchBox() + { + InitializeComponent(); + + Loaded += MySearchBox_Loaded; + } + + // 属性 + public string HintText + { + get => TextBox.HintText; + set => TextBox.HintText = value; + } + + public string Text + { + get => TextBox.Text; + set => TextBox.Text = value; + } + + public Visibility SearchButtonVisibility + { + get => (Visibility)Conversions.ToByte(BtnSearch.Visibility == Visibility.Visible); + set + { + BtnClear.Margin = new Thickness(0d, 0d, value == Visibility.Visible ? 70 : 10, 0d); + BtnSearch.Visibility = value; + } + } + + public event TextChangedEventHandler? TextChanged; + + private void MySearchBox_Loaded(object sender, RoutedEventArgs e) + { + TextBox.Focus(); + } + + private void Text_TextChanged(object sender, TextChangedEventArgs e) + { + if (string.IsNullOrEmpty(TextBox.Text)) + { + ModAnimation.AniStart(ModAnimation.AaOpacity(BtnClear, -BtnClear.Opacity, 90), + "MySearchBox ClearBtn " + Uuid); + BtnClear.IsHitTestVisible = false; + } + else + { + ModAnimation.AniStart(ModAnimation.AaOpacity(BtnClear, 1d - BtnClear.Opacity, 90), + "MySearchBox ClearBtn " + Uuid); + BtnClear.IsHitTestVisible = true; + } + + TextChanged?.Invoke(sender, e); + } + + private void BtnClear_Click(object sender, EventArgs e) + { + TextBox.Text = ""; + TextBox.Focus(); + } + + public event SearchEventHandler? Search; + + private void BtnSearch_Click(object sender, MouseButtonEventArgs e) + { + Search?.Invoke(sender, e); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MySlider.xaml b/Plain Craft Launcher 2/Controls/MySlider.xaml index 3bae85a9b..f73a30bdf 100644 --- a/Plain Craft Launcher 2/Controls/MySlider.xaml +++ b/Plain Craft Launcher 2/Controls/MySlider.xaml @@ -1,29 +1,39 @@ - + - - - + + + - - + + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MySlider.xaml.cs b/Plain Craft Launcher 2/Controls/MySlider.xaml.cs new file mode 100644 index 000000000..99673e8cd --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MySlider.xaml.cs @@ -0,0 +1,298 @@ +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class MySlider +{ + public delegate void ChangeEventHandler(object sender, bool user); + + public delegate void PreviewChangeEventHandler(object sender, ModBase.RouteEventArgs e); + + // 自定义属性 + + private int _MaxValue = 100; + private int _Value; + private bool ChangeByKey; + + // 拖动 + + public Delegate GetHintText; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MySlider() + { + InitializeComponent(); + SizeChanged += RefreshWidth; + MouseLeftButtonDown += DragStart; + IsEnabledChanged += (_, __) => RefreshColor(); + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + MouseEnter += (_, __) => MySlider_MouseEnter(); + KeyDown += MySlider_KeyDown; + } + + public int MaxValue + { + get => _MaxValue; + set + { + if (value == _MaxValue) + return; + _MaxValue = value; + RefreshWidth(null, null); + } + } + + public int Value + { + get => _Value; + set + { + try + { + value = (int)Math.Round(ModBase.MathClamp(value, 0d, MaxValue)); + if (_Value == value) + return; + + // 触发 Preview 事件,修改新值 + var OldValue = _Value; + _Value = value; + if (ModAnimation.AniControlEnabled == 0) + { + var e = new ModBase.RouteEventArgs(); + PreviewChange?.Invoke(this, e); + if (e.Handled) + { + _Value = OldValue; + DragStop(); + return; + } + } + + if (IsLoaded && ModAnimation.AniControlEnabled == 0) + { + if (ActualWidth < ShapeDot.Width) + return; + var NewWidth = _Value / (double)MaxValue * (ActualWidth - ShapeDot.Width); + var DeltaProcess = + Math.Abs(LineFore.Width / (ActualWidth - ShapeDot.Width) - _Value / (double)MaxValue); + var Time = (1d - Math.Pow(1d - DeltaProcess, 3d)) * 300d + (ChangeByKey ? 100 : 0); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaWidth(LineFore, + Math.Max(0d, NewWidth + (NewWidth < 0.5d ? 0d : 0.5d)) - LineFore.Width, + (int)Math.Round(Time), + Ease: Time > 50d + ? new ModAnimation.AniEaseOutFluent() + : new ModAnimation.AniEaseLinear()), + ModAnimation.AaWidth(LineBack, + Math.Max(0d, + ActualWidth - ShapeDot.Width - NewWidth + + (ActualWidth - ShapeDot.Width - NewWidth < 0.5d ? 0d : 0.5d)) - LineBack.Width, + (int)Math.Round(Time), + Ease: Time > 50d + ? new ModAnimation.AniEaseOutFluent() + : new ModAnimation.AniEaseLinear()), + ModAnimation.AaX(ShapeDot, NewWidth - ShapeDot.Margin.Left, (int)Math.Round(Time), + Ease: Time > 50d + ? new ModAnimation.AniEaseOutFluent() + : new ModAnimation.AniEaseLinear()) + }, "MySlider Progress " + Uuid); + } + else + { + RefreshWidth(null, null); + } + + if (ModAnimation.AniControlEnabled == 0) + Change?.Invoke(this, false); + } + + catch (Exception ex) + { + ModBase.Log(ex, "滑动条进度改变出错", ModBase.LogLevel.Hint); + } + } + } + + // 按键改变 + + public uint ValueByKey { get; set; } = 1U; + public event ChangeEventHandler? Change; + public event PreviewChangeEventHandler? PreviewChange; + + private void RefreshWidth(object sender, SizeChangedEventArgs? e) + { + if (e != null) + PanMain.Width = e.NewSize.Width; + ModAnimation.AniStop("MySlider Progress " + Uuid); + var NewWidth = _Value / (double)MaxValue * (ActualWidth - ShapeDot.Width); + LineFore.Width = Math.Max(0d, NewWidth + (NewWidth < 0.5d ? 0d : 0.5d)); + LineBack.Width = Math.Max(0d, + ActualWidth - ShapeDot.Width - NewWidth + (ActualWidth - ShapeDot.Width - NewWidth < 0.5d ? 0d : 0.5d)); + ModBase.SetLeft(ShapeDot, NewWidth); + } + + private void DragStart(object sender, MouseButtonEventArgs e) + { + e.Handled = true; // 防止 ScrollViewer 失焦问题 + ModMain.DragControl = this; + RefreshColor(); + ModMain.FrmMain.DragDoing(); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeDot, 1.3d - ((ScaleTransform)ShapeDot.RenderTransform).ScaleX, 40, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MySlider Scale " + Uuid); + RefreshPopup(); + ModAnimation.AniStop("MySlider KeyPopup " + Uuid); + } + + public void DragDoing() + { + var Percent = + ModBase.MathClamp((Mouse.GetPosition(PanMain).X - ShapeDot.Width / 2d) / (ActualWidth - ShapeDot.Width), 0d, + 1d); + var NewValue = (int)Math.Round(Percent * MaxValue); + if (!(NewValue == Value)) Value = NewValue; + RefreshPopup(); + } + + public void DragStop() + { + RefreshColor(); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaScaleTransform(ShapeDot, 1d - ((ScaleTransform)ShapeDot.RenderTransform).ScaleX, 200, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MySlider Scale " + Uuid); + Popup.IsOpen = false; + } + + public void RefreshPopup() + { + if (GetHintText is null) + return; + Popup.IsOpen = true; + TextHint.Text = Conversions.ToString(GetHintText.DynamicInvoke(Value)); + var typeface = new Typeface(TextHint.FontFamily, TextHint.FontStyle, TextHint.FontWeight, TextHint.FontStretch); + var formattedText = new FormattedText(TextHint.Text, Thread.CurrentThread.CurrentCulture, + TextHint.FlowDirection, typeface, TextHint.FontSize, TextHint.Foreground, ModBase.DPI); + TextHint.Width = formattedText.Width; // 使用手动测量的宽度修复 #1057 + } + + // 指向动画 + + private void RefreshColor() + { + try + { + // 判断当前颜色 + string ForegroundName; + string DotFillName; + int AnimationTime; + if (IsEnabled) + { + if (!(ModMain.DragControl == null) && ModMain.DragControl.Equals(this)) + { + ForegroundName = "ColorBrush3"; + DotFillName = "ColorBrush3"; + AnimationTime = 40; + } + else if (IsMouseOver) + { + ForegroundName = "ColorBrush3"; + DotFillName = "ColorBrush3"; + AnimationTime = 40; + } + else + { + ForegroundName = "ColorBrushBg0"; + DotFillName = "ColorBrushBg0"; + AnimationTime = 100; + } + } + else + { + ForegroundName = "ColorBrushGray5"; + DotFillName = "ColorBrushGray5"; + AnimationTime = 200; + } + + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, BorderBrushProperty, ForegroundName, AnimationTime), + ModAnimation.AaColor(ShapeDot, Shape.FillProperty, DotFillName, AnimationTime) + }, "MySlider Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MySlider Color " + Uuid); + SetResourceReference(BorderBrushProperty, ForegroundName); + ShapeDot.SetResourceReference(Shape.FillProperty, DotFillName); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "滑动条颜色改变出错"); + } + } + + private void MySlider_MouseEnter() + { + Focus(); // 确保按键能改变值 + } + + private void MySlider_KeyDown(object sender, KeyEventArgs e) + { + // 拒绝一边拖动一边用按键改变 + if (ReferenceEquals(this, ModMain.DragControl)) + return; + // 改变值 + if (e.Key == Key.Left) + { + ChangeByKey = true; + Value = (int)(Value - ValueByKey); + ChangeByKey = false; + e.Handled = true; + } + else if (e.Key == Key.Right) + { + ChangeByKey = true; + Value = (int)(Value + ValueByKey); + ChangeByKey = false; + e.Handled = true; + } + else + { + return; + } + + // 更新 Popup + if (GetHintText is not null) + { + RefreshPopup(); + ModAnimation.AniStop("MySlider KeyPopup " + Uuid); + ModAnimation.AniStart( + ModAnimation.AaCode(() => Popup.IsOpen = false, (int)Math.Round(700d * ModAnimation.AniSpeed)), + "MySlider KeyPopup " + Uuid); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyTextBox.cs b/Plain Craft Launcher 2/Controls/MyTextBox.cs new file mode 100644 index 000000000..bccfa8259 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyTextBox.cs @@ -0,0 +1,399 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class MyTextBox : TextBox +{ + public delegate void ValidateChangedEventHandler(object sender, EventArgs e); + + public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", + typeof(CornerRadius), typeof(MyTextBox), new PropertyMetadata(new CornerRadius(3d))); + + public static readonly DependencyProperty ValidateResultProperty = DependencyProperty.Register("ValidateResult", + typeof(string), typeof(MyTextBox), + new PropertyMetadata("", + (d, e) => d.SetValue(IsValidatedPropertyKey, + string.IsNullOrEmpty(Conversions.ToString(e.NewValue))))); + + private static readonly DependencyPropertyKey IsValidatedPropertyKey = + DependencyProperty.RegisterReadOnly("IsValidated", typeof(bool), typeof(MyTextBox), new PropertyMetadata(true)); + + public static readonly DependencyProperty IsValidatedProperty = IsValidatedPropertyKey.DependencyProperty; + + public static readonly DependencyProperty HintTextProperty = DependencyProperty.Register("HintText", typeof(string), + typeof(MyTextBox), new PropertyMetadata("", (t, e) => + { + var textBox = (MyTextBox)t; + if (textBox._labHint is not null) + textBox._labHint.Text = string.IsNullOrEmpty(textBox.Text) ? textBox.HintText : ""; + })); + + private TextBlock _labHint; + + // 额外控件初始化 + + private TextBlock _labWrong; + private Collection _ValidateRules = new(); + public List ChangedEventList = new(); + + // 提示文本 + + /// + /// 是否已经由用户输入过文本,若尚未输入过,则不显示输入检查的失败。 + /// + private bool IsTextChanged; + + private ValidateState ShownValidateResult = ValidateState.NotInited; + + // 事件 + + public int Uuid = ModBase.GetUuid(); + + public MyTextBox() + { + Loaded += (_, __) => Validate(); + TextChanged += (a, b) => MyTextBox_TextChanged((MyTextBox)a, b); + IsEnabledChanged += (_, __) => RefreshColor(); + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + GotFocus += (_, __) => RefreshColor(); + LostFocus += (_, __) => RefreshColor(); + IsEnabledChanged += (_, __) => RefreshTextColor(); + } + + // 自定义属性 + + public bool HasBackground { get; set; } = true; + public bool ShowValidateResult { get; set; } = true; + + public CornerRadius CornerRadius + { + get => (CornerRadius)GetValue(CornerRadiusProperty); + set + { + if (value == null) return; + SetValue(CornerRadiusProperty, value); + } + } + + private TextBlock labWrong + { + get + { + if (Template is null) + return null; + if (_labWrong is null) + _labWrong = (TextBlock)Template.FindName("labWrong", this); + return _labWrong; + } + } + + private TextBlock labHint + { + get + { + if (Template is null) + return null; + if (_labHint is null) + _labHint = (TextBlock)Template.FindName("labHint", this); + return _labHint; + } + } + + // 输入验证 + + /// + /// 输入验证结果。若为空字符串则无错误,否则为第一个错误原因。 + /// + public string ValidateResult + { + get => Conversions.ToString(GetValue(ValidateResultProperty)); + set => SetValue(ValidateResultProperty, value); + } + + /// + /// 是否通过了输入验证。 + /// + public bool IsValidated => Conversions.ToBoolean(GetValue(IsValidatedProperty)); + + /// + /// 输入验证的规则。 + /// + public Collection ValidateRules + { + get => _ValidateRules; + set + { + _ValidateRules = value; + Validate(); + } + } + + public string HintText + { + get => Conversions.ToString(GetValue(HintTextProperty)); + set => SetValue(HintTextProperty, value); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + if (string.IsNullOrEmpty(HintText) || !string.IsNullOrEmpty(labHint.Text)) + return; + labHint.Text = string.IsNullOrEmpty(Text) ? HintText : ""; + } + + public event ValidateChangedEventHandler? ValidateChanged; + + public event RoutedEventHandler ValidatedTextChanged + { + add => ChangedEventList.Add(value); + remove => ChangedEventList.Remove(value); + } + + private void OnValidatedTextChanged(object sender, TextChangedEventArgs e) + { + foreach (var handler in ChangedEventList) + if (!(handler == null)) + handler.Invoke(sender, e); + } + + /// + /// 进行输入验证。 + /// + public void Validate() + { + // 执行输入验证 + ValidateResult = ModValidate.Validate(Text, ValidateRules); + // 根据结果改变样式 + if (ShownValidateResult != (IsValidated ? ValidateState.Success : ValidateState.FailedAndShowDetail)) + { + if (IsLoaded && labWrong is not null) + ChangeValidateResult(IsValidated, true); + else + ModBase.RunInNewThread(() => + { + Thread.Sleep(30); + ModBase.RunInUi(() => ChangeValidateResult(IsValidated, false)); + }, "DelayedValidate Change"); + } + + // 更新错误信息 + if (ShowValidateResult && !IsValidated) + { + if (IsLoaded && labWrong is not null) + labWrong.Text = ValidateResult; + else + ModBase.RunInNewThread(() => + { + var IsFinished = false; + while (!IsFinished) + { + Thread.Sleep(20); + ModBase.RunInUiWait(() => + { + if (labWrong is not null) + { + labWrong.Text = ValidateResult; + IsFinished = true; + } + + if (!IsLoaded) + IsFinished = true; + }); + } + }, "DelayedValidate Text"); + } + } + + /// + /// 强制显示结果为正常,类似尚未输入过文本的状态。不影响实际的检查结果。 + /// + public void ForceShowAsSuccess() + { + IsTextChanged = false; + ChangeValidateResult(IsValidated, true); + } + + private void ChangeValidateResult(bool IsSuccessful, bool IsLoaded) + { + if (IsLoaded && ModAnimation.AniControlEnabled == 0 && labWrong is not null) + { + if (IsSuccessful || !IsTextChanged) + { + // 变为正确 + ShownValidateResult = IsSuccessful ? ValidateState.Success : ValidateState.FailedButTextNotChanged; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(labWrong, -labWrong.Opacity, 150), + ModAnimation.AaHeight(labWrong, -labWrong.Height, 150, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => labWrong.Visibility = Visibility.Collapsed, After: true) + }, "MyTextBox Validate " + Uuid); + } + else if (ShowValidateResult) + { + // 变为错误 + ShownValidateResult = ValidateState.FailedAndShowDetail; + labWrong.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(labWrong, 1d - labWrong.Opacity, 150), + ModAnimation.AaHeight(labWrong, 21d - labWrong.Height, 150, + Ease: new ModAnimation.AniEaseOutFluent()) + }, "MyTextBox Validate " + Uuid); + } + else + { + // 变为错误,但不显示文本 + ShownValidateResult = ValidateState.FailedAndHideDetail; + } + } + else + { + ShownValidateResult = ValidateState.NotLoaded; + } + + RefreshColor(); + ValidateChanged?.Invoke(this, new EventArgs()); + } + + private void MyTextBox_TextChanged(MyTextBox sender, TextChangedEventArgs e) + { + try + { + // 改变提示文本 + if (labHint is not null) + labHint.Text = string.IsNullOrEmpty(Text) ? HintText : ""; + // 改变输入记录 + IsTextChanged = IsLoaded; + // 进行输入验证 + Validate(); + if (!IsValidated) + return; + // 改变文本 + OnValidatedTextChanged(sender, e); + } + catch (Exception ex) + { + ModBase.Log(ex, "进行输入验证时出错", ModBase.LogLevel.Critical); + } + } + + // 颜色 + + private void RefreshColor() + { + try + { + // 不对 ComboBox 从属进行动画 + if (TemplatedParent is not null && TemplatedParent is MyComboBox) + return; + // 判断当前颜色 + string ForeColorName; + string BackColorName; + int AnimationTime; + if (IsEnabled) + { + if (IsValidated || !IsTextChanged) + { + if (IsFocused) + { + ForeColorName = "ColorBrush3"; + BackColorName = "ColorBrush7"; + AnimationTime = 10; + } + else if (IsMouseOver) + { + ForeColorName = "ColorBrush4"; + BackColorName = "ColorBrush7"; + AnimationTime = 100; + } + else // 未选中 + { + ForeColorName = "ColorBrushBg0"; + BackColorName = "ColorBrushHalfWhite"; + AnimationTime = 100; + } + } + else + { + ForeColorName = "ColorBrushRedLight"; + BackColorName = "ColorBrushRedBack"; + AnimationTime = 200; + } + } + else + { + ForeColorName = "ColorBrushGray5"; + BackColorName = "ColorBrushGray6"; + AnimationTime = 200; + } + + if (!HasBackground) + BackColorName = "ColorBrushTransparent"; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, BorderBrushProperty, ForeColorName, AnimationTime), + ModAnimation.AaColor(this, BackgroundProperty, BackColorName, AnimationTime) + }, "MyTextBox Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyTextBox Color " + Uuid); + SetResourceReference(BorderBrushProperty, ForeColorName); + SetResourceReference(BackgroundProperty, BackColorName); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "文本框颜色改变出错"); + } + } + + private void RefreshTextColor() + { + var NewColor = IsEnabled ? ModSecret.ColorGray1 : ModSecret.ColorGray4; + if (((SolidColorBrush)Foreground).Color.R == NewColor.R) + return; + if (IsLoaded && ModAnimation.AniControlEnabled == 0 && !string.IsNullOrEmpty(Text)) + { + // 有动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaColor(this, ForegroundProperty, IsEnabled ? "ColorBrushGray1" : "ColorBrushGray4", + 200) + }, "MyTextBox TextColor " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyTextBox TextColor " + Uuid); + Foreground = NewColor; + } + } + + private enum ValidateState + { + NotInited, + Success, + FailedButTextNotChanged, + FailedAndShowDetail, + FailedAndHideDetail, + NotLoaded + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyTextButton.cs b/Plain Craft Launcher 2/Controls/MyTextButton.cs new file mode 100644 index 000000000..b74ac4740 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyTextButton.cs @@ -0,0 +1,145 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public class MyTextButton : Label +{ + public delegate void ClickEventHandler(object sender, EventArgs e); + + // 指向动画 + + private const int AnimationTimeIn = 100; + private const int AnimationTimeOut = 200; + + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), + typeof(MyTextButton), new PropertyMetadata("", (sender, e) => + { + if (Conversions.ToBoolean(!Operators.ConditionalCompareObjectEqual(e.OldValue, e.NewValue, false))) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(sender, -((dynamic)sender).Opacity, 50), + ModAnimation.AaCode(() => ((dynamic)sender).Content = e.NewValue, After: true), + ModAnimation.AaOpacity(sender, 1d, 170) + }, "MyTextButton Text " + ((dynamic)sender).Uuid); + })); + + public static readonly DependencyProperty EventTypeProperty = + DependencyProperty.Register("EventType", typeof(string), typeof(MyTextButton), new PropertyMetadata(null)); + + public static readonly DependencyProperty EventDataProperty = + DependencyProperty.Register("EventData", typeof(string), typeof(MyTextButton), new PropertyMetadata(null)); + + private string ColorName; + + // 鼠标事件 + + public bool IsMouseDown; + + // 基础 + + public int Uuid = ModBase.GetUuid(); + + public MyTextButton() + { + SetResourceReference(ForegroundProperty, "ColorBrush1"); + Background = ModSecret.ColorSemiTransparent; + PreviewMouseLeftButtonDown += MyTextButton_MouseLeftButtonDown; + MouseLeave += (_, __) => MyTextButton_MouseLeave(); + PreviewMouseLeftButtonUp += MyTextButton_MouseLeftButtonUp; + MouseEnter += (_, __) => RefreshColor(); + MouseLeave += (_, __) => RefreshColor(); + IsEnabledChanged += (_, __) => RefreshColor(); + MouseLeftButtonDown += (_, __) => RefreshColor(); + MouseLeftButtonUp += (_, __) => RefreshColor(); + } + + // 文本 + + public string Text + { + get => Conversions.ToString(GetValue(TextProperty)); + set => SetValue(TextProperty, value); + } + + // 实现自定义事件 + public string EventType + { + get => Conversions.ToString(GetValue(EventTypeProperty)); + set => SetValue(EventTypeProperty, value); + } + + public string EventData + { + get => Conversions.ToString(GetValue(EventDataProperty)); + set => SetValue(EventDataProperty, value); + } + + public event ClickEventHandler? Click; + + private void MyTextButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + IsMouseDown = true; + e.Handled = true; + } + + private void MyTextButton_MouseLeave() + { + IsMouseDown = false; + } + + private void MyTextButton_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (IsMouseDown) + { + IsMouseDown = false; + ModBase.Log("[Control] 按下文本按钮:" + Text); + Click?.Invoke(this, null); + ModEvent.TryStartEvent(EventType, EventData); + e.Handled = true; + } + } + + private void RefreshColor() + { + // 判断当前颜色 + string ForeName; + int Time; + if (IsMouseDown) + { + ForeName = "ColorBrush4"; + Time = 30; + } + else if (IsMouseOver) + { + ForeName = "ColorBrush3"; + Time = AnimationTimeIn; + } + else + { + ForeName = "ColorBrush1"; + Time = AnimationTimeOut; + } + + // 重复性验证 + if ((ColorName ?? "") == (ForeName ?? "")) + return; + ColorName = ForeName; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + ModAnimation.AniStart(ModAnimation.AaColor(this, ForegroundProperty, ForeName, Time), + "MyTextButton Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("MyTextButton Color " + Uuid); + SetResourceReference(ForegroundProperty, ForeName); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Controls/MyVirtualizingElement.cs b/Plain Craft Launcher 2/Controls/MyVirtualizingElement.cs new file mode 100644 index 000000000..65c7f0982 --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyVirtualizingElement.cs @@ -0,0 +1,83 @@ +using System.Windows; +using System.Windows.Controls; + +namespace PCL; + +public class MyVirtualizingElement : FrameworkElement where T : FrameworkElement +{ + private readonly Func _initializer; + + public MyVirtualizingElement(Func initializer) + { + _initializer = initializer; + this.EnableLazyLoad(() => Init()); + } + + /// + /// 实例化此控件。 + /// + public T Init() + { + var element = _initializer(); + if (Parent is not null) + { + if (!(Parent is Panel)) + throw new Exception("MyVirtualizingElement 的父级必须是一个 Panel"); + var parentPanel = (Panel)Parent; + var currentIndex = parentPanel.Children.IndexOf(this); + parentPanel.Children.RemoveAt(currentIndex); + parentPanel.Children.Insert(currentIndex, element); + } + + return element; + } + + public static implicit operator T(MyVirtualizingElement virtualized) + { + return virtualized.Init(); + } +} + +// 非泛型形式 +public class MyVirtualizingElement : FrameworkElement +{ + private readonly Func _initializer; + + public MyVirtualizingElement(Func initializer) + { + _initializer = initializer; + this.EnableLazyLoad(() => Init()); + } + + /// + /// 实例化此控件。 + /// + public FrameworkElement Init() + { + var element = _initializer(); + if (Parent is not null) + { + if (!(Parent is Panel)) + throw new Exception("MyVirtualizingElement 的父级必须是一个 Panel"); + var parentPanel = (Panel)Parent; + var currentIndex = parentPanel.Children.IndexOf(this); + parentPanel.Children.RemoveAt(currentIndex); + parentPanel.Children.Insert(currentIndex, element); + } + + return element; + } + + /// + /// 获取实例化后的控件。 + /// 如果该控件没有实例化,则会立即实例化。 + /// 如果类型错误,则返回原值。 + /// + public static FrameworkElement TryInit(FrameworkElement element) + { + if (typeof(MyVirtualizingElement<>).IsInstanceOfGenericType(element)) + return (FrameworkElement)((dynamic)element).Init(); + + return element is MyVirtualizingElement ? ((MyVirtualizingElement)element).Init() : element; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/FormMain.xaml b/Plain Craft Launcher 2/FormMain.xaml index 278fcf147..37632e339 100644 --- a/Plain Craft Launcher 2/FormMain.xaml +++ b/Plain Craft Launcher 2/FormMain.xaml @@ -2,11 +2,15 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PCL" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core" mc:Ignorable="d" - x:Name="WindMain" x:Class="FormMain" Style="{StaticResource MyWindow}" - AllowDrop="True" Topmost="True" Title="Plain Craft Launcher Community Edition " MinHeight="470" MinWidth="810" + KeyDown="FormMain_KeyDown" MouseDown="FormMain_MouseDown" SizeChanged="FormMain_SizeChanged" + Closing="FormMain_Closing" Activated="FormMain_Activated" Drop="FrmMain_Drop" StateChanged="WindowStateChanged" + MouseMove="FormMain_MouseMove" + x:Name="WindMain" x:Class="PCL.FormMain" Style="{StaticResource MyWindow}" + AllowDrop="True" Topmost="True" Title="Plain Craft Launcher Community Edition " MinHeight="470" MinWidth="810" ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico" ResizeMode="CanResize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" WindowStartupLocation="CenterScreen" WindowStyle="SingleBorderWindow" AllowsTransparency="False"> @@ -19,9 +23,10 @@ UseAeroCaptionButtons="False" ResizeBorderThickness="0" CornerRadius="12" - GlassFrameThickness="0"/> + GlassFrameThickness="0" /> - + @@ -29,7 +34,7 @@ - + @@ -40,108 +45,170 @@ - - + + - + - - - - + + + + - + - - - - - + + + + + - - + + + Check="BtnTitleSelect_Click" + LogoScale="0.9" + Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" /> + Check="BtnTitleSelect_Click" + LogoScale="1.1" + Logo="M940.4 463.7L773.3 174.2c-17.3-30-49.2-48.4-83.8-48.4H340.2c-34.6 0-66.5 18.5-83.8 48.4L89.2 463.7c-17.3 30-17.3 66.9 0 96.8L256.4 850c17.3 30 49.2 48.4 83.8 48.4h349.2c34.6 0 66.5-18.5 83.8-48.4l167.2-289.5c17.3-29.9 17.3-66.8 0-96.8z m-94.6 96.8L725.9 768.1c-17.3 30-49.2 48.4-83.8 48.4H387.5c-34.6 0-66.5-18.5-83.8-48.4L183.9 560.5c-17.3-30-17.3-66.9 0-96.8l119.8-207.5c17.3-30 49.2-48.4 83.8-48.4h254.6c34.6 0 66.5 18.5 83.8 48.4l119.8 207.5c17.3 30 17.3 66.9 0.1 96.8z M522.3 321.2c-2.5-0.1-5-0.2-7.5-0.2-119.9 0-214 110.3-186.3 235 15.8 70.9 71.5 126.6 142.4 142.4 17.5 3.9 34.7 5.4 51.4 4.7 102.1-3.9 183.6-87.9 183.6-191 0.1-103-81.5-187-183.6-190.9z m68.6 269.1c-18.5 18-43 28.9-68.6 30.7l-6 0.3c-30.2 0.4-58.6-11.4-79.7-33-19.5-20.1-30.7-47-30.9-75-0.3-29.6 11.1-57.4 32-78.3 20.6-20.6 48-32 77.2-32 2.5 0 5 0.1 7.5 0.3 26.7 1.8 51.5 13.2 70.5 32.5 19.6 20 30.8 46.9 31.2 74.9 0.2 30.2-11.5 58.6-33.2 79.6z" /> + Check="BtnTitleSelect_Click" + LogoScale="1" + Logo="M623.0016 208.5376c-103.6288-103.6288-269.4144-103.6288-352.256-20.736L415.744 332.8512 332.8 415.7952 187.8016 270.6944c-82.944 82.944-82.944 248.6784 20.736 352.3072 66.56 66.6112 158.9248 88.32 276.8896 64.9728l13.2608-2.7648 198.656 198.656a41.472 41.472 0 0 0 54.7328 3.4304l3.8912-3.4304 127.8976-127.8976a41.472 41.472 0 0 0 3.4304-54.7328l-3.4304-3.8912-198.656-198.656c27.648-124.3648 6.912-221.0816-62.208-290.1504z m-253.2352-9.6256l1.1776-0.4096c64.9728-20.736 150.6304-3.4816 208.0768 54.016 50.6368 50.5344 67.4816 121.7024 48.128 220.16l-2.56 12.4928-7.4752 33.28 208.1792 208.1792-98.6624 98.6112-208.128-208.128-33.28 7.3728c-105.0624 23.3472-180.0192 7.2704-232.704-45.4656-55.04-54.9376-73.216-135.68-56.5248-199.5264l2.9696-9.728L332.8 503.6544 503.7056 332.8 369.7664 198.912z" /> - - - + + + - - + + - + - + - - + + - - + + - - - + + - - - + + - + - - - - - - - + + + + + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/FormMain.xaml.cs b/Plain Craft Launcher 2/FormMain.xaml.cs new file mode 100644 index 000000000..ae9377fc1 --- /dev/null +++ b/Plain Craft Launcher 2/FormMain.xaml.cs @@ -0,0 +1,2255 @@ +using System.ComponentModel; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Effects; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.App.IoC; +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.UI.Theme; +using PCL.Core.Utils; +using PCL.Core.Utils.OS; + +namespace PCL; + +public partial class FormMain +{ + // 愚人节鼠标位置 + public MouseEventArgs lastMouseArg; + + private void FormMain_MouseMove(object sender, MouseEventArgs e) + { + lastMouseArg = e; + } + + #region 基础 + + // 更新日志 + private void ShowUpdateLog() + { + ModBase.RunInNewThread(() => + { + var ChangelogFile = $"{ModBase.PathTemp}CEUpdateLog.md"; + string Changelog; + if (File.Exists(ChangelogFile)) + Changelog = ModBase.ReadFile(ChangelogFile); + else + Changelog = "欢迎使用呀~"; + if (ModMain.MyMsgBoxMarkdown(Changelog, + "PCL CE 已更新至 " + ModBase.VersionBranchName + " " + ModBase.VersionBaseName, "确定", "完整更新日志") == + 2) ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases"); + }, "UpdateLog Output"); + } + + // 窗口加载 + private bool IsWindowLoadFinished; + private readonly DragHelper _helper = new(); + + public FormMain() + { + ModBase.ApplicationStartTick = TimeUtils.GetTimeTick(); + // 刷新主题 + // ThemeCheckAll(False) + // ThemeRefreshColor() + ThemeService.ColorModeChanged += (_, _) => ModSecret.ThemeRefresh(); + ThemeService.ColorThemeChanged += theme => ModSecret.ThemeRefresh((int)theme); + // 窗体参数初始化 + ModMain.FrmMain = this; + ModMain.FrmLaunchLeft = new PageLaunchLeft(); + ModMain.FrmLaunchRight = new PageLaunchRight(); + // 版本号改变 + var LastVersion = States.System.LastVersion; + if (LastVersion < ModBase.VersionCode) + // 触发升级 + UpgradeSub(LastVersion); + else if (LastVersion > ModBase.VersionCode) + // 触发降级 + DowngradeSub(LastVersion); + // 版本隔离设置迁移 + if (ModBase.Setup.IsUnset("LaunchArgumentIndieV2")) + { + if (!ModBase.Setup.IsUnset("LaunchArgumentIndie")) + { + ModBase.Log("[Start] 从老 PCL 迁移版本隔离"); + Config.Launch.IndieSolutionV2 = Config.Launch.IndieSolutionV1; + } + else if (!ModBase.Setup.IsUnset("WindowHeight")) + { + ModBase.Log("[Start] 从老 PCL 升级,但此前未调整版本隔离,使用老的版本隔离默认值"); + Config.Launch.IndieSolutionV2Config.Reset(Config.Launch.IndieSolutionV1Config.DefaultValue); + } + else + { + ModBase.Log("[Start] 全新的 PCL,使用新的版本隔离默认值"); + Config.Launch.IndieSolutionV2Config.Reset(Config.Launch.IndieSolutionV2Config.DefaultValue); + } + } + + ModBase.Setup.Load("UiLauncherTheme"); + // 注册拖拽事件(不能直接加 Handles,否则没用;#6340) + AddHandler(DragDrop.DragEnterEvent, new DragEventHandler(HandleDrag), true); + AddHandler(DragDrop.DragOverEvent, new DragEventHandler(HandleDrag), true); + // 注册 MsgBox 事件 + MsgBoxWrapper.OnShow += ModMain.MsgBoxWrapper_OnShow; + // 注册 Hint 事件 + HintWrapper.OnShow += ModMain.HintWrapper_OnShow; + // 加载 UI + InitializeComponent(); + Opacity = 0d; + try + { + Height = Conversions.ToDouble(States.UI.WindowHeight); + Width = Conversions.ToDouble(States.UI.WindowWidth); + } + catch (Exception ex) // 修复 #2019 + { + ModBase.Log(ex, "读取窗口默认大小失败", ModBase.LogLevel.Hint); + Height = MinHeight + 100d; + Width = MinWidth + 100d; + } + + // 管理员权限下文件拖拽 + if (ProcessInterop.IsAdmin()) + { + ModBase.Log("[Start] PCL 当前正以管理员权限运行"); + SourceInitialized += (_, _) => + { + var windowInterop = new WindowInteropHelper(this); + _helper.HwndSource = HwndSource.FromHwnd(windowInterop.Handle); + _helper.AddHook(); + }; + Closing += (_, _) => _helper.RemoveHook(); + _helper.DragDrop += (_, _) => FileDrag(_helper.DropFilePaths); + } + + if (!(ModMain.FrmLaunchLeft.Parent == null)) + ModMain.FrmLaunchLeft.SetValue(ContentPresenter.ContentProperty, null); + if (!(ModMain.FrmLaunchRight.Parent == null)) + ModMain.FrmLaunchRight.SetValue(ContentPresenter.ContentProperty, null); + PanMainLeft.Child = ModMain.FrmLaunchLeft; + PageLeft = ModMain.FrmLaunchLeft; + PanMainRight.Child = ModMain.FrmLaunchRight; + PageRight = ModMain.FrmLaunchRight; + ModMain.FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay; + // 调试模式提醒 + if (ModBase.ModeDebug) + ModMain.Hint("[调试模式] PCL 正以调试模式运行,这可能会导致性能下降,若无必要请不要开启!"); + // 尽早执行的加载池 + ModMinecraft.McFolderListLoader + .Start(0); // 为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid + + ModBase.Log("[Start] 第二阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick) + " ms"); + // 注册生命周期状态事件 + Lifecycle.When(LifecycleState.WindowCreated, FormMain_Loaded); + } + + private void FormMain_Loaded() // (sender As Object, e As RoutedEventArgs) Handles Me.Loaded + { + FormMain_SizeChanged(); + ModBase.ApplicationStartTick = TimeUtils.GetTimeTick(); + ModBase.FrmHandle = new WindowInteropHelper(this).Handle; + // 读取设置 + ModBase.Setup.Load("UiBackgroundOpacity"); + ModBase.Setup.Load("UiBackgroundBlur"); + ModBase.Setup.Load("UiLogoType"); + ModBase.Setup.Load("UiHiddenPageDownload"); + ModBase.Setup.Load("UiAutoPauseVideo"); // 智能暂停视频背景 + PageSetupUI.HiddenRefresh(); + PageSetupUI.BackgroundRefresh(false, true); + ModMusic.MusicRefreshPlay(false, true); + // 扩展按钮 + BtnExtraUpdateRestart.ShowCheck = BtnExtraUpdateRestart_ShowCheck; + BtnExtraDownload.ShowCheck = BtnExtraDownload_ShowCheck; + BtnExtraBack.ShowCheck = BtnExtraBack_ShowCheck; + BtnExtraApril.ShowCheck = BtnExtraApril_ShowCheck; + BtnExtraShutdown.ShowCheck = BtnExtraShutdown_ShowCheck; + BtnExtraLog.ShowCheck = BtnExtraLog_ShowCheck; + BtnExtraApril.ShowRefresh(); + // 初始化尺寸改变 + if (!Config.Preference.LockWindowSize) + AddResizer(); + else + RemoveResizer(); + // PLC 彩蛋 + if (RandomUtils.NextInt(1, 1000) == 233) + ShapeTitleLogo.Data = (Geometry)new GeometryConverter().ConvertFromString( + "M26,29 v-25 h6 a7,7 180 0 1 0,14 h-6 M83,6.5 a10,11.5 180 1 0 0,18 M48,2.5 v24.5 h13.5"); + // 加载窗口 + + ModSecret.ThemeRefresh(); + + System.Windows.Application.Current.Resources["BlurSamplingRate"] = + Operators.MultiplyObject(Config.Preference.Blur.SamplingRate, 0.01d); + System.Windows.Application.Current.Resources["BlurType"] = + (KernelType)Conversions.ToInteger(Config.Preference.Blur.KernelType); + if (Conversions.ToBoolean(Config.Preference.Blur.IsEnabled)) + System.Windows.Application.Current.Resources["BlurRadius"] = + Operators.MultiplyObject(Config.Preference.Blur.Radius, 1.0d); + else + System.Windows.Application.Current.Resources["BlurRadius"] = 0.0d; + + // #If DEBUG Then + // MinHeight = 50 + // MinWidth = 50 + // #End If + Topmost = false; + if (ModMain.FrmStart is not null) + ModMain.FrmStart.Close(new TimeSpan(0, 0, 0, 0, (int)Math.Round(400d / ModAnimation.AniSpeed))); + // 更改窗口 + // Top = (GetWPFSize(My.Computer.Screen.WorkingArea.Height) - Height) / 2 + // Left = (GetWPFSize(My.Computer.Screen.WorkingArea.Width) - Width) / 2 + IsSizeSaveable = true; + ShowWindowToTop(); + var HwndSource = (HwndSource)PresentationSource.FromVisual(this); + HwndSource.AddHook(WndProc); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => ModAnimation.AniControlEnabled -= 1, 50), + ModAnimation.AaOpacity(this, + Conversions.ToDouble( + Operators.AddObject(Operators.DivideObject(Config.Preference.Theme.WindowOpacity, 1000), + 0.4d)), 250, 100), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, -TransformPos.Y, 600, + 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => + { + RenderTransform = null; + IsWindowLoadFinished = true; + ModBase.Log( + $"[System] DPI:{ModBase.DPI},系统版本:{Environment.OSVersion.VersionString},PCL 位置:{ModBase.ExePathWithName}"); + }, After: true) + }, "Form Show"); + // Timer 启动 + ModAnimation.AniStart(); + ModMain.TimerMainStart(); + // 特殊版本提示 + ModBase.RunInNewThread(() => + { + // 特殊版本提示 + try + { + + +#if DEBUG || DEBUGCI + + if (Environment.GetEnvironmentVariable("PCL_DISABLE_DEBUG_HINT") is null) + { + +#if DEBUG + const string hint = """ + 当前运行的 PCL 社区版为 Debug 版本。 + 该版本仅适合开发者调试运行,可能会有严重的性能下降以及各种奇怪的网络问题。 + + 非开发者用户使用该版本造成的一切问题均不被社区支持,相关 issue 可能会被直接关闭。 + 除非您是开发者,否则请立即删除该版本,并下载最新稳定版使用。 + """; +#else + const string hint = """ + 当前运行的 PCL 社区版为 CI 自动构建版本。 + 该版本包含最新的漏洞修复、优化和新特性,但性能和稳定性较差,不适合日常使用和制作整合包。 + + 除非社区开发者要求或您自己想要这么做,否则请下载最新稳定版使用。 + """; +#endif + + ModMain.MyMsgBox( + $"{hint}{"\r\n"}{"\r\n"}可以添加 PCL_DISABLE_DEBUG_HINT 环境变量 (任意值) 来隐藏这个提示。", + "特殊版本提示", "我清楚我在做什么", "打开最新版下载页并退出", IsWarn: true, Button2Action: () => + { + ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/latest"); + EndProgram(false); + }); + } + + +#endif + // EULA 提示 + if (!States.System.LauncherEula) + switch (ModMain.MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明", + Button3Action: () => ModBase.OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))) + { + case 1: + { + States.System.LauncherEula = true; + break; + } + case 2: + { + EndProgram(false); + break; + } + } + + // 遥测提示 + if (Config.System.TelemetryConfig.IsDefault()) + { + var selection = ModMain.MyMsgBox( + "这是一项与 Steam 硬件调查类似的计划,参与调查可以帮助我们更好的进行规划和开发,且我们会不定期发布该调查的统计结果。" + "\r\n" + + "如果选择参与调查,我们将会收集以下信息:" + "\r\n" + "\r\n" + "- 启动器版本信息与识别码" + + "\r\n" + "- Windows 系统版本与架构" + "\r\n" + "- 已安装的物理内存大小" + + "\r\n" + "- NAT 与 IPv6 支持情况" + "\r\n" + "- 是否使用过官方版 PCL、HMCL 或 BakaXL" + + "\r\n" + "\r\n" + "这些数据均不与你关联,我们也绝不会向第三方出售数据。" + "\r\n" + + "如果不想参与该调查,可以选择拒绝,不会影响其他功能使用。" + "\r\n" + "你可以随时在启动器设置中调整这项设置。", + "参与 PCL CE 软硬件调查", "同意", "拒绝"); + Config.System.TelemetryConfig.SetValue(selection == 1, forceNewValue: true); + } + // 启动加载器池 + try + { + ModDownload.DlClientListMojangLoader.Start(1); // PCL 会同时根据这里的加载结果决定是否使用官方源进行下载 + RunCountSub(); + ModSecret.ServerLoader.Start(1); + ModBase.RunInNewThread(ModMain.TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化加载池运行失败", ModBase.LogLevel.Feedback); + } + + ModSecret.GetSystemInfo(); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始弹窗提示运行失败", ModBase.LogLevel.Feedback); + } + }, "Start Loader", ThreadPriority.BelowNormal); + + ModBase.Log("[Start] 第三阶段加载用时:" + (TimeUtils.GetTimeTick() - ModBase.ApplicationStartTick) + " ms"); + } + + // 根据打开次数触发的事件 + private void RunCountSub() + { + States.System.StartupCount += 1; + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectGreaterEqual(States.System.StartupCount, 99, false))) + if (ModSecret.ThemeUnlock(6, false)) + ModMain.MyMsgBox("你已经打开了 99 次 PCL 社区版啦,感谢你长期以来的支持!" + "\r\n" + "隐藏主题 铁杆粉 未解锁!社区版不包含隐藏主题!"); + } + + // 升级与降级事件 + private void UpgradeSub(int LastVersionCode) + { + ModBase.Log("[Start] 版本号从 " + LastVersionCode + " 升高到 " + ModBase.VersionCode); + States.System.LastVersion = ModBase.VersionCode; + // 检查有记录的最高版本号 + int LowerVersionCode; +#if BETA + LowerVersionCode = Setup.Get("SystemHighestBetaVersionReg") + If LowerVersionCode < VersionCode Then + Setup.Set("SystemHighestBetaVersionReg", VersionCode) + Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode) + End If +#else + LowerVersionCode = Conversions.ToInteger(States.System.LastAlphaVersion); + if (LowerVersionCode < ModBase.VersionCode) + { + States.System.LastAlphaVersion = ModBase.VersionCode; + ModBase.Log("[Start] 最高版本号从 " + LowerVersionCode + " 升高到 " + ModBase.VersionCode); + } +#endif + + // 被移除的窗口设置选项 + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.GameWindowMode, 5, false))) + Config.Launch.GameWindowMode = GameWindowSizeMode.Default; + // 修改主题设置项名称 + if (LowerVersionCode <= 207) + { + var UnlockedTheme = new List { "2" }; + UnlockedTheme.AddRange(new List(States.UI.ThemeHiddenV1.ToString().Split("|"))); + UnlockedTheme.AddRange(new List(States.UI.ThemeHiddenV2.ToString().Split("|"))); + States.UI.ThemeHiddenV2 = UnlockedTheme.Distinct().ToList().Join("|"); + } + + // 重置欧皇彩 + if (LastVersionCode <= 115 && States.UI.ThemeHiddenV2.ToString().Split("|").Contains("13")) + { + var UnlockedTheme = new List(States.UI.ThemeHiddenV2.ToString().Split("|")); + UnlockedTheme.Remove("13"); + States.UI.ThemeHiddenV2 = UnlockedTheme.Join("|"); + ModMain.MyMsgBox("由于新版 PCL 修改了欧皇彩的解锁方式,你需要重新解锁欧皇彩。" + "\r\n" + "多谢各位的理解啦!", "重新解锁提醒"); + } + + // 重置滑稽彩 + if (LastVersionCode <= 152 && States.UI.ThemeHiddenV2.ToString().Split("|").Contains("12")) + { + var UnlockedTheme = new List(States.UI.ThemeHiddenV2.ToString().Split("|")); + UnlockedTheme.Remove("12"); + States.UI.ThemeHiddenV2 = UnlockedTheme.Join("|"); + ModMain.MyMsgBox("由于新版 PCL 修改了滑稽彩的解锁方式,你需要重新解锁滑稽彩。" + "\r\n" + "多谢各位的理解啦!", "重新解锁提醒"); + } + + // 移动自定义皮肤 + if (LastVersionCode <= 161 && File.Exists(ModBase.ExePath + @"PCL\CustomSkin.png") && + !File.Exists(ModBase.PathAppdata + "CustomSkin.png")) + { + ModBase.CopyFile(ModBase.ExePath + @"PCL\CustomSkin.png", ModBase.PathAppdata + "CustomSkin.png"); + ModBase.Log("[Start] 已移动离线自定义皮肤 (162)"); + } + + if (LastVersionCode <= 263 && File.Exists(ModBase.PathTemp + "CustomSkin.png") && + !File.Exists(ModBase.PathAppdata + "CustomSkin.png")) + { + ModBase.CopyFile(ModBase.PathTemp + "CustomSkin.png", ModBase.PathAppdata + "CustomSkin.png"); + ModBase.Log("[Start] 已移动离线自定义皮肤 (264)"); + } + + // 解除帮助页面的隐藏 + if (LastVersionCode <= 205) + { + Config.Preference.Hide.SetupAbout = false; + ModBase.Log("[Start] 已解除帮助页面的隐藏"); + } + + // 迁移旧版用户档案 + if (LastVersionCode <= 368) ModBase.RunInNewThread(() => ModProfile.MigrateOldProfile()); + // Mod 命名设置迁移 + if (!ModBase.Setup.IsUnset("ToolDownloadTranslate") && ModBase.Setup.IsUnset("ToolDownloadTranslateV2")) + { + Config.Download.Comp.NameFormatV2 += 1; + ModBase.Log("[Start] 已从老版本迁移 Mod 命名设置"); + } + + // 更新后展示社区版提示 + ModSecret.ShowCEAnnounce(); + // 输出更新日志 + if (LastVersionCode <= 0) + return; + if (LowerVersionCode >= ModBase.VersionCode) + return; + ShowUpdateLog(); + } + + private void DowngradeSub(int LastVersionCode) + { + ModBase.Log("[Start] 版本号从 " + LastVersionCode + " 降低到 " + ModBase.VersionCode); + States.System.LastVersion = ModBase.VersionCode; + } + + #endregion + + #region 自定义窗口 + + private bool CanResize = true; + + // 重写窗口边缘判定以使 DWM 自带的 resizer 行为看起来比较正常 + private nint _SizeWndProc(nint hWnd, int msg, nint wParam, nint lParam, ref bool handled) + { + // 窗口活动常量 + const int WM_NCHITTEST = 0x84; + const int HTCLIENT = 1; + const int HTLEFT = 10; + const int HTRIGHT = 11; + const int HTTOP = 12; + const int HTTOPLEFT = 13; + const int HTTOPRIGHT = 14; + const int HTBOTTOM = 15; + const int HTBOTTOMLEFT = 16; + const int HTBOTTOMRIGHT = 17; + + // WPF 尺寸的 offset + const int offsetWpf = 6; + const int hitWidthWpf = 5; + + // 过滤非 WM_NCHITTEST 事件 + if (msg != WM_NCHITTEST) + return nint.Zero; + + // 提取鼠标坐标 + // 没妈的 VB 强转还得检查一下幻想的妈是不是还活着 + var mouseBytes = BitConverter.GetBytes(lParam.ToInt64()); + var xMouse = BitConverter.ToInt16(mouseBytes, 0); + var yMouse = BitConverter.ToInt16(mouseBytes, 2); + + // 获取窗口参数 + var windowRect = WindowInterop.GetWindowRectangle(hWnd); + var windowBounds = windowRect.ToWindowBounds(); + + // 判断鼠标是否在窗口范围内 + var isInWindow = xMouse >= windowRect.Left && xMouse <= windowRect.Right && yMouse >= windowRect.Top && + yMouse <= windowRect.Bottom; + + // 过滤不在窗口内的请求 + if (!isInWindow) + return nint.Zero; + + // 如果 CanResize 为 False,直接返回 HTCLIENT + if (!CanResize) + return new nint(HTCLIENT); + + // 真实像素尺寸的 offset + var dpi = VisualTreeHelper.GetDpi(this); + var offsetPxX = offsetWpf * dpi.DpiScaleX; + var offsetPxY = offsetWpf * dpi.DpiScaleY; + var hitWidthPxX = hitWidthWpf * dpi.DpiScaleX; + var hitWidthPxY = hitWidthWpf * dpi.DpiScaleY; + + // 计算鼠标相对于窗口左上角的物理像素位置 + var relX = xMouse - windowRect.Left; + var relY = yMouse - windowRect.Top; + var w = windowBounds.Width; + var h = windowBounds.Height; + + // 判定是否命中偏移后的热区 + var inLeft = relX >= offsetPxX && relX <= offsetPxX + hitWidthPxX; + var inRight = relX <= w - offsetPxX && relX >= w - offsetPxX - hitWidthPxX; + var inTop = relY >= offsetPxY && relY <= offsetPxY + hitWidthPxY; + var inBottom = relY <= h - offsetPxY && relY >= h - offsetPxY - hitWidthPxY; + + handled = true; // 接管该区域的消息 + + // 返回结果 + if (inTop && inLeft) + return new nint(HTTOPLEFT); + if (inTop && inRight) + return new nint(HTTOPRIGHT); + if (inBottom && inLeft) + return new nint(HTBOTTOMLEFT); + if (inBottom && inRight) + return new nint(HTBOTTOMRIGHT); + if (inLeft) + return new nint(HTLEFT); + if (inRight) + return new nint(HTRIGHT); + if (inTop) + return new nint(HTTOP); + if (inBottom) + return new nint(HTBOTTOM); + + // 如果在 0-offset 范围内,返回 HTCLIENT 杀掉默认缩放 + return new nint(HTCLIENT); + } + + protected override void OnSourceInitialized(EventArgs e) + { + // 硬件加速 + if (Conversions.ToBoolean(Config.System.DisableHardwareAcceleration)) + { + var hwndSource = PresentationSource.FromVisual(this) as HwndSource; + if (hwndSource is not null) hwndSource.CompositionTarget.RenderMode = RenderMode.SoftwareOnly; + } + + base.OnSourceInitialized(e); + + // 获取当前窗口句柄 + var hwnd = new WindowInteropHelper(this).Handle; + var source = HwndSource.FromHwnd(hwnd); + if (source is not null) + { + // 渲染层允许 Alpha 通道通过 + source.CompositionTarget.BackgroundColor = Colors.Transparent; + // 魔改窗口边缘判定 + source.AddHook(_SizeWndProc); + } + + // 设置 DWM 窗口框架 + try + { + WindowInterop.ExtendFrameIntoClientArea(hwnd, -1); + } + catch (Exception ex) + { + LogWrapper.Error("DWM 窗口框架应用失败: " + ex.Message); + } + } + + // 关闭 + private void FormMain_Closing(object sender, CancelEventArgs e) + { + EndProgram(true); + e.Cancel = true; + } + + /// + /// 正常关闭程序。程序将在执行此方法后约 0.3s 退出。 + /// + /// 是否在还有下载任务未完成时发出警告。 + /// 是否正在更新重启 + public void EndProgram(bool SendWarning, bool isUpdating = false) + { + // 发出警告 + if (SendWarning && ModNet.HasDownloadingTask()) + { + if (ModMain.MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") == 1) + // 强行结束下载任务 + ModBase.RunInNewThread(() => + { + ModBase.Log("[System] 正在强行停止任务"); + foreach (var Task in ModLoader.LoaderTaskbar.ToList()) + Task.Abort(); + }, "强行停止下载任务"); + else + return; + } + + // 关闭联机大厅 + // Await LobbyController.CloseAsync().ConfigureAwait(False) + // 存储上次使用的档案编号 + ModProfile.SaveProfile(); + // 关闭 + ModBase.RunInUiWait(() => + { + // 清理视频背景 + VideoBack.Stop(); + VideoBack.Source = null; + VideoBack.Close(); + IsHitTestVisible = false; + if (RenderTransform is null) + { + var TransformPos = new TranslateTransform(0d, 0d); + var TransformRotate = new RotateTransform(0d); + var TransformScale = new ScaleTransform(1d, 1d); + TransformScale.CenterX = Width / 2d; + TransformScale.CenterY = Height / 2d; + RenderTransform = new TransformGroup + { Children = new TransformCollection([TransformRotate, TransformPos, TransformScale]) }; + ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(this, -Opacity, 140, 40, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => + { + TransformScale.ScaleX += (double)i; + TransformScale.ScaleY += (double)i; + }, 0.88d - TransformScale.ScaleX, 180), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + 20d - TransformPos.Y, 180, 0, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 0.6d - TransformRotate.Angle, 180, 0, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => + { + IsHitTestVisible = false; + Visibility = Visibility.Collapsed; + ShowInTaskbar = false; + }, 210), + ModAnimation.AaCode(() => EndProgramForce(force: false, isUpdating: isUpdating), 230) + }, "Form Close"); + } + else + { + EndProgramForce(force: false, isUpdating: isUpdating); + } + + ModBase.Log("[System] 收到关闭指令"); + }); + } + + private static bool IsLogShown; + + public static void EndProgramForce(ModBase.ProcessReturnValues ReturnCode = ModBase.ProcessReturnValues.Success, + bool force = true, bool isUpdating = false) + { + // On Error Resume Next + // 关闭联机大厅 + // Await LobbyController.CloseAsync().ConfigureAwait(False) + ModBase.IsProgramEnded = true; + ModAnimation.AniControlEnabled += 1; + if (ModSecret.IsUpdateWaitingRestart && !isUpdating) + ModSecret.UpdateRestart(false, false); + if (ReturnCode == ModBase.ProcessReturnValues.Exception) + { + if (!IsLogShown) + { + ModBase.FeedbackInfo(); + ModBase.Log("请在 https://github.com/PCL-Community/PCL2-CE/issues 提交错误报告,以便于社区解决此问题!(这也有可能是原版 PCL 的问题)"); + IsLogShown = true; + ModBase.ShellOnly(LogWrapper.CurrentLogger.CurrentLogFiles.Last()); + } + + Thread.Sleep(500); // 防止 PCL 在记事本打开前就被掐掉 + } + + ModBase.Log("[System] 程序已退出,返回值:" + ModBase.GetStringFromEnum(ReturnCode)); + // If ReturnCode <> ProcessReturnValues.Success Then Environment.Exit(ReturnCode) + // Process.GetCurrentProcess.Kill() + Lifecycle.Shutdown((int)ReturnCode, force); + } + + private void BtnTitleClose_Click(object sender, EventArgs e) + { + EndProgram(true); + } + + // 移动 + private void FormDragMove(object sender, MouseButtonEventArgs e) + { + // On Error Resume Next + if (((Grid)sender).IsMouseDirectlyOver) + DragMove(); + } + + // 改变大小 + /// + /// 是否可以向注册表储存尺寸改变信息。以此避免初始化时误储存。 + /// + public bool IsSizeSaveable; + + private void FormMain_SizeChanged(object? sender = null, EventArgs? e = null) + { + if (IsSizeSaveable) + { + States.UI.WindowHeight = Height; + States.UI.WindowWidth = Width; + } + + if (PanBack is not null) + { + RectForm.Rect = new Rect(0d, 0d, PanBack.ActualWidth, PanBack.ActualHeight); + + var formWidth = PanBack.ActualWidth + 0.001d; + var formHeight = PanBack.ActualHeight + 0.001d; + + PanForm.Width = formWidth; + PanForm.Height = formHeight; + PanMain.Width = formWidth; + + if (PanTitle is not null) + PanMain.Height = Math.Max(0d, formHeight - PanTitle.ActualHeight); + else + PanMain.Height = formHeight; + + VideoBack.Width = formWidth; + VideoBack.Height = formHeight; + } + + if (WindowState == WindowState.Maximized) + WindowState = WindowState.Normal; // 修复 #1938 + } + + // 标题栏改变大小 + private void PanTitle_SizeChanged(object sender, EventArgs e) + { + if (PanTitleMain.ColumnDefinitions[0].ActualWidth - 30 <= 0) + PanTitleLeft.ColumnDefinitions[0].MaxWidth = 0; + else + PanTitleLeft.ColumnDefinitions[0].MaxWidth = PanTitleMain.ColumnDefinitions[0].ActualWidth - 30; + } + + // 最小化 + private void BtnTitleMin_Click(object sender, EventArgs e) + { + WindowState = WindowState.Minimized; + } + + #endregion + + #region 窗体事件 + + public void AddResizer() + { + CanResize = true; + } + + public void RemoveResizer() + { + CanResize = false; + } + + // 按键事件 + private void FormMain_KeyDown(object sender, KeyEventArgs e) + { + if (e.IsRepeat) + return; + // 调用弹窗:回车选择第一个,Esc 选择最后一个 + if (PanMsg.Children.Count > 0) + { + if (e.Key == Key.Enter) + { + ((MyMsgInput)PanMsg.Children[0]).Btn1_Click(sender, null); + return; + } + + if (e.Key == Key.Escape) + { + object Msg = PanMsg.Children[0]; + if (!(Msg is MyMsgInput) && !(Msg is MyMsgSelect) && Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(((dynamic)Msg).Btn3.Visibility, Visibility.Visible, + false))) + ((dynamic)Msg).Btn3_Click(); + else if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(((dynamic)Msg).Btn2.Visibility, + Visibility.Visible, false))) + ((dynamic)Msg).Btn2_Click(); + else + ((dynamic)Msg).Btn1_Click(); + return; + } + } + + // 按 ESC 返回上一级 + if (e.Key == Key.Escape) + TriggerPageBack(); + // 更改隐藏实例可见性 + if (e.Key == Key.F11 && PageCurrent == PageType.InstanceSelect) + { + ModMain.FrmSelectRight.ShowHidden = !ModMain.FrmSelectRight.ShowHidden; + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + return; + } + + // 更改功能隐藏可见性 + if (e.Key == Key.F12) + { + PageSetupUI.HiddenForceShow = !PageSetupUI.HiddenForceShow; + if (PageSetupUI.HiddenForceShow) + ModMain.Hint("功能隐藏设置已暂时关闭!", ModMain.HintType.Finish); + else + ModMain.Hint("功能隐藏设置已重新开启!", ModMain.HintType.Finish); + PageSetupUI.HiddenRefresh(); + return; + } + + // 按 F5 刷新页面 + if (e.Key == Key.F5) + { + if (PageLeft is IRefreshable) + ((IRefreshable)PageLeft).Refresh(); + if (PageRight is IRefreshable) + ((IRefreshable)PageRight).Refresh(); + return; + } + + // 调用启动游戏 + if (e.Key == Key.Enter && PageCurrent == PageType.Launch) + { + if (ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup) + ModMain.Hint("木大!"); + else + ModMain.FrmLaunchLeft.LaunchButtonClick(); + } + + // 修复按下 Alt 后误认为弹出系统菜单导致的冻结 + if (e.SystemKey == Key.LeftAlt || e.SystemKey == Key.RightAlt) + e.Handled = true; + } + + private void FormMain_MouseDown(object sender, MouseButtonEventArgs e) + { + // 鼠标侧键返回上一级 + if (ModMain.FrmMain!.PanMsg.Children.Count > 0 || ModMain.WaitingMyMsgBox.Any()) + return; // 弹窗中(#5513) + if (e.ChangedButton == MouseButton.XButton1 || e.ChangedButton == MouseButton.XButton2) + TriggerPageBack(); + } + + private void TriggerPageBack() + { + if (PageCurrent == PageType.Download && PageCurrentSub == PageSubType.DownloadInstall && + ModMain.FrmDownloadInstall.IsInSelectPage) + ModMain.FrmDownloadInstall.ExitSelectPage(); + else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionInstall && + ModMain.FrmInstanceInstall.IsInSelectPage) + ModMain.FrmInstanceInstall.ExitSelectPage(); + else + PageBack(); + } + + // 切回窗口 + private void FormMain_Activated(object sender, EventArgs e) + { + try + { + if (Conversions.ToBoolean(Config.Download.Comp.ReadClipboard)) + ModComp.CompClipboard.GetClipboardResource(); + if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionMod) + { + // Mod 管理自动刷新 + ModMain.FrmInstanceMod.ReloadCompFileList(); + } + else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionResourcePack) + { + // 资源包管理自动刷新 + if (ModMain.FrmInstanceResourcePack is not null) + ModMain.FrmInstanceResourcePack.ReloadCompFileList(); + } + else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionShader) + { + // 光影包管理自动刷新 + if (ModMain.FrmInstanceShader is not null) + ModMain.FrmInstanceShader.ReloadCompFileList(); + } + else if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic) + { + // 投影原理图管理自动刷新 + if (ModMain.FrmInstanceSchematic is not null) + ModMain.FrmInstanceSchematic.ReloadCompFileList(); + } + else if (PageCurrent == PageType.InstanceSelect) + { + // 实例选择自动刷新 + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.RunOnUpdated, 1, @"versions\"); + } + else if (ModMain.FrmMain.PageRight is PageInstanceSavesDatapack && + ModMain.FrmInstanceSavesDatapack is not null) + { + // 数据包管理自动刷新 + ModMain.FrmInstanceSavesDatapack.ReloadDatapackFileList(); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "切回窗口时出错", ModBase.LogLevel.Feedback); + } + } + + private IDataObject _HandleDrag_PrevData; + private DragDropEffects _HandleDrag_PrevEffects; + + // 文件拖放 + private void HandleDrag(object sender, DragEventArgs e) + { + try + { + if (e.Handled && e.Effects != DragDropEffects.None) + return; + // 缓存 + e.Handled = true; + if (ReferenceEquals(e.Data, _HandleDrag_PrevData)) + { + e.Effects = _HandleDrag_PrevEffects; + return; + } + + // 确定拖放效果 + e.Effects = DragDropEffects.None; + if (e.Data.GetDataPresent(DataFormats.Text)) + { + var Str = Conversions.ToString(e.Data.GetData(DataFormats.Text)); + if (Str.StartsWithF("authlib-injector:yggdrasil-server:")) + e.Effects = DragDropEffects.Copy; + else if (Str.StartsWithF("file:///")) e.Effects = DragDropEffects.Copy; + } + else if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + var Files = (string[])e.Data.GetData(DataFormats.FileDrop); + if (Files is not null && Files.Length > 0) e.Effects = DragDropEffects.Link; + } + + _HandleDrag_PrevData = e.Data; + _HandleDrag_PrevEffects = e.Effects; + ModBase.Log("[System] 设置拖放类型:" + ModBase.GetStringFromEnum(e.Effects)); + } + catch (Exception ex) + { + ModBase.Log(ex, "处理拖放时出错", ModBase.LogLevel.Feedback); + } + } + + private void FrmMain_Drop(object sender, DragEventArgs e) + { + try + { + if (e.Data.GetDataPresent(DataFormats.Text)) + { + // 获取文本 + try + { + var Str = Conversions.ToString(e.Data.GetData(DataFormats.Text)); + ModBase.Log("[System] 接受文本拖拽:" + Str); + if (Str.StartsWithF("authlib-injector:yggdrasil-server:")) + { + // Authlib 拖拽 + e.Handled = true; + e.Effects = DragDropEffects.Copy; + var AuthlibServer = + WebUtility.UrlDecode(Str.Substring("authlib-injector:yggdrasil-server:".Length)); + ModBase.Log("[System] Authlib 拖拽:" + AuthlibServer); + if (!string.IsNullOrEmpty(new ValidateHttp().Validate(AuthlibServer))) + { + ModMain.Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", ModMain.HintType.Critical); + return; + } + + if (ModMain.MyMsgBox($"是否要创建新的第三方验证档案?{"\r\n"}验证服务器地址:{AuthlibServer}", "创建新的第三方验证档案", + "确定", "取消") == 2) + return; + ModProfile.SelectedProfile = null; + ModBase.RunInUi(() => + { + PageLoginAuth.DraggedAuthServer = AuthlibServer; + ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth); + }); + if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSetup) + // 正在服务器选项页,需要刷新设置项显示 + ModMain.FrmInstanceSetup.Reload(); + } + else if (Str.StartsWithF("file:///")) + { + // 文件拖拽(例如从浏览器下载窗口拖入) + var FilePath = WebUtility.UrlDecode(Str).Substring("file:///".Length).Replace("/", @"\"); + e.Handled = true; + e.Effects = DragDropEffects.Copy; + FileDrag(new List { FilePath }); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "无法接取文本拖拽事件", ModBase.LogLevel.Developer); + } + } + else if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + // 获取文件并检查 + var FilePathRaw = e.Data.GetData(DataFormats.FileDrop); + if (FilePathRaw is null) // #2690 + { + ModMain.Hint("请将文件解压后再拖入!", ModMain.HintType.Critical); + return; + } + + e.Handled = true; + e.Effects = DragDropEffects.Link; + FileDrag((IEnumerable)FilePathRaw); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "接取拖拽事件失败", ModBase.LogLevel.Feedback); + } + } + + private void FileDrag(IEnumerable FilePathList) + { + ModBase.RunInNewThread(() => + { + var FilePath = FilePathList.First(); + ModBase.Log("[System] 接受文件拖拽:" + FilePath + (FilePathList.Any() ? $" 等 {FilePathList.Count()} 个文件" : ""), + ModBase.LogLevel.Developer); + // 基础检查 + if (Directory.Exists(FilePathList.First()) && !File.Exists(FilePathList.First())) + { + ModMain.Hint("请拖入一个文件,而非文件夹!", ModMain.HintType.Critical); + return; + } + + if (!File.Exists(FilePathList.First())) + { + ModMain.Hint("拖入的文件不存在:" + FilePathList.First(), ModMain.HintType.Critical); + return; + } + + // 多文件拖拽 + if (FilePathList.Count() > 1) + { + // 检查是否为同类型文件 + var FirstExtension = FilePathList.First().AfterLast(".").ToLower(); + var AllSameType = FilePathList.All(f => (f.AfterLast(".").ToLower() ?? "") == (FirstExtension ?? "")); + + if (AllSameType && + new[] { "jar", "litemod", "disabled", "old", "litematic", "nbt", "schematic", "schem" }.Contains( + FirstExtension)) + { + } + // 允许同类型的 Mod 文件或投影文件批量拖拽 + else + { + ModMain.Hint("一次请只拖入相同类型的文件!", ModMain.HintType.Critical); + return; + } + } + + // 主页 + var Extension = FilePath.AfterLast(".").ToLower(); + if (Extension == "xaml") + { + ModBase.Log("[System] 文件后缀为 XAML,作为主页加载"); + if (File.Exists(ModBase.ExePath + @"PCL\Custom.xaml")) + if (ModMain.MyMsgBox("已存在一个主页文件,是否要将它覆盖?", "覆盖确认", "覆盖", "取消") == 2) + return; + + ModBase.CopyFile(FilePath, ModBase.ExePath + @"PCL\Custom.xaml"); + ModBase.RunInUi(() => + { + Config.Preference.Homepage.Type = 1; + ModMain.FrmLaunchRight.ForceRefresh(); + ModMain.Hint("已加载主页自定义文件!", ModMain.HintType.Finish); + }); + return; + } + + // 安装 Mod + if (PageInstanceCompResource.InstallMods(FilePathList)) + return; + // 安装投影文件 + if (new[] { "litematic", "nbt", "schematic", "schem" }.Contains(Extension)) + { + ModBase.Log($"[System] 文件为 {Extension} 格式,尝试作为原理图安装"); + // 获取当前文件夹路径(如果在资源管理页面) + string targetFolderPath = null; + if (PageCurrent == PageType.InstanceSetup && PageCurrentSub == PageSubType.VersionSchematic && + ModMain.FrmInstanceSchematic is not null && + ModMain.FrmInstanceSchematic is PageInstanceCompResource) + targetFolderPath = ModMain.FrmInstanceSchematic.CurrentFolderPath; + PageInstanceCompResource.InstallCompFiles(FilePathList, ModComp.CompType.Schematic, targetFolderPath); + return; + } + + // 处理资源安装 + if (PageCurrent == PageType.InstanceSetup && new[] { "zip" }.Any(i => (i ?? "") == (Extension ?? ""))) + switch (PageCurrentSub) + { + case PageSubType.VersionWorld: + { + var DestFolder = PageInstanceLeft.Instance.PathIndie + @"saves\" + + ModBase.GetFileNameWithoutExtentionFromPath(FilePath); + if (Directory.Exists(DestFolder)) + { + ModMain.Hint("发现同名文件夹,无法粘贴:" + DestFolder, ModMain.HintType.Critical); + return; + } + + ModBase.ExtractFile(FilePath, DestFolder); + ModMain.Hint($"已导入 {ModBase.GetFileNameWithoutExtentionFromPath(FilePath)}", + ModMain.HintType.Finish); + if (ModMain.FrmInstanceSaves is not null) + ModBase.RunInUi(() => ModMain.FrmInstanceSaves.Reload()); + return; + } + case PageSubType.VersionResourcePack: + { + var DestFile = PageInstanceLeft.Instance.PathIndie + @"resourcepacks\" + + ModBase.GetFileNameFromPath(FilePath); + if (File.Exists(DestFile)) + { + ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical); + return; + } + + ModBase.CopyFile(FilePath, DestFile); + ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish); + if (ModMain.FrmInstanceResourcePack is not null) + ModBase.RunInUi(() => ModMain.FrmInstanceResourcePack.ReloadCompFileList()); + return; + } + case PageSubType.VersionShader: + { + var DestFile = PageInstanceLeft.Instance.PathIndie + @"shaderpacks\" + + ModBase.GetFileNameFromPath(FilePath); + if (File.Exists(DestFile)) + { + ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical); + return; + } + + ModBase.CopyFile(FilePath, DestFile); + ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish); + if (ModMain.FrmInstanceShader is not null) + ModBase.RunInUi(() => ModMain.FrmInstanceShader.ReloadCompFileList()); + return; + } + } + + // 处理投影文件 + if (PageCurrent == PageType.InstanceSetup && + new[] { "litematic", "nbt", "schematic", "schem" }.Contains(Extension) && + PageCurrentSub == PageSubType.VersionSchematic) + { + var DestFile = PageInstanceLeft.Instance.PathIndie + @"schematics\" + + ModBase.GetFileNameFromPath(FilePath); + if (File.Exists(DestFile)) + { + ModMain.Hint("已存在同名文件:" + DestFile, ModMain.HintType.Critical); + return; + } + + Directory.CreateDirectory(PageInstanceLeft.Instance.PathIndie + @"schematics\"); + ModBase.CopyFile(FilePath, DestFile); + ModMain.Hint($"已导入 {ModBase.GetFileNameFromPath(FilePath)}", ModMain.HintType.Finish); + if (ModMain.FrmInstanceSchematic is not null) + ModBase.RunInUi(() => ModMain.FrmInstanceSchematic.ReloadCompFileList()); + return; + } + + // 安装整合包 + if (new[] { "zip", "rar", "mrpack" }.Any(t => + (t ?? "") == (Extension ?? ""))) // 部分压缩包是 zip 格式但后缀为 rar,总之试一试 + { + ModBase.Log("[System] 文件为压缩包,尝试作为整合包安装"); + try + { + ModModpack.ModpackInstall(FilePath); + return; + } + catch (ModBase.CancelledException ex) + { + return; // 用户主动取消 + } + catch (Exception ex) + { + // 安装失败,继续往后尝试 + } + } + + if (new[] { "zip", "rar" }.Any(t => (t ?? "") == (Extension ?? ""))) + { + ModBase.Log("[System] 文件为压缩包,尝试作为存档分析"); + try + { + ModWorld.ReadWorld(FilePath); + return; + } + catch (ModBase.CancelledException ex) + { + return; // 是存档,但是损坏了 + } + catch (Exception ex) + { + // 不是存档(或遇到了其他问题),继续往后尝试 + } + } + + // 错误报告分析 + do + { + try + { + ModBase.Log("[System] 尝试进行错误报告分析"); + var Analyzer = new CrashAnalyzer(ModBase.GetUuid()); + Analyzer.Import(FilePath); + if (!Analyzer.Prepare()) + break; + Analyzer.Analyze(); + Analyzer.Output(true, new List()); + return; + } + catch (Exception ex) + { + ModBase.Log(ex, "自主错误报告分析失败", ModBase.LogLevel.Feedback); + } + } while (false); + + // 未知操作 + ModMain.Hint("PCL 无法确定应当执行的文件拖拽操作……"); + }, "文件拖拽"); + } + + // 接受到 Windows 窗体事件 + public bool IsSystemTimeChanged; + + private nint WndProc(nint hwnd, int msg, nint wParam, nint lParam, ref bool handled) + { + if (msg == 30) + { + var NowDate = DateTime.Now; + if (NowDate.Date == ModBase.ApplicationOpenTime.Date) + { + ModBase.Log("[System] 系统时间微调为:" + NowDate.ToLongDateString() + " " + NowDate.ToLongTimeString()); + IsSystemTimeChanged = false; + } + else + { + ModBase.Log("[System] 系统时间修改为:" + NowDate.ToLongDateString() + " " + NowDate.ToLongTimeString()); + IsSystemTimeChanged = true; + } + } + else if (msg == 400 * 16 + 2) + { + ModBase.Log("[System] 收到置顶信息:" + hwnd.ToInt64()); + if (!IsWindowLoadFinished) + { + ModBase.Log("[System] 窗口尚未加载完成,忽略置顶请求"); + return nint.Zero; + } + + ShowWindowToTop(); + handled = true; + } + else if (msg == 26) // WM_SETTINGCHANGE + { + if (Marshal.PtrToStringAuto(lParam) == "ImmersiveColorSet") + { + ModBase.Log($"[System] 系统主题更改,深色模式:{SystemTheme.IsSystemInDarkMode()}"); + if (Operators.ConditionalCompareObjectEqual(Config.Preference.Theme.ColorMode, 2, false) & + (ModSecret.IsDarkMode != SystemTheme.IsSystemInDarkMode())) ThemeService.RefreshColorMode(); + } + } + + return nint.Zero; + } + + // 窗口隐藏与置顶 + private bool _Hidden; + + public bool Hidden + { + get => _Hidden; + set + { + if (_Hidden == value) + return; + _Hidden = value; + if (value) + { + // 隐藏 + Left -= 10000d; + ShowInTaskbar = false; + Visibility = Visibility.Hidden; + ModBase.Log("[System] 窗口已隐藏,位置:(" + Left + "," + Top + ")"); + } + else + { + // 取消隐藏 + if (Left < -2000) + Left += 10000d; + ShowWindowToTop(); + } + } + } + + // 解决龙猫的非通用实现史山 + protected override void OnActivated(EventArgs e) + { + base.OnActivated(e); + if (Hidden) + Hidden = false; + } + + /// + /// 把当前窗口拖到最前面。 + /// + public void ShowWindowToTop() + { + ModBase.RunInUi(() => + { + // 这一坨乱七八糟的,别改,改了指不定就炸了,自己电脑还复现不出来 + Visibility = Visibility.Visible; + ShowInTaskbar = true; + WindowState = WindowState.Normal; + Hidden = false; + Topmost = true; // 偶尔 SetForegroundWindow 失效 + Topmost = false; + ModMain.SetForegroundWindow(ModBase.FrmHandle); + Focus(); + ModBase.Log($"[System] 窗口已置顶,位置:({Left}, {Top}), {Width} x {Height}"); + }); + } + + // 背景视频循环播放 + private void VideoEnded(object sender, RoutedEventArgs e) + { + VideoBack.Position = TimeSpan.Zero; + VideoBack.Play(); + } + + // 最小化时暂停背景视频 + private void WindowStateChanged(object sender, EventArgs e) + { + switch (WindowState) + { + case WindowState.Minimized: + { + ModVideoBack.IsMinimized = true; + ModVideoBack.VideoPause(); + break; + } + case WindowState.Normal: + { + ModVideoBack.IsMinimized = false; + ModVideoBack.VideoPlay(); + break; + } + } + } + + #endregion + + #region 切换页面 + + // 页面种类与属性 + // 注意,这一枚举在 “切换页面” EventType 中调用,应视作公开 API 的一部分 + /// + /// 页面种类。 + /// + public enum PageType + { + /// + /// 启动。 + /// + Launch = 0, + + /// + /// 下载。 + /// + Download = 1, + + /// + /// 联机。 + /// + Tools = 3, + + /// + /// 设置。 + /// + Setup = 2, + + /// + /// 实例选择。这是一个副页面。 + /// + InstanceSelect = 5, + + /// + /// 任务管理。这是一个副页面。 + /// + TaskManager = 6, + + /// + /// 实例设置。这是一个副页面。 + /// + InstanceSetup = 7, + + /// + /// 资源工程详情。这是一个副页面。 + /// + CompDetail = 8, + + /// + /// 帮助详情。这是一个副页面。 + /// + HelpDetail = 9, + + /// + /// 游戏实时日志。这是一个副页面。 + /// + GameLog = 10, + + /// + /// 存档详细管理,这是一个副页面。 + /// + VersionSaves = 12, + + /// + /// 主页市场,这是一个副页面。 + /// + HomePageMarket = 13 + } + + /// + /// 次要页面种类。其数值必须与 StackPanel 中的下标一致。 + /// + public enum PageSubType + { + Default = 0, + DownloadInstall = 1, + DownloadMod = 2, + DownloadPack = 3, + DownloadDataPack = 4, + DownloadResourcePack = 5, + DownloadShader = 6, + DownloadWorld = 7, + DownloadCompFavorites = 8, + DownloadClient = 9, + DownloadOptiFine = 10, + DownloadForge = 11, + DownloadNeoForge = 12, + DownloadCleanroom = 13, + DownloadFabric = 14, + DownloadQuilt = 15, + DownloadLiteLoader = 16, + DownloadLabyMod = 17, + DownloadLegacyFabric = 18, + + SetupLaunch = 0, + SetupUI = 1, + SetupGameManage = 2, + SetupLink = 3, + SetupAbout = 4, + SetupLog = 5, + SetupFeedback = 6, + SetupGameLink = 7, + SetupUpdate = 8, + SetupJava = 9, + SetupLauncherMisc = 10, + + ToolsGameLink = 1, + ToolsLauncherHelp = 2, + ToolsTest = 3, + + VersionOverall = 0, + VersionSetup = 1, + VersionExport = 2, + VersionWorld = 3, + VersionScreenshot = 4, + VersionMod = 5, + VersionModDisabled = 6, + VersionResourcePack = 7, + VersionShader = 8, + VersionSchematic = 9, + VersionInstall = 10, + VersionServer = 11, + VersionSavesInfo = 0, + VersionSavesBackup = 1, + VersionSavesDatapack = 2 + } + + /// + /// 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。 + /// + private string PageNameGet(PageStackData Stack) + { + switch (Stack.Page) + { + case PageType.InstanceSelect: + { + return "实例选择"; + } + case PageType.TaskManager: + { + return "任务管理"; + } + case PageType.GameLog: + { + return "实时日志"; + } + case PageType.InstanceSetup: + { + return "实例设置 - " + (PageInstanceLeft.Instance is null ? "未知实例" : PageInstanceLeft.Instance.Name); + } + case PageType.CompDetail: + { + return "资源下载 - " + ((ModComp.CompProject)((object[])Stack.Additional)[0]).TranslatedName; + } + case PageType.HelpDetail: + { + return ((ModMain.HelpEntry)((object[])Stack.Additional)[0]).Title; + } + case PageType.VersionSaves: + { + return $"存档管理 - {ModBase.GetFolderNameFromPath(Conversions.ToString(Stack.Additional))}"; + } + case PageType.HomePageMarket: + { + return "主页市场"; + } + + default: + { + return ""; + } + } + } + + /// + /// 刷新次级页面的名称。 + /// + public void PageNameRefresh(PageStackData Type) + { + LabTitleInner.Text = PageNameGet(Type); + } + + /// + /// 刷新次级页面的名称。 + /// + public void PageNameRefresh() + { + PageNameRefresh(PageCurrent); + } + + // 页面状态存储 + /// + /// 当前的主页面。 + /// + public PageStackData PageCurrent = PageType.Launch; + + /// + /// 上一个主页面。 + /// + public PageStackData PageLast = PageType.Launch; + + /// + /// 当前的子页面。 + /// + public PageSubType PageCurrentSub + { + get + { + switch ((dynamic)PageCurrent) + { + case PageType.Download: + { + if (ModMain.FrmDownloadLeft is null) + ModMain.FrmDownloadLeft = new PageDownloadLeft(); + return ModMain.FrmDownloadLeft.PageID; + } + + case PageType.Setup: + { + if (ModMain.FrmSetupLeft is null) + ModMain.FrmSetupLeft = new PageSetupLeft(); + return ModMain.FrmSetupLeft.PageID; + } + + case PageType.InstanceSetup: + { + if (ModMain.FrmInstanceLeft is null) + ModMain.FrmInstanceLeft = new PageInstanceLeft(); + return ModMain.FrmInstanceLeft.PageID; + } + + default: + { + return 0; // 没有子页面 + } + } + } + } + + /// + /// 上层页面的编号堆栈,用于返回。 + /// + public List PageStack = new(); + + public class PageStackData + { + public object Additional; + + public PageType Page; + + public override bool Equals(object other) + { + if (other is null) + return false; + if (other is PageStackData) + { + var PageOther = (PageStackData)other; + if (Page != PageOther.Page) + return false; + if (Additional is null) return PageOther.Additional is null; + + return PageOther.Additional is not null && Additional.Equals(PageOther.Additional); + } + + if (other is int) + { + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectNotEqual(Page, other, false))) + return false; + return Additional is null; + } + + return false; + } + + public static bool operator ==(PageStackData left, PageStackData right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(PageStackData left, PageStackData right) + { + return !(left == right); + } + + public static implicit operator PageStackData(PageType Value) + { + return new PageStackData { Page = Value }; + } + + public static implicit operator PageType(PageStackData Value) + { + return Value.Page; + } + } + + public MyPageLeft PageLeft; + public MyPageRight PageRight; + + // 引发实际页面切换的入口 + private bool IsChangingPage; + + /// + /// 切换页面,并引起对应选择 UI 的改变。 + /// + public void PageChange(PageStackData Stack, PageSubType SubType = PageSubType.Default) + { + if (string.IsNullOrEmpty(PageNameGet(Stack))) + { + // 切换到主页面 + PageChangeExit(); + IsChangingPage = true; // 防止下面的勾选直接触发了 PageChangeActual + ((MyRadioButton)PanTitleSelect.Children[(int)Stack.Page]).SetChecked(true, true, + string.IsNullOrEmpty(PageNameGet(PageCurrent))); + IsChangingPage = false; + switch (Stack.Page) + { + case PageType.Download: + { + if (ModMain.FrmDownloadLeft is null) + ModMain.FrmDownloadLeft = new PageDownloadLeft(); + foreach (var item in ModMain.FrmDownloadLeft.PanItem.Children) + if (ReferenceEquals(item.GetType(), typeof(MyListItem)) && + ModBase.Val(((dynamic)item).tag) == (double)SubType) + { + ((MyListItem)item).SetChecked(true, true, Stack == PageCurrent); + break; + } + + break; + } + case PageType.Setup: + { + if (ModMain.FrmSetupLeft is null) + ModMain.FrmSetupLeft = new PageSetupLeft(); + if (ModMain.FrmSetupLeft.PanItem.Children[(int)SubType] is MyListItem) + ((MyListItem)ModMain.FrmSetupLeft.PanItem.Children[(int)SubType]).SetChecked(true, true, + Stack == PageCurrent); + break; + } + } + + PageChangeActual(Stack, SubType); + } + else + { + // 切换到次页面 + switch (Stack.Page) + { + case PageType.InstanceSetup: + { + if (ModMain.FrmInstanceLeft is null) + ModMain.FrmInstanceLeft = new PageInstanceLeft(); + foreach (var item in ModMain.FrmInstanceLeft.PanItem.Children) + if (ReferenceEquals(item.GetType(), typeof(MyListItem)) && + ModBase.Val(((dynamic)item).tag) == (double)SubType) + { + ((MyListItem)item).SetChecked(true, true, Stack == PageCurrent); + break; + } + + break; + } + case PageType.VersionSaves: + { + if (ModMain.FrmInstanceSavesLeft is null) + ModMain.FrmInstanceSavesLeft = new PageInstanceSavesLeft(); + foreach (var item in ModMain.FrmInstanceSavesLeft.PanItem.Children) + if (ReferenceEquals(item.GetType(), typeof(MyListItem)) && + ModBase.Val(((dynamic)item).tag) == (double)SubType) + { + ((MyListItem)item).SetChecked(true, true, Stack == PageCurrent); + break; + } + + break; + } + } + + PageChangeActual(Stack, SubType); + } + } + + /// + /// 通过点击导航栏改变页面。 + /// + private void BtnTitleSelect_Click(MyRadioButton sender, bool raiseByMouse) + { + if (IsChangingPage) + return; + var pageType = (PageType)int.Parse(sender.Tag.ToString()); + PageChangeActual(pageType, PageSubType.Default); + } + + private void BtnTitleInner_Click(object sender, EventArgs e) + { + PageBack(); + } + + /// + /// 通过点击返回按钮或手动触发返回来改变页面。 + /// + public void PageBack() + { + if (PageStack.Any()) + PageChangeActual(PageStack[0], PageSubType.Default); + else + PageChange(PageType.Launch); + } + + // 实际处理页面切换 + /// + /// 切换现有页面的实际方法。 + /// + private void PageChangeActual(PageStackData Stack, PageSubType SubType) + { + if (PageCurrent == Stack && (PageCurrentSub == SubType || (int)SubType == -1)) + return; + ModAnimation.AniControlEnabled += 1; + try + { + #region 子页面处理 + + var PageName = PageNameGet(Stack); + if (string.IsNullOrEmpty(PageName)) + { + // 即将切换到一个顶级页面 + PageChangeExit(); + } + // 即将切换到一个子页面 + else if (PageStack.Any()) + { + // 子页面 → 另一个子页面,更新 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabTitleInner, -LabTitleInner.Opacity, 130), + ModAnimation.AaCode(() => LabTitleInner.Text = PageName, After: true), + ModAnimation.AaOpacity(LabTitleInner, 1d, 150, 30) + }, "FrmMain Titlebar SubLayer"); + if (PageStack.Contains(Stack)) + // 返回到更上层的子页面 + while (PageStack.Contains(Stack)) + PageStack.RemoveAt(0); + else + // 进入更深层的子页面 + PageStack.Insert(0, PageCurrent); + } + else + { + // 主页面 → 子页面,进入 + PanTitleInner.Visibility = Visibility.Visible; + PanTitleMain.IsHitTestVisible = false; + PanTitleInner.IsHitTestVisible = true; + PageNameRefresh(Stack); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(PanTitleMain, -PanTitleMain.Opacity, 150), + ModAnimation.AaX(PanTitleMain, 12d - PanTitleMain.Margin.Left, 150, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaOpacity(PanTitleInner, 1d - PanTitleInner.Opacity, 150, 200), + ModAnimation.AaX(PanTitleInner, -PanTitleInner.Margin.Left, 350, 200, + new ModAnimation.AniEaseOutBack()), + ModAnimation.AaCode(() => PanTitleMain.Visibility = Visibility.Collapsed, After: true) + }, "FrmMain Titlebar FirstLayer"); + PageStack.Insert(0, PageCurrent); + } + + #endregion + + #region 实际更改页面框架 UI + + PageLast = PageCurrent; + PageCurrent = Stack; + switch (Stack.Page) + { + case PageType.Launch: // 启动 + { + PageChangeAnim(ModMain.FrmLaunchLeft, ModMain.FrmLaunchRight); + break; + } + case PageType.Download: // 下载 + { + ModMain.FrmDownloadLeft ??= new PageDownloadLeft(); + SubType = ModMain.FrmDownloadLeft.PageID; + // PageGet 方法会在未设置 SubType 时指定默认值,并建立相关页面的实例 + PageChangeAnim(ModMain.FrmDownloadLeft, (FrameworkElement)ModMain.FrmDownloadLeft.PageGet(SubType)); + break; + } + case PageType.Tools: // 联机 + { + ModMain.FrmToolsLeft ??= new PageToolsLeft(); + SubType = ModMain.FrmToolsLeft.PageID; + PageChangeAnim(ModMain.FrmToolsLeft, (FrameworkElement)ModMain.FrmToolsLeft.PageGet(SubType)); + break; + } + case PageType.Setup: // 设置 + { + ModMain.FrmSetupLeft ??= new PageSetupLeft(); + SubType = ModMain.FrmSetupLeft.PageID; + PageChangeAnim(ModMain.FrmSetupLeft, (FrameworkElement)ModMain.FrmSetupLeft.PageGet(SubType)); + break; + } + case PageType.GameLog: // 实时日志 + { + if (ModMain.FrmLogLeft is null) + ModMain.FrmLogLeft = new PageLogLeft(); + if (ModMain.FrmLogLeft is null) + ModMain.FrmLogRight = new PageLogRight(); + PageChangeAnim(ModMain.FrmLogLeft, ModMain.FrmLogRight); + break; + } + case PageType.InstanceSelect: // 实例选择 + { + if (ModMain.FrmSelectLeft is null) + ModMain.FrmSelectLeft = new PageSelectLeft(); + if (ModMain.FrmSelectRight is null) + ModMain.FrmSelectRight = new PageSelectRight(); + PageChangeAnim(ModMain.FrmSelectLeft, ModMain.FrmSelectRight); + break; + } + case PageType.TaskManager: // 任务管理 + { + if (ModMain.FrmSpeedLeft is null) + ModMain.FrmSpeedLeft = new PageSpeedLeft(); + if (ModMain.FrmSpeedRight is null) + ModMain.FrmSpeedRight = new PageSpeedRight(); + PageChangeAnim(ModMain.FrmSpeedLeft, ModMain.FrmSpeedRight); + break; + } + case PageType.InstanceSetup: // 实例设置 + { + if (ModMain.FrmInstanceLeft is null) + ModMain.FrmInstanceLeft = new PageInstanceLeft(); + PageChangeAnim(ModMain.FrmInstanceLeft, (FrameworkElement)ModMain.FrmInstanceLeft.PageGet(SubType)); + break; + } + case PageType.CompDetail: // Mod 信息 + { + if (ModMain.FrmDownloadCompDetail is null) + ModMain.FrmDownloadCompDetail = new PageDownloadCompDetail(); + PageChangeAnim(new MyPageLeft(), ModMain.FrmDownloadCompDetail); + break; + } + case PageType.HelpDetail: // 帮助详情 + { + PageChangeAnim(new MyPageLeft(), ((dynamic)Stack.Additional)[1]); + break; + } + case PageType.VersionSaves: // 存档管理 + { + if (ModMain.FrmInstanceSavesLeft is null) + ModMain.FrmInstanceSavesLeft = new PageInstanceSavesLeft(); + PageInstanceSavesLeft.CurrentSave = Conversions.ToString(Stack.Additional); + PageChangeAnim(ModMain.FrmInstanceSavesLeft, + (FrameworkElement)ModMain.FrmInstanceSavesLeft.PageGet(SubType)); + break; + } + case PageType.HomePageMarket: // 主页市场 + { + ModMain.FrmHomePageMarket = ModMain.FrmHomePageMarket ?? new PageHomePageMarket(); + PageChangeAnim(new MyPageLeft(), ModMain.FrmHomePageMarket); + break; + } + } + + #endregion + + #region 设置为最新状态 + + BtnExtraDownload.ShowRefresh(); + BtnExtraApril.ShowRefresh(); + + #endregion + + ModBase.Log("[Control] 切换主要页面:" + ModBase.GetStringFromEnum(Stack) + ", " + (int)SubType); + } + catch (Exception ex) + { + ModBase.Log(ex, "切换主要页面失败(ID " + (int)PageCurrent.Page + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private void PageChangeAnim(FrameworkElement TargetLeft, FrameworkElement TargetRight) + { + ModAnimation.AniStop("FrmMain LeftChange"); + ModAnimation.AniStop("PageLeft PageChange"); // 停止左边栏变更导致的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + ModAnimation.AniControlEnabled += 1; + // 清除新页面关联性 + if (!(TargetLeft.Parent == null)) + TargetLeft.SetValue(ContentPresenter.ContentProperty, null); + if (!(TargetRight == null) && !(TargetRight.Parent == null)) + TargetRight.SetValue(ContentPresenter.ContentProperty, null); + PageLeft = (MyPageLeft)TargetLeft; + PageRight = (MyPageRight)TargetRight; + // 触发页面通用动画 + ((MyPageLeft)PanMainLeft.Child).TriggerHideAnimation(); + ((MyPageRight)PanMainRight.Child).PageOnExit(); + ModAnimation.AniControlEnabled -= 1; + // 执行动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ModAnimation.AniControlEnabled += 1; + // 把新页面添加进容器 + PanMainLeft.Child = PageLeft; + PageLeft.Opacity = 0d; + PanMainLeft.Background = null; + ModAnimation.AniControlEnabled -= 1; + ModBase.RunInUi(() => PanMainLeft_Resize(PanMainLeft.ActualWidth), true); + }, 110), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + PageLeft.Opacity = 1d; + PageLeft.TriggerShowAnimation(); + }, 30, true) + }, "FrmMain PageChangeLeft"); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ModAnimation.AniControlEnabled += 1; + ((MyPageRight)PanMainRight.Child).PageOnForceExit(); + // 把新页面添加进容器 + PanMainRight.Child = PageRight; + PageRight.Opacity = 0d; + PanMainRight.Background = null; + ModAnimation.AniControlEnabled -= 1; + ModBase.RunInUi(() => BtnExtraBack.ShowRefresh(), true); + }, 110), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + PageRight.Opacity = 1d; + PageRight.PageOnEnter(); + }, 30, true) + }, "FrmMain PageChangeRight"); + } + + /// + /// 退出子界面。 + /// + private void PageChangeExit() + { + if (PageStack.Any()) + { + // 子页面 → 主页面,退出 + PanTitleMain.Visibility = Visibility.Visible; + PanTitleMain.IsHitTestVisible = true; + PanTitleInner.IsHitTestVisible = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(PanTitleInner, -PanTitleInner.Opacity, 150), + ModAnimation.AaX(PanTitleInner, -18 - PanTitleInner.Margin.Left, 150, + Ease: new ModAnimation.AniEaseInFluent()), + ModAnimation.AaOpacity(PanTitleMain, 1d - PanTitleMain.Opacity, 150, 200), + ModAnimation.AaX(PanTitleMain, -PanTitleMain.Margin.Left, 350, 200, + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => PanTitleInner.Visibility = Visibility.Collapsed, After: true) + }, "FrmMain Titlebar FirstLayer"); + PageStack.Clear(); + } + // 主页面 → 主页面,无事发生 + } + + // 左边栏改变 + private void PanMainLeft_SizeChanged(object sender, SizeChangedEventArgs e) + { + if (!e.WidthChanged) + return; + PanMainLeft_Resize(e.NewSize.Width); + } + + private void PanMainLeft_Resize(double NewWidth) + { + var Delta = NewWidth - RectLeftBackground.Width; + if (Math.Abs(Delta) > 0.1d && ModAnimation.AniControlEnabled == 0) + { + if (PanMain.Opacity < 0.1d) + PanMainLeft.IsHitTestVisible = false; // 避免左边栏指向背景未能完美覆盖左边栏 + if (NewWidth > 0d) + // 宽度足够,显示 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaWidth(RectLeftBackground, NewWidth - RectLeftBackground.Width, 180, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), + ModAnimation.AaOpacity(RectLeftShadow, 1d - RectLeftShadow.Opacity, 180), + ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150) + }, "FrmMain LeftChange", true); + else + // 宽度不足,隐藏 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaWidth(RectLeftBackground, -RectLeftBackground.Width, 180, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 180), + ModAnimation.AaCode(() => PanMainLeft.IsHitTestVisible = true, 150) + }, "FrmMain LeftChange", true); + } + else + { + RectLeftBackground.Width = NewWidth; + PanMainLeft.IsHitTestVisible = true; + ModAnimation.AniStop("FrmMain LeftChange"); + } + } + + #endregion + + #region 控件拖动 + + // 在时钟中调用,使得即使鼠标在窗口外松开,也可以释放控件 + public void DragTick() + { + if (ModMain.DragControl is null) + return; + if (!(Mouse.LeftButton == MouseButtonState.Pressed)) DragStop(); + } + + // 在鼠标移动时调用,以改变 Slider 位置 + public void DragDoing() + { + if (ModMain.DragControl is null) + return; + if (Mouse.LeftButton == MouseButtonState.Pressed) + ((dynamic)ModMain.DragControl).DragDoing(); + else + DragStop(); + } + + private void PanBack_MouseMove(object sender, EventArgs e) + { + DragDoing(); + } + + public void DragStop() + { + // 存在其他线程调用的可能性,因此需要确保在 UI 线程运行 + ModBase.RunInUi(() => + { + if (ModMain.DragControl is null) + return; + var Control = ModMain.DragControl; + ModMain.DragControl = null; + ((dynamic)Control).DragStop(); // 控件会在该事件中判断 DragControl,所以得放在后面 + }); + } + + #endregion + + #region 附加按钮 + + // 更新重启 + private void BtnExtraUpdateRestart_Click(object sender, MouseButtonEventArgs e) + { + ModSecret.UpdateRestart(true); + } + + private bool BtnExtraUpdateRestart_ShowCheck() + { + return ModSecret.IsUpdateWaitingRestart; + } + + // 音乐 + private void BtnExtraMusic_Click(object sender, MouseButtonEventArgs e) + { + ModMusic.MusicControlPause(); + } + + private void BtnExtraMusic_RightClick(object sender, MouseButtonEventArgs e) + { + ModMusic.MusicControlNext(); + } + + // 任务管理 + private void BtnExtraDownload_Click(object sender, MouseButtonEventArgs e) + { + PageChange(PageType.TaskManager); + } + + private bool BtnExtraDownload_ShowCheck() + { + return ModNet.HasDownloadingTask() && !(PageCurrent == PageType.TaskManager); + } + + // 投降 + public void AprilGiveup() + { + if (ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup) + { + ModMain.Hint("=D", ModMain.HintType.Finish); + ModMain.IsAprilGiveup = true; + ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleX = 1d; + ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleY = 1d; + BtnExtraApril.ShowRefresh(); + } + } + + private void BtnExtraApril_Click(object sender, MouseButtonEventArgs e) + { + AprilGiveup(); + } + + public bool BtnExtraApril_ShowCheck() + { + return ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup && PageCurrent == PageType.Launch; + } + + // 关闭 Minecraft + private void BtnExtraShutdown_Click(object sender, MouseButtonEventArgs e) + { + try + { + if (ModLaunch.McLaunchLoaderReal is not null) + ModLaunch.McLaunchLoaderReal.Abort(); + foreach (var Watcher in ModWatcher.McWatcherList) + Watcher.Kill(); + ModMain.Hint("已关闭运行中的 Minecraft!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "强制关闭所有 Minecraft 失败", ModBase.LogLevel.Feedback); + } + } + + public bool BtnExtraShutdown_ShowCheck() + { + return ModWatcher.HasRunningMinecraft; + } + + // 游戏日志 + private void BtnExtraLog_Click(object sender, MouseButtonEventArgs e) + { + PageChange(PageType.GameLog); + } + + public bool BtnExtraLog_ShowCheck() + { + if (ModMain.FrmLogLeft is null || ModMain.FrmLogRight is null || PageCurrent == PageType.GameLog) + return false; + return ModMain.FrmLogLeft.ShownLogs.Count > 0; + } + + /// + /// 返回顶部。 + /// + public void BackToTop() + { + var RealScroll = BtnExtraBack_GetRealChild(); + if (RealScroll is not null) + RealScroll.PerformVerticalOffsetDelta(-RealScroll.VerticalOffset); + else + ModBase.Log("[UI] 无法返回顶部,未找到合适的 RealScroll", ModBase.LogLevel.Hint); + } + + private void BtnExtraBack_Click(object sender, MouseButtonEventArgs e) + { + BackToTop(); + } + + private bool BtnExtraBack_ShowCheck() + { + var RealScroll = BtnExtraBack_GetRealChild(); + return RealScroll is not null && RealScroll.Visibility == Visibility.Visible && + RealScroll.VerticalOffset > Height + (BtnExtraBack.Show ? 0 : 700); + } + + private MyScrollViewer? BtnExtraBack_GetRealChild() + { + if (PanMainRight.Child is null || !(PanMainRight.Child is MyPageRight)) + return null; + return ((MyPageRight)PanMainRight.Child).PanScroll; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index 556b746f9..e39abee43 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -186,63 +186,65 @@ Public Class FormMain 'Timer 启动 AniStart() TimerMainStart() - '加载池 RunInNewThread( - Sub() - '特殊版本提示 + Sub() + Try + '特殊版本提示 #If DEBUG Or DEBUGCI Then - If Environment.GetEnvironmentVariable("PCL_DISABLE_DEBUG_HINT") Is Nothing Then + If Environment.GetEnvironmentVariable("PCL_DISABLE_DEBUG_HINT") Is Nothing Then #If DEBUG Then - Const hint = "当前运行的 PCL 社区版为 Debug 版本。" & vbCrLf & - "该版本仅适合开发者调试运行,可能会有严重的性能下降以及各种奇怪的网络问题。" & vbCrLf & - vbCrLf & - "非开发者用户使用该版本造成的一切问题均不被社区支持,相关 issue 可能会被直接关闭。" & vbCrLf & - "除非您是开发者,否则请立即删除该版本,并下载最新稳定版使用。" + Const hint = "当前运行的 PCL 社区版为 Debug 版本。" & vbCrLf & + "该版本仅适合开发者调试运行,可能会有严重的性能下降以及各种奇怪的网络问题。" & vbCrLf & + vbCrLf & + "非开发者用户使用该版本造成的一切问题均不被社区支持,相关 issue 可能会被直接关闭。" & vbCrLf & + "除非您是开发者,否则请立即删除该版本,并下载最新稳定版使用。" #Else - Const hint = "当前运行的 PCL 社区版为 CI 自动构建版本。" & vbCrLf & - "该版本包含最新的漏洞修复、优化和新特性,但性能和稳定性较差,不适合日常使用和制作整合包。" & vbCrLf & - vbCrLf & - "除非社区开发者要求或您自己想要这么做,否则请下载最新稳定版使用。" + Const hint = "当前运行的 PCL 社区版为 CI 自动构建版本。" & vbCrLf & + "该版本包含最新的漏洞修复、优化和新特性,但性能和稳定性较差,不适合日常使用和制作整合包。" & vbCrLf & + vbCrLf & + "除非社区开发者要求或您自己想要这么做,否则请下载最新稳定版使用。" #End If - MyMsgBox($"{hint}{vbCrLf}{vbCrLf}可以添加 PCL_DISABLE_DEBUG_HINT 环境变量 (任意值) 来隐藏这个提示。", - "特殊版本提示", "我清楚我在做什么", "打开最新版下载页并退出", IsWarn:=True, - Button2Action:=Sub() - OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/latest") - EndProgram(False) - End Sub) - End If + MyMsgBox($"{hint}{vbCrLf}{vbCrLf}可以添加 PCL_DISABLE_DEBUG_HINT 环境变量 (任意值) 来隐藏这个提示。", + "特殊版本提示", "我清楚我在做什么", "打开最新版下载页并退出", IsWarn:=True, + Button2Action:=Sub() + OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/latest") + EndProgram(False) + End Sub) + End If #End If - 'EULA 提示 - If Not Setup.Get("SystemEula") Then - Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明", - Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW")) - Case 1 - Setup.Set("SystemEula", True) - Case 2 - EndProgram(False) - End Select - End If - '遥测提示 - If Setup.IsUnset("SystemTelemetry") Then - Select Case MyMsgBox("这是一项与 Steam 硬件调查类似的计划,参与调查可以帮助我们更好的进行规划和开发,且我们会不定期发布该调查的统计结果。" & vbCrLf & - "如果选择参与调查,我们将会收集以下信息:" & vbCrLf & vbCrLf & - "- 启动器版本信息与识别码" & vbCrLf & - "- Windows 系统版本与架构" & vbCrLf & - "- 已安装的物理内存大小" & vbCrLf & - "- NAT 与 IPv6 支持情况" & vbCrLf & - "- 是否使用过官方版 PCL、HMCL 或 BakaXL" & vbCrLf & vbCrLf & - "这些数据均不与你关联,我们也绝不会向第三方出售数据。" & vbCrLf & - "如果不想参与该调查,可以选择拒绝,不会影响其他功能使用。" & vbCrLf & - "你可以随时在启动器设置中调整这项设置。", "参与 PCL CE 软硬件调查", "同意", "拒绝") - Case 1 - Setup.Set("SystemTelemetry", True) - Case 2 - Setup.Set("SystemTelemetry", False) - End Select - End If + 'EULA 提示 + If Not Setup.Get("SystemEula") Then + Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明", + Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW")) + Case 1 + Setup.Set("SystemEula", True) + Case 2 + EndProgram(False) + End Select + End If + '遥测提示 + If Config.System.TelemetryConfig.IsDefault() Then + Dim selection = MyMsgBox("这是一项与 Steam 硬件调查类似的计划,参与调查可以帮助我们更好的进行规划和开发,且我们会不定期发布该调查的统计结果。" & vbCrLf & + "如果选择参与调查,我们将会收集以下信息:" & vbCrLf & vbCrLf & + "- 启动器版本信息与识别码" & vbCrLf & + "- Windows 系统版本与架构" & vbCrLf & + "- 已安装的物理内存大小" & vbCrLf & + "- NAT 与 IPv6 支持情况" & vbCrLf & + "- 是否使用过官方版 PCL、HMCL 或 BakaXL" & vbCrLf & vbCrLf & + "这些数据均不与你关联,我们也绝不会向第三方出售数据。" & vbCrLf & + "如果不想参与该调查,可以选择拒绝,不会影响其他功能使用。" & vbCrLf & + "你可以随时在启动器设置中调整这项设置。", "参与 PCL CE 软硬件调查", "同意", "拒绝") + Config.System.TelemetryConfig.SetValue(selection = 1, forceNewValue:=True) + End If + Catch ex As Exception + Log(ex, "初始弹窗提示运行失败", LogLevel.Feedback) + End Try + End Sub, "Start MsgBox", ThreadPriority.Lowest) + '加载池 + RunInNewThread( + Sub() '启动加载器池 Try - Thread.Sleep(100) DlClientListMojangLoader.Start(1) 'PCL 会同时根据这里的加载结果决定是否使用官方源进行下载 RunCountSub() ServerLoader.Start(1) @@ -251,7 +253,7 @@ Public Class FormMain Log(ex, "初始化加载池运行失败", LogLevel.Feedback) End Try GetSystemInfo() - End Sub, "Start Loader", ThreadPriority.Lowest) + End Sub, "Start Loader", ThreadPriority.BelowNormal) Log("[Start] 第三阶段加载用时:" & TimeUtils.GetTimeTick() - ApplicationStartTick & " ms") End Sub diff --git a/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs b/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs new file mode 100644 index 000000000..69fe22746 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModAnimation.cs @@ -0,0 +1,1507 @@ +using System.Collections; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils; + +namespace PCL; + +public static partial class ModAnimation +{ + private static int AniCount; + private static int AniFPSCounter; + private static long AniFPSTimer; + + /// + /// 当前的动画 FPS。 + /// + public static int AniFPS; + + /// + /// 开始动画执行。 + /// + public static void AniStart() + { + // 初始化计时器 + AniLastTick = TimeUtils.GetTimeTick(); + AniFPSTimer = AniLastTick; + AniRunning = true; // 标记动画执行开始 + + var MinFrameGap = 1000d / (Config.System.AnimationFpsLimit + 1) / 2; + + + ModBase.RunInNewThread(() => + { + try + { + ModBase.Log("[Animation] 动画线程开始"); + while (true) + { + // 两帧之间的间隔时间 + var DeltaTime = + (long)Math.Round(ModBase.MathClamp(TimeUtils.GetTimeTick() - AniLastTick, 0, 100000)); + if (DeltaTime < MinFrameGap) + { + // 限制 FPS + Thread.Sleep(1); + continue; + } + + AniLastTick = TimeUtils.GetTimeTick(); + // 记录 FPS + if (ModBase.ModeDebug) + { + if (ModBase.MathClamp(AniLastTick - AniFPSTimer, 0d, 100000d) >= 500d) + { + AniFPS = AniFPSCounter; + AniFPSCounter = 0; + AniFPSTimer = AniLastTick; + } + + AniFPSCounter += 2; + } + + // 执行动画 + ModBase.RunInUiWait(() => + { + AniCount = 0; + AniTimer((int)Math.Round(DeltaTime * AniSpeed)); + // #If DEBUG Then + // FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain + // #Else + // If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain + // #End If + if (RandomUtils.NextInt(0, 64 * (ModBase.ModeDebug ? 5 : 30)) == 0 && + ((AniFPS < 62 && AniFPS > 0) || AniCount > 4 || ModNet.NetManager.FileRemain != 0)) + ModBase.Log("[Report] FPS " + AniFPS + ", 动画 " + AniCount + ", 下载中 " + + ModNet.NetManager.FileRemain + "(" + + ModBase.GetString(ModNet.NetManager.Speed) + "/s)"); + }); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "动画帧执行失败", ModBase.LogLevel.Critical); + } + }, "Animation", ThreadPriority.AboveNormal); + } + + /// + /// 动画定时器事件。 + /// + public static void AniTimer(int DeltaTick) + { + try + { + if (DeltaTick / AniSpeed > 100d) + ModBase.Log("[Animation] 两个动画帧间隔 " + DeltaTick + " ms", ModBase.LogLevel.Developer); + var i = -1; + // 循环每个动画组 + while (i + 1 < AniGroups.Count) + { + i += 1; + // 初始化 + var Entry = AniGroups.Values.ElementAtOrDefault(i); + if (Entry.StartTick > AniLastTick) + continue; // 跳过本刻之后开始的动画 + var CanRemoveAfter = true; // 是否应该去除“之后”标记 + var ii = 0; + + // 循环每个动画 + while (ii < Entry.Data.Count) + { + var Anim = Entry.Data[ii]; + // 执行种类 + if (!Anim.IsAfter) // 之前 + { + CanRemoveAfter = false; // 取消“之后”标记 + // 增加执行时间 + Anim.TimeFinished += DeltaTick; + // 执行动画 + if (Anim.TimeFinished > 0) + { + Anim = AniRun(Anim); + AniCount += 1; + } + + // 如果当前动画已执行完毕 + if (Anim.TimeFinished >= Anim.TimeTotal) + { + // 如果是去向颜色资源的动画,设置引用 + if (Conversions.ToBoolean(Anim.TypeMain == AniType.Color && + !Operators.ConditionalCompareObjectEqual(((dynamic)Anim.Obj)[2], + "", false))) + ((dynamic)Anim.Obj)[0] + .SetResourceReference(((dynamic)Anim.Obj)[1], ((dynamic)Anim.Obj)[2]); + // 删除 + Entry.Data.RemoveAt(ii); + goto NextAni; + } + + Entry.Data[ii] = Anim; + } + else if (CanRemoveAfter) // 之后 + { + // 之后改为之前 + CanRemoveAfter = false; + Anim.IsAfter = false; + Entry.Data[ii] = Anim; + // 重新循环该动画 + goto NextAni; + } + else + { + // 不能去除该“之后”标记,结束该动画组 + break; + } + + ii += 1; + NextAni: ; + } + + // 如果当前动画组都执行完毕则删除 + if (!Entry.Data.Any()) + { + // 为了避免新添加的动画影响顺序,不能 RemoveAt(i) + // 为了允许动画在执行中添加同名动画组,不能按名字移除 + for (int Current = 0, loopTo = AniGroups.Count - 1; Current <= loopTo; Current++) + if (AniGroups.ElementAt(Current).Value.Uuid == Entry.Uuid) + { + AniGroups.Remove(AniGroups.ElementAt(Current).Key); + break; + } + + i -= 1; + } + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "动画刻执行失败", ModBase.LogLevel.Hint); + } + } + + /// + /// 执行一个动画。 + /// + /// 执行的动画对象。 + private static AniData AniRun(AniData Ani) + { + try + { + switch (Ani.TypeMain) + { + case AniType.Number: + { + var Delta = ModBase.MathPercent(0d, Conversions.ToDouble(Ani.Value), + Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent)); + if (Delta != 0d) + switch (Ani.TypeSub) + { + case AniTypeSub.X: + { + ModBase.DeltaLeft((FrameworkElement)Ani.Obj, Delta); + break; + } + case AniTypeSub.Y: + { + ModBase.DeltaTop((FrameworkElement)Ani.Obj, Delta); + break; + } + case AniTypeSub.Opacity: + { + ((dynamic)Ani.Obj).Opacity = ModBase.MathClamp( + Conversions.ToDouble(Operators.AddObject(((dynamic)Ani.Obj).Opacity, Delta)), 0d, + 1d); + break; + } + case AniTypeSub.Width: + { + var Obj = (FrameworkElement)Ani.Obj; + Obj.Width = Math.Max((double.IsNaN(Obj.Width) ? Obj.ActualWidth : Obj.Width) + Delta, + 0d); + break; + } + case AniTypeSub.Height: + { + var Obj = (FrameworkElement)Ani.Obj; + Obj.Height = + Math.Max((double.IsNaN(Obj.Height) ? Obj.ActualHeight : Obj.Height) + Delta, 0d); + break; + } + case AniTypeSub.Value: + { + ((dynamic)Ani.Obj).Value += Delta; + break; + } + case AniTypeSub.Radius: + { + ((dynamic)Ani.Obj).Radius += Delta; + break; + } + case AniTypeSub.StrokeThickness: + { + ((dynamic)Ani.Obj).StrokeThickness = + Math.Max(Operators.AddObject(((dynamic)Ani.Obj).StrokeThickness, Delta), 0); + break; + } + case AniTypeSub.BorderThickness: + { + ((dynamic)Ani.Obj).BorderThickness = + new Thickness(((Thickness)((dynamic)Ani.Obj).BorderThickness).Bottom + Delta); + break; + } + case AniTypeSub.TranslateX: + { + if (((dynamic)Ani.Obj).RenderTransform == null || + !(((dynamic)Ani.Obj).RenderTransform is TranslateTransform)) + ((dynamic)Ani.Obj).RenderTransform = new TranslateTransform(0d, 0d); + ((TranslateTransform)((dynamic)Ani.Obj).RenderTransform).X += Delta; + break; + } + case AniTypeSub.TranslateY: + { + if (((dynamic)Ani.Obj).RenderTransform == null || + !(((dynamic)Ani.Obj).RenderTransform is TranslateTransform)) + ((dynamic)Ani.Obj).RenderTransform = new TranslateTransform(0d, 0d); + ((TranslateTransform)((dynamic)Ani.Obj).RenderTransform).Y += Delta; + break; + } + case AniTypeSub.Double: + { + ((dynamic)Ani.Obj)[0].SetValue(((dynamic)Ani.Obj)[1], + Operators.AddObject(((dynamic)Ani.Obj)[0].GetValue(((dynamic)Ani.Obj)[1]), Delta)); + break; + } + case AniTypeSub.DoubleParam: + { + ((ParameterizedThreadStart)Ani.Obj)(Delta); + break; + } + case AniTypeSub.GridLengthWidth: + { + ((dynamic)Ani.Obj).Width = + new GridLength( + Conversions.ToDouble( + Math.Max(Operators.AddObject(((dynamic)Ani.Obj).Width.Value, Delta), 0)), + GridUnitType.Star); + break; + } + } + + break; + } + + case AniType.Color: + { + // 利用 Last 记录了余下的小数值 + var Delta = ModBase.MathPercent(new ModBase.MyColor(0d, 0d, 0d, 0d), (ModBase.MyColor)Ani.Value, + Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent)) + + (ModBase.MyColor)Ani.ValueLast; + var Obj = (FrameworkElement)((dynamic)Ani.Obj)[0]; + var Prop = (DependencyProperty)((dynamic)Ani.Obj)[1]; + var NewColor = new ModBase.MyColor(Obj.GetValue(Prop)) + Delta; + Obj.SetValue(Prop, Prop.PropertyType.Name == "Color" ? (Color)NewColor : (SolidColorBrush)NewColor); + Ani.ValueLast = NewColor - new ModBase.MyColor(Obj.GetValue(Prop)); + break; + } + + case AniType.Scale: + { + var Obj = (FrameworkElement)Ani.Obj; + var Delta = Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent); + Obj.Margin = new Thickness( + Obj.Margin.Left + + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Left), Delta), + Obj.Margin.Top + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Top), Delta), + Obj.Margin.Right + + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Left), Delta), + Obj.Margin.Bottom + + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Top), Delta)); + Obj.Width = Math.Max( + Obj.Width + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Width), Delta), + 0d); + Obj.Height = + Math.Max( + Obj.Height + ModBase.MathPercent(0d, Conversions.ToDouble(((dynamic)Ani.Value).Height), + Delta), 0d); + break; + } + + case AniType.TextAppear: + { + var TextCount = (int)Math.Round( + (double)(Conversions.ToBoolean(((dynamic)Ani.Value)[1]) + ? ((dynamic)Ani.Value)[0].ToString().Length + : 0) + Math.Round( + ((dynamic)Ani.Value)[0].ToString().Length * + (Conversions.ToBoolean(((dynamic)Ani.Value)[1]) ? -1 : 1) * + Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, 0d))); + var NewText = Strings.Mid(Conversions.ToString(((dynamic)Ani.Value)[0]), 1, TextCount); + // 添加乱码 + if (TextCount < ((dynamic)Ani.Value)[0].ToString().Length) + { + var NextText = Strings.Mid(Conversions.ToString(((dynamic)Ani.Value)[0]), TextCount + 1, 1); + if (Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128))) + NewText += Encoding.GetEncoding("GB18030").GetString(new[] + { + (byte)RandomUtils.NextInt(16 + 160, 87 + 160), + (byte)RandomUtils.NextInt(1 + 160, 89 + 160) + }); + else + NewText += RandomUtils.PickRandom( + @"0123456789./*-+\[]{};':/?,!@#$%^&*()_+-=qwwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM" + .ToCharArray()); + } + + // 设置文本 + if (Ani.Obj is TextBlock) + ((dynamic)Ani.Obj).Text = NewText; + else + ((dynamic)Ani.Obj).Context = NewText; + + break; + } + + case AniType.Code: + { + ((ThreadStart)Ani.Value)(); + break; + } + + case AniType.ScaleTransform: + { + var Obj = (FrameworkElement)Ani.Obj; + if (!(Obj.RenderTransform is ScaleTransform)) + { + Obj.RenderTransformOrigin = new Point(0.5d, 0.5d); + Obj.RenderTransform = new ScaleTransform(1d, 1d); + } + + var Delta = ModBase.MathPercent(0d, Conversions.ToDouble(Ani.Value), + Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent)); + ((ScaleTransform)Obj.RenderTransform).ScaleX = + Math.Max(((ScaleTransform)Obj.RenderTransform).ScaleX + Delta, 0d); + ((ScaleTransform)Obj.RenderTransform).ScaleY = + Math.Max(((ScaleTransform)Obj.RenderTransform).ScaleY + Delta, 0d); + break; + } + + case AniType.RotateTransform: + { + var Obj = (FrameworkElement)Ani.Obj; + if (!(Obj.RenderTransform is RotateTransform)) + { + Obj.RenderTransformOrigin = new Point(0.5d, 0.5d); + Obj.RenderTransform = new RotateTransform(0d); + } + + var Delta = ModBase.MathPercent(0d, Conversions.ToDouble(Ani.Value), + Ani.Ease.GetDelta(Ani.TimeFinished / (double)Ani.TimeTotal, Ani.TimePercent)); + ((RotateTransform)Obj.RenderTransform).Angle = ((RotateTransform)Obj.RenderTransform).Angle + Delta; + break; + } + } + + Ani.TimePercent = Ani.TimeFinished / (double)Ani.TimeTotal; // 修改执行百分比 + } + catch (Exception ex) + { + ModBase.Log(ex, "执行动画失败:" + Ani, ModBase.LogLevel.Hint); + } + + return Ani; + } + + #region 声明 + + /// + /// 动画速度。最大为 200。 + /// + public static double AniSpeed = 1d; + + /// + /// 动画组列表。 + /// + public static Dictionary AniGroups = new(); + + public class AniGroupEntry + { + public List Data; + public long StartTick; + public int Uuid = ModBase.GetUuid(); + } + + /// + /// 上一次记刻的时间。 + /// + private static long AniLastTick; + + /// + /// 动画模块是否正在运行。 + /// + public static bool AniRunning; + + private static int _AniControlEnabled; + private static readonly object AniControlEnabledLock = new(); + + /// + /// 控件动画执行是否开启。先 +1,再 -1。 + /// + public static int AniControlEnabled + { + get => _AniControlEnabled; + set + { + lock (AniControlEnabledLock) + { + _AniControlEnabled = value; + } + } + } + + #endregion + + #region 类与枚举 + + /// + /// 单个动画对象。 + /// + /// + public struct AniData + { + /// + /// 动画种类。 + /// + /// + public AniType TypeMain; + + /// + /// 动画副种类。 + /// + /// + public AniTypeSub TypeSub; + + /// + /// 动画总长度。 + /// + /// + public int TimeTotal; + + /// + /// 已经执行的动画长度。如果为负数则为延迟。 + /// + /// + public int TimeFinished; + + /// + /// 已经完成的百分比。 + /// + /// + public double TimePercent; + + /// + /// 是否为“以后”。 + /// + /// + public bool IsAfter; + + /// + /// 插值器类型。 + /// + /// + public AniEase Ease; + + /// + /// 动画对象。 + /// + /// + public object Obj; + + /// + /// 动画值。 + /// + /// + public object Value; + + /// + /// 上次执行时的动画值。 + /// + /// + public object ValueLast; + + public override string ToString() + { + return ModBase.GetStringFromEnum(TypeMain) + " | " + TimeFinished + "/" + TimeTotal + "(" + + Math.Round(TimePercent * 100d) + "%)" + + (Obj is null ? "" : " | " + Obj + "(" + Obj.GetType().Name + ")"); + } + } + + /// + /// 动画基础种类。 + /// + public enum AniType + { + /// + /// 单个Double的动画,包括位置、长宽、透明度等。这需要附属类型。 + /// + /// + Number, + + /// + /// 颜色属性的动画。这需要附属类型。 + /// + /// + Color, + + /// + /// 缩放控件大小。比起4个DoubleAnimation来说效率更高。 + /// + /// + Scale, + + /// + /// 文字一个个出现。 + /// + /// + TextAppear, + + /// + /// 执行代码。 + /// + /// + Code, + + /// + /// 以 WPF 方式缩放控件。 + /// + ScaleTransform, + + /// + /// 以 WPF 方式旋转控件。 + /// + RotateTransform + } + + /// + /// 动画扩展种类。 + /// + public enum AniTypeSub + { + X, + Y, + Width, + Height, + Opacity, + Value, + Radius, + BorderThickness, + StrokeThickness, + TranslateX, + TranslateY, + Double, + DoubleParam, + GridLengthWidth + } + + #endregion + + #region 种类 + + // DoubleAnimation + + /// + /// 移动X轴的动画。 + /// + /// 动画的对象。 + /// 进行移动的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaX(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.X, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 移动Y轴的动画。 + /// + /// 动画的对象。 + /// 进行移动的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaY(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Y, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变宽度的动画。 + /// + /// 动画的对象。 + /// 宽度改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaWidth(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Width, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变高度的动画。 + /// + /// 动画的对象。 + /// 高度改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaHeight(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Height, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变透明度的动画。 + /// + /// 动画的对象。 + /// 透明度改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaOpacity(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Opacity, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变对象的Value属性的动画。 + /// + /// 动画的对象。 + /// Value属性改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaValue(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Value, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变对象的Radius属性的动画。 + /// + /// 动画的对象。 + /// Radius属性改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaRadius(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.Radius, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变对象的BorderThickness属性的动画。 + /// + /// 动画的对象。 + /// BorderThickness属性改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaBorderThickness(object Obj, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.BorderThickness, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变对象的StrokeThickness属性的动画。 + /// + /// 动画的对象。 + /// StrokeThickness属性改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + public static AniData AaStrokeThickness(object Obj, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.StrokeThickness, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 改变 Width 的 GridLength 属性的动画。必须为 Star。 + /// + /// 动画的对象。 + /// GridLength.Value 改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + public static AniData AaGridLengthWidth(object Obj, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.GridLengthWidth, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + // DoubleAnimation(Obj, Prop, [Res]) + + /// + /// 改变数字属性的动画。 + /// + /// 动画的对象。 + /// 动画的依赖属性。 + /// 改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaDouble(object Obj, DependencyProperty Prop, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, TypeSub = AniTypeSub.Double, TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), Obj = new[] { Obj, Prop, "" }, Value = Value, IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 获取数字动画值。 + /// + /// 改变的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaDouble(ParameterizedThreadStart Lambda, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, TypeSub = AniTypeSub.DoubleParam, TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), Obj = Lambda, Value = Value, IsAfter = After, TimeFinished = -Delay + }; + } + + // ColorAnimation(Obj, Prop, [Res]) + + /// + /// 改变颜色属性的动画。 + /// + /// 动画的对象。 + /// 动画的依赖属性。 + /// 颜色改变的值。以RGB加减法进行计算。不用担心超额。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaColor(FrameworkElement Obj, DependencyProperty Prop, ModBase.MyColor Value, int Time = 400, + int Delay = 0, AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Color, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), + Obj = new object[] { Obj, Prop, "" }, Value = Value, IsAfter = After, TimeFinished = -Delay, + ValueLast = new ModBase.MyColor(0d, 0d, 0d, 0d) + }; + } + + /// + /// 改变颜色属性为一个资源的动画。 + /// + /// 动画的对象。 + /// 动画的依赖属性。 + /// 要将颜色改变为该资源值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaColor(FrameworkElement Obj, DependencyProperty Prop, string Res, int Time = 400, + int Delay = 0, AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.Color, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), + Obj = new object[] { Obj, Prop, Res }, + Value = new ModBase.MyColor(System.Windows.Application.Current.FindResource(Res)) - + new ModBase.MyColor(Obj.GetValue(Prop)), + IsAfter = After, TimeFinished = -Delay, ValueLast = new ModBase.MyColor(0d, 0d, 0d, 0d) + }; + } + + // Scale + + /// + /// 缩放控件的动画。 + /// + /// 动画的对象。 + /// 大小改变的百分比(如-0.6)或值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// 大小改变是否为绝对值。若为 True 则为绝对像素,若为 False 则为相对百分比。 + /// + /// + public static AniData AaScale(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false, bool Absolute = false) + { + ModBase.MyRect ChangeRect; + if (Absolute) + ChangeRect = new ModBase.MyRect(-0.5d * Value, -0.5d * Value, Value, Value); + else + ChangeRect = new ModBase.MyRect( + Conversions.ToDouble( + Operators.MultiplyObject(Operators.MultiplyObject(-0.5d, ((dynamic)Obj).ActualWidth), Value)), + Conversions.ToDouble( + Operators.MultiplyObject(Operators.MultiplyObject(-0.5d, ((dynamic)Obj).ActualHeight), Value)), + Conversions.ToDouble(Operators.MultiplyObject(((dynamic)Obj).ActualWidth, Value)), + Conversions.ToDouble(Operators.MultiplyObject(((dynamic)Obj).ActualHeight, Value))); + return new AniData + { + TypeMain = AniType.Scale, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj, + Value = ChangeRect, IsAfter = After, TimeFinished = -Delay + }; + } + + // TextAppear + + /// + /// 让一段文字一个个字出现或消失的动画。 + /// + /// 动画的对象。必须是Label或TextBlock。 + /// 是否为一个个字隐藏。默认为False(一个个字出现)。这些字必须已经存在了。 + /// 是否采用根据文本长度决定时间的方式。 + /// 动画长度(毫秒)。若TimePerText为True,这代表每个字所占据的时间。 + /// 动画延迟执行的时间(毫秒)。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaTextAppear(object Obj, bool Hide = false, bool TimePerText = true, int Time = 70, + int Delay = 0, AniEase Ease = null, bool After = false) + { + // Are we cool yet? + return new AniData + { + TypeMain = AniType.TextAppear, Ease = Ease ?? new AniEaseLinear(), + TimeTotal = TimePerText + ? Time * (Obj is TextBlock ? ((dynamic)Obj).Text : ((dynamic)Obj).Context.ToString()).ToString().Length + : Time, + Obj = Obj, + Value = new[] { Obj is TextBlock ? ((dynamic)Obj).Text : ((dynamic)Obj).Context.ToString(), Hide }, + IsAfter = After, TimeFinished = -Delay + }; + } + + // Code + + /// + /// 执行代码。 + /// + /// 一个ThreadStart。这将会在执行时在主线程调用。 + /// 代码延迟执行的时间(毫秒)。 + /// 是否等到以前的动画完成后才执行。 + /// + /// + public static AniData AaCode(ThreadStart Code, int Delay = 0, bool After = false) + { + return new AniData + { + TypeMain = AniType.Code, + TimeTotal = 1, + Value = Code, + IsAfter = After, + TimeFinished = -Delay + }; + } + + // ScaleTransform + + /// + /// 按照 WPF 方式缩放控件的动画。 + /// + /// 动画的对象。它必须已经拥有了单一的 ScaleTransform 值。 + /// 大小改变的百分比(如-0.6)。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaScaleTransform(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.ScaleTransform, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj, + Value = Value, IsAfter = After, TimeFinished = -Delay + }; + } + + // RotateTransform + + /// + /// 按照 WPF 方式旋转控件的动画。 + /// + /// 动画的对象。它必须已经拥有了单一的 ScaleTransform 值。 + /// 大小改变的百分比(如-0.6)。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + /// + /// + public static AniData AaRotateTransform(object Obj, double Value, int Time = 400, int Delay = 0, + AniEase Ease = null, bool After = false) + { + return new AniData + { + TypeMain = AniType.RotateTransform, TimeTotal = Time, Ease = Ease ?? new AniEaseLinear(), Obj = Obj, + Value = Value, IsAfter = After, TimeFinished = -Delay + }; + } + + // TranslateTransform + + /// + /// 利用 TranslateTransform 移动 X 轴的动画,这不会造成布局更新。 + /// + /// 动画的对象。 + /// 进行移动的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + public static AniData AaTranslateX(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.TranslateX, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + /// + /// 利用 TranslateTransform 移动 Y 轴的动画,这不会造成布局更新。 + /// + /// 动画的对象。 + /// 进行移动的值。 + /// 动画长度(毫秒)。 + /// 动画延迟执行的时间(毫秒)。 + /// 插值器类型。 + /// 是否等到以前的动画完成后才继续本动画。 + public static AniData AaTranslateY(object Obj, double Value, int Time = 400, int Delay = 0, AniEase Ease = null, + bool After = false) + { + return new AniData + { + TypeMain = AniType.Number, + TypeSub = AniTypeSub.TranslateY, + TimeTotal = Time, + Ease = Ease ?? new AniEaseLinear(), + Obj = Obj, + Value = Value, + IsAfter = After, + TimeFinished = -Delay + }; + } + + // 特殊 + + /// + /// 将一个StackPanel中的各个项目依次显示。 + /// + /// + public static List AaStack(StackPanel Stack, int Time = 100, int Delay = 25) + { + List AaStackRet = default; + AaStackRet = new List(); + var AniDelay = 0; + foreach (var Item in Stack.Children) + { + ((dynamic)Item).Opacity = 0; + AaStackRet.Add(AaOpacity(Item, 1d, Time, AniDelay)); + AniDelay += Delay; + } + + return AaStackRet; + } + + #endregion + + #region 缓动函数 + + // 基类 + public enum AniEasePower + { + Weak = 2, + Middle = 3, + Strong = 4, + ExtraStrong = 5 + } + + /// + /// 缓动函数基类。 + /// + public abstract class AniEase + { + /// + /// 获取函数值。 + /// + /// 时间百分比。 + public abstract double GetValue(double t); + + /// + /// 获取增量值。 + /// + /// 较大的 X。 + /// 较小的 X。 + public virtual double GetDelta(double t1, double t0) + { + return GetValue(t1) - GetValue(t0); + } + } + + /// + /// 渐入渐出组合。 + /// + public class AniEaseInout : AniEase + { + private readonly AniEase EaseIn; + private readonly double EaseInPercent; + private readonly AniEase EaseOut; + + public AniEaseInout(AniEase EaseIn, AniEase EaseOut, double EaseInPercent = 0.5d) + { + this.EaseIn = EaseIn; + this.EaseOut = EaseOut; + this.EaseInPercent = EaseInPercent; + } + + public override double GetValue(double t) + { + if (t < EaseInPercent) return EaseInPercent * EaseIn.GetValue(t / EaseInPercent); + + return (1d - EaseInPercent) * EaseOut.GetValue((t - EaseInPercent) / (1d - EaseInPercent)) + EaseInPercent; + } + } + + // Linear / 线性 + /// + /// 线性,无缓动。 + /// + public class AniEaseLinear : AniEase + { + public override double GetValue(double t) + { + return ModBase.MathClamp(t, 0d, 1d); + } + + public override double GetDelta(double t1, double t0) + { + return ModBase.MathClamp(t1, 0d, 1d) - ModBase.MathClamp(t0, 0d, 1d); + } + } + + // Fluent / 平滑 + /// + /// 平滑开始。 + /// + public class AniEaseInFluent : AniEase + { + private readonly AniEasePower p; + + public AniEaseInFluent(AniEasePower Power = AniEasePower.Middle) + { + p = Power; + } + + public override double GetValue(double t) + { + return Math.Pow(ModBase.MathClamp(t, 0d, 1d), (double)p); + } + } + + /// + /// 平滑结束。 + /// + public class AniEaseOutFluent : AniEase + { + private readonly AniEasePower p; + + public AniEaseOutFluent(AniEasePower Power = AniEasePower.Middle) + { + p = Power; + } + + public override double GetValue(double t) + { + return 1d - Math.Pow(ModBase.MathClamp(1d - t, 0d, 1d), (double)p); + } + } + + /// + /// 平滑开始与结束。 + /// + public class AniEaseInoutFluent : AniEase + { + private readonly AniEaseInout Ease; + + public AniEaseInoutFluent(AniEasePower Power = AniEasePower.Middle, double Middle = 0.5d) + { + Ease = new AniEaseInout(new AniEaseInFluent(Power), new AniEaseOutFluent(Power), Middle); + } + + public override double GetValue(double t) + { + return Ease.GetValue(t); + } + } + + /// + /// 以特定速度开始的平滑结束。 + /// + public class AniEaseOutFluentWithInitial : AniEase + { + private readonly double alpha; // (初速度 / 平均速度) – 1 + + /// 初速度,px/s + /// 总时长,s + /// 总路程,px + public AniEaseOutFluentWithInitial(double InitialPixelPerSecond, double TotalSecond, double TotalDistance) + { + var v0_norm = InitialPixelPerSecond * TotalSecond / TotalDistance; // 归一化初速度 + alpha = v0_norm - 1.0d; + if (alpha < 0d) + alpha = 0d; // 初速度小于平均速度时,退化为线性 + } + + public override double GetValue(double percent) + { + var p = ModBase.MathClamp(percent, 0d, 1d); + if (alpha == 0d) + return p; // 退化到线性 + return (alpha + 1d) * p / (1d + alpha * p); + } + } + + // Back / 回弹 + /// + /// 回弹开始。有效时间为 1/3。 + /// + public class AniEaseInBack : AniEase + { + private readonly double p; + + public AniEaseInBack(AniEasePower Power = AniEasePower.Middle) + { + p = 3d - (double)Power * 0.5d; + } + + public override double GetValue(double t) + { + t = ModBase.MathClamp(t, 0d, 1d); + return Math.Pow(t, p) * Math.Cos(1.5d * Math.PI * (1d - t)); + } + } + + /// + /// 回弹结束。有效时间为 1/3。 + /// + public class AniEaseOutBack : AniEase + { + private readonly double p; + + public AniEaseOutBack(AniEasePower Power = AniEasePower.Middle) + { + p = 3d - (double)Power * 0.5d; + } + + public override double GetValue(double t) + { + t = ModBase.MathClamp(t, 0d, 1d); + return 1d - Math.Pow(1d - t, p) * Math.Cos(1.5d * Math.PI * t); + } + } + + // Car / 平滑-回弹 + /// + /// 回弹开始,短平滑结束。 + /// + public class AniEaseInCar : AniEase + { + private readonly AniEaseInout Ease; + + public AniEaseInCar(double Middle = 0.7d, AniEasePower Power = AniEasePower.Middle) + { + Ease = new AniEaseInout(new AniEaseInBack(Power), new AniEaseOutFluent(Power), Middle); + } + + public override double GetValue(double t) + { + return Ease.GetValue(t); + } + } + + /// + /// 短平滑开始,回弹结束。 + /// + public class AniEaseOutCar : AniEase + { + private readonly AniEaseInout Ease; + + public AniEaseOutCar(double Middle = 0.3d, AniEasePower Power = AniEasePower.Middle) + { + Ease = new AniEaseInout(new AniEaseInFluent(Power), new AniEaseOutBack(Power), Middle); + } + + public override double GetValue(double t) + { + return Ease.GetValue(t); + } + } + + // Elastic / 弹簧 + /// + /// 弹簧开始。约在 60% 到达最小值。 + /// + public class AniEaseInElastic : AniEase + { + private readonly int p; // 6~9 + + public AniEaseInElastic(AniEasePower Power = AniEasePower.Middle) + { + p = (int)Power + 4; + } + + public override double GetValue(double t) + { + t = ModBase.MathClamp(t, 0d, 1d); + return Math.Pow(t, (p - 1) * 0.25d) * Math.Cos((p - 3.5d) * Math.PI * Math.Pow(1d - t, 1.5d)); + } + } + + /// + /// 弹簧结束。约在 40% 到达最大值。 + /// + public class AniEaseOutElastic : AniEase + { + private readonly int p; + + public AniEaseOutElastic(AniEasePower Power = AniEasePower.Middle) + { + p = (int)Power + 4; + } + + public override double GetValue(double t) + { + t = 1d - ModBase.MathClamp(t, 0d, 1d); + return 1d - Math.Pow(t, (p - 1) * 0.25d) * Math.Cos((p - 3.5d) * Math.PI * Math.Pow(1d - t, 1.5d)); + } + } + + #endregion + + #region 接口(开始、中断、检测) + + /// + /// 开始一个动画组。 + /// + /// 由 Aa 开头的函数初始化的 AniData 对象集合。 + /// 动画组的名称。如果重复会直接停止同名动画组。 + public static void AniStart(IList AniGroup, string Name = "", bool RefreshTime = false) + { + if (RefreshTime) + AniLastTick = TimeUtils.GetTimeTick(); // 避免处理动画时已经造成了极大的延迟,导致动画突然结束 + // 添加到正在执行的动画组 + var NewEntry = new AniGroupEntry + { Data = ModBase.GetFullList(AniGroup), StartTick = TimeUtils.GetTimeTick() }; + if (string.IsNullOrEmpty(Name)) + Name = NewEntry.Uuid.ToString(); + else + AniStop(Name); + AniGroups.Add(Name, NewEntry); + } + + /// + /// 开始一个动画组。 + /// + public static void AniStart(AniData AniGroup, string Name = "", bool RefreshTime = false) + { + AniStart(new List { AniGroup }, Name, RefreshTime); + } + + /// + /// 直接停止一个动画组。 + /// + /// 需要停止的动画组的名称。 + public static void AniStop(string Name) + { + AniGroups.Remove(Name); + } + + /// + /// 获取动画是否正在进行中。 + /// + public static bool AniIsRun(string Name) + { + return AniGroups.ContainsKey(Name); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs new file mode 100644 index 000000000..63c430236 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -0,0 +1,4196 @@ +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using System.Xaml; +using System.Xml.Linq; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.Win32; +using Newtonsoft.Json; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.Utils; +using PCL.Core.Utils.Codecs; +using PCL.Core.Utils.Hash; +using PCL.Core.Utils.OS; +using Brush = System.Windows.Media.Brush; +using Color = System.Windows.Media.Color; +using ColorConverter = System.Windows.Media.ColorConverter; +using FontFamily = System.Windows.Media.FontFamily; +using Size = System.Windows.Size; + +namespace PCL; + +public static class ModBase +{ + #region 声明 + + // 下列版本信息由更新器自动修改 + public static readonly string VersionBaseName = Basics.VersionName; + public static readonly string VersionStandardCode = Basics.Metadata.Version.StandardVersion; + public static readonly string UpstreamVersion = Basics.Metadata.Version.UpstreamVersion; + public static readonly string CommitHash = Basics.Metadata.Version.Commit; + public static readonly string CommitHashShort = Basics.Metadata.Version.CommitDigest; + public static readonly int VersionCode = Basics.VersionCode; + +#if DEBUG + public const string VersionBranchName = "Debug"; + public const string VersionBranchCode = "100"; +#elif DEBUGCI + public const string VersionBranchName = "CI"; + public const string VersionBranchCode = "50"; +#else + public const string VersionBranchName = "Publish"; + public const string VersionBranchCode = "0"; +#endif + /// + /// 主窗口句柄。 + /// + public static nint FrmHandle; + + // 龙猫味石山小记: 用最不靠谱的实现写出能跑的代码 (AppDomain.CurrentDomain.SetupInformation.ApplicationBase 获取到的是当前工作目录而不是可执行文件所在目录) + /// + /// 程序可执行文件所在目录,以“\”结尾。 + /// + public static readonly string ExePath = Conversions.ToString(Basics.ExecutableDirectory.EndsWith(@"\") + ? Basics.ExecutableDirectory + : Basics.ExecutableDirectory + @"\"); + + /// + /// 程序可执行文件完整路径。 + /// + public static readonly string ExePathWithName = Basics.ExecutablePath; + + /// + /// 程序内嵌图片文件夹路径,以“/”结尾。 + /// + public static readonly string PathImage = "pack://application:,,,/Plain Craft Launcher 2;component/Images/"; + + /// + /// 当前程序的语言。 + /// + public static string Lang = "zh_CN"; + + /// + /// 设置对象。 + /// + public static ModSetup Setup = new(); + + /// + /// 程序的打开计时。 + /// + public static long ApplicationStartTick = TimeUtils.GetTimeTick(); + + /// + /// 程序打开时的时间。 + /// + public static DateTime ApplicationOpenTime = DateTime.Now; + + /// + /// 识别码。 + /// + public static string UniqueAddress = ModSecret.SecretGetUniqueAddress(); + + /// + /// 程序是否已结束。 + /// + public static bool IsProgramEnded = false; + + /// + /// 是否为 32 位系统。 + /// + public static bool Is32BitSystem = !Environment.Is64BitOperatingSystem; + + /// + /// 是否为 ARM64 架构。 + /// + public static bool IsArm64System = RuntimeInformation.OSArchitecture == Architecture.Arm64; + + /// + /// 是否使用 GBK 编码。 + /// + public static bool IsGBKEncoding = Encoding.Default.CodePage == 936; + + /// + /// 系统盘盘符,以 \ 结尾。例如 “C:\”。 + /// + public static string OsDrive = + Environment.GetLogicalDrives().Where(p => Directory.Exists(p)).First().ToUpper().First() + @":\"; // #3799 + + /// + /// 程序的缓存文件夹路径,以 \ 结尾。 + /// + public static string PathTemp = Paths.Temp + @"\"; + + /// + /// AppData 中的 PCL 文件夹路径,以 \ 结尾。 + /// + public static string PathAppdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\PCL\"; + + /// + /// AppData 中的 PCLCE 配置文件夹路径,以 \ 结尾。 + /// + public static string PathAppdataConfig = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + + (VersionBranchName == "Debug" ? @"\.pclcedebug\" : @"\.pclce\"); + + public static string PathHelpFolder = PathTemp + @"CE\Help\"; + + #endregion + + #region 矢量图标 + + public class Logo + { + /// + /// 图标按钮,心(空心),1.1x + /// + public const string IconButtonLikeLine = + "M512 896a42.666667 42.666667 0 0 1-30.293333-12.373333l-331.52-331.946667a224.426667 224.426667 0 0 1 0-315.733333 223.573333 223.573333 0 0 1 315.733333 0L512 282.026667l46.08-46.08a223.573333 223.573333 0 0 1 315.733333 0 224.426667 224.426667 0 0 1 0 315.733333l-331.52 331.946667A42.666667 42.666667 0 0 1 512 896zM308.053333 256a136.533333 136.533333 0 0 0-97.28 40.106667 138.24 138.24 0 0 0 0 194.986666L512 792.746667l301.226667-301.653334a138.24 138.24 0 0 0 0-194.986666 141.653333 141.653333 0 0 0-194.56 0l-76.373334 76.8a42.666667 42.666667 0 0 1-60.586666 0L405.333333 296.106667A136.533333 136.533333 0 0 0 308.053333 256z"; + + /// + /// 图标按钮,心(实心),1.1x + /// + public const string IconButtonLikeFill = + "M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z m0 0"; + + /// + /// 图标按钮,垃圾桶,1.1x + /// + public const string IconButtonDelete = + "M520.192 0C408.43 0 317.44 82.87 313.563 186.734H52.736c-29.038 0-52.663 21.943-52.663 49.079s23.625 49.152 52.663 49.152h58.075v550.473c0 103.35 75.118 187.757 167.717 187.757h472.43c92.599 0 167.716-83.894 167.716-187.757V285.477h52.59c29.038 0 52.59-21.943 52.663-49.08-0.073-27.135-23.625-49.151-52.663-49.151H726.235C723.237 83.017 631.955 0 520.192 0zM404.846 177.957c3.803-50.03 50.176-89.015 107.447-89.015 57.197 0 103.57 38.985 106.788 89.015H404.92zM284.379 933.669c-33.353 0-69.997-39.351-69.997-95.525v-549.01H833.39v549.522c0 56.247-36.645 95.525-69.998 95.525H284.379v-0.512z M357.23 800.695a48.274 48.274 0 0 0 47.616-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.69 49.006V751.69c0 27.282 20.846 49.006 47.617 49.006z m166.62 0a48.274 48.274 0 0 0 47.688-49.006V471.7a48.274 48.274 0 0 0-47.689-49.08 48.274 48.274 0 0 0-47.543 49.006V751.69c0 27.282 21.431 49.006 47.543 49.006z m142.92 0a48.274 48.274 0 0 0 47.543-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.616 49.006V751.69c0 27.282 20.773 49.006 47.543 49.006z"; + + /// + /// 图标按钮,禁止,1x + /// + public const string IconButtonStop = + "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z"; + + /// + /// 图标按钮,勾选,1x + /// + public const string IconButtonCheck = + "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z"; + + /// + /// 图标按钮,笔,1x + /// + public const string IconButtonEdit = + "M732.64 64.32C688.576 21.216 613.696 21.216 569.6 64.32L120.128 499.52c-17.6 12.896-26.432 30.144-30.848 51.68L32 870.048c0 25.856 8.8 56 26.432 73.248 17.632 17.216 17.632 48.704 88.64 48.704h13.248l326.08-56c22.016-4.32 39.68-12.928 52.864-30.176l449.472-435.2c22.048-21.536 35.264-47.36 35.264-77.536 0-30.176-13.216-56-35.264-77.568l-256.096-251.2zM139.712 903.776l56-326.912 311.04-295.136 267.104 269.44-310.976 295.168-323.168 57.44zM844.576 467.84l-273.984-260.672 61.856-59.84c8.832-8.512 26.528-8.512 39.776 0l234.24 226.496c4.384 4.288 8.832 12.8 8.832 17.088s-4.416 8.544-8.864 12.8l-61.856 64.128z"; + + /// + /// 图标按钮,齿轮,1.1x + /// + public const string IconButtonSetup = + "M651.946667 1001.813333c-22.186667 0-42.666667-10.24-61.44-27.306666-23.893333-23.893333-49.493333-35.84-75.093334-35.84-29.013333 0-56.32 11.946667-73.386666 30.72v3.413333c-17.066667 17.066667-42.666667 27.306667-66.56 27.306667h-6.826667c-6.826667 0-11.946667-1.706667-15.36-1.706667l-6.826667-1.706667c-64.853333-20.48-121.173333-54.613333-168.96-98.986666-29.013333-23.893333-37.546667-63.146667-25.6-95.573334 8.533333-23.893333 5.12-51.2-10.24-75.093333-15.36-27.306667-34.133333-40.96-59.733333-47.786667h-1.706667l-5.12-1.706666c-35.84-8.533333-61.44-34.133333-66.56-69.973334C1.706667 575.146667 0 537.6 0 512c0-32.426667 3.413333-63.146667 8.533333-93.866667v-6.826666l3.413334-8.533334c10.24-23.893333 23.893333-40.96 44.373333-51.2 5.12-3.413333 11.946667-6.826667 20.48-8.533333 27.306667-8.533333 51.2-25.6 63.146667-44.373333 13.653333-23.893333 17.066667-52.906667 10.24-81.92-11.946667-34.133333 0-71.68 30.72-93.866667 44.373333-37.546667 97.28-68.266667 158.72-93.866667l3.413333-1.706666c44.373333-13.653333 75.093333 3.413333 92.16 20.48 23.893333 23.893333 49.493333 35.84 75.093333 35.84 30.72 0 56.32-10.24 71.68-30.72l3.413334-3.413334c27.306667-27.306667 63.146667-35.84 93.866666-22.186666 63.146667 22.186667 117.76 54.613333 165.546667 97.28 29.013333 23.893333 37.546667 63.146667 25.6 95.573333-8.533333 23.893333-5.12 51.2 10.24 75.093333 15.36 27.306667 34.133333 40.96 59.733333 47.786667h1.706667l5.12 1.706667c35.84 8.533333 61.44 34.133333 66.56 71.68 6.826667 30.72 10.24 63.146667 11.946667 93.866666v3.413334c0 32.426667-3.413333 63.146667-8.533334 93.866666v6.826667l-3.413333 8.533333c-10.24 23.893333-23.893333 40.96-44.373333 51.2-5.12 3.413333-11.946667 6.826667-20.48 8.533334-27.306667 8.533333-51.2 25.6-63.146667 46.08-13.653333 23.893333-17.066667 52.906667-10.24 81.92 11.946667 35.84-1.706667 75.093333-30.72 95.573333-44.373333 35.84-95.573333 66.56-157.013333 92.16-15.36 3.413333-27.306667 3.413333-35.84 3.413333z m3.413333-83.626666z m1.706667 0zM517.12 853.333333c47.786667 0 93.866667 20.48 134.826667 59.733334 1.706667 1.706667 3.413333 1.706667 3.413333 3.413333 52.906667-22.186667 97.28-49.493333 136.533333-80.213333l1.706667-1.706667v-3.413333c-13.653333-52.906667-8.533333-104.106667 17.066667-148.48 23.893333-39.253333 64.853333-69.973333 114.346666-85.333334 1.706667 0 3.413333-1.706667 6.826667-6.826666 5.12-25.6 8.533333-51.2 8.533333-78.506667-1.706667-29.013333-3.413333-56.32-10.24-81.92v-5.12h-1.706666c-51.2-11.946667-90.453333-39.253333-119.466667-87.04-27.306667-44.373333-34.133333-100.693333-17.066667-148.48l-1.706666-1.706667h-3.413334c-39.253333-35.84-85.333333-63.146667-136.533333-80.213333H648.533333s-1.706667 1.706667-3.413333 1.706667c-32.426667 39.253333-80.213333 59.733333-136.533333 59.733333-47.786667 0-93.866667-20.48-134.826667-59.733333l-1.706667-1.706667h-1.706666c-54.613333 22.186667-98.986667 49.493333-136.533334 80.213333l-1.706666 1.706667v3.413333c13.653333 52.906667 8.533333 104.106667-17.066667 148.48-23.893333 39.253333-64.853333 69.973333-114.346667 85.333334-1.706667 0-3.413333 1.706667-6.826666 6.826666-6.826667 25.6-8.533333 51.2-8.533334 78.506667 0 30.72 3.413333 58.026667 6.826667 76.8l1.706667 5.12h1.706666c51.2 11.946667 90.453333 39.253333 119.466667 87.04 27.306667 44.373333 34.133333 100.693333 17.066667 148.48l1.706666 1.706667 1.706667 1.706666c37.546667 35.84 83.626667 63.146667 134.826667 80.213334 1.706667 0 3.413333 0 3.413333 1.706666h1.706667s1.706667 0 5.12-1.706666c34.133333-37.546667 81.92-59.733333 136.533333-59.733334z m-6.826667-146.773333c-110.933333 0-199.68-85.333333-199.68-196.266667 0-109.226667 87.04-196.266667 199.68-196.266666s199.68 85.333333 199.68 196.266666c-1.706667 109.226667-88.746667 196.266667-199.68 196.266667z m0-307.2c-63.146667 0-114.346667 49.493333-114.346666 110.933333 0 63.146667 49.493333 110.933333 114.346666 110.933334 30.72 0 59.733333-11.946667 80.213334-32.426667 20.48-20.48 32.426667-49.493333 32.426666-78.506667 0-63.146667-49.493333-110.933333-112.64-110.933333z"; + + /// + /// 图标按钮,重置,0.9x + /// + public const string IconButtonReset = + "M667.6817627 313.65283203l-45.28564454 55.76660156L858.06933594 391.27124023 787.61950684 165.93066406l-56.01379395 69.01611328A354.47387695 354.47387695 0 0 0 520.89892578 165.93066406C324.87536621 165.93066406 165.93066406 324.43041992 165.93066406 519.91015625c0 195.52917481 158.94470215 353.97949219 354.96826172 353.97949219a355.06713867 355.06713867 0 0 0 331.73217774-227.66418458 50.52612305 50.52612305 0 0 0-29.21813966-65.25878905 50.77331543 50.77331543 0 0 0-65.50598144 29.16870117A253.61938477 253.61938477 0 0 1 520.94836426 772.78796387c-140.05920411 0-253.61938477-113.21411133-253.61938477-252.87780762 0-139.61425781 113.56018067-252.82836914 253.61938477-252.82836914 53.59130859 0 104.46350098 16.61132813 146.73339843 46.57104492"; + + /// + /// 图标按钮,刷新,0.85x + /// + public const string IconButtonRefresh = + "M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"; + + /// + /// 图标按钮,软盘,1x + /// + public const string IconButtonSave = + "M819.392 0L1024 202.752v652.16a168.96 168.96 0 0 1-168.832 168.768h-104.192a47.296 47.296 0 0 1-10.752 0H283.776a47.232 47.232 0 0 1-10.752 0H168.832A168.96 168.96 0 0 1 0 854.912V168.768A168.96 168.96 0 0 1 168.832 0h650.56z m110.208 854.912V242.112l-149.12-147.776H168.896c-41.088 0-74.432 33.408-74.432 74.432v686.144c0 41.024 33.344 74.432 74.432 74.432h62.4v-190.528c0-33.408 27.136-60.544 60.544-60.544h440.448c33.408 0 60.544 27.136 60.544 60.544v190.528h62.4c41.088 0 74.432-33.408 74.432-74.432z m-604.032 74.432h372.864v-156.736H325.568v156.736z m403.52-596.48a47.168 47.168 0 1 1 0 94.336H287.872a47.168 47.168 0 1 1 0-94.336h441.216z m0-153.728a47.168 47.168 0 1 1 0 94.4H287.872a47.168 47.168 0 1 1 0-94.4h441.216z"; + + /// + /// 图标按钮,信息,1.05x + /// + public const string IconButtonInfo = + "M512 917.333333c223.861333 0 405.333333-181.472 405.333333-405.333333S735.861333 106.666667 512 106.666667 106.666667 288.138667 106.666667 512s181.472 405.333333 405.333333 405.333333z m0 106.666667C229.226667 1024 0 794.773333 0 512S229.226667 0 512 0s512 229.226667 512 512-229.226667 512-512 512z m-32-597.333333h64a21.333333 21.333333 0 0 1 21.333333 21.333333v320a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333V448a21.333333 21.333333 0 0 1 21.333333-21.333333z m0-192h64a21.333333 21.333333 0 0 1 21.333333 21.333333v64a21.333333 21.333333 0 0 1-21.333333 21.333333h-64a21.333333 21.333333 0 0 1-21.333333-21.333333v-64a21.333333 21.333333 0 0 1 21.333333-21.333333z"; + + /// + /// 图标按钮,列表,1x + /// + public const string IconButtonList = + "M384 128h640v128H384zM160 192m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 448h640v128H384zM160 512m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0ZM384 768h640v128H384zM160 832m-96 0a96 96 0 1 0 192 0 96 96 0 1 0-192 0Z"; + + /// + /// 图标按钮,文件夹,1.15x + /// + public const string IconButtonOpen = + "M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z"; + + /// + /// 图标按钮,名片,1.1x + /// + public const string IconButtonCard = + "M834.5 684.1c-31.2-70.4-98.9-120.9-179.1-127.3 63.5-8.5 112.6-63 112.6-128.8 0-71.8-58.2-130-130-130s-130 58.2-130 130c0 65.9 49 120.3 112.6 128.8-80.2 6.4-148 57-179.1 127.3-8.7 19.7 6 42 27.6 42 12.1 0 22.7-7.5 27.7-18.5 24.3-53.9 78.5-91.5 141.3-91.5s117 37.6 141.3 91.5c5 11.1 15.6 18.5 27.7 18.5 21.4 0 36.1-22.3 27.4-42zM567.9 427.9c0-38.6 31.4-70 70-70s70 31.4 70 70-31.4 70-70 70-70-31.4-70-70zM460.3 347.9H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h243.3c16.6 0 30-13.4 30-30 0.1-16.5-13.4-30-29.9-30zM367.4 459.6H216.9c-16.6 0-30 13.4-30 30s13.4 30 30 30h150.4c16.6 0 30-13.4 30-30 0.1-16.6-13.4-30-29.9-30zM297.4 571.2H217c-16.6 0-30 13.4-30 30s13.4 30 30 30h80.4c16.6 0 30-13.4 30-30 0-16.5-13.5-30-30-30zM900 236v552H124V236h776m0-60H124c-33.1 0-60 26.9-60 60v552c0 33.1 26.9 60 60 60h776c33.1 0 60-26.9 60-60V236c0-33.1-26.9-60-60-60z"; + + /// + /// 图标按钮,×,0.85x + /// + public const string IconButtonCross = + "F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z"; + + /// + /// 图标按钮,验证,1.1x + /// + public const string IconButtonAuth = + "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z"; + + /// + /// 图标按钮,第三方 + /// + public const string IconButtonThirdparty = + "M865.004 167.069c-10.794-9.687-24.91-15.085-39.579-15.085-1.383 0-2.629 0-4.013 0.139-0.831 0.139-10.102 0.692-24.771 0.692-24.218 0-71.408-1.522-116.107-12.178-57.708-13.7-124.411-77.083-143.785-89.675-9.687-6.227-21.034-9.41-32.244-9.41-11.21 0-22.42 3.182-32.244 9.41-2.353 1.522-72.1 73.484-140.324 89.675-44.699 10.655-92.72 12.178-116.938 12.178-14.53 0-23.941-0.554-24.771-0.692-1.246-0.139-2.629-0.139-3.875-0.139-14.67 0-28.924 5.396-39.717 15.085-11.763 10.655-18.405 25.325-18.405 40.825v140.048c0 517.846 351.089 584.411 366.034 587.040 3.46 0.554 6.782 0.831 10.241 0.831 3.46 0 6.918-0.276 10.241-0.831 14.946-2.629 368.663-69.33 368.663-587.040v-139.911c0.139-15.5-6.642-30.446-18.405-40.962v0zM825.425 348.080c0 476.883-320.783 531.961-320.783 531.961s-318.291-55.078-318.291-531.961v-140.048c0 0 10.933 0.831 28.785 0.831 30.446 0 81.648-2.214 130.777-13.839 80.403-19.098 158.731-97.564 158.731-97.564s81.787 78.466 162.19 97.564c49.129 11.625 99.501 13.839 129.946 13.839 17.714 0 28.785-0.831 28.785-0.831l-0.139 140.048zM463.405 491.173z M349.925 603.958l66.841-15.085c10.102 54.663 40.962 81.925 92.72 81.925 57.43-1.383 87.045-29.476 88.429-84.14 0-50.373-35.289-75.421-105.728-75.421-17.299 0-30.998 0-40.962 0v-51.757c10.102 0 20.757 0 32.382 0 66.149 0 99.916-25.187 101.3-75.421-1.383-45.945-26.571-69.747-75.421-71.132-48.85 0-77.635 26.571-86.215 79.85l-64.766-15.085c18.683-76.252 70.438-114.308 155.27-114.308 87.738 2.906 134.373 40.962 140.187 114.308-1.383 53.279-30.998 87.738-88.429 103.514 63.244 13.008 97.009 49.542 101.3 110.019-4.29 81.925-56.878 124.411-157.486 127.316-87.461 1.246-140.739-36.811-159.422-114.585z"; + + /// + /// 图标按钮,用户,0.95x + /// + public const string IconButtonUser = + "M660.338 528.065c63.61-46.825 105.131-121.964 105.131-206.83 0-141.7-115.29-256.987-256.997-256.987-141.706 0-256.998 115.288-256.998 256.987 0 85.901 42.52 161.887 107.456 208.562-152.1 59.92-260.185 207.961-260.185 381.077 0 21.276 17.253 38.53 38.53 38.53 21.278 0 38.53-17.254 38.53-38.53 0-183.426 149.232-332.671 332.667-332.671 1.589 0 3.113-0.207 4.694-0.244 0.8 0.056 1.553 0.244 2.362 0.244 183.434 0 332.664 149.245 332.664 332.671 0 21.276 17.255 38.53 38.533 38.53 21.277 0 38.53-17.254 38.53-38.53 0-174.885-110.354-324.13-264.917-382.809z m-331.803-206.83c0-99.22 80.72-179.927 179.935-179.927s179.937 80.708 179.937 179.927c0 99.203-80.721 179.91-179.937 179.91s-179.935-80.708-179.935-179.91z"; + + /// + /// 图标按钮,盾牌,1x + /// + public const string IconButtonShield = + "M511.488256 95.184408c35.310345 22.516742 95.184408 55.78011 167.34033 84.437781 75.738131 29.681159 148.405797 40.93953 191.392304 45.033483v353.615193c0 73.691154-50.662669 164.781609-136.123938 244.101949C649.65917 901.181409 558.568716 942.12094 512 942.12094c-46.568716 0-137.65917-40.93953-222.096952-119.748126C204.441779 742.54073 153.77911 651.450275 153.77911 577.247376v-353.103448c42.474763-4.093953 116.165917-15.352324 191.904048-45.545227 75.226387-30.192904 133.565217-63.456272 165.805098-83.414293M512 0c-4.093953 0-8.187906 1.535232-11.258371 3.582209l-14.84058 10.234882c-1.023488 0.511744-67.550225 47.592204-170.410794 88.531735-100.813593 39.916042-198.556722 41.963018-199.58021 41.963018l-25.075462 0.511744c-10.746627 0.511744-18.934533 8.187906-18.934533 18.422789v414.000999c0 216.97951 286.064968 446.24088 440.09995 446.24088s440.09995-229.261369 440.09995-445.729136V163.758121c0-10.234883-8.69965-18.422789-18.934533-18.422789l-24.563718-0.511744c-1.023488 0-98.766617-2.046977-199.58021-41.963018-103.372314-40.93953-170.410795-88.01999-170.922538-88.531734L523.258371 3.582209c-3.070465-2.558721-7.164418-3.582209-11.258371-3.582209z M743.308346 410.930535l-260.477761 260.477761c-15.864068 15.864068-41.963018 15.864068-57.827087 0l-144.823588-144.823588c-15.864068-15.864068-15.864068-41.963018 0-57.827087 8.187906-8.187906 18.422789-11.770115 29.169415-11.770115 10.234883 0 20.981509 4.093953 29.169416 11.770115l115.654173 115.654173L685.993003 352.591704c15.864068-15.864068 41.963018-15.864068 57.827087 0 15.352324 16.375812 15.352324 42.474763-0.511744 58.338831z"; + + /// + /// 图标按钮,离线,0.85x + /// + public const string IconButtonOffline = + "M533.293176 788.841412a60.235294 60.235294 0 1 1 85.202824 85.202823l-42.616471 42.586353c-129.355294 129.385412-339.124706 129.385412-468.510117 0-129.385412-129.385412-129.385412-339.124706 0-468.510117l42.586353-42.616471a60.235294 60.235294 0 1 1 85.202823 85.202824l-42.61647 42.586352a210.823529 210.823529 0 1 0 298.164706 298.164706l42.586352-42.61647z m255.548236-255.548236l42.61647-42.586352a210.823529 210.823529 0 1 0-298.164706-298.164706l-42.586352 42.61647a60.235294 60.235294 0 1 1-85.202824-85.202823l42.616471-42.586353c129.355294-129.385412 339.124706-129.385412 468.510117 0 129.385412 129.385412 129.385412 339.124706 0 468.510117l-42.586353 42.616471a60.235294 60.235294 0 1 1-85.202823-85.202824zM192.542118 192.542118a60.235294 60.235294 0 0 1 85.202823 0l553.712941 553.712941a60.235294 60.235294 0 0 1-85.202823 85.202823L192.542118 277.744941a60.235294 60.235294 0 0 1 0-85.202823z"; + + /// + /// 图标,服务端,1x + /// + public const string IconButtonServer = + "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"; + + /// + /// 图标按钮,复制 + /// + public const string IconButtonCopy = + "M394.666667 106.666667h448a74.666667 74.666667 0 0 1 74.666666 74.666666v448a74.666667 74.666667 0 0 1-74.666666 74.666667H394.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V181.333333a74.666667 74.666667 0 0 1 74.666667-74.666666z m0 64a10.666667 10.666667 0 0 0-10.666667 10.666666v448a10.666667 10.666667 0 0 0 10.666667 10.666667h448a10.666667 10.666667 0 0 0 10.666666-10.666667V181.333333a10.666667 10.666667 0 0 0-10.666666-10.666666H394.666667z m245.333333 597.333333a32 32 0 0 1 64 0v74.666667a74.666667 74.666667 0 0 1-74.666667 74.666666H181.333333a74.666667 74.666667 0 0 1-74.666666-74.666666V394.666667a74.666667 74.666667 0 0 1 74.666666-74.666667h74.666667a32 32 0 0 1 0 64h-74.666667a10.666667 10.666667 0 0 0-10.666666 10.666667v448a10.666667 10.666667 0 0 0 10.666666 10.666666h448a10.666667 10.666667 0 0 0 10.666667-10.666666v-74.666667z"; + + /// + /// 图标按钮,外链 + /// + public const string IconButtonlink = + "M433.230769 74.830769a43.323077 43.323077 0 0 1 0 86.646154l-236.307692 0.157539a35.446154 35.446154 0 0 0-35.446154 35.446153v630.153847a35.446154 35.446154 0 0 0 35.446154 35.446153h630.153846a35.446154 35.446154 0 0 0 35.446154-35.446153V590.769231a43.323077 43.323077 0 1 1 86.646154 0v236.425846a122.092308 122.092308 0 0 1-122.092308 122.092308H196.923077a122.092308 122.092308 0 0 1-122.092308-122.092308v-630.153846a122.092308 122.092308 0 0 1 122.092308-122.092308z m452.923077 0a63.015385 63.015385 0 0 1 63.015385 63.015385V354.461538a43.323077 43.323077 0 0 1-43.323077 43.323077l-4.726154-0.236307A43.323077 43.323077 0 0 1 862.523077 354.461538l-0.039385-131.702153-287.074461 287.15323-90.072616 90.072616a43.323077 43.323077 0 1 1-61.243077-61.243077l90.033231-90.072616 287.113846-287.192615H669.538462a43.323077 43.323077 0 0 1-43.08677-38.596923L626.215385 118.153846A43.323077 43.323077 0 0 1 669.538462 74.830769z"; + + /// + /// 图标,音符,1x + /// + public const string IconMusic = + "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"; + + /// + /// 图标,播放,0.8x + /// + public const string IconPlay = + "M803.904 463.936a55.168 55.168 0 0 1 0 96.128l-463.616 264.448C302.848 845.888 256 819.136 256 776.448V247.616c0-42.752 46.848-69.44 84.288-48.064l463.616 264.384z"; + + /// + /// 图标,创建,0.9x + /// + public const string IconButtonCreate = + "F1 M 4 2 C 2.35499 2 1 3.35499 1 5 v 13 c 0 1.64501 1.35499 3 3 3 h 16 c 1.64501 0 3 -1.35499 3 -3 V 8 C 23 6.35499 21.645 5 20 5 h -7.90039 a 1.0001 1.0001 0 0 0 -0.0098 0 C 11.7487 5.00334 11.4337 4.83568 11.2461 4.55078 a 1.0001 1.0001 0 0 0 -0.0078 -0.00977 L 10.4355 3.34961 C 9.88132 2.50803 8.93736 2.00017 7.92969 2 Z m 0 2 h 3.92969 c 0.337496 5.56e-05 0.650315 0.167354 0.835938 0.449219 a 1.0001 1.0001 0 0 0 0.00586 0.00977 l 0.802734 1.19141 c 0.000794 0.00121 0.00311 0.0007486 0.00391 0.00195 C 10.1385 6.50064 11.0926 7.00997 12.1094 7 H 20 c 0.564129 0 1 0.435871 1 1 v 10 c 0 0.564129 -0.435871 1 -1 1 H 4 C 3.43587 19 3 18.5641 3 18 V 5 C 3 4.43587 3.43587 4 4 4 Z m 5 8 a 1 1 0 0 0 -1 1 a 1 1 0 0 0 1 1 h 6 a 1 1 0 0 0 1 -1 a 1 1 0 0 0 -1 -1 z m 3 -3 a 1 1 0 0 0 -1 1 v 6 a 1 1 0 0 0 1 1 a 1 1 0 0 0 1 -1 V 10 A 1 1 0 0 0 12 9 Z"; + + /// + /// 图标,分享,1x + /// + public const string IconButtonShare = + "F1 M 14.9062 5.64648 L 8.08594 9.62695 A 1 1 0 0 0 7.72656 10.9941 A 1 1 0 0 0 9.09375 11.3535 L 15.9141 7.37305 A 1 1 0 0 0 16.2734 6.00586 A 1 1 0 0 0 14.9062 5.64648 Z m -5.8125 7 a 1 1 0 0 0 -1.36719 0.359375 a 1 1 0 0 0 0.359375 1.36719 l 6.83008 3.98047 a 1 1 0 0 0 1.36719 -0.359375 a 1 1 0 0 0 -0.359375 -1.36719 z M 18 15 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 c 0 -2.19729 -1.80271 -4 -4 -4 z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 c -1.11641 0 -2 -0.883586 -2 -2 c 0 -1.11641 0.883586 -2 2 -2 z M 6 8 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 C 10 9.80271 8.19729 8 6 8 Z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 C 4.88359 14 4 13.1164 4 12 C 4 10.8836 4.88359 10 6 10 Z M 18 1 c -2.19729 0 -4 1.80271 -4 4 c 0 2.19729 1.80271 4 4 4 c 2.19729 0 4 -1.80271 4 -4 c 0 -2.19729 -1.80271 -4 -4 -4 z m 0 2 c 1.11641 0 2 0.883586 2 2 c 0 1.11641 -0.883586 2 -2 2 c -1.11641 0 -2 -0.883586 -2 -2 c 0 -1.11641 0.883586 -2 2 -2 z"; + + /// + /// 图标,添加,1x + /// + public const string IconButtonAdd = + "F1 m 12 7 a 1 1 0 0 0 -1 1 v 8 a 1 1 0 0 0 1 1 a 1 1 0 0 0 1 -1 V 8 A 1 1 0 0 0 12 7 Z m -4 4 a 1 1 0 0 0 -1 1 a 1 1 0 0 0 1 1 h 8 a 1 1 0 0 0 1 -1 a 1 1 0 0 0 -1 -1 z M 12 1 C 5.93671 1 1 5.93671 1 12 C 1 18.0633 5.93671 23 12 23 C 18.0633 23 23 18.0633 23 12 C 23 5.93671 18.0633 1 12 1 Z m 0 2 c 4.98241 0 9 4.01759 9 9 c 0 4.98241 -4.01759 9 -9 9 C 7.01759 21 3 16.9824 3 12 C 3 7.01759 7.01759 3 12 3 Z"; + + /// + /// 图标,开始游戏,1x + /// + public const string IconPlayGame = + "M213.333333 65.386667a85.333333 85.333333 0 0 1 43.904 12.16L859.370667 438.826667a85.333333 85.333333 0 0 1 0 146.346666L257.237333 946.453333A85.333333 85.333333 0 0 1 128 873.28V150.72a85.333333 85.333333 0 0 1 85.333333-85.333333z m0 64a21.333333 21.333333 0 0 0-21.184 18.837333L192 150.72v722.56a21.333333 21.333333 0 0 0 30.101333 19.456l2.197334-1.152L826.453333 530.282667a21.333333 21.333333 0 0 0 2.048-35.178667l-2.048-1.386667L224.298667 132.416A21.333333 21.333333 0 0 0 213.333333 129.386667z"; + + public const string IconButtonEnable = + "M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z"; + + public const string IconButtonDisable = + "M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z"; + } + + #endregion + + #region 自定义类 + + /// + /// 支持小数与常见类型隐式转换的颜色。 + /// + public class MyColor + { + public double A = 255d; + public double B; + public double G; + public double R; + + // 构造函数 + public MyColor() + { + } + + public MyColor(Color col) + { + A = col.A; + R = col.R; + G = col.G; + B = col.B; + } + + public MyColor(string HexString) + { + var StringColor = (Color)ColorConverter.ConvertFromString(HexString); + A = StringColor.A; + R = StringColor.R; + G = StringColor.G; + B = StringColor.B; + } + + public MyColor(double newA, MyColor col) + { + A = newA; + R = col.R; + G = col.G; + B = col.B; + } + + public MyColor(double newR, double newG, double newB) + { + A = 255d; + R = newR; + G = newG; + B = newB; + } + + public MyColor(double newA, double newR, double newG, double newB) + { + A = newA; + R = newR; + G = newG; + B = newB; + } + + public MyColor(Brush brush) + { + var Color = ((SolidColorBrush)brush).Color; + A = Color.A; + R = Color.R; + G = Color.G; + B = Color.B; + } + + public MyColor(SolidColorBrush brush) + { + var Color = brush.Color; + A = Color.A; + R = Color.R; + G = Color.G; + B = Color.B; + } + + public MyColor(object obj) + { + if (obj is null) + { + A = 255d; + R = 255d; + G = 255d; + B = 255d; + } + else if (obj is SolidColorBrush) + { + // 避免反复获取 Color 对象造成性能下降 + var Color = ((SolidColorBrush)obj).Color; + A = Color.A; + R = Color.R; + G = Color.G; + B = Color.B; + } + else + { + A = Conversions.ToDouble(((dynamic)obj).A); + R = Conversions.ToDouble(((dynamic)obj).R); + G = Conversions.ToDouble(((dynamic)obj).G); + B = Conversions.ToDouble(((dynamic)obj).B); + } + } + + // 类型转换 + public static implicit operator MyColor(string str) + { + return new MyColor(str); + } + + public static implicit operator MyColor(Color col) + { + return new MyColor(col); + } + + public static implicit operator Color(MyColor conv) + { + return Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), MathByte(conv.B)); + } + + public static implicit operator System.Drawing.Color(MyColor conv) + { + return System.Drawing.Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), + MathByte(conv.B)); + } + + public static implicit operator MyColor(SolidColorBrush bru) + { + return new MyColor(bru.Color); + } + + public static implicit operator SolidColorBrush(MyColor conv) + { + return new SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), + MathByte(conv.B))); + } + + public static implicit operator MyColor(Brush bru) + { + return new MyColor(bru); + } + + public static implicit operator Brush(MyColor conv) + { + return new SolidColorBrush(Color.FromArgb(MathByte(conv.A), MathByte(conv.R), MathByte(conv.G), + MathByte(conv.B))); + } + + // 颜色运算 + public static MyColor operator +(MyColor a, MyColor b) + { + return new MyColor { A = a.A + b.A, B = a.B + b.B, G = a.G + b.G, R = a.R + b.R }; + } + + public static MyColor operator -(MyColor a, MyColor b) + { + return new MyColor { A = a.A - b.A, B = a.B - b.B, G = a.G - b.G, R = a.R - b.R }; + } + + public static MyColor operator *(MyColor a, double b) + { + return new MyColor { A = a.A * b, B = a.B * b, G = a.G * b, R = a.R * b }; + } + + public static MyColor operator /(MyColor a, double b) + { + return new MyColor { A = a.A / b, B = a.B / b, G = a.G / b, R = a.R / b }; + } + + public static bool operator ==(MyColor a, MyColor b) + { + if (a == null && b == null) + return true; + if (a == null || b == null) + return false; + return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; + } + + public static bool operator !=(MyColor a, MyColor b) + { + if (a == null && b == null) + return false; + if (a == null || b == null) + return true; + return !(a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B); + } + + // HSL + public double Hue(double v1, double v2, double vH) + { + if (vH < 0d) + vH += 1d; + if (vH > 1d) + vH -= 1d; + if (vH < 0.16667d) + return v1 + (v2 - v1) * 6d * vH; + if (vH < 0.5d) + return v2; + if (vH < 0.66667d) + return v1 + (v2 - v1) * (4d - vH * 6d); + return v1; + } + + public MyColor FromHSL(double sH, double sS, double sL) + { + if (sS == 0d) + { + R = sL * 2.55d; + G = R; + B = R; + } + else + { + var H = sH / 360d; + var S = sS / 100d; + var L = sL / 100d; + S = L < 0.5d ? S * L + L : S * (1.0d - L) + L; + L = 2d * L - S; + R = 255d * Hue(L, S, H + 1d / 3d); + G = 255d * Hue(L, S, H); + B = 255d * Hue(L, S, H - 1d / 3d); + } + + A = 255d; + return this; + } + + public MyColor FromHSL2(double sH, double sS, double sL) + { + if (sS == 0d) + { + R = sL * 2.55d; + G = R; + B = R; + } + else + { + // 初始化 + sH = (sH + 3600000d) % 360d; + var cent = new[] + { + +0.1d, -0.06d, -0.3d, -0.19d, -0.15d, -0.24d, -0.32d, -0.09d, +0.18d, +0.05d, -0.12d, -0.02d, +0.1d, + -0.06d + }; // 0, 30, 60 + // 90, 120, 150 + // 180, 210, 240 + // 270, 300, 330 + // 最后两位与前两位一致,加是变亮,减是变暗 + // 计算色调对应的亮度片区 + var center = sH / 30.0d; + var intCenter = (int)Math.Round(Math.Floor(center)); // 亮度片区编号 + center = 50d - + ((1d - center + intCenter) * cent[intCenter] + (center - intCenter) * cent[intCenter + 1]) * + sS; + // center = 50 + (cent(intCenter) + (center - intCenter) * (cent(intCenter + 1) - cent(intCenter))) * sS + sL = (sL < center ? sL / center : 1d + (sL - center) / (100d - center)) * 50d; + FromHSL(sH, sS, sL); + } + + A = 255d; + return this; + } + + public MyColor Alpha(double sA) + { + A = sA; + return this; + } + + public override string ToString() + { + return "(" + A + "," + R + "," + G + "," + B + ")"; + } + + public override bool Equals(object obj) + { + return Operators.ConditionalCompareObjectEqual(this, obj, false); + } + } + + /// + /// 支持负数与浮点数的矩形。 + /// + public class MyRect + { + // 构造函数 + public MyRect() + { + } + + public MyRect(double left, double top, double width, double height) + { + Left = left; + Top = top; + Width = width; + Height = height; + } + + // 属性 + public double Width { get; set; } + public double Height { get; set; } + public double Left { get; set; } + public double Top { get; set; } + } + + /// + /// 模块加载状态枚举。 + /// + public enum LoadState + { + Waiting, + Loading, + Finished, + Failed, + Aborted + } + + /// + /// 执行返回值。 + /// + public enum ProcessReturnValues + { + /// + /// 执行成功,或进程被中断。 + /// + Aborted = -1, + + /// + /// 执行成功。 + /// + Success = 0, + + /// + /// 执行失败。 + /// + Fail = 1, + + /// + /// 执行时出现未经处理的异常。 + /// + Exception = 2, + + /// + /// 执行超时。 + /// + Timeout = 3, + + /// + /// 取消执行。可能是由于不满足执行的前置条件。 + /// + Cancel = 4, + + /// + /// 任务成功完成。 + /// + TaskDone = 5 + } + + /// + /// 可以使用 Equals 和等号的 List。 + /// + public class EqualableList : List + { + public override bool Equals(object obj) + { + if (obj as List is null) + // 类型不同 + return false; + + // 类型相同 + var objList = (List)obj; + if (objList.Count != Count) + return false; + for (int i = 0, loopTo = objList.Count - 1; i <= loopTo; i++) + if (!objList[i].Equals(this[i])) + return false; + return true; + } + + public static bool operator ==(EqualableList left, EqualableList right) + { + return EqualityComparer>.Default.Equals(left, right); + } + + public static bool operator !=(EqualableList left, EqualableList right) + { + return !(left == right); + } + } + + #endregion + + #region 数学 + + /// + /// 2~65 进制的转换。 + /// + public static string RadixConvert(string Input, int FromRadix, int ToRadix) + { + const string Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz/+="; + // 零与负数的处理 + if (string.IsNullOrEmpty(Input)) + return "0"; + var IsNegative = Input.StartsWithF("-"); + if (IsNegative) + Input = Input.TrimStart('-'); + // 转换为十进制 + var RealNum = 0L; + var Scale = 1L; + foreach (var Digit in Input.Reverse().Select(l => Digits.IndexOfF(Conversions.ToString(l)))) + { + RealNum += Digit * Scale; + Scale *= FromRadix; + } + + // 转换为指定进制 + var Result = ""; + while (RealNum > 0L) + { + var NewNum = (int)(RealNum % ToRadix); + RealNum = (long)Math.Round((RealNum - NewNum) / (double)ToRadix); + Result = Digits[NewNum] + Result; + } + + // 负数的结束处理与返回 + return (IsNegative ? "-" : "") + Result; + } + + /// + /// 计算二阶贝塞尔曲线。 + /// + public static double MathBezier(double x, double x1, double y1, double x2, double y2, double acc = 0.01d) + { + if (x <= 0d || double.IsNaN(x)) return 0d; + if (x >= 1d) return 1d; + double a, b; + a = x; + do + { + b = 3 * a * ((0.33333333 + x1 - x2) * a * a + (x2 - 2 * x1) * a + x1); + a += (x - b) * 0.5; + } while (!(Math.Abs(b - x) < acc)); // 精度 + + return 3 * a * ((0.33333333 + y1 - y2) * a * a + (y2 - 2 * y1) * a + y1); + } + + /// + /// 将一个数字限制为 0~255 的 Byte 值。 + /// + public static byte MathByte(double d) + { + if (d < 0d) + d = 0d; + if (d > 255d) + d = 255d; + return (byte)Math.Round(Math.Round(d)); + } + + /// + /// 提供 MyColor 类型支持的 Math.Round。 + /// + public static MyColor MathRound(MyColor col, int w = 0) + { + return new MyColor + { A = Math.Round(col.A, w), R = Math.Round(col.R, w), G = Math.Round(col.G, w), B = Math.Round(col.B, w) }; + } + + /// + /// 获取两数间的百分比。小数点精确到 6 位。 + /// + /// + public static double MathPercent(double ValueA, double ValueB, double Percent) + { + return Math.Round(ValueA * (1d - Percent) + ValueB * Percent, 6); // 解决 Double 计算错误 + } + + /// + /// 获取两颜色间的百分比,根据 RGB 计算。小数点精确到 6 位。 + /// + public static MyColor MathPercent(MyColor ValueA, MyColor ValueB, double Percent) + { + return MathRound(ValueA * (1d - Percent) + ValueB * Percent, 6); // 解决Double计算错误 + } + + /// + /// 将数值限定在某个范围内。 + /// + public static double MathClamp(double value, double min, double max) + { + return Math.Max(min, Math.Min(max, value)); + } + + /// + /// 符号函数。 + /// + public static int MathSgn(double Value) + { + if (Value == 0d) return 0; + + if (Value > 0d) return 1; + + return -1; + } + + #endregion + + #region 文件 + + // ============================= + // 注册表 + // ============================= + + /// + /// 重命名一个注册表子键。不可用于包含子键的子键。 + /// + public static void RenameReg(RegistryKey parentKey, string subKeyName, string newSubKeyName) + { + if (parentKey.GetSubKeyNames().Contains(newSubKeyName)) + parentKey.DeleteSubKeyTree(newSubKeyName, false); + var SourceKey = parentKey.OpenSubKey(subKeyName); + if (SourceKey == null) + return; // 没有目标项 + var NewKey = parentKey.CreateSubKey(newSubKeyName); + if (SourceKey.GetSubKeyNames().Length > 0) + throw new NotSupportedException("不支持对包含子键的子键进行重命名:" + SourceKey.GetSubKeyNames()[0] + "。"); + foreach (var valueName in SourceKey.GetValueNames()) + { + var objValue = SourceKey.GetValue(valueName); + var valKind = SourceKey.GetValueKind(valueName); + NewKey.SetValue(valueName, objValue, valKind); + } + + parentKey.DeleteSubKeyTree(subKeyName, false); + } + + /// + /// 读取注册表,默认为程序所属。 + /// + public static string ReadReg(string Key, string DefaultValue = "", string Path = "") + { + string ReadRegRet = default; + try + { + RegistryKey parentKey; + RegistryKey softKey; + parentKey = Registry.CurrentUser; + softKey = parentKey.OpenSubKey(@"Software\" + (string.IsNullOrEmpty(Path) ? ModSecret.RegFolder : Path), + true); + if (softKey is null) + { + ReadRegRet = DefaultValue; // 不存在则返回默认值 + } + else + { + var readValue = new StringBuilder(); + readValue.AppendLine(softKey.GetValue(Key).ToString()); + var value = readValue.ToString().Replace("\r\n", ""); // 去除莫名的回车 + return string.IsNullOrEmpty(value) ? DefaultValue : value; + } // 错误则返回默认值 + } + catch (Exception ex) + { + Log(ex, "读取注册表出错:" + Key, LogLevel.Hint); + return DefaultValue; + } + + return ReadRegRet; + } + + /// + /// 写入注册表,默认为程序所属。 + /// + public static void WriteReg(string Key, string Value, bool ShowException = false, string Path = "", + bool ThrowException = false) + { + try + { + RegistryKey parentKey; + RegistryKey softKey; + parentKey = Registry.CurrentUser; + softKey = parentKey.OpenSubKey(@"Software\" + (string.IsNullOrEmpty(Path) ? ModSecret.RegFolder : Path), + true); + if (softKey is null) + softKey = parentKey.CreateSubKey(@"Software\" + + (string.IsNullOrEmpty(Path) + ? ModSecret.RegFolder + : Path)); // 如果不存在就创建 + softKey.SetValue(Key, Value); + } + catch (Exception ex) + { + Log(ex, "写入注册表出错:" + Key, ThrowException ? LogLevel.Hint : LogLevel.Developer); + if (ThrowException) + throw; + } + } + + /// + /// 是否存在某个注册表键。 + /// + public static bool HasReg(string Key) + { + return ReadReg(Key, null) is not null; + } + + /// + /// 删除注册表键。 + /// + public static void DeleteReg(string Key, bool ThrowException = false) + { + try + { + var SubKey = Registry.CurrentUser.OpenSubKey(@"Software\" + ModSecret.RegFolder, true); + SubKey?.DeleteValue(Key); + } + catch (Exception ex) + { + Log(ex, "删除注册表出错:" + Key, ThrowException ? LogLevel.Hint : LogLevel.Developer); + if (ThrowException) + throw; + } + } + + // ============================= + // ini + // ============================= + + private static readonly SafeDictionary> IniCache = new(); + + /// + /// 清除某 ini 文件的运行时缓存。 + /// + /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。 + public static void IniClearCache(string FileName) + { + if (!FileName.Contains(@":\")) + FileName = $@"{ExePath}PCL\{FileName}.ini"; + if (IniCache.ContainsKey(FileName)) + IniCache.Remove(FileName); + } + + /// + /// 获取 ini 文件缓存。如果没有,则新读取 ini 文件内容。 + /// 在文件不存在或读取失败时返回 Nothing。 + /// + /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。 + private static SafeDictionary IniGetContent(string FileName) + { + try + { + // 还原文件路径 + if (!FileName.Contains(@":\")) + FileName = $@"{ExePath}PCL\{FileName}.ini"; + // 检索缓存 + if (IniCache.ContainsKey(FileName)) + return IniCache[FileName]; + // 读取文件 + if (!File.Exists(FileName)) + return null; + var Ini = new SafeDictionary(); + foreach (var Line in ReadFile(FileName) + .Split("\r\n".ToArray(), StringSplitOptions.RemoveEmptyEntries)) + { + var Index = Line.IndexOfF(":"); + if (Index > 0) + Ini[Line.Substring(0, Index)] = Line.Substring(Index + 1); // 可能会有重复键,见 #3616 + } + + IniCache[FileName] = Ini; + return Ini; + } + catch (Exception ex) + { + Log(ex, $"生成 ini 文件缓存失败({FileName})", LogLevel.Hint); + return null; + } + } + + /// + /// 读取 ini 文件。这可能会使用到缓存。 + /// + /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。 + /// 键。 + /// 没有找到键时返回的默认值。 + public static string ReadIni(string FileName, string Key, string DefaultValue = "") + { + var Content = IniGetContent(FileName); + if (Content is null || !Content.ContainsKey(Key)) + return DefaultValue; + return Content[Key]; + } + + /// + /// 判断 ini 文件中是否包含某个键。这可能会使用到缓存。 + /// + public static bool HasIniKey(string FileName, string Key) + { + var Content = IniGetContent(FileName); + return Content is not null && Content.ContainsKey(Key); + } + + /// + /// 从 ini 文件中移除某个键。这会更新缓存。 + /// + public static void DeleteIniKey(string FileName, string Key) + { + WriteIni(FileName, Key, null); + } + + /// + /// 写入 ini 文件,这会更新缓存。 + /// 若 Value 为 Nothing,则删除该键。 + /// + /// 文件完整路径或简写文件名。简写将会使用“ApplicationName\文件名.ini”作为路径。 + /// 键。 + /// 值。 + /// + public static void WriteIni(string FileName, string Key, string Value) + { + try + { + // 预处理 + if (Key.Contains(":")) + throw new Exception($"尝试写入 ini 文件 {FileName} 的键名中包含了冒号:{Key}"); + Key = Key.Replace("\r", "").Replace("\n", ""); + Value = Value?.Replace("\r", "").Replace("\n", ""); + // 防止争用 + lock (WriteIniLock) + { + // 获取目前文件 + var Content = IniGetContent(FileName); + if (Content is null) + Content = new SafeDictionary(); + // 更新值 + if (Value is null) + { + if (!Content.ContainsKey(Key)) + return; // 无需处理 + Content.Remove(Key); + } + else + { + if (Content.ContainsKey(Key) && (Content[Key] ?? "") == (Value ?? "")) + return; // 无需处理 + Content[Key] = Value; + } + + // 写入文件 + var FileContent = new StringBuilder(); + foreach (var Pair in Content) + { + FileContent.Append(Pair.Key); + FileContent.Append(":"); + FileContent.Append(Pair.Value); + FileContent.Append("\r\n"); + } + + if (!FileName.Contains(@":\")) + FileName = $@"{ExePath}PCL\{FileName}.ini"; + WriteFile(FileName, FileContent.ToString()); + } + } + catch (Exception ex) + { + Log(ex, $"写入文件失败({FileName} → {Key}:{Value})", LogLevel.Hint); + } + } + + private static readonly object WriteIniLock = new(); + + // 路径处理 + /// + /// 从文件路径或者 Url 获取不包含文件名的路径,或获取文件夹的父文件夹路径。 + /// 取决于原路径格式,路径以 / 或 \ 结尾。 + /// 不包含路径将会抛出异常。 + /// + public static string GetPathFromFullPath(string FilePath) + { + string GetPathFromFullPathRet = default; + if (!(FilePath.Contains(@"\") || FilePath.Contains("/"))) + throw new Exception("不包含路径:" + FilePath); + if (FilePath.EndsWithF(@"\") || FilePath.EndsWithF("/")) + { + // 是文件夹路径 + var IsRight = FilePath.EndsWithF(@"\"); + FilePath = Strings.Left(FilePath, Strings.Len(FilePath) - 1); + GetPathFromFullPathRet = Strings.Left(FilePath, FilePath.LastIndexOfAny(new[] { '\\', '/' })) + + (IsRight ? @"\" : "/"); + } + else + { + // 是文件路径 + GetPathFromFullPathRet = Strings.Left(FilePath, FilePath.LastIndexOfAny(new[] { '\\', '/' }) + 1); + if (string.IsNullOrEmpty(GetPathFromFullPathRet)) + throw new Exception("不包含路径:" + FilePath); + } + + return GetPathFromFullPathRet; + } + + /// + /// 从文件路径或者 Url 获取不包含路径的文件名。不包含文件名将会抛出异常。 + /// + public static string GetFileNameFromPath(string FilePath) + { + FilePath = FilePath.Replace("/", @"\"); + if (FilePath.EndsWithF(@"\")) + throw new Exception("不包含文件名:" + FilePath); + if (FilePath.Contains("?")) + FilePath = FilePath.Substring(0, FilePath.IndexOfF("?")); // 去掉网络参数后的 ? + if (FilePath.Contains(@"\")) + FilePath = FilePath.Substring(FilePath.LastIndexOfF(@"\") + 1); + var length = FilePath.Length; + if (length == 0) + throw new Exception("不包含文件名:" + FilePath); + if (length > 250) + throw new PathTooLongException("文件名过长:" + FilePath); + return FilePath; + } + + /// + /// 从文件路径或者 Url 获取不包含路径与扩展名的文件名。不包含文件名将会抛出异常。 + /// + public static string GetFileNameWithoutExtentionFromPath(string FilePath) + { + return Path.GetFileNameWithoutExtension(FilePath); + } + + /// + /// 从文件夹路径获取文件夹名。 + /// + public static string GetFolderNameFromPath(string FolderPath) + { + if (FolderPath.EndsWithF(@":\") || FolderPath.EndsWithF(@":\\")) + return FolderPath.Substring(0, 1); + if (FolderPath.EndsWithF(@"\") || FolderPath.EndsWithF("/")) + FolderPath = Strings.Left(FolderPath, FolderPath.Length - 1); + return GetFileNameFromPath(FolderPath); + } + + // 读取、写入、复制文件 + /// + /// 复制文件。会自动创建文件夹、会覆盖已有的文件。 + /// + public static void CopyFile(string FromPath, string ToPath) + { + try + { + // 还原文件路径 + if (!FromPath.Contains(@":\")) + FromPath = ExePath + FromPath; + if (!ToPath.Contains(@":\")) + ToPath = ExePath + ToPath; + // 如果复制同一个文件则跳过 + if ((FromPath ?? "") == (ToPath ?? "")) + return; + // 确保目录存在 + Directory.CreateDirectory(GetPathFromFullPath(ToPath)); + // 复制文件 + File.Copy(FromPath, ToPath, true); + } + catch (Exception ex) + { + throw new Exception("复制文件出错:" + FromPath + " → " + ToPath, ex); + } + } + + /// + /// 读取文件,如果失败则返回空数组。 + /// + public static byte[] ReadFileBytes(string FilePath, Encoding Encoding = null) + { + try + { + // 还原文件路径 + if (!FilePath.Contains(@":\")) + FilePath = ExePath + FilePath; + if (File.Exists(FilePath)) + using (var ReadStream = + new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) // 支持读取使用中的文件 + { + using (var ms = new MemoryStream()) + { + ReadStream.CopyTo(ms); + return ms.ToArray(); + } + } + + Log("[System] 欲读取的文件不存在,已返回空内容:" + FilePath); + return Array.Empty(); + } + catch (Exception ex) + { + Log(ex, "读取文件出错:" + FilePath); + return Array.Empty(); + } + } + + /// + /// 读取文件,如果失败则返回空字符串。 + /// + /// 文件完整或相对路径。 + public static string ReadFile(string FilePath, Encoding Encoding = null) + { + string ReadFileRet = default; + var FileBytes = ReadFileBytes(FilePath); + ReadFileRet = Encoding is null ? DecodeBytes(FileBytes) : Encoding.GetString(FileBytes); + return ReadFileRet; + } + + /// + /// 读取流中的所有文本。 + /// + public static string ReadFile(Stream Stream, Encoding Encoding = null) + { + try + { + var readedContent = new MemoryStream(); + Stream.CopyTo(readedContent); + var Bts = readedContent.ToArray(); + return (Encoding ?? EncodingDetector.DetectEncoding(Bts)).GetString(Bts); + } + catch (Exception ex) + { + Log(ex, "读取流出错"); + return ""; + } + } + + /// + /// 写入文件。 + /// + /// 文件完整或相对路径。 + /// 文件内容。 + /// 是否将文件内容追加到当前文件,而不是覆盖它。 + public static void WriteFile(string FilePath, string Text, bool Append = false, Encoding? Encoding = null) + { + // 处理相对路径 + if (!FilePath.Contains(@":\")) + FilePath = ExePath + FilePath; + // 确保目录存在 + Directory.CreateDirectory(GetPathFromFullPath(FilePath)); + // 写入文件 + if (Append) + // 追加目前文件 + using (var writer = new StreamWriter(FilePath, true, + Encoding ?? EncodingDetector.DetectEncoding(ReadFileBytes(FilePath)))) + { + writer.Write(Text); + } + else + // 直接写入字节 + File.WriteAllBytes(FilePath, + Encoding is null ? new UTF8Encoding(false).GetBytes(Text) : Encoding.GetBytes(Text)); + } + + /// + /// 写入文件。 + /// 如果 CanThrow 设置为 False,返回是否写入成功。 + /// + /// 文件完整或相对路径。 + /// 文件内容。 + /// 是否将文件内容追加到当前文件,而不是覆盖它。 + public static void WriteFile(string FilePath, byte[] Content, bool Append = false) + { + // 处理相对路径 + if (!FilePath.Contains(@":\")) + FilePath = ExePath + FilePath; + // 确保目录存在 + Directory.CreateDirectory(GetPathFromFullPath(FilePath)); + // 写入文件 + File.WriteAllBytes(FilePath, Content); + } + + /// + /// 将流写入文件。 + /// + /// 文件完整或相对路径。 + public static bool WriteFile(string FilePath, Stream Stream) + { + try + { + // 还原文件路径 + if (!FilePath.Contains(@":\")) + FilePath = ExePath + FilePath; + // 确保目录存在 + Directory.CreateDirectory(GetPathFromFullPath(FilePath)); + // 读取流 + using (var fs = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + fs.SetLength(0L); + Stream.CopyTo(fs); + } + + return true; + } + catch (Exception ex) + { + Log(ex, "保存流出错"); + return false; + } + } + + /// + /// 解码 Bytes。 + /// + public static string DecodeBytes(byte[] Bytes) + { + var Length = Bytes.Length; + if (Length < 3) + return Encoding.UTF8.GetString(Bytes); + // 根据 BOM 判断编码 + if (Bytes[0] >= 0xEF) + { + // 有 BOM 类型 + if (Bytes[0] == 0xEF && Bytes[1] == 0xBB) return Encoding.UTF8.GetString(Bytes, 3, Length - 3); + + if (Bytes[0] == 0xFE && Bytes[1] == 0xFF) return Encoding.BigEndianUnicode.GetString(Bytes, 3, Length - 3); + + if (Bytes[0] == 0xFF && Bytes[1] == 0xFE) return Encoding.Unicode.GetString(Bytes, 3, Length - 3); + + return Encoding.GetEncoding("GB18030").GetString(Bytes, 3, Length - 3); + } + + // 无 BOM 文件:GB18030(ANSI)或 UTF8 + var UTF8 = Encoding.UTF8.GetString(Bytes); + var ErrorChar = Encoding.UTF8.GetString(new[] { (byte)239, (byte)191, (byte)189 }).ToCharArray()[0]; + if (UTF8.Contains(ErrorChar)) return Encoding.GetEncoding("GB18030").GetString(Bytes); + + return UTF8; + } + + public static object GetHexString(Memory bytes) + { + var sb = new StringBuilder(bytes.Length * 2); + foreach (var c in bytes.Span) + sb.Append(c.ToString("x2")); + + return sb.ToString(); + } + + // 文件校验 + /// + /// 获取文件 MD5,若失败则返回空字符串。 + /// + public static string GetFileMD5(string FilePath) + { + var Retry = false; + Re: ; + + try + { + // 获取 MD5 + using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + return Conversions.ToString(GetHexString(MD5Provider.Instance.ComputeHash(fs))); + } + } + catch (Exception ex) + { + if (Retry || ex is FileNotFoundException) + { + Log(ex, "获取文件 MD5 失败:" + FilePath); + return ""; + } + + Retry = true; + Log(ex, "获取文件 MD5 可重试失败:" + FilePath, LogLevel.Normal); + Thread.Sleep(RandomUtils.NextInt(200, 500)); + goto Re; + } + } + + /// + /// 获取文件 SHA512,若失败则返回空字符串。 + /// + public static string GetFileSHA512(string FilePath) + { + var Retry = false; + Re: ; + + try + { + // '检测该文件是否在下载中,若在下载则放弃检测 + // If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return "" + // 获取 SHA512 + using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + return Conversions.ToString(GetHexString(SHA512Provider.Instance.ComputeHash(fs))); + } + } + catch (Exception ex) + { + if (Retry || ex is FileNotFoundException) + { + Log(ex, "获取文件 SHA512 失败:" + FilePath); + return ""; + } + + Retry = true; + Log(ex, "获取文件 SHA512 可重试失败:" + FilePath, LogLevel.Normal); + Thread.Sleep(RandomUtils.NextInt(200, 500)); + goto Re; + } + } + + /// + /// 获取文件 SHA256,若失败则返回空字符串。 + /// + public static string GetFileSHA256(string FilePath) + { + var Retry = false; + Re: ; + + try + { + // '检测该文件是否在下载中,若在下载则放弃检测 + // If IgnoreOnDownloading AndAlso NetManage.Files.ContainsKey(FilePath) AndAlso NetManage.Files(FilePath).State <= NetState.Merge Then Return "" + // 获取 SHA256 + using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + return Conversions.ToString(GetHexString(SHA256Provider.Instance.ComputeHash(fs))); + } + } + catch (Exception ex) + { + if (Retry || ex is FileNotFoundException) + { + Log(ex, "获取文件 SHA256 失败:" + FilePath); + return ""; + } + + Retry = true; + Log(ex, "获取文件 SHA256 可重试失败:" + FilePath, LogLevel.Normal); + Thread.Sleep(RandomUtils.NextInt(200, 500)); + goto Re; + } + } + + /// + /// 获取文件 SHA1,若失败则返回空字符串。 + /// + public static string GetFileSHA1(string FilePath) + { + var Retry = false; + Re: ; + + try + { + // 获取 SHA1 + using (var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + return Conversions.ToString(GetHexString(SHA1Provider.Instance.ComputeHash(fs))); + } + } + catch (Exception ex) + { + if (Retry || ex is FileNotFoundException) + { + Log(ex, "获取文件 SHA1 失败:" + FilePath); + return ""; + } + + Retry = true; + Log(ex, "获取文件 SHA1 可重试失败:" + FilePath, LogLevel.Normal); + Thread.Sleep(RandomUtils.NextInt(200, 500)); + goto Re; + } + } + + /// + /// 获取流的 SHA1,若失败则返回空字符串。 + /// + public static string GetAuthSHA1(Stream inputStream) + { + try + { + return Conversions.ToString(GetHexString(SHA1Provider.Instance.ComputeHash(inputStream))); + } + catch (Exception ex) + { + Log(ex, "获取流 SHA1 失败"); + return ""; + } + } + + /// + /// 文件的校验规则。 + /// + public class FileChecker + { + /// + /// 文件的准确大小。 + /// 不检查则为 -1。 + /// + public long ActualSize = -1; + + /// + /// 是否可以使用已经存在的文件。 + /// + public bool CanUseExistsFile = true; + + /// + /// 文件的 MD5、SHA1 或 SHA256。会根据输入字符串的长度自动判断种类。 + /// 不检查则为 Nothing。 + /// + public string Hash; + + /// + /// 是否要求为 JSON 文件。 + /// 即,开头结尾必须为 {} 或 []。 + /// + public bool IsJson; + + /// + /// 文件的最小大小。 + /// 不检查则为 -1。 + /// + public long MinSize = -1; + + public FileChecker(long MinSize = -1, long ActualSize = -1, string Hash = null, bool CanUseExistsFile = true, + bool IsJson = false) + { + this.ActualSize = ActualSize; + this.MinSize = MinSize; + this.Hash = Hash; + this.CanUseExistsFile = CanUseExistsFile; + this.IsJson = IsJson; + } + + /// + /// 检查文件。若成功则返回 Nothing,失败则返回错误的描述文本,描述文本不以句号结尾。不会抛出错误。 + /// + public string Check(string LocalPath) + { + try + { + Log($"[Checker] 开始校验文件 {LocalPath}", LogLevel.Developer); + var Info = new FileInfo(LocalPath); + if (!Info.Exists) + return "文件不存在:" + LocalPath; + var FileSize = Info.Length; + var ErrorMessage = new List(); + var AllowIgnore = false; // 允许相信哈希正确但是大小不正确 + if (!string.IsNullOrEmpty(Hash)) + { + if (Hash.Length < 35) // MD5 + { + var ComputedHash = GetFileMD5(LocalPath); + if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? "")) + ErrorMessage.Add("文件 MD5 应为 " + Hash + ",实际为 " + ComputedHash); + } + else if (Hash.Length == 64) // SHA256 + { + var ComputedHash = GetFileSHA256(LocalPath); + if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? "")) + ErrorMessage.Add("文件 SHA256 应为 " + Hash + ",实际为 " + ComputedHash); + } + else // SHA1 (40) + { + var ComputedHash = GetFileSHA1(LocalPath); + if ((Hash.ToLowerInvariant() ?? "") != (ComputedHash ?? "")) + ErrorMessage.Add("文件 SHA1 应为 " + Hash + ",实际为 " + ComputedHash); + } + + AllowIgnore = ErrorMessage.Count == 0; + } + + if (ActualSize >= 0L && ActualSize != FileSize && !AllowIgnore) // 不允许忽略大小不正确的情况 + ErrorMessage.Add($"文件大小应为 {ActualSize} B,实际为 {FileSize} B" + + (FileSize < 2000L ? ",内容为" + ReadFile(LocalPath) : "")); + + if (MinSize >= 0L && MinSize > FileSize) + ErrorMessage.Add($"文件大小应大于 {MinSize} B,实际为 {FileSize} B" + + (FileSize < 2000L ? ",内容为:" + ReadFile(LocalPath) : "")); + + if (IsJson) + { + var Content = ReadFile(LocalPath); + if (string.IsNullOrEmpty(Content)) + throw new Exception("读取到的文件为空"); + try + { + GetJson(Content); + } + catch (Exception ex) + { + throw new Exception("不是有效的 Json 文件", ex); + } + } + + if (ErrorMessage.Count != 0) + { + ErrorMessage.Insert(0, $"实际校验地址:{LocalPath}"); + return ErrorMessage.Join(";"); + } + + return null; + } + catch (Exception ex) + { + Log(ex, "检查文件出错"); + return ex.ToString(); + } + } + } + + /// + /// 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 Jar 以 zip 方式解压。 + /// 会尝试创建,但不会清空目标文件夹。 + /// + public static void ExtractFile(string CompressFilePath, string DestDirectory, Encoding Encode = null, + Action ProgressIncrementHandler = null) + { + Directory.CreateDirectory(DestDirectory); + DestDirectory = Path.GetFullPath(DestDirectory); + if (!DestDirectory.EndsWith(Path.DirectorySeparatorChar.ToString())) + DestDirectory += Conversions.ToString(Path.DirectorySeparatorChar); + if (CompressFilePath.EndsWithF(".gz", true)) + // 以 gz 方式解压 + using (var compressedFile = new FileStream(CompressFilePath, FileMode.Open, FileAccess.Read)) + { + using (var decompressStream = new GZipStream(compressedFile, CompressionMode.Decompress)) + { + using (var extractFileStream = + new FileStream( + Path.Combine(DestDirectory, + GetFileNameFromPath(CompressFilePath).ToLower().Replace(".tar", "") + .Replace(".gz", "")), FileMode.OpenOrCreate, FileAccess.Write)) + { + decompressStream.CopyTo(extractFileStream); + } + } + } + else + // 以 zip 方式解压 + using (var Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read, + Encode ?? Encoding.GetEncoding("GB18030"))) + { + var TotalCount = Archive.Entries.Count; + foreach (var Entry in Archive.Entries) + { + if (ProgressIncrementHandler is not null) + ProgressIncrementHandler(1d / TotalCount); + var DestinationPath = Path.GetFullPath(Path.Combine(DestDirectory, Entry.FullName)); + if (!DestinationPath.StartsWithF(DestDirectory)) + throw new Exception( + $"解压文件 {Entry.FullName} 错误:解压文件路径 {DestinationPath} 不在目标目录 {DestDirectory} 内"); + if (DestinationPath.EndsWithF(@"\") || DestinationPath.EndsWithF("/")) + { + } + else + { + Directory.CreateDirectory(GetPathFromFullPath(DestinationPath)); + Entry.ExtractToFile(DestinationPath, true); + } + } + } + } + + /// + /// 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。 + /// + public static int DeleteDirectory(string Path, bool IgnoreIssue = false) + { + if (!Directory.Exists(Path)) + return 0; + var DeletedCount = 0; + string[] Files; + try + { + Files = Directory.GetFiles(Path); + } + catch (DirectoryNotFoundException ex) // #4549 + { + Log(ex, $"疑似为孤立符号链接,尝试直接删除({Path})", LogLevel.Developer); + Directory.Delete(Path); + return 0; + } + + foreach (var FilePath in Files) + { + var RetriedFile = false; + RetryFile: ; + + try + { + File.Delete(FilePath); + DeletedCount += 1; + } + catch (Exception ex) + { + if (!RetriedFile) + { + RetriedFile = true; + Log(ex, $"删除文件失败,将在 0.3s 后重试({FilePath})"); + Thread.Sleep(300); + goto RetryFile; + } + + if (IgnoreIssue) + Log(ex, "删除单个文件可忽略地失败"); + else + throw; + } + } + + foreach (var str in Directory.GetDirectories(Path)) + DeleteDirectory(str, IgnoreIssue); + var RetriedDir = false; + RetryDir: ; + + try + { + Directory.Delete(Path, true); + } + catch (Exception ex) + { + if (!RetriedDir && !RunInUi()) + { + RetriedDir = true; + Log(ex, $"删除文件夹失败,将在 0.3s 后重试({Path})"); + Thread.Sleep(300); + goto RetryDir; + } + + if (IgnoreIssue) + Log(ex, "删除单个文件夹可忽略地失败"); + else + throw; + } + + return DeletedCount; + } + + /// + /// 复制文件夹,失败会抛出异常。 + /// + public static void CopyDirectory(string FromPath, string ToPath, Action ProgressIncrementHandler = null) + { + FromPath = FromPath.Replace("/", @"\"); + if (!FromPath.EndsWithF(@"\")) + FromPath += @"\"; + ToPath = ToPath.Replace("/", @"\"); + if (!ToPath.EndsWithF(@"\")) + ToPath += @"\"; + var AllFiles = EnumerateFiles(FromPath).ToList(); + var FileCount = AllFiles.Count; + foreach (var File in AllFiles) + { + CopyFile(File.FullName, File.FullName.Replace(FromPath, ToPath)); + if (ProgressIncrementHandler is not null) + ProgressIncrementHandler(1d / FileCount); + } + } + + /// + /// 遍历文件夹中的所有文件。 + /// + public static IEnumerable EnumerateFiles(string Directory) + { + var Info = new DirectoryInfo(ShortenPath(Directory)); + if (!Info.Exists) + return new List(); + return Info.EnumerateFiles("*", SearchOption.AllDirectories); + } + + /// + /// 若路径长度大于指定值,则将长路径转换为短路径。 + /// + public static string ShortenPath(string LongPath, int ShortenThreshold = 247) + { + if (LongPath.Length <= ShortenThreshold) + return LongPath; + var ShortPath = new StringBuilder(260); + GetShortPathName(LongPath, ShortPath, 260); + return ShortPath.ToString(); + } + + public static void MoveDirectory(string SourceDir, string TargetDir) + { + if (!Directory.Exists(TargetDir)) + Directory.CreateDirectory(TargetDir); + foreach (var FilePath in Directory.GetFiles(SourceDir)) + { + var FileName = GetFileNameFromPath(FilePath); + File.Move(FilePath, Path.Combine(TargetDir, FileName)); + } + + foreach (var DirPath in Directory.GetDirectories(SourceDir)) + { + var DirName = GetFolderNameFromPath(DirPath); + MoveDirectory(DirPath, Path.Combine(TargetDir, DirName)); + } + } + + [DllImport("kernel32", EntryPoint = "GetShortPathNameA")] + private static extern int GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, int cchBuffer); + + public static void CreateSymbolicLink(string LinkPath, string TargetPath, int Flags) + { + var CMDProcess = new Process(); + var LinkDPath = ModLaunch.ExtractLinkD(); + { + var withBlock = CMDProcess.StartInfo; + withBlock.FileName = LinkDPath; + withBlock.Arguments = $"\"{LinkPath}\" \"{TargetPath}\""; + withBlock.CreateNoWindow = true; + withBlock.UseShellExecute = false; + } + CMDProcess.Start(); + while (!CMDProcess.HasExited) + { + } + } + + #endregion + + #region 文本 + + public static char vbLQ = Convert.ToChar(8220); + public static char vbRQ = Convert.ToChar(8221); + + /// + /// 返回一个枚举对应的字符串。 + /// + /// 一个已经实例化的枚举类型。 + public static string GetStringFromEnum(Enum EnumData) + { + return Enum.GetName(EnumData.GetType(), EnumData); + } + + /// + /// 将文件大小转化为适合的文本形式,如“1.28 M”。 + /// + /// 以字节为单位的大小表示。 + public static string GetString(long FileSize) + { + var IsNegative = FileSize < 0L; + if (IsNegative) + FileSize *= -1; + if (FileSize < 1000L) + // B 级 + return (IsNegative ? "-" : "") + FileSize + " B"; + + if (FileSize < 1024 * 1000) + { + // K 级 + var RoundResult = Math.Round(FileSize / 1024d).ToString(); + return (IsNegative ? "-" : "") + + Math.Round(FileSize / 1024d, (int)Math.Round(MathClamp(3 - RoundResult.Length, 0d, 2d))) + " K"; + } + + if (FileSize < 1024 * 1024 * 1000) + { + // M 级 + var RoundResult = Math.Round(FileSize / 1024d / 1024d).ToString(); + return (IsNegative ? "-" : "") + Math.Round(FileSize / 1024d / 1024d, + (int)Math.Round(MathClamp(3 - RoundResult.Length, 0d, 2d))) + " M"; + } + else + { + // G 级 + var RoundResult = Math.Round(FileSize / 1024d / 1024d / 1024d).ToString(); + return (IsNegative ? "-" : "") + Math.Round(FileSize / 1024d / 1024d / 1024d, + (int)Math.Round(MathClamp(3 - RoundResult.Length, 0d, 2d))) + " G"; + } + } + + /// + /// 获取 JSON 对象。 + /// + public static object GetJson(string Data) + { + try + { + return JsonConvert.DeserializeObject(Data, + new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Local }); + } + catch (Exception ex) + { + var Length = (Data ?? "").Length; + throw new Exception("格式化 JSON 失败:" + (Length > 2000 + ? Data.Substring(0, 500) + $"...(全长 {Length} 个字符)..." + Strings.Right(Data, 500) + : Data)); + } + } + + /// + /// 将第一个字符转换为大写,其余字符转换为小写。 + /// + public static string Capitalize(this string word) + { + if (string.IsNullOrEmpty(word)) + return word; + return word.Substring(0, 1).ToUpperInvariant() + word.Substring(1).ToLowerInvariant(); + } + + /// + /// 将字符串统一至某个长度,过短则以 Code 将其右侧填充,过长则截取靠左的指定长度。 + /// + public static string StrFill(string Str, string Code, byte Length) + { + if (Str.Length > Length) + return Strings.Mid(Str, 1, Length); + return Strings.Mid(Str.PadRight(Length, Conversions.ToChar(Code)), Str.Length + 1) + Str; + } + + /// + /// 将一个小数显示为固定的小数点后位数形式,将向零取整。 + /// 如 12 保留 2 位则输出 12.00,而 95.678 保留 2 位则输出 95.67。 + /// + public static string StrFillNum(double Num, int Length) + { + string StrFillNumRet = default; + Num = Math.Round(Num, Length, MidpointRounding.AwayFromZero); + StrFillNumRet = Num.ToString(); + if (!StrFillNumRet.Contains(".")) + return (StrFillNumRet + ".").PadRight(StrFillNumRet.Length + 1 + Length, '0'); + return StrFillNumRet.PadRight(StrFillNumRet.Split(".")[0].Length + 1 + Length, '0'); + } + + /// + /// 移除字符串首尾的标点符号、回车,以及括号中、冒号后的补充说明内容。 + /// + public static object StrTrim(string Str, bool RemoveQuote = true) + { + if (RemoveQuote) + Str = Str.Split("(")[0].Split(":")[0].Split("(")[0].Split(":")[0]; + return Str.Trim('.', '。', '!', ' ', '!', '?', '?', Conversions.ToChar("\r"), + Conversions.ToChar("\n")); + } + + /// + /// 连接字符串。 + /// + public static string Join(this IEnumerable List, string Split) + { + var Builder = new StringBuilder(); + var IsFirst = true; + foreach (var Element in List) + { + if (IsFirst) + IsFirst = false; + else + Builder.Append(Split); + if (Element is not null) + Builder.Append(Element); + } + + return Builder.ToString(); + } + + /// + /// 分割字符串。 + /// + public static string[] Split(this string FullStr, string SplitStr) + { + if (SplitStr.Length == 1) return FullStr.Split(SplitStr[0]); + + return FullStr.Split(new[] { SplitStr }, StringSplitOptions.None); + } + + /// + /// 获取字符串哈希值。 + /// + public static ulong GetHash(string Str) + { + ulong GetHashRet = default; + GetHashRet = 5381UL; + for (int i = 0, loopTo = Str.Length - 1; i <= loopTo; i++) + GetHashRet = (GetHashRet << 5) ^ GetHashRet ^ (ulong)Strings.AscW(Str[i]); + return GetHashRet ^ 0xA98F501BC684032FUL; + } + + /// + /// 获取字符串 MD5。 + /// + public static string GetStringMD5(string Str) + { + return Conversions.ToString(GetHexString(MD5Provider.Instance.ComputeHash(Str))); + } + + /// + /// 检查字符串中的字符是否均为 ASCII 字符。 + /// + public static bool IsASCII(this string Input) + { + return Input.All(c => Strings.AscW(c) < 128); + } + + /// + /// 获取在子字符串第一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024。 + /// 如果未找到子字符串则不裁切。 + /// + public static string BeforeFirst(this string Str, string Text, bool IgnoreCase = false) + { + var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.IndexOfF(Text, IgnoreCase); + if (Pos >= 0) return Str.Substring(0, Pos); + + return Str; + } + + /// + /// 获取在子字符串最后一次出现之前的部分,例如对 2024/11/08 拆切 / 会得到 2024/11。 + /// 如果未找到子字符串则不裁切。 + /// + public static string BeforeLast(this string Str, string Text, bool IgnoreCase = false) + { + var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.LastIndexOfF(Text, IgnoreCase); + if (Pos >= 0) return Str.Substring(0, Pos); + + return Str; + } + + /// + /// 获取在子字符串第一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 11/08。 + /// 如果未找到子字符串则不裁切。 + /// + public static string AfterFirst(this string Str, string Text, bool IgnoreCase = false) + { + var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.IndexOfF(Text, IgnoreCase); + if (Pos >= 0) return Str.Substring(Pos + Text.Length); + + return Str; + } + + /// + /// 获取在子字符串最后一次出现之后的部分,例如对 2024/11/08 拆切 / 会得到 08。 + /// 如果未找到子字符串则不裁切。 + /// + public static string AfterLast(this string Str, string Text, bool IgnoreCase = false) + { + var Pos = string.IsNullOrEmpty(Text) ? -1 : Str.LastIndexOfF(Text, IgnoreCase); + if (Pos >= 0) return Str.Substring(Pos + Text.Length); + + return Str; + } + + /// + /// 获取处于两个子字符串之间的部分,裁切尽可能多的内容。 + /// 等效于 AfterLast 后接 BeforeFirst。 + /// 如果未找到子字符串则不裁切。 + /// + public static string Between(this string Str, string After, string Before, bool IgnoreCase = false) + { + var StartPos = string.IsNullOrEmpty(After) ? -1 : Str.LastIndexOfF(After, IgnoreCase); + if (StartPos >= 0) + StartPos += After.Length; + else + StartPos = 0; + var EndPos = string.IsNullOrEmpty(Before) ? -1 : Str.IndexOfF(Before, StartPos, IgnoreCase); + if (EndPos >= 0) return Str.Substring(StartPos, EndPos - StartPos); + + if (StartPos > 0) return Str.Substring(StartPos); + + return Str; + } + + /// + /// 高速的 StartsWith。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool StartsWithF(this string Str, string Prefix, bool IgnoreCase = false) + { + return Str.StartsWith(Prefix, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 高速的 EndsWith。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EndsWithF(this string Str, string Suffix, bool IgnoreCase = false) + { + return Str.EndsWith(Suffix, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 支持可变大小写判断的 Contains。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsF(this string Str, string SubStr, bool IgnoreCase = false) + { + return Str.IndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) >= 0; + } + + /// + /// 高速的 IndexOf。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfF(this string Str, string SubStr, bool IgnoreCase = false) + { + return Str.IndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 高速的 IndexOf。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int IndexOfF(this string Str, string SubStr, int StartIndex, bool IgnoreCase = false) + { + return Str.IndexOf(SubStr, StartIndex, + IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 高速的 LastIndexOf。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LastIndexOfF(this string Str, string SubStr, bool IgnoreCase = false) + { + return Str.LastIndexOf(SubStr, IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 高速的 LastIndexOf。 + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LastIndexOfF(this string Str, string SubStr, int StartIndex, bool IgnoreCase = false) + { + return Str.LastIndexOf(SubStr, StartIndex, + IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + /// + /// 不会报错的 Val。 + /// 如果输入有误,返回 0。 + /// + public static double Val(object Str) + { + try + { + return Str is string && Str == "&" + ? 0d + : Conversion.Val(Str); + } + catch + { + return 0d; + } + } + + // 转义 + /// + /// 为字符串进行 XML 转义。 + /// + public static string EscapeXML(string Str) + { + if (Str.StartsWithF("{")) + Str = "{}" + Str; // #4187 + return Str.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("'", "'") + .Replace("\"", """).Replace("\r\n", " "); + } + + /// + /// 为字符串进行 Like 关键字转义。 + /// + public static string EscapeLikePattern(string input) + { + var sb = new StringBuilder(); + foreach (var c in input) + switch (c) + { + case '[': + case ']': + case '*': + case '?': + case '#': + { + sb.Append('[').Append(c).Append(']'); + break; + } + + default: + { + sb.Append(c); + break; + } + } + + return sb.ToString(); + } + + // 正则 + /// + /// 搜索字符串中的所有正则匹配项。 + /// + public static List RegexSearch(this string str, string regex, RegexOptions options = RegexOptions.None) + { + List RegexSearchRet = default; + try + { + RegexSearchRet = new List(); + var RegexSearchRes = new Regex(regex, options).Matches(str); + if (RegexSearchRes is null) + return RegexSearchRet; + foreach (Match item in RegexSearchRes) + RegexSearchRet.Add(item.Value); + } + catch (Exception ex) + { + Log(ex, "正则匹配全部项出错"); + return new List(); + } + + return RegexSearchRet; + } + + /// + /// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。 + /// + public static string RegexSeek(this string str, string regex, RegexOptions options = RegexOptions.None) + { + try + { + var Result = Regex.Match(str, regex, options).Value; + return string.IsNullOrEmpty(Result) ? null : Result; + } + catch (Exception ex) + { + Log(ex, "正则匹配第一项出错"); + return null; + } + } + + /// + /// 获取字符串中的第一个正则匹配项,若无匹配则返回 Nothing。 + /// + public static string RegexSeek(this string str, Regex regex, RegexOptions options = RegexOptions.None) + { + try + { + var Result = regex.Match(str, (int)options).Value; + return string.IsNullOrEmpty(Result) ? null : Result; + } + catch (Exception ex) + { + Log(ex, "正则匹配第一项出错"); + return null; + } + } + + /// + /// 检查字符串是否匹配某正则模式。 + /// + public static bool RegexCheck(this string str, string regex, RegexOptions options = RegexOptions.None) + { + try + { + return Regex.IsMatch(str, regex, options); + } + catch (Exception ex) + { + Log(ex, "正则检查出错"); + return false; + } + } + + /// + /// 进行正则替换,会抛出错误。 + /// + public static string RegexReplace(this string AllContents, string SearchRegex, string ReplaceTo, + RegexOptions options = RegexOptions.None) + { + return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options); + } + + /// + /// 对每个正则匹配分别进行替换,会抛出错误。 + /// + public static string RegexReplaceEach(this string AllContents, string SearchRegex, MatchEvaluator ReplaceTo, + RegexOptions options = RegexOptions.None) + { + return Regex.Replace(AllContents, SearchRegex, ReplaceTo, options); + } + + #endregion + + #region 搜索 + + /// + /// 获取搜索文本的相似度。 + /// + /// 被搜索的长内容。 + /// 用户输入的搜索文本。 + private static double SearchSimilarity(string Source, string Query) + { + var qp = 0; + var lenSum = 0d; + Source = Source.ToLower().Replace(" ", ""); + Query = Query.ToLower().Replace(" ", ""); + var sourceLength = Source.Length; + var queryLength = Query.Length; // 用于计算最后因数的长度缓存 + while (qp < queryLength) + { + // 对 qp 作为开始位置计算 + var sp = 0; + var lenMax = 0; + var spMax = 0; + // 查找以 qp 为头的最大子串 + while (sp < Source.Length) + { + // 对每个 sp 作为开始位置计算最大子串 + var len = 0; + while (qp + len < queryLength && sp + len < Source.Length && Source[sp + len] == Query[qp + len]) + len += 1; + // 存储 len + if (len > lenMax) + { + lenMax = len; + spMax = sp; + } + + // 根据结果增加 sp + sp += Math.Max(1, len); + } + + if (lenMax > 0) + { + Source = Source.Substring(0, spMax) + + (Source.Count() > spMax + lenMax + ? Source.Substring(spMax + lenMax) + : string.Empty); // 将源中的对应字段替换空 + // 存储 lenSum + var IncWeight = Math.Pow(1.4d, 3 + lenMax) - 3.6d; // 根据长度加成 + IncWeight *= 1d + 0.3d * Math.Max(0, 3 - Math.Abs(qp - spMax)); // 根据位置加成 + lenSum += IncWeight; + } + + // 根据结果增加 qp + qp += Math.Max(1, lenMax); + } + + // 计算结果:重复字段量 × 源长度影响比例 + return lenSum / queryLength * (3d / Math.Pow(sourceLength + 15, 0.5d)) * + (queryLength <= 2 ? 3 - queryLength : 1); + } + + /// + /// 获取多段文本加权后的相似度。 + /// + private static double SearchSimilarityWeighted(List> Source, string Query) + { + var TotalWeight = 0d; + var Sum = 0d; + foreach (var Pair in Source) + { + Sum += SearchSimilarity(Pair.Key, Query) * Pair.Value; + TotalWeight += Pair.Value; + } + + return Sum / TotalWeight; + } + + /// + /// 用于搜索的项目。 + /// + public class SearchEntry + { + /// + /// 是否完全匹配。 + /// + public bool AbsoluteRight; + + /// + /// 该项目对应的源数据。 + /// + public T Item; + + /// + /// 该项目用于搜索的源。 + /// + public List> SearchSource; + + /// + /// 相似度。 + /// + public double Similarity; + } + + /// + /// 进行多段文本加权搜索,获取相似度较高的数项结果。 + /// + /// 返回的最大模糊结果数。 + /// 返回结果要求的最低相似度。 + public static List> Search(List> Entries, string Query, int MaxBlurCount = 5, + double MinBlurSimilarity = 0.1d) + { + var ResultList = new List>(); + + if (Entries is null || !Entries.Any()) return ResultList; + + // Preprocess query into parts + var queryParts = Query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (queryParts.Length == 0) + { + ResultList.AddRange(Entries); + return ResultList; + } + + // Precompute query parts in lowercase for case-insensitive comparison + var queryPartsLower = queryParts.Select(q => q.ToLower()).ToArray(); + + // Process each entry to compute similarity and absolute match status + foreach (var Entry in Entries) + { + Entry.Similarity = SearchSimilarityWeighted(Entry.SearchSource, Query); + + // Preprocess search source keys: remove spaces and convert to lowercase + var processedSources = Entry.SearchSource.Select(s => s.Key.Replace(" ", "").ToLower()).ToList(); + + // Check if all query parts are matched exactly by at least one source + var isAbsoluteRight = true; + foreach (var qp in queryPartsLower) + { + var found = false; + foreach (var ps in processedSources) + if (ps.Contains(qp)) + { + found = true; + break; + } + + if (!found) + { + isAbsoluteRight = false; + break; + } + } + + Entry.AbsoluteRight = isAbsoluteRight; + } + + // Sort by absolute match (descending), then by similarity (descending) + var sortedEntries = Entries.OrderByDescending(e => e.AbsoluteRight).ThenByDescending(e => e.Similarity) + .ToList(); + + // Build the final result list + var blurCount = 0; + foreach (var Entry in sortedEntries) + if (Entry.AbsoluteRight) + { + ResultList.Add(Entry); + } + else + { + if (Entry.Similarity < MinBlurSimilarity || blurCount >= MaxBlurCount) break; + ResultList.Add(Entry); + blurCount += 1; + } + + return ResultList; + } + + #endregion + + #region 系统 + + public static bool IsUtf8CodePage() + { + return Encoding.Default.CodePage == 65001; + } + + /// + /// 线程安全的 List。 + /// 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。 + /// + public class SafeList : IEnumerable, IDisposable, ICollection + { + private readonly List _internalList; + private readonly ReaderWriterLockSlim _lock = new(); + + public SafeList() + { + _internalList = new List(); + } + + public SafeList(IEnumerable data) + { + _internalList = new List(data); + } + + public T this[int index] + { + get => _internalList[index]; + set => _internalList[index] = value; + } + + public void Add(T item) + { + _lock.EnterWriteLock(); + try + { + _internalList.Add(item); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public bool Remove(T item) + { + _lock.EnterWriteLock(); + try + { + return _internalList.Remove(item); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public void Clear() + { + _lock.EnterWriteLock(); + try + { + _internalList.Clear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + public int Count + { + get + { + _lock.EnterReadLock(); + try + { + return _internalList.Count; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + public bool IsReadOnly => ((ICollection)_internalList).IsReadOnly; + + public bool Contains(T item) + { + return ((ICollection)_internalList).Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + ((ICollection)_internalList).CopyTo(array, arrayIndex); + } + + public void Dispose() + { + _lock.Dispose(); + } + + public IEnumerator GetEnumerator() + { + return ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public List ToList() + { + _lock.EnterReadLock(); + try + { + return _internalList.ToList(); + } + finally + { + _lock.ExitReadLock(); + } + } + + public void RemoveAt(int index) + { + _lock.EnterWriteLock(); + try + { + _internalList.RemoveAt(index); + } + finally + { + _lock.ExitWriteLock(); + } + } + } + + /// + /// 线程安全的字典。 + /// 通过在 For Each 循环中使用一个浅表副本规避多线程操作或移除自身导致的异常。 + /// + public class SafeDictionary : IDictionary, IEnumerable> + { + private readonly Dictionary _Dictionary = new(); + + public readonly object SyncRoot = new(); + + // 构造函数 + public SafeDictionary() + { + } + + public SafeDictionary(IEnumerable> data) + { + foreach (var DataItem in data) + _Dictionary.Add(DataItem.Key, DataItem.Value); + } + + // 线程安全的方法实现 + public void Add(TKey key, TValue value) + { + lock (SyncRoot) + { + _Dictionary.Add(key, value); + } + } + + public bool ContainsKey(TKey key) + { + lock (SyncRoot) + { + return _Dictionary.ContainsKey(key); + } + } + + public ICollection Keys + { + get + { + lock (SyncRoot) + { + return new List(_Dictionary.Keys); + } + } + } + + public bool Remove(TKey key) + { + lock (SyncRoot) + { + return _Dictionary.Remove(key); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + lock (SyncRoot) + { + return _Dictionary.TryGetValue(key, out value); + } + } + + public ICollection Values + { + get + { + lock (SyncRoot) + { + return new List(_Dictionary.Values); + } + } + } + + public TValue this[TKey key] + { + get + { + lock (SyncRoot) + { + return _Dictionary[key]; + } + } + set + { + lock (SyncRoot) + { + _Dictionary[key] = value; + } + } + } + + public void Add(KeyValuePair item) + { + lock (SyncRoot) + { + _Dictionary.Add(item.Key, item.Value); + } + } + + public void Clear() + { + lock (SyncRoot) + { + _Dictionary.Clear(); + } + } + + public bool Contains(KeyValuePair item) + { + lock (SyncRoot) + { + return ((IDictionary)_Dictionary).Contains(item); + } + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + lock (SyncRoot) + { + ((IDictionary)_Dictionary).CopyTo(array, arrayIndex); + } + } + + public int Count + { + get + { + lock (SyncRoot) + { + return _Dictionary.Count; + } + } + } + + public bool IsReadOnly => false; + + public bool Remove(KeyValuePair item) + { + lock (SyncRoot) + { + return ((IDictionary)_Dictionary).Remove(item); + } + } + + // 枚举器 + public IEnumerator> GetEnumerator() + { + lock (SyncRoot) + { + return new List>(_Dictionary).GetEnumerator(); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumeratorGeneral(); + } + + private IEnumerator GetEnumeratorGeneral() + { + return GetEnumerator(); + } + } + + /// + /// 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。 + /// + public static string PathPure = GetPureASCIIDir(); + + private static string GetPureASCIIDir() + { + if (ExePath.IsASCII()) return ExePath + @"PCL\"; + + if (PathAppdata.IsASCII()) return PathAppdata; + + if (PathTemp.IsASCII()) return PathTemp; + + return OsDrive + @"ProgramData\PCL\"; + } + + /// + /// 指示接取到这个异常的函数进行重试。 + /// + public class RestartException : Exception + { + } + + /// + /// 指示用户手动取消了操作,或用户已知晓操作被取消的原因。 + /// + public class CancelledException : Exception + { + } + + /// + /// 判断对象是否为某个泛型类型的实例。 + /// + public static bool IsInstanceOfGenericType(this Type genericType, object obj) + { + if (obj is null) + return false; + var t = obj.GetType(); + while (t is not null) + { + if (t.IsGenericType && ReferenceEquals(t.GetGenericTypeDefinition(), genericType)) + return true; + t = t.BaseType; + } + + return false; + } + + private static int Uuid = 1; + private static object UuidLock; + + /// + /// 获取一个全程序内不会重复的数字(伪 Uuid)。 + /// + public static int GetUuid() + { + if (UuidLock is null) + UuidLock = new object(); + lock (UuidLock) + { + Uuid += 1; + return Uuid; + } + } + + /// + /// 将元素与 List 的混合体拆分为元素组。 + /// + public static List GetFullList(IList data) + { + List GetFullListRet = default; + GetFullListRet = new List(); + for (int i = 0, loopTo = data.Count - 1; i <= loopTo; i++) + if (data[i] is ICollection) + GetFullListRet.AddRange((IEnumerable)data[i]); + else + GetFullListRet.Add(Conversions.ToGenericParameter(data[i])); + + return GetFullListRet; + } + + /// + /// 数组去重。 + /// + public static List Distinct(this ICollection Arr, ComparisonBoolean IsEqual) + { + var ResultArray = new List(); + for (int i = 0, loopTo = Arr.Count - 1; i <= loopTo; i++) + { + for (int ii = i + 1, loopTo1 = Arr.Count - 1; ii <= loopTo1; ii++) + if (IsEqual(Arr.ElementAtOrDefault(i), Arr.ElementAtOrDefault(ii))) + goto NextElement; + ResultArray.Add(Arr.ElementAtOrDefault(i)); + NextElement: ; + } + + return ResultArray; + } + + /// + /// 对集合的每个元素执行指定操作。 + /// + public static IEnumerable ForEach(this IEnumerable Collection, Action Action) + { + foreach (var Item in Collection) + Action(Item); + return Collection; + } + + /// + /// 用于储存 RaiseByMouse 的 EventArgs。 + /// + public sealed class RouteEventArgs : EventArgs + { + public bool Handled = false; + public bool RaiseByMouse; + + public RouteEventArgs(bool RaiseByMouse = false) + { + this.RaiseByMouse = RaiseByMouse; + } + } + + /// + /// 前台运行文件。 + /// + /// 文件名。可以为“notepad”等缩写。 + /// 运行参数。 + public static void ShellOnly(string FileName, string Arguments = "") + { + try + { + FileName = ShortenPath(FileName); + using (var Program = new Process()) + { + Program.StartInfo.Arguments = Arguments; + Program.StartInfo.FileName = FileName; + Program.StartInfo.UseShellExecute = true; + Log("[System] 执行外部命令:" + FileName + " " + Arguments); + Program.Start(); + } + } + catch (Exception ex) + { + Log(ex, "打开文件或程序失败:" + FileName, LogLevel.Msgbox); + } + } + + /// + /// 前台运行文件并返回返回值。 + /// + /// 文件名。可以为“notepad”等缩写。 + /// 运行参数。 + /// 等待该程序结束的最长时间(毫秒)。超时会返回 Result.Timeout。 + public static ProcessReturnValues ShellAndGetExitCode(string FileName, string Arguments = "", int Timeout = 1000000) + { + try + { + using (var Program = new Process()) + { + Program.StartInfo.Arguments = Arguments; + Program.StartInfo.FileName = FileName; + Log("[System] 执行外部命令并等待返回码:" + FileName + " " + Arguments); + Program.Start(); + if (Program.WaitForExit(Timeout)) return (ProcessReturnValues)Program.ExitCode; + + return ProcessReturnValues.Timeout; + } + } + catch (Exception ex) + { + Log(ex, "执行命令失败:" + FileName, LogLevel.Msgbox); + return ProcessReturnValues.Fail; + } + } + + /// + /// 静默运行文件并返回输出流字符串。执行失败会抛出异常。 + /// + /// 文件名。可以为“notepad”等缩写。 + /// 运行参数。 + /// 等待该程序结束的最长时间(毫秒)。超时会抛出错误。 + public static string ShellAndGetOutput(string FileName, string Arguments = "", int Timeout = 1000000, + string WorkingDirectory = null) + { + var Info = new ProcessStartInfo + { + FileName = FileName, + Arguments = Arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + // 设置工作目录(如果提供) + if (!string.IsNullOrEmpty(WorkingDirectory)) Info.WorkingDirectory = WorkingDirectory.TrimEnd('\\'); + + Log("[System] 执行外部命令并等待返回结果:" + FileName + " " + Arguments); + + using (var Program = new Process { StartInfo = Info }) + { + Program.Start(); + + // 异步读取输出和错误流 + var outputTask = Program.StandardOutput.ReadToEndAsync(); + var errorTask = Program.StandardError.ReadToEndAsync(); + + // 等待进程退出或超时 + if (Program.WaitForExit(Timeout)) + { + // 确保异步读取完成 + Task.WaitAll(outputTask, errorTask); + } + else + { + // 超时后终止进程 + Program.Kill(); + // 仍然尝试获取已输出的内容 + Task.WaitAll(outputTask, errorTask); + } + + // 合并结果并返回 + return outputTask.Result + errorTask.Result; + } + } + + /// + /// 在新的工作线程中执行代码。 + /// + public static Thread RunInNewThread(Action Action, string Name = null, + ThreadPriority Priority = ThreadPriority.Normal) + { + var th = new Thread(() => + { + try + { + Action(); + } + catch (ThreadInterruptedException ex) + { + Log(Name + ":线程已中止"); + } + catch (Exception ex) + { + Log(ex, Name + ":线程执行失败", LogLevel.Feedback); + } + }) { Name = Name ?? "Runtime New Invoke " + GetUuid() + "#", Priority = Priority }; + th.Start(); + return th; + } + + /// + /// 确保在 UI 线程中执行代码。 + /// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。 + /// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用! + /// + public static Output RunInUiWait(Func Action) + { + if (RunInUi()) return Action(); + + return System.Windows.Application.Current.Dispatcher.Invoke(Action); + } + + /// + /// 确保在 UI 线程中执行代码。 + /// 如果当前并非 UI 线程,则会阻断当前线程,直至 UI 线程执行完毕。 + /// 为防止线程互锁,请仅在开始加载动画、从 UI 获取输入时使用! + /// + public static void RunInUiWait(Action Action) + { + if (System.Windows.Application.Current is null) + return; + if (RunInUi()) + Action(); + else + System.Windows.Application.Current.Dispatcher.Invoke(Action); + } + + /// + /// 确保在 UI 线程中执行代码,代码按触发顺序执行。 + /// 如果当前并非 UI 线程,也不阻断当前线程的执行。 + /// + public static void RunInUi(Action Action, bool ForceWaitUntilLoaded = false) + { + if (System.Windows.Application.Current is null) + return; + if (RunInUi()) + Action(); + else + System.Windows.Application.Current.Dispatcher.InvokeAsync(Action, + ForceWaitUntilLoaded ? DispatcherPriority.Loaded : DispatcherPriority.Normal); + } + + /// + /// 确保在工作线程中执行代码。 + /// + public static void RunInThread(Action Action) + { + if (RunInUi()) + RunInNewThread(Action, "Runtime Invoke " + GetUuid() + "#"); + else + Action(); + } + + /// + /// 使用优化的归并排序算法进行稳定排序。 + /// + /// 传入两个对象,若第一个对象应该排在前面,则返回 True。 + public static List Sort(this IList List, ComparisonBoolean SortRule) + { + // 创建原列表的副本以避免修改原始列表 + var tempList = new List(List); + if (tempList.Count <= 1) + return tempList; + + // 使用归并排序核心算法 + MergeSort_Sort(ref tempList, 0, tempList.Count - 1, SortRule); + return tempList; + } + + private static void MergeSort_Sort(ref List array, int left, int right, ComparisonBoolean comparator) + { + if (left >= right) + return; + + var mid = (left + right) / 2; + MergeSort_Sort(ref array, left, mid, comparator); + MergeSort_Sort(ref array, mid + 1, right, comparator); + MergeSort_Merge(ref array, left, mid, right, comparator); + } + + private static void MergeSort_Merge(ref List array, int left, int mid, int right, + ComparisonBoolean comparator) + { + var leftArray = new List(); + var rightArray = new List(); + + for (int i = left, loopTo = mid; i <= loopTo; i++) + leftArray.Add(array[i]); + + for (int j = mid + 1, loopTo1 = right; j <= loopTo1; j++) + rightArray.Add(array[j]); + + var leftPtr = 0; + var rightPtr = 0; + var current = left; + + while (leftPtr < leftArray.Count && rightPtr < rightArray.Count) + { + // 保持稳定性的关键比较逻辑:当相等时优先取左数组元素 + if (comparator(leftArray[leftPtr], rightArray[rightPtr])) + { + array[current] = leftArray[leftPtr]; + leftPtr += 1; + } + else + { + array[current] = rightArray[rightPtr]; + rightPtr += 1; + } + + current += 1; + } + + while (leftPtr < leftArray.Count) + { + array[current] = leftArray[leftPtr]; + leftPtr += 1; + current += 1; + } + + while (rightPtr < rightArray.Count) + { + array[current] = rightArray[rightPtr]; + rightPtr += 1; + current += 1; + } + } + + public delegate bool ComparisonBoolean(T Left, T Right); + + /// + /// 返回列表的浅表副本。 + /// + public static IList Clone(this IList list) + { + return new List(list); + } + + /// + /// 尝试从字典中获取某项,如果该项不存在,则返回默认值。 + /// + public static TValue GetOrDefault(this Dictionary Dict, TKey Key, + TValue DefaultValue = default) + { + if (Dict.ContainsKey(Key)) return Dict[Key]; + + return DefaultValue; + } + + /// + /// 将某项添加到以列表作为值的字典中。 + /// + public static void AddToList(this Dictionary> Dict, TKey Key, TValue Value) + { + if (Dict.ContainsKey(Key)) + Dict[Key].Add(Value); + else + Dict.Add(Key, new List { Value }); + } + + /// + /// 获取程序启动参数。 + /// + /// 参数名。 + /// 默认值。 + public static object GetProgramArgument(string Name, object DefaultValue = null) + { + var AllArguments = Interaction.Command().Split(" "); + for (int i = 0, loopTo = AllArguments.Length - 1; i <= loopTo; i++) + if ((AllArguments[i] ?? "") == ("-" + Name ?? "")) + { + if (AllArguments.Length == i + 1 || AllArguments[i + 1].StartsWithF("-")) + return true; + return AllArguments[i + 1]; + } + + return DefaultValue; + } + + /// + /// 打开网页。 + /// + public static void OpenWebsite(string Url) + { + try + { + if (!Url.StartsWithF("http", true) && !Url.StartsWithF("minecraft://", true)) + throw new Exception(Url + " 不是一个有效的网址,它必须以 http 开头!"); + Log("[System] 正在打开网页:" + Url); + Basics.OpenUri(Url); + } + catch (Exception ex) + { + Log(ex, "无法打开网页(" + Url + ")"); + ClipboardSet(Url, false); + ModMain.MyMsgBox( + "可能由于浏览器未正确配置,PCL 无法为你打开网页。" + "\r\n" + "网址已经复制到剪贴板,若有需要可以手动粘贴访问。" + "\r\n" + + $"网址:{Url}", "无法打开网页"); + } + } + + /// + /// 打开 explorer。 + /// 若不以 \ 结尾,则将视作文件路径,打开并选中此文件。 + /// + public static void OpenExplorer(string Location) + { + try + { + Location = ShortenPath(Location.Replace("/", @"\").Trim(' ', '"')); + Log("[System] 正在打开资源管理器:" + Location); + if (Location.EndsWithF(@"\")) + ShellOnly(Location); + else + ShellOnly("explorer", $"/select,\"{Location}\""); + } + catch (Exception ex) + { + Log(ex, "打开资源管理器失败,请尝试关闭安全软件(如 360 安全卫士)", LogLevel.Msgbox); + } + } + + /// + /// 设置剪贴板。将在另一线程运行,且不会抛出异常。 + /// + public static void ClipboardSet(string Text, bool ShowSuccessHint = true) + { + RunInThread(() => + { + var success = false; + + for (var attempt = 0; attempt <= 5; attempt++) + try + { + RunInUi(() => Clipboard.SetText(Text)); + success = true; + break; + } + catch (Exception ex) when (attempt < 5) + { + Thread.Sleep(20); + } + catch (Exception finalEx) + { + Log(finalEx, "剪贴板被占用,文本复制失败", LogLevel.Hint); + } + + if (success && ShowSuccessHint) RunInUi(() => ModMain.Hint("已成功复制!", ModMain.HintType.Finish)); + }); + } + + /// + /// 从剪切板粘贴文件或文件夹 + /// + /// 目标文件夹 + /// 是否粘贴文件 + /// 是否粘贴文件夹 + /// 总共粘贴的数量 + public static int PasteFileFromClipboard(string dest, bool copyFile = true, bool copyDir = true) + { + Log("[System] 从剪贴板粘贴文件到:" + dest); + try + { + var files = Clipboard.GetFileDropList(); + if (files.Count.Equals(0)) + { + Log("[System] 剪贴板内无文件可粘贴"); + return 0; + } + + var CopiedFiles = 0; + var CopiedFolders = 0; + foreach (var i in files) + { + if (copyFile && File.Exists(i)) // 文件 + try + { + var thisDest = dest + GetFileNameFromPath(i); + if (File.Exists(thisDest)) + { + Log("[System] 已存在同名文件:" + thisDest); + } + else + { + File.Copy(i, thisDest); + CopiedFiles += 1; + } + } + catch (Exception ex) + { + Log(ex, "[System] 复制文件时出错"); + continue; + } + + if (copyDir && Directory.Exists(i)) // 文件夹 + try + { + var thisDest = dest + GetFolderNameFromPath(i); + if (Directory.Exists(thisDest)) + { + Log("[System] 已存在同名文件夹:" + thisDest); + } + else + { + CopyDirectory(i, thisDest); + CopiedFolders += 1; + } + } + catch (Exception ex) + { + Log(ex, "[System] 复制文件时出错"); + } + } + + ModMain.Hint("[System] 已粘贴 " + CopiedFiles + " 个文件和 " + CopiedFolders + " 个文件夹"); + } + catch (Exception ex) + { + Log(ex, "[System] 从剪切板粘贴文件失败", LogLevel.Hint); + } + + return 0; + } + + /// + /// 获取程序打包资源的输入流。该资源必须声明为 Resource 类型,否则将会报错,Images + /// 和 Resources 目录已默认声明该类型。 + /// + public static Stream GetResourceStream(string path) + { + var resourceInfo = + System.Windows.Application.GetResourceStream(new Uri($"pack://application:,,,/{path}", UriKind.Absolute)); + return resourceInfo?.Stream; + } + + #endregion + + /// + /// 检查是否拥有某一文件夹的 I/O 权限。如果文件夹不存在,会返回 False。 + /// + public static bool CheckPermission(string Path) + { + try + { + if (string.IsNullOrEmpty(Path)) + return false; + if (!Path.EndsWithF(@"\")) + Path += @"\"; + if (Path.EndsWithF(@":\System Volume Information\") || Path.EndsWithF(@":\$RECYCLE.BIN\")) + return false; + if (!Directory.Exists(Path)) + return false; + var FileName = "CheckPermission" + GetUuid(); + if (File.Exists(Path + FileName)) + File.Delete(Path + FileName); + File.Create(Path + FileName).Dispose(); + File.Delete(Path + FileName); + return true; + } + catch (Exception ex) + { + Log(ex, "没有对文件夹 " + Path + " 的权限,请尝试以管理员权限运行 PCL"); + return false; + } + } + + /// + /// 检查是否拥有某一文件夹的 I/O 权限。如果出错,则抛出异常。 + /// + public static void CheckPermissionWithException(string Path) + { + if (string.IsNullOrWhiteSpace(Path)) + throw new ArgumentNullException("文件夹名不能为空!"); + if (!Path.EndsWithF(@"\")) + Path += @"\"; + if (!Directory.Exists(Path)) + throw new DirectoryNotFoundException("文件夹不存在!"); + if (File.Exists(Path + "CheckPermission")) + File.Delete(Path + "CheckPermission"); + File.Create(Path + "CheckPermission").Dispose(); + File.Delete(Path + "CheckPermission"); + } + + #region UI + + public static void SetLaunchFont(string FontName = null) + { + try + { + FontFamily TargetFont; + if (string.IsNullOrEmpty(FontName)) + TargetFont = new FontFamily(new Uri("pack://application:,,,/"), + "./Resources/#PCL English, Segoe UI, Microsoft YaHei UI"); + else + TargetFont = new FontFamily($"{FontName}, Segoe UI, Microsoft YaHei UI"); + System.Windows.Application.Current.Resources["LaunchFontFamily"] = TargetFont; + } + catch (Exception ex) + { + Log(ex, "设置字体失败", LogLevel.Hint); + } + } + + // 边距改变 + /// + /// 相对增减控件的左边距。 + /// + public static void DeltaLeft(FrameworkElement control, double newValue) + { + // 安全性检查 + DebugAssert(!double.IsNaN(newValue)); + DebugAssert(!double.IsInfinity(newValue)); + + if (control is Window) + // 窗口改变 + ((Window)control).Left += newValue; + else + // 根据 HorizontalAlignment 改变数值 + switch (control.HorizontalAlignment) + { + case HorizontalAlignment.Left: + case HorizontalAlignment.Stretch: + { + control.Margin = new Thickness(control.Margin.Left + newValue, control.Margin.Top, + control.Margin.Right, control.Margin.Bottom); + break; + } + case HorizontalAlignment.Right: + { + // control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom) + control.Margin = new Thickness(control.Margin.Left, control.Margin.Top, + control.Margin.Right - newValue, control.Margin.Bottom); + break; + } + + default: + { + DebugAssert(false); + break; + } + } + } + + /// + /// 设置控件的左边距。(仅针对置左控件) + /// + public static void SetLeft(FrameworkElement control, double newValue) + { + DebugAssert(control.HorizontalAlignment == HorizontalAlignment.Left); + control.Margin = new Thickness(newValue, control.Margin.Top, control.Margin.Right, control.Margin.Bottom); + } + + /// + /// 相对增减控件的上边距。 + /// + public static void DeltaTop(FrameworkElement control, double newValue) + { + // 安全性检查 + DebugAssert(!double.IsNaN(newValue)); + DebugAssert(!double.IsInfinity(newValue)); + + if (control is Window) + // 窗口改变 + ((Window)control).Top += newValue; + else + // 根据 VerticalAlignment 改变数值 + switch (control.VerticalAlignment) + { + case VerticalAlignment.Top: + { + control.Margin = new Thickness(control.Margin.Left, control.Margin.Top + newValue, + control.Margin.Right, control.Margin.Bottom); + break; + } + case VerticalAlignment.Bottom: + { + // control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, CType(control.Parent, Object).ActualWidth - control.ActualWidth - newValue, control.Margin.Bottom) + control.Margin = new Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, + control.Margin.Bottom - newValue); + break; + } + + default: + { + DebugAssert(false); + break; + } + } + + // If Double.IsNaN(newValue) OrElse Double.IsInfinity(newValue) Then Return '安全性检查 + // Select Case control.VerticalAlignment + // Case VerticalAlignment.Top, VerticalAlignment.Stretch, VerticalAlignment.Center + // control.Margin = New Thickness(control.Margin.Left, newValue, control.Margin.Right, control.Margin.Bottom) + // Case VerticalAlignment.Bottom + // control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, -newValue) + // 'control.Margin = New Thickness(control.Margin.Left, control.Margin.Top, control.Margin.Right, CType(control.Parent, Object).ActualHeight - control.ActualHeight - newValue) + // End Select + } + + /// + /// 设置控件的顶边距。(仅针对置上控件) + /// + public static void SetTop(FrameworkElement control, double newValue) + { + DebugAssert(control.VerticalAlignment == VerticalAlignment.Top); + control.Margin = new Thickness(control.Margin.Left, newValue, control.Margin.Right, control.Margin.Bottom); + } + + // DPI 转换 + public static readonly int DPI = (int)Math.Round(Graphics.FromHwnd(nint.Zero).DpiX); + + /// + /// 将经过 DPI 缩放的 WPF 尺寸转化为实际的像素尺寸。 + /// + public static double GetPixelSize(double WPFSize) + { + return WPFSize / 96d * DPI; + } + + /// + /// 将实际的像素尺寸转化为经过 DPI 缩放的 WPF 尺寸。 + /// + public static double GetWPFSize(double PixelSize) + { + return PixelSize * 96d / DPI; + } + + // UI 截图 + /// + /// 将某个控件的呈现转换为图片。 + /// + public static ImageBrush ControlBrush(FrameworkElement UI) + { + var Width = UI.ActualWidth; + var Height = UI.ActualHeight; + if (Width < 1d || Height < 1d) + return new ImageBrush(); + var bmp = new RenderTargetBitmap((int)Math.Round(GetPixelSize(Width)), (int)Math.Round(GetPixelSize(Height)), + DPI, DPI, PixelFormats.Pbgra32); + bmp.Render(UI); + return new ImageBrush(bmp); + } + + /// + /// 将某个控件的模拟呈现转换为图片。 + /// + public static ImageBrush ControlBrush(FrameworkElement UI, double Width, double Height, double Left = 0d, + double Top = 0d) + { + UI.Measure(new Size(Width, Height)); + UI.Arrange(new Rect(0d, 0d, Width, Height)); + var bmp = new RenderTargetBitmap((int)Math.Round(GetPixelSize(Width)), (int)Math.Round(GetPixelSize(Height)), + DPI, DPI, PixelFormats.Default); + bmp.Render(UI); + if (!(Left == 0d && Top == 0d)) + UI.Arrange(new Rect(Left, Top, Width, Height)); + return new ImageBrush(bmp); + } + + /// + /// 将 UI 内容固定为图片并进行 Clear。 + /// + public static void ControlFreeze(Panel UI) + { + UI.Background = ControlBrush(UI); + UI.Children.Clear(); + } + + /// + /// 将 UI 内容固定为图片并进行 Clear。 + /// + public static void ControlFreeze(Border UI) + { + UI.Background = ControlBrush(UI); + UI.Child = null; + } + + /// + /// 将 XML 转换为对应 UI 对象。 + /// + public static object GetObjectFromXML(XElement Str) + { + return GetObjectFromXML(Str.ToString()); + } + + /// + /// 将 XML 转换为对应 UI 对象。 + /// + public static object GetObjectFromXML(string Str) + { + using (var Stream = new MemoryStream(Encoding.UTF8.GetBytes(Str))) + { + // 类型检查 + using (var Reader = new XamlXmlReader(Stream)) + { + while (Reader.Read()) + { + foreach (var BlackListType in new[] + { + typeof(WebBrowser), typeof(Frame), typeof(MediaElement), typeof(ObjectDataProvider), + typeof(XamlReader), typeof(Window), typeof(XmlDataProvider) + }) + { + if (Reader.Type is not null && BlackListType.IsAssignableFrom(Reader.Type.UnderlyingType)) + throw new UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 类型。"); + if (Reader.Value is not null && Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Reader.Value, BlackListType.Name, false))) + throw new UnauthorizedAccessException($"不允许使用 {BlackListType.Name} 值。"); + } + + foreach (var BlackListMember in new[] { "Code", "FactoryMethod", "Static" }) + if (Reader.Member is not null && (Reader.Member.Name ?? "") == (BlackListMember ?? "")) + throw new UnauthorizedAccessException($"不允许使用 {BlackListMember} 成员。"); + } + } + + // 实际的加载 + Stream.Position = 0L; + using (var Writer = new StreamWriter(Stream)) + { + Writer.Write(Str); + Writer.Flush(); + Stream.Position = 0L; + return System.Windows.Markup.XamlReader.Load(Stream); + } + } + } + + private static readonly int UiThreadId = Thread.CurrentThread.ManagedThreadId; + + /// + /// 当前线程是否为主线程。 + /// + public static bool RunInUi() + { + return Thread.CurrentThread.ManagedThreadId == UiThreadId; + } + + #endregion + + #region Debug + + public static bool ModeDebug = false; + + // Log + public enum LogLevel + { + /// + /// 不提示,只记录日志。 + /// + Normal = 0, + + /// + /// 只提示开发者。 + /// + Developer = 1, + + /// + /// 只提示开发者与调试模式用户。 + /// + Debug = 2, + + /// + /// 弹出提示所有用户。 + /// + Hint = 3, + + /// + /// 弹窗,不要求反馈。 + /// + Msgbox = 4, + + /// + /// 弹窗,要求反馈。 + /// + Feedback = 5, + + /// + /// 弹出 Windows 原生弹窗,要求反馈。在无法保证 WPF 窗口能正常运行时使用此级别。 + /// 在第二次触发后会直接结束程序。 + /// + Critical = 6 + } + + private static bool IsCriticalErrorTriggered; + + /// + /// 输出 Log。 + /// + /// 如果要求弹窗,指定弹窗的标题。 + public static void Log(string Text, LogLevel Level = LogLevel.Normal, string Title = "出现错误") + { + // On Error Resume Next + // 放在最后会导致无法显示极端错误下的弹窗(如无法写入日志文件) + // 处理错误会导致再次调用 Log() 导致无限循环 + + // 输出日志 + if (new[] { LogLevel.Msgbox, LogLevel.Hint }.Contains(Level)) + LogWrapper.Warn(Text); + else if (LogLevel.Feedback == Level) + LogWrapper.Error(Text); + else if (LogLevel.Critical == Level) + LogWrapper.Fatal(Text); + else if (LogLevel.Debug == Level) + LogWrapper.Debug(Text); + else if (LogLevel.Developer == Level) + LogWrapper.Trace(Text); + else + LogWrapper.Info(Text); + + if (IsProgramEnded || Level == LogLevel.Normal) + return; + + // 去除前缀 + Text = Text.RegexReplace(@"\[[^\]]+?\] ", ""); + + // 输出提示 + switch (Level) + { + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + Case LogLevel.Developer + Hint("[开发者模式] " & Text, HintType.Info, False) + Case LogLevel.Debug + Hint("[调试模式] " & Text, HintType.Info, False) + */ /* TODO ERROR: Skipped ElseDirectiveTrivia + #Else + */ + case LogLevel.Developer: + { + break; + } + case LogLevel.Debug: + { + if (ModeDebug) + ModMain.Hint("[调试模式] " + Text, ModMain.HintType.Info, false); + break; + } + /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + case LogLevel.Hint: + { + ModMain.Hint(Text, ModMain.HintType.Critical, false); + break; + } + case LogLevel.Msgbox: + { + ModMain.MyMsgBox(Text, Title, IsWarn: true); + break; + } + case LogLevel.Feedback: + { + if (CanFeedback(false)) + { + if (ModMain.MyMsgBox(Text + "\r\n" + "\r\n" + "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", + Title, "反馈", "取消", IsWarn: true) == 1) + Feedback(false, true); + } + else + { + ModMain.MyMsgBox(Text + "\r\n" + "\r\n" + "将 PCL 更新至最新版或许可以解决这个问题……", Title, + IsWarn: true); + } + + break; + } + case LogLevel.Critical: + { + if (IsCriticalErrorTriggered) + { + FormMain.EndProgramForce(ProcessReturnValues.Exception); + return; + } + + IsCriticalErrorTriggered = true; + if (CanFeedback(false)) + { + if (Interaction.MsgBox(Text + "\r\n" + "\r\n" + "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", + (MsgBoxStyle)((int)MsgBoxStyle.Critical + (int)MsgBoxStyle.YesNo), Title) == + MsgBoxResult.Yes) + Feedback(false, true); + } + else + { + Interaction.MsgBox(Text + "\r\n" + "\r\n" + "将 PCL 更新至最新版或许可以解决这个问题……", + MsgBoxStyle.Critical, Title); + } + + break; + } + } + } + + /// + /// 输出错误信息。 + /// + /// 错误描述。会在处理时在末尾加入冒号。 + public static void Log(Exception Ex, string Desc, LogLevel Level = LogLevel.Debug, string Title = "出现错误") + { + // On Error Resume Next + if (Ex is ThreadInterruptedException) + return; + + // 获取错误信息 + var ExFull = Desc + ":" + Ex.Message; + + // 输出日志 + if (new[] { LogLevel.Msgbox, LogLevel.Hint }.Contains(Level)) + LogWrapper.Warn(Ex, Desc); + else if (LogLevel.Feedback == Level) + LogWrapper.Error(Ex, Desc); + else if (LogLevel.Critical == Level) + LogWrapper.Fatal(Ex, Desc); + else if (LogLevel.Debug == Level) + LogWrapper.Debug($"{Desc}:{Ex}"); + else if (LogLevel.Developer == Level) + LogWrapper.Trace($"{Desc}:{Ex}"); + else + LogWrapper.Error(Ex, Desc); + + if (IsProgramEnded) + return; + + if (Ex.GetType() == typeof(Win32Exception)) + ExFull += "\r\n" + "与系统底层交互失败,请尝试重新安装 .NET 8 解决此问题"; + + // 输出提示 + switch (Level) + { + case LogLevel.Normal: + { + break; + } + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + Case LogLevel.Developer + Dim ExLine As String = Desc & ":" & Ex.ToString() + Hint("[开发者模式] " & ExLine, HintType.Info, False) + Case LogLevel.Debug + Dim ExLine As String = Desc & ":" & Ex.ToString() + Hint("[调试模式] " & ExLine, HintType.Info, False) + */ /* TODO ERROR: Skipped ElseDirectiveTrivia + #Else + */ + case LogLevel.Developer: + { + break; + } + case LogLevel.Debug: + { + var ExLine = Desc + ":" + Ex; + if (ModeDebug) + ModMain.Hint("[调试模式] " + ExLine, ModMain.HintType.Info, false); + break; + } + /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + case LogLevel.Hint: + { + var ExLine = Desc + ":" + Ex; + ModMain.Hint(ExLine, ModMain.HintType.Critical, false); + break; + } + case LogLevel.Msgbox: + { + ModMain.MyMsgBox(ExFull, Title, IsWarn: true); + break; + } + case LogLevel.Feedback: + { + if (CanFeedback(false)) + { + if (ModMain.MyMsgBox(ExFull + "\r\n" + "\r\n" + "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", + Title, "反馈", "取消", IsWarn: true) == 1) + Feedback(false, true); + } + else + { + ModMain.MyMsgBox(ExFull + "\r\n" + "\r\n" + "将 PCL 更新至最新版或许可以解决这个问题……", Title, + IsWarn: true); + } + + break; + } + case LogLevel.Critical: + { + if (IsCriticalErrorTriggered) + { + FormMain.EndProgramForce(ProcessReturnValues.Exception); + return; + } + + IsCriticalErrorTriggered = true; + if (CanFeedback(false)) + { + if (Interaction.MsgBox( + ExFull + "\r\n" + "\r\n" + "是否反馈此问题?如果不反馈,这个问题可能永远无法得到解决!", + (MsgBoxStyle)((int)MsgBoxStyle.Critical + (int)MsgBoxStyle.YesNo), Title) == + MsgBoxResult.Yes) + Feedback(false, true); + } + else + { + Interaction.MsgBox(ExFull + "\r\n" + "\r\n" + "将 PCL 更新至最新版或许可以解决这个问题……", + MsgBoxStyle.Critical, Title); + } + + break; + } + } + } + + public static string Base64Decode(string Text) + { + if (string.IsNullOrWhiteSpace(Text)) + return ""; + var decodedBytes = Convert.FromBase64String(Text); + return Encoding.UTF8.GetString(decodedBytes); + } + + public static string Base64Encode(string Text) + { + var bytes = Encoding.UTF8.GetBytes(Text); + return Convert.ToBase64String(bytes); + } + + public static string Base64Encode(byte[] bytes) + { + return Convert.ToBase64String(bytes); + } + + // 反馈 + public static void Feedback(bool ShowMsgbox = true, bool ForceOpenLog = false) + { + // On Error Resume Next + FeedbackInfo(); + string currentDate; + currentDate = Strings.Format(DateTime.Now, "yyyy-M-dd"); + + if (ForceOpenLog || (ShowMsgbox && + ModMain.MyMsgBox( + "若你在汇报一个 Bug,请点击 打开文件夹 按钮,并上传 Launch-" + currentDate + "-[一串数字].log 中包含错误信息的文件。" + + "\r\n" + "游戏崩溃一般与启动器无关,请不要因为游戏崩溃而提交反馈。", "反馈提交提醒", "打开文件夹", "不需要") == + 1)) OpenExplorer(ExePath + @"PCL\Log\"); + OpenWebsite("https://github.com/PCL-Community/PCL2-CE/issues/"); + } + + public static bool CanFeedback(bool ShowHint) + { + var stat = ModSecret.GetVersionStatus(); + if (stat != ModSecret.VersionStatus.Latest) + { + if (ShowHint) + if (ModMain.MyMsgBox( + stat == ModSecret.VersionStatus.NotLatest + ? $"你的 PCL 不是最新版,因此无法提交反馈。{"\r\n"}请在更新后,确认该问题在最新版中依然存在,然后再提交反馈。" + : $"你的 PCL 检查更新失败,因此无法提交反馈。{"\r\n"}请连接到互联网,在检查更新后,确认该问题在最新版中依然存在,然后再提交反馈。", + "无法提交反馈", stat == ModSecret.VersionStatus.NotLatest ? "更新" : "重新检查更新", "取消") == 1) + ModMain.FrmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupUpdate); + + return false; + } + + return true; + } + + /// + /// 在日志中输出系统诊断信息。 + /// + public static void FeedbackInfo() + { + try + { + // Get system memory info + var phyRam = KernelInterop.GetPhysicalMemoryBytes(); + + // Calculate memory and DPI scale + var availableMb = phyRam.Available / 1024 / 1024; + var totalMb = phyRam.Total / 1024 / 1024; + var dpiScale = Math.Round(DPI / 96.0, 2); + + // Build diagnostic information string + var info = $"[System] Diagnostic Information:{"\r\n"}" + + $"OS: {RuntimeInformation.OSDescription} (32-bit: {Is32BitSystem}){"\r\n"}" + + $"Memory: {availableMb} MB / {totalMb} MB{"\r\n"}" + + $"DPI: {DPI} ({dpiScale * 100}%){"\r\n"}" + + $"MC Folder: {ModMinecraft.McFolderSelected ?? "Nothing"}{"\r\n"}" + + $"Executable Path: {ExePath}"; + + LogWrapper.Info(info); + } + catch (Exception ex) + { + // Basic fail-safe to replace "On Error Resume Next" + LogWrapper.Error(ex, "Failed to collect feedback information"); + } + } + + // 断言 + public static void DebugAssert(bool Exp) + { + if (!Exp) + throw new Exception("断言命中"); + } + + // 获取当前的堆栈信息 + public static string GetStackTrace() + { + var Stack = new StackTrace(); + return Stack.GetFrames().Skip(1).Select(f => f.GetMethod()) + .Select(f => f.Name + "(" + f.GetParameters().Select(p => p.ToString()).ToList().Join(", ") + ") - " + + f.Module).ToList().Join("\r\n") + .Replace("\r\n" + "\r\n", "\r\n"); + } + + #endregion +} + +#region WPF + +/// +/// 对数据绑定进行加法运算,使用参数决定加数。 +/// +public class AdditionConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return 0; + double before; + if (!double.TryParse(value.ToString(), out before)) + return 0; + var scale = 1d; + if (parameter is not null) + double.TryParse(parameter.ToString(), out scale); + return before + scale; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return Binding.DoNothing; + double before; + if (!double.TryParse(value.ToString(), out before)) + return Binding.DoNothing; + var scale = 1d; + if (parameter is not null) + double.TryParse(parameter.ToString(), out scale); + if (scale == 0d) + return Binding.DoNothing; + return before - scale; + } +} + +/// +/// 对数据绑定进行乘法运算,使用参数决定乘数。 +/// +public class MultiplicationConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return 0; + double before; + if (!double.TryParse(value.ToString(), out before)) + return 0; + var scale = 1d; + if (parameter is not null) + double.TryParse(parameter.ToString(), out scale); + return before * scale; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return Binding.DoNothing; + double before; + if (!double.TryParse(value.ToString(), out before)) + return Binding.DoNothing; + var scale = 1d; + if (parameter is not null) + double.TryParse(parameter.ToString(), out scale); + if (scale == 0d) + return Binding.DoNothing; + return before / scale; + } +} + +/// +/// 将取反的 Boolean 绑定到 Visibility。 +/// +public class InverseBooleanToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return Visibility.Visible; + bool boolValue; + return bool.TryParse(value.ToString(), out boolValue) + ? boolValue ? Visibility.Collapsed : Visibility.Visible + : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return false; + return value is Visibility + ? Operators.ConditionalCompareObjectNotEqual(value, Visibility.Visible, false) + : false; + } +} + +/// +/// 将 Boolean 取反。 +/// +public class InverseBooleanConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is null) + return false; + bool boolValue; + return bool.TryParse(value.ToString(), out boolValue) ? !boolValue : false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) return false; + + if (bool.TryParse(value.ToString(), out var result)) return !result; + + return false; + } +} + +#endregion \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index 31213dd29..84c8dd920 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -967,7 +967,7 @@ Public Module ModBase '确保目录存在 Directory.CreateDirectory(GetPathFromFullPath(FilePath)) '读取流 - Using fs As New FileStream(FilePath, FileMode.Create, FileAccess.Write) + Using fs As New FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.Read) fs.SetLength(0) Stream.CopyTo(fs) End Using diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.cs b/Plain Craft Launcher 2/Modules/Base/ModLoader.cs new file mode 100644 index 000000000..959336d6a --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.cs @@ -0,0 +1,1005 @@ +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils; +using System.Collections; +using System.IO; +using System.Windows.Shell; + +namespace PCL; + +public static class ModLoader +{ + public enum LoaderFolderRunType + { + RunOnUpdated, + ForceRun, + UpdateOnly + } + + // 任务栏进度条 + public static ModBase.SafeList LoaderTaskbar = new(); + public static double LoaderTaskbarProgress; // 平滑后的进度 + private static TaskbarItemProgressState LoaderTaskbarProgressLast = TaskbarItemProgressState.None; + + // 文件夹刷新类委托 + private static readonly Dictionary LoaderFolderDictionary = new(); + + public static void LoaderTaskbarAdd(LoaderCombo Loader) + { + if (ModMain.FrmSpeedLeft is not null) + ModMain.FrmSpeedLeft.TaskRemove(Loader); + LoaderTaskbar.Add(Loader); + ModBase.Log($"[Taskbar] {Loader.Name} 已加入任务列表"); + } + + public static void LoaderTaskbarProgressRefresh() + { + try + { + TaskbarItemProgressState NewState; + var NewProgress = LoaderTaskbarProgressGet(); + // 若单个任务已中止,或全部任务已完成,则刷新并移除 + foreach (var Task in LoaderTaskbar) + if (LoaderTaskbar.All(l => l.State != ModBase.LoadState.Loading) || + Task.State == ModBase.LoadState.Waiting || Task.State == ModBase.LoadState.Aborted) + { + ModMain.FrmSpeedLeft?.TaskRefresh(Task); + LoaderTaskbar.Remove(Task); + ModBase.Log($"[Taskbar] {Task.Name} 已移出任务列表"); + } + + // 更新平滑后的进度 + if (NewProgress <= 0d || NewProgress >= 1d || LoaderTaskbarProgress > NewProgress) + LoaderTaskbarProgress = NewProgress; + else + LoaderTaskbarProgress = LoaderTaskbarProgress * 0.9d + NewProgress * 0.1d; + ModBase.RunInUi(() => ModMain.FrmMain.BtnExtraDownload.Progress = LoaderTaskbarProgress); + // 更新任务栏信息 + if (!LoaderTaskbar.Any() || LoaderTaskbarProgress == 1d) + { + NewState = TaskbarItemProgressState.None; + } + else if (LoaderTaskbarProgress < 0.015d) + { + NewState = TaskbarItemProgressState.Indeterminate; + } + else + { + NewState = TaskbarItemProgressState.Normal; + ModMain.FrmMain.TaskbarItemInfo.ProgressValue = LoaderTaskbarProgress; + } + + if (LoaderTaskbarProgressLast != NewState) + { + LoaderTaskbarProgressLast = NewState; + ModMain.FrmMain.TaskbarItemInfo.ProgressState = NewState; + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新任务栏进度显示失败", ModBase.LogLevel.Feedback); + } + } + + public static double LoaderTaskbarProgressGet() + { + try + { + if (!LoaderTaskbar.Any()) + return 1d; + + return ModBase.MathClamp( + LoaderTaskbar.Select(l => l.Progress).Average(), + 0, + 1 + ); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取任务栏进度出错", ModBase.LogLevel.Feedback); + return 0.5d; + } + } + + /// + /// 执行以文件夹检测作为输入的加载器。加载器需以文件夹路径为输入值。 + /// 返回是否执行了加载器。 + /// + /// 用于检查文件夹修改的额外路径。该路径不会传入加载器。 + /// 如果不想要文件夹路径为输入值,则传入期望数据 + public static bool LoaderFolderRun(LoaderBase Loader, string FolderPath, LoaderFolderRunType Type, int MaxDepth = 0, + string ExtraPath = "", bool WaitForExit = false, object LoaderInput = null) + { + DirectoryInfo FolderInfo; + var Value = new LoaderFolderDictionaryEntry { FolderPath = FolderPath + ExtraPath, LastCheckTime = default }; + try + { + // 获取数据 + FolderInfo = new DirectoryInfo(FolderPath + ExtraPath); + Value.LastCheckTime = FolderInfo.Exists ? GetActualLastWriteTimeUtc(FolderInfo, MaxDepth) : null; + // 如果已经检查过,则跳过 + if (Type == LoaderFolderRunType.RunOnUpdated && LoaderFolderDictionary.ContainsKey(Loader)) + { + if (FolderInfo.Exists) + { + if (LoaderFolderDictionary[Loader].LastCheckTime is not null && + Value.Equals(LoaderFolderDictionary[Loader])) + return false; + } + else if (LoaderFolderDictionary[Loader].LastCheckTime is null) + { + return false; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "文件夹加载器启动检测出错"); + } + + // 写入检查数据 + LoaderFolderDictionary[Loader] = Value; + // 开始检查 + if (Type == LoaderFolderRunType.UpdateOnly) + return false; + if (WaitForExit) + Loader.WaitForExit(LoaderInput ?? FolderPath, IsForceRestart: true); + else + Loader.Start(LoaderInput ?? FolderPath, true); + return true; + } + + private static DateTime GetActualLastWriteTimeUtc(DirectoryInfo FolderInfo, int MaxDepth) + { + var Time = FolderInfo.LastWriteTimeUtc; + if (MaxDepth > 0) + foreach (var Folder in FolderInfo.EnumerateDirectories()) + { + var FolderTime = GetActualLastWriteTimeUtc(Folder, MaxDepth - 1); + if (FolderTime > Time) + Time = FolderTime; + } + + return Time; + } + + // 各类加载器 + /// + /// 加载器的统一基类。 + /// + public abstract class LoaderBase : ILoadingTrigger + { + public delegate void OnStateChangedThreadEventHandler(LoaderBase Loader, ModBase.LoadState NewState, + ModBase.LoadState OldState); + + public delegate void OnStateChangedUiEventHandler(LoaderBase Loader, ModBase.LoadState NewState, + ModBase.LoadState OldState); + + public delegate void PreviewFinishEventHandler(LoaderBase Loader); + + // 等待结束 + public const string WaitForExitTimeoutMessage = "等待加载器执行超时。"; + + /// + /// 用于状态改变检测的同步锁。 + /// + public readonly object LockState = new(); + + private MyLoading.MyLoadingState _LoadingState = MyLoading.MyLoadingState.Stop; + private double _Progress = -1; + private ModBase.LoadState _State = ModBase.LoadState.Waiting; + + /// + /// 使用 LoaderCombo 加载时,该任务是否会阻碍后续任务的进行。 + /// + public bool Block = true; + + public bool HasOnStateChangedThread; + + /// + /// 当前加载器是否由 IsForceRestart 强制调起。 + /// 这个属性自身不会干任何事,而是提供给加载器执行的函数,使得加载器调用另一个加载器时,可以继承强制重启属性。 + /// + public bool IsForceRestarting; + + /// + /// 加载器的名称。 + /// + public string Name; + + /// + /// 父加载器。 + /// + public LoaderBase Parent; + + /// + /// 该加载器是否显示在列表中。 + /// + public bool Show = true; + + // 基础属性 + /// + /// 加载器的标识编号。 + /// + public int Uuid = ModBase.GetUuid(); + + public LoaderBase() + { + Name = "未命名任务 " + Uuid + "#"; + } + + /// + /// 最上级的加载器。 + /// + public LoaderBase RealParent + { + get + { + LoaderBase RealParentRet = default; + try + { + RealParentRet = Parent; + while (RealParentRet is not null && RealParentRet.Parent is not null) + RealParentRet = RealParentRet.Parent; + } + catch (Exception ex) + { + ModBase.Log(ex, "获取父加载器失败(" + Name + ")", ModBase.LogLevel.Feedback); + return null; + } + + return RealParentRet; + } + } + + /// + /// 简易的在 UI 线程添加触发事件的方式。主要用于在新建 Loader 时直接使用 With 绑定事件,以及进行老代码兼容。 + /// + public Action OnStateChanged + { + set { OnStateChangedUi += (Loader, NewState, OldState) => value(Loader); } + } + + // 状态监控 + /// + /// 加载器的状态。 + /// + public ModBase.LoadState State + { + get => _State; + set + { + if (_State == value) + return; + var OldState = _State; + if (Conversions.ToBoolean(value == ModBase.LoadState.Finished && + (bool)Config.Debug.AddRandomDelay)) + Thread.Sleep(RandomUtils.NextInt(100, 2000)); + _State = value; + ModBase.Log("[Loader] 加载器 " + Name + " 状态改变:" + ModBase.GetStringFromEnum(value)); + // 实现 ILoadingTrigger 接口与 OnStateChanged 回调 + ModBase.RunInUi(() => + { + switch (value) + { + case ModBase.LoadState.Loading: + { + LoadingState = MyLoading.MyLoadingState.Run; + break; + } + case ModBase.LoadState.Failed: + { + LoadingState = MyLoading.MyLoadingState.Error; + break; + } + + default: + { + LoadingState = MyLoading.MyLoadingState.Stop; + break; + } + } + + OnStateChangedUi?.Invoke(this, value, OldState); + }); + if (HasOnStateChangedThread) + ModBase.RunInThread(() => OnStateChangedThread?.Invoke(this, value, OldState)); + } + } + + /// + /// 若加载器出错,可提供给外部参考的异常。 + /// + public Exception Error { get; set; } + + // 进度监控 + /// + /// 加载器的执行进度,为 0 至 1 的小数。 + /// + public virtual double Progress + { + get + { + switch (State) + { + case ModBase.LoadState.Waiting: + { + return 0d; + } + case ModBase.LoadState.Loading: + { + return _Progress == -1 ? 0.02d : _Progress; + } + + default: + { + return 1d; + } + } + } + set + { + if (_Progress == value) + return; + var OldValue = _Progress; + _Progress = value; + ProgressChanged?.Invoke(value, OldValue); + } + } + + /// + /// 计算总进度时的权重。它应该为预计时间(秒)。 + /// + public double ProgressWeight { get; set; } = 1d; + + public bool IsLoader { get; } = true; + + public MyLoading.MyLoadingState LoadingState + { + get => _LoadingState; + set + { + if (_LoadingState == value) + return; + var OldState = _LoadingState; + _LoadingState = value; + LoadingStateChanged?.Invoke(value, OldState); + } + } + + public event ILoadingTrigger.LoadingStateChangedEventHandler? LoadingStateChanged; + public event ILoadingTrigger.ProgressChangedEventHandler? ProgressChanged; + + public virtual void InitParent(LoaderBase Parent) + { + this.Parent = Parent; + } + + // 事件 + + /// + /// 当状态改变时,在工作线程触发代码。在添加事件后,必须将 HasOnStateChangedThread 设为 True。 + /// + public event OnStateChangedThreadEventHandler? OnStateChangedThread; + + /// + /// 当状态改变时,在 UI 线程触发代码。 + /// + public event OnStateChangedUiEventHandler? OnStateChangedUi; + + /// + /// 在加载器目标事件执行完成,加载器状态即将变为 Finish 时调用。可以视为扩展加载器目标事件。 + /// + public event PreviewFinishEventHandler? PreviewFinish; + + protected void RaisePreviewFinish() + { + PreviewFinish?.Invoke(this); + } + + // 状态变化 + public abstract void Start(object? Input = null, bool IsForceRestart = false); + public abstract void Abort(); + + /// + /// 无限期地等待加载器完成,直到结束或抛出异常。若加载器尚未开始,则会开始执行。 + /// + public void WaitForExit(object Input = null, LoaderBase LoaderToSyncProgress = null, + bool IsForceRestart = false) + { + Start(Input, IsForceRestart); + while (State == ModBase.LoadState.Loading) + { + if (LoaderToSyncProgress is not null) + LoaderToSyncProgress.Progress = Progress; + Thread.Sleep(10); + } + + if (State == ModBase.LoadState.Finished) + { + } + else if (State == ModBase.LoadState.Aborted) + { + throw new ThreadInterruptedException("加载器执行已中断。"); + } + else if (Error == null) + { + throw new Exception("未知错误!"); + } + else + { + throw new Exception(Error.Message, Error); + } // 保留调用堆栈,同时不影响信息输出与单元测试 + } + + /// + /// 等待加载器完成,直到结束、抛出异常或超时。若加载器尚未开始,则会开始执行。 + /// + /// 等待的超时时间,以毫秒为单位。 + /// 若执行超时,将会抛出的异常信息。 + public void WaitForExitTime(int Timeout, object Input = null, string TimeoutMessage = WaitForExitTimeoutMessage, + object LoaderToSyncProgress = null, bool IsForceRestart = false) + { + Start(Input, IsForceRestart); + while (State == ModBase.LoadState.Loading) + { + if (LoaderToSyncProgress is not null) + ((dynamic)LoaderToSyncProgress).Progress = Progress; + Thread.Sleep(10); + Timeout -= 10; + if (Timeout < 0) + throw new TimeoutException(TimeoutMessage); + } + + if (State == ModBase.LoadState.Finished) + { + } + else if (State == ModBase.LoadState.Aborted) + { + throw new ThreadInterruptedException("加载器执行已中断。"); + } + else if (Error == null) + { + throw new Exception("未知错误!"); + } + else + { + throw Error; + } + } + + // 相同重载 + public override bool Equals(object obj) + { + var @base = obj as LoaderBase; + return @base is not null && Uuid == @base.Uuid; + } + } + + // 说实话,我真的觉得 C# 应该学学 VB 的那种近乎 Java 泛型擦除的兼容性,省掉一堆麻烦 + public abstract class LoaderTask : LoaderBase + { + /// + /// 上次完成加载时的时间。 + /// + public long LastFinishedTime; + + /// + /// 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。 + /// + public Task? LastRunningTask; + + /// + /// 在输入相同时使用原有结果的超时,单位为毫秒。 + /// + public int ReloadTimeout = -1; + + // 状态指示 + /// + /// 当前执行线程是否应当中断。只应用在加载器的工作线程中判断,不可跨线程调用。 + /// + public bool IsAborted => IsAbortedWithThread(Task.CurrentId ?? -1); + + /// + /// 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。 + /// + public bool IsAbortedWithThread(int compareTaskId) + { + return LastRunningTask is null || compareTaskId != LastRunningTask.Id || + State == ModBase.LoadState.Aborted; + } + + public abstract bool ShouldStart(ref object? input, bool isForceRestart = false, bool ignoreReloadTimeout = false); + + // 装箱!装箱!装箱圣地! + public abstract object? StartGetInputNoType(object? input = null, Func? inputDelegate = null); + + } + + /// + /// 用于异步执行并监控单一函数的加载器。 + /// + public class LoaderTask : LoaderTask + { + // 输入输出 + public InputType Input; + protected internal Func? InputDelegate; + + // 执行事件 + protected internal Action> LoadDelegate; + public OutputType Output = default; + + private CancellationTokenSource? CancelToken; + + // 线程设定 + protected internal ThreadPriority ThreadPriority; + + public LoaderTask(string Name, Action> LoadDelegate, + Func? InputDelegate = null, ThreadPriority Priority = ThreadPriority.Normal) + { + this.Name = Name; + this.LoadDelegate = LoadDelegate; + this.InputDelegate = InputDelegate; + } + + // 获取输入 + public InputType? StartGetInput(InputType? Input = default, Func? InputDelegate = null) // InputDelegate 参数存在匿名调用 + { + InputDelegate ??= this.InputDelegate; + // 按照龙猫的逻辑,此处将 input 与默认值直接进行等价比较,若相等则认为 input 未传入具体值,而调用 inputDelegate 获取 + // 这种逻辑未考虑值类型恰好传入 default 值 (如 double 传了 0.0) 的情况,这是一个陷阱,可能会产生 undefined behavior + if (EqualityComparer.Default.Equals(Input, default) && InputDelegate is not null) + ModBase.RunInUiWait(() => Input = InputDelegate()); + return Input; + } + + public override object? StartGetInputNoType(object? Input = null, Func? InputDelegate = null) + { + return StartGetInput(Input == null ? default : (InputType?)Input, InputDelegate == null ? null : () => (InputType?)InputDelegate()); + } + + // 代码执行 + public override bool ShouldStart(ref object? Input, bool IsForceRestart = false, bool IgnoreReloadTimeout = false) + { + // 获取输入 + try + { + Input = StartGetInput(Conversions.ToGenericParameter(Input)); + } + catch (Exception ex) + { + ModBase.Log(ex, "加载输入获取失败(" + Name + ")", ModBase.LogLevel.Hint); + Error = ex; + lock (LockState) + { + State = ModBase.LoadState.Failed; + } + } + + // 检验输入以确定情况 + if (IsForceRestart) + return true; // 强制要求重启 + if (Input is null != this.Input is null || (Input is not null && !Input.Equals(this.Input))) + return true; // 输入不同 + if ((State == ModBase.LoadState.Loading || State == ModBase.LoadState.Finished) && (IgnoreReloadTimeout || + ReloadTimeout == -1 || LastFinishedTime == 0L || + TimeUtils.GetTimeTick() - LastFinishedTime < ReloadTimeout)) // 正在加载或已结束 + // 没有超时 + return false; // 则不重试 + + return true; + // 需要开始 + } + + public override void Start(object Input = null, bool IsForceRestart = false) + { + // 确认是否开始加载 + if (ShouldStart(ref Input, IsForceRestart)) + { + // 输入不同或失败,开始加载 + if (State == ModBase.LoadState.Loading) + TriggerThreadAbort(); + this.Input = Conversions.ToGenericParameter(Input); + lock (LockState) + { + State = ModBase.LoadState.Loading; + Progress = -1; + } + } + else return; + + // 如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted + // 如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted + LastRunningTask = Task.Run(() => + { + try + { + IsForceRestarting = IsForceRestart; + if (ModBase.ModeDebug) + ModBase.Log( + $"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已{(IsForceRestarting ? "强制" : "")}启动"); + LoadDelegate(this); + if (IsAborted) + { + ModBase.Log( + $"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{(LastRunningTask is null ? -1 : LastRunningTask.Id)})", + ModBase.LogLevel.Developer); + return; + } + + if (ModBase.ModeDebug) + ModBase.Log($"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已完成"); + RaisePreviewFinish(); + State = ModBase.LoadState.Finished; + LastFinishedTime = TimeUtils.GetTimeTick(); + } + catch (ModBase.CancelledException ex) + { + if (ModBase.ModeDebug) + ModBase.Log(ex, + $"加载线程 {Name} ({Task.CurrentId}) 已触发取消中断,已完成 {Math.Round(Progress * 100d)}%"); + if (!IsAborted) State = ModBase.LoadState.Aborted; + } + catch (ThreadInterruptedException ex) + { + if (ModBase.ModeDebug) + ModBase.Log(ex, + $"加载线程 {Name} ({Task.CurrentId}) 已触发线程中断,已完成 {Math.Round(Progress * 100d)}%"); + if (!IsAborted) State = ModBase.LoadState.Aborted; + } + catch (Exception ex) + { + if (IsAborted) return; + ModBase.Log(ex, + $"加载线程 {Name} ({Task.CurrentId}) 出错,已完成 {Math.Round(Progress * 100d)}%", + ModBase.LogLevel.Developer); + Error = ex; + State = ModBase.LoadState.Failed; + } + }, (CancelToken ??= new CancellationTokenSource()).Token); // 未中断,本次输出有效 + // LastRunningTask.Start(); // 不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted + } + + public override void Abort() + { + if (State != ModBase.LoadState.Loading) + return; + lock (LockState) + { + State = ModBase.LoadState.Aborted; + } + + TriggerThreadAbort(); + } + + private void TriggerThreadAbort() + { + if (LastRunningTask is null) return; + if (ModBase.ModeDebug) ModBase.Log($"[Loader] 加载线程 {Name} ({LastRunningTask.Id}) 已中断"); + if (!LastRunningTask.IsCompleted) CancelToken?.Cancel(); + LastRunningTask = null; + CancelToken = null; + } + } + + /// + /// 支持多个加载器连续运作的复合加载器。 + /// + public class LoaderCombo : LoaderBase + { + public InputType Input; + + public List Loaders = new(); + + public LoaderCombo(string Name, IEnumerable Loaders) + { + this.Loaders.Clear(); + foreach (var Loader in Loaders) + if (Loader is not null) + { + this.Loaders.Add(Loader); + Loader.OnStateChangedThread += SubTaskStateChanged; + Loader.HasOnStateChangedThread = true; + } + + InitParent(null); + this.Name = Name; + } + + public override double Progress + { + get + { + switch (State) + { + case ModBase.LoadState.Waiting: + { + return 0d; + } + case ModBase.LoadState.Loading: + { + var Total = 0d; + var Finished = 0d; + foreach (var Loader in Loaders) + { + Total += Loader.ProgressWeight; + Finished += Loader.ProgressWeight * Loader.Progress; + } + + if (Total == 0d) + return 0d; + return Finished / Total; + } + + default: + { + return 1d; + } + } + } + set => throw new Exception("多重加载器不支持设置进度"); + } + + public override void InitParent(LoaderBase Parent) + { + this.Parent = Parent; + foreach (var Loader in Loaders) + Loader.InitParent(this); + } + + public override void Start(object Input = null, bool IsForceRestart = false) + { + IsForceRestarting = IsForceRestart; + // 改变状态 + lock (LockState) + { + if (State == ModBase.LoadState.Loading) return; + + State = ModBase.LoadState.Loading; + } + + // 启动加载 + this.Input = Conversions.ToGenericParameter(Input); + if (IsForceRestart) + foreach (var Loader in Loaders) + Loader.State = ModBase.LoadState.Waiting; + ModBase.RunInThread(Update); + } + + public override void Abort() + { + // 改变状态 + lock (LockState) + { + if (State == ModBase.LoadState.Loading || State == ModBase.LoadState.Waiting) + State = ModBase.LoadState.Aborted; + else + return; + } + + // 中断加载器 + ModBase.RunInThread(() => + { + foreach (var Loader in Loaders) Loader.Abort(); + }); + } + + /// + /// 子任务状态变更。 + /// + private void SubTaskStateChanged(LoaderBase Loader, ModBase.LoadState NewState, ModBase.LoadState OldState) + { + switch (NewState) + { + case ModBase.LoadState.Loading: + { + break; + } + // 开始,啥都不干 + case ModBase.LoadState.Waiting: + { + break; + } + // 子加载器可能由于外部输入改变而暂时变为 Waiting,之后会立即重新启动 + // 所以啥都不干就行 + case ModBase.LoadState.Finished: + { + // 正常结束,触发刷新 + Update(); + break; + } + case ModBase.LoadState.Aborted: + { + // 被中断,这个任务也中断 + Abort(); + break; + } + + default: + { + // 完蛋,出错了 + lock (LockState) + { + if (State >= ModBase.LoadState.Finished) + return; + Error = new Exception(Loader.Name + "失败", Loader.Error); + State = Loader.State; + } + + foreach (var currentLoader in Loaders) + { + Loader = currentLoader; + Loader.Abort(); + } + + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + return; + } + } + } + + /// + /// 触发一次更新,以启动新加载器或完成。 + /// + private void Update() + { + if (State == ModBase.LoadState.Finished + || State == ModBase.LoadState.Failed + || State == ModBase.LoadState.Aborted) + return; + + var isFinished = true; + var blocked = false; + object input = Input; + + foreach (var loader in Loaders) + switch (loader.State) + { + case ModBase.LoadState.Finished: + { + if (loader.GetType().Name.StartsWithF("LoaderTask")) + { + var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault(); + var shouldInput = input != null && genericArg == input.GetType() + ? input + : null; + + if (((dynamic)loader).ShouldStart(ref shouldInput, false, true)) + { + ModBase.Log("[Loader] 由于输入条件变更,重启已完成的加载器 " + loader.Name); + goto Restart; + } + + input = ((dynamic)loader).Output; + } + + if (loader.Block && !isFinished) + blocked = true; + + break; + } + + case ModBase.LoadState.Loading: + { + if (loader.GetType().Name.StartsWithF("LoaderTask")) + { + var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault(); + var shouldInput = input != null && genericArg == input.GetType() + ? input + : null; + + if (((dynamic)loader).ShouldStart(ref shouldInput, false, true)) + { + ModBase.Log("[Loader] 由于输入条件变更,重启进行中的加载器 " + + loader.Name, + ModBase.LogLevel.Developer); + goto Restart; + } + } + + isFinished = false; + blocked = true; + break; + } + + default: + + Restart: + + isFinished = false; + + if (blocked) + continue; + + if (input != null) + { + var loaderType = loader.GetType().Name; + + if (loaderType.StartsWithF("LoaderTask") + || loaderType.StartsWithF("LoaderCombo")) + { + var genericArg = loader.GetType().GenericTypeArguments.FirstOrDefault(); + + loader.Start( + genericArg == input.GetType() ? input : null, + IsForceRestarting); + } + else if (loaderType.StartsWithF("LoaderDownload")) + { + loader.Start( + input is List ? input : null, + IsForceRestarting); + } + else + { + throw new Exception("未知的加载器类型(" + loaderType + ")"); + } + } + else + { + loader.Start(IsForceRestart: IsForceRestarting); + } + + if (loader.Block) + blocked = true; + + break; + } + + if (isFinished) + { + RaisePreviewFinish(); + State = ModBase.LoadState.Finished; + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + } + } + + /// + /// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。 + /// + public static void GetLoaderList(object Loader, ref List List, bool RequireShow = true) + { + foreach (var SubLoader in (IEnumerable)((dynamic)Loader).Loaders) + { + if (Conversions.ToBoolean(((dynamic)SubLoader).Show || !RequireShow)) + List.Add((LoaderBase)SubLoader); + if (SubLoader.GetType().Name.StartsWithF("LoaderCombo")) + GetLoaderList(SubLoader, ref List); + } + } + + /// + /// 获得最底层的,应被显示给用户的加载器列表,并追加于 List。 + /// + public void GetLoaderList(ref List List, bool RequireShow = true) + { + GetLoaderList(this, ref List, RequireShow); + } + + /// + /// 获得最底层的,应被显示给用户的加载器列表。 + /// + public List GetLoaderList(bool RequireShow = true) + { + var List = new List(); + GetLoaderList(ref List, RequireShow); + return List; + } + } + + private struct LoaderFolderDictionaryEntry + { + public DateTime? LastCheckTime; + public string FolderPath; + + public override bool Equals(object obj) + { + if (!(obj is LoaderFolderDictionaryEntry)) + return false; + var entry = (LoaderFolderDictionaryEntry)obj; + return EqualityComparer.Default.Equals(LastCheckTime, entry.LastCheckTime) && + (FolderPath ?? "") == (entry.FolderPath ?? ""); + } + } +} diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb index eb4167aaf..27796682e 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb @@ -232,8 +232,6 @@ Public Module ModLoader Public Class LoaderTask(Of InputType, OutputType) Inherits LoaderBase - '线程设定 - Protected Friend ThreadPriority As ThreadPriority '执行事件 Protected Friend LoadDelegate As Action(Of LoaderTask(Of InputType, OutputType)) Protected Friend InputDelegate As Func(Of Object) @@ -243,14 +241,16 @@ Public Module ModLoader ''' Public ReadOnly Property IsAborted As Boolean Get - Return IsAbortedWithThread(Thread.CurrentThread) + Return IsAbortedWithThread(If(Task.CurrentId, -1)) End Get End Property ''' ''' 当前执行线程是否应当中断。需要手动提供加载器线程,用于需要跨线程检查的情况。 ''' - Public Function IsAbortedWithThread(Thread As Thread) As Boolean - Return LastRunningThread Is Nothing OrElse Not ReferenceEquals(Thread, LastRunningThread) OrElse State = LoadState.Aborted + Public Function IsAbortedWithThread(compareTaskId As Integer) As Boolean + Return LastRunningTask Is Nothing OrElse + compareTaskId <> LastRunningTask.Id OrElse + State = LoadState.Aborted End Function ''' ''' 在输入相同时使用原有结果的超时,单位为毫秒。 @@ -263,7 +263,8 @@ Public Module ModLoader ''' ''' 最后一次运行加载器的线程。可能为 Nothing,或线程已结束。 ''' - Public LastRunningThread As Thread = Nothing + Public LastRunningTask As Task = Nothing + Private CancelToken As CancellationTokenSource = Nothing '输入输出 Public Input As InputType = Nothing Public Output As OutputType = Nothing @@ -313,49 +314,52 @@ Public Module ModLoader Return End If - LastRunningThread = New Thread( + CancelToken = New CancellationTokenSource() + LastRunningTask = Task.Run( Sub() Try IsForceRestarting = IsForceRestart - If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动") + If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已{If(IsForceRestarting, "强制", "")}启动") LoadDelegate(Me) If IsAborted Then - Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer) + Log($"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningTask Is Nothing, -1, LastRunningTask.Id)})", LogLevel.Developer) Return End If - If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成") + If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({Task.CurrentId}) 已完成") RaisePreviewFinish() State = LoadState.Finished LastFinishedTime = TimeUtils.GetTimeTick() '未中断,本次输出有效 Catch ex As CancelledException - If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发取消中断,已完成 {Math.Round(Progress * 100)}%") + If ModeDebug Then Log(ex, $"加载线程 {Name} ({Task.CurrentId}) 已触发取消中断,已完成 {Math.Round(Progress * 100)}%") If Not IsAborted Then State = LoadState.Aborted Catch ex As ThreadInterruptedException - If ModeDebug Then Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已触发线程中断,已完成 {Math.Round(Progress * 100)}%") + If ModeDebug Then Log(ex, $"加载线程 {Name} ({Task.CurrentId}) 已触发线程中断,已完成 {Math.Round(Progress * 100)}%") '如果线程是因为判断到 IsAborted 而提前中止,则代表已有新线程被重启,此时不应当改为 Aborted '如果线程是在没有 IsAborted 时手动引发了 ThreadInterruptedException,则代表没有重启线程,这通常代表用户手动取消,应当改为 Aborted If Not IsAborted Then State = LoadState.Aborted Catch ex As Exception If IsAborted Then Return - Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer) + Log(ex, $"加载线程 {Name} ({Task.CurrentId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer) [Error] = ex State = LoadState.Failed End Try - End Sub) With {.Name = Name, .Priority = ThreadPriority} - LastRunningThread.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted + End Sub, CancelToken.Token) + 'LastRunningTask.Start() '不能使用 RunInNewThread,否则在函数返回前线程就会运行完,导致误判 IsAborted End Sub Public Overrides Sub Abort() If State <> LoadState.Loading Then Return SyncLock LockState State = LoadState.Aborted End SyncLock + TriggerThreadAbort() End Sub Private Sub TriggerThreadAbort() - If LastRunningThread Is Nothing Then Return - If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({LastRunningThread.ManagedThreadId}) 已中断") - If LastRunningThread.IsAlive Then LastRunningThread.Interrupt() - LastRunningThread = Nothing + If LastRunningTask Is Nothing Then Return + If ModeDebug Then Log($"[Loader] 加载线程 {Name} ({LastRunningTask.Id}) 已中断") + If Not LastRunningTask.IsCompleted Then CancelToken.Cancel() + LastRunningTask = Nothing + CancelToken = Nothing End Sub Public Sub New() @@ -365,7 +369,6 @@ Public Module ModLoader Me.Name = Name Me.LoadDelegate = LoadDelegate Me.InputDelegate = InputDelegate - ThreadPriority = Priority End Sub End Class @@ -606,11 +609,11 @@ Restart: '更新任务栏信息 If Not LoaderTaskbar.Any() OrElse LoaderTaskbarProgress = 1 Then NewState = Shell.TaskbarItemProgressState.None - ElseIf LoaderTaskbarProgress < 0.015 Then - NewState = Shell.TaskbarItemProgressState.Indeterminate + ElseIf LoaderTaskbarProgress <0.015 Then + NewState= Shell.TaskbarItemProgressState.Indeterminate Else - NewState = Shell.TaskbarItemProgressState.Normal - FrmMain.TaskbarItemInfo.ProgressValue = LoaderTaskbarProgress + NewState= Shell.TaskbarItemProgressState.Normal + FrmMain.TaskbarItemInfo.ProgressValue= LoaderTaskbarProgress End If If LoaderTaskbarProgressLast <> NewState Then LoaderTaskbarProgressLast = NewState diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.cs b/Plain Craft Launcher 2/Modules/Base/ModNet.cs new file mode 100644 index 000000000..62e5f8ce7 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.cs @@ -0,0 +1,2816 @@ +using System.Buffers; +using System.Collections; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.NetworkInformation; +using System.Text; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.IO.Net; +using PCL.Core.Logging; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public static class ModNet +{ + /// + /// 预下载检查行为。 + /// + public enum NetPreDownloadBehaviour + { + /// + /// 当文件已存在时,显示提示以提醒用户是否继续下载。 + /// + HintWhileExists, + + /// + /// 当文件已存在或正在下载时,直接退出下载函数执行,不对用户进行提示。 + /// + ExitWhileExistsOrDownloading, + + /// + /// 不进行已存在检查。 + /// + IgnoreCheck + } + + /// + /// 下载进度标示。 + /// + public enum NetState + { + /// + /// 尚未进行已存在检查。 + /// + WaitingToCheck = -1, + + /// + /// 尚未开始。 + /// + WaitingToDownload = 0, + + /// + /// 正在连接,尚未获取文件大小。 + /// + Connecting = 1, + + /// + /// 已获取文件大小,尚未有有效下载。 + /// + Reading = 2, + + /// + /// 正在下载。 + /// + Downloading = 3, + + /// + /// 正在合并文件。 + /// + Merging = 4, + + /// + /// 已完成。 + /// + Finished = 5, + + /// + /// 已失败或中断。 + /// + Interrupted = 6 + } + + public const string NetDownloadEnd = ".PCLDownloading"; + + /// + /// 最大线程数。 + /// + public static int NetTaskThreadLimit; + + /// + /// 速度下限。 + /// + public static long NetTaskSpeedLimitLow = 256L * 1024L; // 256K/s + + /// + /// 速度上限。若无限制则为 -1。 + /// + public static long NetTaskSpeedLimitHigh = -1; + + /// + /// 基于限速,当前可以下载的剩余量。 + /// + public static long NetTaskSpeedLimitLeft = -1; + + private static readonly object NetTaskSpeedLimitLeftLock = new(); + private static long NetTaskSpeedLimitLeftLast; + + /// + /// 正在运行中的线程数。 + /// + public static int NetTaskThreadCount; + + private static readonly object NetTaskThreadCountLock = new(); + + // 快速进行大小校验 + private static readonly ModBase.SafeDictionary _CheckExistingFile_Sizes = new(); + public static NetManagerClass NetManager = new(); + + /// + /// 测试 Ping。失败则返回 -1。 + /// + public static int Ping(string Ip, int Timeout = 10000, bool MakeLog = true) + { + PingReply PingResult; + try + { + PingResult = new Ping().Send(Ip); + } + catch (Exception ex) + { + if (MakeLog) + ModBase.Log("[Net] Ping " + Ip + " 失败:" + ex.Message); + return -1; + } + + if (PingResult.Status == IPStatus.Success) + { + if (MakeLog) + ModBase.Log("[Net] Ping " + Ip + " 结束:" + PingResult.RoundtripTime + "ms"); + return (int)PingResult.RoundtripTime; + } + + if (MakeLog) + ModBase.Log("[Net] Ping " + Ip + " 失败"); + return -1; + } + + /// + /// 的改进版,将抛出附带 StatusCodeReasonPhrase + /// 属性的异常。 + /// 这个改进已经在 .NET 5 官方实装,鬼知道为什么 .NET Framework 连最新的 4.8.1 都这么原始。 + /// + /// HTTP 响应失败 + private static void EnsureSuccessStatusCode(HttpResponseMessage response) + { + if (!response.IsSuccessStatusCode) + { + var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + response.Content?.Dispose(); + throw new HttpRequestFailedException(response, content); + } + } + + /// + /// 以 WebRequest 获取网页源代码或 Json。会进行至多 45 秒 3 次的尝试,允许最长 30s 的超时。 + /// + /// 网页的 Url。 + /// 网页的编码,通常为 UTF-8。 + /// 如果第一次尝试失败,换用的备用 URL。 + /// 是否解析为 Json。 + /// 请求的套接字类型。 + /// 是否使用浏览器 User-Agent。 + public static object NetGetCodeByRequestRetry(string Url, Encoding Encode = null, string Accept = "", + bool IsJson = false, string BackupUrl = null, bool UseBrowserUserAgent = false) + { + var RetryCount = 0; + Exception RetryException = null; + var StartTime = TimeUtils.GetTimeTick(); + while (RetryCount <= 3) + { + RetryCount += 1; + try + { + switch (RetryCount) + { + case 0: // 正常尝试 + { + return NetGetCodeByRequestOnce(Url, Encode, 10000, IsJson, Accept, UseBrowserUserAgent); + } + case 1: // 慢速重试 + { + Thread.Sleep(500); + return NetGetCodeByRequestOnce(BackupUrl ?? Url, Encode, 30000, IsJson, Accept, + UseBrowserUserAgent); // 快速重试 + } + + default: + { + if (TimeUtils.GetTimeTick() - StartTime > 5500) + { + // 若前两次加载耗费 5 秒以上,才进行重试 + Thread.Sleep(500); + return NetGetCodeByRequestOnce(BackupUrl ?? Url, Encode, 4000, IsJson, Accept, + UseBrowserUserAgent); + } + + throw RetryException; + } + } + } + catch (ThreadInterruptedException ex) + { + throw; + } + catch (Exception ex) + { + RetryException = ex; + } + } + + throw RetryException; + } + + public static object NetGetCodeByRequestOnce(string Url, Encoding Encode = null, int Timeout = 30000, + bool IsJson = false, string Accept = "", bool UseBrowserUserAgent = false) + { + if (ModBase.RunInUi() && !Url.Contains("//127.")) + throw new Exception("在 UI 线程执行了网络请求"); + try + { + Url = Conversions.ToString(ModSecret.SecretCdnSign(Url)); + ModBase.Log($"[Net] 获取网络结果:{Url},超时 {Timeout}ms{(IsJson ? ",要求 Json" : "")}"); + using (var cts = new CancellationTokenSource()) + { + cts.CancelAfter(Timeout); + using (var request = new HttpRequestMessage(HttpMethod.Get, Url)) + { + request.Headers.Accept.ParseAdd(Accept); + var argClient = request; + ModSecret.SecretHeadersSign(Url, ref argClient, UseBrowserUserAgent); + using (var response = NetworkService.GetClient() + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token).GetAwaiter() + .GetResult()) + { + EnsureSuccessStatusCode(response); + if (Encode is null) + Encode = Encoding.UTF8; + using (var responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + { + // 读取流并转换为字符串 + using (var reader = new StreamReader(responseStream, Encode)) + { + var content = reader.ReadToEnd(); + if (string.IsNullOrEmpty(content)) + throw new WebException("获取结果失败,内容为空(" + Url + ")"); + return IsJson ? ModBase.GetJson(content) : content; + } + } + } + } + } + } + catch (TaskCanceledException ex) + { + throw new TimeoutException("连接服务器超时(" + Url + ")", ex); + } + catch (HttpRequestFailedException ex) + { + throw new HttpWebException("获取结果失败," + ex.Message + "(" + Url + ")", ex); + } + catch (Exception ex) + { + throw new WebException("获取结果失败," + ex.Message + "(" + Url + ")", ex); + } + } + + /// + /// 以多线程下载网页文件的方式获取网页源代码。 + /// + /// 网页的 Url。 + public static string NetGetCodeByLoader(string Url, int Timeout = 45000, bool IsJson = false, + bool UseBrowserUserAgent = false) + { + string NetGetCodeByLoaderRet = default; + var Temp = ModMain.RequestTaskTempFolder() + "download.txt"; + var NewTask = new LoaderDownload("源码获取 " + ModBase.GetUuid() + "#", + new List + { new(new[] { Url }, Temp, new ModBase.FileChecker { IsJson = IsJson }, UseBrowserUserAgent) }); + try + { + NewTask.WaitForExitTime(Timeout, TimeoutMessage: "连接服务器超时(" + Url + ")"); + NetGetCodeByLoaderRet = ModBase.ReadFile(Temp); + File.Delete(Temp); + } + finally + { + NewTask.Abort(); + } + + return NetGetCodeByLoaderRet; + } + + /// + /// 以多线程下载网页文件的方式获取网页源代码。 + /// + /// 网页的 Url 列表。 + public static string NetGetCodeByLoader(IEnumerable Urls, int Timeout = 45000, bool IsJson = false, + bool UseBrowserUserAgent = false) + { + string NetGetCodeByLoaderRet = default; + var Temp = ModMain.RequestTaskTempFolder() + "download.txt"; + var NewTask = new LoaderDownload("源码获取 " + ModBase.GetUuid() + "#", + new List { new(Urls, Temp, new ModBase.FileChecker { IsJson = IsJson }, UseBrowserUserAgent) }); + try + { + NewTask.WaitForExitTime(Timeout, TimeoutMessage: "连接服务器超时(第一下载源:" + Urls.First() + ")"); + NetGetCodeByLoaderRet = ModBase.ReadFile(Temp); + File.Delete(Temp); + } + finally + { + NewTask.Abort(); + } + + return NetGetCodeByLoaderRet; + } + + /// + /// 使用 HttpClient 从网络中下载文件。这不能下载 CDN 中的文件。 + /// + /// 网络 Url。 + /// 下载的本地地址。 + public static async Task NetDownloadByClient(string Url, string LocalFile, bool UseBrowserUserAgent = false) + { + ModBase.Log("[Net] 直接下载文件:" + Url); + try + { + Directory.CreateDirectory(ModBase.GetPathFromFullPath(LocalFile)); + if (File.Exists(LocalFile)) + File.Delete(LocalFile); + using (var request = new HttpRequestMessage(HttpMethod.Get, Url)) + { + var argClient = request; + ModSecret.SecretHeadersSign(Url, ref argClient, UseBrowserUserAgent); + using (var response = await NetworkService.GetClient() + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) + { + EnsureSuccessStatusCode(response); + using (var httpStream = await response.Content.ReadAsStreamAsync()) + { + using (var fileStream = new FileStream(LocalFile, FileMode.Create)) + { + await httpStream.CopyToAsync(fileStream); + } + } + } + } + } + catch (TaskCanceledException ex) when (ex.InnerException is null) + { + throw new TimeoutException($"下载超时({Url})", ex); + } + catch (HttpRequestFailedException ex) + { + throw new HttpWebException($"下载失败:{ex.Message}({Url})", ex); + } + catch (Exception ex) + { + if (File.Exists(LocalFile)) + File.Delete(LocalFile); + throw new WebException($"下载失败:{ex.Message}({Url})", ex); + } + } + + /// + /// 简单的多线程下载文件。可以下载 CDN 中的文件。 + /// + /// 文件的 Url。 + /// 下载的本地地址。 + public static void NetDownloadByLoader(string Url, string LocalFile, + ModLoader.LoaderBase LoaderToSyncProgress = null, ModBase.FileChecker Check = null, + bool UseBrowserUserAgent = false) + { + var NewTask = new LoaderDownload("文件下载 " + ModBase.GetUuid() + "#", + new List { new(new[] { Url }, LocalFile, Check, UseBrowserUserAgent) }); + try + { + NewTask.WaitForExit(LoaderToSyncProgress: LoaderToSyncProgress); + } + catch (Exception ex) + { + throw new WebException($"多线程直接下载文件失败({Url})", ex); + } + finally + { + NewTask.Abort(); + } + } + + /// + /// 简单的多线程下载文件。可以下载 CDN 中的文件。 + /// + /// 文件的 Url 列表。 + /// 下载的本地地址。 + public static void NetDownloadByLoader(IEnumerable Urls, string LocalFile, + ModLoader.LoaderBase LoaderToSyncProgress = null, ModBase.FileChecker Check = null, + bool UseBrowserUserAgent = false) + { + var NewTask = new LoaderDownload("文件下载 " + ModBase.GetUuid() + "#", + new List { new(Urls, LocalFile, Check, UseBrowserUserAgent) }); + try + { + NewTask.WaitForExit(LoaderToSyncProgress: LoaderToSyncProgress); + } + catch (Exception ex) + { + throw new WebException("多线程直接下载文件失败(第一下载源:" + Urls.First() + ")", ex); + } + finally + { + NewTask.Abort(); + } + } + + /// + /// 发送一个网络请求并获取返回内容,会重试三次并在最长 45s 后超时。 + /// + /// 请求的服务器地址。 + /// 请求方式(POST 或 GET)。 + /// 请求的内容。 + /// 请求的套接字类型。 + /// 当返回 40x 时不重试。 + public static string NetRequestRetry(string Url, string Method, object Data, string ContentType, + bool DontRetryOnRefused = true, Dictionary Headers = null) + { + var RetryCount = 0; + Exception RetryException = null; + var StartTime = TimeUtils.GetTimeTick(); + while (RetryCount <= 3) + { + RetryCount += 1; + try + { + switch (RetryCount) + { + case 0: // 正常尝试 + { + return NetRequestOnce(Url, Method, Data, ContentType, 15000, Headers); + } + case 1: // 慢速重试 + { + Thread.Sleep(500); + return NetRequestOnce(Url, Method, Data, ContentType, 25000, Headers); // 快速重试 + } + + default: + { + if (TimeUtils.GetTimeTick() - StartTime > 5500) + { + // 若前两次加载耗费 5 秒以上,才进行重试 + Thread.Sleep(500); + return NetRequestOnce(Url, Method, Data, ContentType, 4000, Headers); + } + + throw RetryException; + } + } + } + catch (ThreadInterruptedException ex) + { + throw; + } + catch (Exception ex) + { + if (ex.InnerException is not null && ex.InnerException is HttpRequestFailedException && + ((int)((HttpRequestFailedException)ex.InnerException).StatusCode).ToString().StartsWithF("4") && + DontRetryOnRefused) + throw; + RetryException = ex; + ModBase.Log(ex, $"[Net] 网络请求第 {RetryCount} 次失败({Url})"); + } + } + + throw RetryException; + } + + /// + /// 发送一次网络请求并获取返回内容。 + /// + /// + /// + /// + /// 仅 Data 为 string 时可用 + /// + /// + /// + /// + /// + public static string NetRequestOnce(string Url, string Method, object Data, string ContentType, int Timeout = 25000, + Dictionary Headers = null, bool MakeLog = true, bool UseBrowserUserAgent = false) + { + if (ModBase.RunInUi() && !Url.Contains("//127.")) + throw new Exception("在 UI 线程执行了网络请求"); + Url = Conversions.ToString(ModSecret.SecretCdnSign(Url)); + if (MakeLog) + ModBase.Log("[Net] 发起网络请求(" + Method + "," + Url + "),最大超时 " + Timeout); + try + { + using (var cts = new CancellationTokenSource()) + { + cts.CancelAfter(Timeout); + var RequestMethod = HttpMethod.Get; + switch (Method.ToUpper() ?? "") // 我不相信上面的输入.jpg + { + case "POST": + { + RequestMethod = HttpMethod.Post; + break; + } + case "PUT": + { + RequestMethod = HttpMethod.Put; + break; + } + case "DELETE": + { + RequestMethod = HttpMethod.Delete; + break; + } + case "HEAD": + { + RequestMethod = HttpMethod.Head; + break; + } + case "OPTIONS": + { + RequestMethod = HttpMethod.Options; + break; + } + } + + using (var request = new HttpRequestMessage(RequestMethod, Url)) + { + var argClient = request; + ModSecret.SecretHeadersSign(Url, ref argClient, UseBrowserUserAgent); + if (new[] { HttpMethod.Post, HttpMethod.Put }.Contains(RequestMethod)) + if (!(Data == null)) + { + if (Data is byte[]) + request.Content = new ByteArrayContent((byte[])Data); + else if (Data is string) + request.Content = new StringContent(Conversions.ToString(Data), Encoding.UTF8, + ContentType); + else if (Data.GetType().IsSubclassOf(typeof(HttpContent))) + request.Content = (HttpContent)Data; + else + throw new ArgumentException("Data 参数类型不支持"); + } + + if (Headers is not null) + foreach (var Pair in Headers) + { + if (string.IsNullOrWhiteSpace(Pair.Key) || string.IsNullOrWhiteSpace(Pair.Value)) + continue; + // 标头覆盖 + if (request.Headers.Contains(Pair.Key)) request.Headers.Remove(Pair.Key); + request.Headers.Add(Pair.Key, Pair.Value); + } + + using (var response = NetworkService.GetClient() + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token).GetAwaiter() + .GetResult()) + { + EnsureSuccessStatusCode(response); + using (var responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()) + { + using (var reader = new StreamReader(responseStream, Encoding.UTF8)) + { + return reader.ReadToEnd(); + } + } + } + } + } + } + catch (ThreadInterruptedException ex) + { + throw; + } + catch (Exception ex) + { + var nx = ex is HttpRequestFailedException + ? new HttpWebException("网络请求失败(" + Url + ")", (HttpRequestFailedException)ex) + : new WebException("网络请求失败(" + Url + ")", ex); + if (MakeLog) + ModBase.Log(nx, "NetRequestOnce 请求失败", ModBase.LogLevel.Developer); + throw nx; + } + } + + /// + /// 是否有正在进行中、需要在任务管理页面显示的下载任务? + /// + public static bool HasDownloadingTask(bool IgnoreCustomDownload = false) + { + foreach (var Task in ModLoader.LoaderTaskbar.ToList()) + if (Task.Show && Task.State == ModBase.LoadState.Loading && + (!IgnoreCustomDownload || !Task.Name.Contains("自定义下载"))) + return true; + + return false; + } + + /// + /// 安全获取路径所在的根盘符(如 "C:\"),仅支持本地绝对路径。 + /// 若路径无效或非本地盘,返回 Nothing。 + /// + private static string TryGetLocalDriveRoot(string path) + { + if (string.IsNullOrEmpty(path)) + return null; + try + { + var root = Path.GetPathRoot(path); + // 仅接受 X:\ 格式(长度为3,第二个字符是冒号) + if (((((root?.Length is { } arg2 ? arg2 == 3 : (bool?)null) is var arg3 && arg3.HasValue && !arg3.Value + ? false + : root[1] == ':' + ? arg3 + : false) is var arg4 && !arg4.HasValue) || arg4.Value) && root[2] == '\\' && + arg4.HasValue) return root.ToUpperInvariant(); + } + catch + { + // 路径非法(如包含通配符、相对路径、UNC 等) + } + + return null; + } + + /// + /// 当调用 时,若给定响应的 IsSuccessStatusCode 属性不为 True 则抛出该异常。 + /// + public class HttpRequestFailedException : HttpRequestException + { + public HttpRequestFailedException(HttpResponseMessage response, string webResponse = null) : base( + $"HTTP 响应失败: {response.ReasonPhrase} ({(int)response.StatusCode})") + { + Response = response; + StatusCode = response.StatusCode; + ReasonPhrase = response.ReasonPhrase; + WebResponse = webResponse; + } + + public new HttpStatusCode StatusCode { get; } + public string ReasonPhrase { get; private set; } + + /// + /// 不要尝试读取 Content 属性的内容,它已经被 dispose 了 + /// + public HttpResponseMessage Response { get; private set; } + + /// + /// 站点的原始返回内容 + /// + public string WebResponse { get; private set; } + } + + /// + /// 的套壳,包含 StatusCode 属性。
+ /// 在此,向龙猫的石山代码致敬。 + ///
+ public class HttpWebException : WebException + { + public HttpWebException(string message, HttpRequestFailedException ex) : base(message, ex) + { + InnerHttpException = ex; + } + + public HttpRequestFailedException InnerHttpException { get; } + public HttpStatusCode StatusCode => InnerHttpException.StatusCode; + } + + public class ResponsedWebException : WebException + { + public ResponsedWebException(string Message, string Response, Exception InnerException) : base(Message, + InnerException) + { + this.Response = Response; + } + + /// + /// 远程服务器给予的回复。 + /// + public new string Response { get; set; } + } + + /// + /// 下载源。 + /// + public class NetSource + { + public Exception Ex; + public int FailCount; + public int Id; + public bool IsFailed; + + /// + /// 若该下载源正在进行强制单线程下载,标记这个唯一的线程。 + /// + public NetThread SingleThread; + + public string Url; + + public override string ToString() + { + return Url; + } + } + + /// + /// 下载线程。 + /// + public class NetThread : IEnumerable, IEquatable + { + private long _Speed; + + /// + /// 线程已下载的文件大小。 + /// + public long DownloadDone; + + /// + /// 线程下载起始位置。 + /// + public long DownloadStart; + + /// + /// 线程初始化时的时间。 + /// + public long InitTime = TimeUtils.GetTimeTick(); + + /// + /// 上次接受到有效数据的时间,-1 表示尚未有有效数据。 + /// + public long LastReceiveTime = -1; + + /// + /// 链表中的下一个线程。 + /// + public NetThread NextThread; + + /// + /// 当前选取的是哪一个 Url。 + /// + public NetSource Source; + + /// + /// 上次记速时的已下载大小。 + /// + private long SpeedLastDone; + + /// + /// 上次记速时的时间。 + /// + private long SpeedLastTime = TimeUtils.GetTimeTick(); + + /// + /// 当前线程的状态。 + /// + public NetState State = NetState.WaitingToDownload; + + /// + /// 对应的下载任务。 + /// + public NetFile Task; + + /// + /// 该线程的缓存文件。 + /// + public string Temp; + + /// + /// 对应的线程。 + /// + public Thread Thread; + + /// + /// 分配给任务中每个线程(无论其是否失败)的编号。 + /// + public int Uuid; + + private IEnumerable Next + { + get + { + var CurrentChain = this; + while (CurrentChain is not null) + { + yield return CurrentChain; + CurrentChain = CurrentChain.NextThread; + } + } + } + + /// + /// 是否为第一个线程。 + /// + public bool IsFirstThread => DownloadStart == 0L && Task.FileSize == -2; + + /// + /// 线程下载结束位置。 + /// + public long DownloadEnd + { + get + { + lock (Task.LockChain) + { + if (NextThread is null) + { + if (Task.IsUnknownSize) return 5 * 1024 * 1024 * 1024L; // 5G + + return Task.FileSize - 1L; + } + + return NextThread.DownloadStart - 1L; + } + } + } + + /// + /// 线程未下载的文件大小。 + /// + public long DownloadUndone => DownloadEnd - (DownloadStart + DownloadDone) + 1L; + + /// + /// 当前的下载速度,单位为 Byte / 秒。 + /// + public long Speed + { + get + { + if (TimeUtils.GetTimeTick() - SpeedLastTime > 200) + { + var DeltaTime = TimeUtils.GetTimeTick() - SpeedLastTime; + _Speed = (long)Math.Round((DownloadDone - SpeedLastDone) / (DeltaTime / 1000d)); + SpeedLastDone = DownloadDone; + SpeedLastTime += DeltaTime; + } + + return _Speed; + } + } + + /// + /// 是否已经结束。 + /// + public bool IsEnded => State == NetState.Finished || State == NetState.Interrupted; + + public IEnumerator GetEnumerator() + { + return Next.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return IEnumerable_GetEnumerator(); + } + + // 允许进行 UUID 比较 + public bool Equals(NetThread other) + { + return other is not null && Uuid == other.Uuid; + } + + private IEnumerator IEnumerable_GetEnumerator() + { + return Next.GetEnumerator(); + } + + public override bool Equals(object obj) + { + return Equals(obj as NetThread); + } + + public static bool operator ==(NetThread left, NetThread right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(NetThread left, NetThread right) + { + return !(left == right); + } + } + + /// + /// 下载单个文件。 + /// + public class NetFile + { + /// + /// 新建一个需要下载的文件。 + /// + /// 包含文件名的本地地址。 + public NetFile(IEnumerable urls, string localPath, ModBase.FileChecker checker = null, + bool useBrowserUserAgent = false, string customUserAgent = "") + { + var sources = new List(); + var count = 0; + urls = urls.Distinct().ToArray(); + foreach (var source in urls) + { + sources.Add(new NetSource + { + FailCount = 0, + Url = Conversions.ToString(ModSecret.SecretCdnSign(source.Replace("\r", "") + .Replace("\n", "").Trim())), + Id = count, IsFailed = false, Ex = null + }); + count += 1; + } + + Sources = new ModBase.SafeList(sources); + LocalPath = localPath; + Check = checker; + UseBrowserUserAgent = useBrowserUserAgent; + CustomUserAgent = customUserAgent; + LocalName = ModBase.GetFileNameFromPath(localPath); + } + + /// + /// 尝试开始一个新的下载线程。 + /// 如果失败,返回 Nothing。 + /// + public NetThread? TryBeginThread() + { + try + { + // 1. Pre-check: status and limits + if (NetTaskThreadCount >= NetTaskThreadLimit || !HasAvailableSource()) return null; + + // Stuck detection for single-thread / no-split mode + if (IsNoSplit && Threads != null && + Threads.State is not (NetState.Interrupted or NetState.WaitingToDownload) && + TimeUtils.GetTimeTick() - Threads.InitTime < 30000) return null; + + if (State >= NetState.Merging || State == NetState.WaitingToCheck) return null; + + lock (LockState) + { + if (State < NetState.Connecting) State = NetState.Connecting; + } + + long startPosition = 0; + NetSource? startSource = null; + + lock (LockChain) + { + // 2. Core scheduling logic + var shouldCapture = false; + if (IsNoSplit) + { + shouldCapture = true; + } + else if (!HasAvailableSource(false)) + { + if (SourcesOnce[0].SingleThread != null && + SourcesOnce[0].SingleThread.State != NetState.Interrupted) + return null; + shouldCapture = true; + } + + if (shouldCapture) + { + if (IsNoSplit && SmallFileCache != null && Threads != null && + Threads.State is not (NetState.Interrupted or NetState.Finished)) + return null; + + SmallFileCache?.Dispose(); + SmallFileCache = null; + Threads = null; + NetManager.DownloadDone -= DownloadDone; + lock (LockDone) + { + DownloadDone = 0; + } + + SpeedLastDone = 0; + State = NetState.Reading; + } + + // 3. Coordinate Calculation + if (Threads == null) + { + startPosition = 0; + startSource = GetSource(FirstThreadSource); + FirstThreadSource = startSource.Id + 1; + } + else + { + // Find interrupted fragments + foreach (var thread in Threads) + if (thread.State == NetState.Interrupted && thread.DownloadUndone > 0) + { + startPosition = thread.DownloadStart + thread.DownloadDone; + startSource = GetSource(thread.Source.Id + 1); + break; + } + + // Try multi-thread splitting + if (startSource == null) + { + var targetUrl = GetSource().Url; + string[] restrictedDomains = + { + "pcl2-server", "bmclapi", "github.com", "optifine.net", "modrinth", "gitcode", + "pysio.online", "mirrorchyan.com", "naids.com" + }; + + if (AllowMultiThread && !restrictedDomains.Any(d => targetUrl.Contains(d))) + { + var filePieceMax = Threads; + foreach (var thread in Threads) + if (thread.DownloadUndone > filePieceMax.DownloadUndone) + filePieceMax = thread; + + if (filePieceMax != null && filePieceMax.DownloadUndone >= FilePieceLimit) + { + startPosition = + (long)(filePieceMax.DownloadEnd - filePieceMax.DownloadUndone * 0.4); + startSource = GetSource(); + } + } + } + } + + // 4. Thread initialization and validation + if (startSource == null || startPosition < 0 || + (startPosition > FileSize && FileSize >= 0 && !IsUnknownSize) || !Tasks.Any()) return null; + + var threadUuid = ModBase.GetUuid(); + var threadInfo = new NetThread + { + Uuid = threadUuid, + DownloadStart = startPosition, + Source = startSource, + Task = this, + State = NetState.WaitingToDownload + }; + + // Fix: Use ParameterizedThreadStart and set IsBackground + var th = new Thread(obj => Thread((NetThread)obj!)) + { + Name = $"NetTask {Tasks[0].Uuid}/{Uuid} Download {threadUuid}#", + Priority = ThreadPriority.BelowNormal, + IsBackground = true + }; + threadInfo.Thread = th; + + // 5. Link-list Maintenance + if (threadInfo.IsFirstThread || Threads == null) + { + Threads = threadInfo; + } + else + { + var current = Threads; + while (current.DownloadEnd <= startPosition && current.NextThread != null) + current = current.NextThread; + threadInfo.NextThread = current.NextThread; + current.NextThread = threadInfo; + } + + // 6. Global Resource Accounting + lock (NetTaskThreadCountLock) + { + NetTaskThreadCount++; + } + + lock (LockSource) + { + if (!HasAvailableSource(false)) SourcesOnce[0].SingleThread = threadInfo; + } + + th.Start(threadInfo); + return threadInfo; + } + } + catch (Exception ex) + { + LogWrapper.Warn(ex, $"Failed to try begin thread for {LocalName ?? "Unknown"}"); + return null; + } + } + + /// + /// 每个下载线程执行的代码。 + /// + private void Thread(NetThread th) + { + if (ModBase.ModeDebug || th.DownloadStart == 0) + LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:开始,起始点 {th.DownloadStart},{th.Source.Url}"); + + Stream? resultStream = null; + var timeout = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + th.Source.FailCount), 25000); + long contentLength = 0; + th.State = NetState.Connecting; + + try + { + var httpDataCount = 0; + if (SourcesOnce.Contains(th.Source) && !th.Equals(th.Source.SingleThread)) return; + + using var temp = new HttpRequestMessage(HttpMethod.Get, th.Source.Url); + var request = temp; + ModSecret.SecretHeadersSign(th.Source.Url, ref request, UseBrowserUserAgent, CustomUserAgent); + + if (!th.IsFirstThread || th.DownloadStart != 0) + request.Headers.Range = new RangeHeaderValue(th.DownloadStart, null); + + using var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout); + + using var response = NetworkService.GetClient() + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token) + .GetAwaiter().GetResult(); + + EnsureSuccessStatusCode(response); + if (State == NetState.Interrupted) return; + + var redirected = response.RequestMessage?.RequestUri; + if (redirected != null && redirected.OriginalString != th.Source.Url) + { + LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:重定向至 {redirected.OriginalString}"); + th.Source.Url = redirected.OriginalString; + } + + // --- 内嵌 HandleContentLength --- + contentLength = response.Content.Headers.ContentLength.GetValueOrDefault(-1); + if (contentLength == -1) + { + if (FileSize > 1) + { + if (th.DownloadStart != 0) + { + LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:ContentLength 返回 -1,视作不支持分段"); + lock (LockSource) + { + if (!SourcesOnce.Contains(th.Source)) SourcesOnce.Add(th.Source); + } + + throw new WebException($"该下载源不支持分段下载(Range: {th.DownloadStart})"); + } + } + else + { + FileSize = -1; + IsUnknownSize = true; + LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:文件大小未知"); + } + } + else if (contentLength < 0) + { + throw new Exception("获取片大小失败,结果为 " + contentLength); + } + else if (th.IsFirstThread) + { + // 首次线程校验文件大小 + if (Check != null) + { + if (Check.MinSize > 0 && contentLength < Check.MinSize) + throw new Exception($"文件大小不足:获取到 {contentLength},要求至少 {Check.MinSize}"); + if (Check.ActualSize > 0 && contentLength != Check.ActualSize) + throw new Exception($"文件大小不一致:获取到 {contentLength},要求必须为 {Check.ActualSize}"); + } + + FileSize = contentLength; + IsUnknownSize = false; + LogWrapper.Info( + $"[Download] {LocalName} {th.Uuid}#:文件大小 {contentLength} ({ModBase.GetString(contentLength)})"); + + // 磁盘空间校验 + if (contentLength > 50 * 1024 * 1024) + { + var tempRoot = TryGetLocalDriveRoot(ModBase.PathTemp); + var localRoot = TryGetLocalDriveRoot(LocalPath); + if (tempRoot != null && localRoot != null) + foreach (var drive in DriveInfo.GetDrives()) + { + if (!drive.IsReady || (drive.DriveType != DriveType.Fixed && + drive.DriveType != DriveType.Removable)) continue; + + long requiredSpace = 0; + if (string.Equals(drive.Name, tempRoot, StringComparison.OrdinalIgnoreCase)) + requiredSpace += (long)(contentLength * 1.1); + if (string.Equals(drive.Name, localRoot, StringComparison.OrdinalIgnoreCase)) + requiredSpace += contentLength + 5 * 1024 * 1024; + + if (requiredSpace > 0 && drive.TotalFreeSpace < requiredSpace) + throw new IOException( + $"{drive.Name.TrimEnd('\\')} 盘空间不足,需要 {ModBase.GetString(requiredSpace)}。"); + } + } + } + else if (FileSize < 0) + { + throw new Exception("尚未获取文件大小"); + } + else if (th.DownloadStart > 0 && contentLength == FileSize) + { + lock (LockSource) + { + if (!SourcesOnce.Contains(th.Source)) SourcesOnce.Add(th.Source); + } + + throw new WebException("该下载源不支持分段下载(返回全量大小)"); + } + else if (FileSize - th.DownloadStart != contentLength) + { + throw new WebException($"分段大小不一致:预期 {FileSize - th.DownloadStart},实际 {contentLength}"); + } + // --- HandleContentLength 结束 --- + + th.State = NetState.Reading; + lock (LockState) + { + if (State < NetState.Reading) State = NetState.Reading; + } + + if (IsNoSplit) + { + th.Temp = null; + SmallFileCache = new MemoryStream(); + resultStream = SmallFileCache; + } + else + { + th.Temp = Path.Combine(ModBase.PathTemp, "Download", + $"{Uuid}_{th.Uuid}_{RandomUtils.NextInt(0, 999999)}.tmp"); + resultStream = new FileStream(th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read); + } + + using var httpStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + const int bufferSize = 16384; + using var bufferOwner = MemoryPool.Shared.Rent(bufferSize); + var dataBuffer = bufferOwner.Memory; + + httpDataCount = httpStream.Read(dataBuffer.Span); + th.LastReceiveTime = TimeUtils.GetTimeTick(); + + while ((IsUnknownSize || th.DownloadUndone > 0) && httpDataCount > 0 && + !ModBase.IsProgramEnded && State < NetState.Merging && !th.Source.IsFailed) + { + while (NetTaskSpeedLimitHigh > 0 && NetTaskSpeedLimitLeft <= 0) System.Threading.Thread.Sleep(8); + + var realDataCount = IsUnknownSize ? httpDataCount : (int)Math.Min(httpDataCount, th.DownloadUndone); + lock (NetTaskSpeedLimitLeftLock) + { + if (NetTaskSpeedLimitHigh > 0) NetTaskSpeedLimitLeft -= realDataCount; + } + + if (th.DownloadDone == 0) + { + th.State = NetState.Downloading; + lock (LockState) + { + if (State < NetState.Downloading) State = NetState.Downloading; + } + + lock (LockCount) + { + ConnectCount++; + ConnectTime += TimeUtils.GetTimeTick() - th.InitTime; + } + } + + lock (LockCount) + { + th.Source.FailCount = 0; + foreach (var task in Tasks) task.FailCount = 0; + } + + lock (LockDone) + { + DownloadDone += realDataCount; + } + + NetManager.DownloadDone += realDataCount; + th.DownloadDone += realDataCount; + + resultStream.Write(dataBuffer.Span.Slice(0, realDataCount)); + + var deltaTime = TimeUtils.GetTimeTick() - th.LastReceiveTime; + if (deltaTime > 1500 && deltaTime > realDataCount) throw new TimeoutException("速度过慢断开连接"); + + th.LastReceiveTime = TimeUtils.GetTimeTick(); + if (th.DownloadUndone == 0 && !IsUnknownSize) break; + + var readStartTime = TimeUtils.GetTimeTick(); + httpDataCount = httpStream.Read(dataBuffer.Span); + if (TimeUtils.GetTimeTick() - readStartTime > timeout * 0.5 && httpDataCount == 0) + throw new TimeoutException("读取超时"); + } + + if (State == NetState.Interrupted || th.Source.IsFailed || (th.DownloadUndone > 0 && !IsUnknownSize)) + { + th.State = NetState.Interrupted; + LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:中断"); + } + else if (httpDataCount == 0 && th.DownloadUndone > 0 && !IsUnknownSize) + { + throw new Exception($"数据不足:服务器提前关闭连接 ({th.DownloadDone}/{contentLength})"); + } + else + { + th.State = NetState.Finished; + if (ModBase.ModeDebug) LogWrapper.Info($"[Download] {LocalName} {th.Uuid}#:完成"); + } + } + catch (Exception ex) + { + LogWrapper.Debug($"[Download] {LocalName}:出错,{(ex is TimeoutException ? "已超时" : ex.Message)}"); + SourceFail(th, ex, false); + } + finally + { + if (!IsNoSplit) resultStream?.Dispose(); + lock (NetTaskThreadCountLock) + { + NetTaskThreadCount--; + } + + if ((FileSize >= 0 ? DownloadDone >= FileSize : DownloadDone > 0) && State < NetState.Merging) Merge(); + } + } + + private void SourceFail(NetThread th, Exception ex, bool isMergeFailure) + { + // 状态变更 + lock (LockCount) + { + th.Source.FailCount += 1; + foreach (var Task in Tasks) + Task.FailCount += 1; + } + + var isTimeoutString = ex.ToString().ToLower().Replace(" ", ""); + var isTimeout = isTimeoutString.Contains("由于连接方在一段时间后没有正确答复或连接的主机没有反应") || isTimeoutString.Contains("超时") || + isTimeoutString.Contains("timeout") || isTimeoutString.Contains("timedout") || + ex.GetType() == typeof(TimeoutException) || ex.GetType() == typeof(TaskCanceledException) || + (ex.GetType() == typeof(AggregateException) && + ((AggregateException)ex).InnerExceptions.Any(x => + x.GetType() == typeof(TaskCanceledException) || + x.GetType() == typeof(TimeoutException))); + // Log("[Download] " & LocalName & " " & th.Uuid & If(isTimeout, "#:超时(" & (th. * 0.001) & "s)", "#:出错," & ex.ToString())) + th.State = NetState.Interrupted; + th.Source.Ex = ex; + // 根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传) + var IsRangeNotSupported = ex is RangeNotSupportedException || ex.Message.Contains("(416)"); + if (isMergeFailure || IsRangeNotSupported || ex.Message.Contains("(502)") || ex.Message.Contains("(404)") || + ex.Message.Contains("未能解析") || ex.Message.Contains("无返回数据") || ex.Message.Contains("空间不足") || + ((ex.Message.Contains("(403)") || ex.Message.Contains("(429)")) && + !th.Source.Url.ContainsF("bmclapi")) || + (th.Source.FailCount >= ModBase.MathClamp(NetTaskThreadLimit, 5d, 30d) && DownloadDone < 1L) || + th.Source.FailCount > NetTaskThreadLimit + 2) // BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源 + { + // 当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理 + var IsThisFail = false; + lock (LockSource) + { + if (!th.Source.IsFailed || th.Source.SingleThread == th) + { + IsThisFail = true; + th.Source.IsFailed = true; + } + } + + // ……后续处理 + if (IsThisFail) + { + ModBase.Log( + $"[Download] {LocalName}:下载源被禁用({th.Source.Id},Range 问题:{IsRangeNotSupported}):{th.Source.Url}"); + ModBase.Log(ex, + $"{(SourcesOnce.FirstOrDefault()?.SingleThread is null ? "" : "单线程")}下载源 {th.Source.Id} 已被禁用", + IsRangeNotSupported || ex.Message.Contains("(404)") + ? ModBase.LogLevel.Developer + : ModBase.LogLevel.Debug); + lock (LockSource) + { + SourcesOnce.Remove(th.Source); + } + + if (ex.Message.Contains("空间不足")) + { + // 硬盘空间不足:强制失败 + Fail(ex); + } + else if (HasAvailableSource() && !isMergeFailure) + { + } + // 当前源失败,但还有下载源:正常地继续执行 + else if (!Retried) + { + // 合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载 + // 若所有源均不支持 Range,也会走到这里重试 + if (!IsRangeNotSupported) + ModBase.Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", ModBase.LogLevel.Debug); + Retried = true; + lock (LockSource) + { + SourcesOnce.Clear(); + foreach (var Source in Sources) + { + SourcesOnce.Add(Source); + Source.IsFailed = true; + } + } + + FileSystem.Reset(); + lock (LockState) + { + State = NetState.WaitingToDownload; + } + } + else if (HasAvailableSource() && isMergeFailure) + { + // 合并失败且单个源失败:继续下一个源 + FileSystem.Reset(); + lock (LockState) + { + State = NetState.WaitingToDownload; + } + } + else + { + // 失败 + ModBase.Log($"[Download] {LocalName}:已无可用下载源,下载失败"); + Exception ExampleEx = null; + lock (LockSource) + { + foreach (var Source in Sources) + { + ModBase.Log("[Download] 已禁用的下载源:" + Source.Url); + if (Source.Ex is not null) + { + ExampleEx = Source.Ex; + ModBase.Log(Source.Ex, "下载源禁用原因", ModBase.LogLevel.Developer); + } + } + } + + Fail(ExampleEx); + } + } + } + + // 清理当前已下载的内容 + if (FileSize == -2) + FileSystem.Reset(); + } + + /// + /// 从 HTTP 响应头中获取文件名。 + /// 如果没有,返回 Nothing。 + /// + private string GetFileNameFromResponse(HttpResponseMessage response) + { + return response.Content.Headers.ContentDisposition.FileName; + } + + // 下载文件的最终收束事件 + /// + /// 下载完成。合并文件。 + /// + private void Merge() + { + // 1. 状态判断:确保合并逻辑只被触发一次 + lock (LockState) + { + if (State < NetState.Merging) + State = NetState.Merging; + else + return; + } + + var retryCount = 0; + while (true) + try + { + lock (LockChain) + { + // 2. 准备目录与清理旧文件 + if (File.Exists(LocalPath)) File.Delete(LocalPath); + var directory = Path.GetDirectoryName(LocalPath); + if (!string.IsNullOrEmpty(directory)) Directory.CreateDirectory(directory); + + // 3. 开始合并文件逻辑 + if (IsNoSplit) + { + // 情况 A:从内存缓存输出(小文件) + if (SmallFileCache == null) + throw new Exception($"小文件缓存为空,无法合并文件({LocalName})。"); + + if (ModBase.ModeDebug) + LogWrapper.Info($"[Download] {LocalName}:下载结束,从缓存输出文件,长度:{SmallFileCache.Length}"); + + SmallFileCache.Seek(0, SeekOrigin.Begin); + using (var mergeFile = new FileStream(LocalPath, FileMode.Create, FileAccess.Write)) + { + SmallFileCache.CopyTo(mergeFile); + } + } + else if (Threads.Count() == 1 && Threads.Temp != null) + { + // 情况 B:仅有一个分段文件,直接移动/复制 + if (ModBase.ModeDebug) LogWrapper.Info($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并"); + File.Copy(Threads.Temp, LocalPath, true); + } + else + { + // 情况 C:多线程分段合并 + if (ModBase.ModeDebug) LogWrapper.Info($"[Download] {LocalName}:下载结束,开始合并分段文件"); + using (var mergeFile = new FileStream(LocalPath, FileMode.Create, FileAccess.Write)) + { + foreach (var th in Threads) + { + if (th.DownloadDone == 0 || th.Temp == null) continue; + using (var fs = new FileStream(th.Temp, FileMode.Open, FileAccess.Read, + FileShare.Read)) + { + fs.CopyTo(mergeFile); + } + } + } + } + + // 4. 最终大小一致性校验 + if (!IsUnknownSize && Check != null) + { + if (Check.ActualSize == -1) + Check.ActualSize = FileSize; + else if (Check.ActualSize != FileSize) + throw new Exception($"文件大小不一致:任务要求 {Check.ActualSize} B,网络结果 {FileSize} B"); + } + + // 5. 业务自定义校验 (MD5/SHA1 等) + var checkResult = Check?.Check(LocalPath); + if (checkResult != null) + { + LogWrapper.Info($"[Download] {LocalName} 文件校验失败,下载线程细节:"); + foreach (var th in Threads) + LogWrapper.Info( + $"[Download] {th.Uuid}#,状态 {th.State},完成 {th.DownloadDone},剩余 {th.DownloadUndone}"); + throw new Exception(checkResult); + } + + // 6. 清理临时资源 + if (IsNoSplit) + { + SmallFileCache?.Dispose(); + SmallFileCache = null; + } + else + { + foreach (var th in Threads) + if (th.Temp != null && File.Exists(th.Temp)) + File.Delete(th.Temp); + } + + Finish(); // 调用完成回调 + return; // 合并成功,退出循环 + } + } + catch (Exception ex) + { + LogWrapper.Error(ex, $"合并文件出错({LocalName})"); + + if (retryCount < 3) + { + retryCount++; + System.Threading.Thread.Sleep(RandomUtils.NextInt(500, 1000)); + continue; // 重新进入 while 循环尝试重试 + } + + Fail(ex); // 重试次数耗尽,彻底失败 + return; + } + } + + /// + /// 下载失败。 + /// + private void Fail(Exception RaiseEx = null) + { + lock (LockState) + { + if (State >= NetState.Finished) + return; + if (RaiseEx is not null) + Ex.Add(RaiseEx); + // 凉凉 + State = NetState.Interrupted; + } + + InterruptAndDelete(); + foreach (var Task in Tasks) + Task.OnFileFail(this); + } + + /// + /// 下载中断。 + /// + public void Abort(LoaderDownload CausedByTask) + { + // 从特定任务中移除,如果它还属于其他任务,则继续下载 + Tasks.Remove(CausedByTask); + if (Tasks.Any()) + return; + // 确认中断 + lock (LockState) + { + if (State >= NetState.Finished) + return; + State = NetState.Interrupted; + } + + InterruptAndDelete(); + } + + private void InterruptAndDelete() + { + // On Error Resume Next + try + { + if (File.Exists(LocalPath)) + File.Delete(LocalPath); + } + catch (Exception ex) + { + ModBase.Log(ex, $"[Download] 尝试删除文件 {LocalPath} 失败,忽略错误", ModBase.LogLevel.Normal); + } + lock (NetManager.LockRemain) + { + NetManager.FileRemain -= 1; + ModBase.Log($"[Download] {LocalName}:状态 {State},剩余文件 {NetManager.FileRemain}"); + } + } + + // 状态改变接口 + /// + /// 将该文件设置为已下载完成。 + /// + public void Finish(bool PrintLog = true) + { + lock (LockState) + { + if (State >= NetState.Finished) + return; + State = NetState.Finished; + } + + lock (NetManager.LockRemain) + { + NetManager.FileRemain -= 1; + if (PrintLog) + ModBase.Log("[Download] " + LocalName + ":已完成,剩余文件 " + NetManager.FileRemain); + } + + foreach (var Task in Tasks) + Task.OnFileFinish(this); + } + + #region 属性 + + /// + /// 所属的文件列表任务。 + /// + public ModBase.SafeList Tasks = new(); + + /// + /// 所有下载源。 + /// + public ModBase.SafeList Sources; + + /// + /// 用于在第一个线程出错时切换下载源。 + /// + private int FirstThreadSource; + + /// + /// 所有已经被标记为失败的,但未完整尝试过的,不允许断点续传的下载源。 + /// + public ModBase.SafeList SourcesOnce = new(); + + /// + /// 仅当合并失败或首次下载失败时,会将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载。 + /// 这一策略可以兼容多个下载源中的一部分返回错误的文件的情况,以及部分在多线程下载时会抽风的源。 + /// + private bool Retried; + + /// + /// 获取从某个源开始,第一个可用的源。 + /// + private NetSource GetSource(int Id = 0) + { + if (Sources.Count == 0) + return null; + Id = Id % Sources.Count; + lock (LockSource) + { + if (HasAvailableSource(false)) + { + // 存在多线程可用源 + var CurrentSource = Sources[Id]; + while (CurrentSource.IsFailed) + { + Id += 1; + if (Id >= Sources.Count) + Id = 0; + CurrentSource = Sources[Id]; + } + + return CurrentSource; + } + + if (SourcesOnce.Any()) + // 仅存在单线程可用源 + return SourcesOnce[0]; + + // 没有可用源 + return null; + } + } + + /// + /// 是否存在可用源。 + /// + public bool HasAvailableSource(bool AllowOnceSource = true) + { + lock (LockSource) + { + if (Sources.Any(s => !s.IsFailed)) + return true; // 存在多线程可用源 + if (AllowOnceSource && SourcesOnce.Any()) + return true; // 存在单线程可用源 + } + + return false; + } + + /// + /// 存储在本地的带文件名的地址。 + /// + public string LocalPath; + + /// + /// 存储在本地的文件名。 + /// + public string LocalName; + + /// + /// 当前的下载状态。 + /// + public NetState State = NetState.WaitingToCheck; + + /// + /// 导致下载失败的原因。 + /// + public List Ex = new(); + + /// + /// 作为文件组成部分的线程链表。 + /// 如果没有线程,可以为 Nothing。 + /// + public NetThread Threads; + + /// + /// 文件的总大小。若为 -2 则为未获取,若为 -1 则为无法获取准确大小。 + /// + public long FileSize = -2; + + /// + /// 该文件是否无法获取准确大小。 + /// + public bool IsUnknownSize; + + /// + /// 该文件是否不需要分割。 + /// + public bool IsNoSplit => IsUnknownSize || FileSize < FilePieceLimit; + + /// + /// 为不需要分割的小文件进行临时存储。 + /// + private MemoryStream SmallFileCache; + + /// + /// 文件的已下载大小。 + /// + public long DownloadDone; + + private readonly object LockDone = new(); + + /// + /// 文件的校验规则。 + /// + public ModBase.FileChecker Check; + + /// + /// 下载时是否添加浏览器 UA。 + /// + public bool UseBrowserUserAgent; + + /// + /// 是否允许多线程下载 + /// + public bool AllowMultiThread = true; + + /// + /// 自定义User-Agent + /// + public string CustomUserAgent = ""; + + /// + /// 上次记速时的时间。 + /// + private long SpeedLastTime = TimeUtils.GetTimeTick(); + + /// + /// 上次记速时的已下载大小。 + /// + private long SpeedLastDone; + + /// + /// 当前的下载速度,单位为 Byte / 秒。 + /// + public long Speed + { + get + { + if (TimeUtils.GetTimeTick() - SpeedLastTime > 200) + { + var DeltaTime = TimeUtils.GetTimeTick() - SpeedLastTime; + _Speed = (long)Math.Round((DownloadDone - SpeedLastDone) / (DeltaTime / 1000d)); + SpeedLastDone = DownloadDone; + SpeedLastTime += DeltaTime; + } + + return _Speed; + } + } + + private long _Speed; + + /// + /// 该文件是否由本地文件直接拷贝完成。 + /// + public bool IsCopy; + + /// + /// 本文件的显示进度。 + /// + public double Progress + { + get + { + switch (State) + { + case NetState.WaitingToCheck: + { + return 0d; + } + case NetState.WaitingToDownload: + { + return 0.01d; + } + case NetState.Connecting: + { + return 0.02d; + } + case NetState.Reading: + { + return 0.04d; + } + case NetState.Downloading: + { + // 正在下载中,对应 5% ~ 98% + var OriginalProgress = IsUnknownSize ? 0.5d : DownloadDone / (double)Math.Max(FileSize, 1L); + OriginalProgress = 1d - Math.Pow(1d - OriginalProgress, 0.9d); + return OriginalProgress * 0.93d + 0.05d; + } + case NetState.Merging: + { + return 0.99d; + } + case NetState.Finished: + case NetState.Interrupted: + { + return 1d; + } + + default: + { + return 0.5d; + } + // Throw New ArgumentOutOfRangeException("文件状态未知:" & State) + } + } + } + + /// + /// 各个线程建立连接成功的总次数。 + /// + private int ConnectCount; + + /// + /// 各个线程建立连接成功的总时间。 + /// + private long ConnectTime; + + /// + /// 各个线程建立连接成功的平均时间,单位为毫秒,-1 代表尚未有成功连接。 + /// + private int ConnectAverage + { + get + { + lock (LockCount) + { + return (int)Math.Round(ConnectCount == 0 ? -1 : ConnectTime / (double)ConnectCount); + } + } + } + + private const long FilePieceLimit = 262144L; + public readonly object LockCount = new(); + public readonly object LockState = new(); + public readonly object LockChain = new(); + public readonly object LockSource = new(); + + public readonly int Uuid = ModBase.GetUuid(); + + public override bool Equals(object obj) + { + var file = obj as NetFile; + return file is not null && Uuid == file.Uuid; + } + + #endregion + } + + private class RangeNotSupportedException : WebException + { + public RangeNotSupportedException(string message) : base(message) + { + } + } + + /// + /// 下载一系列文件的加载器。 + /// + public class LoaderDownload : ModLoader.LoaderBase + { + public LoaderDownload(string Name, List FileTasks) + { + this.Name = Name; + Files = new ModBase.SafeList(FileTasks); + } + + /// + /// 刷新公开属性。由 NetManager 每 0.1 秒调用一次。 + /// + public void RefreshStat() + { + // 计算进度 + var NewProgress = 0d; + var TotalProgress = 0d; + foreach (var File in Files) + if (File.IsCopy) + { + NewProgress += File.Progress * 0.2d; + TotalProgress += 0.2d; + } + else + { + NewProgress += File.Progress; + TotalProgress += 1d; + } + + if (TotalProgress > 0d && !double.IsNaN(TotalProgress)) + NewProgress /= TotalProgress; + // 刷新进度 + _Progress = NewProgress; + } + + public override void Start(object Input = null, bool IsForceRestart = false) + { + if (Input is not null) + Files = new ModBase.SafeList((IEnumerable)Input); + // 去重 + Files = new ModBase.SafeList(Files.Distinct((a, b) => (a.LocalPath ?? "") == (b.LocalPath ?? ""))); + // 设置剩余文件数 + lock (FileRemainLock) + { + FileRemain += Files.Where(f => f.State != NetState.Finished).Count(); + } + + State = ModBase.LoadState.Loading; + // 开始执行 + // 输入检测 + // 接入任务管理器 + // ==================================== + // 已存在文件查找 + // ==================================== + + // 整理允许进行查找的文件 + // 获取 MC 文件夹列表 + // 平均分配到多个检查线程 + ModBase.RunInNewThread(() => + { + try + { + if (!Files.Any()) + { + OnFinish(); + return; + } + + foreach (var File in Files) + { + if (File is null) throw new ArgumentException("存在空文件请求!"); + foreach (var Source in File.Sources) + if (!(Source.Url.StartsWithF("https://", true) || Source.Url.StartsWithF("http://", true))) + { + Source.Ex = new ArgumentException("输入的下载链接不正确!"); + Source.IsFailed = true; + } + + if (!File.HasAvailableSource()) throw new ArgumentException("输入的下载链接不正确!"); + File.LocalPath = File.LocalPath.Replace("/", @"\"); + if (!File.LocalPath.ToLower().Contains(@":\")) + throw new ArgumentException("输入的本地文件地址不正确: " + File.LocalPath); + if (File.LocalPath.EndsWithF(@"\")) + throw new ArgumentException("请输入含文件名的完整文件路径: " + File.LocalPath); + Directory.CreateDirectory(ModBase.GetPathFromFullPath(File.LocalPath)); + } + + NetManager.Start(this); + var FilesToCheck = new List(); + var DisabledCopy = Conversions.ToBoolean(Config.Debug.DontCopy); + foreach (var File in Files) + if (!DisabledCopy && (File.Check?.CanUseExistsFile).GetValueOrDefault()) + FilesToCheck.Add(File); + else + lock (LockState) + { + File.State = NetState.WaitingToDownload; + File.IsCopy = false; + } + + if (!FilesToCheck.Any()) return; + var Folders = new List(); + Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\.minecraft\"); + Folders.AddRange(ModMinecraft.McFolderList.Select(f => f.Location)); + Folders = Folders.Distinct().Where(f => Directory.Exists(f)).ToList(); + var ThreadCount = (int)Math.Round(ModBase.MathClamp(FilesToCheck.Count / 40, 1d, 8d)); + if (ThreadCount == 1) + { + CheckExistingFiles(FilesToCheck, Folders); + } + else + { + var BaseSize = FilesToCheck.Count / ThreadCount; + var Remainder = FilesToCheck.Count % ThreadCount; + var Index = 0; + for (int i = 0, loopTo = ThreadCount - 1; i <= loopTo; i++) + { + var Size = BaseSize + (i < Remainder ? 1 : 0); + var ThreadFiles = FilesToCheck.GetRange(Index, Size); + Index += Size; + ModBase.RunInNewThread(() => CheckExistingFiles(ThreadFiles, Folders), + $"下载 文件复制 {Uuid}/{ModBase.GetUuid()}"); + } + } + } + catch (Exception ex) + { + OnFail(new List { new("下载初始化失败", ex) }); + } + }, "L/下载 " + Uuid); // 创建目标文件夹 + // 在设置中禁用了复制 + // 不允许,直接开始下载 + // 总是添加官启文件夹,因为 HMCL 会把所有文件存在这里 + // 每个线程至少 40 个文件,最多 8 线程 + // 只有一个线程,直接执行 + } + + private void CheckExistingFiles(List Files, List FolderList) + { + try + { + if (ModBase.ModeDebug) + ModBase.Log($"[Download] 文件检查线程已启动,分配的文件数:{Files.Count}"); + // 列出 MC 文件夹中的各个版本文件夹 + var VersionFolders = new List(); + foreach (var McFolder in FolderList) + { + var VersionsFolder = new DirectoryInfo(McFolder + @"versions\"); + if (VersionsFolder.Exists) + foreach (var VersionFolder in VersionsFolder.GetDirectories()) + VersionFolders.Add(VersionFolder.FullName + @"\"); + } + + // 处理每个文件 + foreach (var File in Files) + { + var Target = CheckExistingFile(FolderList, VersionFolders, File); + if (File.State >= NetState.WaitingToDownload) + return; // 中断 + if (Target is null) + { + // 未找到相同文件 + lock (LockState) + { + File.State = NetState.WaitingToDownload; + File.IsCopy = false; + } + } + else + { + // 已找到相同文件 + File.IsCopy = true; + var RetryCount = 0; + Retry: ; + + try + { + if ((Target ?? "") != (File.LocalPath ?? "")) + { + ModBase.Log($"[Download] 复制已存在的文件:{Target} → {File.LocalPath}"); + ModBase.CopyFile(Target, File.LocalPath); + } + + File.Finish(false); + } + catch (Exception ex) + { + RetryCount += 1; + ModBase.Log(ex, $"复制已存在的文件失败,第 {RetryCount} 次重试({Target} → {File.LocalPath})"); + if (RetryCount < 3) + { + Thread.Sleep(200); + goto Retry; + } + + // 失败,回退到下载 + lock (LockState) + { + File.State = NetState.WaitingToDownload; + File.IsCopy = false; + } + } + } + } + } + catch (Exception ex) + { + OnFail(new List { new("下载已存在文件查找失败", ex) }); + } + } + + private string CheckExistingFile(List FolderList, List VersionFolders, NetFile File) + { + // 目标文件已存在 + if (File.Check.Check(File.LocalPath) is null) + return File.LocalPath; + // 没有可用的检查规则,只能开始下载 + if (File.Check.Hash is null && File.Check.ActualSize < 0L) + return null; + // 大致判断文件类别 + var TypeIndexes = + new[] + { + @"\assets\", @"\libraries\", @"\versions\", @"\mods\", @"\coremods\", @"\lib\", + @"\resourcepacks\", + @"\texturepacks\", @"\shaderpacks\" + }.Select(FolderName => (FolderName, File.LocalPath.IndexOfF(FolderName, true))) + .Where(kv => kv.Item2 >= 0).ToList(); + if (!TypeIndexes.Any()) + { + if (File.LocalName.EndsWithF(".jar")) + TypeIndexes.Add((@"\versions\", 1)); // 总是对 jar 进行版本文件检查,以包括另存为 jar 的情况 + else + return null; + } + + var Type = TypeIndexes.MaxOrDefault(kv => kv.Item2).FolderName.TrimStart('\\'); + // 根据类别进行查找 + switch (Type) + { + case @"assets\": + case @"libraries\": + { + // assets/libraries:查找 MC 文件夹下的相同路径 + foreach (var Folder in FolderList) + { + var Candidate = Folder + Type + File.LocalPath.AfterFirst(Type); + if (File.Check.Check(Candidate) is null) + return Candidate; + } + + break; + } + case @"versions\": + { + // 版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹 + foreach (var VersionFolder in VersionFolders) + foreach (var Candidate in Directory.GetFiles(VersionFolder, + "*." + ModBase.GetFileNameFromPath(File.LocalPath).AfterLast(".").ToLower(), + SearchOption.TopDirectoryOnly)) + if (File.Check.Check(Candidate) is null) + return Candidate; + + break; + } + + default: + { + // 社区资源 + if (File.Check.ActualSize < 0L || File.Check.Hash is null) + return null; // 必须要求指定了文件大小和 Hash + foreach (var Folder in FolderList.Concat(VersionFolders)) + { + var TargetFolder = Folder + Type; + if (!Directory.Exists(TargetFolder)) + continue; + foreach (var Candidate in Directory.GetFiles(TargetFolder)) + { + if (!_CheckExistingFile_Sizes.ContainsKey(Conversions.ToString(Candidate))) + _CheckExistingFile_Sizes[Conversions.ToString(Candidate)] = + new FileInfo(Conversions.ToString(Candidate)).Length; + if (File.Check.ActualSize != _CheckExistingFile_Sizes[Conversions.ToString(Candidate)]) + continue; + // Hash 校验 + if (File.Check.Check(Conversions.ToString(Candidate)) is null) + return Conversions.ToString(Candidate); + } + } + + break; + } + } + + return null; + } + + public void OnFileFinish(NetFile File) + { + // 要求全部文件完成 + lock (FileRemainLock) + { + FileRemain -= 1; + if (FileRemain > 0) + return; + } + + OnFinish(); + } + + public void OnFinish() + { + RaisePreviewFinish(); + lock (LockState) + { + if (State > ModBase.LoadState.Loading) + return; + State = ModBase.LoadState.Finished; + } + } + + public void OnFileFail(NetFile File) + { + // 将下载源的错误加入主错误列表 + foreach (var Source in File.Sources) + if (!(Source.Ex == null)) + File.Ex.Add(Source.Ex); + OnFail(File.Ex); + } + + public void OnFail(List ExList) + { + lock (LockState) + { + if (State > ModBase.LoadState.Loading) + return; + if (ExList is null || !ExList.Any()) + ExList = new List { new("未知错误!") }; + // 寻找第一个不是 404 的下载源 + var UsefulExs = ExList.Where(e => !e.Message.Contains("404 (")).ToList(); + Error = UsefulExs.Any() ? UsefulExs[0] : ExList[0]; + // 获取实际失败的文件 + foreach (var File in Files) + if (File.State == NetState.Interrupted) + { + Error = new Exception( + "文件下载失败:" + File.LocalPath + "\r\n" + File.Sources + .Select(s => s.Ex is null ? s.Url : s.Ex.Message + "(" + s.Url + ")") + .Join("\r\n"), Error); + break; + } + + // 在设置 Error 对象后再更改为失败,避免 WaitForExit 无法捕获错误 + State = ModBase.LoadState.Failed; + } + + // 中断所有文件 + foreach (var TaskFile in Files) + if (TaskFile.State < NetState.Merging) + TaskFile.State = NetState.Interrupted; + // 在退出同步锁后再进行日志输出 + var ErrOutput = new List(); + foreach (var Ex in ExList) + ErrOutput.Add(Ex.Message); + ModBase.Log("[Download] " + ErrOutput.Distinct().ToArray().Join("\r\n")); + } + + public override void Abort() + { + lock (LockState) + { + if (State >= ModBase.LoadState.Finished) + return; + State = ModBase.LoadState.Aborted; + } + + ModBase.Log("[Download] " + Name + " 已取消!"); + // 中断所有文件 + foreach (var TaskFile in Files) + TaskFile.Abort(this); + } + + #region 属性 + + /// + /// 需要下载的文件。 + /// + public ModBase.SafeList Files; + + /// + /// 剩余未完成的文件数。(用于减轻 FilesLock 的占用) + /// + private int FileRemain; + + private readonly object FileRemainLock = new(); + + /// + /// 用于显示的百分比进度。 + /// + public override double Progress + { + get + { + if (State >= ModBase.LoadState.Finished) + return 1d; + if (!Files.Any()) + return 0d; // 必须返回 0,否则在获取列表的时候会错觉已经下载完了 + return _Progress; + } + set => throw new Exception("文件下载不允许指定进度"); + } + + private double _Progress; + + /// + /// 任务中的文件的连续失败计数。 + /// + public int FailCount + { + get => _FailCount; + set + { + _FailCount = value; + if (State == ModBase.LoadState.Loading && value >= Math.Min(10000d, + Math.Max(FileRemain * 5.5d, NetTaskThreadLimit * 5.5d + 3d))) + { + ModBase.Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " + value + " 次", ModBase.LogLevel.Debug); + // On Error Resume Next + var ExList = new List(); + foreach (var File in Files) + foreach (var Source in File.Sources) + if (Source.Ex is not null) + { + ExList.Add(Source.Ex); + if (ExList.Count > 10) + goto FinishExCatch; + } + + FinishExCatch: ; + + OnFail(ExList); + } + } + } + + private int _FailCount; + + #endregion + } + + /// + /// 下载单个 UNC 文件的加载器。 + /// + public class LoaderDownloadUnc : ModLoader.LoaderBase + { + /// + /// 下载线程。 + /// + private Thread DlThread; + + /// + /// 保存路径。 + /// + public string SavePath; + + /// + /// UNC 路径。 + /// + public string Unc; + + public LoaderDownloadUnc(string Name, Tuple File) + { + this.Name = Name; + Unc = File.Item1; + SavePath = File.Item2; + } + + public override void Start(object Input = null, bool IsForceRestart = false) + { + if (Input is not null) + { + Unc = Conversions.ToString(((dynamic)Input).Item1); + SavePath = Conversions.ToString(((dynamic)Input).Item2); + } + + State = ModBase.LoadState.Loading; + Directory.CreateDirectory(ModBase.GetPathFromFullPath(SavePath)); + DlThread = ModBase.RunInNewThread(DownloadThread, "Download UNC File"); + } + + private void DownloadThread() + { + try + { + var fileInfo = new FileInfo(Unc); + var totalBytes = fileInfo.Length; + var bytesRead = 0L; + + var tempFile = ModBase.PathTemp + Uuid + @"\" + ModBase.GetFileNameFromPath(SavePath); + Directory.CreateDirectory(ModBase.GetPathFromFullPath(tempFile)); + if (File.Exists(tempFile)) + File.Delete(tempFile); + using (var sourceStream = new FileStream(Unc, FileMode.Open, FileAccess.Read)) + { + using (var destStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write)) + { + var buffer = new byte[81921]; // 80KB 缓冲区 + int currentBytesRead; + + do + { + currentBytesRead = sourceStream.Read(buffer, 0, buffer.Length); + destStream.Write(buffer, 0, currentBytesRead); + bytesRead += currentBytesRead; + + Progress = bytesRead / (double)totalBytes; + } while (currentBytesRead > 0 && State == ModBase.LoadState.Loading); + } + } + + if (State > ModBase.LoadState.Loading) + return; + ModBase.CopyFile(tempFile, SavePath); + if (State == ModBase.LoadState.Loading) + State = ModBase.LoadState.Finished; + } + catch (ThreadAbortException ex) + { + } + } + + public override void Abort() + { + if (State >= ModBase.LoadState.Finished) + return; + State = ModBase.LoadState.Aborted; + ModBase.Log("[Download] " + Name + " 已取消!"); + } + } + + #region 刷新整体速度 + + // 计算瞬时速度 + private static readonly List _RefreshStat_SpeedLast = new(); // 记录至多最近 30 次下载速度的记录,较新的在前面 + + // 上次记速时的已下载大小 + private static long _RefreshStat_SpeedLastDone; + private static bool _StartManager_IsStarted = false; + + /// + /// 下载文件管理。 + /// + public class NetManagerClass + { + #region 属性 + + /// + /// 需要下载的文件。为“本地地址 - 文件对象”键值对。 + /// + public Dictionary Files = new(); + + public readonly object LockFiles = new(); + + /// + /// 当前的所有下载任务。 + /// + public ModBase.SafeList Tasks = new(); + + /// + /// 已下载完成的大小。 + /// + public long DownloadDone + { + get => _DownloadDone; + set + { + lock (LockDone) + { + _DownloadDone = value; + } + } + } + + private long _DownloadDone; + private readonly object LockDone = new(); + + + /// + /// 尚未完成下载的文件数。 + /// + public int FileRemain; + + public readonly object LockRemain = new(); + + // 这些属性由 RefreshStat 刷新 + /// + /// 当前的全局下载速度,单位为 Byte / 秒。 + /// + public long Speed; + + public readonly int Uuid = ModBase.GetUuid(); + + #endregion + + /// + /// 进度与下载速度由任务管理线程每隔约 0.1 秒刷新一次。 + /// + private void RefreshStat() + { + try + { + var DeltaTime = TimeUtils.GetTimeTick() - RefreshStatLast; + if (DeltaTime == 0L) + return; + RefreshStatLast += DeltaTime; + var ActualSpeed = Math.Max(0d, (DownloadDone - _RefreshStat_SpeedLastDone) / (DeltaTime / 1000d)); + _RefreshStat_SpeedLast.Insert(0, (long)Math.Round(ActualSpeed)); + if (_RefreshStat_SpeedLast.Count >= 31) + _RefreshStat_SpeedLast.RemoveAt(30); + _RefreshStat_SpeedLastDone = DownloadDone; + // 计算用于显示的速度 + var SpeedSum = 0L; + var SpeedDiv = 0L; + var Weight = _RefreshStat_SpeedLast.Count; + foreach (var SpeedRecord in _RefreshStat_SpeedLast) + { + SpeedSum += SpeedRecord * Weight; + SpeedDiv += Weight; + Weight -= 1; + } + + Speed = (long)Math.Round(SpeedDiv > 0L ? SpeedSum / (double)SpeedDiv : 0d); + // 计算新的速度下限 + var Limit = 0L; + if (_RefreshStat_SpeedLast.Count >= 10) + Limit = (long)Math.Round(_RefreshStat_SpeedLast.Take(10).Average() * 0.85d); // 取近 1 秒的平均速度的 85% + if (Limit > NetTaskSpeedLimitLow) + { + NetTaskSpeedLimitLow = Limit; + ModBase.Log("[Download] " + "速度下限已提升到 " + ModBase.GetString(Limit)); + } + + #endregion + + #region 刷新下载任务属性 + + foreach (var Task in Tasks) + Task.RefreshStat(); + } + + #endregion + + catch (Exception ex) + { + ModBase.Log(ex, "刷新下载公开属性失败"); + } + } + + /// + /// 启动监控线程,用于新增下载线程。 + /// + private static bool _isManagerStarted; + + // Public FileRemainList As New List(Of String) + private bool IsDownloadCacheCleared; + private long RefreshStatLast; + + private void StartManager() + { + if (_isManagerStarted) return; + _isManagerStarted = true; + + // 调度器逻辑封装 + Action threadStarter = id => + { + try + { + while (true) + { + Thread.Sleep(20); + + // 1. 获取文件快照 + List allFiles; + lock (LockFiles) + { + // 若已完成则清空列表 (仅由 ID 为 0 的线程负责) + if (id == 0 && FileRemain == 0 && Files.Any()) Files.Clear(); + allFiles = Files.Values.ToList(); + } + + var waitingFiles = new List(); + var ongoingFiles = new List(); + + // 2. 任务分类 + foreach (var file in allFiles) + { + if (file.Uuid % 2 == id) continue; // 根据 UUID 奇偶性分工 + + if (file.State == NetState.WaitingToDownload) + waitingFiles.Add(file); + else if (file.State < NetState.Merging) + ongoingFiles.Add(file); + } + + // 3. 启动等待中的任务 + foreach (var file in waitingFiles) + { + if (NetTaskThreadCount >= NetTaskThreadLimit) break; // 最大线程数限制 + + var newThread = file.TryBeginThread(); + // 针对 BMCLAPI 限流优化 + if (newThread?.Source.Url.Contains("bmclapi") == true) Thread.Sleep(100); + } + + // 4. 为进行中的任务追加线程(提速逻辑) + if (Speed >= NetTaskSpeedLimitLow) continue; // 速度够快就不管了 + + foreach (var file in ongoingFiles) + { + if (NetTaskThreadCount >= NetTaskThreadLimit) break; + + var preparingCount = 0; + var downloadingCount = 0; + + if (file.Threads != null) + foreach (var thread in file.Threads.ToList()) + if (thread.State < NetState.Downloading) preparingCount++; + else if (thread.State == NetState.Downloading) downloadingCount++; + + // 如果准备中的线程已经比下载中的多了,先等等 + if (preparingCount > downloadingCount) continue; + + var newThread = file.TryBeginThread(); + if (newThread?.Source.Url.Contains("bmclapi") == true) Thread.Sleep(100); + } + } + } + catch (Exception ex) + { + LogWrapper.Error(ex, $"任务管理启动线程 {id} 出错"); + } + }; + + // 启动两个调度线程 + Basics.RunInNewThread(() => threadStarter(0), "NetManager ThreadStarter 0"); + Basics.RunInNewThread(() => threadStarter(1), "NetManager ThreadStarter 1"); + + // 统计刷新线程 + Basics.RunInNewThread(() => + { + try + { + var nextTick = TimeUtils.GetTimeTick(); + while (true) + { + // 刷新限速余量与公开属性 + if (NetTaskSpeedLimitHigh > 0) NetTaskSpeedLimitLeft = NetTaskSpeedLimitHigh / 10; + RefreshStat(); + + // 精准定时:等待 100ms 并补偿追帧 + nextTick += 100; + var sleepTime = nextTick - TimeUtils.GetTimeTick(); + + if (sleepTime > 0) Thread.Sleep((int)sleepTime); + else nextTick = TimeUtils.GetTimeTick(); // 已经超时,重置时间戳追帧 + } + } + catch (Exception ex) + { + LogWrapper.Error(ex, "任务管理刷新线程出错"); + } + }, "NetManager StatRefresher"); + } + + /// + /// 开始一个下载任务。 + /// + public void Start(LoaderDownload Task) + { + StartManager(); + // 清理缓存 + if (!IsDownloadCacheCleared) + { + try + { + ModBase.DeleteDirectory(ModBase.PathTemp + "Download"); + } + catch (Exception ex) + { + ModBase.Log(ex, "清理下载缓存失败"); + } + + IsDownloadCacheCleared = true; + } + + Directory.CreateDirectory(ModBase.PathTemp + "Download"); + // 文件处理 + lock (LockFiles) + { + // 添加每个文件 + for (int i = 0, loopTo = Task.Files.Count - 1; i <= loopTo; i++) + { + var File = Task.Files[i]; + if (Files.ContainsKey(File.LocalPath)) + { + // 已有该文件 + if (Files[File.LocalPath].State >= NetState.Finished) + { + // 该文件已经下载过一次,且下载完成 + // 将已下载的文件替换成当前文件,重新下载 + File.Tasks.Add(Task); + Files[File.LocalPath] = File; + lock (LockRemain) + { + FileRemain += 1; + if (ModBase.ModeDebug) + ModBase.Log("[Download] " + File.LocalName + ":已替换列表,剩余文件 " + FileRemain); + // FileRemainList.Add(File.LocalPath) + } + } + else + { + // 该文件正在下载中 + // 将当前文件替换成下载中的文件,即两个任务指向同一个文件 + File = Files[File.LocalPath]; + File.Tasks.Add(Task); + } + } + else + { + // 没有该文件 + File.Tasks.Add(Task); + Files.Add(File.LocalPath, File); + lock (LockRemain) + { + FileRemain += 1; + if (ModBase.ModeDebug) + ModBase.Log("[Download] " + File.LocalName + ":已加入列表,剩余文件 " + FileRemain); + // FileRemainList.Add(File.LocalPath) + } + } + + Task.Files[i] = File; // 回设 + } + } + + Tasks.Add(Task); + } + } +} diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index a3192ab35..8849a33bd 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -1440,11 +1440,17 @@ Retry: If State >= NetState.Finished Then Return State = NetState.Interrupted End SyncLock + InterruptAndDelete() End Sub Private Sub InterruptAndDelete() 'On Error Resume Next - If File.Exists(LocalPath) Then File.Delete(LocalPath) + Try + If File.Exists(LocalPath) Then File.Delete(LocalPath) + Catch ex As Exception + Log(ex, $"[Download] 尝试删除文件 {LocalPath} 失败,忽略错误", LogLevel.Normal) + End Try + SyncLock NetManager.LockRemain NetManager.FileRemain -= 1 Log($"[Download] {LocalName}:状态 {State},剩余文件 {NetManager.FileRemain}") diff --git a/Plain Craft Launcher 2/Modules/Base/ModSetup.cs b/Plain Craft Launcher 2/Modules/Base/ModSetup.cs new file mode 100644 index 000000000..7cb1f108c --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModSetup.cs @@ -0,0 +1,774 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Reflection; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Effects; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.App.Configuration; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public class ModSetup : IConfigScope +{ + #region 基础 + + public IEnumerable CheckScope(IReadOnlySet keys) + { + var methods = typeof(ModSetup).GetMethods(); + foreach (var method in methods) + _methodCache.TryAdd(method.Name, method); + return methods.Where(method => keys.Contains(method.Name)).Select(method => method.Name); + } + + public bool Reset(object? argument = null) + { + throw new NotSupportedException(); + } + + public bool IsDefault(object? argument = null) + { + throw new NotSupportedException(); + } + + public ModSetup() + { + ConfigService.RegisterObserver(this, new ConfigObserver(ConfigEvent.Changed, OnConfigChanged)); + } + + private readonly ConcurrentDictionary _methodCache = new(); + + private void InvokeEventMethod(string key, Func valueGetter) + { + var method = _methodCache.GetOrAdd(key, typeof(ModSetup).GetMethod); + if (method == null) return; + var para = method.GetParameters(); + if (para.Length < 1) return; + var paraType = para[0].ParameterType; + var value = valueGetter(); + var valueType = value.GetType(); + if (valueType != paraType) + { + if (valueType.IsEnum) value = (int)value; + else if (value is string s) value = StringConvertExtension.Convert(s, paraType); + else if (paraType == typeof(string)) value = value.ConvertToString(); + else + throw new InvalidCastException( + $"{key}: {valueType.FullName} cannot be converted to {paraType.FullName}"); + } + + method.Invoke(this, [value]); + } + + public void OnConfigChanged(ConfigEventArgs e) + { + var key = e.Item.Key; + InvokeEventMethod(key, () => e.Value ?? GetConfigItem(key).DefaultValueNoType); + } + + private static ConfigItem GetConfigItem(string key) + { + var result = ConfigService.TryGetConfigItemNoType(key, out var item); + return result ? item! : throw new KeyNotFoundException($"配置项 '{key}' 不存在"); + } + + /// + /// 改变某个设置项的值。 + /// + public void Set(string key, object value, bool forceReload = false, ModMinecraft.McInstance? instance = null) + { + GetConfigItem(key).SetValueNoType(value, instance?.PathInstance); + } + + /// + /// 应用某个设置项的值。 + /// + public object Load(string key, bool forceReload = false, ModMinecraft.McInstance? instance = null) + { + var value = Get(key, instance); + InvokeEventMethod(key, () => value); + return value; + } + + /// + /// 获取某个设置项的值。 + /// + public object Get(string key, ModMinecraft.McInstance? instance = null) + { + return GetConfigItem(key).GetValueNoType(instance?.PathInstance); + } + + /// + /// 初始化某个设置项的值。 + /// + public void Reset(string key, bool forceReload = false, ModMinecraft.McInstance? instance = null) + { + GetConfigItem(key).Reset(instance?.PathInstance); + } + + /// + /// 获取某个设置项的默认值。 + /// + public object GetDefault(string key) + { + return GetConfigItem(key).DefaultValueNoType; + } + + /// + /// 某个设置项是否从未被设置过。 + /// + public bool IsUnset(string key, ModMinecraft.McInstance? instance = null) + { + return GetConfigItem(key).IsDefault(instance?.PathInstance); + } + + #endregion + + #region Launch + + // 切换选择 + public void LaunchInstanceSelect(string Value) + { + ModBase.Log("[Setup] 当前选择的 Minecraft 版本:" + Value); + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "Version", + ModMinecraft.McInstanceSelected == null ? "" : ModMinecraft.McInstanceSelected.Name); + } + + public void LaunchFolderSelect(string Value) + { + ModBase.Log("[Setup] 当前选择的 Minecraft 文件夹:" + Value.Replace("$", ModBase.ExePath)); + ModMinecraft.McFolderSelected = Value.Replace("$", ModBase.ExePath); + } + + // 游戏内存 + public void LaunchRamType(int Type) + { + if (ModMain.FrmSetupLaunch is null) + return; + ModMain.FrmSetupLaunch.RamType(Type); + } + + #endregion + + #region Tool + + public void ToolDownloadThread(int Value) + { + ModNet.NetTaskThreadLimit = Value + 1; + } + + public void ToolDownloadSpeed(int Value) + { + if (Value <= 14) + ModNet.NetTaskSpeedLimitHigh = (long)Math.Round((Value + 1) * 0.1d * 1024d * 1024d); + else if (Value <= 31) + ModNet.NetTaskSpeedLimitHigh = (long)Math.Round((Value - 11) * 0.5d * 1024d * 1024d); + else if (Value <= 41) + ModNet.NetTaskSpeedLimitHigh = (Value - 21) * 1024 * 1024L; + else + ModNet.NetTaskSpeedLimitHigh = -1; + } + + #endregion + + #region UI + + // 启动器 + public void UiLauncherTransparent(int Value) + { + ModMain.FrmMain.Opacity = Value / 1000d + 0.4d; + } + + public void UiLauncherTheme(int Value) + { + ModSecret.ThemeRefresh(Value); + } + + public void UiBackgroundColorful(bool Value) + { + ModSecret.ThemeRefresh(); + } + + public void UiLockWindowSize(bool Value) + { + if (Value) + ModMain.FrmMain.RemoveResizer(); + else + ModMain.FrmMain.AddResizer(); + } + + // 视频背景 + public void UiAutoPauseVideo(bool Value) + { + if (!Value) + { + ModVideoBack.ForcePlay = true; + ModVideoBack.VideoPlay(); + } + else + { + ModVideoBack.ForcePlay = false; + if (ModVideoBack.IsGaming) + ModVideoBack.VideoPause(); + } + } + + // 背景图片 + public void UiBackgroundOpacity(int Value) + { + ModMain.FrmMain.ImgBack.Opacity = Value / 1000d; + } + + public void UiBackgroundBlur(int Value) + { + if (Value == 0) + ModMain.FrmMain.ImgBack.Effect = null; + else + ModMain.FrmMain.ImgBack.Effect = new BlurEffect { Radius = Value + 1 }; + ModMain.FrmMain.ImgBack.Margin = new Thickness(-(Value + 1) / 1.8d); + } + + public void UiBackgroundSuit(int Value) + { + if (ModMain.FrmMain.ImgBack.Background == null) + return; + var Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + var Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + if (Value == 0) + { + // 智能:当图片较小时平铺,较大时适应 + if (Width < ModMain.FrmMain.PanMain.ActualWidth / 2d && Height < ModMain.FrmMain.PanMain.ActualHeight / 2d) + Value = 4; // 平铺 + else + Value = 2; // 适应 + } + + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).TileMode = TileMode.None; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Viewport = new Rect(0d, 0d, 1d, 1d); + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ViewportUnits = BrushMappingMode.RelativeToBoundingBox; + switch (Value) + { + case 1: // 居中 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Center; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Center; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ModMain.FrmMain.ImgBack.Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + ModMain.FrmMain.ImgBack.Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + break; + } + case 2: // 适应 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.UniformToFill; + ModMain.FrmMain.ImgBack.Width = double.NaN; + ModMain.FrmMain.ImgBack.Height = double.NaN; + break; + } + case 3: // 拉伸 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.Fill; + ModMain.FrmMain.ImgBack.Width = double.NaN; + ModMain.FrmMain.ImgBack.Height = double.NaN; + break; + } + case 4: // 平铺 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).TileMode = TileMode.Tile; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Viewport = new Rect(0d, 0d, + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width, + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height); + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ViewportUnits = BrushMappingMode.Absolute; + ModMain.FrmMain.ImgBack.Width = double.NaN; + ModMain.FrmMain.ImgBack.Height = double.NaN; + break; + } + case 5: // 左上 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ModMain.FrmMain.ImgBack.Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + ModMain.FrmMain.ImgBack.Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + break; + } + case 6: // 右上 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ModMain.FrmMain.ImgBack.Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + ModMain.FrmMain.ImgBack.Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + break; + } + case 7: // 左下 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ModMain.FrmMain.ImgBack.Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + ModMain.FrmMain.ImgBack.Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + break; + } + case 8: // 右下 + { + ModMain.FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right; + ModMain.FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom; + ((ImageBrush)ModMain.FrmMain.ImgBack.Background).Stretch = Stretch.None; + ModMain.FrmMain.ImgBack.Width = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Width; + ModMain.FrmMain.ImgBack.Height = ((ImageBrush)ModMain.FrmMain.ImgBack.Background).ImageSource.Height; + break; + } + } + } + + // 字体 + public void UiFont(string value) + { + try + { + ModBase.SetLaunchFont(value); + } + catch (Exception ex) + { + ModBase.Log(ex, "字体加载失败", ModBase.LogLevel.Hint); + } + } + + // 主页 + public void UiCustomType(int Value) + { + if (ModMain.FrmSetupUI is null) + return; + switch (Value) + { + case 0: // 无 + { + ModMain.FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.HintCustom.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.HintCustomWarn.Visibility = Visibility.Collapsed; + break; + } + case 1: // 本地 + { + ModMain.FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomLocal.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.HintCustom.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.HintCustomWarn.Visibility = + Conversions.ToBoolean(States.Hint.UntrustedHomepage) + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupUI.HintCustom.Text = + $"从 PCL 文件夹下的 Custom.xaml 读取主页内容。{"\r\n"}你可以手动编辑该文件,向主页添加文本、图片、常用网站、快捷启动等功能。"; + ModMain.FrmSetupUI.HintCustom.EventType = ""; + ModMain.FrmSetupUI.HintCustom.EventData = ""; + break; + } + case 2: // 联网 + { + ModMain.FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomNet.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.HintCustom.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.HintCustomWarn.Visibility = + Conversions.ToBoolean(States.Hint.UntrustedHomepage) + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupUI.HintCustom.Text = + $"从指定网址联网获取主页内容。服主也可以用于动态更新服务器公告。{"\r\n"}如果你制作了稳定运行的联网主页,可以点击这条提示投稿,若合格即可加入预设!"; + ModMain.FrmSetupUI.HintCustom.EventType = "打开网页"; + ModMain.FrmSetupUI.HintCustom.EventData = "https://github.com/Meloong-Git/PCL/discussions/2528"; + break; + } + case 3: // 预设 + { + ModMain.FrmSetupUI.PanCustomPreset.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.HintCustom.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.HintCustomWarn.Visibility = Visibility.Collapsed; + break; + } + } + + ModMain.FrmSetupUI.CardCustom.TriggerForceResize(); + } + + +#if False + public static UiDarkMode(int value) + { + switch (value) + { + case 0: + IsDarkMode = false; + break; + case 1: + IsDarkMode = true; + break; + default: + IsDarkMode = SystemTheme.IsSystemInDarkMode(); + } + ThemeRefresh(); + } +#endif + + // 高级材质 + public void UiBlur(bool Value) + { + ModMain.FrmSetupUI.PanBlurValue.Visibility = Value ? Visibility.Visible : Visibility.Collapsed; + if (Value) + UiBlurValue(Conversions.ToInteger(Config.Preference.Blur.Radius)); + else + UiBlurValue(0); + } + + public void UiBlurValue(int Value) + { + System.Windows.Application.Current.Resources["BlurRadius"] = Value * 1.0d; + } + + public void UiBlurSamplingRate(int Value) + { + System.Windows.Application.Current.Resources["BlurSamplingRate"] = Value * 0.01d; + } + + public void UiBlurType(int Value) + { + System.Windows.Application.Current.Resources["BlurType"] = (KernelType)Value; + } + + // 顶部栏 + public void UiLogoType(int Value) + { + switch (Value) + { + case 0: // 无 + { + ModMain.FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.CELogo.Visibility = Visibility.Collapsed; + if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed; + } + + break; + } + case 1: // 默认 + { + ModMain.FrmMain.ShapeTitleLogo.Visibility = Visibility.Visible; + ModMain.FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.CELogo.Visibility = Visibility.Visible; + if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed; + } + + break; + } + case 2: // 文本 + { + ModMain.FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.LabTitleLogo.Visibility = Visibility.Visible; + ModMain.FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.CELogo.Visibility = Visibility.Visible; + if (ModMain.FrmSetupUI != null) + { + ModMain.FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoText.Visibility = Visibility.Visible; + ModMain.FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed; + } + + ModBase.Setup.Load("UiLogoText", true); + break; + } + case 3: // 图片 + { + ModMain.FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed; + ModMain.FrmMain.ImageTitleLogo.Visibility = Visibility.Visible; + ModMain.FrmMain.CELogo.Visibility = Visibility.Visible; + if (ModMain.FrmSetupUI != null) + { + ModMain.FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed; + ModMain.FrmSetupUI.PanLogoChange.Visibility = Visibility.Visible; + } + + try + { + ModMain.FrmMain.ImageTitleLogo.Source = ModBase.ExePath + @"PCL\Logo.png"; + } + catch (Exception ex) + { + ModMain.FrmMain.ImageTitleLogo.Source = null; + ModBase.Log(ex, "显示标题栏图片失败", ModBase.LogLevel.Msgbox); + } + + break; + } + } + + ModBase.Setup.Load("UiLogoLeft", true); + if (ModMain.FrmSetupUI != null) + ModMain.FrmSetupUI.CardLogo.TriggerForceResize(); + } + + public void UiLogoText(string Value) + { + ModMain.FrmMain.LabTitleLogo.Text = Value; + } + + public void UiLogoLeft(bool Value) + { + ModMain.FrmMain.PanTitleMain.ColumnDefinitions[0].Width = new GridLength( + Value && Operators.ConditionalCompareObjectEqual(Config.Preference.WindowTitleType, 0, false) ? 0 : 1, + GridUnitType.Star); + } + + public void UiHiddenPageDownload(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenPageSetup(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenPageTools(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupLaunch(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupUi(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupLauncherMisc(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupGameManage(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupJava(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupUpdate(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupGameLink(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupAbout(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupFeedback(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenSetupLog(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenToolsGameLink(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenToolsHelp(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenToolsTest(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionEdit(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionExport(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionSave(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionScreenshot(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionMod(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionResourcePack(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionShader(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionSchematic(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenVersionServer(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenFunctionSelect(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenFunctionModUpdate(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + public void UiHiddenFunctionHidden(bool Value) + { + PageSetupUI.HiddenRefresh(); + } + + #endregion + + #region System + + // 调试选项 + public void SystemDebugMode(bool Value) + { + ModBase.ModeDebug = Value; + } + + public void SystemDebugAnim(int Value) + { + ModAnimation.AniSpeed = Value >= 30 ? 200d : ModBase.MathClamp(Value * 0.1d + 0.1d, 0.1d, 3d); + } + + public void SystemHttpProxy(string value) + { + if (value.IsNullOrWhiteSpace()) return; + try + { + HttpProxyManager.Instance.CustomProxyAddress = new Uri(value); + } + catch (Exception ex) + { + ModBase.Log(ex, "HTTP 代理应用出错"); + } + } + + public void SystemHttpProxyType(int value) + { + var mode = (HttpProxyManager.ProxyMode)value; + HttpProxyManager.Instance.Mode = Enum.IsDefined(mode) + ? mode + : HttpProxyManager.Instance.Mode; + } + + public void SystemHttpProxyCustomUsername(string value) + { + if (!string.IsNullOrEmpty(value)) + { + var password = Conversions.ToString(Config.Network.HttpProxy.CustomPassword); + HttpProxyManager.Instance.Credentials = new NetworkCredential(value, password); + } + else + { + HttpProxyManager.Instance.Credentials = null; + } + } + + public void SystemHttpProxyCustomPassword(string value) + { + var username = Conversions.ToString(Config.Network.HttpProxy.CustomUsername); + if (!string.IsNullOrEmpty(username)) + HttpProxyManager.Instance.Credentials = new NetworkCredential(username, value); + else + HttpProxyManager.Instance.Credentials = null; + } + + #endregion + + #region Version + + // 游戏内存 + public void VersionRamType(int Type) + { + if (ModMain.FrmInstanceSetup is null) + return; + ModMain.FrmInstanceSetup.RamType(Type); + } + + // 服务器 + public void VersionServerLogin(int Type) + { + if (ModMain.FrmInstanceSetup is null) + return; + // 为第三方登录清空缓存以更新描述 + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "InstanceCache", ""); + if (PageInstanceLeft.Instance is null) + return; + PageInstanceLeft.Instance = new ModMinecraft.McInstance(PageInstanceLeft.Instance.Name).Load(); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Base/ModValidate.cs b/Plain Craft Launcher 2/Modules/Base/ModValidate.cs new file mode 100644 index 000000000..696a85aed --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/ModValidate.cs @@ -0,0 +1,582 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.IO; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem; + +namespace PCL; + +public static class ModValidate +{ + /// + /// 进行输入验证,并返回错误原因。 + /// 如果没有错误则返回空字符串。 + /// + public static string Validate(string Text, IEnumerable ValidateRules) + { + var Result = ""; + foreach (var ValidateRule in ValidateRules) + { + Result = ValidateRule.Validate(Text); + if (Result is null) + return ""; + if (!string.IsNullOrEmpty(Result)) + return Result; + } + + return Result; + } +} + +/// +/// 输入验证规则基类。要查看所有的输入验证规则,可在输入 Validate 后查看自动补全。 +/// +public abstract class ValidateType +{ + /// + /// 验证某字符串是否符合验证要求。若符合,返回空字符串;若不符合,返回错误原因;若需要中断检查并直接通过,返回 Nothing。 + /// + public abstract string Validate(string Str); +} + +/// +/// 若为空则直接通过检查。 +/// +public class ValidateNullable : ValidateType +{ + public override string Validate(string Str) + { + if (Str == null || string.IsNullOrEmpty(Str)) + return null; + return ""; + } +} + +/// +/// 不能为 Nothing 或空字符串(不包括全空格检查)。 +/// +public class ValidateNullOrEmpty : ValidateType +{ + public override string Validate(string Str) + { + if (Str == null || string.IsNullOrEmpty(Str)) + return "输入内容不能为空!"; + return ""; + } +} + +/// +/// 不能为 Nothing 或空字符串(包括全空格检查)。 +/// +public class ValidateNullOrWhiteSpace : ValidateType +{ + public override string Validate(string Str) + { + if (Str == null || string.IsNullOrWhiteSpace(Str)) + return "输入内容不能为空!"; + return ""; + } +} + +/// +/// 必须满足正则表达式。 +/// +public class ValidateRegex : ValidateType +{ + public ValidateRegex() + { + } // 用于 XAML 初始化 + + public ValidateRegex(string Regex, string ErrorDescription = "正则检查失败!") + { + this.Regex = Regex; + this.ErrorDescription = ErrorDescription; + } + + public string Regex { get; set; } + public string ErrorDescription { get; set; } = "正则检查失败!"; + + public override string Validate(string Str) + { + if (!Str.RegexCheck(Regex)) + return ErrorDescription; + return ""; + } +} + +/// +/// 必须是一个完整网址。 +/// +public class ValidateHttp : ValidateType +{ + public ValidateHttp() + { + } // 用于 XAML 初始化 + + public ValidateHttp(bool AllowsNullOrEmpty = false) + { + this.AllowsNullOrEmpty = AllowsNullOrEmpty; + } + + public bool AllowsNullOrEmpty { get; set; } + + public override string Validate(string Str) + { + if (AllowsNullOrEmpty && string.IsNullOrEmpty(Str)) + return ""; + if (Str.EndsWithF("/")) + Str = Str.Substring(0, Str.Length - 1); + if (!Str.RegexCheck(@"^(http[s]?)\://")) + return "输入的网址无效!"; + return ""; + } +} + +/// +/// 必须是一个完整网址或 UNC 路径。 +/// +public class ValidateHttpOrUnc : ValidateType +{ + public ValidateHttpOrUnc() + { + } // 用于 XAML 初始化 + + public ValidateHttpOrUnc(bool AllowsNullOrEmpty = false) + { + this.AllowsNullOrEmpty = AllowsNullOrEmpty; + } + + public bool AllowsNullOrEmpty { get; set; } + + public override string Validate(string Str) + { + if (AllowsNullOrEmpty && string.IsNullOrEmpty(Str)) + return ""; + if (Str.EndsWithF("/") || Str.EndsWithF(@"\")) + Str = Str.Substring(0, Str.Length - 1); + if (!(Str.RegexCheck(@"^(http[s]?)\://") || Str.StartsWithF(@"\\"))) + return "输入的网址无效!"; + return ""; + } +} + +/// +/// 必须为整数。 +/// +public class ValidateInteger : ValidateType +{ + public ValidateInteger() + { + } // 用于 XAML 初始化 + + public ValidateInteger(int Min, int Max) + { + this.Min = Min; + this.Max = Max; + } + + public int Min { get; set; } + public int Max { get; set; } = int.MaxValue; + + public override string Validate(string Str) + { + if (Str.Length > 9) + return "请输入一个大小合理的数字!"; + var Valed = (int)Math.Round(ModBase.Val(Str)); + if ((Valed.ToString() ?? "") != (Str ?? "")) + return "请输入一个整数!"; + if (ModBase.Val(Str) > Max) + return "不可超过 " + Max + "!"; + if (ModBase.Val(Str) < Min) + return "不可低于 " + Min + "!"; + return ""; + } +} + +/// +/// 长度限制。 +/// +public class ValidateLength : ValidateType +{ + public ValidateLength() + { + } // 用于 XAML 初始化 + + public ValidateLength(int Min, int Max = int.MaxValue) + { + this.Min = Min; + this.Max = Max; + } + + public int Min { get; set; } + public int Max { get; set; } = int.MaxValue; + + public override string Validate(string Str) + { + if (Strings.Len(Str) != Max && Max == Min) + return $"长度必须为 {Max} 个字符!"; + if (Strings.Len(Str) > Max) + return $"长度最长为 {Max} 个字符!"; + if (Strings.Len(Str) < Min) + return $"长度至少需 {Min} 个字符!"; + return ""; + } +} + +/// +/// 不能包含某些特定字符串。忽略大小写。 +/// +public class ValidateExcept : ValidateType +{ + public ValidateExcept() + { + ErrorMessage = "输入内容不能包含 %"; + } // 用于 XAML 初始化 + + public ValidateExcept(Collection Excepts, string ErrorMessage = "输入内容不能包含 %") + { + this.Excepts = Excepts; + this.ErrorMessage = ErrorMessage; + } + + public ValidateExcept(IEnumerable Excepts, string ErrorMessage = "输入内容不能包含 %") + { + this.Excepts = new Collection(); + this.ErrorMessage = ErrorMessage; + foreach (var Data in Excepts) + this.Excepts.Add(Data?.ToString() ?? ""); + } + + public Collection Excepts { get; set; } = new(); + public string ErrorMessage { get; set; } + + public override string Validate(string Str) + { + foreach (var Ch in Excepts) + if (Str.IndexOfF(Ch, true) >= 0) + { + if (ErrorMessage == null) + ErrorMessage = ""; + return ErrorMessage.Replace("%", Ch); + } + + return ""; + } +} + +/// +/// 不能与某些特定字符串相同。 +/// +public class ValidateExceptSame : ValidateType +{ + public ValidateExceptSame() + { + } + + public ValidateExceptSame(Collection Excepts, string ErrorMessage = "输入内容不能为 %", bool IgnoreCase = false) + { + this.Excepts = Excepts; + this.ErrorMessage = ErrorMessage; + this.IgnoreCase = IgnoreCase; + } + + public ValidateExceptSame(IEnumerable Excepts, string ErrorMessage = "输入内容不能为 %", bool IgnoreCase = false) + { + this.Excepts = new Collection(); + foreach (var Data in Excepts) + this.Excepts.Add(Data?.ToString() ?? ""); + this.ErrorMessage = ErrorMessage; + this.IgnoreCase = IgnoreCase; + } + + public Collection Excepts { get; set; } = new(); + public string ErrorMessage { get; set; } + public bool IgnoreCase { get; set; } + + public override string Validate(string Str) + { + if (Str is null) + return ErrorMessage.Replace("%", "null"); + foreach (var Ch in Excepts) + if (IgnoreCase) + { + if ((Str.ToLower() ?? "") == (Ch.ToLower() ?? "")) + return ErrorMessage.Replace("%", Ch); + } + else if (Str.Equals(Ch)) + { + return ErrorMessage.Replace("%", Ch); + } + // 使用 = 不确定是否会忽略大小写 + + return ""; + } +} + +/// +/// 对文件夹名的粗略的特化检测。 +/// +public class ValidateFolderName : ValidateType +{ + private readonly bool IsIgnoreSameName; + private readonly IEnumerable PathIgnore; + + public ValidateFolderName() + { + } + + public ValidateFolderName(string Path, bool UseMinecraftCharCheck = true, bool IgnoreCase = true, + bool IgnoreSameName = false) + { + this.Path = Path; + this.IgnoreCase = IgnoreCase; + this.UseMinecraftCharCheck = UseMinecraftCharCheck; + // On Error Resume Next + try + { + PathIgnore = new DirectoryInfo(Path).EnumerateDirectories(); + } + catch (DirectoryNotFoundException ex) // ignored + { + } + + IsIgnoreSameName = IgnoreSameName; + } + + public string Path { get; set; } + public bool UseMinecraftCharCheck { get; set; } = true; + public bool IgnoreCase { get; set; } = true; + + public override string Validate(string Str) + { + try + { + // 检查是否为空 + var LengthCheck = new ValidateNullOrWhiteSpace().Validate(Str); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查空格 + if (Str.StartsWithF(" ")) + return "文件夹名不能以空格开头!"; + if (Str.EndsWithF(" ")) + return "文件夹名不能以空格结尾!"; + // 检查长度 + LengthCheck = new ValidateLength(1, 100).Validate(Str); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查尾部小数点 + if (Str.EndsWithF(".")) + return "文件夹名不能以小数点结尾!"; + // 检查特殊字符 + var CharactCheck = + new ValidateExcept(new string(System.IO.Path.GetInvalidFileNameChars()) + (UseMinecraftCharCheck ? "!;" : ""), + "文件夹名不可包含 % 字符!").Validate(Str); + if (!string.IsNullOrEmpty(CharactCheck)) + return CharactCheck; + // 检查特殊字符串 + var InvalidStrCheck = new ValidateExceptSame( + new[] + { + "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", + "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³" + }, "文件夹名不可为 %!", true).Validate(Str); + if (!string.IsNullOrEmpty(InvalidStrCheck)) + return InvalidStrCheck; + // 检查 NTFS 8.3 文件名(#4505) + if (Str.RegexCheck(@".{2,}~\d")) + return "文件夹名不能包含这一特殊格式!"; + // 检查文件夹重名 + var Arr = new List(); + if (PathIgnore is not null) + foreach (var Folder in PathIgnore) + Arr.Add(Folder.Name); + if (!IsIgnoreSameName) + { + var SameNameCheck = new ValidateExceptSame(Arr, "不可与现有文件夹重名!", IgnoreCase).Validate(Str); + if (!string.IsNullOrEmpty(SameNameCheck)) + return SameNameCheck; + } + + return ""; + } + catch (Exception ex) + { + ModBase.Log(ex, "检查文件夹名出错"); + return "错误:" + ex.Message; + } + } +} + +/// +/// 对文件名的粗略的特化检测。 +/// +public class ValidateFileName : ValidateType +{ + public ValidateFileName() + { + } + + public ValidateFileName(string Name, bool UseMinecraftCharCheck = true, bool IgnoreCase = true) + { + this.Name = Name; + this.IgnoreCase = IgnoreCase; + this.UseMinecraftCharCheck = UseMinecraftCharCheck; + } + + public string Name { get; set; } + public bool UseMinecraftCharCheck { get; set; } = true; + public bool IgnoreCase { get; set; } = true; + public string ParentFolder { get; set; } = null; + public object RequireParentFolderExists { get; set; } = true; + + public override string Validate(string Str) + { + try + { + // 检查是否为空 + var LengthCheck = new ValidateNullOrWhiteSpace().Validate(Str); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查空格 + if (Str.StartsWithF(" ")) + return "文件名不能以空格开头!"; + if (Str.EndsWithF(" ")) + return "文件名不能以空格结尾!"; + // 检查长度 + LengthCheck = new ValidateLength(1, 253).Validate(Str + (ParentFolder ?? "")); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查尾部小数点 + if (Str.EndsWithF(".")) + return "文件名不能以小数点结尾!"; + // 检查特殊字符 + var CharactCheck = new ValidateExcept(new string(System.IO.Path.GetInvalidFileNameChars()) + (UseMinecraftCharCheck ? "!;" : ""), + "文件名不可包含 % 字符!").Validate(Str); + if (!string.IsNullOrEmpty(CharactCheck)) + return CharactCheck; + // 检查特殊字符串 + var InvalidStrCheck = new ValidateExceptSame( + new[] + { + "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", + "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³" + }, "文件名不可为 %!", true).Validate(Str); + if (!string.IsNullOrEmpty(InvalidStrCheck)) + return InvalidStrCheck; + // 检查 NTFS 8.3 文件名(#4505) + if (Str.RegexCheck(@".{2,}~\d")) + return "文件名不能包含这一特殊格式!"; + // 检查文件重名 + if (ParentFolder is not null) + { + var DirInfo = new DirectoryInfo(ParentFolder); + if (DirInfo.Exists) + { + var SameNameCheck = new ValidateExceptSame(DirInfo.EnumerateFiles("*").Select(f => f.Name), + "不可与现有文件重名!", IgnoreCase).Validate(Str); + if (!string.IsNullOrEmpty(SameNameCheck)) + return SameNameCheck; + } + else if (Conversions.ToBoolean(RequireParentFolderExists)) + { + return $"父文件夹不存在:{ParentFolder}"; + } + } + + return ""; + } + catch (Exception ex) + { + ModBase.Log(ex, "检查文件名出错"); + return "错误:" + ex.Message; + } + } +} + +/// +/// 要求输入一个可用的文件夹路径。 +/// +public class ValidateFolderPath : ValidateType +{ + public ValidateFolderPath() + { + } + + public ValidateFolderPath(bool UseMinecraftCharCheck) + { + this.UseMinecraftCharCheck = UseMinecraftCharCheck; + } + + public bool UseMinecraftCharCheck { get; set; } = true; + + public override string Validate(string Str) + { + // 去除尾部斜线,统一为 \ + Str = Str.Replace("/", @"\"); + if (!Str.TrimEnd(@"\").EndsWith(":")) + Str = Str.TrimEnd('\\'); + // 检查是否为空 + var LengthCheck = new ValidateNullOrWhiteSpace().Validate(Str); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查长度 + LengthCheck = new ValidateLength(1, 254).Validate(Str); + if (!string.IsNullOrEmpty(LengthCheck)) + return LengthCheck; + // 检查开头 + if (Str.StartsWithF(@"\\Mac\")) + goto Fin; + foreach (var Drive in FileSystem.Drives) + { + if ((Str.ToUpper() ?? "") == (Drive.Name ?? "")) + return ""; + if (Str.StartsWithF(Drive.Name, true)) + goto Fin; + } + + return "文件夹路径头存在错误!"; + Fin: ; + + // 对首层以外的路径检查 + for (int i = Str.StartsWithF(@"\\Mac\") ? 2 : 1, loopTo = Str.Split(@"\").Count() - 1; i <= loopTo; i++) + { + var SubStr = Str.Split(@"\")[i]; + // 检查是否为空 + var SubLengthCheck = new ValidateNullOrWhiteSpace().Validate(SubStr); + if (!string.IsNullOrEmpty(SubLengthCheck)) + return "文件夹路径存在错误!"; + // 检查特殊字符 + var CharactCheck = + new ValidateExcept(new string(Path.GetInvalidFileNameChars()) + (UseMinecraftCharCheck ? "!;" : ""), "路径中存在无效字符!") + .Validate(SubStr); + if (!string.IsNullOrEmpty(CharactCheck)) + return CharactCheck; + // 检查头部空格 + if (SubStr.StartsWithF(" ")) + return "文件夹名不能以空格开头!"; + if (SubStr.EndsWithF(" ")) + return "文件夹名不能以空格结尾!"; + // 检查尾部小数点 + if (SubStr.EndsWithF(".")) + return "文件夹名不能以小数点结尾!"; + // 检查特殊字符串 + var InvalidStrCheck = new ValidateExceptSame( + new[] + { + "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", + "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", + "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³" + }, "文件夹名不可为 %!").Validate(SubStr); + if (!string.IsNullOrEmpty(InvalidStrCheck)) + return InvalidStrCheck; + // 检查 NTFS 8.3 文件名(#4505) + if (Str.RegexCheck(@".{2,}~\d")) + return "文件夹名不能包含这一特殊格式!"; + } + + return ""; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Base/MyBitmap.cs b/Plain Craft Launcher 2/Modules/Base/MyBitmap.cs new file mode 100644 index 000000000..b49ad61df --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Base/MyBitmap.cs @@ -0,0 +1,282 @@ +using PCL.Core.UI.Media; +using System.Collections.Concurrent; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using PixelFormat = System.Drawing.Imaging.PixelFormat; + + +namespace PCL; + +/// +/// 一个万能的自动图片类型转换工具类 +/// +public class MyBitmap +{ + private static readonly ConcurrentDictionary Cache = []; + + /// + /// 存储的图片 + /// + public readonly Bitmap Picture; + + #region Constructor + + /// + /// Contructe with argument + /// + /// The file path or resource name that used to contruct + /// Throws when failed to load . + /// Throws when image type is not supported or image is broken. + public MyBitmap(string filePathOrResName) + { + try + { + filePathOrResName = filePathOrResName.Replace("pack://application:,,,/images/", ModBase.PathImage); + if (filePathOrResName.StartsWithF(ModBase.PathImage)) + { + if (Cache.TryGetValue(filePathOrResName, out var value)) + { + Picture = value.Picture; + } + else + { + var conterted = ConvertToImageSource(filePathOrResName); + + if (conterted is null) + { + throw new InvalidDataException("Cannot convert resource path to ImageSource"); + } + + Picture = new MyBitmap(conterted).Picture; + Cache.TryAdd(filePathOrResName, Picture); + } + } + else + { + // 使用这种自己接管 FileStream 的方法加载才能解除文件占用 + using var picStream = new FileStream(filePathOrResName, FileMode.Open); + if (picStream.Length > 2L && picStream.ReadByte() == 82 && picStream.ReadByte() == 73) + { + picStream.Seek(0L, SeekOrigin.Begin); + // 调用 WIC 转换,需要系统内置 WebP 组件,专治各种精简系统 + using var ms = picStream.FromWebpToPng(); + Picture = new Bitmap(ms); + } + else + { + Picture = new Bitmap(picStream); + } + } + } + catch (Exception ex) + { + Picture = (Bitmap)System.Windows.Application.Current.TryFindResource(filePathOrResName); + if (Picture is null) + { + Picture = new Bitmap(1, 1); + if (ex is ArgumentException) + { + throw new InvalidDataException($"图片格式不支持,或图片文件损坏({filePathOrResName})", ex); + } + + throw new Exception($"加载 MyBitmap 意外失败({filePathOrResName})", ex); + } + + ModBase.Log(ex, $"指定类型有误的 MyBitmap 加载({filePathOrResName})", ModBase.LogLevel.Developer); + } + } + + /// + /// Contruct from + /// + public MyBitmap(ImageSource image) + { + using var ms = new MemoryStream(); + var encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create((BitmapSource)image)); + encoder.Save(ms); + Picture = new Bitmap(ms); + } + + /// + /// Construct from + /// + public MyBitmap(Image image) + { + Picture = (Bitmap)image; + } + + /// + /// Construct from + /// + public MyBitmap(Bitmap image) + { + Picture = image; + } + + /// + /// Construct from + /// + public MyBitmap(ImageBrush image) + { + using var ms = new MemoryStream(); + var encoder = new BmpBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create((BitmapSource)image.ImageSource)); + encoder.Save(ms); + Picture = new Bitmap(ms); + } + + #endregion + + #region Implicit Converter + + /// + /// Convert to + /// + public static implicit operator MyBitmap(Image? image) + { + ArgumentNullException.ThrowIfNull(image); + return new MyBitmap(image); + } + + /// + /// Convert to + /// + public static implicit operator Image(MyBitmap? image) + { + ArgumentNullException.ThrowIfNull(image); + return image.Picture; + } + + /// + /// Convert to + /// + public static implicit operator MyBitmap(ImageSource? image) + { + ArgumentNullException.ThrowIfNull(image); + return new MyBitmap(image); + } + + /// + /// Convert to + /// + public static implicit operator ImageSource(MyBitmap? image) + { + ArgumentNullException.ThrowIfNull(image); + + var bitmapPic = image.Picture; + var rect = new Rectangle(0, 0, bitmapPic.Width, bitmapPic.Height); + var bitmapData = bitmapPic.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); + try + { + var result = BitmapSource.Create(bitmapPic.Width, + bitmapPic.Height, + bitmapPic.HorizontalResolution, + bitmapPic.VerticalResolution, + PixelFormats.Bgra32, + null, + bitmapData.Scan0, + rect.Width * rect.Height * 4, + bitmapData.Stride); + + result.Freeze(); + return result; + } + finally + { + bitmapPic.UnlockBits(bitmapData); + } + } + + /// + /// Convert to + /// + public static implicit operator MyBitmap(Bitmap? image) + { + ArgumentNullException.ThrowIfNull(image); + return new MyBitmap(image); + } + + /// + /// Convert to + /// + public static implicit operator Bitmap(MyBitmap? image) + { + ArgumentNullException.ThrowIfNull(image); + return image.Picture; + } + + /// + /// Convert to + /// + public static implicit operator MyBitmap(ImageBrush? image) + { + ArgumentNullException.ThrowIfNull(image); + return new MyBitmap(image); + } + + /// + /// Convert to + /// + public static implicit operator ImageBrush(MyBitmap? image) + { + ArgumentNullException.ThrowIfNull(image); + return new ImageBrush(new MyBitmap(image.Picture)); + } + + #endregion + + /// + /// 获取裁切的图片 + /// + /// + /// 这个方法不会导致原对象改变,而是会返回一个新的对象。 + /// + public MyBitmap Clip(int x, int y, int width, int height) + { + var bitmap = new Bitmap(width, height, Picture.PixelFormat); + bitmap.SetResolution(Picture.HorizontalResolution, Picture.VerticalResolution); + using var graph = Graphics.FromImage(bitmap); + graph.InterpolationMode = InterpolationMode.NearestNeighbor; + graph.TranslateTransform(-x, -y); + graph.DrawImage(Picture, new Rectangle(0, 0, Picture.Width, Picture.Height)); + + return bitmap; + } + + /// + /// 获取旋转或翻转后的图片 + /// + /// + /// 这个方法不会导致原对象改变,而是会返回一个新的对象。 + /// + public MyBitmap RotateFlip(RotateFlipType type) + { + var bitmap = new Bitmap(Picture); + bitmap.SetResolution(Picture.HorizontalResolution, Picture.VerticalResolution); + bitmap.RotateFlip(type); + return bitmap; + } + + /// + /// 将图像保存到文件。 + /// + public void Save(string filePath) + { + BitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create((BitmapSource)this)); + using var fileStream = new FileStream(filePath, FileMode.Create); + encoder.Save(fileStream); + } + + private static readonly ImageSourceConverter ImageSourceConverter = new(); + + private ImageSource? ConvertToImageSource(string val) + { + return ImageSourceConverter.ConvertFromString(val) as ImageSource; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs new file mode 100644 index 000000000..615280dfb --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.cs @@ -0,0 +1,3574 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.Utils; +using PCL.Core.Utils.Hash; +using ProtoBuf; + +namespace PCL; + +public static class ModComp +{ + public enum CompLoaderType + { + // https://docs.curseforge.com/?http#tocS_ModLoaderType + /// + /// 模组加载器 + /// + Any = 0, + + /// + /// 模组加载器 + /// + Forge = 1, + + /// + /// 模组加载器 + /// + LiteLoader = 3, + + /// + /// 模组加载器 + /// + Fabric = 4, + + /// + /// 模组加载器 + /// + Quilt = 5, + + /// + /// 模组加载器 + /// + NeoForge = 6, + + /// + /// 材质包 + /// + Minecraft = 7, + + /// + /// 光影包 + /// + Canvas = 8, + + /// + /// 光影包 + /// + Iris = 9, + + /// + /// 光影包 + /// + OptiFine = 10, + + /// + /// 光影包 + /// + Vanilla = 11, + + /// + /// LabyMod 客户端 + /// + LabyMod = 12 + } + + /// + /// 搜索结果排序方式 + /// + public enum CompSortType + { + /// + /// 默认 + /// + Default = 1, + + /// + /// 相关性 (CurseForge Name (4) / Modrinth relevance) + /// + Relevance = 2, + + /// + /// 下载量 (CurseForge TotalDownloads (6) / Modrinth downloads) + /// + Downloads = 3, + + /// + /// 关注量 (CurseForge Popularity (2) / Modrinth follows) + /// + Follows = 4, + + /// + /// 最新发布 (CurseForge ReleasedDate (11) / Modrinth newest) + /// + Newest = 5, + + /// + /// 最近更新 (CurseForge LastUpdated (3) / Modrinth updated) + /// + Updated = 6 + } + + [Flags] + public enum CompSourceType + { + CurseForge = 1, + Modrinth = 2, + Any = CurseForge | Modrinth + } + + public enum CompType + { + /// + /// 允许任意种类,或种类未知。 + /// + Any = -1, + + /// + /// Mod。 + /// + Mod = 0, + + /// + /// 整合包。 + /// + ModPack = 1, + + /// + /// 资源包。 + /// + ResourcePack = 2, + + /// + /// 光影包。 + /// + Shader = 3, + + /// + /// CurseForge:数据包。 + /// Modrinth:数据包,或数据包与 Mod 的混合。 + /// + DataPack = 4, + + /// + /// 服务端插件。 + /// + Plugin = 5, + + /// + /// 投影原理图。 + /// + Schematic = 6, + + /// + /// 世界。 + /// + World = 7 + } + + #region CompFavorites | 收藏 + + public class CompFavorites + { + private static List _FavoritesList; + + /// + /// 收藏的工程列表 + /// + public static List FavoritesList + { + get + { + if (_FavoritesList is null) + { + var RawData = States.Game.CompFavorites; + List RawList = null; + // 尝试作为新格式解析 + try + { + RawList = JsonSerializer.Deserialize>(RawData); + } + catch (Exception ex1) + { + // 尝试作为旧格式(HashSet)迁移 + try + { + var Migrate = JsonSerializer.Deserialize>(RawData); + if (Migrate is not null) RawList = new List { GetNewFav("默认", Migrate) }; + } + catch (Exception ex2) + { + // 两种都失败,使用默认 + } + } + + // 最终兜底:确保至少有一个收藏夹 + if (RawList is null || RawList.Count == 0) RawList = new List { GetNewFav("默认", null) }; + _FavoritesList = RawList; + Save(); + } + + return _FavoritesList; + } + set + { + _FavoritesList = value; + foreach (var item in _FavoritesList) + item.Notes = item.Notes.Where(n => !string.IsNullOrWhiteSpace(n.Value)).ToDictionary(); + var RawList = JArray.FromObject(_FavoritesList); + States.Game.CompFavorites = JsonSerializer.Serialize(_FavoritesList); + } + } + + public static string GetShareCode(HashSet Data) + { + try + { + return JsonSerializer.Serialize(Data); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavorites] 生成分享出错"); + } + + return ""; + } + + public static HashSet GetIdsByShareCode(string Code) + { + try + { + return JsonSerializer.Deserialize>(Code); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavorites] 通过分享获取 ID 出错"); + } + + return new HashSet(); + } + + /// + /// 显示收藏菜单。 + /// + /// + /// + public static void ShowMenu(CompProject Project, UIElement Pos, Action ClosedCallBack = null) + { + var Body = new ContextMenu(); + foreach (var i in FavoritesList) + { + var Item = new MyMenuItem(); + Item.MaxWidth = 240d; + var HasFavs = i.Favs.Contains(Project.Id); + if (HasFavs) + { + Item.Header = $"取消收藏 {i.Name}"; + Item.Icon = ModBase.Logo.IconButtonLikeFill; + } + else + { + Item.Header = $"收藏到 {i.Name}"; + Item.Icon = ModBase.Logo.IconButtonLikeLine; + } + + Item.Click += (_, _) => + { + try + { + if (HasFavs) + { + i.Favs.Remove(Project.Id); + ModMain.Hint($"已将 {Project.TranslatedName} 从 {i.Name} 中删除", ModMain.HintType.Finish); + } + else + { + i.Favs.Add(Project.Id); + ModMain.Hint($"已将 {Project.TranslatedName} 添加到 {i.Name} 中", ModMain.HintType.Finish); + } + + Save(); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavorites] 改变收藏项出错"); + } + }; + Body.Items.Add(Item); + } + + Body.Closed += (_, _) => ClosedCallBack?.Invoke(); + Body.Placement = PlacementMode.Bottom; + Body.PlacementTarget = Pos; + Body.IsOpen = true; + } + + /// + /// 显示收藏菜单。 + /// + public static void ShowMenu(List Project, UIElement Pos, Action ClosedCallBack = null) + { + var Body = new ContextMenu(); + foreach (var i in FavoritesList) + { + var Item = new MyMenuItem + { + MaxWidth = 240d, + Header = $"收藏到 {i.Name}" + }; + Item.Click += (_, _) => + { + try + { + var Count = i.Favs.Count; + Project.Select(p => p.Id).ToList().ForEach(x => i.Favs.Add(x)); + Save(); + var SuccessCount = i.Favs.Count - Count; + var FailedCount = Project.Count - SuccessCount; + ModMain.Hint( + $"已将 {SuccessCount} 个资源添加到 {i.Name} 中{(FailedCount > 0 ? $",{FailedCount} 个资源已添加" : "")}!", + ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavorites] 改变收藏项出错"); + } + }; + Body.Items.Add(Item); + } + + Body.Closed += (_, _) => ClosedCallBack?.Invoke(); + Body.Placement = PlacementMode.Bottom; + Body.PlacementTarget = Pos; + Body.IsOpen = true; + } + + /// + /// 保存收藏夹数据 + /// + public static void Save() + { + FavoritesList = _FavoritesList; + } + + /// + /// 获取一个新的收藏夹 + /// + /// + /// 没有传 Nothing + /// + public static FavData GetNewFav(string Name, HashSet FavList) + { + var res = new FavData { Name = Name, Id = Guid.NewGuid().ToString() }; + if (FavList is null) + res.Favs = new HashSet(); + else + res.Favs = FavList; + return res; + } + + public static bool IsFavourite(string Id) + { + if (FavoritesList is null) + return false; + foreach (var i in FavoritesList) + if (i.Favs.Contains(Id)) + return true; + return false; + } + + public class FavData + { + /// + /// 收藏夹名称 + /// + /// + [JsonPropertyName("Name")] + public string Name { get; set; } + + /// + /// Guid + /// + /// + [JsonPropertyName("Id")] + public string Id { get; set; } + + /// + /// 收藏的工程 ID 列表 + /// + /// + [JsonPropertyName("Favs")] + public HashSet Favs { get; set; } = new(); + + /// + /// 备注 + /// + /// + [JsonPropertyName("Notes")] + public Dictionary Notes { get; set; } = new(); + } + } + + #endregion + + #region CompProject | 项目信息 + + public class CompRequest + { + /// + /// 通过项目 Id 判断是否来自 CurseForge + /// + /// + /// + public static bool IsFromCurseForge(string Id) + { + var res = 0; + return int.TryParse(Id, out res); // CurseForge 数字 ID Modrinth 乱序 ID + } + + /// + /// 通过一堆 ID 从 Modrinth 那获取项目信息 + /// + /// + /// + public static async Task> GetListByIdsFromModrinthAsync(List Ids) + { + var Res = new List(); + try + { + await Task.Run(() => + { + var RawProjectsData = + ModDownload.DlModRequest($"https://api.modrinth.com/v2/projects?ids=[\"{Ids.Join("\",\"")}\"]", + true); + foreach (var RawData in (IEnumerable)RawProjectsData) + Res.Add(new CompProject((JObject)RawData)); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "从 Modrinth 获取数据失败"); + } + + return Res; + } + + /// + /// 通过一堆 ID 从 CurseForge 那获取项目信息 + /// + /// + /// + public static async Task> GetListByIdsFromCurseforgeAsync(List ids) + { + var res = new List(); + try + { + // 使用 Task.Run 将同步的 DlModRequest 包装为异步 + await Task.Run(() => + { + // 构建请求 Body,建议使用 string.Join + var jsonBody = "{\"modIds\": [" + string.Join(",", ids) + "]}"; + + // DlModRequest 返回 object,先强转 JObject,再获取 "data" 并强转为 JArray + var response = (JObject)ModDownload.DlModRequest( + "https://api.curseforge.com/v1/mods", + "POST", + jsonBody, + "application/json" + ); + + var rawProjectsData = (JArray)response["data"]; + + // 2. 使用 LINQ 快速转换并填充列表 + if (rawProjectsData != null) + { + var projectList = rawProjectsData + .Cast() + .Select(data => new CompProject(data)) + .ToList(); + + res.AddRange(projectList); + } + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "Failed to get project data from CurseForge"); + } + + return res; + } + + public static List GetCompProjectsByIds(List Input) + { + return GetCompProjectsByIdsAsync(Input).GetAwaiter().GetResult(); + } + + public static async Task> GetCompProjectsByIdsAsync(List Input) + { + if (Input?.Any() == false) + return new List(); + + var modrinthIds = new List(); + var curseForgeIds = new List(); + foreach (var id in Input) + if (IsFromCurseForge(id)) + curseForgeIds.Add(id); + else + modrinthIds.Add(id); + + var tasks = new List>>(); + if (curseForgeIds.Any()) tasks.Add(GetListByIdsFromCurseforgeAsync(curseForgeIds)); + if (modrinthIds.Any()) tasks.Add(GetListByIdsFromModrinthAsync(modrinthIds)); + + await Task.WhenAll(tasks.ToArray()); + var result = new List(); + foreach (var task in tasks) + result.AddRange(task.Result); + + return result; + } + } + + #endregion + + #region CompClipboard | 剪贴板识别 + + public class CompClipboard + { + // 剪贴板已读取内容 + public static string? CurrentText; + + // 识别剪贴板内容 + public static void GetClipboardResource() + { + string? text = null; + ModBase.RunInUiWait(() => text = Clipboard.GetText()); + + if (string.IsNullOrEmpty(text) || text == CurrentText) return; + CurrentText = text; + + // 在新线程中处理网络请求 + ModBase.RunInNewThread(() => + { + try + { + string? slug = null; + string? projectId = null; + var processedText = text.Replace("https://", "").Replace("http://", ""); + + // 1. 处理 CurseForge 链接 + if (processedText.Contains("curseforge.com/minecraft/")) + { + var parts = processedText.Split('/'); + if (parts.Length < 4) return; + + var categoryUrl = parts[2]; + slug = parts[3]; + + // 获取资源信息 + var json = (JObject)ModDownload.DlModRequest( + $"https://api.curseforge.com/v1/mods/search?gameId=432&slug={slug}", true); + var dataArray = (JArray)json["data"]; + + if (dataArray.Any()) + { + var firstData = (JObject)dataArray[0]; + var receivedClassId = firstData["classId"]?.ToString(); + + // 映射分类 ID + var categoryMapping = new Dictionary + { + { "mc-mods", "6" }, + { "modpacks", "4471" }, + { "texture-packs", "12" }, + { "shaders", "6552" } + }; + + if (categoryMapping.TryGetValue(categoryUrl, out var targetClassId) && + receivedClassId != targetClassId) + { + // 如果分类不匹配,带上 classId 重新搜索 + json = (JObject)ModDownload.DlModRequest( + $"https://api.curseforge.com/v1/mods/search?gameId=432&slug={slug}&classId={targetClassId}", + true); + dataArray = (JArray)json["data"]; + } + + if (dataArray.Any()) projectId = dataArray[0]["id"]?.ToString(); + } + } + // 2. 处理 Modrinth 链接 + else if (processedText.Contains("modrinth.com/")) + { + var parts = processedText.Split('/'); + if (parts.Length < 3) return; + + slug = parts[2]; + var json = (JObject)ModDownload.DlModRequest($"https://api.modrinth.com/v2/project/{slug}", + true); + projectId = json["id"]?.ToString(); + } + else + { + return; + } + + if (string.IsNullOrEmpty(projectId)) return; + ModBase.Log($"[Clipboard] Found ProjectId: {projectId}"); + + // 3. UI 交互:跳转到详情页 + System.Windows.Application.Current.Dispatcher.BeginInvoke(new Func(async () => + { + if (ModMain.MyMsgBox( + "PCL detected a resource link in clipboard. Do you want to jump to the details page?", + "Link Detected", "Confirm", "Cancel", ForceWait: true) == 1) + { + ModMain.Hint("Fetching resource info..."); + + var ids = new List { projectId }; + var compProjects = await CompRequest.GetCompProjectsByIdsAsync(ids); + + if (compProjects.Count == 0) + { + ModMain.Hint("Invalid resource content.", ModMain.HintType.Critical); + return; + } + + ModMain.FrmMain.PageChange(new FormMain.PageStackData + { + Page = FormMain.PageType.CompDetail, + Additional = new object[] + { + compProjects.First(), new List(), string.Empty, CompLoaderType.Any, + CompType.Any + } + }); + } + })); + } + catch (Exception ex) + { + ModBase.Log(ex, "Error processing clipboard resource"); + } + }, "Clipboard Resource Processing"); + } + } + + #endregion + + #region CompDatabase | Mod 数据库 + + private static readonly Lazy _dbInitializer = new(InitializeModDbAndGetConnectionString); + + private static string CompDBConnectionString => _dbInitializer.Value; + + private static string InitializeModDbAndGetConnectionString() + { + ModBase.Log("[DB] 解压 ModData (SQLite) 中"); + using (var compressedDbData = ModBase.GetResourceStream("Resources/mcmod.buf")) + { + using (var trueDbFile = new GZipStream(compressedDbData, CompressionMode.Decompress)) + { + using (var ms = new MemoryStream()) + { + trueDbFile.CopyTo(ms); + ms.Seek(0L, SeekOrigin.Begin); + var fileHash = ModBase.GetHexString(SHA1Provider.Instance.ComputeHash(ms)); + + var dbPath = Path.GetFullPath(Path.Combine(ModBase.PathTemp, $@"Cache\ModData{fileHash}.sqlite")); + if (!File.Exists(dbPath)) + { + ms.Seek(0L, SeekOrigin.Begin); + var entries = Serializer.Deserialize>(ms); + Directory.CreateDirectory(Path.GetDirectoryName(dbPath)); + using (var buildDbConnection = new SqliteConnection($"Data Source=\"{dbPath}\";Pooling=False")) + { + buildDbConnection.Open(); + buildDbConnection.Execute(@" + CREATE TABLE ModTranslation ( + WikiId INTEGER, + ChineseName TEXT, + CurseForgeSlug TEXT, + ModrinthSlug TEXT + ); + CREATE INDEX idx_curseforge ON ModTranslation (CurseForgeSlug); + CREATE INDEX idx_modrinth ON ModTranslation (ModrinthSlug); + CREATE INDEX idx_chinesename ON ModTranslation (ChineseName); + "); + + using (var tran = buildDbConnection.BeginTransaction()) + { + var insertSql = + @"INSERT INTO ModTranslation (WikiId, ChineseName, CurseForgeSlug, ModrinthSlug) + VALUES (@WikiId, @ChineseName, @CurseForgeSlug, @ModrinthSlug)"; + + foreach (var entry in entries) + buildDbConnection.Execute(insertSql, entry, tran); + + tran.Commit(); + } + } + } + + return $"Data Source=\"{dbPath}\""; + } + } + } + } + + private static SqliteConnection CompDB + { + get + { + var conn = new SqliteConnection(CompDBConnectionString); + conn.Open(); + return conn; + } + } + + private static CompDatabaseEntry GetCompWikiEntryBySlug(string slug) + { + try + { + using (var conn = CompDB) + { + return conn.QueryFirstOrDefault( + "SELECT * FROM ModTranslation WHERE CurseForgeSlug = @s OR ModrinthSlug = @s LIMIT 1", + new { s = slug }); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取模组翻译信息失败", ModBase.LogLevel.Hint); + return null; + } + } + + [ProtoContract] + private class CompDatabaseEntry + { + /// + /// McMod 的对应 ID。 + /// + [ProtoMember(1)] + public int WikiId { get; set; } + + /// + /// 中文译名。空字符串代表没有翻译。 + /// + [ProtoMember(2)] + public string ChineseName { get; set; } = ""; + + /// + /// CurseForge Slug(例如 advanced-solar-panels)。 + /// + [ProtoMember(3)] + public string CurseForgeSlug { get; set; } + + /// + /// Modrinth Slug(例如 advanced-solar-panels)。 + /// + [ProtoMember(4)] + public string ModrinthSlug { get; set; } + + public override string ToString() + { + return (CurseForgeSlug ?? "") + "&" + (ModrinthSlug ?? "") + "|" + WikiId + "|" + ChineseName; + } + } + + #endregion + + #region CompProject | 工程信息 + + // 类定义 + + public class CompProject + { + /// + /// CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。 + /// + public readonly List CurseForgeFileIds; + + /// + /// 英文描述。 + /// + public readonly string Description; + + /// + /// 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量! + /// + public readonly int DownloadCount; + + /// + /// 支持的 Drop 编号,从高到低排序,不为 Nothing。 + /// 例如:261(26.1.x)、180(1.18.x)。 + /// + public readonly List Drops; + + // 源信息 + + /// + /// 该工程信息来自 CurseForge 还是 Modrinth。 + /// + public readonly bool FromCurseForge; + + /// + /// CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。 + /// + public readonly string Id; + + /// + /// 最后一次更新的时间。可能为 Nothing。 + /// + public readonly DateTime? LastUpdate; + + /// + /// 支持的 Mod 加载器列表。可能为空。 + /// + public readonly List ModLoaders; + + // 描述性信息 + + /// + /// 原始的英文名称。 + /// + public readonly string RawName; + + /// + /// 工程的短名。例如 technical-enchant。 + /// + public readonly string Slug; + + /// + /// 描述性标签的内容。已转换为中文。 + /// + public readonly List Tags; + + /// + /// 工程的种类。 + /// 由于 Modrinth 混合使用 Mod 和数据包,结果不一定准确。 + /// + public readonly CompType Type; + + /// + /// 来源网站的工程页面网址。确保格式一定标准。 + /// CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei + /// Modrinth:https://modrinth.com/mod/technical-enchant + /// + public readonly string Website; + + private CompDatabaseEntry _DatabaseEntry; + + // 数据库信息 + + private bool LoadedDatabase; + + /// + /// Logo 图片的下载地址。 + /// 若为 Nothing 则没有,保证不为空字符串。 + /// + public string LogoUrl; + + // 实例化 + + /// + /// 从工程 Json 中初始化实例。若出错会抛出异常。 + /// + public CompProject(JObject Data) + { + if (Data.ContainsKey("Tags")) + { + #region CompJson + + FromCurseForge = (string)Data["DataSource"] == "CurseForge"; + Type = (CompType)Data["Type"].ToObject(); + Slug = (string)Data["Slug"]; + Id = (string)Data["Id"]; + if (Data.ContainsKey("CurseForgeFileIds")) + CurseForgeFileIds = ((JArray)Data["CurseForgeFileIds"]).Select(t => t.ToObject()).ToList(); + RawName = (string)Data["RawName"]; + Description = (string)Data["Description"]; + Website = (string)Data["Website"]; + if (Data.ContainsKey("LastUpdate")) + LastUpdate = (DateTime?)Data["LastUpdate"]; + DownloadCount = (int)Data["DownloadCount"]; + if (Data.ContainsKey("ModLoaders")) + ModLoaders = ((JArray)Data["ModLoaders"]).Select(t => (CompLoaderType)t.ToObject()).ToList(); + else + ModLoaders = new List(); + Tags = ((JArray)Data["Tags"]).Select(t => t.ToString()).ToList(); + if (Data.ContainsKey("LogoUrl")) + LogoUrl = (string)Data["LogoUrl"]; + if (Data.ContainsKey("Drops")) + Drops = ((JArray)Data["Drops"]).Select(t => t.ToObject()).ToList(); + else + Drops = new List(); + } + + #endregion + + else + { + FromCurseForge = Data.ContainsKey("summary"); + if (FromCurseForge) + { + #region CurseForge + + // 简单信息 + Id = (string)Data["id"]; + Slug = (string)Data["slug"]; + RawName = (string)Data["name"]; + Description = (string)Data["summary"]; + Website = Data["links"]["websiteUrl"].ToString().TrimEnd('/'); + LastUpdate = (DateTime?)Data["dateReleased"]; // #1194 + DownloadCount = (int)Data["downloadCount"]; + if (Data["logo"].Count() > 0) + { + if (Data["logo"]["thumbnailUrl"] is null || (string)Data["logo"]["thumbnailUrl"] == "") + LogoUrl = (string)Data["logo"]["url"]; + else + LogoUrl = (string)Data["logo"]["thumbnailUrl"]; + } + + if (string.IsNullOrEmpty(LogoUrl)) + LogoUrl = null; + // Type + if (Website.Contains("/mc-mods/") || Website.Contains("/mod/")) + Type = CompType.Mod; + else if (Website.Contains("/modpacks/")) + Type = CompType.ModPack; + else if (Website.Contains("/resourcepacks/")) + Type = CompType.ResourcePack; + else if (Website.Contains("/texture-packs/")) + Type = CompType.ResourcePack; + else if (Website.Contains("/shaders/")) + Type = CompType.Shader; + else if (Website.Contains("/worlds/")) + Type = CompType.World; + else + Type = CompType.DataPack; + // FileIndexes / VanillaMajorVersions / ModLoaders + ModLoaders = new List(); + var Files = new List>>(); // FileId, GameVersions + foreach (var File in Data["latestFiles"] ?? new JArray()) + { + var NewFile = new CompFile((JObject)File, Type); + if (!NewFile.Available) + continue; + ModLoaders.AddRange(NewFile.ModLoaders); + var GameVersions = File["gameVersions"].ToObject>(); + if (!GameVersions.Any(v => ModMinecraft.McInstanceInfo.IsFormatFit(v))) + continue; + Files.Add(new KeyValuePair>((int)File["id"], GameVersions)); + } + + foreach (var File in Data["latestFilesIndexes"] ?? new JArray()) // 这俩玩意儿包含的文件不一样,见 #3599 + { + if (!ModMinecraft.McInstanceInfo.IsFormatFit((string)File["gameVersion"])) + continue; + Files.Add(new KeyValuePair>((int)File["fileId"], + new[] { File["gameVersion"].ToString() }.ToList())); + } + + CurseForgeFileIds = Files.Select(f => f.Key).Distinct().ToList(); + Drops = Files.SelectMany(f => f.Value).Select(v => ModMinecraft.McInstanceInfo.VersionToDrop(v)) + .Where(v => v > 0).Distinct().OrderByDescending(v => v).ToList(); + ModLoaders = ModLoaders.Distinct().OrderBy(t => t).ToList(); + // Tags + Tags = new List(); + foreach (var Category in (Data["categories"] ?? new JArray()).Select(t => (int)t["id"]).Distinct() + .OrderByDescending(c => c)) // 镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831) + switch (Category) + { + // Mod + case 406: + { + Tags.Add("世界元素"); + break; + } + case 407: + { + Tags.Add("生物群系"); + break; + } + case 410: + { + Tags.Add("维度"); + break; + } + case 408: + { + Tags.Add("矿物/资源"); + break; + } + case 409: + { + Tags.Add("天然结构"); + break; + } + case 412: + { + Tags.Add("科技"); + break; + } + case 415: + { + Tags.Add("管道/物流"); + break; + } + case 4843: + { + Tags.Add("自动化"); + break; + } + case 417: + { + Tags.Add("能源"); + break; + } + case 4558: + { + Tags.Add("红石"); + break; + } + case 436: + { + Tags.Add("食物/烹饪"); + break; + } + case 416: + { + Tags.Add("农业"); + break; + } + case 414: + { + Tags.Add("运输"); + break; + } + case 420: + { + Tags.Add("仓储"); + break; + } + case 419: + { + Tags.Add("魔法"); + break; + } + case 422: + { + Tags.Add("冒险"); + break; + } + case 424: + { + Tags.Add("装饰"); + break; + } + case 411: + { + Tags.Add("生物"); + break; + } + case 434: + { + Tags.Add("装备"); + break; + } + case 6814: + { + Tags.Add("性能优化"); + break; + } + case 9026: + { + Tags.Add("创造模式"); + break; + } + case 423: + { + Tags.Add("信息显示"); + break; + } + case 435: + { + Tags.Add("服务器"); + break; + } + case 5191: + { + Tags.Add("改良"); + break; + } + case 421: + { + Tags.Add("支持库"); + break; + } + // 整合包 + case 4484: + { + Tags.Add("多人"); + break; + } + case 4479: + { + Tags.Add("硬核"); + break; + } + case 4483: + { + Tags.Add("战斗"); + break; + } + case 4478: + { + Tags.Add("任务"); + break; + } + case 4472: + { + Tags.Add("科技"); + break; + } + case 4473: + { + Tags.Add("魔法"); + break; + } + case 4475: + { + Tags.Add("冒险"); + break; + } + case 4476: + { + Tags.Add("探索"); + break; + } + case 4477: + { + Tags.Add("小游戏"); + break; + } + case 4471: + { + Tags.Add("科幻"); + break; + } + case 4736: + { + Tags.Add("空岛"); + break; + } + case 5128: + { + Tags.Add("原版改良"); + break; + } + case 4487: + { + Tags.Add("FTB"); + break; + } + case 4480: + { + Tags.Add("基于地图"); + break; + } + case 4481: + { + Tags.Add("轻量"); + break; + } + case 4482: + { + Tags.Add("大型"); + break; + } + // 资源包 + case 403: + { + Tags.Add("原版风"); + break; + } + case 400: + { + Tags.Add("写实风"); + break; + } + case 401: + { + Tags.Add("现代风"); + break; + } + case 402: + { + Tags.Add("中世纪"); + break; + } + case 399: + { + Tags.Add("蒸汽朋克"); + break; + } + case 5244: + { + Tags.Add("含字体"); + break; + } + case 404: + { + Tags.Add("动态效果"); + break; + } + case 4465: + { + Tags.Add("兼容 Mod"); + break; + } + case 393: + { + Tags.Add("16x"); + break; + } + case 394: + { + Tags.Add("32x"); + break; + } + case 395: + { + Tags.Add("64x"); + break; + } + case 396: + { + Tags.Add("128x"); + break; + } + case 397: + { + Tags.Add("256x"); + break; + } + case 398: + { + Tags.Add("超高清"); + break; + } + case 5193: + { + Tags.Add("数据包"); // 有这个 Tag 的项会从资源包请求中被移除 + break; + } + // 光影包 + case 6553: + { + Tags.Add("写实风"); + break; + } + case 6554: + { + Tags.Add("幻想风"); + break; + } + case 6555: + { + Tags.Add("原版风"); + break; + } + // 数据包 + case 6948: + { + Tags.Add("冒险"); + break; + } + case 6949: + { + Tags.Add("幻想"); + break; + } + case 6950: + { + Tags.Add("支持库"); + break; + } + case 6952: + { + Tags.Add("魔法"); + break; + } + case 6946: + { + Tags.Add("Mod 相关"); + break; + } + case 6951: + { + Tags.Add("科技"); + break; + } + case 6953: + { + Tags.Add("实用"); + break; + } + // 世界 + case 248: + { + Tags.Add("冒险"); + break; + } + case 249: + { + Tags.Add("创造"); + break; + } + case 250: + { + Tags.Add("小游戏"); + break; + } + case 251: + { + Tags.Add("跑酷"); + break; + } + case 252: + { + Tags.Add("解谜"); + break; + } + case 253: + { + Tags.Add("生存"); + break; + } + case 4464: + { + Tags.Add("Mod 世界"); + break; + } + } + } + + #endregion + + else + { + #region Modrinth + + // 简单信息 + Id = (string)(Data["project_id"] ?? Data["id"]); // 两个 API 会返回的 key 不一样 + Slug = (string)Data["slug"]; + RawName = (string)Data["title"]; + Description = (string)Data["description"]; + LastUpdate = (DateTime?)Data["date_modified"]; + DownloadCount = (int)Data["downloads"]; + LogoUrl = (string)Data["icon_url"]; + if (string.IsNullOrEmpty(LogoUrl)) + LogoUrl = null; + Website = $"https://modrinth.com/{Data["project_type"]}/{Slug}"; + // GameVersions + // 搜索结果的键为 versions,获取特定工程的键为 game_versions + Drops = ((JArray)(Data["game_versions"] ?? Data["versions"]) ?? new JArray()) + .Select(v => ModMinecraft.McInstanceInfo.VersionToDrop((string)v)).Where(v => v > 0).Distinct() + .OrderByDescending(v => v).ToList(); + // Type + switch (Data["project_type"].ToString() ?? "") + { + case "modpack": + { + Type = CompType.ModPack; + break; + } + case "resourcepack": + { + Type = CompType.ResourcePack; + break; + } + case "shader": + { + Type = CompType.Shader; + break; + } + + default: + { + Type = CompType.Mod; // Modrinth 将数据包标为 Mod + break; + } + } + + // Tags & ModLoaders + Tags = new List(); + ModLoaders = new List(); + if (Data?["loaders"] is not null) + foreach (var Category in Data["loaders"].Select(t => t.ToString())) + switch (Category ?? "") + { + case "forge": + { + ModLoaders.Add(CompLoaderType.Forge); + break; + } + case "fabric": + { + ModLoaders.Add(CompLoaderType.Fabric); + break; + } + case "quilt": + { + ModLoaders.Add(CompLoaderType.Quilt); + break; + } + case "neoforge": + { + ModLoaders.Add(CompLoaderType.NeoForge); + break; + } + } + + foreach (var Category in Data["categories"].Select(t => t.ToString())) + switch (Category ?? "") + { + // 加载器 + case "forge": + { + ModLoaders.Add(CompLoaderType.Forge); + break; + } + case "fabric": + { + ModLoaders.Add(CompLoaderType.Fabric); + break; + } + case "quilt": + { + ModLoaders.Add(CompLoaderType.Quilt); + break; + } + case "neoforge": + { + ModLoaders.Add(CompLoaderType.NeoForge); + break; + } + case "datapack": + { + Type = CompType.DataPack; // 若包含数据包版本,则优先标为 DataPack + break; + } + // 共用 + case "technology": + { + Tags.Add("科技"); + break; + } + case "magic": + { + Tags.Add("魔法"); + break; + } + case "adventure": + { + Tags.Add("冒险"); + break; + } + case "utility": + { + Tags.Add("实用"); + break; + } + case "optimization": + { + Tags.Add("性能优化"); + break; + } + case "vanilla-like": + { + Tags.Add("原版风"); + break; + } + case "realistic": + { + Tags.Add("写实风"); + break; + } + // Mod/数据包 + case "worldgen": + { + Tags.Add("世界元素"); + break; + } + case "food": + { + Tags.Add("食物/烹饪"); + break; + } + case "game-mechanics": + { + Tags.Add("游戏机制"); + break; + } + case "transportation": + { + Tags.Add("运输"); + break; + } + case "storage": + { + Tags.Add("仓储"); + break; + } + case "decoration": + { + if (Type != CompType.ResourcePack) + Tags.Add("装饰"); + break; + } + case "mobs": + { + if (Type != CompType.ResourcePack) + Tags.Add("生物"); + break; + } + case "equipment": + { + if (Type != CompType.ResourcePack) + Tags.Add("装备"); + break; + } + case "social": + { + Tags.Add("服务器"); + break; + } + case "library": + { + Tags.Add("支持库"); + break; + } + // 整合包 + case "multiplayer": + { + Tags.Add("多人"); + break; + } + case "challenging": + { + Tags.Add("硬核"); + break; + } + case "combat": + { + Tags.Add("战斗"); + break; + } + case "quests": + { + Tags.Add("任务"); + break; + } + case "kitchen-sink": + { + Tags.Add("水槽包"); + break; + } + case "lightweight": + { + Tags.Add("轻量"); + break; + } + // 资源包 + case "simplistic": + { + Tags.Add("简洁"); + break; + } + case var @case when @case == "combat": + { + Tags.Add("战斗"); + break; + } + case "tweaks": + { + Tags.Add("改良"); + break; + } + + case "8x-": + { + Tags.Add("极简"); + break; + } + case "16x": + { + Tags.Add("16x"); + break; + } + case "32x": + { + Tags.Add("32x"); + break; + } + case "48x": + { + Tags.Add("48x"); + break; + } + case "64x": + { + Tags.Add("64x"); + break; + } + case "128x": + { + Tags.Add("128x"); + break; + } + case "256x": + { + Tags.Add("256x"); + break; + } + case "512x+": + { + Tags.Add("超高清"); + break; + } + + case "audio": + { + Tags.Add("含声音"); + break; + } + case "fonts": + { + Tags.Add("含字体"); + break; + } + case "models": + { + Tags.Add("含模型"); + break; + } + case "gui": + { + Tags.Add("含 UI"); + break; + } + case "locale": + { + Tags.Add("含语言"); + break; + } + case "core-shaders": + { + Tags.Add("核心着色器"); + break; + } + case "modded": + { + Tags.Add("兼容 Mod"); + break; + } + // 光影包 + case "fantasy": + { + Tags.Add("幻想风"); + break; + } + case "semi-realistic": + { + Tags.Add("半写实风"); + break; + } + case "cartoon": + { + Tags.Add("卡通风"); + break; + } + // 暂时不添加性能负荷 Tag + // Case "potato" : Tags.Add("极低") + // Case "low" : Tags.Add("低") + // Case "medium" : Tags.Add("中") + // Case "high" : Tags.Add("高") + case "colored-lighting": + { + Tags.Add("彩色光照"); + break; + } + case "path-tracing": + { + Tags.Add("路径追踪"); + break; + } + case "pbr": + { + Tags.Add("PBR"); + break; + } + case "reflections": + { + Tags.Add("反射"); + break; + } + + case "iris": + { + Tags.Add("Iris"); + break; + } + case "optifine": + { + Tags.Add("OptiFine"); + break; + } + case "vanilla": + { + Tags.Add("原版可用"); + break; + } + } + + #endregion + } + + if (!Tags.Any()) + Tags.Add("其他"); + Tags.Sort(); + ModLoaders.Sort(); + } + + // 保存缓存 + CompProjectCache[Id] = this; + } + + /// + /// 关联的数据库条目。若为 Nothing 则没有。 + /// + private CompDatabaseEntry DatabaseEntry + { + get + { + if (!LoadedDatabase) + { + LoadedDatabase = true; + if (Type == CompType.Mod || Type == CompType.DataPack) + _DatabaseEntry = GetCompWikiEntryBySlug(Slug); + } + + return _DatabaseEntry; + } + set + { + LoadedDatabase = true; + _DatabaseEntry = value; + } + } + + /// + /// MC 百科的页面 ID。若为 0 则没有。 + /// + public int WikiId => DatabaseEntry is null ? 0 : DatabaseEntry.WikiId; + + /// + /// 翻译后的中文名。若数据库没有则等同于 RawName。 + /// + public string TranslatedName => DatabaseEntry is null || string.IsNullOrEmpty(DatabaseEntry.ChineseName) + ? RawName + : DatabaseEntry.ChineseName; + + /// + /// 中文描述。若为 Nothing 则没有。 + /// + public Task ChineseDescription => GetChineseDescriptionAsync(); + + private async Task GetChineseDescriptionAsync() + { + var from = FromCurseForge ? "curseforge" : "modrinth"; + var para = FromCurseForge ? "modId" : "project_id"; + string result = null; + + var DescHash = $"{Id}{ModBase.GetStringMD5(Description)}"; + var CacheFilePath = $@"{ModBase.PathTemp}Cache\CompTranslation.ini"; + var CacheTranslation = ModBase.ReadIni(CacheFilePath, DescHash); + if (!string.IsNullOrWhiteSpace(CacheTranslation)) + { + result = ModBase.Base64Decode(CacheTranslation); + return result; + } + + try + { + var jsonObject = (JObject)await Task.Run(() => + ModNet.NetGetCodeByRequestOnce($"https://mod.mcimirror.top/translate/{from}/{Id}", Encoding.UTF8, + IsJson: true)); + if (Conversions.ToBoolean(((dynamic)jsonObject).ContainsKey("translated"))) + { + result = jsonObject["translated"].ToString(); + ModBase.WriteIni(CacheFilePath, DescHash, ModBase.Base64Encode(result)); + } + } + catch (HttpRequestException ex) + { + if (ex.Message.Contains("404")) + { + ModMain.MyMsgBox("当前资源的简介暂无译文", "获取译文失败", "我知道了"); + return null; + } + + ModBase.Log(ex, "获取中文描述时出现错误", ModBase.LogLevel.Hint); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取中文描述时出现错误", ModBase.LogLevel.Hint); + } + + return result; + } + + /// + /// 将当前实例转为可用于保存缓存的 Json。 + /// + public JObject ToJson() + { + var Json = new JObject(); + Json["DataSource"] = FromCurseForge ? "CurseForge" : "Modrinth"; + Json["Type"] = (int)Type; + Json["Slug"] = Slug; + Json["Id"] = Id; + if (CurseForgeFileIds is not null) + Json["CurseForgeFileIds"] = new JArray(CurseForgeFileIds); + Json["RawName"] = RawName; + Json["Description"] = Description; + Json["Website"] = Website; + if (LastUpdate is not null) + Json["LastUpdate"] = LastUpdate; + Json["DownloadCount"] = DownloadCount; + if (ModLoaders is not null && ModLoaders.Any()) + Json["ModLoaders"] = new JArray(ModLoaders.Select(m => (int)m)); + Json["Tags"] = new JArray(Tags); + if (LogoUrl is not null) + Json["LogoUrl"] = LogoUrl; + if (Drops.Any()) + Json["Drops"] = new JArray(Drops); + Json["CacheTime"] = DateTime.Now; // 用于检查缓存时间 + return Json; + } + + /// + /// 将当前工程信息实例化为控件。 + /// + public MyVirtualizingElement ToCompItem(bool showMcVersionDesc, bool showLoaderDesc) + { + // --- 1. 获取版本描述 (核心算法优化) --- + string gameVersionDescription; + if (Drops == null || !Drops.Any()) + { + gameVersionDescription = "仅快照版本"; + } + else + { + var segments = new List(); + var isOld = false; + + for (var i = 0; i < Drops.Count; i++) + { + int startDrop = Drops[i], endDrop = Drops[i]; + + if (startDrop < 100) + { + if (segments.Any() && !isOld) break; + isOld = true; + } + + // 查找连续的版本段 + for (var ii = i + 1; ii < Drops.Count; ii++) + { + if (ModDownload.AllDrops == null || ModDownload.AllDrops.IndexOf(Drops[ii]) != + ModDownload.AllDrops.IndexOf(endDrop) + 1) break; + endDrop = Drops[ii]; + i = ii; + } + + // 将段转为文本的逻辑 + var startName = ModMinecraft.McInstanceInfo.DropToVersion(startDrop); + var endName = ModMinecraft.McInstanceInfo.DropToVersion(endDrop); + + if (startDrop == endDrop) + { + segments.Add(startName); + } + else if (ModDownload.AllDrops?.Any() == true && startDrop >= ModDownload.AllDrops.First()) + { + if (endDrop < 100) + { + segments.Clear(); + segments.Add("全版本"); + break; + } + + segments.Add(endName + "+"); + } + else if (endDrop < 100) + { + segments.Add(startName + "-"); + break; + } + else if (ModDownload.AllDrops == null || + ModDownload.AllDrops.IndexOf(endDrop) - ModDownload.AllDrops.IndexOf(startDrop) == 1) + { + segments.Add($"{startName}, {endName}"); + } + else + { + segments.Add($"{startName}~{endName}"); + } + } + + gameVersionDescription = string.Join(", ", segments); + } + + // --- 2. 获取 Mod 加载器描述 (使用 Switch 表达式) --- + var modLoadersForDesc = ModLoaders.ToList(); + if (Config.Download.Comp.IgnoreQuilt) modLoadersForDesc.Remove(CompLoaderType.Quilt); + + var (fullDesc, partDesc) = modLoadersForDesc.Count switch + { + 0 => ModLoaders.Count == 1 ? ($"仅 {ModLoaders.Single()}", ModLoaders.Single().ToString()) : ("未知", ""), + 1 => ($"仅 {modLoadersForDesc.Single()}", modLoadersForDesc.Single().ToString()), + _ => GetMultiLoaderDesc() + }; + + // 局部函数处理复杂的“任意”判断逻辑 + (string, string) GetMultiLoaderDesc() + { + var newestDrop = Drops?.FirstOrDefault() ?? 9999; + var isAny = ModLoaders.Contains(CompLoaderType.Forge) && + (newestDrop < 140 || ModLoaders.Contains(CompLoaderType.Fabric)) && + (newestDrop < 200 || ModLoaders.Contains(CompLoaderType.NeoForge)) && + (newestDrop < 140 || ModLoaders.Contains(CompLoaderType.Quilt) || + Config.Download.Comp.IgnoreQuilt); + + var joined = string.Join(" / ", modLoadersForDesc); + return isAny ? ("任意", "") : (joined, joined); + } + + // --- 3. 实例化 UI (精简布局逻辑) --- + return new MyVirtualizingElement(() => + { + var newItem = new MyCompItem { Tag = this }; + ApplyLogoToMyImage(newItem.PathLogo); + + var title = GetControlTitle(true); + newItem.Title = title.Key; + + if (string.IsNullOrEmpty(title.Value)) + ((StackPanel)newItem.LabTitleRaw.Parent).Children.Remove(newItem.LabTitleRaw); + else + newItem.SubTitle = title.Value; + + newItem.Tags = Tags; + newItem.Description = Description.Replace("\r", "").Replace("\n", ""); + + // 下边栏逻辑切换 + newItem.LabVersion.Text = (showMcVersionDesc, showLoaderDesc) switch + { + (true, true) => + $"{(string.IsNullOrEmpty(partDesc) ? "" : partDesc + " ")}{gameVersionDescription}", + (true, false) => gameVersionDescription, + (false, true) => fullDesc, + _ => "" // 处理隐藏逻辑见下 + }; + + if (!showMcVersionDesc && !showLoaderDesc) + { + ((Grid)newItem.PathVersion.Parent).Children.Remove(newItem.PathVersion); + ((Grid)newItem.LabVersion.Parent).Children.Remove(newItem.LabVersion); + newItem.ColumnVersion1.Width = new GridLength(0); + newItem.ColumnVersion2.MaxWidth = 0; + newItem.ColumnVersion3.Width = new GridLength(0); + } + + newItem.LabSource.Text = FromCurseForge ? "CurseForge" : "Modrinth"; + + if (LastUpdate != null) + { + newItem.LabTime.Text = TimeUtils.GetTimeSpanString(LastUpdate.Value - DateTime.Now, true); + } + else + { + newItem.LabTime.Visibility = Visibility.Collapsed; + newItem.ColumnTime1.Width = + newItem.ColumnTime2.Width = newItem.ColumnTime3.Width = new GridLength(0); + } + + // 下载量数值缩写 + newItem.LabDownload.Text = DownloadCount switch + { + > 100_000_000 => $"{Math.Round(DownloadCount / 100_000_000.0, 2)} 亿", + > 100_000 => $"{Math.Floor(DownloadCount / 10_000.0)} 万", + _ => DownloadCount.ToString() + }; + + return newItem; + }) + { Height = 64 }; + } + + public MyListItem ToListItem() + { + var result = new MyListItem(); + result.Title = TranslatedName; + result.Info = Description.Replace("\r", "").Replace("\n", ""); + result.Logo = LogoUrl; + result.Tags = Tags; + result.Tag = this; + return result; + } + + public void ApplyLogoToMyImage(MyImage img) + { + if (string.IsNullOrEmpty(LogoUrl)) + { + img.Source = ModBase.PathImage + "Icons/NoIcon.png"; + } + else + { + img.Source = LogoUrl; + img.FallbackSource = ModDownload.DlSourceModGet(LogoUrl); + } + } + + public KeyValuePair GetControlTitle(bool hasModLoaderDescription) + { + // 参考 #1567 测试例 + var title = RawName; + List subtitleList = new(); + + if (TranslatedName == RawName) + { + // --- 场景 A: 没有中文翻译 --- + var nameLists = TranslatedName.Split(new[] { " | ", " - ", "(", ")", "[", "]", "{", "}" }, + StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim(' ', '/', '\\', '"')) + .Where(w => !string.IsNullOrEmpty(w)) + .ToList(); + + if (nameLists.Count <= 1) return BuildResult(title, ""); + + var normalNameList = new List(); + foreach (var name in nameLists) + { + var lowerName = name.ToLower(); + // 匹配缩写 (全大写且不是特定词) + if (name.ToUpper() == name && name != "FPS" && name != "HUD") + subtitleList.Add(name); + // 匹配加载器标记 (Forge/Fabric/Quilt 且去掉后不含其他字母) + else if (IsModLoaderMarker(lowerName)) + subtitleList.Add(name); + else + normalNameList.Add(name); + } + + if (!normalNameList.Any() || !subtitleList.Any()) + return BuildResult(title, ""); + + title = string.Join(" - ", normalNameList); + } + else + { + // --- 场景 B: 有中文翻译 --- + // 尝试拆分:Title (EnglishName) - Suffix + title = TranslatedName.BeforeFirst(" (").BeforeFirst(" - "); + + var suffix = ""; + if (TranslatedName.AfterLast(")").Contains(" - ")) + suffix = TranslatedName.AfterLast(")").AfterLast(" - "); + + var englishName = TranslatedName; + if (!string.IsNullOrEmpty(suffix)) + englishName = englishName.Replace(" - " + suffix, ""); + + englishName = englishName.Replace(title, "").Trim('(', ')', ' '); + + subtitleList = englishName.Split(new[] { " | ", " - ", "(", ")", "[", "]", "{", "}" }, + StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim(' ', '/')) + .Where(w => !string.IsNullOrEmpty(w)) + .ToList(); + + // 特殊逻辑:如果看起来不像版本标记或特定缩写,则保持原名 + if (subtitleList.Count > 1 && + !subtitleList.Any(s => IsModLoaderMarker(s.ToLower())) && + !(subtitleList.Count == 2 && subtitleList.Last().ToUpper() == subtitleList.Last())) + subtitleList = new List { englishName }; + + if (!string.IsNullOrEmpty(suffix)) subtitleList.Add(suffix); + } + + // --- 后处理: 构建 Subtitle 字符串 --- + var finalSubtitles = new List(); + foreach (var rawEx in subtitleList.Distinct()) + { + var ex = rawEx; + var lowerEx = ex.ToLower(); + var isModLoader = lowerEx.Contains("forge") || lowerEx.Contains("fabric") || lowerEx.Contains("quilt"); + + if (!hasModLoaderDescription && isModLoader) continue; + if (ex.Length < 16 && lowerEx.Contains("fabric") && lowerEx.Contains("forge")) continue; + + if (isModLoader && !ex.Contains("版") && + lowerEx.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3) + ex = ex.Replace("Edition", "", StringComparison.OrdinalIgnoreCase) + .Replace("edition", "", StringComparison.OrdinalIgnoreCase) + .Trim().Capitalize() + " 版"; + + // 规范化名称大小写 + ex = ex.Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric") + .Replace("quilt", "Quilt"); + finalSubtitles.Add(ex.Trim()); + } + + var subtitleResult = finalSubtitles.Any() ? " | " + string.Join(" | ", finalSubtitles) : ""; + return BuildResult(title, subtitleResult); + + bool IsModLoaderMarker(string input) + { + return (input.Contains("forge") || input.Contains("fabric") || input.Contains("quilt")) && + !input.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").RegexCheck("[a-z]+"); + } + + KeyValuePair BuildResult(string t, string s) + { + return new KeyValuePair(t, s); + } + } + + // 辅助函数 + + /// + /// 检查是否与某个 Project 是相同的工程,只是在不同的网站。 + /// + public bool IsLike(CompProject Project) + { + if ((Id ?? "") == (Project.Id ?? "")) + return true; // 相同实例 + + // 提取字符串中的字母和数字 + string GetRaw(string Data) + { + var Result = new StringBuilder(); + foreach (var r in Data.Where(c => char.IsLetterOrDigit(c))) + Result.Append(r); + return Result.ToString().ToLower(); + } + + ; + // 来自不同的网站 + if (FromCurseForge == Project.FromCurseForge) + return false; + // Mod 加载器一致 + if (ModLoaders.Count != Project.ModLoaders.Count || ModLoaders.Except(Project.ModLoaders).Any()) + return false; + // 若不为光影,则要求 MC 版本一致 + if (Type != CompType.Shader && (Drops.Count != Project.Drops.Count || Drops.Except(Project.Drops).Any())) + return false; + // 最近更新时间差距在一周以内 + if (LastUpdate is not null && Project.LastUpdate is not null && + Math.Abs((LastUpdate - Project.LastUpdate).Value.TotalDays) > 7d) + return false; + // MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同 + if ((TranslatedName ?? "") == (Project.TranslatedName ?? "") || + (RawName ?? "") == (Project.RawName ?? "") || (Description ?? "") == (Project.Description ?? "") || + (GetRaw(Slug) ?? "") == (GetRaw(Project.Slug) ?? "")) + { + ModBase.Log($"[Comp] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程"); + // 如果只有一个有 DatabaseEntry,设置给另外一个 + if (DatabaseEntry is null && Project.DatabaseEntry is not null) + DatabaseEntry = Project.DatabaseEntry; + if (DatabaseEntry is not null && Project.DatabaseEntry is null) + Project.DatabaseEntry = DatabaseEntry; + return true; + } + + return false; + } + + public override string ToString() + { + return $"{Id} ({Slug}): {RawName}"; + } + + public override bool Equals(object obj) + { + var project = obj as CompProject; + return project is not null && (Id ?? "") == (project.Id ?? ""); + } + + public static bool operator ==(CompProject left, CompProject right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(CompProject left, CompProject right) + { + return !(left == right); + } + } + + // 输入与输出 + + public class CompProjectRequest + { + /// + /// 筛选 MC 版本。 + /// + public string GameVersion = null; + + /// + /// 筛选 Mod 加载器类别。 + /// + public CompLoaderType ModLoader = CompLoaderType.Any; + + /// + /// 搜索的文本内容。 + /// + public string SearchText; + + /// + /// 搜索结果排序方式。 + /// + public CompSortType Sort = CompSortType.Default; + + /// + /// 允许的来源。 + /// + public CompSourceType Source = CompSourceType.Any; + + // 结果要求 + + /// + /// 加载后应输出到的结果存储器。 + /// + public CompProjectStorage Storage; + + /// + /// 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。 + /// + public string Tag = ""; + + /// + /// 应当尽量达成的结果数量。 + /// + public int TargetResultCount; + + // 输入内容 + + /// + /// 筛选资源种类。 + /// + public CompType Type; + + /// + /// 构造函数。 + /// + public CompProjectRequest(CompType Type, CompProjectStorage Storage, int TargetResultCount) + { + this.Type = Type; + this.Storage = Storage; + this.TargetResultCount = TargetResultCount; + } + + /// + /// 根据加载位置记录,是否还可以继续获取内容。 + /// + public bool CanContinue + { + get + { + if (Tag.StartsWithF("/") || !Source.HasFlag(CompSourceType.CurseForge)) + Storage.CurseForgeTotal = 0; + if (Tag.EndsWithF("/") || !Source.HasFlag(CompSourceType.Modrinth)) + Storage.ModrinthTotal = 0; + if (Storage.CurseForgeTotal == -1 || Storage.ModrinthTotal == -1) + return true; + return Storage.CurseForgeOffset < Storage.CurseForgeTotal || + Storage.ModrinthOffset < Storage.ModrinthTotal; + } + } + + // 构造请求 + + /// + /// 获取对应的 CurseForge API 请求链接。若返回 Nothing 则为不进行 CurseForge 请求。 + /// + public string GetCurseForgeAddress() + { + if (!Source.HasFlag(CompSourceType.CurseForge)) + return null; + if (Tag.StartsWithF("/")) + Storage.CurseForgeTotal = 0; + if (Storage.CurseForgeTotal > -1 && Storage.CurseForgeTotal <= Storage.CurseForgeOffset) + return null; + // 应用筛选参数 + var Address = + new StringBuilder( + $"https://api.curseforge.com/v1/mods/search?gameId=432&sortOrder=desc&pageSize={CompPageSize}"); + switch (Type) + { + case CompType.Mod: + { + Address.Append("&classId=6"); + break; + } + case CompType.ModPack: + { + Address.Append("&classId=4471"); + break; + } + case CompType.DataPack: + { + Address.Append("&classId=6945"); + break; + } + case CompType.Shader: + { + Address.Append("&classId=6552"); + break; + } + case CompType.ResourcePack: + { + Address.Append("&classId=12"); + break; + } + case CompType.World: + { + Address.Append("&classId=17"); + break; + } + } + + if (!string.IsNullOrEmpty(Tag)) Address.Append($"&categoryId={Tag.BeforeFirst("/")}"); + if (ModLoader != CompLoaderType.Any) + Address.Append("&modLoaderType=").Append(((int)ModLoader).ToString()); + if (!string.IsNullOrEmpty(GameVersion)) + Address.Append("&gameVersion=").Append(GameVersion); + if (!string.IsNullOrEmpty(SearchText)) + Address.Append("&searchFilter=").Append(WebUtility.UrlEncode(SearchText)); + if (Storage.CurseForgeOffset > 0) + Address.Append("&index=").Append(Storage.CurseForgeOffset); + switch (Sort) + { + case CompSortType.Relevance: + { + Address.Append("&sortField=4"); + break; + } + case CompSortType.Downloads: + { + Address.Append("&sortField=6"); + break; + } + case CompSortType.Follows: + { + Address.Append("&sortField=2"); + break; + } + case CompSortType.Newest: + { + Address.Append("&sortField=11"); + break; + } + case CompSortType.Updated: + { + Address.Append("&sortField=3"); + break; + } + + default: + { + Address.Append("&sortField=2"); + break; + } + } + + return Address.ToString(); + } + + /// + /// 获取对应的 Modrinth API 请求链接。若返回 Nothing 则为不进行 Modrinth 请求。 + /// + public string GetModrinthAddress() + { + if (!Source.HasFlag(CompSourceType.Modrinth)) + return null; + if (Tag.EndsWithF("/")) + Storage.ModrinthTotal = 0; + if (Storage.ModrinthTotal > -1 && Storage.ModrinthTotal <= Storage.ModrinthOffset) + return null; + // 应用筛选参数 + var Address = $"https://api.modrinth.com/v2/search?limit={CompPageSize}"; + switch (Sort) + { + case CompSortType.Relevance: + { + Address += "&index=relevance"; + break; + } + case CompSortType.Downloads: + { + Address += "&index=downloads"; + break; + } + case CompSortType.Follows: + { + Address += "&index=follows"; + break; + } + case CompSortType.Newest: + { + Address += "&index=newest"; + break; + } + case CompSortType.Updated: + { + Address += "&index=updated"; + break; + } + + default: + { + Address += "&index=relevance"; + break; + } + } + + if (!string.IsNullOrEmpty(SearchText)) + Address += "&query=" + WebUtility.UrlEncode(SearchText); + if (Storage.ModrinthOffset > 0) + Address += "&offset=" + Storage.ModrinthOffset; + // facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]] + var Facets = new List(); + Facets.Add($"[\"project_type:{ModBase.GetStringFromEnum(Type).ToLower()}\"]"); + if (!string.IsNullOrEmpty(Tag)) + Facets.Add($"[\"categories:'{Tag.AfterLast("/")}'\"]"); + if (ModLoader != CompLoaderType.Any) + Facets.Add($"[\"categories:'{ModBase.GetStringFromEnum(ModLoader).ToLower()}'\"]"); + if (!string.IsNullOrEmpty(GameVersion)) + Facets.Add($"[\"versions:'{GameVersion}'\"]"); + Address += "&facets=[" + string.Join(",", Facets) + "]"; + return Address; + } + + // 相同判断 + public override bool Equals(object obj) + { + var request = obj as CompProjectRequest; + return request is not null && Type == request.Type && TargetResultCount == request.TargetResultCount && + (Tag ?? "") == (request.Tag ?? "") && ModLoader == request.ModLoader && Source == request.Source && + (GameVersion ?? "") == (request.GameVersion ?? "") && + (SearchText ?? "") == (request.SearchText ?? "") && Sort == request.Sort; + } + + public static bool operator ==(CompProjectRequest left, CompProjectRequest right) + { + return EqualityComparer.Default.Equals(left, right); + } + + public static bool operator !=(CompProjectRequest left, CompProjectRequest right) + { + return !(left == right); + } + } + + public class CompProjectStorage + { + // 加载位置记录 + + public int CurseForgeOffset; + public int CurseForgeTotal = -1; + + /// + /// 当前的错误信息。如果没有则为 Nothing。 + /// + public string ErrorMessage = null; + + public int ModrinthOffset; + public int ModrinthTotal = -1; + + // 结果列表 + + /// + /// 可供展示的所有工程的列表。 + /// + public List Results = new(); + } + + // 实际的获取 + + private const int CompPageSize = 40; + + /// + /// 已知工程信息的缓存。 + /// + public static ConcurrentDictionary CompProjectCache = new(); + + /// + /// 根据搜索请求获取一系列的工程列表。需要基于加载器运行。 + /// + public static void CompProjectsGet(ModLoader.LoaderTask task) + { + var request = task.Input; + var storage = request.Storage; + + #region 状态与版本初步检查 + + if (storage.Results.Count >= request.TargetResultCount) + { + LogWrapper.Info($"[Comp] 已有 {storage.Results.Count} 个结果,多于所需的 {request.TargetResultCount} 个结果,结束处理"); + return; + } + + if (!request.CanContinue) + { + if (!storage.Results.Any()) throw new Exception("没有符合条件的结果"); + LogWrapper.Info( + $"[Comp] 已有 {storage.Results.Count} 个结果,少于所需的 {request.TargetResultCount} 个结果,但无法继续获取,结束处理"); + return; + } + + // 拒绝不支持的版本 + if (request.ModLoader == CompLoaderType.Quilt && + ModMinecraft.CompareVersion(request.GameVersion ?? "1.15", "1.14") == -1) + throw new Exception($"Quilt 不支持 Minecraft {request.GameVersion}"); + + #endregion + + #region 处理搜索文本 (内嵌关键词转换逻辑) + + var rawFilter = (request.SearchText ?? "").Trim(); + request.SearchText = rawFilter; + var rawFilterLower = rawFilter.ToLower(); + LogWrapper.Info("[Comp] 工程列表搜索原始文本:" + rawFilter); + + // 中文请求关键字处理 + var isChineseSearch = RegexPatterns.HasChineseChar.IsMatch(rawFilter) && !string.IsNullOrEmpty(rawFilter); + if (isChineseSearch && (request.Type == CompType.Mod || request.Type == CompType.DataPack)) + { + var searchEntries = new List>(); + using (var conn = CompDB) + { + var sql = + "SELECT * FROM ModTranslation WHERE ChineseName LIKE @p OR CurseForgeSlug LIKE @p OR ModrinthSlug LIKE @p"; + var searchRes = conn.Query(sql, new { p = $"%{rawFilter}%" }); + foreach (var item in searchRes) + { + if (item.ChineseName.Contains("动态的树")) continue; + searchEntries.Add(new ModBase.SearchEntry + { + Item = item, + SearchSource = new List> + { + new(item.ChineseName + (item.CurseForgeSlug ?? "") + (item.ModrinthSlug ?? ""), 1.0) + } + }); + } + } + + var searchResults = ModBase.Search(searchEntries, request.SearchText, 3); + if (!searchResults.Any()) throw new Exception("无搜索结果,请尝试搜索英文名称"); + + var searchResultText = ""; + for (var i = 0; i < Math.Min(5, searchResults.Count); i++) + { + if (!searchResults[i].AbsoluteRight && i >= Math.Min(3, searchResults.Count)) break; + var item = searchResults[i].Item; + if (item.CurseForgeSlug != null) + searchResultText += item.CurseForgeSlug.Replace("-", " ").Replace("/", " ") + " "; + if (item.ModrinthSlug != null) + searchResultText += item.ModrinthSlug.Replace("-", " ").Replace("/", " ") + " "; + searchResultText += item.ChineseName.AfterLast(" (").TrimEnd(')', ' ').BeforeFirst(" - ") + .Replace(":", "").Replace("(", "").Replace(")", "").ToLower().Replace("/", " ") + " "; + } + + var realFilter = ""; + var words = searchResultText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var word in words) + { + var wordLower = word.ToLowerInvariant(); + if (new[] { "the", "of", "a", "mod", "and" }.Contains(wordLower) || + double.TryParse(word, out _)) continue; + if (words.Length > 3 && wordLower == "ftb") continue; + realFilter += word.TrimStart('{', '[', '(').TrimEnd('}', ']', ')') + " "; + } + + request.SearchText = realFilter.Trim(); + LogWrapper.Debug("[Comp] 中文搜索最终关键词:" + request.SearchText); + } + + // 驼峰与拼合逻辑处理 + var spacedKeywords = RegexPatterns.EnglishSpacedKeywords.Replace(request.SearchText, "$& "); + var connectedKeywords = request.SearchText.Replace(" ", ""); + var allPossibleKeywords = + (spacedKeywords + " " + (isChineseSearch ? request.SearchText : connectedKeywords + " " + rawFilter)) + .ToLower(); + + var rightKeywords = new List(); + foreach (var keyword in allPossibleKeywords.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) + { + var cleanKeyword = keyword.Trim('[', ']'); + if (string.IsNullOrEmpty(cleanKeyword)) continue; + if (new[] { "forge", "fabric", "for", "mod", "quilt" }.Contains(cleanKeyword)) continue; + rightKeywords.Add(cleanKeyword); + } + + if (rawFilter.Length > 0 && !rightKeywords.Any()) + request.SearchText = rawFilter; + else + request.SearchText = string.Join(" ", rightKeywords.Distinct()).ToLower(); + + // 例外项处理 + if (rawFilter.Replace(" ", "").ContainsF("optiforge", true)) request.SearchText = "optiforge"; + if (rawFilter.Replace(" ", "").ContainsF("optifabric", true)) request.SearchText = "optifabric"; + + task.Progress = 0.1; + + #endregion + + var realResults = new List(); + + #region 网络请求与结果获取 (Retry 循环) + + while (true) + { + var rawResults = new List(); + Exception lastError = null; + var resultsLock = new object(); + + // 1.14 以下 Forge 筛选处理 + var isOldForgeRequest = request.ModLoader == CompLoaderType.Forge && + ModMinecraft.McInstanceInfo.VersionToDrop(request.GameVersion, true) < 140; + if (isOldForgeRequest) request.ModLoader = CompLoaderType.Any; + var curseForgeUrl = request.GetCurseForgeAddress(); + var modrinthUrl = request.GetModrinthAddress(); + if (isOldForgeRequest) request.ModLoader = CompLoaderType.Forge; + + var tasks = new List(); + + // CurseForge 线程内嵌 + if (curseForgeUrl != null) + tasks.Add(Task.Run(() => + { + try + { + LogWrapper.Info("[Comp] 开始从 CurseForge 获取列表:" + curseForgeUrl); + var json = (JObject)ModDownload.DlModRequest(curseForgeUrl, true); + var projects = json["data"].Select(j => new CompProject((JObject)j)) + .Where(p => !(request.Type == CompType.ResourcePack && p.Tags.Contains("数据包"))) + .ToList(); + lock (resultsLock) + { + rawResults.AddRange(projects); + } + + storage.CurseForgeOffset += projects.Count; + storage.CurseForgeTotal = json["pagination"]["totalCount"].ToObject(); + } + catch (Exception ex) + { + lastError = ex; + LogWrapper.Error(ex, "CurseForge 获取失败"); + } + })); + + // Modrinth 线程内嵌 + if (modrinthUrl != null) + tasks.Add(Task.Run(() => + { + try + { + LogWrapper.Info("[Comp] 开始从 Modrinth 获取列表:" + modrinthUrl); + var json = (JObject)ModDownload.DlModRequest(modrinthUrl, true); + var projects = json["hits"].Select(j => new CompProject((JObject)j)).ToList(); + lock (resultsLock) + { + rawResults.AddRange(projects); + } + + storage.ModrinthOffset += projects.Count; + storage.ModrinthTotal = json["total_hits"].ToObject(); + } + catch (Exception ex) + { + lastError = ex; + LogWrapper.Error(ex, "Modrinth 获取失败"); + } + })); + + Task.WaitAll(tasks.ToArray()); + task.Progress += 0.4; + if (task.IsAborted) return; + + // 过滤老版本 Forge + if (isOldForgeRequest) + rawResults = rawResults.Where(p => !p.ModLoaders.Any() || p.ModLoaders.Contains(CompLoaderType.Forge)) + .ToList(); + + // 错误检查与空结果处理 + if (!rawResults.Any()) + { + if (lastError != null) throw lastError; + // 处理各平台不兼容报错... (此处省略具体 Exception 文本以保持简略) + throw new Exception("没有搜索结果"); + } + + #region 去重与分页判断 + + // 优先保留 Modrinth 顺序并去重 + var processedResults = rawResults.OrderBy(x => x.FromCurseForge) + .Where(r => !realResults.Any(b => r.IsLike(b)) && !storage.Results.Any(b => r.IsLike(b))) + .ToList(); + + realResults.AddRange(processedResults); + LogWrapper.Info($"[Comp] 去重后新增 {processedResults.Count} 个结果"); + + if (realResults.Count + storage.Results.Count < request.TargetResultCount && request.CanContinue && + lastError == null) + { + LogWrapper.Info("[Comp] 数量不足,继续加载下一页"); + continue; + } + + break; + + #endregion + } + + #endregion + + #region 排序与最终输出 + + var scores = new Dictionary(); + Func getDownloadCountMult = p => + { + switch (request.Type) + { + case CompType.Mod: + case CompType.ModPack: return p.FromCurseForge ? 1 : 7; + case CompType.DataPack: return p.FromCurseForge ? 10 : 1; + case CompType.ResourcePack: + case CompType.Shader: return p.FromCurseForge ? 1 : 5; + default: return 1; + } + }; + + if (string.IsNullOrEmpty(rawFilter)) + { + foreach (var res in realResults) scores.Add(res, res.DownloadCount * getDownloadCountMult(res)); + } + else + { + var searchEntries = new List>(); + foreach (var res in realResults) + { + scores.Add(res, + (res.WikiId > 0 ? 0.2 : 0) + + Math.Log10(Math.Max(res.DownloadCount, 1) * getDownloadCountMult(res)) / 9); + searchEntries.Add(new ModBase.SearchEntry + { + Item = res, + SearchSource = new List> + { + new(isChineseSearch ? res.TranslatedName : res.RawName, 1), + new(res.Description, 0.05) + } + }); + } + + var searchRes = ModBase.Search(searchEntries, rawFilter, 101, -1); + foreach (var item in searchRes) scores[item.Item] += item.Similarity / searchRes[0].Similarity; + } + + storage.Results.AddRange(scores.OrderByDescending(s => s.Value).Select(s => s.Key)); + + #endregion + } + + #endregion + + #region CompFile | 文件信息 + + // 类定义 + + public enum CompFileStatus + { + Release = 1, // 枚举值来源:https://docs.curseforge.com/#tocS_FileReleaseType + Beta = 2, + Alpha = 3 + } + + public class CompFile + { + /// + /// 该文件的所有必要依赖工程的 Project.Id。 + /// + public readonly List Dependencies = new(); + + /// + /// 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量,且 CurseForge 可能错误地返回 0。 + /// + public readonly int DownloadCount; + + /// + /// 下载的文件名。 + /// + public readonly string FileName; + + /// + /// 该文件来自 CurseForge 还是 Modrinth。 + /// + public readonly bool FromCurseForge; + + /// + /// 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。 + /// + public readonly List GameVersions; + + /// + /// 文件的 SHA1 或 MD5。 + /// + public readonly string Hash; + + /// + /// 用于唯一性鉴别该文件的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。 + /// + public readonly string Id; + + /// + /// 支持的 Mod 加载器列表。可能为空。 + /// + public readonly List ModLoaders; + + /// + /// 该文件的所有可选依赖工程的 Project.Id。 + /// + public readonly List OptionalDependencies = new(); + + /// + /// 该文件所属项目的 ID。 + /// + public readonly string ProjectId; + + /// + /// 该文件的所有必要依赖工程的原始 ID。 + /// 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。 + /// + public readonly List RawDependencies = new(); + + /// + /// 该文件的所有可选依赖工程的原始 ID。 + /// 这些 ID 可能没有加载,在加载后会添加到 OptionalDependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。 + /// + public readonly List RawOptionalDependencies = new(); + + /// + /// 发布时间。 + /// + public readonly DateTime ReleaseDate; + + /// + /// 发布状态:Release/Beta/Alpha。 + /// + public readonly CompFileStatus Status; + + // 源信息 + + /// + /// 文件的种类。 + /// + public readonly CompType Type; + + // 描述性信息 + + /// + /// 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。 + /// + public string DisplayName; + + /// + /// 文件所有可能的下载源。 + /// + public List DownloadUrls; + + /// + /// Mod 版本号。 + /// 不一定是标准格式。CurseForge 上默认为 Nothing。 + /// + public string Version; + + // 实例化 + + /// + /// 从文件 Json 中初始化实例。若出错会抛出异常。 + /// + public CompFile(JObject Data, CompType DefaultType) + { + Type = DefaultType; + if (Data.ContainsKey("FromCurseForge")) + { + #region CompJson + + FromCurseForge = Data["FromCurseForge"].ToObject(); + Id = Data["Id"].ToString(); + DisplayName = Data["DisplayName"].ToString(); + if (Data.ContainsKey("Version")) + Version = Data["Version"].ToString(); + ReleaseDate = Data["ReleaseDate"].ToObject(); + DownloadCount = Data["DownloadCount"].ToObject(); + Status = (CompFileStatus)Data["Status"].ToObject(); + if (Data.ContainsKey("FileName")) + FileName = Data["FileName"].ToString(); + if (Data.ContainsKey("DownloadUrls")) + DownloadUrls = Data["DownloadUrls"].ToObject>(); + if (Data.ContainsKey("ModLoaders")) + ModLoaders = Data["ModLoaders"].ToObject>(); + if (Data.ContainsKey("Hash")) + Hash = Data["Hash"].ToString(); + if (Data.ContainsKey("GameVersions")) + GameVersions = Data["GameVersions"].ToObject>(); + if (Data.ContainsKey("RawDependencies")) + RawDependencies = Data["RawDependencies"].ToObject>(); + if (Data.ContainsKey("Dependencies")) + Dependencies = Data["Dependencies"].ToObject>(); + if (Data.ContainsKey("RawOptionalDependencies")) + RawDependencies = Data["RawOptionalDependencies"].ToObject>(); + if (Data.ContainsKey("OptionalDependencies")) + Dependencies = Data["OptionalDependencies"].ToObject>(); + } + + #endregion + + else + { + FromCurseForge = Data.ContainsKey("gameId"); + if (FromCurseForge) + { + #region CurseForge + + // 简单信息 + Id = (string)Data["id"]; + ProjectId = (string)Data["modId"]; + DisplayName = Data["displayName"].ToString().Replace(" ", "").Trim(' '); + Version = null; + ReleaseDate = (DateTime)Data["fileDate"]; + Status = (CompFileStatus)Data["releaseType"].ToObject(); + DownloadCount = (int)Data["downloadCount"]; + FileName = (string)Data["fileName"]; + Hash = + (string)((JArray)Data["hashes"]).ToList().FirstOrDefault(s => s["algo"].ToObject() == 1)?[ + "value"]; + if (Hash is null) + Hash = (string)((JArray)Data["hashes"]).ToList() + .FirstOrDefault(s => s["algo"].ToObject() == 2)?["value"]; + // DownloadAddress + var Url = Data["downloadUrl"].ToString(); + // TODO: 移除龙猫写的直接下载,换用提醒用户手动下载相关模组 + if (string.IsNullOrWhiteSpace(Url)) + Url = + $"https://edge.forgecdn.net/files/{Conversions.ToInteger(Id.Substring(0, 4))}/{Conversions.ToInteger(Id.Substring(4))}/{FileName}"; + Url = Url.Replace(FileName, WebUtility.UrlEncode(FileName)); // 对文件名进行编码 + Url = Url.Replace("+", "%20"); // 修正被编码成 + 的空格,CurseForge 会对 + 号也进行编码 + DownloadUrls = ModDownload.DlSourceModDownloadGet(HandleCurseForgeDownloadUrls(Url)); // 添加镜像源 + // Dependencies + if (Data.ContainsKey("dependencies")) + { + RawDependencies = Data["dependencies"] + .Where(d => d["relationType"].ToObject() == 3 && + d["modId"].ToObject() != 306612 && d["modId"].ToObject() != 634179) + .Select(d => d["modId"].ToString()).ToList(); // 种类为必要依赖 + // 排除 Fabric API 和 Quilt API + RawOptionalDependencies = Data["dependencies"] + .Where(d => d["relationType"].ToObject() == 2 && + d["modId"].ToObject() != 306612 && d["modId"].ToObject() != 634179) + .Select(d => d["modId"].ToString()).ToList(); // 种类为可选依赖 + // 排除 Fabric API 和 Quilt API + } + + // GameVersions + var RawVersions = Data["gameVersions"].Select(t => t.ToString().Trim().ToLower()).ToList(); + GameVersions = RawVersions.Where(v => ModMinecraft.McInstanceInfo.IsFormatFit(v)) + .Select(v => v.Replace("-snapshot", " 预览版")).ToList(); + if (GameVersions.Count > 1) + { + GameVersions = GameVersions.Sort(ModMinecraft.CompareVersionGe).ToList(); + if (Type == CompType.ModPack) + GameVersions = new List { GameVersions[0] }; // 整合包理应只 "支持" 一个版本 + } + else if (GameVersions.Count == 1) + { + GameVersions = GameVersions.ToList(); + } + else + { + GameVersions = new List { "未知版本" }; + } + + // ModLoaders + ModLoaders = new List(); + if (RawVersions.Contains("forge")) + ModLoaders.Add(CompLoaderType.Forge); + if (RawVersions.Contains("fabric")) + ModLoaders.Add(CompLoaderType.Fabric); + if (RawVersions.Contains("quilt")) + ModLoaders.Add(CompLoaderType.Quilt); + if (RawVersions.Contains("neoforge")) + ModLoaders.Add(CompLoaderType.NeoForge); + } + + #endregion + + else + { + #region Modrinth + + // 简单信息 + Id = (string)Data["id"]; + ProjectId = (string)Data["project_id"]; + DisplayName = Data["name"].ToString().Replace(" ", "").Trim(' '); + Version = (string)Data["version_number"]; + ReleaseDate = (DateTime)Data["date_published"]; + Status = Data["version_type"].ToString() == "release" ? CompFileStatus.Release : + Data["version_type"].ToString() == "beta" ? CompFileStatus.Beta : CompFileStatus.Alpha; + DownloadCount = (int)Data["downloads"]; + if (((JArray)Data["files"]).Any()) // 可能为空 + { + var File = Data["files"][0]; + FileName = (string)File["filename"]; + DownloadUrls = ModDownload.DlSourceModDownloadGet(File["url"].ToString()); // 同时添加了镜像源 + Hash = (string)File["hashes"]["sha1"]; + } + + // ModLoaders + // 结果可能混杂着 Mod、数据包和服务端插件 + var RawLoaders = Data["loaders"].Select(v => v.ToString()).ToList(); + ModLoaders = new List(); + if (Type == CompType.Mod) // 以尽量宽容的方式检测加载器,以免同时兼容两种的项被删除 + { + if (RawLoaders.Intersect(new[] { "bukkit", "folia", "paper", "purpur", "spigot" }).Any()) + Type = CompType.Plugin; // Veinminer Enchantment 同时支持服务端与 Fabric + if (RawLoaders.Contains("datapack")) + Type = CompType.DataPack; + if (RawLoaders.Contains("forge")) + { + ModLoaders.Add(CompLoaderType.Forge); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("neoforge")) + { + ModLoaders.Add(CompLoaderType.NeoForge); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("fabric")) + { + ModLoaders.Add(CompLoaderType.Fabric); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("quilt")) + { + ModLoaders.Add(CompLoaderType.Quilt); + Type = CompType.Mod; + } + } + else if (Type == CompType.DataPack) + { + if (RawLoaders.Intersect(new[] { "bukkit", "folia", "paper", "purpur", "spigot" }).Any()) + Type = CompType.Plugin; + if (RawLoaders.Contains("forge")) + { + ModLoaders.Add(CompLoaderType.Forge); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("neoforge")) + { + ModLoaders.Add(CompLoaderType.NeoForge); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("fabric")) + { + ModLoaders.Add(CompLoaderType.Fabric); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("quilt")) + { + ModLoaders.Add(CompLoaderType.Quilt); + Type = CompType.Mod; + } + + if (RawLoaders.Contains("datapack")) + Type = CompType.DataPack; + } + + // Dependencies + if (Data.ContainsKey("dependencies")) + { + RawDependencies = Data["dependencies"] + .Where(d => (string)d["dependency_type"] == "required" && + (string)d["project_id"] != "P7dR8mSH" && + (string)d["project_id"] != "qvIfYCYJ" && d["project_id"].ToString().Length > 0) + .Select(d => d["project_id"].ToString()).ToList(); // 种类为必要依赖 + // 排除 Fabric API 和 Quilt API + // 有时候真的会空…… + RawOptionalDependencies = Data["dependencies"] + .Where(d => (string)d["dependency_type"] == "optional" && + (string)d["project_id"] != "P7dR8mSH" && + (string)d["project_id"] != "qvIfYCYJ" && d["project_id"].ToString().Length > 0) + .Select(d => d["project_id"].ToString()).ToList(); // 种类为可选依赖 + // 排除 Fabric API 和 Quilt API + // 有时候真的会空…… + } + + // GameVersions + var RawVersions = Data["game_versions"].Select(t => t.ToString().Trim().ToLower()).ToList(); + GameVersions = RawVersions.Where(v => v.Contains(".")).Select(v => + v.Contains("-") ? v.BeforeFirst("-") + " 预览版" : v.StartsWithF("b1.") ? "远古版本" : v).ToList(); + if (GameVersions.Count > 1) + { + GameVersions = GameVersions.Sort(ModMinecraft.CompareVersionGe).ToList(); + if (Type == CompType.ModPack) + GameVersions = new List { GameVersions[0] }; // 整合包理应只 “支持” 一个版本 + } + else if (GameVersions.Count == 1) + { + } + // 无需处理 + else if (RawVersions.Any(v => v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]"))) + { + GameVersions = RawVersions.Where(v => v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList(); + } + else + { + GameVersions = new List { "未知版本" }; + } + + #endregion + } + } + } + + /// + /// 发布状态的友好描述。例如:"正式版","Beta 版"。 + /// + public string StatusDescription + { + get + { + switch (Status) + { + case CompFileStatus.Release: + { + return "正式版"; + } + case CompFileStatus.Beta: + { + return ModBase.ModeDebug ? "Beta 版" : "测试版"; + } + + default: + { + return ModBase.ModeDebug ? "Alpha 版" : "早期测试版"; + } + } + } + } + + // 下载信息 + /// + /// 下载信息是否可用。 + /// + public bool Available => FileName is not null && DownloadUrls is not null; + + /// + /// 获取下载信息。 + /// + /// 目标本地文件夹,或完整的文件路径。会自动判断类型。 + public ModNet.NetFile ToNetFile(string LocalAddress) + { + return new ModNet.NetFile(DownloadUrls, LocalAddress + (LocalAddress.EndsWithF(@"\") ? FileName : ""), + new ModBase.FileChecker(Hash: Hash), true); + } + + /// + /// 对之前错误的 CurseForge 的下载地址进行修正。 + /// + public static string HandleCurseForgeDownloadUrls(string Url) + { + return Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://media.", "://edge.") + .Replace("://mediafilez.", "://edge."); + } + + /// + /// 将当前实例转为可用于保存缓存的 Json。 + /// + public JObject ToJson() + { + var Json = new JObject(); + Json.Add("FromCurseForge", FromCurseForge); + Json.Add("Id", Id); + if (Version is not null) + Json.Add("Version", Version); + Json.Add("DisplayName", DisplayName); + Json.Add("ReleaseDate", ReleaseDate); + Json.Add("DownloadCount", DownloadCount); + Json.Add("ModLoaders", new JArray(ModLoaders.Select(m => (int)m))); + Json.Add("GameVersions", new JArray(GameVersions)); + Json.Add("Status", (int)Status); + if (FileName is not null) + Json.Add("FileName", FileName); + if (DownloadUrls is not null) + Json.Add("DownloadUrls", new JArray(DownloadUrls)); + if (Hash is not null) + Json.Add("Hash", Hash); + Json.Add("RawDependencies", new JArray(RawDependencies)); + Json.Add("RawOptionalDependencies", new JArray(RawOptionalDependencies)); + Json.Add("Dependencies", new JArray(Dependencies)); + Json.Add("OptionalDependencies", new JArray(OptionalDependencies)); + return Json; + } + + /// + /// 将当前文件信息实例化为控件。 + /// + public MyVirtualizingElement ToListItem(MyListItem.ClickEventHandler onClick, + MyIconButton.ClickEventHandler? onSaveClick = null, + bool badDisplayName = false) + { + return new MyVirtualizingElement(() => + { + // 1. 获取基础描述信息 + var title = badDisplayName ? FileName : DisplayName; + var info = new List(); + + // 2. 填充信息列表 + if (title != FileName.BeforeLast(".")) + info.Add(FileName.BeforeLast(".")); + + if (Dependencies.Any()) + info.Add($"{Dependencies.Count()} 项前置"); + + // 简化后的游戏版本逻辑喵 + var snapshotKeywords = new[] { "w", "snapshot", "rc", "pre", "experimental", "-" }; + if (GameVersions.All(ver => + !ver.Contains('.') || snapshotKeywords.Any(s => ver.ContainsF(s, true)))) + info.Add($"游戏版本 {string.Join("、", GameVersions)}"); + + if (DownloadCount > 0) + info.Add("下载 " + (DownloadCount > 100000 + ? $"{Math.Round(DownloadCount / 10000.0)} 万次" + : $"{DownloadCount} 次")); + + info.Add($"更新于 {TimeUtils.GetTimeSpanString(ReleaseDate - DateTime.Now, false)}"); + + if (Status != CompFileStatus.Release) + info.Add(StatusDescription); + + // 3. 建立控件 + var newItem = new MyListItem + { + Title = title, + SnapsToDevicePixels = true, + Height = 42, + Type = MyListItem.CheckType.Clickable, + Tag = this, + Info = string.Join(",", info), + // 使用 switch 表达式精简 Logo 选择喵! + Logo = Status switch + { + CompFileStatus.Release => ModBase.PathImage + "Icons/R.png", + CompFileStatus.Beta => ModBase.PathImage + "Icons/B.png", + _ => ModBase.PathImage + "Icons/A.png" + } + }; + newItem.Click += onClick; + + // 4. 建立另存为按钮 + if (onSaveClick != null) + { + var btnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(btnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnSave, 30); + ToolTipService.SetHorizontalOffset(btnSave, 2); + btnSave.Click += onSaveClick; + newItem.Buttons = new[] { btnSave }; + } + + return newItem; + }) + { Height = 42 }; + } + + public override string ToString() + { + return $"{Id}: {FileName}"; + } + } + + // 获取 + + /// + /// 已知文件信息的缓存。 + /// + public static ConcurrentDictionary> CompFilesCache = new(); + + /// + /// 获取某个工程下的全部文件列表。 + /// 必须在工作线程执行,失败会抛出异常。 + /// + public static List CompFilesGet(string ProjectId, bool FromCurseForge) + { + // 1. 获取工程对象(使用 TryGetValue 提高效率并防止并发异常) + CompProject TargetProject = null; + if (!CompProjectCache.TryGetValue(ProjectId, out TargetProject)) + { + var url = FromCurseForge + ? $"https://api.curseforge.com/v1/mods/{ProjectId}" + : $"https://api.modrinth.com/v2/project/{ProjectId}"; + if (FromCurseForge) + { + var json = (JObject)ModDownload.DlModRequest(url, true); + TargetProject = new CompProject((JObject)json["data"]); + } + else + { + TargetProject = new CompProject((JObject)ModDownload.DlModRequest(url, true)); + } + // 假设 CompProject 构造函数内已处理缓存,否则此处应添加缓存逻辑 + } + + // 2. 获取并缓存文件列表 + if (!CompFilesCache.ContainsKey(ProjectId)) + { + ModBase.Log("[Comp] 开始获取文件列表:" + ProjectId); + JArray ResultJsonArray; + if (FromCurseForge) + { + // 注意:若 pageSize=10000 失效,需考虑分页逻辑 + var response = (JObject)ModDownload.DlModRequest( + $"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=10000", + true + ); + + ResultJsonArray = (JArray)response["data"]; + } + else + { + ResultJsonArray = + (JArray)ModDownload.DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version", true); + } + + CompFilesCache[ProjectId] = ResultJsonArray.Select(a => new CompFile((JObject)a, TargetProject.Type)) + .Where(a => a.Available).GroupBy(a => a.Id).Select(g => g.First()) + .ToList(); // 使用 GroupBy 实现更高效的 Distinct + } + + var CurrentFiles = CompFilesCache[ProjectId]; + + // 3. 提取所有需要获取信息的前置 ID(合并必要和可选) + var AllRawDeps = CurrentFiles.SelectMany(f => f.RawDependencies.Concat(f.RawOptionalDependencies)).Distinct() + .ToList(); + var UndoneDeps = AllRawDeps.Where(id => !CompProjectCache.ContainsKey(id)).ToList(); + + // 4. 批量请求缺失的前置工程信息 + if (UndoneDeps.Any()) + { + ModBase.Log($"[Comp] {ProjectId} 需要补全信息的依赖项共 {UndoneDeps.Count} 个"); + JArray Projects; + if (FromCurseForge) + { + // 1. 获取响应并转为 JObject + var response = (JObject)ModDownload.DlModRequest( + "https://api.curseforge.com/v1/mods", + "POST", + "{\"modIds\": [" + string.Join(",", UndoneDeps) + "]}", + "application/json" + ); + + // 2. 提取 data 数组 + Projects = (JArray)response["data"]; + } + else + { + Projects = (JArray)ModDownload.DlModRequest( + $"https://api.modrinth.com/v2/projects?ids=[\"{UndoneDeps.Join("\",\"")}\"]", true); + } + + foreach (var Project in Projects) + new CompProject((JObject)Project); + } + + // 5. 建立文件与依赖工程的关联映射 + // 优化:预先筛选出存在于缓存中的依赖工程,避免在多层循环中重复查询字典 + var AvailableDeps = AllRawDeps.Where(id => CompProjectCache.ContainsKey(id) && (id ?? "") != (ProjectId ?? "")) + .Select(id => CompProjectCache[id]).ToList(); + + foreach (var file in CurrentFiles) + foreach (var dep in AvailableDeps) + { + // 处理必要依赖 + if (file.RawDependencies.Contains(dep.Id)) + if (!file.Dependencies.Contains(dep.Id)) + file.Dependencies.Add(dep.Id); + + // 处理可选依赖 + if (file.RawOptionalDependencies.Contains(dep.Id)) + if (!file.OptionalDependencies.Contains(dep.Id)) + file.OptionalDependencies.Add(dep.Id); + } + + return CompFilesCache[ProjectId]; + } + + public static string CompFileNameGet(CompProject proj, CompFile file) + { + string FileName; + if ((proj.TranslatedName ?? "") == (proj.RawName ?? "")) + { + FileName = file.FileName; + } + else + { + var ChineseName = proj.TranslatedName.BeforeFirst(" (").BeforeFirst(" - ").Replace(@"\", "\") + .Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">") + .Replace("*", "*").Replace("?", "?").Replace("\"", "").Replace(": ", ":"); + switch (Config.Download.Comp.NameFormatV2) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + FileName = $"【{ChineseName}】{file.FileName}"; + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + FileName = $"[{ChineseName}] {file.FileName}"; + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 2, false): + { + FileName = $"{ChineseName}-{file.FileName}"; + break; + } + case var case3 when Operators.ConditionalCompareObjectEqual(case3, 3, false): + { + FileName = $"{file.FileName}-{ChineseName}"; + break; + } + + default: + { + FileName = file.FileName; + break; + } + } + } + + if (file.Type == CompType.Mod) + FileName = FileName.Replace("~", "-"); // ~ 会导致 Mixin 加载失败 + return FileName; + } + + /// + /// 预载包含大量 CompFile 的卡片,添加必要的元素和前置列表。 + /// + public static void CompFilesCardPreload(StackPanel Stack, List Files) + { + // 获取卡片对应的前置 ID + // 如果为整合包就不会有 Dependencies 信息,所以不用管 + var Deps = Files.SelectMany(f => f.Dependencies).Distinct().ToList(); + var OptionalDeps = Files.SelectMany(f => f.OptionalDependencies).Distinct().ToList(); + if (!Deps.Any() && !OptionalDeps.Any()) + return; + // 必要前置 + if (Deps.Any()) + { + Deps.Sort(); + Deps = Deps.Where(dep => + { + if (!CompProjectCache.ContainsKey(dep)) + ModBase.Log($"[Comp] 未找到 ID {dep} 的前置信息", ModBase.LogLevel.Debug); + return CompProjectCache.ContainsKey(dep); + }).ToList(); + // 添加开头间隔 + Stack.Children.Add(new TextBlock + { + Text = "必要前置资源", FontSize = 14d, HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 2d, 0d, 5d) + }); + // 添加前置列表 + foreach (var Dep in Deps) + { + var Item = CompProjectCache[Dep].ToCompItem(false, false); + Stack.Children.Add(Item); + } + } + + // 可选前置 + if (OptionalDeps.Any()) + { + OptionalDeps.Sort(); + OptionalDeps = OptionalDeps.Where(dep => + { + if (!CompProjectCache.ContainsKey(dep)) + ModBase.Log($"[Comp] 未找到 ID {dep} 的前置信息", ModBase.LogLevel.Debug); + return CompProjectCache.ContainsKey(dep); + }).ToList(); + // 添加开头间隔 + Stack.Children.Add(new TextBlock + { + Text = "可选前置资源", FontSize = 14d, HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 2d, 0d, 5d) + }); + // 添加前置列表 + foreach (var Dep in OptionalDeps) + { + var Item = CompProjectCache[Dep].ToCompItem(false, false); + Stack.Children.Add(Item); + } + } + + // 添加结尾间隔 + Stack.Children.Add(new TextBlock + { + Text = "版本列表", FontSize = 14d, HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 12d, 0d, 5d) + }); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.cs new file mode 100644 index 000000000..704eefcc4 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.cs @@ -0,0 +1,1779 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Core.Utils.Codecs; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public class CrashAnalyzer +{ + // 1:准备用于分析的 Log 文件 + private readonly List> AnalyzeRawFiles = new(); // 暂存的日志文件:文件完整路径 -> 文件内容 + + // 可能导致崩溃的原因与附加信息 + private readonly Dictionary> CrashReasons = new(); + + // 4:根据原因输出信息 + private readonly List OutputFiles = new(); + + // 构造函数 + private readonly string TempFolder; + + // 暂存分析的实例供特殊用途 + // 龙猫味石山代码小记: CrashAnalyze 猛一顿分析不知道自己在分析啥实例 + private ModMinecraft.McInstance _version; + private KeyValuePair? DirectFile; // 在弹窗中选择直接打开的文件 + private string LogAll; + private string LogCrash; + private string LogHs; + + // 3:根据文本分析崩溃原因 + private string LogMc; + private string LogMcDebug; + + public CrashAnalyzer(int UUID) + { + // 构建文件结构 + TempFolder = ModMain.RequestTaskTempFolder(); + Directory.CreateDirectory(TempFolder + @"Temp\"); + Directory.CreateDirectory(TempFolder + @"Report\"); + ModBase.Log("[Crash] 崩溃分析暂存文件夹:" + TempFolder); + } + + /// + /// 将可用于分析的日志存储到 AnalyzeRawFiles。 + /// + /// 从 PCL 捕获到的最后 200 行程序输出。 + public void Collect(string VersionPathIndie, IList LatestLog = null) + { + ModBase.Log("[Crash] 步骤 1:收集日志文件"); + + // 简单收集可能的日志文件路径 + var PossibleLogs = new List(); + try + { + var DirInfo = new DirectoryInfo(VersionPathIndie + @"crash-reports\"); + if (DirInfo.Exists) + foreach (var File in DirInfo.EnumerateFiles()) + PossibleLogs.Add(File.FullName); + } + catch (Exception ex) + { + ModBase.Log(ex, "收集 Minecraft 崩溃日志文件夹下的日志失败"); + } + + try + { + foreach (var File in new DirectoryInfo(VersionPathIndie).Parent.Parent.EnumerateFiles()) + { + if ((File.Extension ?? "") != ".log") + continue; + PossibleLogs.Add(File.FullName); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "收集 Minecraft 主文件夹下的日志失败"); + } + + try + { + foreach (var File in new DirectoryInfo(VersionPathIndie).EnumerateFiles()) + { + if ((File.Extension ?? "") != ".log") + continue; + PossibleLogs.Add(File.FullName); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "收集 Minecraft 隔离文件夹下的日志失败"); + } + + PossibleLogs.Add(VersionPathIndie + @"logs\latest.log"); // Minecraft 日志 + var LaunchScript = ModBase.ReadFile(ModBase.ExePath + @"PCL\LatestLaunch.bat"); + if (LaunchScript.ContainsF("-Dlog4j2.formatMsgNoLookups=false")) + PossibleLogs.Add(VersionPathIndie + @"logs\debug.log"); // Minecraft Debug 日志 + PossibleLogs = PossibleLogs.Distinct().ToList(); + + // 确定最新的日志文件 + var RightLogs = new List(); + foreach (var LogFile in PossibleLogs) + try + { + var Info = new FileInfo(LogFile); + if (!Info.Exists) + continue; + var Time = Math.Abs((Info.LastWriteTime - DateTime.Now).TotalMinutes); + if (Time < 3d && Info.Length > 0L) + { + RightLogs.Add(LogFile); + ModBase.Log("[Crash] 可能可用的日志文件:" + LogFile + "(" + Math.Round(Time, 1) + " 分钟)"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "确认崩溃日志时间失败(" + LogFile + ")"); + } + + if (!RightLogs.Any()) + ModBase.Log("[Crash] 未发现可能可用的日志文件"); + + // 将可能可用的日志文件导出 + foreach (var FilePath in RightLogs) + try + { + AnalyzeRawFiles.Add(new KeyValuePair(FilePath, + ModBase.ReadFile(FilePath).Split("\r\n".ToCharArray()))); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取可能的崩溃日志文件失败(" + FilePath + ")"); + } + + if (LatestLog is not null && LatestLog.Any()) + { + var RawOutput = LatestLog.Join("\r\n"); + ModBase.Log("[Crash] 以下为游戏输出的最后一段内容:" + "\r\n" + RawOutput); + ModBase.WriteFile(TempFolder + "RawOutput.log", RawOutput); + AnalyzeRawFiles.Add(new KeyValuePair(TempFolder + "RawOutput.log", LatestLog.ToArray())); + LatestLog.Clear(); + } + + ModBase.Log("[Crash] 步骤 1:收集日志文件完成,收集到 " + AnalyzeRawFiles.Count + " 个文件"); + } + + /// + /// 从文件路径直接导入日志文件或崩溃报告压缩包。 + /// + public void Import(string FilePath) + { + ModBase.Log("[Crash] 步骤 1:自主导入日志文件"); + + // 尝试视作压缩包解压 + try + { + var Info = new FileInfo(FilePath); + if (Info.Exists && Info.Length > 0L && !FilePath.EndsWithF(".jar", true)) + { + ModBase.ExtractFile(FilePath, TempFolder + @"Temp\"); + ModBase.Log("[Crash] 已解压导入的日志文件:" + FilePath); + goto Extracted; + } + } + catch + { + } + + // 并非压缩包 + ModBase.CopyFile(FilePath, TempFolder + @"Temp\" + ModBase.GetFileNameFromPath(FilePath)); + ModBase.Log("[Crash] 已复制导入的日志文件:" + FilePath); + Extracted: ; + + + // 导入其中的日志文件 + foreach (var TargetFile in new DirectoryInfo(TempFolder + @"Temp\").EnumerateFiles().ToList()) + try + { + if (!TargetFile.Exists || TargetFile.Length == 0L) + continue; + var Ext = TargetFile.Extension.ToLower(); + if (Ext == ".log" || Ext == ".txt") + AnalyzeRawFiles.Add(new KeyValuePair(TargetFile.FullName, + ModBase.ReadFile(TargetFile.FullName).Split("\r\n".ToCharArray()))); + else + File.Delete(TargetFile.FullName); + } + catch (Exception ex) + { + ModBase.Log(ex, "导入单个日志文件失败"); + } + + ModBase.Log("[Crash] 步骤 1:自主导入日志文件,收集到 " + AnalyzeRawFiles.Count + " 个文件"); + } + + /// + /// 从 AnalyzeRawFiles 中提取实际有用的文本片段存储到 AnalyzeFiles,并整理可用于生成报告的文件。 + /// 返回是否有足够信息可用于分析。 + /// + public bool Prepare() + { + bool PrepareRet = default; + ModBase.Log("[Crash] 步骤 2:准备日志文本"); + + // 对日志文件进行分类 + DirectFile = default; + var AllFiles = new List>>(); + foreach (var LogFile in AnalyzeRawFiles) + { + var MatchName = ModBase.GetFileNameFromPath(LogFile.Key).ToLower(); + AnalyzeFileType TargetType; + if (MatchName.StartsWithF("hs_err")) + { + TargetType = AnalyzeFileType.HsErr; + DirectFile = LogFile; + } + else if (MatchName.StartsWithF("crash-")) + { + TargetType = AnalyzeFileType.CrashReport; + DirectFile = LogFile; + } + else if (MatchName == "latest.log" || MatchName == "latest log.txt" || MatchName == "debug.log" || + MatchName == "debug log.txt" || MatchName == "游戏崩溃前的输出.txt" || MatchName == "rawoutput.log") + { + TargetType = AnalyzeFileType.MinecraftLog; + if (DirectFile is null) + DirectFile = LogFile; + } + else if (MatchName == "启动器日志.txt" || MatchName == "PCL2 启动器日志.txt" || MatchName == "PCL 启动器日志.txt" || + MatchName == "log1.txt" || MatchName == "log-ce1.log") + { + if (LogFile.Value.Any(s => s.Contains("以下为游戏输出的最后一段内容"))) + { + TargetType = AnalyzeFileType.MinecraftLog; + if (DirectFile is null) + DirectFile = LogFile; + } + else + { + TargetType = AnalyzeFileType.ExtraLogFile; + } + } + else if (MatchName.EndsWithF(".log", true)) + { + TargetType = AnalyzeFileType.ExtraLogFile; + } + else if (MatchName.EndsWithF(".txt", true)) + { + TargetType = AnalyzeFileType.ExtraReportFile; + } + else + { + ModBase.Log("[Crash] " + MatchName + " 分类为 Ignore"); + continue; + } + + if (LogFile.Value.Any()) + { + AllFiles.Add(new KeyValuePair>(TargetType, LogFile)); + ModBase.Log("[Crash] " + MatchName + " 分类为 " + ModBase.GetStringFromEnum(TargetType)); + } + else + { + ModBase.Log("[Crash] " + MatchName + " 由于内容为空跳过"); + } + } + + // 若只有额外日志,则将它们视作 Minecraft 日志 + if (AllFiles.Any() && AllFiles.All(p => p.Key == AnalyzeFileType.ExtraLogFile)) + { + ModBase.Log("[Crash] 由于仅发现了额外日志,将它们视作 Minecraft 日志进行分析"); + AllFiles = AllFiles.Select(p => + new KeyValuePair>(AnalyzeFileType.MinecraftLog, + p.Value)).ToList(); + } + + // 将分类后的文件分别写入 + foreach (var SelectType in new[] + { + AnalyzeFileType.MinecraftLog, AnalyzeFileType.HsErr, AnalyzeFileType.ExtraLogFile, + AnalyzeFileType.CrashReport + }) + { + // 获取该种类的所有文件 {文件路径 -> 文件内容行} + var SelectedFiles = new List>(); + foreach (var File in AllFiles) + if (SelectType == File.Key) + SelectedFiles.Add(File.Value); + if (!SelectedFiles.Any()) + continue; + try + { + // 根据文件类别判断 + switch (SelectType) + { + case AnalyzeFileType.HsErr: + case AnalyzeFileType.CrashReport: + { + // 获取文件的修改日期 + var DatedFiles = new SortedList>(); + foreach (var File in SelectedFiles) + try + { + DatedFiles.Add(new FileInfo(File.Key).LastWriteTime, File); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取日志文件修改时间失败"); + DatedFiles.Add(new DateTime(1900, 1, 1), File); + } + + // 输出最新的文件 + var NewestFile = DatedFiles.Last().Value; + OutputFiles.Add(NewestFile.Key); + if (SelectType == AnalyzeFileType.HsErr) + { + LogHs = GetHeadTailLines(NewestFile.Value, 200, 100); + ModBase.Log("[Crash] 输出报告:" + NewestFile.Key + ",作为虚拟机错误信息"); + ModBase.Log("[Crash] 导入分析:" + NewestFile.Key + ",作为虚拟机错误信息"); + } + else + { + LogCrash = GetHeadTailLines(NewestFile.Value, 300, 700); + ModBase.Log("[Crash] 输出报告:" + NewestFile.Key + ",作为 Minecraft 崩溃报告"); + ModBase.Log("[Crash] 导入分析:" + NewestFile.Key + ",作为 Minecraft 崩溃报告"); + } + + break; + } + case AnalyzeFileType.MinecraftLog: + { + LogMc = ""; + LogMcDebug = ""; + // 创建文件名词典 + var FileNameDict = new Dictionary>(); + foreach (var SelectedFile in SelectedFiles) + { + FileNameDict[ModBase.GetFileNameFromPath(SelectedFile.Key).ToLower()] = SelectedFile; + OutputFiles.Add(SelectedFile.Key); + ModBase.Log("[Crash] 输出报告:" + SelectedFile.Key + ",作为 Minecraft 或启动器日志"); + } + + // 选择一份最佳的来自启动器的游戏日志 + foreach (var FileName in new[] + { + "rawoutput.log", "启动器日志.txt", "log1.txt", "log-ce1.log", "游戏崩溃前的输出.txt", + "PCL2 启动器日志.txt", "PCL 启动器日志.txt" + }) + { + if (!FileNameDict.ContainsKey(FileName)) + continue; + var CurrentLog = FileNameDict[FileName]; + // 截取 “以下为游戏输出的最后一段内容” 后的内容 + var HasLauncherMark = false; + foreach (var Line in CurrentLog.Value) + if (HasLauncherMark) + { + LogMc += Line + "\n"; + } + else if (Line.Contains("以下为游戏输出的最后一段内容")) + { + HasLauncherMark = true; + ModBase.Log("[Crash] 找到 PCL 输出的游戏实时日志头"); + } + + // 导入后 500 行 + if (!HasLauncherMark) + LogMc += GetHeadTailLines(CurrentLog.Value, 0, 500); + LogMc = LogMc.TrimEnd("\r\n".ToCharArray()); + ModBase.Log("[Crash] 导入分析:" + CurrentLog.Key + ",作为启动器日志"); + break; + } + + // 选择一份最佳的 Minecraft Log + foreach (var FileName in new[] { "latest.log", "latest log.txt", "debug.log", "debug log.txt" }) + { + if (!FileNameDict.ContainsKey(FileName)) + continue; + var CurrentLog = FileNameDict[FileName]; + LogMc += GetHeadTailLines(CurrentLog.Value, 1500, 500); + ModBase.Log("[Crash] 导入分析:" + CurrentLog.Key + ",作为 Minecraft 日志"); + break; + } + + // 查找 Debug Log + foreach (var FileName in new[] { "debug.log", "debug log.txt" }) + { + if (!FileNameDict.ContainsKey(FileName)) + continue; + var CurrentLog = FileNameDict[FileName]; + LogMcDebug += GetHeadTailLines(CurrentLog.Value, 1000, 0); + ModBase.Log("[Crash] 导入分析:" + CurrentLog.Key + ",作为 Minecraft Debug 日志"); + break; + } + + // 兜底 + if (string.IsNullOrEmpty(LogMc)) + { + if (!string.IsNullOrEmpty(LogMcDebug)) // 如果没有找到 Minecraft 日志,则使用 Debug 日志作为兜底 + { + LogMc = LogMcDebug; + } + else if (FileNameDict.Any()) // 如果都没有找到,则使用第一个文件 + { + var CurrentLog = FileNameDict.First().Value; + LogMc += GetHeadTailLines(CurrentLog.Value, 1500, 500); + ModBase.Log("[Crash] 导入分析:" + CurrentLog.Key + ",作为兜底日志"); + } + else + { + LogMc = null; + throw new Exception("无法找到匹配的 Minecraft Log"); + } + } + + if (string.IsNullOrEmpty(LogMcDebug)) + LogMcDebug = null; + break; + } + case AnalyzeFileType.ExtraLogFile: + case AnalyzeFileType.ExtraReportFile: + { + // 全部丢过去 + foreach (var SelectedFile in SelectedFiles) + { + OutputFiles.Add(SelectedFile.Key); + ModBase.Log("[Crash] 输出报告:" + SelectedFile.Key + ",不用作分析"); + } + + break; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "分类处理日志文件时出错"); + } + } + + // 结束 + PrepareRet = LogMc is not null || LogHs is not null || LogCrash is not null; + if (PrepareRet) + ModBase.Log(("[Crash] 步骤 2:准备日志文本完成,找到" + (LogMc is null ? "" : "游戏日志、") + + (LogMcDebug is null ? "" : "游戏 Debug 日志、") + (LogHs is null ? "" : "虚拟机日志、") + + (LogCrash is null ? "" : "崩溃日志、")).TrimEnd('、') + "用作分析"); + else + ModBase.Log("[Crash] 步骤 2:准备日志文本完成,没有任何可供分析的日志"); + + return PrepareRet; + } + + /// + /// 输出字符串的前后某些行,并统一行尾为 vbLf (正则 \n)、删除空行和重复行。 + /// + private string GetHeadTailLines(string[] Raw, int HeadLines, int TailLines) + { + if (Raw.Length <= HeadLines + TailLines) + return Raw.Distinct().Join("\n"); + var Lines = new List(); + var RealHeadLines = 0; + int ViewedLines; + var loopTo = Raw.Length - 1; + for (ViewedLines = 0; ViewedLines <= loopTo; ViewedLines++) + { + if (Lines.Contains(Raw[ViewedLines])) + continue; + RealHeadLines += 1; + Lines.Add(Raw[ViewedLines]); + if (RealHeadLines >= HeadLines) + break; + } + + var RealTailLines = 0; + for (int i = Raw.Length - 1, loopTo1 = ViewedLines; i >= loopTo1; i -= 1) + { + if (Lines.Contains(Raw[i])) + continue; + RealTailLines += 1; + Lines.Insert(RealHeadLines, Raw[i]); + if (RealTailLines >= TailLines) + break; + } + + var Result = new StringBuilder(); + foreach (var Line in Lines) + { + if (string.IsNullOrEmpty(Line)) + continue; + Result.Append(Line); + Result.Append("\n"); + } + + return Result.ToString(); + } + + /// + /// 根据 AnalyzeLogs 与可能的实例信息分析崩溃原因。 + /// + public void Analyze(ModMinecraft.McInstance version = null) + { + _version = version; + ModBase.Log("[Crash] 步骤 3:分析崩溃原因"); + LogAll = (LogMc ?? LogMcDebug ?? "") + (LogHs ?? "") + (LogCrash ?? ""); + + // 处理 Quilt Mod Table 以避免错误分析 (CE #107) + if (LogAll.Contains("quilt") && LogAll.Contains("Mod Table Version")) + { + ModBase.Log("[Crash] 处理 Quilt Mod Table 后再继续分析"); + var beforeTable = LogAll.BeforeFirst("| Index"); + var afterTable = LogAll.AfterFirst("Mod Table Version:"); + LogAll = beforeTable + afterTable; + } + + // 1. 精准日志匹配,中/高优先级 + AnalyzeCrit1(); + if (CrashReasons.Any()) + goto Done; + AnalyzeCrit2(); + if (CrashReasons.Any()) + goto Done; + + // 2. 堆栈分析 + if (LogAll.Contains("orge") || LogAll.Contains("abric") || LogAll.Contains("uilt") || + LogAll.Contains("iteloader")) + { + var Keywords = new List(); + // 崩溃日志 + if (LogCrash is not null) + { + ModBase.Log("[Crash] 开始进行崩溃日志堆栈分析"); + Keywords.AddRange(AnalyzeStackKeyword(LogCrash.BeforeFirst("System Details"))); + } + + // Minecraft 日志 + if (LogMc is not null) + { + var Fatals = LogMc.RegexSearch(@"/FATAL] .+?(?=[\n]+\[)"); + if (LogMc.Contains("Unreported exception thrown!")) + Fatals.Add(LogMc.Between("Unreported exception thrown!", "at oolloo.jlw.Wrapper")); + ModBase.Log("[Crash] 开始进行 Minecraft 日志堆栈分析,发现 " + Fatals.Count + " 个报错项"); + foreach (var Fatal in Fatals) + Keywords.AddRange(AnalyzeStackKeyword(Fatal)); + } + + // 虚拟机日志 + if (LogHs is not null) + { + ModBase.Log("[Crash] 开始进行虚拟机堆栈分析"); + var StackLogs = LogHs.Between("T H R E A D", "Registers:"); + Keywords.AddRange(AnalyzeStackKeyword(StackLogs)); + } + + // Mod 名称分析 + if (Keywords.Any()) + { + var Names = AnalyzeModName(Keywords); + if (Names is null) + AppendReason(CrashReason.堆栈分析发现关键字, Keywords); + else + AppendReason(CrashReason.堆栈分析发现Mod名称, Names); + goto Done; + } + } + else + { + ModBase.Log("[Crash] 可能并未安装 Mod,不进行堆栈分析"); + } + + // 3. 精准日志匹配,低优先级 + AnalyzeCrit3(); + + // 输出到日志 + Done: ; + + if (!CrashReasons.Any()) + { + ModBase.Log("[Crash] 步骤 3:分析崩溃原因完成,未找到可能的原因"); + } + else + { + ModBase.Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " + CrashReasons.Count + " 条可能的原因"); + foreach (var Reason in CrashReasons) + ModBase.Log("[Crash] - " + ModBase.GetStringFromEnum(Reason.Key) + + (Reason.Value.Any() ? "(" + Reason.Value.Join(";") + ")" : "")); + } + } + + /// + /// 增加一个可能的崩溃原因。 + /// + private void AppendReason(CrashReason Reason, ICollection Additional = null) + { + if (CrashReasons.ContainsKey(Reason)) + { + if (Additional is not null) + { + CrashReasons[Reason].AddRange(Additional); + CrashReasons[Reason] = CrashReasons[Reason].Distinct().ToList(); + } + } + else + { + CrashReasons.Add(Reason, new List(Additional ?? Array.Empty())); + } + + ModBase.Log("[Crash] 可能的崩溃原因:" + ModBase.GetStringFromEnum(Reason) + + (Additional is not null && Additional.Any() ? "(" + Additional.Join(";") + ")" : "")); + } + + private void AppendReason(CrashReason Reason, string Additional) + { + AppendReason(Reason, string.IsNullOrEmpty(Additional) ? null : new List { Additional }); + } + + // 具体的分析代码 + /// + /// 进行精准日志匹配。匹配优先级高于堆栈分析的崩溃。 + /// + private void AnalyzeCrit1() + { + // 空白分析 + if (LogMc is null && LogHs is null && LogCrash is null) + { + AppendReason(CrashReason.没有可用的分析文件); + return; + } + + // 崩溃报告分析,高优先级 + if (LogCrash is not null) + if (LogCrash.Contains("Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass")) + AppendReason(CrashReason.Java版本过高); + + // 游戏日志分析 + if (LogMc is not null) + { + if (LogMc.Contains("Found multiple arguments for option fml.forgeVersion, but you asked for only one")) + AppendReason(CrashReason.实例Json中存在多个Forge); + if (LogMc.Contains("The driver does not appear to support OpenGL")) + AppendReason(CrashReason.显卡不支持OpenGL); + if (LogMc.Contains("java.lang.ClassCastException: java.base/jdk")) + AppendReason(CrashReason.使用JDK); + if (LogMc.Contains("java.lang.ClassCastException: class jdk.")) + AppendReason(CrashReason.使用JDK); + if (LogMc.Contains("TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.(Reflector.java")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + if (LogMc.Contains("Open J9 is not supported") || LogMc.Contains("OpenJ9 is incompatible") || + LogMc.Contains(".J9VMInternals.")) + AppendReason(CrashReason.使用OpenJ9); + if (LogMc.Contains("java.lang.NoSuchFieldException: ucp")) + AppendReason(CrashReason.Java版本过高); + if (LogMc.Contains("because module java.base does not export")) + AppendReason(CrashReason.Java版本过高); + if (LogMc.Contains( + "java.lang.ClassNotFoundException: jdk.nashorn.api.scripting.NashornScriptEngineFactory")) + AppendReason(CrashReason.Java版本过高); + if (LogMc.Contains("java.lang.ClassNotFoundException: java.lang.invoke.LambdaMetafactory")) + AppendReason(CrashReason.Java版本过高); + if (LogMc.Contains("The directories below appear to be extracted jar files. Fix this before you continue.")) + AppendReason(CrashReason.Mod文件被解压); + if (LogMc.Contains("Extracted mod jars found, loading will NOT continue")) + AppendReason(CrashReason.Mod文件被解压); + if (LogMc.Contains("java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker")) + AppendReason(CrashReason.MixinBootstrap缺失); + if (LogMc.Contains("Couldn't set pixel format")) + AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式); + if (LogMc.Contains("java.lang.OutOfMemoryError") || LogMc.Contains("an out of memory error")) + AppendReason(CrashReason.内存不足); + if (LogMc.Contains( + "java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.")) + AppendReason(CrashReason.ShadersMod与OptiFine同时安装); + if (LogMc.Contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier")) + AppendReason(CrashReason.低版本Forge与高版本Java不兼容); + if (LogMc.Contains("1282: Invalid operation")) + AppendReason(CrashReason.光影或资源包导致OpenGL1282错误); + if (LogMc.Contains( + "signer information does not match signer information of other classes in the same package")) + AppendReason(CrashReason.文件或内容校验失败, + (LogMc.RegexSeek("(?<=class \")[^']+(?=\"'s signer information)") ?? "").TrimEnd( + Conversions.ToChar("\r\n"))); + if (LogMc.Contains("Maybe try a lower resolution resourcepack?")) + AppendReason(CrashReason.材质过大或显卡配置不足); + if (LogMc.Contains( + "java.lang.NoSuchMethodError: net.minecraft.world.server.ChunkManager$ProxyTicketManager.shouldForceTicks(J)Z") && + LogMc.Contains("OptiFine")) + AppendReason(CrashReason.OptiFine导致无法加载世界); + if (LogMc.Contains("Unsupported class file major version")) + AppendReason(CrashReason.Java版本不兼容); + if (LogMc.Contains("com.electronwill.nightconfig.core.io.ParsingException: Not enough data available")) + AppendReason(CrashReason.NightConfig的Bug); + if (LogMc.Contains("Cannot find launch target fmlclient, unable to launch")) + AppendReason(CrashReason.Forge安装不完整); + if (LogMc.Contains("Invalid paths argument, contained no existing paths") && + LogMc.Contains(@"libraries\net\minecraftforge\fmlcore")) + AppendReason(CrashReason.Forge安装不完整); + if (LogMc.Contains("Invalid module name: '' is not a Java identifier")) + AppendReason(CrashReason.Mod名称包含特殊字符); + if (LogMc.Contains( + "has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to")) + AppendReason(CrashReason.Mod需要Java11); + if (LogMc.Contains( + "java.lang.RuntimeException: java.lang.NoSuchMethodException: no such method: sun.misc.Unsafe.defineAnonymousClass(Class,byte[],Object[])Class/invokeVirtual")) + AppendReason(CrashReason.Mod需要Java11); + if (LogMc.Contains( + "java.lang.IllegalArgumentException: The requested compatibility level JAVA_11 could not be set. Level is not supported by the active JRE or ASM version")) + AppendReason(CrashReason.Mod需要Java11); + if (LogMc.Contains("Unsupported major.minor version")) + AppendReason(CrashReason.Java版本不兼容); + if (LogMc.Contains("Invalid maximum heap size")) + AppendReason(CrashReason.使用32位Java导致JVM无法分配足够多的内存); + if (LogMc.Contains("Could not reserve enough space")) + { + if (LogMc.Contains("for 1048576KB object heap")) + AppendReason(CrashReason.使用32位Java导致JVM无法分配足够多的内存); + else + AppendReason(CrashReason.内存不足); + } + + // 确定的 Mod 导致崩溃 + if (LogMc.Contains("Caught exception from ")) + AppendReason(CrashReason.确定Mod导致游戏崩溃, + TryAnalyzeModName(LogMc.RegexSeek(@"(?<=Caught exception from )[^\n]+?") + ?.TrimEnd(("\r\n" + " ").ToCharArray()))); + // Mod 重复 / 前置问题 + if (LogMc.Contains("DuplicateModsFoundException")) + AppendReason(CrashReason.Mod重复安装, + LogMc.RegexSearch(@"(?<=\n\t[\w]+ : [A-Z]:[^\n]+(/|\\))[^/\\\n]+?.jar", RegexOptions.IgnoreCase)); + if (LogMc.Contains("Found a duplicate mod")) + AppendReason(CrashReason.Mod重复安装, + (LogMc.RegexSeek(@"Found a duplicate mod[^\n]+") ?? "").RegexSearch(@"[^\\/]+.jar", + RegexOptions.IgnoreCase)); + if (LogMc.Contains("Found duplicate mods")) + AppendReason(CrashReason.Mod重复安装, + LogMc.RegexSearch(@"(?<=Mod ID: ')\w+?(?=' from mod files:)").Distinct().ToList()); + if (LogMc.Contains("ModResolutionException: Duplicate")) + AppendReason(CrashReason.Mod重复安装, + (LogMc.RegexSeek(@"ModResolutionException: Duplicate[^\n]+") ?? "").RegexSearch(@"[^\\/]+.jar", + RegexOptions.IgnoreCase)); + if (LogMc.Contains("Incompatible mods found!")) // #5006 + AppendReason(CrashReason.Mod互不兼容, + LogMc.RegexSeek(@"(?<=Incompatible mods found![\s\S]+: )[\s\S]+?(?=\tat )") ?? ""); + if (LogMc.Contains("Missing or unsupported mandatory dependencies:")) + AppendReason(CrashReason.Mod缺少前置或MC版本错误, + LogMc.RegexSearch(@"(?<=Missing or unsupported mandatory dependencies:)([\n\r]+\t(.*))+", + RegexOptions.IgnoreCase) + .Select(s => s.Trim(("\r\n" + Constants.vbTab + " ").ToCharArray())).Distinct() + .ToList()); + } + + // 虚拟机日志分析 + if (LogHs is not null) + { + if (LogHs.Contains("The system is out of physical RAM or swap space")) + AppendReason(CrashReason.内存不足); + if (LogHs.Contains("Out of Memory Error")) + AppendReason(CrashReason.内存不足); + if (LogHs.Contains("EXCEPTION_ACCESS_VIOLATION")) + { + if (LogHs.Contains("# C [ig")) + AppendReason(CrashReason.Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION); + if (LogHs.Contains("# C [atio")) + AppendReason(CrashReason.AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION); + if (LogHs.Contains("# C [nvoglv")) + AppendReason(CrashReason.Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION); + } + } + + // 崩溃报告分析 + if (LogCrash is not null) + { + if (LogCrash.Contains("maximum id range exceeded")) + AppendReason(CrashReason.Mod过多导致超出ID限制); + if (LogCrash.Contains("java.lang.OutOfMemoryError")) + AppendReason(CrashReason.内存不足); + if (LogCrash.Contains("Pixel format not accelerated")) + AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式); + if (LogCrash.Contains("Manually triggered debug crash")) + AppendReason(CrashReason.玩家手动触发调试崩溃); + if (LogCrash.Contains("has mods that were not found") && + LogCrash.RegexCheck(@"The Mod File [^\n]+optifine\\OptiFine[^\n]+ has mods that were not found")) + AppendReason(CrashReason.OptiFine与Forge不兼容); + // Mod 导致的崩溃 + if (LogCrash.Contains("-- MOD ")) + { + var LogCrashMod = LogCrash.Between("-- MOD ", "Failure message:"); + if (LogCrashMod.ContainsF(".jar", true)) + AppendReason(CrashReason.确定Mod导致游戏崩溃, + (LogCrashMod.RegexSeek("(?<=Mod File: ).+") ?? "").TrimEnd( + ("\r\n" + " ").ToCharArray())); + else + AppendReason(CrashReason.Mod加载器报错, + (LogCrash.RegexSeek(@"(?<=Failure message: )[\w\W]+?(?=\tMod)") ?? "") + .Replace(Constants.vbTab, " ").TrimEnd(("\r\n" + " ").ToCharArray())); + } + + if (LogCrash.Contains("Multiple entries with same key: ")) + AppendReason(CrashReason.确定Mod导致游戏崩溃, + TryAnalyzeModName( + (LogCrash.RegexSeek("(?<=Multiple entries with same key: )[^=]+") ?? "").TrimEnd( + ("\r\n" + " ").ToCharArray()))); + if (LogCrash.Contains("LoaderExceptionModCrash: Caught exception from ")) + AppendReason(CrashReason.确定Mod导致游戏崩溃, + TryAnalyzeModName( + (LogCrash.RegexSeek(@"(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+") ?? "") + .TrimEnd(("\r\n" + " ").ToCharArray()))); + if (LogCrash.Contains("Failed loading config file ")) + AppendReason(CrashReason.Mod配置文件导致游戏崩溃, + new[] + { + TryAnalyzeModName( + (LogCrash.RegexSeek(@"(?<=Failed loading config file .+ for modid )[^\n]+") ?? "").TrimEnd( + Conversions.ToChar("\r\n"))).First(), + (LogCrash.RegexSeek("(?<=Failed loading config file ).+(?= of type)") ?? "").TrimEnd( + Conversions.ToChar("\r\n")) + }); + } + } + + /// + /// 进行精准日志匹配。匹配优先级高于堆栈分析的崩溃,但低于上面的。 + /// 如果第一步已经找到了原因则不执行该检测。 + /// + private void AnalyzeCrit2() + { + // Mixin 分析 + bool MixinAnalyze(string LogText) + { + var IsMixin = LogText.Contains("Mixin prepare failed ") || LogText.Contains("Mixin apply failed ") || + LogText.Contains("MixinApplyError") || LogText.Contains("MixinTransformerError") || + LogText.Contains("mixin.injection.throwables.") || LogText.Contains(".json] FAILED during )"); + if (!IsMixin) + return false; + // Mod 名称匹配 + var ModName = LogText.RegexSeek(@"(?<=from mod )[^.\/ ]+(?=\] from)"); + if (ModName is null) + ModName = LogText.RegexSeek(@"(?<=for mod )[^.\/ ]+(?= failed)"); + if (ModName is not null) + { + AppendReason(CrashReason.ModMixin失败, + TryAnalyzeModName(ModName.TrimEnd(("\r\n" + " ").ToCharArray()))); + return true; + } + + // JSON 名称匹配 + foreach (var JsonName in LogText.RegexSearch(@"(?<=^[^\t]+[ \[{(]{1})[^ \[{(]+\.[^ ]+(?=\.json)", + RegexOptions.Multiline)) + { + AppendReason(CrashReason.ModMixin失败, + TryAnalyzeModName(JsonName.Replace("mixins", "mixin").Replace(".mixin", "").Replace("mixin.", ""))); + return true; + } + + // 没有明确匹配 + AppendReason(CrashReason.ModMixin失败); + return true; + } + + ; + + // 游戏日志分析 + if (LogMc is not null) + { + // Mixin 崩溃 + var IsMixin = MixinAnalyze(LogMc); + // 常规信息 + if (LogMc.Contains("An exception was thrown, the game will display an error screen and halt.")) + AppendReason(CrashReason.Forge报错, + LogMc.RegexSeek( + @"(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)") + ?.Trim(Conversions.ToChar("\r\n"))); + if (LogMc.Contains("A potential solution has been determined:")) + AppendReason(CrashReason.Fabric报错并给出解决方案, + (LogMc.RegexSeek(@"(?<=A potential solution has been determined:\n)(\s+ - [^\n]+\n)+") ?? "") + .RegexSearch(@"(?<=\s+)[^\n]+").Join("\n")); + if (LogMc.Contains("A potential solution has been determined, this may resolve your problem:")) + AppendReason(CrashReason.Fabric报错并给出解决方案, + (LogMc.RegexSeek( + @"(?<=A potential solution has been determined, this may resolve your problem:\n)(\s+ - [^\n]+\n)+") ?? + "").RegexSearch(@"(?<=\s+)[^\n]+").Join("\n")); + if (LogMc.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:")) + AppendReason(CrashReason.Fabric报错并给出解决方案, + (LogMc.RegexSeek(@"(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)(\s+ - [^\n]+\n)+") ?? "") + .RegexSearch(@"(?<=\s+)[^\n]+").Join("\n")); + if (!IsMixin && + LogMc.Contains( + "due to errors, provided by ")) // 在 #3104 的情况下,这一句导致 OptiFabric 的 Mixin 失败错判为 Fabric Loader 加载失败 + AppendReason(CrashReason.确定Mod导致游戏崩溃, + TryAnalyzeModName( + (LogMc.RegexSeek("(?<=due to errors, provided by ')[^']+") ?? "").TrimEnd( + ("\r\n" + " ").ToCharArray()))); + } + + // 崩溃报告分析 + if (LogCrash is not null) + { + // Mixin 崩溃 + MixinAnalyze(LogCrash); + // 常规信息 + if (LogCrash.Contains("Suspected Mod")) + { + var SuspectsRaw = LogCrash.Between("Suspected Mod", "Stacktrace"); + if (!SuspectsRaw.StartsWithF("s: None")) // Suspected Mods: None + { + var Suspects = SuspectsRaw.RegexSearch(@"(?<=\n\t[^(\t]+\()[^)\n]+"); + if (Suspects.Any()) + AppendReason(CrashReason.怀疑Mod导致游戏崩溃, TryAnalyzeModName(Suspects)); + } + } + } + } + + /// + /// 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。 + /// + private void AnalyzeCrit3() + { + // 游戏日志分析 + if (LogMc is not null) + { + // 极短的程序输出 + if (!(LogMc.Contains("at net.") || LogMc.Contains("INFO]")) && LogHs is null && LogCrash is null && + LogMc.Length < 100) AppendReason(CrashReason.极短的程序输出, LogMc); + // Mod 解析错误(常见于 Fabric 前置校验失败) + if (LogMc.Contains("Mod resolution failed")) + AppendReason(CrashReason.Mod加载器报错); + // Mixin 失败可以导致大量 Mod 实例创建失败 + if (LogMc.Contains("Failed to create mod instance.")) + AppendReason(CrashReason.Mod初始化失败, + TryAnalyzeModName( + (LogMc.RegexSeek("(?<=Failed to create mod instance. ModID: )[^,]+") ?? + LogMc.RegexSeek(@"(?<=Failed to create mod instance. ModId )[^\n]+(?= for )") ?? "") + .TrimEnd(Conversions.ToChar("\r\n")))); + // 注意:Fabric 的 Warnings were found! 不一定是崩溃原因,它可能是单纯的警报 + } + + // 崩溃报告分析 + if (LogCrash is not null) + { + if (LogCrash.Contains(Constants.vbTab + "Block location: World: ")) + AppendReason(CrashReason.特定方块导致崩溃, + (LogCrash.RegexSeek(@"(?<=\tBlock: Block\{)[^\}]+") ?? "") + " " + + (LogCrash.RegexSeek(@"(?<=\tBlock location: World: )\([^\)]+\)") ?? "")); + if (LogCrash.Contains(Constants.vbTab + "Entity's Exact location: ")) + AppendReason(CrashReason.特定实体导致崩溃, + (LogCrash.RegexSeek(@"(?<=\tEntity Type: )[^\n]+(?= \()") ?? "") + " (" + + (LogCrash.RegexSeek(@"(?<=\tEntity's Exact location: )[^\n]+") ?? "").TrimEnd( + "\r\n".ToCharArray()) + ")"); + } + } + + /// + /// 从堆栈中提取 Mod ID 关键字。若失败则返回空列表。 + /// + private List AnalyzeStackKeyword(string ErrorStack) + { + ErrorStack = "\n" + (ErrorStack ?? "") + "\n"; + + // 进行正则匹配 + var StackSearchResults = new List(); + StackSearchResults.AddRange( + ErrorStack.RegexSearch(@"(?<=\n[^{]+)[a-zA-Z_]+\w+\.[a-zA-Z_]+[\w\.]+(?=\.[\w\.$]+\.)")); + StackSearchResults.AddRange(ErrorStack.RegexSearch(@"(?<=at [^(]+?\.\w+\$\w+\$)[\w\$]+?(?=\$\w+\()") + .Select(s => s.Replace("$", "."))); // Mixin 堆栈:xxx.xxx.xxxx$xxxx$xxx + StackSearchResults = StackSearchResults.Distinct().ToList(); + + // 检查堆栈开头 + var PossibleStacks = new List(); + foreach (var Stack in StackSearchResults) + { + // If Not Stack.Contains(".") Then Continue For + foreach (var IgnoreStack in new[] + { + "java", "sun", "javax", "jdk", "oolloo", "org.lwjgl", "com.sun", "net.minecraftforge", + "paulscode.sound", "com.mojang", "net.minecraft", "cpw.mods", "com.google", "org.apache", + "org.spongepowered", "net.fabricmc", "com.mumfrey", "org.quiltmc", + "com.electronwill.nightconfig", "it.unimi.dsi", "MojangTricksIntelDriversForPerformance_javaw" + }) + if (Stack.StartsWithF(IgnoreStack)) + goto NextStack; + PossibleStacks.Add(Stack.Trim()); + NextStack: ; + } + + PossibleStacks = PossibleStacks.Distinct().ToList(); + + ModBase.Log("[Crash] 找到 " + PossibleStacks.Count + " 条可能的堆栈信息"); + if (!PossibleStacks.Any()) + return new List(); + foreach (var Stack in PossibleStacks) + ModBase.Log("[Crash] - " + Stack); + + // 检查堆栈关键词 + var PossibleWords = new List(); + foreach (var Stack in PossibleStacks) + { + var Splited = Stack.Split("."); + for (int i = 0, loopTo = Math.Min(3, Splited.Count() - 1); i <= loopTo; i++) // 最多取前 4 节 + { + var Word = Splited[i]; + if (Word.Length <= 2 || Word.StartsWithF("func_")) + continue; + if (new[] + { + "com", "org", "net", "asm", "fml", "mod", "jar", "sun", "lib", "map", "gui", "dev", "nio", + "api", "dsi", "top", "mcp", "core", "init", "mods", "main", "file", "game", "load", "read", + "done", "util", "tile", "item", "base", "oshi", "impl", "data", "pool", "task", "forge", + "setup", "block", "model", "mixin", "event", "unimi", "netty", "world", "lwjgl", "gitlab", + "common", "server", "config", "mixins", "compat", "loader", "launch", "entity", "assist", + "client", "plugin", "modapi", "mojang", "shader", "events", "github", "recipe", "render", + "packet", "events", "preinit", "preload", "machine", "reflect", "channel", "general", "handler", + "content", "systems", "modules", "service", "fastutil", "optifine", "internal", "platform", + "override", "fabricmc", "neoforge", "injection", "listeners", "scheduler", "minecraft", + "universal", "multipart", "neoforged", "microsoft", "transformer", "transformers", + "minecraftforge", "blockentity", "spongepowered", "electronwill" + }.Contains(Word.ToLower())) + continue; + PossibleWords.Add(Word.Trim()); + } + } + + PossibleWords = PossibleWords.Distinct().ToList(); + ModBase.Log("[Crash] 从堆栈信息中找到 " + PossibleWords.Count + " 个可能的 Mod ID 关键词"); + if (PossibleWords.Any()) + ModBase.Log("[Crash] - " + PossibleWords.Join(", ")); + if (PossibleWords.Count > 10) + { + ModBase.Log("[Crash] 关键词过多,考虑匹配出错,不纳入考虑"); + return new List(); + } + + return PossibleWords; + } + + /// + /// 根据 Mod 关键词尝试获取实际的 Mod 名称。 + /// 若失败则返回 Nothing。 + /// + private List AnalyzeModName(List Keywords) + { + var ModFileNames = new List(); + + // 预处理关键词(分割括号) + var RealKeywords = new List(); + foreach (var Keyword in Keywords) + foreach (var SubKeyword in Keyword.Split("(")) + RealKeywords.Add(SubKeyword.Trim(" )".ToCharArray())); + Keywords = RealKeywords; + + // 从崩溃报告获取 Mod 信息 + if (LogCrash is not null && LogCrash.Contains("A detailed walkthrough of the error")) + { + var Details = LogCrash.Replace("A detailed walkthrough of the error", "¨"); + var IsFabricDetail = Details.Contains("Fabric Mods"); // 是否为 Fabric 信息格式 + if (IsFabricDetail) + { + Details = Details.Replace("Fabric Mods", "¨"); + ModBase.Log("[Crash] 崩溃报告中检测到 Fabric Mod 信息格式"); + } + + var isQuiltDetail = Details.Contains("quilt-loader"); + if (isQuiltDetail) + { + Details = Details.Replace("Mod Table Version", "¨"); + ModBase.Log("[Crash] 崩溃报告中检测到 Quilt Mod 信息格式"); + } + + Details = Details.AfterLast("¨"); + + // [Forge] 获取所有包含 .jar 的行 + // [Fabric] 获取所有包含 Mod 信息的行 + var ModNameLines = new List(); + foreach (var Line in Details.Split("\n")) + if ((Line.ContainsF(".jar", true) && Line.Length - Line.Replace(".jar", "").Length == 4) || + (IsFabricDetail && Line.StartsWithF(Constants.vbTab + Constants.vbTab) && + !Line.RegexCheck(@"\t\tfabric[\w-]*: Fabric"))) // 只有一个 .jar + ModNameLines.Add(Line); + ModBase.Log("[Crash] 崩溃报告中找到 " + ModNameLines.Count + " 个可能的 Mod 项目行"); + + // 获取 Mod ID 与关键词的匹配行 + var HintLines = new List(); + foreach (var KeyWord in Keywords) + foreach (var ModString in ModNameLines) + { + var RealModString = ModString.ToLower().Replace("_", ""); + if (!RealModString.Contains(KeyWord.ToLower().Replace("_", ""))) + continue; + if (RealModString.Contains("minecraft.jar") || RealModString.Contains(" forge-") || + RealModString.Contains(" mixin-")) + continue; + HintLines.Add(ModString.Trim("\r\n".ToCharArray())); + break; + } + + HintLines = HintLines.Distinct().ToList(); + ModBase.Log("[Crash] 崩溃报告中找到 " + HintLines.Count + " 个可能的崩溃 Mod 匹配行"); + foreach (var ModLine in HintLines) + ModBase.Log("[Crash] - " + ModLine); + + // 从 Mod 匹配行中提取 .jar 文件的名称 + foreach (var Line in HintLines) + { + string Name; + if (IsFabricDetail) + Name = Line.RegexSeek(@"(?<=: )[^\n]+(?= [^\n]+)"); + else + Name = Line.RegexSeek(@"(?<=\()[^\t]+.jar(?=\))|(?<=(\t\t)|(\| ))[^\t\|]+.jar", + RegexOptions.IgnoreCase); + if (Name is not null) + ModFileNames.Add(Name); + } + } + + // 从 debug.log 获取 Mod 信息 + if (LogMcDebug is not null) + { + // Forge: Found valid mod file YungsBetterStrongholds-1.20-Forge-4.0.1.jar with {betterstrongholds} mods - versions {1.20-Forge-4.0.1} + var ModNameLines = LogMcDebug.RegexSearch("(?<=valid mod file ).*", RegexOptions.Multiline); + ModBase.Log("[Crash] Debug 信息中找到 " + ModNameLines.Count + " 个可能的 Mod 项目行"); + + // 获取 Mod ID 与关键词的匹配行 + var HintLines = new List(); + foreach (var KeyWord in Keywords) + foreach (var ModString in ModNameLines) + if (ModString.Contains($"{{{KeyWord}}}")) + HintLines.Add(ModString); + + HintLines = HintLines.Distinct().ToList(); + ModBase.Log("[Crash] Debug 信息中找到 " + HintLines.Count + " 个可能的崩溃 Mod 匹配行"); + foreach (var ModLine in HintLines) + ModBase.Log("[Crash] - " + ModLine); + + // 从 Mod 匹配行中提取 .jar 文件的名称 + foreach (var Line in HintLines) + { + string Name; + Name = Line.RegexSeek(".*(?= with)"); + if (Name is not null) + ModFileNames.Add(Name); + } + } + + // 输出 + ModFileNames = ModFileNames.Distinct().ToList(); + if (!ModFileNames.Any()) return null; + + ModBase.Log("[Crash] 找到 " + ModFileNames.Count + " 个可能的崩溃 Mod 文件名"); + foreach (var ModFileName in ModFileNames) + ModBase.Log("[Crash] - " + ModFileName); + return ModFileNames; + } + + /// + /// 尝试从关键字获取 Mod 名称,若失败则返回原关键字。 + /// + private List TryAnalyzeModName(string Keyword) + { + var RawList = new List { Keyword ?? "" }; + if (string.IsNullOrEmpty(Keyword)) + return RawList; + return AnalyzeModName(RawList) ?? RawList; + } + + /// + /// 尝试从关键字获取 Mod 名称,若失败则返回原关键字。 + /// + private List TryAnalyzeModName(List Keywords) + { + if (!Keywords.Any()) + return Keywords; + return AnalyzeModName(Keywords) ?? Keywords; + } + + /// + /// 弹出崩溃弹窗,并指导导出崩溃报告。 + /// + public void Output(bool IsHandAnalyze, List ExtraFiles = null) + { + // 弹窗提示 + ModMain.FrmMain.ShowWindowToTop(); + var resultText = GetAnalyzeResult(IsHandAnalyze); + // 确定是否是加载器版本不兼容问题 + var isModLoaderIncompatible = _version is not null && resultText.StartsWith("Mod 加载器版本与 Mod 不兼容"); + // 弹窗选择:查看日志 + switch (ModMain.MyMsgBox(resultText, IsHandAnalyze ? "错误报告分析结果" : "Minecraft 出现错误", "确定", + IsHandAnalyze || DirectFile is null ? "" : isModLoaderIncompatible ? "前往修改" : "查看日志", + IsHandAnalyze ? "" : "导出错误报告", + Button2Action: IsHandAnalyze || DirectFile is null || isModLoaderIncompatible + ? null + : new Action(() => + { + if (File.Exists(DirectFile.Value.Key)) + { + ModBase.ShellOnly(DirectFile.Value.Key); + } + else + { + var FilePath = ModBase.PathTemp + "Crash.txt"; + ModBase.WriteFile(FilePath, DirectFile.Value.Value.Join("\r\n")); + ModBase.ShellOnly(FilePath); + } + }))) + { + case 2: + { + // 弹窗选择:前往修改 + PageInstanceLeft.Instance = _version; + ModBase.RunInUi(() => + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSetup, FormMain.PageSubType.VersionInstall)); + break; + } + case 3: + { + // 弹窗选择:导出错误报告 + string FileAddress = null; + try + { + // 获取文件路径 + ModBase.RunInUiWait(() => FileAddress = SystemDialogs.SelectSaveFile("选择保存位置", + "错误报告-" + DateTime.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") + + ".zip", "Minecraft 错误报告(*.zip)|*.zip")); + if (string.IsNullOrEmpty(FileAddress)) + return; + Directory.CreateDirectory(ModBase.GetPathFromFullPath(FileAddress)); + if (File.Exists(FileAddress)) + File.Delete(FileAddress); + // 输出诊断信息 + ModBase.FeedbackInfo(); + // 复制文件 + if (ExtraFiles is not null) + OutputFiles.AddRange(ExtraFiles); + foreach (var OutputFile in OutputFiles) + { + var FileName = ModBase.GetFileNameFromPath(OutputFile); + Encoding FileEncoding = null; + switch (FileName ?? "") + { + case "LatestLaunch.bat": + { + FileName = "启动脚本.bat"; + break; + } + case "RawOutput.log": + { + FileName = "游戏崩溃前的输出.txt"; + FileEncoding = Encoding.UTF8; + break; + } + } + + if (LogWrapper.CurrentLogger.CurrentLogFiles.Last().AfterLast(@"\") == FileName) + { + FileName = "PCL 启动器日志.txt"; + FileEncoding = Encoding.UTF8; + } + + if (File.Exists(OutputFile)) + { + if (FileEncoding is null) + FileEncoding = EncodingDetector.DetectEncoding(ModBase.ReadFileBytes(OutputFile)); + var FileContent = ModBase.ReadFile(OutputFile, FileEncoding); + FileContent = ModMinecraft.FilterAccessToken(FileContent, + Conversions.ToChar(FileName == "启动脚本.bat" ? "F" : "*")); + FileContent = ModMinecraft.FilterUserName(FileContent, '*'); + ModBase.WriteFile(TempFolder + @"Report\" + FileName, FileContent, Encoding: FileEncoding); + ModBase.Log($"[Crash] 导出文件:{FileName},编码:{FileEncoding.HeaderName}"); + } + } + + // 输出环境与启动信息 + string EnvInfo = null; + string McLauncherLog = null; + McLauncherLog = ModBase.ReadFile(TempFolder + @"Report\PCL 启动器日志.txt") + .AfterLast("[Launch] ~ 基础参数 ~").BeforeFirst("开始 Minecraft 日志监控"); + var LaunchScript = ModBase.ReadFile(TempFolder + @"Report\启动脚本.bat"); + EnvInfo += $"PCL CE 版本:{ModBase.VersionBaseName} {"\r\n"}"; + EnvInfo += $"识别码:{ModBase.UniqueAddress}{"\r\n"}"; + EnvInfo += $"{"\r\n"}- 档案信息 -{"\r\n"}"; + EnvInfo += + $"档案名称:{McLauncherLog.Between("玩家用户名:", "[").TrimEnd('[').Trim()} (验证方式:{McLauncherLog.Between("验证方式:", "[").TrimEnd('[').Trim()}){"\r\n"}"; + EnvInfo += $"{"\r\n"}- 实例信息 -{"\r\n"}"; + EnvInfo += + $"选定的 Java 虚拟机:{McLauncherLog.Between("Java 信息:", "[").TrimEnd('[').Trim()}{"\r\n"}"; + EnvInfo += + $"Log4j2 NoLookups:{!LaunchScript.ContainsF("-Dlog4j2.formatMsgNoLookups=false")}{"\r\n"}"; + EnvInfo += $"MC 文件夹:{McLauncherLog.Between("MC 文件夹:", "[").TrimEnd('[').Trim()}{"\r\n"}"; + EnvInfo += $"{"\r\n"}- 环境信息 -{"\r\n"}"; + EnvInfo += + $"操作系统:{ModSecret.OSInfo}(64 位:{!ModBase.Is32BitSystem}, ARM64: {ModBase.IsArm64System}){"\r\n"}"; + EnvInfo += $"CPU:{ModSecret.CPUName}{"\r\n"}"; + EnvInfo += + $"内存分配 (分配的内存 / 已安装物理内存):{McLauncherLog.Between("分配的内存:", "[").TrimEnd('[').Trim()} / {Math.Round(ModSecret.SystemMemorySize / 1024d, 2)} GB ({ModSecret.SystemMemorySize} MB){"\r\n"}"; + foreach (var GPU in ModSecret.GPUs) + { + EnvInfo += + $"显卡 {ModSecret.GPUs.IndexOf(GPU)}:{GPU.Name} ({(GPU.Memory >= 4095L ? ">= " + GPU.Memory : GPU.Memory)} MB, {GPU.DriverVersion})"; + EnvInfo += "\r\n"; + } + + File.CreateText(TempFolder + @"Report\环境与启动信息.txt").Close(); + ModBase.WriteFile(TempFolder + @"Report\环境与启动信息.txt", EnvInfo, Encoding: Encoding.UTF8); + // 导出报告 + ZipFile.CreateFromDirectory(TempFolder + @"Report\", FileAddress); + ModBase.DeleteDirectory(TempFolder + @"Report\"); + ModMain.Hint("错误报告已导出!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "导出错误报告失败", ModBase.LogLevel.Feedback); + return; + } + + ModBase.OpenExplorer(FileAddress); + break; + } + } + } + + /// + /// 获取崩溃分析的结果描述。 + /// + private string GetAnalyzeResult(bool IsHandAnalyze) + { + // 没有结果的处理 + if (!CrashReasons.Any()) + { + if (IsHandAnalyze) return "很抱歉,PCL 无法确定错误原因。"; + + return $"很抱歉,你的游戏出现了一些问题……{"\r\n"}如果要寻求帮助,请把错误报告文件发给对方,而不是发送这个窗口的照片或者截图。"; + } + + // 根据不同原因判断 + var Results = new List(); + const string LoaderIncompatibleResultText = @"Mod 加载器版本与 Mod 不兼容,请前往 实例设置 - 修改 更换加载器版本。\n\n详细信息:\n"; + foreach (var Reason in CrashReasons) + { + var Additional = Reason.Value; + switch (Reason.Key) + { + case CrashReason.Mod文件被解压: + { + Results.Add( + @"由于 Mod 文件被解压了,导致游戏无法继续运行。\n直接把整个 Mod 文件放进 Mod 文件夹中即可,若解压就会导致游戏出错。\n\n请删除 Mod 文件夹中已被解压的 Mod,然后再启动游戏。"); + break; + } + case CrashReason.内存不足: + { + Results.Add( + @"Minecraft 内存不足,导致其无法继续运行。\n这很可能是因为电脑内存不足、游戏分配的内存不足,或是配置要求过高。\n\n你可以尝试在 更多 → 百宝箱 中选择 内存优化,然后再启动游戏。\n如果还是不行,请在启动设置中增加为游戏分配的内存,并删除配置要求较高的材质、Mod、光影。\n如果依然不奏效,请在开始游戏前尽量关闭其他软件,或者……换台电脑?\h"); + break; + } + case CrashReason.使用OpenJ9: + { + Results.Add(@"游戏因为使用 OpenJ9 而崩溃了。\n请在启动设置的 Java 选择一项中改用非 OpenJ9 的 Java,然后再启动游戏。"); + break; + } + case CrashReason.使用JDK: + { + Results.Add( + @"游戏似乎因为使用 JDK,或 Java 版本过高而崩溃了。\n请在启动设置的 Java 选择一项中改用 JRE 8(Java 8),然后再启动游戏。\n如果你没有安装 JRE 8,你可以从网络中下载、安装一个。"); + break; + } + case CrashReason.Java版本过高: + { + Results.Add( + @"游戏似乎因为你所使用的 Java 版本过高而崩溃了。\n请在启动设置的 Java 选择一项中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从网络中下载、安装一个。"); + break; + } + case CrashReason.Java版本不兼容: + { + Results.Add(@"游戏不兼容你当前使用的 Java。\n如果没有合适的 Java,可以从网络中下载、安装一个。"); + break; + } + case CrashReason.Mod名称包含特殊字符: + { + Results.Add(@"由于有 Mod 的名称包含特殊字符,导致游戏崩溃。\n请尝试修改 Mod 文件名,让它只包含英文字母、数字、减号(-)、下划线(_)和小数点,然后再启动游戏。"); + break; + } + case CrashReason.MixinBootstrap缺失: + { + Results.Add(@"由于缺失 MixinBootstrap,导致游戏崩溃。\n请尝试安装 MixinBootstrap。若安装后依然崩溃,可以尝试在文件名前添加英文感叹号。"); + break; + } + case CrashReason.使用32位Java导致JVM无法分配足够多的内存: + { + if (Environment.Is64BitOperatingSystem) + Results.Add( + @"你似乎正在使用 32 位 Java,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n请在启动设置的 Java 选择一项中改用 64 位的 Java 再启动游戏,然后再启动游戏。\n如果你没有安装 64 位的 Java,你可以从网络中下载、安装一个。"); + else + Results.Add( + @"你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h"); + + break; + } + case CrashReason.Mod缺少前置或MC版本错误: + { + if (Additional.Any()) + { + var info = Additional.Join(@"\n - "); + if (info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)) + Results.Add(LoaderIncompatibleResultText + info); + else + Results.Add(@"由于未安装正确的前置 Mod,导致游戏退出。\n缺失的依赖项:\n - " + info + + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + } + else + { + Results.Add(@"由于未安装正确的前置 Mod,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + } + + break; + } + case CrashReason.堆栈分析发现关键字: + { + if (Additional.Count == 1) + Results.Add("你的游戏遇到了一些问题,PCL 为此找到了一个可疑的关键词:" + Additional.First() + + @"。\n\n如果你知道某个关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h"); + else + Results.Add(@"你的游戏遇到了一些问题,PCL 为此找到了以下可疑的关键词:\n - " + Additional.Join(", ") + + @"\n\n如果你知道某个关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h"); + + break; + } + case CrashReason.堆栈分析发现Mod名称: + case CrashReason.怀疑Mod导致游戏崩溃: + { + if (Additional.Count == 1) + Results.Add("PCL 怀疑名为 " + Additional.First() + + @" 的 Mod 导致了游戏出错,但不能完全确定。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + else + Results.Add(@"PCL 怀疑以下 Mod 导致了游戏出错,但不能完全确定:\n - " + Additional.Join(@"\n - ") + + @"\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + + break; + } + case CrashReason.确定Mod导致游戏崩溃: + { + if (Additional.Count == 1) + Results.Add("名为 " + Additional.First() + @" 的 Mod 导致了游戏出错。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + else + Results.Add(@"以下 Mod 导致了游戏出错:\n - " + Additional.Join(@"\n - ") + + @"\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + + break; + } + case CrashReason.ModMixin失败: + { + if (Additional.Count == 0) + Results.Add( + @"部分 Mod 注入失败,导致游戏出错。\n这一般代表着部分 Mod 与其他 Mod 或当前环境不兼容,或是它存在 Bug。\n你可以尝试逐步禁用 Mod,然后观察游戏是否还会崩溃,以此定位导致崩溃的 Mod。\n\e\h"); + else if (Additional.Count == 1) + Results.Add("名为 " + Additional.First() + + @" 的 Mod 注入失败,导致游戏出错。\n这一般代表着它与其他 Mod 或当前环境不兼容,或是它存在 Bug。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + else + Results.Add(@"以下 Mod 导致了游戏出错:\n - " + Additional.Join(@"\n - ") + + @"\n这一般代表着它们与其他 Mod 或当前环境不兼容,或是它存在 Bug。\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + + break; + } + case CrashReason.Mod配置文件导致游戏崩溃: + { + if (Additional[1] is null) + Results.Add("名为 " + Additional.First() + @" 的 Mod 导致了游戏出错。\n\e\h"); + else + Results.Add("名为 " + Additional.First() + @" 的 Mod 导致了游戏出错:\n其配置文件 " + Additional[1] + + " 存在异常,无法读取。"); + + break; + } + case CrashReason.Mod初始化失败: + { + if (Additional.Count == 1) + Results.Add("名为 " + Additional.First() + + @" 的 Mod 初始化失败,导致游戏无法继续加载。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + else + Results.Add(@"以下 Mod 初始化失败,导致游戏出错:\n - " + Additional.Join(@"\n - ") + + @"\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h"); + + break; + } + case CrashReason.特定方块导致崩溃: + { + if (Additional.Count == 1) + Results.Add("游戏似乎因为方块 " + Additional.First() + + @" 出现了问题。\n\n你可以创建一个新世界,并观察游戏的运行情况:\n - 若正常运行,则是该方块导致出错,你或许需要使用一些方式删除此方块。\n - 若仍然出错,问题就可能来自其他原因……\h"); + else + Results.Add( + @"游戏似乎因为世界中的某些方块出现了问题。\n\n你可以创建一个新世界,并观察游戏的运行情况:\n - 若正常运行,则是某些方块导致出错,你或许需要删除该世界。\n - 若仍然出错,问题就可能来自其他原因……\h"); + + break; + } + case CrashReason.Mod重复安装: + { + if (Additional.Count >= 2) + Results.Add(@"你重复安装了多个相同的 Mod:\n - " + Additional.Join(@"\n - ") + + @"\n\n每个 Mod 只能出现一次,请删除重复的 Mod,然后再启动游戏。"); + else + Results.Add(@"你可能重复安装了多个相同的 Mod,导致游戏出错。\n\n每个 Mod 只能出现一次,请删除重复的 Mod,然后再启动游戏。\e\h"); + + break; + } + case CrashReason.特定实体导致崩溃: + { + if (Additional.Count == 1) + Results.Add("游戏似乎因为实体 " + Additional.First() + + @" 出现了问题。\n\n你可以创建一个新世界,并生成一个该实体,然后观察游戏的运行情况:\n - 若正常运行,则是该实体导致出错,你或许需要使用一些方式删除此实体。\n - 若仍然出错,问题就可能来自其他原因……\h"); + else + Results.Add( + @"游戏似乎因为世界中的某些实体出现了问题。\n\n你可以创建一个新世界,并生成各种实体,观察游戏的运行情况:\n - 若正常运行,则是某些实体导致出错,你或许需要删除该世界。\n - 若仍然出错,问题就可能来自其他原因……\h"); + + break; + } + case CrashReason.OptiFine与Forge不兼容: + { + Results.Add( + @"由于 OptiFine 与当前版本的 Forge 不兼容,导致了游戏崩溃。\n\n请前往 OptiFine 官网(https://optifine.net/downloads)查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏。"); + break; + } + case CrashReason.ShadersMod与OptiFine同时安装: + { + Results.Add( + @"无需同时安装 OptiFine 和 Shaders Mod,OptiFine 已经集成了 Shaders Mod 的功能。\n在删除 Shaders Mod 后,游戏即可正常运行。"); + break; + } + case CrashReason.低版本Forge与高版本Java不兼容: + { + Results.Add( + @"由于低版本 Forge 与当前 Java 不兼容,导致了游戏崩溃。\n\n请尝试以下解决方案:\n - 更新 Forge 到 36.2.26 或更高版本\n - 换用版本低于 1.8.0.320 的 Java"); + break; + } + case CrashReason.实例Json中存在多个Forge: + { + Results.Add(@"可能由于其他启动器修改了 Forge 版本,当前实例的文件存在异常,导致了游戏崩溃。\n请尝试重新全新安装 Forge,而非使用其他启动器修改 Forge 版本。"); + break; + } + case CrashReason.玩家手动触发调试崩溃: + { + Results.Add(@"* 事实上,你的游戏没有任何问题,这是你自己触发的崩溃。\n* 你难道没有更重要的事要做吗?"); + break; + } + case CrashReason.Mod需要Java11: + { + Results.Add( + @"你所安装的部分 Mod 似乎需要使用 Java 11 启动。\n请在启动设置的 Java 选择一项中改用 Java 11,然后再启动游戏。\n如果你没有安装 Java 11,你可以从网络中下载、安装一个。"); + break; + } + case CrashReason.极短的程序输出: + { + Results.Add($@"程序返回了以下信息:\n{Additional.First()}\n\h"); + break; + } + case CrashReason.OptiFine导致无法加载世界 + : // https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world + { + Results.Add(@"你所使用的 OptiFine 可能导致了你的游戏出现问题。\n\n该问题只在特定 OptiFine 版本中出现,你可以尝试更换 OptiFine 的版本。\h"); + break; + } + case CrashReason.显卡驱动不支持导致无法设置像素格式: + case CrashReason.Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION: + case CrashReason.AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION: + case CrashReason.Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION: + case CrashReason.显卡不支持OpenGL: + { + if (LogAll.Contains("hd graphics ")) + Results.Add( + @"你的显卡驱动存在问题,或未使用独立显卡,导致游戏无法正常运行。\n\n如果你的电脑存在独立显卡,请使用独立显卡而非 Intel 核显启动 PCL 与 Minecraft。\n如果问题依然存在,请尝试升级你的显卡驱动到最新版本,或回退到出厂版本。\n如果还是不行,还可以尝试使用 8.0.51 或更低版本的 Java。\h"); + else + Results.Add( + @"你的显卡驱动存在问题,导致游戏无法正常运行。\n\n请尝试升级你的显卡驱动到最新版本,或回退到出厂版本,然后再启动游戏。\n如果还是不行,可以尝试使用 8.0.51 或更低版本的 Java。\n如果问题依然存在,那么你可能需要换个更好的显卡……\h"); + + break; + } + case CrashReason.材质过大或显卡配置不足: + { + Results.Add( + @"你所使用的材质分辨率过高,或显卡配置不足,导致游戏无法继续运行。\n\n如果你正在使用高清材质,请将它移除。\n如果你没有使用材质,那么你可能需要更新显卡驱动,或者换个更好的显卡……\h"); + break; + } + case CrashReason.NightConfig的Bug: + { + Results.Add(@"由于 Night Config 存在问题,导致了游戏崩溃。\n你可以尝试安装 Night Config Fixes 模组,这或许能解决此问题。\h"); + break; + } + case CrashReason.光影或资源包导致OpenGL1282错误: + { + Results.Add(@"你所使用的光影或材质导致游戏出现了一些问题……\n\n请尝试删除你所添加的这些额外资源。\h"); + break; + } + case CrashReason.Mod过多导致超出ID限制: + { + Results.Add(@"你所安装的 Mod 过多,超出了游戏的 ID 限制,导致了游戏崩溃。\n请尝试安装 JEID 等修复 Mod,或删除部分大型 Mod。"); + break; + } + case CrashReason.文件或内容校验失败: + { + Results.Add(@"部分文件或内容校验失败,导致游戏出现了问题。\n\n请尝试删除游戏(包括 Mod)并重新下载,或尝试在重新下载时使用 VPN。\h"); + break; + } + case CrashReason.Forge安装不完整: + { + Results.Add( + @"由于安装的 Forge 文件丢失,导致游戏无法正常运行。\n请前往实例设置重置该实例,然后再启动游戏。\n在打包游戏时删除 libraries 文件夹可能导致此错误。\h"); + break; + } + case CrashReason.Fabric报错: + { + if (Additional.Count == 1) + Results.Add(@"Fabric 提供了以下错误信息:\n" + Additional.First() + + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + else + Results.Add(@"Fabric 可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + + break; + } + case CrashReason.Mod互不兼容: + { + if (Additional.Count == 1) + { + var info = Additional.First(); + if (info.IsMatch(RegexPatterns.IncompatibleModLoaderErrorHint)) + Results.Add(LoaderIncompatibleResultText + info); + else + Results.Add(@"你所安装的 Mod 不兼容:\n" + info + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + } + else + { + Results.Add(@"你所安装的 Mod 不兼容,Mod 加载器可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + } + + break; + } + case CrashReason.Mod加载器报错: + { + if (Additional.Count == 1) + Results.Add(@"Mod 加载器提供了以下错误信息:\n" + Additional.First() + + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + else + Results.Add(@"Mod 加载器可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + + break; + } + case CrashReason.Fabric报错并给出解决方案: + { + if (Additional.Count == 1) + Results.Add(@"Fabric 提供了以下解决方案:\n" + Additional.First() + + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + else + Results.Add(@"Fabric 可能已经提供了解决方案,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + + break; + } + case CrashReason.Forge报错: + { + if (Additional.Count == 1) + Results.Add(@"Forge 提供了以下错误信息:\n" + Additional.First() + @"\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。"); + else + Results.Add(@"Forge 可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h"); + + break; + } + case CrashReason.没有可用的分析文件: + { + Results.Add(@"你的游戏出现了一些问题,但 PCL 未能找到相关记录文件,因此无法进行分析。\h"); + break; + } + + default: + { + Results.Add("PCL 获取到了没有详细信息的错误原因(" + (int)CrashReasons.First().Key + @"),请向 PCL 作者提交反馈以获取详情。\h"); + break; + } + } + } + + var isLauncherLatest = false; + try + { + isLauncherLatest = ModSecret.GetVersionStatus() == ModSecret.VersionStatus.Latest; + } + catch (Exception ex) + { + ModBase.Log(ex, "确认启动器更新失败", ModBase.LogLevel.Feedback); + } + + return Results.Join(@"\n\n此外,").Replace(@"\n", "\r\n").Replace(@"\h", "") + .Replace(@"\e", IsHandAnalyze ? "" : "\r\n" + "你可以查看错误报告了解错误具体是如何发生的。") + .Replace("\r\n", "\r").Replace("\n", "\r") + .Replace("\r", "\r\n").Trim("\r\n".ToCharArray()) + + (!Results.Any(r => r.EndsWithF(@"\h")) || IsHandAnalyze + ? "" + : "\r\n" + "如果要寻求帮助,请把错误报告文件发给对方,而不是发送这个窗口的照片或者截图。" + (isLauncherLatest + ? "" + : "\r\n" + "\r\n" + "此外,你正在使用老版本 PCL,更新 PCL 或许也能解决这个问题。" + "\r\n" + + "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。")); + } + + // 2:确认实际用于分析的 Log 文本 + private enum AnalyzeFileType + { + HsErr, + MinecraftLog, + ExtraLogFile, + ExtraReportFile, + CrashReport + } + + /// + /// 导致崩溃的原因枚举。 + /// + private enum CrashReason + { + Mod文件被解压, + MixinBootstrap缺失, + 内存不足, + 使用JDK, + 显卡不支持OpenGL, + 使用OpenJ9, + Java版本过高, + Java版本不兼容, + Mod名称包含特殊字符, + 显卡驱动不支持导致无法设置像素格式, + 极短的程序输出, + Intel驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, // https://bugs.mojang.com/browse/MC-32606 + AMD驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, // https://bugs.mojang.com/browse/MC-31618 + Nvidia驱动不兼容导致EXCEPTION_ACCESS_VIOLATION, + 玩家手动触发调试崩溃, + 光影或资源包导致OpenGL1282错误, + 文件或内容校验失败, + 确定Mod导致游戏崩溃, + 怀疑Mod导致游戏崩溃, + Mod配置文件导致游戏崩溃, + ModMixin失败, + Mod加载器报错, + Mod初始化失败, + 堆栈分析发现关键字, + 堆栈分析发现Mod名称, + OptiFine导致无法加载世界, // https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world + 特定方块导致崩溃, + 特定实体导致崩溃, + 材质过大或显卡配置不足, + 没有可用的分析文件, + 使用32位Java导致JVM无法分配足够多的内存, + Mod重复安装, + Mod互不兼容, + OptiFine与Forge不兼容, + Fabric报错, + Fabric报错并给出解决方案, + Forge报错, + 低版本Forge与高版本Java不兼容, + 实例Json中存在多个Forge, + Mod过多导致超出ID限制, + NightConfig的Bug, + ShadersMod与OptiFine同时安装, + Forge安装不完整, + Mod需要Java11, + Mod缺少前置或MC版本错误 + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.cs new file mode 100644 index 000000000..0f1e4faea --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.cs @@ -0,0 +1,2472 @@ +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Utils; + +namespace PCL; + +public static class ModDownload +{ + #region DlClient* | Minecraft 客户端 + + /// + /// 返回某 Minecraft 版本对应的原版主 Jar 文件的下载信息,要求对应依赖实例已存在。 + /// 失败则抛出异常,不需要下载则返回 Nothing。 + /// + public static ModNet.NetFile DlClientJarGet(ModMinecraft.McInstance Version, bool ReturnNothingOnFileUseable) + { + // 获取底层继承实例 + try + { + while (!string.IsNullOrEmpty(Version.InheritInstanceName)) + Version = new ModMinecraft.McInstance(Version.InheritInstanceName); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取底层继承实例失败"); + } + + // 检查 Json 是否标准 + if (Version.JsonObject["downloads"] is null || Version.JsonObject["downloads"]["client"] is null || + Version.JsonObject["downloads"]["client"]["url"] is null) + throw new Exception("底层实例 " + Version.Name + " 中无 Jar 文件下载信息"); + // 检查文件 + var Checker = new ModBase.FileChecker(1024L, (long)(Version.JsonObject["downloads"]["client"]["size"] ?? -1), + (string)Version.JsonObject["downloads"]["client"]["sha1"]); + if (ReturnNothingOnFileUseable && Checker.Check(Version.PathInstance + Version.Name + ".jar") is null) + return null; // 通过校验 + // 返回下载信息 + var JarUrl = (string)Version.JsonObject["downloads"]["client"]["url"]; + return new ModNet.NetFile(DlSourceLauncherOrMetaGet(JarUrl), Version.PathInstance + Version.Name + ".jar", + Checker); + } + + /// + /// 返回某 Minecraft 版本对应的原版主 AssetIndex 文件的下载信息,要求对应依赖实例已存在。 + /// 若未找到,则会返回 Legacy 资源文件或 Nothing。 + /// + public static ModNet.NetFile DlClientAssetIndexGet(ModMinecraft.McInstance Version) + { + // 获取底层继承实例 + while (!string.IsNullOrEmpty(Version.InheritInstanceName)) + Version = new ModMinecraft.McInstance(Version.InheritInstanceName); + // 获取信息 + var IndexInfo = ModMinecraft.McAssetsGetIndex(Version, true, true); + var IndexAddress = ModMinecraft.McFolderSelected + @"assets\indexes\" + IndexInfo["id"] + ".json"; + ModBase.Log("[Download] 实例 " + Version.Name + " 对应的资源文件索引为 " + IndexInfo["id"]); + var IndexUrl = (string)(IndexInfo["url"] ?? ""); + if (string.IsNullOrEmpty(IndexUrl)) return null; + + return new ModNet.NetFile(DlSourceLauncherOrMetaGet(IndexUrl), IndexAddress, + new ModBase.FileChecker(CanUseExistsFile: false, IsJson: true)); + } + + /// + /// 构造补全某 Minecraft 版本的所有文件的加载器列表。失败会抛出异常。 + /// + public static List DlClientFix(ModMinecraft.McInstance Version, bool CheckAssetsHash, + AssetsIndexExistsBehaviour AssetsIndexBehaviour) + { + var Loaders = new List(); + + #region 下载支持库文件 + + if (Conversions.ToBoolean(ModMinecraft.ShouldIgnoreFileCheck(Version))) + { + ModBase.Log("[Download] 已跳过所有 Libraries 检查"); + } + else + { + var LoadersLib = new List + { + new ModLoader.LoaderTask>("分析缺失支持库文件", + Task => Task.Output = ModMinecraft.McLibNetFilesFromInstance(Version)) { ProgressWeight = 1d }, + new ModNet.LoaderDownload("下载支持库文件", new List()) { ProgressWeight = 15d } + }; + // 构造加载器 + Loaders.Add(new ModLoader.LoaderCombo("下载支持库文件(主加载器)", LoadersLib) + { Block = false, Show = false, ProgressWeight = 16d }); + } + + #endregion + + #region 下载资源文件 + + if (Conversions.ToBoolean(ModMinecraft.ShouldIgnoreFileCheck(Version))) + { + ModBase.Log("[Download] 已跳过所有 Assets 检查"); + } + else + { + var LoadersAssets = new List(); + // 获取资源文件索引地址 + LoadersAssets.Add(new ModLoader.LoaderTask>("分析资源文件索引地址", Task => + { + try + { + var IndexFile = DlClientAssetIndexGet(Version); + var IndexFileInfo = new FileInfo(IndexFile.LocalPath); + if (AssetsIndexBehaviour != AssetsIndexExistsBehaviour.AlwaysDownload && + IndexFile.Check.Check(IndexFile.LocalPath) is null) + Task.Output = new List(); + else + Task.Output = new List { IndexFile }; + } + catch (Exception ex) + { + throw new Exception("分析资源文件索引地址失败", ex); + } + }) { ProgressWeight = 0.5d, Show = false }); + // 下载资源文件索引 + LoadersAssets.Add(new ModNet.LoaderDownload("下载资源文件索引", new List()) + { ProgressWeight = 2d }); + // 要求独立更新索引 + if (AssetsIndexBehaviour == AssetsIndexExistsBehaviour.DownloadInBackground) + { + var LoadersAssetsUpdate = new List(); + string TempAddress = null; + string RealAddress = null; + LoadersAssetsUpdate.Add(new ModLoader.LoaderTask>("后台分析资源文件索引地址", Task => + { + var BackAssetsFile = DlClientAssetIndexGet(Version); + RealAddress = BackAssetsFile.LocalPath; + TempAddress = ModBase.PathTemp + @"Cache\" + BackAssetsFile.LocalName; + BackAssetsFile.LocalPath = TempAddress; + Task.Output = new List { BackAssetsFile }; + // 检查是否需要更新:每天只更新一次 + if (File.Exists(RealAddress) && + Math.Abs((File.GetLastWriteTime(RealAddress).Date - DateTime.Now.Date).TotalDays) < 1d) + { + ModBase.Log("[Download] 无需更新资源文件索引,取消"); + Task.Abort(); + } + })); + LoadersAssetsUpdate.Add(new ModNet.LoaderDownload("后台下载资源文件索引", new List())); + LoadersAssetsUpdate.Add(new ModLoader.LoaderTask, string>("后台复制资源文件索引", Task => + { + ModBase.CopyFile(TempAddress, RealAddress); + ModLaunch.McLaunchLog("后台更新资源文件索引成功:" + TempAddress); + })); + var Updater = new ModLoader.LoaderCombo("后台更新资源文件索引", LoadersAssetsUpdate); + ModBase.Log("[Download] 开始后台检查资源文件索引"); + Updater.Start(); + } + + // 获取资源文件地址 + LoadersAssets.Add(new ModLoader.LoaderTask>("分析缺失资源文件", Task => + { + ModLoader.LoaderBase argprogressFeed = Task; + Task.Output = ModMinecraft.McAssetsFixList(Version, CheckAssetsHash, ref argprogressFeed); + Task = (ModLoader.LoaderTask>)argprogressFeed; + }) + { + ProgressWeight = 3d + }); + // 下载资源文件 + LoadersAssets.Add(new ModNet.LoaderDownload("下载资源文件", new List()) { ProgressWeight = 25d }); + // 构造加载器 + Loaders.Add(new ModLoader.LoaderCombo("下载资源文件(主加载器)", LoadersAssets) + { Block = false, Show = false, ProgressWeight = 30.5d }); + } + + #endregion + + return Loaders; + } + + public enum AssetsIndexExistsBehaviour + { + /// + /// 如果文件存在,则不进行下载。 + /// + DontDownload, + + /// + /// 如果文件存在,则启动新的下载加载器进行独立的更新。 + /// + DownloadInBackground, + + /// + /// 如果文件存在,也同样进行下载。 + /// + AlwaysDownload + } + + #endregion + + #region DlClientList | Minecraft 客户端 版本列表 + + /// + /// 所有正式版的 Minecraft Drop 序数。 + /// 若从未完成过获取,返回 Nothing;否则必定存在元素,且从高到低排列。 + /// + public static List AllDrops + { + get + { + lock (_allDropsLock) + { + if (_allDrops is null) + { + var rawData = States.Game.Drops; + if (string.IsNullOrEmpty(rawData)) + _allDrops = new List(); + else + _allDrops = rawData.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(d => (int)Math.Round(ModBase.Val(d))).ToList(); + } + + return _allDrops.Count != 0 ? _allDrops : null; + } + } + set + { + lock (_allDropsLock) + { + _allDrops = value; + States.Game.Drops = value.Join(","); + } + } + } + + private static List _allDrops; + private static readonly object _allDropsLock = new(); + + // 主加载器 + public struct DlClientListResult + { + /// + /// 数据来源名称,如“Mojang”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的 Json 数据。 + /// + public JObject Value; + // ''' + // ''' 官方源的失败原因。若没有则为 Nothing。 + // ''' + // Public OfficialError As Exception + } + + /// + /// Minecraft 客户端 版本列表,主加载器。 + /// 若要求镜像源必须包含某个版本,则将该版本 ID 作为输入(#5195)。 + /// + public static ModLoader.LoaderTask DlClientListLoader = + new("DlClientList Main", DlClientListMain); + + private static void DlClientListMain(ModLoader.LoaderTask loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(loader, + new List, int>> + { new(DlClientListBmclapiLoader, 30), new(DlClientListMojangLoader, 30 + 60) }, + loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(loader, + new List, int>> + { new(DlClientListMojangLoader, 5), new(DlClientListBmclapiLoader, 5 + 30) }, + loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(loader, + new List, int>> + { new(DlClientListMojangLoader, 60), new(DlClientListBmclapiLoader, 60 + 60) }, + loader.IsForceRestarting); + break; + } + } + + // 提取所有 Drop 序数 + var drops = new List(); + foreach (JObject version in loader.Output.Value["versions"]) + drops.Add(ModMinecraft.McInstanceInfo.VersionToDrop((string)version["id"])); + AllDrops = drops.Distinct().OrderByDescending(d => d).ToList(); + } + + // 各个下载源的分加载器 + /// + /// Minecraft 客户端 版本列表,Mojang 官方源加载器。 + /// + public static ModLoader.LoaderTask DlClientListMojangLoader = + new("DlClientList Mojang", DlClientListMojangMain); + + private static bool IsNewClientVersionHinted = false; + + // MC 更新提示 + private static bool _DlClientListMojangMain_IsHinted; + + private static void DlClientListMojangMain(ModLoader.LoaderTask Loader) + { + var StartTime = TimeUtils.GetTimeTick(); + var Json = (JObject)ModNet.NetGetCodeByRequestRetry( + "https://launchermeta.mojang.com/mc/game/version_manifest.json", IsJson: true); + try + { + var Versions = (JArray)Json["versions"]; + if (Versions.Count < 200) + throw new Exception("获取到的版本列表长度不足(" + Json + ")"); + // 添加 UVMC 项 + var CacheFilePath = ModBase.PathTemp + @"Cache\uvmc-download.json"; + if (!File.Exists(CacheFilePath)) + try + { + var UnlistedJson = (JObject)ModNet.NetGetCodeByRequestRetry( + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto/version_manifest.json", + IsJson: true); + File.WriteAllText(CacheFilePath, UnlistedJson.ToString()); + } + catch (Exception ex) + { + ModBase.Log("[Download] 未列出的版本官方源下载失败: " + ex.Message); + } + + try + { + var CachedJson = (JObject)ModBase.GetJson(ModBase.ReadFile(CacheFilePath)); + Versions.Merge(CachedJson["versions"]); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Download] UVMC 列表加载失败,忽略列表内容"); + } + + // 确定官方源是否可用 + if (!DlPreferMojang) + { + var DeltaTime = TimeUtils.GetTimeTick() - StartTime; + DlPreferMojang = DeltaTime < 4000; + ModBase.Log($"[Download] Mojang 官方源加载耗时:{DeltaTime}ms,{(DlPreferMojang ? "可优先使用官方源" : "不优先使用官方源")}"); + } + + // 添加 PCL 特供项 + // 这个社区版下不了 + // If File.Exists(PathTemp & "Cache\download.json") Then Versions.Merge(GetJson(ReadFile(PathTemp & "Cache\download.json"))) + // 返回 + Loader.Output = new DlClientListResult { IsOfficial = true, SourceName = "Mojang 官方源", Value = Json }; + string Version; + // 快照版 + Version = (string)Json["latest"]["snapshot"]; + if (Conversions.ToBoolean((bool)Config.Tool.SnapshotNotification && + !Operators.ConditionalCompareObjectEqual( + States.Tool.LastSnapshot, "", false) && + Operators.ConditionalCompareObjectNotEqual( + States.Tool.LastSnapshot, Version, false) && + !_DlClientListMojangMain_IsHinted)) + { + _DlClientListMojangMain_IsHinted = true; + ModMinecraft.McDownloadClientUpdateHint(Version, Json); + } + + States.Tool.LastSnapshot = Version ?? "Nothing"; + // 正式版 + Version = (string)Json["latest"]["release"]; + if (Conversions.ToBoolean((bool)Config.Tool.ReleaseNotification && + !Operators.ConditionalCompareObjectEqual( + States.Tool.LastRelease, "", false) && + Operators.ConditionalCompareObjectNotEqual( + States.Tool.LastRelease, Version, false) && + !_DlClientListMojangMain_IsHinted)) + { + _DlClientListMojangMain_IsHinted = true; + ModMinecraft.McDownloadClientUpdateHint(Version, Json); + } + + States.Tool.LastRelease = Version; + } + catch (Exception ex) + { + throw new Exception("Minecraft 官方源版本列表解析失败", ex); + } + } + + /// + /// Minecraft 客户端 版本列表,BMCLAPI 源加载器。 + /// + public static ModLoader.LoaderTask DlClientListBmclapiLoader = + new("DlClientList Bmclapi", DlClientListBmclapiMain); + + private static void DlClientListBmclapiMain(ModLoader.LoaderTask Loader) + { + var Json = (JObject)ModNet.NetGetCodeByRequestRetry( + "https://bmclapi2.bangbang93.com/mc/game/version_manifest.json", IsJson: true); + try + { + var Versions = (JArray)Json["versions"]; + if (Versions.Count < 200) + throw new Exception("获取到的版本列表长度不足(" + Json + ")"); + // 添加 UVMC 项 + var CacheFilePath = ModBase.PathTemp + @"Cache\uvmc-download.json"; + if (!File.Exists(CacheFilePath)) + try + { + var UnlistedJson = (JObject)ModNet.NetGetCodeByRequestRetry( + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto/version_manifest.json", + IsJson: true); + File.WriteAllText(CacheFilePath, UnlistedJson.ToString()); + } + catch (Exception ex) + { + ModBase.Log("[Download] 未列出的版本镜像源下载失败: " + ex.Message); + } + + try + { + var CachedJson = (JObject)ModBase.GetJson(ModBase.ReadFile(CacheFilePath)); + Versions.Merge(CachedJson["versions"]); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Download] UVMC 列表加载失败,忽略列表内容"); + } + + // 检查是否有要求的版本(#5195) + if (!string.IsNullOrEmpty(Loader.Input)) + { + var Id = Loader.Input; + if (DlClientListLoader.Output.Value is not null && + !DlClientListLoader.Output.Value["versions"].Any(v => (string)v["id"] == Id)) + throw new Exception("BMCLAPI 源未包含目标版本 " + Id); + } + + // 返回 + Loader.Output = new DlClientListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = Json }; + } + catch (Exception ex) + { + throw new Exception("Minecraft BMCLAPI 版本列表解析失败(" + Json + ")", ex); + } + } + + /// + /// 获取某个版本的 Json 下载地址,若失败则返回 Nothing。必须在工作线程执行。 + /// + public static object DlClientListGet(string Id) + { + try + { + // 确认版本格式标准 + Id = Id.Replace("_", "-"); // 1.7.10_pre4 在版本列表中显示为 1.7.10-pre4 + if (Id != "1.0" && Id.EndsWithF(".0")) + Id = Strings.Left(Id, Id.Length - 2); // OptiFine 1.8 的下载会触发此问题,显示版本为 1.8.0 + // 获取 Minecraft 版本列表 + switch (DlClientListLoader.State) + { + case ModBase.LoadState.Finished: + { + // 从当前的结果获取目标版本… + foreach (JObject Version in DlClientListLoader.Output.Value["versions"]) + if ((string)Version["id"] == Id) + return Version["url"].ToString(); + // …如果没有,则重新尝试获取(在版本刚更新时可能出现这种情况,#5195) + DlClientListLoader.WaitForExit(Id, IsForceRestart: true); + break; + } + case ModBase.LoadState.Loading: + { + DlClientListLoader.WaitForExit(Id); + break; + } + case ModBase.LoadState.Failed: + case ModBase.LoadState.Aborted: + case ModBase.LoadState.Waiting: + { + DlClientListLoader.WaitForExit(Id, IsForceRestart: true); + break; + } + } + + // 重新查找版本 + foreach (JObject Version in DlClientListLoader.Output.Value["versions"]) + if ((string)Version["id"] == Id) + return Version["url"].ToString(); + ModBase.Log($"未发现版本 {Id} 的 json 下载地址,版本列表返回为:{"\r\n"}{DlClientListLoader.Output.Value}", + ModBase.LogLevel.Debug); + return null; + } + catch (Exception ex) + { + ModBase.Log(ex, $"获取版本 {Id} 的 json 下载地址失败"); + return null; + } + } + + #endregion + + #region DlOptiFineList | OptiFine 版本列表 + + public struct DlOptiFineListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public List Value; + } + + public class DlOptiFineListEntry + { + private string _inherit; + + /// + /// 显示名称,已去除 HD_U 字样,如“1.12.2 C8”。 + /// + public string DisplayName; + + /// + /// 是否为测试版。 + /// + public bool IsPreview; + + /// + /// 原始文件名称,如“preview_OptiFine_1.11_HD_U_E1_pre.jar”。 + /// + public string NameFile; + + /// + /// 对应的版本名称,如“1.13.2-OptiFine_HD_U_E6”。 + /// + public string NameVersion; + + /// + /// 发布时间,格式为“yyyy/mm/dd”。OptiFine 源无此数据。 + /// + public string ReleaseTime; + + /// + /// 需要的最低 Forge 版本。空字符串为无限制,Nothing 为不兼容,“28.1.56” 表示版本号,“1161” 表示版本号的最后一位。 + /// + public string RequiredForgeVersion; + + /// + /// 对应的 Minecraft 版本,如“1.12.2”。 + /// + public string Inherit + { + get => _inherit; + set + { + if (value.EndsWithF(".0")) + value = Strings.Left(value, value.Length - 2); + _inherit = value; + } + } + } + + /// + /// OptiFine 版本列表,主加载器。 + /// + public static ModLoader.LoaderTask DlOptiFineListLoader = + new("DlOptiFineList Main", DlOptiFineListMain); + + private static void DlOptiFineListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlOptiFineListBmclapiLoader, 30), new(DlOptiFineListOfficialLoader, 30 + 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlOptiFineListOfficialLoader, 5), new(DlOptiFineListBmclapiLoader, 5 + 30) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlOptiFineListOfficialLoader, 60), new(DlOptiFineListBmclapiLoader, 60 + 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// OptiFine 版本列表,官方源。 + /// + public static ModLoader.LoaderTask DlOptiFineListOfficialLoader = + new("DlOptiFineList Official", DlOptiFineListOfficialMain); + + private static void DlOptiFineListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = HttpRequestBuilder.Create("https://optifine.net/downloads", HttpMethod.Get) + .WithHeader("Accept", "application/json, text/javascript, */*; q=0.01") + .WithHeader("Accept-Language", "en-US,en;q=0.5").WithHeader("X-Requested-With", "XMLHttpRequest") + .SendAsync(true).GetAwaiter().GetResult().AsStringContent(); + if (Result.Length < 200) + throw new Exception("获取到的版本列表长度不足(" + Result + ")"); + try + { + // 获取所有版本信息 + var Forge = Result.RegexSearch("(?<=colForge'>)[^<]*"); + var ReleaseTime = Result.RegexSearch("(?<=colDate'>)[^<]+"); + var Name = Result.RegexSearch("(?<=OptiFine_)[0-9A-Za-z_.]+(?=.jar\")"); + if (!(ReleaseTime.Count == Name.Count)) + throw new Exception("版本与发布时间数据无法对应"); + if (!(Forge.Count == Name.Count)) + throw new Exception("版本与 Forge 兼容数据无法对应"); + if (ReleaseTime.Count < 10) + throw new Exception("获取到的版本数量不足(" + Result + ")"); + // 转化为列表输出 + var Versions = new List(); + for (int i = 0, loopTo = ReleaseTime.Count - 1; i <= loopTo; i++) + { + Name[i] = Name[i].Replace("_", " "); + var Entry = new DlOptiFineListEntry + { + DisplayName = Name[i].Replace("HD U ", "").Replace(".0 ", " "), + ReleaseTime = new[] + { ReleaseTime[i].Split(".")[2], ReleaseTime[i].Split(".")[1], ReleaseTime[i].Split(".")[0] } + .Join("/"), + IsPreview = Name[i].ContainsF("pre", true), + Inherit = Name[i].Split(" ")[0], + NameFile = (Name[i].ContainsF("pre", true) ? "preview_" : "") + "OptiFine_" + + Name[i].Replace(" ", "_") + ".jar", + RequiredForgeVersion = Forge[i].Replace("Forge ", "").Replace("#", "") + }; + if (Entry.RequiredForgeVersion.Contains("N/A")) + Entry.RequiredForgeVersion = null; + Entry.NameVersion = Entry.Inherit + "-OptiFine_" + + Name[i].Replace(" ", "_").Replace(Entry.Inherit + "_", ""); + Versions.Add(Entry); + } + + Loader.Output = new DlOptiFineListResult + { IsOfficial = true, SourceName = "OptiFine 官方源", Value = Versions }; + } + catch (Exception ex) + { + throw new Exception("OptiFine 官方源版本列表解析失败(" + Result + ")", ex); + } + } + + /// + /// OptiFine 版本列表,BMCLAPI。 + /// + public static ModLoader.LoaderTask DlOptiFineListBmclapiLoader = + new("DlOptiFineList Bmclapi", DlOptiFineListBmclapiMain); + + private static void DlOptiFineListBmclapiMain(ModLoader.LoaderTask Loader) + { + var Json = (JArray)ModNet.NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/optifine/versionList", + IsJson: true); + try + { + var Versions = new List(); + foreach (JObject Token in Json) + { + var Entry = new DlOptiFineListEntry + { + DisplayName = + (Token["mcversion"] + Token["type"].ToString().Replace("HD_U", "").Replace("_", " ") + " " + + Token["patch"]).Replace(".0 ", " "), + ReleaseTime = "", + IsPreview = Token["patch"].ToString().ContainsF("pre", true), + Inherit = Token["mcversion"].ToString(), + NameFile = Token["filename"].ToString(), + RequiredForgeVersion = (Token["forge"] ?? "").ToString().Replace("Forge ", "").Replace("#", "") + }; + if (Entry.RequiredForgeVersion.Contains("N/A")) + Entry.RequiredForgeVersion = null; + Entry.NameVersion = Entry.Inherit + "-OptiFine_" + (Token["type"] + " " + Token["patch"]) + .Replace(".0 ", " ").Replace(" ", "_").Replace(Entry.Inherit + "_", ""); + Versions.Add(Entry); + } + + Loader.Output = new DlOptiFineListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = Versions }; + } + catch (Exception ex) + { + throw new Exception("OptiFine BMCLAPI 版本列表解析失败(" + Json + ")", ex); + } + } + + #endregion + + #region DlForgeList | Forge Minecraft 版本列表 + + public struct DlForgeListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public List Value; + } + + /// + /// Forge 版本列表,主加载器。 + /// + public static ModLoader.LoaderTask DlForgeListLoader = + new("DlForgeList Main", DlForgeListMain); + + private static void DlForgeListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlForgeListBmclapiLoader, 30), new(DlForgeListOfficialLoader, 30 + 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlForgeListOfficialLoader, 5), new(DlForgeListBmclapiLoader, 5 + 30) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlForgeListOfficialLoader, 60), new(DlForgeListBmclapiLoader, 60 + 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// Forge 版本列表,官方源。 + /// + public static ModLoader.LoaderTask DlForgeListOfficialLoader = + new("DlForgeList Official", DlForgeListOfficialMain); + + private static void DlForgeListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = Conversions.ToString(ModNet.NetGetCodeByRequestRetry( + "https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.2.4.html", Encoding.Default, + "text/html", UseBrowserUserAgent: true)); + if (Result.Length < 200) + throw new Exception("获取到的版本列表长度不足(" + Result + ")"); + // 获取所有版本信息 + var Names = Result.RegexSearch("(?<=a href=\"index_)[0-9.]+(_pre[0-9]?)?(?=.html)"); + Names.Add("1.2.4"); // 1.2.4 不会被匹配上 + if (Names.Count < 10) + throw new Exception("获取到的版本数量不足(" + Result + ")"); + Loader.Output = new DlForgeListResult { IsOfficial = true, SourceName = "Forge 官方源", Value = Names }; + } + + /// + /// Forge 版本列表,BMCLAPI。 + /// + public static ModLoader.LoaderTask DlForgeListBmclapiLoader = + new("DlForgeList Bmclapi", DlForgeListBmclapiMain); + + private static void DlForgeListBmclapiMain(ModLoader.LoaderTask Loader) + { + var Result = + Conversions.ToString(ModNet.NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/forge/minecraft", + Encoding.Default)); + if (Result.Length < 200) + throw new Exception("获取到的版本列表长度不足(" + Result + ")"); + // 获取所有版本信息 + var Names = Result.RegexSearch("[0-9.]+(_pre[0-9]?)?"); + if (Names.Count < 10) + throw new Exception("获取到的版本数量不足(" + Result + ")"); + Loader.Output = new DlForgeListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = Names }; + } + + #endregion + + #region DlForgeVersion | Forge 版本列表 + + public abstract class DlForgelikeEntry : IComparable + { + public enum ForgelikeType + { + Forge, + NeoForge, + Cleanroom + } + + /// + /// Forgelike 种类。Forge、NeoForge、Cleanroom。 + /// + public ForgelikeType ForgeType; + + /// + /// 对应的 Minecraft 版本,如“1.12.2”。 + /// + public string Inherit; + + /// + /// 标准化后的版本号,仅可用于比较与排序。 + /// 格式:Major.Minor.Build.Revision + /// Forge:如 “50.1.9.0”(最后一位固定为 0)、“14.22.1.2478”(Legacy)。 + /// NeoForge:如 “20.4.30.0”(最后一位固定为 0)、“19.47.1.99”(Legacy:第一位固定为 19)。 + /// Cleanroom:如 “0.2.4.1”(Alpha:最后一位固定为 1)。 + /// + public Version Version; + + /// + /// 可对玩家显示的非格式化版本名。 + /// Forge:如 “50.1.9”、“14.22.1.2478”(Legacy)。 + /// NeoForge:如 “20.4.30-beta”、“47.1.99”(Legacy)。 + /// Cleanroom:如 “0.2.4-alpha”。 + /// + public string VersionName; + + /// + /// 加载器名称。Forge 或 NeoForge。 + /// + public string LoaderName => (int)ForgeType == 1 ? "NeoForge" : "Forge"; + + /// + /// 文件扩展名。不以小数点开头。 + /// + public string FileExtension + { + get + { + if (ForgeType == 0) return ((DlForgeVersionEntry)this).Category == "installer" ? "jar" : "zip"; + + return "jar"; + } + } + + /// + /// Forge:MC 版本是否小于 1.13。 + /// NeoForge:MC 版本是否为 1.20.1。 + /// Cleanroom:固定为 False。 + /// + public bool IsLegacy + { + get + { + // Cleanroom 始终为 False + if ((int)ForgeType == 2) + return false; + // 虽然很抽象,但确实可以这样判断 + // Forge:1.13+ 的版本号首位都大于 20 + // NeoForge:1.20.1 的版本号首位人为规定为 19 开头 + return Version.Major < 20; + } + } + + public int CompareTo(DlForgelikeEntry other) + { + if (Version != other.Version) return Version.CompareTo(other.Version); + + return ModMinecraft.CompareVersion(VersionName, other.VersionName); + } + } + + public class DlForgeVersionEntry : DlForgelikeEntry + { + /// + /// 安装类型。有 installer、client、universal 三种。 + /// + public string Category; + + /// + /// 用于下载的文件版本名。可能在 Version 的基础上添加了分支。 + /// + public string FileVersion; + + /// + /// 文件的 MD5 或 SHA1(BMCLAPI 的老版本是 MD5,新版本是 SHA1;官方源总是 MD5)。 + /// + public string Hash; + + /// + /// 是否为推荐版本。 + /// + public bool IsRecommended; + + /// + /// 发布时间,格式为“yyyy/MM/dd HH:mm”。 + /// + public string ReleaseTime; + + public DlForgeVersionEntry(string Version, string Branch, string Inherit) + { + // 司马版本的特殊处理 + if (Version == "11.15.1.2318" || Version == "11.15.1.1902" || Version == "11.15.1.1890") + Branch = "1.8.9"; + if (Branch is null && Inherit == "1.7.10" && Conversions.ToDouble(Version.Split(".")[3]) >= 1300d) + Branch = "1.7.10"; + // 为 DlForgelikeEntry 提供所有信息 + ForgeType = ForgelikeType.Forge; + VersionName = Version; + this.Version = new Version(Version); + this.Inherit = Inherit; + FileVersion = Version + (Branch is null ? "" : "-" + Branch); + } + } + + /// + /// Forge 版本列表,主加载器。 + /// + public static void DlForgeVersionMain(ModLoader.LoaderTask> Loader) + { + var DlForgeVersionOfficialLoader = + new ModLoader.LoaderTask>("DlForgeVersion Official", + DlForgeVersionOfficialMain); + var DlForgeVersionBmclapiLoader = + new ModLoader.LoaderTask>("DlForgeVersion Bmclapi", + DlForgeVersionBmclapiMain); + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List>, int>> + { new(DlForgeVersionBmclapiLoader, 30), new(DlForgeVersionOfficialLoader, 30 + 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List>, int>> + { new(DlForgeVersionOfficialLoader, 5), new(DlForgeVersionBmclapiLoader, 5 + 30) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List>, int>> + { new(DlForgeVersionOfficialLoader, 60), new(DlForgeVersionBmclapiLoader, 60 + 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// Forge 版本列表,官方源。 + /// + public static void DlForgeVersionOfficialMain(ModLoader.LoaderTask> Loader) + { + string Result; + try + { + Result = Conversions.ToString(ModNet.NetGetCodeByRequestRetry( + "https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_" + + Loader.Input.Replace("-", "_") + ".html", UseBrowserUserAgent: true)); // 兼容 Forge 1.7.10-pre4,#4057 + } + catch (ModNet.HttpRequestFailedException ex) + { + if (ex.StatusCode == HttpStatusCode.NotFound) throw new Exception("无可用版本"); + + throw; + } + catch (Exception ex) + { + if (ex.Message.Contains("(404)")) throw new Exception("无可用版本"); + + throw; + } + + if (Result.Length < 1000) + throw new Exception("获取到的版本列表长度不足(" + Result + ")"); + var Versions = new List(); + try + { + // 分割版本信息 + var VersionCodes = Strings.Mid(Result, 1, Result.LastIndexOfF("")) + .Split(" )[^<]+"); + Category = "installer"; + } + else if (VersionCode.Contains("classifier-universal\"")) + { + // 类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分) + VersionCode = VersionCode.Substring(VersionCode.IndexOfF("universal.zip")); + MD5 = VersionCode.RegexSeek("(?<=MD5: )[^<]+"); + Category = "universal"; + } + else if (VersionCode.Contains("client.zip")) + { + // 类型为 client.zip,支持范围 182~ (1.3.2 部分 ~) + VersionCode = VersionCode.Substring(VersionCode.IndexOfF("client.zip")); + MD5 = VersionCode.RegexSeek("(?<=MD5: )[^<]+"); + Category = "client"; + } + else + { + // 没有任何下载(1.6.4 有一部分这种情况) + continue; + } + + // 添加进列表 + Versions.Add(new DlForgeVersionEntry(Name, Branch, Inherit) + { + Category = Category, IsRecommended = IsRecommended, + Hash = MD5.Trim(Conversions.ToChar("\r"), Conversions.ToChar("\n")), + ReleaseTime = ReleaseTime + }); + } + catch (Exception ex) + { + throw new Exception("Forge 官方源版本信息提取失败(" + VersionCode + ")", ex); + } + } + } + catch (Exception ex) + { + throw new Exception("Forge 官方源版本列表解析失败(" + Result + ")", ex); + } + + if (!Versions.Any()) + throw new Exception("无可用版本"); + Loader.Output = Versions; + } + + /// + /// Forge 版本列表,BMCLAPI。 + /// + public static void DlForgeVersionBmclapiMain(ModLoader.LoaderTask> Loader) + { + var Json = (JArray)ModNet.NetGetCodeByRequestRetry( + "https://bmclapi2.bangbang93.com/forge/minecraft/" + Loader.Input.Replace("-", "_"), + IsJson: true); // 兼容 Forge 1.7.10-pre4,#4057 + var Versions = new List(); + try + { + var Recommended = ModDownloadLib.McDownloadForgeRecommendedGet(Loader.Input); + foreach (JObject Token in Json) + { + // 分类与 Hash 获取 + string Hash = null; + var Category = "unknown"; + var Proi = -1; + foreach (JObject File in Token["files"]) + switch (File["category"].ToString() ?? "") + { + case "installer": + { + if (File["format"].ToString() == "jar") + { + // 类型为 installer.jar,支持范围 ~753 (~ 1.6.1 部分), 738~684 (1.5.2 全部) + Hash = (string)File["hash"]; + Category = "installer"; + Proi = 2; + } + + break; + } + case "universal": + { + if (Proi <= 1 && File["format"].ToString() == "zip") + { + // 类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分) + Hash = (string)File["hash"]; + Category = "universal"; + Proi = 1; + } + + break; + } + case "client": + { + if (Proi <= 0 && File["format"].ToString() == "zip") + { + // 类型为 client.zip,支持范围 182~ (1.3.2 部分 ~) + Hash = (string)File["hash"]; + Category = "client"; + Proi = 0; + } + + break; + } + } + + // 获取 Entry + var Branch = (string)Token["branch"]; + var Name = (string)Token["version"]; + // 基础信息获取 + var Entry = new DlForgeVersionEntry(Name, Branch, Loader.Input) + { Hash = Hash, Category = Category, IsRecommended = (Recommended ?? "") == (Name ?? "") }; + var TimeSplit = Token["modified"].ToString().Split('-', 'T', ':', '.', ' ', '/'); + Entry.ReleaseTime = Token["modified"].ToObject().ToLocalTime() + .ToString("yyyy'/'MM'/'dd HH':'mm"); + // 添加项 + Versions.Add(Entry); + } + } + catch (Exception ex) + { + throw new Exception("Forge BMCLAPI 版本列表解析失败(" + Json + ")", ex); + } + + if (!Versions.Any()) + throw new Exception("无可用版本"); + Loader.Output = Versions; + } + + #endregion + + #region DlNeoForgeList | NeoForge 版本列表 + + public struct DlNeoForgeListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 所有版本的列表。已经按从新到老排序。 + /// + public List Value; + } + + public class DlNeoForgeListEntry : DlForgelikeEntry + { + /// + /// API 使用的原始版本字符串,如 “20.4.30-beta”、“1.20.1-47.1.99”(Legacy)。 + /// + public string ApiName; + + /// + /// 是否是 Beta 版。 + /// + public bool IsBeta; + + public DlNeoForgeListEntry(string ApiName) + { + ForgeType = ForgelikeType.NeoForge; + this.ApiName = ApiName; + IsBeta = ApiName.Contains("beta") || ApiName.Contains("alpha"); + if (ApiName.Contains("1.20.1")) // 1.20.1-47.1.99 + { + VersionName = ApiName.Replace("1.20.1-", ""); + Version = new Version("19." + VersionName); + Inherit = "1.20.1"; + } + else if (ApiName.StartsWith("0.")) // 0.25w14craftmine.3-beta + { + VersionName = ApiName; + var Segments = ApiName.BeforeFirst("-").Split('.'); + Version = new Version(0, 0, Conversions.ToInteger(Segments.Last())); + Inherit = Segments[1]; + } + else // 20.4.30-beta;26.1.0.0-alpha.1+snapshot-1 + { + VersionName = ApiName; + Version = new Version(ApiName.BeforeFirst("-")); + if (Version.Major >= 24) + Inherit = Version.ToString().Replace(".0", ""); + else + Inherit = "1." + Version.Major + (Version.Minor > 0 ? "." + Version.Minor : ""); + if (VersionName.Contains("+")) + Inherit += "-" + VersionName.AfterFirst("+"); + } + } + + /// + /// 文件在官网的基础地址,不包含后缀。 + /// + public string UrlBase + { + get + { + var PackageName = IsLegacy ? "forge" : "neoforge"; + return + $"https://maven.neoforged.net/releases/net/neoforged/{PackageName}/{ApiName}/{PackageName}-{ApiName}"; + } + } + } + + /// + /// NeoForge 版本列表,主加载器。 + /// + public static ModLoader.LoaderTask DlNeoForgeListLoader = + new("DlNeoForgeList Main", DlNeoForgeListMain); + + private static void DlNeoForgeListMain(ModLoader.LoaderTask loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(loader, + new List, int>> + { new(DlNeoForgeListBmclapiLoader, 30), new(DlNeoForgeListOfficialLoader, 30 + 60) }, + loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(loader, + new List, int>> + { new(DlNeoForgeListOfficialLoader, 5), new(DlNeoForgeListBmclapiLoader, 5 + 30) }, + loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(loader, + new List, int>> + { new(DlNeoForgeListOfficialLoader, 60), new(DlNeoForgeListBmclapiLoader, 60 + 60) }, + loader.IsForceRestarting); + break; + } + } + } + + /// + /// NeoForge 版本列表,官方源。 + /// + public static ModLoader.LoaderTask DlNeoForgeListOfficialLoader = + new("DlNeoForgeList Official", DlNeoForgeListOfficialMain); + + private static void DlNeoForgeListOfficialMain(ModLoader.LoaderTask loader) + { + // 获取版本列表 JSON + var resultLatest = ModNet + .NetGetCodeByRequestRetry("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", + UseBrowserUserAgent: true, IsJson: true).ToString(); + var resultLegacy = ModNet + .NetGetCodeByRequestRetry("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge", + UseBrowserUserAgent: true, IsJson: true).ToString(); + if (resultLatest.Length < 100 || resultLegacy.Length < 100) + throw new Exception("获取到的版本列表长度不足(" + resultLatest + ")"); + // 解析 + try + { + loader.Output = new DlNeoForgeListResult + { + IsOfficial = true, + SourceName = "NeoForge 官方源", + Value = GetNeoForgeEntries(resultLatest, resultLegacy) + }; + } + catch (Exception ex) + { + throw new Exception( + "NeoForge 官方源版本列表解析失败(" + resultLatest + "\r\n" + "\r\n" + resultLegacy + ")", ex); + } + } + + /// + /// NeoForge 版本列表,BMCLAPI。 + /// + public static ModLoader.LoaderTask DlNeoForgeListBmclapiLoader = + new("DlNeoForgeList Bmclapi", DlNeoForgeListBmclapiMain); + + public static void DlNeoForgeListBmclapiMain(ModLoader.LoaderTask loader) + { + // 获取版本列表 JSON + var resultLatest = ModNet + .NetGetCodeByRequestRetry( + "https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge", + UseBrowserUserAgent: true, IsJson: true).ToString(); + var resultLegacy = ModNet + .NetGetCodeByRequestRetry( + "https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/forge", + UseBrowserUserAgent: true, IsJson: true).ToString(); + if (resultLatest.Length < 100 || resultLegacy.Length < 100) + throw new Exception("获取到的版本列表长度不足(" + resultLatest + ")"); + // 解析 + try + { + loader.Output = new DlNeoForgeListResult + { + IsOfficial = true, + SourceName = "BMCLAPI", + Value = GetNeoForgeEntries(resultLatest, resultLegacy) + }; + } + catch (Exception ex) + { + throw new Exception( + "NeoForge BMCLAPI 版本列表解析失败(" + resultLatest + "\r\n" + "\r\n" + resultLegacy + ")", + ex); + } + } + + private static List GetNeoForgeEntries(string latestJson, string latestLegacyJson) + { + var versionNames = (latestLegacyJson + latestJson).RegexSearch( + @"(?<="")(1\.20\.1-)?\d+\.[^\.]+\.\d+(\.\d+)?(-(beta|alpha)(\.\d+)?)?(\+snapshot-\d+)?(?="")"); + var versions = versionNames.Where(name => name != "47.1.82").Select(name => new DlNeoForgeListEntry(name)) + .OrderByDescending(a => a).ToList(); // 这个版本虽然在版本列表中,但不能下载 + if (!versions.Any()) + throw new Exception("无可用版本"); + return versions; + } + + #endregion + + #region DlCleanroomList | Cleanroom 版本列表 + + public struct DlCleanroomListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 所有版本的列表。已经按从新到老排序。 + /// + public List Value; + } + + public class DlCleanroomListEntry : DlForgelikeEntry + { + /// + /// API 使用的原始版本字符串,如 “0.2.4-alpha”。 + /// + public string ApiName; + + /// + /// 是否是 Beta 版。 + /// + public bool IsBeta; + + public DlCleanroomListEntry(string ApiName) + { + ForgeType = ForgelikeType.Cleanroom; + this.ApiName = ApiName; + IsBeta = ApiName.Contains("alpha"); + VersionName = ApiName; + Version = new Version(ApiName.BeforeFirst("-")); + Inherit = "1.12.2"; + } + + /// + /// 文件在官网的基础地址,不包含后缀。 + /// + public string UrlBase => + $"https://github.com/CleanroomMC/Cleanroom/releases/download/{ApiName}/cleanroom-{ApiName}"; + } + + /// + /// Cleanroom 版本列表,主加载器。 + /// + public static ModLoader.LoaderTask DlCleanroomListLoader = + new("DlCleanroomList Main", DlCleanroomListMain); + + private static void DlCleanroomListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlCleanroomListOfficialLoader, 30) }, Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlCleanroomListOfficialLoader, 5) }, Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlCleanroomListOfficialLoader, 60) }, Loader.IsForceRestarting); + break; + } + } + } + + /// + /// Cleanroom 版本列表,官方源。 + /// + public static ModLoader.LoaderTask DlCleanroomListOfficialLoader = + new("DlCleanroomList Official", DlCleanroomListOfficialMain); + + private static void DlCleanroomListOfficialMain(ModLoader.LoaderTask Loader) + { + // 获取版本列表 JSON + var ResultLatest = Conversions.ToString(ModNet.NetGetCodeByRequestRetry( + "https://api.github.com/repos/CleanroomMC/Cleanroom/releases", UseBrowserUserAgent: true)); + if (ResultLatest.Length < 100) + throw new Exception("获取到的版本列表长度不足(" + ResultLatest + ")"); + // 解析 + try + { + Loader.Output = new DlCleanroomListResult + { + IsOfficial = true, + SourceName = "Cleanroom 官方源", + Value = GetCleanroomEntries(ResultLatest) + }; + } + catch (Exception ex) + { + throw new Exception("Cleanroom 官方源版本列表解析失败(" + ResultLatest + ")", ex); + } + } + + private static List GetCleanroomEntries(string LatestJson) + { + var Versions = new List(); + var Json = JArray.Parse(LatestJson); + foreach (JObject Token in Json) + Versions.Add(new DlCleanroomListEntry(Token["tag_name"].ToString()) + { ForgeType = (DlForgelikeEntry.ForgelikeType)2 }); + if (!Versions.Any()) + throw new Exception("没有可用版本"); + Versions = Versions.OrderByDescending(a => a.Version).ToList(); + return Versions; + } + + #endregion + + #region DlLiteLoaderList | LiteLoader 版本列表 + + public struct DlLiteLoaderListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public List Value; + + /// + /// 官方源的失败原因。若没有则为 Nothing。 + /// + public Exception OfficialError; + } + + public class DlLiteLoaderListEntry + { + /// + /// 实际的文件名,如“liteloader-installer-1.12-00-SNAPSHOT.jar”。 + /// + public string FileName; + + /// + /// 对应的 Minecraft 版本,如“1.12.2”。 + /// + public string Inherit; + + /// + /// 是否为 1.7 及更早的远古版。 + /// + public bool IsLegacy; + + /// + /// 是否为测试版。 + /// + public bool IsPreview; + + /// + /// 对应的 Json 项。 + /// + public JToken JsonToken; + + /// + /// 文件的 MD5。 + /// + public string MD5; + + /// + /// 发布时间,格式为“yyyy/mm/dd HH:mm”。 + /// + public string ReleaseTime; + } + + /// + /// LiteLoader 版本列表,主加载器。 + /// + public static ModLoader.LoaderTask DlLiteLoaderListLoader = + new("DlLiteLoaderList Main", DlLiteLoaderListMain); + + private static void DlLiteLoaderListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { + new(DlLiteLoaderListBmclapiLoader, 30), new(DlLiteLoaderListOfficialLoader, 30 + 60) + }, Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { + new(DlLiteLoaderListOfficialLoader, 5), new(DlLiteLoaderListBmclapiLoader, 5 + 30) + }, Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { + new(DlLiteLoaderListOfficialLoader, 60), new(DlLiteLoaderListBmclapiLoader, 60 + 60) + }, Loader.IsForceRestarting); + break; + } + } + } + + /// + /// LiteLoader 版本列表,官方源。 + /// + public static ModLoader.LoaderTask DlLiteLoaderListOfficialLoader = + new("DlLiteLoaderList Official", DlLiteLoaderListOfficialMain); + + private static void DlLiteLoaderListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = + (JObject)ModNet.NetGetCodeByRequestRetry("https://dl.liteloader.com/versions/versions.json", IsJson: true); + try + { + var Json = (JObject)Result["versions"]; + var Versions = new List(); + foreach (var Pair in Json) + { + if (Pair.Key.StartsWithF("1.6") || Pair.Key.StartsWithF("1.5")) + continue; + var RealEntry = + (Pair.Value["artefacts"] ?? Pair.Value["snapshots"])["com.mumfrey:liteloader"]["latest"]; + Versions.Add(new DlLiteLoaderListEntry + { + Inherit = Pair.Key, + IsLegacy = Conversions.ToDouble(Pair.Key.Split(".")[1]) < 8d, + IsPreview = RealEntry["stream"].ToString().ToLower() == "snapshot", + FileName = "liteloader-installer-" + Pair.Key + + (Pair.Key == "1.8" || Pair.Key == "1.9" ? ".0" : "") + "-00-SNAPSHOT.jar", + MD5 = (string)RealEntry["md5"], + ReleaseTime = TimeUtils.FormatUnixTimestamp((long)RealEntry["timestamp"]), + JsonToken = RealEntry + }); + } + + Loader.Output = new DlLiteLoaderListResult + { IsOfficial = true, SourceName = "LiteLoader 官方源", Value = Versions }; + } + catch (Exception ex) + { + throw new Exception("LiteLoader 官方源版本列表解析失败(" + Result + ")", ex); + } + } + + /// + /// LiteLoader 版本列表,BMCLAPI。 + /// + public static ModLoader.LoaderTask DlLiteLoaderListBmclapiLoader = + new("DlLiteLoaderList Bmclapi", DlLiteLoaderListBmclapiMain); + + private static void DlLiteLoaderListBmclapiMain(ModLoader.LoaderTask Loader) + { + var Result = + (JObject)ModNet.NetGetCodeByRequestRetry( + "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json", IsJson: true); + try + { + var Json = (JObject)Result["versions"]; + var Versions = new List(); + foreach (var Pair in Json) + { + if (Pair.Key.StartsWithF("1.6") || Pair.Key.StartsWithF("1.5")) + continue; + var RealEntry = + (Pair.Value["artefacts"] ?? Pair.Value["snapshots"])["com.mumfrey:liteloader"]["latest"]; + Versions.Add(new DlLiteLoaderListEntry + { + Inherit = Pair.Key, + IsLegacy = Conversions.ToDouble(Pair.Key.Split(".")[1]) < 8d, + IsPreview = RealEntry["stream"].ToString().ToLower() == "snapshot", + FileName = "liteloader-installer-" + Pair.Key + + (Pair.Key == "1.8" || Pair.Key == "1.9" ? ".0" : "") + "-00-SNAPSHOT.jar", + MD5 = (string)RealEntry["md5"], + ReleaseTime = TimeUtils.FormatUnixTimestamp((long)RealEntry["timestamp"]), + JsonToken = RealEntry + }); + } + + Loader.Output = new DlLiteLoaderListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = Versions }; + } + catch (Exception ex) + { + throw new Exception("LiteLoader BMCLAPI 版本列表解析失败(" + Result + ")", ex); + } + } + + #endregion + + #region DlFabricList | Fabric 列表 + + public struct DlFabricListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public JObject Value; + } + + /// + /// Fabric 列表,主加载器。 + /// + public static ModLoader.LoaderTask DlFabricListLoader = + new("DlFabricList Main", DlFabricListMain); + + private static void DlFabricListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlFabricListBmclapiLoader, 30), new(DlFabricListOfficialLoader, 30 + 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlFabricListOfficialLoader, 5), new(DlFabricListBmclapiLoader, 5 + 30) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlFabricListOfficialLoader, 60), new(DlFabricListBmclapiLoader, 60 + 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// Fabric 列表,官方源。 + /// + public static ModLoader.LoaderTask DlFabricListOfficialLoader = + new("DlFabricList Official", DlFabricListOfficialMain); + + private static void DlFabricListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = (JObject)ModNet.NetGetCodeByRequestRetry("https://meta.fabricmc.net/v2/versions", IsJson: true); + try + { + var Output = new DlFabricListResult { IsOfficial = true, SourceName = "Fabric 官方源", Value = Result }; + if (Output.Value["game"] is null || Output.Value["loader"] is null || Output.Value["installer"] is null) + throw new Exception("获取到的列表缺乏必要项"); + Loader.Output = Output; + } + catch (Exception ex) + { + throw new Exception("Fabric 官方源版本列表解析失败(" + Result + ")", ex); + } + } + + /// + /// Fabric 列表,BMCLAPI。 + /// + public static ModLoader.LoaderTask DlFabricListBmclapiLoader = + new("DlFabricList Bmclapi", DlFabricListBmclapiMain); + + private static void DlFabricListBmclapiMain(ModLoader.LoaderTask Loader) + { + var Result = (JObject)ModNet.NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/fabric-meta/v2/versions", + IsJson: true); + try + { + var Output = new DlFabricListResult { IsOfficial = false, SourceName = "BMCLAPI", Value = Result }; + if (Output.Value["game"] is null || Output.Value["loader"] is null || Output.Value["installer"] is null) + throw new Exception("获取到的列表缺乏必要项"); + Loader.Output = Output; + } + catch (Exception ex) + { + throw new Exception("Fabric BMCLAPI 版本列表解析失败(" + Result + ")", ex); + } + } + + /// + /// Fabric API 列表,官方源。 + /// + public static ModLoader.LoaderTask> DlFabricApiLoader = new("Fabric API List Loader", + Task => Task.Output = ModComp.CompFilesGet("fabric-api", false)); + + /// + /// OptiFabric 列表,官方源。 + /// + public static ModLoader.LoaderTask> DlOptiFabricLoader = + new("OptiFabric List Loader", Task => Task.Output = ModComp.CompFilesGet("322385", true)); + + #endregion + + #region DlQuiltList | Quilt 列表 + + public struct DlQuiltListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public JObject Value; + } + + /// + /// Quilt 列表,主加载器。 + /// + public static ModLoader.LoaderTask DlQuiltListLoader = + new("DlQuiltList Main", DlQuiltListMain); + + private static void DlQuiltListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlQuiltListOfficialLoader, 30), new(DlQuiltListOfficialLoader, 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlQuiltListOfficialLoader, 5), new(DlQuiltListOfficialLoader, 35) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlQuiltListOfficialLoader, 60), new(DlQuiltListOfficialLoader, 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// Quilt 列表,官方源。 + /// + public static ModLoader.LoaderTask DlQuiltListOfficialLoader = + new("DlQuiltList Official", DlQuiltListOfficialMain); + + private static void DlQuiltListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = (JObject)ModNet.NetGetCodeByRequestRetry("https://meta.quiltmc.org/v3/versions", IsJson: true); + try + { + var Output = new DlQuiltListResult { IsOfficial = true, SourceName = "Quilt 官方源", Value = Result }; + if (Output.Value["game"] is null || Output.Value["loader"] is null || Output.Value["installer"] is null) + throw new Exception("获取到的列表缺乏必要项"); + Loader.Output = Output; + } + catch (Exception ex) + { + throw new Exception("Quilt 官方源版本列表解析失败(" + Result + ")", ex); + } + } + + // ''' + // ''' TODO: Quilt 列表,BMCLAPI。 + // ''' + // Public DlQuiltListBmclapiLoader As New LoaderTask(Of Integer, DlQuiltListResult)("DlQuiltList Bmclapi", AddressOf DlQuiltListBmclapiMain) + // Private Sub DlQuiltListBmclapiMain(Loader As LoaderTask(Of Integer, DlQuiltListResult)) + // Dim Result As JObject = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/Quilt-meta/v2/versions", IsJson:=True) + // Try + // Dim Output = New DlQuiltListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Result} + // If Output.Value("game") Is Nothing OrElse Output.Value("loader") Is Nothing OrElse Output.Value("installer") Is Nothing Then Throw New Exception("获取到的列表缺乏必要项") + // Loader.Output = Output + // Catch ex As Exception + // Throw New Exception("Quilt BMCLAPI 版本列表解析失败(" & Result.ToString & ")", ex) + // End Try + // End Sub + + /// + /// QSL 列表,官方源。 + /// + public static ModLoader.LoaderTask> DlQSLLoader = new("QSL List Loader", + Task => Task.Output = ModComp.CompFilesGet("qsl", false)); + + #endregion + + #region DlLabyModList | LabyMod 列表 + + public struct DlLabyModListResult + { + /// + /// 获取到的数据。 + /// + public JObject Value; + } + + /// + /// LabyMod 列表,主加载器。 + /// + public static ModLoader.LoaderTask DlLabyModListLoader = + new("DlLabyModList Main", DlLabyModListMain); + + private static void DlLabyModListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLabyModListOfficialLoader, 30), new(DlLabyModListOfficialLoader, 60) }, + Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLabyModListOfficialLoader, 5), new(DlLabyModListOfficialLoader, 35) }, + Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLabyModListOfficialLoader, 60), new(DlLabyModListOfficialLoader, 60) }, + Loader.IsForceRestarting); + break; + } + } + } + + /// + /// LabyMod 列表,官方源。 + /// + public static ModLoader.LoaderTask DlLabyModListOfficialLoader = + new("DlLabyModList Official", DlLabyModListOfficialMain); + + private static void DlLabyModListOfficialMain(ModLoader.LoaderTask Loader) + { + JObject ResultProduction; + using (var productionResponse = HttpRequestBuilder + .Create("https://releases.r2.labymod.net/api/v1/manifest/production/latest.json", HttpMethod.Get) + .WithHttpVersionOption(HttpVersion.Version20).SendAsync(true).GetAwaiter().GetResult()) + { + ResultProduction = (JObject)ModBase.GetJson(productionResponse.AsStringContent()); + } + + JObject ResultSnapshot; + using (var snapshotResponse = HttpRequestBuilder + .Create("https://releases.r2.labymod.net/api/v1/manifest/snapshot/latest.json", HttpMethod.Get) + .WithHttpVersionOption(HttpVersion.Version20).SendAsync(true).GetAwaiter().GetResult()) + { + ResultSnapshot = (JObject)ModBase.GetJson(snapshotResponse.AsStringContent()); + } + + var Result = new JObject(); + Result.Add("production", ResultProduction); + Result.Add("snapshot", ResultSnapshot); + try + { + var Output = new DlLabyModListResult { Value = Result }; + if (Output.Value["production"]["labyModVersion"] is null || + Output.Value["snapshot"]["labyModVersion"] is null) + throw new Exception("获取到的列表缺乏必要项"); + Loader.Output = Output; + } + catch (Exception ex) + { + throw new Exception("LabyMod 版本列表解析失败(" + Result + ")", ex); + } + } + + #endregion + + #region DlMod | Mod 镜像源请求 + + /// + /// 对可能涉及 Mod 镜像源的请求进行处理,返回字符串或 JObject。 + /// 调用 NetGetCodeByRequest,会进行重试。 + /// + public static object DlModRequest(string Url, bool IsJson = false) + { + var Urls = new List>(); + var McimUrl = DlSourceModGet(Url); + if ((McimUrl ?? "") != (Url ?? "")) + switch (Config.Download.Comp.CompSourceSolution) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + Urls.Add(new KeyValuePair(McimUrl, 5)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + Urls.Add(new KeyValuePair(Url, 15)); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + Urls.Add(new KeyValuePair(Url, 5)); + Urls.Add(new KeyValuePair(McimUrl, 5)); + Urls.Add(new KeyValuePair(Url, 15)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + break; + } + + default: + { + Urls.Add(new KeyValuePair(Url, 5)); + Urls.Add(new KeyValuePair(Url, 15)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + break; + } + } + + var Exs = ""; + foreach (var Source in Urls) + try + { + return ModNet.NetGetCodeByRequestOnce(Source.Key, Encoding.UTF8, Source.Value * 1000, IsJson, + UseBrowserUserAgent: true); + } + catch (Exception ex) + { + // 镜像源可能随机爆炸,忽略就好 + if (!ex.Message.ContainsF("mcimirror")) Exs += ex.Message + "\r\n"; + } + + throw new Exception(Exs); + } + + /// + /// 对可能涉及 Mod 镜像源的请求进行处理。 + /// 调用 NetRequest,会进行重试。 + /// + public static string DlModRequest(string Url, string Method, string Data, string ContentType, + bool allowMirror = false) + { + var Urls = new List>(); + var McimUrl = DlSourceModGet(Url); + if ((McimUrl ?? "") != (Url ?? "")) + switch (allowMirror ? Config.Download.Comp.CompSourceSolution : 2) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + Urls.Add(new KeyValuePair(McimUrl, 5)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + Urls.Add(new KeyValuePair(Url, 15)); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + Urls.Add(new KeyValuePair(Url, 5)); + Urls.Add(new KeyValuePair(McimUrl, 5)); + Urls.Add(new KeyValuePair(Url, 15)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + break; + } + + default: + { + Urls.Add(new KeyValuePair(Url, 5)); + Urls.Add(new KeyValuePair(Url, 15)); + Urls.Add(new KeyValuePair(McimUrl, 10)); + break; + } + } + + var Exs = ""; + foreach (var Source in Urls) + try + { + return ModNet.NetRequestOnce(Source.Key, Method, Data, ContentType, Source.Value * 1000); + } + catch (Exception ex) + { + if (!ex.Message.ContainsF("mcimirror")) Exs += ex.Message + "\r\n"; + } + + throw new Exception(Exs); + } + + #endregion + + #region DlSource | 镜像下载源 + + private static bool DlPreferMojang; + + /// + /// 下载文件(而非获取版本列表)的时候,是否优先使用官方源。 + /// + public static bool DlSourcePreferMojang => Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Download.FileSource, 2, false) || + (Operators.ConditionalCompareObjectEqual(Config.Download.FileSource, 1, false) && DlPreferMojang)); + + /// + /// 下载文件(而非获取版本列表)的时候,根据是否优先使用官方源决定使用 Url 的顺序。 + /// + public static IEnumerable DlSourceOrder(IEnumerable OfficialUrls, IEnumerable MirrorUrls) + { + return DlSourcePreferMojang ? OfficialUrls.Union(MirrorUrls) : MirrorUrls.Union(OfficialUrls); + } + + /// + /// 获取版本列表(而非下载文件)的时候,是否优先使用官方源。 + /// + public static bool DlVersionListPreferMojang => Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Download.VersionListSource, 2, false) || + (Operators.ConditionalCompareObjectEqual(Config.Download.VersionListSource, 1, false) && + DlPreferMojang)); + + /// + /// 获取版本列表(而非下载文件)的时候,根据是否优先使用官方源决定使用 Url 的顺序。 + /// + public static IEnumerable DlVersionListOrder(IEnumerable OfficialUrls, + IEnumerable MirrorUrls) + { + return DlVersionListPreferMojang ? OfficialUrls.Union(MirrorUrls) : MirrorUrls.Union(OfficialUrls); + } + + + /// + /// 下载 Assets 文件。 + /// + public static IEnumerable DlSourceAssetsGet(string Original) + { + Original = Original.Replace("http://resources.download.minecraft.net", + "https://resources.download.minecraft.net"); + return DlSourceOrder(new[] { Original }, + new[] + { + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/assets") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/assets") + .Replace("https://resources.download.minecraft.net", "https://bmclapi2.bangbang93.com/assets") + }); + } + + /// + /// 下载 Libraries 文件。 + /// + public static IEnumerable DlSourceLibraryGet(string Original) + { + if (new[] { "minecraftforge", "fabricmc", "neoforged" }.Any(k => Original.Contains(k))) // 不添加原版源 + return new[] + { + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft", + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft", + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto") + }; + + return DlSourceOrder(new[] { Original }, + new[] + { + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/maven") + .Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft", + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries") + .Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft", + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + Original + }); + } + + /// + /// 下载 Launcher 或 Meta 文件。 + /// 不应使用它来获取版本列表(因为它只使用文件下载源设置来决定源顺序)。 + /// + public static IEnumerable DlSourceLauncherOrMetaGet(string Original) + { + if (Original is null) + throw new Exception("无对应的 json 下载地址"); + return DlSourceOrder(new[] { Original }, + new[] + { + Original.Replace("https://piston-data.mojang.com", "https://bmclapi2.bangbang93.com") + .Replace("https://piston-meta.mojang.com", "https://bmclapi2.bangbang93.com") + .Replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com") + .Replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com") + .Replace("https://zkitefly.github.io/unlisted-versions-of-minecraft", + "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"), + Original + }); + } + + /// + /// Mod Api 镜像源 + /// + /// + /// + public static string DlSourceModGet(string Original) + { + return Original.Replace("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth") + .Replace("https://api.curseforge.com", "https://mod.mcimirror.top/curseforge"); + } + + /// + /// Mod 下载镜像源 + /// + /// + /// + public static List DlSourceModDownloadGet(string original) + { + var res = new List(); + var mirrorDl = original.Replace("https://cdn.modrinth.com", "https://mod.mcimirror.top") + .Replace("https://edge.forgecdn.net", + "https://mod.mcimirror.top"); // like https://cdn.modrinth.com/data/P7dR8mSH/versions/X2hTodix/fabric-api-0.129.0%2B1.21.8.jar + // like https://edge.forgecdn.net/files/6767/951/jei-1.21.5-neoforge-21.4.0.27.jar + switch (Config.Download.Comp.CompSourceSolution) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 镜像源 + { + res.Add(mirrorDl); + res.Add(mirrorDl); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): // 平衡 + { + res.Add(original); + res.Add(mirrorDl); + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 2, false): // 官方源 + { + res.Add(original); + res.Add(original); // 错误 + break; + } + + default: + { + ModBase.Setup.Reset("ToolDownloadMod"); + res.Add(original); + break; + } + } + + res.Add(original); + return res; + } + + // Loader 自动切换 + private static void DlSourceLoader(ModLoader.LoaderTask MainLoader, + List, int>> LoaderList, bool IsForceRestart = false) + { + var WaitCycle = 0; + while (true) + { + // 检查状态 + var BeforeLoadersAllFailed = true; + foreach (var SubLoader in LoaderList) + { + if (WaitCycle == 0) // 判断是否可以不加载,直接使用已经加载好的结果 + { + if (IsForceRestart) + continue; // 强制刷新,不行 + if (SubLoader.Key.Input is null ^ MainLoader.Input is null || (SubLoader.Key.Input is not null && + !SubLoader.Key.Input.Equals(MainLoader.Input))) + continue; // 父子加载器的输入不一样,也不行 + } + + if (SubLoader.Key.State != ModBase.LoadState.Failed) + BeforeLoadersAllFailed = false; + if (SubLoader.Key.State == ModBase.LoadState.Finished) + { + // 检查加载器成功 + MainLoader.Output = SubLoader.Key.Output; + DlSourceLoaderAbort(LoaderList); + return; + } + + if (BeforeLoadersAllFailed) + // 此前的加载器全部失败,直接启动后续加载器 + if (WaitCycle < SubLoader.Value * 100) + WaitCycle = SubLoader.Value * 100; + } + + // 第一轮时:既然不直接使用已经加载好的结果,那就启动第一个加载器 + if (WaitCycle == 0) + { + LoaderList.First().Key.Start(MainLoader.Input, IsForceRestart); + foreach (var Loader in LoaderList.Skip(1)) + Loader.Key.State = ModBase.LoadState.Waiting; // 将其他源标记为未启动,以确保可以切换下载源(#184) + } + + // 检查加载器失败或超时 + for (int i = 0, loopTo = LoaderList.Count - 1; i <= loopTo; i++) + { + if (WaitCycle != LoaderList[i].Value * 100) + continue; + if (i < LoaderList.Count - 1 && !LoaderList.All(l => l.Key.State == ModBase.LoadState.Failed)) + { + // 若还有下一个源,则启动下一个源 + LoaderList[i + 1].Key.Start(MainLoader.Input, IsForceRestart); + } + else + { + // 若没有,则失败 + Exception ErrorInfo = null; + for (int ii = 0, loopTo1 = LoaderList.Count - 1; ii <= loopTo1; ii++) + { + LoaderList[ii].Key.Input = default; // 重置输入,以免以同样的输入“重试加载”时直接失败 + if (LoaderList[ii].Key.Error is not null) + if (ErrorInfo is null || LoaderList[ii].Key.Error.Message.Contains("无可用版本")) + ErrorInfo = LoaderList[ii].Key.Error; + } + + if (ErrorInfo is null) + ErrorInfo = new TimeoutException("下载源连接超时"); + DlSourceLoaderAbort(LoaderList); + throw ErrorInfo; + } + + break; + } + + // 计时 + Thread.Sleep(10); + WaitCycle += 1; + // 检查父加载器中断 + if (MainLoader.IsAborted) + { + DlSourceLoaderAbort(LoaderList); + return; + } + } + } + + private static void DlSourceLoaderAbort( + List, int>> LoaderList) + { + foreach (var Loader in LoaderList) + if (Loader.Key.State == ModBase.LoadState.Loading) + Loader.Key.Abort(); + } + + #endregion + + #region DlLegacyFabricList | LegacyFabric 列表 + + public struct DlLegacyFabricListResult + { + /// + /// 数据来源名称,如“Official”,“BMCLAPI”。 + /// + public string SourceName; + + /// + /// 是否为官方的实时数据。 + /// + public bool IsOfficial; + + /// + /// 获取到的数据。 + /// + public JObject Value; + } + + /// + /// LegacyFabric 列表,主加载器。 + /// + public static ModLoader.LoaderTask DlLegacyFabricListLoader = + new("DlLegacyFabricList Main", DlLegacyFabricListMain); + + private static void DlLegacyFabricListMain(ModLoader.LoaderTask Loader) + { + switch (Config.Download.VersionListSource) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLegacyFabricListOfficialLoader, 30) }, Loader.IsForceRestarting); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLegacyFabricListOfficialLoader, 5) }, Loader.IsForceRestarting); + break; + } + + default: + { + DlSourceLoader(Loader, + new List, int>> + { new(DlLegacyFabricListOfficialLoader, 60) }, Loader.IsForceRestarting); + break; + } + } + } + + /// + /// LegacyFabric 列表,官方源。 + /// + public static ModLoader.LoaderTask DlLegacyFabricListOfficialLoader = + new("DlLegacyFabricList Official", DlLegacyFabricListOfficialMain); + + private static void DlLegacyFabricListOfficialMain(ModLoader.LoaderTask Loader) + { + var Result = + (JObject)ModNet.NetGetCodeByRequestRetry("https://meta.legacyfabric.net/v2/versions", IsJson: true); + try + { + var Output = new DlLegacyFabricListResult + { IsOfficial = true, SourceName = "LegacyFabric 官方源", Value = Result }; + if (Output.Value["game"] is null || Output.Value["loader"] is null || Output.Value["installer"] is null) + throw new Exception("获取到的列表缺乏必要项"); + Loader.Output = Output; + } + catch (Exception ex) + { + throw new Exception("LegacyFabric 官方源版本列表解析失败(" + Result + ")", ex); + } + } + + /// + /// Legacy Fabric API 列表,官方源。 + /// + public static ModLoader.LoaderTask> DlLegacyFabricApiLoader = + new("Legacy Fabric API List Loader", Task => Task.Output = ModComp.CompFilesGet("legacy-fabric-api", false)); + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.cs new file mode 100644 index 000000000..3b64f719b --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.cs @@ -0,0 +1,413 @@ +using System.IO; +using System.Text.Json; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO; +using PCL.Core.Minecraft; +using PCL.Core.Minecraft.Java.UserPreference; + +namespace PCL; + +public static class ModJava +{ + public static int JavaListCacheVersion = 7; + + /// + /// 防止多个需要 Java 的部分同时要求下载 Java(#3797)。 + /// + public static object JavaLock = new(); + + /// + /// 目前所有可用的 Java。 + /// + public static JavaManager Javas => JavaService.JavaManager; + + /// + /// 根据要求返回最适合的 Java,若找不到则返回 Nothing。 + /// 最小与最大版本在与输入相同时也会通过。 + /// 必须在工作线程调用,且必须包括 SyncLock JavaLock。 + /// + public static JavaEntry JavaSelect(string CancelException, Version MinVersion = null, Version MaxVersion = null, + ModMinecraft.McInstance RelatedInstance = null) + { + ModBase.Log( + $"[Java] 要求选择合适 Java,要求最低版本 {(MinVersion is not null ? MinVersion.ToString() : "未指定")},要求选择的最高版本 {(MaxVersion is not null ? MaxVersion.ToString() : "未指定")},关联实例 {(RelatedInstance is not null ? RelatedInstance.Name : "未指定")}"); + + // 版本范围验证函数(安全处理 null 边界) + bool IsVersionSuitable(Version ver) + { + return (MinVersion is null || ver >= MinVersion) && (MaxVersion is null || ver <= MaxVersion); + } + + // ===== 优先级 1:实例专属 Java 偏好 ===== + if (RelatedInstance is not null && RelatedInstance.PathInstance is not null) + { + var rawPreference = Config.Instance.SelectedJava[RelatedInstance.PathInstance]; + + if (!string.IsNullOrWhiteSpace(rawPreference)) + { + var preference = GetInstanceJavaPreference(RelatedInstance); + + // 处理解析成功的偏好 + if (preference is not null) + switch (true) + { + case object _ when preference is ExistingJava: // "exist" + { + var existPref = (ExistingJava)preference; + var candidate = Javas.AddOrGet(existPref.JavaExePath); + + if (candidate is not null && candidate.IsEnabled) + { + if (!IsVersionSuitable(candidate.Installation.Version)) + ModMain.Hint( + $"实例指定的 Java ({candidate.Installation.Version}) 超出版本要求范围 [{MinVersion?.ToString() ?? "无下限"}, {MaxVersion?.ToString() ?? "无上限"}],可能导致游戏崩溃"); + ModBase.Log($"[Java] 返回实例 '{RelatedInstance.Name}' 指定的 Java: {candidate}"); + return candidate; + } + + ModBase.Log($"[Java] 警告:实例指定的 Java 路径无效或不可用: {existPref.JavaExePath}"); + + break; + } + + case object _ when preference is UseRelativePath: // "relative" + { + var relPref = (UseRelativePath)preference; + var absPath = + Path.GetFullPath(Path.Combine(Basics.ExecutableDirectory, relPref.RelativePath)); + + if (Files.IsPathWithinDirectory(absPath, Basics.ExecutableDirectory)) + { + var candidate = Javas.Get(absPath); + if (candidate is not null && candidate.IsEnabled) + { + if (!IsVersionSuitable(candidate.Installation.Version)) + ModMain.Hint( + $"实例相对路径指定的 Java (v{candidate.Installation.Version}) 超出版本要求范围,可能导致游戏崩溃", + ModMain.HintType.Critical); + ModBase.Log( + $"[Java] 返回实例 '{RelatedInstance.Name}' 相对路径指定的 Java ({relPref.RelativePath}): {candidate}"); + return candidate; + } + } + else + { + ModBase.Log($"[Java] 警告:实例相对路径指定的 Java 无效: {absPath}"); + } + + break; + } + + case object _ when preference is UseGlobalPreference: // "global" + { + // 不返回,继续到全局设置检查 + ModBase.Log($"[Java] 实例 '{RelatedInstance.Name}' 配置为使用全局 Java 设置,继续检查全局配置"); + break; + } + + default: + { + ModBase.Log($"[Java] 警告:未知的 Java 偏好类型 '{preference}',跳过处理"); + break; + } + } + else + ModBase.Log($"[Java] 实例 '{RelatedInstance.Name}' 未指定 Java 偏好(空值),使用自动选择策略"); + } + else + { + ModBase.Log($"[Java] 实例 '{RelatedInstance.Name}' 无 Java 偏好配置,使用自动选择策略"); + } + } + + // ===== 优先级 2:全局指定的 Java ===== + var globalJavaPath = Config.Launch.SelectedJava; + if (!string.IsNullOrWhiteSpace(globalJavaPath)) + { + globalJavaPath = globalJavaPath.Trim(); + var candidate = Javas.AddOrGet(globalJavaPath); + + if (candidate is not null && candidate.IsEnabled) + { + if (!IsVersionSuitable(candidate.Installation.Version)) + ModMain.Hint($"全局指定的 Java (v{candidate.Installation.Version}) 超出版本要求范围,可能导致游戏崩溃"); + ModBase.Log($"[Java] 返回全局指定的 Java: {candidate}"); + return candidate; + } + + ModBase.Log($"[Java] 警告:全局指定的 Java 路径无效或不可用: {globalJavaPath}"); + } + else + { + ModBase.Log("[Java] 无全局 Java 配置,使用自动选择策略"); + } + + // ===== 优先级 3:自动搜索合适版本 ===== + ModBase.Log("[Java] 开始自动搜索符合版本要求的 Java 运行时"); + Javas.CheckAllAvailability(); + + var reqMin = MinVersion ?? new Version(1, 0, 0); + var reqMax = MaxVersion ?? new Version(999, 999, 999); + + var candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult(); + var ret = candidates.FirstOrDefault(); + + if (ret is null && candidates.Length == 0) + { + ModBase.Log("[Java] 未找到符合版本要求的 Java,触发全盘重新扫描"); + Javas.ScanJavaAsync().GetAwaiter().GetResult(); + candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult(); + ret = candidates.FirstOrDefault(); + } + + if (ret is not null) + ModBase.Log($"[Java] 返回自动选择的 Java: {ret}"); + else + ModBase.Log("[Java] 最终未能确定可用的 Java 运行时"); + + return ret; + } + + public static JavaPreference GetInstanceJavaPreference(ModMinecraft.McInstance instance) + { + var rawPreference = Config.Instance.SelectedJava[instance.PathInstance]; + + JavaPreference preference = default; + + try + { + preference = JsonSerializer.Deserialize(rawPreference.Trim()); + } + catch (JsonException ex) + { + var trimmed = rawPreference.Trim(); + if (trimmed == "使用全局设置") // 全局设置 + preference = new UseGlobalPreference(); + else if (string.IsNullOrEmpty(trimmed)) + preference = new AutoSelect(); + else + preference = new ExistingJava(trimmed); + } + + switch (true) + { + case object _ when preference is ExistingJava: + { + var m = (ExistingJava)preference; + if (!Path.IsPathRooted(m.JavaExePath)) preference = new UseGlobalPreference(); + + break; + } + case object _ when preference is UseRelativePath: + { + var m = (UseRelativePath)preference; + if (!Files.IsPathWithinDirectory(m.RelativePath, Basics.ExecutableDirectory)) + preference = new UseGlobalPreference(); + + break; + } + } + + return preference; + } + + /// + /// 是否强制指定了 64 位 Java。如果没有强制指定,返回是否安装了 64 位 Java。 + /// + public static bool IsGameSet64BitJava(ModMinecraft.McInstance RelatedVersion = null) + { + try + { + // 检查强制指定 + var UserSetup = Conversions.ToString(Config.Launch.SelectedJava); + if (UserSetup.StartsWith("{")) // 旧版本 Json 格式 + { + var js = JToken.Parse(UserSetup); + UserSetup = $"{js["Path"]}java.exe"; + Config.Launch.SelectedJava = UserSetup; + } + + if (RelatedVersion is not null) + { + var instancePreference = GetInstanceJavaPreference(RelatedVersion); + switch (true) + { + case object _ when instancePreference is AutoSelect: + { + return Javas.Existing64BitJava(); + } + case object _ when instancePreference is ExistingJava: + { + var m = (ExistingJava)instancePreference; + var java = Javas.AddOrGet(m.JavaExePath); + return java is not null && java.Installation.Is64Bit; + } + case object _ when instancePreference is UseRelativePath: + { + var m = (UseRelativePath)instancePreference; + var javaExePath = Path.GetFullPath(m.RelativePath); + if (Files.IsPathWithinDirectory(javaExePath, Basics.ExecutableDirectory)) + { + var java = Javas.Get(javaExePath); + return java is not null && java.Installation.Is64Bit; + } + + break; + } + } + } + + if (!string.IsNullOrEmpty(UserSetup) && !File.Exists(UserSetup)) + { + Config.Launch.SelectedJava = ""; + UserSetup = string.Empty; + } + + if (string.IsNullOrEmpty(UserSetup)) return Javas.Existing64BitJava(); + var j = Javas.AddOrGet(UserSetup); + return j is not null && j.Installation.Is64Bit; + } + catch (Exception ex) + { + ModBase.Log(ex, "检查 Java 类别时出错", ModBase.LogLevel.Feedback); + if (RelatedVersion is not null) + ModBase.Setup.Reset("VersionArgumentJavaSelect", instance: RelatedVersion); + Config.Launch.SelectedJava = ""; + } + + return true; + } + + #region 下载 + + /// + /// 提示 Java 缺失,并弹窗确认是否自动下载。返回玩家选择是否下载。 + /// + public static bool JavaDownloadConfirm(string VersionDescription, bool ForcedManualDownload = false) + { + if (ForcedManualDownload) + { + ModMain.MyMsgBox( + $"PCL 未找到 {VersionDescription}。" + "\r\n" + + $"请自行搜索并安装 {VersionDescription},安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", "未找到 Java"); + return false; + } + + return ModMain.MyMsgBox( + $"PCL 未找到 {VersionDescription},是否需要 PCL 自动下载?" + "\r\n" + + $"如果你已经安装了 {VersionDescription},可以在 设置 → 启动选项 → 游戏 Java 中手动导入。", "自动下载 Java?", "自动下载", "取消") == 1; + } + + /// + /// 获取下载 Java 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。 + /// + public static ModLoader.LoaderCombo GetJavaDownloadLoader() + { + var JavaDownloadLoader = new ModNet.LoaderDownload("下载 Java 文件", new List()) + { ProgressWeight = 10d }; + var Loader = new ModLoader.LoaderCombo("下载 Java", + new ModLoader.LoaderBase[] + { + new ModLoader.LoaderTask>("获取 Java 下载信息", JavaFileList) + { ProgressWeight = 2d }, + JavaDownloadLoader + }); + JavaDownloadLoader.OnStateChangedThread += (Raw, NewState, OldState) => + { + if ((NewState == ModBase.LoadState.Failed || NewState == ModBase.LoadState.Aborted) && + LastJavaBaseDir is not null) + { + ModBase.Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", ModBase.LogLevel.Debug); + ModBase.DeleteDirectory(LastJavaBaseDir); + } + else if (NewState == ModBase.LoadState.Finished) + { + Javas.ScanJavaAsync().GetAwaiter().GetResult(); + LastJavaBaseDir = null; + } + }; + JavaDownloadLoader.HasOnStateChangedThread = true; + return Loader; + } + + private static string LastJavaBaseDir; // 用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java + + private static readonly HashSet IgnoreHash = new[] + { + "12976a6c2b227cbac58969c1455444596c894656", "c80e4bab46e34d02826eab226a4441d0970f2aba", + "84d2102ad171863db04e7ee22a259d1f6c5de4a5" + }.ToHashSet(); + + private static void JavaFileList(ModLoader.LoaderTask> Loader) + { + ModBase.Log("[Java] 开始获取 Java 下载信息"); + var IndexFileStr = ModNet.NetGetCodeByLoader( + ModDownload.DlVersionListOrder( + new[] + { + "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json" + }, + new[] + { + "https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json" + }), IsJson: true); + // 查找要下载的目标 Java + JProperty TargetEntry = null; + var Components = + (JObject)((JObject)ModBase.GetJson(IndexFileStr))[$"windows-x{(ModBase.Is32BitSystem ? "86" : "64")}"]; + if (Components.ContainsKey(Loader.Input)) // 精确匹配 + { + TargetEntry = Components.Property(Loader.Input); + } + else // 模糊匹配 + { + TargetEntry = Components.Properties().FirstOrDefault(c => + c.Value?.ToArray().FirstOrDefault()?["version"]["name"].ToString().StartsWithF(Loader.Input) ?? false); + if (TargetEntry is null) + throw new Exception($"未能找到所需的 Java {Loader.Input}"); + } + + var TargetComponent = TargetEntry.Value.ToArray().FirstOrDefault(); + if (TargetComponent is null) + throw new Exception($"Mojang 未提供所需的 Java {Loader.Input}"); + // 获取文件列表 + var Address = (string)TargetComponent["manifest"]["url"]; + ModLaunch.McLaunchLog($"准备下载 Java {TargetComponent["version"]["name"]}({TargetEntry.Name}):{Address}"); + var ListFileStr = (JObject)ModNet.NetGetCodeByRequestRetry( + ModDownload.DlSourceOrder(new[] { Address }, + new[] { Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com") }).First(), IsJson: true); + LastJavaBaseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + ".minecraft", "runtime", TargetEntry.Name); + var Results = new List(ListFileStr["files"].Count()); + foreach (JProperty File in ListFileStr["files"]) + { + if (((JObject)File.Value)["downloads"]?["raw"] is null) + continue; + + var Info = (JObject)((JObject)File.Value)["downloads"]["raw"]; + var checkHash = Info["sha1"]; + if (IgnoreHash.Contains((string)checkHash)) + continue; // 跳过 3 个无意义大量重复文件(#3827) + + var Checker = new ModBase.FileChecker(ActualSize: (long)Info["size"], Hash: (string)Info["sha1"]); + var filePath = Path.GetFullPath(Path.Combine(LastJavaBaseDir, File.Name)); + if (!Files.IsPathWithinDirectory(filePath, LastJavaBaseDir)) + throw new Exception($"{filePath} 不在 {LastJavaBaseDir} 中"); + + if (Checker.Check(filePath) is null) + continue; // 跳过已存在的文件 + var Url = (string)Info["url"]; + Results.Add(new ModNet.NetFile( + ModDownload.DlSourceOrder(new[] { Url }, + new[] { Url.Replace("piston-data.mojang.com", "bmclapi2.bangbang93.com") }), filePath, Checker)); + } + + Loader.Output = Results; + ModBase.Log($"[Java] 需要下载 {Results.Count} 个文件,目标文件夹:{LastJavaBaseDir}"); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs new file mode 100644 index 000000000..04caa9a90 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs @@ -0,0 +1,3531 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json.Nodes; +using System.Windows; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Minecraft; +using PCL.Core.Minecraft.Launch.Utils; +using PCL.Core.Utils; +using PCL.Core.Utils.OS; +using PCL.Core.Utils.Secret; + +namespace PCL; + +public static class ModLaunch +{ + #region 内存优化 + + private static void McLaunchMemoryOptimize(ModLoader.LoaderTask Loader) + { + McLaunchLog("内存优化开始"); + var Finished = false; + ModBase.RunInNewThread(() => + { + PageToolsTest.MemoryOptimize(false); + Finished = true; + }, "Launch Memory Optimize"); + while (!Finished && !Loader.IsAborted) + { + if (Loader.Progress < 0.7d) + Loader.Progress += 0.007d; // 10s + else + Loader.Progress += (0.95d - Loader.Progress) * 0.02d; // 最快 += 0.005 + + Thread.Sleep(100); + } + } + + #endregion + + #region 预检测 + + private static void McLaunchPrecheck() + { + if (Conversions.ToBoolean(Config.Debug.AddRandomDelay)) + Thread.Sleep(RandomUtils.NextInt(100, 2000)); + // 检查路径 + if (ModMinecraft.McInstanceSelected.PathIndie.Contains("!") || + ModMinecraft.McInstanceSelected.PathIndie.Contains(";")) + throw new Exception("游戏路径中不可包含 ! 或 ;(" + ModMinecraft.McInstanceSelected.PathIndie + ")"); + if (ModMinecraft.McInstanceSelected.PathInstance.Contains("!") || + ModMinecraft.McInstanceSelected.PathInstance.Contains(";")) + throw new Exception("游戏路径中不可包含 ! 或 ;(" + ModMinecraft.McInstanceSelected.PathInstance + ")"); + if (Conversions.ToBoolean(ModBase.IsUtf8CodePage() && !(bool)States.Hint.NonAsciiGamePath && + !ModMinecraft.McInstanceSelected.PathInstance.IsASCII())) + { + var userChoice = ModMain.MyMsgBox( + $"欲启动实例 \"{ModMinecraft.McInstanceSelected.Name}\" 的路径中存在可能影响游戏正常运行的字符(非 ASCII 字符),是否仍旧启动游戏?{"\r\n"}{"\r\n"}如果不清楚具体作用,你可以先选择 \"继续\",发现游戏在启动后很快出现崩溃的情况后再尝试修改游戏路径等操作", + "游戏路径检查", "继续", "返回处理", "不再提示"); + if (userChoice == 2) throw new Exception("$$"); + if (userChoice == 3) States.Hint.NonAsciiGamePath = true; + } + + // 检查实例 + if (ModMinecraft.McInstanceSelected is null) + throw new Exception("未选择 Minecraft 实例!"); + ModMinecraft.McInstanceSelected.Load(); + if (ModMinecraft.McInstanceSelected.State == ModMinecraft.McInstanceState.Error) + throw new Exception("Minecraft 存在问题:" + ModMinecraft.McInstanceSelected.Desc); + // 检查输入信息 + var CheckResult = ""; + ModBase.RunInUiWait(() => CheckResult = Conversions.ToString(ModProfile.IsProfileValid())); + if (ModProfile.SelectedProfile is null) // 没选档案 + { + CheckResult = "请先选择一个档案再启动游戏!"; + } + else if (ModMinecraft.McInstanceSelected.Info.HasLabyMod || Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual( + ModBase.Setup.Get("VersionServerLoginRequire", ModMinecraft.McInstanceSelected), 1, + false))) // 要求正版验证 + { + if (!(ModProfile.SelectedProfile.Type == McLoginType.Ms)) CheckResult = "当前实例要求使用正版验证,请使用正版验证档案启动游戏!"; + } + else if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual( + ModBase.Setup.Get("VersionServerLoginRequire", ModMinecraft.McInstanceSelected), 2, + false))) // 要求第三方验证 + { + if (!(ModProfile.SelectedProfile.Type == McLoginType.Auth)) + CheckResult = "当前实例要求使用第三方验证,请使用第三方验证档案启动游戏!"; + else if (Conversions.ToBoolean(!Operators.ConditionalCompareObjectEqual( + ModProfile.SelectedProfile.Server.BeforeLast("/authserver"), + ModBase.Setup.Get("VersionServerAuthServer", ModMinecraft.McInstanceSelected), false))) + CheckResult = "当前档案使用的第三方验证服务器与实例要求使用的不一致,请使用符合要求的档案启动游戏!"; + } + else if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual( + ModBase.Setup.Get("VersionServerLoginRequire", ModMinecraft.McInstanceSelected), 3, + false))) // 要求正版验证或第三方验证 + { + if (ModProfile.SelectedProfile.Type == McLoginType.Legacy) + CheckResult = "当前实例要求使用正版验证或第三方验证,请使用符合要求的档案启动游戏!"; + else if (Conversions.ToBoolean(ModProfile.SelectedProfile.Type == McLoginType.Auth && + !Operators.ConditionalCompareObjectEqual( + ModProfile.SelectedProfile.Server.BeforeLast("/authserver"), + ModBase.Setup.Get("VersionServerAuthServer", + ModMinecraft.McInstanceSelected), false))) + CheckResult = "当前档案使用的第三方验证服务器与实例要求使用的不一致,请使用符合要求的档案启动游戏!"; + } + + if (!string.IsNullOrEmpty(CheckResult)) + throw new ArgumentException(CheckResult); + +#if BETA + if (CurrentLaunchOptions?.SaveBatch == null) // 保存脚本时不提示 + { + RunInNewThread(() => + { + switch ((int)States.System.LaunchCount) + { + case 10: + case 20: + case 40: + case 60: + case 80: + case 100: + case 120: + case 150: + case 200: + case 250: + case 300: + case 350: + case 400: + case 500: + case 600: + case 700: + case 800: + case 900: + case 1000: + case 1200: + case 1400: + case 1600: + case 1800: + case 2000: + if (ModMain.MyMsgBox( + "PCL 已经为你启动了 " + Setup.Get("SystemLaunchCount") + " 次游戏啦!\n" + + "如果 PCL 还算好用的话,也许可以考虑赞助一下 PCL 原作者……\n" + + "如果没有大家的支持,PCL 很难在免费、无任何广告的情况下维持数年的更新(磕头)……!", + Setup.Get("SystemLaunchCount") + " 次启动!", + "支持一下!", + "但是我拒绝") == 1) + { + OpenWebsite("https://afdian.com/a/LTCat"); + } + break; + } + }, "Donate"); + } +#endif + + // 正版购买提示 + if (!ModProfile.ProfileList.Any(x => x.Type == McLoginType.Ms)) + { + if (RegionUtils.IsRestrictedFeatAllowed) + { + if (ModMain.MyMsgBox( + $"看起来你似乎没买正版...{"\r\n"}如果觉得 Minecraft 还不错,可以购买正版支持一下,毕竟开发游戏也真的很不容易...不要一直白嫖啦。{"\r\n"}{"\r\n"}在验证一个正版账号之后,就不会出现这个提示了!", + "考虑一下正版?", "支持正版游戏!", "下次一定") == + 1) + ModBase.OpenWebsite( + "https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"); + } + else + { + switch (ModMain.MyMsgBox("你必须先登录正版账号才能启动游戏!", "正版验证", "购买正版", "试玩", "返回", + Button1Action: () => + ModBase.OpenWebsite( + "https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"))) + { + case 2: + { + ModMain.Hint("游戏将以试玩模式启动!", ModMain.HintType.Critical); + CurrentLaunchOptions.ExtraArgs.Add("--demo"); + break; + } + case 3: + { + throw new Exception("$$"); + } + } + } + } + } + + #endregion + + #region 开始 + + public static bool IsLaunching; + public static McLaunchOptions CurrentLaunchOptions; + + public class McLaunchOptions + { + /// + /// 额外的启动参数。 + /// + public List ExtraArgs = new(); + + /// + /// 强行指定启动的 MC 实例。 + /// 默认值:Nothing。使用 McInstanceCurrent。 + /// + public ModMinecraft.McInstance Instance = null; + + /// + /// 是否为 “测试游戏” 按钮启动的游戏。 + /// 如果是,则显示游戏实时日志。 + /// + public bool IsTest = false; + + /// + /// 将启动脚本保存到该地址,然后取消启动。这同时会改变启动时的提示等。 + /// 默认值:Nothing。不保存。 + /// + public string SaveBatch = null; + + /// + /// 强制指定在启动后进入的服务器 IP。 + /// 默认值:Nothing。使用实例设置的值。 + /// + public string ServerIp = null; + + /// + /// 指定在启动之后进入的存档名称。 + /// 默认值:Nothing。使用实例设置的值。 + /// + public string WorldName = null; + } + + /// + /// 尝试启动 Minecraft。必须在 UI 线程调用。 + /// 返回是否实际开始了启动(如果没有,则一定弹出了错误提示)。 + /// + public static bool McLaunchStart(McLaunchOptions Options = null) + { + IsLaunching = true; + CurrentLaunchOptions = Options ?? new McLaunchOptions(); + // 预检查 + if (!ModBase.RunInUi()) + throw new Exception("McLaunchStart 必须在 UI 线程调用!"); + if (McLaunchLoader.State == ModBase.LoadState.Loading) + { + ModMain.Hint("已有游戏正在启动中!", ModMain.HintType.Critical); + IsLaunching = false; + return false; + } + + // 强制切换需要启动的实例 + if (CurrentLaunchOptions.Instance is not null && + ModMinecraft.McInstanceSelected != CurrentLaunchOptions.Instance) + { + McLaunchLog("在启动前切换到实例 " + CurrentLaunchOptions.Instance.Name); + // 检查实例 + CurrentLaunchOptions.Instance.Load(); + if (CurrentLaunchOptions.Instance.State == ModMinecraft.McInstanceState.Error) + { + ModMain.Hint("无法启动 Minecraft:" + CurrentLaunchOptions.Instance.Desc, ModMain.HintType.Critical); + IsLaunching = false; + return false; + } + + // 切换实例 + ModMinecraft.McInstanceSelected = CurrentLaunchOptions.Instance; + States.Game.SelectedInstance = ModMinecraft.McInstanceSelected.Name; + ModMain.FrmLaunchLeft.RefreshButtonsUI(); + ModMain.FrmLaunchLeft.RefreshPage(false); + } + + ModMain.FrmMain.AprilGiveup(); + // 禁止进入实例选择页面(否则就可以在启动中切换 McInstanceCurrent 了) + ModMain.FrmMain.PageStack = + ModMain.FrmMain.PageStack.Where(p => p.Page != FormMain.PageType.InstanceSelect).ToList(); + // 实际启动加载器 + McLaunchLoader.Start(Options, true); + return true; + } + + /// + /// 记录启动日志。 + /// + public static void McLaunchLog(string Text) + { + Text = ModMinecraft.FilterUserName(ModMinecraft.FilterAccessToken(Text, '*'), '*'); + ModBase.RunInUi(() => + ModMain.FrmLaunchRight.LabLog.Text += "\r\n" + "[" + TimeUtils.GetTimeNow() + "] " + Text); + ModBase.Log("[Launch] " + Text); + } + + // 启动状态切换 + public static ModLoader.LoaderTask McLaunchLoader = new("Loader Launch", McLaunchStart) + { OnStateChanged = a => McLaunchState((dynamic)a) }; + + public static ModLoader.LoaderCombo McLaunchLoaderReal; + public static Process McLaunchProcess; + public static ModWatcher.Watcher McLaunchWatcher; + + private static void McLaunchState(ModLoader.LoaderTask Loader) + { + switch (McLaunchLoader.State) + { + case ModBase.LoadState.Finished: + case ModBase.LoadState.Failed: + case ModBase.LoadState.Waiting: + case ModBase.LoadState.Aborted: + { + ModMain.FrmLaunchLeft.PageChangeToLogin(); + break; + } + case ModBase.LoadState.Loading: + { + // 在预检测结束后再触发动画 + ModMain.FrmLaunchRight.LabLog.Text = ""; + break; + } + } + } + + /// + /// 指定启动中断时的提示文本。若不为 Nothing 则会显示为绿色。 + /// + private static string AbortHint; + + // 实际的启动方法 + private static void McLaunchStart(ModLoader.LoaderTask Loader) + { + // 开始动画 + ModBase.RunInUiWait(ModMain.FrmLaunchLeft.PageChangeToLaunching); + // 预检测(预检测的错误将直接抛出) + try + { + McLaunchPrecheck(); + McLaunchLog("预检测已通过"); + } + catch (Exception ex) + { + if (!ex.Message.StartsWithF("$$")) + ModMain.Hint(ex.Message, ModMain.HintType.Critical); + throw; + } + + // 正式加载 + try + { + // 构造主加载器 + var Loaders = new List + { + new ModLoader.LoaderTask("获取 Java", McLaunchJava) { ProgressWeight = 4d, Block = false }, + McLoginLoader, + new ModLoader.LoaderCombo("补全文件", + ModDownload.DlClientFix(ModMinecraft.McInstanceSelected, false, + ModDownload.AssetsIndexExistsBehaviour.DownloadInBackground)) + { ProgressWeight = 15d, Show = false }, + new ModLoader.LoaderTask>("获取启动参数", McLaunchArgumentMain) + { ProgressWeight = 2d }, + new ModLoader.LoaderTask, int>("解压文件", McLaunchNatives) + { ProgressWeight = 2d }, + new ModLoader.LoaderTask("预启动处理", _ => McLaunchPrerun()) { ProgressWeight = 1d }, + new ModLoader.LoaderTask("执行自定义命令", McLaunchCustom) { ProgressWeight = 1d }, + new ModLoader.LoaderTask("启动进程", McLaunchRun) { ProgressWeight = 2d }, + new ModLoader.LoaderTask("等待游戏窗口出现", McLaunchWait) { ProgressWeight = 1d }, + new ModLoader.LoaderTask("结束处理", _ => McLaunchEnd()) { ProgressWeight = 1d } + }; // .ProgressWeight = 15, .Block = False + // 内存优化 + switch (ModBase.Setup.Get("VersionRamOptimize", ModMinecraft.McInstanceSelected)) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 全局 + { + if (Conversions.ToBoolean(Config.Launch.OptimizeMemory)) // 使用全局设置 + { + ((ModLoader.LoaderCombo)Loaders[2]).Block = false; + Loaders.Insert(3, + new ModLoader.LoaderTask("内存优化", McLaunchMemoryOptimize) + { ProgressWeight = 30d }); + } + + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): // 开启 + { + ((ModLoader.LoaderCombo)Loaders[2]).Block = false; + Loaders.Insert(3, + new ModLoader.LoaderTask("内存优化", McLaunchMemoryOptimize) { ProgressWeight = 30d }); + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 2, false): // 关闭 + { + break; + } + } + + var LaunchLoader = new ModLoader.LoaderCombo("Minecraft 启动", Loaders) { Show = false }; + if (McLoginLoader.State == ModBase.LoadState.Finished) + McLoginLoader.State = ModBase.LoadState.Waiting; // 要求重启登录主加载器,它会自行决定是否启动副加载器 + // 等待加载器执行并更新 UI + McLaunchLoaderReal = LaunchLoader; + AbortHint = null; + LaunchLoader.Start(); + // 任务栏进度条 + ModLoader.LoaderTaskbarAdd(LaunchLoader); + while (LaunchLoader.State == ModBase.LoadState.Loading) + { + ModMain.FrmLaunchLeft.Dispatcher.Invoke(ModMain.FrmLaunchLeft.LaunchingRefresh); + Thread.Sleep(100); + } + + ModMain.FrmLaunchLeft.Dispatcher.Invoke(ModMain.FrmLaunchLeft.LaunchingRefresh); + // 成功与失败处理 + switch (LaunchLoader.State) + { + case ModBase.LoadState.Finished: + { + ModMain.Hint(ModMinecraft.McInstanceSelected.Name + " 启动成功!", ModMain.HintType.Finish); + break; + } + case ModBase.LoadState.Aborted: + { + if (AbortHint is null) + ModMain.Hint(CurrentLaunchOptions?.SaveBatch is null ? "已取消启动!" : "已取消导出启动脚本!"); + else + ModMain.Hint(AbortHint, ModMain.HintType.Finish); + + break; + } + case ModBase.LoadState.Failed: + { + throw LaunchLoader.Error; + } + + default: + { + throw new Exception("错误的状态改变:" + ModBase.GetStringFromEnum(LaunchLoader.State)); + } + } + + IsLaunching = false; + } + catch (Exception ex) + { + var CurrentEx = ex; + NextInner: ; + + if (CurrentEx.Message.StartsWithF("$")) + { + // 若有以 $ 开头的错误信息,则以此为准显示提示 + // 若错误信息为 $$,则不提示 + if (!(CurrentEx.Message == "$$")) + ModMain.MyMsgBox(CurrentEx.Message.TrimStart('$'), + CurrentLaunchOptions?.SaveBatch is null ? "启动失败" : "导出启动脚本失败"); + throw; + } + + if (CurrentEx.InnerException is not null) + { + // 检查下一级错误 + CurrentEx = CurrentEx.InnerException; + goto NextInner; + } + + // 没有特殊处理过的错误信息 + McLaunchLog("错误:" + ex); + ModBase.Log(ex, CurrentLaunchOptions?.SaveBatch is null ? "Minecraft 启动失败" : "导出启动脚本失败", + ModBase.LogLevel.Msgbox, CurrentLaunchOptions?.SaveBatch is null ? "启动失败" : "导出启动脚本失败"); + throw; + } + } + + #endregion + + #region 档案验证 + + #region 主模块 + + // 登录方式 + public enum McLoginType + { + Legacy = 1, + Auth = 2, + Ms = 3 + } + + // 各个登录方式的对应数据 + public abstract class McLoginData + { + /// + /// 登录方式。 + /// + public McLoginType Type; + + public override bool Equals(object obj) + { + return obj is not null && obj.GetHashCode() == GetHashCode(); + } + } + + #region 第三方验证类型 + + public class McLoginServer : McLoginData + { + /// + /// 登录服务器基础地址。 + /// + public string BaseUrl; + + /// + /// 登录方式的描述字符串,如 “正版”、“统一通行证”。 + /// + public string Description; + + /// + /// 是否在本次登录中强制要求玩家重新选择角色,目前仅对 Authlib-Injector 生效。 + /// + public bool ForceReselectProfile = false; + + /// + /// 是否已经存在该验证信息,用于判断是否为新增档案。 + /// + public bool IsExist = false; + + /// + /// 登录密码。 + /// + public string Password; + + /// + /// 登录用户名。 + /// + public string UserName; + + public McLoginServer(McLoginType Type) + { + this.Type = Type; + } + + public override int GetHashCode() + { + return (int)Math.Round(ModBase.GetHash(UserName + Password + BaseUrl + (int)Type) % + (decimal)int.MaxValue); + } + } + + #endregion + + #region 正版验证类型 + + public class McLoginMs : McLoginData + { + public string AccessToken = ""; + + /// + /// 缓存的 OAuth RefreshToken。若没有则为空字符串。 + /// + public string OAuthRefreshToken = ""; + + public string ProfileJson = ""; + public string UserName = ""; + public string Uuid = ""; + + public McLoginMs() + { + Type = McLoginType.Ms; + } + + public override int GetHashCode() + { + return (int)Math.Round(ModBase.GetHash(OAuthRefreshToken + AccessToken + Uuid + UserName + ProfileJson) % + (decimal)int.MaxValue); + } + } + + #endregion + + #region 离线验证类型 + + public class McLoginLegacy : McLoginData + { + /// + /// 若采用正版皮肤,则为该皮肤名。 + /// + public string SkinName; + + /// + /// 皮肤种类。 + /// + public int SkinType; + + /// + /// 登录用户名。 + /// + public string UserName; + + /// + /// UUID。 + /// + public string Uuid; + + public McLoginLegacy() + { + Type = McLoginType.Legacy; + } + + public override int GetHashCode() + { + return (int)Math.Round( + ModBase.GetHash(UserName + SkinType + SkinName + (int)Type) % (decimal)int.MaxValue); + } + } + + #endregion + + // 登录返回结果 + public struct McLoginResult + { + public string Name; + public string Uuid; + public string AccessToken; + public string Type; + public string ClientToken; + + /// + /// 进行微软登录时返回的 profile 信息。 + /// + public string ProfileJson; + } + + // 登录主模块加载器 + public static ModLoader.LoaderTask McLoginLoader = + new("登录", McLoginStart, McLoginInput, ThreadPriority.BelowNormal) + { ReloadTimeout = 1, ProgressWeight = 15d, Block = false }; + + public static McLoginData McLoginInput() + { + McLoginData LoginData = null; + try + { + LoginData = ModProfile.GetLoginData(); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取登录输入信息失败", ModBase.LogLevel.Feedback); + } + + return LoginData; + } + + private static void McLoginStart(ModLoader.LoaderTask Data) + { + ModBase.Log("[Profile] 开始加载选定档案"); + // 校验登录信息 + var CheckResult = Conversions.ToString(ModProfile.IsProfileValid()); + if (!string.IsNullOrEmpty(CheckResult)) + throw new ArgumentException(CheckResult); + // 获取对应加载器 + ModLoader.LoaderBase Loader = null; + switch (Data.Input.Type) + { + case McLoginType.Ms: + { + Loader = McLoginMsLoader; + break; + } + case McLoginType.Legacy: + { + Loader = McLoginLegacyLoader; + break; + } + case McLoginType.Auth: + { + Loader = McLoginAuthLoader; + break; + } + } + + // 尝试加载 + Loader.WaitForExit(Data.Input, McLoginLoader, Data.IsForceRestarting); + Data.Output = (McLoginResult)((dynamic)Loader).Output; + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(false)); // 刷新自动填充列表 + ModBase.Log("[Profile] 选定档案加载完成"); + } + + #endregion + + // 各个登录方式的主对象与输入构造 + public static ModLoader.LoaderTask McLoginMsLoader = + new("Loader Login Ms", McLoginMsStart) { ReloadTimeout = 1 }; + + public static ModLoader.LoaderTask McLoginLegacyLoader = + new("Loader Login Legacy", McLoginLegacyStart); + + public static ModLoader.LoaderTask McLoginAuthLoader = + new("Loader Login Auth", McLoginServerStart) { ReloadTimeout = 1000 * 60 * 10 }; + + // 主加载函数,返回所有需要的登录信息 + private static long McLoginMsRefreshTime; // 上次刷新登录的时间 + + #region 正版验证 + + private static void McLoginMsStart(ModLoader.LoaderTask data) + { + var input = data.Input; + var logUsername = input.UserName; + var isNewProfile = true; + + ModProfile.ProfileLog($"验证方式:正版({(string.IsNullOrEmpty(logUsername) ? "尚未登录" : logUsername)})"); + data.Progress = 0.05d; + + // 已登录且不需要强制重启且登录未过期 + if (!data.IsForceRestarting && !string.IsNullOrEmpty(input.AccessToken) && + McLoginMsRefreshTime > 0L && + TimeUtils.GetTimeTick() - McLoginMsRefreshTime < 1000 * 60 * 10) + { + data.Output = new McLoginResult + { + AccessToken = input.AccessToken, + Name = input.UserName, + Uuid = input.Uuid, + Type = "Microsoft", + ClientToken = input.Uuid, + ProfileJson = input.ProfileJson + }; + + McLoginMsRefreshTime = TimeUtils.GetTimeTick(); + ModProfile.ProfileLog("正版验证完成"); + return; + } + + data.Progress = 0.1d; + + // 尝试获取 OAuthToken + var oauthTokens = GetOAuthTokens(data, input, out var skipAuth); + if (skipAuth) + { + data.Progress = 0.99d; + var profile = ModProfile.SelectedProfile; + data.Output = new McLoginResult + { + AccessToken = profile.AccessToken, + Name = profile.Username, + Uuid = profile.Uuid, + Type = "Microsoft" + }; + return; + } + + var oauthAccessToken = oauthTokens[0]; + var oauthRefreshToken = oauthTokens[1]; + ThrowIfAborted(data); + + data.Progress = 0.25d; + + // Step 2: XBL Token + var xblToken = MsLoginStep2(oauthAccessToken); + if (string.IsNullOrEmpty(xblToken) || xblToken == "Ignore") + goto SkipLogin; + + data.Progress = 0.4d; + ThrowIfAborted(data); + + // Step 3: XSTS / Minecraft login + var tokens = MsLoginStep3(xblToken); + if (tokens.Length < 2 || tokens[1] == "Ignore") + goto SkipLogin; + + data.Progress = 0.55d; + ThrowIfAborted(data); + + // Step 4: Final access token + var accessToken = MsLoginStep4(tokens); + if (string.IsNullOrEmpty(accessToken) || accessToken == "Ignore") + goto SkipLogin; + + data.Progress = 0.7d; + ThrowIfAborted(data); + + // Step 5: Additional setup + MsLoginStep5(accessToken); + data.Progress = 0.85d; + ThrowIfAborted(data); + + // Step 6: Profile info + var result = MsLoginStep6(accessToken); + if (result.Length < 3 || result[2] == "Ignore") + goto SkipLogin; + + data.Progress = 0.98d; + + // 检查是否已有相同档案 + foreach (var profile in ModProfile.ProfileList) + if (profile.Type == McLoginType.Ms && + string.Equals(profile.Username, result[1], StringComparison.Ordinal) && + string.Equals(profile.Uuid, result[0], StringComparison.Ordinal)) + { + isNewProfile = false; + if (ModProfile.IsCreatingProfile) + { + var index = ModProfile.ProfileList.IndexOf(profile); + ModProfile.ProfileList[index].Username = result[1]; + ModProfile.ProfileList[index].AccessToken = accessToken; + ModProfile.ProfileList[index].RefreshToken = oauthRefreshToken; + ModMain.Hint("你已经添加了这个档案..."); + goto SkipLogin; + } + } + + // 输出登录结果 + if (isNewProfile) + { + var newProfile = new ModProfile.McProfile + { + Type = McLoginType.Ms, + Uuid = result[0], + Username = result[1], + AccessToken = accessToken, + RefreshToken = oauthRefreshToken, + Expires = 1743779140286L, + Desc = "", + RawJson = result[2] + }; + ModProfile.ProfileList.Add(newProfile); + ModProfile.SelectedProfile = newProfile; + ModProfile.IsCreatingProfile = false; + } + else + { + var index = ModProfile.ProfileList.IndexOf(ModProfile.SelectedProfile); + ModProfile.ProfileList[index].Username = result[1]; + ModProfile.ProfileList[index].AccessToken = accessToken; + ModProfile.ProfileList[index].RefreshToken = oauthRefreshToken; + } + + ModProfile.SaveProfile(); + + data.Output = new McLoginResult + { + AccessToken = accessToken, + Name = result[1], + Uuid = result[0], + Type = "Microsoft", + ClientToken = result[0], + ProfileJson = result[2] + }; + + SkipLogin: + McLoginMsRefreshTime = TimeUtils.GetTimeTick(); + ModProfile.ProfileLog("正版验证完成"); + } + + /// + /// 获取 OAuth Tokens,处理刷新和重新登录逻辑 + /// + private static string[] GetOAuthTokens(ModLoader.LoaderTask data, McLoginMs input, + out bool skipAuth) + { + skipAuth = false; + string[] tokens; + + while (true) + { + if (string.IsNullOrEmpty(input.OAuthRefreshToken)) + { + tokens = MsLoginStep1New(data); + } + else + { + tokens = MsLoginStep1Refresh(input.OAuthRefreshToken); + if (tokens.Length > 0 && tokens[0] == "Relogin") + continue; // 重新登录 + } + + if (tokens.Length > 0 && tokens[0] == "Ignore") + { + skipAuth = true; + return tokens; + } + + return tokens; + } + } + + /// + /// 检查是否被中断 + /// + private static void ThrowIfAborted(ModLoader.LoaderTask data) + { + if (data.IsAborted) + throw new ThreadInterruptedException(); + } + + /// + /// 正版验证步骤 1:通过设备代码流获取账号信息 + /// + /// OAuth 验证完成的返回结果 + private static string[] MsLoginStep1New(ModLoader.LoaderTask Data) + { + // 参考:https://learn.microsoft.com/zh-cn/entra/identity-platform/v2-oauth2-device-code + + // 初始请求 + Retry: ; + + McLaunchLog("开始正版验证 Step 1/6(原始登录)"); + JObject PrepareJson; + using (var response = HttpRequestBuilder + .Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", HttpMethod.Post) + .WithContent( + new ByteArrayContent(Encoding.UTF8.GetBytes( + $"client_id={ModSecret.OAuthClientId}&tenant=/consumers&scope=XboxLive.signin%20offline_access")), + "application/x-www-form-urlencoded").SendAsync(true).GetAwaiter().GetResult()) + { + PrepareJson = (JObject)ModBase.GetJson(response.AsStringContent()); + } + + McLaunchLog("网页登录地址:" + PrepareJson["verification_uri"]); + + // 弹窗 + var Converter = new ModMain.MyMsgBoxConverter + { Content = PrepareJson, ForceWait = true, Type = ModMain.MyMsgBoxType.Login }; + ModMain.WaitingMyMsgBox.Add(Converter); + while (Converter.Result is null) + Thread.Sleep(100); + if (Converter.Result is ModBase.RestartException) + { + if (ModMain.MyMsgBox( + $"请在登录时选择 {ModBase.vbLQ}其他登录方法{ModBase.vbRQ},然后选择 {ModBase.vbLQ}使用我的密码{ModBase.vbRQ}。{"\r\n"}如果没有该选项,请选择 {ModBase.vbLQ}设置密码{ModBase.vbRQ},设置完毕后再登录。", + "需要使用密码登录", "重新登录", "设置密码", "取消", + Button2Action: () => ModBase.OpenWebsite("https://account.live.com/password/Change")) == + 1) goto Retry; + + throw new Exception("$$"); + } + + if (Converter.Result is Exception) throw (Exception)Converter.Result; + + return (string[])Converter.Result; + } + + /// + /// 正版验证步骤 1,刷新登录:从 OAuth Code 或 OAuth RefreshToken 获取 {OAuth accessToken, OAuth RefreshToken} + /// + /// + /// + private static string[] MsLoginStep1Refresh(string Code) + { + McLaunchLog("开始正版验证 Step 1/6(刷新登录)"); + if (string.IsNullOrEmpty(Code)) + throw new ArgumentException("传入的 Code 为空", nameof(Code)); + string Result = null; + try + { + using (var response = HttpRequestBuilder.Create("https://login.live.com/oauth20_token.srf", HttpMethod.Post) + .WithContent( + $"client_id={ModSecret.OAuthClientId}&refresh_token={Uri.EscapeDataString(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access", + "application/x-www-form-urlencoded").SendAsync(true).GetAwaiter().GetResult()) + { + Result = response.AsStringContent(); + } + } + catch (ThreadInterruptedException ex) + { + ModBase.Log(ex, "加载线程已终止"); + } + catch (Exception ex) + { + if (ex.Message.ContainsF("must sign in again", true) || ex.Message.ContainsF("password expired", true) || + (ex.Message.Contains("refresh_token") && ex.Message.Contains("is not valid"))) // #269 + return new[] { "Relogin", "" }; + + ModProfile.ProfileLog("正版验证 Step 1/6 获取 OAuth Token 失败:" + ex); + var IsIgnore = false; + ModBase.RunInUiWait(() => + { + if (!IsLaunching) + return; + if (ModMain.MyMsgBox( + $"启动器在尝试刷新账号信息时遇到了网络错误。{"\r\n"}你可以选择取消,检查网络后再次启动,也可以选择忽略错误继续启动,但可能无法游玩部分服务器。", + "账号信息获取失败", "继续", "取消") == 1) + IsIgnore = true; + }); + if (IsIgnore) return new[] { "Ignore", "" }; + } + + var ResultJson = (JObject)ModBase.GetJson(Result); + var AccessToken = ResultJson["access_token"].ToString(); + var RefreshToken = ResultJson["refresh_token"].ToString(); + return new[] { AccessToken, RefreshToken }; + } + + + private class XBLTokenRequestData + { + public PropertiesData Properties { get; set; } + public string RelyingParty { get; set; } + public string TokenType { get; set; } + + public class PropertiesData + { + public string AuthMethod { get; set; } + public string SiteName { get; set; } + public string RpsTicket { get; set; } + } + } + + /// + /// 正版验证步骤 2:从 OAuth accessToken 获取 XBLToken + /// + /// OAuth accessToken + /// XBLToken + private static string MsLoginStep2(string accessToken) + { + ModProfile.ProfileLog("开始正版验证 Step 2/6: 获取 XBLToken"); + if (string.IsNullOrEmpty(accessToken)) + throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken)); + var requestData = new XBLTokenRequestData + { + Properties = new XBLTokenRequestData.PropertiesData + { + AuthMethod = "RPS", + SiteName = "user.auth.xboxlive.com", + RpsTicket = $"d={accessToken}" + }, + RelyingParty = "http://auth.xboxlive.com", + TokenType = "JWT" + }; + string Result = null; + try + { + using (var response = HttpRequestBuilder + .Create("https://user.auth.xboxlive.com/user/authenticate", HttpMethod.Post) + .WithJsonContent(requestData).SendAsync(true).GetAwaiter().GetResult()) + { + Result = response.AsStringContent(); + } + } + catch (Exception ex) + { + ModProfile.ProfileLog("正版验证 Step 2/6 获取 XBLToken 失败:" + ex); + var IsIgnore = false; + ModBase.RunInUiWait(() => + { + if (!IsLaunching) + return; + if (ModMain.MyMsgBox( + $"启动器在尝试刷新账号信息时(Step 2)遇到了网络错误。{"\r\n"}你可以选择取消,检查网络后再次启动,也可以选择忽略错误继续启动,但可能无法游玩部分服务器。", + "账号信息获取失败", "继续", "取消") == 1) + IsIgnore = true; + }); + if (IsIgnore) return "Ignore"; + } + + var ResultJson = (JObject)ModBase.GetJson(Result); + var XBLToken = ResultJson["Token"].ToString(); + return XBLToken; + } + + + private class XSTSTokenRequestData + { + public PropertiesData Properties { get; set; } + public string RelyingParty { get; set; } + public string TokenType { get; set; } + + public class PropertiesData + { + public string SandboxId { get; set; } + public List UserTokens { get; set; } + } + } + + /// + /// 正版验证步骤 3:从 XBLToken 获取 {XSTSToken, UHS} + /// + /// 包含 XSTSToken 与 UHS 的字符串组 + private static string[] MsLoginStep3(string XBLToken) + { + ModProfile.ProfileLog("开始正版验证 Step 3/6: 获取 XSTSToken"); + if (string.IsNullOrEmpty(XBLToken)) + throw new ArgumentException("XBLToken 为空,无法获取数据", nameof(XBLToken)); + var requestData = new XSTSTokenRequestData + { + Properties = new XSTSTokenRequestData.PropertiesData + { + SandboxId = "RETAIL", + UserTokens = new[] { XBLToken }.ToList() + }, + RelyingParty = "rp://api.minecraftservices.com/", + TokenType = "JWT" + }; + string result; + using (var response = HttpRequestBuilder + .Create("https://xsts.auth.xboxlive.com/xsts/authorize", HttpMethod.Post) + .WithJsonContent(requestData).SendAsync().GetAwaiter().GetResult()) + { + result = response.AsStringContent(); + + if (!response.IsSuccess) + { + // 参考 https://github.com/PrismarineJS/prismarine-auth/blob/master/src/common/Constants.js + if (result.Contains("2148916227")) + { + ModMain.MyMsgBox("该账号似乎已被微软封禁,无法登录。", "登录失败", "我知道了", IsWarn: true); + throw new Exception("$$"); + } + + if (result.Contains("2148916233")) + { + if (ModMain.MyMsgBox("你尚未注册 Xbox 账户,请在注册后再登录。", "登录提示", "注册", "取消") == 1) + ModBase.OpenWebsite("https://signup.live.com/signup"); + throw new Exception("$$"); + } + + if (result.Contains("2148916235")) + { + ModMain.MyMsgBox($"你的网络所在的国家或地区无法登录微软账号。{"\r\n"}请使用加速器或 VPN。", "登录失败", "我知道了"); + throw new Exception("$$"); + } + + if (result.Contains("2148916238")) + { + if (ModMain.MyMsgBox("该账号年龄不足,你需要先修改出生日期,然后才能登录。" + "\r\n" + "该账号目前填写的年龄是否在 13 岁以上?", + "登录提示", "13 岁以上", "12 岁以下", "我不知道") == 1) + { + ModBase.OpenWebsite("https://account.live.com/editprof.aspx"); + ModMain.MyMsgBox( + "请在打开的网页中修改账号的出生日期(至少改为 18 岁以上)。" + "\r\n" + "在修改成功后等待一分钟,然后再回到 PCL,就可以正常登录了!", + "登录提示"); + } + else + { + ModBase.OpenWebsite( + "https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"); + ModMain.MyMsgBox( + "请根据打开的网页的说明,修改账号的出生日期(至少改为 18 岁以上)。" + "\r\n" + + "在修改成功后等待一分钟,然后再回到 PCL,就可以正常登录了!", "登录提示"); + } + + throw new Exception("$$"); + } + + ModProfile.ProfileLog("正版验证 Step 3/6 获取 XSTSToken 失败:" + response.StatusCode); + var IsIgnore = false; + ModBase.RunInUiWait(() => + { + if (!IsLaunching) + return; + if (ModMain.MyMsgBox( + $"启动器在尝试刷新账号信息时(Step 3)遇到了网络错误。{"\r\n"}你可以选择取消,检查网络后再次启动,也可以选择忽略错误继续启动,但可能无法游玩部分服务器。", + "账号信息获取失败", "继续", "取消") == 1) + IsIgnore = true; + }); + if (IsIgnore) + { + return new[] { ModProfile.SelectedProfile.AccessToken, "Ignore" }; + return default; + } + + response.EnsureSuccessStatusCode(); + } + } + + var ResultJson = (JObject)ModBase.GetJson(result); + var XSTSToken = ResultJson["Token"].ToString(); + var UHS = ResultJson["DisplayClaims"]["xui"][0]["uhs"].ToString(); + return new[] { XSTSToken, UHS }; + } + + /// + /// 正版验证步骤 4:从 {XSTSToken, UHS} 获取 Minecraft accessToken + /// + /// 包含 XSTSToken 与 UHS 的字符串组 + /// Minecraft accessToken + private static string MsLoginStep4(string[] Tokens) + { + ModProfile.ProfileLog("开始正版验证 Step 4/6: 获取 Minecraft AccessToken"); + if (Tokens.Length < 2 || string.IsNullOrEmpty(Tokens.ElementAt(0)) || string.IsNullOrEmpty(Tokens.ElementAt(1))) + throw new ArgumentException("传入的 XSTSToken 或者 UHS 错误", nameof(Tokens)); + var requestData = new Dictionary { { "identityToken", $"XBL3.0 x={Tokens[1]};{Tokens[0]}" } }; + string Result; + try + { + using (var response = HttpRequestBuilder + .Create("https://api.minecraftservices.com/authentication/login_with_xbox", HttpMethod.Post) + .WithJsonContent(requestData).SendAsync(true).GetAwaiter().GetResult()) + { + Result = response.AsStringContent(); + } + } + catch (HttpRequestException ex) + { + var Message = ex.Message; + if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests)) + { + ModBase.Log(ex, "正版验证 Step 4 汇报 429"); + throw new Exception("$登录尝试太过频繁,请等待几分钟后再试!"); + } + + if (ex.StatusCode is { } arg1 && arg1 == HttpStatusCode.Forbidden) + { + ModBase.Log(ex, "正版验证 Step 4 汇报 403"); + throw new Exception("$当前 IP 的登录尝试异常。" + "\r\n" + "如果你使用了 VPN 或加速器,请把它们关掉或更换节点后再试!"); + } + + ModProfile.ProfileLog("正版验证 Step 4/6 获取 MC AccessToken 失败:" + ex); + var IsIgnore = false; + ModBase.RunInUiWait(() => + { + if (!IsLaunching) + return; + if (ModMain.MyMsgBox( + $"启动器在尝试刷新账号信息时(Step 4)遇到了网络错误。{"\r\n"}你可以选择取消,检查网络后再次启动,也可以选择忽略错误继续启动,但可能无法游玩部分服务器。", + "账号信息获取失败", "继续", "取消") == 1) + IsIgnore = true; + }); + if (IsIgnore) + { + return "Ignore"; + return default; + } + + throw; + } + + var ResultJson = (JObject)ModBase.GetJson(Result); + var AccessToken = ResultJson["access_token"].ToString(); + if (string.IsNullOrWhiteSpace(AccessToken)) + throw new Exception("获取到的 Minecraft AccessToken 为空,登录流程异常!"); + return AccessToken; + } + + /// + /// 正版验证步骤 5:验证微软账号是否持有 MC,这也会刷新 XGP + /// + /// Minecraft accessToken + private static void MsLoginStep5(string accessToken) + { + ModProfile.ProfileLog("开始正版验证 Step 5/6: 验证账户是否持有 MC"); + if (string.IsNullOrEmpty(accessToken)) + throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken)); + var result = ""; + try + { + using (var response = HttpRequestBuilder + .Create("https://api.minecraftservices.com/entitlements/mcstore", HttpMethod.Get) + .WithBearerToken(accessToken).SendAsync(true).GetAwaiter().GetResult()) + { + result = response.AsStringContent(); + } + + var ResultJson = (JObject)ModBase.GetJson(result); + if (!(ResultJson.ContainsKey("items") && ResultJson["items"].Any(x => + x["name"]?.ToString() == "product_minecraft" || x["name"]?.ToString() == "game_minecraft"))) + { + switch (ModMain.MyMsgBox("暂时无法获取到此账户信息,此账户可能没有购买 Minecraft Java Edition 或者账户的 Xbox Game Pass 已过期", + "登录失败", "购买 Minecraft", "取消")) + { + case 1: + { + ModBase.OpenWebsite( + "https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"); + break; + } + } + + throw new Exception("$$"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "正版验证 Step 5 异常:" + result); + throw; + } + } + + /// + /// 正版验证步骤 6:从 Minecraft accessToken 获取 {UUID, UserName, ProfileJson} + /// + /// Minecraft accessToken + /// 包含 UUID, UserName 和 ProfileJson 的字符串组 + private static string[] MsLoginStep6(string AccessToken) + { + ModProfile.ProfileLog("开始正版验证 Step 6/6: 获取玩家 ID 与 UUID 等相关信息"); + if (string.IsNullOrEmpty(AccessToken)) + throw new ArgumentException("传入的 AccessToken 为空", nameof(AccessToken)); + string Result; + try + { + using (var response = HttpRequestBuilder + .Create("https://api.minecraftservices.com/minecraft/profile", HttpMethod.Get) + .WithBearerToken(AccessToken).SendAsync(true).GetAwaiter().GetResult()) + { + Result = response.AsStringContent(); + } + } + catch (HttpRequestException ex) + { + var Message = ex.Message; + if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests)) + { + ModBase.Log(ex, "正版验证 Step 6 汇报 429"); + throw new Exception("$登录尝试太过频繁,请等待几分钟后再试!"); + } + + if (ex.StatusCode is { } arg2 && arg2 == HttpStatusCode.NotFound) + { + ModBase.Log(ex, "正版验证 Step 6 汇报 404"); + ModBase.RunInNewThread(() => + { + switch (ModMain.MyMsgBox("请先创建 Minecraft 玩家档案,然后再重新登录。", "登录失败", "创建档案", "取消")) + { + case 1: + { + ModBase.OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile"); + break; + } + } + }, "Login Failed: Create Profile"); + throw new Exception("$$"); + } + + ModProfile.ProfileLog("正版验证 Step 6/6 获取玩家档案信息失败:" + ex); + var IsIgnore = false; + ModBase.RunInUiWait(() => + { + if (!IsLaunching) + return; + if (ModMain.MyMsgBox( + $"启动器在尝试刷新账号信息时(Step 6)遇到了网络错误。{"\r\n"}你可以选择取消,检查网络后再次启动,也可以选择忽略错误继续启动,但可能无法游玩部分服务器。", + "账号信息获取失败", "继续", "取消") == 1) + IsIgnore = true; + }); + if (IsIgnore) + { + return new[] { ModProfile.SelectedProfile.Uuid, ModProfile.SelectedProfile.Username, "Ignore" }; + return default; + } + + throw; + } + + var ResultJson = (JObject)ModBase.GetJson(Result); + var UUID = ResultJson["id"].ToString(); + var UserName = ResultJson["name"].ToString(); + return new[] { UUID, UserName, Result }; + } + + #endregion + + #region 第三方验证 + + private static void McLoginServerStart(ModLoader.LoaderTask data) + { + var input = data.Input; + var needRefresh = false; + var wasRefreshed = false; + + ModProfile.ProfileLog("验证方式:" + input.Description); + data.Progress = 0.05d; + + // 尝试验证登录(如果不需要重新选择档案且不是创建档案) + if (!input.ForceReselectProfile && !ModProfile.IsCreatingProfile) + { + try + { + ThrowIfAborted(data); + McLoginRequestValidate(ref data); + data.Progress = 0.95d; + return; // 登录成功,直接返回 + } + catch (ModNet.HttpWebException ex) + { + HandleHttpWebException(ex, "验证登录失败"); + } + catch (Exception ex) + { + HandleException(ex, "验证登录失败"); + } + + data.Progress = 0.25d; + + // 尝试刷新登录 + try + { + ThrowIfAborted(data); + McLoginRequestRefresh(ref data, needRefresh); + data.Progress = needRefresh ? 0.85d : 0.45d; + data.Progress = 0.95d; + return; // 刷新成功,直接返回 + } + catch (Exception ex) + { + ModProfile.ProfileLog("刷新登录失败:" + ex); + ModMain.MyMsgBox("刷新登录失败: " + ex, "第三方验证失败", IsWarn: true); + if (wasRefreshed) + throw new Exception("二轮刷新登录失败", ex); + } + } + + // 尝试普通登录 + try + { + ThrowIfAborted(data); + needRefresh = McLoginRequestLogin(ref data); + } + catch (ModNet.HttpWebException ex) + { + HandleLoginHttpException(ex); + } + catch (Exception ex) + { + HandleException(ex, "第三方验证登录失败"); + } + + // 如果需要刷新,循环刷新一次 + if (needRefresh) + { + ModProfile.ProfileLog("重新进行刷新登录"); + wasRefreshed = true; + data.Progress = 0.65d; + + try + { + ThrowIfAborted(data); + McLoginRequestRefresh(ref data, needRefresh); + data.Progress = 0.95d; + return; + } + catch (Exception ex) + { + ModProfile.ProfileLog("刷新登录失败:" + ex); + ModMain.MyMsgBox("刷新登录失败: " + ex, "第三方验证失败", IsWarn: true); + throw new Exception("二轮刷新登录失败", ex); + } + } + + // 最终完成 + data.Progress = 0.95d; + } + + /// + /// 检查任务是否被中断 + /// + private static void ThrowIfAborted(ModLoader.LoaderTask data) + { + if (data.IsAborted) + throw new ThreadInterruptedException(); + } + + /// + /// 统一处理 HttpWebException + /// + private static void HandleHttpWebException(ModNet.HttpWebException ex, string logPrefix) + { + var allMessage = ex.ToString(); + ModProfile.ProfileLog(logPrefix + ":" + allMessage); + + if ((allMessage.Contains("超时") || allMessage.Contains("imeout")) && !allMessage.Contains("403")) + { + ModProfile.ProfileLog("已触发超时登录失败"); + ModMain.MyMsgBox( + "$登录失败:连接登录服务器超时。" + "\r\n" + + "请检查你的网络状况是否良好,或尝试使用 VPN!" + "\r\n" + "\r\n" + + "详细信息:" + ex.InnerHttpException.WebResponse, + "第三方验证失败", IsWarn: true); + + throw new Exception("$登录失败:连接登录服务器超时。" + "\r\n" + + "请检查你的网络状况是否良好,或尝试使用 VPN!" + "\r\n" + + "\r\n" + "详细信息:" + ex.InnerHttpException.WebResponse); + } + } + + /// + /// 统一处理普通异常 + /// + private static void HandleException(Exception ex, string logPrefix) + { + ModProfile.ProfileLog(logPrefix + ":" + ex); + ModMain.MyMsgBox(logPrefix + ": " + ex, "第三方验证失败", IsWarn: true); + throw new Exception("$" + logPrefix + "\r\n" + "\r\n" + "详细信息:" + ex); + } + + /// + /// 处理普通登录 HttpWebException + /// + private static void HandleLoginHttpException(ModNet.HttpWebException ex) + { + ModProfile.ProfileLog("验证失败:" + ex); + string message = null; + var responseText = ex.InnerHttpException.WebResponse; + + try + { + var err = JsonNode.Parse(responseText)["errorMessage"]; + if (err is not null) + message = "登录失败:" + err; + } + catch + { + // 忽略解析错误 + } + + if (message is null) + message = "第三方验证登录失败,请检查你的网络状况是否良好。" + "\r\n" + "\r\n" + + "详细信息:" + responseText; + + ModMain.MyMsgBox("刷新登录失败: " + ex, "第三方验证失败", IsWarn: true); + throw new Exception("$" + message); + } + + // Server 登录:三种验证方式的请求 + private static void McLoginRequestValidate(ref ModLoader.LoaderTask Data) + { + ModProfile.ProfileLog("验证登录开始(Validate, Authlib"); + // 提前缓存信息,否则如果在登录请求过程中退出登录,设置项目会被清空,导致输出存在空值 + var AccessToken = ""; + var ClientToken = ""; + var Uuid = ""; + var Name = ""; + if (ModProfile.SelectedProfile is not null) + { + AccessToken = ModProfile.SelectedProfile.AccessToken; + ClientToken = ModProfile.SelectedProfile.ClientToken; + Uuid = ModProfile.SelectedProfile.Uuid; + Name = ModProfile.SelectedProfile.Username; + } + + // 发送登录请求 + var RequestData = new JObject(new JProperty("accessToken", AccessToken), + new JProperty("clientToken", ClientToken)); + ModNet.NetRequestRetry(Data.Input.BaseUrl + "/validate", "POST", RequestData.ToString(0), + Headers: new Dictionary { { "Accept-Language", "zh-CN" } }, + ContentType: "application/json"); // 没有返回值的 + // 将登录结果输出 + Data.Output.AccessToken = AccessToken; + Data.Output.ClientToken = ClientToken; + Data.Output.Uuid = Uuid; + Data.Output.Name = Name; + Data.Output.Type = "Auth"; + // 不更改缓存,直接结束 + ModProfile.ProfileLog("验证登录成功(Validate, Authlib"); + } + + private static void McLoginRequestRefresh(ref ModLoader.LoaderTask Data, + bool RequestUser) + { + var RefreshInfo = new JObject(); + var SelectProfile = new JObject + { { "name", ModProfile.SelectedProfile.Username }, { "id", ModProfile.SelectedProfile.Uuid } }; + RefreshInfo.Add("selectedProfile", SelectProfile); + RefreshInfo.Add(new JProperty("accessToken", ModProfile.SelectedProfile.AccessToken)); + RefreshInfo.Add(new JProperty("requestUser", true)); + ModProfile.ProfileLog("刷新登录开始(Refresh, Authlib"); + var LoginJson = (JObject)ModBase.GetJson(ModNet.NetRequestRetry(Data.Input.BaseUrl + "/refresh", "POST", + RefreshInfo.ToString(0), Headers: new Dictionary { { "Accept-Language", "zh-CN" } }, + ContentType: "application/json")); + // 将登录结果输出 + if (LoginJson["selectedProfile"] is null) + throw new Exception("选择的角色 " + ModProfile.SelectedProfile.Username + " 无效!"); + Data.Output.AccessToken = LoginJson["accessToken"].ToString(); + Data.Output.ClientToken = LoginJson["clientToken"].ToString(); + Data.Output.Uuid = LoginJson["selectedProfile"]["id"].ToString(); + Data.Output.Name = LoginJson["selectedProfile"]["name"].ToString(); + Data.Output.Type = "Auth"; + // 保存缓存 + var ProfileIndex = ModProfile.ProfileList.IndexOf(ModProfile.SelectedProfile); + ModProfile.ProfileList[ProfileIndex].Username = Data.Output.Name; + ModProfile.ProfileList[ProfileIndex].AccessToken = Data.Output.AccessToken; + ModProfile.ProfileList[ProfileIndex].ClientToken = Data.Output.ClientToken; + ModProfile.ProfileList[ProfileIndex].Uuid = Data.Output.Uuid; + ModProfile.ProfileList[ProfileIndex].Name = Data.Input.UserName; + ModProfile.ProfileList[ProfileIndex].Password = Data.Input.Password; + ModProfile.ProfileLog("刷新登录成功(Refresh, Authlib)"); + } + + private static bool McLoginRequestLogin(ref ModLoader.LoaderTask Data) + { + try + { + var NeedRefresh = false; + ModProfile.ProfileLog("登录开始(Login, Authlib)"); + var RequestData = new JObject( + new JProperty("agent", new JObject(new JProperty("name", "Minecraft"), new JProperty("version", 1))), + new JProperty("username", Data.Input.UserName), new JProperty("password", Data.Input.Password), + new JProperty("requestUser", true)); + var LoginJson = (JObject)ModBase.GetJson(ModNet.NetRequestRetry(Data.Input.BaseUrl + "/authenticate", + "POST", RequestData.ToString(0), + Headers: new Dictionary { { "Accept-Language", "zh-CN" } }, + ContentType: "application/json")); + // 检查登录结果 + if (LoginJson["availableProfiles"].Count() == 0) + { + if (Data.Input.ForceReselectProfile) + ModMain.Hint("你还没有创建角色,无法更换!", ModMain.HintType.Critical); + throw new Exception("$你还没有创建角色,请在创建角色后再试!"); + } + + if (Data.Input.ForceReselectProfile && LoginJson["availableProfiles"].Count() == 1) + ModMain.Hint("你的账户中只有一个角色,无法更换!", ModMain.HintType.Critical); + string SelectedName = null; + string SelectedId = null; + if ((LoginJson["selectedProfile"] is null || Data.Input.ForceReselectProfile) && + LoginJson["availableProfiles"].Count() > 1) + { + // 要求选择档案;优先从缓存读取 + NeedRefresh = true; + var CacheId = ModProfile.SelectedProfile is not null ? ModProfile.SelectedProfile.Uuid : ""; + foreach (var Profile in LoginJson["availableProfiles"]) + if ((Profile["id"].ToString() ?? "") == (CacheId ?? "")) + { + SelectedName = Profile["name"].ToString(); + SelectedId = Profile["id"].ToString(); + ModProfile.ProfileLog("根据缓存选择的角色:" + SelectedName); + } + + // 缓存无效,要求玩家选择 + if (SelectedName is null) + { + ModProfile.ProfileLog("要求玩家选择角色"); + ModBase.RunInUiWait(() => + { + var SelectionControl = new List(); + var SelectionJson = new List(); + foreach (var Profile in LoginJson["availableProfiles"]) + { + SelectionControl.Add(new MyRadioBox { Text = Profile["name"].ToString() }); + SelectionJson.Add(Profile); + } + + var SelectedIndex = (int)ModMain.MyMsgBoxSelect(SelectionControl, "选择使用的角色"); + SelectedName = SelectionJson[SelectedIndex]["name"].ToString(); + SelectedId = SelectionJson[SelectedIndex]["id"].ToString(); + }); + + ModProfile.ProfileLog("玩家选择的角色:" + SelectedName); + } + } + else + { + SelectedName = LoginJson["selectedProfile"]["name"].ToString(); + SelectedId = LoginJson["selectedProfile"]["id"].ToString(); + } + + // 将登录结果输出 + Data.Output.AccessToken = LoginJson["accessToken"].ToString(); + Data.Output.ClientToken = LoginJson["clientToken"].ToString(); + Data.Output.Name = SelectedName; + Data.Output.Uuid = SelectedId; + Data.Output.Type = "Auth"; + // 获取服务器信息 + var Response = + Conversions.ToString(ModNet.NetGetCodeByRequestRetry(Data.Input.BaseUrl.Replace("/authserver", ""), + Encoding.UTF8)); + var ServerName = JObject.Parse(Response)["meta"]["serverName"].ToString(); + // 保存缓存 + if (Data.Input.IsExist) + { + var ProfileIndex = ModProfile.ProfileList.IndexOf(ModProfile.SelectedProfile); + ModProfile.ProfileList[ProfileIndex].Username = Data.Output.Name; + ModProfile.ProfileList[ProfileIndex].Uuid = Data.Output.Uuid; + ModProfile.ProfileList[ProfileIndex].ServerName = ServerName; + ModProfile.ProfileList[ProfileIndex].AccessToken = Data.Output.AccessToken; + ModProfile.ProfileList[ProfileIndex].ClientToken = Data.Output.ClientToken; + } + else + { + var NewProfile = new ModProfile.McProfile + { + Type = McLoginType.Auth, + Uuid = Data.Output.Uuid, + Username = Data.Output.Name, + Server = Data.Input.BaseUrl, + ServerName = ServerName, + Name = Data.Input.UserName, + Password = Data.Input.Password, + AccessToken = Data.Output.AccessToken, + ClientToken = Data.Output.ClientToken, + Expires = 1743779140286L, + Desc = "" + }; + ModProfile.ProfileList.Add(NewProfile); + ModProfile.SelectedProfile = NewProfile; + ModProfile.IsCreatingProfile = false; + } + + ModProfile.SaveProfile(); + ModProfile.ProfileLog("登录成功(Login, Authlib)"); + return NeedRefresh; + } + catch (ModNet.HttpWebException ex) + { + throw; + } + catch (Exception ex) + { + var AllMessage = ex.ToString(); + ModProfile.ProfileLog("第三方验证失败: " + ex); + if (ex.Message.StartsWithF("$")) throw; + + throw new Exception("登录失败:" + ex.Message, ex); + } + } + + #endregion + + #region 离线验证 + + private static void McLoginLegacyStart(ModLoader.LoaderTask Data) + { + var Input = Data.Input; + ModProfile.ProfileLog($"验证方式:离线({Input.UserName}, {Input.Uuid})"); + Data.Progress = 0.1d; + { + ref var withBlock = ref Data.Output; + withBlock.Name = Input.UserName; + withBlock.Uuid = ModProfile.SelectedProfile.Uuid; + withBlock.Type = "Legacy"; + } + // 将结果扩展到所有项目中 + Data.Output.AccessToken = Data.Output.Uuid; + Data.Output.ClientToken = Data.Output.Uuid; + } + + #endregion + + #endregion + + #region Java 处理 + + public static JavaEntry McLaunchJavaSelected; + + private static void McLaunchJava(ModLoader.LoaderTask task) + { + var minVer = new Version(0, 0, 0, 0); + var maxVer = new Version(999, 999, 999, 999); + + // MC 大版本检测 + if ((!ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.ReleaseTime >= new DateTime(2024, 4, 2)) || + (ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.Info.Vanilla >= new Version(20, 0, 5))) + { + // 1.20.5+ (24w14a+):至少 Java 21 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] MC 1.20.5+ (24w14a+) 要求至少 Java 21"); + minVer = new Version(21, 0, 0, 0); + } + else if ((!ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.ReleaseTime >= new DateTime(2021, 11, 16)) || + (ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 18)) + { + // 1.18 pre2+:至少 Java 17 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] MC 1.18 pre2+ 要求至少 Java 17"); + minVer = new Version(17, 0, 0, 0); + } + else if ((!ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.ReleaseTime >= new DateTime(2021, 5, 11)) || + (ModMinecraft.McInstanceSelected.Info.Valid && + ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 17)) + { + // 1.17+ (21w19a+):至少 Java 16 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] MC 1.17+ (21w19a+) 要求至少 Java 16"); + minVer = new Version(16, 0, 0, 0); + } + else if (ModMinecraft.McInstanceSelected.ReleaseTime.Year >= 2017) // Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了 + { + // 1.12+:至少 Java 8 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] MC 1.12+ 要求至少 Java 8"); + minVer = new Version(1, 8, 0, 0); + } + else if (ModMinecraft.McInstanceSelected.ReleaseTime <= new DateTime(2013, 5, 1) && + ModMinecraft.McInstanceSelected.ReleaseTime.Year >= 2001) // 避免某些版本写个 1960 年 + { + // 1.5.2-:最高 Java 8 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] MC 1.5.2- 要求最高 Java 12"); + maxVer = new Version(1, 8, 999, 999); + } + + // 原版 26+:获取 Mojang 要求的 Java 版本 + string recommendedComponent = null; + var recommendedCode = + ModMinecraft.McInstanceSelected.JsonObject?["javaVersion"]?["majorVersion"]?.ToObject() ?? + ModMinecraft.McInstanceSelected.JsonVersion?["java_version"]?.ToObject() ?? 0; + if (recommendedCode >= 22) + { + McLaunchLog("Mojang 要求至少使用 Java " + recommendedCode); + minVer = new Version(1, recommendedCode, 0, 0); + recommendedComponent = + ModMinecraft.McInstanceSelected.JsonObject?["javaVersion"]?["component"]?.ToString() ?? + ModMinecraft.McInstanceSelected.JsonVersion?["java_component"]?.ToString(); + if (string.IsNullOrEmpty(recommendedComponent)) + recommendedComponent = null; + } + + // OptiFine 检测 + if (ModMinecraft.McInstanceSelected.Info.HasOptiFine && ModMinecraft.McInstanceSelected.Info.Valid) // 不管非标准版本 + { + if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major < 7) + { + // <1.7:至多 Java 8 + maxVer = new Version(1, 8, 999, 999); + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 8 && + ModMinecraft.McInstanceSelected.Info.Vanilla.Major < 12) + { + // 1.8 - 1.11:必须恰好 Java 8 + minVer = new Version(1, 8, 0, 0); + maxVer = new Version(1, 8, 999, 999); + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major == 12) + { + // 1.12:最高 Java 8 + maxVer = new Version(1, 8, 999, 999); + } + } + + // Forge 检测 + if (ModMinecraft.McInstanceSelected.Info.HasForge) + { + if (ModMinecraft.McInstanceSelected.Info.Vanilla >= new Version(6, 0, 1) && + ModMinecraft.McInstanceSelected.Info.Vanilla <= new Version(7, 0, 2)) + { + // 1.6.1 - 1.7.2:必须 Java 7 + minVer = new Version(1, 7, 0, 0) > minVer ? new Version(1, 7, 0, 0) : minVer; + maxVer = new Version(1, 7, 999, 999) < maxVer ? new Version(1, 7, 999, 999) : maxVer; + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major <= 12 || + !ModMinecraft.McInstanceSelected.Info.Valid) // 非标准版本 + { + // <=1.12:Java 8 + maxVer = new Version(1, 8, 999, 999); + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major <= 14) + { + // 1.13 - 1.14:Java 8 - 10 + minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer; + maxVer = new Version(1, 10, 999, 999) < maxVer ? new Version(1, 10, 999, 999) : maxVer; + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major == 15) + { + // 1.15:Java 8 - 15 + minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer; + maxVer = new Version(1, 15, 999, 999) < maxVer ? new Version(1, 15, 999, 999) : maxVer; + } + else if (ModMinecraft.CompareVersionGe(ModMinecraft.McInstanceSelected.Info.Forge, "34.0.0") && + ModMinecraft.CompareVersionGe("36.2.25", ModMinecraft.McInstanceSelected.Info.Forge)) + { + // 1.16,Forge 34.X ~ 36.2.25:最高 Java 8u321 + maxVer = new Version(1, 8, 0, 320) < maxVer ? new Version(1, 8, 0, 321) : maxVer; + } + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 18 && + ModMinecraft.McInstanceSelected.Info.Vanilla.Major < 19 && + ModMinecraft.McInstanceSelected.Info.HasOptiFine) // #305 + { + // 1.18:若安装了 OptiFine,最高 Java 18 + maxVer = new Version(1, 18, 999, 999) < maxVer ? new Version(1, 18, 999, 999) : maxVer; + } + } + + // Cleanroom 检测 + if (ModMinecraft.McInstanceSelected.Info.HasCleanroom) + { + // 需要至少 Java 21 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] Cleanroom 要求至少 Java 21"); + minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer; + } + + // Fabric 检测 + if (ModMinecraft.McInstanceSelected.Info.HasFabric && ModMinecraft.McInstanceSelected.Info.Valid) // 不管非标准版本 + { + if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 15 && + ModMinecraft.McInstanceSelected.Info.Vanilla.Major <= 16) + // 1.15 - 1.16:Java 8+ + minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer; + else if (ModMinecraft.McInstanceSelected.Info.Vanilla.Major >= 18) + // 1.18+:Java 17+ + minVer = new Version(1, 17, 0, 0) > minVer ? new Version(1, 17, 0, 0) : minVer; + } + + // LiteLoader 检测 + if (ModMinecraft.McInstanceSelected.Info.HasLiteLoader && ModMinecraft.McInstanceSelected.Info.Valid) + { + // 最高 Java 8 + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] LiteLoader 要求最高 Java 8"); + maxVer = new Version(8, 999, 999, 999) < maxVer ? new Version(8, 999, 999, 999) : maxVer; + } + + // LabyMod 检测 + if (ModMinecraft.McInstanceSelected.Info.HasLabyMod) + { + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] LabyMod 要求至少 Java 21"); + minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer; + maxVer = new Version(999, 999, 999, 999); + } + + // JSON 中要求的版本 + if (ModMinecraft.McInstanceSelected.JsonObject["javaVersion"] is not null) + { + var majorVersion = ModBase.Val(ModMinecraft.McInstanceSelected.JsonObject["javaVersion"]["majorVersion"]); + if (ModBase.ModeDebug) + ModBase.Log("[Launch] [Debug] JSON 中参数要求至少 Java " + majorVersion); + if (majorVersion <= 8d) + minVer = new Version(1, (int)Math.Round(majorVersion), 0, 0) > minVer + ? new Version(1, (int)Math.Round(majorVersion), 0, 0) + : minVer; + else + minVer = new Version((int)Math.Round(majorVersion), 0, 0, 0) > minVer + ? new Version((int)Math.Round(majorVersion), 0, 0, 0) + : minVer; + + if (maxVer < minVer) + maxVer = new Version(999, 999, 999, 999); + } + + lock (ModJava.JavaLock) + { + // 选择 Java + McLaunchLog("Java 版本需求:最低 " + minVer + ",最高 " + maxVer); + McLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModMinecraft.McInstanceSelected); + if (task.IsAborted) + return; + if (McLaunchJavaSelected is not null) + { + McLaunchLog("选择的 Java:" + McLaunchJavaSelected.ToString); + return; + } + + // 无合适的 Java + if (task.IsAborted) + return; // 中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java + McLaunchLog("无合适的 Java,需要确认是否自动下载"); + string javaCode; + if (minVer >= new Version(1, 9)) + { + javaCode = minVer.Major.ToString(); + } + else if (maxVer < new Version(1, 8)) + { + if (ModMinecraft.McInstanceSelected.Info.HasForge) + ModMain.MyMsgBox( + $"你需要先安装 LegacyJavaFixer Mod,或安装 Java 7 才能启动该版本。{"\r\n"}请自行搜索并安装 Java 7,安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", + "未找到 Java"); + else + ModMain.MyMsgBox( + $"你需要安装 Java 7 才能启动该版本。{"\r\n"}请自行搜索并安装 Java 7,安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", + "未找到 Java"); + throw new Exception("$$"); + } + else if (minVer > new Version(1, 8, 0, 140) && maxVer < new Version(1, 8, 0, 321)) + { + ModMain.MyMsgBox( + $"你需要安装 Java 8u141 ~ 8u320 才能启动该版本。{"\r\n"}请自行搜索并安装,安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", + "未找到 Java"); + throw new Exception("$$"); + } + else if (minVer > new Version(1, 8, 0, 140)) + { + ModMain.MyMsgBox( + $"你需要安装 Java 8u141 或更高版本的 Java 8 才能启动该版本。{"\r\n"}请自行搜索并安装,安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", + "未找到 Java"); + throw new Exception("$$"); + } + else + { + javaCode = 8.ToString(); + } + + if (!ModJava.JavaDownloadConfirm($"Java {javaCode}")) + throw new Exception("$$"); + // 开始自动下载 + var javaLoader = ModJava.GetJavaDownloadLoader(); + try + { + javaLoader.Start(recommendedComponent ?? javaCode, true); // 在 Java 22+ 时优先使用 Mojang 提供的 Component 字段 + while (javaLoader.State == ModBase.LoadState.Loading && !task.IsAborted) + { + task.Progress = javaLoader.Progress; + Thread.Sleep(10); + } + } + finally + { + javaLoader.Abort(); // 确保取消时中止 Java 下载 + } + + // 检查下载结果 + McLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModMinecraft.McInstanceSelected); + if (task.IsAborted) + return; + if (McLaunchJavaSelected is not null) + { + McLaunchLog("选择的 Java:" + McLaunchJavaSelected); + } + else + { + ModMain.Hint("没有可用的 Java,已取消启动!", ModMain.HintType.Critical); + throw new Exception("$$"); + } + } + } + + #endregion + + #region 启动参数 + + public class LaunchArgument + { + private readonly List _features = new(); + + public LaunchArgument(ModMinecraft.McInstance Minecraft) + { + var curArgu = string.Empty; + if (Minecraft.IsOldJson) + _features = Minecraft.JsonObject["minecraftArguments"].ToString().Split(' ').ToList(); + else + foreach (var item in Minecraft.JsonObject["arguments"]["game"]) + if (item.Type == JTokenType.String) + _features.Add(item.ToString()); + else if (item.Type == JTokenType.Object) + _features.AddRange(item["value"].Select(x => x.ToString())); + } + + public object HasArguments(string key) + { + return _features.Contains(key); + } + } + + private static string McLaunchArgument; + + /// + /// 释放 Java Wrapper 并返回完整文件路径。 + /// + public static string ExtractJavaWrapper() + { + var WrapperPath = ModBase.PathPure + "JavaWrapper.jar"; + ModBase.Log("[Java] 选定的 Java Wrapper 路径:" + WrapperPath); + lock (ExtractJavaWrapperLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突 + { + try + { + WriteJavaWrapper(WrapperPath); + } + catch (Exception ex) + { + if (File.Exists(WrapperPath)) + { + // 因为未知原因 Java Wrapper 可能变为只读文件(#4243) + ModBase.Log(ex, "Java Wrapper 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer); + try + { + File.Delete(WrapperPath); + WriteJavaWrapper(WrapperPath); + } + catch (Exception ex2) + { + ModBase.Log(ex2, "Java Wrapper 文件重新释放失败,将尝试更换文件名重新生成", ModBase.LogLevel.Developer); + WrapperPath = ModBase.PathPure + "JavaWrapper2.jar"; + try + { + WriteJavaWrapper(WrapperPath); + } + catch (Exception ex3) + { + throw new FileNotFoundException("释放 Java Wrapper 最终尝试失败", ex3); + } + } + } + else + { + throw new FileNotFoundException("释放 Java Wrapper 失败", ex); + } + } + } + + return WrapperPath; + } + + private static readonly object ExtractJavaWrapperLock = new(); + + private static void WriteJavaWrapper(string Path) + { + ModBase.WriteFile(Path, ModBase.GetResourceStream("Resources/java-wrapper.jar")); + } + + /// + /// 释放 linkd 并返回完整文件路径。 + /// + public static string ExtractLinkD() + { + var LinkDPath = ModBase.PathPure + "linkd.exe"; + lock (ExtractLinkDLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突 + { + try + { + WriteLinkD(LinkDPath); + } + catch (Exception ex) + { + if (File.Exists(LinkDPath)) + { + ModBase.Log(ex, "linkd 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer); + try + { + File.Delete(LinkDPath); + WriteLinkD(LinkDPath); + } + catch (Exception ex2) + { + throw new FileNotFoundException("释放 linkd 失败", ex2); + } + } + else + { + throw new FileNotFoundException("释放 linkd 失败", ex); + } + } + } + + return LinkDPath; + } + + private static readonly object ExtractLinkDLock = new(); + + private static void WriteLinkD(string Path) + { + ModBase.WriteFile(Path, ModBase.GetResourceStream("Resources/linkd.exe")); + } + + /// + /// 判断是否使用 RetroWrapper。 + /// TODO: 在更换为 Drop 比较版本号后可能不准确,需要测试确认。 + /// + private static bool McLaunchNeedsRetroWrapper(ModMinecraft.McInstance Mc) + { + return Conversions.ToBoolean((Mc.ReleaseTime >= new DateTime(2013, 6, 25) && Mc.Info.Drop == 99) || + (Mc.Info.Drop < 60 && Mc.Info.Drop != 99 && + !(bool)Config.Launch.DisableRw && + !(bool)ModBase.Setup.Get("VersionAdvanceDisableRW", Mc))); // <1.6 + } + + + // 主方法,合并 Jvm、Game、Replace 三部分的参数数据 + private static void McLaunchArgumentMain(ModLoader.LoaderTask> Loader) + { + McLaunchLog("开始获取 Minecraft 启动参数"); + // 获取基准字符串与参数信息 + string Arguments; + if (ModMinecraft.McInstanceSelected.JsonObject["arguments"] is not null && + ModMinecraft.McInstanceSelected.JsonObject["arguments"]["jvm"] is not null) + { + McLaunchLog("获取新版 JVM 参数"); + Arguments = McLaunchArgumentsJvmNew(ModMinecraft.McInstanceSelected); + McLaunchLog("新版 JVM 参数获取成功:"); + McLaunchLog(Arguments); + } + else + { + McLaunchLog("获取旧版 JVM 参数"); + Arguments = McLaunchArgumentsJvmOld(ModMinecraft.McInstanceSelected); + McLaunchLog("旧版 JVM 参数获取成功:"); + McLaunchLog(Arguments); + } + + if (!string.IsNullOrEmpty( + (string)ModMinecraft.McInstanceSelected.JsonObject["minecraftArguments"])) // 有的实例 JSON 中是空字符串 + { + McLaunchLog("获取旧版 Game 参数"); + Arguments += " " + McLaunchArgumentsGameOld(ModMinecraft.McInstanceSelected); + McLaunchLog("旧版 Game 参数获取成功"); + } + + if (ModMinecraft.McInstanceSelected.JsonObject["arguments"] is not null && + ModMinecraft.McInstanceSelected.JsonObject["arguments"]["game"] is not null) + { + McLaunchLog("获取新版 Game 参数"); + Arguments += " " + McLaunchArgumentsGameNew(ModMinecraft.McInstanceSelected); + McLaunchLog("新版 Game 参数获取成功"); + } + + // 编码参数(#4700、#5892、#5909) + if (McLaunchJavaSelected.Installation.MajorVersion > 8) + { + if (!Arguments.Contains("-Dstdout.encoding=")) + Arguments = "-Dstdout.encoding=UTF-8 " + Arguments; + if (!Arguments.Contains("-Dstderr.encoding=")) + Arguments = "-Dstderr.encoding=UTF-8 " + Arguments; + } + + if (McLaunchJavaSelected.Installation.MajorVersion >= 18) + if (!Arguments.Contains("-Dfile.encoding=")) + Arguments = "-Dfile.encoding=COMPAT " + Arguments; + // MJSB + Arguments = Arguments.Replace(" -Dos.name=Windows 10", " -Dos.name=\"Windows 10\""); + // 全屏 + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.GameWindowMode, 0, false))) + Arguments += " --fullscreen"; + // 由 Option 传入的额外参数 + foreach (var Arg in CurrentLaunchOptions.ExtraArgs) + Arguments += " " + Arg.Trim(); + // 自定义参数 + var ArgumentGame = + Conversions.ToString(ModBase.Setup.Get("VersionAdvanceGame", ModMinecraft.McInstanceSelected)); + Arguments = Conversions.ToString(Arguments + Operators.ConcatenateObject(" ", + string.IsNullOrEmpty(ArgumentGame) ? Config.Launch.GameArgs : ArgumentGame)); + // 替换参数 + var ReplaceArguments = McLaunchArgumentsReplace(ModMinecraft.McInstanceSelected, ref Loader); + if (string.IsNullOrWhiteSpace(ReplaceArguments["${version_type}"])) + { + // 若自定义信息为空,则去掉该部分 + Arguments = Arguments.Replace(" --versionType ${version_type}", ""); + ReplaceArguments["${version_type}"] = "\"\""; + } + + var FinalArguments = ""; + foreach (var ArgumentRaw in Arguments.Split(" ")) + { + var Argument = ArgumentRaw; + foreach (var Entry in ReplaceArguments) + Argument = Argument.Replace(Entry.Key, Entry.Value); + if ((Argument.Contains(" ") || Argument.Contains(@":\")) && !Argument.EndsWithF("\"")) + Argument = $"\"{Argument}\""; + FinalArguments += Argument + " "; + } + + FinalArguments = FinalArguments.TrimEnd(); + // 进存档 + var WorldName = CurrentLaunchOptions.WorldName; + if (WorldName is not null) FinalArguments += $" --quickPlaySingleplayer \"{WorldName}\""; + // 进服 + var Server = Conversions.ToString(string.IsNullOrEmpty(CurrentLaunchOptions.ServerIp) + ? ModBase.Setup.Get("VersionServerEnter", ModMinecraft.McInstanceSelected) + : CurrentLaunchOptions.ServerIp); + if (string.IsNullOrWhiteSpace(WorldName) && !string.IsNullOrWhiteSpace(Server)) + { + if (ModMinecraft.McInstanceSelected.ReleaseTime > new DateTime(2023, 4, 4)) + { + // QuickPlay + FinalArguments += $" --quickPlayMultiplayer \"{Server}\""; + } + else + { + // 老版本 + if (Server.Contains(":")) + // 包含端口号 + FinalArguments += " --server " + Server.Split(":")[0] + " --port " + Server.Split(":")[1]; + else + // 不包含端口号 + FinalArguments += " --server " + Server + " --port 25565"; + if (ModMinecraft.McInstanceSelected.Info.HasOptiFine) + ModMain.Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", ModMain.HintType.Critical); + } + } + + // 输出 + McLaunchLog("Minecraft 启动参数:"); + McLaunchLog(FinalArguments); + McLaunchArgument = FinalArguments; + } + + // Jvm 部分(第一段) + private static string McLaunchArgumentsJvmOld(ModMinecraft.McInstance instance) + { + // 存储以空格为间隔的启动参数列表 + var DataList = new List(); + + // 输出固定参数 + DataList.Add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"); + var ArgumentJvm = Conversions.ToString(ModBase.Setup.Get("VersionAdvanceJvm", ModMinecraft.McInstanceSelected)); + if (string.IsNullOrEmpty(ArgumentJvm)) + ArgumentJvm = Conversions.ToString(Config.Launch.JvmArgs); + if (!ArgumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true")) + ArgumentJvm += " -Dlog4j2.formatMsgNoLookups=true"; + ArgumentJvm = ArgumentJvm.Replace(" -XX:MaxDirectMemorySize=256M", ""); // #3511 的清理 + DataList.Insert(0, ArgumentJvm); // 可变 JVM 参数 + DataList.Add("-Xmn" + + Math.Floor(PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected, + !McLaunchJavaSelected.Installation.Is64Bit) * 1024d * 0.15d) + "m"); + DataList.Add("-Xmx" + + Math.Floor(PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected, + !McLaunchJavaSelected.Installation.Is64Bit) * 1024d) + "m"); + DataList.Add("\"-Djava.library.path=" + GetNativesFolder() + "\""); + DataList.Add("-cp ${classpath}"); // 把支持库添加进启动参数表 + + // Authlib-Injector + if (McLoginLoader.Output.Type == "Auth") + { + if (McLaunchJavaSelected.Installation.MajorVersion >= 6) + DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252) + var Server = McLoginAuthLoader.Input.BaseUrl.Replace("/authserver", ""); + try + { + var Response = Conversions.ToString(ModNet.NetGetCodeByRequestRetry(Server, Encoding.UTF8)); + DataList.Insert(0, + "-javaagent:\"" + ModBase.PathPure + "authlib-injector.jar\"=" + Server + + " -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" + + Convert.ToBase64String(Encoding.UTF8.GetBytes(Response))); + } + catch (ModNet.HttpWebException ex) + { + throw new Exception( + $"无法连接到第三方登录服务器({Server ?? null}){"\r\n"}详细信息:" + ex.InnerHttpException.WebResponse, ex); + } + catch (Exception ex) + { + throw new Exception($"无法连接到第三方登录服务器({Server ?? null})", ex); + } + } + + if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie]) + { + if (ModMinecraft.McInstanceSelected.ReleaseTime.Year >= 2017) + DataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\""); + else + DataList.Insert(0, + "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\""); + } + + // 渲染器 + var Renderer = 0; + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectNotEqual( + ModBase.Setup.Get("VersionAdvanceRenderer", ModMinecraft.McInstanceSelected), 0, false))) + Renderer = Conversions.ToInteger( + Operators.SubtractObject(ModBase.Setup.Get("VersionAdvanceRenderer", ModMinecraft.McInstanceSelected), + 1)); + else + Renderer = Conversions.ToInteger(Config.Launch.Renderer); + var MesaLoaderWindowsVersion = "25.3.5"; + var MesaLoaderWindowsTargetFile = + ModBase.PathPure + @"\mesa-loader-windows\" + MesaLoaderWindowsVersion + @"\Loader.jar"; + + if (Renderer != 0) + DataList.Insert(0, + "-javaagent:\"" + MesaLoaderWindowsTargetFile + "\"=" + + (Renderer == 1 ? "llvmpipe" : Renderer == 2 ? "d3d12" : "zink")); + + // 设置代理 + if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) && + !string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress)) + try + { + var ProxyAddress = new Uri(Conversions.ToString(Config.Network.HttpProxy.CustomAddress)); + DataList.Add( + $"-D{(ProxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={ProxyAddress.AbsoluteUri}"); + DataList.Add( + $"-D{(ProxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={ProxyAddress.Port}"); + } + catch (Exception ex) + { + ModBase.Log(ex, "添加代理信息到游戏失败,放弃加入", ModBase.LogLevel.Hint); + } + + // 添加 Java Wrapper 作为主 Jar + if (Conversions.ToBoolean(ModBase.IsUtf8CodePage() && !(bool)Config.Launch.DisableJlw && + !(bool)ModBase.Setup.Get("VersionAdvanceDisableJLW", + ModMinecraft.McInstanceSelected))) + { + if (McLaunchJavaSelected.Installation.MajorVersion >= 9) + DataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED"); + DataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.PathPure.TrimEnd('\\') + "\""); + DataList.Add("-jar \"" + ExtractJavaWrapper() + "\""); + } + + // 添加 MainClass + if (instance.JsonObject["mainClass"] is null) throw new Exception("实例 JSON 中没有 mainClass 项!"); + + DataList.Add((string)instance.JsonObject["mainClass"]); + + return DataList.Join(" "); + } + + private static string McLaunchArgumentsJvmNew(ModMinecraft.McInstance instance) + { + var DataList = new List(); + + // 获取 Json 中的 DataList + var currentInstance = instance; + NextInstance: ; + + if (currentInstance.JsonObject["arguments"] is not null && + currentInstance.JsonObject["arguments"]["jvm"] is not null) + foreach (var SubJson in currentInstance.JsonObject["arguments"]["jvm"]) + if (SubJson.Type == JTokenType.String) + { + // 字符串类型 + DataList.Add(SubJson.ToString()); + } + // 非字符串类型 + else if (ModMinecraft.McJsonRuleCheck(SubJson["rules"])) + { + // 满足准则 + if (SubJson["value"].Type == JTokenType.String) + DataList.Add(SubJson["value"].ToString()); + else + foreach (var value in SubJson["value"]) + DataList.Add(value.ToString()); + } + + if (!string.IsNullOrEmpty(currentInstance.InheritInstanceName)) + { + currentInstance = new ModMinecraft.McInstance(currentInstance.InheritInstanceName); + goto NextInstance; + } + + // 内存、Log4j 防御参数等 + ModSecret.SecretLaunchJvmArgs(ref DataList); + + // Authlib-Injector + if (McLoginLoader.Output.Type == "Auth") + { + if (McLaunchJavaSelected.Installation.MajorVersion >= 6) + DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252) + var Server = McLoginAuthLoader.Input.BaseUrl.Replace("/authserver", ""); + try + { + var Response = Conversions.ToString(ModNet.NetGetCodeByRequestRetry(Server, Encoding.UTF8)); + DataList.Insert(0, + "-javaagent:\"" + ModBase.PathPure + "authlib-injector.jar\"=" + Server + + " -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" + + Convert.ToBase64String(Encoding.UTF8.GetBytes(Response))); + } + catch (Exception ex) + { + throw new Exception("无法连接到第三方登录服务器(" + (Server ?? null) + ")", ex); + } + } + + if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie]) + { + if (ModMinecraft.McInstanceSelected.ReleaseTime.Year >= 2017) + DataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\""); + else + DataList.Insert(0, + "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\""); + } + + // 渲染器 + var Renderer = 0; + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectNotEqual( + ModBase.Setup.Get("VersionAdvanceRenderer", ModMinecraft.McInstanceSelected), 0, false))) + Renderer = Conversions.ToInteger( + Operators.SubtractObject(ModBase.Setup.Get("VersionAdvanceRenderer", ModMinecraft.McInstanceSelected), + 1)); + else + Renderer = Conversions.ToInteger(Config.Launch.Renderer); + var MesaLoaderWindowsVersion = "25.3.5"; + var MesaLoaderWindowsTargetFile = + ModBase.PathPure + @"\mesa-loader-windows\" + MesaLoaderWindowsVersion + @"\Loader.jar"; + + if (Renderer != 0) + DataList.Insert(0, + "-javaagent:\"" + MesaLoaderWindowsTargetFile + "\"=" + + (Renderer == 1 ? "llvmpipe" : Renderer == 2 ? "d3d12" : "zink")); + + // 设置代理 + if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) && + !string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress)) + try + { + var ProxyAddress = new Uri(Conversions.ToString(Config.Network.HttpProxy.CustomAddress)); + DataList.Add( + $"-D{(ProxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={ProxyAddress.AbsoluteUri}"); + DataList.Add( + $"-D{(ProxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={ProxyAddress.Port}"); + } + catch (Exception ex) + { + ModBase.Log(ex, "添加代理信息到游戏失败,放弃加入", ModBase.LogLevel.Hint); + } + + // 添加 RetroWrapper 相关参数 + if (McLaunchNeedsRetroWrapper(instance)) + // https://github.com/NeRdTheNed/RetroWrapper/wiki/RetroWrapper-flags + DataList.Add("-Dretrowrapper.doUpdateCheck=false"); + // 添加 Java Wrapper 作为主 Jar + if (Conversions.ToBoolean(ModBase.IsUtf8CodePage() && !(bool)Config.Launch.DisableJlw && + !(bool)ModBase.Setup.Get("VersionAdvanceDisableJLW", + ModMinecraft.McInstanceSelected))) + { + if (McLaunchJavaSelected.Installation.MajorVersion >= 9) + DataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED"); + DataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.PathPure.TrimEnd('\\') + "\""); + DataList.Add("-jar \"" + ExtractJavaWrapper() + "\""); + } + + + // 将 "-XXX" 与后面 "XXX" 合并到一起 + // 如果不合并,会导致 Forge 1.17 启动无效,它有两个 --add-exports,进一步导致其中一个在后面被去重 + var DeDuplicateDataList = new List(); + for (int i = 0, loopTo = DataList.Count - 1; i <= loopTo; i++) + { + var CurrentEntry = DataList[i]; + if (DataList[i].StartsWithF("-")) + while (i < DataList.Count - 1) + { + if (DataList[i + 1].StartsWithF("-")) break; + + i += 1; + CurrentEntry += " " + DataList[i]; + } + + DeDuplicateDataList.Add(CurrentEntry.Trim().Replace("McEmu= ", "McEmu=")); + } + + // #3511 的清理 + DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M"); + + // 去重 + var Result = DeDuplicateDataList.Distinct().ToList().Join(" "); + + // 添加 MainClass + if (instance.JsonObject["mainClass"] is null) throw new Exception("实例 JSON 中没有 mainClass 项!"); + + Result += " " + instance.JsonObject["mainClass"]; + + return Result; + } + + // Game 部分(第二段) + private static string McLaunchArgumentsGameOld(ModMinecraft.McInstance Version) + { + var DataList = new List(); + + // 添加 RetroWrapper 相关参数 + if (McLaunchNeedsRetroWrapper(Version)) DataList.Add("--tweakClass com.zero.retrowrapper.RetroTweaker"); + + // 本地化 Minecraft 启动信息 + var BasicString = Version.JsonObject["minecraftArguments"].ToString(); + if (!BasicString.Contains("--height")) + BasicString += " --height ${resolution_height} --width ${resolution_width}"; + DataList.Add(BasicString); + + var Result = DataList.Join(" "); + + // 特别改变 OptiFineTweaker + if ((Version.Info.HasForge || Version.Info.HasLiteLoader) && Version.Info.HasOptiFine) + { + // 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃! + if (Result.Contains("--tweakClass optifine.OptiFineForgeTweaker")) + { + ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + Result); + Result = Result.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "") + .Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") + + " --tweakClass optifine.OptiFineForgeTweaker"; + } + + if (Result.Contains("--tweakClass optifine.OptiFineTweaker")) + { + ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + Result); + Result = Result.Replace(" --tweakClass optifine.OptiFineTweaker", "") + .Replace("--tweakClass optifine.OptiFineTweaker ", "") + + " --tweakClass optifine.OptiFineForgeTweaker"; + try + { + ModBase.WriteFile(Version.PathInstance + Version.Name + ".json", + ModBase.ReadFile(Version.PathInstance + Version.Name + ".json") + .Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker")); + } + catch (Exception ex) + { + ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败"); + } + } + } + + return Result; + } + + private static string McLaunchArgumentsGameNew(ModMinecraft.McInstance instance) + { + string McLaunchArgumentsGameNewRet = default; + var dataList = new List(); + + // 获取 Json 中的 DataList + var currentInstance = instance; + NextInstance: ; + + if (currentInstance.JsonObject["arguments"] is not null && + currentInstance.JsonObject["arguments"]["game"] is not null) + foreach (var SubJson in currentInstance.JsonObject["arguments"]["game"]) + if (SubJson.Type == JTokenType.String) + { + // 字符串类型 + dataList.Add(SubJson.ToString()); + } + // 非字符串类型 + else if (ModMinecraft.McJsonRuleCheck(SubJson["rules"])) + { + // 满足准则 + if (SubJson["value"].Type == JTokenType.String) + dataList.Add(SubJson["value"].ToString()); + else + foreach (var value in SubJson["value"]) + dataList.Add(value.ToString()); + } + + if (!string.IsNullOrEmpty(currentInstance.InheritInstanceName)) + { + currentInstance = new ModMinecraft.McInstance(currentInstance.InheritInstanceName); + goto NextInstance; + } + + // 将 "-XXX" 与后面 "XXX" 合并到一起 + // 如果不进行合并 Impact 会启动无效,它有两个 --tweakclass + var DeDuplicateDataList = new List(); + for (int i = 0, loopTo = dataList.Count - 1; i <= loopTo; i++) + { + var CurrentEntry = dataList[i]; + if (dataList[i].StartsWithF("-")) + while (i < dataList.Count - 1) + { + if (dataList[i + 1].StartsWithF("-")) break; + + i += 1; + CurrentEntry += " " + dataList[i]; + } + + DeDuplicateDataList.Add(CurrentEntry); + } + + // 去重 + McLaunchArgumentsGameNewRet = DeDuplicateDataList.Distinct().ToList().Join(" "); + + // 特别改变 OptiFineTweaker + if ((instance.Info.HasForge || instance.Info.HasLiteLoader) && instance.Info.HasOptiFine) + { + // 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃! + if (McLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineForgeTweaker")) + { + ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + McLaunchArgumentsGameNewRet); + McLaunchArgumentsGameNewRet = + McLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "") + .Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") + + " --tweakClass optifine.OptiFineForgeTweaker"; + } + + if (McLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineTweaker")) + { + ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + McLaunchArgumentsGameNewRet); + McLaunchArgumentsGameNewRet = + McLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineTweaker", "") + .Replace("--tweakClass optifine.OptiFineTweaker ", "") + + " --tweakClass optifine.OptiFineForgeTweaker"; + try + { + ModBase.WriteFile(instance.PathInstance + instance.Name + ".json", + ModBase.ReadFile(instance.PathInstance + instance.Name + ".json") + .Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker")); + } + catch (Exception ex) + { + ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败"); + } + } + } + + return McLaunchArgumentsGameNewRet; + } + + // 替换 Arguments + private static Dictionary McLaunchArgumentsReplace(ModMinecraft.McInstance instance, + ref ModLoader.LoaderTask> loader) + { + var GameArguments = new Dictionary(); + + // 基础参数 + GameArguments.Add("${classpath_separator}", ";"); + GameArguments.Add("${natives_directory}", ModBase.ShortenPath(GetNativesFolder())); + GameArguments.Add("${library_directory}", ModBase.ShortenPath(ModMinecraft.McFolderSelected + "libraries")); + GameArguments.Add("${libraries_directory}", ModBase.ShortenPath(ModMinecraft.McFolderSelected + "libraries")); + GameArguments.Add("${launcher_name}", "PCLCE"); + GameArguments.Add("${launcher_version}", ModBase.VersionCode.ToString()); + GameArguments.Add("${version_name}", instance.Name); + var ArgumentInfo = + Conversions.ToString(ModBase.Setup.Get("VersionArgumentInfo", ModMinecraft.McInstanceSelected)); + GameArguments.Add("${version_type}", + Conversions.ToString(string.IsNullOrEmpty(ArgumentInfo) + ? Config.Launch.TypeInfo + : ArgumentInfo)); + GameArguments.Add("${game_directory}", + ModBase.ShortenPath(Strings.Left(ModMinecraft.McInstanceSelected.PathIndie, + ModMinecraft.McInstanceSelected.PathIndie.Count() - 1))); + GameArguments.Add("${assets_root}", ModBase.ShortenPath(ModMinecraft.McFolderSelected + "assets")); + GameArguments.Add("${user_properties}", "{}"); + GameArguments.Add("${auth_player_name}", McLoginLoader.Output.Name); + GameArguments.Add("${auth_uuid}", McLoginLoader.Output.Uuid); + GameArguments.Add("${auth_access_token}", McLoginLoader.Output.AccessToken); + GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken); + GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken); + GameArguments.Add("${user_type}", "msa"); // #1221 + + // 窗口尺寸参数 + Size GameSize; + switch (Config.Launch.GameWindowMode) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 2, false): // 与启动器尺寸一致 + { + Size Result; + ModBase.RunInUiWait(() => Result = new Size(ModBase.GetPixelSize(ModMain.FrmMain.PanForm.ActualWidth), + ModBase.GetPixelSize(ModMain.FrmMain.PanForm.ActualHeight))); + GameSize = Result; + GameSize.Height -= 29.5d * ModBase.DPI / 96d; // 标题栏高度 + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 3, false): // 自定义 + { + GameSize = new Size(Math.Max(100, (double)Config.Launch.GameWindowWidth), + Math.Max(100, (double)Config.Launch.GameWindowHeight)); + break; + } + + default: + { + GameSize = new Size(854d, 480d); + break; + } + } + + if (ModMinecraft.McInstanceSelected.Info.Drop <= 120 && McLaunchJavaSelected.Installation.MajorVersion <= 8 && + McLaunchJavaSelected.Installation.Version.Revision >= 200 && + McLaunchJavaSelected.Installation.Version.Revision <= 321 && + !ModMinecraft.McInstanceSelected.Info.HasOptiFine && !ModMinecraft.McInstanceSelected.Info.HasForge) + { + // 修复 #3463:1.12.2-,JRE 8u200~321 下窗口大小为设置大小的 DPI% 倍 + McLaunchLog($"已应用窗口大小过大修复({McLaunchJavaSelected.Installation.Version.Revision})"); + GameSize.Width /= ModBase.DPI / 96d; + GameSize.Height /= ModBase.DPI / 96d; + } + + GameArguments.Add("${resolution_width}", Math.Round(GameSize.Width).ToString()); + GameArguments.Add("${resolution_height}", Math.Round(GameSize.Height).ToString()); + + // Assets 相关参数 + GameArguments.Add("${game_assets}", + ModBase.ShortenPath(ModMinecraft.McFolderSelected + + @"assets\virtual\legacy")); // 1.5.2 的 pre-1.6 资源索引应与 legacy 合并 + GameArguments.Add("${assets_index_name}", ModMinecraft.McAssetsGetIndexName(instance)); + + // 支持库参数 + var LibList = ModMinecraft.McLibListGet(instance, true); + loader.Output = LibList; + var CpStrings = new List(); + string OptiFineCp = null; + + // RetroWrapper 释放 + if (McLaunchNeedsRetroWrapper(instance)) + { + var WrapperPath = ModMinecraft.McFolderSelected + @"libraries\retrowrapper\RetroWrapper.jar"; + try + { + ModBase.WriteFile(WrapperPath, ModBase.GetResourceStream("Resources/retro-wrapper.jar")); + CpStrings.Add(WrapperPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "RetroWrapper 释放失败"); + } + } + + foreach (var Library in LibList) + { + if (Library.IsNatives) + continue; + if (Library.Name is not null && + Library.Name.Contains("com.cleanroommc:cleanroom:0.2")) // Cleanroom 的主 Jar 必须放在 ClassPath 第一位 + CpStrings.Insert(0, Library.LocalPath); + if (Library.Name is not null && Library.Name == "optifine:OptiFine") + OptiFineCp = Library.LocalPath; + else + CpStrings.Add(Library.LocalPath); + } + + foreach (var library in Config.Instance.ClasspathHead[instance.PathInstance].Split(";")) // 自定义 Classpath 头部 + { + if (string.IsNullOrWhiteSpace(library)) + continue; + CpStrings.Insert(0, library); + } + + if (OptiFineCp is not null) + CpStrings.Insert(CpStrings.Count - 2, OptiFineCp); // OptiFine 的总是需要放到倒数第二位 + GameArguments.Add("${classpath}", CpStrings.Select(c => ModBase.ShortenPath(c)).Join(";")); + + return GameArguments; + } + + #endregion + + #region 解压 Natives + + private static void McLaunchNatives(ModLoader.LoaderTask, int> Loader) + { + // 创建文件夹 + var Target = GetNativesFolder() + @"\"; + Directory.CreateDirectory(Target); + + // 解压文件 + McLaunchLog("正在解压 Natives 文件"); + var ExistFiles = new List(); + foreach (var Native in Loader.Input) + { + if (!Native.IsNatives) + continue; + ZipArchive Zip; + try + { + Zip = new ZipArchive(new FileStream(Native.LocalPath, FileMode.Open)); + } + catch (InvalidDataException ex) + { + ModBase.Log(ex, "打开 Natives 文件失败(" + Native.LocalPath + ")"); + File.Delete(Native.LocalPath); + throw new Exception("无法打开 Natives 文件(" + Native.LocalPath + "),该文件可能已损坏,请重新尝试启动游戏"); + } + + foreach (var Entry in Zip.Entries) + { + var FileName = Entry.FullName; + if (FileName.EndsWithF(".dll", true)) + { + // 实际解压文件的步骤 + var FilePath = Target + FileName; + ExistFiles.Add(FilePath); + var OriginalFile = new FileInfo(FilePath); + if (OriginalFile.Exists) + { + if (OriginalFile.Length == Entry.Length) + { + if (ModBase.ModeDebug) + McLaunchLog("无需解压:" + FilePath); + continue; + } + + // 删除原文件 + try + { + File.Delete(FilePath); + } + catch (UnauthorizedAccessException ex) + { + McLaunchLog("删除原 dll 访问被拒绝,这通常代表有一个 MC 正在运行,跳过解压:" + FilePath); + McLaunchLog("实际的错误信息:" + ex); + break; + } + } + + // 解压新文件 + ModBase.WriteFile(FilePath, Entry.Open()); + McLaunchLog("已解压:" + FilePath); + } + } + + if (Zip is not null) + Zip.Dispose(); + } + + // 删除多余文件 + foreach (var FileName in Directory.GetFiles(Target)) + { + if (ExistFiles.Contains(FileName)) + continue; + try + { + McLaunchLog("删除:" + FileName); + File.Delete(FileName); + } + catch (UnauthorizedAccessException ex) + { + McLaunchLog("删除多余文件访问被拒绝,跳过删除步骤"); + McLaunchLog("实际的错误信息:" + ex); + return; + } + } + } + + /// + /// 获取 Natives 文件夹路径,不以 \ 结尾。 + /// + private static string GetNativesFolder() + { + var Result = ModMinecraft.McInstanceSelected.PathInstance + ModMinecraft.McInstanceSelected.Name + "-natives"; + if (ModBase.IsGBKEncoding || Result.IsASCII()) + return Result; + Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\.minecraft\bin\natives"; + if (Result.IsASCII()) + return Result; + return ModBase.OsDrive + @"ProgramData\PCL\natives"; + } + + #endregion + + #region 启动与前后处理 + + private static void McLaunchPrerun() + { + // 要求 Java 使用高性能显卡 + var javaExePath = McLaunchJavaSelected.Installation.JavawExePath ?? + McLaunchJavaSelected.Installation.JavaExePath; + try + { + ModMain.SetGPUPreference(javaExePath, Config.Launch.SetGpuPreference); + } + catch (Exception ex) + { + if (ProcessInterop.IsAdmin() || !Config.Launch.SetGpuPreference) + { + ModBase.Log(ex, "直接调整显卡设置失败"); + } + else + { + ModBase.Log(ex, "直接调整显卡设置失败,将以管理员权限重启 PCL 再次尝试"); + try + { + if (ProcessInterop.StartAsAdmin($"--gpu \"{javaExePath}\"").ExitCode == + (int)ModBase.ProcessReturnValues.TaskDone) + McLaunchLog("以管理员权限重启 PCL 并调整显卡设置成功"); + else + throw new Exception("调整过程中出现异常"); + } + catch (Exception exx) + { + ModBase.Log(exx, "调整显卡设置失败,Minecraft 可能会使用默认显卡运行", ModBase.LogLevel.Hint); + } + } + } + + // 更新 launcher_profiles.json + do + { + try + { + // 确保可用 + if (!(McLoginLoader.Output.Type == "Microsoft")) + break; + ModMinecraft.McFolderLauncherProfilesJsonCreate(ModMinecraft.McFolderSelected); + // 构建需要替换的 Json 对象 + var ReplaceJsonString = @" + { + ""authenticationDatabase"": { + ""00000111112222233333444445555566"": { + ""username"": """ + McLoginLoader.Output.Name.Replace("\"", "-") + @""", + ""profiles"": { + ""66666555554444433333222221111100"": { + ""displayName"": """ + McLoginLoader.Output.Name + @""" + } + } + } + }, + ""clientToken"": """ + McLoginLoader.Output.ClientToken + @""", + ""selectedUser"": { + ""account"": ""00000111112222233333444445555566"", + ""profile"": ""66666555554444433333222221111100"" + } + }"; + var ReplaceJson = (JObject)ModBase.GetJson(ReplaceJsonString); + // 更新文件 + var Profiles = + (JObject)ModBase.GetJson( + ModBase.ReadFile(ModMinecraft.McFolderSelected + "launcher_profiles.json")); + Profiles.Merge(ReplaceJson); + ModBase.WriteFile(ModMinecraft.McFolderSelected + "launcher_profiles.json", Profiles.ToString(), + Encoding: Encoding.GetEncoding("GB18030")); + McLaunchLog("已更新 launcher_profiles.json"); + } + catch (Exception ex) + { + ModBase.Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试"); + try + { + File.Delete(ModMinecraft.McFolderSelected + "launcher_profiles.json"); + ModMinecraft.McFolderLauncherProfilesJsonCreate(ModMinecraft.McFolderSelected); + // 构建需要替换的 Json 对象 + var ReplaceJsonString = @" + { + ""authenticationDatabase"": { + ""00000111112222233333444445555566"": { + ""username"": """ + McLoginLoader.Output.Name.Replace("\"", "-") + @""", + ""profiles"": { + ""66666555554444433333222221111100"": { + ""displayName"": """ + McLoginLoader.Output.Name + @""" + } + } + } + }, + ""clientToken"": """ + McLoginLoader.Output.ClientToken + @""", + ""selectedUser"": { + ""account"": ""00000111112222233333444445555566"", + ""profile"": ""66666555554444433333222221111100"" + } + }"; + var ReplaceJson = (JObject)ModBase.GetJson(ReplaceJsonString); + // 更新文件 + var Profiles = + (JObject)ModBase.GetJson( + ModBase.ReadFile(ModMinecraft.McFolderSelected + "launcher_profiles.json")); + Profiles.Merge(ReplaceJson); + ModBase.WriteFile(ModMinecraft.McFolderSelected + "launcher_profiles.json", Profiles.ToString(), + Encoding: Encoding.GetEncoding("GB18030")); + McLaunchLog("已在删除后更新 launcher_profiles.json"); + } + catch (Exception exx) + { + ModBase.Log(exx, "更新 launcher_profiles.json 失败", ModBase.LogLevel.Feedback); + } + } + } while (false); + + // 更新 options.txt + var SetupFileAddress = ModMinecraft.McInstanceSelected.PathIndie + "options.txt"; + + // 辅助切换游戏语言 + if (Config.Tool.AutoChangeLanguage) + { + if (!File.Exists(SetupFileAddress)) + { + // Yosbr Mod 兼容(#2385):https://www.curseforge.com/minecraft/mc-mods/yosbr + var YosbrFileAddress = ModMinecraft.McInstanceSelected.PathIndie + @"config\yosbr\options.txt"; + if (File.Exists(YosbrFileAddress)) + { + McLaunchLog("将修改 Yosbr Mod 中的 options.txt"); + SetupFileAddress = YosbrFileAddress; + ModBase.WriteIni(SetupFileAddress, "lang", "none"); // 忽略默认语言 + } + } + + try + { + // 语言 + // 1.0- :没有语言选项 + // 1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃) + // 1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文 + // 1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文 + // 1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文 + var CurrentLang = ModBase.ReadIni(SetupFileAddress, "lang", "none"); + string RequiredLang; // 需要的语言 + var hasExistingSaves = Directory.Exists(ModMinecraft.McInstanceSelected.PathIndie + "saves"); + var shouldUseDefault = CurrentLang == "none" || !hasExistingSaves; + + // 获取 Minecraft 版本信息 + DateTime? mcReleaseTime = ModMinecraft.McInstanceSelected.ReleaseTime; + var isUnder1dot1 = + (bool)((new DateTime(2000, 1, 1) is var arg3 && mcReleaseTime.HasValue + ? mcReleaseTime.Value > arg3 + : (bool?)null) is var arg5 && arg5.HasValue && !arg5.Value ? false : + !((new DateTime(2011, 11, 18) is var arg4 && mcReleaseTime.HasValue + ? mcReleaseTime.Value <= arg4 + : (bool?)null) is { } arg6) ? null : + arg6 ? arg5 : false); // 1.11 发布日期 + + // 对于 1.0 及以下版本,没有语言选项,返回 "none" + if (isUnder1dot1) + { + RequiredLang = "none"; + } + else + { + // 根据配置确定默认语言 + var defaultLang = "zh_cn"; + RequiredLang = shouldUseDefault ? defaultLang : CurrentLang.ToLower(); + + // 应用版本特定的语言格式规则 + if (((new DateTime(2012, 1, 12) is var arg7 && mcReleaseTime.HasValue + ? mcReleaseTime.Value >= arg7 + : (bool?)null) is var arg9 && arg9.HasValue && !arg9.Value ? false : + !((new DateTime(2016, 6, 8) is var arg8 && mcReleaseTime.HasValue + ? mcReleaseTime.Value <= arg8 + : (bool?)null) is { } arg10) ? null : + arg10 ? arg9 : false) == true) + // 1.1~1.10:最后两位字母必须大写(zh_CN) + RequiredLang = "zh_CN"; + } + + if ((CurrentLang ?? "") == (RequiredLang ?? "")) + { + McLaunchLog($"需要的语言为 {RequiredLang},当前语言为 {CurrentLang},无需修改"); + } + else + { + ModBase.WriteIni(SetupFileAddress, "lang", "-"); // 触发缓存更改,避免删除后重新下载残留缓存 + ModBase.WriteIni(SetupFileAddress, "lang", RequiredLang); + McLaunchLog($"已将语言从 {CurrentLang} 修改为 {RequiredLang}"); + } + + // 如果是初次设置,一并修改 forceUnicodeFont,确保中文能正常显示 + if (CurrentLang == "none" || !Directory.Exists(ModMinecraft.McInstanceSelected.PathIndie + "saves")) + { + ModBase.WriteIni(SetupFileAddress, "forceUnicodeFont", "true"); + McLaunchLog("已开启 forceUnicodeFont,确保中文字体正常显示"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "更新 options.txt 失败", ModBase.LogLevel.Hint); + } + } + + // 窗口 + switch (Config.Launch.GameWindowMode) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 全屏 + { + ModBase.WriteIni(SetupFileAddress, "fullscreen", "true"); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): // 默认 + // 其他 + { + break; + } + + default: + { + ModBase.WriteIni(SetupFileAddress, "fullscreen", "false"); + break; + } + } + } + + private static void McLaunchCustom(ModLoader.LoaderTask Loader) + { + // 获取自定义命令 + var CustomCommandGlobal = Conversions.ToString(Config.Launch.PreLaunchCommand); + if (!string.IsNullOrEmpty(CustomCommandGlobal)) + CustomCommandGlobal = ArgumentReplace(CustomCommandGlobal, true); + var CustomCommandVersion = + Conversions.ToString(ModBase.Setup.Get("VersionAdvanceRun", ModMinecraft.McInstanceSelected)); + if (!string.IsNullOrEmpty(CustomCommandVersion)) + CustomCommandVersion = ArgumentReplace(CustomCommandVersion, true); + + // 输出 bat + try + { + var CmdString = + $"{(McLaunchJavaSelected.Installation.MajorVersion > 8 ? "chcp 65001>nul" + "\r\n" : "")}" + + "@echo off" + "\r\n" + $"title 启动 - {ModMinecraft.McInstanceSelected.Name}" + + "\r\n" + "echo 游戏正在启动,请稍候。" + "\r\n" + + $"cd /D \"{ModBase.ShortenPath(ModMinecraft.McInstanceSelected.PathIndie)}\"" + "\r\n" + + CustomCommandGlobal + "\r\n" + CustomCommandVersion + "\r\n" + + $"\"{McLaunchJavaSelected.Installation.JavaExePath}\" {McLaunchArgument}" + "\r\n" + + "echo 游戏已退出。" + "\r\n" + "pause"; + ModBase.WriteFile(CurrentLaunchOptions.SaveBatch ?? ModBase.ExePath + @"PCL\LatestLaunch.bat", + ModMinecraft.FilterAccessToken(CmdString, 'F'), + Encoding: McLaunchJavaSelected.Installation.MajorVersion > 8 ? Encoding.UTF8 : Encoding.Default); + if (CurrentLaunchOptions.SaveBatch is not null) + { + McLaunchLog("导出启动脚本完成,强制结束启动过程"); + AbortHint = "导出启动脚本成功!"; + ModBase.OpenExplorer(CurrentLaunchOptions.SaveBatch); + Loader.Parent.Abort(); + return; // 导出脚本完成 + } + } + catch (Exception ex) + { + ModBase.Log(ex, "输出启动脚本失败"); + if (CurrentLaunchOptions.SaveBatch is not null) + throw; // 直接触发启动失败 + } + + // 执行自定义命令 + if (!string.IsNullOrEmpty(CustomCommandGlobal)) + { + McLaunchLog("正在执行全局自定义命令:" + CustomCommandGlobal); + var CustomProcess = new Process(); + try + { + CustomProcess.StartInfo.FileName = "cmd.exe"; + CustomProcess.StartInfo.Arguments = "/c \"" + CustomCommandGlobal + "\""; + CustomProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModMinecraft.McFolderSelected); + CustomProcess.StartInfo.UseShellExecute = false; + CustomProcess.StartInfo.CreateNoWindow = true; + CustomProcess.Start(); + if (Conversions.ToBoolean(Config.Launch.PreLaunchCommandWait)) + while (!CustomProcess.HasExited && !Loader.IsAborted) + Thread.Sleep(10); + } + catch (Exception ex) + { + ModBase.Log(ex, "执行全局自定义命令失败", ModBase.LogLevel.Hint); + } + finally + { + if (!CustomProcess.HasExited && Loader.IsAborted) + { + McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183 + CustomProcess.Kill(); + } + } + } + + if (!string.IsNullOrEmpty(CustomCommandVersion)) + { + McLaunchLog("正在执行实例自定义命令:" + CustomCommandVersion); + var CustomProcess = new Process(); + try + { + CustomProcess.StartInfo.FileName = "cmd.exe"; + CustomProcess.StartInfo.Arguments = "/c \"" + CustomCommandVersion + "\""; + CustomProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModMinecraft.McFolderSelected); + CustomProcess.StartInfo.UseShellExecute = false; + CustomProcess.StartInfo.CreateNoWindow = true; + CustomProcess.Start(); + if (Conversions.ToBoolean(ModBase.Setup.Get("VersionAdvanceRunWait", ModMinecraft.McInstanceSelected))) + while (!CustomProcess.HasExited && !Loader.IsAborted) + Thread.Sleep(10); + } + catch (Exception ex) + { + ModBase.Log(ex, "执行实例自定义命令失败", ModBase.LogLevel.Hint); + } + finally + { + if (!CustomProcess.HasExited && Loader.IsAborted) + { + McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183 + CustomProcess.Kill(); + } + } + } + } + + private static void McLaunchRun(ModLoader.LoaderTask Loader) + { + var noJavaw = Conversions.ToBoolean((bool)Config.Launch.NoJavaw && + McLaunchJavaSelected.Installation.JavawExePath is not null); + + // 启动信息 + var GameProcess = new Process(); + var StartInfo = new ProcessStartInfo(noJavaw + ? McLaunchJavaSelected.Installation.JavaExePath + : McLaunchJavaSelected.Installation.JavawExePath); + + // 设置环境变量 + var Paths = new List(StartInfo.EnvironmentVariables["Path"].Split(";")); + Paths.Add(ModBase.ShortenPath(McLaunchJavaSelected.Installation.JavaFolder)); + StartInfo.EnvironmentVariables["Path"] = Paths.Distinct().ToList().Join(";"); + StartInfo.EnvironmentVariables["appdata"] = ModBase.ShortenPath(ModMinecraft.McFolderSelected); + + // 设置其他参数 + StartInfo.WorkingDirectory = ModBase.ShortenPath(ModMinecraft.McInstanceSelected.PathIndie); + StartInfo.UseShellExecute = false; + StartInfo.RedirectStandardOutput = true; + StartInfo.RedirectStandardError = true; + StartInfo.CreateNoWindow = noJavaw; + StartInfo.Arguments = McLaunchArgument; + GameProcess.StartInfo = StartInfo; + + // 开始进程 + GameProcess.Start(); + McLaunchLog("已启动游戏进程:" + StartInfo.FileName); + if (Loader.IsAborted) + { + McLaunchLog("由于取消启动,已强制结束游戏进程"); // #1631 + GameProcess.Kill(); + return; + } + + Loader.Output = GameProcess; + McLaunchProcess = GameProcess; + // 进程优先级处理 + try + { + GameProcess.PriorityBoostEnabled = true; + switch (Config.Launch.ProcessPriority) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 高 + { + GameProcess.PriorityClass = ProcessPriorityClass.AboveNormal; + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 2, false): // 低 + { + GameProcess.PriorityClass = ProcessPriorityClass.BelowNormal; // 中 + break; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "设置进程优先级失败", ModBase.LogLevel.Feedback); + } + } + + private static void McLaunchWait(ModLoader.LoaderTask Loader) + { + // 输出信息 + McLaunchLog(""); + McLaunchLog("~ 基础参数 ~"); + McLaunchLog("PCL 版本:" + ModBase.VersionBaseName + " (" + ModBase.VersionCode + ")"); + McLaunchLog( + $"游戏版本:{ModMinecraft.McInstanceSelected.Info.VanillaName}({ModMinecraft.McInstanceSelected.Info.Vanilla},Drop {ModMinecraft.McInstanceSelected.Info.Drop}{(ModMinecraft.McInstanceSelected.Info.Reliable ? "" : ",无法完全确定")})"); + McLaunchLog("资源版本:" + ModMinecraft.McAssetsGetIndexName(ModMinecraft.McInstanceSelected)); + McLaunchLog("实例继承:" + (string.IsNullOrEmpty(ModMinecraft.McInstanceSelected.InheritInstanceName) + ? "无" + : ModMinecraft.McInstanceSelected.InheritInstanceName)); + McLaunchLog("分配的内存:" + + PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected, + !McLaunchJavaSelected.Installation.Is64Bit) + " GB(" + + Math.Round(PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected, + !McLaunchJavaSelected.Installation.Is64Bit) * 1024d) + " MB)"); + McLaunchLog("MC 文件夹:" + ModMinecraft.McFolderSelected); + McLaunchLog("实例文件夹:" + ModMinecraft.McInstanceSelected.PathInstance); + McLaunchLog("版本隔离:" + ((ModMinecraft.McInstanceSelected.PathIndie ?? "") == + (ModMinecraft.McInstanceSelected.PathInstance ?? ""))); + McLaunchLog("HMCL 格式:" + ModMinecraft.McInstanceSelected.IsHmclFormatJson); + McLaunchLog("Java 信息:" + (McLaunchJavaSelected is not null ? McLaunchJavaSelected.ToString : "无可用 Java")); + // McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置")) + McLaunchLog("Natives 文件夹:" + GetNativesFolder()); + McLaunchLog(""); + McLaunchLog("~ 档案参数 ~"); + McLaunchLog("玩家用户名:" + McLoginLoader.Output.Name); + McLaunchLog("AccessToken:" + McLoginLoader.Output.AccessToken); + McLaunchLog("ClientToken:" + McLoginLoader.Output.ClientToken); + McLaunchLog("UUID:" + McLoginLoader.Output.Uuid); + McLaunchLog("验证方式:" + McLoginLoader.Output.Type); + McLaunchLog(""); + + // 获取窗口标题 + var WindowTitle = (string?)ModBase.Setup.Get("VersionArgumentTitle", ModMinecraft.McInstanceSelected); + if (string.IsNullOrEmpty(WindowTitle) && + !(bool)ModBase.Setup.Get("VersionArgumentTitleEmpty", ModMinecraft.McInstanceSelected)) + WindowTitle = Conversions.ToString(Config.Launch.Title); + WindowTitle = ArgumentReplace(WindowTitle, false); + + // JStack 路径 + var JStackPath = McLaunchJavaSelected.Installation.JavaFolder + @"\jstack.exe"; + + // 初始化等待 + var Watcher = new ModWatcher.Watcher(Loader, ModMinecraft.McInstanceSelected, WindowTitle, + File.Exists(JStackPath) ? JStackPath : "", CurrentLaunchOptions.IsTest); + McLaunchWatcher = Watcher; + + // 显示实时日志 + if (CurrentLaunchOptions.IsTest) + { + if (ModMain.FrmLogLeft is null) + ModBase.RunInUiWait(() => ModMain.FrmLogLeft = new PageLogLeft()); + if (ModMain.FrmLogRight is null) + ModBase.RunInUiWait(() => + { + ModAnimation.AniControlEnabled += 1; + ModMain.FrmLogRight = new PageLogRight(); + ModAnimation.AniControlEnabled -= 1; + }); + ModMain.FrmLogLeft.Add(Watcher); + McLaunchLog("已显示游戏实时日志"); + } + + // 等待 + while (Watcher.State == ModWatcher.Watcher.MinecraftState.Loading) + Thread.Sleep(100); + if (Watcher.State == ModWatcher.Watcher.MinecraftState.Crashed) throw new Exception("$$"); + } + + private static void McLaunchEnd() + { + McLaunchLog("开始启动结束处理"); + + // 暂停或开始音乐播放 + if (Conversions.ToBoolean(Config.Preference.Music.StopInGame)) + ModBase.RunInUi(() => + { + if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在启动后暂停音乐播放"); + }); + else if (Conversions.ToBoolean(Config.Preference.Music.StartInGame)) + ModBase.RunInUi(() => + { + if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在启动后开始音乐播放"); + }); + // 暂停视频背景播放 + ModVideoBack.IsGaming = true; + ModVideoBack.VideoPause(); + // 启动器可见性 + McLaunchLog( + Conversions.ToString(Operators.ConcatenateObject("启动器可见性:", Config.Launch.LauncherVisibility))); + switch (Config.Launch.LauncherVisibility) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + // 直接关闭 + McLaunchLog("已根据设置,在启动后关闭启动器"); + ModBase.RunInUi(() => ModMain.FrmMain.EndProgram(false)); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 2, false): + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 3, false): + { + // 隐藏 + McLaunchLog("已根据设置,在启动后隐藏启动器"); + ModBase.RunInUi(() => ModMain.FrmMain.Hidden = true); + break; + } + case var case3 when Operators.ConditionalCompareObjectEqual(case3, 4, false): + { + // 最小化 + McLaunchLog("已根据设置,在启动后最小化启动器"); + ModBase.RunInUi(() => ModMain.FrmMain.WindowState = WindowState.Minimized); + break; + } + case var case4 when Operators.ConditionalCompareObjectEqual(case4, 5, false): + { + break; + } + // 啥都不干 + } + + // 启动计数 + States.System.LaunchCount += 1; + + ModBase.Setup.Set("VersionLaunchCount", + Operators.AddObject(ModBase.Setup.Get("VersionLaunchCount", ModMinecraft.McInstanceSelected), 1), + instance: ModMinecraft.McInstanceSelected); + } + + /// + /// 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。 + /// + private static string ArgumentReplace(string text, bool replaceTime, Func escapeHandler = null) + { + // 预处理 + if (text is null) + return null; + + string replacer(string s) + { + if (s is null) + return ""; + if (escapeHandler is null) + return s; + if (s.Contains(@":\")) + s = ModBase.ShortenPath(s); + return escapeHandler(s); + } + + ; + // 基础 + text = text.Replace("{pcl_version}", replacer(ModBase.VersionBaseName)); + text = text.Replace("{pcl_version_code}", replacer(ModBase.VersionCode.ToString())); + text = text.Replace("{pcl_version_branch}", replacer(ModBase.VersionBranchName)); + text = text.Replace("{identify}", replacer(Identify.LauncherId)); + text = text.Replace("{path}", replacer(Basics.CurrentDirectory)); + text = text.Replace("{path_with_name}", replacer(Basics.ExecutablePath)); + text = text.Replace("{path_temp}", replacer(ModBase.PathTemp)); + // 时间 + if (replaceTime) // 在窗口标题中,时间会被后续动态替换,所以此时不应该替换 + { + text = text.Replace("{date}", replacer(DateTime.Now.ToString("yyyy'/'M'/'d"))); + text = text.Replace("{time}", replacer(DateTime.Now.ToString("HH':'mm':'ss"))); + } + + // Minecraft + text = text.Replace("{java}", replacer(McLaunchJavaSelected?.Installation.JavaFolder)); + text = text.Replace("{minecraft}", replacer(ModMinecraft.McFolderSelected)); + if (ModMinecraft.McInstanceSelected?.IsLoaded == true) + { + text = text.Replace("{version_path}", replacer(ModMinecraft.McInstanceSelected.PathInstance)); + text = text.Replace("{verpath}", replacer(ModMinecraft.McInstanceSelected.PathInstance)); + text = text.Replace("{version_indie}", replacer(ModMinecraft.McInstanceSelected.PathIndie)); + text = text.Replace("{verindie}", replacer(ModMinecraft.McInstanceSelected.PathIndie)); + text = text.Replace("{name}", replacer(ModMinecraft.McInstanceSelected.Name)); + if (new[] { "unknown", "old", "pending" }.Contains( + ModMinecraft.McInstanceSelected.Info.VanillaName.ToLower())) + text = text.Replace("{version}", replacer(ModMinecraft.McInstanceSelected.Name)); + else + text = text.Replace("{version}", replacer(ModMinecraft.McInstanceSelected.Info.VanillaName)); + } + else + { + text = text.Replace("{version_path}", replacer(null)); + text = text.Replace("{verpath}", replacer(null)); + text = text.Replace("{version_indie}", replacer(null)); + text = text.Replace("{verindie}", replacer(null)); + text = text.Replace("{name}", replacer(null)); + text = text.Replace("{version}", replacer(null)); + } + + // 登录信息 + if (McLoginLoader.State == ModBase.LoadState.Finished) + { + text = text.Replace("{user}", replacer(McLoginLoader.Output.Name)); + text = text.Replace("{uuid}", replacer(McLoginLoader.Output.Uuid?.ToLower())); + switch (McLoginLoader.Input.Type) + { + case McLoginType.Legacy: + { + text = text.Replace("{login}", replacer("离线")); + break; + } + case McLoginType.Ms: + { + text = text.Replace("{login}", replacer("正版")); + break; + } + case McLoginType.Auth: + { + text = text.Replace("{login}", replacer("Authlib-Injector")); + break; + } + } + } + else + { + text = text.Replace("{user}", replacer(null)); + text = text.Replace("{uuid}", replacer(null)); + text = text.Replace("{login}", replacer(null)); + } + + return text; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.cs new file mode 100644 index 000000000..66cd70d1e --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.cs @@ -0,0 +1,2609 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using fNbt; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.Utils; +using static PCL.ModComp; +using static PCL.ModLoader; + +namespace PCL; + +public static class ModLocalComp +{ + private const int LocalModCacheVersion = 7; + + public class LocalCompFile + { + /// + /// 是否可能为前置 Mod。 + /// + public bool IsPresetMod() + { + return !Dependencies.Any() && Name is not null && + (Name.ToLower().Contains("core") || Name.ToLower().Contains("lib")); + } + + /// + /// 根据完整文件路径的文件扩展名判断是否为 Mod 文件。 + /// + public static bool IsModFile(string Path) + { + if (Path is null || !Path.Contains(".")) + return false; + Path = Path.ToLower(); + if (Path.EndsWithF(".jar", true) || Path.EndsWithF(".zip", true) || Path.EndsWithF(".litemod", true) || + Path.EndsWithF(".jar.disabled", true) || Path.EndsWithF(".zip.disabled", true) || + Path.EndsWithF(".litemod.disabled", true) || Path.EndsWithF(".jar.old", true) || + Path.EndsWithF(".zip.old", true) || Path.EndsWithF(".litemod.old", true)) + return true; + return false; + } + + /// + /// 检查是否为指定类型的组件文件。 + /// + public static bool IsCompFile(string Path, CompType CompType) + { + if (Path is null || !Path.Contains(".")) + return false; + Path = Path.ToLower(); + switch (CompType) + { + case CompType.Mod: + { + return IsModFile(Path); + } + case CompType.ResourcePack: + case CompType.Shader: + { + return Path.EndsWithF(".zip", true); + } + case CompType.DataPack: + { + return Path.EndsWithF(".zip", true) || Path.EndsWithF(".zip.disabled", true); + } + case CompType.Schematic: + { + return Path.EndsWithF(".litematic", true) || Path.EndsWithF(".nbt", true) || + Path.EndsWithF(".schematic", true) || Path.EndsWithF(".schem", true); + } + + default: + { + return false; + } + } + } + + /// + /// 获取图标路径。 + /// + public string GetLogo() + { + if (Comp is not null && Comp.LogoUrl is not null) + return Comp.LogoUrl; + if (Logo is not null) + return Logo; + + // 为文件夹设置特定图标 + if (IsFolder) return "pack://application:,,,/images/Icons/Folder.png"; + + return ModBase.PathImage + "Icons/NoIcon.png"; + } + + #region Litematic 文件处理 + + /// + /// 读取 Litematic 文件的 NBT 数据。 + /// + private void LoadLitematicNbtData() + { + try + { + ModBase.Log($"开始读取 Litematic NBT 数据:{Path}", ModBase.LogLevel.Debug); + using (var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var scheNbt = new NbtFile(); + scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); + // 读取版本信息 + var versionTag = (NbtInt)scheNbt.RootTag.Get("Version"); + if (versionTag is not null) _litematicVersion = versionTag.Value; + + // 读取 Metadata 节点 + var metadataTag = scheNbt.RootTag.Get("Metadata"); + if (metadataTag is not null) + { + ModBase.Log("找到 Litematic Metadata 节点", ModBase.LogLevel.Debug); + + // 读取名称 + var nameTag = metadataTag.Get("Name"); + if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value) && + nameTag.Value != "Unnamed") _litematicOriginalName = nameTag.Value; + + // 读取描述信息 + var descriptionTag = metadataTag.Get("Description"); + if (descriptionTag is not null && !string.IsNullOrWhiteSpace(descriptionTag.Value)) + _Description = descriptionTag.Value; + + // 读取作者信息 + var authorTag = metadataTag.Get("Author"); + if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) + _Authors = authorTag.Value; + + // 读取时间信息 + var timeCreatedTag = metadataTag.Get("TimeCreated"); + if (timeCreatedTag is not null) _litematicTimeCreated = timeCreatedTag.Value; + + var timeModifiedTag = metadataTag.Get("TimeModified"); + if (timeModifiedTag is not null) _litematicTimeModified = timeModifiedTag.Value; + + // 读取包围盒大小 + var enclosingSizeTag = metadataTag.Get("EnclosingSize"); + if (enclosingSizeTag is not null) + { + var xTag = enclosingSizeTag.Get("x"); + var yTag = enclosingSizeTag.Get("y"); + var zTag = enclosingSizeTag.Get("z"); + if (xTag is not null && yTag is not null && zTag is not null) + _litematicEnclosingSize = $"{xTag.Value} × {yTag.Value} × {zTag.Value}"; + } + + // 读取区域数量 + var regionCountTag = metadataTag.Get("RegionCount"); + if (regionCountTag is not null) _litematicRegionCount = regionCountTag.Value; + + // 读取总方块数 + var totalBlocksTag = metadataTag.Get("TotalBlocks"); + if (totalBlocksTag is not null) _litematicTotalBlocks = totalBlocksTag.Value; + + // 读取总体积 + var totalVolumeTag = metadataTag.Get("TotalVolume"); + if (totalVolumeTag is not null) _litematicTotalVolume = totalVolumeTag.Value; + } + else + { + ModBase.Log("未找到 Litematic Metadata 节点", ModBase.LogLevel.Debug); + } + } + + ModBase.Log("Litematic NBT 数据读取完成", ModBase.LogLevel.Debug); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 Litematic NBT 数据时出错(" + Path + ")"); + } + } + + #endregion + + #region Schem 文件处理 + + /// + /// 读取 .schem 文件的 NBT 数据(Sponge Schematic 格式)。 + /// + private void LoadSchemNbtData() + { + try + { + ModBase.Log($"开始读取 Schem NBT 数据:{Path}", ModBase.LogLevel.Debug); + + // 使用自动检测压缩格式 + using (var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var scheNbt = new NbtFile(); + scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); + + // 读取Sponge版本信息 + var versionTag = scheNbt.RootTag.Get("Version"); + if (versionTag is not null) _spongeVersion = versionTag.Value; + + // 读取数据版本信息 + var dataVersionTag = scheNbt.RootTag.Get("DataVersion"); + if (dataVersionTag is not null) _structureDataVersion = dataVersionTag.Value; + + // 读取尺寸信息 + var widthTag = scheNbt.RootTag.Get("Width"); + var heightTag = scheNbt.RootTag.Get("Height"); + var lengthTag = scheNbt.RootTag.Get("Length"); + + if (widthTag is not null && heightTag is not null && lengthTag is not null) + { + _litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}"; + _litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value; + + // 对于Sponge格式,方块数量等于总体积(因为包含空气方块) + _litematicTotalBlocks = _litematicTotalVolume; + } + + // 读取调色板信息来计算区域数量 + var paletteTag = scheNbt.RootTag.Get("Palette"); + if (paletteTag is not null) _litematicRegionCount = 1; // Sponge Schematic 通常只有一个区域 + + // 读取元数据 + var metadataTag = scheNbt.RootTag.Get("Metadata"); + if (metadataTag is not null) + { + // 读取名称 + var nameTag = metadataTag.Get("Name"); + if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value)) + _schemOriginalName = nameTag.Value; + + // 读取作者信息 + var authorTag = metadataTag.Get("Author"); + if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) + { + _structureAuthor = authorTag.Value; + if (_Authors is null) + _Authors = _structureAuthor; + } + } + } + + ModBase.Log("Schem NBT 数据读取完成", ModBase.LogLevel.Debug); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 Schem NBT 数据时出错(" + Path + ")"); + } + } + + #endregion + + #region Schematic 文件处理 + + /// + /// 读取 .schematic 文件的 NBT 数据(MCEdit/WorldEdit 格式)。 + /// + private void LoadSchematicNbtData() + { + try + { + ModBase.Log($"开始读取 Schematic NBT 数据:{Path}", ModBase.LogLevel.Debug); + using (var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var scheNbt = new NbtFile(); + scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); + // 读取尺寸信息 + var widthTag = scheNbt.RootTag.Get("Width"); + var heightTag = scheNbt.RootTag.Get("Height"); + var lengthTag = scheNbt.RootTag.Get("Length"); + if (widthTag is not null && heightTag is not null && lengthTag is not null) + { + _litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}"; + _litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value; + } + + // 读取材料列表 + var materialsTag = scheNbt.RootTag.Get("Materials"); + if (materialsTag is not null) + ModBase.Log($"Schematic 材料类型:{materialsTag.Value}", ModBase.LogLevel.Debug); + + ModBase.Log("Schematic NBT 数据读取完成", ModBase.LogLevel.Debug); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 Schematic NBT 数据时出错(" + Path + ")"); + } + } + + #endregion + + #region NBT 结构文件处理 + + /// + /// 读取 .nbt 文件的 NBT 数据(Minecraft 结构文件格式)。 + /// + private void LoadStructureNbtData() + { + try + { + ModBase.Log($"开始读取 NBT 结构文件数据:{Path}", ModBase.LogLevel.Debug); + using (var fs = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var scheNbt = new NbtFile(); + scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); + // 读取作者信息 + var authorTag = scheNbt.RootTag.Get("author"); + if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) + { + _structureAuthor = authorTag.Value; + if (_Authors is null) + _Authors = _structureAuthor; + } + + // 读取尺寸信息 + var sizeTag = scheNbt.RootTag.Get("size"); + if (sizeTag is not null) + { + var sizeElements = sizeTag.ToArray(); + if (sizeElements.Length >= 3) + { + var sizeArray = sizeElements.Take(3).Select(e => e.IntValue).ToArray(); + _litematicEnclosingSize = $"{sizeArray[0]} × {sizeArray[1]} × {sizeArray[2]}"; + _litematicTotalVolume = sizeArray[0] * sizeArray[1] * sizeArray[2]; + } + } + + // 读取方块数量信息 + var blocksTag = scheNbt.RootTag.Get("blocks"); + if (blocksTag is not null) + _litematicTotalBlocks = blocksTag.Where(x => x.TagType == NbtTagType.Compound).Count(); + + // 读取调色板信息来计算区域数量 + var paletteTag = scheNbt.RootTag.Get("palette"); + if (paletteTag is not null) _litematicRegionCount = 1; // 原版结构文件通常只有一个区域 + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 NBT 结构文件数据时出错(" + Path + ")"); + } + } + + #endregion + + #region 基础 + + /// + /// 资源的文件的地址。 + /// + public readonly string Path; + + /// + /// 是否为文件夹项。 + /// + public bool IsFolder => Path.EndsWithF(@"\__FOLDER__", true); + + /// + /// 获取实际的文件夹路径(去除 __FOLDER__ 标记)。 + /// + public string ActualPath + { + get + { + if (IsFolder) return Path.Replace(@"\__FOLDER__", ""); + + return Path; + } + } + + public LocalCompFile(string Path) + { + this.Path = Path ?? ""; + } + + /// + /// NBT数据是否已加载(用于延迟加载优化)。 + /// + private bool _nbtDataLoaded; + + /// + /// Mod 资源的完整路径,去除最后的 .disabled 和 .old。 + /// + public string RawPath => ModBase.GetPathFromFullPath(Path) + RawFileName; + + /// + /// 资源的完整文件名。 + /// + public string FileName + { + get + { + if (IsFolder && !string.IsNullOrEmpty(Name)) return Name; + + return ModBase.GetFileNameFromPath(Path); + } + } + + /// + /// Mod 资源的完整文件名,去除最后的 .disabled 和 .old。 + /// + public string RawFileName => FileName.Replace(".disabled", "").Replace(".old", ""); + + /// + /// 资源的状态。对于 Mod 有 Disabled + /// + public LocalFileStatus State + { + get + { + Load(); + if (!IsFileAvailable) return LocalFileStatus.Unavailable; + + if (Path.EndsWithF(".disabled", true) || Path.EndsWithF(".old", true)) return LocalFileStatus.Disabled; + + return LocalFileStatus.Fine; + } + } + + public enum LocalFileStatus + { + Fine = 0, + Disabled = 1, + Unavailable = 2 + } + + #endregion + + #region 信息项 + + /// + /// Mod 的名称。若不可用则为 ModID 或无扩展的文件名。 + /// + public string Name + { + get + { + if (_Name is null) + Load(); + if (_Name is null) + _Name = _ModId; + if (_Name is null) + { + if (IsFolder) + _Name = ModBase.GetFolderNameFromPath(ActualPath); + else + _Name = ModBase.GetFileNameWithoutExtentionFromPath(Path); + } + + return _Name; + } + set + { + if (_Name is null && value is not null && !value.Contains("modname") && value.ToLower() != "name" && + value.Count() > 1 && (ModBase.Val(value).ToString() ?? "") != (value ?? "")) _Name = value; + } + } + + private string _Name; + + /// + /// Mod 的描述信息。 + /// + public string Description + { + get + { + if (_Description is null) + Load(); + if (_Description is null && FileUnavailableReason is not null) + _Description = FileUnavailableReason.Message; + // If _Description Is Nothing Then _Description = Path + return _Description; + } + set + { + if (_Description is null && value is not null && value.Count() > 2) + { + _Description = value.Trim(Conversions.ToChar("\n")); + // 优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号 + if (_Description.ToLower().LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789".ToCharArray()) == + _Description.Count() - 1) + _Description += "."; + } + } + } + + private string _Description; + + /// + /// 文件类型标签。 + /// + public List Tags + { + get + { + if (_tags is null) + { + _tags = new List(); + if (IsFolder) + { + _tags.Add("文件夹"); + } + else + { + var extension = System.IO.Path.GetExtension(RawPath).ToLower(); + switch (extension ?? "") + { + case ".litematic": + { + _tags.Add("原理图"); + break; + } + case ".schem": + case ".schematic": + { + _tags.Add("Schematic结构"); + break; + } + case ".nbt": + { + _tags.Add("原版结构"); + break; + } + } + } + } + + return _tags; + } + } + + private List _tags; + + /// + /// Mod 的版本,不保证符合版本格式规范。 + /// + public string Version + { + get + { + if (_Version is null) + Load(); + return _Version; + } + set + { + if (_Version is not null && _Version.RegexCheck(@"[0-9.\-]+")) + return; + if (value?.ContainsF("version", true) == true) + value = "version"; // 需要修改的标识 + _Version = value; + } + } + + public string _Version; + + /// + /// 用于依赖检查的 ModID。 + /// + public string ModId + { + get + { + if (_ModId is null) + Load(); + return _ModId; + } + set + { + if (value is null) + return; + value = value.RegexSeek(RegexPatterns.ModIdMatch); + if (value is null || value.Count() <= 1 || (ModBase.Val(value).ToString() ?? "") == (value ?? "")) + return; + if (value.ContainsF("name", true) || value.ContainsF("modid", true)) + return; + if (!PossibleModId.Contains(value)) + PossibleModId.Add(value); + if (_ModId is null) + _ModId = value; + } + } + + private string _ModId; + + /// + /// 其他可能的 ModID。 + /// + public List PossibleModId = new(); + + /// + /// Mod 的主页。 + /// + public string Url + { + get + { + if (_Url is null) + Load(); + return _Url; + } + set + { + if (_Url is null && value is not null && value.StartsWithF("http")) _Url = value; + } + } + + private string _Url; + + /// + /// Mod 的作者列表。 + /// + public string Authors + { + get + { + if (_Authors is null) + Load(); + return _Authors; + } + set + { + if (_Authors is null && !string.IsNullOrWhiteSpace(value)) _Authors = value; + } + } + + private string _Authors; + + /// + /// Litematic 文件的创建时间戳。 + /// + public long? LitematicTimeCreated + { + get + { + LoadNbtDataIfNeeded(); + return _litematicTimeCreated; + } + } + + private long? _litematicTimeCreated; + + /// + /// Litematic 文件的修改时间戳。 + /// + public long? LitematicTimeModified + { + get + { + LoadNbtDataIfNeeded(); + return _litematicTimeModified; + } + } + + private long? _litematicTimeModified; + + /// + /// Schem 读取到的原始名称。 + /// + public string SchemOriginalName + { + get + { + LoadNbtDataIfNeeded(); + return _schemOriginalName; + } + } + + private string _schemOriginalName; + + /// + /// Litematic 读取到的原始名称。 + /// + public string LitematicOriginalName + { + get + { + LoadNbtDataIfNeeded(); + return _litematicOriginalName; + } + } + + private string _litematicOriginalName; + + /// + /// Litematic 文件的版本。 + /// + public int? LitematicVersion + { + get + { + LoadNbtDataIfNeeded(); + return _litematicVersion; + } + } + + private int? _litematicVersion; + + /// + /// Litematic 文件的包围盒大小。 + /// + public string LitematicEnclosingSize + { + get + { + LoadNbtDataIfNeeded(); + return _litematicEnclosingSize; + } + } + + private string _litematicEnclosingSize; + + /// + /// Litematic 文件的区域数量。 + /// + public int? LitematicRegionCount + { + get + { + LoadNbtDataIfNeeded(); + return _litematicRegionCount; + } + } + + private int? _litematicRegionCount; + + /// + /// Litematic 文件的总方块数。 + /// + public int? LitematicTotalBlocks + { + get + { + LoadNbtDataIfNeeded(); + return _litematicTotalBlocks; + } + } + + private int? _litematicTotalBlocks; + + /// + /// Litematic 文件的总体积。 + /// + public int? LitematicTotalVolume + { + get + { + LoadNbtDataIfNeeded(); + return _litematicTotalVolume; + } + } + + private int? _litematicTotalVolume; + + /// + /// 原版结构文件的游戏版本。 + /// + public string StructureGameVersion + { + get + { + LoadNbtDataIfNeeded(); + return _structureGameVersion; + } + } + + private string _structureGameVersion; + + /// + /// 原版结构文件的数据版本。 + /// + public int? StructureDataVersion + { + get + { + LoadNbtDataIfNeeded(); + return _structureDataVersion; + } + } + + private int? _structureDataVersion; + + /// + /// 原版结构文件的作者。 + /// + public string StructureAuthor + { + get + { + LoadNbtDataIfNeeded(); + return _structureAuthor; + } + } + + private string _structureAuthor; + + /// + /// Sponge Schematic 文件的版本。 + /// + public int? SpongeVersion + { + get + { + LoadNbtDataIfNeeded(); + return _spongeVersion; + } + } + + private int? _spongeVersion; + + /// + /// Mod 图标路径。 + /// + public string Logo { get; set; } + + /// + /// 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。 + /// + public Dictionary Dependencies + { + get + { + Load(); + return _Dependencies; + } + } + + private Dictionary _Dependencies = new(); + + private void AddDependency(string ModID, string VersionRequirement = null) + { + // 确保信息正确 + if (ModID is null || ModID.Count() < 2) + return; + ModID = ModID.ToLower(); + if (ModID == "name" || (ModBase.Val(ModID).ToString() ?? "") == (ModID ?? "")) + return; // 跳过 name 与纯数字 id + if (VersionRequirement is null || + (!VersionRequirement.Contains(".") && !VersionRequirement.Contains("-")) || + VersionRequirement.Contains("$")) + VersionRequirement = null; + else if (!VersionRequirement.StartsWithF("[") && !VersionRequirement.StartsWithF("(") && + !VersionRequirement.EndsWithF("]") && !VersionRequirement.EndsWithF(")")) + VersionRequirement = "[" + VersionRequirement + ",)"; + // 向依赖项中添加 + if (_Dependencies.ContainsKey(ModID)) + { + if (_Dependencies[ModID] is null) + _Dependencies[ModID] = VersionRequirement; + } + else + { + _Dependencies.Add(ModID, VersionRequirement); + } + } + + #endregion + + #region 加载步骤标记 + + // 1. 进行文件可用性检查 + // 成功:继续第二步。 + // 失败:标记 FileUnavailableReason, 并停止后续加载。 + /// + /// 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步) + /// + private bool IsLoaded; + + /// + /// Mod 文件是否可被正常读取。 + /// + public bool IsFileAvailable + { + get + { + Load(); + return FileUnavailableReason is null; + } + } + + /// + /// Mod 文件出错的原因。若无错误,则为 Nothing。 + /// + public Exception FileUnavailableReason + { + get + { + Load(); + return _FileUnavailableReason; + } + } + + private Exception _FileUnavailableReason; + + // 2. 进行 .class 以外的信息获取 + // 成功:标记 IsInfoWithoutClassAvailable。 + // 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。 + /// + /// 是否已在不获取 .class 文件的前提下完成了所需信息的加载。 + /// + private bool IsInfoWithoutClassAvailable = false; + + // 3. 尝试从 .class 文件中获取信息 + // 成功:标记 IsInfoWithClassAvailable。 + // 失败:什么也不干。 + /// + /// 是否已进行 .class 文件的信息获取。 + /// + private bool IsInfoWithClassLoaded; + + /// + /// 是否已在 .class 文件中完成了所需信息的加载。 + /// + private bool IsInfoWithClassAvailable; + + #endregion + + #region 加载 + + /// + /// 初始化所有数据。 + /// + private void Init() + { + _Name = null; + _Description = null; + _Version = null; + _ModId = null; + PossibleModId = new List(); + _Dependencies = new Dictionary(); + IsLoaded = false; + _FileUnavailableReason = null; + IsInfoWithClassLoaded = false; + IsInfoWithClassAvailable = false; + } + + /// + /// 加载基本信息(不解析NBT数据)。 + /// + public void LoadBasicInfo() + { + try + { + // 可用性检查 + if (IsFolder) + { + // 文件夹项不需要进一步处理 + IsLoaded = true; + return; + } + + if (!File.Exists(Path)) + { + _FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + Path + ")"); + IsLoaded = true; + return; + } + + // 对于原理图文件,只设置基本状态,不解析NBT数据 + if (Path.EndsWithF(".litematic", true) || Path.EndsWithF(".nbt", true) || + Path.EndsWithF(".schem", true) || Path.EndsWithF(".schematic", true)) + { + _Name = ModBase.GetFileNameWithoutExtentionFromPath(Path); + IsLoaded = true; + return; + } + + // 对于其他文件类型,正常加载 + Load(); + } + catch (Exception ex) + { + ModBase.Log(ex, $"加载基本信息失败:{Path}"); + } + } + + /// + /// 延迟加载NBT数据。 + /// + public void LoadNbtDataIfNeeded() + { + try + { + // 如果已经加载过NBT数据,则跳过 + if (_nbtDataLoaded) + return; + + // 根据文件类型加载NBT数据 + if (Path.EndsWithF(".litematic", true)) + LoadLitematicNbtData(); + else if (Path.EndsWithF(".nbt", true)) + LoadStructureNbtData(); + else if (Path.EndsWithF(".schem", true)) + LoadSchemNbtData(); + else if (Path.EndsWithF(".schematic", true)) LoadSchematicNbtData(); + + _nbtDataLoaded = true; + } + catch (Exception ex) + { + ModBase.Log(ex, $"延迟加载NBT数据失败:{Path}"); + } + } + + /// + /// 进行文件可用性检查与 .class 以外的信息获取。 + /// + public void Load(bool ForceReload = false) + { + if (IsLoaded && !ForceReload) + return; + // 初始化 + Init(); + + // 基础可用性检查 + if (Path.Length < 2) + { + _FileUnavailableReason = new FileNotFoundException("错误的资源文件路径(" + (Path ?? "null") + ")"); + IsLoaded = true; + return; + } + + // 对于文件夹项,检查实际文件夹路径是否存在 + if (IsFolder) + { + if (!Directory.Exists(ActualPath)) + { + _FileUnavailableReason = new DirectoryNotFoundException("未找到文件夹(" + ActualPath + ")"); + IsLoaded = true; + return; + } + + // 文件夹项不需要进一步处理 + IsLoaded = true; + return; + } + + if (!File.Exists(Path)) + { + _FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + Path + ")"); + IsLoaded = true; + return; + } + + // 对于投影文件,跳过 zip 解析 + if (Path.EndsWithF(".litematic", true) || Path.EndsWithF(".nbt", true) || Path.EndsWithF(".schem", true) || + Path.EndsWithF(".schematic", true)) + { + try + { + _Name = ModBase.GetFileNameWithoutExtentionFromPath(Path); + // 根据文件类型加载数据 + if (Path.EndsWithF(".litematic", true)) + { + LoadLitematicNbtData(); + } + else if (Path.EndsWithF(".schem", true) || Path.EndsWithF(".schematic", true)) + { + if (Path.EndsWithF(".schem", true)) + LoadSchemNbtData(); + else + LoadSchematicNbtData(); + } + else if (Path.EndsWithF(".nbt", true)) + { + LoadStructureNbtData(); + } + + _nbtDataLoaded = true; + } + catch (Exception ex) + { + ModBase.Log(ex, "投影文件信息获取失败(" + Path + ")", ModBase.LogLevel.Developer); + _FileUnavailableReason = ex; + } + + IsLoaded = true; + return; + } + + // 对于其他文件,尝试作为 Jar 文件打开 + ZipArchive Jar = null; + try + { + Jar = new ZipArchive(new FileStream(Path, FileMode.Open)); + // 信息获取 + LookupMetadata(Jar); + } + catch (UnauthorizedAccessException ex) + { + ModBase.Log(ex, "资源文件由于无权限无法打开(" + Path + ")", ModBase.LogLevel.Developer); + _FileUnavailableReason = new UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex); + } + catch (Exception ex) + { + ModBase.Log(ex, "资源文件无法打开(" + Path + ")", ModBase.LogLevel.Developer); + _FileUnavailableReason = ex; + } + finally + { + if (Jar is not null) + Jar.Dispose(); + } + + // 完成标记 + IsLoaded = true; + } + + /// + /// 从 Jar 文件中获取 Mod 信息。 + /// + private void LookupMetadata(ZipArchive Jar) + { + #region 尝试使用 mcmod.info + + do + { + try + { + // 获取信息文件 + var InfoEntry = Jar.GetEntry("mcmod.info"); + string InfoString = null; + if (InfoEntry is not null) + { + InfoString = ModBase.ReadFile(InfoEntry.Open()); + if (InfoString.Length < 15) + InfoString = null; + } + + if (InfoString is null) + break; + // 获取可用 Json 项 + JObject InfoObject; + var JsonObject = (JToken)ModBase.GetJson(InfoString); + if (JsonObject.Type is JTokenType.Array) + InfoObject = (JObject)JsonObject[0]; + else + InfoObject = (JObject)JsonObject["modList"][0]; + // 从文件中获取 Mod 信息项 + Name = (string)InfoObject["name"]; + Description = (string)InfoObject["description"]; + Version = (string)InfoObject["version"]; + Url = (string)InfoObject["url"]; + ModId = (string)InfoObject["modid"]; + var AuthorJson = (JArray)InfoObject["authorList"]; + if (AuthorJson is not null) + { + var Author = new List(); + foreach (var Token in AuthorJson) + Author.Add(Token.ToString()); + if (Author.Any()) + Authors = Author.Join(", "); + } + + var LogoFile = (string)InfoObject["logoFile"]; + if (LogoFile is not null) + { + var LogoItem = Jar.GetEntry(LogoFile); + if (LogoItem is not null) + { + var md5 = ModBase.GetStringMD5(LogoItem.Length + LogoItem.CompressedLength + Path); + Logo = System.IO.Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{md5}.png"); + using (var EntryStream = LogoItem.Open()) + { + ModBase.WriteFile(Logo, EntryStream); + } + } + } + + var Reqs = (JArray)InfoObject["requiredMods"]; + if (Reqs is not null) + foreach (string item in Reqs) // 将迭代变量重命名为 item + if (!string.IsNullOrEmpty(item)) + { + // 使用一个局部变量 token 来处理逻辑 + var token = item; + + token = token.Substring(token.IndexOfF(":") + 1); + if (token.Contains("@")) + { + var parts = token.Split("@"); + AddDependency(parts[0], parts[1]); + } + else + { + AddDependency(token); + } + } + + Reqs = (JArray)InfoObject["dependancies"]; + if (Reqs is not null) + foreach (string rawToken in Reqs) + if (!string.IsNullOrEmpty(rawToken)) + { + var id = rawToken.Substring(rawToken.IndexOfF(":") + 1); + + if (id.Contains("@")) + { + var parts = id.Split("@"); + AddDependency(parts[0], parts[1]); + } + else + { + AddDependency(id); + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 mcmod.info 时出现未知错误(" + Path + ")", ModBase.LogLevel.Developer); + } + } while (false); + + #endregion + + #region 尝试使用 fabric.mod.json + + do + { + try + { + var FabricEntry = Jar.GetEntry("fabric.mod.json"); + string FabricText = null; + if (FabricEntry != null) + { + FabricText = ModBase.ReadFile(FabricEntry.Open(), Encoding.UTF8); + if (!FabricText.Contains("schemaVersion")) FabricText = null; + } + + if (FabricText == null) break; + + var FabricObject = (JObject)ModBase.GetJson(FabricText); + + if (FabricObject.ContainsKey("name")) Name = FabricObject["name"].ToString(); + if (FabricObject.ContainsKey("version")) Version = FabricObject["version"].ToString(); + if (FabricObject.ContainsKey("description")) Description = FabricObject["description"].ToString(); + if (FabricObject.ContainsKey("id")) ModId = FabricObject["id"].ToString(); + if (FabricObject.ContainsKey("contact") && FabricObject["contact"]["homepage"] != null) + Url = FabricObject["contact"]["homepage"].ToString(); + + var AuthorJson = (JArray)FabricObject["authors"]; + if (AuthorJson != null) + { + var AuthorList = AuthorJson.Select(t => t.ToString()).ToList(); + if (AuthorList.Any()) Authors = string.Join(", ", AuthorList); + } + + if (FabricObject.ContainsKey("icon")) + { + var LogoFile = FabricObject["icon"].ToString(); + var LogoItem = Jar.GetEntry(LogoFile); + if (LogoItem != null) + { + var md5 = ModBase.GetStringMD5(LogoItem.Length + LogoItem.CompressedLength + Path); + Logo = System.IO.Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{md5}.png"); + using (var EntryStream = LogoItem.Open()) + { + ModBase.WriteFile(Logo, EntryStream); + } + } + } + + // 依赖处理 (省略了 VB 中的注释部分,按逻辑实现) + if (FabricObject.ContainsKey("depends")) + foreach (var dep in (JObject)FabricObject["depends"]) + AddDependency(dep.Key, dep.Value.ToString()); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 fabric.mod.json 时出错(" + Path + ")", ModBase.LogLevel.Developer); + } + } while (false); + + #endregion + + #region 尝试使用 quilt.mod.json + + do + { + try + { + // 获取 quilt.mod.json 文件 + var QuiltEntry = Jar.GetEntry("quilt.mod.json"); + string QuiltText = null; + if (QuiltEntry is not null) + { + QuiltText = ModBase.ReadFile(QuiltEntry.Open(), Encoding.UTF8); + if (!QuiltText.Contains("schema_version")) + QuiltText = null; + } + + if (QuiltText is null) + break; + var QuiltObject = (JObject)((JObject)ModBase.GetJson(QuiltText))["quilt_loader"]; + // 从文件中获取 Mod 信息项 + if (QuiltObject.ContainsKey("id")) + ModId = (string)QuiltObject["id"]; + if (QuiltObject.ContainsKey("version")) + Version = (string)QuiltObject["version"]; + if (QuiltObject.ContainsKey("metadata")) + { + var QuiltMetadata = (JObject)QuiltObject["metadata"]; + if (QuiltMetadata.ContainsKey("name")) + Name = (string)QuiltMetadata["name"]; + if (QuiltMetadata.ContainsKey("description")) + Description = (string)QuiltMetadata["description"]; + if (QuiltMetadata.ContainsKey("contact")) + Url = (string)(QuiltMetadata["contact"]["homepage"] ?? ""); + } + + if (QuiltObject.ContainsKey("icon")) + { + var LogoFile = (string)QuiltObject["icon"]; + if (LogoFile is not null) + { + var LogoItem = Jar.GetEntry(LogoFile); + if (LogoItem is not null) + { + var md5 = ModBase.GetStringMD5(LogoItem.Length + LogoItem.CompressedLength + Path); + Logo = System.IO.Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{md5}.png"); + using (var EntryStream = LogoItem.Open()) + { + ModBase.WriteFile(Logo, EntryStream); + } + } + } + } + + goto Finished; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 quilt.mod.json 时出现未知错误(" + Path + ")", ModBase.LogLevel.Developer); + } + } while (false); + + #endregion + + #region 尝试使用 mods.toml + + try + { + // 获取 mods.toml 文件 + var TomlEntry = Jar.GetEntry("META-INF/mods.toml"); + string TomlText = null; + if (TomlEntry != null) + { + using (var reader = new StreamReader(TomlEntry.Open())) + { + TomlText = reader.ReadToEnd(); + } + + if (TomlText.Length < 15) TomlText = null; + } + + if (TomlText != null) + { + // 文件标准化:统一换行符为 \n,去除注释、头尾的空格、空行 + var Lines = new List(); + var rawLines = TomlText.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); + + foreach (var rawLine in rawLines) + { + var line = rawLine; + if (line.StartsWithF("#")) continue; // 去除注释 + if (line.Contains("#")) line = line.Substring(0, line.IndexOfF("#")); + // 去除头尾的空格(包含全角空格) + line = line.Trim(' ', '\t', ' '); + if (!string.IsNullOrEmpty(line)) Lines.Add(line); + } + + // 读取文件数据 + // TomlData 存储段落名及其对应的键值对 + var TomlData = new List>> + { + new("", new Dictionary()) + }; + + for (var i = 0; i < Lines.Count; i++) + { + var Line = Lines[i]; + if (Line.StartsWithF("[") && Line.EndsWithF("]")) + { + // 段落标记 + var Header = Line.Trim('[', ']'); + TomlData.Add( + new KeyValuePair>(Header, + new Dictionary())); + } + else if (Line.Contains("=")) + { + // 字段标记 + var Key = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(' ', '\t', ' '); + var RawValue = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(' ', '\t', ' '); + object Value; + + if (RawValue.StartsWithF("\"") && RawValue.EndsWithF("\"")) + { + // 单行字符串 + Value = RawValue.Trim('\"'); + } + else if (RawValue.StartsWithF("'''")) + { + // 多行字符串 + var ValueLines = new List { RawValue.Replace("'''", "") }; + if (!RawValue.EndsWithF("'''") || RawValue.Length == 3) + while (i < Lines.Count - 1) + { + i++; + var ValueLine = Lines[i]; + if (ValueLine.EndsWithF("'''")) + { + ValueLines.Add(ValueLine.Replace("'''", "")); + break; + } + + ValueLines.Add(ValueLine); + } + + Value = string.Join("\n", ValueLines).Trim('\n').Replace("\n", "\r\n"); + } + else if (RawValue.ToLower() == "true" || RawValue.ToLower() == "false") + { + // 布尔型 + Value = RawValue.ToLower() == "true"; + } + else if (double.TryParse(RawValue, out var num)) + { + // 数字型 (模拟 VB 的 Val) + Value = num; + } + else + { + // 默认当做字符串存储 + Value = RawValue; + } + + // 将值存入当前最后的段落中 + var lastPair = TomlData[TomlData.Count - 1]; + lastPair.Value[Key] = Value; + } + } + + // 从解析出的数据中提取 Mod 信息 + Dictionary ModEntry = null; + foreach (var subData in TomlData) + if (subData.Key == "mods") + { + ModEntry = subData.Value; + break; + } + + if (ModEntry != null && ModEntry.ContainsKey("modId")) + { + ModId = ModEntry["modId"].ToString(); + // 假设 _ModId 是内部属性,如果为 null 说明设置失败 + if (_ModId != null) + { + if (ModEntry.ContainsKey("displayName")) Name = ModEntry["displayName"].ToString(); + if (ModEntry.ContainsKey("description")) Description = ModEntry["description"].ToString(); + if (ModEntry.ContainsKey("version")) Version = ModEntry["version"].ToString(); + + // [0] 是全局段落(无 Header) + if (TomlData[0].Value.ContainsKey("displayURL")) + Url = TomlData[0].Value["displayURL"].ToString(); + if (TomlData[0].Value.ContainsKey("authors")) + Authors = TomlData[0].Value["authors"].ToString(); + + // 读取依赖 + foreach (var subData in TomlData) + if (subData.Key.ToLower() == $"dependencies.{ModId.ToLower()}") + { + var DepEntry = subData.Value; + if (DepEntry.ContainsKey("modId") && + DepEntry.ContainsKey("mandatory") && (bool)DepEntry["mandatory"] && + DepEntry.ContainsKey("side") && + DepEntry["side"].ToString().ToLower() != "server") + AddDependency( + DepEntry["modId"].ToString(), + DepEntry.ContainsKey("versionRange") + ? DepEntry["versionRange"].ToString() + : null + ); + } + + // 加载成功,跳转到完成标签 + goto Finished; + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 mods.toml 时出现未知错误(" + Path + ")", ModBase.LogLevel.Developer); + } + + #endregion + + #region 尝试使用 fml_cache_annotation.json + + do + { + try + { + // 获取 fml_cache_annotation.json 文件 + var FmlEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json"); + string FmlText = null; + if (FmlEntry is not null) + { + FmlText = ModBase.ReadFile(FmlEntry.Open(), Encoding.UTF8); + if (!FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;")) + FmlText = null; + } + + if (FmlText is null) + break; + var FmlJson = (JObject)ModBase.GetJson(FmlText); + // 获取可用 Json 项 + JObject FmlObject = null; + foreach (var ModFilePair in FmlJson) + { + var ModFileAnnos = (JArray)ModFilePair.Value["annotations"]; + if (ModFileAnnos is not null) + // 先获取 Mod + foreach (var ModFileAnno in ModFileAnnos) + { + var Name = (string)(ModFileAnno["name"] ?? ""); + if (Name == "Lnet/minecraftforge/fml/common/Mod;") + { + FmlObject = (JObject)ModFileAnno["values"]; + goto Got; + } + } + } + + break; + Got: ; + + // 从文件中获取 Mod 信息项 + if (FmlObject.ContainsKey("useMetadata") && + (FmlObject["useMetadata"]["value"] ?? "").ToString().ToLower() == "true") + { + // 要求使用 mcmod.info 中的信息 + var value = (string)FmlObject["modid"]["value"]; + if (value is null) + break; + value = value.ToLower().RegexSeek(RegexPatterns.ModIdMatch); + if (value is not null && value.ToLower() != "name" && value.Count() > 1 && + (ModBase.Val(value).ToString() ?? "") != (value ?? "")) + if (!PossibleModId.Contains(value)) + PossibleModId.Add(value); + break; + } + + if (FmlObject.ContainsKey("name")) + Name = (string)FmlObject["name"]["value"]; + if (FmlObject.ContainsKey("version")) + Version = (string)FmlObject["version"]["value"]; + if (FmlObject.ContainsKey("modid")) + ModId = (string)FmlObject["modid"]["value"]; + if (!FmlObject.ContainsKey("serverSideOnly") || + !FmlObject["serverSideOnly"]["value"].ToObject()) + { + // 添加 Minecraft 依赖 + var DepMinecraft = (string)((FmlObject["acceptedMinecraftVersions"] is not null + ? FmlObject["acceptedMinecraftVersions"]["value"] + : "") ?? ""); + if (!string.IsNullOrEmpty(DepMinecraft)) + AddDependency("minecraft", DepMinecraft); + // 添加其他依赖 + var Deps = (string)((FmlObject["dependencies"] is not null + ? FmlObject["dependencies"]["value"] + : "") ?? ""); + if (!string.IsNullOrEmpty(Deps)) + foreach (var item in Deps.Split(";")) + { + if (string.IsNullOrEmpty(item) || !item.StartsWithF("required-")) + continue; + + // 使用局部变量处理逻辑,不要直接修改迭代变量 item + var dep = item.Substring(item.IndexOfF(":") + 1); + + if (dep.Contains("@")) + { + var parts = dep.Split("@"); + AddDependency(parts[0], parts[1]); + } + else + { + AddDependency(dep); + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" + Path + ")"); + } + } while (false); + + #endregion + + #region 尝试识别资源包图标 + + try + { + // 检查并提取资源包的 pack.png 图标 + var packPngEntry = Jar.GetEntry("pack.png"); + if (packPngEntry is not null) + try + { + var md5 = ModBase.GetStringMD5(packPngEntry.Length + packPngEntry.CompressedLength + Path); + Logo = System.IO.Path.Combine(ModBase.PathTemp, "Cache", "Images", $"{md5}.png"); + using (var entryStream = packPngEntry.Open()) + { + ModBase.WriteFile(Logo, entryStream); + } + + ModBase.Log("成功提取资源包图标:" + Path, ModBase.LogLevel.Debug); + } + catch (Exception logoEx) + { + ModBase.Log(logoEx, "提取 pack.png 图标失败(" + Path + ")", ModBase.LogLevel.Developer); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "识别资源包图标时出现未知错误(" + Path + ")", ModBase.LogLevel.Developer); + } + + #endregion + + Finished: ; + + #region 将 Version 代号转换为 META-INF 中的版本 + + if (_Version == "version") + try + { + var MetaEntry = Jar.GetEntry("META-INF/MANIFEST.MF"); + if (MetaEntry is not null) + { + var MetaString = ModBase.ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":"); + if (MetaString.Contains("Implementation-Version:")) + { + MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + + "Implementation-Version:".Count()); + MetaString = MetaString.Substring(0, MetaString.IndexOfAny("\r\n".ToCharArray())) + .Trim(); + Version = MetaString; + } + } + } + catch (Exception ex) + { + ModBase.Log("获取 META-INF 中的版本信息失败(" + Path + ")", ModBase.LogLevel.Developer); + Version = null; + } + + if (_Version is not null && !(_Version.Contains(".") || _Version.Contains("-"))) + Version = null; + + #endregion + } + + #endregion + + #region 网络信息 + + /// + /// 当任何网络信息更新时触发。 + /// + public event OnCompUpdateEventHandler? OnCompUpdate; + + public delegate void OnCompUpdateEventHandler(LocalCompFile sender); + + /// + /// 该 Mod 关联的网络项目。 + /// + public CompProject Comp + { + get => _Comp; + set + { + _Comp = value; + OnCompUpdate?.Invoke(this); + } + } + + private CompProject _Comp; + + /// + /// 本地文件对应的联网文件信息。 + /// + public CompFile CompFile; + + /// + /// 该 Mod 对应的联网最新版本。 + /// + public CompFile UpdateFile + { + get => _UpdateFile; + set + { + _UpdateFile = value; + OnCompUpdate?.Invoke(this); + } + } + + private CompFile _UpdateFile; + + /// + /// 该 Mod 的更新日志网址。 + /// + public List ChangelogUrls = new(); + + /// + /// 所有网络信息是否已成功加载。 + /// + public bool CompLoaded; + + /// + /// 将网络信息保存为 Json。 + /// + public JObject ToJson() + { + var Json = new JObject(); + if (Comp is not null) + Json.Add("Comp", Comp.ToJson()); + Json.Add("ChangelogUrls", new JArray(ChangelogUrls)); + Json.Add("CompLoaded", CompLoaded); + if (CompFile is not null) + Json.Add("CompFile", CompFile.ToJson()); + if (UpdateFile is not null) + Json.Add("UpdateFile", UpdateFile.ToJson()); + return Json; + } + + /// + /// 从 Json 中读取网络信息。 + /// + public void FromJson(JObject Json) + { + CompLoaded = (bool)Json["CompLoaded"]; + if (Json.ContainsKey("Comp")) + Comp = new CompProject((JObject)Json["Comp"]); + if (Json.ContainsKey("ChangelogUrls")) + ChangelogUrls = Json["ChangelogUrls"].ToObject>(); + if (Json.ContainsKey("CompFile")) + CompFile = new CompFile((JObject)Json["CompFile"], CompType.Mod); + if (Json.ContainsKey("UpdateFile")) + UpdateFile = new CompFile((JObject)Json["UpdateFile"], CompType.Mod); + } + + /// + /// 该文件是否可以更新。 + /// + public bool CanUpdate => !Config.Preference.Hide.FunctionModUpdate && ChangelogUrls.Any(); + + /// + /// 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。 + /// + public uint CurseForgeHash + { + get + { + if (_CurseForgeHash is null) + { + // 读取缓存 + var Info = new FileInfo(Path); + var CacheKey = ModBase.GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString()}-{Info.Length}-C") + .ToString(); + var Cached = ModBase.ReadIni(ModBase.PathTemp + @"Cache\CompHash.ini", CacheKey); + if (!string.IsNullOrEmpty(Cached) && Cached.RegexCheck(@"^\d+$")) // #5062 + { + _CurseForgeHash = Conversions.ToUInteger(Cached); + return (uint)_CurseForgeHash; + } + + // 读取文件 + var data = new List(); + foreach (var b in ModBase.ReadFileBytes(Path)) + { + if (b == 9 || b == 10 || b == 13 || b == 32) + continue; + data.Add(b); + } + + // 计算 MurmurHash2 + var length = data.Count; + var h = (uint)(1 ^ length); // 1 是种子 + int i; + var loopTo = length - 4; + for (i = 0; i <= loopTo; i += 4) + { + var k = data[i] | ((uint)data[i + 1] << 8) | ((uint)data[i + 2] << 16) | + ((uint)data[i + 3] << 24); + k = (uint)((k * 0x5BD1E995L) & 0xFFFFFFFFL); + k = k ^ (k >> 24); + k = (uint)((k * 0x5BD1E995L) & 0xFFFFFFFFL); + h = (uint)((h * 0x5BD1E995L) & 0xFFFFFFFFL); + h = h ^ k; + } + + switch (length - i) + { + case 3: + { + h = h ^ (data[i] | ((uint)data[i + 1] << 8)); + h = h ^ ((uint)data[i + 2] << 16); + h = (uint)((h * 0x5BD1E995L) & 0xFFFFFFFFL); + break; + } + case 2: + { + h = h ^ (data[i] | ((uint)data[i + 1] << 8)); + h = (uint)((h * 0x5BD1E995L) & 0xFFFFFFFFL); + break; + } + case 1: + { + h = h ^ data[i]; + h = (uint)((h * 0x5BD1E995L) & 0xFFFFFFFFL); + break; + } + } + + h = h ^ (h >> 13); + h = (uint)((h * 0x5BD1E995L) & 0xFFFFFFFFL); + h = h ^ (h >> 15); + _CurseForgeHash = h; + // 写入缓存 + ModBase.WriteIni(ModBase.PathTemp + @"Cache\CompHash.ini", CacheKey, h.ToString()); + } + + return (uint)_CurseForgeHash; + } + } + + private uint? _CurseForgeHash; + + /// + /// 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。 + /// + public string ModrinthHash + { + get + { + if (_ModrinthHash is null) + { + // 读取缓存 + var Info = new FileInfo(Path); + var CacheKey = ModBase.GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString()}-{Info.Length}-M") + .ToString(); + var Cached = ModBase.ReadIni(ModBase.PathTemp + @"Cache\CompHash.ini", CacheKey); + if (!string.IsNullOrEmpty(Cached)) + { + _ModrinthHash = Cached; + return _ModrinthHash; + } + + // 计算 SHA1 + _ModrinthHash = ModBase.GetFileSHA1(Path); + // 写入缓存 + ModBase.WriteIni(ModBase.PathTemp + @"Cache\CompHash.ini", CacheKey, _ModrinthHash); + } + + return _ModrinthHash; + } + } + + private string _ModrinthHash; + + #endregion + + #region API + + public override string ToString() + { + return $"{State} - {Path}"; + } + + public override bool Equals(object obj) + { + var target = obj as LocalCompFile; + return target is not null && (Path ?? "") == (target.Path ?? ""); + } + + #endregion + } + + /// + /// 获取文件夹描述信息。 + /// + private static string GetFolderDescription(string FolderPath) + { + try + { + if (!Directory.Exists(FolderPath)) + return "空文件夹"; + return "文件夹"; + } + catch (Exception ex) + { + ModBase.Log(ex, $"获取文件夹描述失败:{FolderPath}"); + return "文件夹"; + } + } + + public class CompLocalLoaderData + { + public string CompPath; + public CompType CompType; + + public KeyValuePair, JObject> DetailInfo; + public PageInstanceCompResource Frm; + public ModMinecraft.McInstance GameVersion; + public List Loaders; + } + + // 加载资源列表 + public static LoaderTask> CompResourceListLoader = + new("Comp Resource List Loader", CompResourceListLoad); + + private static void CompResourceListLoad(LoaderTask> Loader) + { + try + { + ModBase.RunInUiWait(() => + { + if (Loader.Input.Frm is not null) Loader.Input.Frm.Load.ShowProgress = false; + }); + + // 等待 Mod 更新完成 + if (PageInstanceCompResource.UpdatingVersions.Contains(Loader.Input.CompPath)) + { + ModBase.Log("[Mod] 等待资源更新完成后才能继续加载资源列表:" + Loader.Input.CompPath); + try + { + ModBase.RunInUiWait(() => + { + if (Loader.Input.Frm is not null) Loader.Input.Frm.Load.Text = "正在更新资源"; + }); + while (PageInstanceCompResource.UpdatingVersions.Contains(Loader.Input.CompPath)) + { + if (Loader.IsAborted) + return; + Thread.Sleep(100); + } + } + finally + { + ModBase.RunInUiWait(() => + { + if (Loader.Input.Frm is not null) Loader.Input.Frm.Load.Text = "正在加载资源列表"; + }); + } + + Loader.Input.Frm.LoaderRun(LoaderFolderRunType.UpdateOnly); + } + + // 获取 Mod 文件夹下的可用文件列表 + var ModList = new List(); + if (Directory.Exists(Loader.Input.CompPath)) + { + var RawName = Loader.Input.CompPath.ToLower(); + + if (Loader.Input.CompType == CompType.Schematic) + { + var CurrentFolderPath = ""; + if (Loader.Input.Frm is not null) CurrentFolderPath = Loader.Input.Frm.CurrentFolderPath; + + var SearchPath = string.IsNullOrEmpty(CurrentFolderPath) + ? Loader.Input.CompPath + : CurrentFolderPath; + + try + { + var DirInfo = new DirectoryInfo(SearchPath); + foreach (var Dir in DirInfo.EnumerateDirectories("*", SearchOption.AllDirectories)) + ModList.Add(new LocalCompFile(Dir.FullName + @"\__FOLDER__")); + foreach (var File in DirInfo.EnumerateFiles("*", SearchOption.AllDirectories)) + try + { + if (Conversions.ToBoolean( + LocalCompFile.IsCompFile(File.FullName, Loader.Input.CompType))) + ModList.Add(new LocalCompFile(File.FullName)); + } + catch (Exception ex) + { + ModBase.Log(ex, $"处理文件失败:{File.FullName}"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"枚举文件失败:{SearchPath}"); + } + } + else + { + try + { + foreach (var File in ModBase.EnumerateFiles(Loader.Input.CompPath)) + try + { + if ((File.DirectoryName.ToLower() + @"\" ?? "") != (RawName ?? "")) + if (!(PageInstanceLeft.Instance is not null && + PageInstanceLeft.Instance.Info.HasForge && + PageInstanceLeft.Instance.Info.Drop < 130 && (File.Directory.Name ?? "") == + (PageInstanceLeft.Instance.Info.VanillaName ?? ""))) + continue; + + if (Conversions.ToBoolean( + LocalCompFile.IsCompFile(File.FullName, Loader.Input.CompType))) + ModList.Add(new LocalCompFile(File.FullName)); + } + catch (Exception ex) + { + ModBase.Log(ex, $"处理文件失败:{File.FullName}"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"枚举文件夹失败:{Loader.Input.CompPath}"); + } + } + } + + // 确定是否显示进度 + Loader.Progress = 0.05d; + if (ModList.Count > 50) + ModBase.RunInUi(() => + { + if (Loader.Input.Frm is not null) Loader.Input.Frm.Load.ShowProgress = true; + }); + + // 获取本地文件缓存 + var CachePath = ModBase.PathTemp + @"Cache\LocalComp.json"; + var Cache = new JObject(); + try + { + var CacheContent = ModBase.ReadFile(CachePath); + if (!string.IsNullOrWhiteSpace(CacheContent)) + { + Cache = (JObject)ModBase.GetJson(CacheContent); + if (!Cache.ContainsKey("version") || Cache["version"].ToObject() != LocalModCacheVersion) + { + ModBase.Log("[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", ModBase.LogLevel.Debug); + Cache = new JObject(); + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "读取本地 Mod 信息缓存失败,已重置"); + Cache = new JObject(); + } + + Cache["version"] = LocalModCacheVersion; + + // 加载 Mod 列表 - 优化:对于原理图文件,延迟加载NBT数据 + var ModUpdateList = new List(); + foreach (var ModEntry in ModList) + { + Loader.Progress += 0.94d / ModList.Count; + if (Loader.IsAborted) + return; + if (ModEntry.IsFolder) + continue; + + // 优化:对于原理图文件,只进行基础加载,不解析NBT数据 + if (Loader.Input.CompType == CompType.Schematic) + ModEntry.LoadBasicInfo(); + else + // 加载 McMod 对象 + ModEntry.Load(); + + // Dim DumpMod As LocalCompFile = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName AndAlso Not m.IsFolder) + // If DumpMod IsNot Nothing AndAlso DumpMod IsNot ModEntry Then + // Dim DisabledMod As LocalCompFile = If(DumpMod.State = LocalCompFile.LocalFileStatus.Disabled, DumpMod, ModEntry) + // Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug) + // If DisabledMod Is ModEntry Then + // Continue For + // Else + // ModList.Remove(DisabledMod) + // ModUpdateList.Remove(DisabledMod) + // End If + // End If + // 读取 Comp 缓存 + if (ModEntry.State == LocalCompFile.LocalFileStatus.Unavailable) + continue; + var CacheKey = ModEntry.ModrinthHash + Loader.Input.GameVersion.Info.VanillaName + + Loader.Input.Loaders.Join(""); + if (Cache.ContainsKey(CacheKey)) + { + ModEntry.FromJson((JObject)Cache[CacheKey]); + // 如果缓存中的信息在 6 小时以内更新过,则无需重新获取 + if (ModEntry.CompLoaded && + DateTime.Now - Cache[CacheKey]["Comp"]["CacheTime"].ToObject() < + new TimeSpan(6, 0, 0)) + continue; + } + + ModUpdateList.Add(ModEntry); + } + + Loader.Progress = 0.99d; + ModBase.Log( + $"[Mod] 共有 {ModList.Count} 个 Mod,其中 {ModUpdateList.Where(m => m.Comp is null).Count()} 个需要联网获取信息,{ModUpdateList.Where(m => m.Comp is not null).Count()} 个需要更新信息"); + + // 排序 + ModList = ModList.Sort((Left, Right) => + { + if (Left.State == LocalCompFile.LocalFileStatus.Unavailable != + (Right.State == LocalCompFile.LocalFileStatus.Unavailable)) + return Left.State == LocalCompFile.LocalFileStatus.Unavailable; + + return Conversions.ToBoolean(~Right.FileName.CompareTo(Left.FileName)); + }); + + // 回设 + if (Loader.IsAborted) + return; + Loader.Output = ModList; + + // 开始联网加载 + if (ModUpdateList.Any()) + { + // TODO: 添加信息获取中提示 + Loader.Input.DetailInfo = new KeyValuePair, JObject>(ModUpdateList, Cache); + CompUpdateDetailLoader.Start(Loader.Input, true); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "Mod 列表加载失败"); + throw; + } + } + + // 联网加载 Mod 详情 + public static LoaderTask CompUpdateDetailLoader = + new("Comp List Detail Loader", CompUpdateDetailLoad); + + private static void CompUpdateDetailLoad(LoaderTask Loader) + { + var Mods = Loader.Input.DetailInfo.Key; + var Cache = Loader.Input.DetailInfo.Value; + // 获取作为检查目标的加载器和版本 + var ModLoaders = Loader.Input.Loaders; + var CompType = Loader.Input.CompType; + var McInstance = Loader.Input.GameVersion.Info.VanillaName; + + // 开始网络获取 + ModBase.Log($"[Mod] 目标加载器:{string.Join("/", ModLoaders)},版本:{McInstance}"); + var EndedThreadCount = 0; + var IsFailed = false; + var CurrentTaskId = Task.CurrentId ?? -1; + + // 从 Modrinth 获取信息 + ModBase.RunInNewThread(() => + { + try + { + // 步骤 1:获取 Hash 与对应的工程 ID + var ModrinthHashes = Mods.Select(m => m.ModrinthHash).ToList(); + var ModrinthVersion = (JObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.modrinth.com/v2/version_files", "POST", + $"{{\"hashes\": [\"{string.Join("\",\"", ModrinthHashes)}\"], \"algorithm\": \"sha1\"}}", + "application/json")); + ModBase.Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息"); + + // 步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 + if (ModrinthVersion.Count == 0) return; + var ModrinthMapping = new Dictionary>(); + foreach (var Entry in Mods) + { + if (ModrinthVersion[Entry.ModrinthHash] == null) continue; + if (ModrinthVersion[Entry.ModrinthHash]["files"][0]["hashes"]["sha1"].ToString() != + Entry.ModrinthHash) continue; + + var ProjectId = ModrinthVersion[Entry.ModrinthHash]["project_id"].ToString(); + // 读取已加载的缓存,加快结果出现速度 + if (CompProjectCache.ContainsKey(ProjectId) && Entry.Comp == null) + Entry.Comp = CompProjectCache[ProjectId]; + + if (!ModrinthMapping.ContainsKey(ProjectId)) ModrinthMapping[ProjectId] = new List(); + ModrinthMapping[ProjectId].Add(Entry); + + // 记录对应的 CompFile + var FileInfo = new CompFile((JObject)ModrinthVersion[Entry.ModrinthHash], CompType.Mod); + if (Entry.CompFile == null || Entry.CompFile.ReleaseDate < FileInfo.ReleaseDate) + Entry.CompFile = FileInfo; + } + + if (Loader.IsAbortedWithThread(CurrentTaskId)) return; + ModBase.Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息"); + + // 步骤 3:获取工程信息 + if (!ModrinthMapping.Any()) return; + var ModrinthProject = (JArray)ModBase.GetJson(ModDownload.DlModRequest( + $"https://api.modrinth.com/v2/projects?ids=[\"{string.Join("\",\"", ModrinthMapping.Keys)}\"]", + "GET", "", "application/json")); + + foreach (var ProjectJson in ModrinthProject) + { + var Project = new CompProject((JObject)ProjectJson); + foreach (var Entry in ModrinthMapping[Project.Id]) Entry.Comp = Project; + } + + ModBase.Log("[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息"); + + // 步骤 4:获取更新信息 + var targetLoaders = CompType == CompType.DataPack + ? "datapack" + : string.Join("\",\"", ModLoaders).ToLower(); + var ModrinthUpdate = (JObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.modrinth.com/v2/version_files/update", "POST", + $"{{\"hashes\": [\"{string.Join("\",\"", ModrinthMapping.SelectMany(l => l.Value.Select(m => m.ModrinthHash)))}\"], \"algorithm\": \"sha1\", " + + $"\"loaders\": [\"{targetLoaders}\"],\"game_versions\": [\"{McInstance}\"]}}", "application/json")); + + foreach (var Entry in Mods) + { + if (ModrinthUpdate[Entry.ModrinthHash] == null || Entry.CompFile == null) continue; + var UpdateFile = new CompFile((JObject)ModrinthUpdate[Entry.ModrinthHash], CompType.Mod); + if (!UpdateFile.Available) continue; + + if (ModBase.ModeDebug) + ModBase.Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}"); + if (Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate || + Entry.CompFile.Hash == UpdateFile.Hash) continue; + + // 设置更新日志与更新文件 + if (Entry.UpdateFile != null && UpdateFile.Hash == Entry.UpdateFile.Hash) + { + Entry.ChangelogUrls.Add( + $"https://modrinth.com/mod/{ModrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={McInstance}"); + Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls); + Entry.UpdateFile = UpdateFile; + } + else if (Entry.UpdateFile == null || UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate) + { + Entry.ChangelogUrls = new List + { + $"https://modrinth.com/mod/{ModrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={McInstance}" + }; + Entry.UpdateFile = UpdateFile; + } + } + + ModBase.Log("[Mod] 从 Modrinth 获取本地 Mod 信息结束"); + } + catch (Exception ex) + { + ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败"); + IsFailed = true; + } + finally + { + Interlocked.Increment(ref EndedThreadCount); + } + }, "Mod List Detail Loader Modrinth"); + + // CurseForge 部分转换逻辑类似,注意其 ID 多为 Integer 类型 + ModBase.RunInNewThread(() => + { + try + { + // 步骤 1:获取 Hash 与对应的工程 ID + var CurseForgeHashes = Mods.Select(m => m.CurseForgeHash).ToList(); + var CurseForgeResponse = (JObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.curseforge.com/v1/fingerprints/432", "POST", + $"{{\"fingerprints\": [{string.Join(",", CurseForgeHashes)}]}}", "application/json")); + var CurseForgeRaw = (JArray)CurseForgeResponse["data"]["exactMatches"]; + ModBase.Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息"); + + // 步骤 2:构建映射 (此处省略具体循环,逻辑同 Modrinth,注意 ProjectId 转换) + // ... + + // 步骤 4:获取更新文件信息 + // 注意 C# 中 Dictionary 的键值对遍历:foreach (var pair in UpdateFiles) { var Entry = pair.Key; ... } + } + catch (Exception ex) + { + ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败"); + IsFailed = true; + } + finally + { + Interlocked.Increment(ref EndedThreadCount); + } + }, "Mod List Detail Loader CurseForge"); + + // 等待线程结束 + while (EndedThreadCount < 2) + { + if (Loader.IsAborted) return; + Thread.Sleep(10); + } + + // 保存缓存 + var CachedMods = Mods.Where(m => m.Comp != null).ToList(); + ModBase.Log($"[Mod] 联网获取本地 Mod 信息完成,为 {CachedMods.Count} 个 Mod 更新缓存"); + if (!CachedMods.Any()) return; + + foreach (var Entry in CachedMods) + { + Entry.CompLoaded = !IsFailed; + Cache[Entry.ModrinthHash + McInstance + string.Join("", ModLoaders)] = Entry.ToJson(); + } + + ModBase.WriteFile(ModBase.PathTemp + "Cache\\LocalComp.json", + Cache.ToString(ModBase.ModeDebug ? Formatting.Indented : Formatting.None)); + + // 刷新 UI + ModBase.RunInUi(() => + { + if (ModMain.FrmInstanceMod?.Filter == PageInstanceCompResource.FilterType.CanUpdate) + ModMain.FrmInstanceMod?.RefreshUI(); + else + ModMain.FrmInstanceMod?.RefreshBars(); + }); + } + + public static List GetCurrentVersionModLoader() + { + var ModLoaders = new List(); + if (PageInstanceLeft.Instance.Info.HasForge) + ModLoaders.Add(CompLoaderType.Forge); + if (PageInstanceLeft.Instance.Info.HasNeoForge) + ModLoaders.Add(CompLoaderType.NeoForge); + if (PageInstanceLeft.Instance.Info.HasFabric) + ModLoaders.Add(CompLoaderType.Fabric); + if (PageInstanceLeft.Instance.Info.HasQuilt) + ModLoaders.AddRange(new[] { CompLoaderType.Fabric, CompLoaderType.Quilt }); + if (PageInstanceLeft.Instance.Info.HasLiteLoader) + ModLoaders.Add(CompLoaderType.LiteLoader); + if (!ModLoaders.Any()) + ModLoaders.AddRange(new[] + { + CompLoaderType.Forge, CompLoaderType.NeoForge, CompLoaderType.Fabric, CompLoaderType.LiteLoader, + CompLoaderType.Quilt + }); + return ModLoaders; + } + + public static string GetPathNameByCompType(CompType TheType) + { + switch (TheType) + { + case CompType.Mod: + { + return "mods"; + } + case CompType.ResourcePack: + { + return "resourcepacks"; + } + case CompType.Shader: + { + return "shaderpacks"; + } + case CompType.Schematic: + { + return "schematics"; + } + case CompType.World: + { + return "saves"; + } + } + + return "Nothing"; + } + + private static readonly Regex RegexIsJarFile = new(@"\.jar(\.disabled)?$"); + + /// + /// 通过文件名关键字和 Mod ID 比如 fabric apifabric-api 来获取给定实例 mods 目录中某个 Mod 的 + /// 对象 + ///
+ /// 为了不浪费性能,关键字统一用小写 + ///
+ /// + /// 如果文件名包含主关键字,以及其他关键字中的任意一个,同时 Mod ID 一致,即认为匹配,返回对应的对象,若没有匹配的文件则返回空值。 + /// + public static LocalCompFile GetModLocalCompByKeywords(ModMinecraft.McInstance instance, string modId, + string mainKeyword, params string[] keywords) + { + if (modId is null) + return null; + return GetModLocalCompByKeywords(instance, new[] { modId }, mainKeyword, keywords); + } + + public static LocalCompFile GetModLocalCompByKeywords(ModMinecraft.McInstance instance, string[] modIds, + string mainKeyword, params string[] keywords) + { + if (!instance.Modable) + return null; // 跳过不可安装 Mod 实例 + var modFolder = $"{instance.PathInstance}mods"; + if (!Directory.Exists(modFolder)) + return null; // 确保 mods 目录存在 + foreach (var file in Directory.EnumerateFiles(modFolder, $"*{mainKeyword}*")) + { + var lowerFilePath = file.ToLower(); // 统一转为小写 + if (!RegexIsJarFile.IsMatch(lowerFilePath)) + continue; // 检查是否是 jar 文件 + if ((keywords.Length > 0) & !keywords.Any(keyword => lowerFilePath.Contains(keyword))) + continue; // 检查是否包含关键字 + var localComp = new LocalCompFile(file); + localComp.Load(); + if (modIds.Any(modId => (localComp.ModId ?? "") == (modId ?? ""))) + return localComp; + } + + return null; + } + +#if DEBUGRESERVED + /// + /// 检查 Mod 列表中存在的错误,返回错误信息的集合。 + /// + public static List McModCheck(McInstance Version, List Mods) + { + var Result = new List(); + // 令所有 Mod 进行基础检查,并归纳需要检查的 Mod + var CurrentModList = new List(); + foreach (var ModEntity in Mods) + { + if (!ModEntity.IsFileAvailable) + { + Result.Add("无法读取的 Mod 文件。" + "\r\n" + " - " + ModEntity.Path); + continue; + } + if (ModEntity.State == McMod.McModState.Fine && ModEntity.ModId != null) + { + CurrentModList.Add(ModEntity); + } + } + + // 添加默认依赖 {DependencyVersion, Path, Count} + // 使用 object[] 对应 VB 的 String(),或者定义一个简单的 struct/class + var CurrentDependencies = new Dictionary(); + + if (Version.State == McInstanceState.Forge) + { + CurrentDependencies.Add("forge", new object[] { Version.Version.ForgeVersion, "Forge", 1 }); + } + CurrentDependencies.Add("minecraft", new object[] { Version.Version.McName, "Minecraft", 1 }); + + // 检查重复的 Mod,并添加对应的依赖 + foreach (var ModEntity in CurrentModList) + { + foreach (var PossibleModId in ModEntity.PossibleModId) + { + if (CurrentDependencies.ContainsKey(PossibleModId)) + { + if ((int)CurrentDependencies[PossibleModId][2] == 1) + { + Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" + PossibleModId + ")。" + "\r\n" + + " - " + ModEntity.FileName + "\r\n" + + " - " + CurrentDependencies[PossibleModId][1]); + } + else + { + ModBase.Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" + PossibleModId + ")。" + "\r\n" + + " - " + ModEntity.FileName + "\r\n" + + " - " + CurrentDependencies[PossibleModId][1], ModBase.LogLevel.Developer); + } + } + else + { + CurrentDependencies.Add(PossibleModId, new object[] { ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count }); + } + } + } + + // 检查依赖 + foreach (var ModEntity in CurrentModList) + { + try + { + foreach (var Dependency in ModEntity.Dependencies) + { + string ReqId = Dependency.Key; + if (ReqId.Length < 2) continue; // 确保正常 + if (ReqId == ModEntity.ModId) continue; // 跳过自体引用 + if (ReqId == "forgemultipartcbe") continue; // 跳过莫名其妙的引用 + + if (Dependency.Value != null) + { + // 获取分段后的详细版本信息 + string ReqVersion = Dependency.Value; + bool ReqVersionHeadCanEqual = ReqVersion.StartsWithF("["); + bool ReqVersionTailCanEqual = ReqVersion.EndsWithF("]"); + string ReqVersionHead; + string ReqVersionTail; + + if (ReqVersion.Contains(",")) + { + var parts = ReqVersion.Split(','); + ReqVersionHead = parts[0].Trim("([ ".ToCharArray()); + ReqVersionTail = parts[1].Trim("]) ".ToCharArray()); + } + else + { + ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray()); + ReqVersionTail = ReqVersionHead; + if (ReqId == "minecraft" && ReqVersionHead.Split('.').Length == 2) + { + var mcParts = ReqVersionHead.Split('.'); + ReqVersionTail = mcParts[0] + "." + (Conversion.Val(mcParts[1]) + 1); + ReqVersionTailCanEqual = false; + } + } + + if (ReqVersionHead.StartsWithF("1.") && ReqVersionHead.Contains("-")) + ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOfF("-") + 1); + if (ReqVersionTail.StartsWithF("1.") && ReqVersionTail.Contains("-")) + ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOfF("-") + 1); + + // 获取报错描述文本 + string VersionRequire = ""; + if (ReqVersionHead == ReqVersionTail) + { + VersionRequire = "应为 " + ReqVersionHead; + } + else if (ReqVersionHead.Contains(".") && ReqVersionTail.Contains(".")) + { + VersionRequire = "应为 " + ReqVersionHead + " 至 " + ReqVersionTail; + } + else if (ReqVersionHead.Contains(".")) + { + VersionRequire = + ReqVersionHeadCanEqual ? "最低应为 " + ReqVersionHead : "应高于 " + ReqVersionHead; + } + else if (ReqVersionTail.Contains(".")) + { + VersionRequire = + ReqVersionTailCanEqual ? "最高应为 " + ReqVersionHead : "应低于 " + ReqVersionHead; + } + + // 检查前置 Mod 是否存在 + if (!CurrentDependencies.ContainsKey(ReqId)) + { + Result.Add("缺少前置 Mod:" + ReqId + (VersionRequire == "" ? "" : ",其版本" + VersionRequire) + "。" + "\r\n" + " - " + ModEntity.FileName); + continue; + } + + string CurrentVersion = (string)CurrentDependencies[ReqId][0] ?? "0.0"; + if (CurrentVersion.StartsWithF("1.") && CurrentVersion.Contains("-")) + CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOfF("-") + 1); + + // 对比前置 Mod 头部版本 + if (ReqVersionHead.Contains(".")) + { + if (ModBase.VersionSortInteger(ReqVersionHead, CurrentVersion) > (ReqVersionHeadCanEqual ? 0 : -1)) + { + Result.Add(ReqId.Substring(0, 1).ToUpper() + ReqId.Substring(1) + " 版本过低,其版本" + VersionRequire + ",而当前版本为 " + CurrentVersion + "。" + "\r\n" + + " - " + ModEntity.FileName + (ReqId != "minecraft" && ReqId != "forge" ? "\r\n" + " - 前置:" + ((object[])CurrentDependencies[ReqId])[1] : "")); + continue; + } + } + + // 对比前置 Mod 尾部版本 + if (ReqVersionTail.Contains(".")) + { + if (ModBase.VersionSortInteger(CurrentVersion, ReqVersionTail) > (ReqVersionTailCanEqual ? 0 : -1)) + { + Result.Add(ReqId.Substring(0, 1).ToUpper() + ReqId.Substring(1) + " 版本过高,其版本" + VersionRequire + ",而当前版本为 " + CurrentVersion + "。" + "\r\n" + + " - " + ModEntity.FileName + (ReqId != "minecraft" && ReqId != "forge" ? "\r\n" + " - 前置:" + ((object[])CurrentDependencies[ReqId])[1] : "")); + continue; + } + } + } + else + { + if (!CurrentDependencies.ContainsKey(Dependency.Key)) + { + Result.Add("缺少前置 Mod:" + Dependency.Key + "。" + "\r\n" + " - " + ModEntity.FileName); + continue; + } + } + } + } + catch (Exception ex) + { + Result.Add("检查 Mod 时出错:" + ex.Message + "\r\n" + " - " + ModEntity.FileName); + ModBase.Log(ex, "检查 Mod 时出错"); + } + } + + if (!Result.Any()) + ModBase.Log("[Minecraft] Mod 检查未发现异常"); + else + ModBase.Log("[Minecraft] Mod 检查异常结果:" + "\r\n" + string.Join("\r\n", Result)); + + return Result; + } +#endif +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.vb index 362240a9d..84c979563 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLocalComp.vb @@ -667,11 +667,9 @@ Public Module ModLocalComp Dim LogoItem As ZipArchiveEntry = Jar.GetEntry(LogoFile) If LogoItem IsNot Nothing Then Dim md5 = GetStringMD5(LogoItem.Length.ToString & LogoItem.CompressedLength.ToString & Path) - Logo = $"{PathTemp}MyImage\{md5}.png" + Logo = IO.Path.Combine(PathTemp, "Cache", "Images", $"{md5}.png") Using EntryStream As Stream = LogoItem.Open() - Using FileStream As FileStream = File.Create(Logo) - EntryStream.CopyTo(FileStream) - End Using + WriteFile(Logo, EntryStream) End Using End If End If @@ -737,11 +735,9 @@ GotFabric: Dim LogoItem As ZipArchiveEntry = Jar.GetEntry(LogoFile) If LogoItem IsNot Nothing Then Dim md5 = GetStringMD5(LogoItem.Length.ToString & LogoItem.CompressedLength.ToString & Path) - Logo = $"{PathTemp}MyImage\{md5}.png" + Logo = IO.Path.Combine(PathTemp, "Cache", "Images", $"{md5}.png") Using EntryStream As Stream = LogoItem.Open() - Using FileStream As FileStream = File.Create(Logo) - EntryStream.CopyTo(FileStream) - End Using + WriteFile(Logo, EntryStream) End Using End If End If @@ -796,11 +792,9 @@ GotFabric: Dim LogoItem As ZipArchiveEntry = Jar.GetEntry(LogoFile) If LogoItem IsNot Nothing Then Dim md5 = GetStringMD5(LogoItem.Length.ToString & LogoItem.CompressedLength.ToString & Path) - Logo = $"{PathTemp}MyImage\{md5}.png" + Logo = IO.Path.Combine(PathTemp, "Cache", "Images", $"{md5}.png") Using EntryStream As Stream = LogoItem.Open() - Using FileStream As FileStream = File.Create(Logo) - EntryStream.CopyTo(FileStream) - End Using + WriteFile(Logo, EntryStream) End Using End If End If @@ -982,11 +976,10 @@ Got: Dim packPngEntry = Jar.GetEntry("pack.png") If packPngEntry IsNot Nothing Then Try - Logo = PathTemp & "MyImage\" & GetStringMD5(packPngEntry.Length.ToString & packPngEntry.CompressedLength.ToString & Path) & ".png" + Dim md5 = GetStringMD5(packPngEntry.Length.ToString & packPngEntry.CompressedLength.ToString & Path) + Logo = IO.Path.Combine(PathTemp, "Cache", "Images", $"{md5}.png") Using entryStream As Stream = packPngEntry.Open() - Using fileStream As FileStream = File.Create(Logo) - entryStream.CopyTo(fileStream) - End Using + WriteFile(Logo, entryStream) End Using Log("成功提取资源包图标:" & Path, LogLevel.Debug) Catch logoEx As Exception @@ -1691,7 +1684,7 @@ Finished: '开始网络获取 Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{McInstance}") Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False - Dim CurrentThread As Thread = Thread.CurrentThread + Dim CurrentTaskId As Integer = If(Task.CurrentId, -1) '从 Modrinth 获取信息 RunInNewThread( Sub() @@ -1715,7 +1708,7 @@ Finished: Dim File As New CompFile(ModrinthVersion(Entry.ModrinthHash), CompType.Mod) If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File Next - If Loader.IsAbortedWithThread(CurrentThread) Then Exit Sub + If Loader.IsAbortedWithThread(CurrentTaskId) Then Exit Sub Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息") '步骤 3:获取工程信息 If Not ModrinthMapping.Any() Then Exit Sub @@ -1766,7 +1759,7 @@ Finished: Dim CurseForgeHashes As New List(Of UInteger) For Each Entry In Mods CurseForgeHashes.Add(Entry.CurseForgeHash) - If Loader.IsAbortedWithThread(CurrentThread) Then Exit Sub + If Loader.IsAbortedWithThread(CurrentTaskId) Then Exit Sub Next Dim CurseForgeRaw = CType(CType(GetJson(DlModRequest("https://api.curseforge.com/v1/fingerprints/432", "POST", $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer) @@ -1787,7 +1780,7 @@ Finished: If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File Next Next - If Loader.IsAbortedWithThread(CurrentThread) Then Exit Sub + If Loader.IsAbortedWithThread(CurrentTaskId) Then Exit Sub Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息") '步骤 3:获取工程信息 If Not CurseForgeMapping.Any() Then Exit Sub diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs new file mode 100644 index 000000000..240ba70ff --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.cs @@ -0,0 +1,3500 @@ +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; +using System.Collections; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace PCL; + +public static class ModMinecraft +{ + /// + /// 发送 Minecraft 更新提示。 + /// + public static void McDownloadClientUpdateHint(string versionName, JObject json) + { + try + { + // 获取对应版本 + JToken version = null; + foreach (var Token in json["versions"]) + if (Token["id"] is not null && (Token["id"].ToString() ?? "") == (versionName ?? "")) + { + version = Token; + break; + } + + // 进行提示 + if (version is null) + return; + var time = (DateTime)version["releaseTime"]; + var msgBoxText = $"新版本:{versionName}{"\r\n"}" + ((DateTime.Now - time).TotalDays > 1d + ? "更新时间:" + time + : "更新于:" + TimeUtils.GetTimeSpanString(time - DateTime.Now, false)); + var msgResult = ModMain.MyMsgBox(msgBoxText, "Minecraft 更新提示", "确定", "下载", + (DateTime.Now - time).TotalHours > 3d ? "更新日志" : "", + Button3Action: () => ModDownloadLib.McUpdateLogShow(version)); + // 弹窗结果 + if (msgResult == 2) + // 下载 + ModBase.RunInUi(() => + { + PageDownloadInstall.McVersionWaitingForSelect = versionName; + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall); + }); + } + + catch (Exception ex) + { + ModBase.Log(ex, "Minecraft 更新提示发送失败(" + (versionName ?? "Nothing") + ")", ModBase.LogLevel.Feedback); + } + } + + /// + /// 比较两个版本名;等同 Left >= Right。 + /// 无法比较两个预发布版的大小。 + /// 支持的格式:未知版本, 1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6 + /// + public static bool CompareVersionGe(string left, string right) + { + return CompareVersion(left, right) >= 0; + } + + /// + /// 比较两个版本名,若 Left 较新则返回 1,相同则返回 0,Right 较新则返回 -1;等同 Left - Right。 + /// 无法比较两个预发布版的大小。 + /// 支持的格式:未知版本, 26.1-snapshot-1,1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6 + /// + public static int CompareVersion(string left, string right) + { + if (left == "未知版本" || right == "未知版本") + { + if (left == "未知版本" && right != "未知版本") + return 1; + if (left == "未知版本" && right == "未知版本") + return 0; + if (left != "未知版本" && right == "未知版本") + return -1; + } + + left = left.ToLowerInvariant(); + right = right.ToLowerInvariant(); + var lefts = left.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+"); + var rights = right.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+"); + var i = 0; + while (true) + { + // 两边均缺失,感觉是一个东西 + if (lefts.Count - 1 < i && rights.Count - 1 < i) + { + if (Operators.CompareString(left, right, false) > 0) + return 1; + if (Operators.CompareString(left, right, false) < 0) + return -1; + return 0; + } + + // 确定两边的数值 + var leftValue = Conversions.ToString(lefts.Count - 1 < i ? 0 : lefts[i]); + var rightValue = Conversions.ToString(rights.Count - 1 < i ? 0 : rights[i]); + if ((leftValue ?? "") == (rightValue ?? "")) + goto NextEntry; + if (leftValue == "rc") + leftValue = (-1).ToString(); + if (leftValue == "pre") + leftValue = (-2).ToString(); + if (leftValue == "snapshot") + leftValue = (-3).ToString(); + if (leftValue == "experimental") + leftValue = (-4).ToString(); + var leftValValue = ModBase.Val(leftValue); + if (rightValue == "rc") + rightValue = (-1).ToString(); + if (rightValue == "pre") + rightValue = (-2).ToString(); + if (rightValue == "snapshot") + rightValue = (-3).ToString(); + if (rightValue == "experimental") + rightValue = (-4).ToString(); + var rightValValue = ModBase.Val(rightValue); + if (leftValValue == 0d && rightValValue == 0d) + { + // 如果没有数值则直接比较字符串 + if (Operators.CompareString(leftValue, rightValue, false) > 0) return 1; + + if (Operators.CompareString(leftValue, rightValue, false) < 0) return -1; + } + // 如果有数值则比较数值 + // 这会使得一边是数字一边是字母时数字方更大 + else if (leftValValue > rightValValue) + { + return 1; + } + else if (leftValValue < rightValValue) + { + return -1; + } + + NextEntry:; + + i += 1; + } + + return 0; + } + + /// + /// 打码字符串中的 AccessToken。 + /// + public static string FilterAccessToken(string Raw, char FilterChar) + { + // 打码 "accessToken " 后的内容 + if (Raw.Contains("accessToken ")) + foreach (var Token in Raw.RegexSearch("(?<=accessToken ([^ ]{5}))[^ ]+(?=[^ ]{5})")) + Raw = Raw.Replace(Token, new string(FilterChar, Token.Count())); + // 打码当前登录的结果 + var AccessToken = ModLaunch.McLoginLoader.Output.AccessToken; + if (AccessToken is not null && AccessToken.Length >= 10 && Raw.ContainsF(AccessToken, true) && + (ModLaunch.McLoginLoader.Output.Uuid ?? "") != + (ModLaunch.McLoginLoader.Output.AccessToken ?? "")) // UUID 和 AccessToken 一样则不打码 + Raw = Raw.Replace(AccessToken, + Strings.Left(AccessToken, 5) + new string(FilterChar, AccessToken.Length - 10) + + Strings.Right(AccessToken, 5)); + return Raw; + } + + /// + /// 打码字符串中的 Windows 用户名。 + /// + public static string FilterUserName(string Raw, char FilterChar) + { + var UserProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var UserName = UserProfile.Split(@"\").Last(); + var MaskedProfile = UserProfile.Replace(UserName, new string(FilterChar, UserName.Length)); + return Raw.Replace(UserProfile, MaskedProfile); + } + + /// + /// 比较两个版本名的排序器。 + /// + public class VersionComparer : IComparer + { + public int Compare(string x, string y) + { + return CompareVersion(x, y); + } + } + + #region 文件夹 + + /// + /// 当前的 Minecraft 文件夹路径,以“\”结尾。 + /// + public static string McFolderSelected; + + /// + /// 当前的 Minecraft 文件夹列表。 + /// + public static List McFolderList = new(); + + public class McFolder // 必须是 Class,否则不是引用类型,在 ForEach 中不会得到刷新 + { + public enum Types + { + Original, + RenamedOriginal, + Custom + } + + /// + /// 文件夹路径。 + /// 以 \ 结尾,例如 "D:\Game\MC\.minecraft\"。 + /// + public string Location; + + public string Name; + public Types Type; + + public override bool Equals(object obj) + { + if (!(obj is McFolder)) + return false; + var folder = (McFolder)obj; + return (Name ?? "") == (folder.Name ?? "") && (Location ?? "") == (folder.Location ?? "") && + Type == folder.Type; + } + + public override string ToString() + { + return Location; + } + } + + /// + /// 加载 Minecraft 文件夹列表。 + /// + public static ModLoader.LoaderTask McFolderListLoader = new("Minecraft Folder List", + _ => McFolderListLoadSub(), Priority: ThreadPriority.AboveNormal); + + private static void McFolderListLoadSub() + { + try + { + // 初始化 + var cacheMcFolderList = new List(); + + #region 读取自定义(Custom)文件夹,可能没有结果 + + // 格式:TMZ 12>C://xxx/xx/|Test>D://xxx/xx/|名称>路径 + foreach (string folder in (IEnumerable)((dynamic)States.Game.Folders).Split("|")) + { + if (string.IsNullOrEmpty(folder)) + continue; + if (!folder.Contains(">") || !folder.EndsWithF(@"\")) + { + ModMain.Hint("无效的 Minecraft 文件夹:" + folder, ModMain.HintType.Critical); + continue; + } + + var name = folder.Split(">")[0]; + var path = folder.Split(">")[1]; + try + { + ModBase.CheckPermissionWithException(path); + cacheMcFolderList.Add(new McFolder { Name = name, Location = path, Type = McFolder.Types.Custom }); + } + catch (Exception ex) + { + ModMain.MyMsgBox( + "失效的 Minecraft 文件夹:" + "\r\n" + path + "\r\n" + "\r\n" + + ex.Message, "Minecraft 文件夹失效", IsWarn: true); + ModBase.Log(ex, $"无法访问 Minecraft 文件夹 {path}"); + } + } + + #endregion + + #region 读取默认(Original)文件夹,即当前、官启文件夹,可能没有结果 + + var currentMcFolderList = new List(); + var originalMcFolderList = new List(); + // 扫描当前文件夹 + try + { + if (Directory.Exists(ModBase.ExePath + @"versions\")) + originalMcFolderList.Add(new McFolder + { Name = "当前文件夹", Location = ModBase.ExePath, Type = McFolder.Types.Original }); + foreach (var folder in new DirectoryInfo(ModBase.ExePath).GetDirectories()) + if (Directory.Exists(folder.FullName + @"versions\") || folder.Name == ".minecraft") + { + var newCurrentFolder = new McFolder + { Name = folder.Name, Location = folder.FullName + @"\", Type = McFolder.Types.Original }; + originalMcFolderList.Add(newCurrentFolder); + currentMcFolderList.Add(newCurrentFolder); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "扫描 PCL 所在文件夹中是否有 MC 文件夹失败"); + } + + // 扫描官启文件夹 + var MojangPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\.minecraft\"; + if ((!currentMcFolderList.Any() || (MojangPath ?? "") != (currentMcFolderList[0].Location ?? "")) && + Directory.Exists(MojangPath + @"versions\")) // 当前文件夹不是官启文件夹 + // 具有权限且存在 versions 文件夹 + originalMcFolderList.Add(new McFolder + { Name = "官方启动器文件夹", Location = MojangPath, Type = McFolder.Types.Original }); + + ModBase.Log(cacheMcFolderList.Count + " 个自定义文件夹," + originalMcFolderList.Count + " 个原始文件夹"); + + var unAdded = false; + foreach (var newOriginalFolder in originalMcFolderList) + { + foreach (var cacheFolder in cacheMcFolderList) + if ((cacheFolder.Location ?? "") == (newOriginalFolder.Location ?? "")) + { + if ((cacheFolder.Name ?? "") != (newOriginalFolder.Name ?? "")) + cacheFolder.Type = McFolder.Types.RenamedOriginal; + else + cacheFolder.Type = McFolder.Types.Original; + unAdded = true; + } + + if (!unAdded) + cacheMcFolderList.Add(newOriginalFolder); // 如果没有重命名,则添加当前文件夹 + } + + #endregion + + #region 读取自定义文件夹情况并写入设置 + + // 将自定义文件夹情况同步到设置 + var config = new List(); + foreach (var Folder in cacheMcFolderList) + config.Add(Folder.Name + ">" + Folder.Location); + if (!config.Any()) + config.Add(""); // 防止 0 元素 Join 返回 Nothing + States.Game.Folders = config.Join("|"); + + #endregion + + // 若没有可用文件夹,则创建 .minecraft + if (!cacheMcFolderList.Any()) + { + Directory.CreateDirectory(ModBase.ExePath + @".minecraft\versions\"); + cacheMcFolderList.Add(new McFolder + { Name = "当前文件夹", Location = ModBase.ExePath + @".minecraft\", Type = McFolder.Types.Original }); + } + + foreach (var Folder in cacheMcFolderList) McFolderLauncherProfilesJsonCreate(Folder.Location); + if (Conversions.ToBoolean(Config.Debug.AddRandomDelay)) + Thread.Sleep(RandomUtils.NextInt(200, 2000)); + + // 回设 + McFolderList = cacheMcFolderList; + } + + catch (Exception ex) + { + ModBase.Log(ex, "加载 Minecraft 文件夹列表失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 为 Minecraft 文件夹创建 launcher_profiles.json 文件。 + /// + public static void McFolderLauncherProfilesJsonCreate(string Folder) + { + try + { + if (File.Exists(Folder + "launcher_profiles.json")) + return; + var ResultJson = @"{ + ""profiles"": { + ""PCL"": { + ""icon"": ""Grass"", + ""name"": ""PCL"", + ""lastVersionId"": ""latest-release"", + ""type"": ""latest-release"", + ""lastUsed"": """ + DateTime.Now.ToString("yyyy'-'MM'-'dd") + "T" + DateTime.Now.ToString("HH':'mm':'ss") + + @".0000Z"" + } + }, + ""selectedProfile"": ""PCL"", + ""clientToken"": ""23323323323323323323323323323333"" +}"; + ModBase.WriteFile(Folder + "launcher_profiles.json", ResultJson, Encoding: Encoding.GetEncoding("GB18030")); + ModBase.Log("[Minecraft] 已创建 launcher_profiles.json:" + Folder); + } + catch (Exception ex) + { + ModBase.Log(ex, "创建 launcher_profiles.json 失败(" + Folder + ")", ModBase.LogLevel.Feedback); + } + } + + #endregion + + #region 实例处理 + + public const int McInstanceCacheVersion = 30; + + private static McInstance _mcInstanceSelected; + private static object _McInstanceSelected_mcInstanceSelectedLast = 0; // 为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化 + + /// + /// 当前的 Minecraft 版本。 + /// + public static McInstance McInstanceSelected + { + get => _mcInstanceSelected; + set + { + if (ReferenceEquals(_McInstanceSelected_mcInstanceSelectedLast, value)) + return; + _mcInstanceSelected = value; // 由于有可能是 Nothing,导致无法初始化,才得这样弄一圈 + _McInstanceSelected_mcInstanceSelectedLast = value; + if (value is null) + return; + // 重置缓存的 Mod 文件夹 + PageDownloadCompDetail.CachedFolder.Clear(); + } + } + + private static bool _JsonVersion_jsonVersionInited; + + public class McInstance + { + private McInstanceInfo _info; + private string _inheritInstanceName; + private JObject _JsonObject; + private string _JsonText; + private JObject _jsonVersion; + private string _Name; + + /// + /// 显示的描述文本。 + /// + public string Desc = "该实例未被加载,请向作者反馈此问题"; + + /// + /// 强制实例分类,0 为未启用,1 为隐藏,2 及以上为其他普通分类。 + /// + public McInstanceCardType DisplayType = McInstanceCardType.Auto; + + public bool IsLoaded; + + /// + /// 是否为收藏的实例。 + /// + public bool IsStar; + + /// + /// 显示的实例图标。 + /// + public string Logo; + + /// + /// 实例的发布时间。 + /// + public DateTime ReleaseTime = new(1970, 1, 1, 15, 0, 0); + + /// + /// 该实例的列表检查原始结果,不受自定义影响。 + /// + public McInstanceState State = McInstanceState.Error; + + /// + /// 实例名,或实例文件夹的完整路径(不规定是否以 \ 结尾)。 + public McInstance(string name) + { + PathInstance = (name.Contains(":") ? "" : McFolderSelected + @"versions\") + name + + (name.EndsWithF(@"\") ? "" : @"\"); // 补全完整路径 + // 补全右划线 + } + + /// + /// 该实例的实例文件夹,以“\”结尾。 + /// + public string PathInstance { get; } + + /// + /// 应用版本隔离后,该实例所对应的 Minecraft 根文件夹,以“\”结尾。 + /// + public string PathIndie + { + get + { + if (ModBase.Setup.IsUnset("VersionArgumentIndieV2", this)) + { + if (!IsLoaded) + Load(); + + // 决定该实例是否应该被隔离 + bool ShouldBeIndie() + { + // 从老的实例独立设置中迁移:-1 未决定,0 使用全局设置,1 手动开启,2 手动关闭 + if (!ModBase.Setup.IsUnset("VersionArgumentIndie", this) && Conversions.ToBoolean( + Operators.ConditionalCompareObjectGreater( + ModBase.Setup.Get("VersionArgumentIndie", this), 0, false))) + { + ModBase.Log($"[Minecraft] 版本隔离初始化({Name}):从老的实例独立设置中迁移"); + return Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(ModBase.Setup.Get("VersionArgumentIndie", this), + 1, false)); + } + + // 若实例文件夹下包含 mods 或 saves 文件夹,则自动开启版本隔离 + var ModFolder = new DirectoryInfo(PathInstance + @"mods\"); + var SaveFolder = new DirectoryInfo(PathInstance + @"saves\"); + if ((ModFolder.Exists && ModFolder.EnumerateFiles().Any()) || + (SaveFolder.Exists && SaveFolder.EnumerateDirectories().Any())) + { + ModBase.Log($"[Minecraft] 版本隔离初始化({Name}):实例文件夹下存在 mods 或 saves 文件夹,自动开启"); + return true; + } + + // 根据全局的默认设置决定是否隔离 + var IsRelease = State != McInstanceState.Fool && State != McInstanceState.Old && + State != McInstanceState.Snapshot; + ModBase.Log( + $"[Minecraft] 版本隔离初始化({Name}):从全局默认设置中({Config.Launch.IndieSolutionV2})判断,State {ModBase.GetStringFromEnum(State)},IsRelease {IsRelease},Modable {Modable}"); + switch (Config.Launch.IndieSolutionV2) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 关闭 + { + return false; + } + case var case1 + when Operators.ConditionalCompareObjectEqual(case1, 1, false): // 仅隔离可安装 Mod 的实例 + { + return Info.HasLabyMod || Modable; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 2, false): // 仅隔离非正式版 + { + return !IsRelease; + } + case var case3 + when Operators.ConditionalCompareObjectEqual(case3, 3, false): // 隔离非正式版与可安装 Mod 的实例 + { + return Info.HasLabyMod || Modable || !IsRelease; // 隔离所有实例 + } + + default: + { + return true; + } + } + } + + ; + Config.Instance.IndieV2[this] = ShouldBeIndie(); + } + + return Conversions.ToBoolean(ModBase.Setup.Get("VersionArgumentIndieV2", this)) + ? PathInstance + : McFolderSelected; + } + } + + /// + /// 该实例的实例文件夹名称。 + /// + public string Name + { + get + { + if (_Name is null && !string.IsNullOrEmpty(PathInstance)) + _Name = ModBase.GetFolderNameFromPath(PathInstance); + return _Name; + } + } + + /// + /// 该实例是否可以安装 Mod。 + /// + public bool Modable + { + get + { + if (!IsLoaded) + Load(); + return Info.HasFabric || Info.HasLegacyFabric || Info.HasQuilt || Info.HasForge || Info.HasLiteLoader || + Info.HasNeoForge || Info.HasCleanroom || DisplayType == McInstanceCardType.API; // #223 + } + } + + /// + /// 实例信息。 + /// + public McInstanceInfo Info + { + get + { + if (_info is not null) + return _info; + _info = new McInstanceInfo(); + + #region 获取游戏版本 + + try + { + // 获取发布时间并判断是否为老版本 + try + { + if (JsonObject["releaseTime"] is null) + ReleaseTime = new DateTime(1970, 1, 1, 15, 0, 0); // 未知版本也可能显示为 1970 年 + else + ReleaseTime = JsonObject["releaseTime"].ToObject(); + if (ReleaseTime.Year > 2000 && ReleaseTime.Year < 2013) + { + _info.VanillaName = "Old"; + goto VersionSearchFinish; + } + } + catch + { + ReleaseTime = new DateTime(1970, 1, 1, 15, 0, 0); + } + + // 实验性快照 + if ((string)(JsonObject["type"] ?? "") == "pending") + { + _info.VanillaName = "pending"; + goto VersionSearchFinish; + } + + // 从 PCL 下载的版本信息中获取版本号 + if (JsonObject["clientVersion"] is not null) + { + _info.VanillaName = (string)JsonObject["clientVersion"]; + goto VersionSearchFinish; + } + + // 从 HMCL 下载的版本信息中获取版本号 + if (JsonObject["patches"] is not null) + foreach (JObject patch in JsonObject["patches"]) + if ((patch["id"] ?? "").ToString() == "game" && patch["version"] is not null) + { + _info.VanillaName = patch["version"].ToString(); + goto VersionSearchFinish; + } + + // 从 Forge / NeoForge / LabyMod Arguments 中获取版本号 + if (JsonObject["arguments"] is not null) + { + if (JsonObject["arguments"]["game"] is not null) + { + var Mark = false; + foreach (var Argument in JsonObject["arguments"]["game"]) + { + if (Mark) + { + _info.VanillaName = Argument.ToString(); + goto VersionSearchFinish; + } + + if (Argument.ToString() == "--fml.mcVersion") + Mark = true; + } + } + + if (JsonObject["arguments"]["jvm"] is not null) + foreach (var Argument in JsonObject["arguments"]["game"]) + { + var regexArgument = Argument.ToString().RegexSeek(RegexPatterns.LabyModVersion); + if (regexArgument is not null) + { + _info.VanillaName = regexArgument; + goto VersionSearchFinish; + } + } + } + + // 从继承实例中获取版本号 + if (!string.IsNullOrEmpty(InheritInstanceName)) + { + _info.VanillaName = (JsonObject["jar"] ?? "").ToString(); // LiteLoader 优先使用 Jar + if (string.IsNullOrEmpty(_info.VanillaName)) + _info.VanillaName = InheritInstanceName; + goto VersionSearchFinish; + } + + // 从下载地址中获取版本号 + var regex = (JsonObject["downloads"] ?? "").ToString() + .RegexSeek(RegexPatterns.MinecraftDownloadUrlVersion); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 从 Forge 版本中获取版本号 + var librariesString = JsonObject["libraries"].ToString(); + regex = librariesString.RegexSeek(RegexPatterns.ForgeLibVersion); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 从 OptiFine 版本中获取版本号 + regex = librariesString.RegexSeek(RegexPatterns.OptiFineLibVersion); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 从 Fabric / Quilt / Legacy Fabric 版本中获取版本号 + regex = librariesString.RegexSeek(RegexPatterns.FabricLikeLibVersion); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 从 jar 项中获取版本号 + if (JsonObject["jar"] is not null) + { + _info.VanillaName = JsonObject["jar"].ToString(); + goto VersionSearchFinish; + } + + // 从 jar 文件的 version.json 中获取版本号 + if (JsonVersion?["name"] is not null) + { + var jsonVerName = JsonVersion["name"].ToString(); + if (jsonVerName.Length < 32) // 因为 wiki 说这玩意儿可能是个 hash,虽然我没发现 + { + _info.VanillaName = jsonVerName; + ModBase.Log("[Minecraft] 从版本 jar 中的 version.json 获取到版本号:" + jsonVerName); + goto VersionSearchFinish; + } + } + + // 从 JSON 的 ID 中获取 + regex = ((string)JsonObject["id"]).RegexSeek(RegexPatterns.MinecraftJsonVersion, + RegexOptions.IgnoreCase); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 非准确的版本判断警告 + ModBase.Log("[Minecraft] 无法完全确认 MC 版本号的版本:" + Name); + _info.Reliable = false; + // 从文件夹名中获取 + regex = Name.RegexSeek(RegexPatterns.MinecraftJsonVersion, RegexOptions.IgnoreCase); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 从 JSON 出现的版本号中获取 + var JsonRaw = (JObject)JsonObject.DeepClone(); + JsonRaw.Remove("libraries"); + var JsonRawText = JsonRaw.ToString(); + regex = JsonRawText.RegexSeek(RegexPatterns.MinecraftJsonVersion, RegexOptions.IgnoreCase); + if (regex is not null) + { + _info.VanillaName = regex; + goto VersionSearchFinish; + } + + // 无法获取 + _info.VanillaName = "Unknown"; + Desc = "PCL 无法识别该版本的 MC 版本号"; + } + catch (Exception ex) + { + ModBase.Log(ex, "识别 Minecraft 版本时出错"); + _info.VanillaName = "Unknown"; + Desc = "无法识别:" + ex.Message; + } + + #endregion + + VersionSearchFinish:; + + _info.VanillaName = _info.VanillaName.Replace("_unobfuscated", "").Replace(" Unobfuscated", ""); + // 获取版本号 + if (_info.VanillaName.StartsWithF("1.")) + { + var segments = _info.VanillaName.Split(" _-.".ToCharArray()); + _info.Vanilla = new Version((int)Math.Round(ModBase.Val(segments.Count() >= 2 ? segments[1] : "0")), + 0, (int)Math.Round(ModBase.Val(segments.Count() >= 3 ? segments[2] : "0"))); + } + else if (_info.VanillaName.RegexCheck(@"^[2-9][0-9]\.")) + { + var segments = _info.VanillaName.Split(" _-.".ToCharArray()); + _info.Vanilla = new Version((int)Math.Round(ModBase.Val(segments[0])), + (int)Math.Round(ModBase.Val(segments.Count() >= 2 ? segments[1] : "0")), + (int)Math.Round(ModBase.Val(segments.Count() >= 3 ? segments[2] : "0"))); + } + else + { + _info.Vanilla = new Version(9999, 0, 0); + } + + return _info; + } + set { _info = value; } + } + + /// + /// 该实例的 JSON 文本。 + /// + public string JsonText + { + get + { + // 快速检查 JSON 是否以 { 开头、} 结尾;忽略空白字符 + bool FastJsonCheck(string Json) + { + var TrimedJson = Json.Trim(); + return TrimedJson.StartsWithF("{") && TrimedJson.EndsWithF("}"); + } + + ; + if (_JsonText is null) + { + var JsonPath = PathInstance + Name + ".json"; + if (!File.Exists(JsonPath)) + { + // 如果文件夹下只有一个 JSON 文件,则将其作为实例 JSON + var JsonFiles = Directory.GetFiles(PathInstance, "*.json"); + if (JsonFiles.Count() == 1) + { + JsonPath = JsonFiles[0]; + ModBase.Log("[Minecraft] 未找到同名实例 JSON,自动换用 " + JsonPath, ModBase.LogLevel.Debug); + } + else + { + throw new Exception($"未找到实例 JSON 文件:{PathInstance}{Name}.json"); + } + } + + _JsonText = ModBase.ReadFile(JsonPath); + // 如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试 + if (!FastJsonCheck(_JsonText)) + { + if (ModBase.RunInUi()) + { + ModBase.Log("[Minecraft] 实例 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", ModBase.LogLevel.Debug); + ModBase.GetJson(_JsonText); // 触发异常 + } + else + { + ModBase.Log($"[Minecraft] 实例 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", ModBase.LogLevel.Debug); + Thread.Sleep(2000); + _JsonText = ModBase.ReadFile(JsonPath); + if (!FastJsonCheck(_JsonText)) + ModBase.GetJson(_JsonText); + } // 触发异常 + } + } + + return _JsonText; + } + set => _JsonText = value; + } + + /// + /// 该实例的 JSON 对象。 + /// 若 JSON 存在问题,在获取该属性时即会抛出异常。 + /// + public JObject JsonObject + { + get + { + if (_JsonObject is null) + { + var Text = JsonText; // 触发 JsonText 的 Get 事件 + try + { + _JsonObject = (JObject)ModBase.GetJson(Text); + // 转换 HMCL 关键项 + if (_JsonObject.ContainsKey("patches") && !_JsonObject.ContainsKey("time")) + { + IsHmclFormatJson = true; + // 合并 JSON + // Dim HasOptiFine As Boolean = False, HasForge As Boolean = False + JObject CurrentObject = null; + var SubjsonList = new List(); + foreach (JObject Subjson in _JsonObject["patches"]) + SubjsonList.Add(Subjson); + SubjsonList.Sort((left, right) => + ModBase.Val((left["priority"] ?? "0").ToString()) < + ModBase.Val((right["priority"] ?? "0").ToString())); + foreach (var Subjson in SubjsonList) + { + var Id = (string)Subjson["id"]; + if (Id is not null) + { + // 合并 JSON + ModBase.Log("[Minecraft] 合并 HMCL 分支项:" + Id); + if (CurrentObject is not null) + CurrentObject.Merge(Subjson); + else + CurrentObject = Subjson; + } + else + { + ModBase.Log("[Minecraft] 存在为空的 HMCL 分支项"); + } + } + + _JsonObject = CurrentObject; + // 修改附加项 + _JsonObject["id"] = Name; + if (_JsonObject.ContainsKey("inheritsFrom")) + _JsonObject.Remove("inheritsFrom"); + } + + // 与继承实例合并 + object inheritInstanceName = null; + do + { + try + { + inheritInstanceName = _JsonObject["inheritsFrom"] is null + ? "" + : _JsonObject["inheritsFrom"].ToString(); + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(inheritInstanceName, Name, false))) + { + ModBase.Log("[Minecraft] 自引用的继承实例:" + Name, ModBase.LogLevel.Debug); + inheritInstanceName = ""; + break; + } + + Recheck:; + + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectNotEqual(inheritInstanceName, "", false))) + { + var inheritInstance = new McInstance(Conversions.ToString(inheritInstanceName)); + // 继续循环 + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(inheritInstance.InheritInstanceName, + inheritInstanceName, false))) + throw new Exception(Conversions.ToString( + Operators.ConcatenateObject("版本依赖项出现嵌套:", inheritInstanceName))); + inheritInstanceName = inheritInstance.InheritInstanceName; + // 合并 + inheritInstance.JsonObject.Merge(_JsonObject); + _JsonObject = inheritInstance.JsonObject; + goto Recheck; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "合并实例依赖项 JSON 失败(" + (inheritInstanceName ?? "null") + ")"); + } + } while (false); + } + catch (Exception ex) + { + throw new Exception("初始化实例 JSON 时失败(" + (Name ?? "null") + ")", ex); + } + } + + return _JsonObject; + } + set => _JsonObject = value; + } + + /// + /// 是否为旧版 JSON 格式。 + /// + public bool IsOldJson => JsonObject["minecraftArguments"] is not null && + (string)JsonObject["minecraftArguments"] != ""; + + /// + /// JSON 是否为 HMCL 格式。 + /// + public bool IsHmclFormatJson { get; set; } + + /// + /// 实例 JAR 中的 version.json 文件对象。 + /// 若没有则返回 Nothing。 + /// + public JObject JsonVersion + { + get + { + if (!_JsonVersion_jsonVersionInited) + { + _JsonVersion_jsonVersionInited = true; + do + { + try + { + if (!File.Exists(PathInstance + Name + ".jar")) + break; + using (var jarArchive = new ZipArchive(new FileStream(PathInstance + Name + ".jar", + FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) + { + var versionJson = jarArchive.GetEntry("version.json"); + if (versionJson is not null) + using (var versionJsonStream = new StreamReader(versionJson.Open())) + { + _jsonVersion = (JObject)ModBase.GetJson(versionJsonStream.ReadToEnd()); + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"从实例 JAR 中读取 version.json 失败 ({PathInstance}{Name}.jar)"); + } + } while (false); + } + + return _jsonVersion; + } + } + + /// + /// 该实例的依赖实例。若无依赖实例则为空字符串。 + /// + public string InheritInstanceName + { + get + { + if (_inheritInstanceName is null) + { + _inheritInstanceName = (JsonObject["inheritsFrom"] ?? "").ToString(); + // 由于过老的 LiteLoader 中没有 Inherits(例如 1.5.2),需要手动判断以获取真实继承实例 + // 此外,由于这里的加载早于实例种类判断,所以需要手动判断是否为 LiteLoader + // 如果实例提供了不同的 JAR,代表所需的 JAR 可能已被更改,则跳过 Inherit 替换 + if (JsonText.Contains("liteloader") && (Info.VanillaName ?? "") != (Name ?? "") && + !JsonText.Contains("logging")) + if (((JsonObject["jar"] ?? Info.VanillaName).ToString() ?? "") == (Info.VanillaName ?? "")) + _inheritInstanceName = Info.VanillaName; + // HMCL 实例无 JSON + if (IsHmclFormatJson) + _inheritInstanceName = ""; + } + + return _inheritInstanceName; + } + } + + /// + /// 检查 Minecraft 版本,若检查通过 State 则为 Original 且返回 True。 + /// + public bool Check() + { + // 检查文件夹 + if (!Directory.Exists(PathInstance)) + { + State = McInstanceState.Error; + Desc = "未找到实例 " + Name; + return false; + } + + // 检查权限 + try + { + Directory.CreateDirectory(PathInstance + @"PCL\"); + ModBase.CheckPermissionWithException(PathInstance + @"PCL\"); + } + catch (Exception ex) + { + State = McInstanceState.Error; + Desc = "PCL 没有对该文件夹的访问权限,请右键以管理员身份运行 PCL"; + ModBase.Log(ex, "没有访问实例文件夹的权限"); + return false; + } + + // 确认 JSON 可用性 + try + { + var jsonObjCheck = JsonObject; + } + catch (Exception ex) + { + ModBase.Log(ex, "实例 JSON 可用性检查失败(" + PathInstance + ")"); + JsonText = ""; + JsonObject = null; + Desc = ex.Message; + State = McInstanceState.Error; + return false; + } + + // 检查版本号获取 + try + { + if (string.IsNullOrEmpty(Info.VanillaName)) + throw new Exception("无法获取版本号,结果为空"); + } + catch (Exception ex) + { + ModBase.Log(ex, "版本号获取失败(" + Name + ")"); + State = McInstanceState.Error; + Desc = "版本号获取失败:" + ex; + return false; + } + + // 检查依赖实例 + try + { + if (!string.IsNullOrEmpty(InheritInstanceName)) + if (!File.Exists(ModBase.GetPathFromFullPath(PathInstance) + InheritInstanceName + @"\" + + InheritInstanceName + ".json")) + { + State = McInstanceState.Error; + Desc = "需要安装 " + InheritInstanceName + " 作为前置实例"; + return false; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "依赖实例检查出错(" + Name + ")"); + State = McInstanceState.Error; + Desc = "未知错误:" + ex; + return false; + } + + State = McInstanceState.Original; + return true; + } + + /// + /// 加载 Minecraft 实例的详细信息。不使用其缓存,且会更新缓存。 + /// + public McInstance Load() + { + try + { + // 检查实例,若出错则跳过数据确定阶段 + if (!Check()) + goto ExitDataLoad; + + #region 确定实例分类 + + switch (Info.VanillaName ?? "") // 在获取 Version.Original 对象时会完成它的加载 + { + case "Unknown": + { + State = McInstanceState.Error; + break; + } + case "Old": + { + State = McInstanceState.Old; // 根据 API 进行筛选 + break; + } + + default: + { + var realJson = JsonObject != null ? JsonObject.ToString() : JsonText; + // 愚人节与快照版本 + if ((JsonObject["type"] ?? "").ToString() == "fool" || + !string.IsNullOrEmpty(GetMcFoolName(Info.VanillaName))) + State = McInstanceState.Fool; + else if (IsSnapshot()) State = McInstanceState.Snapshot; + // OptiFine + if (realJson.Contains("optifine")) + { + State = McInstanceState.OptiFine; + Info.HasOptiFine = true; + Info.OptiFine = realJson.RegexSeek(RegexPatterns.OptiFineVersion) ?? "未知版本"; + } + + // LiteLoader + if (realJson.Contains("liteloader")) + { + State = McInstanceState.LiteLoader; + Info.HasLiteLoader = true; + } + + // Fabric、Forge、Quilt、LabyMod、Legacy Fabric + if (realJson.Contains("labymod_data")) + { + State = McInstanceState.LabyMod; + Info.HasLabyMod = true; + Info.LabyMod = (string)JsonObject["labymod_data"]["version"]; + } + else if (realJson.Contains("net.legacyfabric:intermediary")) + { + State = McInstanceState.LegacyFabric; + Info.HasLegacyFabric = true; + Info.LegacyFabric = + (realJson.RegexSeek(RegexPatterns.LegacyFabricVersion) ?? "未知版本") + .Replace("+build", ""); + } + else if (realJson.Contains("net.fabricmc:fabric-loader")) + { + State = McInstanceState.Fabric; + Info.HasFabric = true; + Info.Fabric = + (realJson.RegexSeek(RegexPatterns.FabricVersion) ?? "未知版本").Replace("+build", ""); + } + else if (realJson.Contains("org.quiltmc:quilt-loader")) + { + State = McInstanceState.Quilt; + Info.HasQuilt = true; + Info.Quilt = + (realJson.RegexSeek(RegexPatterns.QuiltVersion) ?? "未知版本").Replace("+build", ""); + } + else if (realJson.Contains("com.cleanroommc:cleanroom:")) + { + State = McInstanceState.Cleanroom; + Info.HasCleanroom = true; + Info.Cleanroom = + (realJson.RegexSeek(RegexPatterns.CleanroomVersion) ?? "未知版本").Replace("+build", ""); + } + else if (realJson.Contains("minecraftforge") && !realJson.Contains("net.neoforge")) + { + State = McInstanceState.Forge; + Info.HasForge = true; + Info.Forge = realJson.RegexSeek(RegexPatterns.ForgeMainVersion); + if (Info.Forge is null) + Info.Forge = realJson.RegexSeek(RegexPatterns.ForgeLibVersion) ?? "未知版本"; + } + else if (realJson.Contains("net.neoforge")) + { + // 1.20.1 JSON 范例:"--fml.forgeVersion", "47.1.99" + // 1.20.2+ JSON 范例:"--fml.neoForgeVersion", "20.6.119-beta" + State = McInstanceState.NeoForge; + Info.HasNeoForge = true; + Info.NeoForge = realJson.RegexSeek(RegexPatterns.NeoForgeVersion) ?? "未知版本"; + } + + break; + } + } + + #endregion + + ExitDataLoad:; + + // 确定实例图标 + Logo = Config.Instance.LogoPath[PathInstance]; + if (string.IsNullOrEmpty(Logo) || !Config.Instance.IsLogoCustom[PathInstance]) + switch (State) + { + case McInstanceState.Original: + { + Logo = ModBase.PathImage + "Blocks/Grass.png"; + break; + } + case McInstanceState.Snapshot: + { + Logo = ModBase.PathImage + "Blocks/CommandBlock.png"; + break; + } + case McInstanceState.Old: + { + Logo = ModBase.PathImage + "Blocks/CobbleStone.png"; + break; + } + case McInstanceState.Forge: + { + Logo = ModBase.PathImage + "Blocks/Anvil.png"; + break; + } + case McInstanceState.NeoForge: + { + Logo = ModBase.PathImage + "Blocks/NeoForge.png"; + break; + } + case McInstanceState.Cleanroom: + { + Logo = ModBase.PathImage + "Blocks/Cleanroom.png"; + break; + } + case McInstanceState.Fabric: + { + Logo = ModBase.PathImage + "Blocks/Fabric.png"; + break; + } + case McInstanceState.LegacyFabric: + { + Logo = ModBase.PathImage + "Blocks/Fabric.png"; + break; + } + case McInstanceState.Quilt: + { + Logo = ModBase.PathImage + "Blocks/Quilt.png"; + break; + } + case McInstanceState.OptiFine: + { + Logo = ModBase.PathImage + "Blocks/GrassPath.png"; + break; + } + case McInstanceState.LiteLoader: + { + Logo = ModBase.PathImage + "Blocks/Egg.png"; + break; + } + case McInstanceState.Fool: + { + Logo = ModBase.PathImage + "Blocks/GoldBlock.png"; + break; + } + case McInstanceState.LabyMod: + { + Logo = ModBase.PathImage + "Blocks/LabyMod.png"; + break; + } + + default: + { + Logo = ModBase.PathImage + "Blocks/RedstoneBlock.png"; + break; + } + } + + // 确定实例描述 + if (State == McInstanceState.Error) + { + Desc = Desc; + } + else + { + Desc = Config.Instance.CustomInfo[PathInstance]; + if ((Desc ?? "") == (GetDefaultDescription() ?? "")) + Desc = ""; + } + + // 确定实例收藏状态 + IsStar = Config.Instance.Starred[PathInstance]; + // 确定实例显示种类 + DisplayType = (McInstanceCardType)Conversions.ToInteger(Config.Instance.CardType[PathInstance]); + // 写入缓存 + if (Directory.Exists(PathInstance)) + { + Config.Instance.State[PathInstance] = (int)State; + Config.Instance.Info[PathInstance] = Desc; + Config.Instance.LogoPath[PathInstance] = Logo; + } + + if (State != McInstanceState.Error) + { + Config.Instance.ReleaseTime[PathInstance] = ReleaseTime.ToString("yyyy'-'MM'-'dd HH':'mm"); + Config.Instance.FabricVersion[PathInstance] = Info.Fabric; + Config.Instance.LegacyFabricVersion[PathInstance] = Info.LegacyFabric; + Config.Instance.QuiltVersion[PathInstance] = Info.Quilt; + Config.Instance.LabyModVersion[PathInstance] = Info.LabyMod; + Config.Instance.OptiFineVersion[PathInstance] = Info.OptiFine; + Config.Instance.HasLiteLoader[PathInstance] = Info.HasLiteLoader; + Config.Instance.ForgeVersion[PathInstance] = Info.Forge; + Config.Instance.NeoForgeVersion[PathInstance] = Info.NeoForge; + Config.Instance.CleanroomVersion[PathInstance] = Info.Cleanroom; + Config.Instance.VanillaVersionName[PathInstance] = Info.VanillaName; + Config.Instance.VanillaVersion[PathInstance] = Info.Vanilla.ToString(); + } + } + catch (Exception ex) + { + Desc = "未知错误:" + ex; + Logo = ModBase.PathImage + "Blocks/RedstoneBlock.png"; + State = McInstanceState.Error; + ModBase.Log(ex, "加载实例失败(" + Name + ")", ModBase.LogLevel.Feedback); + } + finally + { + IsLoaded = true; + } + + return this; + } + + private bool IsSnapshot() + { + return new[] { "w", "snapshot", "rc", "pre", "experimental", "-" }.Any(s => + Info.VanillaName.ContainsF(s, true)) || Name.ContainsF("combat", true) || + (JsonObject["type"] ?? "").ToString() == "snapshot" || + (JsonObject["type"] ?? "").ToString() == "pending"; + } + + /// + /// 获取实例的默认描述。 + /// + public string GetDefaultDescription() + { + // Mod Loader 信息 + var ModLoaderInfo = ""; + if (this.Info.HasForge) + ModLoaderInfo += ", Forge" + (this.Info.Forge == "未知版本" ? "" : " " + this.Info.Forge); + if (this.Info.HasNeoForge) + ModLoaderInfo += ", NeoForge" + (this.Info.NeoForge == "未知版本" ? "" : " " + this.Info.NeoForge); + if (this.Info.HasCleanroom) + ModLoaderInfo += ", Cleanroom" + (this.Info.Cleanroom == "未知版本" ? "" : " " + this.Info.Cleanroom); + if (this.Info.HasLabyMod) + ModLoaderInfo += ", LabyMod" + (this.Info.LabyMod == "未知版本" ? "" : " " + this.Info.LabyMod); + if (this.Info.HasFabric) + ModLoaderInfo += ", Fabric" + (this.Info.Fabric == "未知版本" ? "" : " " + this.Info.Fabric); + if (this.Info.HasQuilt) + ModLoaderInfo += ", Quilt" + (this.Info.Quilt == "未知版本" ? "" : " " + this.Info.Quilt); + if (this.Info.HasLegacyFabric) + ModLoaderInfo += ", Legacy Fabric" + + (this.Info.LegacyFabric == "未知版本" ? "" : " " + this.Info.LegacyFabric); + if (this.Info.HasOptiFine) + ModLoaderInfo += ", OptiFine" + (this.Info.OptiFine == "未知版本" + ? "" + : " " + this.Info.OptiFine.Replace("-", " ").Replace("_", " ")); + if (this.Info.HasLiteLoader) + ModLoaderInfo += ", LiteLoader"; + // 基础信息 + string Info; + switch (State) + { + case McInstanceState.Snapshot: + case McInstanceState.Original: + case McInstanceState.Forge: + case McInstanceState.NeoForge: + case McInstanceState.Fabric: + case McInstanceState.OptiFine: + case McInstanceState.LiteLoader: + { + if (this.Info.VanillaName.ContainsF("pre", true)) + Info = "预发布版 " + this.Info.VanillaName; + else if (this.Info.VanillaName.ContainsF("rc", true)) + Info = "发布候选 " + this.Info.VanillaName; + else if (this.Info.VanillaName.Contains("experimental")) + Info = "实验性快照" + this.Info.VanillaName; + else if (this.Info.VanillaName == "pending") + Info = "实验性快照"; + else if (IsSnapshot()) + Info = this.Info.Reliable ? "快照版 " + this.Info.VanillaName.Replace("-snapshot", "") : "快照版"; + else + Info = this.Info.Reliable ? "正式版 " + this.Info.VanillaName : "正式版"; + + break; + } + case McInstanceState.Old: + { + Info = "远古版本"; + break; + } + case McInstanceState.Fool: + { + Info = "愚人节版本 " + this.Info.VanillaName; + break; + } + case McInstanceState.Error: + { + return Desc; // 已有错误信息 + } + + default: + { + return "发生了未知错误,请向作者反馈此问题"; + } + } + + return (Info + ModLoaderInfo).Replace("_", "-"); + } + + // 运算符支持 + public override bool Equals(object obj) + { + var instance = obj as McInstance; + return instance is not null && (PathInstance ?? "") == (instance.PathInstance ?? ""); + } + + public static bool operator ==(McInstance a, McInstance b) + { + if (a is null && b is null) + return true; + if (a is null || b is null) + return false; + return (a.PathInstance ?? "") == (b.PathInstance ?? ""); + } + + public static bool operator !=(McInstance a, McInstance b) + { + return !(a == b); + } + } + + public enum McInstanceState + { + Error, + Original, + Snapshot, + Fool, + OptiFine, + Old, + Forge, + NeoForge, + LiteLoader, + Fabric, + LegacyFabric, + Quilt, + Cleanroom, + LabyMod + } + + /// + /// 某个 Minecraft 实例的版本名、附加组件信息。 + /// + public class McInstanceInfo + { + /// + /// Cleanroom 版本号,如 0.2.4-alpha。 + /// + public string Cleanroom = ""; + + /// + /// Fabric 版本号,如 0.7.2.175。 + /// + public string Fabric = ""; + + /// + /// Forge 版本号,如 31.1.2、14.23.5.2847。 + /// + public string Forge = ""; + + // Cleanroom + + /// + /// 该实例是否安装了 Cleanroom。 + /// + public bool HasCleanroom; + + // Fabric + + /// + /// 该实例是否安装了 Fabric。 + /// + public bool HasFabric; + + // Forge + + /// + /// 该实例是否安装了 Forge。 + /// + public bool HasForge; + + // LabyMod + + /// + /// 该实例是否安装了 LabyMod。 + /// + public bool HasLabyMod; + + // LegacyFabric + + /// + /// 该实例是否安装了 Fabric。 + /// + public bool HasLegacyFabric; + + // LiteLoader + + /// + /// 该实例是否安装了 LiteLoader。 + /// + public bool HasLiteLoader; + + // NeoForge + + /// + /// 该实例是否安装了 NeoForge。 + /// + public bool HasNeoForge; + + // OptiFine + + /// + /// 该实例是否通过 JSON 安装了 OptiFine。 + /// + public bool HasOptiFine; + + + // Quilt + + /// + /// 该实例是否安装了 Quilt。 + /// + public bool HasQuilt; + + /// + /// LabyMod 版本号,如 4.2.59。 + /// + public string LabyMod = ""; + + /// + /// Fabric 版本号,如 0.7.2.175。 + /// + public string LegacyFabric = ""; + + /// + /// NeoForge 版本号,如 21.0.2-beta、47.1.79。 + /// + public string NeoForge = ""; + + /// + /// OptiFine 版本号,如 C8、C9_pre10。 + /// + public string OptiFine = ""; + + /// + /// Quilt 版本号,如 0.26.1-beta.1、0.26.0。 + /// + public string Quilt = ""; + + /// + /// 指示原版版本号是否可靠(不是通过猜测获取)。 + /// + public bool Reliable = true; + + /// + /// 可比较的三段式原版版本号。 + /// 对老版本格式,例如 1.20.3,会被转换为 20.0.3。 + /// 若没有版本号,例如旧快照,则为 9999.0.0。 + /// + public Version Vanilla; + + // 原版 + + /// + /// 原版版本名。 + /// 如 26.1,26.1-snapshot-1,1.12.2,16w01a。 + /// + public string VanillaName; + + /// + /// 原版版本号是否有效。 + /// + public bool Valid => Vanilla.Major < 1000; + + /// + /// 可供比较的原版 Drop 序数。 + /// 例如 26.3.2 为 263,1.21.5 为 210。 + /// 若没有版本号,例如旧快照,则直接指定为 209。 + /// + public int Drop => Valid ? Vanilla.Major * 10 + Vanilla.Minor : 209; + + /// + /// 可供比较的 OptiFine 版本序数。 + /// + public int OptiFineCode + { + get + { + if (string.IsNullOrEmpty(OptiFine) || OptiFine == "未知版本") + return 0; + // 字母编号,如 G2 中的 G(7) + var result = Strings.Asc(OptiFine.ToUpper().First()) - Strings.Asc('A') + 1; + // 末尾数字,如 C5 beta4 中的 5 + result *= 100; + result = (int)Math.Round(result + + ModBase.Val(Strings.Right(OptiFine, OptiFine.Length - 1).RegexSeek("[0-9]+"))); + // 测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x) + result *= 100; + if (OptiFine.ContainsF("pre", true)) + result += 50; + if (OptiFine.ContainsF("pre", true) || OptiFine.ContainsF("beta", true)) + { + if (ModBase.Val(Strings.Right(OptiFine, 1)) == 0d && Strings.Right(OptiFine, 1) != "0") + result += 1; // 为 pre 或 beta 结尾,视作 1 + else + result = + (int)Math.Round(result + + ModBase.Val(OptiFine.ToLower().RegexSeek("(?<=((pre)|(beta)))[0-9]+"))); + } + else + { + result += 99; + } + + return result; + } + } + + // Forgelike + + /// + /// 该版本是否安装了 Forgelike 加载器。 + /// + public bool HasForgelike => HasForge || HasNeoForge || HasCleanroom; + + /// + /// 可供比较的类 Forge 版本序数。 + /// + public int ForgelikeCode + { + get + { + if (!HasForgelike) + return 0; + if ((string.IsNullOrEmpty(Forge) || Forge == "未知版本") && + (string.IsNullOrEmpty(NeoForge) || NeoForge == "未知版本")) + return 0; + var segments = (HasForge ? Forge : NeoForge).RegexSearch(@"\d+"); + switch (segments.Count) + { + case var @case when @case > 4: + { + return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d + + ModBase.Val(segments[3])); + } + case 3: + { + return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d + + ModBase.Val(segments[2])); + } + case 2: + { + return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d + ModBase.Val(segments[1]) * 10000d); + } + + default: + { + return (int)Math.Round(ModBase.Val(segments[0]) * 1000000d); + } + } + } + } + + // Fabriclike + + /// + /// 该版本是否安装了 Fabriclike 加载器。 + /// + public bool HasFabriclike => HasFabric || HasQuilt || HasLegacyFabric; + + // API + + /// + /// 生成对此实例信息的用户友好的描述性字符串。 + /// + public override string ToString() + { + string ToStringRet = default; + ToStringRet = ""; + if (HasForge) + ToStringRet += ", Forge" + (Forge == "未知版本" ? "" : " " + Forge); + if (HasNeoForge) + ToStringRet += ", NeoForge" + (NeoForge == "未知版本" ? "" : " " + NeoForge); + if (HasCleanroom) + ToStringRet += ", Cleanroom" + (Cleanroom == "未知版本" ? "" : " " + Cleanroom); + if (HasFabric) + ToStringRet += ", Fabric" + (Fabric == "未知版本" ? "" : " " + Fabric); + if (HasLegacyFabric) + ToStringRet += ", LegacyFabric" + (LegacyFabric == "未知版本" ? "" : " " + LegacyFabric); + if (HasQuilt) + ToStringRet += ", Quilt" + (Quilt == "未知版本" ? "" : " " + Quilt); + if (HasLabyMod) + ToStringRet += ", LabyMod" + (LabyMod == "未知版本" ? "" : " " + LabyMod); + if (HasOptiFine) + ToStringRet += ", OptiFine" + (OptiFine == "未知版本" ? "" : " " + OptiFine); + if (HasLiteLoader) + ToStringRet += ", LiteLoader"; + if (string.IsNullOrEmpty(ToStringRet)) return "原版 " + VanillaName; + + return VanillaName + ToStringRet; + } + + // Helpers + + /// + /// 版本字符串是否符合 Minecraft 原版格式,例如 1.x、26.x。 + /// + public static bool IsFormatFit(string version) + { + if (version is null) + return false; + if (version.RegexCheck(@"^1\.\d")) + return true; + if (ModBase.Val(version.RegexSeek(@"^[2-9]\d\.\d+")) > 25d) + return true; + return false; + } + + /// + /// 尝试将版本字符串转换为 Drop 序数。 + /// 若无法转换则返回 0。 + /// + public static int VersionToDrop(string? version, bool allowSnapshot = false) + { + if (!allowSnapshot && version.Contains("-")) + return 0; + if (version is null) + return 0; + var segments = version.BeforeFirst("-").Split("."); + if (segments.Length < 2) + return 0; + var major = (int)Math.Round(ModBase.Val(segments[0])); + var minor = (int)Math.Round(ModBase.Val(segments[1])); + if (major == 1) return minor * 10; + + if (major < 25) return 0; + + return major * 10 + minor; + } + + /// + /// 将 Drop 序数转换为版本字符串。 + /// + public static string DropToVersion(int drop) + { + if (drop >= 250) return $"{drop / 10}.{drop % 10}"; + + return $"1.{drop / 10}"; + } + } + + /// + /// 根据版本名获取对应的愚人节版本描述。非愚人节版本会返回空字符串。 + /// + public static string GetMcFoolName(string name) + { + name = name.ToLower(); + if (name.StartsWithF("2.0") || name.StartsWithF("2point0")) + { + var tag = ""; + if (name.EndsWith("red")) + tag = "(红色版本)"; + else if (name.EndsWith("blue")) + tag = "(蓝色版本)"; + else if (name.EndsWith("purple")) tag = "(紫色版本)"; + return "2013 | 这个秘密计划了两年的更新将游戏推向了一个新高度!" + tag; + } + + if (name == "15w14a") return "2015 | 作为一款全年龄向的游戏,我们需要和平,需要爱与拥抱。"; + + if (name == "1.rv-pre1") return "2016 | 是时候将现代科技带入 Minecraft 了!"; + + if (name == "3d shareware v1.34") return "2019 | 我们从地下室的废墟里找到了这个开发于 1994 年的杰作!"; + + if (name.StartsWithF("20w14inf") || name == "20w14∞") return "2020 | 我们加入了 20 亿个新的维度,让无限的想象变成了现实!"; + + if (name == "22w13oneblockatatime") return "2022 | 一次一个方块更新!迎接全新的挖掘、合成与骑乘玩法吧!"; + + if (name == "23w13a_or_b") return "2023 | 研究表明:玩家喜欢作出选择——越多越好!"; + + if (name == "24w14potato") return "2024 | 毒马铃薯一直都被大家忽视和低估,于是我们超级加强了它!"; + + if (name == "25w14craftmine") return "2025 | 你可以合成任何东西——包括合成你的世界!"; + + return ""; + } + + /// + /// 当前按卡片分类的所有版本列表。 + /// + public static Dictionary> McInstanceList = new(); + + #endregion + + #region 实例列表加载 + + /// + /// 是否要求本次加载强制刷新实例列表。 + /// + public static bool McInstanceListForceRefresh; + + /// + /// 是否为本次打开 PCL 后第一次加载实例列表。 + /// 这会清理所有 .pclignore 文件,而非跳过这些对应实例。 + /// + private static bool _isFirstMcInstanceListLoad = true; + + /// + /// 加载 Minecraft 文件夹的实例列表。 + /// + public static ModLoader.LoaderTask McInstanceListLoader = + new("Minecraft Instance List", InitMcInstanceList) { ReloadTimeout = 1 }; + + private static void InitMcInstanceList(ModLoader.LoaderTask loader) + { + var path = loader.Input; + try + { + // 初始化 + McInstanceList = new Dictionary>(); + var versionsPath = Path.Combine(path, "versions"); + var folderList = new List(); + + // 读取版本文件夹 + if (Directory.Exists(versionsPath)) + try + { + foreach (var folder in new DirectoryInfo(versionsPath).GetDirectories()) + folderList.Add(folder.Name); + } + catch (Exception ex) + { + throw new Exception($"无法读取实例文件夹,可能是由于没有权限({versionsPath})", ex); + } + + // 如果没有可用实例,清空缓存并跳过后续处理 + if (!folderList.Any()) + { + ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", ""); + McInstanceSelected = null; + States.Game.SelectedInstance = ""; + ModBase.Log("[Minecraft] 未找到可用 Minecraft 实例"); + return; + } + + // 根据文件夹名列表生成辨识码 + var folderListHash = ModBase.GetHash(McInstanceCacheVersion + "#" + string.Join("#", folderList)); + var folderListCheck = (int)(folderListHash % (int.MaxValue - 1)); + + // 尝试使用缓存 + var useCache = !McInstanceListForceRefresh && + ModBase.Val(ModBase.ReadIni(Path.Combine(path, "PCL.ini"), "InstanceCache")) == + folderListCheck; + + if (useCache) + { + var cachedResult = InitMcInstanceListWithCache(path); + if (cachedResult != null) + McInstanceList = cachedResult; + else + useCache = false; // 缓存无效,需要重载 + } + + // 如果不能使用缓存,重新加载 + if (!useCache) + { + McInstanceListForceRefresh = false; + ModBase.Log("[Minecraft] 文件夹列表变更或缓存无效,重载所有实例"); + ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", folderListCheck.ToString()); + McInstanceList = InitMcInstanceListWithoutCache(path); + } + + _isFirstMcInstanceListLoad = false; + + if (loader.IsAborted) + return; + + // 尝试读取已储存的选择 + var savedSelection = ModBase.ReadIni(Path.Combine(path, "PCL.ini"), "Version"); + if (!string.IsNullOrEmpty(savedSelection)) + foreach (var card in McInstanceList) + foreach (var instance in card.Value) + if ((instance.Name ?? "") == savedSelection && instance.State != McInstanceState.Error) + { + McInstanceSelected = instance; + States.Game.SelectedInstance = McInstanceSelected.Name; + ModBase.Log("[Minecraft] 选择该文件夹储存的 Minecraft 实例:" + McInstanceSelected.PathInstance); + return; + } + + // 自动选择第一项 + var firstInstance = McInstanceList + .SelectMany(kv => kv.Value) + .FirstOrDefault(i => i.State != McInstanceState.Error); + + if (firstInstance != null) + { + McInstanceSelected = firstInstance; + States.Game.SelectedInstance = McInstanceSelected.Name; + ModBase.Log("[Launch] 自动选择 Minecraft 实例:" + McInstanceSelected.PathInstance); + } + else + { + McInstanceSelected = null; + States.Game.SelectedInstance = ""; + ModBase.Log("[Minecraft] 未找到可用 Minecraft 实例"); + } + + // 调试延迟 + if (Config.Debug.AddRandomDelay is bool debugDelay && debugDelay) + Thread.Sleep(RandomUtils.NextInt(200, 3000)); + } + catch (ThreadInterruptedException) + { + // 中断线程时什么也不做 + } + catch (Exception ex) + { + ModBase.WriteIni(Path.Combine(path, "PCL.ini"), "InstanceCache", ""); // 要求下次重新加载 + ModBase.Log(ex, "加载 .minecraft 实例列表失败", ModBase.LogLevel.Feedback); + } + } + + // 获取实例列表 + private static Dictionary> InitMcInstanceListWithCache(string path) + { + var results = new Dictionary>(); + try + { + var cardCount = Conversions.ToInteger(ModBase.ReadIni(path + "PCL.ini", "CardCount", (-1).ToString())); + if (cardCount == -1) + return null; + for (int i = 0, loopTo = cardCount - 1; i <= loopTo; i++) + { + var cardType = + (McInstanceCardType)Conversions.ToInteger(ModBase.ReadIni(path + "PCL.ini", "CardKey" + (i + 1), + ":")); + var instanceList = new List(); + + // 循环读取实例 + foreach (var folder in ModBase.ReadIni(path + "PCL.ini", "CardValue" + (i + 1), ":").Split(":")) + { + if (string.IsNullOrEmpty(folder)) + continue; + var versionFolder = $@"{path}versions\{folder}\"; + if (File.Exists(versionFolder + ".pclignore")) + { + if (_isFirstMcInstanceListLoad) + { + ModBase.Log("[Minecraft] 清理残留的忽略项目:" + versionFolder); // #2781 + File.Delete(versionFolder + ".pclignore"); + } + else + { + ModBase.Log("[Minecraft] 跳过要求忽略的项目:" + versionFolder); + continue; + } + } + + try + { + // 读取单个实例 + var instance = new McInstance(versionFolder); + instanceList.Add(instance); + instance.Desc = Config.Instance.CustomInfo[instance.PathInstance]; + + var instanceCfg = Config.Instance; + if (string.IsNullOrEmpty(instance.Desc)) + instance.Desc = instanceCfg.Info[instance.PathInstance]; + if (!instanceCfg.LogoPathConfig.IsDefault(instance.PathInstance)) + instance.Logo = instanceCfg.LogoPath[instance.PathInstance]; + if (!instanceCfg.ReleaseTimeConfig.IsDefault(instance.PathInstance)) + instance.ReleaseTime = (dynamic)instanceCfg.ReleaseTime[instance.PathInstance]; + if (!instanceCfg.StateConfig.IsDefault(instance.PathInstance)) + instance.State = + (McInstanceState)Conversions.ToInteger(instanceCfg.State[instance.PathInstance]); + instance.IsStar = instanceCfg.Starred[instance.PathInstance]; + instance.DisplayType = + (McInstanceCardType)Conversions.ToInteger(instanceCfg.CardType[instance.PathInstance]); + if (instance.State != McInstanceState.Error && + !instanceCfg.VanillaVersionNameConfig.IsDefault(instance.PathInstance) && + !instanceCfg.VanillaVersionConfig + .IsDefault(instance.PathInstance)) // 旧版本可能没有这一项,导致 Instance 不加载(#643) + { + var instanceInfo = new McInstanceInfo + { + Fabric = instanceCfg.FabricVersion[instance.PathInstance], + LegacyFabric = instanceCfg.LegacyFabricVersion[instance.PathInstance], + Quilt = instanceCfg.QuiltVersion[instance.PathInstance], + Forge = instanceCfg.ForgeVersion[instance.PathInstance], + LabyMod = instanceCfg.LabyModVersion[instance.PathInstance], + NeoForge = instanceCfg.NeoForgeVersion[instance.PathInstance], + Cleanroom = instanceCfg.CleanroomVersion[instance.PathInstance], + OptiFine = instanceCfg.OptiFineVersion[instance.PathInstance], + HasLiteLoader = instanceCfg.HasLiteLoader[instance.PathInstance], + VanillaName = instanceCfg.VanillaVersionName[instance.PathInstance], + Vanilla = new Version(instanceCfg.VanillaVersion[instance.PathInstance]) + }; + instanceInfo.HasFabric = instanceInfo.Fabric.Any(); + instanceInfo.HasLegacyFabric = instanceInfo.LegacyFabric.Any(); + instanceInfo.HasQuilt = instanceInfo.Quilt.Any(); + instanceInfo.HasForge = instanceInfo.Forge.Any(); + instanceInfo.HasNeoForge = instanceInfo.NeoForge.Any(); + instanceInfo.HasCleanroom = instanceInfo.Cleanroom.Any(); + instanceInfo.HasOptiFine = instanceInfo.OptiFine.Any(); + instance.Info = instanceInfo; + } + + // 重新检查错误实例 + if (instance.State == McInstanceState.Error) + { + // 重新获取实例错误信息 + var OldDesc = instance.Desc; + instance.State = McInstanceState.Original; + instance.Check(); + // 校验错误原因是否改变 + var CustomInfo = Config.Instance.CustomInfo[instance.PathInstance]; + if (instance.State == McInstanceState.Original || (string.IsNullOrEmpty(CustomInfo) && + !((OldDesc ?? "") == + (instance.Desc ?? "")))) + { + ModBase.Log("[Minecraft] 实例 " + instance.Name + " 的错误状态已变更,新的状态为:" + instance.Desc); + return null; + } + } + + // 校验未加载的实例 + if (string.IsNullOrEmpty(instance.Logo)) + { + ModBase.Log("[Minecraft] 实例 " + instance.Name + " 未被加载"); + return null; + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "读取实例加载缓存失败(" + folder + ")"); + return null; + } + } + + if (instanceList.Any()) + results.Add(cardType, instanceList); + } + + return results; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取实例缓存失败"); + return null; + } + } + + private static Dictionary> InitMcInstanceListWithoutCache(string path) + { + var instanceList = new List(); + + #region 循环加载每个实例的信息 + + foreach (var folder in new DirectoryInfo(path + "versions").GetDirectories()) + { + if (!folder.Exists || !folder.EnumerateFiles().Any()) + { + ModBase.Log("[Minecraft] 跳过空文件夹:" + folder.FullName); + continue; + } + + if ((folder.Name == "cache" || folder.Name == "BLClient" || folder.Name == "PCL") && + !File.Exists(folder.FullName + @"\" + folder.Name + ".json")) + { + ModBase.Log("[Minecraft] 跳过可能不是实例文件夹的项目:" + folder.FullName); + continue; + } + + var instanceFolder = folder.FullName + @"\"; + if (File.Exists(instanceFolder + ".pclignore")) + { + if (_isFirstMcInstanceListLoad) + { + ModBase.Log("[Minecraft] 清理残留的忽略项目:" + instanceFolder); // #2781 + try + { + File.Delete(instanceFolder + ".pclignore"); + } + catch (Exception ex) + { + ModBase.Log(ex, "清理残留的忽略项目失败(" + instanceFolder + ")", ModBase.LogLevel.Hint); + } + } + else + { + ModBase.Log("[Minecraft] 跳过要求忽略的项目:" + instanceFolder); + continue; + } + } + + var instance = new McInstance(instanceFolder); + instanceList.Add(instance); + instance.Load(); + } + + #endregion + + var results = new Dictionary>(); + + #region 将实例分类到各个卡片 + + try + { + // 未经过自定义的实例列表 + var instanceListOriginal = new Dictionary>(); + + // 单独列出收藏的实例 + var staredInstances = new List(); + foreach (var instance in instanceList.ToList()) + { + if (!instance.IsStar) + continue; + if (instance.DisplayType == McInstanceCardType.Hidden) + continue; + staredInstances.Add(instance); + instanceList.Remove(instance); + } + + if (staredInstances.Any()) + instanceListOriginal.Add(McInstanceCardType.Star, staredInstances); + + // 预先筛选出愚人节和错误的实例 + McInstanceFilter(ref instanceList, ref instanceListOriginal, new[] { McInstanceState.Error }, + McInstanceCardType.Error); + McInstanceFilter(ref instanceList, ref instanceListOriginal, new[] { McInstanceState.Fool }, + McInstanceCardType.Fool); + + // 筛选 API 实例 + McInstanceFilter(ref instanceList, ref instanceListOriginal, + new[] + { + McInstanceState.Forge, McInstanceState.NeoForge, McInstanceState.LiteLoader, McInstanceState.Fabric, + McInstanceState.LegacyFabric, McInstanceState.Quilt, McInstanceState.Cleanroom, + McInstanceState.LabyMod + }, McInstanceCardType.API); + + // 将老实例预先分类入不常用,只剩余原版、快照、OptiFine + var instanceUseful = new List(); + var instanceRubbish = new List(); + McInstanceFilter(ref instanceList, new[] { McInstanceState.Old }, ref instanceRubbish); + + // 确认最新实例,若为快照则加入常用列表 + var latestInstance = instanceList + .Where(v => v.State == McInstanceState.Original || v.State == McInstanceState.Snapshot) + .MaxOrDefault(v => v.ReleaseTime); + if (latestInstance is not null && latestInstance.State == McInstanceState.Snapshot) + { + instanceUseful.Add(latestInstance); + instanceList.Remove(latestInstance); + } + + // 将剩余的快照全部拖进不常用列表 + McInstanceFilter(ref instanceList, new[] { McInstanceState.Snapshot }, ref instanceRubbish); + + // 获取每个 Drop 下最新的原版与 OptiFine + var newerInstance = new Dictionary(); + var existDrops = new List(); + foreach (var instance in instanceList) + { + if (!instance.Info.Valid) + continue; + if (!existDrops.Contains(instance.Info.Drop)) + existDrops.Add(instance.Info.Drop); + var key = instance.Info.Drop + "-" + (int)instance.State; + if (!newerInstance.ContainsKey(key)) + { + newerInstance.Add(key, instance); + continue; + } + + if (instance.Info.HasOptiFine) + { + if (instance.Info.OptiFineCode > newerInstance[key].Info.OptiFineCode) + newerInstance[key] = instance; // OptiFine 根据版本号判断 + } + else if (instance.ReleaseTime > newerInstance[key].ReleaseTime) + { + newerInstance[key] = instance; // 原版根据发布时间判断 + } + } + + // 将每个 Drop 下的最常规版本加入 + foreach (var drop in existDrops) + if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.OptiFine) && + newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.Original)) + { + // 同时存在 OptiFine 与原版 + var vanillaInstance = newerInstance[drop + "-" + (int)McInstanceState.Original]; + var optiFineInstance = newerInstance[drop + "-" + (int)McInstanceState.OptiFine]; + if (vanillaInstance.Info.Drop > optiFineInstance.Info.Drop) + { + // 仅在原版比 OptiFine 更新时才加入原版 + instanceUseful.Add(vanillaInstance); + instanceList.Remove(vanillaInstance); + } + + instanceUseful.Add(optiFineInstance); + instanceList.Remove(optiFineInstance); + } + else if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.OptiFine)) + { + // 没有原版,直接加入 OptiFine + instanceUseful.Add(newerInstance[drop + "-" + (int)McInstanceState.OptiFine]); + instanceList.Remove(newerInstance[drop + "-" + (int)McInstanceState.OptiFine]); + } + else if (newerInstance.ContainsKey(drop + "-" + (int)McInstanceState.Original)) + { + // 没有 OptiFine,直接加入原版 + instanceUseful.Add(newerInstance[drop + "-" + (int)McInstanceState.Original]); + instanceList.Remove(newerInstance[drop + "-" + (int)McInstanceState.Original]); + } + + // 将剩余的东西添加进去 + instanceRubbish.AddRange(instanceList); + if (instanceUseful.Any()) + instanceListOriginal.Add(McInstanceCardType.OriginalLike, instanceUseful); + if (instanceRubbish.Any()) + instanceListOriginal.Add(McInstanceCardType.Rubbish, instanceRubbish); + + // 按照自定义实例分类重新添加 + foreach (var instancePair in instanceListOriginal) + foreach (var instance in instancePair.Value) + { + var realType = instance.DisplayType == 0 || instancePair.Key == McInstanceCardType.Star + ? instancePair.Key + : instance.DisplayType; + if (!results.ContainsKey(realType)) + results.Add(realType, new List()); + results[realType].Add(instance); + } + } + + catch (Exception ex) + { + results.Clear(); + ModBase.Log(ex, "分类实例列表失败", ModBase.LogLevel.Feedback); + } + + #endregion + + #region 对卡片与实例进行排序 + + // 卡片排序 + var sortedInstanceList = new Dictionary>(); + foreach (var sortRule in new[] + { + McInstanceCardType.Star, McInstanceCardType.API, McInstanceCardType.OriginalLike, + McInstanceCardType.Rubbish, McInstanceCardType.Fool, McInstanceCardType.Error, + McInstanceCardType.Hidden + }) + if (results.ContainsKey((McInstanceCardType)Conversions.ToInteger(sortRule))) + sortedInstanceList.Add((McInstanceCardType)Conversions.ToInteger(sortRule), + results[(McInstanceCardType)Conversions.ToInteger(sortRule)]); + results = sortedInstanceList; + + // 版本排序 + foreach (var cardType in new[] + { + McInstanceCardType.Star, McInstanceCardType.API, McInstanceCardType.OriginalLike, + McInstanceCardType.Rubbish, McInstanceCardType.Fool + }) + { + if (!results.ContainsKey(cardType)) + continue; + + int getComponentCode(McInstance instance) + { + if (instance.Info.ForgelikeCode > 0) + return instance.Info.ForgelikeCode; + if (instance.Info.HasOptiFine) + return instance.Info.OptiFineCode; + return 0; + } + + ; + results[cardType] = SortUtils.Sort(results[cardType], (left, right) => + { + // 发布时间 + if ((left.ReleaseTime.Year >= 2000 || right.ReleaseTime.Year >= 2000) && + left.ReleaseTime != right.ReleaseTime) + return left.ReleaseTime > right.ReleaseTime; + // 附加组件种类 + if (left.Info.HasFabric != right.Info.HasFabric) + return left.Info.HasFabric; + if (left.Info.HasQuilt != right.Info.HasQuilt) + return left.Info.HasQuilt; + if (left.Info.HasLegacyFabric != right.Info.HasLegacyFabric) + return left.Info.HasLegacyFabric; + if (left.Info.HasNeoForge != right.Info.HasNeoForge) + return left.Info.HasNeoForge; + if (left.Info.HasForge != right.Info.HasForge) + return left.Info.HasForge; + if (left.Info.HasCleanroom != right.Info.HasCleanroom) + return left.Info.HasCleanroom; + if (left.Info.HasLabyMod != right.Info.HasLabyMod) + return left.Info.HasLabyMod; + if (left.Info.HasOptiFine != right.Info.HasOptiFine) + return left.Info.HasOptiFine; + if (left.Info.HasLiteLoader != right.Info.HasLiteLoader) + return left.Info.HasLiteLoader; + // 附加组件版本 + if (getComponentCode(left) != getComponentCode(right)) + return getComponentCode(left) > getComponentCode(right); + // 名称 + return Operators.CompareString(left.Name, right.Name, false) > 0; + }); + } + + #endregion + + #region 保存卡片缓存 + + ModBase.WriteIni(path + "PCL.ini", "CardCount", results.Count.ToString()); + for (int i = 0, loopTo = results.Count - 1; i <= loopTo; i++) + { + ModBase.WriteIni(path + "PCL.ini", "CardKey" + (i + 1), + ((int)results.Keys.ElementAtOrDefault(i)).ToString()); + var Value = ""; + foreach (var Instance in results.Values.ElementAtOrDefault(i)) + Value += Instance.Name + ":"; + ModBase.WriteIni(path + "PCL.ini", "CardValue" + (i + 1), Value); + } + + #endregion + + return results; + } + + /// + /// 筛选特定种类的实例,并直接添加为卡片。 + /// + /// 用于筛选的列表。 + /// 需要筛选出的实例类型。-2 代表隐藏的实例。 + /// 卡片的名称。 + private static void McInstanceFilter(ref List instanceList, + ref Dictionary> target, McInstanceState[] formula, + McInstanceCardType cardType) + { + var keepList = instanceList.Where(v => formula.Contains(v.State)).ToList(); + // 加入实例列表,并从剩余中删除 + if (keepList.Any()) + { + target.Add(cardType, keepList); + instanceList = instanceList.Except(keepList).ToList(); + } + } + + /// + /// 筛选特定种类的实例,并增加入一个已有列表中。 + /// + /// 用于筛选的列表。 + /// 需要筛选出的实例类型。-2 代表隐藏的实例。 + /// 传入需要增加入的列表。 + private static void McInstanceFilter(ref List instanceList, McInstanceState[] formula, + ref List keepList) + { + keepList.AddRange(instanceList.Where(v => formula.Contains(v.State))); + // 加入实例列表,并从剩余中删除 + if (keepList.Any()) instanceList = instanceList.Except(keepList).ToList(); + } + + public enum McInstanceCardType + { + Star = -1, + Auto = 0, // 仅用于强制实例分类的自动 + Hidden = 1, + API = 2, + OriginalLike = 3, + Rubbish = 4, + Fool = 5, + Error = 6 + } + + #endregion + + #region 皮肤 + + public struct McSkinInfo + { + public bool IsSlim; + public string LocalFile; + public bool IsVaild; + } + + /// + /// 要求玩家选择一个皮肤文件,并进行相关校验。 + /// + public static McSkinInfo McSkinSelect() + { + var FileName = SystemDialogs.SelectFile("皮肤文件(*.png;*.jpg;*.webp)|*.png;*.jpg;*.webp", "选择皮肤文件"); + + // 验证有效性 + if (string.IsNullOrEmpty(FileName)) + return new McSkinInfo { IsVaild = false }; + try + { + var Image = new MyBitmap(FileName); + if (Image.Picture.Width != 64 || !(Image.Picture.Height == 32 || Image.Picture.Height == 64)) + { + ModMain.Hint("皮肤图片大小应为 64x32 像素或 64x64 像素!", ModMain.HintType.Critical); + return new McSkinInfo { IsVaild = false }; + } + + var FileInfo = new FileInfo(FileName); + if (FileInfo.Length > 24 * 1024) + { + ModMain.Hint("皮肤文件大小需小于 24 KB,而所选文件大小为 " + Math.Round(FileInfo.Length / 1024d, 2) + " KB", + ModMain.HintType.Critical); + return new McSkinInfo { IsVaild = false }; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "皮肤文件存在错误", ModBase.LogLevel.Hint); + return new McSkinInfo { IsVaild = false }; + } + + // 获取皮肤种类 + var IsSlim = ModMain.MyMsgBox("此皮肤为 Steve 模型(粗手臂)还是 Alex 模型(细手臂)?", "选择皮肤种类", "Steve 模型", "Alex 模型", "我不知道", + HighLight: false); + if (IsSlim == 3) + { + ModMain.Hint("请在皮肤下载页面确认皮肤种类后再使用此皮肤!"); + return new McSkinInfo { IsVaild = false }; + } + + return new McSkinInfo { IsVaild = true, IsSlim = IsSlim == 2, LocalFile = FileName }; + } + + /// + /// 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。 + /// + public static string McSkinGetAddress(string uuid, string type) + { + if (string.IsNullOrEmpty(uuid)) + throw new Exception("Uuid 为空。"); + + if (uuid.StartsWith("00000")) + throw new Exception("离线 Uuid 无正版皮肤文件。"); + + // 尝试读取缓存 + var cachePath = Path.Combine(ModBase.PathTemp, $"Cache\\Skin\\Index{type}.ini"); + var cacheSkinAddress = ModBase.ReadIni(cachePath, uuid); + if (!string.IsNullOrEmpty(cacheSkinAddress)) + return cacheSkinAddress; + + // 获取皮肤地址 + var url = type switch + { + "Mojang" => "https://sessionserver.mojang.com/session/minecraft/profile/", + "Ms" => "https://sessionserver.mojang.com/session/minecraft/profile/", + "Auth" => ModProfile.SelectedProfile.Server.Replace("/authserver", "") + + "/sessionserver/session/minecraft/profile/", + _ => throw new ArgumentException($"皮肤地址种类无效:{type ?? "null"}") + }; + + var skinString = ModNet.NetGetCodeByRequestRetry(url + uuid); + if (string.IsNullOrEmpty((string?)skinString)) + throw new Exception("皮肤返回值为空,可能是未设置自定义皮肤的用户"); + + // 解析皮肤 Property + string skinValue = null; + try + { + var json = (JObject)ModBase.GetJson((string)skinString); + foreach (var property in json["properties"]) + if (property["name"]?.ToString() == "textures") + { + skinValue = property["value"]?.ToString(); + break; + } + + if (skinValue == null) + throw new Exception("未从皮肤返回值中找到符合条件的 Property"); + } + catch (Exception ex) + { + ModBase.Log(ex, + $"无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:{skinString}", + ModBase.LogLevel.Developer); + throw new Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex); + } + + // 解码 Base64 并解析 JSON + var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(skinValue)); + var skinJson = (JObject)ModBase.GetJson(decoded.ToLowerInvariant()); + + if (skinJson["textures"]?["skin"]?["url"] == null) + throw new Exception("用户未设置自定义皮肤"); + + var skinUrl = skinJson["textures"]["skin"]["url"].ToString(); + skinUrl = skinUrl.Contains("minecraft.net/") ? skinUrl.Replace("http://", "https://") : skinUrl; + + // 保存缓存 + ModBase.WriteIni(cachePath, uuid, skinUrl); + ModBase.Log($"[Skin] UUID {uuid} 对应的皮肤文件为 {skinUrl}"); + + return skinUrl; + } + + private static readonly object McSkinDownloadLock = new(); + + /// + /// 从 Url 下载皮肤。返回本地文件路径,失败将抛出异常。 + /// + public static string McSkinDownload(string Address) + { + var SkinName = ModBase.GetFileNameFromPath(Address); + var FileAddress = ModBase.PathTemp + @"Cache\Skin\" + ModBase.GetHash(Address) + ".png"; + lock (McSkinDownloadLock) + { + if (!File.Exists(FileAddress)) + { + ModNet.NetDownloadByClient(Address, FileAddress + ModNet.NetDownloadEnd).GetAwaiter().GetResult(); + File.Delete(FileAddress); + FileSystem.Rename(FileAddress + ModNet.NetDownloadEnd, FileAddress); + ModBase.Log("[Minecraft] 皮肤下载成功:" + FileAddress); + } + + return FileAddress; + } + } + + /// + /// 获取 Uuid 对应的皮肤,返回“Steve”或“Alex”。 + /// + public static string McSkinSex(string Uuid) + { + if (!(Uuid.Length == 32)) + return "Steve"; + var a = int.Parse(Conversions.ToString(Uuid[7]), NumberStyles.AllowHexSpecifier); + var b = int.Parse(Conversions.ToString(Uuid[15]), NumberStyles.AllowHexSpecifier); + var c = int.Parse(Conversions.ToString(Uuid[23]), NumberStyles.AllowHexSpecifier); + var d = int.Parse(Conversions.ToString(Uuid[31]), NumberStyles.AllowHexSpecifier); + return Conversions.ToBoolean((a ^ b ^ c ^ d) % 2) ? "Alex" : "Steve"; + // Math.floorMod(uuid.hashCode(), 18) + + // Public Function hashCode(ByVal str As String) As Integer + // Dim hash As Integer = 0 + // Dim n As Integer = str.Length + // If n = 0 Then + // Return hash + // End If + // For i As Integer = 0 To n - 1 + // hash = hash + Asc(str(i)) * (1 << (n - i - 1)) + // Next + // Return hash + // End Function + } + + #endregion + + #region 支持库文件(Libraries) + + public class McLibToken + { + private string _Url; + + /// + /// 是否为纯本地文件,若是则不尝试联网下载。 + /// + public bool IsLocal; + + /// + /// 是否为 Natives 文件。 + /// + public bool IsNatives; + + /// + /// 文件的完整本地路径。 + /// + public string LocalPath; + + /// + /// 原 JSON 中的 Name 项。 + /// + public string OriginalName; + + /// + /// 文件的 SHA1。 + /// + public string SHA1; + + /// + /// 文件大小。若无有效数据即为 0。 + /// + public long Size; + + /// + /// 由 JSON 提供的 URL,若没有则为 Nothing。 + /// + public string Url + { + get => _Url; + set => + // 孤儿 Forge 作者喜欢把没有 URL 的写个空字符串 + _Url = string.IsNullOrWhiteSpace(value) ? null : value; + } + + /// + /// 原 JSON 中 Name 项除去版本号部分的较前部分。可能为 Nothing。 + /// + public string Name + { + get + { + if (OriginalName is null) + return null; + var Splited = new List(OriginalName.Split(":")); + Splited.RemoveAt(2); // Java 的此格式下版本号固定为第三段,第四段可能包含架构、分包等其他信息 + return Splited.Join(":"); + } + } + + public override string ToString() + { + return (IsNatives ? "[Native] " : "") + ModBase.GetString(Size) + " | " + LocalPath; + } + } + + /// + /// 检查是否符合 JSON 中的 Rules。 + /// + /// JSON 中的 "rules" 项目。 + public static bool McJsonRuleCheck(JToken RuleToken) + { + if (RuleToken is null) + return true; + + // 初始化 + var Required = false; + foreach (var Rule in RuleToken) + { + // 单条条件验证 + var IsRightRule = true; // 是否为正确的规则 + if (Rule["os"] is not null) // 操作系统 + { + if (Rule["os"]["name"] is not null) // 操作系统名称 + { + var OsName = Rule["os"]["name"].ToString(); + if (OsName == "unknown") + { + } + else if (OsName == "windows") + { + if (Rule["os"]["version"] is not null) // 操作系统版本 + { + var Cr = Rule["os"]["version"].ToString(); + IsRightRule = IsRightRule && OSVersion.RegexCheck(Cr); + } + } + else + { + IsRightRule = false; + } + } + + if (Rule["os"]["arch"] is not null) // 操作系统架构 + IsRightRule = IsRightRule && Rule["os"]["arch"].ToString() == "x86" == ModBase.Is32BitSystem; + } + + if (!(Rule["features"] == null)) // 标签 + { + IsRightRule = IsRightRule && Rule["features"]["is_demo_user"] == null; // 反选是否为 Demo 用户 + if (((JObject)Rule["features"]).Children().OfType().Any(j => j.Name.Contains("quick_play"))) + IsRightRule = false; // 不开 Quick Play,让玩家自己加去 + } + + // 反选确认 + if (Rule["action"].ToString() == "allow") + { + if (IsRightRule) + Required = true; // allow + } + else if (IsRightRule) + { + Required = false; // disallow + } + } + + return Required; + } + + private static readonly string OSVersion = Environment.OSVersion.Version.ToString(); + + /// + /// 递归获取 Minecraft 某一实例的完整支持库列表。 + /// + public static List McLibListGet(McInstance Instance, bool IncludeInstanceJar) + { + // 获取当前支持库列表 + ModBase.Log("[Minecraft] 获取支持库列表:" + Instance.Name); + var result = McLibListGetWithJson(Instance.JsonObject, TargetInstance: Instance); + + // 需要添加原版 Jar + if (IncludeInstanceJar) + { + McInstance RealInstance; + var RequiredJar = Instance.JsonObject["jar"]?.ToString(); + if (Instance.IsHmclFormatJson || RequiredJar is null) + { + // HMCL 项直接使用自身的 Jar + // 根据 Inherit 获取最深层实例 + var OriginalInstance = Instance; + // 1.17+ 的 Forge 不寻找 Inherit + if (!((Instance.Info.HasForge || Instance.Info.HasNeoForge) && Instance.Info.Drop >= 170)) + while (!string.IsNullOrEmpty(OriginalInstance.InheritInstanceName)) + { + if ((OriginalInstance.InheritInstanceName ?? "") == (OriginalInstance.Name ?? "")) + break; + OriginalInstance = new McInstance(McFolderSelected + @"versions\" + + OriginalInstance.InheritInstanceName + @"\"); + } + + // 需要新建对象,否则后面的 Check 会导致 McInstanceCurrent 的 State 变回 Original + // 复现:启动一个 Snapshot 实例 + RealInstance = new McInstance(OriginalInstance.PathInstance); + } + else + { + // Json 已提供 Jar 字段,使用该字段的信息 + RealInstance = new McInstance(RequiredJar); + } + + string ClientUrl; + string ClientSHA1; + // 判断需求的实例是否存在 + // 不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在 + if (!File.Exists(RealInstance.PathInstance + RealInstance.Name + ".json")) + { + RealInstance = Instance; + ModBase.Log("[Minecraft] 可能缺少前置实例 " + RealInstance.Name + ",找不到对应的 JSON 文件", ModBase.LogLevel.Debug); + } + + // 获取详细下载信息 + if (RealInstance.JsonObject["downloads"] is not null && + RealInstance.JsonObject["downloads"]["client"] is not null) + { + ClientUrl = (string)RealInstance.JsonObject["downloads"]["client"]["url"]; + ClientSHA1 = (string)RealInstance.JsonObject["downloads"]["client"]["sha1"]; + } + else + { + ClientUrl = null; + ClientSHA1 = null; + } + + // 把所需的原版 Jar 添加进去 + result.Add(new McLibToken + { + LocalPath = RealInstance.PathInstance + RealInstance.Name + ".jar", + Size = 0L, + IsNatives = false, + Url = ClientUrl, + SHA1 = ClientSHA1 + }); + } + + return result; + } + + /// + /// 获取 Minecraft 某一实例忽视继承的支持库列表,即结果中没有继承项。 + /// + public static List McLibListGetWithJson(JObject JsonObject, + bool KeepSameNameDifferentVersionResult = false, string CustomMcFolder = null, McInstance TargetInstance = null) + { + CustomMcFolder = CustomMcFolder ?? McFolderSelected; + var BasicArray = new List(); + + // 添加基础 Json 项 + var AllLibs = (JArray)JsonObject["libraries"]; + + // 转换为 LibToken + foreach (JObject Library in AllLibs.Children()) + { + // 清理 null 项(BakaXL 会把没有的项序列化为 null,但会被 Newtonsoft 转换为 JValue,导致 Is Nothing = false;这导致了 #409) + for (var i = Library.Properties().Count() - 1; i >= 0; i -= 1) + if (Library.Properties().ElementAtOrDefault(i).Value.Type == JTokenType.Null) + Library.Remove(Library.Properties().ElementAtOrDefault(i).Name); + + // 检查是否需要(Rules) + if (!McJsonRuleCheck(Library["rules"])) + continue; + + // 获取根节点下的 url + var RootUrl = (string)Library["url"]; + if (RootUrl is not null) + RootUrl += McLibGet((string)Library["name"], false, true, CustomMcFolder).Replace(@"\", "/"); + + // 是否为纯本地项 + var Hint = (string)Library["hint"]; + var IsLocal = Hint is not null ? Hint == "local" : false; + + // 根据是否本地化处理(Natives) + if (Library["natives"] is null) // 没有 Natives + { + string LocalPath; + if (IsLocal && TargetInstance is not null) // 纯本地项 + LocalPath = TargetInstance.PathInstance + @"libraries\" + + Library["name"].ToString().AfterFirst(":").Replace(":", "-") + ".jar"; + else + LocalPath = McLibGet((string)Library["name"], customMcFolder: CustomMcFolder); + try + { + if (Library["downloads"] is not null && Library["downloads"]["artifact"] is not null) + { + var init = new McLibToken(); + BasicArray.Add((init.OriginalName = (string)Library["name"], + init.Url = (string)(RootUrl ?? Library["downloads"]["artifact"]["url"]), + init.LocalPath = Library["downloads"]["artifact"]["path"] is null + ? McLibGet((string)Library["name"], customMcFolder: CustomMcFolder) + : CustomMcFolder + @"libraries\" + Library["downloads"]["artifact"]["path"].ToString() + .Replace("/", @"\"), + init.Size = (long)Math.Round( + ModBase.Val(Library["downloads"]["artifact"]["size"].ToString())), + init.IsNatives = false, init.SHA1 = Library["downloads"]["artifact"]["sha1"]?.ToString(), + init.IsLocal = IsLocal, init).init); + } + else + { + BasicArray.Add(new McLibToken + { + OriginalName = (string)Library["name"], + Url = RootUrl, + LocalPath = LocalPath, + Size = 0L, + IsNatives = false, + SHA1 = null, + IsLocal = IsLocal + }); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "处理实际支持库列表失败(无 Natives," + (Library["name"] ?? "Nothing") + ")"); + BasicArray.Add(new McLibToken + { + OriginalName = (string)Library["name"], + Url = RootUrl, + LocalPath = LocalPath, + Size = 0L, + IsNatives = false, + SHA1 = null + }); + } + } + else if (Library["natives"]["windows"] is not null) // 有 Windows Natives + { + try + { + if (Library["downloads"] is not null && Library["downloads"]["classifiers"] is not null && + Library["downloads"]["classifiers"]["natives-windows"] is not null) + BasicArray.Add(new McLibToken + { + OriginalName = (string)Library["name"], + Url = (string)(RootUrl ?? Library["downloads"]["classifiers"]["natives-windows"]["url"]), + LocalPath = Library["downloads"]["classifiers"]["natives-windows"]["path"] is null + ? McLibGet((string)Library["name"], customMcFolder: CustomMcFolder) + .Replace(".jar", "-" + Library["natives"]["windows"] + ".jar") + .Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32") + : CustomMcFolder + @"libraries\" + + Library["downloads"]["classifiers"]["natives-windows"]["path"].ToString() + .Replace("/", @"\"), + Size = (long)Math.Round( + ModBase.Val(Library["downloads"]["classifiers"]["natives-windows"]["size"].ToString())), + IsNatives = true, + SHA1 = Library["downloads"]["classifiers"]["natives-windows"]["sha1"].ToString(), + IsLocal = IsLocal + }); + else + BasicArray.Add(new McLibToken + { + OriginalName = (string)Library["name"], + Url = RootUrl, + LocalPath = McLibGet((string)Library["name"], customMcFolder: CustomMcFolder) + .Replace(".jar", "-" + Library["natives"]["windows"] + ".jar") + .Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32"), + Size = 0L, + IsNatives = true, + SHA1 = null, + IsLocal = IsLocal + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "处理实际支持库列表失败(有 Natives," + (Library["name"] ?? "Nothing") + ")"); + BasicArray.Add(new McLibToken + { + OriginalName = (string)Library["name"], + Url = RootUrl, + LocalPath = McLibGet((string)Library["name"], customMcFolder: CustomMcFolder) + .Replace(".jar", "-" + Library["natives"]["windows"] + ".jar") + .Replace("${arch}", Environment.Is64BitOperatingSystem ? "64" : "32"), + Size = 0L, + IsNatives = true, + SHA1 = null, + IsLocal = false + }); + } + } + } + + // 去重 + var ResultArray = new Dictionary(); + + // 测试例: + // D:\Minecraft\test\libraries\net\neoforged\mergetool\2.0.0\mergetool-2.0.0-api.jar + // D:\Minecraft\test\libraries\org\apache\commons\commons-collections4\4.2\commons-collections4-4.2.jar + // D:\Minecraft\test\libraries\com\google\guava\guava\31.1-jre\guava-31.1-jre.jar + string GetVersion(McLibToken Token) + { + return ModBase.GetFolderNameFromPath(ModBase.GetPathFromFullPath(Token.LocalPath)); + } + + for (int i = 0, loopTo = BasicArray.Count - 1; i <= loopTo; i++) + { + var Key = BasicArray[i].Name + BasicArray[i].IsNatives; + if (ResultArray.ContainsKey(Key)) + { + var BasicArrayVersion = GetVersion(BasicArray[i]); + var ResultArrayVersion = GetVersion(ResultArray[Key]); + if ((BasicArrayVersion ?? "") != (ResultArrayVersion ?? "") && KeepSameNameDifferentVersionResult) + { + ModBase.Log( + $"[Minecraft] 发现疑似重复的支持库:{BasicArray[i]} ({BasicArrayVersion}) 与 {ResultArray[Key]} ({ResultArrayVersion})"); + ResultArray.Add(Key + ModBase.GetUuid(), BasicArray[i]); + } + else + { + ModBase.Log( + $"[Minecraft] 发现重复的支持库:{BasicArray[i]} ({BasicArrayVersion}) 与 {ResultArray[Key]} ({ResultArrayVersion}),已忽略其中之一"); + if (CompareVersionGe(BasicArrayVersion, ResultArrayVersion)) ResultArray[Key] = BasicArray[i]; + } + } + else + { + ResultArray.Add(Key, BasicArray[i]); + } + } + + return ResultArray.Values.ToList(); + } + + /// + /// 获取实例所需支持库文件的 NetFile。 + /// + public static List McLibNetFilesFromInstance(McInstance instance) + { + if (!instance.IsLoaded) + instance.Load(); + var result = new List(); + + // 更新此方法时需要同步更新 Forge 新版自动安装方法! + + // 主 Jar 文件 + try + { + var mainJar = ModDownload.DlClientJarGet(instance, true); + if (mainJar is not null) + result.Add(mainJar); + } + catch (Exception ex) + { + ModBase.Log(ex, "实例缺失主 Jar 文件所必须的信息", ModBase.LogLevel.Developer); + } + + // Library 文件 + result.AddRange(McLibNetFilesFromTokens(McLibListGet(instance, false))); + + // Authlib-Injector 文件 + var authlibTargetFile = ModBase.PathPure + @"\authlib-injector.jar"; + JObject authlibDownloadInfo = null; + try + { + ModBase.Log("[Minecraft] 开始获取 Authlib-Injector 下载信息"); + authlibDownloadInfo = (JObject)ModBase.GetJson(ModNet.NetGetCodeByLoader( + new[] + { + "https://authlib-injector.yushi.moe/artifact/latest.json", + "https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json" + }, IsJson: true)); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 Authlib-Injector 下载信息失败"); + } + + // 校验文件 + if (authlibDownloadInfo is not null) + { + var checker = new ModBase.FileChecker(Hash: authlibDownloadInfo["checksums"]["sha256"].ToString()); + if (checker.Check(authlibTargetFile) is not null) + { + // 开始下载 + var downloadAddress = authlibDownloadInfo["download_url"].ToString() + .Replace("bmclapi2.bangbang93.com/mirrors/authlib-injector", "authlib-injector.yushi.moe"); + ModBase.Log("[Minecraft] Authlib-Injector 需要更新:" + downloadAddress, ModBase.LogLevel.Developer); + result.Add(new ModNet.NetFile( + new[] + { + downloadAddress, + downloadAddress.Replace("authlib-injector.yushi.moe", + "bmclapi2.bangbang93.com/mirrors/authlib-injector") + }, authlibTargetFile, + new ModBase.FileChecker(Hash: authlibDownloadInfo["checksums"]["sha256"].ToString()))); + } + } + + // 修改渲染器 + var mesaLoaderWindowsVersion = "25.3.5"; + var mesaLoaderWindowsTargetFile = + ModBase.PathPure + @"\mesa-loader-windows\" + mesaLoaderWindowsVersion + @"\Loader.jar"; + var renderer = -1; + if (McInstanceSelected is not null) + renderer = Conversions.ToInteger( + Operators.SubtractObject(ModBase.Setup.Get("VersionAdvanceRenderer", McInstanceSelected), 1)); + if (renderer == -1) renderer = Conversions.ToInteger(Config.Launch.Renderer); + + if (renderer != 0 && !File.Exists(mesaLoaderWindowsTargetFile)) + { + var downloadAddress = + "https://mirrors.cloud.tencent.com/nexus/repository/maven-public/org/glavo/mesa-loader-windows/" + + mesaLoaderWindowsVersion + "/mesa-loader-windows-" + mesaLoaderWindowsVersion + "-" + + (ModBase.Is32BitSystem ? "x86" : ModBase.IsArm64System ? "arm64" : "x64") + ".jar"; + result.Add(new ModNet.NetFile(new[] { downloadAddress }, mesaLoaderWindowsTargetFile)); + } + + // LabyMod Assets 文件 + if (instance.Info.HasLabyMod) + { + if ((instance.PathIndie ?? "") == (instance.PathInstance ?? "")) + { + if (Directory.Exists(instance.PathInstance + "labymod-neo")) + Directory.Delete(instance.PathInstance + "labymod-neo", true); + ModBase.CreateSymbolicLink(instance.PathInstance + "labymod-neo", McFolderSelected + "labymod-neo", + 0x2); + } + + try + { + var channelType = instance.JsonObject["labymod_data"]["channelType"].ToString(); + Directory.CreateDirectory($@"{McFolderSelected}labymod-neo\libraries"); + ModBase.Log("[Minecraft] 开始获取 LabyMod 信息"); + var labyManifest = (JObject)ModNet.NetGetCodeByRequestRetry( + $"https://releases.r2.labymod.net/api/v1/manifest/{channelType}/latest.json", IsJson: true); + var labyAssets = (JObject)labyManifest["assets"]; + var labyModCommitRef = labyManifest["commitReference"].ToString(); + foreach (var Asset in labyAssets) + { + var assetName = Asset.Key; + var assetSHA1 = Asset.Value.ToString(); + var assetPath = $@"{McFolderSelected}labymod-neo\assets\{assetName}.jar"; + var assetUrl = + $"https://releases.r2.labymod.net/api/v1/download/assets/labymod4/{channelType}/{labyModCommitRef}/{assetName}/{assetSHA1}.jar"; + var checker = new ModBase.FileChecker(Hash: assetSHA1); + if (checker.Check(assetPath) is null) + continue; + result.Add(new ModNet.NetFile(new[] { assetUrl }, assetPath, checker)); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 LabyMod 信息失败,跳过检查"); + } + } + + // 跳过校验 + if (Conversions.ToBoolean(ShouldIgnoreFileCheck(instance))) + { + ModBase.Log("[Minecraft] 用户要求尽量忽略文件检查,这可能会保留有误的文件"); + result = result.Where(f => + { + if (File.Exists(f.LocalPath)) + { + ModBase.Log("[Minecraft] 跳过下载的支持库文件:" + f.LocalPath, ModBase.LogLevel.Debug); + return false; + } + + return true; + }).ToList(); + } + + return result; + } + + /// + /// 将 McLibToken 列表转换为 NetFile。 + /// + public static List McLibNetFilesFromTokens(List libs, string customMcFolder = null) + { + customMcFolder = customMcFolder ?? McFolderSelected; + var result = new List(); + // 获取 + foreach (var token in libs) + { + // 检查文件 + var checker = new ModBase.FileChecker(ActualSize: token.Size == 0L ? -1 : token.Size, Hash: token.SHA1); + if (checker.Check(token.LocalPath) is null) + continue; + if (token.IsLocal) + { + ModBase.Log("[Download] 已跳过被标记为本地文件的支持库: " + token.OriginalName); + continue; + } + + // URL + var urls = new List(); + if (token.Url is null && token.Name == "net.minecraftforge:forge:universal") + // 特判修复 Forge 部分 universal 文件缺失 URL(#5455) + token.Url = "https://maven.minecraftforge.net" + + token.LocalPath.Replace(customMcFolder + "libraries", "").Replace(@"\", "/"); + if (token.Url is not null) + { + // 获取 URL 的真实地址 + urls.Add(token.Url); + if (token.Url.Contains("launcher.mojang.com/v1/objects") || token.Url.Contains("client.txt") || + token.Url.Contains(".tsrg")) + urls.AddRange(ModDownload.DlSourceLauncherOrMetaGet(token.Url)); // Mappings(#4425) + if (token.Url.Contains("maven")) + { + var bmclapiUrl = token.Url + .Replace(Strings.Mid(token.Url, 1, token.Url.IndexOfF("maven")), + "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven") + .Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven"); + if (ModDownload.DlSourcePreferMojang) + urls.Add(bmclapiUrl); // 官方源优先 + else + urls.Insert(0, bmclapiUrl); // 镜像源优先 + } + } + + if (token.LocalPath.Contains("transformer-discovery-service")) + { + // Transformer 文件释放 + if (!File.Exists(token.LocalPath)) + ModBase.WriteFile(token.LocalPath, ModBase.GetResourceStream("Resources/transformer.jar")); + ModBase.Log("[Download] 已自动释放 Transformer Discovery Service", ModBase.LogLevel.Developer); + continue; + } + + if (token.LocalPath.Contains(@"optifine\OptiFine")) + { + // OptiFine 主 Jar + var optiFineBase = + token.LocalPath.Replace(customMcFolder + @"libraries\optifine\OptiFine\", "").Split("_")[0] + "/" + + ModBase.GetFileNameFromPath(token.LocalPath).Replace("-", "_"); + optiFineBase = "/maven/com/optifine/" + optiFineBase; + if (optiFineBase.Contains("_pre")) + optiFineBase = optiFineBase.Replace("com/optifine/", "com/optifine/preview_"); + urls.Add("https://bmclapi2.bangbang93.com" + optiFineBase); + } + else if (token.Name.Contains("LabyMod")) + { + // LabyMod 只有一个下载源 + urls.Add(token.Url); + ModBase.Log( + $"[Download] 获取到 LabyMod 主要库文件的 Size = {token.Size},SHA1 = {token.SHA1},由于 LabyMod 乱写 Size,已忽略 Size"); + checker = new ModBase.FileChecker(Hash: token.SHA1); // 只校验 SHA1 + } + else if (urls.Count <= 2) + { + // 普通文件 + urls.AddRange(ModDownload.DlSourceLibraryGet("https://libraries.minecraft.net" + + token.LocalPath.Replace(customMcFolder + "libraries", "") + .Replace(@"\", "/"))); + } + + result.Add(new ModNet.NetFile(urls.Distinct(), token.LocalPath, checker)); + } + + // 去重并返回 + return result.Distinct((a, b) => (a.LocalPath ?? "") == (b.LocalPath ?? "")); + } + + /// + /// 获取对应的支持库文件地址。 + /// + /// 原始地址,如 com.mumfrey:liteloader:1.12.2-SNAPSHOT。 + /// 是否包含 Lib 文件夹头部,若不包含,则会类似以 com\xxx\ 开头。 + public static string McLibGet(string original, bool withHead = true, bool ignoreLiteLoader = false, + string customMcFolder = null) + { + string McLibGetRet = default; + customMcFolder = customMcFolder ?? McFolderSelected; + var splited = original.Split(":"); + McLibGetRet = (withHead ? customMcFolder + @"libraries\" : "") + splited[0].Replace(".", @"\") + @"\" + + splited[1] + @"\" + splited[2] + @"\" + splited[1] + "-" + splited[2] + ".jar"; + // 判断 OptiFine 是否应该使用 installer + if (McLibGetRet.Contains(@"optifine\OptiFine\1.") && splited[2].Split(".").Count() > 1) + { + var majorVersion = (int)Math.Round(ModBase.Val(splited[2].Split(".")[1].BeforeFirst("_"))); + var minorVersion = (int)Math.Round(splited[2].Split(".").Count() > 2 + ? ModBase.Val(splited[2].Split(".")[2].BeforeFirst("_")) + : 0d); + if ((majorVersion == 12 || (majorVersion == 20 && minorVersion >= 4) || majorVersion >= 21) && File.Exists( + $@"{customMcFolder}libraries\{splited[0].Replace(".", @"\")}\{splited[1]}\{splited[2]}\{splited[1]}-{splited[2]}-installer.jar")) // 仅在 1.12 (无法追溯) 和 1.20.4+ (#5376) 遇到此问题 + { + ModLaunch.McLaunchLog("已将 " + original + " 替换为对应的 Installer 文件"); + McLibGetRet = McLibGetRet.Replace(".jar", "-installer.jar"); + } + } + + return McLibGetRet; + } + + /// + /// 检查设置,是否应当忽略文件检查? + /// + public static object ShouldIgnoreFileCheck(McInstance Version) + { + return (bool)ModBase.Setup.Get("VersionAdvanceAssetsV2", Version) || + Operators.ConditionalCompareObjectEqual(ModBase.Setup.Get("VersionAdvanceAssets", Version), 2, false); + } + + #endregion + + #region 资源文件(Assets) + + // 获取索引 + /// + /// 获取某实例资源文件索引的对应 Json 项,详见实例 Json 中的 assetIndex 项。失败会抛出异常。 + /// + public static JToken McAssetsGetIndex(McInstance instance, bool returnLegacyOnError = false, + bool checkURLEmpty = false) + { + string assetsName; + try + { + while (true) + { + var index = instance.JsonObject["assetIndex"]; + if (index is not null && index["id"] is not null) + return index; + if (instance.JsonObject["assets"] is not null) + assetsName = instance.JsonObject["assets"].ToString(); + if (checkURLEmpty && index["url"] is not null) + return index; + // 下一个实例 + if (string.IsNullOrEmpty(instance.InheritInstanceName)) + break; + instance = new McInstance(McFolderSelected + @"versions\" + instance.InheritInstanceName); + } + } + catch + { + } + + // 无法获取到下载地址 + if (returnLegacyOnError) + { + // 返回 assets 文件名会由于没有下载地址导致全局失败 + // If AssetsName IsNot Nothing AndAlso AssetsName <> "legacy" Then + // Log("[Minecraft] 无法获取资源文件索引下载地址,使用 assets 项提供的资源文件名:" & AssetsName) + // Return GetJson("{""id"": """ & AssetsName & """}") + // Else + ModBase.Log("[Minecraft] 无法获取资源文件索引下载地址,使用默认的 legacy 下载地址"); + return (JToken)ModBase.GetJson(@"{ + ""id"": ""legacy"", + ""sha1"": ""c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729"", + ""size"": 134284, + ""url"": ""https://launchermeta.mojang.com/mc-staging/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"", + ""totalSize"": 111220701 + }"); + } + // End If + + throw new Exception("该实例不存在资源文件索引信息"); + } + + /// + /// 获取某实例资源文件索引名,优先使用 assetIndex,其次使用 assets。失败会返回 legacy。 + /// + public static string McAssetsGetIndexName(McInstance instance) + { + try + { + while (true) + { + if (instance.JsonObject["assetIndex"] is not null && + instance.JsonObject["assetIndex"]["id"] is not null) + return instance.JsonObject["assetIndex"]["id"].ToString(); + if (instance.JsonObject["assets"] is not null) return instance.JsonObject["assets"].ToString(); + if (string.IsNullOrEmpty(instance.InheritInstanceName)) + break; + instance = new McInstance(McFolderSelected + @"versions\" + instance.InheritInstanceName); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取资源文件索引名失败"); + } + + return "legacy"; + } + + // 获取列表 + private struct McAssetsToken + { + /// + /// 文件的完整本地路径。 + /// + public string LocalPath; + + /// + /// Json 中书写的源路径。例如 minecraft/sounds/mob/stray/death2.ogg 。 + /// + public string SourcePath; + + /// + /// 文件大小。若无有效数据即为 0。 + /// + public long Size; + + /// + /// 文件的 Hash 校验码。 + /// + public string Hash; + + public override string ToString() + { + return ModBase.GetString(Size) + " | " + LocalPath; + } + } + + /// + /// 获取 Minecraft 的资源文件列表。失败会抛出异常。 + /// + private static List McAssetsListGet(McInstance instance) + { + var indexName = McAssetsGetIndexName(instance); + try + { + // 初始化 + if (!File.Exists($@"{McFolderSelected}assets\indexes\{indexName}.json")) + throw new FileNotFoundException("未找到 Asset Index", + McFolderSelected + @"assets\indexes\" + indexName + ".json"); + var result = new List(); + var json = (JsonObject)JsonNode.Parse( + ModBase.ReadFile($@"{McFolderSelected}assets\indexes\{indexName}.json")); + + // 读取列表 + foreach (var file in json["objects"].AsObject()) + { + string localPath; + if (json["map_to_resources"] is not null && json["map_to_resources"].GetValue()) + // Remap + localPath = instance.PathIndie + @"resources\" + file.Key.Replace("/", @"\"); + else if (json["virtual"] is not null && json["virtual"].GetValue()) + // Virtual + localPath = McFolderSelected + @"assets\virtual\legacy\" + file.Key.Replace("/", @"\"); + else + // 正常 + localPath = McFolderSelected + @"assets\objects\" + Strings.Left(file.Value["hash"].ToString(), 2) + + @"\" + file.Value["hash"]; + result.Add(new McAssetsToken + { + LocalPath = localPath, + SourcePath = file.Key, + Hash = file.Value["hash"].ToString(), + Size = Conversions.ToLong(file.Value["size"].ToString()) + }); + } + + return result; + } + + catch (Exception ex) + { + ModBase.Log(ex, "获取资源文件列表失败:" + indexName); + throw; + } + } + + // 获取缺失列表 + /// + /// 获取实例缺失的资源文件所对应的 NetTaskFile。 + /// + public static List McAssetsFixList(McInstance instance, bool checkHash, + [Optional] ref ModLoader.LoaderBase progressFeed) + { + // 如果需要检查 Hash,则留到下载时处理,以借助多线程加快检查速度 + if (checkHash) + return McAssetsListGet(instance).Select(token => new ModNet.NetFile( + ModDownload.DlSourceAssetsGet( + $"https://resources.download.minecraft.net/{Strings.Left(token.Hash, 2)}/{token.Hash}"), + token.LocalPath, + new ModBase.FileChecker(ActualSize: token.Size == 0L ? -1 : token.Size, Hash: token.Hash))).ToList(); + // 如果不检查 Hash,则立即处理 + var result = new List(); + + List assetsList; + try + { + assetsList = McAssetsListGet(instance); + McAssetsToken token; + if (progressFeed is not null) + progressFeed.Progress = 0.04d; + for (int i = 0, loopTo = assetsList.Count - 1; i <= loopTo; i++) + { + // 初始化 + token = assetsList[i]; + if (progressFeed is not null) + progressFeed.Progress = 0.05d + 0.94d * i / assetsList.Count; + // 检查文件是否存在 + var file = new FileInfo(token.LocalPath); + if (file.Exists && (token.Size == 0L || token.Size == file.Length)) + continue; + // 文件不存在,添加下载 + result.Add(new ModNet.NetFile( + ModDownload.DlSourceAssetsGet( + $"https://resources.download.minecraft.net/{Strings.Left(token.Hash, 2)}/{token.Hash}"), + token.LocalPath, + new ModBase.FileChecker(ActualSize: token.Size == 0L ? -1 : token.Size, Hash: token.Hash))); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取实例缺失的资源文件下载列表失败"); + } + + if (progressFeed is not null) + progressFeed.Progress = 0.99d; + return result; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs new file mode 100644 index 000000000..92d2eb172 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -0,0 +1,1695 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.UI; +using static PCL.ModLoader; + +namespace PCL; + +public static class ModModpack +{ + // 触发整合包安装的外部接口 + /// + /// 弹窗要求选择一个整合包文件并进行安装。 + /// + public static void ModpackInstall() + { + var File = SystemDialogs.SelectFile("整合包文件(*.rar;*.zip;*.mrpack)|*.rar;*.zip;*.mrpack", "选择整合包压缩文件"); // 选择整合包文件 + if (string.IsNullOrEmpty(File)) + return; + ModBase.RunInThread(() => + { + try + { + ModpackInstall(File); + } + catch (ModBase.CancelledException ex) + { + } + catch (Exception ex) + { + ModBase.Log(ex, "手动安装整合包失败", ModBase.LogLevel.Msgbox); + } + }); + } + + /// + /// 构建并启动安装给定的整合包文件的加载器,并返回该加载器。若失败则抛出异常。 + /// 必须在工作线程执行。 + /// + /// + public static LoaderCombo ModpackInstall(string File, string InstanceName = null, string Logo = null, + string resourceId = null, bool isOnlineInstall = false) + { + ModBase.Log("[ModPack] 整合包安装请求:" + (File ?? "null")); + ZipArchive Archive = null; + var ArchiveBaseFolder = ""; + try + { + // 字符校验 + var TargetFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\"; + if (TargetFolder.Contains("!") || TargetFolder.Contains(";")) + { + ModMain.Hint("游戏路径中不能含有感叹号或分号:" + TargetFolder, ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 获取整合包种类与关键 Json + var PackType = -1; + do + { + try + { + Archive = new ZipArchive(new FileStream(File, FileMode.Open, FileAccess.Read, FileShare.Read)); + if (Archive.Entries.Any(e => e.IsEncrypted)) + throw new Exception("PCL 无法处理加密的压缩包,请在解压后重新压缩为不加密的 zip 格式再试"); + // 从根目录判断整合包类型 + if (Archive.GetEntry("mcbbs.packmeta") is not null) + { + PackType = 3; + break; + } // MCBBS 整合包(优先于 manifest.json 判断) + + if (Archive.GetEntry("mmc-pack.json") is not null) + { + PackType = 2; + break; + } // MMC 整合包(优先于 manifest.json 判断,#4194) + + if (Archive.GetEntry("modrinth.index.json") is not null) + { + PackType = 4; + break; + } // Modrinth 整合包 + + if (Archive.GetEntry("manifest.json") is not null) + { + var Json = (JObject)ModBase.GetJson(ModBase.ReadFile(Archive.GetEntry("manifest.json").Open(), + Encoding.UTF8)); + if (Json["addons"] is null) + { + PackType = 0; + break; // CurseForge 整合包 + } + + PackType = 3; + break; + // MCBBS 整合包 + } + + if (Archive.GetEntry("modpack.json") is not null) + { + PackType = 1; + break; + } // HMCL 整合包 + + if (Archive.GetEntry("modpack.zip") is not null || Archive.GetEntry("modpack.mrpack") is not null) + { + PackType = 9; + break; + } // 带启动器的压缩包 + + // 从一级目录判断整合包类型 + var exitTry = false; + foreach (var Entry in Archive.Entries) + { + var FullNames = Entry.FullName.Split("/"); + ArchiveBaseFolder = FullNames[0] + "/"; + // 确定为一级目录下 + if (FullNames.Count() != 2) + continue; + // 判断是否为关键文件 + if (FullNames[1] == "mcbbs.packmeta") + { + PackType = 3; + exitTry = true; + break; + } // MCBBS 整合包(优先于 manifest.json 判断) + + if (FullNames[1] == "mmc-pack.json") + { + PackType = 2; + exitTry = true; + break; + } // MMC 整合包(优先于 manifest.json 判断,#4194) + + if (FullNames[1] == "modrinth.index.json") + { + PackType = 4; + exitTry = true; + break; + } // Modrinth 整合包 + + if (FullNames[1] == "manifest.json") + { + var Json = (JObject)ModBase.GetJson(ModBase.ReadFile(Entry.Open(), Encoding.UTF8)); + if (Json["addons"] is null) + { + PackType = 0; + exitTry = true; + break; // CurseForge 整合包 + } + + PackType = 3; + ArchiveBaseFolder = "overrides/"; + exitTry = true; + break; + // MCBBS 整合包 + } + + if (FullNames[1] == "modpack.json") + { + PackType = 1; + exitTry = true; + break; + } // HMCL 整合包 + + if (FullNames[1] == "modpack.zip" || FullNames[1] == "modpack.mrpack") + { + PackType = 9; + exitTry = true; + break; + } // 带启动器的压缩包 + } + + if (exitTry) break; + } + catch (Exception ex) + { + if (ex.Message.Contains("Error.WinIOError")) + throw new Exception("打开整合包文件失败", ex); + else if (File.EndsWithF(".rar", true)) + throw new Exception("PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试", ex); + else + throw new Exception("打开整合包文件失败,文件可能损坏或为不支持的压缩包格式", ex); + } + } while (false); + + // 执行对应的安装方法 + switch (PackType) + { + case 0: + { + ModBase.Log("[ModPack] 整合包种类:CurseForge"); + return InstallPackCurseForge(File, Archive, ArchiveBaseFolder, InstanceName, Logo, resourceId, + isOnlineInstall); + } + case 1: + { + ModBase.Log("[ModPack] 整合包种类:HMCL"); + return InstallPackHMCL(File, Archive, ArchiveBaseFolder); + } + case 2: + { + ModBase.Log("[ModPack] 整合包种类:MMC"); + return InstallPackMMC(File, Archive, ArchiveBaseFolder); + } + case 3: + { + ModBase.Log("[ModPack] 整合包种类:MCBBS"); + return InstallPackMCBBS(File, Archive, ArchiveBaseFolder, InstanceName); + } + case 4: + { + ModBase.Log("[ModPack] 整合包种类:Modrinth"); + return InstallPackModrinth(File, Archive, ArchiveBaseFolder, InstanceName, Logo, resourceId, + isOnlineInstall); + } + case 9: + { + ModBase.Log("[ModPack] 整合包种类:带启动器的压缩包"); + return InstallPackLauncherPack(File, Archive, ArchiveBaseFolder); + } + + default: + { + ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包"); + return InstallPackCompress(File, Archive); + } + } + } + finally + { + if (Archive is not null) + Archive.Dispose(); + } + } + + private static void ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader, + double progressIncrement) + { + // 解压文件 + var retryCount = 1; + var encode = Encoding.GetEncoding("GB18030"); + var initialProgress = loader.Progress; + + while (retryCount <= 5) + try + { + loader.Progress = initialProgress; + + // 删除旧目录 + ModBase.DeleteDirectory(installTemp); + + // 解压文件,ProgressIncrementHandler 通过 Lambda 更新进度 + ModBase.ExtractFile(fileAddress, installTemp, encode, + delta => loader.Progress += delta * progressIncrement); + + // 解压成功,更新进度并退出循环 + loader.Progress = initialProgress + progressIncrement; + return; + } + catch (Exception ex) + { + ModBase.Log(ex, $"第 {retryCount} 次解压尝试失败"); + + if (ex is ArgumentException || ex is IOException) + { + encode = Encoding.UTF8; + ModBase.Log("[ModPack] 已切换压缩包解压编码为 UTF8"); + } + + // 检查加载器状态,决定是否中止 + if (loader is not null && loader.LoadingState != MyLoading.MyLoadingState.Run) + return; + + // 增加重试次数 + retryCount++; + + if (retryCount <= 5) + // 等待一段时间再重试 + Thread.Sleep((retryCount - 1) * 2000); + else + throw new Exception("解压整合包文件失败", ex); + } + } + + /// + /// 从整合包的 override 目录复制文件,同时设置 PCL 的配置文件与版本隔离。 + /// 对路径末尾是否为 \ 没有要求。 + /// + private static void CopyOverrideDirectory(string OverridesFolder, string VersionFolder, LoaderBase Loader, + double ProgressIncrement) + { + if (!OverridesFolder.EndsWithF(@"\")) + OverridesFolder += @"\"; + if (!VersionFolder.EndsWithF(@"\")) + VersionFolder += @"\"; + // 复制文件 + if (Directory.Exists(OverridesFolder)) + { + ModBase.Log($"[ModPack] 处理整合包覆写文件夹:{OverridesFolder} → {VersionFolder}"); + ModBase.CopyDirectory(OverridesFolder, VersionFolder, + Delta => Loader.Progress += Delta * ProgressIncrement); + } + else + { + ModBase.Log($"[ModPack] 整合包中没有覆写文件夹:{OverridesFolder}"); + Loader.Progress += ProgressIncrement; + } + + // 设置 ini + var OverridesIni = $@"{OverridesFolder}PCL\Setup.ini"; + var VersionIni = $@"{VersionFolder}PCL\Setup.ini"; + if (File.Exists(OverridesIni)) + { + ModBase.WriteIni(OverridesIni, "VersionArgumentIndie", 1.ToString()); // 开启版本隔离 + ModBase.WriteIni(OverridesIni, "VersionArgumentIndieV2", Conversions.ToString(true)); + ModBase.CopyFile(OverridesIni, VersionIni); // 覆写已有的 ini + } + else + { + ModBase.WriteIni(VersionIni, "VersionArgumentIndie", 1.ToString()); // 开启版本隔离 + ModBase.WriteIni(VersionIni, "VersionArgumentIndieV2", Conversions.ToString(true)); + } + + ModBase.IniClearCache(VersionIni); // 重置缓存,避免被安装过程中写入的 ini 覆盖 + } + + #region CurseForge + + private static LoaderCombo InstallPackCurseForge(string FileAddress, ZipArchive Archive, + string ArchiveBaseFolder, string InstanceName = null, string Logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JObject Json; + try + { + Json = (JObject)ModBase.GetJson( + ModBase.ReadFile(Archive.GetEntry(ArchiveBaseFolder + "manifest.json").Open())); + } + catch (Exception ex) + { + throw new Exception("CurseForge 整合包安装信息存在问题", ex); + } + + if (Json["minecraft"] is null || Json["minecraft"]["version"] is null) + throw new Exception("CurseForge 整合包未提供 Minecraft 版本信息"); + + // 获取实例名 + if (InstanceName is null) + { + InstanceName = (string)(Json["name"] ?? ""); + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + if (!string.IsNullOrEmpty(Validate.Validate(InstanceName))) + InstanceName = ""; + if (string.IsNullOrEmpty(InstanceName)) + InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", "", new Collection { Validate }); + if (string.IsNullOrEmpty(InstanceName)) + throw new ModBase.CancelledException(); + } + + // 获取 Mod API 版本信息 + string ForgeVersion = null; + string NeoForgeVersion = null; + string FabricVersion = null; + string QuiltVersion = null; + foreach (var Entry in (dynamic)Json["minecraft"]["modLoaders"] ?? Array.Empty()) + { + var Id = (Entry["id"] ?? "").ToString().ToLower(); + if (Id.StartsWithF("forge-")) + { + // Forge 指定 + if (Id.Contains("recommended")) + throw new Exception("该整合包版本过老,已不支持进行安装!"); + ModBase.Log("[ModPack] 整合包 Forge 版本:" + Id); + ForgeVersion = Id.Replace("forge-", ""); + } + else if (Id.StartsWithF("neoforge-")) + { + // NeoForge 指定 + ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + Id); + NeoForgeVersion = Id.Replace("neoforge-", ""); + } + else if (Id.StartsWithF("fabric-")) + { + // Fabric 指定 + try + { + ModBase.Log("[ModPack] 整合包 Fabric 版本:" + Id); + FabricVersion = Id.Replace("fabric-", ""); + break; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取整合包 Fabric 版本失败:" + Id); + } + } + else if (Id.StartsWithF("quilt-")) + { + // Quilt 指定 + try + { + ModBase.Log("[ModPack] 整合包 Quilt 版本:" + Id); + QuiltVersion = Id.Replace("quilt-", ""); + break; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取整合包 Quilt 版本失败:" + Id); + } + } + } + + // 解压 + var InstallTemp = ModMain.RequestTaskTempFolder(); + var InstallLoaders = new List(); + var OverrideHome = (string)(Json["overrides"] ?? ""); + if (!string.IsNullOrEmpty(OverrideHome)) + InstallLoaders.Add(new LoaderTask("解压整合包文件", Task => + { + ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.6d); + CopyOverrideDirectory( + InstallTemp + ArchiveBaseFolder + (OverrideHome == "." || OverrideHome == "./" ? "" : OverrideHome), + $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}", Task, 0.4d); // #5613 + }) + { + ProgressWeight = new FileInfo(FileAddress).Length / 1024d / 1024d / 6d, + Block = false + }); // 每 6M 需要 1s + // 获取 Mod 列表 + var ModList = new List(); + var ModOptionalList = new List(); + foreach (var ModEntry in (dynamic)Json["files"] ?? Array.Empty()) + { + if (ModEntry["projectID"] is null || ModEntry["fileID"] is null) + { + ModMain.Hint("某项 Mod 缺少必要信息,已跳过:" + ModEntry); + continue; + } + + ModList.Add((int)ModEntry["fileID"]); + if (ModEntry["required"] is not null && !ModEntry["required"].ToObject()) + ModOptionalList.Add((int)ModEntry["fileID"]); + } + + if (ModList.Any()) + { + var ModDownloadLoaders = new List(); + // 获取 Mod 下载信息 + ModDownloadLoaders.Add(new LoaderTask("获取 Mod 下载信息", Task => + { + var allowMirror = true; + JArray ret; + var tryCount = 0; + do + { + tryCount += 1; + ret = (JArray)((JObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.curseforge.com/v1/mods/files", + "POST", "{\"fileIds\": [" + ModList.Join(",") + "]}", "application/json", + allowMirror)))["data"]; + if (ModList.Count <= ret.Count) + { + ModBase.Log("[Modpack] 已获取到的模组数量足够,开始进行下一步"); + break; + } + + allowMirror = false; + ModBase.Log($"[Modpack] 获取模组数量不达标,设置镜像源允许状态为: {allowMirror}"); + if (tryCount > 3) throw new Exception("整合包中的部分 Mod 版本已被 Mod 作者删除,所以没法继续安装了,请向整合包作者反馈该问题"); + } while (true); + + Task.Output = ret; + }) + { + ProgressWeight = ModList.Count / 10d + }); // 每 10 Mod 需要 1s + // 构造 NetFile + ModDownloadLoaders.Add(new LoaderTask>("构造 Mod 下载信息", Task => + { + var FileList = new Dictionary(); + foreach (var ModJson in Task.Input) + { + var Id = ModJson["id"].ToObject(); + // 跳过重复的 Mod(疑似 CurseForge Bug) + if (FileList.ContainsKey(Id)) + continue; + // 可选 Mod 提示 + if (ModOptionalList.Contains(Id)) + if (ModMain.MyMsgBox("是否要下载整合包中的可选文件 " + ModJson["displayName"] + "?", "下载可选文件", "是", "否") == 2) + continue; + + // 根据 modules 和文件名后缀判断资源类型 + string TargetFolder; + ModComp.CompType Type; + if (ModJson["modules"].Any()) // modules 可能返回 null(#1006) + { + var ModuleNames = ((JArray)ModJson["modules"]).Select(l => l["name"].ToString()).ToList(); + if (ModuleNames.Contains("META-INF") || ModuleNames.Contains("mcmod.info") || + (ModJson?["FileName"]?.ToString()?.EndsWithF(".jar", true)).GetValueOrDefault()) + { + TargetFolder = "mods"; + Type = ModComp.CompType.Mod; + } + else if (ModuleNames.Contains("pack.mcmeta")) + { + TargetFolder = "resourcepacks"; + Type = ModComp.CompType.ResourcePack; + } + else if (ModuleNames.Contains("level.dat")) + { + TargetFolder = "saves"; + Type = ModComp.CompType.World; + } + else + { + TargetFolder = "shaderpacks"; + Type = ModComp.CompType.Shader; + } + } + else + { + TargetFolder = "mods"; + Type = ModComp.CompType.Mod; + } + + // 建立 CompFile + var File = new ModComp.CompFile((JObject)ModJson, Type); + if (!File.Available) + continue; + // 实际的添加 + FileList.Add(Id, + File.ToNetFile($@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\{TargetFolder}\")); + Task.Progress += 1d / (1 + ModList.Count); + } + + Task.Output = FileList.Values.ToList(); + }) + { + ProgressWeight = ModList.Count / 200d, + Show = false + }); // 每 200 Mod 需要 1s + // 下载 Mod 文件 + ModDownloadLoaders.Add(new ModNet.LoaderDownload("下载 Mod", new List()) + { ProgressWeight = ModList.Count * 1.5d }); // 每个 Mod 需要 1.5s + // 构造加载器 + InstallLoaders.Add(new LoaderCombo("下载 Mod(主加载器)", ModDownloadLoaders) + { Show = false, ProgressWeight = ModDownloadLoaders.Sum(l => l.ProgressWeight) }); + } + + // 构造加载器 + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = InstanceName, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\", + MinecraftName = Json["minecraft"]["version"].ToString(), + ForgeVersion = ForgeVersion, + NeoForgeVersion = NeoForgeVersion, + FabricVersion = FabricVersion, + QuiltVersion = QuiltVersion + }; + var MergeLoaders = ModDownloadLib.McInstallLoader(Request); + // 构造总加载器 + var Loaders = new List(); + Loaders.Add(new LoaderCombo("整合包安装", InstallLoaders) + { Show = false, Block = false, ProgressWeight = InstallLoaders.Sum(l => l.ProgressWeight) }); + Loaders.Add(new LoaderCombo("游戏安装", MergeLoaders) + { Show = false, ProgressWeight = MergeLoaders.Sum(l => l.ProgressWeight) }); + Loaders.Add(new LoaderTask("最终整理文件", Task => + { + // 设置图标 + var VersionFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\"; + if (Logo is not null && File.Exists(Logo)) + { + File.Copy(Logo, VersionFolder + @"PCL\Logo.png", true); + Config.Instance.LogoPath[VersionFolder] = @"PCL\Logo.png"; + Config.Instance.IsLogoCustom[VersionFolder] = true; + ModBase.Log("[ModPack] 已设置整合包 Logo:" + Logo); + } + + // 删除原始整合包文件 + foreach (var Target in new[] { VersionFolder + "原始整合包.zip", VersionFolder + "原始整合包.mrpack" }) + if (File.Exists(Target)) + { + ModBase.Log("[ModPack] 删除原始整合包文件:" + Target); + File.Delete(Target); + } + + if (File.Exists(FileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(FileAddress) == "modpack") + { + ModBase.Log("[ModPack] 删除安装整合包文件:" + FileAddress); + File.Delete(FileAddress); + } + + // 整合包版本 + if (Json["version"] is not null) Config.Instance.ModpackVersion[VersionFolder] = Json["version"].ToString(); + Config.Instance.ModpackSource[VersionFolder] = "CurseForge"; + Config.Instance.ModpackId[VersionFolder] = resourceId; + do + { + try + { + var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]); + if (projects.Count == 0) + break; + Config.Instance.CustomInfo[VersionFolder] = projects.First().Description; + } + catch (Exception ex) + { + ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); + } + } while (false); + }) + { + ProgressWeight = 0.1d, + Show = false + }); + + // 重复任务检查 + var LoaderName = "CurseForge 整合包安装:" + InstanceName + " "; + if (LoaderTaskbar.Any(l => (l.Name ?? "") == (LoaderName ?? ""))) + { + ModMain.Hint("该整合包正在安装中!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 启动 + var Loader = new LoaderCombo(LoaderName, Loaders) { OnStateChanged = ModDownloadLib.McInstallState }; + Loader.Start(Request.TargetInstanceFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + if (!isOnlineInstall) + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager)); + return Loader; + } + + #endregion + + #region Modrinth + + private static LoaderCombo InstallPackModrinth(string FileAddress, ZipArchive Archive, + string ArchiveBaseFolder, string InstanceName = null, string Logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JObject Json; + try + { + Json = (JObject)ModBase.GetJson( + ModBase.ReadFile(Archive.GetEntry(ArchiveBaseFolder + "modrinth.index.json").Open())); + } + catch (Exception ex) + { + throw new Exception("Modrinth 整合包安装信息存在问题", ex); + } + + if (Json["dependencies"] is null || Json["dependencies"]["minecraft"] is null) + throw new Exception("Modrinth 整合包未提供 Minecraft 版本信息"); + // 获取 Mod API 版本信息 + string MinecraftVersion = null; + string ForgeVersion = null; + string NeoForgeVersion = null; + string FabricVersion = null; + string QuiltVersion = null; + foreach (JProperty Entry in (dynamic)Json["dependencies"] ?? Array.Empty()) + switch (Entry.Name.ToLower() ?? "") + { + case "minecraft": + { + MinecraftVersion = Entry.Value.ToString(); + break; + } + case "forge": // eg. 14.23.5.2859 / 1.19-41.1.0 + { + ForgeVersion = Entry.Value.ToString(); + ModBase.Log("[ModPack] 整合包 Forge 版本:" + ForgeVersion); + break; + } + case "neoforge": + case "neo-forge": // eg. 20.6.98-beta + { + NeoForgeVersion = Entry.Value.ToString(); + ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + NeoForgeVersion); + break; + } + case "fabric-loader": // eg. 0.14.14 + { + FabricVersion = Entry.Value.ToString(); + ModBase.Log("[ModPack] 整合包 Fabric 版本:" + FabricVersion); + break; + } + case "quilt-loader": // eg. 0.26.0 + { + QuiltVersion = Entry.Value.ToString(); + ModBase.Log("[ModPack] 整合包 Quilt 版本:" + QuiltVersion); + break; + } + + default: + { + ModMain.Hint($"无法安装整合包,其中出现了未知的 Mod 加载器 {Entry.Name}(版本为 {Entry.Value})!", + ModMain.HintType.Critical); + break; + } + } + + // 获取实例名 + if (InstanceName is null) + { + InstanceName = (string)(Json["name"] ?? ""); + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + if (!string.IsNullOrEmpty(Validate.Validate(InstanceName))) + InstanceName = ""; + if (string.IsNullOrEmpty(InstanceName)) + InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", "", new Collection { Validate }); + if (string.IsNullOrEmpty(InstanceName)) + throw new ModBase.CancelledException(); + } + + // 解压 + var InstallTemp = ModMain.RequestTaskTempFolder(); + var InstallLoaders = new List(); + InstallLoaders.Add(new LoaderTask("解压整合包文件", Task => + { + ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.5d); + CopyOverrideDirectory(InstallTemp + ArchiveBaseFolder + "overrides", + ModMinecraft.McFolderSelected + @"versions\" + InstanceName, Task, 0.4d); + CopyOverrideDirectory(InstallTemp + ArchiveBaseFolder + "client-overrides", + ModMinecraft.McFolderSelected + @"versions\" + InstanceName, Task, 0.1d); + }) + { + ProgressWeight = new FileInfo(FileAddress).Length / 1024d / 1024d / 6d, + Block = false + }); // 每 6M 需要 1s + // 获取下载文件列表 + var FileList = new List(); + foreach (var File in (dynamic)Json["files"] ?? Array.Empty()) + { + // 检查是否需要该文件 + if (File["env"] is not null) + switch (File["env"]["client"].ToString() ?? "") + { + case "optional": + { + if (ModMain.MyMsgBox("是否要下载可选文件 " + ModBase.GetFileNameFromPath(File["path"].ToString()) + "?", + "下载可选文件", "是", "否") == 2) continue; + + break; + } + case "unsupported": + { + continue; + } + } + + // 添加下载文件 + var Urls = ((JArray)File["downloads"]) + .OfType() + .Select(x => ModComp.CompFile.HandleCurseForgeDownloadUrls(x.ToString())) + .ToList(); + // 镜像源 + Urls = Urls.SelectMany(x => ModDownload.DlSourceModDownloadGet(x)).ToList(); + var TargetPath = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\{File["path"]}"; + if (!Path.GetFullPath(TargetPath) + .StartsWithF($@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\", true)) + { + ModMain.MyMsgBox("整合包的文件路径超出了实例文件夹,请向整合包作者反馈此问题!" + "\r\n" + "错误的文件:" + TargetPath, + "文件路径校验失败", IsWarn: true); + throw new ModBase.CancelledException(); + } + + FileList.Add(new ModNet.NetFile(Urls, TargetPath, + new ModBase.FileChecker(ActualSize: File["fileSize"].ToObject(), + Hash: File["hashes"]["sha1"].ToString()), true)); + } + + if (FileList.Any()) + InstallLoaders.Add(new ModNet.LoaderDownload("下载额外文件", FileList) + { ProgressWeight = FileList.Count * 1.5d }); // 每个 Mod 需要 1.5s + + // 构造加载器 + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = InstanceName, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\", + MinecraftName = MinecraftVersion, + ForgeVersion = ForgeVersion, + NeoForgeVersion = NeoForgeVersion, + FabricVersion = FabricVersion, + QuiltVersion = QuiltVersion + }; + var MergeLoaders = ModDownloadLib.McInstallLoader(Request); + // 构造总加载器 + var Loaders = new List(); + Loaders.Add(new LoaderCombo("整合包安装", InstallLoaders) + { Show = false, Block = false, ProgressWeight = InstallLoaders.Sum(l => l.ProgressWeight) }); + Loaders.Add(new LoaderCombo("游戏安装", MergeLoaders) + { Show = false, ProgressWeight = MergeLoaders.Sum(l => l.ProgressWeight) }); + Loaders.Add(new LoaderTask("最终整理文件", Task => + { + // 设置图标 + var VersionFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\"; + if (Logo is not null && File.Exists(Logo)) + { + File.Copy(Logo, VersionFolder + @"PCL\Logo.png", true); + Config.Instance.LogoPath[VersionFolder] = @"PCL\Logo.png"; + Config.Instance.IsLogoCustom[VersionFolder] = true; + ModBase.Log("[ModPack] 已设置整合包 Logo:" + Logo); + } + + // 删除原始整合包文件 + foreach (var Target in new[] { VersionFolder + "原始整合包.zip", VersionFolder + "原始整合包.mrpack" }) + if (File.Exists(Target)) + { + ModBase.Log("[ModPack] 删除原始整合包文件:" + Target); + File.Delete(Target); + } + + if (File.Exists(FileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(FileAddress) == "modpack") + { + ModBase.Log("[ModPack] 删除安装整合包文件:" + FileAddress); + File.Delete(FileAddress); + } + + // 整合包版本 + if (Json["versionId"] is not null) + Config.Instance.ModpackVersion[VersionFolder] = Json["versionId"].ToString(); + Config.Instance.ModpackSource[VersionFolder] = "Modrinth"; + Config.Instance.ModpackId[VersionFolder] = resourceId; + do + { + try + { + var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]); + if (projects.Count == 0) + break; + Config.Instance.CustomInfo[VersionFolder] = projects.First().Description; + } + catch (Exception ex) + { + ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); + } + } while (false); + }) + { + ProgressWeight = 0.1d, + Show = false + }); + + // 重复任务检查 + var LoaderName = $"Modrinth 整合包安装:{InstanceName} "; + if (LoaderTaskbar.Any(l => (l.Name ?? "") == (LoaderName ?? ""))) + { + ModMain.Hint("该整合包正在安装中!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 启动 + var Loader = new LoaderCombo(LoaderName, Loaders) { OnStateChanged = ModDownloadLib.McInstallState }; + Loader.Start(Request.TargetInstanceFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + if (!isOnlineInstall) + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager)); + return Loader; + } + + #endregion + + #region HMCL + + private static LoaderCombo InstallPackHMCL(string FileAddress, ZipArchive Archive, string ArchiveBaseFolder) + { + // 读取 Json 文件 + JObject Json; + try + { + Json = (JObject)ModBase.GetJson( + ModBase.ReadFile(Archive.GetEntry(ArchiveBaseFolder + "modpack.json").Open(), Encoding.UTF8)); + } + catch (Exception ex) + { + throw new Exception("HMCL 整合包安装信息存在问题", ex); + } + + // 获取实例名 + var InstanceName = (string)(Json["name"] ?? ""); + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + if (!string.IsNullOrEmpty(Validate.Validate(InstanceName))) + InstanceName = ""; + if (string.IsNullOrEmpty(InstanceName)) + InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", "", new Collection { Validate }); + if (string.IsNullOrEmpty(InstanceName)) + throw new ModBase.CancelledException(); + // 解压 + var InstallTemp = ModMain.RequestTaskTempFolder(); + var InstallLoaders = new List(); + InstallLoaders.Add(new LoaderTask("解压整合包文件", Task => + { + ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.6d); + CopyOverrideDirectory(InstallTemp + ArchiveBaseFolder + "minecraft", + ModMinecraft.McFolderSelected + @"versions\" + InstanceName, Task, 0.4d); + }) + { + ProgressWeight = new FileInfo(FileAddress).Length / 1024d / 1024d / 6d, + Block = false + }); // 每 6M 需要 1s + // 构造游戏本体安装加载器 + if (Json["gameVersion"] is null) + throw new Exception("该 HMCL 整合包未提供游戏版本信息,无法安装!"); + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = InstanceName, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\", + MinecraftName = Json["gameVersion"].ToString() + }; + var MergeLoaders = ModDownloadLib.McInstallLoader(Request); + // 构造总加载器 + var Loaders = new List + { + new LoaderCombo("整合包安装", InstallLoaders) + { Show = false, Block = false, ProgressWeight = InstallLoaders.Sum(l => l.ProgressWeight) }, + new LoaderCombo("游戏安装", MergeLoaders) + { Show = false, ProgressWeight = MergeLoaders.Sum(l => l.ProgressWeight) } + }; + // 重复任务检查 + var LoaderName = "HMCL 整合包安装:" + InstanceName + " "; + if (LoaderTaskbar.Any(l => (l.Name ?? "") == (LoaderName ?? ""))) + { + ModMain.Hint("该整合包正在安装中!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 启动 + var Loader = new LoaderCombo(LoaderName, Loaders) { OnStateChanged = ModDownloadLib.McInstallState }; + Loader.Start(Request.TargetInstanceFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager)); + return Loader; + } + + #endregion + + #region MCBBS + + private static LoaderCombo InstallPackMCBBS(string FileAddress, ZipArchive Archive, + string ArchiveBaseFolder, string InstanceName = null) + { + // 读取 Json 文件 + JObject Json; + try + { + // VB 的 If(a, b) 在 C# 中如果是 null 合并则用 ??,如果是三元运算则用 ?: + var Entry = Archive.GetEntry(ArchiveBaseFolder + "mcbbs.packmeta") ?? + Archive.GetEntry(ArchiveBaseFolder + "manifest.json"); + using (var stream = Entry.Open()) + { + Json = (JObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8)); + } + } + catch (Exception ex) + { + throw new Exception("MCBBS 整合包安装信息存在问题", ex); + } + + // 获取实例名 + if (InstanceName == null) + { + InstanceName = Json["name"]?.ToString() ?? ""; + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + + if (!string.IsNullOrEmpty(Validate.Validate(InstanceName))) InstanceName = ""; + + if (string.IsNullOrEmpty(InstanceName)) + InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", "", [Validate]); + + if (string.IsNullOrEmpty(InstanceName)) throw new ModBase.CancelledException(); + } + + // 解压与路径准备 + var InstallTemp = ModMain.RequestTaskTempFolder(); + var VersionFolder = $"{ModMinecraft.McFolderSelected}versions\\{InstanceName}"; + var InstallLoaders = new List(); + + // 解压整合包文件任务 + var unzipTask = new LoaderTask("解压整合包文件", Task => + { + ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.6); + CopyOverrideDirectory( + InstallTemp + ArchiveBaseFolder + "overrides", + ModMinecraft.McFolderSelected + "versions\\" + InstanceName, + Task, 0.4); + + // JVM 参数处理 + if (Json["launchInfo"] != null) + { + var LaunchInfo = (JObject)Json["launchInfo"]; + Config.Instance.JvmArgs[VersionFolder] = string.Join(" ", LaunchInfo["javaArgument"]); + Config.Instance.GameArgs[VersionFolder] = string.Join(" ", LaunchInfo["launchArgument"]); + } + + // 整合包版本 + if (Json["version"] != null) Config.Instance.ModpackVersion[VersionFolder] = Json["version"].ToString(); + }); + + unzipTask.ProgressWeight = new FileInfo(FileAddress).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s + unzipTask.Block = false; + InstallLoaders.Add(unzipTask); + + // 构造加载器 + if (Json["addons"] == null) throw new Exception("该 MCBBS 整合包未提供游戏版本附加信息,无法安装!"); + + var Addons = new Dictionary(); + foreach (var Entry in Json["addons"]) Addons.Add(Entry["id"].ToString(), Entry["version"].ToString()); + + if (!Addons.ContainsKey("game")) + { + ModMain.Hint("该整合包未提供游戏版本信息,无法安装!", ModMain.HintType.Critical); + return null; + } + + // 构造安装请求 + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = InstanceName, + TargetInstanceFolder = $"{ModMinecraft.McFolderSelected}versions\\{InstanceName}\\", + MinecraftName = Addons["game"], + OptiFineVersion = Addons.ContainsKey("optifine") ? Addons["optifine"] : null, + ForgeVersion = Addons.ContainsKey("forge") ? Addons["forge"] : null, + NeoForgeVersion = Addons.ContainsKey("neoforge") ? Addons["neoforge"] : null, + FabricVersion = Addons.ContainsKey("fabric") ? Addons["fabric"] : null, + QuiltVersion = Addons.ContainsKey("quilt") ? Addons["quilt"] : null + }; + + var MergeLoaders = ModDownloadLib.McInstallLoader(Request); + + // 构造总加载器 + var Loaders = new List(); + Loaders.Add(new LoaderCombo("整合包安装", InstallLoaders) + { + Show = false, + Block = false, + ProgressWeight = InstallLoaders.Sum(l => l.ProgressWeight) + }); + Loaders.Add(new LoaderCombo("游戏安装", MergeLoaders) + { + Show = false, + ProgressWeight = MergeLoaders.Sum(l => l.ProgressWeight) + }); + + // 重复任务检查 + var LoaderName = "MCBBS 整合包安装:" + InstanceName + " "; + if (LoaderTaskbar.Any(l => l.Name == LoaderName)) + { + ModMain.Hint("该整合包正在安装中!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 启动任务 + var Loader = new LoaderCombo(LoaderName, Loaders); + Loader.OnStateChanged = ModDownloadLib.McInstallState; + + Loader.Start(Request.TargetInstanceFolder); + LoaderTaskbarAdd(Loader); + + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager)); + + return Loader; + } + + #endregion + + #region 带启动器的压缩包 + + private static LoaderCombo InstallPackLauncherPack(string FileAddress, ZipArchive Archive, + string ArchiveBaseFolder) + { + // 获取解压路径 + ModMain.MyMsgBox("接下来请选择一个空文件夹,它会被安装到这个文件夹里。", "安装", "继续", ForceWait: true); + var TargetFolder = SystemDialogs.SelectFolder("选择安装目标(必须是一个空文件夹)"); + if (string.IsNullOrEmpty(TargetFolder)) + throw new ModBase.CancelledException(); + if (Directory.GetFileSystemEntries(TargetFolder).Length > 0) + { + ModMain.Hint("请选择一个空文件夹作为安装目标!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 解压 + var Loader = new LoaderCombo("解压压缩包", new[] + { + new LoaderTask("解压压缩包", Task => + { + ExtractModpackFiles(TargetFolder, FileAddress, Task, 0.9d); + Thread.Sleep(400); // 避免文件争用 + // 查找解压后的 exe 文件 + string Launcher = null; + foreach (var ExeFile in Directory.GetFiles(TargetFolder, "*.exe", SearchOption.TopDirectoryOnly)) + { + var Info = FileVersionInfo.GetVersionInfo(ExeFile); + ModBase.Log($"[Modpack] 文件 {ExeFile} 的产品名标识为 {Info.ProductName}"); + if (Info.ProductName == "Plain Craft Launcher") + { + Launcher = ExeFile; + ModBase.Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}"); + } + else if ((Info.ProductName.ContainsF("Launcher", true) || Info.ProductName.ContainsF("启动", true)) && + !(Info.ProductName == "Plain Craft Launcher Admin Manager")) + { + if (Launcher is null) + { + Launcher = ExeFile; + ModBase.Log($"[Modpack] 发现整合包附带的疑似第三方启动器:{ExeFile}"); + } + } + } + + Task.Progress = 0.95d; + // 尝试使用附带的启动器打开 + if (Launcher is not null) + { + ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + Launcher); + if (ModMain.MyMsgBox($"整合包里似乎自带了启动器,是否换用它继续安装?{"\r\n"}即将打开:{Launcher}", "换用整合包启动器?", "换用", + "不换用") == 1) + { + ModBase.OpenExplorer(TargetFolder); + ModBase.ShellOnly(Launcher, "--wait"); // 要求等待已有的 PCL 退出 + ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序"); + ModMain.FrmMain.EndProgram(false); + return; + } + } + else + { + ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); + } + + ModBase.OpenExplorer(TargetFolder); + // 加入文件夹列表 + var InstanceName = ModBase.GetFolderNameFromPath(TargetFolder); + Directory.CreateDirectory(TargetFolder + @".minecraft\"); + PageSelectLeft.AddFolder( + TargetFolder + @".minecraft\" + ArchiveBaseFolder.Replace("/", @"\").TrimStart('\\'), InstanceName, + false); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) + // 调用 modpack 文件进行安装 + var ModpackFile = Directory.GetFiles(TargetFolder, "modpack.*", SearchOption.AllDirectories).First(); + ModBase.Log("[Modpack] 调用 modpack 文件继续安装:" + ModpackFile); + ModpackInstall(ModpackFile); + }) + }); + Loader.Start(TargetFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + return Loader; + } + + #endregion + + #region 普通压缩包 + + private static LoaderCombo InstallPackCompress(string FileAddress, ZipArchive Archive) + { + // 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径 + Match Match = null; + var Regex = new Regex(@"^.*\/(?=versions\/(?[^\/]+)\/(\k)\.json$)", RegexOptions.IgnoreCase); + foreach (var Entry in Archive.Entries) + { + var EntryMatch = Regex.Match("/" + Entry.FullName); + if (EntryMatch.Success) + { + Match = EntryMatch; + break; + } + } + + if (Match is null) + throw new Exception("未能找到适合的文件结构,这可能不是一个 MC 压缩包"); // 没有匹配 + var ArchiveBaseFolder = Match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) + var InstanceName = Match.Groups[1].Value; + ModBase.Log("[ModPack] 检测到压缩包的 .minecraft 根目录:" + ArchiveBaseFolder + ",命中的实例名:" + InstanceName); + // 获取解压路径 + ModMain.MyMsgBox("接下来请选择一个空文件夹,它会被安装到这个文件夹里。", "安装", "继续", ForceWait: true); + var TargetFolder = SystemDialogs.SelectFolder("选择安装目标(必须是一个空文件夹)"); + if (string.IsNullOrEmpty(TargetFolder)) + throw new ModBase.CancelledException(); + if (TargetFolder.Contains("!") || TargetFolder.Contains(";")) + { + ModMain.Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + if (Directory.GetFileSystemEntries(TargetFolder).Length > 0) + { + ModMain.Hint("请选择一个空文件夹作为安装目标!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 解压 + var Loader = new LoaderCombo("解压压缩包", new[] + { + new LoaderTask("解压压缩包", Task => + { + ExtractModpackFiles(TargetFolder, FileAddress, Task, 0.95d); + // 加入文件夹列表 + PageSelectLeft.AddFolder(TargetFolder + ArchiveBaseFolder, ModBase.GetFolderNameFromPath(TargetFolder), + false); + Thread.Sleep(400); // 避免文件争用 + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSelect)); + }) + }) + { + OnStateChanged = ModDownloadLib.McInstallState + }; + Loader.Start(TargetFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + return Loader; + } + + #endregion + + #region MultiMC + + public class MMCPackInfo + { + public JObject AdditionalJson = new(); + public bool IsCleanroomOverrided; + public bool IsFabricOverrided; + public bool IsForgeOverrided; + public bool IsMcArgsEdited; + public bool IsMinecraftOverrided; + public bool IsNeoForgeOverrided; + public bool IsQuiltOverrided; + public JArray JvmArgs = new(); + public JArray Libraries = new(); + public JObject OverridedJson = new(); + public string Tweakers = null; + } + + private static LoaderCombo InstallPackMMC(string FileAddress, ZipArchive Archive, string ArchiveBaseFolder) + { + // 读取 Json 文件 + JObject PackJson; + string PackInstance; + MMCPackInfo PackInfo = null; + try + { + PackJson = (JObject)ModBase.GetJson( + ModBase.ReadFile(Archive.GetEntry(ArchiveBaseFolder + "mmc-pack.json").Open(), Encoding.UTF8)); + PackInstance = ModBase.ReadFile(Archive.GetEntry(ArchiveBaseFolder + "instance.cfg").Open(), Encoding.UTF8); + + #region JSON Patches + + // 参考 https://github.com/MultiMC/Launcher/wiki/JSON-Patches + do + { + try + { + if (!Archive.Entries.Any(e => + e.FullName.Equals(ArchiveBaseFolder + "patches/", StringComparison.OrdinalIgnoreCase))) + break; + ModBase.Log("[ModPack] 安装的 MultiMC 整合包存在 JSON Patches"); + // 排序预处理 + var Patches = new List>(); + foreach (var entry in Archive.Entries) + if (!entry.FullName.EndsWith("/") && entry.FullName.StartsWith(ArchiveBaseFolder + "patches/")) + { + var Patch = (JObject)ModBase.GetJson(ModBase.ReadFile( + Archive.GetEntry(ArchiveBaseFolder + "patches/" + entry.Name).Open(), Encoding.UTF8)); + Patches.Add(new KeyValuePair(Patch, + (int)(Patch["order"] is not null ? Patch["order"] : 0))); + } + + var Components = (JArray)PackJson["components"]; + foreach (var Patch in Patches) + { + // 检查 Patch 是否在 mmc-pack.json 中 + var IsContainedInPackJson = false; + foreach (var Component in Components) + if ((Component["uid"].ToString() ?? "") == (Patch.Key["uid"].ToString() ?? "")) + { + IsContainedInPackJson = true; + break; + } + + if (!IsContainedInPackJson) + { + ModBase.Log($"[ModPack] JSON-Patch {Patch.Key["uid"]} 未包含于 mmc-pack.json, 跳过该 Patch"); + Patches.Remove(Patch); + } + } + + Patches.Sort((x, y) => x.Value.CompareTo(y.Value)); + // 应用 Patches + PackInfo = new MMCPackInfo(); + + string Tweakers = null; + JObject AssetIndex = null; + JObject JavaVerJson = null; + string MainClass = null; + var GameArguments = new JArray(); + var JvmArguments = new JArray(); + var LibJson = new JArray(); + var AddLibJson = new JArray(); + foreach (var Patch in Patches) + { + var PatchJson = Patch.Key; + if ((string)PatchJson["uid"] == "net.minecraft") + { + PackInfo.IsMinecraftOverrided = true; + } + else if ((string)PatchJson["uid"] == "net.minecraftforge") + { + if (PatchJson["version"].ToString().StartsWithF("0.")) + PackInfo.IsCleanroomOverrided = true; + else + PackInfo.IsForgeOverrided = true; + } + else if ((string)PatchJson["uid"] == "net.neoforged") + { + PackInfo.IsNeoForgeOverrided = true; + } + else if ((string)PatchJson["uid"] == "net.fabricmc.fabric-loader") + { + PackInfo.IsFabricOverrided = true; + } + else if ((string)PatchJson["uid"] == "org.quiltmc.quilt-loader") + { + PackInfo.IsQuiltOverrided = true; + } + + // JVM 参数 + if (PatchJson["+jvmArgs"] is not null) + { + JvmArguments.Merge(PatchJson["+jvmArgs"]); + ModBase.Log($"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 JVM 参数"); + } + + // Libraries + if (PatchJson["libraries"] is not null || PatchJson["+libraries"] is not null) + { + var Libs = new JArray(); + if (PatchJson["libraries"] is not null) + foreach (var Library in PatchJson["libraries"]) + { + var LibJobj = (JObject)Library; + if (LibJobj["MMC-hint"] is not null) + { + LibJobj.Add("hint", LibJobj["MMC-hint"]); + LibJobj.Remove("MMC-hint"); + } + + Libs.Add(LibJobj); + } + + if (PatchJson["+libraries"] is not null) + foreach (var Library in PatchJson["+libraries"]) // TODO: 此处处理不严谨,但也能用吧 + { + var LibJobj = (JObject)Library; + if (LibJobj["MMC-hint"] is not null) + { + LibJobj.Add("hint", LibJobj["MMC-hint"]); + LibJobj.Remove("MMC-hint"); + } + + Libs.Add(LibJobj); + } + + LibJson.Merge(Libs); + ModBase.Log($"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 Libraries"); + } + + // Tweakers + if (PatchJson["+tweakers"] is not null) + { + Tweakers = (string)PatchJson["+tweakers"][0]; + ModBase.Log($"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 Tweakers"); + } + + // AssetIndex + if (PatchJson["assetIndex"] is not null) + { + AssetIndex = (JObject)PatchJson["assetIndex"]; + ModBase.Log($"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 AssetIndex"); + } + + // minecraftArguments -> arguments.game + if (PatchJson["minecraftArguments"] is not null) + { + foreach (var Arg in PatchJson["minecraftArguments"].ToString().Split(" ")) + GameArguments.Add(Arg); + PackInfo.IsMcArgsEdited = true; + ModBase.Log( + $"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 minecraftArguments 至 arguments.game"); + } + + // mainClass + if (PatchJson["mainClass"] is not null) + { + MainClass = (string)PatchJson["mainClass"]; + ModBase.Log($"[ModPack] 已应用 JSON-Patch {PatchJson["uid"]} 的 mainClass"); + } + + // Java 版本要求 + if (PatchJson["compatibleJavaMajors"] is not null) + { + var JavaVersion = 0; + string JavaComponent = null; + var JavaMajors = (JArray)PatchJson["compatibleJavaMajors"]; + foreach (var Java in JavaMajors) + { + if (JavaVersion > ModBase.Val(Java)) + continue; + // 优先选择主要的版本 + if (ModBase.Val(Java) == 21d) + { + JavaVersion = 21; + JavaComponent = "java-runtime-delta"; + } + else if (ModBase.Val(Java) == 17d) + { + JavaVersion = 17; + JavaComponent = "java-runtime-gamma"; + } + else if (ModBase.Val(Java) == 11d) + { + JavaVersion = 11; + JavaComponent = null; + } + else if (ModBase.Val(Java) == 8d) + { + JavaVersion = 8; + JavaComponent = "jre-legacy"; + } + } + + if (JavaVersion == 0) + { + JavaVersion = (int)JavaMajors[0]; + JavaComponent = null; + } + + JavaVerJson = new JObject { { "majorVersion", JavaVersion } }; + if (JavaComponent is not null) JavaVerJson.Add("component", JavaComponent); + ModBase.Log($"[ModPack] JSON-Patch {PatchJson["uid"]} 要求 Java 版本: " + JavaVersion); + } + } + + JObject JsonArguments = null; + if (!string.IsNullOrWhiteSpace(Tweakers)) + { + GameArguments.Add("--tweakClass"); + GameArguments.Add(Tweakers); + } + + if (GameArguments is not null || JvmArguments is not null) + { + JvmArguments.Insert(0, "-Djava.library.path=${natives_directory}"); + JvmArguments.Insert(1, "-Dminecraft.launcher.brand=${launcher_name}"); + JvmArguments.Insert(2, "-Dminecraft.launcher.version=${launcher_version}"); + JvmArguments.Insert(3, "-cp"); + JvmArguments.Insert(4, "${classpath}"); + JsonArguments = new JObject { { "game", GameArguments }, { "jvm", JvmArguments } }; + } + + PackInfo.OverridedJson = new JObject(); + if (JsonArguments is not null) + PackInfo.OverridedJson.Add("arguments", JsonArguments); + if (MainClass is not null) + PackInfo.OverridedJson.Add("mainClass", MainClass); + if (AssetIndex is not null) + PackInfo.OverridedJson.Add("assetIndex", AssetIndex); + if (JavaVerJson is not null) + PackInfo.OverridedJson.Add("javaVersion", JavaVerJson); + if (LibJson is not null) + PackInfo.OverridedJson.Add("libraries", LibJson); + } + catch (Exception ex) + { + ModBase.Log(ex, "应用 MMC JSON-Patches 失败"); + } + } while (false); + } + + #endregion + + catch (Exception ex) + { + throw new Exception("MMC 整合包安装信息存在问题", ex); + } + + // 获取实例名 + var InstanceName = PackInstance.RegexSeek(@"(?<=\nname\=)[^\n]+") ?? ""; + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + if (!string.IsNullOrEmpty(Validate.Validate(InstanceName))) + InstanceName = ""; + if (string.IsNullOrEmpty(InstanceName)) + InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", "", new Collection { Validate }); + if (string.IsNullOrEmpty(InstanceName)) + throw new ModBase.CancelledException(); + // 解压 + var InstallTemp = ModMain.RequestTaskTempFolder(); + var VersionFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}"; + var InstallLoaders = new List(); + InstallLoaders.Add(new LoaderTask("解压整合包文件", Task => + { + ExtractModpackFiles(InstallTemp, FileAddress, Task, 0.55d); + CopyOverrideDirectory(InstallTemp + ArchiveBaseFolder + "libraries", + ModMinecraft.McFolderSelected + @"versions\" + InstanceName + @"\libraries", Task, 0.2d); + CopyOverrideDirectory(InstallTemp + ArchiveBaseFolder + ".minecraft", + ModMinecraft.McFolderSelected + @"versions\" + InstanceName, Task, 0.2d); + + #region instance.cfg + + // 读取 MMC 设置文件(#2655) + try + { + var MMCSetupFile = InstallTemp + ArchiveBaseFolder + "instance.cfg"; + // 将其中的等号替换为冒号,以符合 ini 文件格式 + if (File.Exists(MMCSetupFile)) + { + List Lines = []; + foreach (var Line in ModBase.ReadFile(MMCSetupFile).Split(new[] { "\r", "\n" }, + StringSplitOptions.RemoveEmptyEntries)) + { + if (!Line.Contains("=")) + continue; + Lines.Add(Line.BeforeFirst("=") + ":" + Line.AfterFirst("=")); + } + + ModBase.WriteFile(MMCSetupFile, Lines.Join("\r\n")); + // 读取文件 + if (Conversions.ToBoolean(ModBase.ReadIni(MMCSetupFile, "OverrideCommands", + Conversions.ToString(false)))) + { + var PreLaunchCommand = ModBase.ReadIni(MMCSetupFile, "PreLaunchCommand"); + if (!string.IsNullOrEmpty(PreLaunchCommand)) + { + PreLaunchCommand = PreLaunchCommand.Replace(@"\""", "\"") + .Replace("$INST_JAVA", "{java}javaw.exe").Replace(@"$INST_MC_DIR\", "{minecraft}") + .Replace("$INST_MC_DIR", "{minecraft}").Replace(@"$INST_DIR\", "{verpath}") + .Replace("$INST_DIR", "{verpath}").Replace("$INST_ID", "{name}") + .Replace("$INST_NAME", "{name}"); + Config.Instance.PreLaunchCommand[VersionFolder] = PreLaunchCommand; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:启动前执行命令:" + PreLaunchCommand); + } + } + + if (Conversions.ToBoolean(ModBase.ReadIni(MMCSetupFile, "JoinServerOnLaunch", + Conversions.ToString(false)))) + { + var ServerAddress = ModBase.ReadIni(MMCSetupFile, "JoinServerOnLaunchAddress") + .Replace(@"\""", "\""); + Config.Instance.ServerToEnter[VersionFolder] = ServerAddress; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:自动进入服务器:" + ServerAddress); + } + + if (Conversions.ToBoolean(ModBase.ReadIni(MMCSetupFile, "IgnoreJavaCompatibility", + Conversions.ToString(false)))) + { + Config.Instance.IgnoreJavaCompatibility[VersionFolder] = true; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:忽略 Java 兼容性警告"); + } + + var Logo = Path.GetFileName(ModBase.ReadIni(MMCSetupFile, "iconKey")); + if (!string.IsNullOrEmpty(Logo) && File.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png")) + { + Config.Instance.IsLogoCustom[VersionFolder] = true; + Config.Instance.LogoPath[VersionFolder] = @"PCL\Logo.png"; + ModBase.CopyFile($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png", + $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\PCL\Logo.png"); + ModBase.Log($"[ModPack] 迁移 MultiMC 实例独立设置:实例图标({Logo}.png)"); + } + + // JVM 参数 + var JvmArgs = ModBase.ReadIni(MMCSetupFile, "JvmArgs"); + if (!string.IsNullOrEmpty(JvmArgs)) + { + if (Conversions.ToBoolean(ModBase.ReadIni(MMCSetupFile, "OverrideJavaArgs", + Conversions.ToString(false)))) + { + Config.Instance.JvmArgs[VersionFolder] = JvmArgs; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(覆盖):" + JvmArgs); + } + else + { + JvmArgs = Conversions.ToString(JvmArgs + + Operators.ConcatenateObject(" ", + Config.Launch.JvmArgs)); + Config.Instance.JvmArgs[VersionFolder] = JvmArgs; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(追加):" + JvmArgs); + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"读取 MMC 配置文件失败({InstallTemp}{ArchiveBaseFolder}instance.cfg)"); + } + + #endregion + }) + { + ProgressWeight = new FileInfo(FileAddress).Length / 1024d / 1024d / 6d, + Block = false + }); // 每 6M 需要 1s + // 构造实例安装请求 + if (PackJson["components"] is null) + throw new Exception("该 MMC 整合包未提供游戏版本信息,无法安装!"); + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = InstanceName, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\" + }; + foreach (var Component in PackJson["components"]) + switch ((Component["uid"] ?? "").ToString() ?? "") + { + case "org.lwjgl": + { + ModBase.Log("[ModPack] 已跳过 LWJGL 项"); + break; + } + case "net.minecraft": + { + Request.MinecraftName = (string)Component["version"]; + break; + } + case "net.minecraftforge": + { + if (Component["version"].ToString().StartsWithF("0.")) + Request.CleanroomVersion = (string)Component["version"]; + else + Request.ForgeVersion = (string)Component["version"]; + + break; + } + case "net.neoforged": + { + Request.NeoForgeVersion = (string)Component["version"]; + break; + } + case "net.fabricmc.fabric-loader": + { + Request.FabricVersion = (string)Component["version"]; + break; + } + case "org.quiltmc.quilt-loader": + { + Request.QuiltVersion = (string)Component["version"]; + break; + } + } + + if (PackInfo is not null) + Request.MMCPackInfo = PackInfo; + // 构造加载器 + var MergeLoaders = ModDownloadLib.McInstallLoader(Request); + // 构造总加载器 + var Loaders = new List(); + Loaders.Add(new LoaderCombo("整合包安装", InstallLoaders) + { Show = false, Block = false, ProgressWeight = InstallLoaders.Sum(l => l.ProgressWeight) }); + Loaders.Add(new LoaderCombo("游戏安装", MergeLoaders) + { Show = false, ProgressWeight = MergeLoaders.Sum(l => l.ProgressWeight) }); + + // 重复任务检查 + var LoaderName = "MMC 整合包安装:" + InstanceName + " "; + if (LoaderTaskbar.Any(l => (l.Name ?? "") == (LoaderName ?? ""))) + { + ModMain.Hint("该整合包正在安装中!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + // 启动 + var Loader = new LoaderCombo(LoaderName, Loaders) { OnStateChanged = ModDownloadLib.McInstallState }; + Loader.Start(Request.TargetInstanceFolder); + LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModBase.RunInUi(() => ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager)); + return Loader; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb index 4da8d6813..e6e32d1c4 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb @@ -844,7 +844,7 @@ Retry: Dim PreLaunchCommand As String = ReadIni(MMCSetupFile, "PreLaunchCommand") If PreLaunchCommand <> "" Then PreLaunchCommand = PreLaunchCommand.Replace("\""", """"). - Replace("$INST_JAVA", "{java}javaw.exe"). + Replace("$INST_JAVA", "{java}\java.exe"). Replace("$INST_MC_DIR\", "{minecraft}").Replace("$INST_MC_DIR", "{minecraft}"). Replace("$INST_DIR\", "{verpath}").Replace("$INST_DIR", "{verpath}"). Replace("$INST_ID", "{name}").Replace("$INST_NAME", "{name}") diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModProfile.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModProfile.cs new file mode 100644 index 000000000..8c19b6b5c --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModProfile.cs @@ -0,0 +1,1173 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO.Net; +using PCL.Core.Utils; +using PCL.Core.Utils.Secret; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace PCL; + +public static class ModProfile +{ + /// + /// 当前选定的档案 + /// + public static McProfile SelectedProfile; + + /// + /// 上次选定的档案编号 + /// + public static int LastUsedProfile; + + /// + /// 档案列表 + /// + public static List ProfileList = new(); + + public static bool IsCreatingProfile; + + /// + /// 档案操作日志 + /// + public static void ProfileLog(string content, ModBase.LogLevel level = ModBase.LogLevel.Normal) + { + var output = "[Profile] " + content; + ModBase.Log(output, level); + } + + #region 旧版迁移 + + /// + /// 从旧版配置文件迁移档案,不能在 UI 线程调用 + /// + public static void MigrateOldProfile() + { + ProfileLog("开始从旧版配置迁移档案"); + var profileCount = 0; + // 正版档案 + if (Conversions.ToBoolean( + !Operators.ConditionalCompareObjectEqual(States.Game.LegacyProfile.LoginMsJson, "{}", false))) + { + var oldMsJson = (JObject)ModBase.GetJson(Conversions.ToString(States.Game.LegacyProfile.LoginMsJson)); + ProfileLog($"找到 {oldMsJson.Count} 个旧版正版档案信息"); + foreach (var Profile in oldMsJson) + { + var newProfile = new McProfile + { + Username = Profile.Key, Uuid = Conversions.ToString(McLoginMojangUuid(Profile.Key, false)), + Type = ModLaunch.McLoginType.Ms + }; + ProfileList.Add(newProfile); + profileCount += 1; + } + + SaveProfile(); + ProfileLog("旧版正版档案迁移完成"); + ModBase.Setup.Reset("LoginMsJson"); + } + else + { + ProfileLog("无旧版正版档案信息"); + } + + // 离线档案 + if (!string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.LoginLegacyName))) + { + var oldOfflineInfo = (string[])((dynamic)States.Game.LegacyProfile.LoginLegacyName).Split("¨"); + ProfileLog($"找到 {oldOfflineInfo.Count()} 个旧版离线档案信息"); + foreach (var OfflineId in oldOfflineInfo) + { + var newProfile = new McProfile + { + Username = OfflineId, Uuid = GetOfflineUuid(OfflineId, isLegacy: true), + Type = ModLaunch.McLoginType.Legacy + }; // 迁移的档案默认使用旧版 UUID 生成方式以避免存档丢失 + ProfileList.Add(newProfile); + profileCount += 1; + } + + SaveProfile(); + ProfileLog("旧版离线档案迁移完成"); + ModBase.Setup.Reset("LoginLegacyName"); + } + else + { + ProfileLog("无旧版离线档案信息"); + } + + // 第三方验证档案 + if (!(string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.AuthUserName)) || + string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.AuthUuid)) || + string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.AuthServerAddress)) || + string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.AuthThirdPartyUserName)) || + string.IsNullOrWhiteSpace(Conversions.ToString(States.Game.LegacyProfile.AuthPassword)))) + { + ProfileLog("找到旧版第三方验证档案信息"); + var newProfile = new McProfile + { + Username = Conversions.ToString(States.Game.LegacyProfile.AuthUserName), + Uuid = Conversions.ToString(States.Game.LegacyProfile.AuthUuid), + Name = Conversions.ToString(States.Game.LegacyProfile.AuthThirdPartyUserName), + Password = Conversions.ToString(States.Game.LegacyProfile.AuthPassword), + Server = Conversions.ToString(Operators.ConcatenateObject(States.Game.LegacyProfile.AuthServerAddress, + "/authserver")), + Type = ModLaunch.McLoginType.Auth + }; + ProfileList.Add(newProfile); + SaveProfile(); + ProfileLog("旧版第三方验证档案迁移完成"); + profileCount += 1; + ModBase.Setup.Reset("CacheAuthName"); + ModBase.Setup.Reset("CacheAuthUuid"); + ModBase.Setup.Reset("CacheAuthServerServer"); + ModBase.Setup.Reset("CacheAuthUsername"); + ModBase.Setup.Reset("CacheAuthPass"); + } + else + { + ProfileLog("无旧版第三方验证档案信息"); + } + + if (!(profileCount == 0)) + ModMain.Hint($"已自动从旧版配置文件迁移档案,共迁移了 {profileCount} 个档案"); + ProfileLog("档案迁移结束"); + } + + #endregion + + #region 获取正版档案 UUID + + /// + /// 根据用户名返回对应 UUID,需要多线程 + /// + /// 玩家 ID + public static object McLoginMojangUuid(string name, bool throwOnNotFound) + { + if (name.Trim().Length == 0) + return ModBase.StrFill("", "0", 32); + // 从缓存获取 + var uuid = ModBase.ReadIni(ModBase.PathTemp + @"Cache\Uuid\Mojang.ini", name); + if (Strings.Len(uuid) == 32) + return uuid; + // 从官网获取 + try + { + JObject gotJson = null; + var finished = false; + ModBase.RunInNewThread(() => + { + try + { + gotJson = (JObject)ModNet.NetGetCodeByRequestRetry( + "https://api.mojang.com/users/profiles/minecraft/" + name, IsJson: true); + } + catch (Exception ex) + { + } + finally + { + finished = true; + } + }, $"{name} Uuid Get"); + while (!finished) + Thread.Sleep(50); + if (gotJson is null) + throw new FileNotFoundException("正版玩家档案不存在(" + name + ")"); + uuid = (string)(gotJson["id"] ?? ""); + } + catch (Exception ex) + { + ModBase.Log(ex, "从官网获取正版 UUID 失败(" + name + ")"); + if (!throwOnNotFound && ex.GetType().Name == "FileNotFoundException") + uuid = GetOfflineUuid(name, isLegacy: true); // 玩家档案不存在 + else + throw new Exception("从官网获取正版 UUID 失败", ex); + } + + // 写入缓存 + if (!(Strings.Len(uuid) == 32)) + throw new Exception("获取的正版 UUID 长度不足(" + uuid + ")"); + ModBase.WriteIni(ModBase.PathTemp + @"Cache\Uuid\Mojang.ini", name, uuid); + return uuid; + } + + #endregion + + #region 类型声明 + + public class McProfile + { + public string AccessToken; + public string ClientToken; + + /// + /// 档案描述,暂时没做功能 + /// + public string Desc; + + /// + /// 联网验证档案的验证有效期 + /// + public long Expires; + + /// + /// 用于识别正版档案的 ID 标识符 + /// + [Obsolete("暂时弃用,应当使用 AccessToken 与 RefreshToken")] + public string IdentityId; + + /// + /// 登录用户名,用于第三方验证 + /// + public string Name; + + /// + /// 登录密码,用于第三方验证 + /// + public string Password; + + /// + /// 原始 JSON 数据,用于正版验证部分功能 + /// + public string RawJson; + + public string RefreshToken; + + /// + /// 验证服务器地址,用于第三方验证 + /// + public string Server; + + /// + /// 验证服务器名称,来自第三方验证服务器返回的 Metadata + /// + public string ServerName; + + /// + /// 用于档案列表头像显示的皮肤 ID + /// + public string SkinHeadId; + + /// + /// 档案类型 + /// + public ModLaunch.McLoginType Type; + + /// + /// 玩家 ID + /// + public string Username; + + public string Uuid; + } + + #endregion + + #region 读写档案 + + /// + /// 重新获取已有档案列表 + /// + public static void GetProfile() + { + ProfileLog("开始获取本地档案"); + ProfileList.Clear(); + var profilePath = Path.Combine(ModBase.PathAppdataConfig, "profiles.json"); + try + { + if (!Directory.Exists(ModBase.PathAppdataConfig)) + Directory.CreateDirectory(ModBase.PathAppdataConfig); + if (!File.Exists(profilePath)) + { + File.Create(profilePath).Close(); + ModBase.WriteFile(profilePath, "{\"lastUsed\":0,\"profiles\":[]}"); // 创建档案列表文件 + } + + var profileJobj = JObject.Parse(ModBase.ReadFile(profilePath)); + LastUsedProfile = (int)profileJobj["lastUsed"]; + var profileListJobj = (JArray)profileJobj["profiles"]; + foreach (var Profile in profileListJobj) + { + McProfile newProfile = null; + if ((string)Profile["type"] == "microsoft") + newProfile = new McProfile + { + Type = ModLaunch.McLoginType.Ms, + Uuid = (string)Profile["uuid"], + Username = (string)Profile["username"], + AccessToken = EncryptHelper.SecretDecrypt((string?)Profile["accessToken"]), + RefreshToken = EncryptHelper.SecretDecrypt((string?)Profile["refreshToken"]), + Expires = (long)Profile["expires"], + Desc = (string)Profile["desc"], + RawJson = EncryptHelper.SecretDecrypt((string?)Profile["rawJson"]), + SkinHeadId = (string)Profile["skinHeadId"] + }; + else if ((string)Profile["type"] == "authlib") + newProfile = new McProfile + { + Type = ModLaunch.McLoginType.Auth, + Uuid = (string)Profile["uuid"], + Username = (string)Profile["username"], + AccessToken = EncryptHelper.SecretDecrypt((string?)Profile["accessToken"]), + RefreshToken = EncryptHelper.SecretDecrypt((string?)Profile["refreshToken"]), + Expires = (long)Profile["expires"], + Server = (string)Profile["server"], + ServerName = (string)Profile["serverName"], + Name = EncryptHelper.SecretDecrypt((string?)Profile["name"]), + Password = EncryptHelper.SecretDecrypt((string?)Profile["password"]), + ClientToken = EncryptHelper.SecretDecrypt((string?)Profile["clientToken"]), + Desc = (string)Profile["desc"], + SkinHeadId = (string)Profile["skinHeadId"] + }; + else + newProfile = new McProfile + { + Type = ModLaunch.McLoginType.Legacy, + Uuid = (string)Profile["uuid"], + Username = (string)Profile["username"], + Desc = (string)Profile["desc"], + SkinHeadId = (string)Profile["skinHeadId"] + }; + ProfileList.Add(newProfile); + } + + ProfileLog($"获取到 {ProfileList.Count} 个档案"); + } + catch (Exception ex) + { + try + { + var profilePathBak = + Path.Combine(ModBase.PathAppdataConfig, $"profiles.json.bak{DateTime.Now.ToBinary()}"); + File.Move(profilePath, profilePathBak); + } + catch (Exception ex1) + { + } + + ModBase.Log(ex, "档案数据读取失败,文件可能意外损坏。已对档案文件进行备份重置。", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 以当前的档案列表写入配置文件 + /// + public static void SaveProfile(JArray listJson = null) + { + try + { + var json = new JObject(); + if (listJson is not null) + { + json = new JObject { { "lastUsed", LastUsedProfile }, { "profiles", listJson } }; + } + else + { + var list = new JArray(); + foreach (var Profile in ProfileList) + { + JObject profileJobj = null; + if (Profile.Type == ModLaunch.McLoginType.Ms) + profileJobj = new JObject + { + { "type", "microsoft" }, { "uuid", Profile.Uuid }, { "username", Profile.Username }, + { "accessToken", EncryptHelper.SecretEncrypt(Profile.AccessToken) }, + { "refreshToken", EncryptHelper.SecretEncrypt(Profile.RefreshToken) }, + { "expires", Profile.Expires }, { "desc", Profile.Desc }, + { "rawJson", EncryptHelper.SecretEncrypt(Profile.RawJson) }, + { "skinHeadId", Profile.SkinHeadId } + }; + else if (Profile.Type == ModLaunch.McLoginType.Auth) + profileJobj = new JObject + { + { "type", "authlib" }, { "uuid", Profile.Uuid }, { "username", Profile.Username }, + { "accessToken", EncryptHelper.SecretEncrypt(Profile.AccessToken) }, + { "refreshToken", EncryptHelper.SecretEncrypt(Profile.RefreshToken) }, + { "expires", Profile.Expires }, { "server", Profile.Server }, + { "serverName", Profile.ServerName }, { "name", EncryptHelper.SecretEncrypt(Profile.Name) }, + { "password", EncryptHelper.SecretEncrypt(Profile.Password) }, + { "clientToken", EncryptHelper.SecretEncrypt(Profile.ClientToken) }, + { "desc", Profile.Desc }, { "skinHeadId", Profile.SkinHeadId } + }; + else + profileJobj = new JObject + { + { "type", "offline" }, { "uuid", Profile.Uuid }, { "username", Profile.Username }, + { "desc", Profile.Desc }, { "skinHeadId", Profile.SkinHeadId } + }; + list.Add(profileJobj); + } + + ProfileLog($"开始保存档案,共 {list.Count} 个"); + json = new JObject { { "lastUsed", LastUsedProfile }, { "profiles", list } }; + } + + var actualFile = Path.Combine(ModBase.PathAppdataConfig, "profiles.json"); + var tempFile = actualFile + ".tmp"; + var bakFile = actualFile + ".bak"; + File.WriteAllBytes(tempFile, Encoding.UTF8.GetBytes(json.ToString(Formatting.None))); + if (File.Exists(actualFile)) + File.Replace(tempFile, actualFile, bakFile); + else + File.Move(tempFile, actualFile); + ProfileLog("档案已保存"); + } + catch (Exception ex) + { + ModBase.Log(ex, "写入档案列表失败", ModBase.LogLevel.Feedback); + } + } + + #endregion + + #region 新建与编辑 + + /// + /// 新建档案 + /// + public static void CreateProfile() + { + int? selectedAuthTypeNum = default; // 验证类型序号 + ModBase.RunInUiWait(() => + { + List authTypeList; + var HasMinecraftAccount = ProfileList.Any(x => x.Type == ModLaunch.McLoginType.Ms); + var Restricted = RegionUtils.IsRestrictedFeatAllowed && ProfileList.Count > 0; + var HasNetwork = NetworkHelper.IsNetworkAvailable(); + if (HasMinecraftAccount || Restricted || !HasNetwork) + authTypeList = + [ + new MyListItem + { + Title = "正版验证", + Type = MyListItem.CheckType.RadioBox, + Logo = ModBase.Logo.IconButtonAuth + }, + + new MyListItem + { + Title = "第三方验证", + Type = MyListItem.CheckType.RadioBox, + Logo = ModBase.Logo.IconButtonThirdparty + }, + + new MyListItem + { + Title = "离线验证", + Type = MyListItem.CheckType.RadioBox, + Logo = ModBase.Logo.IconButtonOffline + } + ]; + else + authTypeList = + [ + new MyListItem + { + Title = "正版验证", + Type = MyListItem.CheckType.RadioBox, + Logo = ModBase.Logo.IconButtonAuth + } + ]; + selectedAuthTypeNum = ModMain.MyMsgBoxSelect(authTypeList, "新建档案 - 选择验证类型", "继续", "取消"); + }); + if (selectedAuthTypeNum is null) + return; + IsCreatingProfile = true; + if (selectedAuthTypeNum.HasValue && selectedAuthTypeNum.Value == 0) // 正版验证 + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Ms)); + else if (selectedAuthTypeNum.HasValue && selectedAuthTypeNum.Value == 1) // 第三方验证 + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth)); + else // 离线验证 + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Legacy)); + } + + /// + /// 编辑当前档案的 ID + /// + public static void EditProfileId() + { + if (SelectedProfile.Type == ModLaunch.McLoginType.Ms) + { + string newUsername = null; + ModBase.RunInUiWait(() => newUsername = ModMain.MyMsgBoxInput("输入新的玩家 ID", "玩家 ID 只能每 30 天更改一次名称,请谨慎考虑!", + SelectedProfile.Username, + new Collection { new ValidateLength(3, 16), new ValidateRegex("([A-z]|[0-9]|_)+") }, + "3 - 16 个字符,只可以包含大小写字母、数字、下划线", "确认")); + if (string.IsNullOrEmpty(newUsername)) + return; + if (string.IsNullOrWhiteSpace(newUsername)) + { + ModMain.Hint("欲设置的玩家名称为空"); + return; + } + + if (ModMain.MyMsgBox("注意:玩家 ID 只能每 30 天更改一次,请务必谨慎考虑!", "确认修改", "继续修改", "取消", IsWarn: true) == 2) + return; + // 更新档案信息 + // 刷新页面信息 + ModBase.RunInNewThread(() => + { + try + { + var checkResult = (JObject)ModBase.GetJson(ModNet.NetRequestRetry( + $"https://api.minecraftservices.com/minecraft/profile/name/{newUsername}/available", "GET", + null, null, + Headers: new Dictionary + { { "Authorization", "Bearer " + SelectedProfile.AccessToken } })); + if ((string)checkResult["status"] == "DUPLICATE") + { + ModMain.MyMsgBox("此 ID 已被使用,请换一个 ID。", "ID 修改失败", "确认", IsWarn: true); + return; + } + + if ((string)checkResult["status"] == "NOT_ALLOWED") + { + ModMain.MyMsgBox("此 ID 包含了除大小写字母、数字、下划线以外的不合法字符。", "ID 修改失败", "确认", IsWarn: true); + return; + } + + var result = ModNet.NetRequestRetry( + $"https://api.minecraftservices.com/minecraft/profile/name/{newUsername}", "PUT", "", + "application/json", Conversions.ToBoolean(2), + new Dictionary + { { "Authorization", "Bearer " + SelectedProfile.AccessToken } }); + var resultJson = (JObject)ModBase.GetJson(result); + ModMain.Hint($"玩家 ID 修改成功,当前 ID 为:{resultJson["name"]}", ModMain.HintType.Finish); + ProfileList.Remove(SelectedProfile); + SelectedProfile.Username = (string)resultJson["name"]; + ProfileList.Add(SelectedProfile); + LastUsedProfile = ProfileList.Count - 1; + ModMain.FrmLaunchLeft.RefreshPage(true); + SaveProfile(); + } + catch (HttpRequestException ex) + { + var exSummary = ex.ToString(); + if (exSummary.Contains("403")) + ModMain.MyMsgBox("首次更改 ID 后,必须等待 30 天后才能再次修改 ID,你可以前往官网查询具体时间。", "ID 修改失败", "我知道了"); + else + ModBase.Log(ex, "修改档案 ID 失败", ModBase.LogLevel.Msgbox); + } + }); + } + + + else if (SelectedProfile.Type == ModLaunch.McLoginType.Auth) + { + var server = SelectedProfile.Server; + ModBase.OpenWebsite(server.Replace("/api/yggdrasil/authserver" + (server.EndsWithF("/") ? "/" : ""), + "/user/profile")); + } + else + { + string newUsername = null; + ModBase.RunInUiWait(() => newUsername = ModMain.MyMsgBoxInput("输入新的玩家 ID", + DefaultInput: SelectedProfile.Username, + ValidateRules: new Collection + { new ValidateLength(3, 16), new ValidateRegex("([A-z]|[0-9]|_)+") }, + HintText: "3 - 16 个字符,只可以包含大小写字母、数字、下划线", Button1: "确认", Button2: "取消")); + if (string.IsNullOrEmpty(newUsername)) + return; + EditOfflineUuid(SelectedProfile, GetOfflineUuid(newUsername)); + } + } + + /// + /// 编辑离线档案的 UUID + /// + /// 目标档案 + public static void EditOfflineUuid(McProfile profile, string uuid = null) + { + var profileIndex = ProfileList.IndexOf(profile); + string newUuid; + if (uuid is not null) + { + newUuid = uuid; + goto Write; + } + + int uuidType; + int? uuidTypeInput = default; + ModBase.RunInUiWait(() => + { + var uuidTypeList = new List + { + new MyRadioBox { Text = "行业规范 UUID(推荐)" }, new MyRadioBox { Text = "官方版 PCL UUID(若单人存档的部分信息丢失,可尝试此项)" }, + new MyRadioBox { Text = "自定义" } + }; + uuidTypeInput = ModMain.MyMsgBoxSelect(uuidTypeList, "新建档案 - 选择 UUID 类型", "继续", "取消"); + }); + if (uuidTypeInput is null) + return; + uuidType = (int)uuidTypeInput; + if (uuidType == 0) + newUuid = GetOfflineUuid(profile.Username); + else if (uuidType == 1) + newUuid = GetOfflineUuid(profile.Username, isLegacy: true); + else + newUuid = ModMain.MyMsgBoxInput($"更改档案 {profile.Username} 的 UUID", DefaultInput: profile.Uuid, + HintText: "32 位,不含连字符", + ValidateRules: new Collection + { new ValidateLength(32, 32), new ValidateRegex("([A-z]|[0-9]){32}", "UUID 只应该包括英文字母和数字!") }, + Button1: "继续", Button2: "取消"); + if (string.IsNullOrEmpty(newUuid)) + return; + Write: ; + + ProfileList[profileIndex].Uuid = newUuid; + SelectedProfile = ProfileList[profileIndex]; + SaveProfile(); + ModMain.Hint("档案信息已保存!", ModMain.HintType.Finish); + } + + /// + /// 编辑指定档案的验证服务器显示名称 + /// + public static void EditAuthServerName(McProfile profile, string serverName) + { + var profileIndex = ProfileList.IndexOf(profile); + ProfileList[profileIndex].ServerName = serverName; + SaveProfile(); + ModMain.Hint("档案信息已保存!", ModMain.HintType.Finish); + } + + /// + /// 删除特定档案 + /// + /// 目标档案 + public static void RemoveProfile(McProfile profile) + { + ProfileList.Remove(profile); + LastUsedProfile = default; + SaveProfile(); + ModMain.Hint("档案删除成功!", ModMain.HintType.Finish); + } + + #endregion + + #region 导入与导出 + + public static void MigrateProfile() + { + // 1. 初始化路径与状态检查 + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var hmclAccountPath = Path.Combine(appData, ".hmcl", "accounts.json"); + var hasProfiles = ProfileList.Count > 0; + var opType = 3; // 1: 导入, 2: 导出, 3: 取消 + + // 2. 用户交互 + ModBase.RunInUiWait(() => + { + if (hasProfiles) + { + opType = ModMain.MyMsgBox($"PCL CE 支持与 HMCL 相互同步全局档案列表。{"\r\n"}请选择操作:", "档案迁移", "导入", "导出", + "取消", ForceWait: true); + } + else + { + opType = ModMain.MyMsgBox("由于当前档案列表为空,仅支持从 HMCL 导入档案。", "档案迁移", "导入", "取消", ForceWait: true); + if (opType == 2) opType = 3; + } + }); + + if (opType == 3) + return; + + // 3. 分发逻辑 + if (opType == 1) + PerformImport(hmclAccountPath); + else + PerformExport(hmclAccountPath); + } + + // --- 核心业务逻辑 --- + + private static void PerformImport(string path) + { + ModMain.Hint("正在从 HMCL 导入..."); + + // 使用 System.Text.Json 解析 + + + // 查重逻辑 + + + ModBase.RunInNewThread(() => + { + try + { + if (!File.Exists(path)) + { + ModMain.Hint("未找到 HMCL 的配置文件。", ModMain.HintType.Critical); + return; + } + + var jsonBytes = File.ReadAllBytes(path); + using (var doc = JsonDocument.Parse(jsonBytes)) + { + var importCount = 0; + var importProfiles = new List(); + var hasMsProfile = ProfileList.Any(p => p.Type == ModLaunch.McLoginType.Ms); + foreach (var element in doc.RootElement.EnumerateArray()) + { + var profile = ConvertToPclProfile(element); + if (profile is null) continue; + if (profile.Type == ModLaunch.McLoginType.Ms) + { + hasMsProfile = true; + if (ProfileList.Any(p => + p.Type == ModLaunch.McLoginType.Ms && (p.Uuid ?? "") == (profile.Uuid ?? ""))) + continue; + } + + importProfiles.Add(profile); + importCount += 1; + } + + if (!hasMsProfile) + { + ModMain.Hint("你必须先进行一次正版验证才能导入这些档案!", ModMain.HintType.Critical); + return; + } + + ProfileList.AddRange(importProfiles); + SaveProfile(); + if (importCount == 0) + { + ModMain.Hint("没有新档案可供导入。"); + } + else + { + ModMain.Hint($"成功导入 {importCount} 个档案!", ModMain.HintType.Finish); + ModBase.RunInUi(() => ModMain.FrmLoginProfile.RefreshProfileList()); + } + } + } + catch (Exception ex) + { + ProfileLog("导入失败: " + ex.Message); + ModMain.Hint("导入出错,请检查文件格式。", ModMain.HintType.Critical); + } + }, "Profile Import"); + } + + private static void PerformExport(string path) + { + ModMain.Hint("正在导出至 HMCL..."); + try + { + // 1. 读取并解析现有列表,准备合并 + var finalDictList = new List>(); + + if (File.Exists(path)) + { + var oldJson = File.ReadAllText(path); + if (!string.IsNullOrWhiteSpace(oldJson)) + // 这里简单处理:将旧的转回原始结构,避免丢失 HMCL 自己的其他账户 + using (var doc = JsonDocument.Parse(oldJson)) + { + foreach (var el in doc.RootElement.EnumerateArray()) + { + // 此处可根据需要转换回 Dictionary + } + } + } + + // 2. 转换当前 PCL 列表 + foreach (var profile in ProfileList) + finalDictList.Add(ConvertToHmclDict(profile)); + + // 3. 序列化并写入 + var options = new JsonSerializerOptions { WriteIndented = true }; + var jsonString = JsonSerializer.Serialize(finalDictList, options); + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllText(path, jsonString); + + ModMain.Hint($"已成功同步 {ProfileList.Count} 个档案。", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ProfileLog("导出失败: " + ex.Message); + ModMain.Hint("导出失败。", ModMain.HintType.Critical); + } + } + + // --- 类型转换辅助 --- + + private static McProfile ConvertToPclProfile(JsonElement el) + { + try + { + var typeStr = el.GetProperty("type").GetString(); + JsonElement argvalue = default; + var profile = new McProfile + { + Uuid = el.TryGetProperty("uuid", out argvalue) ? el.GetProperty("uuid").GetString() : "", + Expires = 1743779140286L + }; + + switch (typeStr ?? "") + { + case "microsoft": + { + profile.Type = ModLaunch.McLoginType.Ms; + profile.Username = el.GetProperty("displayName").GetString(); + break; + } + case "authlibInjector": + { + profile.Type = ModLaunch.McLoginType.Auth; + profile.Username = el.GetProperty("displayName").GetString(); + profile.Server = el.GetProperty("serverBaseURL").GetString(); + profile.Name = el.GetProperty("username").GetString(); + profile.ClientToken = el.GetProperty("clientToken").GetString(); + break; + } + + default: + { + profile.Type = ModLaunch.McLoginType.Legacy; + profile.Username = el.GetProperty("username").GetString(); + break; + } + } + + return profile; + } + catch + { + return null; + } + } + + private static Dictionary ConvertToHmclDict(McProfile profile) + { + var dict = new Dictionary(); + dict["uuid"] = profile.Uuid; + + switch (profile.Type) + { + case ModLaunch.McLoginType.Ms: + { + dict["displayName"] = profile.Username; + dict["type"] = "microsoft"; + dict["tokenType"] = "Bearer"; + dict["accessToken"] = ""; + dict["notAfter"] = 1743779140286L; + break; + } + case ModLaunch.McLoginType.Auth: + { + dict["serverBaseURL"] = profile.Server; + dict["displayName"] = profile.Username; + dict["username"] = profile.Name; + dict["type"] = "authlibInjector"; + dict["clientToken"] = profile.ClientToken; + break; + } + + default: + { + dict["username"] = profile.Username; + dict["type"] = "offline"; + break; + } + } + + return dict; + } + + #endregion + + #region 离线 UUID 获取 + + /// + /// 获取离线 UUID + /// + /// 玩家 ID + /// 返回的 UUID 是否有连字符分割 + /// 是否使用旧版 PCL 生成方式,若为 True 则返回的 UUID 总是不带连字符 + public static string GetOfflineUuid(string userName, bool isSplited = false, bool isLegacy = false) + { + if (isLegacy) + { + var fullUuid = ModBase.StrFill(userName.Length.ToString("X"), "0", 16) + + ModBase.StrFill(ModBase.GetHash(userName).ToString("X"), "0", 16); + return fullUuid.Substring(0, 12) + "3" + fullUuid.Substring(13, 3) + "9" + fullUuid.Substring(17, 15); + } + + var md5Hash = MD5.Create(); + var hash = md5Hash.ComputeHash(Encoding.UTF8.GetBytes("OfflinePlayer:" + userName)); + hash[6] = (byte)(hash[6] & 0xF); + hash[6] = (byte)(hash[6] | 0x30); + hash[8] = (byte)(hash[8] & 0x3F); + hash[8] = (byte)(hash[8] | 0x80); + var parsed = new Guid(ToUuidString(hash)); + ProfileLog("获取到离线 UUID: " + parsed); + if (isSplited) return parsed.ToString(); + + return parsed.ToString().Replace("-", ""); + } + + private static string ToUuidString(byte[] bytes) + { + var msb = 0L; + var lsb = 0L; + for (var i = 0; i <= 7; i++) + msb = (msb << 8) | (bytes[i] & 0xFF); + for (var i = 8; i <= 15; i++) + lsb = (lsb << 8) | (bytes[i] & 0xFF); + return Conversions.ToString(Operators.AddObject( + Operators.AddObject( + Operators.AddObject( + Operators.AddObject( + Operators.AddObject( + Operators.AddObject( + Operators.AddObject(Operators.AddObject(Digits(msb >> 32, 8), "-"), + Digits(msb >> 16, 4)), "-"), Digits(msb, 4)), "-"), Digits(lsb >> 48, 4)), "-"), + Digits(lsb, 12))); + } + + private static object Digits(long val, int digs) + { + var hi = 1L << (digs * 4); + return (hi | (val & (hi - 1L))).ToString("X").Substring(1); + } + + #endregion + + #region 档案信息获取 + + /// + /// 获取档案详情信息用于显示 + /// + /// 目标档案 + /// 显示的详情信息 + public static object GetProfileInfo(McProfile profile) + { + string info = null; + if (profile.Type == ModLaunch.McLoginType.Auth) + { + info += "第三方验证"; + if (!string.IsNullOrWhiteSpace(profile.ServerName)) + info += $" / {profile.ServerName}"; + } + else if (profile.Type == ModLaunch.McLoginType.Ms) + { + info += "正版验证"; + } + else + { + info += "离线验证"; + } + + if (!string.IsNullOrWhiteSpace(profile.Desc)) + info += $",{profile.Desc}"; + return info; + } + + /// + /// 获取当前档案的验证信息。 + /// 验证类型,若为新档案需填 + /// + public static ModLaunch.McLoginData GetLoginData(ModLaunch.McLoginType targetAuthType = default) + { + ModLaunch.McLoginType authType = default; + if (SelectedProfile is null) // 新档案 + { + if (targetAuthType != default) + authType = targetAuthType; + else + authType = ModLaunch.McLoginType.Legacy; + if (authType == ModLaunch.McLoginType.Auth) + return new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth) + { + Description = "Authlib-Injector", + Type = ModLaunch.McLoginType.Auth, + IsExist = ModMain.FrmLoginAuth is null + }; + + if (authType == ModLaunch.McLoginType.Ms) return new ModLaunch.McLoginMs(); + + return new ModLaunch.McLoginLegacy(); + } + + // 已有档案 + authType = SelectedProfile.Type; + if (authType == ModLaunch.McLoginType.Auth) + return new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth) + { + BaseUrl = SelectedProfile.Server, + UserName = SelectedProfile.Name, + Password = SelectedProfile.Password, + Description = "Authlib-Injector", + Type = ModLaunch.McLoginType.Auth, + IsExist = ModMain.FrmLoginAuth is null + }; + + if (authType == ModLaunch.McLoginType.Ms) + { + if (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Finished) + return new ModLaunch.McLoginMs + { + OAuthRefreshToken = SelectedProfile.RefreshToken, + UserName = SelectedProfile.Username, + AccessToken = SelectedProfile.AccessToken, + Uuid = SelectedProfile.Uuid, + ProfileJson = SelectedProfile.RawJson + }; + + return new ModLaunch.McLoginMs + { OAuthRefreshToken = SelectedProfile.RefreshToken, UserName = SelectedProfile.Name }; + } + + return new ModLaunch.McLoginLegacy { UserName = SelectedProfile.Username, Uuid = SelectedProfile.Uuid }; + } + + /// + /// 检查当前档案是否有效 + /// + /// 若档案验证有效,则返回空字符串,否则返回错误原因 + public static object IsProfileValid() + { + switch (SelectedProfile.Type) + { + case ModLaunch.McLoginType.Legacy: + { + if (string.IsNullOrEmpty(SelectedProfile.Username.Trim())) + return "玩家名不能为空!"; + if (SelectedProfile.Username.Contains("\"")) + return "玩家名不能包含英文引号!"; + if (ModMinecraft.McInstanceSelected is not null && ModMinecraft.McInstanceSelected.Info.Drop >= 203 && + SelectedProfile.Username.Trim().Length > 16) return "自 1.20.3 起,玩家名至多只能包含 16 个字符!"; + return ""; + } + case ModLaunch.McLoginType.Ms: + { + return ""; + } + case ModLaunch.McLoginType.Auth: + { + return ""; + } + } + + return "未知的验证方式"; + } + + #endregion + + #region 皮肤 + + private static bool _isMsSkinChanging; + + public static void ChangeSkinMs() + { + // 检查条件,获取新皮肤 + if (_isMsSkinChanging) + { + ModMain.Hint("正在更改皮肤中,请稍候!"); + return; + } + + if (ModLaunch.McLoginLoader.State == ModBase.LoadState.Failed) + { + ModMain.Hint("登录失败,无法更改皮肤!", ModMain.HintType.Critical); + return; + } + + var skinInfo = ModMinecraft.McSkinSelect(); + if (!skinInfo.IsVaild) + return; + ModMain.Hint("正在更改皮肤……"); + _isMsSkinChanging = true; + // 开始实际获取 + + // 获取登录信息 + + // 获取新皮肤地址 + ModBase.RunInNewThread(() => + { + try + { + Retry: ; + if (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Loading) + ModLaunch.McLoginMsLoader.WaitForExit(); + if (ModLaunch.McLoginMsLoader.State != ModBase.LoadState.Finished) + ModLaunch.McLoginMsLoader.WaitForExit(GetLoginData()); + if (ModLaunch.McLoginMsLoader.State != ModBase.LoadState.Finished) + { + ModMain.Hint("登录失败,无法更改皮肤!", ModMain.HintType.Critical); + return; + } + + var accessToken = SelectedProfile.AccessToken; + var headers = new Dictionary(); + headers.Add("Authorization", $"Bearer {accessToken}"); + headers.Add("Accept", "*/*"); + headers.Add("User-Agent", "MojangSharp/0.1"); + var contents = new MultipartFormDataContent + { + { new StringContent(skinInfo.IsSlim ? "slim" : "classic"), "variant" }, + { + new ByteArrayContent(ModBase.ReadFileBytes(skinInfo.LocalFile)), "file", + ModBase.GetFileNameFromPath(skinInfo.LocalFile) + } + }; + var res = ModNet.NetRequestRetry("https://api.minecraftservices.com/minecraft/profile/skins", "POST", + contents, null, Headers: headers); + if (res.Contains("request requires user authentication")) + { + ModMain.Hint("正在登录,将在登录完成后继续更改皮肤……"); + ModLaunch.McLoginMsLoader.Start(GetLoginData(), true); + goto Retry; + } + + if (res.Contains("\"error\"")) + { + ModMain.Hint( + Conversions.ToString(Operators.ConcatenateObject("更改皮肤失败:", + ((JObject)ModBase.GetJson(res))["error"])), + ModMain.HintType.Critical); + return; + } + + ModBase.Log("[Skin] 皮肤修改返回值:" + "\r\n" + res); + var resultJson = (JObject)ModBase.GetJson(res); + if (resultJson.ContainsKey("errorMessage")) throw new Exception(resultJson["errorMessage"].ToString()); + foreach (JObject skin in resultJson["skins"]) + if (skin["state"].ToString() == "ACTIVE") + { + MySkin.ReloadCache((string)skin["url"]); + return; + } + + throw new Exception("未知错误(" + res + ")"); + } + catch (Exception ex) + { + if (ex.GetType().Equals(typeof(TaskCanceledException))) + ModMain.Hint("更改皮肤失败:与 Mojang 皮肤服务器的连接超时,请检查你的网络是否通畅!", ModMain.HintType.Critical); + else + ModBase.Log(ex, "更改皮肤失败", ModBase.LogLevel.Hint); + } + finally + { + _isMsSkinChanging = false; + } + }, "Ms Skin Upload"); // 等待登录结束 + // #5309 + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModStyle.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModStyle.cs new file mode 100644 index 000000000..d0a0e6646 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModStyle.cs @@ -0,0 +1,298 @@ +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Media; +using System.Windows.Threading; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.UI.Controls; + +namespace PCL; + +internal static class ModStyle +{ + // Partly generated by claude-sonnet-4-20250514 + public class TimerRun : Run, IDisposable + { + // 定时器事件 + public delegate void TimerTickDelegate(TimerRun sender); + + // 定义依赖属性 + public static readonly DependencyProperty UpdateIntervalProperty = + DependencyProperty.Register(nameof(UpdateInterval), typeof(TimeSpan), typeof(TimerRun), + new PropertyMetadata(TimeSpan.FromSeconds(1d))); + + private object _isDisposed = false; + + private DispatcherTimer _timer; + + public TimerRun(TimeSpan interval = default, bool autoStart = false) + { + _timer = new DispatcherTimer(); + _timer.Tick += _timerTick; + UpdateInterval = interval == default ? TimeSpan.FromSeconds(1d) : interval; + AutoStart = autoStart; + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + private object _isTimerRunning => _timer is not null && _timer.IsEnabled; + + // UpdateInterval 属性 + public TimeSpan UpdateInterval + { + get => (TimeSpan)GetValue(UpdateIntervalProperty); + set + { + if (value > TimeSpan.Zero) SetValue(UpdateIntervalProperty, value); + } + } + + public bool AutoStart { get; set; } + + public void Dispose() + { + if (Conversions.ToBoolean(_isDisposed)) + return; + _isDisposed = true; + // 资源释放 + _timer.Tick -= _timerTick; + _timer?.Stop(); + _timer = null; + } + + // 属性变化处理 + protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + if (ReferenceEquals(e.Property, UpdateIntervalProperty) && _timer is not null) + _timer.Interval = UpdateInterval; + } + + public event TimerTickDelegate? TimerTick; + + private void _timerTick(object sender, EventArgs e) + { + TimerTick?.Invoke(this); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + if (AutoStart) + StartTimer(); + } + + private void OnUnloaded(object sender, RoutedEventArgs e) + { + StopTimer(); + } + + public void StartTimer() + { + if (Dispatcher is null) + { + ModBase.Log("[TimerRun] Dispatcher is null, unable to run", ModBase.LogLevel.Critical); + return; + } + + if (!(bool)_isTimerRunning) + _timer?.Start(); + } + + public void StopTimer() + { + if ((bool)_isTimerRunning) + _timer?.Stop(); + } + } + + public class MinecraftFormatter + { + private static readonly Dictionary colorMap = new() + { + { "black", "0" }, { "dark_blue", "1" }, { "dark_green", "2" }, { "dark_aqua", "3" }, { "dark_red", "4" }, + { "dark_purple", "5" }, { "gold", "6" }, { "gray", "7" }, { "dark_gray", "8" }, { "blue", "9" }, + { "green", "a" }, { "aqua", "b" }, { "red", "c" }, { "light_purple", "d" }, { "yellow", "e" }, + { "white", "f" } + }; + + private static readonly Random random = new(); + + private static readonly string randomChars = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?/~"; + + public static string ConvertToMinecraftFormat(JObject data) + { + var result = ""; + foreach (var item in data["extra"]) + result += ProcessElement((JObject)item, new List()); + return result.Replace("§§", "§"); + } + + private static string ProcessElement(JObject element, List currentFormat) + { + var text = ""; + var formats = new List(currentFormat); + + // 处理格式 + if (element.ContainsKey("bold") && element["bold"].ToObject()) formats.Add("l"); + + if (element.ContainsKey("color")) + { + var color = element["color"].ToString(); + var colorCode = "f"; + if (colorMap.ContainsKey(color)) colorCode = colorMap[color]; + formats.Insert(0, colorCode); // 颜色代码在前 + } + + // 应用格式 + if (formats.Count > 0) text += "§" + string.Join("§", formats); + + // 添加文本内容 + if (element.ContainsKey("text")) text += element["text"].ToString(); + + // 处理子元素 + if (element.ContainsKey("extra")) + foreach (var child in element["extra"]) + text += ProcessElement((JObject)child, new List(formats)); + + return text; + } + + /// + /// Minecraft 文本格式化代码,用于显示不同颜色的文本 + /// + /// 要格式化的文本 + /// 控件 + public static void SetColorfulTextLab(string text, TextBlock lab, bool isDarkMode = true) + { + if (lab is null) + { + ModBase.Log("[Style] SetColorfulTextLab: lab is null"); + return; + } + + lab.Inlines.Clear(); + + var HasItalicProperty = false; // 斜体 + var HasDeleteLineProperty = false; // 删除线 + var HasStrickThroughProperty = false; // 下划线 + var HasBlodProperty = false; // 粗体 + var IsRandomText = false; // 随机文本模式 + + var color = isDarkMode ? "#FFFFFF" : "#888888"; + var isColorCode = false; + var curRun = new TimerRun(); + lab.Inlines.Add(curRun); + + // 用于存储需要随机化的文本段 + var randomTextRuns = new List(); + + foreach (var c in text) + { + if (Conversions.ToString(c) == "§") // 下一字符是格式化代码 + { + isColorCode = true; + continue; + } + + if (isColorCode) + { + if (!MotdRenderer.TryGetColorFromCode(c.ToString(), isDarkMode, out color)) + switch (c) + { + // 格式化代码 + case 'k': + case 'K': // 随机字符 + { + IsRandomText = true; + // 开始新的Run用于随机文本 + if (!string.IsNullOrEmpty(curRun.Text)) + { + curRun = new TimerRun(); + lab.Inlines.Add(curRun); + } + + curRun.AutoStart = true; + randomTextRuns.Add(curRun); + break; + } + case 'l': // 粗体 + { + HasBlodProperty = true; + break; + } + case 'o': // 斜体 + { + HasItalicProperty = true; + break; + } + case 'n': // 下划线 + { + HasStrickThroughProperty = true; + break; + } + case 'm': // 删除线 + { + HasDeleteLineProperty = true; + break; + } + case 'r': // 重置 + { + color = isDarkMode ? "#FFFFFF" : "#888888"; + HasBlodProperty = false; + HasItalicProperty = false; + HasStrickThroughProperty = false; + HasDeleteLineProperty = false; + IsRandomText = false; + break; + } + } + + if (!string.IsNullOrEmpty(curRun.Text) && Conversions.ToString(c) != "k" && + Conversions.ToString(c) != "K") // 遇到格式代码但是有文本,重开一个Run + { + curRun = new TimerRun(); + lab.Inlines.Add(curRun); + } + + curRun.Foreground = new SolidColorBrush(new ModBase.MyColor(color)); + curRun.FontWeight = HasBlodProperty ? FontWeights.Bold : FontWeights.Normal; + curRun.FontStyle = HasItalicProperty ? FontStyles.Italic : FontStyles.Normal; + curRun.TextDecorations = HasStrickThroughProperty ? TextDecorations.Strikethrough : null; + curRun.TextDecorations = HasDeleteLineProperty ? TextDecorations.Underline : null; + } + else if (IsRandomText) + { + // 随机模式下,添加随机字符 + curRun.Text += Conversions.ToString(randomChars[random.Next(randomChars.Length)]); + } + else + { + curRun.Text += Conversions.ToString(c); + } + + if (isColorCode) + isColorCode = false; + } + + // 设置定时器来更新随机文本 + if (randomTextRuns.Count > 0) + foreach (var run in randomTextRuns) + { + run.UpdateInterval = TimeSpan.FromMilliseconds(20d); + run.TimerTick += sender => + { + if (!string.IsNullOrEmpty(sender.Text)) + { + var sb = new StringBuilder(); + for (int i = 0, loopTo = sender.Text.Length - 1; i <= loopTo; i++) + sb.Append(randomChars[random.Next(randomChars.Length)]); + sender.Text = sb.ToString(); + } + }; + } + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.cs new file mode 100644 index 000000000..1144d4f88 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.cs @@ -0,0 +1,799 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Logging; + +namespace PCL; + +public static class ModWatcher +{ + // 对全体的监视 + public static List McWatcherList = new(); + private static bool IsWatcherRunning; + public static bool HasRunningMinecraft; + + private static void WatcherStateChanged() + { + var IsRunning = false; + var TriggerLauncherShutdown = true; + foreach (var Watcher in McWatcherList) + { + if (Watcher.State == Watcher.MinecraftState.Loading || Watcher.State == Watcher.MinecraftState.Running) + { + IsRunning = true; + break; + } + + if (Watcher.State == Watcher.MinecraftState.Crashed || Watcher.State == Watcher.MinecraftState.Canceled) + TriggerLauncherShutdown = false; + } + + if (IsWatcherRunning == IsRunning) + return; + IsWatcherRunning = IsRunning; + if (IsWatcherRunning) + MinecraftStart(); + else + MinecraftStop(TriggerLauncherShutdown); + } + + private static void MinecraftStart() + { + ModLaunch.McLaunchLog("[全局] 出现运行中的 Minecraft"); + HasRunningMinecraft = true; + ModMain.FrmMain.BtnExtraShutdown.ShowRefresh(); + } + + private static void MinecraftStop(bool TriggerLauncherShutdown) + { + ModLaunch.McLaunchLog("[全局] 已无运行中的 Minecraft"); + HasRunningMinecraft = false; + ModMain.FrmMain.BtnExtraShutdown.ShowRefresh(); + // 音乐播放 + if (Conversions.ToBoolean(Config.Preference.Music.StopInGame)) + ModBase.RunInUi(() => + { + if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在结束后开始音乐播放"); + }); + else if (Conversions.ToBoolean(Config.Preference.Music.StartInGame)) + ModBase.RunInUi(() => + { + if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在结束后暂停音乐播放"); + }); + // 开始视频背景播放 + ModVideoBack.IsGaming = false; + ModVideoBack.VideoPlay(); + // 启动器可见性 + switch (Config.Launch.LauncherVisibility) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 2, false): + { + // 直接关闭 + if (TriggerLauncherShutdown) + ModBase.RunInUi(() => ModMain.FrmMain.EndProgram(false)); + else + ModBase.RunInUi(() => ModMain.FrmMain.Hidden = false); + + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 3, false): + { + // 恢复 + ModBase.RunInUi(() => ModMain.FrmMain.Hidden = false); + break; + } + } + } + + private static GameLogLevel GetLevel(string line, GameLogLevel lastLevel) + { + Func GetColorBrush = + name => (SolidColorBrush)System.Windows.Application.Current.Resources[name]; + var Starting = line.Split(": ")[0]; + if (Starting.ContainsF("FATAL")) + return GameLogLevel.Fatal; + if (Starting.ContainsF("ERROR")) + return GameLogLevel.Error; + if (Starting.ContainsF("WARN")) + return GameLogLevel.Warn; + if (Starting.ContainsF("INFO")) + return GameLogLevel.Info; + if (Starting.ContainsF("DEBUG")) + return GameLogLevel.Debug; + if (line.StartsWithF("Exception in thread \"")) + return GameLogLevel.Error; + if ((line.ContainsF("Exception") || line.ContainsF("Realms authentication error with message ")) && + lastLevel >= GameLogLevel.Warn) + return lastLevel; + if (line.StartsWithF(" at ") && lastLevel >= GameLogLevel.Warn) + return lastLevel; + return GameLogLevel.Info; + } + + private static SolidColorBrush GetColor(GameLogLevel level) + { + Func GetColorBrush = + name => (SolidColorBrush)System.Windows.Application.Current.Resources[name]; + switch (level) + { + case GameLogLevel.Debug: + { + return GetColorBrush("ColorBrushDebug"); + } + case GameLogLevel.Info: + { + GetColorBrush(ModSecret.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo"); + break; + } + case GameLogLevel.Warn: + { + return GetColorBrush("ColorBrushWarn"); + } + case GameLogLevel.Error: + { + return GetColorBrush("ColorBrushError"); + } + case GameLogLevel.Fatal: + { + return GetColorBrush("ColorBrushFatal"); + } + } + + return GetColorBrush(ModSecret.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo"); + } + + // 实时日志处理 + public class LogOutputEventArgs : EventArgs + { + public SolidColorBrush Color; + public string LogText; + + public LogOutputEventArgs(string LogText, SolidColorBrush Color) + { + this.LogText = LogText; + this.Color = Color; + } + } + + private enum GameLogLevel + { + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, + Fatal = 4 + } + + // 对单个进程的监视 + public class Watcher + { + public delegate void GameExitEventHandler(); + + public delegate void LogOutputEventHandler(Watcher sender, LogOutputEventArgs e); + + public enum MinecraftState + { + Loading, + Running, + Crashed, + Ended, + Canceled + } + + private readonly int PID; + + /// + /// 是否处理实时日志。 + /// + private readonly bool RealTime; + + private readonly object WaitingLogLock = new(); + private MinecraftState _State = MinecraftState.Loading; + public uint CountDebug; + public uint CountError; + public uint CountFatal; + public uint CountInfo; + public uint CountWarn; + + /// + /// 游戏的所有日志输出,只有处理实时日志的情况下才会记录。 + /// + public List FullLog = new(); + + // 初始化 + public Process GameProcess; + + // 窗口检查 + private bool IsWindowAppeared; + + /// + /// 窗口检查是否已经完成。这不一定代表着找到了窗口(如果没有找到,IsWindowAppeared 仍为 False)。 + /// + private bool IsWindowFinished; + + public string JStackPath; + + /// + /// 上一行日志级别。 + /// + private GameLogLevel LastLevel = GameLogLevel.Info; + + public Queue LatestLog = new(); + public ModLoader.LoaderTask Loader; + + // 进度更新 + private int LogProgress; + public ModMinecraft.McInstance Version; + + // 日志 + public List WaitingLog = new(1000); + private nint WindowHandle; + private string WindowTitle = ""; + + public Watcher(ModLoader.LoaderTask Loader, ModMinecraft.McInstance Version, string WindowTitle, + string JStackPath, bool OutputRealTime = false) + { + this.Loader = Loader; + this.Version = Version; + this.WindowTitle = WindowTitle; + RealTime = OutputRealTime; + PID = Loader.Input.Id; + this.JStackPath = JStackPath; + + WatcherLog("开始 Minecraft 日志监控"); + if (string.IsNullOrWhiteSpace(WindowTitle)) + WatcherLog("要求窗口标题:" + WindowTitle); + + // 更改列表 + var NewWatcherList = new List(); + foreach (var Watch in McWatcherList) + { + if (Watch.State == MinecraftState.Crashed || Watch.State == MinecraftState.Ended || + Watch.State == MinecraftState.Canceled) + continue; + NewWatcherList.Add(Watch); + } + + NewWatcherList.Add(this); + McWatcherList = NewWatcherList; + WatcherStateChanged(); + + // 初始化进程与日志读取 + GameProcess = Loader.Input; + GameProcess.BeginOutputReadLine(); + GameProcess.BeginErrorReadLine(); + GameProcess.OutputDataReceived += LogReceived; + GameProcess.ErrorDataReceived += LogReceived; + + // 初始化时钟 + // 设置窗口标题 + + ModBase.RunInNewThread(() => + { + try + { + while (State != MinecraftState.Ended && State != MinecraftState.Crashed && + State != MinecraftState.Canceled && Loader.State != ModBase.LoadState.Aborted) + { + TimerWindow(); + TimerLog(); + if (!string.IsNullOrWhiteSpace(WindowTitle)) + for (var i = 1; i <= 3; i++) + { + if (State == MinecraftState.Running && !GameProcess.HasExited) + { + var RealTitle = WindowTitle.Replace("{date}", DateTime.Now.ToString("yyyy'/'M'/'d")) + .Replace("{time}", DateTime.Now.ToString("HH':'mm':'ss")); + SetWindowText(WindowHandle, RealTitle); + } + + Thread.Sleep(64); + } + + Thread.Sleep(10); + } + + WatcherLog("Minecraft 日志监控已退出"); + } + catch (Exception ex) + { + ModBase.Log(ex, "Minecraft 日志监控主循环出错", ModBase.LogLevel.Feedback); + State = MinecraftState.Ended; + } + }, "Minecraft Watcher PID " + PID); + } + + public MinecraftState State + { + get => _State; + set + { + if (_State == value) + return; + _State = value; + WatcherStateChanged(); + } + } + + /// + /// 是否处理实时日志。 + /// + public bool RealTimeLog => RealTime; + + // 状态 + /// + /// 游戏退出时触发。 + /// + public event GameExitEventHandler? GameExit; + + private void LogReceived(object sender, DataReceivedEventArgs e) + { + lock (WaitingLogLock) + { + WaitingLog.Add(e.Data); + } + + if (RealTime) + { + LogRealTime(e.Data, ref LastLevel); + if (e.Data is not null) + FullLog.Add(e.Data); + } + } + + /// + /// 触发日志改变事件,并统计日志行数。 + /// + private void LogRealTime(string line, ref GameLogLevel level) + { + if (line is null) + return; // 杀游戏进程时有概率传 null + level = line.StartsWithF(" at ") || line.StartsWithF("Caused by: ") || line.StartsWithF(" ... ") + ? level + : GetLevel(line, level); + + // “ ... 4 more” + var color = GetColor(level); + switch (level) + { + case GameLogLevel.Debug: + { + CountDebug = (uint)(CountDebug + 1L); + break; + } + case GameLogLevel.Info: + { + CountInfo = (uint)(CountInfo + 1L); + break; + } + case GameLogLevel.Warn: + { + CountWarn = (uint)(CountWarn + 1L); + break; + } + case GameLogLevel.Error: + { + CountError = (uint)(CountError + 1L); + break; + } + case GameLogLevel.Fatal: + { + CountFatal = (uint)(CountFatal + 1L); + break; + } + } + + LogOutput?.Invoke(this, new LogOutputEventArgs(line, color)); + } + + /// + /// 有新的日志输出,日志计数器发生改变时触发。 + /// + public event LogOutputEventHandler? LogOutput; + + private void TimerLog() + { + try + { + // 输出文本 + var Copyed = new List(); + lock (WaitingLogLock) + { + if (!WaitingLog.Any()) + return; + Copyed = WaitingLog; + WaitingLog = new List(1000); + } + + foreach (var Str in Copyed) + GameLog(Str); + if (State == MinecraftState.Loading) + ProgressUpdate(); + // 游戏退出检查 + if (GameProcess.HasExited) + { + WatcherLog("Minecraft 已退出,返回值:" + GameProcess.ExitCode); + // 实时日志输出 + if (RealTime) + { + var arglevel = GameLogLevel.Info; + LogRealTime($"Minecraft 已退出,返回值:{GameProcess.ExitCode}", ref arglevel); + } + + GameExit?.Invoke(); + // If Process.ExitCode = 1 Then + // '返回值为 1,考虑是任务管理器结束 + // WatcherLog("Minecraft 返回值为 1,考虑为任务管理器结束") '并不,崩了照样是 1 + // State = MinecraftState.Ended + // Else + if (State == MinecraftState.Loading) + { + // 窗口未出现 + WatcherLog("Minecraft 尚未加载完成,可能已崩溃"); + Crashed(); + } + else if (GameProcess.ExitCode != 0 && State == MinecraftState.Running && + Version.ReleaseTime.Year >= 2012) + { + // 返回值不为 0 且未结束 + WatcherLog("Minecraft 返回值异常,可能已崩溃"); + Crashed(); + } + else if (State != MinecraftState.Crashed) + { + // 正常关闭 + State = MinecraftState.Ended; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "输出 Minecraft 日志失败", ModBase.LogLevel.Feedback); + } + } + + private void GameLog(string Text) + { + // 预处理 + if (Text is null) + return; + Text = Text.Replace("\r\n", "\r").Replace("\n", "\r") + .Replace("\r", "\r\n"); + // If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text) + // 加入预存储 + LatestLog.Enqueue(Text); + if (LatestLog.Count >= 501) + LatestLog.Dequeue(); + // 进度处理 + if (LogProgress < 1) + { + WatcherLog("日志 1/5:已出现日志输出"); + LogProgress = 1; + } // 可能第一句就是后面需要判断的 Log(重现:启动 1.15.2 原版) + + if (LogProgress < 2 && Text.Contains("Setting user:")) + { + WatcherLog("日志 2/5:游戏用户已设置"); // 仅确保支持 Minecraft 1.7+ + LogProgress = 2; + } + else if (LogProgress < 3 && Text.ContainsF("lwjgl version", true)) + { + WatcherLog("日志 3/5:LWJGL 版本已确认"); + LogProgress = 3; + } + else if (LogProgress < 4 && + (Text.Contains("OpenAL initialized") || Text.Contains("Starting up SoundSystem"))) + { + WatcherLog("日志 4/5:OpenAL 已加载"); // 仅确保支持 Minecraft 1.7+ + LogProgress = 4; + } + else if (LogProgress < 5 && + ((Text.Contains("Created") && Text.Contains("textures") && Text.Contains("-atlas")) || + Text.Contains("Found animation info"))) + { + WatcherLog("日志 5/5:材质已加载"); // 仅确保支持 Minecraft 1.7+ + LogProgress = 5; + } + + // 输出日志 + // Log(Text) + // 关闭与崩溃检测 + if (!Text.Contains("[CHAT]")) + { + if (Text.Contains("Someone is closing me!") || + Text.Contains("Restarting Minecraft with command")) // #1258 + { + WatcherLog("识别为关闭的 Log:" + Text); + State = MinecraftState.Ended; + } + else if (Text.Contains("Crash report saved to") || + Text.Contains("This crash report has been saved to:")) + { + // Text.Contains("Minecraft ran into a problem! Report saved to:") Then + // Minecraft 崩溃,忽略 VanillaFix + WatcherLog("识别为崩溃的 Log:" + Text); + Crashed(); + } + else if (Text.Contains("Could not save crash report to")) + { + // Minecraft 崩溃,无法保存崩溃日志 + WatcherLog("识别为崩溃的 Log:" + Text); + Crashed(); + } + else if (Text.Contains("/ERROR]: Unable to launch") || + Text.Contains("An exception was thrown, the game will display an error screen and halt.")) + { + // Forge 崩溃 + WatcherLog("识别为崩溃的 Log:" + Text); + Crashed(); + // ElseIf Text.Contains("Shutdown failure!") Then + // 'Minecraft 强行崩溃,由于点 X 强行关闭也会触发这句话,所以不可用 + // Crashed(Nothing) + } + } + } + + private void WatcherLog(string Text) + { + ModLaunch.McLaunchLog("[" + PID + "] " + Text); + } + + private void ProgressUpdate() + { + double CurrentProgress; + if (IsWindowAppeared || LogProgress >= 4) + { + CurrentProgress = 0.95d; + WatcherLog("Minecraft 加载已完成"); + State = MinecraftState.Running; + } + else + { + CurrentProgress = Math.Min(LogProgress, 3) / 3d * 0.9d; + } + + Loader.Progress = CurrentProgress; + } + + private void TimerWindow() + { + try + { + if (GameProcess.HasExited) + return; + if (IsWindowFinished) + return; + // 获取全部窗口,检查是否有新增的 + KeyValuePair? MinecraftWindow = default; + try + { + MinecraftWindow = TryGetMinecraftWindow(); + } + catch (Win32Exception ex) + { + // 拒绝访问(#1062) + ModBase.Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", ModBase.LogLevel.Hint); + IsWindowFinished = true; + } + + if (MinecraftWindow is null) + return; + var MinecraftWindowName = MinecraftWindow.Value.Value; + var MinecraftWindowHandle = MinecraftWindow.Value.Key; + // 已找到窗口 + if (!MinecraftWindowName.StartsWithF("FML") && !MinecraftWindowName.StartsWithF("Quilt Loader")) + { + // 已找到 Minecraft 窗口 + WindowHandle = MinecraftWindowHandle; + WatcherLog($"Minecraft 窗口已加载:{MinecraftWindowName}({MinecraftWindowHandle.ToInt64()})"); + IsWindowFinished = true; + // 最大化 + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.GameWindowMode, 4, + false))) + // 如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug + // ……虽然我很想这样说,但总有人反馈,算了 + ModBase.RunInNewThread(() => + { + try + { + Thread.Sleep(2000); + ShowWindow(WindowHandle, 3U); + WatcherLog($"已最大化 Minecraft 窗口:{MinecraftWindowHandle.ToInt64()}"); + } + catch (Exception ex) + { + ModBase.Log(ex, "最大化 Minecraft 窗口时出现错误"); + } + }, "MinecraftWindowMaximize"); + } + else if (!IsWindowAppeared) + { + // 已找到 FML 窗口 + WatcherLog("FML 窗口已加载:" + MinecraftWindowName + "(" + MinecraftWindowHandle.ToInt64() + ")"); + } + + IsWindowAppeared = true; + } + catch (Exception ex) + { + ModBase.Log(ex, "检查 Minecraft 窗口失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取可能是当前进程对应的 Minecraft 窗口的句柄和标题。 + /// Nothing 代表未找到。 + /// + private KeyValuePair? TryGetMinecraftWindow() + { + KeyValuePair? TryGetMinecraftWindowRet = default; + TryGetMinecraftWindowRet = default; + EnumWindows((hwnd, lParam) => + { + if (TryGetMinecraftWindowRet is not null) + return false; // 找到后停止枚举 + + var str = new StringBuilder(512); + GetClassName(hwnd, str, str.Capacity); + var ClassName = str.ToString(); + + if (!(ClassName == "GLFW30" || ClassName == "LWJGL" || ClassName == "SunAwtFrame")) + return true; + + // 获取窗口标题名 + str = new StringBuilder(512); + GetWindowText(hwnd, str, str.Capacity); + var WindowText = str.ToString(); + + // 部分版本会搞个 GLFW message window 出来所以得反选 + if (!(WindowText.StartsWithF("FML") || + (WindowText != "PopupMessageWindow" && !WindowText.StartsWithF("GLFW")))) + return true; + + // 获取窗口关联的进程 + var ProcessId = default(int); + GetWindowThreadProcessId(hwnd, ref ProcessId); + try + { + if (ProcessId != GameProcess.Id) + return true; + } + catch (Exception ex) + { + return true; + } + + // 找到目标,赋值并停止枚举 + TryGetMinecraftWindowRet = new KeyValuePair(hwnd, WindowText); + return false; + }, nint.Zero); + return TryGetMinecraftWindowRet; + } + + [DllImport("user32")] + private static extern bool EnumWindows(EnumWindowsSub lpEnumFunc, nint lParam); + + [DllImport("user32", EntryPoint = "GetClassNameW", CharSet = CharSet.Unicode)] + private static extern int GetClassName(nint hWnd, StringBuilder lpClassName, int nMaxCount); + + [DllImport("user32", EntryPoint = "GetWindowTextW", CharSet = CharSet.Unicode)] + private static extern int GetWindowText(nint hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32", EntryPoint = "SetWindowTextW", CharSet = CharSet.Unicode)] + private static extern bool SetWindowText(nint hWnd, string lpString); + + [DllImport("user32")] + private static extern bool ShowWindow(nint hWnd, uint cmdWindow); + + [DllImport("user32")] + private static extern int GetWindowThreadProcessId(nint hWnd, ref int lpdwProcessId); + + // 崩溃处理 + private void Crashed() + { + if (State == MinecraftState.Crashed || State == MinecraftState.Ended) + return; + State = MinecraftState.Crashed; + // 崩溃分析 + WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析"); + ModMain.Hint("检测到 Minecraft 出现错误,错误分析已开始……"); + ModBase.FeedbackInfo(); + ModBase.RunInNewThread(() => + { + try + { + Thread.Sleep(2000); + WatcherLog("崩溃分析开始"); + ; + var Analyzer = new CrashAnalyzer(PID); + Analyzer.Collect(Version.PathIndie, LatestLog.ToList()); + Analyzer.Prepare(); + Analyzer.Analyze(Version); + Analyzer.Output(false, + new List + { + Version.PathInstance + Version.Name + ".json", + LogWrapper.CurrentLogger.CurrentLogFiles.Last(), ModBase.ExePath + @"PCL\LatestLaunch.bat" + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "崩溃分析失败", ModBase.LogLevel.Feedback); + } + }, "Crash Analyzer"); + } + + // 强制关闭 + public bool CheckAlive(Process p) + { + if (!p.HasExited) + return true; + var exists = Array.Exists(Process.GetProcesses(), item => item.Id == p.Id); + if (exists) + return true; + return false; + } + + public void Kill() + { + State = MinecraftState.Canceled; + ModBase.RunInNewThread(() => + { + WatcherLog("尝试强制结束 Minecraft 进程"); + try + { + if (CheckAlive(GameProcess)) + GameProcess.Kill(); + GameProcess.WaitForExit(5000); + if (CheckAlive(GameProcess)) + { + WatcherLog("进程仍未退出,尝试使用 taskkill.exe"); + var taskkillProcess = Process.Start("taskkill.exe", $"/PID {GameProcess.Id} /F /T"); + var output = taskkillProcess.StandardOutput.ReadToEnd(); + ModBase.Log($"执行 taskkill.exe 结果: {output}"); + GameProcess.WaitForExit(5000); + if (CheckAlive(GameProcess)) + { + WatcherLog("强制结束 Minecraft 进程失败: 等待进程退出超时"); + return; + } + } + + WatcherLog("已强制结束 Minecraft 进程"); + if (RealTime) + { + var arglevel = GameLogLevel.Info; + LogRealTime($"Minecraft 已退出,返回值:{GameProcess.ExitCode}", ref arglevel); + } + + GameExit?.Invoke(); + } + catch (Exception ex) + { + ModBase.Log(ex, "强制结束 Minecraft 进程失败", ModBase.LogLevel.Hint); + } + }); + } + + // 导出运行栈 + public List ExportStackDump(string SavePath) + { + var Dump = new List(); + for (var i = 1; i <= 3; i++) + { + Dump.Add(ModBase.ShellAndGetOutput(JStackPath, "-l -e " + GameProcess.Id)); + Thread.Sleep(3000); + } + + return Dump; + } + + private delegate bool EnumWindowsSub(nint hwnd, nint lParam); + } +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModWorld.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModWorld.cs new file mode 100644 index 000000000..fc3add076 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModWorld.cs @@ -0,0 +1,125 @@ +using System.IO; +using System.Text; +using fNbt; +using PCL.Core.Utils; + +namespace PCL; + +public static class ModWorld +{ + #region 压缩包处理 + + /// + /// 尝试处理存档。 + /// + /// 确定这是一个存档文件(夹),但存档文件损坏时抛出的异常。 + /// + public static void ReadWorld(string SavePath) + { + if (File.Exists(SavePath)) + { + var ExtractPath = $@"{ModBase.PathTemp}Cache\{RandomUtils.NextInt(0, 1000_0000)}\"; + if (Directory.Exists(ExtractPath)) + ModBase.DeleteDirectory(ExtractPath); + ModBase.ExtractFile(SavePath, ExtractPath); + SavePath = ExtractPath; + } + + var world = new McWorld(SavePath); + if (!File.Exists(world.LevelDatPath)) + throw new Exception("无效的 Minecraft 存档"); + if (!world.Read()) + { + ModMain.Hint("存档文件可能已损坏,无法读取!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + var sb = new StringBuilder(); + if (world.VersionName is not null) + sb.AppendLine($"存档版本:{world.VersionName}"); + if (world.VersionId is not null) + sb.AppendLine($"存档数据版本:{world.VersionId}"); + if (sb.Length == 0) + sb.AppendLine("无法获取存档的版本信息,存档版本可能低于 15w32a(对应正式版 1.9)!"); + ModMain.MyMsgBox(sb.ToString(), "存档版本信息"); + } + + #endregion + + #region 存档 + + /// + /// 存档。 + /// + public class McWorld + { + /// + /// 存档路径。文件夹,以 “\” 结尾。 + /// + public string SavePath; + + /// + /// 版本 ID。 + /// + public string VersionId; + + /// + /// 版本名。 + /// + public string VersionName; + + /// + /// 存档。 + /// + /// 存档路径。文件夹,以 “\” 结尾。 + public McWorld(string SavePath) + { + if (!SavePath.EndsWithF(@"\")) + SavePath = SavePath + @"\"; + this.SavePath = SavePath; + } + + public string LevelDatPath => + File.Exists(SavePath + "level.dat") ? SavePath + "level.dat" : SavePath + "level.dat_old"; + + /// + /// 读取存档。返回是否成功。 + /// + public bool Read() + { + try + { + ModBase.Log($"[World] 读取存档:{SavePath}"); + if (!File.Exists(LevelDatPath)) + { + ModBase.Log("[World] 存档没有 level.dat 文件,读取失败"); + return false; + } + + using (var fs = new FileStream(LevelDatPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var gameData = new NbtFile(); + gameData.LoadFromStream(fs, NbtCompression.AutoDetect); + var gameVersion = gameData.RootTag.Get("Version"); + if (gameVersion is null) + { + ModBase.Log("[World] Version 标签存在问题,读取失败"); + return false; + } + + VersionName = gameVersion.Get("Name").Value; + VersionId = gameVersion.Get("Id").Value.ToString(); + } + + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取存档时出错"); + return false; + } + } + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/ModDevelop.cs b/Plain Craft Launcher 2/Modules/ModDevelop.cs new file mode 100644 index 000000000..01414ac89 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModDevelop.cs @@ -0,0 +1,13 @@ +namespace PCL; + +public static class ModDevelop +{ + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUGRESERVED Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + Public Sub Start() + End Sub + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/ModEvent.cs b/Plain Craft Launcher 2/Modules/ModEvent.cs new file mode 100644 index 000000000..797ff5a38 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModEvent.cs @@ -0,0 +1,332 @@ +using System.Diagnostics; +using System.IO; +using Microsoft.VisualBasic; +using PCL.Core.App; + +namespace PCL; + +public static class ModEvent +{ + public static void TryStartEvent(string Type, string Data) + { + if (string.IsNullOrWhiteSpace(Type)) + return; + var RealData = new[] { "" }; + if (Data is not null) + RealData = Data.Split("|"); + StartEvent(Type, RealData); + } + + public static void StartEvent(string Type, string[] Data) + { + try + { + ModBase.Log("[Control] 执行自定义事件:" + Type + ", " + Data.Join(", ")); + switch (Type ?? "") + { + case "打开网页": + { + Data[0] = Data[0].Replace(@"\", "/"); + if (!Data[0].Contains("://") || Data[0].StartsWithF("file", true)) // 为了支持更多协议(#2200) + { + ModMain.MyMsgBox("EventData 必须为一个网址。" + "\r\n" + "如果想要启动程序,请将 EventType 改为 打开文件。", + "事件执行失败"); + return; + } + + ModMain.Hint("正在开启中,请稍候……"); + ModBase.OpenWebsite(Data[0]); + break; + } + + case "打开文件": + case "打开帮助": + case "执行命令": + { + ModBase.RunInThread(() => + { + try + { + // 确认实际路径 + var ActualPaths = GetEventAbsoluteUrls(Data[0], Type); + var Location = ActualPaths[0]; + var WorkingDir = ActualPaths[1]; + ModBase.Log($"[Control] 打开类自定义事件实际路径:{Location},工作目录:{WorkingDir}"); + // 执行 + if (Type == "打开帮助") + { + PageToolsHelp.EnterHelpPage(Location); + } + else + { + if (States.Hint.HomepageCommand) + switch (ModMain.MyMsgBox( + "即将执行:" + Location + (Data.Length >= 2 ? " " + Data[1] : "") + + "\r\n" + "请在确认该操作没有安全隐患后继续。", "执行确认", "继续", "继续且今后不再要求确认", + "取消")) + { + case 2: + { + States.Hint.HomepageCommand = true; + break; + } + case 3: + { + return; + } + } + + var Info = new ProcessStartInfo + { + Arguments = Data.Length >= 2 ? Data[1] : "", + FileName = Location, + WorkingDirectory = ModBase.ShortenPath(WorkingDir), + UseShellExecute = true + }; + Process.Start(Info); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "执行打开类自定义事件失败", ModBase.LogLevel.Msgbox); + } + }); + break; + } + + case "启动游戏": + { + if (Data[0] == @"\current") + { + if (ModMinecraft.McInstanceSelected is null) + { + ModMain.Hint("请先选择一个 Minecraft 实例!", ModMain.HintType.Critical); + return; + } + + Data[0] = ModMinecraft.McInstanceSelected.Name; + } + + if (ModLaunch.McLaunchStart(new ModLaunch.McLaunchOptions + { + ServerIp = Data.Length >= 2 ? Data[1] : null, + Instance = new ModMinecraft.McInstance(Data[0]) + })) ModMain.Hint("正在启动 " + Data[0] + "……"); + + break; + } + + case "复制文本": + { + ModBase.ClipboardSet(Data.Join("|")); + break; + } + + case "刷新主页": + { + ModMain.FrmLaunchRight.ForceRefresh(); + if (string.IsNullOrEmpty(Data[0])) + ModMain.Hint("已刷新主页!", ModMain.HintType.Finish); + break; + } + + case "刷新主页市场": + { + ModMain.FrmHomePageMarket.Refresh(); + if (string.IsNullOrEmpty(Data[0])) + ModMain.Hint("已刷新主页市场!", ModMain.HintType.Finish); + break; + } + + case "刷新帮助": + { + PageToolsLeft.RefreshHelp(); + break; + } + + case "今日人品": + { + PageToolsTest.Jrrp(); + break; + } + + case "内存优化": + { + ModBase.RunInThread(() => PageToolsTest.MemoryOptimize(true)); + break; + } + + case "清理垃圾": + { + ModBase.RunInThread(() => PageToolsTest.RubbishClear()); + break; + } + + case "弹出窗口": + { + ModMain.MyMsgBox(Data[1].Replace(@"\n", "\r\n"), + Data[0].Replace(@"\n", "\r\n")); + break; + } + + case "切换页面": + { + ModMain.FrmMain.PageChange((dynamic)ModBase.Val(Data[0]), + (FormMain.PageSubType)ModBase.Val(Data[1])); + break; + } + + case "导入整合包": + case "安装整合包": + { + ModBase.RunInUi(() => ModModpack.ModpackInstall()); + break; + } + + case "下载文件": + { + Data[0] = Data[0].Replace(@"\", "/"); + if (!(Data[0].StartsWithF("http://", true) || Data[0].StartsWithF("https://", true))) + { + ModMain.MyMsgBox( + "EventData 必须为以 http:// 或 https:// 开头的网址。" + "\r\n" + "PCL 不支持其他乱七八糟的下载协议。", + "事件执行失败"); + return; + } + + try + { + switch (Data.Length) + { + case 1: + { + PageToolsTest.StartCustomDownload(Data[0], ModBase.GetFileNameFromPath(Data[0])); + break; + } + case 2: + { + PageToolsTest.StartCustomDownload(Data[0], Data[1]); + break; + } + + default: + { + PageToolsTest.StartCustomDownload(Data[0], Data[1], Data[2]); + break; + } + } + } + catch + { + PageToolsTest.StartCustomDownload(Data[0], "未知"); + } + + break; + } + + default: + { + ModMain.MyMsgBox("未知的事件类型:" + Type + "\r\n" + "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败"); + break; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "事件执行失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 返回自定义事件的绝对 Url。实际返回 {绝对 Url, WorkingDir}。 + /// 失败会抛出异常。 + /// + public static string[] GetEventAbsoluteUrls(string RelativeUrl, string EventType) + { + // 网页确认 + if (RelativeUrl.StartsWithF("http", true)) + { + if (ModBase.RunInUi()) throw new Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!"); + // 获取文件名 + string RawFileName; + try + { + RawFileName = ModBase.GetFileNameFromPath(RelativeUrl); + if (!RawFileName.EndsWithF(".json", true)) + throw new Exception("未指向 .json 后缀的文件"); + } + catch (Exception ex) + { + throw new Exception( + "联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" + "\r\n" + "例如:" + "\r\n" + + " - https://www.baidu.com/test.json(填写这个路径)" + "\r\n" + + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", ex); + } + + // 下载文件 + var LocalTemp = ModMain.RequestTaskTempFolder() + RawFileName; + ModBase.Log("[Event] 转换网络资源:" + RelativeUrl + " -> " + LocalTemp); + try + { + ModNet.NetDownloadByClient(RelativeUrl, LocalTemp).GetAwaiter().GetResult(); + ModNet.NetDownloadByClient(RelativeUrl.Replace(".json", ".xaml"), LocalTemp.Replace(".json", ".xaml")) + .GetAwaiter().GetResult(); + } + catch (Exception ex) + { + throw new Exception( + "下载指定的文件失败!" + "\r\n" + "注意,联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" + + "\r\n" + "例如:" + "\r\n" + " - https://www.baidu.com/test.json(填写这个路径)" + + "\r\n" + " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", ex); + } + + RelativeUrl = LocalTemp; + } + + RelativeUrl = RelativeUrl.Replace("/", @"\").ToLower().TrimStart('\\'); + + // 确认实际路径 + string Location; + var WorkingDir = ModBase.ExePath + "PCL"; + ModMain.HelpExtract(); + if (RelativeUrl.Contains(@":\")) + { + // 绝对路径 + Location = RelativeUrl; + ModBase.Log("[Control] 自定义事件中由绝对路径" + EventType + ":" + Location); + } + else if (File.Exists(ModBase.ExePath + @"PCL\" + RelativeUrl)) + { + // 相对 PCL 文件夹的路径 + Location = ModBase.ExePath + @"PCL\" + RelativeUrl; + ModBase.Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" + EventType + ":" + Location); + } + else if (File.Exists(ModBase.ExePath + @"PCL\Help\" + RelativeUrl)) + { + // 相对 PCL 本地帮助文件夹的路径 + Location = ModBase.ExePath + @"PCL\Help\" + RelativeUrl; + WorkingDir = ModBase.ExePath + @"PCL\Help\"; + ModBase.Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" + EventType + ":" + Location); + } + else if (EventType == "打开帮助" && File.Exists(ModBase.PathHelpFolder + RelativeUrl)) + { + // 相对 PCL 自带帮助文件夹的路径 + Location = ModBase.PathHelpFolder + RelativeUrl; + WorkingDir = ModBase.PathHelpFolder; + ModBase.Log("[Control] 自定义事件中由相对 PCL 自带帮助文件夹的路径" + EventType + ":" + Location); + } + else if (EventType == "打开文件" || EventType == "执行命令") + { + // 直接使用原有路径启动程序 + Location = RelativeUrl; + ModBase.Log("[Control] 自定义事件中直接" + EventType + ":" + Location); + } + else + { + // 打开帮助,但是格式不对劲 + throw new FileNotFoundException("未找到 EventData 指向的本地 xaml 文件:" + RelativeUrl, RelativeUrl); + } + + return new[] { Location, WorkingDir }; + } +} diff --git a/Plain Craft Launcher 2/Modules/ModLink.cs b/Plain Craft Launcher 2/Modules/ModLink.cs new file mode 100644 index 000000000..1e658adde --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModLink.cs @@ -0,0 +1,397 @@ +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.Link.EasyTier; +using PCL.Core.Link.Lobby; +using PCL.Core.Link.McPing; +using PCL.Core.Link.McPing.Model; +using PCL.Core.Link.Natayark; +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.Utils.OS; + +namespace PCL; + +public static class ModLink +{ + #region 大厅操作 + + public static bool LobbyPrecheck() + { + if (!LobbyInfoProvider.IsLobbyAvailable) + { + ModMain.Hint("大厅功能暂不可用,请稍后再试", ModMain.HintType.Critical); + return false; + } + + if (ModProfile.SelectedProfile is not null) + if (ModProfile.SelectedProfile.Username.Contains("|")) + { + ModMain.Hint("MC 玩家 ID 不可包含分隔符 (|) !"); + return false; + } + + if (LobbyInfoProvider.RequiresLogin) + { + if (string.IsNullOrWhiteSpace(Conversions.ToString(States.Link.NaidRefreshToken))) + { + ModMain.Hint("请先前往联机设置并登录至 Natayark Network 再进行联机!", ModMain.HintType.Critical); + return false; + } + + try + { + NatayarkProfileManager.GetNaidDataAsync((string)States.Link.NaidRefreshToken, true) + .GetAwaiter().GetResult(); + } + catch (Exception ex) + { + ModBase.Log("[Link] 刷新 Natayark ID 信息失败,需要重新登录"); + ModMain.Hint("请重新登录 Natayark Network 账号再试!", ModMain.HintType.Critical); + return false; + } + + var waitCount = 0; + while (string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username)) + { + if (waitCount > 30) + break; + Thread.Sleep(500); + waitCount += 1; + } + + if (string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username)) + { + ModMain.Hint("尝试获取 Natayark ID 信息失败", ModMain.HintType.Critical); + return false; + } + + if (LobbyInfoProvider.RequiresRealName && !NatayarkProfileManager.NaidProfile.IsRealNamed) + { + ModMain.Hint("请先前往 Natayark 账户中心进行实名验证再尝试操作!", ModMain.HintType.Critical); + return false; + } + + if (!(NatayarkProfileManager.NaidProfile.Status == 0)) + { + ModMain.Hint("你的 Natayark Network 账号状态异常,可能已被封禁!", ModMain.HintType.Critical); + return false; + } + } + + if (string.IsNullOrWhiteSpace(Conversions.ToString(Config.Link.Username)) && + string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username)) + { + ModMain.Hint("请先前往设置输入一个用户名,或登录至 Natayark Network 再进行联机!", ModMain.HintType.Critical); + return false; + } + + if (ETController.Precheck() == 1) + { + ModMain.Hint("正在下载联机依赖组件,请稍后..."); + DownloadEasyTier(); + return false; + } + + if (DlEasyTierLoader is not null) + { + if (DlEasyTierLoader.State == ModBase.LoadState.Loading) + { + ModMain.Hint("EasyTier 尚未下载完成,请等待其下载完成后再试!"); + return false; + } + + if (DlEasyTierLoader.State == ModBase.LoadState.Failed || + DlEasyTierLoader.State == ModBase.LoadState.Aborted) + { + ModMain.Hint("正在下载 EasyTier,请稍后..."); + DownloadEasyTier(); + return false; + } + } + + return true; + } + + #endregion + + #region 端口查找 + + public class PortFinder + { + [DllImport("iphlpapi.dll", SetLastError = true)] + public static extern int GetExtendedTcpTable(nint pTcpTable, ref int dwOutBufLen, bool bOrder, int ulAf, + int TableClass, int reserved); + + public static List GetProcessPort(int dwProcessId) + { + var ports = new List(); + var tcpTable = nint.Zero; + var dwSize = 0; + int dwRetVal; + + if (dwProcessId == 0) return ports; + + dwRetVal = GetExtendedTcpTable(nint.Zero, ref dwSize, true, 2, 3, 0); + if (dwRetVal != 0 && dwRetVal != 122) // 122 表示缓冲区不足 + return ports; + + tcpTable = Marshal.AllocHGlobal(dwSize); + try + { + if (GetExtendedTcpTable(tcpTable, ref dwSize, true, 2, 3, 0) != 0) return ports; + + var tablePtr = tcpTable; + var dwNumEntries = Marshal.ReadInt32(tablePtr); + tablePtr = nint.Add(tablePtr, 4); + + for (int i = 0, loopTo = dwNumEntries - 1; i <= loopTo; i++) + { + var row = Marshal.PtrToStructure(tablePtr); + if (row.dwOwningPid == dwProcessId) + ports.Add((row.dwLocalPort >> 8) | ((row.dwLocalPort & 0xFF) << 8)); // 转换端口号 + tablePtr = nint.Add(tablePtr, Marshal.SizeOf()); + } + } + finally + { + Marshal.FreeHGlobal(tcpTable); + } + + return ports; + } + + // 定义需要的结构和常量 + [StructLayout(LayoutKind.Sequential)] + public struct MIB_TCPROW_OWNER_PID + { + public int dwState; + public int dwLocalAddr; + public int dwLocalPort; + public int dwRemoteAddr; + public int dwRemotePort; + public int dwOwningPid; + } + } + + #endregion + + #region Minecraft 实例探测 + + public static async Task>> MCInstanceFinding() + { + // Java 进程 PID 查询 + var PIDLookupResult = new List(); + var JavaNames = new List(); + JavaNames.Add("java"); + JavaNames.Add("javaw"); + + foreach (var TargetJava in JavaNames) + { + var JavaProcesses = Process.GetProcessesByName(TargetJava); + ModBase.Log($"[MCDetect] 找到 {TargetJava} 进程 {JavaProcesses.Length} 个"); + + if (JavaProcesses is null || JavaProcesses.Length == 0) + { + } + else + { + foreach (var p in JavaProcesses) + { + ModBase.Log("[MCDetect] 检测到 Java 进程,PID: " + p.Id); + PIDLookupResult.Add(p.Id.ToString()); + } + } + } + + var res = new List>(); + try + { + if (PIDLookupResult.Count == 0) + return res; + var lookupList = new List>(); + foreach (var pid in PIDLookupResult) + { + var infos = new List>(); + var ports = PortFinder.GetProcessPort(int.Parse(pid)); + foreach (var port in ports) + infos.Add(new Tuple(port, Conversions.ToInteger(pid))); + lookupList.AddRange(infos); + } + + ModBase.Log($"[MCDetect] 获取到端口数量 {lookupList.Count}"); + // 并行查找本地,超时 3s 自动放弃 + var checkTasks = lookupList.Select( + lookup => Task.Run(async () => + { + ModBase.Log($"[MCDetect] 找到疑似端口,开始验证:{lookup}"); + using (var test = McPingServiceFactory.CreateService("127.0.0.1", lookup.Item1, 3000)) + { + try + { + var info = await test.PingAsync(); + var launcher = GetLauncherBrand(lookup.Item2); + if (!string.IsNullOrWhiteSpace(info?.Version.Name)) + { + ModBase.Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界"); + res.Add(new Tuple(lookup.Item1, info, launcher)); + return Task.CompletedTask; + } + } + catch (Exception ex) + { + if (ex.InnerException is ObjectDisposedException) + { + ModBase.Log($"[McDetect] {lookup} 验证超时,已强制断开连接,将尝试旧版检测"); + } + else + { + ModBase.Log(ex, $"[McDetect] {lookup} 验证出错,将尝试旧版检测"); + } + } + } + using (var test = McPingServiceFactory.CreateLegacyService("127.0.0.1", lookup.Item1, 3000)) + { + try + { + var info = await test.PingAsync(); + if (!string.IsNullOrWhiteSpace(info?.Version.Name)) + { + ModBase.Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界"); + res.Add(new Tuple(lookup.Item1, info, string.Empty)); + return Task.CompletedTask; + } + } + catch (Exception ex) + { + if (ex.InnerException is ObjectDisposedException) + { + ModBase.Log($"[McDetect] {lookup} 验证超时,已强制断开连接"); + } + else + { + ModBase.Log(ex, $"[McDetect] {lookup} 验证出错"); + } + } + } + return Task.CompletedTask; + })).ToArray(); + await Task.WhenAll(checkTasks); + } + catch (Exception ex) + { + ModBase.Log(ex, "[MCDetect] 获取端口信息错误"); + } + + return res; + } + + public static string GetLauncherBrand(int pid) + { + try + { + var cmd = ProcessInterop.GetCommandLine(pid); + if (cmd.Contains("-Dminecraft.launcher.brand=")) + return cmd.AfterFirst("-Dminecraft.launcher.brand=").BeforeFirst("-").TrimEnd('\'', ' '); + + return cmd.AfterFirst("--versionType ").BeforeFirst("-").TrimEnd('\'', ' '); + } + catch (Exception ex) + { + ModBase.Log(ex, $"[MCDetect] 检测 PID {pid} 进程的启动参数失败"); + return ""; + } + } + + #endregion + + #region EasyTier + + public static ModLoader.LoaderCombo DlEasyTierLoader; + + public static int DownloadEasyTier() + { + var dlTargetPath = $"{ModBase.PathTemp}EasyTier\\EasyTier-{ETInfoProvider.ETVersion}.zip"; + + Basics.RunInNewThread(() => + { + try + { + // Initialize loaders + var loaders = new List(); + + // Setup download addresses + var architecture = ModBase.IsArm64System ? "arm64" : "x86_64"; + var addresses = new List + { + $"https://staticassets.naids.com/resources/pclce/static/easytier/easytier-windows-{architecture}-v{ETInfoProvider.ETVersion}.zip", + $"https://s3.pysio.online/pcl2-ce/static/easytier/easytier-windows-{architecture}-v{ETInfoProvider.ETVersion}.zip" + }; + + // 1. Download EasyTier + loaders.Add(new ModNet.LoaderDownload("下载 EasyTier", new List + { + new(addresses.ToArray(), dlTargetPath, new ModBase.FileChecker(1024 * 64)) + }) { ProgressWeight = 15 }); + + // 2. Extract files + loaders.Add(new ModLoader.LoaderTask("解压文件", _ => + ModBase.ExtractFile(dlTargetPath, + Path.Combine(Paths.SharedLocalData, "EasyTier", ETInfoProvider.ETVersion)) + ) { Block = true }); + + // 3. Cleanup + loaders.Add(new ModLoader.LoaderTask("清理缓存与冗余组件", _ => + { + File.Delete(dlTargetPath); + CleanupEasyTierCache(); + })); + + // 4. Update UI hint + loaders.Add(new ModLoader.LoaderTask("刷新界面", _ => + HintWrapper.Show("联机组件下载完成!", HintTheme.Error) + ) { Show = false }); + + // Start loader combo + DlEasyTierLoader = new ModLoader.LoaderCombo("大厅初始化", loaders); + DlEasyTierLoader.Start(); + + // Taskbar and UI notification + ModLoader.LoaderTaskbarAdd(DlEasyTierLoader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + // Error handling with concise English logs + LogWrapper.Warn(ex, "Failed to download EasyTier dependency files"); + HintWrapper.Show("下载 EasyTier 依赖文件失败,请检查网络连接", HintTheme.Error); + } + }); + + return 0; + } + + private static void CleanupEasyTierCache() + { + var subDirs = Directory.GetDirectories(Path.Combine(Paths.SharedLocalData, "EasyTier")); + foreach (var folderPath in subDirs) + { + var name = Path.GetFileName(folderPath); + if (!name.Equals(ETInfoProvider.ETVersion)) + try + { + Directory.Delete(folderPath, true); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Link] 清理旧版本 EasyTier 出错"); + } + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/ModLink.vb b/Plain Craft Launcher 2/Modules/ModLink.vb index 0d2d27a22..9131c9ea4 100644 --- a/Plain Craft Launcher 2/Modules/ModLink.vb +++ b/Plain Craft Launcher 2/Modules/ModLink.vb @@ -1,9 +1,9 @@ Imports System.Runtime.InteropServices Imports PCL.Core.App -Imports PCL.Core.IO -Imports PCL.Core.Link Imports PCL.Core.Link.EasyTier Imports PCL.Core.Link.Lobby +Imports PCL.Core.Link.McPing +Imports PCL.Core.Link.McPing.Model Imports PCL.Core.Link.Natayark.NatayarkProfileManager Imports PCL.Core.Utils.OS @@ -109,41 +109,46 @@ Public Module ModLink Next Log($"[MCDetect] 获取到端口数量 {lookupList.Count}") '并行查找本地,超时 3s 自动放弃 - Dim checkTasks = lookupList.Select(Function(lookup) Task.Run(Async Function() - Log($"[MCDetect] 找到疑似端口,开始验证:{lookup}") - Using test As New McPing("127.0.0.1", lookup.Item1, 3000) - Dim info As McPingResult - Try - info = Await test.PingAsync() - Dim launcher = GetLauncherBrand(lookup.Item2) - If Not String.IsNullOrWhiteSpace(info.Version.Name) Then - Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界") - res.Add(New Tuple(Of Integer, McPingResult, String)(lookup.Item1, info, launcher)) - Return - End If - Catch ex As Exception - If TypeOf ex.InnerException Is ObjectDisposedException Then - Log($"[McDetect] {lookup} 验证超时,已强制断开连接,将尝试旧版检测") - Else - Log(ex, $"[McDetect] {lookup} 验证出错,将尝试旧版检测") - End If - End Try - Try - info = Await test.PingOldAsync() - If Not String.IsNullOrWhiteSpace(info.Version.Name) Then - Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界") - res.Add(New Tuple(Of Integer, McPingResult, String)(lookup.Item1, info, String.Empty)) - Return - End If - Catch ex As Exception - If TypeOf ex.InnerException Is ObjectDisposedException Then - Log($"[McDetect] {lookup} 验证超时,已强制断开连接") - Else - Log(ex, $"[McDetect] {lookup} 验证出错") - End If - End Try - End Using - End Function)).ToArray() + Dim checkTasks = lookupList.Select( + Function(lookup) Task.Run( + Async Function() + Log($"[MCDetect] 找到疑似端口,开始验证:{lookup}") + Using test = McPingServiceFactory.CreateService("127.0.0.1", lookup.Item1, 3000) + Dim info As McPingResult + Try + info = Await test.PingAsync() + Dim launcher = GetLauncherBrand(lookup.Item2) + If Not String.IsNullOrWhiteSpace(info.Version.Name) Then + Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界") + res.Add(New Tuple(Of Integer, McPingResult, String)(lookup.Item1, info, launcher)) + Return Task.CompletedTask + End If + Catch ex As Exception + If TypeOf ex.InnerException Is ObjectDisposedException Then + Log($"[McDetect] {lookup} 验证超时,已强制断开连接,将尝试旧版检测") + Else + Log(ex, $"[McDetect] {lookup} 验证出错,将尝试旧版检测") + End If + End Try + End Using + Using test = McPingServiceFactory.CreateLegacyService("127.0.0.1", lookup.Item1, 3000) + Try + Dim info = Await test.PingAsync() + If Not String.IsNullOrWhiteSpace(info.Version.Name) Then + Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界") + res.Add(New Tuple(Of Integer, McPingResult, String)(lookup.Item1, info, String.Empty)) + Return Task.CompletedTask + End If + Catch ex As Exception + If TypeOf ex.InnerException Is ObjectDisposedException Then + Log($"[McDetect] {lookup} 验证超时,已强制断开连接") + Else + Log(ex, $"[McDetect] {lookup} 验证出错") + End If + End Try + End Using + Return Task.CompletedTask + End Function)).ToArray() Await Task.WhenAll(checkTasks) Catch ex As Exception Log(ex, "[MCDetect] 获取端口信息错误", LogLevel.Debug) diff --git a/Plain Craft Launcher 2/Modules/ModMain.cs b/Plain Craft Launcher 2/Modules/ModMain.cs new file mode 100644 index 000000000..026125555 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModMain.cs @@ -0,0 +1,1514 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using PCL.Core.UI; +using PCL.Core.Utils; + +namespace PCL; + +public static class ModMain +{ + public static FormMain? FrmMain; + public static SplashScreen? FrmStart; + public static PageLaunchLeft? FrmLaunchLeft; + public static PageLaunchRight? FrmLaunchRight; + public static PageLogLeft? FrmLogLeft; + public static PageLogRight? FrmLogRight; + public static PageSelectLeft? FrmSelectLeft; + public static PageSelectRight? FrmSelectRight; + public static PageSpeedLeft? FrmSpeedLeft; + public static PageSpeedRight? FrmSpeedRight; + public static PageToolsLeft? FrmToolsLeft; + public static PageToolsGameLink? FrmToolsGameLink; + public static PageToolsHelp? FrmToolsHelp; + public static PageToolsTest? FrmToolsTest; + public static PageDownloadLeft? FrmDownloadLeft; + public static PageDownloadInstall? FrmDownloadInstall; + public static PageDownloadClient? FrmDownloadClient; + public static PageDownloadOptiFine? FrmDownloadOptiFine; + public static PageDownloadLiteLoader? FrmDownloadLiteLoader; + public static PageDownloadForge? FrmDownloadForge; + public static PageDownloadNeoForge? FrmDownloadNeoForge; + public static PageDownloadCleanroom? FrmDownloadCleanroom; + public static PageDownloadFabric? FrmDownloadFabric; + public static PageDownloadQuilt? FrmDownloadQuilt; + public static PageDownloadLabyMod? FrmDownloadLabyMod; + public static PageDownloadLegacyFabric? FrmDownloadLegacyFabric; + public static PageDownloadMod? FrmDownloadMod; + public static PageDownloadPack? FrmDownloadPack; + public static PageDownloadDataPack? FrmDownloadDataPack; + public static PageDownloadShader? FrmDownloadShader; + public static PageDownloadResourcePack? FrmDownloadResourcePack; + public static PageDownloadWorld? FrmDownloadWorld; + public static PageDownloadCompFavorites? FrmDownloadCompFavorites; + public static PageSetupLeft? FrmSetupLeft; + public static PageSetupLaunch? FrmSetupLaunch; + public static PageSetupUI? FrmSetupUI; + public static PageSetupGameManage? FrmSetupGameManage; + public static PageSetupUpdate? FrmSetupUpdate; + public static PageSetupJava? FrmSetupJava; + public static PageHomePageMarket? FrmHomePageMarket; + public static PageSetupAbout? FrmSetupAbout; + public static PageSetupLog? FrmSetupLog; + public static PageSetupFeedback? FrmSetupFeedback; + public static PageSetupGameLink? FrmSetupGameLink; + public static PageSetupLauncherMisc? FrmSetupLauncherMisc; + public static PageLoginAuth? FrmLoginAuth; + public static PageLoginMs? FrmLoginMs; + public static PageLoginProfile? FrmLoginProfile; + public static PageLoginProfileSkin? FrmLoginProfileSkin; + public static PageLoginOffline? FrmLoginOffline; + public static PageInstanceLeft? FrmInstanceLeft; + public static PageInstanceOverall? FrmInstanceOverall; + public static PageInstanceCompResource? FrmInstanceMod; + public static PageInstanceModDisabled? FrmInstanceModDisabled; + public static PageInstanceScreenshot? FrmInstanceScreenshot; + public static PageInstanceSaves? FrmInstanceSaves; + public static PageInstanceCompResource? FrmInstanceShader; + public static PageInstanceCompResource? FrmInstanceSchematic; + public static PageInstanceCompResource? FrmInstanceResourcePack; + public static PageInstanceSetup? FrmInstanceSetup; + public static PageInstanceInstall? FrmInstanceInstall; + public static PageInstanceExport? FrmInstanceExport; + public static PageInstanceServer? FrmInstanceServer; + public static PageInstanceSavesLeft? FrmInstanceSavesLeft; + public static PageInstanceSavesInfo? FrmInstanceSavesInfo; + public static PageInstanceSavesBackup? FrmInstanceSavesBackup; + public static PageInstanceSavesDatapack? FrmInstanceSavesDatapack; + public static PageDownloadCompDetail? FrmDownloadCompDetail; + + public static ModLoader.LoaderTask> HelpLoader = new("Help Page", HelpLoad, null, + ThreadPriority.BelowNormal); + + public static object? DragControl = null; + private static int Timer4Count; + private static int Timer150Count; + + /// + /// 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。 + /// + private static ModBase.SafeList HintWaiting + { + get => field ??= new ModBase.SafeList(); + set; + } + + /// + /// 等待显示的弹窗。 + /// + public static List WaitingMyMsgBox { get; } = []; + + private static void TimerMain() + { + try + { + #region 每 50ms 执行一次的代码 + + HintTick(); + MyMsgBoxTick(); + FrmMain!.DragTick(); + ModLoader.LoaderTaskbarProgressRefresh(); + if (ModSecret.ThemeDontClick == 2) + ModSecret.ThemeRefresh(); + } + + #endregion + + catch (Exception ex) + { + ModBase.Log(ex, "短程主时钟执行异常", ModBase.LogLevel.Critical); + } + + Timer4Count += 1; + if (Timer4Count == 4) + { + Timer4Count = 0; + try + { + #region 每 250ms 执行一次的代码 + + if (ModSecret.ThemeNow == 12) + ModSecret.ThemeRefresh(); + } + + #endregion + + catch (Exception ex) + { + ModBase.Log(ex, "中程主时钟执行异常"); + } + } + + Timer150Count += 1; + if (Timer150Count == 150) + { + Timer150Count = 0; + try + { + #region 每 7.5s 执行一次的代码 + + if (FrmMain!.BtnExtraApril_ShowCheck() && AprilDistance != 0) + FrmMain.BtnExtraApril.Ribble(); + // 以未知原因窗口被丢到一边去的修复(Top、Left = -25600),还有 #745 + ModBase.RunInUi(() => + { + if (!FrmMain.Hidden) + { + if (FrmMain.Top < -9000) FrmMain.Top = 100d; + if (FrmMain.Left < -9000) FrmMain.Left = 100d; + } + }); // 窗口拉至最大时 Left = -18.8 + } + + #endregion + + catch (Exception ex) + { + ModBase.Log(ex, "长程主时钟执行异常", ModBase.LogLevel.Critical); + } + } + } + + public static void TimerMainStart() + { + ModBase.RunInNewThread(() => + { + try + { + while (true) + { + ModBase.RunInUiWait(TimerMain); + Thread.Sleep((int)Math.Round(50d * 0.98d)); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "程序主时钟出错", ModBase.LogLevel.Feedback); + } + }, "Timer Main"); + if (!IsAprilEnabled) + return; + ModBase.RunInNewThread(() => + { + try + { + var LastTime = Environment.TickCount; + while (true) + { + if (LastTime != Environment.TickCount) + { + LastTime = Environment.TickCount; + ModBase.RunInUiWait(TimerFool); + } + + Thread.Sleep(1); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "愚人节主时钟出错", ModBase.LogLevel.Feedback); + } + }, "Timer Main Fool"); + } + + #region 弹出提示 + + /// + /// 提示信息的种类。 + /// + public enum HintType + { + /// + /// 信息,通常是蓝色的“i”。 + /// + /// + Info, + + /// + /// 已完成,通常是绿色的“√”。 + /// + /// + Finish, + + /// + /// 错误,通常是红色的“×”。 + /// + /// + Critical + } + + private struct HintMessage + { + public string Text; + public HintType Type; + public bool Log; + } + + + /// + /// 在窗口左下角弹出提示文本。 + /// + public static void Hint(string? Text, HintType Type = HintType.Info, bool Log = true) + { + HintWaiting.Add(new HintMessage { Text = Text ?? "", Type = Type, Log = Log }); + } + + public static void HintWrapper_OnShow(string message, HintTheme messageTheme) + { + var hintType = messageTheme switch + { + HintTheme.Error => HintType.Critical, + HintTheme.Info => HintType.Info, + _ => HintType.Finish + }; + Hint(message, hintType); + } + + private static void HintTick() + { + try + { + // Tag 存储了:{ 是否可以重用, Uuid } + if (!HintWaiting.Any()) + return; + while (HintWaiting.Any()) + { + // '清除空提示 + // If IsNothing(HintWaiting(0)) OrElse IsNothing(HintWaiting(0)(0)) Then + // HintWaiting.RemoveAt(0) + // Continue Do + // End If + var CurrentHint = HintWaiting[0]; + // 去回车 + CurrentHint.Text = CurrentHint.Text.Replace("\r\n", " ").Replace("\r", " ") + .Replace("\n", " "); + // 超量提示直接忽略 + if (FrmMain!.PanHint.Children.Count >= 20) + goto EndHint; + // 检查是否有重复提示 + Border? DoubleStack = null; + foreach (Border stack in FrmMain.PanHint.Children) + if (stack.Tag is object[] tagArray && Conversions.ToBoolean(tagArray[0]) && + (((TextBlock)stack.Child).Text ?? "") == (CurrentHint.Text ?? "")) + DoubleStack = stack; + // 获取渐变颜色 + ModBase.MyColor TargetColor0, TargetColor1; + var Percent = 0.3d; + switch (CurrentHint.Type) + { + case HintType.Info: + { + TargetColor0 = new ModBase.MyColor(215d, 37d, 155d, 252d); + TargetColor1 = new ModBase.MyColor(215d, 10d, 142d, 252d); + break; + } + case HintType.Finish: + { + TargetColor0 = new ModBase.MyColor(215d, 33d, 177d, 33d); + TargetColor1 = new ModBase.MyColor(215d, 29d, 160d, 29d); // HintType.Critical + break; + } + + default: + { + TargetColor0 = new ModBase.MyColor(215d, 255d, 53d, 11d); + TargetColor1 = new ModBase.MyColor(215d, 255d, 43d, 0d); + break; + } + } + + if (DoubleStack != null) + { + var doubleStackTag = (object[])DoubleStack.Tag; + // 有重复提示,且该提示的进入动画已播放 + if (!ModAnimation.AniIsRun($"Hint Show {doubleStackTag[1]}")) + { + ModAnimation.AniStop($"Hint Hide {doubleStackTag[1]}"); + var Delay = (800d + ModBase.MathClamp(CurrentHint.Text!.Length, 5d, 23d) * 180d) * + ModAnimation.AniSpeed; + ModAnimation.AniStart(new[] + { + ModAnimation.AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaX(DoubleStack, -8, 50, 50, new ModAnimation.AniEaseInFluent()), + ModAnimation.AaX(DoubleStack, 8d, 50, 100, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaX(DoubleStack, -8, 50, 150, new ModAnimation.AniEaseInFluent()), + ModAnimation.AaDouble(i => + { + Percent += Conversions.ToDouble(i); + var Gradient = (LinearGradientBrush)DoubleStack.Background; + Gradient.GradientStops[0].Color = TargetColor0 * Percent + + new ModBase.MyColor(255d, 255d, 255d) * + (1d - Percent); + Gradient.GradientStops[1].Color = TargetColor1 * Percent + + new ModBase.MyColor(255d, 255d, 255d) * + (1d - Percent); + }, 0.7d, 250), + ModAnimation.AaX(DoubleStack, -50, 200, (int)Math.Round(Delay), + new ModAnimation.AniEaseInFluent()), + ModAnimation.AaOpacity(DoubleStack, -1, 150, (int)Math.Round(Delay)), + ModAnimation.AaCode(() => doubleStackTag[0] = false, + (int)Math.Round(Delay)), + ModAnimation.AaHeight(DoubleStack, -26, 100, Ease: new ModAnimation.AniEaseOutFluent(), + After: true), + ModAnimation.AaCode(() => FrmMain.PanHint.Children.Remove(DoubleStack), After: true) + }, + Conversions.ToString(Operators.ConcatenateObject("Hint Hide ", + doubleStackTag[1]))); + } + } + else + { + // 准备控件 + var newHintTag = new object[] { true, ModBase.GetUuid() }; + var NewHintControl = new Border + { + Tag = newHintTag, Margin = new Thickness(-70, 0d, 20d, 0d), + Opacity = 0d, + Height = 0d, HorizontalAlignment = HorizontalAlignment.Left, + CornerRadius = new CornerRadius(0d, 6d, 6d, 0d), + Background = new LinearGradientBrush( + new GradientStopCollection(new List + { + new(TargetColor0 * Percent + new ModBase.MyColor(255d, 255d, 255d) * (1d - Percent), + 0d), + new(TargetColor1 * Percent + new ModBase.MyColor(255d, 255d, 255d) * (1d - Percent), 1d) + }), 90d), + Child = new TextBlock + { + TextTrimming = TextTrimming.CharacterEllipsis, FontSize = 13d, Text = CurrentHint.Text, + Foreground = new ModBase.MyColor(255d, 255d, 255d), Margin = new Thickness(33d, 5d, 8d, 5d) + } + }; + // AddHandler NewHintControl.MouseLeftButtonDown, AddressOf HideAllHint + FrmMain.PanHint.Children.Add(NewHintControl); + // 控件动画 + var Animations = new List(); + if (FrmMain.PanHint.Children.Count > 1) + // 已有提示 + Animations.Add(ModAnimation.AaHeight(NewHintControl, 26d, 150, + Ease: new ModAnimation.AniEaseOutFluent())); + else + // 是唯一提示 + NewHintControl.Height = 26d; + // 开始动画 + Animations.AddRange([ + ModAnimation.AaX(NewHintControl, 30d, + Ease: new ModAnimation.AniEaseOutElastic(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaX(NewHintControl, 20d, 200, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaOpacity(NewHintControl, 1d, 100), + ModAnimation.AaDouble(i => + { + Percent += Conversions.ToDouble(i); + var Gradient = (LinearGradientBrush)NewHintControl.Background; + Gradient.GradientStops[0].Color = TargetColor0 * Percent + + new ModBase.MyColor(255d, 255d, 255d) * (1d - Percent); + Gradient.GradientStops[1].Color = TargetColor1 * Percent + + new ModBase.MyColor(255d, 255d, 255d) * (1d - Percent); + }, 0.7d, 250, 100) + ]); + ModAnimation.AniStart(Animations, $"Hint Show {newHintTag[1]}"); + // 结束动画 + var Delay = (800d + ModBase.MathClamp(CurrentHint.Text!.Length, 5d, 23d) * 180d) * + ModAnimation.AniSpeed; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(NewHintControl, -50, 200, (int)Math.Round(Delay), + new ModAnimation.AniEaseInFluent()), + ModAnimation.AaOpacity(NewHintControl, -1, 150, (int)Math.Round(Delay)), + ModAnimation.AaCode(() => newHintTag[0] = false, (int)Math.Round(Delay)), + ModAnimation.AaHeight(NewHintControl, -26, 100, Ease: new ModAnimation.AniEaseOutFluent(), + After: true), + ModAnimation.AaCode(() => FrmMain.PanHint.Children.Remove(NewHintControl), After: true) + }, $"Hint Hide {newHintTag[1]}"); + } + + // 结束处理 + EndHint: ; + + if (CurrentHint.Log) + ModBase.Log("[UI] 弹出提示:" + CurrentHint.Text); + HintWaiting.RemoveAt(0); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "显示弹出提示失败", ModBase.LogLevel.Normal); + } + } + + private static void HideAllHint() + { + foreach (Border Control in FrmMain!.PanHint.Children) + { + var controlTag = (object[])Control.Tag; + Control.IsHitTestVisible = false; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(Control, -50, 200, Ease: new ModAnimation.AniEaseInFluent()), + ModAnimation.AaOpacity(Control, -1, 150, Ease: new ModAnimation.AniEaseInFluent()), + ModAnimation.AaCode(() => controlTag[0] = false), + ModAnimation.AaHeight(Control, -26, 100, Ease: new ModAnimation.AniEaseOutFluent(), After: true), + ModAnimation.AaCode(() => FrmMain.PanHint.Children.Remove(Control), After: true) + }, Conversions.ToString(Operators.ConcatenateObject("Hint Hide ", controlTag[1]))); + } + } + + #endregion + + #region 弹窗 + + /// + /// 存储弹窗信息的转换器。 + /// + public class MyMsgBoxConverter + { + // 设置轮询 Url + public object AuthUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + public string Button1 = "确定"; + + /// + /// 点击第一个按钮将执行该方法,不关闭弹窗。 + /// + public Action Button1Action; + + public string Button2 = ""; + + /// + /// 点击第二个按钮将执行该方法,不关闭弹窗。 + /// + public Action Button2Action; + + public string Button3 = ""; + + /// + /// 点击第三个按钮将执行该方法,不关闭弹窗。 + /// + public Action Button3Action; + + /// + /// 输入模式:文本框的文本。 + /// 选择模式:需要放进去的 List(Of MyListItem)。 + /// 登录模式:登录步骤 1 中返回的 JSON。 + /// + public object Content; + + public bool ForceWait; + + /// + /// 有多个按钮时,是否给第一个按钮加高亮。 + /// + public bool HighLight; + + /// + /// 输入模式:提示文本。 + /// + public string HintText = ""; + + /// + /// 弹窗是否已经关闭。 + /// + public bool IsExited = false; + + public bool IsWarn; + + /// + /// 输入模式:输入的文本。若点击了 非 第一个按钮,则为 Nothing。 + /// 选择模式:点击的按钮编号,从 1 开始。 + /// 登录模式:字符串数组 {AccessToken, RefreshToken} 或一个 Exception。 + /// + public object Result; + + public string Text; + public string Title; + public MyMsgBoxType Type; + + /// + /// 输入模式:输入验证规则。 + /// + public Collection ValidateRules; + + public DispatcherFrame WaitFrame = new(true); + } + + public enum MyMsgBoxType + { + Text, + Select, + Input, + Login, + Markdown + } + + /// + /// 显示弹窗,返回点击按钮的编号(从 1 开始)。 + /// + /// 弹窗的标题。 + /// 弹窗的内容。 + /// 显示的第一个按钮,默认为“确定”。 + /// 显示的第二个按钮,默认为空。 + /// 显示的第三个按钮,默认为空。 + /// 点击第一个按钮将执行该方法,不关闭弹窗。 + /// 点击第二个按钮将执行该方法,不关闭弹窗。 + /// 点击第三个按钮将执行该方法,不关闭弹窗。 + /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 + public static int MyMsgBox(string Caption, string Title = "提示", string Button1 = "确定", string Button2 = "", + string Button3 = "", bool IsWarn = false, bool HighLight = true, bool ForceWait = false, + Action Button1Action = null, Action Button2Action = null, Action Button3Action = null) + { + // 将弹窗列入队列 + var Converter = new MyMsgBoxConverter + { + Type = MyMsgBoxType.Text, Button1 = Button1, Button2 = Button2, Button3 = Button3, Text = Caption, + IsWarn = IsWarn, Title = Title, HighLight = HighLight, ForceWait = true, Button1Action = Button1Action, + Button2Action = Button2Action, Button3Action = Button3Action + }; + WaitingMyMsgBox.Add(Converter); + if (ModBase.RunInUi()) + // 若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗 + MyMsgBoxTick(); + if (Button2.Length > 0 || ForceWait) + { + // 若有多个按钮则开始等待 + if (FrmMain is null || (FrmMain.PanMsg is null && ModBase.RunInUi())) + { + // 主窗体尚未加载,用老土的弹窗来替代 + WaitingMyMsgBox.Remove(Converter); + if (Button2.Length > 0) + { + var RawResult = Interaction.MsgBox(Caption, + (MsgBoxStyle)((int)(Button3.Length > 0 ? MsgBoxStyle.YesNoCancel : MsgBoxStyle.YesNo) + + (int)(IsWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), Title); + switch (RawResult) + { + case MsgBoxResult.Yes: + { + Converter.Result = 1; + break; + } + case MsgBoxResult.No: + { + Converter.Result = 2; + break; + } + case MsgBoxResult.Cancel: + { + Converter.Result = 3; + break; + } + } + } + else + { + Interaction.MsgBox(Caption, + (MsgBoxStyle)((int)MsgBoxStyle.OkOnly + + (int)(IsWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), Title); + Converter.Result = 1; + } + + ModBase.Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" + Button1 + "," + Button2 + "," + Button3, + ModBase.LogLevel.Debug); + } + else + { + try + { + FrmMain.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(Converter.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + } + + ModBase.Log( + Conversions.ToString(Operators.ConcatenateObject("[Control] 普通弹框返回:", Converter.Result ?? "null"))); + return Conversions.ToInteger(Converter.Result); + } + + // 不进行等待,直接返回 + return 1; + } + + /// + /// 显示弹窗,返回点击按钮的编号(从 1 开始)。 + /// + /// 弹窗的标题。 + /// 弹窗的内容。 + /// 显示的第一个按钮,默认为“确定”。 + /// 显示的第二个按钮,默认为空。 + /// 显示的第三个按钮,默认为空。 + /// 点击第一个按钮将执行该方法,不关闭弹窗。 + /// 点击第二个按钮将执行该方法,不关闭弹窗。 + /// 点击第三个按钮将执行该方法,不关闭弹窗。 + /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 + public static int MyMsgBoxMarkdown(string Caption, string Title = "提示", string Button1 = "确定", string Button2 = "", + string Button3 = "", bool IsWarn = false, bool HighLight = true, bool ForceWait = false, + Action Button1Action = null, Action Button2Action = null, Action Button3Action = null) + { + // 将弹窗列入队列 + var Converter = new MyMsgBoxConverter + { + Type = MyMsgBoxType.Markdown, Button1 = Button1, Button2 = Button2, Button3 = Button3, Text = Caption, + IsWarn = IsWarn, Title = Title, HighLight = HighLight, ForceWait = true, Button1Action = Button1Action, + Button2Action = Button2Action, Button3Action = Button3Action + }; + WaitingMyMsgBox.Add(Converter); + if (ModBase.RunInUi()) + // 若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗 + MyMsgBoxTick(); + if (Button2.Length > 0 || ForceWait) + { + // 若有多个按钮则开始等待 + if (FrmMain is null || (FrmMain.PanMsg is null && ModBase.RunInUi())) + { + // 主窗体尚未加载,用老土的弹窗来替代 + WaitingMyMsgBox.Remove(Converter); + if (Button2.Length > 0) + { + var RawResult = Interaction.MsgBox(Caption, + (MsgBoxStyle)((int)(Button3.Length > 0 ? MsgBoxStyle.YesNoCancel : MsgBoxStyle.YesNo) + + (int)(IsWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), Title); + switch (RawResult) + { + case MsgBoxResult.Yes: + { + Converter.Result = 1; + break; + } + case MsgBoxResult.No: + { + Converter.Result = 2; + break; + } + case MsgBoxResult.Cancel: + { + Converter.Result = 3; + break; + } + } + } + else + { + Interaction.MsgBox(Caption, + (MsgBoxStyle)((int)MsgBoxStyle.OkOnly + + (int)(IsWarn ? MsgBoxStyle.Critical : MsgBoxStyle.Question)), Title); + Converter.Result = 1; + } + + ModBase.Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" + Button1 + "," + Button2 + "," + Button3, + ModBase.LogLevel.Debug); + } + else + { + try + { + FrmMain.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(Converter.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + } + + ModBase.Log( + Conversions.ToString(Operators.ConcatenateObject("[Control] 普通弹框返回:", Converter.Result ?? "null"))); + return Conversions.ToInteger(Converter.Result); + } + + // 不进行等待,直接返回 + return 1; + } + + /// + /// 显示输入框并返回输入的文本。若点击第二个按钮,则返回 Nothing。 + /// + /// 弹窗的标题。 + /// 文本框的输入检测。 + /// 弹窗的介绍文本。 + /// 文本框的默认内容。 + /// 文本框的提示内容。 + /// 显示的第一个按钮,默认为“确定”。 + /// 显示的第二个按钮,默认为“取消”。 + /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 + public static string MyMsgBoxInput(string Title, string Text = "", string DefaultInput = "", + Collection? ValidateRules = null, string HintText = "", string Button1 = "确定", + string Button2 = "取消", bool IsWarn = false) + { + // 将弹窗列入队列 + var Converter = new MyMsgBoxConverter + { + Text = Text, HintText = HintText, Type = MyMsgBoxType.Input, + ValidateRules = ValidateRules ?? [], Button1 = Button1, Button2 = Button2, + Content = DefaultInput, IsWarn = IsWarn, Title = Title + }; + WaitingMyMsgBox.Add(Converter); + // 虽然我也不知道这是啥但是能用就成了 :) + try + { + FrmMain?.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(Converter.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + + ModBase.Log($"[Control] 输入弹框返回:{Converter.Result}"); + return Converter.Result?.ToString(); + } + + /// + /// 显示选择框并返回选择的第几项(从 0 开始)。若点击第二个按钮,则返回 Nothing。 + /// + /// 弹窗的标题。 + /// 显示的第一个按钮,默认为 “确定”。 + /// 显示的第二个按钮,默认为空。 + /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 + public static int? MyMsgBoxSelect(List Selections, string Title = "提示", string Button1 = "确定", + string Button2 = "", bool IsWarn = false) + { + // 将弹窗列入队列 + var Converter = new MyMsgBoxConverter + { + Type = MyMsgBoxType.Select, Button1 = Button1, Button2 = Button2, Content = Selections, IsWarn = IsWarn, + Title = Title + }; + WaitingMyMsgBox.Add(Converter); + // 虽然我也不知道这是啥但是能用就成了 :) + try + { + if (FrmMain is not null) + FrmMain.DragStop(); + ComponentDispatcher.PushModal(); + Dispatcher.PushFrame(Converter.WaitFrame); + } + finally + { + ComponentDispatcher.PopModal(); + } + + ModBase.Log(Conversions.ToString(Operators.ConcatenateObject("[Control] 选择弹框返回:", Converter.Result ?? "null"))); + return (int?)Converter.Result; + } + + + public static void MyMsgBoxTick() + { + try + { + if (FrmMain is null || FrmMain.PanMsg is null || FrmMain.WindowState == WindowState.Minimized) + return; + if (FrmMain.PanMsg.Children.Count > 0) + { + // 弹窗中 + FrmMain.PanMsgBackground.Visibility = Visibility.Visible; + } + else if (WaitingMyMsgBox.Any()) + { + // 没有弹窗,显示一个等待的弹窗 + FrmMain.PanMsgBackground.Visibility = Visibility.Visible; + switch (WaitingMyMsgBox[0].Type) + { + case MyMsgBoxType.Input: + { + FrmMain.PanMsg.Children.Add(new MyMsgInput(WaitingMyMsgBox[0])); + break; + } + case MyMsgBoxType.Select: + { + FrmMain.PanMsg.Children.Add(new MyMsgSelect(WaitingMyMsgBox[0])); + break; + } + case MyMsgBoxType.Text: + { + FrmMain.PanMsg.Children.Add(new MyMsgText(WaitingMyMsgBox[0])); + break; + } + case MyMsgBoxType.Login: + { + FrmMain.PanMsg.Children.Add(new MyMsgLogin(WaitingMyMsgBox[0])); + break; + } + case MyMsgBoxType.Markdown: + { + FrmMain.PanMsg.Children.Add(new MyMsgMarkdown(WaitingMyMsgBox[0])); + break; + } + } + + WaitingMyMsgBox.RemoveAt(0); + } + // 没有弹窗,没有等待的弹窗 + else if (!(FrmMain.PanMsgBackground.Visibility == Visibility.Collapsed)) + { + FrmMain.PanMsgBackground.Visibility = Visibility.Collapsed; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "处理等待中的弹窗失败", ModBase.LogLevel.Feedback); + } + } + + public static void MsgBoxWrapper_OnShow(string message, string caption, ICollection buttons, + MsgBoxTheme theme, bool block, ref int result) + { + var btnText1 = buttons.Count < 1 ? "确定" : buttons.ElementAt(0).Context; + var btnAct1 = (Action)(buttons.Count < 1 ? (object)null : buttons.ElementAt(0).OnClick); + var btnText2 = buttons.Count < 2 ? "取消" : buttons.ElementAt(1).Context; + var btnAct2 = (Action)(buttons.Count < 2 ? (object)null : buttons.ElementAt(1).OnClick); + var btnText3 = buttons.Count < 3 ? "" : buttons.ElementAt(2).Context; + var btnAct3 = (Action)(buttons.Count < 3 ? (object)null : buttons.ElementAt(2).OnClick); + + var isWarn = theme == MsgBoxTheme.Warning || theme == MsgBoxTheme.Error; + + result = MyMsgBox(message, caption, btnText1, btnText2, btnText3, isWarn, ForceWait: block, + Button1Action: btnAct1, Button2Action: btnAct2, Button3Action: btnAct3); + } + + #endregion + + #region 页面声明 + + // 在最后进行页面声明,避免颜色尚未加载完毕 + + // 窗体声明 + + + // 页面声明(出于单元测试考虑,初始化页面已转入 FormMain 中) + + + // 工具页面声明 + + + // 下载页面声明 + + + // 设置页面声明 + + + // 登录页面声明 + + + // 实例设置页面声明 + + + // 实例存档页面 + + + // 资源信息分页声明 + + #endregion + + #region 帮助 + + public class HelpEntry + { + /// + /// 显示描述。 + /// + public string Desc; + + public string EventData; + public string EventType; + + // 动作 + + /// + /// 是否为 “执行事件”。 + /// + public bool IsEvent; + + // 显示(可选) + + /// + /// 帮助项的自定义图标。可能为 Nothing。 + /// + public string Logo; + + /// + /// 原始信息路径。用于刷新。 + /// + public string RawPath; + + /// + /// 检索关键字。 + /// + public string Search; + + /// + /// 是否在公开版的 PCL 中显示(这会影响主页与搜索)。默认为 True。 + /// + public bool ShowInPublic = true; + + /// + /// 是否显示在搜索结果。默认为 True。 + /// + public bool ShowInSearch = true; + + /// + /// 是否在快照版的 PCL 中显示(这会影响主页与搜索)。默认为 True。 + /// + public bool ShowInSnapshot = true; + + // 基础 + + /// + /// 显示标题。 + /// + public string Title; + + /// + /// 用于分类的标签列表。 + /// + public List Types; + + /// + /// 若非执行事件,其对应的 .xaml 本地文件内容。 + /// + public string XamlContent; + + // 转换 + + /// + /// 从文件初始化 HelpEntry 对象,失败会抛出异常。 + /// + public HelpEntry(string FilePath) + { + RawPath = FilePath; + var JsonData = (JObject)ModBase.GetJson(HelpArgumentReplace(ModBase.ReadFile(FilePath))); + if (JsonData is null) + throw new FileNotFoundException("未找到帮助文件:" + FilePath, FilePath); + // 加载常规信息 + if (JsonData["Title"] is not null) + Title = (string)JsonData["Title"]; + else + throw new ArgumentException("未找到 Title 项"); + Desc = (string)(JsonData["Description"] ?? ""); + Search = (string)(JsonData["Keywords"] ?? ""); + Logo = (string)JsonData["Logo"]; // 为保持 Nothing,不要加 If + ShowInSearch = (bool)(JsonData["ShowInSearch"] ?? ShowInSearch); + ShowInPublic = (bool)(JsonData["ShowInPublic"] ?? ShowInPublic); + ShowInSnapshot = (bool)(JsonData["ShowInSnapshot"] ?? ShowInSnapshot); + Types = new List(); + foreach (var NameOfType in (IEnumerable)(JsonData["Types"] ?? ModBase.GetJson("[]"))) + Types.Add(Conversions.ToString(NameOfType)); + // 加载事件信息 + if ((bool)(JsonData["IsEvent"] ?? false)) + { + EventType = (string)JsonData["EventType"]; + if (EventType is null) + throw new ArgumentException("未找到 EventType 项"); + EventData = (string)(JsonData["EventData"] ?? ""); + IsEvent = true; + } + else + { + var XamlAddress = FilePath.ToLower().Replace(".json", ".xaml"); + if (File.Exists(XamlAddress)) + { + XamlContent = ModBase.ReadFile(XamlAddress); + IsEvent = false; + } + else + { + throw new FileNotFoundException("未找到帮助条目 .json 对应的 .xaml 文件(" + XamlAddress + ")"); + } + } + } + + /// + /// 获取该 HelpEntry 对应的 MyListItem。 + /// + public MyListItem ToListItem() + { + return SetToListItem(new MyListItem()); + } + + /// + /// 将属性设置入一个现有的 ListItem。 + /// + public MyListItem SetToListItem(MyListItem Item) + { + string Logo; + if (IsEvent) + { + if (EventType == "弹出窗口") + Logo = ModBase.PathImage + "Blocks/GrassPath.png"; + else + Logo = ModBase.PathImage + "Blocks/CommandBlock.png"; + } + else + { + Logo = ModBase.PathImage + "Blocks/Grass.png"; + } + + // 设置属性 + Item.SnapsToDevicePixels = true; + Item.Title = Title; + Item.Info = Desc; + Item.Logo = this.Logo ?? Logo; + Item.Height = 42d; + Item.Type = MyListItem.CheckType.Clickable; + Item.Tag = this; + Item.EventType = null; + Item.EventData = null; + // 项目的点击事件 + Item.Click += (sender, e) => PageToolsHelp.OnItemClick((HelpEntry)((MyListItem)sender).Tag); + return Item; + } + } + + + private static readonly object HelpLoadLock = new(); + + /// + /// 初始化帮助列表对象。 + /// + private static void HelpLoad(ModLoader.LoaderTask> Loader) + { + lock (HelpLoadLock) // 避免重复解压文件导致出错 + { + try + { + // 解压内置文件 + HelpExtract(); + + // 遍历文件 + var FileList = new List(); + try + { + var IgnoreList = new List(); + // 读取自定义文件 + if (Directory.Exists(ModBase.ExePath + @"PCL\Help\")) + foreach (var File in ModBase.EnumerateFiles(ModBase.ExePath + @"PCL\Help\")) + switch (File.Extension.ToLower() ?? "") + { + case ".helpignore": + { + // 加载忽略列表 + ModBase.Log("[Help] 发现 .helpignore 文件:" + File.FullName); + foreach (var Line in ModBase.ReadFile(File.FullName) + .Split("\r\n".ToCharArray())) + { + var RealString = Line.BeforeFirst("#").Trim(); + if (string.IsNullOrWhiteSpace(RealString)) + continue; + IgnoreList.Add(RealString); + if (ModBase.ModeDebug) + ModBase.Log("[Help] > " + RealString); + } + + break; + } + case ".json": + { + FileList.Add(File.FullName); + break; + } + } + + ModBase.Log("[Help] 已扫描 PCL 文件夹下的帮助文件,目前总计 " + FileList.Count + " 条"); + // 读取自带文件 + foreach (var File in ModBase.EnumerateFiles(ModBase.PathHelpFolder)) + { + // 跳过非 Json 文件与以 . 开头的文件夹 + if (File.Extension.ToLower() != ".json" || File.Directory.FullName + .Replace(ModBase.PathHelpFolder.TrimEnd('\\'), "").Contains(@"\.")) + continue; + // 检查忽略列表 + var RealPath = File.FullName.Replace(ModBase.PathHelpFolder.TrimEnd('\\'), ""); + foreach (var Ignore in IgnoreList) + if (RealPath.RegexCheck(Ignore)) + { + if (ModBase.ModeDebug) + ModBase.Log("[Help] 已忽略 " + RealPath + ":" + Ignore); + goto NextFile; + } + + FileList.Add(File.FullName); + NextFile: ; + } + + ModBase.Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " + FileList.Count + " 条"); + } + catch (Exception ex) + { + ModBase.Log(ex, "检查帮助文件夹失败", ModBase.LogLevel.Msgbox); + } + + if (Loader.IsAborted) + return; + + // 将文件实例化 + var Dict = new List(); + foreach (var FilePath in FileList) + try + { + var Entry = new HelpEntry(FilePath); + Dict.Add(Entry); + if (ModBase.ModeDebug) + ModBase.Log("[Help] 已加载的帮助条目:" + Entry.Title + " ← " + FilePath); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化帮助条目失败(" + FilePath + ")", ModBase.LogLevel.Msgbox); + } + + // 回设 + if (!Dict.Any()) + throw new Exception("未找到可用的帮助;若不需要帮助页面,可以在 设置 → 个性化 → 功能隐藏 中将其隐藏"); + if (Loader.IsAborted) + return; + Loader.Output = Dict; + } + + catch (Exception ex) + { + ModBase.Log(ex, "帮助列表初始化失败"); + throw; + } + } + } + + /// + /// 解压内置帮助文件。 + /// + public static void HelpExtract() + { + ModBase.DeleteDirectory(ModBase.PathTemp + @"CE\Help"); + Directory.CreateDirectory(ModBase.PathTemp + @"CE\Help"); + ModBase.WriteFile(ModBase.PathTemp + @"CE\Cache\Help.zip", ModBase.GetResourceStream("Resources/Help.zip")); + ModBase.ExtractFile(ModBase.PathTemp + @"CE\Cache\Help.zip", ModBase.PathTemp + @"CE\Help", Encoding.UTF8); + ModBase.Log("[Help] 已解压内置帮助文件,目前状态:" + File.Exists(ModBase.PathTemp + @"CE\Help\启动器\备份设置.xaml"), + ModBase.LogLevel.Debug); + } + + /// + /// 对帮助文件约定的替换标记进行处理,如果遇到需要转义的字符会进行转义。 + /// + public static string HelpArgumentReplace(string Xaml) + { + var Result = Xaml.Replace("{path}", ModBase.EscapeXML(ModBase.ExePath)); + Result = Result.RegexReplaceEach(@"\{hint\}", _ => ModBase.EscapeXML(PageToolsTest.GetRandomHint())); + Result = Result.RegexReplaceEach(@"\{cave\}", _ => ModBase.EscapeXML(PageToolsTest.GetRandomCave())); + return Result; + } + + #endregion + + #region 愚人节 + + public static bool IsAprilEnabled = DateTime.Now.Month == 4 && DateTime.Now.Day == 1; + public static bool IsAprilGiveup = false; + private static Vector AprilSpeed = new(0d, 0d); + private static int AprilIdieCount; + private static Point AprilMousePosLast = new(0d, 0d); + private static int AprilDistance; + + private static void TimerFool() + { + try + { + if (FrmLaunchLeft is null || FrmLaunchLeft.AprilPosTrans is null || FrmMain.lastMouseArg is null) + return; + if (IsAprilGiveup || FrmMain.PageCurrent != FormMain.PageType.Launch || + ModAnimation.AniControlEnabled != 0 || !FrmLaunchLeft.BtnLaunch.IsLoaded) + return; + + // 计算是否空闲 + var MousePos = FrmMain.lastMouseArg.GetPosition(FrmMain); + if (MousePos == AprilMousePosLast) + { + AprilIdieCount += 1; + } + else + { + AprilMousePosLast = MousePos; + AprilIdieCount = 0; + } + + // 计算躲避移动 + Vector Direction; + double Distance; + var ButtonWidth = FrmLaunchLeft.BtnLaunch.ActualWidth / 2d; + var ButtonHeight = FrmLaunchLeft.BtnLaunch.ActualHeight / 2d; + var Vec = (Vector)(FrmMain.lastMouseArg.GetPosition(FrmLaunchLeft.BtnLaunch) - + new Vector(ButtonWidth, ButtonHeight)); + var Dir = new Vector(Vec.X, Vec.Y); + Dir.Normalize(); + Direction = -Dir; + Distance = new Vector(Math.Max(0d, Math.Abs(Vec.X) - ButtonWidth), + Math.Max(0d, Math.Abs(Vec.Y) - ButtonHeight)).Length; + var BreathScale = Math.Sin(Timer150Count / 37.5d * Math.PI); + var Acc = Math.Max(0d, BreathScale * 0.25d - 0.65d - Math.Log((Distance + 0.4d) / 200d)) * Direction; // 加速度 + // 计算回归移动 + if (AprilIdieCount >= 64 * 5) + { + var SafeDist = (Vector)(FrmMain.lastMouseArg.GetPosition(FrmMain.PanMain) - + new Vector(ButtonWidth, FrmMain.PanMain.ActualHeight - ButtonHeight * 3d)); + var Back = new Vector(FrmLaunchLeft.AprilPosTrans.X, FrmLaunchLeft.AprilPosTrans.Y); + if (SafeDist.Length > 250d && Back.Length > 0.4d) + { + Acc -= Back * 0.0005d; + Back.Normalize(); + Acc -= Back * 0.15d; + } + } + + // 回到边界 + var Relative = FrmLaunchLeft.BtnLaunch.TranslatePoint(new Point(0d, 0d), FrmMain.PanForm); + if (Relative.X < -ButtonWidth * 2d) + { + FrmLaunchLeft.AprilPosTrans.X += FrmMain.PanForm.ActualWidth + ButtonWidth * 2d; // 离开左边界 + AprilSpeed.X -= 80d; + if (Relative.Y < 0d) + FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5d; + else if (Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2d) + FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5d; + } + else if (Relative.X > FrmMain.PanForm.ActualWidth) + { + FrmLaunchLeft.AprilPosTrans.X -= FrmMain.PanForm.ActualWidth + ButtonWidth * 2d; // 离开右边界 + AprilSpeed.X += 80d; + if (Relative.Y < 0d) + FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5d; + else if (Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2d) + FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5d; + } + else if (Relative.Y < -ButtonHeight * 2d) + { + FrmLaunchLeft.AprilPosTrans.Y += FrmMain.PanForm.ActualHeight + ButtonHeight * 2d; // 离开上边界 + AprilSpeed.Y -= 25d; + if (Relative.X < 0d) + FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2d; + else if (Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2d) + FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2d; + } + else if (Relative.Y > FrmMain.PanForm.ActualHeight) + { + FrmLaunchLeft.AprilPosTrans.Y -= FrmMain.PanForm.ActualHeight + ButtonHeight * 2d; // 离开下边界 + AprilSpeed.Y += 25d; + if (Relative.X < 0d) + FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2d; + else if (Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2d) + FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2d; + } + + // 移动 + AprilSpeed = AprilSpeed * 0.8d + Acc; + var SpeedValue = Math.Min(60d, AprilSpeed.Length); + if (SpeedValue < 0.01d) + return; + AprilSpeed.Normalize(); + AprilSpeed *= SpeedValue; + AprilDistance = (int)Math.Round(AprilDistance + SpeedValue); + FrmLaunchLeft.AprilPosTrans.X += AprilSpeed.X; + FrmLaunchLeft.AprilPosTrans.Y += AprilSpeed.Y; + // 大小改变 + FrmLaunchLeft.AprilScaleTrans.ScaleX = + ModBase.MathClamp(1d - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160d), 0.2d, + 1.8d); + FrmLaunchLeft.AprilScaleTrans.ScaleY = + ModBase.MathClamp(1d - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100d), 0.2d, + 1.8d); + // 放弃提示 + if (AprilDistance > 4000) + { + AprilDistance = -4000; + switch (RandomUtils.NextInt(0, 3)) + { + case 0: + { + Hint("放弃吧!只需要点一下右下角的小白旗……"); + break; + } + case 1: + { + Hint("看到右下角的那面小白旗了吗?"); + break; + } + case 2: + { + Hint("这里建议点一下右下角的小白旗投降呢.jpg"); + break; + } + case 3: + { + Hint("右下角的小白旗永远等着你……"); + break; + } + } + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "愚人节移动出错", ModBase.LogLevel.Feedback); + } + } + + #endregion + + #region 系统 + + /// + /// 把某个 PCL 窗口拖到最前面。 + /// + public static void ShowWindowToTop(nint Handle) + { + try + { + PostMessage(Handle, 400 * 16 + 2, 0L, 0L); + SetForegroundWindow(Handle); // 不在这里放不行,神秘 WinAPI,建议别动 + } + catch (Exception ex) + { + ModBase.Log(ex, "设置窗口置顶失败", ModBase.LogLevel.Hint); + } + } + + [DllImport("user32", EntryPoint = "FindWindowA")] + public static extern nint FindWindow(string ClassName, string WindowName); + + [DllImport("user32")] + public static extern int SetForegroundWindow(nint hWnd); + + [DllImport("user32", EntryPoint = "PostMessageA")] + private static extern bool PostMessage(nint hWnd, uint msg, long wParam, long lParam); + + /// + /// 将特定程序设置为使用高性能显卡启动。 + /// 如果失败,则抛出异常。 + /// + public static void SetGPUPreference(string Executeable, bool WantHighPerformance = true) + { + const string GPU_PERFERENCE_REG_KEY = @"Software\Microsoft\DirectX\UserGpuPreferences"; + const string GPU_PERFERENCE_REG_VALUE_HIGH = "GpuPreference=2;"; + const string GPU_PERFERENCE_REG_VALUE_DEFAULT = "GpuPreference=0;"; + // Const GPU_PERFERENCE_REG_VALUE_POWER_SAVING As String = "GpuPreference=1;" + + var IsCurrentHighPerformance = false; + // 查看现有设置 + // 就知道 My.Computer,改个注册表 Microsoft.Win32.Registry 几年前的 API 了不用,还在这 My.Computer 都 5202 年了 My 你大爷 + using (var ReadOnlyKey = Registry.CurrentUser.OpenSubKey(GPU_PERFERENCE_REG_KEY, false)) + { + if (ReadOnlyKey is not null) + { + var CurrentValue = ReadOnlyKey.GetValue(Executeable); + if (GPU_PERFERENCE_REG_VALUE_HIGH == (CurrentValue?.ToString() ?? "")) IsCurrentHighPerformance = true; + } + else + { + // 创建父级键 + ModBase.Log("[System] 需要创建显卡设置的父级键"); + Registry.CurrentUser.CreateSubKey(GPU_PERFERENCE_REG_KEY); + } + } + + ModBase.Log($"[System] 当前程序 ({Executeable}) 的显卡设置为高性能: {IsCurrentHighPerformance}"); + if (IsCurrentHighPerformance ^ WantHighPerformance) + // 写入新设置 + using (var WriteKey = Registry.CurrentUser.OpenSubKey(GPU_PERFERENCE_REG_KEY, true)) + { + WriteKey.SetValue(Executeable, + WantHighPerformance ? GPU_PERFERENCE_REG_VALUE_HIGH : GPU_PERFERENCE_REG_VALUE_DEFAULT); + ModBase.Log($"[System] 已调整程序 ({Executeable}) 显卡设置: {WantHighPerformance}"); + } + } + + #endregion + + #region 任务缓存 + + private static bool IsTaskTempCleared; + private static bool IsTaskTempClearing; + + /// + /// 尝试清理任务缓存文件夹。 + /// 在整次运行中只会实际清理一次。 + /// + public static void TryClearTaskTemp() + { + if (!IsTaskTempCleared) + { + IsTaskTempCleared = true; + IsTaskTempClearing = true; + try + { + ModBase.Log("[System] 开始清理任务缓存文件夹"); + ModBase.DeleteDirectory($@"{ModBase.OsDrive}ProgramData\PCL\TaskTemp\"); + ModBase.DeleteDirectory($@"{ModBase.PathTemp}TaskTemp\"); + ModBase.Log("[System] 已清理任务缓存文件夹"); + } + catch (Exception ex) + { + ModBase.Log(ex, "清理任务缓存文件夹失败"); + } + finally + { + IsTaskTempClearing = false; + } + } + else if (IsTaskTempClearing) + { + // 等待另一个清理步骤完成 + while (IsTaskTempClearing) + Thread.Sleep(1); + } + } + + /// + /// 申请一个可用于任务缓存的临时文件夹,以 \ 结尾。这些文件夹无需进行后续清理。 + /// 若所有缓存位置均没有权限,会抛出异常。 + /// + /// 是否要求路径不包含空格。 + public static string RequestTaskTempFolder(bool RequireNonSpace = false) + { + TryClearTaskTemp(); + string ResultFolder; + do + { + try + { + ResultFolder = $@"{ModBase.PathTemp}TaskTemp\{ModBase.GetUuid()}-{RandomUtils.NextInt(0, 1000000)}\"; + if (RequireNonSpace && ResultFolder.Contains(" ")) + break; // 带空格 + Directory.CreateDirectory(ResultFolder); + ModBase.CheckPermissionWithException(ResultFolder); + return ResultFolder; + } + catch + { + } + } while (false); + + // 使用备用路径 + ResultFolder = + $@"{ModBase.OsDrive}ProgramData\PCL\TaskTemp\{ModBase.GetUuid()}-{RandomUtils.NextInt(0, 1000000)}\"; + Directory.CreateDirectory(ResultFolder); + ModBase.CheckPermission(ResultFolder); + return ResultFolder; + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/ModMusic.cs b/Plain Craft Launcher 2/Modules/ModMusic.cs new file mode 100644 index 000000000..8c02d3b50 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModMusic.cs @@ -0,0 +1,438 @@ +using System.IO; +using System.Windows.Controls; +using Microsoft.VisualBasic; +using NAudio; +using NAudio.Wave; +using PCL.Core.App; +using PCL.Core.Utils; + +namespace PCL; + +public static class ModMusic +{ + /// + /// 当前正在播放的 NAudio.Wave.WaveOutEvent。 + /// + public static WaveOutEvent MusicNAudio; + + /// + /// 当前播放的音乐地址。 + /// + private static string MusicCurrent = ""; + + private static void MusicLoop(bool IsFirstLoad = false) + { + WaveOutEvent currentWave = null; + AudioFileReader reader = null; + + try + { + currentWave = new WaveOutEvent(); + MusicNAudio = currentWave; + currentWave.DeviceNumber = -1; // 使用默认设备 + + reader = new AudioFileReader(MusicCurrent); + currentWave.Init(reader); + currentWave.Play(); + + // 首次加载且用户未启用自动播放,则暂停 + if (IsFirstLoad && !Config.Preference.Music.StartOnStartup) currentWave.Pause(); + + MusicRefreshUI(); + + var lastVolume = Config.Preference.Music.Volume; + currentWave.Volume = lastVolume / 1000.0f; + + // 播放主循环 + while (currentWave.Equals(MusicNAudio) && currentWave.PlaybackState != PlaybackState.Stopped) + { + // 音量动态更新 + var currentVolume = Config.Preference.Music.Volume; + if (currentVolume != lastVolume) + { + lastVolume = currentVolume; + currentWave.Volume = currentVolume / 1000.0f; + } + + // 更新进度条 + if (reader.TotalTime.TotalMilliseconds > 0d) + { + var progress = reader.CurrentTime.TotalMilliseconds / reader.TotalTime.TotalMilliseconds; + ModBase.RunInUi(() => ModMain.FrmMain.BtnExtraMusic.Progress = progress); + } + + Thread.Sleep(100); + } + + // 播放结束,继续下一首 + if (currentWave.PlaybackState == PlaybackState.Stopped && + (MusicAllList?.Any() is { } arg5 ? arg5 : (bool?)null).GetValueOrDefault()) + MusicStartPlay(DequeueNextMusicAddress(), IsFirstLoad); + } + + catch (Exception ex) + { + ModBase.Log(ex, $"播放音乐出现内部错误({MusicCurrent})", ModBase.LogLevel.Developer); + + // 错误处理:精准提示用户 + var fileName = ModBase.GetFileNameFromPath(MusicCurrent); + if (ex is MmException) + { + var msg = ex.Message; + if (msg.Contains("AlreadyAllocated")) + ModMain.Hint("你的音频设备正被其他程序占用。请关闭占用程序后重启 PCL 以恢复音乐功能!", ModMain.HintType.Critical); + else if (msg.Contains("NoDriver") || msg.Contains("BadDeviceId")) + ModMain.Hint("音频设备发生变更,音乐播放功能需重启 PCL 后恢复!", ModMain.HintType.Critical); + else + ModBase.Log(ex, $"播放失败({fileName})", ModBase.LogLevel.Hint); + } + else if (ex.Message.Contains("Got a frame at sample rate") || + ex.Message.Contains("does not support changes to")) + { + ModMain.Hint($"播放失败({fileName}):PCL 不支持中途变更音频属性的音乐文件", ModMain.HintType.Critical); + } + else if ((!MusicCurrent.EndsWithF(".wav", true) && !MusicCurrent.EndsWithF(".mp3", true) && + !MusicCurrent.EndsWithF(".flac", true)) || ex.Message.Contains("0xC00D36C4")) + { + ModMain.Hint($"播放失败({fileName}):PCL 可能不支持此格式,请转换为 .wav/.mp3/.flac", ModMain.HintType.Critical); + } + else + { + ModBase.Log(ex, $"播放失败({fileName})", ModBase.LogLevel.Hint); + } + + // 移除无效文件 + MusicAllList?.Remove(MusicCurrent); + MusicWaitingList?.Remove(MusicCurrent); + MusicRefreshUI(); + + Thread.Sleep(2000); + + // 尝试播放下一首 + if (ex is FileNotFoundException) + MusicRefreshPlay(true, IsFirstLoad); + else + MusicStartPlay(DequeueNextMusicAddress(), IsFirstLoad); + } + finally + { + currentWave?.Dispose(); + reader?.Dispose(); + MusicRefreshUI(); + } + } + + #region 播放列表 + + /// + /// 接下来要播放的音乐文件路径。未初始化时为 Nothing。 + /// + public static List MusicWaitingList; + + /// + /// 全部音乐文件路径。未初始化时为 Nothing。 + /// + public static List MusicAllList; + + /// + /// 初始化音乐播放列表。 + /// + /// 强制全部重新载入列表。 + /// 在重载列表时避免让某项成为第一项。 + private static void MusicListInit(bool ForceReload, string PreventFirst = null) + { + if (ForceReload) + MusicAllList = null; + + try + { + if (MusicAllList is null) + { + MusicAllList = new List(); + var musicDir = Path.Combine(ModBase.ExePath, "PCL", "Musics"); + Directory.CreateDirectory(musicDir); + foreach (var file in ModBase.EnumerateFiles(musicDir)) + { + var ext = file.Extension.ToLowerInvariant(); + // 忽略非音频文件 + if (new[] { ".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png" }.Contains(ext)) + continue; + MusicAllList.Add(file.FullName); + } + } + + // 根据设置决定是否随机 + if (Config.Preference.Music.ShufflePlayback) + MusicWaitingList = RandomUtils.Shuffle(new List(MusicAllList)); + else + MusicWaitingList = new List(MusicAllList); + + // 避免 PreventFirst 成为第一项 + if (PreventFirst is not null && MusicWaitingList.Count > 0 && string.Equals(MusicWaitingList[0], + PreventFirst, StringComparison.OrdinalIgnoreCase)) + { + MusicWaitingList.RemoveAt(0); + MusicWaitingList.Add(PreventFirst); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "初始化音乐列表失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下一首播放的音乐路径并将其从列表中移除。 + /// 如果没有,可能会返回 Nothing。 + /// + private static string DequeueNextMusicAddress() + { + if (MusicAllList is null || !MusicAllList.Any() || !MusicWaitingList.Any()) MusicListInit(false); + + if (MusicWaitingList.Any()) + { + var nextMusic = MusicWaitingList[0]; + MusicWaitingList.RemoveAt(0); + if (!MusicWaitingList.Any()) MusicListInit(false, nextMusic); + return nextMusic; + } + + return null; + } + + #endregion + + #region UI 控制 + + /// + /// 刷新背景音乐按钮 UI 与设置页 UI。 + /// + private static void MusicRefreshUI() + { + ModBase.RunInUi(() => + { + try + { + if ((MusicAllList?.Any() is { } arg1 ? arg1 : null) == false) + { + ModMain.FrmMain.BtnExtraMusic.Show = false; + } + else + { + ModMain.FrmMain.BtnExtraMusic.Show = true; + var fileName = ModBase.GetFileNameWithoutExtentionFromPath(MusicCurrent); + var isSingle = MusicAllList.Count == 1; + string tipText; + if (MusicState == MusicStates.Pause) + { + ModMain.FrmMain.BtnExtraMusic.Logo = ModBase.Logo.IconPlay; + ModMain.FrmMain.BtnExtraMusic.LogoScale = 0.8d; + tipText = $"已暂停:{fileName}"; + tipText += "\r\n" + (isSingle ? "左键恢复播放,右键重新从头播放。" : "左键恢复播放,右键播放下一曲。"); + } + else + { + ModMain.FrmMain.BtnExtraMusic.Logo = ModBase.Logo.IconMusic; + ModMain.FrmMain.BtnExtraMusic.LogoScale = 1d; + tipText = $"正在播放:{fileName}"; + tipText += "\r\n" + (isSingle ? "左键暂停,右键重新从头播放。" : "左键暂停,右键播放下一曲。"); + } + + ModMain.FrmMain.BtnExtraMusic.ToolTip = tipText; + ToolTipService.SetVerticalOffset(ModMain.FrmMain.BtnExtraMusic, + tipText.Contains("\n") ? 10 : 16); + } + + ModMain.FrmSetupUI?.MusicRefreshUI(); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新背景音乐 UI 失败", ModBase.LogLevel.Feedback); + } + }); + } + + public static void MusicControlPause() + { + if (MusicNAudio is null) + { + ModMain.Hint("音乐播放尚未开始!", ModMain.HintType.Critical); + return; + } + + switch (MusicState) + { + case MusicStates.Pause: + { + MusicResume(); + break; + } + case MusicStates.Play: + { + MusicPause(); // Stop + break; + } + + default: + { + ModBase.Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", ModBase.LogLevel.Debug); + MusicRefreshPlay(false); + break; + } + } + } + + public static void MusicControlNext() + { + if (MusicAllList?.Count is { } arg2 && arg2 == 1) + { + MusicStartPlay(MusicCurrent); + ModMain.Hint("重新播放:" + ModBase.GetFileNameFromPath(MusicCurrent), ModMain.HintType.Finish); + } + else + { + var addr = DequeueNextMusicAddress(); + if (addr is null) + { + ModMain.Hint("没有可以播放的音乐!", ModMain.HintType.Critical); + } + else + { + MusicStartPlay(addr); + ModMain.Hint("正在播放:" + ModBase.GetFileNameFromPath(addr), ModMain.HintType.Finish); + } + } + + MusicRefreshUI(); + } + + #endregion + + #region 主状态控制 + + public static MusicStates MusicState + { + get + { + if (MusicNAudio is null) + return MusicStates.Stop; + return MusicNAudio.PlaybackState == PlaybackState.Paused ? MusicStates.Pause : + MusicNAudio.PlaybackState == PlaybackState.Stopped ? MusicStates.Stop : MusicStates.Play; + } + } + + public enum MusicStates + { + Stop, + Play, + Pause + } + + public static void MusicRefreshPlay(bool ShowHint, bool IsFirstLoad = false) + { + try + { + MusicListInit(true); + + if ((MusicAllList?.Any() is { } arg3 ? arg3 : null) == false) + { + if (MusicNAudio is not null) + { + MusicNAudio = null; + if (ShowHint) + ModMain.Hint("背景音乐已清除!", ModMain.HintType.Finish); + } + else if (ShowHint) + { + ModMain.Hint("未检测到可用的背景音乐!", ModMain.HintType.Critical); + } + } + else + { + var addr = DequeueNextMusicAddress(); + if (addr is null) + { + if (ShowHint) + ModMain.Hint("没有可以播放的音乐!", ModMain.HintType.Critical); + } + else + { + try + { + MusicStartPlay(addr, IsFirstLoad); + if (ShowHint) + ModMain.Hint("背景音乐已刷新:" + ModBase.GetFileNameFromPath(addr), ModMain.HintType.Finish, + false); + } + catch + { + // 容错:播放失败已在 MusicLoop 中处理 + } + } + } + + MusicRefreshUI(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "刷新背景音乐播放失败", ModBase.LogLevel.Feedback); + } + } + + private static void MusicStartPlay(string Address, bool IsFirstLoad = false) + { + if (string.IsNullOrEmpty(Address)) + return; + ModBase.Log("[Music] 播放开始:" + Address); + MusicCurrent = Address; + ModBase.RunInNewThread(() => MusicLoop(IsFirstLoad), "Music", ThreadPriority.BelowNormal); + } + + public static bool MusicPause() + { + if (MusicState != MusicStates.Play) + { + ModBase.Log($"[Music] 无需暂停播放,当前状态为 {MusicState}"); + return false; + } + + ModBase.RunInThread(() => + { + ModBase.Log("[Music] 已暂停播放"); + MusicNAudio?.Pause(); + MusicRefreshUI(); + }); + return true; + } + + public static bool MusicResume() + { + if (MusicState == MusicStates.Play || MusicAllList.Count == 0) + { + ModBase.Log($"[Music] 无需继续播放,当前状态为 {MusicState}"); + return false; + } + + ModBase.RunInThread(() => + { + ModBase.Log("[Music] 已恢复播放"); + try + { + MusicNAudio?.Play(); + } + catch + { + // 参考 PR #5415:设备变更后需 Stop + Play + MusicNAudio?.Stop(); + MusicNAudio?.Play(); + } + + MusicRefreshUI(); + }); + return true; + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/ModSecret.cs b/Plain Craft Launcher 2/Modules/ModSecret.cs new file mode 100644 index 000000000..995380517 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModSecret.cs @@ -0,0 +1,870 @@ +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Management; +using System.Net; +using System.Net.Http; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.UI.Theme; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; +using PCL.Core.Utils.OS; +using PCL.Core.Utils.Secret; + +namespace PCL; + +internal static class ModSecret +{ + #region 杂项 + +#if DEBUG + public const string RegFolder = "PCLCEDebug"; // 社区开发版的注册表与社区常规版的注册表隔离,以防数据冲突 +#else + public const string RegFolder = "PCLCE"; // PCL 社区版的注册表与 PCL 的注册表隔离,以防数据冲突 +#endif + + // 用于微软登录的 ClientId + public static readonly string OAuthClientId = + EnvironmentInterop.GetSecret("MS_CLIENT_ID", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + // CurseForge API Key + public static readonly string CurseForgeAPIKey = + EnvironmentInterop.GetSecret("CURSEFORGE_API_KEY", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + // 遥测鉴权密钥 + public static readonly string TelemetryKey = + EnvironmentInterop.GetSecret("TELEMETRY_KEY", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + // Natayark ID Client Id + public static readonly string NatayarkClientId = + EnvironmentInterop.GetSecret("NAID_CLIENT_ID", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + // Natayark ID Client Secret,需要经过 PASSWORD HASH 处理(https://uutool.cn/php-password/) + public static readonly string NatayarkClientSecret = + EnvironmentInterop.GetSecret("NAID_CLIENT_SECRET", readEnvDebugOnly: true).ReplaceNullOrEmpty(); + + // 联机服务根地址 + public static readonly string[] LinkServers = EnvironmentInterop + .GetSecret("LINK_SERVER_ROOT", readEnvDebugOnly: true).ReplaceNullOrEmpty().Split("|"); + + internal static void SecretOnApplicationStart() + { + // 提升 UI 线程优先级 + Thread.CurrentThread.Priority = ThreadPriority.Highest; + // 确保 .NET Framework 版本 + try + { + var VersionTest = new FormattedText("", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, + Fonts.SystemTypefaces.First(), 96d, new ModBase.MyColor(), ModBase.DPI); + } + catch (UriFormatException ex) // 修复 #3555 + { + Environment.SetEnvironmentVariable("windir", Environment.GetEnvironmentVariable("SystemRoot"), + EnvironmentVariableTarget.User); + var VersionTest = new FormattedText("", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, + Fonts.SystemTypefaces.First(), 96d, new ModBase.MyColor(), ModBase.DPI); + } + + // 检测当前文件夹权限 + var dataPath = Paths.Data; + try + { + Directory.CreateDirectory(dataPath); + } + catch (Exception ex) + { + Interaction.MsgBox( + $"PCL 无法创建 PCL 文件夹({dataPath}),请尝试:" + "\r\n" + "1. 将 PCL 移动到其他文件夹" + + (ModBase.ExePath.StartsWithF("C:", true) ? ",例如 C 盘和桌面以外的其他位置。" : "。") + "\r\n" + + "2. 删除当前目录中的 PCL 文件夹,然后再试。" + "\r\n" + "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。", + MsgBoxStyle.Critical, "运行环境错误"); + Environment.Exit((int)ModBase.ProcessReturnValues.Cancel); + } + + if (!ModBase.CheckPermission(ModBase.ExePath + "PCL")) + { + Interaction.MsgBox( + "PCL 没有对当前文件夹的写入权限,请尝试:" + "\r\n" + "1. 将 PCL 移动到其他文件夹" + + (ModBase.ExePath.StartsWithF("C:", true) ? ",例如 C 盘和桌面以外的其他位置。" : "。") + "\r\n" + + "2. 删除当前目录中的 PCL 文件夹,然后再试。" + "\r\n" + "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。", + MsgBoxStyle.Critical, "运行环境错误"); + Environment.Exit((int)ModBase.ProcessReturnValues.Cancel); + } + } + + /// + /// 展示社区版提示 + /// + /// 是否为更新时启动 + public static void ShowCEAnnounce() + { + ModMain.MyMsgBox(@"你正在使用来自 PCL-Community 的 PCL 社区版本,遇到问题请不要向官方仓库反馈! +PCL-Community 及其成员与龙腾猫跃无从属关系,且均不会为您的使用做担保。 + +如果你是意外下载的社区版,建议下载官方版 PCL 使用。 +如果你是意外下载的社区版,建议下载官方版 PCL 使用。 +如果你是意外下载的社区版,建议下载官方版 PCL 使用。 + +该版本与官方版本的特性区别: +- 主题切换:仅部分固定蓝色系主题,没有计划新增其它主题。 +- 百宝箱:缺失部分官方版中的内容(回声洞、千万别点)。 + +此提示会在启动器更新后展示一次。", "社区版本说明", "我知道了"); + } + + /// + /// 获取设备的短标识码 + /// + internal static string SecretGetUniqueAddress() + { + return Identify.LauncherId; + } + + internal static void SecretLaunchJvmArgs(ref List DataList) + { + var DataJvmCustom = + Conversions.ToString(ModBase.Setup.Get("VersionAdvanceJvm", ModMinecraft.McInstanceSelected)); + DataList.Insert(0, + Conversions.ToString(string.IsNullOrEmpty(DataJvmCustom) + ? Config.Launch.JvmArgs + : DataJvmCustom)); // 可变 JVM 参数 + switch (Config.Launch.PreferredIpStack) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): + { + DataList.Add("-Djava.net.preferIPv4Stack=true"); + DataList.Add("-Djava.net.preferIPv4Addresses=true"); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 2, false): + { + DataList.Add("-Djava.net.preferIPv6Stack=true"); + DataList.Add("-Djava.net.preferIPv6Addresses=true"); + break; + } + } + + ModLaunch.McLaunchLog("当前剩余内存:" + + Math.Round((double)(KernelInterop.GetAvailablePhysicalMemoryBytes() + / 1024 / 1024 / 1024 * 10)) / + 10 + "G"); + DataList.Add("-Xmn" + Math.Floor(PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected) * 1024d * 0.15d) + + "m"); + DataList.Add("-Xmx" + Math.Floor(PageInstanceSetup.GetRam(ModMinecraft.McInstanceSelected) * 1024d) + "m"); + if (!DataList.Any(d => d.Contains("-Dlog4j2.formatMsgNoLookups=true"))) + DataList.Add("-Dlog4j2.formatMsgNoLookups=true"); + } + + #endregion + + #region 网络鉴权 + + internal static object SecretCdnSign(string UrlWithMark) + { + if (!UrlWithMark.EndsWithF("{CDN}")) + return UrlWithMark; + return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20"); + } + + /// + /// 设置 Headers 的 UA、Referer。 + /// + internal static void SecretHeadersSign(string Url, ref HttpRequestMessage Client, bool UseBrowserUserAgent = false, + string CustomUserAgent = "") + { + Client.Version = HttpVersion.Version20; + Client.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower; + if (Url.Contains("api.curseforge.com")) + Client.Headers.Add("x-api-key", CurseForgeAPIKey); + var userAgent = !string.IsNullOrEmpty(CustomUserAgent) + ? CustomUserAgent + : UseBrowserUserAgent + ? $"PCL2/{ModBase.UpstreamVersion}.{ModBase.VersionBranchCode} PCLCE/{ModBase.VersionStandardCode} Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0" + : $"PCL2/{ModBase.UpstreamVersion}.{ModBase.VersionBranchCode} PCLCE/{ModBase.VersionStandardCode}"; + Client.Headers.Add("User-Agent", userAgent); + + Client.Headers.Add("Referer", "http://" + ModBase.VersionCode + ".ce.open.pcl2.server/"); + } + + #endregion + + #region 主题 + +#if DEBUG + public static readonly bool EnableCustomTheme = Environment.GetEnvironmentVariable("PCL_CUSTOM_THEME") is not null; + private static readonly object EnvThemeHue = Environment.GetEnvironmentVariable("PCL_THEME_HUE"); // 0 ~ 359 + private static readonly object EnvThemeSat = Environment.GetEnvironmentVariable("PCL_THEME_SAT"); // 0 ~ 100 + private static readonly object EnvThemeLight = Environment.GetEnvironmentVariable("PCL_THEME_LIGHT"); // -20 ~ 20 + + private static readonly object + EnvThemeHueDelta = Environment.GetEnvironmentVariable("PCL_THEME_HUE_DELTA"); // -90 ~ 90 + + private static readonly object CustomThemeHue = + EnvThemeHue is null ? default(int?) : int.Parse((dynamic)EnvThemeHue); + + private static readonly object CustomThemeSat = + EnvThemeSat is null ? default(int?) : int.Parse((dynamic)EnvThemeSat); + + private static readonly object CustomThemeLight = + EnvThemeLight is null ? default(int?) : int.Parse((dynamic)EnvThemeLight); + + private static readonly object CustomThemeHueDelta = + EnvThemeHueDelta is null ? default(int?) : int.Parse((dynamic)EnvThemeHueDelta); +#else + public static readonly bool EnableCustomTheme = false; +#endif + + public static bool IsDarkMode => ThemeService.IsDarkMode; + + public static ResourceDictionary AppResources => System.Windows.Application.Current.Resources; + + public static ModBase.MyColor ColorGray1 = new(AppResources["ColorObjectGray1"]); + public static ModBase.MyColor ColorGray4 = new(AppResources["ColorObjectGray4"]); + public static ModBase.MyColor ColorGray5 = new(AppResources["ColorObjectGray5"]); + public static ModBase.MyColor ColorSemiTransparent = new(AppResources["ColorBrushSemiTransparent"]); + + public static int ThemeNow = -1; + + // Public ColorHue As Integer = If(IsDarkMode, 200, 210), ColorSat As Integer = If(IsDarkMode, 100, 85), ColorLightAdjust As Integer = If(IsDarkMode, 15, 0), ColorHueTopbarDelta As Object = 0 + public static int ThemeDontClick = 0; + + // 深色模式事件 + /* TODO ERROR: Skiped IfDirectiveTrivia + #If False + */ /* TODO ERROR: Skipped DisabledTextTrivia + ' 定义自定义事件 + Public Event ThemeChanged As EventHandler(Of Boolean) + + ' 触发事件的函数 + Public Sub RaiseThemeChanged(isDarkMode As Boolean) + RaiseEvent ThemeChanged("", isDarkMode) + End Sub + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + public static void ThemeRefresh(int NewTheme = -1) + { + // ThemeRefreshColor() + // RaiseThemeChanged(IsDarkMode) + ColorGray1 = new ModBase.MyColor(AppResources["ColorObjectGray1"]); + ColorGray4 = new ModBase.MyColor(AppResources["ColorObjectGray4"]); + ColorGray5 = new ModBase.MyColor(AppResources["ColorObjectGray5"]); + ColorSemiTransparent = new ModBase.MyColor(AppResources["ColorBrushSemiTransparent"]); + ThemeRefreshMain(); + } + + public static double GetDarkThemeLight(double OriginalLight) + { + if (IsDarkMode) return OriginalLight * 0.2d; + + return OriginalLight; + } + + /* TODO ERROR: Skipped IfDirectiveTrivia + #If False + */ /* TODO ERROR: Skipped DisabledTextTrivia + Private ReadOnly HueList As Integer() = {200, 210, 225} + Private ReadOnly SatList As Integer() = {100, 85, 70} + Private ReadOnly LightList As Integer() = {7, 0, -2} + + Public Sub ThemeRefreshColor() + #If DEBUG Then + If EnableCustomTheme Then + If CustomThemeHue IsNot Nothing Then ColorHue = CustomThemeHue + If CustomThemeSat IsNot Nothing Then ColorSat = CustomThemeSat + If CustomThemeLight IsNot Nothing Then ColorLightAdjust = CustomThemeLight + If CustomThemeHueDelta IsNot Nothing Then ColorHueTopbarDelta = CustomThemeHueDelta + Else + #End If + Dim colorIndex As Integer = If(IsDarkMode, Setup.Get("UiDarkColor"), Setup.Get("UiLightColor")) + ColorHue = HueList(colorIndex) + ColorSat = SatList(colorIndex) + ColorLightAdjust = LightList(colorIndex) + ColorHueTopbarDelta = 0 + #If DEBUG Then + End If + #End If + End Sub + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + public static void ThemeRefreshMain() + { + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUG Then + */ + if (EnableCustomTheme) + ThemeNow = 14; + /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + ModBase.RunInUi(() => + { + if (!ModMain.FrmMain.IsLoaded) + return; + /* TODO ERROR: Skipped IfDirectiveTrivia + #If False + */ /* TODO ERROR: Skipped DisabledTextTrivia + '顶部条背景 + Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)} + Dim lightAdjust = ColorLightAdjust * 1.2 + If ThemeNow = 5 Then + Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 25)}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 15)}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 25)}) + FrmMain.PanTitle.Background = Brush + FrmMain.PanTitle.Background.Freeze() + ElseIf Not (ThemeNow = 12 OrElse ThemeDontClick = 2) Then + If TypeOf ColorHueTopbarDelta Is Integer Then + Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue - ColorHueTopbarDelta, ColorSat, AdjustLight(48, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, AdjustLight(54, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta, ColorSat, AdjustLight(48, lightAdjust))}) + Else + Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(0), ColorSat, AdjustLight(48, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(1), ColorSat, AdjustLight(54, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(2), ColorSat, AdjustLight(48, lightAdjust))}) + End If + FrmMain.PanTitle.Background = Brush + FrmMain.PanTitle.Background.Freeze() + Else + Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue - 21, ColorSat, AdjustLight(53, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 0.33, .Color = New MyColor().FromHSL2(ColorHue - 7, ColorSat, AdjustLight(47, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 0.67, .Color = New MyColor().FromHSL2(ColorHue + 7, ColorSat, AdjustLight(47, lightAdjust))}) + Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + 21, ColorSat, AdjustLight(53, lightAdjust))}) + FrmMain.PanTitle.Background = Brush + End If + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ // 主页面背景 + if (Conversions.ToBoolean(Config.Preference.Background.BackgroundColorful)) + { + LinearGradientBrush brush = new() + { + EndPoint = new Point(0.1, 1), + StartPoint = new Point(0.9, 0) + }; + + var hue = ThemeService.GetCurrentThemeArgs().Hue; + var hue1 = hue - 15; + var hue2 = hue + 15; + var tone = ThemeService.CurrentTone; + brush.GradientStops.Add(new GradientStop + { Offset = -0.1d, Color = LabColor.FromLch(GetDarkThemeLight(0.84d), tone.C5, hue1) }); + brush.GradientStops.Add(new GradientStop + { Offset = 0.4d, Color = LabColor.FromLch(GetDarkThemeLight(0.96d), tone.C7, hue) }); + brush.GradientStops.Add(new GradientStop + { Offset = 1.1d, Color = LabColor.FromLch(GetDarkThemeLight(0.84d), tone.C5, hue2) }); + ModMain.FrmMain.PanForm.Background = brush; + } + else + { + ModMain.FrmMain.PanForm.Background = + (Brush)System.Windows.Application.Current.Resources["ColorBrushBackground"]; + } + + ModMain.FrmMain.PanForm.Background.Freeze(); + + // 通用ContextMenu主题刷新 + RefreshAllContextMenuThemes(); + ModMain.FrmMain.PanTitleSelect.Children.OfType().ToList() + .ForEach(btn => btn.RefreshMyRadioButtonColor()); + }); + } + + internal static void ThemeCheckAll(bool EffectSetup) + { + } + + internal static bool ThemeCheckOne(int Id) + { + return true; + } + + internal static bool ThemeUnlock(int Id, bool ShowDoubleHint = true, string UnlockHint = null) + { + return false; + } + + internal static bool ThemeCheckGold(string Code = null) + { + return false; + } + + internal static bool? DonateCodeInput() + { + return default; + } + + #endregion + + #region 更新 + + public static bool IsCheckingUpdates = false; + public static bool IsUpdateWaitingRestart; + + public static UpdatesWrapperModel RemoteServer = new(new List + { + 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 enum VersionStatus + { + Latest, + NotLatest, + Unknown + } + + public static VersionStatus GetVersionStatus() + { + try + { + if (IsCurrentVersionBeta && !((int)Config.Update.UpdateChannel == 1)) + { + var isNewerThanStable = RemoteServer.IsLatest(UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.VersionBaseName), + ModBase.VersionCode); + var isBetaLatest = RemoteServer.IsLatest(UpdateChannel.beta, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.VersionBaseName), + ModBase.VersionCode); + return (VersionStatus)Conversions.ToInteger(isNewerThanStable && isBetaLatest); + } + + return RemoteServer.IsLatest( + Conversions.ToBoolean(IsCurrentVersionBeta) ? UpdateChannel.beta : UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, SemVer.Parse(ModBase.VersionBaseName), + ModBase.VersionCode) + ? VersionStatus.Latest + : VersionStatus.NotLatest; + } + catch (Exception ex) + { + ModBase.Log(ex, "无法获取最新版本信息,请检查网络连接", ModBase.LogLevel.Hint); + return VersionStatus.Unknown; + } + } + + public enum UpdateType + { + Silent = 0, + PromptOnly = 1, + DownloadAndPrompt = 2, + UpdateNow = 3 + } + + public static ModLoader.LoaderCombo UpdateLoader; + + public static void UpdateStart(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, + ModBase.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 == UpdateType.PromptOnly) + { + ModBase.Log("[Test]"); + ModBase.RunInUi(() => + { + if (ModMain.MyMsgBox( + $"启动器有新版本可用({ModBase.VersionBaseName} -> {version.VersionName}){"\r\n"}是否立即更新?", + "启动器更新", "更新", "取消") == + 1) ModMain.FrmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupUpdate); + }); + return; + // 构造步骤加载器 + } + + var loaders = new List(); + // 下载 + loaders.AddRange(RemoteServer.GetDownloadLoader( + Conversions.ToBoolean(IsCurrentVersionBeta) ? UpdateChannel.beta : UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, dlTargetPath)); + loaders.Add(new ModLoader.LoaderTask("校验更新", _ => + { + var curHash = ModBase.GetFileSHA256(dlTargetPath); + if ((curHash ?? "") != (version.SHA256 ?? "")) + throw new Exception($"更新文件 SHA256 不正确,应该为 {version.SHA256},实际为 {curHash}"); + })); + if (type == UpdateType.UpdateNow) + loaders.Add(new ModLoader.LoaderTask("安装更新", _ => UpdateRestart(true))); + else if (type == UpdateType.Silent) + loaders.Add(new ModLoader.LoaderTask("准备更新", _ => IsUpdateWaitingRestart = true)); + else if (type == UpdateType.DownloadAndPrompt) + loaders.Add(new ModLoader.LoaderTask("显示按钮", _ => + { + IsUpdateWaitingRestart = true; + ModBase.RunInUi(() => + { + ModMain.FrmMain.BtnExtraUpdateRestart.ToolTip = + $"重启 PCL CE 以应用软件更新 ({ModBase.VersionBaseName} → {version.VersionName})"; + ModMain.FrmMain.BtnExtraUpdateRestart.ShowRefresh(); + ModMain.FrmMain.BtnExtraUpdateRestart.Ribble(); + }); + }) + { + Show = false + }); + loaders.Add(new ModLoader.LoaderTask("刷新设置 UI", _ => + { + if (ModMain.FrmSetupUpdate is not null) + ModBase.RunInUi(() => + { + ModMain.FrmSetupUpdate.BtnUpdate.Text = "重启安装"; + ModMain.FrmSetupUpdate.BtnUpdate.IsEnabled = true; + }); + }) + { + Show = false + }); + // 启动 + UpdateLoader = new ModLoader.LoaderCombo("启动器更新", loaders); + UpdateLoader.Start(); + if (type == UpdateType.UpdateNow) + { + ModLoader.LoaderTaskbarAdd(UpdateLoader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "[Update] 获取启动器更新失败"); + if (type != UpdateType.Silent) + ModMain.Hint("获取启动器更新失败,请检查网络连接", ModMain.HintType.Critical); + } + }); + } + + 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} \"{ModBase.ExePathWithName}\" \"{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 错误,疑似被拦截"); + if (ModMain.MyMsgBox( + string.Format( + @"由于被 Windows 安全中心拦截,或者存在权限问题,导致 PCL 无法更新。{0}请将 PCL 所在文件夹加入白名单,或者手动用 {1}PCL\Plain Craft Launcher Community Edition.exe 替换当前文件!", + "\r\n", ModBase.ExePath), "更新失败", "查看帮助", "确定", "", true) == + 1) ModEvent.TryStartEvent("打开帮助", "启动器/Microsoft Defender 添加排除项.json"); + } + } + + /// + /// 确保 PathTemp 下的 Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。 + /// 如果不是,则下载一个。 + /// + internal static void DownloadLatestPCL(ModLoader.LoaderBase LoaderToSyncProgress = null) + { + // 注意:如果要自行实现这个功能,请换用另一个文件路径,以免与官方版本冲突 + var LatestPCLPath = ModBase.PathTemp + "CE-Latest.exe"; + var target = RemoteServer.GetLatestVersion(UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64); + if (target is null) + throw new Exception("无法获取更新"); + if (File.Exists(LatestPCLPath) && (ModBase.GetFileSHA256(LatestPCLPath) ?? "") == (target.SHA256 ?? "")) + { + ModBase.Log("[System] 最新版 PCL 已存在,跳过下载"); + return; + } + + if ((ModBase.GetFileSHA256(ModBase.ExePathWithName) ?? "") == (target.SHA256 ?? "")) // 正在使用的版本符合要求,直接拿来用 + { + ModBase.CopyFile(ModBase.ExePathWithName, LatestPCLPath); + return; + } + + var loaders = RemoteServer.GetDownloadLoader(UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, LatestPCLPath); + var loader = new ModLoader.LoaderCombo("下载最新稳定版", loaders); + loader.Start(); + loader.WaitForExit(); + } + + #endregion + + #region 联网通知 + + public static ModLoader.LoaderTask ServerLoader = new("PCL CE 服务", _ => LoadOnlineInfo(), + Priority: ThreadPriority.BelowNormal); + + private static void LoadOnlineInfo() + { + var updateDesire = Config.Update.UpdateMode; + var AnnouncementDesire = States.System.AnnounceSolution; + switch (updateDesire) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, 0, false): // 静默更新 + { + ModBase.Log("[Update] 更新设置: 自动下载并安装更新"); + if (GetVersionStatus() != VersionStatus.Latest) UpdateStart(UpdateType.Silent); + + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, 1, false): // 自动下载,提示更新 + { + ModBase.Log("[Update] 更新设置: 自动下载并提示更新"); + UpdateStart(UpdateType.DownloadAndPrompt); + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, 2, false): // 提示更新 + { + ModBase.Log("[Update] 更新设置: 提示更新"); + UpdateStart(UpdateType.PromptOnly); + break; + } + + default: + { + ModBase.Log("[Update] 更新设置: 不自动检查更新"); + return; + } + } + + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectLessEqual(AnnouncementDesire, 1, false))) + { + var ShowedAnnounced = States.Hint.ShowedAnnouncements.ToString() + .Split("|".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList(); + var ShowAnnounce = RemoteServer.GetAnnouncementList().content.Where(x => !ShowedAnnounced.Contains(x.id)) + .ToList(); + ModBase.Log("[System] 需要展示的公告数量:" + ShowAnnounce.Count); + ModBase.RunInNewThread(() => + { + foreach (var item in ShowAnnounce) + { + var SelectedBtn = ModMain.MyMsgBox(item.detail, item.title, item.btn1 is null ? "" : item.btn1.text, + item.btn2 is null ? "" : item.btn2.text, "关闭", + Button1Action: () => ModEvent.TryStartEvent(item.btn1.command, item.btn1.command_paramter), + Button2Action: () => ModEvent.TryStartEvent(item.btn2.command, item.btn2.command_paramter)); + } + }); + ShowedAnnounced.AddRange(ShowAnnounce.Select(x => x.id).ToList()); + ShowedAnnounced = ShowedAnnounced.Distinct().ToList(); + States.Hint.ShowedAnnouncements = ShowedAnnounced.Join("|"); + } + } + + #endregion + + #region 系统信息 + + internal static string CPUName; + + /// + /// 系统 GPU 信息 + /// + internal static List GPUs = new(); + + /// + /// 已安装物理内存大小,单位 MB + /// + internal static long SystemMemorySize = (long)KernelInterop.GetPhysicalMemoryBytes().Total / 1024 / 1024; + + /// + /// 系统信息描述,例如 Microsoft Windows 11 专业工作站版 10.0.22635.0 + /// + public static string OSInfo = RuntimeInformation.OSDescription + " " + Environment.OSVersion.Version; + + public class GPUInfo + { + internal string DriverVersion; + + /// + /// 显存大小,单位 MB + /// + internal long Memory; + + internal string Name; + } + + /// + /// 获取系统信息,例如 CPU 与 GPU,并存储到 CPUName 和 GPUs + /// + internal static void GetSystemInfo() + { + // CPU + try + { + var searcher = new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_Processor"); + + foreach (ManagementObject queryObj in searcher.Get()) + { + CPUName = queryObj["Name"].ToString().Trim(); + break; // 通常只需要第一个CPU的信息 + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 CPU 信息时出错", ModBase.LogLevel.Normal); + } + + // GPU + try + { + var searcher = new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_VideoController"); + + foreach (ManagementObject queryObj in searcher.Get()) + { + var gpuInfo = new GPUInfo(); + + if (queryObj["Name"] is not null) gpuInfo.Name = Conversions.ToString(queryObj["Name"]); + if (queryObj["AdapterRAM"] is not null) + { + var ramMB = Conversions.ToLong(queryObj["AdapterRAM"]) / (1024 * 1024); + gpuInfo.Memory = ramMB; + } + + if (queryObj["DriverVersion"] is not null) + gpuInfo.DriverVersion = Conversions.ToString(queryObj["DriverVersion"]); + + GPUs.Add(gpuInfo); + } + + ModBase.Log("已获取系统环境信息"); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 GPU 信息时出错", ModBase.LogLevel.Normal); + } + } + + #endregion + + #region 主题 + + /// + /// 通用的ContextMenu主题刷新方法 + /// + private static void RefreshAllContextMenuThemes() + { + try + { + // 注册全局的ContextMenu主题刷新事件处理器 + EventManager.RegisterClassHandler(typeof(ContextMenu), ContextMenu.OpenedEvent, + new RoutedEventHandler(OnContextMenuOpened)); + + // 刷新当前打开的ContextMenu + // 获取当前应用程序中所有的窗口 + ModBase.RunInUi(() => + { + foreach (Window window in System.Windows.Application.Current.Windows) + RefreshContextMenusInElement(window); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新ContextMenu主题时出错"); + } + } + + /// + /// ContextMenu打开事件处理器,确保在显示时应用正确主题 + /// + private static void OnContextMenuOpened(object sender, RoutedEventArgs e) + { + try + { + if (sender is ContextMenu) + { + var contextMenu = (ContextMenu)sender; + // 强制重新应用样式 + contextMenu.ClearValue(FrameworkElement.StyleProperty); + contextMenu.UpdateDefaultStyle(); + } + } + catch (Exception ex) + { + // 忽略个别错误 + } + } + + /// + /// 递归刷新元素及其子元素中的ContextMenu + /// + private static void RefreshContextMenusInElement(DependencyObject element) + { + if (element is null) + return; + + try + { + // 检查当前元素是否有ContextMenu + if (element is FrameworkElement) + { + var fe = (FrameworkElement)element; + if (fe.ContextMenu is not null) + { + // 强制重新应用样式 + fe.ContextMenu.ClearValue(FrameworkElement.StyleProperty); + fe.ContextMenu.UpdateDefaultStyle(); + } + } + + // 递归处理子元素 + var childrenCount = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0, loopTo = childrenCount - 1; i <= loopTo; i++) + { + var child = VisualTreeHelper.GetChild(element, i); + RefreshContextMenusInElement(child); + } + } + catch (Exception ex) + { + // 忽略个别元素的错误,继续处理其他元素 + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/ModVideoBack.cs b/Plain Craft Launcher 2/Modules/ModVideoBack.cs new file mode 100644 index 000000000..ded5a573f --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModVideoBack.cs @@ -0,0 +1,174 @@ +namespace PCL; + +public static class ModVideoBack +{ + private static bool _isGaming; + private static bool _forcePlay; + public static bool IsMinimized = false; // 窗口是否被最小化 + + public static bool IsGaming // 判断用户是否在游戏中 + { + get => _isGaming; + set + { + if (_isGaming != value) + { + _isGaming = value; + GamingStateChanged?.Invoke(null, new BooleanEventArgs(value)); + } + } + } + + public static bool ForcePlay // 判断是否强行播放 + { + get => _forcePlay; + set + { + if (_forcePlay != value) + { + _forcePlay = value; + ForcePlayChanged?.Invoke(null, new BooleanEventArgs(value)); + } + } + } + + public static event EventHandler? GamingStateChanged; + public static event EventHandler? ForcePlayChanged; + + public static void OnGamingStateChanged(object sender, BooleanEventArgs e) // 用户是否在游戏中 事件 + { + ModBase.RunInUi(() => + { + if (IsGaming) + { + if (ForcePlay) + { + if (!(ModMain.FrmSetupUI == null)) ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = true; + } + else if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = false; + } + } + else if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = true; + } + }); + } + + public static void OnForcePlayChanged(object sender, BooleanEventArgs e) // 是否强行播放 事件 + { + ModBase.RunInUi(() => + { + if (IsGaming) + { + if (ForcePlay) + { + if (!(ModMain.FrmSetupUI == null)) ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = true; + } + else if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = false; + } + } + else if (!(ModMain.FrmSetupUI == null)) + { + ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = true; + } + }); + } + + /// + /// 尝试开始视频背景播放 + /// + public static void VideoPlay() + { + ModBase.RunInUi(() => + { + if (ModMain.FrmMain.VideoBack.Source is not null & !IsMinimized) + if (!IsGaming | ForcePlay) + try + { + ModMain.FrmMain.VideoBack.Play(); + ModBase.Log("[UI] 已开始视频背景播放"); + } + catch (Exception ex) + { + ModBase.Log(ex, "[UI] 开始视频背景播放失败"); + } + }); + } + + /// + /// 尝试停止视频背景播放 + /// + public static void VideoStop() + { + ModBase.RunInUi(() => + { + try + { + ModMain.FrmMain.VideoBack.Source = null; + ModMain.FrmMain.VideoBack.Stop(); + ModMain.FrmMain.VideoBack.Position = TimeSpan.Zero; + ModBase.Log("[UI] 已停止视频背景播放"); + } + catch (Exception ex) + { + ModBase.Log(ex, "[UI] 停止视频背景播放失败"); + } + }); + } + + /// + /// 尝试暂停视频背景播放 + /// + public static void VideoPause() + { + // 窗口最小化后暂停 + // 游戏启动后暂停 + ModBase.RunInUi(() => + { + if (IsMinimized) + { + if (ModMain.FrmMain.VideoBack.Source is not null) + try + { + ModMain.FrmMain.VideoBack.Pause(); + ModBase.Log("[UI] 已暂停视频背景播放"); + } + catch (Exception ex) + { + ModBase.Log(ex, "[UI] 暂停视频背景播放失败"); + } + } + else if (ForcePlay) + { + } + else if (ModMain.FrmMain.VideoBack.Source is not null) + { + try + { + if (!(ModMain.FrmSetupUI == null)) ModMain.FrmSetupUI.BtnBackgroundRefresh.IsEnabled = false; + ModMain.FrmMain.VideoBack.Pause(); + ModBase.Log("[UI] 已暂停视频背景播放"); + } + catch (Exception ex) + { + ModBase.Log(ex, "[UI] 暂停视频背景播放失败"); + } + } + }); + } + + public class BooleanEventArgs : EventArgs + { + public BooleanEventArgs(bool value) + { + Value = value; + } + + public bool Value { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/ModWebServer.cs b/Plain Craft Launcher 2/Modules/ModWebServer.cs new file mode 100644 index 000000000..731563336 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/ModWebServer.cs @@ -0,0 +1,476 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using Microsoft.VisualBasic; +using PCL.Core.IO.Net.Http.Server; +using PCL.Core.Link.Natayark; + +namespace PCL; + +public static class ModWebServer +{ + private static readonly Dictionary _webServers = new(); + + /// + /// 在新的 中开始 HTTP 服务端响应。 + /// + /// 服务端名称 + /// 服务端实例 + /// 是否成功开始,若已存在同名实例则返回 false + public static bool StartWebServer(string name, HttpServer server) + { + name = name.ToLowerInvariant(); + lock (_webServers) + { + if (_webServers.ContainsKey(name)) + return false; + _webServers[name] = server; + } + + Task.Run(() => + { + ModBase.Log($"[WebServer] 服务端 '{name}' 已启动"); + try + { + server.Start(); + // 保持服务器运行直到被停止(通过检查字典中是否还存在该服务器) + while (true) + { + lock (_webServers) + { + if (!_webServers.ContainsKey(name)) + break; + } + + Thread.Sleep(100); + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"[WebServer] 服务端 '{name}' 运行出错"); + } + finally + { + try + { + server.Dispose(); + } + catch + { + // 忽略已释放的异常 + } + + ModBase.Log($"[WebServer] 服务端 '{name}' 已停止"); + lock (_webServers) + { + _webServers.Remove(name); + } + } + }); + return true; + } + + /// + /// 检查指定名称的 HTTP 服务端是否正在运行 + /// + /// 服务端名称 + /// 是否正在运行 + public static bool IsWebServerRunning(string name) + { + name = name.ToLowerInvariant(); + return _webServers.ContainsKey(name); + } + + /// + /// 销毁 HTTP 服务端。若服务端正在运行,可能会引发异常。 + /// + /// 服务端名称 + /// 是否成功销毁,若名称不存在或已经销毁则返回 false + public static bool DisposeWebServer(string name) + { + name = name.ToLowerInvariant(); + lock (_webServers) + { + if (!_webServers.ContainsKey(name)) + return false; + try + { + _webServers[name].Dispose(); + } + catch (ObjectDisposedException ex) + { + return false; + } + + _webServers.Remove(name); + return true; + } + } + + #region 网页登录回调 + + public class NaidCallbackServer : HttpServer + { + private readonly OAuthComplete _completeCallback; + private readonly string _picAddress; + private readonly string _serviceName; + private string _callbackContent; + private IDictionary _callbackParameters; + + private OAuthCompleteStatus _status; + + public NaidCallbackServer(string serviceName, OAuthComplete completeCallback, string picAddress) : base(new[] + { IPAddress.Parse("127.0.0.1") }) + { + _serviceName = serviceName; + _completeCallback = completeCallback; + _picAddress = picAddress; + } + + protected override void Init() + { + // 注册回调路由 + Register(HttpMethod.Get, "/callback", HandleCallback); + Register(HttpMethod.Post, "/callback", HandleCallback); + + // 注册状态路由 + Register(HttpMethod.Get, "/status", HandleStatus); + + // 注册资源路由 + Register(HttpMethod.Get, "/assets/background", HandleBackground); + Register(HttpMethod.Get, "/assets/icon", HandleIcon); + Register(HttpMethod.Get, "/complete", HandleComplete); + } + + private Task HandleCallback(HttpListenerRequest request) + { + if (!request.IsLocal) + return HttpRouteResponse.Forbidden.AsTask(); + + var redirect = HttpRouteResponse.Redirect("/complete").AsTask(); + + // 解析回调 URL 参数 + var parameterMap = new Dictionary(); + var query = request.Url.Query; + var queryIndex = query.IndexOf('?'); + if (queryIndex != -1 && query.Length > queryIndex) + try + { + var sq = query.Substring(queryIndex + 1).Split('&'); + var splitChar = new[] { '=' }; + foreach (var iq in sq) + { + var q = iq.Split(splitChar, 2); + if (q.Length == 2) parameterMap[q[0]] = q[1]; + } + } + catch (Exception ex) + { + _status = OAuthCompleteStatus.Failed("回调参数解析出错", ex); + return redirect; + } + + _callbackParameters = parameterMap; + + // 读取回调内容 + if (request.HasEntityBody) + try + { + using (var reader = new StreamReader(request.InputStream, request.ContentEncoding)) + { + _callbackContent = reader.ReadToEnd(); + } + } + catch (Exception ex) + { + _status = OAuthCompleteStatus.Failed("读取回调内容出错", ex); + return redirect; + } + + return redirect; + } + + private Task HandleStatus(HttpListenerRequest request) + { + if (_callbackParameters is null) + return HttpRouteResponse.NotFound.AsTask(); + + try + { + if (_status is null) + { + _callbackParameters["Port"] = Port.ToString(); + _status = _completeCallback(true, _callbackParameters, _callbackContent); + } + else if (!_status.success) + { + ModBase.Log($"[OAuth] {_serviceName}: {_status.message}{"\r\n"}{_status.stacktrace}"); + var pa = new Dictionary(); + pa["Port"] = Port.ToString(); + _completeCallback(false, pa, _status.message); + } + } + catch (Exception ex) + { + _status = OAuthCompleteStatus.Failed("处理回调出错", ex); + } + + return HttpRouteResponse.Json(_status).AsTask(); + } + + private Task HandleBackground(HttpListenerRequest request) + { + if (string.IsNullOrWhiteSpace(_picAddress)) + return Task.FromResult(HttpRouteResponse.NotFound); + return HttpRouteResponse + .Input(new FileStream(_picAddress, FileMode.Open, FileAccess.Read, FileShare.None, 16384, true)) + .AsTask(); + } + + private Task HandleIcon(HttpListenerRequest request) + { + return HttpRouteResponse.Input(ModBase.GetResourceStream("Images/icon.ico")).AsTask(); + } + + private Task HandleComplete(HttpListenerRequest request) + { + return HttpRouteResponse.Input(ModBase.GetResourceStream("Resources/oauth-complete.html"), "text/html") + .AsTask(); + } + } + + private static readonly object ChangeLock = new(); + private static string PicAddress; + + public static object BackgroundPicChangeCallback(string Pic) + { + lock (ChangeLock) + { + PicAddress = Pic; + return true; + } + } + + public class OAuthCompleteStatus + { + public bool success { get; set; } + public string username { get; set; } + public string message { get; set; } + public string stacktrace { get; set; } + + public static OAuthCompleteStatus Complete(string username) + { + return new OAuthCompleteStatus { success = true, username = username }; + } + + public static OAuthCompleteStatus Failed(string message, Exception ex = null) + { + var init = new OAuthCompleteStatus(); + return (init.success = false, init.message = message, init.stacktrace = ex?.ToString(), init).init; + } + } + + public delegate OAuthCompleteStatus? OAuthComplete(bool success, IDictionary parameters, + string content); + + public static bool StartOAuthWaitingCallback(string serviceName, string url, OAuthComplete completeCallback) + { + if (IsWebServerRunning(serviceName)) + return false; + ModBase.RunInNewThread(() => + { + var serverPort = 0; + lock (_webServers) + { + NaidCallbackServer? server; + + string currentPicAddress; + lock (ChangeLock) + { + currentPicAddress = PicAddress; + } + + server = new NaidCallbackServer(serviceName, completeCallback, currentPicAddress); + serverPort = server.Port; + ModBase.Log($"[OAuth] {serviceName}: 已开始监听 {server.Port} 端口,正在初始化路由"); + // 开始响应请求 + var webServiceName = $"oauth/{serviceName}"; + if (DisposeWebServer(webServiceName)) ModBase.Log("[OAuth] 已关闭先前认证服务服务端"); + StartWebServer(webServiceName, server); + ModBase.Log($"[OAuth] {serviceName}: 初始化完成,开始响应 HTTP 请求"); + } + + // 打开 OAuth URL + ModBase.OpenWebsite(url.Replace("%r", $"http://localhost:{serverPort}/callback")); + }, $"CallbackWebServerLoading/{serviceName}"); + return true; + } + + public static void StartNaidAuthorize(Action? completeCallback = null) + { + Exception? resultEx = null; + StartOAuthWaitingCallback("NatayarkID", + $"https://account.naids.com/oauth2/authorize?response_type=code&client_id={ModSecret.NatayarkClientId}&redirect_uri=%r", + (success, parameters, content) => + { + OAuthCompleteStatus? status; + if (!success) + { + ModMain.MyMsgBox(content, IsWarn: true); + completeCallback?.Invoke(); + return null; + } + + + var code = parameters["code"]; + + try + { + NatayarkProfileManager.GetNaidDataAsync(code, port: ushort.Parse(parameters["Port"])).Wait(); + } + catch (AggregateException ex) + { + resultEx = ex.InnerExceptions[0]; + } + + if (resultEx is null) + status = OAuthCompleteStatus.Complete(NatayarkProfileManager.NaidProfile.Username); + else + status = OAuthCompleteStatus.Failed("获取用户信息失败,请尝试重新登录", resultEx); + completeCallback?.Invoke(); + return status; + }); + } + + #endregion + + #region 旧的 HTTP 服务端实现 + + /* TODO ERROR: Skipped IfDirectiveTrivia + #If False + */ /* TODO ERROR: Skipped DisabledTextTrivia + Private Server As HttpListener + Public Class HttpServer + Public Sub New() + Server = New HttpListener() + Server.Prefixes.Add("http://127.0.0.1:29992/") + Server.Start() + Task.Run( + Async Function() + While True + Try + Dim Context As HttpListenerContext = Await Server.GetContextAsync() + ApiRoute(Context) + Catch ex As Exception + Log(ex, "[Server] 处理响应时发生错误") + End Try + End While + End Function) + End Sub + + Private CurrentStatus As New OAuthCompleteStatus() + + Public Sub ApiRoute(Context As HttpListenerContext) + + + Dim RequestUrl As String = Context.Request.Url.AbsolutePath + Dim OAuthCode As String = Nothing + + ' 多斜杠处理 + While RequestUrl.Contains("//") + RequestUrl = RequestUrl.Replace("//", "/") + End While + + Select Case RequestUrl + Case "/api/naid/oauth20/callback" + + Dim Query = Context.Request.Url.Query + If Query.StartsWith("?") Then Query = Query.Substring(1) + + '在 URL 参数中寻找授权码 + For Each Param As String In Query.Split("&"c) + If Param.StartsWithF("code=") Then + OAuthCode = Param.Substring(5) + End If + Next + + '设置状态信息 + If OAuthCode IsNot Nothing Then + Dim result = NatayarkProfileManager.GetNaidDataSync(OAuthCode) + If result Then + CurrentStatus.success = True + CurrentStatus.username = NatayarkProfileManager.NaidProfile.Username + Else + CurrentStatus.success = False + CurrentStatus.message = $"获取用户信息失败,请尝试重新登录" + CurrentStatus.stacktrace = NatayarkProfileManager.Exception.ToString() + End If + Else + CurrentStatus.success = False + CurrentStatus.message = $"回调参数无效: {Query}" + End If + + '重定向至结束页 + Context.Response.StatusCode = HttpStatusCode.Redirect + Context.Response.AddHeader("location", "/complete") + Context.Response.Close() + Case "/complete" + Try + Dim Data = GetResourceStream("Resources/oauth-complete.html") + If Data Is Nothing Then GoTo NotFound + Context.Response.StatusCode = HttpStatusCode.OK + Context.Response.AddHeader("Content-Type", "text/html, charset=utf-8") + Data.CopyTo(Context.Response.OutputStream) + Context.Response.OutputStream.Dispose() + Context.Response.Close() + Catch ex As Exception + GoTo NotFound + End Try + Case "/assets/background" + SyncLock ChangeLock + If PicAddress Is Nothing OrElse String.IsNullOrWhiteSpace(PicAddress) Then GoTo NotFound + Using FileReadStream As New FileStream(PicAddress, FileMode.Open, FileAccess.Read, FileShare.None, 16384, True) + Context.Response.StatusCode = HttpStatusCode.OK + Context.Response.AddHeader("Content-Type", "application/octet-stream") + FileReadStream.CopyTo(Context.Response.OutputStream) + Context.Response.OutputStream.Dispose() + Context.Response.Close() + End Using + End SyncLock + Case "/assets/icon.ico" + Try + Dim Data = GetResourceStream("Images/icon.ico") + If Data Is Nothing Then GoTo NotFound + Context.Response.StatusCode = HttpStatusCode.OK + Context.Response.AddHeader("Content-Type", "application/octet-stream") + Data.CopyTo(Context.Response.OutputStream) + Context.Response.OutputStream.Dispose() + Context.Response.Close() + Catch ex As Exception + GoTo NotFound + End Try + Case "/api/naid/oauth20/status" + Try + Dim status = JsonConvert.SerializeObject(CurrentStatus) + Dim buffer = Encoding.UTF8.GetBytes(status) + Context.Response.StatusCode = HttpStatusCode.OK + Context.Response.AddHeader("Content-Type", "application/json, charset=utf-8") + Context.Response.OutputStream.Write(buffer, 0, buffer.Length) + Context.Response.OutputStream.Dispose() + Context.Response.Close() + Catch ex As Exception + GoTo NotFound + End Try + Case Else + NotFound: + Context.Response.StatusCode = HttpStatusCode.NotFound + Context.Response.Close() + End Select + End Sub + End Class + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/EnumUpdateChannel.cs b/Plain Craft Launcher 2/Modules/Updates/EnumUpdateChannel.cs new file mode 100644 index 000000000..8fc491471 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/EnumUpdateChannel.cs @@ -0,0 +1,13 @@ +namespace PCL; + +public enum UpdateChannel +{ + stable, + beta +} + +public enum UpdateArch +{ + x64, + arm64 +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/IUpdateSource.cs b/Plain Craft Launcher 2/Modules/Updates/IUpdateSource.cs new file mode 100644 index 000000000..09639eb65 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/IUpdateSource.cs @@ -0,0 +1,25 @@ +using PCL.Core.Utils; + +namespace PCL; + +public interface IUpdateSource +{ + string SourceName { get; set; } + + /// + /// 是否可用,根据本地情况判断 + /// + /// + bool IsAvailable(); + + /// + /// 确保最新版本 + /// + /// True 表示更新成功,False 表示没有数据更新 + bool RefreshCache(); + + VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch); + bool IsLatest(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, int currentVersionCode); + VersionAnnouncementDataModel GetAnnouncementList(); + List GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output); +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.cs b/Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.cs new file mode 100644 index 000000000..891b2c53b --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/UpdatesMinioModel.cs @@ -0,0 +1,257 @@ +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Utils; +using PCL.Core.Utils.Diff; + +namespace PCL; + +public class UpdatesMinioModel : IUpdateSource // 社区自己的更新系统格式 +{ + private readonly string _baseUrl; + + private Dictionary _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 = + JToken.Parse(Conversions.ToString(ModNet.NetGetCodeByRequestRetry($"{_baseUrl}apiv2/cache.json"))); + _remoteCache = remoteCache.ToObject>(); + 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(); + if (deJsonData is null) + throw new NullReferenceException("Can not get remote announcement info!"); + return deJsonData; + } + + public List GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output) + { + if (_remoteCache is null) + RefreshCache(); + var loaders = new List(); + var patchUpdate = true; + var tempPath = $@"{ModBase.PathTemp}Cache\Update\Download\"; + loaders.Add(new ModLoader.LoaderTask>("获取版本信息", load => + { + var channelName = GetChannelName(channel, arch); + var deJsonData = GetRemoteInfoByName($"updates-{channelName}", "updates/") + ?.ToObject() + ?.assets + ?.FirstOrDefault(); + if (deJsonData is null) + throw new Exception("No assets can download!"); + var selfSha256 = ModBase.GetFileSHA256(ModBase.ExePathWithName); + var remoteUpdSha256 = deJsonData.sha256; + var patchFileName = $"{selfSha256}_{remoteUpdSha256}.patch"; + if (deJsonData.patches.Contains(patchFileName)) + { + patchUpdate = true; + tempPath += patchFileName; + load.Output = new List + { new(new[] { $"{_baseUrl}static/patch/{patchFileName}" }, tempPath) }; + } + else + { + patchUpdate = false; + + tempPath += $"{deJsonData.sha256}.bin"; + load.Output = new List { new(RandomUtils.Shuffle(deJsonData.downloads), tempPath) }; + } + })); + loaders.Add(new ModNet.LoaderDownload("下载文件", new List())); + loaders.Add(new ModLoader.LoaderTask("应用文件", _ => + { + if (patchUpdate) + { + var diff = new BsDiff(); + var newFile = diff + .ApplyAsync(ModBase.ReadFileBytes(ModBase.ExePathWithName), 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 == null) + throw new Exception("找不到更新文件"); + + // 解压到指定文件(覆盖已存在文件) + 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().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 JToken GetRemoteInfoByName(string name, string path = "") + { + var localInfoFile = Path.Combine(ModBase.PathTemp, "Cache", "Update", $"{name}.json"); + JToken jsonData; + if (IsCacheValid($"{name}.json", _remoteCache[name])) + { + jsonData = JToken.Parse(ModBase.ReadFile(localInfoFile)); + } + else + { + var response = HttpRequestBuilder.Create($"{_baseUrl}apiv2/{path}{name}.json", HttpMethod.Get).SendAsync() + .GetAwaiter().GetResult(); + jsonData = JToken.Parse(response.AsStringContent()); + ModBase.WriteFile(localInfoFile, response.AsStringContent()); + } + + return jsonData; + } + + /// + /// 缓存是否有效 + /// + /// + /// + /// + 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).Hours < 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 + { + public List assets { get; set; } + } + + private class MinioUpdateAsset + { + public string file_name { get; set; } + public MinioUpdateAssetVersionInfo version { get; set; } + public string upd_time { get; set; } + public List downloads { get; set; } + public List patches { get; set; } + public string sha256 { get; set; } + public string changelog { get; set; } + } + + private class MinioUpdateAssetVersionInfo + { + public string channel { get; set; } + public string name { get; set; } + public int code { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.cs b/Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.cs new file mode 100644 index 000000000..222df45cc --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/UpdatesMirrorChyanModel.cs @@ -0,0 +1,85 @@ +using System.Net.Http; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Utils; + +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(Conversions.ToString(Config.Update.MirrorChyanKey)); + } + + public VersionDataModel GetLatestVersion(UpdateChannel channel, UpdateArch arch) + { + var response = HttpRequestBuilder.Create(GetUrl(channel, arch), HttpMethod.Get).SendAsync().GetAwaiter() + .GetResult(); + var ret = (JObject)ModBase.GetJson(response.AsStringContent()); + 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 GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output) + { + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask>("获取下载信息", load => + { + var ret = (JObject)ModNet.NetGetCodeByRequestRetry(GetUrl(channel, arch), IsJson: true); + var dlUrl = ret["data"]["url"]?.ToString(); + if (dlUrl is null) + throw new Exception("Mirror 酱下载源不可用"); + load.Output = new List { new(new[] { dlUrl }, output) }; + })); + loaders.Add(new ModNet.LoaderDownload("下载更新文件", new List())); + return loaders; + } + + private string GetUrl(UpdateChannel channel, UpdateArch arch) + { + var ReqUrl = MirrorChyanBaseUrl; + var CDKey = Conversions.ToString(Config.Update.MirrorChyanKey); + ReqUrl = ReqUrl.Replace("{cid}", MyCid); + ReqUrl = ReqUrl.Replace("{cdk}", CDKey); + ReqUrl = ReqUrl.Replace("{arch}", arch.ToString()); + ReqUrl = ReqUrl.Replace("{channel}", channel.ToString()); + return ReqUrl; + } +} diff --git a/Plain Craft Launcher 2/Modules/Updates/UpdatesRandomModel.cs b/Plain Craft Launcher 2/Modules/Updates/UpdatesRandomModel.cs new file mode 100644 index 000000000..1ea9258f9 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/UpdatesRandomModel.cs @@ -0,0 +1,53 @@ +using PCL.Core.Utils; + +namespace PCL; + +public class UpdatesRandomModel : IUpdateSource // 社区自己的更新系统格式 +{ + private readonly int _randIndex; + + private readonly IEnumerable _sources; + + public UpdatesRandomModel(IEnumerable 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 GetDownloadLoader(UpdateChannel channel, UpdateArch arch, string output) + { + return _sources.ElementAt(_randIndex).GetDownloadLoader(channel, arch, output); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/UpdatesWrapperModel.cs b/Plain Craft Launcher 2/Modules/Updates/UpdatesWrapperModel.cs new file mode 100644 index 000000000..9513eecfd --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/UpdatesWrapperModel.cs @@ -0,0 +1,166 @@ +using PCL.Core.Utils; + +namespace PCL; + +public class UpdatesWrapperModel : IUpdateSource +{ + private readonly IEnumerable _sources; + private IUpdateSource _announcementSource; + private IUpdateSource _versionSource; + + public UpdatesWrapperModel(IEnumerable 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("获取版本信息失败"); + } + + 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("获取版本信息失败"); + } + + 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("获取公告信息失败"); + } + + public List 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("获取版本信息失败"); + } + + public async Task IsLatestAsync(UpdateChannel channel, UpdateArch arch, SemVer currentVersion, + int currentVersionCode) + { + return await Task.Run(() => IsLatest(channel, arch, currentVersion, currentVersionCode)); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/VersionAnnouncementDataModel.cs b/Plain Craft Launcher 2/Modules/Updates/VersionAnnouncementDataModel.cs new file mode 100644 index 000000000..1fbbfbb51 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/VersionAnnouncementDataModel.cs @@ -0,0 +1,23 @@ +namespace PCL; + +public class VersionAnnouncementDataModel +{ + public List content { get; set; } +} + +public class VersionAnnouncementContentModel +{ + public string title { get; set; } + public string detail { get; set; } + public string id { get; set; } + public string date { get; set; } + public AnnouncementBtnInfoModel btn1 { get; set; } + public AnnouncementBtnInfoModel btn2 { get; set; } +} + +public class AnnouncementBtnInfoModel +{ + public string text { get; set; } + public string command { get; set; } + public string command_paramter { get; set; } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Updates/VersionDataModel.cs b/Plain Craft Launcher 2/Modules/Updates/VersionDataModel.cs new file mode 100644 index 000000000..43a5a89eb --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Updates/VersionDataModel.cs @@ -0,0 +1,10 @@ +namespace PCL; + +public class VersionDataModel +{ + public string Changelog; + public string SHA256; + public string Source; + public int VersionCode; + public string VersionName; +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml index 0132a2f2a..aa7e0146f 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml @@ -1,9 +1,10 @@ - + @@ -20,21 +21,27 @@ - - + - - - + + - + @@ -55,12 +62,12 @@ - + - + @@ -68,21 +75,29 @@ - - - - - + - - + - + - + + Logo="M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z m0 0" /> - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs new file mode 100644 index 000000000..dac1b29de --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.cs @@ -0,0 +1,431 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class MyCompItem +{ + private string StateLast; + + /// + /// 是否允许交互。目前仅用于 PageDownloadCompDetail 的顶部栏展示:若关闭碰撞检测,则无法展开 Tooltip。 + /// + public bool CanInteraction { get; set; } = true; + + public void RefreshColor(object sender, EventArgs e) + { + if (!CanInteraction) + return; + // 判断当前颜色 + string StateNew; + int Time; + if (IsMouseOver) + { + if (IsMouseDown) + { + StateNew = "MouseDown"; + Time = 120; + } + else + { + StateNew = "MouseOver"; + Time = 120; + } + } + else + { + StateNew = "Idle"; + Time = 180; + } + + if ((StateLast ?? "") == (StateNew ?? "")) + return; + StateLast = StateNew; + // 触发颜色动画 + if (IsLoaded && ModAnimation.AniControlEnabled == 0) // 防止默认属性变更触发动画 + { + // 有动画 + var Ani = new List(); + if (IsMouseOver) + { + if (PanButtons is not null && ShowFavoriteBtn) + Ani.Add(ModAnimation.AaOpacity(PanButtons, 1d - PanButtons.Opacity, (int)Math.Round(Time * 0.35d), + (int)Math.Round(Time * 0.15d))); + Ani.AddRange(new[] + { + ModAnimation.AaColor(RectBack, Border.BackgroundProperty, + IsMouseDown ? "ColorBrush6" : "ColorBrushBg1", Time), + ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, Time, + Ease: new ModAnimation.AniEaseOutFluent()) + }); + if (IsMouseDown) + Ani.Add(ModAnimation.AaScaleTransform(RectBack, + 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(Time * 1.2d), + Ease: new ModAnimation.AniEaseOutFluent())); + else + Ani.Add(ModAnimation.AaScaleTransform(RectBack, + 1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(Time * 1.2d), + Ease: new ModAnimation.AniEaseOutFluent())); + } + else + { + if (PanButtons is not null && ShowFavoriteBtn) + Ani.Add(ModAnimation.AaOpacity(PanButtons, -PanButtons.Opacity, (int)Math.Round(Time * 0.4d))); + Ani.AddRange(new[] + { + ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, Time), + ModAnimation.AaColor(RectBack, Border.BackgroundProperty, + IsMouseDown ? "ColorBrush6" : "ColorBrush7", Time), + ModAnimation.AaScaleTransform(RectBack, 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, + Time, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaScaleTransform(RectBack, -0.196d, 1, After: true) + }); + } + + ModAnimation.AniStart(Ani, "CompItem Color " + Uuid); + } + else + { + // 无动画 + ModAnimation.AniStop("CompItem Color " + Uuid); + if (_RectBack is not null) + RectBack.Opacity = 0d; + if (PanButtons is not null) + PanButtons.Opacity = 0d; + } + } + + #region 基础属性 + + public int Uuid = ModBase.GetUuid(); + + // Logo + public string Logo + { + get => PathLogo.Source; + set => PathLogo.Source = value; + } + + // 标题 + public string Title + { + get => LabTitle.Text; + set + { + if ((LabTitle.Text ?? "") == (value ?? "")) + return; + LabTitle.Text = value; + } + } + + // 副标题 + public string SubTitle + { + get => LabTitleRaw?.Text ?? ""; + set + { + if ((LabTitleRaw.Text ?? "") == (value ?? "")) + return; + LabTitleRaw.Text = value; + LabTitleRaw.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible; + } + } + + // 描述 + public string Description + { + get => LabInfo.Text; + set + { + if ((LabInfo.Text ?? "") == (value ?? "")) + return; + LabInfo.Text = value; + } + } + + public MyCompItem() + { + InitializeComponent(); + Click += (sender, e) => MyCompItem_Click((MyCompItem)sender, e); + PreviewMouseLeftButtonUp += Button_MouseUp; + PreviewMouseLeftButtonDown += Button_MouseDown; + MouseLeave += Button_MouseLeave; + PreviewMouseLeftButtonUp += Button_MouseLeave; + MouseEnter += RefreshColor; + MouseLeave += RefreshColor; + MouseLeftButtonDown += RefreshColor; + MouseLeftButtonUp += RefreshColor; + // Handles + LabInfo.MouseEnter += LabInfo_MouseEnter; + BtnDelete.Click += BtnDelete_Click; + } + + // 指向时扩展描述 + private void LabInfo_MouseEnter(object sender, MouseEventArgs e) + { + if (IsTextTrimmed(LabInfo)) + { + ToolTipInfo.Content = LabInfo.Text; + ToolTipInfo.Width = LabInfo.ActualWidth + 25d; + LabInfo.ToolTip = ToolTipInfo; + } + else + { + LabInfo.ToolTip = null; + } + } + + private bool IsTextTrimmed(TextBlock textBlock) + { + var typeface = new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, + textBlock.FontStretch); + var formattedText = new FormattedText(textBlock.Text, Thread.CurrentThread.CurrentCulture, + textBlock.FlowDirection, typeface, textBlock.FontSize, textBlock.Foreground, ModBase.DPI); + return formattedText.Width > textBlock.ActualWidth; + } + + // Tag + public List Tags + { + set + { + PanTags.Children.Clear(); + PanTags.Visibility = value.Any() ? Visibility.Visible : Visibility.Collapsed; + foreach (var tagText in value) + { + var newTag = new Border + { + Background = new SolidColorBrush(Color.FromArgb(17, 0, 0, 0)), + Padding = new Thickness(3d, 1d, 3d, 1d), + CornerRadius = new CornerRadius(3d), + Margin = new Thickness(0d, 0d, 3d, 0d), + SnapsToDevicePixels = true, + UseLayoutRounding = false + }; + var tagTextBlock = new TextBlock + { + Text = tagText, + Foreground = new SolidColorBrush(Color.FromRgb(134, 134, 134)), + FontSize = 11d + }; + newTag.Child = tagTextBlock; + PanTags.Children.Add(newTag); + } + } + } + + // ‘收藏按钮 + public bool ShowFavoriteBtn + { + set => PanButtons.Visibility = value ? Visibility.Visible : Visibility.Collapsed; + get => PanButtons.Visibility == Visibility.Visible; + } + + /// + /// 刷新收藏状态 + /// + public void RefreshFavoriteStatus() + { + if (Tag is ModComp.CompProject) + { + var project = (ModComp.CompProject)Tag; + ShowFavoriteBtn = ModComp.CompFavorites.IsFavourite(project.Id); + } + } + + #endregion + + #region 点击 + + // 触发点击事件 + public event ClickEventHandler? Click; + + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); + + private void BtnDelete_Click(object sender, EventArgs e) + { + if (PanButtons.Opacity > 0d && Tag is ModComp.CompProject) + { + var project = (ModComp.CompProject)Tag; + ModComp.CompFavorites.ShowMenu(project, (UIElement)sender, () => RefreshFavoriteStatus()); + } + } + + private void MyCompItem_Click(MyCompItem sender, EventArgs e) + { + // 记录当前展开的卡片标题(#2712) + var Titles = new List(); + if (ModMain.FrmMain.PageCurrent.Page == FormMain.PageType.CompDetail) + { + foreach (MyCard Card in ModMain.FrmDownloadCompDetail.PanResults.Children) + if (!string.IsNullOrEmpty(Card.Title) && !Card.IsSwapped) + Titles.Add(Card.Title); + ModBase.Log("[Comp] 记录当前已展开的卡片:" + string.Join("、", Titles)); + ((object[])ModMain.FrmMain.PageCurrent.Additional)[1] = Titles; + } + + // 打开详情页 + var TargetType = default(ModComp.CompType); + string TargetVersion = null; + var TargetLoader = ModComp.CompLoaderType.Any; + if (ModMain.FrmMain.PageCurrent.Page == FormMain.PageType.Download) + { + if (ModMain.FrmMain.PageCurrentSub == FormMain.PageSubType.DownloadCompFavorites) + { + TargetVersion = ""; + TargetLoader = ModComp.CompLoaderType.Any; + } + else + { + // 从下载页进入 + switch (ModMain.FrmMain.PageCurrentSub) + { + case FormMain.PageSubType.DownloadMod: + { + TargetType = ModComp.CompType.Mod; + TargetVersion = ModMain.FrmDownloadMod.Content.Loader.Input.GameVersion; + TargetLoader = ModMain.FrmDownloadMod.Content.Loader.Input.ModLoader; + break; + } + case FormMain.PageSubType.DownloadPack: + { + TargetType = ModComp.CompType.ModPack; + TargetVersion = ModMain.FrmDownloadPack.Content.Loader.Input.GameVersion; + break; + } + case FormMain.PageSubType.DownloadDataPack: + { + TargetType = ModComp.CompType.DataPack; + TargetVersion = ModMain.FrmDownloadDataPack.Content.Loader.Input.GameVersion; + break; + } + case FormMain.PageSubType.DownloadResourcePack: + { + TargetType = ModComp.CompType.ResourcePack; + TargetVersion = ModMain.FrmDownloadResourcePack.Content.Loader.Input.GameVersion; + break; + } + case FormMain.PageSubType.DownloadShader: + { + TargetType = ModComp.CompType.Shader; + TargetVersion = ModMain.FrmDownloadShader.Content.Loader.Input.GameVersion; + break; + } + case FormMain.PageSubType.DownloadWorld: + { + TargetType = ModComp.CompType.World; + TargetVersion = ModMain.FrmDownloadWorld.Content.Loader.Input.GameVersion; + break; + } + } + } + } + else if (ModMain.FrmMain.PageCurrent.Page == FormMain.PageType.InstanceSetup) + { + // 从实例设置页进入(查看整合包信息) + TargetType = ModComp.CompType.ModPack; + } + else + { + // 从详情页进入(查看前置) + TargetType = ModComp.CompType.Any; // 允许任意类别 + TargetVersion = Conversions.ToString(((object[])ModMain.FrmMain.PageCurrent.Additional)[2]); + TargetLoader = + (ModComp.CompLoaderType)Conversions.ToInteger(((object[])ModMain.FrmMain.PageCurrent.Additional)[3]); + } + + ModMain.FrmMain.PageChange(new FormMain.PageStackData + { + Page = FormMain.PageType.CompDetail, + Additional = new[] { sender.Tag, new List(), TargetVersion, TargetLoader, TargetType } + }); + } + + // 鼠标点击判定 + private bool IsMouseDown; + + // 触发点击事件 + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDown) + return; + Click?.Invoke(sender, e); + } + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + if (!CanInteraction) + return; + // 检查点击位置是否在按钮区域内 + var clickPosition = e.GetPosition(this); + var isClickOnButton = false; + + if (PanButtons.Visibility == Visibility.Visible) + { + var buttonBounds = new Rect(BtnDelete.TranslatePoint(new Point(0d, 0d), this), BtnDelete.RenderSize); + isClickOnButton = buttonBounds.Contains(clickPosition); + } + + // 如果点击在按钮上,不处理主项目点击事件 + if (isClickOnButton) return; + + // 如果点击在其他区域,按原逻辑处理 + // 也要检查是否点击在LabInfo区域(支持ToolTip点击) + var isClickOnLabInfo = false; + if (LabInfo.Visibility == Visibility.Visible) + { + var labInfoBounds = new Rect(LabInfo.TranslatePoint(new Point(0d, 0d), this), LabInfo.RenderSize); + isClickOnLabInfo = labInfoBounds.Contains(clickPosition); + } + + if (IsMouseDirectlyOver || isClickOnLabInfo) IsMouseDown = true; + } + + private void Button_MouseLeave(object sender, object e) + { + IsMouseDown = false; + } + + #endregion + + #region 后加载指向背景 + + private Border _RectBack; + + public Border RectBack + { + get + { + if (_RectBack is null) + { + var Rect = new Border + { + Name = "RectBack", + CornerRadius = new CornerRadius(3d), + RenderTransform = new ScaleTransform(0.8d, 0.8d), + RenderTransformOrigin = new Point(0.5d, 0.5d), + BorderThickness = new Thickness(ModBase.GetWPFSize(1d)), + SnapsToDevicePixels = true, + IsHitTestVisible = false, + Opacity = 0d + }; + Rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7"); + Rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6"); + SetColumnSpan(Rect, 999); + SetRowSpan(Rect, 999); + Children.Insert(0, Rect); + _RectBack = Rect; + // + } + + return _RectBack; + } + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml index fa7bb12c4..c020eddfc 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml @@ -1,23 +1,23 @@ - + - + - + - + @@ -30,17 +30,22 @@ - + - - + + - + - + @@ -50,8 +55,10 @@ - - + + @@ -67,7 +74,8 @@ - + @@ -75,7 +83,8 @@ - + @@ -83,34 +92,43 @@ - - + + - + + Logo="M496.213333 329.856L315.306667 510.848l180.992 181.077333a42.666667 42.666667 0 1 1-60.330667 60.330667l-211.2-211.2a42.453333 42.453333 0 0 1-11.818667-22.613333l-0.597333-5.034667v-5.034667a42.496 42.496 0 0 1 12.373333-27.648l211.2-211.2a42.666667 42.666667 0 0 1 60.373334 60.330667z m298.666667 0l-180.949333 180.992 180.992 181.077333a42.666667 42.666667 0 1 1-60.330667 60.330667l-211.2-211.2a42.453333 42.453333 0 0 1-11.818667-22.613333l-0.597333-5.034667v-5.034667a42.496 42.496 0 0 1 12.373333-27.648l211.2-211.2a42.666667 42.666667 0 0 1 60.373334 60.330667z" /> - + Logo="M650.752 278.357333l-241.322667 241.365334 241.322667 241.365333a42.666667 42.666667 0 1 1-60.330667 60.330667l-271.530666-271.530667a42.453333 42.453333 0 0 1-11.818667-22.613333l-0.597333-5.034667v-5.034667a42.496 42.496 0 0 1 12.416-27.648l271.530666-271.530666a42.666667 42.666667 0 0 1 60.330667 60.330666z" /> + + Logo="M404.309333 278.357333l241.322667 241.365334-241.322667 241.365333a42.666667 42.666667 0 1 0 60.330667 60.330667l271.530667-271.530667a42.453333 42.453333 0 0 0 11.818666-22.613333l0.597334-5.034667v-5.034667a42.496 42.496 0 0 0-12.416-27.648L464.64 218.026667a42.666667 42.666667 0 0 0-60.330667 60.330666z" /> - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs new file mode 100644 index 000000000..5d6d74307 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.cs @@ -0,0 +1,404 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Markup; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +[ContentProperty("SearchTags")] +public partial class PageComp +{ + /// + /// 每页展示的结果数量。 + /// + public const int PageSize = 40; + + public int Page; + + public ModComp.CompProjectStorage Storage = new(); + + // 结果 UI 化 + private void Load_OnFinish() + { + try + { + ModBase.Log($"[Comp] 开始可视化{TypeNameSpaced}列表,已储藏 {Storage.Results.Count} 个结果,当前在第 {Page + 1} 页"); + // 列表项 + PanProjects.Children.Clear(); + var index = Math.Min(Page * PageSize, Storage.Results.Count - 1); + foreach (var result in Storage.Results.GetRange(index, Math.Min(Storage.Results.Count - index, PageSize))) + PanProjects.Children.Add(result.ToCompItem(Loader.Input.GameVersion is null, + Loader.Input.ModLoader == ModComp.CompLoaderType.Any && + (PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack))); + // 页码 + CardPages.Visibility = + Storage.Results.Count > 40 || Storage.CurseForgeOffset < Storage.CurseForgeTotal || + Storage.ModrinthOffset < Storage.ModrinthTotal + ? Visibility.Visible + : Visibility.Collapsed; + LabPage.Text = (Page + 1).ToString(); + BtnPageFirst.IsEnabled = Page > 1; + BtnPageFirst.Opacity = Page > 1 ? 1d : 0.2d; + BtnPageLeft.IsEnabled = Page > 0; + BtnPageLeft.Opacity = Page > 0 ? 1d : 0.2d; + var IsRightEnabled = Storage.Results.Count > PageSize * (Page + 1) || + Storage.CurseForgeOffset < Storage.CurseForgeTotal || + Storage.ModrinthOffset < + Storage.ModrinthTotal; // 由于 WPF 的未知 bug,读取到的 IsEnabled 可能是错误的值(#3319) + BtnPageRight.IsEnabled = IsRightEnabled; + BtnPageRight.Opacity = IsRightEnabled ? 1d : 0.2d; + // 错误信息 + if (Storage.ErrorMessage is null) + { + HintError.Visibility = Visibility.Collapsed; + } + else + { + HintError.Visibility = Visibility.Visible; + HintError.Text = Storage.ErrorMessage; + } + + // 强制返回顶部 + ScrollToTop(); + } + catch (Exception ex) + { + ModBase.Log(ex, $"可视化{TypeNameSpaced}列表出错", ModBase.LogLevel.Feedback); + } + } + + // 自动重试 + private void Load_State(object sender, MyLoading.MyLoadingState state, MyLoading.MyLoadingState oldState) + { + switch (Loader.State) + { + case ModBase.LoadState.Failed: + { + var ErrorMessage = ""; + if (Loader.Error is not null) + ErrorMessage = Loader.Error.Message; + if (ErrorMessage.Contains("不是有效的 json 文件")) + { + ModBase.Log($"[Download] 下载的{TypeNameSpaced}列表 json 文件损坏,已自动重试", ModBase.LogLevel.Debug); + ((MyPageRight)Parent).PageLoaderRestart(); + } + + break; + } + } + } + + // 切换页码 + private void BtnPageFirst_Click(object sender, EventArgs e) + { + ChangePage(0); + } + + private void BtnPageLeft_Click(object sender, EventArgs e) + { + ChangePage(Page - 1); + } + + private void BtnPageRight_Click(object sender, EventArgs e) + { + ChangePage(Page + 1); + } + + private void ChangePage(int NewPage) + { + CardPages.IsEnabled = false; + Page = NewPage; + ModMain.FrmMain.BackToTop(); + ModBase.Log($"[Download] {TypeName}:切换到第 {Page + 1} 页"); + ModBase.RunInThread(() => + { + Thread.Sleep(100); // 等待向上滚的动画结束 + ModBase.RunInUi(() => CardPages.IsEnabled = true); + Loader.Start(); + }); + } + + // 安装已有整合包按钮 + private void BtnSearchInstallModPack_Click(object sender, EventArgs e) + { + ModModpack.ModpackInstall(); + } + + /// + /// 刷新所有已显示项目的收藏状态 + /// + public void RefreshAllFavoriteStatus() + { + try + { + foreach (var item in PanProjects.Children) + if (item is MyCompItem) + ((MyCompItem)item).RefreshFavoriteStatus(); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新收藏状态时出错"); + } + } + + #region 属性 + + /// + /// 用于 XAML 快速设置的 Tag 下拉框列表。 + /// + public ItemCollection SearchTags => ComboSearchTag.Items; + + public static readonly DependencyProperty SupportCurseForgeProperty = + DependencyProperty.Register("SupportCurseForge", typeof(bool), typeof(PageComp), new PropertyMetadata(true)); + + public bool SupportCurseForge + { + get => Conversions.ToBoolean(GetValue(SupportCurseForgeProperty)); + set => SetValue(SupportCurseForgeProperty, value); + } + + public static readonly DependencyProperty SupportModrinthProperty = + DependencyProperty.Register("SupportModrinth", typeof(bool), typeof(PageComp), new PropertyMetadata(true)); + + public bool SupportModrinth + { + get => Conversions.ToBoolean(GetValue(SupportModrinthProperty)); + set => SetValue(SupportModrinthProperty, value); + } + + /// + /// 英文前后不含空格的可读资源类型名,例如 "Mod"、"整合包"。 + /// + public string TypeName + { + get => _TypeName; + set + { + if ((_TypeName ?? "") == (value ?? "")) + return; + _TypeName = value; + Loader.Name = $"社区资源获取:{value}"; + } + } + + private string _TypeName = ""; + + /// + /// 英文前后含一个空格的可读资源类型名,例如 " Mod "、"整合包"。 + /// + public string TypeNameSpaced + { + get => _TypeNameSpaced; + set + { + if ((_TypeNameSpaced ?? "") == (value ?? "")) + return; + _TypeNameSpaced = value; + PanSearchBox.HintText = $"搜索{value}"; + Load.Text = $"正在获取{value}列表"; + } + } + + private string _TypeNameSpaced = ""; + + /// + /// 该页面对应的资源类型。 + /// + public ModComp.CompType PageType + { + get => _Type; + set + { + if (_Type == value) + return; + _Type = value; + BtnSearchInstallModPack.Visibility = + value == ModComp.CompType.ModPack ? Visibility.Visible : Visibility.Collapsed; + } + } + + private ModComp.CompType _Type = (ModComp.CompType)(-1); + + #endregion + + #region 加载 + + /// + /// 在切换到页面时,应自动将筛选项设置为与该目标 MC 版本和加载器相同。 + /// + public static ModMinecraft.McInstance TargetVersion; + + // 在点击 MyCompItem 时会获取 Loader 的输入,以使资源详情页面可以应用相同的筛选项 + public ModLoader.LoaderTask Loader; + + private bool IsLoaderInited; + + public PageComp() + { + Loader = new ModLoader.LoaderTask("社区资源获取:XXX", ModComp.CompProjectsGet, + LoaderInput) { ReloadTimeout = 60 * 1000 }; + Loaded += PageCompControls_Inited; + IsVisibleChanged += PageComp_IsVisibleChanged; + InitializeComponent(); + Load.StateChanged += Load_State; + BtnPageFirst.Click += BtnPageFirst_Click; + BtnPageLeft.Click += BtnPageLeft_Click; + BtnPageRight.Click += BtnPageRight_Click; + PanSearchBox.Search += (_, _) => StartNewSearch(); + PanSearchBox.KeyDown += EnterTrigger; + TextSearchVersion.KeyDown += EnterTrigger; + BtnSearchReset.Click += (_, _) => ResetFilter(); + BtnSearchInstallModPack.Click += BtnSearchInstallModPack_Click; + } + + private void PageCompControls_Inited(object sender, EventArgs e) + { + // 不知道从 Initialized 改成 Loaded 会不会有问题,但用 Initialized 会导致初始的筛选器修改被覆盖回默认值 + if (TargetVersion is not null) + { + // 设置目标 + ResetFilter(); // 重置筛选器 + TextSearchVersion.Text = TargetVersion.Info.VanillaName; + + MyComboBoxItem GetTargetItemByName(string Name) + { + foreach (MyComboBoxItem Item in ComboSearchLoader.Items) + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(Item.Content, Name, false))) + return Item; + return (MyComboBoxItem)ComboSearchLoader.Items[0]; + } + + ; + if (TargetVersion.Info.HasForge) + ComboSearchLoader.SelectedItem = GetTargetItemByName("Forge"); + else if (TargetVersion.Info.HasFabric) + ComboSearchLoader.SelectedItem = GetTargetItemByName("Fabric"); + else if (TargetVersion.Info.HasNeoForge) + ComboSearchLoader.SelectedItem = GetTargetItemByName("NeoForge"); + else if (TargetVersion.Info.HasQuilt) ComboSearchLoader.SelectedItem = GetTargetItemByName("Quilt"); + TargetVersion = null; + // 如果已经完成请求,则重新开始 + if (IsLoaderInited) + StartNewSearch(); + ScrollToHome(); + } + + // 加载器初始化 + if (IsLoaderInited) + return; + IsLoaderInited = true; + ((MyPageRight)Parent).PageLoaderInit(Load, PanLoad, PanContent, PanAlways, Loader, _ => Load_OnFinish(), + LoaderInput); + // 将最高 Drop 加入筛选 + if (ModDownload.AllDrops is not null && ModDownload.AllDrops.Count != 0 && ModDownload.AllDrops.First() > 250) + { + var HighestVersion = ModMinecraft.McInstanceInfo.DropToVersion(ModDownload.AllDrops.First()); + if ((((MyComboBoxItem)TextSearchVersion.Items[1]).Content.ToString() ?? "") != + (HighestVersion ?? "")) // 0 是全部 + TextSearchVersion.Items.Insert(1, new MyComboBoxItem { Content = HighestVersion }); + } + + // 根据页面类型控制加载器选择的显示 + if (PageType == ModComp.CompType.Shader) + { + LabLoader.Visibility = Visibility.Visible; + ComboSearchLoader.Visibility = Visibility.Collapsed; + ComboSearchShaderLoader.Visibility = Visibility.Visible; + } + else if (PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack) + { + LabLoader.Visibility = Visibility.Visible; + ComboSearchLoader.Visibility = Visibility.Visible; + ComboSearchShaderLoader.Visibility = Visibility.Collapsed; + } + else + { + LabLoader.Visibility = Visibility.Collapsed; + ComboSearchLoader.Visibility = Visibility.Collapsed; + ComboSearchShaderLoader.Visibility = Visibility.Collapsed; + } + } + + private void PageComp_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + // 当页面变为可见时刷新收藏按钮状态 + if (IsVisible) RefreshAllFavoriteStatus(); + } + + private ModComp.CompProjectRequest LoaderInput() + { + var Request = new ModComp.CompProjectRequest(PageType, Storage, (Page + 1) * PageSize); + var GameVersion = TextSearchVersion.Text == "全部 (也可自行输入)" ? null : + TextSearchVersion.Text.Contains(".") || TextSearchVersion.Text.Contains("w") ? TextSearchVersion.Text : + null; + var ModLoader = ModComp.CompLoaderType.Any; + if (PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack) // 只有 Mod 考虑加载器 + { + ModLoader = (ModComp.CompLoaderType)ModBase.Val(((dynamic)ComboSearchLoader.SelectedItem).Tag); + if (GameVersion is not null && GameVersion.Contains(".") && ModBase.Val(GameVersion.Split(".")[1]) < 14d && + ModLoader == ModComp.CompLoaderType.Forge) // 1.14- + // 选择了 Forge + ModLoader = ModComp.CompLoaderType.Any; // 此时,视作没有筛选 Mod Loader(因为部分老 Mod 没有设置自己支持的加载器) + } + + Request.SearchText = PanSearchBox.Text; + Request.GameVersion = GameVersion; + var selectedTag = (ComboSearchTag.SelectedItem as FrameworkElement)?.Tag?.ToString(); + var loaderTag = (ComboSearchShaderLoader.SelectedItem as FrameworkElement)?.Tag?.ToString(); + + Request.Tag = PageType == ModComp.CompType.Shader + ? string.IsNullOrEmpty(loaderTag) + ? selectedTag + : selectedTag + loaderTag + : selectedTag; + Request.ModLoader = + (ModComp.CompLoaderType)(PageType == ModComp.CompType.Mod || PageType == ModComp.CompType.ModPack + ? ModBase.Val(((dynamic)ComboSearchLoader.SelectedItem).Tag) + : (double)ModComp.CompLoaderType.Any); + Request.Source = (ModComp.CompSourceType)ModBase.Val(((dynamic)ComboSearchSource.SelectedItem).Tag); + Request.Sort = (ModComp.CompSortType)ModBase.Val(((dynamic)ComboSearchSort.SelectedItem).Tag); + return Request; + } + + #endregion + + #region 搜索 + + // 搜索按钮 + private void StartNewSearch() + { + Page = 0; + object argInput = LoaderInput(); + if (Loader.ShouldStart(ref argInput)) + Storage = new ModComp.CompProjectStorage(); // 避免连续搜索两次使得 CompProjectStorage 引用丢失(#1311) + Loader.Start(); + } + + private void EnterTrigger(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + StartNewSearch(); + } + + // 重置按钮 + private void ResetFilter() + { + PanSearchBox.Text = ""; + TextSearchVersion.Text = "全部 (也可自行输入)"; + TextSearchVersion.SelectedIndex = 0; + ComboSearchSource.SelectedIndex = 0; + ComboSearchTag.SelectedIndex = 0; + ComboSearchLoader.SelectedIndex = 0; + ComboSearchShaderLoader.SelectedIndex = 0; + ComboSearchSort.SelectedIndex = 0; + Loader.LastFinishedTime = 0L; // 要求强制重新开始 + } + + private void BtnSearchReset_Click(object sender, EventArgs e) + { + ResetFilter(); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml index eca077f6f..d517c88f0 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml @@ -1,8 +1,9 @@  @@ -13,27 +14,49 @@ - - - - - - + + + + + + - - + + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs new file mode 100644 index 000000000..83eed8402 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.cs @@ -0,0 +1,993 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.IO; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.UI; +using Control = System.Windows.Forms.Control; + +namespace PCL; + +public partial class PageDownloadCompDetail +{ + // 资源下载;整合包另存为 + public static Dictionary CachedFolder = new(); // 仅在本次缓存的下载文件夹 + private MyCompItem _compItem; + private bool _isFirstInit = true; + + private void Init() + { + ModAnimation.AniControlEnabled += 1; + _project = (ModComp.CompProject)((object[])ModMain.FrmMain.PageCurrent.Additional)[0]; + PanBack.ScrollToHome(); + // 重启加载器 + if (_isFirstInit) + // 在 Me.Initialized 已经初始化了加载器,不再重复初始化 + _isFirstInit = false; + else + PageLoaderRestart(IsForceRestart: true); + // 放置当前工程 + if (_compItem is not null) + PanIntro.Children.Remove(_compItem); + _compItem = _project.ToCompItem(true, true); + _compItem.CanInteraction = false; + _compItem.ShowFavoriteBtn = false; + _compItem.Margin = new Thickness(-7, -7, 0d, 8d); + PanIntro.Children.Insert(0, _compItem); + + // 决定按钮显示 + BtnIntroWeb.Text = _project.FromCurseForge ? "CurseForge" : "Modrinth"; + BtnIntroWiki.Visibility = _project.WikiId == 0 ? Visibility.Collapsed : Visibility.Visible; + + ModAnimation.AniControlEnabled -= 1; + } + + // 整合包安装 + public void Install_Click(MyListItem sender, EventArgs e) + { + try + { + // 获取基本信息 + var File = (ModComp.CompFile)sender.Tag; + var LoaderName = + $"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} 整合包下载:{_project.TranslatedName} "; + + // 获取实例名 + var PackName = _project.TranslatedName.Replace(".zip", "").Replace(".rar", "").Replace(".mrpack", "") + .Replace(@"\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<") + .Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("\"", "").Replace(": ", ":"); + var Validate = new ValidateFolderName(ModMinecraft.McFolderSelected + "versions"); + if (!string.IsNullOrEmpty(Validate.Validate(PackName))) + PackName = ""; + var InstanceName = ModMain.MyMsgBoxInput("输入实例名称", "", PackName, new Collection { Validate }); + if (string.IsNullOrEmpty(InstanceName)) + return; + + // 构造步骤加载器 + var Loaders = new List(); + var Target = + $@"{ModMinecraft.McFolderSelected}versions\{InstanceName}\原始整合包.{(_project.FromCurseForge ? "zip" : "mrpack")}"; + var LogoFileAddress = MyImage.GetTempPath(_compItem.Logo); + Loaders.Add(new ModNet.LoaderDownload("下载整合包文件", new List { File.ToNetFile(Target) }) + { ProgressWeight = 10d, Block = true }); + Loaders.Add(new ModLoader.LoaderTask("准备安装整合包", + _ => ModModpack.ModpackInstall(Target, InstanceName, + System.IO.File.Exists(LogoFileAddress) ? LogoFileAddress : null, File.ProjectId, + true)) { ProgressWeight = 0.1d }); + + // 启动 + var Loader = new ModLoader.LoaderCombo(LoaderName, Loaders) + { + OnStateChanged = MyLoader => + { + switch (MyLoader.State) + { + case ModBase.LoadState.Failed: + { + ModMain.Hint(MyLoader.Name + "失败:" + MyLoader.Error.Message, ModMain.HintType.Critical); + break; + } + case ModBase.LoadState.Aborted: + { + ModMain.Hint(MyLoader.Name + "已取消!"); + break; + } + case ModBase.LoadState.Loading: + { + return; // 不重新加载版本列表 + } + } + + ModDownloadLib.McInstallFailedClearFolder(MyLoader); + } + }; + Loader.Start(ModMinecraft.McFolderSelected + @"versions\" + InstanceName + @"\"); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "下载资源整合包失败", ModBase.LogLevel.Feedback); + } + } + + // 世界下载 + public void InstallWorld_Click(MyListItem sender, EventArgs e) + { + try + { + // 获取基本信息 + var File = (ModComp.CompFile)sender.Tag; + var LoaderName = $"{(_project.FromCurseForge ? "CurseForge" : "Modrinth")} 世界下载:{_project.TranslatedName} "; + + // 确认默认保存位置 + string DefaultFolder = null; + var SubFolder = @"saves\"; + Func IsVersionSuitable = null; + // 获取资源所需的加载器 + var AllowedLoaders = new List(); + if (File.ModLoaders.Any()) + AllowedLoaders = File.ModLoaders; + else if (_project.ModLoaders.Any()) AllowedLoaders = _project.ModLoaders; + ModBase.Log("[Comp] 世界要求的加载器种类:" + (AllowedLoaders.Any() ? AllowedLoaders.Join(" / ") : "无要求")); + // 判断某个版本是否符合资源要求 + IsVersionSuitable = Version => + { + if (Version is null) + return false; + if (!Version.IsLoaded) + Version.Load(); + if (File.GameVersions.Any(v => v.Contains(".")) && !File.GameVersions.Any(v => + v.Contains(".") && (v ?? "") == (Version.Info.VanillaName ?? ""))) + return false; + // 加载器 + if (!AllowedLoaders.Any()) + return true; // 无要求 + return false; + }; + // 获取常规资源默认下载位置 + if (CachedFolder.ContainsKey(File.Type) && !string.IsNullOrEmpty(CachedFolder[File.Type])) + { + DefaultFolder = CachedFolder.GetOrDefault(File.Type, + ModMinecraft.McInstanceSelected?.PathIndie ?? ModBase.ExePath); + ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}"); + } + else if (ModMinecraft.McInstanceSelected is not null && IsVersionSuitable(ModMinecraft.McInstanceSelected)) + { + DefaultFolder = $"{ModMinecraft.McInstanceSelected.PathIndie}{SubFolder}"; + Directory.CreateDirectory(DefaultFolder); + ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{DefaultFolder}"); + } + else + { + // 查找所有可能的实例 + var NeedLoad = ModMinecraft.McInstanceListLoader.State != ModBase.LoadState.Finished; + if (NeedLoad) + { + ModMain.Hint("正在查找适合的游戏实例……"); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true); + } + + var SuitableVersions = ModMinecraft.McInstanceList.Values.SelectMany(l => l) + .Where(v => IsVersionSuitable(v)).Select(v => new DirectoryInfo($"{v.PathIndie}{SubFolder}")); + if (SuitableVersions.Any()) + { + var SelectedVersion = SuitableVersions + .OrderByDescending(Dir => Dir.Exists ? Dir.LastWriteTimeUtc : DateTime.MinValue) + .ThenByDescending(Dir => Dir.Exists ? Dir.GetFiles().Length : -1).First(); // 先按文件夹更改时间降序 + // 再按文件夹中的文件数量降序 + DefaultFolder = SelectedVersion.FullName; + Directory.CreateDirectory(DefaultFolder); + ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{DefaultFolder}"); + } + else + { + DefaultFolder = ModMinecraft.McFolderSelected; + if (NeedLoad) + ModMain.Hint("当前 MC 文件夹中没有找到适合此资源文件的实例!"); + else + ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置"); + } + } + + var Target = SystemDialogs.SelectSaveFile("选择世界安装位置 (saves 文件夹)", File.FileName, "世界文件|" + "*.zip", + DefaultFolder); + if (string.IsNullOrEmpty(Target)) + return; + + // 构造步骤加载器 + var Loaders = new List(); + var TargetPath = Target.BeforeLast(@"\"); + var LogoFileAddress = MyImage.GetTempPath(_compItem.Logo); + Loaders.Add(new ModNet.LoaderDownload("下载世界文件", new List { File.ToNetFile(Target) }) + { ProgressWeight = 10d, Block = true }); + Loaders.Add( + new ModLoader.LoaderTask("安装世界", _ => ModBase.ExtractFile(Target, TargetPath, Encoding.UTF8)) + { ProgressWeight = 0.1d, Block = true }); + Loaders.Add(new ModLoader.LoaderTask("清理缓存", _ => System.IO.File.Delete(Target))); + + // 启动 + var Loader = new ModLoader.LoaderCombo(LoaderName, Loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + Loader.Start(); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "下载世界资源失败", ModBase.LogLevel.Feedback); + } + } + + public void Save_Click(object sender, EventArgs e) + { + // 获取点击项关联的文件对象 + // 使用模式匹配 (Pattern Matching) 获取目标 Control/Item + object target = sender switch + { + MyListItem item => item, + Control ctrl => ctrl.Parent, + _ => null + }; + + // 安全地访问 Tag 并转换 + var File = (ModComp.CompFile)(target as dynamic)?.Tag; + + ModBase.RunInNewThread(() => + { + try + { + var Desc = ""; + switch (File.Type) + { + case ModComp.CompType.ModPack: Desc = "整合包"; break; + case ModComp.CompType.Mod: Desc = "Mod "; break; + case ModComp.CompType.ResourcePack: Desc = "资源包"; break; + case ModComp.CompType.Shader: Desc = "光影包"; break; + case ModComp.CompType.DataPack: Desc = "数据包"; break; + case ModComp.CompType.World: Desc = "世界"; break; + } + + // 确认默认保存位置 + string DefaultFolder = null; + if (File.Type != ModComp.CompType.ModPack) + { + var SubFolder = ""; + switch (File.Type) + { + case ModComp.CompType.Mod: SubFolder = "mods\\"; break; + case ModComp.CompType.ResourcePack: SubFolder = "resourcepacks\\"; break; + case ModComp.CompType.Shader: SubFolder = "shaderpacks\\"; break; + case ModComp.CompType.World: SubFolder = "saves\\"; break; + case ModComp.CompType.DataPack: SubFolder = ""; break; // 导航到版本根目录 + } + + // 获取资源所需的加载器 + var AllowedLoaders = new List(); + if (File.ModLoaders.Any()) + AllowedLoaders = File.ModLoaders; + else if (_project.ModLoaders.Any()) AllowedLoaders = _project.ModLoaders; + ModBase.Log( + $"[Comp] {Desc}要求的加载器种类:{(AllowedLoaders.Any() ? string.Join(" / ", AllowedLoaders) : "无要求")}"); + + // 判断某个版本是否符合资源要求 (局部函数) + Func IsVersionSuitable = Version => + { + if (Version == null) return false; + if (!Version.IsLoaded) Version.Load(); + + // 只对 Mod 和数据包进行版本检测 + if (File.Type == ModComp.CompType.Mod || File.Type == ModComp.CompType.DataPack) + if (File.GameVersions.Any(v => v.Contains(".")) && + !File.GameVersions.Any(v => v.Contains(".") && v == Version.Info.VanillaName)) + return false; + + // 加载器判定 + if (!AllowedLoaders.Any()) return true; // 无要求 + if (AllowedLoaders.Contains(ModComp.CompLoaderType.Forge) && Version.Info.HasForge) return true; + if (AllowedLoaders.Contains(ModComp.CompLoaderType.Fabric) && + (Version.Info.HasFabric || Version.Info.HasLegacyFabric)) return true; + if (AllowedLoaders.Contains(ModComp.CompLoaderType.NeoForge) && Version.Info.HasNeoForge) + return true; + if (AllowedLoaders.Contains(ModComp.CompLoaderType.LiteLoader) && Version.Info.HasLiteLoader) + return true; + return false; + }; + + // 获取常规资源默认下载位置逻辑 + if (CachedFolder.ContainsKey(File.Type) && !string.IsNullOrEmpty(CachedFolder[File.Type])) + { + DefaultFolder = CachedFolder.GetOrDefault(File.Type, + ModMinecraft.McInstanceSelected?.PathIndie ?? ModBase.ExePath); + ModBase.Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}"); + } + else if (ModMinecraft.McInstanceSelected != null && + IsVersionSuitable(ModMinecraft.McInstanceSelected)) + { + DefaultFolder = $"{ModMinecraft.McInstanceSelected.PathIndie}{SubFolder}"; + Directory.CreateDirectory(DefaultFolder); + ModBase.Log($"[Comp] 使用当前实例作为默认下载位置:{DefaultFolder}"); + } + else + { + // 查找所有可能的实例 + var NeedLoad = ModMinecraft.McInstanceListLoader.State != ModBase.LoadState.Finished; + if (NeedLoad) + { + ModMain.Hint("正在查找适合的游戏实例……"); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, "versions\\", true); + } + + var SuitableVersions = ModMinecraft.McInstanceList.Values.SelectMany(l => l) + .Where(v => IsVersionSuitable(v)) + .Select(v => new DirectoryInfo($"{v.PathIndie}{SubFolder}")); + + if (SuitableVersions.Any()) + { + var SelectedVersion = SuitableVersions + .OrderByDescending(Dir => Dir.Exists ? Dir.LastWriteTimeUtc : DateTime.MinValue) + .ThenByDescending(Dir => Dir.Exists ? Dir.GetFiles().Length : -1) + .First(); + DefaultFolder = SelectedVersion.FullName; + Directory.CreateDirectory(DefaultFolder); + ModBase.Log($"[Comp] 使用适合的游戏实例作为默认下载位置:{DefaultFolder}"); + } + else + { + DefaultFolder = ModMinecraft.McFolderSelected; + if (NeedLoad) + ModMain.Hint("当前 MC 文件夹中没有找到适合此资源文件的实例!"); + else + ModBase.Log("[Comp] 由于当前实例不兼容,使用当前的 MC 文件夹作为默认下载位置"); + } + } + } + + // 获取文件名并弹窗 + var FileName = ModComp.CompFileNameGet(_project, File); + ModBase.RunInUi(() => + { + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, + $"{Desc}文件|" + (File.Type == ModComp.CompType.Mod + ? + File.FileName.EndsWith(".litemod") ? "*.litemod" : "*.jar" + : + File.FileName.EndsWith(".mrpack") + ? "*.mrpack" + : "*.zip"), + DefaultFolder); + + if (!Target.Contains("\\")) return; + + // 记录缓存路径 + var targetDir = ModBase.GetPathFromFullPath(Target); + if (Target != DefaultFolder) + { + if (CachedFolder.ContainsKey(File.Type)) + CachedFolder[File.Type] = targetDir; + else + CachedFolder.Add(File.Type, targetDir); + } + + // 构造下载任务 + var LoaderName = $"{Desc}下载:{ModBase.GetFileNameWithoutExtentionFromPath(Target)} "; + var Loaders = new List + { + new ModNet.LoaderDownload("下载文件", new List { File.ToNetFile(Target) }) + { + ProgressWeight = 6, + Block = true + } + }; + + // 启动加载器 + var Loader = new ModLoader.LoaderCombo(LoaderName, Loaders); + Loader.OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly; + Loader.Start(1); + ModLoader.LoaderTaskbarAdd(Loader); + + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "保存资源文件失败", ModBase.LogLevel.Feedback); + } + }, "Download CompDetail Save"); + } + + private void BtnIntroWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite(_project.Website); + } + + private void BtnIntroWiki_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://www.mcmod.cn/class/" + _project.WikiId + ".html"); + } + + private void BtnIntroCopy_Click(object sender, EventArgs e) + { + ModBase.ClipboardSet(_compItem.LabTitle.Text + _compItem.LabTitleRaw.Text); + } + + private void BtnFavorites_Click(object sender, EventArgs e) + { + ModComp.CompFavorites.ShowMenu(_project, (UIElement)sender); + } + + private void BtnIntroLinkCopy_Click(object sender, EventArgs e) + { + ModComp.CompClipboard.CurrentText = _project.Website; + ModBase.ClipboardSet(_project.Website); + } + + // 翻译简介 + private async void BtnTranslate_Click(object sender, EventArgs e) + { + ModMain.Hint($"正在获取 {_project.TranslatedName} 的简介译文……"); + var ChineseDescription = await _project.ChineseDescription; + if (ChineseDescription is null) + return; + ModMain.MyMsgBox($"原文:{_project.Description}{"\r\n"}译文:{ChineseDescription}"); + } + + /// + /// 刷新收藏按钮的显示状态 + /// + public void RefreshFavoriteButton() + { + try + { + if (_project is not null) + // 刷新顶部的项目卡片收藏状态 + if (_compItem is not null) + _compItem.RefreshFavoriteStatus(); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新收藏按钮状态时出错"); + } + } + + #region 加载器 + + private readonly ModLoader.LoaderTask> _compFileLoader; + + public PageDownloadCompDetail() + { + _compFileLoader = new ModLoader.LoaderTask>("Comp File", task => + { + LoadTargetFromAdditional(); + var result = ModComp.CompFilesGet(_project.Id, _project.FromCurseForge); + if (task.IsAborted) + return; + task.Output = result; + }); + Initialized += PageDownloadCompDetail_Inited; + Loaded += (_, _) => LoadTargetFromAdditional(); + PageEnter += Init; + InitializeComponent(); + Load.StateChanged += Load_State; + BtnIntroWeb.Click += BtnIntroWeb_Click; + BtnIntroWiki.Click += BtnIntroWiki_Click; + BtnIntroCopy.Click += BtnIntroCopy_Click; + BtnFavorites.Click += BtnFavorites_Click; + BtnIntroLinkCopy.Click += BtnIntroLinkCopy_Click; + BtnTranslate.Click += BtnTranslate_Click; + } + + // 初始化加载器信息 + private void PageDownloadCompDetail_Inited(object sender, EventArgs e) + { + LoadTargetFromAdditional(); + PageLoaderInit(Load, PanLoad, PanMain, CardIntro, _compFileLoader, _ => Load_OnFinish()); + } + + public void LoadTargetFromAdditional() + { + var array = (object[])ModMain.FrmMain.PageCurrent.Additional; + _project = (ModComp.CompProject)array[0]; + _targetInstance = Conversions.ToString(array[2]); + _targetLoader = (ModComp.CompLoaderType)Conversions.ToInteger(array[3]); + _pageType = (ModComp.CompType)Conversions.ToInteger(array[4]); + } + + private ModComp.CompProject _project; + private string _targetInstance; + private ModComp.CompLoaderType _targetLoader; + + /// + /// 当前页面应展示的内容类别。可能为 Any。 + /// + private ModComp.CompType _pageType; + + // 自动重试 + private void Load_State(object sender, MyLoading.MyLoadingState state, MyLoading.MyLoadingState oldState) + { + switch (_compFileLoader.State) + { + case ModBase.LoadState.Failed: + { + var errorMessage = ""; + if (_compFileLoader.Error is not null) + errorMessage = _compFileLoader.Error.Message; + if (errorMessage.Contains("不是有效的 Json 文件")) + { + ModBase.Log("[Comp] 下载的文件 Json 列表损坏,已自动重试", ModBase.LogLevel.Debug); + PageLoaderRestart(); + } + + break; + } + } + } + + // 结果 UI 化 + private class CardSorter : IComparer + { + public readonly string Topmost = ""; + + public CardSorter(string topmost = "") + { + Topmost = topmost ?? ""; + } + + public int Compare(string x, string y) + { + // 相同 + if ((x ?? "") == (y ?? "")) + return 0; + // 置顶 + if ((x ?? "") == (Topmost ?? "")) + return -1; + if ((y ?? "") == (Topmost ?? "")) + return 1; + // 特殊版本 + var isXSpecial = !x.Contains("."); + var isYSpecial = !y.Contains("."); + if (isXSpecial && isYSpecial) + return string.Compare(x, y, StringComparison.Ordinal); + if (isXSpecial) + return 1; + if (isYSpecial) + return -1; + // 比较版本号 + var versionCodeSort = -ModMinecraft.CompareVersion(x.Replace(x.BeforeFirst(" ") + " ", ""), + y.Replace(y.BeforeFirst(" ") + " ", "")); + if (versionCodeSort != 0) + return versionCodeSort; + // 比较全部 + return -ModMinecraft.CompareVersion(x, y); + } + } + + private string? _instanceFilter; + private string? _modLoaderFilter; + private bool GroupedDrop; // 是否按 Drop 筛选(1.21 / 1.20 / 1.19 / ...)而非小版本号(1.21.1 / 1.21 / 1.20.4 / ...) + + private bool GroupedOld; // 是否折叠远古版本为一个选项 + + // 筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表) + private List GetResults() + { + var results = _compFileLoader.Output; + if (_pageType == ModComp.CompType.Any) + { + results = results.Where(r => r.Type != ModComp.CompType.Plugin).ToList(); + } + else if (_pageType == ModComp.CompType.Shader || _pageType == ModComp.CompType.ResourcePack) + { + } + // 不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(Meloong-Git/#6473) + else + { + results = results.Where(r => r.Type == _pageType).ToList(); + } + + return results; + } + + private void Load_OnFinish() + { + var results = GetResults(); + + // 初始化筛选器 + List instanceFilters = null; + List modLoaderFilters = null; + + void updateFilters() + { + instanceFilters = results.SelectMany(v => v.GameVersions) + .Select(v => GetGroupedVersionName(v, GroupedDrop, GroupedOld)).Distinct() + .OrderByDescending(s => s, new ModMinecraft.VersionComparer()).ToList(); + modLoaderFilters = results.SelectMany(v => v.ModLoaders).Select(l => l.ToString()).Distinct() + .OrderByDescending(s => s).ToList(); + } + + ; + + // 确定分组方式 + GroupedDrop = false; + GroupedOld = false; + updateFilters(); + if (instanceFilters.Count < 9) + goto GroupDone; + GroupedDrop = true; + GroupedOld = false; + updateFilters(); + if (instanceFilters.Count < 9) + goto GroupDone; + GroupedDrop = false; + GroupedOld = true; + updateFilters(); + if (instanceFilters.Count < 9) + goto GroupDone; + GroupedDrop = true; + GroupedOld = true; + updateFilters(); + GroupDone: ; + + + // UI 化筛选器 + PanInstanceFilter.Children.Clear(); + PanModLoaderFilter.Children.Clear(); + if (!(_pageType == ModComp.CompType.Mod)) + { + PanInstanceFilter.Margin = new Thickness(10d, 10d, 0d, 10d); + PanModLoaderFilter.Margin = new Thickness(0d); + } + + if (instanceFilters.Count < 2) + { + CardFilter.Visibility = Visibility.Collapsed; + _instanceFilter = null; + } + else + { + CardFilter.Visibility = Visibility.Visible; + // 插入标签 + if (_pageType == ModComp.CompType.Mod) + { + var instanceTextBlock = new TextBlock + { + Text = "实例筛选:", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(2d, 0d, 0d, 0d) + }; + PanInstanceFilter.Children.Add(instanceTextBlock); + var modLoaderTextBlock = new TextBlock + { + Text = "模组加载器筛选:", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(2d, 0d, 0d, 0d) + }; + PanModLoaderFilter.Children.Add(modLoaderTextBlock); + } + + instanceFilters.Insert(0, "全部"); + modLoaderFilters.Insert(0, "全部"); + // 转化为按钮 + foreach (var version in instanceFilters) + { + var newButton = new MyRadioButton + { + Text = version, Margin = new Thickness(2d, 0d, 2d, 0d), + ColorType = MyRadioButton.ColorState.Highlight + }; + newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d); + newButton.Check += (sender, raiseByMouse) => + { + _instanceFilter = sender.Text == "全部" ? null : sender.Text; + UpdateFilterResult(); + }; + PanInstanceFilter.Children.Add(newButton); + } + + if (_pageType == ModComp.CompType.Mod) + foreach (var loader in modLoaderFilters) + { + var newButton = new MyRadioButton + { + Text = loader, + Margin = new Thickness(2d, 0d, 2d, 0d), + ColorType = MyRadioButton.ColorState.Highlight + }; + newButton.LabText.Margin = new Thickness(-2, 0d, 10d, 0d); + newButton.Check += (sender, raiseByMouse) => + { + _modLoaderFilter = sender.Text == "全部" ? null : sender.Text; + UpdateFilterResult(); + }; + PanModLoaderFilter.Children.Add(newButton); + } + + // 自动选择 + MyRadioButton instanceToCheck = null; + MyRadioButton modLoaderToCheck = null; + if (!string.IsNullOrEmpty(_targetInstance)) + { + var targetFile = results.FirstOrDefault(v => v.GameVersions.Contains(_targetInstance)); + if (targetFile is not null) + { + var targetGroup = GetGroupedVersionName(_targetInstance, GroupedDrop, GroupedOld); + var children = _pageType == ModComp.CompType.Mod + ? PanInstanceFilter.Children.Cast().Skip(1) + : PanInstanceFilter.Children.Cast(); + foreach (MyRadioButton button in (IEnumerable)children) + { + if ((button.Text ?? "") != (targetGroup ?? "")) + continue; + instanceToCheck = button; + break; + } + } + } + + if (_pageType == ModComp.CompType.Mod) + if (_targetLoader != ModComp.CompLoaderType.Any) + { + var targetFile = results.FirstOrDefault(v => v.ModLoaders.Contains(_targetLoader)); + if (targetFile is not null) + { + var children = _pageType == ModComp.CompType.Mod + ? PanInstanceFilter.Children.Cast().Skip(1) + : PanInstanceFilter.Children.Cast(); + foreach (MyRadioButton button in (IEnumerable)children) + { + if ((button.Text ?? "") != (_targetLoader.ToString() ?? "")) + continue; + modLoaderToCheck = button; + break; + } + } + } + + // 注意:在 Mod 下 index 0 是 TextBlock + var index = _pageType == ModComp.CompType.Mod ? 1 : 0; + if (instanceToCheck is null) + instanceToCheck = (MyRadioButton)PanInstanceFilter.Children[index]; + if (modLoaderToCheck is null & (_pageType == ModComp.CompType.Mod)) + modLoaderToCheck = (MyRadioButton)PanModLoaderFilter.Children[index]; + instanceToCheck.Checked = true; + if (_pageType == ModComp.CompType.Mod) + modLoaderToCheck.Checked = true; + } + + // 更新筛选结果(文件列表 UI 化) + UpdateFilterResult(); + } + + private void UpdateFilterResult() + { + var results = GetResults(); + if (results is null) + return; + + // 1. 预处理基础变量 + var targetVersionText = _targetLoader != ModComp.CompLoaderType.Any ? _targetLoader + " " : ""; + var targetCardName = !string.IsNullOrEmpty(_targetInstance) || _targetLoader != ModComp.CompLoaderType.Any + ? $"所选版本:{targetVersionText}{_targetInstance}" + : ""; + + // 使用 HashSet 提高查询性能 O(1) + var supportedLoaders = + new HashSet(Enum.GetValues(typeof(ModComp.CompLoaderType)) + .Cast()); + var ignoreQuilt = Conversions.ToBoolean(Config.Download.Comp.IgnoreQuilt); + var hasMultipleLoaders = _project.ModLoaders.Count > 1; + + // 2. 核心数据归类 (使用 Dictionary 配合 HashSet 去重) + var dict = new SortedDictionary>(new CardSorter(targetCardName)); + dict.Add("其他", new List()); + + // 用于记录每个卡片内已存在的 version,防止 Contains(version) 的 O(n) 消耗 + var versionDuplicateChecker = new Dictionary>(); + + foreach (var version in results) + { + // 处理普通卡片归类 + foreach (var gameVersion in version.GameVersions) + { + // 筛选器预检查 + var currentGroupedName = GetGroupedVersionName(gameVersion, GroupedDrop, GroupedOld); + if (_instanceFilter is not null && (currentGroupedName ?? "") != (_instanceFilter ?? "")) + continue; + var verName = GetGroupedVersionName(gameVersion, false, false); + var loaders = new List(); + + // 判定 Loader 逻辑 + if (hasMultipleLoaders && version.Type == ModComp.CompType.Mod && + ModMinecraft.McInstanceInfo.IsFormatFit(verName)) + { + foreach (var loader in version.ModLoaders) + { + if (loader == ModComp.CompLoaderType.Quilt && ignoreQuilt) + continue; + if (!supportedLoaders.Contains(loader)) + continue; + + // 模组加载器筛选器 + if (_modLoaderFilter is not null && (loader.ToString() ?? "") != (_modLoaderFilter ?? "")) + continue; + + loaders.Add(loader + " "); + } + + if (loaders.Count == 0 && _modLoaderFilter is not null) continue; + } + + if (loaders.Count == 0) + loaders.Add(""); + + // 填充数据 + foreach (var loaderPrefix in loaders) + { + var targetKey = loaderPrefix + verName; + AddVersionToDict(dict, versionDuplicateChecker, targetKey, version); + } + } + + // 处理“所选版本”卡片 (逻辑合并,减少二次循环) + if (!string.IsNullOrEmpty(targetCardName)) + { + var isMatchFilter = _instanceFilter is null || + GetGroupedVersionName(_targetInstance, GroupedDrop, GroupedOld) + .StartsWithF(_instanceFilter); + + if (isMatchFilter && version.GameVersions.Contains(_targetInstance)) + if (_targetLoader == ModComp.CompLoaderType.Any || version.ModLoaders.Contains(_targetLoader)) + // 再次检查 version 是否符合筛选器(针对该文件的所有游戏版本) + if (_instanceFilter is null || version.GameVersions.Any(v => + (GetGroupedVersionName(v, GroupedDrop, GroupedOld) ?? "") == (_instanceFilter ?? ""))) + AddVersionToDict(dict, versionDuplicateChecker, targetCardName, version); + } + } + + // 3. 渲染 UI + try + { + PanResults.Children.Clear(); + var array = (object[])ModMain.FrmMain.PageCurrent.Additional; + var additionalTitles = array is not null + ? (List)array[1] + : new List(); + + foreach (var pair in dict) + { + if (pair.Value.Count == 0) + continue; + + // 创建卡片组件 + var newCard = new MyCard + { + Title = pair.Key, + Margin = new Thickness(0d, 0d, 0d, 15d) + }; + + // 闭包引用:避免在 Sub 内做高耗时操作 + var files = pair.Value; + var currentKey = pair.Key; + + var newStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, + Tag = files + }; + + newCard.Children.Add(newStack); + newCard.SwapControl = newStack; + + // 延迟加载安装项的逻辑 + newCard.InstallMethod = stack => + { + var list = (List)stack.Tag; + // 排序和去重检查 + list.Sort((a, b) => b.ReleaseDate.CompareTo(a.ReleaseDate)); + var distinctCount = list.Select(f => f.DisplayName).Distinct().Count(); + var badDisplayName = distinctCount != list.Count; + + // 批量添加子项 + switch (_project.Type) + { + case ModComp.CompType.ModPack: + { + foreach (var item in list) + stack.Children.Add(item.ToListItem( + (sender, e) => ModMain.FrmDownloadCompDetail.Install_Click((MyListItem)sender, e), + ModMain.FrmDownloadCompDetail.Save_Click, badDisplayName)); + break; + } + case ModComp.CompType.World: + { + foreach (var item in list) + stack.Children.Add(item.ToListItem( + (sender, e) => + ModMain.FrmDownloadCompDetail.InstallWorld_Click((MyListItem)sender, e), + ModMain.FrmDownloadCompDetail.Save_Click, badDisplayName)); + break; + } + + default: + { + ModComp.CompFilesCardPreload(stack, list); + foreach (var item in list) + stack.Children.Add(item.ToListItem(ModMain.FrmDownloadCompDetail.Save_Click, + badDisplayName: badDisplayName)); + break; + } + } + }; + + PanResults.Children.Add(newCard); + + // 展开逻辑 + if ((currentKey ?? "") == (targetCardName ?? "") || additionalTitles.Contains(newCard.Title)) + newCard.StackInstall(); + else + newCard.IsSwapped = true; + + // 特殊提示 + if (currentKey == "其他") + newStack.Children.Add(new MyHint + { + Text = "由于版本信息更新缓慢,可能无法识别刚更新的 MC 版本。几天后即可正常识别。", + Theme = MyHint.Themes.Yellow, + Margin = new Thickness(5d, 0d, 0d, 8d) + }); + } + + // 单卡片自动展开 + if (PanResults.Children.Count == 1) ((MyCard)PanResults.Children[0]).IsSwapped = false; + } + + catch (Exception ex) + { + ModBase.Log(ex, "可视化工程下载列表出错", ModBase.LogLevel.Feedback); + } + } + + /// + /// 辅助方法:向字典添加数据并处理去重 + /// + private void AddVersionToDict(SortedDictionary> dict, + Dictionary> checker, string key, ModComp.CompFile version) + { + if (!dict.ContainsKey(key)) + { + dict.Add(key, new List()); + checker.Add(key, new HashSet()); + } + + // 使用 HashSet.Add 判断是否重复,比 List.Contains 快得多 + if (checker[key].Add(version)) dict[key].Add(version); + } + + private string GetGroupedVersionName(string name, bool groupedByDrop, bool foldOld) + { + if (name is null) return "其他"; + + if (name.Contains("w")) return "快照版"; + + if (!ModMinecraft.McInstanceInfo.IsFormatFit(name) || + (foldOld && ModMinecraft.McInstanceInfo.VersionToDrop(name, true) < 120)) return "远古版"; + + if (groupedByDrop) + return ModMinecraft.McInstanceInfo.DropToVersion(ModMinecraft.McInstanceInfo.VersionToDrop(name, true)); + + return name; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml index f7c01e924..8b7ca95ea 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml @@ -1,8 +1,9 @@  @@ -23,4 +24,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml.cs new file mode 100644 index 000000000..2a44b805b --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml.cs @@ -0,0 +1,9 @@ +namespace PCL; + +public partial class PageDownloadDataPack +{ + public PageDownloadDataPack() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml index 7792c84e0..d8a1390b3 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml @@ -1,8 +1,9 @@  diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml.cs new file mode 100644 index 000000000..da0b09f22 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml.cs @@ -0,0 +1,10 @@ +namespace PCL; + +public partial class PageDownloadMod +{ + public PageDownloadMod() + { + InitializeComponent(); + } + +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml index 51f04d644..8bcc72737 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml @@ -1,8 +1,9 @@  @@ -25,4 +26,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml.cs new file mode 100644 index 000000000..8fc25f701 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml.cs @@ -0,0 +1,9 @@ +namespace PCL; + +public partial class PageDownloadPack +{ + public PageDownloadPack() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml index 91932d587..12b7f2bdc 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml @@ -1,8 +1,9 @@  @@ -42,4 +43,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml.cs new file mode 100644 index 000000000..0b6c60808 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml.cs @@ -0,0 +1,9 @@ +namespace PCL; + +public partial class PageDownloadResourcePack +{ + public PageDownloadResourcePack() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml index 0e88cae7c..e8fbff879 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml @@ -1,12 +1,13 @@ - + @@ -28,4 +29,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml.cs new file mode 100644 index 000000000..54f6775be --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml.cs @@ -0,0 +1,9 @@ +namespace PCL; + +public partial class PageDownloadShader +{ + public PageDownloadShader() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml index ce53ba55b..8b8820bdc 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml @@ -1,8 +1,9 @@  @@ -15,4 +16,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml.cs new file mode 100644 index 000000000..b86b9062e --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadWorld.xaml.cs @@ -0,0 +1,9 @@ +namespace PCL; + +public partial class PageDownloadWorld +{ + public PageDownloadWorld() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs new file mode 100644 index 000000000..4b01a7d9b --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -0,0 +1,4487 @@ +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Minecraft; +using PCL.Core.UI; +using PCL.Core.Utils; + +namespace PCL; + +public static class ModDownloadLib +{ + /// + /// 如果 OptiFine 与 Forge 同时开始安装,就会导致 Forge 安装失败。 + /// + private static readonly object InstallSyncLock = new(); + + /// + /// 如果 OptiFine 与 Forge 同时复制原版 Jar,就会导致复制文件时冲突。 + /// + private static readonly object VanillaSyncLock = new(); + + #region Minecraft 下载 + + /// + /// 下载某个 Minecraft 实例,这会创造一个单独的下载任务,失败会跳过执行并要求反馈。 + /// 返回正在下载的任务,若跳过或失败,则返回 Nothing。 + /// + /// 所下载的 Minecraft 的版本名。 + /// Json 文件的 Mojang 官方地址。 + public static ModLoader.LoaderCombo McDownloadClient(ModNet.NetPreDownloadBehaviour behaviour, string id, + string jsonUrl = null) + { + try + { + var versionFolder = ModMinecraft.McFolderSelected + @"versions\" + id + @"\"; + + // 重复任务检查 + foreach (var ongoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if (ongoingLoader.Name != $"Minecraft {id} 下载") + continue; + if (behaviour == ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) + return (ModLoader.LoaderCombo)ongoingLoader; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return (ModLoader.LoaderCombo)ongoingLoader; + } + + // 已有实例检查 + if (behaviour != ModNet.NetPreDownloadBehaviour.IgnoreCheck && File.Exists(versionFolder + id + ".json") && + File.Exists(versionFolder + id + ".jar")) + { + if (behaviour == ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) + return null; + if (ModMain.MyMsgBox( + "实例 " + id + " 已存在,是否重新下载?" + "\r\n" + "这会覆盖实例的 Json 与 Jar 文件,但不会影响版本隔离的文件。", "实例已存在", + "继续", "取消") == 1) + { + File.Delete(versionFolder + id + ".jar"); + File.Delete(versionFolder + id + ".json"); + } + else + { + return null; + } + } + + // 启动 + var Loader = + new ModLoader.LoaderCombo("Minecraft " + id + " 下载", McDownloadClientLoader(id, jsonUrl)) + { OnStateChanged = McInstallState }; + Loader.Start(versionFolder); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + return Loader; + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback); + return null; + } + } + + /// + /// 保存某个 Minecraft 实例的核心文件(仅 Json 与核心 Jar)。 + /// + /// 所下载的 Minecraft 的版本名。 + /// Json 文件的 Mojang 官方地址。 + public static void McDownloadClientCore(string Id, string JsonUrl, ModNet.NetPreDownloadBehaviour Behaviour) + { + try + { + var VersionFolder = SystemDialogs.SelectFolder(); + if (!VersionFolder.Contains(@"\")) + return; + VersionFolder = VersionFolder + Id + @"\"; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar) + { + if ((OngoingLoader.Name ?? "") != ($"Minecraft {Id} 下载" ?? "")) + continue; + if (Behaviour == ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading) + return; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + var Loaders = new List(); + // 下载实例 Json 文件 + Loaders.Add(new ModNet.LoaderDownload("下载实例 Json 文件", + new List + { + new(ModDownload.DlSourceLauncherOrMetaGet(JsonUrl), VersionFolder + Id + ".json", + new ModBase.FileChecker(CanUseExistsFile: false, IsJson: true)) + }) { ProgressWeight = 2d }); + // 获取支持库文件地址 + Loaders.Add(new ModLoader.LoaderTask>("分析核心 Jar 文件下载地址", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 0.5d, Show = false }); + // 下载支持库文件 + Loaders.Add(new ModNet.LoaderDownload("下载核心 Jar 文件", new List()) { ProgressWeight = 5d }); + + // 启动 + var Loader = new ModLoader.LoaderCombo("Minecraft " + Id + " 下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(Id); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 Minecraft 版本的加载器列表。 + /// 它必须安装到 McFolderSelected,但是可以自定义版本名(不过自定义的实例名不会修改 Json 中的 id 项)。 + /// + private static List McDownloadClientLoader(string id, string jsonUrl = null, + string instanceName = null) + { + instanceName = instanceName ?? id; + var instanceFolder = ModMinecraft.McFolderSelected + @"versions\" + instanceName + @"\"; + + var loaders = new List(); + + // 下载实例 Json 文件 + if (jsonUrl is null) + loaders.Add(new ModLoader.LoaderTask>("获取原版 Json 文件下载地址", task => + { + var jsonAddress = Conversions.ToString(ModDownload.DlClientListGet(id)); + task.Output = new List + { + new(ModDownload.DlSourceLauncherOrMetaGet(jsonAddress), instanceFolder + instanceName + ".json") + }; + }) + { + ProgressWeight = 2d, + Show = false + }); + loaders.Add(new ModNet.LoaderDownload(McDownloadClientJsonName, + new List + { + new(ModDownload.DlSourceLauncherOrMetaGet(jsonUrl ?? ""), instanceFolder + instanceName + ".json", + new ModBase.FileChecker(CanUseExistsFile: false, IsJson: true)) + }) { ProgressWeight = 3d }); + + // 下载支持库文件 + var loadersLib = new List(); + loadersLib.Add(new ModLoader.LoaderTask>("分析原版支持库文件(副加载器)", task => + { + Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘(#3710) + ModBase.Log("[Download] 开始分析原版支持库文件:" + instanceFolder); + if (Conversions.ToBoolean(id == "1.16.5" && Config.Download.FixAuthLib != null)) // 1.16.5 Authlib 修复 + try + { + var json = ModBase.ReadFile(instanceFolder + instanceName + ".json"); + json = json.Replace("2.1.28/authlib-2.1.28.jar", "2.3.31/authlib-2.3.31.jar") + .Replace("com.mojang:authlib:2.1.28", "com.mojang:authlib:2.3.31") + .Replace("ad54da276bf59983d02d5ed16fc14541354c71fd", "bbd00ca33b052f73a6312254780fc580d2da3535") + .Replace("76328", "87662"); + ModBase.WriteFile(instanceFolder + instanceName + ".json", json); + } + catch (Exception ex) + { + ModBase.Log("[Download] 替换 Authlib 版本失败: " + ex.Message); + } + + task.Output = ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(instanceFolder)); + }) + { + ProgressWeight = 1d, + Show = false + }); + loadersLib.Add(new ModNet.LoaderDownload("下载原版支持库文件(副加载器)", new List()) + { ProgressWeight = 13d, Show = false }); + loaders.Add(new ModLoader.LoaderCombo(McDownloadClientLibName, loadersLib) + { Block = false, ProgressWeight = 14d }); + + // 下载资源文件 + var loadersAssets = new List(); + loadersAssets.Add(new ModLoader.LoaderTask>("分析资源文件索引地址(副加载器)", task => + { + try + { + var assetIndex = new ModMinecraft.McInstance(instanceFolder); + task.Output = new List { ModDownload.DlClientAssetIndexGet(assetIndex) }; + } + catch (Exception ex) + { + throw new Exception("分析资源文件索引地址失败", ex); + } + + // 顺手添加 Json 项目 + try + { + var versionJson = (JObject)ModBase.GetJson(ModBase.ReadFile(instanceFolder + instanceName + ".json")); + versionJson.Add("clientVersion", id); + ModBase.WriteFile(instanceFolder + instanceName + ".json", versionJson.ToString()); + } + catch (Exception ex) + { + throw new Exception("添加客户端版本失败", ex); + } + }) + { + ProgressWeight = 1d, + Show = false + }); + loadersAssets.Add(new ModNet.LoaderDownload("下载资源文件索引(副加载器)", new List()) + { ProgressWeight = 3d, Show = false }); + loadersAssets.Add(new ModLoader.LoaderTask>("分析所需资源文件(副加载器)", task => + { + ModLoader.LoaderBase argprogressFeed = task; + task.Output = + ModMinecraft.McAssetsFixList(new ModMinecraft.McInstance(instanceFolder), true, ref argprogressFeed); + task = (ModLoader.LoaderTask>)argprogressFeed; + }) + { + ProgressWeight = 0.01d, + Show = false + }); + loadersAssets.Add(new ModNet.LoaderDownload("下载资源文件(副加载器)", new List()) + { ProgressWeight = 14d, Show = false }); + loaders.Add( + new ModLoader.LoaderCombo("下载原版资源文件", loadersAssets) { Block = false, ProgressWeight = 18d }); + + return loaders; + } + + private const string McDownloadClientLibName = "下载原版支持库文件"; + private const string McDownloadClientJsonName = "下载原版 Json 文件"; + + #endregion + + #region Minecraft 下载菜单 + + public static MyListItem McDownloadListItem(JObject Entry, MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 确定图标 + string Logo = Entry["type"].ToString() switch + { + "release" => ModBase.PathImage + "Blocks/Grass.png", + "snapshot" => ModBase.PathImage + "Blocks/CommandBlock.png", + "pending" => ModBase.PathImage + "Blocks/CommandBlock.png", + "special" => ModBase.PathImage + "Blocks/GoldBlock.png", + _ => ModBase.PathImage + "Blocks/CobbleStone.png" + }; + + // 建立控件 + var FormattedVersion = McFormatter.FormatVersion(Entry["id"].ToString()).Replace("_", " "); + var NewItem = new MyListItem + { + Logo = Logo, SnapsToDevicePixels = true, Title = FormattedVersion, Height = 42d, + Type = MyListItem.CheckType.Clickable, Tag = Entry + }; + if (Entry["lore"] is null) + { + if (FormattedVersion != (string)Entry["id"]) + NewItem.Info = Entry["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm") + " | " + + Entry["id"]; + else + NewItem.Info = Entry["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm"); + } + else if (FormattedVersion != (string)Entry["id"]) + { + NewItem.Info = Entry["lore"] + " | " + Entry["id"]; + } + else + { + NewItem.Info = Entry["lore"].ToString(); + } + + if (Entry["url"].ToString().Contains("unlisted-versions-of-minecraft")) + NewItem.Tags = "UVMC 特供下载"; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = McDownloadSaveMenuBuild; + else + NewItem.ContentHandler = McDownloadMenuBuild; + // 结束 + return NewItem; + } + + private static void McDownloadSaveMenuBuild(object sender, EventArgs _) + { + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (ss, ee) => McDownloadMenuLog(ss, (dynamic)ee); + var BtnServer = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonServer, ToolTip = "下载服务端" }; + ToolTipService.SetPlacement(BtnServer, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnServer, 30d); + ToolTipService.SetHorizontalOffset(BtnServer, 2d); + BtnServer.Click += (ss, ee) => McDownloadMenuSaveServer(ss, (dynamic)ee); + ((dynamic)sender).Buttons = new[] { BtnServer, BtnInfo }; + } + + private static void McDownloadMenuBuild(object sender, EventArgs e) + { + var BtnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(BtnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnSave, 30d); + ToolTipService.SetHorizontalOffset(BtnSave, 2d); + BtnSave.Click += (a, b) => McDownloadMenuSave(a, (dynamic)b); // dynamic! + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (a, b) => McDownloadMenuLog(a, (dynamic)b); // dynamic! + var BtnServer = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonServer, ToolTip = "下载服务端" }; + ToolTipService.SetPlacement(BtnServer, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnServer, 30d); + ToolTipService.SetHorizontalOffset(BtnServer, 2d); + BtnServer.Click += (a, b) => McDownloadMenuSaveServer(a, (dynamic)b); // dynamic! + ((dynamic)sender).Buttons = new[] { BtnSave, BtnInfo, BtnServer }; + } + + private static void McDownloadMenuLog(object sender, RoutedEventArgs e) + { + JToken Version; + if (((dynamic)sender).Tag is not null) + Version = (JToken)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Version = (JToken)((dynamic)sender).Parent.Tag; + else + Version = (JToken)((dynamic)sender).Parent.Parent.Tag; + McUpdateLogShow(Version); + } + + private static void McDownloadMenuSaveServer(object sender, RoutedEventArgs e) + { + MyListItem Version; + if (sender is MyListItem) + Version = (MyListItem)sender; + else if (((dynamic)sender).Parent is MyListItem) + Version = (MyListItem)((dynamic)sender).Parent; + else + Version = (MyListItem)((dynamic)sender).Parent.Parent; + try + { + var Id = Version.Title; + string JsonUrl = ((dynamic)Version.Tag)["url"].ToString(); + var VersionFolder = SystemDialogs.SelectFolder(); + if (!VersionFolder.Contains(@"\")) + return; + VersionFolder = VersionFolder + Id + @"\"; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"Minecraft {Id} 服务端下载" ?? "")) + continue; + ModMain.Hint("该服务端正在下载中!", ModMain.HintType.Critical); + return; + } + + var Loaders = new List(); + // 下载实例 JSON 文件 + Loaders.Add(new ModNet.LoaderDownload("下载实例 JSON 文件", + new List + { + new(ModDownload.DlSourceLauncherOrMetaGet(JsonUrl), VersionFolder + Id + ".json", + new ModBase.FileChecker(CanUseExistsFile: false, IsJson: true)) + }) { ProgressWeight = 2d }); + // 构建服务端 + Loaders.Add(new ModLoader.LoaderTask>("构建服务端", Task => + { + // 分析服务端 JAR 文件下载地址 + var McInstance = new ModMinecraft.McInstance(VersionFolder); + if (McInstance.JsonObject["downloads"] is null || + McInstance.JsonObject["downloads"]["server"] is null || + McInstance.JsonObject["downloads"]["server"]["url"] is null) + { + File.Delete(VersionFolder + Id + ".json"); + if (!new DirectoryInfo(VersionFolder).GetFileSystemInfos().Any()) + Directory.Delete(VersionFolder); + Task.Output = new List(); + ModMain.Hint($"Mojang 没有给 Minecraft {Id} 提供官方服务端下载,没法下,撤退!", ModMain.HintType.Critical); + Thread.Sleep(2000); // 等玩家把上一个提示看完 + Task.Abort(); + return; + } + + var JarUrl = (string)McInstance.JsonObject["downloads"]["server"]["url"]; + var Checker = new ModBase.FileChecker(1024L, + (long)(McInstance.JsonObject["downloads"]["server"]["size"] ?? -1), + (string)McInstance.JsonObject["downloads"]["server"]["sha1"]); + Task.Output = new List + { new(ModDownload.DlSourceLauncherOrMetaGet(JarUrl), VersionFolder + Id + "-server.jar", Checker) }; + // 添加启动脚本 + var Bat = $@"@echo off +title {Id} 原版服务端 +echo 如果服务端立即停止,请右键编辑该脚本,将下一行开头的 java 替换为适合该 Minecraft 版本的完整 java.exe 的路径。 +echo 你可以在 PCL 的 [设置 → 启动选项] 中查看已安装的 java,所需的 java.exe 一般在其中的 bin 文件夹下。 +echo ------------------------------ +echo 如果提示 ""You need to agree to the EULA in order to run the server"",请打开 eula.txt,按说明阅读并同意 Minecraft EULA 后,将该文件最后一行中的 eula=false 改为 eula=true。 +echo ------------------------------ +""java"" -server -XX:+UseG1GC -Xmx4096M -Xms1024M -XX:+UseCompressedOops -jar {Id}-server.jar nogui +echo ---------------------- +echo 服务端已停止。 +pause"; + ModBase.WriteFile(VersionFolder + "Launch Server.bat", Bat.Replace("\n", "\r\n"), + Encoding: Encoding.Default.Equals(Encoding.UTF8) ? Encoding.UTF8 : Encoding.GetEncoding("GB18030")); + // 删除实例 JSON + File.Delete(VersionFolder + Id + ".json"); + }) + { + ProgressWeight = 0.5d, + Show = false + }); + // 下载服务端文件 + Loaders.Add(new ModNet.LoaderDownload("下载服务端文件", new List()) { ProgressWeight = 5d }); + + // 启动 + var Loader = new ModLoader.LoaderCombo("Minecraft " + Id + " 服务端下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(Id); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "开始 Minecraft 服务端下载失败", ModBase.LogLevel.Feedback); + } + } + + public static void McDownloadMenuSave(object sender, RoutedEventArgs e) + { + var element = (FrameworkElement)sender; + MyListItem Version; + if (element is MyListItem s1) Version = s1; + else if (element.Parent is MyListItem s2) Version = s2; + else Version = (MyListItem)((FrameworkElement)element.Parent).Parent; + try + { + var Id = Version.Title; + var JsonUrl = ((JObject)Version.Tag)["url"]!.ToString(); + var VersionFolder = SystemDialogs.SelectFolder(); + if (!VersionFolder.Contains(@"\")) + return; + VersionFolder = VersionFolder + Id + @"\"; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"Minecraft {Id} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + var Loaders = new List(); + // 下载实例 JSON 文件 + Loaders.Add(new ModNet.LoaderDownload("下载实例 JSON 文件", + new List + { + new(ModDownload.DlSourceLauncherOrMetaGet(JsonUrl), VersionFolder + Id + ".json", + new ModBase.FileChecker(CanUseExistsFile: false, IsJson: true)) + }) { ProgressWeight = 2d }); + // 获取支持库文件地址 + Loaders.Add(new ModLoader.LoaderTask>("分析核心 JAR 文件下载地址", + Task => Task.Output = new List + { ModDownload.DlClientJarGet(new ModMinecraft.McInstance(VersionFolder), false) }) + { ProgressWeight = 0.5d, Show = false }); + // 下载支持库文件 + Loaders.Add(new ModNet.LoaderDownload("下载核心 JAR 文件", new List()) { ProgressWeight = 5d }); + + // 启动 + var Loader = new ModLoader.LoaderCombo("Minecraft " + Id + " 下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(Id); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "开始 Minecraft 下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 显示某 Minecraft 版本的更新日志。 + /// + /// 在 version_manifest.json 中的对应项。 + public static void McUpdateLogShow(JToken VersionJson) + { + var wikiName = McFormatter.GetWikiUrlSuffix(VersionJson["id"].ToString()); + ModBase.OpenWebsite("https://zh.minecraft.wiki/w/Special:Search?search=" + wikiName); + } + + #endregion + + #region OptiFine 下载 + + public static void McDownloadOptiFine(ModDownload.DlOptiFineListEntry DownloadInfo) + { + try + { + var Id = DownloadInfo.NameVersion; + var VersionFolder = ModMinecraft.McFolderSelected + @"versions\" + Id + @"\"; + var IsNewVersion = ModBase.Val(DownloadInfo.Inherit.Split(".")[1]) >= 14d; + var Target = IsNewVersion + ? ModBase.PathTemp + @"Cache\Code\" + DownloadInfo.NameVersion + "_" + ModBase.GetUuid() + : ModMinecraft.McFolderSelected + @"libraries\optifine\OptiFine\" + + DownloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", "") + @"\" + + DownloadInfo.NameFile.Replace("OptiFine_", "OptiFine-").Replace("preview_", ""); + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar) + { + if ((OngoingLoader.Name ?? "") != ($"OptiFine {DownloadInfo.DisplayName} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 已有实例检查 + if (File.Exists(VersionFolder + Id + ".json")) + { + if (ModMain.MyMsgBox( + "实例 " + Id + " 已存在,是否重新下载?" + "\r\n" + "这会覆盖实例的 Json 和 Jar 文件,但不会影响版本隔离的文件。", "实例已存在", + "继续", "取消") == 1) + { + File.Delete(VersionFolder + Id + ".jar"); + File.Delete(VersionFolder + Id + ".json"); + } + else + { + return; + } + } + + // 启动 + var Loader = + new ModLoader.LoaderCombo("OptiFine " + DownloadInfo.DisplayName + " 下载", + McDownloadOptiFineLoader(DownloadInfo)) { OnStateChanged = McInstallState }; + Loader.Start(VersionFolder); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 OptiFine 下载失败", ModBase.LogLevel.Feedback); + } + } + + private static void McDownloadOptiFineSave(ModDownload.DlOptiFineListEntry DownloadInfo) + { + try + { + var Id = DownloadInfo.NameVersion; + var Target = SystemDialogs.SelectSaveFile("选择保存位置", DownloadInfo.NameFile, "OptiFine Jar (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"OptiFine {DownloadInfo.DisplayName} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + var Loader = + new ModLoader.LoaderCombo( + "OptiFine " + DownloadInfo.DisplayName + " 下载", + McDownloadOptiFineSaveLoader(DownloadInfo, Target)) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(DownloadInfo); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 OptiFine 下载失败", ModBase.LogLevel.Feedback); + } + } + + private static void McDownloadOptiFineInstall(string BaseMcFolderHome, string Target, + ModLoader.LoaderTask, bool> Task, bool UseJavaWrapper) + { + // 选择 Java + JavaEntry Java; + lock (ModJava.JavaLock) + { + Java = ModJava.JavaSelect("已取消安装。", new Version(1, 8, 0, 0)); + if (Java is null) + { + if (!ModJava.JavaDownloadConfirm("Java 8 或更高版本")) + throw new Exception("由于未找到 Java,已取消安装。"); + // 开始自动下载 + var JavaLoader = ModJava.GetJavaDownloadLoader(); + try + { + JavaLoader.Start(17, true); + while (JavaLoader.State == ModBase.LoadState.Loading && !Task.IsAborted) + Thread.Sleep(10); + } + finally + { + JavaLoader.Abort(); // 确保取消时中止 Java 下载 + } + + // 检查下载结果 + Java = ModJava.JavaSelect("已取消安装。", new Version(1, 8, 0, 0)); + if (Task.IsAborted) + return; + if (Java is null) + throw new Exception("由于未找到 Java,已取消安装。"); + } + } + + // 添加 Java Wrapper 作为主 Jar + string Arguments; + if (Conversions.ToBoolean(UseJavaWrapper && + !(dynamic)Config.Launch.DisableJlw)) // dynamic! + Arguments = + $"-Doolloo.jlw.tmpdir=\"{ModBase.PathPure.TrimEnd('\\')}\" -Duser.home=\"{BaseMcFolderHome.TrimEnd('\\')}\" -cp \"{Target}\" -jar \"{ModLaunch.ExtractJavaWrapper()}\" optifine.Installer"; + else + Arguments = $"-Duser.home=\"{BaseMcFolderHome.TrimEnd('\\')}\" -cp \"{Target}\" optifine.Installer"; + if (Java.Installation.MajorVersion >= 9) + Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " + Arguments; + // 开始启动 + lock (InstallSyncLock) + { + var Info = new ProcessStartInfo + { + FileName = Java.Installation.JavaExePath, + Arguments = Arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + WorkingDirectory = ModBase.ShortenPath(BaseMcFolderHome) + }; + if (Info.EnvironmentVariables.ContainsKey("appdata")) + Info.EnvironmentVariables["appdata"] = BaseMcFolderHome; + else + Info.EnvironmentVariables.Add("appdata", BaseMcFolderHome); + ModBase.Log("[Download] 开始安装 OptiFine:" + Target); + var TotalLength = 0; + var process = new Process { StartInfo = Info }; + var LastResult = ""; + using (var outputWaitHandle = new AutoResetEvent(false)) + { + using (var errorWaitHandle = new AutoResetEvent(false)) + { + process.OutputDataReceived += (_, e) => + { + try + { + if (e.Data is null) + { + outputWaitHandle.Set(); + } + else + { + LastResult = e.Data; + if (ModBase.ModeDebug) + ModBase.Log("[Installer] " + LastResult); + TotalLength += 1; + Task.Progress += 0.9d / 7000d; + } + } + catch (ObjectDisposedException ex) + { + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 OptiFine 安装器信息失败"); + } + + try + { + if (Task.State == ModBase.LoadState.Aborted && !process.HasExited) + { + ModBase.Log("[Installer] 由于任务取消,已中止 OptiFine 安装"); + process.Kill(); + } + } + catch + { + } + }; + process.ErrorDataReceived += (sender, e) => + { + try + { + if (e.Data is null) + { + errorWaitHandle.Set(); + } + else + { + LastResult = e.Data; + if (ModBase.ModeDebug) + ModBase.Log("[Installer] " + LastResult); + TotalLength += 1; + Task.Progress += 0.9d / 7000d; + } + } + catch (ObjectDisposedException ex) + { + } + catch (Exception ex) + { + ModBase.Log(ex, "读取 OptiFine 安装器错误信息失败"); + } + + try + { + if (Task.State == ModBase.LoadState.Aborted && !process.HasExited) + { + ModBase.Log("[Installer] 由于任务取消,已中止 OptiFine 安装"); + process.Kill(); + } + } + catch + { + } + }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + // 等待 + while (!process.HasExited) + Thread.Sleep(10); + // 输出 + outputWaitHandle.WaitOne(10000); + errorWaitHandle.WaitOne(10000); + process.Dispose(); + if (TotalLength < 1000 || LastResult.Contains("at ")) + throw new Exception("安装器运行出错,末行为 " + LastResult); + } + } + } + } + + /// + /// 获取下载某个 OptiFine 实例的加载器列表。 + /// + private static List McDownloadOptiFineLoader(ModDownload.DlOptiFineListEntry DownloadInfo, + string McFolder = null, ModLoader.LoaderCombo ClientDownloadLoader = null, string ClientFolder = null, + bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = DownloadInfo.NameVersion; + var VersionFolder = McFolder + @"versions\" + Id + @"\"; + var IsNewVersion = DownloadInfo.Inherit.Contains("w") || ModBase.Val(DownloadInfo.Inherit.Split(".")[1]) >= 14d; + var Target = IsNewVersion + ? $"{ModMain.RequestTaskTempFolder()}OptiFine.jar" + : $@"{McFolder}libraries\optifine\OptiFine\{DownloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", "")}\{DownloadInfo.NameFile.Replace("OptiFine_", "OptiFine-").Replace("preview_", "")}"; + var Loaders = new List(); + + // 获取下载地址 + Loaders.Add(new ModLoader.LoaderTask>("获取 OptiFine 主文件下载地址", Task => + { + // 启动依赖实例的下载 + if (ClientDownloadLoader is null) + { + if (IsCustomFolder) + throw new Exception("如果没有指定原版下载器,则不能指定 MC 安装文件夹"); + ClientDownloadLoader = McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, + DownloadInfo.Inherit); + } + + Task.Progress = 0.1d; + var Sources = new List(); + // BMCLAPI 源 + var BmclapiInherit = DownloadInfo.Inherit; + if (BmclapiInherit == "1.8" || BmclapiInherit == "1.9") + BmclapiInherit += ".0"; // #4281 + if (DownloadInfo.IsPreview) + Sources.Add("https://bmclapi2.bangbang93.com/optifine/" + BmclapiInherit + "/HD_U_" + + DownloadInfo.DisplayName.Replace(DownloadInfo.Inherit + " ", "").Replace(" ", "/")); + else + Sources.Add("https://bmclapi2.bangbang93.com/optifine/" + BmclapiInherit + "/HD_U/" + + DownloadInfo.DisplayName.Replace(DownloadInfo.Inherit + " ", "")); + // 官方源 + string PageData; + try + { + PageData = HttpRequestBuilder + .Create("https://optifine.net/adloadx?f=" + DownloadInfo.NameFile, HttpMethod.Get) + .WithHeader("Accept", "text/html").WithHeader("Accept-Language", "en-US,en;q=0.5") + .WithHeader("X-Requested-With", "XMLHttpRequest").SendAsync(true).GetAwaiter().GetResult() + .AsStringContent(); + Task.Progress = 0.8d; + Sources.Add("https://optifine.net/" + PageData.RegexSearch(@"downloadx\?f=[^""']+")[0]); + ModBase.Log("[Download] OptiFine " + DownloadInfo.DisplayName + " 官方下载地址:" + Sources.Last()); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 OptiFine " + DownloadInfo.DisplayName + " 官方下载地址失败"); + } + + // 构造文件请求 + Task.Output = new List + { new(Sources.ToArray(), Target, new ModBase.FileChecker(300 * 1024)) }; + }) + { + ProgressWeight = 8d + }); + Loaders.Add(new ModNet.LoaderDownload("下载 OptiFine 主文件", new List()) { ProgressWeight = 8d }); + Loaders.Add(new ModLoader.LoaderTask, bool>("等待原版下载", Task => + { + // 等待原版文件下载完成 + if (ClientDownloadLoader is null) + return; + var TargetLoaders = ClientDownloadLoader.GetLoaderList() + .Where(l => (l.Name ?? "") == McDownloadClientLibName || (l.Name ?? "") == McDownloadClientJsonName) + .Where(l => l.State != ModBase.LoadState.Finished).ToList(); + if (TargetLoaders.Any()) + ModBase.Log("[Download] OptiFine 安装正在等待原版文件下载完成"); + while (TargetLoaders.Any() && !Task.IsAborted) + { + TargetLoaders = TargetLoaders.Where(l => l.State != ModBase.LoadState.Finished).ToList(); + Thread.Sleep(50); + } + + if (Task.IsAborted) + return; + // 拷贝原版文件 + if (!IsCustomFolder) + return; + lock (VanillaSyncLock) + { + var ClientName = ModBase.GetFolderNameFromPath(ClientFolder); + Directory.CreateDirectory(McFolder + @"versions\" + DownloadInfo.Inherit); + if (!File.Exists(McFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + + ".json")) + ModBase.CopyFile($"{ClientFolder}{ClientName}.json", + $@"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.json"); + if (!File.Exists(McFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".jar")) + ModBase.CopyFile($"{ClientFolder}{ClientName}.jar", + $@"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.jar"); + } + }) + { + ProgressWeight = 0.1d, + Show = false + }); + + // 安装(新旧方式均需要原版 Jar 和 Json) + if (IsNewVersion) + { + ModBase.Log("[Download] 检测为新版 OptiFine:" + DownloadInfo.Inherit); + Loaders.Add(new ModLoader.LoaderTask, bool>("安装 OptiFine(方式 A)", Task => + { + var BaseMcFolderHome = ModMain.RequestTaskTempFolder(); + var BaseMcFolder = BaseMcFolderHome + @".minecraft\"; + try + { + // 准备安装环境 + if (Directory.Exists(BaseMcFolder + @"versions\" + DownloadInfo.Inherit)) + ModBase.DeleteDirectory(BaseMcFolder + @"versions\" + DownloadInfo.Inherit); + Directory.CreateDirectory(BaseMcFolder + @"versions\" + DownloadInfo.Inherit + @"\"); + ModMinecraft.McFolderLauncherProfilesJsonCreate(BaseMcFolder); + ModBase.CopyFile( + McFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".json", + BaseMcFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".json"); + ModBase.CopyFile( + McFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".jar", + BaseMcFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".jar"); + Task.Progress = 0.06d; + // 进行安装 + var UseJavaWrapper = ModBase.IsUtf8CodePage(); + Retry: ; + + try + { + McDownloadOptiFineInstall(BaseMcFolderHome, Target, Task, UseJavaWrapper); + } + catch (Exception ex) + { + if (!UseJavaWrapper) + { + ModBase.Log(ex, "不使用 JavaWrapper 安装 OptiFine 失败,将使用 JavaWrapper 并重试"); + UseJavaWrapper = true; + goto Retry; + } + + throw new Exception("运行 OptiFine 安装器失败", ex); + } + + Task.Progress = 0.96d; + // 复制文件 + File.Delete(BaseMcFolder + "launcher_profiles.json"); + ModBase.CopyDirectory(BaseMcFolder, McFolder); + Task.Progress = 0.98d; + // 清理文件 + File.Delete(Target); + ModBase.DeleteDirectory(BaseMcFolderHome); + } + catch (Exception ex) + { + throw new Exception("安装 OptiFine(方式 A)失败", ex); + } + }) + { + ProgressWeight = 8d + }); + } + else + { + ModBase.Log("[Download] 检测为旧版 OptiFine:" + DownloadInfo.Inherit); + // 新建实例文件夹 + // 复制 Jar 文件 + // 建立 Json 文件 + Loaders.Add(new ModLoader.LoaderTask, bool>("安装 OptiFine(方式 B)", Task => + { + try + { + Directory.CreateDirectory(VersionFolder); + Task.Progress = 0.1d; + if (File.Exists(VersionFolder + Id + ".jar")) File.Delete(VersionFolder + Id + ".jar"); + ModBase.CopyFile( + McFolder + @"versions\" + DownloadInfo.Inherit + @"\" + DownloadInfo.Inherit + ".jar", + VersionFolder + Id + ".jar"); + Task.Progress = 0.7d; + var InheritInstance = + new ModMinecraft.McInstance(McFolder + @"versions\" + DownloadInfo.Inherit); + var Json = @"{ + ""id"": """ + Id + @""", + ""inheritsFrom"": """ + DownloadInfo.Inherit + @""", + ""time"": """ + + (string.IsNullOrEmpty(DownloadInfo.ReleaseTime) + ? InheritInstance.ReleaseTime.ToString("yyyy'-'MM'-'dd") + : DownloadInfo.ReleaseTime.Replace("/", "-")) + @"T23:33:33+08:00"", + ""releaseTime"": """ + + (string.IsNullOrEmpty(DownloadInfo.ReleaseTime) + ? InheritInstance.ReleaseTime.ToString("yyyy'-'MM'-'dd") + : DownloadInfo.ReleaseTime.Replace("/", "-")) + @"T23:33:33+08:00"", + ""type"": ""release"", + ""libraries"": [ + {""name"": ""optifine:OptiFine:" + + DownloadInfo.NameFile.Replace("OptiFine_", "").Replace(".jar", "") + .Replace("preview_", "") + // 输出旧版 Json 格式 + @"""}, + {""name"": ""net.minecraft:launchwrapper:1.12""} + ], + ""mainClass"": ""net.minecraft.launchwrapper.Launch"","; + Task.Progress = 0.8d; + if (InheritInstance.IsOldJson) + Json += @" + ""minimumLauncherVersion"": 18, + ""minecraftArguments"": """ + InheritInstance.JsonObject["minecraftArguments"] + // 输出新版 Json 格式 + @" --tweakClass optifine.OptiFineTweaker"" +}"; + else + Json += @" + ""minimumLauncherVersion"": ""21"", + ""arguments"": { + ""game"": [ + ""--tweakClass"", + ""optifine.OptiFineTweaker"" + ] + } +}"; + ModBase.WriteFile(VersionFolder + Id + ".json", Json); + } + catch (Exception ex) + { + throw new Exception("安装 OptiFine(方式 B)失败", ex); + } + }) + { ProgressWeight = 1d }); + } + + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 OptiFine 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add(new ModNet.LoaderDownload("下载 OptiFine 支持库文件", new List()) + { ProgressWeight = 4d }); + } + + return Loaders; + } + + /// + /// 获取保存某个 OptiFine 版本的加载器列表。 + /// + private static List McDownloadOptiFineSaveLoader(ModDownload.DlOptiFineListEntry downloadInfo, + string targetFolder) + { + var loaders = new List(); + // 获取下载地址 + loaders.Add(new ModLoader.LoaderTask>("获取 OptiFine 下载地址", + Task => + { + var sources = new List(); + // BMCLAPI 源 + var BmclapiInherit = downloadInfo.Inherit; + if (BmclapiInherit == "1.8" || BmclapiInherit == "1.9") + BmclapiInherit += ".0"; // #4281 + if (downloadInfo.IsPreview) + sources.Add("https://bmclapi2.bangbang93.com/optifine/" + BmclapiInherit + "/HD_U_" + + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "").Replace(" ", "/")); + else + sources.Add("https://bmclapi2.bangbang93.com/optifine/" + BmclapiInherit + "/HD_U/" + + downloadInfo.DisplayName.Replace(downloadInfo.Inherit + " ", "")); + // 官方源 + string PageData; + try + { + PageData = HttpRequestBuilder + .Create("https://optifine.net/adloadx?f=" + downloadInfo.NameFile, HttpMethod.Get) + .WithHeader("Accept", "text/html").WithHeader("Accept-Language", "en-US,en;q=0.5") + .WithHeader("X-Requested-With", "XMLHttpRequest").SendAsync(true).GetAwaiter().GetResult() + .AsStringContent(); + Task.Progress = 0.8d; + sources.Add("https://optifine.net/" + PageData.RegexSearch(@"downloadx\?f=[^""']+")[0]); + ModBase.Log("[Download] OptiFine " + downloadInfo.DisplayName + " 官方下载地址:" + sources.Last()); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 OptiFine " + downloadInfo.DisplayName + " 官方下载地址失败"); + } + + Task.Progress = 0.9d; + // 构造文件请求 + Task.Output = new List + { new(sources.ToArray(), targetFolder, new ModBase.FileChecker(64 * 1024)) }; + }) + { + ProgressWeight = 6d + }); + // 下载 + loaders.Add(new ModNet.LoaderDownload("下载 OptiFine 主文件", new List()) + { ProgressWeight = 10d, Block = true }); + return loaders; + } + + #endregion + + #region OptiFine 下载菜单 + + public static MyListItem OptiFineDownloadListItem(ModDownload.DlOptiFineListEntry Entry, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.DisplayName, + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = (Entry.IsPreview ? "测试版" : "正式版") + + (string.IsNullOrEmpty(Entry.ReleaseTime) ? "" : ",发布于 " + Entry.ReleaseTime) + + (Entry.RequiredForgeVersion is null ? ",不兼容 Forge" : + string.IsNullOrEmpty(Entry.RequiredForgeVersion) ? "" : + ",兼容 Forge " + Entry.RequiredForgeVersion), + Logo = ModBase.PathImage + "Blocks/GrassPath.png" + }; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = OptiFineSaveContMenuBuild; + else + NewItem.ContentHandler = OptiFineContMenuBuild; + // 结束 + return NewItem; + } + + private static void OptiFineSaveContMenuBuild(object sender, EventArgs e) + { + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += static (sender, e) => OptiFineSaveContMenuBuild(sender, e); + ((dynamic)sender).Buttons = new[] { BtnInfo }; + } + + private static void OptiFineContMenuBuild(object sender, EventArgs e) + { + var btnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(btnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnSave, 30d); + ToolTipService.SetHorizontalOffset(btnSave, 2d); + //btnSave.Click += () ModDownloadLib.OptiFineSave_Click; + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (sender, e) => OptiFineLog_Click(sender, (RoutedEventArgs)e); + ((dynamic)sender).Buttons = new[] { btnSave, BtnInfo }; + } + + private static void OptiFineLog_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlOptiFineListEntry Version; + if (((dynamic)sender).Tag is not null) + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Tag; + else + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Parent.Tag; + ModBase.OpenWebsite("https://optifine.net/changelog?f=" + Version.NameFile); + } + + public static void OptiFineSave_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlOptiFineListEntry Version; + if (((dynamic)sender).Tag is not null) + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Tag; + else + Version = (ModDownload.DlOptiFineListEntry)((dynamic)sender).Parent.Parent.Tag; + McDownloadOptiFineSave(Version); + } + + #endregion + + #region LiteLoader 下载 + + public static void McDownloadLiteLoader(ModDownload.DlLiteLoaderListEntry DownloadInfo) + { + try + { + var Id = DownloadInfo.Inherit; + var Target = ModBase.PathTemp + @"Download\" + Id + "-Liteloader.jar"; + var VersionName = DownloadInfo.Inherit + "-LiteLoader"; + var VersionFolder = ModMinecraft.McFolderSelected + @"versions\" + VersionName + @"\"; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"LiteLoader {Id} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 已有实例检查 + if (File.Exists(VersionFolder + VersionName + ".json")) + { + if (ModMain.MyMsgBox( + "实例 " + VersionName + " 已存在,是否重新下载?" + "\r\n" + "这会覆盖实例的 Json 和 Jar 文件,但不会影响版本隔离的文件。", + "实例已存在", "继续", "取消") == 1) + { + File.Delete(VersionFolder + VersionName + ".jar"); + File.Delete(VersionFolder + VersionName + ".json"); + } + else + { + return; + } + } + + // 启动 + var Loader = + new ModLoader.LoaderCombo("LiteLoader " + Id + " 下载", McDownloadLiteLoaderLoader(DownloadInfo)) + { OnStateChanged = McInstallState }; + Loader.Start(VersionFolder); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 LiteLoader 下载失败", ModBase.LogLevel.Feedback); + } + } + + private static void McDownloadLiteLoaderSave(ModDownload.DlLiteLoaderListEntry DownloadInfo) + { + try + { + var Id = DownloadInfo.Inherit; + var Target = SystemDialogs.SelectSaveFile("选择保存位置", DownloadInfo.FileName.Replace("-SNAPSHOT", ""), + "LiteLoader 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"LiteLoader {Id} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + var Address = new List(); + if (DownloadInfo.IsLegacy) + // 老版本 + switch (DownloadInfo.Inherit ?? "") + { + case "1.7.10": + { + Address.Add("https://dl.liteloader.com/redist/1.7.10/liteloader-installer-1.7.10-04.jar"); + break; + } + case "1.7.2": + { + Address.Add("https://dl.liteloader.com/redist/1.7.2/liteloader-installer-1.7.2-04.jar"); + break; + } + case "1.6.4": + { + Address.Add("https://dl.liteloader.com/redist/1.6.4/liteloader-installer-1.6.4-01.jar"); + break; + } + case "1.6.2": + { + Address.Add("https://dl.liteloader.com/redist/1.6.2/liteloader-installer-1.6.2-04.jar"); + break; + } + case "1.5.2": + { + Address.Add("https://dl.liteloader.com/redist/1.5.2/liteloader-installer-1.5.2-01.jar"); + break; + } + + default: + { + throw new NotSupportedException("未知的 Minecraft 版本(" + DownloadInfo.Inherit + ")"); + } + } + else + // 官方源 + Address.Add("http://jenkins.liteloader.com/job/LiteLoaderInstaller%20" + DownloadInfo.Inherit + + "/lastSuccessfulBuild/artifact/" + + (DownloadInfo.Inherit == "1.8" ? "ant/dist/" : "build/libs/") + DownloadInfo.FileName); + + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 1024)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = + new ModLoader.LoaderCombo("LiteLoader " + Id + " 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(DownloadInfo); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 LiteLoader 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 LiteLoader 实例的加载器列表。 + /// + private static List McDownloadLiteLoaderLoader(ModDownload.DlLiteLoaderListEntry DownloadInfo, + string McFolder = null, ModLoader.LoaderCombo ClientDownloadLoader = null, bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = DownloadInfo.Inherit; + var Target = ModBase.PathTemp + @"Download\" + Id + "-Liteloader.jar"; + var VersionName = DownloadInfo.Inherit + "-LiteLoader"; + var VersionFolder = McFolder + @"versions\" + VersionName + @"\"; + var Loaders = new List(); + + // 启动依赖实例的下载 + if (ClientDownloadLoader is null) + Loaders.Add(new ModLoader.LoaderTask("启动 LiteLoader 依赖实例下载", _ => + { + if (IsCustomFolder) + throw new Exception("如果没有指定原版下载器,则不能指定 MC 安装文件夹"); + ClientDownloadLoader = McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, + DownloadInfo.Inherit); + }) + { + ProgressWeight = 0.2d, + Show = false, + Block = false + }); + // 安装 + // 新建实例文件夹 + // 构造实例 Json + // 输出 Json 文件 + Loaders.Add(new ModLoader.LoaderTask("安装 LiteLoader", _ => + { + try + { + Directory.CreateDirectory(VersionFolder); + var VersionJson = new JObject(); + VersionJson.Add("id", VersionName); + VersionJson.Add("time", + DateTime.ParseExact(DownloadInfo.ReleaseTime, "yyyy/MM/dd HH:mm", CultureInfo.CurrentCulture)); + VersionJson.Add("releaseTime", + DateTime.ParseExact(DownloadInfo.ReleaseTime, "yyyy/MM/dd HH:mm", CultureInfo.CurrentCulture)); + VersionJson.Add("type", "release"); + VersionJson.Add("arguments", + (JToken)ModBase.GetJson("{\"game\":[\"--tweakClass\",\"" + DownloadInfo.JsonToken["tweakClass"] + + "\"]}")); + VersionJson.Add("libraries", DownloadInfo.JsonToken["libraries"]); + ((JContainer)VersionJson["libraries"]).Add(ModBase.GetJson("{\"name\": \"com.mumfrey:liteloader:" + + DownloadInfo.JsonToken["version"] + + "\",\"url\": \"https://dl.liteloader.com/versions/\"}")); + VersionJson.Add("mainClass", "net.minecraft.launchwrapper.Launch"); + VersionJson.Add("minimumLauncherVersion", 18); + VersionJson.Add("inheritsFrom", DownloadInfo.Inherit); + VersionJson.Add("jar", DownloadInfo.Inherit); + ModBase.WriteFile(VersionFolder + VersionName + ".json", VersionJson.ToString()); + } + catch (Exception ex) + { + throw new Exception("安装新 LiteLoader 实例失败", ex); + } + }) { ProgressWeight = 1d }); + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 LiteLoader 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add(new ModNet.LoaderDownload("下载 LiteLoader 支持库文件", new List()) + { ProgressWeight = 6d }); + } + + return Loaders; + } + + #endregion + + #region LiteLoader 下载菜单 + + public static MyListItem LiteLoaderDownloadListItem(ModDownload.DlLiteLoaderListEntry Entry, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.Inherit, + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = (Entry.IsPreview ? "测试版" : "稳定版") + + (string.IsNullOrEmpty(Entry.ReleaseTime) ? "" : ",发布于 " + Entry.ReleaseTime), + Logo = ModBase.PathImage + "Blocks/Egg.png" + }; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = LiteLoaderSaveContMenuBuild; + else + NewItem.ContentHandler = LiteLoaderContMenuBuild; + // 结束 + return NewItem; + } + + private static void LiteLoaderSaveContMenuBuild(MyListItem sender, EventArgs e) + { + if (Conversions.ToBoolean(((dynamic)sender.Tag).IsLegacy)) + { + sender.Buttons = Array.Empty(); + } + else + { + var BtnList = new MyIconButton { Logo = ModBase.Logo.IconButtonList, ToolTip = "查看全部版本", Tag = sender }; + ToolTipService.SetPlacement(BtnList, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnList, 30d); + ToolTipService.SetHorizontalOffset(BtnList, 2d); + BtnList.Click += (sender, e) => LiteLoaderAll_Click(sender, (RoutedEventArgs)e); + sender.Buttons = new[] { BtnList }; + } + } + + private static void LiteLoaderContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "保存安装器", Tag = sender }; + ToolTipService.SetPlacement(BtnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnSave, 30d); + ToolTipService.SetHorizontalOffset(BtnSave, 2d); + BtnSave.Click += (sender, e) => LiteLoaderSave_Click(sender, (RoutedEventArgs)e); + if (Conversions.ToBoolean(((dynamic)sender.Tag).IsLegacy)) + { + sender.Buttons = [BtnSave]; + } + else + { + var BtnList = new MyIconButton { Logo = ModBase.Logo.IconButtonList, ToolTip = "查看全部版本", Tag = sender }; + ToolTipService.SetPlacement(BtnList, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnList, 30d); + ToolTipService.SetHorizontalOffset(BtnList, 2d); + BtnList.Click += (sender, e) => LiteLoaderAll_Click(sender, (RoutedEventArgs)e); + sender.Buttons = [BtnSave, BtnList]; + } + } + + private static void LiteLoaderAll_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlLiteLoaderListEntry Version; + if (((dynamic)sender).Tag is ModDownload.DlLiteLoaderListEntry) + Version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag; + else + Version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag.Tag; + ModBase.OpenWebsite("https://jenkins.liteloader.com/view/" + Version.Inherit); + } + + public static void LiteLoaderSave_Click(object sender, RoutedEventArgs e) + { + // ListItem 与小按钮都会调用这个方法 + ModDownload.DlLiteLoaderListEntry Version; + if (((dynamic)sender).Tag is ModDownload.DlLiteLoaderListEntry) + Version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag; + else + Version = (ModDownload.DlLiteLoaderListEntry)((dynamic)sender).Tag.Tag; + McDownloadLiteLoaderSave(Version); + } + + #endregion + + #region Forgelike 下载 + + public static void McDownloadForgelikeSave(ModDownload.DlForgelikeEntry Info) + { + try + { + var Target = SystemDialogs.SelectSaveFile("选择保存位置", + $"{Info.LoaderName}-{Info.Inherit}-{Info.VersionName}.{Info.FileExtension}", + $"{Info.LoaderName} 安装器 (*.{Info.FileExtension})|*.{Info.FileExtension}"); + var DisplayName = $"{Info.LoaderName} {Info.Inherit} - {Info.VersionName}"; + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"{DisplayName} 下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 获取下载地址 + var Files = new List(); + if ((int)Info.ForgeType == 1) + { + // NeoForge + var Neo = (ModDownload.DlNeoForgeListEntry)Info; + var Url = Neo.UrlBase + "-installer.jar"; + Files.Add(new ModNet.NetFile( + new[] { Url.Replace("maven.neoforged.net/releases", "bmclapi2.bangbang93.com/maven"), Url }, Target, + new ModBase.FileChecker(64 * 1024))); + } + else + { + // Forge + var Forge = (ModDownload.DlForgeVersionEntry)Info; + Files.Add(new ModNet.NetFile( + new[] + { + $"https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{Forge.Inherit}-{Forge.FileVersion}/forge-{Forge.Inherit}-{Forge.FileVersion}-{Forge.Category}.{Forge.FileExtension}", + $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{Forge.Inherit}-{Forge.FileVersion}/forge-{Forge.Inherit}-{Forge.FileVersion}-{Forge.Category}.{Forge.FileExtension}" + }, Target, new ModBase.FileChecker(64 * 1024, Hash: Forge.Hash))); + } + + // 构造加载器 + var Loaders = new List(); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", Files) { ProgressWeight = 6d }); + + // 启动 + var Loader = new ModLoader.LoaderCombo(DisplayName + " 下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(Info); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, $"开始 {Info.LoaderName} 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + private static void ForgelikeInjector(string Target, ModLoader.LoaderTask Task, string McFolder, + bool UseJavaWrapper, string ForgeType) + { + // 选择 Java + JavaEntry Java; + lock (ModJava.JavaLock) + { + Java = ModJava.JavaSelect("已取消安装。", new Version(1, 8, 0, 60)); + if (Java is null) + { + if (!ModJava.JavaDownloadConfirm("Java 8 或更高版本")) + throw new Exception("由于未找到 Java,已取消安装。"); + // 开始自动下载 + var JavaLoader = ModJava.GetJavaDownloadLoader(); + try + { + JavaLoader.Start(17, true); + while (JavaLoader.State == ModBase.LoadState.Loading && !Task.IsAborted) + Thread.Sleep(10); + } + finally + { + JavaLoader.Abort(); // 确保取消时中止 Java 下载 + } + + // 检查下载结果 + Java = ModJava.JavaSelect("已取消安装。", new Version(1, 8, 0, 60)); + if (Task.IsAborted) + return; + if (Java is null) + throw new Exception("由于未找到 Java,已取消安装。"); + } + } + + // 添加 Java Wrapper 作为主 Jar + string Arguments; + if (Conversions.ToBoolean(UseJavaWrapper && !(bool)Config.Launch.DisableJlw)) + Arguments = + $@"-Doolloo.jlw.tmpdir=""{ModBase.PathPure.TrimEnd('\\')}"" -cp ""{ModBase.PathTemp}Cache\forge_installer.jar;{Target}"" -jar ""{ModLaunch.ExtractJavaWrapper()}"" com.bangbang93.ForgeInstaller ""{McFolder}"; + else + Arguments = + $@"-cp ""{ModBase.PathTemp}Cache\forge_installer.jar;{Target}"" com.bangbang93.ForgeInstaller ""{McFolder}"; + if (Java.Installation.MajorVersion >= 9) + Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " + Arguments; + // 开始启动 + lock (InstallSyncLock) + { + var Info = new ProcessStartInfo + { + FileName = Java.Installation.JavaExePath, + Arguments = Arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true + }; + var LoaderName = ForgeType; + ModBase.Log($"[Download] 开始安装 {LoaderName}:" + Arguments); + var process = new Process { StartInfo = Info }; + var LastResults = new Queue(); + using (var outputWaitHandle = new AutoResetEvent(false)) + { + using (var errorWaitHandle = new AutoResetEvent(false)) + { + process.OutputDataReceived += (sender, e) => + { + try + { + if (e.Data is null) + { + outputWaitHandle.Set(); + } + else + { + LastResults.Enqueue(e.Data); + if (LastResults.Count > 100) + LastResults.Dequeue(); + ForgelikeInjectorLine(e.Data, Task); + } + } + catch (ObjectDisposedException ex) + { + } + catch (Exception ex) + { + ModBase.Log(ex, $"读取 {LoaderName} 安装器信息失败"); + } + + try + { + if (Task.State == ModBase.LoadState.Aborted && !process.HasExited) + { + ModBase.Log($"[Installer] 由于任务取消,已中止 {LoaderName} 安装"); + process.Kill(); + } + } + catch + { + } + }; + process.ErrorDataReceived += (sender, e) => + { + try + { + if (e.Data is null) + { + errorWaitHandle.Set(); + } + else + { + LastResults.Enqueue(e.Data); + if (LastResults.Count > 100) + LastResults.Dequeue(); + ForgelikeInjectorLine(e.Data, Task); + } + } + catch (ObjectDisposedException ex) + { + } + catch (Exception ex) + { + ModBase.Log(ex, $"读取 {LoaderName} 安装器错误信息失败"); + } + + try + { + if (Task.State == ModBase.LoadState.Aborted && !process.HasExited) + { + ModBase.Log($"[Installer] 由于任务取消,已中止 {LoaderName} 安装"); + process.Kill(); + } + } + catch + { + } + }; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + // 等待 + while (!process.HasExited) + Thread.Sleep(10); + // 输出 + outputWaitHandle.WaitOne(10000); + errorWaitHandle.WaitOne(10000); + process.Dispose(); + // 检查是否安装成功:最后 5 行中是否有 true(true 可能在倒数数行,见 #832) + if (LastResults.Reverse().Take(5).Any(l => l == "true")) + return; + ModBase.Log(LastResults.Join("\r\n")); + var LastLines = ""; + for (int i = Math.Max(0, LastResults.Count - 5), loopTo = LastResults.Count - 1; + i <= loopTo; + i++) // 最后 5 行 + LastLines += "\r\n" + LastResults.ElementAtOrDefault(i); + throw new Exception($"{LoaderName} 安装器出错,日志结束部分为:" + LastLines); + } + } + } + } + + private static void ForgelikeInjectorLine(string Content, ModLoader.LoaderTask Task) + { + switch (Content ?? "") + { + case "Extracting json": + { + ModBase.Log("[Installer] " + Content); + Task.Progress = 0.07d; + break; + } + case "Downloading libraries": + { + ModBase.Log("[Installer] " + Content); + Task.Progress = 0.08d; + break; + } + case " File exists: Checksum validated.": + { + if (ModBase.ModeDebug) + ModBase.Log("[Installer] " + Content); + Task.Progress += 0.003d; + break; + } + case "Building Processors": + { + Task.Progress = 0.18d; + break; + } + case "Task: DOWNLOAD_MOJMAPS": // B + { + Task.Progress = 0.2d; + break; + } + case "Task: MERGE_MAPPING": // B + { + Task.Progress = 0.3d; + break; + } + case "Splitting: ": + { + Task.Progress = 0.35d; + break; + } + case "Parameter Annotations": // B + { + Task.Progress = 0.4d; + break; + } + case "Processing Complete": // B + { + Task.Progress = 0.5d; + break; + } + case "log: null": // new + { + Task.Progress = 0.5d; + break; + } + case "Sorting": // new + { + Task.Progress = 0.65d; + break; + } + case "Remapping final jar": // A + { + Task.Progress = 0.72d; + break; + } + case "Remapping jar... 50%": // A + { + Task.Progress = 0.76d; + break; + } + case "Remapping jar... 100%": // A + { + Task.Progress = 0.81d; + break; + } + case "Injecting profile": + { + Task.Progress = 0.91d; + break; + } + + default: + { + if (ModBase.ModeDebug) + ModBase.Log("[Installer] " + Content); + return; + } + } + + ModBase.Log("[Installer] " + Content); + } + + /// + /// 获取下载某个 Forgelike 实例的加载器列表。 + /// + private static List McDownloadForgelikeLoader(string ForgeType, string LoaderVersion, + string TargetVersion, string Inherit, ModDownload.DlForgelikeEntry Info = null, string McFolder = null, + ModLoader.LoaderCombo ClientDownloadLoader = null, string ClientFolder = null) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + if (ForgeType == "NeoForge" && Info is null) + { + // 需要传入 API Name,但整合包版本可能不以 1.20.1- 开头,所以需要进行特别处理 + if (Inherit == "1.20.1" && !LoaderVersion.StartsWithF("1.20.1-")) + Info = new ModDownload.DlNeoForgeListEntry("1.20.1-" + LoaderVersion); + else + Info = new ModDownload.DlNeoForgeListEntry(LoaderVersion); + } + + if (ForgeType == "Cleanroom" && Info is null) Info = new ModDownload.DlCleanroomListEntry(LoaderVersion); + if (!(ForgeType == "NeoForge") && LoaderVersion.StartsWithF("1.") && LoaderVersion.Contains("-")) + { + // 类似 1.19.3-41.2.8 格式,优先使用 Version 中要求的版本而非 Inherit(例如 1.19.3 却使用了 1.19 的 Forge) + Inherit = LoaderVersion.BeforeFirst("-"); + LoaderVersion = LoaderVersion.AfterLast("-"); + } + + var LoaderName = ForgeType; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var InstallerAddress = ModMain.RequestTaskTempFolder() + "forge_installer.jar"; + var VersionFolder = $@"{McFolder}versions\{TargetVersion}\"; + var DisplayName = $"{LoaderName} {Inherit} - {LoaderVersion}"; + var Loaders = new List(); + var LibVersionFolder = $@"{ModMinecraft.McFolderSelected}versions\{TargetVersion}\"; // 作为 Lib 文件目标的实例文件夹 + + // 获取 Forge 下载信息 + if (Info is null) + Loaders.Add(new ModLoader.LoaderTask($"获取 {LoaderName} 详细信息", Task => + { + // 获取 Forge 对应 MC 版本列表 + var ForgeLoader = + new ModLoader.LoaderTask>( + "McDownloadForgeLoader " + Inherit, ModDownload.DlForgeVersionMain); + ForgeLoader.WaitForExit(Inherit); + Task.Progress = 0.8d; + // 查找对应版本 + foreach (var ForgeVersion in ForgeLoader.Output) + if (ModMinecraft.CompareVersion(ForgeVersion.Version.ToString(), LoaderVersion) == 0) + { + Info = ForgeVersion; + return; + } + + throw new Exception($"未能找到 {LoaderName} " + Inherit + "-" + LoaderVersion + " 的详细信息!"); + }) + { + ProgressWeight = 3d + }); + // 下载 Forgelike 主文件 + Loaders.Add(new ModLoader.LoaderTask>($"准备下载 {LoaderName}", Task => + { + // 启动依赖实例的下载 + if (ClientDownloadLoader is null) + { + if (IsCustomFolder) + throw new Exception("如果没有指定原版下载器,则不能指定 MC 安装文件夹"); + ClientDownloadLoader = + McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, Inherit); + } + + // 添加主文件下载 + var Files = new List(); + if (Info.ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge) + { + // NeoForge + var Neo = (ModDownload.DlNeoForgeListEntry)Info; + var Url = Neo.UrlBase + "-installer.jar"; + Files.Add(new ModNet.NetFile( + new[] { Url.Replace("maven.neoforged.net/releases", "bmclapi2.bangbang93.com/maven"), Url }, + InstallerAddress, new ModBase.FileChecker(64 * 1024))); + } + else if (Info.ForgeType == ModDownload.DlForgelikeEntry.ForgelikeType.Cleanroom) + { + // Cleanroom + var Clr = (ModDownload.DlCleanroomListEntry)Info; + var Url = Clr.UrlBase + "-installer.jar"; + Files.Add(new ModNet.NetFile(new[] { Url }, InstallerAddress, new ModBase.FileChecker(64 * 1024))); + } + else + { + // Forge + var Forge = (ModDownload.DlForgeVersionEntry)Info; + var FileName = + $"{Forge.Inherit.Replace("-", "_")}-{Forge.FileVersion}/forge-{Forge.Inherit.Replace("-", "_")}-{Forge.FileVersion}-{Forge.Category}.{Forge.FileExtension}"; + Files.Add(new ModNet.NetFile( + new[] + { + $"https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/{FileName}", + $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{FileName}" + }, InstallerAddress, new ModBase.FileChecker(64 * 1024, Hash: Forge.Hash))); + } + + Task.Output = Files; + }) + { + ProgressWeight = 0.5d, + Show = false + }); + Loaders.Add(new ModNet.LoaderDownload($"下载 {LoaderName} 主文件", new List()) + { ProgressWeight = 9d }); + + // 安装(仅在新版安装时需要原版 Jar) + if (ForgeType == "NeoForge" || Conversions.ToDouble(LoaderVersion.BeforeFirst(".")) >= 20d) + { + ModBase.Log($"[Download] 检测为{(ForgeType == "Forge" ? "新版 Forge" : " " + ForgeType)}:" + LoaderVersion); + List Libs = null; + Loaders.Add(new ModLoader.LoaderTask>($"分析 {LoaderName} 支持库文件", Task => + { + Task.Output = new List(); + ZipArchive Installer = null; + try + { + // 解压并获取、合并两个 Json 的信息 + Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); + Task.Progress = 0.2d; + var Json = (JObject)ModBase.GetJson( + ModBase.ReadFile(Installer.GetEntry("install_profile.json").Open())); + var Json2 = (JObject)ModBase.GetJson(ModBase.ReadFile(Installer.GetEntry("version.json").Open())); + Json.Merge(Json2); + // 如果是 1.16.5 就升级一下 Authlib + if (Conversions.ToBoolean(Inherit == "1.16.5" && (bool)Config.Download.FixAuthLib)) + Json = JObject.Parse(Json.ToString() + .Replace("2.1.28/authlib-2.1.28.jar", "2.3.31/authlib-2.3.31.jar") + .Replace("com.mojang:authlib:2.1.28", "com.mojang:authlib:2.3.31") + .Replace("ad54da276bf59983d02d5ed16fc14541354c71fd", + "bbd00ca33b052f73a6312254780fc580d2da3535").Replace("76328", "87662")); + // 获取 Lib 下载信息 + Libs = ModMinecraft.McLibListGetWithJson(Json, true); + // 添加 Mappings 下载信息 + if (Json["data"] is not null && Json["data"]["MOJMAPS"] is not null) + { + // 下载原版 Json 文件 + Task.Progress = 0.4d; + var RawJson = (JObject)ModBase.GetJson(ModNet.NetGetCodeByLoader( + ModDownload.DlSourceLauncherOrMetaGet( + Conversions.ToString(ModDownload.DlClientListGet(Inherit))), IsJson: true)); + // [net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg] + var OriginalName = Json["data"]["MOJMAPS"]["client"].ToString().Trim("[]".ToCharArray()) + .BeforeFirst("@"); + var Address = ModMinecraft.McLibGet(OriginalName).Replace(".jar", + "-mappings." + Json["data"]["MOJMAPS"]["client"].ToString().Trim("[]".ToCharArray()) + .Split("@")[1]); + var ClientMappings = RawJson["downloads"]["client_mappings"]; + Libs.Add(new ModMinecraft.McLibToken + { + IsNatives = false, + LocalPath = Address, + OriginalName = OriginalName, + Url = (string)ClientMappings["url"], + Size = (long)ClientMappings["size"], + SHA1 = (string)ClientMappings["sha1"] + }); + ModBase.Log( + $"[Download] 需要下载 Mappings:{ClientMappings["url"]} (SHA1: {ClientMappings["sha1"]})"); + } + + Task.Progress = 0.8d; + // 去除其中的原始 Forgelike 项 + for (int i = 0, loopTo = Libs.Count - 1; i <= loopTo; i++) + if (Libs[i].LocalPath.EndsWithF($"{LoaderName.ToLower()}-{Inherit}-{LoaderVersion}.jar") || + Libs[i].LocalPath.EndsWithF($"{LoaderName.ToLower()}-{Inherit}-{LoaderVersion}-client.jar")) + { + ModBase.Log($"[Download] 已从待下载 {LoaderName} 支持库中移除:" + Libs[i].LocalPath, + ModBase.LogLevel.Debug); + Libs.RemoveAt(i); + break; + } + + Task.Output = ModMinecraft.McLibNetFilesFromTokens(Libs); + } + catch (Exception ex) + { + throw new Exception($"获取{(ForgeType == "Forge" ? "新版 Forge" : " " + ForgeType)} 支持库列表失败", ex); + } + finally + { + // 释放文件 + if (Installer is not null) + Installer.Dispose(); + } + }) + { + ProgressWeight = 2d + }); + Loaders.Add(new ModNet.LoaderDownload($"下载 {LoaderName} 支持库文件", new List()) + { ProgressWeight = 12d }); + Loaders.Add(new ModLoader.LoaderTask, bool>($"获取 {LoaderName} 支持库文件", Task => + { + #region Forgelike 文件 + + if (IsCustomFolder) + foreach (var LibFile in Libs) + { + var RealPath = LibFile.LocalPath.Replace(ModMinecraft.McFolderSelected, McFolder); + if (!File.Exists(RealPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(RealPath)); + ModBase.CopyFile(LibFile.LocalPath, RealPath); + } + + if (ModBase.ModeDebug) + ModBase.Log($"[Download] 复制的 {LoaderName} 支持库文件:" + LibFile.LocalPath); + } + + #endregion + + #region 原版文件 + + // 等待原版文件下载完成 + if (ClientDownloadLoader is null) + return; + var TargetLoaders = ClientDownloadLoader.GetLoaderList() + .Where(l => (l.Name ?? "") == McDownloadClientLibName || (l.Name ?? "") == McDownloadClientJsonName) + .Where(l => l.State != ModBase.LoadState.Finished).ToList(); + if (TargetLoaders.Any()) + ModBase.Log($"[Download] {LoaderName} 安装正在等待原版文件下载完成"); + while (TargetLoaders.Any() && !Task.IsAborted) + { + TargetLoaders = TargetLoaders.Where(l => l.State != ModBase.LoadState.Finished).ToList(); + Thread.Sleep(50); + } + + if (Task.IsAborted) + return; + // 拷贝原版文件 + if (!IsCustomFolder) + return; + lock (VanillaSyncLock) + { + var ClientName = ModBase.GetFolderNameFromPath(ClientFolder); + Directory.CreateDirectory(McFolder + @"versions\" + Inherit); + if (!File.Exists(McFolder + @"versions\" + Inherit + @"\" + Inherit + ".json")) + ModBase.CopyFile(ClientFolder + ClientName + ".json", + McFolder + @"versions\" + Inherit + @"\" + Inherit + ".json"); + if (!File.Exists(McFolder + @"versions\" + Inherit + @"\" + Inherit + ".jar")) + ModBase.CopyFile(ClientFolder + ClientName + ".jar", + McFolder + @"versions\" + Inherit + @"\" + Inherit + ".jar"); + } + + #endregion + }) + { + ProgressWeight = 0.1d, + Show = false + }); + Loaders.Add(new ModLoader.LoaderTask( + ForgeType == "Forge" ? "安装 Forge(方式 A)" : "安装 " + ForgeType, Task => + { + var Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); + try + { + // 记录当前文件夹列表(在新建目标文件夹之前) + ModBase.Log("[Download] 开始进行 Forgelike 安装:" + InstallerAddress); + // 解压并获取信息 + var OldList = new DirectoryInfo(McFolder + "versions/") + .EnumerateDirectories().Select(i => i.FullName).ToList(); + + + // 新建目标实例文件夹 + var Json = ModBase.GetJson(ModBase.ReadFile(Installer.GetEntry("install_profile.json").Open())); + Directory.CreateDirectory(VersionFolder); + Task.Progress = 0.04d; + // 释放 launcher_installer.json + ModMinecraft.McFolderLauncherProfilesJsonCreate(McFolder); + Task.Progress = 0.05d; + // 运行 Forge 安装器 + var UseJavaWrapper = ModBase.IsUtf8CodePage(); + Retry: + + try + { + // 释放 Forge 注入器 + ModBase.WriteFile(ModBase.PathTemp + @"Cache\forge_installer.jar", + ModBase.GetResourceStream("Resources/forge-installer.jar")); + Task.Progress = 0.06d; + // 运行注入器 + ForgelikeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper, ForgeType); + Task.Progress = 0.97d; + } + catch (Exception ex) + { + if (!UseJavaWrapper) + { + ModBase.Log(ex, $"不使用 JavaWrapper 安装 {LoaderName} 失败,将使用 JavaWrapper 并重试"); + UseJavaWrapper = true; + goto Retry; + } + + throw new Exception($"运行 {LoaderName} 安装器失败", ex); + // 拷贝新增的实例 Json + } + + var DeltaList = new DirectoryInfo(McFolder + "versions/").EnumerateDirectories() + .SkipWhile(i => OldList.Contains(i.FullName)).ToList(); + + if (DeltaList.Count > 1) + // 它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151) + // 也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空 + DeltaList = DeltaList + .Where(l => l.Name.ContainsF("forge", true) && l.EnumerateFiles().Any()) + .ToList(); + // 如果没有新增文件夹,那么预测的文件夹名就是正确的 + // 如果只新增 1 个文件夹,那么拷贝 Json 文件 + if (DeltaList.Count == 1) + { + var JsonFile = DeltaList[0].EnumerateFiles().First(); + ModBase.WriteFile(VersionFolder + TargetVersion + ".json", + ModBase.ReadFile(JsonFile.FullName)); + ModBase.Log( + $"[Download] 已拷贝新增的实例 Json 文件:{JsonFile.FullName} -> {VersionFolder}{TargetVersion}.json"); + } + else if (DeltaList.Count > 1) + { + // 新增了多个文件夹 + //Enumerable.Select((IEnumerable)DeltaList, d => d.Name).Join(";") + ModBase.Log( + $"[Download] 有多个疑似的新增实例,无法确定:{string.Join(";", DeltaList.Select(d => d.Name))}"); + } + else + { + // 没有新增文件夹 + ModBase.Log("[Download] 未找到新增的实例文件夹"); + } + } + catch (Exception ex) + { + throw new Exception($"安装新 {LoaderName} 实例失败", ex); + } + finally + { + // 清理文件 + try + { + if (Installer is not null) + Installer.Dispose(); + if (File.Exists(InstallerAddress)) + File.Delete(InstallerAddress); + } + catch (Exception ex) + { + ModBase.Log(ex, $"安装 {LoaderName} 清理文件时出错"); + } + } + }) + { + ProgressWeight = 10d + }); + } + else + { + ModBase.Log("[Download] 检测为非新版 Forge:" + LoaderVersion); + Loaders.Add(new ModLoader.LoaderTask, bool>( + $"安装 {(ForgeType == "Forge" ? "Forge(方式 B)" : ForgeType)}", Task => + { + ZipArchive Installer = null; + try + { + // 解压并获取信息 + Installer = new ZipArchive(new FileStream(InstallerAddress, FileMode.Open)); + Task.Progress = 0.2d; + var Json = (JObject)ModBase.GetJson( + ModBase.ReadFile(Installer.GetEntry("install_profile.json").Open())); + Task.Progress = 0.4d; + // 新建实例文件夹 + Directory.CreateDirectory(VersionFolder); + Task.Progress = 0.5d; + if (Json["install"] is null) + { + // 中版:Legacy 方式 1 + ModBase.Log("[Download] 开始进行 Forge 安装,Legacy 方式 1:" + InstallerAddress); + // 建立 Json 文件 + var JsonVersion = (JObject)ModBase.GetJson( + ModBase.ReadFile(Installer.GetEntry(Json["json"].ToString().TrimStart('/')).Open())); + JsonVersion["id"] = TargetVersion; + ModBase.WriteFile(VersionFolder + TargetVersion + ".json", JsonVersion.ToString()); + Task.Progress = 0.6d; + // 解压支持库文件 + Installer.Dispose(); + ModBase.ExtractFile(InstallerAddress, InstallerAddress + @"_unrar\"); + ModBase.CopyDirectory(InstallerAddress + @"_unrar\maven\", McFolder + @"libraries\"); + ModBase.DeleteDirectory(InstallerAddress + @"_unrar\"); + } + else + { + // 旧版:Legacy 方式 2 + ModBase.Log("[Download] 开始进行 Forge 安装,Legacy 方式 2:" + InstallerAddress); + // 解压 Jar 文件 + var JarAddress = ModMinecraft.McLibGet((string)Json["install"]["path"], + customMcFolder: McFolder); + if (File.Exists(JarAddress)) + File.Delete(JarAddress); + ModBase.WriteFile(JarAddress, + Installer.GetEntry((string)Json["install"]["filePath"]).Open()); + Task.Progress = 0.9d; + // 建立 Json 文件 + Json["versionInfo"]["id"] = TargetVersion; + if (Json["versionInfo"]["inheritsFrom"] is null) + ((JObject)Json["versionInfo"]).Add("inheritsFrom", Inherit); + ModBase.WriteFile(VersionFolder + TargetVersion + ".json", Json["versionInfo"].ToString()); + } + } + catch (Exception ex) + { + throw new Exception("非新版方式安装 Forge 失败", ex); + } + finally + { + try + { + // 清理文件 + if (Installer is not null) + Installer.Dispose(); + if (File.Exists(InstallerAddress)) + File.Delete(InstallerAddress); + if (Directory.Exists(InstallerAddress + @"_unrar\")) + ModBase.DeleteDirectory(InstallerAddress + @"_unrar\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "非新版方式安装 Forge 清理文件时出错"); + } + } + }) + { + ProgressWeight = 1d + }); + } + + return Loaders; + } + + #endregion + + #region Forge 下载菜单 + + public static void ForgeDownloadListItemPreload(StackPanel Stack, List Entries, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 如果只有一个版本,则不特别列出 + if (Entries.Count == 1) + return; + // 获取推荐版本与最新版本 + ModDownload.DlForgeVersionEntry FreshVersion = null; + if (Entries.Any()) + FreshVersion = Entries[0]; + else + ModBase.Log("[System] 未找到可用的 Forge 版本", ModBase.LogLevel.Debug); + ModDownload.DlForgeVersionEntry RecommendedVersion = null; + foreach (var Entry in Entries) + if (Entry.IsRecommended) + RecommendedVersion = Entry; + // 若推荐版本与最新版本为同一版本,则仅显示推荐版本 + if (FreshVersion is not null && ReferenceEquals(FreshVersion, RecommendedVersion)) + FreshVersion = null; + // 显示各个版本 + if (RecommendedVersion is not null) + { + var Recommended = ForgeDownloadListItem(RecommendedVersion, OnClick, IsSaveOnly); + Recommended.Info = "推荐版" + (string.IsNullOrEmpty(Recommended.Info) ? "" : "," + Recommended.Info); + Stack.Children.Add(Recommended); + } + + if (FreshVersion is not null) + { + var Fresh = ForgeDownloadListItem(FreshVersion, OnClick, IsSaveOnly); + Fresh.Info = "最新版" + (string.IsNullOrEmpty(Fresh.Info) ? "" : "," + Fresh.Info); + Stack.Children.Add(Fresh); + } + + // 添加间隔 + Stack.Children.Add(new TextBlock + { + Text = "全部版本 (" + Entries.Count + ")", HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 13d, 0d, 4d) + }); + } + + public static MyListItem ForgeDownloadListItem(ModDownload.DlForgeVersionEntry Entry, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.VersionName, + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = new[] + { + string.IsNullOrEmpty(Entry.ReleaseTime) ? "" : "发布于 " + Entry.ReleaseTime, + ModBase.ModeDebug ? "种类:" + Entry.Category : "" + }.Where(d => !string.IsNullOrEmpty(d)).Join(","), + Logo = ModBase.PathImage + "Blocks/Anvil.png" + }; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = ForgeSaveContMenuBuild; + else + NewItem.ContentHandler = ForgeContMenuBuild; + // 结束 + return NewItem; + } + + private static void ForgeContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(BtnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnSave, 30d); + ToolTipService.SetHorizontalOffset(BtnSave, 2d); + BtnSave.Click += (ss, ee) => ForgeSave_Click(ss, (dynamic)ee); + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (ss, ee) => ForgeLog_Click(ss, (dynamic)ee); + sender.Buttons = new[] { BtnSave, BtnInfo }; + } + + private static void ForgeSaveContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (ss, ee) => ForgeLog_Click(ss, (dynamic)e); + sender.Buttons = new[] { BtnInfo }; + } + + private static void ForgeLog_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlForgeVersionEntry Version; + if (((dynamic)sender).Tag is not null) + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Tag; + else + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Parent.Tag; + ModBase.OpenWebsite( + $"https://files.minecraftforge.net/maven/net/minecraftforge/forge/{Version.Inherit}-{Version.VersionName}/forge-{Version.Inherit}-{Version.VersionName}-changelog.txt"); + } + + public static void ForgeSave_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlForgeVersionEntry Version; + if (((dynamic)sender).Tag is not null) + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Tag; + else + Version = (ModDownload.DlForgeVersionEntry)((dynamic)sender).Parent.Parent.Tag; + McDownloadForgelikeSave(Version); + } + + #endregion + + #region Forge 推荐版本获取 + + /// + /// 尝试刷新 Forge 推荐版本缓存。 + /// + public static void McDownloadForgeRecommendedRefresh() + { + if (IsForgeRecommendedRefreshed) + return; + IsForgeRecommendedRefreshed = true; + // 获取所有推荐版本列表 + // 内容为:"1.15.2":"31.2.0" + // 保存 + ModBase.RunInNewThread(() => + { + try + { + ModBase.Log("[Download] 刷新 Forge 推荐版本缓存开始"); + var Result = ModNet.NetGetCodeByLoader("https://bmclapi2.bangbang93.com/forge/promos"); + if (Result.Length < 1000) throw new Exception("获取的结果过短(" + Result + ")"); + var ResultJson = (JContainer)ModBase.GetJson(Result); + var RecommendedList = new List(); + foreach (JObject Version in ResultJson) + { + if (Version["name"] is null || Version["build"] is null) continue; + var Name = (string)Version["name"]; + if (!Name.EndsWithF("-recommended")) continue; + RecommendedList.Add("\"" + Name.Replace("-recommended", + "\":\"" + Version["build"]["version"] + "\"")); + } + + if (RecommendedList.Count < 5) throw new Exception("获取的推荐版本数过少(" + Result + ")"); + var CacheJson = "{" + RecommendedList.Join(",") + "}"; + ModBase.WriteFile(ModBase.PathTemp + @"Cache\ForgeRecommendedList.json", CacheJson); + ModBase.Log("[Download] 刷新 Forge 推荐版本缓存成功"); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新 Forge 推荐版本缓存失败"); + } + }, "ForgeRecommendedRefresh"); + } + + private static bool IsForgeRecommendedRefreshed; + + /// + /// 尝试获取某个 MC 版本对应的 Forge 推荐版本。如果不可用会返回 Nothing。 + /// + public static string McDownloadForgeRecommendedGet(string McInstance) + { + try + { + if (McInstance is null) + return null; + var List = ModBase.ReadFile(ModBase.PathTemp + @"Cache\ForgeRecommendedList.json"); + if (List is null || string.IsNullOrEmpty(List)) + { + ModBase.Log("[Download] 没有 Forge 推荐版本缓存文件"); + return null; + } + + var Json = (JObject)ModBase.GetJson(List); + if (Json is null || !(McInstance ?? "null").Contains(".") || !Json.ContainsKey(McInstance)) + return null; + return (Json[McInstance] ?? "").ToString(); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 Forge 推荐版本失败(" + (McInstance ?? "null") + ")", ModBase.LogLevel.Feedback); + return null; + } + } + + #endregion + + #region NeoForge 下载菜单 + + public static void NeoForgeDownloadListItemPreload(StackPanel Stack, List Entries, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 如果只有一个版本,则不特别列出 + if (Entries.Count == 1) + return; + // 获取最新稳定版和测试版 + ModDownload.DlNeoForgeListEntry FreshStableVersion = null; + ModDownload.DlNeoForgeListEntry FreshBetaVersion = null; + if (Entries.Any()) + foreach (var Entry in Entries.ToList()) + if (Entry.IsBeta) + { + if (FreshBetaVersion is null) + FreshBetaVersion = Entry; + } + else + { + FreshStableVersion = Entry; + break; + } + else + ModBase.Log("[System] 未找到可用的 NeoForge 版本", ModBase.LogLevel.Debug); + + // 显示各个版本 + if (FreshStableVersion is not null) + { + var Fresh = NeoForgeDownloadListItem(FreshStableVersion, OnClick, IsSaveOnly); + Fresh.Info = string.IsNullOrEmpty(Fresh.Info) ? "最新稳定版" : "最新" + Fresh.Info; + Stack.Children.Add(Fresh); + } + + if (FreshBetaVersion is not null) + { + var Fresh = NeoForgeDownloadListItem(FreshBetaVersion, OnClick, IsSaveOnly); + Fresh.Info = string.IsNullOrEmpty(Fresh.Info) ? "最新测试版" : "最新" + Fresh.Info; + Stack.Children.Add(Fresh); + } + + // 添加间隔 + Stack.Children.Add(new TextBlock + { + Text = "全部版本 (" + Entries.Count + ")", HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 13d, 0d, 4d) + }); + } + + public static MyListItem NeoForgeDownloadListItem(ModDownload.DlNeoForgeListEntry Info, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Info.VersionName, + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Info, + Info = Info.IsBeta ? "测试版" : "稳定版", + Logo = ModBase.PathImage + "Blocks/NeoForge.png" + }; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = NeoForgeSaveContMenuBuild; + else + NewItem.ContentHandler = NeoForgeContMenuBuild; + // 结束 + return NewItem; + } + + private static void NeoForgeContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(BtnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnSave, 30d); + ToolTipService.SetHorizontalOffset(BtnSave, 2d); + BtnSave.Click += (sender, e) => NeoForgeSave_Click(sender, (RoutedEventArgs)e); + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (sender, e) => NeoForgeLog_Click(sender, (RoutedEventArgs)e); + sender.Buttons = new[] { BtnSave, BtnInfo }; + } + + private static void NeoForgeSaveContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (sender, e) => NeoForgeLog_Click(sender, (RoutedEventArgs)e); + sender.Buttons = new[] { BtnInfo }; + } + + private static void NeoForgeLog_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlNeoForgeListEntry Info; + if (((dynamic)sender).Tag is not null) + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Tag; + else + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Parent.Tag; + ModBase.OpenWebsite(Info.UrlBase + "-changelog.txt"); + } + + public static void NeoForgeSave_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlNeoForgeListEntry Info; + if (((dynamic)sender).Tag is not null) + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Tag; + else + Info = (ModDownload.DlNeoForgeListEntry)((dynamic)sender).Parent.Parent.Tag; + McDownloadForgelikeSave(Info); + } + + #endregion + + #region Cleanroom 下载菜单 + + public static void CleanroomDownloadListItemPreload(StackPanel Stack, + List Entries, MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 获取最新稳定版和测试版 + // Dim FreshStableVersion As DlCleanroomListEntry = Nothing + ModDownload.DlCleanroomListEntry FreshBetaVersion = null; + if (Entries.Any()) + FreshBetaVersion = Entries[0]; + else + ModBase.Log("[System] 未找到可用的 Cleanroom 版本", ModBase.LogLevel.Debug); + // 显示各个版本 + // If FreshStableVersion IsNot Nothing Then + // Dim Fresh = NeoForgeDownloadListItem(FreshStableVersion, OnClick, IsSaveOnly) + // Fresh.Info = If(Fresh.Info = "", "最新稳定版", "最新" & Fresh.Info) + // Stack.Children.Add(Fresh) + // End If + if (FreshBetaVersion is not null) + { + var Fresh = CleanroomDownloadListItem(FreshBetaVersion, OnClick, IsSaveOnly); + Fresh.Info = string.IsNullOrEmpty(Fresh.Info) ? "最新测试版" : "最新" + Fresh.Info; + Stack.Children.Add(Fresh); + } + + // 添加间隔 + Stack.Children.Add(new TextBlock + { + Text = "全部版本 (" + Entries.Count + ")", HorizontalAlignment = HorizontalAlignment.Left, + Margin = new Thickness(6d, 13d, 0d, 4d) + }); + } + + public static MyListItem CleanroomDownloadListItem(ModDownload.DlCleanroomListEntry Info, + MyListItem.ClickEventHandler OnClick, bool IsSaveOnly) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Info.VersionName, + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Info, + Info = Info.IsBeta ? "测试版" : "稳定版", + Logo = ModBase.PathImage + "Blocks/Cleanroom.png" + }; + NewItem.Click += OnClick; + // 建立菜单 + if (IsSaveOnly) + NewItem.ContentHandler = CleanroomSaveContMenuBuild; + else + NewItem.ContentHandler = CleanroomContMenuBuild; + // 结束 + return NewItem; + } + + private static void CleanroomContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(BtnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnSave, 30d); + ToolTipService.SetHorizontalOffset(BtnSave, 2d); + BtnSave.Click += (sender, _e) => CleanroomSave_Click(sender, (RoutedEventArgs)e); + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (sender, e) => CleanroomLog_Click(sender, (RoutedEventArgs)e); + sender.Buttons = new[] { BtnSave, BtnInfo }; + } + + private static void CleanroomSaveContMenuBuild(MyListItem sender, EventArgs e) + { + var BtnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(BtnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnInfo, 30d); + ToolTipService.SetHorizontalOffset(BtnInfo, 2d); + BtnInfo.Click += (a, b) => CleanroomLog_Click(a, (dynamic)b); + sender.Buttons = new[] { BtnInfo }; + } + + private static void CleanroomLog_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlCleanroomListEntry Info; + if (((dynamic)sender).Tag is not null) + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Tag; + else + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Parent.Tag; + ModBase.OpenWebsite(Info.UrlBase + "-changelog.txt"); + } + + public static void CleanroomSave_Click(object sender, RoutedEventArgs e) + { + ModDownload.DlCleanroomListEntry Info; + if (((dynamic)sender).Tag is not null) + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Tag; + else + Info = (ModDownload.DlCleanroomListEntry)((dynamic)sender).Parent.Parent.Tag; + McDownloadForgelikeSave(Info); + } + + #endregion + + #region Fabric 下载 + + public static void McDownloadFabricLoaderSave(JObject DownloadInfo) + { + try + { + var Url = DownloadInfo["url"].ToString(); + var FileName = ModBase.GetFileNameFromPath(Url); + var Version = ModBase.GetFileNameFromPath(DownloadInfo["version"].ToString()); + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, "Fabric 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"Fabric {Version} 安装器下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + // BMCLAPI 不支持 Fabric Installer 下载 + var Address = new List(); + Address.Add(Url); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 64)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = new ModLoader.LoaderCombo("Fabric " + Version + " 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(DownloadInfo); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 Fabric 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 Fabric 实例的加载器列表。 + /// + private static List McDownloadFabricLoader(string FabricVersion, string MinecraftName, + string McFolder = null, bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = "fabric-loader-" + FabricVersion + "-" + MinecraftName; + var VersionFolder = McFolder + @"versions\" + Id + @"\"; + var Loaders = new List(); + + // 下载 Json + MinecraftName = MinecraftName.Replace("∞", "infinite"); // 放在 ID 后面避免影响实例文件夹名称 + Loaders.Add(new ModLoader.LoaderTask>("获取 Fabric 主文件下载地址", Task => + { + // 启动依赖实例的下载 + if (FixLibrary) + McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, MinecraftName); + Task.Progress = 0.5d; + // 构造文件请求 + Task.Output = new List + { + new( + new[] + { + "https://bmclapi2.bangbang93.com/fabric-meta/v2/versions/loader/" + MinecraftName + "/" + + FabricVersion + "/profile/json", + "https://meta.fabricmc.net/v2/versions/loader/" + MinecraftName + "/" + FabricVersion + + "/profile/json" + }, VersionFolder + Id + ".json", new ModBase.FileChecker(IsJson: true)) + }; + }) + { + ProgressWeight = 0.5d + }); + Loaders.Add(new ModNet.LoaderDownload("下载 Fabric 主文件", new List()) { ProgressWeight = 2.5d }); + + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 Fabric 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add( + new ModNet.LoaderDownload("下载 Fabric 支持库文件", new List()) { ProgressWeight = 8d }); + } + + return Loaders; + } + + #endregion + + #region LegacyFabric 下载 + + public static void McDownloadLegacyFabricLoaderSave(JObject DownloadInfo) + { + try + { + var Url = DownloadInfo["url"].ToString(); + var FileName = ModBase.GetFileNameFromPath(Url); + var Version = ModBase.GetFileNameFromPath(DownloadInfo["version"].ToString()); + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, "LegacyFabric 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ($"Legacy Fabric {Version} 安装器下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + var Address = new List(); + Address.Add(Url); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 64)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = new ModLoader.LoaderCombo("Legacy Fabric " + Version + " 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(DownloadInfo); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 Legacy Fabric 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 LegacyFabric 实例的加载器列表。 + /// + private static List McDownloadLegacyFabricLoader(string LegacyFabricVersion, + string MinecraftName, string McFolder = null, bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = "legacy-fabric-loader-" + LegacyFabricVersion + "-" + MinecraftName; + var VersionFolder = McFolder + @"versions\" + Id + @"\"; + var Loaders = new List(); + + // 下载 Json + Loaders.Add(new ModLoader.LoaderTask>("获取 Legacy Fabric 主文件下载地址", Task => + { + // 启动依赖实例的下载 + if (FixLibrary) + McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, MinecraftName); + Task.Progress = 0.5d; + // 构造文件请求 + Task.Output = new List + { + new( + new[] + { + "https://meta.legacyfabric.net/v2/versions/loader/" + MinecraftName + "/" + + LegacyFabricVersion + "/profile/json" + }, VersionFolder + Id + ".json", new ModBase.FileChecker(IsJson: true)) + }; + }) + { + ProgressWeight = 0.5d + }); + Loaders.Add(new ModNet.LoaderDownload("下载 Legacy Fabric 主文件", new List()) + { ProgressWeight = 2.5d }); + + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 Legacy Fabric 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add(new ModNet.LoaderDownload("下载 Legacy Fabric 支持库文件", new List()) + { ProgressWeight = 8d }); + } + + return Loaders; + } + + #endregion + + #region Fabric 下载菜单 + + public static MyListItem FabricDownloadListItem(JObject Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry["version"].ToString().Replace("+build", ""), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry["stable"].ToObject() ? "稳定版" : "测试版", + Logo = ModBase.PathImage + "Blocks/Fabric.png" + }; + NewItem.Click += OnClick; + NewItem.ContentHandler = FabricContMenuBuild; + // 结束 + return NewItem; + } + + private static void FabricContMenuBuild(object sender, EventArgs e) + { + var btnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnInfo, 30d); + ToolTipService.SetHorizontalOffset(btnInfo, 2d); + btnInfo.Click += (a, b) => FabricLog_Click(a, (dynamic)b); + ((dynamic)sender).Buttons = new[] { btnInfo }; + } + + private static void FabricLog_Click(object sender, RoutedEventArgs e) + { + ModBase.OpenWebsite("https://fabricmc.net/blog"); + } + + public static MyListItem FabricApiDownloadListItem(ModComp.CompFile Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.DisplayName.Split("]")[1].Replace("Fabric API ", "").Replace(" build ", ".").BeforeFirst("+") + .Trim(), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry.StatusDescription + ",发布于 " + Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"), + Logo = ModBase.PathImage + "Blocks/Fabric.png" + }; + NewItem.Click += OnClick; + // 结束 + return NewItem; + } + + public static MyListItem OptiFabricDownloadListItem(ModComp.CompFile Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.DisplayName.ToLower().Replace("optifabric-", "").Replace(".jar", "").Trim().TrimStart('v'), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry.StatusDescription + ",发布于 " + Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"), + Logo = ModBase.PathImage + "Blocks/OptiFabric.png" + }; + NewItem.Click += OnClick; + // 结束 + return NewItem; + } + + #endregion + + #region LegacyFabric 下载菜单 + + public static MyListItem LegacyFabricDownloadListItem(JObject Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry["version"].ToString(), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry["stable"].ToObject() ? "稳定版" : "测试版", + Logo = ModBase.PathImage + "Blocks/Fabric.png" + }; + NewItem.Click += OnClick; + // 结束 + return NewItem; + } + + public static MyListItem LegacyFabricApiDownloadListItem(ModComp.CompFile Entry, + MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.DisplayName.Replace("Legacy Fabric API ", ""), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry.StatusDescription + ",发布于 " + Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"), + Logo = ModBase.PathImage + "Blocks/Fabric.png" + }; + NewItem.Click += OnClick; + // 结束 + return NewItem; + } + + #endregion + + #region Quilt 下载 + + public static void McDownloadQuiltLoaderSave(JObject DownloadInfo) + { + try + { + var Url = DownloadInfo["url"].ToString(); + var FileName = ModBase.GetFileNameFromPath(Url); + var Version = ModBase.GetFileNameFromPath(DownloadInfo["version"].ToString()); + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, "Quilt 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar) + { + if ((OngoingLoader.Name ?? "") != ($"Quilt {Version} 安装器下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + // TODO: BMCLAPI 不支持 Quilt Installer 下载 + var Address = new List(); + Address.Add(Url); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 64)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = new ModLoader.LoaderCombo("Quilt " + Version + " 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(DownloadInfo); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始 Quilt 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 Quilt 实例的加载器列表。 + /// + private static List McDownloadQuiltLoader(string QuiltVersion, string MinecraftName, + string McFolder = null, bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = "quilt-loader-" + QuiltVersion + "-" + MinecraftName; + var VersionFolder = McFolder + @"versions\" + Id + @"\"; + var Loaders = new List(); + + // 下载 Json + MinecraftName = MinecraftName.Replace("∞", "infinite"); // 放在 ID 后面避免影响实例文件夹名称 + Loaders.Add(new ModLoader.LoaderTask>("获取 Quilt 主文件下载地址", Task => + { + // 启动依赖实例的下载 + if (FixLibrary) + McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, MinecraftName); + Task.Progress = 0.5d; + // 构造文件请求 + Task.Output = new List + { + new( + new[] + { + "https://meta.quiltmc.org/v3/versions/loader/" + MinecraftName + "/" + QuiltVersion + + "/profile/json" + }, VersionFolder + Id + ".json", new ModBase.FileChecker(IsJson: true)) + }; + // 新建 mods 文件夹 + Directory.CreateDirectory($@"{McFolder ?? ModMinecraft.McFolderSelected}mods\"); + }) + { + ProgressWeight = 0.5d + }); + Loaders.Add(new ModNet.LoaderDownload("下载 Quilt 主文件", new List()) { ProgressWeight = 2.5d }); + + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 Quilt 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add(new ModNet.LoaderDownload("下载 Quilt 支持库文件", new List()) + { ProgressWeight = 8d }); + } + + return Loaders; + } + + #endregion + + #region Quilt 下载菜单 + + public static MyListItem QuiltDownloadListItem(JObject Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry["version"].ToString(), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry["maven"].ToString().Contains("installer") ? "安装器" : + Entry["version"].ToString().Contains("beta") || Entry["version"].ToString().Contains("pre") ? "测试版" : + "稳定版", + Logo = ModBase.PathImage + "Blocks/Quilt.png" + }; + NewItem.Click += OnClick; + NewItem.ContentHandler = QuiltContMenuBuild; + // 结束 + return NewItem; + } + + private static void QuiltContMenuBuild(object sender, EventArgs e) + { + var btnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnInfo, 30d); + ToolTipService.SetHorizontalOffset(btnInfo, 2d); + btnInfo.Click += (a, b) => QuiltLog_Click(a, (dynamic)b); + ((dynamic)sender).Buttons = new[] { btnInfo }; + } + + private static void QuiltLog_Click(object sender, RoutedEventArgs e) + { + ModBase.OpenWebsite("https://quiltmc.org/en/blog/1/"); + } + + public static MyListItem QSLDownloadListItem(ModComp.CompFile Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry.DisplayName.Split("]")[1].Replace(" build ", ".").Split("+")[0].Trim(), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry.StatusDescription + ",发布于 " + Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"), + Logo = ModBase.PathImage + "Blocks/Quilt.png" + }; + NewItem.Click += OnClick; + // 结束 + return NewItem; + } + + #endregion + + #region LabyMod 下载 + + public static void McDownloadLabyModProductionLoaderSave() + { + try + { + var Url = "https://releases.labymod.net/api/v1/installer/production/java"; + var FileName = "LabyMod4ProductionInstaller.jar"; + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, "LabyMod 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ("LabyMod 安装器下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + var Address = new List(); + Address.Add(Url); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 64)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = new ModLoader.LoaderCombo("LabyMod 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "开始 LabyMod 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + public static void McDownloadLabyModSnapshotLoaderSave() + { + try + { + var Url = "https://releases.labymod.net/api/v1/installer/snapshot/java"; + var FileName = "LabyMod4SnapshotInstaller.jar"; + var Target = SystemDialogs.SelectSaveFile("选择保存位置", FileName, "LabyMod 安装器 (*.jar)|*.jar"); + if (!Target.Contains(@"\")) + return; + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar.ToList()) + { + if ((OngoingLoader.Name ?? "") != ("LabyMod 安装器下载" ?? "")) + continue; + ModMain.Hint("该实例正在下载中!", ModMain.HintType.Critical); + return; + } + + // 构造步骤加载器 + var Loaders = new List(); + // 下载 + var Address = new List(); + Address.Add(Url); + Loaders.Add(new ModNet.LoaderDownload("下载主文件", + new List { new(Address.ToArray(), Target, new ModBase.FileChecker(1024 * 64)) }) + { ProgressWeight = 15d }); + // 启动 + var Loader = new ModLoader.LoaderCombo("LabyMod 安装器下载", Loaders) + { OnStateChanged = LoaderStateChangedHintOnly }; + Loader.Start(); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "开始 LabyMod 安装器下载失败", ModBase.LogLevel.Feedback); + } + } + + /// + /// 获取下载某个 LabyMod 实例的加载器列表。 + /// + private static List McDownloadLabyModLoader(string LabyModCommitRef, string LabyModChannel, + string MinecraftName, string McFolder = null, bool FixLibrary = true) + { + // 参数初始化 + McFolder = McFolder ?? ModMinecraft.McFolderSelected; + var IsCustomFolder = (McFolder ?? "") != (ModMinecraft.McFolderSelected ?? ""); + var Id = "labymod-" + LabyModCommitRef + "-" + MinecraftName; + var VersionFolder = McFolder + @"versions\" + Id + @"\"; + var Loaders = new List(); + + // 下载 Json + MinecraftName = MinecraftName.Replace("∞", "infinite"); // 放在 ID 后面避免影响实例文件夹名称 + Loaders.Add(new ModLoader.LoaderTask>("获取 LabyMod 客户端文件下载地址", Task => + { + // 启动依赖实例的下载 + if (FixLibrary) + McDownloadClient(ModNet.NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, MinecraftName, + $"https://releases.r2.labymod.net/api/v1/download/manifest/labymod4/{LabyModChannel}/{MinecraftName}/{LabyModCommitRef}.json"); + Task.Progress = 0.5d; + // 构造文件请求 + Task.Output = new List + { + new( + new[] + { + $"https://releases.r2.labymod.net/api/v1/download/manifest/labymod4/{LabyModChannel}/{MinecraftName}/{LabyModCommitRef}.json" + }, VersionFolder + Id + ".json", new ModBase.FileChecker(IsJson: true)) + }; + Task.Progress = 1d; + }) + { + ProgressWeight = 2d + }); + Loaders.Add(new ModNet.LoaderDownload("下载 LabyMod 客户端 Json 文件", new List()) + { ProgressWeight = 10d }); + // 下载支持库 + if (FixLibrary) + { + Loaders.Add(new ModLoader.LoaderTask>("分析 LabyMod 支持库文件", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder))) + { ProgressWeight = 1d, Show = false }); + Loaders.Add(new ModNet.LoaderDownload("下载 LabyMod 支持库文件", new List()) + { ProgressWeight = 8d }); + } + + return Loaders; + } + + /// + /// 获取下载某个 Minecraft 实例的加载器列表。 + /// 它必须安装到 PathMcFolder,但是可以自定义实例名(不过自定义的实例名不会修改 Json 中的 id 项)。 + /// + private static List McDownloadLabyModClientLoader(string Id, string LabyChannel, + string LabyCommitRef, string VersionName = null) + { + VersionName = VersionName ?? Id; + var VersionFolder = ModMinecraft.McFolderSelected + @"versions\" + VersionName + @"\"; + + var Loaders = new List(); + + // 下载支持库文件 + var LoadersLib = new List(); + LoadersLib.Add(new ModLoader.LoaderTask>("分析原版与 LabyMod 支持库文件(副加载器)", Task => + { + Thread.Sleep(50); // 等待 JSON 文件实际写入硬盘(#3710) + ModBase.Log("[Download] 开始分析原版与 LabyMod 支持库文件:" + VersionFolder); + Task.Output = ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(VersionFolder)); + }) + { + ProgressWeight = 1d, + Show = false + }); + LoadersLib.Add(new ModNet.LoaderDownload("下载原版与 LabyMod 支持库文件(副加载器)", new List()) + { ProgressWeight = 13d, Show = false }); + Loaders.Add(new ModLoader.LoaderCombo(McDownloadClientLibName, LoadersLib) + { Block = false, ProgressWeight = 14d }); + + // 下载资源文件 + var LoadersAssets = new List(); + LoadersAssets.Add(new ModLoader.LoaderTask>("分析资源文件索引地址(副加载器)", Task => + { + try + { + var Version = new ModMinecraft.McInstance(VersionFolder); + Task.Output = new List { ModDownload.DlClientAssetIndexGet(Version) }; + } + catch (Exception ex) + { + throw new Exception("分析资源文件索引地址失败", ex); + } + + // 顺手添加 Json 项目 + try + { + var VersionJson = (JObject)ModBase.GetJson(ModBase.ReadFile(VersionFolder + VersionName + ".json")); + VersionJson.Add("clientVersion", Id); + ModBase.WriteFile(VersionFolder + VersionName + ".json", VersionJson.ToString()); + } + catch (Exception ex) + { + throw new Exception("添加客户端版本失败", ex); + } + }) + { + ProgressWeight = 1d, + Show = false + }); + LoadersAssets.Add(new ModNet.LoaderDownload("下载资源文件索引(副加载器)", new List()) + { ProgressWeight = 3d, Show = false }); + LoadersAssets.Add(new ModLoader.LoaderTask>("分析所需资源文件(副加载器)", Task => + { + ModLoader.LoaderBase argprogressFeed = Task; + Task.Output = + ModMinecraft.McAssetsFixList(new ModMinecraft.McInstance(VersionFolder), true, ref argprogressFeed); + Task = (ModLoader.LoaderTask>)argprogressFeed; + }) + { + ProgressWeight = 3d, + Show = false + }); + LoadersAssets.Add(new ModNet.LoaderDownload("下载资源文件(副加载器)", new List()) + { ProgressWeight = 14d, Show = false }); + Loaders.Add( + new ModLoader.LoaderCombo("下载原版资源文件", LoadersAssets) { Block = false, ProgressWeight = 21d }); + + return Loaders; + } + + #endregion + + #region LabyMod 下载菜单 + + public static MyListItem LabyModDownloadListItem(JObject Entry, MyListItem.ClickEventHandler OnClick) + { + // 建立控件 + var NewItem = new MyListItem + { + Title = Entry["version"] + (Entry["channel"].ToString().Contains("snapshot") ? " 快照版" : " 稳定版"), + SnapsToDevicePixels = true, + Height = 42d, + Type = MyListItem.CheckType.Clickable, + Tag = Entry, + Info = Entry["channel"].ToString().Contains("snapshot") ? "快照版" : "稳定版", + Logo = ModBase.PathImage + "Blocks/LabyMod.png" + }; + NewItem.Click += OnClick; + NewItem.ContentHandler = LabyModContMenuBuild; + // 结束 + return NewItem; + } + + private static void LabyModContMenuBuild(object sender, EventArgs e) + { + var btnSave = new MyIconButton { Logo = ModBase.Logo.IconButtonSave, ToolTip = "另存为" }; + ToolTipService.SetPlacement(btnSave, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnSave, 30d); + ToolTipService.SetHorizontalOffset(btnSave, 2d); + btnSave.Click += (a, b) => LabyModSave_Click(a, (dynamic)b); + var btnInfo = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更新日志" }; + ToolTipService.SetPlacement(btnInfo, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnInfo, 30d); + ToolTipService.SetHorizontalOffset(btnInfo, 2d); + btnInfo.Click += (a, b) => LabyModLog_Click(a, (dynamic)b); + ((dynamic)sender).Buttons = new[] { btnSave, btnInfo }; + } + + private static void LabyModLog_Click(object sender, RoutedEventArgs e) + { + ModBase.OpenWebsite("https://www.labymod.net/zh_Hans/download"); + } + + private static void LabyModSave_Click(object sender, RoutedEventArgs e) + { + JObject version; + if (((dynamic)sender).Tag is not null) + version = (JObject)((dynamic)sender).Tag; + else if (((dynamic)sender).Parent.Tag is not null) + version = (JObject)((dynamic)sender).Parent.Tag; + else + version = (JObject)((dynamic)sender).Parent.Parent.Tag; + if ((string)version["channel"] == "snapshot") + McDownloadLabyModSnapshotLoaderSave(); + else + McDownloadLabyModProductionLoaderSave(); + } + + #endregion + + #region 合并安装 + + /// + /// 安装请求。 + /// + public class McInstallRequest + { + /// + /// 欲下载的 Cleanroom。 + /// + public ModDownload.DlCleanroomListEntry CleanroomEntry = null; + + // 若要下载 Cleanroom,则需要在下面两项中完成至少一项 + /// + /// 欲下载的 Cleanroom 版本名。 + /// + public string CleanroomVersion; + + /// + /// 欲下载的 Fabric API 信息。 + /// + public ModComp.CompFile FabricApi = null; + + /// + /// 欲下载的 Fabric Loader 版本名。 + /// + public string FabricVersion = null; + + /// + /// 欲下载的 Forge。 + /// + public ModDownload.DlForgeVersionEntry ForgeEntry = null; + + // 若要下载 Forge,则需要在下面两项中完成至少一项 + /// + /// 欲下载的 Forge 版本名。接受例如 36.1.4 / 14.23.5.2859 / 1.19-41.1.0 的输入。 + /// + public string ForgeVersion; + + /// + /// 欲下载的 LabyMod 通道。 + /// + public string LabyModChannel = null; + + /// + /// 欲下载的 LabyMod 版本。 + /// + public string LabyModCommitRef = null; + + /// + /// 欲下载的 Legacy Fabric API 信息。 + /// + public ModComp.CompFile LegacyFabricApi = null; + + /// + /// 欲下载的 Legacy Fabric Loader 版本名。 + /// + public string LegacyFabricVersion = null; + + /// + /// 欲下载的 LiteLoader 详细信息。 + /// + public ModDownload.DlLiteLoaderListEntry LiteLoaderEntry = null; + + /// + /// 可选。欲下载的 Minecraft Json 地址。 + /// + public string MinecraftJson = null; + + /// + /// 必填。欲下载的 Minecraft 的版本名。 + /// + public string MinecraftName = null; + + /// + /// 若 MMC 整合包安装包含特殊参数,则填写此项。 + /// + public ModModpack.MMCPackInfo MMCPackInfo = null; + + /// + /// 欲下载的 NeoForge。 + /// + public ModDownload.DlNeoForgeListEntry NeoForgeEntry = null; + + // 若要下载 NeoForge,则需要在下面两项中完成至少一项 + /// + /// 欲下载的 NeoForge 版本名。 + /// + public string NeoForgeVersion; + + /// + /// 欲下载的 OptiFabric 信息。 + /// + public ModComp.CompFile OptiFabric = null; + + /// + /// 欲下载的 OptiFine 详细信息。 + /// + public ModDownload.DlOptiFineListEntry OptiFineEntry; + + // 若要下载 OptiFine,则需要在下面两项中完成至少一项 + /// + /// 欲下载的 OptiFine 版本名。例如 HD_U_F6_pre1。 + /// + public string OptiFineVersion; + + /// + /// 欲下载的 Quilted Fabric API (QFAPI) / Quilt Standard Libraries (QSL) 信息。 + /// + public ModComp.CompFile QSL = null; + + /// + /// 欲下载的 Quilt Loader 版本名。 + /// + public string QuiltVersion = null; + + /// + /// 必填。安装目标文件夹。 + /// + public string TargetInstanceFolder; + + /// + /// 必填。安装目标实例名称。 + /// + public string TargetInstanceName; + } + + /// + /// 在加载器状态改变后显示一条提示。 + /// 不会进行任何其他操作。 + /// + public static void LoaderStateChangedHintOnly(object Loader) + { + switch (((dynamic)Loader).State) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, ModBase.LoadState.Finished, false): + { + ModMain.Hint(Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Name, "成功!")), + ModMain.HintType.Finish); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, ModBase.LoadState.Failed, false): + { + ModMain.Hint( + Conversions.ToString(Operators.ConcatenateObject( + Operators.ConcatenateObject(((dynamic)Loader).Name, "失败:"), ((dynamic)Loader).Error.Message)), + ModMain.HintType.Critical); + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, ModBase.LoadState.Aborted, false): + { + ModMain.Hint(Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Name, "已取消!")), + ModMain.HintType.Info); + break; + } + } + } + + /// + /// 安装加载器状态改变后进行提示和重载文件夹列表的方法。 + /// + public static void McInstallState(object Loader) + { + switch (((dynamic)Loader).State) + { + case var @case when Operators.ConditionalCompareObjectEqual(@case, ModBase.LoadState.Finished, false): + { + if (Conversions.ToBoolean(Config.Download.AutoSelectInstance)) + { + string VersionName = ((dynamic)Loader).Name.ToString(); + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "Version", + VersionName.Remove(VersionName.Length - 3, 3)); + } + + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "InstanceCache", + ""); // 清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存) + ModBase.DeleteDirectory( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"PCLInstallBackups\"))); + ModMain.Hint(Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Name, "成功!")), + ModMain.HintType.Finish); + break; + } + case var case1 when Operators.ConditionalCompareObjectEqual(case1, ModBase.LoadState.Failed, false): + { + ModMain.Hint( + Conversions.ToString(Operators.ConcatenateObject( + Operators.ConcatenateObject(((dynamic)Loader).Name, "失败:"), ((dynamic)Loader).Error.Message)), + ModMain.HintType.Critical); + break; + } + case var case2 when Operators.ConditionalCompareObjectEqual(case2, ModBase.LoadState.Aborted, false): + { + ModMain.Hint(Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Name, "已取消!")), + ModMain.HintType.Info); + break; + } + case var case3 when Operators.ConditionalCompareObjectEqual(case3, ModBase.LoadState.Loading, false): + { + return; // 不重新加载实例列表 + } + } + + if (Conversions.ToBoolean( + !Operators.ConditionalCompareObjectEqual(((dynamic)Loader).State, ModBase.LoadState.Finished, false) && + Directory.Exists( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, + @"PCLInstallBackups\"))))) // 实例修改失败回滚 + { + ModBase.CopyDirectory( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"PCLInstallBackups\")), + Conversions.ToString(((dynamic)Loader).Input)); + File.Delete(Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, ".pclignore"))); + ModBase.DeleteDirectory( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"PCLInstallBackups\"))); + } + else + { + McInstallFailedClearFolder(Loader); + } + + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + + public static void McInstallFailedClearFolder(object Loader) + { + try + { + Thread.Sleep(1000); // 防止存在尚未完全释放的文件,导致清理失败(例如整合包安装) + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(((dynamic)Loader).State, ModBase.LoadState.Failed, + false)) || Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(((dynamic)Loader).State, ModBase.LoadState.Aborted, false))) + { + // 删除实例文件夹 + if (Directory.Exists( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"saves\"))) || + Directory.Exists( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"versions\"))) || + Directory.Exists( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, @"mods\"))) || + File.Exists( + Conversions.ToString(Operators.ConcatenateObject(((dynamic)Loader).Input, "server.dat")))) + { + ModBase.Log( + Conversions.ToString(Operators.ConcatenateObject("[Download] 由于实例已被独立启动,不清理实例文件夹:", + ((dynamic)Loader).Input)), ModBase.LogLevel.Developer); + } + else + { + ModBase.Log( + Conversions.ToString(Operators.ConcatenateObject("[Download] 由于下载失败或取消,清理实例文件夹:", + ((dynamic)Loader).Input)), ModBase.LogLevel.Developer); + ModBase.DeleteDirectory(Conversions.ToString(((dynamic)Loader).Input)); + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "下载失败或取消后清理实例文件夹失败"); + } + } + + /// + /// 进行合并安装。返回是否已经开始安装(例如如果没有安装 Java 则会进行提示并返回 False) + /// + public static bool McInstall(McInstallRequest Request, string Type = "安装") + { + try + { + var SubLoaders = McInstallLoader(Request, IgnoreDump: Type != "安装"); + if (SubLoaders is null) + return false; + var Loader = new ModLoader.LoaderCombo(Request.TargetInstanceName + " " + Type, SubLoaders) + { OnStateChanged = McInstallState }; + + // 启动 + Loader.Start(Request.TargetInstanceFolder); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + return true; + } + + catch (ModBase.CancelledException ex) + { + return false; + } + catch (Exception ex) + { + ModBase.Log(ex, "开始合并安装失败", ModBase.LogLevel.Feedback); + return false; + } + } + + /// + /// 获取合并安装加载器列表,并进行前期的缓存清理与 Java 检查工作。 + /// + /// + public static List McInstallLoader(McInstallRequest Request, bool DontFixLibraries = false, + bool IgnoreDump = false) + { + // 获取缓存目录(安装 Mod 加载器的文件夹不能包含空格) + var TempMcFolder = ModMain.RequestTaskTempFolder(Request.OptiFineEntry is not null || + Request.ForgeEntry is not null || + Request.NeoForgeEntry is not null); + + // 获取参数 + var InstanceFolder = ModMinecraft.McFolderSelected + @"versions\" + Request.TargetInstanceName + @"\"; + if (Directory.Exists(TempMcFolder)) + ModBase.DeleteDirectory(TempMcFolder); + string OptiFineFolder = null; + if (Request.OptiFineVersion is not null) + { + if (Request.OptiFineVersion.Contains("_HD_U_")) + Request.OptiFineVersion = "HD_U_" + Request.OptiFineVersion.AfterLast("_HD_U_"); // #735 + Request.OptiFineEntry = new ModDownload.DlOptiFineListEntry + { + DisplayName = Request.MinecraftName + " " + Request.OptiFineVersion.Replace("HD_U_", "") + .Replace("_", "").Replace("pre", " pre"), + Inherit = Request.MinecraftName, + IsPreview = Request.OptiFineVersion.ContainsF("pre", true), + NameVersion = Request.MinecraftName + "-OptiFine_" + Request.OptiFineVersion, + NameFile = (Request.OptiFineVersion.ContainsF("pre", true) ? "preview_" : "") + "OptiFine_" + + Request.MinecraftName + "_" + Request.OptiFineVersion + ".jar" + }; + } + + if (Request.OptiFineEntry is not null) + OptiFineFolder = TempMcFolder + @"versions\" + Request.OptiFineEntry.NameVersion; + string ForgeFolder = null; + if (Request.ForgeEntry is not null) + Request.ForgeVersion = Request.ForgeVersion ?? Request.ForgeEntry.VersionName; + if (Request.ForgeVersion is not null) + ForgeFolder = TempMcFolder + @"versions\forge-" + Request.ForgeVersion; + string NeoForgeFolder = null; + if (Request.NeoForgeEntry is not null) + Request.NeoForgeVersion = Request.NeoForgeVersion ?? Request.NeoForgeEntry.VersionName; + if (Request.NeoForgeVersion is not null) + NeoForgeFolder = TempMcFolder + @"versions\neoforge-" + Request.NeoForgeVersion; + string CleanroomFolder = null; + if (Request.CleanroomEntry is not null) + Request.CleanroomVersion = Request.CleanroomVersion ?? Request.CleanroomEntry.VersionName; + if (Request.CleanroomVersion is not null) + CleanroomFolder = TempMcFolder + @"versions\cleanroom-" + Request.CleanroomVersion; + string FabricFolder = null; + if (Request.FabricVersion is not null) + FabricFolder = TempMcFolder + @"versions\fabric-loader-" + Request.FabricVersion + "-" + + Request.MinecraftName; + string LegacyFabricFolder = null; + if (Request.LegacyFabricVersion is not null) + LegacyFabricFolder = TempMcFolder + @"versions\legacy-fabric-loader-" + Request.LegacyFabricVersion + "-" + + Request.MinecraftName; + string QuiltFolder = null; + if (Request.QuiltVersion is not null) + QuiltFolder = TempMcFolder + @"versions\quilt-loader-" + Request.QuiltVersion + "-" + Request.MinecraftName; + string LabyModFolder = null; + if (Request.LabyModCommitRef is not null) + LabyModFolder = TempMcFolder + @"versions\labymod-" + Request.LabyModCommitRef + "-" + + Request.MinecraftName; + string LiteLoaderFolder = null; + if (Request.LiteLoaderEntry is not null) + LiteLoaderFolder = TempMcFolder + @"versions\" + Request.MinecraftName + "-LiteLoader"; + + // 判断 OptiFine 是否作为 Mod 进行下载 + var Modable = Request.FabricVersion is not null || Request.LegacyFabricVersion is not null || + Request.ForgeEntry is not null || Request.NeoForgeEntry is not null || + Request.LiteLoaderEntry is not null; + var ModsTempFolder = TempMcFolder + @"mods\"; + var OptiFineAsMod = Request.OptiFineEntry is not null && Modable; // 选择了 OptiFine 与任意 Mod 加载器 + if (OptiFineAsMod) + { + ModBase.Log("[Download] OptiFine 将作为 Mod 进行下载"); + OptiFineFolder = ModsTempFolder; + } + + // 记录日志 + if (OptiFineFolder is not null) + ModBase.Log("[Download] OptiFine 缓存:" + OptiFineFolder); + if (ForgeFolder is not null) + ModBase.Log("[Download] Forge 缓存:" + ForgeFolder); + if (NeoForgeFolder is not null) + ModBase.Log("[Download] NeoForge 缓存:" + NeoForgeFolder); + if (CleanroomFolder is not null) + ModBase.Log("[Download] Cleanroom 缓存:" + CleanroomFolder); + if (FabricFolder is not null) + ModBase.Log("[Download] Fabric 缓存:" + FabricFolder); + if (LegacyFabricFolder is not null) + ModBase.Log("[Download] LegacyFabric 缓存:" + LegacyFabricFolder); + if (QuiltFolder is not null) + ModBase.Log("[Download] Quilt 缓存:" + QuiltFolder); + if (LabyModFolder is not null) + ModBase.Log("[Download] LabyMod 缓存:" + LabyModFolder); + if (LiteLoaderFolder is not null) + ModBase.Log("[Download] LiteLoader 缓存:" + LiteLoaderFolder); + ModBase.Log("[Download] 对应的原版版本:" + Request.MinecraftName); + + // 重复实例检查 + if (File.Exists(InstanceFolder + Request.TargetInstanceName + ".json") && !IgnoreDump) + { + ModMain.Hint("实例 " + Request.TargetInstanceName + " 已经存在!", ModMain.HintType.Critical); + throw new ModBase.CancelledException(); + } + + var LoaderList = new List(); + // 添加忽略标识 + LoaderList.Add(new ModLoader.LoaderTask("添加忽略标识", + _ => ModBase.WriteFile(InstanceFolder + ".pclignore", "用于临时地在 PCL 的实例列表中屏蔽此实例。")) + { Show = false, Block = false }); + // Fabric API + if (Request.FabricApi is not null) + LoaderList.Add(new ModNet.LoaderDownload("下载 Fabric API", + new List { Request.FabricApi.ToNetFile(ModsTempFolder) }) + { ProgressWeight = 3d, Block = false }); + // LegacyFabric API + if (Request.LegacyFabricApi is not null) + LoaderList.Add(new ModNet.LoaderDownload("下载 Legacy Fabric API", + new List { Request.LegacyFabricApi.ToNetFile(ModsTempFolder) }) + { ProgressWeight = 3d, Block = false }); + // Quilted Fabric API (QFAPI) / Quilt Standard Libraries (QSL) + if (Request.QSL is not null) + LoaderList.Add( + new ModNet.LoaderDownload("下载 QFAPI / QSL", + new List { Request.QSL.ToNetFile(ModsTempFolder) }) + { ProgressWeight = 3d, Block = false }); + // OptiFabric + if (Request.OptiFabric is not null) + LoaderList.Add(new ModNet.LoaderDownload("下载 OptiFabric", + new List { Request.OptiFabric.ToNetFile(ModsTempFolder) }) + { ProgressWeight = 3d, Block = false }); + // LabyMod + if (Request.LabyModCommitRef is not null) + { + LoaderList.Add(new ModLoader.LoaderCombo("下载 LabyMod " + Request.LabyModCommitRef, + McDownloadLabyModLoader(Request.LabyModCommitRef, Request.LabyModChannel, Request.MinecraftName, + TempMcFolder, false)) { Show = false, ProgressWeight = 10d, Block = true }); + goto LabyModSkip; + } + + // 原版 + var ClientLoader = new ModLoader.LoaderCombo("下载原版 " + Request.MinecraftName, + McDownloadClientLoader(Request.MinecraftName, Request.MinecraftJson, Request.TargetInstanceName)) + { + Show = false, + ProgressWeight = 39d, + Block = Request.ForgeVersion is null && Request.NeoForgeVersion is null && Request.OptiFineEntry is null && + Request.FabricVersion is null && Request.LiteLoaderEntry is null && Request.QuiltVersion is null && + Request.CleanroomEntry is null && Request.LegacyFabricVersion is null + }; + LoaderList.Add(ClientLoader); + // OptiFine + if (Request.OptiFineEntry is not null) + { + if (OptiFineAsMod) + LoaderList.Add(new ModLoader.LoaderCombo("下载 OptiFine " + Request.OptiFineEntry.DisplayName, + McDownloadOptiFineSaveLoader(Request.OptiFineEntry, + OptiFineFolder + Request.OptiFineEntry.NameFile)) + { + Show = false, + ProgressWeight = 16d, + Block = Request.ForgeVersion is null && Request.NeoForgeVersion is null && + Request.FabricVersion is null && Request.LiteLoaderEntry is null + }); + else + LoaderList.Add(new ModLoader.LoaderCombo("下载 OptiFine " + Request.OptiFineEntry.DisplayName, + McDownloadOptiFineLoader(Request.OptiFineEntry, TempMcFolder, ClientLoader, + Request.TargetInstanceFolder, false)) + { + Show = false, + ProgressWeight = 24d, + Block = Request.ForgeVersion is null && Request.NeoForgeVersion is null && + Request.FabricVersion is null && Request.LiteLoaderEntry is null + }); + } + + // Forge + if (Request.ForgeVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 Forge " + Request.ForgeVersion, + McDownloadForgelikeLoader("Forge", Request.ForgeVersion, "forge-" + Request.ForgeVersion, + Request.MinecraftName, Request.ForgeEntry, TempMcFolder, ClientLoader, + Request.TargetInstanceFolder)) + { + Show = false, ProgressWeight = 25d, + Block = Request.FabricVersion is null && Request.LiteLoaderEntry is null && + Request.NeoForgeEntry is null + }); + // NeoForge + if (Request.NeoForgeVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 NeoForge " + Request.NeoForgeVersion, + McDownloadForgelikeLoader("NeoForge", Request.NeoForgeVersion, "neoforge-" + Request.NeoForgeVersion, + Request.MinecraftName, Request.NeoForgeEntry, TempMcFolder, ClientLoader, + Request.TargetInstanceFolder)) + { + Show = false, ProgressWeight = 25d, + Block = Request.ForgeEntry is null && Request.FabricVersion is null && Request.LiteLoaderEntry is null + }); + // Cleanroom + if (Request.CleanroomVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 Cleanroom " + Request.CleanroomVersion, + McDownloadForgelikeLoader("Cleanroom", Request.CleanroomVersion, + "cleanroom-" + Request.CleanroomVersion, Request.MinecraftName, Request.CleanroomEntry, + TempMcFolder, ClientLoader, Request.TargetInstanceFolder)) + { + Show = false, ProgressWeight = 25d, + Block = Request.ForgeEntry is null && Request.FabricVersion is null && Request.LiteLoaderEntry is null + }); + // LiteLoader + if (Request.LiteLoaderEntry is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 LiteLoader " + Request.MinecraftName, + McDownloadLiteLoaderLoader(Request.LiteLoaderEntry, TempMcFolder, ClientLoader, false)) + { + Show = false, + ProgressWeight = 1d, + Block = Request.FabricVersion is null + }); + // Fabric + if (Request.FabricVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 Fabric " + Request.FabricVersion, + McDownloadFabricLoader(Request.FabricVersion, Request.MinecraftName, TempMcFolder, false)) + { + Show = false, + ProgressWeight = 2d, + Block = true + }); + // LegacyFabric + if (Request.LegacyFabricVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 Legacy Fabric " + Request.LegacyFabricVersion, + McDownloadLegacyFabricLoader(Request.LegacyFabricVersion, Request.MinecraftName, TempMcFolder, false)) + { + Show = false, + ProgressWeight = 2d, + Block = true + }); + // Quilt + if (Request.QuiltVersion is not null) + LoaderList.Add(new ModLoader.LoaderCombo("下载 Quilt " + Request.QuiltVersion, + McDownloadQuiltLoader(Request.QuiltVersion, Request.MinecraftName, TempMcFolder, false)) + { Show = false, ProgressWeight = 2d, Block = true }); + + LabyModSkip: ; + + // 合并安装 + LoaderList.Add(new ModLoader.LoaderTask("安装游戏", Task => + { + // 合并 JSON + MergeJson(InstanceFolder, InstanceFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, + NeoForgeFolder, Request.NeoForgeVersion, CleanroomFolder, Request.CleanroomVersion, FabricFolder, + QuiltFolder, LabyModFolder, Request.LabyModChannel, LiteLoaderFolder, Request.MMCPackInfo, + LegacyFabricFolder); + Task.Progress = 0.2d; + // 迁移文件 + if (Directory.Exists(TempMcFolder + "libraries")) + ModBase.CopyDirectory(TempMcFolder + "libraries", ModMinecraft.McFolderSelected + "libraries"); + Task.Progress = 0.8d; + // 创建 Mod 和资源包文件夹 + var ModsFolder = new ModMinecraft.McInstance(InstanceFolder).PathIndie + @"mods\"; // 版本隔离信息在此时被决定 + if (Directory.Exists(ModsTempFolder)) + { + ModBase.CopyDirectory(ModsTempFolder, ModsFolder); + } + else if (Modable) + { + Directory.CreateDirectory(ModsFolder); + ModBase.Log("[Download] 自动创建 Mod 文件夹:" + ModsFolder); + } + + var ResourcepacksFolder = new ModMinecraft.McInstance(InstanceFolder).PathIndie + @"resourcepacks\"; + Directory.CreateDirectory(ResourcepacksFolder); + ModBase.Log("[Download] 自动创建资源包文件夹:" + ResourcepacksFolder); + }) + { + ProgressWeight = 2d, + Block = true + }); + // 补全文件 + if (!DontFixLibraries && (Request.OptiFineEntry is not null || + (Request.ForgeVersion is not null && + Conversions.ToDouble(Request.ForgeVersion.BeforeFirst(".")) >= 20d) || + Request.NeoForgeVersion is not null || Request.FabricVersion is not null || + Request.QuiltVersion is not null || Request.CleanroomVersion is not null || + Request.LiteLoaderEntry is not null || Request.LabyModCommitRef is not null)) + { + var LoadersLib = new List(); + if (Request.LabyModCommitRef is not null) + { + var LabyModClientLoader = new ModLoader.LoaderCombo("下载原版 " + Request.MinecraftName, + McDownloadLabyModClientLoader(Request.MinecraftName, Request.LabyModChannel, + Request.LabyModCommitRef, Request.TargetInstanceName)) + { Show = false, ProgressWeight = 39d, Block = false }; + LoaderList.Add(LabyModClientLoader); + } + else + { + LoadersLib.Add(new ModLoader.LoaderTask>("分析游戏支持库文件(副加载器)", + Task => Task.Output = + ModMinecraft.McLibNetFilesFromInstance(new ModMinecraft.McInstance(InstanceFolder))) + { ProgressWeight = 1d, Show = false }); + LoadersLib.Add(new ModNet.LoaderDownload("下载游戏支持库文件(副加载器)", new List()) + { ProgressWeight = 7d, Show = false }); + LoaderList.Add(new ModLoader.LoaderCombo("下载游戏支持库文件", LoadersLib) { ProgressWeight = 8d }); + } + } + + // 删除忽略标识 + LoaderList.Add(new ModLoader.LoaderTask("删除忽略标识", _ => File.Delete(InstanceFolder + ".pclignore")) + { Show = false }); + // 总加载器 + return LoaderList; + } + + /// + /// 将多个实例 JSON 进行合并,如果目标已存在则直接覆盖。失败会抛出异常。 + /// + private static void MergeJson(string OutputFolder, string MinecraftFolder, string OptiFineFolder = null, + bool OptiFineAsMod = false, string ForgeFolder = null, string ForgeVersion = null, string NeoForgeFolder = null, + string NeoForgeVersion = null, string CleanroomFolder = null, string CleanroomVersion = null, + string FabricFolder = null, string QuiltFolder = null, string LabyModFolder = null, + string LabyModChannel = null, string LiteLoaderFolder = null, ModModpack.MMCPackInfo MMCPackInfo = null, + string LegacyFabricFolder = null) + { + ModBase.Log("[Download] 开始进行实例合并,输出:" + OutputFolder + ",Minecraft:" + MinecraftFolder + + (OptiFineFolder is not null ? ",OptiFine:" + OptiFineFolder : "") + + (ForgeFolder is not null ? ",Forge:" + ForgeFolder : "") + + (NeoForgeFolder is not null ? ",NeoForge:" + NeoForgeFolder : "") + + (CleanroomFolder is not null ? ",Cleanroom:" + CleanroomFolder : "") + + (LiteLoaderFolder is not null ? ",LiteLoader:" + LiteLoaderFolder : "") + + (FabricFolder is not null ? ",Fabric:" + FabricFolder : "") + + (LegacyFabricFolder is not null ? ",LegacyFabric:" + LegacyFabricFolder : "") + + (QuiltFolder is not null ? ",Quilt:" + QuiltFolder : "") + + (LabyModFolder is not null ? ",LabyMod:" + LabyModFolder : "")); + Directory.CreateDirectory(OutputFolder); + + var HasOptiFine = OptiFineFolder is not null && !OptiFineAsMod; + var HasForge = ForgeFolder is not null; + var HasLegacyFabric = LegacyFabricFolder is not null; + var HasNeoForge = NeoForgeFolder is not null; + var HasCleanroom = CleanroomFolder is not null; + var HasLiteLoader = LiteLoaderFolder is not null; + var HasFabric = FabricFolder is not null; + var HasQuilt = QuiltFolder is not null; + var HasLabyMod = LabyModFolder is not null; + string OutputName; + string MinecraftName; + string OptiFineName; + string ForgeName; + string NeoForgeName; + string CleanroomName; + string LiteLoaderName; + string FabricName; + string LegacyFabricName; + string QuiltName; + string LabyModName; + string OutputJsonPath; + string MinecraftJsonPath; + string OptiFineJsonPath = null; + string ForgeJsonPath = null; + string NeoForgeJsonPath = null; + string CleanroomJsonPath = null; + string LiteLoaderJsonPath = null; + string FabricJsonPath = null; + string QuiltJsonPath = null; + string LabyModJsonPath = null; + string LegacyFabricJsonPath = null; + string OutputJar; + string MinecraftJar; + + #region 初始化路径信息 + + if (!OutputFolder.EndsWithF(@"\")) + OutputFolder += @"\"; + OutputName = ModBase.GetFolderNameFromPath(OutputFolder); + OutputJsonPath = OutputFolder + OutputName + ".json"; + OutputJar = OutputFolder + OutputName + ".jar"; + + if (!MinecraftFolder.EndsWithF(@"\")) + MinecraftFolder += @"\"; + MinecraftName = ModBase.GetFolderNameFromPath(MinecraftFolder); + MinecraftJsonPath = MinecraftFolder + MinecraftName + ".json"; + MinecraftJar = MinecraftFolder + MinecraftName + ".jar"; + + if (HasOptiFine) + { + if (!OptiFineFolder.EndsWithF(@"\")) + OptiFineFolder += @"\"; + OptiFineName = ModBase.GetFolderNameFromPath(OptiFineFolder); + OptiFineJsonPath = OptiFineFolder + OptiFineName + ".json"; + } + + if (HasForge) + { + if (!ForgeFolder.EndsWithF(@"\")) + ForgeFolder += @"\"; + ForgeName = ModBase.GetFolderNameFromPath(ForgeFolder); + ForgeJsonPath = ForgeFolder + ForgeName + ".json"; + } + + if (HasNeoForge) + { + if (!NeoForgeFolder.EndsWithF(@"\")) + NeoForgeFolder += @"\"; + NeoForgeName = ModBase.GetFolderNameFromPath(NeoForgeFolder); + NeoForgeJsonPath = NeoForgeFolder + NeoForgeName + ".json"; + } + + if (HasCleanroom) + { + if (!CleanroomFolder.EndsWithF(@"\")) + CleanroomFolder += @"\"; + CleanroomName = ModBase.GetFolderNameFromPath(CleanroomFolder); + CleanroomJsonPath = CleanroomFolder + CleanroomName + ".json"; + } + + if (HasLiteLoader) + { + if (!LiteLoaderFolder.EndsWithF(@"\")) + LiteLoaderFolder += @"\"; + LiteLoaderName = ModBase.GetFolderNameFromPath(LiteLoaderFolder); + LiteLoaderJsonPath = LiteLoaderFolder + LiteLoaderName + ".json"; + } + + if (HasFabric) + { + if (!FabricFolder.EndsWithF(@"\")) + FabricFolder += @"\"; + FabricName = ModBase.GetFolderNameFromPath(FabricFolder); + FabricJsonPath = FabricFolder + FabricName + ".json"; + } + + if (HasLegacyFabric) + { + if (!LegacyFabricFolder.EndsWithF(@"\")) + LegacyFabricFolder += @"\"; + LegacyFabricName = ModBase.GetFolderNameFromPath(LegacyFabricFolder); + LegacyFabricJsonPath = LegacyFabricFolder + LegacyFabricName + ".json"; + } + + if (HasQuilt) + { + if (!QuiltFolder.EndsWithF(@"\")) + QuiltFolder += @"\"; + QuiltName = ModBase.GetFolderNameFromPath(QuiltFolder); + QuiltJsonPath = QuiltFolder + QuiltName + ".json"; + } + + if (HasLabyMod) + { + if (!LabyModFolder.EndsWithF(@"\")) + LabyModFolder += @"\"; + LabyModName = ModBase.GetFolderNameFromPath(LabyModFolder); + LabyModJsonPath = LabyModFolder + LabyModName + ".json"; + } + + #endregion + + JObject OutputJson; + JObject MinecraftJson = null; + JObject OptiFineJson = null; + JObject ForgeJson = null; + JObject NeoForgeJson = null; + JObject LegacyFabricJson = null; + JObject CleanroomJson = null; + JObject LiteLoaderJson = null; + JObject FabricJson = null; + JObject QuiltJson = null; + JObject LabyModJson = null; + + #region 读取文件并检查文件是否合规 + + var MinecraftJsonText = ModBase.ReadFile(MinecraftJsonPath); + if (!HasLabyMod) + { + if (!MinecraftJsonText.StartsWithF("{")) + throw new Exception("Minecraft Json 有误,地址:" + MinecraftJsonPath + ",前段内容:" + + MinecraftJsonText.Substring(0, Math.Min(MinecraftJsonText.Length, 1000))); + MinecraftJson = (JObject)ModBase.GetJson(MinecraftJsonText); + } + + if (HasOptiFine) + { + var OptiFineJsonText = ModBase.ReadFile(OptiFineJsonPath); + if (!OptiFineJsonText.StartsWithF("{")) + throw new Exception("OptiFine Json 有误,地址:" + OptiFineJsonPath + ",前段内容:" + + OptiFineJsonText.Substring(0, Math.Min(OptiFineJsonText.Length, 1000))); + OptiFineJson = (JObject)ModBase.GetJson(OptiFineJsonText); + } + + if (HasForge) + { + var ForgeJsonText = ModBase.ReadFile(ForgeJsonPath); + if (!ForgeJsonText.StartsWithF("{")) + throw new Exception("Forge Json 有误,地址:" + ForgeJsonPath + ",前段内容:" + + ForgeJsonText.Substring(0, Math.Min(ForgeJsonText.Length, 1000))); + ForgeJson = (JObject)ModBase.GetJson(ForgeJsonText); + } + + if (HasNeoForge) + { + var NeoForgeJsonText = ModBase.ReadFile(NeoForgeJsonPath); + if (!NeoForgeJsonText.StartsWithF("{")) + throw new Exception("NeoForge Json 有误,地址:" + NeoForgeJsonPath + ",前段内容:" + + NeoForgeJsonText.Substring(0, Math.Min(NeoForgeJsonText.Length, 1000))); + NeoForgeJson = (JObject)ModBase.GetJson(NeoForgeJsonText); + } + + if (HasCleanroom) + { + var CleanroomJsonText = ModBase.ReadFile(CleanroomJsonPath); + if (!CleanroomJsonText.StartsWithF("{")) + throw new Exception("Cleanroom Json 有误,地址:" + CleanroomJsonPath + ",前段内容:" + + CleanroomJsonText.Substring(0, Math.Min(CleanroomJsonText.Length, 1000))); + CleanroomJson = (JObject)ModBase.GetJson(CleanroomJsonText); + } + + if (HasLiteLoader) + { + var LiteLoaderJsonText = ModBase.ReadFile(LiteLoaderJsonPath); + if (!LiteLoaderJsonText.StartsWithF("{")) + throw new Exception("LiteLoader Json 有误,地址:" + LiteLoaderJsonPath + ",前段内容:" + + LiteLoaderJsonText.Substring(0, Math.Min(LiteLoaderJsonText.Length, 1000))); + LiteLoaderJson = (JObject)ModBase.GetJson(LiteLoaderJsonText); + } + + if (HasFabric) + { + var FabricJsonText = ModBase.ReadFile(FabricJsonPath); + if (!FabricJsonText.StartsWithF("{")) + throw new Exception("Fabric Json 有误,地址:" + FabricJsonPath + ",前段内容:" + + FabricJsonText.Substring(0, Math.Min(FabricJsonText.Length, 1000))); + FabricJson = (JObject)ModBase.GetJson(FabricJsonText); + } + + if (HasLegacyFabric) + { + var LegacyFabricJsonText = ModBase.ReadFile(LegacyFabricJsonPath); + if (!LegacyFabricJsonText.StartsWithF("{")) + throw new Exception("Legacy Fabric Json 有误,地址:" + FabricJsonPath + ",前段内容:" + + LegacyFabricJsonText.Substring(0, Math.Min(LegacyFabricJsonText.Length, 1000))); + LegacyFabricJson = (JObject)ModBase.GetJson(LegacyFabricJsonText); + } + + if (HasQuilt) + { + var QuiltJsonText = ModBase.ReadFile(QuiltJsonPath); + if (!QuiltJsonText.StartsWithF("{")) + throw new Exception("Quilt Json 有误,地址:" + QuiltJsonPath + ",前段内容:" + + QuiltJsonText.Substring(0, Math.Min(QuiltJsonText.Length, 1000))); + QuiltJson = (JObject)ModBase.GetJson(QuiltJsonText); + } + + if (HasLabyMod) + { + var LabyModJsonText = ModBase.ReadFile(LabyModJsonPath); + if (!LabyModJsonText.StartsWithF("{")) + throw new Exception("LabyMod Json 有误,地址:" + LabyModJsonPath + ",前段内容:" + + LabyModJsonText.Substring(0, Math.Min(LabyModJsonText.Length, 1000))); + LabyModJson = (JObject)ModBase.GetJson(LabyModJsonText); + } + + #endregion + + #region 处理 JSON 文件 + + // 获取 minecraftArguments + var AllArguments = (MinecraftJson is not null ? (MinecraftJson["minecraftArguments"] ?? " ").ToString() : " ") + + " " + + (LabyModJson is not null ? (LabyModJson["minecraftArguments"] ?? " ").ToString() : " ") + + " " + + (OptiFineJson is not null ? (OptiFineJson["minecraftArguments"] ?? " ").ToString() : " ") + + " " + (ForgeJson is not null ? (ForgeJson["minecraftArguments"] ?? " ").ToString() : " ") + + " " + + (NeoForgeJson is not null ? (NeoForgeJson["minecraftArguments"] ?? " ").ToString() : " ") + + " " + (LiteLoaderJson is not null + ? (LiteLoaderJson["minecraftArguments"] ?? " ").ToString() + : " ") + " " + (CleanroomJson is not null + ? (CleanroomJson["minecraftArguments"] ?? " ").ToString() + : " "); + // 分割参数字符串 + var RawArguments = AllArguments.Split(" ").Where(l => !string.IsNullOrEmpty(l)).Select(l => l.Trim()).ToList(); + var SplitArguments = new List(); + for (int i = 0, loopTo = RawArguments.Count - 1; i <= loopTo; i++) + if (RawArguments[i].StartsWithF("-")) + SplitArguments.Add(RawArguments[i]); + else if (SplitArguments.Any() && SplitArguments.Last().StartsWithF("-") && + !SplitArguments.Last().Contains(" ")) + SplitArguments[SplitArguments.Count - 1] = SplitArguments.Last() + " " + RawArguments[i]; + else + SplitArguments.Add(RawArguments[i]); + + var RealArguments = SplitArguments.Distinct().ToList().Join(" "); + // 合并 + // 相关讨论见 #2801 + if (MMCPackInfo is not null) + { + if (MMCPackInfo.IsMinecraftOverrided) + { + ModBase.Log("[Download] 当前实例的 MC 核心已被修改,使用对应的 MMC 整合包参数"); + OutputJson = MMCPackInfo.OverridedJson; + } + else + { + ModBase.Log("[Download] 存在无修改 MC 核心文件的 MMC 整合包信息,应用相关参数"); + OutputJson = MinecraftJson; + // 合并来自 MultiMC 的 JSON + OutputJson.Merge(MMCPackInfo.OverridedJson); + } + } + else + { + OutputJson = MinecraftJson; + } + + if (HasOptiFine) + { + // 合并 OptiFine + OptiFineJson.Remove("releaseTime"); + OptiFineJson.Remove("time"); + OutputJson.Merge(OptiFineJson); + } + + if (HasForge) + if (MMCPackInfo is null || !MMCPackInfo.IsForgeOverrided) + { + // 合并 Forge + ForgeJson.Remove("releaseTime"); + ForgeJson.Remove("time"); + OutputJson.Merge(ForgeJson); + } + + if (HasNeoForge) + if (MMCPackInfo is null || !MMCPackInfo.IsNeoForgeOverrided) + { + // 合并 NeoForge + NeoForgeJson.Remove("releaseTime"); + NeoForgeJson.Remove("time"); + OutputJson.Merge(NeoForgeJson); + } + + if (HasCleanroom) + if (MMCPackInfo is null || !MMCPackInfo.IsCleanroomOverrided) + { + // 合并 Cleanroom + CleanroomJson.Remove("releaseTime"); + CleanroomJson.Remove("time"); + OutputJson.Merge(CleanroomJson); + } + + if (HasLiteLoader) + { + // 合并 LiteLoader + LiteLoaderJson.Remove("releaseTime"); + LiteLoaderJson.Remove("time"); + OutputJson.Merge(LiteLoaderJson); + } + + if (HasFabric) + if (MMCPackInfo is null || !MMCPackInfo.IsFabricOverrided) + { + // 合并 Fabric + FabricJson.Remove("releaseTime"); + FabricJson.Remove("time"); + OutputJson.Merge(FabricJson); + } + + if (HasLegacyFabric) + if (MMCPackInfo is null || !MMCPackInfo.IsFabricOverrided) + { + // 合并 Fabric + LegacyFabricJson.Remove("releaseTime"); + LegacyFabricJson.Remove("time"); + OutputJson.Merge(LegacyFabricJson); + } + + if (HasQuilt) + if (MMCPackInfo is null || !MMCPackInfo.IsQuiltOverrided) + { + // 合并 Quilt + QuiltJson.Remove("releaseTime"); + QuiltJson.Remove("time"); + OutputJson.Merge(QuiltJson); + } + + if (HasLabyMod) + { + // 合并 LabyMod + LabyModJson.Remove("releaseTime"); + LabyModJson.Remove("time"); + if (OutputJson is null) + OutputJson = new JObject(); + OutputJson.Merge(LabyModJson); + + var LabyModLib = + (JObject)ModNet.NetGetCodeByRequestRetry( + $"https://releases.r2.labymod.net/api/v1/libraries/{LabyModChannel}.json", IsJson: true); + var LabyModCore = (JObject)ModNet.NetGetCodeByRequestRetry( + $"https://releases.r2.labymod.net/api/v1/manifest/{LabyModChannel}/latest.json", IsJson: true); + var OutputLibraries = new JArray(); + var IsolatedLibraries = new Dictionary(); + var MinecraftVersion = LabyModJson["_minecraftVersion"]; + + foreach (var Library in LabyModLib["isolated_libraries"]) + if (((JArray)Library["versions"]).Contains(MinecraftVersion)) + IsolatedLibraries.Add(Library["name"].ToString(), true); + + foreach (var Library in LabyModJson["libraries"]) + { + var RegexMatchResult = Library["name"].ToString().RegexSeek(RegexPatterns.CatchLwjglInLib); + if (RegexMatchResult is null || + !IsolatedLibraries.Contains(new KeyValuePair(RegexMatchResult, true))) + OutputLibraries.Add(Library); + } + + foreach (var Library in LabyModLib["libraries"]) + OutputLibraries.Add(JObject.Parse($@"{{ + ""name"": ""{Library["name"]}"", + ""downloads"": {{ + ""artifact"": {{ + ""path"": ""{Library["url"].ToString().Substring(Library["url"].ToString().LastIndexOfF("https://releases.r2.labymod.net/libraries/") + 42)}"", + ""sha1"": ""{Library["sha1"]}"", + ""size"": {Library["size"]}, + ""url"": ""{Library["url"]}"" + }} + }} + }}")); + + OutputLibraries.Add(JObject.Parse($@"{{ + ""name"": ""net.labymod:LabyMod:4"", + ""downloads"": {{ + ""artifact"": {{ + ""path"": ""net/labymod/LabyMod/4/LabyMod-4.jar"", + ""sha1"": ""{LabyModCore["sha1"]}"", + ""size"": {LabyModCore["size"]}, + ""url"": ""https://releases.r2.labymod.net/api/v1/download/labymod4/{LabyModChannel}/{LabyModCore["commitReference"]}.jar"" + }} + }} + }}")); + OutputJson["libraries"] = OutputLibraries; + OutputJson.Add("labymod_data", JObject.Parse($@"{{ + ""channelType"": ""{LabyModChannel}"", + ""commitReference"": ""{LabyModCore["commitReference"]}"", + ""version"": ""{LabyModCore["labyModVersion"]}"", + ""versionType"": ""release"" + }}")); + } + + // 修改 + if (RealArguments is not null && !string.IsNullOrEmpty(RealArguments.Replace(" ", ""))) + OutputJson["minecraftArguments"] = RealArguments; + if (MMCPackInfo is not null && MMCPackInfo.IsMcArgsEdited) + OutputJson.Remove("minecraftArguments"); + OutputJson.Remove("_comment_"); + OutputJson.Remove("inheritsFrom"); + OutputJson.Remove("jar"); + OutputJson["id"] = OutputName; + + #endregion + + #region 保存 + + ModBase.WriteFile(OutputJsonPath, OutputJson.ToString()); + if ((MinecraftJar ?? "") != (OutputJar ?? "")) // 可能是同一个文件 + { + if (File.Exists(OutputJar)) + File.Delete(OutputJar); + ModBase.CopyFile(MinecraftJar, OutputJar); + } + + ModBase.Log("[Download] 实例合并 " + OutputName + " 完成"); + + #endregion + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml index 63e8f7540..a505f6eff 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml @@ -1,26 +1,32 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml.cs new file mode 100644 index 000000000..6848c4e6c --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCleanroom.xaml.cs @@ -0,0 +1,75 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace PCL; + +public partial class PageDownloadCleanroom +{ + public PageDownloadCleanroom() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanMain, CardTip, ModDownload.DlCleanroomListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + // 归类 + var Dict = ModDownload.DlCleanroomListLoader.Output.Value.GroupBy(d => d.Inherit) + .OrderByDescending(g => g.Key).ToDictionary(g => g.Key, g => g.ToList()); + // 清空当前 + PanMain.Children.Clear(); + // 转化为 UI + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + NewCard.IsSwapped = true; + NewCard.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.CleanroomDownloadListItem( + (ModDownload.DlCleanroomListEntry)item, ModDownloadLib.CleanroomSave_Click, true)); + }; + PanMain.Children.Add(NewCard); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Cleanroom 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 介绍栏 + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://cleanroommc.com/zh/"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadClient.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadClient.xaml index 5c2aff08b..ba8c677d0 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadClient.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadClient.xaml @@ -1,11 +1,13 @@  - + - + @@ -48,32 +52,44 @@ - - - + + + + HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="25,25,25,0" UseAnimation="False" + HasMouseAnimation="False"> - + + LogoScale="1" + Logo="M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z m0 0" /> + LogoScale="1" + Logo="M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z" /> + LogoScale="1" + Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" /> + LogoScale="0.8" + Logo="M867.648 951.296 512 595.648l-355.648 355.648c-11.52 11.52-30.272 11.52-41.856 0L72.64 909.44c-11.52-11.52-11.52-30.272 0-41.856L428.352 512 72.64 156.352c-11.52-11.52-11.52-30.272 0-41.856l41.856-41.856c11.52-11.52 30.272-11.52 41.856 0L512 428.288l355.648-355.648c11.52-11.52 30.272-11.52 41.856 0l41.856 41.856c11.52 11.52 11.52 30.272 0 41.856L595.648 512l355.648 355.648c11.52 11.52 11.52 30.272 0 41.856l-41.856 41.856C897.984 962.88 879.168 962.88 867.648 951.296L867.648 951.296z" /> - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.cs new file mode 100644 index 000000000..ef4bccb05 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.cs @@ -0,0 +1,918 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageDownloadCompFavorites +{ + private readonly List CompItemList = new(); + private List SelectedItemList = new(); + + public PageDownloadCompFavorites() + { + Loader = new ModLoader.LoaderTask, List>("CompProject Favorites", + CompFavoritesGet, LoaderInput); + Initialized += PageDownloadCompFavorites_Inited; + Loaded += PageDownloadCompFavorites_Loaded; + KeyDown += Page_KeyDown; + InitializeComponent(); + { + // 这是选择收藏夹旁边那个图标按钮 + // 实在不想把布局写动态代码里,但是奈何龙猫的石山没办法在 XAML 里定义 Logo 属性为已有常量值 + // 还有一个很扯淡的点,同样自定义的 MyButton 能在 XAML 直接设置 Click 事件 + // 到 MyIconButton 就不行了,死活跑不了,也不知道是不是漏了什么依赖属性没写 + Btn_ManageTargetFav.Logo = ModBase.Logo.IconButtonSetup; + Btn_ManageTargetFav.Click += Manage_Click; + } + // Handles + Load.StateChanged += Load_State; + Btn_FavoritesCancel.Click += Btn_FavoritesCancel_Clicked; + Btn_SelectCancel.Click += Btn_SelectCancel_Clicked; + Btn_FavoritesShare.Click += Btn_FavoritesShare_Clicked; + Btn_FavoritesDownload.Click += Btn_FavoritesDownload_Clicked; + ComboTargetFav.SelectionChanged += ComboTargetFav_Selected; + HintGetFail.MouseLeftButtonDown += HintGetFail_MouseLeftButtonDown; + PanSearchBox.TextChanged += SearchRun; + } + + private ModComp.CompFavorites.FavData CurrentFavTarget + { + get + { + var SelectedItem = (MyComboBoxItem)ComboTargetFav.SelectedItem; + if (SelectedItem is null) + { + ModBase.Log("[Favorites] 异常:未选择收藏夹"); + SelectedItem = (MyComboBoxItem)ComboTargetFav.Items.GetItemAt(0); + } + + return ModComp.CompFavorites.FavoritesList + .Where(e => Operators.ConditionalCompareObjectEqual(e.Id, SelectedItem.Tag, false)).First(); + } + } + + #region 加载器信息 + + // 加载器信息 + public ModLoader.LoaderTask, List> Loader; + + private void PageDownloadCompFavorites_Inited(object sender, EventArgs e) + { + RefreshFavTargets(); + PageLoaderInit(Load, PanLoad, PanContent, null, Loader, _ => Load_OnFinish(), LoaderInput); + } + + private void PageDownloadCompFavorites_Loaded(object sender, EventArgs e) + { + Items_SetSelectAll(false); + RefreshBar(); + if (Loader.Input is not null && !Loader.Input.Count.Equals(CurrentFavTarget.Favs.Count)) RefreshFavTargets(); + } + + private List LoaderInput() + { + List TargetList = null; + try + { + TargetList = CurrentFavTarget.Favs.Distinct().ToList(); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Favorites] 加载收藏夹列表时出错"); + } + + return (List)TargetList.Clone(); // 复制而不是直接引用! + } + + private void CompFavoritesGet(ModLoader.LoaderTask, List> Task) + { + Task.Output = ModComp.CompRequest.GetCompProjectsByIds(Task.Input); + } + + #endregion + + #region UI 化 - 自适应卡片 + + public class CompListItemContainer // 用来存储自动依据类型生成的卡片及其相关信息 + { + public MyCard Card { get; set; } + public StackPanel ContentList { get; set; } + public string Title { get; set; } + public int CompType { get; set; } + } + + private readonly List ItemList = new(); + + /// + /// 刷新收藏夹列表 + /// + private void RefreshFavTargets() + { + ComboTargetFav.Items.Clear(); + foreach (var Target in ModComp.CompFavorites.FavoritesList) + { + var Item = new MyComboBoxItem + { + Content = Target.Name, + Tag = Target.Id + }; + ComboTargetFav.Items.Add(Item); + } + + if (ComboTargetFav.SelectedIndex == -1) ComboTargetFav.SelectedIndex = 0; // 默认选择第一个 + } + + /// + /// 返回适合当前工程项目的卡片记录 + /// + /// 工程项目类型 + /// + private CompListItemContainer GetSuitListContainer(int Type) + { + if (ItemList.Any(e => e.CompType.Equals(Type))) return ItemList.First(e => e.CompType.Equals(Type)); + + var NewItem = new CompListItemContainer + { + Card = new MyCard + { + CanSwap = true, + Margin = new Thickness(0d, 0d, 0d, 15d) + }, + ContentList = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(12d, 38d, 12d, 12d) + }, + CompType = Type + }; + switch (Type) + { + case -1: + { + NewItem.Title = "搜索结果 ({0})"; // 搜索结果 + break; + } + case (int)ModComp.CompType.Mod: + { + NewItem.Title = "Mod ({0})"; + break; + } + case (int)ModComp.CompType.ModPack: + { + NewItem.Title = "整合包 ({0})"; + break; + } + case (int)ModComp.CompType.ResourcePack: + { + NewItem.Title = "资源包 ({0})"; + break; + } + case (int)ModComp.CompType.Shader: + { + NewItem.Title = "光影包 ({0})"; + break; + } + case (int)ModComp.CompType.DataPack: + { + NewItem.Title = "数据包 ({0})"; + break; + } + case (int)ModComp.CompType.Plugin: + { + NewItem.Title = "插件 ({0})"; + break; + } + case (int)ModComp.CompType.World: + { + NewItem.Title = "世界 ({0})"; + break; + } + + default: + { + NewItem.Title = "未分类类型 ({0})"; + break; + } + } + + NewItem.Card.Title = string.Format(NewItem.Title, 0); + NewItem.Card.Children.Add(NewItem.ContentList); + ItemList.Add(NewItem); + return NewItem; + } + + private void RefreshContent() + { + foreach (var item in ItemList) // 清除逻辑父子关系 + item.ContentList.Children.Clear(); + PanContentList.Children.Clear(); + var DataSource = IsSearching ? SearchResult : CompItemList; + foreach (var item in DataSource) + GetSuitListContainer(IsSearching ? -1 : (int)((ModComp.CompProject)item.Tag).Type).ContentList.Children + .Add(item); + foreach (var item in ItemList) + { + if (item.ContentList.Children.Count == 0) + continue; + PanContentList.Children.Add(item.Card); + } + } + + private void RefreshCardTitle() + { + foreach (var item in ItemList) + item.Card.Title = string.Format(item.Title, + CompItemList.Where(e => (int)((ModComp.CompProject)e.Tag).Type == item.CompType).Count()); + if (!ItemList.Any(e => e.CompType.Equals(-1))) + return; + var SearchItem = ItemList.First(e => e.CompType.Equals(-1)); + if (SearchItem is not null) SearchItem.Card.Title = string.Format(SearchItem.Title, SearchResult.Count); + } + + #endregion + + #region UI 化 - 加载主逻辑 + + // 结果 UI 化 + private void Load_OnFinish() + { + ItemList.Clear(); + try + { + AllowSearch = false; + PanSearchBox.Text = string.Empty; + AllowSearch = true; + CompItemList.Clear(); + var SomeGetFail = Loader.Input.Count != Loader.Output.Count; + HintGetFail.Visibility = SomeGetFail ? Visibility.Visible : Visibility.Collapsed; + foreach (var item in Loader.Output) + { + var CompItem = item.ToListItem(); + ListItemBuild(CompItem); + CompItemList.Add(CompItem); + } + + if (CompItemList.Any()) // 有收藏 + { + if (!IsSearching) + { + PanSearchBox.Visibility = Visibility.Visible; + PanContentList.Visibility = Visibility.Visible; + CardNoContent.Visibility = Visibility.Collapsed; + } + } + else // 没有收藏 + { + PanSearchBox.Visibility = Visibility.Collapsed; + PanContentList.Visibility = Visibility.Collapsed; + CardNoContent.Visibility = Visibility.Visible; + } + + // If SomeGetFail Then + // Dim FailList As New List(Of MyListItem) + // Dim FailIds = Loader.Input.Except(Loader.Output.Select(Function(e) e.Id)) + // For Each Id In FailIds + // Dim FailItem As New MyListItem + // FailItem.Title = $"{Id}" + // FailItem.Info = "此资源获取失败,可能在线资源被删除或者未获取成功" + // FailItem.Tag = Id + + // ListItemBuild(FailItem) + + // FailList.Add(FailItem) + // Next + // CompItemList.AddRange(FailList) + // End If + + RefreshContent(); + RefreshCardTitle(); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化收藏夹列表出错", ModBase.LogLevel.Feedback); + } + } + + private void ListItemBuild(MyListItem CompItem) + { + CompItem.Type = MyListItem.CheckType.CheckBox; + var CompId = ((ModComp.CompProject)CompItem.Tag).Id; + // ----备注---- + var Notes = ""; + CurrentFavTarget.Notes.TryGetValue(CompId, out Notes); + var NoteItem = new Run { Foreground = new SolidColorBrush(Color.FromRgb(0, 184, 148)) }; + if (!string.IsNullOrWhiteSpace(Notes)) NoteItem.Text = $" ({Notes})"; + CompItem.LabTitle.Inlines.Add(NoteItem); + // ----添加按钮---- + // 修改备注按钮 + var Btn_EditNote = new MyIconButton(); + Btn_EditNote.Logo = ModBase.Logo.IconButtonEdit; + Btn_EditNote.ToolTip = "修改备注"; + ToolTipService.SetPlacement(Btn_EditNote, PlacementMode.Center); + ToolTipService.SetVerticalOffset(Btn_EditNote, 30d); + ToolTipService.SetHorizontalOffset(Btn_EditNote, 2d); + Btn_EditNote.Click += (sender, e) => + { + CurrentFavTarget.Notes.TryGetValue(CompId, out Notes); + var DesiredNote = ModMain.MyMsgBoxInput("修改备注", DefaultInput: Notes); + // 只有在用户确认时才更新备注,避免取消时清空原有备注 + if (DesiredNote is not null) + { + CurrentFavTarget.Notes[CompId] = DesiredNote; + NoteItem.Text = string.IsNullOrWhiteSpace(DesiredNote) ? "" : $" ({DesiredNote})"; + ModComp.CompFavorites.Save(); + } + }; + // 删除按钮 + var Btn_Delete = new MyIconButton(); + Btn_Delete.Logo = ModBase.Logo.IconButtonLikeFill; + Btn_Delete.ToolTip = "取消收藏"; + ToolTipService.SetPlacement(Btn_Delete, PlacementMode.Center); + ToolTipService.SetVerticalOffset(Btn_Delete, 30d); + ToolTipService.SetHorizontalOffset(Btn_Delete, 2d); + Btn_Delete.Click += (sender, e) => + { + Items_CancelFavorites(CompItem); + RefreshContent(); + RefreshCardTitle(); + RefreshBar(); + }; + CompItem.Buttons = new[] { Btn_EditNote, Btn_Delete }; + // ---操作逻辑--- + // 右键查看详细信息界面 + if (CompItem.Tag is ModComp.CompProject) + CompItem.MouseRightButtonUp += (_, _) => ModMain.FrmMain.PageChange( + new FormMain.PageStackData + { + Page = FormMain.PageType.CompDetail, + Additional = new[] + { + CompItem.Tag, new List(), string.Empty, ModComp.CompLoaderType.Any, + ((ModComp.CompProject)CompItem.Tag).Type + } + }); + // ---其它事件--- + CompItem.Changed += ItemCheckStatusChanged; + } + + #endregion + + #region UI 化 - 选择操作 + + private int BottomBarShownCount; + + private void RefreshBar() + { + var NewCount = SelectedItemList.Count; + var Selected = NewCount > 0; + if (Selected) + LabSelect.Text = $"已选择 {NewCount} 个收藏项目"; // 取消所有选择时不更新数字 + // 更新显示状态 + if (ModAnimation.AniControlEnabled == 0) + { + PanContentList.Margin = new Thickness(0d, 0d, 0d, Selected ? 80 : 0); + if (Selected) + { + // 仅在数量增加时播放出现/跳跃动画 + if (BottomBarShownCount >= NewCount) + { + BottomBarShownCount = NewCount; + return; + } + + BottomBarShownCount = NewCount; + // 出现/跳跃动画 + CardSelect.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, 1d - CardSelect.Opacity, 60), + ModAnimation.AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, 3d, 150, 120, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, -1, 90, 270, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)) + }, "CompFavorites Sidebar"); + } + else + { + // 不重复播放隐藏动画 + if (BottomBarShownCount == 0) + return; + BottomBarShownCount = 0; + // 隐藏动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, -CardSelect.Opacity, 90), + ModAnimation.AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => CardSelect.Visibility = Visibility.Collapsed, After: true) + }, "CompFavorites Sidebar"); + } + } + else + { + ModAnimation.AniStop("CompFavorites Sidebar"); + BottomBarShownCount = NewCount; + if (Selected) + { + CardSelect.Visibility = Visibility.Visible; + CardSelect.Opacity = 1d; + TransSelect.Y = -25; + } + else + { + CardSelect.Visibility = Visibility.Collapsed; + CardSelect.Opacity = 0d; + TransSelect.Y = -10; + } + } + } + + #endregion + + #region 事件 + + // 选中状态改变 + private void ItemCheckStatusChanged(object sender, ModBase.RouteEventArgs e) + { + var SenderItem = (MyListItem)sender; + if (SelectedItemList.Contains(SenderItem)) + SelectedItemList.Remove(SenderItem); + if (SenderItem.Checked) + SelectedItemList.Add(SenderItem); + RefreshBar(); + } + + // 自动重试 + private void Load_State(object sender, MyLoading.MyLoadingState state, MyLoading.MyLoadingState oldState) + { + switch (Loader.State) + { + case ModBase.LoadState.Failed: + { + var ErrorMessage = ""; + if (Loader.Error is not null) + ErrorMessage = Loader.Error.Message; + if (ErrorMessage.Contains("不是有效的 json 文件")) + { + ModBase.Log("[Download] 下载的工程列表 JSON 文件损坏,已自动重试", ModBase.LogLevel.Debug); + PageLoaderRestart(); + } + + break; + } + } + } + + private void Btn_FavoritesCancel_Clicked(object sender, ModBase.RouteEventArgs e) + { + foreach (var Items in SelectedItemList.Clone()) + Items_CancelFavorites(Items); + if (CompItemList.Any()) + { + RefreshContent(); + RefreshCardTitle(); + } + else + { + Loader.Start(); + } + + RefreshBar(); + } + + private void Btn_SelectCancel_Clicked(object sender, ModBase.RouteEventArgs e) + { + Items_SetSelectAll(false); + } + + private void Btn_FavoritesShare_Clicked(object sender, ModBase.RouteEventArgs e) + { + try + { + ModBase.ClipboardSet( + ModComp.CompFavorites.GetShareCode(SelectedItemList.Select(i => ((ModComp.CompProject)i.Tag).Id) + .ToHashSet())); + Items_SetSelectAll(false); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavourites] 分享收藏时发生错误", ModBase.LogLevel.Hint); + } + } + + private void Btn_FavoritesDownload_Clicked(object sender, ModBase.RouteEventArgs e) + { + try + { + if (1 != ModMain.MyMsgBox( + $"批量下载功能仍旧处于测试状态。{"\r\n"}使用此功能下载模组不会自动下载前置项。{"\r\n"}请在下载前仔细思考自己的需求,并仔细检查自己的选择,避免下载错误导致时间和网络流量的浪费。", + "确定使用此功能?", "继续", "算了", IsWarn: true)) + return; + var SupportedModLoader = new List(); + var LoaderFirstSet = true; + var HasMod = false; + foreach (var Item in SelectedItemList) // 获取共同支持的 ModLoader + { + var Proj = (ModComp.CompProject)Item.Tag; + if (Proj.Type == ModComp.CompType.Mod) + { + HasMod = true; + if (LoaderFirstSet) + { + LoaderFirstSet = false; + SupportedModLoader = Proj.ModLoaders; + } + else + { + SupportedModLoader = SupportedModLoader.Intersect(Proj.ModLoaders).ToList(); + } + } + } + + // 检查是否有共同支持的 ModLoader + if (HasMod && SupportedModLoader.Count == 0) + { + ModMain.Hint("所选模组不支持相同的加载器", ModMain.HintType.Critical); + return; + } + + // 要求选择版本 + var DesiredModLoader = ModComp.CompLoaderType.Any; + if (HasMod && SupportedModLoader.Count > 0) + if (SupportedModLoader.Count > 0) + { + var MSelection = new List(); + foreach (var i in SupportedModLoader) + MSelection.Add(new MyRadioBox { Text = i.ToString() }); + var SelectedModLoaderStr = ModMain.MyMsgBoxSelect(MSelection, "选择期望的加载器", Button2: "取消"); + if (SelectedModLoaderStr is null) + return; + DesiredModLoader = SupportedModLoader[(int)SelectedModLoaderStr]; + } + + ModMain.Hint("请稍后,正在查询详细版本支持中,这可能需要一段时间……"); + // 输入 Ids,输出合适版本 + var GetInfoAndDownloadLoader = new List(); + GetInfoAndDownloadLoader.Add(new ModLoader.LoaderTask, List>("查询资源信息", Ts => + { + List> AllFiles = []; + List SuitVersion = []; + var VersionFirstSet = true; + // 工程支持的全部版本获取 + Func>, List> GetAllVersionList = ls => + { + var allVersionList = new List(); + foreach (var i in ls) allVersionList.AddRange(i); + + return allVersionList.Distinct().ToList(); + }; + // 获取多个工程之间支持的版本的交集 + var FinishedTasks = 0; + foreach (var Item in Ts.Input) + ModBase.RunInNewThread(() => + { + try + { + AllFiles.Add(ModComp.CompFilesGet(Item, ModComp.CompRequest.IsFromCurseForge(Item)) + .Where(i => i.Type != ModComp.CompType.Mod || i.ModLoaders.Contains(DesiredModLoader)) + .ToList()); + } + catch (Exception ex) + { + ModBase.Log(ex, $"获取 {Item} 的下载信息失败", ModBase.LogLevel.Hint); + } + finally + { + FinishedTasks += 1; + } + }); + while (FinishedTasks != Ts.Input.Count) + Thread.Sleep(200); + // 求取共同的版本 + foreach (var Item in AllFiles) + { + var Current = GetAllVersionList(Item.Select(i => i.GameVersions).ToList()); + if (VersionFirstSet) + { + VersionFirstSet = false; + SuitVersion = Current; + } + else + { + SuitVersion = SuitVersion.Intersect(Current).ToList(); + } + + // Log(SuitVersion.Join(",")) + if (SuitVersion.Count == 0) + { + ModMain.Hint("不存在指定加载器并且同版本的资源", ModMain.HintType.Critical); + Ts.Abort(); + return; + } + // 要求用户选择希望下载的版本 + } + + int? SelectedVersion = 0; + ModBase.RunInUiWait(() => + { + List Selection = []; + foreach (var i in SuitVersion) + Selection.Add(new MyRadioBox { Text = i }); + SelectedVersion = ModMain.MyMsgBoxSelect(Selection, "选择期望的游戏版本", Button2: "取消"); + if (SelectedVersion is null) Ts.Abort(); + }); + string SelectedVersionStr = SuitVersion[(dynamic)SelectedVersion]; + ModMain.Hint($"已选择 {SelectedVersionStr} 版本,下面请选择保存位置"); + var SaveFolder = SystemDialogs.SelectFolder(); + if (string.IsNullOrWhiteSpace(SaveFolder)) + { + Ts.Abort(); + return; + } + + ; + // 获取有期望版本号的文件 + List Res = []; + foreach (var Target in AllFiles) + { + // 按照发布日期排序 + var FinalChoices = Target.Where(i => i.GameVersions.Contains(SelectedVersionStr)).ToList(); + FinalChoices.Sort((a, b) => a.ReleaseDate > b.ReleaseDate); + // 获取文件名 + var TargetProject = ModComp.CompProjectCache[FinalChoices.First().ProjectId]; + var FileName = ModComp.CompFileNameGet(TargetProject, FinalChoices.First()); + // 选择最新版本进行下载 + Res.Add(FinalChoices.First().ToNetFile(System.IO.Path.Combine(SaveFolder, FileName))); + } + + Ts.Output = Res; + }) + { + ProgressWeight = 2d + }); + GetInfoAndDownloadLoader.Add(new ModNet.LoaderDownload("批量下载合适资源", new List()) + { ProgressWeight = 8d }); + var CheckLoader = + new ModLoader.LoaderCombo>($"批量下载资源({ModBase.GetUuid()})", GetInfoAndDownloadLoader) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + CheckLoader.Start(SelectedItemList.Select(i => ((ModComp.CompProject)i.Tag).Id).ToList()); + ModLoader.LoaderTaskbarAdd(CheckLoader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + Items_SetSelectAll(false); + } + catch (Exception ex) + { + ModBase.Log(ex, "批量下载收藏时发生错误", ModBase.LogLevel.Hint); + } + } + + private void Items_SetSelectAll(bool TargetStatus) + { + if (IsSearching) + foreach (var Item in SearchResult) + Item.Checked = TargetStatus; + else + foreach (var Item in CompItemList) + Item.Checked = TargetStatus; + SelectedItemList = CompItemList.Where(e => e.Checked).ToList(); + } + + private void Items_CancelFavorites(MyListItem Item) + { + try + { + CompItemList.Remove(Item); + if (SelectedItemList.Contains(Item)) + SelectedItemList.Remove(Item); + if (SearchResult.Contains(Item)) + SearchResult.Remove(Item); + CurrentFavTarget.Favs.Remove(Conversions.ToString(((dynamic)Item.Tag).Id)); + ModComp.CompFavorites.Save(); + if (!CompItemList.Any()) + ModMain.FrmDownloadCompFavorites.PageLoaderRestart(); + } + catch (Exception ex) + { + ModBase.Log(ex, "[CompFavourites] 移除收藏时发生错误"); + } + } + + private void Page_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.A && (e.KeyboardDevice.IsKeyDown(Key.LeftCtrl) || e.KeyboardDevice.IsKeyDown(Key.RightCtrl))) + Items_SetSelectAll(true); + } + + private void Manage_Click(object sender, EventArgs _) + { + var Body = new ContextMenu(); + var NewItem = new MyMenuItem + { + Header = "分享当前收藏夹", + Icon = ModBase.Logo.IconButtonShare + }; + NewItem.Click += (_, _) => + { + try + { + if (CurrentFavTarget.Favs.Count == 0) + { + HintWrapper.Show("分享了个寂寞啊!"); + return; + } + + ModBase.ClipboardSet(ModComp.CompFavorites.GetShareCode(CurrentFavTarget.Favs)); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Favourites] 分享收藏时发生错误", ModBase.LogLevel.Hint); + } + }; + Body.Items.Add(NewItem); + NewItem = new MyMenuItem + { + Header = "导入收藏", + Icon = ModBase.Logo.IconButtonAdd + }; + NewItem.Click += (_, _) => + { + try + { + var ClipData = ModMain.MyMsgBoxInput("输入分享的收藏", HintText: "例如 [\"23333\"]"); + if (string.IsNullOrWhiteSpace(ClipData)) return; + var NewFavs = ModComp.CompFavorites.GetIdsByShareCode(ClipData); + if (NewFavs.Count == 0) + { + ModMain.Hint("分享了个寂寞啊!"); + return; + } + + var UserWant = ModMain.MyMsgBox("你希望将分享的收藏加入到当前收藏夹还是新的收藏夹中?", Button1: "新的收藏夹", Button2: "当前收藏夹"); + switch (UserWant) + { + case 1: + { + var NewFavName = ModMain.MyMsgBoxInput("新收藏夹名称", "请输入新收藏夹名称"); + if (string.IsNullOrWhiteSpace(NewFavName)) return; + ModComp.CompFavorites.FavoritesList.Add(ModComp.CompFavorites.GetNewFav(NewFavName, NewFavs)); + ModComp.CompFavorites.Save(); + RefreshFavTargets(); + ComboTargetFav.SelectedIndex = ComboTargetFav.Items.Count - 1; + break; + } + case 2: + { + NewFavs.ToList().ForEach(x => CurrentFavTarget.Favs.Add(x)); + ModComp.CompFavorites.Save(); + Loader.Start(IsForceRestart: true); + break; + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "解析分享数据失败", ModBase.LogLevel.Hint); + } + }; + Body.Items.Add(NewItem); + NewItem = new MyMenuItem + { + Header = "新建收藏夹", + Icon = ModBase.Logo.IconButtonCreate + }; + NewItem.Click += (_, _) => + { + var NewFavName = ModMain.MyMsgBoxInput("新建收藏夹", "请输入新收藏夹名称"); + if (string.IsNullOrWhiteSpace(NewFavName)) + return; + ModComp.CompFavorites.FavoritesList.Add(ModComp.CompFavorites.GetNewFav(NewFavName, null)); + ModComp.CompFavorites.Save(); + RefreshFavTargets(); + ComboTargetFav.SelectedIndex = ComboTargetFav.Items.Count - 1; + }; + Body.Items.Add(NewItem); + NewItem = new MyMenuItem + { + Header = "重命名收藏夹名称", + Icon = ModBase.Logo.IconButtonEdit + }; + NewItem.Click += (_, _) => + { + var newName = ModMain.MyMsgBoxInput("输入新名称", DefaultInput: CurrentFavTarget.Name); + if (string.IsNullOrWhiteSpace(newName) || (CurrentFavTarget.Name ?? "") == (newName ?? "")) + return; + CurrentFavTarget.Name = newName; + ModComp.CompFavorites.Save(); + RefreshFavTargets(); + }; + Body.Items.Add(NewItem); + NewItem = new MyMenuItem + { + Header = "删除当前收藏夹", + Icon = ModBase.Logo.IconButtonDelete + }; + NewItem.Click += (_, _) => + { + if (ModComp.CompFavorites.FavoritesList.Count == 1) + { + ModMain.Hint("您不能删除最后一个收藏夹"); + return; + } + + var content = $"确认删除 {CurrentFavTarget.Name} 收藏夹?" + "\r\n" + "\r\n"; + content += $"此收藏夹有 {CurrentFavTarget.Favs.Count} 个收藏项目" + "\r\n"; + content += "收藏夹 ID 为 " + CurrentFavTarget.Id + "\r\n"; + content += "此操作不可逆!"; + var res = ModMain.MyMsgBox(content, "删除确认", IsWarn: true, Button1: "否", Button2: "是", Button3: "否"); + if (res == 2) + { + ModComp.CompFavorites.FavoritesList.Remove(CurrentFavTarget); + ModComp.CompFavorites.Save(); + ModMain.Hint("已删除收藏夹", ModMain.HintType.Finish); + RefreshFavTargets(); + ComboTargetFav.SelectedIndex = 0; + } + }; + Body.Items.Add(NewItem); + Body.PlacementTarget = (UIElement)sender; + Body.Placement = PlacementMode.Bottom; + Body.IsOpen = true; + } + + private void ComboTargetFav_Selected(object sender, RoutedEventArgs e) + { + if (ComboTargetFav.SelectedItem is null) + return; + Items_SetSelectAll(false); + Loader.Start(IsForceRestart: true); + } + + private void HintGetFail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + var Content = "由于在线资源被删除或者网络问题等因素导致以下资源未获取成功(以资源的 ID 展示)" + "\r\n" + "\r\n"; + var FailIds = Loader.Input.Except(Loader.Output.Select(i => i.Id).ToList()).ToList(); + foreach (var Id in FailIds) + Content += $" - {Id}" + "\r\n"; + ModMain.MyMsgBox(Content, "部分收藏项目获取失败", Button2: "复制这些 ID", Button3: "移除这些收藏", + Button2Action: () => ModBase.ClipboardSet(FailIds.Join("\r\n")), Button3Action: () => + { + foreach (var Id in FailIds) + CurrentFavTarget.Favs.Remove(Id); + ModComp.CompFavorites.Save(); + ModMain.Hint("已移除相关收藏", ModMain.HintType.Finish); + }); + } + + #endregion + + #region 搜索 + + private bool IsSearching => !string.IsNullOrWhiteSpace(PanSearchBox.Text); + + private bool AllowSearch = true; + private List SearchResult = new(); + + public void SearchRun(object sender, EventArgs e) + { + if (!AllowSearch) + return; + if (IsSearching) + { + // 构造请求 + var QueryList = new List>(); + foreach (var Item in CompItemList) + { + if (!(Item.Tag is ModComp.CompProject)) + continue; + var Entry = (ModComp.CompProject)Item.Tag; + var SearchSource = new List>(); + SearchSource.Add(new KeyValuePair(Entry.RawName, 1d)); + if (Entry.Description is not null && !string.IsNullOrEmpty(Entry.Description)) + SearchSource.Add(new KeyValuePair(Entry.Description, 0.4d)); + if ((Entry.TranslatedName ?? "") != (Entry.RawName ?? "")) + SearchSource.Add(new KeyValuePair(Entry.TranslatedName, 1d)); + SearchSource.Add(new KeyValuePair(string.Join("", Entry.Tags), 0.2d)); + QueryList.Add(new ModBase.SearchEntry { Item = Item, SearchSource = SearchSource }); + } + + // 进行搜索 + SearchResult = ModBase.Search(QueryList, PanSearchBox.Text, 6, 0.35d).Select(r => r.Item).ToList(); + } + + RefreshContent(); + RefreshCardTitle(); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.vb index 35919105b..96570309b 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompFavorites.xaml.vb @@ -371,11 +371,7 @@ Public Class PageDownloadCompFavorites Private Sub Btn_FavoritesDownload_Clicked(sender As Object, e As RouteEventArgs) Handles Btn_FavoritesDownload.Click Try - If SelectedItemList.Count = 1 Then - Hint("要不……你直接进详情页里下载吧……") - Exit Sub - End If - If 1 <> MyMsgBox($"批量下载功能仍旧处于测试状态{vbCrLf}使用此功能下载模组不会自动下载前置项。{vbCrLf}请在下载前仔细思考自己的需求,并仔细检查自己的选择,避免下载错误导致时间和网络流量的浪费。", "确定使用此功能?", Button1:="继续", Button2:="算了", IsWarn:=True) Then Exit Sub + If 1 <> MyMsgBox($"批量下载功能仍旧处于测试状态。{vbCrLf}使用此功能下载模组不会自动下载前置项。{vbCrLf}请在下载前仔细思考自己的需求,并仔细检查自己的选择,避免下载错误导致时间和网络流量的浪费。", "确定使用此功能?", Button1:="继续", Button2:="算了", IsWarn:=True) Then Exit Sub Dim SupportedModLoader As New List(Of CompLoaderType) Dim LoaderFirstSet As Boolean = True Dim HasMod As Boolean = False @@ -484,8 +480,11 @@ Public Class PageDownloadCompFavorites Dim FinalChoices = Target.Where(Function(i) i.GameVersions.Contains(SelectedVersionStr)).ToList() ' 按照发布日期排序 FinalChoices.Sort(Function(a As CompFile, b As CompFile) a.ReleaseDate > b.ReleaseDate) + ' 获取文件名 + Dim TargetProject As CompProject = ModComp.CompProjectCache(FinalChoices.First.ProjectId) + Dim FileName As String = CompFileNameGet(TargetProject, FinalChoices.First) ' 选择最新版本进行下载 - Res.Add(FinalChoices.First.ToNetFile(SaveFolder & FinalChoices.First.FileName)) + Res.Add(FinalChoices.First.ToNetFile(IO.Path.Combine(SaveFolder, FileName))) Next Ts.Output = Res End Sub) With {.ProgressWeight = 2}) diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml index 0c19a78a1..65d97943e 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml @@ -1,28 +1,34 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml.cs new file mode 100644 index 000000000..9f58e1c3a --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadFabric.xaml.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; + +namespace PCL; + +public partial class PageDownloadFabric +{ + public PageDownloadFabric() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, CardVersions, CardTip, ModDownload.DlFabricListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + var Versions = (JArray)ModDownload.DlFabricListLoader.Output.Value["installer"]; + PanVersions.Children.Clear(); + foreach (var Version in Versions) + PanVersions.Children.Add( + ModDownloadLib.FabricDownloadListItem((JObject)Version, + (sender, e) => Fabric_Selected((MyListItem)sender, e))); + CardVersions.Title = "版本列表 (" + Versions.Count + ")"; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Fabric 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + private void Fabric_Selected(MyListItem sender, EventArgs e) + { + ModDownloadLib.McDownloadFabricLoaderSave((JObject)sender.Tag); + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://www.fabricmc.net"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml index a4379c4ae..19fd0c794 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml @@ -1,26 +1,31 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml.cs new file mode 100644 index 000000000..2589eb86b --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadForge.xaml.cs @@ -0,0 +1,117 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace PCL; + +public partial class PageDownloadForge +{ + public PageDownloadForge() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanMain, CardTip, ModDownload.DlForgeListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + // 清空当前 + PanMain.Children.Clear(); + // 转化为 UI + foreach (var Version in ModDownload.DlForgeListLoader.Output.Value.Sort(ModMinecraft.CompareVersionGe)) + { + // 增加卡片 + var NewCard = new MyCard + { Title = Version.Replace("_p", " P"), Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Version + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + NewCard.InstallMethod = Stack => + { + var LoadingPickaxe = new MyLoading { Text = "正在获取版本列表", Margin = new Thickness(5d) }; + var Loader = + new ModLoader.LoaderTask>("DlForgeVersion Main", + ModDownload.DlForgeVersionMain); + LoadingPickaxe.State = Loader; + Loader.Start(Stack.Tag); + LoadingPickaxe.StateChanged += (a, b, c) => + ModMain.FrmDownloadForge.Forge_StateChanged((MyLoading)a, b, c); + LoadingPickaxe.Click += (a, b) => ModMain.FrmDownloadForge.Forge_Click((MyLoading)a, b); + Stack.Children.Add(LoadingPickaxe); + }; + NewCard.IsSwapped = true; + PanMain.Children.Add(NewCard); + } + } + // '非官方源警示 + // If Setup.Get("ToolDownloadOutOfDate") AndAlso Not DlForgeListLoader.Output.IsOfficial Then + // Dim CardWarn As New MyCard With {.Title = "过期提示", .Margin = New Thickness(0, 0, 0, 15)} + // CardWarn.Children.Add(New TextBlock With { + // .Margin = New Thickness(25, MyCard.SwapedHeight, 15, 15), .VerticalAlignment = VerticalAlignment.Top, .HorizontalAlignment = HorizontalAlignment.Left, .TextTrimming = TextTrimming.None, .TextWrapping = TextWrapping.Wrap, + // .Text = "获取官方源失败,正在使用 " & DlForgeListLoader.Output.SourceName & " 镜像源,版本列表可能并非最新。" & vbCrLf & "官方源错误原因:" & If(DlForgeListLoader.Output.OfficialError, New Exception("连接服务器超时")).Message}) + // PanMain.Children.Insert(0, CardWarn) + // End If + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Forge 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // Forge 版本列表加载 + public void Forge_Click(MyLoading sender, MouseButtonEventArgs e) + { + if (sender.State.LoadingState == MyLoading.MyLoadingState.Error) + ((ModLoader.LoaderTask>)sender.State).Start( + IsForceRestart: true); + } + + public void Forge_StateChanged(MyLoading sender, MyLoading.MyLoadingState newState, + MyLoading.MyLoadingState oldState) + { + if (newState != MyLoading.MyLoadingState.Stop) + return; + + var Card = (MyCard)((FrameworkElement)sender.Parent).Parent; + var Loader = (ModLoader.LoaderTask>)sender.State; + // 载入列表 + ((dynamic)Card.SwapControl).Children.Clear(); + ((dynamic)Card.SwapControl).Tag = Loader.Output; + Card.InstallMethod = Stack => + { + Stack.Tag = ((List)Stack.Tag).Sort((a, b) => a.Version > b.Version); + ModDownloadLib.ForgeDownloadListItemPreload(Stack, (List)Stack.Tag, + ModDownloadLib.ForgeSave_Click, true); + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.ForgeDownloadListItem((ModDownload.DlForgeVersionEntry)item, + ModDownloadLib.ForgeSave_Click, true)); + }; + Card.StackInstall(); + } + + // 介绍栏 + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://files.minecraftforge.net"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml index 482f30cf5..d82c6419c 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml @@ -1,110 +1,192 @@ + xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" x:Class="PCL.PageDownloadInstall"> - + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + @@ -112,35 +194,26 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - + - + @@ -148,53 +221,163 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -202,79 +385,33 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + HorizontalAlignment="Center" VerticalAlignment="Bottom" + LogoScale="0.95" + Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" /> - - + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.cs new file mode 100644 index 000000000..431ad0c32 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.cs @@ -0,0 +1,2643 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; + +namespace PCL; + +public partial class PageDownloadInstall +{ + private bool IsLoad; + + public PageDownloadInstall() + { + PanScroll = PanBack; + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnBack.Click += (_, _) => ExitSelectPage(); + CardOptiFine.Swap += (_, _) => ReloadSelected(); + LoadOptiFine.StateChanged += (_, _, _) => ReloadSelected(); + CardForge.Swap += (_, _) => ReloadSelected(); + LoadForge.StateChanged += (_, _, _) => ReloadSelected(); + CardNeoForge.Swap += (_, _) => ReloadSelected(); + LoadNeoForge.StateChanged += (_, _, _) => ReloadSelected(); + CardFabric.Swap += (_, _) => ReloadSelected(); + LoadFabric.StateChanged += (_, _, _) => ReloadSelected(); + CardFabricApi.Swap += (_, _) => ReloadSelected(); + LoadFabricApi.StateChanged += (_, _, _) => ReloadSelected(); + CardOptiFabric.Swap += (_, _) => ReloadSelected(); + LoadOptiFabric.StateChanged += (_, _, _) => ReloadSelected(); + CardLiteLoader.Swap += (_, _) => ReloadSelected(); + LoadLiteLoader.StateChanged += (_, _, _) => ReloadSelected(); + LoadQuilt.StateChanged += (_, _, _) => ReloadSelected(); + CardQuilt.Swap += (_, _) => ReloadSelected(); + LoadQSL.StateChanged += (_, _, _) => ReloadSelected(); + CardQSL.Swap += (_, _) => ReloadSelected(); + LoadCleanroom.StateChanged += (_, _, _) => ReloadSelected(); + CardCleanroom.Swap += (_, _) => ReloadSelected(); + LoadLabyMod.StateChanged += (_, _, _) => ReloadSelected(); + CardLabyMod.Swap += (_, _) => ReloadSelected(); + TextSelectName.TextChanged += TextSelectName_TextChanged; + TextSelectName.ValidateChanged += TextSelectName_ValidateChanged; + CardOptiFine.PreviewSwap += CardOptiFine_PreviewSwap; + LoadOptiFine.StateChanged += (_, _, _) => OptiFine_Loaded(); + BtnOptiFineClear.MouseLeftButtonUp += OptiFine_Clear; + CardLiteLoader.PreviewSwap += CardLiteLoader_PreviewSwap; + LoadLiteLoader.StateChanged += (_, _, _) => LiteLoader_Loaded(); + BtnLiteLoaderClear.MouseLeftButtonUp += LiteLoader_Clear; + CardForge.PreviewSwap += CardForge_PreviewSwap; + LoadForge.StateChanged += (_, _, _) => Forge_Loaded(); + BtnForgeClear.MouseLeftButtonUp += Forge_Clear; + CardNeoForge.PreviewSwap += CardNeoForge_PreviewSwap; + LoadNeoForge.StateChanged += (_, _, _) => NeoForge_Loaded(); + BtnNeoForgeClear.MouseLeftButtonUp += NeoForge_Clear; + CardCleanroom.PreviewSwap += CardCleanroom_PreviewSwap; + LoadCleanroom.StateChanged += (_, _, _) => Cleanroom_Loaded(); + BtnCleanroomClear.MouseLeftButtonUp += Cleanroom_Clear; + CardFabric.PreviewSwap += CardFabric_PreviewSwap; + LoadFabric.StateChanged += (_, _, _) => Fabric_Loaded(); + BtnFabricClear.MouseLeftButtonUp += Fabric_Clear; + CardFabricApi.PreviewSwap += CardFabricApi_PreviewSwap; + LoadFabricApi.StateChanged += (_, _, _) => FabricApi_Loaded(); + BtnFabricApiClear.MouseLeftButtonUp += FabricApi_Clear; + CardLegacyFabric.PreviewSwap += CardLegacyFabric_PreviewSwap; + LoadLegacyFabric.StateChanged += (_, _, _) => LegacyFabric_Loaded(); + BtnLegacyFabricClear.MouseLeftButtonUp += LegacyFabric_Clear; + CardLegacyFabricApi.PreviewSwap += CardLegacyFabricApi_PreviewSwap; + LoadLegacyFabricApi.StateChanged += (_, _, _) => LegacyFabricApi_Loaded(); + BtnLegacyFabricApiClear.MouseLeftButtonUp += LegacyFabricApi_Clear; + CardQuilt.PreviewSwap += CardQuilt_PreviewSwap; + LoadQuilt.StateChanged += (_, _, _) => Quilt_Loaded(); + BtnQuiltClear.MouseLeftButtonUp += Quilt_Clear; + CardQSL.PreviewSwap += CardQSL_PreviewSwap; + LoadQSL.StateChanged += (_, _, _) => QSL_Loaded(); + BtnQSLClear.MouseLeftButtonUp += QSL_Clear; + CardOptiFabric.PreviewSwap += CardOptiFabric_PreviewSwap; + LoadOptiFabric.StateChanged += (_, _, _) => OptiFabric_Loaded(); + BtnOptiFabricClear.MouseLeftButtonUp += OptiFabric_Clear; + CardLabyMod.PreviewSwap += CardLabyMod_PreviewSwap; + LoadLabyMod.StateChanged += (_, _, _) => LabyMod_Loaded(); + BtnLabyModClear.MouseLeftButtonUp += LabyMod_Clear; + TextSelectName.KeyDown += TextSelectName_KeyDown; + BtnStart.Click += (_, _) => BtnStart_Click(); + } + + private void LoaderInit() + { + DisabledPageAnimControls.Add(BtnStart); + PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, null, ModDownload.DlClientListLoader, + _ => LoadMinecraft_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + ModDownload.DlOptiFineListLoader.Start(); + ModDownload.DlLiteLoaderListLoader.Start(); + ModDownload.DlFabricListLoader.Start(); + ModDownload.DlQuiltListLoader.Start(); + ModDownload.DlNeoForgeListLoader.Start(); + ModDownload.DlCleanroomListLoader.Start(); + ModDownload.DlLabyModListLoader.Start(); + ModDownload.DlLegacyFabricListLoader.Start(); + + // 重载预览 + TextSelectName.ValidateRules = new Collection + { new ValidateFolderName(ModMinecraft.McFolderSelected + "versions") }; + TextSelectName.Validate(); + ReloadSelected(); + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + + ModDownloadLib.McDownloadForgeRecommendedRefresh(); + + LoadOptiFine.State = ModDownload.DlOptiFineListLoader; + LoadLiteLoader.State = ModDownload.DlLiteLoaderListLoader; + LoadFabric.State = ModDownload.DlFabricListLoader; + LoadFabricApi.State = ModDownload.DlFabricApiLoader; + LoadQuilt.State = ModDownload.DlQuiltListLoader; + LoadQSL.State = ModDownload.DlQSLLoader; + LoadNeoForge.State = ModDownload.DlNeoForgeListLoader; + LoadCleanroom.State = ModDownload.DlCleanroomListLoader; + LoadOptiFabric.State = ModDownload.DlOptiFabricLoader; + LoadLabyMod.State = ModDownload.DlLabyModListLoader; + LoadLegacyFabric.State = ModDownload.DlLegacyFabricListLoader; + LoadLegacyFabricApi.State = ModDownload.DlLegacyFabricApiLoader; + } + + private string GetLoaderError(MyLoading loader) + { + if (loader is null) + return "获取中……"; + if (!loader.State.IsLoader) + return "获取中……"; + switch (loader.State.LoadingState) + { + case MyLoading.MyLoadingState.Run: + { + return "获取中……"; + } + case MyLoading.MyLoadingState.Error: + { + var message = ((ModLoader.LoaderBase)loader.State).Error.Message; + return message == "无可用版本" ? "无可用版本" : "获取失败:" + message; + } + case MyLoading.MyLoadingState.Unloaded: + { + return "未知错误,状态为 Unloaded"; + } + + default: + { + return null; + } + } + } + + private void BtnBack_Click(object sender, EventArgs e) + { + ExitSelectPage(); + } + + #region 页面切换 + + // 页面切换动画 + public bool IsInSelectPage; + private bool IsFirstLoaded; + + private void EnterSelectPage() + { + if (IsInSelectPage) + return; + IsInSelectPage = true; + + PanInner.Margin = new Thickness(25d, 10d, 25d, 40d); + + AutoSelectedFabricApi = false; + AutoSelectedQSL = false; + AutoSelectedOptiFabric = false; + IsSelectNameEdited = false; + PanSelect.Visibility = Visibility.Visible; + PanSelect.IsHitTestVisible = true; + PanMinecraft.IsHitTestVisible = false; + PanBack.IsHitTestVisible = false; + PanBack.ScrollToHome(); + + DisabledPageAnimControls.Remove(BtnStart); + BtnStart.Show = true; + CardOptiFine.IsSwapped = true; + CardLiteLoader.IsSwapped = true; + CardForge.IsSwapped = true; + CardNeoForge.IsSwapped = true; + CardCleanroom.IsSwapped = true; + CardFabric.IsSwapped = true; + CardFabricApi.IsSwapped = true; + CardQuilt.IsSwapped = true; + CardQSL.IsSwapped = true; + CardOptiFabric.IsSwapped = true; + CardLabyMod.IsSwapped = true; + + if (!States.Hint.InstallPageBack) + { + States.Hint.InstallPageBack = true; + ModMain.Hint("点击 Minecraft 项即可返回游戏主版本选择页面!"); + } + + // 如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复 + foreach (var control in GetAllAnimControls(PanSelect)) + { + control.Opacity = 1d; + if (control.RenderTransform is null || control.RenderTransform is TranslateTransform) + control.RenderTransform = new TranslateTransform(); + } + + // 启动 Forge 加载 + if (ModMinecraft.McInstanceInfo.IsFormatFit(_vanillaName)) + { + var ForgeLoader = + new ModLoader.LoaderTask>( + "DlForgeVersion " + _vanillaName, ModDownload.DlForgeVersionMain); + LoadForge.State = ForgeLoader; + ForgeLoader.Start(_vanillaName); + } + + // 启动 Fabric API、QSL、Legacy Fabric API、OptiFabric、LabyMod 加载 + ModDownload.DlFabricApiLoader.Start(); + ModDownload.DlQSLLoader.Start(); + ModDownload.DlLegacyFabricApiLoader.Start(); + ModDownload.DlOptiFabricLoader.Start(); + ModDownload.DlLabyModListLoader.Start(); + + ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(PanMinecraft, -PanMinecraft.Opacity, 70, 10), + ModAnimation.AaTranslateX(PanMinecraft, -50 - ((TranslateTransform)PanMinecraft.RenderTransform).X, 90, 10), + ModAnimation.AaCode(() => + { + PanBack.ScrollToHome(); + TextSelectName.Validate(); + OptiFine_Loaded(); + LiteLoader_Loaded(); + Forge_Loaded(); + NeoForge_Loaded(); + Cleanroom_Loaded(); + Fabric_Loaded(); + LegacyFabric_Loaded(); + FabricApi_Loaded(); + LegacyFabricApi_Loaded(); + Quilt_Loaded(); + QSL_Loaded(); + OptiFabric_Loaded(); + LabyMod_Loaded(); + ReloadSelected(); + PanMinecraft.Visibility = Visibility.Collapsed; + }, After: true), + ModAnimation.AaOpacity(PanSelect, 1d - PanSelect.Opacity, 70, 100), + ModAnimation.AaTranslateX(PanSelect, -((TranslateTransform)PanSelect.RenderTransform).X, 160, 100, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), + ModAnimation.AaCode(() => + { + PanBack.IsHitTestVisible = true; + // 初始化 Binding + if (IsFirstLoaded) + return; + IsFirstLoaded = true; + BtnOptiFineClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardOptiFine.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLiteLoaderClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLiteLoader.MainTextBlock, Mode = BindingMode.OneWay }); + BtnForgeClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardForge.MainTextBlock, Mode = BindingMode.OneWay }); + BtnNeoForgeClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardNeoForge.MainTextBlock, Mode = BindingMode.OneWay }); + BtnCleanroomClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardCleanroom.MainTextBlock, Mode = BindingMode.OneWay }); + BtnFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardFabric.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLegacyFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLegacyFabric.MainTextBlock, Mode = BindingMode.OneWay }); + BtnFabricApiClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardFabricApi.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLegacyFabricApiClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") + { Source = CardLegacyFabricApi.MainTextBlock, Mode = BindingMode.OneWay }); + BtnQuiltClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardQuilt.MainTextBlock, Mode = BindingMode.OneWay }); + BtnQSLClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardQSL.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLabyModClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLabyMod.MainTextBlock, Mode = BindingMode.OneWay }); + BtnOptiFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardOptiFabric.MainTextBlock, Mode = BindingMode.OneWay }); + }, After: true) + }, "FrmDownloadInstall SelectPageSwitch", true); + } + + public void ExitSelectPage() + { + if (!IsInSelectPage) + return; + IsInSelectPage = false; + + PanInner.Margin = new Thickness(25d, 10d, 25d, 25d); + + DisabledPageAnimControls.Add(BtnStart); + BtnStart.Show = false; + ClearSelected(); // 清除已选择项 + PanMinecraft.Visibility = Visibility.Visible; + PanSelect.IsHitTestVisible = false; + PanMinecraft.IsHitTestVisible = true; + PanBack.IsHitTestVisible = false; + PanBack.ScrollToHome(); + + ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(PanSelect, -PanSelect.Opacity, 70, 10), + ModAnimation.AaTranslateX(PanSelect, 50d - ((TranslateTransform)PanSelect.RenderTransform).X, 90, 10), + ModAnimation.AaCode(() => PanBack.ScrollToHome(), After: true), + ModAnimation.AaOpacity(PanMinecraft, 1d - PanMinecraft.Opacity, 70, 100), + ModAnimation.AaTranslateX(PanMinecraft, -((TranslateTransform)PanMinecraft.RenderTransform).X, 160, 100, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.ExtraStrong)), + ModAnimation.AaCode(() => + { + PanSelect.Visibility = Visibility.Collapsed; + PanBack.IsHitTestVisible = true; + }, After: true) + }, "FrmDownloadInstall SelectPageSwitch"); + } + + public void MinecraftSelected(MyListItem sender, MouseButtonEventArgs e) + { + _vanillaName = sender.Title; + _vanillaData = (JObject)(dynamic)sender.Tag; + _vanillaIcon = sender.Logo; + EnterSelectPage(); + } + + #endregion + + #region 选择 + + // Minecraft + private string? _vanillaName; + private JObject? _vanillaData; + private string? _vanillaIcon; + private int VanillaDrop => ModMinecraft.McInstanceInfo.VersionToDrop(_vanillaName, true); + + // OptiFine + private ModDownload.DlOptiFineListEntry? SelectedOptiFine; + + /// + /// 选定的 Mod Loader 名称,内容应为 Forge / NeoForge / Fabric / Quilt / Cleanroom / LabyMod + /// + private string? SelectedLoaderName; + + /// + /// 选定的 Mod Loader API 名称,内容应为 Fabric API 或 QFAPI / QSL + /// + private string? SelectedAPIName; + + // LiteLoader + private ModDownload.DlLiteLoaderListEntry? SelectedLiteLoader; + + // Forge + private ModDownload.DlForgeVersionEntry? SelectedForge; + + // Cleanroom + private ModDownload.DlCleanroomListEntry? SelectedCleanroom; + + // NeoForge + private ModDownload.DlNeoForgeListEntry? SelectedNeoForge; + + // Fabric + private string? SelectedFabric; + + // FabricApi + private ModComp.CompFile? SelectedFabricApi; + + // LegacyFabric + private string? SelectedLegacyFabric; + + // Legacy FabricApi + private ModComp.CompFile? SelectedLegacyFabricApi; + + // Quilt + private string? SelectedQuilt; + + // QSL + private ModComp.CompFile? SelectedQSL; + + // LabyMod + private string? SelectedLabyModChannel; + private string? SelectedLabyModCommitRef; + private string? SelectedLabyModVersion; + + // OptiFabric + private ModComp.CompFile? SelectedOptiFabric; + + private bool _ReloadSelected_Ongoing; // #3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload + + /// + /// 重载已选择的项目的显示。 + /// + private void ReloadSelected() + { + if (_vanillaName is null || _ReloadSelected_Ongoing) + return; + _ReloadSelected_Ongoing = true; + // 主预览 + SelectNameUpdate(); + ImgLogo.Source = GetSelectLogo(); + // OptiFine + var OptiFineError = LoadOptiFineGetError(); + CardOptiFine.MainSwap.Visibility = OptiFineError is null ? Visibility.Visible : Visibility.Collapsed; + if (OptiFineError is not null) + CardOptiFine.IsSwapped = true; // 例如在同时展开卡片时选择了不兼容项则强制折叠 + SetPanelVisibility(PanOptiFineInfo, CardOptiFine.IsSwapped); + if (SelectedOptiFine is null) + { + BtnOptiFineClear.Visibility = Visibility.Collapsed; + ImgOptiFine.Visibility = Visibility.Collapsed; + LabOptiFine.Text = OptiFineError ?? "可以添加"; + LabOptiFine.Foreground = ModSecret.ColorGray4; + } + else + { + BtnOptiFineClear.Visibility = Visibility.Visible; + ImgOptiFine.Visibility = Visibility.Visible; + LabOptiFine.Text = SelectedOptiFine.DisplayName.Replace(_vanillaName + " ", ""); + LabOptiFine.Foreground = ModSecret.ColorGray1; + } + + // LiteLoader + if (VanillaDrop >= 130) + { + CardLiteLoader.Visibility = Visibility.Collapsed; + } + else + { + CardLiteLoader.Visibility = Visibility.Visible; + var LiteLoaderError = LoadLiteLoaderGetError(); + CardLiteLoader.MainSwap.Visibility = LiteLoaderError is null ? Visibility.Visible : Visibility.Collapsed; + if (LiteLoaderError is not null) + CardLiteLoader.IsSwapped = true; // 例如在同时展开卡片时选择了不兼容项则强制折叠 + SetPanelVisibility(PanLiteLoaderInfo, CardLiteLoader.IsSwapped); + if (SelectedLiteLoader is null) + { + BtnLiteLoaderClear.Visibility = Visibility.Collapsed; + ImgLiteLoader.Visibility = Visibility.Collapsed; + LabLiteLoader.Text = LiteLoaderError ?? "可以添加"; + LabLiteLoader.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLiteLoaderClear.Visibility = Visibility.Visible; + ImgLiteLoader.Visibility = Visibility.Visible; + LabLiteLoader.Text = SelectedLiteLoader.Inherit; + LabLiteLoader.Foreground = ModSecret.ColorGray1; + } + } + + // Forge + if (!ModMinecraft.McInstanceInfo.IsFormatFit(_vanillaName)) + { + CardForge.Visibility = Visibility.Collapsed; + } + else + { + CardForge.Visibility = Visibility.Visible; + var forgeError = LoadForgeGetError(); + CardForge.MainSwap.Visibility = forgeError is null ? Visibility.Visible : Visibility.Collapsed; + if (forgeError is not null) + CardForge.IsSwapped = true; + SetPanelVisibility(PanForgeInfo, CardForge.IsSwapped); + if (SelectedForge is null) + { + BtnForgeClear.Visibility = Visibility.Collapsed; + ImgForge.Visibility = Visibility.Collapsed; + LabForge.Text = forgeError ?? "可以添加"; + LabForge.Foreground = ModSecret.ColorGray4; + } + else + { + BtnForgeClear.Visibility = Visibility.Visible; + ImgForge.Visibility = Visibility.Visible; + LabForge.Text = SelectedForge.VersionName; + LabForge.Foreground = ModSecret.ColorGray1; + } + } + + // Cleanroom + if (_vanillaName == "1.12.2") + { + CardCleanroom.Visibility = Visibility.Visible; + var cleanroomError = LoadCleanroomGetError(); + CardCleanroom.MainSwap.Visibility = cleanroomError is null ? Visibility.Visible : Visibility.Collapsed; + if (cleanroomError is not null) + CardCleanroom.IsSwapped = true; + SetPanelVisibility(PanCleanroomInfo, CardCleanroom.IsSwapped); + if (SelectedCleanroom is null) + { + BtnCleanroomClear.Visibility = Visibility.Collapsed; + ImgCleanroom.Visibility = Visibility.Collapsed; + LabCleanroom.Text = cleanroomError ?? "可以添加"; + LabCleanroom.Foreground = ModSecret.ColorGray4; + } + else + { + BtnCleanroomClear.Visibility = Visibility.Visible; + ImgCleanroom.Visibility = Visibility.Visible; + LabCleanroom.Text = SelectedCleanroom.VersionName; + LabCleanroom.Foreground = ModSecret.ColorGray1; + } + } + else + { + CardCleanroom.Visibility = Visibility.Collapsed; + } + + // NeoForge + if (VanillaDrop is > 0 and < 200) // 匹配 1.20.1+ 与一些愚人节版本 + { + CardNeoForge.Visibility = Visibility.Collapsed; + } + else + { + CardNeoForge.Visibility = Visibility.Visible; + var neoForgeError = LoadNeoForgeGetError(); + CardNeoForge.MainSwap.Visibility = neoForgeError is null ? Visibility.Visible : Visibility.Collapsed; + if (neoForgeError is not null) + CardNeoForge.IsSwapped = true; + SetPanelVisibility(PanNeoForgeInfo, CardNeoForge.IsSwapped); + if (SelectedNeoForge is null) + { + BtnNeoForgeClear.Visibility = Visibility.Collapsed; + ImgNeoForge.Visibility = Visibility.Collapsed; + LabNeoForge.Text = neoForgeError ?? "可以添加"; + LabNeoForge.Foreground = ModSecret.ColorGray4; + } + else + { + BtnNeoForgeClear.Visibility = Visibility.Visible; + ImgNeoForge.Visibility = Visibility.Visible; + LabNeoForge.Text = SelectedNeoForge.VersionName; + LabNeoForge.Foreground = ModSecret.ColorGray1; + } + } + + // Fabric + if (VanillaDrop <= 130) + { + CardFabric.Visibility = Visibility.Collapsed; + } + else + { + CardFabric.Visibility = Visibility.Visible; + var fabricError = LoadFabricGetError(); + CardFabric.MainSwap.Visibility = fabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (fabricError is not null) + CardFabric.IsSwapped = true; + SetPanelVisibility(PanFabricInfo, CardFabric.IsSwapped); + if (SelectedFabric is null) + { + BtnFabricClear.Visibility = Visibility.Collapsed; + ImgFabric.Visibility = Visibility.Collapsed; + LabFabric.Text = fabricError ?? "可以添加"; + LabFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnFabricClear.Visibility = Visibility.Visible; + ImgFabric.Visibility = Visibility.Visible; + LabFabric.Text = SelectedFabric.Replace("+build", ""); + LabFabric.Foreground = ModSecret.ColorGray1; + } + } + + // FabricApi + if (SelectedFabric is null && SelectedQuilt is null) + { + CardFabricApi.Visibility = Visibility.Collapsed; + } + else + { + CardFabricApi.Visibility = Visibility.Visible; + var fabricApiError = LoadFabricApiGetError(); + CardFabricApi.MainSwap.Visibility = fabricApiError is null ? Visibility.Visible : Visibility.Collapsed; + if (fabricApiError is not null || (SelectedFabric is null && SelectedQuilt is null)) + CardFabricApi.IsSwapped = true; + SetPanelVisibility(PanFabricApiInfo, CardFabricApi.IsSwapped); + if (SelectedFabricApi is null) + { + BtnFabricApiClear.Visibility = Visibility.Collapsed; + ImgFabricApi.Visibility = Visibility.Collapsed; + LabFabricApi.Text = fabricApiError ?? "可以添加"; + LabFabricApi.Foreground = ModSecret.ColorGray4; + } + else + { + BtnFabricApiClear.Visibility = Visibility.Visible; + ImgFabricApi.Visibility = Visibility.Visible; + LabFabricApi.Text = SelectedFabricApi.DisplayName.Split("]")[1].Replace("Fabric API ", "") + .Replace(" build ", ".").Split("+").First().Trim(); + LabFabricApi.Foreground = ModSecret.ColorGray1; + } + } + + // LegacyFabric + if (VanillaDrop > 130) + { + CardLegacyFabric.Visibility = Visibility.Collapsed; + } + else + { + CardLegacyFabric.Visibility = Visibility.Visible; + var legacyFabricError = LoadLegacyFabricGetError(); + CardLegacyFabric.MainSwap.Visibility = + legacyFabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (legacyFabricError is not null) + CardLegacyFabric.IsSwapped = true; + SetPanelVisibility(PanLegacyFabricInfo, CardLegacyFabric.IsSwapped); + if (SelectedLegacyFabric is null) + { + BtnLegacyFabricClear.Visibility = Visibility.Collapsed; + ImgLegacyFabric.Visibility = Visibility.Collapsed; + LabLegacyFabric.Text = legacyFabricError ?? "可以添加"; + LabLegacyFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLegacyFabricClear.Visibility = Visibility.Visible; + ImgLegacyFabric.Visibility = Visibility.Visible; + LabLegacyFabric.Text = SelectedLegacyFabric.Replace("+build", ""); + LabLegacyFabric.Foreground = ModSecret.ColorGray1; + } + } + + // LegacyFabricApi + if (SelectedLegacyFabric is null) + { + CardLegacyFabricApi.Visibility = Visibility.Collapsed; + } + else + { + CardLegacyFabricApi.Visibility = Visibility.Visible; + var legacyFabricApiError = LoadLegacyFabricApiGetError(); + CardLegacyFabricApi.MainSwap.Visibility = + legacyFabricApiError is null ? Visibility.Visible : Visibility.Collapsed; + if (legacyFabricApiError is not null || (SelectedLegacyFabric is null && SelectedQuilt is null)) + CardLegacyFabricApi.IsSwapped = true; + SetPanelVisibility(PanLegacyFabricApiInfo, CardLegacyFabricApi.IsSwapped); + if (SelectedLegacyFabricApi is null) + { + BtnLegacyFabricApiClear.Visibility = Visibility.Collapsed; + ImgLegacyFabricApi.Visibility = Visibility.Collapsed; + LabLegacyFabricApi.Text = legacyFabricApiError ?? "可以添加"; + LabLegacyFabricApi.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLegacyFabricApiClear.Visibility = Visibility.Visible; + ImgLegacyFabricApi.Visibility = Visibility.Visible; + LabLegacyFabricApi.Text = SelectedLegacyFabricApi.DisplayName.Replace("Legacy Fabric API ", ""); + LabLegacyFabricApi.Foreground = ModSecret.ColorGray1; + } + } + + // Quilt + if (VanillaDrop < 144) + { + CardQuilt.Visibility = Visibility.Collapsed; + } + else + { + CardQuilt.Visibility = Visibility.Visible; + var quiltError = LoadQuiltGetError(); + CardQuilt.MainSwap.Visibility = quiltError is null ? Visibility.Visible : Visibility.Collapsed; + if (quiltError is not null) + CardQuilt.IsSwapped = true; + SetPanelVisibility(PanQuiltInfo, CardQuilt.IsSwapped); + if (SelectedQuilt is null) + { + BtnQuiltClear.Visibility = Visibility.Collapsed; + ImgQuilt.Visibility = Visibility.Collapsed; + LabQuilt.Text = quiltError ?? "可以添加"; + LabQuilt.Foreground = ModSecret.ColorGray4; + } + else + { + BtnQuiltClear.Visibility = Visibility.Visible; + ImgQuilt.Visibility = Visibility.Visible; + LabQuilt.Text = SelectedQuilt.Replace("+build", ""); + LabQuilt.Foreground = ModSecret.ColorGray1; + } + } + + // QSL + if (SelectedQuilt is null) + { + CardQSL.Visibility = Visibility.Collapsed; + } + else + { + CardQSL.Visibility = Visibility.Visible; + var qslError = LoadQSLGetError(); + CardQSL.MainSwap.Visibility = qslError is null ? Visibility.Visible : Visibility.Collapsed; + if (qslError is not null || SelectedQuilt is null) + CardQSL.IsSwapped = true; + SetPanelVisibility(PanQSLInfo, CardQSL.IsSwapped); + if (SelectedQSL is null) + { + BtnQSLClear.Visibility = Visibility.Collapsed; + ImgQSL.Visibility = Visibility.Collapsed; + LabQSL.Text = qslError ?? "可以添加"; + LabQSL.Foreground = ModSecret.ColorGray4; + } + else + { + BtnQSLClear.Visibility = Visibility.Visible; + ImgQSL.Visibility = Visibility.Visible; + LabQSL.Text = SelectedQSL.DisplayName.Split("]")[1].Trim(); + LabQSL.Foreground = ModSecret.ColorGray1; + } + } + + // LabyMod + if (VanillaDrop < 80) + { + CardLabyMod.Visibility = Visibility.Collapsed; + } + else + { + CardLabyMod.Visibility = Visibility.Visible; + var labyModError = LoadLabyModGetError(); + CardLabyMod.MainSwap.Visibility = labyModError is null ? Visibility.Visible : Visibility.Collapsed; + if (labyModError is not null) + CardLabyMod.IsSwapped = true; + SetPanelVisibility(PanLabyModInfo, CardLabyMod.IsSwapped); + if (SelectedLabyModVersion is null) + { + BtnLabyModClear.Visibility = Visibility.Collapsed; + ImgLabyMod.Visibility = Visibility.Collapsed; + LabLabyMod.Text = labyModError ?? "可以添加"; + LabLabyMod.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLabyModClear.Visibility = Visibility.Visible; + ImgLabyMod.Visibility = Visibility.Visible; + LabLabyMod.Text = SelectedLabyModVersion; + LabLabyMod.Foreground = ModSecret.ColorGray1; + } + } + + // OptiFabric + if (SelectedFabric is null || SelectedOptiFine is null) + { + CardOptiFabric.Visibility = Visibility.Collapsed; + } + else + { + CardOptiFabric.Visibility = Visibility.Visible; + var optiFabricError = LoadOptiFabricGetError(); + CardOptiFabric.MainSwap.Visibility = optiFabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (optiFabricError is not null || SelectedFabric is null) + CardOptiFabric.IsSwapped = true; + SetPanelVisibility(PanOptiFabricInfo, CardOptiFabric.IsSwapped); + if (SelectedOptiFabric is null) + { + BtnOptiFabricClear.Visibility = Visibility.Collapsed; + ImgOptiFabric.Visibility = Visibility.Collapsed; + LabOptiFabric.Text = optiFabricError ?? "可以添加"; + LabOptiFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnOptiFabricClear.Visibility = Visibility.Visible; + ImgOptiFabric.Visibility = Visibility.Visible; + LabOptiFabric.Text = SelectedOptiFabric.DisplayName.ToLower().Replace("optifabric-", "") + .Replace(".jar", "").Trim().TrimStart('v'); + LabOptiFabric.Foreground = ModSecret.ColorGray1; + } + } + + // 主警告 + if (SelectedFabric is not null && SelectedFabricApi is null) + HintFabricAPI.Visibility = Visibility.Visible; + else + HintFabricAPI.Visibility = Visibility.Collapsed; + if (SelectedLegacyFabric is not null && SelectedLegacyFabricApi is null) + HintLegacyFabricAPI.Visibility = Visibility.Visible; + else + HintLegacyFabricAPI.Visibility = Visibility.Collapsed; + if (SelectedQuilt is not null && SelectedQSL is null && SelectedFabricApi is null) + HintQSL.Visibility = Visibility.Visible; + else + HintQSL.Visibility = Visibility.Collapsed; + if (SelectedQuilt is not null && SelectedFabricApi is not null && ModDownload.DlQSLLoader.Output is not null) + foreach (var Version in ModDownload.DlQSLLoader.Output) + { + if (IsSuitableQSL(Version.GameVersions, _vanillaName)) + { + HintQuiltFabricAPI.Visibility = Visibility.Visible; + break; + } + + HintQuiltFabricAPI.Visibility = Visibility.Collapsed; + } + else + HintQuiltFabricAPI.Visibility = Visibility.Collapsed; + + if (SelectedFabric is not null | SelectedLegacyFabric is not null && SelectedOptiFine is not null && + SelectedOptiFabric is null) + { + if (VanillaDrop >= 140 && VanillaDrop <= 150) + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + HintOptiFabricOld.Visibility = Visibility.Visible; + } + else if (SelectedLegacyFabric is not null) + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Visible; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + } + else + { + HintOptiFabric.Visibility = Visibility.Visible; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + } + } + else + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + } + + if (VanillaDrop >= 160 && SelectedOptiFine is not null && + (SelectedForge is not null || SelectedFabric is not null)) + HintModOptiFine.Visibility = Visibility.Visible; + else + HintModOptiFine.Visibility = Visibility.Collapsed; + // 结束 + _ReloadSelected_Ongoing = false; + } + + /// + /// 清空已选择的项目。 + /// + private void ClearSelected() + { + _vanillaName = null; + _vanillaData = null; + _vanillaIcon = null; + SelectedOptiFine = null; + SelectedLiteLoader = null; + SelectedLoaderName = null; + SelectedAPIName = null; + SelectedForge = null; + SelectedNeoForge = null; + SelectedCleanroom = null; + SelectedFabric = null; + SelectedFabricApi = null; + SelectedQuilt = null; + SelectedQSL = null; + SelectedOptiFabric = null; + SelectedLabyModCommitRef = null; + SelectedLabyModVersion = null; + SelectedLabyModChannel = null; + SelectedLegacyFabric = null; + SelectedLegacyFabricApi = null; + } + + // 信息栏动画 + private void SetPanelVisibility(Grid panel, bool visible) + { + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(panel.Tag, visible.ToString(), false))) + return; + panel.Tag = visible.ToString(); + if (visible) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaTranslateY(panel, -((TranslateTransform)panel.RenderTransform).Y, 150, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaOpacity(panel, 1d - panel.Opacity, 60) + }, "PageDownloadInstall Visibility " + panel.Name); + else + ModAnimation.AniStart( + new[] + { + ModAnimation.AaTranslateY(panel, 6d - ((TranslateTransform)panel.RenderTransform).Y, 60), + ModAnimation.AaOpacity(panel, -panel.Opacity, 60) + }, "PageDownloadInstall Visibility " + panel.Name); + } + + /// + /// 获取实例图标。 + /// + private string GetSelectLogo() + { + if (SelectedFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png"; + + if (SelectedLegacyFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png"; + + if (SelectedForge is not null) return "pack://application:,,,/images/Blocks/Anvil.png"; + + if (SelectedNeoForge is not null) return "pack://application:,,,/images/Blocks/NeoForge.png"; + + if (SelectedLiteLoader is not null) return "pack://application:,,,/images/Blocks/Egg.png"; + + if (SelectedOptiFine is not null) return "pack://application:,,,/images/Blocks/GrassPath.png"; + + if (SelectedQuilt is not null) return "pack://application:,,,/images/Blocks/Quilt.png"; + + if (SelectedCleanroom is not null) return "pack://application:,,,/images/Blocks/Cleanroom.png"; + + if (SelectedLabyModVersion is not null) return "pack://application:,,,/images/Blocks/LabyMod.png"; + + return _vanillaIcon; + } + + // 实例名处理 + /// + /// 获取默认实例名。 + /// + private string GetSelectName() + { + var name = _vanillaName; + if (SelectedFabric is not null) name += "-Fabric_" + SelectedFabric.Replace("+build", ""); + if (SelectedLegacyFabric is not null) name += "-LegacyFabric_" + SelectedLegacyFabric; + if (SelectedQuilt is not null) name += "-Quilt_" + SelectedQuilt; + if (SelectedLabyModVersion is not null) + name += "-LabyMod_" + SelectedLabyModVersion.Replace(" 稳定版", "_Production").Replace(" 快照版", "_Snapshot"); + if (SelectedForge is not null) name += "-Forge_" + SelectedForge.VersionName; + if (SelectedNeoForge is not null) name += "-NeoForge_" + SelectedNeoForge.VersionName; + if (SelectedCleanroom is not null) name += "-Cleanroom_" + SelectedCleanroom.VersionName; + if (SelectedLiteLoader is not null) name += "-LiteLoader"; + if (SelectedOptiFine is not null) + name += "-OptiFine_" + SelectedOptiFine.DisplayName.Replace(_vanillaName + " ", "").Replace(" ", "_"); + return name; + } + + private bool IsSelectNameEdited; + private bool IsSelectNameChanging; + + private void SelectNameUpdate() + { + if (IsSelectNameEdited || IsSelectNameChanging) + return; + IsSelectNameChanging = true; + TextSelectName.Text = GetSelectName(); + IsSelectNameChanging = false; + } + + private void TextSelectName_TextChanged(object sender, TextChangedEventArgs e) + { + if (IsSelectNameChanging) + return; + IsSelectNameEdited = true; + ReloadSelected(); + } + + private void TextSelectName_ValidateChanged(object sender, EventArgs e) + { + BtnStart.IsEnabled = TextSelectName.IsValidated; + } + + #endregion + + #region 加载器 + + // 结果数据化 + private void LoadMinecraft_OnFinish() + { + ExitSelectPage(); // 返回 + do + { + try + { + var Dict = new Dictionary> + { + { "正式版", new List() }, { "预览版", new List() }, { "远古版", new List() }, + { "愚人节版", new List() } + }; + var Versions = (JArray)ModDownload.DlClientListLoader.Output.Value["versions"]; + foreach (JObject Version in Versions) + { + // 确定分类 + var Type = Version["type"].ToString(); + var versionId = Version["id"].ToString().ToLower(); + switch (Type ?? "") + { + case "release": + { + Type = "正式版"; + break; + } + case "snapshot": + case "pending": + { + Type = "预览版"; + // Mojang 误分类 + if (versionId.StartsWith("1.") && !versionId.Contains("combat") && + !versionId.Contains("rc") && !versionId.Contains("experimental") && + !versionId.Equals("1.2") && !versionId.Contains("pre")) + { + Type = "正式版"; + Version["type"] = "release"; + } + + // 愚人节版本 + switch (Version["id"].ToString().ToLower() ?? "") + { + case "2point0_blue": + case "2point0_red": + case "2point0_purple": + case "2.0_blue": + case "2.0_red": + case "2.0_purple": + case "2.0": + { + Type = "愚人节版"; + Version["id"] = Version["id"].ToString().Replace("point", "."); + Version["type"] = "special"; + Version.Add("lore", ModMinecraft.GetMcFoolName((string)Version["id"])); + break; + } + case "20w14infinite": + case "20w14∞": + { + Type = "愚人节版"; + Version["id"] = "20w14∞"; + Version["type"] = "special"; + Version.Add("lore", ModMinecraft.GetMcFoolName((string)Version["id"])); + break; + } + case "3d shareware v1.34": + case "1.rv-pre1": + case "15w14a": + case var @case when @case == "2.0": + case "22w13oneblockatatime": + case "23w13a_or_b": + case "24w14potato": + case "25w14craftmine": + { + Type = "愚人节版"; + Version["type"] = "special"; + Version.Add("lore", + ModMinecraft.GetMcFoolName((string)Version["id"])); // 4/1 自动视作愚人节版 + break; + } + + default: + { + var ReleaseDate = Version["releaseTime"].Value().ToUniversalTime() + .AddHours(2d); + if (ReleaseDate.Month == 4 && ReleaseDate.Day == 1) + { + Type = "愚人节版"; + Version["type"] = "special"; + } + + break; + } + } + + break; + } + case "special": + { + // 已被处理的愚人节版 + Type = "愚人节版"; + break; + } + + default: + { + Type = "远古版"; + break; + } + } + + // 加入辞典 + Dict[Type].Add(Version); + } + + // 排序 + foreach (var Pair in Dict.ToList()) + Dict[Pair.Key] = Pair.Value.OrderByDescending(j => j["releaseTime"].Value()).ToList(); + // 清空当前 + PanMinecraft.Children.Clear(); + // 添加最新版本 + var CardInfo = new MyCard { Title = "最新版本", Margin = new Thickness(0d, 15d, 0d, 15d) }; + var TopestVersions = new List(); + var Release = (JObject)Dict["正式版"][0].DeepClone(); + Release["lore"] = "最新正式版,发布于 " + + Release["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm"); + TopestVersions.Add(Release); + if (Dict["正式版"][0]["releaseTime"].Value() < Dict["预览版"][0]["releaseTime"].Value()) + { + var Snapshot = (JObject)Dict["预览版"][0].DeepClone(); + Snapshot["lore"] = "最新预览版,发布于 " + + Snapshot["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm"); + TopestVersions.Add(Snapshot); + } + + var PanInfo = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = TopestVersions + }; + + void StackInstall(StackPanel Stack) + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.McDownloadListItem((JObject)item, + (sender, e) => ModMain.FrmDownloadInstall.MinecraftSelected((MyListItem)sender, e), false)); + } + + ; + MyCard.StackInstall(ref PanInfo, StackInstall); + CardInfo.Children.Add(PanInfo); + PanMinecraft.Children.Insert(0, CardInfo); + // 添加其他版本 + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + // 不能使用 AddressOf,这导致了 #535,原因完全不明,疑似是编译器 Bug + NewCard.InstallMethod = StackInstall; + NewCard.IsSwapped = true; + PanMinecraft.Children.Add(NewCard); + } + + // 自动选择版本 + if (McVersionWaitingForSelect is null) + break; + ModBase.Log("[Download] 自动选择 MC 版本:" + McVersionWaitingForSelect); + foreach (JObject Version in Versions) + { + if ((Version["id"].ToString() ?? "") != (McVersionWaitingForSelect ?? "")) + continue; + var Item = ModDownloadLib.McDownloadListItem(Version, (_, _) => { }, false); + MinecraftSelected(Item, null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化安装版本列表出错", ModBase.LogLevel.Feedback); + } + } while (false); + } + + /// + /// 当 MC 版本列表加载完时,立即自动选择的版本。用于外部调用。 + /// + public static string McVersionWaitingForSelect = null; + + #endregion + + #region OptiFine 列表 + + /// + /// 获取 OptiFine 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadOptiFineGetError() + { + if (SelectedLoaderName == "NeoForge" || SelectedLoaderName == "Quilt" || SelectedLoaderName == "LabyMod") + return $"与 {SelectedLoaderName} 不兼容"; + if (LoadOptiFine is null || LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadOptiFine.State).Error.Message)); + // 是否有 Cleanroom + if (SelectedCleanroom is not null) + return "与 Cleanroom 不兼容"; + // 检查 Forge 1.13 - 1.14.3:全部不兼容 + if (SelectedLoaderName == "Forge" && ModMinecraft.CompareVersion(_vanillaName, "1.13") >= 0 && + ModMinecraft.CompareVersion("1.14.3", _vanillaName) >= 0) return "与 Forge 不兼容"; + // 检查 Fabric 1.20.5+: 全部不兼容 + if (SelectedFabric is not null && ModMinecraft.CompareVersion(_vanillaName, "1.20.4") > 0) + return "与 Fabric 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadOptiFine) is not null) + return GetLoaderError(LoadOptiFine); + // 检查 Forge 版本 + var HasAny = false; + var HasRequiredVersion = false; + foreach (var OptiFineVersion in ModDownload.DlOptiFineListLoader.Output.Value) + { + if (!OptiFineVersion.DisplayName.StartsWith(_vanillaName + " ")) + continue; // 不是同一个大版本 + HasAny = true; + if (SelectedForge is null) + return null; // 未选择 Forge + if (Conversions.ToBoolean(IsOptiFineSuitForForge(OptiFineVersion, SelectedForge))) + return null; // 该版本可用 + if (OptiFineVersion.RequiredForgeVersion is not null) + HasRequiredVersion = true; + } + + if (!HasAny) return "无可用版本"; + + if (HasRequiredVersion) return "仅兼容特定版本的 Forge"; + + return "与 Forge 不兼容"; + } + + // 检查某个 OptiFine 是否与某个 Forge 兼容 + private object IsOptiFineSuitForForge(ModDownload.DlOptiFineListEntry OptiFine, + ModDownload.DlForgeVersionEntry Forge) + { + if ((Forge.Inherit ?? "") != (OptiFine.Inherit ?? "")) + return false; // 不是同一个大版本 + if (OptiFine.RequiredForgeVersion is null) + return false; // 不兼容 Forge + if (string.IsNullOrWhiteSpace(OptiFine.RequiredForgeVersion)) + return true; // #4183 + if (OptiFine.RequiredForgeVersion.Contains(".")) // XX.X.XXX + return ModMinecraft.CompareVersion(Forge.Version.ToString(), OptiFine.RequiredForgeVersion) == 0; + + // XXXX + return Forge.Version.Revision == Conversions.ToDouble(OptiFine.RequiredForgeVersion); + } + + // 限制展开 + private void CardOptiFine_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadOptiFineGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 OptiFine 版本列表。 + /// + private void OptiFine_Loaded() + { + try + { + if (ModDownload.DlOptiFineListLoader.State != ModBase.LoadState.Finished) + return; + + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlOptiFineListLoader.Output.Value) + { + if (Conversions.ToBoolean(SelectedForge is not null && + !(bool)IsOptiFineSuitForForge(Version, SelectedForge))) + continue; + if (Version.DisplayName.StartsWith(_vanillaName + " ")) + Versions.Add(Version); + } + + if (!Versions.Any()) + return; + // 排序 + Versions.Sort((Left, Right) => + { + if (!Left.IsPreview && Right.IsPreview) + return true; + if (Left.IsPreview && !Right.IsPreview) + return false; + return ModMinecraft.CompareVersionGe(Left.DisplayName, Right.DisplayName); + }); + // 可视化 + PanOptiFine.Children.Clear(); + foreach (var Version in Versions) + PanOptiFine.Children.Add( + ModDownloadLib.OptiFineDownloadListItem(Version, (a, b) => this.OptiFine_Selected((dynamic)a, b), + false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 OptiFine 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void OptiFine_Selected(MyListItem sender, EventArgs e) + { + SelectedOptiFine = (ModDownload.DlOptiFineListEntry)(dynamic)sender.Tag; + if (Conversions.ToBoolean(SelectedForge is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, SelectedForge))) + SelectedForge = null; + OptiFabric_Loaded(); + Forge_Loaded(); + NeoForge_Loaded(); + CardOptiFine.IsSwapped = true; + ReloadSelected(); + } + + private void OptiFine_Clear(object sender, MouseButtonEventArgs e) + { + SelectedOptiFine = null; + SelectedOptiFabric = null; + AutoSelectedOptiFabric = false; + CardOptiFine.IsSwapped = true; + e.Handled = true; + Forge_Loaded(); + NeoForge_Loaded(); + ReloadSelected(); + } + + #endregion + + #region LiteLoader 列表 + + /// + /// 获取 LiteLoader 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLiteLoaderGetError() + { + // 检查 Loader + if (GetLoaderError(LoadLiteLoader) is not null) + return GetLoaderError(LoadLiteLoader); + // 检查版本 + return ModDownload.DlLiteLoaderListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardLiteLoader_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLiteLoaderGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LiteLoader 版本列表。 + /// + private void LiteLoader_Loaded() + { + try + { + if (ModDownload.DlLiteLoaderListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlLiteLoaderListLoader.Output.Value) + if ((Version.Inherit ?? "") == (_vanillaName ?? "")) + Versions.Add(Version); + if (!Versions.Any()) + return; + // 可视化 + PanLiteLoader.Children.Clear(); + foreach (var Version in Versions) + PanLiteLoader.Children.Add(ModDownloadLib.LiteLoaderDownloadListItem(Version, + (a, b) => this.LiteLoader_Selected((dynamic)a, b), false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LiteLoader 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void LiteLoader_Selected(MyListItem sender, EventArgs e) + { + SelectedLiteLoader = (ModDownload.DlLiteLoaderListEntry)(dynamic)sender.Tag; + CardLiteLoader.IsSwapped = true; + ReloadSelected(); + } + + private void LiteLoader_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLiteLoader = null; + CardLiteLoader.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Forge 列表 + + /// + /// 获取 Forge 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadForgeGetError() + { + if (ModMinecraft.CompareVersionGe("1.5.1", _vanillaName) && ModMinecraft.CompareVersionGe(_vanillaName, "1.1")) + return "无可用版本"; + // 检查 Loader + if (GetLoaderError(LoadForge) is not null) + return GetLoaderError(LoadForge); + var loader = (ModLoader.LoaderTask>)LoadForge.State; + if ((_vanillaName ?? "") != (loader.Input ?? "")) + return "获取中……"; + // 检查版本 + foreach (var Version in loader.Output) + { + if (Version.Category == "universal" || Version.Category == "client") + continue; // 跳过无法自动安装的版本 + if (SelectedNeoForge is not null || SelectedFabric is not null || SelectedQuilt is not null) + return $"与 {SelectedLoaderName} 不兼容"; + if (SelectedOptiFine is not null && ModMinecraft.CompareVersionGe(_vanillaName, "1.13") && + ModMinecraft.CompareVersionGe("1.14.3", _vanillaName)) + return "与 OptiFine 不兼容"; // 1.13 ~ 1.14.3 OptiFine 检查 + if (Conversions.ToBoolean( + SelectedOptiFine is not null && !(bool)IsOptiFineSuitForForge(SelectedOptiFine, Version))) + continue; + return null; + } + + return "与 OptiFine 不兼容"; + } + + // 限制展开 + private void CardForge_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadForgeGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Forge 版本列表。 + /// + private void Forge_Loaded() + { + try + { + if (!LoadForge.State.IsLoader) + return; + var loader = (ModLoader.LoaderTask>)LoadForge.State; + if ((_vanillaName ?? "") != (loader.Input ?? "")) + return; + if (loader.State != ModBase.LoadState.Finished) + return; + // 获取要显示的版本 + var versions = loader.Output.ToList(); // 复制数组,以免 Output 在实例化后变空 + if (!loader.Output.Any()) + return; + PanForge.Children.Clear(); + versions = versions.Where(v => + { + if (v.Category == "universal" || v.Category == "client") + return false; // 跳过无法自动安装的版本 + if (Conversions.ToBoolean(SelectedOptiFine is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, v))) + return false; + return true; + }).OrderByDescending(v => v).ToList(); + ModDownloadLib.ForgeDownloadListItemPreload(PanForge, versions, + (a, b) => this.Forge_Selected((dynamic)a, b), false); + foreach (var Version in versions) + PanForge.Children.Add( + ModDownloadLib.ForgeDownloadListItem(Version, (a, b) => this.Forge_Selected((dynamic)a, b), false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Forge 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void Forge_Selected(MyListItem sender, EventArgs e) + { + SelectedForge = (ModDownload.DlForgeVersionEntry)(dynamic)sender.Tag; + SelectedLoaderName = "Forge"; + CardForge.IsSwapped = true; + if (Conversions.ToBoolean(SelectedOptiFine is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, SelectedForge))) + SelectedOptiFine = null; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void Forge_Clear(object sender, MouseButtonEventArgs e) + { + SelectedForge = null; + SelectedLoaderName = null; + CardForge.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region NeoForge 列表 + + /// + /// 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadNeoForgeGetError() + { + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "NeoForge")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadNeoForge) is not null) + return GetLoaderError(LoadNeoForge); + // 检查版本 + return ModDownload.DlNeoForgeListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardNeoForge_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadNeoForgeGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 NeoForge 版本列表。 + /// + private void NeoForge_Loaded() + { + try + { + // 获取版本列表 + if (ModDownload.DlNeoForgeListLoader.State != ModBase.LoadState.Finished) + return; + var Versions = ModDownload.DlNeoForgeListLoader.Output.Value + .Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList(); + if (!Versions.Any()) + return; + // 可视化 + PanNeoForge.Children.Clear(); + ModDownloadLib.NeoForgeDownloadListItemPreload(PanNeoForge, Versions, + (a, b) => this.NeoForge_Selected((dynamic)a, b), + false); + foreach (var Version in Versions) + PanNeoForge.Children.Add( + ModDownloadLib.NeoForgeDownloadListItem(Version, (a, b) => this.NeoForge_Selected((dynamic)a, b), + false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 NeoForge 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void NeoForge_Selected(MyListItem sender, EventArgs e) + { + SelectedNeoForge = (ModDownload.DlNeoForgeListEntry)(dynamic)sender.Tag; + SelectedLoaderName = "NeoForge"; + CardNeoForge.IsSwapped = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void NeoForge_Clear(object sender, MouseButtonEventArgs e) + { + SelectedNeoForge = null; + SelectedLoaderName = null; + CardNeoForge.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region Cleanroom 列表 + + /// + /// 获取 Cleanroom 的加载异常信息。若正常则返回 Nothing。 + /// + private string? LoadCleanroomGetError() + { + if (!_vanillaName.StartsWith("1.")) + return "没有可用版本"; + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Cleanroom")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadCleanroom) is not null) + return GetLoaderError(LoadNeoForge); + // 检查版本 + return ModDownload.DlCleanroomListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardCleanroom_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadCleanroomGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Cleanroom 版本列表。 + /// + private void Cleanroom_Loaded() + { + try + { + // 获取版本列表 + if (ModDownload.DlCleanroomListLoader.State != ModBase.LoadState.Finished) + return; + var Versions = ModDownload.DlCleanroomListLoader.Output.Value + .Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList(); + if (!Versions.Any()) + return; + // 可视化 + PanCleanroom.Children.Clear(); + ModDownloadLib.CleanroomDownloadListItemPreload(PanCleanroom, Versions, + (a, b) => this.Cleanroom_Selected((dynamic)a, b), false); + foreach (var Version in Versions) + PanCleanroom.Children.Add( + ModDownloadLib.CleanroomDownloadListItem(Version, (a, b) => this.Cleanroom_Selected((dynamic)a, b), + false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Cleanroom 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void Cleanroom_Selected(MyListItem sender, EventArgs e) + { + SelectedCleanroom = (ModDownload.DlCleanroomListEntry)(dynamic)sender.Tag; + SelectedLoaderName = "Cleanroom"; + CardCleanroom.IsSwapped = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void Cleanroom_Clear(object sender, MouseButtonEventArgs e) + { + SelectedCleanroom = null; + SelectedLoaderName = null; + CardCleanroom.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region Fabric 列表 + + /// + /// 获取 Fabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadFabricGetError() + { + // 检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容 + if (SelectedOptiFine is not null && ModMinecraft.CompareVersionGe(_vanillaName, "1.20.5")) + return "与 OptiFine 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadFabric) is not null) + return GetLoaderError(LoadFabric); + // 检查版本 + foreach (JObject version in ModDownload.DlFabricListLoader.Output.Value["game"]) + if ((version["version"].ToString() ?? "") == + (_vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") ?? "")) + { + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Fabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Fabric 版本列表。 + /// + private void Fabric_Loaded() + { + try + { + if (ModDownload.DlFabricListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var versions = (JArray)ModDownload.DlFabricListLoader.Output.Value["loader"]; + if (!versions.Any()) + return; + // 可视化 + PanFabric.Children.Clear(); + PanFabric.Tag = versions; + CardFabric.SwapControl = PanFabric; + CardFabric.InstallMethod = stack => + { + foreach (var item in (IEnumerable)stack.Tag) + stack.Children.Add( + ModDownloadLib.FabricDownloadListItem((JObject)item, + (a, b) => this.Fabric_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Fabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void Fabric_Selected(MyListItem sender, EventArgs e) + { + ModBase.Log(((dynamic)sender.Tag).ToString()); + SelectedFabric = ((dynamic)sender.Tag)["version"].ToString(); + SelectedLoaderName = "Fabric"; + FabricApi_Loaded(); + OptiFabric_Loaded(); + CardFabric.IsSwapped = true; + ReloadSelected(); + } + + private void Fabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedFabric = null; + SelectedFabricApi = null; + AutoSelectedFabricApi = false; + SelectedOptiFabric = null; + AutoSelectedOptiFabric = false; + SelectedLoaderName = null; + SelectedAPIName = null; + CardFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Fabric API 列表 + + /// + /// 判断某 Fabric API 是否适配当前选择的原版版本。 + /// + public bool IsFabricApiCompatible(ModComp.CompFile fabricApi) + { + var fabricApiName = fabricApi.DisplayName; + try + { + if (fabricApiName is null || _vanillaName is null) + return false; + fabricApiName = fabricApiName.ToLower(); + _vanillaName = _vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").ToLower(); + if (fabricApiName.StartsWith("[" + _vanillaName + "]")) + return true; + if (!fabricApiName.Contains("/") || !fabricApiName.Contains("]")) + return false; + // 直接的判断(例如 1.18.1/22w03a) + foreach (var part in fabricApiName.BeforeFirst("]").TrimStart('[').Split("/")) + if ((part ?? "") == (_vanillaName ?? "")) + return true; + // 将版本名分割语素(例如 1.16.4/5) + var lefts = fabricApiName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+"); + var rights = _vanillaName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+"); + // 对每段进行判断 + var i = 0; + while (true) + { + // 两边均缺失,感觉是一个东西 + if (lefts.Count - 1 < i && rights.Count - 1 < i) + return true; + // 确定两边是否一致 + var leftValue = lefts.Count - 1 < i ? "-1" : lefts[i]; + var rightValue = rights.Count - 1 < i ? "-1" : rights[i]; + if (!leftValue.Contains("/")) + { + if ((leftValue ?? "") != (rightValue ?? "")) + return false; + } + // 左边存在斜杠 + else if (!leftValue.Contains(rightValue)) + { + return false; + } + + i += 1; + } + + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 Fabric API 版本适配性出错(" + fabricApiName + ", " + _vanillaName + ")"); + return false; + } + } + + /// + /// 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadFabricApiGetError() + { + // 检查 Loader + if (GetLoaderError(LoadFabricApi) is not null) + return GetLoaderError(LoadFabricApi); + if (ModDownload.DlFabricApiLoader.Output is null) + return SelectedFabric is null && SelectedQuilt is null ? "需要安装 Fabric" : "获取中……"; + // 检查版本 + if (ModDownload.DlFabricApiLoader.Output.Any(f => IsFabricApiCompatible(f))) + return SelectedFabric is null && SelectedQuilt is null ? "需要安装 Fabric" : null; + + return "无可用版本"; + } + + // 限制展开 + private void CardFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadFabricApiGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedFabricApi; + + /// + /// 尝试重新可视化 FabricApi 版本列表。 + /// + private void FabricApi_Loaded() + { + try + { + if (ModDownload.DlFabricApiLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || (SelectedFabric is null && SelectedQuilt is null)) + return; + // 获取版本列表 + var versions = new List(); + foreach (var version in ModDownload.DlFabricApiLoader.Output) + if (IsFabricApiCompatible(version)) + { + if (!version.DisplayName.StartsWith("[")) + { + ModBase.Log("[Download] 已特判修改 Fabric API 显示名:" + version.DisplayName, ModBase.LogLevel.Debug); + version.DisplayName = "[" + _vanillaName + "] " + version.DisplayName; + } + + versions.Add(version); + } + + if (!versions.Any()) + return; + versions = versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanFabricApi.Children.Clear(); + foreach (var version in versions) + { + if (!IsFabricApiCompatible(version)) + continue; + PanFabricApi.Children.Add( + ModDownloadLib.FabricApiDownloadListItem(version, (a, b) => this.Fabric_Selected((dynamic)a, b))); + } + + // 自动选择 Fabric API + if ((!AutoSelectedFabricApi && SelectedQuilt is null) || + (SelectedQuilt is not null && ReferenceEquals(LoadQSLGetError(), "没有可用版本"))) + { + AutoSelectedFabricApi = true; + ModBase.Log($"[Download] 已自动选择 Fabric API:{((MyListItem)PanFabricApi.Children[0]).Title}"); + FabricApi_Selected((MyListItem)PanFabricApi.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Fabric API 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void FabricApi_Selected(MyListItem sender, EventArgs e) + { + SelectedFabricApi = (ModComp.CompFile)(dynamic)sender.Tag; + SelectedAPIName = "Fabric API"; + CardFabricApi.IsSwapped = true; + ReloadSelected(); + } + + private void FabricApi_Clear(object sender, MouseButtonEventArgs e) + { + SelectedFabricApi = null; + SelectedAPIName = null; + CardFabricApi.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region LegacyFabric 列表 + + /// + /// 获取 LegacyFabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLegacyFabricGetError() + { + if (LoadLegacyFabric is null || LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLegacyFabric.State).Error.Message)); + foreach (JObject Version in ModDownload.DlLegacyFabricListLoader.Output.Value["game"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + if (SelectedLiteLoader is not null) + return "与 LiteLoader 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "LegacyFabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardLegacyFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLegacyFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LegacyFabric 版本列表。 + /// + private void LegacyFabric_Loaded() + { + try + { + if (ModDownload.DlLegacyFabricListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = (JArray)ModDownload.DlLegacyFabricListLoader.Output.Value["loader"]; + if (!Versions.Any()) + return; + // 可视化 + PanLegacyFabric.Children.Clear(); + PanLegacyFabric.Tag = Versions; + CardLegacyFabric.SwapControl = PanLegacyFabric; + CardLegacyFabric.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.LegacyFabricDownloadListItem((JObject)item, + (a, b) => this.LegacyFabric_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LegacyFabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void LegacyFabric_Selected(MyListItem sender, EventArgs e) + { + SelectedLegacyFabric = ((dynamic)sender.Tag)["version"].ToString(); + SelectedLoaderName = "LegacyFabric"; + LegacyFabricApi_Loaded(); + CardLegacyFabric.IsSwapped = true; + ReloadSelected(); + } + + private void LegacyFabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLegacyFabric = null; + SelectedLegacyFabricApi = null; + AutoSelectedLegacyFabricApi = false; + SelectedLoaderName = null; + SelectedAPIName = null; + CardLegacyFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Legacy Fabric API 列表 + + /// + /// 从显示名判断该 API 是否与某版本适配。 + /// + public static bool IsSuitableLegacyFabricApi(List SupportVersions, string MinecraftVersion) + { + try + { + if (SupportVersions.Contains(MinecraftVersion)) return true; + + return false; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 Legacy Fabric API 版本适配性出错(" + SupportVersions + ", " + MinecraftVersion + ")"); + return false; + } + } + + /// + /// 获取 LegacyFabricApi 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLegacyFabricApiGetError() + { + if (LoadLegacyFabricApi is null || LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLegacyFabricApi.State).Error.Message)); + if (SelectedAPIName is not null && !ReferenceEquals(SelectedAPIName, "Legacy Fabric API")) + return $"与 {SelectedAPIName} 不兼容"; + if (ModDownload.DlLegacyFabricApiLoader.Output is null) + { + if (SelectedLegacyFabric is null) + return "需要安装 LegacyFabric"; + return "加载中……"; + } + + foreach (var Version in ModDownload.DlLegacyFabricApiLoader.Output) + { + if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + continue; + if (SelectedLegacyFabric is null) + return "需要安装 LegacyFabric"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardLegacyFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLegacyFabricApiGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedLegacyFabricApi; + + /// + /// 尝试重新可视化 LegacyFabricApi 版本列表。 + /// + private void LegacyFabricApi_Loaded() + { + try + { + if (ModDownload.DlLegacyFabricApiLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || (SelectedLegacyFabric is null && SelectedQuilt is null)) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlLegacyFabricApiLoader.Output) + if (IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + Versions.Add(Version); + + if (!Versions.Any()) + return; + Versions = Versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanLegacyFabricApi.Children.Clear(); + foreach (var Version in Versions) + { + if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + continue; + PanLegacyFabricApi.Children.Add( + ModDownloadLib.LegacyFabricApiDownloadListItem(Version, + (a, b) => this.LegacyFabricApi_Selected((dynamic)a, b))); + } + + // 自动选择 Legacy Fabric API + if ((!AutoSelectedLegacyFabricApi && SelectedQuilt is null) || + (SelectedQuilt is not null && ReferenceEquals(LoadQSLGetError(), "没有可用版本"))) + { + AutoSelectedLegacyFabricApi = true; + ModBase.Log($"[Download] 已自动选择 Legacy Fabric API:{((MyListItem)PanLegacyFabricApi.Children[0]).Title}"); + LegacyFabricApi_Selected((MyListItem)PanLegacyFabricApi.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Legacy Fabric API 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void LegacyFabricApi_Selected(MyListItem sender, EventArgs e) + { + SelectedLegacyFabricApi = (ModComp.CompFile)(dynamic)sender.Tag; + SelectedAPIName = "Legacy Fabric API"; + CardLegacyFabricApi.IsSwapped = true; + ReloadSelected(); + } + + private void LegacyFabricApi_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLegacyFabricApi = null; + SelectedAPIName = null; + CardLegacyFabricApi.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Quilt 列表 + + /// + /// 获取 Quilt 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadQuiltGetError() + { + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Quilt")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadQuilt) is not null) + return GetLoaderError(LoadQuilt); + // 检查版本 + foreach (JObject version in ModDownload.DlQuiltListLoader.Output.Value["game"]) + if ((version["version"].ToString() ?? "") == + (_vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") ?? "")) + { + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Fabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardQuilt_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadQuiltGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Quilt 版本列表。 + /// + private void Quilt_Loaded() + { + try + { + if (ModDownload.DlQuiltListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = (JArray)ModDownload.DlQuiltListLoader.Output.Value["loader"]; + if (!Versions.Any()) + return; + // 可视化 + PanQuilt.Children.Clear(); + PanQuilt.Tag = Versions; + CardQuilt.SwapControl = PanQuilt; + CardQuilt.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add( + ModDownloadLib.QuiltDownloadListItem((JObject)item, + (a, b) => this.Quilt_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Quilt 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void Quilt_Selected(MyListItem sender, EventArgs e) + { + SelectedQuilt = ((dynamic)sender.Tag)["version"].ToString(); + SelectedLoaderName = "Quilt"; + FabricApi_Loaded(); + QSL_Loaded(); + CardQuilt.IsSwapped = true; + ReloadSelected(); + } + + private void Quilt_Clear(object sender, MouseButtonEventArgs e) + { + SelectedQuilt = null; + SelectedQSL = null; + SelectedFabricApi = null; + SelectedLoaderName = null; + SelectedAPIName = null; + CardQuilt.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region QSL 列表 + + /// + /// 从显示名判断该 API 是否与某版本适配。 + /// + public static bool IsSuitableQSL(List SupportVersions, string MinecraftVersion) + { + try + { + if (SupportVersions.Contains(MinecraftVersion)) return true; + + return false; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 QSL 版本适配性出错(" + SupportVersions + ", " + MinecraftVersion + ")"); + return false; + } + } + + /// + /// 获取 QSL 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadQSLGetError() + { + if (LoadQSL is null || LoadQSL.State.LoadingState == MyLoading.MyLoadingState.Run) + return "正在获取版本列表……"; + if (LoadQSL.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadQSL.State).Error.Message)); + if (SelectedAPIName is not null && !ReferenceEquals(SelectedAPIName, "QFAPI / QSL")) + return $"与 {SelectedAPIName} 不兼容"; + if (ModDownload.DlQSLLoader.Output is null) + { + if (SelectedQuilt is null) + return "需要安装 Quilt"; + return "正在获取版本列表……"; + } + + foreach (var Version in ModDownload.DlQSLLoader.Output) + { + if (!IsSuitableQSL(Version.GameVersions, _vanillaName)) + continue; + if (SelectedQuilt is null) + return "需要安装 Quilt"; + return null; + } + + return "没有可用版本"; + } + + // 限制展开 + private void CardQSL_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadQSLGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedQSL; + + /// + /// 尝试重新可视化 QSL 版本列表。 + /// + private void QSL_Loaded() + { + try + { + if (ModDownload.DlQSLLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || SelectedQuilt is null) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlQSLLoader.Output) + if (IsSuitableQSL(Version.GameVersions, _vanillaName)) + { + if (!Version.DisplayName.StartsWith("[")) + { + ModBase.Log("[Download] 已特判修改 QSL 显示名:" + Version.DisplayName, ModBase.LogLevel.Debug); + Version.DisplayName = "[" + _vanillaName + "] " + Version.DisplayName; + } + + Versions.Add(Version); + } + + if (!Versions.Any()) + return; + Versions = Versions.Sort((a, b) => a.ReleaseDate > b.ReleaseDate); + // 可视化 + PanQSL.Children.Clear(); + foreach (var Version in Versions) + { + if (!IsSuitableQSL(Version.GameVersions, _vanillaName)) + continue; + PanQSL.Children.Add( + ModDownloadLib.QSLDownloadListItem(Version, (a, b) => this.QSL_Selected((dynamic)a, b))); + } + + // 自动选择 QSL + if (!AutoSelectedQSL) + { + AutoSelectedQSL = true; + ModBase.Log($"[Download] 已自动选择 QSL:{((MyListItem)PanQSL.Children[0]).Title}"); + QSL_Selected((MyListItem)PanQSL.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 QSL 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void QSL_Selected(MyListItem sender, EventArgs e) + { + SelectedQSL = (ModComp.CompFile)(dynamic)sender.Tag; + SelectedAPIName = "QFAPI / QSL"; + CardQSL.IsSwapped = true; + ReloadSelected(); + } + + private void QSL_Clear(object sender, MouseButtonEventArgs e) + { + SelectedQSL = null; + SelectedAPIName = null; + CardQSL.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region OptiFabric 列表 + + /// + /// 判断某 OptiFabric 是否适配当前选择的原版版本。 + /// + private bool IsOptiFabricCompatible(ModComp.CompFile modFile) + { + try + { + if (_vanillaName is null) + return false; + return modFile.GameVersions.Contains(_vanillaName); + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 OptiFabric 版本适配性出错(" + _vanillaName + ")"); + return false; + } + } + + private bool AutoSelectedOptiFabric; + + /// + /// 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadOptiFabricGetError() + { + if (VanillaDrop >= 140 && VanillaDrop <= 150) + return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins"; + // 检查 Loader + if (GetLoaderError(LoadOptiFabric) is not null) + return GetLoaderError(LoadOptiFabric); + // 检查版本 + if (ModDownload.DlOptiFabricLoader.Output is null) + { + if (SelectedFabric is null && SelectedOptiFine is null) + return "需要安装 OptiFine 与 Fabric"; + if (SelectedFabric is null) + return "需要安装 Fabric"; + if (SelectedOptiFine is null) + return "需要安装 OptiFine"; + return "获取中……"; + } + + foreach (var version in ModDownload.DlOptiFabricLoader.Output) + { + if (!IsOptiFabricCompatible(version)) + continue; // 2135# + if (SelectedFabric is null && SelectedOptiFine is null) + return "需要安装 OptiFine 与 Fabric"; + if (SelectedFabric is null) + return "需要安装 Fabric"; + if (SelectedOptiFine is null) + return "需要安装 OptiFine"; + return null; // 通过检查 + } + + return "无可用版本"; + } + + // 限制展开 + private void CardOptiFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadOptiFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 OptiFabric 版本列表。 + /// + private void OptiFabric_Loaded() + { + try + { + if (ModDownload.DlOptiFabricLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || SelectedFabric is null || SelectedOptiFine is null) + return; + // 获取版本列表 + var versions = new List(); + foreach (var Version in ModDownload.DlOptiFabricLoader.Output) + if (IsOptiFabricCompatible(Version)) + versions.Add(Version); + if (!versions.Any()) + return; + // 排序 + versions = versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanOptiFabric.Children.Clear(); + foreach (var Version in versions) + { + if (!IsOptiFabricCompatible(Version)) + continue; + PanOptiFabric.Children.Add( + ModDownloadLib.OptiFabricDownloadListItem(Version, + (a, b) => this.OptiFabric_Selected((dynamic)a, b))); + } + + // 自动选择 OptiFabric + if (AutoSelectedOptiFabric || (VanillaDrop >= 140 && VanillaDrop <= 150)) + return; // 1.14~15 不自动选择 + AutoSelectedOptiFabric = true; + ModBase.Log($"[Download] 已自动选择 OptiFabric:{((MyListItem)PanOptiFabric.Children[0]).Title}"); + OptiFabric_Selected((MyListItem)PanOptiFabric.Children[0], null); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 OptiFabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void OptiFabric_Selected(MyListItem sender, EventArgs e) + { + SelectedOptiFabric = (ModComp.CompFile)(dynamic)sender.Tag; + CardOptiFabric.IsSwapped = true; + ReloadSelected(); + } + + private void OptiFabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedOptiFabric = null; + CardOptiFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region LabyMod 列表 + + /// + /// 获取 LabyMod 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLabyModGetError() + { + if (LoadLabyMod is null || LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLabyMod.State).Error.Message)); + // 检查 Loader + if (GetLoaderError(LoadLabyMod) is not null) + return GetLoaderError(LoadLabyMod); + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "LabyMod")) + return $"与 {SelectedLoaderName} 不兼容"; + foreach (JObject Version in ModDownload.DlLabyModListLoader.Output.Value["production"]["minecraftVersions"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + return null; + foreach (JObject Version in ModDownload.DlLabyModListLoader.Output.Value["snapshot"]["minecraftVersions"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + return null; + return "无可用版本"; + } + + // 限制展开 + private void CardLabyMod_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLabyModGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LabyMod 版本列表。 + /// + private void LabyMod_Loaded() + { + try + { + if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run) + return; + // 获取版本列表 + var Versions = ModDownload.DlLabyModListLoader.Output.Value; + if (Versions is null || Versions["production"] is null || Versions["snapshot"] is null) + return; + // 可视化 + var ProcessedVersions = new JArray(); + foreach (JObject Production in Versions["production"]["minecraftVersions"]) + if ((Production["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + var ProductionVersion = new JObject(); + ProductionVersion.Add("version", Versions["production"]["labyModVersion"]); + ProductionVersion.Add("channel", "production"); + ProductionVersion.Add("commitReference", Versions["production"]["commitReference"]); + ProcessedVersions.Add(ProductionVersion); + } + + foreach (JObject Snapshot in Versions["snapshot"]["minecraftVersions"]) + if ((Snapshot["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + var SnapshotVersion = new JObject(); + SnapshotVersion.Add("version", Versions["production"]["labyModVersion"]); + SnapshotVersion.Add("channel", "snapshot"); + SnapshotVersion.Add("commitReference", Versions["snapshot"]["commitReference"]); + ProcessedVersions.Add(SnapshotVersion); + } + + // MyMsgBox(If(ProcessedVersions.ToString, "Nothing")) + PanLabyMod.Children.Clear(); + PanLabyMod.Tag = ProcessedVersions; + CardLabyMod.SwapControl = PanLabyMod; + CardLabyMod.InstallMethod = Stack => + { + foreach (JObject item in (IEnumerable)Stack.Tag) + Stack.Children.Add( + ModDownloadLib.LabyModDownloadListItem(item, (a, b) => this.LabyMod_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LabyMod 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void LabyMod_Selected(MyListItem sender, EventArgs e) + { + SelectedLabyModChannel = ((dynamic)sender.Tag)["channel"].ToString(); + SelectedLabyModCommitRef = ((dynamic)sender.Tag)["commitReference"].ToString(); + SelectedLabyModVersion = + ((dynamic)sender.Tag)["version"].ToString() + (SelectedLabyModChannel == "snapshot" ? " 快照版" : " 稳定版"); + SelectedLoaderName = "LabyMod"; + CardLabyMod.IsSwapped = true; + ReloadSelected(); + } + + private void LabyMod_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLabyModCommitRef = null; + SelectedLabyModVersion = null; + SelectedLabyModChannel = null; + SelectedLoaderName = null; + SelectedAPIName = null; + CardLabyMod.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region 安装 + + private void TextSelectName_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter && BtnStart.IsEnabled) + BtnStart_Click(); + } + + private void BtnStart_Click() + { + // 确认版本隔离 + if (SelectedLoaderName is not null && + (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.IndieSolutionV2, 0, false)) || + Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.IndieSolutionV2, 2, false)))) + if (ModMain.MyMsgBox( + "你尚未开启版本隔离,多个 MC 实例会共用同一个 Mod 文件夹。" + "\r\n" + "因此,游戏可能会因为读取到与当前实例不符的 Mod 而崩溃。" + + "\r\n" + "推荐先在 设置 → 启动选项 → 默认版本隔离 中开启版本隔离!", "版本隔离提示", "取消下载", "继续") == 1) + return; + + // 提交安装申请 + var instanceName = TextSelectName.Text; + var request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = instanceName, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{instanceName}\", + MinecraftJson = _vanillaData["url"].ToString(), + MinecraftName = _vanillaName, + OptiFineEntry = SelectedOptiFine, + ForgeEntry = SelectedForge, + NeoForgeEntry = SelectedNeoForge, + CleanroomEntry = SelectedCleanroom, + FabricVersion = SelectedFabric, + FabricApi = SelectedFabricApi, + QuiltVersion = SelectedQuilt, + QSL = SelectedQSL, + OptiFabric = SelectedOptiFabric, + LiteLoaderEntry = SelectedLiteLoader, + LabyModChannel = SelectedLabyModChannel, + LabyModCommitRef = SelectedLabyModCommitRef, + LegacyFabricVersion = SelectedLegacyFabric, + LegacyFabricApi = SelectedLegacyFabricApi + }; + if (!ModDownloadLib.McInstall(request)) + return; + // 返回,这样在再次进入安装页面时这个实例就会显示文件夹已重复 + ExitSelectPage(); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb index b4d06b3af..1567ef186 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb @@ -346,7 +346,7 @@ Public Class PageDownloadInstall CardCleanroom.Visibility = Visibility.Collapsed End If 'NeoForge - If VanillaDrop < 200 Then '匹配 1.20.1+ 与一些愚人节版本 + If VanillaDrop > 0 AndAlso VanillaDrop < 200 Then '匹配 1.20.1+ 与一些愚人节版本 CardNeoForge.Visibility = Visibility.Collapsed Else CardNeoForge.Visibility = Visibility.Visible @@ -1148,9 +1148,9 @@ Public Class PageDownloadInstall If SelectedOptiFine IsNot Nothing Then Return "与 OptiFine 不兼容" If SelectedLoaderName IsNot Nothing AndAlso SelectedLoaderName IsNot "Cleanroom" Then Return $"与 {SelectedLoaderName} 不兼容" '检查 Loader - If GetLoaderError(LoadNeoForge) IsNot Nothing Then Return GetLoaderError(LoadNeoForge) + If GetLoaderError(LoadCleanroom) IsNot Nothing Then Return GetLoaderError(LoadCleanroom) '检查版本 - Return If(DlNeoForgeListLoader.Output.Value.Any(Function(v) v.Inherit = _vanillaName), Nothing, "无可用版本") + Return If(DlCleanroomListLoader.Output.Value.Any(Function(v) v.Inherit = _vanillaName), Nothing, "无可用版本") End Function '限制展开 diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml index 4190c9ff6..82426c20e 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml @@ -1,28 +1,34 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml.cs new file mode 100644 index 000000000..5457d3fb6 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLabyMod.xaml.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json.Linq; + +namespace PCL; + +public partial class PageDownloadLabyMod +{ + public PageDownloadLabyMod() + { + InitializeComponent(); + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, CardVersions, CardTip, ModDownload.DlLabyModListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + var Versions = ModDownload.DlLabyModListLoader.Output.Value; + if (Versions is null) + return; + var ProductionEntry = new JObject(); + ProductionEntry.Add("channel", "production"); + ProductionEntry.Add("version", Versions["production"]["labyModVersion"].ToString()); + var SnapshotEntry = new JObject(); + SnapshotEntry.Add("channel", "snapshot"); + SnapshotEntry.Add("version", Versions["snapshot"]["labyModVersion"].ToString()); + PanVersions.Children.Clear(); + PanVersions.Children.Add(ModDownloadLib.LabyModDownloadListItem(ProductionEntry, + (a, b) => this.LabyMod_Production_Selected((dynamic)a, b))); + PanVersions.Children.Add(ModDownloadLib.LabyModDownloadListItem(SnapshotEntry, + (a, b) => this.LabyMod_Snapshot_Selected((dynamic)a, b))); + CardVersions.Title = "版本列表 (" + Versions.Count + ")"; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LabyMod 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + private void LabyMod_Production_Selected(MyListItem sender, EventArgs e) + { + ModDownloadLib.McDownloadLabyModProductionLoaderSave(); + } + + private void LabyMod_Snapshot_Selected(MyListItem sender, EventArgs e) + { + ModDownloadLib.McDownloadLabyModSnapshotLoaderSave(); + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://labymod.net"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml index fd49941e1..84772bd49 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml @@ -1,159 +1,266 @@ - - + + - - + + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.cs new file mode 100644 index 000000000..7e51fca24 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.cs @@ -0,0 +1,415 @@ +using System.Windows.Controls; +using System.Windows.Input; + +namespace PCL; + +public partial class PageDownloadLeft : IRefreshable +{ + public void Refresh() + { + Refresh(ModMain.FrmMain.PageCurrentSub); + } + + // 强制刷新 + public void RefreshButton_Click(object sender, EventArgs e) // 由边栏按钮匿名调用 + { + Refresh((FormMain.PageSubType)ModBase.Val(((dynamic)sender).Tag)); + } + + public void Refresh(FormMain.PageSubType SubType) + { + switch (SubType) + { + case FormMain.PageSubType.DownloadInstall: + { + ModDownload.DlClientListLoader.Start(IsForceRestart: true); + ModDownload.DlOptiFineListLoader.Start(IsForceRestart: true); + ModDownload.DlForgeListLoader.Start(IsForceRestart: true); + ModDownload.DlNeoForgeListLoader.Start(IsForceRestart: true); + ModDownload.DlCleanroomListLoader.Start(IsForceRestart: true); + ModDownload.DlLiteLoaderListLoader.Start(IsForceRestart: true); + ModDownload.DlFabricListLoader.Start(IsForceRestart: true); + ModDownload.DlLegacyFabricListLoader.Start(IsForceRestart: true); + ModDownload.DlFabricApiLoader.Start(IsForceRestart: true); + ModDownload.DlLegacyFabricApiLoader.Start(IsForceRestart: true); + ModDownload.DlQuiltListLoader.Start(IsForceRestart: true); + ModDownload.DlQSLLoader.Start(IsForceRestart: true); + ModDownload.DlOptiFabricLoader.Start(IsForceRestart: true); + ModDownload.DlLabyModListLoader.Start(IsForceRestart: true); + ItemInstall.Checked = true; + break; + } + case FormMain.PageSubType.DownloadMod: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadMod is not null) + { + ModMain.FrmDownloadMod.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadMod.Content.Page = 0; + ModMain.FrmDownloadMod.PageLoaderRestart(); + } + + ItemMod.Checked = true; + break; + } + case FormMain.PageSubType.DownloadPack: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadPack is not null) + { + ModMain.FrmDownloadPack.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadPack.Content.Page = 0; + ModMain.FrmDownloadPack.PageLoaderRestart(); + } + + ItemPack.Checked = true; + break; + } + case FormMain.PageSubType.DownloadDataPack: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadDataPack is not null) + { + ModMain.FrmDownloadDataPack.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadDataPack.Content.Page = 0; + ModMain.FrmDownloadDataPack.PageLoaderRestart(); + } + + ItemDataPack.Checked = true; + break; + } + case FormMain.PageSubType.DownloadResourcePack: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadResourcePack is not null) + { + ModMain.FrmDownloadResourcePack.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadResourcePack.Content.Page = 0; + ModMain.FrmDownloadResourcePack.PageLoaderRestart(); + } + + ItemResourcePack.Checked = true; + break; + } + case FormMain.PageSubType.DownloadShader: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadShader is not null) + { + ModMain.FrmDownloadShader.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadShader.Content.Page = 0; + ModMain.FrmDownloadShader.PageLoaderRestart(); + } + + ItemShader.Checked = true; + break; + } + case FormMain.PageSubType.DownloadWorld: + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + if (ModMain.FrmDownloadWorld is not null) + { + ModMain.FrmDownloadWorld.Content.Storage = new ModComp.CompProjectStorage(); + ModMain.FrmDownloadWorld.Content.Page = 0; + ModMain.FrmDownloadWorld.PageLoaderRestart(); + } + + ItemWorld.Checked = true; + break; + } + case FormMain.PageSubType.DownloadClient: + { + ModDownload.DlClientListLoader.Start(IsForceRestart: true); + ItemClient.Checked = true; + break; + } + case FormMain.PageSubType.DownloadOptiFine: + { + ModDownload.DlOptiFineListLoader.Start(IsForceRestart: true); + ItemOptiFine.Checked = true; + break; + } + case FormMain.PageSubType.DownloadForge: + { + ModDownload.DlForgeListLoader.Start(IsForceRestart: true); + ItemForge.Checked = true; + break; + } + case FormMain.PageSubType.DownloadNeoForge: + { + ModDownload.DlNeoForgeListLoader.Start(IsForceRestart: true); + ItemNeoForge.Checked = true; + break; + } + case FormMain.PageSubType.DownloadCleanroom: + { + ModDownload.DlCleanroomListLoader.Start(IsForceRestart: true); + ItemCleanroom.Checked = true; + break; + } + case FormMain.PageSubType.DownloadLiteLoader: + { + ModDownload.DlLiteLoaderListLoader.Start(IsForceRestart: true); + ItemLiteLoader.Checked = true; + break; + } + case FormMain.PageSubType.DownloadFabric: + { + ModDownload.DlFabricListLoader.Start(IsForceRestart: true); + ItemFabric.Checked = true; + break; + } + case FormMain.PageSubType.DownloadQuilt: + { + ModDownload.DlQuiltListLoader.Start(IsForceRestart: true); + ItemQuilt.Checked = true; + break; + } + case FormMain.PageSubType.DownloadLabyMod: + { + ModDownload.DlLabyModListLoader.Start(IsForceRestart: true); + ItemLabyMod.Checked = true; + break; + } + case FormMain.PageSubType.DownloadLegacyFabric: + { + ModDownload.DlLegacyFabricListLoader.Start(IsForceRestart: true); + ItemLegacyFabric.Checked = true; + break; + } + case FormMain.PageSubType.DownloadCompFavorites: + { + if (ModMain.FrmDownloadCompFavorites is not null) + ModMain.FrmDownloadCompFavorites.PageLoaderRestart(); + ItemFavorites.Checked = true; + break; + } + } + + ModMain.Hint("正在刷新……", Log: false); + } + + // 点击返回 + private void ItemInstall_Click(object sender, MouseButtonEventArgs e) + { + if (!ItemInstall.Checked) + return; + ModMain.FrmDownloadInstall.ExitSelectPage(); + } + + #region 页面切换 + + /// + /// 当前页面的编号。 + /// + public FormMain.PageSubType PageID = FormMain.PageSubType.DownloadInstall; + + public PageDownloadLeft() + { + AnimatedControl = PanItem; + InitializeComponent(); + ItemInstall.Check += PageCheck; + ItemMod.Check += PageCheck; + ItemPack.Check += PageCheck; + ItemDataPack.Check += PageCheck; + ItemResourcePack.Check += PageCheck; + ItemShader.Check += PageCheck; + ItemWorld.Check += PageCheck; + ItemFavorites.Check += PageCheck; + ItemClient.Check += PageCheck; + ItemOptiFine.Check += PageCheck; + ItemForge.Check += PageCheck; + ItemNeoForge.Check += PageCheck; + ItemLiteLoader.Check += PageCheck; + ItemFabric.Check += PageCheck; + ItemLegacyFabric.Check += PageCheck; + ItemQuilt.Check += PageCheck; + ItemLabyMod.Check += PageCheck; + } + + /// + /// 勾选事件改变页面。 + /// + private void PageCheck(object sender, ModBase.RouteEventArgs e) + { + if (sender is MyListItem { Tag: { } tag }) + PageChange((FormMain.PageSubType)ModBase.Val(tag)); + } + + public object PageGet(FormMain.PageSubType ID) + { + if (ID == default) + ID = PageID; + switch (ID) + { + case FormMain.PageSubType.DownloadInstall: + { + if (ModMain.FrmDownloadInstall is null) + ModMain.FrmDownloadInstall = new PageDownloadInstall(); + return ModMain.FrmDownloadInstall; + } + case FormMain.PageSubType.DownloadMod: + { + if (ModMain.FrmDownloadMod is null) + ModMain.FrmDownloadMod = new PageDownloadMod(); + return ModMain.FrmDownloadMod; + } + case FormMain.PageSubType.DownloadPack: + { + if (ModMain.FrmDownloadPack is null) + ModMain.FrmDownloadPack = new PageDownloadPack(); + return ModMain.FrmDownloadPack; + } + case FormMain.PageSubType.DownloadDataPack: + { + if (ModMain.FrmDownloadDataPack is null) + ModMain.FrmDownloadDataPack = new PageDownloadDataPack(); + return ModMain.FrmDownloadDataPack; + } + case FormMain.PageSubType.DownloadResourcePack: + { + if (ModMain.FrmDownloadResourcePack is null) + ModMain.FrmDownloadResourcePack = new PageDownloadResourcePack(); + return ModMain.FrmDownloadResourcePack; + } + case FormMain.PageSubType.DownloadShader: + { + if (ModMain.FrmDownloadShader is null) + ModMain.FrmDownloadShader = new PageDownloadShader(); + return ModMain.FrmDownloadShader; + } + case FormMain.PageSubType.DownloadWorld: + { + if (ModMain.FrmDownloadWorld is null) + ModMain.FrmDownloadWorld = new PageDownloadWorld(); + return ModMain.FrmDownloadWorld; + } + case FormMain.PageSubType.DownloadCompFavorites: + { + if (ModMain.FrmDownloadCompFavorites is null) + ModMain.FrmDownloadCompFavorites = new PageDownloadCompFavorites(); + return ModMain.FrmDownloadCompFavorites; + } + case FormMain.PageSubType.DownloadClient: + { + if (ModMain.FrmDownloadClient is null) + ModMain.FrmDownloadClient = new PageDownloadClient(); + return ModMain.FrmDownloadClient; + } + case FormMain.PageSubType.DownloadOptiFine: + { + if (ModMain.FrmDownloadOptiFine is null) + ModMain.FrmDownloadOptiFine = new PageDownloadOptiFine(); + return ModMain.FrmDownloadOptiFine; + } + case FormMain.PageSubType.DownloadForge: + { + if (ModMain.FrmDownloadForge is null) + ModMain.FrmDownloadForge = new PageDownloadForge(); + return ModMain.FrmDownloadForge; + } + case FormMain.PageSubType.DownloadNeoForge: + { + if (ModMain.FrmDownloadNeoForge is null) + ModMain.FrmDownloadNeoForge = new PageDownloadNeoForge(); + return ModMain.FrmDownloadNeoForge; + } + case FormMain.PageSubType.DownloadCleanroom: + { + if (ModMain.FrmDownloadCleanroom is null) + ModMain.FrmDownloadCleanroom = new PageDownloadCleanroom(); + return ModMain.FrmDownloadCleanroom; + } + case FormMain.PageSubType.DownloadLiteLoader: + { + if (ModMain.FrmDownloadLiteLoader is null) + ModMain.FrmDownloadLiteLoader = new PageDownloadLiteLoader(); + return ModMain.FrmDownloadLiteLoader; + } + case FormMain.PageSubType.DownloadFabric: + { + if (ModMain.FrmDownloadFabric is null) + ModMain.FrmDownloadFabric = new PageDownloadFabric(); + return ModMain.FrmDownloadFabric; + } + case FormMain.PageSubType.DownloadQuilt: + { + if (ModMain.FrmDownloadQuilt is null) + ModMain.FrmDownloadQuilt = new PageDownloadQuilt(); + return ModMain.FrmDownloadQuilt; + } + case FormMain.PageSubType.DownloadLabyMod: + { + if (ModMain.FrmDownloadLabyMod is null) + ModMain.FrmDownloadLabyMod = new PageDownloadLabyMod(); + return ModMain.FrmDownloadLabyMod; + } + case FormMain.PageSubType.DownloadLegacyFabric: + { + if (ModMain.FrmDownloadLegacyFabric is null) + ModMain.FrmDownloadLegacyFabric = new PageDownloadLegacyFabric(); + return ModMain.FrmDownloadLegacyFabric; + } + + default: + { + throw new Exception("未知的下载子页面种类:" + (int)ID); + } + } + } + + /// + /// 切换现有页面。 + /// + public void PageChange(FormMain.PageSubType ID) + { + if (PageID == ID) + return; + ModAnimation.AniControlEnabled += 1; + try + { + PageChangeRun((MyPageRight)PageGet(ID)); + PageID = ID; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换分页面失败(ID " + (int)ID + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private static void PageChangeRun(MyPageRight Target) + { + ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + if (Target.Parent is not null) + Target.SetValue(ContentPresenter.ContentProperty, null); + ModMain.FrmMain.PageRight = Target; + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnExit(); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnForceExit(); + ModMain.FrmMain.PanMainRight.Child = ModMain.FrmMain.PageRight; + ModMain.FrmMain.PageRight.Opacity = 0d; + }, 130), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + ModMain.FrmMain.PageRight.Opacity = 1d; + ModMain.FrmMain.PageRight.PageOnEnter(); + }, 30, true) + }, "PageLeft PageChange"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml index ef99f8586..64b978d89 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml @@ -1,28 +1,34 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml.cs new file mode 100644 index 000000000..c84d8bfe8 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLegacyFabric.xaml.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; + +namespace PCL; + +public partial class PageDownloadLegacyFabric +{ + public PageDownloadLegacyFabric() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, CardVersions, CardTip, ModDownload.DlLegacyFabricListLoader, + _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + var Versions = (JArray)ModDownload.DlLegacyFabricListLoader.Output.Value["installer"]; + PanVersions.Children.Clear(); + foreach (var Version in Versions) + PanVersions.Children.Add(ModDownloadLib.LegacyFabricDownloadListItem((JObject)Version, + (a, b) => this.LegacyFabric_Selected((dynamic)a, b))); + CardVersions.Title = "版本列表 (" + Versions.Count + ")"; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LegacyFabric 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + private void LegacyFabric_Selected(MyListItem sender, EventArgs e) + { + ModDownloadLib.McDownloadLegacyFabricLoaderSave((JObject)sender.Tag); + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://legacyfabric.net/"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml index 69c4db0bd..2de0930a4 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml @@ -1,26 +1,31 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml.cs new file mode 100644 index 000000000..ff07c9604 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLiteLoader.xaml.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace PCL; + +public partial class PageDownloadLiteLoader +{ + public PageDownloadLiteLoader() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanMain, CardTip, ModDownload.DlLiteLoaderListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + // 归类 + var Dict = new Dictionary>(); + for (var VersionCode = 30; VersionCode >= 0; VersionCode -= 1) + Dict.Add("1." + VersionCode, new List()); + Dict.Add("未知版本", new List()); + foreach (var Version in ModDownload.DlLiteLoaderListLoader.Output.Value) + { + var MainVersion = "1." + Version.Inherit.Split(".")[1]; + if (Dict.ContainsKey(MainVersion)) + Dict[MainVersion].Add(Version); + else + Dict["未知版本"].Add(Version); + } + + // 清空当前 + PanMain.Children.Clear(); + // 转化为 UI + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + NewCard.IsSwapped = true; + NewCard.InstallMethod = Stack => + { + Stack.Tag = ((List)Stack.Tag).Sort((a, b) => + ModMinecraft.CompareVersion(a.Inherit, b.Inherit) == 1); + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.LiteLoaderDownloadListItem( + (ModDownload.DlLiteLoaderListEntry)item, ModDownloadLib.LiteLoaderSave_Click, true)); + }; + PanMain.Children.Add(NewCard); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LiteLoader 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + public void DownloadStart(MyListItem sender, object e) + { + ModDownloadLib.McDownloadLiteLoader((ModDownload.DlLiteLoaderListEntry)sender.Tag); + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://www.liteloader.com"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml index 886c34374..e072bd95d 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml @@ -1,26 +1,32 @@  - + - - - + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml.cs new file mode 100644 index 000000000..7445c2431 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml.cs @@ -0,0 +1,68 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace PCL; + +public partial class PageDownloadNeoForge +{ + public PageDownloadNeoForge() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanMain, CardTip, ModDownload.DlNeoForgeListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + // 归类 + var Dict = ModDownload.DlNeoForgeListLoader.Output.Value.GroupBy(d => d.Inherit) + .OrderByDescending(g => g.Key).ToDictionary(g => g.Key, g => g.ToList()); + // 清空当前 + PanMain.Children.Clear(); + // 转化为 UI + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + NewCard.IsSwapped = true; + NewCard.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.NeoForgeDownloadListItem( + (ModDownload.DlNeoForgeListEntry)item, ModDownloadLib.NeoForgeSave_Click, true)); + }; + PanMain.Children.Add(NewCard); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 NeoForge 版本列表出错", ModBase.LogLevel.Feedback); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml index 1a08b5126..aca8497fe 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml @@ -1,26 +1,31 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml.cs new file mode 100644 index 000000000..2d1325274 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadOptiFine.xaml.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; + +namespace PCL; + +public partial class PageDownloadOptiFine +{ + public PageDownloadOptiFine() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanMain, CardTip, ModDownload.DlOptiFineListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + // 归类 + var Dict = new Dictionary>(); + Dict.Add("快照版本", new List()); + for (var VersionCode = 50; VersionCode >= 0; VersionCode -= 1) + Dict.Add("1." + VersionCode, new List()); + foreach (var Version in ModDownload.DlOptiFineListLoader.Output.Value) + if (Version.Inherit.StartsWith("1.")) + { + var MainVersion = "1." + Version.DisplayName.Split(".")[1].Split(" ")[0]; + if (Dict.ContainsKey(MainVersion)) + Dict[MainVersion].Add(Version); + else + Dict["快照版本"].Add(Version); + } + else + { + Dict["快照版本"].Add(Version); + } + + // 清空当前 + PanMain.Children.Clear(); + // 转化为 UI + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + NewCard.IsSwapped = true; + NewCard.InstallMethod = Stack => + { + Stack.Tag = ((List)Stack.Tag).Sort((a, b) => + ModMinecraft.CompareVersion(a.DisplayName, b.DisplayName) == 1); + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.OptiFineDownloadListItem( + (ModDownload.DlOptiFineListEntry)item, ModDownloadLib.OptiFineSave_Click, true)); + }; + PanMain.Children.Add(NewCard); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 OptiFine 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://www.optifine.net/"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml index 92cc5ca99..79525c15e 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml @@ -1,28 +1,34 @@  - + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml.cs b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml.cs new file mode 100644 index 000000000..40b7f6a2f --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadQuilt.xaml.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json.Linq; + +namespace PCL; + +public partial class PageDownloadQuilt +{ + public PageDownloadQuilt() + { + Initialized += (_, _) => LoaderInit(); + Loaded += (_, _) => Init(); + InitializeComponent(); + BtnWeb.Click += BtnWeb_Click; + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, CardVersions, CardTip, ModDownload.DlQuiltListLoader, _ => Load_OnFinish()); + } + + private void Init() + { + PanBack.ScrollToHome(); + } + + private void Load_OnFinish() + { + // 结果数据化 + try + { + var Versions = (JArray)ModDownload.DlQuiltListLoader.Output.Value["installer"]; + PanVersions.Children.Clear(); + foreach (var Version in Versions) + PanVersions.Children.Add( + ModDownloadLib.QuiltDownloadListItem((JObject)Version, + (a, b) => this.Quilt_Selected((dynamic)a, b))); + CardVersions.Title = "版本列表 (" + Versions.Count + ")"; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Quilt 版本列表出错", ModBase.LogLevel.Feedback); + } + } + + private void Quilt_Selected(MyListItem sender, EventArgs e) + { + ModDownloadLib.McDownloadQuiltLoaderSave((JObject)sender.Tag); + } + + private void BtnWeb_Click(object sender, EventArgs e) + { + ModBase.OpenWebsite("https://quiltmc.org"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml b/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml index 8d6635325..c648ddad7 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml @@ -1,9 +1,10 @@ - + RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}" + SnapsToDevicePixels="True"> @@ -19,16 +20,18 @@ - + - + + SnapsToDevicePixels="False" UseLayoutRounding="False" x:Name="PanTitle"> @@ -37,16 +40,20 @@ + TextTrimming="CharacterEllipsis" FontSize="12" IsHitTestVisible="False" + Foreground="{DynamicResource ColorBrushGray1}" Opacity="0.4" VerticalAlignment="Center" + Visibility="Collapsed" /> + Width="21" Height="21" Margin="-2,-1.6,0,0" x:Name="BtnUpdate" Theme="Black" Opacity="0.4" + Visibility="Collapsed" + ToolTipService.Placement="Right" ToolTipService.InitialShowDelay="100" + ToolTipService.VerticalOffset="-9" + Logo="M640 768H384l-32-32V509H213L190 454l298-298h45l298 298L810 509h-138v226z m-224-64h192V477l32-32h93L512 223 290 445H384l32 32zM352 831h320v64h-320z" /> - + @@ -54,4 +61,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.cs new file mode 100644 index 000000000..fa9a586d1 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.cs @@ -0,0 +1,889 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public partial class MyLocalCompItem +{ + private string GetUpdateCompareDescription() + { + var CurrentName = Entry.CompFile.FileName.Replace(".jar", ""); + var NewestName = Entry.UpdateFile.FileName.Replace(".jar", ""); + // 简化名称对比 + var CurrentSegs = CurrentName.Split('-').ToList(); + var NewestSegs = NewestName.Split('-').ToList(); + var Shortened = false; + foreach (var Seg in CurrentSegs.ToList()) + { + if (!NewestSegs.Contains(Seg)) + continue; + CurrentSegs.Remove(Seg); + NewestSegs.Remove(Seg); + Shortened = true; + } + + if (Shortened && CurrentSegs.Any() && NewestSegs.Any()) + { + CurrentName = CurrentSegs.Join("-"); + NewestName = NewestSegs.Join("-"); + Entry._Version = CurrentName; // 使用网络信息作为显示的版本号 + } + + return + $"当前版本:{CurrentName}({TimeUtils.GetTimeSpanString(Entry.CompFile.ReleaseDate - DateTime.Now, false)}){"\r\n"}最新版本:{NewestName}({TimeUtils.GetTimeSpanString(Entry.UpdateFile.ReleaseDate - DateTime.Now, false)})"; + } + + public void Refresh() + { + Dispatcher.BeginInvoke(new Func(async () => + { + // 更新 + if (Entry.CanUpdate) + { + BtnUpdate.Visibility = Visibility.Visible; + BtnUpdate.ToolTip = $"{GetUpdateCompareDescription()}{"\r\n"}点击以更新,右键查看更新日志。"; + } + else + { + BtnUpdate.Visibility = Visibility.Collapsed; + } + + // 标题与描述 + string DescFileName; + if (Entry.IsFolder) + // 文件夹项的特殊处理 + DescFileName = Entry.Name; + else + switch (Entry.State) + { + case ModLocalComp.LocalCompFile.LocalFileStatus.Fine: + { + DescFileName = ModBase.GetFileNameWithoutExtentionFromPath(Entry.Path); + break; + } + case ModLocalComp.LocalCompFile.LocalFileStatus.Disabled: + { + DescFileName = + ModBase.GetFileNameWithoutExtentionFromPath(Entry.Path.Replace(".disabled", "") + .Replace(".old", "")); // McMod.McModState.Unavailable + break; + } + + default: + { + DescFileName = ModBase.GetFileNameFromPath(Entry.Path); + break; + } + } + + string NewDescription; + var compTemp = Entry.Comp; + if (Entry.IsFolder) + { + // 文件夹项的特殊显示 + Title = Entry.Name; + NewDescription = Entry.Description; + } + else if (Config.Download.Comp.UiCompNameSolution == 1) + { + // 标题显示文件名,详情显示译名 + // 标题 + Title = DescFileName; + SubTitle = ""; + // 描述 + if (Entry.Comp is null) + { + NewDescription = Entry.Name; + } + else + { + var Titles = await Task.Run(() => compTemp.GetControlTitle(false)); + NewDescription = Titles.Key + Titles.Value; + } + + NewDescription = NewDescription.Replace(" | ", " / "); + if (Entry.Version is not null) + NewDescription += $" ({Entry.Version})"; + } + else + { + // 标题显示译名,详情显示文件名 + // 标题 + if (Entry.Comp is null) + { + Title = Entry.Name; + SubTitle = Entry.Version is null ? "" : " | " + Entry.Version; + } + else + { + var Titles = await Task.Run(() => compTemp.GetControlTitle(false)); + Title = Titles.Key; + SubTitle = Titles.Value + (Entry.Version is null ? "" : " | " + Entry.Version); + } + + // 描述 + NewDescription = DescFileName; + } + + if (Entry.Comp is not null) + NewDescription += ": " + Entry.Comp.Description.Replace("\r", "").Replace("\n", ""); + else if (Entry.Description is not null) + NewDescription += ": " + Entry.Description.Replace("\r", "").Replace("\n", ""); + else if (!Entry.IsFileAvailable) NewDescription += ": " + "存在错误,无法获取信息"; + Description = NewDescription; + if (Checked) + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, + Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? "ColorBrush2" : "ColorBrush5"); + else + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, + Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? "ColorBrush1" : "ColorBrushGray4"); + // 主 Logo + Logo = Entry.GetLogo(); + + // 图标右下角的 Logo + if (Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + { + if (ImgState is not null) + { + Children.Remove(ImgState); + ImgState = null; + } + } + else + { + if (ImgState is null) + { + ImgState = new Image + { + Width = 20d, + Height = 20d, + Margin = new Thickness(0d, 0d, -5, -3), + IsHitTestVisible = false, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Bottom + }; + RenderOptions.SetBitmapScalingMode(ImgState, BitmapScalingMode.HighQuality); + SetColumn(ImgState, 1); + SetRow(ImgState, 1); + SetRowSpan(ImgState, 2); + Children.Add(ImgState); + // + } + + ImgState.Source = new MyBitmap(ModBase.PathImage + $"Icons/{Entry.State}.png"); + } + + // 标签 + if (Entry.IsFolder) + // 为文件夹添加标签 + Tags = new List { "文件夹" }; + else if (Entry.Comp is not null) Tags = Entry.Comp.Tags; + })); + } + + public void RefreshColor(object sender, EventArgs e) + { + InitLate(sender, e); + // 触发颜色动画 + var Time = IsMouseOver ? 120 : 180; + var Ani = new List(); + // ButtonStack + if (ButtonStack is not null) + { + if (IsMouseOver) + { + Ani.Add(ModAnimation.AaOpacity(ButtonStack, 1d - ButtonStack.Opacity, (int)Math.Round(Time * 0.7d), + (int)Math.Round(Time * 0.3d))); + Ani.Add(ModAnimation.AaDouble( + i => ColumnPaddingRight.Width = + new GridLength(Conversions.ToDouble(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i))), + 5 + Buttons.Count() * 25 - ColumnPaddingRight.Width.Value, (int)Math.Round(Time * 0.3d), + (int)Math.Round(Time * 0.7d))); + } + else + { + Ani.Add(ModAnimation.AaOpacity(ButtonStack, -ButtonStack.Opacity, (int)Math.Round(Time * 0.4d))); + Ani.Add(ModAnimation.AaDouble( + i => ColumnPaddingRight.Width = + new GridLength(Math.Max(0, ColumnPaddingRight.Width.Value + (double)i)), + 4d - ColumnPaddingRight.Width.Value, (int)Math.Round(Time * 0.4d))); + } + } + + // RectBack + if (IsMouseOver || Checked) + { + Ani.AddRange(new[] + { + ModAnimation.AaColor(RectBack, Border.BackgroundProperty, IsMouseDown ? "ColorBrush6" : "ColorBrushBg1", + Time), + ModAnimation.AaOpacity(RectBack, 1d - RectBack.Opacity, Time, Ease: new ModAnimation.AniEaseOutFluent()) + }); + if (IsMouseDown) + Ani.Add(ModAnimation.AaScaleTransform(RectBack, + 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, (int)Math.Round(Time * 1.2d), + Ease: new ModAnimation.AniEaseOutFluent())); + else + Ani.Add(ModAnimation.AaScaleTransform(RectBack, 1d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, + (int)Math.Round(Time * 1.2d), Ease: new ModAnimation.AniEaseOutFluent())); + } + else + { + Ani.AddRange(new[] + { + ModAnimation.AaOpacity(RectBack, -RectBack.Opacity, Time), + ModAnimation.AaScaleTransform(RectBack, 0.996d - ((ScaleTransform)RectBack.RenderTransform).ScaleX, + Time, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaScaleTransform(RectBack, -0.196d, 1, After: true) + }); + } + + ModAnimation.AniStart(Ani, "LocalModItem Color " + Uuid); + } + + // 触发虚拟化内容 + private void InitLate(object sender, EventArgs e) + { + if (ButtonHandler is not null) + { + ButtonHandler((MyLocalCompItem)sender, e); + ButtonHandler = null; + } + } + + // 显示更新日志 + private void BtnUpdate_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + ShowUpdateLog(); + } + + private void ShowUpdateLog() + { + if (Entry.Comp is not null) + { + if (!Information.IsNumeric(Entry.Comp.Id)) + { + var modrinthUrl = Entry.ChangelogUrls.FirstOrDefault(x => x.Contains("modrinth.com")); + if (modrinthUrl is not null) + { + ModBase.OpenWebsite(modrinthUrl); + return; + } + } + else + { + var curseForgeUrl = Entry.ChangelogUrls.FirstOrDefault(x => x.Contains("curseforge.com")); + if (curseForgeUrl is not null) + { + ModBase.OpenWebsite(curseForgeUrl); + return; + } + } + } + + ModBase.Log("打开更新日志出现错误", ModBase.LogLevel.Hint); + } + + // 触发更新 + private void BtnUpdate_Click(object sender, EventArgs e) + { + switch (ModMain.MyMsgBox( + $"是否要更新 {Entry.Name}?{"\r\n"}{"\r\n"}{GetUpdateCompareDescription()}", "更新确认", + "更新", "查看更新日志", "取消")) + { + case 1: // 更新 + { + switch (Entry.Comp.Type) + { + case ModComp.CompType.Mod: + { + ModMain.FrmInstanceMod.UpdateResource(new[] { Entry }); + break; + } + case ModComp.CompType.ResourcePack: + { + ModMain.FrmInstanceResourcePack.UpdateResource(new[] { Entry }); + break; + } + case ModComp.CompType.Shader: + { + ModMain.FrmInstanceShader.UpdateResource(new[] { Entry }); + break; + } + case ModComp.CompType.DataPack: + { + ModMain.FrmInstanceSavesDatapack.UpdateResource(new[] { Entry }); + break; + } + } + + break; + } + case 2: // 查看更新日志 + { + ShowUpdateLog(); + break; + } + case 3: // 取消 + { + break; + } + } + } + + // 自适应(#4465) + private void PanTitle_SizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs) + { + // 0:全部舒展:Auto - Auto - (Auto) - 1* + // 1:压缩 Subtitle:Auto - 1* - (Auto) - 0 + // 2:继续压缩 Title:1* - 0 - (Auto) - 0 + var CurrentCompressLevel = + ColumnExtend.Width.IsStar ? 0 : ColumnTitle.Width.IsStar ? 2 : 1; // Subtitle 可能是 Collapsed + var NewCompressLevel = default(int); + switch (CurrentCompressLevel) + { + case 0: + { + if (ColumnExtend.ActualWidth < 0.5d) + NewCompressLevel = LabSubtitle.Visibility == Visibility.Collapsed ? 2 : 1; + else + return; + + break; + } + case 1: + { + if (ColumnSubtitle.ActualWidth < 0.5d) + NewCompressLevel = 2; + else if (!LabSubtitle.IsTextTrimmed()) + NewCompressLevel = 0; + else + return; + + break; + } + case 2: + { + if (!LabTitle.IsTextTrimmed()) + NewCompressLevel = LabSubtitle.Visibility == Visibility.Collapsed ? 0 : 1; + else + return; + + break; + } + } + + switch (NewCompressLevel) + { + case 0: + { + // 全部舒展:Auto - Auto - (Auto) - 1* + ColumnTitle.Width = GridLength.Auto; + ColumnSubtitle.Width = GridLength.Auto; + ColumnExtend.Width = new GridLength(1d, GridUnitType.Star); + break; + } + case 1: + { + // 压缩 Subtitle:Auto - 1* - (Auto) - 0 + ColumnTitle.Width = GridLength.Auto; + ColumnSubtitle.Width = new GridLength(1d, GridUnitType.Star); + ColumnExtend.Width = new GridLength(0d, GridUnitType.Pixel); + break; + } + case 2: + { + // 继续压缩 Title:1* - 0 - (Auto) - 0 + ColumnTitle.Width = new GridLength(1d, GridUnitType.Star); + ColumnSubtitle.Width = new GridLength(0d, GridUnitType.Pixel); + ColumnExtend.Width = new GridLength(0d, GridUnitType.Pixel); + break; + } + } + } + + #region 基础属性 + + public int Uuid = ModBase.GetUuid(); + + // Logo + public string Logo + { + get => PathLogo.Source; + set => PathLogo.Source = value; + } + + // 标题 + private string _Title; + + public string Title + { + get => _Title; + set + { + var RawValue = value; + switch (Entry.State) + { + case ModLocalComp.LocalCompFile.LocalFileStatus.Fine: + { + LabTitle.TextDecorations = null; + break; + } + case ModLocalComp.LocalCompFile.LocalFileStatus.Disabled: + { + LabTitle.TextDecorations = TextDecorations.Strikethrough; + break; + } + case ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable: + { + LabTitle.TextDecorations = TextDecorations.Strikethrough; + value += " [错误]"; + break; + } + } + + if ((LabTitle.Text ?? "") == (value ?? "")) + return; + LabTitle.Text = value; + _Title = RawValue; + } + } + + // 副标题 + public string SubTitle + { + get => LabSubtitle?.Text ?? ""; + set + { + if ((LabSubtitle.Text ?? "") == (value ?? "")) + return; + LabSubtitle.Text = value; + LabSubtitle.Visibility = string.IsNullOrEmpty(value) ? Visibility.Collapsed : Visibility.Visible; + } + } + + // 描述 + public string Description + { + get => LabInfo.Text; + set + { + if ((LabInfo.Text ?? "") == (value ?? "")) + return; + LabInfo.Text = value; + } + } + + // Tag + public List Tags + { + set + { + PanTags.Children.Clear(); + PanTags.Visibility = value.Any() ? Visibility.Visible : Visibility.Collapsed; + foreach (var TagText in value) + { + var NewTag = new Border + { + Background = new SolidColorBrush(Color.FromArgb(12, 0, 0, 0)), + Padding = new Thickness(3d, 1d, 3d, 1d), + CornerRadius = new CornerRadius(3d), + Margin = new Thickness(0d, 0d, 3d, 0d), + SnapsToDevicePixels = true, + UseLayoutRounding = false + }; + var TagTextBlock = new TextBlock + { + Text = TagText, + Foreground = new SolidColorBrush(ModSecret.IsDarkMode + ? Color.FromArgb(88, 255, 255, 255) + : Color.FromArgb(88, 136, 136, 136)), + FontSize = 11d + }; + NewTag.Child = TagTextBlock; + PanTags.Children.Add(NewTag); + } + } + } + + // 相关联的 Mod + public ModLocalComp.LocalCompFile Entry + { + get => (ModLocalComp.LocalCompFile)Tag; + set => Tag = value; + } + + #endregion + + #region 点击与勾选 + + // 触发点击事件 + public event ClickEventHandler? Click; + + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); + + public MyLocalCompItem() + { + InitializeComponent(); + PreviewMouseLeftButtonUp += Button_MouseUp; + PreviewMouseLeftButtonDown += Button_MouseDown; + MouseLeave += Button_MouseLeave; + PreviewMouseLeftButtonUp += Button_MouseLeave; + MouseLeftButtonDown += Button_MouseSwipeStart; + MouseEnter += Button_MouseSwipe; + MouseLeave += Button_MouseSwipe; + MouseLeftButtonUp += Button_MouseSwipe; + Loaded += (_, _) => Refresh(); + MouseEnter += RefreshColor; + MouseLeave += RefreshColor; + MouseLeftButtonDown += RefreshColor; + MouseLeftButtonUp += RefreshColor; + Changed += RefreshColor; + // Handles + BtnUpdate.PreviewMouseRightButtonUp += BtnUpdate_PreviewMouseRightButtonUp; + BtnUpdate.Click += BtnUpdate_Click; + PanTitle.SizeChanged += PanTitle_SizeChanged; + } + + private void Button_MouseUp(object sender, MouseButtonEventArgs e) + { + if (IsMouseDown) + { + Click?.Invoke(sender, e); + if (e.Handled) + return; + ModBase.Log("[Control] 按下本地 Mod 列表项:" + LabTitle.Text); + } + } + + // 鼠标点击判定 + private bool IsMouseDown; + + private void Button_MouseDown(object sender, MouseButtonEventArgs e) + { + if (!IsMouseDirectlyOver) + return; + IsMouseDown = true; + if (ButtonStack is not null) + ButtonStack.IsHitTestVisible = false; + } + + private void Button_MouseLeave(object sender, object e) + { + IsMouseDown = false; + if (ButtonStack is not null) + ButtonStack.IsHitTestVisible = true; + } + + // 滑动选中 + public class SwipeSelect + { + private bool _Swiping; + public int Start { get; set; } + public int End { get; set; } + + public bool Swiping + { + get => _Swiping; + set + { + _Swiping = value; + if (TargetFrm is not null) + try + { + var cardSelect = Interaction.CallByName(TargetFrm, "CardSelect", CallType.Get); + Interaction.CallByName(cardSelect, "IsHitTestVisible", CallType.Set, !value); + } + catch + { + } + } + } + + public bool SwipeToState { get; set; } + public object TargetFrm { get; set; } + } + + public SwipeSelect CurrentSwipe { get; set; } + + private void Button_MouseSwipeStart(object sender, object e) + { + if (Parent is null) + return; // Mod 可能已被删除(#3824) + // 开始滑动 + var Index = ((StackPanel)Parent).Children.IndexOf(this); + CurrentSwipe.Start = Index; + CurrentSwipe.End = Index; + CurrentSwipe.Swiping = true; + CurrentSwipe.SwipeToState = !Checked; + } + + private void Button_MouseSwipe(object sender, object e) + { + if (Parent is null) + return; // Mod 可能已被删除(#3824) + // 结束滑动 + if (Mouse.LeftButton != MouseButtonState.Pressed || !(Mouse.DirectlyOver is MyLocalCompItem)) // #5771 + { + CurrentSwipe.Swiping = false; + return; + } + + // 计算滑动范围 + var Elements = ((StackPanel)Parent).Children; + var Index = Elements.IndexOf(this); + CurrentSwipe.Start = + (int)Math.Round(ModBase.MathClamp(Math.Min(CurrentSwipe.Start, Index), 0d, Elements.Count - 1)); + CurrentSwipe.End = + (int)Math.Round(ModBase.MathClamp(Math.Max(CurrentSwipe.End, Index), 0d, Elements.Count - 1)); + // 勾选所有范围中的项 + if (CurrentSwipe.Start == CurrentSwipe.End) + return; + for (int i = CurrentSwipe.Start, loopTo = CurrentSwipe.End; i <= loopTo; i++) + { + var Item = (MyLocalCompItem)Elements[i]; + Item.InitLate(Item, (EventArgs)e); + Item.Checked = CurrentSwipe.SwipeToState; + } + } + + // 勾选状态 + public event CheckEventHandler? Check; + + public delegate void CheckEventHandler(object sender, ModBase.RouteEventArgs e); + + public event ChangedEventHandler? Changed; + + public delegate void ChangedEventHandler(object sender, ModBase.RouteEventArgs e); + + private bool _Checked; + + public bool Checked + { + get => _Checked; + set + { + try + { + // 触发属性值修改 + var RawValue = _Checked; + if (value == _Checked) + return; + _Checked = value; + var ChangedEventArgs = new ModBase.RouteEventArgs(); + if (IsInitialized) + { + Changed?.Invoke(this, ChangedEventArgs); + if (ChangedEventArgs.Handled) + { + _Checked = RawValue; + return; + } + } + + if (value) + { + var CheckEventArgs = new ModBase.RouteEventArgs(); + Check?.Invoke(this, CheckEventArgs); + if (CheckEventArgs.Handled) + return; + } + + // 更改动画 + if (this.IsVisibleInWindow(ModMain.FrmMain)) + { + var Anim = new List(); + if (Checked) + { + // 由无变有 + var Delta = 32d - RectCheck.ActualHeight; + Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.4d, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaHeight(RectCheck, Delta * 0.6d, 300, + Ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaOpacity(RectCheck, 1d - RectCheck.Opacity, 30)); + RectCheck.VerticalAlignment = VerticalAlignment.Center; + RectCheck.Margin = new Thickness(-3, 0d, 0d, 0d); + Anim.Add(ModAnimation.AaColor(LabTitle, TextBlock.ForegroundProperty, + Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine + ? "ColorBrush2" + : "ColorBrush5", 200)); + } + else + { + // 由有变无 + Anim.Add(ModAnimation.AaHeight(RectCheck, -RectCheck.ActualHeight, 120, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak))); + Anim.Add(ModAnimation.AaOpacity(RectCheck, -RectCheck.Opacity, 70, 40)); + RectCheck.VerticalAlignment = VerticalAlignment.Center; + Anim.Add(ModAnimation.AaColor(LabTitle, TextBlock.ForegroundProperty, + LabTitle.TextDecorations is null ? "ColorBrush1" : "ColorBrushGray4", 120)); + } + + ModAnimation.AniStart(Anim, "MyLocalCompItem Checked " + Uuid); + } + else + { + // 不在窗口上时直接设置 + RectCheck.VerticalAlignment = VerticalAlignment.Center; + RectCheck.Margin = new Thickness(-3, 0d, 0d, 0d); + if (Checked) + { + RectCheck.Height = 32d; + RectCheck.Opacity = 1d; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, + Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine + ? "ColorBrush2" + : "ColorBrush5"); + } + else + { + RectCheck.Height = 0d; + RectCheck.Opacity = 0d; + LabTitle.SetResourceReference(TextBlock.ForegroundProperty, + Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine + ? "ColorBrush1" + : "ColorBrushGray4"); + } + + ModAnimation.AniStop("MyLocalCompItem Checked " + Uuid); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "设置 Checked 失败"); + } + } + } + + #endregion + + #region 后加载内容 + + // 右下角状态指示图标 + private Image ImgState; + + // 指向背景 + private Border _RectBack; + + public Border RectBack + { + get + { + if (_RectBack is null) + { + var Rect = new Border + { + Name = "RectBack", + CornerRadius = new CornerRadius(3d), + RenderTransform = new ScaleTransform(0.8d, 0.8d), + RenderTransformOrigin = new Point(0.5d, 0.5d), + BorderThickness = new Thickness(ModBase.GetWPFSize(1d)), + SnapsToDevicePixels = true, + IsHitTestVisible = false, + Opacity = 0d + }; + Rect.SetResourceReference(Border.BackgroundProperty, "ColorBrush7"); + Rect.SetResourceReference(Border.BorderBrushProperty, "ColorBrush6"); + SetColumnSpan(Rect, 999); + SetRowSpan(Rect, 999); + Children.Insert(0, Rect); + _RectBack = Rect; + // + } + + return _RectBack; + } + } + + // 按钮 + public Action ButtonHandler; + public FrameworkElement ButtonStack; + private IEnumerable _Buttons; + + public IEnumerable Buttons + { + get => _Buttons; + set + { + _Buttons = value; + // 移除原 Stack + if (ButtonStack is not null) + { + Children.Remove(ButtonStack); + ButtonStack = null; + } + + if (!value.Any()) + return; + // 添加新 Stack + ButtonStack = new StackPanel + { + Opacity = 0d, + Margin = new Thickness(0d, 0d, 5d, 0d), + SnapsToDevicePixels = false, + Orientation = (Orientation)System.Windows.Forms.Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + UseLayoutRounding = false + }; + SetColumnSpan(ButtonStack, 10); + SetRowSpan(ButtonStack, 10); + // 构造按钮 + foreach (var Btn in value) + { + if (Btn.Height.Equals(double.NaN)) + Btn.Height = 25d; + if (Btn.Width.Equals(double.NaN)) + Btn.Width = 25d; + ((StackPanel)ButtonStack).Children.Add(Btn); + } + + Children.Add(ButtonStack); + } + } + + // 勾选条 + private Border _RectCheck; + + public Border RectCheck + { + get + { + if (_RectCheck is null) + { + _RectCheck = new Border + { + Width = 5d, + Height = Checked ? double.NaN : 0d, + CornerRadius = new CornerRadius(2d, 2d, 2d, 2d), + VerticalAlignment = Checked ? VerticalAlignment.Stretch : VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + UseLayoutRounding = false, + SnapsToDevicePixels = false, + Margin = Checked ? new Thickness(-3, 6d, 0d, 6d) : new Thickness(-3, 0d, 0d, 0d) + }; + _RectCheck.SetResourceReference(Border.BackgroundProperty, "ColorBrush3"); + SetRowSpan(_RectCheck, 10); + Children.Add(_RectCheck); + } + + return _RectCheck; + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml index e8c5c441a..9c07c2360 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml @@ -1,92 +1,137 @@ - + - + - - + + - - - - - + + + + + - - - - - - - - + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + + - + + LogoScale="1" + Logo="M640 768H384l-32-32V509H213L190 454l298-298h45l298 298L810 509h-138v226z m-224-64h192V477l32-32h93L512 223 290 445H384l32 32zM352 831h320v64h-320z" /> + LogoScale="1.05" + Logo="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z" /> + LogoScale="1" + Logo="M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z" /> + LogoScale="1" + Logo="M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z" /> + LogoScale="1" + Logo="M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z" /> + LogoScale="0.96" + Logo="M520.192 0C408.43 0 317.44 82.87 313.563 186.734H52.736c-29.038 0-52.663 21.943-52.663 49.079s23.625 49.152 52.663 49.152h58.075v550.473c0 103.35 75.118 187.757 167.717 187.757h472.43c92.599 0 167.716-83.894 167.716-187.757V285.477h52.59c29.038 0 52.59-21.943 52.663-49.08-0.073-27.135-23.625-49.151-52.663-49.151H726.235C723.237 83.017 631.955 0 520.192 0zM404.846 177.957c3.803-50.03 50.176-89.015 107.447-89.015 57.197 0 103.57 38.985 106.788 89.015H404.92zM284.379 933.669c-33.353 0-69.997-39.351-69.997-95.525v-549.01H833.39v549.522c0 56.247-36.645 95.525-69.998 95.525H284.379v-0.512z M357.23 800.695a48.274 48.274 0 0 0 47.616-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.69 49.006V751.69c0 27.282 20.846 49.006 47.617 49.006z m166.62 0a48.274 48.274 0 0 0 47.688-49.006V471.7a48.274 48.274 0 0 0-47.689-49.08 48.274 48.274 0 0 0-47.543 49.006V751.69c0 27.282 21.431 49.006 47.543 49.006z m142.92 0a48.274 48.274 0 0 0 47.543-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.616 49.006V751.69c0 27.282 20.773 49.006 47.543 49.006z" /> + LogoScale="0.8" + Logo="M867.648 951.296 512 595.648l-355.648 355.648c-11.52 11.52-30.272 11.52-41.856 0L72.64 909.44c-11.52-11.52-11.52-30.272 0-41.856L428.352 512 72.64 156.352c-11.52-11.52-11.52-30.272 0-41.856l41.856-41.856c11.52-11.52 30.272-11.52 41.856 0L512 428.288l355.648-355.648c11.52-11.52 30.272-11.52 41.856 0l41.856 41.856c11.52 11.52 11.52 30.272 0 41.856L595.648 512l355.648 355.648c11.52 11.52 11.52 30.272 0 41.856l-41.856 41.856C897.984 962.88 879.168 962.88 867.648 951.296L867.648 951.296z" /> - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.cs new file mode 100644 index 000000000..103e06364 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.cs @@ -0,0 +1,2869 @@ +using System.IO; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.VisualBasic.FileIO; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.UI.Theme; +using FileSystem = Microsoft.VisualBasic.FileSystem; +using SearchOption = System.IO.SearchOption; + +namespace PCL; + +public partial class PageInstanceCompResource : IRefreshable +{ + #region 模组信息缓存 + + // 模组信息缓存 - 解决排序时重复创建FileInfo导致的性能问题 + private readonly Dictionary ModFileInfoCache = new(); + + public PageInstanceCompResource() + { + InitializeComponent(); + Unloaded += Page_Unloaded; + Loaded += (_, __) => PageOther_Loaded(); + Initialized += (_, __) => LoaderInit(); + PageExit += UnselectedAllWithAnimation; + Load.Click += Load_Click; + BtnManageBack.Click += BtnManageBack_Click; + BtnHintBack.Click += BtnHintBack_Click; + BtnManageOpen.Click += BtnManageOpen_Click; + BtnHintOpen.Click += BtnManageOpen_Click; + BtnManageSelectAll.Click += BtnManageSelectAll_Click; + BtnManageInstall.Click += BtnManageInstall_Click; + BtnHintInstall.Click += BtnManageInstall_Click; + BtnManageInfoExport.Click += BtnManageInfoExport_Click; + BtnManageDownload.Click += BtnManageDownload_Click; + BtnHintDownload.Click += BtnManageDownload_Click; + BtnSchematicDownloadMod.Click += BtnSchematicDownloadMod_Click; + BtnSchematicVersionSelect.Click += BtnSchematicVersionSelect_Click; + Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation(); + SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown; + BtnFilterAll.Check += ChangeFilter; + BtnFilterCanUpdate.Check += ChangeFilter; + BtnFilterDisabled.Check += ChangeFilter; + BtnFilterEnabled.Check += ChangeFilter; + BtnFilterError.Check += ChangeFilter; + BtnFilterDuplicate.Check += ChangeFilter; + BtnSort.Click += BtnSortClick; + BtnSelectEnable.Click += BtnSelectED_Click; + BtnSelectDisable.Click += BtnSelectED_Click; + BtnSelectUpdate.Click += BtnSelectUpdate_Click; + BtnSelectDelete.Click += BtnSelectDelete_Click; + BtnSelectCancel.Click += BtnSelectCancel_Click; + BtnSelectFavorites.Click += BtnSelectFavorites_Click; + BtnSelectShare.Click += BtnSelectShare_Click; + SearchBox.TextChanged += SearchRun; + } + + // 获取模组信息(带缓存) + private (DateTime CreationTime, long Length) GetModFileInfo(string path) + { + (DateTime CreationTime, long Length) cacheItem; + if (ModFileInfoCache.TryGetValue(path, out cacheItem)) return cacheItem; + + try + { + var fileInfo = new FileInfo(path); + var newItem = (fileInfo.CreationTime, fileInfo.Length); + if (!ModFileInfoCache.ContainsKey(path)) ModFileInfoCache.Add(path, newItem); + return newItem; + } + catch (Exception ex) + { + ModBase.Log(ex, "获取模组信息失败: " + path); + return (DateTime.MinValue, 0L); + } + } + + // 页面关闭时清理缓存 + private void Page_Unloaded(object sender, RoutedEventArgs e) + { + ModFileInfoCache.Clear(); + } + + #endregion + + #region 初始化 + + private readonly ModComp.CompType CurrentCompType = ModComp.CompType.Mod; + + private readonly MyLocalCompItem.SwipeSelect CurrentSwipSelect; + + public PageInstanceCompResource(ModComp.CompType LoadCompType) + { + CurrentCompType = LoadCompType; + CurrentFolderPath = ""; // 确保文件夹路径被重置为根目录 + CurrentSwipSelect = new MyLocalCompItem.SwipeSelect { TargetFrm = this }; + + // 此调用是设计器所必需的。 + InitializeComponent(); + + // 在 InitializeComponent() 调用之后添加任何初始化。 + + if (new[] { ModComp.CompType.Shader, ModComp.CompType.ResourcePack, ModComp.CompType.Schematic }.Contains( + CurrentCompType)) + { + BtnSelectEnable.Visibility = Visibility.Collapsed; + BtnSelectDisable.Visibility = Visibility.Collapsed; + } + + // 投影文件管理页隐藏下载按钮 + if (CurrentCompType == ModComp.CompType.Schematic) + { + BtnManageDownload.Visibility = Visibility.Collapsed; + BtnHintDownload.Visibility = Visibility.Collapsed; + } + + Unloaded += Page_Unloaded; + Loaded += (_, __) => PageOther_Loaded(); + LoaderInit(); + PageExit += UnselectedAllWithAnimation; + // Handles + Load.Click += Load_Click; + BtnManageBack.Click += BtnManageBack_Click; + BtnHintBack.Click += BtnHintBack_Click; + BtnManageOpen.Click += BtnManageOpen_Click; + BtnHintOpen.Click += BtnManageOpen_Click; + BtnManageSelectAll.Click += BtnManageSelectAll_Click; + BtnManageInstall.Click += BtnManageInstall_Click; + BtnHintInstall.Click += BtnManageInstall_Click; + BtnManageDownload.Click += BtnManageDownload_Click; + BtnHintDownload.Click += BtnManageDownload_Click; + BtnManageInfoExport.Click += BtnManageInfoExport_Click; + BtnSchematicDownloadMod.Click += BtnSchematicDownloadMod_Click; + BtnSchematicVersionSelect.Click += BtnSchematicVersionSelect_Click; + Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation(); + SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown; + BtnFilterAll.Check += ChangeFilter; + BtnFilterCanUpdate.Check += ChangeFilter; + BtnFilterDisabled.Check += ChangeFilter; + BtnFilterEnabled.Check += ChangeFilter; + BtnFilterError.Check += ChangeFilter; + BtnFilterDuplicate.Check += ChangeFilter; + BtnSort.Click += BtnSortClick; + BtnSelectEnable.Click += BtnSelectED_Click; + BtnSelectDisable.Click += BtnSelectED_Click; + BtnSelectUpdate.Click += BtnSelectUpdate_Click; + BtnSelectDelete.Click += BtnSelectDelete_Click; + BtnSelectCancel.Click += BtnSelectCancel_Click; + BtnSelectFavorites.Click += BtnSelectFavorites_Click; + BtnSelectShare.Click += BtnSelectShare_Click; + SearchBox.TextChanged += SearchRun; + } + + private ModLocalComp.CompLocalLoaderData GetRequireLoaderData() + { + var res = new ModLocalComp.CompLocalLoaderData(); + res.GameVersion = PageInstanceLeft.Instance; + res.Frm = this; + var RequireLoaders = new List(); + switch (CurrentCompType) + { + case ModComp.CompType.Mod: + { + RequireLoaders = ModLocalComp.GetCurrentVersionModLoader(); + break; + } + case ModComp.CompType.ResourcePack: + { + RequireLoaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList(); + break; + } + case ModComp.CompType.Shader: + { + RequireLoaders = new[] + { + ModComp.CompLoaderType.OptiFine, ModComp.CompLoaderType.Iris, ModComp.CompLoaderType.Vanilla, + ModComp.CompLoaderType.Canvas + }.ToList(); + break; + } + case ModComp.CompType.Schematic: + { + RequireLoaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList(); + break; + } + } + + res.Loaders = RequireLoaders; + res.CompPath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + res.CompType = CurrentCompType; + return res; + } + + private bool IsLoad; + + public void PageOther_Loaded() + { + CurrentFolderPath = string.Empty; + + if (ModMain.FrmMain.PageLast.Page != FormMain.PageType.CompDetail) + PanBack.ScrollToHome(); + ModAnimation.AniControlEnabled += 1; + SelectedMods.Clear(); + ReloadCompFileList(); + ChangeAllSelected(false); + ModAnimation.AniControlEnabled -= 1; + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + + // 检查是否为原理图管理界面且首次打开 + if (Conversions.ToBoolean(CurrentCompType == ModComp.CompType.Schematic && + !States.Hint.SchematicFirstTime)) + // 显示首次打开提示 + ModBase.RunInUi(() => + { + ModMain.MyMsgBox("现改为双击文件夹进入子文件夹。", "操作提示", "我知道了"); + States.Hint.SchematicFirstTime = true; + }, true); + + ModMain.FrmMain.KeyDown += FrmMain_KeyDown; + // 调整按钮边距(这玩意儿没法从 XAML 改) + foreach (MyRadioButton Btn in PanFilter.Children) + Btn.LabText.Margin = new Thickness(-2, 0d, 8d, 0d); + } + + /// + /// 刷新 Mod 列表。 + /// + public void ReloadCompFileList(bool ForceReload = false) + { + if (LoaderRun(ForceReload + ? ModLoader.LoaderFolderRunType.ForceRun + : ModLoader.LoaderFolderRunType.RunOnUpdated)) + { + ModBase.Log($"[System] 已刷新 {CurrentCompType} 列表"); + ModFileInfoCache.Clear(); + + ModBase.RunInUi(() => + { + Filter = FilterType.All; + PanBack.ScrollToHome(); + SearchBox.Text = ""; + }); + } + } + + // 强制刷新 + private void RefreshSelf() + { + Refresh(CurrentCompType); + } + + void IRefreshable.Refresh() + { + RefreshSelf(); + } + + public static void Refresh(ModComp.CompType WhichPage) + { + // 强制刷新 + try + { + ModComp.CompProjectCache.Clear(); + ModComp.CompFilesCache.Clear(); + File.Delete(ModBase.PathTemp + @"Cache\LocalComp.json"); + ModBase.Log("[CompResource] 由于点击刷新按钮,清理本地工程信息缓存"); + } + catch (Exception ex) + { + ModBase.Log(ex, "强制刷新时清理本地工程信息缓存失败"); + } + + switch (WhichPage) + { + case ModComp.CompType.Mod: + { + if (ModMain.FrmInstanceMod is not null) + ModMain.FrmInstanceMod.ReloadCompFileList(true); // 无需 Else,还没加载刷个鬼的新 + ModMain.FrmInstanceLeft.ItemMod.Checked = true; + break; + } + case ModComp.CompType.ResourcePack: + { + if (ModMain.FrmInstanceResourcePack is not null) + ModMain.FrmInstanceResourcePack.ReloadCompFileList(true); + ModMain.FrmInstanceLeft.ItemResourcePack.Checked = true; + break; + } + case ModComp.CompType.Shader: + { + if (ModMain.FrmInstanceShader is not null) + ModMain.FrmInstanceShader.ReloadCompFileList(true); + ModMain.FrmInstanceLeft.ItemShader.Checked = true; + break; + } + case ModComp.CompType.Schematic: + { + if (ModMain.FrmInstanceSchematic is not null) + ModMain.FrmInstanceSchematic.ReloadCompFileList(true); + ModMain.FrmInstanceLeft.ItemSchematic.Checked = true; + break; + } + } + + ModMain.Hint("正在刷新……", Log: false); + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanAllBack, null, ModLocalComp.CompResourceListLoader, + _ => LoadUIFromLoaderOutput(), () => CurrentCompType, false); + } + + private void Load_Click(object sender, MouseButtonEventArgs e) + { + if (ModLocalComp.CompResourceListLoader.State == ModBase.LoadState.Failed) + LoaderRun(ModLoader.LoaderFolderRunType.ForceRun); + } + + public bool LoaderRun(ModLoader.LoaderFolderRunType Type) + { + string LoadPath; + if (string.IsNullOrEmpty(CurrentFolderPath)) + // 加载根目录 + LoadPath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + else + // 加载当前文件夹 + LoadPath = CurrentFolderPath; + return ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, LoadPath, Type, + LoaderInput: GetRequireLoaderData()); + } + + #endregion + + #region 文件夹导航 + + /// + /// 当前显示的文件夹路径。空字符串表示根目录。 + /// + public string CurrentFolderPath { get; set; } = ""; + + /// + /// 进入指定的文件夹。 + /// + private void EnterFolder(string folderPath) + { + try + { + if (string.IsNullOrEmpty(folderPath) || !Directory.Exists(folderPath)) + { + ModMain.Hint("文件夹不存在或已被删除", ModMain.HintType.Critical); + return; + } + + CurrentFolderPath = folderPath; + ModBase.Log($"[原理图] 进入文件夹:{folderPath}"); + + ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, folderPath, + ModLoader.LoaderFolderRunType.ForceRun, LoaderInput: GetRequireLoaderData()); + } + catch (Exception ex) + { + ModBase.Log(ex, "进入文件夹失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 进入指定文件夹。 + /// + private void EnterFolderWithCheck(string folderPath) + { + try + { + EnterFolder(folderPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "进入文件夹失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 返回上级文件夹。 + /// + private void GoBackToParentFolder() + { + if (string.IsNullOrEmpty(CurrentFolderPath)) + return; + + try + { + // 获取根路径 + var rootPath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + rootPath = Path.GetFullPath(rootPath.TrimEnd('\\')); + + // 获取父级路径 + var parentPath = Directory.GetParent(CurrentFolderPath)?.FullName; + + // 如果父级路径就是根路径或者父级路径不在根路径范围内,则返回根目录 + if (parentPath is null || parentPath.Equals(rootPath, StringComparison.OrdinalIgnoreCase) || + !parentPath.StartsWith(rootPath + @"\", StringComparison.OrdinalIgnoreCase)) + CurrentFolderPath = ""; + else + CurrentFolderPath = parentPath; + } + catch (Exception ex) + { + ModBase.Log(ex, "路径处理失败"); + // 发生错误时直接返回根目录 + CurrentFolderPath = ""; + } + + ModBase.Log($"[原理图] 返回上级文件夹:{(string.IsNullOrEmpty(CurrentFolderPath) ? "根目录" : CurrentFolderPath)}"); + + // 重新加载当前文件夹的内容 + string LoadPath; + if (string.IsNullOrEmpty(CurrentFolderPath)) + // 返回到根目录 + LoadPath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + else + // 加载当前文件夹 + LoadPath = CurrentFolderPath; + + // 强制刷新UI状态 + // 确保按钮状态正确 + ModBase.RunInUi(() => + BtnManageBack.Visibility = + !string.IsNullOrEmpty(CurrentFolderPath) ? Visibility.Visible : Visibility.Collapsed); + + // 延迟一帧后再加载,确保UI状态已更新 + ModBase.RunInUi( + () => ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, LoadPath, + ModLoader.LoaderFolderRunType.ForceRun, LoaderInput: GetRequireLoaderData()), true); + } + + #endregion + + #region UI 化 + + /// + /// 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawPath。 + /// + public Dictionary ModItems = new(); + + /// + /// 将加载器结果的 Mod 列表加载为 UI。 + /// + private void LoadUIFromLoaderOutput() + { + try + { + // 判断应该显示哪一个页面 + if (ModLocalComp.CompResourceListLoader.Output.Any()) + { + PanBack.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + PanSchematicEmpty.Visibility = Visibility.Collapsed; + } + else + { + // 检查是否为投影文件类型且schematics文件夹不存在 + if (CurrentCompType == ModComp.CompType.Schematic) + { + var schematicsPath = PageInstanceLeft.Instance.PathIndie + @"schematics\"; + if (!Directory.Exists(schematicsPath)) + { + PanSchematicEmpty.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + PanBack.Visibility = Visibility.Collapsed; + return; + } + } + + // 根据组件类型设置PanEmpty的文本内容 + if (CurrentCompType == ModComp.CompType.Schematic) + { + // 检查是否在子文件夹中 + if (!string.IsNullOrEmpty(CurrentFolderPath)) + { + // 子文件夹为空的提示 + TxtEmptyTitle.Text = "该文件夹为空"; + TxtEmptyDescription.Text = "你可以从已经下载好的文件安装资源"; + } + else + { + // 根目录为空的提示 + TxtEmptyTitle.Text = "尚未安装资源"; + TxtEmptyDescription.Text = "你可以从已经下载好的文件安装资源。" + "\r\n" + + "如果你已经安装了资源,可能是版本隔离设置有误,请在设置中调整版本隔离选项。"; + } + } + else + { + TxtEmptyTitle.Text = "尚未安装资源"; + TxtEmptyDescription.Text = "你可以下载新的资源,也可以从已经下载好的文件安装资源。" + "\r\n" + + "如果你已经安装了资源,可能是版本隔离设置有误,请在设置中调整版本隔离选项。"; + } + + // 如果当前在子文件夹中,显示返回上一级按钮 + if (!string.IsNullOrEmpty(CurrentFolderPath)) + BtnHintBack.Visibility = Visibility.Visible; + else + BtnHintBack.Visibility = Visibility.Collapsed; + + PanEmpty.Visibility = Visibility.Visible; + PanBack.Visibility = Visibility.Collapsed; + PanSchematicEmpty.Visibility = Visibility.Collapsed; + return; + } + + // 修改缓存 + ModItems.Clear(); + var rootPath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + rootPath = Path.GetFullPath(rootPath.TrimEnd('\\')); + + var itemsToShow = ModLocalComp.CompResourceListLoader.Output.Where(item => + { + var itemPath = item.IsFolder ? item.ActualPath : item.Path; + var parentDir = Directory.GetParent(itemPath)?.FullName; + if (string.IsNullOrEmpty(CurrentFolderPath)) + return parentDir.Equals(rootPath, StringComparison.OrdinalIgnoreCase); + + return parentDir.Equals(CurrentFolderPath, StringComparison.OrdinalIgnoreCase); + }).ToList(); + + foreach (var ModEntity in itemsToShow) + ModItems[ModEntity.RawPath] = BuildLocalCompItem(ModEntity); + // 显示结果 + ModBase.RunInUi(() => + { + Filter = FilterType.All; + SearchBox.Text = ""; // 这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频 + RefreshUI(); + SetSortMethod(SortMethod.CompName); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, $"加载 {CurrentCompType} 列表 UI 失败", ModBase.LogLevel.Feedback); + } + } + + private MyLocalCompItem BuildLocalCompItem(ModLocalComp.LocalCompFile Entry) + { + try + { + ModAnimation.AniControlEnabled += 1; + var NewItem = new MyLocalCompItem + { + SnapsToDevicePixels = true, + Entry = Entry, + ButtonHandler = BuildLocalCompItemBtnHandler, + Checked = SelectedMods.Contains(Entry.RawPath) + }; + NewItem.CurrentSwipe = CurrentSwipSelect; + NewItem.Tags = Entry.Tags; + Entry.OnCompUpdate += _ => NewItem.Refresh(); + // AddHandler Entry.OnCompUpdate, Sub() RunInUi(Sub() DoSort()) + NewItem.Refresh(); + ModAnimation.AniControlEnabled -= 1; + return NewItem; + } + catch (Exception ex) + { + ModAnimation.AniControlEnabled -= 1; + ModBase.Log(ex, $"创建 UI 项失败:{Entry.RawPath}"); + throw; + } + } + + private void BuildLocalCompItemBtnHandler(MyLocalCompItem sender, EventArgs e) + { + // 点击事件 + sender.Changed += (ss, ee) => CheckChanged((MyLocalCompItem)ss, ee); + if (sender.Entry.IsFolder) + { + // 文件夹项的点击事件:双击进入文件夹,单击切换选中状态 + var lastClickTime = DateTime.MinValue; + sender.Click += (sss, _) => + { + dynamic ss = sss; + var currentTime = DateTime.Now; + var timeDiff = (currentTime - lastClickTime).TotalMilliseconds; + + if (timeDiff <= 300d) + // 300ms内双击,进入文件夹 + EnterFolderWithCheck(ss.Entry.ActualPath); + else + // 单击切换选中状态 + ss.Checked = !ss.Checked; + + lastClickTime = currentTime; + }; + } + else + { + // 文件项的点击事件:切换选中状态 + sender.Click += (sss, _) => + { + dynamic ss = sss; + ss.Checked = !ss.Checked; + }; + } + + // 图标按钮 + var BtnOpen = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonOpen, Tag = sender }; + BtnOpen.ToolTip = "打开文件位置"; + ToolTipService.SetPlacement(BtnOpen, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnOpen, 30d); + ToolTipService.SetHorizontalOffset(BtnOpen, 2d); + BtnOpen.Click += (ss, ee) => Open_Click((MyIconButton)ss, ee); + var BtnCont = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonInfo, Tag = sender }; + BtnCont.ToolTip = "详情"; + ToolTipService.SetPlacement(BtnCont, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnCont, 30d); + ToolTipService.SetHorizontalOffset(BtnCont, 2d); + BtnCont.Click += Info_Click; + sender.MouseRightButtonUp += Info_Click; + var BtnDelete = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonDelete, Tag = sender }; + BtnDelete.ToolTip = "删除"; + ToolTipService.SetPlacement(BtnDelete, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnDelete, 30d); + ToolTipService.SetHorizontalOffset(BtnDelete, 2d); + BtnDelete.Click += (ss, ee) => Delete_Click((MyIconButton)ss, ee); + if (CurrentCompType != ModComp.CompType.Mod || + sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) + { + sender.Buttons = new[] { BtnCont, BtnOpen, BtnDelete }; + } + else + { + var BtnED = new MyIconButton + { + LogoScale = 1d, + Logo = sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine + ? ModBase.Logo.IconButtonStop + : ModBase.Logo.IconButtonCheck, + Tag = sender + }; + BtnED.ToolTip = sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine ? "禁用" : "启用"; + ToolTipService.SetPlacement(BtnED, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnED, 30d); + ToolTipService.SetHorizontalOffset(BtnED, 2d); + BtnED.Click += (ss, ee) => ED_Click((MyIconButton)ss, ee); + sender.Buttons = new[] { BtnCont, BtnOpen, BtnED, BtnDelete }; + } + } + + /// + /// 刷新整个 UI。 + /// + public void RefreshUI() + { + if (PanList is null) + return; + var ShowingMods = (IsSearching ? SearchResult : ModItems.Values.Select(i => i.Entry)) + .Where(m => CanPassFilter(m)).ToList(); + + // 对显示的资源进行排序,确保文件夹置顶 + if (ShowingMods.Any()) + { + var sortMethod = GetSortMethod(CurrentSortMethod); + ShowingMods.Sort((a, b) => sortMethod(a, b)); + } + + // 重新列出列表 + ModAnimation.AniControlEnabled += 1; + if (ShowingMods.Any()) + { + PanList.Visibility = Visibility.Visible; + PanList.Children.Clear(); + foreach (var TargetMod in ShowingMods) + { + if (!ModItems.ContainsKey(TargetMod.RawPath)) + continue; + var Item = ModItems[TargetMod.RawPath]; + + // 确保元素没有父容器,避免重复添加异常 + if (Item.Parent is not null) ((Panel)Item.Parent).Children.Remove(Item); + + ModStyle.MinecraftFormatter.SetColorfulTextLab(Item.LabTitle.Text, Item.LabTitle, + ThemeService.IsDarkMode); + ModStyle.MinecraftFormatter.SetColorfulTextLab(Item.LabInfo.Text, Item.LabInfo, + ThemeService.IsDarkMode); + Item.Checked = SelectedMods.Contains(TargetMod.RawPath); // 更新选中状态 + PanList.Children.Add(Item); + } + } + else + { + PanList.Visibility = Visibility.Collapsed; + } + + ModAnimation.AniControlEnabled -= 1; + SelectedMods = + new HashSet(SelectedMods.Where(m => ShowingMods.Any(s => (s.RawPath ?? "") == (m ?? "")))); + RefreshBars(); + } + + /// + /// 刷新顶栏和底栏显示。 + /// + public void RefreshBars() + { + Dispatcher.BeginInvoke(new Func(async () => + { + // ----------------- + // 顶部栏 + // ----------------- + + // 计数 + var AnyCount = 0; + var EnabledCount = 0; + var DisabledCount = 0; + var UpdateCount = 0; + var UnavalialeCount = 0; + var ItemSource = (IsSearching ? SearchResult : ModItems.Values.Select(i => i.Entry)).ToArray(); + await Task.Run(() => + { + foreach (var item in ItemSource) + { + AnyCount += 1; + if (item.CanUpdate) UpdateCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) EnabledCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) DisabledCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) UnavalialeCount += 1; + } + }); + // 显示 + BtnFilterAll.Text = (IsSearching ? "搜索结果" : "全部") + $" ({AnyCount})"; + BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"; + BtnFilterCanUpdate.Visibility = Filter == FilterType.CanUpdate || UpdateCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterEnabled.Text = $"启用 ({EnabledCount})"; + BtnFilterEnabled.Visibility = Filter == FilterType.Enabled || (EnabledCount > 0 && EnabledCount < AnyCount) + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"; + BtnFilterDisabled.Visibility = Filter == FilterType.Disabled || DisabledCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterError.Text = $"错误 ({UnavalialeCount})"; + BtnFilterError.Visibility = Filter == FilterType.Unavailable || UnavalialeCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + // 查找重复项目 + var DuplicateItems = await Task.Run(() => ItemSource.GroupBy(m => + { + if (m.Comp is null) return ":Nothing:"; + + return m.Comp.Id; + }).Where(g => g.Count() > 1 && g.First().Comp is not null).SelectMany(g => g).ToList()); + BtnFilterDuplicate.Text = $"重复 ({DuplicateItems.Count})"; + BtnFilterDuplicate.Visibility = Filter == FilterType.Duplicate || DuplicateItems.Any() + ? Visibility.Visible + : Visibility.Collapsed; + + // 返回按钮显示控制(在子文件夹中时显示) + if (!string.IsNullOrEmpty(CurrentFolderPath)) + BtnManageBack.Visibility = Visibility.Visible; + else + BtnManageBack.Visibility = Visibility.Collapsed; + + // ----------------- + // 底部栏 + // ----------------- + + // 计数 + var NewCount = SelectedMods.Count; + var Selected = NewCount > 0; + if (Selected) + LabSelect.Text = $"已选择 {NewCount} 个文件"; // 取消所有选择时不更新数字 + // 按钮可用性 + if (Selected) + { + var HasUpdate = false; + var HasEnabled = false; + var HasDisabled = false; + var CanFavoriteAndShare = true; // 是否可以收藏和分享 + + + // 检查是否所有选中的资源都有有效的项目信息(即已完成联网更新) + await Task.Run(() => + { + foreach (var ModEntity in ModLocalComp.CompResourceListLoader.Output) + if (SelectedMods.Contains(ModEntity.RawPath)) + { + if (ModEntity.CanUpdate) HasUpdate = true; + if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + HasEnabled = true; + else if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) + HasDisabled = true; + if (ModEntity.Comp is null || string.IsNullOrEmpty(ModEntity.Comp.Id)) + CanFavoriteAndShare = false; + } + }); + + BtnSelectDisable.IsEnabled = HasEnabled; + BtnSelectEnable.IsEnabled = HasDisabled; + BtnSelectUpdate.IsEnabled = HasUpdate; + + // 针对投影原理图隐藏分享 更新 收藏按钮 + if (CurrentCompType == ModComp.CompType.Schematic) + { + BtnSelectUpdate.Visibility = Visibility.Collapsed; + BtnSelectFavorites.Visibility = Visibility.Collapsed; + BtnSelectShare.Visibility = Visibility.Collapsed; + } + else + { + BtnSelectUpdate.Visibility = Visibility.Visible; + BtnSelectFavorites.Visibility = Visibility.Visible; + BtnSelectShare.Visibility = Visibility.Visible; + + // 根据是否已加载项目信息来启用/禁用收藏和分享按钮 + BtnSelectFavorites.IsEnabled = CanFavoriteAndShare; + BtnSelectShare.IsEnabled = CanFavoriteAndShare; + } + } + + // 更新显示状态 + if (ModAnimation.AniControlEnabled == 0) + { + PanListBack.Margin = new Thickness(0d, 0d, 0d, Selected ? 95 : 15); + if (Selected) + { + // 仅在数量增加时播放出现/跳跃动画 + if (BottomBarShownCount >= NewCount) + { + BottomBarShownCount = NewCount; + return; + } + + BottomBarShownCount = NewCount; + // 出现/跳跃动画 + CardSelect.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, 1d - CardSelect.Opacity, 60), + ModAnimation.AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, 3d, 150, 120, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, -1, 90, 270, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)) + }, "Mod Sidebar"); + } + else + { + // 不重复播放隐藏动画 + if (BottomBarShownCount == 0) + return; + BottomBarShownCount = 0; + // 隐藏动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, -CardSelect.Opacity, 90), + ModAnimation.AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => CardSelect.Visibility = Visibility.Collapsed, After: true) + }, "Mod Sidebar"); + } + } + else + { + ModAnimation.AniStop("Mod Sidebar"); + BottomBarShownCount = NewCount; + if (Selected) + { + CardSelect.Visibility = Visibility.Visible; + CardSelect.Opacity = 1d; + TransSelect.Y = -25; + } + else + { + CardSelect.Visibility = Visibility.Collapsed; + CardSelect.Opacity = 0d; + TransSelect.Y = -10; + } + } + })); + } + + private int BottomBarShownCount; + + #endregion + + #region 管理 + + /// + /// 打开 Mods 文件夹。 + /// + private void BtnManageBack_Click(object sender, EventArgs e) + { + GoBackToParentFolder(); + } + + private void BtnHintBack_Click(object sender, EventArgs e) + { + GoBackToParentFolder(); + } + + private void BtnManageOpen_Click(object sender, EventArgs e) + { + try + { + string CompFilePath; + + // 如果当前在子文件夹中,则打开当前子文件夹;否则打开根目录 + if (string.IsNullOrEmpty(CurrentFolderPath)) + // 打开根目录 + CompFilePath = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + else + // 打开当前子文件夹 + CompFilePath = CurrentFolderPath.EndsWith(@"\") ? CurrentFolderPath : CurrentFolderPath + @"\"; + Directory.CreateDirectory(CompFilePath); + ModBase.OpenExplorer(CompFilePath); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开 Mods 文件夹失败", ModBase.LogLevel.Msgbox); + } + } + + + /// + /// 全选。 + /// + private void BtnManageSelectAll_Click(object sender, MouseButtonEventArgs e) + { + ChangeAllSelected(SelectedMods.Count < PanList.Children.Count); + } + + /// + /// 安装 Mod。 + /// + private void BtnManageInstall_Click(object sender, MouseButtonEventArgs e) + { + string[] FileList = null; + switch (CurrentCompType) + { + case ModComp.CompType.Mod: + { + FileList = SystemDialogs.SelectFiles( + "Mod 文件(*.jar;*.litemod;*.disabled;*.old)|*.jar;*.litemod;*.disabled;*.old", "选择要安装的 Mod"); + break; + } + case ModComp.CompType.ResourcePack: + { + FileList = SystemDialogs.SelectFiles("资源包文件(*.zip)|*.zip", "选择要安装的资源包"); + break; + } + case ModComp.CompType.Shader: + { + FileList = SystemDialogs.SelectFiles("光影包文件(*.zip)|*.zip", "选择要安装的光影包"); + break; + } + case ModComp.CompType.Schematic: + { + FileList = SystemDialogs.SelectFiles( + "投影原理图文件(*.litematic;*.nbt;*.schematic;*.schem)|*.litematic;*.nbt;*.schematic;*.schem", + "选择要安装的投影原理图"); + break; + } + } + + if (FileList is null || !FileList.Any()) + return; + InstallCompFiles(FileList, CurrentCompType, CurrentFolderPath); + } + + /// + /// 尝试安装 Mod。 + /// 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。 + /// + public static bool InstallMods(IEnumerable filePathList) + { + if (!filePathList.Any()) return false; + + // 1. Check file extension + var firstFile = filePathList.First(); + var extension = firstFile.Split('.').LastOrDefault()?.ToLower(); + string[] allowedExtensions = { "jar", "litemod", "disabled", "old" }; + + if (!allowedExtensions.Contains(extension)) return false; + + LogWrapper.Info("[System] 文件格式为 jar/litemod,尝试安装为 Mod"); + + // 2. Check recycle bin + if (firstFile.Contains(@":\$RECYCLE.BIN\")) + { + HintWrapper.Show("请先将文件从回收站还原,再尝试安装!", HintTheme.Error); + return true; + } + + // 3. Determine target instance + var targetInstance = ModMinecraft.McInstanceSelected; + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup) targetInstance = PageInstanceLeft.Instance; + + // 4. Validate instance status + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSelect || targetInstance == null || + !targetInstance.Modable) + { + HintWrapper.Show("若要安装 Mod,请先选择一个可以安装 Mod 的实例!"); + return true; + } + + // 5. Check if user confirmation is required + var isModPage = ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmMain.PageCurrentSub == FormMain.PageSubType.VersionMod; + + if (!isModPage) + { + var countSuffix = filePathList.Count() == 1 ? "个" : "些"; + if (ModMain.MyMsgBox($"是否要将这{countSuffix}文件作为 Mod 安装到 {targetInstance.Name}?", "Mod 安装确认", "确定", "取消") != + 1) return true; + } + + // 6. Execution: Install Mods + ExecuteModInstallation(targetInstance, filePathList, isModPage); + + return true; + } + + private static void ExecuteModInstallation(ModMinecraft.McInstance targetInstance, IEnumerable filePathList, + bool refreshList) + { + // Path resolution logic + var modPathSuffix = targetInstance.Info.HasLabyMod + ? $@"labymod-neo\fabric\{targetInstance.Info.VanillaName}\" + : ""; + var modFolder = $@"{targetInstance.PathIndie}{modPathSuffix}mods\"; + + try + { + foreach (var modFile in filePathList) + { + var fileName = ModBase.GetFileNameFromPath(modFile) + .Replace(".disabled", "") + .Replace(".old", ""); + + if (!fileName.Contains(".")) fileName += ".jar"; // Ensure extension (#4227) + + ModBase.CopyFile(modFile, Path.Combine(modFolder, fileName)); + } + + // Success hint + if (filePathList.Count() == 1) + { + var installedName = ModBase.GetFileNameFromPath(filePathList.First()).Replace(".disabled", "") + .Replace(".old", ""); + HintWrapper.Show($"已安装 {installedName}!", HintTheme.Success); + } + else + { + HintWrapper.Show($"已安装 {filePathList.Count()} 个 Mod!", HintTheme.Success); + } + + // 7. Refresh list if necessary + if (refreshList) + ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, + modFolder, + ModLoader.LoaderFolderRunType.ForceRun, + LoaderInput: ModMain.FrmInstanceMod.GetRequireLoaderData() + ); + } + catch (Exception ex) + { + LogWrapper.Error(ex, "拷贝文件失败"); + } + } + + /// + /// 安装组件文件(Mod、资源包、光影包、投影文件等)。 + /// + public static void InstallCompFiles(IEnumerable FilePathList, ModComp.CompType CompType, + string TargetFolderPath = "") + { + if (!FilePathList.Any()) + return; + + var Extension = FilePathList.First().AfterLast(".").ToLower(); + string[] ValidExtensions = null; + var CompTypeName = ""; + var CompFolder = ""; + + // 检查回收站:回收站中的文件有错误的文件名 + if (FilePathList.First().Contains(@":\$RECYCLE.BIN\")) + { + ModMain.Hint("请先将文件从回收站还原,再尝试安装!", ModMain.HintType.Critical); + return; + } + + // 获取并检查目标实例 + var targetInstance = ModMinecraft.McInstanceSelected; + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup) + targetInstance = PageInstanceLeft.Instance; + + // 根据组件类型设置相关参数 + switch (CompType) + { + case ModComp.CompType.Mod: + { + ValidExtensions = new[] { "jar", "litemod", "disabled", "old" }; + CompTypeName = "Mod"; + if (string.IsNullOrEmpty(TargetFolderPath)) + CompFolder = targetInstance.PathIndie + + (targetInstance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + targetInstance.Info.VanillaName + @"\" + : "") + @"mods\"; + else + CompFolder = TargetFolderPath + @"\"; + + break; + } + case ModComp.CompType.ResourcePack: + { + ValidExtensions = new[] { "zip" }; + CompTypeName = "资源包"; + if (string.IsNullOrEmpty(TargetFolderPath)) + CompFolder = targetInstance.PathIndie + @"resourcepacks\"; + else + CompFolder = TargetFolderPath + @"\"; + + break; + } + case ModComp.CompType.Shader: + { + ValidExtensions = new[] { "zip" }; + CompTypeName = "光影包"; + if (string.IsNullOrEmpty(TargetFolderPath)) + CompFolder = targetInstance.PathIndie + @"shaderpacks\"; + else + CompFolder = TargetFolderPath + @"\"; + + break; + } + case ModComp.CompType.Schematic: + { + ValidExtensions = new[] { "litematic", "nbt", "schematic", "schem" }; + CompTypeName = "投影原理图"; + if (string.IsNullOrEmpty(TargetFolderPath)) + CompFolder = targetInstance.PathIndie + @"schematics\"; + else + CompFolder = TargetFolderPath + @"\"; + + break; + } + } + + // 检查文件扩展名 + if (!ValidExtensions.Contains(Extension)) + { + ModMain.Hint($"不支持的文件格式:{Extension},{CompTypeName}支持的格式:{string.Join(", ", ValidExtensions)}", + ModMain.HintType.Critical); + return; + } + + ModBase.Log($"[System] 文件为 {Extension} 格式,尝试作为{CompTypeName}安装"); + + // 检查实例兼容性 + if (CompType == ModComp.CompType.Mod && (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSelect || + targetInstance is null || !targetInstance.Modable)) + { + ModMain.Hint("若要安装 Mod,请先选择一个可以安装 Mod 的实例!"); + return; + } + + // 确认安装 + var CurrentPage = FormMain.PageSubType.VersionMod; + switch (CompType) + { + case ModComp.CompType.Mod: + { + CurrentPage = FormMain.PageSubType.VersionMod; + break; + } + case ModComp.CompType.ResourcePack: + { + CurrentPage = FormMain.PageSubType.VersionResourcePack; + break; + } + case ModComp.CompType.Shader: + { + CurrentPage = FormMain.PageSubType.VersionShader; + break; + } + case ModComp.CompType.Schematic: + { + CurrentPage = FormMain.PageSubType.VersionSchematic; + break; + } + } + + if (!(ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmMain.PageCurrentSub == CurrentPage)) + if (ModMain.MyMsgBox( + $"是否要将这{(FilePathList.Count() == 1 ? "个" : "些")}文件作为{CompTypeName}安装到 {targetInstance.Name}?", + $"{CompTypeName}安装确认", "确定", "取消") != 1) + return; + + // 执行安装 + try + { + Directory.CreateDirectory(CompFolder); + foreach (var FilePath in FilePathList) + { + var NewFileName = ModBase.GetFileNameFromPath(FilePath); + if (CompType == ModComp.CompType.Mod) + { + NewFileName = NewFileName.Replace(".disabled", "").Replace(".old", ""); + if (!NewFileName.Contains(".")) + NewFileName += ".jar"; + } + + var DestFile = CompFolder + NewFileName; + if (File.Exists(DestFile)) + if (ModMain.MyMsgBox($"已存在同名文件:{NewFileName},是否要覆盖?", "文件覆盖确认", "覆盖", "取消") != 1) + continue; + + ModBase.CopyFile(FilePath, DestFile); + } + + if (FilePathList.Count() == 1) + ModMain.Hint($"已安装 {ModBase.GetFileNameFromPath(FilePathList.First())}!", ModMain.HintType.Finish); + else + ModMain.Hint($"已安装 {FilePathList.Count()} 个{CompTypeName}!", ModMain.HintType.Finish); + + // 刷新列表 + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmMain.PageCurrentSub == CurrentPage) + switch (CompType) + { + case ModComp.CompType.Mod: + { + if (ModMain.FrmInstanceMod is not null) + ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, CompFolder, + ModLoader.LoaderFolderRunType.ForceRun, + LoaderInput: ModMain.FrmInstanceMod?.GetRequireLoaderData()); + + break; + } + case ModComp.CompType.ResourcePack: + case ModComp.CompType.Shader: + case ModComp.CompType.Schematic: + { + var CurrentForm = GetCurrentCompResourceForm(); + if (CurrentForm is not null) ModBase.RunInUi(() => CurrentForm.ReloadCompFileList(true)); + + break; + } + } + } + + catch (Exception ex) + { + ModBase.Log(ex, $"复制{CompTypeName}文件失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 获取当前的组件资源管理窗体。 + /// + private static PageInstanceCompResource GetCurrentCompResourceForm() + { + switch (ModMain.FrmMain.PageCurrentSub) + { + case FormMain.PageSubType.VersionMod: + { + return ModMain.FrmInstanceMod; + } + case FormMain.PageSubType.VersionResourcePack: + { + return ModMain.FrmInstanceResourcePack; + } + case FormMain.PageSubType.VersionShader: + { + return ModMain.FrmInstanceShader; + } + case FormMain.PageSubType.VersionSchematic: + { + return ModMain.FrmInstanceSchematic; + } + + default: + { + return null; + } + } + } + + private void BtnManageInfoExport_Click(object sender, MouseButtonEventArgs e) + { + var Choice = + ModMain.MyMsgBox( + "TXT 格式:仅导出当前的资源文件名称信息,通常足够他人获取已安装的资源信息" + "\r\n" + + "CSV 格式:导出详细的资源信息,包括其文件名,工程的 ID,文件内版本信息等详细信息", "选择导出模式", "TXT 格式", "CSV 格式", "取消"); + + void ExportText(string Content, string FileName) + { + try + { + var savePath = + SystemDialogs.SelectSaveFile("选择保存位置", FileName, "文本文件(*.txt)|*.txt|CSV 文件(*.csv)|*.csv"); + if (string.IsNullOrWhiteSpace(savePath)) return; + File.WriteAllText(savePath, Content, Encoding.UTF8); + ModBase.OpenExplorer(savePath); + } + catch (Exception ex) + { + ModBase.Log(ex, "导出资源信息失败", ModBase.LogLevel.Msgbox); + } + } + + ; + switch (Choice) + { + case 1: // TXT + { + var ExportContent = new List(); + foreach (var ModEntity in ModLocalComp.CompResourceListLoader.Output) + ExportContent.Add(ModEntity.FileName); + ExportText(ExportContent.Join("\r\n"), PageInstanceLeft.Instance.Name + "已安装的资源信息.txt"); + break; + } + + case 2: // CSV + { + var ExportContent = new List(); + ExportContent.Add("文件名,资源名称,资源版本,此版本更新时间,Mod ID,对应平台工程 ID,文件大小(字节),文件路径"); + foreach (var ModEntity in ModLocalComp.CompResourceListLoader.Output) + ExportContent.Add( + $"{ModEntity.FileName},{ModEntity.Comp?.TranslatedName},{ModEntity.Version},{ModEntity.CompFile?.ReleaseDate},{ModEntity.ModId},{ModEntity.Comp?.Id},{GetModFileInfo(ModEntity.Path).Length},{ModEntity.Path}"); + ExportText(ExportContent.Join("\r\n"), PageInstanceLeft.Instance.Name + "已安装的资源信息.csv"); + break; + } + } + } + + /// + /// 下载 Mod。 + /// + private void BtnManageDownload_Click(object sender, MouseButtonEventArgs e) + { + switch (CurrentCompType) + { + case ModComp.CompType.Mod: + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod); + break; + } + case ModComp.CompType.ResourcePack: + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadResourcePack); + break; + } + case ModComp.CompType.Shader: + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadShader); + break; + } + } + + PageComp.TargetVersion = PageInstanceLeft.Instance; // 将当前实例设置为筛选器 + } + + /// + /// 下载投影Mod按钮点击事件。 + /// + private void BtnSchematicDownloadMod_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod); + PageComp.TargetVersion = PageInstanceLeft.Instance; // 将当前实例设置为筛选器 + } + + /// + /// 实例选择按钮点击事件。 + /// + private void BtnSchematicVersionSelect_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Launch); + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSelect); + } + + #endregion + + #region 选择 + + /// + /// 选择的 Mod 的路径(不含 .disabled 和 .old)。 + /// + public HashSet SelectedMods = new(); + + // 单项切换选择状态 + public void CheckChanged(MyLocalCompItem sender, ModBase.RouteEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + // 更新选择了的内容 + var SelectedKey = sender.Entry.RawPath; + if (sender.Checked) + SelectedMods.Add(SelectedKey); + else + SelectedMods.Remove(SelectedKey); + RefreshBars(); + } + + // 切换所有项的选择状态 + private void ChangeAllSelected(bool Value) + { + ModAnimation.AniControlEnabled += 1; + SelectedMods.Clear(); + foreach (var Item in ModItems.Values) + { + // #4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断 + var ShouldSelected = Value && PanList.Children.Contains(Item); + Item.Checked = ShouldSelected; + if (ShouldSelected) + SelectedMods.Add(Item.Entry.RawPath); + } + + ModAnimation.AniControlEnabled -= 1; + RefreshBars(); + } + + private void UnselectedAllWithAnimation() + { + var CacheAniControlEnabled = ModAnimation.AniControlEnabled; + ModAnimation.AniControlEnabled = 0; + ChangeAllSelected(false); + ModAnimation.AniControlEnabled += CacheAniControlEnabled; + } + + private void FrmMain_KeyDown(object sender, KeyEventArgs e) // 若监听自己的事件则在进入页面后需点击右侧控件才可监听到 (#4311) + { + if (!ReferenceEquals(ModMain.FrmMain.PageRight, this)) + return; + if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A) + ChangeAllSelected(true); + } + + private void SearchBox_PreviewKeyDown(object sender, KeyEventArgs e) + { + // Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获 + if (SearchBox.Text.Any()) + return; + if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A) + PanBack.Focus(); + } + + #endregion + + #region 筛选 + + private FilterType _Filter = FilterType.All; + + public FilterType Filter + { + get => _Filter; + set + { + if (_Filter == value) + return; + _Filter = value; + switch (value) + { + case FilterType.All: + { + BtnFilterAll.Checked = true; + break; + } + case FilterType.Enabled: + { + BtnFilterEnabled.Checked = true; + break; + } + case FilterType.Disabled: + { + BtnFilterDisabled.Checked = true; + break; + } + case FilterType.CanUpdate: + { + BtnFilterCanUpdate.Checked = true; + break; + } + case FilterType.Duplicate: + { + BtnFilterDuplicate.Checked = true; + break; + } + + default: + { + BtnFilterError.Checked = true; + break; + } + } + + RefreshUI(); + } + } + + public enum FilterType + { + All = 0, + Enabled = 1, + Disabled = 2, + CanUpdate = 3, + Unavailable = 4, + Duplicate = 5 + } + + /// + /// 检查该 Mod 项是否符合当前筛选的类别。 + /// + private bool CanPassFilter(ModLocalComp.LocalCompFile CheckingMod) + { + switch (Filter) + { + case FilterType.All: + { + return true; + } + case FilterType.Enabled: + { + return CheckingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine; + } + case FilterType.Disabled: + { + return CheckingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled; + } + case FilterType.CanUpdate: + { + return CheckingMod.CanUpdate; + } + case FilterType.Unavailable: + { + return CheckingMod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable; + } + case FilterType.Duplicate: + { + var ItemSource = IsSearching + ? SearchResult + : ModLocalComp.CompResourceListLoader.Output ?? new List(); + return ItemSource is not null && ItemSource.Where(m => + CheckingMod.Comp is not null && m.Comp is not null && + (CheckingMod.Comp.Id ?? "") == (m.Comp.Id ?? "")).Any(); + } + + default: + { + return false; + } + } + } + + // 点击筛选项触发的改变 + private void ChangeFilter(MyRadioButton sender, bool raiseByMouse) + { + Filter = (FilterType)Conversions.ToInteger(sender.Tag); + RefreshUI(); + DoSort(); + } + + #endregion + + #region 排序 + + private SortMethod CurrentSortMethod = SortMethod.CompName; + + private void SetSortMethod(SortMethod Target) + { + CurrentSortMethod = Target; + BtnSort.Text = $"排序:{GetSortName(Target)}"; + // RefreshUI() + DoSort(); + } + + private enum SortMethod + { + FileName, + CompName, + TagNums, + CreateTime, + ModFileSize + } + + private string GetSortName(SortMethod Method) + { + switch (Method) + { + case SortMethod.FileName: + { + return "文件名"; + } + case SortMethod.CompName: + { + return "资源名称"; + } + case SortMethod.TagNums: + { + return "标签数量"; + } + case SortMethod.CreateTime: + { + return "加入时间"; + } + case SortMethod.ModFileSize: + { + return "文件大小"; + } + + default: + { + return "资源名称"; + } + } + + return ""; + } + + private void BtnSortClick(object sender, ModBase.RouteEventArgs e) + { + var Body = new ContextMenu(); + foreach (SortMethod i in Enum.GetValues(typeof(SortMethod))) + { + var Item = new MyMenuItem(); + Item.Header = GetSortName(i); + Item.Click += (_, _) => SetSortMethod(i); + Body.Items.Add(Item); + } + + Body.PlacementTarget = (UIElement)sender; + Body.Placement = PlacementMode.Bottom; + Body.IsOpen = true; + } + + private readonly object SortLock = new(); + + private void DoSort() + { + lock (SortLock) + { + try + { + if (PanList is null || PanList.Children.Count < 2) + return; + + // 将子元素转换为可排序的列表 + var items = PanList.Children.OfType().ToList(); + var Method = GetSortMethod(CurrentSortMethod); + + // 分离有效和无效项(保持原始相对顺序) + var invalid = items.Where(i => + i.Entry is null || (CurrentSortMethod == SortMethod.TagNums && i.Entry.Comp is null && + !i.Entry.IsFolder)).ToList(); + var valid = items.Except(invalid).ToList(); + // 仅对有效项进行排序 + valid.Sort((x, y) => Method(x.Entry, y.Entry)); + // 合并保持无效项的原始顺序 + items = valid.Concat(invalid).ToList(); + + // 批量更新UI元素 + PanList.Children.Clear(); + items.ForEach(i => PanList.Children.Add(i)); + } + + catch (Exception ex) + { + ModBase.Log(ex, "执行排序时出错", ModBase.LogLevel.Hint); + } + } + } + + private Func GetSortMethod(SortMethod Method) + { + // 通用的文件夹置顶比较函数 + int folderFirstCompare(ModLocalComp.LocalCompFile a, ModLocalComp.LocalCompFile b) + { + if (a.IsFolder && !b.IsFolder) + return -1; + if (!a.IsFolder && b.IsFolder) + return 1; + return 0; // 相同类型,需要进一步比较 + } + + ; + + switch (Method) + { + case SortMethod.FileName: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹或都是文件,则按文件名排序 + return string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase); + }; + } + case SortMethod.CompName: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹或都是文件,则按资源名称排序 + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + }; + } + case SortMethod.TagNums: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹,则按名称排序 + if (a.IsFolder && b.IsFolder) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + // 如果都是文件,则按标签数量排序(标签多的在前) + if (!a.IsFolder && !b.IsFolder) + { + // 安全检查,确保Comp不为空 + var aTagCount = a.Comp?.Tags?.Count ?? 0; + var bTagCount = b.Comp?.Tags?.Count ?? 0; + return bTagCount.CompareTo(aTagCount); + } + + // 理论上不会到达这里,但为了安全起见 + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + }; + } + case SortMethod.CreateTime: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹或都是文件,则按创建时间排序(新的在前) + var aPath = a.IsFolder ? a.ActualPath : a.Path; + var bPath = b.IsFolder ? b.ActualPath : b.Path; + var aDate = GetModFileInfo(aPath).CreationTime; + var bDate = GetModFileInfo(bPath).CreationTime; + if (aDate == DateTime.MinValue && bDate == DateTime.MinValue) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + + if (aDate == DateTime.MinValue) return 1; // 出错的文件排在后面 + + if (bDate == DateTime.MinValue) return -1; + return bDate.CompareTo(aDate); + }; + } + case SortMethod.ModFileSize: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹,则按名称排序 + if (a.IsFolder && b.IsFolder) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + // 如果都是文件,则按文件大小排序(大的在前) + if (!a.IsFolder && !b.IsFolder) + { + var aSize = GetModFileInfo(a.ActualPath).Length; + var bSize = GetModFileInfo(b.ActualPath).Length; + if (aSize == 0L && bSize == 0L) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + + if (aSize == 0L) return 1; + + if (bSize == 0L) return -1; + return bSize.CompareTo(aSize); + } + + // 理论上不会到达这里,但为了安全起见 + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + }; + } + + default: + { + return (a, b) => + { + // 文件夹始终排在最前面 + var folderResult = folderFirstCompare(a, b); + if (folderResult != 0) + return folderResult; + // 如果都是文件夹或都是文件,则按名称排序 + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + }; + } + } + } + + #endregion + + #region 下边栏 + + // 启用 / 禁用 + private void BtnSelectED_Click(object sender, ModBase.RouteEventArgs e) + { + EDMods(ModLocalComp.CompResourceListLoader.Output.Where(m => SelectedMods.Contains(m.RawPath)).ToList(), + !sender.Equals(BtnSelectDisable)); + ChangeAllSelected(false); + } + + private void EDMods(IEnumerable ModList, bool IsEnable) + { + var IsSuccessful = true; + foreach (var ModE in ModList) + { + var ModEntity = ModE; // 仅用于去除迭代变量无法修改的限制 + string NewPath = null; + if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine && !IsEnable) + // 禁用 + NewPath = ModEntity.Path + (File.Exists(ModEntity.Path + ".old") ? ".old" : ".disabled"); + else if (ModEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled && IsEnable) + // 启用 + NewPath = ModEntity.RawPath; + else + continue; + // 重命名 + try + { + if (File.Exists(NewPath)) + { + if (File.Exists(ModEntity.Path)) + { + // 同时存在两个名称的 Mod + if ((ModBase.GetFileMD5(ModEntity.Path) ?? "") != (ModBase.GetFileMD5(NewPath) ?? "")) + { + ModMain.MyMsgBox( + $"目前同时存在启用和禁用的两个 Mod 文件:{"\r\n"} - {NewPath}{"\r\n"} - {ModEntity.Path}{"\r\n"}{"\r\n"}注意,这两个文件的内容并不相同。{"\r\n"}在手动删除或重命名其中一个文件后,才能继续操作。", + "存在文件冲突"); + continue; + } + } + else + { + // 已经重命名过了 + ModBase.Log("[Mod] Mod 的状态已被切换", ModBase.LogLevel.Debug); + continue; + } + } + + File.Delete(NewPath); + FileSystem.Rename(ModEntity.Path, NewPath); + } + catch (FileNotFoundException ex) + { + ModBase.Log(ex, $"未找到需要重命名的 Mod({ModEntity.Path ?? "null"})", ModBase.LogLevel.Feedback); + ReloadCompFileList(true); + return; + } + catch (Exception ex) + { + ModBase.Log(ex, $"重命名 Mod 失败({ModEntity.Path ?? "null"})"); + IsSuccessful = false; + } + + // 更改 Loader 中的列表 + var NewModEntity = new ModLocalComp.LocalCompFile(NewPath); + NewModEntity.FromJson(ModEntity.ToJson()); + if (ModLocalComp.CompResourceListLoader.Output.Contains(ModEntity)) + { + var IndexOfLoader = ModLocalComp.CompResourceListLoader.Output.IndexOf(ModEntity); + ModLocalComp.CompResourceListLoader.Output.RemoveAt(IndexOfLoader); + ModLocalComp.CompResourceListLoader.Output.Insert(IndexOfLoader, NewModEntity); + } + + if (SearchResult is not null && SearchResult.Contains(ModEntity)) // #4862 + { + var IndexOfResult = SearchResult.IndexOf(ModEntity); + SearchResult.Remove(ModEntity); + SearchResult.Insert(IndexOfResult, NewModEntity); + } + + // 更改 UI 中的列表 + try + { + var NewItem = BuildLocalCompItem(NewModEntity); + ModItems[ModEntity.RawPath] = NewItem; + var IndexOfUi = PanList.Children.IndexOf(PanList.Children.OfType() + .FirstOrDefault(i => ReferenceEquals(i.Entry, ModEntity))); + if (IndexOfUi == -1) + continue; // 因为未知原因 Mod 的状态已经切换完了 + PanList.Children.RemoveAt(IndexOfUi); + PanList.Children.Insert(IndexOfUi, NewItem); + } + catch (Exception ex) + { + ModBase.Log(ex, $"更新 UI 列表项失败:{ModEntity.FileName}", ModBase.LogLevel.Hint); + } + } + + Dispatcher.Invoke(() => PanList.UpdateLayout(), DispatcherPriority.Background); + if (IsSuccessful) + { + RefreshBars(); + } + else + { + ModMain.Hint("由于文件被占用,Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", ModMain.HintType.Critical); + ReloadCompFileList(true); + } + + LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly); + } + + // 更新 + private void BtnSelectUpdate_Click(object sender, ModBase.RouteEventArgs e) + { + var UpdateList = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedMods.Contains(m.RawPath) && m.CanUpdate).ToList(); + if (!UpdateList.Any()) + return; + UpdateResource(UpdateList); + ChangeAllSelected(false); + } + + /// + /// 记录正在进行 Mod 更新的 mods 文件夹路径。 + /// + public static List UpdatingVersions = new(); + + public void UpdateResource(IEnumerable ModList) + { + // 更新前警告 + if (Conversions.ToBoolean(CurrentCompType == ModComp.CompType.Mod && + (!States.Hint.UpdateMod || ModList.Count() >= 15))) + { + if (ModMain.MyMsgBox( + $"新版本 Mod 可能不兼容旧存档或者其他 Mod,这可能导致游戏崩溃,甚至永久损坏存档!{"\r\n"}如果你在游玩整合包,请千万不要自行更新 Mod!{"\r\n"}{"\r\n"}在更新前,请先备份存档,并检查 Mod 的更新日志。{"\r\n"}如果更新后出现问题,你也可以在回收站找回更新前的 Mod。", + "Mod 更新警告", "我已了解风险,继续更新", "取消", IsWarn: true) == 1) + States.Hint.UpdateMod = true; + else + return; + } + + try + { + // 构造下载信息 + ModList = ModList.ToList(); // 防止刷新影响迭代器 + var FileList = new List(); + var FileCopyList = new Dictionary(); + foreach (var Entry in ModList) + { + var File = Entry.UpdateFile; + if (!File.Available) + continue; + // 确认更新后的文件名 + var CurrentReplaceName = Entry.CompFile.FileName.Replace(".jar", "").Replace(".old", "") + .Replace(".disabled", ""); + var NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "") + .Replace(".disabled", ""); + var CurrentSegs = CurrentReplaceName.Split('-').ToList(); + var NewestSegs = NewestReplaceName.Split('-').ToList(); + var Shortened = false; + while (true) // 移除前导相同部分(不能移除所有相同项,这会导致例如 1.2-forge-2 和 1.3-forge-3 中间的 forge 被去掉,导致尝试替换 1.2-2) + { + if (!CurrentSegs.Any() || !NewestSegs.Any()) + break; + if ((CurrentSegs.First() ?? "") != (NewestSegs.First() ?? "")) + break; + CurrentSegs.RemoveAt(0); + NewestSegs.RemoveAt(0); + Shortened = true; + } + + while (true) // 移除后导相同部分 + { + if (!CurrentSegs.Any() || !NewestSegs.Any()) + break; + if ((CurrentSegs.Last() ?? "") != (NewestSegs.Last() ?? "")) + break; + CurrentSegs.RemoveAt(CurrentSegs.Count - 1); + NewestSegs.RemoveAt(NewestSegs.Count - 1); + Shortened = true; + } + + if (Shortened && CurrentSegs.Any() && NewestSegs.Any()) + { + CurrentReplaceName = CurrentSegs.Join("-"); + NewestReplaceName = NewestSegs.Join("-"); + } + + // 添加到下载列表 + var TempAddress = ModBase.PathTemp + @"DownloadedComp\" + + Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName); + var RealAddress = ModBase.GetPathFromFullPath(Entry.Path) + + Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName); + FileList.Add(File.ToNetFile(TempAddress)); + FileCopyList[TempAddress] = RealAddress; + } + + // 构造加载器 + var InstallLoaders = new List(); + var FinishedFileNames = new List(); + InstallLoaders.Add(new ModNet.LoaderDownload("下载新版资源文件", FileList) + { ProgressWeight = ModList.Count() * 1.5d }); // 每个 Mod 需要 1.5s + InstallLoaders.Add(new ModLoader.LoaderTask("替换旧版资源文件", _ => + { + try + { + foreach (var Entry in ModList) + if (File.Exists(Entry.Path)) + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Path, UIOption.AllDialogs, + RecycleOption.SendToRecycleBin); + else + ModBase.Log($"[CompUpdate] 未找到更新前的资源文件,跳过对它的删除:{Entry.Path}", ModBase.LogLevel.Debug); + + foreach (var Entry in FileCopyList) + { + if (File.Exists(Entry.Value)) + { + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Value, UIOption.AllDialogs, + RecycleOption.SendToRecycleBin); + ModBase.Log($"[Mod] 更新后的资源文件已存在,将会把它放入回收站:{Entry.Value}", ModBase.LogLevel.Debug); + } + + if (Directory.Exists(ModBase.GetPathFromFullPath(Entry.Value))) + { + File.Move(Entry.Key, Entry.Value); + FinishedFileNames.Add(ModBase.GetFileNameFromPath(Entry.Value)); + } + else + { + ModBase.Log($"[Mod] 更新后的目标文件夹已被删除:{Entry.Value}", ModBase.LogLevel.Debug); + } + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "替换旧版资源文件时被主动取消"); + } + })); + // 结束处理 + var Loader = + new ModLoader.LoaderCombo>( + "资源更新:" + PageInstanceLeft.Instance.Name, InstallLoaders); + var PathMods = PageInstanceLeft.Instance.PathIndie + + (PageInstanceLeft.Instance.Info.HasLabyMod + ? @"labymod-neo\fabric\" + PageInstanceLeft.Instance.Info.VanillaName + @"\" + : "") + ModLocalComp.GetPathNameByCompType(CurrentCompType) + @"\"; + Loader.OnStateChanged = _ => + { + // 结果提示 + switch (Loader.State) + { + case ModBase.LoadState.Finished: + { + switch (FinishedFileNames.Count) + { + case 0: // 一般是由于 Mod 文件被占用,然后玩家主动取消 + { + ModBase.Log("[CompUpdate] 没有资源被成功更新"); + break; + } + case 1: + { + ModMain.Hint($"已成功更新 {FinishedFileNames.Single()}!", ModMain.HintType.Finish); + break; + } + + default: + { + ModMain.Hint($"已成功更新 {FinishedFileNames.Count} 个资源!", ModMain.HintType.Finish); + break; + } + } + + break; + } + case ModBase.LoadState.Failed: + { + ModMain.Hint("资源更新失败:" + Loader.Error.Message, ModMain.HintType.Critical); + break; + } + case ModBase.LoadState.Aborted: + { + ModMain.Hint("资源更新已中止!"); + break; + } + + default: + { + return; + } + } + + ModBase.Log($"[CompUpdate] 已从正在进行资源更新的文件夹列表移除:{PathMods}"); + UpdatingVersions.Remove(PathMods); + // 清理缓存 + ModBase.RunInNewThread(() => + { + try + { + foreach (var TempFile in FileCopyList.Keys) + if (File.Exists(TempFile)) + File.Delete(TempFile); + } + catch (Exception ex) + { + ModBase.Log(ex, "清理资源更新缓存失败"); + } + }, "Clean Comp Update Cache", ThreadPriority.BelowNormal); + }; + // 启动加载器 + ModBase.Log($"[CompUpdate] 开始更新 {ModList.Count()} 个资源:{PathMods}"); + UpdatingVersions.Add(PathMods); + Loader.Start(); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + ReloadCompFileList(true); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化资源更新失败"); + } + } + + // 删除 + private void BtnSelectDelete_Click(object sender, ModBase.RouteEventArgs e) + { + DeleteMods(ModLocalComp.CompResourceListLoader.Output.Where(m => SelectedMods.Contains(m.RawPath))); + ChangeAllSelected(false); + } + + private void DeleteMods(IEnumerable ModList) + { + try + { + var IsSuccessful = true; + var IsShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + // 确认需要删除的文件 + // 文件夹只需要删除自身 + ModList = ModList.SelectMany(Target => + { + if (Target.IsFolder) return new[] { Target.Path }; + + if (Target.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + return new[] + { Target.Path, Target.Path + (File.Exists(Target.Path + ".old") ? ".old" : ".disabled") }; + + return new[] { Target.Path, Target.RawPath }; + }).Distinct() + .Where(m => m.EndsWithF(@"\__FOLDER__", true) + ? Directory.Exists(m.Replace(@"\__FOLDER__", "")) + : File.Exists(m)).Select(m => new ModLocalComp.LocalCompFile(m)).ToList(); + // 实际删除文件 + foreach (var ModEntity in ModList) + { + // 删除 + try + { + if (ModEntity.IsFolder) + { + // 删除文件夹 + if (IsShiftPressed) + Directory.Delete(ModEntity.ActualPath, true); + else + Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(ModEntity.ActualPath, + UIOption.AllDialogs, RecycleOption.SendToRecycleBin); + } + // 删除文件 + else if (IsShiftPressed) + { + File.Delete(ModEntity.Path); + } + else + { + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(ModEntity.Path, UIOption.OnlyErrorDialogs, + RecycleOption.SendToRecycleBin); + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除资源被主动取消"); + ReloadCompFileList(true); + return; + } + catch (Exception ex) + { + ModBase.Log(ex, $"删除资源失败({ModEntity.Path})", ModBase.LogLevel.Msgbox); + IsSuccessful = false; + } + + // 取消选中 + SelectedMods.Remove(ModEntity.RawPath); + // 更改 Loader 和 UI 中的列表 + ModLocalComp.CompResourceListLoader.Output.Remove(ModEntity); + SearchResult?.Remove(ModEntity); + ModItems.Remove(ModEntity.RawPath); + var IndexOfUi = PanList.Children.IndexOf(PanList.Children.OfType() + .FirstOrDefault(i => i.Entry.Equals(ModEntity))); + if (IndexOfUi >= 0) + PanList.Children.RemoveAt(IndexOfUi); + } + + RefreshBars(); + if (!IsSuccessful) + { + ModMain.Hint("由于文件被占用,删除失败,请尝试关闭正在运行的游戏后再试!", ModMain.HintType.Critical); + ReloadCompFileList(true); + } + else if (PanList.Children.Count == 0) + { + ReloadCompFileList(true); // 删除了全部项目 + } + else + { + RefreshBars(); + } + + // 显示结果提示 + if (!IsSuccessful) + return; + if (IsShiftPressed) + { + if (ModList.Count() == 1) + ModMain.Hint($"已彻底删除 {ModList.Single().FileName}!", ModMain.HintType.Finish); + else + ModMain.Hint($"已彻底删除 {ModList.Count()} 个项目!", ModMain.HintType.Finish); + } + else if (ModList.Count() == 1) + { + ModMain.Hint($"已将 {ModList.Single().FileName} 删除到回收站!", ModMain.HintType.Finish); + } + else + { + ModMain.Hint($"已将 {ModList.Count()} 个项目删除到回收站!", ModMain.HintType.Finish); + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除资源被主动取消"); + ReloadCompFileList(true); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除资源出现未知错误", ModBase.LogLevel.Feedback); + ReloadCompFileList(true); + } + + LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly); + } + + // 取消选择 + private void BtnSelectCancel_Click(object sender, ModBase.RouteEventArgs e) + { + ChangeAllSelected(false); + } + + // 收藏 + private void BtnSelectFavorites_Click(object sender, ModBase.RouteEventArgs e) + { + var Selected = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedMods.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp).ToList(); + ModComp.CompFavorites.ShowMenu(Selected, (UIElement)sender); + } + + // 分享 + private void BtnSelectShare_Click(object sender, ModBase.RouteEventArgs e) + { + var ShareList = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedMods.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp.Id).ToHashSet(); + ModBase.ClipboardSet(ModComp.CompFavorites.GetShareCode(ShareList)); + ChangeAllSelected(false); + } + + #endregion + + #region 单个资源项 + + // 详情 + public void Info_Click(object sender, EventArgs e) + { + try + { + var ModEntry = ((MyLocalCompItem)(sender is MyIconButton ? ((dynamic)sender).Tag : sender)).Entry; + // 判断该 LabyMod 是否支持安装 Fabric Mod + var ModdedLabyMod = PageInstanceLeft.Instance.Info.HasLabyMod && PageInstanceLeft.Instance.Modable; + // 加载失败信息 + if (ModEntry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) + { + ModMain.MyMsgBox( + "无法读取此资源的信息。" + "\r\n" + "\r\n" + "详细的错误信息:" + + ModEntry.FileUnavailableReason.Message, "资源读取失败"); + return; + } + + if (ModEntry.Comp is not null) + { + // 跳转到 Mod 下载页面 + ModMain.FrmMain.PageChange(new FormMain.PageStackData + { + Page = FormMain.PageType.CompDetail, + Additional = new object[] + { + ModEntry.Comp, new List(), PageInstanceLeft.Instance.Info.VanillaName, + PageInstanceLeft.Instance.Info.HasForge ? ModComp.CompLoaderType.Forge : + PageInstanceLeft.Instance.Info.HasNeoForge ? ModComp.CompLoaderType.NeoForge : + PageInstanceLeft.Instance.Info.HasFabric || ModdedLabyMod ? ModComp.CompLoaderType.Fabric : + ModComp.CompLoaderType.Any, + CurrentCompType + } + }); + } + else + { + // 对于原理图文件,使用异步加载避免UI卡顿 + if (ModEntry.Path.EndsWithF(".litematic", true) || ModEntry.Path.EndsWithF(".schem", true) || + ModEntry.Path.EndsWithF(".schematic", true) || ModEntry.Path.EndsWithF(".nbt", true)) + { + ShowSchematicInfoAsync(ModEntry); + return; + } + + // 获取信息 + var ContentLines = new List(); + + // 检查是否为文件夹 + if (ModEntry.IsFolder) + { + // 处理文件夹详情 + var folderPath = ModEntry.ActualPath; + if (Directory.Exists(folderPath)) + { + var fileCount = 0; + try + { + // 根据当前资源类型计算文件数量 + switch (CurrentCompType) + { + case ModComp.CompType.Schematic: + { + fileCount = new DirectoryInfo(folderPath) + .EnumerateFiles("*", SearchOption.AllDirectories).Where(f => + ModLocalComp.LocalCompFile.IsCompFile(f.FullName, + ModComp.CompType.Schematic)).Count(); + break; + } + case ModComp.CompType.Mod: + { + fileCount = new DirectoryInfo(folderPath) + .EnumerateFiles("*.jar", SearchOption.AllDirectories).Count(); + break; + } + case ModComp.CompType.ResourcePack: + { + fileCount = new DirectoryInfo(folderPath) + .EnumerateFiles("*.zip", SearchOption.AllDirectories).Count(); + break; + } + case ModComp.CompType.Shader: + { + fileCount = new DirectoryInfo(folderPath) + .EnumerateFiles("*.zip", SearchOption.AllDirectories).Count(); + break; + } + + default: + { + fileCount = new DirectoryInfo(folderPath) + .EnumerateFiles("*", SearchOption.AllDirectories).Count(); + break; + } + } + } + catch (Exception ex) + { + fileCount = 0; + } + + if (fileCount == 0) + ContentLines.Add("空文件夹" + "\r\n"); + else if (fileCount == 1) + ContentLines.Add("包含 1 个文件" + "\r\n"); + else + ContentLines.Add($"包含 {fileCount} 个文件" + "\r\n"); + } + else + { + ContentLines.Add("文件夹不存在" + "\r\n"); + } + + ContentLines.Add("路径:" + folderPath); + } + else + { + // 处理普通文件详情 + if (ModEntry.Description is not null) + ContentLines.Add(ModEntry.Description + "\r\n"); + if (ModEntry.Authors is not null) + ContentLines.Add("作者:" + ModEntry.Authors); + ContentLines.Add("文件:" + ModEntry.FileName + "(" + + ModBase.GetString(GetModFileInfo(ModEntry.Path).Length) + ")"); + if (ModEntry.Version is not null) + ContentLines.Add("版本:" + ModEntry.Version); + + // 原理图文件的详情信息已通过异步方法处理 + } + + // 只有普通文件才显示调试信息 + if (!ModEntry.IsFolder) + { + var DebugInfo = new List(); + if (ModEntry.ModId is not null) DebugInfo.Add("Mod ID:" + ModEntry.ModId); + if (ModEntry.Dependencies.Any()) + { + DebugInfo.Add("依赖于:"); + foreach (var Dep in ModEntry.Dependencies) + DebugInfo.Add(" - " + Dep.Key + (Dep.Value is null ? "" : ",版本:" + Dep.Value)); + } + + if (DebugInfo.Any()) + { + ContentLines.Add(""); + ContentLines.AddRange(DebugInfo); + } + } + + // 显示详情信息 + if (ModEntry.IsFolder) + { + // 文件夹只显示基本信息,不提供搜索功能 + ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "返回"); + } + else + { + // 获取用于搜索的 Mod 名称 + var ModOriginalName = ModEntry.Name.Replace(" ", "+"); + var ModSearchName = ModOriginalName.Substring(0, 1); + for (int i = 1, loopTo = ModOriginalName.Count() - 1; i <= loopTo; i++) + { + var IsLastLower = ModOriginalName[i - 1].ToString().ToLower() + .Equals(ModOriginalName[i - 1].ToString()); + var IsCurrentLower = ModOriginalName[i].ToString().ToLower() + .Equals(ModOriginalName[i].ToString()); + if (IsLastLower && !IsCurrentLower) + // 上一个字母为小写,这一个字母为大写 + ModSearchName += "+"; + ModSearchName += Conversions.ToString(ModOriginalName[i]); + } + + ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine"); + // 显示 + if (CurrentCompType == ModComp.CompType.Schematic) + { + // 投影原理图文件不显示百科搜索选项 + if (ModEntry.Url is null) + ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "返回"); + else if (ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "打开官网", "返回") == + 1) ModBase.OpenWebsite(ModEntry.Url); + } + // 其他资源类型保留百科搜索功能 + else if (ModEntry.Url is null) + { + if (ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "百科搜索", "返回") == 1) + ModBase.OpenWebsite("https://www.mcmod.cn/s?key=" + ModSearchName + "&site=all&filter=0"); + } + else + { + switch (ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "打开官网", "百科搜索", + "返回")) + { + case 1: + { + ModBase.OpenWebsite(ModEntry.Url); + break; + } + case 2: + { + ModBase.OpenWebsite( + "https://www.mcmod.cn/s?key=" + ModSearchName + "&site=all&filter=0"); + break; + } + } + } + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取资源详情失败", ModBase.LogLevel.Feedback); + } + } + + // 打开文件所在的位置 + public void Open_Click(MyIconButton sender, EventArgs e) + { + try + { + var ListItem = (MyLocalCompItem)sender.Tag; + // 对于文件夹使用实际路径,对于文件使用原路径 + var targetPath = ListItem.Entry.IsFolder ? ListItem.Entry.ActualPath : ListItem.Entry.Path; + ModBase.OpenExplorer(targetPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开资源文件位置失败", ModBase.LogLevel.Feedback); + } + } + + // 删除 + public void Delete_Click(MyIconButton sender, EventArgs e) + { + var ListItem = (MyLocalCompItem)sender.Tag; + DeleteMods(new[] { ListItem.Entry }); + } + + // 启用 / 禁用 + public void ED_Click(MyIconButton sender, EventArgs e) + { + var ListItem = (MyLocalCompItem)sender.Tag; + EDMods(new[] { ListItem.Entry }, ListItem.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled); + } + + /// + /// 异步显示原理图详情信息,避免UI卡顿 + /// + private void ShowSchematicInfoAsync(ModLocalComp.LocalCompFile ModEntry) + { + // 显示加载提示 + ModMain.Hint("正在加载详情...."); + + // 在后台线程中加载NBT数据 + // 确保 NBT 数据已加载 + + // 在 UI 线程中显示详情 + // 构建详情信息 + + + // 根据文件类型显示详细信息 + + // 显示调试信息 + + // 显示详情对话框 + + + // 记录错误日志但不显示错误提示,因为通用的文件状态检查已经处理了 + ModBase.RunInNewThread(() => + { + try + { + ModEntry.LoadNbtDataIfNeeded(); + ModBase.RunInUi(() => + { + try + { + var ContentLines = new List(); + if (ModEntry.Description is not null) ContentLines.Add(ModEntry.Description + "\r\n"); + if (ModEntry.Authors is not null) ContentLines.Add("作者:" + ModEntry.Authors); + ContentLines.Add("文件:" + ModEntry.FileName + "(" + + ModBase.GetString(GetModFileInfo(ModEntry.Path).Length) + ")"); + if (ModEntry.Version is not null) ContentLines.Add("版本:" + ModEntry.Version); + if (ModEntry.Path.EndsWithF(".litematic", true)) + ShowLitematicDetails(ContentLines, ModEntry); + else if (ModEntry.Path.EndsWithF(".schem", true)) + ShowSchemDetails(ContentLines, ModEntry); + else if (ModEntry.Path.EndsWithF(".schematic", true)) + ShowSchematicDetails(ContentLines, ModEntry); + else if (ModEntry.Path.EndsWithF(".nbt", true)) ShowNbtDetails(ContentLines, ModEntry); + ShowDebugInfo(ContentLines, ModEntry); + ShowSchematicDialog(ContentLines, ModEntry); + } + catch (Exception ex) + { + ModBase.Log(ex, "显示原理图详情失败", ModBase.LogLevel.Feedback); + } + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "加载原理图 NBT 数据失败", ModBase.LogLevel.Feedback); + } + }); + } + + #region 原理图文件详细信息显示 + + /// + /// 显示 Litematic 文件的详细信息 + /// + private void ShowLitematicDetails(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + ContentLines.Add(""); + ContentLines.Add("详细信息:"); + + // 显示原始名称(从 NBT Metadata/Name 读取) + if (ModEntry.LitematicOriginalName is not null) ContentLines.Add("原始名称:" + ModEntry.LitematicOriginalName); + + // 显示版本信息 + if (ModEntry.LitematicVersion.HasValue) ContentLines.Add("原理图版本:" + ModEntry.LitematicVersion.Value); + + // 显示尺寸信息 + if (ModEntry.LitematicEnclosingSize is not null) ContentLines.Add("包围盒大小:" + ModEntry.LitematicEnclosingSize); + + // 显示方块和体积统计 + if (ModEntry.LitematicTotalBlocks.HasValue) + ContentLines.Add("总方块数:" + ModEntry.LitematicTotalBlocks.Value.ToString("N0")); + + if (ModEntry.LitematicTotalVolume.HasValue) + ContentLines.Add("总体积:" + ModEntry.LitematicTotalVolume.Value.ToString("N0")); + + // 显示区域数量 + if (ModEntry.LitematicRegionCount.HasValue) ContentLines.Add("区域数量:" + ModEntry.LitematicRegionCount.Value); + + // 显示时间信息 + if (ModEntry.LitematicTimeCreated.HasValue) + try + { + var createdTime = DateTimeOffset.FromUnixTimeMilliseconds(ModEntry.LitematicTimeCreated.Value) + .ToLocalTime().DateTime; + ContentLines.Add("创建时间:" + createdTime.ToString("yyyy-MM-dd HH:mm:ss")); + } + catch + { + ContentLines.Add("创建时间:" + ModEntry.LitematicTimeCreated.Value); + } + + if (ModEntry.LitematicTimeModified.HasValue) + try + { + var modifiedTime = DateTimeOffset.FromUnixTimeMilliseconds(ModEntry.LitematicTimeModified.Value) + .ToLocalTime().DateTime; + ContentLines.Add("修改时间:" + modifiedTime.ToString("yyyy-MM-dd HH:mm:ss")); + } + catch + { + ContentLines.Add("修改时间:" + ModEntry.LitematicTimeModified.Value); + } + } + + /// + /// 显示 Schem 文件的详细信息 + /// + private void ShowSchemDetails(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + ContentLines.Add(""); + ContentLines.Add("详细信息:"); + + // 显示原始名称(从 NBT Metadata/Name 读取) + if (ModEntry.SchemOriginalName is not null) ContentLines.Add("原始名称:" + ModEntry.SchemOriginalName); + + // 显示版本信息 + if (ModEntry.StructureGameVersion is not null) ContentLines.Add("游戏版本:" + ModEntry.StructureGameVersion); + + if (ModEntry.SpongeVersion.HasValue) ContentLines.Add("Sponge 版本:" + ModEntry.SpongeVersion.Value); + + if (ModEntry.StructureDataVersion.HasValue) ContentLines.Add("数据版本:" + ModEntry.StructureDataVersion.Value); + + // 显示尺寸信息 + if (ModEntry.LitematicEnclosingSize is not null) ContentLines.Add("包围盒尺寸:" + ModEntry.LitematicEnclosingSize); + + // 显示方块和体积统计 + if (ModEntry.LitematicTotalBlocks.HasValue) + ContentLines.Add("总方块数:" + ModEntry.LitematicTotalBlocks.Value.ToString("N0")); + + if (ModEntry.LitematicTotalVolume.HasValue) + ContentLines.Add("总体积:" + ModEntry.LitematicTotalVolume.Value.ToString("N0")); + + // 显示区域数量 + if (ModEntry.LitematicRegionCount.HasValue) ContentLines.Add("区域数量:" + ModEntry.LitematicRegionCount.Value); + + ContentLines.Add("文件类型:Sponge Schematic"); + } + + /// + /// 显示 Schematic 文件的详细信息 + /// + private void ShowSchematicDetails(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + ContentLines.Add(""); + ContentLines.Add("详细信息:"); + + // 显示尺寸信息 + if (ModEntry.LitematicEnclosingSize is not null) ContentLines.Add("大小:" + ModEntry.LitematicEnclosingSize); + + // 显示方块和体积统计 + if (ModEntry.LitematicTotalBlocks.HasValue) + ContentLines.Add("总方块数:" + ModEntry.LitematicTotalBlocks.Value.ToString("N0")); + + if (ModEntry.LitematicTotalVolume.HasValue) + ContentLines.Add("总体积:" + ModEntry.LitematicTotalVolume.Value.ToString("N0")); + + ContentLines.Add("文件类型:MCEdit/WorldEdit Schematic"); + } + + /// + /// 显示 NBT 结构文件的详细信息 + /// + private void ShowNbtDetails(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + ContentLines.Add(""); + ContentLines.Add("详细信息:"); + + // 显示作者信息 + if (ModEntry.StructureAuthor is not null) ContentLines.Add("作者:" + ModEntry.StructureAuthor); + + // 显示版本信息 + if (ModEntry.StructureGameVersion is not null) ContentLines.Add("游戏版本:" + ModEntry.StructureGameVersion); + + if (ModEntry.StructureDataVersion.HasValue) ContentLines.Add("数据版本:" + ModEntry.StructureDataVersion.Value); + + // 显示尺寸信息 + if (ModEntry.LitematicEnclosingSize is not null) ContentLines.Add("包围盒尺寸:" + ModEntry.LitematicEnclosingSize); + + // 显示方块和体积统计 + if (ModEntry.LitematicTotalBlocks.HasValue) + ContentLines.Add("总方块数:" + ModEntry.LitematicTotalBlocks.Value.ToString("N0")); + + if (ModEntry.LitematicTotalVolume.HasValue) + ContentLines.Add("总体积:" + ModEntry.LitematicTotalVolume.Value.ToString("N0")); + + // 显示区域数量 + if (ModEntry.LitematicRegionCount.HasValue) ContentLines.Add("区域数量:" + ModEntry.LitematicRegionCount.Value); + + ContentLines.Add("文件类型:原版结构"); + } + + #endregion + + private void ShowDebugInfo(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + var DebugInfo = new List(); + if (ModEntry.ModId is not null) DebugInfo.Add("Mod ID:" + ModEntry.ModId); + if (ModEntry.Dependencies.Any()) + { + DebugInfo.Add("依赖于:"); + foreach (var Dep in ModEntry.Dependencies) + DebugInfo.Add(" - " + Dep.Key + (Dep.Value is null ? "" : ",版本:" + Dep.Value)); + } + + if (DebugInfo.Any()) + { + ContentLines.Add(""); + ContentLines.AddRange(DebugInfo); + } + } + + private void ShowSchematicDialog(List ContentLines, ModLocalComp.LocalCompFile ModEntry) + { + // 投影原理图文件不显示百科搜索选项 + if (ModEntry.Url is null) + ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "返回"); + else if (ModMain.MyMsgBox(ContentLines.Join("\r\n"), ModEntry.Name, "打开官网", "返回") == 1) + ModBase.OpenWebsite(ModEntry.Url); + } + + #endregion + + #region 搜索 + + public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text); + private List SearchResult; + private CancellationTokenSource _cancelToken; + + public void SearchRun(object sender, EventArgs e) + { + var curToken = new CancellationTokenSource(); + var oldToken = Interlocked.Exchange(ref _cancelToken, curToken); + oldToken?.Cancel(); + oldToken?.Dispose(); + + // this exception is ignored + Dispatcher.BeginInvoke(new Func(async () => + { + try + { + await Task.Delay(350, curToken.Token); + if (curToken.IsCancellationRequested) return; + if (IsSearching) + { + var searchText = SearchBox.Text; + SearchResult = await Task.Run(() => GetSearchResult(searchText), curToken.Token); + } + + if (curToken.IsCancellationRequested) return; + RefreshUI(); + } + catch (TaskCanceledException ignore) + { + } + catch (Exception ex) + { + ModBase.Log(ex, "搜索过程中发生异常"); + } + })); + } + + private List GetSearchResult(string query) + { + // 构造请求 + var QueryList = new List>(); + foreach (var Entry in ModLocalComp.CompResourceListLoader.Output.AsReadOnly()) + { + var SearchSource = new List>(); + SearchSource.Add(new KeyValuePair(Entry.Name, 1d)); + SearchSource.Add(new KeyValuePair(Entry.FileName, 1d)); + if (Entry.Version is not null) SearchSource.Add(new KeyValuePair(Entry.Version, 0.2d)); + if (Entry.Description is not null && !string.IsNullOrEmpty(Entry.Description)) + SearchSource.Add(new KeyValuePair(Entry.Description, 0.4d)); + if (Entry.Comp is not null) + { + if ((Entry.Comp.RawName ?? "") != (Entry.Name ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.RawName, 1d)); + if ((Entry.Comp.TranslatedName ?? "") != (Entry.Comp.RawName ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.TranslatedName, 1d)); + if ((Entry.Comp.Description ?? "") != (Entry.Description ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.Description, 0.4d)); + SearchSource.Add(new KeyValuePair(string.Join("", Entry.Comp.Tags), 0.2d)); + } + + QueryList.Add(new ModBase.SearchEntry + { Item = Entry, SearchSource = SearchSource }); + } + + // 进行搜索 + return ModBase.Search(QueryList, query, 6, 0.35d).Select(r => r.Item).ToList(); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.vb b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.vb index 67523d4b9..5b81ecb37 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceCompResource.xaml.vb @@ -1051,7 +1051,7 @@ Install: Return CheckingMod.State = LocalCompFile.LocalFileStatus.Unavailable Case FilterType.Duplicate Dim ItemSource = If(IsSearching, SearchResult, If(CompResourceListLoader.Output, New List(Of LocalCompFile))) - Return ItemSource IsNot Nothing AndAlso ItemSource.Where(Function(m) CheckingMod.Comp IsNot Nothing AndAlso m.Comp IsNot Nothing AndAlso CheckingMod.Comp.Id = m.Comp.Id).Count > 1 + Return ItemSource IsNot Nothing AndAlso ItemSource.Where(Function(m) CheckingMod.Comp IsNot Nothing AndAlso m.Comp IsNot Nothing AndAlso CheckingMod.Comp.Id = m.Comp.Id).Any() Case Else Return False End Select diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml index 891ae0045..464d951c4 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml @@ -1,19 +1,21 @@  - + - + - + @@ -25,211 +27,214 @@ - + - - + + - + Text="重置" + Logo="M530 0c287 0 521 229 521 511s-233 511-521 511c-233 0-436-151-500-368a63 63 0 0 1 44-79 65 65 0 0 1 80 43c48 162 200 276 375 276 215 0 390-171 390-383s-174-383-390-383c-103 0-199 39-270 106l21-5a63 63 0 0 1 33 123l-157 42a65 65 0 0 1-90-42l-49-183a65 65 0 1 1 126-33l6 26A524 524 0 0 1 530 0z" + LogoScale="0.9" /> - + - + - + - + - + + Visibility="{Binding Checked, ElementName=CheckOptionsMod, Converter={StaticResource BooleanToVisibilityConverter}}"> - + - + - + - + - + - + - + - + - + Visibility="{Binding Checked, ElementName=CheckOptionsResourcePacks, Converter={StaticResource BooleanToVisibilityConverter}}" /> - + - + Visibility="{Binding Checked, ElementName=CheckOptionsShaderPacks, Converter={StaticResource BooleanToVisibilityConverter}}" /> - + - + - + - + - + Visibility="{Binding Checked, ElementName=CheckOptionsSaves, Converter={StaticResource BooleanToVisibilityConverter}}" /> - + - + + Visibility="{Binding Checked, ElementName=CheckOptionsPcl, Converter={StaticResource BooleanToVisibilityConverter}}"> - + @@ -237,11 +242,15 @@ - + @@ -251,9 +260,12 @@ - - - + + @@ -261,7 +273,8 @@ + x:Name="BtnExport" Text="开始导出" + LogoScale="1.1" + Logo="M511 995a128 128 0 0 1-57-13L70 791a126 126 0 0 1-70-113V311a126 126 0 0 1 15-60V248c1-2 3-5 5-8a127 127 0 0 1 49-42L454 13a128 128 0 0 1 112 0l383 190a126 126 0 0 1 72 113v360a126 126 0 0 1-70 115L568 984c-17 7-37 11-57 11z m42-470v370l360-178c14-7 23-21 23-38v-335L554 524zM85 330v347a42 42 0 0 0 23 38l360 178V523L85 330zM135 260l375 189 137-65L286 188 135 260z m245-118l363 197 150-71-365-180a42 42 0 0 0-37 0l-111 53z" /> \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.cs new file mode 100644 index 000000000..1bac34d38 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.cs @@ -0,0 +1,1087 @@ +using System.IO; +using System.IO.Compression; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.UI; + +namespace PCL; + +public class ExportOption +{ + public string Title { get; set; } + public string Description { get; set; } + public string Rules { get; set; } + + /// + /// 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。 + /// 如果 ShowRules 也为空,则始终显示。 + /// + public string ShowRules { get; set; } + + public bool DefaultChecked { get; set; } + public bool RequireModLoader { get; set; } = false; + public bool RequireOptiFine { get; set; } = false; + public bool RequireModLoaderOrOptiFine { get; set; } = false; +} + +public partial class PageInstanceExport : IRefreshable +{ + private string CurrentVersion = ""; + + public PageInstanceExport() + { + InitializeComponent(); + Loaded += (_, __) => PageInstanceExport_Loaded(); + CardOptions.MouseLeftButtonDown += CardOptions_MouseLeftButtonDown; + BtnAdvancedExport.Click += ExportConfig; + BtnAdvancedImport.Click += ImportConfig; + BtnExport.Click += StartExport; + TextExportName.GotFocus += TextExportName_GotFocus; + CheckAdvancedModrinth.Change += CheckAdvancedModrinth_Change; + CheckAdvancedInclude.Change += CheckAdvancedInclude_Change; + } + + void IRefreshable.Refresh() + { + RefreshAll(); + } + + private void PageInstanceExport_Loaded() + { + ModAnimation.AniControlEnabled += 1; + if ((CurrentVersion ?? "") != (PageInstanceLeft.Instance.PathInstance ?? "")) + RefreshAll(); // 切换到了另一个实例,重置页面 + BtnAdvancedHelp.EventData = "指南/整合包制作.json"; + ModAnimation.AniControlEnabled -= 1; + } + + public void RefreshAll() + { + ModBase.Log("[Export] 刷新导出页面"); + HintOptiFine.Visibility = + PageInstanceLeft.Instance.Info.HasOptiFine ? Visibility.Visible : Visibility.Collapsed; + CurrentVersion = PageInstanceLeft.Instance.PathInstance; + TextExportName.Text = ""; + TextExportName.HintText = PageInstanceLeft.Instance.Name; + TextExportVersion.Text = ""; + TextExportVersion.HintText = "1.0.0"; + CheckAdvancedInclude.Checked = false; + CheckAdvancedModrinth.Checked = false; + GetExportOption(CheckOptionsBasic).Description = PageInstanceLeft.Instance.GetDefaultDescription(); + ResetConfigOverrides(); + ReloadAllSubOptions(); + RefreshAllOptionsUI(); + PanBack.ScrollToHome(); + } + + // 自动填写整合包名称 + private void TextExportName_GotFocus(object sender, RoutedEventArgs routedEventArgs) + { + if (string.IsNullOrEmpty(TextExportName.Text)) + { + TextExportName.Text = TextExportName.HintText; + TextExportName.SelectionStart = TextExportName.Text.Length; + } + } + + // 勾选 Modrinth 上传模式时,禁止打包 PCL + private void CheckAdvancedModrinth_Change(object sender, bool user) + { + if (CheckAdvancedModrinth.Checked == true) + CheckOptionsPcl.Checked = false; + CheckOptionsPcl.IsEnabled = (bool)!CheckAdvancedModrinth.Checked; + } + + // 勾选打包资源文件时,禁止开启 Modrinth 上传模式 + private void CheckAdvancedInclude_Change(object sender, bool user) + { + if (CheckAdvancedInclude.Checked == true) + CheckAdvancedModrinth.Checked = false; + CheckAdvancedModrinth.IsEnabled = (bool)!CheckAdvancedInclude.Checked; + } + + #region 子选项 + + private readonly string[] SubOptionBlackList = new[] { "Quark Programmer Art.zip", "+ EuphoriaPatches_" }; + + /// + /// 动态生成子文件夹下的选项,例如资源包、存档等。 + /// + private void ReloadAllSubOptions() + { + ReloadSubOptions(PanOptionsResourcePacks, true, true, "resourcepacks", "texturepacks"); + ReloadSubOptions(PanOptionsSaves, false, true, "saves"); + ReloadSubOptions(PanOptionsShaderPacks, true, true, "shaderpacks"); + } + + private void ReloadSubOptions(StackPanel Panel, bool AcceptCompressedFile, bool AcceptFolder, + params string[] Folders) + { + Panel.Children.Clear(); + foreach (var Folder in Folders) + { + var TargetFolder = new DirectoryInfo(PageInstanceLeft.Instance.PathIndie + Folder); + if (!TargetFolder.Exists) + continue; + // 查找文件夹下的对应项 + if (AcceptCompressedFile) + foreach (var File in TargetFolder.EnumerateFiles("*.zip").Concat(TargetFolder.EnumerateFiles("*.rar"))) + { + if (SubOptionBlackList.Any(b => File.Name.ContainsF(b))) + continue; + Panel.Children.Add(new MyCheckBox + { + Tag = new ExportOption + { + Title = File.Name, DefaultChecked = true, + Rules = ModBase.EscapeLikePattern($"{Folder}/{File.Name}") + } + }); + if (Folder == "shaderpacks") // 处理光影包的配置文件 + { + var shaderConfig = new FileInfo(Path.Combine(File.Directory.FullName, + $"{Path.GetFileNameWithoutExtension(File.Name)}.txt")); + if (shaderConfig.Exists) + Panel.Children.Add(new MyCheckBox + { + Tag = new ExportOption + { + Title = $"{shaderConfig.Name} (光影配置文件)", DefaultChecked = true, + Rules = ModBase.EscapeLikePattern($"{Folder}/{shaderConfig.Name}") + } + }); + } + } + + if (AcceptFolder) + foreach (var SubFolder in TargetFolder.EnumerateDirectories().OrderByDescending(f => f.LastWriteTime)) + { + if (SubOptionBlackList.Any(b => SubFolder.Name.ContainsF(b))) + continue; + if (!SubFolder.EnumerateFileSystemInfos().Any()) + continue; + var NewCheckBox = new MyCheckBox + { + Tag = new ExportOption + { + Title = SubFolder.Name, DefaultChecked = true, + Rules = ModBase.EscapeLikePattern($"{Folder}/{SubFolder.Name}/") + } + }; + if (ReferenceEquals(Panel, PanOptionsSaves)) + GetExportOption(NewCheckBox).Description = + SubFolder.LastWriteTime.ToString("yyyy'/'MM'/'dd HH':'mm"); + Panel.Children.Add(NewCheckBox); + } + } + } + + #endregion + + #region 选项 + + /// + /// 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。 + /// + private void RefreshAllOptionsUI() + { + // 预先归纳所有至多二级的文件/文件夹 + var AllEntries = new List(); + + bool IsValidDirectory(DirectoryInfo Folder) + { + try + { + return Folder.Exists && Folder.EnumerateFileSystemInfos() + .Any(i => !SubOptionBlackList.Any(b => i.Name.ContainsF(b))); + } + catch + { + return false; + } + } + + ; // 检查文件夹不为空 + // 一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接) + var PathInfo = new DirectoryInfo(PageInstanceLeft.Instance.PathIndie); + AllEntries.AddRange(PathInfo.EnumerateFiles().Select(f => f.Name)); + foreach (var SubFolder in PathInfo.EnumerateDirectories().Where(IsValidDirectory)) + { + AllEntries.Add($@"{SubFolder.Name}\"); + AllEntries.AddRange(SubFolder.EnumerateFiles().Select(f => $@"{SubFolder.Name}\{f.Name}")); + AllEntries.AddRange(SubFolder.EnumerateDirectories().Where(IsValidDirectory) + .Select(d => $@"{SubFolder.Name}\{d.Name}\")); + } + + ModBase.Log($"[Export] 共发现 {AllEntries.Count} 个可行的二级文件/文件夹"); + + // 确认选项是否应该被显示 + bool IsVisible(ExportOption TargetOption) + { + // 检查需要 OptiFine 或 Mod 加载器 + if (TargetOption.RequireOptiFine && !PageInstanceLeft.Instance.Info.HasOptiFine) + return false; + if (TargetOption.RequireModLoader && !PageInstanceLeft.Instance.Modable) + return false; + if (TargetOption.RequireModLoaderOrOptiFine && !PageInstanceLeft.Instance.Info.HasOptiFine && + !PageInstanceLeft.Instance.Modable) + return false; + // 粗略检查是否可能有符合规则的文件/文件夹 + return StandardizeLines((TargetOption.Rules ?? TargetOption.ShowRules).Split('|'), true).Any(Rule => + { + if (Rule.StartsWithF("!")) + return false; // 只看正向规则 + // 检查前两级 + try + { + if (AllEntries.Any(Entry => LikeOperator.LikeString(Entry, Rule, CompareMethod.Binary))) + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, $"错误的规则:{Rule}", ModBase.LogLevel.Hint); + return false; + } + + // 粗略检查所有级 + Rule = Rule.Trim("*?".ToCharArray()); + if (Rule.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries).Count() >= 3) + { + if (Rule.EndsWithF(@"\")) + return IsValidDirectory(new DirectoryInfo(PageInstanceLeft.Instance.PathIndie + Rule)); // 文件夹有效 + + return File.Exists(PageInstanceLeft.Instance.PathIndie + Rule); + // 文件有效 + } + + return false; + }); + } + + ; + // 逐个检查选项 + foreach (var CheckBox in GetAllOptions(true)) + { + var TargetOption = GetExportOption(CheckBox); + // 名称与简介 + CheckBox.Inlines.Clear(); + CheckBox.Inlines.Add(new Run(TargetOption.Title)); + if (!string.IsNullOrEmpty(TargetOption.Description)) + CheckBox.Inlines.Add(new Run(" " + TargetOption.Description) { Foreground = ModSecret.ColorGray5 }); + // 可见性、默认勾选 + if (string.IsNullOrEmpty(TargetOption.Rules) && string.IsNullOrEmpty(TargetOption.ShowRules)) + { + CheckBox.Visibility = Visibility.Visible; + CheckBox.Checked = TargetOption.DefaultChecked; + } + else + { + var Pass = IsVisible(TargetOption); + CheckBox.Visibility = Pass ? Visibility.Visible : Visibility.Collapsed; + CheckBox.Checked = TargetOption.DefaultChecked && Pass; + } + } + } + + /// + /// 对文本行进行标准化处理,以便使用 Like 进行匹配。 + /// + private IEnumerable StandardizeLines(IEnumerable Raw, bool AddSuffixStarToFolderPath) + { + foreach (var IgnoreLineRaw in Raw) + { + var IgnoreLine = IgnoreLineRaw; + IgnoreLine = IgnoreLine.Trim(); + if (string.IsNullOrEmpty(IgnoreLine) || IgnoreLine.StartsWithF("#") || IgnoreLine.StartsWithF("=")) + continue; + IgnoreLine = IgnoreLine.Replace("/", @"\"); + yield return IgnoreLine + (IgnoreLine.EndsWithF(@"\") && AddSuffixStarToFolderPath ? "*" : ""); + } + } + + /// + /// 获取所有可作为选项的 CheckBox。 + /// + private IEnumerable GetAllOptions(bool IncludeHidden) + { + foreach (var Element in PanOptions.Children) + { + if (!IncludeHidden && + Conversions.ToBoolean(Operators.ConditionalCompareObjectNotEqual(((dynamic)Element).Visibility, + Visibility.Visible, false))) + continue; + if (Element is MyCheckBox) + yield return (MyCheckBox)Element; + else if (Element is StackPanel) + foreach (var SubElement in ((StackPanel)Element).Children) + { + if (!IncludeHidden && Conversions.ToBoolean( + Operators.ConditionalCompareObjectNotEqual(((dynamic)SubElement).Visibility, + Visibility.Visible, false))) + continue; + if (SubElement is MyCheckBox) + yield return (MyCheckBox)SubElement; + } + } + } + + /// + /// 获取该 CheckBox 对应的 ExportOption。 + /// + private ExportOption GetExportOption(MyCheckBox CheckBox) + { + return (ExportOption)CheckBox.Tag; + } + + #endregion + + #region 配置文件 + + private const string Sperator = "=============================================================="; + + // ================ 导出内容段 ================ + + /// + /// 从配置文件中读取的规则。 + /// 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。 + /// + private List RulesOverrides + { + get => _RulesOverrides; + set + { + _RulesOverrides = value; + if (value is null) + { + BtnOverrideCancel.Visibility = Visibility.Collapsed; + PanOptions.Visibility = Visibility.Visible; + CardOptions.Inlines.Clear(); + CardOptions.Inlines.Add(new Run("导出内容列表") { FontWeight = FontWeights.Bold }); + } + else + { + BtnOverrideCancel.Visibility = Visibility.Visible; + PanOptions.Visibility = Visibility.Collapsed; + CardOptions.Inlines.Clear(); + CardOptions.Inlines.Add(new Run("导出内容列表:    ") { FontWeight = FontWeights.Bold }); + CardOptions.Inlines.Add(new Run("从配置文件中读取") { FontWeight = FontWeights.Normal }); + } + } + } + + private List _RulesOverrides; + + /// + /// 获取当前实际生效的所有规则。 + /// + private IEnumerable GetAllRules() + { + if (RulesOverrides is not null) + { + // 返回覆盖的列表 + foreach (var Rule in RulesOverrides) + yield return Rule; + } + else + { + // 从当前勾选的所有选项中获取所有规则行 + yield return ""; + yield return "# 修改下方的规则以控制需要导出的内容。"; + yield return "# 以 ! 开头以反选。可以使用 *、?、[] 通配符。靠后的行覆盖靠前的。"; + yield return ""; + foreach (var CheckBox in GetAllOptions(false)) + { + if (CheckBox.Checked == false) + continue; + var TargetOption = GetExportOption(CheckBox); + if (TargetOption.Rules is null) + continue; + yield return $"# {TargetOption.Title}"; + foreach (var Rule in TargetOption.Rules.Split('|')) + yield return Rule; + yield return ""; + } + + yield return "# 排除的文件"; + yield return "!*.log"; + yield return "!*.dat_old"; + yield return "!*.BakaCoreInfo"; + yield return "!hmclversion.cfg"; + yield return "!log4j2.xml"; + yield return ""; + } + } + + // ================ 追加内容段 ================ + + private List ExtraFiles; + + /// + /// 获取当前实际生效的追加内容。 + /// + private IEnumerable GetExtraFileLines() + { + if (ExtraFiles is not null) + { + // 返回覆盖的列表 + foreach (var File in ExtraFiles) + yield return File; + } + else + { + // 从当前勾选的所有选项中获取所有规则行 + yield return ""; + yield return "# 如果想将额外的文件自动放到压缩包根目录中,可以将它们的路径写在下方。"; + yield return @"# 必须是完整路径。每行中,若以 \ 结尾则代表是文件夹,不以 \ 结尾则代表是文件。"; + yield return ""; + } + } + + // ================ 重置 ================ + + /// + /// 重置配置文件所带来的影响。 + /// + private void ResetConfigOverrides() + { + RulesOverrides = null; + ConfigPackPath = null; + ExtraFiles = null; + PanBack.ScrollToHome(); + } + + private void CardOptions_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (RulesOverrides is null) + return; + ResetConfigOverrides(); + } + + // ================ 保存 / 读取 ================ + + // 保存配置文件 + private void ExportConfig(object sender, MouseButtonEventArgs e) + { + try + { + var ConfigPath = SystemDialogs.SelectSaveFile("选择文件位置", "export_config.txt", "整合包导出配置(*.txt)|*.txt", + (string?)States.System.ExportConfigPath); + if (string.IsNullOrEmpty(ConfigPath)) + return; + States.System.ExportConfigPath = ConfigPath; + var ConfigLines = new List(); + // ini 段 + ConfigLines.Add("Name:" + TextExportName.Text); + ConfigLines.Add("Version:" + TextExportVersion.Text); + ConfigLines.Add(""); + ConfigLines.Add("# 是否打包正式版 PCL,以便没有启动器的玩家安装整合包。"); + ConfigLines.Add("IncludeLauncher:" + CheckOptionsPcl.Checked); + ConfigLines.Add(""); + ConfigLines.Add("# 是否打包 PCL 个性化内容,例如功能隐藏设置、主页、背景音乐和图片等。"); + ConfigLines.Add("IncludeLauncherCustom:" + CheckOptionsPclCustom.Checked); + ConfigLines.Add(""); + ConfigLines.Add("# 是否将 Mod、资源包、光影包的文件直接放入整合包中,这样在导入时就无需联网下载它们。"); + ConfigLines.Add("# 建议仅在无法稳定连接 CurseForge 或 Modrinth 时才考虑启用。"); + ConfigLines.Add("# 二次分发可能违反使用协议,请尽量不要公开发布包含资源文件的整合包!"); + ConfigLines.Add("DontCheckHostedAssets:" + CheckAdvancedInclude.Checked); + ConfigLines.Add(""); + ConfigLines.Add("# 如果你想要打包上传到 Modrinth,启用此项会生成完全符合 Modrinth 要求的整合包文件。"); + ConfigLines.Add("# 由于 Modrinth 要求,只能从 CurseForge 下载的资源将无法联网下载,会被直接放入整合包中。"); + ConfigLines.Add("# 此选项与 IncludeLauncher、IncludeLauncherCustom、DontCheckHostedAssets 冲突。"); + ConfigLines.Add("ModrinthUploadMode:" + CheckAdvancedModrinth.Checked); + ConfigLines.Add(""); + ConfigLines.Add("# 导出的文件的存放位置。"); + ConfigLines.Add("# 若设置了此项,在导出时会直接将文件放到此路径,不会弹窗要求选择。"); + ConfigLines.Add("# 若 IncludeLauncher 为 True,应以 .zip 结尾;若为 False,应以 .mrpack 结尾。"); + ConfigLines.Add("PackPath:" + (ConfigPackPath ?? "")); + ConfigLines.Add(""); + // 导出内容段 + ConfigLines.Add(Sperator); + ConfigLines.AddRange(GetAllRules()); + // 追加内容段 + ConfigLines.Add(Sperator); + ConfigLines.AddRange(GetExtraFileLines()); + // 结束 + ModBase.WriteFile(ConfigPath, ConfigLines.Join("\r\n")); + ModMain.Hint("已保存配置文件:" + ConfigPath, ModMain.HintType.Finish); + ModBase.OpenExplorer(ConfigPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "保存配置失败", ModBase.LogLevel.Msgbox); + } + } + + #region 配置文件核心读取逻辑 + + /// + /// 从指定路径读取配置文件(供按钮和拖放调用) + /// + /// 配置文件路径 + private void ReadConfigFile(string configPath) + { + try + { + // 保存配置文件路径到缓存 + States.System.ExportConfigPath = configPath; + + var fileContent = ModBase.ReadFile(configPath); + var Segments = fileContent.Split(Sperator); + + if (Segments.Length == 0) + { + ModMain.Hint("配置文件内容无效或为空!", ModMain.HintType.Critical); + return; + } + + // === 解析INI段 === + var Ini = new Dictionary(); + foreach (var LineRaw in Segments[0].Split("\r\n".ToCharArray())) + { + var Line = LineRaw; + Line = Line.Trim(); + if (string.IsNullOrEmpty(Line) || Line.StartsWithF("#") || Line.StartsWithF("=")) + continue; + var Index = Line.IndexOfF(":"); + if (Index > 0) Ini[Line.Substring(0, Index)] = Line.Substring(Index + 1); + } + + // 赋值到界面控件 + TextExportName.Text = Ini.GetOrDefault("Name", ""); + TextExportVersion.Text = Ini.GetOrDefault("Version", ""); + CheckOptionsPcl.Checked = + Convert.ToBoolean(Ini.GetOrDefault("IncludeLauncher", Conversions.ToString(true))); + CheckOptionsPclCustom.Checked = + Convert.ToBoolean(Ini.GetOrDefault("IncludeLauncherCustom", Conversions.ToString(true))); + CheckAdvancedModrinth.Checked = + Convert.ToBoolean(Ini.GetOrDefault("ModrinthUploadMode", Conversions.ToString(false))); + CheckAdvancedInclude.Checked = + Convert.ToBoolean(Ini.GetOrDefault("DontCheckHostedAssets", Conversions.ToString(false))); + ConfigPackPath = Ini.GetOrDefault("PackPath"); + + // === 解析导出内容段 === + RulesOverrides = Segments[1].Replace("\r", "\n") + .Replace("\n" + "\n", "\n").Split("\n").ToList(); + + // === 解析追加内容段 === + if (Segments.Length > 2) + ExtraFiles = Segments[2].Replace("\r", "\n") + .Replace("\n" + "\n", "\n").Split("\n").ToList(); + else + ExtraFiles = null; + + // 提示成功 + ModMain.Hint("已读取配置文件:" + configPath, ModMain.HintType.Finish); + } + + catch (Exception ex) + { + ModBase.Log(ex, $"读取配置文件失败:{configPath}", ModBase.LogLevel.Msgbox); + } + } + + #endregion + + // 读取配置文件 + private void ImportConfig(object sender, MouseButtonEventArgs e) + { + try + { + var ConfigPath = SystemDialogs.SelectFile("整合包导出配置(*.txt)|*.txt", "选择配置文件", + (string?)States.System.ExportConfigPath); + if (string.IsNullOrEmpty(ConfigPath)) + return; + + // 调用核心读取逻辑 + ReadConfigFile(ConfigPath); + } + + catch (Exception ex) + { + ModBase.Log(ex, "选择配置文件失败", ModBase.LogLevel.Msgbox); + } + } + + #region 拖放事件处理 + + /// + /// 文件拖入界面时触发:验证文件类型 + /// + private void PanAllBack_DragEnter(object sender, DragEventArgs e) + { + // 检查是否包含文件拖放数据 + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + // 获取拖入的文件路径数组 + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + + // 验证:仅允许单个.txt文件 + if (files.Length == 1 && + files[0].EndsWithF(".txt", Conversions.ToBoolean(StringComparison.OrdinalIgnoreCase))) + e.Effects = DragDropEffects.Copy; // 设置拖放效果为“复制” + else + e.Effects = DragDropEffects.None; // 不允许拖放 + } + else + { + e.Effects = DragDropEffects.None; + } + + e.Handled = true; + } + + /// + /// 文件放下时触发:读取配置文件 + /// + private void PanAllBack_Drop(object sender, DragEventArgs e) + { + // 获取拖入的文件路径 + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + var files = (string[])e.Data.GetData(DataFormats.FileDrop); + var configPath = files[0]; + + // 调用核心读取逻辑 + ReadConfigFile(configPath); + } + + e.Handled = true; + } + + #endregion + + #endregion + + #region 导出 + + /// + /// 配置文件中指定的导出位置。 + /// + private string ConfigPackPath; + + /// + /// 开始导出。 + /// + private void StartExport(object sender, MouseButtonEventArgs e) + { + var PackName = string.IsNullOrEmpty(TextExportName.Text) ? TextExportName.HintText : TextExportName.Text; + var PackVersion = string.IsNullOrEmpty(TextExportVersion.Text) ? "1.0.0" : TextExportVersion.Text; + + // 重复任务检查 + var LoaderName = "导出整合包:" + PackName; + foreach (var OngoingLoader in ModLoader.LoaderTaskbar) + { + if ((OngoingLoader.Name ?? "") != (LoaderName ?? "")) + continue; + ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager); + return; + } + + // 确认导出位置 + string PackPath = null; + if (!string.IsNullOrWhiteSpace(ConfigPackPath) && !ConfigPackPath.EndsWithF(@"\") && + !ConfigPackPath.EndsWithF("/")) + try + { + Directory.CreateDirectory(ModBase.GetPathFromFullPath(ConfigPackPath)); + PackPath = ConfigPackPath; + ModBase.Log($"[Export] 使用配置文件中指定的导出路径:{ConfigPackPath}"); + } + catch (Exception ex) + { + ModBase.Log(ex, $"无法使用配置文件中指定的导出路径({ConfigPackPath})"); + if (ModMain.MyMsgBox($"指定的路径:{ConfigPackPath}{"\r\n"}{"\r\n"}{ex}", + "无法使用配置文件中指定的导出路径", "确定", "取消") == 2) + return; + } + + if (PackPath is null) + { + var Extensions = new List(); + if (CheckAdvancedModrinth.Checked == false) + Extensions.Add("压缩文件(*.zip)|*.zip"); + if (CheckOptionsPcl.Checked == false) + Extensions.Add("Modrinth 整合包文件(*.mrpack)|*.mrpack"); + PackPath = SystemDialogs.SelectSaveFile("选择导出位置", + PackName + (string.IsNullOrEmpty(TextExportVersion.Text) ? "" : " " + TextExportVersion.Text), + Extensions.Join("|")); + ModBase.Log($"[Export] 手动指定的导出路径:{PackPath}"); + } + + if (string.IsNullOrEmpty(PackPath)) + return; + + // 缓存所需参数 + var CacheFolder = ModMain.RequestTaskTempFolder(); + var OverridesFolder = CacheFolder + @"modpack\overrides\"; + var McInstance = PageInstanceLeft.Instance; + var PathIndie = McInstance.PathIndie; + var CheckHostedAssets = (bool)!CheckAdvancedInclude.Checked; + var ModrinthUploadMode = (bool)CheckAdvancedModrinth.Checked; + var IncludePCL = (bool)CheckOptionsPcl.Checked; + var IncludePCLCustom = (bool)(IncludePCL ? CheckOptionsPclCustom.Checked : (bool?)false); + var AllRules = StandardizeLines(GetAllRules(), true).ToList(); + var AllExtraFiles = StandardizeLines(GetExtraFileLines(), false).ToList(); + ModBase.Log($"[Export] 准备导出整合包,共有 {AllRules.Count} 条规则,{AllExtraFiles.Count} 条追加内容行"); + + // 构造步骤加载器 + var Loaders = new List(); + + #region 准备 PCL 文件 + + /* TODO ERROR: Skipped IfDirectiveTrivia + #If Not RELEASE Then + */ + if (IncludePCL) + Loaders.Add(new ModLoader.LoaderTask("下载 PCL 正式版", Loader => + { + ModSecret.DownloadLatestPCL(Loader); + ModBase.CopyFile(ModBase.PathTemp + "CE-Latest.exe", CacheFolder + "Plain Craft Launcher.exe"); + }) + { + ProgressWeight = 0.5d, + Block = false + }); + /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + + #endregion + + #region 复制文件 + + Loaders.Add(new ModLoader.LoaderTask>("复制导出内容", Loader => + { + Loader.Output = new List(); + // 复制实例文件 + var Progress = 0; + Action SearchFolder = null; + SearchFolder = Folder => + { + // 文件夹:进一步搜索 + foreach (var SubFolder in Folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + // 跳过部分又没用文件又多的文件夹,加快搜索 + if ((Folder.FullName ?? "") == (PathIndie ?? "") && + new[] { "assets", "versions", "libraries" }.Contains(SubFolder.Name)) + continue; + if (new[] { "structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache" }.Contains( + SubFolder.Name)) + continue; + SearchFolder(SubFolder); + } + + // 文件:检查规则并复制 + foreach (var Entry in Folder.EnumerateFiles("*", SearchOption.TopDirectoryOnly)) + { + var RelativePath = Entry.FullName.AfterFirst(PathIndie); + // 检查规则 + var ShouldKeep = false; + foreach (var Rule in AllRules) + { + var Revert = Rule.StartsWith("!"); + if (LikeOperator.LikeString(RelativePath, Rule.TrimStart('!'), CompareMethod.Binary)) + ShouldKeep = !Revert; + } + + if (!ShouldKeep) + continue; + var TargetPath = OverridesFolder + RelativePath; + ModBase.CopyFile(Entry.FullName, TargetPath); + // 若为压缩包,考虑联网获取路径 + if (CheckHostedAssets && + new[] { ".zip", ".rar", ".jar", ".disabled", ".old" }.Contains(Entry.Extension.ToLower()) && + new[] { "mods", "packs", "openloader", "resource" }.Any(s => RelativePath.Contains(s))) + { + var ModFile = new ModLocalComp.LocalCompFile(TargetPath); + var Unused = ModFile.ModrinthHash; // 提前计算 Hash + Unused = ModFile.CurseForgeHash.ToString(); + Loader.Output.Add(ModFile); + } + + // 更新进度(进度并不准确,主要突出一个我还没似) + Progress += 1; + if (Progress == 25) + { + Loader.Progress += (0.94d - Loader.Progress) * 0.012d; + Progress = 0; + } + } + }; + SearchFolder(new DirectoryInfo(PathIndie)); + ModBase.Log($"[Export] 复制 overrides 文件完成,有 {Loader.Output.Count} 个文件需要联网检查"); + Loader.Progress = 0.95d; + // 复制追加内容到根目录 + var BaseFolder = IncludePCL ? CacheFolder : CacheFolder + @"modpack\"; + foreach (var Line in AllExtraFiles) + if (Line.EndsWithF(@"\") || Line.EndsWithF("/")) + { + if (Directory.Exists(Line)) + ModBase.CopyDirectory(Line, BaseFolder + ModBase.GetFolderNameFromPath(Line) + @"\"); + else + ModMain.Hint($"未找到配置文件中指定的文件夹:{Line}", ModMain.HintType.Critical); + } + else if (File.Exists(Line)) + { + ModBase.CopyFile(Line, BaseFolder + ModBase.GetFileNameFromPath(Line)); + } + else + { + ModMain.Hint($"未找到配置文件中指定的单个文件:{Line}", ModMain.HintType.Critical); + } + + Loader.Progress = 0.97d; + // 复制 PCL 实例设置 + ModBase.CopyDirectory(McInstance.PathInstance + @"PCL\", OverridesFolder + @"PCL\"); + /* TODO ERROR: Skipped IfDirectiveTrivia + #If RELEASE Then + */ /* TODO ERROR: Skipped DisabledTextTrivia + '复制 PCL 本体 + If IncludePCL Then CopyFile(ExePathWithName, CacheFolder & "Plain Craft Launcher.exe") + */ /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ // 复制 PCL 个性化内容 + if (IncludePCLCustom) + { + if (Directory.Exists(ModBase.ExePath + @"PCL\Pictures\")) + ModBase.CopyDirectory(ModBase.ExePath + @"PCL\Pictures\", CacheFolder + @"PCL\Pictures\"); + if (Directory.Exists(ModBase.ExePath + @"PCL\Musics\")) + ModBase.CopyDirectory(ModBase.ExePath + @"PCL\Musics\", CacheFolder + @"PCL\Musics\"); + if (Directory.Exists(ModBase.ExePath + @"PCL\Help\")) + ModBase.CopyDirectory(ModBase.ExePath + @"PCL\Help\", CacheFolder + @"PCL\Help\"); + if (File.Exists(ModBase.ExePath + @"PCL\Custom.xaml")) + ModBase.CopyFile(ModBase.ExePath + @"PCL\Custom.xaml", CacheFolder + @"PCL\Custom.xaml"); + if (File.Exists(ModBase.ExePath + @"PCL\Setup.ini")) + ModBase.CopyFile(ModBase.ExePath + @"PCL\Setup.ini", CacheFolder + @"PCL\Setup.ini"); + if (File.Exists(ModBase.ExePath + @"PCL\hints.txt")) + ModBase.CopyFile(ModBase.ExePath + @"PCL\hints.txt", CacheFolder + @"PCL\hints.txt"); + if (File.Exists(ModBase.ExePath + @"PCL\Logo.png")) + ModBase.CopyFile(ModBase.ExePath + @"PCL\Logo.png", CacheFolder + @"PCL\Logo.png"); + } + }) + { + ProgressWeight = 5d + }); + + #endregion + + #region 联网检查 + + Loaders.Add( + new ModLoader.LoaderTask, + Dictionary>>("联网获取文件信息", Loader => + { + Loader.Output = new Dictionary>(); + if (!CheckHostedAssets) + { + ModBase.Log("[Export] 要求跳过联网获取步骤"); + return; + } + + if (!Loader.Input.Any()) + { + ModBase.Log("[Export] 没有需要联网检查的文件,跳过联网获取步骤"); + return; + } + + // 分平台获取下载地址 + var EndedThreadCount = 0; + var FailedExceptions = new List(); + + // 从 Modrinth 获取信息 + // 查找对应的文件 + // 写入下载地址 + ModBase.RunInNewThread(() => + { + try + { + var ModrinthHashes = Loader.Input.Select(m => m.ModrinthHash); + var ModrinthRaw = (JObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.modrinth.com/v2/version_files", "POST", + $"{{\"hashes\": [\"{ModrinthHashes.Join("\",\"")}\"], \"algorithm\": \"sha1\"}}", + "application/json")); + foreach (var ModFile in Loader.Input) + { + if (!ModrinthRaw.ContainsKey(ModFile.ModrinthHash)) continue; + if ((string)ModrinthRaw[ModFile.ModrinthHash]?["files"]?[0]["hashes"]?["sha1"] != + ModFile.ModrinthHash) continue; + Loader.Output.AddToList(ModFile, + (string)ModrinthRaw[ModFile.ModrinthHash]["files"][0]["url"]); + } + + ModBase.Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地资源项的对应信息"); + } + catch (Exception ex) + { + ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败"); + FailedExceptions.Add(ex); + } + finally + { + EndedThreadCount += 1; + Loader.Progress += 0.45d; + } + }, "Modrinth - " + LoaderName); + + // 从 CurseForge 获取信息 + // 查找对应的文件 + // 写入下载地址 + ModBase.RunInNewThread(() => + { + try + { + if (ModrinthUploadMode) return; + var CurseForgeHashes = Loader.Input.Select(m => m.CurseForgeHash); + var CurseForgeRaw = (JContainer)((JObject)ModBase.GetJson( + ModDownload.DlModRequest("https://api.curseforge.com/v1/fingerprints/432/", "POST", + $"{{\"fingerprints\": [{CurseForgeHashes.Join(",")}]}}", "application/json")))["data"][ + "exactMatches"]; + foreach (JObject ResultJson in CurseForgeRaw) + { + if (!ResultJson.ContainsKey("file")) continue; + var File = (JObject)ResultJson["file"]; + if (string.IsNullOrEmpty((string)File["downloadUrl"])) continue; + var ModFile = Loader.Input.FirstOrDefault(m => + m.CurseForgeHash == File["fileFingerprint"].ToObject()); + if (ModFile is null) continue; + Loader.Output.AddToList(ModFile, + ModComp.CompFile.HandleCurseForgeDownloadUrls(File["downloadUrl"].ToString())); + } + + ModBase.Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地资源项的对应信息"); + } + catch (Exception ex) + { + ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败"); + FailedExceptions.Add(ex); + } + finally + { + EndedThreadCount += 1; + Loader.Progress += 0.45d; + } + }, "CurseForge - " + LoaderName); // Modrinth 上传模式下,不能从 CurseForge 获取信息 + + // 等待线程结束 + while (EndedThreadCount != 2) + { + if (Loader.IsAborted) + return; + Thread.Sleep(10); + } + + // 若失败,确认是否继续 + if (FailedExceptions.Count == 1) + { + if (ModMain.MyMsgBox( + "联网获取部分文件信息失败,是否继续导出?" + "\r\n" + "\r\n" + "若继续,无法获取信息的文件将被直接打包。" + + "\r\n" + "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!", "部分文件信息获取失败", "继续", "取消") == 2) + throw FailedExceptions.First(); + } + else if (FailedExceptions.Count > 1) + { + if (ModMain.MyMsgBox( + "联网获取文件信息失败,是否继续导出?" + "\r\n" + "\r\n" + "若继续,所有文件都将被直接打包。" + + "\r\n" + "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!", "文件信息获取失败", "继续", "取消") == 2) + throw FailedExceptions.First(); + } + }) + { + Show = CheckHostedAssets, + ProgressWeight = CheckHostedAssets ? 2d : 0.01d + }); + + #endregion + + #region 生成压缩包 + + Loaders.Add(new ModLoader.LoaderTask>, int>("生成压缩包", + Loader => + { + // 整理文件列表 + var Files = new JArray(); + foreach (var Pair in Loader.Input) + { + var ModFile = Pair.Key; + Files.Add(new JObject + { + { "path", ModFile.Path.AfterFirst(OverridesFolder).Replace(@"\", "/") }, + { + "hashes", + new JObject + { + { "sha1", ModFile.ModrinthHash }, { "sha512", ModBase.GetFileSHA512(ModFile.Path) } + } + }, + { "downloads", new JArray(Pair.Value.OrderByDescending(u => u.Contains("modrinth.com"))) }, + { "fileSize", new FileInfo(ModFile.Path).Length } + }); + File.Delete(ModFile.Path); + } + + Loader.Progress = 0.2d; + // 导出最终 JSON 文件 + var Dependencies = new JObject { { "minecraft", McInstance.Info.VanillaName } }; + if (McInstance.Info.HasForge) + Dependencies.Add("forge", McInstance.Info.Forge); + if (McInstance.Info.HasFabric) + Dependencies.Add("fabric-loader", McInstance.Info.Fabric); + if (McInstance.Info.HasNeoForge) + Dependencies.Add("neoforge", McInstance.Info.NeoForge); + var ResultJson = new JObject + { + { "game", "minecraft" }, { "formatVersion", 1 }, { "versionId", PackVersion }, { "name", PackName }, + { "summary", McInstance.Desc }, { "files", Files }, { "dependencies", Dependencies } + }; + File.WriteAllText(CacheFolder + @"modpack\modrinth.index.json", + ResultJson.ToString(Formatting.Indented)); + // 打包 + Directory.CreateDirectory(ModBase.GetPathFromFullPath(PackPath)); + if (File.Exists(PackPath)) + File.Delete(PackPath); + if (IncludePCL) + { + // 首次压缩整合包 + ZipFile.CreateFromDirectory(CacheFolder + @"modpack\", CacheFolder + "modpack.mrpack"); + Loader.Progress = 0.5d; + Directory.Delete(CacheFolder + @"modpack\", true); + Loader.Progress = 0.6d; + // 二次压缩整合包 + ZipFile.CreateFromDirectory(CacheFolder, PackPath); + Loader.Progress = 0.9d; + } + else + { + // 直接压缩整合包 + ZipFile.CreateFromDirectory(CacheFolder + @"modpack\", PackPath); + Loader.Progress = 0.8d; + } + + Directory.Delete(CacheFolder, true); + ModBase.OpenExplorer(PackPath); + }) + { + ProgressWeight = 6d + }); + + #endregion + + // 启动 + var MainLoader = new ModLoader.LoaderCombo(LoaderName, Loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + MainLoader.Start(); + ModLoader.LoaderTaskbarAdd(MainLoader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + ModMain.FrmMain.PageChange(FormMain.PageType.TaskManager); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml index 1c2f96883..5c73084e1 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml @@ -1,129 +1,218 @@  - + - + - + - - - - - - - - + + + + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + @@ -131,35 +220,27 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - + - + @@ -167,53 +248,82 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -221,79 +331,121 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -309,4 +461,4 @@ Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" / - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.cs new file mode 100644 index 000000000..f8b5f3320 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.cs @@ -0,0 +1,2787 @@ +using System.Collections; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.App; + +namespace PCL; + +public partial class PageInstanceInstall +{ + private bool IsLoad; + private string LastVersionName; + + public PageInstanceInstall() + { + Initialized += (a, b) => LoaderInit(); + Loaded += (a, b) => Init(); + InitializeComponent(); + } + + private void LoaderInit() + { + DisabledPageAnimControls.Add(BtnSelectStart); + // PageLoaderInit(LoadMinecraft, PanLoad, PanBack, Nothing, DlClientListLoader, AddressOf LoadMinecraft_OnFinish) + PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, null, ModDownload.DlClientListLoader, _ => GetCurrentInfo()); + } + + private void Init() + { + PanBack.ScrollToHome(); + + GetCurrentInfo(); + + var NeedRefresh = LastVersionName is null || (LastVersionName ?? "") != (_vanillaName ?? ""); + LastVersionName = _vanillaName; + + ModDownload.DlOptiFineListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlLiteLoaderListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlFabricListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlQuiltListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlNeoForgeListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlCleanroomListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlLabyModListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlLegacyFabricListLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlFabricApiLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlQSLLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlLegacyFabricApiLoader.Start(IsForceRestart: NeedRefresh); + ModDownload.DlOptiFabricLoader.Start(IsForceRestart: NeedRefresh); + + // 重载预览 + ReloadSelected(); + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + + ModDownloadLib.McDownloadForgeRecommendedRefresh(); + + LoadOptiFine.State = ModDownload.DlOptiFineListLoader; + LoadLiteLoader.State = ModDownload.DlLiteLoaderListLoader; + LoadFabric.State = ModDownload.DlFabricListLoader; + LoadFabricApi.State = ModDownload.DlFabricApiLoader; + LoadQuilt.State = ModDownload.DlQuiltListLoader; + LoadQSL.State = ModDownload.DlQSLLoader; + LoadNeoForge.State = ModDownload.DlNeoForgeListLoader; + LoadCleanroom.State = ModDownload.DlCleanroomListLoader; + LoadOptiFabric.State = ModDownload.DlOptiFabricLoader; + LoadLabyMod.State = ModDownload.DlLabyModListLoader; + LoadLegacyFabric.State = ModDownload.DlLegacyFabricListLoader; + LoadLegacyFabricApi.State = ModDownload.DlLegacyFabricApiLoader; + } + + #region 安装 + + private void BtnSelectStart_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs) + { + // 确认版本隔离 + if (SelectedLoaderName is not null && + (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.IndieSolutionV2, 0, false)) || + Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.IndieSolutionV2, 2, false)))) + if (ModMain.MyMsgBox( + "你尚未开启版本隔离,这会导致多个 MC 共用同一个 Mod 文件夹。" + "\r\n" + + "因此在切换 MC 实例时,MC 会因为读取到与当前实例不符的 Mod 而崩溃。" + "\r\n" + + "PCL 推荐你在开始下载前,在 设置 → 版本隔离 中开启版本隔离选项!", "版本隔离提示", "取消下载", "继续") == 1) + return; + + if (BtnSelectStart.Text == "开始重置") + if (ModMain.MyMsgBox( + "你正在重置当前实例。" + "\r\n" + "PCL 将会重新联网下载该实例所需的文件,并重新安装 Mod 加载器(如有)。" + "\r\n" + + "此操作不会丢失你的存档、Mod、资源包等。", "重置此实例", "继续", "取消") == 2) + return; + + // 删除 LabyMod Neo 文件 + if ((PageInstanceLeft.Instance.PathIndie ?? "") != (PageInstanceLeft.Instance.PathInstance ?? "") && + PageInstanceLeft.Instance.Info.HasLabyMod) + Directory.Delete(PageInstanceLeft.Instance.PathIndie + "labymod-neo", true); + // 备份实例核心文件 + ModBase.CopyFile(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + ".json", + PageInstanceLeft.Instance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.Instance.Name + ".json"); + if (File.Exists(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + ".jar")) + ModBase.CopyFile(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + ".jar", + PageInstanceLeft.Instance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.Instance.Name + + ".jar"); + // 确认独立 API (如 Fabric API 等) 是否需要被修改 + if (SelectedFabricApi?.Equals(_currentFabricApi) == true) + SelectedFabricApi = null; + if (SelectedLegacyFabricApi?.Equals(_currentLegacyFabricApi) == true) + SelectedLegacyFabricApi = null; + if (SelectedQSL?.Equals(_currentQsl) == true) + SelectedQSL = null; + if (SelectedOptiFabric?.Equals(_currentOptiFabric) == true) + SelectedOptiFabric = null; + // 提交安装申请 + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = PageInstanceLeft.Instance.Name, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{PageInstanceLeft.Instance.Name}\", + MinecraftJson = _vanillaData?["url"].ToString(), + MinecraftName = _vanillaName, + OptiFineEntry = SelectedOptiFine, + ForgeEntry = SelectedForge, + NeoForgeEntry = SelectedNeoForge, + NeoForgeVersion = SelectedNeoForgeVersion, + CleanroomEntry = SelectedCleanroom, + CleanroomVersion = SelectedCleanroomVersion, + FabricVersion = SelectedFabric, + FabricApi = SelectedFabricApi, + QuiltVersion = SelectedQuilt, + QSL = SelectedQSL, + OptiFabric = SelectedOptiFabric, + LiteLoaderEntry = SelectedLiteLoader, + LabyModChannel = SelectedLabyModChannel, + LabyModCommitRef = SelectedLabyModCommitRef, + LegacyFabricVersion = SelectedLegacyFabric, + LegacyFabricApi = SelectedLegacyFabricApi + }; + BtnSelectStart.IsEnabled = false; + if (!ModDownloadLib.McInstall(Request, BtnSelectStart.Text.AfterFirst("开始"))) + return; + // 删除旧的独立 API 文件 + if (SelectedFabricApi is not null & _currentFabricApiPath is not null) + File.Delete(_currentFabricApiPath); + if (SelectedLegacyFabricApi is not null & _currentLegacyFabricApiPath is not null) + File.Delete(_currentLegacyFabricApiPath); + if (SelectedQSL is not null & _currentQslPath is not null) + File.Delete(_currentQslPath); + if (SelectedOptiFabric is not null & _currentOptiFabricPath is not null) + File.Delete(_currentOptiFabricPath); + // 返回主页 + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.Launch }); + } + + #endregion + + private string GetLoaderError(MyLoading loader) + { + if (loader is null) + return "获取中……"; + if (!loader.State.IsLoader) + return "获取中……"; + switch (loader.State.LoadingState) + { + case MyLoading.MyLoadingState.Run: + { + return "获取中……"; + } + case MyLoading.MyLoadingState.Error: + { + var message = ((ModLoader.LoaderBase)loader.State).Error.Message; + return message == "无可用版本" ? "无可用版本" : "获取失败:" + message; + } + case MyLoading.MyLoadingState.Unloaded: + { + return "未知错误,状态为 Unloaded"; + } + + default: + { + return null; + } + } + } + + #region 页面切换 + + // 页面切换动画 + public bool IsInSelectPage; + private bool IsFirstLoaded; + + private void EnterSelectPage() + { + if (IsInSelectPage) + return; + IsInSelectPage = true; + + DisabledPageAnimControls.Remove(BtnSelectStart); + BtnSelectStart.Show = true; + AutoSelectedFabricApi = false; + AutoSelectedQSL = false; + AutoSelectedOptiFabric = false; + PanSelect.Visibility = Visibility.Visible; + PanSelect.IsHitTestVisible = true; + PanMinecraft.IsHitTestVisible = false; + PanBack.IsHitTestVisible = false; + PanBack.ScrollToHome(); + + CardMinecraft.IsSwapped = true; + CardOptiFine.IsSwapped = true; + CardLiteLoader.IsSwapped = true; + CardForge.IsSwapped = true; + CardNeoForge.IsSwapped = true; + CardCleanroom.IsSwapped = true; + CardFabric.IsSwapped = true; + CardFabricApi.IsSwapped = true; + CardQuilt.IsSwapped = true; + CardQSL.IsSwapped = true; + CardOptiFabric.IsSwapped = true; + CardLabyMod.IsSwapped = true; + CardLegacyFabric.IsSwapped = true; + CardLegacyFabricApi.IsSwapped = true; + + if (!(bool)States.Hint.InstallPageBack) + { + States.Hint.InstallPageBack = true; + ModMain.Hint("点击 Minecraft 项即可返回游戏主版本选择页面!"); + } + + // 如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复 + foreach (var Card in GetAllAnimControls(PanSelect)) + { + Card.Opacity = 1d; + Card.RenderTransform = new TranslateTransform(); + } + + // 启动 Forge 加载 + if (ModMinecraft.McInstanceInfo.IsFormatFit(_vanillaName)) + { + var ForgeLoader = + new ModLoader.LoaderTask>( + "DlForgeVersion " + _vanillaName, ModDownload.DlForgeVersionMain); + LoadForge.State = ForgeLoader; + ForgeLoader.Start(_vanillaName); + } + + // 启动 Fabric API、QSL、Legacy Fabric API、OptiFabric、LabyMod 加载 + ModDownload.DlFabricApiLoader.Start(); + ModDownload.DlQSLLoader.Start(); + ModDownload.DlLegacyFabricApiLoader.Start(); + ModDownload.DlOptiFabricLoader.Start(); + + ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(PanMinecraft, -PanMinecraft.Opacity, 100, 10), + ModAnimation.AaCode(() => + { + PanBack.ScrollToHome(); + OptiFine_Loaded(); + LiteLoader_Loaded(); + Forge_Loaded(); + NeoForge_Loaded(); + Cleanroom_Loaded(); + Fabric_Loaded(); + LegacyFabric_Loaded(); + FabricApi_Loaded(); + LegacyFabricApi_Loaded(); + Quilt_Loaded(); + QSL_Loaded(); + LabyMod_Loaded(); + OptiFabric_Loaded(); + ReloadSelected(); + }, After: true), + ModAnimation.AaOpacity(PanSelect, 1d - PanSelect.Opacity, 250, 150), + ModAnimation.AaCode(() => + { + PanMinecraft.Visibility = Visibility.Collapsed; + PanBack.IsHitTestVisible = true; + // 初始化 Binding + if (IsFirstLoaded) + return; + IsFirstLoaded = true; + BtnOptiFineClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardOptiFine.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLiteLoaderClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLiteLoader.MainTextBlock, Mode = BindingMode.OneWay }); + BtnForgeClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardForge.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLegacyFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLegacyFabric.MainTextBlock, Mode = BindingMode.OneWay }); + BtnNeoForgeClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardNeoForge.MainTextBlock, Mode = BindingMode.OneWay }); + BtnCleanroomClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardCleanroom.MainTextBlock, Mode = BindingMode.OneWay }); + BtnFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardFabric.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLegacyFabricApiClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") + { Source = CardLegacyFabricApi.MainTextBlock, Mode = BindingMode.OneWay }); + BtnFabricApiClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardFabricApi.MainTextBlock, Mode = BindingMode.OneWay }); + BtnQuiltClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardQuilt.MainTextBlock, Mode = BindingMode.OneWay }); + BtnQSLClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardQSL.MainTextBlock, Mode = BindingMode.OneWay }); + BtnLabyModClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardLabyMod.MainTextBlock, Mode = BindingMode.OneWay }); + BtnOptiFabricClearInner.SetBinding(Shape.FillProperty, + new Binding("Foreground") { Source = CardOptiFabric.MainTextBlock, Mode = BindingMode.OneWay }); + }, After: true) + }, "FrmInstanceInstall SelectPageSwitch", true); + } + + public void ExitSelectPage() + { + if (!IsInSelectPage) + return; + IsInSelectPage = false; + + LoadMinecraft_OnFinish(); + + DisabledPageAnimControls.Add(BtnSelectStart); + BtnSelectStart.Show = false; + + ClearSelected(); // 清除已选择项 + PanMinecraft.Visibility = Visibility.Visible; + PanSelect.IsHitTestVisible = false; + PanMinecraft.IsHitTestVisible = true; + PanBack.IsHitTestVisible = false; + PanBack.ScrollToHome(); + + ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(PanSelect, -PanSelect.Opacity, 90, 10), + ModAnimation.AaCode(() => PanBack.ScrollToHome(), After: true), + ModAnimation.AaOpacity(PanMinecraft, 1d - PanMinecraft.Opacity, 150, 100), + ModAnimation.AaCode(() => + { + PanSelect.Visibility = Visibility.Collapsed; + PanBack.IsHitTestVisible = true; + }, After: true) + }, "FrmInstanceInstall SelectPageSwitch"); + } + + // 页面切换触发 + public void MinecraftSelected(MyListItem sender, MouseButtonEventArgs e) + { + _vanillaName = sender.Title; + _vanillaData = (JObject)sender.Tag; + _vanillaIcon = sender.Logo; + EnterSelectPage(); + } + + private void CardMinecraft_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + ExitSelectPage(); + e.Handled = true; + } + + #endregion + + #region 选择 + + // Minecraft + private string? _vanillaName; + private JObject? _vanillaData; + private string? _vanillaIcon; + private int VanillaDrop => ModMinecraft.McInstanceInfo.VersionToDrop(_vanillaName, true); + + // OptiFine + private ModDownload.DlOptiFineListEntry? SelectedOptiFine; + + /// + /// 选定的 Mod Loader 名称,内容应为 Forge / NeoForge / Fabric / Quilt / Cleanroom / LabyMod + /// + private string? SelectedLoaderName; + + /// + /// 选定的 Mod Loader API 名称,内容应为 Fabric API 或 QFAPI / QSL + /// + private string? SelectedAPIName; + + // LiteLoader + private ModDownload.DlLiteLoaderListEntry? SelectedLiteLoader; + + // Forge + private ModDownload.DlForgeVersionEntry? SelectedForge; + + // Cleanroom + private ModDownload.DlCleanroomListEntry? SelectedCleanroom; + private string? SelectedCleanroomVersion; + + // NeoForge + private ModDownload.DlNeoForgeListEntry? SelectedNeoForge; + private string? SelectedNeoForgeVersion; + + // Fabric + private string? SelectedFabric; + + // FabricApi + private ModComp.CompFile? SelectedFabricApi; + + // LegacyFabric + private string? SelectedLegacyFabric; + + // Legacy FabricApi + private ModComp.CompFile? SelectedLegacyFabricApi; + + // Quilt + private string? SelectedQuilt; + + // QSL + private ModComp.CompFile? SelectedQSL; + + // LabyMod + private string? SelectedLabyModChannel; + private string? SelectedLabyModCommitRef; + private string? SelectedLabyModVersion; + + // OptiFabric + private ModComp.CompFile? SelectedOptiFabric; + + private bool _ReloadSelected_Ongoing; // #3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload + + /// + /// 重载已选择的项目的显示。 + /// + private void ReloadSelected() + { + if (_vanillaName is null || _ReloadSelected_Ongoing) + return; + _ReloadSelected_Ongoing = true; + var selectedInfo = GetSelectInfo(); + // 主预览 + ItemSelect.Title = PageInstanceLeft.Instance.Name; + ItemSelect.Logo = GetSelectLogo(); + BtnSelectStart.IsEnabled = true; + if ((selectedInfo ?? "") == (CurrentInfo ?? "")) + { + ItemSelect.Info = selectedInfo; + BtnSelectStart.Text = "开始重置"; + BtnSelectStart.Logo = ModBase.Logo.IconButtonReset; + } + else + { + ItemSelect.Info = CurrentInfo + " → " + selectedInfo; + BtnSelectStart.Text = "开始修改"; + BtnSelectStart.Logo = ModBase.Logo.IconButtonEdit; + } + + // Minecraft + ImgMinecraft.Source = new MyBitmap(_vanillaIcon); + LabMinecraft.Text = _vanillaName; + LabMinecraft.Foreground = ModSecret.ColorGray1; + // OptiFine + var OptiFineError = LoadOptiFineGetError(); + CardOptiFine.MainSwap.Visibility = OptiFineError is null ? Visibility.Visible : Visibility.Collapsed; + if (OptiFineError is not null) + CardOptiFine.IsSwapped = true; // 例如在同时展开卡片时选择了不兼容项则强制折叠 + SetPanelVisibility(PanOptiFineInfo, CardOptiFine.IsSwapped); + if (SelectedOptiFine is null) + { + BtnOptiFineClear.Visibility = Visibility.Collapsed; + ImgOptiFine.Visibility = Visibility.Collapsed; + LabOptiFine.Text = OptiFineError ?? "可以添加"; + LabOptiFine.Foreground = ModSecret.ColorGray4; + } + else + { + BtnOptiFineClear.Visibility = Visibility.Visible; + ImgOptiFine.Visibility = Visibility.Visible; + LabOptiFine.Text = SelectedOptiFine.DisplayName.Replace(_vanillaName + " ", ""); + LabOptiFine.Foreground = ModSecret.ColorGray1; + } + + // LiteLoader + if (VanillaDrop >= 130) + { + CardLiteLoader.Visibility = Visibility.Collapsed; + } + else + { + CardLiteLoader.Visibility = Visibility.Visible; + var LiteLoaderError = LoadLiteLoaderGetError(); + CardLiteLoader.MainSwap.Visibility = LiteLoaderError is null ? Visibility.Visible : Visibility.Collapsed; + if (LiteLoaderError is not null) + CardLiteLoader.IsSwapped = true; // 例如在同时展开卡片时选择了不兼容项则强制折叠 + SetPanelVisibility(PanLiteLoaderInfo, CardLiteLoader.IsSwapped); + if (SelectedLiteLoader is null) + { + BtnLiteLoaderClear.Visibility = Visibility.Collapsed; + ImgLiteLoader.Visibility = Visibility.Collapsed; + LabLiteLoader.Text = LiteLoaderError ?? "可以添加"; + LabLiteLoader.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLiteLoaderClear.Visibility = Visibility.Visible; + ImgLiteLoader.Visibility = Visibility.Visible; + LabLiteLoader.Text = SelectedLiteLoader.Inherit; + LabLiteLoader.Foreground = ModSecret.ColorGray1; + } + } + + // Forge + if (!ModMinecraft.McInstanceInfo.IsFormatFit(_vanillaName)) + { + CardForge.Visibility = Visibility.Collapsed; + } + else + { + CardForge.Visibility = Visibility.Visible; + var forgeError = LoadForgeGetError(); + CardForge.MainSwap.Visibility = forgeError is null ? Visibility.Visible : Visibility.Collapsed; + if (forgeError is not null) + CardForge.IsSwapped = true; + SetPanelVisibility(PanForgeInfo, CardForge.IsSwapped); + if (SelectedForge is null) + { + BtnForgeClear.Visibility = Visibility.Collapsed; + ImgForge.Visibility = Visibility.Collapsed; + LabForge.Text = forgeError ?? "可以添加"; + LabForge.Foreground = ModSecret.ColorGray4; + } + else + { + BtnForgeClear.Visibility = Visibility.Visible; + ImgForge.Visibility = Visibility.Visible; + LabForge.Text = SelectedForge.VersionName; + LabForge.Foreground = ModSecret.ColorGray1; + } + } + + // Cleanroom + if (_vanillaName == "1.12.2") + { + CardCleanroom.Visibility = Visibility.Visible; + var cleanroomError = LoadCleanroomGetError(); + CardCleanroom.MainSwap.Visibility = cleanroomError is null ? Visibility.Visible : Visibility.Collapsed; + if (cleanroomError is not null) + CardCleanroom.IsSwapped = true; + SetPanelVisibility(PanCleanroomInfo, CardCleanroom.IsSwapped); + if (SelectedCleanroom is null) + { + BtnCleanroomClear.Visibility = Visibility.Collapsed; + ImgCleanroom.Visibility = Visibility.Collapsed; + LabCleanroom.Text = cleanroomError ?? "可以添加"; + LabCleanroom.Foreground = ModSecret.ColorGray4; + } + else + { + BtnCleanroomClear.Visibility = Visibility.Visible; + ImgCleanroom.Visibility = Visibility.Visible; + LabCleanroom.Text = SelectedCleanroom.VersionName; + LabCleanroom.Foreground = ModSecret.ColorGray1; + } + } + else + { + CardCleanroom.Visibility = Visibility.Collapsed; + } + + // NeoForge + if (VanillaDrop is > 0 and < 200) // 匹配 1.20.1+ 与一些愚人节版本 + { + CardNeoForge.Visibility = Visibility.Collapsed; + } + else + { + CardNeoForge.Visibility = Visibility.Visible; + var neoForgeError = LoadNeoForgeGetError(); + CardNeoForge.MainSwap.Visibility = neoForgeError is null ? Visibility.Visible : Visibility.Collapsed; + if (neoForgeError is not null) + CardNeoForge.IsSwapped = true; + SetPanelVisibility(PanNeoForgeInfo, CardNeoForge.IsSwapped); + if (SelectedNeoForge is null) + { + BtnNeoForgeClear.Visibility = Visibility.Collapsed; + ImgNeoForge.Visibility = Visibility.Collapsed; + LabNeoForge.Text = neoForgeError ?? "可以添加"; + LabNeoForge.Foreground = ModSecret.ColorGray4; + } + else + { + BtnNeoForgeClear.Visibility = Visibility.Visible; + ImgNeoForge.Visibility = Visibility.Visible; + LabNeoForge.Text = SelectedNeoForge.VersionName; + LabNeoForge.Foreground = ModSecret.ColorGray1; + } + } + + // Fabric + if (VanillaDrop <= 130) + { + CardFabric.Visibility = Visibility.Collapsed; + } + else + { + CardFabric.Visibility = Visibility.Visible; + var fabricError = LoadFabricGetError(); + CardFabric.MainSwap.Visibility = fabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (fabricError is not null) + CardFabric.IsSwapped = true; + SetPanelVisibility(PanFabricInfo, CardFabric.IsSwapped); + if (SelectedFabric is null) + { + BtnFabricClear.Visibility = Visibility.Collapsed; + ImgFabric.Visibility = Visibility.Collapsed; + LabFabric.Text = fabricError ?? "可以添加"; + LabFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnFabricClear.Visibility = Visibility.Visible; + ImgFabric.Visibility = Visibility.Visible; + LabFabric.Text = SelectedFabric.Replace("+build", ""); + LabFabric.Foreground = ModSecret.ColorGray1; + } + } + + // FabricApi + if (SelectedFabric is null && SelectedQuilt is null) + { + CardFabricApi.Visibility = Visibility.Collapsed; + } + else + { + CardFabricApi.Visibility = Visibility.Visible; + var fabricApiError = LoadFabricApiGetError(); + CardFabricApi.MainSwap.Visibility = fabricApiError is null ? Visibility.Visible : Visibility.Collapsed; + if (fabricApiError is not null || (SelectedFabric is null && SelectedQuilt is null)) + CardFabricApi.IsSwapped = true; + SetPanelVisibility(PanFabricApiInfo, CardFabricApi.IsSwapped); + if (SelectedFabricApi is null) + { + BtnFabricApiClear.Visibility = Visibility.Collapsed; + ImgFabricApi.Visibility = Visibility.Collapsed; + LabFabricApi.Text = fabricApiError ?? "可以添加"; + LabFabricApi.Foreground = ModSecret.ColorGray4; + } + else + { + BtnFabricApiClear.Visibility = Visibility.Visible; + ImgFabricApi.Visibility = Visibility.Visible; + LabFabricApi.Text = SelectedFabricApi.DisplayName.Split("]")[1].Replace("Fabric API ", "") + .Replace(" build ", ".").Split("+").First().Trim(); + LabFabricApi.Foreground = ModSecret.ColorGray1; + } + } + + // LegacyFabric + if (VanillaDrop > 130) + { + CardLegacyFabric.Visibility = Visibility.Collapsed; + } + else + { + CardLegacyFabric.Visibility = Visibility.Visible; + var legacyFabricError = LoadLegacyFabricGetError(); + CardLegacyFabric.MainSwap.Visibility = + legacyFabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (legacyFabricError is not null) + CardLegacyFabric.IsSwapped = true; + SetPanelVisibility(PanLegacyFabricInfo, CardLegacyFabric.IsSwapped); + if (SelectedLegacyFabric is null) + { + BtnLegacyFabricClear.Visibility = Visibility.Collapsed; + ImgLegacyFabric.Visibility = Visibility.Collapsed; + LabLegacyFabric.Text = legacyFabricError ?? "可以添加"; + LabLegacyFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLegacyFabricClear.Visibility = Visibility.Visible; + ImgLegacyFabric.Visibility = Visibility.Visible; + LabLegacyFabric.Text = SelectedLegacyFabric.Replace("+build", ""); + LabLegacyFabric.Foreground = ModSecret.ColorGray1; + } + } + + // LegacyFabricApi + if (SelectedLegacyFabric is null) + { + CardLegacyFabricApi.Visibility = Visibility.Collapsed; + } + else + { + CardLegacyFabricApi.Visibility = Visibility.Visible; + var legacyFabricApiError = LoadLegacyFabricApiGetError(); + CardLegacyFabricApi.MainSwap.Visibility = + legacyFabricApiError is null ? Visibility.Visible : Visibility.Collapsed; + if (legacyFabricApiError is not null || (SelectedLegacyFabric is null && SelectedQuilt is null)) + CardLegacyFabricApi.IsSwapped = true; + SetPanelVisibility(PanLegacyFabricApiInfo, CardLegacyFabricApi.IsSwapped); + if (SelectedLegacyFabricApi is null) + { + BtnLegacyFabricApiClear.Visibility = Visibility.Collapsed; + ImgLegacyFabricApi.Visibility = Visibility.Collapsed; + LabLegacyFabricApi.Text = legacyFabricApiError ?? "可以添加"; + LabLegacyFabricApi.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLegacyFabricApiClear.Visibility = Visibility.Visible; + ImgLegacyFabricApi.Visibility = Visibility.Visible; + LabLegacyFabricApi.Text = SelectedLegacyFabricApi.DisplayName.Replace("Legacy Fabric API ", ""); + LabLegacyFabricApi.Foreground = ModSecret.ColorGray1; + } + } + + // Quilt + if (VanillaDrop < 144) + { + CardQuilt.Visibility = Visibility.Collapsed; + } + else + { + CardQuilt.Visibility = Visibility.Visible; + var quiltError = LoadQuiltGetError(); + CardQuilt.MainSwap.Visibility = quiltError is null ? Visibility.Visible : Visibility.Collapsed; + if (quiltError is not null) + CardQuilt.IsSwapped = true; + SetPanelVisibility(PanQuiltInfo, CardQuilt.IsSwapped); + if (SelectedQuilt is null) + { + BtnQuiltClear.Visibility = Visibility.Collapsed; + ImgQuilt.Visibility = Visibility.Collapsed; + LabQuilt.Text = quiltError ?? "可以添加"; + LabQuilt.Foreground = ModSecret.ColorGray4; + } + else + { + BtnQuiltClear.Visibility = Visibility.Visible; + ImgQuilt.Visibility = Visibility.Visible; + LabQuilt.Text = SelectedQuilt.Replace("+build", ""); + LabQuilt.Foreground = ModSecret.ColorGray1; + } + } + + // QSL + if (SelectedQuilt is null) + { + CardQSL.Visibility = Visibility.Collapsed; + } + else + { + CardQSL.Visibility = Visibility.Visible; + var qslError = LoadQSLGetError(); + CardQSL.MainSwap.Visibility = qslError is null ? Visibility.Visible : Visibility.Collapsed; + if (qslError is not null || SelectedQuilt is null) + CardQSL.IsSwapped = true; + SetPanelVisibility(PanQSLInfo, CardQSL.IsSwapped); + if (SelectedQSL is null) + { + BtnQSLClear.Visibility = Visibility.Collapsed; + ImgQSL.Visibility = Visibility.Collapsed; + LabQSL.Text = qslError ?? "可以添加"; + LabQSL.Foreground = ModSecret.ColorGray4; + } + else + { + BtnQSLClear.Visibility = Visibility.Visible; + ImgQSL.Visibility = Visibility.Visible; + LabQSL.Text = SelectedQSL.DisplayName.Split("]")[1].Trim(); + LabQSL.Foreground = ModSecret.ColorGray1; + } + } + + // LabyMod + if (VanillaDrop < 80) + { + CardLabyMod.Visibility = Visibility.Collapsed; + } + else + { + CardLabyMod.Visibility = Visibility.Visible; + var labyModError = LoadLabyModGetError(); + CardLabyMod.MainSwap.Visibility = labyModError is null ? Visibility.Visible : Visibility.Collapsed; + if (labyModError is not null) + CardLabyMod.IsSwapped = true; + SetPanelVisibility(PanLabyModInfo, CardLabyMod.IsSwapped); + if (SelectedLabyModVersion is null) + { + BtnLabyModClear.Visibility = Visibility.Collapsed; + ImgLabyMod.Visibility = Visibility.Collapsed; + LabLabyMod.Text = labyModError ?? "可以添加"; + LabLabyMod.Foreground = ModSecret.ColorGray4; + } + else + { + BtnLabyModClear.Visibility = Visibility.Visible; + ImgLabyMod.Visibility = Visibility.Visible; + LabLabyMod.Text = SelectedLabyModVersion; + LabLabyMod.Foreground = ModSecret.ColorGray1; + } + } + + // OptiFabric + if (SelectedFabric is null || SelectedOptiFine is null) + { + CardOptiFabric.Visibility = Visibility.Collapsed; + } + else + { + CardOptiFabric.Visibility = Visibility.Visible; + var optiFabricError = LoadOptiFabricGetError(); + CardOptiFabric.MainSwap.Visibility = optiFabricError is null ? Visibility.Visible : Visibility.Collapsed; + if (optiFabricError is not null || SelectedFabric is null) + CardOptiFabric.IsSwapped = true; + SetPanelVisibility(PanOptiFabricInfo, CardOptiFabric.IsSwapped); + if (SelectedOptiFabric is null) + { + BtnOptiFabricClear.Visibility = Visibility.Collapsed; + ImgOptiFabric.Visibility = Visibility.Collapsed; + LabOptiFabric.Text = optiFabricError ?? "可以添加"; + LabOptiFabric.Foreground = ModSecret.ColorGray4; + } + else + { + BtnOptiFabricClear.Visibility = Visibility.Visible; + ImgOptiFabric.Visibility = Visibility.Visible; + LabOptiFabric.Text = SelectedOptiFabric.DisplayName.ToLower().Replace("optifabric-", "") + .Replace(".jar", "").Trim().TrimStart('v'); + LabOptiFabric.Foreground = ModSecret.ColorGray1; + } + } + + // 主警告 + if (SelectedFabric is not null && SelectedFabricApi is null) + HintFabricAPI.Visibility = Visibility.Visible; + else + HintFabricAPI.Visibility = Visibility.Collapsed; + if (SelectedLegacyFabric is not null && SelectedLegacyFabricApi is null) + HintLegacyFabricAPI.Visibility = Visibility.Visible; + else + HintLegacyFabricAPI.Visibility = Visibility.Collapsed; + if (SelectedQuilt is not null && SelectedQSL is null && SelectedFabricApi is null) + HintQSL.Visibility = Visibility.Visible; + else + HintQSL.Visibility = Visibility.Collapsed; + if (SelectedQuilt is not null && SelectedFabricApi is not null && ModDownload.DlQSLLoader.Output is not null) + foreach (var Version in ModDownload.DlQSLLoader.Output) + { + if (IsSuitableQSL(Version.GameVersions, _vanillaName)) + { + HintQuiltFabricAPI.Visibility = Visibility.Visible; + break; + } + + HintQuiltFabricAPI.Visibility = Visibility.Collapsed; + } + else + HintQuiltFabricAPI.Visibility = Visibility.Collapsed; + + if (SelectedFabric is not null | SelectedLegacyFabric is not null && SelectedOptiFine is not null && + SelectedOptiFabric is null) + { + if (VanillaDrop >= 140 && VanillaDrop <= 150) + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + HintOptiFabricOld.Visibility = Visibility.Visible; + } + else if (SelectedLegacyFabric is not null) + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Visible; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + } + else + { + HintOptiFabric.Visibility = Visibility.Visible; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + } + } + else + { + HintOptiFabric.Visibility = Visibility.Collapsed; + HintOptiFabricOld.Visibility = Visibility.Collapsed; + HintLegacyOptiFabric.Visibility = Visibility.Collapsed; + } + + if (VanillaDrop >= 160 && SelectedOptiFine is not null && + (SelectedForge is not null || SelectedFabric is not null)) + HintModOptiFine.Visibility = Visibility.Visible; + else + HintModOptiFine.Visibility = Visibility.Collapsed; + // 结束 + _ReloadSelected_Ongoing = false; + } + + /// + /// 清空已选择的项目。 + /// + private void ClearSelected() + { + _vanillaName = null; + _vanillaData = null; + _vanillaIcon = null; + SelectedOptiFine = null; + SelectedLiteLoader = null; + SelectedLoaderName = null; + SelectedAPIName = null; + SelectedForge = null; + SelectedNeoForge = null; + SelectedNeoForgeVersion = null; + SelectedCleanroom = null; + SelectedCleanroomVersion = null; + SelectedFabric = null; + SelectedFabricApi = null; + SelectedQuilt = null; + SelectedQSL = null; + SelectedOptiFabric = null; + SelectedLabyModCommitRef = null; + SelectedLabyModVersion = null; + SelectedLabyModChannel = null; + SelectedLegacyFabric = null; + SelectedLegacyFabricApi = null; + } + + // 信息栏动画 + private void SetPanelVisibility(Grid panel, bool visible) + { + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(panel.Tag, visible.ToString(), false))) + return; + panel.Tag = visible.ToString(); + if (visible) + ModAnimation.AniStart( + new[] + { + ModAnimation.AaTranslateY(panel, -((TranslateTransform)panel.RenderTransform).Y, 150, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaOpacity(panel, 1d - panel.Opacity, 60) + }, "PageDownloadInstall Visibility " + panel.Name); + else + ModAnimation.AniStart( + new[] + { + ModAnimation.AaTranslateY(panel, 6d - ((TranslateTransform)panel.RenderTransform).Y, 60), + ModAnimation.AaOpacity(panel, -panel.Opacity, 60) + }, "PageDownloadInstall Visibility " + panel.Name); + } + + /// + /// 获取实例图标。 + /// + private string GetSelectLogo() + { + if (SelectedFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png"; + + if (SelectedLegacyFabric is not null) return "pack://application:,,,/images/Blocks/Fabric.png"; + + if (SelectedForge is not null) return "pack://application:,,,/images/Blocks/Anvil.png"; + + if (SelectedNeoForge is not null) return "pack://application:,,,/images/Blocks/NeoForge.png"; + + if (SelectedLiteLoader is not null) return "pack://application:,,,/images/Blocks/Egg.png"; + + if (SelectedOptiFine is not null) return "pack://application:,,,/images/Blocks/GrassPath.png"; + + if (SelectedQuilt is not null) return "pack://application:,,,/images/Blocks/Quilt.png"; + + if (SelectedCleanroom is not null) return "pack://application:,,,/images/Blocks/Cleanroom.png"; + + if (SelectedLabyModVersion is not null) return "pack://application:,,,/images/Blocks/LabyMod.png"; + + return _vanillaIcon; + } + + /// + /// 获取实例描述信息。 + /// + private string? GetSelectInfo() + { + var Info = ""; + Info += _vanillaName; + if (SelectedFabric is not null) Info += ", Fabric " + SelectedFabric.Replace("+build", ""); + if (SelectedLegacyFabric is not null) Info += ", Legacy Fabric " + SelectedLegacyFabric; + if (SelectedQuilt is not null) Info += ", Quilt " + SelectedQuilt; + if (SelectedForge is not null) Info += ", Forge " + SelectedForge.VersionName; + if (SelectedNeoForge is not null || SelectedNeoForgeVersion != default) + Info += ", NeoForge " + + (SelectedNeoForge is not null ? SelectedNeoForge.VersionName : SelectedNeoForgeVersion); + if (SelectedCleanroom is not null || SelectedCleanroomVersion != default) + Info += ", Cleanroom " + + (SelectedCleanroom is not null ? SelectedCleanroom.VersionName : SelectedCleanroomVersion); + if (SelectedLabyModVersion is not null) Info += ", LabyMod " + SelectedLabyModVersion; + if (SelectedLiteLoader is not null) Info += ", LiteLoader"; + if (SelectedOptiFine is not null) + Info += ", OptiFine " + SelectedOptiFine.DisplayName.Replace(_vanillaName + " ", ""); + if ((Info ?? "") == (_vanillaName ?? "")) + Info += ", 无附加安装"; + return Info?.TrimStart(", ".ToCharArray()); + } + + #endregion + + #region 当前信息获取 + + private ModComp.CompFile _currentFabricApi; // 加载完成后直接调用以提高性能 + private string _currentFabricApiPath; + + private object GetCurrentFabricApi() // 进入页面和联网加载时调用 + { + var loaderOutput = ModDownload.DlFabricApiLoader.Output; + if (loaderOutput is null) + return null; // 确保联网信息已加载 + var localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.Instance, + new[] { "fabric-api", "fabric" }, "fabric", "api"); + if (localComp is null) + return null; + var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? "")); + if (result is not null) + { + _currentFabricApi = result; + _currentFabricApiPath = localComp.Path; + } + + return result; + } + + private ModComp.CompFile _currentLegacyFabricApi; // 加载完成后直接调用以提高性能 + private string _currentLegacyFabricApiPath; + + private object GetCurrentLegacyFabricApi() // 进入页面和联网加载时调用 + { + var loaderOutput = ModDownload.DlLegacyFabricApiLoader.Output; + if (loaderOutput is null) + return null; // 确保联网信息已加载 + var localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.Instance, + new[] { "legacy-fabric-api", "legacy-fabric" }, "legacy-fabric", "api"); + if (localComp is null) + return null; + var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? "")); + if (result is not null) + { + _currentLegacyFabricApi = result; + _currentLegacyFabricApiPath = localComp.Path; + } + + return result; + } + + private ModComp.CompFile _currentQsl; + private string _currentQslPath; + + private object GetCurrentQsl() + { + var loaderOutput = ModDownload.DlQSLLoader.Output; + if (loaderOutput is null) + return null; + var localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.Instance, "quilted_fabric_api", "qsl", + "qf", "fabric", "api"); + // 兼容测试版的文件名 没错这玩意测试版命名方式甚至与正式版不一样 + if (localComp is null) + localComp = ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.Instance, "quilted_fabric_api", + "quilted-fabric-api"); + if (localComp is null) + return null; + var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? "")); + if (result is not null) + { + _currentQsl = result; + _currentQslPath = localComp.Path; + } + + return result; + } + + private ModComp.CompFile _currentOptiFabric; + private string _currentOptiFabricPath; + + private object GetCurrentOptiFabric() + { + var loaderOutput = ModDownload.DlOptiFabricLoader.Output; + if (loaderOutput is null) + return null; + var localComp = + ModLocalComp.GetModLocalCompByKeywords(PageInstanceLeft.Instance, "optifabric", "optifabric", "opti"); + if (localComp is null) + return null; + var result = loaderOutput.FirstOrDefault(comp => (comp.Hash ?? "") == (localComp.ModrinthHash ?? "")); + if (result is not null) + { + _currentOptiFabric = result; + _currentOptiFabricPath = localComp.Path; + } + + return result; + } + + // 当前信息获取 + public void GetCurrentInfo() + { + ClearSelected(); + BtnSelectStart.IsEnabled = true; + var CurrentInstance = PageInstanceLeft.Instance.Info; + _vanillaName = CurrentInstance.VanillaName; + if (CurrentInstance.HasLiteLoader) + SelectedLiteLoader = new ModDownload.DlLiteLoaderListEntry { Inherit = CurrentInstance.VanillaName }; + if (CurrentInstance.HasOptiFine) + SelectedOptiFine = new ModDownload.DlOptiFineListEntry + { + DisplayName = CurrentInstance.VanillaName + " " + CurrentInstance.OptiFine.Replace("_", " "), + IsPreview = CurrentInstance.OptiFine.ContainsF("pre"), Inherit = CurrentInstance.VanillaName, + NameVersion = CurrentInstance.VanillaName + "-OptiFine_HD_U_" + CurrentInstance.OptiFine + }; + if (CurrentInstance.HasCleanroom) + { + SelectedAPIName = "Cleanroom"; + SelectedCleanroomVersion = CurrentInstance.Cleanroom; + } + else if (CurrentInstance.HasForge) + { + SelectedLoaderName = "Forge"; + SelectedForge = + new ModDownload.DlForgeVersionEntry(CurrentInstance.Forge, null, CurrentInstance.VanillaName) + { + Category = "installer", ForgeType = ModDownload.DlForgelikeEntry.ForgelikeType.Forge, + Inherit = CurrentInstance.VanillaName + }; + } + else if (CurrentInstance.HasLegacyFabric) + { + SelectedLoaderName = "LegacyFabric"; + SelectedLegacyFabric = CurrentInstance.LegacyFabric; + SelectedLegacyFabricApi = (ModComp.CompFile)GetCurrentLegacyFabricApi(); + } + else if (CurrentInstance.HasFabric) + { + SelectedLoaderName = "Fabric"; + SelectedFabric = CurrentInstance.Fabric; + SelectedFabricApi = (ModComp.CompFile)GetCurrentFabricApi(); + } + else if (CurrentInstance.HasLabyMod) + { + SelectedLoaderName = "LabyMod"; + SelectedLabyModVersion = CurrentInstance.LabyMod; + } + else if (CurrentInstance.HasNeoForge) + { + SelectedLoaderName = "NeoForge"; + SelectedNeoForgeVersion = CurrentInstance.NeoForge; + SelectedNeoForge = new ModDownload.DlNeoForgeListEntry(CurrentInstance.NeoForge) + { + VersionName = CurrentInstance.NeoForge, Inherit = CurrentInstance.VanillaName, + ForgeType = ModDownload.DlForgelikeEntry.ForgelikeType.NeoForge + }; + } + else if (CurrentInstance.HasQuilt) + { + SelectedLoaderName = "Quilt"; + SelectedQuilt = CurrentInstance.Quilt; + SelectedQSL = (ModComp.CompFile)GetCurrentQsl(); + SelectedFabricApi = (ModComp.CompFile)GetCurrentFabricApi(); + } + + if ((CurrentInstance.HasFabric || CurrentInstance.HasQuilt) && CurrentInstance.HasOptiFine) + SelectedOptiFabric = (ModComp.CompFile)GetCurrentOptiFabric(); + _vanillaIcon = "pack://application:,,,/images/Blocks/Grass.png"; // TODO: 需要判断 Icon + CurrentInfo = GetSelectInfo(); + EnterSelectPage(); + } + + private string CurrentInfo; + + #endregion + + #region 加载器 + + // 结果数据化 + private void LoadMinecraft_OnFinish() + { + ExitSelectPage(); // 返回 + do + { + try + { + var Dict = new Dictionary> + { + { "正式版", new List() }, { "预览版", new List() }, { "远古版", new List() }, + { "愚人节版", new List() } + }; + var Versions = (JArray)ModDownload.DlClientListLoader.Output.Value["versions"]; + foreach (JObject Version in Versions) + { + // 确定分类 + var Type = Version["type"].ToString(); + var versionId = Version["id"].ToString().ToLower(); + switch (Type ?? "") + { + case "release": + { + Type = "正式版"; + break; + } + case "snapshot": + case "pending": + { + Type = "预览版"; + // Mojang 误分类 + if (versionId.StartsWith("1.") && !versionId.Contains("combat") && + !versionId.Contains("rc") && !versionId.Contains("experimental") && + !versionId.Equals("1.2") && !versionId.Contains("pre")) + { + Type = "正式版"; + Version["type"] = "release"; + } + + // 愚人节版本 + switch (Version["id"].ToString().ToLower() ?? "") + { + case "2point0_blue": + case "2point0_red": + case "2point0_purple": + case "2.0_blue": + case "2.0_red": + case "2.0_purple": + case "2.0": + { + Type = "愚人节版"; + Version["id"] = Version["id"].ToString().Replace("point", "."); + Version["type"] = "special"; + Version.Add("lore", ModMinecraft.GetMcFoolName((string)Version["id"])); + break; + } + case "20w14infinite": + case "20w14∞": + { + Type = "愚人节版"; + Version["id"] = "20w14∞"; + Version["type"] = "special"; + Version.Add("lore", ModMinecraft.GetMcFoolName((string)Version["id"])); + break; + } + case "3d shareware v1.34": + case "1.rv-pre1": + case "15w14a": + case var @case when @case == "2.0": + case "22w13oneblockatatime": + case "23w13a_or_b": + case "24w14potato": + case "25w14craftmine": + { + Type = "愚人节版"; + Version["type"] = "special"; + Version.Add("lore", + ModMinecraft.GetMcFoolName((string)Version["id"])); // 4/1 自动视作愚人节版 + break; + } + + default: + { + var ReleaseDate = Version["releaseTime"].Value().ToUniversalTime() + .AddHours(2d); + if (ReleaseDate.Month == 4 && ReleaseDate.Day == 1) + { + Type = "愚人节版"; + Version["type"] = "special"; + } + + break; + } + } + + break; + } + case "special": + { + // 已被处理的愚人节版 + Type = "愚人节版"; + break; + } + + default: + { + Type = "远古版"; + break; + } + } + + // 加入辞典 + Dict[Type].Add(Version); + } + + // 排序 + foreach (var Pair in Dict.ToList()) + Dict[Pair.Key] = Pair.Value.OrderByDescending(j => j["releaseTime"].Value()).ToList(); + // 清空当前 + PanMinecraft.Children.Clear(); + // 添加最新版本 + var CardInfo = new MyCard { Title = "最新版本", Margin = new Thickness(0d, 15d, 0d, 15d) }; + var TopestVersions = new List(); + var Release = (JObject)Dict["正式版"][0].DeepClone(); + Release["lore"] = "最新正式版,发布于 " + + Release["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm"); + TopestVersions.Add(Release); + if (Dict["正式版"][0]["releaseTime"].Value() < Dict["预览版"][0]["releaseTime"].Value()) + { + var Snapshot = (JObject)Dict["预览版"][0].DeepClone(); + Snapshot["lore"] = "最新预览版,发布于 " + + Snapshot["releaseTime"].Value().ToString("yyyy'/'MM'/'dd HH':'mm"); + TopestVersions.Add(Snapshot); + } + + var PanInfo = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = TopestVersions + }; + + void StackInstall(StackPanel Stack) + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.McDownloadListItem((JObject)item, + (sender, e) => MinecraftSelected((MyListItem)sender, e), false)); + } + + ; + MyCard.StackInstall(ref PanInfo, StackInstall); + CardInfo.Children.Add(PanInfo); + PanMinecraft.Children.Insert(0, CardInfo); + // 添加其他版本 + foreach (var Pair in Dict) + { + if (!Pair.Value.Any()) + continue; + // 增加卡片 + var NewCard = new MyCard + { Title = Pair.Key + " (" + Pair.Value.Count + ")", Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = Pair.Value + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + // 不能使用 AddressOf,这导致了 #535,原因完全不明,疑似是编译器 Bug + NewCard.InstallMethod = StackInstall; + NewCard.IsSwapped = true; + PanMinecraft.Children.Add(NewCard); + } + + // 自动选择版本 + if (McVersionWaitingForSelect is null) + break; + ModBase.Log("[Download] 自动选择 MC 版本:" + McVersionWaitingForSelect); + foreach (JObject Version in Versions) + { + if ((Version["id"].ToString() ?? "") != (McVersionWaitingForSelect ?? "")) + continue; + var Item = ModDownloadLib.McDownloadListItem(Version, (_, _) => { }, false); + MinecraftSelected(Item, null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化安装版本列表出错", ModBase.LogLevel.Feedback); + } + } while (false); + } + + /// + /// 当 MC 版本列表加载完时,立即自动选择的版本。用于外部调用。 + /// + public static string McVersionWaitingForSelect = null; + + #endregion + + #region OptiFine 列表 + + /// + /// 获取 OptiFine 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadOptiFineGetError() + { + if (SelectedLoaderName == "NeoForge" || SelectedLoaderName == "Quilt" || SelectedLoaderName == "LabyMod") + return $"与 {SelectedLoaderName} 不兼容"; + if (LoadOptiFine is null || LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadOptiFine.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadOptiFine.State).Error.Message)); + // 是否有 Cleanroom + if (SelectedCleanroom is not null) + return "与 Cleanroom 不兼容"; + // 检查 Forge 1.13 - 1.14.3:全部不兼容 + if (SelectedLoaderName == "Forge" && ModMinecraft.CompareVersion(_vanillaName, "1.13") >= 0 && + ModMinecraft.CompareVersion("1.14.3", _vanillaName) >= 0) return "与 Forge 不兼容"; + // 检查 Fabric 1.20.5+: 全部不兼容 + if (SelectedFabric is not null && ModMinecraft.CompareVersion(_vanillaName, "1.20.4") > 0) + return "与 Fabric 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadOptiFine) is not null) + return GetLoaderError(LoadOptiFine); + // 检查 Forge 版本 + var HasAny = false; + var HasRequiredVersion = false; + foreach (var OptiFineVersion in ModDownload.DlOptiFineListLoader.Output.Value) + { + if (!OptiFineVersion.DisplayName.StartsWith(_vanillaName + " ")) + continue; // 不是同一个大版本 + HasAny = true; + if (SelectedForge is null) + return null; // 未选择 Forge + if (Conversions.ToBoolean(IsOptiFineSuitForForge(OptiFineVersion, SelectedForge))) + return null; // 该版本可用 + if (OptiFineVersion.RequiredForgeVersion is not null) + HasRequiredVersion = true; + } + + if (!HasAny) return "无可用版本"; + + if (HasRequiredVersion) return "仅兼容特定版本的 Forge"; + + return "与 Forge 不兼容"; + } + + // 检查某个 OptiFine 是否与某个 Forge 兼容 + private object IsOptiFineSuitForForge(ModDownload.DlOptiFineListEntry OptiFine, + ModDownload.DlForgeVersionEntry Forge) + { + if ((Forge.Inherit ?? "") != (OptiFine.Inherit ?? "")) + return false; // 不是同一个大版本 + if (OptiFine.RequiredForgeVersion is null) + return false; // 不兼容 Forge + if (string.IsNullOrWhiteSpace(OptiFine.RequiredForgeVersion)) + return true; // #4183 + if (OptiFine.RequiredForgeVersion.Contains(".")) // XX.X.XXX + return ModMinecraft.CompareVersion(Forge.Version.ToString(), OptiFine.RequiredForgeVersion) == 0; + + // XXXX + return Forge.Version.Revision == Conversions.ToDouble(OptiFine.RequiredForgeVersion); + } + + // 限制展开 + private void CardOptiFine_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadOptiFineGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 OptiFine 版本列表。 + /// + private void OptiFine_Loaded() + { + try + { + if (ModDownload.DlOptiFineListLoader.State != ModBase.LoadState.Finished) + return; + + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlOptiFineListLoader.Output.Value) + { + if (Conversions.ToBoolean(SelectedForge is not null && + !(bool)IsOptiFineSuitForForge(Version, SelectedForge))) + continue; + if (Version.DisplayName.StartsWith(_vanillaName + " ")) + Versions.Add(Version); + } + + if (!Versions.Any()) + return; + // 排序 + Versions.Sort((Left, Right) => + { + if (!Left.IsPreview && Right.IsPreview) + return true; + if (Left.IsPreview && !Right.IsPreview) + return false; + return Conversions.ToBoolean(ModMinecraft.CompareVersion(Left.DisplayName, Right.DisplayName)); + }); + // 可视化 + PanOptiFine.Children.Clear(); + foreach (var Version in Versions) + PanOptiFine.Children.Add( + ModDownloadLib.OptiFineDownloadListItem(Version, (a, b) => + this.OptiFine_Selected((dynamic)a, b), false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 OptiFine 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void OptiFine_Selected(MyListItem sender, EventArgs e) + { + SelectedOptiFine = (ModDownload.DlOptiFineListEntry)sender.Tag; + if (Conversions.ToBoolean(SelectedForge is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, SelectedForge))) + SelectedForge = null; + OptiFabric_Loaded(); + Forge_Loaded(); + NeoForge_Loaded(); + CardOptiFine.IsSwapped = true; + ReloadSelected(); + } + + private void OptiFine_Clear(object sender, MouseButtonEventArgs e) + { + SelectedOptiFine = null; + SelectedOptiFabric = null; + AutoSelectedOptiFabric = false; + CardOptiFine.IsSwapped = true; + e.Handled = true; + Forge_Loaded(); + NeoForge_Loaded(); + ReloadSelected(); + } + + #endregion + + #region LiteLoader 列表 + + /// + /// 获取 LiteLoader 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLiteLoaderGetError() + { + // 检查 Loader + if (GetLoaderError(LoadLiteLoader) is not null) + return GetLoaderError(LoadLiteLoader); + // 检查版本 + return ModDownload.DlLiteLoaderListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardLiteLoader_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLiteLoaderGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LiteLoader 版本列表。 + /// + private void LiteLoader_Loaded() + { + try + { + if (ModDownload.DlLiteLoaderListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlLiteLoaderListLoader.Output.Value) + if ((Version.Inherit ?? "") == (_vanillaName ?? "")) + Versions.Add(Version); + if (!Versions.Any()) + return; + // 可视化 + PanLiteLoader.Children.Clear(); + foreach (var Version in Versions) + PanLiteLoader.Children.Add(ModDownloadLib.LiteLoaderDownloadListItem(Version, + (a, b) => this.LiteLoader_Selected((dynamic)a, b), false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LiteLoader 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void LiteLoader_Selected(MyListItem sender, EventArgs e) + { + SelectedLiteLoader = (ModDownload.DlLiteLoaderListEntry)sender.Tag; + CardLiteLoader.IsSwapped = true; + ReloadSelected(); + } + + private void LiteLoader_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLiteLoader = null; + CardLiteLoader.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Forge 列表 + + /// + /// 获取 Forge 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadForgeGetError() + { + if (ModMinecraft.CompareVersionGe("1.5.1", _vanillaName) && ModMinecraft.CompareVersionGe(_vanillaName, "1.1")) + return "无可用版本"; + // 检查 Loader + if (GetLoaderError(LoadForge) is not null) + return GetLoaderError(LoadForge); + var loader = (ModLoader.LoaderTask>)LoadForge.State; + if ((_vanillaName ?? "") != (loader.Input ?? "")) + return "获取中……"; + // 检查版本 + foreach (var Version in loader.Output) + { + if (Version.Category == "universal" || Version.Category == "client") + continue; // 跳过无法自动安装的版本 + if (SelectedNeoForge is not null) + return "与 NeoForge 不兼容"; + if (SelectedFabric is not null) + return "与 Fabric 不兼容"; + if (SelectedOptiFine is not null && ModMinecraft.CompareVersionGe(_vanillaName, "1.13") && + ModMinecraft.CompareVersionGe("1.14.3", _vanillaName)) + return "与 OptiFine 不兼容"; // 1.13 ~ 1.14.3 OptiFine 检查 + if (Conversions.ToBoolean( + SelectedOptiFine is not null && !(bool)IsOptiFineSuitForForge(SelectedOptiFine, Version))) + continue; + return null; + } + + return "与 OptiFine 不兼容"; + } + + // 限制展开 + private void CardForge_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadForgeGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Forge 版本列表。 + /// + private void Forge_Loaded() + { + try + { + if (!LoadForge.State.IsLoader) + return; + var loader = (ModLoader.LoaderTask>)LoadForge.State; + if ((_vanillaName ?? "") != (loader.Input ?? "")) + return; + if (loader.State != ModBase.LoadState.Finished) + return; + // 获取要显示的版本 + var versions = loader.Output.ToList(); // 复制数组,以免 Output 在实例化后变空 + if (!loader.Output.Any()) + return; + PanForge.Children.Clear(); + versions = versions.Where(v => + { + if (v.Category == "universal" || v.Category == "client") + return false; // 跳过无法自动安装的版本 + if (Conversions.ToBoolean(SelectedOptiFine is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, v))) + return false; + return true; + }).OrderByDescending(v => v).ToList(); + ModDownloadLib.ForgeDownloadListItemPreload(PanForge, versions, + (a, b) => this.Forge_Selected((dynamic)a, b), false); + foreach (var Version in versions) + PanForge.Children.Add( + ModDownloadLib.ForgeDownloadListItem(Version, (a, b) => this.Forge_Selected((dynamic)a, b), false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Forge 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void Forge_Selected(MyListItem sender, EventArgs e) + { + SelectedForge = (ModDownload.DlForgeVersionEntry)sender.Tag; + SelectedLoaderName = "Forge"; + CardForge.IsSwapped = true; + if (Conversions.ToBoolean(SelectedOptiFine is not null && + !(bool)IsOptiFineSuitForForge(SelectedOptiFine, SelectedForge))) + SelectedOptiFine = null; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void Forge_Clear(object sender, MouseButtonEventArgs e) + { + SelectedForge = null; + SelectedLoaderName = null; + CardForge.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region NeoForge 列表 + + /// + /// 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadNeoForgeGetError() + { + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "NeoForge")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadNeoForge) is not null) + return GetLoaderError(LoadNeoForge); + // 检查版本 + return ModDownload.DlNeoForgeListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardNeoForge_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadNeoForgeGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 NeoForge 版本列表。 + /// + private void NeoForge_Loaded() + { + try + { + // 获取版本列表 + if (ModDownload.DlNeoForgeListLoader.State != ModBase.LoadState.Finished) + return; + var Versions = ModDownload.DlNeoForgeListLoader.Output.Value + .Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList(); + if (!Versions.Any()) + return; + // 可视化 + PanNeoForge.Children.Clear(); + ModDownloadLib.NeoForgeDownloadListItemPreload(PanNeoForge, Versions, + (a, b) => this.NeoForge_Selected((dynamic)a, b), + false); + foreach (var Version in Versions) + PanNeoForge.Children.Add( + ModDownloadLib.NeoForgeDownloadListItem(Version, (a, b) => this.NeoForge_Selected((dynamic)a, b), + false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 NeoForge 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void NeoForge_Selected(MyListItem sender, EventArgs e) + { + SelectedNeoForge = (ModDownload.DlNeoForgeListEntry)sender.Tag; + SelectedLoaderName = "NeoForge"; + CardNeoForge.IsSwapped = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void NeoForge_Clear(object sender, MouseButtonEventArgs e) + { + SelectedNeoForge = null; + SelectedLoaderName = null; + CardNeoForge.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region Cleanroom 列表 + + /// + /// 获取 Cleanroom 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadCleanroomGetError() + { + if (!_vanillaName.StartsWith("1.")) + return "没有可用版本"; + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Cleanroom")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadNeoForge) is not null) + return GetLoaderError(LoadNeoForge); + // 检查版本 + return ModDownload.DlNeoForgeListLoader.Output.Value.Any(v => (v.Inherit ?? "") == (_vanillaName ?? "")) + ? null + : "无可用版本"; + } + + // 限制展开 + private void CardCleanroom_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadCleanroomGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Cleanroom 版本列表。 + /// + private void Cleanroom_Loaded() + { + try + { + // 获取版本列表 + if (ModDownload.DlCleanroomListLoader.State != ModBase.LoadState.Finished) + return; + var Versions = ModDownload.DlCleanroomListLoader.Output.Value + .Where(v => (v.Inherit ?? "") == (_vanillaName ?? "")).ToList(); + if (!Versions.Any()) + return; + // 可视化 + PanCleanroom.Children.Clear(); + ModDownloadLib.CleanroomDownloadListItemPreload(PanCleanroom, Versions, + (a, b) => this.Cleanroom_Selected((dynamic)a, b), false); + foreach (var Version in Versions) + PanCleanroom.Children.Add( + ModDownloadLib.CleanroomDownloadListItem(Version, (a, b) => this.Cleanroom_Selected((dynamic)a, b), + false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Cleanroom 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void Cleanroom_Selected(MyListItem sender, EventArgs e) + { + SelectedCleanroom = (ModDownload.DlCleanroomListEntry)sender.Tag; + SelectedLoaderName = "Cleanroom"; + CardCleanroom.IsSwapped = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + private void Cleanroom_Clear(object sender, MouseButtonEventArgs e) + { + SelectedCleanroom = null; + SelectedLoaderName = null; + CardCleanroom.IsSwapped = true; + e.Handled = true; + OptiFine_Loaded(); + ReloadSelected(); + } + + #endregion + + #region Fabric 列表 + + /// + /// 获取 Fabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadFabricGetError() + { + // 检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容 + if (SelectedOptiFine is not null && ModMinecraft.CompareVersionGe(_vanillaName, "1.20.5")) + return "与 OptiFine 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadFabric) is not null) + return GetLoaderError(LoadFabric); + // 检查版本 + foreach (JObject version in ModDownload.DlFabricListLoader.Output.Value["game"]) + if ((version["version"].ToString() ?? "") == + (_vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") ?? "")) + { + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Fabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Fabric 版本列表。 + /// + private void Fabric_Loaded() + { + try + { + if (ModDownload.DlFabricListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var versions = (JArray)ModDownload.DlFabricListLoader.Output.Value["loader"]; + if (!versions.Any()) + return; + // 可视化 + PanFabric.Children.Clear(); + PanFabric.Tag = versions; + CardFabric.SwapControl = PanFabric; + CardFabric.InstallMethod = stack => + { + foreach (var item in (IEnumerable)stack.Tag) + stack.Children.Add( + ModDownloadLib.FabricDownloadListItem((JObject)item, + (a, b) => this.Fabric_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Fabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void Fabric_Selected(MyListItem sender, EventArgs e) + { + SelectedFabric = ((dynamic)sender.Tag)["version"].ToString(); + SelectedLoaderName = "Fabric"; + FabricApi_Loaded(); + OptiFabric_Loaded(); + CardFabric.IsSwapped = true; + ReloadSelected(); + } + + private void Fabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedFabric = null; + SelectedFabricApi = null; + AutoSelectedFabricApi = false; + SelectedOptiFabric = null; + AutoSelectedOptiFabric = false; + SelectedLoaderName = null; + SelectedAPIName = null; + CardFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Fabric API 列表 + + /// + /// 判断某 Fabric API 是否适配当前选择的原版版本。 + /// + public bool IsFabricApiCompatible(ModComp.CompFile fabricApi) + { + var fabricApiName = fabricApi.DisplayName; + try + { + if (fabricApiName is null || _vanillaName is null) + return false; + fabricApiName = fabricApiName.ToLower(); + _vanillaName = _vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").ToLower(); + if (fabricApiName.StartsWith("[" + _vanillaName + "]")) + return true; + if (!fabricApiName.Contains("/") || !fabricApiName.Contains("]")) + return false; + // 直接的判断(例如 1.18.1/22w03a) + foreach (var part in fabricApiName.BeforeFirst("]").TrimStart('[').Split("/")) + if ((part ?? "") == (_vanillaName ?? "")) + return true; + // 将版本名分割语素(例如 1.16.4/5) + var lefts = fabricApiName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+"); + var rights = _vanillaName.BeforeFirst("]").RegexSearch("[a-z/]+|[0-9/]+"); + // 对每段进行判断 + var i = 0; + while (true) + { + // 两边均缺失,感觉是一个东西 + if (lefts.Count - 1 < i && rights.Count - 1 < i) + return true; + // 确定两边是否一致 + var leftValue = lefts.Count - 1 < i ? "-1" : lefts[i]; + var rightValue = rights.Count - 1 < i ? "-1" : rights[i]; + if (!leftValue.Contains("/")) + { + if ((leftValue ?? "") != (rightValue ?? "")) + return false; + } + // 左边存在斜杠 + else if (!leftValue.Contains(rightValue)) + { + return false; + } + + i += 1; + } + + return true; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 Fabric API 版本适配性出错(" + fabricApiName + ", " + _vanillaName + ")"); + return false; + } + } + + /// + /// 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadFabricApiGetError() + { + // 检查 Loader + if (GetLoaderError(LoadFabricApi) is not null) + return GetLoaderError(LoadFabricApi); + if (ModDownload.DlFabricApiLoader.Output is null) + return SelectedFabric is null ? "需要安装 Fabric" : "获取中……"; + // 检查版本 + if (ModDownload.DlFabricApiLoader.Output.Any(f => IsFabricApiCompatible(f))) + return SelectedFabric is null ? "需要安装 Fabric" : null; + + return "无可用版本"; + } + + // 限制展开 + private void CardFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadFabricApiGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedFabricApi; + + /// + /// 尝试重新可视化 FabricApi 版本列表。 + /// + private void FabricApi_Loaded() + { + try + { + if (ModDownload.DlFabricApiLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || (SelectedFabric is null && SelectedQuilt is null)) + return; + // 获取版本列表 + var versions = new List(); + foreach (var version in ModDownload.DlFabricApiLoader.Output) + if (IsFabricApiCompatible(version)) + { + if (!version.DisplayName.StartsWith("[")) + { + ModBase.Log("[Download] 已特判修改 Fabric API 显示名:" + version.DisplayName, ModBase.LogLevel.Debug); + version.DisplayName = "[" + _vanillaName + "] " + version.DisplayName; + } + + versions.Add(version); + } + + if (!versions.Any()) + return; + versions = versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanFabricApi.Children.Clear(); + foreach (var version in versions) + { + if (!IsFabricApiCompatible(version)) + continue; + PanFabricApi.Children.Add( + ModDownloadLib.FabricApiDownloadListItem(version, + (a, b) => this.FabricApi_Selected((dynamic)a, b))); + } + + // 自动选择 Fabric API + if ((!AutoSelectedFabricApi && SelectedQuilt is null) || + (SelectedQuilt is not null && ReferenceEquals(LoadQSLGetError(), "没有可用版本"))) + { + AutoSelectedFabricApi = true; + ModBase.Log($"[Download] 已自动选择 Fabric API:{((MyListItem)PanFabricApi.Children[0]).Title}"); + FabricApi_Selected((MyListItem)PanFabricApi.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Fabric API 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void FabricApi_Selected(MyListItem sender, EventArgs e) + { + SelectedFabricApi = (ModComp.CompFile)sender.Tag; + SelectedAPIName = "Fabric API"; + CardFabricApi.IsSwapped = true; + ReloadSelected(); + } + + private void FabricApi_Clear(object sender, MouseButtonEventArgs e) + { + SelectedFabricApi = null; + SelectedAPIName = null; + CardFabricApi.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region LegacyFabric 列表 + + /// + /// 获取 LegacyFabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLegacyFabricGetError() + { + if (LoadLegacyFabric is null || LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLegacyFabric.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLegacyFabric.State).Error.Message)); + foreach (JObject Version in ModDownload.DlLegacyFabricListLoader.Output.Value["game"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + if (SelectedLiteLoader is not null) + return "与 LiteLoader 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "LegacyFabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardLegacyFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLegacyFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LegacyFabric 版本列表。 + /// + private void LegacyFabric_Loaded() + { + try + { + if (ModDownload.DlLegacyFabricListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = (JArray)ModDownload.DlLegacyFabricListLoader.Output.Value["loader"]; + if (!Versions.Any()) + return; + // 可视化 + PanLegacyFabric.Children.Clear(); + PanLegacyFabric.Tag = Versions; + CardLegacyFabric.SwapControl = PanLegacyFabric; + CardLegacyFabric.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(ModDownloadLib.LegacyFabricDownloadListItem((JObject)item, + (a, b) => this.LegacyFabric_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LegacyFabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void LegacyFabric_Selected(MyListItem sender, EventArgs e) + { + SelectedLegacyFabric = ((dynamic)sender.Tag)("version").ToString(); + SelectedLoaderName = "LegacyFabric"; + LegacyFabricApi_Loaded(); + CardLegacyFabric.IsSwapped = true; + ReloadSelected(); + } + + private void LegacyFabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLegacyFabric = null; + SelectedLegacyFabricApi = null; + AutoSelectedLegacyFabricApi = false; + SelectedLoaderName = null; + SelectedAPIName = null; + CardLegacyFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Legacy Fabric API 列表 + + /// + /// 从显示名判断该 API 是否与某版本适配。 + /// + public static bool IsSuitableLegacyFabricApi(List SupportVersions, string MinecraftVersion) + { + try + { + if (SupportVersions.Contains(MinecraftVersion)) return true; + + return false; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 Legacy Fabric API 版本适配性出错(" + SupportVersions + ", " + MinecraftVersion + ")"); + return false; + } + } + + /// + /// 获取 LegacyFabricApi 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLegacyFabricApiGetError() + { + if (LoadLegacyFabricApi is null || LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLegacyFabricApi.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLegacyFabricApi.State).Error.Message)); + if (SelectedAPIName is not null && !ReferenceEquals(SelectedAPIName, "Legacy Fabric API")) + return $"与 {SelectedAPIName} 不兼容"; + if (ModDownload.DlLegacyFabricApiLoader.Output is null) + { + if (SelectedLegacyFabric is null) + return "需要安装 LegacyFabric"; + return "加载中……"; + } + + foreach (var Version in ModDownload.DlLegacyFabricApiLoader.Output) + { + if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + continue; + if (SelectedLegacyFabric is null) + return "需要安装 LegacyFabric"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardLegacyFabricApi_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLegacyFabricApiGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedLegacyFabricApi; + + /// + /// 尝试重新可视化 LegacyFabricApi 版本列表。 + /// + private void LegacyFabricApi_Loaded() + { + try + { + if (ModDownload.DlLegacyFabricApiLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || (SelectedLegacyFabric is null && SelectedQuilt is null)) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlLegacyFabricApiLoader.Output) + if (IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + Versions.Add(Version); + + if (!Versions.Any()) + return; + Versions = Versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanLegacyFabricApi.Children.Clear(); + foreach (var Version in Versions) + { + if (!IsSuitableLegacyFabricApi(Version.GameVersions, _vanillaName)) + continue; + PanLegacyFabricApi.Children.Add( + ModDownloadLib.LegacyFabricApiDownloadListItem(Version, + (a, b) => this.LegacyFabricApi_Selected((dynamic)a, b))); + } + + // 自动选择 Legacy Fabric API + if ((!AutoSelectedLegacyFabricApi && SelectedQuilt is null) || + (SelectedQuilt is not null && ReferenceEquals(LoadQSLGetError(), "没有可用版本"))) + { + AutoSelectedLegacyFabricApi = true; + ModBase.Log($"[Download] 已自动选择 Legacy Fabric API:{((MyListItem)PanLegacyFabricApi.Children[0]).Title}"); + LegacyFabricApi_Selected((MyListItem)PanLegacyFabricApi.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Legacy Fabric API 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void LegacyFabricApi_Selected(MyListItem sender, EventArgs e) + { + SelectedLegacyFabricApi = (ModComp.CompFile)sender.Tag; + SelectedAPIName = "Legacy Fabric API"; + CardLegacyFabricApi.IsSwapped = true; + ReloadSelected(); + } + + private void LegacyFabricApi_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLegacyFabricApi = null; + SelectedAPIName = null; + CardLegacyFabricApi.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region Quilt 列表 + + /// + /// 获取 Quilt 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadQuiltGetError() + { + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Quilt")) + return $"与 {SelectedLoaderName} 不兼容"; + // 检查 Loader + if (GetLoaderError(LoadQuilt) is not null) + return GetLoaderError(LoadQuilt); + // 检查版本 + foreach (JObject version in ModDownload.DlFabricListLoader.Output.Value["game"]) + if ((version["version"].ToString() ?? "") == + (_vanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") ?? "")) + { + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "Fabric")) + return $"与 {SelectedLoaderName} 不兼容"; + return null; + } + + return "无可用版本"; + } + + // 限制展开 + private void CardQuilt_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadQuiltGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 Quilt 版本列表。 + /// + private void Quilt_Loaded() + { + try + { + if (ModDownload.DlQuiltListLoader.State != ModBase.LoadState.Finished) + return; + // 获取版本列表 + var Versions = (JArray)ModDownload.DlQuiltListLoader.Output.Value["loader"]; + if (!Versions.Any()) + return; + // 可视化 + PanQuilt.Children.Clear(); + PanQuilt.Tag = Versions; + CardQuilt.SwapControl = PanQuilt; + CardQuilt.InstallMethod = Stack => + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add( + ModDownloadLib.QuiltDownloadListItem((JObject)item, + (a, b) => this.Quilt_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 Quilt 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void Quilt_Selected(MyListItem sender, EventArgs e) + { + SelectedQuilt = ((dynamic)sender.Tag)["version"].ToString(); + SelectedLoaderName = "Quilt"; + FabricApi_Loaded(); + QSL_Loaded(); + CardQuilt.IsSwapped = true; + ReloadSelected(); + } + + private void Quilt_Clear(object sender, MouseButtonEventArgs e) + { + SelectedQuilt = null; + SelectedQSL = null; + SelectedFabricApi = null; + SelectedLoaderName = null; + SelectedAPIName = null; + CardQuilt.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region QSL 列表 + + /// + /// 从显示名判断该 API 是否与某版本适配。 + /// + public static bool IsSuitableQSL(List SupportVersions, string MinecraftVersion) + { + try + { + if (SupportVersions.Contains(MinecraftVersion)) return true; + + return false; + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 QSL 版本适配性出错(" + SupportVersions + ", " + MinecraftVersion + ")"); + return false; + } + } + + /// + /// 获取 QSL 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadQSLGetError() + { + if (LoadQSL is null || LoadQSL.State.LoadingState == MyLoading.MyLoadingState.Run) + return "正在获取版本列表……"; + if (LoadQSL.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadQSL.State).Error.Message)); + if (SelectedAPIName is not null && !ReferenceEquals(SelectedAPIName, "QFAPI / QSL")) + return $"与 {SelectedAPIName} 不兼容"; + if (ModDownload.DlQSLLoader.Output is null) + { + if (SelectedQuilt is null) + return "需要安装 Quilt"; + return "正在获取版本列表……"; + } + + foreach (var Version in ModDownload.DlQSLLoader.Output) + { + if (!IsSuitableQSL(Version.GameVersions, _vanillaName)) + continue; + if (SelectedQuilt is null) + return "需要安装 Quilt"; + return null; + } + + return "没有可用版本"; + } + + // 限制展开 + private void CardQSL_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadQSLGetError() is not null) + e.Handled = true; + } + + private bool AutoSelectedQSL; + + /// + /// 尝试重新可视化 QSL 版本列表。 + /// + private void QSL_Loaded() + { + try + { + if (ModDownload.DlQSLLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || SelectedQuilt is null) + return; + // 获取版本列表 + var Versions = new List(); + foreach (var Version in ModDownload.DlQSLLoader.Output) + if (IsSuitableQSL(Version.GameVersions, _vanillaName)) + { + if (!Version.DisplayName.StartsWith("[")) + { + ModBase.Log("[Download] 已特判修改 QSL 显示名:" + Version.DisplayName, ModBase.LogLevel.Debug); + Version.DisplayName = "[" + _vanillaName + "] " + Version.DisplayName; + } + + Versions.Add(Version); + } + + if (!Versions.Any()) + return; + Versions = Versions.Sort((a, b) => a.ReleaseDate > b.ReleaseDate); + // 可视化 + PanQSL.Children.Clear(); + foreach (var Version in Versions) + { + if (!IsSuitableQSL(Version.GameVersions, _vanillaName)) + continue; + PanQSL.Children.Add( + ModDownloadLib.QSLDownloadListItem(Version, (a, b) => this.QSL_Selected((dynamic)a, b))); + } + + // 自动选择 QSL + if (!AutoSelectedQSL) + { + AutoSelectedQSL = true; + ModBase.Log($"[Download] 已自动选择 QSL:{((MyListItem)PanQSL.Children[0]).Title}"); + QSL_Selected((MyListItem)PanQSL.Children[0], null); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 QSL 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void QSL_Selected(MyListItem sender, EventArgs e) + { + SelectedQSL = (ModComp.CompFile)sender.Tag; + SelectedAPIName = "QFAPI / QSL"; + CardQSL.IsSwapped = true; + ReloadSelected(); + } + + private void QSL_Clear(object sender, MouseButtonEventArgs e) + { + SelectedQSL = null; + SelectedAPIName = null; + CardQSL.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region OptiFabric 列表 + + /// + /// 判断某 OptiFabric 是否适配当前选择的原版版本。 + /// + private bool IsOptiFabricCompatible(ModComp.CompFile modFile) + { + try + { + if (_vanillaName is null) + return false; + return modFile.GameVersions.Contains(_vanillaName); + } + catch (Exception ex) + { + ModBase.Log(ex, "判断 OptiFabric 版本适配性出错(" + _vanillaName + ")"); + return false; + } + } + + private bool AutoSelectedOptiFabric; + + /// + /// 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadOptiFabricGetError() + { + if (VanillaDrop >= 140 && VanillaDrop <= 150) + return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins"; + // 检查 Loader + if (GetLoaderError(LoadOptiFabric) is not null) + return GetLoaderError(LoadOptiFabric); + // 检查版本 + if (ModDownload.DlOptiFabricLoader.Output is null) + { + if (SelectedFabric is null && SelectedOptiFine is null) + return "需要安装 OptiFine 与 Fabric"; + if (SelectedFabric is null) + return "需要安装 Fabric"; + if (SelectedOptiFine is null) + return "需要安装 OptiFine"; + return "获取中……"; + } + + foreach (var version in ModDownload.DlOptiFabricLoader.Output) + { + if (!IsOptiFabricCompatible(version)) + continue; // 2135# + if (SelectedFabric is null && SelectedOptiFine is null) + return "需要安装 OptiFine 与 Fabric"; + if (SelectedFabric is null) + return "需要安装 Fabric"; + if (SelectedOptiFine is null) + return "需要安装 OptiFine"; + return null; // 通过检查 + } + + return "无可用版本"; + } + + // 限制展开 + private void CardOptiFabric_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadOptiFabricGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 OptiFabric 版本列表。 + /// + private void OptiFabric_Loaded() + { + try + { + if (ModDownload.DlOptiFabricLoader.State != ModBase.LoadState.Finished) + return; + if (_vanillaName is null || SelectedFabric is null || SelectedOptiFine is null) + return; + // 获取版本列表 + var versions = new List(); + foreach (var Version in ModDownload.DlOptiFabricLoader.Output) + if (IsOptiFabricCompatible(Version)) + versions.Add(Version); + if (!versions.Any()) + return; + // 排序 + versions = versions.OrderByDescending(v => v.ReleaseDate).ToList(); + // 可视化 + PanOptiFabric.Children.Clear(); + foreach (var Version in versions) + { + if (!IsOptiFabricCompatible(Version)) + continue; + PanOptiFabric.Children.Add( + ModDownloadLib.OptiFabricDownloadListItem(Version, + (a, b) => this.OptiFabric_Selected((dynamic)a, b))); + } + + // 自动选择 OptiFabric + if (AutoSelectedOptiFabric || (VanillaDrop >= 140 && VanillaDrop <= 150)) + return; // 1.14~15 不自动选择 + AutoSelectedOptiFabric = true; + ModBase.Log($"[Download] 已自动选择 OptiFabric:{((MyListItem)PanOptiFabric.Children[0]).Title}"); + OptiFabric_Selected((MyListItem)PanOptiFabric.Children[0], null); + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 OptiFabric 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + private void OptiFabric_Selected(MyListItem sender, EventArgs e) + { + SelectedOptiFabric = (ModComp.CompFile)sender.Tag; + CardOptiFabric.IsSwapped = true; + ReloadSelected(); + } + + private void OptiFabric_Clear(object sender, MouseButtonEventArgs e) + { + SelectedOptiFabric = null; + CardOptiFabric.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion + + #region LabyMod 列表 + + /// + /// 获取 LabyMod 的加载异常信息。若正常则返回 Nothing。 + /// + private string LoadLabyModGetError() + { + if (LoadLabyMod is null || LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run) + return "加载中……"; + if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Error) + return Conversions.ToString(Operators.ConcatenateObject("获取版本列表失败:", + ((dynamic)LoadLabyMod.State).Error.Message)); + // 检查 Loader + if (GetLoaderError(LoadLabyMod) is not null) + return GetLoaderError(LoadLabyMod); + if (SelectedOptiFine is not null) + return "与 OptiFine 不兼容"; + if (SelectedLoaderName is not null && !ReferenceEquals(SelectedLoaderName, "LabyMod")) + return $"与 {SelectedLoaderName} 不兼容"; + foreach (JObject Version in ModDownload.DlLabyModListLoader.Output.Value["production"]["minecraftVersions"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + return null; + foreach (JObject Version in ModDownload.DlLabyModListLoader.Output.Value["snapshot"]["minecraftVersions"]) + if ((Version["version"].ToString() ?? "") == (_vanillaName ?? "")) + return null; + return "无可用版本"; + } + + // 限制展开 + private void CardLabyMod_PreviewSwap(object sender, ModBase.RouteEventArgs e) + { + if (LoadLabyModGetError() is not null) + e.Handled = true; + } + + /// + /// 尝试重新可视化 LabyMod 版本列表。 + /// + private void LabyMod_Loaded() + { + try + { + if (LoadLabyMod.State.LoadingState == MyLoading.MyLoadingState.Run) + return; + // 获取版本列表 + var Versions = ModDownload.DlLabyModListLoader.Output.Value; + if (Versions is null || Versions["production"] is null || Versions["snapshot"] is null) + return; + // 可视化 + var ProcessedVersions = new JArray(); + foreach (JObject Production in Versions["production"]["minecraftVersions"]) + if ((Production["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + var ProductionVersion = new JObject(); + ProductionVersion.Add("version", Versions["production"]["labyModVersion"]); + ProductionVersion.Add("channel", "production"); + ProductionVersion.Add("commitReference", Versions["production"]["commitReference"]); + ProcessedVersions.Add(ProductionVersion); + } + + foreach (JObject Snapshot in Versions["snapshot"]["minecraftVersions"]) + if ((Snapshot["version"].ToString() ?? "") == (_vanillaName ?? "")) + { + var SnapshotVersion = new JObject(); + SnapshotVersion.Add("version", Versions["production"]["labyModVersion"]); + SnapshotVersion.Add("channel", "snapshot"); + SnapshotVersion.Add("commitReference", Versions["snapshot"]["commitReference"]); + ProcessedVersions.Add(SnapshotVersion); + } + + // MyMsgBox(If(ProcessedVersions.ToString, "Nothing")) + PanLabyMod.Children.Clear(); + PanLabyMod.Tag = ProcessedVersions; + CardLabyMod.SwapControl = PanLabyMod; + CardLabyMod.InstallMethod = Stack => + { + foreach (JObject item in (IEnumerable)Stack.Tag) + Stack.Children.Add( + ModDownloadLib.LabyModDownloadListItem(item, (a, b) => this.LabyMod_Selected((dynamic)a, b))); + }; + } + catch (Exception ex) + { + ModBase.Log(ex, "可视化 LabyMod 安装版本列表出错", ModBase.LogLevel.Feedback); + } + } + + // 选择与清除 + public void LabyMod_Selected(MyListItem sender, EventArgs e) + { + SelectedLabyModChannel = ((dynamic)sender.Tag)("channel").ToString(); + SelectedLabyModCommitRef = ((dynamic)sender.Tag)("commitReference").ToString(); + SelectedLabyModVersion = + ((dynamic)sender.Tag)("version").ToString() + (SelectedLabyModChannel == "snapshot" ? " 快照版" : " 稳定版"); + SelectedLoaderName = "LabyMod"; + CardLabyMod.IsSwapped = true; + ReloadSelected(); + } + + private void LabyMod_Clear(object sender, MouseButtonEventArgs e) + { + SelectedLabyModCommitRef = null; + SelectedLabyModVersion = null; + SelectedLabyModChannel = null; + SelectedLoaderName = null; + SelectedAPIName = null; + CardLabyMod.IsSwapped = true; + e.Handled = true; + ReloadSelected(); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.vb b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.vb index 0538d4ea8..bc4761884 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceInstall.xaml.vb @@ -366,7 +366,7 @@ Public Class PageInstanceInstall CardCleanroom.Visibility = Visibility.Collapsed End If 'NeoForge - If VanillaDrop < 200 Then '匹配 1.20.1+ 与一些愚人节版本 + If VanillaDrop > 0 AndAlso VanillaDrop < 200 Then '匹配 1.20.1+ 与一些愚人节版本 CardNeoForge.Visibility = Visibility.Collapsed Else CardNeoForge.Visibility = Visibility.Visible diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml index 5e7ba77bf..836f2374a 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml @@ -1,95 +1,153 @@ - - + + - - + + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.cs new file mode 100644 index 000000000..bc13c7759 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.cs @@ -0,0 +1,332 @@ +using System.Windows; +using System.Windows.Controls; +using PCL.Core.App; + +namespace PCL; + +public partial class PageInstanceLeft : IRefreshable +{ + /// + /// 当前显示设置的 MC 实例。 + /// + public static ModMinecraft.McInstance Instance = null; + + public PageInstanceLeft() + { + InitializeComponent(); + Loaded += (_, __) => RefreshModDisabled(); + } + + public void Refresh() + { + Refresh(ModMain.FrmMain.PageCurrentSub); + } + + public void RefreshModDisabled() + { + var hide = Config.Preference.Hide; + + if (Instance is not null && Instance.Modable) + { + ItemMod.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceMod + ? Visibility.Collapsed + : Visibility.Visible; + ItemModDisabled.Visibility = Visibility.Collapsed; + } + else + { + ItemMod.Visibility = Visibility.Collapsed; + ItemModDisabled.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceMod + ? Visibility.Collapsed + : Visibility.Visible; + } + + // 功能隐藏 + if (!PageSetupUI.HiddenForceShow) + { + var DisableCount = 0; + if (hide.InstanceSave) + DisableCount += 1; + if (hide.InstanceScreenshot) + DisableCount += 1; + if (hide.InstanceMod) + DisableCount += 1; + if (hide.InstanceResourcePack) + DisableCount += 1; + if (hide.InstanceShader) + DisableCount += 1; + if (hide.InstanceSchematic) + DisableCount += 1; + if (hide.InstanceServer) + DisableCount += 1; + if (DisableCount == 7) + TextResource.Visibility = Visibility.Collapsed; + else + TextResource.Visibility = Visibility.Visible; + } + else + { + TextResource.Visibility = Visibility.Visible; + } + + ItemInstall.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceEdit + ? Visibility.Collapsed + : Visibility.Visible; + ItemExport.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceExport + ? Visibility.Collapsed + : Visibility.Visible; + ItemWorld.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceSave + ? Visibility.Collapsed + : Visibility.Visible; + ItemScreenshot.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceScreenshot + ? Visibility.Collapsed + : Visibility.Visible; + ItemResourcePack.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceResourcePack + ? Visibility.Collapsed + : Visibility.Visible; + ItemShader.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceShader + ? Visibility.Collapsed + : Visibility.Visible; + ItemSchematic.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceSchematic + ? Visibility.Collapsed + : Visibility.Visible; + ItemServer.Visibility = !PageSetupUI.HiddenForceShow && hide.InstanceServer + ? Visibility.Collapsed + : Visibility.Visible; + } + + private void RefreshButton_Click(object sender, EventArgs e) // 由边栏按钮匿名调用 + { + Refresh((FormMain.PageSubType)ModBase.Val(((dynamic)sender).Tag)); + } + + public void Refresh(FormMain.PageSubType SubType) + { + switch (SubType) + { + case FormMain.PageSubType.VersionMod: + { + PageInstanceCompResource.Refresh(ModComp.CompType.Mod); + break; + } + case FormMain.PageSubType.VersionScreenshot: + { + PageInstanceScreenshot.Refresh(); + break; + } + case FormMain.PageSubType.VersionWorld: + { + PageInstanceSaves.Refresh(); + break; + } + case FormMain.PageSubType.VersionResourcePack: + { + PageInstanceCompResource.Refresh(ModComp.CompType.ResourcePack); + break; + } + case FormMain.PageSubType.VersionShader: + { + PageInstanceCompResource.Refresh(ModComp.CompType.Shader); + break; + } + case FormMain.PageSubType.VersionSchematic: + { + PageInstanceCompResource.Refresh(ModComp.CompType.Schematic); + break; + } + case FormMain.PageSubType.VersionInstall: + { + ModDownload.DlClientListLoader.Start(IsForceRestart: true); + ModDownload.DlOptiFineListLoader.Start(IsForceRestart: true); + ModDownload.DlForgeListLoader.Start(IsForceRestart: true); + ModDownload.DlNeoForgeListLoader.Start(IsForceRestart: true); + ModDownload.DlLiteLoaderListLoader.Start(IsForceRestart: true); + ModDownload.DlFabricListLoader.Start(IsForceRestart: true); + ModDownload.DlFabricApiLoader.Start(IsForceRestart: true); + ModDownload.DlQuiltListLoader.Start(IsForceRestart: true); + ModDownload.DlQSLLoader.Start(IsForceRestart: true); + ModDownload.DlOptiFabricLoader.Start(IsForceRestart: true); + ModDownload.DlLabyModListLoader.Start(IsForceRestart: true); + ItemInstall.Checked = true; + ModMain.FrmInstanceInstall.GetCurrentInfo(); + break; + } + case FormMain.PageSubType.VersionExport: + { + if (ModMain.FrmInstanceExport is not null) + ModMain.FrmInstanceExport.RefreshAll(); + ItemExport.Checked = true; + break; + } + case FormMain.PageSubType.VersionServer: + { + if (ModMain.FrmInstanceServer is not null) + ModMain.FrmInstanceServer.RefreshServers(); + ItemServer.Checked = true; + break; + } + } + } + + public void Reset(object sender, EventArgs e) + { + if (ModMain.MyMsgBox("是否要初始化该实例的实例独立设置?该操作不可撤销。", "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmInstanceSetup == null) + ModMain.FrmInstanceSetup = new PageInstanceSetup(); + ModMain.FrmInstanceSetup.Reset(); + ItemSetup.Checked = true; + } + } + + #region 页面切换 + + /// + /// 当前页面的编号。从 0 开始计算。 + /// + public FormMain.PageSubType PageID = FormMain.PageSubType.Default; + + /// + /// 勾选事件改变页面。 + /// + private void PageCheck(object sender, ModBase.RouteEventArgs e) + { + if (sender is MyListItem item && item.Tag is not null) + PageChange((FormMain.PageSubType)ModBase.Val(item.Tag)); + } + + public object PageGet(FormMain.PageSubType ID) + { + if ((int)ID == -1) + ID = PageID; + switch (ID) + { + case FormMain.PageSubType.VersionOverall: + { + if (ModMain.FrmInstanceOverall is null) + ModMain.FrmInstanceOverall = new PageInstanceOverall(); + return ModMain.FrmInstanceOverall; + } + case FormMain.PageSubType.VersionMod: + { + if (ModMain.FrmInstanceMod is null) + ModMain.FrmInstanceMod = new PageInstanceCompResource(ModComp.CompType.Mod); + return ModMain.FrmInstanceMod; + } + case FormMain.PageSubType.VersionModDisabled: + { + if (ModMain.FrmInstanceModDisabled is null) + ModMain.FrmInstanceModDisabled = new PageInstanceModDisabled(); + return ModMain.FrmInstanceModDisabled; + } + case FormMain.PageSubType.VersionSetup: + { + if (ModMain.FrmInstanceSetup == null) + ModMain.FrmInstanceSetup = new PageInstanceSetup(); + return ModMain.FrmInstanceSetup; + } + case FormMain.PageSubType.VersionWorld: + { + if (ModMain.FrmInstanceSaves is null) + ModMain.FrmInstanceSaves = new PageInstanceSaves(); + return ModMain.FrmInstanceSaves; + } + case FormMain.PageSubType.VersionScreenshot: + { + if (ModMain.FrmInstanceScreenshot is null) + ModMain.FrmInstanceScreenshot = new PageInstanceScreenshot(); + return ModMain.FrmInstanceScreenshot; + } + case FormMain.PageSubType.VersionResourcePack: + { + if (ModMain.FrmInstanceResourcePack is null) + ModMain.FrmInstanceResourcePack = new PageInstanceCompResource(ModComp.CompType.ResourcePack); + return ModMain.FrmInstanceResourcePack; + } + case FormMain.PageSubType.VersionShader: + { + if (ModMain.FrmInstanceShader is null) + ModMain.FrmInstanceShader = new PageInstanceCompResource(ModComp.CompType.Shader); + return ModMain.FrmInstanceShader; + } + case FormMain.PageSubType.VersionSchematic: + { + if (ModMain.FrmInstanceSchematic is null) + ModMain.FrmInstanceSchematic = new PageInstanceCompResource(ModComp.CompType.Schematic); + return ModMain.FrmInstanceSchematic; + } + case FormMain.PageSubType.VersionInstall: + { + if (ModMain.FrmInstanceInstall is null) + ModMain.FrmInstanceInstall = new PageInstanceInstall(); + return ModMain.FrmInstanceInstall; + } + case FormMain.PageSubType.VersionExport: + { + if (ModMain.FrmInstanceExport is null) + ModMain.FrmInstanceExport = new PageInstanceExport(); + return ModMain.FrmInstanceExport; + } + case FormMain.PageSubType.VersionServer: + { + if (ModMain.FrmInstanceServer is null) + ModMain.FrmInstanceServer = new PageInstanceServer(); + return ModMain.FrmInstanceServer; + } + + default: + { + throw new Exception("未知的实例设置子页面种类:" + (int)ID); + } + } + } + + /// + /// 切换现有页面。 + /// + public void PageChange(FormMain.PageSubType ID) + { + if (PageID == ID) + return; + ModAnimation.AniControlEnabled += 1; + try + { + PageChangeRun((MyPageRight)PageGet(ID)); + PageID = ID; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换分页面失败(ID " + (int)ID + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private static void PageChangeRun(MyPageRight Target) + { + ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + if (Target.Parent is not null) + Target.SetValue(ContentPresenter.ContentProperty, null); + ModMain.FrmMain.PageRight = Target; + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnExit(); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnForceExit(); + ModMain.FrmMain.PanMainRight.Child = ModMain.FrmMain.PageRight; + ModMain.FrmMain.PageRight.Opacity = 0d; + }, 130), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + ModMain.FrmMain.PageRight.Opacity = 1d; + ModMain.FrmMain.PageRight.PageOnEnter(); + }, 30, true) + }, "PageLeft PageChange"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml index 09fcfb338..fc0f6c9e8 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml @@ -1,11 +1,11 @@ - + @@ -20,11 +20,18 @@ - - - - - + + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml.cs new file mode 100644 index 000000000..26103a85a --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceModDisabled.xaml.cs @@ -0,0 +1,41 @@ +using System.Windows; +using PCL.Core.App; + +namespace PCL; + +public partial class PageInstanceModDisabled +{ + public PageInstanceModDisabled() + { + InitializeComponent(); + BtnDownload.Click += BtnDownload_Click; + BtnVersion.Click += BtnVersion_Click; + BtnDownload.Loaded += BtnDownload_Loaded; + } + + private void BtnDownload_Click(object sender, EventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall); + } + + private void BtnVersion_Click(object sender, EventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType + .Launch); // 在实例选择页面选定实例的时候只会返回一层,因此如果不先锚定 Launch,在选择实例后会回退到实例设置的这个页面 + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSelect); + } + + public void BtnDownload_Loaded(object? sender = null, RoutedEventArgs? e = null) + { + var NewVisibility = + (Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow) || + (ModMain.FrmSelectRight is not null && ModMain.FrmSelectRight.ShowHidden) + ? Visibility.Collapsed + : Visibility.Visible; + if (BtnDownload.Visibility != NewVisibility) + { + BtnDownload.Visibility = NewVisibility; + PanMain.TriggerForceResize(); + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml index 2a4c9ad8e..964f6d072 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml @@ -1,9 +1,9 @@  @@ -11,8 +11,8 @@ - - + + @@ -31,25 +31,36 @@ - - + + - - - + + + - - - - + + + + - + - + @@ -62,9 +73,15 @@ - - - + + + @@ -76,24 +93,40 @@ - - - + + + - - - - - - + + + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs new file mode 100644 index 000000000..55afb84d4 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.cs @@ -0,0 +1,766 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.VisualBasic.FileIO; +using Newtonsoft.Json.Linq; +using PCL.Core.App; +using PCL.Core.App.Configuration; +using PCL.Core.App.Configuration.Storage; +using PCL.Core.Minecraft; +using PCL.Core.UI; +using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem; + +namespace PCL; + +public partial class PageInstanceOverall +{ + private ModLoader.LoaderCombo InstanceInfoLoader; + + private bool IsLoad; + + public MyListItem ItemVersion; + private MyCompItem ModpackCompItem; + + public PageInstanceOverall() + { + InitializeComponent(); + Loaded += PageSetupLaunch_Loaded; + // Handles + ComboDisplayType.SelectionChanged += ComboDisplayType_SelectionChanged; + BtnDisplayDesc.Click += BtnDisplayDesc_Click; + BtnDisplayRename.Click += BtnDisplayRename_Click; + ComboDisplayLogo.SelectionChanged += ComboDisplayLogo_SelectionChanged; + BtnDisplayStar.Click += BtnDisplayStar_Click; + BtnFolderVersion.Click += BtnFolderVersion_Click; + BtnFolderSaves.Click += BtnFolderSaves_Click; + BtnFolderMods.Click += BtnFolderMods_Click; + BtnManageScript.Click += BtnManageScript_Click; + BtnManageCheck.Click += BtnManageCheck_Click; + BtnManageRestore.Click += BtnManageRestore_Click; + BtnManageTest.Click += BtnManageTest_Click; + BtnManageDelete.Click += BtnManageDelete_Click; + BtnManagePatch.Click += BtnManagePatch_Click; + } + + private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + + // 更新设置 + ItemDisplayLogoCustom.Tag = @"PCL\Logo.png"; + Reload(); + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + PanDisplay.TriggerForceResize(); + } + + /// + /// 确保当前页面上的信息已正确显示。 + /// + private void Reload() + { + ModAnimation.AniControlEnabled += 1; + + var instance = PageInstanceLeft.Instance; + // 刷新设置项目 + ComboDisplayType.SelectedIndex = Config.Instance.CardType[instance.PathInstance]; + BtnDisplayStar.Text = instance.IsStar ? "从收藏夹中移除" : "加入收藏夹"; + BtnFolderMods.Visibility = instance.Modable ? Visibility.Visible : Visibility.Collapsed; + // 刷新实例显示 + PanDisplayItem.Children.Clear(); + ItemVersion = PageSelectRight.McVersionListItem(instance); + ItemVersion.IsHitTestVisible = false; + PanDisplayItem.Children.Add(ItemVersion); + ModMain.FrmMain.PageNameRefresh(); + // 刷新实例信息 + GetInstanceInfo(); + // 刷新实例图标 + ComboDisplayLogo.SelectedIndex = 0; + var Logo = Config.Instance.LogoPath[instance.PathInstance]; + var LogoCustom = Config.Instance.IsLogoCustom[instance.PathInstance]; + if (LogoCustom) + foreach (MyComboBoxItem Selection in ComboDisplayLogo.Items) + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(Selection.Tag, Logo, false)) || + (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Selection.Tag, @"PCL\Logo.png", false)) && + Logo.EndsWith(@"PCL\Logo.png"))) + { + ComboDisplayLogo.SelectedItem = Selection; + break; + } + + ModAnimation.AniControlEnabled -= 1; + } + + private void GetInstanceInfo() + { + ModpackCompItem = null; + ModBase.RunInUi(() => + { + PanInfo.Children.Clear(); + PanInfo.Children.Add(new MyLoading { Text = "正在获取信息", Margin = new Thickness(0d, 0d, 0d, 10d) }); + }); + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask("获取可能的整合包信息", _ => + { + var modpackId = Config.Instance.ModpackId[PageInstanceLeft.Instance.PathInstance]; + if (!string.IsNullOrWhiteSpace(modpackId)) + { + var compProjects = ModComp.CompRequest.GetCompProjectsByIds(new List { modpackId }); + if (!(compProjects.Count == 0)) + ModBase.RunInUi(() => + { + ModpackCompItem = compProjects.First().ToCompItem(false, false); + ModpackCompItem.Tag = compProjects.First(); + }); + } + }) + { + Block = true + }); + loaders.Add(new ModLoader.LoaderTask("获取实例信息", _ => ModBase.RunInUi(() => + { + var instance = PageInstanceLeft.Instance; + var instanceInfo = instance.Info; + List items = []; + var launchCount = Config.Instance.LaunchCount[instance.PathInstance]; + if (launchCount == 0) + items.Add(new MyListItem + { + Title = "启动次数", Info = "从未启动", Logo = "pack://application:,,,/images/Blocks/RedstoneLampOff.png" + }); + else + items.Add(new MyListItem + { + Title = "启动次数", + Info = "已启动 " + Config.Instance.LaunchCount[instance.PathInstance] + " 次", + Logo = "pack://application:,,,/images/Blocks/RedstoneLampOn.png" + }); + if (!string.IsNullOrWhiteSpace(Config.Instance.ModpackVersion[instance.PathInstance])) + items.Add(new MyListItem + { + Title = "整合包版本", Info = Config.Instance.ModpackVersion[instance.PathInstance], + Logo = "pack://application:,,,/images/Blocks/CommandBlock.png" + }); + items.Add(new MyListItem + { + Title = "Minecraft", Info = instanceInfo.VanillaName, + Logo = "pack://application:,,,/images/Blocks/Grass.png" + }); + if (instanceInfo.HasForge) + items.Add(new MyListItem + { + Title = "Forge", Info = instanceInfo.Forge, Logo = "pack://application:,,,/images/Blocks/Anvil.png" + }); + if (instanceInfo.HasNeoForge) + items.Add(new MyListItem + { + Title = "NeoForge", Info = instanceInfo.NeoForge, + Logo = "pack://application:,,,/images/Blocks/NeoForge.png" + }); + if (instanceInfo.HasCleanroom) + items.Add(new MyListItem + { + Title = "Cleanroom", Info = instanceInfo.Cleanroom, + Logo = "pack://application:,,,/images/Blocks/Cleanroom.png" + }); + if (instanceInfo.HasFabric) + items.Add(new MyListItem + { + Title = "Fabric", Info = instanceInfo.Fabric, + Logo = "pack://application:,,,/images/Blocks/Fabric.png" + }); + if (instanceInfo.HasQuilt) + items.Add(new MyListItem + { + Title = "Quilt", Info = instanceInfo.Quilt, Logo = "pack://application:,,,/images/Blocks/Quilt.png" + }); + if (instanceInfo.HasOptiFine) + items.Add(new MyListItem + { + Title = "OptiFine", Info = instanceInfo.OptiFine, + Logo = "pack://application:,,,/images/Blocks/GrassPath.png" + }); + if (instanceInfo.HasLiteLoader) + items.Add(new MyListItem + { Title = "LiteLoader", Info = "已安装", Logo = "pack://application:,,,/images/Blocks/Egg.png" }); + if (instanceInfo.HasLegacyFabric) + items.Add(new MyListItem + { + Title = "Legacy Fabric", Info = instanceInfo.LegacyFabric, + Logo = "pack://application:,,,/images/Blocks/Fabric.png" + }); + if (instanceInfo.HasLabyMod) + items.Add(new MyListItem + { + Title = "LabyMod", Info = instanceInfo.LabyMod, + Logo = "pack://application:,,,/images/Blocks/LabyMod.png" + }); + var wrapPanel = new WrapPanel { Margin = new Thickness(0, -5, -20, 7) }; + foreach (var item in items) + { + wrapPanel.Children.Add(item); + wrapPanel.Children.Add(new TextBlock { Width = 2d }); + } + + PanInfo.Children.Clear(); + if (ModpackCompItem is not null) + { + PanInfo.Children.Add(ModpackCompItem); + PanInfo.Children.Add(new TextBlock()); + } + + PanInfo.Children.Add(wrapPanel); + }))); + InstanceInfoLoader = new ModLoader.LoaderCombo("Instance Info Loader", loaders) { Show = false }; + InstanceInfoLoader.Start(); + } + + #region 卡片:个性化 + + // 实例分类 + private void ComboDisplayType_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!(IsLoad && ModAnimation.AniControlEnabled == 0)) + return; + if (ComboDisplayType.SelectedIndex != 1) + { + // 改为不隐藏 + try + { + // 若设置分类为可安装 Mod,则显示正常的 Mod 管理页面 + Config.Instance.CardType[PageInstanceLeft.Instance.PathInstance] = ComboDisplayType.SelectedIndex; + PageInstanceLeft.Instance.DisplayType = + (ModMinecraft.McInstanceCardType)Conversions.ToInteger( + Config.Instance.CardType[PageInstanceLeft.Instance.PathInstance]); + ModMain.FrmInstanceLeft.RefreshModDisabled(); + + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存 + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "修改实例分类失败(" + PageInstanceLeft.Instance.Name + ")", ModBase.LogLevel.Feedback); + } + + Reload(); // 更新 “打开 Mod 文件夹” 按钮 + } + else + { + // 改为隐藏 + try + { + if (Conversions.ToBoolean(!(bool)States.Hint.HideGameInstance)) + { + if (ModMain.MyMsgBox( + "确认要从实例列表中隐藏该实例吗?隐藏该实例后,它将不再出现于 PCL 显示的实例列表中。" + "\r\n" + + "此后,在实例列表页面按下 F11 才可以查看被隐藏的实例。", "隐藏实例提示", Button2: "取消") != 1) + { + ComboDisplayType.SelectedIndex = 0; + return; + } + + States.Hint.HideGameInstance = true; + } + + Config.Instance.CardType[PageInstanceLeft.Instance.PathInstance] = + (int)ModMinecraft.McInstanceCardType.Hidden; + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存 + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "隐藏实例 " + PageInstanceLeft.Instance.Name + " 失败", ModBase.LogLevel.Feedback); + } + } + } + + // 更改描述 + private void BtnDisplayDesc_Click(object sender, MouseButtonEventArgs e) + { + try + { + var OldInfo = Config.Instance.CustomInfo[PageInstanceLeft.Instance.PathInstance]; + var NewInfo = ModMain.MyMsgBoxInput("更改描述", "修改实例的描述文本,留空则使用 PCL 的默认描述。", OldInfo, + new Collection(), "默认描述"); + if (NewInfo is not null && (OldInfo ?? "") != (NewInfo ?? "")) + Config.Instance.CustomInfo[PageInstanceLeft.Instance.PathInstance] = NewInfo; + PageInstanceLeft.Instance = new ModMinecraft.McInstance(PageInstanceLeft.Instance.Name).Load(); + Reload(); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "实例 " + PageInstanceLeft.Instance.Name + " 描述更改失败", ModBase.LogLevel.Msgbox); + } + } + + // 重命名实例 + private void BtnDisplayRename_Click(object sender, MouseButtonEventArgs e) + { + try + { + // 确认输入的新名称 + var OldName = PageInstanceLeft.Instance.Name; + var OldPath = PageInstanceLeft.Instance.PathInstance; + // 修改此部分的同时修改快速安装的实例名检测* + var NewName = ModMain.MyMsgBoxInput("重命名实例", "", OldName, + new Collection + { new ValidateFolderName(ModMinecraft.McFolderSelected + "versions", IgnoreCase: false) }); + if (string.IsNullOrWhiteSpace(NewName)) + return; + var NewPath = ModMinecraft.McFolderSelected + @"versions\" + NewName + @"\"; + // 获取临时中间名,以防止仅修改大小写的重命名失败 + var TempName = NewName + "_temp"; + var TempPath = ModMinecraft.McFolderSelected + @"versions\" + TempName + @"\"; + var IsCaseChangedOnly = (NewName.ToLower() ?? "") == (OldName.ToLower() ?? ""); + // 重新加载实例 Json 信息,避免 HMCL 项被合并 + JObject JsonObject; + try + { + JsonObject = (JObject)ModBase.GetJson(ModBase.ReadFile(PageInstanceLeft.Instance.PathInstance + + PageInstanceLeft.Instance.Name + ".json")); + } + catch (Exception ex) + { + ModBase.Log(ex, "重命名读取 Json 时失败"); + JsonObject = PageInstanceLeft.Instance.JsonObject; + } + + // 重命名主文件夹 + FileSystem.RenameDirectory(OldPath, TempName); + FileSystem.RenameDirectory(TempPath, NewName); + // 清理 ini 缓存 + ModBase.IniClearCache(PageInstanceLeft.Instance.PathIndie + "options.txt"); + // 重命名 Jar 文件与 natives 文件夹 + // 不能进行遍历重命名,否则在实例名很短的时候容易误伤其他文件(Meloong-Git/#6443) + if (Directory.Exists($"{NewPath}{OldName}-natives")) + { + if (IsCaseChangedOnly) + { + FileSystem.RenameDirectory($"{NewPath}{OldName}-natives", $"{OldName}natives_temp"); + FileSystem.RenameDirectory($"{NewPath}{OldName}-natives_temp", $"{NewName}-natives"); + } + else + { + ModBase.DeleteDirectory($"{NewPath}{NewName}-natives"); + FileSystem.RenameDirectory($"{NewPath}{OldName}-natives", $"{NewName}-natives"); + } + } + + if (File.Exists($"{NewPath}{OldName}.jar")) + { + if (IsCaseChangedOnly) + { + FileSystem.RenameFile($"{NewPath}{OldName}.jar", $"{OldName}_temp.jar"); + FileSystem.RenameFile($"{NewPath}{OldName}_temp.jar", $"{NewName}.jar"); + } + else + { + File.Delete($"{NewPath}{NewName}.jar"); + FileSystem.RenameFile($"{NewPath}{OldName}.jar", $"{NewName}.jar"); + } + } + + // 替换实例设置文件中的路径 + if (File.Exists(NewPath + @"PCL\Setup.ini")) + ModBase.WriteFile(NewPath + @"PCL\Setup.ini", + ModBase.ReadFile(NewPath + @"PCL\Setup.ini").Replace(OldPath, NewPath)); + // 更改已选中的实例 + if ((ModBase.ReadIni(ModMinecraft.McFolderSelected + "PCL.ini", "Version") ?? "") == (OldName ?? "")) + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "Version", NewName); + // 写入实例 Json + try + { + JsonObject["id"] = NewName; + ModBase.WriteFile(NewPath + NewName + ".json", JsonObject.ToString()); + } + catch (Exception ex) + { + ModBase.Log(ex, "重命名实例 Json 失败"); + } + + // 刷新与提示 + ModMain.Hint("重命名成功!", ModMain.HintType.Finish); + PageInstanceLeft.Instance = new ModMinecraft.McInstance(NewName).Load(); + if (!(ModMinecraft.McInstanceSelected == null) && + ModMinecraft.McInstanceSelected.Equals(PageInstanceLeft.Instance)) + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "Version", NewName); + Reload(); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "重命名实例失败", ModBase.LogLevel.Msgbox); + } + } + + // 实例图标 + private void ComboDisplayLogo_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!(IsLoad && ModAnimation.AniControlEnabled == 0)) + return; + // 选择 自定义 时修改图片 + try + { + if (ReferenceEquals(ComboDisplayLogo.SelectedItem, ItemDisplayLogoCustom)) + { + var FileName = SystemDialogs.SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片"); + if (string.IsNullOrEmpty(FileName)) + { + Reload(); // 还原选项 + return; + } + + ModBase.CopyFile(FileName, PageInstanceLeft.Instance.PathInstance + @"PCL\Logo.png"); + } + else + { + File.Delete(PageInstanceLeft.Instance.PathInstance + @"PCL\Logo.png"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "更改自定义实例图标失败(" + PageInstanceLeft.Instance.Name + ")", ModBase.LogLevel.Feedback); + } + + // 进行更改 + try + { + string NewLogo = Conversions.ToString(((dynamic)ComboDisplayLogo.SelectedItem).Tag); + Config.Instance.LogoPath[PageInstanceLeft.Instance.PathInstance] = NewLogo; + Config.Instance.IsLogoCustom[PageInstanceLeft.Instance.PathInstance] = !string.IsNullOrEmpty(NewLogo); + // 刷新显示 + ModBase.WriteIni(ModMinecraft.McFolderSelected + "PCL.ini", "InstanceCache", ""); // 要求刷新缓存 + PageInstanceLeft.Instance = new ModMinecraft.McInstance(PageInstanceLeft.Instance.Name).Load(); + Reload(); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "更改实例图标失败(" + PageInstanceLeft.Instance.Name + ")", ModBase.LogLevel.Feedback); + } + } + + // 收藏夹 + private void BtnDisplayStar_Click(object sender, MouseButtonEventArgs e) + { + try + { + Config.Instance.Starred[PageInstanceLeft.Instance.PathInstance] = !PageInstanceLeft.Instance.IsStar; + PageInstanceLeft.Instance = new ModMinecraft.McInstance(PageInstanceLeft.Instance.Name).Load(); + Reload(); + ModMinecraft.McInstanceListForceRefresh = true; + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + catch (Exception ex) + { + ModBase.Log(ex, "实例 " + PageInstanceLeft.Instance.Name + " 收藏状态更改失败", ModBase.LogLevel.Msgbox); + } + } + + #endregion + + #region 卡片:快捷方式 + + // 实例文件夹 + private void BtnFolderVersion_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs) + { + OpenVersionFolder(PageInstanceLeft.Instance); + } + + public static void OpenVersionFolder(ModMinecraft.McInstance Version) + { + ModBase.OpenExplorer(Version.PathInstance); + } + + // 存档文件夹 + private void BtnFolderSaves_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs) + { + var FolderPath = PageInstanceLeft.Instance.PathIndie + @"saves\"; + Directory.CreateDirectory(FolderPath); + ModBase.OpenExplorer(FolderPath); + } + + // Mod 文件夹 + private void BtnFolderMods_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs) + { + var FolderPath = PageInstanceLeft.Instance.PathIndie + @"mods\"; + Directory.CreateDirectory(FolderPath); + ModBase.OpenExplorer(FolderPath); + } + + #endregion + + #region 卡片:管理 + + // 导出启动脚本 + private void BtnManageScript_Click(object sender, MouseButtonEventArgs mouseButtonEventArgs) + { + try + { + // 弹窗要求指定脚本的保存位置 + var SavePath = SystemDialogs.SelectSaveFile("选择脚本保存位置", "启动 " + PageInstanceLeft.Instance.Name + ".bat", + "批处理文件(*.bat)|*.bat"); + if (string.IsNullOrEmpty(SavePath)) + return; + // 检查中断(等玩家选完弹窗指不定任务就结束了呢……) + if (ModLaunch.McLaunchLoader.State == ModBase.LoadState.Loading) + { + ModMain.Hint("请在当前启动任务结束后再试!", ModMain.HintType.Critical); + return; + } + + // 生成脚本 + if (ModLaunch.McLaunchStart(new ModLaunch.McLaunchOptions + { SaveBatch = SavePath, Instance = PageInstanceLeft.Instance })) + { + if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Legacy) + ModMain.Hint("正在导出启动脚本……"); + else + ModMain.Hint("正在导出启动脚本……(注意,使用脚本启动可能会导致登录失效!)"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "导出启动脚本失败(" + PageInstanceLeft.Instance.Name + ")", ModBase.LogLevel.Msgbox); + } + } + + // 补全文件 + private void BtnManageCheck_Click(object sender, MouseButtonEventArgs e) + { + try + { + // 忽略文件检查提示 + if (Conversions.ToBoolean(ModMinecraft.ShouldIgnoreFileCheck(PageInstanceLeft.Instance))) + { + ModMain.Hint("请先关闭 [实例设置 → 设置 → 高级启动选项 → 关闭文件校验],然后再尝试补全文件!"); + return; + } + + // 重复任务检查 + foreach (var OngoingLoader in ModLoader.LoaderTaskbar) + { + if ((OngoingLoader.Name ?? "") != (PageInstanceLeft.Instance.Name + " 文件补全" ?? "")) + continue; + ModMain.Hint("正在处理中,请稍候!", ModMain.HintType.Critical); + return; + } + + // 启动 + var Loader = new ModLoader.LoaderCombo(PageInstanceLeft.Instance.Name + " 文件补全", + ModDownload.DlClientFix(PageInstanceLeft.Instance, true, + ModDownload.AssetsIndexExistsBehaviour.AlwaysDownload)); + Loader.OnStateChanged = _ => + { + switch (Loader.State) + { + case ModBase.LoadState.Finished: + { + ModMain.Hint(Loader.Name + "成功!", ModMain.HintType.Finish); + break; + } + case ModBase.LoadState.Failed: + { + ModMain.Hint(Loader.Name + "失败:" + Loader.Error.Message, ModMain.HintType.Critical); + break; + } + case ModBase.LoadState.Aborted: + { + ModMain.Hint(Loader.Name + "已取消!"); + break; + } + } + }; + Loader.Start(PageInstanceLeft.Instance.Name); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "尝试补全文件失败(" + PageInstanceLeft.Instance.Name + ")", ModBase.LogLevel.Msgbox); + } + } + + // 重置 + private void BtnManageRestore_Click(object sender, MouseButtonEventArgs e) + { + try + { + var CurrentVersion = PageInstanceLeft.Instance.Info; + if (!(CurrentVersion.Drop == 99) && + ModMinecraft.CompareVersion(CurrentVersion.VanillaName, "1.5.2") == -1 && CurrentVersion.HasForge) + { + ModMain.Hint("该实例暂不支持重置!"); + return; + } + + // 确认操作 + if (ModMain.MyMsgBox( + "你确定要重置实例 " + PageInstanceLeft.Instance.Name + " 吗?" + "\r\n" + + "PCL 将会尝试重新从互联网获取此实例的资源文件信息,并重新执行自动安装。", "实例重置确认", "确认", "取消") == 2) + return; + + // 备份实例核心文件 + ModBase.CopyFile(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + ".json", + PageInstanceLeft.Instance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.Instance.Name + + ".json"); + ModBase.CopyFile(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + ".jar", + PageInstanceLeft.Instance.PathInstance + @"PCLInstallBackups\" + PageInstanceLeft.Instance.Name + + ".jar"); + // 提交安装申请 + var Request = new ModDownloadLib.McInstallRequest + { + TargetInstanceName = PageInstanceLeft.Instance.Name, + TargetInstanceFolder = $@"{ModMinecraft.McFolderSelected}versions\{PageInstanceLeft.Instance.Name}\", + MinecraftName = CurrentVersion.VanillaName, + OptiFineEntry = CurrentVersion.HasOptiFine + ? new ModDownload.DlOptiFineListEntry + { + Inherit = CurrentVersion.VanillaName, + DisplayName = CurrentVersion.VanillaName + " " + CurrentVersion.OptiFine + } + : null, + ForgeEntry = CurrentVersion.HasForge + ? new ModDownload.DlForgeVersionEntry(CurrentVersion.Forge, null, CurrentVersion.VanillaName) + { Category = "installer" } + : null, + ForgeVersion = CurrentVersion.HasForge ? CurrentVersion.Forge : null, + NeoForgeVersion = CurrentVersion.HasNeoForge ? CurrentVersion.NeoForge : null, + CleanroomVersion = CurrentVersion.HasCleanroom ? CurrentVersion.Cleanroom : null, + FabricVersion = CurrentVersion.HasFabric ? CurrentVersion.Fabric : null, + QuiltVersion = CurrentVersion.HasQuilt ? CurrentVersion.Quilt : null, + LiteLoaderEntry = CurrentVersion.HasLiteLoader + ? new ModDownload.DlLiteLoaderListEntry { Inherit = CurrentVersion.VanillaName } + : null, + LegacyFabricVersion = CurrentVersion.HasLegacyFabric ? CurrentVersion.LegacyFabric : null + }; + // .MinecraftJson = CurrentVersion.McName, + if (!ModDownloadLib.McInstall(Request, "重置")) + return; + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.Launch }); + } + catch (Exception ex) + { + ModBase.Log(ex, "重置实例 " + PageInstanceLeft.Instance.Name + " 失败", ModBase.LogLevel.Msgbox); + } + } + + // 测试游戏 + private void BtnManageTest_Click(object sender, MouseButtonEventArgs e) + { + try + { + ModLaunch.McLaunchStart(new ModLaunch.McLaunchOptions + { Instance = PageInstanceLeft.Instance, IsTest = true }); + ModMain.FrmMain.PageChange(FormMain.PageType.Launch); + } + catch (Exception ex) + { + ModBase.Log(ex, "测试游戏失败", ModBase.LogLevel.Feedback); + } + } + + // 删除实例 + // 修改此代码时,同时修改 PageSelectRight 中的代码 + private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e) + { + try + { + var IsShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + var IsHintIndie = PageInstanceLeft.Instance.State != ModMinecraft.McInstanceState.Error && + (PageInstanceLeft.Instance.PathIndie ?? "") != (ModMinecraft.McFolderSelected ?? ""); + switch (ModMain.MyMsgBox( + $"你确定要{(IsShiftPressed ? "永久" : "")}删除实例 {PageInstanceLeft.Instance.Name} 吗?" + (IsHintIndie + ? "\r\n" + "由于该实例开启了版本隔离,删除时该实例对应的存档、资源包、Mod 等文件也将被一并删除!" + : ""), "实例删除确认", Button2: "取消", IsWarn: IsHintIndie || IsShiftPressed)) + { + case 1: + { + var instancePath = PageInstanceLeft.Instance.PathInstance; + var instanceName = PageInstanceLeft.Instance.Name; + ModBase.IniClearCache(PageInstanceLeft.Instance.PathIndie + "options.txt"); + ((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache( + instancePath); + if (IsShiftPressed) + { + ModBase.DeleteDirectory(instancePath); + ModMain.Hint("实例 " + instanceName + " 已永久删除!", ModMain.HintType.Finish); + } + else + { + FileSystem.DeleteDirectory(instancePath, UIOption.OnlyErrorDialogs, + RecycleOption.SendToRecycleBin); + ModMain.Hint("实例 " + instanceName + " 已删除到回收站!", ModMain.HintType.Finish); + } + + break; + } + case 2: + { + return; + } + } + + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + ModMain.FrmMain.PageBack(); + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除实例 " + PageInstanceLeft.Instance.Name + " 被主动取消"); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除实例 " + PageInstanceLeft.Instance.Name + " 失败", ModBase.LogLevel.Msgbox); + } + } + + // 修补核心 + private void BtnManagePatch_Click(object sender, MouseButtonEventArgs e) + { + switch (ModMain.MyMsgBox( + $"你确定要对 {PageInstanceLeft.Instance.Name} 的核心文件进行修补吗? {"\r\n"}修补游戏核心可能导致游戏崩溃等问题。{"\r\n"}在修补核心后,文件校验会自动关闭。", + "修补提示", Button2: "取消")) + { + case 1: + { + var UserInput = SystemDialogs.SelectFile("压缩文件(*.jar;*.zip)|*.jar;*.zip", "选择用于修补核心的文件"); + if (UserInput is null | string.IsNullOrWhiteSpace(UserInput)) + return; + ModMain.Hint("正在修补游戏核心,这可能需要一段时间"); + ModBase.RunInNewThread(() => + { + var Core = new GameCore(PageInstanceLeft.Instance.PathInstance + PageInstanceLeft.Instance.Name + + ".jar"); + Core.AddToCore(UserInput); + ModMain.Hint("修补游戏核心成功", ModMain.HintType.Finish); + Config.Instance.DisableAssetVerifyV2[PageInstanceLeft.Instance] = true; + }); + break; + } + case 2: + { + return; + } + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml index e1ba0f0d3..31f1f2d60 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml @@ -1,9 +1,9 @@  @@ -21,11 +21,19 @@ - - - - - + + + + + @@ -39,15 +47,20 @@ - - + + - + - - + + diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml.cs new file mode 100644 index 000000000..6f3f4f7ab --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves.xaml.cs @@ -0,0 +1,537 @@ +using System.Collections.Specialized; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Threading; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.VisualBasic.FileIO; + +namespace PCL; + +public partial class PageInstanceSaves : IRefreshable +{ + private readonly DispatcherTimer fileSystemRefreshTimer; + private readonly DispatcherTimer searchTimer; + private FileSystemWatcher fileSystemWatcher; + private bool IsLoad; + + private object QuickPlayFeature = false; + + private List saveFolders = new(); + private string WorldPath; + + public PageInstanceSaves() + { + InitializeComponent(); + fileSystemRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) }; + searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) }; + Loaded += PageSetupLaunch_Loaded; + Unloaded += Page_Unloaded; + fileSystemRefreshTimer.Tick += FileSystemRefreshTimer_Tick; + searchTimer.Tick += SearchTimer_Tick; + SearchBox.TextChanged += SearchRun; + } + + void IRefreshable.Refresh() + { + RefreshSelf(); + } + + private void RefreshSelf() + { + Refresh(); + CheckQuickPlay(); + } + + public static void Refresh() + { + if (ModMain.FrmInstanceSaves is not null) + ModMain.FrmInstanceSaves.Reload(); + ModMain.FrmInstanceLeft.ItemWorld.Checked = true; + ModMain.Hint("正在刷新……", Log: false); + } + + private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + WorldPath = PageInstanceLeft.Instance.PathIndie + @"saves\"; + if (!Directory.Exists(WorldPath)) + Directory.CreateDirectory(WorldPath); + Reload(); + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + CheckQuickPlay(); + + // 初始化文件系统监视器和排序按钮 + SetupFileSystemWatcher(); + BtnSort.Click += BtnSortClick; + } + + private string GetFolderNameFromPath(string fullPath) + { + return string.IsNullOrEmpty(fullPath) ? "" : + fullPath.EndsWith(@"\") ? new DirectoryInfo(fullPath).Parent?.Name : new DirectoryInfo(fullPath).Name; + } + + private string GetFileNameFromPath(string fullPath) + { + return Path.GetFileName(fullPath); + } + + private void SetupFileSystemWatcher() + { + if (fileSystemWatcher is not null) fileSystemWatcher.Dispose(); + + // 确保目录存在 + if (!Directory.Exists(WorldPath)) + Directory.CreateDirectory(WorldPath); + + fileSystemWatcher = new FileSystemWatcher(); + fileSystemWatcher.Path = WorldPath; + fileSystemWatcher.IncludeSubdirectories = false; + fileSystemWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.LastWrite; + + fileSystemWatcher.Created += OnFileSystemChanged; + fileSystemWatcher.Deleted += OnFileSystemChanged; + fileSystemWatcher.Renamed += OnFileSystemChanged; + + fileSystemWatcher.EnableRaisingEvents = true; + } + + private void OnFileSystemChanged(object sender, FileSystemEventArgs e) + { + fileSystemRefreshTimer.Stop(); + fileSystemRefreshTimer.Start(); + } + + private void FileSystemRefreshTimer_Tick(object sender, EventArgs e) + { + fileSystemRefreshTimer.Stop(); + ModBase.RunInUi(() => Reload(), true); + } + + private void Page_Unloaded(object sender, RoutedEventArgs e) + { + if (fileSystemWatcher is not null) + { + fileSystemWatcher.Created -= OnFileSystemChanged; + fileSystemWatcher.Deleted -= OnFileSystemChanged; + fileSystemWatcher.Renamed -= OnFileSystemChanged; + fileSystemWatcher.Dispose(); + fileSystemWatcher = null; + } + + fileSystemRefreshTimer.Stop(); + searchTimer.Stop(); + } + + /// + /// 确保当前页面上的信息已正确显示。 + /// + public void Reload() + { + ModAnimation.AniControlEnabled += 1; + PanBack.ScrollToHome(); + LoadFileList(); + ModAnimation.AniControlEnabled -= 1; + } + + private void RefreshUI() + { + try + { + if (IsSearching) + { + var resultCount = _searchResult is null ? 0 : _searchResult.Count; + PanListBack.Title = $"搜索结果 ({resultCount})"; + } + else + { + PanListBack.Title = $"存档列表 ({saveFolders.Count})"; + } + + if (saveFolders.Count == 0) + { + PanNoWorld.Visibility = Visibility.Visible; + PanContent.Visibility = Visibility.Collapsed; + PanNoWorld.UpdateLayout(); + } + else + { + PanNoWorld.Visibility = Visibility.Collapsed; + PanContent.Visibility = Visibility.Visible; + PanContent.UpdateLayout(); + + var showingSaves = (IsSearching ? _searchResult : saveFolders).ToList(); + + if (showingSaves.Any()) + { + var sortMethod = GetSortMethod(_currentSortMethod); + showingSaves.Sort((a, b) => sortMethod(a, b)); + } + + ModAnimation.AniControlEnabled += 1; + PanList.Children.Clear(); + + foreach (var curFolder in showingSaves) + { + // 检查文件夹是否仍然存在 + if (!Directory.Exists(curFolder)) continue; + + var saveLogo = curFolder + @"\icon.png"; + var tmpCurFolder = curFolder; + if (File.Exists(saveLogo)) + { + var target = + $@"{PageInstanceLeft.Instance.PathInstance}PCL\ImgCache\{ModBase.GetStringMD5(saveLogo)}.png"; + ModBase.CopyFile(saveLogo, target); + saveLogo = target; + } + else + { + saveLogo = ModBase.PathImage + "Icons/NoIcon.png"; + } + + var worldItem = new MyListItem + { + Logo = saveLogo, + Title = GetFolderNameFromPath(curFolder), + Info = + $"创建时间:{Directory.GetCreationTime(curFolder).ToString("yyyy\"/\"MM\"/\"dd")},最后修改时间:{Directory.GetLastWriteTime(curFolder).ToString("yyyy\"/\"MM\"/\"dd")}", + Type = MyListItem.CheckType.Clickable + }; + worldItem.Click += (_, _) => ModMain.FrmMain.PageChange(new FormMain.PageStackData + { Page = FormMain.PageType.VersionSaves, Additional = tmpCurFolder }); + + var BtnOpen = new MyIconButton + { + Logo = ModBase.Logo.IconButtonOpen, + ToolTip = "打开" + }; + BtnOpen.Click += (_, _) => ModBase.OpenExplorer(tmpCurFolder); + var BtnDelete = new MyIconButton + { + Logo = ModBase.Logo.IconButtonDelete, + ToolTip = "删除" + }; + BtnDelete.Click += (_, _) => + { + worldItem.IsEnabled = false; + worldItem.Info = "删除中……"; + ModBase.RunInNewThread(() => + { + try + { + FileSystem.DeleteDirectory(tmpCurFolder, UIOption.OnlyErrorDialogs, + RecycleOption.SendToRecycleBin); + ModMain.Hint("已将存档移至回收站!"); + ModBase.RunInUiWait(() => RemoveItem(worldItem)); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除存档失败!", ModBase.LogLevel.Hint); + ModBase.RunInUiWait(() => Reload()); + } + }); + }; + var BtnCopy = new MyIconButton + { + Logo = ModBase.Logo.IconButtonCopy, + ToolTip = "复制" + }; + BtnCopy.Click += (_, _) => + { + try + { + if (Directory.Exists(tmpCurFolder)) + { + Clipboard.SetFileDropList(new StringCollection { tmpCurFolder }); + ModMain.Hint("已复制存档文件夹到剪贴板!"); + ModMain.Hint("注意!在粘贴之前进行删除操作会导致存档丢失!"); + } + else + { + ModMain.Hint("存档文件夹不存在!"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "复制失败……", ModBase.LogLevel.Hint); + } + }; + var BtnInfo = new MyIconButton + { + Logo = ModBase.Logo.IconButtonInfo, + ToolTip = "详情" + }; + BtnInfo.Click += (_, _) => ModMain.FrmMain.PageChange(new FormMain.PageStackData + { Page = FormMain.PageType.VersionSaves, Additional = tmpCurFolder }); + + var BtnLaunch = new MyIconButton + { + Logo = ModBase.Logo.IconPlayGame, + ToolTip = "快捷启动" + }; + BtnLaunch.Click += (_, _) => + { + var WorldName = GetFileNameFromPath(tmpCurFolder); + var LaunchOptions = new ModLaunch.McLaunchOptions { WorldName = WorldName }; + ModLaunch.McLaunchStart(LaunchOptions); + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.Launch }); + }; + if (Conversions.ToBoolean(QuickPlayFeature)) + worldItem.Buttons = new[] { BtnOpen, BtnDelete, BtnCopy, BtnInfo, BtnLaunch }; + else + worldItem.Buttons = new[] { BtnOpen, BtnDelete, BtnCopy, BtnInfo }; + + PanList.Children.Add(worldItem); + } + + ModAnimation.AniControlEnabled -= 1; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新存档UI失败", ModBase.LogLevel.Hint); + } + } + + private void CheckQuickPlay() + { + try + { + var cur = new ModLaunch.LaunchArgument(PageInstanceLeft.Instance); + QuickPlayFeature = cur.HasArguments("--quickPlaySingleplayer"); + } + catch (Exception ex) + { + ModBase.Log(ex, "检查存档快捷启动失败", ModBase.LogLevel.Hint); + } + } + + private void LoadFileList() + { + try + { + ModBase.Log("[World] 刷新存档文件"); + saveFolders.Clear(); + if (Directory.Exists(WorldPath)) + saveFolders = Directory.EnumerateDirectories(WorldPath).ToList(); + else + saveFolders = new List(); + + if (ModBase.ModeDebug) + ModBase.Log("[World] 共发现 " + saveFolders.Count + " 个存档文件夹", ModBase.LogLevel.Debug); + PanList.Children.Clear(); + CheckQuickPlay(); + + if (ModBase.ModeDebug) + { + if (Conversions.ToBoolean(QuickPlayFeature)) + ModBase.Log("[World] 该实例支持存档快捷启动", ModBase.LogLevel.Debug); + else + ModBase.Log("[World] 该实例不支持存档快捷启动", ModBase.LogLevel.Debug); + } + + RefreshUI(); // 确保UI刷新 + } + catch (Exception ex) + { + ModBase.Log(ex, "载入存档列表失败", ModBase.LogLevel.Hint); + } + } + + private void RemoveItem(MyListItem item) + { + if (PanList.Children.IndexOf(item) == -1) + return; + PanList.Children.Remove(item); + RefreshUI(); + } + + private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenExplorer(WorldPath); + } + + private void BtnPaste_Click(object sender, MouseButtonEventArgs e) + { + var files = Clipboard.GetFileDropList(); + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask("Copy saves", _ => + { + var Copied = 0; + foreach (var i in files) + try + { + if (Directory.Exists(i)) + { + if (Directory.Exists(WorldPath + GetFolderNameFromPath(i))) + { + ModMain.Hint("发现同名文件夹,无法粘贴:" + GetFolderNameFromPath(i)); + } + else + { + ModBase.CopyDirectory(i, WorldPath + GetFolderNameFromPath(i)); + Copied += 1; + } + } + else + { + ModMain.Hint("源文件夹不存在或源目标不是文件夹"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "粘贴存档文件夹失败", ModBase.LogLevel.Hint); + } + + if (Copied > 0) + ModMain.Hint("已粘贴 " + Copied + " 个文件夹", ModMain.HintType.Finish); + ModBase.RunInUi(() => Reload()); + })); + var loader = new ModLoader.LoaderCombo($"{PageInstanceLeft.Instance.Name} - 复制存档", loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + #region 搜索和排序 + + private SortMethod _currentSortMethod = SortMethod.FileName; + private List _searchResult; + + public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text); + + private enum SortMethod + { + FileName, + CreateTime, + ModifyTime + } + + private string GetSortName(SortMethod method) + { + switch (method) + { + case SortMethod.FileName: + { + return "文件名"; + } + case SortMethod.CreateTime: + { + return "创建时间"; + } + case SortMethod.ModifyTime: + { + return "修改时间"; + } + + default: + { + return "文件名"; + } + } + } + + private void SetSortMethod(SortMethod target) + { + _currentSortMethod = target; + BtnSort.Text = $"排序:{GetSortName(target)}"; + RefreshUI(); + } + + private void BtnSortClick(object sender, EventArgs e) + { + var body = new ContextMenu(); + foreach (SortMethod i in Enum.GetValues(typeof(SortMethod))) + { + var item = new MyMenuItem(); + item.Header = GetSortName(i); + item.Click += (_, _) => SetSortMethod(i); + body.Items.Add(item); + } + + body.PlacementTarget = (UIElement)sender; + body.Placement = PlacementMode.Bottom; + body.IsOpen = true; + } + + private void SearchRun(object sender, EventArgs e) + { + searchTimer.Stop(); + searchTimer.Start(); + } + + private void SearchTimer_Tick(object sender, EventArgs e) + { + searchTimer.Stop(); + PerformSearch(); + } + + private void PerformSearch() + { + try + { + if (IsSearching) + { + var queryList = new List>(); + foreach (var saveFolder in saveFolders) + { + var folderName = GetFolderNameFromPath(saveFolder); + var searchSource = new List>(); + searchSource.Add(new KeyValuePair(folderName, 1d)); + queryList.Add(new ModBase.SearchEntry { Item = saveFolder, SearchSource = searchSource }); + } + + _searchResult = ModBase.Search(queryList, SearchBox.Text, 6, 0.35d).Select(r => r.Item).ToList(); + } + else + { + _searchResult = null; + } + + RefreshUI(); + } + catch (Exception ex) + { + ModBase.Log(ex, "搜索过程中发生异常"); + } + } + + private Func GetSortMethod(SortMethod method) + { + switch (method) + { + case SortMethod.FileName: + { + return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b), + StringComparison.OrdinalIgnoreCase); + } + case SortMethod.CreateTime: + { + return (a, b) => Directory.GetCreationTime(b).CompareTo(Directory.GetCreationTime(a)); + } + case SortMethod.ModifyTime: + { + return (a, b) => Directory.GetLastWriteTime(b).CompareTo(Directory.GetLastWriteTime(a)); + } + + default: + { + return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b), + StringComparison.OrdinalIgnoreCase); + } + } + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml index 9f5586b70..485e4cbe1 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml @@ -1,19 +1,22 @@  - + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" x:Class="PCL.PageInstanceSavesBackup" + PanScroll="{Binding ElementName=PanBack}"> + - - + + - + @@ -28,11 +31,15 @@ - - - + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml.cs new file mode 100644 index 000000000..0dc834551 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesBackup.xaml.cs @@ -0,0 +1,266 @@ +using System.Windows; +using Microsoft.VisualBasic; +using PCL.Core.IO; +using PCL.Core.UI; +using PCL.Core.Utils.VersionControl; + +namespace PCL; + +public partial class PageInstanceSavesBackup : IRefreshable +{ + private bool _loaded; + + public PageInstanceSavesBackup() + { + InitializeComponent(); + Loaded += (_, _) => Init(); + BtnCreate.Click += (_, _) => BtnCreate_Click(); + BtnClean.Click += (_, _) => BtnClean_Click(); + } + + void IRefreshable.Refresh() + { + IRefreshable_Refresh(); + } + + private void IRefreshable_Refresh() + { + Refresh(); + } + + public void Refresh() + { + RefreshList(); + } + + private void Init() + { + PanBack.ScrollToHome(); + + RefreshList(); + + _loaded = true; + if (_loaded) + return; + } + + private void RefreshList() + { + try + { + PanList.Children.Clear(); + List versions; + using (var snap = new SnapLiteVersionControl(PageInstanceSavesLeft.CurrentSave)) + { + versions = snap.GetVersions(); + if (versions.Count != 0) + { + PanDisplay.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + } + else + { + PanDisplay.Visibility = Visibility.Collapsed; + PanEmpty.Visibility = Visibility.Visible; + } + } + + if (versions.Count != 0) return; + foreach (var item in versions) + { + var newItem = new MyListItem + { + Title = item.Name, + Info = item.Desc, + Tags = new[] { item.Created }.ToList() + }; + + var btnApply = new MyIconButton + { + Logo = ModBase.Logo.IconPlayGame, + ToolTip = "回到到此快照" + }; + + btnApply.Click += (_, _) => + { + try + { + if (ModMain.MyMsgBox("确定要应用此备份吗?请确保当前的存档已完成备份或者十分确定不再使用!", Button1: "确定", Button2: "取消") == 2) + return; + ModMain.Hint("应用快照中,请勿执行其他操作!"); + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask("搜寻并应用文件", load => + { + load.Progress = 0.2d; + load.Progress = 1d; + })); + var loader = new ModLoader.LoaderCombo($"{item.Name} - 备份应用", loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "应用快照过程中出现错误", ModBase.LogLevel.Msgbox); + } + }; + + var btnExport = new MyIconButton + { + Logo = ModBase.Logo.IconButtonSave, + ToolTip = "导出到压缩包" + }; + + btnExport.Click += (_, _) => + { + try + { + var savePath = SystemDialogs.SelectSaveFile("选择保存备份导出的位置", $"{item.Name}.zip", + "压缩文件(*.zip)|*.zip", ModBase.ExePath); + if (string.IsNullOrEmpty(savePath)) + return; + ModMain.Hint("快照导出中,请勿执行其他操作!"); + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask("制作压缩包", load => + { + load.Progress = 0.2d; + ; + load.Progress = 1d; + })); + var loader = new ModLoader.LoaderCombo($"{item.Name} - 导出备份", loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + catch (Exception ex) + { + ModBase.Log(ex, "备份导出过程中出现错误", ModBase.LogLevel.Msgbox); + } + }; + + var btnDelete = new MyIconButton + { + Logo = ModBase.Logo.IconButtonDelete, + ToolTip = "删除" + }; + + btnDelete.Click += (_, _) => + { + try + { + if (ModMain.MyMsgBox( + $"你确定要删除备份 {item.Name} 吗?{"\r\n"}描述:{item.Desc}{"\r\n"}创建时间:{item.Created}", + "删除确认", "确认", "取消") == 2) return; + using (var snap = new SnapLiteVersionControl(PageInstanceSavesLeft.CurrentSave)) + { + snap.DeleteVersion(item.NodeId); + } + + RefreshList(); + ModMain.Hint("已删除!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "执行删除任务失败"); + } + }; + + var btnInfo = new MyIconButton + { + Logo = ModBase.Logo.IconButtonInfo, + ToolTip = "信息" + }; + + + btnInfo.Click += (_, _) => + { + try + { + List data; + using (var snap = new SnapLiteVersionControl(PageInstanceSavesLeft.CurrentSave)) + { + data = snap.GetNodeObjects(item.NodeId); + } + + var totalSize = data.Select(x => x.Length).Sum(); + ModMain.MyMsgBox($@"描述: {item.Desc} + 创建时间: {item.Created} + 存档大小: {ByteStream.GetReadableLength(totalSize)} ({data.Count} 个对象)", item.Name); + } + catch (Exception ex) + { + ModBase.Log(ex, "执行删除任务失败"); + } + }; + newItem.Buttons = [btnDelete, btnExport, btnInfo, btnApply]; + + PanList.Children.Add(newItem); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取备份信息失败", ModBase.LogLevel.Msgbox); + } + } + + private void BtnCreate_Click() + { + try + { + var input = ModMain.MyMsgBoxInput("请输入名称", DefaultInput: $"{DateTime.Now:yyyy/dd/MM-HH:mm:ss}"); + if (input is null) + return; + if (string.IsNullOrWhiteSpace(input)) + input = null; + if (ModMain.MyMsgBox("备份功能不具备热备份功能,请确你没有在使用存档内的任何文件!", "请注意!", "继续", "返回") == 2) + return; + BtnCreate.IsEnabled = false; + ModMain.Hint("开始备份任务,请勿执行其他操作!"); + var loaders = new List(); + loaders.Add(new ModLoader.LoaderTask("搜寻并制作备份", load => + { + load.Progress = 0.2d; + + load.Progress = 1d; + ModBase.RunInUi(() => RefreshList()); + })); + var loader = new ModLoader.LoaderCombo($"{input} - 制作备份", loaders) + { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + BtnCreate.IsEnabled = true; + } + catch (Exception ex) + { + ModBase.Log(ex, "备份过程中出现错误", ModBase.LogLevel.Msgbox); + } + } + + private void BtnClean_Click() + { + if (ModMain.MyMsgBox("此功能可以清理备份文件中已不再需要的文件,建议在发生备份删除后使用。", "确定使用吗?", "确定", "返回") == 2) + return; + var loaders = new List + { + new ModLoader.LoaderTask("寻找并清理备份文件", load => + { + load.Progress = 0.2d; + ; + load.Progress = 1d; + }) + }; + var loader = + new ModLoader.LoaderCombo($"{ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.CurrentSave)} - 备份清理", + loaders) { OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly }; + loader.Start(1); + ModLoader.LoaderTaskbarAdd(loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml index 9c4223912..90326a308 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml @@ -1,33 +1,49 @@  - + - + - - - - - + + + + + - - - - - - - + + + + + + + - - + + @@ -35,41 +51,57 @@ - + - + - - - + + + - - + + - + + LogoScale="1" + Logo="M640 768H384l-32-32V509H213L190 454l298-298h45l298 298L810 509h-138v226z m-224-64h192V477l32-32h93L512 223 290 445H384l32 32zM352 831h320v64h-320z" /> + LogoScale="1.05" + Logo="M512 0a512 512 0 1 0 512 512A512 512 0 0 0 512 0z m0 921.6a409.6 409.6 0 1 1 409.6-409.6 409.6 409.6 0 0 1-409.6 409.6z M716.8 339.968l-256 253.44L328.192 460.8A51.2 51.2 0 0 0 256 532.992l168.448 168.96a51.2 51.2 0 0 0 72.704 0l289.28-289.792A51.2 51.2 0 0 0 716.8 339.968z" /> + LogoScale="1" + Logo="M508 990.4c-261.6 0-474.4-212-474.4-474.4S246.4 41.6 508 41.6s474.4 212 474.4 474.4S769.6 990.4 508 990.4zM508 136.8c-209.6 0-379.2 169.6-379.2 379.2 0 209.6 169.6 379.2 379.2 379.2s379.2-169.6 379.2-379.2C887.2 306.4 717.6 136.8 508 136.8zM697.6 563.2 318.4 563.2c-26.4 0-47.2-21.6-47.2-47.2 0-26.4 21.6-47.2 47.2-47.2l379.2 0c26.4 0 47.2 21.6 47.2 47.2C744.8 542.4 724 563.2 697.6 563.2z" /> + LogoScale="1" + Logo="M700.856 155.543c-74.769 0-144.295 72.696-190.046 127.26-45.737-54.576-115.247-127.26-190.056-127.26-134.79 0-244.443 105.78-244.443 235.799 0 77.57 39.278 131.988 70.845 175.713C238.908 694.053 469.62 852.094 479.39 858.757c9.41 6.414 20.424 9.629 31.401 9.629 11.006 0 21.998-3.215 31.398-9.63 9.782-6.662 240.514-164.703 332.238-291.701 31.587-43.724 70.874-98.143 70.874-175.713-0.001-130.02-109.656-235.8-244.445-235.8z" /> + LogoScale="1" + Logo="M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z" /> + LogoScale="0.96" + Logo="M520.192 0C408.43 0 317.44 82.87 313.563 186.734H52.736c-29.038 0-52.663 21.943-52.663 49.079s23.625 49.152 52.663 49.152h58.075v550.473c0 103.35 75.118 187.757 167.717 187.757h472.43c92.599 0 167.716-83.894 167.716-187.757V285.477h52.59c29.038 0 52.59-21.943 52.663-49.08-0.073-27.135-23.625-49.151-52.663-49.151H726.235C723.237 83.017 631.955 0 520.192 0zM404.846 177.957c3.803-50.03 50.176-89.015 107.447-89.015 57.197 0 103.57 38.985 106.788 89.015H404.92zM284.379 933.669c-33.353 0-69.997-39.351-69.997-95.525v-549.01H833.39v549.522c0 56.247-36.645 95.525-69.998 95.525H284.379v-0.512z M357.23 800.695a48.274 48.274 0 0 0 47.616-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.69 49.006V751.69c0 27.282 20.846 49.006 47.617 49.006z m166.62 0a48.274 48.274 0 0 0 47.688-49.006V471.7a48.274 48.274 0 0 0-47.689-49.08 48.274 48.274 0 0 0-47.543 49.006V751.69c0 27.282 21.431 49.006 47.543 49.006z m142.92 0a48.274 48.274 0 0 0 47.543-49.006V471.7a48.274 48.274 0 0 0-47.543-49.08 48.274 48.274 0 0 0-47.616 49.006V751.69c0 27.282 20.773 49.006 47.543 49.006z" /> + LogoScale="0.8" + Logo="M867.648 951.296 512 595.648l-355.648 355.648c-11.52 11.52-30.272 11.52-41.856 0L72.64 909.44c-11.52-11.52-11.52-30.272 0-41.856L428.352 512 72.64 156.352c-11.52-11.52-11.52-30.272 0-41.856l41.856-41.856c11.52-11.52 30.272-11.52 41.856 0L512 428.288l355.648-355.648c11.52-11.52 30.272-11.52 41.856 0l41.856 41.856c11.52 11.52 11.52 30.272 0 41.856L595.648 512l355.648 355.648c11.52 11.52 11.52 30.272 0 41.856l-41.856 41.856C897.984 962.88 879.168 962.88 867.648 951.296L867.648 951.296z" /> diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml.cs new file mode 100644 index 000000000..6c4d75ac8 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesDatapack.xaml.cs @@ -0,0 +1,1596 @@ +using System.IO; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.VisualBasic.FileIO; +using PCL.Core.App; +using PCL.Core.UI; +using PCL.Core.UI.Theme; +using FileSystem = Microsoft.VisualBasic.FileSystem; + +namespace PCL; + +public partial class PageInstanceSavesDatapack : IRefreshable +{ + #region 数据包信息缓存 + + private readonly Dictionary DatapackFileInfoCache = new(); + + // 获取数据包信息(带缓存) + private (DateTime CreationTime, long Length) GetDatapackFileInfo(string path) + { + (DateTime CreationTime, long Length) cacheItem; + if (DatapackFileInfoCache.TryGetValue(path, out cacheItem)) return cacheItem; + + try + { + var fileInfo = new FileInfo(path); + var newItem = (fileInfo.CreationTime, fileInfo.Length); + if (!DatapackFileInfoCache.ContainsKey(path)) DatapackFileInfoCache.Add(path, newItem); + return newItem; + } + catch (Exception ex) + { + ModBase.Log(ex, "获取数据包信息失败: " + path); + return (DateTime.MinValue, 0L); + } + } + + // 页面关闭时清理缓存 + private void Page_Unloaded(object sender, RoutedEventArgs e) + { + DatapackFileInfoCache.Clear(); + } + + #endregion + + #region 初始化 + + private readonly MyLocalCompItem.SwipeSelect CurrentSwipSelect; + + public PageInstanceSavesDatapack() + { + CurrentSwipSelect = new MyLocalCompItem.SwipeSelect { TargetFrm = this }; + + InitializeComponent(); + Unloaded += Page_Unloaded; + Loaded += (_, _) => PageOther_Loaded(); + LoaderInit(); + PageExit += UnselectedAllWithAnimation; + // Handles + Load.Click += Load_Click; + BtnManageOpen.Click += BtnManageOpen_Click; + BtnHintOpen.Click += BtnManageOpen_Click; + BtnManageSelectAll.Click += BtnManageSelectAll_Click; + BtnManageInstall.Click += BtnManageInstall_Click; + BtnHintInstall.Click += BtnManageInstall_Click; + BtnManageDownload.Click += BtnManageDownload_Click; + BtnHintDownload.Click += BtnManageDownload_Click; + BtnManageInfoExport.Click += BtnManageInfoExport_Click; + Load.StateChanged += (_, _, _) => UnselectedAllWithAnimation(); + SearchBox.PreviewKeyDown += SearchBox_PreviewKeyDown; + BtnFilterAll.Check += ChangeFilter; + BtnFilterCanUpdate.Check += ChangeFilter; + BtnFilterDisabled.Check += ChangeFilter; + BtnFilterEnabled.Check += ChangeFilter; + BtnFilterError.Check += ChangeFilter; + BtnSort.Click += BtnSortClick; + BtnSelectEnable.Click += BtnSelectEnable_Click; + BtnSelectDisable.Click += BtnSelectDisable_Click; + BtnSelectUpdate.Click += BtnSelectUpdate_Click; + BtnSelectDelete.Click += BtnSelectDelete_Click; + BtnSelectCancel.Click += BtnSelectCancel_Click; + BtnSelectFavorites.Click += BtnSelectFavorites_Click; + BtnSelectShare.Click += BtnSelectShare_Click; + SearchBox.TextChanged += SearchRun; + } + + private ModLocalComp.CompLocalLoaderData GetRequireLoaderData() + { + var res = new ModLocalComp.CompLocalLoaderData(); + res.GameVersion = PageInstanceLeft.Instance; + res.Frm = null; + res.Loaders = new[] { ModComp.CompLoaderType.Minecraft }.ToList(); + res.CompPath = PageInstanceSavesLeft.CurrentSave + @"\datapacks\"; + res.CompType = ModComp.CompType.DataPack; + return res; + } + + private bool IsLoad; + + public void PageOther_Loaded() + { + if (ModMain.FrmMain.PageLast.Page != FormMain.PageType.CompDetail) + PanBack.ScrollToHome(); + ModAnimation.AniControlEnabled += 1; + SelectedDatapacks.Clear(); + ReloadDatapackFileList(); + ChangeAllSelected(false); + ModAnimation.AniControlEnabled -= 1; + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + + ModMain.FrmMain.KeyDown += FrmMain_KeyDown; + // 调整按钮边距(这玩意儿没法从 XAML 改) + foreach (MyRadioButton Btn in PanFilter.Children) + Btn.LabText.Margin = new Thickness(-2, 0d, 8d, 0d); + } + + /// + /// 刷新数据包列表。 + /// + public void ReloadDatapackFileList(bool ForceReload = false) + { + if (LoaderRun(ForceReload + ? ModLoader.LoaderFolderRunType.ForceRun + : ModLoader.LoaderFolderRunType.RunOnUpdated)) + { + ModBase.Log("[System] 已刷新数据包列表"); + DatapackFileInfoCache.Clear(); + + ModBase.RunInUi(() => + { + Filter = FilterType.All; + PanBack.ScrollToHome(); + SearchBox.Text = ""; + }); + } + } + + // 强制刷新 + private void RefreshSelf() + { + Refresh(); + } + + void IRefreshable.Refresh() + { + RefreshSelf(); + } + + public void Refresh() + { + ModMain.FrmInstanceSavesDatapack.ReloadDatapackFileList(true); + ModBase.Log("[Datapack] 刷新数据包列表"); + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanAllBack, null, ModLocalComp.CompResourceListLoader, + _ => LoadUIFromLoaderOutput(), () => ModComp.CompType.DataPack, false); + } + + private void Load_Click(object sender, MouseButtonEventArgs e) + { + if (ModLocalComp.CompResourceListLoader.State == ModBase.LoadState.Failed) + LoaderRun(ModLoader.LoaderFolderRunType.ForceRun); + } + + public bool LoaderRun(ModLoader.LoaderFolderRunType Type) + { + var LoadPath = PageInstanceSavesLeft.CurrentSave + @"\datapacks\"; + return ModLoader.LoaderFolderRun(ModLocalComp.CompResourceListLoader, LoadPath, Type, + LoaderInput: GetRequireLoaderData()); + } + + #endregion + + #region UI 化 + + /// + /// 已加载的数据包 UI 缓存。Key 为数据包的 RawPath。 + /// + public Dictionary DatapackItems = new(); + + /// + /// 将加载器结果的数据包列表加载为 UI。 + /// + private void LoadUIFromLoaderOutput() + { + try + { + // 判断应该显示哪一个页面 + if (ModLocalComp.CompResourceListLoader.Output.Any()) + { + PanBack.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + } + else + { + // 根据组件类型设置 PanEmpty 的文本内容 + TxtEmptyTitle.Text = "尚未安装数据包"; + TxtEmptyDescription.Text = "你可以从已经下载好的文件安装数据包。" + "\r\n" + "数据包需要放置在存档的 datapacks 文件夹中才能生效。"; + + PanEmpty.Visibility = Visibility.Visible; + PanBack.Visibility = Visibility.Collapsed; + return; + } + + // 修改缓存 + DatapackItems.Clear(); + var itemsToShow = ModLocalComp.CompResourceListLoader.Output.ToList(); + + foreach (var DatapackEntity in itemsToShow) + DatapackItems[DatapackEntity.RawPath] = BuildLocalCompItem(DatapackEntity); + + // 显示结果 + ModBase.RunInUi(() => + { + Filter = FilterType.All; + SearchBox.Text = ""; // 这会触发结果刷新,所以需要在 DatapackItems 更新之后 + RefreshUI(); + SetSortMethod(SortMethod.CompName); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "加载数据包列表 UI 失败", ModBase.LogLevel.Feedback); + } + } + + private MyLocalCompItem BuildLocalCompItem(ModLocalComp.LocalCompFile Entry) + { + try + { + ModAnimation.AniControlEnabled += 1; + var NewItem = new MyLocalCompItem + { + SnapsToDevicePixels = true, + Entry = Entry, + ButtonHandler = BuildLocalCompItemBtnHandler, + Checked = SelectedDatapacks.Contains(Entry.RawPath) + }; + NewItem.CurrentSwipe = CurrentSwipSelect; + NewItem.Tags = Entry.Tags; + Entry.OnCompUpdate += _ => NewItem.Refresh(); + NewItem.Refresh(); + ModAnimation.AniControlEnabled -= 1; + return NewItem; + } + catch (Exception ex) + { + ModAnimation.AniControlEnabled -= 1; + ModBase.Log(ex, $"创建 UI 项失败:{Entry.RawPath}"); + throw; + } + } + + private void BuildLocalCompItemBtnHandler(MyLocalCompItem sender, EventArgs e) + { + // 点击事件 + sender.Changed += (ss, e) => CheckChanged((MyLocalCompItem)ss, e); + + // 文件项的点击事件:切换选中状态 + sender.Click += (ss, e) => + { + var s = (MyLocalCompItem)ss; + s.Checked = !s.Checked; + }; + + // 图标按钮 + var BtnOpen = new MyIconButton { LogoScale = 1.05d, Logo = ModBase.Logo.IconButtonOpen, Tag = sender }; + BtnOpen.ToolTip = "打开文件位置"; + ToolTipService.SetPlacement(BtnOpen, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnOpen, 30d); + ToolTipService.SetHorizontalOffset(BtnOpen, 2d); + BtnOpen.Click += (sender, e) => Open_Click((MyIconButton)sender, e); + + var BtnCont = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonInfo, Tag = sender }; + BtnCont.ToolTip = "详情"; + ToolTipService.SetPlacement(BtnCont, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnCont, 30d); + ToolTipService.SetHorizontalOffset(BtnCont, 2d); + BtnCont.Click += Info_Click; + sender.MouseRightButtonUp += Info_Click; + + var BtnDelete = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonDelete, Tag = sender }; + BtnDelete.ToolTip = "删除"; + ToolTipService.SetPlacement(BtnDelete, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnDelete, 30d); + ToolTipService.SetHorizontalOffset(BtnDelete, 2d); + BtnDelete.Click += (sender, e) => Delete_Click((MyIconButton)sender, e); + + if (sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + { + var BtnDisable = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonStop, Tag = sender }; + BtnDisable.ToolTip = "禁用"; + ToolTipService.SetPlacement(BtnDisable, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnDisable, 30d); + ToolTipService.SetHorizontalOffset(BtnDisable, 2d); + BtnDisable.Click += (ss, e) => Disable_Click((MyIconButton)ss, e); + sender.Buttons = new[] { BtnCont, BtnOpen, BtnDisable, BtnDelete }; + } + else if (sender.Entry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) + { + var BtnEnable = new MyIconButton { LogoScale = 1d, Logo = ModBase.Logo.IconButtonCheck, Tag = sender }; + BtnEnable.ToolTip = "启用"; + ToolTipService.SetPlacement(BtnEnable, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnEnable, 30d); + ToolTipService.SetHorizontalOffset(BtnEnable, 2d); + BtnEnable.Click += (ss, e) => Enable_Click((MyIconButton)ss, e); + sender.Buttons = new[] { BtnCont, BtnOpen, BtnEnable, BtnDelete }; + } + else + { + sender.Buttons = new[] { BtnCont, BtnOpen, BtnDelete }; + } + } + + /// + /// 刷新整个 UI。 + /// + public void RefreshUI() + { + if (PanList is null) + return; + var ShowingDatapacks = (IsSearching ? SearchResult : DatapackItems.Values.Select(i => i.Entry)) + .Where(m => CanPassFilter(m)).ToList(); + + // 对显示的数据包进行排序 + if (ShowingDatapacks.Any()) + { + var sortMethod = GetSortMethod(CurrentSortMethod); + ShowingDatapacks.Sort((a, b) => sortMethod(a, b)); + } + + // 重新列出列表 + ModAnimation.AniControlEnabled += 1; + if (ShowingDatapacks.Any()) + { + PanList.Visibility = Visibility.Visible; + PanList.Children.Clear(); + foreach (var TargetDatapack in ShowingDatapacks) + { + if (!DatapackItems.ContainsKey(TargetDatapack.RawPath)) + continue; + var Item = DatapackItems[TargetDatapack.RawPath]; + + // 确保元素没有父容器,避免重复添加异常 + if (Item.Parent is not null) ((Panel)Item.Parent).Children.Remove(Item); + + ModStyle.MinecraftFormatter.SetColorfulTextLab(Item.LabTitle.Text, Item.LabTitle, + ThemeService.IsDarkMode); + ModStyle.MinecraftFormatter.SetColorfulTextLab(Item.LabInfo.Text, Item.LabInfo, + ThemeService.IsDarkMode); + Item.Checked = SelectedDatapacks.Contains(TargetDatapack.RawPath); // 更新选中状态 + PanList.Children.Add(Item); + } + } + else + { + PanList.Visibility = Visibility.Collapsed; + } + + ModAnimation.AniControlEnabled -= 1; + SelectedDatapacks = + new HashSet(SelectedDatapacks.Where(m => + ShowingDatapacks.Any(s => (s.RawPath ?? "") == (m ?? "")))); + RefreshBars(); + } + + /// + /// 刷新顶栏和底栏显示。 + /// + public void RefreshBars() + { + Dispatcher.BeginInvoke(new Func(async () => + { + // ----------------- + // 顶部栏 + // ----------------- + + // 计数 + var AnyCount = 0; + var EnabledCount = 0; + var DisabledCount = 0; + var UpdateCount = 0; + var UnavalialeCount = 0; + var ItemSource = (IsSearching ? SearchResult : DatapackItems.Values.Select(i => i.Entry)).ToArray(); + await Task.Run(() => + { + foreach (var item in ItemSource) + { + AnyCount += 1; + if (item.CanUpdate) UpdateCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) EnabledCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) DisabledCount += 1; + if (item.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) UnavalialeCount += 1; + } + }); + // 显示 + BtnFilterAll.Text = (IsSearching ? "搜索结果" : "全部") + $" ({AnyCount})"; + BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"; + BtnFilterCanUpdate.Visibility = Filter == FilterType.CanUpdate || UpdateCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterEnabled.Text = $"启用 ({EnabledCount})"; + BtnFilterEnabled.Visibility = Filter == FilterType.Enabled || (EnabledCount > 0 && EnabledCount < AnyCount) + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterDisabled.Text = $"禁用 ({DisabledCount})"; + BtnFilterDisabled.Visibility = Filter == FilterType.Disabled || DisabledCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + BtnFilterError.Text = $"错误 ({UnavalialeCount})"; + BtnFilterError.Visibility = Filter == FilterType.Unavailable || UnavalialeCount > 0 + ? Visibility.Visible + : Visibility.Collapsed; + + // ----------------- + // 底部栏 + // ----------------- + + // 计数 + var NewCount = SelectedDatapacks.Count; + var Selected = NewCount > 0; + if (Selected) + LabSelect.Text = $"已选择 {NewCount} 个文件"; + + // 按钮可用性 + if (Selected) + { + var HasUpdate = false; + var HasEnabled = false; + var HasDisabled = false; + var CanFavoriteAndShare = true; + + + // 检查是否所有选中的数据包都有有效的项目信息 + await Task.Run(() => + { + foreach (var DatapackEntity in ModLocalComp.CompResourceListLoader.Output) + if (SelectedDatapacks.Contains(DatapackEntity.RawPath)) + { + if (DatapackEntity.CanUpdate) HasUpdate = true; + if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + HasEnabled = true; + else if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled) + HasDisabled = true; + if (DatapackEntity.Comp is null || string.IsNullOrEmpty(DatapackEntity.Comp.Id)) + CanFavoriteAndShare = false; + } + }); + + BtnSelectDisable.IsEnabled = HasEnabled; + BtnSelectEnable.IsEnabled = HasDisabled; + BtnSelectUpdate.IsEnabled = HasUpdate; + BtnSelectFavorites.IsEnabled = CanFavoriteAndShare; + BtnSelectShare.IsEnabled = CanFavoriteAndShare; + } + + // 更新显示状态 + if (ModAnimation.AniControlEnabled == 0) + { + PanListBack.Margin = new Thickness(0d, 0d, 0d, Selected ? 95 : 15); + if (Selected) + { + // 仅在数量增加时播放出现/跳跃动画 + if (BottomBarShownCount >= NewCount) + { + BottomBarShownCount = NewCount; + return; + } + + BottomBarShownCount = NewCount; + // 出现/跳跃动画 + CardSelect.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, 1d - CardSelect.Opacity, 60), + ModAnimation.AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, 3d, 150, 120, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaTranslateY(CardSelect, -1, 90, 270, + new ModAnimation.AniEaseInoutFluent(ModAnimation.AniEasePower.Weak)) + }, "Datapack Sidebar"); + } + else + { + // 不重复播放隐藏动画 + if (BottomBarShownCount == 0) + return; + BottomBarShownCount = 0; + // 隐藏动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(CardSelect, -CardSelect.Opacity, 90), + ModAnimation.AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, + Ease: new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => CardSelect.Visibility = Visibility.Collapsed, After: true) + }, "Datapack Sidebar"); + } + } + else + { + ModAnimation.AniStop("Datapack Sidebar"); + BottomBarShownCount = NewCount; + if (Selected) + { + CardSelect.Visibility = Visibility.Visible; + CardSelect.Opacity = 1d; + TransSelect.Y = -25; + } + else + { + CardSelect.Visibility = Visibility.Collapsed; + CardSelect.Opacity = 0d; + TransSelect.Y = -10; + } + } + })); + } + + private int BottomBarShownCount; + + #endregion + + #region 管理 + + /// + /// 打开 datapacks 文件夹。 + /// + private void BtnManageOpen_Click(object sender, EventArgs e) + { + try + { + var DatapackPath = PageInstanceSavesLeft.CurrentSave + @"\datapacks\"; + Directory.CreateDirectory(DatapackPath); + ModBase.OpenExplorer(DatapackPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开 datapacks 文件夹失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 全选。 + /// + private void BtnManageSelectAll_Click(object sender, MouseButtonEventArgs e) + { + ChangeAllSelected(SelectedDatapacks.Count < PanList.Children.Count); + } + + /// + /// 安装数据包。 + /// + private void BtnManageInstall_Click(object sender, MouseButtonEventArgs e) + { + var FileList = SystemDialogs.SelectFiles("数据包文件(*.zip)|*.zip", "选择要安装的数据包"); + if (FileList is null || !FileList.Any()) + return; + InstallDatapackFiles(FileList); + Refresh(); + } + + /// + /// 安装数据包文件。 + /// + public static void InstallDatapackFiles(IEnumerable FilePathList) + { + if (!FilePathList.Any()) + return; + + var Extension = FilePathList.First().AfterLast(".").ToLower(); + + // 检查文件扩展名 + if (Extension != "zip") + { + ModMain.Hint($"不支持的文件格式:{Extension},数据包支持的格式:zip", ModMain.HintType.Critical); + return; + } + + // 检查回收站 + if (FilePathList.First().Contains(@":\$RECYCLE.BIN\")) + { + ModMain.Hint("请先将文件从回收站还原,再尝试安装!", ModMain.HintType.Critical); + return; + } + + ModBase.Log($"[System] 文件为 {Extension} 格式,尝试作为数据包安装"); + + // 确认安装 + if (!(ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmMain.PageCurrentSub == FormMain.PageSubType.VersionSavesDatapack)) + if (ModMain.MyMsgBox($"是否要将这{(FilePathList.Count() == 1 ? "个" : "些")}文件作为数据包安装到当前存档?", "数据包安装确认", "确定", + "取消") != 1) + return; + + // 执行安装 + try + { + var DatapackFolder = PageInstanceSavesLeft.CurrentSave + @"\datapacks\"; + Directory.CreateDirectory(DatapackFolder); + + foreach (var FilePath in FilePathList) + { + var NewFileName = ModBase.GetFileNameFromPath(FilePath); + var DestFile = DatapackFolder + NewFileName; + + if (File.Exists(DestFile)) + if (ModMain.MyMsgBox($"已存在同名文件:{NewFileName},是否要覆盖?", "文件覆盖确认", "覆盖", "取消") != 1) + continue; + + ModBase.CopyFile(FilePath, DestFile); + } + + if (FilePathList.Count() == 1) + ModMain.Hint($"已安装 {ModBase.GetFileNameFromPath(FilePathList.First())}!", ModMain.HintType.Finish); + else + ModMain.Hint($"已安装 {FilePathList.Count()} 个数据包!", ModMain.HintType.Finish); + + // 刷新列表 + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmMain.PageCurrentSub == FormMain.PageSubType.VersionSavesDatapack) + if (ModMain.FrmInstanceSavesDatapack is not null) + ModMain.FrmInstanceSavesDatapack.ReloadDatapackFileList(true); + } + + catch (Exception ex) + { + ModBase.Log(ex, "复制数据包文件失败", ModBase.LogLevel.Msgbox); + } + } + + /// + /// 下载数据包。 + /// + private void BtnManageDownload_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadDataPack); + PageComp.TargetVersion = PageInstanceLeft.Instance; // 将当前实例设置为筛选器 + } + + /// + /// 导出信息。 + /// + private void BtnManageInfoExport_Click(object sender, MouseButtonEventArgs e) + { + var Choice = + ModMain.MyMsgBox("TXT 格式:仅导出当前的数据包文件名称信息" + "\r\n" + "CSV 格式:导出详细的数据包信息,包括文件名、工程 ID、版本信息等详细信息", + "选择导出模式", "TXT 格式", "CSV 格式", "取消"); + + void ExportText(string Content, string FileName) + { + try + { + var savePath = + SystemDialogs.SelectSaveFile("选择保存位置", FileName, "文本文件(*.txt)|*.txt|CSV 文件(*.csv)|*.csv"); + if (string.IsNullOrWhiteSpace(savePath)) return; + File.WriteAllText(savePath, Content, Encoding.UTF8); + ModBase.OpenExplorer(savePath); + } + catch (Exception ex) + { + ModBase.Log(ex, "导出数据包信息失败", ModBase.LogLevel.Msgbox); + } + } + + ; + switch (Choice) + { + case 1: // TXT + { + var ExportContent = new List(); + foreach (var DatapackEntity in ModLocalComp.CompResourceListLoader.Output) + ExportContent.Add(DatapackEntity.FileName); + ExportText(ExportContent.Join("\r\n"), + ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.CurrentSave) + "的数据包信息.txt"); + break; + } + + case 2: // CSV + { + var ExportContent = new List(); + ExportContent.Add("文件名,数据包名称,数据包版本,此版本更新时间,工程 ID,文件大小(字节),文件路径"); + foreach (var DatapackEntity in ModLocalComp.CompResourceListLoader.Output) + ExportContent.Add( + $"{DatapackEntity.FileName},{DatapackEntity.Comp?.TranslatedName},{DatapackEntity.Version},{DatapackEntity.CompFile?.ReleaseDate},{DatapackEntity.Comp?.Id},{GetDatapackFileInfo(DatapackEntity.Path).Length},{DatapackEntity.Path}"); + ExportText(ExportContent.Join("\r\n"), + ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.CurrentSave) + "的数据包信息.csv"); + break; + } + } + } + + #endregion + + #region 选择 + + /// + /// 选择的数据包的路径。 + /// + public HashSet SelectedDatapacks = new(); + + // 单项切换选择状态 + public void CheckChanged(MyLocalCompItem sender, ModBase.RouteEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + // 更新选择了的内容 + var SelectedKey = sender.Entry.RawPath; + if (sender.Checked) + SelectedDatapacks.Add(SelectedKey); + else + SelectedDatapacks.Remove(SelectedKey); + RefreshBars(); + } + + // 切换所有项的选择状态 + private void ChangeAllSelected(bool Value) + { + ModAnimation.AniControlEnabled += 1; + SelectedDatapacks.Clear(); + foreach (var Item in DatapackItems.Values) + { + var ShouldSelected = Value && PanList.Children.Contains(Item); + Item.Checked = ShouldSelected; + if (ShouldSelected) + SelectedDatapacks.Add(Item.Entry.RawPath); + } + + ModAnimation.AniControlEnabled -= 1; + RefreshBars(); + } + + private void UnselectedAllWithAnimation() + { + var CacheAniControlEnabled = ModAnimation.AniControlEnabled; + ModAnimation.AniControlEnabled = 0; + ChangeAllSelected(false); + ModAnimation.AniControlEnabled += CacheAniControlEnabled; + } + + private void FrmMain_KeyDown(object sender, KeyEventArgs e) + { + if (!ReferenceEquals(ModMain.FrmMain.PageRight, this)) + return; + if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A) + ChangeAllSelected(true); + } + + private void SearchBox_PreviewKeyDown(object sender, KeyEventArgs e) + { + // Ctrl + A 会被搜索框捕获,导致无法全选,所以在按下 Ctrl + A 时转移焦点以便捕获 + if (SearchBox.Text.Any()) + return; + if ((Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) && e.Key == Key.A) + PanBack.Focus(); + } + + #endregion + + #region 筛选 + + private FilterType _Filter = FilterType.All; + + public FilterType Filter + { + get => _Filter; + set + { + if (_Filter == value) + return; + _Filter = value; + switch (value) + { + case FilterType.All: + { + BtnFilterAll.Checked = true; + break; + } + case FilterType.Enabled: + { + BtnFilterEnabled.Checked = true; + break; + } + case FilterType.Disabled: + { + BtnFilterDisabled.Checked = true; + break; + } + case FilterType.CanUpdate: + { + BtnFilterCanUpdate.Checked = true; + break; + } + + default: + { + BtnFilterError.Checked = true; + break; + } + } + + RefreshUI(); + } + } + + public enum FilterType + { + All = 0, + Enabled = 1, + Disabled = 2, + CanUpdate = 3, + Unavailable = 4 + } + + /// + /// 检查该数据包项是否符合当前筛选的类别。 + /// + private bool CanPassFilter(ModLocalComp.LocalCompFile CheckingDatapack) + { + switch (Filter) + { + case FilterType.All: + { + return true; + } + case FilterType.Enabled: + { + return CheckingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine; + } + case FilterType.Disabled: + { + return CheckingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled; + } + case FilterType.CanUpdate: + { + return CheckingDatapack.CanUpdate; + } + case FilterType.Unavailable: + { + return CheckingDatapack.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable; + } + + default: + { + return false; + } + } + } + + // 点击筛选项触发的改变 + private void ChangeFilter(MyRadioButton sender, bool raiseByMouse) + { + Filter = (FilterType)Conversions.ToInteger(sender.Tag); + RefreshUI(); + DoSort(); + } + + #endregion + + #region 排序 + + private SortMethod CurrentSortMethod = SortMethod.CompName; + + private void SetSortMethod(SortMethod Target) + { + CurrentSortMethod = Target; + BtnSort.Text = $"排序:{GetSortName(Target)}"; + DoSort(); + } + + private enum SortMethod + { + FileName, + CompName, + CreateTime, + DatapackFileSize + } + + private string GetSortName(SortMethod Method) + { + switch (Method) + { + case SortMethod.FileName: + { + return "文件名"; + } + case SortMethod.CompName: + { + return "资源名称"; + } + case SortMethod.CreateTime: + { + return "加入时间"; + } + case SortMethod.DatapackFileSize: + { + return "文件大小"; + } + + default: + { + return "资源名称"; + } + } + + return ""; + } + + private void BtnSortClick(object sender, ModBase.RouteEventArgs e) + { + var Body = new ContextMenu(); + foreach (SortMethod i in Enum.GetValues(typeof(SortMethod))) + { + var Item = new MyMenuItem(); + Item.Header = GetSortName(i); + Item.Click += (_, _) => SetSortMethod(i); + Body.Items.Add(Item); + } + + Body.PlacementTarget = (UIElement)sender; + Body.Placement = PlacementMode.Bottom; + Body.IsOpen = true; + } + + private readonly object SortLock = new(); + + private void DoSort() + { + lock (SortLock) + { + try + { + if (PanList is null || PanList.Children.Count < 2) + return; + + // 将子元素转换为可排序的列表 + var items = PanList.Children.OfType().ToList(); + var Method = GetSortMethod(CurrentSortMethod); + + // 分离有效和无效项(保持原始相对顺序) + var invalid = items.Where(i => i.Entry is null).ToList(); + var valid = items.Except(invalid).ToList(); + // 仅对有效项进行排序 + valid.Sort((x, y) => Method(x.Entry, y.Entry)); + // 合并保持无效项的原始顺序 + items = valid.Concat(invalid).ToList(); + + // 批量更新UI元素 + PanList.Children.Clear(); + items.ForEach(i => PanList.Children.Add(i)); + } + + catch (Exception ex) + { + ModBase.Log(ex, "执行排序时出错", ModBase.LogLevel.Hint); + } + } + } + + private Func GetSortMethod(SortMethod Method) + { + switch (Method) + { + case SortMethod.FileName: + { + return (a, b) => string.Compare(a.FileName, b.FileName, StringComparison.OrdinalIgnoreCase); + } + case SortMethod.CompName: + { + return (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + } + case SortMethod.CreateTime: + { + return (a, b) => + { + var aDate = GetDatapackFileInfo(a.Path).CreationTime; + var bDate = GetDatapackFileInfo(b.Path).CreationTime; + if (aDate == DateTime.MinValue && bDate == DateTime.MinValue) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + + if (aDate == DateTime.MinValue) return 1; + + if (bDate == DateTime.MinValue) return -1; + return bDate.CompareTo(aDate); + }; + } + case SortMethod.DatapackFileSize: + { + return (a, b) => + { + var aSize = GetDatapackFileInfo(a.Path).Length; + var bSize = GetDatapackFileInfo(b.Path).Length; + if (aSize == 0L && bSize == 0L) + return string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + + if (aSize == 0L) return 1; + + if (bSize == 0L) return -1; + return bSize.CompareTo(aSize); + }; + } + + default: + { + return (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase); + } + } + } + + #endregion + + #region 下边栏 + + // 启用 + private void BtnSelectEnable_Click(object sender, ModBase.RouteEventArgs e) + { + ToggleDatapacks( + ModLocalComp.CompResourceListLoader.Output.Where(m => SelectedDatapacks.Contains(m.RawPath)).ToList(), + true); + ChangeAllSelected(false); + } + + // 禁用 + private void BtnSelectDisable_Click(object sender, ModBase.RouteEventArgs e) + { + ToggleDatapacks( + ModLocalComp.CompResourceListLoader.Output.Where(m => SelectedDatapacks.Contains(m.RawPath)).ToList(), + false); + ChangeAllSelected(false); + } + + /// + /// 启用/禁用数据包(通过重命名文件夹为 .disabled) + /// + private void ToggleDatapacks(IEnumerable DatapackList, bool IsEnable) + { + var IsSuccessful = true; + foreach (var DatapackE in DatapackList) + { + var DatapackEntity = DatapackE; + string NewPath = null; + + if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine && !IsEnable) + // 禁用 - 添加 .disabled 后缀 + NewPath = DatapackEntity.Path + ".disabled"; + else if (DatapackEntity.State == ModLocalComp.LocalCompFile.LocalFileStatus.Disabled && IsEnable) + // 启用 - 移除 .disabled 后缀 + NewPath = DatapackEntity.RawPath; + else + continue; + + // 重命名 + try + { + if (File.Exists(NewPath)) + { + ModMain.MyMsgBox($"已存在同名文件:{ModBase.GetFileNameFromPath(NewPath)},请先处理该文件再重试。"); + continue; + } + + FileSystem.Rename(DatapackEntity.Path, NewPath); + } + catch (FileNotFoundException ex) + { + ModBase.Log(ex, $"未找到需要重命名的数据包({DatapackEntity.Path ?? "null"})", ModBase.LogLevel.Feedback); + ReloadDatapackFileList(true); + return; + } + catch (Exception ex) + { + ModBase.Log(ex, $"重命名数据包失败({DatapackEntity.Path ?? "null"})"); + IsSuccessful = false; + } + + // 更改 Loader 中的列表 + var NewDatapackEntity = new ModLocalComp.LocalCompFile(NewPath); + NewDatapackEntity.FromJson(DatapackEntity.ToJson()); + if (ModLocalComp.CompResourceListLoader.Output.Contains(DatapackEntity)) + { + var IndexOfLoader = ModLocalComp.CompResourceListLoader.Output.IndexOf(DatapackEntity); + ModLocalComp.CompResourceListLoader.Output.RemoveAt(IndexOfLoader); + ModLocalComp.CompResourceListLoader.Output.Insert(IndexOfLoader, NewDatapackEntity); + } + + if (SearchResult is not null && SearchResult.Contains(DatapackEntity)) + { + var IndexOfResult = SearchResult.IndexOf(DatapackEntity); + SearchResult.Remove(DatapackEntity); + SearchResult.Insert(IndexOfResult, NewDatapackEntity); + } + + // 更改 UI 中的列表 + try + { + var NewItem = BuildLocalCompItem(NewDatapackEntity); + DatapackItems[DatapackEntity.RawPath] = NewItem; + var IndexOfUi = PanList.Children.IndexOf(PanList.Children.OfType() + .FirstOrDefault(i => ReferenceEquals(i.Entry, DatapackEntity))); + if (IndexOfUi == -1) + continue; + PanList.Children.RemoveAt(IndexOfUi); + PanList.Children.Insert(IndexOfUi, NewItem); + } + catch (Exception ex) + { + ModBase.Log(ex, $"更新 UI 列表项失败:{DatapackEntity.FileName}", ModBase.LogLevel.Hint); + } + } + + Dispatcher.Invoke(() => PanList.UpdateLayout(), DispatcherPriority.Background); + + if (IsSuccessful) + { + RefreshBars(); + } + else + { + ModMain.Hint("由于文件被占用,数据包的状态切换失败,请尝试关闭正在运行的游戏后再试!", ModMain.HintType.Critical); + ReloadDatapackFileList(true); + } + + LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly); + } + + // 更新 + private void BtnSelectUpdate_Click(object sender, ModBase.RouteEventArgs e) + { + var UpdateList = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedDatapacks.Contains(m.RawPath) && m.CanUpdate).ToList(); + if (!UpdateList.Any()) + return; + UpdateResource(UpdateList); + ChangeAllSelected(false); + } + + /// + /// 记录正在进行数据包更新的 datapacks 文件夹路径。 + /// + public static List UpdatingVersions = new(); + + public void UpdateResource(IEnumerable DatapackList) + { + // 更新前警告 + if (Conversions.ToBoolean(!States.Hint.FunctionDatapackUpdate || DatapackList.Count() >= 15)) + { + if (ModMain.MyMsgBox( + $"新版本数据包可能不兼容旧存档或者其他数据包,这可能导致游戏崩溃或存档损坏!{"\r\n"}{"\r\n"}在更新前,请先备份存档。{"\r\n"}如果更新后出现问题,你也可以在回收站找回更新前的数据包。", + "数据包更新警告", "我已了解风险,继续更新", "取消", IsWarn: true) == 1) + States.Hint.FunctionDatapackUpdate = true; + else + return; + } + + try + { + // 构造下载信息 + DatapackList = DatapackList.ToList(); // 防止刷新影响迭代器 + var FileList = new List(); + var FileCopyList = new Dictionary(); + foreach (var Entry in DatapackList) + { + var File = Entry.UpdateFile; + if (!File.Available) + continue; + // 添加到下载列表 + var TempAddress = ModBase.PathTemp + @"DownloadedComp\" + File.FileName; + var RealAddress = PageInstanceSavesLeft.CurrentSave + @"\datapacks\" + File.FileName; + FileList.Add(File.ToNetFile(TempAddress)); + FileCopyList[TempAddress] = RealAddress; + } + + // 构造加载器 + var InstallLoaders = new List(); + var FinishedFileNames = new List(); + InstallLoaders.Add(new ModNet.LoaderDownload("下载新版数据包文件", FileList) + { ProgressWeight = DatapackList.Count() * 1.5d }); + + InstallLoaders.Add(new ModLoader.LoaderTask("替换旧版数据包文件", _ => + { + try + { + foreach (var Entry in DatapackList) + if (File.Exists(Entry.Path)) + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Path, UIOption.AllDialogs, + RecycleOption.SendToRecycleBin); + else + ModBase.Log($"[DatapackUpdate] 未找到更新前的数据包文件,跳过对它的删除:{Entry.Path}", ModBase.LogLevel.Debug); + + foreach (var Entry in FileCopyList) + { + if (File.Exists(Entry.Value)) + { + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(Entry.Value, UIOption.AllDialogs, + RecycleOption.SendToRecycleBin); + ModBase.Log($"[Datapack] 更新后的数据包文件已存在,将会把它放入回收站:{Entry.Value}", ModBase.LogLevel.Debug); + } + + if (Directory.Exists(ModBase.GetPathFromFullPath(Entry.Value))) + { + File.Move(Entry.Key, Entry.Value); + FinishedFileNames.Add(ModBase.GetFileNameFromPath(Entry.Value)); + } + else + { + ModBase.Log($"[Datapack] 更新后的目标文件夹已被删除:{Entry.Value}", ModBase.LogLevel.Debug); + } + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "替换旧版数据包文件时被主动取消"); + } + })); + + // 结束处理 + var Loader = new ModLoader.LoaderCombo>( + $"数据包更新:{ModBase.GetFolderNameFromPath(PageInstanceSavesLeft.CurrentSave)}", InstallLoaders); + var PathDatapacks = PageInstanceSavesLeft.CurrentSave + @"\datapacks\"; + + Loader.OnStateChanged = _ => + { + switch (Loader.State) + { + case ModBase.LoadState.Finished: + { + switch (FinishedFileNames.Count) + { + case 0: + { + ModBase.Log("[DatapackUpdate] 没有数据包被成功更新"); + break; + } + case 1: + { + ModMain.Hint($"已成功更新 {FinishedFileNames.Single()}!", ModMain.HintType.Finish); + break; + } + + default: + { + ModMain.Hint($"已成功更新 {FinishedFileNames.Count} 个数据包!", ModMain.HintType.Finish); + break; + } + } + + break; + } + case ModBase.LoadState.Failed: + { + ModMain.Hint("数据包更新失败:" + Loader.Error.Message, ModMain.HintType.Critical); + break; + } + case ModBase.LoadState.Aborted: + { + ModMain.Hint("数据包更新已中止!"); + break; + } + + default: + { + return; + } + } + + ModBase.Log($"[DatapackUpdate] 已从正在进行数据包更新的文件夹列表移除:{PathDatapacks}"); + UpdatingVersions.Remove(PathDatapacks); + + // 清理缓存 + ModBase.RunInNewThread(() => + { + try + { + foreach (var TempFile in FileCopyList.Keys) + if (File.Exists(TempFile)) + File.Delete(TempFile); + } + catch (Exception ex) + { + ModBase.Log(ex, "清理数据包更新缓存失败"); + } + }, "Clean Datapack Update Cache", ThreadPriority.BelowNormal); + }; + + // 启动加载器 + ModBase.Log($"[DatapackUpdate] 开始更新 {DatapackList.Count()} 个数据包:{PathDatapacks}"); + UpdatingVersions.Add(PathDatapacks); + Loader.Start(); + ModLoader.LoaderTaskbarAdd(Loader); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + ReloadDatapackFileList(true); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化数据包更新失败"); + } + } + + // 删除 + private void BtnSelectDelete_Click(object sender, ModBase.RouteEventArgs e) + { + DeleteDatapacks(ModLocalComp.CompResourceListLoader.Output.Where(m => SelectedDatapacks.Contains(m.RawPath))); + ChangeAllSelected(false); + } + + private void DeleteDatapacks(IEnumerable DatapackList) + { + try + { + var IsSuccessful = true; + var IsShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + + // 确认需要删除的文件 + DatapackList = DatapackList.SelectMany(Target => + { + if (Target.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine) + return new[] { Target.Path, Target.Path + ".disabled" }; + + return new[] { Target.Path, Target.RawPath }; + }).Distinct().Where(m => File.Exists(m)).Select(m => new ModLocalComp.LocalCompFile(m)).ToList(); + + // 实际删除文件 + foreach (var DatapackEntity in DatapackList) + { + try + { + if (IsShiftPressed) + File.Delete(DatapackEntity.Path); + else + Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(DatapackEntity.Path, + UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除数据包被主动取消"); + ReloadDatapackFileList(true); + return; + } + catch (Exception ex) + { + ModBase.Log(ex, $"删除数据包失败({DatapackEntity.Path})", ModBase.LogLevel.Msgbox); + IsSuccessful = false; + } + + // 取消选中 + SelectedDatapacks.Remove(DatapackEntity.RawPath); + // 更改 Loader 和 UI 中的列表 + ModLocalComp.CompResourceListLoader.Output.Remove(DatapackEntity); + SearchResult?.Remove(DatapackEntity); + DatapackItems.Remove(DatapackEntity.RawPath); + var IndexOfUi = PanList.Children.IndexOf(PanList.Children.OfType() + .FirstOrDefault(i => i.Entry.Equals(DatapackEntity))); + if (IndexOfUi >= 0) + PanList.Children.RemoveAt(IndexOfUi); + } + + RefreshBars(); + if (!IsSuccessful) + { + ModMain.Hint("由于文件被占用,删除失败,请尝试关闭正在运行的游戏后再试!", ModMain.HintType.Critical); + ReloadDatapackFileList(true); + } + else if (PanList.Children.Count == 0) + { + ReloadDatapackFileList(true); + } + else + { + RefreshBars(); + } + + if (!IsSuccessful) + return; + if (IsShiftPressed) + { + if (DatapackList.Count() == 1) + ModMain.Hint($"已彻底删除 {DatapackList.Single().FileName}!", ModMain.HintType.Finish); + else + ModMain.Hint($"已彻底删除 {DatapackList.Count()} 个项目!", ModMain.HintType.Finish); + } + else if (DatapackList.Count() == 1) + { + ModMain.Hint($"已将 {DatapackList.Single().FileName} 删除到回收站!", ModMain.HintType.Finish); + } + else + { + ModMain.Hint($"已将 {DatapackList.Count()} 个项目删除到回收站!", ModMain.HintType.Finish); + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除数据包被主动取消"); + ReloadDatapackFileList(true); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除数据包出现未知错误", ModBase.LogLevel.Feedback); + ReloadDatapackFileList(true); + } + + LoaderRun(ModLoader.LoaderFolderRunType.UpdateOnly); + } + + // 取消选择 + private void BtnSelectCancel_Click(object sender, ModBase.RouteEventArgs e) + { + ChangeAllSelected(false); + } + + // 收藏 + private void BtnSelectFavorites_Click(object sender, ModBase.RouteEventArgs e) + { + var Selected = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedDatapacks.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp).ToList(); + ModComp.CompFavorites.ShowMenu(Selected, (UIElement)sender); + } + + // 分享 + private void BtnSelectShare_Click(object sender, ModBase.RouteEventArgs e) + { + var ShareList = ModLocalComp.CompResourceListLoader.Output + .Where(m => SelectedDatapacks.Contains(m.RawPath) && m.Comp is not null).Select(i => i.Comp.Id).ToHashSet(); + ModBase.ClipboardSet(ModComp.CompFavorites.GetShareCode(ShareList)); + ChangeAllSelected(false); + } + + #endregion + + #region 单个资源项 + + // 详情 + public void Info_Click(object sender, EventArgs e) + { + try + { + var DatapackEntry = ((MyLocalCompItem)(sender is MyIconButton ? ((dynamic)sender).Tag : sender)).Entry; + + // 加载失败信息 + if (DatapackEntry.State == ModLocalComp.LocalCompFile.LocalFileStatus.Unavailable) + { + ModMain.MyMsgBox( + "无法读取此数据包的信息。" + "\r\n" + "\r\n" + "详细的错误信息:" + + DatapackEntry.FileUnavailableReason.Message, "数据包读取失败"); + return; + } + + if (DatapackEntry.Comp is not null) + { + // 跳转到数据包下载页面 + ModMain.FrmMain.PageChange(new FormMain.PageStackData + { + Page = FormMain.PageType.CompDetail, + Additional = new object[] + { + DatapackEntry.Comp, new List(), PageInstanceLeft.Instance.Info.VanillaName, + ModComp.CompLoaderType.Minecraft, ModComp.CompType.DataPack + } + }); + } + else + { + // 获取信息 + var ContentLines = new List(); + + if (DatapackEntry.Description is not null) + ContentLines.Add(DatapackEntry.Description + "\r\n"); + if (DatapackEntry.Authors is not null) + ContentLines.Add("作者:" + DatapackEntry.Authors); + ContentLines.Add("文件:" + DatapackEntry.FileName + "(" + + ModBase.GetString(GetDatapackFileInfo(DatapackEntry.Path).Length) + ")"); + if (DatapackEntry.Version is not null) + ContentLines.Add("版本:" + DatapackEntry.Version); + + var DebugInfo = new List(); + if (DatapackEntry.ModId is not null) DebugInfo.Add("数据包 ID:" + DatapackEntry.ModId); + if (DebugInfo.Any()) + { + ContentLines.Add(""); + ContentLines.AddRange(DebugInfo); + } + + // 显示详情信息 + if (DatapackEntry.Url is null) + ModMain.MyMsgBox(ContentLines.Join("\r\n"), DatapackEntry.Name, "返回"); + else if (ModMain.MyMsgBox(ContentLines.Join("\r\n"), DatapackEntry.Name, "打开官网", "返回") == 1) + ModBase.OpenWebsite(DatapackEntry.Url); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取数据包详情失败", ModBase.LogLevel.Feedback); + } + } + + // 打开文件所在的位置 + public void Open_Click(MyIconButton sender, EventArgs e) + { + try + { + var ListItem = (MyLocalCompItem)sender.Tag; + ModBase.OpenExplorer(ListItem.Entry.Path); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开数据包文件位置失败", ModBase.LogLevel.Feedback); + } + } + + // 删除 + public void Delete_Click(MyIconButton sender, EventArgs e) + { + var ListItem = (MyLocalCompItem)sender.Tag; + DeleteDatapacks(new[] { ListItem.Entry }); + } + + // 启用 + public void Enable_Click(MyIconButton sender, EventArgs e) + { + var ListItem = (MyLocalCompItem)sender.Tag; + ToggleDatapacks(new[] { ListItem.Entry }, true); + } + + // 禁用 + public void Disable_Click(MyIconButton sender, EventArgs e) + { + var ListItem = (MyLocalCompItem)sender.Tag; + ToggleDatapacks(new[] { ListItem.Entry }, false); + } + + #endregion + + #region 搜索 + + public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text); + private List SearchResult; + + public void SearchRun(object sender, EventArgs e) + { + try + { + if (IsSearching) + { + // 构造请求 + var QueryList = new List>(); + foreach (var Entry in ModLocalComp.CompResourceListLoader.Output) + { + var SearchSource = new List>(); + SearchSource.Add(new KeyValuePair(Entry.Name, 1d)); + SearchSource.Add(new KeyValuePair(Entry.FileName, 1d)); + if (Entry.Version is not null) + SearchSource.Add(new KeyValuePair(Entry.Version, 0.2d)); + if (Entry.Description is not null && !string.IsNullOrEmpty(Entry.Description)) + SearchSource.Add(new KeyValuePair(Entry.Description, 0.4d)); + if (Entry.Comp is not null) + { + if ((Entry.Comp.RawName ?? "") != (Entry.Name ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.RawName, 1d)); + if ((Entry.Comp.TranslatedName ?? "") != (Entry.Comp.RawName ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.TranslatedName, 1d)); + if ((Entry.Comp.Description ?? "") != (Entry.Description ?? "")) + SearchSource.Add(new KeyValuePair(Entry.Comp.Description, 0.4d)); + SearchSource.Add(new KeyValuePair(string.Join("", Entry.Comp.Tags), 0.2d)); + } + + QueryList.Add(new ModBase.SearchEntry + { Item = Entry, SearchSource = SearchSource }); + } + + // 进行搜索 + SearchResult = ModBase.Search(QueryList, SearchBox.Text, 6, 0.35d).Select(r => r.Item).ToList(); + } + + RefreshUI(); + } + catch (Exception ex) + { + ModBase.Log(ex, "搜索过程中发生异常"); + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml index e35f7cadb..5c954f63a 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml @@ -1,33 +1,30 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" x:Class="PCL.PageInstanceSavesInfo" + PanScroll="{Binding ElementName=PanBack}"> - - - + + + - - - - - + + + - - - - - + + + diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml.cs new file mode 100644 index 000000000..8290dd91f --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesInfo.xaml.cs @@ -0,0 +1,503 @@ +using System.IO; +using System.Windows; +using System.Windows.Controls; +using fNbt; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class PageInstanceSavesInfo : IRefreshable +{ + private bool _loaded; + + public PageInstanceSavesInfo() + { + InitializeComponent(); + Loaded += (_, _) => Init(); + } + + void IRefreshable.Refresh() + { + IRefreshable_Refresh(); + } + + private void IRefreshable_Refresh() + { + Refresh(); + } + + public void Refresh() + { + RefreshInfo(); + } + + private void Init() + { + PanBack.ScrollToHome(); + + RefreshInfo(); + + _loaded = true; + if (_loaded) + return; + } + + private void RefreshInfo() + { + try + { + var saveDatPath = Path.Combine(PageInstanceSavesLeft.CurrentSave, "level.dat"); + using (var fs = new FileStream(saveDatPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var saveInfo = new NbtFile(); + saveInfo.LoadFromStream(fs, NbtCompression.AutoDetect); + ClearInfoTable(); + PanSettingsList.Children.Clear(); + PanSettingsList.RowDefinitions.Clear(); + + Hintversion1_9.Visibility = Visibility.Collapsed; + Hintversion1_8.Visibility = Visibility.Collapsed; + Hintversion1_3.Visibility = Visibility.Collapsed; + PanSettings.Visibility = Visibility.Collapsed; + + var gameLevel = saveInfo.RootTag.Get("Data"); + AddInfoTable("存档名称", gameLevel.Get("LevelName").Value); + NbtString versionName = null; + NbtInt versionId = null; + var gameVersion = gameLevel.Get("Version"); + if (gameVersion is not null) + { + gameVersion.TryGet("Name", out versionName); + gameVersion.TryGet("Id", out versionId); + } + + var CurrentVersionId = versionId?.Value ?? default(int?); + ModMain.FrmInstanceSavesLeft.ItemDatapack.Visibility = + !CurrentVersionId.HasValue || CurrentVersionId < 1444 ? Visibility.Collapsed : Visibility.Visible; + + var hasDifficulty = gameLevel.Contains("Difficulty"); + var hasAllowCommands = gameLevel.Contains("allowCommands"); + + if (versionName is null) + { + if (hasDifficulty) + { + Hintversion1_9.Visibility = Visibility.Visible; + Hintversion1_9.Text = "1.9 以下的版本无法获取存档版本"; + } + else if (hasAllowCommands) + { + Hintversion1_8.Visibility = Visibility.Visible; + Hintversion1_8.Text = "1.8 以下的版本无法获取存档版本和游戏难度"; + } + else + { + Hintversion1_3.Visibility = Visibility.Visible; + Hintversion1_3.Text = "1.3 以下的版本无法获取存档版本、游戏难度和是否允许作弊"; + } + } + else + { + AddInfoTable("存档版本", $"{versionName.Value} ({versionId.Value})"); + } + + NbtLong seedNbt = null; + string seed; + if (gameLevel.TryGet("RandomSeed", out seedNbt)) + seed = seedNbt.Value.ToString(); + else + seed = gameLevel.Get("WorldGenSettings").Get("seed").Value.ToString(); + + AddInfoTable("种子", seed, true, versionName?.Value, true); + + if (hasAllowCommands) + { + PanSettings.Visibility = Visibility.Visible; + var allowCommandValue = int.Parse(gameLevel.Get("allowCommands").Value.ToString()); + var combo = new MyComboBox + { + Width = 100d, HorizontalAlignment = HorizontalAlignment.Left, + ToolTip = "修改设置前请确保该存档未在游戏中打开,否则会导致设置无效" + }; + combo.Items.Add(new { Value = 0, Display = "不允许" }); + combo.Items.Add(new { Value = 1, Display = "允许" }); + combo.SelectedValuePath = "Value"; + combo.DisplayMemberPath = "Display"; + combo.SelectedValue = allowCommandValue; + + combo.SelectionChanged += (s, e) => + { + try + { + var newVal = Conversions.ToInteger(combo.SelectedValue); + gameLevel.Get("allowCommands").Value = (byte)newVal; + using (var fileStream = new FileStream(saveDatPath, FileMode.Create, FileAccess.Write, + FileShare.None)) + { + saveInfo.SaveToStream(fileStream, NbtCompression.GZip); + } + + ModMain.Hint("作弊设置修改成功", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "作弊设置修改失败", ModBase.LogLevel.Hint); + } + }; + var rowIndex = PanSettingsList.RowDefinitions.Count; + PanSettingsList.RowDefinitions.Add(new RowDefinition + { Height = new GridLength(1d, GridUnitType.Auto) }); + + var headTextBlock = new TextBlock { Text = "是否允许作弊", Margin = new Thickness(0d, 3d, 0d, 3d) }; + Grid.SetRow(headTextBlock, rowIndex); + Grid.SetColumn(headTextBlock, 0); + + Grid.SetRow(combo, rowIndex); + Grid.SetColumn(combo, 2); + + PanSettingsList.Children.Add(headTextBlock); + PanSettingsList.Children.Add(combo); + PanSettingsList.RowDefinitions.Add(new RowDefinition + { Height = new GridLength(8d, GridUnitType.Pixel) }); + } + + if (hasDifficulty) + { + PanSettings.Visibility = Visibility.Visible; + var difficultyElement = gameLevel.Get("Difficulty"); + var difficultyValue = int.Parse(difficultyElement.Value.ToString()); + + var difficultyCombo = new MyComboBox + { + Width = 100d, HorizontalAlignment = HorizontalAlignment.Left, + ToolTip = "修改设置前请确保该存档未在游戏中打开,否则会导致设置无效" + }; + difficultyCombo.Items.Add(new { Value = 0, Display = "和平" }); + difficultyCombo.Items.Add(new { Value = 1, Display = "简单" }); + difficultyCombo.Items.Add(new { Value = 2, Display = "普通" }); + difficultyCombo.Items.Add(new { Value = 3, Display = "困难" }); + difficultyCombo.SelectedValuePath = "Value"; + difficultyCombo.DisplayMemberPath = "Display"; + difficultyCombo.SelectedValue = difficultyValue; + + var isHardcoreCheck = gameLevel.Get("hardcore"); + var isHardcoreMode = isHardcoreCheck.Value == Conversions.ToDouble("1"); + + var lockCheckBox = new MyCheckBox + { + Text = "锁定难度", ToolTip = "锁定当前难度设置,锁定后无法在游戏中更改游戏难度", + VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(10d, 0d, 0d, 0d) + }; + + if (isHardcoreMode) + { + lockCheckBox.Visibility = Visibility.Collapsed; + } + else + { + var lockedElement = gameLevel.Get("DifficultyLocked"); + var isLocked = lockedElement is not null && lockedElement.Value == Conversions.ToDouble("1"); + lockCheckBox.Checked = isLocked; + } + + var difficultyPanel = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Left + }; + difficultyPanel.Children.Add(difficultyCombo); + difficultyPanel.Children.Add(lockCheckBox); + + + difficultyCombo.SelectionChanged += (s, e) => + { + try + { + if (difficultyCombo.SelectedValue is null) return; + var newDifficulty = Conversions.ToInteger(difficultyCombo.SelectedValue); + gameLevel.Get("Difficulty").Value = (byte)newDifficulty; + if (!isHardcoreMode) + { + var newLocked = lockCheckBox.Checked == true ? 1 : 0; + if (gameLevel.Contains("DifficultyLocked")) + gameLevel.Get("DifficultyLocked").Value = (byte)newLocked; + else if (newLocked == 1) + gameLevel.Add(new NbtByte("DifficultyLocked", (byte)newLocked)); + } + + using (var fileStream = new FileStream(saveDatPath, FileMode.Create, FileAccess.Write, + FileShare.None)) + { + saveInfo.SaveToStream(fileStream, NbtCompression.GZip); + } + + ModMain.Hint("难度设置修改成功", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "难度设置修改失败", ModBase.LogLevel.Hint); + } + }; + + + lockCheckBox.Change += (sender, user) => + { + try + { + if (difficultyCombo.SelectedValue is null) return; + var newDifficulty = Conversions.ToInteger(difficultyCombo.SelectedValue); + gameLevel.Get("Difficulty").Value = (byte)newDifficulty; + if (!isHardcoreMode) + { + var newLocked = lockCheckBox.Checked == true ? 1 : 0; + if (gameLevel.Contains("DifficultyLocked")) + gameLevel.Get("DifficultyLocked").Value = (byte)newLocked; + else if (newLocked == 1) + gameLevel.Add(new NbtByte("DifficultyLocked", (byte)newLocked)); + } + + using (var fileStream = new FileStream(saveDatPath, FileMode.Create, FileAccess.Write, + FileShare.None)) + { + saveInfo.SaveToStream(fileStream, NbtCompression.GZip); + } + + ModMain.Hint("难度设置修改成功", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "难度设置修改失败", ModBase.LogLevel.Hint); + } + }; + + var rowIndex = PanSettingsList.RowDefinitions.Count; + PanSettingsList.RowDefinitions.Add(new RowDefinition + { Height = new GridLength(1d, GridUnitType.Auto) }); + + var headTextBlock = new TextBlock { Text = "游戏难度", Margin = new Thickness(0d, 3d, 0d, 3d) }; + Grid.SetRow(headTextBlock, rowIndex); + Grid.SetColumn(headTextBlock, 0); + + Grid.SetRow(difficultyPanel, rowIndex); + Grid.SetColumn(difficultyPanel, 2); + + PanSettingsList.Children.Add(headTextBlock); + PanSettingsList.Children.Add(difficultyPanel); + } + + AddInfoTable("最后一次游玩", + new DateTime(1970, 1, 1, 0, 0, 0) + .AddMilliseconds(long.Parse(gameLevel.Get("LastPlayed").Value.ToString())) + .ToLocalTime().ToString()); + + NbtInt spawnX = null; + if (gameLevel.TryGet("SpawnX", out spawnX)) + { + var spawnY = gameLevel.Get("SpawnY"); + var spawnZ = gameLevel.Get("SpawnZ"); + AddInfoTable("出生点 (X/Y/Z)", $"{spawnX.Value} / {spawnY.Value} / {spawnZ.Value}"); + } + else + { + var spawnPos = gameLevel.Get("spawn").Get("pos"); + var spawnXPos = spawnPos[0]; + var spawnYPos = spawnPos[1]; + var spawnZPos = spawnPos[2]; + AddInfoTable("出生点 (X/Y/Z)", $"{spawnXPos} / {spawnYPos} / {spawnZPos}"); + } + + var gameTypeName = "获取失败"; + + var isHardcore = gameLevel.Get("hardcore"); + if (isHardcore.Value == Conversions.ToDouble("1")) + { + gameTypeName = "极限模式"; + } + else + { + var gameType = gameLevel.Get("GameType"); + switch (gameType.Value) + { + case 0: + { + gameTypeName = "生存模式"; + break; + } + case 1: + { + gameTypeName = "创造模式"; + break; + } + case 2: + { + gameTypeName = "冒险模式"; + break; + } + case 3: + { + gameTypeName = "旁观模式"; + break; + } + + default: + { + gameTypeName = "生存模式"; + break; + } + } + } + + AddInfoTable("游戏模式", gameTypeName); + + if (hasDifficulty) + { + var difficultyElement = gameLevel.Get("Difficulty"); + var difficultyName = "获取失败"; + var difficultyValue = int.Parse(difficultyElement.Value.ToString()); + switch (difficultyValue) + { + case 0: + { + difficultyName = "和平"; + break; + } + case 1: + { + difficultyName = "简单"; + break; + } + case 2: + { + difficultyName = "普通"; + break; + } + case 3: + { + difficultyName = "困难"; + break; + } + } + + var lockedElement = gameLevel.Get("DifficultyLocked"); + var isDifficultyLocked = + (lockedElement is not null && lockedElement.Value == Conversions.ToDouble("1")) || + isHardcore.Value == Conversions.ToDouble("1") ? "是" : + lockedElement is not null ? "否" : "获取失败"; + if (Hintversion1_8.Visibility != Visibility.Visible) + AddInfoTable("困难度", $"{difficultyName} (是否已锁定难度:{isDifficultyLocked})"); + } + + var totalTicks = long.Parse(gameLevel.Get("Time").Value.ToString()); + var totalSeconds = totalTicks / 20.0d; + var playTime = TimeSpan.FromSeconds(totalSeconds); + var formattedPlayTime = $"{playTime.Days} 天 {playTime.Hours} 小时 {playTime.Minutes} 分钟"; + AddInfoTable("游戏时长", formattedPlayTime); + PanContent.Visibility = Visibility.Visible; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "获取存档信息失败", ModBase.LogLevel.Msgbox); + PanContent.Visibility = Visibility.Collapsed; + PanSettings.Visibility = Visibility.Collapsed; + Hintversion1_9.Visibility = Visibility.Collapsed; + Hintversion1_8.Visibility = Visibility.Collapsed; + Hintversion1_3.Visibility = Visibility.Collapsed; + } + } + + private void ClearInfoTable() + { + PanList.Children.Clear(); + PanList.RowDefinitions.Clear(); + } + + private void AddInfoTable(string head, string content, bool isSeed = false, string versionName = null, + bool allowCopy = false) + { + var headTextBlock = new TextBlock { Text = head, Margin = new Thickness(0d, 3d, 0d, 3d) }; + var contentStack = new StackPanel { Orientation = Orientation.Horizontal }; + UIElement contentTextBlock; + if (allowCopy) + { + var thisBtn = new MyTextButton { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) }; + contentTextBlock = thisBtn; + thisBtn.Click += (_, _) => + { + try + { + ModBase.ClipboardSet(content); + } + catch (Exception ex) + { + ModBase.Log(ex, "复制到剪贴板失败", ModBase.LogLevel.Hint); + } + }; + } + else + { + contentTextBlock = new TextBlock { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) }; + } + + contentStack.Children.Add(contentTextBlock); + + if (isSeed && content != "获取失败") + { + var BtnChunkbase = new MyIconButton + { + Logo = ModBase.Logo.IconButtonlink, + ToolTip = "跳转到 Chunkbase", + Width = 22d, + Height = 22d + }; + contentStack.Children.Add(BtnChunkbase); + + + BtnChunkbase.Click += (_, _) => + { + try + { + if (versionName is null) + { + ModBase.Log("当前存档版本无法确定,因此无法跳转到 Chunkbase", ModBase.LogLevel.Hint); + return; + } + + if (versionName.Any(c => char.IsLetter(c))) + { + ModBase.Log($"当前存档版本 '{versionName}' 可能是预览版,不受支持,无法跳转到 Chunkbase", ModBase.LogLevel.Hint); + return; + } + + var versionParts = versionName.Split('.'); + string usedVersion; + if (versionName.StartsWith("1.21")) + usedVersion = versionName.Replace(".", "_"); + else if (versionName.Contains(".")) + usedVersion = string.Join("_", versionName.Split('.').Take(2)); + else + usedVersion = versionName.Replace(".", "_"); + var cbUri = + $"https://www.chunkbase.com/apps/seed-map#seed={content}&platform=java_{usedVersion}&dimension=overworld"; + ModBase.OpenWebsite(cbUri); + } + catch (Exception ex) + { + ModBase.Log(ex, "跳转到 Chunkbase 失败", ModBase.LogLevel.Hint); + } + }; + } + + PanList.Children.Add(headTextBlock); + PanList.Children.Add(contentStack); + var targetRow = new RowDefinition(); + PanList.RowDefinitions.Add(targetRow); + var rowIndex = PanList.RowDefinitions.IndexOf(targetRow); + Grid.SetRow(headTextBlock, rowIndex); + Grid.SetColumn(headTextBlock, 0); + Grid.SetRow(contentTextBlock, rowIndex); + Grid.SetColumn(contentTextBlock, 2); + Grid.SetRow(contentStack, rowIndex); + Grid.SetColumn(contentStack, 2); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml index b6a4a51b8..35bec30aa 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml @@ -1,32 +1,44 @@ - + - - - + + - + - + - + - + - - \ No newline at end of file + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml.cs new file mode 100644 index 000000000..952ed95ae --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSaves/PageInstanceSavesLeft.xaml.cs @@ -0,0 +1,174 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace PCL; + +public partial class PageInstanceSavesLeft : IRefreshable +{ + public static string CurrentSave; + + // 初始化 + private bool IsLoad; + + private void Page_Loaded(object sender, RoutedEventArgs e) + { + if (IsLoad) + return; + IsLoad = true; + } + + private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e) + { + e.Handled = true; + ModBase.OpenExplorer($@"{CurrentSave}\"); + } + + #region 龙猫牌 页面管理 + + /// + /// 当前页面的编号。从 0 开始计算。 + /// + public FormMain.PageSubType PageID = FormMain.PageSubType.Default; + + public PageInstanceSavesLeft() + { + InitializeComponent(); + Loaded += Page_Loaded; + ItemBackup.Check += PageCheck; + ItemInfo.Check += PageCheck; + ItemDatapack.Check += PageCheck; + BtnOpenFolder.Click += BtnOpenFolder_Click; + } + + /// + /// 勾选事件改变页面。 + /// + private void PageCheck(object sender, ModBase.RouteEventArgs e) + { + if (sender is MyListItem item && item.Tag is not null) + PageChange((FormMain.PageSubType)ModBase.Val(item.Tag)); + } + + public object PageGet(FormMain.PageSubType ID = FormMain.PageSubType.Default) + { + if ((int)ID == -1) + ID = PageID; + switch (ID) + { + case FormMain.PageSubType.VersionSavesInfo: + { + if (ModMain.FrmInstanceSavesInfo is null) + ModMain.FrmInstanceSavesInfo = new PageInstanceSavesInfo(); + return ModMain.FrmInstanceSavesInfo; + } + case FormMain.PageSubType.VersionSavesBackup: + { + if (ModMain.FrmInstanceSavesBackup is null) + ModMain.FrmInstanceSavesBackup = new PageInstanceSavesBackup(); + return ModMain.FrmInstanceSavesBackup; + } + case FormMain.PageSubType.VersionSavesDatapack: + { + if (ModMain.FrmInstanceSavesDatapack is null) + ModMain.FrmInstanceSavesDatapack = new PageInstanceSavesDatapack(); + return ModMain.FrmInstanceSavesDatapack; + } + + default: + { + throw new Exception("未知的实例设置子页面种类:" + (int)ID); + } + } + } + + /// + /// 切换现有页面。 + /// + public void PageChange(FormMain.PageSubType ID) + { + if (PageID == ID) + return; + ModAnimation.AniControlEnabled += 1; + try + { + PageChangeRun((MyPageRight)PageGet(ID)); + PageID = ID; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换分页面失败(ID " + (int)ID + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private static void PageChangeRun(MyPageRight Target) + { + ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + if (Target.Parent is not null) + Target.SetValue(ContentPresenter.ContentProperty, null); + ModMain.FrmMain.PageRight = Target; + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnExit(); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnForceExit(); + ModMain.FrmMain.PanMainRight.Child = ModMain.FrmMain.PageRight; + ModMain.FrmMain.PageRight.Opacity = 0d; + }, 130), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + ModMain.FrmMain.PageRight.Opacity = 1d; + ModMain.FrmMain.PageRight.PageOnEnter(); + }, 30, true) + }, "PageLeft PageChange"); + } + + public void RefreshButton_Click(object sender, EventArgs e) // 由边栏按钮匿名调用 + { + Refresh((FormMain.PageSubType)ModBase.Val(((dynamic)sender).Tag)); + } + + public void Refresh() + { + Refresh(ModMain.FrmMain.PageCurrentSub); + } + + public void Refresh(FormMain.PageSubType SubType) + { + switch (SubType) + { + case FormMain.PageSubType.VersionSavesBackup: + { + if (ModMain.FrmInstanceSavesBackup is null) + ModMain.FrmInstanceSavesBackup = new PageInstanceSavesBackup(); + if (ItemBackup.Checked) + ModMain.FrmInstanceSavesBackup.Refresh(); + else + ItemBackup.Checked = true; + + break; + } + case FormMain.PageSubType.VersionSavesDatapack: + { + if (ModMain.FrmInstanceSavesDatapack is null) + ModMain.FrmInstanceSavesDatapack = new PageInstanceSavesDatapack(); + if (ItemDatapack.Checked) + ModMain.FrmInstanceSavesDatapack.Refresh(); + else + ItemDatapack.Checked = true; + + break; + } + } + + ModMain.Hint("刷新中……"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml index da8d9b0bf..b21ac077f 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml @@ -1,8 +1,9 @@  @@ -20,10 +21,16 @@ - - - - + + + + @@ -33,7 +40,8 @@ - + @@ -54,4 +62,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml.cs new file mode 100644 index 000000000..80d9943fb --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceScreenshot.xaml.cs @@ -0,0 +1,325 @@ +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Microsoft.VisualBasic.CompilerServices; +using Microsoft.VisualBasic.FileIO; +using PCL.Core.App; +using SearchOption = System.IO.SearchOption; + +namespace PCL; + +public partial class PageInstanceScreenshot : IRefreshable +{ + private bool _AppendLock; + private int _Offset; + + private List FileList = new(); + + private bool IsLoad; + private string ScreenshotPath; + + public PageInstanceScreenshot() + { + InitializeComponent(); + Loaded += PageSetupLaunch_Loaded; + PanBack.ScrollChanged += RequireAppend; + BtnOpenFolder.Click += BtnOpenFolder_Click; + BtnOpenFolderTop.Click += BtnOpenFolder_Click; + } + + void IRefreshable.Refresh() + { + RefreshSelf(); + } + + private void RefreshSelf() + { + Refresh(); + } + + public static async void Refresh() + { + if (ModMain.FrmInstanceScreenshot is not null) + await ModMain.FrmInstanceScreenshot.Reload(); + ModMain.FrmInstanceLeft.ItemScreenshot.Checked = true; + ModMain.Hint("正在刷新……", Log: false); + } + + private async void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + ScreenshotPath = PageInstanceLeft.Instance.PathIndie + @"screenshots\"; + if (!Directory.Exists(ScreenshotPath)) + Directory.CreateDirectory(ScreenshotPath); + await Reload(); + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + } + + /// + /// 确保当前页面上的信息已正确显示。 + /// + public async Task Reload() + { + ModAnimation.AniControlEnabled += 1; + PanBack.ScrollToHome(); + await LoadFileList(); + ModAnimation.AniControlEnabled -= 1; + } + + private void RefreshTip() + { + if (FileList.Count.Equals(0)) + { + PanNoPic.Visibility = Visibility.Visible; + PanContent.Visibility = Visibility.Collapsed; + } + else + { + PanNoPic.Visibility = Visibility.Collapsed; + PanContent.Visibility = Visibility.Visible; + } + } + + private async Task LoadFileList() + { + ModBase.Log("[Screenshot] 刷新截图文件"); + FileList.Clear(); + if (Directory.Exists(ScreenshotPath)) + FileList = Directory.EnumerateFiles(ScreenshotPath, "*", SearchOption.TopDirectoryOnly).ToList(); + var AllowedSuffix = new[] { ".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tiff" }; + FileList = FileList.Where(e => AllowedSuffix.Contains(new FileInfo(e).Extension.ToLower())).ToList(); + PanList.Children.Clear(); + RefreshTip(); + FileList = FileList.Where(e => !e.ContainsF(@"\debug\")).ToList(); // 排除资源包调试输出 + FileList.Sort((a, b) => new FileInfo(a).CreationTime > new FileInfo(b).CreationTime); + ModBase.Log("[Screenshot] 共发现 " + FileList.Count + " 个截图文件"); + if (FileList.Count == 0) + return; + await ListAppend(20, 0); + } + + private async void RequireAppend(object sender, ScrollChangedEventArgs e) + { + if (!_AppendLock && PanBack.VerticalOffset + PanBack.ViewportHeight >= PanBack.ExtentHeight) await ListAppend(); + } + + private async Task ListAppend(int Count = 20, int Offset = -1) + { + _AppendLock = true; + if (Offset == -1) + { + if (_Offset * Count > FileList.Count) + return; + Offset = _Offset + 1; + _Offset += 1; + } + else + { + _Offset = Offset; + } + + if (Count * Offset > FileList.Count) + return; + for (int j = Count * Offset, loopTo = Count * (Offset + 1) - 1; j <= loopTo; j++) + { + if (j >= FileList.Count) + break; + var i = FileList.ElementAt(j); + try + { + if (!File.Exists(i)) + continue; // 文件在加载途中消失了 + if (File.GetAttributes(i).HasFlag(FileAttributes.Hidden)) + continue; // 隐藏文件 + if (new FileInfo(i).Length == 0L) + continue; // 空文件 + var myCard = new MyCard + { + Height = double.NaN, // 允许高度自适应 + Width = double.NaN, // 允许宽度自适应 + Margin = new Thickness(7d), + Tag = i, + ToolTip = i.Replace(ScreenshotPath, "") // 适配高清截图模组 + }; + var grid = new Grid(); + myCard.Children.Add(grid); + + grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(9d) }); + grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(120d) }); + grid.RowDefinitions.Add(new RowDefinition()); + + // 图片 + var image = new Image(); + image.Source = await Task.Run(() => + { + var bitmapImage = new BitmapImage(); + var loadSource = i; + using (var fs = new FileStream(loadSource, FileMode.Open, FileAccess.Read)) + { + bitmapImage.BeginInit(); + bitmapImage.DecodePixelHeight = 200; + bitmapImage.DecodePixelWidth = 400; + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.StreamSource = fs; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + } + + return bitmapImage; + }); + image.Stretch = Stretch.Uniform; // 使图片自适应控件大小 + image.Cursor = Cursors.Hand; + image.MouseLeftButtonDown += (sender, e) => + { + try + { + Basics.OpenPath(i); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开截图失败!", ModBase.LogLevel.Hint); + } + }; // 使用系统默认程序打开 + Grid.SetRow(image, 1); + grid.Children.Add(image); + + // 按钮 + var stackPanel = new StackPanel(); + stackPanel.Orientation = Orientation.Horizontal; + stackPanel.HorizontalAlignment = HorizontalAlignment.Center; + stackPanel.Margin = new Thickness(3d, 5d, 3d, 5d); + Grid.SetRow(stackPanel, 2); + grid.Children.Add(stackPanel); + + var btnOpen = new MyIconTextButton + { + Name = "BtnOpen", + Text = "打开", + LogoScale = 0.8d, + Logo = ModBase.Logo.IconButtonOpen, + Tag = i + }; + btnOpen.Click += (s, ev) => btnOpen_Click((MyIconTextButton)s, ev); + stackPanel.Children.Add(btnOpen); + var btnDelete = new MyIconTextButton + { + Name = "BtnDelete", + Text = "删除", + LogoScale = 0.8d, + Logo = ModBase.Logo.IconButtonDelete, + Tag = i + }; + btnDelete.Click += (s, ev) => btnDelete_Click((MyIconTextButton)s, ev); + stackPanel.Children.Add(btnDelete); + var btnCopy = new MyIconTextButton + { + Name = "BtnCopy", + Text = "复制", + LogoScale = 0.8d, + Logo = ModBase.Logo.IconButtonCopy, + Tag = i + }; + btnDelete.Click += (s, ev) => btnDelete_Click((MyIconTextButton)s, ev); + stackPanel.Children.Add(btnCopy); + PanList.Children.Add(myCard); + myCard.Opacity = 0d; + ModAnimation.AniStart(new[] { ModAnimation.AaOpacity(myCard, 1d, 200) }); + } + catch (Exception ex) + { + ModBase.Log(ex, $"[Screenshot] 创建 {i} 截图预览失败,图像可能损坏"); + } + } + + _AppendLock = false; + } + + private void RemoveItem(string Path) + { + try + { + foreach (var i in PanList.Children) + if (((MyCard)i).Tag.Equals(Path)) + { + PanList.Children.Remove((UIElement)i); + break; + } + + FileList.Remove(Path); + } + catch (Exception ex) + { + ModBase.Log(ex, "未能找到对应 UI"); + } + } + + private string GetPathFromSender(MyIconTextButton sender) + { + return Conversions.ToString(sender.Tag); + } + + private void btnOpen_Click(MyIconTextButton sender, EventArgs e) + { + ModBase.OpenExplorer(GetPathFromSender(sender)); + } + + private void btnDelete_Click(MyIconTextButton sender, EventArgs e) + { + var path = GetPathFromSender(sender); + try + { + FileSystem.DeleteFile(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); + RemoveItem(path); + RefreshTip(); + ModMain.Hint("已将截图移至回收站!"); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除截图失败!", ModBase.LogLevel.Hint); + } + } + + private void BtnCopy_Click(MyIconTextButton sender, EventArgs e) + { + var imagePath = GetPathFromSender(sender); + if (File.Exists(imagePath)) + { + var TryTime = 0; + while (TryTime <= 5) + try + { + ModBase.Log("[Screenshot] 尝试复制" + imagePath + "到剪贴板"); + Clipboard.SetImage(new BitmapImage(new Uri(imagePath))); + ModMain.Hint("已复制截图到剪贴板!"); + TryTime = 6; + return; + } + catch (Exception ex) + { + TryTime += 1; + ModBase.Log(ex, $"[Screenshot]第 {TryTime} 次复制尝试失败"); + } + + ModMain.Hint("截图复制失败!", ModMain.HintType.Critical); + } + else + { + ModMain.Hint("截图文件不存在!"); + } + } + + private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e) + { + if (!Directory.Exists(ScreenshotPath)) + Directory.CreateDirectory(ScreenshotPath); + ModBase.OpenExplorer(ScreenshotPath); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml index b89f02b21..a127c5eb6 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml @@ -1,12 +1,12 @@ - - + + @@ -23,11 +23,19 @@ - - - - - + + + + + @@ -38,18 +46,19 @@ - - + + - + - + - - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.cs new file mode 100644 index 000000000..a8b2f67fc --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.cs @@ -0,0 +1,482 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using System.Windows.Input; +using fNbt; +using PCL.Core.Link.McPing; +using PCL.Core.Link.McPing.Model; +using PCL.Core.Minecraft; + +namespace PCL; + +public partial class PageInstanceServer : MyPageRight +{ + private const int DebounceInterval = 2000; + + public static readonly List ServerList = new(); + private static readonly List ServerCardList = new(); + + private CancellationTokenSource _cts; + + private DateTime _lastRefresh = DateTime.MinValue; + + public PageInstanceServer() + { + InitializeComponent(); + Loaded += PageLoaded; + IsVisibleChanged += PageInstanceServer_IsVisibleChanged; + } + + private async void PageLoaded(object e, RoutedEventArgs sender) + { + ServerList.Clear(); + ServerCardList.Clear(); + PanServers.Children.Clear(); + + await LoadServersFromFile(); + RefreshTip(); + + foreach (var server in ServerList) + { + var serverCard = new ServerCard(); + serverCard.RemoveServer += RemoveServerEvent; + serverCard.EditServer += (a, b) => this.EditServer(a, (dynamic)b); + serverCard.UpdateServerInfo(server); + ServerCardList.Add(serverCard); + PanServers.Children.Add(serverCard); + } + + PingAllServers(); + } + + private void PageInstanceServer_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (!IsVisible) + if (_cts is not null) + { + _cts.Cancel(); + _cts.Dispose(); // 清理旧的 CancellationTokenSource + _cts = null; + } + } + + private async void RemoveServerEvent(object sender, EventArgs e) + { + // Get server index + var index = PanServers.Children.IndexOf((UIElement)sender); + if (index < 0) + { + ModMain.Hint("无法找到服务器在列表中的索引", ModMain.HintType.Critical); + return; + } + + // Read NBT file + var nbtData = + await NbtFileHandler.ReadTagInNbtFileAsync( + Path.Combine(PageInstanceLeft.Instance.PathIndie, "servers.dat"), "servers"); + if (nbtData is null) + { + ModMain.Hint("无法读取服务器数据文件", ModMain.HintType.Critical); + return; + } + + // Remove server from NBT data + nbtData.RemoveAt(index); + var clonedNbtData = (NbtList)nbtData.Clone(); + + // Write back to NBT file + if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, + PageInstanceLeft.Instance.PathIndie + "servers.dat")) + { + ModMain.Hint("无法写入服务器数据文件", ModMain.HintType.Critical); + return; + } + + // Remove server from list and UI + ServerList.RemoveAt(index); + ServerCardList.Remove((ServerCard)sender); + if (ServerList.Count == 0) RefreshTip(); + + // Remove UI element + PanServers.Children.Remove((UIElement)sender); + + // Success message + ModMain.Hint("服务器已移除", ModMain.HintType.Finish); + } + + private async void EditServer(object sender, ServerCard.ResultEventArgs e) + { + // Read NBT file + var nbtData = + await NbtFileHandler.ReadTagInNbtFileAsync(PageInstanceLeft.Instance.PathIndie + "servers.dat", + "servers"); + if (nbtData is null) + { + ModMain.Hint("无法读取服务器数据文件", ModMain.HintType.Critical); + return; + } + + // Get server index + var index = PanServers.Children.IndexOf((UIElement)sender); + if (index < 0 || index >= nbtData.Count) + { + ModMain.Hint("无法找到服务器在列表中的索引", ModMain.HintType.Critical); + return; + } + + // Verify server data + var server = nbtData[index] as NbtCompound; + + // Update server data + server["name"] = new NbtString("name", e.Param1); + server["ip"] = new NbtString("ip", e.Param2); + + // Write updated NBT data + var clonedNbtData = (NbtList)nbtData.Clone(); + if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, + PageInstanceLeft.Instance.PathIndie + "servers.dat")) + { + ModMain.Hint("无法写入服务器数据文件", ModMain.HintType.Critical); + return; + } + + var serverCard = sender as ServerCard; + + serverCard.Server.Name = e.Param1; + serverCard.Server.Address = e.Param2; + + await serverCard.RefreshServerStatus(true); + + // Success message + ModMain.Hint("服务器信息已更新", ModMain.HintType.Finish); + } + + /// + /// 刷新服务器列表 + /// + public async void RefreshServers() + { + ModBase.Log("刷新服务器列表"); + try + { + // 读取服务器信息 + await LoadServersFromFile(); + + // 在UI线程中更新界面 + ModBase.RunInUi(() => UpdateServerUi()); + + // 异步ping所有服务器 + PingAllServers(); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新服务器列表失败", ModBase.LogLevel.Feedback); + ModBase.RunInUi(() => ModMain.Hint("刷新服务器列表失败:" + ex.Message, ModMain.HintType.Critical)); + } + } + + private void BtnRefresh_Click(object sender, MouseButtonEventArgs e) + { + if ((DateTime.Now - _lastRefresh).TotalMilliseconds < DebounceInterval) + { + ModMain.Hint("请勿频繁刷新!"); + return; + } + + _lastRefresh = DateTime.Now; + ModMain.Hint("正在刷新服务器列表,请稍候..."); + try + { + RefreshServers(); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新服务器列表失败", ModBase.LogLevel.Feedback); + ModMain.Hint("刷新服务器列表失败:" + ex.Message, ModMain.HintType.Critical); + } + } + + private async void BtnAddServer_Click(object sender, MouseButtonEventArgs e) + { + var result = GetServerInfo(new MinecraftServerInfo { Name = "Minecraft服务器", Address = "" }); + if (result.Success) + { + var newServer = new MinecraftServerInfo + { + Name = result.Name, + Address = result.Address, + Status = ServerStatus.Unknown + }; + ServerList.Add(newServer); + + RefreshTip(); + + var serverCard = new ServerCard(); + serverCard.RemoveServer += RemoveServerEvent; + serverCard.EditServer += (a, b) => this.EditServer(a, (dynamic)b); + serverCard.UpdateServerInfo(newServer); + ServerCardList.Add(serverCard); + PanServers.Children.Add(serverCard); + + await serverCard.RefreshServerStatus(false); + + var serversDatPath = Path.Combine(PageInstanceLeft.Instance.PathIndie, "servers.dat"); + + object nbtData; + if (!File.Exists(serversDatPath)) + { + nbtData = new NbtList("servers", NbtTagType.Compound); + RefreshTip(); + } + else + { + nbtData = await NbtFileHandler.ReadTagInNbtFileAsync(serversDatPath, "servers"); + } + + if (nbtData is not null) + { + var server = new NbtCompound(); + server["name"] = new NbtString("name", result.Name); + server["ip"] = new NbtString("ip", result.Address); + ((dynamic)nbtData).Add(server); + var clonedNbtData = (NbtList)((dynamic)nbtData).Clone(); + await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, serversDatPath); + } + } + } + + public static (string Name, string Address, bool Success) GetServerInfo(MinecraftServerInfo server) + { + var newName = ModMain.MyMsgBoxInput("编辑服务器信息", "请输入新的服务器名称:", server.Name, + new Collection { new ValidateNullOrWhiteSpace() }); + + if (string.IsNullOrEmpty(newName)) return (string.Empty, string.Empty, false); + + var newAddress = ModMain.MyMsgBoxInput("编辑服务器信息", "请输入新的服务器地址:", server.Address, + new Collection { new ValidateNullOrWhiteSpace() }); + if (string.IsNullOrEmpty(newAddress)) return (string.Empty, string.Empty, false); + return (newName, newAddress, true); + } + + /// + /// 从servers.dat文件读取服务器信息 + /// + private async Task LoadServersFromFile() + { + ServerList.Clear(); + + var serversFile = PageInstanceLeft.Instance.PathIndie + "servers.dat"; + if (!File.Exists(serversFile)) + return; + + try + { + // 读取NBT格式的servers.dat文件 + var nbtData = await NbtFileHandler.ReadTagInNbtFileAsync(serversFile, "servers"); + ParseServersFromNBT(nbtData); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取servers.dat文件失败"); + } + } + + /// + /// 解析NBT格式的服务器数据 + /// + private void ParseServersFromNBT(NbtList serversList) + { + if (serversList is not null) + { + ModBase.Log($"Found {serversList.Count} servers:"); + + // 遍历 servers 列表中的每个服务器 + for (int i = 0, loopTo = serversList.Count - 1; i <= loopTo; i++) + { + var server = serversList[i] as NbtCompound; + if (server is not null) + { + // 提取服务器信息 + // Dim hidden As Byte = If(server.Get(Of NbtByte)("hidden")?.Value, 0) + var ip = server.Get("ip")?.Value ?? "Unknown"; + var name = server.Get("name")?.Value ?? "Unknown"; + var iconBase64 = server.Get("icon")?.Value; + + ModBase.Log($"服务器 {i + 1}:"); + ModBase.Log($" 名字: {name}"); + ModBase.Log($" IP: {ip}"); + // Log($" Hidden: {If(hidden = 1, "Yes", "No")}") + ServerList.Add(new MinecraftServerInfo + { + Name = name, + Address = ip, + Status = ServerStatus.Unknown, + Icon = iconBase64 + }); + } + } + } + else + { + ModBase.Log("No 'servers' list found in servers.dat."); + } + } + + /// + /// 更新服务器UI显示 + /// + private void UpdateServerUi() + { + PanServers.Children.Clear(); + + RefreshTip(); + + foreach (var server in ServerList) + { + var serverCard = new ServerCard(); + serverCard.RemoveServer += RemoveServerEvent; + serverCard.EditServer += (a, b) => this.EditServer(a, (dynamic)b); + serverCard.UpdateServerInfo(server); + ServerCardList.Add(serverCard); + PanServers.Children.Add(serverCard); + } + } + + private void RefreshTip() + { + if (ServerList.Count == 0) + { + ModBase.Log("没有找到任何服务器"); + PanNoServer.Visibility = Visibility.Visible; + PanContent.Visibility = Visibility.Collapsed; + PanServers.Visibility = Visibility.Collapsed; + return; + } + + ModBase.Log("找到服务器列表"); + PanNoServer.Visibility = Visibility.Collapsed; + PanContent.Visibility = Visibility.Visible; + PanServers.Visibility = Visibility.Visible; + } + + private async void PingAllServers() + { + if (_cts is not null) + { + _cts.Cancel(); + _cts.Dispose(); + } + + _cts = new CancellationTokenSource(); + var token = _cts.Token; + var semaphore = new SemaphoreSlim(5); // 限制最多 5 个并发任务 + + var tasks = new List(); + try + { + var snapshot = ServerCardList.ToList(); + foreach (var server in snapshot) + { + var currentServer = server; + await semaphore.WaitAsync(token); + tasks.Add(Task.Run(async () => + { + try + { + await currentServer.RefreshServerStatus(false, token); + } + catch (Exception ex) + { + ModBase.Log(ex, $"Ping 服务器失败: {currentServer}"); + } + finally + { + semaphore.Release(); + } + }, token)); + } + + await Task.WhenAll(tasks); // 等待所有任务完成 + } + catch (OperationCanceledException ex) + { + ModBase.Log("PingAllServers 被取消", ModBase.LogLevel.Debug); + } + catch (Exception ex) + { + ModBase.Log(ex, "PingAllServers 失败"); + } + } + + /// + /// ping单个服务器 + /// + public static async Task PingServer(MinecraftServerInfo server, CancellationToken token) + { + try + { + var addr = await ServerAddressResolver.GetReachableAddressAsync(server.Address, token); + using (var query = McPingServiceFactory.CreateService(addr.Ip, addr.Port)) + { + McPingResult? result; + ModBase.Log("Pinging server: " + server.Address + ":" + addr.Port); + result = await query.PingAsync(token); // 传递 token + ModBase.Log("Ping result: " + (result is not null ? "Success" : "Failed")); + if (result is not null) + { + server.Status = ServerStatus.Online; + server.PlayerCount = result.Players.Online; + server.MaxPlayers = result.Players.Max; + server.Description = result.Description; + server.Version = result.Version.Name; + server.Ping = (int)result.Latency; + server.Icon = result.Favicon; + } + else + { + server.Status = ServerStatus.Offline; + } + } + } + catch (OperationCanceledException ex) + { + server.Status = ServerStatus.Offline; + ModBase.Log("Ping 服务器被取消: " + server.Address, ModBase.LogLevel.Debug); + } + catch (Exception ex) + { + server.Status = ServerStatus.Offline; + ModBase.Log(ex, $"Ping 服务器失败: {server.Address}:{server.Port}"); + } + + return server; + } +} + +/// +/// Minecraft服务器信息类 +/// +public class MinecraftServerInfo +{ + public string Name { get; set; } + public string Address { get; set; } + public int Port { get; set; } = 25565; + public ServerStatus Status { get; set; } = ServerStatus.Unknown; + public int PlayerCount { get; set; } + public int MaxPlayers { get; set; } + public string Description { get; set; } = ""; + public string Version { get; set; } = ""; + public int Ping { get; set; } + public string Icon { get; set; } = ""; +} + +/// +/// 服务器状态枚举 +/// +public enum ServerStatus +{ + Unknown, + Online, + Offline, + Pinging +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.vb b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.vb index 60c3cb2fe..cba4bfd68 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceServer.xaml.vb @@ -1,8 +1,9 @@ Imports System.Collections.ObjectModel Imports System.IO -Imports System.Threading.Tasks Imports fNbt Imports PCL.Core.Link +Imports PCL.Core.Link.McPing +Imports PCL.Core.Link.McPing.Model Imports PCL.Core.Minecraft Public Class PageInstanceServer @@ -342,7 +343,7 @@ Public Class PageInstanceServer Public Async Shared Function PingServer(server As MinecraftServerInfo, token As CancellationToken) As Task(Of MinecraftServerInfo) Try Dim addr = Await ServerAddressResolver.GetReachableAddressAsync(server.Address, token) - Using query = New McPing(addr.Ip, addr.Port) + Using query = McPingServiceFactory.CreateService(addr.Ip, addr.Port) Dim result As McPingResult Log("Pinging server: " & server.Address & ":" & addr.Port) result = Await query.PingAsync(token) ' 传递 token diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml index ff31ce9b1..0e95458ac 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml @@ -1,14 +1,15 @@  + CanClose="True" RelativeSetup="HintIndieSetup" /> @@ -28,20 +29,36 @@ - - - - + + + + - - - + + + - - - + + + @@ -52,19 +69,23 @@ - - + - - - + + @@ -76,12 +97,18 @@ - - - - - + + + + @@ -92,50 +119,66 @@ - - + + - + - - - + + + - + - - + + - - - - - - - + + + + + + + - + - - - - + + + + @@ -150,29 +193,48 @@ - - - + + + - - + + - - + + - - - - + + + + @@ -186,9 +248,12 @@ - - - + + + @@ -211,8 +276,10 @@ - - + + @@ -220,46 +287,70 @@ - - - - - + + - - + + - - + + + + + - - - - - - - + + + + + + x:Name="BtnSwitch" Text="全局设置" + LogoScale="0.9" + Logo="M73 584L920 584 608 896C579 925 579 972 608 1001 637 1030 683 1030 712 1001L1149 565C1164 550 1170 531 1170 511 1170 492 1164 472 1149 457L712 21C683-7 637-7 608 21 579 50 579 97 608 126L920 438 73 438C33 438 0 471 0 511 0 551 33 584 73 584Z" /> - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs new file mode 100644 index 000000000..5e044b6b0 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs @@ -0,0 +1,1029 @@ +using System.IO; +using System.Text.Json; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.IO; +using PCL.Core.Minecraft; +using PCL.Core.Minecraft.Java.UserPreference; +using PCL.Core.UI; +using PCL.Core.Utils.OS; + +namespace PCL; + +public partial class PageInstanceSetup +{ + private new bool IsLoaded; + + public PageInstanceSetup() + { + InitializeComponent(); + Loaded += PageSetupSystem_Loaded; + } + + private void PageSetupSystem_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + RefreshRam(false); + + // 由于各个实例不同,每次都需要重新加载 + ModAnimation.AniControlEnabled += 1; + Reload(); + ModAnimation.AniControlEnabled -= 1; + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + // 内存自动刷新 + var timer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) }; + timer.Tick += (_, __) => RefreshRam(); + timer.Start(); + RectRamGame.SizeChanged += (s, e) => RefreshRamText(); + } + + public void Reload() + { + try + { + // 启动参数 + TextArgumentTitle.Text = Config.Instance.Title[PageInstanceLeft.Instance]; + CheckArgumentTitleEmpty.Checked = Config.Instance.UseGlobalTitle[PageInstanceLeft.Instance]; + TextArgumentInfo.Text = Config.Instance.TypeInfo[PageInstanceLeft.Instance]; + var _unused = PageInstanceLeft.Instance.PathIndie; // 触发自动判定 + ComboArgumentIndieV2.SelectedIndex = Config.Instance.IndieV2[PageInstanceLeft.Instance] ? 0 : 1; + CheckArgumentTitleEmpty.Visibility = + TextArgumentTitle.Text.Length > 0 ? Visibility.Collapsed : Visibility.Visible; + TextArgumentTitle.HintText = CheckArgumentTitleEmpty.Checked == true ? "默认" : "跟随全局设置"; + RefreshJavaComboBox(); + + // 游戏内存 + ((MyRadioBox)FindName(Conversions.ToString(Operators.ConcatenateObject("RadioRamType", + ModBase.Setup.Load("VersionRamType", instance: PageInstanceLeft.Instance))))).Checked = true; + SliderRamCustom.Value = Config.Instance.CustomMemorySize[PageInstanceLeft.Instance]; + ComboRamOptimize.SelectedIndex = Config.Instance.OptimizeMemoryResolution[PageInstanceLeft.Instance]; + + // 服务器 + TextServerEnter.Text = Config.Instance.ServerToEnter[PageInstanceLeft.Instance]; + ComboServerLoginRequire.SelectedIndex = Config.Instance.LoginRequirementSolution[PageInstanceLeft.Instance]; + ComboServerLoginLast = ComboServerLoginRequire.SelectedIndex; + ServerLogin(ComboServerLoginRequire.SelectedIndex); + TextServerAuthServer.Text = Config.Instance.AuthServerAddress[PageInstanceLeft.Instance]; + TextServerAuthName.Text = Config.Instance.AuthServerDisplayName[PageInstanceLeft.Instance]; + TextServerAuthRegister.Text = Config.Instance.AuthRegisterAddress[PageInstanceLeft.Instance]; + + // 高级设置 + ComboAdvanceRenderer.SelectedIndex = Config.Instance.Renderer[PageInstanceLeft.Instance]; + TextAdvanceJvm.Text = Config.Instance.JvmArgs[PageInstanceLeft.Instance]; + TextAdvanceGame.Text = Config.Instance.GameArgs[PageInstanceLeft.Instance]; + TextAdvanceRun.Text = Config.Instance.PreLaunchCommand[PageInstanceLeft.Instance]; + CheckAdvanceRunWait.Checked = (bool?)ModBase.Setup.Get("VersionAdvanceRunWait", PageInstanceLeft.Instance); + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual( + ModBase.Setup.Get("VersionAdvanceAssets", PageInstanceLeft.Instance), 2, false))) + { + ModBase.Log("[Setup] 已迁移老版本的关闭文件校验设置"); + ModBase.Setup.Reset("VersionAdvanceAssets", instance: PageInstanceLeft.Instance); + Config.Instance.DisableAssetVerifyV2[PageInstanceLeft.Instance] = true; + } + + CheckAdvanceAssetsV2.Checked = + (bool?)ModBase.Setup.Get("VersionAdvanceAssetsV2", PageInstanceLeft.Instance); + CheckAdvanceUseProxyV2.Checked = + (bool?)ModBase.Setup.Get("VersionAdvanceUseProxyV2", PageInstanceLeft.Instance); + CheckAdvanceJava.Checked = (bool?)ModBase.Setup.Get("VersionAdvanceJava", PageInstanceLeft.Instance); + if (ModBase.IsArm64System) + { + CheckAdvanceDisableJLW.Checked = true; + CheckAdvanceDisableJLW.IsEnabled = false; + CheckAdvanceDisableJLW.ToolTip = "在启动游戏时不使用 Java Wrapper 进行包装。 由于系统为 ARM64 架构,Java Wrapper 已被强制禁用。"; + } + else + { + CheckAdvanceDisableJLW.Checked = + (bool?)ModBase.Setup.Get("VersionAdvanceDisableJLW", PageInstanceLeft.Instance); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "重载实例独立设置时出错", ModBase.LogLevel.Feedback); + } + } + + // 初始化 + public void Reset() + { + try + { + if (Conversions.ToBoolean(!(bool)ModBase.Setup.Get("VersionServerLoginLock", PageInstanceLeft.Instance))) + { + ModBase.Setup.Reset("VersionServerLoginRequire", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionServerAuthServer", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionServerAuthRegister", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionServerAuthName", instance: PageInstanceLeft.Instance); + } + + ModBase.Setup.Reset("VersionServerEnter", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionArgumentTitle", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionArgumentInfo", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionArgumentIndieV2", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionRamType", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionRamCustom", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionRamOptimize", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceJvm", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceGame", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceAssets", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceAssetsV2", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceJava", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceDisableJlw", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceRun", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceRunWait", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceDisableJLW", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceUseProxyV2", instance: PageInstanceLeft.Instance); + ModBase.Setup.Reset("VersionAdvanceRenderer", instance: PageInstanceLeft.Instance); + + ModBase.Setup.Reset("VersionArgumentJavaSelect", instance: PageInstanceLeft.Instance); + + ModBase.Log("[Setup] 已初始化实例独立设置"); + ModMain.Hint("已初始化实例独立设置!", ModMain.HintType.Finish, false); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化实例独立设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void RadioBoxChange(object o, ModBase.RouteEventArgs routeEventArgs) + { + var sender = (MyRadioBox)o; + var gotCfg = sender.Tag.ToString().Split("/"); + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(gotCfg[0], int.Parse(gotCfg[1]), instance: PageInstanceLeft.Instance); + } + + private void TextBoxChange(object o, TextChangedEventArgs textChangedEventArgs) + { + var sender = (MyComboBox)o; + if (ModAnimation.AniControlEnabled == 0) + // #3194,不能删减 / + // Dim HandledText As String = sender.Text + // If sender.Tag = "VersionServerAuthServer" OrElse sender.Tag = "VersionServerAuthRegister" Then HandledText = HandledText.TrimEnd("/") + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.Text, instance: PageInstanceLeft.Instance); + } + + private void SliderChange(object o, bool user) + { + var sender = (MySlider)o; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.Value, instance: PageInstanceLeft.Instance); + } + + private static void ComboChange(MyComboBox sender, object e) + { + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex, + instance: PageInstanceLeft.Instance); + } + + private static void CheckBoxLikeComboChange(MyComboBox sender, object e) + { + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex == 0, + instance: PageInstanceLeft.Instance); + } + + private static void CheckBoxChange(MyCheckBox sender, object e) + { + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.Checked, instance: PageInstanceLeft.Instance); + } + + // 切换到全局设置 + private void BtnSwitch_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Setup); + } + + #region 游戏内存 + + public void RamType(int Type) + { + if (SliderRamCustom is null) + return; + SliderRamCustom.IsEnabled = Type == 1; + } + + /// + /// 刷新 UI 上的 RAM 显示。 + /// + public void RefreshRam(bool ShowAnim) + { + if (LabRamGame is null || LabRamUsed is null || + ModMain.FrmMain.PageCurrent != FormMain.PageType.InstanceSetup || + ModMain.FrmInstanceLeft.PageID != FormMain.PageSubType.VersionSetup) + return; + // 获取内存情况 + var RamGame = Math.Round(GetRam(PageInstanceLeft.Instance), 5); + var phyRam = KernelInterop.GetPhysicalMemoryBytes(); + var RamTotal = Math.Round((double)(phyRam.Total / 1024 / 1024 / 1024), 1); + var RamAvailable = Math.Round((double)(phyRam.Available / 1024 / 1024 / 1024), 1); + var RamGameActual = Math.Round(Math.Min(RamGame, RamAvailable), 5); + var RamUsed = Math.Round(RamTotal - RamAvailable, 5); + var RamEmpty = Math.Round(ModBase.MathClamp(RamTotal - RamUsed - RamGame, 0d, 1000d), 1); + // 设置最大可用内存 + if (RamTotal <= 1.5d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Max(Math.Floor((RamTotal - 0.3d) / 0.1d), 1d)); + else if (RamTotal <= 8d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((RamTotal - 1.5d) / 0.5d) + 12d); + else if (RamTotal <= 16d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((RamTotal - 8d) / 1d) + 25d); + else + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((RamTotal - 16d) / 2d) + 33d); + // 设置文本 + LabRamGame.Text = Conversions.ToString(Operators.ConcatenateObject( + Operators.ConcatenateObject(RamGame == Math.Floor(RamGame) ? RamGame + ".0" : RamGame, " GB"), + RamGame != RamGameActual + ? Operators.ConcatenateObject( + Operators.ConcatenateObject(" (可用 ", + RamGameActual == Math.Floor(RamGameActual) ? RamGameActual + ".0" : RamGameActual), " GB)") + : "")); + LabRamUsed.Text = + Conversions.ToString(Operators.ConcatenateObject(RamUsed == Math.Floor(RamUsed) ? RamUsed + ".0" : RamUsed, + " GB")); + LabRamTotal.Text = Conversions.ToString(Operators.ConcatenateObject( + Operators.ConcatenateObject(" / ", RamTotal == Math.Floor(RamTotal) ? RamTotal + ".0" : RamTotal), " GB")); + LabRamWarn.Visibility = + RamGame == 1d && !ModJava.IsGameSet64BitJava(PageInstanceLeft.Instance) && !ModBase.Is32BitSystem && + ModJava.Javas.ExistAnyJava() + ? Visibility.Visible + : Visibility.Collapsed; + HintRamTooHigh.Visibility = RamGame / RamTotal > 0.75d ? Visibility.Visible : Visibility.Collapsed; + if (ShowAnim) + { + // 宽度动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaGridLengthWidth(ColumnRamUsed, RamUsed - ColumnRamUsed.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaGridLengthWidth(ColumnRamGame, RamGameActual - ColumnRamGame.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaGridLengthWidth(ColumnRamEmpty, RamEmpty - ColumnRamEmpty.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)) + }, "VersionSetup Ram Grid"); + } + else + { + // 宽度设置 + ColumnRamUsed.Width = new GridLength(RamUsed, GridUnitType.Star); + ColumnRamGame.Width = new GridLength(RamGameActual, GridUnitType.Star); + ColumnRamEmpty.Width = new GridLength(RamEmpty, GridUnitType.Star); + } + } + + private void RefreshRam() + { + RefreshRam(true); + } + + private int RamTextLeft = 2; + private int RamTextRight = 1; + + /// + /// 刷新 UI 上的文本位置。 + /// + private void RefreshRamText() + { + // 获取宽度信息 + var RectUsedWidth = RectRamUsed.ActualWidth; + var TotalWidth = PanRamDisplay.ActualWidth; + var LabGameWidth = LabRamGame.ActualWidth; + var LabUsedWidth = LabRamUsed.ActualWidth; + var LabTotalWidth = LabRamTotal.ActualWidth; + var LabGameTitleWidth = LabRamGameTitle.ActualWidth; + var LabUsedTitleWidth = LabRamUsedTitle.ActualWidth; + // 左侧 + int Left; + if (RectUsedWidth - 30d < LabUsedWidth || RectUsedWidth - 30d < LabUsedTitleWidth) + // 全写不下了 + Left = 0; + else if (RectUsedWidth - 25d < LabUsedWidth + LabTotalWidth) + // 显示不下完整数据 + Left = 1; + else + // 正常 + Left = 2; + if (RamTextLeft != Left) + { + RamTextLeft = Left; + switch (Left) + { + case 0: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, -LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, -LabRamUsedTitle.Opacity, 100) + }, "VersionSetup Ram TextLeft"); + break; + } + case 1: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100) + }, "VersionSetup Ram TextLeft"); + break; + } + case 2: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, 1d - LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100) + }, "VersionSetup Ram TextLeft"); + break; + } + } + } + + // 右侧 + int Right; + if (TotalWidth < LabGameWidth + 2d + RectUsedWidth || TotalWidth < LabGameTitleWidth + 2d + RectUsedWidth) + // 挤到最右边 + Right = 0; + else + // 正常情况 + Right = 1; + if (Right == 0) + { + if (ModAnimation.AniControlEnabled == 0 && + (RamTextRight != Right || ModAnimation.AniIsRun("VersionSetup Ram TextRight"))) + { + // 需要动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(LabRamGame, TotalWidth - LabGameWidth - LabRamGame.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaX(LabRamGameTitle, TotalWidth - LabGameTitleWidth - LabRamGameTitle.Margin.Left, + 100, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "VersionSetup Ram TextRight"); + } + else + { + // 不需要动画 + LabRamGame.Margin = new Thickness(TotalWidth - LabGameWidth, 3d, 0d, 0d); + LabRamGameTitle.Margin = new Thickness(TotalWidth - LabGameTitleWidth, 0d, 0d, 5d); + } + } + else if (ModAnimation.AniControlEnabled == 0 && + (RamTextRight != Right || ModAnimation.AniIsRun("VersionSetup Ram TextRight"))) + { + // 需要动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(LabRamGame, 2d + RectUsedWidth - LabRamGame.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaX(LabRamGameTitle, 2d + RectUsedWidth - LabRamGameTitle.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "VersionSetup Ram TextRight"); + } + else + { + // 不需要动画 + LabRamGame.Margin = new Thickness(2d + RectUsedWidth, 3d, 0d, 0d); + LabRamGameTitle.Margin = new Thickness(2d + RectUsedWidth, 0d, 0d, 5d); + } + + RamTextRight = Right; + } + + /// + /// 获取当前设置的 RAM 值。单位为 GB。 + /// + public static double GetRam(ModMinecraft.McInstance Version, bool? Is32BitJava = default) + { + // 跟随全局设置 + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(ModBase.Setup.Get("VersionRamType", Version), 2, false))) + return PageSetupLaunch.GetRam(Version, true, Is32BitJava); + + // ------------------------------------------ + // 修改下方代码时需要一并修改 PageSetupLaunch + // ------------------------------------------ + + // 使用当前实例的设置 + var RamGive = default(double); + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(ModBase.Setup.Get("VersionRamType", Version), 0, false))) + { + // 自动配置 + var RamAvailable = + Math.Round((double)(KernelInterop.GetAvailablePhysicalMemoryBytes() / 1024 / 1024 / 1024 * 10)) / 10; + // 确定需求的内存值 + double RamMininum; // 无论如何也需要保证的最低限度内存 + double RamTarget1; // 估计能勉强带动了的内存 + double RamTarget2; // 估计没啥问题了的内存 + double RamTarget3; // 安装过多附加组件需要的内存 + if (Version is not null && !Version.IsLoaded) + Version.Load(); + if (Version is not null && Version.Modable) + { + // 可安装 Mod 的实例 + var ModDir = new DirectoryInfo(Version.PathIndie + @"mods\"); + var ModCount = ModDir.Exists ? ModDir.GetFiles().Length : 0; + RamMininum = 0.5d + ModCount / 150d; + RamTarget1 = 1.5d + ModCount / 90d; + RamTarget2 = 2.7d + ModCount / 50d; + RamTarget3 = 4.5d + ModCount / 25d; + } + else if (Version is not null && Version.Info.HasOptiFine) + { + // OptiFine 实例 + RamMininum = 0.5d; + RamTarget1 = 1.5d; + RamTarget2 = 3d; + RamTarget3 = 5d; + } + else + { + // 普通实例 + RamMininum = 0.5d; + RamTarget1 = 1.5d; + RamTarget2 = 2.5d; + RamTarget3 = 4d; + } + + double RamDelta; + // 预分配内存,阶段一,0 ~ T1,100% + RamDelta = RamTarget1; + RamGive += Math.Min(RamAvailable, RamDelta); + RamAvailable -= RamDelta; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段二,T1 ~ T2,70% + RamDelta = RamTarget2 - RamTarget1; + RamGive += Math.Min(RamAvailable * 0.7d, RamDelta); + RamAvailable -= RamDelta / 0.7d; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段三,T2 ~ T3,40% + RamDelta = RamTarget3 - RamTarget2; + RamGive += Math.Min(RamAvailable * 0.4d, RamDelta); + RamAvailable -= RamDelta / 0.4d; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段四,T3 ~ T3 * 2,15% + RamDelta = RamTarget3; + RamGive += Math.Min(RamAvailable * 0.15d, RamDelta); + RamAvailable -= RamDelta / 0.15d; + if (RamAvailable < 0.1d) + goto PreFin; + PreFin: ; + + // 不低于最低值 + RamGive = Math.Round(Math.Max(RamGive, RamMininum), 1); + } + else + { + // 手动配置 + var Value = Conversions.ToInteger(ModBase.Setup.Get("VersionRamCustom", Version)); + if (Value <= 12) + RamGive = Value * 0.1d + 0.3d; + else if (Value <= 25) + RamGive = (Value - 12) * 0.5d + 1.5d; + else if (Value <= 33) + RamGive = (Value - 25) * 1 + 8; + else + RamGive = (Value - 33) * 2 + 16; + } + + // 若使用 32 位 Java,则限制为 1G + if (Is32BitJava ?? !ModJava.IsGameSet64BitJava(PageInstanceLeft.Instance)) + RamGive = Math.Min(1d, RamGive); + return RamGive; + } + + #endregion + + #region 服务器 + + // 全局 + private int ComboServerLoginLast; + + private void ComboServerLogin_Changed() + { + if (ModAnimation.AniControlEnabled != 0) + return; + ServerLogin(ComboServerLoginRequire.SelectedIndex); + // 检查是否输入正确,正确才触发设置改变 + if (TextServerAuthServer.IsValidated) + BtnServerAuthLock.IsEnabled = true; + else + BtnServerAuthLock.IsEnabled = false; + if ((ComboServerLoginRequire.SelectedIndex == 2 || ComboServerLoginRequire.SelectedIndex == 3) && + !TextServerAuthServer.IsValidated) + return; + // 检查结果是否发生改变,未改变则不触发设置改变 + if (ComboServerLoginLast == ComboServerLoginRequire.SelectedIndex) + return; + // 触发 + ComboServerLoginLast = ComboServerLoginRequire.SelectedIndex; + ComboChange(ComboServerLoginRequire, null); + } + + private void TextServerAuthServer_MouseLeave() + { + if (string.IsNullOrWhiteSpace(TextServerAuthServer.Text)) + return; + if (!(TextServerAuthServer.Text.EndsWithF("/api/yggdrasil/") || + TextServerAuthServer.Text.EndsWithF("/api/yggdrasil"))) + { + if (TextServerAuthServer.Text.EndsWithF("/")) + { + TextServerAuthServer.Text = TextServerAuthServer.Text + "api/yggdrasil"; + ModMain.Hint("已自动格式化验证服务器地址!"); + } + else + { + TextServerAuthServer.Text = TextServerAuthServer.Text + "/api/yggdrasil"; + ModMain.Hint("已自动格式化验证服务器地址!"); + } + } + + if (TextServerAuthServer.Text.EndsWithF("/api/yggdrasil/")) + { + TextServerAuthServer.Text = TextServerAuthServer.Text.BeforeLast("/"); + ModMain.Hint("已自动格式化验证服务器地址!"); + } + + ComboServerLoginLast = ComboServerLoginRequire.SelectedIndex; + ComboChange(ComboServerLoginRequire, null); + } + + public void ServerLogin(int Type) + { + LabServerAuthName.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + TextServerAuthName.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + LabServerAuthRegister.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + TextServerAuthRegister.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + LabServerAuthServer.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + TextServerAuthServer.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + BtnServerAuthLittle.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + BtnServerNewProfile.Visibility = Type == 2 || Type == 3 ? Visibility.Visible : Visibility.Collapsed; + if (Type == 0 || Type == 1) + BtnServerAuthLock.Visibility = Visibility.Collapsed; + else + BtnServerAuthLock.Visibility = Visibility.Visible; + if (Conversions.ToBoolean(ModBase.Setup.Get("VersionServerLoginLock", PageInstanceLeft.Instance))) + { + HintServerLoginLock.Visibility = Visibility.Visible; + ComboServerLoginRequire.IsEnabled = false; + TextServerAuthServer.IsEnabled = false; + TextServerAuthName.IsEnabled = false; + TextServerAuthRegister.IsEnabled = false; + BtnServerAuthLittle.IsEnabled = false; + } + else + { + HintServerLoginLock.Visibility = Visibility.Collapsed; + ComboServerLoginRequire.IsEnabled = true; + TextServerAuthServer.IsEnabled = true; + TextServerAuthName.IsEnabled = true; + TextServerAuthRegister.IsEnabled = true; + BtnServerAuthLittle.IsEnabled = true; + } + + CardServer.TriggerForceResize(); + // 避免正版验证和离线验证出现此提示 + if (!(Type == 2 || Type == 3)) + { + LabServerAuthServerSecurity.Visibility = Visibility.Collapsed; + LabServerAuthServerSecurityCL.Visibility = Visibility.Collapsed; + LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed; + } + // 如果开头为 http:// 给予警告 + else if (TextServerAuthServer.Text.StartsWithF("https://")) + { + LabServerAuthServerSecurity.Visibility = Visibility.Collapsed; + LabServerAuthServerSecurityVerify.Visibility = Visibility.Visible; + LabServerAuthServerSecurityCL.Visibility = Visibility.Visible; + } + else if (TextServerAuthServer.Text.StartsWithF("http://")) + { + LabServerAuthServerSecurity.Visibility = Visibility.Visible; + LabServerAuthServerSecurityCL.Visibility = Visibility.Visible; + LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed; + } + else + { + LabServerAuthServerSecurity.Visibility = Visibility.Collapsed; + LabServerAuthServerSecurityVerify.Visibility = Visibility.Collapsed; + LabServerAuthServerSecurityCL.Visibility = Visibility.Collapsed; + } + } + + // LittleSkin + private void BtnServerAuthLittle_Click(object sender, EventArgs e) + { + if (!string.IsNullOrEmpty(TextServerAuthServer.Text) && + TextServerAuthServer.Text != "https://littleskin.cn/api/yggdrasil" && ModMain.MyMsgBox( + "即将把第三方登录设置覆盖为 LittleSkin 登录。" + "\r\n" + "除非你是服主,或者服主要求你这样做,否则请不要继续。" + "\r\n" + + "\r\n" + "是否确实需要覆盖当前设置?", "设置覆盖确认", "继续", "取消") == 2) + return; + TextServerAuthServer.Text = "https://littleskin.cn/api/yggdrasil"; + TextServerAuthRegister.Text = "https://littleskin.cn/auth/register"; + TextServerAuthName.Text = "LittleSkin 登录"; + } + + // 锁定设置 + private void BtnServerAuthLock_Click() + { + if (ModMain.MyMsgBox( + $"你正在选择锁定此实例的验证方式。锁定之后,将无法再更改此实例的验证方式要求,启动此实例将必须使用指定的验证方式。{"\r\n"}此功能可能会帮助一些服主吧。{"\r\n"}是否继续?", + "锁定验证方式确认", "确定", "取消", IsWarn: true) == 1) + { + Config.Instance.AuthTypeLucked[PageInstanceLeft.Instance] = true; + Reload(); + } + } + + // 跳转新建档案 + private void BtnServerNewProfile_Click() + { + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.Launch }); + PageLoginAuth.DraggedAuthServer = TextServerAuthServer.Text; + ModBase.RunInNewThread(() => + { + Thread.Sleep(150); + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true, ModLaunch.McLoginType.Auth)); + }); + } + + private static void TextServerEnter_Change(MyTextBox sender, object e) + { + sender.Text = sender.Text.Replace(":", ":"); + } + + #endregion + + #region Java 选择 + + // 刷新 Java 下拉框显示 + public void RefreshJavaComboBox() + { + if (ComboArgumentJava is null) + return; + + // 获取实例的 Java 偏好(已兼容新旧格式) + var preference = ModJava.GetInstanceJavaPreference(PageInstanceLeft.Instance); + + // === 1. 初始化固定选项(使用类型安全的 Tag) === + ComboArgumentJava.Items.Clear(); + + // 选项 0: 跟随全局设置 + ComboArgumentJava.Items.Add(new MyComboBoxItem + { + Content = "跟随全局设置", + Tag = new UseGlobalPreference() + }); + + // 选项 1: 自动选择 + ComboArgumentJava.Items.Add(new MyComboBoxItem + { + Content = "自动选择合适的 Java", + Tag = new AutoSelect() // Nothing 表示自动选择 + }); + + // 选项 2: 相对路径选项 + MyComboBoxItem relativePathItem; + if (preference is UseRelativePath) + { + var relPref = (UseRelativePath)preference; + var absPath = Path.GetFullPath(Path.Combine(Basics.ExecutableDirectory, relPref.RelativePath)); + var javaEntry = ModJava.Javas.Get(absPath); + + if (Files.IsPathWithinDirectory(absPath, Basics.ExecutableDirectory) && javaEntry is not null && + javaEntry.IsEnabled) + // 有效路径:显示具体 Java 信息 + relativePathItem = new MyComboBoxItem + { + Content = $"启动器目录下的 Java | {javaEntry}", + Tag = new UseRelativePath(relPref.RelativePath), + ToolTip = $"相对路径: {relPref.RelativePath}{"\r\n"}解析路径: {absPath}" + }; + else + // 无效路径:提示用户重新选择 + relativePathItem = new MyComboBoxItem + { + Content = "选择启动器目录下的 Java(当前路径无效)", + Tag = new UseRelativePath(relPref.RelativePath), + ToolTip = $"无效路径: {absPath}{"\r\n"}点击此项重新选择有效 Java" + }; + } + else + { + // 未配置相对路径:使用默认模板 + relativePathItem = new MyComboBoxItem + { + Content = "选择启动器目录下的 Java", + Tag = new UseRelativePath(@"jre\bin\java.exe"), + ToolTip = "将选择相对于实例目录的 Java 路径" + }; + } + + ComboArgumentJava.Items.Add(relativePathItem); + + // === 2. 添加所有可用 Java 运行时 === + MyComboBoxItem selectedItem = null; + try + { + foreach (var curJava in ModJava.Javas.GetSortedJavaList()) + { + var item = new MyComboBoxItem + { + Content = curJava.ToString(), + ToolTip = + $"路径: {((dynamic)curJava).Installation.JavaExePath}{"\r\n"}版本: {((dynamic)curJava).Installation.Version}{"\r\n"}来源: {((dynamic)curJava).Source}", + Tag = curJava + }; + ToolTipService.SetInitialShowDelay(item, 300); + ToolTipService.SetBetweenShowDelay(item, 100); + ComboArgumentJava.Items.Add(item); + } + } + catch (Exception ex) + { + Config.Instance.SelectedJava[PageInstanceLeft.Instance] = "使用全局设置"; + ModBase.Log(ex, "更新实例设置 Java 下拉框失败", ModBase.LogLevel.Feedback); + ComboArgumentJava.Items.Clear(); + ComboArgumentJava.Items.Add(new MyComboBoxItem + { + Content = "列表加载失败,请重试", + IsEnabled = false + }); + ComboArgumentJava.SelectedIndex = 0; + RefreshRam(true); + return; + } + + // === 3. 根据当前偏好设置选中项(优先使用新格式 preference) === + if (preference is null) + { + // 自动选择 + selectedItem = ComboArgumentJava.Items[1] as MyComboBoxItem; + } + else if (preference is UseGlobalPreference) + { + selectedItem = ComboArgumentJava.Items[0] as MyComboBoxItem; + } + else if (preference is UseRelativePath) + { + selectedItem = ComboArgumentJava.Items[2] as MyComboBoxItem; + } + else if (preference is ExistingJava) + { + var existPref = (ExistingJava)preference; + // 在 Java 列表中查找匹配项(从索引 3 开始) + for (int i = 3, loopTo = ComboArgumentJava.Items.Count - 1; i <= loopTo; i++) + { + var item = ComboArgumentJava.Items[i] as MyComboBoxItem; + if (item is not null && item.Tag is JavaEntry) + { + var javaEntry = (JavaEntry)item.Tag; + if (string.Equals(javaEntry.Installation.JavaExePath, existPref.JavaExePath, + StringComparison.OrdinalIgnoreCase)) + { + selectedItem = item; + break; + } + } + } + } + + // 降级处理:无匹配项时回退到自动选择 + if (selectedItem is null && ComboArgumentJava.Items.Count > 1) + selectedItem = ComboArgumentJava.Items[1] as MyComboBoxItem; + + // 设置选中项 + if (selectedItem is not null) ComboArgumentJava.SelectedItem = selectedItem; + + // === 4. 无可用 Java 时的降级处理 === + if (!ModJava.Javas.ExistAnyJava() && ComboArgumentJava.Items.Count <= 3) + { + ComboArgumentJava.Items.Clear(); + var noJavaItem = new MyComboBoxItem + { + Content = "未检测到可用的 Java 运行时", + ToolTip = "请在设置中手动指定 Java 路径,或点击'扫描'按钮重新检测", + IsEnabled = false + }; + ComboArgumentJava.Items.Add(noJavaItem); + ComboArgumentJava.SelectedItem = noJavaItem; + } + + // === 5. 刷新关联控件 === + RefreshRam(true); + } + + // 阻止在无效状态下展开下拉框 + private void ComboArgumentJava_DropDownOpened(object sender, EventArgs e) + { + if (ComboArgumentJava.SelectedItem is null) + { + ComboArgumentJava.IsDropDownOpen = false; + return; + } + + var firstItem = ComboArgumentJava.Items[0] as MyComboBoxItem; + if (firstItem is not null && + (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(firstItem.Content, "未检测到可用的 Java 运行时", false)) || + Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(firstItem.Content, "列表加载失败,请重试", false)))) + ComboArgumentJava.IsDropDownOpen = false; + } + + // 下拉框选择更改处理(保存新格式配置) + private void JavaSelectionUpdate(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (ComboArgumentJava.SelectedItem is null) + return; + + var selectedItem = ComboArgumentJava.SelectedItem as MyComboBoxItem; + if (selectedItem is null || (selectedItem.Tag is null && + Conversions.ToBoolean( + Operators.ConditionalCompareObjectNotEqual(selectedItem.Content, + "自动选择合适的 Java", false)))) + return; + + JavaPreference preference = default; + var logMessage = ""; + + // 根据 Tag 类型生成偏好对象 + if (selectedItem.Tag is null) + { + // 自动选择:存储空字符串 + preference = new AutoSelect(); + logMessage = "[Java] 修改实例 Java 选择设置:自动选择"; + } + else if (selectedItem.Tag is UseGlobalPreference) + { + preference = new UseGlobalPreference(); + logMessage = "[Java] 修改实例 Java 选择设置:跟随全局设置"; + } + else if (selectedItem.Tag is UseRelativePath) + { + // 相对路径:需要用户选择实际文件 + var ret = SystemDialogs.SelectFile("Java 程序(java.exe)|java.exe", "选择 Java 程序", Basics.ExecutableDirectory); + if (string.IsNullOrWhiteSpace(ret)) + // 用户取消,不保存配置,保持原选择 + return; + + ret = Path.GetFullPath(ret); + var relativePath = Path.GetRelativePath(Basics.ExecutableDirectory, ret); + + // 验证路径是否在启动器目录内 + if (!Files.IsPathWithinDirectory(relativePath, Basics.ExecutableDirectory)) + { + ModMain.Hint("超出路径允许范围,请选择启动器文件夹或其子文件夹下的文件", ModMain.HintType.Critical); + return; + } + + preference = new UseRelativePath(relativePath); + logMessage = $"[Java] 修改实例 Java 选择设置:相对路径 | {relativePath}"; + } + else if (selectedItem.Tag is JavaEntry) + { + var javaEntry = (JavaEntry)selectedItem.Tag; + preference = new ExistingJava(javaEntry.Installation.JavaExePath); + logMessage = $"[Java] 修改实例 Java 选择设置:{javaEntry}"; + } + + // 保存配置 + var json = JsonSerializer.Serialize(preference); + Config.Instance.SelectedJava[PageInstanceLeft.Instance.PathInstance] = json; + + + ModBase.Log(logMessage); + RefreshRam(true); + } + + #endregion + + #region 其他设置 + + // 版本隔离警告 + private bool IsReverting; + + private void ComboArgumentIndieV2_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (IsReverting) + return; + if (ModMain.MyMsgBox( + "调整版本隔离后,你可能得把游戏存档、Mod 等文件手动迁移到新的游戏文件夹中。" + "\r\n" + "如果修改后发现存档消失,把这项设置改回来就能恢复。" + + "\r\n" + "如果你不会迁移存档,不建议修改这项设置!", "警告", "我知道我在做什么", "取消", IsWarn: true) == 2) + { + IsReverting = true; + ComboArgumentIndieV2.SelectedItem = e.RemovedItems[0]; + IsReverting = false; + } + } + + // 游戏窗口 + private void CheckArgumentTitleEmpty_Change(MyCheckBox sender, object e) + { + TextArgumentTitle.HintText = CheckArgumentTitleEmpty.Checked == true ? "默认" : "跟随全局设置"; + } + + private void TextArgumentTitle_TextChanged(object sender, TextChangedEventArgs e) + { + CheckArgumentTitleEmpty.Visibility = + TextArgumentTitle.Text.Length > 0 ? Visibility.Collapsed : Visibility.Visible; + } + + #endregion + + #region 高级设置 + + private void TextAdvanceRun_TextChanged(object sender, TextChangedEventArgs e) + { + CheckAdvanceRunWait.Visibility = + string.IsNullOrEmpty(TextAdvanceRun.Text) ? Visibility.Collapsed : Visibility.Visible; + } + + private void ComboAdvanceRenderer_SelectionChanged(MyComboBox sender, object e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (Conversions.ToBoolean(!(bool)States.Hint.Renderer && ComboAdvanceRenderer.SelectedIndex != 0)) + { + if (ModMain.MyMsgBox("修改此项会严重影响游戏的稳定性与性能。如果你不知道你在做什么,不要修改此选项!" + "\r\n" + "你确定要继续修改吗?", "警告", + "我知道我在做什么", "取消", IsWarn: true) == 2) + { + ComboAdvanceRenderer.SelectedItem = ((dynamic)e).RemovedItems(0); + } + else + { + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex, + instance: PageInstanceLeft.Instance); + States.Hint.Renderer = true; + } + } + else + { + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex, + instance: PageInstanceLeft.Instance); + } + } + + private void CheckAdvanceRenderer_CheckChanged(MyCheckBox sender, object e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (CheckUseDebugLog4j2Config.Checked.GetValueOrDefault() && !States.Hint.DebugLog4j2Config) + { + if (ModMain.MyMsgBox( + "本选项会修改游戏日志级别修改为最低,大量日志输出会消耗大量磁盘空间并可能影响游戏性能。这也可能带来一定安全风险。如果你不知道你在做什么,不要修改此选项!" + "\r\n" + + "你确定要继续修改吗?", "警告", "我知道我在做什么", "取消", IsWarn: true) == 2) + { + sender.Checked = false; + } + else + { + Config.Instance.UseDebugLof4j2Config[PageInstanceLeft.Instance] = sender.Checked.GetValueOrDefault(); + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.Checked, + instance: PageInstanceLeft.Instance); + States.Hint.DebugLog4j2Config = true; + } + } + else + { + Config.Instance.UseDebugLof4j2Config[PageInstanceLeft.Instance] = sender.Checked.GetValueOrDefault(); + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml b/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml index 381cd07c5..9854e3c08 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml @@ -1,19 +1,20 @@ - - + + RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}" + SnapsToDevicePixels="True"> - + - - + + @@ -22,53 +23,61 @@ - + - + - + ToolTipService.Placement="Top" MaxWidth="180" /> - - + + - + - + - + + Click="BtnConnect_Click" /> + Logo="M577.173333 138.666667v38.986666A92.8 92.8 0 0 0 629.333333 260.693333a276.106667 276.106667 0 0 1 37.92 22.293334 91.733333 91.733333 0 0 0 99.04 4.266666l32.693334-19.04 65.706666 114.773334-33.493333 19.52a92.693333 92.693333 0 0 0-45.386667 87.413333c0.693333 7.84 1.013333 15.093333 1.013334 22.08s-0.32 14.24-1.013334 22.08a92.693333 92.693333 0 0 0 45.386667 87.413333l33.493333 19.52-65.706666 114.773334-32.693334-19.04a91.733333 91.733333 0 0 0-99.04 4.266666 276.106667 276.106667 0 0 1-37.92 22.293334 92.8 92.8 0 0 0-52.266666 83.04V885.333333H446.826667v-38.986666A92.8 92.8 0 0 0 394.666667 763.306667a276.106667 276.106667 0 0 1-37.92-22.293334 91.733333 91.733333 0 0 0-99.04-4.266666l-32.693334 19.04-65.813333-114.773334 33.493333-19.52a92.693333 92.693333 0 0 0 45.386667-87.413333c-0.693333-7.84-1.013333-15.093333-1.013333-22.08s0.32-14.24 1.013333-22.08a92.693333 92.693333 0 0 0-45.386667-87.413333l-33.493333-19.52 65.706667-114.773334 32.693333 19.04a91.733333 91.733333 0 0 0 99.04-4.266666A276.106667 276.106667 0 0 1 394.666667 260.693333a92.8 92.8 0 0 0 52.266666-83.04V138.666667h130.346667M512 692.48a182.08 182.08 0 0 0 40.426667-4.533333 178.826667 178.826667 0 0 0 134.666666-135.573334 181.333333 181.333333 0 0 0-35.36-153.653333A178.453333 178.453333 0 0 0 512 331.52a182.08 182.08 0 0 0-40.426667 4.533333 178.826667 178.826667 0 0 0-134.666666 135.573334 181.333333 181.333333 0 0 0 35.36 153.653333 178.453333 178.453333 0 0 0 139.733333 67.2M634.666667 64H389.6a17.546667 17.546667 0 0 0-17.44 17.6v96a17.546667 17.546667 0 0 1-10.026667 16 352.746667 352.746667 0 0 0-48.32 28.373333 17.6 17.6 0 0 1-9.813333 3.093334 17.013333 17.013333 0 0 1-8.533333-2.346667l-82.666667-48a17.12 17.12 0 0 0-8.693333-2.4A17.386667 17.386667 0 0 0 188.746667 181.333333l-122.666667 213.866667a17.706667 17.706667 0 0 0 6.4 24.053333l82.346667 48a17.493333 17.493333 0 0 1 8.533333 16.746667c-0.746667 9.333333-1.226667 18.72-1.226667 28.266667s0.48 18.933333 1.226667 28.266666a17.6 17.6 0 0 1-8.533333 16.746667l-82.346667 48a17.706667 17.706667 0 0 0-6.4 24.053333l122.666667 213.866667a17.333333 17.333333 0 0 0 15.093333 8.8 17.066667 17.066667 0 0 0 8.693333-2.346667l82.666667-48a16.48 16.48 0 0 1 8.533333-2.346666 17.6 17.6 0 0 1 10.08 3.253333 352.746667 352.746667 0 0 0 48.32 28.373333 17.546667 17.546667 0 0 1 10.026667 16v96a17.546667 17.546667 0 0 0 17.44 17.6H634.666667a17.546667 17.546667 0 0 0 17.44-17.6v-96a17.546667 17.546667 0 0 1 10.026666-16 352.746667 352.746667 0 0 0 48.32-28.373333 17.6 17.6 0 0 1 10.08-3.253333 16.48 16.48 0 0 1 8.533334 2.346666l82.666666 48a17.066667 17.066667 0 0 0 8.693334 2.346667 17.333333 17.333333 0 0 0 15.093333-8.8l122.133333-213.866667a17.706667 17.706667 0 0 0-6.4-24.053333l-82.346666-48a17.6 17.6 0 0 1-8.533334-16.746667c0.746667-9.333333 1.226667-18.72 1.226667-28.266666s-0.48-18.933333-1.226667-28.266667a17.493333 17.493333 0 0 1 8.533334-16.746667l82.346666-48a17.706667 17.706667 0 0 0 6.4-24.053333L835.253333 181.333333a17.386667 17.386667 0 0 0-15.093333-8.853333 17.12 17.12 0 0 0-8.693333 2.4l-82.666667 48a17.013333 17.013333 0 0 1-8.533333 2.346667 17.6 17.6 0 0 1-10.08-3.253334 352.746667 352.746667 0 0 0-48.32-28.373333 17.546667 17.546667 0 0 1-10.026667-16v-96A17.546667 17.546667 0 0 0 634.666667 64zM512 617.813333a105.973333 105.973333 0 0 1-24-208.96 109.973333 109.973333 0 0 1 24-2.666666 105.973333 105.973333 0 0 1 24 208.96 109.973333 109.973333 0 0 1-24 2.666666z"> - - - + + + - + diff --git a/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml.cs new file mode 100644 index 000000000..bddd44410 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageInstance/ServerCard.xaml.cs @@ -0,0 +1,228 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using Microsoft.VisualBasic; +using PCL.Core.UI; +using PCL.Core.UI.Theme; + +namespace PCL; + +public partial class ServerCard +{ + private readonly IconManager _manager; + public MinecraftServerInfo Server; + + public ServerCard() + { + InitializeComponent(); + + DataContext = new IconManager(); + + // 示例:可在代码中切换图标 + _manager = DataContext as IconManager; + _manager.AddIconFromXaml("signal_1", + ""); + _manager.AddIconFromXaml("signal_2", + ""); + _manager.AddIconFromXaml("signal_3", + ""); + _manager.AddIconFromXaml("signal_4", + ""); + _manager.AddIconFromXaml("signal_5", + ""); + _manager.AddIconFromXaml("signal_offline", + ""); + _manager.AddIconFromXaml("loading", + ""); + } + + public event EventHandler? RemoveServer; + public event EventHandler? EditServer; + + private void BtnSkin_Click(object sender, EventArgs eventArgs) + { + BtnSetting.ContextMenu.IsOpen = true; + } + + /// + /// 初始化服务器卡片 + /// + public void UpdateServerInfo(MinecraftServerInfo serverInfo) + { + Server = serverInfo; + ModBase.RunInUi(() => UpdateServerUi()); + } + + /// + /// 更新服务器UI + /// + private async void UpdateServerUi() + { + if (Server is null) + return; + + // 更新服务器名称 + ServerName.Text = Server.Name; + await ImageLoaderHelper.SetServerLogoAsync(Server.Icon, ServerIcon); + if (Server.Status == ServerStatus.Online) + { + _manager.SetSelectedIconByName(GetSignalIcon(Server.Ping)); + Signal.ToolTip = Server.Ping + "ms"; + ToolTipService.SetInitialShowDelay(Signal, 0); + ToolTipService.SetBetweenShowDelay(Signal, 50); + ToolTipService.SetPlacement(Signal, PlacementMode.Top); + + if (Server.PlayerCount != default && Server.MaxPlayers != default) + ServerPlayer.Text = $"{Server.PlayerCount} / {Server.MaxPlayers}"; + else + ServerPlayer.Text = "???"; + + ServerMotD.Visibility = Visibility.Collapsed; + MotdRenderer.RenderMotd(Server.Description, ThemeService.IsDarkMode, 2); + MotdRenderer.RenderCanvas(); + } + else if (Server.Status == ServerStatus.Pinging) + { + _manager.SetSelectedIconByName("loading"); + MotdRenderer.ClearCanvas(); + ServerPlayer.Text = "正在连接"; + ServerMotD.Text = "正在连接..."; + ServerMotD.Visibility = Visibility.Visible; + } + else if (Server.Status == ServerStatus.Offline) + { + _manager.SetSelectedIconByName("signal_offline"); + MotdRenderer.ClearCanvas(); + ServerPlayer.Text = "离线"; + ServerMotD.Text = "服务器离线"; + ServerMotD.Visibility = Visibility.Visible; + } + } + + private string GetSignalIcon(int ping) + { + switch (ping) + { + case var @case when 0 <= @case && @case <= 99: + { + return "signal_5"; // 5 条信号 + } + case var case1 when 100 <= case1 && case1 <= 299: + { + return "signal_4"; // 4 条信号 + } + case var case2 when 300 <= case2 && case2 <= 599: + { + return "signal_3"; // 3 条信号 + } + case var case3 when 600 <= case3 && case3 <= 999: + { + return "signal_2"; // 2 条信号 + } + + default: + { + return "signal_1"; // 1 条信号 + } + } + } + + /// + /// 刷新服务器状态 + /// + public async Task RefreshServerStatus(bool withHint, CancellationToken token = default) + { + if (withHint) ModMain.Hint($"正在刷新服务器 {Server.Name} 的状态..."); + Server.Status = ServerStatus.Pinging; + await Dispatcher.InvokeAsync(() => UpdateServerUi()); + var serverInfo = await PageInstanceServer.PingServer(Server, token); + UpdateServerInfo(serverInfo); + } + + /// + /// 连接到服务器 + /// + private void BtnConnect_Click(object sender, EventArgs e) + { + try + { + var launchOptions = new ModLaunch.McLaunchOptions { ServerIp = Server.Address }; + ModLaunch.McLaunchStart(launchOptions); + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.Launch }); + ModMain.Hint($"正在连接到服务器 {Server.Name}..."); + } + catch (Exception ex) + { + ModBase.Log(ex, "启动服务器失败", ModBase.LogLevel.Feedback); + ModMain.Hint("启动服务器失败:" + ex.Message, ModMain.HintType.Critical); + } + } + + /// + /// 复制服务器地址 + /// + private void BtnCopy_Click(object sender, RoutedEventArgs e) + { + try + { + Clipboard.SetText(Server.Address); + ModMain.Hint($"已复制服务器地址:{Server.Address}", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "复制服务器地址失败"); + ModMain.Hint("复制服务器地址失败", ModMain.HintType.Critical); + } + } + + /// + /// 刷新服务器状态 + /// + private async void BtnRefresh_Click(object sender, RoutedEventArgs e) + { + await Task.Run(async () => await RefreshServerStatus(true)); + } + + /// + /// 编辑服务器信息 + /// + private void BtnEdit_Click(object sender, RoutedEventArgs e) + { + try + { + // Get server information + var result = PageInstanceServer.GetServerInfo(Server); + if (!result.Success) return; + + EditServer?.Invoke(this, new ResultEventArgs(result.Name, result.Address)); + } + + // Update server object + // _server.Name = result.Name + // _server.Address = result.Address + + catch (Exception ex) + { + ModMain.Hint("编辑服务器信息失败:" + ex.Message, ModMain.HintType.Critical); + } + } + + private void BtnRemove_Click(object sender, RoutedEventArgs e) + { + if (ModMain.MyMsgBox( + "你确定要移除服务器 " + Server.Name + " 吗?" + "\r\n" + "'" + Server.Address + + "' 将从您的列表中移除,包括游戏内列表,且无法恢复。", "移除服务器确认", "确认", "取消") == 1) RemoveServer?.Invoke(this, EventArgs.Empty); + } + + public class ResultEventArgs : EventArgs + { + public ResultEventArgs(string param1, string param2) + { + Param1 = param1; + Param2 = param2; + } + + public string Param1 { get; set; } + public string Param2 { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml index 8ed5b6f55..5dbb56fc1 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml @@ -1,18 +1,22 @@ - + - + - + @@ -22,20 +26,31 @@ - + - - + - - - + + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs new file mode 100644 index 000000000..5263bdbf7 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.cs @@ -0,0 +1,261 @@ +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.UI.Controls; + +namespace PCL; + +public partial class MyMsgLogin +{ + private readonly JObject Data; + private string DeviceCode; // 用于轮询的设备代码 + private string OAuthUrl = ""; // OAuth 轮询验证地址 + private string UserCode; // 需要用户在网页上输入的设备代码 + private string Website; // 验证网页的网址 + + public MyMsgLogin() + { + InitializeComponent(); + // Handles + Loaded += Load; + Btn1.Click += Btn1_Click; + Btn3.Click += Btn3_Click; + PanBorder.MouseLeftButtonDown += Drag; + LabTitle.MouseLeftButtonDown += Drag; + } + + private void Finished(object Result) + { + if (MyConverter.IsExited) + return; + MyConverter.IsExited = true; + MyConverter.Result = Result; + ModBase.RunInUi(Close); + Thread.Sleep(200); + ModMain.FrmMain.ShowWindowToTop(); + } + + private void Init() + { + UserCode = (string)Data["user_code"]; + DeviceCode = (string)Data["device_code"]; + ModBase.ClipboardSet(DeviceCode); + if (Data["verification_uri_complete"] is not null) + { + Website = (string)Data["verification_uri_complete"]; + LabCaption.Text = "登录网页将自动开启,授权码将自动填充。" + "\r\n" + "\r\n" + + "如果网络环境不佳,网页可能一直加载不出来,届时请使用 VPN 并重试。" + "\r\n" + + $"如果没有自动填充,请在页面内粘贴此授权码 {UserCode} (将自动复制)" + "\r\n" + + $"你也可以用其他设备打开 {Website} 并输入授权码。"; + } + else + { + Website = (string)Data["verification_uri"]; + LabCaption.Text = $"登录网页将自动开启,请在网页中输入授权码 {UserCode}(将自动复制)。" + "\r\n" + "\r\n" + + "如果网络环境不佳,网页可能一直加载不出来,届时请使用 VPN 并重试。" + "\r\n" + + $"你也可以用其他设备打开 {Website} 并输入上述授权码。"; + } + + // 设置 UI + LabTitle.Text = "登录 Minecraft"; + Btn1.EventData = Website; + Btn2.EventData = UserCode; + // 启动工作线程 + ModBase.RunInNewThread(WorkThread, "MyMsgLogin"); + } + + private void WorkThread() + { + Thread.Sleep(3000); + if (MyConverter.IsExited) + return; + ModBase.OpenWebsite(Website); + ModBase.ClipboardSet(UserCode); + Thread.Sleep((Data["interval"].ToObject() - 1) * 1000); + // 轮询 + var UnknownFailureCount = 0; + while (!MyConverter.IsExited) + try + { + var Result = ModNet.NetRequestOnce("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", + "POST", + "grant_type=urn:ietf:params:oauth:grant-type:device_code" + "&" + "client_id=" + + ModSecret.OAuthClientId + "&" + "device_code=" + DeviceCode + "&" + + "scope=XboxLive.signin%20offline_access", "application/x-www-form-urlencoded", + 5000 + UnknownFailureCount * 5000, MakeLog: false); + // 获取结果 + var ResultJson = (JObject)ModBase.GetJson(Result); + ModProfile.ProfileLog($"令牌过期时间:{ResultJson["expires_in"]} 秒"); + ModMain.Hint("网页登录成功!", ModMain.HintType.Finish); + Finished(new[] { ResultJson["access_token"].ToString(), ResultJson["refresh_token"].ToString() }); + return; + } + catch (ModNet.HttpWebException ex) + { + var response = ex.InnerHttpException.WebResponse; + if (response.Contains("authorization_declined")) + { + Finished(new Exception("$你拒绝了 PCL 申请的权限……")); + return; + } + + if (response.Contains("expired_token")) + { + Finished(new Exception("$登录用时太长啦,重新试试吧!")); + return; + } + + if (response.Contains("Account security interrupt")) + { + Finished(new Exception("$非常抱歉,该账号由于安全问题无法登陆,请前往 Microsoft 账户页获取更多信息。")); + return; + } + + if (response.Contains("service abuse")) + { + Finished(new Exception("$非常抱歉,该账号已被微软封禁,无法登录。")); + return; + } + + if (response.Contains("AADSTS70000")) // 可能不能判 “invalid_grant”,见 #269 + { + Finished(new ModBase.RestartException()); + return; + } + + if (response.Contains("authorization_pending")) + { + Thread.Sleep(2000); + } + else if (UnknownFailureCount <= 2) + { + UnknownFailureCount += 1; + ModBase.Log(ex, $"正版验证轮询第 {UnknownFailureCount} 次失败"); + ModBase.Log("原始返回内容: " + response); + Thread.Sleep(2000); + } + else + { + Finished(new Exception("正版验证轮询失败", ex)); + return; + } + } + catch (Exception ex) + { + if (UnknownFailureCount <= 2) + { + UnknownFailureCount += 1; + ModBase.Log(ex, $"正版验证轮询第 {UnknownFailureCount} 次失败"); + ModBase.Log(ex.Message); + Thread.Sleep(2000); + } + else + { + Finished(new Exception("正版验证轮询失败", ex)); + return; + } + } + } + + + #region 弹窗 + + private readonly ModMain.MyMsgBoxConverter MyConverter; + private readonly int Uuid = ModBase.GetUuid(); + + public MyMsgLogin(ModMain.MyMsgBoxConverter Converter) + { + try + { + InitializeComponent(); + Btn1.Name += ModBase.GetUuid(); + Btn2.Name += ModBase.GetUuid(); + Btn3.Name += ModBase.GetUuid(); + MyConverter = Converter; + ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); + Data = (JObject)Converter.Content; + OAuthUrl = Conversions.ToString(Converter.AuthUrl); + Init(); + } + catch (Exception ex) + { + ModBase.Log(ex, "正版验证弹窗初始化失败", ModBase.LogLevel.Hint); + } + + Loaded += Load; + } + + private void Load(object sender, EventArgs e) + { + try + { + // 动画 + Opacity = 0d; + ModAnimation.AniStart( + ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, BlurBorder.BackgroundProperty, + (MyConverter.IsWarn + ? new ModBase.MyColor(140d, 80d, 0d, 0d) + : new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.FrmMain.PanMsgBackground.Background, 200), + "PanMsgBackground Background"); + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(this, 1d, 120, 60), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, + -TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + -TransformRotate.Angle, 300, 60, + new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "MyMsgBox " + Uuid); + // 记录日志 + ModBase.Log("[Control] 正版验证弹窗:" + LabTitle.Text + "\r\n" + LabCaption.Text); + } + catch (Exception ex) + { + ModBase.Log(ex, "正版验证弹窗加载失败", ModBase.LogLevel.Hint); + } + } + + private void Close() + { + // 动画 + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + if (!ModMain.WaitingMyMsgBox.Any()) + ModAnimation.AniStart(ModAnimation.AaColor(ModMain.FrmMain.PanMsgBackground, + BlurBorder.BackgroundProperty, + new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.FrmMain.PanMsgBackground.Background, 200, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))); + }, 30), + ModAnimation.AaOpacity(this, -Opacity, 80, 20), + ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y, + 150, 0, new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i, + 6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), After: true) + }, "MyMsgBox " + Uuid); + } + + // 实现回车和 Esc 的接口(#4857) + public void Btn1_Click(object sender, MouseButtonEventArgs e) + { + } + + public void Btn3_Click(object sender, MouseButtonEventArgs e) + { + Finished(new ThreadInterruptedException()); + } + + private void Drag(object sender, MouseButtonEventArgs e) + { + // On Error Resume Next + if (e.GetPosition(ShapeLine).Y <= 2d) + ModMain.FrmMain.DragMove(); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml index 3a2e68c5e..3b97a58c8 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml @@ -1,22 +1,30 @@ - + Height="64" Width="64" UseLayoutRounding="True" RenderTransformOrigin="0.5,0.5" + Background="{StaticResource ColorBrushSemiTransparent}" + ToolTipService.Placement="Center" ToolTipService.VerticalOffset="-50" ToolTipService.HorizontalOffset="2" + ToolTipService.InitialShowDelay="100"> - + - - - + + + - - - + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.cs new file mode 100644 index 000000000..de2f3a32c --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.cs @@ -0,0 +1,486 @@ +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.UI; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; + +namespace PCL; + +public partial class MySkin +{ + public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e); + + // 皮肤储存 + private string _Address; + private bool IsChanging; + + // 点击 + private bool IsSkinMouseDown; + public ModLoader.LoaderTask, string> Loader; + + public MySkin() + { + InitializeComponent(); + MouseEnter += PanSkin_MouseEnter; + MouseLeave += PanSkin_MouseLeave; + MouseLeftButtonDown += PanSkin_MouseLeftButtonDown; + MouseLeftButtonUp += PanSkin_MouseLeftButtonUp; + // Handles + BtnSkinSave.Click += BtnSkinSave_Click; + BtnSkinSave.Checked += BtnSkinSave_Checked; + BtnSkinRefresh.Click += RefreshClick; + BtnSkinCape.Click += BtnSkinCape_Click; + } + + public string Address + { + get => _Address; + set + { + _Address = value; + ToolTip = string.IsNullOrEmpty(_Address) ? "加载中" : "点击更换皮肤(右键查看更多选项)"; + } + } + + // 披风 + public bool HasCape + { + get => BtnSkinCape.Visibility == Visibility.Collapsed; + set + { + if (value) + BtnSkinCape.Visibility = Visibility.Visible; + else + BtnSkinCape.Visibility = Visibility.Collapsed; + } + } + + // 事件 + public event ClickEventHandler? Click; + + // 控件动画 + private void PanSkin_MouseEnter(object sender, MouseEventArgs e) + { + ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.8d - ShadowSkin.Opacity, 200, 100), "Skin Shadow"); + } + + private void PanSkin_MouseLeave(object sender, MouseEventArgs e) + { + ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.2d - ShadowSkin.Opacity, 200), "Skin Shadow"); + IsSkinMouseDown = false; + ModAnimation.AniStart( + ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60, + Ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale"); + } + + private void PanSkin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + IsSkinMouseDown = true; + ModAnimation.AniStart( + ModAnimation.AaScaleTransform(this, 0.9d - ((ScaleTransform)RenderTransform).ScaleX, 60, + Ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale"); + } + + private void PanSkin_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + ModAnimation.AniStart( + ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60, + Ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale"); + if (IsSkinMouseDown) + { + IsSkinMouseDown = false; + Click?.Invoke(sender, e); + } + } + + // 保存皮肤 + public void BtnSkinSave_Click(object sender, RoutedEventArgs e) + { + Save(Loader); + } + + public static void Save(ModLoader.LoaderTask, string> Loader) + { + var Address = Loader.Output; + if (!(Loader.State == ModBase.LoadState.Finished)) + { + ModMain.Hint("皮肤正在获取中,请稍候!", ModMain.HintType.Critical); + if (!(Loader.State == ModBase.LoadState.Loading)) + Loader.Start(); + return; + } + + try + { + var FileAddress = SystemDialogs.SelectSaveFile("选取保存皮肤的位置", ModBase.GetFileNameFromPath(Address), + "皮肤图片文件(*.png)|*.png"); + if (FileAddress.Contains(@"\")) + { + File.Delete(FileAddress); + if (Address.StartsWith(ModBase.PathImage)) + { + var Image = new MyBitmap(Address); + Image.Save(FileAddress); + } + else + { + ModBase.CopyFile(Address, FileAddress); + } + + ModMain.Hint("皮肤保存成功!", ModMain.HintType.Finish); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "保存皮肤失败", ModBase.LogLevel.Hint); + } + } + + private void BtnSkinSave_Checked(object sender, RoutedEventArgs e) + { + ((MyMenuItem)sender).IsEnabled = string.IsNullOrEmpty(Address); + } + + /// + /// 载入皮肤。 + /// + public void Load() + { + try + { + // 检查文件存在 + Address = Loader.Output; + if (string.IsNullOrEmpty(Address)) + throw new Exception("皮肤加载器 " + Loader.Name + " 没有输出"); + if (!Address.StartsWith(ModBase.PathImage) && !File.Exists(Address)) + throw new FileNotFoundException("皮肤文件未找到", Address); + // 加载 + MyBitmap Image; + try + { + Image = new MyBitmap(Address); + } + catch (Exception ex) // #2272 + { + ModBase.Log(ex, $"皮肤文件已损坏:{Address}", ModBase.LogLevel.Hint); + File.Delete(Address); + return; + } + + ImgBack.Tag = Address; + // 大小检查 + var Scale = (int)Math.Round(Image.Picture.Width / 64d); + if (Image.Picture.Width < 32 || Image.Picture.Height < 32) + { + ImgFore.Source = null; + ImgBack.Source = null; + throw new Exception("图片大小不足,长为 " + Image.Picture.Height + ",宽为 " + Image.Picture.Width); + } + + MyBitmap SkinHead = null; + // 头发层(附加层) + if (Image.Picture.Width >= 64 && Image.Picture.Height >= 32) + { + if (Image.Picture.GetPixel(1, 1).A == 0 || + Image.Picture.GetPixel(Image.Picture.Width - 1, Image.Picture.Height - 1).A == 0 || + Image.Picture.GetPixel(Image.Picture.Width - 2, (int)Math.Round(Image.Picture.Height / 2d - 2d)) + .A == 0 || + (Image.Picture.GetPixel(1, 1) != Image.Picture.GetPixel(Scale * 41, Scale * 9) && + Image.Picture.GetPixel(Image.Picture.Width - 1, Image.Picture.Height - 1) != + Image.Picture.GetPixel(Scale * 41, Scale * 9) && + Image.Picture.GetPixel(Image.Picture.Width - 2, (int)Math.Round(Image.Picture.Height / 2d - 2d)) != + Image.Picture.GetPixel(Scale * 41, Scale * 9))) // 如果图片中有任何透明像素(避免纯色白底) + // 或是头部颜色和透明区均不一样 + { + ImgFore.Source = Image.Clip(Scale * 40, Scale * 8, Scale * 8, Scale * 8); + SkinHead = Image.Clip(Scale * 40, Scale * 8, Scale * 8, Scale * 8); + } + else + { + ImgFore.Source = null; + } + } + else + { + ImgFore.Source = null; + } + + // 脸层 + ImgBack.Source = Image.Clip(Scale * 8, Scale * 8, Scale * 8, Scale * 8); + // 用于显示档案列表头像的图片 + var SkinHeadId = Address.Between(new[] { Address.Contains("Images/Skins/") ? "Skins/" : @"Skin\" }[0], + ".png"); + var CachePath = ModBase.PathTemp + $@"Cache\Skin\Head\{SkinHeadId}.png"; + ModProfile.SelectedProfile.SkinHeadId = SkinHeadId; + ModProfile.SaveProfile(); + var CompleteHead = new Bitmap(56, 56); + using (var g = Graphics.FromImage(CompleteHead)) + { + g.InterpolationMode = InterpolationMode.NearestNeighbor; + g.PixelOffsetMode = PixelOffsetMode.Half; + using (Bitmap FaceBitmap = Image.Clip(Scale * 8, Scale * 8, Scale * 8, Scale * 8)) + { + g.DrawImage(FaceBitmap, new Rectangle(4, 4, 48, 48)); + } + + if (ImgFore.Source is not null) + using (Bitmap HairBitmap = Image.Clip(Scale * 40, Scale * 8, Scale * 8, Scale * 8)) + { + g.DrawImage(HairBitmap, new Rectangle(0, 0, 56, 56)); + } + } + + if (!Directory.Exists(ModBase.PathTemp + @"Cache\Skin\Head")) + Directory.CreateDirectory(ModBase.PathTemp + @"Cache\Skin\Head"); + CompleteHead.Save(CachePath, ImageFormat.Png); + ModBase.Log("[Skin] 载入头像成功:" + Loader.Name); + } + catch (Exception ex) + { + ModBase.Log(ex, "载入头像失败(" + (Address ?? "null") + "," + Loader.Name + ")", ModBase.LogLevel.Hint); + } + } + + private object ScaleToSize(Bitmap Bitmap, int Width, int Height) + { + var ScaledBitmap = new Bitmap(Width, Height); + using (var g = Graphics.FromImage(ScaledBitmap)) + { + g.InterpolationMode = InterpolationMode.NearestNeighbor; + g.PixelOffsetMode = PixelOffsetMode.Half; + g.DrawImage(Bitmap, 0, 0, Width, Height); + } + + return ScaledBitmap; + } + + /// + /// 清空皮肤。 + /// + public void Clear() + { + Address = ""; + ImgFore.Source = null; + ImgBack.Source = null; + } + + // 刷新缓存 + public void RefreshClick(object sender, RoutedEventArgs e) + { + RefreshCache(Loader); + } + + /// + /// 刷新皮肤缓存。 + /// + public static void RefreshCache(ModLoader.LoaderTask, string> sender = null) + { + var HasLoaderRunning = false; + foreach (var SkinLoader in PageLaunchLeft.SkinLoaders) + if (SkinLoader.State == ModBase.LoadState.Loading) + { + HasLoaderRunning = true; + break; + } + + if (ModMain.FrmLaunchLeft is not null && HasLoaderRunning) + // 由于 Abort 不是实时的,暂时不会释放文件,会导致删除报错,故只能取消执行 + ModMain.Hint("有正在获取中的皮肤,请稍后再试!"); + else + // 清空缓存 + // 刷新控件 + ModBase.RunInThread(() => + { + try + { + ModMain.Hint("正在刷新头像……"); + ModBase.Log("[Skin] 正在清空皮肤缓存"); + if (Directory.Exists(ModBase.PathTemp + @"Cache\Skin")) + ModBase.DeleteDirectory(ModBase.PathTemp + @"Cache\Skin"); + if (Directory.Exists(ModBase.PathTemp + @"Cache\Uuid")) + ModBase.DeleteDirectory(ModBase.PathTemp + @"Cache\Uuid"); + ModBase.IniClearCache(ModBase.PathTemp + @"Cache\Skin\IndexMs.ini"); + ModBase.IniClearCache(ModBase.PathTemp + @"Cache\Skin\IndexAuth.ini"); + ModBase.IniClearCache(ModBase.PathTemp + @"Cache\Uuid\Mojang.ini"); + foreach (var SkinLoader in sender is not null + ? new[] { sender } + : new[] { PageLaunchLeft.SkinLegacy, PageLaunchLeft.SkinMs }) + SkinLoader.WaitForExit(IsForceRestart: true); + ModMain.Hint("已刷新头像!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新皮肤缓存失败", ModBase.LogLevel.Msgbox); + } + }); + } + + /// + /// 在更换正版皮肤后,刷新正版皮肤。 + /// + /// 新的正版皮肤完整地址。 + public static void ReloadCache(string SkinAddress) + { + // 更新缓存 + // 刷新控件 + // 完成提示 + ModBase.RunInThread(() => + { + try + { + ModBase.WriteIni(ModBase.PathTemp + @"Cache\Skin\IndexMs.ini", ModProfile.SelectedProfile.Uuid, + SkinAddress); + ModBase.Log(string.Format("[Skin] 已写入皮肤地址缓存 {0} -> {1}", ModProfile.SelectedProfile.Uuid, SkinAddress)); + foreach (var SkinLoader in new[] { PageLaunchLeft.SkinMs, PageLaunchLeft.SkinLegacy }) + SkinLoader.WaitForExit(IsForceRestart: true); + ModMain.Hint("更改皮肤成功!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "更改正版皮肤后刷新皮肤失败", ModBase.LogLevel.Feedback); + } + }); + } + + public void BtnSkinCape_Click(object sender, RoutedEventArgs e) + { + // 检查条件,获取新披风 + if (IsChanging) + { + ModMain.Hint("正在更改披风中,请稍候!"); + return; + } + + if (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Failed) + { + ModMain.Hint("登录失败,无法更改披风!", ModMain.HintType.Critical); + return; + } + + ModMain.Hint("正在获取披风列表,请稍候……"); + IsChanging = true; + // 开始实际获取 + ModBase.RunInNewThread(() => + { + try + { + // 获取登录信息 + if (ModLaunch.McLoginMsLoader.State != ModBase.LoadState.Finished) + ModLaunch.McLoginMsLoader.WaitForExit(ModProfile.GetLoginData()); + if (ModLaunch.McLoginMsLoader.State != ModBase.LoadState.Finished) + { + ModMain.Hint("登录失败,无法更改披风!", ModMain.HintType.Critical); + return; + } + + var AccessToken = ModLaunch.McLoginMsLoader.Output.AccessToken; + var Uuid = ModLaunch.McLoginMsLoader.Output.Uuid; + var SkinData = (JObject)ModBase.GetJson(ModLaunch.McLoginMsLoader.Output.ProfileJson); + foreach (var itemSkin in SkinData["capes"]) + { + if (itemSkin["url"] is null) + continue; + var localFile = $@"{ModBase.PathTemp}Cache\Capes\{itemSkin["alias"]}.png"; + var capeFrontFile = $@"{ModBase.PathTemp}Cache\Capes\{itemSkin["alias"]}-front.png"; + if (File.Exists(localFile) && File.Exists(capeFrontFile)) + { + itemSkin["url"] = capeFrontFile; + continue; + } + + ModNet.NetDownloadByLoader(itemSkin["url"].ToString(), localFile); + var capeFrontRegion = new Rectangle(1, 0, 11, 17); + var capeFront = new Bitmap(capeFrontRegion.Width, capeFrontRegion.Height); + var capeImage = Image.FromFile(localFile); + var gra = Graphics.FromImage(capeFront); + gra.DrawImage(capeImage, capeFrontRegion, capeFrontRegion, GraphicsUnit.Pixel); + capeFront.Save(capeFrontFile); + itemSkin["url"] = capeFrontFile; + } + + // 获取玩家的所有披风 + int? SelId = default; + ModBase.RunInUiWait(() => + { + try + { + var CapeNames = new Dictionary + { + { "Migrator", "迁移者披风" }, { "MapMaker", "Realms 地图制作者披风" }, { "Moderator", "Mojira 管理员披风" }, + { "Translator-Chinese", "Crowdin 中文翻译者披风" }, { "Translator", "Crowdin 翻译者披风" }, + { "Cobalt", "Cobalt 披风" }, { "Vanilla", "原版披风" }, { "Minecon2011", "Minecon 2011 参与者披风" }, + { "Minecon2012", "Minecon 2012 参与者披风" }, { "Minecon2013", "Minecon 2013 参与者披风" }, + { "Minecon2015", "Minecon 2015 参与者披风" }, { "Minecon2016", "Minecon 2016 参与者披风" }, + { "Cherry Blossom", "樱花披风" }, { "15th Anniversary", "15 周年纪念披风" }, + { "Purple Heart", "紫色心形披风" }, { "Follower's", "追随者披风" }, { "MCC 15th Year", "MCC 15 周年披风" }, + { "Minecraft Experience", "村民救援披风" }, { "Mojang Office", "Mojang 办公室披风" }, + { "Home", "家园披风" }, { "Menace", "入侵披风" }, { "Yearn", "渴望披风" }, { "Common", "普通披风" }, + { "Pan", "薄煎饼披风" }, { "Founder's", "创始人披风" }, { "Copper", "铜披风" }, + { "Zombie Horse", "僵尸马披风" } + }; + var SelectionControl = new List + { + new MyListItem + { + Title = "无披风", + Info = "Null" + } + }; + foreach (var Cape in SkinData["capes"]) + { + var CapeName = Cape["alias"].ToString(); + if (CapeNames.ContainsKey(CapeName)) + CapeName = CapeNames[CapeName]; + var state = Cape["state"]; // 检测披风状态,若为 ACTIVE 则选中 + var active = state is not null & state.ToString().ToUpper().Equals("ACTIVE"); + SelectionControl.Add(new MyListItem + { + Title = CapeName, + Info = Cape["alias"].ToString(), + Checked = active, + Type = MyListItem.CheckType.RadioBox, + Logo = (string)Cape["url"], + LogoScale = 0.8d + }); + } + + SelId = ModMain.MyMsgBoxSelect(SelectionControl, "选择披风", "确定", "取消"); + } + catch (Exception ex) + { + ModBase.Log(ex, "获取玩家皮肤列表失败", ModBase.LogLevel.Feedback); + } + }); + if (SelId is null) + return; + // 发送请求 + var Result = ModNet.NetRequestRetry("https://api.minecraftservices.com/minecraft/profile/capes/active", + SelId.HasValue && SelId.Value == 0 ? "DELETE" : "PUT", + SelId.HasValue && SelId.Value == 0 + ? "" + : new JObject(new JProperty("capeId", SkinData["capes"][SelId - 1]["id"])).ToString(0), + "application/json", + Headers: new Dictionary { { "Authorization", "Bearer " + AccessToken } }); + if (Result.Contains("\"errorMessage\"")) + ModMain.Hint( + Conversions.ToString(Operators.ConcatenateObject("更改披风失败:", + ((dynamic)ModBase.GetJson(Result))["errorMessage"])), ModMain.HintType.Critical); + else + ModMain.Hint("更改披风成功!等待一段时间后将会生效……", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "更改披风失败", ModBase.LogLevel.Hint); + } + finally + { + IsChanging = false; + } + }, "Cape Change"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml index fc87380aa..283c4bc0a 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml @@ -1,11 +1,11 @@ - + @@ -24,8 +24,11 @@ - - + + @@ -34,10 +37,15 @@ - - + + - + @@ -55,35 +63,39 @@ - + - + - + - + - + - - + + - + - - + + - + @@ -97,16 +109,28 @@ - - - - - - - - - - + + + + + + + + + + @@ -118,12 +142,16 @@ - - + + - + - - \ No newline at end of file + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.cs new file mode 100644 index 000000000..465ef8b9c --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.cs @@ -0,0 +1,978 @@ +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils; + +namespace PCL; + +public partial class PageLaunchLeft +{ + private double ActualUsedHeight; + private double ActualUsedWidth; + private int BtnLaunchState; + private ModMinecraft.McInstance BtnLaunchVersion; + private bool IsHeightAnimating; + + // 加载当前实例 + private bool IsLoad; + + private bool IsLoadFinished; + + // 尺寸改变动画 + private bool IsWidthAnimating; + private double ShowProgress; + + public PageLaunchLeft() + { + InitializeComponent(); + Loaded += PageLaunchLeft_Loaded; + // Handles + BtnInstance.Click += BtnInstance_Click; + BtnLaunch.Click += BtnLaunch_Click; + BtnLaunch.Loaded += (_, _) => RefreshButtonsUI(); + BtnCancel.Click += BtnCancel_Click; + BtnMore.Click += BtnMore_Click; + PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedW; + PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedH; + } + + public void PageLaunchLeft_Loaded(object sender, RoutedEventArgs e) + { + if (IsLoad) + RefreshPage(false); + + AprilPosTrans.X = 0d; + AprilPosTrans.Y = 0d; + + if (IsLoad) + return; + IsLoad = true; + ModAnimation.AniControlEnabled += 1; + + // 开始按钮 + ModMinecraft.McInstanceListLoader.LoadingStateChanged += (_, __) => RefreshButtonsUI(); + ModMinecraft.McFolderListLoader.LoadingStateChanged += (_, __) => RefreshButtonsUI(); + RefreshButtonsUI(); + + // 初始化档案 + ModProfile.GetProfile(); + if (!(ModProfile.ProfileList.Count == 0) && ModProfile.LastUsedProfile >= 0 && + ModProfile.LastUsedProfile < ModProfile.ProfileList.Count) + ModProfile.SelectedProfile = ModProfile.ProfileList[ModProfile.LastUsedProfile]; + + // 加载实例 + ModBase.RunInNewThread(() => + { + // 自动整合包安装:准备 + string PackInstallPath = null; + if (File.Exists(ModBase.ExePath + "modpack.zip")) + PackInstallPath = ModBase.ExePath + "modpack.zip"; + if (File.Exists(ModBase.ExePath + "modpack.mrpack")) + PackInstallPath = ModBase.ExePath + "modpack.mrpack"; + if (PackInstallPath is not null) + { + ModBase.Log("[Launch] 需自动安装整合包:" + PackInstallPath, ModBase.LogLevel.Debug); + States.Game.SelectedFolder = @"$.minecraft\"; + if (!Directory.Exists(ModBase.ExePath + @".minecraft\")) + { + Directory.CreateDirectory(ModBase.ExePath + @".minecraft\"); + Directory.CreateDirectory(ModBase.ExePath + @".minecraft\versions\"); + ModMinecraft.McFolderLauncherProfilesJsonCreate(ModBase.ExePath + @".minecraft\"); + } + + PageSelectLeft.AddFolder(ModBase.ExePath + @".minecraft\", + ModBase.GetFolderNameFromPath(ModBase.ExePath), false); + ModMinecraft.McFolderListLoader.WaitForExit(); + } + + // 确认 Minecraft 文件夹存在 + ModMinecraft.McFolderSelected = + States.Game.SelectedFolder.ToString().Replace("$", ModBase.ExePath); + if (string.IsNullOrEmpty(ModMinecraft.McFolderSelected) || !Directory.Exists(ModMinecraft.McFolderSelected)) + { + // 无效的文件夹 + if (string.IsNullOrEmpty(ModMinecraft.McFolderSelected)) + ModBase.Log("[Launch] 没有已储存的 Minecraft 文件夹"); + else + ModBase.Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" + ModMinecraft.McFolderSelected, + ModBase.LogLevel.Debug); + ModMinecraft.McFolderListLoader.WaitForExit(IsForceRestart: true); + States.Game.SelectedFolder = ModMinecraft.McFolderList[0].Location.Replace(ModBase.ExePath, "$"); + } + + ModBase.Log("[Launch] Minecraft 文件夹:" + ModMinecraft.McFolderSelected); + if (Conversions.ToBoolean(Config.Debug.AddRandomDelay)) + Thread.Sleep(RandomUtils.NextInt(500, 3000)); + // 自动整合包安装 + if (PackInstallPath is not null) + try + { + var InstallLoader = ModModpack.ModpackInstall(PackInstallPath); + ModBase.Log("[Launch] 自动安装整合包已开始:" + PackInstallPath); + InstallLoader.WaitForExit(); + if (InstallLoader.State == ModBase.LoadState.Finished) + { + ModBase.Log("[Launch] 自动安装整合包成功,清理安装包:" + PackInstallPath); + if (File.Exists(PackInstallPath)) + File.Delete(PackInstallPath); + } + } + catch (ModBase.CancelledException ex) + { + ModBase.Log(ex, "自动安装整合包被用户取消:" + PackInstallPath); + } + catch (Exception ex) + { + ModBase.Log(ex, "自动安装整合包失败:" + PackInstallPath, ModBase.LogLevel.Msgbox); + } + + // 确认 Minecraft 版本实例 + var Selection = Conversions.ToString(States.Game.SelectedInstance); + var Instance = Selection == "" ? null : new ModMinecraft.McInstance(Selection); + if (Instance is null || !Instance.PathInstance.StartsWithF(ModMinecraft.McFolderSelected) || + !Instance.Check()) + { + // 无效的实例 + ModBase.Log("[Launch] 当前选择的 Minecraft 实例无效:" + (Instance is null ? "null" : Instance.PathInstance), + Instance == null ? ModBase.LogLevel.Normal : ModBase.LogLevel.Debug); + if (!(ModMinecraft.McInstanceListLoader.State == ModBase.LoadState.Finished)) + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true); + if (!ModMinecraft.McInstanceList.Any() || + ModMinecraft.McInstanceList.First().Value[0].Logo.Contains("RedstoneBlock")) + { + Instance = null; + States.Game.SelectedInstance = ""; + ModBase.Log("[Launch] 无可用 Minecraft 实例"); + } + else + { + Instance = ModMinecraft.McInstanceList.First().Value[0]; + States.Game.SelectedInstance = Instance.Name; + ModBase.Log("[Launch] 自动选择 Minecraft 实例:" + Instance.PathInstance); + } + } + + ModBase.RunInUi(() => + { + ModMinecraft.McInstanceSelected = Instance; // 绕这一圈是为了避免 McInstanceCheck 触发第二次实例改变 + IsLoadFinished = true; + RefreshButtonsUI(); + RefreshPage(false); // 有可能选择的版本变化了,需要重新刷新 + // If IsProfileVaild() = "" Then McLoginLoader.Start() '自动登录 + }); + }, "Instance Check", ThreadPriority.AboveNormal); + + // 改变页面 + RefreshPage(false); + + ModAnimation.AniControlEnabled -= 1; + } + + // 实例选择按钮 + private void BtnInstance_Click(object sender, MouseButtonEventArgs e) + { + if (ModLaunch.McLaunchLoader.State == ModBase.LoadState.Loading) + return; + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSelect); + } + + // 启动按钮 + public void LaunchButtonClick() + { + if (ModLaunch.McLaunchLoader.State == ModBase.LoadState.Loading || !BtnLaunch.IsEnabled || + (ModMain.FrmMain.PageRight is not null && + ModMain.FrmMain.PageRight.PageState != MyPageRight.PageStates.ContentStay && + ModMain.FrmMain.PageRight.PageState != MyPageRight.PageStates.ContentEnter)) + return; + // 愚人节处理 + if (ModMain.IsAprilEnabled && !ModMain.IsAprilGiveup) + { + ModSecret.ThemeUnlock(12, false, "隐藏主题 滑稽彩 已解锁!"); + ModMain.IsAprilGiveup = true; + ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleX = 1d; + ModMain.FrmLaunchLeft.AprilScaleTrans.ScaleY = 1d; + ModMain.FrmLaunchLeft.AprilPosTrans.X = 0d; + ModMain.FrmLaunchLeft.AprilPosTrans.Y = 0d; + ModMain.FrmMain.BtnExtraApril.ShowRefresh(); + } + + // 实际的启动 + if (BtnLaunch.Text == "启动游戏") + { + if (File.Exists(ModMinecraft.McInstanceSelected.PathInstance + ".pclignore")) + { + ModMain.Hint("当前实例正在安装,无法启动!", ModMain.HintType.Critical); + return; + } + + ModLaunch.McLaunchStart(); + } + else if (BtnLaunch.Text == "下载游戏") + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall); + } + } + + public void RefreshButtonsUI() + { + if (!BtnLaunch.IsLoaded) + return; + // 获取当前状态 + int CurrentState; + if (!IsLoadFinished || ModMinecraft.McInstanceListLoader.State == ModBase.LoadState.Loading || + ModMinecraft.McFolderListLoader.State == ModBase.LoadState.Loading) + { + CurrentState = 0; + } + else if (ModMinecraft.McInstanceSelected is null) + { + if (Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow) + CurrentState = 1; + else + CurrentState = 2; + } + else + { + CurrentState = 3; + } + + // 更新状态 + if (CurrentState == BtnLaunchState && + ((ModMinecraft.McInstanceSelected is null ? "" : ModMinecraft.McInstanceSelected.PathInstance) ?? "") == + ((BtnLaunchVersion is null ? "" : BtnLaunchVersion.PathInstance) ?? "")) + goto ExitRefresh; + BtnLaunchVersion = ModMinecraft.McInstanceSelected; + BtnLaunchState = CurrentState; + switch (CurrentState) + { + case 0: + { + ModBase.Log("[Minecraft] 启动按钮:正在加载 Minecraft 实例"); + ModMain.FrmLaunchLeft.BtnLaunch.Text = "正在加载"; + ModMain.FrmLaunchLeft.BtnLaunch.IsEnabled = false; + ModMain.FrmLaunchLeft.LabVersion.Text = "正在加载中,请稍候"; + ModMain.FrmLaunchLeft.BtnInstance.IsEnabled = false; + ModMain.FrmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed; + break; + } + case 1: + { + ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,下载已禁用"); + ModMain.FrmLaunchLeft.BtnLaunch.Text = "启动游戏"; + ModMain.FrmLaunchLeft.BtnLaunch.IsEnabled = false; + ModMain.FrmLaunchLeft.LabVersion.Text = "未找到可用的游戏实例"; + ModMain.FrmLaunchLeft.BtnInstance.IsEnabled = true; + ModMain.FrmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed; + break; + } + case 2: + { + ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,要求下载"); + ModMain.FrmLaunchLeft.BtnLaunch.Text = "下载游戏"; + ModMain.FrmLaunchLeft.BtnLaunch.IsEnabled = true; + ModMain.FrmLaunchLeft.LabVersion.Text = "未找到可用的游戏实例"; + ModMain.FrmLaunchLeft.BtnInstance.IsEnabled = true; + ModMain.FrmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed; + break; + } + case 3: + { + ModBase.Log("[Minecraft] 启动按钮:Minecraft 实例:" + ModMinecraft.McInstanceSelected.PathInstance); + ModMain.FrmLaunchLeft.BtnLaunch.Text = "启动游戏"; + ModMain.FrmLaunchLeft.BtnInstance.IsEnabled = true; + if (ModProfile.SelectedProfile is not null) + BtnLaunch.IsEnabled = true; + else + BtnLaunch.IsEnabled = false; + ModMain.FrmLaunchLeft.LabVersion.Text = ModMinecraft.McInstanceSelected.Name; + break; + } + // FrmLaunchLeft.BtnMore.Visibility = Visibility.Visible '由功能隐藏设置修改 + } + + ExitRefresh: ; + + // 功能隐藏 + ModMain.FrmLaunchLeft.BtnInstance.Visibility = + !PageSetupUI.HiddenForceShow && Config.Preference.Hide.FunctionSelect + ? Visibility.Collapsed + : Visibility.Visible; + if (CurrentState == 3) ModMain.FrmLaunchLeft.BtnMore.Visibility = ModMain.FrmLaunchLeft.BtnInstance.Visibility; + } + + // 取消按钮 + private void BtnCancel_Click(object sender, MouseButtonEventArgs e) + { + if (ModLaunch.McLaunchLoaderReal is not null) + { + ModLaunch.McLaunchLoaderReal.Abort(); + ModLaunch.McLaunchLog("已取消启动"); + try + { + if (ModLaunch.McLaunchWatcher is not null) + ModLaunch.McLaunchWatcher.Kill(); + else if (ModLaunch.McLaunchProcess is not null) + if (!ModLaunch.McLaunchProcess.HasExited) + ModLaunch.McLaunchProcess.Kill(); + } + catch (Exception ex) + { + ModBase.Log(ex, "取消启动结束进程失败", ModBase.LogLevel.Hint); + } + } + } + + // 实例设置按钮 + private void BtnMore_Click(object sender, MouseButtonEventArgs e) + { + if (ModLaunch.McLaunchLoader.State == ModBase.LoadState.Loading) + return; + ModMinecraft.McInstanceSelected.Load(); + PageInstanceLeft.Instance = ModMinecraft.McInstanceSelected; + if (File.Exists(ModMinecraft.McInstanceSelected.PathInstance + ".pclignore")) + { + ModMain.Hint("当前实例正在安装,暂无法进行实例设置!", ModMain.HintType.Critical); + return; + } + + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSetup); + } + + /// + /// 每 0.2s 执行一次,刷新启动的数据 UI 显示。 + /// + public void LaunchingRefresh() + { + try + { + if (ModLaunch.McLaunchLoaderReal.State == ModBase.LoadState.Aborted) + return; + // 阶段状态获取 + var IsLaunched = false; // 是否已经启动游戏,只是在等待窗口 + do + { + try + { + var exitTry = false; + foreach (var Loader in ModLaunch.McLaunchLoaderReal.GetLoaderList(false)) + if (Loader.State == ModBase.LoadState.Loading || Loader.State == ModBase.LoadState.Waiting) + { + LabLaunchingStage.Text = Loader.Name; + IsLaunched = Loader.Name == "等待游戏窗口出现" || Loader.Name == "结束处理"; + exitTry = true; + break; + } + + if (exitTry) break; + LabLaunchingStage.Text = "已完成"; + } + catch (Exception ex) + { + ModBase.Log(ex, "获取是否启动完成失败,可能是由于启动状态改变导致集合已修改"); + return; + } + } while (false); + + if (ModAnimation.AniIsRun("Launch State Page")) + IsLaunched = false; // 等待页面切换动画完成 + // 计算应显示的进度 + var ActualProgress = ModLaunch.McLaunchLoaderReal.Progress; + if (ActualProgress >= ShowProgress) + ShowProgress += (ActualProgress - ShowProgress) * 0.2d + 0.005d; // 向实际进度靠一点 + if (ActualProgress <= ShowProgress) + ShowProgress = ActualProgress; // 原来或处理后变得比实际进度高,直接回退 + if (IsLaunched) + ShowProgress = 1d; // 如果已经完成了,就不卖关子了 + // 文本 + LabLaunchingTitle.Text = IsLaunched ? "已启动游戏" : + ModLaunch.CurrentLaunchOptions.SaveBatch is null ? "正在启动游戏" : "正在导出启动脚本"; + LabLaunchingProgress.Text = ModBase.StrFillNum(ShowProgress * 100d, 2) + " %"; + var HasLaunchDownloader = false; + try + { + foreach (var Loader in ModNet.NetManager.Tasks) + if (Loader.RealParent is not null && Loader.RealParent.Name == "Minecraft 启动" && + Loader.State == ModBase.LoadState.Loading) + HasLaunchDownloader = true; + } + catch (Exception ex) + { + ModBase.Log(ex, "获取 Minecraft 启动下载器失败,可能是因为启动被取消"); + HasLaunchDownloader = false; + } + + LabLaunchingDownload.Text = ModBase.GetString(ModNet.NetManager.Speed) + "/s"; + var ShouldShowHint = Conversions.ToBoolean(Config.Preference.ShowLaunchingHint); + // 进度改变动画 + var AnimList = new List + { + ModAnimation.AaGridLengthWidth(ProgressLaunchingFinished, + ShowProgress - ProgressLaunchingFinished.Width.Value, 260, + Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaGridLengthWidth(ProgressLaunchingUnfinished, + 1d - ShowProgress - ProgressLaunchingUnfinished.Width.Value, 260, + Ease: new ModAnimation.AniEaseOutFluent()) + }; + var IsDownloadStateChanged = + HasLaunchDownloader == (LabLaunchingDownload.Visibility == Visibility.Collapsed); + if (IsDownloadStateChanged) + { + LabLaunchingDownload.Visibility = Visibility.Visible; + LabLaunchingDownloadLeft.Visibility = Visibility.Visible; + AnimList.AddRange(new[] + { + ModAnimation.AaOpacity(LabLaunchingDownload, + (HasLaunchDownloader ? 1 : 0) - LabLaunchingDownload.Opacity, 100), + ModAnimation.AaOpacity(LabLaunchingDownloadLeft, + (HasLaunchDownloader ? 0.5d : 0d) - LabLaunchingDownloadLeft.Opacity, 100), + ModAnimation.AaCode(() => + { + if (!HasLaunchDownloader) + { + LabLaunchingDownload.Visibility = Visibility.Collapsed; + LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed; + } + }, 110) + }); + } + + var IsProgressStateChanged = !IsLaunched == (LabLaunchingProgress.Visibility == Visibility.Collapsed); + if (IsProgressStateChanged) + { + LabLaunchingProgress.Visibility = Visibility.Visible; + LabLaunchingProgressLeft.Visibility = Visibility.Visible; + if (IsLaunched && ShouldShowHint) PanLaunchingHint.Visibility = Visibility.Visible; + AnimList.AddRange(new[] + { + ModAnimation.AaOpacity(LabLaunchingProgress, (!IsLaunched ? 1 : 0) - LabLaunchingProgress.Opacity, + 100), + ModAnimation.AaOpacity(LabLaunchingProgressLeft, + (!IsLaunched ? 0.5d : 0d) - LabLaunchingProgressLeft.Opacity, 100), + ModAnimation.AaOpacity(PanLaunchingHint, + (IsLaunched && ShouldShowHint ? 1 : 0) - PanLaunchingHint.Opacity, 100) + }); + } + + ModAnimation.AniStart(AnimList, "Launching Progress"); + } + catch (Exception ex) + { + ModBase.Log(ex, "刷新启动信息失败", ModBase.LogLevel.Feedback); + } + } + + private void PanLaunchingInfo_SizeChangedW(object sender, SizeChangedEventArgs e) + { + var DeltaWidth = e.NewSize.Width - e.PreviousSize.Width; + if (e.PreviousSize.Width == 0d || IsWidthAnimating || Math.Abs(DeltaWidth) < 1d || + PanLaunchingInfo.ActualWidth == 0d) + return; + ModAnimation.AniStart(new[] + { + ModAnimation.AaWidth(PanLaunchingInfo, DeltaWidth, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => + { + IsWidthAnimating = false; + PanLaunchingInfo.Width = ActualUsedWidth; + }, After: true) + }, "Launching Info Width"); + IsWidthAnimating = true; + ActualUsedWidth = PanLaunchingInfo.Width; + PanLaunchingInfo.Width = e.PreviousSize.Width; + } + + private void PanLaunchingInfo_SizeChangedH(object sender, SizeChangedEventArgs e) + { + var DeltaHeight = e.NewSize.Height - e.PreviousSize.Height; + if (e.PreviousSize.Height == 0d || IsHeightAnimating || Math.Abs(DeltaHeight) < 1d || + PanLaunchingInfo.ActualHeight == 0d) + return; + ModAnimation.AniStart(new[] + { + ModAnimation.AaHeight(PanLaunchingInfo, DeltaHeight, 180, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => + { + IsHeightAnimating = false; + PanLaunchingInfo.Height = ActualUsedHeight; + }, After: true) + }, "Launching Info Height"); + IsHeightAnimating = true; + ActualUsedHeight = PanLaunchingInfo.Height; + PanLaunchingInfo.Height = e.PreviousSize.Height; + } + + // 启动游戏按钮 + private void BtnLaunch_Click(object sender, MouseButtonEventArgs e) + { + LaunchButtonClick(); + } + + #region 切换大页面 + + /// + /// 获取你知道吗。 + /// + private string GetRandomHint() + { + return PageLaunchRight.GetRandomHint(true); + } + + /// + /// 切换至启动中页面。 + /// + public void PageChangeToLaunching() + { + // 修改验证方式 + switch (ModProfile.SelectedProfile.Type) + { + case ModLaunch.McLoginType.Legacy: + { + LabLaunchingMethod.Text = "离线验证"; + break; + } + case ModLaunch.McLoginType.Ms: + { + LabLaunchingMethod.Text = "正版验证"; + break; + } + case ModLaunch.McLoginType.Auth: + { + LabLaunchingMethod.Text = "第三方验证" + (!string.IsNullOrEmpty(ModProfile.SelectedProfile.ServerName) + ? " / " + ModProfile.SelectedProfile.ServerName + : ""); + break; + } + } + + // 初始化页面 + LabLaunchingName.Text = ModMinecraft.McInstanceSelected.Name; + LabLaunchingStage.Text = "初始化"; + LabLaunchingTitle.Text = ModLaunch.CurrentLaunchOptions?.SaveBatch is null ? "正在启动游戏" : "正在导出启动脚本"; + LabLaunchingProgress.Text = "0.00 %"; + LabLaunchingProgress.Opacity = 1d; + LabLaunchingDownload.Visibility = Visibility.Visible; + LabLaunchingProgressLeft.Opacity = 0.6d; + LabLaunchingDownload.Visibility = Visibility.Visible; + LabLaunchingDownload.Text = "0 B/s"; + LabLaunchingDownload.Opacity = 0d; + LabLaunchingDownload.Visibility = Visibility.Collapsed; + LabLaunchingDownloadLeft.Opacity = 0d; + LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed; + ProgressLaunchingFinished.Width = new GridLength(0d, GridUnitType.Star); + ProgressLaunchingUnfinished.Width = new GridLength(1d, GridUnitType.Star); + PanLaunchingHint.Opacity = 0d; + PanLaunchingHint.Visibility = Visibility.Collapsed; + PanLaunchingInfo.Width = double.NaN; // 重置宽度改变动画 + ModLaunch.McLaunchProcess = null; + ModLaunch.McLaunchWatcher = null; + + var ShouldShowHint = Conversions.ToBoolean(Config.Preference.ShowLaunchingHint); + if (ShouldShowHint) + LabLaunchingHint.Text = GetRandomHint(); + else + LabLaunchingHint.Text = ""; + + // 初始化其他页面 + PanInput.IsHitTestVisible = false; + PanLaunching.IsHitTestVisible = false; + LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Run; + PanLaunching.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(PanInput, 0d, 50), + ModAnimation.AaOpacity(PanInput, -PanInput.Opacity, 110, Ease: new ModAnimation.AniEaseInFluent(), + After: true), + ModAnimation.AaScaleTransform(PanInput, 1.2d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 160), + ModAnimation.AaOpacity(PanLaunching, 1d - PanLaunching.Opacity, 150, 100), + ModAnimation.AaScaleTransform(PanLaunching, 1d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX, + 500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => PanLaunching.IsHitTestVisible = true, 150) + }, "Launch State Page"); // 略作延迟,这样如果预检测失败,不会出现奇怪的弹一下的动画 + } + + /// + /// 切换至登录页面。 + /// + public void PageChangeToLogin() + { + ((dynamic)PageGet(PageCurrent)).Reload(); + PanInput.IsHitTestVisible = false; + PanLaunching.IsHitTestVisible = false; + LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Stop; + PanInput.Visibility = Visibility.Visible; + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(PanLaunching, -PanLaunching.Opacity, 150), + ModAnimation.AaScaleTransform(PanLaunching, + 0.8d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX, 150, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaOpacity(PanInput, 1d - PanInput.Opacity, 250, 50), + ModAnimation.AaScaleTransform(PanInput, 1d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 300, 50, + new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaCode(() => PanInput.IsHitTestVisible = true, 200) + }, "Launch State Page", true); + } + + #endregion + + #region 切换登录页面 + + private enum PageType + { + None, + Auth, + Ms, + Profile, + ProfileSkin, + Offline + } + + /// + /// 当前页面的种类。 + /// + private PageType PageCurrent = PageType.None; + + private object PageGet(PageType Type) + { + switch (Type) + { + case PageType.Auth: + { + if (ModMain.FrmLoginAuth == null) + ModMain.FrmLoginAuth = new PageLoginAuth(); + return ModMain.FrmLoginAuth; + } + case PageType.Ms: + { + if (ModMain.FrmLoginMs == null) + ModMain.FrmLoginMs = new PageLoginMs(); + return ModMain.FrmLoginMs; + } + case PageType.Profile: + { + if (ModMain.FrmLoginProfile == null) + ModMain.FrmLoginProfile = new PageLoginProfile(); + return ModMain.FrmLoginProfile; + } + case PageType.ProfileSkin: + { + if (ModMain.FrmLoginProfileSkin == null) + ModMain.FrmLoginProfileSkin = new PageLoginProfileSkin(); + return ModMain.FrmLoginProfileSkin; + } + case PageType.Offline: + { + if (ModMain.FrmLoginOffline == null) + ModMain.FrmLoginOffline = new PageLoginOffline(); + return ModMain.FrmLoginOffline; + } + + default: + { + throw new ArgumentOutOfRangeException("Type", "即将切换的登录分页编号越界"); + } + } + } + + /// + /// 切换现有登录页面种类,返回新页面的实例。 + /// + /// 新页面的种类。 + /// 是否显示动画。 + private object PageChange(PageType Type, bool Anim) + { + object PageNew = ModMain.FrmLoginMs; // 初始化一个东西,避免在执行时出现异常导致雪崩 + try + { + #region 确定更改的页面实例并实例化 + + if (PageCurrent == Type) + return PageNew; + PageNew = PageGet(Type); + + #endregion + + #region 切换页面 + + ModAnimation.AniStop("FrmLogin PageChange"); + // 清除页面关联性 + if (!(PageNew == null) && !(((dynamic)PageNew).Parent == null)) + ((dynamic)PageNew).SetValue(ContentPresenter.ContentProperty, (object)null); + if (Anim) + { + // 动画 + // 执行动画 + Dispatcher.Invoke(() => ModAnimation.AniStart(new[] + { + ModAnimation.AaOpacity(PanLogin, -PanLogin.Opacity, 100, Ease: new ModAnimation.AniEaseOutFluent()), + ModAnimation.AaCode(() => + { + ModAnimation.AniControlEnabled += 1; + PanLogin.Children.Clear(); + PanLogin.Children.Add((UIElement)PageNew); + ModAnimation.AniControlEnabled -= 1; + }, 100), + ModAnimation.AaOpacity(PanLogin, 1d, 100, 120, new ModAnimation.AniEaseInFluent()) + }, "FrmLogin PageChange"), DispatcherPriority.Render); + } + else + { + // 无动画 + ModAnimation.AniControlEnabled += 1; + PanLogin.Children.Clear(); + PanLogin.Children.Add((UIElement)PageNew); + ModAnimation.AniControlEnabled -= 1; + } + + #endregion + + PageCurrent = Type; + return PageNew; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换登录分页失败(" + ModBase.GetStringFromEnum(Type) + ")", ModBase.LogLevel.Feedback); + return PageNew; + } + } + + /// + /// 确认当前显示的子页面正确,并刷新该页面。 + /// + /// 是否显示动画 + /// 目标验证方式,若正在创建档案需填 + public void RefreshPage(bool Anim, ModLaunch.McLoginType TargetLoginType = default) + { + var Type = default(PageType); + if (TargetLoginType != default) + { + if (TargetLoginType == ModLaunch.McLoginType.Ms) + Type = PageType.Ms; + if (TargetLoginType == ModLaunch.McLoginType.Auth) + Type = PageType.Auth; + if (TargetLoginType == ModLaunch.McLoginType.Legacy) + Type = PageType.Offline; + } + else if (ModProfile.SelectedProfile is not null) + { + Type = PageType.ProfileSkin; + BtnLaunch.IsEnabled = true; + } + else + { + Type = PageType.Profile; + if (!(BtnLaunch.Text == "下载游戏")) + BtnLaunch.IsEnabled = false; + } + + // 刷新页面 + if (PageCurrent == Type) + return; + PageChange(Type, Anim); + } + + #endregion + + #region 皮肤 + + // 正版皮肤 + public static ModLoader.LoaderTask, string> SkinMs = new("Loader Skin Ms", SkinMsLoad, + SkinMsInput, ThreadPriority.AboveNormal); + + private static ModBase.EqualableList SkinMsInput() + { + // 获取名称 + return new ModBase.EqualableList + { ModProfile.SelectedProfile.Username, ModProfile.SelectedProfile.Uuid }; + } + + private static void SkinMsLoad(ModLoader.LoaderTask, string> Data) + { + // 清空已有皮肤 + // 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载 + ModBase.RunInUi(() => + { + if (ModMain.FrmLoginProfileSkin is not null && ModMain.FrmLoginProfileSkin.Skin is not null) + ModMain.FrmLoginProfileSkin.Skin.Clear(); + }); + // 获取 Url + var UserName = Data.Input[0]; + var Uuid = Data.Input[1]; + if (ModProfile.SelectedProfile is not null) + { + UserName = ModProfile.SelectedProfile.Username; + Uuid = ModProfile.SelectedProfile.Uuid; + } + + if (string.IsNullOrEmpty(UserName)) + { + Data.Output = ModBase.PathImage + "Skins/" + ModMinecraft.McSkinSex(ModProfile.GetOfflineUuid(UserName)) + + ".png"; + ModBase.Log("[Minecraft] 获取微软正版皮肤失败,ID 为空"); + goto Finish; + } + + try + { + var Result = ModMinecraft.McSkinGetAddress(Uuid, "Ms"); + if (Data.IsAborted) + throw new ThreadInterruptedException("当前任务已取消:" + UserName); + Result = ModMinecraft.McSkinDownload(Result); + if (Data.IsAborted) + throw new ThreadInterruptedException("当前任务已取消:" + UserName); + Data.Output = Result; + } + catch (Exception ex) + { + if (ex.GetType().Name == "ThreadInterruptedException") + { + Data.Output = ""; + ModBase.Log("[Minecraft] 已取消皮肤获取:" + UserName); + return; + } + + if (ex.ToString().Contains("429")) + { + Data.Output = ModBase.PathImage + "Skins/" + + ModMinecraft.McSkinSex(ModProfile.GetOfflineUuid(UserName)) + ".png"; + ModBase.Log("[Minecraft] 获取正版皮肤失败(" + UserName + "):获取皮肤太过频繁,请 5 分钟后再试!", ModBase.LogLevel.Hint); + } + else if (ex.ToString().Contains("未设置自定义皮肤")) + { + Data.Output = ModBase.PathImage + "Skins/" + + ModMinecraft.McSkinSex(ModProfile.GetOfflineUuid(UserName)) + ".png"; + ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载"); + } + else + { + Data.Output = ModBase.PathImage + "Skins/" + + ModMinecraft.McSkinSex(ModProfile.GetOfflineUuid(UserName)) + ".png"; + ModBase.Log(ex, "获取微软正版皮肤失败(" + UserName + ")", ModBase.LogLevel.Hint); + } + } + + Finish: ; + + // 刷新显示 + if (ModMain.FrmLoginProfileSkin is not null && ReferenceEquals(ModMain.FrmLoginProfileSkin.Skin.Loader, Data)) + ModBase.RunInUi(ModMain.FrmLoginProfileSkin.Skin.Load); + else if (!Data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新 + Data.Input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染 + } + + // 离线皮肤 + public static ModLoader.LoaderTask, string> SkinLegacy = new("Loader Skin Legacy", + SkinLegacyLoad, SkinLegacyInput, ThreadPriority.AboveNormal); + + private static ModBase.EqualableList SkinLegacyInput() + { + return new ModBase.EqualableList + { ModProfile.SelectedProfile.Username, ModProfile.SelectedProfile.Uuid }; + } + + private static void SkinLegacyLoad(ModLoader.LoaderTask, string> Data) + { + // 清空已有皮肤 + ModBase.RunInUi(() => + { + if (ModMain.FrmLoginProfileSkin is not null && ModMain.FrmLoginProfileSkin.Skin is not null) + ModMain.FrmLoginProfileSkin.Skin.Clear(); + }); + Data.Output = ModBase.PathImage + "Skins/" + ModMinecraft.McSkinSex(Data.Input[1]) + ".png"; + // 刷新显示 + if (ModMain.FrmLoginProfileSkin is not null && ReferenceEquals(ModMain.FrmLoginProfileSkin.Skin.Loader, Data)) + ModBase.RunInUi(() => ModMain.FrmLoginProfileSkin.Skin.Load()); + else if (!Data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新 + Data.Input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染 + } + + // Authlib-Injector 皮肤 + public static ModLoader.LoaderTask, string> SkinAuth = new("Loader Skin Auth", + SkinAuthLoad, SkinAuthInput, ThreadPriority.AboveNormal); + + private static ModBase.EqualableList SkinAuthInput() + { + // 获取名称 + return new ModBase.EqualableList + { ModProfile.SelectedProfile.Username, ModProfile.SelectedProfile.Uuid }; + } + + private static void SkinAuthLoad(ModLoader.LoaderTask, string> Data) + { + // 清空已有皮肤 + // 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载 + ModBase.RunInUi(() => + { + if (ModMain.FrmLoginProfileSkin is not null && ModMain.FrmLoginProfileSkin.Skin is not null) + ModMain.FrmLoginProfileSkin.Skin.Clear(); + }); + // 获取 Url + var UserName = Data.Input[0]; + var Uuid = Data.Input[1]; + if (string.IsNullOrEmpty(UserName)) + { + Data.Output = ModBase.PathImage + "Skins/Steve.png"; + ModBase.Log("[Minecraft] 获取 Authlib-Injector 皮肤失败,ID 为空"); + goto Finish; + } + + try + { + var Result = ModMinecraft.McSkinGetAddress(Uuid, "Auth"); + if (Data.IsAborted) + throw new ThreadInterruptedException("当前任务已取消:" + UserName); + Result = ModMinecraft.McSkinDownload(Result); + if (Data.IsAborted) + throw new ThreadInterruptedException("当前任务已取消:" + UserName); + Data.Output = Result; + } + catch (Exception ex) + { + if (ex.GetType().Name == "ThreadInterruptedException") + { + Data.Output = ""; + return; + } + + if (ex.ToString().Contains("429")) + { + Data.Output = ModBase.PathImage + "Skins/Steve.png"; + ModBase.Log("[Minecraft] 获取 Authlib-Injector 皮肤失败(" + UserName + "):获取皮肤太过频繁,请 5 分钟后再试!", + ModBase.LogLevel.Hint); + } + else if (ex.ToString().Contains("未设置自定义皮肤")) + { + Data.Output = ModBase.PathImage + "Skins/Steve.png"; + ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载"); + } + else + { + Data.Output = ModBase.PathImage + "Skins/Steve.png"; + ModBase.Log(ex, "获取 Authlib-Injector 皮肤失败(" + UserName + ")", ModBase.LogLevel.Hint); + } + } + + Finish: ; + + // 刷新显示 + if (ModMain.FrmLoginProfileSkin is not null && ReferenceEquals(ModMain.FrmLoginProfileSkin.Skin.Loader, Data)) + ModBase.RunInUi(ModMain.FrmLoginProfileSkin.Skin.Load); + else if (!Data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新 + Data.Input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染 + } + + // 全部皮肤加载器 + // 需要放在其中元素的后面,否则会因为它提前被加载而莫名其妙变成 Nothing + public static List, string>> SkinLoaders = new() + { SkinMs, SkinLegacy, SkinAuth }; + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml index ae8d7f5e2..594498836 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml @@ -1,17 +1,16 @@ - + - - + - + x:Name="BtnHintClose" + Click="BtnHintClose_Click" /> @@ -22,4 +21,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs new file mode 100644 index 000000000..018dcb9a1 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.cs @@ -0,0 +1,465 @@ +using System.IO; +using System.Windows; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageLaunchRight : IRefreshable +{ + public PageLaunchRight() + { + InitializeComponent(); + OnlineLoader = new ModLoader.LoaderTask("下载主页", OnlineLoaderSub) + { ReloadTimeout = 10 * 60 * 1000 }; + Loaded += (_, _) => Init(); + Loaded += (_, _) => Refresh(); + } + + private void Init() + { + PanBack.ScrollToHome(); + PanScroll = PanBack; // 不知道为啥不能在 XAML 设置 + PanLog.Visibility = ModBase.ModeDebug ? Visibility.Visible : Visibility.Collapsed; + // 社区版提示 + PanHint.Visibility = Conversions.ToBoolean(States.Hint.CEMessage) + ? Visibility.Visible + : Visibility.Collapsed; + LabHint1.Text = + $"你正在使用 PCL 社区版!此版本为独立开发和维护,与官方版本维护路线不同,体验有所出入。{"\r\n"}{"\r\n"}如果你是意外下载到了社区版,我们十分建议您下载 PCL 官方版长期使用,此发行版本对新手用户体验可能不友好。{"\r\n"}此外,社区版的问题请向社区版的仓库提交 Issue,不要向官方仓库反馈社区版的问题哦!{"\r\n"}"; + LabHint2.Text = "若要永久隐藏此提示,请输入正确的 PCL CE 开发组织名称。"; + } + + // 暂时关闭快照版提示 + private void BtnHintClose_Click(object sender, EventArgs e) + { + var input = ModMain.MyMsgBoxInput("输入 PCL CE 开发组织名称"); + if (string.IsNullOrWhiteSpace(input)) + return; + input = new string(input.Where(x => char.IsAsciiLetter(x)).ToArray()).ToLower(); + if (input.Contains("pclcommunity")) + { + ModAnimation.AniDispose(PanHint, true); + States.Hint.CEMessage = false; + } + else + { + ModMain.Hint("不太对哦……"); + } + } + + #region 主页 + + /// + /// 刷新主页。 + /// + private void Refresh() + { + ModBase.RunInNewThread(() => + { + try + { + lock (RefreshLock) + { + RefreshReal(); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "加载 PCL 主页自定义信息失败", + ModBase.ModeDebug ? ModBase.LogLevel.Msgbox : ModBase.LogLevel.Hint); + } + }, $"刷新主页 #{ModBase.GetUuid()}"); + } + + private void RefreshReal() + { + var content = ""; + string url = null; + + var uiCustomType = (int)Config.Preference.Homepage.Type; + + if (uiCustomType == 1) + { + // 本地文件 + LogWrapper.Info("[Page] 主页自定义数据来源:本地文件"); + content = ModBase.ReadFile(Path.Combine(ModBase.ExePath, "PCL", "Custom.xaml")); + } + else if (uiCustomType == 2) + { + // 网络文件 + url = (string)Config.Preference.Homepage.CustomUrl; + content = LoadFromNetwork(url); + } + else if (uiCustomType == 3) + { + // 预设主页 + var preset = (int)Config.Preference.Homepage.SelectedPreset; + switch (preset) + { + case 0: + LogWrapper.Info("[Page] 主页预设:你知道吗"); + var hintText = GetRandomHint(); + content = $@" + + + + "; + break; + + case 1: + LogWrapper.Info("[Page] 主页预设:回声洞 已被移除"); + ModMain.MyMsgBox("回声洞 因为只有空壳因此已被移除,请前往设置选择其他预设主页"); + return; + + case 2: + LogWrapper.Info("[Page] 主页预设:Minecraft 新闻"); + url = "https://pcl.mcnews.thestack.top"; + content = LoadFromNetwork(url); + break; + + case 3: + LogWrapper.Info("[Page] 主页预设:简单主页"); + url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/MFn233/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 4: + LogWrapper.Info("[Page] 主页预设:每日整合包推荐"); + url = "https://pclsub.sodamc.com/"; + content = LoadFromNetwork(url); + break; + + case 5: + LogWrapper.Info("[Page] 主页预设:Minecraft 皮肤推荐"); + url = "https://forgepixel.com/pcl_sub_file"; + content = LoadFromNetwork(url); + break; + + case 6: + LogWrapper.Info("[Page] 主页预设:OpenBMCLAPI 仪表盘 Lite"); + url = "https://pcl-bmcl.milu.ink/"; + content = LoadFromNetwork(url); + break; + + case 7: + LogWrapper.Info("[Page] 主页预设:主页市场"); + url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/JingHai-Lingyun/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 8: + LogWrapper.Info("[Page] 主页预设:更新日志"); + url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Joker2184/UpdateHomepage.xaml"; + content = LoadFromNetwork(url); + break; + + case 9: + LogWrapper.Info("[Page] 主页预设:PCL 新功能说明书"); + url = "https://raw.gitcode.com/WForst-Breeze/WhatsNewPCL/raw/main/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 10: + LogWrapper.Info("[Page] 主页预设:OpenMCIM Dashboard"); + url = "https://files.mcimirror.top/PCL"; + content = LoadFromNetwork(url); + break; + + case 11: + LogWrapper.Info("[Page] 主页预设:杂志主页"); + url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Ext1nguisher/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 12: + LogWrapper.Info("[Page] 主页预设:PCL GitHub 仪表盘"); + url = "https://ddf.pcl-community.org/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 13: + LogWrapper.Info("[Page] 主页预设:Minecraft 更新摘要"); + url = "https://raw.gitcode.com/ENC_Euphony/PCL-AI-Summary-HomePage/raw/master/Custom.xaml"; + content = LoadFromNetwork(url); + break; + + case 14: + LogWrapper.Info("[Page] 主页预设:PCL CE 公告栏"); + url = "https://s3.pysio.online/pcl2-ce/apiv2/pages/announce.xaml"; + content = LoadFromNetwork(url); + break; + } + } + + ModBase.RunInUi(() => LoadContent(content)); + } + + /// + /// 根据 URL 加载网络内容,优先使用缓存 + /// + private string LoadFromNetwork(string url) + { + if (string.IsNullOrWhiteSpace(url)) return ""; + + var cachePath = Path.Combine(ModBase.PathTemp, "Cache", "Custom.xaml"); + var cachedUrl = (string)States.UI.SavedHomepageUrl; + + if (url == cachedUrl && File.Exists(cachePath)) + { + LogWrapper.Info("[Page] 主页自定义数据来源:联网缓存文件"); + // 后台更新缓存 + OnlineLoader.Start(url); + return ModBase.ReadFile(cachePath); + } + + LogWrapper.Info("[Page] 主页自定义数据来源:联网全新下载"); + HintWrapper.Show("正在加载主页……"); + ModBase.RunInUiWait(() => LoadContent("")); // 先清空页面 + States.UI.SavedHomepageVersion = ""; + OnlineLoader.Start(url); // 下载完成后将会再次触发更新 + return ""; + } + + private readonly object RefreshLock = new(); + + public static string GetRandomHint(bool enableLengthLimit = false) + { + // 优先尝试外部文件 + var externalPath = ModBase.ExePath + @"PCL\hints.txt"; + if (File.Exists(externalPath)) + try + { + var lines = File.ReadAllLines(externalPath).Where(l => !string.IsNullOrWhiteSpace(l)) + .Select(l => l.Trim()).ToArray(); + if (lines.Length > 0) + { + var validHints = lines; + if (enableLengthLimit) + { + validHints = lines.Where(l => l.Length < 50).ToArray(); + if (validHints.Length == 0) + { + validHints = lines; + ModBase.Log("[Page] 外部 hints.txt 中没有字数小于50的提示,已取消字数限制", ModBase.LogLevel.Debug); + } + } + + var hint = validHints[new Random().Next(validHints.Length)]; + hint = hint.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); + return hint; + } + + ModBase.Log("[Page] 外部 hints.txt 文件为空", ModBase.LogLevel.Debug); + return "PCL CE 是由 PCL-Community 开发的 PCL 社区衍生版本"; + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] 读取外部 hints.txt 失败", ModBase.LogLevel.Hint); + } + + // 回退到嵌入式资源 + try + { + using (var reader = new StreamReader(System.Windows.Application + .GetResourceStream(new Uri( + "pack://application:,,,/Plain Craft Launcher 2;component/Resources/hints.txt", + UriKind.Absolute)).Stream)) + { + var lines = reader.ReadToEnd() + .Split(new[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries) + .Where(l => !string.IsNullOrWhiteSpace(l)).Select(l => l.Trim()).ToArray(); + var validHints = enableLengthLimit ? lines.Where(l => l.Length < 50).ToArray() : lines; + var hint = validHints[new Random().Next(validHints.Length)]; + hint = hint.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """); + return hint; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "[Page] 嵌入式资源 hints.txt 读取失败", ModBase.LogLevel.Hint); + return "PCL CE 是由 PCL-Community 开发的 PCL 社区衍生版本"; + } + } + + // 联网获取主页文件 + private readonly ModLoader.LoaderTask OnlineLoader; + + private void OnlineLoaderSub(ModLoader.LoaderTask Task) + { + var Address = Task.Input; // #3721 中连续触发两次导致内容变化 + try + { + // 获取版本校验地址 + string VersionAddress; + if (Address.Contains(".xaml")) + { + VersionAddress = Address.Replace(".xaml", ".xaml.ini"); + } + else + { + VersionAddress = Address.BeforeFirst("?"); + if (!VersionAddress.EndsWith("/")) + VersionAddress += "/"; + VersionAddress += "version"; + if (Address.Contains("?")) + VersionAddress += "?" + Address.AfterFirst("?"); + } + + // 校验版本 + var Version = ""; + var NeedDownload = true; + try + { + Version = Conversions.ToString(ModNet.NetGetCodeByRequestOnce(VersionAddress, Timeout: 10000)); + if (Version.Length > 1000) + throw new Exception($"获取的主页版本过长({Version.Length} 字符)"); + var CurrentVersion = Conversions.ToString(States.UI.SavedHomepageVersion); + if (!string.IsNullOrEmpty(Version) && !string.IsNullOrEmpty(CurrentVersion) && + (Version ?? "") == (CurrentVersion ?? "")) + { + ModBase.Log($"[Page] 当前缓存的主页已为最新,当前版本:{Version},检查源:{VersionAddress}"); + NeedDownload = false; + } + else + { + ModBase.Log($"[Page] 需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}"); + } + } + catch (Exception exx) + { + ModBase.Log(exx, "联网获取主页版本失败", ModBase.LogLevel.Developer); + ModBase.Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{VersionAddress}"); + } + + // 实际下载 + if (NeedDownload) + { + var FileContent = Conversions.ToString(ModNet.NetGetCodeByRequestRetry(Address)); + ModBase.Log($"[Page] 已联网下载主页,内容长度:{FileContent.Length},来源:{Address}"); + States.UI.SavedHomepageUrl = Address; + States.UI.SavedHomepageVersion = Version; + ModBase.WriteFile(ModBase.PathTemp + @"Cache\Custom.xaml", FileContent); + } + + // 要求刷新 + ModBase.RunInUi(Refresh); // 不直接调用 Refresh,以防止死循环(#6245) + } + catch (Exception ex) + { + ModBase.Log(ex, $"下载主页失败({Address})", ModBase.ModeDebug ? ModBase.LogLevel.Msgbox : ModBase.LogLevel.Hint); + } + } + + /// + /// 立即强制刷新主页。 + /// 必须在 UI 线程调用。 + /// + public void ForceRefresh() + { + ModBase.Log("[Page] 要求强制刷新主页"); + ClearCache(); + // 实际的刷新 + if (ModMain.FrmMain.PageCurrent.Page == FormMain.PageType.Launch) + { + PanBack.ScrollToHome(); + Refresh(); + } + else + { + ModMain.FrmMain.PageChange(FormMain.PageType.Launch); + } + } + + void IRefreshable.Refresh() + { + ForceRefresh(); + } + + /// + /// 清空主页缓存信息。 + /// + private void ClearCache() + { + LoadedContentHash = -1; + OnlineLoader.Input = ""; + States.UI.SavedHomepageUrl = ""; + States.UI.SavedHomepageVersion = ""; + ModBase.Log("[Page] 已清空主页缓存"); + } + + /// + /// 从文本内容中加载主页。 + /// 必须在 UI 线程调用。 + /// + private void LoadContent(string Content) + { + lock (LoadContentLock) + { + // 如果加载目标内容一致则不加载 + var Hash = Content.GetHashCode(); + if (Hash == LoadedContentHash) + return; + LoadedContentHash = Hash; + // 实际加载内容 + PanCustom.Children.Clear(); + if (string.IsNullOrWhiteSpace(Content)) + { + ModBase.Log("[Page] 实例化:清空主页 UI,来源为空"); + return; + } + + var LoadStartTime = DateTime.Now; + try + { + // 修改时应同时修改 PageOtherHelpDetail.Init + Content = ModMain.HelpArgumentReplace(Content); + while (Content.Contains("xmlns")) + Content = Content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", ""); + Content = + "" + + Content + ""; + ModBase.Log($"[Page] 实例化:加载主页 UI 开始,最终内容长度:{Content.Count()}"); + PanCustom.Children.Add((UIElement)ModBase.GetObjectFromXML(Content)); + } + catch (Exception ex) + { + if (ModBase.ModeDebug) + { + ModBase.Log(ex, "加载失败的主页内容:" + "\r\n" + Content); + if (ModMain.MyMsgBox( + ex is UnauthorizedAccessException + ? ex.Message + : $"主页内容编写有误,请根据下列错误信息进行检查:{"\r\n"}{ex}", "加载主页界面失败", "重试", "取消") == + 1) goto Refresh; // 防止 SyncLock 死锁 + } + else + { + ModBase.Log(ex, "加载主页界面失败", ModBase.LogLevel.Hint); + } + + return; + } + + var LoadCostTime = (DateTime.Now - LoadStartTime).Milliseconds; + ModBase.Log($"[Page] 实例化:加载主页 UI 完成,耗时 {LoadCostTime}ms"); + if (LoadCostTime > 3000) + ModMain.Hint($"主页加载过于缓慢(花费了 {Math.Round(LoadCostTime / 1000d, 1)} 秒),请向主页作者反馈此问题,或暂时停止使用该主页"); + } + + return; + Refresh: ; + + ForceRefresh(); + } + + private int LoadedContentHash = -1; + private readonly object LoadContentLock = new(); + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml index 975ade027..73808287c 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml @@ -1,8 +1,8 @@ - @@ -18,19 +18,35 @@ - + - - - - + + + + - - - + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.cs new file mode 100644 index 000000000..168a6bb88 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.cs @@ -0,0 +1,202 @@ +using System.Net.Http; +using System.Windows; +using System.Windows.Controls; +using Microsoft.VisualBasic.CompilerServices; +using Newtonsoft.Json.Linq; +using PCL.Core.IO.Net.Http.Client; +using PCL.Core.Minecraft.Yggdrasil; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public partial class PageLoginAuth +{ + public static string DraggedAuthServer; + + // 预设服务器 + private static readonly Dictionary PredefinedAuthServers = new() + { { "预设 - LittleSkin", "https://littleskin.cn/api/yggdrasil" }, { "自定义", "" } }; + + public PageLoginAuth() + { + InitializeComponent(); + Loaded += (_, __) => Reload(); + Loaded += (_, __) => ReloadRegisterButton(); + // Handles + BtnBack.Click += BtnBack_Click; + BtnLogin.Click += BtnLogin_Click; + TextServer.TextChanged += TextServer_TextChanged; + BtnLink.Click += Btn_Click; + } + + private void Reload() + { + var serverItems = TextServer.Items; + serverItems.Clear(); + foreach (var serverName in PredefinedAuthServers.Keys) + serverItems.Add(new MyComboBoxItem { Content = serverName }); + if (DraggedAuthServer is not null) + { + TextServer.Text = DraggedAuthServer; + DraggedAuthServer = null; + } + } + + private void BtnBack_Click(object sender, EventArgs e) + { + TextServer.Text = null; + TextName.Text = null; + TextPass.Password = null; + ModMain.FrmLaunchLeft.RefreshPage(true); + } + + private void BtnLogin_Click(object sender, EventArgs e) + { + if (string.IsNullOrWhiteSpace(TextServer.Text) || string.IsNullOrWhiteSpace(TextName.Text) || + string.IsNullOrWhiteSpace(TextPass.Password)) + { + ModMain.Hint("验证服务器、用户名与密码均不能为空!", ModMain.HintType.Critical); + return; + } + + if (!TextServer.Text.IsMatch(RegexPatterns.HttpUri)) + { + ModMain.Hint("输入的验证服务器地址无效", ModMain.HintType.Critical); + return; + } + + BtnLogin.IsEnabled = false; + BtnBack.IsEnabled = false; + var LoginData = new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth) + { + BaseUrl = TextServer.Text.EndsWithF("/") ? TextServer.Text + "authserver" : TextServer.Text + "/authserver", + UserName = TextName.Text, Password = TextPass.Password, Description = "Authlib-Injector", + Type = ModLaunch.McLoginType.Auth + }; + Dispatcher.BeginInvoke(new Func(async () => + { + try + { + ModProfile.IsCreatingProfile = true; + ModLaunch.McLoginAuthLoader.Start(LoginData, true); + while (ModLaunch.McLoginAuthLoader.State == ModBase.LoadState.Loading) + { + BtnLogin.Text = Math.Round(ModLaunch.McLoginAuthLoader.Progress * 100d) + "%"; + await Task.Delay(50); + } + + if (ModLaunch.McLoginAuthLoader.State == ModBase.LoadState.Finished) + ModMain.FrmLaunchLeft.RefreshPage(true); + else if (ModLaunch.McLoginAuthLoader.State == ModBase.LoadState.Aborted) + ModMain.Hint("已取消登录!"); + else if (ModLaunch.McLoginAuthLoader.Error is null) + throw new Exception("未知错误!"); + else + throw new Exception(ModLaunch.McLoginAuthLoader.Error.Message, ModLaunch.McLoginAuthLoader.Error); + } + catch (Exception ex) + { + if (ex.Message == "$$") + { + } + else if (ex.Message.StartsWith("$")) + { + ModMain.Hint(ex.Message.TrimStart('$'), ModMain.HintType.Critical); + } + else + { + ModBase.Log(ex, "第三方登录尝试失败", ModBase.LogLevel.Msgbox); + } + } + finally + { + ModProfile.IsCreatingProfile = false; + BtnLogin.IsEnabled = true; + BtnBack.IsEnabled = true; + BtnLogin.Text = "登录"; + } + })); + } + + // 获取验证服务器名称 + private void GetServerName() + { + var serverUriInput = TextServer.Text; + if (string.IsNullOrWhiteSpace(serverUriInput)) + { + TextServerName.Visibility = Visibility.Hidden; + return; + } + + Dispatcher.BeginInvoke(async () => + { + string serverUri = null; + string serverName = null; + try + { + serverUri = await ApiLocation.TryRequestAsync(serverUriInput); + var response = await HttpRequestBuilder.Create(serverUri, HttpMethod.Get).SendAsync(); + var responseText = await response.AsStringAsync(); + serverName = await Task.Run(() => JObject.Parse(responseText)["meta"]["serverName"].ToString()); + } + catch (Exception ex) + { + ModBase.Log(ex, "从服务器获取名称失败"); + } + + if (serverUri is not null) + TextServer.Text = serverUri; + if (serverName is null) + { + TextServerName.Visibility = Visibility.Hidden; + } + else + { + TextServerName.Text = "验证服务器: " + serverName; + TextServerName.Visibility = Visibility.Visible; + } + }); + } + + // 链接处理 + private void ComboName_TextChanged(object sender, TextChangedEventArgs e) + { + BtnLink.Content = string.IsNullOrEmpty(TextName.Text) ? "注册账号" : "找回密码"; + } + + private void Btn_Click(object sender, EventArgs e) + { + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(BtnLink.Content, "注册账号", false))) + { + ModBase.OpenWebsite(Conversions.ToString(ModMinecraft.McInstanceSelected is not null + ? ModBase.Setup.Get("VersionServerAuthRegister", ModMinecraft.McInstanceSelected) + : "")); + } + else + { + var Website = Conversions.ToString(ModMinecraft.McInstanceSelected is not null + ? ModBase.Setup.Get("VersionServerAuthRegister", ModMinecraft.McInstanceSelected) + : ""); + ModBase.OpenWebsite(Website.Replace("/auth/register", "/auth/forgot")); + } + } + + // 切换注册按钮可见性 + private void ReloadRegisterButton() + { + var Address = Conversions.ToString(ModMinecraft.McInstanceSelected is not null + ? ModBase.Setup.Get("VersionServerAuthRegister", ModMinecraft.McInstanceSelected) + : ""); + BtnLink.Visibility = string.IsNullOrEmpty(new ValidateHttp().Validate(Address)) + ? Visibility.Visible + : Visibility.Collapsed; + } + + private void TextServer_TextChanged(object sender, TextChangedEventArgs e) + { + string server = null; + PredefinedAuthServers.TryGetValue(TextServer.Text, out server); + if (server is not null) TextServer.Text = server; + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml index d3e282f4c..457e345c4 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml @@ -1,10 +1,10 @@ - + @@ -17,9 +17,11 @@ - - + @@ -27,11 +29,13 @@ - + + Grid.Column="2" /> - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.cs new file mode 100644 index 000000000..97b91383a --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.cs @@ -0,0 +1,83 @@ +using System.Security.Authentication; +using System.Windows; +using Microsoft.VisualBasic; + +namespace PCL; + +public partial class PageLoginMs +{ + public PageLoginMs() + { + // Handles + InitializeComponent(); + BtnBack.Click += BtnBack_Click; + BtnLogin.Click += BtnLogin_Click; + } + + private void BtnBack_Click(object sender, EventArgs e) + { + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true)); + } + + private void BtnLogin_Click(object sender, EventArgs e) + { + BtnLogin.IsEnabled = false; + BtnBack.Visibility = Visibility.Collapsed; + BtnLogin.Text = "0%"; + ModBase.RunInNewThread(() => + { + try + { + ModProfile.SelectedProfile = null; + ModLaunch.McLoginMsLoader.Start(ModProfile.GetLoginData(ModLaunch.McLoginType.Ms), true); + while (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Loading) + { + ModBase.RunInUi(() => BtnLogin.Text = Math.Round(ModLaunch.McLoginMsLoader.Progress * 100d) + "%"); + Thread.Sleep(50); + } + + if (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Finished) + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true)); + else if (ModLaunch.McLoginMsLoader.State == ModBase.LoadState.Aborted) + throw new ThreadInterruptedException(); + else if (ModLaunch.McLoginMsLoader.Error is null) + throw new Exception("未知错误!"); + else + throw new Exception(ModLaunch.McLoginMsLoader.Error.Message, ModLaunch.McLoginMsLoader.Error); + } + catch (ThreadInterruptedException ex) + { + ModMain.Hint("已取消登录!"); + } + catch (Exception ex) + { + if (ex.Message == "$$") + { + } + else if (ex.Message.StartsWith("$")) + { + ModMain.Hint(ex.Message.TrimStart('$'), ModMain.HintType.Critical); + } + else if (ex is AuthenticationException && ex.Message.ContainsF("SSL/TLS")) + { + ModBase.Log(ex, + "正版登录验证失败,请考虑在 [设置 → 其他] 中关闭 [在正版登录时验证 SSL 证书],然后再试。" + "\r\n" + "\r\n" + + "原始错误信息:", ModBase.LogLevel.Msgbox); + } + else + { + ModBase.Log(ex, "正版登录尝试失败", ModBase.LogLevel.Msgbox); + } + } + finally + { + ModBase.RunInUi(() => + { + BtnLogin.IsEnabled = true; + BtnBack.Visibility = Visibility.Visible; + BtnLogin.Text = "登录"; + }); + } + }, "Ms Login"); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml index f64435a8e..989e0173a 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml @@ -1,8 +1,8 @@ - @@ -18,27 +18,34 @@ - + - + - - + - + - - - - + + + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml.cs new file mode 100644 index 000000000..2df706097 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginOffline.xaml.cs @@ -0,0 +1,87 @@ +using System.Windows; +using Microsoft.VisualBasic; + +namespace PCL; + +public partial class PageLoginOffline +{ + public PageLoginOffline() + { + // Handles + InitializeComponent(); + BtnBack.Click += BtnBack_Click; + RadioUuidCustom.Check += RadioUuid_Checked; + RadioUuidStandard.Check += RadioUuid_Checked; + RadioUuidLegacy.Check += RadioUuid_Checked; + BtnLogin.Click += BtnLogin_Click; + } + + private void BtnBack_Click(object sender, EventArgs e) + { + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true)); + } + + private void RadioUuid_Checked(object sender, ModBase.RouteEventArgs e) + { + if (RadioUuidCustom.Checked) + { + TextUuidTitle.Visibility = Visibility.Visible; + TextUuid.Visibility = Visibility.Visible; + } + else + { + TextUuidTitle.Visibility = Visibility.Collapsed; + TextUuid.Visibility = Visibility.Collapsed; + } + } + + private void BtnLogin_Click(object sender, EventArgs e) + { + // 玩家 ID 输入检查 + var Username = TextName.Text; + var UsernameValidateResult = new ValidateRegex("^[A-z0-9_]{3,16}$").Validate(Username); + if (!string.IsNullOrEmpty(UsernameValidateResult)) + if (ModMain.MyMsgBox( + $"你输入的玩家 ID 不符合标准(3 - 16 位,只可以包含英文字母、数字与下划线),可能导致部分版本的游戏无法启动或发生错误。{"\r\n"}强烈建议使用规范的玩家 ID!{"\r\n"}如果你坚持,仍然可以继续创建档案。", + "玩家 ID 不符合规范", "继续", "取消", IsWarn: true, ForceWait: true) == 2) + return; + // UUID + string UserUuid = null; + if (RadioUuidCustom.Checked) + { + // 自定义输入检查 + var UuidInput = TextUuid.Text.Replace("-", ""); + var UuidValidateResult = new ValidateRegex("^[a-fA-F0-9]{32}$").Validate(UuidInput); + if (RadioUuidCustom.Checked && !string.IsNullOrEmpty(UuidValidateResult)) + { + ModMain.Hint("UUID 不符合要求:" + UuidValidateResult, ModMain.HintType.Critical); + return; + } + + UserUuid = UuidInput; + } + else if (RadioUuidLegacy.Checked) + { + UserUuid = ModProfile.GetOfflineUuid(Username, isLegacy: true); + } + else + { + UserUuid = ModProfile.GetOfflineUuid(Username); + } + + // 创建档案 + var NewProfile = new ModProfile.McProfile + { + Type = ModLaunch.McLoginType.Legacy, + Uuid = UserUuid, + Username = Username, + Desc = "" + }; + ModProfile.ProfileList.Add(NewProfile); + ModProfile.SaveProfile(); + ModProfile.SelectedProfile = NewProfile; + ModProfile.IsCreatingProfile = false; + ModMain.Hint("档案新建成功!", ModMain.HintType.Finish); + ModBase.RunInUi(() => ModMain.FrmLaunchLeft.RefreshPage(true)); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml index e448254e1..afc103a52 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml @@ -1,13 +1,13 @@ - - + @@ -16,36 +16,42 @@ - - - - + + + + + Title="{Binding Username}" Info="{Binding Info}" + Type="Clickable" Logo="{Binding Logo}" Tag="{Binding Profile}" + Click="SelectProfile" ContentHandler="ProfileContMenuBuild" /> + CornerRadius="5" Margin="0,0,4,0"> - - + - + Click="BtnPort_Click" + ToolTip="导入 / 导出" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35" + ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" + LogoScale="1.1" + Logo="M768.704 703.616c-35.648 0-67.904 14.72-91.136 38.304l-309.152-171.712c9.056-17.568 14.688-37.184 14.688-58.272 0-12.576-2.368-24.48-5.76-35.936l304.608-189.152c22.688 20.416 52.384 33.184 85.216 33.184 70.592 0 128-57.408 128-128s-57.408-128-128-128-128 57.408-128 128c0 14.56 2.976 28.352 7.456 41.408l-301.824 187.392c-23.136-22.784-54.784-36.928-89.728-36.928-70.592 0-128 57.408-128 128 0 70.592 57.408 128 128 128 25.664 0 49.504-7.744 69.568-20.8l321.216 178.4c-3.04 10.944-5.184 22.208-5.184 34.08 0 70.592 57.408 128 128 128s128-57.408 128-128S839.328 703.616 768.704 703.616zM767.2 128.032c35.296 0 64 28.704 64 64s-28.704 64-64 64-64-28.704-64-64S731.904 128.032 767.2 128.032zM191.136 511.936c0-35.296 28.704-64 64-64s64 28.704 64 64c0 35.296-28.704 64-64 64S191.136 547.232 191.136 511.936zM768.704 895.616c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S804 895.616 768.704 895.616z" /> - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml.cs new file mode 100644 index 000000000..6a572cd94 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfile.xaml.cs @@ -0,0 +1,191 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; + +namespace PCL; + +public partial class PageLoginProfile +{ + public PageLoginProfile() + { + InitializeComponent(); + Loaded += (_, _) => Reload(); + } + + public ObservableCollection ProfileCollection { get; set; } = new(); + + /// + /// 刷新页面显示的所有信息。 + /// + public void Reload() + { + RefreshProfileList(); + ModMain.FrmLoginProfileSkin = null; + // RunInNewThread(Sub() + // Thread.Sleep(800) + // RunInUi(Sub() FrmLaunchLeft.RefreshPage(True)) + // End Sub) + } + + /// + /// 刷新档案列表 + /// + public void RefreshProfileList() + { + ModBase.Log("[Profile] 刷新档案列表"); + ProfileCollection.Clear(); + ModProfile.GetProfile(); + try + { + foreach (var Profile in ModProfile.ProfileList) + ProfileCollection.Add(new ProfileItem(Profile)); + ModBase.Log("[Profile] 档案列表刷新完成"); + } + catch (Exception ex) + { + ModBase.Log(ex, "读取档案列表失败", ModBase.LogLevel.Feedback); + } + + if (!ModProfile.ProfileList.Any()) + { + States.Hint.LaunchWithProfile = true; + HintCreate.Visibility = Visibility.Visible; + } + else + { + HintCreate.Visibility = Visibility.Collapsed; + } + } + + public class ProfileItem + { + public ProfileItem(ModProfile.McProfile profile) + { + Profile = profile; + Info = Conversions.ToString(ModProfile.GetProfileInfo(profile)); + var LogoPath = ModBase.PathTemp + $@"Cache\Skin\Head\{profile.SkinHeadId}.png"; + if (!(File.Exists(LogoPath) && !(new FileInfo(LogoPath).Length == 0L))) + LogoPath = ModBase.Logo.IconButtonUser; + Logo = LogoPath; + } + + public string Info { get; private set; } + public string Logo { get; private set; } + public ModProfile.McProfile Profile { get; } + public string Username => Profile.Username; + } + + #region 控件 + + private void SelectProfile(object sender, MouseButtonEventArgs e) + { + var item = (MyListItem)sender; + var tag = (ModProfile.McProfile)item.Tag; + ModProfile.SelectedProfile = (ModProfile.McProfile)((MyListItem)sender).Tag; + ModBase.Log($"[Profile] 选定档案: {tag.Username}, 以 {tag.Type} 方式验证"); + ModProfile.LastUsedProfile = + ModProfile.ProfileList.IndexOf((ModProfile.McProfile)((MyListItem)sender).Tag); // 获取当前档案的序号 + ModProfile.SaveProfile(); // 保存档案配置,确保切换后的档案被正确保存 + + // 清除登录验证缓存,确保使用新档案的验证信息 + ModLaunch.McLoginMsLoader.State = ModBase.LoadState.Waiting; + ModLaunch.McLoginAuthLoader.State = ModBase.LoadState.Waiting; + ModLaunch.McLoginLegacyLoader.State = ModBase.LoadState.Waiting; + + ModBase.RunInUi(() => + { + ModMain.FrmLaunchLeft.RefreshPage(true); + ModMain.FrmLaunchLeft.BtnLaunch.IsEnabled = true; + }); + } + + private void ProfileContMenuBuild(MyListItem sender, EventArgs e) + { + // 更改 UUID + var btnEditUuid = new MyIconButton + { Logo = ModBase.Logo.IconButtonEdit, ToolTip = "更改 UUID", Tag = sender.Tag }; + ToolTipService.SetPlacement(btnEditUuid, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnEditUuid, 30d); + ToolTipService.SetHorizontalOffset(btnEditUuid, 2d); + btnEditUuid.Click += EditProfileUuid; + // 复制 UUID + var btnCopyUuid = new MyIconButton + { Logo = ModBase.Logo.IconButtonCopy, ToolTip = "复制 UUID", Tag = sender.Tag }; + ToolTipService.SetPlacement(btnCopyUuid, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnCopyUuid, 30d); + ToolTipService.SetHorizontalOffset(btnCopyUuid, 2d); + btnCopyUuid.Click += CopyProfileUuid; + // 更改验证服务器名称 + var btnEditServerName = new MyIconButton + { Logo = ModBase.Logo.IconButtonInfo, ToolTip = "更改验证服务器名称", Tag = sender.Tag }; + ToolTipService.SetPlacement(btnEditServerName, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnEditServerName, 30d); + ToolTipService.SetHorizontalOffset(btnEditServerName, 2d); + btnEditServerName.Click += EditProfileServer; + // 删除档案 + var btnDelete = new MyIconButton { Logo = ModBase.Logo.IconButtonDelete, ToolTip = "删除档案", Tag = sender.Tag }; + ToolTipService.SetPlacement(btnDelete, PlacementMode.Center); + ToolTipService.SetVerticalOffset(btnDelete, 30d); + ToolTipService.SetHorizontalOffset(btnDelete, 2d); + btnDelete.Click += DeleteProfile; + // 根据档案类型显示不同的菜单项 + if (((ModProfile.McProfile)sender.Tag).Type == ModLaunch.McLoginType.Legacy) + sender.Buttons = new[] { btnEditUuid, btnDelete }; + else + sender.Buttons = new[] { btnCopyUuid, btnDelete }; + } + + // 创建档案 + private void BtnNew_Click(object sender, EventArgs e) + { + ModBase.RunInNewThread(() => + { + ModProfile.CreateProfile(); + ModBase.RunInUi(() => RefreshProfileList()); + }); + } + + // 编辑 UUID + private void EditProfileUuid(object sender, EventArgs e) + { + ModProfile.EditOfflineUuid((ModProfile.McProfile)((MyIconButton)sender).Tag); + } + + private void CopyProfileUuid(object sender, EventArgs e) + { + if (sender is MyIconButton { Tag: ModProfile.McProfile profile }) ModBase.ClipboardSet(profile.Uuid); + } + + // 编辑验证服务器名称 + private void EditProfileServer(object sender, EventArgs e) + { + string name = ModMain.MyMsgBoxInput("修改验证服务器名称", "请输入新的验证服务器名称", + Conversions.ToString(((dynamic)sender).Tag.ServerName)); + if (name is not null) ModProfile.EditAuthServerName((ModProfile.McProfile)((dynamic)sender).Tag, name); + } + + // 删除档案 + private void DeleteProfile(object sender, EventArgs e) + { + if (ModMain.MyMsgBox($"你正在选择删除此档案,该操作无法撤销。{"\r\n"}确定继续?", "删除档案确认", "继续", "取消", IsWarn: true, + ForceWait: true) == 2) + return; + ModProfile.RemoveProfile((ModProfile.McProfile)((dynamic)sender).Tag); + ModBase.RunInUi(() => RefreshProfileList()); + } + + // 导入 / 导出档案 + private void BtnPort_Click(object sender, EventArgs e) + { + ModProfile.MigrateProfile(); + ModBase.RunInUi(() => RefreshProfileList()); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml index 14de570cd..975e561b6 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml @@ -1,10 +1,10 @@ - @@ -18,28 +18,36 @@ - - - + + + + CornerRadius="5" Margin="0,8,0,0"> - + - - - - + + + + + ToolTip="修改信息" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35" + ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" + LogoScale="1.1" + Logo="M462.336 924.891429L73.142857 950.857143l25.965714-389.193143 467.017143-467.017143a73.398857 73.398857 0 0 1 103.789715 0l259.437714 259.437714a73.398857 73.398857 0 0 1 0 103.789715l-467.017143 467.017143z m155.684571-778.349715L202.861714 561.664l259.474286 259.474286L877.458286 405.942857 618.057143 146.541714zM151.003429 873.033143l233.508571-25.965714-207.579429-207.579429-25.965714 233.545143z m544.841142-492.982857l51.894858 51.894857-233.508572 233.508571-51.931428-51.894857 233.545142-233.508571z"> @@ -47,10 +55,12 @@ - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml.cs b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml.cs new file mode 100644 index 000000000..92ba04942 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml.cs @@ -0,0 +1,159 @@ +using System.Windows; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; + +namespace PCL; + +public partial class PageLoginProfileSkin +{ + public PageLoginProfileSkin() + { + InitializeComponent(); + Loaded += (_, _) => Reload(); + // Handles + PanData.MouseEnter += ShowPanel; + PanData.MouseLeave += HidePanel; + BtnSkin.Click += BtnSkin_Click; + BtnEdit.Click += BtnEdit_Click; + BtnSelect.Click += ChangeProfile; + } + + /// + /// 刷新页面显示的所有信息。 + /// + public void Reload() + { + ModBase.Log("[Profile] 刷新档案界面"); + Skin.Clear(); + if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Ms) + { + BtnEdit.Visibility = Visibility.Visible; + ModBase.Log("[Profile] 使用正版皮肤加载器"); + Skin.Loader = PageLaunchLeft.SkinMs; + } + else if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Auth) + { + BtnEdit.Visibility = Visibility.Visible; + ModBase.Log("[Profile] 使用 Authlib 皮肤加载器"); + Skin.Loader = PageLaunchLeft.SkinAuth; + } + else + { + BtnEdit.Visibility = Visibility.Collapsed; + ModBase.Log("[Profile] 使用离线皮肤加载器"); + Skin.Loader = PageLaunchLeft.SkinLegacy; + } + + Skin.Loader.Start(IsForceRestart: true); + TextName.Text = ModProfile.SelectedProfile.Username; + TextType.Text = Conversions.ToString(ModProfile.GetProfileInfo(ModProfile.SelectedProfile)); + } + + #region 控制与编辑 + + // 显示 / 隐藏控制 + private void ShowPanel(object sender, MouseEventArgs e) + { + ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, 1d - PanButtons.Opacity, 120), + "PageLoginProfileSkin Button"); + } + + private void HidePanel(object sender, EventArgs e) + { + if (BtnEdit.ContextMenu.IsOpen || BtnSkin.ContextMenu.IsOpen || PanData.IsMouseOver) + return; + ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, -PanButtons.Opacity, 120), + "PageLoginProfileSkin Button"); + } + + private void MenuAccountOptions_Closed(object sender, RoutedEventArgs e) + { + HidePanel(sender, e); + } + + // 皮肤与披风子菜单 + private void BtnSkin_Click(object sender, EventArgs e) + { + BtnSkin.ContextMenu.IsOpen = true; + } + + // 账号信息子菜单 + private void BtnEdit_Click(object sender, EventArgs e) + { + BtnEdit.ContextMenu.IsOpen = true; + } + + // 修改密码 + private void BtnEditPassword_Click(object sender, RoutedEventArgs e) + { + if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Ms) + { + ModBase.OpenWebsite("https://account.live.com/password/Change"); + } + else if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Auth) + { + var Server = ModProfile.SelectedProfile.Server; + ModBase.OpenWebsite(Server.Replace("/api/yggdrasil/authserver" + (Server.EndsWithF("/") ? "/" : ""), + "/user/profile")); + } + else + { + ModMain.Hint("当前档案不支持修改密码!"); + } + } + + // 修改 ID + private void BtnEditName_Click(object sender, RoutedEventArgs e) + { + ModProfile.EditProfileId(); + } + + // 选择档案 + private void ChangeProfile(object sender, EventArgs e) + { + ModProfile.SelectedProfile = null; + ModBase.RunInUi(() => + { + ModMain.FrmLaunchLeft.RefreshPage(true); + ModMain.FrmLaunchLeft.BtnLaunch.IsEnabled = false; + }); + } + + // 修改皮肤 + private void Skin_Click(object sender, RoutedEventArgs e) + { + if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Ms) + ModProfile.ChangeSkinMs(); + else if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Auth) + ModBase.OpenWebsite(ModProfile.SelectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") + + "user/closet"); + else + ModMain.Hint("当前档案不支持修改皮肤!"); + } + + // 保存皮肤 + private void BtnSkinSave_Click(object sender, RoutedEventArgs e) + { + Skin.BtnSkinSave_Click(sender, e); + } + + // 刷新皮肤 + private void BtnSkinRefresh_Click(object sender, RoutedEventArgs e) + { + Skin.RefreshClick(sender, e); + } + + // 修改披风 + private void BtnSkinCape_Click(object sender, RoutedEventArgs e) + { + if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Ms) + Skin.BtnSkinCape_Click(sender, e); + else if (ModProfile.SelectedProfile.Type == ModLaunch.McLoginType.Auth) + ModBase.OpenWebsite(ModProfile.SelectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") + + "user/closet"); + else + ModMain.Hint("当前档案不支持修改披风!"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLogLeft.xaml b/Plain Craft Launcher 2/Pages/PageLogLeft.xaml index 74af9ef65..3c761f01a 100644 --- a/Plain Craft Launcher 2/Pages/PageLogLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageLogLeft.xaml @@ -1,7 +1,8 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PCL;assembly=" + x:Class="PCL.PageLogLeft" + AnimatedControl="{Binding ElementName=PanList, Mode=OneWay}"> @@ -9,9 +10,11 @@ - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageLogLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageLogLeft.xaml.cs new file mode 100644 index 000000000..43d820aca --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLogLeft.xaml.cs @@ -0,0 +1,229 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; + +namespace PCL; + +public partial class PageLogLeft +{ + public ModWatcher.Watcher CurrentLog; + public int CurrentUuid; + public Dictionary FlowDocuments = new(); + public int IsLoading; + public List> ShownLogs = new(); + + public PageLogLeft() + { + InitializeComponent(); + Loaded += PageLogLeft_Loaded; + Unloaded += PageLogLeft_Unloaded; + } + + private void PageLogLeft_Loaded(object sender, RoutedEventArgs e) + { + Reload(); + ModMain.FrmMain.BtnExtraLog.ShowRefresh(); + } + + private void PageLogLeft_Unloaded(object sender, RoutedEventArgs e) + { + ModMain.FrmMain.BtnExtraLog.ShowRefresh(); + } + + private void Reload() + { + try + { + if (ShownLogs.Count == 0) + { + ModMain.FrmMain.PageChange((FormMain.PageType)ModMain.FrmMain.PageCurrentSub); + return; + } + + IsLoading += 1; + + // 创建 UI + ModMain.FrmLogLeft.PanList.Children.Clear(); + + // 测试实例列表 + // TODO(i18n): 文本 @ PageLog 左侧 - 列表标题 + ModMain.FrmLogLeft.PanList.Children.Add(new TextBlock + { Text = "测试实例列表", Margin = new Thickness(13d, 18d, 5d, 4d), Opacity = 0.6d, FontSize = 12d }); + foreach (var item in ShownLogs) + { + // 添加控件 + var Uuid = item.Key; + var Version = item.Value.Version; + var Proc = item.Value.GameProcess; + var NewItem = new MyListItem + { + IsScaleAnimationEnabled = false, Type = MyListItem.CheckType.RadioBox, MinPaddingRight = 30, + Title = Version.Name, Info = $"{Version.Info} - {Proc.StartTime:HH:mm:ss}", Height = 40d, Tag = Uuid + }; + NewItem.Changed += ModMain.FrmLogLeft.Version_Change; + // Dim KillButton As New MyIconButton With {.Logo = Logo.IconButtonCross, .LogoScale = 0.85} + var RemoveButton = new MyIconButton { Logo = ModBase.Logo.IconButtonDelete, LogoScale = 1.1d }; + // AddHandler KillButton.Click, AddressOf FrmLogLeft.Kill_Click + RemoveButton.Click += (a, b) => ModMain.FrmLogLeft.Remove_Click(a, (RoutedEventArgs)b); + NewItem.Buttons = new[] { RemoveButton }; + if (Uuid == CurrentUuid) + NewItem.Checked = true; + ModMain.FrmLogLeft.PanList.Children.Add(NewItem); + } + + // 通知日志保留设置 + // TODO(i18n): 文本 @ PageLog 左侧 - 日志保留设置通知 + if (!States.Hint.MaxGameLog) + { + States.Hint.MaxGameLog = true; + ModMain.Hint("实时日志默认只保留 500 行,你可以在 实时日志行数 设置中修改!"); + } + + IsLoading -= 1; + } + catch (Exception ex) + { + ModBase.Log(ex, "构建游戏实时日志 UI 出错", ModBase.LogLevel.Feedback); + } + } + + private void OnLogOutput(ModWatcher.Watcher sender, ModWatcher.LogOutputEventArgs e) + { + foreach (var Item in ShownLogs) + if (Item.Value.GameProcess.Id == sender.GameProcess.Id) + { + var Uuid = Item.Key; + Thickness Margin; + if (Item.Value.GameProcess.HasExited) + Margin = new Thickness(0d, 12d, 0d, 0d); + else + Margin = new Thickness(0d); + ModBase.RunInUi(() => + { + var Paragraph = new Paragraph(new Run(e.LogText)) { Foreground = e.Color, Margin = Margin }; + FlowDocuments[Uuid].Blocks.Add(Paragraph); + var MaxLog = Conversions.ToULong(Config.System.MaxGameLog); + switch (MaxLog) + { + case var @case when @case <= 5UL: + { + MaxLog = (ulong)Math.Round(MaxLog * 10m + 50m); + break; + } + + case var case1 when case1 <= 13UL: + { + MaxLog = (ulong)Math.Round(MaxLog * 50m - 150m); + break; + } + + case var case2 when case2 <= 28UL: + { + MaxLog = (ulong)Math.Round(MaxLog * 100m - 800m); + break; + } + + default: + { + MaxLog = 18446744073709551615UL; + break; + } + } + + while (FlowDocuments[Uuid].Blocks.Count > (decimal)MaxLog) + FlowDocuments[Uuid].Blocks.Remove(FlowDocuments[Uuid].Blocks.FirstBlock); + }); + return; + } + } + + public void Add(ModWatcher.Watcher watcher) + { + var uuid = ModBase.GetUuid(); + ShownLogs.Add(new KeyValuePair(uuid, watcher)); + watcher.LogOutput += OnLogOutput; + ModBase.RunInUi(() => FlowDocuments.Add(uuid, new FlowDocument())); // TODO:在 UI 线程创建 + SelectionChange(uuid); + ModMain.FrmMain.BtnExtraLog.ShowRefresh(); + } + + public void SelectionChange(int Uuid) + { + if (IsLoading > 0) + return; + // If CurrentUuid > 0 Then FlowDocuments(CurrentUuid) = FrmLogRight.PanLog.Document + if (Uuid <= 0) + { + CurrentUuid = -1; + CurrentLog = null; + } + else + { + foreach (var item in ShownLogs) + if (item.Key == Uuid) + { + CurrentUuid = Uuid; + CurrentLog = item.Value; + break; + } + } + + ModBase.RunInUi(() => + { + ModMain.FrmLogRight.Reload(); + Reload(); + }); + } + + public void RemoveItem(int Uuid) + { + for (int i = 0, loopTo = ShownLogs.Count - 1; i <= loopTo; i++) + { + var item = ShownLogs[i]; + if (item.Key != Uuid) + continue; + ShownLogs.RemoveAt(i); + if (CurrentUuid == item.Key) + { + if (ShownLogs.Count == 0) + // 没有可以显示的了 + SelectionChange(-1); + else + SelectionChange(ShownLogs[new[] { new[] { i, ShownLogs.Count - 1 }.Min(), 0 }.Max()].Key); + } + else + { + ModBase.RunInUi(() => + { + ModMain.FrmLogRight.Reload(); + Reload(); + }); + } + + break; + } + + ModMain.FrmMain.BtnExtraLog.ShowRefresh(); + } + + // Public Sub Kill_Click(sender As Object, e As RoutedEventArgs) + // Dim Uuid As Integer = (CType(CType(sender, MyIconButton).Parent, MyListItem).Tag) + // For Each item In ShownLogs + // If item.Key = Uuid Then + // item.Value.proc.Kill() + // End If + // Next + // End Sub + public void Remove_Click(object sender, RoutedEventArgs e) + { + RemoveItem(Conversions.ToInteger(((MyListItem)((MyIconButton)sender).Parent).Tag)); + } + + // 点击选项 + public void Version_Change(object sender, ModBase.RouteEventArgs e) + { + SelectionChange(Conversions.ToInteger(((MyListItem)sender).Tag)); + } +} diff --git a/Plain Craft Launcher 2/Pages/PageLogRight.xaml b/Plain Craft Launcher 2/Pages/PageLogRight.xaml index 8d8177489..f1d9b0209 100644 --- a/Plain Craft Launcher 2/Pages/PageLogRight.xaml +++ b/Plain Craft Launcher 2/Pages/PageLogRight.xaml @@ -1,31 +1,32 @@  + xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" x:Class="PCL.PageLogRight"> - + - + VerticalScrollBarVisibility="Auto" + HorizontalScrollBarVisibility="Disabled" + x:Name="PanBack"> + - + + HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,15,0,10" UseAnimation="False" + HasMouseAnimation="True"> - + - - - - - - - + + + + + + + + Click="BtnOperationExportStackDump_Click" + LogoScale="0.85" + Logo="M913.066667 264.533333l-371.2-209.066666c-25.6-12.8-59.733333-12.8-85.333334 0L89.6 264.533333C34.133333 298.666667 34.133333 379.733333 89.6 413.866667l371.2 209.066666c25.6 12.8 59.733333 12.8 85.333333 0l371.2-209.066666c55.466667-34.133333 55.466667-119.466667-4.266666-149.333334z m-413.866667 281.6L132.266667 337.066667 499.2 128l371.2 209.066667-371.2 209.066666z M46.933333 516.266667c12.8-21.333333 38.4-25.6 59.733334-17.066667l384 221.866667c12.8 8.533333 29.866667 8.533333 42.666666 0l388.266667-217.6c21.333333-12.8 46.933333-4.266667 59.733333 17.066666 12.8 21.333333 4.266667 46.933333-17.066666 59.733334l-388.266667 217.6c-38.4 21.333333-89.6 21.333333-128 0l-384-221.866667c-21.333333-12.8-25.6-38.4-17.066667-59.733333z M106.666667 669.866667c-21.333333-12.8-46.933333-4.266667-59.733334 17.066666-12.8 21.333333-4.266667 46.933333 17.066667 59.733334l388.266667 217.6c38.4 21.333333 85.333333 21.333333 128 0l379.733333-217.6c21.333333-12.8 25.6-38.4 17.066667-59.733334-12.8-21.333333-38.4-25.6-59.733334-17.066666l-379.733333 217.6c-12.8 8.533333-29.866667 8.533333-42.666667 0l-388.266666-217.6z" /> - - + + - + diff --git a/Plain Craft Launcher 2/Pages/PageLogRight.xaml.cs b/Plain Craft Launcher 2/Pages/PageLogRight.xaml.cs new file mode 100644 index 000000000..441a571f6 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageLogRight.xaml.cs @@ -0,0 +1,210 @@ +using System.IO; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageLogRight +{ + public Run LabDebug; + public Run LabError; + public Run LabFatal; + public Run LabInfo; + public Run LabWarn; + + public PageLogRight() + { + Initialized += (_, __) => Init(); + Loaded += PageLogRight_Loaded; + InitializeComponent(); + } + + public void Init() + { + PanLogCard.Inlines.Clear(); + // TODO(i18n): 文本 @ 标题栏 - 实时日志卡片标题 + PanLogCard.Inlines.Add(new Run("实时日志")); + PanLogCard.Inlines.Add(new Run(" | ")); + LabDebug = new Run("0 Debug") + { Foreground = (Brush)System.Windows.Application.Current.Resources["ColorBrushDebug"] }; + PanLogCard.Inlines.Add(LabDebug); + PanLogCard.Inlines.Add(new Run(" | ")); + LabInfo = new Run("0 Info") + { + Foreground = + (Brush)System.Windows.Application.Current.Resources[ + ModSecret.IsDarkMode ? "ColorBrushInfoDark" : "ColorBrushInfo"] + }; + PanLogCard.Inlines.Add(LabInfo); + PanLogCard.Inlines.Add(new Run(" | ")); + LabWarn = new Run("0 Warn") + { Foreground = (Brush)System.Windows.Application.Current.Resources["ColorBrushWarn"] }; + PanLogCard.Inlines.Add(LabWarn); + PanLogCard.Inlines.Add(new Run(" | ")); + LabError = new Run("0 Error") + { Foreground = (Brush)System.Windows.Application.Current.Resources["ColorBrushError"] }; + PanLogCard.Inlines.Add(LabError); + PanLogCard.Inlines.Add(new Run(" | ")); + LabFatal = new Run("0 Fatal") + { Foreground = (Brush)System.Windows.Application.Current.Resources["ColorBrushFatal"] }; + PanLogCard.Inlines.Add(LabFatal); + } + + private void PageLogRight_Loaded(object sender, RoutedEventArgs e) + { + ModAnimation.AniControlEnabled += 1; + Reload(); + ModAnimation.AniControlEnabled -= 1; + } + + public void Reload() + { + // 初始化 + if (ModMain.FrmLogLeft.CurrentLog is null || ModMain.FrmLogLeft.CurrentUuid <= 0 || + ModMain.FrmLogLeft.ShownLogs.Count == 0) + { + ModMain.FrmMain.PageChange(ModMain.FrmMain.PageCurrent); + return; + } + + PanAllBack.Visibility = Visibility.Visible; + CardOperation.Visibility = Visibility.Visible; + BtnOperationKill.IsEnabled = !ModMain.FrmLogLeft.CurrentLog.GameProcess.HasExited; + BtnOperationExportStackDump.IsEnabled = !ModMain.FrmLogLeft.CurrentLog.GameProcess.HasExited & + !string.IsNullOrWhiteSpace(ModMain.FrmLogLeft.CurrentLog.JStackPath); + SliderMaxLog.Value = Conversions.ToInteger(Config.System.MaxGameLog); + // y = 10x + 50 (0 <= x <= 5, 50 <= y <= 100) + // y = 50x - 150 (5 < x <= 13, 100 < y <= 500) + // y = 100x - 800 (13 < x <= 28, 500 < y <= 2000) + SliderMaxLog.GetHintText = new Func(v => + { + switch (v) + { + case var @case when Operators.ConditionalCompareObjectLessEqual(@case, 5, false): + { + return Operators.AddObject(Operators.MultiplyObject(v, 10), 50); + } + case var case1 when Operators.ConditionalCompareObjectLessEqual(case1, 13, false): + { + return Operators.SubtractObject(Operators.MultiplyObject(v, 50), 150); + } + case var case2 when Operators.ConditionalCompareObjectLessEqual(case2, 28, false): + { + return Operators.SubtractObject(Operators.MultiplyObject(v, 100), 800); + } + default: + { + return "无限制"; + } + } + }); + // 绑定日志输出 + PanLog.Document = ModMain.FrmLogLeft.FlowDocuments[ModMain.FrmLogLeft.CurrentUuid]; + // 绑定事件 + ModMain.FrmLogLeft.CurrentLog.LogOutput += OnLogOutput; + ModMain.FrmLogLeft.CurrentLog.GameExit += OnGameExit; + RefreshLabText(); + } + + private void RefreshLabText() + { + // 刷新计数器 + + LabFatal.Text = $"{ModMain.FrmLogLeft.CurrentLog.CountFatal} Fatal"; + LabError.Text = $"{ModMain.FrmLogLeft.CurrentLog.CountError} Error"; + LabWarn.Text = $"{ModMain.FrmLogLeft.CurrentLog.CountWarn} Warn"; + LabInfo.Text = $"{ModMain.FrmLogLeft.CurrentLog.CountInfo} Info"; + LabDebug.Text = $"{ModMain.FrmLogLeft.CurrentLog.CountDebug} Debug"; + } + + private void OnLogOutput(ModWatcher.Watcher sender, ModWatcher.LogOutputEventArgs e) + { + ModBase.RunInUi(() => + { + if (ModMain.FrmLogLeft.CurrentLog is not null) + { + if (CheckAutoScroll.Checked == true) PanBack.ScrollToBottom(); + RefreshLabText(); + } + }); + } + + #region 滑动条 + + private void SliderMaxLog_ValueChanged(object o, bool user) + { + var sender = (MySlider)o; + ModBase.Setup.Set(sender.Tag.ToString(), sender.Value); + if (ModMain.FrmSetupLauncherMisc is null) + return; + ModMain.FrmSetupLauncherMisc.SliderMaxLog.Value = sender.Value; + } + + #endregion + + #region 卡片按钮 + + private void BtnOperationClear_Click(object sender, ModBase.RouteEventArgs e) + { + ModMain.FrmLogLeft.FlowDocuments[ModMain.FrmLogLeft.CurrentUuid].Blocks.Clear(); + } + + private void BtnOperationExport_Click(object sender, ModBase.RouteEventArgs e) + { + // TODO(i18n): 文本 @ 文件选择弹窗 - 窗口标题 & 类型选择器选项 + var SavePath = SystemDialogs.SelectSaveFile("选择导出位置", + $"游戏日志 - {ModMain.FrmLogLeft.CurrentLog.Version.Name}.log", "游戏日志(*.log)|*.log"); + if (SavePath.Length < 3) + return; + File.WriteAllLines(SavePath, ModMain.FrmLogLeft.CurrentLog.FullLog); + // TODO(i18n): 文本 @ 左下角提示 - 导出成功提示 + ModMain.Hint("日志已导出!", ModMain.HintType.Finish); + ModBase.OpenExplorer(SavePath); + } + + private void BtnOperationKill_Click(object sender, ModBase.RouteEventArgs e) + { + if (ModMain.FrmLogLeft.CurrentLog.State <= ModWatcher.Watcher.MinecraftState.Running) + { + ModMain.FrmLogLeft.CurrentLog.Kill(); + // TODO(i18n): 文本 @ 左下角提示 - 客户端关闭提示 + ModMain.Hint($"已关闭游戏 {ModMain.FrmLogLeft.CurrentLog.Version.Name}!", ModMain.HintType.Finish); + } + } + + private void BtnOperationExportStackDump_Click(object sender, ModBase.RouteEventArgs e) + { + var SavePath = SystemDialogs.SelectSaveFile("选择导出位置", + $"游戏运行栈 - {DateTime.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_")}.log", + "游戏运行栈(*.log)|*.log"); + if (SavePath.Length < 3) + return; + // TODO(i18n): 文本 @ 左下角提示 - 导出运行栈提示 + ModMain.Hint("正在导出运行栈,请稍等(可能需要 15 秒 ~ 1 分钟)"); + BtnOperationExportStackDump.IsEnabled = false; + ModBase.RunInNewThread(() => + { + var Dump = ModMain.FrmLogLeft.CurrentLog.ExportStackDump(SavePath); + File.WriteAllLines(SavePath, Dump); + ModBase.RunInUi(() => + { + // TODO(i18n): 文本 @ 左下角提示 - 导出运行栈提示 + ModMain.Hint("运行栈已导出!", ModMain.HintType.Finish); + BtnOperationExportStackDump.IsEnabled = true; + }); + ModBase.OpenExplorer(SavePath); + }); + } + + private void OnGameExit() + { + ModBase.RunInUi(() => BtnOperationKill.IsEnabled = false); + ModBase.RunInUi(() => BtnOperationExportStackDump.IsEnabled = false); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml b/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml index 2dfe90320..80087eb69 100644 --- a/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml @@ -1,7 +1,8 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PCL;assembly=" + x:Class="PCL.PageSelectLeft" + AnimatedControl="{Binding ElementName=PanList, Mode=OneWay}"> @@ -9,9 +10,11 @@ - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.cs new file mode 100644 index 000000000..a4949d043 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.cs @@ -0,0 +1,874 @@ +using System.Collections.ObjectModel; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using Microsoft.VisualBasic; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageSelectLeft : IRefreshable +{ + private bool IsFirstLoad = true; + private List McFolderListLast; + + public PageSelectLeft() + { + Initialized += PageSelectLeft_Initialized; + Loaded += PageSelectLeft_Loaded; + InitializeComponent(); + } + + void IRefreshable.Refresh() + { + RefreshCurrent(); + } + + private void PageSelectLeft_Initialized(object sender, EventArgs e) + { + ModMinecraft.McFolderListLoader.PreviewFinish += _ => + { + if (ModMain.FrmSelectLeft is not null) ModBase.RunInUiWait(McFolderListUI); + }; + } + + private void PageSelectLeft_Loaded(object sender, RoutedEventArgs e) + { + if (IsFirstLoad) + McFolderListUI(); // 若已经执行完成,触发首次加载 + IsFirstLoad = false; + } + + private void McFolderListUI() + { + try + { + // 确认数据有变化 + if (McFolderListLast != null && McFolderListLast.SequenceEqual(ModMinecraft.McFolderList)) + return; + + McFolderListLast = new List(ModMinecraft.McFolderList); + + // 创建 UI + ModMain.FrmSelectLeft.PanList.Children.Clear(); + + // 文件夹列表标题 + ModMain.FrmSelectLeft.PanList.Children.Add(new TextBlock + { + Text = "文件夹列表", + Margin = new Thickness(13, 18, 5, 4), + Opacity = 0.6, + FontSize = 12 + }); + + for (var i = 0; i < ModMinecraft.McFolderList.Count; i++) + { + var folder = ModMinecraft.McFolderList[i]; + + // 创建 ContextMenu + var contMenu = new ContextMenu(); + + // 添加菜单项 + void AddMenuItem(string name, string header, string icon = null, Thickness? padding = null, + RoutedEventHandler clickHandler = null) + { + var item = new MyMenuItem + { + Name = name, + Header = header, + Icon = icon, + Padding = padding ?? new Thickness(0) + }; + if (clickHandler != null) + item.Click += clickHandler; + contMenu.Items.Add(item); + } + + const string ICON_RENAME = + "F1 M 53.2929,21.2929L 54.7071,22.7071C 56.4645,24.4645 56.4645,27.3137 54.7071,29.0711L 52.2323,31.5459L 44.4541,23.7677L 46.9289,21.2929C 48.6863,19.5355 51.5355,19.5355 53.2929,21.2929 Z M 31.7262,52.052L 23.948,44.2738L 43.0399,25.182L 50.818,32.9601L 31.7262,52.052 Z M 23.2409,47.1023L 28.8977,52.7591L 21.0463,54.9537L 23.2409,47.1023 Z"; + const string ICON_MOVEUP = + "M104.704 685.248a64 64 0 0 0 90.496 0L512 368.448l316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496z"; + const string ICON_MOVEDOWN = + "M104.704 338.752a64 64 0 0 1 90.496 0L512 655.552l316.8-316.8a64 64 0 0 1 90.496 90.496l-362.048 362.048a64 64 0 0 1-90.496 0L104.704 429.248a64 64 0 0 1 0-90.496z"; + const string ICON_OPEN = + "F1 M 19,50L 28,34L 63,34L 54,50L 19,50 Z M 19,28.0001L 35,28C 36,25 37.4999,24.0001 37.4999,24.0001L 48.75,24C 49.3023,24 50,24.6977 50,25.25L 50,28L 54,28.0001L 54,32L 27,32L 19,46.4L 19,28.0001 Z"; + const string ICON_REFRESH = + "F1 M 38,20.5833C 42.9908,20.5833 47.4912,22.6825 50.6667,26.046L 50.6667,17.4167L 55.4166,22.1667L 55.4167,34.8333L 42.75,34.8333L 38,30.0833L 46.8512,30.0833C 44.6768,27.6539 41.517,26.125 38,26.125C 31.9785,26.125 27.0037,30.6068 26.2296,36.4167L 20.6543,36.4167C 21.4543,27.5397 28.9148,20.5833 38,20.5833 Z M 38,49.875C 44.0215,49.875 48.9963,45.3932 49.7703,39.5833L 55.3457,39.5833C 54.5457,48.4603 47.0852,55.4167 38,55.4167C 33.0092,55.4167 28.5088,53.3175 25.3333,49.954L 25.3333,58.5833L 20.5833,53.8333L 20.5833,41.1667L 33.25,41.1667L 38,45.9167L 29.1487,45.9167C 31.3231,48.3461 34.483,49.875 38,49.875 Z"; + const string ICON_DELETE = + "F1 M 26.9166,22.1667L 37.9999,33.25L 49.0832,22.1668L 53.8332,26.9168L 42.7499,38L 53.8332,49.0834L 49.0833,53.8334L 37.9999,42.75L 26.9166,53.8334L 22.1666,49.0833L 33.25,38L 22.1667,26.9167L 26.9166,22.1667 Z"; + + switch (folder.Type) + { + case ModMinecraft.McFolder.Types.Original: + AddMenuItem("Rename", "重命名", ICON_RENAME, new Thickness(0, 2, 0, 0), + ModMain.FrmSelectLeft.Rename_Click); + AddMenuItem("MoveUp", "上移", ICON_MOVEUP, null, ModMain.FrmSelectLeft.MoveUp_Click); + AddMenuItem("MoveDown", "下移", ICON_MOVEDOWN, null, ModMain.FrmSelectLeft.MoveDown_Click); + AddMenuItem("Open", "打开", ICON_OPEN, null, ModMain.FrmSelectLeft.Open_Click); + AddMenuItem("Refresh", "刷新", ICON_REFRESH, null, ModMain.FrmSelectLeft.Refresh_Click); + AddMenuItem("Delete", + ModMinecraft.McFolderList.Count == 1 && folder.Location == ModBase.ExePath + ".minecraft\\" + ? "清空" + : "删除", ICON_DELETE, new Thickness(0, 0, 0, 2), ModMain.FrmSelectLeft.Delete_Click); + break; + + case ModMinecraft.McFolder.Types.RenamedOriginal: + AddMenuItem("Restore", "复原名称", ICON_RENAME, new Thickness(0, 2, 0, 0), + ModMain.FrmSelectLeft.Restore_Click); + AddMenuItem("Rename", "重命名", ICON_RENAME, null, ModMain.FrmSelectLeft.Rename_Click); + AddMenuItem("MoveUp", "上移", ICON_MOVEUP, null, ModMain.FrmSelectLeft.MoveUp_Click); + AddMenuItem("MoveDown", "下移", ICON_MOVEDOWN, null, ModMain.FrmSelectLeft.MoveDown_Click); + AddMenuItem("Open", "打开", ICON_OPEN, null, ModMain.FrmSelectLeft.Open_Click); + AddMenuItem("Refresh", "刷新", ICON_REFRESH, null, ModMain.FrmSelectLeft.Refresh_Click); + AddMenuItem("Delete", "删除", ICON_DELETE, new Thickness(0, 0, 0, 2), + ModMain.FrmSelectLeft.Delete_Click); + break; + + case ModMinecraft.McFolder.Types.Custom: + AddMenuItem("Rename", "重命名", ICON_RENAME, new Thickness(0, 2, 0, 0), + ModMain.FrmSelectLeft.Rename_Click); + AddMenuItem("MoveUp", "上移", ICON_MOVEUP, null, ModMain.FrmSelectLeft.MoveUp_Click); + AddMenuItem("MoveDown", "下移", ICON_MOVEDOWN, null, ModMain.FrmSelectLeft.MoveDown_Click); + AddMenuItem("Open", "打开", ICON_OPEN, null, ModMain.FrmSelectLeft.Open_Click); + AddMenuItem("Refresh", "刷新", ICON_REFRESH, null, ModMain.FrmSelectLeft.Refresh_Click); + AddMenuItem("Remove", "移出列表", + "F1 M 23.3428,25.205L 23.3805,25.4461C 23.9229,27.177 30.261,29.0992 38,29.0992C 45.7386,29.0992 52.0765,27.1771 52.6194,25.4463L 52.6571,25.205C 52.6571,23.3616 46.0949,21.3109 38,21.3109C 29.9051,21.3109 23.3428,23.3616 23.3428,25.205 Z M 23.3428,53.0204L 19.1571,26.2111C 19.0534,25.8817 19,25.5459 19,25.205C 19,20.9036 27.5066,17.4167 38,17.4167C 48.4934,17.4167 57,20.9036 57,25.205C 57,25.5459 56.9466,25.8818 56.8429,26.2112L 52.6571,53.0204L 52.5974,53.0204C 51.9241,56.1393 45.6457,58.5833 38,58.5833C 30.3543,58.5833 24.076,56.1393 23.4026,53.0204L 23.3428,53.0204 Z M 51.8228,30.5485C 48.3585,32.0537 43.4469,32.9933 38,32.9933C 32.5531,32.9933 27.6415,32.0537 24.1771,30.5484L 27.5988,52.464L 27.6857,52.464C 27.6857,53.3857 32.3036,54.6892 38,54.6892C 43.6964,54.6892 48.3143,53.3857 48.3143,52.464L 48.4011,52.464L 51.8228,30.5485 Z ", + null, ModMain.FrmSelectLeft.Remove_Click); + AddMenuItem("Delete", "删除", ICON_DELETE, new Thickness(0, 0, 0, 2), + ModMain.FrmSelectLeft.Delete_Click); + break; + } + + // 控制上移下移显示 + var moveUpItem = contMenu.Items.OfType().FirstOrDefault(x => x.Name == "MoveUp"); + var moveDownItem = contMenu.Items.OfType().FirstOrDefault(x => x.Name == "MoveDown"); + + // 如果是第一个项目,隐藏上移按钮 + if (i == 0) moveUpItem.Visibility = Visibility.Collapsed; + + // 如果是最后一个项目,隐藏下移按钮 + if (i == ModMinecraft.McFolderList.Count - 1) moveDownItem.Visibility = Visibility.Collapsed; + + // 构建列表项 + var newItem = new MyListItem + { + IsScaleAnimationEnabled = false, + Type = MyListItem.CheckType.RadioBox, + MinPaddingRight = 30, + Title = folder.Name, + Info = folder.Location, + Height = 40, + ContextMenu = contMenu, + Tag = folder + }; + + newItem.Changed += (a, b) => ModMain.FrmSelectLeft.Folder_Change((MyListItem)a, b); + + // 拖拽 + newItem.AllowDrop = true; + newItem.MouseMove += ModMain.FrmSelectLeft.Item_MouseMove; + newItem.DragEnter += ModMain.FrmSelectLeft.Item_DragEnter; + newItem.DragOver += ModMain.FrmSelectLeft.Item_DragOver; + newItem.DragLeave += ModMain.FrmSelectLeft.Item_DragLeave; + newItem.Drop += ModMain.FrmSelectLeft.Item_Drop; + + // 图标按钮 + var newIconButton = new MyIconButton + { + Logo = ModBase.Logo.IconButtonSetup, + LogoScale = 1.1 + }; + newIconButton.Click += (_, _) => + { + contMenu.PlacementTarget = newItem; + contMenu.IsOpen = true; + }; + newItem.Buttons = new[] { newIconButton }; + + ModMain.FrmSelectLeft.PanList.Children.Add(newItem); + + LogWrapper.Info("[Minecraft] 有效的 Minecraft 文件夹:" + folder.Name + " > " + folder.Location); + } + + // 标题文本 + ModMain.FrmSelectLeft.PanList.Children.Add(new TextBlock + { + Text = "添加或导入", + Margin = new Thickness(13, 18, 5, 4), + Opacity = 0.6, + FontSize = 12 + }); + + // 创建新文件夹按钮 + if (!Directory.Exists(ModBase.ExePath + ".minecraft\\")) + { + var itemCreate = new MyListItem + { + IsScaleAnimationEnabled = false, + Type = MyListItem.CheckType.Clickable, + Title = "新建 .minecraft 文件夹", + Height = 34, + ToolTip = "在 PCL 当前所在文件夹下创建新的 .minecraft 文件夹", + LogoScale = 0.9, + Logo = ModBase.Logo.IconButtonCreate + }; + ToolTipService.SetPlacement(itemCreate, PlacementMode.Right); + ToolTipService.SetHorizontalOffset(itemCreate, -50); + ToolTipService.SetVerticalOffset(itemCreate, 2.5); + itemCreate.Click += (_, _) => ModMain.FrmSelectLeft.Create_Click(); + ModMain.FrmSelectLeft.PanList.Children.Add(itemCreate); + } + + // 添加按钮 + var itemAdd = new MyListItem + { + IsScaleAnimationEnabled = false, + Type = MyListItem.CheckType.Clickable, + Title = "添加已有文件夹", + Height = 34, + ToolTip = "将一个已有的 Minecraft 文件夹添加到列表", + Logo = ModBase.Logo.IconButtonAdd + }; + ToolTipService.SetPlacement(itemAdd, PlacementMode.Right); + ToolTipService.SetHorizontalOffset(itemAdd, -50); + ToolTipService.SetVerticalOffset(itemAdd, 2.5); + itemAdd.Click += (_, _) => ModMain.FrmSelectLeft.Add_Click(); + ModMain.FrmSelectLeft.PanList.Children.Add(itemAdd); + + // 导入整合包 + var itemInstall = new MyListItem + { + IsScaleAnimationEnabled = false, + Type = MyListItem.CheckType.Clickable, + Title = "导入整合包", + Height = 34, + ToolTip = "在当前选择的 Minecraft 文件夹下安装整合包", + Logo = + "F1 m 11.293 11.293 l -3 3 a 1 1 0 0 0 0 1.41406 a 1 1 0 0 0 1.41406 0 L 12 13.4141 l 2.29297 2.29297 a 1 1 0 0 0 1.41406 0 a 1 1 0 0 0 0 -1.41406 l -3 -3 a 1.0001 1.0001 0 0 0 -1.41406 0 z M 12 11 a 1 1 0 0 0 -1 1 v 6 a 1 1 0 0 0 1 1 a 1 1 0 0 0 1 -1 V 12 A 1 1 0 0 0 12 11 Z M 14 1 a 1 1 0 0 0 -1 1 v 5 c 0 1.09272 0.907275 2 2 2 h 5 A 1 1 0 0 0 21 8 A 1 1 0 0 0 20 7 H 15 V 2 A 1 1 0 0 0 14 1 Z M 6 1 C 4.35499 1 3 2.35499 3 4 v 16 c 0 1.64501 1.35499 3 3 3 h 12 c 1.64501 0 3 -1.35499 3 -3 V 8.00195 V 8 C 21.001 7.09394 20.6387 6.22279 19.9961 5.58398 L 16.4121 2 L 16.4101 1.99805 C 15.7718 1.35838 14.9038 0.999054 14 1 Z m 0 2 h 8 a 1.0001 1.0001 0 0 0 0.002 0 c 0.373356 -0.0006051 0.730614 0.147632 0.994141 0.412109 a 1.0001 1.0001 0 0 0 0 0.00195 l 3.58789 3.58789 a 1.0001 1.0001 0 0 0 0.0039 0.00195 C 18.8531 7.26753 19.0006 7.62412 19 7.99805 A 1.0001 1.0001 0 0 0 19 8 v 12 c 0 0.564129 -0.435871 1 -1 1 H 6 C 5.43587 21 5 20.5641 5 20 V 4 C 5 3.43587 5.43587 3 6 3 Z" + }; + ToolTipService.SetPlacement(itemInstall, PlacementMode.Right); + ToolTipService.SetHorizontalOffset(itemInstall, -50); + ToolTipService.SetVerticalOffset(itemInstall, 2.5); + itemInstall.Click += (_, _) => ModModpack.ModpackInstall(); + ModMain.FrmSelectLeft.PanList.Children.Add(itemInstall); + + // 边距 + ModMain.FrmSelectLeft.PanList.Children.Add(new FrameworkElement { Height = 10, IsHitTestVisible = false }); + + // 确认勾选状态 + for (var i = 0; i < ModMinecraft.McFolderList.Count; i++) + if (ModMinecraft.McFolderList[i].Location == ModMinecraft.McFolderSelected) + { + ((MyListItem)ModMain.FrmSelectLeft.PanList.Children[i + 1]).Checked = true; //去掉第一个标题 + return; + } + + if (!ModMinecraft.McFolderList.Any()) + throw new ArgumentNullException("没有可用的 Minecraft 文件夹"); + States.Game.SelectedFolder = ModMinecraft.McFolderList[0].Location.Replace(ModBase.ExePath, "$"); + ((MyListItem)ModMain.FrmSelectLeft.PanList.Children[1]).Checked = true; + } + catch (Exception ex) + { + LogWrapper.Error(ex, "构建 Minecraft 文件夹列表 UI 出错"); + } + finally + { + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, + ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.RunOnUpdated, + 1, + "versions\\"); + } + } + + private void MoveUp_Click(object sender, RoutedEventArgs e) + { + var folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent).PlacementTarget) + .Tag; + var index = ModMinecraft.McFolderList.IndexOf(folder); + if (index > 0) + { + ModMinecraft.McFolderList.RemoveAt(index); + ModMinecraft.McFolderList.Insert(index - 1, folder); + UpdateFolderOrder(); + } + } + + private void MoveDown_Click(object sender, RoutedEventArgs e) + { + var folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent).PlacementTarget) + .Tag; + var index = ModMinecraft.McFolderList.IndexOf(folder); + if (index < ModMinecraft.McFolderList.Count - 1) + { + ModMinecraft.McFolderList.RemoveAt(index); + ModMinecraft.McFolderList.Insert(index + 1, folder); + UpdateFolderOrder(); + } + } + + private void UpdateFolderOrder() + { + var folders = new List(); + foreach (var folder in ModMinecraft.McFolderList) + folders.Add(folder.Name + ">" + folder.Location); + States.Game.Folders = folders.ToArray().Join("|"); + McFolderListUI(); + } + + private void Restore_Click(object sender, RoutedEventArgs e) + { + var folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyListItem)sender).Parent).Parent).PlacementTarget) + .Tag; + var index = ModMinecraft.McFolderList.IndexOf(folder); + ModMinecraft.McFolderList[index].Type = ModMinecraft.McFolder.Types.Original; + ModMinecraft.McFolderList[index].Name = "官方启动器文件夹"; + UpdateFolderOrder(); + } + + // 添加文件夹 + private void Add_Click() + { + var NewFolder = ""; + // 检查是否有下载任务 + if (ModNet.HasDownloadingTask()) + { + ModMain.Hint("在下载任务进行时,无法添加游戏文件夹!", ModMain.HintType.Critical); + return; + } + + try + { + // 获取输入 + NewFolder = SystemDialogs.SelectFolder(); + if (string.IsNullOrEmpty(NewFolder)) + return; + if (NewFolder.Contains("!") || NewFolder.Contains(";")) + { + ModMain.Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", ModMain.HintType.Critical); + return; + } + + // 要求输入显示名称 + var SplitedNames = NewFolder.TrimEnd('\\').Split(@"\"); + var DefaultName = SplitedNames.Last() == ".minecraft" + ? SplitedNames.Count() >= 3 ? SplitedNames[SplitedNames.Count() - 2] : "" + : SplitedNames.Last(); + if (DefaultName.Length > 40) + DefaultName = DefaultName.Substring(0, 39); + var NewName = ModMain.MyMsgBoxInput("输入显示名称", "输入该文件夹在左边栏列表中显示的名称。", DefaultName, + new Collection + { + new ValidateNullOrWhiteSpace(), new ValidateLength(1, 30), new ValidateExcept(new[] { ">", "|" }) + }); + if (string.IsNullOrWhiteSpace(NewName)) + return; + // 添加文件夹 + AddFolder(NewFolder, NewName, true); + } + catch (Exception ex) + { + ModBase.Log(ex, "添加文件夹失败(" + NewFolder + ")", ModBase.LogLevel.Feedback); + } + } + + /// + /// 将指定文件夹添加到 Minecraft 文件夹列表,并选中它。 + /// + public static void AddFolder(string FolderPath, string DisplayName, bool ShowHint) + { + // 检查文件夹权限 + // 检查实际的 Minecraft 文件夹位置(没有问题,或是在子文件夹中) + // 判断是否已经添加过,若添加过则直接修改自定义名 + // 如果没有添加过,则添加进去 + // 保存 + // 切换选择并更新列表 + // 提示 + // 检查是否为根目录整合包,自动关闭版本隔离 + // 1. 根目录中存在数个 Mod + // 2. 实例数较少,可能为整合包 + // 3. 能够找到可安装 Mod 的实例 + // 4. 该实例的隔离文件夹下不存在 mods + // 满足以上全部条件则视为根目录整合包 + ModBase.RunInThread(() => + { + try + { + if (!FolderPath.EndsWith(@"\")) FolderPath += @"\"; + if (!ModBase.CheckPermission(FolderPath)) + { + if (ShowHint) + { + ModMain.Hint("添加文件夹失败:PCL 没有访问该文件夹的权限!", ModMain.HintType.Critical); + return; + } + + throw new Exception("PCL 没有访问文件夹的权限:" + FolderPath); + } + + if (!ModBase.CheckPermission(FolderPath + @"versions\")) + foreach (var Folder in new DirectoryInfo(FolderPath).GetDirectories()) + if (ModBase.CheckPermission(Folder.FullName + @"\versions\")) + { + FolderPath = Folder.FullName + @"\"; + break; + } + + var Folders = new List(States.Game.Folders.ToString().Split("|")); + var IsAdded = false; + var IsReplace = false; + for (int i = 0, loopTo = Folders.Count - 1; i <= loopTo; i++) + { + var Folder = Folders[i]; + if (string.IsNullOrEmpty(Folder)) continue; + if ((Folder.Split(">")[1] ?? "") == (FolderPath ?? "")) + { + IsAdded = true; + if ((Folder.Split(">")[0] ?? "") == (DisplayName ?? "")) + { + if (ShowHint) ModMain.Hint("此文件夹已在列表中!"); + return; + } + + Folders[i] = DisplayName + ">" + FolderPath; + IsReplace = true; + if (ShowHint) ModMain.Hint("文件夹名称已更新为 " + DisplayName + " !", ModMain.HintType.Finish); + break; + } + } + + if (!IsAdded) Folders.Add(DisplayName + ">" + FolderPath); + States.Game.Folders = Folders.ToArray().Join("|"); + States.Game.SelectedFolder = FolderPath.Replace(ModBase.ExePath, "$"); + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + if (IsReplace) return; + if (ShowHint) ModMain.Hint("文件夹 " + DisplayName + " 已添加!", ModMain.HintType.Finish); + var ModFolder = new DirectoryInfo(FolderPath + @"mods\"); + if (!(ModFolder.Exists && ModFolder.EnumerateFiles().Count() >= 3)) return; + var VersionFolder = new DirectoryInfo(FolderPath + @"versions\"); + if (!(VersionFolder.Exists && VersionFolder.EnumerateDirectories().Count() <= 3)) return; + foreach (var VersionPath in VersionFolder.EnumerateDirectories()) + { + var Version = new ModMinecraft.McInstance(VersionPath.FullName); + Version.Load(); + if (!Version.Modable) continue; + var ModIndieFolder = new DirectoryInfo(Version.PathInstance + @"mods\"); + if (ModIndieFolder.Exists && ModIndieFolder.EnumerateFiles().Any()) return; + Config.Instance.IndieV1[Version] = 2; + Config.Instance.IndieV2[Version] = false; + ModBase.Log("[Setup] 已自动关闭单版本隔离:" + Version.Name, ModBase.LogLevel.Debug); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "向文件夹列表中添加新文件夹失败", ModBase.LogLevel.Feedback); + } + }); // 加上斜杠…… + } + + // 创建文件夹 + public void Create_Click() + { + // 检查是否有下载任务 + if (ModNet.HasDownloadingTask()) + { + ModMain.Hint("在下载任务进行时,无法创建游戏文件夹!", ModMain.HintType.Critical); + return; + } + + if (!Directory.Exists(ModBase.ExePath + @".minecraft\")) + { + Directory.CreateDirectory(ModBase.ExePath + @".minecraft\"); + Directory.CreateDirectory(ModBase.ExePath + @".minecraft\versions\"); + States.Game.SelectedFolder = @"$.minecraft\"; + ModMinecraft.McFolderLauncherProfilesJsonCreate(ModBase.ExePath + @".minecraft\"); + ModMain.Hint("新建 .minecraft 文件夹成功!", ModMain.HintType.Finish); + } + + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + } + + // 右键菜单 + public void Remove_Click(object sender, RoutedEventArgs e) + { + try + { + var Folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent) + .PlacementTarget).Tag; + switch (ModMain.MyMsgBox( + "是否需要清理 PCL 在该文件夹中的配置文件?" + "\r\n" + "这包括各个实例的独立设置(如自定义图标、第三方登录配置)等,对游戏本身没有影响。", + "配置文件清理", "删除", "保留", "取消")) + { + case 1: + { + // 删除配置文件 + if (File.Exists(Folder.Location + "PCL.ini")) + File.Delete(Folder.Location + "PCL.ini"); + if (Directory.Exists(Folder.Location + @"versions\")) + foreach (var Version in new DirectoryInfo(Folder.Location + @"versions\") + .EnumerateDirectories()) + if (Directory.Exists(Version.FullName + @"\PCL\")) + Directory.Delete(Version.FullName + @"\PCL\", true); + + break; + } + case 2: + { + break; + } + // 不删除 + case 3: + { + // 取消 + return; + } + } + + // 若修改了本部分代码,应对应修改 Delete_Click 中的代码 + // 获取并删除列表项 + var Folders = new List(States.Game.Folders.ToString().Split("|")); + var Name = ""; + for (int i = 0, loopTo = Folders.Count - 1; i <= loopTo; i++) + { + if (string.IsNullOrEmpty(Folders[i])) + break; + if (Folders[i].EndsWith(Folder.Location)) + { + Name = Folders[i].BeforeFirst(">"); + Folders.RemoveAt(i); + break; + } + } + + // 保存 + States.Game.Folders = !Folders.Any() ? "" : Folders.ToArray().Join("|"); + ModMain.Hint(Folder.Type == ModMinecraft.McFolder.Types.Custom ? "文件夹 " + Name + " 已从列表中移除!" : "文件夹名称已复原!", + ModMain.HintType.Finish); + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + } + + catch (Exception ex) + { + ModBase.Log(ex, "从列表中移除游戏文件夹失败", ModBase.LogLevel.Feedback); + } + } + + public void Delete_Click(object sender, RoutedEventArgs e) + { + var Folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent).PlacementTarget) + .Tag; + var DeleteText = + (Folder.Type == ModMinecraft.McFolder.Types.Original || + Folder.Type == ModMinecraft.McFolder.Types.RenamedOriginal) && + (Folder.Location ?? "") == (ModBase.ExePath + @".minecraft\" ?? "") && ModMinecraft.McFolderList.Count == 1 + ? "清空" + : "删除"; + if (ModMain.MyMsgBox( + "你确定要" + DeleteText + "这个文件夹吗?" + "\r\n" + "目标文件夹:" + Folder.Location + "\r\n" + + "\r\n" + "这会导致该文件夹中的所有存档与其他文件永久丢失,且不可恢复!", "删除警告", "取消", "确认", "取消") != 2) + return; + if (ModMain.MyMsgBox( + "如果你在该文件夹中存放了除 MC 以外的其他文件,这些文件也会被一同删除!" + "\r\n" + "继续删除会导致该文件夹中的所有文件永久丢失,请在仔细确认后再继续!" + + "\r\n" + "目标文件夹:" + Folder.Location + "\r\n" + "\r\n" + "这是最后一次警告!", + "删除警告", "确认" + DeleteText, "取消", IsWarn: true) != 1) + return; + // 移出列表 + var Folders = new List(States.Game.Folders.ToString().Split("|")); + for (var i = Folders.Count - 1; i >= 0; i -= 1) + if (!string.IsNullOrEmpty(Folders[i]) && Folders[i].EndsWith(Folder.Location)) + { + Folders.RemoveAt(i); + break; + } + + States.Game.Folders = !Folders.Any() ? "" : Folders.ToArray().Join("|"); + // 删除文件夹 + // 刷新列表 + ModBase.RunInNewThread(() => + { + try + { + ModMain.Hint("正在" + DeleteText + "文件夹 " + Folder.Name + "!"); + ModBase.DeleteDirectory(Folder.Location); + if (DeleteText == "清空") Directory.CreateDirectory(Folder.Location); + ModMain.Hint("已" + DeleteText + "文件夹 " + Folder.Name + "!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, DeleteText + "文件夹 " + Folder.Name + " 失败", ModBase.LogLevel.Hint); + } + finally + { + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + } + }, "Folder Delete " + ModBase.GetUuid(), ThreadPriority.BelowNormal); + } + + public void Open_Click(object sender, RoutedEventArgs e) + { + ModBase.OpenExplorer(((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent).PlacementTarget) + .Info); + } + + public void Refresh_Click(object sender, RoutedEventArgs e) + { + var Data = (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((MyMenuItem)sender).Parent).Parent) + .PlacementTarget).Tag; + RefreshCurrent(Data.Location); + } + + public void RefreshCurrent() + { + RefreshCurrent(ModMinecraft.McFolderSelected); + } + + public static void RefreshCurrent(string Folder) + { + ModBase.WriteIni(Folder + "PCL.ini", "InstanceCache", ""); // 删除缓存以强制要求下一次加载时更新列表 + if ((Folder ?? "") == (ModMinecraft.McFolderSelected ?? "")) + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + + public void Rename_Click(object sender, RoutedEventArgs e) + { + var Folder = + (ModMinecraft.McFolder)((MyListItem)((Popup)((ContextMenu)((dynamic)sender).Parent).Parent).PlacementTarget) + .Tag; + try + { + // 获取输入 + var NewName = ModMain.MyMsgBoxInput("输入新名称", "", Folder.Name, + new Collection + { + new ValidateNullOrWhiteSpace(), new ValidateLength(1, 30), new ValidateExcept(new[] { ">", "|" }) + }); + if (string.IsNullOrWhiteSpace(NewName)) + return; + // 修改自定义名 + var Folders = new List(States.Game.Folders.ToString().Split("|")); + var IsAdded = false; + for (int i = 0, loopTo = Folders.Count - 1; i <= loopTo; i++) + { + var FolderCurrent = Folders[i]; + if (string.IsNullOrEmpty(FolderCurrent)) + continue; + if ((FolderCurrent.Split(">")[1] ?? "") == (Folder.Location ?? "")) + { + IsAdded = true; + if ((FolderCurrent.Split(">")[0] ?? "") == (NewName ?? "")) + // 名称未修改 + return; + + Folders[i] = NewName + ">" + Folder.Location; + break; + } + } + + // 如果没有添加过,则添加进去(因为修改了默认项的名称) + if (!IsAdded) + Folders.Add(NewName + ">" + Folder.Location); + ModMain.Hint("文件夹名称已更新为 " + NewName + " !", ModMain.HintType.Finish); + // 保存 + States.Game.Folders = Folders.ToArray().Join("|"); + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + } + catch (Exception ex) + { + ModBase.Log(ex, "重命名文件夹失败", ModBase.LogLevel.Feedback); + } + } + + // 点击选项 + public void Folder_Change(MyListItem sender, ModBase.RouteEventArgs e) + { + if (!e.RaiseByMouse || !sender.Checked) + return; + // 检查是否有下载任务 + if (ModNet.HasDownloadingTask(true)) + { + ModMain.Hint("在下载任务进行时,无法切换游戏文件夹!", ModMain.HintType.Critical); + e.Handled = true; + return; + } + + // 更换 + States.Game.SelectedFolder = ((ModMinecraft.McFolder)sender.Tag).Location.Replace(ModBase.ExePath, "$"); + ModMinecraft.McFolderListLoader.Start(IsForceRestart: true); + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.RunOnUpdated, 1, @"versions\"); // 刷新实例列表 + } + + #region 拖拽排序功能 + + // 拖拽开始时的鼠标移动处理 + private void Item_MouseMove(object sender, MouseEventArgs e) + { + var Item = (MyListItem)sender; + // 当按住鼠标左键时开始拖拽操作 + if (e.LeftButton == MouseButtonState.Pressed) + try + { + DragDrop.DoDragDrop(Item, Item.Tag, DragDropEffects.Move); + } + catch (Exception ex) + { + ModBase.Log(ex, "开始拖拽操作失败"); + } + } + + // 拖拽进入时的处理 + private void Item_DragEnter(object sender, DragEventArgs e) + { + try + { + if (e.Data.GetDataPresent(typeof(ModMinecraft.McFolder))) + { + e.Effects = DragDropEffects.Move; + // 添加视觉反馈 + var Item = (MyListItem)sender; + Item.Opacity = 0.7d; + } + else + { + e.Effects = DragDropEffects.None; + } + } + catch (Exception ex) + { + e.Effects = DragDropEffects.None; + } + + e.Handled = true; + } + + // 拖拽悬停时的处理 + private void Item_DragOver(object sender, DragEventArgs e) + { + try + { + if (e.Data.GetDataPresent(typeof(ModMinecraft.McFolder))) + e.Effects = DragDropEffects.Move; + else + e.Effects = DragDropEffects.None; + } + catch (Exception ex) + { + e.Effects = DragDropEffects.None; + } + + e.Handled = true; + } + + // 拖拽离开时的处理 + private void Item_DragLeave(object sender, DragEventArgs e) + { + try + { + // 恢复视觉状态 + var Item = (MyListItem)sender; + Item.Opacity = 1.0d; + } + catch (Exception ex) + { + ModBase.Log(ex, "拖拽离开处理失败"); + } + + e.Handled = true; + } + + // 拖拽放下时的处理 + private void Item_Drop(object sender, DragEventArgs e) + { + try + { + var TargetItem = (MyListItem)sender; + var TargetFolder = (ModMinecraft.McFolder)TargetItem.Tag; + + // 恢复视觉状态 + TargetItem.Opacity = 1.0d; + + // 检查数据有效性 + if (!e.Data.GetDataPresent(typeof(ModMinecraft.McFolder))) + { + e.Handled = true; + return; + } + + var SourceFolder = (ModMinecraft.McFolder)e.Data.GetData(typeof(ModMinecraft.McFolder)); + + // 检查是否为有效的拖拽操作 + if (SourceFolder is null || ReferenceEquals(SourceFolder, TargetFolder)) + { + e.Handled = true; + return; + } + + // 检查文件夹是否在列表中 + if (!ModMinecraft.McFolderList.Contains(SourceFolder) || !ModMinecraft.McFolderList.Contains(TargetFolder)) + { + e.Handled = true; + return; + } + + // 获取源文件夹和目标文件夹的索引 + var SourceIndex = ModMinecraft.McFolderList.IndexOf(SourceFolder); + var TargetIndex = ModMinecraft.McFolderList.IndexOf(TargetFolder); + + // 执行移动操作 + if (SourceIndex != TargetIndex) + { + // 先移除源文件夹 + ModMinecraft.McFolderList.RemoveAt(SourceIndex); + + // 计算新的插入位置 + int NewTargetIndex; + + if (SourceIndex < TargetIndex) + // 向下拖拽:插入到目标项目的后面 + // 由于移除了源项目,目标索引已经自动减1,所以直接使用TargetIndex就是插入到目标后面 + NewTargetIndex = TargetIndex; + else + // 向上拖拽:插入到目标项目的前面 + NewTargetIndex = TargetIndex; + + // 确保插入位置不超出列表范围 + if (NewTargetIndex > ModMinecraft.McFolderList.Count) + NewTargetIndex = ModMinecraft.McFolderList.Count; + else if (NewTargetIndex < 0) NewTargetIndex = 0; + + // 插入到新位置 + ModMinecraft.McFolderList.Insert(NewTargetIndex, SourceFolder); + + // 更新文件夹顺序并刷新UI + UpdateFolderOrder(); + + var Direction = SourceIndex < TargetIndex ? "后面" : "前面"; + ModBase.Log( + "[Control] 文件夹拖拽排序:" + SourceFolder.Name + " -> 位置 " + NewTargetIndex + " (在 " + TargetFolder.Name + + " " + Direction + ")", ModBase.LogLevel.Debug); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "拖拽放下操作失败", ModBase.LogLevel.Feedback); + } + finally + { + e.Handled = true; + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml index 474bdc164..83361737c 100644 --- a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml +++ b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml @@ -1,37 +1,53 @@  - - + + - - + + - + - + - - + + - + - + - - + + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs new file mode 100644 index 000000000..eeceb58e6 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs @@ -0,0 +1,613 @@ +using System.Collections; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.FileIO; +using PCL.Core.App; +using PCL.Core.App.Configuration; +using PCL.Core.App.Configuration.Storage; +using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem; + +namespace PCL; + +public partial class PageSelectRight +{ + private const int NormalDelay = 75; // 正常输入延迟0.075秒 + private const int QuickDelay = 50; // 清空搜索框延迟0.05秒 + private bool IsRefreshing; + + private DateTime LastInputTime = DateTime.MinValue; + private DispatcherTimer ReloadTimer; + + // 窗口属性 + /// + /// 是否显示隐藏的 Minecraft 实例。 + /// + public bool ShowHidden = false; + + public PageSelectRight() + { + InitializeComponent(); + Loaded += PageSelectRight_Loaded; + Unloaded += PageSelectRight_Unloaded; + LoaderInit(); + } + + // 窗口基础 + private void PageSelectRight_Loaded(object sender, RoutedEventArgs e) + { + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.RunOnUpdated, 1, @"versions\"); + PanBack.ScrollToHome(); + PanVerSearchBox.TextChanged += (a, b) => PanVerSearchBox_TextChanged(a, (TextChangedEventArgs)b); + + ReloadTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(NormalDelay) }; + ReloadTimer.Tick += ReloadTimer_Tick; + } + + private void PanVerSearchBox_TextChanged(object sender, TextChangedEventArgs e) + { + // 记录最后一次输入时间 + LastInputTime = DateTime.Now; + + IsRefreshing = false; + + // 动态调整延迟时间 + if (string.IsNullOrWhiteSpace(PanVerSearchBox.Text)) + { + if (ReloadTimer.Interval.TotalMilliseconds != QuickDelay) + ReloadTimer.Interval = TimeSpan.FromMilliseconds(QuickDelay); + } + else if (ReloadTimer.Interval.TotalMilliseconds != NormalDelay) + { + ReloadTimer.Interval = TimeSpan.FromMilliseconds(NormalDelay); + } + + + if (!ReloadTimer.IsEnabled) ReloadTimer.Start(); + } + + private void ReloadTimer_Tick(object sender, EventArgs e) + { + // 检查是否超过当前设定的延迟时间没有新输入 + var elapsed = (DateTime.Now - LastInputTime).TotalMilliseconds; + var currentDelay = ReloadTimer.Interval.TotalMilliseconds; + + if (elapsed >= currentDelay && ModMinecraft.McInstanceListLoader.State == ModBase.LoadState.Finished && + !IsRefreshing) + { + IsRefreshing = true; + + // 确保在UI线程执行刷新 + Dispatcher.BeginInvoke(new Action(() => + { + McInstanceListUI(ModMinecraft.McInstanceListLoader); + IsRefreshing = false; + })); + ReloadTimer.Stop(); + } + } + + private void PageSelectRight_Unloaded(object sender, RoutedEventArgs e) + { + // 清理计时器 + if (ReloadTimer is not null) + { + ReloadTimer.Stop(); + ReloadTimer.Tick -= ReloadTimer_Tick; + ReloadTimer = null; + } + } + + private void LoaderInit() + { + PageLoaderInit(Load, PanLoad, PanAllBack, null, ModMinecraft.McInstanceListLoader, + a => this.McInstanceListUI((ModLoader.LoaderTask)a), + AutoRun: false); + } + + private void Load_Click(object sender, MouseButtonEventArgs e) + { + if (ModMinecraft.McInstanceListLoader.State == ModBase.LoadState.Failed) + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + + #region 结果 UI 化 + + private void McInstanceListUI(ModLoader.LoaderTask Loader) + { + try + { + var Path = Loader.Input; + // 加载 UI + PanMain.Children.Clear(); + + var hasVisibleFolders = false; + var searchText = PanVerSearchBox.Text.Trim().ToLower(); // 获取搜索框文本 + var hasAnyResults = false; + var originalHasInstances = ModMinecraft.McInstanceList.ToArray().Any(c => c.Value.Count > 0); + + // 搜索无结果时显示 PanEmptySearch + PanEmptySearch.Visibility = Visibility.Collapsed; // 默认隐藏 + + foreach (var Card in ModMinecraft.McInstanceList.ToArray()) + { + if ((Card.Key == ModMinecraft.McInstanceCardType.Hidden) ^ ShowHidden) + continue; + var filteredInstances = Card.Value.Where(v => + { + if (string.IsNullOrEmpty(searchText)) + return true; + return v.Name.ToLower().Contains(searchText) || + (v.Desc is not null && v.Desc.ToLower().Contains(searchText)) || v.GetDefaultDescription() + .Replace(",", "").ToLower().Trim().Contains(searchText); + }).ToList(); + if (filteredInstances.Count == 0) + continue; + + hasVisibleFolders = true; + hasAnyResults = true; + if (filteredInstances.Count == 0) + continue; + hasVisibleFolders = true; + + #region 确认卡片名称 + + var CardName = ""; + switch (Card.Key) + { + case ModMinecraft.McInstanceCardType.OriginalLike: + { + CardName = "常规实例"; + break; + } + case ModMinecraft.McInstanceCardType.API: + { + var IsForgeExists = false; + var IsNeoForgeExists = false; + var IsFabricExists = false; + var IsQuiltExists = false; + var IsLiteExists = false; + var IsCleanroomExists = false; + var IsLabyModExists = false; + foreach (var instance in Card.Value) + { + if (!instance.IsLoaded) + instance.Load(); + if (instance.Info.HasFabric) + IsFabricExists = true; + if (instance.Info.HasQuilt) + IsQuiltExists = true; + if (instance.Info.HasLiteLoader) + IsLiteExists = true; + if (instance.Info.HasForge) + IsForgeExists = true; + if (instance.Info.HasNeoForge) + IsNeoForgeExists = true; + if (instance.Info.HasCleanroom) + IsCleanroomExists = true; + if (instance.Info.HasLabyMod) + IsLabyModExists = true; + } + + if ((IsLiteExists ? 1 : 0) + (IsForgeExists ? 1 : 0) + (IsFabricExists ? 1 : 0) + + (IsNeoForgeExists ? 1 : 0) + (IsQuiltExists ? 1 : 0) + (IsCleanroomExists ? 1 : 0) + + (IsLabyModExists ? 1 : 0) > 1) + CardName = "可安装 Mod"; + else if (IsForgeExists) + CardName = "Forge 实例"; + else if (IsNeoForgeExists) + CardName = "NeoForge 实例"; + else if (IsCleanroomExists) + CardName = "Cleanroom 实例"; + else if (IsLabyModExists) + CardName = "LabyMod 实例"; + else if (IsLiteExists) + CardName = "LiteLoader 实例"; + else if (IsQuiltExists) + CardName = "Quilt 实例"; + else + CardName = "Fabric 实例"; + + break; + } + case ModMinecraft.McInstanceCardType.Error: + { + CardName = "错误的实例"; + break; + } + case ModMinecraft.McInstanceCardType.Hidden: + { + CardName = "隐藏的实例"; + break; + } + case ModMinecraft.McInstanceCardType.Rubbish: + { + CardName = "不常用实例"; + break; + } + case ModMinecraft.McInstanceCardType.Star: + { + CardName = "收藏夹"; + break; + } + case ModMinecraft.McInstanceCardType.Fool: + { + CardName = "愚人节版本"; + break; + } + + default: + { + throw new ArgumentException("未知的卡片种类(" + (int)Card.Key + ")"); + } + } + + #endregion + + // 建立控件 + var CardTitle = CardName + (CardName == "收藏夹" ? "" : " (" + filteredInstances.Count + ")"); + var NewCard = new MyCard { Title = CardTitle, Margin = new Thickness(0d, 0d, 0d, 15d) }; + var NewStack = new StackPanel + { + Margin = new Thickness(20d, MyCard.SwapedHeight, 18d, 0d), + VerticalAlignment = VerticalAlignment.Top, RenderTransform = new TranslateTransform(0d, 0d), + Tag = filteredInstances + }; + NewCard.Children.Add(NewStack); + NewCard.SwapControl = NewStack; + PanMain.Children.Add(NewCard); + + // 确定卡片是否展开 + void PutMethod(StackPanel Stack) + { + foreach (var item in (IEnumerable)Stack.Tag) + Stack.Children.Add(McVersionListItem((ModMinecraft.McInstance)item)); + } + + ; + if (Card.Key == ModMinecraft.McInstanceCardType.Rubbish || + Card.Key == ModMinecraft.McInstanceCardType.Error || + Card.Key == ModMinecraft.McInstanceCardType.Fool) + { + NewCard.IsSwapped = true; + NewCard.InstallMethod = PutMethod; + } + else + { + MyCard.StackInstall(ref NewStack, PutMethod); + } + } + + // 若只有一个卡片,则强制展开 + if (PanMain.Children.Count == 1 && ((MyCard)PanMain.Children[0]).IsSwapped) + ((MyCard)PanMain.Children[0]).IsSwapped = false; + + PanVerSearchBox.Visibility = hasVisibleFolders ? Visibility.Visible : Visibility.Collapsed; + + // 判断应该显示哪一个页面 + if (!hasAnyResults) + { + if (!originalHasInstances) + { + // 完全没有实例的情况 + PanEmpty.Visibility = Visibility.Visible; + PanBack.Visibility = Visibility.Collapsed; + if (ShowHidden) + { + LabEmptyTitle.Text = "无隐藏实例"; + LabEmptyContent.Text = "没有实例被隐藏,你可以在实例设置的实例分类选项中隐藏实例。" + "\r\n" + + "再次按下 F11 即可退出隐藏实例查看模式。"; + BtnEmptyDownload.Visibility = Visibility.Collapsed; + } + else + { + LabEmptyTitle.Text = "无可用实例"; + LabEmptyContent.Text = "未找到任何游戏实例,请先下载一个游戏实例。" + "\r\n" + + "若有已存在的实例,请在左边的列表中选择添加文件夹,选择 .minecraft 文件夹将其导入。"; + BtnEmptyDownload.Visibility = + Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow + ? Visibility.Collapsed + : Visibility.Visible; + } + } + // 有实例但搜索无结果的情况 + else if (ShowHidden && ModMinecraft.McInstanceList.ToArray().Any(c => + c.Key == ModMinecraft.McInstanceCardType.Hidden && c.Value.Count > 0)) + { + // 有隐藏实例但搜索无结果 - 显示搜索无结果提示 + PanVerSearchBox.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + PanBack.Visibility = Visibility.Visible; + PanEmptySearch.Visibility = Visibility.Visible; + LabEmptySearchTitle.Text = "无匹配的隐藏实例"; + LabEmptySearchContent.Text = string.IsNullOrWhiteSpace(searchText) + ? "请输入搜索内容" + : $"没有找到与 '{searchText}' 匹配的隐藏实例"; + } + else if (ShowHidden) + { + // 无隐藏实例 - 显示"无隐藏实例"提示 + PanEmpty.Visibility = Visibility.Visible; + PanBack.Visibility = Visibility.Collapsed; + LabEmptyTitle.Text = "无隐藏实例"; + LabEmptyContent.Text = + "没有实例被隐藏,你可以在实例设置的实例分类选项中隐藏实例。" + "\r\n" + "再次按下 F11 即可退出隐藏实例查看模式。"; + BtnEmptyDownload.Visibility = Visibility.Collapsed; + PanVerSearchBox.Visibility = Visibility.Collapsed; + } + else + { + // 普通模式下的搜索无结果 + PanVerSearchBox.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + PanBack.Visibility = Visibility.Visible; + PanEmptySearch.Visibility = Visibility.Visible; + LabEmptySearchTitle.Text = "无匹配的游戏实例"; + LabEmptySearchContent.Text = string.IsNullOrWhiteSpace(searchText) + ? "请输入搜索内容" + : $"没有找到与 '{searchText}' 匹配的实例"; + } + } + else + { + PanBack.Visibility = Visibility.Visible; + PanEmpty.Visibility = Visibility.Collapsed; + PanEmptySearch.Visibility = Visibility.Collapsed; + } // 有结果时隐藏 + } + + + catch (Exception ex) + { + ModBase.Log(ex, "将实例列表转换显示时失败", ModBase.LogLevel.Feedback); + } + } + + public static MyListItem McVersionListItem(ModMinecraft.McInstance instance) + { + var NewItem = new MyListItem + { + Title = instance.Name, Info = instance.Desc, Height = 42d, Tag = instance, SnapsToDevicePixels = true, + Type = MyListItem.CheckType.Clickable + }; + var instanceInfo = instance.Info; + var tags = new List(); + tags.Add(instanceInfo.VanillaName); + if (instanceInfo.HasForge) + tags.Add("Forge " + instanceInfo.Forge); + else if (instanceInfo.HasNeoForge) + tags.Add("NeoForge " + instanceInfo.NeoForge); + else if (instanceInfo.HasCleanroom) + tags.Add("Cleanroom " + instanceInfo.Cleanroom); + else if (instanceInfo.HasLabyMod) + tags.Add("LabyMod " + instanceInfo.LabyMod); + else if (instanceInfo.HasQuilt) + tags.Add("Quilt " + instanceInfo.Quilt); + else if (instanceInfo.HasFabric) tags.Add("Fabric " + instanceInfo.Fabric); + if (instanceInfo.HasLiteLoader) + tags.Add("LiteLoader"); + if (instanceInfo.HasOptiFine) + tags.Add("OptiFine " + instanceInfo.OptiFine); + NewItem.Tags = tags; + try + { + if (instance.Logo.EndsWith(@"PCL\Logo.png")) + NewItem.Logo = instance.PathInstance + @"PCL\Logo.png"; // 修复老版本中,存储的自定义 Logo 使用完整路径,导致移动后无法加载的 Bug + else + NewItem.Logo = instance.Logo; + } + catch (Exception ex) + { + ModBase.Log(ex, "加载实例图标失败", ModBase.LogLevel.Hint); + NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"; + } + + NewItem.ContentHandler = McVersionListContent; + return NewItem; + } + + private static void McVersionListContent(MyListItem sender, EventArgs e) + { + var Version = (ModMinecraft.McInstance)sender.Tag; + // 注册点击事件 + sender.Click += (a, b) => Item_Click((MyListItem)a, b); + // 图标按钮 + var BtnStar = new MyIconButton(); + if (Version.IsStar) + { + BtnStar.ToolTip = "取消收藏"; + ToolTipService.SetPlacement(BtnStar, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnStar, 30d); + ToolTipService.SetHorizontalOffset(BtnStar, 2d); + BtnStar.LogoScale = 1.1d; + BtnStar.Logo = ModBase.Logo.IconButtonLikeFill; + } + else + { + BtnStar.ToolTip = "收藏"; + ToolTipService.SetPlacement(BtnStar, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnStar, 30d); + ToolTipService.SetHorizontalOffset(BtnStar, 2d); + BtnStar.LogoScale = 1.1d; + BtnStar.Logo = ModBase.Logo.IconButtonLikeLine; + } + + BtnStar.Click += (_, _) => + { + Config.Instance.Starred[Version.PathInstance] = !Version.IsStar; + ModMinecraft.McInstanceListForceRefresh = true; + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + }; + var BtnOpenFolder = new MyIconButton { LogoScale = 1.1d, Logo = ModBase.Logo.IconButtonOpen }; + BtnOpenFolder.ToolTip = "打开实例目录"; + ToolTipService.SetPlacement(BtnOpenFolder, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnOpenFolder, 30d); + ToolTipService.SetHorizontalOffset(BtnOpenFolder, 2d); + BtnOpenFolder.Click += (_, _) => PageInstanceOverall.OpenVersionFolder(Version); + var BtnDel = new MyIconButton { LogoScale = 1.1d, Logo = ModBase.Logo.IconButtonDelete }; + BtnDel.ToolTip = "删除"; + ToolTipService.SetPlacement(BtnDel, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnDel, 30d); + ToolTipService.SetHorizontalOffset(BtnDel, 2d); + BtnDel.Click += (_, _) => DeleteVersion(sender, Version); + if (Version.State != ModMinecraft.McInstanceState.Error) + { + var BtnCont = new MyIconButton { LogoScale = 1.1d, Logo = ModBase.Logo.IconButtonSetup }; + BtnCont.ToolTip = "设置"; + ToolTipService.SetPlacement(BtnCont, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnCont, 30d); + ToolTipService.SetHorizontalOffset(BtnCont, 2d); + BtnCont.Click += (_, _) => + { + PageInstanceLeft.Instance = Version; + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSetup); + }; + sender.MouseRightButtonUp += (_, _) => + { + PageInstanceLeft.Instance = Version; + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSetup); + }; + sender.Buttons = new[] { BtnStar, BtnOpenFolder, BtnDel, BtnCont }; + } + else + { + var BtnCont = new MyIconButton { LogoScale = 1.15d, Logo = ModBase.Logo.IconButtonOpen }; + BtnCont.ToolTip = "打开文件夹"; + ToolTipService.SetPlacement(BtnCont, PlacementMode.Center); + ToolTipService.SetVerticalOffset(BtnCont, 30d); + ToolTipService.SetHorizontalOffset(BtnCont, 2d); + BtnCont.Click += (_, _) => PageInstanceOverall.OpenVersionFolder(Version); + sender.MouseRightButtonUp += (_, _) => PageInstanceOverall.OpenVersionFolder(Version); + sender.Buttons = new[] { BtnStar, BtnOpenFolder, BtnDel, BtnCont }; + } + } + + #endregion + + #region 页面事件 + + // 点击选项 + public static void Item_Click(MyListItem sender, EventArgs e) + { + var instance = (ModMinecraft.McInstance)sender.Tag; + if (new ModMinecraft.McInstance(instance.PathInstance).Check()) + { + // 正常实例 + ModMinecraft.McInstanceSelected = instance; + States.Game.SelectedInstance = ModMinecraft.McInstanceSelected.Name; + ModMain.FrmMain.PageBack(); + } + else + { + // 错误实例 + PageInstanceOverall.OpenVersionFolder(instance); + } + } + + private void BtnDownload_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall); + } + + // 修改此代码时,同时修改 PageInstanceOverall 中的代码 + public static void DeleteVersion(MyListItem item, ModMinecraft.McInstance instance) + { + try + { + var IsShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift); + var IsHintIndie = instance.State != ModMinecraft.McInstanceState.Error && + (instance.PathIndie ?? "") != (ModMinecraft.McFolderSelected ?? ""); + switch (ModMain.MyMsgBox( + $"你确定要{(IsShiftPressed ? "永久" : "")}删除实例 {instance.Name} 吗?" + (IsHintIndie + ? "\r\n" + "由于该实例开启了版本隔离,删除时该实例对应的存档、资源包、Mod 等文件也将被一并删除!" + : ""), "实例删除确认", Button2: "取消", IsWarn: true)) + { + case 1: + { + ModBase.IniClearCache(instance.PathIndie + "options.txt"); + ((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache( + instance.PathInstance); + if (IsShiftPressed) + { + ModBase.DeleteDirectory(instance.PathInstance); + ModMain.Hint("实例 " + instance.Name + " 已永久删除!", ModMain.HintType.Finish); + } + else + { + FileSystem.DeleteDirectory(instance.PathInstance, UIOption.AllDialogs, + RecycleOption.SendToRecycleBin); + ModMain.Hint("实例 " + instance.Name + " 已删除到回收站!", ModMain.HintType.Finish); + } + + break; + } + case 2: + { + return; + } + } + + // 从 UI 中移除 + if (instance.DisplayType == ModMinecraft.McInstanceCardType.Hidden || !instance.IsStar) + { + // 仅出现在当前卡片 + var Parent = (StackPanel)item.Parent; + if (Parent.Children.Count > 2) // 当前的项目与一个占位符 + { + // 删除后还有剩 + var Card = (MyCard)Parent.Parent; + Card.Title = Card.Title.Replace((Parent.Children.Count - 1).ToString(), + (Parent.Children.Count - 2).ToString()); // 有一个占位符 + Parent.Children.Remove(item); + if (ModMinecraft.McInstanceSelected is not null && (instance.PathInstance ?? "") == + (ModMinecraft.McInstanceSelected.PathInstance ?? "")) + // 删除当前实例就更改选择 + ModMinecraft.McInstanceSelected = (ModMinecraft.McInstance)((MyListItem)Parent.Children[0]).Tag; + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.UpdateOnly, 1, @"versions\"); + } + else + { + // 删除后没剩了 + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + } + else + { + // 同时出现在当前卡片与收藏夹 + ModLoader.LoaderFolderRun(ModMinecraft.McInstanceListLoader, ModMinecraft.McFolderSelected, + ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\"); + } + } + catch (OperationCanceledException ex) + { + ModBase.Log(ex, "删除实例 " + instance.Name + " 被主动取消"); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除实例 " + instance.Name + " 失败", ModBase.LogLevel.Msgbox); + } + } + + public void BtnEmptyDownload_Loaded() + { + var NewVisibility = (Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow) || ShowHidden + ? Visibility.Collapsed + : Visibility.Visible; + if (BtnEmptyDownload.Visibility != NewVisibility) + { + BtnEmptyDownload.Visibility = NewVisibility; + PanLoad.TriggerForceResize(); + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml index baddcf5d1..aa311d7e9 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml @@ -1,20 +1,21 @@  - - - - - + + + + diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml.cs new file mode 100644 index 000000000..89c92fc22 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageHomePageMarket.xaml.cs @@ -0,0 +1,56 @@ +using PCL.Core.IO.Net.Http.Client; +using System; +using System.Windows; +using System.Windows.Input; + +namespace PCL +{ + public partial class PageHomePageMarket : MyPageRight, IRefreshable + { + private ModLoader.LoaderTask Loader; + + public PageHomePageMarket() + { + InitializeComponent(); + Loaded += Page_Loaded; + } + + private void Page_Loaded(object sender, RoutedEventArgs e) + { + Loader = new ModLoader.LoaderTask("HomepageMarket", HomepageMarketGet); + PageLoaderInit(Load, PanLoad, PanMain, PanCustom, Loader, _ => Refresh()); + } + + public void Refresh() + { + Loader.Start(); + } + + private void HomepageMarketGet(ModLoader.LoaderTask Task) + { + try + { + const string HomepageMarketUri = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/JingHai-Lingyun/Custom.xaml"; + var content = HttpRequestBuilder.Create(HomepageMarketUri).SendAsync(true).Result.AsStringAsync().Result; + content = content.Replace("EventType=\"刷新主页\"", "EventType=\"刷新主页市场\""); + + ModBase.RunInUi(() => + { + PanCustom.Children.Clear(); + var element = ModBase.GetObjectFromXML($"{content}") as UIElement; + + if (element != null) + { + PanCustom.Children.Add(element); + } + }); + + Task.Output = content; + } + catch (Exception ex) + { + throw new Exception("加载主页市场失败", ex); + } + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml index 59ddb5481..809ec0d2d 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml @@ -1,9 +1,10 @@ @@ -20,11 +21,16 @@ - - + + - + Margin="-5,0,15,0" + Info="当前版本: %VERSION% (%BRANCH%, %VERSIONCODE%, %COMMIT_HASH%)" Grid.Row="2" + Grid.Column="1" x:Name="ItemAboutPcl" /> + @@ -34,10 +40,14 @@ - - - - + + + + @@ -60,23 +70,41 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -133,12 +161,13 @@ - + - - - + + + @@ -148,81 +177,105 @@ - + + Source="{Binding AvatarUrl}" + RenderOptions.BitmapScalingMode="HighQuality"> + FontSize="14" + Margin="0,8,0,0" + HorizontalAlignment="Center" + MaxWidth="90" + EventType="打开网页" + EventData="{Binding HtmlUrl}" + ToolTip="{Binding Login}" /> - + - + - + - - - + + + - + - + - - + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml.cs new file mode 100644 index 000000000..e91e37dfd --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupAbout.xaml.cs @@ -0,0 +1,191 @@ +using System.Collections.ObjectModel; +using System.Text.Json.Serialization; +using System.Windows; +using System.Windows.Input; +using PCL.Core.IO.Net.Http.Client; + +namespace PCL; + +public partial class PageSetupAbout +{ + // 彩蛋 + private int ClickCount; + + private new bool IsLoaded; + + public PageSetupAbout() + { + InitializeComponent(); + Loaded += PageOtherAbout_Loaded; + } + + public ObservableCollection Contributors { get; set; } = new(); + + private void PageOtherAbout_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + ItemAboutPcl.Info = ItemAboutPcl.Info.Replace("%VERSION%", ModBase.VersionBaseName) + .Replace("%VERSIONCODE%", ModBase.VersionCode.ToString()).Replace("%BRANCH%", ModBase.VersionBranchName) + .Replace("%COMMIT_HASH%", ModBase.CommitHashShort); + LoadContributersAsync(); + } + + private async void LoadContributersAsync() + { + try + { + using (var response = await HttpRequestBuilder + .Create("https://api.github.com/repos/PCL-Community/PCL2-CE/contributors").SendAsync(true)) + { + var cos = await response.AsJsonAsync>(); + Contributors.Clear(); + foreach (var item in cos) + Contributors.Add((GitHubContributor)item); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "加载贡献者信息失败"); + } + } + + private void ImgPCLCommunity_Click(object sender, MouseButtonEventArgs e) + { + ModAnimation.AniStart(new[] { ModAnimation.AaRotateTransform(sender, 360d) }); + } + + private void ImgPCLLogo_Click(object sender, MouseButtonEventArgs e) + { + if (ClickCount < 200) + { + ClickCount += 1; + switch (ClickCount) + { + case 5: + { + ModMain.Hint("点这个很好玩么……"); + break; + } + case 15: + { + ModMain.Hint("还点?"); + break; + } + case 25: + { + switch (ModMain.MyMsgBox("你现在是不是超无聊的?", "咕咕咕?", "是的", "并不是")) + { + case 2: + { + ModMain.Hint("那你还点啥……真是搞不懂。"); + break; + } + } + + break; + } + case 50: + { + ModMain.Hint("嗯,加油吧,嗯……"); + break; + } + case 75: + { + ModMain.Hint("隐藏主题 混乱黄 已……嗯不对,这是 PCL 社区版,应该没有这玩意……"); + break; + } + case 100: + { + ModMain.Hint("你咋还这么无聊啊?"); + break; + } + case 130: + { + ModMain.Hint("后面什么都没有了哦!"); + break; + } + case 150: + { + switch (ModMain.MyMsgBox("你真的不累么?", "温馨提示", "累死了", "真的不累")) + { + case 1: + { + ModMain.Hint("那你就别点了喂……后面真的真的真的什么都没有了!"); + break; + } + case 2: + { + switch (ModMain.MyMsgBox("你真的真的不累么?", "超温馨的温馨提示", "累死了", "真的真的不累")) + { + case 1: + { + ModMain.Hint("那你就别点了喂……后面真的真的真的什么都没有了!"); + break; + } + case 2: + { + switch (ModMain.MyMsgBox("你真的真的真的不累么?", "超超超温馨的温馨提示", "累死了", "真的真的真的不累")) + { + case 1: + { + ModMain.Hint("那你就别点了喂……后面真的真的真的什么都没有了!"); + break; + } + case 2: + { + ModMain.Hint("好吧……不过后面是真的啥也没了,不用点了真的。"); + break; + } + } + + break; + } + } + + break; + } + } + + break; + } + case 200: + { + ModMain.Hint("还点,还点就不让你点了……"); + ImgPCLLogo.IsHitTestVisible = false; + return; + } + } + + var rand = new Random(); + var mx = rand.Next(-1, 1); + if (mx == 0) + mx = 1; + var my = rand.Next(-1, 1); + if (my == 0) + my = 1; + ModAnimation.AniStart(new[] + { + ModAnimation.AaTranslateX(sender, mx, 0), ModAnimation.AaTranslateY(sender, my, 0), + ModAnimation.AaTranslateX(sender, -mx, 0, 100), ModAnimation.AaTranslateY(sender, -my, 0, 100) + }); + } + } + + public class GitHubContributor + { + [JsonPropertyName("login")] public string Login { get; set; } + + [JsonPropertyName("avatar_url")] public string AvatarUrl { get; set; } + + [JsonPropertyName("html_url")] public string HtmlUrl { get; set; } + + [JsonPropertyName("contributions")] public int Contributions { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml index bcb6233f3..27f9cbbaf 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml @@ -1,16 +1,15 @@ - + - + @@ -18,42 +17,47 @@ - + - + - + - + - + - + - - + + - + - + - + - - - + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml.cs new file mode 100644 index 000000000..5a12e46e5 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupFeedback.xaml.cs @@ -0,0 +1,199 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Newtonsoft.Json.Linq; +using PCL.Core.Utils; + +namespace PCL; + +public partial class PageSetupFeedback +{ + public enum TagID : long + { + Processing = 6820804544L, + WaitingProcess = 6820804546L, + Completed = 6820804547L, + Decline = 6820804539L, + Ignored = 8064650117L, + Duplicate = 6820804541L, + Wait = 8743070786L, + Pause = 8558220235L, + Upnext = 8550609020L + } + + private new bool IsLoaded; + + public ModLoader.LoaderTask> Loader; + + public PageSetupFeedback() + { + InitializeComponent(); + Loader = new ModLoader.LoaderTask>("FeedbackList", FeedbackListGet); + Loaded += PageOtherFeedback_Loaded; + } + + private void PageOtherFeedback_Loaded(object sender, RoutedEventArgs e) + { + PageLoaderInit(Load, PanLoad, PanContent, PanInfo, Loader, _ => RefreshList()); + // 重复加载部分 + PanBack.ScrollToHome(); + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + } + + public void FeedbackListGet(ModLoader.LoaderTask> Task) + { + JArray list; + list = (JArray)ModNet.NetGetCodeByRequestRetry( + "https://api.github.com/repos/PCL-Community/PCL2-CE/issues?state=all&sort=created&per_page=200", + IsJson: true, UseBrowserUserAgent: true); // 获取近期 200 条数据就够了 + if (list is null) + throw new Exception("无法获取到内容"); + var res = new List(); + foreach (JObject i in list) + { + var pullRequestToken = i["pull_request"]; + if (pullRequestToken is not null && pullRequestToken.Type != JTokenType.Null) continue; + + var item = new Feedback + { + Title = i["title"].ToString(), + Url = i["html_url"].ToString(), + Content = i["body"].ToString(), + Time = DateTime.Parse(i["created_at"].ToString()), + User = i["user"]["login"].ToString(), + ID = (string)i["number"], + Open = i["state"].ToString().Equals("open"), + IsPullRequest = false + }; + + var issueType = "未分类"; + var typeToken = i["type"]; + if (typeToken is not null && typeToken.Type == JTokenType.Object) + { + var typeNameToken = typeToken["name"]; + if (typeNameToken is not null) issueType = typeNameToken.ToString().ToLower(); + } + + item.Type = issueType; + + var thisTags = (JArray)i["labels"]; + foreach (JObject thisTag in thisTags) + item.Tags.Add((string)thisTag["id"]); + res.Add(item); + } + + Task.Output = res; + } + + private MyListItem CreateFeedbackItem(Feedback item, string logo) + { + var commonInfo = $"{item.User} | {item.Time:yyyy-MM-dd HH:mm:ss}"; + + var li = new MyListItem(); + li.Title = item.Title; + li.Type = MyListItem.CheckType.Clickable; + li.Info = commonInfo; + li.Logo = ModBase.PathImage + logo; + li.Tags = item.Type; + + li.Click += (sender, e) => ShowFeedbackDetail(item); + + return li; + } + + private void ShowFeedbackDetail(Feedback item) + { + var timeSpanText = TimeUtils.GetTimeSpanString(item.Time - DateTime.Now, false); + switch (ModMain.MyMsgBoxMarkdown( + $"提交者:{item.User}({timeSpanText})" + "\r\n" + $"类型:{item.Type}" + "\r\n" + + "\r\n" + $"{item.Content}", $"#{item.ID} {item.Title}", Button2: "查看详情")) + { + case 2: + { + ModBase.OpenWebsite(item.Url); + break; + } + } + } + + private void SetPanelVisibility(StackPanel panel, MyCard card) + { + card.Visibility = panel.Children.Count == 0 ? Visibility.Collapsed : Visibility.Visible; + } + + public void RefreshList() + { + PanListProcessing.Children.Clear(); + PanListWaitingProcess.Children.Clear(); + PanListWait.Children.Clear(); + PanListPause.Children.Clear(); + PanListUpnext.Children.Clear(); + PanListCompleted.Children.Clear(); + PanListDecline.Children.Clear(); + PanListIgnored.Children.Clear(); + PanListDuplicate.Children.Clear(); + + foreach (var item in Loader.Output) + { + if (item.Tags.Contains(((long)TagID.Processing).ToString())) + PanListProcessing.Children.Add(CreateFeedbackItem(item, "Blocks/CommandBlock.png")); + + if (item.Tags.Contains(((long)TagID.WaitingProcess).ToString())) + PanListWaitingProcess.Children.Add(CreateFeedbackItem(item, "Blocks/RedstoneBlock.png")); + + if (item.Tags.Contains(((long)TagID.Wait).ToString())) + PanListWait.Children.Add(CreateFeedbackItem(item, "Blocks/Anvil.png")); + + if (item.Tags.Contains(((long)TagID.Pause).ToString())) + PanListPause.Children.Add(CreateFeedbackItem(item, "Blocks/RedstoneLampOff.png")); + + if (item.Tags.Contains(((long)TagID.Upnext).ToString())) + PanListUpnext.Children.Add(CreateFeedbackItem(item, "Blocks/RedstoneLampOn.png")); + + if (item.Tags.Contains(((long)TagID.Completed).ToString())) + PanListCompleted.Children.Add(CreateFeedbackItem(item, "Blocks/Grass.png")); + + if (item.Tags.Contains(((long)TagID.Decline).ToString())) + PanListDecline.Children.Add(CreateFeedbackItem(item, "Blocks/CobbleStone.png")); + + if (item.Tags.Contains(((long)TagID.Ignored).ToString())) + PanListIgnored.Children.Add(CreateFeedbackItem(item, "Blocks/CobbleStone.png")); + + if (item.Tags.Contains(((long)TagID.Duplicate).ToString())) + PanListDuplicate.Children.Add(CreateFeedbackItem(item, "Blocks/CobbleStone.png")); + } + + SetPanelVisibility(PanListProcessing, PanContentProcessing); + SetPanelVisibility(PanListWaitingProcess, PanContentWaitingProcess); + SetPanelVisibility(PanListWait, PanContentWait); + SetPanelVisibility(PanListPause, PanContentPause); + SetPanelVisibility(PanListUpnext, PanContentUpnext); + SetPanelVisibility(PanListCompleted, PanContentCompleted); + SetPanelVisibility(PanListDecline, PanContentDecline); + SetPanelVisibility(PanListIgnored, PanContentIgnored); + SetPanelVisibility(PanListDuplicate, PanContentDuplicate); + } + + private void Feedback_Click(object sender, MouseButtonEventArgs e) + { + PageSetupLeft.TryFeedback(); + } + + public class Feedback + { + public string User { get; set; } + public string Title { get; set; } + public DateTime Time { get; set; } + public string Content { get; set; } + public string Url { get; set; } + public string ID { get; set; } + public List Tags { get; set; } = new(); + public bool Open { get; set; } = true; + public string Type { get; set; } + public bool IsPullRequest { get; set; } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml index 10564e237..61fe2fb13 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml @@ -1,17 +1,18 @@ - + - + @@ -35,8 +36,11 @@ - - + + - - - - + + + + - - - - + + + + - + - + @@ -88,10 +102,12 @@ - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml.cs new file mode 100644 index 000000000..5ebabcafe --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameLink.xaml.cs @@ -0,0 +1,157 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Link.Scaffolding.EasyTier; + +namespace PCL; + +public partial class PageSetupGameLink +{ + private bool IsFirstLoad = true; + + private new bool IsLoaded; + + public PageSetupGameLink() + { + InitializeComponent(); + Loaded += PageSetupLink_Loaded; + Loaded += (_, __) => Reload(); + } + + private void PageSetupLink_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + ModAnimation.AniControlEnabled += 1; + Reload(); + ModAnimation.AniControlEnabled -= 1; + } + + public void Reload() + { + TextLinkUsername.Text = Config.Link.Username; + // TextLinkRelay.Text = Config.Link.RelayServer + // ComboRelayType.SelectedIndex = Config.Link.RelayType + // ComboServerType.SelectedIndex = Config.Link.ServerType + CheckLatencyFirstMode.Checked = Config.Link.UseLatencyFirstMode; + ComboPreferProtocol.SelectedIndex = (int)Config.Link.ProtocolPreference; + CheckTryPunchSym.Checked = Config.Link.TryPunchSym; + CheckEnableIPv6.Checked = Config.Link.EnableIPv6; + CheckEnableCliOutput.Checked = Config.Link.EnableCliOutput; + + // TextRelays.Text = "正在获取信息..." + // Do While Not (PageLinkLobby.LobbyAnnouncementLoader.State = LoadState.Finished OrElse PageLinkLobby.LobbyAnnouncementLoader.State = LoadState.Failed) + // Thread.Sleep(500) + // Loop + // If ETRelay.RelayList.Count > 0 Then + // TextRelays.Text = "" + // For Each Relay In ETRelay.RelayList + // Select Case Relay.Type + // Case ETRelayType.Community + // TextRelays.Text += "[社区] " + // Case ETRelayType.Selfhosted + // TextRelays.Text += "[自有] " + // Case Else 'ETRelayType.Custom + // TextRelays.Text += "[自定义] " + // End Select + // TextRelays.Text += Relay.Name & "," + // Next + // TextRelays.Text = TextRelays.Text.BeforeLast(",") + // Else + // TextRelays.Text = "暂无,你可能需要手动添加中继服务器" + // End If + } + + // 初始化 + public void Reset() + { + try + { + Config.Link.Reset(); + ModBase.Log("[Setup] 已初始化联机页设置"); + ModMain.Hint("已初始化联机页设置!", ModMain.HintType.Finish, false); + Reload(); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化联机页设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void TextBoxChange(object senderRaw, TextChangedEventArgs e) // , TextLinkRelay.ValidatedTextChanged + { + var sender = (MyTextBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Text); + } + + private static void + ComboBoxChange(MyComboBox sender, + object e) // Handles ComboRelayType.SelectionChanged, ComboServerType.SelectionChanged + { + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + } + + private void CheckBoxChange(object senderRaw, bool user) + { + var sender = (MyCheckBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Checked); + } + + private void LinkProtocolPerferenceChange(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled == 0) + try + { + var selection = (LinkProtocolPreference)((MyComboBox)sender).SelectedIndex; + Config.Link.ProtocolPreference = selection; + } + catch (Exception ex) + { + ModBase.Log(ex, "改变配置项失败", ModBase.LogLevel.Hint); + } + } + + // 网络测试 + private void BtnNetTest_Click(object sender, MouseButtonEventArgs e) + { + try + { + BtnNetTest.IsEnabled = false; + BtnNetTest.Text = "正在测试"; + ModBase.RunInNewThread(() => + { + var status = CliNetTest.GetNetStatusAsync().GetAwaiter().GetResult(); + ModBase.RunInUi(() => + { + TextUdpNatType.Text = + "UDP NAT 类型: " + CliNetTest.GetNatTypeString(status.UdpNatType); + TextTcpNatType.Text = + "TCP NAT 类型: " + CliNetTest.GetNatTypeString(status.TcpNatType); + TextIpv6Status.Text = "IPv6: " + (status.SupportIPv6 ? "支持" : "不支持"); + BtnNetTest.IsEnabled = true; + BtnNetTest.Text = "开始测试"; + }); + }); + } + catch (Exception ex) + { + ModBase.Log(ex, "[Link] 获取网络测试结果失败", ModBase.LogLevel.Hint); + BtnNetTest.IsEnabled = true; + BtnNetTest.Text = "开始测试"; + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml index 8917b43c1..82a8b2ac6 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml @@ -1,9 +1,9 @@  @@ -25,33 +25,48 @@ - + - - + + - - + - - + - - + + - - + + - - + + @@ -72,14 +87,18 @@ - - + + - - + @@ -87,8 +106,10 @@ - - + @@ -96,10 +117,11 @@ - + - + @@ -119,14 +141,22 @@ - - - - - + + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs new file mode 100644 index 000000000..b8090d789 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupGameManage.xaml.cs @@ -0,0 +1,140 @@ +using System.Windows; +using System.Windows.Controls; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; + +namespace PCL; + +public partial class PageSetupGameManage +{ + private new bool IsLoaded; + + public PageSetupGameManage() + { + InitializeComponent(); + Loaded += PageSetupSystem_Loaded; + } + + private void PageSetupSystem_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + ModAnimation.AniControlEnabled += 1; + Reload(); + SliderLoad(); + ModAnimation.AniControlEnabled -= 1; + } + + public void Reload() + { + // 下载 + SliderDownloadThread.Value = Conversions.ToInteger(Config.Download.ThreadLimit); + SliderDownloadSpeed.Value = Conversions.ToInteger(Config.Download.SpeedLimit); + ComboDownloadSource.SelectedIndex = Conversions.ToInteger(Config.Download.FileSource); + ComboDownloadVersion.SelectedIndex = Conversions.ToInteger(Config.Download.VersionListSource); + CheckDownloadAutoSelectVersion.Checked = (bool?)Config.Download.AutoSelectInstance; + CheckFixAuthlib.Checked = (bool?)Config.Download.FixAuthLib; + + // Mod 与整合包 + ComboDownloadTranslateV2.SelectedIndex = Conversions.ToInteger(Config.Download.Comp.NameFormatV2); + ComboDownloadMod.SelectedIndex = Conversions.ToInteger(Config.Download.Comp.CompSourceSolution); + ComboModLocalNameStyle.SelectedIndex = Conversions.ToInteger(Config.Download.Comp.UiCompNameSolution); + CheckDownloadIgnoreQuilt.Checked = (bool?)Config.Download.Comp.IgnoreQuilt; + CheckDownloadClipboard.Checked = (bool?)Config.Download.Comp.ReadClipboard; + + // Minecraft 更新提示 + CheckUpdateRelease.Checked = (bool?)Config.Tool.ReleaseNotification; + CheckUpdateSnapshot.Checked = (bool?)Config.Tool.SnapshotNotification; + + // 辅助设置 + CheckHelpChinese.Checked = (bool?)Config.Tool.AutoChangeLanguage; + } + + // 初始化 + public void Reset() + { + try + { + Config.Download.Reset(); + Config.Tool.Reset(); + ModBase.Log("[Setup] 已初始化其他页设置"); + ModMain.Hint("已初始化其他页设置!", ModMain.HintType.Finish, false); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化其他页设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void CheckBoxChange(object senderRaw, bool user) + { + var sender = (MyCheckBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Checked); + } + + private void SliderChange(object senderRaw, bool user) + { + var sender = (MySlider)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Value); + } + + private void ComboChange(object senderRaw, SelectionChangedEventArgs e) + { + var sender = (MyComboBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + } + + // 滑动条 + private void SliderLoad() + { + SliderDownloadThread.GetHintText = new Func(v => Operators.AddObject(v, 1)); + SliderDownloadSpeed.GetHintText = new Func(v => + { + switch (v) + { + case var @case when Operators.ConditionalCompareObjectLessEqual(@case, 14, false): + { + return $"{Operators.MultiplyObject(Operators.AddObject(v, 1), 0.1d):F1} M/s"; + } + case var case1 when Operators.ConditionalCompareObjectLessEqual(case1, 31, false): + { + return $"{Operators.MultiplyObject(Operators.SubtractObject(v, 11), 0.5d):F1} M/s"; + } + case var case2 when Operators.ConditionalCompareObjectLessEqual(case2, 41, false): + { + return Operators.ConcatenateObject(Operators.SubtractObject(v, 21), " M/s"); + } + default: + { + return "无限制"; + } + } + }); + } + + private void SliderDownloadThread_PreviewChange(object sender, ModBase.RouteEventArgs e) + { + if (SliderDownloadThread.Value < 100) + return; + if (!(States.Hint.LargeDownloadThread as bool? ?? false)) + { + States.Hint.LargeDownloadThread = true; + ModMain.MyMsgBox( + "如果设置过多的下载线程,可能会导致下载时出现非常严重的卡顿。" + "\r\n" + "一般设置 64 线程即可满足大多数下载需求,除非你知道你在干什么,否则不建议设置更多的线程数!", + "警告", "我知道了", IsWarn: true); + } + } +} diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml index 709da86d5..944cfc54d 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml @@ -1,25 +1,26 @@  - + - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml.cs new file mode 100644 index 000000000..523d015e6 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupJava.xaml.cs @@ -0,0 +1,183 @@ +using System.IO; +using System.Windows; +using System.Windows.Media; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Minecraft; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageSetupJava +{ + private bool IsLoad = false; + + public ModLoader.LoaderTask> Loader; + + public PageSetupJava() + { + InitializeComponent(); + Loader = new ModLoader.LoaderTask>("JavaPageLoader", Load_GetJavaList); + Loaded += PageSetupLaunch_Loaded; + } + + private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e) + { + PageLoaderInit(PanLoad, CardLoad, PanMain, null, Loader, _ => OnLoadFinished(), Load_Input); + } + + private object Load_Input() + { + return false; + } + + private void Load_GetJavaList(ModLoader.LoaderTask> loader) + { + if (loader.Input) JavaService.JavaManager.ScanJavaAsync().GetAwaiter().GetResult(); + loader.Output = ModJava.Javas.GetSortedJavaList(); + } + + private void OnLoadFinished() + { + MyListItem ItemBuilder(JavaEntry J) + { + var Item = new MyListItem(); + var VersionTypeDesc = J.Installation.IsJre ? "JRE" : "JDK"; + var VersionNameDesc = J.Installation.MajorVersion.ToString(); + Item.Title = $"{VersionTypeDesc} {VersionNameDesc}"; + + Item.Info = J.Installation.JavaFolder; + var displayTags = new List(); + var DisplayBits = J.Installation.Is64Bit ? "64 Bit" : "32 Bit"; + displayTags.Add(DisplayBits); + var DisplayBrand = J.Installation.Brand.ToString(); + displayTags.Add(DisplayBrand); + Item.Tags = displayTags; + + Item.Type = MyListItem.CheckType.RadioBox; + Item.Check += (sender, e) => + { + if (J.IsEnabled) + { + Config.Launch.SelectedJava = J.Installation.JavaExePath; + } + else + { + ModMain.Hint("请先启用此 Java 后再选择其作为默认 Java"); + e.Handled = true; + } + }; + var BtnOpenFolder = new MyIconButton(); + BtnOpenFolder.Logo = ModBase.Logo.IconButtonOpen; + BtnOpenFolder.ToolTip = "打开"; + BtnOpenFolder.Click += (sender, e) => ModBase.OpenExplorer(J.Installation.JavaFolder); + var BtnInfo = new MyIconButton(); + BtnInfo.Logo = ModBase.Logo.IconButtonInfo; + BtnInfo.ToolTip = "详细信息"; + BtnInfo.Click += (sender, e) => + ModMain.MyMsgBox( + $"类型: {VersionTypeDesc}" + "\r\n" + $"版本: {J.Installation.Version.ToString()}" + + "\r\n" + $"架构: {J.Installation.Architecture.ToString()} ({DisplayBits})" + + "\r\n" + $"品牌: {DisplayBrand}" + "\r\n" + $"位置: {J.Installation.JavaFolder}", + "Java 信息"); + var BtnEnableSwitch = new MyIconButton(); + + + Item.Buttons = new[] { BtnOpenFolder, BtnInfo, BtnEnableSwitch }; + + void UpdateEnableStyle(bool IsCurEnable) + { + if (IsCurEnable) + { + Item.LabTitle.TextDecorations = null; + Item.LabTitle.Foreground = (Brush)ModSecret.AppResources["ColorBrush1"]; + BtnEnableSwitch.Logo = ModBase.Logo.IconButtonDisable; + BtnEnableSwitch.ToolTip = "禁用此 Java"; + } + else + { + Item.LabTitle.TextDecorations = TextDecorations.Strikethrough; + Item.LabTitle.Foreground = (Brush)ModSecret.AppResources["ColorBrushGray4"]; + BtnEnableSwitch.Logo = ModBase.Logo.IconButtonEnable; + BtnEnableSwitch.ToolTip = "启用此 Java"; + } + } + + ; + BtnInfo.Click += (sender, e) => + { + try + { + var target = ModJava.Javas.AddOrGet(J.Installation.JavaExePath); + if (target.IsEnabled && Operators.ConditionalCompareObjectEqual( + Config.Launch.SelectedJava, target.Installation.JavaExePath, false)) + { + ModMain.Hint("请先取消选择此 Java 作为默认 Java 后再禁用"); + return; + } + + target.IsEnabled = !target.IsEnabled; + UpdateEnableStyle(target.IsEnabled); + ModJava.Javas.SaveConfig(); + } + catch (Exception ex) + { + ModBase.Log(ex, "调整 Java 启用状态失败", ModBase.LogLevel.Hint); + } + }; + UpdateEnableStyle(J.IsEnabled); + + return Item; + } + + ; + PanContent.Children.Clear(); + var ItemAuto = new MyListItem + { + Type = MyListItem.CheckType.RadioBox, + Title = "自动选择", + Info = "Java 选择自动挡,依据游戏需要自动选择合适的 Java" + }; + ItemAuto.Check += (sender, e) => Config.Launch.SelectedJava = ""; + PanContent.Children.Add(ItemAuto); + var CurrentSetJava = Config.Launch.SelectedJava; + foreach (var entry in ModJava.Javas.GetSortedJavaList()) + { + var item = ItemBuilder(entry); + PanContent.Children.Add(item); + if (entry.Installation.JavaExePath == CurrentSetJava) + item.SetChecked(true, false, false); + } + + if (string.IsNullOrEmpty(Conversions.ToString(CurrentSetJava))) + ItemAuto.SetChecked(true, false, false); + } + + private void BtnAdd_Click(object sender, ModBase.RouteEventArgs e) + { + var ret = SystemDialogs.SelectFile("Java 程序(java.exe)|java.exe", "选择 Java 程序"); + if (string.IsNullOrEmpty(ret) || !File.Exists(ret)) + return; + if (ModJava.Javas.Exist(ret)) + ModMain.Hint("Java 已经存在,不用再次添加……"); + else + Dispatcher.BeginInvoke(new Action(async () => + { + await Task.Run(() => + { + ModJava.Javas.AddOrGet(ret); + ModJava.Javas.SaveConfig(); + }); + if (ModJava.Javas.Exist(ret)) + { + ModMain.Hint("已添加 Java!", ModMain.HintType.Finish); + Loader.Start(true, true); + } + else + { + ModMain.Hint("未能成功将 Java 加入列表中", ModMain.HintType.Critical); + } + })); + } +} diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml index 376a7fdaf..f0beadd84 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml @@ -1,9 +1,11 @@  @@ -33,21 +35,36 @@ - - - - + + + + - + - - - + + + - - + + @@ -58,8 +75,11 @@ - - + + @@ -67,13 +87,17 @@ - - + + - + @@ -81,35 +105,52 @@ - + - + - - + + - - + + - - + @@ -120,8 +161,11 @@ - - + + @@ -134,43 +178,58 @@ - - - - - + + + + - + - - - + + + - + - - + + - - - - - - - + + + + + + + - + @@ -192,45 +251,74 @@ - - + + - - - + + + - - - - - - + + + + + + - - - - + + + + - + x:Name="BtnSwitch" Text="实例独立设置" Click="BtnSwitch_Click" + LogoScale="0.9" + Logo="M73 584L920 584 608 896C579 925 579 972 608 1001 637 1030 683 1030 712 1001L1149 565C1164 550 1170 531 1170 511 1170 492 1164 472 1149 457L712 21C683-7 637-7 608 21 579 50 579 97 608 126L920 438 73 438C33 438 0 471 0 511 0 551 33 584 73 584Z" /> - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs new file mode 100644 index 000000000..d8cdb090c --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.cs @@ -0,0 +1,602 @@ +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils.OS; + +namespace PCL; + +public partial class PageSetupLaunch +{ + private bool IsLoad; + + public PageSetupLaunch() + { + InitializeComponent(); + Loaded += PageSetupLaunch_Loaded; + } + + private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + RefreshRam(false); + if (ModMinecraft.McInstanceSelected is null) + BtnSwitch.Visibility = Visibility.Collapsed; + else + BtnSwitch.Visibility = Visibility.Visible; + + // 非重复加载部分 + if (IsLoad) + return; + IsLoad = true; + + ModAnimation.AniControlEnabled += 1; + Reload(); + ModAnimation.AniControlEnabled -= 1; + + // 内存自动刷新 + var timer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) }; + timer.Tick += (_, __) => RefreshRam(); + timer.Start(); + RectRamGame.SizeChanged += (s, e) => RefreshRamText(); + } + + public void Reload() + { + try + { + // 启动参数 + TextArgumentTitle.Text = Config.Launch.Title; + TextArgumentInfo.Text = Config.Launch.TypeInfo; + ComboArgumentIndieV2.SelectedIndex = Config.Launch.IndieSolutionV2; + ComboArgumentVisibie.SelectedIndex = (int)Config.Launch.LauncherVisibility; + ComboArgumentPriority.SelectedIndex = (int)Config.Launch.ProcessPriority; + ComboArgumentWindowType.SelectedIndex = (int)Config.Launch.GameWindowMode; + TextArgumentWindowWidth.Text = Config.Launch.GameWindowWidth.ToString(); + TextArgumentWindowHeight.Text = Config.Launch.GameWindowHeight.ToString(); + CheckArgumentRam.Checked = Config.Launch.OptimizeMemory; + ComboMsAuthType.SelectedIndex = Config.Launch.LoginMsAuthType; + ComboPreferredIpStack.SelectedIndex = (int)Config.Launch.PreferredIpStack; + // CheckArgumentJavaTraversal.Checked = Setup.Get("LaunchArgumentJavaTraversal") + + // 游戏内存 + ((MyRadioBox)FindName( + Conversions.ToString(Operators.ConcatenateObject("RadioRamType", + ModBase.Setup.Load("LaunchRamType"))))) + .Checked = true; + SliderRamCustom.Value = Conversions.ToInteger(Config.Launch.CustomMemorySize); + + // 高级设置 + ComboAdvanceRenderer.SelectedIndex = Config.Launch.Renderer; + TextAdvanceJvm.Text = Config.Launch.JvmArgs; + TextAdvanceGame.Text = Config.Launch.GameArgs; + TextAdvanceRun.Text = Config.Launch.PreLaunchCommand; + CheckAdvanceRunWait.Checked = Config.Launch.PreLaunchCommandWait; + CheckAdvanceDisableRW.Checked = Config.Launch.DisableRw; + CheckAdvanceGraphicCard.Checked = Config.Launch.SetGpuPreference; + CheckAdvanceNoJavaw.Checked = Config.Launch.NoJavaw; + if (ModBase.IsArm64System) + { + CheckAdvanceDisableJLW.Checked = true; + CheckAdvanceDisableJLW.IsEnabled = false; + CheckAdvanceDisableJLW.ToolTip = "在启动游戏时不使用 Java Wrapper 进行包装。 由于系统为 ARM64 架构,Java Wrapper 已被强制禁用。"; + } + else + { + CheckAdvanceDisableJLW.Checked = Config.Launch.DisableJlw; + } + } + + catch (NullReferenceException ex) + { + ModBase.Log(ex, "启动设置项存在异常,已被自动重置", ModBase.LogLevel.Msgbox); + Reset(); + } + catch (Exception ex) + { + ModBase.Log(ex, "重载启动设置时出错", ModBase.LogLevel.Feedback); + } + } + + // 初始化 + public void Reset() + { + try + { + Config.Launch.Reset(); + ModBase.Log("[Setup] 已初始化启动设置"); + ModMain.Hint("已初始化启动设置!", ModMain.HintType.Finish, false); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化启动设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void RadioBoxChange(object senderRaw, ModBase.RouteEventArgs e) + { + var sender = (MyRadioBox)senderRaw; + var gotCfg = sender.Tag?.ToString()?.Split("/") ?? Array.Empty(); + if (ModAnimation.AniControlEnabled == 0 && gotCfg.Length >= 2) + ModBase.Setup.Set(gotCfg[0], int.Parse(gotCfg[1])); + } + + private void TextBoxChange(object senderRaw, RoutedEventArgs e) + { + var sender = (MyTextBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Text); + } + + private void TextArgumentTitle_OnTextChanged(object senderRaw, TextChangedEventArgs e) + { + var sender = (MyTextBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Text); + } + + private void SliderChange(object senderRaw, bool user) + { + var sender = (MySlider)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Value); + } + + private void ComboChange(object senderRaw, SelectionChangedEventArgs e) + { + var sender = (MyComboBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + } + + private void CheckBoxChange(object senderRaw, bool user) + { + var sender = (MyCheckBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Checked); + } + + // 切换到实例独立设置 + private void BtnSwitch_Click(object sender, MouseButtonEventArgs e) + { + ModMinecraft.McInstanceSelected.Load(); + PageInstanceLeft.Instance = ModMinecraft.McInstanceSelected; + ModMain.FrmMain.PageChange(FormMain.PageType.InstanceSetup, FormMain.PageSubType.VersionSetup); + } + + private void ComboAdvanceRenderer_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + ComboChange(sender, e); + ComboAdvanceRenderer_SelectionChanged((MyComboBox)sender, e); + } + + private void ComboArgumentIndie_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + ComboChange(sender, e); + ComboArgumentIndie_SelectionChanged(sender, e); + } + + private void CheckArgumentRam_OnChange(object sender, bool user) + { + CheckBoxChange(sender, user); + CheckArgumentRam_Change(); + } + + #region 游戏内存 + + public void RamType(int Type) + { + if (SliderRamCustom is null) + return; + SliderRamCustom.IsEnabled = Type == 1; + } + + /// + /// 刷新 UI 上的 RAM 显示。 + /// + public void RefreshRam(bool showAnim) + { + if (LabRamGame is null || LabRamUsed is null || ModMain.FrmMain.PageCurrent != FormMain.PageType.Setup || + ModMain.FrmSetupLeft.PageID != FormMain.PageSubType.SetupLaunch) + return; + // 获取内存情况 + var ramGame = Math.Round(GetRam(ModMinecraft.McInstanceSelected, false), 5); + var phyRam = KernelInterop.GetPhysicalMemoryBytes(); + var ramTotal = Math.Round((double)phyRam.Total / 1024 / 1024 / 1024, 1); + var ramAvailable = Math.Round((double)phyRam.Available / 1024 / 1024 / 1024, 1); + var ramGameActual = Math.Round(Math.Min(ramGame, ramAvailable), 5); + var ramUsed = Math.Round(ramTotal - ramAvailable, 5); + var ramEmpty = Math.Round(ModBase.MathClamp(ramTotal - ramUsed - ramGame, 0d, 1000d), 1); + // 设置最大可用内存 + if (ramTotal <= 1.5d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Max(Math.Floor((ramTotal - 0.3d) / 0.1d), 1d)); + else if (ramTotal <= 8d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 1.5d) / 0.5d) + 12d); + else if (ramTotal <= 16d) + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 8d) / 1d) + 25d); + else + SliderRamCustom.MaxValue = (int)Math.Round(Math.Floor((ramTotal - 16d) / 2d) + 33d); + // 设置文本 + LabRamGame.Text = (ramGame == Math.Floor(ramGame) ? ramGame + ".0" : ramGame.ToString()) + " GB" + + (ramGame != ramGameActual + ? " (可用 " + (ramGameActual == Math.Floor(ramGameActual) + ? ramGameActual + ".0" + : ramGameActual.ToString()) + " GB)" + : ""); + LabRamUsed.Text = (ramUsed == Math.Floor(ramUsed) ? ramUsed + ".0" : ramUsed.ToString()) + " GB"; + LabRamTotal.Text = " / " + (ramTotal == Math.Floor(ramTotal) ? ramTotal + ".0" : ramTotal.ToString()) + " GB"; + LabRamWarn.Visibility = + ramGame == 1d && !ModJava.IsGameSet64BitJava() && !ModBase.Is32BitSystem && ModJava.Javas.ExistAnyJava() + ? Visibility.Visible + : Visibility.Collapsed; + HintRamTooHigh.Visibility = ramGame / ramTotal > 0.75d ? Visibility.Visible : Visibility.Collapsed; + if (showAnim) + { + // 宽度动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaGridLengthWidth(ColumnRamUsed, ramUsed - ColumnRamUsed.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaGridLengthWidth(ColumnRamGame, ramGameActual - ColumnRamGame.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)), + ModAnimation.AaGridLengthWidth(ColumnRamEmpty, ramEmpty - ColumnRamEmpty.Width.Value, 800, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Strong)) + }, "SetupLaunch Ram Grid"); + } + else + { + // 宽度设置 + ColumnRamUsed.Width = new GridLength(ramUsed, GridUnitType.Star); + ColumnRamGame.Width = new GridLength(ramGameActual, GridUnitType.Star); + ColumnRamEmpty.Width = new GridLength(ramEmpty, GridUnitType.Star); + } + } + + private void RefreshRam() + { + RefreshRam(true); + } + + private int RamTextLeft = 2; + private int RamTextRight = 1; + + /// + /// 刷新 UI 上的文本位置。 + /// + private void RefreshRamText() + { + // 获取宽度信息 + var RectUsedWidth = RectRamUsed.ActualWidth; + var TotalWidth = PanRamDisplay.ActualWidth; + var LabGameWidth = LabRamGame.ActualWidth; + var LabUsedWidth = LabRamUsed.ActualWidth; + var LabTotalWidth = LabRamTotal.ActualWidth; + var LabGameTitleWidth = LabRamGameTitle.ActualWidth; + var LabUsedTitleWidth = LabRamUsedTitle.ActualWidth; + // 左侧 + int Left; + if (RectUsedWidth - 30d < LabUsedWidth || RectUsedWidth - 30d < LabUsedTitleWidth) + // 全写不下了 + Left = 0; + else if (RectUsedWidth - 25d < LabUsedWidth + LabTotalWidth) + // 显示不下完整数据 + Left = 1; + else + // 正常 + Left = 2; + if (RamTextLeft != Left) + { + RamTextLeft = Left; + switch (Left) + { + case 0: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, -LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, -LabRamUsedTitle.Opacity, 100) + }, "SetupLaunch Ram TextLeft"); + break; + } + case 1: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, -LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100) + }, "SetupLaunch Ram TextLeft"); + break; + } + case 2: + { + ModAnimation.AniStart( + new[] + { + ModAnimation.AaOpacity(LabRamUsed, 1d - LabRamUsed.Opacity, 100), + ModAnimation.AaOpacity(LabRamTotal, 1d - LabRamTotal.Opacity, 100), + ModAnimation.AaOpacity(LabRamUsedTitle, 0.7d - LabRamUsedTitle.Opacity, 100) + }, "SetupLaunch Ram TextLeft"); + break; + } + } + } + + // 右侧 + int Right; + if (TotalWidth < LabGameWidth + 2d + RectUsedWidth || TotalWidth < LabGameTitleWidth + 2d + RectUsedWidth) + // 挤到最右边 + Right = 0; + else + // 正常情况 + Right = 1; + if (Right == 0) + { + if (ModAnimation.AniControlEnabled == 0 && + (RamTextRight != Right || ModAnimation.AniIsRun("SetupLaunch Ram TextRight"))) + { + // 需要动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(LabRamGame, TotalWidth - LabGameWidth - LabRamGame.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaX(LabRamGameTitle, TotalWidth - LabGameTitleWidth - LabRamGameTitle.Margin.Left, + 100, Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "SetupLaunch Ram TextRight"); + } + else + { + // 不需要动画 + ModAnimation.AniStop("SetupLaunch Ram TextRight"); + LabRamGame.Margin = new Thickness(TotalWidth - LabGameWidth, 3d, 0d, 0d); + LabRamGameTitle.Margin = new Thickness(TotalWidth - LabGameTitleWidth, 0d, 0d, 5d); + } + } + else if (ModAnimation.AniControlEnabled == 0 && + (RamTextRight != Right || ModAnimation.AniIsRun("SetupLaunch Ram TextRight"))) + { + // 需要动画 + ModAnimation.AniStart( + new[] + { + ModAnimation.AaX(LabRamGame, 2d + RectUsedWidth - LabRamGame.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)), + ModAnimation.AaX(LabRamGameTitle, 2d + RectUsedWidth - LabRamGameTitle.Margin.Left, 100, + Ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)) + }, "SetupLaunch Ram TextRight"); + } + else + { + // 不需要动画 + ModAnimation.AniStop("SetupLaunch Ram TextRight"); + LabRamGame.Margin = new Thickness(2d + RectUsedWidth, 3d, 0d, 0d); + LabRamGameTitle.Margin = new Thickness(2d + RectUsedWidth, 0d, 0d, 5d); + } + + RamTextRight = Right; + } + + /// + /// 获取当前设置的 RAM 值。单位为 GB。 + /// + public static double GetRam(ModMinecraft.McInstance Version, bool UseVersionJavaSetup, bool? Is32BitJava = default) + { + // ------------------------------------------ + // 修改下方代码时需要一并修改 PageInstanceSetup + // ------------------------------------------ + + var RamGive = default(double); + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Launch.MemoryAllocationMode, 0, false))) + { + // 自动配置 + var RamAvailable = + Math.Round((double)KernelInterop.GetAvailablePhysicalMemoryBytes() / 1024 / 1024 / 1024 * 10) / 10; + // 确定需求的内存值 + double RamMininum; // 无论如何也需要保证的最低限度内存 + double RamTarget1; // 估计能勉强带动了的内存 + double RamTarget2; // 估计没啥问题了的内存 + double RamTarget3; // 放一百万个材质和 Mod 和光影需要的内存 + if (Version is not null && !Version.IsLoaded) + Version.Load(); + if (Version is not null && Version.Modable) + { + // 可安装 Mod 的实例 + var ModDir = new DirectoryInfo(Version.PathIndie + @"mods\"); + var ModCount = ModDir.Exists ? ModDir.GetFiles().Length : 0; + RamMininum = 0.5d + ModCount / 150d; + RamTarget1 = 1.5d + ModCount / 90d; + RamTarget2 = 2.7d + ModCount / 50d; + RamTarget3 = 4.5d + ModCount / 25d; + } + else if (Version is not null && Version.Info.HasOptiFine) + { + // OptiFine 实例 + RamMininum = 0.5d; + RamTarget1 = 1.5d; + RamTarget2 = 3d; + RamTarget3 = 5d; + } + else + { + // 普通实例 + RamMininum = 0.5d; + RamTarget1 = 1.5d; + RamTarget2 = 2.5d; + RamTarget3 = 4d; + } + + double RamDelta; + // 预分配内存,阶段一,0 ~ T1,100% + RamDelta = RamTarget1; + RamGive += Math.Min(RamAvailable, RamDelta); + RamAvailable -= RamDelta; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段二,T1 ~ T2,70% + RamDelta = RamTarget2 - RamTarget1; + RamGive += Math.Min(RamAvailable * 0.7d, RamDelta); + RamAvailable -= RamDelta / 0.7d; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段三,T2 ~ T3,40% + RamDelta = RamTarget3 - RamTarget2; + RamGive += Math.Min(RamAvailable * 0.4d, RamDelta); + RamAvailable -= RamDelta / 0.4d; + if (RamAvailable < 0.1d) + goto PreFin; + // 预分配内存,阶段四,T3 ~ T3 * 2,15% + RamDelta = RamTarget3; + RamGive += Math.Min(RamAvailable * 0.15d, RamDelta); + RamAvailable -= RamDelta / 0.15d; + if (RamAvailable < 0.1d) + goto PreFin; + PreFin: ; + + // 不低于最低值 + RamGive = Math.Round(Math.Max(RamGive, RamMininum), 1); + } + else + { + // 手动配置 + var Value = Conversions.ToInteger(Config.Launch.CustomMemorySize); + if (Value <= 12) + RamGive = Value * 0.1d + 0.3d; + else if (Value <= 25) + RamGive = (Value - 12) * 0.5d + 1.5d; + else if (Value <= 33) + RamGive = (Value - 25) * 1 + 8; + else + RamGive = (Value - 33) * 2 + 16; + } + + // 若使用 32 位 Java,则限制为 1G + if (Is32BitJava ?? !ModJava.IsGameSet64BitJava(UseVersionJavaSetup ? Version : null)) + RamGive = Math.Min(1d, RamGive); + return RamGive; + } + + #endregion + + #region 其他选项 + + private void WindowTypeUIRefresh() + { + if (ComboArgumentWindowType is null) + return; + if (ComboArgumentWindowType.SelectedIndex == 3 && LabArgumentWindowMiddle is not null && + LabArgumentWindowMiddle.Visibility == Visibility.Collapsed) + { + LabArgumentWindowMiddle.Visibility = Visibility.Visible; + TextArgumentWindowHeight.Visibility = Visibility.Visible; + TextArgumentWindowWidth.Visibility = Visibility.Visible; + } + else if (ComboArgumentWindowType.SelectedIndex != 3 && LabArgumentWindowMiddle is not null && + LabArgumentWindowMiddle.Visibility == Visibility.Visible) + { + LabArgumentWindowMiddle.Visibility = Visibility.Collapsed; + TextArgumentWindowHeight.Visibility = Visibility.Collapsed; + TextArgumentWindowWidth.Visibility = Visibility.Collapsed; + } + } + + // 可见性选择直接关闭的警告 + private void ComboArgumentVisibie_SelectionChanged(object sender, SelectionChangedEventArgs sizeChangedEventArgs) + { + ComboChange(sender, sizeChangedEventArgs); + if (ModAnimation.AniControlEnabled != 0) + return; + if (ComboArgumentVisibie.SelectedIndex == 0) + if (ModMain.MyMsgBox( + "若在游戏启动后立即关闭启动器,崩溃检测、更改游戏标题等功能将失效。" + "\r\n" + "如果想保留这些功能,可以选择让启动器在游戏启动后隐藏,游戏退出后自动关闭。", + "提醒", "继续", "取消") == 2) + ComboArgumentVisibie.SelectedItem = sizeChangedEventArgs.RemovedItems[0]; + } + + // 开启自动内存优化的警告 + private void CheckArgumentRam_Change() + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (CheckArgumentRam.Checked == false) + return; + if (ModMain.MyMsgBox( + "内存优化会显著延长启动耗时,建议仅在内存不足时开启。" + "\r\n" + "如果你在使用机械硬盘,这还可能导致一小段时间的严重卡顿。" + + (ProcessInterop.IsAdmin() + ? "" + : $"{"\r\n"}{"\r\n"}每次启动游戏,PCL 都需要申请管理员权限以进行内存优化。{"\r\n"}若想自动授予权限,可以右键 PCL,打开 属性 → 兼容性 → 以管理员身份运行此程序。"), + "提醒", "确定", "取消") == 2) CheckArgumentRam.Checked = false; + } + + // 实例隔离提示 + private void ComboArgumentIndie_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + ModMain.MyMsgBox("默认策略只会对今后新安装的实例生效。" + "\r\n" + "已有实例的隔离策略需要在它的设置中调整。"); + } + + #endregion + + #region 高级设置 + + private void TextAdvanceRun_TextChanged(object sender, TextChangedEventArgs e) + { + CheckAdvanceRunWait.Visibility = + string.IsNullOrEmpty(TextAdvanceRun.Text) ? Visibility.Collapsed : Visibility.Visible; + } + + // JVM 参数重设 + private void TextAdvanceJvm_TextChanged(object sender, TextChangedEventArgs e) + { + BtnAdvanceJvmReset.Visibility = + Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(TextAdvanceJvm.Text, + ModBase.Setup.GetDefault("LaunchAdvanceJvm"), false)) + ? Visibility.Hidden + : Visibility.Visible; + } + + private void BtnAdvanceJvmReset_Click(object sender, EventArgs e) + { + ModBase.Setup.Reset("LaunchAdvanceJvm"); + Reload(); + } + + private void ComboAdvanceRenderer_SelectionChanged(MyComboBox sender, object e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (!Conversions.ToBoolean(States.Hint.Renderer) && ComboAdvanceRenderer.SelectedIndex != 0) + { + if (ModMain.MyMsgBox("修改此项会严重影响游戏的稳定性与性能。如果你不知道你在做什么,不要修改此选项!" + "\r\n" + "你确定要继续修改吗?", "警告", + "我知道我在做什么", "取消", IsWarn: true) == 2) + { + ComboAdvanceRenderer.SelectedItem = ((SelectionChangedEventArgs)e).RemovedItems[0]; + } + else + { + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex); + States.Hint.Renderer = true; + } + } + else + { + ModBase.Setup.Set(Conversions.ToString(sender.Tag), sender.SelectedIndex); + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml index f13e6f8e2..8e8eb9372 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml @@ -1,8 +1,9 @@ @@ -23,18 +24,27 @@ - - + + - - - - + + + + @@ -43,11 +53,16 @@ - + - - + + @@ -57,31 +72,18 @@ - + - - + + - - @@ -95,8 +97,12 @@ - - + + @@ -104,32 +110,46 @@ - - - + + + - + - - - - - + + + + + - - - - - + + + + + - - - - - - - + + + + + + + @@ -142,7 +162,8 @@ - + @@ -150,12 +171,20 @@ - - - + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml.cs new file mode 100644 index 000000000..1eb8485ca --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLauncherMisc.xaml.cs @@ -0,0 +1,228 @@ +using System.Diagnostics; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.App.Configuration; +using PCL.Core.UI; + +namespace PCL; + +public partial class PageSetupLauncherMisc +{ + private bool IsFirstLoad = true; + + private new bool IsLoaded; + + public PageSetupLauncherMisc() + { + InitializeComponent(); + Loaded += PageSetupLink_Loaded; + Loaded += (_, __) => Reload(); + } + + private void PageSetupLink_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + ModAnimation.AniControlEnabled += 1; + SliderLoad(); + Reload(); + ModAnimation.AniControlEnabled -= 1; + } + + public void Reload() + { + // 系统设置 + ComboSystemActivity.SelectedIndex = States.System.AnnounceSolution; + CheckSystemDisableHardwareAcceleration.Checked = Config.System.DisableHardwareAcceleration; + SliderAniFPS.Value = Config.System.AnimationFpsLimit; + SliderMaxLog.Value = Config.System.MaxGameLog; + CheckSystemTelemetry.Checked = Config.System.Telemetry; + + // 网络 + TextSystemHttpProxy.Text = Config.Network.HttpProxy.CustomAddress; + TextSystemHttpProxyCustomUsername.Text = Config.Network.HttpProxy.CustomUsername; + TextSystemHttpProxyCustomPassword.Text = Config.Network.HttpProxy.CustomPassword; + ((MyRadioBox)FindName($"RadioHttpProxyType{Config.Network.HttpProxy.Type}")).SetChecked(true, false); + CheckNetDohEnable.Checked = Config.Network.EnableDoH; + + // 调试选项 + CheckDebugMode.Checked = Config.Debug.Enabled; + SliderDebugAnim.Value = Config.Debug.AnimationSpeed; + CheckDebugDelay.Checked = Config.Debug.DontCopy; + } + + // 初始化 + public void Reset() + { + try + { + Config.Network.Reset(); + Config.Debug.Reset(); + Config.System.Reset(); + ModBase.Log("[Setup] 已初始化启动器-杂项页设置"); + ModMain.Hint("已初始化杂项页设置!", ModMain.HintType.Finish, false); + Reload(); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化启动器-杂项页设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void ComboChange(object senderRaw, SelectionChangedEventArgs e) + { + var sender = (MyComboBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + } + + private void RadioBoxChange(object senderRaw, ModBase.RouteEventArgs e) + { + var sender = (MyRadioBox)senderRaw; + var gotCfg = sender.Tag?.ToString()?.Split("/") ?? Array.Empty(); + if (ModAnimation.AniControlEnabled == 0 && gotCfg.Length >= 2) + ModBase.Setup.Set(gotCfg[0], int.Parse(gotCfg[1])); + } + + private void CheckBoxChange(object senderRaw, bool user) + { + var sender = (MyCheckBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Checked); + } + + private void SliderChange(object senderRaw, bool user) + { + var sender = (MySlider)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Value); + } + + // 网络 + private void ApplyHttpProxyBtn_OnClicked(object sender, MouseButtonEventArgs e) + { + Config.Network.HttpProxy.CustomAddress = TextSystemHttpProxy.Text; + Config.Network.HttpProxy.CustomUsername = TextSystemHttpProxyCustomUsername.Text; + Config.Network.HttpProxy.CustomPassword = TextSystemHttpProxyCustomPassword.Text; + } + + // 滑动条 + private void SliderLoad() + { + SliderDebugAnim.GetHintText = new Func(v => + (int)v > 29 + ? "关闭" + : Math.Round(Convert.ToDouble(v) / 10 + 0.1d, 1) + "x"); + SliderAniFPS.GetHintText = new Func(v => $"{Operators.AddObject(v, 1)} FPS"); + // y = 10x + 50 (0 <= x <= 5, 50 <= y <= 100) + // y = 50x - 150 (5 < x <= 13, 100 < y <= 500) + // y = 100x - 800 (13 < x <= 28, 500 < y <= 2000) + SliderMaxLog.GetHintText = new Func(v => + { + switch (v) + { + case var @case when Operators.ConditionalCompareObjectLessEqual(@case, 5, false): + { + return Operators.AddObject(Operators.MultiplyObject(v, 10), 50); + } + case var case1 when Operators.ConditionalCompareObjectLessEqual(case1, 13, false): + { + return Operators.SubtractObject(Operators.MultiplyObject(v, 50), 150); + } + case var case2 when Operators.ConditionalCompareObjectLessEqual(case2, 28, false): + { + return Operators.SubtractObject(Operators.MultiplyObject(v, 100), 800); + } + default: + { + return "无限制"; + } + } + }); + } + + // 硬件加速 + private void Check_DisableHardwareAcceleration(object _, bool __) + { + ModMain.Hint("此项变更将在重启 PCL 后生效"); + } + + // 调试模式 + private void CheckDebugMode_Change(object _, bool __) + { + if (ModAnimation.AniControlEnabled == 0) + ModMain.Hint("部分调试信息将在刷新或启动器重启后切换显示!", Log: false); + } + + // 自动更新 + private void ComboSystemActivity_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (ComboSystemActivity.SelectedIndex != 2) + return; + if (ModMain.MyMsgBox( + "若选择此项,即使在将来出现严重问题时,你也无法获取相关通知。" + "\r\n" + "例如,如果发现某个版本游戏存在严重 Bug,你可能就会因为无法得到通知而导致无法预知的后果。" + + "\r\n" + "\r\n" + "一般选择 仅在有重要通知时显示公告 就可以让你尽量不受打扰了。" + "\r\n" + + "除非你在制作服务器整合包,或时常手动更新启动器,否则极度不推荐选择此项!", "警告", "我知道我在做什么", "取消", IsWarn: true) == + 2) ComboSystemActivity.SelectedItem = e.RemovedItems[0]; + } + + private void CheckDebugMode_OnChange(object sender, bool user) + { + CheckBoxChange(sender, user); + CheckDebugMode_Change(sender, user); + } + + private void CheckSystemDisableHardwareAcceleration_OnChange(object sender, bool user) + { + CheckBoxChange(sender, user); + Check_DisableHardwareAcceleration(sender, user); + } + + private void ComboSystemActivity_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + ComboChange(sender, e); + ComboSystemActivity_SelectionChanged(sender, e); + } + + #region 导出 / 导入设置 + + private void BtnSystemSettingExp_Click(object sender, MouseButtonEventArgs e) + { + var savePath = + SystemDialogs.SelectSaveFile("选择保存位置", "PCL 全局配置.json", "PCL 配置文件(*.json)|*.json", ModBase.ExePath); + if (string.IsNullOrWhiteSpace(savePath)) + return; + File.Copy(ConfigService.SharedConfigPath, savePath, true); + ModMain.Hint("配置导出成功!", ModMain.HintType.Finish); + ModBase.OpenExplorer(savePath); + } + + private void BtnSystemSettingImp_Click(object sender, MouseButtonEventArgs e) + { + var sourcePath = SystemDialogs.SelectFile("PCL 配置文件(*.json)|*.json", "选择配置文件"); + if (string.IsNullOrWhiteSpace(sourcePath)) + return; + File.Copy(sourcePath, ConfigService.SharedConfigPath, true); + ModMain.MyMsgBox("配置导入成功!请重启 PCL 以应用配置……", Button1: "重启", ForceWait: true); + Process.Start(new ProcessStartInfo(ModBase.ExePathWithName)); + FormMain.EndProgramForce(); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml index e8bbc74b1..ba27137c3 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml @@ -1,87 +1,140 @@  - + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="PCL.PageSetupLeft" + d:DesignWidth="152"> + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + + + - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.cs new file mode 100644 index 000000000..776270f28 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLeft.xaml.cs @@ -0,0 +1,356 @@ +using System.Windows; +using System.Windows.Controls; +using Microsoft.VisualBasic; +using PCL.Core.App; + +namespace PCL; + +public partial class PageSetupLeft +{ + private bool IsLoad; + private bool IsPageSwitched; // 如果在 Loaded 前切换到其他页面,会导致触发 Loaded 时再次切换一次 + + private void PageSetupLeft_Loaded(object sender, RoutedEventArgs e) + { + // 是否处于隐藏的子页面 + var IsHiddenPage = false; + var hide = Config.Preference.Hide; + + if (ItemLaunch.Checked && hide.SetupLaunch) + IsHiddenPage = true; + if (ItemUI.Checked && hide.SetupUi) + IsHiddenPage = true; + if (ItemGameManage.Checked && hide.SetupGameManage) + IsHiddenPage = true; + if (ItemAbout.Checked && Config.Preference.Hide.SetupAbout) + IsHiddenPage = true; + if (PageSetupUI.HiddenForceShow) + IsHiddenPage = false; + // 若页面错误,或尚未加载,则继续 + if (IsLoad && !IsHiddenPage) + return; + IsLoad = true; + // 刷新子页面隐藏情况 + PageSetupUI.HiddenRefresh(); + // 选择第一个未被禁用的子页面 + if (IsPageSwitched) + return; + var hideCfg = Config.Preference.Hide; + if (!hideCfg.SetupLaunch) + ItemLaunch.SetChecked(true, false, false); + else if (!hideCfg.SetupUi) + ItemUI.SetChecked(true, false, false); + else if (!hideCfg.SetupGameManage) + ItemGameManage.SetChecked(true, false, false); + else if (!hideCfg.SetupLauncherMisc) + ItemLauncherMisc.SetChecked(true, false, false); + else if (!hideCfg.SetupUpdate) + ItemUpdate.SetChecked(true, false, false); + else if (!hideCfg.SetupAbout) + ItemAbout.SetChecked(true, false, false); + else if (!hideCfg.SetupFeedback) + ItemFeedback.SetChecked(true, false, false); + else if (!hideCfg.SetupGameLink) + ItemGameLink.SetChecked(true, false, false); + else if (!hideCfg.SetupJava) + ItemJava.SetChecked(true, false, false); + else if (!hideCfg.SetupLog) + ItemLog.SetChecked(true, false, false); + else + ItemLaunch.SetChecked(true, false, false); + } + + private void PageOtherLeft_Unloaded(object sender, RoutedEventArgs e) + { + IsPageSwitched = false; + } + + public void Reset(object sender, EventArgs e) + { + switch (ModBase.Val(((MyIconButton)sender).Tag)) + { + case (double)FormMain.PageSubType.SetupLaunch: + { + if (ModMain.MyMsgBox("是否要初始化 游戏-启动 页面的所有设置?该操作不可撤销。", "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmSetupLaunch is null) + ModMain.FrmSetupLaunch = new PageSetupLaunch(); + ModMain.FrmSetupLaunch.Reset(); + ItemLaunch.Checked = true; + } + + break; + } + case (double)FormMain.PageSubType.SetupUI: + { + if (ModMain.MyMsgBox("是否要初始化 启动器-个性化 页面的所有设置?该操作不可撤销。" + "\r\n" + "(背景图片与音乐、主页等外部文件不会被删除)", + "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmSetupUI is null) + ModMain.FrmSetupUI = new PageSetupUI(); + ModMain.FrmSetupUI.Reset(); + ItemUI.Checked = true; + } + + break; + } + case (double)FormMain.PageSubType.SetupGameManage: + { + if (ModMain.MyMsgBox("是否要初始化 游戏-管理 页面的所有设置?该操作不可撤销。", "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmSetupGameManage is null) + ModMain.FrmSetupGameManage = new PageSetupGameManage(); + ModMain.FrmSetupGameManage.Reset(); + ItemGameManage.Checked = true; + } + + break; + } + case (double)FormMain.PageSubType.SetupGameLink: + { + if (ModMain.MyMsgBox("是否要初始化 工具-联机 页面的所有设置?该操作不可撤销。", "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmSetupGameLink is null) + ModMain.FrmSetupGameLink = new PageSetupGameLink(); + ModMain.FrmSetupGameLink.Reset(); + ItemGameLink.Checked = true; + } + + break; + } + case (double)FormMain.PageSubType.SetupLauncherMisc: + { + if (ModMain.MyMsgBox("是否要初始化 启动器-杂项 页面的所有设置?该操作不可撤销。", "初始化确认", Button2: "取消", IsWarn: true) == 1) + { + if (ModMain.FrmSetupLauncherMisc is null) + ModMain.FrmSetupLauncherMisc = new PageSetupLauncherMisc(); + ModMain.FrmSetupLauncherMisc.Reset(); + ItemLauncherMisc.Checked = true; + } + + break; + } + } + } + + public static void TryFeedback() // Handles ItemFeedback.Click + { + ModBase.RunInNewThread(() => + { + if (!ModBase.CanFeedback(true)) + return; + switch (ModMain.MyMsgBox("在提交新反馈前,建议先搜索反馈列表,以避免重复提交。" + "\r\n" + "如果无法打开该网页,请尝试使用加速器或 VPN。", "反馈", + "提交新反馈", "查看反馈列表", "取消")) + { + case 1: + { + ModBase.Feedback(); + break; + } + case 2: + { + ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/issues/"); + break; + } + } + }); + } + + public void Refresh(object sender, EventArgs e) // 由边栏按钮匿名调用 + { + switch (ModBase.Val(((MyIconButton)sender).Tag)) + { + case (double)FormMain.PageSubType.SetupFeedback: + { + if (ModMain.FrmSetupFeedback is not null) ModMain.FrmSetupFeedback.Loader.Start(IsForceRestart: true); + ItemFeedback.Checked = true; + break; + } + case (double)FormMain.PageSubType.SetupJava: + { + if (ModMain.FrmSetupJava is not null) ModMain.FrmSetupJava.Loader.Start(IsForceRestart: true); + ItemJava.Checked = true; + break; + } + } + + ModMain.Hint("正在刷新……", Log: false); + } + + #region 页面切换 + + /// + /// 当前页面的编号。从左往右从 0 开始计算。 + /// + public FormMain.PageSubType PageID; + + public PageSetupLeft() + { + InitializeComponent(); + // 选择第一个未被禁用的子页面 + var hideCfg = Config.Preference.Hide; + if (!hideCfg.SetupLaunch) + PageID = FormMain.PageSubType.SetupLaunch; + else if (!hideCfg.SetupUi) + PageID = FormMain.PageSubType.SetupUI; + else if (!hideCfg.SetupGameManage) + PageID = FormMain.PageSubType.SetupGameManage; + else if (!hideCfg.SetupLauncherMisc) + PageID = FormMain.PageSubType.SetupLauncherMisc; + else if (!hideCfg.SetupUpdate) + PageID = FormMain.PageSubType.SetupUpdate; + else if (!hideCfg.SetupAbout) + PageID = FormMain.PageSubType.SetupAbout; + else if (!hideCfg.SetupFeedback) + PageID = FormMain.PageSubType.SetupFeedback; + else if (!hideCfg.SetupGameLink) + PageID = FormMain.PageSubType.SetupGameLink; + else if (!hideCfg.SetupJava) + PageID = FormMain.PageSubType.SetupJava; + else if (!hideCfg.SetupLog) + PageID = FormMain.PageSubType.SetupLog; + else + PageID = FormMain.PageSubType.SetupLaunch; + AnimatedControl = PanItem; + Loaded += PageSetupLeft_Loaded; + Unloaded += PageOtherLeft_Unloaded; + } + + /// + /// 勾选事件改变页面。 + /// + private void PageCheck(object senderRaw, ModBase.RouteEventArgs e) + { + var sender = (MyRadioBox)senderRaw; + // 尚未初始化控件属性时,sender.Tag 为 Nothing,会跳过切换,且由于 PageID 默认为 0 而切换到第一个页面 + // 若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False) + if (sender.Tag is not null) + PageChange((FormMain.PageSubType)ModBase.Val(sender.Tag)); + } + + /// + /// 获取当前导航指定的右页面。 + /// + public object PageGet(FormMain.PageSubType? ID = null) + { + var targetID = ID ?? PageID; + switch (ID) + { + case FormMain.PageSubType.SetupLaunch: + { + if (ModMain.FrmSetupLaunch is null) + ModMain.FrmSetupLaunch = new PageSetupLaunch(); + return ModMain.FrmSetupLaunch; + } + case FormMain.PageSubType.SetupUI: + { + if (ModMain.FrmSetupUI is null) + ModMain.FrmSetupUI = new PageSetupUI(); + return ModMain.FrmSetupUI; + } + case FormMain.PageSubType.SetupGameManage: + { + if (ModMain.FrmSetupGameManage is null) + ModMain.FrmSetupGameManage = new PageSetupGameManage(); + return ModMain.FrmSetupGameManage; + } + case FormMain.PageSubType.SetupUpdate: + { + if (ModMain.FrmSetupUpdate is null) + ModMain.FrmSetupUpdate = new PageSetupUpdate(); + return ModMain.FrmSetupUpdate; + } + case FormMain.PageSubType.SetupAbout: + { + if (ModMain.FrmSetupAbout is null) + ModMain.FrmSetupAbout = new PageSetupAbout(); + return ModMain.FrmSetupAbout; + } + case FormMain.PageSubType.SetupLog: + { + if (ModMain.FrmSetupLog is null) + ModMain.FrmSetupLog = new PageSetupLog(); + return ModMain.FrmSetupLog; + } + case FormMain.PageSubType.SetupFeedback: + { + if (ModMain.FrmSetupFeedback is null) + ModMain.FrmSetupFeedback = new PageSetupFeedback(); + return ModMain.FrmSetupFeedback; + } + case FormMain.PageSubType.SetupGameLink: + { + if (ModMain.FrmSetupGameLink is null) + ModMain.FrmSetupGameLink = new PageSetupGameLink(); + return ModMain.FrmSetupGameLink; + } + case FormMain.PageSubType.SetupLauncherMisc: + { + if (ModMain.FrmSetupLauncherMisc is null) + ModMain.FrmSetupLauncherMisc = new PageSetupLauncherMisc(); + return ModMain.FrmSetupLauncherMisc; + } + case FormMain.PageSubType.SetupJava: + { + if (ModMain.FrmSetupJava is null) + ModMain.FrmSetupJava = new PageSetupJava(); + return ModMain.FrmSetupJava; + } + + default: + { + throw new Exception("未知的设置子页面种类:" + (int)ID); + } + } + } + + /// + /// 切换现有页面。 + /// + public void PageChange(FormMain.PageSubType ID) + { + if (PageID == ID) + return; + ModAnimation.AniControlEnabled += 1; + IsPageSwitched = true; + try + { + PageChangeRun((MyPageRight)PageGet(ID)); + PageID = ID; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换分页面失败(ID " + (int)ID + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private static void PageChangeRun(MyPageRight Target) + { + ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + if (Target.Parent is not null) + Target.SetValue(ContentPresenter.ContentProperty, null); + ModMain.FrmMain.PageRight = Target; + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnExit(); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnForceExit(); + ModMain.FrmMain.PanMainRight.Child = ModMain.FrmMain.PageRight; + ModMain.FrmMain.PageRight.Opacity = 0d; + }, 130), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + ModMain.FrmMain.PageRight.Opacity = 1d; + ModMain.FrmMain.PageRight.PageOnEnter(); + }, 30, true) + }, "PageLeft PageChange"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml index 5448b01c9..2fc6c4591 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml @@ -1,28 +1,29 @@ - + - - - - + + + + - - - + - + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml.cs new file mode 100644 index 000000000..53728a13e --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupLog.xaml.cs @@ -0,0 +1,159 @@ +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Windows; +using System.Windows.Input; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Logging; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Core.Utils.Exts; + +namespace PCL; + +public partial class PageSetupLog +{ + public PageSetupLog() + { + InitializeComponent(); + Loaded += PageOtherLog_Loaded; + } + + private static string LogDirectory => LogService.Logger.Configuration.StoreFolder; + + private static List CurrentLogs + { + get + { + var logs = LogService.Logger.CurrentLogFiles; + return logs.Select(item => Path.GetFullPath(Conversions.ToString(item))).ToList(); + } + } + + private void PageOtherLog_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + LoadList(); + // 非重复加载部分 + if (IsLoaded) + return; + } + + public void LoadList() + { + PanList.Children.Clear(); + var current = CurrentLogs; + var logFiles = Directory.GetFiles(LogDirectory).OrderByDescending(f => File.GetLastWriteTime(f)).ToArray(); + foreach (var item in logFiles) + { + var fullPath = Path.GetFullPath(item); + var title = Path.GetFileName(item); + if (title.StartsWith("Launch")) + { + title = title.Substring(7, title.Length - 11); + DateTime dt; + var r = DateTime.TryParseExact(title, "yyyy-M-d-HHmmssfff", CultureInfo.InvariantCulture, + DateTimeStyles.None, out dt); + if (r) + title = dt.ToString("yyyy 年 M 月 d 日 HH:mm:ss.fff"); + if (current.Any(log => log.Equals(fullPath))) + title = title + " (当前)"; + } + else if (title.StartsWith("LastPending")) + { + title = title.Substring(11, title.Length - 15); + if (title.Length > 1) + title = "临时存储的日志 (" + title.Substring(1) + ")"; + else + title = "临时存储的未输出日志"; + } + + var ele = new MyListItem + { + Type = MyListItem.CheckType.Clickable, + Title = title, + Info = fullPath, + Tag = fullPath + }; + ele.Click += (sender, e) => + { + var s = (MyListItem)sender; + var file = Conversions.ToString(s.Tag); + Basics.OpenPath(file); + }; + PanList.Children.Add(ele); + } + } + + private static void ExportLog(IEnumerable sourceFiles) + { + const string filter = "PCL CE 日志压缩包|*.zip"; + var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + var baseName = "PCL_CE_Logs_" + DateTime.Now.ToString("yyyyMMddHHmmss"); + var tempDirName = baseName + ".tmp"; + var fileName = baseName + ".zip"; + var selectedPath = SystemDialogs.SelectSaveFile("导出日志文件", fileName, filter, desktopPath); + if (string.IsNullOrEmpty(selectedPath)) + return; + try + { + Directory.CreateDirectory(tempDirName); + if (File.Exists(selectedPath)) + File.Delete(selectedPath); + using (var zip = ZipFile.Open(selectedPath, ZipArchiveMode.Create)) + { + foreach (var item in sourceFiles) + { + var itemFileName = Path.GetFileName(item); + var tempPath = Path.Combine(tempDirName, itemFileName); + File.Copy(item, tempPath); + zip.CreateEntryFromFile(tempPath, itemFileName, CompressionLevel.Fastest); + File.Delete(tempPath); + } + } + + ModMain.Hint("日志保存成功!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "日志保存失败", ModBase.LogLevel.Hint); + } + finally + { + if (Directory.Exists(tempDirName)) + Directory.Delete(tempDirName); + } + } + + private void ButtonOpenDir_OnClick(object sender, MouseButtonEventArgs e) + { + Basics.OpenPath(LogDirectory); + } + + private void ButtonClean_OnClick(object sender, MouseButtonEventArgs e) + { + var r = ModMain.MyMsgBox("是否删除所有历史日志?", "清理历史日志", "确定", "取消", IsWarn: true); + if (r != 1) + return; + var currentSet = new HashSet(CurrentLogs); + foreach (var item in Directory.GetFiles(LogDirectory)) + if (!currentSet.Contains(item)) + File.Delete(item); + ModMain.Hint("清理日志文件成功!", ModMain.HintType.Finish); + LoadList(); + } + + private void ButtonExportAll_OnClick(object sender, MouseButtonEventArgs e) + { + ExportLog(Directory.GetFiles(LogDirectory)); + } + + private void ButtonExport_OnClick(object sender, MouseButtonEventArgs e) + { + var pendingLogs = Array.FindAll(Directory.GetFiles(LogDirectory), + s => s.IsMatch(RegexPatterns.LastPendingLogPath)); + ExportLog(CurrentLogs.Concat(pendingLogs)); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml index ec3209505..40baed3b4 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml @@ -2,8 +2,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PCL" - xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - mc:Ignorable="d" x:Class="PageSetupUI" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" x:Class="PCL.PageSetupUI" DataContext="{Binding RelativeSource={RelativeSource Self}}" PanScroll="{Binding ElementName=PanBack}"> @@ -24,16 +25,31 @@ - - - - - - - - - - + + + + + + + + + + @@ -49,33 +65,83 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + - - + + @@ -87,26 +153,33 @@ - - - - - - + + + + + - - - - - - - + + + + - - - + + + - + @@ -119,17 +192,27 @@ - - - - - - - - - + + + + + + + + @@ -140,18 +223,20 @@ - - + + - - - + + + - + - + @@ -161,8 +246,10 @@ - - + + @@ -180,7 +267,8 @@ - + @@ -188,19 +276,31 @@ - + - - + + - - - + + + @@ -212,15 +312,21 @@ - + - - - - - + + + + + @@ -228,9 +334,15 @@ - - - + + + @@ -244,27 +356,35 @@ - - - - + + + + - + - + - - + + @@ -278,13 +398,18 @@ - - - - + + + + - @@ -292,9 +417,15 @@ - - - + + + @@ -302,7 +433,8 @@ - + @@ -315,9 +447,10 @@ - - - + + + @@ -330,16 +463,20 @@ - + - + - - + + - - + + @@ -368,41 +505,96 @@ - - - + + + - - - - - - - - - - + + + + + + + + + + - - - + + + - - - - - - - - - + + + + + + + + + - - - + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.cs new file mode 100644 index 000000000..a305117f7 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.cs @@ -0,0 +1,1025 @@ +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.UI; +using PCL.Core.Utils; + +namespace PCL; + +public partial class PageSetupUI +{ + public readonly string[] ThemeColors = new[] { "天空蓝", "龙猫蓝", "死机蓝" }; + + public new bool IsLoaded; + + public PageSetupUI() + { + InitializeComponent(); + // 还是石山控件,不支持 ItemsSource Binding,虽然龙猫确实就没考虑 MVVM + // 或者说,支持了一半(内容用了原生的 ComboBoxItem 而不是自定义的 MyComboBoxItem) + foreach (var color in ThemeColors) + { + ComboLightColor.Items.Add(new MyComboBoxItem { Content = color }); + ComboDarkColor.Items.Add(new MyComboBoxItem { Content = color }); + } + + Loaded += PageSetupUI_Loaded; + Loaded += (_, __) => HiddenRefresh(); + } + + private void PageSetupUI_Loaded(object sender, RoutedEventArgs e) + { + // 重复加载部分 + PanBack.ScrollToHome(); + ModSecret.ThemeCheckAll(true); + + if (ModSecret.ThemeDontClick != 0) + { + string NewText; + switch (ModSecret.ThemeDontClick) + { + case 1: + { + NewText = "眼瞎白"; + break; + } + case 2: + { + NewText = "真·滑稽彩"; + break; + } + + default: + { + NewText = "???"; + break; + } + } + + foreach (var Control in PanLauncherTheme.Children) + if (Control is MyRadioBox && ((MyRadioBox)Control).IsEnabled) + ((MyRadioBox)Control).Text = NewText; + } + + /* TODO ERROR: Skipped IfDirectiveTrivia + #If DEBUG Then + */ + if (ModSecret.EnableCustomTheme) + { + LabLauncherDelta.Visibility = Visibility.Visible; + SliderLauncherDelta.Visibility = Visibility.Visible; + LabLauncherLight.Visibility = Visibility.Visible; + SliderLauncherLight.Visibility = Visibility.Visible; + } + + /* TODO ERROR: Skipped EndIfDirectiveTrivia + #End If + */ + ModAnimation.AniControlEnabled += 1; + Reload(); // #4826,在每次进入页面时都刷新一下 + ModAnimation.AniControlEnabled -= 1; + + // 非重复加载部分 + if (IsLoaded) + return; + IsLoaded = true; + + SliderLoad(); + + PanLauncherHide.Visibility = Visibility.Visible; + + // 设置解锁 + + if (!RadioLauncherTheme8.IsEnabled) + LabLauncherTheme8Copy.ToolTip = "社区版不包含主题功能,请使用官方快照版"; + RadioLauncherTheme8.ToolTip = "社区版不包含主题功能,请使用官方快照版"; + if (!RadioLauncherTheme9.IsEnabled) + LabLauncherTheme9Copy.ToolTip = "社区版不包含主题功能,请使用官方快照版"; + RadioLauncherTheme9.ToolTip = "社区版不包含主题功能,请使用官方快照版"; + // 极客蓝的处理在 ThemeCheck 中 + } + + public void Reload() + { + try + { + // 启动器 + SliderLauncherOpacity.Value = Conversions.ToInteger(Config.Preference.Theme.WindowOpacity); + SliderLauncherHue.Value = Conversions.ToInteger(Config.Preference.Theme.WindowHue); + SliderLauncherSat.Value = Conversions.ToInteger(Config.Preference.Theme.WindowSat); + SliderLauncherDelta.Value = Conversions.ToInteger(Config.Preference.Theme.WindowDelta); + SliderLauncherLight.Value = Conversions.ToInteger(Config.Preference.Theme.WindowLight); + // If Setup.Get("UiLauncherTheme") <= 14 Then CType(FindName("RadioLauncherTheme" & Setup.Get("UiLauncherTheme")), MyRadioBox).Checked = True + CheckLauncherLogo.Checked = (bool?)Config.Preference.ShowStartupLogo; + ComboDarkMode.SelectedIndex = Conversions.ToInteger(Config.Preference.Theme.ColorMode); + ComboDarkColor.SelectedIndex = Conversions.ToInteger(Config.Preference.Theme.DarkColor); + ComboLightColor.SelectedIndex = Conversions.ToInteger(Config.Preference.Theme.LightColor); + CheckShowLaunchingHint.Checked = (bool?)Config.Preference.ShowLaunchingHint; + + // 字体设置 + ComboUiFont.SelectedFontTag = Conversions.ToString(Config.Preference.Font); + ComboUiMotdFont.SelectedFontTag = Conversions.ToString(Config.Preference.MotdFont); + + CheckBlur.Checked = (bool?)Config.Preference.Blur.IsEnabled; + SliderBlurValue.Value = Conversions.ToInteger(Config.Preference.Blur.Radius); + SliderBlurSamplingRate.Value = Conversions.ToInteger(Config.Preference.Blur.SamplingRate); + ComboBlurType.SelectedIndex = Conversions.ToInteger(Config.Preference.Blur.KernelType); + PanBlurValue.Visibility = CheckBlur.Checked == true ? Visibility.Visible : Visibility.Collapsed; + CheckLockWindowSize.Checked = (bool?)Config.Preference.LockWindowSize; + + // 背景图片 + SliderBackgroundOpacity.Value = Conversions.ToInteger(Config.Preference.Background.WallpaperOpacity); + SliderBackgroundBlur.Value = Conversions.ToInteger(Config.Preference.Background.WallpaperBlurRadius); + ComboBackgroundSuit.SelectedIndex = Conversions.ToInteger(Config.Preference.Background.WallpaperSuitMode); + CheckBackgroundColorful.Checked = (bool?)Config.Preference.Background.BackgroundColorful; + var autoPauseVideo = Config.Preference.Background.AutoPauseVideo; + CheckAutoPauseVideo.Checked = (bool?)autoPauseVideo; + if (ModVideoBack.IsGaming) + if (Conversions.ToBoolean(Operators.ConditionalCompareObjectEqual(autoPauseVideo, true, false))) + BtnBackgroundRefresh.IsEnabled = false; + + BackgroundRefresh(false, false); + + // 标题栏 + ((MyRadioBox)FindName( + Conversions.ToString(Operators.ConcatenateObject("RadioLogoType", + Config.Preference.WindowTitleType)))) + .Checked = true; + CheckLogoLeft.Visibility = RadioLogoType0.Checked ? Visibility.Visible : Visibility.Collapsed; + PanLogoText.Visibility = RadioLogoType2.Checked ? Visibility.Visible : Visibility.Collapsed; + PanLogoChange.Visibility = RadioLogoType3.Checked ? Visibility.Visible : Visibility.Collapsed; + TextLogoText.Text = Conversions.ToString(Config.Preference.LogoCustomText); + CheckLogoLeft.Checked = (bool?)Config.Preference.TopBarLeftAlign; + + // 背景音乐 + CheckMusicRandom.Checked = (bool?)Config.Preference.Music.ShufflePlayback; + CheckMusicAuto.Checked = (bool?)Config.Preference.Music.StartOnStartup; + CheckMusicStop.Checked = (bool?)Config.Preference.Music.StopInGame; + CheckMusicStart.Checked = (bool?)Config.Preference.Music.StartInGame; + CheckMusicSMTC.Checked = (bool?)Config.Preference.Music.EnableSMTC; + SliderMusicVolume.Value = Conversions.ToInteger(Config.Preference.Music.Volume); + MusicRefreshUI(); + + // 主页 + try + { + ComboCustomPreset.SelectedIndex = Conversions.ToInteger(Config.Preference.Homepage.SelectedPreset); + } + catch + { + ModBase.Setup.Reset("UiCustomPreset"); + } + + ((MyRadioBox)FindName(Conversions.ToString(Operators.ConcatenateObject("RadioCustomType", + ModBase.Setup.Load("UiCustomType", true))))).Checked = true; + TextCustomNet.Text = Conversions.ToString(Config.Preference.Homepage.CustomUrl); + + // 功能隐藏 + // 获取配置组引用 + var uiHidden = Config.Preference.Hide; + + // 主页面 + CheckHiddenPageDownload.Checked = uiHidden.PageDownload; + CheckHiddenPageSetup.Checked = uiHidden.PageSetup; + CheckHiddenPageTools.Checked = uiHidden.PageTools; + + // 子页面 设置 + CheckHiddenSetupLaunch.Checked = uiHidden.SetupLaunch; + CheckHiddenSetupUI.Checked = uiHidden.SetupUi; + CheckHiddenSetupGameManage.Checked = uiHidden.SetupGameManage; + CheckHiddenSetupJava.Checked = uiHidden.SetupJava; + CheckHiddenLauncherMisc.Checked = uiHidden.SetupLauncherMisc; + CheckHiddenSetupUpdate.Checked = uiHidden.SetupUpdate; + CheckHiddenSetupGameLink.Checked = uiHidden.SetupGameLink; + CheckHiddenSetupAbout.Checked = uiHidden.SetupAbout; + CheckHiddenSetupFeedback.Checked = uiHidden.SetupFeedback; + CheckHiddenSetupLog.Checked = uiHidden.SetupLog; + + // 子页面 工具 + CheckHiddenToolsGameLink.Checked = uiHidden.ToolsGameLink; + CheckHiddenToolsHelp.Checked = uiHidden.ToolsHelp; + CheckHiddenToolsTest.Checked = uiHidden.ToolsTest; + + // 子页面 实例设置 + CheckHiddenVersionEdit.Checked = uiHidden.InstanceEdit; + CheckHiddenVersionExport.Checked = uiHidden.InstanceExport; + CheckHiddenVersionSave.Checked = uiHidden.InstanceSave; + CheckHiddenVersionScreenshot.Checked = uiHidden.InstanceScreenshot; + CheckHiddenVersionMod.Checked = uiHidden.InstanceMod; + CheckHiddenVersionResourcePack.Checked = uiHidden.InstanceResourcePack; + CheckHiddenVersionShader.Checked = uiHidden.InstanceShader; + CheckHiddenVersionSchematic.Checked = uiHidden.InstanceSchematic; + CheckHiddenVersionServer.Checked = uiHidden.InstanceServer; + + // 特定功能 + CheckHiddenFunctionSelect.Checked = uiHidden.FunctionSelect; + CheckHiddenFunctionModUpdate.Checked = uiHidden.FunctionModUpdate; + CheckHiddenFunctionHidden.Checked = uiHidden.FunctionHidden; + } + catch (NullReferenceException ex) + { + ModBase.Log(ex, "个性化设置项存在异常,已被自动重置", ModBase.LogLevel.Msgbox); + Reset(); + } + catch (Exception ex) + { + ModBase.Log(ex, "重载个性化设置时出错", ModBase.LogLevel.Feedback); + } + } + + // 初始化 + public void Reset() + { + try + { + Config.Preference.Reset(); + ModBase.Log("[Setup] 已初始化个性化设置!"); + ModMain.Hint("已初始化个性化设置", ModMain.HintType.Finish, false); + } + catch (Exception ex) + { + ModBase.Log(ex, "初始化个性化设置失败", ModBase.LogLevel.Msgbox); + } + + Reload(); + } + + // 将控件改变路由到设置改变 + private void SliderChange(object senderRaw, bool user) + { + var sender = (MySlider)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Value); + } + + private void ComboChange(object senderRaw, SelectionChangedEventArgs e) + { + var sender = (MyComboBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + } + + private void CheckBoxChange(object senderRaw, bool user) + { + var sender = (MyCheckBox)senderRaw; + // 仅在动画未运行或初始化完成时保存设置,防止初始化时的触发导致重复写入 + if (ModAnimation.AniControlEnabled == 0) ModBase.Setup.Set(sender.Tag?.ToString(), sender.Checked); + } + + private void TextBoxChange(object senderRaw, RoutedEventArgs e) + { + var sender = (MyTextBox)senderRaw; + if (ModAnimation.AniControlEnabled == 0) + ModBase.Setup.Set(sender.Tag?.ToString(), sender.Text); + } + + private void RadioBoxChange(object senderRaw, ModBase.RouteEventArgs e) + { + var sender = (MyRadioBox)senderRaw; + var gotCfg = sender.Tag?.ToString()?.Split("/") ?? Array.Empty(); + if (ModAnimation.AniControlEnabled == 0 && gotCfg.Length >= 2) + ModBase.Setup.Set(gotCfg[0], int.Parse(gotCfg[1])); + } + + private void ComboFontChange(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled == 0) Config.Preference.Font = ComboUiFont.SelectedFontTag; + } + + private void ComboMotdFontChange(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled == 0) Config.Preference.MotdFont = ComboUiMotdFont.SelectedFontTag; + } + + // 背景图片 + private void BtnUIBgOpen_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenExplorer(ModBase.ExePath + @"PCL\Pictures\"); + } + + private void BtnBackgroundRefresh_Click(object sender, MouseButtonEventArgs e) + { + BackgroundRefresh(true, true); + } + + public void BackgroundRefreshUI(bool Show, int Count) + { + if (PanBackgroundOpacity == null) + return; + if (Show) + { + PanBackgroundOpacity.Visibility = Visibility.Visible; + PanBackgroundBlur.Visibility = Visibility.Visible; + PanBackgroundSuit.Visibility = Visibility.Visible; + BtnBackgroundClear.Visibility = Visibility.Visible; + CheckAutoPauseVideo.Visibility = Visibility.Visible; + CardBackground.Title = "背景图片/视频(" + Count + " 张)"; + } + else + { + PanBackgroundOpacity.Visibility = Visibility.Collapsed; + PanBackgroundBlur.Visibility = Visibility.Collapsed; + PanBackgroundSuit.Visibility = Visibility.Collapsed; + BtnBackgroundClear.Visibility = Visibility.Collapsed; + CheckAutoPauseVideo.Visibility = Visibility.Collapsed; + CardBackground.Title = "背景图片/视频"; + } + + CardBackground.TriggerForceResize(); + } + + private void BtnBackgroundClear_Click(object sender, MouseButtonEventArgs e) + { + if (ModMain.MyMsgBox("即将删除背景内容文件夹中的所有文件。" + "\r\n" + "此操作不可撤销,是否确定?", "警告", Button2: "取消", + IsWarn: true) == 1) + { + ModBase.DeleteDirectory(ModBase.ExePath + @"PCL\Pictures"); + BackgroundRefresh(false, true); + ModMain.Hint("背景内容已清空!", ModMain.HintType.Finish); + } + } + + /// + /// 刷新背景图片及设置页 UI。 + /// + /// 是否显示刷新提示。 + /// 是否刷新图片显示。 + public static void BackgroundRefresh(bool IsHint, bool Refresh) + { + try + { + // 获取可用的图片文件 + Directory.CreateDirectory(ModBase.ExePath + @"PCL\Pictures\"); + var Pic = ModBase.EnumerateFiles(ModBase.ExePath + @"PCL\Pictures\").Where(file => + !(file.Extension.Equals(".ini", StringComparison.OrdinalIgnoreCase) || + file.Extension.Equals(".db", StringComparison.OrdinalIgnoreCase))).Select(file => file.FullName) + .ToList(); + + // 视频加载异常处理 + + EventHandler videoHandler = (sender, e) => + { + var videoEx = e.ErrorException; + var videoAddress = ModMain.FrmMain.VideoBack.Source.ToString(); + if (ModMain.FrmMain.VideoBack.Source is not null) + { + ModVideoBack.VideoStop(); + + if (videoEx.Message.Contains("0xC00D109B")) + ModBase.Log( + "刷新背景内容失败,该视频文件可能并非 H.264(AVC) 格式。" + "\r\n" + + "你可以尝试使用视频转码工具打开视频文件并设定目标格式为 H.264(AVC) ,然后转码该视频。" + "\r\n" + "文件:" + + videoAddress, ModBase.LogLevel.Msgbox); + else + ModBase.Log(videoEx, "刷新背景内容失败(" + videoAddress + ")", ModBase.LogLevel.Msgbox); + } + }; + ModMain.FrmMain.VideoBack.MediaFailed -= videoHandler; + ModVideoBack.GamingStateChanged -= ModVideoBack.OnGamingStateChanged; + ModVideoBack.ForcePlayChanged -= ModVideoBack.OnForcePlayChanged; + ModVideoBack.GamingStateChanged += ModVideoBack.OnGamingStateChanged; + ModVideoBack.ForcePlayChanged += ModVideoBack.OnForcePlayChanged; + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectEqual(Config.Preference.Background.AutoPauseVideo, false, false))) + ModVideoBack.ForcePlay = true; + // 加载 + if (Pic.Count == 0) + { + if (Refresh) + { + if (ModMain.FrmMain.ImgBack.Visibility == Visibility.Collapsed) + { + if (IsHint) + ModMain.Hint("未检测到可用背景内容!", ModMain.HintType.Critical); + } + else + { + ModMain.FrmMain.ImgBack.Visibility = Visibility.Collapsed; + if (IsHint) + ModMain.Hint("背景内容已清除!", ModMain.HintType.Finish); + } + } + + if (!(ModMain.FrmSetupUI == null)) + ModMain.FrmSetupUI.BackgroundRefreshUI(false, 0); + } + else + { + if (Refresh) + { + var Address = RandomUtils.PickRandom(Pic); + try + { + ModMain.FrmMain.ImgBack.Background = null; + ModVideoBack.VideoStop(); + ModBase.Log("[UI] 加载背景内容:" + Address); + ModMain.FrmMain.ImgBack.Background = new MyBitmap(Address); + ModBase.Setup.Load("UiBackgroundSuit", true); + ModMain.FrmMain.ImgBack.Visibility = Visibility.Visible; + if (IsHint) + ModMain.Hint("背景内容已刷新:" + ModBase.GetFileNameFromPath(Address), ModMain.HintType.Finish, + false); + } + catch (Exception ex) + { + try + { + ModMain.FrmMain.VideoBack.MediaFailed += videoHandler; + ModBase.Log(ex, "[UI] 加载背景图片失败" + Address); + if (ModBase.ModeDebug) + ModMain.Hint("图片加载失败,尝试将文件作为视频播放:" + Address); + ModMain.FrmMain.ImgBack.Visibility = Visibility.Visible; + ModMain.FrmMain.VideoBack.Source = new Uri(Address, UriKind.Absolute); + ModVideoBack.VideoPlay(); + if (IsHint) + ModMain.Hint("背景内容已刷新:" + ModBase.GetFileNameFromPath(Address), ModMain.HintType.Finish, + false); + } + catch (Exception playEx) + { + ModBase.Log(playEx, "播放背景内容时出现未知错误:"); + } + } + } + + if (!(ModMain.FrmSetupUI == null)) + ModMain.FrmSetupUI.BackgroundRefreshUI(true, Pic.Count); + } + } + + catch (Exception ex) + { + ModBase.Log(ex, "刷新背景内容时出现未知错误", ModBase.LogLevel.Feedback); + } + } + + // 顶部栏 + private void BtnLogoChange_Click(object sender, MouseButtonEventArgs e) + { + var FileName = SystemDialogs.SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp)|*.png;*.jpg;*.gif;*.webp", "选择图片"); + if (string.IsNullOrEmpty(FileName)) + return; + try + { + // 拷贝文件 + File.Delete(ModBase.ExePath + @"PCL\Logo.png"); + ModBase.CopyFile(FileName, ModBase.ExePath + @"PCL\Logo.png"); + // 设置当前显示 + ModMain.FrmMain.ImageTitleLogo.Source = null; // 防止因为 Source 属性前后的值相同而不更新 (#5628) + ModMain.FrmMain.ImageTitleLogo.Source = ModBase.ExePath + @"PCL\Logo.png"; + } + catch (Exception ex) + { + if (ex.Message.Contains("参数无效")) + ModBase.Log("改变标题栏图片失败,该图片文件可能并非标准格式。" + "\r\n" + "你可以尝试使用画图打开该文件并重新保存,这会让图片变为标准格式。", + ModBase.LogLevel.Msgbox); + else + ModBase.Log(ex, "设置标题栏图片失败", ModBase.LogLevel.Msgbox); + ModMain.FrmMain.ImageTitleLogo.Source = null; + } + } + + private void RadioLogoType3_Check(object sender, ModBase.RouteEventArgs e) + { + if (!(ModAnimation.AniControlEnabled == 0 && e.RaiseByMouse)) + return; + Refresh: ; + + // 已有图片则不再选择 + if (File.Exists(ModBase.ExePath + @"PCL\Logo.png")) + { + try + { + ModMain.FrmMain.ImageTitleLogo.Source = null; // 防止因为 Source 属性前后的值相同而不更新 (#5628) + ModMain.FrmMain.ImageTitleLogo.Source = ModBase.ExePath + @"PCL\Logo.png"; + } + catch (Exception ex) + { + if (ex.Message.Contains("参数无效")) + ModBase.Log("调整标题栏图片失败,该图片文件可能并非标准格式。" + "\r\n" + "你可以尝试使用画图打开该文件并重新保存,这会让图片变为标准格式。", + ModBase.LogLevel.Msgbox); + else + ModBase.Log(ex, "调整标题栏图片失败", ModBase.LogLevel.Msgbox); + ModMain.FrmMain.ImageTitleLogo.Source = null; + e.Handled = true; + try + { + File.Delete(ModBase.ExePath + @"PCL\Logo.png"); + } + catch (Exception exx) + { + ModBase.Log(exx, "清理错误的标题栏图片失败", ModBase.LogLevel.Msgbox); + } + } + + return; + } + + // 没有图片则要求选择 + var FileName = SystemDialogs.SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp)|*.png;*.jpg;*.gif;*.webp", "选择图片"); + if (string.IsNullOrEmpty(FileName)) + { + ModMain.FrmMain.ImageTitleLogo.Source = null; + e.Handled = true; + } + else + { + try + { + // 拷贝文件 + File.Delete(ModBase.ExePath + @"PCL\Logo.png"); + ModBase.CopyFile(FileName, ModBase.ExePath + @"PCL\Logo.png"); + goto Refresh; + } + catch (Exception ex) + { + ModBase.Log(ex, "复制标题栏图片失败", ModBase.LogLevel.Msgbox); + } + } + } + + private void BtnLogoDelete_Click(object sender, MouseButtonEventArgs e) + { + try + { + File.Delete(ModBase.ExePath + @"PCL\Logo.png"); + RadioLogoType1.SetChecked(true, true); + ModMain.Hint("标题栏图片已清空!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "清空标题栏图片失败", ModBase.LogLevel.Msgbox); + } + } + + // 背景音乐 + private void BtnMusicOpen_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenExplorer(ModBase.ExePath + @"PCL\Musics\"); + } + + private void BtnMusicRefresh_Click(object sender, MouseButtonEventArgs e) + { + ModMusic.MusicRefreshPlay(true); + } + + public void MusicRefreshUI() + { + if (PanBackgroundOpacity is null) + return; + if (ModMusic.MusicAllList.Any()) + { + PanMusicVolume.Visibility = Visibility.Visible; + PanMusicDetail.Visibility = Visibility.Visible; + BtnMusicClear.Visibility = Visibility.Visible; + CardMusic.Title = "背景音乐(" + ModBase.EnumerateFiles(ModBase.ExePath + @"PCL\Musics\").Count() + " 首)"; + } + else + { + PanMusicVolume.Visibility = Visibility.Collapsed; + PanMusicDetail.Visibility = Visibility.Collapsed; + BtnMusicClear.Visibility = Visibility.Collapsed; + CardMusic.Title = "背景音乐"; + } + + CardMusic.TriggerForceResize(); + } + + private void BtnMusicClear_Click(object sender, MouseButtonEventArgs e) + { + if (ModMain.MyMsgBox("即将删除背景音乐文件夹中的所有文件。" + "\r\n" + "此操作不可撤销,是否确定?", "警告", Button2: "取消", + IsWarn: true) == 1) + ModBase.RunInThread(() => + { + ModMain.Hint("正在删除背景音乐……"); + // 停止播放音乐 + ModMusic.MusicNAudio = null; + ModMusic.MusicWaitingList = new List(); + ModMusic.MusicAllList = new List(); + Thread.Sleep(200); + // 删除文件 + try + { + ModBase.DeleteDirectory(ModBase.ExePath + @"PCL\Musics"); + // DisableSMTCSupport() + ModMain.Hint("背景音乐已删除!", ModMain.HintType.Finish); + } + catch (Exception ex) + { + ModBase.Log(ex, "删除背景音乐失败", ModBase.LogLevel.Msgbox); + } + + try + { + Directory.CreateDirectory(ModBase.ExePath + @"PCL\Musics"); + ModBase.RunInUi(() => ModMusic.MusicRefreshPlay(false)); + } + catch (Exception ex) + { + ModBase.Log(ex, "重建背景音乐文件夹失败", ModBase.LogLevel.Msgbox); + } + }); + } + + private void CheckMusicStart_Change(object sender, bool user) + { + CheckBoxChange(sender, user); + if (ModAnimation.AniControlEnabled != 0) + return; + if (CheckMusicStart.Checked == true) + CheckMusicStop.Checked = false; + } + + private void CheckMusicStop_Change() + { + if (ModAnimation.AniControlEnabled != 0) + return; + if (CheckMusicStop.Checked == true) + CheckMusicStart.Checked = false; + } + + // 主页 + private void BtnCustomFile_Click(object sender, MouseButtonEventArgs e) + { + try + { + if (File.Exists(ModBase.ExePath + @"PCL\Custom.xaml")) + if (ModMain.MyMsgBox("当前已存在布局文件,继续生成教学文件将会覆盖现有布局文件!", "覆盖确认", "继续", "取消", IsWarn: true) == 2) + return; + ModBase.WriteFile(ModBase.ExePath + @"PCL\Custom.xaml", ModBase.GetResourceStream("Resources/Custom.xml")); + ModMain.Hint("教学文件已生成!", ModMain.HintType.Finish); + ModBase.OpenExplorer(ModBase.ExePath + @"PCL\Custom.xaml"); + } + catch (Exception ex) + { + ModBase.Log(ex, "生成教学文件失败", ModBase.LogLevel.Feedback); + } + } + + private void BtnCustomRefresh_Click(object sender, MouseButtonEventArgs e) + { + ModMain.FrmLaunchRight.ForceRefresh(); + ModMain.Hint("已刷新主页!", ModMain.HintType.Finish); + } + + private void BtnCustomTutorial_Click(object sender, MouseButtonEventArgs e) + { + ModMain.MyMsgBox( + "1. 点击 生成教学文件 按钮,这会在 PCL 文件夹下生成 Custom.xaml 布局文件。" + "\r\n" + "2. 使用记事本等工具打开这个文件并进行修改,修改完记得保存。" + + "\r\n" + "3. 点击 刷新主页 按钮,查看主页现在长啥样了。" + "\r\n" + "\r\n" + + "你可以在生成教学文件后直接刷新主页,对照着进行修改,更有助于理解。" + "\r\n" + "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程"); + } + + // 主题 + private void ThemeColor_Change(object senderRaw, SelectionChangedEventArgs e) + { + var sender = (MyComboBox)senderRaw; + ModBase.Setup.Set(sender.Tag?.ToString(), sender.SelectedIndex); + ModSecret.ThemeRefresh(); + } + + // 主题自定义 + private void RadioLauncherTheme14_Change(object sender, ModBase.RouteEventArgs e) + { + // If RadioLauncherTheme14.Checked Then + // If LabLauncherHue.Visibility = Visibility.Visible Then Exit Sub + // LabLauncherHue.Visibility = Visibility.Visible + // SliderLauncherHue.Visibility = Visibility.Visible + // LabLauncherSat.Visibility = Visibility.Visible + // SliderLauncherSat.Visibility = Visibility.Visible + // LabLauncherDelta.Visibility = Visibility.Visible + // SliderLauncherDelta.Visibility = Visibility.Visible + // LabLauncherLight.Visibility = Visibility.Visible + // SliderLauncherLight.Visibility = Visibility.Visible + // Else + if (LabLauncherHue.Visibility == Visibility.Collapsed) + return; + LabLauncherHue.Visibility = Visibility.Collapsed; + SliderLauncherHue.Visibility = Visibility.Collapsed; + LabLauncherSat.Visibility = Visibility.Collapsed; + SliderLauncherSat.Visibility = Visibility.Collapsed; + LabLauncherDelta.Visibility = Visibility.Collapsed; + SliderLauncherDelta.Visibility = Visibility.Collapsed; + LabLauncherLight.Visibility = Visibility.Collapsed; + SliderLauncherLight.Visibility = Visibility.Collapsed; + // End If + CardLauncher.TriggerForceResize(); + } + + private void HSL_Change(object senderRaw, bool user) + { + if (ModAnimation.AniControlEnabled != 0 || SliderLauncherSat is null || !SliderLauncherSat.IsLoaded) + return; +#if False + if (EnableCustomTheme) + { + ColorHueTopbarDelta = SliderLauncherDelta.Value - 90 + ColorLightAdjust = SliderLauncherLight.Value - 20 + } +#endif + ModSecret.ThemeRefresh(); + } + + // 赞助 + private void BtnLauncherDonate_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenWebsite("https://afdian.com/a/LTCat"); + } + + // 滑动条 + private void SliderLoad() + { + SliderMusicVolume.GetHintText = new Func(v => + Operators.ConcatenateObject(Math.Ceiling(Convert.ToDouble(v) * 0.1d), "%")); + SliderLauncherOpacity.GetHintText = new Func(v => + Operators.ConcatenateObject(Math.Round(40 + Convert.ToDouble(v) * 0.1d), "%")); + SliderLauncherHue.GetHintText = new Func(v => Operators.ConcatenateObject(v, "°")); + SliderLauncherSat.GetHintText = new Func(v => Operators.ConcatenateObject(v, "%")); + SliderLauncherDelta.GetHintText = new Func(Value => + { + if (Value > 90) return "+" + (Value - 90); + + if (Value == 90) return 0.ToString(); + + return (Value - 90).ToString(); + }); + SliderLauncherLight.GetHintText = new Func(Value => + { + if (Value > 20) return "+" + (Value - 20); + + if (Value == 20) return 0.ToString(); + + return (Value - 20).ToString(); + }); + SliderBackgroundOpacity.GetHintText = new Func(v => + Operators.ConcatenateObject(Math.Round(Convert.ToDouble(v) * 0.1d), "%")); + SliderBackgroundBlur.GetHintText = new Func(v => Operators.ConcatenateObject(v, " 像素")); + SliderBlurValue.GetHintText = new Func(v => Operators.ConcatenateObject(v, " 像素")); + SliderBlurSamplingRate.GetHintText = new Func(v => Operators.ConcatenateObject(v, "%")); + } + + private void BtnHomepageMarket_Click(object sender, ModBase.RouteEventArgs e) + { + ModMain.FrmMain.PageChange(new FormMain.PageStackData { Page = FormMain.PageType.HomePageMarket }); + } + + private void CheckMusicStart_OnChange(object sender, bool user) + { + CheckBoxChange(sender, user); + CheckMusicStart_Change(sender, user); + } + + private void CheckMusicStop_OnChange(object sender, bool user) + { + CheckBoxChange(sender, user); + CheckMusicStop_Change(); + } + + #region 功能隐藏 + + private static bool _HiddenForceShow; + + /// + /// 是否强制显示被禁用的功能。 + /// + public static bool HiddenForceShow + { + get => _HiddenForceShow; + set + { + _HiddenForceShow = value; + HiddenRefresh(); + } + } + + /// + /// 更新功能隐藏带来的显示变化。 + /// + public static void HiddenRefresh() + { + if (ModMain.FrmMain.PanTitleSelect is null || !ModMain.FrmMain.PanTitleSelect.IsLoaded) + return; + try + { + // 获取配置组引用以缩短代码 + var conf = Config.Preference.Hide; + + // 顶部栏:下载、设置、工具 + var IsAllTitleHidden = !HiddenForceShow && conf.PageDownload && conf.PageSetup && conf.PageTools; + + if (IsAllTitleHidden) + { + ModMain.FrmMain.PanTitleSelect.Visibility = Visibility.Collapsed; + } + else + { + ModMain.FrmMain.PanTitleSelect.Visibility = Visibility.Visible; + ModMain.FrmMain.BtnTitleSelect1.Visibility = !HiddenForceShow && conf.PageDownload + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmMain.BtnTitleSelect2.Visibility = + !HiddenForceShow && conf.PageSetup ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmMain.BtnTitleSelect3.Visibility = + !HiddenForceShow && conf.PageTools ? Visibility.Collapsed : Visibility.Visible; + } + + // 功能隐藏设置卡片 + if (ModMain.FrmSetupUI is not null) + { + ModMain.FrmSetupUI.CardSwitch.Visibility = !HiddenForceShow && conf.FunctionHidden + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupUI.CardSwitch.Title = HiddenForceShow ? "功能隐藏(已暂时关闭,按 F12 以重新启用)" : "功能隐藏"; + } + + // 设置子页面 (FrmSetupLeft) + if (ModMain.FrmSetupLeft is not null) + { + ModMain.FrmSetupLeft.ItemLaunch.Visibility = + !HiddenForceShow && conf.SetupLaunch ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmSetupLeft.ItemUI.Visibility = + !HiddenForceShow && conf.SetupUi ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmSetupLeft.ItemGameManage.Visibility = !HiddenForceShow && conf.SetupGameManage + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupLeft.ItemLauncherMisc.Visibility = !HiddenForceShow && conf.SetupLauncherMisc + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupLeft.ItemJava.Visibility = + !HiddenForceShow && conf.SetupJava ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmSetupLeft.ItemUpdate.Visibility = + !HiddenForceShow && conf.SetupUpdate ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmSetupLeft.ItemGameLink.Visibility = !HiddenForceShow && conf.SetupGameLink + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupLeft.ItemAbout.Visibility = + !HiddenForceShow && conf.SetupAbout ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmSetupLeft.ItemFeedback.Visibility = !HiddenForceShow && conf.SetupFeedback + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmSetupLeft.ItemLog.Visibility = + !HiddenForceShow && conf.SetupLog ? Visibility.Collapsed : Visibility.Visible; + + var categories = new[] + { + (ModMain.FrmSetupLeft.TextGameCategory, + !(conf.SetupLaunch && conf.SetupJava && conf.SetupGameManage)), + (ModMain.FrmSetupLeft.TextToolsCategory, !conf.SetupGameLink), + (ModMain.FrmSetupLeft.TextLauncherCategory, !(conf.SetupUi && conf.SetupLauncherMisc)), + (ModMain.FrmSetupLeft.TextAboutCategory, + !(conf.SetupAbout && conf.SetupUpdate && conf.SetupFeedback && conf.SetupLog)) + }; + + foreach (var category in categories) + { + var isVisible = category.Item2 || HiddenForceShow; + category.Item1.Visibility = + Conversions.ToBoolean(isVisible) ? Visibility.Visible : Visibility.Collapsed; + if (Conversions.ToBoolean(isVisible)) + category.Item1.Opacity = 0.6d; + } + + // 统计设置页可用项数量 + var SetupCount = 0; + if (!conf.SetupLaunch) + SetupCount += 1; + if (!conf.SetupUi) + SetupCount += 1; + if (!conf.SetupGameManage) + SetupCount += 1; + if (!conf.SetupLauncherMisc) + SetupCount += 1; + if (!conf.SetupJava) + SetupCount += 1; + if (!conf.SetupUpdate) + SetupCount += 1; + if (!conf.SetupGameLink) + SetupCount += 1; + if (!conf.SetupAbout) + SetupCount += 1; + if (!conf.SetupFeedback) + SetupCount += 1; + if (!conf.SetupLog) + SetupCount += 1; + ModMain.FrmSetupLeft.PanItem.Visibility = + SetupCount < 2 && !HiddenForceShow ? Visibility.Collapsed : Visibility.Visible; + } + + // 工具子页面 (FrmToolsLeft) + if (ModMain.FrmToolsLeft is not null) + { + ModMain.FrmToolsLeft.ItemGameLink.Visibility = !HiddenForceShow && conf.ToolsGameLink + ? Visibility.Collapsed + : Visibility.Visible; + ModMain.FrmToolsLeft.ItemLauncherHelp.Visibility = + !HiddenForceShow && conf.ToolsHelp ? Visibility.Collapsed : Visibility.Visible; + ModMain.FrmToolsLeft.ItemTest.Visibility = + !HiddenForceShow && conf.ToolsTest ? Visibility.Collapsed : Visibility.Visible; + + // 统计工具页可用项数量 + var ToolsCount = 0; + if (!conf.ToolsGameLink) + ToolsCount += 1; + if (!conf.ToolsHelp) + ToolsCount += 1; + if (!conf.ToolsTest) + ToolsCount += 1; + ModMain.FrmToolsLeft.PanItem.Visibility = + ToolsCount < 2 && !HiddenForceShow ? Visibility.Collapsed : Visibility.Visible; + } + + // 其他入口刷新 + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSelect) + ModMain.FrmSelectRight.BtnEmptyDownload_Loaded(); + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.Launch) + ModMain.FrmLaunchLeft.RefreshButtonsUI(); + if (ModMain.FrmMain.PageCurrent == FormMain.PageType.InstanceSetup && + ModMain.FrmInstanceModDisabled is not null) + ModMain.FrmInstanceModDisabled.BtnDownload_Loaded(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "刷新功能隐藏项目失败", ModBase.LogLevel.Feedback); + } + } + + // ================= 设置页面协同 ================= + private void HiddenSetupMain() + { + var IsChecked = (bool)CheckHiddenPageSetup.Checked; + CheckHiddenSetupLaunch.Checked = IsChecked; + CheckHiddenSetupUI.Checked = IsChecked; + CheckHiddenSetupGameManage.Checked = IsChecked; + CheckHiddenLauncherMisc.Checked = IsChecked; + CheckHiddenSetupJava.Checked = IsChecked; + CheckHiddenSetupUpdate.Checked = IsChecked; + CheckHiddenSetupGameLink.Checked = IsChecked; + CheckHiddenSetupAbout.Checked = IsChecked; + CheckHiddenSetupFeedback.Checked = IsChecked; + CheckHiddenSetupLog.Checked = IsChecked; + } + + // ================= 设置页面协同 ================= + private void HiddenSetupMain(object sender, bool user) + { + if (!user) + return; // 仅处理用户点击,防止死循环 + var IsChecked = (bool)CheckHiddenPageSetup.Checked; + CheckHiddenSetupLaunch.Checked = IsChecked; + CheckHiddenSetupUI.Checked = IsChecked; + CheckHiddenSetupGameManage.Checked = IsChecked; + CheckHiddenLauncherMisc.Checked = IsChecked; + CheckHiddenSetupJava.Checked = IsChecked; + CheckHiddenSetupUpdate.Checked = IsChecked; + CheckHiddenSetupGameLink.Checked = IsChecked; + CheckHiddenSetupAbout.Checked = IsChecked; + CheckHiddenSetupFeedback.Checked = IsChecked; + CheckHiddenSetupLog.Checked = IsChecked; + } + + private void HiddenSetupSub(object sender, bool user) + { + if (!user) + return; + var conf = Config.Preference.Hide; + // 判断是否全部勾选 + var AllChecked = conf.SetupLaunch && conf.SetupUi && conf.SetupJava && conf.SetupUpdate && conf.SetupGameLink && + conf.SetupAbout && conf.SetupFeedback && conf.SetupLog && conf.SetupLauncherMisc && + conf.SetupGameManage; + CheckHiddenPageSetup.Checked = AllChecked; + } + + // ================= 工具页面协同 ================= + private void HiddenToolsMain(object sender, bool user) + { + if (!user) + return; + var IsChecked = (bool)CheckHiddenPageTools.Checked; + CheckHiddenToolsGameLink.Checked = IsChecked; + CheckHiddenToolsHelp.Checked = IsChecked; + CheckHiddenToolsTest.Checked = IsChecked; + } + + private void HiddenToolsSub(object sender, bool user) + { + if (!user) + return; + var conf = Config.Preference.Hide; + var AllChecked = conf.ToolsGameLink && conf.ToolsHelp && conf.ToolsTest; + CheckHiddenPageTools.Checked = AllChecked; + } + + // 警告提示 + private void HiddenHint(object sender, bool user) + { + if (ModAnimation.AniControlEnabled == 0 && sender is MyCheckBox checkBox && checkBox.Checked == true) + ModMain.Hint("按 F12 即可暂时关闭功能隐藏设置。千万别忘了,要不然设置就改不回来了……"); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml index 0ef5d2344..207bfd632 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml @@ -1,10 +1,9 @@ - - @@ -24,26 +23,41 @@ - - - - - + + + + + - - + + - - - + + + - + @@ -62,12 +76,23 @@ - - - - - - + + + + + + @@ -87,16 +112,25 @@ - + - - - - + + + + - + @@ -114,16 +148,25 @@ - + - - - - - + + + + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml.cs b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml.cs new file mode 100644 index 000000000..49a62fd33 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUpdate.xaml.cs @@ -0,0 +1,292 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.Utils; + +namespace PCL; + +public partial class PageSetupUpdate +{ + public VersionDataModel UpdateInfo; + + public PageSetupUpdate() + { + InitializeComponent(); + Loaded += (_, __) => Init(); + } + + private void Init() + { + ModAnimation.AniControlEnabled += 1; + TextMirrorCDK.Password = Config.Update.MirrorChyanKey; + + ComboSystemUpdateChannel.SelectedIndex = (int)Config.Update.UpdateChannel; + ComboSystemUpdateMode.SelectedIndex = (int)Config.Update.UpdateMode; + + TextCurrentVersion.Text = "PCL CE " + VersionNameFormat(ModBase.VersionBaseName); + ModAnimation.AniControlEnabled -= 1; + CheckUpdate(); + } + + private async Task IsLatestAsync() + { + try + { + // 修复:使用 dynamic 绕过命名空间重名导致的编译期类型冲突, + // 或者你可以尝试替换为 PCL.Core.App.SemVer.Parse(ModBase.VersionBaseName) + if (await ModSecret.RemoteServer.IsLatestAsync( + Conversions.ToBoolean(ModSecret.IsCurrentVersionBeta) ? UpdateChannel.beta : UpdateChannel.stable, + ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64, + SemVer.Parse(ModBase.VersionBaseName), + ModBase.VersionCode)) + { + ModBase.Log("[Update] 已是最新版本"); + return UpdateStatus.Latest; + } + + ModBase.Log("[Update] 有可用的新版本"); + return UpdateStatus.Available; + } + catch (Exception ex) + { + ModBase.Log(ex, "无法获取最新版本信息,请检查网络连接", ModBase.LogLevel.Hint); + return UpdateStatus.Error; + } + } + + public async void CheckUpdate() + { + ModBase.Log("[Update] 开始检查更新"); + CardUpdate.Visibility = Visibility.Collapsed; + CardCheck.Visibility = Visibility.Visible; + TextCurrentDesc.Text = "正在检查更新..."; + BtnCheckAgain.IsEnabled = false; + switch (await IsLatestAsync()) + { + case UpdateStatus.Available: + { + Exception checkUpdateEx = null; + try + { + UpdateInfo = ModSecret.RemoteServer.GetLatestVersion( + Conversions.ToBoolean(ModSecret.IsCurrentVersionBeta) + ? UpdateChannel.beta + : UpdateChannel.stable, ModBase.IsArm64System ? UpdateArch.arm64 : UpdateArch.x64); + TextUpdateName.Text = "PCL CE " + VersionNameFormat(UpdateInfo.VersionName); + var summary = UpdateInfo.Changelog.Between("", ""); + if (!UpdateInfo.Changelog.Contains("") || string.IsNullOrWhiteSpace(summary.Trim())) + TextChangelog.Text = "开发者似乎忘记提供更新摘要了...也许你可以点击下方看看完整更新日志?"; + else + TextChangelog.Text = summary; + } + catch (Exception ex) + { + checkUpdateEx = ex; + } + + BtnCheckAgain.IsEnabled = true; + if (UpdateInfo is null) + { + TextCurrentDesc.Text = "检查更新时出错"; + if (checkUpdateEx is not null) + ModBase.Log(checkUpdateEx, "[Update] 检查更新失败", ModBase.LogLevel.Msgbox); + else + ModBase.Log("[Update] 检查更新失败", ModBase.LogLevel.Msgbox); + return; + } + + if (ModSecret.UpdateLoader is not null && ModSecret.UpdateLoader.State == ModBase.LoadState.Loading) + { + BtnUpdate_Timer(); + BtnUpdate.IsEnabled = false; + } + else if (ModSecret.IsUpdateWaitingRestart) + { + BtnUpdate.Text = "重启安装"; + BtnUpdate.IsEnabled = true; + } + else + { + BtnUpdate.Text = "下载并安装"; + BtnUpdate.IsEnabled = true; + } + + CardUpdate.Visibility = Visibility.Visible; + CardCheck.Visibility = Visibility.Collapsed; + break; + } + case UpdateStatus.Latest: + { + CardUpdate.Visibility = Visibility.Collapsed; + CardCheck.Visibility = Visibility.Visible; + BtnCheckAgain.IsEnabled = true; + TextCurrentDesc.Text = "已是最新版本"; + break; + } + case UpdateStatus.Error: + { + CardUpdate.Visibility = Visibility.Collapsed; + CardCheck.Visibility = Visibility.Visible; + BtnCheckAgain.IsEnabled = true; + TextCurrentDesc.Text = "检查更新时出错"; + break; + } + } + } + + public void BtnUpdate_Timer() + { + while (ModSecret.UpdateLoader is not null && ModSecret.UpdateLoader.State == ModBase.LoadState.Loading) + { + ModBase.RunInUi(() => BtnUpdate.Text = $"{Math.Round(ModSecret.UpdateLoader.Progress, 2)}%"); + Thread.Sleep(200); + } + } + + private void BtnUpdate_Click(object sender, MouseButtonEventArgs e) + { + // 检查 .NET 版本 + if (!UpdateInfo.VersionName.StartsWithF("2.13.") && !ModBase + .ShellAndGetOutput("cmd", "/c dotnet --list-runtimes") + .ContainsF("Microsoft.WindowsDesktop.App 8.0.", true)) + { + ModMain.MyMsgBox( + $"发现了启动器更新(版本 {UpdateInfo.VersionName}),但是新版本要求你的电脑安装 .NET 8 才可以运行。{"\r\n"}你需要先安装 .NET 8 才可以继续更新。{"\r\n"}{"\r\n"}点击下方按钮打开网页,然后选择 ⌈.NET 桌面运行时⌋ 中的 {(ModBase.IsArm64System ? "Arm64" : "x64")} 选项下载。", + "启动器更新 - 缺少运行环境", "下载 .NET 8 运行时", "取消", + Button1Action: () => ModBase.OpenWebsite("https://get.dot.net/8"), ForceWait: true); + return; + } + + if (ModSecret.IsUpdateWaitingRestart) ModSecret.UpdateRestart(true); + // 开始更新流程 + ModSecret.UpdateStart(ModSecret.UpdateType.UpdateNow); + } + + private void BtnChangelogDetail_Click(object sender, EventArgs e) + { + if (UpdateInfo is null) + ModMain.MyMsgBox("没有可用的更新日志...", "关于此更新"); + else + ModMain.MyMsgBoxMarkdown(UpdateInfo.Changelog, "关于此更新"); + } + + private void ComboSystemUpdateMode_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled == 0) + Config.Update.UpdateMode = (LauncherAutoUpdateBehavior)ComboSystemUpdateMode.SelectedIndex; + } + + private void ComboSystemUpdateBranch_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (ModAnimation.AniControlEnabled != 0) + return; + + var IsCancelled = false; + switch (ComboSystemUpdateChannel.SelectedIndex) + { + case 0: + { + break; + } + case 1: + { + if (ModMain.MyMsgBox( + "你正在切换启动器更新通道到测试版。" + "\r\n" + "测试版可以提供下个版本更新内容的预览,但可能会包含未经充分测试的功能,稳定性欠佳。" + + "\r\n" + "\r\n" + "在升级到测试版后,你需要等待下一个正式版发布,或是手动重新下载启动器来切换到正式版。" + + "\r\n" + "该选项仅推荐具有一定基础知识和能力的用户选择。如果你正在制作整合包,请使用正式版!", "继续之前...", "我已知晓", "取消", + IsWarn: true) == 2) + IsCancelled = true; + else + CheckUpdate(); + + break; + } + case 2: + { + if (ModMain.MyMsgBox( + "你正在切换启动器更新通道到开发版。" + "\r\n" + "该通道可第一时间获取基于最新代码构建的开发版本,但可能极不稳定,甚至直接无法启动。" + + "\r\n" + "\r\n" + "在升级到开发版后,只能手动重新下载启动器来切换回正式版或测试版。" + "\r\n" + + "该选项仅推荐高级用户选择。如果你正在制作整合包,请使用正式版!", "继续之前...", "我已知晓", "取消", IsWarn: true) == 2) + { + IsCancelled = true; + break; + } + + var ret = ModMain.MyMsgBoxInput("最终确认", + "你确定要切换到开发版通道吗?" + "\r\n" + "开发版可能存在严重问题,甚至无法启动!" + "\r\n" + + "在升级到开发版后,将无法切换回其他任何更新通道,只能手动重新下载启动器来切换回正式版或测试版。" + "\r\n" + "\r\n" + + "该选项仅推荐高级用户选择。如果你正在制作整合包,请使用正式版!" + "\r\n" + "请输入 '我确认切换到此分支并已知晓风险' 以确认。", Button1: "提交", + Button2: "取消", IsWarn: true); + if (ret is null) + { + IsCancelled = true; + break; + } + + if (ret == "我确认切换到此分支并已知晓风险") + { + CheckUpdate(); + } + else + { + ModMain.Hint("你输入了错误的内容..."); + IsCancelled = true; + } + + break; + } + } + + if (IsCancelled) + { + ModAnimation.AniControlEnabled += 1; + ComboSystemUpdateChannel.SelectedItem = e.RemovedItems[0]; + ModAnimation.AniControlEnabled -= 1; + } + else + { + Config.Update.UpdateChannel = (Core.App.UpdateChannel)ComboSystemUpdateChannel.SelectedIndex; + } + } + + private void TextMirrorCDK_PasswordChanged(object sender, EventArgs e) + { + Config.Update.MirrorChyanKey = TextMirrorCDK.Password; + } + + private void BtnGetMirrorCDK_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenWebsite("https://mirrorchyan.com/"); + } + + private void BtnChangelog_Click(object sender, MouseButtonEventArgs e) + { + ModBase.OpenWebsite("https://github.com/PCL-Community/PCL2-CE/releases/v" + ModBase.VersionBaseName); + } + + public string VersionNameFormat(string str) + { + str = str.Replace("v", ""); + if (!str.Contains("-")) + return str; + var add = str.AfterLast("-"); + str = str.BeforeLast("-"); + return str + " " + add.Replace(".", " ").Replace("beta", "Beta").Replace("rc", "RC"); + } + + private void BtnCheckAgain_OnClick(object sender, MouseButtonEventArgs e) + { + CheckUpdate(); + } + + private enum UpdateStatus + { + Checking = 0, + Available = 1, + Error = 2, + Latest = 3 + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml index bdbd6da6c..1f1c3504d 100644 --- a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml @@ -1,11 +1,11 @@ - + diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsHelpDetail.xaml.cs b/Plain Craft Launcher 2/Pages/PageTools/PageToolsHelpDetail.xaml.cs new file mode 100644 index 000000000..555cde02e --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsHelpDetail.xaml.cs @@ -0,0 +1,55 @@ +using System.Windows; +using Microsoft.VisualBasic; + +namespace PCL; + +public partial class PageOtherHelpDetail : IRefreshable +{ + public ModMain.HelpEntry Entry; + + public PageOtherHelpDetail() + { + InitializeComponent(); + Loaded += PageOtherHelpDetail_Loaded; + } + + public void Refresh() + { + Init(new ModMain.HelpEntry(Entry.RawPath)); + } + + private void PageOtherHelpDetail_Loaded(object sender, RoutedEventArgs e) + { + PanBack.ScrollToTop(); + } + + /// + /// 根据特定帮助项初始化页面 UI,返回是否成功加载。 + /// + public bool Init(ModMain.HelpEntry Entry) + { + var Content = Entry.XamlContent ?? ""; + if (string.IsNullOrEmpty(Content)) + throw new Exception("帮助 xaml 文件为空"); + try + { + // 修改时应同时修改 PageLaunchRight.LoadContent + Content = ModMain.HelpArgumentReplace(Content); + if (Content.Contains("xmlns")) + Content = Content.RegexReplace("xmlns[^\"']*(\"|')[^\"']*(\"|')", "").Replace("xmlns", ""); // 禁止声明命名空间 + Content = + "" + + Content + ""; + this.Entry = Entry; + PanCustom.Children.Clear(); + PanCustom.Children.Add((UIElement)ModBase.GetObjectFromXML(Content)); + return true; + } + catch (Exception ex) + { + ModBase.Log("[System] 自定义信息内容:" + "\r\n" + Content); + ModBase.Log(ex, "加载帮助 XAML 文件失败", ModBase.LogLevel.Msgbox); + return false; + } + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml b/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml index 2a94dc5d9..e3e384e67 100644 --- a/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml @@ -1,36 +1,49 @@ - + - + - + - + - + - + - + - + - - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml.cs b/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml.cs new file mode 100644 index 000000000..914ebe9e3 --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsLeft.xaml.cs @@ -0,0 +1,174 @@ +using System.Windows; +using System.Windows.Controls; + +namespace PCL; + +public partial class PageToolsLeft +{ + private bool IsLoad; + private bool IsPageSwitched; // 如果在 Loaded 前切换到其他页面,会导致触发 Loaded 时再次切换一次 + + public PageToolsLeft() + { + InitializeComponent(); + AnimatedControl = PanItem; + Loaded += PageLinkLeft_Loaded; + Unloaded += PageOtherLeft_Unloaded; + } + + private void PageLinkLeft_Loaded(object sender, RoutedEventArgs e) + { + if (IsLoad) + return; + IsLoad = true; + // 切换默认页面 + if (IsPageSwitched) + return; + ItemGameLink.SetChecked(true, false, false); + } + + private void PageOtherLeft_Unloaded(object sender, RoutedEventArgs e) + { + IsPageSwitched = false; + } + + public void Refresh(object sender, EventArgs e) + { + var button = (MyIconButton)sender; + if (button.Tag is null) + return; + double id = ModBase.Val(button.Tag); + switch (id) + { + case (double)FormMain.PageSubType.ToolsGameLink: + { + if (ModMain.FrmToolsGameLink is null) + ModMain.FrmToolsGameLink = new PageToolsGameLink(); + ModMain.FrmToolsGameLink.Reload(); + ItemGameLink.Checked = true; + break; + } + case (double)FormMain.PageSubType.ToolsLauncherHelp: + { + if (ModMain.FrmToolsHelp is null) + ModMain.FrmToolsHelp = new PageToolsHelp(); + ModMain.FrmToolsHelp.Refresh(); + ItemLauncherHelp.Checked = true; + break; + } + } + } + + public static void RefreshHelp() + { + ModMain.FrmToolsHelp.PageLoaderRestart(); + ModMain.FrmToolsHelp.SearchBox.Text = ""; + } + + #region 页面切换 + + /// + /// 当前页面的编号。 + /// + public FormMain.PageSubType PageID = FormMain.PageSubType.ToolsGameLink; + + /// + /// 勾选事件改变页面。 + /// + private void PageCheck(object senderRaw, ModBase.RouteEventArgs e) + { + var sender = (MyRadioBox)senderRaw; + // 尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0 + // 若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False) + if (sender.Tag is not null) + PageChange((FormMain.PageSubType)ModBase.Val(sender.Tag)); + } + + public object PageGet(FormMain.PageSubType? ID = null) + { + var targetID = ID ?? PageID; + switch (ID) + { + case 0: + case FormMain.PageSubType.ToolsGameLink: + { + if (ModMain.FrmToolsGameLink is null) + ModMain.FrmToolsGameLink = new PageToolsGameLink(); + return ModMain.FrmToolsGameLink; + } + case FormMain.PageSubType.SetupGameLink: + { + if (ModMain.FrmSetupGameLink is null) + ModMain.FrmSetupGameLink = new PageSetupGameLink(); + return ModMain.FrmSetupGameLink; + } + case FormMain.PageSubType.ToolsTest: + { + if (ModMain.FrmToolsTest is null) + ModMain.FrmToolsTest = new PageToolsTest(); + return ModMain.FrmToolsTest; + } + case FormMain.PageSubType.ToolsLauncherHelp: + { + if (ModMain.FrmToolsHelp is null) + ModMain.FrmToolsHelp = new PageToolsHelp(); + return ModMain.FrmToolsHelp; + } + + default: + { + throw new Exception("未知的更多子页面种类:" + (int)ID); + } + } + } + + /// + /// 切换现有页面。 + /// + public void PageChange(FormMain.PageSubType ID) + { + if (PageID == ID) + return; + ModAnimation.AniControlEnabled += 1; + IsPageSwitched = true; + try + { + PageChangeRun((MyPageRight)PageGet(ID)); + PageID = ID; + } + catch (Exception ex) + { + ModBase.Log(ex, "切换分页面失败(ID " + (int)ID + ")", ModBase.LogLevel.Feedback); + } + finally + { + ModAnimation.AniControlEnabled -= 1; + } + } + + private static void PageChangeRun(MyPageRight Target) + { + ModAnimation.AniStop("FrmMain PageChangeRight"); // 停止主页面的右页面切换动画,防止它与本动画一起触发多次 PageOnEnter + if (Target.Parent is not null) + Target.SetValue(ContentPresenter.ContentProperty, null); + ModMain.FrmMain.PageRight = Target; + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnExit(); + ModAnimation.AniStart(new[] + { + ModAnimation.AaCode(() => + { + ((MyPageRight)ModMain.FrmMain.PanMainRight.Child).PageOnForceExit(); + ModMain.FrmMain.PanMainRight.Child = ModMain.FrmMain.PageRight; + ModMain.FrmMain.PageRight.Opacity = 0d; + }, 130), + ModAnimation.AaCode(() => + { + // 延迟触发页面通用动画,以使得在 Loaded 事件中加载的控件得以处理 + ModMain.FrmMain.PageRight.Opacity = 1d; + ModMain.FrmMain.PageRight.PageOnEnter(); + }, 30, true) + }, "PageLeft PageChange"); + } + + #endregion +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml index 5ecddad05..52991a116 100644 --- a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml @@ -1,82 +1,94 @@ - + - - - - - - + + + + + + - + - - - + + + - - - - + + + + - - - + + + - + - - - - - - + + + + + + - + - + - + - + - + - - + + @@ -98,75 +110,87 @@ - + - + - - - + + + - - - - - + + + + + - - + + - + - - - + + + - + - - - + + + - + - - - + + + - + - - + + - + - - + + - + 64x64 96x96 @@ -175,22 +199,24 @@ - - - - + + + + - - + + - + - - + + - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs new file mode 100644 index 000000000..0be5c031a --- /dev/null +++ b/Plain Craft Launcher 2/Pages/PageTools/PageToolsTest.xaml.cs @@ -0,0 +1,984 @@ +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using Microsoft.VisualBasic; +using Microsoft.VisualBasic.CompilerServices; +using PCL.Core.App; +using PCL.Core.IO; +using PCL.Core.IO.Net; +using PCL.Core.UI; +using PCL.Core.Utils.OS; +using PCL.Core.Utils.Secret; + +namespace PCL; + +public partial class PageToolsTest +{ + private static object IsMemoryOptimizing; + private Bitmap CurrentSkinBitmap; + private Bitmap GeneratedHeadBitmap; + + private int HeadSize = 64; + private string skinPath = ""; + + public PageToolsTest() + { + InitializeComponent(); + BtnSelectSkin.Click += BtnSelectSkin_Click; + CmbHeadSize.SelectionChanged += CmbHeadSize_SelectionChanged; + Loaded += (_, _) => MeLoaded(); + } + + private void MeLoaded() + { + BtnDownloadStart.IsEnabled = false; + + TextDownloadFolder.Text = Conversions.ToString(States.Tool.DownloadFolder); + TextDownloadFolder.Validate(); + + if (!string.IsNullOrEmpty(TextDownloadFolder.ValidateResult) || string.IsNullOrEmpty(TextDownloadFolder.Text)) + TextDownloadFolder.Text = ModBase.ExePath + @"PCL\MyDownload\"; + + TextDownloadFolder.Validate(); + TextDownloadName.Validate(); + TextUserAgent.Text = Conversions.ToString(States.Tool.DownloadUserAgent); + } + + private void StartButtonRefresh() + { + BtnDownloadStart.IsEnabled = string.IsNullOrEmpty(TextDownloadFolder.ValidateResult) && + string.IsNullOrEmpty(TextDownloadUrl.ValidateResult) && + string.IsNullOrEmpty(TextDownloadName.ValidateResult); + + BtnDownloadOpen.IsEnabled = string.IsNullOrEmpty(TextDownloadFolder.ValidateResult); + + BtnAchievementPreview.IsEnabled = string.IsNullOrEmpty(AchievementBlockTextBox.ValidateResult) && + string.IsNullOrEmpty(AchievementTitleTextBox.ValidateResult) && + string.IsNullOrEmpty(AchievementString1TextBox.ValidateResult); + + BtnAchievementSave.IsEnabled = string.IsNullOrEmpty(AchievementBlockTextBox.ValidateResult) && + string.IsNullOrEmpty(AchievementTitleTextBox.ValidateResult) && + string.IsNullOrEmpty(AchievementString1TextBox.ValidateResult); + } + + private void SaveCacheDownloadFolder(object sender, RoutedEventArgs e) + { + States.Tool.DownloadFolder = TextDownloadFolder.Text; + TextDownloadName.Validate(); + } + + private void SaveCustomUserAgent(object sender, RoutedEventArgs e) + { + States.Tool.DownloadUserAgent = TextUserAgent.Text; + } + + private static void DownloadState(ModLoader.LoaderCombo Loader) + { + try + { + switch (Loader.State) + { + case ModBase.LoadState.Finished: + { + ModMain.Hint(Loader.Name + "完成!", ModMain.HintType.Finish); + Interaction.Beep(); + break; + } + case ModBase.LoadState.Failed: + { + ModBase.Log(Loader.Error, Loader.Name + "失败", ModBase.LogLevel.Msgbox); + Interaction.Beep(); + break; + } + case ModBase.LoadState.Aborted: + { + ModMain.Hint(Loader.Name + "已取消!"); + break; + } + } + } + catch (Exception ex) + { + } + } + + public static void StartCustomDownload(string Url, string FileName, string Folder = null, string UserAgent = "") + { + try + { + if (string.IsNullOrWhiteSpace(Folder)) + { + Folder = SystemDialogs.SelectSaveFile("选择文件保存位置", FileName); + if (!Folder.Contains(@"\")) return; + if (Folder.EndsWith(FileName)) Folder = Strings.Mid(Folder, 1, Folder.Length - FileName.Length); + } + + Folder = Folder.Replace("/", @"\").TrimEnd(new[] { '\\' }) + @"\"; + try + { + Directory.CreateDirectory(Folder); + ModBase.CheckPermissionWithException(Folder); + } + catch (Exception ex) + { + ModBase.Log(ex, "访问文件夹失败(" + Folder + ")", ModBase.LogLevel.Hint); + return; + } + + ModBase.Log("[Download] 自定义下载文件名:" + FileName); + ModBase.Log("[Download] 自定义下载文件目标:" + Folder); + var uuid = ModBase.GetUuid(); + ModLoader.LoaderBase loaderdownload; + if (string.IsNullOrEmpty(new ValidateHttp().Validate(Url))) + loaderdownload = new ModNet.LoaderDownload("自定义下载文件:" + FileName + " ", + new List { new(new[] { Url }, Folder + FileName, null, true, UserAgent) }); + else // UNC 路径 + loaderdownload = new ModNet.LoaderDownloadUnc("自定义下载文件:" + FileName + " ", + new Tuple(Url, Folder + FileName)); + var loaderCombo = new ModLoader.LoaderCombo("自定义下载 (" + uuid + ") ", new[] { loaderdownload }) + { OnStateChanged = a => DownloadState((ModLoader.LoaderCombo)a) }; + loaderCombo.Start(); + ModLoader.LoaderTaskbarAdd(loaderCombo); + ModMain.FrmMain.BtnExtraDownload.ShowRefresh(); + ModMain.FrmMain.BtnExtraDownload.Ribble(); + } + + catch (Exception ex) + { + ModBase.Log(ex, "开始自定义下载失败", ModBase.LogLevel.Feedback); + } + } + + public static void Jrrp() + { + var random = new Random(GenerateDailySeed()); + var luckValue = random.Next(0, 101); + var rating = GetRating(luckValue); + var currentDate = DateTime.Now.ToString("yyyy/MM/dd"); + var title = $"今日人品 - {currentDate}"; + + if (luckValue >= 60) + ModMain.MyMsgBox($"你今天的人品值是:{luckValue}!{rating}", title); + else + ModMain.MyMsgBox($"你今天的人品值是:{luckValue}... {rating}", title, IsWarn: luckValue <= 30); + } + + public static void RubbishClear() + { + ModBase.RunInUi(() => + { + if (!(ModMain.FrmToolsTest == null) && !(ModMain.FrmToolsTest.BtnClear == null)) + ModMain.FrmToolsTest.BtnClear.IsEnabled = false; + }); + // 只有当没有运行中的Minecraft游戏且启动器不在加载状态时才能清理 + + // 清理的文件数量 + // 所有 Minecraft 文件夹 + + + // 寻找所有 Minecraft 文件夹 + + // 删除 Minecraft 的缓存 + // 删除日志和崩溃报告并计数 + + // 删除 Natives 文件 + + // 删除 PCL 的缓存 + + ModBase.RunInNewThread(() => + { + try + { + if (!ModWatcher.HasRunningMinecraft && ModLaunch.McLaunchLoader.State != ModBase.LoadState.Loading) + { + if (ModNet.HasDownloadingTask()) + { + ModMain.Hint("请在所有下载任务完成后再来清理吧……"); + return; + } + + if (!ModMinecraft.McFolderList.Any()) ModMinecraft.McFolderListLoader.Start(); + if (Conversions.ToBoolean( + Operators.ConditionalCompareObjectLessEqual(States.Hint.CleanJunkFile, 2, + false))) + { + if (ModMain.MyMsgBox( + "即将清理游戏日志、错误报告、缓存等文件。" + "\r\n" + "虽然应该没人往这些地方放重要文件,但还是问一下,是否确认继续?" + + "\r\n" + "\r\n" + "在完成清理后,PCL 将自动重启。", "清理确认", "确定", "取消") == + 2) return; + States.Hint.CleanJunkFile += 1; + } + + var num = 0; + var cleanMcFolderList = new List(); + if (!ModMinecraft.McFolderList.Any()) ModMinecraft.McFolderListLoader.WaitForExit(); + foreach (var mcFolder in ModMinecraft.McFolderList) + { + cleanMcFolderList.Add(new DirectoryInfo(mcFolder.Location)); + var dirInfo = new DirectoryInfo(mcFolder.Location + "versions"); + if (dirInfo.Exists) + foreach (var item in dirInfo.EnumerateDirectories()) + cleanMcFolderList.Add(item); + } + + foreach (var dirInfo in cleanMcFolderList) + { + num += ModBase.DeleteDirectory( + dirInfo.FullName + (dirInfo.FullName.EndsWith(@"\") ? "" : @"\") + @"crash-reports\", true); + num += ModBase.DeleteDirectory( + dirInfo.FullName + (dirInfo.FullName.EndsWith(@"\") ? "" : @"\") + @"logs\", true); + foreach (var fileInfo in dirInfo.EnumerateFiles("*")) + if (fileInfo.Name.StartsWith("hs_err_pid") || fileInfo.Name.EndsWith(".log") || + fileInfo.Name == "WailaErrorOutput.txt") + { + fileInfo.Delete(); + num += 1; + } + + foreach (var dirInfo2 in dirInfo.EnumerateDirectories()) + if ((dirInfo2.Name ?? "") == (dirInfo2.Name + "-natives" ?? "") || + dirInfo2.Name == "natives-windows-x86_64") + num += ModBase.DeleteDirectory(dirInfo2.FullName, true); + } + + num += ModBase.DeleteDirectory(ModBase.PathTemp, true); + num += ModBase.DeleteDirectory(ModBase.OsDrive + @"ProgramData\PCL\", true); + if (num != 0) + { + ModMain.MyMsgBox(string.Format("清理了 {0} 个文件!", num) + "\r\n" + "PCL 即将自动重启……", + "缓存已清理", "确定", "", "", false, true, true); + Process.Start(new ProcessStartInfo(ModBase.ExePathWithName)); + FormMain.EndProgramForce(); + } + else + { + ModMain.Hint("没有找到任何可以清理的文件!"); + } + } + else + { + ModMain.Hint("请先关闭所有运行中的游戏……"); + } + } + catch (Exception ex) + { + ModBase.Log(ex, "清理垃圾失败", ModBase.LogLevel.Hint); + } + finally + { + ModBase.RunInUiWait(() => + { + if (!(ModMain.FrmToolsTest == null) && !(ModMain.FrmToolsTest.BtnClear == null)) + ModMain.FrmToolsTest.BtnClear.IsEnabled = true; + }); + } + }, "Rubbish Clear"); + } + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] + private static extern nint GetCurrentProcess(); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern bool CloseHandle(nint handle); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern bool OpenProcessToken(HandleRef ProcessHandle, int DesiredAccess, out nint TokenHandle); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern bool LookupPrivilegeValue([MarshalAs(UnmanagedType.LPTStr)] string lpSystemName, + [MarshalAs(UnmanagedType.LPTStr)] string lpName, out LUID lpLuid); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern bool AdjustTokenPrivileges(HandleRef TokenHandle, bool DisableAllPrivileges, + TokenPrivileges NewState, int BufferLength, nint PreviousState, nint ReturnLength); + + [DllImport("ntdll.dll", CharSet = CharSet.Ansi)] + private static extern uint NtSetSystemInformation(int SystemInformationClass, nint SystemInformation, + int SystemInformationLength); + + public static void MemoryOptimize(bool ShowHint) + { + if (Conversions.ToBoolean(IsMemoryOptimizing)) + { + if (ShowHint) ModMain.Hint("内存优化尚未结束,请稍等!"); + } + else + { + IsMemoryOptimizing = true; + long num; + if (ProcessInterop.IsAdmin()) + { + num = (long)KernelInterop.GetAvailablePhysicalMemoryBytes(); + try + { + MemoryOptimizeInternal(ShowHint); + } + catch (Exception ex) + { + ModBase.Log(ex, "内存优化失败", ShowHint ? ModBase.LogLevel.Hint : ModBase.LogLevel.Debug); + return; + } + finally + { + IsMemoryOptimizing = false; + } + + num = Convert.ToInt64(decimal.Subtract(new decimal(KernelInterop.GetAvailablePhysicalMemoryBytes()), + new decimal(num))); + } + else + { + ModBase.Log("[Test] 没有管理员权限,将以命令行方式进行内存优化"); + try + { + num = ProcessInterop.StartAsAdmin("--memory").ExitCode * 1024L; + } + catch (Exception ex2) + { + ModBase.Log(ex2, "命令行形式内存优化失败"); + if (ShowHint) + ModMain.Hint( + string.Concat("获取管理员权限失败,请尝试右键 PCL,选择 ", Conversions.ToString(ModBase.vbLQ), "以管理员身份运行", + Conversions.ToString(ModBase.vbRQ), "!"), ModMain.HintType.Critical); + return; + } + finally + { + IsMemoryOptimizing = false; + } + + if (num < 0L) return; + } + + var MemAfter = ModBase.GetString((long)KernelInterop.GetAvailablePhysicalMemoryBytes()); + ModBase.Log(string.Format("[Test] 内存优化完成,可用内存改变量:{0},大致剩余内存:{1}", ModBase.GetString(num), MemAfter)); + if (num > 0L) + { + if (ShowHint) + ModMain.Hint( + string.Format("内存优化完成,可用内存增加了 {0},目前剩余内存 {1}!", + ModBase.GetString((long)Math.Round(Math.Round(num * 0.8d))), MemAfter), + ModMain.HintType.Finish); + } + else if (ShowHint) + { + ModMain.Hint(string.Format("内存优化完成,已经优化到了最佳状态,目前剩余内存 {0}!", MemAfter)); + } + } + } + + public static void MemoryOptimizeInternal(bool ShowHint) + { + if (!ProcessInterop.IsAdmin()) + throw new Exception("内存优化功能需要管理员权限!" + "\r\n" + + "如果需要自动以管理员身份启动 PCL,可以右键 PCL,打开 属性 → 兼容性 → 以管理员身份运行此程序。"); + ModBase.Log("[Test] 获取内存优化权限"); + + // 提权部分 + try + { + var processId = GetCurrentProcess(); + LUID luid1 = default; + LUID luid2 = default; + nint hToken = 0; + if (OpenProcessToken(new HandleRef(null, processId), 32, out hToken)) + { + string arglpSystemName = null; + var arglpName = "SeProfileSingleProcessPrivilege"; + LookupPrivilegeValue(arglpSystemName, arglpName, out luid1); + string arglpSystemName1 = null; + var arglpName1 = "SeIncreaseQuotaPrivilege"; + LookupPrivilegeValue(arglpSystemName1, arglpName1, out luid2); + + var tokenPrivileges1 = new TokenPrivileges(); + tokenPrivileges1.Luid = luid1; + tokenPrivileges1.Attributes = 2; + var tokenPrivileges2 = new TokenPrivileges(); + tokenPrivileges2.Luid = luid2; + tokenPrivileges2.Attributes = 2; + + AdjustTokenPrivileges(new HandleRef(null, hToken), false, tokenPrivileges1, 0, nint.Zero, nint.Zero); + AdjustTokenPrivileges(new HandleRef(null, hToken), false, tokenPrivileges2, 0, nint.Zero, nint.Zero); + + CloseHandle(hToken); + } + } + catch (Exception) + { + throw new Exception(string.Format("获取内存优化权限失败(错误代码:{0})", Marshal.GetLastWin32Error())); + } + + if (ShowHint) ModMain.Hint("正在进行内存优化……"); + + // 内存优化部分 + var NowType = "None"; + try + { + int info; + var scfi = default(SYSTEM_FILECACHE_INFORMATION); + var combineInfoEx = default(MEMORY_COMBINE_INFORMATION_EX); + GCHandle _gcHandle; + + NowType = "MemoryEmptyWorkingSets"; + info = 2; + _gcHandle = GCHandle.Alloc(info, GCHandleType.Pinned); + NtSetSystemInformation(80, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(info)); + _gcHandle.Free(); + NowType = "SystemFileCacheInformation"; + scfi.MaximumWorkingSet = uint.MaxValue; + scfi.MinimumWorkingSet = uint.MaxValue; + _gcHandle = GCHandle.Alloc(scfi, GCHandleType.Pinned); + NtSetSystemInformation(81, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(scfi)); + _gcHandle.Free(); + NowType = "MemoryFlushModifiedList"; + info = 3; + _gcHandle = GCHandle.Alloc(info, GCHandleType.Pinned); + NtSetSystemInformation(80, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(info)); + _gcHandle.Free(); + NowType = "MemoryPurgeStandbyList"; + info = 4; + _gcHandle = GCHandle.Alloc(info, GCHandleType.Pinned); + NtSetSystemInformation(80, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(info)); + _gcHandle.Free(); + NowType = "MemoryPurgeLowPriorityStandbyList"; + info = 5; + _gcHandle = GCHandle.Alloc(info, GCHandleType.Pinned); + NtSetSystemInformation(80, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(info)); + _gcHandle.Free(); + NowType = "SystemRegistryReconciliationInformation"; + NtSetSystemInformation(155, new nint(default(int)), 0); + NowType = "SystemCombinePhysicalMemoryInformation"; + _gcHandle = GCHandle.Alloc(combineInfoEx, GCHandleType.Pinned); + NtSetSystemInformation(130, _gcHandle.AddrOfPinnedObject(), Marshal.SizeOf(combineInfoEx)); + _gcHandle.Free(); + } + catch (Exception) + { + throw new Exception(string.Format("内存优化操作 {0} 失败(错误代码:{1})", NowType)); + } + } + + public static string GetRandomCave() + { + return "为便于维护,社区版中不包含百宝箱功能……"; + } + + public static string GetRandomHint() + { + return "为便于维护,社区版中不包含百宝箱功能……"; + } + + public static string GetRandomPresetHint() + { + return "为便于维护,社区版中不包含百宝箱功能……"; + } + + private void TextDownloadUrl_TextChanged(object sender, TextChangedEventArgs e) + { + try + { + if (!string.IsNullOrEmpty(TextDownloadName.Text) || string.IsNullOrEmpty(TextDownloadUrl.Text)) return; + TextDownloadName.Text = ModBase.GetFileNameFromPath(WebUtility.UrlDecode(TextDownloadUrl.Text)); + } + catch + { + } + } + + private void MyTextButton_Click(object sender, EventArgs e) + { + var text = SystemDialogs.SelectFolder(); + if (!string.IsNullOrEmpty(text)) TextDownloadFolder.Text = text; + } + + private void BtnDownloadOpen_Click(object sender, MouseButtonEventArgs e) + { + try + { + var text = TextDownloadFolder.Text; + Directory.CreateDirectory(text); + Basics.OpenPath(text); + } + catch (Exception ex) + { + ModBase.Log(ex, "打开下载文件夹失败"); + } + } + + private void BtnDownloadStart_Click(object sender, MouseButtonEventArgs e) + { + StartCustomDownload(TextDownloadUrl.Text, TextDownloadName.Text, TextDownloadFolder.Text, TextUserAgent.Text); + TextDownloadUrl.Text = ""; + TextDownloadUrl.Validate(); + TextDownloadUrl.ForceShowAsSuccess(); + TextDownloadName.Text = ""; + TextDownloadName.Validate(); + TextDownloadName.ForceShowAsSuccess(); + StartButtonRefresh(); + } + + private void TextDownloadUrl_ValidateChanged(object sender, RoutedEventArgs e) + { + StartButtonRefresh(); + } + + private void TextDownloadFolder_ValidateChanged(object sender, EventArgs e) + { + StartButtonRefresh(); + } + + private void TextDownloadName_ValidateChanged(object sender, EventArgs e) + { + StartButtonRefresh(); + } + + private void BtnClear_Click(object sender, MouseButtonEventArgs e) + { + RubbishClear(); + } + + private void BtnMemory_Click(object sender, MouseButtonEventArgs e) + { + ModBase.RunInThread(() => MemoryOptimize(true)); + } + + // 下载正版玩家皮肤 + private void BtnSkinSave_Click(object sender, MouseButtonEventArgs e) + { + var ID = TextSkinID.Text; + ModMain.Hint("正在获取皮肤..."); + ModBase.RunInNewThread(() => + { + try + { + if (ID.Length < 3) + { + ModMain.Hint("这不是一个有效的 ID..."); + } + else + { + var Result = Conversions.ToString(ModProfile.McLoginMojangUuid(ID, true)); + Result = ModMinecraft.McSkinGetAddress(Result, "Mojang"); + Result = ModMinecraft.McSkinDownload(Result); + ModBase.RunInUi(() => + { + var Path = SystemDialogs.SelectSaveFile("保存皮肤", ID + ".png", "皮肤图片文件(*.png)|*.png"); + ModBase.CopyFile(Result, Path); + ModMain.Hint($"玩家 {ID} 的皮肤已保存!", ModMain.HintType.Finish); + }); + } + } + catch (Exception ex) + { + if (ex.ToString().Contains("429")) + { + ModMain.Hint("获取皮肤太过频繁,请 5 分钟之后再试!", ModMain.HintType.Critical); + ModBase.Log("获取正版皮肤失败(" + ID + "):获取皮肤太过频繁,请 5 分钟后再试!"); + } + else + { + ModBase.Log(ex, "获取正版皮肤失败(" + ID + ")"); + } + } + }); + } + + // 今日人品 + private void BtnLuck_Click(object sender, MouseButtonEventArgs e) + { + Jrrp(); + } + + public static int GenerateDailySeed() + { + var datePart = DateTime.Today.ToString("yyyyMMdd"); + + return DJB2Hash(datePart + Identify.LauncherId); + } + + private static int DJB2Hash(string str) + { + var hash = 5381L; + var prime = 33L; + foreach (var c in str) + { + long charValue = Strings.AscW(c); + hash = (hash * prime + charValue) % 0x100000000L; + } + + return (int)(hash & 0x7FFFFFFFL); + } + + public static string GetRating(int luckValue) + { + if (luckValue == 100) return "100!100!" + "\r\n" + "隐藏主题 欧皇…… 不对,社区版应该没有这玩意……"; + + return luckValue >= 95 ? "差一点就到100了呢..." : + luckValue >= 90 ? "好评如潮!" : + luckValue >= 60 ? "还行啦,还行啦" : + luckValue >= 40 ? "勉强还行吧..." : + luckValue >= 30 ? "呜..." : + luckValue >= 10 ? "不会吧!" : "(是百分制哦)"; + } + + private void BtnCreateShortcut_Click(object sender, MouseButtonEventArgs e) + { + const string shortcutName = "PCL 社区版.lnk"; + const string desktopName = "桌面"; + const string startName = "开始菜单"; + var desktop = Paths.GetSpecialPath(Environment.SpecialFolder.Desktop, shortcutName); + var start = Paths.GetSpecialPath(Environment.SpecialFolder.StartMenu, @"Programs\" + shortcutName); + var choice = + ModMain.MyMsgBox( + "这个快捷方式不会自动移除,在删除/移动启动器前请手动移除快捷方式。" + "\r\n" + "\r\n" + desktopName + "位置: " + + desktop + "\r\n" + startName + "位置: " + start, "选择快捷方式位置", "取消", desktopName, startName); + if (choice == 1) + return; + var shortcutPath = choice == 2 ? desktop : start; + var locationName = choice == 2 ? desktopName : startName; + Files.CreateShortcut(shortcutPath, Basics.ExecutablePath); + ModMain.Hint("已在" + locationName + "创建快捷方式", ModMain.HintType.Finish); + } + + // 启动计数显示 + private void BtnLaunchCount_Click(object sender, MouseButtonEventArgs e) + { + var launchCount = Conversions.ToInteger(States.System.LaunchCount); + ModMain.MyMsgBox($"PCL 已经为你启动了 {launchCount} 次游戏了。", "启动次数"); + } + + private async void BtnAchievementPreview_Click(object sender, MouseButtonEventArgs e) + { + var url = GetAchievementUrl(); + ModBase.Log("[Net] 获取网络结果" + url); + await LoadImageAsync(url); + } + + private async Task LoadImageAsync(string imageUrl) + { + var client = NetworkService.GetClient(); + try + { + var response = await client.GetAsync(imageUrl); + if (response.IsSuccessStatusCode) + using (var stream = await response.Content.ReadAsStreamAsync()) + { + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.StreamSource = stream; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + Dispatcher.Invoke(() => + { + AchievementImage.Source = bitmapImage; + AchievementImage.Visibility = Visibility.Visible; + }); + } + else if (response.StatusCode == HttpStatusCode.NotFound) + Dispatcher.Invoke(() => + { + ModBase.Log("获取成就图片失败(404)"); + ModMain.Hint("获取成就图片失败,请检查文字是否包含特殊字符", ModMain.HintType.Critical); + }); + else + Dispatcher.Invoke(() => ModBase.Log("获取成就图片失败(" + (int)response.StatusCode + ")")); + } + + catch (Exception ex) + { + Dispatcher.Invoke(() => ModBase.Log(ex, "获取成就图片失败")); + } + } + + private async void BtnAchievementSave_Click(object sender, MouseButtonEventArgs e) + { + var url = GetAchievementUrl(); + await DownloadImageToLocalAsync(url); + } + + private async Task DownloadImageToLocalAsync(string imageUrl) + { + var savePath = ModBase.PathTemp + @"Download\" + ModBase.GetHash(imageUrl) + ".png"; + var client = NetworkService.GetClient(); + try + { + // 异步发送 GET 请求 + var response = await client.GetAsync(imageUrl); + + // 如果响应状态码是成功的,则继续 + if (response.IsSuccessStatusCode) + { + // 异步读取响应内容为字节流 + var imageBytes = await response.Content.ReadAsByteArrayAsync(); + + // 将字节写入本地文件 + File.WriteAllBytes(savePath, imageBytes); + + var path = + SystemDialogs.SelectSaveFile("保存皮肤", AchievementTitleTextBox.Text + ".png", "PNG 图片|*.png"); + if (string.IsNullOrEmpty(path)) + { + ModBase.Log("用户取消了保存操作"); + File.Delete(savePath); + return; + } + + ModBase.CopyFile(savePath, path); + File.Delete(savePath); + ModMain.Hint("自定义成就图片已保存!", ModMain.HintType.Finish); + } + // 下载成功,返回 True + else if (response.StatusCode == HttpStatusCode.NotFound) + { + // 捕获 404 错误 + ModBase.Log("获取成就图片失败(404)"); + ModMain.Hint("获取成就图片失败,请检查文字是否包含特殊字符", ModMain.HintType.Critical); + } + else + { + // 处理其他非成功状态码 + ModBase.Log("获取成就图片失败(" + (int)response.StatusCode + ")"); + } + } + + catch (Exception ex) + { + // 捕获所有其他异常(如网络连接问题) + ModBase.Log(ex, "获取成就图片失败"); + } + } + + private string GetAchievementUrl() + { + var block = AchievementBlockTextBox.Text.Trim(); + var title = AchievementTitleTextBox.Text.Replace(" ", ".."); + var str1 = AchievementString1TextBox.Text.Replace(" ", ".."); + var str2 = AchievementString2TextBox.Text.Replace(" ", ".."); + var url = $"https://minecraft-api.com/api/achivements/{block}/{title}/{str1}"; + if (!string.IsNullOrEmpty(str2)) url += $"/{str2}"; + return url; + } + + private void BtnCrash_Click(object sender, MouseButtonEventArgs e) + { + if (ModMain.MyMsgBoxInput("崩溃确认", "你一定是点错了,如果没错请在下方确认", "确认", HintText: "\"sURe\".ToUpper()", IsWarn: true) == + "SURE") throw new Exception("手动崩溃"); + } + + private int GetHeadSize() + { + switch (CmbHeadSize.SelectedIndex) + { + case 0: + { + return 64; + } + case 1: + { + return 96; + } + case 2: + { + return 128; + } + + default: + { + return 64; + } + } + } + + private void BtnSelectSkin_Click(object sender, RoutedEventArgs e) + { + var filePath = SystemDialogs.SelectFile("图像文件(*.png)|*.png", "选择皮肤文件"); + if (!string.IsNullOrEmpty(filePath)) LoadAndGenerateHead(filePath); + } + + private void LoadAndGenerateHead(string skinPath) + { + try + { + using (var stream = new FileStream(skinPath, FileMode.Open, FileAccess.Read)) + { + CurrentSkinBitmap = new Bitmap(stream); + } + + this.skinPath = skinPath; + + if (CurrentSkinBitmap.Width != CurrentSkinBitmap.Height) + { + ModMain.Hint("图片的大小不正确!请确认你选择了正确的文件!", ModMain.HintType.Critical); + SkinPreviewBorder.Visibility = Visibility.Collapsed; + return; + } + + GeneratedHeadBitmap = GenerateHeadFromSkin(CurrentSkinBitmap); + + ImgFace.Source = BitmapToBitmapImage(GeneratedHeadBitmap); + ImgHair.Source = null; + + SkinPreviewBorder.Visibility = Visibility.Visible; + ModMain.Hint("头像生成成功!", ModMain.HintType.Finish); + } + + catch (Exception ex) + { + ModBase.Log(ex, "生成头像失败"); + ModMain.Hint("生成头像失败:" + ex.Message, ModMain.HintType.Critical); + SkinPreviewBorder.Visibility = Visibility.Collapsed; + } + } + + private Bitmap GenerateHeadFromSkin(Bitmap skinBitmap) + { + var scale = skinBitmap.Width / 64; + HeadSize = GetHeadSize(); + var headBitmap = new Bitmap(HeadSize, HeadSize); + + using (var g = Graphics.FromImage(headBitmap)) + { + g.InterpolationMode = InterpolationMode.NearestNeighbor; + g.PixelOffsetMode = PixelOffsetMode.Half; + + DrawFaceLayer(g, skinBitmap, scale); + if (skinBitmap.Width >= 64) DrawHairLayer(headBitmap, skinBitmap, scale); + } + + return headBitmap; + } + + private void DrawFaceLayer(Graphics g, Bitmap skinBitmap, int scale) + { + var faceRect = new Rectangle(8 * scale, 8 * scale, 8 * scale, 8 * scale); + var faceSize = HeadSize - HeadSize / 8; + var faceScaled = new Bitmap(faceSize, faceSize); + + using (var gFace = Graphics.FromImage(faceScaled)) + { + gFace.InterpolationMode = InterpolationMode.NearestNeighbor; + gFace.PixelOffsetMode = PixelOffsetMode.Half; + gFace.DrawImage(skinBitmap, new Rectangle(0, 0, faceSize, faceSize), faceRect, GraphicsUnit.Pixel); + } + + var offset = HeadSize / 16; + g.DrawImage(faceScaled, offset, offset, faceSize, faceSize); + } + + private void DrawHairLayer(Bitmap headBitmap, Bitmap skinBitmap, int scale) + { + var hairRect = new Rectangle(40 * scale, 8 * scale, 8 * scale, 8 * scale); + var hairScaled = new Bitmap(HeadSize, HeadSize); + + using (var gHair = Graphics.FromImage(hairScaled)) + { + gHair.InterpolationMode = InterpolationMode.NearestNeighbor; + gHair.PixelOffsetMode = PixelOffsetMode.Half; + gHair.DrawImage(skinBitmap, new Rectangle(0, 0, HeadSize, HeadSize), hairRect, GraphicsUnit.Pixel); + } + + for (int x = 0, loopTo = HeadSize - 1; x <= loopTo; x++) + for (int y = 0, loopTo1 = HeadSize - 1; y <= loopTo1; y++) + { + var pixel = hairScaled.GetPixel(x, y); + if (pixel.A > 0) headBitmap.SetPixel(x, y, pixel); + } + } + + private void BtnSaveHead_Click(object sender, MouseButtonEventArgs e) + { + if (GeneratedHeadBitmap is null) + { + ModMain.Hint("请先选择皮肤!", ModMain.HintType.Critical); + return; + } + + var savePath = SystemDialogs.SelectSaveFile("保存头像", "Head.png"); + if (string.IsNullOrEmpty(savePath)) + return; + + GeneratedHeadBitmap.Save(savePath, ImageFormat.Png); + ModMain.Hint("头像保存成功!", ModMain.HintType.Finish); + } + + private void CmbHeadSize_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (CurrentSkinBitmap is not null && skinPath is not null) LoadAndGenerateHead(skinPath); + } + + private BitmapImage BitmapToBitmapImage(Bitmap bitmap) + { + using (var memoryStream = new MemoryStream()) + { + bitmap.Save(memoryStream, ImageFormat.Png); + memoryStream.Position = 0L; + + var bitmapImage = new BitmapImage(); + bitmapImage.BeginInit(); + bitmapImage.CacheOption = BitmapCacheOption.OnLoad; + bitmapImage.StreamSource = memoryStream; + bitmapImage.EndInit(); + bitmapImage.Freeze(); + + return bitmapImage; + } + } + + private void TextDownloadFolder_OnValidatedTextChanged(object sender, RoutedEventArgs e) + { + SaveCacheDownloadFolder(sender, e); + TextDownloadName_ValidateChanged(sender, e); + } + + private void TextUserAgent_OnValidatedTextChanged(object sender, RoutedEventArgs e) + { + SaveCustomUserAgent(sender, e); + TextDownloadFolder_ValidateChanged(sender, e); + } + + [StructLayout(LayoutKind.Sequential)] + private class TokenPrivileges + { + public int PrivilegeCount = 1; + public LUID Luid; + public int Attributes; + } + + private struct LUID + { + public int LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_FILECACHE_INFORMATION + { + public nuint CurrentSize; + public nuint PeakSize; + public uint PageFaultCount; + public nuint MinimumWorkingSet; + public nuint MaximumWorkingSet; + public nuint CurrentSizeIncludingTransitionInPages; + public nuint PeakSizeIncludingTransitionInPages; + public uint TransitionRePurposeCount; + public uint Flags; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MEMORY_COMBINE_INFORMATION_EX + { + public nint Handle; + public nuint PagesCombined; + public uint Flags; + } +} diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj new file mode 100644 index 000000000..095c2b428 --- /dev/null +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.csproj @@ -0,0 +1,122 @@ + + + enable + net8.0-windows + WinExe + PCL + Custom + false + PCL.Program + publish\ + true + true + win-x64 + win-arm64 + true + true + zh-CN + false + true + 14 + true + Beta;Debug;Release;CI + AnyCPU;x64;ARM64 + enable + app.manifest + Images\icon.ico + Plain Craft Launcher Community Edition + Minecraft 启动器 (作者: 龙腾猫跃; 经社区二次开发版本) + Plain Craft Launcher Community Edition + Copyright © 龙腾猫跃 2016. All Rights Reserved. + 2.14.2.0 + 2.14.2.0 + + 41999;42016;42017;42018;42019;42020;42021;42022;42032;42036;42314; + VSTHRD002;VSTHRD003;VSTHRD110;VSTHRD200 + + $(DefaultItemExcludes);$(ProjectDir)**\*.vb + + + bin\Debug\ + true + full + false + TRACE;DEBUG + + + bin\CI\ + true + full + false + TRACE;DEBUGCI + + + bin\Release\ + false + none + true + RELEASE + + + bin\Beta\ + false + none + true + BETA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + false + false + false + false + false + + + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj index 713fdf492..051a8c036 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj @@ -1,4 +1,4 @@ - + net8.0-windows WinExe @@ -27,8 +27,8 @@ Minecraft 启动器 (作者: 龙腾猫跃; 经社区二次开发版本) Plain Craft Launcher Community Edition Copyright © 龙腾猫跃 2016. All Rights Reserved. - 2.14.2.0 - 2.14.2.0 + 2.14.3.0 + 2.14.3.0 41999;42016;42017;42018;42019;42020;42021;42022;42032;42036;42314; VSTHRD002;VSTHRD003;VSTHRD110;VSTHRD200 @@ -70,11 +70,11 @@ - - + + - + diff --git a/Plain Craft Launcher 2/Program.cs b/Plain Craft Launcher 2/Program.cs new file mode 100644 index 000000000..c6e2e5110 --- /dev/null +++ b/Plain Craft Launcher 2/Program.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Windows.Input; +using PCL.Core.App; +using PCL.Core.App.Essentials; +using PCL.Core.App.IoC; + +namespace PCL; + +internal static class Program +{ + [DllImport("kernel32.dll")] + private static extern bool AllocConsole(); + + /// + /// Program startup point + /// + [STAThread] + public static void Main() + { + if (Basics.CommandLineArguments.Contains("--console")) AllocConsole(); +#if DEBUG + if (Basics.CommandLineArguments.Contains("--debug")) + while (!Debugger.IsAttached) + Thread.Sleep(50); +#endif + Console.WriteLine("Welcome to Plain Craft Launcher 2 Community Edition!"); + // Preloading tasks + ApplicationService.Loading = static () => + { + var app = new Application(); + app.InitializeComponent(); + return app; + }; + MainWindowService.Loading = static () => + { + var form = new FormMain(); + return form; + }; + // From dotnet/wpf #2393: fix tablet devices broken on .NET Core 3.0+ + _ = Tablet.TabletDevices; + // Start lifecycle + Lifecycle.OnInitialize(); + } +} \ No newline at end of file diff --git a/Plain Craft Launcher 2/Resources/Custom.xml b/Plain Craft Launcher 2/Resources/Custom.xml index d3d94acb6..a9693beec 100644 --- a/Plain Craft Launcher 2/Resources/Custom.xml +++ b/Plain Craft Launcher 2/Resources/Custom.xml @@ -6,384 +6,411 @@ + Text="每个 local:MyCard 代表一张卡片,你可以添加、删除格式类似的 MyCard 来添加多个卡片。每个 TextBlock 代表一段文本,你可以在 Text 属性中书写任何你想写的内容,也可以自行添加更多的 TextBlock。"/> + Text="你可以通过添加、删除属性修改样式,例如上一行的 FontSize 就会将字号改为 11 号。"/> + Text="它还有许多可以调整的属性:上一行的 Margin 调整了边距,Foreground 则会让文字变色。"/> - - - - - + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + - - + - - - + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - + + + + - + diff --git a/Plain Craft Launcher 2/Resources/Markdown.xaml b/Plain Craft Launcher 2/Resources/Markdown.xaml index 696b12eb8..91cdc8f57 100644 --- a/Plain Craft Launcher 2/Resources/Markdown.xaml +++ b/Plain Craft Launcher 2/Resources/Markdown.xaml @@ -8,9 +8,9 @@ - - - + + + @@ -77,7 +77,7 @@ @@ -140,11 +140,11 @@ - + SelectionBrush="{DynamicResource ColorBrush6}" /> diff --git a/Plain Craft Launcher 2/Resources/log4j2-debug.xml b/Plain Craft Launcher 2/Resources/log4j2-debug.xml index 088bccaba..d9dccd901 100644 --- a/Plain Craft Launcher 2/Resources/log4j2-debug.xml +++ b/Plain Craft Launcher 2/Resources/log4j2-debug.xml @@ -2,10 +2,10 @@ - + - + @@ -14,7 +14,8 @@ - + diff --git a/Plain Craft Launcher 2/Resources/log4j2-legacy-debug.xml b/Plain Craft Launcher 2/Resources/log4j2-legacy-debug.xml index 7fabc697d..6d917794c 100644 --- a/Plain Craft Launcher 2/Resources/log4j2-legacy-debug.xml +++ b/Plain Craft Launcher 2/Resources/log4j2-legacy-debug.xml @@ -2,10 +2,10 @@ - + - + @@ -14,7 +14,8 @@ - + diff --git a/Plain Craft Launcher 2/Resources/oauth-complete.html b/Plain Craft Launcher 2/Resources/oauth-complete.html index 5644d587c..cfaa34b16 100644 --- a/Plain Craft Launcher 2/Resources/oauth-complete.html +++ b/Plain Craft Launcher 2/Resources/oauth-complete.html @@ -2,9 +2,9 @@ - - - + + + PCL 社区版提示页