2.12.7.0
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'动画引擎模块
|
||||
'使用 Ani 作为方法或属性的开头,使用 Aa 作为单个动画对象的开头(便于自动补全)
|
||||
|
||||
Imports System.Windows.Media.Effects
|
||||
|
||||
Public Module ModAnimation
|
||||
|
||||
#Region "声明"
|
||||
@@ -10,8 +12,8 @@ Public Module ModAnimation
|
||||
''' </summary>
|
||||
Public ReadOnly Property AniSpeed As Double
|
||||
Get
|
||||
Dim Setting = PCL.Settings.Get("SystemDebugAnim")
|
||||
Return If(Setting >= 30, 200, MathClamp(Setting * 0.1 + 0.1, 0.1, 3))
|
||||
Dim Setting As Integer = Settings.Get("SystemDebugAnim")
|
||||
Return If(Setting >= 30, 200, (Setting * 0.1 + 0.1).Clamp(0.1, 3))
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
@@ -582,10 +584,10 @@ Public Module ModAnimation
|
||||
Public Class AniEaseLinear
|
||||
Inherits AniEase
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
Return MathClamp(t, 0, 1)
|
||||
Return t.Clamp(0, 1)
|
||||
End Function
|
||||
Public Overrides Function GetDelta(t1 As Double, t0 As Double) As Double
|
||||
Return MathClamp(t1, 0, 1) - MathClamp(t0, 0, 1)
|
||||
Return t1.Clamp(0, 1) - t0.Clamp(0, 1)
|
||||
End Function
|
||||
End Class
|
||||
|
||||
@@ -600,7 +602,7 @@ Public Module ModAnimation
|
||||
p = Power
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
Return MathClamp(t, 0, 1) ^ p
|
||||
Return t.Clamp(0, 1) ^ p
|
||||
End Function
|
||||
End Class
|
||||
''' <summary>
|
||||
@@ -613,7 +615,7 @@ Public Module ModAnimation
|
||||
p = Power
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
Return 1 - MathClamp(1 - t, 0, 1) ^ p
|
||||
Return 1 - (1 - t).Clamp(0, 1) ^ p
|
||||
End Function
|
||||
End Class
|
||||
''' <summary>
|
||||
@@ -644,7 +646,7 @@ Public Module ModAnimation
|
||||
If alpha < 0 Then alpha = 0 '初速度小于平均速度时,退化为线性
|
||||
End Sub
|
||||
Public Overrides Function GetValue(percent As Double) As Double
|
||||
Dim p As Double = MathClamp(percent, 0, 1)
|
||||
Dim p As Double = percent.Clamp(0, 1)
|
||||
If alpha = 0 Then Return p '退化到线性
|
||||
Return (alpha + 1) * p / (1 + alpha * p)
|
||||
End Function
|
||||
@@ -661,7 +663,7 @@ Public Module ModAnimation
|
||||
p = 3 - Power * 0.5
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
t = MathClamp(t, 0, 1)
|
||||
t = t.Clamp(0, 1)
|
||||
Return t ^ p * Math.Cos(1.5 * Math.PI * (1 - t))
|
||||
End Function
|
||||
End Class
|
||||
@@ -675,7 +677,7 @@ Public Module ModAnimation
|
||||
p = 3 - Power * 0.5
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
t = MathClamp(t, 0, 1)
|
||||
t = t.Clamp(0, 1)
|
||||
Return 1 - (1 - t) ^ p * Math.Cos(1.5 * Math.PI * t)
|
||||
End Function
|
||||
End Class
|
||||
@@ -719,7 +721,7 @@ Public Module ModAnimation
|
||||
p = Power + 4
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
t = MathClamp(t, 0, 1)
|
||||
t = t.Clamp(0, 1)
|
||||
Return t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
|
||||
End Function
|
||||
End Class
|
||||
@@ -733,7 +735,7 @@ Public Module ModAnimation
|
||||
p = Power + 4
|
||||
End Sub
|
||||
Public Overrides Function GetValue(t As Double) As Double
|
||||
t = 1 - MathClamp(t, 0, 1)
|
||||
t = 1 - t.Clamp(0, 1)
|
||||
Return 1 - t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
|
||||
End Function
|
||||
End Class
|
||||
@@ -750,7 +752,15 @@ Public Module ModAnimation
|
||||
Public Sub AniStart(AniGroup As IList, Optional Name As String = "", Optional RefreshTime As Boolean = False)
|
||||
If RefreshTime Then AniLastTick = GetTimeMs() '避免处理动画时已经造成了极大的延迟,导致动画突然结束
|
||||
'添加到正在执行的动画组
|
||||
Dim NewEntry As New AniGroupEntry With {.Data = GetFullList(Of AniData)(AniGroup), .StartTick = GetTimeMs()}
|
||||
Dim AllAnis = New List(Of AniData)
|
||||
For Each Element In AniGroup
|
||||
If TypeOf Element Is ICollection Then
|
||||
AllAnis.AddRange(Element)
|
||||
Else
|
||||
AllAnis.Add(Element)
|
||||
End If
|
||||
Next
|
||||
Dim NewEntry As New AniGroupEntry With {.Data = AllAnis, .StartTick = GetTimeMs()}
|
||||
If Name = "" Then
|
||||
Name = NewEntry.Uuid
|
||||
Else
|
||||
@@ -803,12 +813,12 @@ Public Module ModAnimation
|
||||
Log("[Animation] 动画线程开始")
|
||||
Do While True
|
||||
'两帧之间的间隔时间
|
||||
Dim DeltaTime As Long = MathClamp(GetTimeMs() - AniLastTick, 0, 100000)
|
||||
Dim DeltaTime As Long = (GetTimeMs() - AniLastTick).Clamp(0, 100000)
|
||||
If DeltaTime < 3 Then GoTo Sleeper
|
||||
AniLastTick = GetTimeMs()
|
||||
'记录 FPS
|
||||
If ModeDebug Then
|
||||
If MathClamp(AniLastTick - AniFPSTimer, 0, 100000) >= 500 Then
|
||||
If (AniLastTick - AniFPSTimer).Clamp(0, 100000) >= 500 Then
|
||||
AniFPS = AniFPSCounter
|
||||
AniFPSCounter = 0
|
||||
AniFPSTimer = AniLastTick
|
||||
@@ -820,13 +830,8 @@ Public Module ModAnimation
|
||||
Sub()
|
||||
AniCount = 0
|
||||
AniTimer(DeltaTime * AniSpeed)
|
||||
'#If DEBUG Then
|
||||
' FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain
|
||||
'#Else
|
||||
' If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain
|
||||
'#End If
|
||||
If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
|
||||
Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & GetString(NetManager.Speed) & "/s)")
|
||||
Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & FormatFileSize(NetManager.Speed) & "/s)")
|
||||
End If
|
||||
End Sub)
|
||||
Sleeper:
|
||||
@@ -834,7 +839,7 @@ Sleeper:
|
||||
Thread.Sleep(1)
|
||||
Loop
|
||||
Catch ex As Exception
|
||||
Log(ex, "动画帧执行失败", LogLevel.Critical)
|
||||
Log(ex, "动画帧执行失败", NotifyLevel.Critical)
|
||||
End Try
|
||||
End Sub, "Animation", ThreadPriority.AboveNormal)
|
||||
End Sub
|
||||
@@ -845,7 +850,7 @@ Sleeper:
|
||||
Public Sub AniTimer(DeltaTick As Integer)
|
||||
Try
|
||||
|
||||
If DeltaTick / AniSpeed > 100 Then Log("[Animation] 两个动画帧间隔 " & DeltaTick & " ms", LogLevel.Developer)
|
||||
If DeltaTick / AniSpeed > 100 Then Log("[Animation] 两个动画帧间隔 " & DeltaTick & " ms", NotifyLevel.DevelopOnly)
|
||||
Dim i As Integer = -1
|
||||
'循环每个动画组
|
||||
Do While i + 1 < AniGroups.Count
|
||||
@@ -910,7 +915,7 @@ NextAni:
|
||||
Loop
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "动画刻执行失败", LogLevel.Hint)
|
||||
Log(ex, "动画刻执行失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -923,15 +928,39 @@ NextAni:
|
||||
Select Case Ani.TypeMain
|
||||
|
||||
Case AniType.Number
|
||||
Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
If Delta <> 0 Then
|
||||
Select Case Ani.TypeSub
|
||||
Case AniTypeSub.X
|
||||
DeltaLeft(Ani.Obj, Delta)
|
||||
If TypeOf Ani.Obj Is Window Then
|
||||
CType(Ani.Obj, Window).Left += Delta
|
||||
Else
|
||||
Select Case Ani.Obj.HorizontalAlignment
|
||||
Case HorizontalAlignment.Left, HorizontalAlignment.Stretch
|
||||
Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left + Delta, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom)
|
||||
Case HorizontalAlignment.Right
|
||||
Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right - Delta, Ani.Obj.Margin.Bottom)
|
||||
End Select
|
||||
End If
|
||||
Case AniTypeSub.Y
|
||||
DeltaTop(Ani.Obj, Delta)
|
||||
If TypeOf Ani.Obj Is Window Then
|
||||
CType(Ani.Obj, Window).Top += Delta
|
||||
Else
|
||||
Select Case Ani.Obj.VerticalAlignment
|
||||
Case VerticalAlignment.Top
|
||||
Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top + Delta, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom)
|
||||
Case VerticalAlignment.Bottom
|
||||
Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom - Delta)
|
||||
End Select
|
||||
End If
|
||||
Case AniTypeSub.Opacity
|
||||
Ani.Obj.Opacity = MathClamp(Ani.Obj.Opacity + Delta, 0, 1)
|
||||
If TypeOf Ani.Obj Is UIElement Then
|
||||
Dim Obj As UIElement = Ani.Obj
|
||||
Obj.Opacity = (Obj.Opacity + Delta).Clamp(0, 1)
|
||||
ElseIf TypeOf Ani.Obj Is DropShadowEffect Then
|
||||
Dim Obj As DropShadowEffect = Ani.Obj
|
||||
Obj.Opacity = (Obj.Opacity + Delta).Clamp(0, 1)
|
||||
End If
|
||||
Case AniTypeSub.Width
|
||||
Dim Obj As FrameworkElement = Ani.Obj
|
||||
Obj.Width = Math.Max(If(Double.IsNaN(Obj.Width), Obj.ActualWidth, Obj.Width) + Delta, 0)
|
||||
@@ -963,7 +992,7 @@ NextAni:
|
||||
|
||||
Case AniType.Color
|
||||
'利用 Last 记录了余下的小数值
|
||||
Dim Delta As MyColor = MathPercent(New MyColor(0, 0, 0, 0), Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)) + Ani.ValueLast
|
||||
Dim Delta As MyColor = MyColor.Lerp(New MyColor(0, 0, 0, 0), Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)) + Ani.ValueLast
|
||||
Dim Obj As FrameworkElement = Ani.Obj(0)
|
||||
Dim Prop As DependencyProperty = Ani.Obj(1)
|
||||
Dim NewColor As MyColor = New MyColor(Obj.GetValue(Prop)) + Delta
|
||||
@@ -973,17 +1002,18 @@ NextAni:
|
||||
Case AniType.Scale
|
||||
Dim Obj As FrameworkElement = Ani.Obj
|
||||
Dim Delta As Double = Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)
|
||||
Obj.Margin = New Thickness(Obj.Margin.Left + MathPercent(0, Ani.Value.Left, Delta), Obj.Margin.Top + MathPercent(0, Ani.Value.Top, Delta), Obj.Margin.Right + MathPercent(0, Ani.Value.Left, Delta), Obj.Margin.Bottom + MathPercent(0, Ani.Value.Top, Delta))
|
||||
Obj.Width = Math.Max(Obj.Width + MathPercent(0, Ani.Value.Width, Delta), 0)
|
||||
Obj.Height = Math.Max(Obj.Height + MathPercent(0, Ani.Value.Height, Delta), 0)
|
||||
Obj.Margin = New Thickness(Obj.Margin.Left + MathUtils.Lerp(0, Ani.Value.Left, Delta), Obj.Margin.Top + MathUtils.Lerp(0, Ani.Value.Top, Delta), Obj.Margin.Right + MathUtils.Lerp(0, Ani.Value.Left, Delta), Obj.Margin.Bottom + MathUtils.Lerp(0, Ani.Value.Top, Delta))
|
||||
Obj.Width = Math.Max(Obj.Width + MathUtils.Lerp(0, Ani.Value.Width, Delta), 0)
|
||||
Obj.Height = Math.Max(Obj.Height + MathUtils.Lerp(0, Ani.Value.Height, Delta), 0)
|
||||
|
||||
Case AniType.TextAppear
|
||||
Dim TextCount As Integer = If(Ani.Value(1), Ani.Value(0).ToString.Length, 0) +
|
||||
Math.Round(Ani.Value(0).ToString.Length * If(Ani.Value(1), -1, 1) * Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, 0))
|
||||
Dim NewText As String = Mid(Ani.Value(0), 1, TextCount)
|
||||
Dim Text As String = Ani.Value(0)
|
||||
Dim TextCount As Integer = If(Ani.Value(1), Text.Length, 0) +
|
||||
Math.Round(Text.Length * If(Ani.Value(1), -1, 1) * Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, 0))
|
||||
Dim NewText As String = Text.Substring(0, TextCount)
|
||||
'添加乱码
|
||||
If TextCount < Ani.Value(0).ToString.Length Then
|
||||
Dim NextText As String = Mid(Ani.Value(0), TextCount + 1, 1)
|
||||
If TextCount < Text.Length Then
|
||||
Dim NextText As String = Text.Substring(TextCount, 1)
|
||||
If Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128)) Then
|
||||
NewText &= Encoding.GetEncoding("GB18030").GetString({RandomInteger(16 + 160, 87 + 160), RandomInteger(1 + 160, 89 + 160)})
|
||||
Else
|
||||
@@ -1006,7 +1036,7 @@ NextAni:
|
||||
If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
|
||||
Obj.RenderTransform = New ScaleTransform(1, 1)
|
||||
End If
|
||||
Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
CType(Obj.RenderTransform, ScaleTransform).ScaleX = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleX + Delta, 0)
|
||||
CType(Obj.RenderTransform, ScaleTransform).ScaleY = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleY + Delta, 0)
|
||||
|
||||
@@ -1016,13 +1046,13 @@ NextAni:
|
||||
If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
|
||||
Obj.RenderTransform = New RotateTransform(0)
|
||||
End If
|
||||
Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
|
||||
CType(Obj.RenderTransform, RotateTransform).Angle = CType(Obj.RenderTransform, RotateTransform).Angle + Delta
|
||||
|
||||
End Select
|
||||
Ani.TimePercent = Ani.TimeFinished / Ani.TimeTotal '修改执行百分比
|
||||
Catch ex As Exception
|
||||
Log(ex, "执行动画失败:" & Ani.ToString, LogLevel.Hint)
|
||||
Log(ex, "执行动画失败:" & Ani.ToString, NotifyLevel.AllUsers)
|
||||
End Try
|
||||
Return Ani
|
||||
End Function
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@
|
||||
RealParent = RealParent.Parent
|
||||
End While
|
||||
Catch ex As Exception
|
||||
Log(ex, "获取父加载器失败(" & Name & ")", LogLevel.Feedback)
|
||||
Log(ex, "获取父加载器失败(" & Name & ")", NotifyLevel.MsgBoxAndFeedback)
|
||||
Return Nothing
|
||||
End Try
|
||||
End Get
|
||||
@@ -283,7 +283,7 @@
|
||||
Try
|
||||
Input = StartGetInput(Input)
|
||||
Catch ex As Exception
|
||||
Log(ex, "加载输入获取失败(" & Name & ")", LogLevel.Hint)
|
||||
Log(ex, "加载输入获取失败(" & Name & ")", NotifyLevel.AllUsers)
|
||||
[Error] = ex
|
||||
SyncLock LockState
|
||||
State = LoadState.Failed
|
||||
@@ -322,7 +322,7 @@
|
||||
If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
|
||||
LoadDelegate(Me)
|
||||
If IsInterrupted Then
|
||||
Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
|
||||
Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", NotifyLevel.DevelopOnly)
|
||||
Return
|
||||
End If
|
||||
If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
|
||||
@@ -354,7 +354,7 @@
|
||||
If IsInterrupted OrElse State >= LoadState.Finished Then Return
|
||||
State = LoadState.Failed
|
||||
End SyncLock
|
||||
Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
|
||||
Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", NotifyLevel.DevelopOnly)
|
||||
TriggerThreadInterrupt()
|
||||
End Sub
|
||||
Public Overrides Sub Interrupt()
|
||||
@@ -527,7 +527,7 @@
|
||||
'检查是否需要重启
|
||||
If Loader.GetType.Name.StartsWithF("LoaderTask") Then
|
||||
If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
|
||||
Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, LogLevel.Developer)
|
||||
Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, NotifyLevel.DevelopOnly)
|
||||
GoTo Restart
|
||||
End If
|
||||
End If
|
||||
@@ -591,7 +591,7 @@ Restart:
|
||||
End Class
|
||||
|
||||
'任务栏进度条
|
||||
Public LoaderTaskbar As New SafeList(Of LoaderBase)
|
||||
Public LoaderTaskbar As New ConcurrentList(Of LoaderBase)
|
||||
Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
|
||||
Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
|
||||
|
||||
@@ -635,15 +635,15 @@ Restart:
|
||||
FrmMain.BtnExtraDownload.ShowRefresh()
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "刷新任务栏进度显示失败", LogLevel.Feedback)
|
||||
Log(ex, "刷新任务栏进度显示失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
Public Function LoaderTaskbarProgressGet() As Double
|
||||
Try
|
||||
If Not LoaderTaskbar.Any Then Return 1
|
||||
Return MathClamp(LoaderTaskbar.Select(Function(l) l.Progress).Average(), 0, 1)
|
||||
Return LoaderTaskbar.Select(Function(l) l.Progress).Average().Clamp(0, 1)
|
||||
Catch ex As Exception
|
||||
Log(ex, "获取任务栏进度出错", LogLevel.Feedback)
|
||||
Log(ex, "获取任务栏进度出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
Return 0.5
|
||||
End Try
|
||||
End Function
|
||||
|
||||
@@ -36,7 +36,7 @@ Public Module ModNet
|
||||
Catch ex As ThreadInterruptedException
|
||||
Throw
|
||||
Catch ex As Exception
|
||||
If MakeLog Then Log(ex, "网络请求失败", LogLevel.Developer)
|
||||
If MakeLog Then Log(ex, "网络请求失败", NotifyLevel.DevelopOnly)
|
||||
Throw
|
||||
End Try
|
||||
End Function
|
||||
@@ -176,7 +176,7 @@ RequestFinished:
|
||||
Log($"[Net] 通过网络请求直接下载小文件:{Url} → {LocalPath}")
|
||||
'初始化文件路径
|
||||
Try
|
||||
Directory.CreateDirectory(GetPathFromFullPath(LocalPath))
|
||||
DirectoryUtils.Create(LocalPath, isFilePath:=True)
|
||||
File.Delete(LocalPath)
|
||||
Catch ex As Exception
|
||||
Throw New Exception($"预处理下载文件路径失败({LocalPath})", ex)
|
||||
@@ -257,9 +257,7 @@ Retry:
|
||||
'DNS 解析
|
||||
CancelToken = New CancellationTokenSource(Timeout)
|
||||
HostIp = DNSLookup(Request, CancelToken.Token)
|
||||
If HostIp IsNot Nothing AndAlso Not IPReliability.ContainsKey(HostIp) Then
|
||||
IPReliability(HostIp) = -0.01 '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
|
||||
End If
|
||||
If HostIp IsNot Nothing Then IPReliability.GetOrAdd(HostIp, -0.01) '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
|
||||
'发送请求
|
||||
SyncLock RequestClientLock
|
||||
If RequestClient Is Nothing Then '延迟初始化,以避免在程序启动前加载 CacheCow 导致 DLL 加载失败
|
||||
@@ -269,8 +267,10 @@ Retry:
|
||||
})
|
||||
End If
|
||||
End SyncLock
|
||||
CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
|
||||
Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
|
||||
Dim ResponseBytes As Byte()
|
||||
CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
|
||||
Using ResponseStream = Response.Content.ReadAsStreamAsync().GetResultWithTimeout(CancelToken, Timeout)
|
||||
Using Stream As New MemoryStream
|
||||
ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
|
||||
@@ -320,17 +320,16 @@ Retry:
|
||||
Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationToken) As String
|
||||
Dim Host = Request.RequestUri.Host
|
||||
'不对部分有严格 SNI 限制的域名进行 DNS 解析(#8295)
|
||||
If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsF(h, True)) Then Return Nothing
|
||||
If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsIgnoreCase(h)) Then Return Nothing
|
||||
'若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
|
||||
SyncLock DNSFailureRecord.SyncRoot
|
||||
If DNSFailureRecord.ContainsKey(Host) Then
|
||||
If Date.Now - DNSFailureRecord(Host) < New TimeSpan(0, 1, 0) Then
|
||||
Return Nothing
|
||||
Else
|
||||
DNSFailureRecord.Remove(Host)
|
||||
End If
|
||||
Dim LastFailureTime As Date
|
||||
If DNSFailureRecord.TryGetValue(Host, LastFailureTime) Then
|
||||
If Date.Now - LastFailureTime < New TimeSpan(0, 1, 0) Then
|
||||
Return Nothing
|
||||
Else
|
||||
DNSFailureRecord.Remove(Host)
|
||||
End If
|
||||
End SyncLock
|
||||
End If
|
||||
'初步 DNS 解析
|
||||
Dim Candidates As IPAddress()
|
||||
Try
|
||||
@@ -363,21 +362,21 @@ Retry:
|
||||
Request.RequestUri = (New UriBuilder(Request.RequestUri) With {.Host = If(Target.AddressFamily = AddressFamily.InterNetworkV6, $"[{Target}]", Target.ToString)}).Uri
|
||||
Return Target.ToString
|
||||
End Function
|
||||
Private DNSFailureRecord As New SafeDictionary(Of String, Date)
|
||||
Private DNSFailureRecord As New ConcurrentDictionary(Of String, Date)
|
||||
''' <summary>
|
||||
''' 记录每个 IP 地址的请求可靠度。
|
||||
''' 通常取值 -1 ~ +0.5,越高越好。未尝试过的 IP 应视为 0。
|
||||
''' </summary>
|
||||
Private IPReliability As New SafeDictionary(Of String, Double)
|
||||
Private IPReliability As New ConcurrentDictionary(Of String, Double)
|
||||
''' <summary>
|
||||
''' 根据请求结果,记录 IP 地址的可靠度。
|
||||
''' </summary>
|
||||
Private Sub RecordIPReliability(IP As String, Result As Double)
|
||||
If IP Is Nothing Then Return
|
||||
SyncLock IPReliability.SyncRoot
|
||||
If Not IPReliability.ContainsKey(IP) Then IPReliability(IP) = 0
|
||||
IPReliability(IP) = (IPReliability(IP) + Result) / 2
|
||||
End SyncLock
|
||||
IPReliability.AddOrUpdate(IP,
|
||||
Function() Result * 0.5,
|
||||
Function(k, v) v * 0.5 + Result * 0.5
|
||||
)
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
@@ -687,11 +686,11 @@ Retry:
|
||||
''' <summary>
|
||||
''' 所属的文件列表任务。
|
||||
''' </summary>
|
||||
Public Tasks As New SafeList(Of LoaderDownload)
|
||||
Public Tasks As New ConcurrentList(Of LoaderDownload)
|
||||
''' <summary>
|
||||
''' 所有下载源。
|
||||
''' </summary>
|
||||
Public Sources As SafeList(Of NetSource)
|
||||
Public Sources As ConcurrentList(Of NetSource)
|
||||
''' <summary>
|
||||
''' 用于在第一个线程出错时切换下载源。
|
||||
''' </summary>
|
||||
@@ -699,7 +698,7 @@ Retry:
|
||||
''' <summary>
|
||||
''' 所有已经被标记为失败的,但未完整尝试过的,不允许断点续传的下载源。
|
||||
''' </summary>
|
||||
Public SourcesOnce As New SafeList(Of NetSource)
|
||||
Public SourcesOnce As New ConcurrentList(Of NetSource)
|
||||
''' <summary>
|
||||
''' 仅当合并失败或首次下载失败时,会将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载。
|
||||
''' 这一策略可以兼容多个下载源中的一部分返回错误的文件的情况,以及部分在多线程下载时会抽风的源。
|
||||
@@ -895,7 +894,7 @@ Retry:
|
||||
Dim Count As Integer = 0
|
||||
Urls = Urls.Distinct.ToArray
|
||||
For Each Source As String In Urls
|
||||
Sources.Add(New NetSource With {.FailCount = 0, .Url = SecretCdnSign(Source.Replace(vbCr, "").Replace(vbLf, "").Trim), .Id = Count, .IsFailed = False, .Ex = Nothing})
|
||||
Sources.Add(New NetSource With {.FailCount = 0, .Url = SecretCdnSign(Source.ReplaceLineEndings("").Trim), .Id = Count, .IsFailed = False, .Ex = Nothing})
|
||||
Count += 1
|
||||
Next
|
||||
Me.Sources = Sources
|
||||
@@ -1049,7 +1048,7 @@ StartThread:
|
||||
End If
|
||||
End If
|
||||
FileSize = ContentLength : IsUnknownSize = False
|
||||
Log($"[Download] {LocalName}:文件大小 {ContentLength}({GetString(ContentLength)})")
|
||||
Log($"[Download] {LocalName}:文件大小 {ContentLength}({FormatFileSize(ContentLength)})")
|
||||
'若文件大小大于 50 M,进行剩余磁盘空间校验
|
||||
If ContentLength > 50 * 1024 * 1024 Then
|
||||
For Each Drive As DriveInfo In DriveInfo.GetDrives
|
||||
@@ -1057,7 +1056,7 @@ StartThread:
|
||||
Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) +
|
||||
If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0)
|
||||
If RequiredSpace > 0 AndAlso Drive.TotalFreeSpace < RequiredSpace Then
|
||||
Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
|
||||
Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & FormatFileSize(RequiredSpace) & " 空间,但当前仅剩余 " & FormatFileSize(Drive.TotalFreeSpace) & "。" &
|
||||
If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
|
||||
End If
|
||||
Next
|
||||
@@ -1084,11 +1083,12 @@ NotSupportRange:
|
||||
Cache = New MemoryStream
|
||||
ResultStream = Cache
|
||||
Else
|
||||
Directory.CreateDirectory(PathTemp & "Download")
|
||||
Th.Temp = $"{PathTemp}Download\{Uuid}_{Th.Uuid}_{RandomInteger(0, 999999)}.tmp"
|
||||
DirectoryUtils.Create(Th.Temp, isFilePath:=True)
|
||||
ResultStream = New FileStream(Th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
|
||||
End If
|
||||
'开始下载
|
||||
CancelToken.Dispose() : CancelToken = New CancellationTokenSource() '重置超时,不再让 CancellationToken 在固定时间后触发(#8488)
|
||||
ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
|
||||
If Settings.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
|
||||
Dim ResponseBytes As Byte() = New Byte(16384) {}
|
||||
@@ -1183,7 +1183,7 @@ SourceBreak:
|
||||
ex.Message.Contains("(502)") OrElse ex.Message.Contains("(404)") OrElse
|
||||
ex.Message.Contains("未能解析") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
|
||||
((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.Contains("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
|
||||
(Th.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
|
||||
(Th.Source.FailCount >= NetTaskThreadLimit.Clamp(5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
|
||||
'当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理
|
||||
Dim IsThisFail As Boolean = False
|
||||
SyncLock LockSource
|
||||
@@ -1196,7 +1196,7 @@ SourceBreak:
|
||||
If IsThisFail Then
|
||||
Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id},Range 问题:{IsRangeNotSupported}):{Th.Source.Url}")
|
||||
Log(ex, $"{If(SourcesOnce.FirstOrDefault?.SingleThread Is Nothing, "", "单线程")}下载源 {Th.Source.Id} 已被禁用",
|
||||
If(IsRangeNotSupported OrElse ex.Message.Contains("(404)"), LogLevel.Developer, LogLevel.Debug))
|
||||
If(IsRangeNotSupported OrElse ex.Message.Contains("(404)"), NotifyLevel.DevelopOnly, NotifyLevel.DebugModeOnly))
|
||||
SyncLock LockSource
|
||||
SourcesOnce.Remove(Th.Source)
|
||||
End SyncLock
|
||||
@@ -1208,7 +1208,7 @@ SourceBreak:
|
||||
ElseIf Not Retried Then
|
||||
'合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
|
||||
'若所有源均不支持 Range,也会走到这里重试
|
||||
If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", LogLevel.Debug)
|
||||
If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", NotifyLevel.DebugModeOnly)
|
||||
Retried = True
|
||||
SyncLock LockSource
|
||||
SourcesOnce.Clear()
|
||||
@@ -1236,7 +1236,7 @@ SourceBreak:
|
||||
Log("[Download] 已禁用的下载源:" & Source.Url)
|
||||
If Source.Ex IsNot Nothing Then
|
||||
ExampleEx = Source.Ex
|
||||
Log(Source.Ex, "下载源禁用原因", LogLevel.Developer)
|
||||
Log(Source.Ex, "下载源禁用原因", NotifyLevel.DevelopOnly)
|
||||
End If
|
||||
Next
|
||||
End SyncLock
|
||||
@@ -1276,15 +1276,14 @@ SourceBreak:
|
||||
Try
|
||||
Retry:
|
||||
'创建文件夹
|
||||
DirectoryUtils.Create(LocalPath, isFilePath:=True)
|
||||
If File.Exists(LocalPath) Then File.Delete(LocalPath)
|
||||
Directory.CreateDirectory(GetPathFromFullPath(LocalPath))
|
||||
SyncLock LockChain
|
||||
'合并文件
|
||||
If IsNoSplit Then
|
||||
'仅有一个线程,从内存中输出
|
||||
If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从内存输出文件")
|
||||
Cache.Position = 0
|
||||
WriteFile(LocalPath, Cache)
|
||||
FileUtils.Write(LocalPath, Cache)
|
||||
ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
|
||||
'仅有一个文件,直接复制
|
||||
If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并")
|
||||
@@ -1420,7 +1419,7 @@ Retry:
|
||||
''' <summary>
|
||||
''' 需要下载的文件。
|
||||
''' </summary>
|
||||
Public Files As SafeList(Of NetFile)
|
||||
Public Files As ConcurrentList(Of NetFile)
|
||||
''' <summary>
|
||||
''' 剩余未完成的文件数。(用于减轻 FilesLock 的占用)
|
||||
''' </summary>
|
||||
@@ -1452,7 +1451,7 @@ Retry:
|
||||
Set(value As Integer)
|
||||
_FailCount = value
|
||||
If State = LoadState.Loading AndAlso value >= Math.Min(10000, Math.Max(FileRemain * 5.5, NetTaskThreadLimit * 5.5 + 3)) Then
|
||||
Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " & value & " 次", LogLevel.Debug)
|
||||
Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " & value & " 次", NotifyLevel.DebugModeOnly)
|
||||
On Error Resume Next
|
||||
Dim ExList As New List(Of Exception)
|
||||
For Each File In Files
|
||||
@@ -1495,12 +1494,12 @@ FinishExCatch:
|
||||
|
||||
Public Sub New(Name As String, FileTasks As List(Of NetFile))
|
||||
Me.Name = Name
|
||||
Files = New SafeList(Of NetFile)(FileTasks)
|
||||
Files = New ConcurrentList(Of NetFile)(FileTasks)
|
||||
End Sub
|
||||
Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
|
||||
If Input IsNot Nothing Then Files = New SafeList(Of NetFile)(Input)
|
||||
If Input IsNot Nothing Then Files = New ConcurrentList(Of NetFile)(Input)
|
||||
'去重
|
||||
Files = New SafeList(Of NetFile)(Files.Distinct(Function(a, b) a.LocalPath = b.LocalPath))
|
||||
Files = New ConcurrentList(Of NetFile)(Files.DistinctBy(Function(a) a.LocalPath))
|
||||
'设置剩余文件数
|
||||
SyncLock FileRemainLock
|
||||
FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
|
||||
@@ -1527,7 +1526,7 @@ FinishExCatch:
|
||||
File.LocalPath = File.LocalPath.Replace("/", "\")
|
||||
If Not File.LocalPath.Lower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
|
||||
If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
|
||||
Directory.CreateDirectory(GetPathFromFullPath(File.LocalPath)) '创建目标文件夹
|
||||
DirectoryUtils.Create(File.LocalPath, isFilePath:=True) '创建目标文件夹
|
||||
Next
|
||||
'接入下载管理器,获取新开始下载的文件列表
|
||||
Dim NewFiles = NetManager.Start(Me)
|
||||
@@ -1555,7 +1554,7 @@ FinishExCatch:
|
||||
Folders.AddRange(McFolderList.Select(Function(f) f.Location))
|
||||
Folders = Folders.Distinct.Where(Function(f) Directory.Exists(f)).ToList
|
||||
'平均分配到多个检查线程
|
||||
Dim ThreadCount As Integer = MathClamp(FilesToCheck.Count \ 40, 1, 8) '每个线程至少 40 个文件,最多 8 线程
|
||||
Dim ThreadCount As Integer = (FilesToCheck.Count \ 40).Clamp(1, 8) '每个线程至少 40 个文件,最多 8 线程
|
||||
If ThreadCount = 1 Then '只有一个线程,直接执行
|
||||
CheckExistingFiles(FilesToCheck, Folders)
|
||||
Else
|
||||
@@ -1590,6 +1589,7 @@ FinishExCatch:
|
||||
'处理每个文件
|
||||
For Each File As NetFile In Files
|
||||
Dim Target As String = CheckExistingFile(FolderList, VersionFolders, File)
|
||||
If ModeDebug Then Log($"[Download] 文件检查结果:{File.LocalName} → {Target},当前状态 {File.State}")
|
||||
If File.State >= NetState.WaitingToDownload Then Continue For '中断
|
||||
'已找到相同文件
|
||||
If Target IsNot Nothing Then
|
||||
@@ -1645,7 +1645,7 @@ Retry:
|
||||
End If
|
||||
Dim Type = TypeIndexes.MaxBy(Function(kv) kv.Item2).FolderName.TrimStart("\"c)
|
||||
'根据类别进行查找
|
||||
Static Sizes As New SafeDictionary(Of String, Long)
|
||||
Static Sizes As New ConcurrentDictionary(Of String, Long)
|
||||
Select Case Type
|
||||
Case "assets\", "libraries\"
|
||||
'assets/libraries:查找 MC 文件夹下的相同路径
|
||||
@@ -1659,8 +1659,8 @@ Retry:
|
||||
For Each Candidate In Directory.GetFiles(VersionFolder,
|
||||
"*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").Lower, SearchOption.TopDirectoryOnly)
|
||||
'快速进行大小校验
|
||||
If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
|
||||
If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
|
||||
Dim Length = Sizes.GetOrAdd(Candidate, Function(c) New FileInfo(c).Length)
|
||||
If File.Check.ActualSize <> Length Then Continue For
|
||||
'Hash 校验
|
||||
If File.Check.Check(Candidate) Is Nothing Then Return Candidate
|
||||
Next
|
||||
@@ -1673,8 +1673,8 @@ Retry:
|
||||
If Not Directory.Exists(TargetFolder) Then Continue For
|
||||
For Each Candidate In Directory.GetFiles(TargetFolder)
|
||||
'快速进行大小校验
|
||||
If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
|
||||
If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
|
||||
Dim Length = Sizes.GetOrAdd(Candidate, Function(c) New FileInfo(c).Length)
|
||||
If File.Check.ActualSize <> Length Then Continue For
|
||||
'Hash 校验
|
||||
If File.Check.Check(Candidate) Is Nothing Then Return Candidate
|
||||
Next
|
||||
@@ -1767,12 +1767,12 @@ Retry:
|
||||
''' <summary>
|
||||
''' 需要下载的文件。为“本地地址 - 文件对象”键值对。
|
||||
''' </summary>
|
||||
Public AllFiles As New SafeDictionary(Of String, NetFile)
|
||||
Public AllFiles As New ConcurrentDictionary(Of String, NetFile)
|
||||
|
||||
''' <summary>
|
||||
''' 当前的所有下载任务。
|
||||
''' </summary>
|
||||
Public Tasks As New SafeList(Of LoaderDownload)
|
||||
Public Tasks As New ConcurrentList(Of LoaderDownload)
|
||||
|
||||
''' <summary>
|
||||
''' 已下载完成的大小。
|
||||
@@ -1823,7 +1823,7 @@ Retry:
|
||||
If SpeedLast.Count >= 10 Then Limit = SpeedLast.Take(10).Average * 0.85 '取近 1 秒的平均速度的 85%
|
||||
If Limit > NetTaskSpeedLimitLow Then
|
||||
NetTaskSpeedLimitLow = Limit
|
||||
Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
|
||||
Log("[Download] " & "速度下限已提升到 " & FormatFileSize(Limit))
|
||||
End If
|
||||
#End Region
|
||||
#Region "刷新下载任务属性"
|
||||
@@ -1850,14 +1850,10 @@ Retry:
|
||||
While True
|
||||
Thread.Sleep(20)
|
||||
'获取文件列表
|
||||
Dim AllFiles As List(Of NetFile)
|
||||
SyncLock Me.AllFiles.SyncRoot
|
||||
If Id = 0 AndAlso FileRemain = 0 AndAlso Me.AllFiles.Any() Then Me.AllFiles.Clear() '若已完成,则清空
|
||||
AllFiles = Me.AllFiles.Values.ToList()
|
||||
End SyncLock
|
||||
Dim WaitingFiles As New List(Of NetFile)
|
||||
Dim OngoingFiles As New List(Of NetFile)
|
||||
For Each File As NetFile In AllFiles
|
||||
If Id = 0 AndAlso FileRemain = 0 Then AllFiles.Clear() '若已完成,则清空
|
||||
For Each File As NetFile In AllFiles.Values
|
||||
If File.Uuid Mod 2 = Id Then Continue For
|
||||
If File.State = NetState.WaitingToDownload Then
|
||||
WaitingFiles.Add(File)
|
||||
@@ -1893,7 +1889,7 @@ Retry:
|
||||
Next
|
||||
End While
|
||||
Catch ex As Exception
|
||||
Log(ex, $"下载管理启动线程 {Id} 出错", LogLevel.Critical)
|
||||
Log(ex, $"下载管理启动线程 {Id} 出错", NotifyLevel.Critical)
|
||||
End Try
|
||||
End Sub
|
||||
RunInNewThread(Sub() ThreadStarter(0), "NetManager ThreadStarter 0")
|
||||
@@ -1917,7 +1913,7 @@ Retry:
|
||||
End If
|
||||
End While
|
||||
Catch ex As Exception
|
||||
Log(ex, "下载管理刷新线程出错", LogLevel.Critical)
|
||||
Log(ex, "下载管理刷新线程出错", NotifyLevel.Critical)
|
||||
End Try
|
||||
End Sub, "NetManager StatRefresher")
|
||||
End Sub
|
||||
@@ -1946,10 +1942,11 @@ Retry:
|
||||
Dim NewFiles As New List(Of NetFile)
|
||||
For i = 0 To Task.Files.Count - 1
|
||||
Dim File As NetFile = Task.Files(i)
|
||||
If AllFiles.ContainsKey(File.LocalPath) AndAlso AllFiles(File.LocalPath).State < NetState.Finished Then
|
||||
Dim OngoingFile As NetFile = Nothing
|
||||
If AllFiles.TryGetValue(File.LocalPath, OngoingFile) AndAlso OngoingFile.State < NetState.Finished Then
|
||||
'该文件正在被另一个任务下载
|
||||
If ModeDebug Then Log($"[Download] {File.LocalName}:无需开始,该文件已在下载中,目标 {File.LocalPath}")
|
||||
File = AllFiles(File.LocalPath) '将列表中的文件替换成下载中的文件,即两个任务指向同一个文件;抛弃现在的 File 对象
|
||||
File = OngoingFile '将列表中的文件替换成下载中的文件,即两个任务指向同一个文件;抛弃现在的 File 对象
|
||||
Task.Files(i) = File
|
||||
Else
|
||||
'常规情况(该文件可能已在其他任务中下载完成,但这并不影响)
|
||||
@@ -2053,7 +2050,7 @@ Retry:
|
||||
Return {
|
||||
"(408)", "超时", "timeout", "网络请求失败", "连接尝试失败", "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称",
|
||||
"由于目标计算机积极拒绝", "基础连接已经关闭"
|
||||
}.Any(Function(k) Detail.ContainsF(k, True))
|
||||
}.Any(Function(k) Detail.ContainsIgnoreCase(k))
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
@@ -2065,7 +2062,7 @@ Retry:
|
||||
Dim Request As HttpWebRequest = TryCast(Sender, HttpWebRequest)
|
||||
If Failure = Net.Security.SslPolicyErrors.None Then Return True '已通过验证
|
||||
'基于 #3018 和 #5879,只在访问正版登录 API 时跳过证书验证
|
||||
Log($"[System] 未通过 SSL 证书验证({Failure}),提供的证书为 {Certificate?.Subject},URL:{Request?.Address}", LogLevel.Debug)
|
||||
Log($"[System] 未通过 SSL 证书验证({Failure}),提供的证书为 {Certificate?.Subject},URL:{Request?.Address}", NotifyLevel.DebugModeOnly)
|
||||
If Request Is Nothing Then
|
||||
Return Not Enabled
|
||||
ElseIf Request.Address.Host.Contains("xboxlive") OrElse Request.Address.Host.Contains("minecraftservices") Then
|
||||
|
||||
@@ -244,18 +244,18 @@ End Class
|
||||
''' </summary>
|
||||
Public Class ValidateFolderName
|
||||
Inherits Validate
|
||||
Public Property Path As String
|
||||
Public Property Folder As String
|
||||
Public Property UseMinecraftCharCheck As Boolean = True
|
||||
Public Property IgnoreCase As Boolean = True
|
||||
Private ReadOnly PathIgnore As IEnumerable(Of DirectoryInfo)
|
||||
Public Sub New()
|
||||
End Sub
|
||||
Public Sub New(Path As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
|
||||
Me.Path = Path
|
||||
Public Sub New(Folder As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
|
||||
Me.Folder = Folder
|
||||
Me.IgnoreCase = IgnoreCase
|
||||
Me.UseMinecraftCharCheck = UseMinecraftCharCheck
|
||||
On Error Resume Next
|
||||
PathIgnore = New DirectoryInfo(Path).EnumerateDirectories
|
||||
PathIgnore = New DirectoryInfo(Folder).EnumerateDirectories
|
||||
End Sub
|
||||
Public Overrides Function Validate(Str As String) As String
|
||||
Try
|
||||
@@ -271,7 +271,7 @@ Public Class ValidateFolderName
|
||||
'检查尾部小数点
|
||||
If Str.EndsWithF(".") Then Return "文件夹名不能以小数点结尾!"
|
||||
'检查特殊字符
|
||||
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
|
||||
Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
|
||||
If CharactCheck <> "" Then Return CharactCheck
|
||||
'检查特殊字符串
|
||||
Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件夹名不可为 %!", True).Validate(Str)
|
||||
@@ -326,7 +326,7 @@ Public Class ValidateFileName
|
||||
'检查尾部小数点
|
||||
If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
|
||||
'检查特殊字符
|
||||
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件名不可包含 % 字符!").Validate(Str)
|
||||
Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件名不可包含 % 字符!").Validate(Str)
|
||||
If CharactCheck <> "" Then Return CharactCheck
|
||||
'检查特殊字符串
|
||||
Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件名不可为 %!", True).Validate(Str)
|
||||
@@ -388,7 +388,7 @@ Fin:
|
||||
Dim SubLengthCheck As String = New ValidateNullOrWhiteSpace().Validate(SubStr)
|
||||
If Not SubLengthCheck = "" Then Return "文件夹路径存在错误!"
|
||||
'检查特殊字符
|
||||
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
|
||||
Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
|
||||
If Not CharactCheck = "" Then Return CharactCheck
|
||||
'检查头部空格
|
||||
If SubStr.StartsWithF(" ") Then Return "文件夹名不能以空格开头!"
|
||||
|
||||
@@ -97,7 +97,7 @@ Public Class MyBitmap
|
||||
Throw New Exception($"加载 MyBitmap 意外失败({FilePathOrResourceName})", ex)
|
||||
End If
|
||||
Else
|
||||
Log(ex, $"指定类型有误的 MyBitmap 加载({FilePathOrResourceName})", LogLevel.Developer)
|
||||
Log(ex, $"指定类型有误的 MyBitmap 加载({FilePathOrResourceName})", NotifyLevel.DevelopOnly)
|
||||
Exit Try
|
||||
End If
|
||||
End Try
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
''' <summary>
|
||||
''' 社区资源的类型。
|
||||
''' </summary>
|
||||
<Flags> Public Enum ResourceTypes
|
||||
''' <summary>
|
||||
''' Mod。
|
||||
''' </summary>
|
||||
[Mod] = 1
|
||||
''' <summary>
|
||||
''' 整合包。
|
||||
''' </summary>
|
||||
ModPack = 2
|
||||
''' <summary>
|
||||
''' 资源包。
|
||||
''' </summary>
|
||||
ResourcePack = 4
|
||||
''' <summary>
|
||||
''' 光影包。
|
||||
''' </summary>
|
||||
Shader = 8
|
||||
''' <summary>
|
||||
''' 数据包。
|
||||
''' </summary>
|
||||
DataPack = 16
|
||||
''' <summary>
|
||||
''' 服务端插件。
|
||||
''' </summary>
|
||||
Plugin = 32
|
||||
''' <summary>
|
||||
''' 同时包含数据包以及 Mod。
|
||||
''' </summary>
|
||||
ModOrDataPack = [Mod] Or DataPack
|
||||
''' <summary>
|
||||
''' 允许任意种类,或种类未知。
|
||||
''' </summary>
|
||||
Any = [Mod] Or ModPack Or ResourcePack Or Shader Or DataPack Or Plugin
|
||||
End Enum
|
||||
|
||||
''' <summary>
|
||||
''' 社区资源的来源平台。
|
||||
''' </summary>
|
||||
<Flags> Public Enum ResourcePlatforms
|
||||
CurseForge = 1
|
||||
Modrinth = 2
|
||||
Any = CurseForge Or Modrinth
|
||||
End Enum
|
||||
|
||||
''' <summary>
|
||||
''' Mod 加载器的类型。
|
||||
''' </summary>
|
||||
Public Enum ModLoaderTypes
|
||||
'https://docs.curseforge.com/rest-api/#tocS_ModLoaderType
|
||||
Any = 0
|
||||
Forge = 1
|
||||
LiteLoader = 3
|
||||
Fabric = 4
|
||||
Quilt = 5
|
||||
NeoForge = 6
|
||||
End Enum
|
||||
|
||||
''' <summary>
|
||||
''' 在爱发电中的赞助等级。
|
||||
''' </summary>
|
||||
Public Enum DonationRanks
|
||||
None = 0
|
||||
Rank6 = 6
|
||||
Rank12 = 12
|
||||
Rank23 = 23
|
||||
Rank54 = 54
|
||||
Rank98 = 98
|
||||
End Enum
|
||||
|
||||
Public Module Constants
|
||||
''' <summary>
|
||||
''' 土豆码版本号。
|
||||
''' </summary>
|
||||
Public Const POTATO_VERSION As Char = "1"c
|
||||
End Module
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
Me.TargetInstance = TargetInstance
|
||||
'构建文件结构
|
||||
TempFolder = RequestTaskTempFolder()
|
||||
Directory.CreateDirectory(TempFolder & "Temp\")
|
||||
Directory.CreateDirectory(TempFolder & "Report\")
|
||||
DirectoryUtils.Create(TempFolder & "Temp\")
|
||||
DirectoryUtils.Create(TempFolder & "Report\")
|
||||
Log("[Crash] 崩溃分析暂存文件夹:" & TempFolder)
|
||||
End Sub
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
If LatestLog IsNot Nothing AndAlso LatestLog.Any Then
|
||||
Dim RawOutput As String = LatestLog.Join(vbCrLf)
|
||||
Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
|
||||
WriteFile(TempFolder & "RawOutput.log", RawOutput)
|
||||
FileUtils.Write(TempFolder & "RawOutput.log", RawOutput)
|
||||
AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TempFolder & "RawOutput.log", LatestLog.ToArray))
|
||||
LatestLog.Clear()
|
||||
End If
|
||||
@@ -573,7 +573,7 @@ Done:
|
||||
'Mod 导致的崩溃
|
||||
If LogCrash.Contains("-- MOD ") Then
|
||||
Dim LogCrashMod As String = LogCrash.Between("-- MOD ", "Failure message:")
|
||||
If LogCrashMod.ContainsF(".jar", True) Then
|
||||
If LogCrashMod.ContainsIgnoreCase(".jar") Then
|
||||
AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrashMod, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
|
||||
Else
|
||||
AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
|
||||
@@ -764,7 +764,7 @@ NextStack:
|
||||
'[Fabric] 获取所有包含 Mod 信息的行
|
||||
Dim ModNameLines As New List(Of String)
|
||||
For Each Line In Details.Split(vbLf)
|
||||
If (Line.ContainsF(".jar", True) AndAlso Line.Length - Line.Replace(".jar", "").Length = 4) OrElse '只有一个 .jar
|
||||
If (Line.ContainsIgnoreCase(".jar") AndAlso Line.Length - Line.Replace(".jar", "").Length = 4) OrElse '只有一个 .jar
|
||||
(IsFabricDetail AndAlso Line.StartsWithF(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
|
||||
Next
|
||||
Log("[Crash] 崩溃报告中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
|
||||
@@ -874,7 +874,7 @@ NextStack:
|
||||
StartProcess(DirectFile.Value.Key)
|
||||
Else
|
||||
Dim FilePath As String = PathTemp & "Crash.txt"
|
||||
WriteFile(FilePath, DirectFile.Value.Value.Join(vbCrLf))
|
||||
FileUtils.Write(FilePath, DirectFile.Value.Value.Join(vbCrLf))
|
||||
StartProcess(FilePath)
|
||||
End If
|
||||
End Sub))
|
||||
@@ -885,8 +885,8 @@ NextStack:
|
||||
'获取文件路径
|
||||
RunInUiWait(Sub() FileAddress = SelectSaveFile("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
|
||||
If String.IsNullOrEmpty(FileAddress) Then Return
|
||||
Directory.CreateDirectory(GetPathFromFullPath(FileAddress))
|
||||
If File.Exists(FileAddress) Then File.Delete(FileAddress)
|
||||
DirectoryUtils.Create(FileAddress, isFilePath:=True)
|
||||
'输出诊断信息
|
||||
FeedbackInfo()
|
||||
LogFlush()
|
||||
@@ -909,7 +909,7 @@ NextStack:
|
||||
If FileEncoding Is Nothing Then FileEncoding = GetEncoding(ReadFileBytes(OutputFile))
|
||||
Dim FileContent As String = ReadFile(OutputFile, FileEncoding)
|
||||
FileContent = FilterUserName(FilterAccessToken(FileContent, If(FileName = "启动脚本.bat", "F", "*")), "*")
|
||||
WriteFile(TempFolder & "Report\" & FileName, FileContent, Encoding:=FileEncoding)
|
||||
FileUtils.Write(TempFolder & "Report\" & FileName, FileContent, Encoding:=FileEncoding)
|
||||
Log($"[Crash] 导出文件:{FileName},编码:{FileEncoding.HeaderName}")
|
||||
End If
|
||||
Next
|
||||
@@ -918,7 +918,7 @@ NextStack:
|
||||
DeleteDirectory(TempFolder & "Report\")
|
||||
Hint("错误报告已导出!", HintType.Green)
|
||||
Catch ex As Exception
|
||||
Log(ex, "导出错误报告失败", LogLevel.Feedback)
|
||||
Log(ex, "导出错误报告失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
Return
|
||||
End Try
|
||||
OpenExplorer(FileAddress)
|
||||
@@ -1117,7 +1117,7 @@ NextStack:
|
||||
Replace("\n", vbCrLf).
|
||||
Replace("\h", "").
|
||||
Replace("\e", If(IsHandAnalyze, "", vbCrLf & "你可以查看错误报告了解错误具体是如何发生的。")).
|
||||
Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf).
|
||||
ReplaceLineEndings(vbCrLf).
|
||||
Trim(vbCrLf.ToCharArray) &
|
||||
If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
|
||||
vbCrLf & "如果要寻求帮助,请把错误报告文件发给对方,而不是发送这个窗口的照片或者截图。" &
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
Dim IndexFile = DlClientAssetIndexGet(Instance)
|
||||
If IndexFile Is Nothing Then
|
||||
Task.Output = New List(Of NetFile)
|
||||
Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug)
|
||||
Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", NotifyLevel.DebugModeOnly)
|
||||
ElseIf AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
|
||||
Task.Output = New List(Of NetFile)
|
||||
Else
|
||||
@@ -96,7 +96,7 @@
|
||||
Sub(Task As LoaderTask(Of String, List(Of NetFile)))
|
||||
Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Instance)
|
||||
If BackAssetsFile Is Nothing Then
|
||||
Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug)
|
||||
Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", NotifyLevel.DebugModeOnly)
|
||||
Task.Interrupt()
|
||||
Throw New ThreadInterruptedException
|
||||
End If
|
||||
@@ -313,7 +313,7 @@
|
||||
For Each Version As JObject In DlClientListLoader.Output.Value("versions")
|
||||
If Version("id") = Id Then Return Version("url").ToString
|
||||
Next
|
||||
Log($"未发现版本 {Id} 的 json 下载地址,版本列表返回为:{vbCrLf}{DlClientListLoader.Output.Value.ToString}", LogLevel.Debug)
|
||||
Log($"未发现版本 {Id} 的 json 下载地址,版本列表返回为:{vbCrLf}{DlClientListLoader.Output.Value.ToString}", NotifyLevel.DebugModeOnly)
|
||||
Return Nothing
|
||||
Catch ex As Exception
|
||||
Log(ex, $"获取版本 {Id} 的 json 下载地址失败")
|
||||
@@ -426,9 +426,9 @@
|
||||
Dim Entry As New DlOptiFineListEntry With {
|
||||
.DisplayName = Name(i).Replace("HD U ", "").Replace(".0 ", " "),
|
||||
.ReleaseTime = {ReleaseTime(i).Split(".")(2), ReleaseTime(i).Split(".")(1), ReleaseTime(i).Split(".")(0)}.Join("/"c),
|
||||
.IsPreview = Name(i).ContainsF("pre", True),
|
||||
.IsPreview = Name(i).ContainsIgnoreCase("pre"),
|
||||
.Inherit = Name(i).ToString.Split(" ")(0),
|
||||
.FileName = If(Name(i).ContainsF("pre", True), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
|
||||
.FileName = If(Name(i).ContainsIgnoreCase("pre"), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
|
||||
.RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
|
||||
If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
|
||||
Entry.InstanceName = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
|
||||
@@ -452,7 +452,7 @@
|
||||
Dim Entry As New DlOptiFineListEntry With {
|
||||
.DisplayName = (Token("mcversion").ToString & Token("type").ToString.Replace("HD_U", "").Replace("_", " ") & " " & Token("patch").ToString).Replace(".0 ", " "),
|
||||
.ReleaseTime = "",
|
||||
.IsPreview = Token("patch").ToString.ContainsF("pre", True),
|
||||
.IsPreview = Token("patch").ToString.ContainsIgnoreCase("pre"),
|
||||
.Inherit = Token("mcversion").ToString,
|
||||
.FileName = Token("filename").ToString,
|
||||
.RequiredForgeVersion = If(Token("forge"), "").ToString.Replace("Forge ", "").Replace("#", "")
|
||||
@@ -679,7 +679,7 @@
|
||||
Dim Versions As New List(Of DlForgeVersionEntry)
|
||||
Try
|
||||
'分割版本信息
|
||||
Dim VersionCodes = Mid(Result, 1, Result.LastIndexOfF("</table>")).Split("<td class=""download-version")
|
||||
Dim VersionCodes = Result.Substring(0, Result.LastIndexOfF("</table>")).Split("<td class=""download-version")
|
||||
'获取所有版本信息
|
||||
For i = 1 To VersionCodes.Count - 1
|
||||
Dim VersionCode = VersionCodes(i)
|
||||
@@ -720,7 +720,7 @@
|
||||
Continue For
|
||||
End If
|
||||
'添加进列表
|
||||
Versions.Add(New DlForgeVersionEntry(Name, Branch, Inherit) With {.Category = Category, .IsRecommended = IsRecommended, .Hash = MD5.Trim(vbCr, vbLf), .ReleaseTime = ReleaseTime})
|
||||
Versions.Add(New DlForgeVersionEntry(Name, Branch, Inherit) With {.Category = Category, .IsRecommended = IsRecommended, .Hash = MD5.ReplaceLineEndings(""), .ReleaseTime = ReleaseTime})
|
||||
Catch ex As Exception
|
||||
Throw New Exception("Forge 官方源版本信息提取失败(" & VersionCode & ")", ex)
|
||||
End Try
|
||||
@@ -846,7 +846,7 @@
|
||||
Public Sub New(ApiName As String)
|
||||
IsNeoForge = True
|
||||
Me.ApiName = ApiName
|
||||
IsBeta = ApiName.ContainsF("beta", True) OrElse ApiName.ContainsF("alpha", True)
|
||||
IsBeta = ApiName.ContainsIgnoreCase("beta") OrElse ApiName.ContainsIgnoreCase("alpha")
|
||||
If ApiName.Contains("1.20.1") Then '1.20.1-47.1.99
|
||||
VersionName = ApiName.Replace("1.20.1-", "")
|
||||
Version = New Version("19." & VersionName)
|
||||
@@ -1151,14 +1151,14 @@
|
||||
''' <summary>
|
||||
''' Fabric API 列表,官方源。
|
||||
''' </summary>
|
||||
Public DlFabricApiLoader As New LoaderTask(Of Integer, List(Of CompFile))("Fabric API List Loader",
|
||||
Sub(Task As LoaderTask(Of Integer, List(Of CompFile))) Task.Output = CompFilesGet("fabric-api", False))
|
||||
Public DlFabricApiLoader As New LoaderTask(Of Integer, List(Of ResourceVersion))("Fabric API List Loader",
|
||||
Sub(Task As LoaderTask(Of Integer, List(Of ResourceVersion))) Task.Output = ResourceVersion.FromProjectId("fabric-api", ResourcePlatforms.Modrinth, LoadDependencies:=False))
|
||||
|
||||
''' <summary>
|
||||
''' OptiFabric 列表,官方源。
|
||||
''' </summary>
|
||||
Public DlOptiFabricLoader As New LoaderTask(Of Integer, List(Of CompFile))("OptiFabric List Loader",
|
||||
Sub(Task As LoaderTask(Of Integer, List(Of CompFile))) Task.Output = CompFilesGet("322385", True))
|
||||
Public DlOptiFabricLoader As New LoaderTask(Of Integer, List(Of ResourceVersion))("OptiFabric List Loader",
|
||||
Sub(Task As LoaderTask(Of Integer, List(Of ResourceVersion))) Task.Output = ResourceVersion.FromProjectId("322385", ResourcePlatforms.CurseForge, LoadDependencies:=False))
|
||||
|
||||
#End Region
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
Public ReadOnly Property HasEnvironment As Boolean
|
||||
Get
|
||||
If PathFolder Is Nothing OrElse PathEnv Is Nothing Then Return False
|
||||
Return PathEnv.Replace("\", "").Replace("/", "").ContainsF(PathFolder.Replace("\", ""), True)
|
||||
Return PathEnv.Replace("\", "").Replace("/", "").ContainsIgnoreCase(PathFolder.Replace("\", ""))
|
||||
End Get
|
||||
End Property
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
''' </summary>
|
||||
Public Overrides Function ToString() As String
|
||||
Dim VersionString = Version.ToString
|
||||
If VersionString.StartsWithF("1.") Then VersionString = Mid(VersionString, 3)
|
||||
If VersionString.StartsWithF("1.") Then VersionString = VersionString.Substring(2)
|
||||
Return If(IsJre, "JRE ", "JDK ") & MajorVersion & " (" & VersionString & ")" & If(Is64Bit, "", ",32 位") & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
|
||||
End Function
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
Try
|
||||
'确定文件存在
|
||||
If Not File.Exists(PathJava) Then Throw New FileNotFoundException("未找到 java.exe 文件", PathJava)
|
||||
If {"finalshell", "Paranoia File"}.Any(Function(n) PathJava.ContainsF(n, True)) Then Throw New Exception("不兼容该精简版 Java") '#2249、#8080
|
||||
If {"finalshell", "Paranoia File"}.Any(Function(n) PathJava.ContainsIgnoreCase(n)) Then Throw New Exception("不兼容该精简版 Java") '#2249、#8080
|
||||
If File.Exists(PathFolder & "pdf-bookmark") Then Throw New Exception("不兼容 PDF Bookmark 的 Java") '#5326
|
||||
IsJre = Not File.Exists(PathFolder & "javac.exe")
|
||||
'运行 -version
|
||||
@@ -176,14 +176,14 @@
|
||||
Next
|
||||
End If
|
||||
If Not JavaList.Any() Then
|
||||
Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", LogLevel.Developer)
|
||||
Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", NotifyLevel.DevelopOnly)
|
||||
JavaSearchLoader.Start(0)
|
||||
Else
|
||||
Log("[Java] 缓存中有 " & JavaList.Count & " 个可用的 Java:")
|
||||
JavaList.ForEach(Sub(j) Log($"[Java] - {j}"))
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "初始化 Java 列表失败", LogLevel.Feedback)
|
||||
Log(ex, "初始化 Java 列表失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
Settings.Set("LaunchArgumentJavaAll", "[]")
|
||||
End Try
|
||||
End Sub
|
||||
@@ -242,7 +242,7 @@
|
||||
Catch ex As Exception
|
||||
UserJava = Nothing
|
||||
Settings.Reset("VersionArgumentJavaSelect", Instance:=GameInstance)
|
||||
Log(ex, "版本独立设置中指定的 Java 已无法使用,此设置已重置", LogLevel.Hint)
|
||||
Log(ex, "版本独立设置中指定的 Java 已无法使用,此设置已重置", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End If
|
||||
End If
|
||||
@@ -257,7 +257,7 @@
|
||||
Catch ex As Exception
|
||||
UserJava = Nothing
|
||||
Settings.Reset("LaunchArgumentJavaSelect")
|
||||
Log(ex, "全局设置中指定的 Java 已无法使用,此设置已重置", LogLevel.Hint)
|
||||
Log(ex, "全局设置中指定的 Java 已无法使用,此设置已重置", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End If
|
||||
|
||||
@@ -332,7 +332,7 @@ RetryGet:
|
||||
Dim JavaCurrent As String = UserJava.MajorVersion & If(ShowRevision, "." & UserJava.Version.MajorRevision & "." & UserJava.Version.MinorRevision, "")
|
||||
If GameInstance IsNot Nothing AndAlso Settings.Get("VersionAdvanceJava", GameInstance) Then
|
||||
'直接跳过弹窗
|
||||
Log("[Java] 设置中指定了使用 Java " & JavaCurrent & ",但当前版本" & Requirement & ",这可能会导致游戏崩溃!", LogLevel.Debug)
|
||||
Log("[Java] 设置中指定了使用 Java " & JavaCurrent & ",但当前版本" & Requirement & ",这可能会导致游戏崩溃!", NotifyLevel.DebugModeOnly)
|
||||
AllowedJavaList = New List(Of JavaEntry) From {UserJava}
|
||||
Else
|
||||
Select Case MyMsgBox("你在设置中手动指定了使用 Java " & JavaCurrent & ",但当前" & Requirement & "。" & vbCrLf &
|
||||
@@ -400,7 +400,7 @@ UserPass:
|
||||
Return Nothing
|
||||
Catch ex As Exception
|
||||
If ex.Message = "$$" Then Throw ex
|
||||
Log(ex, "查找符合条件的 Java 失败", LogLevel.Feedback)
|
||||
Log(ex, "查找符合条件的 Java 失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
Return Nothing
|
||||
End Try
|
||||
End Function
|
||||
@@ -435,7 +435,7 @@ NoUserJava:
|
||||
Next
|
||||
Return False
|
||||
Catch ex As Exception
|
||||
Log(ex, "检查 Java 类别时出错", LogLevel.Feedback)
|
||||
Log(ex, "检查 Java 类别时出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
Settings.Set("LaunchArgumentJavaSelect", "")
|
||||
Return True
|
||||
End Try
|
||||
@@ -444,7 +444,7 @@ NoUserJava:
|
||||
''' 将 Java 按照适用性排序。
|
||||
''' </summary>
|
||||
Public Function JavaSorter(Left As JavaEntry, Right As JavaEntry) As Boolean
|
||||
Dim PathInfo As New DirectoryInfo(Path)
|
||||
Dim PathInfo As New DirectoryInfo(PathExeFolder)
|
||||
Dim PathMcInfo As New DirectoryInfo(McFolderSelected)
|
||||
'1. 尽量在当前文件夹或当前 Minecraft 文件夹
|
||||
Dim ProgramPathParent As String, MinecraftPathParent As String = ""
|
||||
@@ -522,9 +522,9 @@ NoUserJava:
|
||||
JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.jdks\", JavaPreList, False, IsFullSearch:=True)
|
||||
JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.sdkman\candidates\java\", JavaPreList, False, IsFullSearch:=True)
|
||||
'查找启动器目录中的 Java
|
||||
JavaSearchFolder(Path, JavaPreList, False, IsFullSearch:=True)
|
||||
JavaSearchFolder(PathExeFolder, JavaPreList, False, IsFullSearch:=True)
|
||||
'查找所选 Minecraft 文件夹中的 Java
|
||||
If Not String.IsNullOrWhiteSpace(McFolderSelected) AndAlso Path <> McFolderSelected Then JavaSearchFolder(McFolderSelected, JavaPreList, False, IsFullSearch:=True)
|
||||
If Not String.IsNullOrWhiteSpace(McFolderSelected) AndAlso PathExeFolder <> McFolderSelected Then JavaSearchFolder(McFolderSelected, JavaPreList, False, IsFullSearch:=True)
|
||||
|
||||
'若不全为符号链接,则清除符号链接的地址
|
||||
Dim JavaWithoutReparse As New Dictionary(Of String, Boolean)
|
||||
@@ -546,7 +546,7 @@ NoUserJava:
|
||||
'若不全为特殊引用,则清除特殊引用的地址
|
||||
Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
|
||||
For Each Pair In JavaPreList
|
||||
If Pair.Key.Contains("java8path_target_") OrElse Pair.Key.Contains("javapath_target_") OrElse Pair.Key.Contains("javatmp") OrElse Pair.Key.ContainsF("system32") Then
|
||||
If Pair.Key.Contains("java8path_target_") OrElse Pair.Key.Contains("javapath_target_") OrElse Pair.Key.Contains("javatmp") OrElse Pair.Key.Contains("system32") Then
|
||||
Log("[Java] 位于 " & Pair.Key & " 的 Java 位于特殊路径,不应优先使用")
|
||||
Else
|
||||
JavaWithoutInherit.Add(Pair.Key, Pair.Value)
|
||||
@@ -565,7 +565,7 @@ NoUserJava:
|
||||
If Entry.IsUserImport Then JavaPreList(Entry.PathFolder) = True
|
||||
Next
|
||||
Catch ex As Exception
|
||||
Log(ex, "Java 列表已损坏", LogLevel.Feedback)
|
||||
Log(ex, "Java 列表已损坏", NotifyLevel.MsgBoxAndFeedback)
|
||||
Settings.Set("LaunchArgumentJavaAll", "[]")
|
||||
End Try
|
||||
|
||||
@@ -573,7 +573,7 @@ NoUserJava:
|
||||
|
||||
'确保可用并获取详细信息,转入正式列表
|
||||
Dim NewJavaList As New List(Of JavaEntry)
|
||||
For Each Entry In JavaPreList.Distinct(Function(a, b) a.Key.Lower = b.Key.Lower) '#794
|
||||
For Each Entry In JavaPreList.ToList.DistinctBy(Function(a) a.Key.Lower) '#794
|
||||
NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
|
||||
Next
|
||||
NewJavaList = JavaCheckList(NewJavaList).SortByComparison(AddressOf JavaSorter)
|
||||
@@ -587,7 +587,7 @@ NoUserJava:
|
||||
JavaList = NewJavaList
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "搜索 Java 时出错", LogLevel.Feedback)
|
||||
Log(ex, "搜索 Java 时出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
JavaList = New List(Of JavaEntry)
|
||||
End Try
|
||||
|
||||
@@ -618,7 +618,7 @@ NoUserJava:
|
||||
Catch ex As ThreadInterruptedException
|
||||
Catch ex As Exception
|
||||
If Entry.IsUserImport Then
|
||||
Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", LogLevel.Hint)
|
||||
Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", NotifyLevel.AllUsers)
|
||||
Else
|
||||
Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
|
||||
End If
|
||||
@@ -659,10 +659,10 @@ Wait:
|
||||
Try
|
||||
'确认目录存在
|
||||
If Not OriginalPath.Exists Then Return
|
||||
Dim Path As String = OriginalPath.FullName.Replace("\\", "\")
|
||||
If Not Path.EndsWithF("\") Then Path += "\"
|
||||
Dim Folder As String = OriginalPath.FullName.Replace("\\", "\")
|
||||
If Not Folder.EndsWithF("\") Then Folder += "\"
|
||||
'若该目录有 Java,则加入结果
|
||||
If File.Exists(Path & "javaw.exe") Then Results(Path) = Source
|
||||
If File.Exists(Folder & "javaw.exe") Then Results(Folder) = Source
|
||||
'查找其下的所有文件夹
|
||||
'不应使用网易的 Java:https://github.com/Meloong-Git/PCL/issues/1279#issuecomment-2761489121
|
||||
Static Keywords As String() = {
|
||||
@@ -704,7 +704,7 @@ Wait:
|
||||
AddHandler JavaDownloadLoader.OnStateChangedThread,
|
||||
Sub(Raw As LoaderBase, NewState As LoadState, OldState As LoadState)
|
||||
If (NewState = LoadState.Failed OrElse NewState = LoadState.Interrupted) AndAlso LastJavaBaseDir IsNot Nothing Then
|
||||
Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", LogLevel.Debug)
|
||||
Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", NotifyLevel.DebugModeOnly)
|
||||
DeleteDirectory(LastJavaBaseDir)
|
||||
ElseIf NewState = LoadState.Finished Then
|
||||
LastJavaBaseDir = Nothing
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
Imports System.IO.Compression
|
||||
|
||||
Public Module ModLaunch
|
||||
Public Module ModLaunch
|
||||
|
||||
#Region "开始"
|
||||
|
||||
@@ -186,7 +184,7 @@ NextInner:
|
||||
'没有特殊处理过的错误信息
|
||||
McLaunchLog("错误:" & ex.GetDetail())
|
||||
Log(ex,
|
||||
If(IsSavingBatch, "导出启动脚本失败", "Minecraft 启动失败"), LogLevel.Msgbox,
|
||||
If(IsSavingBatch, "导出启动脚本失败", "Minecraft 启动失败"), NotifyLevel.MsgBox,
|
||||
If(IsSavingBatch, "导出启动脚本失败", "启动失败"))
|
||||
'上报
|
||||
If Not IsSavingBatch Then
|
||||
@@ -239,9 +237,8 @@ NextInner:
|
||||
Dim CheckResult As String = ""
|
||||
RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput()))
|
||||
If CheckResult <> "" Then Throw New ArgumentException(CheckResult)
|
||||
#If BETA Then
|
||||
'求赞助
|
||||
If CurrentLaunchOptions?.SaveBatch Is Nothing Then '保存脚本时不提示
|
||||
If BuildType = BuildTypes.Release AndAlso CurrentLaunchOptions?.SaveBatch Is Nothing Then '保存脚本时不提示
|
||||
RunInNewThread(
|
||||
Sub()
|
||||
Select Case Settings.Get("SystemLaunchCount")
|
||||
@@ -255,7 +252,6 @@ NextInner:
|
||||
End Select
|
||||
End Sub, "Donate")
|
||||
End If
|
||||
#End If
|
||||
'正版购买提示
|
||||
If CurrentLaunchOptions?.SaveBatch Is Nothing AndAlso '保存脚本时不提示
|
||||
Not Settings.Get("HintBuy") AndAlso Settings.Get("LoginType") <> McLoginType.Ms Then
|
||||
@@ -496,7 +492,7 @@ NextInner:
|
||||
End If
|
||||
End Select
|
||||
Catch ex As Exception
|
||||
Log(ex, "获取登录输入信息失败(" & GetStringFromEnum(LoginType) & ")", LogLevel.Feedback)
|
||||
Log(ex, "获取登录输入信息失败(" & GetStringFromEnum(LoginType) & ")", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
Return LoginData
|
||||
End Function
|
||||
@@ -665,7 +661,7 @@ LoginFinish:
|
||||
Settings.Set("Login" & Input.Token & "Email", Emails.Join("¨"c))
|
||||
Settings.Set("Login" & Input.Token & "Pass", Passwords.Join("¨"c))
|
||||
Catch ex As Exception
|
||||
Log(ex, "保存启动记录失败", LogLevel.Hint)
|
||||
Log(ex, "保存启动记录失败", NotifyLevel.AllUsers)
|
||||
Settings.Set("Login" & Input.Token & "Email", "")
|
||||
Settings.Set("Login" & Input.Token & "Pass", "")
|
||||
End Try
|
||||
@@ -817,17 +813,17 @@ LoginFinish:
|
||||
Return NeedRefresh
|
||||
Catch ex As Exception
|
||||
Dim AllMessage As String = ex.GetBrief()
|
||||
Log(ex, "登录失败原始错误信息", LogLevel.Normal)
|
||||
Log(ex, "登录失败原始错误信息", NotifyLevel.Silent)
|
||||
'读取服务器返回的错误
|
||||
If TypeOf ex Is ResponsedWebException Then
|
||||
Dim ErrorMessage As String = Nothing
|
||||
Try
|
||||
Dim Response = DirectCast(ex, ResponsedWebException).Response
|
||||
If Response.ContainsF("errorMessage", True) Then ErrorMessage = GetJson(Response)("errorMessage").ToString()
|
||||
If Response.ContainsIgnoreCase("errorMessage") Then ErrorMessage = GetJson(Response)("errorMessage").ToString()
|
||||
Catch
|
||||
End Try
|
||||
If Not String.IsNullOrWhiteSpace(ErrorMessage) Then
|
||||
If ErrorMessage.Contains("密码错误") OrElse ErrorMessage.ContainsF("Incorrect username or password", True) Then
|
||||
If ErrorMessage.Contains("密码错误") OrElse ErrorMessage.ContainsIgnoreCase("Incorrect username or password") Then
|
||||
'密码错误,退出登录 (#5090)
|
||||
McLaunchLog("密码错误,退出登录")
|
||||
Select Case Data.Input.Type
|
||||
@@ -905,15 +901,15 @@ Retry:
|
||||
Dim Result As String
|
||||
Try
|
||||
Result = NetRequestByClientMultiple("https://login.live.com/oauth20_token.srf", HttpMethod.Post,
|
||||
Content:=$"client_id={OAuthClientId}&refresh_token={Uri.EscapeDataString(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access",
|
||||
Content:=$"client_id={OAuthClientId}&refresh_token={EscapeUtils.FormUrlEscape(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access",
|
||||
ContentType:="application/x-www-form-urlencoded",
|
||||
Headers:={{"Accept-Language", "en-US,en;q=0.5"}, {"X-Requested-With", "XMLHttpRequest"}},
|
||||
ThreadCount:=2)
|
||||
Catch ex As ResponsedWebException
|
||||
'修改错误列表时,同时检查 MyMsgLogin.xaml.vb 中的对应代码
|
||||
Dim Response = ex.Response
|
||||
If Response.ContainsF("must sign in again", True) OrElse Response.ContainsF("password expired", True) OrElse
|
||||
(Response.Contains("refresh_token") AndAlso Response.Contains("is not valid")) Then '#269
|
||||
If Response.ContainsIgnoreCase("must sign in again") OrElse Response.ContainsIgnoreCase("password expired") OrElse
|
||||
(Response.Contains("refresh_token") AndAlso Response.ContainsIgnoreCase("is not valid")) Then '#269
|
||||
Return {"Relogin", ""}
|
||||
ElseIf Response.Contains("Account security interrupt") Then
|
||||
MyMsgBox("该账号由于安全问题无法登陆,请前往微软账户页获取更多信息。", "登录失败", "我知道了", IsWarn:=True)
|
||||
@@ -1105,7 +1101,7 @@ Retry:
|
||||
Uuid = McLoginMojangUuid(SkinName, False)
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "皮肤信息获取失败,游戏将以无皮肤的方式启动", LogLevel.Hint)
|
||||
Log(ex, "皮肤信息获取失败,游戏将以无皮肤的方式启动", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
Case 4
|
||||
'自定义
|
||||
@@ -1118,7 +1114,7 @@ Retry:
|
||||
End Function
|
||||
'根据用户名返回对应 UUID,需要多线程
|
||||
Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
|
||||
If Name.Trim.Length = 0 Then Return StrFill("", "0", 32)
|
||||
If Name.Trim.Length = 0 Then Return New String("0"c, 32)
|
||||
'从缓存获取
|
||||
Dim Uuid As String = ReadIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, "")
|
||||
If Len(Uuid) = 32 Then Return Uuid
|
||||
@@ -1141,7 +1137,7 @@ Retry:
|
||||
Return Uuid
|
||||
End Function
|
||||
Public Function McLoginLegacyUuid(Name As String)
|
||||
Dim FullUuid As String = StrFill(Name.Length.ToString("X"), "0", 16) & StrFill(GetHash(Name).ToString("X"), "0", 16)
|
||||
Dim FullUuid As String = Name.Length.ToString("X").EnsureLength("0", 16) & GetHash(Name).ToString("X").EnsureLength("0", 16)
|
||||
Return FullUuid.Substring(0, 12) & "3" & FullUuid.Substring(13, 3) & "9" & FullUuid.Substring(17, 15)
|
||||
End Function
|
||||
|
||||
@@ -1583,7 +1579,7 @@ NextInstance:
|
||||
McLaunchLog("发现错误的 OptiFineForge TweakClass,目前的游戏参数:" & Arg)
|
||||
Arg = Arg.Replace(" --tweakClass optifine.OptiFineTweaker", "") & " --tweakClass optifine.OptiFineForgeTweaker"
|
||||
Try
|
||||
WriteFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json", ReadFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
|
||||
FileUtils.Write(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json", ReadFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
|
||||
Catch ex As Exception
|
||||
Log(ex, "替换 OptiFineForge TweakClass 失败")
|
||||
End Try
|
||||
@@ -1627,15 +1623,15 @@ NextInstance:
|
||||
Private Iterator Function McLaunchArgumentsReplace(Arguments As List(Of String)) As IEnumerable(Of (Key As String, Value As String))
|
||||
'基础参数
|
||||
Yield ("${classpath_separator}", ";")
|
||||
Yield ("${natives_directory}", ShortenPath(GetNativesFolder()))
|
||||
Yield ("${library_directory}", ShortenPath(McFolderSelected & "libraries"))
|
||||
Yield ("${libraries_directory}", ShortenPath(McFolderSelected & "libraries"))
|
||||
Yield ("${natives_directory}", GetNativesFolder().TrimEnd("\"c))
|
||||
Yield ("${library_directory}", FileUtils.ShortenPath(McFolderSelected & "libraries"))
|
||||
Yield ("${libraries_directory}", FileUtils.ShortenPath(McFolderSelected & "libraries"))
|
||||
Yield ("${pure_directory}", PathPure.TrimEnd("\"c)) '由 PCL 添加,这会允许在分割参数并去重后再替换路径,防止路径中的特殊字符影响参数分割和去重
|
||||
Yield ("${launcher_name}", "PCL")
|
||||
Yield ("${launcher_version}", VersionCode)
|
||||
Yield ("${version_name}", McInstanceSelected.Name)
|
||||
Yield ("${game_directory}", ShortenPath(Left(McInstanceSelected.PathIndie, McInstanceSelected.PathIndie.Count - 1)))
|
||||
Yield ("${assets_root}", ShortenPath(McFolderSelected & "assets"))
|
||||
Yield ("${game_directory}", FileUtils.ShortenPath(Left(McInstanceSelected.PathIndie, McInstanceSelected.PathIndie.Count - 1)))
|
||||
Yield ("${assets_root}", FileUtils.ShortenPath(McFolderSelected & "assets"))
|
||||
Yield ("${user_properties}", "{}")
|
||||
Yield ("${auth_player_name}", McLoginLoader.Output.Name)
|
||||
Yield ("${auth_uuid}", McLoginLoader.Output.Uuid)
|
||||
@@ -1643,8 +1639,8 @@ NextInstance:
|
||||
Yield ("${access_token}", McLoginLoader.Output.AccessToken)
|
||||
Yield ("${auth_session}", McLoginLoader.Output.AccessToken)
|
||||
Yield ("${user_type}", "msa") '#1221
|
||||
Yield ("${primary_jar}", ShortenPath(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".jar")) '#6942
|
||||
Yield ("${game_assets}", ShortenPath(McFolderSelected & "assets\virtual\legacy")) '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
|
||||
Yield ("${primary_jar}", FileUtils.ShortenPath(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".jar")) '#6942
|
||||
Yield ("${game_assets}", FileUtils.ShortenPath(McFolderSelected & "assets\virtual\legacy")) '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
|
||||
Yield ("${assets_index_name}", McAssetsGetIndexName(McInstanceSelected))
|
||||
|
||||
'自定义信息
|
||||
@@ -1697,14 +1693,14 @@ NextInstance:
|
||||
End If
|
||||
Next
|
||||
If OptiFineCp IsNot Nothing Then Cps.Insert(Cps.Count - 2, OptiFineCp) 'OptiFine 的总是需要放到倒数第二位
|
||||
Yield ("${classpath}", Cps.Select(Function(c) ShortenPath(c)).Join(";"c))
|
||||
Yield ("${classpath}", Cps.Select(Function(c) FileUtils.ShortenPath(c)).Join(";"c))
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 分割 Java 参数字符串。
|
||||
''' </summary>
|
||||
Private Iterator Function SplitJavaArguments(Arg As String) As IEnumerable(Of String)
|
||||
Arg = Arg.Replace(vbLf, " ").Replace(vbCr, " ").Trim '预处理换行符
|
||||
Arg = Arg.ReplaceLineEndings(" ", mergeMultiple:=True).Trim '预处理换行符
|
||||
If String.IsNullOrWhiteSpace(Arg) Then Return
|
||||
Dim Current As New StringBuilder
|
||||
Dim InQuotes As Boolean = False
|
||||
@@ -1783,8 +1779,8 @@ NextInstance:
|
||||
Private Sub McLaunchNatives(Loader As LoaderTask(Of Integer, Integer))
|
||||
|
||||
'创建文件夹
|
||||
Dim Target As String = GetNativesFolder() & "\"
|
||||
Directory.CreateDirectory(Target)
|
||||
Dim Target As String = GetNativesFolder()
|
||||
DirectoryUtils.Create(Target)
|
||||
|
||||
'解压文件
|
||||
McLaunchLog("正在解压 Natives 文件")
|
||||
@@ -1821,7 +1817,7 @@ NextInstance:
|
||||
End Try
|
||||
End If
|
||||
'解压新文件
|
||||
WriteFile(FilePath, Entry.Open)
|
||||
FileUtils.Write(FilePath, Entry.Open)
|
||||
McLaunchLog("已解压:" & FilePath)
|
||||
End If
|
||||
Next
|
||||
@@ -1843,18 +1839,18 @@ NextInstance:
|
||||
|
||||
End Sub
|
||||
''' <summary>
|
||||
''' 获取 Natives 文件夹路径,不以 \ 结尾。
|
||||
''' 获取 Natives 文件夹路径,以 \ 结尾。
|
||||
''' </summary>
|
||||
Private Function GetNativesFolder() As String
|
||||
Dim Result As String = ShortenPath(McInstanceSelected.PathVersion) & McInstanceSelected.Name & "-natives"
|
||||
If Not (IsGBKEncoding OrElse Result.IsASCII()) Then
|
||||
Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives"
|
||||
If Not Result.IsASCII() Then
|
||||
Result = OsDrive & "ProgramData\PCL\natives"
|
||||
Dim Result As String = FileUtils.ShortenPath(McInstanceSelected.PathVersion) & McInstanceSelected.Name & "-natives\"
|
||||
If Not (IsGBKEncoding OrElse Result.IsAsciiOnly()) Then
|
||||
Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives\"
|
||||
If Not Result.IsAsciiOnly() Then
|
||||
Result = OsDrive & "ProgramData\PCL\natives\"
|
||||
End If
|
||||
End If
|
||||
Directory.CreateDirectory(Result) '提前创建,这样 ShortenPath 才有结果(否则在长路径下首次启动 Forge 会崩溃)
|
||||
Return ShortenPath(Result)
|
||||
DirectoryUtils.Create(Result) '提前创建,这样 DirectoryUtils.Shorten 才有结果(否则在长路径下首次启动 Forge 会崩溃)
|
||||
Return FileUtils.ShortenPath(Result)
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
@@ -1867,7 +1863,7 @@ NextInstance:
|
||||
If Settings.Get("LaunchAdvanceGraphicCard") Then
|
||||
Try
|
||||
SetGPUPreference(McLaunchJavaSelected.PathJava)
|
||||
SetGPUPreference(PathWithName)
|
||||
SetGPUPreference(PathExe)
|
||||
Catch ex As Exception
|
||||
If IsAdmin() Then
|
||||
Log(ex, "直接调整显卡设置失败")
|
||||
@@ -1880,7 +1876,7 @@ NextInstance:
|
||||
Throw New Exception("调整过程中出现异常")
|
||||
End If
|
||||
Catch exx As Exception
|
||||
Log(exx, "调整显卡设置失败,Minecraft 可能会使用默认显卡运行", LogLevel.Hint)
|
||||
Log(exx, "调整显卡设置失败,Minecraft 可能会使用默认显卡运行", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End If
|
||||
End Try
|
||||
@@ -1914,7 +1910,7 @@ NextInstance:
|
||||
'更新文件
|
||||
Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
|
||||
Profiles.Merge(ReplaceJson)
|
||||
WriteFile(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
FileUtils.Write(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
McLaunchLog("已更新 launcher_profiles.json")
|
||||
Catch ex As Exception
|
||||
Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试")
|
||||
@@ -1944,10 +1940,10 @@ NextInstance:
|
||||
'更新文件
|
||||
Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
|
||||
Profiles.Merge(ReplaceJson)
|
||||
WriteFile(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
FileUtils.Write(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
McLaunchLog("已在删除后更新 launcher_profiles.json")
|
||||
Catch exx As Exception
|
||||
Log(exx, "更新 launcher_profiles.json 失败", LogLevel.Feedback)
|
||||
Log(exx, "更新 launcher_profiles.json 失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Try
|
||||
|
||||
@@ -1997,7 +1993,7 @@ NextInstance:
|
||||
WriteIni(SetupFileAddress, "fullscreen", "false")
|
||||
End Select
|
||||
Catch ex As Exception
|
||||
Log(ex, "更新 options.txt 失败", LogLevel.Hint)
|
||||
Log(ex, "更新 options.txt 失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
|
||||
'离线皮肤 Alex 警告
|
||||
@@ -2010,11 +2006,11 @@ NextInstance:
|
||||
|
||||
'离线皮肤资源包
|
||||
Try
|
||||
Directory.CreateDirectory(McInstanceSelected.PathIndie & "resourcepacks\")
|
||||
Dim ZipFileAddress As String = McInstanceSelected.PathIndie & "resourcepacks\PCL2 Skin.zip"
|
||||
DirectoryUtils.Create(ZipFileAddress, isFilePath:=True)
|
||||
Dim NewTypeSetup As Boolean = McInstanceSelected.Version.Vanilla.Major >= 13 OrElse McInstanceSelected.Version.Vanilla.Major < 6
|
||||
If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Settings.Get("LaunchSkinType") = 4 AndAlso File.Exists(PathAppdata & "CustomSkin.png") Then
|
||||
Directory.CreateDirectory(PathTemp)
|
||||
DirectoryUtils.Create(PathTemp)
|
||||
Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
|
||||
Dim PackPicAddress As String = PathTemp & "pack.png"
|
||||
Dim PackFormat As Integer
|
||||
@@ -2063,13 +2059,13 @@ NextInstance:
|
||||
'准备文件
|
||||
Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
|
||||
Bit.Save(PackPicAddress)
|
||||
WriteFile(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL 自定义离线皮肤资源包""}}")
|
||||
FileUtils.Write(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL 自定义离线皮肤资源包""}}")
|
||||
Dim Skin As New MyBitmap(PathAppdata & "CustomSkin.png")
|
||||
If (McInstanceSelected.Version.Vanilla.Major = 6 OrElse McInstanceSelected.Version.Vanilla.Major = 7) AndAlso Skin.Pic.Height = 64 Then
|
||||
McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
|
||||
Skin = Skin.Clip(0, 0, 64, 32)
|
||||
End If
|
||||
Skin.Save(Path & "PCL\CustomSkin_Cliped.png")
|
||||
Skin.Save(PathExeFolder & "PCL\CustomSkin_Cliped.png")
|
||||
'构建压缩文件
|
||||
Using ZipFile As New FileStream(ZipFileAddress, FileMode.Create)
|
||||
Using ZipAr As New ZipArchive(ZipFile, ZipArchiveMode.Create)
|
||||
@@ -2078,15 +2074,15 @@ NextInstance:
|
||||
'1.19.3+ 使用复杂版本的替换
|
||||
If McInstanceSelected.Version.Vanilla >= New Version(19, 0, 3) Then
|
||||
For Each SkinName In {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"}
|
||||
ZipAr.CreateEntryFromFile(Path & "PCL\CustomSkin_Cliped.png",
|
||||
ZipAr.CreateEntryFromFile(PathExeFolder & "PCL\CustomSkin_Cliped.png",
|
||||
$"assets/minecraft/textures/entity/player/{If(Settings.Get("LaunchSkinSlim"), "slim", "wide")}/{SkinName}.png")
|
||||
Next
|
||||
Else
|
||||
ZipAr.CreateEntryFromFile(Path & "PCL\CustomSkin_Cliped.png", "assets/minecraft/textures/entity/" & If(Settings.Get("LaunchSkinSlim"), "alex.png", "steve.png"))
|
||||
ZipAr.CreateEntryFromFile(PathExeFolder & "PCL\CustomSkin_Cliped.png", "assets/minecraft/textures/entity/" & If(Settings.Get("LaunchSkinSlim"), "alex.png", "steve.png"))
|
||||
End If
|
||||
End Using
|
||||
End Using
|
||||
File.Delete(Path & "PCL\CustomSkin_Cliped.png")
|
||||
File.Delete(PathExeFolder & "PCL\CustomSkin_Cliped.png")
|
||||
'更改设置文件
|
||||
IniClearCache(SetupFileAddress)
|
||||
Dim EnabledResourcePack As String = ReadIni(SetupFileAddress, "resourcePacks", "[]").TrimStart("[").TrimEnd("]")
|
||||
@@ -2132,7 +2128,7 @@ IgnoreCustomSkin:
|
||||
End If
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "离线皮肤资源包设置失败", LogLevel.Hint)
|
||||
Log(ex, "离线皮肤资源包设置失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
|
||||
End Sub
|
||||
@@ -2151,13 +2147,13 @@ IgnoreCustomSkin:
|
||||
"@echo off" & vbCrLf &
|
||||
$"title 启动 - {McInstanceSelected.Name}" & vbCrLf &
|
||||
"echo 游戏正在启动,请稍候。" & vbCrLf &
|
||||
$"cd /D ""{ShortenPath(McInstanceSelected.PathIndie)}""" & vbCrLf &
|
||||
$"cd /D ""{FileUtils.ShortenPath(McInstanceSelected.PathIndie)}""" & vbCrLf &
|
||||
CustomCommandGlobal & vbCrLf &
|
||||
CustomCommandVersion & vbCrLf &
|
||||
$"""{McLaunchJavaSelected.PathJava}"" {McLaunchArgument}" & vbCrLf &
|
||||
"echo 游戏已退出。" & vbCrLf &
|
||||
"pause"
|
||||
WriteFile(If(CurrentLaunchOptions.SaveBatch, Path & "PCL\LatestLaunch.bat"), FilterAccessToken(CmdString, "F"),
|
||||
FileUtils.Write(If(CurrentLaunchOptions.SaveBatch, PathExeFolder & "PCL\LatestLaunch.bat"), FilterAccessToken(CmdString, "F"),
|
||||
Encoding:=If(McLaunchJavaSelected.MajorVersion > 8, Encoding.UTF8, Encoding.Default))
|
||||
If CurrentLaunchOptions.SaveBatch IsNot Nothing Then
|
||||
McLaunchLog("导出启动脚本完成,强制结束启动过程")
|
||||
@@ -2189,7 +2185,7 @@ IgnoreCustomSkin:
|
||||
Loop
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "执行全局自定义命令失败", LogLevel.Hint)
|
||||
Log(ex, "执行全局自定义命令失败", NotifyLevel.AllUsers)
|
||||
Finally
|
||||
If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
|
||||
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
|
||||
@@ -2214,7 +2210,7 @@ IgnoreCustomSkin:
|
||||
Loop
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "执行版本自定义命令失败", LogLevel.Hint)
|
||||
Log(ex, "执行版本自定义命令失败", NotifyLevel.AllUsers)
|
||||
Finally
|
||||
If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
|
||||
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
|
||||
@@ -2233,9 +2229,9 @@ IgnoreCustomSkin:
|
||||
Dim PathEnv As String = StartInfo.EnvironmentVariables("Path")
|
||||
Dim Paths As New List(Of String)
|
||||
If Not String.IsNullOrEmpty(PathEnv) Then Paths.AddRange(PathEnv.Split(";"))
|
||||
Paths.Add(ShortenPath(McLaunchJavaSelected.PathFolder))
|
||||
Paths.Add(FileUtils.ShortenPath(McLaunchJavaSelected.PathFolder))
|
||||
StartInfo.EnvironmentVariables("Path") = Paths.Distinct.Join(";"c)
|
||||
StartInfo.EnvironmentVariables("appdata") = ShortenPath(McFolderSelected)
|
||||
StartInfo.EnvironmentVariables("appdata") = FileUtils.ShortenPath(McFolderSelected)
|
||||
|
||||
'设置其他参数
|
||||
StartInfo.WorkingDirectory = McInstanceSelected.PathIndie
|
||||
@@ -2267,7 +2263,7 @@ IgnoreCustomSkin:
|
||||
Case Else '中
|
||||
End Select
|
||||
Catch ex As Exception
|
||||
Log(ex, "设置进程优先级失败", LogLevel.Feedback)
|
||||
Log(ex, "设置进程优先级失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
|
||||
End Sub
|
||||
@@ -2276,7 +2272,7 @@ IgnoreCustomSkin:
|
||||
'输出信息
|
||||
McLaunchLog("")
|
||||
McLaunchLog("~ 基础参数 ~")
|
||||
McLaunchLog("PCL 版本:" & VersionDisplayName & " (" & VersionCode & ")")
|
||||
McLaunchLog("PCL 版本:" & VersionDisplay & " (" & VersionCode & ")")
|
||||
McLaunchLog($"游戏版本:{McInstanceSelected.VersionDisplayName}({McInstanceSelected.Version.Vanilla},Drop {McInstanceSelected.Version.Drop}{If(McInstanceSelected.Version.Reliable, "", ",无法完全确定")})")
|
||||
McLaunchLog("资源版本:" & McAssetsGetIndexName(McInstanceSelected))
|
||||
McLaunchLog("版本继承:" & If(McInstanceSelected.InheritName = "", "无", McInstanceSelected.InheritName))
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
Imports System.IO.Compression
|
||||
|
||||
Public Module ModMinecraft
|
||||
Public Module ModMinecraft
|
||||
|
||||
#Region "文件夹"
|
||||
|
||||
@@ -18,8 +16,8 @@ Public Module ModMinecraft
|
||||
End Property
|
||||
Private Sub SetMcFolder(Value As String) '等同 McFolderSelected.Set
|
||||
If _McFolderSelected = Value Then Return
|
||||
_McFolderSelected = Value.Replace("$", Path)
|
||||
Settings.Set("LaunchFolderSelect", Value.Replace(Path, "$"))
|
||||
_McFolderSelected = Value.Replace("$", PathExeFolder)
|
||||
Settings.Set("LaunchFolderSelect", Value.Replace(PathExeFolder, "$"))
|
||||
Log("[Minecraft] 当前选择的 Minecraft 文件夹:" & _McFolderSelected)
|
||||
End Sub
|
||||
Private _McFolderSelected As String
|
||||
@@ -65,8 +63,8 @@ Public Module ModMinecraft
|
||||
|
||||
'扫描当前文件夹
|
||||
Try
|
||||
If Directory.Exists(Path & "versions\") Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Path, .Type = McFolder.Types.Vanilla})
|
||||
For Each Folder As DirectoryInfo In New DirectoryInfo(Path).GetDirectories
|
||||
If Directory.Exists(PathExeFolder & "versions\") Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = PathExeFolder, .Type = McFolder.Types.Vanilla})
|
||||
For Each Folder As DirectoryInfo In New DirectoryInfo(PathExeFolder).GetDirectories
|
||||
If Directory.Exists(Folder.FullName & "versions\") OrElse Folder.Name = ".minecraft" Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Folder.FullName & "\", .Type = McFolder.Types.Vanilla})
|
||||
Next
|
||||
Catch ex As Exception
|
||||
@@ -122,8 +120,8 @@ Public Module ModMinecraft
|
||||
|
||||
'若没有可用文件夹,则创建 .minecraft
|
||||
If Not CacheMcFolderList.Any() Then
|
||||
Directory.CreateDirectory(Path & ".minecraft\versions\")
|
||||
CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Path & ".minecraft\", .Type = McFolder.Types.Vanilla})
|
||||
DirectoryUtils.Create(PathExeFolder & ".minecraft\versions\")
|
||||
CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = PathExeFolder & ".minecraft\", .Type = McFolder.Types.Vanilla})
|
||||
End If
|
||||
|
||||
For Each Folder As McFolder In CacheMcFolderList
|
||||
@@ -135,7 +133,7 @@ Public Module ModMinecraft
|
||||
McFolderList = CacheMcFolderList
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "加载 Minecraft 文件夹列表失败", LogLevel.Feedback)
|
||||
Log(ex, "加载 Minecraft 文件夹列表失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -159,10 +157,10 @@ Public Module ModMinecraft
|
||||
""selectedProfile"": ""PCL"",
|
||||
""clientToken"": ""23323323323323323323323323323333""
|
||||
}"
|
||||
WriteFile(Folder & "launcher_profiles.json", ResultJson, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
FileUtils.Write(Folder & "launcher_profiles.json", ResultJson, Encoding:=Encoding.GetEncoding("GB18030"))
|
||||
Log("[Minecraft] 已创建 launcher_profiles.json:" & Folder)
|
||||
Catch ex As Exception
|
||||
Log(ex, "创建 launcher_profiles.json 失败(" & Folder & ")", LogLevel.Feedback)
|
||||
Log(ex, "创建 launcher_profiles.json 失败(" & Folder & ")", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -170,7 +168,7 @@ Public Module ModMinecraft
|
||||
|
||||
#Region "版本处理"
|
||||
|
||||
Public Const McInstanceCacheVersion As Integer = 35
|
||||
Public Const McInstanceCacheVersion As Integer = 36
|
||||
|
||||
Private _McInstanceSelected As McInstance
|
||||
''' <summary>
|
||||
@@ -185,13 +183,12 @@ Public Module ModMinecraft
|
||||
If ReferenceEquals(_McInstanceSelectedLast, value) Then Return
|
||||
_McInstanceSelected = value '由于有可能是 Nothing,导致无法初始化,才得这样弄一圈
|
||||
_McInstanceSelectedLast = value
|
||||
Settings.MakeInstanceDirty()
|
||||
'更新 MC 文件夹所选取的 Minecraft 版本
|
||||
Log("[Launch] 当前选择的 Minecraft 版本:" & If(value?.PathVersion, "无"))
|
||||
WriteIni(McFolderSelected & "PCL.ini", "Version", If(value?.Name, ""))
|
||||
If value Is Nothing Then Return
|
||||
'重置缓存的下载文件夹
|
||||
PageDownloadCompDetail.CachedFolder.Clear()
|
||||
PageDownloadResourceDetail.CachedFolder.Clear()
|
||||
'统一通行证重判
|
||||
If AniControlEnabled = 0 AndAlso
|
||||
Settings.Get("VersionServerNide", Instance:=value) <> Settings.Get("CacheNideServer") AndAlso
|
||||
@@ -481,7 +478,7 @@ VersionSearchFinish:
|
||||
If JsonPath Is Nothing Then
|
||||
Throw New Exception($"未找到版本 JSON 文件:{PathVersion}{Name}.json")
|
||||
Else
|
||||
Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, LogLevel.Debug)
|
||||
Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, NotifyLevel.DebugModeOnly)
|
||||
End If
|
||||
End If
|
||||
Return JsonPath
|
||||
@@ -503,10 +500,10 @@ VersionSearchFinish:
|
||||
'如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
|
||||
If Not FastJsonCheck(_JsonText) Then
|
||||
If RunInUi() Then
|
||||
Log("[Minecraft] 版本 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", LogLevel.Debug)
|
||||
Log("[Minecraft] 版本 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", NotifyLevel.DebugModeOnly)
|
||||
GetJson(_JsonText) '触发异常
|
||||
Else
|
||||
Log($"[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", LogLevel.Debug)
|
||||
Log($"[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", NotifyLevel.DebugModeOnly)
|
||||
Thread.Sleep(2000)
|
||||
_JsonText = ReadFile(JsonPath)
|
||||
If Not FastJsonCheck(_JsonText) Then GetJson(_JsonText) '触发异常
|
||||
@@ -566,7 +563,7 @@ VersionSearchFinish:
|
||||
Try
|
||||
InstanceNameInheritFrom = If(_JsonObject("inheritsFrom") Is Nothing, "", _JsonObject("inheritsFrom").ToString)
|
||||
If InstanceNameInheritFrom = Name Then
|
||||
Log("[Minecraft] 自引用的继承版本:" & Name, LogLevel.Debug)
|
||||
Log("[Minecraft] 自引用的继承版本:" & Name, NotifyLevel.DebugModeOnly)
|
||||
InstanceNameInheritFrom = ""
|
||||
Exit Try
|
||||
End If
|
||||
@@ -684,7 +681,7 @@ Recheck:
|
||||
End If
|
||||
'检查权限
|
||||
Try
|
||||
Directory.CreateDirectory(PathVersion & "PCL\")
|
||||
DirectoryUtils.Create(PathVersion & "PCL\")
|
||||
CheckPermissionWithException(PathVersion & "PCL\")
|
||||
Catch ex As Exception
|
||||
State = McInstanceState.Error
|
||||
@@ -823,8 +820,6 @@ ExitDataLoad:
|
||||
If State = McInstanceState.Fool Then
|
||||
Info = GetMcFoolName(Version.VanillaName)
|
||||
ElseIf State <> McInstanceState.Error Then
|
||||
'这里不应使用缓存,因为不同实例有不同值 (#8508)
|
||||
Settings.MakeInstanceDirty()
|
||||
If Settings.Get("VersionServerLogin", Instance:=Me) = 3 Then Info += ", 统一通行证验证"
|
||||
If Settings.Get("VersionServerLogin", Instance:=Me) = 4 Then Info += ", Authlib 验证"
|
||||
End If
|
||||
@@ -853,15 +848,15 @@ ExitDataLoad:
|
||||
Info = "未知错误:" & ex.GetBrief()
|
||||
Logo = PathImage & "Blocks/RedstoneBlock.png"
|
||||
State = McInstanceState.Error
|
||||
Log(ex, "加载版本失败(" & Name & ")", LogLevel.Feedback)
|
||||
Log(ex, "加载版本失败(" & Name & ")", NotifyLevel.MsgBoxAndFeedback)
|
||||
Finally
|
||||
IsLoaded = True
|
||||
End Try
|
||||
Return Me
|
||||
End Function
|
||||
Private Function IsSnapshot() As Boolean
|
||||
Return {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) Version.VanillaName.ContainsF(s, True)) OrElse
|
||||
Name.ContainsF("combat", True) OrElse
|
||||
Return {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) Version.VanillaName.ContainsIgnoreCase(s)) OrElse
|
||||
Name.ContainsIgnoreCase("combat") OrElse
|
||||
If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending"
|
||||
End Function
|
||||
|
||||
@@ -882,15 +877,15 @@ ExitDataLoad:
|
||||
Dim Info As String
|
||||
Select Case State
|
||||
Case McInstanceState.Snapshot, McInstanceState.Original, McInstanceState.Forge, McInstanceState.NeoForge, McInstanceState.Fabric, McInstanceState.OptiFine, McInstanceState.LiteLoader
|
||||
If Version.VanillaName.ContainsF("pre", True) Then
|
||||
If Version.VanillaName.ContainsIgnoreCase("pre") Then
|
||||
Info = "预发布版 " & Version.VanillaName
|
||||
ElseIf Version.VanillaName.ContainsF("rc", True) Then
|
||||
ElseIf Version.VanillaName.ContainsIgnoreCase("rc") Then
|
||||
Info = "发布候选 " & Version.VanillaName
|
||||
ElseIf Version.VanillaName.Contains("experimental") Then
|
||||
ElseIf Version.VanillaName.ContainsIgnoreCase("experimental") Then
|
||||
Info = "实验性快照" & Version.VanillaName
|
||||
ElseIf Version.VanillaName = "pending" Then
|
||||
Info = "实验性快照"
|
||||
ElseIf IsSnapshot Then
|
||||
ElseIf IsSnapshot() Then
|
||||
Info = If(Version.Reliable, "快照版 " & Version.VanillaName.Replace("-snapshot", ""), "快照版")
|
||||
Else
|
||||
Info = If(Version.Reliable, "正式版 " & Version.VanillaName, "正式版")
|
||||
@@ -928,7 +923,7 @@ ExitDataLoad:
|
||||
NewItem.Logo = Logo
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "加载版本图标失败", LogLevel.Hint)
|
||||
Log(ex, "加载版本图标失败", NotifyLevel.AllUsers)
|
||||
NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
|
||||
End Try
|
||||
Return NewItem
|
||||
@@ -1026,8 +1021,8 @@ ExitDataLoad:
|
||||
Result += Val(RegexSeek(Right(OptiFine, OptiFine.Length - 1), "[0-9]+"))
|
||||
'测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)
|
||||
Result *= 100
|
||||
If OptiFine.ContainsF("pre", True) Then Result += 50
|
||||
If OptiFine.ContainsF("pre", True) OrElse OptiFine.ContainsF("beta", True) Then
|
||||
If OptiFine.ContainsIgnoreCase("pre") Then Result += 50
|
||||
If OptiFine.ContainsIgnoreCase("pre") OrElse OptiFine.ContainsIgnoreCase("beta") Then
|
||||
If Val(Right(OptiFine, 1)) = 0 AndAlso Right(OptiFine, 1) <> "0" Then
|
||||
Result += 1 '为 pre 或 beta 结尾,视作 1
|
||||
Else
|
||||
@@ -1134,7 +1129,7 @@ ExitDataLoad:
|
||||
If Drop >= 250 Then
|
||||
Return $"{Drop \ 10}.{Drop Mod 10}"
|
||||
ElseIf Drop = 209 Then
|
||||
Log("[Minecraft] 尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!" & vbCrLf & GetStackTrace(), LogLevel.Debug)
|
||||
Log("[Minecraft] 尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!" & vbCrLf & GetStackTrace(), NotifyLevel.DebugModeOnly)
|
||||
Return "1.20"
|
||||
Else
|
||||
Return $"1.{Drop \ 10}"
|
||||
@@ -1294,24 +1289,24 @@ OnLoaded:
|
||||
Catch ex As Exception
|
||||
If Loader.IsInterrupted Then Return '#5617
|
||||
WriteIni(PathMc & "PCL.ini", "InstanceCache", "") '要求下次重新加载
|
||||
Log(ex, "加载 .minecraft 版本列表失败", LogLevel.Feedback)
|
||||
Log(ex, "加载 .minecraft 版本列表失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
'获取版本列表
|
||||
Private Function InitMcInstanceListWithCache(Path As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
|
||||
Private Function InitMcInstanceListWithCache(Folder As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
|
||||
Dim Results As New Dictionary(Of McInstanceCardType, List(Of McInstance))
|
||||
Try
|
||||
Dim CardCount As Integer = ReadIni(Path & "PCL.ini", "CardCount", -1)
|
||||
Dim CardCount As Integer = ReadIni(Folder & "PCL.ini", "CardCount", -1)
|
||||
If CardCount = -1 Then Return Nothing
|
||||
For i = 0 To CardCount - 1
|
||||
Dim CardType As McInstanceCardType = ReadIni(Path & "PCL.ini", "CardKey" & (i + 1), ":")
|
||||
Dim CardType As McInstanceCardType = ReadIni(Folder & "PCL.ini", "CardKey" & (i + 1), ":")
|
||||
Dim InstanceList As New List(Of McInstance)
|
||||
|
||||
'循环读取版本
|
||||
For Each Folder As String In ReadIni(Path & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
|
||||
If Folder = "" Then Continue For
|
||||
Dim FolderVersions As String = $"{Path}versions\{Folder}\"
|
||||
For Each VersionName As String In ReadIni(Folder & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
|
||||
If VersionName = "" Then Continue For
|
||||
Dim FolderVersions As String = $"{Folder}versions\{VersionName}\"
|
||||
If File.Exists(FolderVersions & ".pclignore") Then
|
||||
If IsFirstMcInstanceListLoad Then
|
||||
Log("[Minecraft] 清理残留的忽略项目:" & FolderVersions) '#2781
|
||||
@@ -1332,7 +1327,7 @@ OnLoaded:
|
||||
Instance.ReleaseTime = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "ReleaseTime", Instance.ReleaseTime)
|
||||
Instance.State = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "State", Instance.State)
|
||||
Instance.IsStar = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "IsStar", False)
|
||||
Instance.DisplayType = ReadIni(Path & "PCL\Setup.ini", "DisplayType", McInstanceCardType.Auto)
|
||||
Instance.DisplayType = ReadIni(Folder & "PCL\Setup.ini", "DisplayType", McInstanceCardType.Auto)
|
||||
If Instance.State <> McInstanceState.Error AndAlso
|
||||
ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionVanillaName", "Unknown") <> "Unknown" Then
|
||||
'读取版本信息
|
||||
@@ -1375,7 +1370,7 @@ OnLoaded:
|
||||
End If
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取版本加载缓存失败(" & Folder & ")", LogLevel.Debug)
|
||||
Log(ex, "读取版本加载缓存失败(" & VersionName & ")", NotifyLevel.DebugModeOnly)
|
||||
Return Nothing
|
||||
End Try
|
||||
Next
|
||||
@@ -1388,20 +1383,21 @@ OnLoaded:
|
||||
Return Nothing
|
||||
End Try
|
||||
End Function
|
||||
Private Function InitMcInstanceListWithoutCache(Path As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
|
||||
Private Function InitMcInstanceListWithoutCache(Folder As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
|
||||
Dim InstanceList As New List(Of McInstance)
|
||||
|
||||
#Region "循环加载每个版本的信息"
|
||||
For Each Folder As DirectoryInfo In New DirectoryInfo(Path & "versions").GetDirectories
|
||||
If Not Folder.Exists OrElse Not Folder.EnumerateFiles.Any Then
|
||||
Log("[Minecraft] 跳过空文件夹:" & Folder.FullName)
|
||||
For Each VersionFolderInfo As DirectoryInfo In New DirectoryInfo(Folder & "versions").GetDirectories
|
||||
If Not VersionFolderInfo.Exists OrElse Not VersionFolderInfo.EnumerateFiles.Any Then
|
||||
Log("[Minecraft] 跳过空文件夹:" & VersionFolderInfo.FullName)
|
||||
Continue For
|
||||
End If
|
||||
If (Folder.Name = "cache" OrElse Folder.Name = "BLClient" OrElse Folder.Name = "PCL") AndAlso Not File.Exists(Folder.FullName & "\" & Folder.Name & ".json") Then
|
||||
Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
|
||||
If (VersionFolderInfo.Name = "cache" OrElse VersionFolderInfo.Name = "BLClient" OrElse VersionFolderInfo.Name = "PCL") AndAlso
|
||||
Not File.Exists(VersionFolderInfo.FullName & "\" & VersionFolderInfo.Name & ".json") Then
|
||||
Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & VersionFolderInfo.FullName)
|
||||
Continue For
|
||||
End If
|
||||
Dim VersionFolder As String = Folder.FullName & "\"
|
||||
Dim VersionFolder As String = VersionFolderInfo.FullName & "\"
|
||||
If File.Exists(VersionFolder & ".pclignore") Then
|
||||
If IsFirstMcInstanceListLoad Then
|
||||
Log("[Minecraft] 清理残留的忽略项目:" & VersionFolder) '#2781
|
||||
@@ -1517,7 +1513,7 @@ OnLoaded:
|
||||
|
||||
Catch ex As Exception
|
||||
Results.Clear()
|
||||
Log(ex, "分类版本列表失败", LogLevel.Feedback)
|
||||
Log(ex, "分类版本列表失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
#End Region
|
||||
#Region "对卡片与版本进行排序"
|
||||
@@ -1558,14 +1554,14 @@ OnLoaded:
|
||||
|
||||
#End Region
|
||||
#Region "保存卡片缓存"
|
||||
WriteIni(Path & "PCL.ini", "CardCount", Results.Count)
|
||||
WriteIni(Folder & "PCL.ini", "CardCount", Results.Count)
|
||||
For i = 0 To Results.Count - 1
|
||||
WriteIni(Path & "PCL.ini", "CardKey" & (i + 1), Results.Keys(i))
|
||||
WriteIni(Folder & "PCL.ini", "CardKey" & (i + 1), Results.Keys(i))
|
||||
Dim Value As String = ""
|
||||
For Each Instance As McInstance In Results.Values(i)
|
||||
Value += Instance.Name & ":"
|
||||
Next
|
||||
WriteIni(Path & "PCL.ini", "CardValue" & (i + 1), Value)
|
||||
WriteIni(Folder & "PCL.ini", "CardValue" & (i + 1), Value)
|
||||
Next
|
||||
#End Region
|
||||
Return Results
|
||||
@@ -1637,7 +1633,7 @@ OnLoaded:
|
||||
Return New McSkinInfo With {.IsVaild = False}
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "皮肤文件存在错误", LogLevel.Hint)
|
||||
Log(ex, "皮肤文件存在错误", NotifyLevel.AllUsers)
|
||||
Return New McSkinInfo With {.IsVaild = False}
|
||||
End Try
|
||||
|
||||
@@ -1685,7 +1681,7 @@ OnLoaded:
|
||||
Next
|
||||
Throw New Exception("未从皮肤返回值中找到符合条件的 Property")
|
||||
Catch ex As Exception
|
||||
Log(ex, "无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:" & SkinString, LogLevel.Developer)
|
||||
Log(ex, "无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:" & SkinString, NotifyLevel.DevelopOnly)
|
||||
Throw New Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex)
|
||||
End Try
|
||||
SkinString = Encoding.GetEncoding("utf-8").GetString(Convert.FromBase64String(SkinValue))
|
||||
@@ -1796,7 +1792,7 @@ OnLoaded:
|
||||
Public OriginalName As String
|
||||
|
||||
Public Overrides Function ToString() As String
|
||||
Return If(IsNatives, "[Native] ", "") & GetString(Size) & " | " & LocalPath
|
||||
Return If(IsNatives, "[Native] ", "") & FormatFileSize(Size) & " | " & LocalPath
|
||||
End Function
|
||||
End Class
|
||||
|
||||
@@ -1885,7 +1881,7 @@ OnLoaded:
|
||||
'不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在
|
||||
If Not File.Exists(RealInstance.PathVersion & RealInstance.Name & ".json") Then
|
||||
RealInstance = Instance
|
||||
Log("[Minecraft] 可能缺少前置版本 " & RealInstance.Name & ",找不到对应的 json 文件", LogLevel.Debug)
|
||||
Log("[Minecraft] 可能缺少前置版本 " & RealInstance.Name & ",找不到对应的 json 文件", NotifyLevel.DebugModeOnly)
|
||||
End If
|
||||
'获取详细下载信息
|
||||
If RealInstance.JsonObject("downloads") IsNot Nothing AndAlso RealInstance.JsonObject("downloads")("client") IsNot Nothing Then
|
||||
@@ -1933,7 +1929,7 @@ OnLoaded:
|
||||
If Library("downloads") IsNot Nothing AndAlso Library("downloads")("artifact") IsNot Nothing Then
|
||||
BasicArray.Add(New McLibToken With {
|
||||
.OriginalName = Library("name"),
|
||||
.Url = If(RootUrl, Library("downloads")("artifact")("url")),
|
||||
.Url = If(Library("downloads")("artifact")("url"), RootUrl),
|
||||
.LocalPath = If(Library("downloads")("artifact")("path") Is Nothing, McLibGet(Library("name"),
|
||||
CustomMcFolder:=CustomMcFolder), CustomMcFolder & "libraries\" & Library("downloads")("artifact")("path").ToString.Replace("/", "\")),
|
||||
.Size = Val(Library("downloads")("artifact")("size").ToString),
|
||||
@@ -1947,24 +1943,26 @@ OnLoaded:
|
||||
BasicArray.Add(New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .LocalPath = LocalPath, .Size = 0, .IsNatives = False, .SHA1 = Nothing})
|
||||
End Try
|
||||
ElseIf Library("natives")("windows") IsNot Nothing Then '有 Windows Natives
|
||||
Dim NativeName As String = Library("natives")("windows")
|
||||
Try
|
||||
If Library("downloads") IsNot Nothing AndAlso Library("downloads")("classifiers") IsNot Nothing AndAlso Library("downloads")("classifiers")("natives-windows") IsNot Nothing Then
|
||||
If Library("downloads") IsNot Nothing AndAlso Library("downloads")("classifiers") IsNot Nothing AndAlso Library("downloads")("classifiers")(NativeName) IsNot Nothing Then
|
||||
Dim NativeData = Library("downloads")("classifiers")(NativeName)
|
||||
BasicArray.Add(New McLibToken With {
|
||||
.OriginalName = Library("name"),
|
||||
.Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
|
||||
.LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
|
||||
.Url = If(NativeData("url"), RootUrl),
|
||||
.LocalPath = If(NativeData("path") Is Nothing,
|
||||
McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
|
||||
Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").
|
||||
Replace(".jar", "-" & NativeName & ".jar").
|
||||
Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
|
||||
$"{CustomMcFolder}libraries\{Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")}"),
|
||||
.Size = Val(Library("downloads")("classifiers")("natives-windows")("size").ToString),
|
||||
$"{CustomMcFolder}libraries\{NativeData("path").ToString.Replace("/", "\")}"),
|
||||
.Size = Val(NativeData("size").ToString),
|
||||
.IsNatives = True,
|
||||
.SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
|
||||
.SHA1 = NativeData("sha1").ToString})
|
||||
Else
|
||||
BasicArray.Add(
|
||||
New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
|
||||
.LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
|
||||
Replace(".jar", $"-{Library("natives")("windows")}.jar").
|
||||
Replace(".jar", $"-{NativeName}.jar").
|
||||
Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
|
||||
})
|
||||
End If
|
||||
@@ -1973,7 +1971,7 @@ OnLoaded:
|
||||
BasicArray.Add(
|
||||
New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
|
||||
.LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
|
||||
Replace(".jar", $"-{Library("natives")("windows")}.jar").
|
||||
Replace(".jar", $"-{NativeName}.jar").
|
||||
Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
|
||||
})
|
||||
End Try
|
||||
@@ -2026,7 +2024,7 @@ OnLoaded:
|
||||
Dim MainJar As NetFile = DlClientJarGet(Instance, True)
|
||||
If MainJar IsNot Nothing Then Result.Add(MainJar)
|
||||
Catch ex As Exception
|
||||
Log(ex, "版本缺失主 jar 文件所必须的信息", LogLevel.Developer)
|
||||
Log(ex, "版本缺失主 jar 文件所必须的信息", NotifyLevel.DevelopOnly)
|
||||
End Try
|
||||
|
||||
'Library 文件
|
||||
@@ -2077,11 +2075,11 @@ OnLoaded:
|
||||
|
||||
'跳过校验
|
||||
If ShouldIgnoreFileCheck(Instance) Then
|
||||
Log("[Minecraft] 设置要求尽量忽略文件检查,这可能会保留有误的文件", LogLevel.Debug)
|
||||
Log("[Minecraft] 设置要求尽量忽略文件检查,这可能会保留有误的文件", NotifyLevel.DebugModeOnly)
|
||||
Result = Result.Where(
|
||||
Function(f)
|
||||
If File.Exists(f.LocalPath) Then
|
||||
Log("[Minecraft] 跳过下载的支持库文件:" & f.LocalPath, LogLevel.Debug)
|
||||
Log("[Minecraft] 跳过下载的支持库文件:" & f.LocalPath, NotifyLevel.DebugModeOnly)
|
||||
Return False
|
||||
Else
|
||||
Return True
|
||||
@@ -2101,7 +2099,7 @@ OnLoaded:
|
||||
For Each Token As McLibToken In Libs
|
||||
'检查器
|
||||
Dim Checker As FileChecker
|
||||
If Token.Name.ContainsF("labymod") Then
|
||||
If Token.Name.ContainsIgnoreCase("labymod") Then
|
||||
Checker = New FileChecker '不检查 LabyMod 的文件,它们提供的文件校验信息是错的(#3225)
|
||||
Else
|
||||
Checker = New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1)
|
||||
@@ -2120,7 +2118,7 @@ OnLoaded:
|
||||
End If
|
||||
If Token.Url.Contains("maven") Then
|
||||
Dim BmclapiUrl As String =
|
||||
Token.Url.Replace(Mid(Token.Url, 1, Token.Url.IndexOfF("maven")), "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven")
|
||||
Token.Url.Replace(Token.Url.Substring(0, Token.Url.IndexOfF("maven")), "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven")
|
||||
If DlSourcePreferMojang Then
|
||||
Urls.Add(BmclapiUrl) '官方源优先
|
||||
Else
|
||||
@@ -2130,8 +2128,8 @@ OnLoaded:
|
||||
End If
|
||||
If Token.LocalPath.Contains("transformer-discovery-service") Then
|
||||
'Transformer 文件释放
|
||||
If Not File.Exists(Token.LocalPath) Then WriteFile(Token.LocalPath, GetResources("Transformer"))
|
||||
Log("[Download] 已自动释放 Transformer Discovery Service", LogLevel.Developer)
|
||||
If Not File.Exists(Token.LocalPath) Then FileUtils.Write(Token.LocalPath, GetResources("Transformer"))
|
||||
Log("[Download] 已自动释放 Transformer Discovery Service", NotifyLevel.DevelopOnly)
|
||||
Continue For
|
||||
ElseIf Token.LocalPath.Contains("optifine\OptiFine") Then
|
||||
'OptiFine 主 Jar
|
||||
@@ -2146,7 +2144,7 @@ OnLoaded:
|
||||
Result.Add(New NetFile(Urls.Distinct, Token.LocalPath, Checker))
|
||||
Next
|
||||
'去重并返回
|
||||
Return Result.Distinct(Function(a, b) a.LocalPath = b.LocalPath)
|
||||
Return Result.DistinctBy(Function(a) a.LocalPath).ToList
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 获取对应的支持库文件地址。
|
||||
@@ -2253,7 +2251,7 @@ OnLoaded:
|
||||
Public Hash As String
|
||||
|
||||
Public Overrides Function ToString() As String
|
||||
Return GetString(Size) & " | " & LocalPath
|
||||
Return FormatFileSize(Size) & " | " & LocalPath
|
||||
End Function
|
||||
End Structure
|
||||
''' <summary>
|
||||
@@ -2367,7 +2365,7 @@ OnLoaded:
|
||||
End If
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "Minecraft 更新提示发送失败(" & If(VersionName, "Nothing") & ")", LogLevel.Feedback)
|
||||
Log(ex, "Minecraft 更新提示发送失败(" & If(VersionName, "Nothing") & ")", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -2459,7 +2457,7 @@ NextEntry:
|
||||
End If
|
||||
'打码当前登录的结果
|
||||
Dim AccessToken As String = McLoginLoader.Output.AccessToken
|
||||
If AccessToken IsNot Nothing AndAlso AccessToken.Length >= 10 AndAlso Raw.ContainsF(AccessToken, True) AndAlso
|
||||
If AccessToken IsNot Nothing AndAlso AccessToken.Length >= 10 AndAlso Raw.ContainsIgnoreCase(AccessToken) AndAlso
|
||||
McLoginLoader.Output.Uuid <> McLoginLoader.Output.AccessToken Then 'UUID 和 AccessToken 一样则不打码
|
||||
Raw = Raw.Replace(AccessToken, Left(AccessToken, 5) & New String(FilterChar, AccessToken.Length - 10) & Right(AccessToken, 5))
|
||||
End If
|
||||
|
||||
@@ -1,876 +0,0 @@
|
||||
Imports System.IO.Compression
|
||||
|
||||
Public Module ModMod
|
||||
Private Const LocalModCacheVersion As Integer = 13
|
||||
|
||||
Public Class McMod
|
||||
|
||||
#Region "基础"
|
||||
|
||||
''' <summary>
|
||||
''' Mod 文件的地址。
|
||||
''' </summary>
|
||||
Public ReadOnly Path As String
|
||||
Public Sub New(Path As String)
|
||||
Me.Path = If(Path, "")
|
||||
End Sub
|
||||
''' <summary>
|
||||
''' Mod 的完整路径,去除最后的 .disabled 和 .old。
|
||||
''' </summary>
|
||||
Public ReadOnly Property RawPath As String
|
||||
Get
|
||||
Return GetPathFromFullPath(Path) & RawFileName
|
||||
End Get
|
||||
End Property
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的完整文件名。
|
||||
''' </summary>
|
||||
Public ReadOnly Property FileName As String
|
||||
Get
|
||||
Return GetFileNameFromPath(Path)
|
||||
End Get
|
||||
End Property
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的完整文件名,去除最后的 .disabled 和 .old。
|
||||
''' </summary>
|
||||
Public ReadOnly Property RawFileName As String
|
||||
Get
|
||||
Return FileName.Replace(".disabled", "").Replace(".old", "")
|
||||
End Get
|
||||
End Property
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的状态。
|
||||
''' </summary>
|
||||
Public ReadOnly Property State As McModState
|
||||
Get
|
||||
If Path.EndsWithF(".disabled", True) OrElse Path.EndsWithF(".old", True) Then
|
||||
Return McModState.Disabled
|
||||
Else
|
||||
Return McModState.Fine
|
||||
End If
|
||||
End Get
|
||||
End Property
|
||||
Public Enum McModState As Integer
|
||||
Fine = 0
|
||||
Disabled = 1
|
||||
End Enum
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "信息项"
|
||||
|
||||
''' <summary>
|
||||
''' 当任何信息在初始化后更新时触发。
|
||||
''' </summary>
|
||||
Public Event OnUpdate(sender As McMod)
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的可读名称。
|
||||
''' 可能为无扩展的文件名,但不会是 Nothing。
|
||||
''' </summary>
|
||||
Public Property DisplayName As String
|
||||
Get
|
||||
If _Name Is Nothing Then _Name = CompFile?.DisplayName
|
||||
If _Name Is Nothing Then _Name = IO.Path.GetFileNameWithoutExtension(Path)
|
||||
Return _Name
|
||||
End Get
|
||||
Set(value As String)
|
||||
If _Name Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
|
||||
value.Lower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
|
||||
_Name = value
|
||||
End If
|
||||
End Set
|
||||
End Property
|
||||
Private _Name As String = Nothing
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的描述信息。
|
||||
''' 可能为 Nothing。
|
||||
''' </summary>
|
||||
Public Property Description As String
|
||||
Get
|
||||
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.Lower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
|
||||
End If
|
||||
End Set
|
||||
End Property
|
||||
Private _Description As String = Nothing
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的版本。
|
||||
''' 不保证符合版本格式规范,且可能为 Nothing。
|
||||
''' </summary>
|
||||
Public Property Version As String
|
||||
Get
|
||||
If _Version Is Nothing Then _Version = CompFile?.Version
|
||||
Return _Version
|
||||
End Get
|
||||
Set(value As String)
|
||||
If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
|
||||
If value IsNot Nothing AndAlso value.ContainsF("version", True) Then value = "version" '需要修改的标识
|
||||
_Version = value
|
||||
End Set
|
||||
End Property
|
||||
Public _Version As String = Nothing
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "从 JAR 获取信息"
|
||||
|
||||
''' <summary>
|
||||
''' 从 JAR 文件中获取 Mod 信息。
|
||||
''' </summary>
|
||||
Public Sub LoadMetadataFromJar()
|
||||
Static IsLoaded As Boolean = False
|
||||
If IsLoaded Then Return
|
||||
IsLoaded = True
|
||||
Dim Jar As ZipArchive = Nothing
|
||||
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, FileAccess.Read, FileShare.Read))
|
||||
'信息获取
|
||||
LoadMetadataFromJar(Jar)
|
||||
Catch ex As UnauthorizedAccessException
|
||||
Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
|
||||
Catch ex As Exception
|
||||
Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
|
||||
Finally
|
||||
If Jar IsNot Nothing Then Jar.Dispose()
|
||||
End Try
|
||||
End Sub
|
||||
Private Sub LoadMetadataFromJar(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 信息项
|
||||
DisplayName = InfoObject("name")
|
||||
Description = InfoObject("description")
|
||||
Version = InfoObject("version")
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取 mcmod.info 时出现未知错误(" & 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 DisplayName = FabricObject("name")
|
||||
If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
|
||||
If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取 fabric.mod.json 时出现未知错误(" & 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.StartsWithF("#") Then '去除注释
|
||||
Continue For
|
||||
ElseIf Line.Contains("#") Then
|
||||
Line = Line.Substring(0, Line.IndexOfF("#"))
|
||||
End If
|
||||
Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
|
||||
If Line.Any 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.StartsWithF("[") AndAlso Line.EndsWithF("]") 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.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
|
||||
Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
|
||||
Dim Value As Object
|
||||
If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
|
||||
'单行字符串
|
||||
Value = RawValue.Trim("""")
|
||||
ElseIf RawValue.StartsWithF("'''") Then
|
||||
'多行字符串
|
||||
Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
|
||||
If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
|
||||
ValueLines(0) = ValueLines(0).TrimEnd("'")
|
||||
Else
|
||||
Do Until i >= Lines.Count - 1
|
||||
i += 1
|
||||
Dim ValueLine As String = Lines(i)
|
||||
If ValueLine.EndsWithF("'''") Then
|
||||
ValueLines.Add(ValueLine.TrimEnd("'"))
|
||||
Exit Do
|
||||
Else
|
||||
ValueLines.Add(ValueLine)
|
||||
End If
|
||||
Loop
|
||||
End If
|
||||
Value = ValueLines.Join(vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
|
||||
ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
|
||||
'布尔型
|
||||
Value = (RawValue.Lower = "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
|
||||
If ModEntry.ContainsKey("displayName") Then DisplayName = ModEntry("displayName")
|
||||
If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
|
||||
If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取 mods.toml 时出现未知错误(" & 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("name") Then DisplayName = FmlObject("name")("value")
|
||||
If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
|
||||
End Try
|
||||
#End Region
|
||||
Return '没能成功获取任何信息
|
||||
|
||||
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.IndexOfF("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
|
||||
RaiseEvent OnUpdate(Me)
|
||||
|
||||
End Sub
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "网络信息"
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 关联的网络项目。
|
||||
''' </summary>
|
||||
Public Property Comp As CompProject
|
||||
Get
|
||||
Return _Comp
|
||||
End Get
|
||||
Set(value As CompProject)
|
||||
_Comp = value
|
||||
RaiseEvent OnUpdate(Me)
|
||||
End Set
|
||||
End Property
|
||||
Private _Comp As CompProject
|
||||
|
||||
''' <summary>
|
||||
''' 本地文件对应的联网文件信息。
|
||||
''' </summary>
|
||||
Public CompFile As CompFile
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 对应的联网最新版本。
|
||||
''' </summary>
|
||||
Public Property UpdateFile As CompFile
|
||||
Get
|
||||
Return _UpdateFile
|
||||
End Get
|
||||
Set(value As CompFile)
|
||||
_UpdateFile = value
|
||||
RaiseEvent OnUpdate(Me)
|
||||
End Set
|
||||
End Property
|
||||
Private _UpdateFile As CompFile
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 的更新日志网址。
|
||||
''' </summary>
|
||||
Public ChangelogUrls As New List(Of String)
|
||||
''' <summary>
|
||||
''' 所有网络信息是否已成功加载。
|
||||
''' </summary>
|
||||
Public CompLoaded As Boolean = False
|
||||
|
||||
''' <summary>
|
||||
''' 将网络信息保存为 Json。
|
||||
''' </summary>
|
||||
Public Function ToJson() As JObject
|
||||
Dim Json As New JObject
|
||||
If Comp IsNot Nothing Then Json.Add("Comp", Comp.ToJson())
|
||||
Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
|
||||
Json.Add("CompLoaded", CompLoaded)
|
||||
If CompFile IsNot Nothing Then Json.Add("CompFile", CompFile.ToJson())
|
||||
If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToJson())
|
||||
Return Json
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 从 Json 中读取网络信息。
|
||||
''' </summary>
|
||||
Public Sub FromJson(Json As JObject)
|
||||
CompLoaded = Json("CompLoaded")
|
||||
If Json.ContainsKey("Comp") Then Comp = New CompProject(Json("Comp"))
|
||||
If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
|
||||
If Json.ContainsKey("CompFile") Then CompFile = New CompFile(Json("CompFile"), CompType.Mod)
|
||||
If Json.ContainsKey("UpdateFile") Then UpdateFile = New CompFile(Json("UpdateFile"), CompType.Mod)
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 该文件是否可以更新。
|
||||
''' </summary>
|
||||
Public ReadOnly Property CanUpdate As Boolean
|
||||
Get
|
||||
Return Not Settings.Get("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
|
||||
End Get
|
||||
End Property
|
||||
|
||||
''' <summary>
|
||||
''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
|
||||
''' </summary>
|
||||
Public ReadOnly Property CurseForgeHash As UInteger
|
||||
Get
|
||||
If _CurseForgeHash Is Nothing Then
|
||||
'读取缓存
|
||||
Dim Info As New FileInfo(Path)
|
||||
Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-C")
|
||||
Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
|
||||
If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
|
||||
_CurseForgeHash = Cached
|
||||
Return _CurseForgeHash
|
||||
End If
|
||||
'读取文件
|
||||
Dim data As New List(Of Byte)
|
||||
For Each b As Byte In ReadFileBytes(Path)
|
||||
If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
|
||||
data.Add(b)
|
||||
Next
|
||||
'计算 MurmurHash2
|
||||
Dim length As Integer = data.Count
|
||||
Dim h As UInteger = 1 Xor length '1 是种子
|
||||
Dim i As Integer
|
||||
For i = 0 To length - 4 Step 4
|
||||
Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
|
||||
k = (k * &H5BD1E995L) And &HFFFFFFFFL
|
||||
k = k Xor (k >> 24)
|
||||
k = (k * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = h Xor k
|
||||
Next
|
||||
Select Case length - i
|
||||
Case 3
|
||||
h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
|
||||
h = h Xor (CUInt(data(i + 2)) << 16)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
Case 2
|
||||
h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
Case 1
|
||||
h = h Xor data(i)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
End Select
|
||||
h = h Xor (h >> 13)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = h Xor (h >> 15)
|
||||
_CurseForgeHash = h
|
||||
'写入缓存
|
||||
WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
|
||||
End If
|
||||
Return _CurseForgeHash
|
||||
End Get
|
||||
End Property
|
||||
Private _CurseForgeHash As UInteger?
|
||||
|
||||
''' <summary>
|
||||
''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
|
||||
''' </summary>
|
||||
Public ReadOnly Property ModrinthHash As String
|
||||
Get
|
||||
If _ModrinthHash Is Nothing Then
|
||||
'读取缓存
|
||||
Dim Info As New FileInfo(Path)
|
||||
Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-M")
|
||||
Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
|
||||
If Cached <> "" Then
|
||||
_ModrinthHash = Cached
|
||||
Return _ModrinthHash
|
||||
End If
|
||||
'计算 SHA1
|
||||
_ModrinthHash = GetFileSHA1(Path)
|
||||
'写入缓存
|
||||
WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
|
||||
End If
|
||||
Return _ModrinthHash
|
||||
End Get
|
||||
End Property
|
||||
Private _ModrinthHash As String
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "API"
|
||||
|
||||
Public Overrides Function ToString() As String
|
||||
Return $"{State} - {Path}"
|
||||
End Function
|
||||
Public Overrides Function Equals(obj As Object) As Boolean
|
||||
Dim target = TryCast(obj, McMod)
|
||||
Return target IsNot Nothing AndAlso Path = target.Path
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
|
||||
''' <summary>
|
||||
''' 是否可能为前置 Mod。目前非常不准确。
|
||||
''' </summary>
|
||||
Public Function IsPresetMod() As Boolean
|
||||
Return DisplayName IsNot Nothing AndAlso (DisplayName.Lower.Contains("core") OrElse DisplayName.Lower.Contains("lib"))
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
|
||||
''' </summary>
|
||||
Public Shared Function IsModFile(Path As String)
|
||||
If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
|
||||
Path = Path.Lower
|
||||
If Path.EndsWithF(".jar", True) OrElse Path.EndsWithF(".zip", True) OrElse Path.EndsWithF(".litemod", True) OrElse
|
||||
Path.EndsWithF(".jar.disabled", True) OrElse Path.EndsWithF(".zip.disabled", True) OrElse Path.EndsWithF(".litemod.disabled", True) OrElse
|
||||
Path.EndsWithF(".jar.old", True) OrElse Path.EndsWithF(".zip.old", True) OrElse Path.EndsWithF(".litemod.old", True) 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 FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
|
||||
|
||||
'等待 Mod 更新完成
|
||||
If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
|
||||
Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
|
||||
Try
|
||||
RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
|
||||
Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
|
||||
If Loader.IsInterrupted Then Return
|
||||
Thread.Sleep(100)
|
||||
Loop
|
||||
Finally
|
||||
RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
|
||||
End Try
|
||||
FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
|
||||
End If
|
||||
|
||||
'获取 Mod 文件夹下的可用文件列表
|
||||
Dim ModFileList As New List(Of FileInfo)
|
||||
If Directory.Exists(Loader.Input) Then
|
||||
Dim RawName As String = Loader.Input.Lower
|
||||
For Each File As FileInfo In EnumerateFiles(Loader.Input)
|
||||
If File.DirectoryName.Lower & "\" <> RawName Then
|
||||
'仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
|
||||
If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
|
||||
PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
|
||||
File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
|
||||
Continue For
|
||||
End If
|
||||
End If
|
||||
If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
|
||||
Next
|
||||
End If
|
||||
|
||||
'获取本地文件缓存
|
||||
Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
|
||||
Dim Cache As New JObject
|
||||
Try
|
||||
Dim CacheContent As String = ReadFile(CachePath)
|
||||
If Not String.IsNullOrWhiteSpace(CacheContent) Then
|
||||
Cache = GetJson(CacheContent)
|
||||
If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
|
||||
Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", LogLevel.Debug)
|
||||
Cache = New JObject
|
||||
End If
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取本地 Mod 信息缓存失败,已重置")
|
||||
Cache = New JObject
|
||||
End Try
|
||||
Cache("version") = LocalModCacheVersion
|
||||
|
||||
'加载 Mod 列表
|
||||
Dim ModList As New List(Of McMod)
|
||||
For Each ModFile As FileInfo In ModFileList
|
||||
If Loader.IsInterrupted Then Return
|
||||
Dim ModEntry As New McMod(ModFile.FullName)
|
||||
Dim DumpMod As McMod = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName) '存在两个文件,名称相同,但一个启用一个禁用
|
||||
If DumpMod IsNot Nothing Then
|
||||
Dim DisabledMod As McMod = If(DumpMod.State = McMod.McModState.Disabled, DumpMod, ModEntry)
|
||||
Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug)
|
||||
If DisabledMod Is ModEntry Then
|
||||
Continue For
|
||||
Else
|
||||
ModList.Remove(DisabledMod)
|
||||
End If
|
||||
End If
|
||||
ModList.Add(ModEntry)
|
||||
Next
|
||||
Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
|
||||
|
||||
'排序
|
||||
ModList = ModList.OrderBy(Function(m) m.FileName).ToList
|
||||
|
||||
'回设
|
||||
If Loader.IsInterrupted Then Return
|
||||
Loader.Output = ModList
|
||||
|
||||
'开始联网加载
|
||||
'TODO: 添加信息获取中提示
|
||||
McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModList, Cache), IsForceRestart:=True)
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "Mod 列表加载失败", LogLevel.Debug)
|
||||
Throw
|
||||
End Try
|
||||
End Sub
|
||||
'联网加载 Mod 详情
|
||||
Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
|
||||
Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
|
||||
Dim Mods As New List(Of McMod)
|
||||
Dim Cache As JObject = Loader.Input.Value
|
||||
'读取 Comp 缓存,获取需要更新的 Mod 列表
|
||||
For Each ModEntry As McMod In Loader.Input.Key
|
||||
If Loader.IsInterrupted Then Return
|
||||
Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
|
||||
If Cache.ContainsKey(CacheKey) Then
|
||||
ModEntry.FromJson(Cache(CacheKey))
|
||||
'如果缓存中的信息在 6 小时以内更新过,则无需重新获取
|
||||
If ModEntry.CompLoaded AndAlso Date.Now - Cache(CacheKey)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
|
||||
End If
|
||||
Mods.Add(ModEntry)
|
||||
Next
|
||||
Log($"[Mod] 有 {Mods.Where(Function(m) m.Comp Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Comp IsNot Nothing).Count} 个 Mod 需要更新信息")
|
||||
If Not Mods.Any Then
|
||||
Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar()) '从 JAR 中获取缺失的版本信息(下面有另一个分支)
|
||||
Return
|
||||
End If
|
||||
'获取作为检查目标的加载器和版本
|
||||
'此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
|
||||
Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
|
||||
Dim ModLoaders = GetTargetModLoaders()
|
||||
Dim VanillaVersion = TargetMcVersion.VanillaName
|
||||
'开始网络获取
|
||||
Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
|
||||
Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
|
||||
Dim CurrentTaskThread As Thread = Thread.CurrentThread
|
||||
'从 Modrinth 获取信息
|
||||
RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
'步骤 1:获取 Hash 与对应的工程 ID
|
||||
Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
|
||||
Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
|
||||
$"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
|
||||
Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
|
||||
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
|
||||
If ModrinthVersion.Count = 0 Then Return
|
||||
Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
|
||||
For Each Entry In Mods
|
||||
If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
|
||||
If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
|
||||
Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
|
||||
If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
|
||||
If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
|
||||
ModrinthMapping(ProjectId).Add(Entry)
|
||||
'记录对应的 CompFile
|
||||
Dim File As New CompFile(ModrinthVersion(Entry.ModrinthHash), CompType.Mod)
|
||||
If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then
|
||||
Entry.CompFile = File
|
||||
Else
|
||||
Entry.CompFile.Version = File.Version '使用来自 Modrinth 的版本号
|
||||
End If
|
||||
Next
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
|
||||
'步骤 3:获取工程信息
|
||||
If Not ModrinthMapping.Any() Then Return
|
||||
Dim ModrinthProject As JArray = DlModRequest(
|
||||
$"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
|
||||
For Each ProjectJson In ModrinthProject
|
||||
Dim Project As New CompProject(ProjectJson)
|
||||
For Each Entry In ModrinthMapping(Project.Id)
|
||||
Entry.Comp = Project
|
||||
Next
|
||||
Next
|
||||
Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
|
||||
'步骤 4:获取更新信息
|
||||
Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
|
||||
$"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"",
|
||||
""loaders"": [""{ModLoaders.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
|
||||
For Each Entry In Mods
|
||||
If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
|
||||
Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod)
|
||||
If Not UpdateFile.Available Then Continue For
|
||||
If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
|
||||
If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
|
||||
'设置更新日志与更新文件
|
||||
If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
|
||||
Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}")
|
||||
UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
|
||||
Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
|
||||
ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
|
||||
Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}"}
|
||||
Entry.UpdateFile = UpdateFile
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
|
||||
Catch ex As Exception
|
||||
Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
|
||||
IsFailed = True
|
||||
Finally
|
||||
EndedThreadCount += 1
|
||||
End Try
|
||||
End Sub, "Mod List Detail Loader Modrinth")
|
||||
'从 CurseForge 获取信息
|
||||
RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
'步骤 1:获取 Hash 与对应的工程 ID
|
||||
Dim CurseForgeHashes As New List(Of UInteger)
|
||||
For Each Entry In Mods
|
||||
CurseForgeHashes.Add(Entry.CurseForgeHash)
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Next
|
||||
Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
|
||||
$"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
|
||||
Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
|
||||
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
|
||||
If Not CurseForgeRaw.Any() Then Return
|
||||
Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of McMod))
|
||||
For Each Project In CurseForgeRaw
|
||||
Dim ProjectId = Project("id").ToString
|
||||
Dim Hash As UInteger = Project("file")("fileFingerprint")
|
||||
For Each Entry In Mods
|
||||
If Entry.CurseForgeHash <> Hash Then Continue For
|
||||
If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
|
||||
If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
|
||||
CurseForgeMapping(ProjectId).Add(Entry)
|
||||
'记录对应的 CompFile
|
||||
Dim File As New CompFile(Project("file"), CompType.Mod)
|
||||
If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File
|
||||
Next
|
||||
Next
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
|
||||
'步骤 3:获取工程信息
|
||||
If Not CurseForgeMapping.Any() Then Return
|
||||
Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
|
||||
$"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
|
||||
Dim UpdateFileIds As New Dictionary(Of Integer, List(Of McMod)) 'FileId -> 本地 Mod 文件列表
|
||||
Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
|
||||
For Each ProjectJson In CurseForgeProject
|
||||
If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
|
||||
'设置 Entry 中的工程信息
|
||||
Dim Project As New CompProject(ProjectJson)
|
||||
For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
|
||||
If Entry.Comp IsNot Nothing AndAlso Not Entry.Comp.FromCurseForge Then
|
||||
Entry.Comp = Entry.Comp '再次触发修改事件
|
||||
Continue For
|
||||
End If
|
||||
Entry.Comp = Project
|
||||
Next
|
||||
'查找或许版本更新的文件列表
|
||||
If ModLoaders.IsSingle Then
|
||||
Dim NewestVersion As String = Nothing
|
||||
Dim NewestFileIds As New List(Of Integer)
|
||||
For Each IndexEntry In ProjectJson("latestFilesIndexes")
|
||||
If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
|
||||
Dim IndexVersion As String = IndexEntry("gameVersion")
|
||||
If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
|
||||
'由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
|
||||
If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
|
||||
If NewestVersion <> IndexVersion Then
|
||||
NewestVersion = IndexVersion
|
||||
NewestFileIds.Clear()
|
||||
End If
|
||||
NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
|
||||
Next
|
||||
For Each FileId In NewestFileIds
|
||||
If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of McMod)
|
||||
UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
|
||||
FileIdToProjectSlug(FileId) = Project.Slug
|
||||
Next
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
|
||||
'步骤 4:获取更新文件信息
|
||||
If Not UpdateFileIds.Any() Then Return
|
||||
Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
|
||||
$"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
|
||||
Dim UpdateFiles As New Dictionary(Of McMod, CompFile)
|
||||
For Each FileJson In CurseForgeFiles
|
||||
Dim File As New CompFile(FileJson, CompType.Mod)
|
||||
If Not File.Available Then Continue For
|
||||
For Each Entry As McMod In UpdateFileIds(File.Id)
|
||||
If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
|
||||
UpdateFiles(Entry) = File
|
||||
Next
|
||||
Next
|
||||
For Each Pair In UpdateFiles
|
||||
Dim Entry As McMod = Pair.Key
|
||||
Dim UpdateFile As CompFile = Pair.Value
|
||||
If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
|
||||
If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
|
||||
'设置更新日志与更新文件
|
||||
If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
|
||||
Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
|
||||
Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
|
||||
ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
|
||||
Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
|
||||
Entry.UpdateFile = UpdateFile
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
|
||||
Catch ex As Exception
|
||||
Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
|
||||
IsFailed = True
|
||||
Finally
|
||||
EndedThreadCount += 1
|
||||
End Try
|
||||
End Sub, "Mod List Detail Loader CurseForge")
|
||||
'从 JAR 中获取缺失的版本信息(上面有另一个分支)
|
||||
Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar())
|
||||
'等待线程结束
|
||||
Do Until EndedThreadCount = 2
|
||||
Thread.Sleep(10)
|
||||
If Loader.IsInterrupted Then Return
|
||||
Loop
|
||||
'保存缓存
|
||||
Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
|
||||
Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
|
||||
If Not Mods.Any() Then Return
|
||||
For Each Entry In Mods
|
||||
Entry.CompLoaded = Not IsFailed
|
||||
Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
|
||||
Next
|
||||
WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
|
||||
'刷新边栏
|
||||
If Loader.IsInterrupted Then Return
|
||||
If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
|
||||
RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
|
||||
Else
|
||||
RunInUi(Sub() FrmInstanceMod?.RefreshBars())
|
||||
End If
|
||||
End Sub
|
||||
Public Function GetTargetModLoaders() As List(Of CompModLoaderType)
|
||||
Dim ModLoaders As New List(Of CompModLoaderType)
|
||||
If PageInstanceLeft.Instance.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
|
||||
If PageInstanceLeft.Instance.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)
|
||||
If PageInstanceLeft.Instance.Version.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
|
||||
If PageInstanceLeft.Instance.Version.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
|
||||
If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.NeoForge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
|
||||
Return ModLoaders
|
||||
End Function
|
||||
|
||||
End Module
|
||||
@@ -1,6 +1,4 @@
|
||||
Imports System.IO.Compression
|
||||
|
||||
Public Module ModModpack
|
||||
Public Module ModModpack
|
||||
|
||||
'触发整合包安装的外部接口
|
||||
''' <summary>
|
||||
@@ -15,7 +13,7 @@ Public Module ModModpack
|
||||
ModpackInstall(File)
|
||||
Catch ex As CancelledException
|
||||
Catch ex As Exception
|
||||
Log(ex, "手动安装整合包失败", LogLevel.Msgbox)
|
||||
Log(ex, "手动安装整合包失败", NotifyLevel.MsgBox)
|
||||
End Try
|
||||
End Sub)
|
||||
End Sub
|
||||
@@ -251,8 +249,35 @@ Retry:
|
||||
ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
|
||||
Sub(Task As LoaderTask(Of Integer, JArray))
|
||||
Task.Output = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & ModList.Join(",") & "]}", "application/json")("data")
|
||||
'如果文件已被删除,则 API 会跳过那一项
|
||||
If ModList.Count > Task.Output.Count Then Throw New Exception("整合包中的部分 Mod 版本已被 Mod 作者删除,所以没法继续安装了,请向整合包作者反馈该问题")
|
||||
If ModList.Count = Task.Output.Count Then Return
|
||||
'如果文件已被删除,则 API 会跳过那一项,导致返回的列表数量小于请求的数量。后面的都是对此的错误处理。
|
||||
'获取缺失的 fileId 列表
|
||||
Dim MissingIds = ModList.Except(Task.Output.Select(Function(j) j("id").ToObject(Of Integer))).ToList
|
||||
Log("[Modpack] 缺失的 fileIds:" & MissingIds.Join("、"))
|
||||
'将 fileId 映射到 projectId
|
||||
Dim Missings = MissingIds.Select(Of (ProjectId As Integer, FileId As Integer))(
|
||||
Function(id) (CType(Json("files").AsEnumerable.First(Function(f) f("fileID").ToObject(Of Integer) = id), JObject)("projectID").ToObject(Of Integer), id)).ToList
|
||||
Log("[Modpack] 缺失项:" & Missings.Join("、"))
|
||||
'尝试获取对应的工程信息
|
||||
Dim Projects As New List(Of ResourceProject)
|
||||
Try
|
||||
Projects = ResourceProject.FromProjectIds(Missings.Select(Function(e) e.ProjectId.ToString), ResourcePlatforms.CurseForge).ToList
|
||||
Catch ex As Exception
|
||||
Log(ex, "获取缺失的工程信息失败")
|
||||
End Try
|
||||
Dim MissingDesc As New List(Of String)
|
||||
For Each Missing In Missings
|
||||
Dim Project = Projects.FirstOrDefault(Function(p) p.Id = Missing.ProjectId)
|
||||
If Project Is Nothing Then
|
||||
MissingDesc.Add($"未知 CurseForge 项目 (项目 ID {Missing.ProjectId},文件 ID {Missing.FileId})")
|
||||
Else
|
||||
MissingDesc.Add($"{Project.TranslatedName} (文件 ID {Missing.FileId})")
|
||||
End If
|
||||
Next
|
||||
If MyMsgBox($"未找到该整合包所需的以下内容,它们可能已被原作者删除:{vbCrLf}- {MissingDesc.Join(vbCrLf & "- ")}{vbCrLf}{vbCrLf}是否继续安装?缺失这些内容可能导致整合包出现异常!",
|
||||
"整合包内容缺失", "继续", "取消", IsWarn:=True) = 2 Then
|
||||
Task.Interrupt()
|
||||
End If
|
||||
End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
|
||||
'构造 NetFile
|
||||
ModDownloadLoaders.Add(New LoaderTask(Of JArray, List(Of NetFile))("构造 Mod 下载信息",
|
||||
@@ -269,23 +294,23 @@ Retry:
|
||||
End If
|
||||
End If
|
||||
'根据 modules 和文件名后缀判断资源类型
|
||||
Dim TargetFolder As String, Type As CompType
|
||||
Dim TargetFolder As String, Type As ResourceTypes
|
||||
If ModJson("modules").Any Then 'modules 可能返回 null(#1006)
|
||||
Dim ModuleNames = CType(ModJson("modules"), JArray).Select(Function(l) l("name").ToString).ToList
|
||||
If ModuleNames.Contains("META-INF") OrElse ModuleNames.Contains("mcmod.info") OrElse
|
||||
ModJson?("FileName")?.ToString.EndsWithF(".jar", True) Then
|
||||
TargetFolder = "mods" : Type = CompType.Mod
|
||||
TargetFolder = "mods" : Type = ResourceTypes.Mod
|
||||
ElseIf ModuleNames.Contains("pack.mcmeta") Then
|
||||
TargetFolder = "resourcepacks" : Type = CompType.ResourcePack
|
||||
TargetFolder = "resourcepacks" : Type = ResourceTypes.ResourcePack
|
||||
Else
|
||||
TargetFolder = "shaderpacks" : Type = CompType.Shader
|
||||
TargetFolder = "shaderpacks" : Type = ResourceTypes.Shader
|
||||
End If
|
||||
Else
|
||||
TargetFolder = "mods" : Type = CompType.Mod
|
||||
TargetFolder = "mods" : Type = ResourceTypes.Mod
|
||||
End If
|
||||
'建立 CompFile
|
||||
Dim File As New CompFile(ModJson, Type)
|
||||
If Not File.Available Then Continue For
|
||||
Dim File = ResourceVersion.FromPlatformJson(ModJson, Type)
|
||||
If Not File.DownloadAvailable Then Continue For
|
||||
'实际的添加
|
||||
FileList.Add(Id, File.ToNetFile($"{McFolderSelected}versions\{InstanceName}\{TargetFolder}\"))
|
||||
Task.Progress += 1 / (1 + ModList.Count)
|
||||
@@ -330,7 +355,7 @@ Retry:
|
||||
File.Delete(Target)
|
||||
End If
|
||||
Next
|
||||
If File.Exists(FileAddress) AndAlso IO.Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
|
||||
If File.Exists(FileAddress) AndAlso Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
|
||||
Log("[ModPack] 删除安装整合包文件:" & FileAddress)
|
||||
File.Delete(FileAddress)
|
||||
End If
|
||||
@@ -425,11 +450,11 @@ Retry:
|
||||
End Select
|
||||
End If
|
||||
'添加下载文件
|
||||
Dim Urls = File("downloads").SelectMany(Function(t) CompFile.HandleCurseForgeDownloadUrls(t.ToString)).ToList
|
||||
Dim Urls = File("downloads").SelectMany(Function(t) ResourceVersion.ParseCurseForgeDownloadUrls(t.ToString)).ToList
|
||||
Urls.AddRange(Urls.Select(Function(u) DlSourceModGet(u)).ToList)
|
||||
Urls = Urls.Distinct.ToList()
|
||||
Dim TargetPath As String = $"{McFolderSelected}versions\{InstanceName}\{File("path")}"
|
||||
If Not IO.Path.GetFullPath(TargetPath).StartsWithF($"{McFolderSelected}versions\{InstanceName}\", True) Then
|
||||
If Not Path.GetFullPath(TargetPath).StartsWithF($"{McFolderSelected}versions\{InstanceName}\", True) Then
|
||||
MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!" & vbCrLf & "错误的文件:" & TargetPath, "文件路径校验失败", IsWarn:=True)
|
||||
Throw New CancelledException
|
||||
End If
|
||||
@@ -471,7 +496,7 @@ Retry:
|
||||
File.Delete(Target)
|
||||
End If
|
||||
Next
|
||||
If File.Exists(FileAddress) AndAlso IO.Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
|
||||
If File.Exists(FileAddress) AndAlso Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
|
||||
Log("[ModPack] 删除安装整合包文件:" & FileAddress)
|
||||
File.Delete(FileAddress)
|
||||
End If
|
||||
@@ -576,18 +601,18 @@ Retry:
|
||||
Task, 0.4)
|
||||
'读取 MMC 设置文件(#2655)
|
||||
Try
|
||||
Dim MMCSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
|
||||
If File.Exists(MMCSetupFile) Then
|
||||
Dim MmcSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
|
||||
If File.Exists(MmcSetupFile) Then
|
||||
'将其中的等号替换为冒号,以符合 ini 文件格式
|
||||
Dim Lines As New List(Of String)
|
||||
For Each Line In ReadFile(MMCSetupFile).Split({vbCr, vbLf}, StringSplitOptions.RemoveEmptyEntries)
|
||||
For Each Line In ReadFile(MmcSetupFile).Split(vbCrLf.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
|
||||
If Not Line.Contains("=") Then Continue For
|
||||
Lines.Add(Line.BeforeFirst("=") & ":" & Line.AfterFirst("="))
|
||||
Next
|
||||
WriteFile(MMCSetupFile, Lines.Join(vbCrLf))
|
||||
FileUtils.Write(MmcSetupFile, Lines.Join(vbCrLf))
|
||||
'读取文件
|
||||
If ReadIni(MMCSetupFile, "OverrideCommands", False) Then
|
||||
Dim PreLaunchCommand As String = ReadIni(MMCSetupFile, "PreLaunchCommand")
|
||||
If ReadIni(MmcSetupFile, "OverrideCommands", False) Then
|
||||
Dim PreLaunchCommand As String = ReadIni(MmcSetupFile, "PreLaunchCommand")
|
||||
If PreLaunchCommand <> "" Then
|
||||
PreLaunchCommand = PreLaunchCommand.Replace("\""", """").
|
||||
Replace("$INST_JAVA", "{java}javaw.exe").
|
||||
@@ -598,16 +623,16 @@ Retry:
|
||||
Log("[ModPack] 迁移 MultiMC 版本独立设置:启动前执行命令:" & PreLaunchCommand)
|
||||
End If
|
||||
End If
|
||||
If ReadIni(MMCSetupFile, "JoinServerOnLaunch", False) Then
|
||||
Dim ServerAddress As String = ReadIni(MMCSetupFile, "JoinServerOnLaunchAddress").Replace("\""", """")
|
||||
If ReadIni(MmcSetupFile, "JoinServerOnLaunch", False) Then
|
||||
Dim ServerAddress As String = ReadIni(MmcSetupFile, "JoinServerOnLaunchAddress").Replace("\""", """")
|
||||
WriteIni(SetupFile, "VersionServerEnter", ServerAddress)
|
||||
Log("[ModPack] 迁移 MultiMC 版本独立设置:自动进入服务器:" & ServerAddress)
|
||||
End If
|
||||
If ReadIni(MMCSetupFile, "IgnoreJavaCompatibility", False) Then
|
||||
If ReadIni(MmcSetupFile, "IgnoreJavaCompatibility", False) Then
|
||||
WriteIni(SetupFile, "VersionAdvanceJava", True)
|
||||
Log("[ModPack] 迁移 MultiMC 版本独立设置:忽略 Java 兼容性警告")
|
||||
End If
|
||||
Dim Logo As String = ReadIni(MMCSetupFile, "iconKey", "")
|
||||
Dim Logo As String = ReadIni(MmcSetupFile, "iconKey", "")
|
||||
If Logo <> "" AndAlso File.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png") Then
|
||||
WriteIni(SetupFile, "LogoCustom", True)
|
||||
WriteIni(SetupFile, "Logo", "PCL\Logo.png")
|
||||
@@ -615,9 +640,9 @@ Retry:
|
||||
Log($"[ModPack] 迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
|
||||
End If
|
||||
'JVM 参数
|
||||
Dim JvmArgs As String = ReadIni(MMCSetupFile, "JvmArgs", "")
|
||||
Dim JvmArgs As String = ReadIni(MmcSetupFile, "JvmArgs", "")
|
||||
If JvmArgs <> "" Then
|
||||
If ReadIni(MMCSetupFile, "OverrideJavaArgs", False) Then
|
||||
If ReadIni(MmcSetupFile, "OverrideJavaArgs", False) Then
|
||||
WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
|
||||
Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数(覆盖):" & JvmArgs)
|
||||
Else
|
||||
@@ -770,6 +795,7 @@ Retry:
|
||||
New LoaderTask(Of String, Integer)("解压压缩包",
|
||||
Sub(Task As LoaderTask(Of String, Integer))
|
||||
ExtractModpackFiles(TargetFolder, FileAddress, Task, 0.9)
|
||||
OpenExplorer(TargetFolder)
|
||||
Thread.Sleep(400) '避免文件争用
|
||||
'查找解压后的 exe 文件
|
||||
Dim Launcher As String = Nothing
|
||||
@@ -779,7 +805,7 @@ Retry:
|
||||
If Info.ProductName = "Plain Craft Launcher" Then
|
||||
Launcher = ExeFile
|
||||
Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}")
|
||||
ElseIf (Info.ProductName.ContainsF("Launcher", True) OrElse Info.ProductName.ContainsF("启动", True)) AndAlso
|
||||
ElseIf (Info.ProductName.ContainsIgnoreCase("Launcher") OrElse Info.ProductName.Contains("启动")) AndAlso
|
||||
Not Info.ProductName = "Plain Craft Launcher Admin Manager" Then
|
||||
If Launcher Is Nothing Then
|
||||
Launcher = ExeFile
|
||||
@@ -792,7 +818,6 @@ Retry:
|
||||
If Launcher IsNot Nothing Then
|
||||
Log("[Modpack] 找到压缩包中附带的启动器:" & Launcher)
|
||||
If MyMsgBox($"整合包里似乎自带了启动器,是否换用它继续安装?{vbCrLf}即将打开:{Launcher}", "换用整合包启动器?", "换用", "不换用") = 1 Then
|
||||
OpenExplorer(TargetFolder)
|
||||
StartProcess(Launcher, "--wait") '要求等待已有的 PCL 退出
|
||||
Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序")
|
||||
FrmMain.EndProgram(False)
|
||||
@@ -801,10 +826,9 @@ Retry:
|
||||
Else
|
||||
Log("[Modpack] 未找到压缩包中附带的启动器")
|
||||
End If
|
||||
OpenExplorer(TargetFolder)
|
||||
'加入文件夹列表
|
||||
Dim InstanceName As String = GetFolderNameFromPath(TargetFolder)
|
||||
Directory.CreateDirectory(TargetFolder & ".minecraft\")
|
||||
DirectoryUtils.Create(TargetFolder & ".minecraft\")
|
||||
PageSelectLeft.AddFolder(
|
||||
TargetFolder & ".minecraft\" & ArchiveBaseFolder.Replace("/", "\").TrimStart("\"), '格式例如:包裹文件夹\.minecraft\(最短为空字符串)
|
||||
InstanceName, False)
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
Loop
|
||||
WatcherLog("Minecraft 日志监控已退出")
|
||||
Catch ex As Exception
|
||||
Log(ex, "Minecraft 日志监控主循环出错", LogLevel.Feedback)
|
||||
Log(ex, "Minecraft 日志监控主循环出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
State = MinecraftState.Ended
|
||||
End Try
|
||||
End Sub, "Minecraft Watcher PID " & PID)
|
||||
@@ -174,14 +174,14 @@
|
||||
End If
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "输出 Minecraft 日志失败", LogLevel.Feedback)
|
||||
Log(ex, "输出 Minecraft 日志失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
Public LatestLog As New Queue(Of String)
|
||||
Private Sub GameLog(Text As String)
|
||||
'预处理
|
||||
If Text Is Nothing Then Return
|
||||
Text = Text.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf)
|
||||
Text = Text.ReplaceLineEndings(vbCrLf)
|
||||
'If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
|
||||
'加入预存储
|
||||
LatestLog.Enqueue(Text)
|
||||
@@ -194,7 +194,7 @@
|
||||
If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
|
||||
WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
|
||||
LogProgress = 2
|
||||
ElseIf LogProgress < 3 AndAlso Text.ContainsF("lwjgl version", True) Then
|
||||
ElseIf LogProgress < 3 AndAlso Text.ContainsIgnoreCase("lwjgl version") Then
|
||||
WatcherLog("日志 3/5:LWJGL 版本已确认")
|
||||
LogProgress = 3
|
||||
ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
|
||||
@@ -265,7 +265,7 @@
|
||||
MinecraftWindow = TryGetMinecraftWindow()
|
||||
Catch ex As ComponentModel.Win32Exception
|
||||
'拒绝访问(#1062)
|
||||
Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", LogLevel.Hint)
|
||||
Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", NotifyLevel.AllUsers)
|
||||
IsWindowFinished = True
|
||||
End Try
|
||||
If MinecraftWindow Is Nothing Then Return
|
||||
@@ -297,7 +297,7 @@
|
||||
End If
|
||||
IsWindowAppeared = True
|
||||
Catch ex As Exception
|
||||
Log(ex, "检查 Minecraft 窗口失败", LogLevel.Feedback)
|
||||
Log(ex, "检查 Minecraft 窗口失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
''' <summary>
|
||||
@@ -361,9 +361,9 @@
|
||||
Analyzer.Prepare()
|
||||
Analyzer.Analyze()
|
||||
Analyzer.Output(False, New List(Of String) From
|
||||
{Instance.PathVersion & Instance.Name & ".json", Path & "PCL\Log1.txt", Path & "PCL\LatestLaunch.bat"})
|
||||
{Instance.PathVersion & Instance.Name & ".json", PathExeFolder & "PCL\Log1.txt", PathExeFolder & "PCL\LatestLaunch.bat"})
|
||||
Catch ex As Exception
|
||||
Log(ex, "崩溃分析失败", LogLevel.Feedback)
|
||||
Log(ex, "崩溃分析失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub, "Crash Analyzer")
|
||||
End Sub
|
||||
@@ -376,7 +376,7 @@
|
||||
If Not GameProcess.HasExited Then GameProcess.Kill()
|
||||
WatcherLog("已强制结束 Minecraft 进程")
|
||||
Catch ex As Exception
|
||||
Log(ex, "强制结束 Minecraft 进程失败", LogLevel.Hint)
|
||||
Log(ex, "强制结束 Minecraft 进程失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
Public Module ModDevelop
|
||||
Public Class ModDevelop
|
||||
|
||||
#If DEBUG Then
|
||||
Public Sub Start()
|
||||
Public Shared Sub Start()
|
||||
End Sub
|
||||
#End If
|
||||
|
||||
End Module
|
||||
End Class
|
||||
|
||||
@@ -186,7 +186,7 @@ Public Class CustomEvent
|
||||
MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
|
||||
Return
|
||||
End If
|
||||
Hint("正在开启中,请稍候:" & Arg)
|
||||
Hint("正在开启,请稍候:" & Arg)
|
||||
RunInThread(Sub() OpenWebsite(Arg))
|
||||
|
||||
Case EventType.打开文件, EventType.打开帮助, EventType.执行命令
|
||||
@@ -210,7 +210,7 @@ Public Class CustomEvent
|
||||
StartProcess(Info)
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "执行打开类自定义事件失败", LogLevel.Msgbox)
|
||||
Log(ex, "执行打开类自定义事件失败", NotifyLevel.MsgBox)
|
||||
End Try
|
||||
End Sub)
|
||||
|
||||
@@ -267,12 +267,12 @@ Public Class CustomEvent
|
||||
MyMsgBox(Args(1).Replace("\n", vbCrLf), Args(0).Replace("\n", vbCrLf), If(Args.Length > 2, Args(2), "确定"))
|
||||
|
||||
Case EventType.弹出提示
|
||||
Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ParseToEnum(Of HintType)))
|
||||
Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ToEnum(Of HintType)))
|
||||
|
||||
Case EventType.切换页面
|
||||
RunInUi(Sub() FrmMain.PageChange(
|
||||
Args(0).ParseToEnum(Of FormMain.PageType),
|
||||
If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ParseToEnum(Of FormMain.PageSubType))))
|
||||
Args(0).ToEnum(Of FormMain.PageType),
|
||||
If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ToEnum(Of FormMain.PageSubType))))
|
||||
|
||||
Case EventType.导入整合包, EventType.安装整合包
|
||||
RunInUi(Sub() ModpackInstall())
|
||||
@@ -328,7 +328,7 @@ Public Class CustomEvent
|
||||
MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败")
|
||||
End Select
|
||||
Catch ex As Exception
|
||||
Log(ex, $"事件执行失败({Type}, {Arg})", LogLevel.Msgbox)
|
||||
Log(ex, $"事件执行失败({Type}, {Arg})", NotifyLevel.MsgBox)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -372,20 +372,20 @@ Public Class CustomEvent
|
||||
RelativeUrl = RelativeUrl.Replace("/", "\").Lower.TrimStart("\")
|
||||
|
||||
'确认实际路径
|
||||
Dim Location As String, WorkingDir As String = Path & "PCL"
|
||||
Dim Location As String, WorkingDir As String = PathExeFolder & "PCL"
|
||||
HelpTryExtract()
|
||||
If RelativeUrl.Contains(":\") Then
|
||||
'绝对路径
|
||||
Location = RelativeUrl
|
||||
Log("[Control] 自定义事件中由绝对路径" & Type & ":" & Location)
|
||||
ElseIf File.Exists(Path & "PCL\" & RelativeUrl) Then
|
||||
ElseIf File.Exists(PathExeFolder & "PCL\" & RelativeUrl) Then
|
||||
'相对 PCL 文件夹的路径
|
||||
Location = Path & "PCL\" & RelativeUrl
|
||||
Location = PathExeFolder & "PCL\" & RelativeUrl
|
||||
Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & Type & ":" & Location)
|
||||
ElseIf File.Exists(Path & "PCL\Help\" & RelativeUrl) Then
|
||||
ElseIf File.Exists(PathExeFolder & "PCL\Help\" & RelativeUrl) Then
|
||||
'相对 PCL 本地帮助文件夹的路径
|
||||
Location = Path & "PCL\Help\" & RelativeUrl
|
||||
WorkingDir = Path & "PCL\Help\"
|
||||
Location = PathExeFolder & "PCL\Help\" & RelativeUrl
|
||||
WorkingDir = PathExeFolder & "PCL\Help\"
|
||||
Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & Type & ":" & Location)
|
||||
ElseIf Type = EventType.打开帮助 AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
|
||||
'相对 PCL 自带帮助文件夹的路径
|
||||
|
||||
@@ -23,12 +23,12 @@ Public Module ModMain
|
||||
''' <summary>
|
||||
''' 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。
|
||||
''' </summary>
|
||||
Private HintWaiting As SafeList(Of HintMessage) = If(HintWaiting, New SafeList(Of HintMessage))
|
||||
Private HintWaiting As ConcurrentList(Of HintMessage) = If(HintWaiting, New ConcurrentList(Of HintMessage))
|
||||
''' <summary>
|
||||
''' 在窗口左下角弹出提示文本。
|
||||
''' </summary>
|
||||
Public Sub Hint(Text As String, Optional Type As HintType = HintType.Blue, Optional Log As Boolean = True)
|
||||
If HintWaiting Is Nothing Then HintWaiting = New SafeList(Of HintMessage)
|
||||
If HintWaiting Is Nothing Then HintWaiting = New ConcurrentList(Of HintMessage)
|
||||
HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
|
||||
End Sub
|
||||
|
||||
@@ -45,7 +45,7 @@ Public Module ModMain
|
||||
'End If
|
||||
Dim CurrentHint = HintWaiting(0)
|
||||
'去回车
|
||||
CurrentHint.Text = CurrentHint.Text.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ")
|
||||
CurrentHint.Text = CurrentHint.Text.ReplaceLineEndings(" ", mergeMultiple:=True)
|
||||
'超量提示直接忽略
|
||||
If FrmMain.PanHint.Children.Count >= 20 Then GoTo EndHint
|
||||
'检查是否有重复提示
|
||||
@@ -71,7 +71,7 @@ Public Module ModMain
|
||||
'有重复提示,且该提示的进入动画已播放
|
||||
If Not AniIsRun("Hint Show " & DoubleStack.Tag(1)) Then
|
||||
AniStop("Hint Hide " & DoubleStack.Tag(1))
|
||||
Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
|
||||
Dim Delay As Double = (800 + CurrentHint.Text.Length.Clamp(5, 23) * 180) * AniSpeed
|
||||
AniStart({
|
||||
AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50,, New AniEaseOutFluent),
|
||||
AaX(DoubleStack, -8, 50, 50, New AniEaseInFluent),
|
||||
@@ -122,7 +122,7 @@ Public Module ModMain
|
||||
})
|
||||
AniStart(Animations, "Hint Show " & NewHintControl.Tag(1))
|
||||
'结束动画
|
||||
Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
|
||||
Dim Delay As Double = (800 + CurrentHint.Text.Length.Clamp(5, 23) * 180) * AniSpeed
|
||||
AniStart({
|
||||
AaX(NewHintControl, -50, 200, Delay, New AniEaseInFluent),
|
||||
AaOpacity(NewHintControl, -1, 150, Delay),
|
||||
@@ -137,7 +137,7 @@ EndHint:
|
||||
HintWaiting.RemoveAt(0)
|
||||
Loop
|
||||
Catch ex As Exception
|
||||
Log(ex, "显示弹出提示失败", LogLevel.Normal)
|
||||
Log(ex, "显示弹出提示失败", NotifyLevel.Silent)
|
||||
End Try
|
||||
End Sub
|
||||
Private Sub HideAllHint()
|
||||
@@ -256,7 +256,7 @@ EndHint:
|
||||
MsgBox(Caption, MsgBoxStyle.OkOnly + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
|
||||
Converter.Result = 1
|
||||
End If
|
||||
Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, LogLevel.Debug)
|
||||
Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, NotifyLevel.DebugModeOnly)
|
||||
Else
|
||||
Try
|
||||
FrmMain.DragStop()
|
||||
@@ -285,9 +285,9 @@ EndHint:
|
||||
''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
|
||||
''' <param name="Button2">显示的第二个按钮,默认为“取消”。</param>
|
||||
''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
|
||||
Public Function MyMsgBoxInput(Title As String, Optional Text As String = "", Optional DefaultInput As String = "", Optional ValidateRules As ObjectModel.Collection(Of Validate) = Nothing, Optional HintText As String = "", Optional Button1 As String = "确定", Optional Button2 As String = "取消", Optional IsWarn As Boolean = False) As String
|
||||
Public Function MyMsgBoxInput(Title As String, Optional Text As String = Nothing, Optional DefaultInput As String = Nothing, Optional ValidateRules As ObjectModel.Collection(Of Validate) = Nothing, Optional HintText As String = "", Optional Button1 As String = "确定", Optional Button2 As String = "取消", Optional IsWarn As Boolean = False) As String
|
||||
'将弹窗列入队列
|
||||
Dim Converter As New MyMsgBoxConverter With {.Text = Text, .HintText = HintText, .Type = MyMsgBoxType.Input, .ValidateRules = If(ValidateRules, New ObjectModel.Collection(Of Validate)), .Button1 = Button1, .Button2 = Button2, .Content = DefaultInput, .IsWarn = IsWarn, .Title = Title}
|
||||
Dim Converter As New MyMsgBoxConverter With {.Text = If(Text, ""), .HintText = If(HintText, ""), .Type = MyMsgBoxType.Input, .ValidateRules = If(ValidateRules, New ObjectModel.Collection(Of Validate)), .Button1 = Button1, .Button2 = Button2, .Content = DefaultInput, .IsWarn = IsWarn, .Title = Title}
|
||||
WaitingMyMsgBox.Add(Converter)
|
||||
'虽然我也不知道这是啥但是能用就成了 :)
|
||||
Try
|
||||
@@ -354,7 +354,7 @@ EndHint:
|
||||
If FrmMain.PanMsg.Visibility <> Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
|
||||
Log(ex, "处理等待中的弹窗失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -418,7 +418,7 @@ EndHint:
|
||||
Public FrmInstanceExport As PageInstanceExport
|
||||
|
||||
'资源信息分页声明
|
||||
Public FrmDownloadCompDetail As PageDownloadCompDetail
|
||||
Public FrmDownloadResourceDetail As PageDownloadResourceDetail
|
||||
|
||||
#End Region
|
||||
|
||||
@@ -488,7 +488,7 @@ EndHint:
|
||||
''' </summary>
|
||||
Public Sub New(FilePath As String)
|
||||
RawPath = FilePath
|
||||
Dim JsonData As JObject = GetJson(ArgumentReplace(ReadFile(FilePath), AddressOf EscapeXML))
|
||||
Dim JsonData As JObject = GetJson(ArgumentReplace(ReadFile(FilePath), AddressOf EscapeUtils.XmlEscape))
|
||||
If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
|
||||
'加载常规信息
|
||||
If JsonData("Title") IsNot Nothing Then
|
||||
@@ -577,8 +577,8 @@ EndHint:
|
||||
Try
|
||||
Dim IgnoreList As New List(Of String)
|
||||
'读取自定义文件
|
||||
If Directory.Exists(Path & "PCL\Help\") Then
|
||||
For Each File In EnumerateFiles(Path & "PCL\Help\")
|
||||
If Directory.Exists(PathExeFolder & "PCL\Help\") Then
|
||||
For Each File In EnumerateFiles(PathExeFolder & "PCL\Help\")
|
||||
Select Case File.Extension.Lower
|
||||
Case ".helpignore"
|
||||
'加载忽略列表
|
||||
@@ -612,7 +612,7 @@ NextFile:
|
||||
Next
|
||||
Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
|
||||
Catch ex As Exception
|
||||
Log(ex, "检查帮助文件夹失败", LogLevel.Msgbox)
|
||||
Log(ex, "检查帮助文件夹失败", NotifyLevel.MsgBox)
|
||||
End Try
|
||||
If Loader.IsInterrupted Then Return
|
||||
|
||||
@@ -624,7 +624,7 @@ NextFile:
|
||||
Dict.Add(Entry)
|
||||
If ModeDebug Then Log("[Help] 已加载的帮助条目:" & Entry.Title & " ← " & FilePath)
|
||||
Catch ex As Exception
|
||||
Log(ex, "初始化帮助条目失败(" & FilePath & ")", LogLevel.Msgbox)
|
||||
Log(ex, "初始化帮助条目失败(" & FilePath & ")", NotifyLevel.MsgBox)
|
||||
End Try
|
||||
Next
|
||||
|
||||
@@ -645,11 +645,11 @@ NextFile:
|
||||
Public Sub HelpTryExtract()
|
||||
If Settings.Get("SystemHelpVersion") <> VersionCode OrElse Not File.Exists(PathTemp & "Help\启动器\备份设置.xaml") Then
|
||||
DeleteDirectory(PathTemp & "Help")
|
||||
Directory.CreateDirectory(PathTemp & "Help")
|
||||
WriteFile(PathTemp & "Cache\Help.zip", GetResources("Help"))
|
||||
DirectoryUtils.Create(PathTemp & "Help")
|
||||
FileUtils.Write(PathTemp & "Cache\Help.zip", GetResources("Help"))
|
||||
ExtractCompressedFile(PathTemp & "Cache\Help.zip", PathTemp & "Help")
|
||||
Settings.Set("SystemHelpVersion", VersionCode)
|
||||
Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), LogLevel.Debug)
|
||||
Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), NotifyLevel.DebugModeOnly)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
@@ -754,8 +754,8 @@ NextFile:
|
||||
FrmLaunchLeft.AprilPosTrans.X += Speed.X
|
||||
FrmLaunchLeft.AprilPosTrans.Y += Speed.Y
|
||||
'大小改变
|
||||
FrmLaunchLeft.AprilScaleTrans.ScaleX = MathClamp(1 - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160), 0.2, 1.8)
|
||||
FrmLaunchLeft.AprilScaleTrans.ScaleY = MathClamp(1 - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100), 0.2, 1.8)
|
||||
FrmLaunchLeft.AprilScaleTrans.ScaleX = (1 - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160)).Clamp(0.2, 1.8)
|
||||
FrmLaunchLeft.AprilScaleTrans.ScaleY = (1 - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100)).Clamp(0.2, 1.8)
|
||||
'放弃提示
|
||||
Static GiveUpDistance As Double = -1500
|
||||
GiveUpDistance += SpeedValue
|
||||
@@ -774,7 +774,7 @@ NextFile:
|
||||
End If
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "愚人节移动出错", LogLevel.Feedback)
|
||||
Log(ex, "愚人节移动出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
@@ -790,7 +790,7 @@ NextFile:
|
||||
PostMessage(Handle, 400 * 16 + 2, 0, 0)
|
||||
SetForegroundWindow(Handle) '不在这里放不行,神秘 WinAPI,建议别动
|
||||
Catch ex As Exception
|
||||
Log(ex, "设置窗口置顶失败", LogLevel.Hint)
|
||||
Log(ex, "设置窗口置顶失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End Sub
|
||||
Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ClassName As String, WindowName As String) As IntPtr
|
||||
@@ -828,24 +828,26 @@ NextFile:
|
||||
''' 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
|
||||
''' </summary>
|
||||
Public Function ArgumentReplace(Text As String, Optional EscapeHandler As Func(Of String, String) = Nothing, Optional ReplaceTime As Boolean = True) As String
|
||||
If Text Is Nothing OrElse Not Text.Contains("{") Then Return Nothing
|
||||
'预处理
|
||||
If Text Is Nothing Then Return Nothing
|
||||
Dim Replacer =
|
||||
Static Replacer As Func(Of String, String) =
|
||||
Function(s As String) As String
|
||||
If s Is Nothing Then Return ""
|
||||
If EscapeHandler Is Nothing Then Return s
|
||||
If s.Contains(":\") Then s = ShortenPath(s)
|
||||
If s.Contains(":\") Then s = FileUtils.ShortenPath(s)
|
||||
Return EscapeHandler(s)
|
||||
End Function
|
||||
'基础
|
||||
Text = Text.Replace("{pcl_version}", Replacer(VersionBaseName))
|
||||
Text = Text.Replace("{pcl_version_code}", Replacer(VersionCode))
|
||||
Text = Text.Replace("{pcl_version_branch}", Replacer(VersionBranchName))
|
||||
Text = Text.Replace("{pcl_version_branch}", Replacer(BuildTypeDisplay))
|
||||
Text = Text.Replace("{pcl_build_type}", Replacer(BuildType.ToString))
|
||||
Text = Text.Replace("{pcl_branch}", Replacer(VersionBranchMain))
|
||||
Text = Text.Replace("{identify}", Replacer(Identify))
|
||||
Text = Text.Replace("{path}", Replacer(Path))
|
||||
Text = Text.Replace("{path_with_name}", Replacer(PathWithName))
|
||||
Text = Text.Replace("{path}", Replacer(PathExeFolder))
|
||||
Text = Text.Replace("{path_with_name}", Replacer(PathExe))
|
||||
Text = Text.Replace("{path_temp}", Replacer(PathTemp))
|
||||
Text = Text.ReplaceOnDemand("{pcl_md5}", Function() Replacer(GetFileMD5(PathExe)))
|
||||
'时间
|
||||
If ReplaceTime Then '在窗口标题中,时间会被后续动态替换,所以此时不应该替换
|
||||
Text = Text.Replace("{date}", Replacer(Date.Now.ToString("yyyy'/'M'/'d")))
|
||||
@@ -889,11 +891,11 @@ NextFile:
|
||||
Text = Text.Replace("{login}", Replacer(Nothing))
|
||||
End If
|
||||
'高级
|
||||
Text = Text.RegexReplaceEach("\{hint\}", Function() Replacer(PageOtherTest.GetRandomHint()))
|
||||
Text = Text.RegexReplaceEach("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
|
||||
Text = Text.RegexReplaceEach("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Settings.GetSafe(m.Groups(1).Value, McInstanceSelected)))
|
||||
Text = Text.RegexReplaceEach("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
|
||||
Text = Text.RegexReplaceEach("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
|
||||
Text = Text.RegexReplace("\{hint\}", Function() Replacer(PageOtherTest.GetRandomHint()))
|
||||
Text = Text.RegexReplace("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
|
||||
Text = Text.RegexReplace("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Settings.GetSafe(m.Groups(1).Value, McInstanceSelected)))
|
||||
Text = Text.RegexReplace("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
|
||||
Text = Text.RegexReplace("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
|
||||
Return Text
|
||||
End Function
|
||||
|
||||
@@ -941,14 +943,14 @@ NextFile:
|
||||
Try
|
||||
ResultFolder = $"{PathTemp}TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
|
||||
If RequireNonSpace AndAlso ResultFolder.Contains(" ") Then Exit Try '带空格
|
||||
Directory.CreateDirectory(ResultFolder)
|
||||
DirectoryUtils.Create(ResultFolder)
|
||||
CheckPermissionWithException(ResultFolder)
|
||||
Return ResultFolder
|
||||
Catch
|
||||
End Try
|
||||
'使用备用路径
|
||||
ResultFolder = $"{OsDrive}ProgramData\PCL\TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
|
||||
Directory.CreateDirectory(ResultFolder)
|
||||
DirectoryUtils.Create(ResultFolder)
|
||||
CheckPermission(ResultFolder)
|
||||
Return ResultFolder
|
||||
End Function
|
||||
@@ -968,7 +970,7 @@ NextFile:
|
||||
If ThemeDontClick = 2 Then ThemeRefresh()
|
||||
#End Region
|
||||
Catch ex As Exception
|
||||
Log(ex, "短程主时钟执行异常", LogLevel.Critical)
|
||||
Log(ex, "短程主时钟执行异常", NotifyLevel.Critical)
|
||||
End Try
|
||||
Timer4Count += 1
|
||||
If Timer4Count = 4 Then
|
||||
@@ -978,7 +980,7 @@ NextFile:
|
||||
If ThemeNow = 12 Then ThemeRefresh()
|
||||
#End Region
|
||||
Catch ex As Exception
|
||||
Log(ex, "中程主时钟执行异常", LogLevel.Debug)
|
||||
Log(ex, "中程主时钟执行异常", NotifyLevel.DebugModeOnly)
|
||||
End Try
|
||||
End If
|
||||
Timer150Count += 1
|
||||
@@ -997,7 +999,7 @@ NextFile:
|
||||
End Sub)
|
||||
#End Region
|
||||
Catch ex As Exception
|
||||
Log(ex, "长程主时钟执行异常", LogLevel.Critical)
|
||||
Log(ex, "长程主时钟执行异常", NotifyLevel.Critical)
|
||||
End Try
|
||||
End If
|
||||
End Sub
|
||||
@@ -1010,7 +1012,7 @@ NextFile:
|
||||
Thread.Sleep(50 * 0.98)
|
||||
Loop
|
||||
Catch ex As Exception
|
||||
Log(ex, "程序主时钟出错", LogLevel.Feedback)
|
||||
Log(ex, "程序主时钟出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub, "Timer Main")
|
||||
If Not IsAprilEnabled Then Return
|
||||
@@ -1026,7 +1028,7 @@ NextFile:
|
||||
Thread.Sleep(1)
|
||||
Loop
|
||||
Catch ex As Exception
|
||||
Log(ex, "愚人节主时钟出错", LogLevel.Feedback)
|
||||
Log(ex, "愚人节主时钟出错", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub, "Timer Main Fool")
|
||||
End Sub
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Public Module ModMusic
|
||||
Imports NAudio.Wave
|
||||
|
||||
Public Module ModMusic
|
||||
|
||||
#Region "播放列表"
|
||||
|
||||
@@ -21,8 +23,8 @@
|
||||
'初始化全部可用音乐列表
|
||||
If MusicAllList Is Nothing Then
|
||||
MusicAllList = New List(Of String)
|
||||
Directory.CreateDirectory(Path & "PCL\Musics\")
|
||||
For Each File In EnumerateFiles(Path & "PCL\Musics\")
|
||||
DirectoryUtils.Create(PathExeFolder & "PCL\Musics\")
|
||||
For Each File In EnumerateFiles(PathExeFolder & "PCL\Musics\")
|
||||
'文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的
|
||||
Dim Ext As String = File.Extension.Lower
|
||||
If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(Ext) Then Continue For
|
||||
@@ -37,7 +39,7 @@
|
||||
MusicWaitingList.Add(PreventFirst)
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "初始化音乐列表失败", LogLevel.Feedback)
|
||||
Log(ex, "初始化音乐列表失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
''' <summary>
|
||||
@@ -79,7 +81,7 @@
|
||||
If MusicState = MusicStates.Pause Then
|
||||
FrmMain.BtnExtraMusic.Logo = Logo.IconPlay
|
||||
FrmMain.BtnExtraMusic.LogoScale = 0.8
|
||||
ToolTipText = "已暂停:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
|
||||
ToolTipText = "已暂停:" & Path.GetFileNameWithoutExtension(MusicCurrent)
|
||||
If MusicAllList.Count > 1 Then
|
||||
ToolTipText += vbCrLf & "左键恢复播放,右键播放下一曲。"
|
||||
Else
|
||||
@@ -88,7 +90,7 @@
|
||||
Else
|
||||
FrmMain.BtnExtraMusic.Logo = Logo.IconMusic
|
||||
FrmMain.BtnExtraMusic.LogoScale = 1
|
||||
ToolTipText = "正在播放:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
|
||||
ToolTipText = "正在播放:" & Path.GetFileNameWithoutExtension(MusicCurrent)
|
||||
If MusicAllList.Count > 1 Then
|
||||
ToolTipText += vbCrLf & "左键暂停,右键播放下一曲。"
|
||||
Else
|
||||
@@ -101,7 +103,7 @@
|
||||
If FrmSetupUI IsNot Nothing Then FrmSetupUI.MusicRefreshUI()
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "刷新背景音乐 UI 失败", LogLevel.Feedback)
|
||||
Log(ex, "刷新背景音乐 UI 失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub)
|
||||
End Sub
|
||||
@@ -119,7 +121,7 @@
|
||||
Case MusicStates.Play
|
||||
MusicPause()
|
||||
Case Else
|
||||
Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", LogLevel.Debug)
|
||||
Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", NotifyLevel.DebugModeOnly)
|
||||
MusicRefreshPlay(False)
|
||||
End Select
|
||||
End If
|
||||
@@ -155,9 +157,9 @@
|
||||
Get
|
||||
If MusicNAudio Is Nothing Then Return MusicStates.Stop
|
||||
Select Case MusicNAudio.PlaybackState
|
||||
Case 0 'NAudio.Wave.PlaybackState.Stopped
|
||||
Case 0 'PlaybackState.Stopped
|
||||
Return MusicStates.Stop
|
||||
Case 2 'NAudio.Wave.PlaybackState.Paused
|
||||
Case 2 'PlaybackState.Paused
|
||||
Return MusicStates.Pause
|
||||
Case Else
|
||||
Return MusicStates.Play
|
||||
@@ -202,7 +204,7 @@
|
||||
MusicRefreshUI()
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "刷新背景音乐播放失败", LogLevel.Feedback)
|
||||
Log(ex, "刷新背景音乐播放失败", NotifyLevel.MsgBoxAndFeedback)
|
||||
End Try
|
||||
End Sub
|
||||
''' <summary>
|
||||
@@ -260,7 +262,7 @@
|
||||
#End Region
|
||||
|
||||
''' <summary>
|
||||
''' 当前正在播放的 NAudio.Wave.WaveOutEvent。
|
||||
''' 当前正在播放的 WaveOutEvent。
|
||||
''' 如果为它赋了类别,则会没有必要地加载 NAudio.dll。
|
||||
''' </summary>
|
||||
Public MusicNAudio = Nothing
|
||||
@@ -273,18 +275,18 @@
|
||||
''' 在 MusicUuid 不变的前提下,持续播放某地址的音乐,且在播放结束后随机播放下一曲。
|
||||
''' </summary>
|
||||
Private Sub MusicLoop(Optional IsFirstLoad As Boolean = False)
|
||||
Dim CurrentWave As NAudio.Wave.WaveOutEvent = Nothing
|
||||
Dim Reader As NAudio.Wave.WaveStream = Nothing
|
||||
Dim CurrentWave As WaveOutEvent = Nothing
|
||||
Dim Reader As WaveStream = Nothing
|
||||
Try
|
||||
'开始播放
|
||||
CurrentWave = New NAudio.Wave.WaveOutEvent()
|
||||
CurrentWave = New WaveOutEvent()
|
||||
MusicNAudio = CurrentWave
|
||||
CurrentWave.DeviceNumber = -1
|
||||
Try
|
||||
Reader = New NAudio.Wave.AudioFileReader(MusicCurrent)
|
||||
Reader = New AudioFileReader(MusicCurrent)
|
||||
Catch ex As Exception
|
||||
Log(ex, "使用 AudioFileReader 加载音频文件失败,换用 MediaFoundationReader 重试", LogLevel.Developer)
|
||||
Reader = New NAudio.Wave.MediaFoundationReader(MusicCurrent)
|
||||
Log(ex, "使用 AudioFileReader 加载音频文件失败,换用 MediaFoundationReader 重试", NotifyLevel.DevelopOnly)
|
||||
Reader = New MediaFoundationReader(MusicCurrent)
|
||||
End Try
|
||||
CurrentWave.Init(Reader)
|
||||
CurrentWave.Play()
|
||||
@@ -293,11 +295,13 @@
|
||||
MusicRefreshUI()
|
||||
'停止条件:播放完毕或变化
|
||||
Dim PreviousVolume = 0
|
||||
While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped
|
||||
While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = PlaybackState.Stopped
|
||||
If Settings.Get("UiMusicVolume") <> PreviousVolume Then
|
||||
'更新音量
|
||||
PreviousVolume = Settings.Get("UiMusicVolume")
|
||||
#Disable Warning BC40008 '类型或成员已过时
|
||||
CurrentWave.Volume = PreviousVolume / 1000
|
||||
#Enable Warning BC40008 '类型或成员已过时
|
||||
End If
|
||||
'更新进度条
|
||||
Dim Percent = Reader.CurrentTime.TotalMilliseconds / Reader.TotalTime.TotalMilliseconds
|
||||
@@ -305,9 +309,9 @@
|
||||
Thread.Sleep(100)
|
||||
End While
|
||||
'当前音乐已播放结束,继续下一曲
|
||||
If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
|
||||
If CurrentWave.PlaybackState = PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
|
||||
Catch ex As Exception
|
||||
Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", LogLevel.Developer)
|
||||
Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", NotifyLevel.DevelopOnly)
|
||||
If TypeOf ex Is NAudio.MmException AndAlso ex.Message.Contains("AlreadyAllocated") Then
|
||||
Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Red)
|
||||
Thread.Sleep(1000000000)
|
||||
@@ -320,7 +324,7 @@
|
||||
ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
|
||||
Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
|
||||
Else
|
||||
Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", LogLevel.Hint)
|
||||
Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", NotifyLevel.AllUsers)
|
||||
End If
|
||||
'将播放错误的音乐从列表中移除
|
||||
MusicAllList.Remove(MusicCurrent)
|
||||
|
||||
@@ -4,8 +4,6 @@ Imports System.Security.Cryptography
|
||||
|
||||
Friend Module ModSecret
|
||||
|
||||
#Region "杂项"
|
||||
|
||||
'标注 PCL 的不同分支,仅用于替换标记
|
||||
Public Const VersionBranchMain As String = "OpenSource"
|
||||
'在开源版的注册表与常规版的注册表隔离,以防数据冲突
|
||||
@@ -17,53 +15,6 @@ Friend Module ModSecret
|
||||
'用于匿名数据收集的腾讯云日志服务上报 URL,形如 https://{region}.cls.tencentcs.com/track?topic_id={topic_id}
|
||||
Public Const ClsBaseUrl As String = ""
|
||||
|
||||
Friend Sub SecretOnApplicationStart()
|
||||
'提升 UI 线程优先级
|
||||
Thread.CurrentThread.Priority = ThreadPriority.Highest
|
||||
'确保 .NET Framework 版本
|
||||
Try
|
||||
Dim VersionTest As New FormattedText("", Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Fonts.SystemTypefaces.First, 96, New MyColor, DPI)
|
||||
Catch ex As UriFormatException '修复 #3555
|
||||
Environment.SetEnvironmentVariable("windir", Environment.GetEnvironmentVariable("SystemRoot"), EnvironmentVariableTarget.User)
|
||||
Dim VersionTest As New FormattedText("", Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Fonts.SystemTypefaces.First, 96, New MyColor, DPI)
|
||||
End Try
|
||||
'检测当前文件夹权限
|
||||
Try
|
||||
Directory.CreateDirectory(Path & "PCL")
|
||||
Catch ex As Exception
|
||||
MsgBox($"PCL 无法创建 PCL 文件夹({Path & "PCL"}),请尝试:" & vbCrLf &
|
||||
"1. 将 PCL 移动到其他文件夹" & If(Path.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
|
||||
"2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
|
||||
"3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
|
||||
MsgBoxStyle.Critical, "运行环境错误")
|
||||
Environment.[Exit](ProcessReturnValues.Cancel)
|
||||
End Try
|
||||
If Not CheckPermission(Path & "PCL") Then
|
||||
MsgBox("PCL 没有对当前文件夹的写入权限,请尝试:" & vbCrLf &
|
||||
"1. 将 PCL 移动到其他文件夹" & If(Path.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
|
||||
"2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
|
||||
"3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
|
||||
MsgBoxStyle.Critical, "运行环境错误")
|
||||
Environment.[Exit](ProcessReturnValues.Cancel)
|
||||
End If
|
||||
'开源版本提示
|
||||
MyMsgBox($"该版本中无法使用以下特性:
|
||||
- CurseForge API 调用:需要你自行申请 API Key,然后添加到 ModSecret.vb 的开头
|
||||
- 正版登录:需要你自行申请 Client ID,然后添加到 ModSecret.vb 的开头
|
||||
- 更新与联网通知:避免滥用隐患
|
||||
- 主题切换:这是需要赞助解锁的纪念性质的功能,别让赞助者太伤心啦……
|
||||
- 百宝箱:开发早期往里面塞了些开发工具,整理起来太麻烦了", "开源版本说明")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 获取设备标识码。
|
||||
''' </summary>
|
||||
Friend Function SecretGetIdentify() As String
|
||||
Return "0000-0000-0000-0000"
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "网络鉴权"
|
||||
|
||||
Friend Function SecretCdnSign(UrlWithMark As String) As String
|
||||
@@ -78,9 +29,9 @@ Friend Module ModSecret
|
||||
If Url.Contains("baidupcs.com") OrElse Url.Contains("baidu.com") Then
|
||||
Req.Headers.Add("User-Agent", "LogStatistic") '#4951
|
||||
ElseIf SimulateBrowserHeaders Then
|
||||
Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{VersionBranchCode} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36")
|
||||
Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{CInt(BuildType)} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36")
|
||||
Else
|
||||
Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{VersionBranchCode}")
|
||||
Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{CInt(BuildType)}")
|
||||
End If
|
||||
End If
|
||||
If Not SimulateBrowserHeaders Then Req.Headers.Add("Referer", $"http://{VersionCode}.open.pcl2.server/")
|
||||
@@ -157,7 +108,7 @@ Friend Module ModSecret
|
||||
Public ColorSemiTransparent As New MyColor(1, Color8)
|
||||
|
||||
Public ThemeNow As Integer = -1
|
||||
Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As Object = 0
|
||||
Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As OneOf(Of Integer, Integer()) = 0
|
||||
Public ThemeDontClick As Integer = 0
|
||||
|
||||
Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
|
||||
@@ -199,7 +150,7 @@ Friend Module ModSecret
|
||||
Application.Current.Resources("ColorObjectBg1") = CType(ColorBg1, Color)
|
||||
ThemeRefreshMain()
|
||||
Catch ex As Exception
|
||||
Log(ex, "刷新主题颜色失败", LogLevel.Hint)
|
||||
Log(ex, "刷新主题颜色失败", NotifyLevel.AllUsers)
|
||||
End Try
|
||||
End Sub
|
||||
Public Sub ThemeRefreshMain()
|
||||
@@ -208,15 +159,10 @@ Friend Module ModSecret
|
||||
If Not FrmMain.IsLoaded Then Return
|
||||
'顶部条背景
|
||||
Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)}
|
||||
If TypeOf ColorHueTopbarDelta Is Integer Then
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue - ColorHueTopbarDelta, ColorSat, 48 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 54 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta, ColorSat, 48 + ColorLightAdjust)})
|
||||
Else
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(0), ColorSat, 48 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(1), ColorSat, 54 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(2), ColorSat, 48 + ColorLightAdjust)})
|
||||
End If
|
||||
Dim Deltas = ColorHueTopbarDelta.Switch(Function(d) New Integer() {-d, 0, d}, Function(d) d)
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + Deltas(0), ColorSat, 48 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + Deltas(1), ColorSat, 54 + ColorLightAdjust)})
|
||||
Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + Deltas(2), ColorSat, 48 + ColorLightAdjust)})
|
||||
FrmMain.PanTitle.Background = Brush
|
||||
FrmMain.PanTitle.Background.Freeze()
|
||||
'主页面背景
|
||||
@@ -240,30 +186,23 @@ Friend Module ModSecret
|
||||
Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
|
||||
Return False
|
||||
End Function
|
||||
Friend Function ThemeCheckGold(Optional Code As String = Nothing) As Boolean
|
||||
Return False
|
||||
End Function
|
||||
Friend Function DonateCodeInput() As Boolean?
|
||||
Return Nothing
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "更新"
|
||||
|
||||
Public IsUpdateStarted As Boolean = False
|
||||
Public IsUpdateWaitingRestart As Boolean = False
|
||||
Public Sub UpdateCheckByButton()
|
||||
Friend Sub UpdateCheckByButton()
|
||||
Hint("该版本中不包含更新功能……")
|
||||
End Sub
|
||||
Public Sub UpdateStart(BaseUrl As String, Slient As Boolean, Optional ReceivedKey As String = Nothing, Optional ForceValidated As Boolean = False)
|
||||
End Sub
|
||||
|
||||
Friend IsUpdateWaitingRestart As Boolean = False
|
||||
Public Sub UpdateRestart(TriggerRestartAndByEnd As Boolean)
|
||||
End Sub
|
||||
Public Sub UpdateReplace(ProcessId As Integer, OldFileName As String, NewFileName As String, TriggerRestart As Boolean)
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 确保 PathTemp 下的 Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
|
||||
''' 确保 PathTemp/Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
|
||||
''' 如果不是,则下载一个。
|
||||
''' </summary>
|
||||
Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
|
||||
@@ -285,4 +224,26 @@ Friend Module ModSecret
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "赞助等级"
|
||||
|
||||
Public ReadOnly Property DonationRank As DonationRanks
|
||||
Get
|
||||
Return DonationRanks.None
|
||||
End Get
|
||||
End Property
|
||||
|
||||
Public Sub InputPotatoCode(IsUpdating As Boolean)
|
||||
End Sub
|
||||
Friend Sub GeneratePotatoCode()
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 获取设备识别码。
|
||||
''' </summary>
|
||||
Friend Function GetIdentify() As String
|
||||
Return "0000-0000-0000-0000"
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
|
||||
End Module
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
''' <summary>
|
||||
''' 本地的社区资源文件。
|
||||
''' </summary>
|
||||
Public Class ResourceFile
|
||||
|
||||
#Region "文件信息"
|
||||
|
||||
''' <summary>
|
||||
''' 该文件的文件信息。
|
||||
''' </summary>
|
||||
Public ReadOnly File As FileInfo
|
||||
Public Sub New(FileInfo As FileInfo)
|
||||
Me.File = FileInfo
|
||||
End Sub
|
||||
Public Sub New(FullName As String)
|
||||
Me.New(New FileInfo(FullName))
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 该文件是否没有被禁用。
|
||||
''' </summary>
|
||||
Public ReadOnly Property IsEnabled As Boolean
|
||||
Get
|
||||
Return {".jar", ".zip", ".litemod"}.Contains(File.Extension.Lower)
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
''' 假若该资源未被禁用时,文件的文件名。
|
||||
''' </summary>
|
||||
Public ReadOnly Property EnabledName As String
|
||||
Get
|
||||
Return File.Name.Replace(".disabled", "").Replace(".old", "")
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
''' 假若该资源未被禁用时,文件的完整路径。
|
||||
''' </summary>
|
||||
Public ReadOnly Property EnabledFullName As String
|
||||
Get
|
||||
Return $"{File.DirectoryName}\{EnabledName}"
|
||||
End Get
|
||||
End Property
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "信息项"
|
||||
|
||||
''' <summary>
|
||||
''' 在初始化结束之后,任何信息被后续更新时触发此事件。
|
||||
''' </summary>
|
||||
Public Event OnUpdate(sender As ResourceFile)
|
||||
|
||||
''' <summary>
|
||||
''' 可读名称。
|
||||
''' 可能为无扩展的文件名,但不会是 Nothing。
|
||||
''' </summary>
|
||||
Public Property Display As String
|
||||
Get
|
||||
If _Display Is Nothing Then _Display = ProjectVersion?.Display
|
||||
If _Display Is Nothing Then _Display = Path.GetFileNameWithoutExtension(File.FullName)
|
||||
Return _Display
|
||||
End Get
|
||||
Set(value As String)
|
||||
If _Display Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
|
||||
value.Lower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
|
||||
_Display = value
|
||||
End If
|
||||
End Set
|
||||
End Property
|
||||
Private _Display As String = Nothing
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的描述信息。
|
||||
''' 可能为 Nothing。
|
||||
''' </summary>
|
||||
Public Property Description As String
|
||||
Get
|
||||
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.Lower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
|
||||
End If
|
||||
End Set
|
||||
End Property
|
||||
Private _Description As String = Nothing
|
||||
|
||||
''' <summary>
|
||||
''' Mod 的版本。
|
||||
''' 不保证符合版本格式规范,且可能为 Nothing。
|
||||
''' </summary>
|
||||
Public Property Version As String
|
||||
Get
|
||||
If _Version Is Nothing Then _Version = ProjectVersion?.Version
|
||||
Return _Version
|
||||
End Get
|
||||
Set(value As String)
|
||||
If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
|
||||
If value IsNot Nothing AndAlso value.ContainsIgnoreCase("version") Then value = "version" '需要修改的标识
|
||||
_Version = value
|
||||
End Set
|
||||
End Property
|
||||
Public _Version As String = Nothing
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "从 JAR 获取信息"
|
||||
|
||||
''' <summary>
|
||||
''' 从 JAR 文件中获取 Mod 信息。
|
||||
''' </summary>
|
||||
Public Sub LoadMetadataFromJar()
|
||||
Static IsLoaded As Boolean = False
|
||||
If IsLoaded Then Return
|
||||
IsLoaded = True
|
||||
Dim Jar As ZipArchive = Nothing
|
||||
Try
|
||||
'基础可用性检查
|
||||
If File.FullName.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(File.FullName, "null") & ")")
|
||||
If Not IO.File.Exists(File.FullName) Then Throw New FileNotFoundException("未找到 Mod 文件(" & File.FullName & ")")
|
||||
'打开 Jar 文件
|
||||
Jar = New ZipArchive(New FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
'信息获取
|
||||
LoadMetadataFromJar(Jar)
|
||||
Catch ex As UnauthorizedAccessException
|
||||
Log(ex, $"Mod 文件由于无权限无法打开({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
Catch ex As Exception
|
||||
Log(ex, $"Mod 文件无法打开({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
Finally
|
||||
If Jar IsNot Nothing Then Jar.Dispose()
|
||||
End Try
|
||||
End Sub
|
||||
Private Sub LoadMetadataFromJar(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 信息项
|
||||
Display = InfoObject("name")
|
||||
Description = InfoObject("description")
|
||||
Version = InfoObject("version")
|
||||
Catch ex As Exception
|
||||
Log(ex, $"读取 mcmod.info 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
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 Display = FabricObject("name")
|
||||
If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
|
||||
If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取 fabric.mod.json 时出现未知错误(" & File.FullName & ")", NotifyLevel.DevelopOnly)
|
||||
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.ReplaceLineEndings(vbLf).Split(vbLf) '统一换行符
|
||||
If Line.StartsWithF("#") Then '去除注释
|
||||
Continue For
|
||||
ElseIf Line.Contains("#") Then
|
||||
Line = Line.Substring(0, Line.IndexOfF("#"))
|
||||
End If
|
||||
Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
|
||||
If Line.Any 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.StartsWithF("[") AndAlso Line.EndsWithF("]") 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.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
|
||||
Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
|
||||
Dim Value As Object
|
||||
If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
|
||||
'单行字符串
|
||||
Value = RawValue.Trim("""")
|
||||
ElseIf RawValue.StartsWithF("'''") Then
|
||||
'多行字符串
|
||||
Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
|
||||
If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
|
||||
ValueLines(0) = ValueLines(0).TrimEnd("'")
|
||||
Else
|
||||
Do Until i >= Lines.Count - 1
|
||||
i += 1
|
||||
Dim ValueLine As String = Lines(i)
|
||||
If ValueLine.EndsWithF("'''") Then
|
||||
ValueLines.Add(ValueLine.TrimEnd("'"))
|
||||
Exit Do
|
||||
Else
|
||||
ValueLines.Add(ValueLine)
|
||||
End If
|
||||
Loop
|
||||
End If
|
||||
Value = ValueLines.Join(vbLf).Trim(vbLf).ReplaceLineEndings(vbCrLf)
|
||||
ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
|
||||
'布尔型
|
||||
Value = (RawValue.Lower = "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
|
||||
If ModEntry.ContainsKey("displayName") Then Display = ModEntry("displayName")
|
||||
If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
|
||||
If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, $"读取 mods.toml 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
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("name") Then Display = FmlObject("name")("value")
|
||||
If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
|
||||
'加载成功
|
||||
GoTo Finished
|
||||
Catch ex As Exception
|
||||
Log(ex, $"读取 fml_cache_annotation.json 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
End Try
|
||||
#End Region
|
||||
Return '没能成功获取任何信息
|
||||
|
||||
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.IndexOfF("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 中的版本信息失败({File.FullName})", NotifyLevel.DevelopOnly)
|
||||
Version = Nothing
|
||||
End Try
|
||||
End If
|
||||
If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
|
||||
#End Region
|
||||
RaiseEvent OnUpdate(Me)
|
||||
|
||||
End Sub
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "网络信息"
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 关联的网络项目。
|
||||
''' </summary>
|
||||
Public Property Project As ResourceProject
|
||||
Get
|
||||
Return _Project
|
||||
End Get
|
||||
Set(value As ResourceProject)
|
||||
_Project = value
|
||||
RaiseEvent OnUpdate(Me)
|
||||
End Set
|
||||
End Property
|
||||
Private _Project As ResourceProject
|
||||
|
||||
''' <summary>
|
||||
''' 本地文件对应的联网文件信息。
|
||||
''' </summary>
|
||||
Public ProjectVersion As ResourceVersion
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 对应的联网最新版本。
|
||||
''' </summary>
|
||||
Public Property UpdateFile As ResourceVersion
|
||||
Get
|
||||
Return _UpdateFile
|
||||
End Get
|
||||
Set(value As ResourceVersion)
|
||||
_UpdateFile = value
|
||||
RaiseEvent OnUpdate(Me)
|
||||
End Set
|
||||
End Property
|
||||
Private _UpdateFile As ResourceVersion
|
||||
|
||||
''' <summary>
|
||||
''' 该 Mod 的更新日志网址。
|
||||
''' </summary>
|
||||
Public ChangelogUrls As New List(Of String)
|
||||
''' <summary>
|
||||
''' 所有网络信息是否已成功加载。
|
||||
''' </summary>
|
||||
Public OnlineDataLoaded As Boolean = False
|
||||
|
||||
''' <summary>
|
||||
''' 将网络信息保存为 JSON。
|
||||
''' </summary>
|
||||
Public Function ToJson() As JObject
|
||||
Dim Json As New JObject
|
||||
If Project IsNot Nothing Then Json.Add("Project", Project.ToJson())
|
||||
Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
|
||||
Json.Add("OnlineDataLoaded", OnlineDataLoaded)
|
||||
If ProjectVersion IsNot Nothing Then Json.Add("Version", ProjectVersion.ToCacheJson())
|
||||
If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToCacheJson())
|
||||
Return Json
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 从 JSON 中读取网络信息。
|
||||
''' </summary>
|
||||
Public Sub FromJson(Json As JObject)
|
||||
OnlineDataLoaded = Json("OnlineDataLoaded")
|
||||
If Json.ContainsKey("Project") Then Project = New ResourceProject(Json("Project"))
|
||||
If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
|
||||
If Json.ContainsKey("Version") Then ProjectVersion = ResourceVersion.FromCacheJson(Json("Version"))
|
||||
If Json.ContainsKey("UpdateFile") Then UpdateFile = ResourceVersion.FromCacheJson(Json("UpdateFile"))
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' 该文件是否可以更新。
|
||||
''' </summary>
|
||||
Public ReadOnly Property HasUpdate As Boolean
|
||||
Get
|
||||
Return Not Settings.Get("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
|
||||
End Get
|
||||
End Property
|
||||
|
||||
''' <summary>
|
||||
''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
|
||||
''' </summary>
|
||||
Public ReadOnly Property CurseForgeHash As UInteger
|
||||
Get
|
||||
If _CurseForgeHash Is Nothing Then
|
||||
'读取缓存
|
||||
Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-C")
|
||||
Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
|
||||
If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
|
||||
_CurseForgeHash = Cached
|
||||
Return _CurseForgeHash
|
||||
End If
|
||||
'读取文件
|
||||
Dim data As New List(Of Byte)
|
||||
For Each b As Byte In ReadFileBytes(File.FullName)
|
||||
If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
|
||||
data.Add(b)
|
||||
Next
|
||||
'计算 MurmurHash2
|
||||
Dim length As Integer = data.Count
|
||||
Dim h As UInteger = 1 Xor length '1 是种子
|
||||
Dim i As Integer
|
||||
For i = 0 To length - 4 Step 4
|
||||
Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
|
||||
k = (k * &H5BD1E995L) And &HFFFFFFFFL
|
||||
k = k Xor (k >> 24)
|
||||
k = (k * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = h Xor k
|
||||
Next
|
||||
Select Case length - i
|
||||
Case 3
|
||||
h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
|
||||
h = h Xor (CUInt(data(i + 2)) << 16)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
Case 2
|
||||
h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
Case 1
|
||||
h = h Xor data(i)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
End Select
|
||||
h = h Xor (h >> 13)
|
||||
h = (h * &H5BD1E995L) And &HFFFFFFFFL
|
||||
h = h Xor (h >> 15)
|
||||
_CurseForgeHash = h
|
||||
'写入缓存
|
||||
WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
|
||||
End If
|
||||
Return _CurseForgeHash
|
||||
End Get
|
||||
End Property
|
||||
Private _CurseForgeHash As UInteger?
|
||||
|
||||
''' <summary>
|
||||
''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
|
||||
''' </summary>
|
||||
Public ReadOnly Property ModrinthHash As String
|
||||
Get
|
||||
If _ModrinthHash Is Nothing Then
|
||||
'读取缓存
|
||||
Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-M")
|
||||
Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
|
||||
If Cached <> "" Then
|
||||
_ModrinthHash = Cached
|
||||
Return _ModrinthHash
|
||||
End If
|
||||
'计算 SHA1
|
||||
_ModrinthHash = GetFileSHA1(File.FullName)
|
||||
'写入缓存
|
||||
WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
|
||||
End If
|
||||
Return _ModrinthHash
|
||||
End Get
|
||||
End Property
|
||||
Private _ModrinthHash As String
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "API"
|
||||
|
||||
Public Overrides Function ToString() As String
|
||||
Return File.FullName
|
||||
End Function
|
||||
Public Overrides Function Equals(obj As Object) As Boolean
|
||||
Dim Target = TryCast(obj, ResourceFile)
|
||||
Return Target IsNot Nothing AndAlso File.FullName = Target.File.FullName
|
||||
End Function
|
||||
|
||||
#End Region
|
||||
|
||||
End Class
|
||||
@@ -0,0 +1,345 @@
|
||||
Public Module ResourceFileLoader
|
||||
Private Const ResourceFileCacheVersion As Integer = 16
|
||||
|
||||
'加载 Mod 列表
|
||||
Public McModLoader As New LoaderTask(Of String, List(Of ResourceFile))("Mod List Loader", AddressOf McModLoad)
|
||||
Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of ResourceFile)))
|
||||
Try
|
||||
RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
|
||||
|
||||
'等待 Mod 更新完成
|
||||
If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
|
||||
Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
|
||||
Try
|
||||
RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
|
||||
Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
|
||||
If Loader.IsInterrupted Then Return
|
||||
Thread.Sleep(100)
|
||||
Loop
|
||||
Finally
|
||||
RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
|
||||
End Try
|
||||
FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
|
||||
End If
|
||||
|
||||
'获取 Mod 文件夹下的可用文件列表
|
||||
Dim ModFileList As New List(Of FileInfo)
|
||||
If Directory.Exists(Loader.Input) Then
|
||||
Dim RawName As String = Loader.Input.Lower
|
||||
For Each File As FileInfo In EnumerateFiles(Loader.Input)
|
||||
If File.DirectoryName.Lower & "\" <> RawName Then
|
||||
'仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
|
||||
If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
|
||||
PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
|
||||
File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
|
||||
Continue For
|
||||
End If
|
||||
End If
|
||||
Static IsModFile As Func(Of String, Boolean) =
|
||||
Function(FullName As String)
|
||||
If FullName Is Nothing OrElse Not FullName.Contains(".") Then Return False
|
||||
FullName = FullName.Lower
|
||||
If FullName.EndsWithF(".jar", True) OrElse FullName.EndsWithF(".zip", True) OrElse FullName.EndsWithF(".litemod", True) OrElse
|
||||
FullName.EndsWithF(".jar.disabled", True) OrElse FullName.EndsWithF(".zip.disabled", True) OrElse FullName.EndsWithF(".litemod.disabled", True) OrElse
|
||||
FullName.EndsWithF(".jar.old", True) OrElse FullName.EndsWithF(".zip.old", True) OrElse FullName.EndsWithF(".litemod.old", True) Then Return True
|
||||
Return False
|
||||
End Function
|
||||
If IsModFile(File.FullName) Then ModFileList.Add(File)
|
||||
Next
|
||||
End If
|
||||
|
||||
'获取本地文件缓存
|
||||
Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
|
||||
Dim Cache As New JObject
|
||||
Try
|
||||
Dim CacheContent As String = ReadFile(CachePath)
|
||||
If Not String.IsNullOrWhiteSpace(CacheContent) Then
|
||||
Cache = GetJson(CacheContent)
|
||||
If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> ResourceFileCacheVersion Then
|
||||
Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", NotifyLevel.DebugModeOnly)
|
||||
Cache = New JObject
|
||||
End If
|
||||
End If
|
||||
Catch ex As Exception
|
||||
Log(ex, "读取本地 Mod 信息缓存失败,已重置")
|
||||
Cache = New JObject
|
||||
End Try
|
||||
Cache("version") = ResourceFileCacheVersion
|
||||
|
||||
'加载 Mod 列表
|
||||
Dim ModList As New List(Of ResourceFile)
|
||||
For Each ModFile As FileInfo In ModFileList
|
||||
If Loader.IsInterrupted Then Return
|
||||
Dim ModEntry As New ResourceFile(ModFile)
|
||||
Dim DumpMod As ResourceFile = ModList.FirstOrDefault(Function(m) m.EnabledName = ModEntry.EnabledName) '存在两个文件,名称相同,但一个启用一个禁用
|
||||
If DumpMod IsNot Nothing Then
|
||||
Dim DisabledMod As ResourceFile = If(DumpMod.IsEnabled, ModEntry, DumpMod)
|
||||
Log($"[Mod] 重复的 Mod 文件:{DumpMod.File.Name} 与 {ModEntry.File.Name},已忽略 {DisabledMod.File.Name}", NotifyLevel.DebugModeOnly)
|
||||
If DisabledMod Is ModEntry Then
|
||||
Continue For
|
||||
Else
|
||||
ModList.Remove(DisabledMod)
|
||||
End If
|
||||
End If
|
||||
ModList.Add(ModEntry)
|
||||
Next
|
||||
Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
|
||||
|
||||
'排序
|
||||
ModList = ModList.OrderBy(Function(m) m.File.Name).ToList
|
||||
|
||||
'回设
|
||||
If Loader.IsInterrupted Then Return
|
||||
Loader.Output = ModList
|
||||
|
||||
'开始联网加载
|
||||
'TODO: 添加信息获取中提示
|
||||
McModDetailLoader.Start(New KeyValuePair(Of List(Of ResourceFile), JObject)(ModList, Cache), IsForceRestart:=True)
|
||||
|
||||
Catch ex As Exception
|
||||
Log(ex, "Mod 列表加载失败", NotifyLevel.DebugModeOnly)
|
||||
Throw
|
||||
End Try
|
||||
End Sub
|
||||
|
||||
'联网加载 Mod 详情
|
||||
Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
|
||||
Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer))
|
||||
Dim Mods As New List(Of ResourceFile)
|
||||
Dim Cache As JObject = Loader.Input.Value
|
||||
Static GetTargetModLoaders As Func(Of List(Of ModLoaderTypes)) =
|
||||
Function() As List(Of ModLoaderTypes)
|
||||
Dim Loaders As New List(Of ModLoaderTypes)
|
||||
If PageInstanceLeft.Instance.Version.HasForge Then Loaders.Add(ModLoaderTypes.Forge)
|
||||
If PageInstanceLeft.Instance.Version.HasNeoForge Then Loaders.Add(ModLoaderTypes.NeoForge)
|
||||
If PageInstanceLeft.Instance.Version.HasFabric Then Loaders.Add(ModLoaderTypes.Fabric)
|
||||
If PageInstanceLeft.Instance.Version.HasLiteLoader Then Loaders.Add(ModLoaderTypes.LiteLoader)
|
||||
If Not Loaders.Any() Then Loaders.AddRange({ModLoaderTypes.Forge, ModLoaderTypes.NeoForge, ModLoaderTypes.Fabric, ModLoaderTypes.LiteLoader, ModLoaderTypes.Quilt})
|
||||
Return Loaders
|
||||
End Function
|
||||
'读取缓存,获取需要更新的 Mod 列表
|
||||
For Each ModEntry As ResourceFile In Loader.Input.Key
|
||||
If Loader.IsInterrupted Then Return
|
||||
Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
|
||||
If Cache.ContainsKey(CacheKey) Then
|
||||
ModEntry.FromJson(Cache(CacheKey))
|
||||
'如果缓存中的信息在 6 小时以内更新过,则无需重新获取
|
||||
If ModEntry.OnlineDataLoaded AndAlso Date.Now - Cache(CacheKey)("Project")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
|
||||
End If
|
||||
Mods.Add(ModEntry)
|
||||
Next
|
||||
Log($"[Mod] 有 {Mods.Where(Function(m) m.Project Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Project IsNot Nothing).Count} 个 Mod 需要更新信息")
|
||||
If Not Mods.Any Then
|
||||
Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar()) '从 JAR 中获取缺失的版本信息(下面有另一个分支)
|
||||
Return
|
||||
End If
|
||||
'获取作为检查目标的加载器和版本
|
||||
'此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
|
||||
Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
|
||||
Dim ModLoaders = GetTargetModLoaders()
|
||||
Dim VanillaVersion = TargetMcVersion.VanillaName
|
||||
'开始网络获取
|
||||
Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
|
||||
Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
|
||||
Dim CurrentTaskThread As Thread = Thread.CurrentThread
|
||||
'从 Modrinth 获取信息
|
||||
RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
'步骤 1:获取 Hash 与对应的工程 ID
|
||||
Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
|
||||
Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
|
||||
$"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
|
||||
Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
|
||||
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
|
||||
If ModrinthVersion.Count = 0 Then Return
|
||||
Dim ModrinthMapping As New Dictionary(Of String, List(Of ResourceFile))
|
||||
For Each Entry In Mods
|
||||
If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
|
||||
If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
|
||||
Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
|
||||
If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
|
||||
If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of ResourceFile)
|
||||
ModrinthMapping(ProjectId).Add(Entry)
|
||||
'记录对应的 ProjectVersion
|
||||
Dim File = ResourceVersion.FromPlatformJson(ModrinthVersion(Entry.ModrinthHash), ResourceTypes.Mod)
|
||||
If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then
|
||||
Entry.ProjectVersion = File
|
||||
Else
|
||||
Entry.ProjectVersion.Version = File.Version '使用来自 Modrinth 的版本号
|
||||
End If
|
||||
Next
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
|
||||
'步骤 3:获取工程信息
|
||||
If Not ModrinthMapping.Any() Then Return
|
||||
Dim ModrinthProject As JArray = DlModRequest(
|
||||
$"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
|
||||
For Each ProjectJson In ModrinthProject
|
||||
Dim Project As New ResourceProject(ProjectJson)
|
||||
For Each Entry In ModrinthMapping(Project.Id)
|
||||
Entry.Project = Project
|
||||
Next
|
||||
Next
|
||||
Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
|
||||
'步骤 4:获取更新信息
|
||||
Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
|
||||
$"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"",
|
||||
""loaders"": [""{ModLoaders.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
|
||||
For Each Entry In Mods
|
||||
If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.ProjectVersion Is Nothing Then Continue For
|
||||
Dim UpdateFile = ResourceVersion.FromPlatformJson(ModrinthUpdate(Entry.ModrinthHash), ResourceTypes.Mod)
|
||||
If Not UpdateFile.DownloadAvailable Then Continue For
|
||||
If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
|
||||
If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
|
||||
'设置更新日志与更新文件
|
||||
If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
|
||||
Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}")
|
||||
UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
|
||||
Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
|
||||
ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
|
||||
Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}"}
|
||||
Entry.UpdateFile = UpdateFile
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
|
||||
Catch ex As Exception
|
||||
Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
|
||||
IsFailed = True
|
||||
Finally
|
||||
EndedThreadCount += 1
|
||||
End Try
|
||||
End Sub, "Mod List Detail Loader Modrinth")
|
||||
'从 CurseForge 获取信息
|
||||
RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
'步骤 1:获取 Hash 与对应的工程 ID
|
||||
Dim CurseForgeHashes As New List(Of UInteger)
|
||||
For Each Entry In Mods
|
||||
CurseForgeHashes.Add(Entry.CurseForgeHash)
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Next
|
||||
Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
|
||||
$"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
|
||||
Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
|
||||
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
|
||||
If Not CurseForgeRaw.Any() Then Return
|
||||
Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of ResourceFile))
|
||||
For Each Project In CurseForgeRaw
|
||||
Dim ProjectId = Project("id").ToString
|
||||
Dim Hash As UInteger = Project("file")("fileFingerprint")
|
||||
For Each Entry In Mods
|
||||
If Entry.CurseForgeHash <> Hash Then Continue For
|
||||
If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
|
||||
If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of ResourceFile)
|
||||
CurseForgeMapping(ProjectId).Add(Entry)
|
||||
'记录对应的 ProjectVersion
|
||||
Dim File = ResourceVersion.FromPlatformJson(Project("file"), ResourceTypes.Mod)
|
||||
If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File
|
||||
Next
|
||||
Next
|
||||
If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
|
||||
Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
|
||||
'步骤 3:获取工程信息
|
||||
If Not CurseForgeMapping.Any() Then Return
|
||||
Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
|
||||
$"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
|
||||
Dim UpdateFileIds As New Dictionary(Of Integer, List(Of ResourceFile)) 'FileId -> 本地 Mod 文件列表
|
||||
Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
|
||||
For Each ProjectJson In CurseForgeProject
|
||||
If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
|
||||
'设置 Entry 中的工程信息
|
||||
Dim Project As New ResourceProject(ProjectJson)
|
||||
For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
|
||||
If Entry.Project IsNot Nothing AndAlso Entry.Project.Platform = ResourcePlatforms.Modrinth Then
|
||||
Entry.Project = Entry.Project '再次触发修改事件
|
||||
Continue For
|
||||
End If
|
||||
Entry.Project = Project
|
||||
Next
|
||||
'查找或许版本更新的文件列表
|
||||
If ModLoaders.IsSingle Then
|
||||
Dim NewestVersion As String = Nothing
|
||||
Dim NewestFileIds As New List(Of Integer)
|
||||
For Each IndexEntry In ProjectJson("latestFilesIndexes")
|
||||
If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
|
||||
Dim IndexVersion As String = IndexEntry("gameVersion")
|
||||
If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
|
||||
'由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
|
||||
If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
|
||||
If NewestVersion <> IndexVersion Then
|
||||
NewestVersion = IndexVersion
|
||||
NewestFileIds.Clear()
|
||||
End If
|
||||
NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
|
||||
Next
|
||||
For Each FileId In NewestFileIds
|
||||
If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of ResourceFile)
|
||||
UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
|
||||
FileIdToProjectSlug(FileId) = Project.Slug
|
||||
Next
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
|
||||
'步骤 4:获取更新文件信息
|
||||
If Not UpdateFileIds.Any() Then Return
|
||||
Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
|
||||
$"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
|
||||
Dim UpdateFiles As New Dictionary(Of ResourceFile, ResourceVersion)
|
||||
For Each FileJson In CurseForgeFiles
|
||||
Dim File = ResourceVersion.FromPlatformJson(FileJson, ResourceTypes.Mod)
|
||||
If Not File.DownloadAvailable Then Continue For
|
||||
For Each Entry As ResourceFile In UpdateFileIds(File.Id)
|
||||
If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
|
||||
UpdateFiles(Entry) = File
|
||||
Next
|
||||
Next
|
||||
For Each Pair In UpdateFiles
|
||||
Dim Entry As ResourceFile = Pair.Key
|
||||
Dim UpdateFile As ResourceVersion = Pair.Value
|
||||
If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
|
||||
If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
|
||||
'设置更新日志与更新文件
|
||||
If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
|
||||
Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
|
||||
Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
|
||||
ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
|
||||
Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
|
||||
Entry.UpdateFile = UpdateFile
|
||||
End If
|
||||
Next
|
||||
Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
|
||||
Catch ex As Exception
|
||||
Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
|
||||
IsFailed = True
|
||||
Finally
|
||||
EndedThreadCount += 1
|
||||
End Try
|
||||
End Sub, "Mod List Detail Loader CurseForge")
|
||||
'从 JAR 中获取缺失的版本信息(上面有另一个分支)
|
||||
Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar())
|
||||
'等待线程结束
|
||||
Do Until EndedThreadCount = 2
|
||||
Thread.Sleep(10)
|
||||
If Loader.IsInterrupted Then Return
|
||||
Loop
|
||||
'保存缓存
|
||||
Mods = Mods.Where(Function(m) m.Project IsNot Nothing).ToList()
|
||||
Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
|
||||
If Not Mods.Any() Then Return
|
||||
For Each Entry In Mods
|
||||
Entry.OnlineDataLoaded = Not IsFailed
|
||||
Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
|
||||
Next
|
||||
FileUtils.Write(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
|
||||
'刷新边栏
|
||||
If Loader.IsInterrupted Then Return
|
||||
If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
|
||||
RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
|
||||
Else
|
||||
RunInUi(Sub() FrmInstanceMod?.RefreshBars())
|
||||
End If
|
||||
End Sub
|
||||
|
||||
End Module
|
||||
@@ -0,0 +1,726 @@
|
||||
''' <summary>
|
||||
''' 社区资源的项目信息。
|
||||
''' </summary>
|
||||
Public Class ResourceProject
|
||||
|
||||
'源信息
|
||||
|
||||
''' <summary>
|
||||
''' 该项目信息的所属平台。
|
||||
''' </summary>
|
||||
Public ReadOnly Platform As ResourcePlatforms
|
||||
''' <summary>
|
||||
''' 工程中包含的文件种类。
|
||||
''' 若为 Modrinth 工程,可能为 Mod 或 数据包。
|
||||
''' </summary>
|
||||
Public ReadOnly Types As ResourceTypes
|
||||
''' <summary>
|
||||
''' 工程的短名。例如 technical-enchant。
|
||||
''' </summary>
|
||||
Public ReadOnly Slug As String
|
||||
''' <summary>
|
||||
''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
|
||||
''' </summary>
|
||||
Public ReadOnly Id As String
|
||||
''' <summary>
|
||||
''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
|
||||
''' </summary>
|
||||
Public ReadOnly CurseForgeFileIds As List(Of Integer)
|
||||
|
||||
'描述性信息
|
||||
|
||||
''' <summary>
|
||||
''' 原始的英文名称。
|
||||
''' </summary>
|
||||
Public ReadOnly RawName As String
|
||||
''' <summary>
|
||||
''' 英文描述。
|
||||
''' </summary>
|
||||
Public ReadOnly Description As String
|
||||
''' <summary>
|
||||
''' 来源网站的工程页面网址。确保格式一定标准。
|
||||
''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
|
||||
''' Modrinth:https://modrinth.com/mod/technical-enchant
|
||||
''' </summary>
|
||||
Public ReadOnly Website As String
|
||||
''' <summary>
|
||||
''' 最后一次更新的时间。
|
||||
''' </summary>
|
||||
Public ReadOnly LastUpdate As Date
|
||||
''' <summary>
|
||||
''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
|
||||
''' </summary>
|
||||
Public ReadOnly DownloadCount As Integer
|
||||
''' <summary>
|
||||
''' 支持的 Mod 加载器列表。可能为空。
|
||||
''' </summary>
|
||||
Public ReadOnly Loaders As List(Of ModLoaderTypes)
|
||||
''' <summary>
|
||||
''' 描述性标签的内容。已转换为中文。
|
||||
''' </summary>
|
||||
Public ReadOnly Tags As List(Of String)
|
||||
''' <summary>
|
||||
''' Logo 图片的下载地址。
|
||||
''' 若为 Nothing 则没有,保证不为空字符串。
|
||||
''' </summary>
|
||||
Public LogoUrl As String = Nothing
|
||||
''' <summary>
|
||||
''' 支持的 Drop 编号,从高到低排序,不为 Nothing。
|
||||
''' 例如:261(26.1.x)、180(1.18.x)。
|
||||
''' </summary>
|
||||
Public ReadOnly Drops As List(Of Integer)
|
||||
''' <summary>
|
||||
''' Modrinth API 返回的原始版本列表。
|
||||
''' 仅用于 Modrinth 工程的二次筛选,不会被缓存,不会被预处理。
|
||||
''' 若非从 Modrinth 获取则为 Nothing。
|
||||
''' </summary>
|
||||
Public ReadOnly UnsafeGameVersions As List(Of String)
|
||||
|
||||
'数据库信息
|
||||
|
||||
Private LoadedDatabase As Boolean = False
|
||||
Private _WikiEntry As WikiEntry = Nothing
|
||||
''' <summary>
|
||||
''' 关联的数据库条目。若为 Nothing 则没有。
|
||||
''' </summary>
|
||||
Private Property WikiEntry As WikiEntry
|
||||
Get
|
||||
If Not LoadedDatabase Then
|
||||
LoadedDatabase = True
|
||||
If Types.HasFlag(ResourceTypes.Mod) OrElse Types.HasFlag(ResourceTypes.DataPack) Then
|
||||
_WikiEntry = WikiEntry.All.FirstOrDefault(Function(c) If(Platform = ResourcePlatforms.CurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
|
||||
End If
|
||||
End If
|
||||
Return _WikiEntry
|
||||
End Get
|
||||
Set(value As WikiEntry)
|
||||
LoadedDatabase = True
|
||||
_WikiEntry = value
|
||||
End Set
|
||||
End Property
|
||||
''' <summary>
|
||||
''' MC 百科的页面 ID。若为 0 则没有。
|
||||
''' </summary>
|
||||
Public ReadOnly Property WikiId As Integer
|
||||
Get
|
||||
Return If(WikiEntry Is Nothing, 0, WikiEntry.Id)
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
''' 翻译后的中文名。若数据库没有则等同于 RawName。
|
||||
''' </summary>
|
||||
Public ReadOnly Property TranslatedName As String
|
||||
Get
|
||||
Return If(WikiEntry Is Nothing OrElse WikiEntry.ChineseName = "", RawName, WikiEntry.ChineseName)
|
||||
End Get
|
||||
End Property
|
||||
|
||||
'实例化
|
||||
|
||||
''' <summary>
|
||||
''' 从工程 Json 中初始化实例。若出错会抛出异常。
|
||||
''' </summary>
|
||||
Public Sub New(Data As JObject)
|
||||
If Data.ContainsKey("Tags") Then
|
||||
#Region "缓存 JSON"
|
||||
Platform = Data("Platform").ToObject(Of Integer)
|
||||
Types = Data("Types").ToObject(Of Integer)
|
||||
Slug = Data("Slug")
|
||||
Id = Data("Id")
|
||||
If Data.ContainsKey("CurseForgeFileIds") Then CurseForgeFileIds = CType(Data("CurseForgeFileIds"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
|
||||
RawName = Data("RawName")
|
||||
Description = Data("Description")
|
||||
Website = Data("Website")
|
||||
LastUpdate = Data("LastUpdate")
|
||||
DownloadCount = Data("DownloadCount")
|
||||
If Data.ContainsKey("ModLoaders") Then
|
||||
Loaders = CType(Data("ModLoaders"), JArray).Select(Function(t) CType(t.ToObject(Of Integer), ModLoaderTypes)).ToList
|
||||
Else
|
||||
Loaders = New List(Of ModLoaderTypes)
|
||||
End If
|
||||
Tags = CType(Data("Tags"), JArray).Select(Function(t) t.ToString).ToList
|
||||
If Data.ContainsKey("LogoUrl") Then LogoUrl = Data("LogoUrl")
|
||||
If Data.ContainsKey("Drops") Then
|
||||
Drops = CType(Data("Drops"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
|
||||
Else
|
||||
Drops = New List(Of Integer)
|
||||
End If
|
||||
#End Region
|
||||
Else
|
||||
Platform = If(Data.ContainsKey("summary"), ResourcePlatforms.CurseForge, ResourcePlatforms.Modrinth)
|
||||
If Platform = ResourcePlatforms.CurseForge Then
|
||||
#Region "CurseForge"
|
||||
'简单信息
|
||||
Id = Data("id")
|
||||
Slug = Data("slug")
|
||||
RawName = Data("name")
|
||||
Description = Data("summary")
|
||||
Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
|
||||
LastUpdate = Data("dateReleased") '#1194
|
||||
DownloadCount = Data("downloadCount")
|
||||
If Data("logo").Count > 0 Then
|
||||
If Data("logo")("thumbnailUrl") Is Nothing OrElse Data("logo")("thumbnailUrl") = "" Then
|
||||
LogoUrl = Data("logo")("url")
|
||||
Else
|
||||
LogoUrl = Data("logo")("thumbnailUrl")
|
||||
End If
|
||||
End If
|
||||
If LogoUrl = "" Then LogoUrl = Nothing
|
||||
'Type
|
||||
If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
|
||||
Types = ResourceTypes.Mod
|
||||
ElseIf Website.Contains("/modpacks/") Then
|
||||
Types = ResourceTypes.ModPack
|
||||
ElseIf Website.Contains("/resourcepacks/") Then
|
||||
Types = ResourceTypes.ResourcePack
|
||||
ElseIf Website.Contains("/texture-packs/") Then
|
||||
Types = ResourceTypes.ResourcePack
|
||||
ElseIf Website.Contains("/shaders/") Then
|
||||
Types = ResourceTypes.Shader
|
||||
Else
|
||||
Types = ResourceTypes.DataPack
|
||||
End If
|
||||
'FileIndexes / VanillaMajorVersions / ModLoaders
|
||||
Loaders = New List(Of ModLoaderTypes)
|
||||
Dim Files As New List(Of KeyValuePair(Of Integer, List(Of String))) 'FileId, GameVersions
|
||||
For Each File In If(Data("latestFiles"), New JArray)
|
||||
Dim NewFile = ResourceVersion.FromPlatformJson(File, Types)
|
||||
If Not NewFile.DownloadAvailable Then Continue For
|
||||
Loaders.AddRange(NewFile.Loaders)
|
||||
Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
|
||||
If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
|
||||
Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
|
||||
Next
|
||||
For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
|
||||
If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
|
||||
Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
|
||||
Next
|
||||
CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
|
||||
Drops = Files.SelectMany(Function(f) f.Value).
|
||||
Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
|
||||
Loaders = Loaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
|
||||
'Tags
|
||||
Tags = New List(Of String)
|
||||
For Each Category In If(Data("categories"), New JArray). '镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831)
|
||||
Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
|
||||
Select Case Category
|
||||
'Mod
|
||||
Case 406 : Tags.Add("世界元素")
|
||||
Case 407 : Tags.Add("生物群系")
|
||||
Case 410 : Tags.Add("维度")
|
||||
Case 408 : Tags.Add("矿物/资源")
|
||||
Case 409 : Tags.Add("天然结构")
|
||||
Case 412 : Tags.Add("科技")
|
||||
Case 415 : Tags.Add("管道/物流")
|
||||
Case 4843 : Tags.Add("自动化")
|
||||
Case 417 : Tags.Add("能源")
|
||||
Case 4558 : Tags.Add("红石")
|
||||
Case 436 : Tags.Add("食物/烹饪")
|
||||
Case 416 : Tags.Add("农业")
|
||||
Case 414 : Tags.Add("运输")
|
||||
Case 420 : Tags.Add("仓储")
|
||||
Case 419 : Tags.Add("魔法")
|
||||
Case 422 : Tags.Add("冒险")
|
||||
Case 424 : Tags.Add("装饰")
|
||||
Case 411 : Tags.Add("生物")
|
||||
Case 434 : Tags.Add("装备")
|
||||
Case 6814 : Tags.Add("性能优化")
|
||||
Case 9026 : Tags.Add("创造模式")
|
||||
Case 423 : Tags.Add("信息显示")
|
||||
Case 435 : Tags.Add("服务器")
|
||||
Case 5191 : Tags.Add("改良")
|
||||
Case 421 : Tags.Add("支持库")
|
||||
'整合包
|
||||
Case 4484 : Tags.Add("多人")
|
||||
Case 4479 : Tags.Add("硬核")
|
||||
Case 4483 : Tags.Add("战斗")
|
||||
Case 4478 : Tags.Add("任务")
|
||||
Case 4472 : Tags.Add("科技")
|
||||
Case 4473 : Tags.Add("魔法")
|
||||
Case 4475 : Tags.Add("冒险")
|
||||
Case 4476 : Tags.Add("探索")
|
||||
Case 4477 : Tags.Add("小游戏")
|
||||
Case 4474 : Tags.Add("科幻")
|
||||
Case 4736 : Tags.Add("空岛")
|
||||
Case 5128 : Tags.Add("原版改良")
|
||||
Case 4487 : Tags.Add("FTB")
|
||||
Case 4480 : Tags.Add("基于地图")
|
||||
Case 4481 : Tags.Add("轻量")
|
||||
Case 4482 : Tags.Add("大型")
|
||||
'资源包
|
||||
Case 403 : Tags.Add("原版风")
|
||||
Case 400 : Tags.Add("写实风")
|
||||
Case 401 : Tags.Add("现代风")
|
||||
Case 402 : Tags.Add("中世纪")
|
||||
Case 399 : Tags.Add("蒸汽朋克")
|
||||
Case 5244 : Tags.Add("含字体")
|
||||
Case 404 : Tags.Add("动态效果")
|
||||
Case 4465 : Tags.Add("兼容 Mod")
|
||||
Case 393 : Tags.Add("16x")
|
||||
Case 394 : Tags.Add("32x")
|
||||
Case 395 : Tags.Add("64x")
|
||||
Case 396 : Tags.Add("128x")
|
||||
Case 397 : Tags.Add("256x")
|
||||
Case 398 : Tags.Add("超高清")
|
||||
Case 5193 : Tags.Add("数据包") '有这个 Tag 的项会从资源包请求中被移除
|
||||
'光影包
|
||||
Case 6553 : Tags.Add("写实风")
|
||||
Case 6554 : Tags.Add("幻想风")
|
||||
Case 6555 : Tags.Add("原版风")
|
||||
'数据包
|
||||
Case 6948 : Tags.Add("冒险")
|
||||
Case 6949 : Tags.Add("幻想")
|
||||
Case 6950 : Tags.Add("支持库")
|
||||
Case 6952 : Tags.Add("魔法")
|
||||
Case 6946 : Tags.Add("Mod 相关")
|
||||
Case 6951 : Tags.Add("科技")
|
||||
Case 6953 : Tags.Add("实用")
|
||||
End Select
|
||||
Next
|
||||
#End Region
|
||||
Else
|
||||
#Region "Modrinth"
|
||||
'简单信息
|
||||
Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
|
||||
Slug = Data("slug")
|
||||
RawName = Data("title")
|
||||
Description = Data("description")
|
||||
LastUpdate = If(Data("date_modified"), Data("updated"))
|
||||
DownloadCount = Data("downloads")
|
||||
LogoUrl = Data("icon_url")
|
||||
If LogoUrl = "" Then LogoUrl = Nothing
|
||||
Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
|
||||
'GameVersions
|
||||
'搜索结果的键为 versions,获取特定工程的键为 game_versions
|
||||
UnsafeGameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).Select(Function(v) v.ToString).Distinct.ToList
|
||||
Drops = UnsafeGameVersions.Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
|
||||
'Type
|
||||
Select Case Data("project_type").ToString
|
||||
Case "modpack" : Types = ResourceTypes.ModPack
|
||||
Case "resourcepack" : Types = ResourceTypes.ResourcePack
|
||||
Case "shader" : Types = ResourceTypes.Shader
|
||||
Case Else : Types = ResourceTypes.Mod 'Modrinth 将数据包标为 Mod,但 categories 字段里有 datapack
|
||||
End Select
|
||||
'Tags & ModLoaders
|
||||
Tags = New List(Of String)
|
||||
Loaders = New List(Of ModLoaderTypes)
|
||||
If Data.ContainsKey("loaders") Then
|
||||
For Each Category In Data("loaders").Select(Function(t) t.ToString)
|
||||
Select Case Category
|
||||
Case "forge" : Loaders.Add(ModLoaderTypes.Forge)
|
||||
Case "fabric" : Loaders.Add(ModLoaderTypes.Fabric)
|
||||
Case "quilt" : Loaders.Add(ModLoaderTypes.Quilt)
|
||||
Case "neoforge" : Loaders.Add(ModLoaderTypes.NeoForge)
|
||||
End Select
|
||||
Next
|
||||
End If
|
||||
For Each Category In Data("categories").Select(Function(t) t.ToString)
|
||||
Select Case Category
|
||||
'加载器
|
||||
Case "forge" : Loaders.Add(ModLoaderTypes.Forge)
|
||||
Case "fabric" : Loaders.Add(ModLoaderTypes.Fabric)
|
||||
Case "quilt" : Loaders.Add(ModLoaderTypes.Quilt)
|
||||
Case "neoforge" : Loaders.Add(ModLoaderTypes.NeoForge)
|
||||
Case "datapack" : Types = ResourceTypes.DataPack
|
||||
'共用
|
||||
Case "technology" : Tags.Add("科技")
|
||||
Case "magic" : Tags.Add("魔法")
|
||||
Case "adventure" : Tags.Add("冒险")
|
||||
Case "utility" : Tags.Add("实用")
|
||||
Case "optimization" : Tags.Add("性能优化")
|
||||
Case "vanilla-like" : Tags.Add("原版风")
|
||||
Case "realistic" : Tags.Add("写实风")
|
||||
'Mod/数据包
|
||||
Case "worldgen" : Tags.Add("世界元素")
|
||||
Case "food" : Tags.Add("食物/烹饪")
|
||||
Case "game-mechanics" : Tags.Add("游戏机制")
|
||||
Case "transportation" : Tags.Add("运输")
|
||||
Case "storage" : Tags.Add("仓储")
|
||||
Case "decoration" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装饰")
|
||||
Case "mobs" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("生物")
|
||||
Case "equipment" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装备")
|
||||
Case "social" : Tags.Add("服务器")
|
||||
Case "library" : Tags.Add("支持库")
|
||||
'整合包
|
||||
Case "multiplayer" : Tags.Add("多人")
|
||||
Case "challenging" : Tags.Add("硬核")
|
||||
Case "combat" : Tags.Add("战斗")
|
||||
Case "quests" : Tags.Add("任务")
|
||||
Case "kitchen-sink" : Tags.Add("水槽包")
|
||||
Case "lightweight" : Tags.Add("轻量")
|
||||
'资源包
|
||||
Case "simplistic" : Tags.Add("简洁")
|
||||
Case "combat" : Tags.Add("战斗")
|
||||
Case "tweaks" : Tags.Add("改良")
|
||||
|
||||
Case "8x-" : Tags.Add("极简")
|
||||
Case "16x" : Tags.Add("16x")
|
||||
Case "32x" : Tags.Add("32x")
|
||||
Case "48x" : Tags.Add("48x")
|
||||
Case "64x" : Tags.Add("64x")
|
||||
Case "128x" : Tags.Add("128x")
|
||||
Case "256x" : Tags.Add("256x")
|
||||
Case "512x+" : Tags.Add("超高清")
|
||||
|
||||
Case "audio" : Tags.Add("含声音")
|
||||
Case "fonts" : Tags.Add("含字体")
|
||||
Case "models" : Tags.Add("含模型")
|
||||
Case "gui" : Tags.Add("含 UI")
|
||||
Case "locale" : Tags.Add("含语言")
|
||||
Case "core-shaders" : Tags.Add("核心着色器")
|
||||
Case "modded" : Tags.Add("兼容 Mod")
|
||||
'光影包
|
||||
Case "fantasy" : Tags.Add("幻想风")
|
||||
Case "semi-realistic" : Tags.Add("半写实风")
|
||||
Case "cartoon" : Tags.Add("卡通风")
|
||||
'暂时不添加性能负荷 Tag
|
||||
'Case "potato" : Tags.Add("极低")
|
||||
'Case "low" : Tags.Add("低")
|
||||
'Case "medium" : Tags.Add("中")
|
||||
'Case "high" : Tags.Add("高")
|
||||
Case "colored-lighting" : Tags.Add("彩色光照")
|
||||
Case "path-tracing" : Tags.Add("路径追踪")
|
||||
Case "pbr" : Tags.Add("PBR")
|
||||
Case "reflections" : Tags.Add("反射")
|
||||
|
||||
Case "iris" : Tags.Add("Iris")
|
||||
Case "optifine" : Tags.Add("OptiFine")
|
||||
Case "vanilla" : Tags.Add("原版可用")
|
||||
End Select
|
||||
Next
|
||||
If Types = ResourceTypes.DataPack AndAlso Loaders.Any Then Types = ResourceTypes.DataPack Or ResourceTypes.Mod
|
||||
#End Region
|
||||
End If
|
||||
If Not Tags.Any() Then Tags.Add("其他")
|
||||
Tags.Sort()
|
||||
Loaders.Sort()
|
||||
End If
|
||||
'保存缓存
|
||||
Cache(Id) = Me
|
||||
End Sub
|
||||
''' <summary>
|
||||
''' 从工程 ID 联网获取对应工程的信息。
|
||||
''' </summary>
|
||||
Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms) As ResourceProject
|
||||
If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
|
||||
If Cache.ContainsKey(ProjectId) Then '存在缓存
|
||||
Return Cache(ProjectId)
|
||||
ElseIf Platform = ResourcePlatforms.CurseForge Then 'CurseForge
|
||||
Return New ResourceProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data"))
|
||||
Else 'Modrinth
|
||||
Return New ResourceProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId))
|
||||
End If
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 从工程 ID 列表联网获取工程信息,结果不按原顺序排列。
|
||||
''' </summary>
|
||||
Public Shared Iterator Function FromProjectIds(ProjectIds As IEnumerable(Of String), Platform As ResourcePlatforms) As IEnumerable(Of ResourceProject)
|
||||
If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
|
||||
'获取缓存
|
||||
Dim MissingProjectIds As New List(Of String)
|
||||
For Each ProjectId In ProjectIds
|
||||
If Cache.ContainsKey(ProjectId) Then
|
||||
Yield Cache(ProjectId)
|
||||
Else
|
||||
MissingProjectIds.Add(ProjectId)
|
||||
End If
|
||||
Next
|
||||
If Not MissingProjectIds.Any() Then Return
|
||||
'联网获取剩余的工程对象
|
||||
Dim Projects As JArray
|
||||
If Platform.HasFlag(ResourcePlatforms.CurseForge) Then
|
||||
Projects = DlModRequest("https://api.curseforge.com/v1/mods",
|
||||
HttpMethod.Post, "{""modIds"": [" & MissingProjectIds.Join(","c) & "]}", "application/json")("data")
|
||||
Else
|
||||
Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{MissingProjectIds.Join(""",""")}""]")
|
||||
End If
|
||||
For Each Project In Projects
|
||||
Yield New ResourceProject(Project)
|
||||
Next
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 将当前实例转为可用于保存缓存的 Json。
|
||||
''' </summary>
|
||||
Public Function ToJson() As JObject
|
||||
Dim Json As New JObject
|
||||
Json("Platform") = CInt(Platform)
|
||||
Json("Types") = CInt(Types)
|
||||
Json("Slug") = Slug
|
||||
Json("Id") = Id
|
||||
If CurseForgeFileIds IsNot Nothing Then Json("CurseForgeFileIds") = New JArray(CurseForgeFileIds)
|
||||
Json("RawName") = RawName
|
||||
Json("Description") = Description
|
||||
Json("Website") = Website
|
||||
Json("LastUpdate") = LastUpdate
|
||||
Json("DownloadCount") = DownloadCount
|
||||
If Loaders IsNot Nothing AndAlso Loaders.Any Then Json("ModLoaders") = New JArray(Loaders.Select(Function(m) CInt(m)))
|
||||
Json("Tags") = New JArray(Tags)
|
||||
If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
|
||||
If Drops.Any Then Json("Drops") = New JArray(Drops)
|
||||
Json("CacheTime") = Date.Now '用于检查缓存时间
|
||||
Return Json
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 将当前工程信息实例化为控件。
|
||||
''' </summary>
|
||||
Public Function ToResourceItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean) As MyVirtualizingElement(Of MyResourceItem)
|
||||
'获取版本描述
|
||||
Dim GameVersionDescription As String
|
||||
If Drops Is Nothing OrElse Not Drops.Any() Then
|
||||
GameVersionDescription = "仅快照版本" '#5412
|
||||
Else
|
||||
Dim Segments As New List(Of String)
|
||||
Dim IsOld As Boolean = False
|
||||
For i = 0 To Drops.Count - 1 '版本号一定为降序
|
||||
'获取当前连续的版本号段
|
||||
Dim StartDrop As Integer = Drops(i), EndDrop As Integer = Drops(i)
|
||||
If StartDrop < 120 Then '如果支持新版本,则不显示 1.11-
|
||||
If Segments.Any() AndAlso Not IsOld Then
|
||||
Exit For
|
||||
Else
|
||||
IsOld = True
|
||||
End If
|
||||
End If
|
||||
For ii = i + 1 To Drops.Count - 1
|
||||
If AllDrops Is Nothing OrElse AllDrops.IndexOf(Drops(ii)) <> AllDrops.IndexOf(EndDrop) + 1 Then Exit For
|
||||
EndDrop = Drops(ii)
|
||||
i = ii
|
||||
Next
|
||||
'将版本号段转为描述文本
|
||||
Dim StartName = McVersion.DropToVersion(StartDrop)
|
||||
Dim EndName = McVersion.DropToVersion(EndDrop)
|
||||
If StartDrop = EndDrop Then
|
||||
Segments.Add(StartName)
|
||||
ElseIf AllDrops IsNot Nothing AndAlso StartDrop >= AllDrops.First Then
|
||||
If EndDrop < 120 Then
|
||||
Segments.Clear()
|
||||
Segments.Add("全版本")
|
||||
Exit For
|
||||
Else
|
||||
Segments.Add(EndName & "+")
|
||||
End If
|
||||
ElseIf EndDrop < 120 Then
|
||||
Segments.Add(StartName & "-")
|
||||
Exit For
|
||||
ElseIf AllDrops Is Nothing OrElse AllDrops.IndexOf(EndDrop) - AllDrops.IndexOf(StartDrop) = 1 Then
|
||||
Segments.Add(StartName & ", " & EndName)
|
||||
Else
|
||||
Segments.Add(StartName & "~" & EndName)
|
||||
End If
|
||||
Next
|
||||
GameVersionDescription = Segments.Join(", ")
|
||||
End If
|
||||
'获取 Mod 加载器描述
|
||||
Dim ModLoaderDescriptionFull As String, ModLoaderDescriptionPart As String
|
||||
Dim ModLoadersForDesc As New List(Of ModLoaderTypes)(Loaders)
|
||||
If Settings.Get("ToolDownloadIgnoreQuilt") Then ModLoadersForDesc.Remove(ModLoaderTypes.Quilt)
|
||||
Select Case ModLoadersForDesc.Count
|
||||
Case 0
|
||||
If Loaders.IsSingle Then
|
||||
ModLoaderDescriptionFull = "仅 " & Loaders.Single.ToString
|
||||
ModLoaderDescriptionPart = Loaders.Single.ToString
|
||||
Else
|
||||
ModLoaderDescriptionFull = "未知"
|
||||
ModLoaderDescriptionPart = ""
|
||||
End If
|
||||
Case 1
|
||||
ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
|
||||
ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
|
||||
Case Else
|
||||
Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
|
||||
If Loaders.Contains(ModLoaderTypes.Forge) AndAlso
|
||||
(NewestDrop < 140 OrElse Loaders.Contains(ModLoaderTypes.Fabric)) AndAlso
|
||||
(NewestDrop < 200 OrElse Loaders.Contains(ModLoaderTypes.NeoForge)) AndAlso
|
||||
(NewestDrop < 140 OrElse Loaders.Contains(ModLoaderTypes.Quilt) OrElse Settings.Get("ToolDownloadIgnoreQuilt")) Then
|
||||
ModLoaderDescriptionFull = "任意"
|
||||
ModLoaderDescriptionPart = ""
|
||||
Else
|
||||
ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
|
||||
ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
|
||||
End If
|
||||
End Select
|
||||
'实例化 UI
|
||||
Return New MyVirtualizingElement(Of MyResourceItem)(
|
||||
Function()
|
||||
Dim NewItem As New MyResourceItem With {.Tag = Me}
|
||||
ApplyLogoToMyImage(NewItem.PathLogo)
|
||||
Dim Titles = GetControlTitle(True)
|
||||
NewItem.Title = Titles.Title
|
||||
If Titles.SubTitle = "" Then
|
||||
CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
|
||||
Else
|
||||
NewItem.SubTitle = Titles.SubTitle
|
||||
End If
|
||||
NewItem.Tags = Tags
|
||||
NewItem.Description = Description.ReplaceLineEndings("")
|
||||
'下边栏
|
||||
If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
|
||||
'全部隐藏
|
||||
CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
|
||||
CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
|
||||
NewItem.ColumnVersion1.Width = New GridLength(0)
|
||||
NewItem.ColumnVersion2.MaxWidth = 0
|
||||
NewItem.ColumnVersion3.Width = New GridLength(0)
|
||||
ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
|
||||
'全部显示
|
||||
NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
|
||||
ElseIf ShowMcVersionDesc Then
|
||||
'仅显示版本
|
||||
NewItem.LabVersion.Text = GameVersionDescription
|
||||
Else
|
||||
'仅显示 Mod 加载器
|
||||
NewItem.LabVersion.Text = ModLoaderDescriptionFull
|
||||
End If
|
||||
NewItem.LabSource.Text = Platform.ToString
|
||||
NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
|
||||
NewItem.LabDownload.Text =
|
||||
If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
|
||||
If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
|
||||
Return NewItem
|
||||
End Function) With {.Height = 64}
|
||||
End Function
|
||||
Public Sub ApplyLogoToMyImage(Img As MyImage)
|
||||
If String.IsNullOrEmpty(LogoUrl) Then
|
||||
Img.Source = PathImage & "Icons/NoIcon.png"
|
||||
Else
|
||||
Img.Source = LogoUrl
|
||||
Img.FallbackSource = DlSourceModGet(LogoUrl)
|
||||
End If
|
||||
End Sub
|
||||
Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
|
||||
'检查下列代码时可以参考 #1567 的测试例
|
||||
Dim Title As String = RawName
|
||||
Dim SubtitleList As List(Of String)
|
||||
If TranslatedName = RawName Then
|
||||
'没有中文翻译
|
||||
'将所有名称分段
|
||||
Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
|
||||
Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
|
||||
If NameLists.IsSingle Then GoTo NoSubtitle
|
||||
'查找其中的缩写、Forge/Fabric 等版本标记
|
||||
SubtitleList = New List(Of String)
|
||||
Dim NormalNameList = New List(Of String)
|
||||
For Each Name In NameLists
|
||||
Dim LowerName As String = Name.Lower
|
||||
If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
|
||||
'缩写
|
||||
SubtitleList.Add(Name)
|
||||
ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
|
||||
Not RegexCheck(LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母
|
||||
'Forge/Fabric 等版本标记
|
||||
SubtitleList.Add(Name)
|
||||
Else
|
||||
'其他部分
|
||||
NormalNameList.Add(Name)
|
||||
End If
|
||||
Next
|
||||
'根据分类后的结果处理
|
||||
If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
|
||||
'同时包含 NormalName 和 Subtitle
|
||||
Title = NormalNameList.Join(" - ")
|
||||
Else
|
||||
'有中文翻译
|
||||
'尝试将文本分为三段:Title (EnglishName) - Suffix
|
||||
'检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
|
||||
Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
|
||||
Dim Suffix As String = ""
|
||||
If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
|
||||
Dim EnglishName As String = TranslatedName
|
||||
If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
|
||||
EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
|
||||
'中段的额外信息截取
|
||||
SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
|
||||
Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
|
||||
If SubtitleList.Count > 1 AndAlso
|
||||
Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
|
||||
Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
|
||||
SubtitleList = New List(Of String) From {EnglishName} '使用原名
|
||||
End If
|
||||
'添加后缀
|
||||
If Suffix <> "" Then SubtitleList.Add(Suffix)
|
||||
End If
|
||||
SubtitleList = SubtitleList.Distinct.ToList()
|
||||
'设置标题与描述
|
||||
Dim Subtitle As String = ""
|
||||
If SubtitleList.Any Then
|
||||
For Each Ex In SubtitleList
|
||||
Dim IsModLoaderDescription As Boolean =
|
||||
Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
|
||||
'是否显示 ModLoader 信息
|
||||
If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
|
||||
'去除 “Forge/Fabric” 这一无意义提示
|
||||
If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
|
||||
'将 “Forge” 等提示改为 “Forge 版”
|
||||
If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
|
||||
Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
|
||||
Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
|
||||
End If
|
||||
'将 “forge” 等词语的首字母大写
|
||||
Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
|
||||
Subtitle &= " | " & Ex.Trim
|
||||
Next
|
||||
Else
|
||||
NoSubtitle:
|
||||
Subtitle = ""
|
||||
End If
|
||||
Return (Title, Subtitle)
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' 工程 ID 对应的工程实例的缓存。不会保存到文件。
|
||||
''' </summary>
|
||||
Protected Friend Shared Cache As New Dictionary(Of String, ResourceProject)
|
||||
|
||||
''' <summary>
|
||||
''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
|
||||
''' </summary>
|
||||
Public Function IsLike(Project As ResourceProject) As Boolean
|
||||
If Id = Project.Id Then Return True '相同实例
|
||||
'提取字符串中的字母和数字
|
||||
Dim GetRaw =
|
||||
Function(Data As String) As String
|
||||
Dim Result As New StringBuilder()
|
||||
For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
|
||||
Result.Append(r)
|
||||
Next
|
||||
Return Result.ToString.Lower
|
||||
End Function
|
||||
'来自不同的网站
|
||||
If Platform = Project.Platform Then Return False
|
||||
'Mod 加载器一致
|
||||
If Loaders.Count <> Project.Loaders.Count OrElse Loaders.Except(Project.Loaders).Any() Then Return False
|
||||
'若不为光影,则要求 MC 版本一致
|
||||
If Types <> ResourceTypes.Shader AndAlso (Drops.Count <> Project.Drops.Count OrElse Drops.Except(Project.Drops).Any()) Then Return False
|
||||
'最近更新时间差距在一周以内
|
||||
If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
|
||||
'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
|
||||
If TranslatedName = Project.TranslatedName OrElse
|
||||
RawName = Project.RawName OrElse Description = Project.Description OrElse
|
||||
GetRaw(Slug) = GetRaw(Project.Slug) Then
|
||||
Log($"[Resource] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
|
||||
'如果只有一个有 DatabaseEntry,设置给另外一个
|
||||
If WikiEntry Is Nothing AndAlso Project.WikiEntry IsNot Nothing Then WikiEntry = Project.WikiEntry
|
||||
If WikiEntry IsNot Nothing AndAlso Project.WikiEntry Is Nothing Then Project.WikiEntry = WikiEntry
|
||||
Return True
|
||||
End If
|
||||
Return False
|
||||
End Function
|
||||
|
||||
'辅助函数
|
||||
Public Overrides Function ToString() As String
|
||||
Return $"{Id} ({Slug}): {RawName}"
|
||||
End Function
|
||||
Public Overrides Function Equals(obj As Object) As Boolean
|
||||
Dim project = TryCast(obj, ResourceProject)
|
||||
Return project IsNot Nothing AndAlso Id = project.Id
|
||||
End Function
|
||||
Public Shared Operator =(left As ResourceProject, right As ResourceProject) As Boolean
|
||||
Return EqualityComparer(Of ResourceProject).Default.Equals(left, right)
|
||||
End Operator
|
||||
Public Shared Operator <>(left As ResourceProject, right As ResourceProject) As Boolean
|
||||
Return Not left = right
|
||||
End Operator
|
||||
|
||||
End Class
|
||||
@@ -0,0 +1,515 @@
|
||||
Public Class ResourceSearcher
|
||||
Private Const RESULT_PAGE_SIZE = 40
|
||||
Private Const MSG_NO_CHINESE_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
|
||||
|
||||
'输入
|
||||
Public Class SearchRequest
|
||||
|
||||
'结果要求
|
||||
|
||||
''' <summary>
|
||||
''' 结果存储器,会从中获取需加载的页面等。
|
||||
''' 加载后也应输出到此处。
|
||||
''' </summary>
|
||||
Public Storage As SearchResult
|
||||
''' <summary>
|
||||
''' 应当尽量达成的结果数量。
|
||||
''' </summary>
|
||||
Public TargetResultCount As Integer
|
||||
''' <summary>
|
||||
''' 根据加载位置记录,是否还可以继续获取内容。
|
||||
''' </summary>
|
||||
Public ReadOnly Property CanContinue As Boolean
|
||||
Get
|
||||
If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0
|
||||
If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0
|
||||
If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
|
||||
Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
|
||||
End Get
|
||||
End Property
|
||||
|
||||
'输入内容
|
||||
|
||||
''' <summary>
|
||||
''' 筛选资源种类。
|
||||
''' </summary>
|
||||
Public Type As ResourceTypes
|
||||
''' <summary>
|
||||
''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
|
||||
''' </summary>
|
||||
Public Tag As String = ""
|
||||
''' <summary>
|
||||
''' 筛选 Mod 加载器类别。
|
||||
''' </summary>
|
||||
Public ModLoader As ModLoaderTypes = ModLoaderTypes.Any
|
||||
''' <summary>
|
||||
''' 筛选 MC 版本。
|
||||
''' </summary>
|
||||
Public GameVersion As String = Nothing
|
||||
''' <summary>
|
||||
''' 搜索的初始内容。
|
||||
''' </summary>
|
||||
Public SearchText As String = Nothing
|
||||
''' <summary>
|
||||
''' 允许的来源。
|
||||
''' </summary>
|
||||
Public Sources As ResourcePlatforms = ResourcePlatforms.Any
|
||||
Public Sub New(Type As ResourceTypes, Storage As SearchResult, TargetResultCount As Integer)
|
||||
Me.Type = Type
|
||||
Me.Storage = Storage
|
||||
Me.TargetResultCount = TargetResultCount
|
||||
End Sub
|
||||
|
||||
'构造请求
|
||||
|
||||
''' <summary>
|
||||
''' 获取对应的 CurseForge API 请求链接。
|
||||
''' </summary>
|
||||
Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
|
||||
If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
|
||||
'应用筛选参数
|
||||
Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={RESULT_PAGE_SIZE}"
|
||||
Select Case Type
|
||||
Case ResourceTypes.Mod
|
||||
Address += "&classId=6"
|
||||
Case ResourceTypes.ModPack
|
||||
Address += "&classId=4471"
|
||||
Case ResourceTypes.DataPack
|
||||
Address += "&classId=6945"
|
||||
Case ResourceTypes.Shader
|
||||
Address += "&classId=6552"
|
||||
Case ResourceTypes.ResourcePack
|
||||
Address += "&classId=12"
|
||||
End Select
|
||||
If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
|
||||
If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
|
||||
If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
|
||||
If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & EscapeUtils.UrlEscape(SearchText)
|
||||
If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
|
||||
Return Address
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 获取对应的 Modrinth API 请求链接。
|
||||
''' </summary>
|
||||
Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
|
||||
If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
|
||||
'应用筛选参数
|
||||
Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
|
||||
If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & EscapeUtils.UrlEscape(SearchText)
|
||||
If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
|
||||
'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
|
||||
Dim Facets As New List(Of String)
|
||||
Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
|
||||
If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
|
||||
If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).Lower}'""]")
|
||||
If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
|
||||
Address += "&facets=[" & String.Join(",", Facets) & "]"
|
||||
Return Address
|
||||
End Function
|
||||
|
||||
'相同判断
|
||||
Public Overrides Function Equals(obj As Object) As Boolean
|
||||
Dim request = TryCast(obj, SearchRequest)
|
||||
Return request IsNot Nothing AndAlso
|
||||
Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
|
||||
Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Sources = request.Sources AndAlso
|
||||
GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
|
||||
End Function
|
||||
Public Shared Operator =(left As SearchRequest, right As SearchRequest) As Boolean
|
||||
Return EqualityComparer(Of SearchRequest).Default.Equals(left, right)
|
||||
End Operator
|
||||
Public Shared Operator <>(left As SearchRequest, right As SearchRequest) As Boolean
|
||||
Return Not left = right
|
||||
End Operator
|
||||
Public Overrides Function GetHashCode() As Integer
|
||||
Return (Type, Tag, ModLoader, GameVersion, SearchText, Sources).GetHashCode()
|
||||
End Function
|
||||
|
||||
End Class
|
||||
|
||||
'输出
|
||||
Public Class SearchResult
|
||||
|
||||
'加载位置记录
|
||||
|
||||
Public CurseForgeOffset As Integer = 0
|
||||
Public CurseForgeTotal As Integer = -1
|
||||
|
||||
Public ModrinthOffset As Integer = 0
|
||||
Public ModrinthTotal As Integer = -1
|
||||
|
||||
'结果列表
|
||||
|
||||
''' <summary>
|
||||
''' 可供展示的所有工程的列表。
|
||||
''' </summary>
|
||||
Public Results As New List(Of ResourceProject)
|
||||
''' <summary>
|
||||
''' 当前的错误信息。如果没有则为 Nothing。
|
||||
''' </summary>
|
||||
Public ErrorMessage As String = Nothing
|
||||
|
||||
End Class
|
||||
|
||||
''' <summary>
|
||||
''' 根据搜索请求获取一系列的工程列表。
|
||||
''' </summary>
|
||||
Public Shared Function GetLoader(InputDelegate As Func(Of SearchRequest)) As LoaderTask(Of SearchRequest, Integer)
|
||||
Return New LoaderTask(Of SearchRequest, Integer)("社区资源获取", AddressOf Run, InputDelegate) With {.ReloadTimeout = 60 * 1000}
|
||||
End Function
|
||||
Private Shared Sub Run(Task As LoaderTask(Of SearchRequest, Integer))
|
||||
Dim Request As SearchRequest = Task.Input
|
||||
Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
|
||||
|
||||
#Region "前置检查"
|
||||
|
||||
If Storage.Results.Count >= Request.TargetResultCount Then
|
||||
Log($"[Resource] 已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
|
||||
Return
|
||||
ElseIf Not Request.CanContinue Then
|
||||
If Not Storage.Results.Any() Then
|
||||
Throw New Exception("没有符合条件的结果")
|
||||
Else
|
||||
Log($"[Resource] 已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
|
||||
Return
|
||||
End If
|
||||
End If
|
||||
|
||||
'拒绝 1.13- Quilt(这个版本根本没有 Quilt)
|
||||
If Request.ModLoader = ModLoaderTypes.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
|
||||
Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
|
||||
End If
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "中文搜索"
|
||||
|
||||
Dim RawSearchText As String = If(Request.SearchText, "").Trim
|
||||
RawSearchText = RawSearchText.Lower
|
||||
Log("[Resource] 工程列表搜索原始文本:" & RawSearchText)
|
||||
|
||||
Dim IsChineseSearch As Boolean =
|
||||
RegexCheck(RawSearchText, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
|
||||
(Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
|
||||
Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
|
||||
If IsChineseSearch Then
|
||||
'帮助方法:从搜索项提取可能的英文单词
|
||||
Dim ExtractWords =
|
||||
Function(Result As SearchEntry(Of WikiEntry), Source As ResourcePlatforms) As IEnumerable(Of String)
|
||||
'从各个可能的来源提取候选
|
||||
Dim Candidates As New List(Of String)
|
||||
If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = ResourcePlatforms.CurseForge Then
|
||||
Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
|
||||
End If
|
||||
If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = ResourcePlatforms.Modrinth Then
|
||||
Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
|
||||
End If
|
||||
Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
|
||||
Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
|
||||
'分词、清洗、去重
|
||||
Candidates = Candidates.
|
||||
SelectMany(Function(c) c.Split(" ")).
|
||||
Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
|
||||
Where(
|
||||
Function(w)
|
||||
If w.Length <= 1 Then Return False '单字或空白
|
||||
If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
|
||||
If Val(w) > 0 Then Return False '数字
|
||||
Return True
|
||||
End Function).Distinct.ToList
|
||||
'如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
|
||||
Dim CanForm As Func(Of String, Boolean) = Nothing
|
||||
CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
|
||||
Candidates = Candidates.Where(Function(w) Not Candidates.Any(Function(c) c.Length < w.Length AndAlso w.StartsWith(c) AndAlso CanForm(w.Substring(c.Length)))).ToList()
|
||||
Return Candidates
|
||||
End Function
|
||||
'CurseForge
|
||||
If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) Then
|
||||
'数据库搜索
|
||||
Static CurseForgeSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
|
||||
WikiEntry.All.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
|
||||
Function(Entry) New SearchEntry(Of WikiEntry) With {
|
||||
.Item = Entry,
|
||||
.SearchSource = New List(Of SearchSource) From {
|
||||
New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
|
||||
New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
|
||||
}).ToList
|
||||
Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
|
||||
If CurseForgeSearchResults.Any Then
|
||||
'选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
|
||||
Dim CurseForgeTarget =
|
||||
If(CurseForgeSearchResults.First.AbsoluteRight,
|
||||
CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
|
||||
CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
|
||||
MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
|
||||
'后处理
|
||||
CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, ResourcePlatforms.CurseForge).Join(" ")
|
||||
Log("[Resource] 中文搜索关键词(CurseForge):" & CurseForgeAltSearchText, NotifyLevel.DebugModeOnly)
|
||||
End If
|
||||
End If
|
||||
'Modrinth
|
||||
If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) Then
|
||||
'数据库搜索
|
||||
Static ModrinthSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
|
||||
WikiEntry.All.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
|
||||
Function(Entry) New SearchEntry(Of WikiEntry) With {
|
||||
.Item = Entry,
|
||||
.SearchSource = New List(Of SearchSource) From {
|
||||
New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
|
||||
New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
|
||||
}).ToList
|
||||
Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
|
||||
If ModrinthSearchResults.Any Then
|
||||
'分词
|
||||
Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
|
||||
For Each Result In ModrinthSearchResults
|
||||
For Each Word In ExtractWords(Result, ResourcePlatforms.Modrinth)
|
||||
If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
|
||||
Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
|
||||
WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
|
||||
Next
|
||||
Next
|
||||
ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
|
||||
Log("[Resource] 中文搜索关键词(Modrinth):" & ModrinthAltSearchText, NotifyLevel.DebugModeOnly)
|
||||
'直接请求工程
|
||||
ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
|
||||
End If
|
||||
End If
|
||||
'结束
|
||||
If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
|
||||
Throw New Exception(MSG_NO_CHINESE_RESULT)
|
||||
End If
|
||||
End If
|
||||
Task.Progress = 0.05
|
||||
|
||||
#End Region
|
||||
|
||||
Dim RealResults As New List(Of ResourceProject)
|
||||
NextPage:
|
||||
Dim RawResults As New ConcurrentList(Of ResourceProject)
|
||||
|
||||
#Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
|
||||
|
||||
Dim WorkThreads As New List(Of Thread)
|
||||
Dim Errors As New ConcurrentList(Of (Ex As Exception, Source As ResourcePlatforms))
|
||||
|
||||
'在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
|
||||
'所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
|
||||
Dim IgnoreModLoaderFilter = Request.ModLoader = ModLoaderTypes.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
|
||||
Try
|
||||
|
||||
'CurseForge 搜索
|
||||
If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) AndAlso
|
||||
Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
|
||||
(Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
|
||||
WorkThreads.Add(RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
'获取工程列表
|
||||
Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
|
||||
Log("[Resource] 开始 CurseForge 搜索:" & CurseForgeUrl)
|
||||
Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
|
||||
Dim ProjectList As New List(Of ResourceProject)
|
||||
For Each JsonEntry As JObject In RequestResult("data")
|
||||
Dim Project As New ResourceProject(JsonEntry)
|
||||
If Request.Type = ResourceTypes.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
|
||||
ProjectList.Add(Project)
|
||||
Next
|
||||
'更新结果
|
||||
ProjectList.ForEach(Sub(p) RawResults.Add(p))
|
||||
Storage.CurseForgeOffset += RequestResult("data").Count
|
||||
Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
|
||||
Log($"[Resource] 从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
|
||||
Catch ex As Exception
|
||||
Log(ex, "CurseForge 搜索失败")
|
||||
Storage.CurseForgeTotal = -1
|
||||
Errors.Add((ex, ResourcePlatforms.CurseForge))
|
||||
End Try
|
||||
If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
|
||||
End Sub, "CurseForge Search"))
|
||||
End If
|
||||
|
||||
'Modrinth 搜索
|
||||
If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
|
||||
Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
|
||||
(Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
|
||||
WorkThreads.Add(RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
|
||||
Log("[Resource] 开始 Modrinth 搜索:" & ModrinthUrl)
|
||||
Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
|
||||
Dim ProjectList As New List(Of ResourceProject)
|
||||
For Each JsonEntry As JObject In RequestResult("hits")
|
||||
ProjectList.Add(New ResourceProject(JsonEntry))
|
||||
Next
|
||||
'更新结果
|
||||
ProjectList.ForEach(Sub(p) RawResults.Add(p))
|
||||
Storage.ModrinthOffset += RequestResult("hits").Count
|
||||
Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
|
||||
Log($"[Resource] 从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
|
||||
Catch ex As Exception
|
||||
Log(ex, "Modrinth 搜索失败")
|
||||
Storage.ModrinthTotal = -1
|
||||
Errors.Add((ex, ResourcePlatforms.Modrinth))
|
||||
End Try
|
||||
If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
|
||||
End Sub, "Modrinth Search"))
|
||||
End If
|
||||
|
||||
'Modrinth 直接获取工程
|
||||
If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
|
||||
Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
|
||||
ModrinthSlugs.Any Then '有直接获取的 Slug
|
||||
WorkThreads.Add(RunInNewThread(
|
||||
Sub()
|
||||
Try
|
||||
Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
|
||||
Log("[Resource] 开始 Modrinth 直接获取:" & ModrinthUrl)
|
||||
Dim ProjectList As New List(Of ResourceProject)
|
||||
For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
|
||||
Dim Project As New ResourceProject(JsonEntry)
|
||||
'应用筛选
|
||||
If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
|
||||
If Not String.IsNullOrEmpty(Request.Tag) AndAlso
|
||||
Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
|
||||
If Request.ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter AndAlso
|
||||
Not Project.Loaders.Any(Function(m) m = Request.ModLoader) Then Continue For
|
||||
If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
|
||||
Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
|
||||
ProjectList.Add(Project)
|
||||
Next
|
||||
'更新结果
|
||||
ProjectList.ForEach(Sub(p) RawResults.Add(p))
|
||||
Log($"[Resource] 从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
|
||||
ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
|
||||
Catch ex As Exception
|
||||
Log(ex, "Modrinth 直接获取失败")
|
||||
Errors.Add((ex, ResourcePlatforms.Modrinth))
|
||||
End Try
|
||||
If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
|
||||
End Sub, "Modrinth Get"))
|
||||
End If
|
||||
|
||||
'等待线程结束
|
||||
For Each Thread In WorkThreads
|
||||
Thread.Join()
|
||||
If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
|
||||
Next
|
||||
|
||||
'筛除不是 Forge 的 Mod
|
||||
If IgnoreModLoaderFilter Then
|
||||
RawResults = RawResults.Where(Function(p) Not p.Loaders.Any() OrElse p.Loaders.Contains(ModLoaderTypes.Forge)).ToList
|
||||
End If
|
||||
|
||||
'确保存在结果
|
||||
Storage.ErrorMessage = Nothing
|
||||
If Not RawResults.Any() Then
|
||||
If Errors.Any() Then
|
||||
Throw Errors.First.Ex
|
||||
Else
|
||||
If IsChineseSearch AndAlso Not (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) Then
|
||||
Throw New Exception(MSG_NO_CHINESE_RESULT)
|
||||
ElseIf Request.Sources = ResourcePlatforms.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
|
||||
Throw New Exception("CurseForge 不兼容所选的类型")
|
||||
ElseIf Request.Sources = ResourcePlatforms.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
|
||||
Throw New Exception("Modrinth 不兼容所选的类型")
|
||||
Else
|
||||
Throw New Exception("没有搜索结果")
|
||||
End If
|
||||
End If
|
||||
ElseIf Errors.Any() Then
|
||||
'有结果但是有错误
|
||||
If Errors.Any(Function(e) e.Source = ResourcePlatforms.CurseForge) Then
|
||||
Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
|
||||
Else
|
||||
Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
|
||||
End If
|
||||
End If
|
||||
|
||||
Finally
|
||||
For Each Thread In WorkThreads
|
||||
If Thread.IsAlive Then Thread.Interrupt()
|
||||
Next
|
||||
End Try
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "提取非重复项,存储于 RealResults"
|
||||
|
||||
'将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
|
||||
RawResults = RawResults.Where(Function(x) x.Platform = ResourcePlatforms.CurseForge).Concat(RawResults.Where(Function(x) x.Platform <> ResourcePlatforms.CurseForge)).ToList
|
||||
'去重
|
||||
RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b)).ToList
|
||||
'已有内容去重
|
||||
RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
|
||||
Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
|
||||
'加入列表
|
||||
RealResults.AddRange(RawResults)
|
||||
Log($"[Resource] 去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "检查结果数量,如果不足且可继续,会继续加载下一页"
|
||||
|
||||
If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
|
||||
Log($"[Resource] 总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
|
||||
If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
|
||||
Log("[Resource] 将继续尝试加载下一页")
|
||||
GoTo NextPage
|
||||
Else
|
||||
Log("[Resource] 无法继续加载,将强制结束")
|
||||
End If
|
||||
End If
|
||||
|
||||
#End Region
|
||||
|
||||
#Region "将结果排序并添加"
|
||||
|
||||
Dim Scores As New Dictionary(Of ResourceProject, Double) '排序分
|
||||
Dim GetDownloadCountMult =
|
||||
Function(Project As ResourceProject) As Double
|
||||
Select Case Request.Type
|
||||
Case ResourceTypes.Mod, ResourceTypes.ModPack
|
||||
Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 5)
|
||||
Case ResourceTypes.DataPack
|
||||
Return If(Project.Platform = ResourcePlatforms.CurseForge, 10, 1)
|
||||
Case ResourceTypes.ResourcePack, ResourceTypes.Shader
|
||||
Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 4)
|
||||
Case Else
|
||||
Return 1
|
||||
End Select
|
||||
End Function
|
||||
If String.IsNullOrEmpty(RawSearchText) Then
|
||||
'如果没有搜索文本,按下载量将结果排序
|
||||
For Each Result As ResourceProject In RealResults
|
||||
Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
|
||||
Next
|
||||
Else
|
||||
'如果有搜索文本,按关联度将结果排序
|
||||
'排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
|
||||
Dim SearchEntries As New List(Of SearchEntry(Of ResourceProject))
|
||||
For Each Result As ResourceProject In RealResults
|
||||
Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
|
||||
Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
|
||||
SearchEntries.Add(New SearchEntry(Of ResourceProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
|
||||
New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1),
|
||||
New SearchSource(Result.Description, 0.05)}})
|
||||
Next
|
||||
Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
|
||||
For Each OneResult In SearchResult
|
||||
Scores(OneResult.Item) +=
|
||||
If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
|
||||
If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
|
||||
Next
|
||||
End If
|
||||
'根据排序分得出结果并添加
|
||||
If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
|
||||
Storage.Results.AddRange(
|
||||
Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
|
||||
|
||||
#End Region
|
||||
|
||||
End Sub
|
||||
|
||||
End Class
|
||||
@@ -0,0 +1,405 @@
|
||||
''' <summary>
|
||||
''' 社区资源的单个可下载版本。
|
||||
''' </summary>
|
||||
Public Class ResourceVersion
|
||||
|
||||
'基础字段
|
||||
|
||||
''' <summary>
|
||||
''' 用于唯一性鉴别该版本的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
|
||||
''' </summary>
|
||||
Public Id As String
|
||||
''' <summary>
|
||||
''' 该版本所对应的单一资源类别。
|
||||
''' </summary>
|
||||
Public ResourceType As ResourceTypes
|
||||
|
||||
'描述性字段
|
||||
|
||||
''' <summary>
|
||||
''' 版本描述名。
|
||||
''' 并非文件名或版本号,是由上传者完全自定义的字段。
|
||||
''' </summary>
|
||||
Public Display As String
|
||||
''' <summary>
|
||||
''' 发布时间。
|
||||
''' </summary>
|
||||
Public ReleaseDate As Date
|
||||
''' <summary>
|
||||
''' 下载量计数。
|
||||
''' 注意,仅为一个平台的计数,无法反映多平台加起来的下载量,且 CurseForge 可能错误地返回 0。
|
||||
''' </summary>
|
||||
Public DownloadCount As Integer
|
||||
''' <summary>
|
||||
''' 版本号。
|
||||
''' 不一定是标准格式。CurseForge 默认为 Nothing。
|
||||
''' </summary>
|
||||
Public Version As String
|
||||
''' <summary>
|
||||
''' 支持的 Mod 加载器列表。
|
||||
''' 可能为空列表,不为 Nothing。
|
||||
''' </summary>
|
||||
Public Loaders As List(Of ModLoaderTypes)
|
||||
''' <summary>
|
||||
''' 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
|
||||
''' </summary>
|
||||
Public GameVersions As List(Of String)
|
||||
|
||||
''' <summary>
|
||||
''' 发布状态。
|
||||
''' </summary>
|
||||
Public ReleaseType As ReleaseTypes
|
||||
''' <summary>
|
||||
''' 发布状态的友好描述。
|
||||
''' 例如:"正式版","Beta 版"。
|
||||
''' </summary>
|
||||
Public ReadOnly Property ReleaseTypeDisplay As String
|
||||
Get
|
||||
Select Case ReleaseType
|
||||
Case ReleaseTypes.Release
|
||||
Return "正式版"
|
||||
Case ReleaseTypes.Beta
|
||||
Return If(ModeDebug, "Beta 版", "测试版")
|
||||
Case Else
|
||||
Return If(ModeDebug, "Alpha 版", "早期测试版")
|
||||
End Select
|
||||
End Get
|
||||
End Property
|
||||
Public Enum ReleaseTypes
|
||||
'https://docs.curseforge.com/rest-api/#tocS_FileReleaseType
|
||||
Release = 1
|
||||
Beta = 2
|
||||
Alpha = 3
|
||||
End Enum
|
||||
|
||||
'下载信息
|
||||
|
||||
''' <summary>
|
||||
''' 下载信息是否可用。
|
||||
''' </summary>
|
||||
Public ReadOnly Property DownloadAvailable As Boolean
|
||||
Get
|
||||
Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
|
||||
End Get
|
||||
End Property
|
||||
''' <summary>
|
||||
''' 文件名。
|
||||
''' </summary>
|
||||
Public FileName As String = Nothing
|
||||
''' <summary>
|
||||
''' 对应文件的所有可能的下载 URL。
|
||||
''' </summary>
|
||||
Public DownloadUrls As List(Of String)
|
||||
''' <summary>
|
||||
''' 对应文件的 SHA1 或 MD5。
|
||||
''' </summary>
|
||||
Public Hash As String = Nothing
|
||||
''' <summary>
|
||||
''' 文件大小。不可用时为 -1。
|
||||
''' </summary>
|
||||
Public Size As Integer = -1
|
||||
''' <summary>
|
||||
''' 作为该版本依赖项的项目 ID。
|
||||
''' 它们可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
|
||||
''' </summary>
|
||||
Private RawDependencies As New List(Of String)
|
||||
''' <summary>
|
||||
''' 作为该版本依赖项的项目 ID。
|
||||
''' </summary>
|
||||
Public Dependencies As New List(Of String)
|
||||
|
||||
'初始化
|
||||
|
||||
''' <summary>
|
||||
''' 从平台 API 返回的 JSON 中初始化实例。若出错会抛出异常。
|
||||
''' </summary>
|
||||
Public Shared Function FromPlatformJson(Data As JObject, ExpectedResourceType As ResourceTypes) As ResourceVersion
|
||||
Dim Result As New ResourceVersion
|
||||
With Result
|
||||
.ResourceType = ExpectedResourceType
|
||||
If Data.ContainsKey("gameId") Then
|
||||
#Region "CurseForge"
|
||||
'简单信息
|
||||
.Id = Data("id")
|
||||
.Display = Data("displayName").ToString.Replace(" ", "").Trim(" ")
|
||||
.Version = Nothing
|
||||
.ReleaseDate = Data("fileDate")
|
||||
.ReleaseType = CType(Data("releaseType").ToObject(Of Integer), ReleaseTypes)
|
||||
.DownloadCount = Data("downloadCount")
|
||||
.FileName = Data("fileName")
|
||||
.Size = Data("fileLength")
|
||||
.Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
|
||||
If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
|
||||
'DownloadAddress
|
||||
Dim Url = Data("downloadUrl").ToString
|
||||
If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
|
||||
.DownloadUrls = ParseCurseForgeDownloadUrls(Url.Replace(.FileName, EscapeUtils.UrlEscape(.FileName))) '对脑残 CurseForge 的下载地址进行多种修正
|
||||
.DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
|
||||
.DownloadUrls = .DownloadUrls.Distinct.ToList '最终去重
|
||||
'Dependencies
|
||||
If Data.ContainsKey("dependencies") Then
|
||||
.RawDependencies = Data("dependencies").
|
||||
Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
|
||||
d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
|
||||
Select(Function(d) d("modId").ToString).ToList
|
||||
End If
|
||||
'GameVersions
|
||||
Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
|
||||
.GameVersions = RawVersions.
|
||||
Where(Function(v) McVersion.IsFormatFit(v)).
|
||||
Select(Function(v) v.Replace("-snapshot", " 预览版")).
|
||||
Distinct.ToList
|
||||
If .GameVersions.IsSingle Then
|
||||
.GameVersions = .GameVersions.ToList
|
||||
ElseIf .GameVersions.Count > 1 Then
|
||||
.GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
|
||||
If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
|
||||
Else
|
||||
.GameVersions = New List(Of String) From {"未知版本"}
|
||||
End If
|
||||
'ModLoaders
|
||||
.Loaders = New List(Of ModLoaderTypes)
|
||||
If RawVersions.Contains("forge") Then .Loaders.Add(ModLoaderTypes.Forge)
|
||||
If RawVersions.Contains("fabric") Then .Loaders.Add(ModLoaderTypes.Fabric)
|
||||
If RawVersions.Contains("quilt") Then .Loaders.Add(ModLoaderTypes.Quilt)
|
||||
If RawVersions.Contains("neoforge") Then .Loaders.Add(ModLoaderTypes.NeoForge)
|
||||
#End Region
|
||||
Else
|
||||
#Region "Modrinth"
|
||||
'简单信息
|
||||
.Id = Data("id")
|
||||
.Display = Data("name").ToString.Replace(" ", "").Trim(" ")
|
||||
.Version = Data("version_number")
|
||||
.ReleaseDate = Data("date_published")
|
||||
.ReleaseType = If(Data("version_type").ToString = "release", ReleaseTypes.Release, If(Data("version_type").ToString = "beta", ReleaseTypes.Beta, ReleaseTypes.Alpha))
|
||||
.DownloadCount = Data("downloads")
|
||||
If CType(Data("files"), JArray).Any() Then '可能为空
|
||||
Dim File As JToken = Data("files")(0)
|
||||
.FileName = File("filename")
|
||||
.DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
|
||||
.Size = File("size")
|
||||
.Hash = File("hashes")("sha1")
|
||||
End If
|
||||
'Loaders
|
||||
'结果可能混杂着 Mod、数据包和服务端插件
|
||||
Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
|
||||
.Loaders = New List(Of ModLoaderTypes)
|
||||
If .ResourceType.HasFlag(ResourceTypes.Mod) OrElse .ResourceType.HasFlag(ResourceTypes.DataPack) Then
|
||||
If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then .ResourceType = ResourceTypes.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
|
||||
If RawLoaders.Contains("datapack") Then .ResourceType = ResourceTypes.DataPack
|
||||
If RawLoaders.Contains("forge") Then .Loaders.Add(ModLoaderTypes.Forge) : .ResourceType = ResourceTypes.Mod
|
||||
If RawLoaders.Contains("neoforge") Then .Loaders.Add(ModLoaderTypes.NeoForge) : .ResourceType = ResourceTypes.Mod
|
||||
If RawLoaders.Contains("fabric") Then .Loaders.Add(ModLoaderTypes.Fabric) : .ResourceType = ResourceTypes.Mod
|
||||
If RawLoaders.Contains("quilt") Then .Loaders.Add(ModLoaderTypes.Quilt) : .ResourceType = ResourceTypes.Mod
|
||||
Else
|
||||
'使用传入的类别,不作修改(#8377)
|
||||
End If
|
||||
'Dependencies
|
||||
If Data.ContainsKey("dependencies") Then
|
||||
.RawDependencies = Data("dependencies").
|
||||
Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
|
||||
d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
|
||||
d("project_id").ToString.Length > 0). '有时候真的会空……
|
||||
Select(Function(d) d("project_id").ToString).ToList
|
||||
End If
|
||||
'GameVersions
|
||||
Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
|
||||
.GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
|
||||
Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
|
||||
Distinct.ToList
|
||||
If .GameVersions.IsSingle Then
|
||||
'无需处理
|
||||
ElseIf .GameVersions.Count > 1 Then
|
||||
.GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
|
||||
If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
|
||||
ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")) Then
|
||||
.GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")).ToList
|
||||
Else
|
||||
.GameVersions = New List(Of String) From {"未知版本"}
|
||||
End If
|
||||
#End Region
|
||||
End If
|
||||
End With
|
||||
Return Result
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 从项目 ID 获取其下的全部版本列表,并获取其前置项目信息。
|
||||
''' </summary>
|
||||
Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms, Optional LoadDependencies As Boolean = True) As List(Of ResourceVersion)
|
||||
If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
|
||||
Dim TargetProject = ResourceProject.FromProjectId(ProjectId, Platform)
|
||||
'获取项目对象的文件列表
|
||||
If Not ProjectFilesCache.ContainsKey(ProjectId) Then
|
||||
Log("[Resource] 开始获取项目版本列表:" & ProjectId)
|
||||
Dim ResultJsonArray As JArray = Nothing
|
||||
If Platform = ResourcePlatforms.CurseForge Then
|
||||
For RetryCount As Integer = 0 To 2
|
||||
Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
|
||||
(10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
|
||||
'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
|
||||
'之前只请求一部分文件的方法备份如下:
|
||||
'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
|
||||
' ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
|
||||
'Else '否则使用全部文件
|
||||
' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
|
||||
'End If
|
||||
If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
|
||||
ResultJsonArray = ResultJson("data")
|
||||
Exit For
|
||||
ElseIf RetryCount < 2 Then
|
||||
Log($"[Resource] CurseForge 返回的资源版本列表存在缺失,即将进行第 {RetryCount + 1} 次重试", NotifyLevel.DebugModeOnly) '#6224
|
||||
Log($"[Resource] 返回的原始内容如下:{vbCrLf}{ResultJson}")
|
||||
Else
|
||||
Log($"[Resource] CurseForge 返回的资源版本列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
|
||||
Throw New Exception("CurseForge 返回的资源版本列表存在缺失")
|
||||
End If
|
||||
Next
|
||||
Else 'Modrinth
|
||||
ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
|
||||
End If
|
||||
ProjectFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) ResourceVersion.FromPlatformJson(a, TargetProject.Types)).
|
||||
Where(Function(a) a.DownloadAvailable).
|
||||
DistinctBy(Function(a) a.Id).ToList 'CurseForge 可能会重复返回相同项(#1330)
|
||||
End If
|
||||
Dim Result As List(Of ResourceVersion) = ProjectFilesCache(ProjectId)
|
||||
If Not LoadDependencies Then Return Result
|
||||
'加载前置信息
|
||||
Dim Dependencies As List(Of String) = Result.SelectMany(Function(f) f.RawDependencies).Distinct().ToList
|
||||
If Dependencies.Any Then
|
||||
Log($"[Resource] 项目 {ProjectId} 中需要加载的前置:{Dependencies.Join(",")}")
|
||||
For Each DependencyProject In ResourceProject.FromProjectIds(Dependencies, TargetProject.Platform)
|
||||
For Each File In Result
|
||||
If File.RawDependencies.Contains(DependencyProject.Id) AndAlso DependencyProject.Id <> ProjectId AndAlso Not File.Dependencies.Contains(DependencyProject.Id) Then
|
||||
File.Dependencies.Add(DependencyProject.Id)
|
||||
End If
|
||||
Next
|
||||
Next
|
||||
End If
|
||||
Return Result
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 项目 ID 对应的版本信息的缓存。不会保存到文件。
|
||||
''' </summary>
|
||||
Protected Friend Shared ProjectFilesCache As New Dictionary(Of String, List(Of ResourceVersion))
|
||||
|
||||
'序列化
|
||||
|
||||
''' <summary>
|
||||
''' 从缓存 JSON 读取字段数据。
|
||||
''' </summary>
|
||||
Public Shared Function FromCacheJson(Data As JObject) As ResourceVersion
|
||||
Dim Result As New ResourceVersion
|
||||
With Result
|
||||
.ResourceType = CType(Data("ResourceType").ToObject(Of Integer), ResourceTypes)
|
||||
.Id = Data("Id").ToString
|
||||
.Display = Data("DisplayName").ToString
|
||||
If Data.ContainsKey("Version") Then .Version = Data("Version").ToString
|
||||
.ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
|
||||
.DownloadCount = Data("DownloadCount").ToObject(Of Integer)
|
||||
.ReleaseType = CType(Data("ReleaseType").ToObject(Of Integer), ReleaseTypes)
|
||||
If Data.ContainsKey("FileName") Then .FileName = Data("FileName").ToString
|
||||
If Data.ContainsKey("DownloadUrls") Then .DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
|
||||
If Data.ContainsKey("ModLoaders") Then .Loaders = Data("ModLoaders").ToObject(Of List(Of ModLoaderTypes))
|
||||
If Data.ContainsKey("Size") Then .Size = Data("Size").ToObject(Of Integer)
|
||||
If Data.ContainsKey("Hash") Then .Hash = Data("Hash").ToString
|
||||
If Data.ContainsKey("GameVersions") Then .GameVersions = Data("GameVersions").ToObject(Of List(Of String))
|
||||
If Data.ContainsKey("RawDependencies") Then .RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
|
||||
If Data.ContainsKey("Dependencies") Then .Dependencies = Data("Dependencies").ToObject(Of List(Of String))
|
||||
End With
|
||||
Return Result
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 转为缓存 JSON。
|
||||
''' </summary>
|
||||
Public Function ToCacheJson() As JObject
|
||||
Dim Result As New JObject
|
||||
With Result
|
||||
.Add("ResourceType", ResourceType)
|
||||
.Add("Id", Id)
|
||||
If Version IsNot Nothing Then .Add("Version", Version)
|
||||
.Add("DisplayName", Display)
|
||||
.Add("ReleaseDate", ReleaseDate)
|
||||
.Add("DownloadCount", DownloadCount)
|
||||
.Add("ModLoaders", New JArray(Loaders.Select(Function(m) CInt(m))))
|
||||
.Add("GameVersions", New JArray(GameVersions))
|
||||
.Add("ReleaseType", CInt(ReleaseType))
|
||||
If FileName IsNot Nothing Then .Add("FileName", FileName)
|
||||
If DownloadUrls IsNot Nothing Then .Add("DownloadUrls", New JArray(DownloadUrls))
|
||||
If Hash IsNot Nothing Then .Add("Hash", Hash)
|
||||
If Size >= 0 Then .Add("Size", Size)
|
||||
.Add("RawDependencies", New JArray(RawDependencies))
|
||||
.Add("Dependencies", New JArray(Dependencies))
|
||||
End With
|
||||
Return Result
|
||||
End Function
|
||||
|
||||
'杂项
|
||||
|
||||
''' <summary>
|
||||
''' 转为列表项。
|
||||
''' </summary>
|
||||
Public Function ToListItem(ClickHandler As MyListItem.ClickEventHandler, Optional SaveAsButtonHandler As MyIconButton.ClickEventHandler = Nothing,
|
||||
Optional IsBadDisplay As Boolean = False) As MyVirtualizingElement(Of MyListItem)
|
||||
Return New MyVirtualizingElement(Of MyListItem)(
|
||||
Function()
|
||||
Dim Result As New MyListItem
|
||||
With Result
|
||||
|
||||
'基础属性
|
||||
.SnapsToDevicePixels = True
|
||||
.Height = 42
|
||||
.Type = MyListItem.CheckType.Clickable
|
||||
.Tag = Me
|
||||
.Title = If(IsBadDisplay, FileName, Display)
|
||||
.Logo = $"{PathImage}ReleaseTypes/{ReleaseType}.png"
|
||||
AddHandler .Click, ClickHandler
|
||||
|
||||
'描述
|
||||
.Info =
|
||||
Iterator Function()
|
||||
If .Title <> FileName.BeforeLast(".") Then Yield FileName.BeforeLast(".")
|
||||
If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
|
||||
If GameVersions.All(Function(v) Not v.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) v.ContainsIgnoreCase(s))) Then Yield $"游戏版本 {GameVersions.Join("、"c)}"
|
||||
If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount)} 次" 'CurseForge 的下载次数经常错误地返回 0
|
||||
Yield $"更新于 {GetTimeSpanString(ReleaseDate - Date.Now, False)}"
|
||||
If ReleaseType <> ReleaseTypes.Release Then Yield ReleaseTypeDisplay
|
||||
End Function().Join(","c)
|
||||
|
||||
'另存为按钮
|
||||
If SaveAsButtonHandler IsNot Nothing Then
|
||||
Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
|
||||
ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
|
||||
ToolTipService.SetVerticalOffset(BtnSave, 30)
|
||||
ToolTipService.SetHorizontalOffset(BtnSave, 2)
|
||||
AddHandler BtnSave.Click, SaveAsButtonHandler
|
||||
.Buttons = {BtnSave}
|
||||
End If
|
||||
|
||||
End With
|
||||
Return Result
|
||||
End Function) With {.Height = 42}
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 获取下载信息。
|
||||
''' </summary>
|
||||
''' <param name="LocalFileOrFolder">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
|
||||
Public Function ToNetFile(LocalFileOrFolder As String) As NetFile
|
||||
Return New NetFile(DownloadUrls, LocalFileOrFolder & If(LocalFileOrFolder.EndsWithF("\"), FileName, ""), New FileChecker(Hash:=Hash, ActualSize:=Size), SimulateBrowserHeaders:=True)
|
||||
End Function
|
||||
''' <summary>
|
||||
''' 重新整理 CurseForge 的下载地址。
|
||||
''' </summary>
|
||||
Public Shared Function ParseCurseForgeDownloadUrls(Url As String) As List(Of String)
|
||||
Return {
|
||||
Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
|
||||
Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
|
||||
Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
|
||||
Url.Replace("://media.", "://edge."),
|
||||
Url
|
||||
}.Distinct.ToList
|
||||
End Function
|
||||
Public Overrides Function ToString() As String
|
||||
Return $"{Id}: {FileName}"
|
||||
End Function
|
||||
|
||||
End Class
|
||||
@@ -0,0 +1,85 @@
|
||||
'获取所有条目:WikiEntry.All
|
||||
|
||||
''' <summary>
|
||||
''' MC 百科条目。
|
||||
''' </summary>
|
||||
Public Class WikiEntry
|
||||
|
||||
'属性
|
||||
|
||||
''' <summary>
|
||||
''' 在 MC 百科中的对应 ID。
|
||||
''' </summary>
|
||||
Public Id As Integer
|
||||
''' <summary>
|
||||
''' 中文译名。空字符串代表没有翻译。
|
||||
''' </summary>
|
||||
Public ChineseName As String = ""
|
||||
''' <summary>
|
||||
''' CurseForge Slug(例如 advanced-solar-panels)。
|
||||
''' </summary>
|
||||
Public CurseForgeSlug As String = Nothing
|
||||
''' <summary>
|
||||
''' Modrinth Slug(例如 advanced-solar-panels)。
|
||||
''' </summary>
|
||||
Public ModrinthSlug As String = Nothing
|
||||
''' <summary>
|
||||
''' MC 百科的浏览量逆序排行,1 代表浏览量最低。
|
||||
''' </summary>
|
||||
Public Popularity As Integer
|
||||
Public Overrides Function ToString() As String
|
||||
Return If(CurseForgeSlug, "") & "&" & If(ModrinthSlug, "") & "|" & Id & "|" & ChineseName & ", Rank " & Popularity
|
||||
End Function
|
||||
|
||||
'读取
|
||||
|
||||
''' <summary>
|
||||
''' 内置数据库中的所有 MC 百科条目。
|
||||
''' </summary>
|
||||
Public Shared ReadOnly Property All As List(Of WikiEntry)
|
||||
Get
|
||||
Static Cache As List(Of WikiEntry) = Nothing
|
||||
If Cache IsNot Nothing Then Return Cache
|
||||
'实际加载
|
||||
Cache = New List(Of WikiEntry)
|
||||
Dim Data As String
|
||||
Using Archive As New ZipArchive(New MemoryStream(GetResources("ModData")), ZipArchiveMode.Read)
|
||||
Data = ReadFile(Archive.GetEntry("moddata.txt").Open(), Encoding.UTF8)
|
||||
End Using
|
||||
Dim i As Integer = 0
|
||||
For Each Line In Data.ReplaceLineEndings(vbLf).Split(vbLf)
|
||||
i += 1
|
||||
If Line = "" Then Continue For
|
||||
For Each EntryData As String In Line.Split("¨")
|
||||
Dim Entry = New WikiEntry
|
||||
Dim Splited = EntryData.Split("|")
|
||||
Entry.Popularity = Val(Splited(0))
|
||||
If Splited(1).StartsWithF("@") Then
|
||||
Entry.CurseForgeSlug = Nothing
|
||||
Entry.ModrinthSlug = Splited(1).Replace("@", "")
|
||||
ElseIf Splited(1).EndsWithF("@") Then
|
||||
Entry.CurseForgeSlug = Splited(1).TrimEnd("@")
|
||||
Entry.ModrinthSlug = Entry.CurseForgeSlug
|
||||
ElseIf Splited(1).Contains("@") Then
|
||||
Entry.CurseForgeSlug = Splited(1).Split("@")(0)
|
||||
Entry.ModrinthSlug = Splited(1).Split("@")(1)
|
||||
Else
|
||||
Entry.CurseForgeSlug = Splited(1)
|
||||
Entry.ModrinthSlug = Nothing
|
||||
End If
|
||||
Entry.Id = i
|
||||
If Splited.Count >= 3 Then
|
||||
Entry.ChineseName = Splited.Last
|
||||
If Entry.ChineseName.Contains("*") Then '处理 *
|
||||
Entry.ChineseName = Entry.ChineseName.Replace("*",
|
||||
$" ({If(Entry.CurseForgeSlug, Entry.ModrinthSlug).Replace("-", " ").Capitalize})")
|
||||
End If
|
||||
End If
|
||||
Cache.Add(Entry)
|
||||
Next
|
||||
Next
|
||||
Return Cache
|
||||
End Get
|
||||
End Property
|
||||
|
||||
End Class
|
||||
Reference in New Issue
Block a user