From e6c69750475593f6311c83bb33f0580c072667fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=99=E8=85=BE=E7=8C=AB=E8=B7=83?= <1043137532@qq.com> Date: Tue, 18 Jul 2023 04:45:38 +0800 Subject: [PATCH] 2.6.4 --- Plain Craft Launcher 2/Application.xaml.vb | 10 +- .../Controls/MyButton.xaml.vb | 21 +- Plain Craft Launcher 2/Controls/MyCard.vb | 3 + .../Controls/MyExtraButton.xaml.vb | 2 +- .../Controls/MyHint.xaml.vb | 4 +- .../Controls/MyIconTextButton.xaml | 9 + .../Controls/MyIconTextButton.xaml.vb | 180 ++++ .../Controls/MyListItem.xaml.vb | 30 +- .../Controls/MyLoading.xaml.vb | 4 +- .../Controls/MyRadioButton.xaml | 2 +- .../Controls/MySearchBox.xaml.vb | 6 +- .../Controls/MyTextButton.vb | 4 +- Plain Craft Launcher 2/FormMain.xaml | 2 +- Plain Craft Launcher 2/FormMain.xaml.vb | 53 +- .../Modules/Base/ModBase.vb | 8 +- .../Modules/Base/MyBitmap.vb | 4 +- .../Modules/Minecraft/ModComp.vb | 47 +- .../Modules/Minecraft/ModCrash.vb | 103 +- .../Modules/Minecraft/ModJava.vb | 37 +- .../Modules/Minecraft/ModLaunch.vb | 30 +- .../Modules/Minecraft/ModMinecraft.vb | 954 +----------------- .../Modules/Minecraft/ModMod.vb | 903 +++++++++++++++++ .../Modules/Minecraft/ModModpack.vb | 28 +- .../Modules/Minecraft/ModWatcher.vb | 2 +- Plain Craft Launcher 2/Modules/ModEvent.vb | 16 +- Plain Craft Launcher 2/Modules/ModMusic.vb | 4 +- .../Modules/Reference/CookieWebClient.vb | 8 +- .../My Project/AssemblyInfo.vb | 4 +- .../Pages/PageDownload/ModDownloadLib.vb | 45 +- .../PageDownload/PageDownloadInstall.xaml.vb | 25 +- .../Pages/PageLaunch/MySkin.xaml.vb | 15 +- .../Pages/PageLink/PageLinkHiper.xaml.vb | 1 + .../Pages/PageLink/PageLinkIoi.xaml.vb | 1 + .../Pages/PageSpeedLeft.xaml.vb | 4 + .../Pages/PageVersion/PageVersionMod.xaml | 27 +- .../Pages/PageVersion/PageVersionMod.xaml.vb | 377 +++++-- .../Pages/PageVersion/PageVersionSetup.xaml | 6 +- .../Plain Craft Launcher 2.vbproj | 8 + Plain Craft Launcher 2/Resources/Custom.xaml | 32 +- Plain Craft Launcher 2/Resources/Help.zip | Bin 68561 -> 70960 bytes Plain Craft Launcher 2/Resources/ModData.txt | 917 +++++++++++++++-- 41 files changed, 2616 insertions(+), 1320 deletions(-) create mode 100644 Plain Craft Launcher 2/Controls/MyIconTextButton.xaml create mode 100644 Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb diff --git a/Plain Craft Launcher 2/Application.xaml.vb b/Plain Craft Launcher 2/Application.xaml.vb index 314afe5..05e70a1 100644 --- a/Plain Craft Launcher 2/Application.xaml.vb +++ b/Plain Craft Launcher 2/Application.xaml.vb @@ -41,8 +41,11 @@ Public Class Application MsgBox(ex.Message, MsgBoxStyle.Critical, "内存优化失败") Environment.Exit(-1) End Try - Ram = My.Computer.Info.AvailablePhysicalMemory - Ram - Environment.Exit(Math.Max(0, Ram / 1024)) '返回清理的内存量(K) + If My.Computer.Info.AvailablePhysicalMemory < Ram Then '避免 ULong 相减出现负数 + Environment.Exit(0) + Else + Environment.Exit((My.Computer.Info.AvailablePhysicalMemory - Ram) / 1024) '返回清理的内存量(K) + End If #If DEBUG Then ElseIf e.Args(0) = "--make" Then '制作更新包 @@ -71,7 +74,8 @@ Public Class Application Directory.CreateDirectory(PathAppdata) '检测单例 #If Not DEBUG Then - Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher 2 ") + Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher ") + If WindowHwnd = IntPtr.Zero Then FindWindow(Nothing, "Plain Craft Launcher 2 ") If WindowHwnd <> IntPtr.Zero Then '将已有的 PCL 窗口拖出来 ShowWindowToTop(WindowHwnd) diff --git a/Plain Craft Launcher 2/Controls/MyButton.xaml.vb b/Plain Craft Launcher 2/Controls/MyButton.xaml.vb index 792dbd1..5ca6b20 100644 --- a/Plain Craft Launcher 2/Controls/MyButton.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MyButton.xaml.vb @@ -134,23 +134,21 @@ '实现自定义事件 Private Sub Button_MouseUp(sender As Object, e As MouseButtonEventArgs) Handles Me.MouseLeftButtonUp - If IsMouseDown Then - Log("[Control] 按下按钮:" & Text) - RaiseEvent Click(sender, e) - If Not String.IsNullOrEmpty(Tag) Then - If Tag.ToString.StartsWith("链接-") OrElse Tag.ToString.StartsWith("启动-") Then - Hint("主页自定义按钮语法已更新,且不再兼容老版本语法,请查看新的自定义示例!") - End If + If Not IsMouseDown Then Exit Sub + Log("[Control] 按下按钮:" & Text) + RaiseEvent Click(sender, e) + If Not String.IsNullOrEmpty(Tag) Then + If Tag.ToString.StartsWith("链接-") OrElse Tag.ToString.StartsWith("启动-") Then + Hint("主页自定义按钮语法已更新,且不再兼容老版本语法,请查看新的自定义示例!") End If - ModEvent.TryStartEvent(EventType, EventData) End If + ModEvent.TryStartEvent(EventType, EventData) End Sub Public Property EventType As String Get Return GetValue(EventTypeProperty) End Get - - Set(ByVal value As String) + Set(value As String) SetValue(EventTypeProperty, value) End Set End Property @@ -159,8 +157,7 @@ Get Return GetValue(EventDataProperty) End Get - - Set(ByVal value As String) + Set(value As String) SetValue(EventDataProperty, value) End Set End Property diff --git a/Plain Craft Launcher 2/Controls/MyCard.vb b/Plain Craft Launcher 2/Controls/MyCard.vb index f6de99e..f938186 100644 --- a/Plain Craft Launcher 2/Controls/MyCard.vb +++ b/Plain Craft Launcher 2/Controls/MyCard.vb @@ -166,7 +166,9 @@ End Sub '事件 + Public Property HasMouseAnimation As Boolean = True Private Sub MyCard_MouseEnter(sender As Object, e As MouseEventArgs) Handles Me.MouseEnter + If Not HasMouseAnimation Then Exit Sub Dim AniList As New List(Of AniData) If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush2", 150)) If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush2", 150)) @@ -178,6 +180,7 @@ AniStart(AniList, "MyCard Mouse " & Uuid) End Sub Private Sub MyCard_MouseLeave(sender As Object, e As MouseEventArgs) Handles Me.MouseLeave + If Not HasMouseAnimation Then Exit Sub Dim AniList As New List(Of AniData) If Not IsNothing(MainTextBlock) Then AniList.Add(AaColor(MainTextBlock, TextBlock.ForegroundProperty, "ColorBrush1", 250)) If Not IsNothing(MainSwap) Then AniList.Add(AaColor(MainSwap, Shapes.Path.FillProperty, "ColorBrush1", 250)) diff --git a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb index 1ac82e3..a31e0d0 100644 --- a/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MyExtraButton.xaml.vb @@ -102,7 +102,7 @@ Get Return _CanRightClick End Get - Set(ByVal value As Boolean) + Set(value As Boolean) _CanRightClick = value End Set End Property diff --git a/Plain Craft Launcher 2/Controls/MyHint.xaml.vb b/Plain Craft Launcher 2/Controls/MyHint.xaml.vb index dd64166..a04d73b 100644 --- a/Plain Craft Launcher 2/Controls/MyHint.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MyHint.xaml.vb @@ -34,7 +34,7 @@ Get Return LabText.Text End Get - Set(ByVal value As String) + Set(value As String) LabText.Text = value End Set End Property @@ -43,7 +43,7 @@ Get Return BtnClose.Visibility = Visibility.Visible End Get - Set(ByVal value As Boolean) + Set(value As Boolean) BtnClose.Visibility = If(value, Visibility.Visible, Visibility.Collapsed) End Set End Property diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml new file mode 100644 index 0000000..abd786d --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb new file mode 100644 index 0000000..6176f1a --- /dev/null +++ b/Plain Craft Launcher 2/Controls/MyIconTextButton.xaml.vb @@ -0,0 +1,180 @@ +Public Class MyIconTextButton + + '基础 + + Public Uuid As Integer = GetUuid() + Public Event Check(sender As Object, raiseByMouse As Boolean) + Public Event Change(sender As Object, raiseByMouse As Boolean) + Public Sub RaiseChange() + RaiseEvent Change(Me, False) + End Sub '使外部程序可以引发本控件的 Change 事件 + + '自定义属性 + + Public Property Logo As String + Get + Return ShapeLogo.Data.ToString + End Get + Set(value As String) + ShapeLogo.Data = (New GeometryConverter).ConvertFromString(value) + End Set + End Property + Private _LogoScale As Double = 1 + Public Property LogoScale() As Double + Get + Return _LogoScale + End Get + Set(value As Double) + _LogoScale = value + If Not IsNothing(ShapeLogo) Then ShapeLogo.RenderTransform = New ScaleTransform With {.ScaleX = LogoScale, .ScaleY = LogoScale} + End Set + End Property + + Public Property Text As String + Get + Return GetValue(TextProperty) + End Get + Set(value As String) + SetValue(TextProperty, value) + End Set + End Property '内容 + Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(New PropertyChangedCallback( + Sub(sender As DependencyObject, e As DependencyPropertyChangedEventArgs) + If Not IsNothing(sender) Then CType(sender, MyIconTextButton).LabText.Text = e.NewValue + End Sub))) + Public Enum ColorState + Black + Highlight + End Enum + Private _ColorType As ColorState = ColorState.Black + Public Property ColorType As ColorState + Get + Return _ColorType + End Get + Set(value As ColorState) + _ColorType = value + RefreshColor() + End Set + End Property '颜色类别 + + '点击事件 + + Public Event Click(sender As Object, e As RouteEventArgs) + Private IsMouseDown As Boolean = False + Private Sub MyIconTextButton_MouseUp() Handles Me.MouseLeftButtonUp + If Not IsMouseDown Then Exit Sub + Log("[Control] 按下带图标按钮:" & Text) + IsMouseDown = False + RaiseEvent Click(Me, New RouteEventArgs(True)) + ModEvent.TryStartEvent(EventType, EventData) + RefreshColor() + End Sub + Private Sub MyIconTextButton_MouseDown() Handles Me.MouseLeftButtonDown + IsMouseDown = True + RefreshColor() + End Sub + Public Property EventType As String + Get + Return GetValue(EventTypeProperty) + End Get + Set(value As String) + SetValue(EventTypeProperty, value) + End Set + End Property + Public Shared ReadOnly EventTypeProperty As DependencyProperty = DependencyProperty.Register("EventType", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(Nothing)) + Public Property EventData As String + Get + Return GetValue(EventDataProperty) + End Get + Set(value As String) + SetValue(EventDataProperty, value) + End Set + End Property + Public Shared ReadOnly EventDataProperty As DependencyProperty = DependencyProperty.Register("EventData", GetType(String), GetType(MyIconTextButton), New PropertyMetadata(Nothing)) + + '动画 + + Private Const AnimationTimeOfMouseIn As Integer = 100 '鼠标指向动画长度 + Private Const AnimationTimeOfMouseOut As Integer = 150 '鼠标移出动画长度 + Private Sub RefreshColor(Optional obj = Nothing, Optional e = Nothing) Handles Me.MouseEnter, Me.MouseLeave, Me.Loaded, Me.IsEnabledChanged + Try + If IsLoaded AndAlso AniControlEnabled = 0 AndAlso Not False.Equals(e) Then '防止默认属性变更触发动画,若强制不执行动画,则 e 为 False + + Select Case ColorType + Case ColorState.Black + If IsMouseDown Then + '按下 + AniStart(AaColor(Me, BackgroundProperty, "ColorBrush6", 70), "MyIconTextButton Color " & Uuid) + ElseIf IsMouseOver Then + '指向 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfMouseIn), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn), "MyIconTextButton Color " & Uuid) + ElseIf IsEnabled Then + '正常 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush1", AnimationTimeOfMouseOut), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush1", AnimationTimeOfMouseOut) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyIconTextButton Color " & Uuid) + Else + '禁用 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrushGray5", 100), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyIconTextButton Color " & Uuid) + End If + Case ColorState.Highlight + If IsMouseDown Then + '按下 + AniStart(AaColor(Me, BackgroundProperty, "ColorBrush6", 70), "MyIconTextButton Color " & Uuid) + ElseIf IsMouseOver Then + '指向 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfMouseIn), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseIn) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, "ColorBrushBg1", AnimationTimeOfMouseIn), "MyIconTextButton Color " & Uuid) + ElseIf IsEnabled Then + '正常 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrush3", AnimationTimeOfMouseOut), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrush3", AnimationTimeOfMouseOut) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyIconTextButton Color " & Uuid) + Else + '禁用 + AniStart({ + AaColor(ShapeLogo, Shapes.Path.FillProperty, "ColorBrushGray5", 100), + AaColor(LabText, TextBlock.ForegroundProperty, "ColorBrushGray5", 100) + }, "MyIconTextButton Checked " & Uuid) + AniStart(AaColor(Me, BackgroundProperty, ColorSemiTransparent - Background, AnimationTimeOfMouseOut), "MyIconTextButton Color " & Uuid) + End If + End Select + + Else + + '不使用动画 + AniStop("MyIconTextButton Checked " & Uuid) + AniStop("MyIconTextButton Color " & Uuid) + Select Case ColorType + Case ColorState.Black + Background = ColorSemiTransparent + ShapeLogo.SetResourceReference(Shapes.Path.FillProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray5")) + LabText.SetResourceReference(TextBlock.ForegroundProperty, If(IsEnabled, "ColorBrush1", "ColorBrushGray5")) + Case ColorState.Highlight + Background = ColorSemiTransparent + ShapeLogo.SetResourceReference(Shapes.Path.FillProperty, If(IsEnabled, "ColorBrush3", "ColorBrushGray5")) + LabText.SetResourceReference(TextBlock.ForegroundProperty, If(IsEnabled, "ColorBrush3", "ColorBrushGray5")) + End Select + + End If + Catch ex As Exception + Log(ex, "刷新按钮颜色出错") + End Try + End Sub + +End Class diff --git a/Plain Craft Launcher 2/Controls/MyListItem.xaml.vb b/Plain Craft Launcher 2/Controls/MyListItem.xaml.vb index f026386..ea0e10f 100644 --- a/Plain Craft Launcher 2/Controls/MyListItem.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MyListItem.xaml.vb @@ -15,7 +15,7 @@ If _RectBack Is Nothing Then Dim Rect As New Border With { .Name = "RectBack", - .CornerRadius = New CornerRadius(If(IsScaleAnimationEnabled, 6, 0)), + .CornerRadius = New CornerRadius(If(IsScaleAnimationEnabled OrElse Height > 40, 6, 0)), .RenderTransform = If(IsScaleAnimationEnabled, New ScaleTransform(0.8, 0.8), Nothing), .RenderTransformOrigin = New Point(0.5, 0.5), .BorderThickness = New Thickness(GetWPFSize(1)), @@ -225,6 +225,7 @@ If _Logo.ToLower.StartsWith("http") Then '网络图片 PathLogo = New Image With { + .Tag = Me, .IsHitTestVisible = LogoClickable, .Source = New ImageSourceConverter().ConvertFromString(_Logo), .RenderTransformOrigin = New Point(0.5, 0.5), @@ -235,6 +236,7 @@ '位图 Dim Bitmap = New MyBitmap(_Logo) PathLogo = New Canvas With { + .Tag = Me, .IsHitTestVisible = LogoClickable, .Background = Bitmap, .RenderTransformOrigin = New Point(0.5, 0.5), @@ -252,6 +254,7 @@ Else '矢量图 PathLogo = New Shapes.Path With { + .Tag = Me, .IsHitTestVisible = LogoClickable, .HorizontalAlignment = HorizontalAlignment.Center, .VerticalAlignment = VerticalAlignment.Center, .Stretch = Stretch.Uniform, .Data = (New GeometryConverter).ConvertFromString(_Logo), .RenderTransformOrigin = New Point(0.5, 0.5), @@ -267,7 +270,7 @@ If LogoClickable Then AddHandler PathLogo.MouseLeave, Sub(sender, e) IsLogoDown = False AddHandler PathLogo.MouseLeftButtonDown, Sub(sender, e) IsLogoDown = True - AddHandler PathLogo.MouseLeftButtonUp, Sub(sender, e) If IsLogoDown Then IsLogoDown = False : RaiseEvent LogoClick(sender, e) + AddHandler PathLogo.MouseLeftButtonUp, Sub(sender, e) If IsLogoDown Then IsLogoDown = False : RaiseEvent LogoClick(sender.Tag, e) End If End If '改变行距 @@ -286,6 +289,9 @@ End Property '图标的点击 + ''' + ''' 该 Logo 是否可用点击触发事件。需要在 Logo 属性之前设置。 + ''' Public Property LogoClickable As Boolean = False Private IsLogoDown As Boolean = False @@ -447,7 +453,7 @@ RectCheck.VerticalAlignment = VerticalAlignment.Center RectCheck.Margin = New Thickness(-1, 0, 0, 0) End If - Anim.Add(AaColor(Me, ForegroundProperty, "ColorBrush3", 200)) + Anim.Add(AaColor(Me, ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2"), 200)) Else '由有变无 If Not IsNothing(RectCheck) Then @@ -469,7 +475,7 @@ RectCheck.Opacity = 1 RectCheck.VerticalAlignment = VerticalAlignment.Stretch End If - SetResourceReference(ForegroundProperty, "ColorBrush3") + SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2")) Else If Not IsNothing(RectCheck) Then RectCheck.Height = 0 @@ -615,12 +621,14 @@ Else If ButtonStack IsNot Nothing Then Ani.Add(AaOpacity(ButtonStack, -ButtonStack.Opacity, Time * 0.5)) Ani.Add(AaOpacity(RectBack, -RectBack.Opacity, Time)) - If IsScaleAnimationEnabled Then Ani.AddRange({ - AaColor(RectBack, Border.BackgroundProperty, If(IsMouseDown, "ColorBrush6", "ColorBrush7"), Time), - AaScaleTransform(Me, 1 - CType(Me.RenderTransform, ScaleTransform).ScaleX, Time * 3,, New AniEaseOutFluent), - AaScaleTransform(RectBack, 0.996 - CType(RectBack.RenderTransform, ScaleTransform).ScaleX, Time,, New AniEaseOutFluent), - AaScaleTransform(RectBack, -0.246, 1,,, True) - }) + If IsScaleAnimationEnabled Then + Ani.AddRange({ + AaColor(RectBack, Border.BackgroundProperty, If(IsMouseDown, "ColorBrush6", "ColorBrush7"), Time), + AaScaleTransform(Me, 1 - CType(RenderTransform, ScaleTransform).ScaleX, Time * 3,, New AniEaseOutFluent), + AaScaleTransform(RectBack, 0.996 - CType(RectBack.RenderTransform, ScaleTransform).ScaleX, Time,, New AniEaseOutFluent), + AaScaleTransform(RectBack, -0.246, 1,,, True) + }) + End If End If AniStart(Ani, "ListItem Color " & Uuid) Else @@ -647,7 +655,7 @@ Private Sub MyListItem_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded If Checked Then - SetResourceReference(ForegroundProperty, "ColorBrush3") + SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2")) Else SetResourceReference(ForegroundProperty, "ColorBrush1") End If diff --git a/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb b/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb index 8fca7bd..8a595b6 100644 --- a/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MyLoading.xaml.vb @@ -46,7 +46,7 @@ Public Class MyLoading Get Return _Text End Get - Set(ByVal value As String) + Set(value As String) _Text = value RefreshText() End Set @@ -57,7 +57,7 @@ Public Class MyLoading Get Return _TextError End Get - Set(ByVal value As String) + Set(value As String) _TextError = value RefreshText() End Set diff --git a/Plain Craft Launcher 2/Controls/MyRadioButton.xaml b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml index aa42f53..e5753e0 100644 --- a/Plain Craft Launcher 2/Controls/MyRadioButton.xaml +++ b/Plain Craft Launcher 2/Controls/MyRadioButton.xaml @@ -4,6 +4,6 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Background="{StaticResource ColorBrushSemiTransparent}" CornerRadius="13.5" MinHeight="27" MaxHeight="27"> - + diff --git a/Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb b/Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb index c8ccf8d..ee3bb15 100644 --- a/Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb +++ b/Plain Craft Launcher 2/Controls/MySearchBox.xaml.vb @@ -1,7 +1,7 @@ Class MySearchBox Inherits MyCard - Public Event TextChanged(ByVal sender As Object, ByVal e As EventArgs) + Public Event TextChanged(sender As Object, e As EventArgs) Private Sub MySearchBox_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded TextBox.Focus() End Sub @@ -11,7 +11,7 @@ Get Return TextBox.HintText End Get - Set(ByVal value As String) + Set(value As String) TextBox.HintText = value End Set End Property @@ -19,7 +19,7 @@ Get Return TextBox.Text End Get - Set(ByVal value As String) + Set(value As String) TextBox.Text = value End Set End Property diff --git a/Plain Craft Launcher 2/Controls/MyTextButton.vb b/Plain Craft Launcher 2/Controls/MyTextButton.vb index 9b56fa8..3d8245f 100644 --- a/Plain Craft Launcher 2/Controls/MyTextButton.vb +++ b/Plain Craft Launcher 2/Controls/MyTextButton.vb @@ -91,7 +91,7 @@ Return GetValue(EventTypeProperty) End Get - Set(ByVal value As String) + Set(value As String) SetValue(EventTypeProperty, value) End Set End Property @@ -101,7 +101,7 @@ Return GetValue(EventDataProperty) End Get - Set(ByVal value As String) + Set(value As String) SetValue(EventDataProperty, value) End Set End Property diff --git a/Plain Craft Launcher 2/FormMain.xaml b/Plain Craft Launcher 2/FormMain.xaml index ea433ba..c54903e 100644 --- a/Plain Craft Launcher 2/FormMain.xaml +++ b/Plain Craft Launcher 2/FormMain.xaml @@ -4,7 +4,7 @@ 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:Name="WindMain" x:Class="FormMain" Style="{StaticResource MyWindow}" - AllowDrop="True" Topmost="True" Title="Plain Craft Launcher 2 " AllowsTransparency="True" MinHeight="470" MinWidth="810" ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico" WindowStyle="None" ResizeMode="CanMinimize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" RenderTransformOrigin="0.5,0.5"> + AllowDrop="True" Topmost="True" Title="Plain Craft Launcher " AllowsTransparency="True" MinHeight="470" MinWidth="810" ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico" WindowStyle="None" ResizeMode="CanMinimize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" RenderTransformOrigin="0.5,0.5"> diff --git a/Plain Craft Launcher 2/FormMain.xaml.vb b/Plain Craft Launcher 2/FormMain.xaml.vb index a9a4611..ba3bcac 100644 --- a/Plain Craft Launcher 2/FormMain.xaml.vb +++ b/Plain Craft Launcher 2/FormMain.xaml.vb @@ -116,8 +116,13 @@ Public Class FormMain '3:BUG+ IMP* FEAT- '2:BUG* IMP- '1:BUG- - If LastVersion < 297 Then 'Snapshot 2.6.3 + If LastVersion < 298 Then 'Snapshot 2.6.4 + FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "Mod 管理中允许多选 Mod 进行批量操作")) FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化崩溃分析,添加多种崩溃情况的判断")) + FeatureCount += 18 + BugCount += 19 + End If + If LastVersion < 297 Then 'Snapshot 2.6.3 FeatureCount += 12 BugCount += 14 End If @@ -744,31 +749,32 @@ Public Class FormMain Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", HintType.Critical) Exit Sub End If - If McVersionCurrent Is Nothing Then + Dim TargetVersion = If(PageCurrent = PageType.VersionSetup, PageVersionLeft.Version, McVersionCurrent) + If TargetVersion Is Nothing Then Hint("请先下载游戏,再设置第三方登录!", HintType.Critical) Exit Sub End If If AuthlibServer = "https://littleskin.cn/api/yggdrasil" Then 'LittleSkin - If MyMsgBox($"是否要在版本 {McVersionCurrent.Name} 中开启 LittleSkin 登录?" & vbCrLf & + If MyMsgBox($"是否要在版本 {TargetVersion.Name} 中开启 LittleSkin 登录?" & vbCrLf & "你可以在 版本设置 → 设置 → 服务器选项 中修改登录方式。", "第三方登录开启确认", "确定", "取消") = 2 Then Exit Sub End If - Setup.Set("VersionServerLogin", 4, Version:=McVersionCurrent) - Setup.Set("VersionServerAuthServer", "https://littleskin.cn/api/yggdrasil", Version:=McVersionCurrent) - Setup.Set("VersionServerAuthRegister", "https://littleskin.cn/auth/register", Version:=McVersionCurrent) - Setup.Set("VersionServerAuthName", "LittleSkin 登录", Version:=McVersionCurrent) + Setup.Set("VersionServerLogin", 4, Version:=TargetVersion) + Setup.Set("VersionServerAuthServer", "https://littleskin.cn/api/yggdrasil", Version:=TargetVersion) + Setup.Set("VersionServerAuthRegister", "https://littleskin.cn/auth/register", Version:=TargetVersion) + Setup.Set("VersionServerAuthName", "LittleSkin 登录", Version:=TargetVersion) Else '第三方 Authlib 服务器 - If MyMsgBox($"是否要在版本 {McVersionCurrent.Name} 中开启第三方登录?" & vbCrLf & + If MyMsgBox($"是否要在版本 {TargetVersion.Name} 中开启第三方登录?" & vbCrLf & $"登录服务器:{AuthlibServer}" & vbCrLf & vbCrLf & "你可以在 版本设置 → 设置 → 服务器选项 中修改登录方式。", "第三方登录开启确认", "确定", "取消") = 2 Then Exit Sub End If - Setup.Set("VersionServerLogin", 4, Version:=McVersionCurrent) - Setup.Set("VersionServerAuthServer", AuthlibServer, Version:=McVersionCurrent) - Setup.Set("VersionServerAuthRegister", AuthlibServer.Replace("api/yggdrasil", "auth/register"), Version:=McVersionCurrent) - Setup.Set("VersionServerAuthName", "", Version:=McVersionCurrent) + Setup.Set("VersionServerLogin", 4, Version:=TargetVersion) + Setup.Set("VersionServerAuthServer", AuthlibServer, Version:=TargetVersion) + Setup.Set("VersionServerAuthRegister", AuthlibServer.Replace("api/yggdrasil", "auth/register"), Version:=TargetVersion) + Setup.Set("VersionServerAuthName", "", Version:=TargetVersion) End If If PageCurrent = PageType.VersionSetup AndAlso PageCurrentSub = PageSubType.VersionSetup Then '正在服务器选项页,需要刷新设置项显示 @@ -916,7 +922,7 @@ Install: Get Return _Hidden End Get - Set(ByVal value As Boolean) + Set(value As Boolean) If _Hidden = value Then Exit Property _Hidden = value If value Then @@ -963,6 +969,7 @@ Install: #Region "切换页面" '页面种类与属性 + '注意,这一枚举在 “切换页面” EventType 中调用,应视作公开 API 的一部分 ''' ''' 页面种类。 ''' @@ -970,43 +977,43 @@ Install: ''' ''' 启动。 ''' - Launch + Launch = 0 ''' ''' 下载。 ''' - Download + Download = 1 ''' ''' 联机。 ''' - Link + Link = 2 ''' ''' 设置。 ''' - Setup + Setup = 3 ''' ''' 更多。 ''' - Other + Other = 4 ''' ''' 版本选择。这是一个副页面。 ''' - VersionSelect + VersionSelect = 5 ''' ''' 下载管理。这是一个副页面。 ''' - DownloadManager + DownloadManager = 6 ''' ''' 版本设置。这是一个副页面。 ''' - VersionSetup + VersionSetup = 7 ''' ''' 资源工程详情。这是一个副页面。 ''' - CompDetail + CompDetail = 8 ''' ''' 帮助详情。这是一个副页面。 ''' - HelpDetail + HelpDetail = 9 End Enum ''' ''' 次要页面种类。其数值必须与 StackPanel 中的下标一致。 diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.vb b/Plain Craft Launcher 2/Modules/Base/ModBase.vb index a40eb48..5d2f6da 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.vb @@ -11,12 +11,12 @@ Public Module ModBase #Region "声明" '下列版本信息由更新器自动修改 - Public Const VersionBaseName As String = "2.6.3" '不含分支前缀的显示用版本名 - Public Const VersionStandardCode As String = "2.6.3." & VersionBranchCode '标准格式的四段式版本号 + Public Const VersionBaseName As String = "2.6.4" '不含分支前缀的显示用版本名 + Public Const VersionStandardCode As String = "2.6.4." & VersionBranchCode '标准格式的四段式版本号 #If BETA Then - Public Const VersionCode As Integer = 293 'Release + Public Const VersionCode As Integer = 296 'Release #Else - Public Const VersionCode As Integer = 297 'Snapshot + Public Const VersionCode As Integer = 298 'Snapshot #End If '自动生成的版本信息 Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName diff --git a/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb b/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb index 7cbe59f..02da732 100644 --- a/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb +++ b/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb @@ -73,10 +73,10 @@ Public Class MyBitmap Pic = My.Application.TryFindResource(FilePathOrResourceName) If Pic Is Nothing Then Pic = New System.Drawing.Bitmap(1, 1) - Log(ex, "加载位图失败(" & FilePathOrResourceName & ")") + Log(ex, $"加载位图失败({FilePathOrResourceName})") Throw Else - Log(ex, "指定类型有误的位图加载(" & FilePathOrResourceName & ")", LogLevel.Developer) + Log(ex, $"指定类型有误的位图加载({FilePathOrResourceName})", LogLevel.Developer) Exit Try End If End Try diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb index 400087c..fbfd8e4 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb @@ -375,6 +375,7 @@ End Select Next If Tags.Count = 0 Then Tags.Add("杂项") + Tags.Sort() ModLoaders.Sort() #End Region End If @@ -424,15 +425,32 @@ Dim ExNameList As List(Of String) If TranslatedName = RawName Then '没有中文翻译 - FinalName = TranslatedName.Split(" | ").First.Split(" - ").First.Split("(").First.Split("[").First - ExNameList = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries). - Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList - ExNameList.RemoveAt(0) - If Not ExNameList.Any(Function(s) s.ToLower.Contains("forge") OrElse s.ToLower.Contains("fabric") OrElse s.ToLower.Contains("quilt")) AndAlso '不是标注 XX 版 - Not (ExNameList.Count = 1 AndAlso ExNameList.First.ToUpper = ExNameList.First) Then '不是缩写 - FinalName = RawName '使用原名 - ExNameList.Clear() - End If + '将所有名称分段 + Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries). + Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList + If NameLists.Count = 1 Then GoTo NoExName + '查找其中的缩写、Forge/Fabric 等版本标记 + ExNameList = New List(Of String) + Dim NormalNameList = New List(Of String) + For Each Name In NameLists + Dim LowerName As String = Name.ToLower + If Name.ToUpper = Name Then + '缩写 + ExNameList.Add(Name) + ElseIf (LowerName.Contains("forge") OrElse LowerName.Contains("fabric") OrElse LowerName.Contains("quilt")) AndAlso + Not RegexCheck(LowerName.Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母 + 'Forge/Fabric 等版本标记 + ExNameList.Add(Name) + Else + '其他部分 + NormalNameList.Add(Name) + End If + Next + '根据分类后的结果处理 + If NormalNameList.Count = 0 OrElse ExNameList.Count = 0 Then GoTo NoExName + '同时包含 NormalName 和 ExName + FinalName = NormalNameList.Join(" - ") + ExNameList = ExNameList Else '有中文翻译 '尝试将文本分为三段:FinalName (EnglishName) - Suffix @@ -452,21 +470,20 @@ ExNameList = New List(Of String) From {EnglishName} '使用原名 End If '添加后缀 - If Suffix <> "" Then - ExNameList.Add(Suffix) - ExNameList = ExNameList.Distinct().ToList() - End If + If Suffix <> "" Then ExNameList.Add(Suffix) End If + ExNameList = ExNameList.Distinct().ToList() '显示 FinalName 与 ExNameList - NewItem.LabTitle.Text = FinalName If ExNameList.Count = 0 Then NoExName: + NewItem.LabTitle.Text = RawName CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw) Else + NewItem.LabTitle.Text = FinalName Dim ExName As String = "" For Each Ex In ExNameList '去除 “Forge/Fabric” 这一无意义提示 - If Ex.Length < 19 AndAlso Ex.ToLower.Contains("fabric") AndAlso Ex.ToLower.Contains("forge") Then Continue For + If Ex.Length < 16 AndAlso Ex.ToLower.Contains("fabric") AndAlso Ex.ToLower.Contains("forge") Then Continue For '将 “Forge” 等提示改为 “Forge 版” If (Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")) AndAlso Not Ex.Contains("版") AndAlso Ex.ToLower.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb index 0d45097..1dc2251 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb @@ -365,7 +365,7 @@ ShadersMod与Optifine同时安装 Forge安装不完整 Mod需要Java11 - Mod依赖项未完善 + Mod缺少前置或MC版本错误 End Enum ''' ''' 根据 AnalyzeLogs 与可能的版本信息分析崩溃原因。 @@ -374,9 +374,11 @@ Log("[Crash] 步骤 3:分析崩溃原因") LogAll = (If(LogMc, "") & If(LogHs, "") & If(LogCrash, "")).ToLower - '1. 精准日志匹配 1 + '1. 精准日志匹配,中/高优先级 AnalyzeCrit1() If CrashReasons.Count > 0 Then GoTo Done + AnalyzeCrit2() + If CrashReasons.Count > 0 Then GoTo Done '2. 堆栈分析 If LogAll.Contains("forge") OrElse LogAll.Contains("fabric") OrElse LogAll.Contains("liteloader") Then @@ -414,8 +416,8 @@ Log("[Crash] 可能并未安装 Mod,不进行堆栈分析") End If - '3. 精准日志匹配 2 - AnalyzeCrit2() + '3. 精准日志匹配,低优先级 + AnalyzeCrit3() '输出到日志 Done: @@ -470,13 +472,12 @@ Done: If LogMc.Contains("java.lang.ClassCastException: java.base/jdk") Then AppendReason(CrashReason.使用JDK) If LogMc.Contains("java.lang.ClassCastException: class jdk.") Then AppendReason(CrashReason.使用JDK) If LogMc.Contains("TRANSFORMER/net.optifine/net.optifine.reflect.Reflector.(Reflector.java") Then AppendReason(CrashReason.OptiFine与Forge不兼容) - If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.(net.minecraft.resources.ResourceLocation, ") Then AppendReason(CrashReason.OptiFine与Forge不兼容) + If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.texture.SpriteContents.") Then AppendReason(CrashReason.OptiFine与Forge不兼容) If LogMc.Contains("java.lang.NoSuchMethodError: 'java.lang.String com.mojang.blaze3d.systems.RenderSystem.getBackendDescription") Then AppendReason(CrashReason.OptiFine与Forge不兼容) - If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.(int[], int, net.minecraft.core.Direction, net.minecraft.client.renderer.texture.TextureAtlasSprite, boolean, boolean)'") Then AppendReason(CrashReason.OptiFine与Forge不兼容)'1.19.2 Optifine HD_U_H9 Forge 43.2.17 - If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager.addRegionTicket(net.minecraft.server.level.TicketType, net.minecraft.world.level.ChunkPos, int, java.lang.Object, boolean)'") Then AppendReason(CrashReason.OptiFine与Forge不兼容) - If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager.removeRegionTicket(net.minecraft.server.level.TicketType, net.minecraft.world.level.ChunkPos, int, java.lang.Object, boolean)'") Then AppendReason(CrashReason.OptiFine与Forge不兼容) - If LogMc.Contains("java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize(net.minecraft.network.chat.FormattedText, int)'") Then AppendReason(CrashReason.OptiFine与Forge不兼容) - If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName(net.minecraft.client.gui.GuiGraphics, int)'") Then AppendReason(CrashReason.OptiFine与Forge不兼容)'1.20.1 OptiFine HD_U_I5 Forge 47.1.28/47.1.14 , 1.20.1 OptiFine_I6_pre1 Forge 47.1.14 + If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.client.renderer.block.model.BakedQuad.") Then AppendReason(CrashReason.OptiFine与Forge不兼容) + If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraftforge.client.gui.overlay.ForgeGui.renderSelectedItemName") Then AppendReason(CrashReason.OptiFine与Forge不兼容) + If LogMc.Contains("java.lang.NoSuchMethodError: 'void net.minecraft.server.level.DistanceManager") Then AppendReason(CrashReason.OptiFine与Forge不兼容) + If LogMc.Contains("java.lang.NoSuchMethodError: 'net.minecraft.network.chat.FormattedText net.minecraft.client.gui.Font.ellipsize") Then AppendReason(CrashReason.OptiFine与Forge不兼容) If LogMc.Contains("Open J9 is not supported") OrElse LogMc.Contains("OpenJ9 is incompatible") OrElse LogMc.Contains(".J9VMInternals.") Then AppendReason(CrashReason.使用OpenJ9) If LogMc.Contains("java.lang.NoSuchFieldException: ucp") Then AppendReason(CrashReason.Java版本过高) If LogMc.Contains("because module java.base does not export") Then AppendReason(CrashReason.Java版本过高) @@ -498,7 +499,7 @@ Done: If LogMc.Contains("Invalid module name: '' is not a Java identifier") Then 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") Then 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") Then AppendReason(CrashReason.Mod需要Java11) - If LogMc.Contains("java.lang.UnsupportedClassVersionError: net/fabricmc/loader/impl/launch/knot/KnotClient : Unsupported major.minor version") Then AppendReason(CrashReason.Java版本不兼容) + If LogMc.Contains("Unsupported major.minor version") Then AppendReason(CrashReason.Java版本不兼容) If LogMc.Contains("Invalid maximum heap size") Then AppendReason(CrashReason.使用32位Java导致JVM无法分配足够多的内存) If LogMc.Contains("Could not reserve enough space") Then If LogMc.Contains("for 1048576KB object heap") Then @@ -507,34 +508,17 @@ Done: AppendReason(CrashReason.内存不足) End If End If + '确定的 Mod 导致崩溃 + If LogMc.Contains("Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(RegexSeek(LogMc, "(?<=Caught exception from )[^\n]+?")?.TrimEnd((vbCrLf & " ").ToCharArray))) 'Mod 重复安装 If LogMc.Contains("DuplicateModsFoundException") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(LogMc, "(?<=\n\t[\w]+ : [A-Z]{1}:[^\n]+(/|\\))[^/\\\n]+?.jar", RegularExpressions.RegexOptions.IgnoreCase)) If LogMc.Contains("Found a duplicate mod") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "Found a duplicate mod[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase)) If LogMc.Contains("ModResolutionException: Duplicate") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "ModResolutionException: Duplicate[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase)) - 'Mod 导致的崩溃 - If LogMc.Contains("Mixin prepare failed ") OrElse LogMc.Contains("Mixin apply failed ") OrElse LogMc.Contains("MixinApplyError") OrElse - LogMc.Contains("mixin.injection.throwables.") OrElse LogMc.Contains(".mixins.json] FAILED during )") Then - Dim ModId As String = RegexSeek(LogMc, "(?<=in )[^./ ]+(?=.mixins.json.+failed injection check)") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=in mixins.)[^./ ]+(?=.json.+failed injection check)") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in mixins.)[^./ ]+(?=.json)") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[)[^./ ]+(?=.mixins.json\] FAILED during )") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[mixins.)[^./ ]+(?=.json\] FAILED during )") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=from mod )[^./ ]+(?=\] from)") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=for mod )[^./ ]+(?= failed)") - '兜底名称判断 - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json") - If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json") - AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray))) - 'Mod 依赖项未完善 - If LogCrash.Contains("Missing or unsupported mandatory dependencies:") Then - AppendReason(CrashReason.Mod依赖项未完善, RegexSearch(LogMc, "(?<=Missing or unsupported mandatory dependencies:)[\n\r]*(\t(.*)[\n\r]*)+"), "").TrimEnd((vbCrLf & " ").ToCharArray) - End If - If LogMc.Contains("Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(RegexSeek(LogMc, "(?<=Caught exception from )[^\n]+?")?.TrimEnd((vbCrLf & " ").ToCharArray))) - '仅当没有其他分析结果时显示 - If CrashReasons.Count = 0 Then - If LogMc.Contains("An exception was thrown, the game will display an error screen and halt.") Then AppendReason(CrashReason.Forge报错, RegexSeek(LogMc, "(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")?.Trim(vbCrLf)) - If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf)) + 'Mod 缺少前置 + If LogMc.Contains("Missing or unsupported mandatory dependencies:") Then + AppendReason(CrashReason.Mod缺少前置或MC版本错误, + RegexSearch(LogMc, "(?<=Missing or unsupported mandatory dependencies:)([\n\r]+\t(.*))+", RegularExpressions.RegexOptions.IgnoreCase). + Select(Function(s) s.Trim((vbCrLf & vbTab & " ").ToCharArray)).Distinct().ToList()) End If End If @@ -572,14 +556,43 @@ Done: End Sub ''' - ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。 + ''' 进行精准日志匹配。匹配优先级高于堆栈分析的崩溃,但低于上面的。 + ''' 如果第一步已经找到了原因则不执行该检测。 ''' Private Sub AnalyzeCrit2() '游戏日志分析 If LogMc IsNot Nothing Then - If Not (LogMc.Contains("at net.") OrElse LogMc.Contains("INFO]")) AndAlso - LogHs Is Nothing AndAlso LogCrash Is Nothing AndAlso LogMc.Length < 100 Then + 'Mixin 崩溃 + If LogMc.Contains("Mixin prepare failed ") OrElse LogMc.Contains("Mixin apply failed ") OrElse LogMc.Contains("MixinApplyError") OrElse + LogMc.Contains("mixin.injection.throwables.") OrElse LogMc.Contains(".mixins.json] FAILED during )") Then + Dim ModId As String = RegexSeek(LogMc, "(?<=in )[^./ ]+(?=.mixins.json.+failed injection check)") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=in mixins.)[^./ ]+(?=.json.+failed injection check)") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in mixins.)[^./ ]+(?=.json)") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[)[^./ ]+(?=.mixins.json\] FAILED during )") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= in config \[mixins.)[^./ ]+(?=.json\] FAILED during )") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=from mod )[^./ ]+(?=\] from)") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=for mod )[^./ ]+(?= failed)") + '兜底名称判断 + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json") + If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json") + AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray))) + End If + '常规信息 + If LogMc.Contains("An exception was thrown, the game will display an error screen and halt.") Then AppendReason(CrashReason.Forge报错, RegexSeek(LogMc, "(?<=the game will display an error screen and halt.[\n\r]+[^\n]+?Exception: )[\s\S]+?(?=\n\tat)")?.Trim(vbCrLf)) + If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf)) + End If + + End Sub + ''' + ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。 + ''' + Private Sub AnalyzeCrit3() + + '游戏日志分析 + If LogMc IsNot Nothing Then + If Not (LogMc.Contains("at net.") OrElse LogMc.Contains("INFO]")) AndAlso LogHs Is Nothing AndAlso LogCrash Is Nothing AndAlso LogMc.Length < 100 Then AppendReason(CrashReason.极短的程序输出, LogMc) End If 'Mixin 失败可以导致大量 Mod 实例创建失败 @@ -814,7 +827,7 @@ NextStack: Case CrashReason.Mod文件被解压 Results.Add("由于 Mod 文件被解压了,导致游戏无法继续运行。\n直接把整个 Mod 文件放进 Mod 文件夹中即可,若解压就会导致游戏出错。\n\n请删除 Mod 文件夹中已被解压的 Mod,然后再启动游戏。") Case CrashReason.内存不足 - Results.Add("Minecraft 内存不足,导致其无法继续运行。\n这很可能是由于你为游戏分配的内存不足,或是游戏的配置要求过高。\n\n你可以在启动设置中增加为游戏分配的内存,删除配置要求较高的材质、Mod、光影。\n如果这依然不奏效,请在开始游戏前尽量关闭其他软件,或者……换台电脑?\h") + Results.Add("Minecraft 内存不足,导致其无法继续运行。\n这很可能是因为电脑内存不足、游戏分配的内存不足,或是配置要求过高。\n\n你可以尝试在 更多 → 百宝箱 中选择 内存优化,然后再启动游戏。\n如果还是不行,请在启动设置中增加为游戏分配的内存,并删除配置要求较高的材质、Mod、光影。\n如果依然不奏效,请在开始游戏前尽量关闭其他软件,或者……换台电脑?\h") Case CrashReason.使用OpenJ9 Results.Add("游戏因为使用 Open J9 而崩溃了。\n请在启动设置的 Java 选择一项中改用非 OpenJ9 的 Java,然后再启动游戏。") Case CrashReason.使用JDK @@ -837,6 +850,12 @@ NextStack: Else Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h") End If + Case CrashReason.Mod缺少前置或MC版本错误 + If Additional.Count > 0 Then + Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n未满足的依赖项:\n - " & Join(Additional, "\n - ") & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。") + Else + Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h") + End If Case CrashReason.崩溃日志堆栈分析发现Mod名称 If Additional.Count = 1 Then Results.Add("名为 " & Additional.First & " 的 Mod 可能导致了游戏出错。\n\e\h") @@ -943,12 +962,6 @@ NextStack: Else Results.Add("Forge 可能已经提供了错误信息,请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h") End If - Case CrashReason.Mod依赖项未完善 - If Additional.Count = 1 Then - Results.Add("由于 Mod 缺少或不受支持的必备依赖项,导致了游戏退出。\n以下是未满足条件的必备依赖项:" & Additional.First & "\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。") - Else - Results.Add("由于 Mod 缺少或不受支持的强制依赖项,导致了游戏退出。请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\h") - End If Case CrashReason.没有可用的分析文件 Results.Add("你的游戏出现了一些问题,但 PCL 未能找到相关记录文件,因此无法进行分析。\h") Case Else diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb index 7c6acf0..81a31f9 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb @@ -1,5 +1,5 @@ Public Module ModJava - Public JavaListCacheVersion As Integer = 4 + Public JavaListCacheVersion As Integer = 5 ''' ''' 目前所有可用的 Java。 @@ -129,8 +129,10 @@ End If Is64Bit = Output.Contains("64-bit") If Version.Minor <= 4 OrElse Version.Minor >= 25 Then Throw New Exception("分析详细信息失败,获取的版本为 " & Version.ToString) - '无论如何不允许使用 JRE 16,它完全是炸的,安 OptiFine、Forge 和启动 MC 都会炸 - If IsJre AndAlso VersionCode = 16 Then Throw New Exception("由于 JRE 16 对 Minecraft 的兼容性很差,因此不再允许使用") + '基于 #2249 发现的 JRE 17 似乎也导致了 Forge 安装失败,干脆禁用更多版本的 JRE + If IsJre AndAlso VersionCode >= 16 Then Throw New Exception("由于高版本 JRE 对 Minecraft 的兼容性很差,因此不再允许使用") + ''无论如何不允许使用 JRE 16,它完全是炸的,安 OptiFine、Forge 和启动 MC 都会炸 + 'If IsJre AndAlso VersionCode = 16 Then Throw New Exception("由于 JRE 16 对 Minecraft 的兼容性很差,因此不再允许使用") Catch ex As Exception Log("[Java] 检查失败的 Java 输出:" & PathFolder & "java.exe" & vbCrLf & If(Output, "无程序输出")) Throw New Exception("检查 Java 失败(" & If(PathJavaw, "Nothing") & ")", ex) @@ -230,6 +232,8 @@ Try UserJava = JavaEntry.FromJson(GetJson(Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion))) UserJava.Check() + Catch ex As ThreadInterruptedException + Throw Catch ex As Exception UserJava = Nothing Setup.Reset("VersionArgumentJavaSelect", Version:=RelatedVersion) @@ -242,6 +246,8 @@ Try UserJava = JavaEntry.FromJson(GetJson(Setup.Get("LaunchArgumentJavaSelect"))) UserJava.Check() + Catch ex As ThreadInterruptedException + Throw Catch ex As Exception UserJava = Nothing Setup.Reset("LaunchArgumentJavaSelect") @@ -450,7 +456,7 @@ NoUserJava: '4. Java 大版本 If Left.VersionCode <> Right.VersionCode Then ' Java 7 8 9 10 11 12 13 14 15 16 17 18 19 20... - Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 29, 10, 12, 15, 13, 9, 8, 7, 11, 31, 30, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28} + Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 30, 10, 12, 15, 13, 9, 8, 7, 11, 31, 29, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28} Return Weight.ElementAtOrDefault(Left.VersionCode) >= Weight.ElementAtOrDefault(Right.VersionCode) End If '5. 最次级版本号更接近 51 @@ -530,10 +536,10 @@ NoUserJava: For Each Pair In JavaPreList If Pair.Key.Contains("javapath_target_") OrElse Pair.Key.Contains("javatmp") Then Log("[Java] 位于 " & Pair.Key & " 的 Java 包含特殊引用") - Continue For + Else + Log("[Java] 位于 " & Pair.Key & " 的 Java 不含特殊引用") + JavaWithoutInherit.Add(Pair.Key, Pair.Value) End If - Log("[Java] 位于 " & Pair.Key & " 的 Java 不含特殊引用") - JavaWithoutInherit.Add(Pair.Key, Pair.Value) Next If JavaWithoutInherit.Count > 0 Then JavaPreList = JavaWithoutInherit @@ -688,18 +694,17 @@ Wait: ''' ''' 提示 Java 缺失,并弹窗确认是否自动下载。返回玩家选择是否下载。 - ''' 支持下载 Java 8、14、17,输入其他 VersionCode 会要求玩家自行下载。 ''' - Public Function JavaDownloadConfirm(VersionCode As String) As Boolean - If VersionCode = 8 OrElse VersionCode = 14 OrElse VersionCode = 17 Then - Return MyMsgBox($"PCL 未找到 Java {VersionCode},是否需要 PCL 自动下载?" & vbCrLf & - $"如果你已经安装了 Java {VersionCode},请在 设置 → 启动选项 → 游戏 Java 中手动导入。", - "未找到 Java", "自动下载", "取消") = 1 - Else - MyMsgBox($"PCL 未找到 Java {VersionCode}。" & vbCrLf & - $"请自行搜索并安装 Java {VersionCode},安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", + Public Function JavaDownloadConfirm(VersionDescription As String, Optional ForcedManualDownload As Boolean = False) As Boolean + If ForcedManualDownload Then + MyMsgBox($"PCL 未找到 {VersionDescription}。" & vbCrLf & + $"请自行搜索并安装 {VersionDescription},安装后在 设置 → 启动选项 → 游戏 Java 中重新搜索或导入。", "未找到 Java") Return False + Else + Return MyMsgBox($"PCL 未找到 {VersionDescription},是否需要 PCL 自动下载?" & vbCrLf & + $"如果你已经安装了 {VersionDescription},请在 设置 → 启动选项 → 游戏 Java 中手动导入。", + "未找到 Java", "自动下载", "取消") = 1 End If End Function diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb index 569cc1b..3bb3b84 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb @@ -1131,11 +1131,11 @@ SystemBrowser: If McVersionCurrent.Version.McName = "1.7.2" Then '1.7.2:必须 Java 7 MinVer = New Version(1, 7, 0, 0) : MaxVer = New Version(1, 7, 999, 999) - ElseIf McVersionCurrent.Version.McCodeMain <= 12 AndAlso McVersionCurrent.Version.McCodeMain > 0 AndAlso VersionSortBoolean("14.23.5.2855", McVersionCurrent.Version.ForgeVersion) Then - '<=1.12,Forge 14.23.5.2855 及更低:Java 8 + ElseIf McVersionCurrent.Version.McCodeMain <= 12 AndAlso McVersionCurrent.Version.McCodeMain > 0 Then + '<=1.12:Java 8 MaxVer = New Version(1, 8, 999, 999) - ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain >= 13 AndAlso VersionSortBoolean("28.2.23", McVersionCurrent.Version.ForgeVersion) Then - '1.13 - 1.14,Forge 28.2.23 及更低:Java 8 - 10 + ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain >= 13 Then + '1.13 - 1.14:Java 8 - 10 MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 10, 999, 999) ElseIf McVersionCurrent.Version.McCodeMain = 15 Then '1.15:Java 8 - 15 @@ -1182,17 +1182,27 @@ SystemBrowser: If Task.IsAborted Then Exit Sub '中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java McLaunchLog("无合适的 Java,需要确认是否自动下载") Dim JavaCode As String - If MinVer >= New Version(1, 16, 0, 0) Then + If MinVer >= New Version(1, 9, 0, 0) Then JavaCode = 17 + If Not JavaDownloadConfirm("Java 17") Then Throw New Exception("$$") ElseIf MaxVer < New Version(1, 8, 0, 0) Then JavaCode = 7 - ElseIf MinVer > New Version(1, 8, 0, 52) Then - '缺少 Java 8 较新版(统一通行证) + If Not JavaDownloadConfirm("Java 7") Then Throw New Exception("$$") + ElseIf MinVer > New Version(1, 8, 0, 100) AndAlso MaxVer < New Version(1, 8, 0, 321) Then JavaCode = "8u101" + JavaDownloadConfirm("Java 8.0.101 ~ 8.0.320", True) + Throw New Exception("$$") + ElseIf MinVer > New Version(1, 8, 0, 100) Then + JavaCode = "8u101" + JavaDownloadConfirm("Java 8.0.101 或更高版本的 Java 8", True) + Throw New Exception("$$") + ElseIf MaxVer < New Version(1, 8, 0, 321) Then + JavaCode = 8 + If Not JavaDownloadConfirm("Java 8.0.320 或更低版本的 Java 8") Then Throw New Exception("$$") Else JavaCode = 8 + If Not JavaDownloadConfirm("Java 8") Then Throw New Exception("$$") End If - If Not JavaDownloadConfirm(JavaCode) Then Throw New Exception("$$") '开始自动下载 Dim JavaLoader = JavaFixLoaders(JavaCode) @@ -1350,7 +1360,7 @@ SystemBrowser: '添加 Java Wrapper 作为主 Jar If McLaunchJavaSelected.VersionCode >= 9 Then DataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED") - DataList.Insert(0, "-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """") + DataList.Add("-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """") DataList.Add("-jar """ & ExtractJavaWrapper() & """") '添加 MainClass @@ -1417,7 +1427,7 @@ NextVersion: '添加 Java Wrapper 作为主 Jar If McLaunchJavaSelected.VersionCode >= 9 Then DataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED") - DataList.Insert(0, "-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """") + DataList.Add("-Doolloo.jlw.tmpdir=""" & GetJavaWrapperDir() & """") DataList.Add("-jar """ & ExtractJavaWrapper() & """") '将 "-XXX" 与后面 "XXX" 合并到一起 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb index 9c8dc16..7be9392 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb @@ -1,4 +1,5 @@ Imports System.IO.Compression +Imports NAudio.Gui Public Module ModMinecraft @@ -392,7 +393,7 @@ Public Module ModMinecraft '非准确的版本判断警告 Log("[Minecraft] 无法完全确认 MC 版本号的版本:" & Name) '从文件夹名中获取 - Regex = RegexSeek(Name, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)") + Regex = RegexSeek(Name, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)", RegularExpressions.RegexOptions.IgnoreCase) If Regex IsNot Nothing Then _Version.McName = Regex GoTo VersionSearchFinish @@ -401,7 +402,7 @@ Public Module ModMinecraft Dim JsonRaw As JObject = JsonObject.DeepClone() JsonRaw.Remove("libraries") Dim JsonRawText As String = JsonRaw.ToString - Regex = RegexSeek(JsonRawText, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)") + Regex = RegexSeek(JsonRawText, "([0-9w]{5}[a-z]{1})|(1\.[0-9]+(\.[0-9]+)?(-(pre|rc)[1-9]?| Pre-Release( [1-9]{1})?)?)", RegularExpressions.RegexOptions.IgnoreCase) If Regex IsNot Nothing Then _Version.McName = Regex GoTo VersionSearchFinish @@ -467,7 +468,7 @@ VersionSearchFinish: End If Return _JsonText End Get - Set(ByVal value As String) + Set(value As String) _JsonText = value End Set End Property @@ -562,7 +563,7 @@ Recheck: End If Return _JsonObject End Get - Set(ByVal value As JObject) + Set(value As JObject) _JsonObject = value End Set End Property @@ -966,7 +967,7 @@ ExitDataLoad: End If Return _SortCode End Get - Set(ByVal value As Integer) + Set(value As Integer) _SortCode = value End Set End Property @@ -1608,7 +1609,7 @@ NextVersion: Get Return _Url End Get - Set(ByVal value As String) + Set(value As String) '孤儿 Forge 作者喜欢把没有的 URL 写个空字符串 _Url = If(String.IsNullOrWhiteSpace(value), Nothing, value) End Set @@ -2122,30 +2123,46 @@ NextVersion: '确认 Virtual 与 Map 状态 Dim IsVirtual As Boolean = False - If Json("virtual") IsNot Nothing Then IsVirtual = Json("virtual").ToString - Dim IsMap As Boolean = False - If Json("map_to_resources") IsNot Nothing Then IsMap = Json("map_to_resources").ToString + If Json("virtual") IsNot Nothing AndAlso Json("virtual").ToString Then IsVirtual = True + If Json("map_to_resources") IsNot Nothing AndAlso Json("map_to_resources").ToString Then + IsVirtual = True + '刷新 resources 文件夹符号链接(#2182) + 'Dim Info As FileSystemInfo = New FileInfo(PathMcFolder & "resources\pack.mcmeta") + 'If Info.Attributes.HasFlag(FileAttributes.ReparsePoint) Then + If Not File.Exists(PathMcFolder & "resources\pack.mcmeta") Then + Log("[Minecraft] 尝试刷新 resources 文件夹符号链接", LogLevel.Debug) + Try + DeleteDirectory(PathMcFolder & "resources\", True) + Directory.CreateDirectory(PathMcFolder & "assets\virtual\legacy\") + Dim Result = ShellAndGetOutput("cmd", $"/C mklink /D /J ""{PathMcFolder}resources"" ""{PathMcFolder}assets\virtual\legacy""") + Log($"[Minecraft] 符号链接创建结果:{Result}") + If Not Result.Contains("<<===>>") Then Throw New Exception($"非预期的结果({Result})") + Catch ex As Exception + Log(ex, "创建资源文件夹链接失败,游戏可能会没有声音", LogLevel.Msgbox) + End Try + End If + End If '加载列表 - If IsVirtual OrElse IsMap Then + If IsVirtual Then For Each File As JProperty In Json("objects").Children McAssetsListGet.Add(New McAssetsToken With { - .IsVirtual = True, - .LocalPath = PathMcFolder & "assets\virtual\legacy\" & File.Name.Replace("/", "\"), - .SourcePath = File.Name, - .Hash = File.Value("hash").ToString, - .Size = File.Value("size").ToString - }) + .IsVirtual = True, + .LocalPath = PathMcFolder & "assets\virtual\legacy\" & File.Name.Replace("/", "\"), + .SourcePath = File.Name, + .Hash = File.Value("hash").ToString, + .Size = File.Value("size").ToString + }) Next Else For Each File As JProperty In Json("objects").Children McAssetsListGet.Add(New McAssetsToken With { - .IsVirtual = False, - .LocalPath = PathMcFolder & "assets\objects\" & Left(File.Value("hash").ToString, 2) & "\" & File.Value("hash").ToString, - .SourcePath = File.Name, - .Hash = File.Value("hash").ToString, - .Size = File.Value("size").ToString - }) + .IsVirtual = False, + .LocalPath = PathMcFolder & "assets\objects\" & Left(File.Value("hash").ToString, 2) & "\" & File.Value("hash").ToString, + .SourcePath = File.Name, + .Hash = File.Value("hash").ToString, + .Size = File.Value("size").ToString + }) Next End If @@ -2186,899 +2203,6 @@ NextVersion: Return Result End Function -#End Region - -#Region "Mod" - - Public Class McMod - -#Region "基础" - - ''' - ''' Mod 文件的地址。 - ''' - Public ReadOnly Path As String - Public Sub New(Path As String) - Me.Path = If(Path, "") - End Sub - - ''' - ''' Mod 的完整文件名。 - ''' - Public ReadOnly Property FileName As String - Get - Return GetFileNameFromPath(Path) - End Get - End Property - - ''' - ''' Mod 的状态。 - ''' - Public ReadOnly Property State As McModState - Get - Load() - If Not IsFileAvailable Then - Return McModState.Unavaliable - ElseIf Path.EndsWith(".disabled") Then - Return McModState.Disabled - Else - Return McModState.Fine - End If - End Get - End Property - Public Enum McModState As Integer - Fine = 0 - Disabled = 1 - Unavaliable = 2 - End Enum - -#End Region - -#Region "信息项" - - ''' - ''' Mod 的名称。若不可用则为 ModID 或无扩展的文件名。 - ''' - Public Property Name As String - Get - If _Name Is Nothing Then Load() - If _Name Is Nothing Then _Name = _ModId - If _Name Is Nothing Then _Name = GetFileNameWithoutExtentionFromPath(Path) - Return _Name - End Get - Set(value As String) - If _Name Is Nothing AndAlso value IsNot Nothing AndAlso - value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then - _Name = value - End If - End Set - End Property - Private _Name As String = Nothing - - ''' - ''' Mod 的描述信息。 - ''' - Public Property Description As String - Get - If _Description Is Nothing Then Load() - If _Description Is Nothing AndAlso FileUnavailableReason IsNot Nothing Then _Description = FileUnavailableReason.Message - 'If _Description Is Nothing Then _Description = Path - Return _Description - End Get - Set(value As String) - If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then - _Description = value.ToString.Trim(vbLf) - '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号 - If _Description.ToLower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "." - End If - End Set - End Property - Private _Description As String = Nothing - - ''' - ''' Mod 的版本,不保证符合版本格式规范。 - ''' - Public Property Version As String - Get - If _Version Is Nothing Then Load() - Return _Version - End Get - Set(value As String) - If _Version IsNot Nothing AndAlso (_Version.Contains(".") OrElse _Version.Contains("-")) Then Exit Property - If value IsNot Nothing AndAlso value.ToLower.Contains("version") Then value = "version" '需要修改的标识 - _Version = value - End Set - End Property - Private _Version As String = Nothing - - ''' - ''' 用于依赖检查的 ModID。 - ''' - Public Property ModId As String - Get - If _ModId Is Nothing Then Load() - Return _ModId - End Get - Set(value As String) - If value Is Nothing Then Exit Property - value = RegexSeek(value.ToLower, "[0-9a-z_]+") - If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then - If Not PossibleModId.Contains(value) Then PossibleModId.Add(value) - If _ModId Is Nothing Then _ModId = value - End If - End Set - End Property - Private _ModId As String = Nothing - ''' - ''' 其他可能的 ModID。 - ''' - Public PossibleModId As New List(Of String) - - ''' - ''' Mod 的主页。 - ''' - Public Property Url As String - Get - If _Url Is Nothing Then Load() - Return _Url - End Get - Set(value As String) - If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWith("http") Then - _Url = value - End If - End Set - End Property - Private _Url As String = Nothing - - ''' - ''' Mod 的作者列表。 - ''' - Public Property Authors As String - Get - If _Authors Is Nothing Then Load() - Return _Authors - End Get - Set(value As String) - If _Authors Is Nothing AndAlso Not String.IsNullOrWhiteSpace(value) Then - _Authors = value - End If - End Set - End Property - Private _Authors As String = Nothing - - ''' - ''' 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。 - ''' - Public ReadOnly Property Dependencies As Dictionary(Of String, String) - Get - Load() - Return _Dependencies - End Get - End Property - Private _Dependencies As New Dictionary(Of String, String) - Private Sub AddDependency(ModID As String, Optional VersionRequirement As String = Nothing) - '确保信息正确 - If ModID Is Nothing OrElse ModID.Count < 2 Then Exit Sub - ModID = ModID.ToLower - If ModID = "name" OrElse Val(ModID).ToString = ModID Then Exit Sub '跳过 name 与纯数字 id - If VersionRequirement Is Nothing OrElse ((Not VersionRequirement.Contains(".")) AndAlso (Not VersionRequirement.Contains("-"))) OrElse VersionRequirement.Contains("$") Then - VersionRequirement = Nothing - Else - If (Not VersionRequirement.StartsWith("[")) AndAlso (Not VersionRequirement.StartsWith("(")) AndAlso (Not VersionRequirement.EndsWith("]")) AndAlso (Not VersionRequirement.EndsWith(")")) Then VersionRequirement = "[" & VersionRequirement & ",)" - End If - '向依赖项中添加 - If _Dependencies.ContainsKey(ModID) Then - If _Dependencies(ModID) Is Nothing Then _Dependencies(ModID) = VersionRequirement - Else - _Dependencies.Add(ModID, VersionRequirement) - End If - End Sub - -#End Region - -#Region "加载步骤标记" - - '1. 进行文件可用性检查 - ' 成功:继续第二步。 - ' 失败:标记 FileUnavailableReason, 并停止后续加载。 - ''' - ''' 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步) - ''' - Private IsLoaded As Boolean = False - ''' - ''' Mod 文件是否可被正常读取。 - ''' - Public ReadOnly Property IsFileAvailable As Boolean - Get - Load() - Return FileUnavailableReason Is Nothing - End Get - End Property - ''' - ''' Mod 文件出错的原因。若无错误,则为 Nothing。 - ''' - Public ReadOnly Property FileUnavailableReason As Exception - Get - Load() - Return _FileUnavailableReason - End Get - End Property - Private _FileUnavailableReason As Exception = Nothing - - '2. 进行 .class 以外的信息获取 - ' 成功:标记 IsInfoWithoutClassAvailable。 - ' 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。 - ''' - ''' 是否已在不获取 .class 文件的前提下完成了所需信息的加载。 - ''' - Private IsInfoWithoutClassAvailable As Boolean = False - - '3. 尝试从 .class 文件中获取信息 - ' 成功:标记 IsInfoWithClassAvailable。 - ' 失败:什么也不干。 - ''' - ''' 是否已进行 .class 文件的信息获取。 - ''' - Private IsInfoWithClassLoaded As Boolean = False - ''' - ''' 是否已在 .class 文件中完成了所需信息的加载。 - ''' - Private IsInfoWithClassAvailable As Boolean = False - -#End Region - -#Region "加载" - - ''' - ''' 初始化所有数据。 - ''' - Private Sub Init() - _Name = Nothing - _Description = Nothing - _Version = Nothing - _ModId = Nothing - PossibleModId = New List(Of String) - _Dependencies = New Dictionary(Of String, String) - IsLoaded = False - _FileUnavailableReason = Nothing - IsInfoWithoutClassAvailable = False - IsInfoWithClassLoaded = False - IsInfoWithClassAvailable = False - End Sub - - ''' - ''' 进行文件可用性检查与 .class 以外的信息获取。 - ''' - Public Sub Load(Optional ForceReload As Boolean = False) - If IsLoaded AndAlso Not ForceReload Then Exit Sub - '初始化 - Init() - '阶段 1:基础可用性检查 - Dim Jar As ZipArchive - Try - '打开 Jar 文件 - If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")") - If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")") - Jar = New ZipArchive(New FileStream(Path, FileMode.Open)) - If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空") - Catch ex As UnauthorizedAccessException - Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer) - _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex) - Jar = Nothing - Catch ex As Exception - Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer) - _FileUnavailableReason = ex - Jar = Nothing - End Try - '阶段 2:信息获取 - If Jar IsNot Nothing Then LoadWithoutClass(Jar) - '阶段 3: Class 信息获取 - If Jar IsNot Nothing AndAlso Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar) - '释放文件 - Try - If Jar IsNot Nothing Then Jar.Dispose() - Catch - End Try - '完成标记 - IsLoaded = True - End Sub - - ''' - ''' 进行不使用 .class 文件的信息获取。 - ''' - Private Sub LoadWithoutClass(Jar As ZipArchive) - -#Region "尝试使用 mcmod.info" - Try - '获取信息文件 - Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info") - Dim InfoString As String = Nothing - If InfoEntry IsNot Nothing Then - InfoString = ReadFile(InfoEntry.Open()) - If InfoString.Length < 15 Then InfoString = Nothing - End If - If InfoString Is Nothing Then Exit Try - '获取可用 Json 项 - Dim InfoObject As JObject - Dim JsonObject = GetJson(InfoString) - If JsonObject.Type = JTokenType.Array Then - InfoObject = JsonObject(0) - Else - InfoObject = JsonObject("modList")(0) - End If - '从文件中获取 Mod 信息项 - Name = InfoObject("name") - Description = InfoObject("description") - Version = InfoObject("version") - Url = InfoObject("url") - ModId = InfoObject("modid") - Dim AuthorJson As JArray = InfoObject("authorList") - If AuthorJson IsNot Nothing Then - Dim Author As New List(Of String) - For Each Token In AuthorJson - Author.Add(Token.ToString) - Next - If Author.Count > 0 Then Authors = Join(Author, ", ") - End If - Dim Reqs As JArray = InfoObject("requiredMods") - If Reqs IsNot Nothing Then - For Each Token As String In Reqs - If Not String.IsNullOrEmpty(Token) Then - Token = Token.Substring(Token.IndexOf(":") + 1) - If Token.Contains("@") Then - AddDependency(Token.Split("@")(0), Token.Split("@")(1)) - Else - AddDependency(Token) - End If - End If - Next - End If - Reqs = InfoObject("dependancies") - If Reqs IsNot Nothing Then - For Each Token As String In Reqs - If Not String.IsNullOrEmpty(Token) Then - Token = Token.Substring(Token.IndexOf(":") + 1) - If Token.Contains("@") Then - AddDependency(Token.Split("@")(0), Token.Split("@")(1)) - Else - AddDependency(Token) - End If - End If - Next - End If - Catch ex As Exception - Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer) - End Try -#End Region -#Region "尝试使用 mods.toml" - Try - '获取 mods.toml 文件 - Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml") - Dim TomlText As String = Nothing - If TomlEntry IsNot Nothing Then - TomlText = ReadFile(TomlEntry.Open()) - If TomlText.Length < 15 Then TomlText = Nothing - End If - If TomlText Is Nothing Then Exit Try - '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行 - Dim Lines As New List(Of String) - For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符 - If Line.StartsWith("#") Then '去除注释 - Continue For - ElseIf Line.Contains("#") Then - Line = Line.Substring(0, Line.IndexOf("#")) - End If - Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格 - If Line.Count > 0 Then Lines.Add(Line) '去除空行 - Next - '读取文件数据 - Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))} - For i = 0 To Lines.Count - 1 - Dim Line As String = Lines(i) - If Line.StartsWith("[[") AndAlso Line.EndsWith("]]") Then - '段落标记 - Dim Header = Line.Replace("[", "").Replace("]", "") - TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object))) - ElseIf Line.Contains("=") Then - '字段标记 - Dim Key As String = Line.Substring(0, Line.IndexOf("=")).TrimEnd(New Char() {" "c, " "c, " "c}) - Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, " "c, " "c}) - Dim Value As Object - If RawValue.StartsWith("""") AndAlso RawValue.EndsWith("""") Then - '单行字符串 - Value = RawValue.Trim("""") - ElseIf RawValue.StartsWith("'''") Then - '多行字符串 - Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")} - Do Until i >= Lines.Count - 1 - i += 1 - Dim ValueLine As String = Lines(i) - If ValueLine.EndsWith("'''") Then - ValueLines.Add(ValueLine.TrimEnd("'")) - Exit Do - Else - ValueLines.Add(ValueLine) - End If - Loop - Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf) - ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then - '布尔型 - Value = (RawValue.ToLower = "true") - ElseIf Val(RawValue).ToString = RawValue Then - '数字型 - Value = Val(RawValue) - Else - '不知道是个啥玩意儿,直接存储 - Value = RawValue - End If - TomlData.Last.Value(Key) = Value - Else - '不知道是个啥玩意儿 - Exit Try - End If - Next - '从文件数据中获取信息 - Dim ModEntry As Dictionary(Of String, Object) = Nothing - For Each TomlSubData In TomlData - If TomlSubData.Key = "mods" Then - ModEntry = TomlSubData.Value - Exit For - End If - Next - If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try - ModId = ModEntry("modId") - If _ModId Is Nothing Then Exit Try '设置了无效的 ModID - If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName") - If ModEntry.ContainsKey("description") Then Description = ModEntry("description") - If ModEntry.ContainsKey("version") Then Version = ModEntry("version") - If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL") - If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors") - For Each TomlSubData In TomlData - If TomlSubData.Key.StartsWith("dependencies") Then - Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value - If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso - DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then - AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing)) - End If - End If - Next - '加载成功 - GoTo Finished - Catch ex As Exception - Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer) - End Try -#End Region -#Region "尝试使用 fabric.mod.json" - Try - '获取 fabric.mod.json 文件 - Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json") - Dim FabricText As String = Nothing - If FabricEntry IsNot Nothing Then - FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8) - If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing - End If - If FabricText Is Nothing Then Exit Try - Dim FabricObject As JObject = GetJson(FabricText) -GotFabric: - '从文件中获取 Mod 信息项 - If FabricObject.ContainsKey("name") Then Name = FabricObject("name") - If FabricObject.ContainsKey("version") Then Version = FabricObject("version") - If FabricObject.ContainsKey("description") Then Description = FabricObject("description") - If FabricObject.ContainsKey("id") Then ModId = FabricObject("id") - If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "") - Dim AuthorJson As JArray = FabricObject("authors") - If AuthorJson IsNot Nothing Then - Dim Author As New List(Of String) - For Each Token In AuthorJson - Author.Add(Token.ToString) - Next - If Author.Count > 0 Then Authors = Join(Author, ", ") - End If - 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then - ' '添加 Minecraft 依赖 - ' Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "") - ' If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft) - ' '添加其他依赖 - ' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "") - ' If Deps <> "" Then - ' For Each Dep In Deps.Split(";") - ' If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For - ' Dep = Dep.Substring(Dep.IndexOf(":") + 1) - ' If Dep.Contains("@") Then - ' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1)) - ' Else - ' AddDependency(Dep) - ' End If - ' Next - ' End If - 'End If - '加载成功 - GoTo Finished - Catch ex As Exception - Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer) - End Try -#End Region -#Region "尝试使用 fml_cache_annotation.json" - Try - '获取 fml_cache_annotation.json 文件 - Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json") - Dim FmlText As String = Nothing - If FmlEntry IsNot Nothing Then - FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8) - If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing - End If - If FmlText Is Nothing Then Exit Try - Dim FmlJson As JObject = GetJson(FmlText) - '获取可用 Json 项 - Dim FmlObject As JObject = Nothing - For Each ModFilePair In FmlJson - Dim ModFileAnnos As JArray = ModFilePair.Value("annotations") - If ModFileAnnos IsNot Nothing Then - '先获取 Mod - For Each ModFileAnno In ModFileAnnos - Dim Name As String = If(ModFileAnno("name"), "") - If Name = "Lnet/minecraftforge/fml/common/Mod;" Then - FmlObject = ModFileAnno("values") - GoTo Got - End If - Next - End If - Next - Exit Try -Got: - '从文件中获取 Mod 信息项 - If FmlObject.ContainsKey("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then - '要求使用 mcmod.info 中的信息 - Dim value As String = FmlObject("modid")("value") - If value Is Nothing Then Exit Try - value = RegexSeek(value.ToLower, "[0-9a-z_]+") - If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then - If Not PossibleModId.Contains(value) Then PossibleModId.Add(value) - End If - Exit Try - End If - If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value") - If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value") - If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value") - If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then - '添加 Minecraft 依赖 - Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "") - If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft) - '添加其他依赖 - Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "") - If Deps <> "" Then - For Each Dep In Deps.Split(";") - If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For - Dep = Dep.Substring(Dep.IndexOf(":") + 1) - If Dep.Contains("@") Then - AddDependency(Dep.Split("@")(0), Dep.Split("@")(1)) - Else - AddDependency(Dep) - End If - Next - End If - End If - Catch ex As Exception - Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer) - End Try -#End Region -Finished: -#Region "将 Version 代号转换为 META-INF 中的版本" - If _Version = "version" Then - Try - Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF") - If MetaEntry IsNot Nothing Then - Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":") - If MetaString.Contains("Implementation-Version:") Then - MetaString = MetaString.Substring(MetaString.IndexOf("Implementation-Version:") + "Implementation-Version:".Count) - MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim - Version = MetaString - End If - End If - Catch ex As Exception - Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer) - Version = Nothing - End Try - End If - If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing -#End Region - - IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing - End Sub - - ''' - ''' 进行使用 .class 文件的信息获取。 - ''' - Private Sub LoadWithClass(Jar As ZipArchive) - Try - '查找入口点文件 - Dim ModClass As String = Nothing - Dim ModClassNotBest As String = Nothing '非完美匹配 - For Each Entry In Jar.Entries - If Entry.Name.EndsWith(".class") Then - Dim Temp As String = ReadFile(Entry.Open()).ToLower - If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then - ModClass = Temp - Exit For - ElseIf Temp.Contains("modid") Then - ModClassNotBest = Temp - End If - End If - Next - If ModClass Is Nothing Then ModClass = ModClassNotBest - If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点") - ModClass = ModClass.Replace("ljava/lang/string;", "") - If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte - Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer - '获取 ModID - If _ModId Is Nothing Then - IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1 - IncreaseCount = 0 - Do While Convert.ToInt32(ModClass(IndexHead)) < 32 - IndexHead += 1 - IncreaseCount += 1 - If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败") - Loop - IndexTail = IndexHead + 1 - IncreaseCount = 0 - Do Until Convert.ToInt32(ModClass(IndexTail)) < 32 - IndexTail += 1 - IncreaseCount += 1 - If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败") - Loop - ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead) - End If - '获取 Version - If _Version Is Nothing AndAlso ModClass.Contains("version") Then - IndexHead = ModClass.IndexOf("version") + "version".Length + 1 - IncreaseCount = 0 - Do While Convert.ToInt32(ModClass(IndexHead)) < 32 - IndexHead += 1 - IncreaseCount += 1 - If IncreaseCount > 10 Then GoTo VersionFindFail - Loop - IndexTail = IndexHead + 1 - IncreaseCount = 0 - Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse - (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse - (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c) - IndexTail += 1 - IncreaseCount += 1 - If IncreaseCount > 50 Then GoTo VersionFindFail - Loop - _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead) - End If -VersionFindFail: - '获取 Dependencies - IndexHead = ModClass.IndexOf("dependencies") - If IndexHead > 0 Then - If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299) - Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?") - For Each Token As String In Deps - If Not String.IsNullOrEmpty(Token) Then - If Token.Contains("@") Then - AddDependency(Token.Split("@")(0), Token.Split("@")(1)) - Else - AddDependency(Token) - End If - End If - Next - End If - Catch ex As Exception - Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal) - End Try - IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing - End Sub - -#End Region - - ''' - ''' 是否可能为前置 Mod。 - ''' - Public Function IsPresetMod() As Boolean - Return Dependencies.Count = 0 AndAlso (Name IsNot Nothing) AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib")) - End Function - - ''' - ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。 - ''' - Public Shared Function IsModFile(Path As String) - If Path Is Nothing OrElse Not Path.Contains(".") Then Return False - Path = Path.ToLower - If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse - Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") Then Return True - Return False - End Function - - End Class - - '加载 Mod 列表 - Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad) - Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod))) - Try - RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False) - - '获取 Mod 文件夹下的可用文件列表 - Dim ModFileList As New List(Of FileInfo) - If Directory.Exists(Loader.Input) Then - Dim RawName As String = Loader.Input.ToLower - For Each File As FileInfo In EnumerateFiles(Loader.Input) - If File.DirectoryName.ToLower & "\" <> RawName Then - '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod - If Not (PageVersionLeft.Version IsNot Nothing AndAlso PageVersionLeft.Version.Version.HasForge AndAlso - PageVersionLeft.Version.Version.McCodeMain < 13 AndAlso - File.Directory.Name = "1." & PageVersionLeft.Version.Version.McCodeMain & "." & PageVersionLeft.Version.Version.McCodeSub) Then - Continue For - End If - End If - If McMod.IsModFile(File.FullName) Then ModFileList.Add(File) - Next - End If - - '确定是否显示进度 - Loader.Progress = 0.03 - If ModFileList.Count > 100 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True) - - '加载为 Mod 列表 - Dim ModList As New List(Of McMod) - For Each ModFile As FileInfo In ModFileList - Dim ModEntity As New McMod(ModFile.FullName) - ModEntity.Load() - ModList.Add(ModEntity) - Loader.Progress += 0.9 / ModFileList.Count - Next - Loader.Progress = 0.93 - - '排序 - ModList = Sort(ModList, Function(Left As McMod, Right As McMod) As Boolean - If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then - Return Left.State = McMod.McModState.Unavaliable - Else - Return Left.FileName < Right.FileName - End If - End Function) - Loader.Progress = 0.98 - - '回设 - If Loader.IsAborted Then Exit Sub - Loader.Output = ModList - - Catch ex As Exception - Log(ex, "Mod 列表加载失败", LogLevel.Debug) - Throw - End Try - End Sub - -#If DEBUG Then - ''' - ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。 - ''' - Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String) - Dim Result As New List(Of String) - '令所有 Mod 进行基础检查,并归纳需要检查的 Mod - Dim CurrentModList As New List(Of McMod) - For Each ModEntity In Mods - If Not ModEntity.IsFileAvailable Then - Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path) - Continue For - End If - If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity) - Next - '添加默认依赖 - Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path} - If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"}) - CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"}) - '检查重复的 Mod,并添加对应的依赖 - For Each ModEntity In CurrentModList - For Each PossibleModId In ModEntity.PossibleModId - If CurrentDependencies.ContainsKey(PossibleModId) Then - If CurrentDependencies(PossibleModId)(2) = 1 Then - Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf & - " - " & ModEntity.FileName & vbCrLf & - " - " & CurrentDependencies(PossibleModId)(1)) - Else - Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf & - " - " & ModEntity.FileName & vbCrLf & - " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer) - End If - Else - CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count}) - End If - Next - Next - '检查依赖 - For Each ModEntity In CurrentModList - Try - For Each Dependency In ModEntity.Dependencies - Dim ReqId As String = Dependency.Key - If ReqId.Count < 2 Then Continue For '确保正常 - If ReqId = ModEntity.ModId Then Continue For '跳过自体引用 - If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用 - If Dependency.Value IsNot Nothing Then - '获取分段后的详细版本信息 - Dim ReqVersion As String = Dependency.Value - Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[") - Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]") - Dim ReqVersionHead As String - Dim ReqVersionTail As String - If ReqVersion.Contains(",") Then - ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray()) - ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray()) - Else - ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray()) - ReqVersionTail = ReqVersionHead - If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then - ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1) - ReqVersionTailCanEqual = False - End If - End If - If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1) - If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1) - '获取报错描述文本 - Dim VersionRequire As String - If ReqVersionHead = ReqVersionTail Then - VersionRequire = "应为 " & ReqVersionHead - ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then - VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail - ElseIf ReqVersionHead.Contains(".") Then - If ReqVersionHeadCanEqual Then - VersionRequire = "最低应为 " & ReqVersionHead - Else - VersionRequire = "应高于 " & ReqVersionHead - End If - ElseIf ReqVersionTail.Contains(".") Then - If ReqVersionTailCanEqual Then - VersionRequire = "最高应为 " & ReqVersionHead - Else - VersionRequire = "应低于 " & ReqVersionHead - End If - Else - VersionRequire = "" - End If - '检查前置 Mod 是否存在,并获取其版本 - If Not CurrentDependencies.ContainsKey(ReqId) Then - Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName) - Continue For - End If - Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0") - If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1) - '对比前置 Mod 头部版本 - If ReqVersionHead.Contains(".") Then - If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then - Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf & - " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), "")) - Continue For - End If - End If - '对比前置 Mod 尾部版本 - If ReqVersionTail.Contains(".") Then - If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then - Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf & - " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), "")) - Continue For - End If - End If - Else - If Not CurrentDependencies.ContainsKey(Dependency.Key) Then - Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName) - Continue For - End If - End If - Next - Catch ex As Exception - Result.Add("检查 Mod 时出错:" & GetExceptionSummary(ex) & vbCrLf & " - " & ModEntity.FileName) - Log(ex, "检查 Mod 时出错") - End Try - Next - If Result.Count = 0 Then - Log("[Minecraft] Mod 检查未发现异常") - Else - Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf)) - End If - Return Result - End Function -#End If - #End Region ''' diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb new file mode 100644 index 0000000..27a31aa --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb @@ -0,0 +1,903 @@ +Imports System.IO.Compression + +Public Module ModMod + + Public Class McMod + +#Region "基础" + + ''' + ''' Mod 文件的地址。 + ''' + Public ReadOnly Path As String + Public Sub New(Path As String) + Me.Path = If(Path, "") + End Sub + + ''' + ''' Mod 的完整文件名。 + ''' + Public ReadOnly Property FileName As String + Get + Return GetFileNameFromPath(Path) + End Get + End Property + + ''' + ''' Mod 的完整文件名,去除最后的 .disabled。 + ''' + Public ReadOnly Property RawFileName As String + Get + Return FileName.Replace(".disabled", "") + End Get + End Property + + ''' + ''' Mod 的状态。 + ''' + Public ReadOnly Property State As McModState + Get + Load() + If Not IsFileAvailable Then + Return McModState.Unavaliable + ElseIf Path.EndsWith(".disabled") Then + Return McModState.Disabled + Else + Return McModState.Fine + End If + End Get + End Property + Public Enum McModState As Integer + Fine = 0 + Disabled = 1 + Unavaliable = 2 + End Enum + +#End Region + +#Region "信息项" + + ''' + ''' Mod 的名称。若不可用则为 ModID 或无扩展的文件名。 + ''' + Public Property Name As String + Get + If _Name Is Nothing Then Load() + If _Name Is Nothing Then _Name = _ModId + If _Name Is Nothing Then _Name = GetFileNameWithoutExtentionFromPath(Path) + Return _Name + End Get + Set(value As String) + If _Name Is Nothing AndAlso value IsNot Nothing AndAlso + value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then + _Name = value + End If + End Set + End Property + Private _Name As String = Nothing + + ''' + ''' Mod 的描述信息。 + ''' + Public Property Description As String + Get + If _Description Is Nothing Then Load() + If _Description Is Nothing AndAlso FileUnavailableReason IsNot Nothing Then _Description = FileUnavailableReason.Message + 'If _Description Is Nothing Then _Description = Path + Return _Description + End Get + Set(value As String) + If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then + _Description = value.ToString.Trim(vbLf) + '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号 + If _Description.ToLower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "." + End If + End Set + End Property + Private _Description As String = Nothing + + ''' + ''' Mod 的版本,不保证符合版本格式规范。 + ''' + Public Property Version As String + Get + If _Version Is Nothing Then Load() + Return _Version + End Get + Set(value As String) + If _Version IsNot Nothing AndAlso (_Version.Contains(".") OrElse _Version.Contains("-")) Then Exit Property + If value IsNot Nothing AndAlso value.ToLower.Contains("version") Then value = "version" '需要修改的标识 + _Version = value + End Set + End Property + Private _Version As String = Nothing + + ''' + ''' 用于依赖检查的 ModID。 + ''' + Public Property ModId As String + Get + If _ModId Is Nothing Then Load() + Return _ModId + End Get + Set(value As String) + If value Is Nothing Then Exit Property + value = RegexSeek(value.ToLower, "[0-9a-z_-]+") + If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then + If Not PossibleModId.Contains(value) Then PossibleModId.Add(value) + If _ModId Is Nothing Then _ModId = value + End If + End Set + End Property + Private _ModId As String = Nothing + ''' + ''' 其他可能的 ModID。 + ''' + Public PossibleModId As New List(Of String) + + ''' + ''' Mod 的主页。 + ''' + Public Property Url As String + Get + If _Url Is Nothing Then Load() + Return _Url + End Get + Set(value As String) + If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWith("http") Then + _Url = value + End If + End Set + End Property + Private _Url As String = Nothing + + ''' + ''' Mod 的作者列表。 + ''' + Public Property Authors As String + Get + If _Authors Is Nothing Then Load() + Return _Authors + End Get + Set(value As String) + If _Authors Is Nothing AndAlso Not String.IsNullOrWhiteSpace(value) Then + _Authors = value + End If + End Set + End Property + Private _Authors As String = Nothing + + ''' + ''' 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。 + ''' + Public ReadOnly Property Dependencies As Dictionary(Of String, String) + Get + Load() + Return _Dependencies + End Get + End Property + Private _Dependencies As New Dictionary(Of String, String) + Private Sub AddDependency(ModID As String, Optional VersionRequirement As String = Nothing) + '确保信息正确 + If ModID Is Nothing OrElse ModID.Count < 2 Then Exit Sub + ModID = ModID.ToLower + If ModID = "name" OrElse Val(ModID).ToString = ModID Then Exit Sub '跳过 name 与纯数字 id + If VersionRequirement Is Nothing OrElse ((Not VersionRequirement.Contains(".")) AndAlso (Not VersionRequirement.Contains("-"))) OrElse VersionRequirement.Contains("$") Then + VersionRequirement = Nothing + Else + If (Not VersionRequirement.StartsWith("[")) AndAlso (Not VersionRequirement.StartsWith("(")) AndAlso (Not VersionRequirement.EndsWith("]")) AndAlso (Not VersionRequirement.EndsWith(")")) Then VersionRequirement = "[" & VersionRequirement & ",)" + End If + '向依赖项中添加 + If _Dependencies.ContainsKey(ModID) Then + If _Dependencies(ModID) Is Nothing Then _Dependencies(ModID) = VersionRequirement + Else + _Dependencies.Add(ModID, VersionRequirement) + End If + End Sub + +#End Region + +#Region "加载步骤标记" + + '1. 进行文件可用性检查 + ' 成功:继续第二步。 + ' 失败:标记 FileUnavailableReason, 并停止后续加载。 + ''' + ''' 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步) + ''' + Private IsLoaded As Boolean = False + ''' + ''' Mod 文件是否可被正常读取。 + ''' + Public ReadOnly Property IsFileAvailable As Boolean + Get + Load() + Return FileUnavailableReason Is Nothing + End Get + End Property + ''' + ''' Mod 文件出错的原因。若无错误,则为 Nothing。 + ''' + Public ReadOnly Property FileUnavailableReason As Exception + Get + Load() + Return _FileUnavailableReason + End Get + End Property + Private _FileUnavailableReason As Exception = Nothing + + '2. 进行 .class 以外的信息获取 + ' 成功:标记 IsInfoWithoutClassAvailable。 + ' 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。 + ''' + ''' 是否已在不获取 .class 文件的前提下完成了所需信息的加载。 + ''' + Private IsInfoWithoutClassAvailable As Boolean = False + + '3. 尝试从 .class 文件中获取信息 + ' 成功:标记 IsInfoWithClassAvailable。 + ' 失败:什么也不干。 + ''' + ''' 是否已进行 .class 文件的信息获取。 + ''' + Private IsInfoWithClassLoaded As Boolean = False + ''' + ''' 是否已在 .class 文件中完成了所需信息的加载。 + ''' + Private IsInfoWithClassAvailable As Boolean = False + +#End Region + +#Region "加载" + + ''' + ''' 初始化所有数据。 + ''' + Private Sub Init() + _Name = Nothing + _Description = Nothing + _Version = Nothing + _ModId = Nothing + PossibleModId = New List(Of String) + _Dependencies = New Dictionary(Of String, String) + IsLoaded = False + _FileUnavailableReason = Nothing + IsInfoWithoutClassAvailable = False + IsInfoWithClassLoaded = False + IsInfoWithClassAvailable = False + End Sub + + ''' + ''' 进行文件可用性检查与 .class 以外的信息获取。 + ''' + Public Sub Load(Optional ForceReload As Boolean = False) + If IsLoaded AndAlso Not ForceReload Then Exit Sub + '初始化 + Init() + '阶段 1:基础可用性检查 + Dim Jar As ZipArchive + Try + '打开 Jar 文件 + If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")") + If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")") + Jar = New ZipArchive(New FileStream(Path, FileMode.Open)) + If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空") + Catch ex As UnauthorizedAccessException + Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer) + _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex) + Jar = Nothing + Catch ex As Exception + Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer) + _FileUnavailableReason = ex + Jar = Nothing + End Try + '阶段 2:信息获取 + If Jar IsNot Nothing Then LoadWithoutClass(Jar) + '阶段 3: Class 信息获取 + If Jar IsNot Nothing AndAlso Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar) + '释放文件 + Try + If Jar IsNot Nothing Then Jar.Dispose() + Catch + End Try + '完成标记 + IsLoaded = True + End Sub + + ''' + ''' 进行不使用 .class 文件的信息获取。 + ''' + Private Sub LoadWithoutClass(Jar As ZipArchive) + +#Region "尝试使用 mcmod.info" + Try + '获取信息文件 + Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info") + Dim InfoString As String = Nothing + If InfoEntry IsNot Nothing Then + InfoString = ReadFile(InfoEntry.Open()) + If InfoString.Length < 15 Then InfoString = Nothing + End If + If InfoString Is Nothing Then Exit Try + '获取可用 Json 项 + Dim InfoObject As JObject + Dim JsonObject = GetJson(InfoString) + If JsonObject.Type = JTokenType.Array Then + InfoObject = JsonObject(0) + Else + InfoObject = JsonObject("modList")(0) + End If + '从文件中获取 Mod 信息项 + Name = InfoObject("name") + Description = InfoObject("description") + Version = InfoObject("version") + Url = InfoObject("url") + ModId = InfoObject("modid") + Dim AuthorJson As JArray = InfoObject("authorList") + If AuthorJson IsNot Nothing Then + Dim Author As New List(Of String) + For Each Token In AuthorJson + Author.Add(Token.ToString) + Next + If Author.Count > 0 Then Authors = Join(Author, ", ") + End If + Dim Reqs As JArray = InfoObject("requiredMods") + If Reqs IsNot Nothing Then + For Each Token As String In Reqs + If Not String.IsNullOrEmpty(Token) Then + Token = Token.Substring(Token.IndexOf(":") + 1) + If Token.Contains("@") Then + AddDependency(Token.Split("@")(0), Token.Split("@")(1)) + Else + AddDependency(Token) + End If + End If + Next + End If + Reqs = InfoObject("dependancies") + If Reqs IsNot Nothing Then + For Each Token As String In Reqs + If Not String.IsNullOrEmpty(Token) Then + Token = Token.Substring(Token.IndexOf(":") + 1) + If Token.Contains("@") Then + AddDependency(Token.Split("@")(0), Token.Split("@")(1)) + Else + AddDependency(Token) + End If + End If + Next + End If + Catch ex As Exception + Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer) + End Try +#End Region +#Region "尝试使用 mods.toml" + Try + '获取 mods.toml 文件 + Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml") + Dim TomlText As String = Nothing + If TomlEntry IsNot Nothing Then + TomlText = ReadFile(TomlEntry.Open()) + If TomlText.Length < 15 Then TomlText = Nothing + End If + If TomlText Is Nothing Then Exit Try + '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行 + Dim Lines As New List(Of String) + For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符 + If Line.StartsWith("#") Then '去除注释 + Continue For + ElseIf Line.Contains("#") Then + Line = Line.Substring(0, Line.IndexOf("#")) + End If + Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格 + If Line.Count > 0 Then Lines.Add(Line) '去除空行 + Next + '读取文件数据 + Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))} + For i = 0 To Lines.Count - 1 + Dim Line As String = Lines(i) + If Line.StartsWith("[") AndAlso Line.EndsWith("]") Then + '段落标记 + Dim Header = Line.Trim("[]".ToCharArray) + TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object))) + ElseIf Line.Contains("=") Then + '字段标记 + Dim Key As String = Line.Substring(0, Line.IndexOf("=")).TrimEnd(New Char() {" "c, " "c, " "c}) + Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, " "c, " "c}) + Dim Value As Object + If RawValue.StartsWith("""") AndAlso RawValue.EndsWith("""") Then + '单行字符串 + Value = RawValue.Trim("""") + ElseIf RawValue.StartsWith("'''") Then + '多行字符串 + Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")} + Do Until i >= Lines.Count - 1 + i += 1 + Dim ValueLine As String = Lines(i) + If ValueLine.EndsWith("'''") Then + ValueLines.Add(ValueLine.TrimEnd("'")) + Exit Do + Else + ValueLines.Add(ValueLine) + End If + Loop + Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf) + ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then + '布尔型 + Value = (RawValue.ToLower = "true") + ElseIf Val(RawValue).ToString = RawValue Then + '数字型 + Value = Val(RawValue) + Else + '不知道是个啥玩意儿,直接存储 + Value = RawValue + End If + TomlData.Last.Value(Key) = Value + Else + '不知道是个啥玩意儿 + Exit Try + End If + Next + '从文件数据中获取信息 + Dim ModEntry As Dictionary(Of String, Object) = Nothing + For Each TomlSubData In TomlData + If TomlSubData.Key = "mods" Then + ModEntry = TomlSubData.Value + Exit For + End If + Next + If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try + ModId = ModEntry("modId") + If _ModId Is Nothing Then Exit Try '设置了无效的 ModID + If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName") + If ModEntry.ContainsKey("description") Then Description = ModEntry("description") + If ModEntry.ContainsKey("version") Then Version = ModEntry("version") + If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL") + If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors") + For Each TomlSubData In TomlData + If TomlSubData.Key.ToLower = $"dependencies.{ModId.ToLower}" Then + Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value + If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso + DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then + AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing)) + End If + End If + Next + '加载成功 + GoTo Finished + Catch ex As Exception + Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer) + End Try +#End Region +#Region "尝试使用 fabric.mod.json" + Try + '获取 fabric.mod.json 文件 + Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json") + Dim FabricText As String = Nothing + If FabricEntry IsNot Nothing Then + FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8) + If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing + End If + If FabricText Is Nothing Then Exit Try + Dim FabricObject As JObject = GetJson(FabricText) +GotFabric: + '从文件中获取 Mod 信息项 + If FabricObject.ContainsKey("name") Then Name = FabricObject("name") + If FabricObject.ContainsKey("version") Then Version = FabricObject("version") + If FabricObject.ContainsKey("description") Then Description = FabricObject("description") + If FabricObject.ContainsKey("id") Then ModId = FabricObject("id") + If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "") + Dim AuthorJson As JArray = FabricObject("authors") + If AuthorJson IsNot Nothing Then + Dim Author As New List(Of String) + For Each Token In AuthorJson + Author.Add(Token.ToString) + Next + If Author.Count > 0 Then Authors = Join(Author, ", ") + End If + 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then + ' '添加 Minecraft 依赖 + ' Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "") + ' If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft) + ' '添加其他依赖 + ' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "") + ' If Deps <> "" Then + ' For Each Dep In Deps.Split(";") + ' If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For + ' Dep = Dep.Substring(Dep.IndexOf(":") + 1) + ' If Dep.Contains("@") Then + ' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1)) + ' Else + ' AddDependency(Dep) + ' End If + ' Next + ' End If + 'End If + '加载成功 + GoTo Finished + Catch ex As Exception + Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer) + End Try +#End Region +#Region "尝试使用 fml_cache_annotation.json" + Try + '获取 fml_cache_annotation.json 文件 + Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json") + Dim FmlText As String = Nothing + If FmlEntry IsNot Nothing Then + FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8) + If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing + End If + If FmlText Is Nothing Then Exit Try + Dim FmlJson As JObject = GetJson(FmlText) + '获取可用 Json 项 + Dim FmlObject As JObject = Nothing + For Each ModFilePair In FmlJson + Dim ModFileAnnos As JArray = ModFilePair.Value("annotations") + If ModFileAnnos IsNot Nothing Then + '先获取 Mod + For Each ModFileAnno In ModFileAnnos + Dim Name As String = If(ModFileAnno("name"), "") + If Name = "Lnet/minecraftforge/fml/common/Mod;" Then + FmlObject = ModFileAnno("values") + GoTo Got + End If + Next + End If + Next + Exit Try +Got: + '从文件中获取 Mod 信息项 + If FmlObject.ContainsKey("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then + '要求使用 mcmod.info 中的信息 + Dim value As String = FmlObject("modid")("value") + If value Is Nothing Then Exit Try + value = RegexSeek(value.ToLower, "[0-9a-z_]+") + If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then + If Not PossibleModId.Contains(value) Then PossibleModId.Add(value) + End If + Exit Try + End If + If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value") + If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value") + If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value") + If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then + '添加 Minecraft 依赖 + Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "") + If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft) + '添加其他依赖 + Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "") + If Deps <> "" Then + For Each Dep In Deps.Split(";") + If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For + Dep = Dep.Substring(Dep.IndexOf(":") + 1) + If Dep.Contains("@") Then + AddDependency(Dep.Split("@")(0), Dep.Split("@")(1)) + Else + AddDependency(Dep) + End If + Next + End If + End If + Catch ex As Exception + Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer) + End Try +#End Region +Finished: +#Region "将 Version 代号转换为 META-INF 中的版本" + If _Version = "version" Then + Try + Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF") + If MetaEntry IsNot Nothing Then + Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":") + If MetaString.Contains("Implementation-Version:") Then + MetaString = MetaString.Substring(MetaString.IndexOf("Implementation-Version:") + "Implementation-Version:".Count) + MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim + Version = MetaString + End If + End If + Catch ex As Exception + Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer) + Version = Nothing + End Try + End If + If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing +#End Region + + IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing + End Sub + + ''' + ''' 进行使用 .class 文件的信息获取。 + ''' + Private Sub LoadWithClass(Jar As ZipArchive) + Try + '查找入口点文件 + Dim ModClass As String = Nothing + Dim ModClassNotBest As String = Nothing '非完美匹配 + For Each Entry In Jar.Entries + If Entry.Name.EndsWith(".class") Then + Dim Temp As String = ReadFile(Entry.Open()).ToLower + If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then + ModClass = Temp + Exit For + ElseIf Temp.Contains("modid") Then + ModClassNotBest = Temp + End If + End If + Next + If ModClass Is Nothing Then ModClass = ModClassNotBest + If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点") + ModClass = ModClass.Replace("ljava/lang/string;", "") + If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte + Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer + '获取 ModID + If _ModId Is Nothing Then + IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1 + IncreaseCount = 0 + Do While Convert.ToInt32(ModClass(IndexHead)) < 32 + IndexHead += 1 + IncreaseCount += 1 + If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败") + Loop + IndexTail = IndexHead + 1 + IncreaseCount = 0 + Do Until Convert.ToInt32(ModClass(IndexTail)) < 32 + IndexTail += 1 + IncreaseCount += 1 + If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败") + Loop + ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead) + End If + '获取 Version + If _Version Is Nothing AndAlso ModClass.Contains("version") Then + IndexHead = ModClass.IndexOf("version") + "version".Length + 1 + IncreaseCount = 0 + Do While Convert.ToInt32(ModClass(IndexHead)) < 32 + IndexHead += 1 + IncreaseCount += 1 + If IncreaseCount > 10 Then GoTo VersionFindFail + Loop + IndexTail = IndexHead + 1 + IncreaseCount = 0 + Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse + (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse + (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c) + IndexTail += 1 + IncreaseCount += 1 + If IncreaseCount > 50 Then GoTo VersionFindFail + Loop + _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead) + End If +VersionFindFail: + '获取 Dependencies + IndexHead = ModClass.IndexOf("dependencies") + If IndexHead > 0 Then + If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299) + Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?") + For Each Token As String In Deps + If Not String.IsNullOrEmpty(Token) Then + If Token.Contains("@") Then + AddDependency(Token.Split("@")(0), Token.Split("@")(1)) + Else + AddDependency(Token) + End If + End If + Next + End If + Catch ex As Exception + Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal) + End Try + IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing + End Sub + +#End Region + + ''' + ''' 是否可能为前置 Mod。 + ''' + Public Function IsPresetMod() As Boolean + Return Dependencies.Count = 0 AndAlso (Name IsNot Nothing) AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib")) + End Function + + ''' + ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。 + ''' + Public Shared Function IsModFile(Path As String) + If Path Is Nothing OrElse Not Path.Contains(".") Then Return False + Path = Path.ToLower + If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse + Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") Then Return True + Return False + End Function + + End Class + + '加载 Mod 列表 + Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad) + Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod))) + Try + RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False) + + '获取 Mod 文件夹下的可用文件列表 + Dim ModFileList As New List(Of FileInfo) + If Directory.Exists(Loader.Input) Then + Dim RawName As String = Loader.Input.ToLower + For Each File As FileInfo In EnumerateFiles(Loader.Input) + If File.DirectoryName.ToLower & "\" <> RawName Then + '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod + If Not (PageVersionLeft.Version IsNot Nothing AndAlso PageVersionLeft.Version.Version.HasForge AndAlso + PageVersionLeft.Version.Version.McCodeMain < 13 AndAlso + File.Directory.Name = "1." & PageVersionLeft.Version.Version.McCodeMain & "." & PageVersionLeft.Version.Version.McCodeSub) Then + Continue For + End If + End If + If McMod.IsModFile(File.FullName) Then ModFileList.Add(File) + Next + End If + + '确定是否显示进度 + Loader.Progress = 0.03 + If ModFileList.Count > 100 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True) + + '加载为 Mod 列表 + Dim ModList As New List(Of McMod) + For Each ModFile As FileInfo In ModFileList + Dim ModEntity As New McMod(ModFile.FullName) + ModEntity.Load() + ModList.Add(ModEntity) + Loader.Progress += 0.9 / ModFileList.Count + Next + Loader.Progress = 0.93 + + '排序 + ModList = Sort(ModList, Function(Left As McMod, Right As McMod) As Boolean + If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then + Return Left.State = McMod.McModState.Unavaliable + Else + Return Not Right.FileName.CompareTo(Left.FileName) + End If + End Function) + Loader.Progress = 0.98 + + '回设 + If Loader.IsAborted Then Exit Sub + Loader.Output = ModList + + Catch ex As Exception + Log(ex, "Mod 列表加载失败", LogLevel.Debug) + Throw + End Try + End Sub + +#If DEBUG Then + ''' + ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。 + ''' + Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String) + Dim Result As New List(Of String) + '令所有 Mod 进行基础检查,并归纳需要检查的 Mod + Dim CurrentModList As New List(Of McMod) + For Each ModEntity In Mods + If Not ModEntity.IsFileAvailable Then + Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path) + Continue For + End If + If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity) + Next + '添加默认依赖 + Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path} + If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"}) + CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"}) + '检查重复的 Mod,并添加对应的依赖 + For Each ModEntity In CurrentModList + For Each PossibleModId In ModEntity.PossibleModId + If CurrentDependencies.ContainsKey(PossibleModId) Then + If CurrentDependencies(PossibleModId)(2) = 1 Then + Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf & + " - " & ModEntity.FileName & vbCrLf & + " - " & CurrentDependencies(PossibleModId)(1)) + Else + Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf & + " - " & ModEntity.FileName & vbCrLf & + " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer) + End If + Else + CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count}) + End If + Next + Next + '检查依赖 + For Each ModEntity In CurrentModList + Try + For Each Dependency In ModEntity.Dependencies + Dim ReqId As String = Dependency.Key + If ReqId.Count < 2 Then Continue For '确保正常 + If ReqId = ModEntity.ModId Then Continue For '跳过自体引用 + If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用 + If Dependency.Value IsNot Nothing Then + '获取分段后的详细版本信息 + Dim ReqVersion As String = Dependency.Value + Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[") + Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]") + Dim ReqVersionHead As String + Dim ReqVersionTail As String + If ReqVersion.Contains(",") Then + ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray()) + ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray()) + Else + ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray()) + ReqVersionTail = ReqVersionHead + If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then + ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1) + ReqVersionTailCanEqual = False + End If + End If + If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1) + If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1) + '获取报错描述文本 + Dim VersionRequire As String + If ReqVersionHead = ReqVersionTail Then + VersionRequire = "应为 " & ReqVersionHead + ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then + VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail + ElseIf ReqVersionHead.Contains(".") Then + If ReqVersionHeadCanEqual Then + VersionRequire = "最低应为 " & ReqVersionHead + Else + VersionRequire = "应高于 " & ReqVersionHead + End If + ElseIf ReqVersionTail.Contains(".") Then + If ReqVersionTailCanEqual Then + VersionRequire = "最高应为 " & ReqVersionHead + Else + VersionRequire = "应低于 " & ReqVersionHead + End If + Else + VersionRequire = "" + End If + '检查前置 Mod 是否存在,并获取其版本 + If Not CurrentDependencies.ContainsKey(ReqId) Then + Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName) + Continue For + End If + Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0") + If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1) + '对比前置 Mod 头部版本 + If ReqVersionHead.Contains(".") Then + If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then + Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf & + " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), "")) + Continue For + End If + End If + '对比前置 Mod 尾部版本 + If ReqVersionTail.Contains(".") Then + If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then + Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf & + " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), "")) + Continue For + End If + End If + Else + If Not CurrentDependencies.ContainsKey(Dependency.Key) Then + Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName) + Continue For + End If + End If + Next + Catch ex As Exception + Result.Add("检查 Mod 时出错:" & GetExceptionSummary(ex) & vbCrLf & " - " & ModEntity.FileName) + Log(ex, "检查 Mod 时出错") + End Try + Next + If Result.Count = 0 Then + Log("[Minecraft] Mod 检查未发现异常") + Else + Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf)) + End If + Return Result + End Function +#End If + +End Module diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb index 6d9ae58..aeefbe9 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb @@ -254,17 +254,21 @@ Retry: For Each ModJson In Task.Input '跳过重复的 Mod(疑似 CurseForge Bug) If FileList.ContainsKey(ModJson("id").ToObject(Of Integer)) Then Continue For - '根据 modules 判断是资源包还是 Mod + '建立 CompFile + Dim File As New CompFile(ModJson, CompType.Mod) + If Not File.Available Then Continue For + '根据 modules 和文件名后缀判断是资源包还是 Mod Dim IsResourcePack As Boolean If ModJson("modules").Any Then 'modules 可能返回 null(#1006) Dim ModuleNames = CType(ModJson("modules"), JArray).Select(Function(l) l("name").ToString).ToList - IsResourcePack = Not ModuleNames.Contains("META-INF") AndAlso ModuleNames.Contains("pack.mcmeta") + IsResourcePack = + (Not ModuleNames.Contains("META-INF")) AndAlso (Not ModuleNames.Contains("mcmod.info")) AndAlso '不包含 META-INF 或 mcmod.info + ModuleNames.Contains("pack.mcmeta") AndAlso '包含 pack.mcmeta + (Not File.FileName.EndsWith(".jar")) '文件后缀不是 .jar Else IsResourcePack = False End If '实际的添加 - Dim File As New CompFile(ModJson, CompType.Mod) - If Not File.Available Then Continue For FileList.Add(ModJson("id"), File.ToNetFile($"{PathMcFolder}versions\{VersionName}\{If(IsResourcePack, "resourcepacks", "mods")}\")) Task.Progress += 1 / (1 + ModFileList.Count) @@ -278,6 +282,7 @@ Retry: '构造加载器 Dim Request As New McInstallRequest With { .TargetVersionName = VersionName, + .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\", .MinecraftName = Json("minecraft")("version").ToString, .ForgeVersion = ForgeVersion, .FabricVersion = FabricVersion @@ -333,7 +338,7 @@ Retry: '启动 Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState} - Loader.Start(PathMcFolder & "versions\" & VersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() If ShowRibble Then FrmMain.BtnExtraDownload.Ribble() @@ -431,6 +436,7 @@ Retry: '构造加载器 Dim Request As New McInstallRequest With { .TargetVersionName = VersionName, + .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\", .MinecraftName = MinecraftVersion, .ForgeVersion = ForgeVersion, .FabricVersion = FabricVersion @@ -486,7 +492,7 @@ Retry: '启动 Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState} - Loader.Start(PathMcFolder & "versions\" & VersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() If ShowRibble Then FrmMain.BtnExtraDownload.Ribble() @@ -533,6 +539,7 @@ Retry: End If Dim Request As New McInstallRequest With { .TargetVersionName = VersionName, + .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\", .MinecraftName = Json("gameVersion").ToString } Dim InstallExpectTime As Double = 0 @@ -584,7 +591,7 @@ Retry: '启动 Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState} 'If Archive IsNot Nothing Then Archive.Dispose() '解除占用,以免在加载器中触发 “正由另一进程使用,因此该进程无法访问此文件” - Loader.Start(PathMcFolder & "versions\" & VersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() FrmMain.BtnExtraDownload.Ribble() @@ -629,7 +636,7 @@ Retry: Hint("该整合包未提供游戏版本信息,无法安装!", HintType.Critical) Exit Sub End If - Dim Request As New McInstallRequest With {.TargetVersionName = VersionName} + Dim Request As New McInstallRequest With {.TargetVersionName = VersionName, .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\"} For Each Component In PackJson("components") Select Case If(Component("uid"), "").ToString Case "org.lwjgl" @@ -679,7 +686,7 @@ Retry: '启动 Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState} - Loader.Start(PathMcFolder & "versions\" & VersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() FrmMain.BtnExtraDownload.Ribble() @@ -738,6 +745,7 @@ Retry: End If Dim Request As New McInstallRequest With { .TargetVersionName = VersionName, + .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\", .MinecraftName = Addons("game"), .OptiFineVersion = If(Addons.ContainsKey("optifine"), Addons("optifine"), Nothing), .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), Nothing), @@ -777,7 +785,7 @@ Retry: '启动 Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState} 'If Archive IsNot Nothing Then Archive.Dispose() '解除占用,以免在加载器中触发 “正由另一进程使用,因此该进程无法访问此文件” - Loader.Start(PathMcFolder & "versions\" & VersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() FrmMain.BtnExtraDownload.Ribble() diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb index 7543692..5059754 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb @@ -116,7 +116,7 @@ Get Return _State End Get - Set(ByVal value As MinecraftState) + Set(value As MinecraftState) If _State = value Then Exit Property _State = value WatcherStateChanged() diff --git a/Plain Craft Launcher 2/Modules/ModEvent.vb b/Plain Craft Launcher 2/Modules/ModEvent.vb index dea939e..51942e9 100644 --- a/Plain Craft Launcher 2/Modules/ModEvent.vb +++ b/Plain Craft Launcher 2/Modules/ModEvent.vb @@ -13,11 +13,11 @@ Case "打开网页" Data(0) = Data(0).Replace("\", "/") - If Not (Data(0).StartsWith("http://") OrElse Data(0).StartsWith("https://")) Then - MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败") + If Not Data(0).Contains("://") OrElse Data(0).ToLower.StartsWith("file") Then '为了支持更多协议(#2200) + MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败") Exit Sub End If - Hint("正在打开网页,请稍候……") + Hint("正在开启中,请稍候……") OpenWebsite(Data(0)) Case "打开文件", "打开帮助" @@ -65,15 +65,21 @@ RunInThread(Sub() PageOtherTest.MemoryOptimize(True)) Case "清理垃圾" - PageOtherTest.RubbishClear() + RunInThread(Sub() PageOtherTest.RubbishClear()) Case "弹出窗口" MyMsgBox(Data(1).Replace("\n", vbCrLf), Data(0).Replace("\n", vbCrLf)) + Case "切换页面" + FrmMain.PageChange(Val(Data(0)), Val(Data(1))) + + Case "导入整合包" + RunInThread(Sub() ModpackInstall()) + Case "下载文件" Data(0) = Data(0).Replace("\", "/") If Not (Data(0).StartsWith("http://") OrElse Data(0).StartsWith("https://")) Then - MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的协议。", "事件执行失败") + MyMsgBox("EventData 必须为以 http:// 或 https:// 开头的网址。" & vbCrLf & "PCL 不支持其他乱七八糟的下载协议。", "事件执行失败") Exit Sub End If PageOtherTest.StartCustomDownload(Data(0), GetFileNameFromPath(Data(0))) diff --git a/Plain Craft Launcher 2/Modules/ModMusic.vb b/Plain Craft Launcher 2/Modules/ModMusic.vb index 1a054a7..f973d7f 100644 --- a/Plain Craft Launcher 2/Modules/ModMusic.vb +++ b/Plain Craft Launcher 2/Modules/ModMusic.vb @@ -25,7 +25,7 @@ For Each File In My.Computer.FileSystem.GetFiles(Path & "PCL\Musics\", FileIO.SearchOption.SearchAllSubDirectories, "*.*") '文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的 Dim Extend As String = File.Split(".").Last.ToLower - If Not (Extend = "ini" OrElse Extend = "jpg" OrElse Extend = "txt" OrElse Extend = "cfg" OrElse Extend = "png") Then + If Not (Extend = "ini" OrElse Extend = "jpg" OrElse Extend = "txt" OrElse Extend = "cfg" OrElse Extend = "lrc" OrElse Extend = "png") Then MusicAllList.Add(File) End If Next @@ -277,7 +277,7 @@ Log(ex, "播放音乐失败(" & MusicCurrent & ")", LogLevel.Developer) If MusicAllList.Count > 1 Then Thread.Sleep(1000) - MusicStartPlay(DequeueNextMusicAddress) + MusicStartPlay(DequeueNextMusicAddress, IsFirstLoad) End If Finally If CurrentWave IsNot Nothing Then CurrentWave.Dispose() diff --git a/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb b/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb index 17ea092..1f5ea45 100644 --- a/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb +++ b/Plain Craft Launcher 2/Modules/Reference/CookieWebClient.vb @@ -25,7 +25,7 @@ Public Class CookieWebClient ''' Public Timeout As Integer = 600000 - Protected Overrides Function GetWebRequest(ByVal address As Uri) As WebRequest + Protected Overrides Function GetWebRequest(address As Uri) As WebRequest Dim r As WebRequest = MyBase.GetWebRequest(address) Dim request = TryCast(r, HttpWebRequest) @@ -37,19 +37,19 @@ Public Class CookieWebClient Return r End Function - Protected Overrides Function GetWebResponse(ByVal request As WebRequest, ByVal result As IAsyncResult) As WebResponse + Protected Overrides Function GetWebResponse(request As WebRequest, result As IAsyncResult) As WebResponse Dim response As WebResponse = MyBase.GetWebResponse(request, result) ReadCookies(response) Return response End Function - Protected Overrides Function GetWebResponse(ByVal request As WebRequest) As WebResponse + Protected Overrides Function GetWebResponse(request As WebRequest) As WebResponse Dim response As WebResponse = MyBase.GetWebResponse(request) ReadCookies(response) Return response End Function - Private Sub ReadCookies(ByVal r As WebResponse) + Private Sub ReadCookies(r As WebResponse) Dim response = TryCast(r, HttpWebResponse) If response IsNot Nothing Then Dim cookies As CookieCollection = response.Cookies diff --git a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb index 861ab2e..3e3c0d8 100644 --- a/Plain Craft Launcher 2/My Project/AssemblyInfo.vb +++ b/Plain Craft Launcher 2/My Project/AssemblyInfo.vb @@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 ' 方法是按如下所示使用“*” - - + + diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb index 8730346..6b50e8f 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb @@ -386,7 +386,7 @@ Public Module ModDownloadLib '选择 Java Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0)) If Java Is Nothing Then - If Not JavaDownloadConfirm(17) Then Throw New Exception("由于未找到 Java,已取消安装。") + If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。") '开始自动下载 Dim JavaLoader = JavaFixLoaders(17) JavaLoader.Start(17, IsForceRestart:=True) @@ -489,7 +489,7 @@ Public Module ModDownloadLib ''' ''' 获取下载某个 OptiFine 版本的加载器列表。 ''' - Private Function McDownloadOptiFineLoader(DownloadInfo As DlOptiFineListEntry, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase) + Private Function McDownloadOptiFineLoader(DownloadInfo As DlOptiFineListEntry, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional ClientFolder As String = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase) '参数初始化 McFolder = If(McFolder, PathMcFolder) @@ -553,14 +553,14 @@ Public Module ModDownloadLib '拷贝原版文件 If Not IsCustomFolder Then Exit Sub SyncLock VanillaSyncLock - Dim ClientName As String = New DirectoryInfo(ClientDownloadLoader.Input).Name '#2031 报告了此处 ClientDownloadLoader.Input 为 null 的问题,但无法复现 + Dim ClientName As String = GetFolderNameFromPath(ClientFolder) Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit) If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then - CopyFile(ClientDownloadLoader.Input & ClientName & ".json", + CopyFile(ClientFolder & ClientName & ".json", McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") End If If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") Then - CopyFile(ClientDownloadLoader.Input & ClientName & ".jar", + CopyFile(ClientFolder & ClientName & ".jar", McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") End If End SyncLock @@ -1114,7 +1114,7 @@ Public Module ModDownloadLib '选择 Java Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0)) If Java Is Nothing Then - If Not JavaDownloadConfirm(17) Then Throw New Exception("由于未找到 Java,已取消安装。") + If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。") '开始自动下载 Dim JavaLoader = JavaFixLoaders(17) JavaLoader.Start(17, IsForceRestart:=True) @@ -1259,7 +1259,7 @@ Public Module ModDownloadLib ''' ''' 获取下载某个 Forge 版本的加载器列表。 ''' - Private Function McDownloadForgeLoader(Version As String, Inherit As String, Optional DownloadInfo As DlForgeVersionEntry = Nothing, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase) + Private Function McDownloadForgeLoader(Version As String, Inherit As String, Optional DownloadInfo As DlForgeVersionEntry = Nothing, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional ClientFolder As String = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase) '参数初始化 McFolder = If(McFolder, PathMcFolder) @@ -1386,13 +1386,13 @@ Public Module ModDownloadLib '拷贝原版文件 If Not IsCustomFolder Then Exit Sub SyncLock VanillaSyncLock - Dim ClientName As String = New DirectoryInfo(ClientDownloadLoader.Input).Name + Dim ClientName As String = GetFolderNameFromPath(ClientFolder) Directory.CreateDirectory(McFolder & "versions\" & Inherit) If Not File.Exists(McFolder & "versions\" & Inherit & "\" & Inherit & ".json") Then - CopyFile(ClientDownloadLoader.Input & ClientName & ".json", McFolder & "versions\" & Inherit & "\" & Inherit & ".json") + CopyFile(ClientFolder & ClientName & ".json", McFolder & "versions\" & Inherit & "\" & Inherit & ".json") End If If Not File.Exists(McFolder & "versions\" & Inherit & "\" & Inherit & ".jar") Then - CopyFile(ClientDownloadLoader.Input & ClientName & ".jar", McFolder & "versions\" & Inherit & "\" & Inherit & ".jar") + CopyFile(ClientFolder & ClientName & ".jar", McFolder & "versions\" & Inherit & "\" & Inherit & ".jar") End If End SyncLock #End Region @@ -1797,6 +1797,10 @@ Public Module ModDownloadLib ''' 必填。安装目标版本名称。 ''' Public TargetVersionName As String + ''' + ''' 必填。安装目标文件夹。 + ''' + Public TargetVersionFolder As String ''' ''' 必填。欲下载的 Minecraft 的版本名。 @@ -1909,7 +1913,7 @@ Public Module ModDownloadLib Dim Loader As New LoaderCombo(Of String)(Request.TargetVersionName & " 安装", SubLoaders) With {.OnStateChanged = AddressOf McInstallState} '启动 - Loader.Start(PathMcFolder & "versions\" & Request.TargetVersionName & "\") + Loader.Start(Request.TargetVersionFolder) LoaderTaskbarAdd(Loader) FrmMain.BtnExtraDownload.ShowRefresh() FrmMain.BtnExtraDownload.Ribble() @@ -2009,28 +2013,29 @@ Public Module ModDownloadLib If OptiFineAsMod Then LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & Request.OptiFineEntry.NameDisplay, McDownloadOptiFineSaveLoader(Request.OptiFineEntry, OptiFineFolder & Request.OptiFineEntry.NameFile)) With {.Show = False, .ProgressWeight = 16, .Block = Request.ForgeVersion Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}) Else - LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & Request.OptiFineEntry.NameDisplay, McDownloadOptiFineLoader(Request.OptiFineEntry, TempMcFolder, ClientLoader, False)) With {.Show = False, .ProgressWeight = 24, .Block = Request.ForgeVersion Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}) + LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & Request.OptiFineEntry.NameDisplay, McDownloadOptiFineLoader(Request.OptiFineEntry, TempMcFolder, ClientLoader, Request.TargetVersionFolder, False)) With {.Show = False, .ProgressWeight = 24, .Block = Request.ForgeVersion Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}) End If End If 'Forge If Request.ForgeVersion IsNot Nothing Then - LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & Request.ForgeVersion, McDownloadForgeLoader(Request.ForgeVersion, Request.MinecraftName, Request.ForgeEntry, TempMcFolder, ClientLoader, False)) With {.Show = False, .ProgressWeight = 25, .Block = Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}) + LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & Request.ForgeVersion, McDownloadForgeLoader(Request.ForgeVersion, Request.MinecraftName, Request.ForgeEntry, TempMcFolder, ClientLoader, Request.TargetVersionFolder, False)) With {.Show = False, .ProgressWeight = 25, .Block = Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}) End If 'LiteLoader If Request.LiteLoaderEntry IsNot Nothing Then LoaderList.Add(New LoaderCombo(Of String)("下载 LiteLoader " & Request.MinecraftName, McDownloadLiteLoaderLoader(Request.LiteLoaderEntry, TempMcFolder, ClientLoader, False)) With {.Show = False, .ProgressWeight = 1, .Block = Request.FabricVersion Is Nothing}) End If - 'Fabric Loader + 'Fabric If Request.FabricVersion IsNot Nothing Then LoaderList.Add(New LoaderCombo(Of String)("下载 Fabric " & Request.FabricVersion, McDownloadFabricLoader(Request.FabricVersion, Request.MinecraftName, TempMcFolder, False)) With {.Show = False, .ProgressWeight = 2, .Block = True}) End If '合并安装 - LoaderList.Add(New LoaderTask(Of String, String)("安装游戏", Sub(Task As LoaderTask(Of String, String)) - InstallMerge(OutputFolder, OutputFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, FabricFolder, LiteLoaderFolder) - Task.Progress = 0.3 - If Directory.Exists(TempMcFolder & "libraries") Then CopyDirectory(TempMcFolder & "libraries", PathMcFolder & "libraries") - If Directory.Exists(TempMcFolder & "mods") Then CopyDirectory(TempMcFolder & "mods", PathMcFolder & "mods") - End Sub) With {.ProgressWeight = 2, .Block = True}) + LoaderList.Add(New LoaderTask(Of String, String)("安装游戏", + Sub(Task As LoaderTask(Of String, String)) + InstallMerge(OutputFolder, OutputFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, FabricFolder, LiteLoaderFolder) + Task.Progress = 0.3 + If Directory.Exists(TempMcFolder & "libraries") Then CopyDirectory(TempMcFolder & "libraries", PathMcFolder & "libraries") + If Directory.Exists(TempMcFolder & "mods") Then CopyDirectory(TempMcFolder & "mods", PathMcFolder & "mods") + End Sub) With {.ProgressWeight = 2, .Block = True}) '补全文件 If Not DontFixLibraries AndAlso (Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.Split(".")(0) >= 20) OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing) Then diff --git a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb index d4034a4..01e94ba 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb @@ -945,11 +945,16 @@ ''' 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。 ''' Private Function LoadFabricApiGetError() As String - If SelectedFabric Is Nothing Then Return "需要安装 Fabric" If LoadFabricApi.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……" If LoadFabricApi.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadFabricApi.State, Object).Error.Message + If DlFabricApiLoader.Output Is Nothing Then + If SelectedFabric Is Nothing Then Return "需要安装 Fabric" + Return "正在获取版本列表……" + End If For Each Version In DlFabricApiLoader.Output - If IsSuitableFabricApi(Version.DisplayName, SelectedMinecraftId) Then Return Nothing + If Not IsSuitableFabricApi(Version.DisplayName, SelectedMinecraftId) Then Continue For + If SelectedFabric Is Nothing Then Return "需要安装 Fabric" + Return Nothing Next Return "没有可用版本" End Function @@ -1032,14 +1037,21 @@ ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。 ''' Private Function LoadOptiFabricGetError() As String - If SelectedFabric Is Nothing AndAlso SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine 与 Fabric" - If SelectedFabric Is Nothing Then Return "需要安装 Fabric" - If SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine" If SelectedMinecraftId.StartsWith("1.14") OrElse SelectedMinecraftId.StartsWith("1.15") Then Return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins" If LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……" If LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadOptiFabric.State, Object).Error.Message + If DlOptiFabricLoader.Output Is Nothing Then + If SelectedFabric Is Nothing AndAlso SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine 与 Fabric" + If SelectedFabric Is Nothing Then Return "需要安装 Fabric" + If SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine" + Return "正在获取版本列表……" + End If For Each Version In DlOptiFabricLoader.Output - If IsSuitableOptiFabric(Version, SelectedMinecraftId) Then Return Nothing + If Not IsSuitableOptiFabric(Version, SelectedMinecraftId) Then Continue For '2135# + If SelectedFabric Is Nothing AndAlso SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine 与 Fabric" + If SelectedFabric Is Nothing Then Return "需要安装 Fabric" + If SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine" + Return Nothing '通过检查 Next Return "没有可用版本" End Function @@ -1114,6 +1126,7 @@ '提交安装申请 Dim Request As New McInstallRequest With { .TargetVersionName = TextSelectName.Text, + .TargetVersionFolder = $"{PathMcFolder}versions\{TextSelectName.Text}\", .MinecraftJson = SelectedMinecraftJsonUrl, .MinecraftName = SelectedMinecraftId, .OptiFineEntry = SelectedOptiFine, diff --git a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb index 6cbaffa..5f1849a 100644 --- a/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb @@ -5,7 +5,7 @@ '皮肤储存 Private _Address As String - Public Property Address() As String + Public Property Address As String Get Return _Address End Get @@ -76,12 +76,19 @@ ''' Public Sub Load() Try - Address = Loader.Output '检查文件存在 - If Address = "" Then Throw New Exception("皮肤加载器 " & Loader.Name & " 没有输出") + Address = Loader.Output + If String.IsNullOrEmpty(Address) Then Throw New Exception("皮肤加载器 " & Loader.Name & " 没有输出") If Not Address.StartsWith(PathImage) AndAlso Not File.Exists(Address) Then Throw New FileNotFoundException("皮肤文件未找到", Address) '加载 - Dim Image As New MyBitmap(Address) + Dim Image As MyBitmap + Try + Image = New MyBitmap(Address) + Catch ex As Exception '#2272 + Log(ex, $"皮肤文件已损坏:{Address}", LogLevel.Hint) + File.Delete(Address) + Exit Sub + End Try ImgBack.Tag = Address '大小检查 Dim Scale As Integer = Image.Pic.Width / 64 diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb index ed51f71..eec5ebe 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkHiper.xaml.vb @@ -17,6 +17,7 @@ Private IsLoad As Boolean = False Private Sub OnLoaded() Handles Me.Loaded + FormMain.EndProgramForce(Result.Aborted) If IsLoad Then Exit Sub IsLoad = True '启动监视线程 diff --git a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb index 282319f..85ee5c3 100644 --- a/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb @@ -11,6 +11,7 @@ Public Class PageLinkIoi '页面初始化 Private IsLoad As Boolean = False Private Sub MeLoaded() Handles Me.Loaded + FormMain.EndProgramForce(Result.Aborted) '重复加载部分 PanBack.ScrollToHome() diff --git a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb index f5b8103..b3874fe 100644 --- a/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb @@ -187,6 +187,7 @@ End Try FrmSpeedRight.PanMain.Children.Insert(0, Card) RightCards.Add(Loader.Name, Card) + Log($"[Watcher] 新建下载管理卡片:{Loader.Name}") '添加取消按钮 Dim Cancel As New MyIconButton With {.Name = "BtnCancel", .Logo = "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", .Height = 20, .Margin = New Thickness(0, 10, 10, 0), .LogoScale = 1.1, .HorizontalAlignment = HorizontalAlignment.Right, .VerticalAlignment = VerticalAlignment.Top} Card.Children.Add(Cancel) @@ -194,6 +195,8 @@ AniDispose(sender, False) AniDispose(Card, True, Sub() If FrmSpeedRight.PanMain.Children.Count = 0 AndAlso FrmMain.PageCurrent = FormMain.PageType.DownloadManager Then FrmMain.PageBack()) RightCards.Remove(Loader.Name) + LoaderTaskbar.Remove(Loader) + Log($"[Watcher] 关闭下载管理卡片:{Loader.Name}") RunInThread(Sub() Loader.Abort()) End Sub '如果已经失败,再刷新一次,修改成失败的控件 @@ -217,6 +220,7 @@ Dim Card As Grid = RightCards(Loader.Name) FrmSpeedRight.PanMain.Children.Remove(Card) RightCards.Remove(Loader.Name) + Log($"[Watcher] 移除下载管理卡片:{Loader.Name}") End Sub) End If End Sub diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml index 33bc4e1..2df274d 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml @@ -7,22 +7,20 @@ - + - + - - - - + + @@ -47,5 +45,22 @@ + + + + + + + + + + + + diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb index 99667ca..bbd3318 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb @@ -1,4 +1,7 @@ -Public Class PageVersionMod +Imports System.Security.Principal +Imports PCL.MyLoading + +Public Class PageVersionMod #Region "初始化" @@ -6,7 +9,11 @@ Public Sub PageOther_Loaded() Handles Me.Loaded PanBack.ScrollToHome() + AniControlEnabled += 1 + SelectedMods.Clear() RefreshList() + ChangeAllSelected(False) + AniControlEnabled -= 1 '非重复加载部分 If IsLoad Then Exit Sub @@ -42,6 +49,7 @@ #Region "UI 化" + Private PanItems As StackPanel ''' ''' 将 Mod 列表加载为 UI。 ''' @@ -62,13 +70,13 @@ SearchBox.Text = "" '建立 StackPanel - Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, If(List.Count > 0, 20, 0)), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)} + PanItems = New StackPanel With {.Margin = New Thickness(18, MyCard.SwapedHeight, 18, If(List.Count > 0, 20, 0)), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)} For Each ModEntity As McMod In List - NewStack.Children.Add(McModListItem(ModEntity)) + PanItems.Children.Add(McModListItem(ModEntity)) Next '建立 MyCard Dim NewCard As New MyCard With {.Title = McModGetTitle(List), .Margin = New Thickness(0, 0, 0, 15)} - NewCard.Children.Add(NewStack) + NewCard.Children.Add(PanItems) PanList.Children.Add(NewCard) '显示提示 If List.Count > 0 AndAlso Not Setup.Get("HintModDisable") Then @@ -88,20 +96,12 @@ For Each ModEntity As McMod In List Counter(ModEntity.State) += 1 Next - If List.Count = 0 Then - '全空 - Return "未找到任何 Mod" - ElseIf Counter(McMod.McModState.Disabled) = 0 AndAlso Counter(McMod.McModState.Unavaliable) = 0 Then - '只有启用的 Mod - Return "Mod 列表(" & Counter(McMod.McModState.Fine) & ")" - Else - '混合种类 - Dim TypeList As New List(Of String) - If Counter(McMod.McModState.Fine) > 0 Then TypeList.Add("启用 " & Counter(McMod.McModState.Fine)) - If Counter(McMod.McModState.Disabled) > 0 Then TypeList.Add("禁用 " & Counter(McMod.McModState.Disabled)) - If Counter(McMod.McModState.Unavaliable) > 0 Then TypeList.Add("错误 " & Counter(McMod.McModState.Unavaliable)) - Return "Mod 列表(" & Join(TypeList, ",") & ")" - End If + If List.Count = 0 Then Return "未找到任何 Mod" + Dim TypeList As New List(Of String) + If Counter(McMod.McModState.Fine) > 0 Then TypeList.Add("启用 " & Counter(McMod.McModState.Fine)) + If Counter(McMod.McModState.Disabled) > 0 Then TypeList.Add("禁用 " & Counter(McMod.McModState.Disabled)) + If Counter(McMod.McModState.Unavaliable) > 0 Then TypeList.Add("错误 " & Counter(McMod.McModState.Unavaliable)) + Return "Mod 列表(" & Join(TypeList, ",") & ")" End Function Private Function McModListItem(Entry As McMod) As MyListItem '图标 @@ -138,16 +138,21 @@ ' Desc += " : " & Join(DepList, ", ") 'End If '实例化 - Dim NewItem As New MyListItem With {.Logo = Logo, .SnapsToDevicePixels = True, .Title = Title, .Info = Desc, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry} - NewItem.PaddingRight = If(Entry.IsFileAvailable, 73, 55) - '事件 - NewItem.ContentHandler = AddressOf McModContent + AniControlEnabled += 1 + Dim NewItem As New MyListItem With { + .LogoClickable = True, .Logo = Logo, .SnapsToDevicePixels = True, .Title = Title, .Info = Desc, .Height = 42, .Tag = Entry, + .Type = MyListItem.CheckType.CheckBox, .IsScaleAnimationEnabled = False, + .PaddingRight = 73, + .ContentHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.RawFileName) + } + AniControlEnabled -= 1 Return NewItem End Function Private Sub McModContent(sender As MyListItem, e As EventArgs) Dim ModEntity As McMod = sender.Tag '注册点击事件 - AddHandler sender.Click, AddressOf Item_Click + AddHandler sender.Changed, AddressOf CheckChanged + AddHandler sender.LogoClick, AddressOf ItemLogo_Click '图标按钮 Dim BtnDel As New MyIconButton With {.LogoScale = 1.1, .Logo = Logo.IconButtonDelete, .Tag = sender} BtnDel.ToolTip = "删除" @@ -161,18 +166,14 @@ ToolTipService.SetVerticalOffset(BtnOpen, 30) ToolTipService.SetHorizontalOffset(BtnOpen, 2) AddHandler BtnOpen.Click, AddressOf Open_Click - If ModEntity.IsFileAvailable Then - Dim BtnCont As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonInfo, .Tag = sender} - BtnCont.ToolTip = "详情" - ToolTipService.SetPlacement(BtnCont, Primitives.PlacementMode.Center) - ToolTipService.SetVerticalOffset(BtnCont, 30) - ToolTipService.SetHorizontalOffset(BtnCont, 2) - AddHandler BtnCont.Click, AddressOf Info_Click - AddHandler sender.MouseRightButtonDown, AddressOf Info_Click - sender.Buttons = {BtnCont, BtnOpen, BtnDel} - Else - sender.Buttons = {BtnOpen, BtnDel} - End If + Dim BtnCont As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonInfo, .Tag = sender} + BtnCont.ToolTip = "详情" + ToolTipService.SetPlacement(BtnCont, Primitives.PlacementMode.Center) + ToolTipService.SetVerticalOffset(BtnCont, 30) + ToolTipService.SetHorizontalOffset(BtnCont, 2) + AddHandler BtnCont.Click, AddressOf Info_Click + AddHandler sender.MouseRightButtonDown, AddressOf Info_Click + sender.Buttons = {BtnCont, BtnOpen, BtnDel} End Sub #End Region @@ -210,13 +211,144 @@ #End If ''' - ''' 一键启用 / 禁用。 + ''' 全选。 ''' - Private Sub BtnManageChange_Click(sender As MyButton, e As EventArgs) Handles BtnManageEnabled.Click, BtnManageDisabled.Click + Private Sub BtnManageSelectAll_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageSelectAll.Click + ChangeAllSelected(True) + End Sub + +#End Region + +#Region "选择" + + '选择的 Mod 的路径(不含 .disabled) + Public SelectedMods As New List(Of String) + + '单项切换选择状态 + Public Sub CheckChanged(sender As MyListItem, e As RouteEventArgs) + If AniControlEnabled <> 0 Then Return + '更新选择了的内容 + Dim SelectedKey As String = CType(sender.Tag, McMod).RawFileName + If sender.Checked Then + If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey) + Else + SelectedMods.Remove(SelectedKey) + End If + '更新下边栏 UI + RefreshBottomBar() + End Sub + + '改变下边栏状态 + Private ShownCount As Integer = 0 + Private Sub RefreshBottomBar() + '计数 + Dim NewCount As Integer = SelectedMods.Count + Dim Selected = NewCount > 0 + If Selected Then LabSelect.Text = $"已选择 {NewCount} 个文件" '取消所有选择时不更新数字 + '按钮可用性 + If Selected Then + Dim HasEnabled As Boolean = False + Dim HasDisabled As Boolean = False + For Each ModEntity In McModLoader.Output + If SelectedMods.Contains(ModEntity.RawFileName) Then + If ModEntity.State = McMod.McModState.Fine Then + HasEnabled = True + ElseIf ModEntity.State = McMod.McModState.Disabled Then + HasDisabled = True + End If + End If + Next + BtnSelectDisable.IsEnabled = HasEnabled + BtnSelectEnable.IsEnabled = HasDisabled + End If + '更新显示状态 + CardSelect.IsHitTestVisible = Selected + If AniControlEnabled = 0 Then + If Selected Then + '仅在数量增加时播放出现/跳跃动画 + If ShownCount >= NewCount Then + ShownCount = NewCount + Return + Else + ShownCount = NewCount + End If + '出现/跳跃动画 + CardSelect.Visibility = Visibility.Visible + AniStart({ + AaOpacity(CardSelect, 1 - CardSelect.Opacity, 60), + AaTranslateY(CardSelect, -27 - TransSelect.Y, 120, Ease:=New AniEaseOutFluent(AniEasePower.Weak)), + AaTranslateY(CardSelect, 3, 150, 120, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)), + AaTranslateY(CardSelect, -1, 90, 270, Ease:=New AniEaseInoutFluent(AniEasePower.Weak)) + }, "Mod Sidebar") + Else + '不重复播放隐藏动画 + If ShownCount = 0 Then Return + ShownCount = 0 + '隐藏动画 + AniStart({ + AaOpacity(CardSelect, -CardSelect.Opacity, 90), + AaTranslateY(CardSelect, -10 - TransSelect.Y, 90, Ease:=New AniEaseInFluent(AniEasePower.Weak)), + AaCode(Sub() CardSelect.Visibility = Visibility.Collapsed, After:=True) + }, "Mod Sidebar") + End If + Else + AniStop("Mod Sidebar") + ShownCount = NewCount + If Selected Then + CardSelect.Visibility = Visibility.Visible + CardSelect.Opacity = 1 + TransSelect.Y = -25 + Else + CardSelect.Visibility = Visibility.Collapsed + CardSelect.Opacity = 0 + TransSelect.Y = -10 + End If + End If + End Sub + + '切换所有项的选择状态 + Private Sub ChangeAllSelected(Value As Boolean) + AniControlEnabled += 1 + SelectedMods.Clear() + If IsSearching Then + '搜索中 + For Each Item As MyListItem In PanSearchList.Children + Item.Checked = Value + If Value Then SelectedMods.Add(CType(Item.Tag, McMod).RawFileName) + Next + If Not Value Then '只取消选择 + If PanItems IsNot Nothing Then + For Each Item As MyListItem In PanItems.Children + Item.Checked = Value + Next + End If + End If + Else + '非搜索中 + If PanItems IsNot Nothing Then + For Each Item As MyListItem In PanItems.Children + Item.Checked = Value + If Value Then SelectedMods.Add(CType(Item.Tag, McMod).RawFileName) + Next + End If + End If + AniControlEnabled -= 1 + '更新下边栏 UI + RefreshBottomBar() + End Sub + Private Sub Load_State(sender As Object, newState As MyLoadingState, oldState As MyLoadingState) Handles Load.StateChanged + ChangeAllSelected(False) + End Sub + +#End Region + +#Region "下边栏" + + '启用 / 禁用 + Private Sub BtnSelectEorD_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click Dim IsSuccessful As Boolean = True - Dim SuccessfulCount As Integer = 0 - Dim IsDisable As Boolean = sender.Text.Contains("禁用") - For Each ModEntity In McModLoader.Output + Dim IsDisable As Boolean = sender.Equals(BtnSelectDisable) + For Each ModEntity In McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)).ToList() Dim NewPath As String = Nothing If ModEntity.State = McMod.McModState.Fine And IsDisable Then '禁用 @@ -225,7 +357,6 @@ '启用 NewPath = ModEntity.Path.Substring(0, ModEntity.Path.Count - ".disabled".Count) Else - SuccessfulCount += 1 Continue For End If '重命名 @@ -233,27 +364,99 @@ If File.Exists(NewPath) AndAlso Not File.Exists(ModEntity.Path) Then Continue For '因为未知原因 Mod 的状态已经切换完了 File.Delete(NewPath) FileSystem.Rename(ModEntity.Path, NewPath) - SuccessfulCount += 1 Catch ex As FileNotFoundException - Log(ex, "未找到需要重命名的 Mod(" & If(ex.FileName, "null") & ")", LogLevel.Feedback) - RefreshList() - Exit Sub + Log(ex, $"未找到需要重命名的 Mod({If(ModEntity.Path, "null")})", LogLevel.Feedback) + RefreshList(True) + Return Catch ex As Exception - Log(ex, "全局状态改变中重命名 Mod 失败") + Log(ex, $"批量重命名 Mod 失败({If(ModEntity.Path, "null")})") IsSuccessful = False End Try + '更改 Loader 和 UI 中的列表 + Dim NewModEntity As New McMod(NewPath) + Dim IndexOfLoader As Integer = McModLoader.Output.IndexOf(ModEntity) + McModLoader.Output.RemoveAt(IndexOfLoader) + McModLoader.Output.Insert(IndexOfLoader, NewModEntity) + Dim Parent As StackPanel = If(IsSearching, PanSearchList, PanItems) + Dim IndexOfUi As Integer = Parent.Children.IndexOf(Parent.Children.OfType(Of MyListItem).First(Function(i) CType(i.Tag, McMod) Is ModEntity)) + Parent.Children.RemoveAt(IndexOfUi) + Parent.Children.Insert(IndexOfUi, McModListItem(NewModEntity)) Next - If Not IsSuccessful Then Hint("由于文件被占用,部分 Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical) - Log("[System] 已确认 " & SuccessfulCount & "/" & McModLoader.Output.Count & " 个 Mod 的状态与要求一致") - RefreshList() + If Not IsSearching Then + '改变禁用数量的显示 + CType(PanItems.Parent, MyCard).Title = McModGetTitle(McModLoader.Output) + '更新加载器状态 + LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", LoaderFolderRunType.UpdateOnly) + End If + If Not IsSuccessful Then + Hint("由于文件被占用,部分 Mod 的状态切换失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical) + RefreshList(True) + Else + ChangeAllSelected(False) + End If + End Sub + + '删除 + Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click + Dim IsSuccessful As Boolean = True + Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown + Dim DeleteList As List(Of McMod) = McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)).ToList() + For Each ModEntity In DeleteList + '删除 + Try + If IsShiftPressed Then + File.Delete(ModEntity.Path) + Else + My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + End If + Catch ex As OperationCanceledException + Log(ex, "批量删除 Mod 被主动取消") + RefreshList(True) + Return + Catch ex As Exception + Log(ex, $"批量删除 Mod 失败({ModEntity.Path})", LogLevel.Msgbox) + IsSuccessful = False + End Try + '更改 Loader 和 UI 中的列表 + McModLoader.Output.Remove(ModEntity) + Dim Parent As StackPanel = If(IsSearching, PanSearchList, PanItems) + Dim IndexOfUi As Integer = Parent.Children.IndexOf(Parent.Children.OfType(Of MyListItem).First(Function(i) CType(i.Tag, McMod) Is ModEntity)) + Parent.Children.RemoveAt(IndexOfUi) + Next + If Not IsSearching Then + '改变禁用数量的显示 + CType(PanItems.Parent, MyCard).Title = McModGetTitle(McModLoader.Output) + '更新加载器状态 + LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", LoaderFolderRunType.UpdateOnly) + End If + If Not IsSuccessful Then + Hint("由于文件被占用,部分 Mod 删除失败,请尝试关闭正在运行的游戏后再试!", HintType.Critical) + RefreshList(True) + ElseIf If(IsSearching, PanSearchList, PanItems).Children.Count = 0 Then + RefreshList(True) + Else + ChangeAllSelected(False) + End If + If IsSuccessful Then + If IsShiftPressed Then + Hint($"已彻底删除 {DeleteList.Count} 个文件!", HintType.Finish) + Else + Hint($"已将 {DeleteList.Count} 个文件删除到回收站!", HintType.Finish) + End If + End If + End Sub + + '取消选择 + Private Sub BtnSelectCancel_Click() Handles BtnSelectCancel.Click + ChangeAllSelected(False) End Sub #End Region #Region "单个 Mod 项" - '点击:切换可用状态 / 显示错误原因 - Public Sub Item_Click(sender As MyListItem, e As EventArgs) + '点击 Logo:切换可用状态 / 显示错误原因 + Public Sub ItemLogo_Click(sender As MyListItem, e As EventArgs) Try Dim ModEntity As McMod = sender.Tag @@ -291,12 +494,13 @@ Parent.Children.RemoveAt(IndexOfUi) Parent.Children.Insert(IndexOfUi, McModListItem(NewModEntity)) '仅在非搜索页面才执行,以确保搜索页内外显示一致 - If String.IsNullOrWhiteSpace(SearchBox.Text) Then + If Not IsSearching Then '改变禁用数量的显示 CType(Parent.Parent, MyCard).Title = McModGetTitle(McModLoader.Output) '更新加载器状态 LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", LoaderFolderRunType.UpdateOnly) End If + RefreshBottomBar() Catch ex As Exception Log(ex, "单个状态改变中重命名 Mod 失败") @@ -358,27 +562,30 @@ Dim ListItem As MyListItem ListItem = If(TypeOf sender Is MyIconButton, sender.Tag, sender) Dim ModEntity As McMod = ListItem.Tag + '加载失败信息 + If ModEntity.State = McMod.McModState.Unavaliable Then + MyMsgBox("无法读取此 Mod 的信息。" & vbCrLf & vbCrLf & "详细的错误信息:" & GetExceptionDetail(ModEntity.FileUnavailableReason), "Mod 读取失败") + Return + End If '获取信息 Dim ContentLines As New List(Of String) If ModEntity.Description IsNot Nothing Then ContentLines.Add(ModEntity.Description & vbCrLf) If ModEntity.Authors IsNot Nothing Then ContentLines.Add("作者:" & ModEntity.Authors) ContentLines.Add("文件:" & ModEntity.FileName & "(" & GetString(New FileInfo(ModEntity.Path).Length) & ")") If ModEntity.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntity.Version) - If ModeDebug Then - Dim DebugInfo As New List(Of String) - If ModEntity.ModId IsNot Nothing Then - DebugInfo.Add("Mod ID:" & ModEntity.ModId) - End If - If ModEntity.Dependencies.Count > 0 Then - DebugInfo.Add("依赖于:") - For Each Dep In ModEntity.Dependencies - DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value)) - Next - End If - If DebugInfo.Count > 0 Then - ContentLines.Add(vbCrLf & "—————— 调试信息 ——————") - ContentLines.AddRange(DebugInfo) - End If + Dim DebugInfo As New List(Of String) + If ModEntity.ModId IsNot Nothing Then + DebugInfo.Add("Mod ID:" & ModEntity.ModId) + End If + If ModEntity.Dependencies.Count > 0 Then + DebugInfo.Add("依赖于:") + For Each Dep In ModEntity.Dependencies + DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value)) + Next + End If + If DebugInfo.Count > 0 Then + ContentLines.Add("") + ContentLines.AddRange(DebugInfo) End If '获取用于搜索的 Mod 名称 Dim ModOriginalName As String = ModEntity.Name.Replace(" ", "+") @@ -426,22 +633,14 @@ #End Region + Public ReadOnly Property IsSearching As Boolean + Get + Return Not String.IsNullOrWhiteSpace(SearchBox.Text) + End Get + End Property Public Sub SearchRun() Handles SearchBox.TextChanged - If String.IsNullOrWhiteSpace(SearchBox.Text) Then - '隐藏 - LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", LoaderFolderRunType.RunOnUpdated) - AniStart({ - AaOpacity(PanSearch, -PanSearch.Opacity, 100), - AaCode(Sub() - PanSearch.Height = 0 - PanSearch.Visibility = Visibility.Collapsed - PanList.Visibility = Visibility.Visible - PanManage.Visibility = Visibility.Visible - End Sub,, True), - AaOpacity(PanList, 1 - PanList.Opacity, 150, 30), - AaOpacity(PanManage, 1 - PanManage.Opacity, 150, 30) - }, "FrmVersionMod Search Switch", True) - Else + ChangeAllSelected(False) + If IsSearching Then '构造请求 Dim QueryList As New List(Of SearchEntry(Of McMod)) For Each Entry As McMod In McModLoader.Output @@ -472,15 +671,25 @@ '显示 AniStart({ AaOpacity(PanList, -PanList.Opacity, 100), - AaOpacity(PanManage, -PanManage.Opacity, 100), AaCode(Sub() PanList.Visibility = Visibility.Collapsed - PanManage.Visibility = Visibility.Collapsed PanSearch.Visibility = Visibility.Visible PanSearch.TriggerForceResize() End Sub,, True), AaOpacity(PanSearch, 1 - PanSearch.Opacity, 200, 60) }, "FrmVersionMod Search Switch", True) + Else + '隐藏 + LoaderFolderRun(McModLoader, PageVersionLeft.Version.PathIndie & "mods\", LoaderFolderRunType.RunOnUpdated) + AniStart({ + AaOpacity(PanSearch, -PanSearch.Opacity, 100), + AaCode(Sub() + PanSearch.Height = 0 + PanSearch.Visibility = Visibility.Collapsed + PanList.Visibility = Visibility.Visible + End Sub,, True), + AaOpacity(PanList, 1 - PanList.Opacity, 150, 30) + }, "FrmVersionMod Search Switch", True) End If End Sub diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml index ac8ad0f..2209d92 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml @@ -7,8 +7,8 @@ d:DesignWidth="800" d:DesignHeight="800"> - - + + @@ -215,4 +215,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj index cd56836..e902ab1 100644 --- a/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj +++ b/Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj @@ -182,8 +182,12 @@ + + MyIconTextButton.xaml + + @@ -406,6 +410,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + Designer MSBuild:Compile diff --git a/Plain Craft Launcher 2/Resources/Custom.xaml b/Plain Craft Launcher 2/Resources/Custom.xaml index f731ba5..1dcaf68 100644 --- a/Plain Craft Launcher 2/Resources/Custom.xaml +++ b/Plain Craft Launcher 2/Resources/Custom.xaml @@ -103,26 +103,34 @@ - - + - - - + + - - - - - - + + + + diff --git a/Plain Craft Launcher 2/Resources/Help.zip b/Plain Craft Launcher 2/Resources/Help.zip index 49a5e43ee2c618662d458aabbc871f90c561575a..d2a0c1a194fbbf0c1c2a388b4a1d01e17cfec4ca 100644 GIT binary patch delta 42685 zcmb5VV{;}Hl{k%uZ<)m&0I!|=z#~wmFb?mkr3P1+@ z?gr^Wn^aR6@uTU@1+SF9%SZR5{sXM)3vDJ`V_197wwH*UG`;1uP3!_e-v8bMQJf>lO^yP1wmj)MA`+TmXn2<*-7qjmL;b#a7g zbM<)|K-mK}h`?(FKtE{IPV-S`^LQEF5y!>sF8EBE4k7Q;ugBAo>2M8?lz?RdD@}vs zx}1s_A z1s82$-Q@XBLdy5DT>G6v3?Q}kFZ2qAhx2ae15PBTEe-R$f!qGzRmNPnQ9Jd$2AVBZ zKHq^Xzd74m=8u6?JA$un)%-`rG{;1i0a=eLl%HQ;K%su{`{9x zpU2$WFr|!ds<+o>hdnH5g(=a_*EygrEXiueq$Wcogiek*5U)8-p`no#0v@#43lc@y ziq%RaJ(Cfu7Bry)*G_j%FYv$>NEnB;sBDceK120xG4aINq+AK+A>_*kcpeB75*-onfn!&jwma%I1xywf(Qr&S+36}PBW}6F=s*P@eCYR1Wq?C~ z39Pn{W_eUYilU((Lkl4BN{)3t20SvgR2!09S3T*S@`g2`|aQH zz_cbsp_H|roFOE)#>W$00_IrOA~tEHgy@CSBCnl)YNb-|!Rcf_Xf^>;1>H(S_E&Wt zt3X%wx7r-RL_$OypnE<>U-_e+QZpzWpoPWV^w?A)HGy6Vw@ps5VdDJIl5D6>B`x1= zm9s3}2^oBaoCirDOtEmnf*TUAlfU9V7)!ex`X&Zg9&uEu-R3PeX^qT+D~^OJ`;^+< zUnjO~HbU00U*FsU$IjfWlZgcFFL*kQtUgUIrB+bsOo*5WoI|Q)*&8ZWmhEMd)f5J! zbDspGSC+14sH0=>fAX2DKDA5TC1K4C^)$|O(cg@;VdtJw?gAGL!5<0XUmyH9!ZPB`f zo_qJ0LlT1XQlXKxRD_1DaG1quN`F@pqY?ipoLgXQu*C@}my9hd%Pi7dBH9ynxYKoh-_gy!Y-h8oJy^Kr#9xCar5Pt9*FWOorWBm*je@nu%E8w z;dn_HxKs>E4dzLqaTIvMyHnJ(Ja5bmnQc(kv+f=c5*@wehV-}B-Rg>kGH=6Uh3H_v z2O>Bi0v*Xbz^56*!L|sff?pgGbFps`{s*@$b~dR~{=;(6hAf;BFx{j9Q9xg4uO3d7 z|NEvr9%34`P~ICu33a=(Fk(tyY%W;(@%>CdCGJezDa)mZ+pHBNiqs>rYTIBP2o+NL zMfe6?r&Gu0=PHs(f7ryCV}RG!?b)q%@6y4xkz-;n0Y5O$lkRthObX15jdKmRiG-wi zstIDyAcPmHP-3wHCpQU7fP&~p1u(kfYlj`9&!7j2QZBX$Z9_keNI05V;Ly*LBd@3R zuuTBP{5bEY2dSw!q0&o%?sSSYhB*{b#NIFwCh#^{+aS4^k1v`TveBJp0%bXaoMn8D z5j95NImJt!KXMLAceUd!M zs|~f@oDx+11L9VF9E1Ct;;v}!>+2~)UlF!nf-fiE0_L?EKj}ys!atx{REvj+O83%s zh>cCO2;4yUehmtMevu^~KUuA;7gx;v_=^qRnH#va=?RJ#4G9}#+*lq?-eS%p%0Dr8 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 delta 40121 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