+ Public Function LastIndexOfF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Integer
+ Return Str.LastIndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
+ End Function
+
'''
''' 输入 And 字符不会报错的 Val。
'''
@@ -1649,14 +1660,14 @@ Re:
Source = Source.ToLower.Replace(" ", "")
Query = Query.ToLower.Replace(" ", "")
Dim sourceLength As Integer = Source.Length, queryLength As Integer = Query.Length '用于计算最后因数的长度缓存
- Do While qp < Query.Length
+ Do While qp < queryLength
'对 qp 作为开始位置计算
Dim sp As Integer = 0, lenMax As Integer = 0, spMax As Integer = 0
'查找以 qp 为头的最大子串
Do While sp < Source.Length
'对每个 sp 作为开始位置计算最大子串
Dim len As Integer = 0
- Do While (qp + len) < Query.Length AndAlso (sp + len) < Source.Length AndAlso Source(sp + len) = Query(qp + len)
+ Do While (qp + len) < queryLength AndAlso (sp + len) < Source.Length AndAlso Source(sp + len) = Query(qp + len)
len += 1
Loop
'存储 len
@@ -1671,7 +1682,7 @@ Re:
Source = Source.Substring(0, spMax) & If(Source.Count > spMax + lenMax, Source.Substring(spMax + lenMax), String.Empty) '将源中的对应字段替换空
'存储 lenSum
Dim IncWeight = (Math.Pow(1.4, 3 + lenMax) - 3.6) '根据长度加成
- IncWeight *= (1 + 0.3 * Math.Max(0, 3 - Math.Abs(qp - spMax))) '根据位置加成
+ IncWeight *= 1 + 0.3 * Math.Max(0, 3 - Math.Abs(qp - spMax)) '根据位置加成
lenSum += IncWeight
End If
'根据结果增加 qp
@@ -1721,23 +1732,24 @@ Re:
Public Function Search(Of T)(Entries As List(Of SearchEntry(Of T)), Query As String, Optional MaxBlurCount As Integer = 5, Optional MinBlurSimilarity As Double = 0.1) As List(Of SearchEntry(Of T))
'初始化
Dim ResultList As New List(Of SearchEntry(Of T))
- If Entries.Count = 0 Then Return ResultList
+ If Not Entries.Any() Then Return ResultList
'进行搜索,获取相似信息
For Each Entry In Entries
Entry.Similarity = SearchSimilarityWeighted(Entry.SearchSource, Query)
Entry.AbsoluteRight = False
For Each Pair In Entry.SearchSource
- If Pair.Key.ToLower.Replace(" ", "").Contains(Query.ToLower.Replace(" ", "")) Then Entry.AbsoluteRight = True
+ If Pair.Key.Replace(" ", "").ContainsF(Query.Replace(" ", ""), True) Then Entry.AbsoluteRight = True
Next
Next
'按照相似度进行排序
- Entries = Sort(Entries, Function(Left As SearchEntry(Of T), Right As SearchEntry(Of T)) As Boolean
- If Left.AbsoluteRight Xor Right.AbsoluteRight Then
- Return Left.AbsoluteRight
- Else
- Return Left.Similarity > Right.Similarity
- End If
- End Function)
+ Entries = Sort(Entries,
+ Function(Left, Right) As Boolean
+ If Left.AbsoluteRight Xor Right.AbsoluteRight Then
+ Return Left.AbsoluteRight
+ Else
+ Return Left.Similarity > Right.Similarity
+ End If
+ End Function)
'返回结果
Dim BlurCount As Integer = 0
For Each Entry In Entries
@@ -2089,7 +2101,7 @@ NextElement:
Dim AllArguments() As String = Command.Split(" ")
For i = 0 To AllArguments.Length - 1
If AllArguments(i) = "-" & Name Then
- If AllArguments.Length = i + 1 OrElse AllArguments(i + 1).StartsWith("-") Then Return True
+ If AllArguments.Length = i + 1 OrElse AllArguments(i + 1).StartsWithF("-") Then Return True
Return AllArguments(i + 1)
End If
Next
@@ -2116,7 +2128,7 @@ NextElement:
'''
Public Sub OpenWebsite(Url As String)
Try
- If Not Url.ToLower.StartsWith("http") Then Throw New Exception(Url & " 不是一个有效的网址,它必须以 http 开头!")
+ If Not Url.StartsWithF("http", True) Then Throw New Exception(Url & " 不是一个有效的网址,它必须以 http 开头!")
Log("[System] 正在打开网页:" & Url)
Process.Start(Url)
Catch ex As Exception
diff --git a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb
index a11be25..f2bf1b4 100644
--- a/Plain Craft Launcher 2/Modules/Base/ModLoader.vb
+++ b/Plain Craft Launcher 2/Modules/Base/ModLoader.vb
@@ -490,7 +490,7 @@
Select Case Loader.State
Case LoadState.Finished
'检查是否需要重启
- If Loader.GetType.Name.StartsWith("LoaderTask") Then '类型名后面带有泛型,必须用 StartsWith
+ If Loader.GetType.Name.StartsWithF("LoaderTask") Then '类型名后面带有泛型,必须用 StartsWith
If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
If ModeDebug Then Log("[Loader] 由于输入条件变更,重启已完成的加载器 " & Loader.Name)
GoTo Restart
@@ -500,7 +500,7 @@
End If
Case LoadState.Loading
'检查是否需要重启
- If Loader.GetType.Name.StartsWith("LoaderTask") Then
+ 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
If ModeDebug Then Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, LogLevel.Developer)
GoTo Restart
@@ -517,9 +517,9 @@ Restart:
If Input IsNot Nothing Then
'若输入类型与下一个加载器相同才继续
Dim LoaderType As String = Loader.GetType.Name
- If LoaderType.StartsWith("LoaderTask") OrElse LoaderType.StartsWith("LoaderCombo") Then
+ If LoaderType.StartsWithF("LoaderTask") OrElse LoaderType.StartsWithF("LoaderCombo") Then
Loader.Start(If(Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IsForceRestarting)
- ElseIf LoaderType.StartsWith("LoaderDownload") Then
+ ElseIf LoaderType.StartsWithF("LoaderDownload") Then
Loader.Start(If(TypeOf Input Is List(Of NetFile), Input, Nothing), IsForceRestarting)
Else
Throw New Exception("未知的加载器类型(" & LoaderType & ")")
@@ -545,7 +545,7 @@ Restart:
Public Shared Sub GetLoaderList(Loader As Object, ByRef List As List(Of LoaderBase), Optional RequireShow As Boolean = True)
For Each SubLoader In Loader.Loaders
If SubLoader.Show OrElse Not RequireShow Then List.Add(SubLoader)
- If SubLoader.GetType.Name.StartsWith("LoaderCombo") Then GetLoaderList(SubLoader, List)
+ If SubLoader.GetType.Name.StartsWithF("LoaderCombo") Then GetLoaderList(SubLoader, List)
Next
End Sub
'''
@@ -606,7 +606,7 @@ Restart:
End If
RunInUi(Sub() FrmMain.BtnExtraDownload.Progress = LoaderTaskbarProgress)
'更新任务栏信息
- If LoaderTaskbar.Count = 0 OrElse LoaderTaskbarProgress = 1 Then
+ If Not LoaderTaskbar.Any() OrElse LoaderTaskbarProgress = 1 Then
NewState = Shell.TaskbarItemProgressState.None
ElseIf LoaderTaskbarProgress < 0.015 Then
NewState = Shell.TaskbarItemProgressState.Indeterminate
diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb
index 8084128..5bdf83a 100644
--- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb
+++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb
@@ -189,7 +189,7 @@ RequestFinished:
Dim Request As HttpWebRequest = WebRequest.Create(Url)
Dim Result As New List(Of Byte)
Try
- If Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then Request.ProtocolVersion = HttpVersion.Version11
+ If Url.StartsWithF("https", True) Then Request.ProtocolVersion = HttpVersion.Version11
Request.Timeout = Timeout
Request.Accept = Accept
SecretHeadersSign(Url, Request, UseBrowserUserAgent)
@@ -397,7 +397,7 @@ RequestFinished:
Req.ContentType = ContentType
Req.Timeout = Timeout
SecretHeadersSign(Url, Req, UseBrowserUserAgent)
- If Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then Req.ProtocolVersion = HttpVersion.Version11
+ If Url.StartsWithF("https", True) Then Req.ProtocolVersion = HttpVersion.Version11
If Method = "POST" OrElse Method = "PUT" Then
Req.ContentLength = SendData.Length
DataStream = Req.GetRequestStream()
@@ -897,7 +897,7 @@ RequestFinished:
''' 新建一个需要下载的文件。
'''
''' 包含文件名的本地地址。
- Public Sub New(Urls As String(), LocalPath As String, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False)
+ Public Sub New(Urls As IEnumerable(Of String), LocalPath As String, Optional Check As FileChecker = Nothing, Optional UseBrowserUserAgent As Boolean = False)
Dim Sources As New List(Of NetSource)
Dim Count As Integer = 0
Urls = Urls.Distinct.ToArray
@@ -987,7 +987,7 @@ StartThread:
'构建线程
Dim ThreadUuid As Integer = GetUuid()
SyncLock LockTasks
- If Tasks.Count = 0 Then Return False '由于中断,已没有可用任务
+ If Not Tasks.Any() Then Return False '由于中断,已没有可用任务
Th = New Thread(AddressOf Thread) With {.Name = "NetTask " & Tasks(0).Uuid & "/" & Uuid & " Download " & ThreadUuid & "#", .Priority = ThreadPriority.BelowNormal}
End SyncLock
ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload}
@@ -1035,7 +1035,7 @@ StartThread:
If SourcesOnce.Contains(Info.Source) AndAlso Not Info.Equals(Info.Source.Thread) Then GoTo SourceBreak
'请求头
HttpRequest = WebRequest.Create(Info.Source.Url)
- If Info.Source.Url.StartsWith("https", StringComparison.OrdinalIgnoreCase) Then HttpRequest.ProtocolVersion = HttpVersion.Version11
+ If Info.Source.Url.StartsWithF("https", True) Then HttpRequest.ProtocolVersion = HttpVersion.Version11
'HttpRequest.Proxy = Nothing 'new WebProxy(Ip, Port)
HttpRequest.Timeout = Timeout
HttpRequest.AddRange(Info.DownloadStart)
@@ -1067,11 +1067,11 @@ StartThread:
If ThreadFileSize > 50 * 1024 * 1024 Then
For Each Drive As DriveInfo In DriveInfo.GetDrives
Dim DriveName As String = Drive.Name.First.ToString
- Dim RequiredSpace = If(PathTemp.StartsWith(DriveName), ThreadFileSize * 1.1, 0) +
- If(LocalPath.StartsWith(DriveName), ThreadFileSize + 5 * 1024 * 1024, 0)
+ Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ThreadFileSize * 1.1, 0) +
+ If(LocalPath.StartsWithF(DriveName), ThreadFileSize + 5 * 1024 * 1024, 0)
If Drive.TotalFreeSpace < RequiredSpace Then
Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
- If(PathTemp.StartsWith(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
+ If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
End If
Next
End If
@@ -1465,7 +1465,7 @@ Retry:
Public Overrides Property Progress As Double
Get
If State >= LoadState.Finished Then Return 1
- If Files.Count = 0 Then Return 0 '必须返回 0,否则在获取列表的时候会错觉已经下载完了
+ If Not Files.Any() Then Return 0 '必须返回 0,否则在获取列表的时候会错觉已经下载完了
Return _Progress
End Get
Set(value As Double)
@@ -1559,21 +1559,21 @@ NextElement:
RunInNewThread(Sub()
Try
'输入检测
- If Files.Count = 0 Then
+ If Not Files.Any() Then
OnFinish()
Exit Sub
End If
For Each File As NetFile In Files
If File Is Nothing Then Throw New ArgumentException("存在空文件请求!")
For Each Source As NetSource In File.Sources
- If Not (Source.Url.ToLower.StartsWith("https://") OrElse Source.Url.ToLower.StartsWith("http://")) Then
+ If Not (Source.Url.StartsWithF("https://", True) OrElse Source.Url.StartsWithF("http://", True)) Then
Source.Ex = New ArgumentException("输入的下载链接不正确!")
Source.IsFailed = True
End If
Next
If File.IsSourceFailed() Then Throw New ArgumentException("输入的下载链接不正确!")
If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确!")
- If File.LocalPath.EndsWith("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!")
+ If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!")
'文件夹检测
Dim DirPath As String = New FileInfo(File.LocalPath).Directory.FullName
If Not Directory.Exists(DirPath) Then Directory.CreateDirectory(DirPath)
@@ -1626,7 +1626,7 @@ NextElement:
For Each File As NetFile In Files
Dim ExistFilePath As String = Nothing
'判断是否有已存在的文件
- If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWith(PathMcFolder) Then
+ If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then
Dim Relative = File.LocalPath.Replace(PathMcFolder, "")
For Each Folder In FolderList
Dim Target = Folder & Relative
@@ -1699,7 +1699,7 @@ Retry:
Public Sub OnFail(ExList As List(Of Exception))
SyncLock LockState
If State > LoadState.Loading Then Exit Sub
- If ExList Is Nothing OrElse ExList.Count = 0 Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
+ If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
'寻找第一个不是 404 的下载源
Dim UsefulExs = ExList.Where(Function(e) Not e.Message.Contains("(404)")).ToList
[Error] = If(UsefulExs.Count > 0, UsefulExs(0), ExList(0))
diff --git a/Plain Craft Launcher 2/Modules/Base/ModValidate.vb b/Plain Craft Launcher 2/Modules/Base/ModValidate.vb
index aeb4192..a89dcc3 100644
--- a/Plain Craft Launcher 2/Modules/Base/ModValidate.vb
+++ b/Plain Craft Launcher 2/Modules/Base/ModValidate.vb
@@ -76,7 +76,7 @@ Public Class ValidateHttp
Public Sub New()
End Sub '用于 XAML 初始化
Public Overrides Function Validate(Str As String) As String
- If Str.EndsWith("/") Then Str = Str.Substring(0, Str.Length - 1)
+ If Str.EndsWithF("/") Then Str = Str.Substring(0, Str.Length - 1)
If Not RegexCheck(Str, "^(http[s]?)\://") Then Return "输入的网址无效!"
Return ""
End Function
@@ -149,7 +149,7 @@ Public Class ValidateExcept
End Sub
Public Overrides Function Validate(Str As String) As String
For Each Ch As String In Excepts
- If Str.IndexOf(Ch, StringComparison.OrdinalIgnoreCase) >= 0 Then
+ If Str.IndexOfF(Ch, StringComparison.OrdinalIgnoreCase) >= 0 Then
If IsNothing(ErrorMessage) Then ErrorMessage = ""
Return ErrorMessage.Replace("%", Ch)
End If
@@ -220,13 +220,13 @@ Public Class ValidateFolderName
Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
If Not LengthCheck = "" Then Return LengthCheck
'检查空格
- If Str.StartsWith(" ") Then Return "文件夹名不能以空格开头!"
- If Str.EndsWith(" ") Then Return "文件夹名不能以空格结尾!"
+ If Str.StartsWithF(" ") Then Return "文件夹名不能以空格开头!"
+ If Str.EndsWithF(" ") Then Return "文件夹名不能以空格结尾!"
'检查长度
LengthCheck = New ValidateLength(1, 100).Validate(Str)
If Not LengthCheck = "" Then Return LengthCheck
'检查尾部小数点
- If Str.EndsWith(".") Then Return "文件夹名不能以小数点结尾!"
+ If Str.EndsWithF(".") Then Return "文件夹名不能以小数点结尾!"
'检查特殊字符
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
If Not CharactCheck = "" Then Return CharactCheck
@@ -273,13 +273,13 @@ Public Class ValidateFileName
Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
If Not LengthCheck = "" Then Return LengthCheck
'检查空格
- If Str.StartsWith(" ") Then Return "文件名不能以空格开头!"
- If Str.EndsWith(" ") Then Return "文件名不能以空格结尾!"
+ If Str.StartsWithF(" ") Then Return "文件名不能以空格开头!"
+ If Str.EndsWithF(" ") Then Return "文件名不能以空格结尾!"
'检查长度
LengthCheck = New ValidateLength(1, 253).Validate(Str & If(ParentFolder, ""))
If Not LengthCheck = "" Then Return LengthCheck
'检查尾部小数点
- If Str.EndsWith(".") Then Return "文件名不能以小数点结尾!"
+ If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
'检查特殊字符
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件名不可包含 % 字符!").Validate(Str)
If Not CharactCheck = "" Then Return CharactCheck
@@ -319,7 +319,7 @@ Public Class ValidateFolderPath
Public Overrides Function Validate(Str As String) As String
'去除尾部斜线,统一为 \
Str = Str.Replace("/", "\")
- If Not Str.TrimEnd("\").EndsWith(":") Then Str = Str.TrimEnd("\")
+ If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
'检查是否为空
Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
If Not LengthCheck = "" Then Return LengthCheck
@@ -327,15 +327,15 @@ Public Class ValidateFolderPath
LengthCheck = New ValidateLength(1, 254).Validate(Str)
If Not LengthCheck = "" Then Return LengthCheck
'检查开头
- If Str.StartsWith("\\Mac\") Then GoTo Fin
+ If Str.StartsWithF("\\Mac\") Then GoTo Fin
For Each Drive As DriveInfo In My.Computer.FileSystem.Drives
If Str.ToUpper = Drive.Name Then Return ""
- If Str.ToUpper.StartsWith(Drive.Name) Then GoTo Fin
+ If Str.StartsWithF(Drive.Name, True) Then GoTo Fin
Next
Return "文件夹路径头存在错误!"
Fin:
'对首层以外的路径检查
- For i = If(Str.StartsWith("\\Mac\"), 2, 1) To Str.Split("\").Count - 1
+ For i = If(Str.StartsWithF("\\Mac\"), 2, 1) To Str.Split("\").Count - 1
Dim SubStr As String = Str.Split("\")(i)
'检查是否为空
Dim SubLengthCheck As String = New ValidateNullOrWhiteSpace().Validate(SubStr)
@@ -344,10 +344,10 @@ Fin:
Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
If Not CharactCheck = "" Then Return CharactCheck
'检查头部空格
- If SubStr.StartsWith(" ") Then Return "文件夹名不能以空格开头!"
- If SubStr.EndsWith(" ") Then Return "文件夹名不能以空格结尾!"
+ If SubStr.StartsWithF(" ") Then Return "文件夹名不能以空格开头!"
+ If SubStr.EndsWithF(" ") Then Return "文件夹名不能以空格结尾!"
'检查尾部小数点
- If SubStr.EndsWith(".") Then Return "文件夹名不能以小数点结尾!"
+ If SubStr.EndsWithF(".") Then Return "文件夹名不能以小数点结尾!"
'检查特殊字符串
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"}, "文件夹名不可为 %!").Validate(SubStr)
If Not InvalidStrCheck = "" Then Return InvalidStrCheck
diff --git a/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb b/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
index a5b9a33..0b074e6 100644
--- a/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
+++ b/Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
@@ -55,7 +55,7 @@ Public Class MyBitmap
Public Sub New(FilePathOrResourceName As String)
Try
FilePathOrResourceName = FilePathOrResourceName.Replace("pack://application:,,,/images/", PathImage)
- If FilePathOrResourceName.StartsWith(PathImage) Then
+ If FilePathOrResourceName.StartsWithF(PathImage) Then
'使用缓存
If BitmapCache.ContainsKey(FilePathOrResourceName) Then
Pic = BitmapCache(FilePathOrResourceName).Pic
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
index 72fc6a7..cc112ad 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
@@ -18,6 +18,7 @@
'https://docs.curseforge.com/?http#tocS_ModLoaderType
Any = 0
Forge = 1
+ LiteLoader = 3
Fabric = 4
Quilt = 5
NeoForge = 6
@@ -43,10 +44,10 @@
For Each EntryData As String In Line.Split("¨")
Dim Entry = New CompDatabaseEntry
Dim SplitedLine = EntryData.Split("|")
- If SplitedLine(0).StartsWith("@") Then
+ If SplitedLine(0).StartsWithF("@") Then
Entry.CurseForgeSlug = Nothing
Entry.ModrinthSlug = SplitedLine(0).Replace("@", "")
- ElseIf SplitedLine(0).EndsWith("@") Then
+ ElseIf SplitedLine(0).EndsWithF("@") Then
Entry.CurseForgeSlug = SplitedLine(0).TrimEnd("@")
Entry.ModrinthSlug = Entry.CurseForgeSlug
ElseIf SplitedLine(0).Contains("@") Then
@@ -247,12 +248,26 @@
LastUpdate = Data("dateReleased") '#1194
DownloadCount = Data("downloadCount")
If Data("logo").Count > 0 Then LogoUrl = Data("logo")("thumbnailUrl")
- 'FileIndexes / GameVersions
- Dim Files = If(CType(Data("latestFilesIndexes"), JArray), New JArray).
- Where(Function(i) i("gameVersion").ToString.Contains("1."))
- CurseForgeFileIds = Files.Select(Of Integer)(Function(v) v("fileId")).Distinct.ToList
- GameVersions = Files.Select(Of Integer)(Function(v) Val(v("gameVersion").ToString.Split(".")(1).Split("-").First)).Where(Function(v) v > 0).
- Distinct.OrderByDescending(Function(v) v).ToList
+ 'FileIndexes / GameVersions / ModLoaders
+ ModLoaders = New List(Of CompModLoaderType)
+ 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 As New CompFile(File, Type)
+ If Not NewFile.Available Then Continue For
+ ModLoaders.AddRange(NewFile.ModLoaders)
+ Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
+ If Not GameVersions.Any(Function(v) v.StartsWithF("1.")) 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 File("gameVersion").ToString.StartsWithF("1.") 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
+ GameVersions = Files.SelectMany(Function(f) f.Value).Where(Function(v) v.StartsWithF("1.")).
+ Select(Function(v) CInt(Val(v.Split(".")(1).Split("-").First))).Where(Function(v) v > 0).
+ Distinct.OrderByDescending(Function(v) v).ToList
+ ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
'Type
If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
Type = CompType.Mod
@@ -309,15 +324,7 @@
'FUTURE: Res
End Select
Next
- If Tags.Count = 0 Then Tags.Add("杂项")
- 'ModLoaders
- ModLoaders = New List(Of CompModLoaderType)
- For Each File In If(Data("latestFiles"), {})
- Dim NewFile As New CompFile(File, Type)
- If Not NewFile.Available Then Continue For
- ModLoaders.AddRange(NewFile.ModLoaders)
- Next
- ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
+ If Not Tags.Any() Then Tags.Add("杂项")
#End Region
Else
#Region "Modrinth"
@@ -334,7 +341,7 @@
'GameVersions
'搜索结果的键为 versions,获取特定工程的键为 game_versions
GameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).
- Select(Function(v) v.ToString).Where(Function(v) v.StartsWith("1.")).
+ Select(Function(v) v.ToString).Where(Function(v) v.StartsWithF("1.")).
Select(Of Integer)(Function(v) Val(v.Split(".")(1).Split("-").First)).Where(Function(v) v > 0).
Distinct.OrderByDescending(Function(v) v).ToList
'Type
@@ -383,7 +390,7 @@
'FUTURE: Res
End Select
Next
- If Tags.Count = 0 Then Tags.Add("杂项")
+ If Not Tags.Any() Then Tags.Add("杂项")
Tags.Sort()
ModLoaders.Sort()
#End Region
@@ -562,7 +569,7 @@
End If
Next
'根据分类后的结果处理
- If NormalNameList.Count = 0 OrElse SubtitleList.Count = 0 Then GoTo NoSubtitle
+ If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
'同时包含 NormalName 和 Subtitle
Title = NormalNameList.Join(" - ")
Else
@@ -683,8 +690,8 @@ NoSubtitle:
'''
Public ReadOnly Property CanContinue As Boolean
Get
- If Tag.StartsWith("/") OrElse Not Source.HasFlag(CompSourceType.CurseForge) Then Storage.CurseForgeTotal = 0
- If Tag.EndsWith("/") OrElse Not Source.HasFlag(CompSourceType.Modrinth) Then Storage.ModrinthTotal = 0
+ If Tag.StartsWithF("/") OrElse Not Source.HasFlag(CompSourceType.CurseForge) Then Storage.CurseForgeTotal = 0
+ If Tag.EndsWithF("/") OrElse Not Source.HasFlag(CompSourceType.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
@@ -732,7 +739,7 @@ NoSubtitle:
'''
Public Function GetCurseForgeAddress() As String
If Not Source.HasFlag(CompSourceType.CurseForge) Then Return Nothing
- If Tag.StartsWith("/") Then Storage.CurseForgeTotal = 0
+ If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
If Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset Then Return Nothing
'应用筛选参数
Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={CompPageSize}"
@@ -756,7 +763,7 @@ NoSubtitle:
'''
Public Function GetModrinthAddress() As String
If Not Source.HasFlag(CompSourceType.Modrinth) Then Return Nothing
- If Tag.EndsWith("/") Then Storage.ModrinthTotal = 0
+ If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
If Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset Then Return Nothing
'应用筛选参数
Dim Address As String = $"https://api.modrinth.com/v2/search?limit={CompPageSize}&index=relevance"
@@ -828,7 +835,7 @@ NoSubtitle:
Log($"[Comp] 已有 {Task.Input.Storage.Results.Count} 个结果,多于所需的 {Task.Input.TargetResultCount} 个结果,结束处理")
Exit Sub
ElseIf Not Task.Input.CanContinue Then
- If Task.Input.Storage.Results.Count = 0 Then
+ If Not Task.Input.Storage.Results.Any() Then
Throw New Exception("没有符合条件的结果")
Else
Log($"[Comp] 已有 {Task.Input.Storage.Results.Count} 个结果,少于所需的 {Task.Input.TargetResultCount} 个结果,但无法继续获取,结束处理")
@@ -867,7 +874,7 @@ NoSubtitle:
Next
'获取搜索结果
Dim SearchResults = Search(SearchEntries, Task.Input.SearchText, 3)
- If SearchResults.Count = 0 Then Throw New Exception("无搜索结果,请尝试搜索英文名称")
+ If Not SearchResults.Any() Then Throw New Exception("无搜索结果,请尝试搜索英文名称")
Dim SearchResult As String = ""
For i = 0 To Math.Min(4, SearchResults.Count - 1) '就算全是准确的,也最多只要 5 个
If Not SearchResults(i).AbsoluteRight AndAlso i >= Math.Min(2, SearchResults.Count - 1) Then Exit For '把 3 个结果拼合以提高准确度
@@ -880,7 +887,7 @@ NoSubtitle:
'去除常见连接词
Dim RealFilter As String = ""
For Each Word In SearchResult.Split(" ")
- If {"the", "of", "a", "mod", "and"}.Contains(Word.ToLower) OrElse Val(Word) > 0 Then Continue For
+ If {"the", "of", "a", "mod", "and"}.Contains(Word.ToLowerInvariant) OrElse Val(Word) > 0 Then Continue For
If SearchResult.Split(" ").Count > 3 AndAlso {"ftb"}.Contains(Word.ToLower) Then Continue For
RealFilter += Word.TrimStart("{[(").TrimEnd("}])") & " "
Next
@@ -904,15 +911,15 @@ NoSubtitle:
End If
RightKeywords.Add(Keyword)
Next
- If RawFilter.Length > 0 AndAlso RightKeywords.Count = 0 Then
+ If RawFilter.Length > 0 AndAlso Not RightKeywords.Any() Then
Task.Input.SearchText = RawFilter '全都被过滤掉了
Else
Task.Input.SearchText = Join(RightKeywords.Distinct.ToList, " ").ToLower
End If
'例外项:OptiForge、OptiFabric(拆词后因为包含 Forge/Fabric 导致无法搜到实际的 Mod)
- If RawFilter.Replace(" ", "").ToLower.Contains("optiforge") Then Task.Input.SearchText = "optiforge"
- If RawFilter.Replace(" ", "").ToLower.Contains("optifabric") Then Task.Input.SearchText = "optifabric"
+ If RawFilter.Replace(" ", "").ContainsF("optiforge", True) Then Task.Input.SearchText = "optiforge"
+ If RawFilter.Replace(" ", "").ContainsF("optifabric", True) Then Task.Input.SearchText = "optifabric"
Log("[Comp] 工程列表搜索最终文本:" & Task.Input.SearchText, LogLevel.Debug)
Task.Progress = 0.1
@@ -1003,15 +1010,15 @@ Retry:
'确保存在结果
Storage.ErrorMessage = Nothing
- If RawResults.Count = 0 Then
+ If Not RawResults.Any() Then
If [Error] IsNot Nothing Then
Throw [Error]
Else
If IsChineseSearch AndAlso Task.Input.Type <> CompType.Mod Then
Throw New Exception($"{If(Task.Input.Type = CompType.ModPack, "整合包", "资源包")}搜索仅支持英文")
- ElseIf Task.Input.Source = CompSourceType.CurseForge AndAlso Task.Input.Tag.StartsWith("/") Then
+ ElseIf Task.Input.Source = CompSourceType.CurseForge AndAlso Task.Input.Tag.StartsWithF("/") Then
Throw New Exception("CurseForge 不兼容所选的类型")
- ElseIf Task.Input.Source = CompSourceType.Modrinth AndAlso Task.Input.Tag.EndsWith("/") Then
+ ElseIf Task.Input.Source = CompSourceType.Modrinth AndAlso Task.Input.Tag.EndsWithF("/") Then
Throw New Exception("Modrinth 不兼容所选的类型")
Else
Throw New Exception("没有符合条件的结果")
@@ -1171,7 +1178,7 @@ Retry:
'''
Public ReadOnly Property Available As Boolean
Get
- Return FileName IsNot Nothing
+ Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
End Get
End Property
'''
@@ -1181,7 +1188,11 @@ Retry:
'''
''' 文件所有可能的下载源。
'''
- Private ReadOnly DownloadAddress As String()
+ Public DownloadUrls As List(Of String)
+ '''
+ ''' 文件的 SHA1 或 MD5。
+ '''
+ Public ReadOnly Hash As String = Nothing
'''
''' 该文件的所有依赖工程的原始 ID。
''' 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
@@ -1196,7 +1207,7 @@ Retry:
'''
''' 目标本地文件夹,或完整的文件路径。会自动判断类型。
Public Function ToNetFile(LocalAddress As String) As NetFile
- Return New NetFile(DownloadAddress, LocalAddress & If(LocalAddress.EndsWith("\"), FileName, ""), UseBrowserUserAgent:=True)
+ Return New NetFile(DownloadUrls, LocalAddress & If(LocalAddress.EndsWithF("\"), FileName, ""), New FileChecker(Hash:=Hash), UseBrowserUserAgent:=True)
End Function
'实例化
@@ -1206,94 +1217,136 @@ Retry:
'''
Public Sub New(Data As JObject, Type As CompType)
Me.Type = Type
- FromCurseForge = Data.ContainsKey("gameId")
- If FromCurseForge Then
-#Region "CurseForge"
- '简单信息
- Id = Data("id")
- DisplayName = Data("displayName").ToString.Replace(" ", "").Trim(" ")
- ReleaseDate = Data("fileDate")
- Status = CType(Data("releaseType").ToObject(Of Integer), CompFileStatus)
- DownloadCount = Data("downloadCount")
- FileName = Data("fileName")
- 'DownloadAddress
- Dim Url = Data("downloadUrl").ToString
- If Url = "" Then Url = $"https://media.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}"
- Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码
- DownloadAddress = {Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"),
- Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
- Url.Replace("://edge", "://media"),
- Url}.Distinct.ToArray
- 'Dependencies
- If Type = CompType.Mod 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.ToLower).ToList
- GameVersions = RawVersions.Where(Function(v) v.StartsWith("1.")).Select(Function(v) v.Replace("-snapshot", " 快照")).ToList
- If GameVersions.Count > 1 Then
- GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
- If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
- ElseIf GameVersions.Count = 1 Then
- GameVersions = GameVersions.ToList
- Else
- GameVersions = New List(Of String) From {"未知版本"}
- End If
- 'ModLoaders
- ModLoaders = New List(Of CompModLoaderType)
- If RawVersions.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
- If RawVersions.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
- If RawVersions.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
- If RawVersions.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge)
+ If Data.ContainsKey("FromCurseForge") Then
+#Region "CompJson"
+ FromCurseForge = Data("FromCurseForge").ToObject(Of Boolean)
+ Id = Data("Id").ToString
+ DisplayName = Data("DisplayName").ToString
+ ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
+ DownloadCount = Data("DownloadCount").ToObject(Of Integer)
+ Status = CType(Data("Status").ToObject(Of Integer), CompFileStatus)
+ 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 ModLoaders = Data("ModLoaders").ToObject(Of List(Of CompModLoaderType))
+ 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 Region
Else
-#Region "Modrinth"
- '简单信息
- Id = Data("id")
- DisplayName = Data("name").ToString.Replace(" ", "").Trim(" ")
- ReleaseDate = Data("date_published")
- Status = If(Data("version_type").ToString = "release", CompFileStatus.Release, If(Data("version_type").ToString = "beta", CompFileStatus.Beta, CompFileStatus.Alpha))
- DownloadCount = Data("downloads")
- '可能为空
- If CType(Data("files"), JArray).Count > 0 Then
- FileName = Data("files")(0)("filename")
- DownloadAddress = {Data("files")(0)("url").ToString.Replace(FileName, FileName)}
- End If
- 'Dependencies
- If Type = CompType.Mod 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.ToLower).ToList
- GameVersions = RawVersions.Where(Function(v) v.StartsWith("1.") OrElse v.StartsWith("b1.")).
- Select(Function(v) If(v.Contains("-"), v.Split("-").First & " 快照", If(v.StartsWith("b1."), "远古版本", v))).ToList
- If GameVersions.Count > 1 Then
- GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
- If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
- ElseIf GameVersions.Count = 1 Then
- '无需处理
- ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")) Then
- GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")).ToList
- Else
- GameVersions = New List(Of String) From {"未知版本"}
- End If
- 'ModLoaders
- Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
- ModLoaders = New List(Of CompModLoaderType)
- If RawLoaders.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
- If RawLoaders.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
- If RawLoaders.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
+ FromCurseForge = Data.ContainsKey("gameId")
+ If FromCurseForge Then
+#Region "CurseForge"
+ '简单信息
+ Id = Data("id")
+ DisplayName = Data("displayName").ToString.Replace(" ", "").Trim(" ")
+ ReleaseDate = Data("fileDate")
+ Status = CType(Data("releaseType").ToObject(Of Integer), CompFileStatus)
+ DownloadCount = Data("downloadCount")
+ FileName = Data("fileName")
+ 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://media.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}"
+ Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码
+ DownloadUrls = (New List(Of String) From {Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"),
+ Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
+ Url.Replace("://edge", "://media"),
+ Url}).Distinct().ToList()
+ 'Dependencies
+ If Type = CompType.Mod 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.ToLower).ToList
+ GameVersions = RawVersions.Where(Function(v) v.StartsWithF("1.")).Select(Function(v) v.Replace("-snapshot", " 快照")).ToList
+ If GameVersions.Count > 1 Then
+ GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
+ If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
+ ElseIf GameVersions.Count = 1 Then
+ GameVersions = GameVersions.ToList
+ Else
+ GameVersions = New List(Of String) From {"未知版本"}
+ End If
+ 'ModLoaders
+ ModLoaders = New List(Of CompModLoaderType)
+ If RawVersions.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
+ If RawVersions.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
+ If RawVersions.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
+ If RawVersions.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge)
#End Region
+ Else
+#Region "Modrinth"
+ '简单信息
+ Id = Data("id")
+ DisplayName = Data("name").ToString.Replace(" ", "").Trim(" ")
+ ReleaseDate = Data("date_published")
+ Status = If(Data("version_type").ToString = "release", CompFileStatus.Release, If(Data("version_type").ToString = "beta", CompFileStatus.Beta, CompFileStatus.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")}
+ Hash = File("hashes")("sha1")
+ End If
+ 'Dependencies
+ If Type = CompType.Mod 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.ToLower).ToList
+ GameVersions = RawVersions.Where(Function(v) v.StartsWithF("1.") OrElse v.StartsWithF("b1.")).
+ Select(Function(v) If(v.Contains("-"), v.Split("-").First & " 快照", If(v.StartsWithF("b1."), "远古版本", v))).ToList
+ If GameVersions.Count > 1 Then
+ GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
+ If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
+ ElseIf GameVersions.Count = 1 Then
+ '无需处理
+ ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")) Then
+ GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")).ToList
+ Else
+ GameVersions = New List(Of String) From {"未知版本"}
+ End If
+ 'ModLoaders
+ Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
+ ModLoaders = New List(Of CompModLoaderType)
+ If RawLoaders.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
+ If RawLoaders.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
+ If RawLoaders.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
+#End Region
+ End If
End If
End Sub
'''
+ ''' 将当前实例转为可用于保存缓存的 Json。
+ '''
+ Public Function ToJson() As JObject
+ Dim Json As New JObject
+ Json.Add("FromCurseForge", FromCurseForge)
+ Json.Add("Id", Id)
+ Json.Add("DisplayName", DisplayName)
+ Json.Add("ReleaseDate", ReleaseDate)
+ Json.Add("DownloadCount", DownloadCount)
+ Json.Add("ModLoaders", New JArray(ModLoaders.Select(Function(m) CInt(m))))
+ Json.Add("GameVersions", New JArray(GameVersions))
+ Json.Add("Status", CInt(Status))
+ If FileName IsNot Nothing Then Json.Add("FileName", FileName)
+ If DownloadUrls IsNot Nothing Then Json.Add("DownloadUrls", New JArray(DownloadUrls))
+ If Hash IsNot Nothing Then Json.Add("Hash", Hash)
+ Json.Add("RawDependencies", New JArray(RawDependencies))
+ Json.Add("Dependencies", New JArray(Dependencies))
+ Return Json
+ End Function
+ '''
''' 将当前文件信息实例化为控件。
'''
Public Function ToListItem(OnClick As MyListItem.ClickEventHandler, Optional OnSaveClick As MyIconButton.ClickEventHandler = Nothing,
@@ -1434,7 +1487,7 @@ Retry:
'如果为整合包就不会有 Dependencies 信息,所以不用管
Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
Deps.Sort()
- If Deps.Count = 0 Then Exit Sub
+ If Not Deps.Any() Then Exit Sub
Deps = Deps.Where(Function(dep)
If Not CompProjectCache.ContainsKey(dep) Then Log($"[Comp] 未找到 ID {dep} 的前置 Mod 信息", LogLevel.Debug)
Return CompProjectCache.ContainsKey(dep)
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
index bf48d19..1655874 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
@@ -80,7 +80,7 @@
Log(ex, "确认崩溃日志时间失败(" & LogFile & ")")
End Try
Next
- If RightLogs.Count = 0 Then Log("[Crash] 未发现可能可用的日志文件")
+ If Not RightLogs.Any() Then Log("[Crash] 未发现可能可用的日志文件")
'将可能可用的日志文件导出
For Each FilePath In RightLogs
@@ -114,7 +114,7 @@
Try
Dim Info As New FileInfo(FilePath)
If Not Info.Exists OrElse Info.Length = 0 Then Exit Try
- If Not FilePath.ToLower.EndsWith(".jar") AndAlso ExtractFile(FilePath, TempFolder & "Temp\") Then
+ If Not FilePath.EndsWithF(".jar", True) AndAlso ExtractFile(FilePath, TempFolder & "Temp\") Then
'解压成功
Log("[Crash] 已解压导入的日志文件:" & FilePath)
Else
@@ -132,7 +132,7 @@
If Not TargetFile.Exists OrElse TargetFile.Length = 0 Then Continue For
Dim Ext As String = TargetFile.Extension.ToLower
If Ext = ".log" OrElse Ext = ".txt" Then
- If TargetFile.Name.StartsWith("crash-") Then
+ If TargetFile.Name.StartsWithF("crash-") Then
AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TargetFile.FullName, ReadFile(TargetFile.FullName).Split(vbCrLf.ToCharArray)))
Else
AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TargetFile.FullName, ReadFile(TargetFile.FullName, Encoding.UTF8).Split(vbCrLf.ToCharArray)))
@@ -169,10 +169,10 @@
For Each LogFile In AnalyzeRawFiles
Dim MatchName As String = GetFileNameFromPath(LogFile.Key).ToLower
Dim TargetType As AnalyzeFileType
- If MatchName.StartsWith("hs_err") Then
+ If MatchName.StartsWithF("hs_err") Then
TargetType = AnalyzeFileType.HsErr
DirectFile = LogFile
- ElseIf MatchName.StartsWith("crash-") Then
+ ElseIf MatchName.StartsWithF("crash-") Then
TargetType = AnalyzeFileType.CrashReport
DirectFile = LogFile
ElseIf MatchName = "latest.log" OrElse MatchName = "latest log.txt" OrElse
@@ -187,13 +187,13 @@
Else
TargetType = AnalyzeFileType.ExtraLog
End If
- ElseIf MatchName.EndsWith(".log") OrElse MatchName.EndsWith(".txt") Then
+ ElseIf MatchName.EndsWithF(".log", True) OrElse MatchName.EndsWithF(".txt", True) Then
TargetType = AnalyzeFileType.ExtraLog
Else
Log("[Crash] " & MatchName & " 分类为 Ignore")
Continue For
End If
- If LogFile.Value.Count = 0 Then
+ If Not LogFile.Value.Any() Then
Log("[Crash] " & MatchName & " 由于内容为空跳过")
Else
TotalFiles.Add(New KeyValuePair(Of AnalyzeFileType, KeyValuePair(Of String, String()))(TargetType, LogFile))
@@ -208,7 +208,7 @@
For Each File In TotalFiles
If SelectType = File.Key Then SelectedFiles.Add(File.Value)
Next
- If SelectedFiles.Count = 0 Then Continue For
+ If Not SelectedFiles.Any() Then Continue For
Try
'根据文件类别判断
Select Case SelectType
@@ -437,7 +437,7 @@
'输出到日志
Done:
- If CrashReasons.Count = 0 Then
+ If Not CrashReasons.Any() Then
Log("[Crash] 步骤 3:分析崩溃原因完成,未找到可能的原因")
Else
Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " & CrashReasons.Count & " 条可能的原因")
@@ -645,14 +645,14 @@ Done:
"org.lwjgl", "com.sun", "net.minecraftforge", "paulscode.sound", "com.mojang", "net.minecraft", "cpw.mods", "com.google", "org.apache", "org.spongepowered", "net.fabricmc", "com.mumfrey",
"com.electronwill.nightconfig", "it.unimi.dsi",
"MojangTricksIntelDriversForPerformance_javaw"}
- If Stack.StartsWith(IgnoreStack) Then GoTo NextStack
+ If Stack.StartsWithF(IgnoreStack) Then GoTo NextStack
Next
PossibleStacks.Add(Stack.Trim) '.Split("$").First)
NextStack:
Next
PossibleStacks = PossibleStacks.Distinct.ToList
Log("[Crash] 找到 " & PossibleStacks.Count & " 条可能的堆栈信息")
- If PossibleStacks.Count = 0 Then Return New List(Of String)
+ If Not PossibleStacks.Any() Then Return New List(Of String)
For Each Stack As String In PossibleStacks
Log("[Crash] - " & Stack)
Next
@@ -663,7 +663,7 @@ NextStack:
Dim Splited = Stack.Split(".")
For i = 0 To Math.Min(3, Splited.Count - 1) '最多取前 4 节
Dim Word As String = Splited(i)
- If Word.Length <= 2 OrElse Word.StartsWith("func_") Then Continue For
+ If Word.Length <= 2 OrElse Word.StartsWithF("func_") Then Continue For
If {"com", "org", "net", "asm", "fml", "mod", "jar", "sun", "lib", "map", "gui", "dev", "nio", "api", "dsi", "top",
"core", "init", "mods", "main", "file", "game", "load", "read", "done", "util", "tile", "item", "base", "oshi",
"forge", "setup", "block", "model", "mixin", "event", "unimi", "netty",
@@ -716,8 +716,8 @@ NextStack:
'[Fabric] 获取所有包含 Mod 信息的行
Dim ModNameLines As New List(Of String)
For Each Line In Details.Split(vbLf)
- If Line.ToLower.Contains(".jar") OrElse
- (IsFabricDetail AndAlso Line.StartsWith(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
+ If Line.ContainsF(".jar", True) OrElse
+ (IsFabricDetail AndAlso Line.StartsWithF(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
Next
Log("[Crash] 崩溃报告中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
@@ -782,7 +782,7 @@ NextStack:
'输出
ModFileNames = ModFileNames.Distinct.ToList
- If ModFileNames.Count = 0 Then
+ If Not ModFileNames.Any() Then
Return Nothing
Else
Log("[Crash] 找到 " & ModFileNames.Count & " 个可能的崩溃 Mod 文件名")
@@ -866,7 +866,7 @@ NextStack:
Private Function GetAnalyzeResult(IsHandAnalyze As Boolean) As String
'没有结果的处理
- If CrashReasons.Count = 0 Then
+ If Not CrashReasons.Any() Then
If IsHandAnalyze Then
Return "很抱歉,PCL 无法确定错误原因。"
Else
@@ -1034,7 +1034,7 @@ NextStack:
Replace("\e", If(IsHandAnalyze, "", vbCrLf & "你可以查看错误报告了解错误具体是如何发生的。")).
Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf).
Trim(vbCrLf.ToCharArray) &
- If(Not Results.Any(Function(r) r.EndsWith("\h")) OrElse IsHandAnalyze, "",
+ If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
vbCrLf & "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。" &
If(If(PageSetupSystem.IsLauncherNewest(), True), "",
vbCrLf & vbCrLf & "此外,你正在使用老版本 PCL,更新到最新版可能会修复这个问题。" & vbCrLf & "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。"))
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
index e41a940..1419ced 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
@@ -57,9 +57,9 @@
#Region "下载支持库文件"
Dim LoadersLib As New List(Of LoaderBase) From {
- New LoaderTask(Of String, List(Of NetFile))("分析缺失支持库文件", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(Version)) With {.ProgressWeight = 1},
- New LoaderDownload("下载支持库文件", New List(Of NetFile)) With {.ProgressWeight = 15}
- }
+ New LoaderTask(Of String, List(Of NetFile))("分析缺失支持库文件", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(Version)) With {.ProgressWeight = 1},
+ New LoaderDownload("下载支持库文件", New List(Of NetFile)) With {.ProgressWeight = 15}
+ }
'构造加载器
Loaders.Add(New LoaderCombo(Of String)("下载支持库文件(主加载器)", LoadersLib) With {.Block = False, .Show = False, .ProgressWeight = 16})
#End Region
@@ -71,19 +71,19 @@
Dim LoadersAssets As New List(Of LoaderBase)
'获取资源文件索引地址
LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析资源文件索引地址",
- Sub(Task As LoaderTask(Of String, List(Of NetFile)))
- Try
- Dim IndexFile = DlClientAssetIndexGet(Version)
- Dim IndexFileInfo As New FileInfo(IndexFile.LocalPath)
- If AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
- Task.Output = New List(Of NetFile)
- Else
- Task.Output = New List(Of NetFile) From {IndexFile}
- End If
- Catch ex As Exception
- Throw New Exception("分析资源文件索引地址失败", ex)
- End Try
- End Sub) With {.ProgressWeight = 0.5, .Show = False})
+ Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+ Try
+ Dim IndexFile = DlClientAssetIndexGet(Version)
+ Dim IndexFileInfo As New FileInfo(IndexFile.LocalPath)
+ If AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
+ Task.Output = New List(Of NetFile)
+ Else
+ Task.Output = New List(Of NetFile) From {IndexFile}
+ End If
+ Catch ex As Exception
+ Throw New Exception("分析资源文件索引地址失败", ex)
+ End Try
+ End Sub) With {.ProgressWeight = 0.5, .Show = False})
'下载资源文件索引
LoadersAssets.Add(New LoaderDownload("下载资源文件索引", New List(Of NetFile)) With {.ProgressWeight = 2})
'要求独立更新索引
@@ -91,27 +91,29 @@
Dim LoadersAssetsUpdate As New List(Of LoaderBase)
Dim TempAddress As String = Nothing
Dim RealAddress As String = Nothing
- LoadersAssetsUpdate.Add(New LoaderTask(Of String, List(Of NetFile))("后台分析资源文件索引地址", Sub(Task As LoaderTask(Of String, List(Of NetFile)))
- Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Version)
- RealAddress = BackAssetsFile.LocalPath
- TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
- BackAssetsFile.LocalPath = TempAddress
- Task.Output = New List(Of NetFile) From {BackAssetsFile}
- End Sub))
+ LoadersAssetsUpdate.Add(New LoaderTask(Of String, List(Of NetFile))("后台分析资源文件索引地址",
+ Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+ Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Version)
+ RealAddress = BackAssetsFile.LocalPath
+ TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
+ BackAssetsFile.LocalPath = TempAddress
+ Task.Output = New List(Of NetFile) From {BackAssetsFile}
+ End Sub))
LoadersAssetsUpdate.Add(New LoaderDownload("后台下载资源文件索引", New List(Of NetFile)))
- LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引", Sub(Task As LoaderTask(Of List(Of NetFile), String))
- CopyFile(TempAddress, RealAddress)
- McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
- End Sub))
+ LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引",
+ Sub(Task As LoaderTask(Of List(Of NetFile), String))
+ CopyFile(TempAddress, RealAddress)
+ McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
+ End Sub))
Dim Updater As New LoaderCombo(Of String)("后台更新资源文件索引", LoadersAssetsUpdate)
Log("[Download] 开始后台更新资源文件索引")
Updater.Start()
End If
'获取资源文件地址
LoadersAssets.Add(New LoaderTask(Of String, List(Of NetFile))("分析缺失资源文件",
- Sub(Task As LoaderTask(Of String, List(Of NetFile)))
- Task.Output = McAssetsFixList(McAssetsGetIndexName(Version), CheckAssetsHash, Task)
- End Sub) With {.ProgressWeight = 3})
+ Sub(Task As LoaderTask(Of String, List(Of NetFile)))
+ Task.Output = McAssetsFixList(McAssetsGetIndexName(Version), CheckAssetsHash, Task)
+ End Sub) With {.ProgressWeight = 3})
'下载资源文件
LoadersAssets.Add(New LoaderDownload("下载资源文件", New List(Of NetFile)) With {.ProgressWeight = 25})
'构造加载器
@@ -248,7 +250,7 @@
End Select
'确认版本格式标准
Id = Id.Replace("_", "-") '1.7.10_pre4 在版本列表中显示为 1.7.10-pre4
- If Id <> "1.0" AndAlso Id.EndsWith(".0") Then Id = Left(Id, Id.Length - 2) 'OptiFine 1.8 的下载会触发此问题,显示版本为 1.8.0
+ If Id <> "1.0" AndAlso Id.EndsWithF(".0") Then Id = Left(Id, Id.Length - 2) 'OptiFine 1.8 的下载会触发此问题,显示版本为 1.8.0
'查找版本并开始
For Each Version As JObject In DlClientListLoader.Output.Value("versions")
If Version("id") = Id Then
@@ -306,7 +308,7 @@
Return _inherit
End Get
Set(value As String)
- If value.EndsWith(".0") Then value = Left(value, value.Length - 2)
+ If value.EndsWithF(".0") Then value = Left(value, value.Length - 2)
_inherit = value
End Set
End Property
@@ -366,9 +368,9 @@
Dim Entry As New DlOptiFineListEntry With {
.NameDisplay = Name(i).Replace("HD U ", "").Replace(".0 ", " "),
.ReleaseTime = Join({ReleaseTime(i).Split(".")(2), ReleaseTime(i).Split(".")(1), ReleaseTime(i).Split(".")(0)}, "/"),
- .IsPreview = Name(i).ToLower.Contains("pre"),
+ .IsPreview = Name(i).ContainsF("pre", True),
.Inherit = Name(i).ToString.Split(" ")(0),
- .NameFile = If(Name(i).ToLower.Contains("pre"), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
+ .NameFile = If(Name(i).ContainsF("pre", True), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
.RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
Entry.NameVersion = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
@@ -389,7 +391,7 @@
Dim Entry As New DlOptiFineListEntry With {
.NameDisplay = (Token("mcversion").ToString & Token("type").ToString.Replace("HD_U", "").Replace("_", " ") & " " & Token("patch").ToString).Replace(".0 ", " "),
.ReleaseTime = "",
- .IsPreview = Token("patch").ToString.ToLower.Contains("pre"),
+ .IsPreview = Token("patch").ToString.ContainsF("pre", True),
.Inherit = Token("mcversion").ToString,
.NameFile = Token("filename").ToString,
.RequiredForgeVersion = If(Token("forge"), "").ToString.Replace("Forge ", "").Replace("#", "")
@@ -601,7 +603,7 @@
Dim Versions As New List(Of DlForgeVersionEntry)
Try
'分割版本信息
- Dim VersionCodes = Mid(Result, 1, Result.LastIndexOf("")).Split("| ")).Split(" | )[^<]+")
Category = "installer"
ElseIf VersionCode.Contains("classifier-universal""") Then
'类型为 universal.zip,支持范围 751~449 (1.6.1 部分), 682~183 (1.5.1 ~ 1.3.2 部分)
- VersionCode = VersionCode.Substring(VersionCode.IndexOf("universal.zip"))
+ VersionCode = VersionCode.Substring(VersionCode.IndexOfF("universal.zip"))
MD5 = RegexSeek(VersionCode, "(?<=MD5: )[^<]+")
Category = "universal"
ElseIf VersionCode.Contains("client.zip") Then
'类型为 client.zip,支持范围 182~ (1.3.2 部分 ~)
- VersionCode = VersionCode.Substring(VersionCode.IndexOf("client.zip"))
+ VersionCode = VersionCode.Substring(VersionCode.IndexOfF("client.zip"))
MD5 = RegexSeek(VersionCode, "(?<=MD5: )[^<]+")
Category = "client"
Else
@@ -652,7 +654,7 @@
Catch ex As Exception
Throw New Exception("版本列表解析失败(" & Result & ")", ex)
End Try
- If Versions.Count = 0 Then Throw New Exception("没有可用版本")
+ If Not Versions.Any() Then Throw New Exception("没有可用版本")
Loader.Output = Versions
End Sub
@@ -708,7 +710,7 @@
Catch ex As Exception
Throw New Exception("版本列表解析失败(" & Json.ToString & ")", ex)
End Try
- If Versions.Count = 0 Then Throw New Exception("没有可用版本")
+ If Not Versions.Any() Then Throw New Exception("没有可用版本")
Loader.Output = Versions
End Sub
@@ -799,7 +801,7 @@
Dim Json As JObject = Result("versions")
Dim Versions As New List(Of DlLiteLoaderListEntry)
For Each Pair As KeyValuePair(Of String, JToken) In Json
- If Pair.Key.StartsWith("1.6") OrElse Pair.Key.StartsWith("1.5") Then Continue For
+ If Pair.Key.StartsWithF("1.6") OrElse Pair.Key.StartsWithF("1.5") Then Continue For
Dim RealEntry As JToken = If(Pair.Value("artefacts"), Pair.Value("snapshots"))("com.mumfrey:liteloader")("latest")
Versions.Add(New DlLiteLoaderListEntry With {
.Inherit = Pair.Key,
@@ -827,7 +829,7 @@
Dim Json As JObject = Result("versions")
Dim Versions As New List(Of DlLiteLoaderListEntry)
For Each Pair As KeyValuePair(Of String, JToken) In Json
- If Pair.Key.StartsWith("1.6") OrElse Pair.Key.StartsWith("1.5") Then Continue For
+ If Pair.Key.StartsWithF("1.6") OrElse Pair.Key.StartsWithF("1.5") Then Continue For
Dim RealEntry As JToken = If(Pair.Value("artefacts"), Pair.Value("snapshots"))("com.mumfrey:liteloader")("latest")
Versions.Add(New DlLiteLoaderListEntry With {
.Inherit = Pair.Key,
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
index abd3dfc..9a725c5 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
@@ -62,7 +62,7 @@
Public ReadOnly Property HasEnvironment As Boolean
Get
If PathFolder Is Nothing OrElse PathEnv Is Nothing Then Return False
- Return PathEnv.Replace("\", "").Replace("/", "").ToLower.Contains(PathFolder.Replace("\", "").ToLower)
+ Return PathEnv.Replace("\", "").Replace("/", "").ContainsF(PathFolder.Replace("\", ""), True)
End Get
End Property
@@ -78,7 +78,7 @@
'''
Public Overrides Function ToString() As String
Dim VersionString = Version.ToString
- If VersionString.StartsWith("1.") Then VersionString = Mid(VersionString, 3)
+ If VersionString.StartsWithF("1.") Then VersionString = Mid(VersionString, 3)
Return If(IsJre, "JRE ", "JDK ") & VersionCode & " (" & VersionString & ")," & If(Is64Bit, "64", "32") & " 位" & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
End Function
@@ -87,7 +87,7 @@
''' 输入 javaw.exe 文件所在文件夹的路径,不限制结尾。
'''
Public Sub New(Folder As String, IsUserImport As Boolean)
- If Not Folder.EndsWith("\") Then Folder += "\"
+ If Not Folder.EndsWithF("\") Then Folder += "\"
PathFolder = Folder.Replace("/", "\")
Me.IsUserImport = IsUserImport
End Sub
@@ -116,8 +116,9 @@
If Output.Contains("/lib/ext exists") Then Throw New ApplicationException("无法运行该 Java,请在删除 Java 文件夹中的 /lib/ext 文件夹后再试")
'获取详细信息
Dim VersionString = If(RegexSeek(Output, "(?<=version "")[^""]+"), If(RegexSeek(Output, "(?<=openjdk )[0-9]+"), "")).Replace("_", ".").Split("-").First
+ If VersionString.Split(".").Count > 4 Then VersionString = VersionString.Replace(".0.", ".") '#3493,VersionString = "21.0.2.0.2"
Do While VersionString.Split(".").Count < 4
- If VersionString.StartsWith("1.") Then
+ If VersionString.StartsWithF("1.") Then
VersionString = VersionString & ".0"
Else
VersionString = "1." & VersionString
@@ -184,7 +185,7 @@
JavaList.Add(JavaEntry.FromJson(JsonEntry))
Next
End If
- If JavaList.Count = 0 Then
+ If Not JavaList.Any() Then
Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", LogLevel.Developer)
JavaSearchLoader.Start(0)
Else
@@ -232,7 +233,7 @@
Dim UserJava As JavaEntry = Nothing
'获取版本独立设置中指定的 Java
- If RelatedVersion IsNot Nothing AndAlso Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion).ToString.StartsWith("{") Then
+ If RelatedVersion IsNot Nothing AndAlso Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion).ToString.StartsWithF("{") Then
Try
UserJava = JavaEntry.FromJson(GetJson(Setup.Get("VersionArgumentJavaSelect", Version:=RelatedVersion)))
UserJava.Check()
@@ -291,7 +292,7 @@ RetryGet:
Next
'若未找到适合的 Java,尝试触发搜索
- If AllowedJavaList.Count = 0 AndAlso JavaSearchLoader.State = LoadState.Waiting Then
+ If Not AllowedJavaList.Any() AndAlso JavaSearchLoader.State = LoadState.Waiting Then
Log("[Java] 未找到满足条件的 Java,尝试进行搜索")
JavaSearchLoader.Start(IsForceRestart:=True)
GoTo RetryGet
@@ -352,7 +353,7 @@ ExitUserJavaCheck:
#End Region
'若依然未找到适合的 Java,直接返回
- If AllowedJavaList.Count = 0 Then Return Nothing
+ If Not AllowedJavaList.Any() Then Return Nothing
'优先使用特定目录下的 Java
For Each Java In AllowedJavaList
@@ -445,11 +446,11 @@ NoUserJava:
Dim ProgramPathParent As String, MinecraftPathParent As String = ""
ProgramPathParent = If(New DirectoryInfo(Path).Parent, New DirectoryInfo(Path)).FullName
If PathMcFolder <> "" Then MinecraftPathParent = If(New DirectoryInfo(PathMcFolder).Parent, New DirectoryInfo(PathMcFolder)).FullName
- If Left.PathFolder.StartsWith(ProgramPathParent) AndAlso Not Right.PathFolder.StartsWith(ProgramPathParent) Then Return True
- If Not Left.PathFolder.StartsWith(ProgramPathParent) AndAlso Right.PathFolder.StartsWith(ProgramPathParent) Then Return False
+ If Left.PathFolder.StartsWithF(ProgramPathParent) AndAlso Not Right.PathFolder.StartsWithF(ProgramPathParent) Then Return True
+ If Not Left.PathFolder.StartsWithF(ProgramPathParent) AndAlso Right.PathFolder.StartsWithF(ProgramPathParent) Then Return False
If PathMcFolder <> "" Then
- If Left.PathFolder.StartsWith(MinecraftPathParent) AndAlso Not Right.PathFolder.StartsWith(MinecraftPathParent) Then Return True
- If Not Left.PathFolder.StartsWith(MinecraftPathParent) AndAlso Right.PathFolder.StartsWith(MinecraftPathParent) Then Return False
+ If Left.PathFolder.StartsWithF(MinecraftPathParent) AndAlso Not Right.PathFolder.StartsWithF(MinecraftPathParent) Then Return True
+ If Not Left.PathFolder.StartsWithF(MinecraftPathParent) AndAlso Right.PathFolder.StartsWithF(MinecraftPathParent) Then Return False
End If
'2. 尽量使用 64 位
If Left.Is64Bit AndAlso Not Right.Is64Bit Then Return True
@@ -459,7 +460,7 @@ NoUserJava:
If Not Left.IsJre AndAlso Right.IsJre Then Return False
'4. Java 大版本
If Left.VersionCode <> Right.VersionCode Then
- ' Java 7 8 9 10 11 12 13 14 15 16 17 18 19 20...
+ ' Java 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21...
Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 30, 10, 12, 15, 13, 9, 8, 7, 11, 31, 29, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28}
Return Weight.ElementAtOrDefault(Left.VersionCode) >= Weight.ElementAtOrDefault(Right.VersionCode)
End If
@@ -500,7 +501,7 @@ NoUserJava:
For Each PathInEnv As String In Split((PathEnv & ";" & PathJavaHome).Replace("\\", "\").Replace("/", "\"), ";")
PathInEnv = PathInEnv.Trim(" """.ToCharArray())
If PathInEnv = "" Then Continue For
- If Not PathInEnv.EndsWith("\") Then PathInEnv += "\"
+ If Not PathInEnv.EndsWithF("\") Then PathInEnv += "\"
'粗略检查有效性
If File.Exists(PathInEnv & "javaw.exe") Then JavaPreList(PathInEnv) = False
Next
@@ -652,7 +653,7 @@ Wait:
'确认目录存在
If Not OriginalPath.Exists Then Exit Sub
Dim Path As String = OriginalPath.FullName.Replace("\\", "\")
- If Not Path.EndsWith("\") Then Path += "\"
+ If Not Path.EndsWithF("\") Then Path += "\"
'若该目录有 Java,则加入结果
If File.Exists(Path & "javaw.exe") Then Results(Path) = Source
'查找其下的所有文件夹
@@ -715,7 +716,7 @@ Wait:
End Function
'''
- ''' 获取下载 Java 8/14/17 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
+ ''' 获取下载 Java 8/14/17/21 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
'''
Public Function JavaFixLoaders(Version As Integer) As LoaderCombo(Of Integer)
Return New LoaderCombo(Of Integer)($"下载 Java {Version}", {
@@ -734,7 +735,7 @@ Wait:
Dim MainEntry As JObject = CType(GetJson(IndexFileStr), JObject)($"windows-x{If(Is32BitSystem, "86", "64")}")
Dim Entries = MainEntry.Children.Reverse. '选择最靠后的一项(最新)
SelectMany(Function(e As JProperty) CType(e.Value, JArray).Select(Function(v) New KeyValuePair(Of String, JObject)(e.Name, v)))
- Dim TargetEntry = Entries.First(Function(t) t.Value("version")("name").ToString.StartsWith(Loader.Input))
+ Dim TargetEntry = Entries.First(Function(t) t.Value("version")("name").ToString.StartsWithF(Loader.Input))
Dim Address As String = TargetEntry.Value("manifest")("url")
Log($"[Java] 准备下载 Java {TargetEntry.Value("version")("name")}({TargetEntry.Key}):{Address}")
'获取文件列表
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
index 998c88a..383eb9a 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
@@ -143,7 +143,7 @@ Public Module ModLaunch
Catch ex As Exception
Dim CurrentEx = ex
NextInner:
- If CurrentEx.Message.StartsWith("$") Then
+ If CurrentEx.Message.StartsWithF("$") Then
'若有以 $ 开头的错误信息,则以此为准显示提示
'若错误信息为 $$,则不提示
If Not CurrentEx.Message = "$$" Then MyMsgBox(CurrentEx.Message.TrimStart("$"), If(Loader.Input.SaveBatch Is Nothing, "启动失败", "导出启动脚本失败"))
@@ -752,7 +752,7 @@ LoginFinish:
End Select
ElseIf AllMessage.Contains("超时") OrElse AllMessage.Contains("imeout") OrElse AllMessage.Contains("网络请求失败") Then
Throw New Exception("$登录失败:连接登录服务器超时。" & vbCrLf & "请检查你的网络状况是否良好,或尝试使用 VPN!")
- ElseIf ex.Message.StartsWith("$") Then
+ ElseIf ex.Message.StartsWithF("$") Then
Throw
Else
Throw New Exception("登录失败:" & ex.Message, ex)
@@ -990,13 +990,13 @@ SystemBrowser:
Case 1
'Steve
Do Until McSkinSex(Uuid) = "Steve"
- If Uuid.EndsWith("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
+ If Uuid.EndsWithF("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
Uuid = Mid(Uuid, 1, 32 - 5) & (Long.Parse(Right(Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
Loop
Case 2
'Alex
Do Until McSkinSex(Uuid) = "Alex"
- If Uuid.EndsWith("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
+ If Uuid.EndsWithF("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
Uuid = Mid(Uuid, 1, 32 - 5) & (Long.Parse(Right(Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
Loop
Case 3
@@ -1013,7 +1013,7 @@ SystemBrowser:
Case 4
'自定义
Do Until McSkinSex(Uuid) = If(Setup.Get("LaunchSkinSlim"), "Alex", "Steve")
- If Uuid.EndsWith("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
+ If Uuid.EndsWithF("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
Uuid = Mid(Uuid, 1, 32 - 5) & (Long.Parse(Right(Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
Loop
End Select
@@ -1057,7 +1057,12 @@ SystemBrowser:
Dim MinVer As New Version(0, 0, 0, 0), MaxVer As New Version(999, 999, 999, 999)
'MC 大版本检测
- If (McVersionCurrent.ReleaseTime >= New Date(2021, 11, 16) AndAlso McVersionCurrent.Version.McCodeMain = 99) OrElse
+ If (McVersionCurrent.ReleaseTime >= New Date(2024, 4, 2) AndAlso McVersionCurrent.Version.McCodeMain = 99) OrElse
+ (McVersionCurrent.Version.McCodeMain > 20 AndAlso McVersionCurrent.Version.McCodeMain <> 99) OrElse
+ (McVersionCurrent.Version.McCodeMain = 20 AndAlso McVersionCurrent.Version.McCodeSub >= 5) Then
+ '1.20.5+(24w14a+):至少 Java 21
+ MinVer = New Version(1, 21, 0, 0)
+ ElseIf (McVersionCurrent.ReleaseTime >= New Date(2021, 11, 16) AndAlso McVersionCurrent.Version.McCodeMain = 99) OrElse
(McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain <> 99) Then
'1.18 pre2+:至少 Java 17
MinVer = New Version(1, 17, 0, 0)
@@ -1140,7 +1145,10 @@ SystemBrowser:
If Task.IsAborted Then Exit Sub '中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java
McLaunchLog("无合适的 Java,需要确认是否自动下载")
Dim JavaCode As String
- If MinVer >= New Version(1, 9, 0, 0) Then
+ If MinVer >= New Version(1, 21, 0, 0) Then
+ JavaCode = 21
+ If Not JavaDownloadConfirm("Java 21") Then Throw New Exception("$$")
+ ElseIf MinVer >= New Version(1, 9, 0, 0) Then
JavaCode = 17
If Not JavaDownloadConfirm("Java 17") Then Throw New Exception("$$")
ElseIf MaxVer < New Version(1, 8, 0, 0) Then
@@ -1394,9 +1402,9 @@ NextVersion:
Dim DeDuplicateDataList As New List(Of String)
For i = 0 To DataList.Count - 1
Dim CurrentEntry As String = DataList(i)
- If DataList(i).StartsWith("-") Then
+ If DataList(i).StartsWithF("-") Then
Do While i < DataList.Count - 1
- If DataList(i + 1).StartsWith("-") Then
+ If DataList(i + 1).StartsWithF("-") Then
Exit Do
Else
i += 1
@@ -1487,9 +1495,9 @@ NextVersion:
Dim DeDuplicateDataList As New List(Of String)
For i = 0 To DataList.Count - 1
Dim CurrentEntry As String = DataList(i)
- If DataList(i).StartsWith("-") Then
+ If DataList(i).StartsWithF("-") Then
Do While i < DataList.Count - 1
- If DataList(i + 1).StartsWith("-") Then
+ If DataList(i + 1).StartsWithF("-") Then
Exit Do
Else
i += 1
@@ -1595,7 +1603,7 @@ NextVersion:
End Try
For Each Entry In Zip.Entries
Dim FileName As String = Entry.FullName
- If FileName.EndsWith(".dll") Then
+ If FileName.EndsWithF(".dll", True) Then
'实际解压文件的步骤
Dim FilePath As String = McVersionCurrent.Path & McVersionCurrent.Name & "-natives\" & FileName
ExistFiles.Add(FilePath)
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
index 1f0f2cf..1f25858 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
@@ -65,7 +65,7 @@
'格式:TMZ 12>C://xxx/xx/|Test>D://xxx/xx/|名称>路径
For Each Folder As String In Setup.Get("LaunchFolders").Split("|")
If Folder = "" Then Continue For
- If Not Folder.Contains(">") OrElse Not Folder.EndsWith("\") Then
+ If Not Folder.Contains(">") OrElse Not Folder.EndsWithF("\") Then
Hint("无效的 Minecraft 文件夹:" & Folder, HintType.Critical)
Continue For
End If
@@ -92,13 +92,13 @@
For Each Folder As McFolder In CacheMcFolderList
If Not Folder.Type = McFolderType.Original Then NewSetup.Add(Folder.Name & ">" & Folder.Path)
Next
- If NewSetup.Count = 0 Then NewSetup.Add("") '防止 0 元素 Join 返回 Nothing
+ If Not NewSetup.Any() Then NewSetup.Add("") '防止 0 元素 Join 返回 Nothing
Setup.Set("LaunchFolders", Join(NewSetup, "|"))
#End Region
'若没有可用文件夹,则创建 .minecraft
- If CacheMcFolderList.Count = 0 Then
+ If Not CacheMcFolderList.Any() Then
Directory.CreateDirectory(Path & ".minecraft\versions\")
CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Path = Path & ".minecraft\", .Type = McFolderType.Original})
End If
@@ -149,7 +149,7 @@
#Region "版本处理"
- Public Const McVersionCacheVersion As Integer = 26
+ Public Const McVersionCacheVersion As Integer = 27
Private _McVersionCurrent As McVersion
Private _McVersionLast = 0 '为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
@@ -418,7 +418,7 @@
End Try
VersionSearchFinish:
'获取版本号
- If _Version.McName.StartsWith("1.") Then
+ If _Version.McName.StartsWithF("1.") Then
Dim SplitVersion = _Version.McName.Split(" "c, "_"c, "-"c, "."c)
Dim SplitResult As String
'分割获取信息
@@ -546,8 +546,8 @@ Recheck:
'处理 JumpLoader
If Text.Contains("minecraftforge") AndAlso File.Exists(PathIndie & "config\jumploader.json") Then
For Each ModFile In Directory.EnumerateFiles(PathIndie & "mods")
- Dim FileName As String = GetFileNameFromPath(ModFile).ToLower
- If FileName.EndsWith(".jar") AndAlso FileName.Contains("jumploader") Then
+ Dim FileName As String = GetFileNameFromPath(ModFile)
+ If FileName.EndsWithF(".jar", True) AndAlso FileName.ContainsF("jumploader", True) Then
Log("[Minecraft] 发现 JumpLoader 分支项:" & FileName)
HasJumpLoader = True
Exit For
@@ -612,7 +612,7 @@ Recheck:
Public Sub New(Path As String)
Me.Path = If(Path.Contains(":"), "", PathMcFolder & "versions\") & '补全完整路径
Path &
- If(Path.EndsWith("\"), "", "\") '补全右划线
+ If(Path.EndsWithF("\"), "", "\") '补全右划线
End Sub
'''
@@ -684,7 +684,7 @@ Recheck:
'愚人节与快照版本
If If(JsonObject("type"), "").ToString = "fool" OrElse GetMcFoolName(Version.McName) <> "" Then
State = McVersionState.Fool
- ElseIf Version.McName.ToLower.Contains("w") OrElse Name.ToLower.Contains("combat") OrElse Version.McName.ToLower.Contains("rc") OrElse Version.McName.ToLower.Contains("pre") OrElse Version.McName.Contains("experimental") OrElse If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending" Then
+ ElseIf Version.McName.ContainsF("w", True) OrElse Name.ContainsF("combat", True) OrElse Version.McName.ContainsF("rc", True) OrElse Version.McName.ContainsF("pre", True) OrElse Version.McName.ContainsF("experimental", True) OrElse If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending" Then
State = McVersionState.Snapshot
End If
'OptiFine
@@ -744,9 +744,9 @@ ExitDataLoad:
If CustomInfo = "" Then
Select Case State
Case McVersionState.Snapshot
- If Version.McName.ToLower.Contains("pre") Then
+ If Version.McName.ContainsF("pre", True) Then
Info = "预发布版 " & Version.McName
- ElseIf Version.McName.ToLower.Contains("rc") Then
+ ElseIf Version.McName.ContainsF("rc", True) Then
Info = "发布候选 " & Version.McName
ElseIf Version.McName.Contains("experimental") OrElse Version.McName = "pending" Then
Info = "实验性快照"
@@ -948,8 +948,8 @@ ExitDataLoad:
(Asc(CType(Left(OptiFineVersion.ToUpper, 1), Char)) - Asc("A"c) + 1) * 10000 + '第二段:字母编号(2 位),如 G2 中的 G(7)
Val(RegexSeek(Right(OptiFineVersion, OptiFineVersion.Length - 1), "[0-9]+")) * 100 '第三段:末尾数字(2 位),如 C5 beta4 中的 5
'第三段:测试标记
- If OptiFineVersion.ToLower.Contains("pre") Then _SortCode += 50
- If OptiFineVersion.ToLower.Contains("pre") OrElse OptiFineVersion.ToLower.Contains("beta") Then
+ If OptiFineVersion.ContainsF("pre", True) Then _SortCode += 50
+ If OptiFineVersion.ContainsF("pre", True) OrElse OptiFineVersion.ContainsF("beta", True) Then
If Val(Right(OptiFineVersion, 1)) = 0 AndAlso Right(OptiFineVersion, 1) <> "0" Then
_SortCode += 1 '为 pre 或 beta 结尾,视作 1
Else
@@ -981,9 +981,9 @@ ExitDataLoad:
'''
Public Function GetMcFoolName(Name As String) As String
Name = Name.ToLower
- If Name.StartsWith("2.0") Then
+ If Name.StartsWithF("2.0") Then
Return "这个秘密计划了两年的更新将游戏推向了一个新高度!"
- ElseIf Name.StartsWith("20w14inf") OrElse Name = "20w14∞" Then
+ ElseIf Name.StartsWithF("20w14inf") OrElse Name = "20w14∞" Then
Return "我们加入了 20 亿个新的维度,让无限的想象变成了现实!"
ElseIf Name = "15w14a" Then
Return "作为一款全年龄向的游戏,我们需要和平,需要爱与拥抱。"
@@ -994,9 +994,9 @@ ExitDataLoad:
ElseIf Name = "22w13oneblockatatime" Then
Return "一次一个方块更新!迎接全新的挖掘、合成与骑乘玩法吧!"
ElseIf Name = "23w13a_or_b" Then
- Return "研究表明玩家喜欢作出选择——越多越好!"
+ Return "研究表明:玩家喜欢作出选择——越多越好!"
ElseIf Name = "24w14potato" Then
- Return "我们的有毒块茎一直被忽视和低估,于是我们加强了毒马铃薯!"
+ Return "毒马铃薯一直都被大家忽视和低估,于是我们超级加强了它!"
Else
Return ""
End If
@@ -1020,6 +1020,12 @@ ExitDataLoad:
'''
Public McVersionListLoader As New LoaderTask(Of String, Integer)("Minecraft Version List", AddressOf McVersionListLoad) With {.ReloadTimeout = 1}
+ '''
+ ''' 是否为本次打开 PCL 后第一次加载版本列表。
+ ''' 这会清理所有 .pclignore 文件,而非跳过这些对应版本。
+ '''
+ Private IsFirstMcVersionListLoad As Boolean = True
+
'''
''' 开始加载当前 Minecraft 文件夹的版本列表。
'''
@@ -1042,7 +1048,7 @@ ExitDataLoad:
End Try
End If
'没有可用版本
- If FolderList.Count = 0 Then
+ If Not FolderList.Any() Then
WriteIni(Path & "PCL.ini", "VersionCache", "") '清空缓存
GoTo OnLoaded
End If
@@ -1064,6 +1070,7 @@ Reload:
WriteIni(Path & "PCL.ini", "VersionCache", FolderListCheck)
McVersionList = McVersionListLoadNoCache(Path)
End If
+ IsFirstMcVersionListLoad = False
If Loader.IsAborted Then Exit Sub
'改变当前选择的版本
@@ -1116,14 +1123,20 @@ OnLoaded:
'循环读取版本
For Each Folder As String In ReadIni(Path & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
If Folder = "" Then Continue For
- If File.Exists(Path & "versions\" & Folder & "\.pclignore") Then
- Log("[Minecraft] 跳过要求忽略的项目:" & Path & "versions\" & Folder)
- Continue For
+ Dim VersionFolder As String = $"{Path}versions\{Folder}\"
+ If File.Exists(VersionFolder & ".pclignore") Then
+ If IsFirstMcVersionListLoad Then
+ Log("[Minecraft] 清理残留的忽略项目:" & VersionFolder) '#2781
+ File.Delete(VersionFolder & ".pclignore")
+ Else
+ Log("[Minecraft] 跳过要求忽略的项目:" & VersionFolder)
+ Continue For
+ End If
End If
Try
'读取单个版本
- Dim Version As New McVersion(Path & "versions\" & Folder & "\")
+ Dim Version As New McVersion(VersionFolder)
VersionList.Add(Version)
Version.Info = ReadIni(Version.Path & "PCL\Setup.ini", "CustomInfo", "")
If Version.Info = "" Then Version.Info = ReadIni(Version.Path & "PCL\Setup.ini", "Info", Version.Info)
@@ -1195,11 +1208,17 @@ OnLoaded:
Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
Continue For
End If
- If File.Exists(Folder.FullName & "\.pclignore") Then
- Log("[Minecraft] 跳过要求忽略的项目:" & Folder.FullName)
- Continue For
+ Dim VersionFolder As String = Folder.FullName & "\"
+ If File.Exists(VersionFolder & ".pclignore") Then
+ If IsFirstMcVersionListLoad Then
+ Log("[Minecraft] 清理残留的忽略项目:" & VersionFolder) '#2781
+ File.Delete(VersionFolder & ".pclignore")
+ Else
+ Log("[Minecraft] 跳过要求忽略的项目:" & VersionFolder)
+ Continue For
+ End If
End If
- Dim Version As New McVersion(Folder.FullName & "\")
+ Dim Version As New McVersion(VersionFolder)
VersionList.Add(Version)
Version.Load()
Next
@@ -1496,7 +1515,7 @@ NextVersion:
'''
Public Function McSkinGetAddress(Uuid As String, Type As String) As String
If Uuid = "" Then Throw New Exception("Uuid 为空。")
- If Uuid.StartsWith("000000") Then Throw New Exception("离线 Uuid 无正版皮肤文件。")
+ If Uuid.StartsWithF("000000") Then Throw New Exception("离线 Uuid 无正版皮肤文件。")
'尝试读取缓存
Dim CacheSkinAddress As String = ReadIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid)
If Not CacheSkinAddress = "" Then Return CacheSkinAddress
@@ -1962,7 +1981,7 @@ NextVersion:
'获取 Url 的真实地址
Urls.Add(Token.Url)
If Token.Url.Contains("launcher.mojang.com/v1/objects") Then Urls = DlSourceLauncherOrMetaGet(Token.Url).ToList() 'Mappings
- If Token.Url.Contains("maven") Then Urls.Insert(0, Token.Url.Replace(Mid(Token.Url, 1, Token.Url.IndexOf("maven")), "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven"))
+ If Token.Url.Contains("maven") Then Urls.Insert(0, 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"))
End If
If Token.LocalPath.Contains("transformer-discovery-service") Then
'Transformer 文件释放
@@ -1992,10 +2011,6 @@ NextVersion:
Public Function McLibGet(Original As String, Optional WithHead As Boolean = True, Optional IgnoreLiteLoader As Boolean = False, Optional CustomMcFolder As String = Nothing) As String
CustomMcFolder = If(CustomMcFolder, PathMcFolder)
Dim Splited = Original.Split(":")
- 'If Original.ToLower.Contains("xray") OrElse Original.ToLower.Contains("rift") OrElse Original.ToLower.Contains("mixin") OrElse Original.Contains("net.minecraftforge:forge") OrElse Splited(2).Contains("beta") OrElse (IgnoreLiteLoader AndAlso Original.Contains("liteloader")) Then
- 'ElseIf Splited(2).Contains("-") Then
- ' Log("[Launch] 可能存在分段问题的支持库:" & Original)
- 'End If
McLibGet = If(WithHead, CustomMcFolder & "libraries\", "") &
Splited(0).Replace(".", "\") & "\" & Splited(1) & "\" & Splited(2) & "\" & Splited(1) & "-" & Splited(2) & ".jar"
'判断 OptiFine 是否应该使用 installer
@@ -2252,8 +2267,10 @@ NextVersion:
If Left = "未知版本" AndAlso Right = "未知版本" Then Return 0
If Left <> "未知版本" AndAlso Right = "未知版本" Then Return -1
End If
- Dim Lefts = RegexSearch(Left.ToLower.Replace("快照", "snapshot"), "[a-z]+|[0-9]+")
- Dim Rights = RegexSearch(Right.ToLower.Replace("快照", "snapshot"), "[a-z]+|[0-9]+")
+ Left = Left.ToLowerInvariant
+ Right = Right.ToLowerInvariant
+ Dim Lefts = RegexSearch(Left.Replace("快照", "snapshot"), "[a-z]+|[0-9]+")
+ Dim Rights = RegexSearch(Right.Replace("快照", "snapshot"), "[a-z]+|[0-9]+")
Dim i As Integer = 0
While True
'两边均缺失,感觉是一个东西
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb
index f768279..1a05fe3 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb
@@ -48,7 +48,7 @@ Public Module ModMod
Load()
If Not IsFileAvailable Then
Return McModState.Unavaliable
- ElseIf Path.EndsWith(".disabled") OrElse Path.EndsWith(".old") Then
+ ElseIf Path.EndsWithF(".disabled", True) OrElse Path.EndsWithF(".old", True) Then
Return McModState.Disabled
Else
Return McModState.Fine
@@ -76,7 +76,7 @@ Public Module ModMod
Return _Name
End Get
Set(value As String)
- If _Name Is Nothing AndAlso value IsNot Nothing AndAlso
+ If _Name Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
_Name = value
End If
@@ -113,12 +113,12 @@ Public Module ModMod
Return _Version
End Get
Set(value As String)
- If _Version IsNot Nothing AndAlso (_Version.Contains(".") OrElse _Version.Contains("-")) Then Exit Property
- If value IsNot Nothing AndAlso value.ToLower.Contains("version") Then value = "version" '需要修改的标识
+ If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
+ If value?.ContainsF("version", True) Then value = "version" '需要修改的标识
_Version = value
End Set
End Property
- Private _Version As String = Nothing
+ Public _Version As String = Nothing
'''
''' 用于依赖检查的 ModID。
@@ -129,12 +129,12 @@ Public Module ModMod
Return _ModId
End Get
Set(value As String)
- If value Is Nothing Then Exit Property
- value = RegexSeek(value.ToLower, "[0-9a-z_-]+")
- If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
- If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
- If _ModId Is Nothing Then _ModId = value
- End If
+ If value Is Nothing Then Return
+ value = RegexSeek(value, "[0-9a-zA-Z_-]+")
+ If value Is Nothing OrElse value.Count <= 1 OrElse Val(value).ToString = value Then Return
+ If value.ContainsF("name", True) OrElse value.ContainsF("modid", True) Then Return
+ If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
+ If _ModId Is Nothing Then _ModId = value
End Set
End Property
Private _ModId As String = Nothing
@@ -152,7 +152,7 @@ Public Module ModMod
Return _Url
End Get
Set(value As String)
- If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWith("http") Then
+ If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWithF("http") Then
_Url = value
End If
End Set
@@ -193,7 +193,7 @@ Public Module ModMod
If VersionRequirement Is Nothing OrElse ((Not VersionRequirement.Contains(".")) AndAlso (Not VersionRequirement.Contains("-"))) OrElse VersionRequirement.Contains("$") Then
VersionRequirement = Nothing
Else
- If (Not VersionRequirement.StartsWith("[")) AndAlso (Not VersionRequirement.StartsWith("(")) AndAlso (Not VersionRequirement.EndsWith("]")) AndAlso (Not VersionRequirement.EndsWith(")")) Then VersionRequirement = "[" & VersionRequirement & ",)"
+ If (Not VersionRequirement.StartsWithF("[")) AndAlso (Not VersionRequirement.StartsWithF("(")) AndAlso (Not VersionRequirement.EndsWithF("]")) AndAlso (Not VersionRequirement.EndsWithF(")")) Then VersionRequirement = "[" & VersionRequirement & ",)"
End If
'向依赖项中添加
If _Dependencies.ContainsKey(ModID) Then
@@ -270,7 +270,6 @@ Public Module ModMod
_Dependencies = New Dictionary(Of String, String)
IsLoaded = False
_FileUnavailableReason = Nothing
- IsInfoWithoutClassAvailable = False
IsInfoWithClassLoaded = False
IsInfoWithClassAvailable = False
End Sub
@@ -284,15 +283,12 @@ Public Module ModMod
Init()
Dim Jar As ZipArchive = Nothing
Try
- '阶段 1:基础可用性检查、打开 Jar 文件
+ '基础可用性检查、打开 Jar 文件
If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")")
If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")")
Jar = New ZipArchive(New FileStream(Path, FileMode.Open))
- If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空")
- '阶段 2:信息获取
- LoadWithoutClass(Jar)
- '阶段 3: Class 信息获取
- If Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar)
+ '信息获取
+ LookupMetadata(Jar)
Catch ex As UnauthorizedAccessException
Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
_FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex)
@@ -307,9 +303,9 @@ Public Module ModMod
End Sub
'''
- ''' 进行不使用 .class 文件的信息获取。
+ ''' 从 jar 文件中获取 Mod 信息。
'''
- Private Sub LoadWithoutClass(Jar As ZipArchive)
+ Private Sub LookupMetadata(Jar As ZipArchive)
#Region "尝试使用 mcmod.info"
Try
@@ -347,7 +343,7 @@ Public Module ModMod
If Reqs IsNot Nothing Then
For Each Token As String In Reqs
If Not String.IsNullOrEmpty(Token) Then
- Token = Token.Substring(Token.IndexOf(":") + 1)
+ Token = Token.Substring(Token.IndexOfF(":") + 1)
If Token.Contains("@") Then
AddDependency(Token.Split("@")(0), Token.Split("@")(1))
Else
@@ -360,7 +356,7 @@ Public Module ModMod
If Reqs IsNot Nothing Then
For Each Token As String In Reqs
If Not String.IsNullOrEmpty(Token) Then
- Token = Token.Substring(Token.IndexOf(":") + 1)
+ Token = Token.Substring(Token.IndexOfF(":") + 1)
If Token.Contains("@") Then
AddDependency(Token.Split("@")(0), Token.Split("@")(1))
Else
@@ -407,8 +403,8 @@ GotFabric:
' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "")
' If Deps <> "" Then
' For Each Dep In Deps.Split(";")
- ' If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
- ' Dep = Dep.Substring(Dep.IndexOf(":") + 1)
+ ' If Dep = "" OrElse Not Dep.StartsWithF("required-") Then Continue For
+ ' Dep = Dep.Substring(Dep.IndexOfF(":") + 1)
' If Dep.Contains("@") Then
' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
' Else
@@ -436,10 +432,10 @@ GotFabric:
'文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
Dim Lines As New List(Of String)
For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
- If Line.StartsWith("#") Then '去除注释
+ If Line.StartsWithF("#") Then '去除注释
Continue For
ElseIf Line.Contains("#") Then
- Line = Line.Substring(0, Line.IndexOf("#"))
+ Line = Line.Substring(0, Line.IndexOfF("#"))
End If
Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
If Line.Count > 0 Then Lines.Add(Line) '去除空行
@@ -448,28 +444,28 @@ GotFabric:
Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))}
For i = 0 To Lines.Count - 1
Dim Line As String = Lines(i)
- If Line.StartsWith("[") AndAlso Line.EndsWith("]") Then
+ 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.IndexOf("=")).TrimEnd(New Char() {" "c, " "c, " "c})
- Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
+ 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.StartsWith("""") AndAlso RawValue.EndsWith("""") Then
+ If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
'单行字符串
Value = RawValue.Trim("""")
- ElseIf RawValue.StartsWith("'''") Then
+ ElseIf RawValue.StartsWithF("'''") Then
'多行字符串
Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
- If ValueLines(0).EndsWith("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
+ 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.EndsWith("'''") Then
+ If ValueLine.EndsWithF("'''") Then
ValueLines.Add(ValueLine.TrimEnd("'"))
Exit Do
Else
@@ -575,8 +571,8 @@ Got:
Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "")
If Deps <> "" Then
For Each Dep In Deps.Split(";")
- If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
- Dep = Dep.Substring(Dep.IndexOf(":") + 1)
+ If Dep = "" OrElse Not Dep.StartsWithF("required-") Then Continue For
+ Dep = Dep.Substring(Dep.IndexOfF(":") + 1)
If Dep.Contains("@") Then
AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
Else
@@ -597,7 +593,7 @@ Finished:
If MetaEntry IsNot Nothing Then
Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
If MetaString.Contains("Implementation-Version:") Then
- MetaString = MetaString.Substring(MetaString.IndexOf("Implementation-Version:") + "Implementation-Version:".Count)
+ MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
Version = MetaString
End If
@@ -610,97 +606,17 @@ Finished:
If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
#End Region
- IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
- End Sub
-
- '''
- ''' 进行使用 .class 文件的信息获取。
- '''
- Private Sub LoadWithClass(Jar As ZipArchive)
- Try
- '查找入口点文件
- Dim ModClass As String = Nothing
- Dim ModClassNotBest As String = Nothing '非完美匹配
- For Each Entry In Jar.Entries
- If Entry.Name.EndsWith(".class") Then
- Dim Temp As String = ReadFile(Entry.Open()).ToLower
- If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then
- ModClass = Temp
- Exit For
- ElseIf Temp.Contains("modid") Then
- ModClassNotBest = Temp
- End If
- End If
- Next
- If ModClass Is Nothing Then ModClass = ModClassNotBest
- If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点")
- ModClass = ModClass.Replace("ljava/lang/string;", "")
- If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte
- Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer
- '获取 ModID
- If _ModId Is Nothing Then
- IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1
- IncreaseCount = 0
- Do While Convert.ToInt32(ModClass(IndexHead)) < 32
- IndexHead += 1
- IncreaseCount += 1
- If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败")
- Loop
- IndexTail = IndexHead + 1
- IncreaseCount = 0
- Do Until Convert.ToInt32(ModClass(IndexTail)) < 32
- IndexTail += 1
- IncreaseCount += 1
- If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败")
- Loop
- ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead)
- End If
- '获取 Version
- If _Version Is Nothing AndAlso ModClass.Contains("version") Then
- IndexHead = ModClass.IndexOf("version") + "version".Length + 1
- IncreaseCount = 0
- Do While Convert.ToInt32(ModClass(IndexHead)) < 32
- IndexHead += 1
- IncreaseCount += 1
- If IncreaseCount > 10 Then GoTo VersionFindFail
- Loop
- IndexTail = IndexHead + 1
- IncreaseCount = 0
- Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse
- (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse
- (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c)
- IndexTail += 1
- IncreaseCount += 1
- If IncreaseCount > 50 Then GoTo VersionFindFail
- Loop
- _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead)
- End If
-VersionFindFail:
- '获取 Dependencies
- IndexHead = ModClass.IndexOf("dependencies")
- If IndexHead > 0 Then
- If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299)
- Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?")
- For Each Token As String In Deps
- If Not String.IsNullOrEmpty(Token) Then
- If Token.Contains("@") Then
- AddDependency(Token.Split("@")(0), Token.Split("@")(1))
- Else
- AddDependency(Token)
- End If
- End If
- Next
- End If
- Catch ex As Exception
- Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal)
- End Try
- IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
End Sub
#End Region
#Region "网络信息"
+ '''
+ ''' 当任何网络信息更新时触发。
+ '''
+ Public Event OnCompUpdate(sender As McMod)
+
'''
''' 该 Mod 关联的网络项目。
'''
@@ -710,12 +626,70 @@ VersionFindFail:
End Get
Set(value As CompProject)
_Comp = value
- RaiseEvent OnGetCompProject(Me)
+ RaiseEvent OnCompUpdate(Me)
End Set
End Property
Private _Comp As CompProject
- Public IsCompFromModrinth = False
- Public Event OnGetCompProject(sender As McMod)
+
+ '''
+ ''' 本地文件对应的联网文件信息。
+ '''
+ Public CompFile As CompFile
+
+ '''
+ ''' 该 Mod 对应的联网最新版本。
+ '''
+ Public Property UpdateFile As CompFile
+ Get
+ Return _UpdateFile
+ End Get
+ Set(value As CompFile)
+ _UpdateFile = value
+ RaiseEvent OnCompUpdate(Me)
+ End Set
+ End Property
+ Private _UpdateFile As CompFile
+
+ '''
+ ''' 该 Mod 的更新日志网址。
+ '''
+ Public ChangelogUrls As New List(Of String)
+ '''
+ ''' 所有网络信息是否已成功加载。
+ '''
+ Public CompLoaded As Boolean = False
+
+ '''
+ ''' 将网络信息保存为 Json。
+ '''
+ 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
+ '''
+ ''' 从 Json 中读取网络信息。
+ '''
+ 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
+
+ '''
+ ''' 该文件是否可以更新。
+ '''
+ Public ReadOnly Property CanUpdate As Boolean
+ Get
+ Return ChangelogUrls.Any()
+ End Get
+ End Property
'''
''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
@@ -723,6 +697,14 @@ VersionFindFail:
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 <> "" Then
+ _CurseForgeHash = Cached
+ Return _CurseForgeHash
+ End If
'读取文件
Dim data As New List(Of Byte)
For Each b As Byte In File.ReadAllBytes(Path)
@@ -757,6 +739,8 @@ VersionFindFail:
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
@@ -769,7 +753,18 @@ VersionFindFail:
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
@@ -794,7 +789,7 @@ VersionFindFail:
''' 是否可能为前置 Mod。
'''
Public Function IsPresetMod() As Boolean
- Return Dependencies.Count = 0 AndAlso Name IsNot Nothing AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib"))
+ Return Not Dependencies.Any() AndAlso Name IsNot Nothing AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib"))
End Function
'''
@@ -803,9 +798,9 @@ VersionFindFail:
Public Shared Function IsModFile(Path As String)
If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
Path = Path.ToLower
- If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse
- Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") OrElse
- Path.EndsWith(".jar.old") OrElse Path.EndsWith(".zip.old") OrElse Path.EndsWith(".litemod.old") Then Return True
+ 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
@@ -817,6 +812,20 @@ VersionFindFail:
Try
RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False)
+ '等待 Mod 更新完成
+ If PageVersionMod.UpdatingVersions.Contains(Loader.Input) Then
+ Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表")
+ Try
+ RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.Text = "正在更新 Mod")
+ Do Until Not PageVersionMod.UpdatingVersions.Contains(Loader.Input)
+ If Loader.IsAborted Then Exit Sub
+ Thread.Sleep(100)
+ Loop
+ Finally
+ RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.Text = "正在加载 Mod 列表")
+ End Try
+ End If
+
'获取 Mod 文件夹下的可用文件列表
Dim ModFileList As New List(Of FileInfo)
If Directory.Exists(Loader.Input) Then
@@ -846,7 +855,7 @@ VersionFindFail:
If CacheContent <> "" Then
Cache = GetJson(CacheContent)
If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
- Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息")
+ Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", LogLevel.Debug)
Cache = New JObject
End If
End If
@@ -859,7 +868,7 @@ VersionFindFail:
Dim ModList As New List(Of McMod)
Dim ModUpdateList As New List(Of McMod)
For Each ModFile As FileInfo In ModFileList
- Loader.Progress += 0.95 / ModFileList.Count
+ Loader.Progress += 0.94 / ModFileList.Count
If Loader.IsAborted Then Exit Sub
'加载 McMod 对象
Dim ModEntry As New McMod(ModFile.FullName)
@@ -880,10 +889,9 @@ VersionFindFail:
If ModEntry.State = McMod.McModState.Unavaliable Then Continue For
Dim Hash = ModEntry.ModrinthHash
If Cache.ContainsKey(Hash) Then
- ModEntry.Comp = New CompProject(Cache(Hash))
- ModEntry.IsCompFromModrinth = Not ModEntry.Comp.FromCurseForge
+ ModEntry.FromJson(Cache(Hash))
'如果缓存中的信息在 6 小时以内更新过,则无需重新获取
- If Date.Now - Cache(Hash)("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
+ If ModEntry.CompLoaded AndAlso Date.Now - Cache(Hash)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
End If
ModUpdateList.Add(ModEntry)
Next
@@ -906,10 +914,10 @@ VersionFindFail:
'开始联网加载
If ModUpdateList.Any() Then
+ 'TODO: 添加信息获取中提示
McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModUpdateList, Cache), IsForceRestart:=True)
End If
- Loader.Progress = 1
Catch ex As Exception
Log(ex, "Mod 列表加载失败", LogLevel.Debug)
Throw
@@ -917,29 +925,31 @@ VersionFindFail:
End Sub
'联网加载 Mod 详情
- Private Const LocalModCacheVersion As Integer = 1
+ Private Const LocalModCacheVersion As Integer = 2
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 List(Of McMod) = Loader.Input.Key
Dim Cache As JObject = Loader.Input.Value
'获取作为检查目标的加载器和版本
Dim TargetMcVersion As McVersionInfo = PageVersionLeft.Version.Version
- Dim ModLoaders As New List(Of String)
- If TargetMcVersion.HasForge Then ModLoaders.Add("forge")
- If TargetMcVersion.HasFabric Then ModLoaders.Add("fabric")
- If TargetMcVersion.HasLiteLoader Then ModLoaders.Add("liteloader")
- If Not ModLoaders.Any() Then ModLoaders.AddRange({"forge", "fabric", "liteloader"})
+ Dim ModLoaders As New List(Of CompModLoaderType)
+ If TargetMcVersion.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
+ If TargetMcVersion.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
+ If TargetMcVersion.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
+ If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
Dim McVersions As New List(Of String) From {TargetMcVersion.McName}
- If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
- McVersions.Add($"1.{TargetMcVersion.McCodeMain}")
- For i = 1 To TargetMcVersion.McCodeSub
- McVersions.Add($"1.{TargetMcVersion.McCodeMain}.{i}")
- Next
- End If
- McVersions = McVersions.Distinct().ToList()
+ '暂不向下扩展检查的 MC 小版本
+ '例如:Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
+ 'If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
+ ' McVersions.Add($"1.{TargetMcVersion.McCodeMain}")
+ ' For i = 1 To TargetMcVersion.McCodeSub
+ ' McVersions.Add($"1.{TargetMcVersion.McCodeMain}.{i}")
+ ' Next
+ 'End If
+ 'McVersions = McVersions.Distinct().ToList()
'开始网络获取
Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{McVersions.Join("/")}")
- Dim CompletedThread As Integer = 0
+ Dim EndedThreadCount As Integer = 0, SucceedThreadCount As Integer = 0
Dim MainThread As Thread = Thread.CurrentThread
'从 Modrinth 获取信息
RunInNewThread(
@@ -947,25 +957,24 @@ VersionFindFail:
Try
'步骤 1:获取 Hash 与对应的工程 ID
Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
- Dim ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
- $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1"",
- ""loaders"": [""{ModLoaders.Join(""",""")}""],""game_versions"": [""{McVersions.Join(""",""")}""]}}", "application/json")), JObject)
- Log($"[Mod] 从 Modrinth 获取到 {ModrinthUpdate.Count} 个本地 Mod 的对应信息")
+ Dim ModrinthVersion = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files", "POST",
+ $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
+ Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
- If ModrinthUpdate.Count = 0 Then Exit Sub
+ If ModrinthVersion.Count = 0 Then Exit Sub
Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
- For Each ModEntity In Mods
- If Not ModrinthUpdate.ContainsKey(ModEntity.ModrinthHash) Then Continue For
- Dim ProjectId = ModrinthUpdate(ModEntity.ModrinthHash)("project_id").ToString
- If CompProjectCache.ContainsKey(ProjectId) Then
- ModEntity.Comp = CompProjectCache(ProjectId)
- Else
- If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
- ModrinthMapping(ProjectId).Add(ModEntity)
- End If
+ For Each Entry In Mods
+ If Not ModrinthVersion.ContainsKey(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
Next
If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
- Log($"[Mod] 读取缓存后还需要从 Modrinth 获取 {ModrinthMapping.Count} 类本地 Mod 的工程信息")
+ Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
'步骤 3:获取工程信息
If Not ModrinthMapping.Any() Then Exit Sub
Dim ModrinthProject = CType(GetJson(NetRequestRetry(
@@ -974,18 +983,37 @@ VersionFindFail:
For Each ProjectJson In ModrinthProject
Dim Project As New CompProject(ProjectJson)
For Each Entry In ModrinthMapping(Project.Id)
- If Entry.Comp IsNot Nothing AndAlso Not Entry.IsCompFromModrinth Then
- Project.LogoUrl = Entry.Comp.LogoUrl 'Modrinth 的部分 Logo 加载不出来
- End If
- Entry.IsCompFromModrinth = True
+ If Entry.Comp IsNot Nothing AndAlso Entry.Comp.FromCurseForge Then Project.LogoUrl = Entry.Comp.LogoUrl 'Modrinth 的部分 Logo 不是图片,如果可能,使用 CurseForge 的 Logo
Entry.Comp = Project
Next
Next
+ Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
+ '步骤 4:获取更新信息
+ Dim ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
+ $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1"",
+ ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersions.Join(""",""")}""]}}", "application/json")), JObject)
+ 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?{McVersions.Select(Function(v) $"g={v}&").Join("")}")
+ 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?{McVersions.Select(Function(v) $"g={v}&").Join("")}"}
+ Entry.UpdateFile = UpdateFile
+ End If
+ Next
Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
+ SucceedThreadCount += 1
Catch ex As Exception
Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
Finally
- CompletedThread += 1
+ EndedThreadCount += 1
End Try
End Sub, "Mod List Detail Loader Modrinth")
'从 CurseForge 获取信息
@@ -994,57 +1022,110 @@ VersionFindFail:
Try
'步骤 1:获取 Hash 与对应的工程 ID
Dim CurseForgeHashes As New List(Of UInteger)
- For Each ModEntity In Mods
- CurseForgeHashes.Add(ModEntity.CurseForgeHash)
+ For Each Entry In Mods
+ CurseForgeHashes.Add(Entry.CurseForgeHash)
If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
Next
Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
$"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
'步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
- If CurseForgeRaw.Count = 0 Then Exit Sub
+ If Not CurseForgeRaw.Any() Then Exit Sub
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) Then
- Entry.Comp = CompProjectCache(ProjectId)
- Else
- If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
- CurseForgeMapping(ProjectId).Add(Entry)
- End If
+ 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.IsAbortedWithThread(MainThread) Then Exit Sub
- Log($"[Mod] 读取缓存后还需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
+ Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
'步骤 3:获取工程信息
If Not CurseForgeMapping.Any() Then Exit Sub
Dim CurseForgeProject = CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods", "POST",
$"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")), JObject)("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 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 Entry.IsCompFromModrinth Then
+ If Entry.Comp IsNot Nothing AndAlso Not Entry.Comp.FromCurseForge Then
Entry.Comp.LogoUrl = Project.LogoUrl 'Modrinth 部分 Logo 加载不出来
Entry.Comp = Entry.Comp '再次触发修改事件
Continue For
End If
- Entry.IsCompFromModrinth = False
Entry.Comp = Project
Next
+ '查找或许版本更新的文件列表
+ If ModLoaders.Count = 1 Then 'TODO: 结果有多个 ModLoader 时提示无法检测更新
+ Dim NewestVersion As String = Nothing
+ Dim NewestFileIds As New List(Of Integer)
+ For Each IndexEntry In ProjectJson("latestFilesIndexes")
+ If ModLoaders.Single <> IndexEntry("modLoader")?.ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
+ Dim IndexVersion As String = IndexEntry("gameVersion")
+ If Not McVersions.Contains(IndexVersion) Then Continue For 'MC 版本匹配
+ '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
+ If NewestVersion IsNot Nothing AndAlso VersionSortInteger(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 信息结束")
+ Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
+ '步骤 4:获取更新文件信息
+ If Not UpdateFileIds.Any() Then Exit Sub
+ Dim CurseForgeFiles = CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST",
+ $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")), JObject)("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 更新信息结束")
+ SucceedThreadCount += 1
Catch ex As Exception
Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
Finally
- CompletedThread += 1
+ EndedThreadCount += 1
End Try
End Sub, "Mod List Detail Loader CurseForge")
'等待线程结束
- Do Until CompletedThread = 2
+ Do Until EndedThreadCount = 2
If Loader.IsAborted Then Exit Sub
Thread.Sleep(10)
Loop
@@ -1052,8 +1133,9 @@ VersionFindFail:
Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
If Not Mods.Any() Then Exit Sub
- For Each ModEntity In Mods
- Cache(ModEntity.ModrinthHash) = ModEntity.Comp.ToJson()
+ For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
+ Entry.CompLoaded = SucceedThreadCount = 2
+ Cache(Entry.ModrinthHash) = Entry.ToJson()
Next
WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
End Sub
@@ -1083,12 +1165,12 @@ VersionFindFail:
If CurrentDependencies.ContainsKey(PossibleModId) Then
If CurrentDependencies(PossibleModId)(2) = 1 Then
Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf &
- " - " & ModEntity.FileName & vbCrLf &
- " - " & CurrentDependencies(PossibleModId)(1))
+ " - " & ModEntity.FileName & vbCrLf &
+ " - " & CurrentDependencies(PossibleModId)(1))
Else
Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf &
- " - " & ModEntity.FileName & vbCrLf &
- " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
+ " - " & ModEntity.FileName & vbCrLf &
+ " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
End If
Else
CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count})
@@ -1106,8 +1188,8 @@ VersionFindFail:
If Dependency.Value IsNot Nothing Then
'获取分段后的详细版本信息
Dim ReqVersion As String = Dependency.Value
- Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[")
- Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]")
+ Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWithF("[")
+ Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWithF("]")
Dim ReqVersionHead As String
Dim ReqVersionTail As String
If ReqVersion.Contains(",") Then
@@ -1121,8 +1203,8 @@ VersionFindFail:
ReqVersionTailCanEqual = False
End If
End If
- If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1)
- If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1)
+ If ReqVersionHead.StartsWithF("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOfF("-") + 1)
+ If ReqVersionTail.StartsWithF("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOfF("-") + 1)
'获取报错描述文本
Dim VersionRequire As String
If ReqVersionHead = ReqVersionTail Then
@@ -1150,7 +1232,7 @@ VersionFindFail:
Continue For
End If
Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0")
- If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1)
+ If CurrentVersion.StartsWithF("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOfF("-") + 1)
'对比前置 Mod 头部版本
If ReqVersionHead.Contains(".") Then
If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then
@@ -1179,7 +1261,7 @@ VersionFindFail:
Log(ex, "检查 Mod 时出错")
End Try
Next
- If Result.Count = 0 Then
+ If Not Result.Any() Then
Log("[Minecraft] Mod 检查未发现异常")
Else
Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf))
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
index 745a6ed..9cc2d6f 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
@@ -40,7 +40,7 @@ Public Module ModModpack
For Each Entry In Archive.Entries
Dim FullNames As String() = Entry.FullName.Split("/")
ArchiveBaseFolder = FullNames(0) & "/"
- If Entry.FullName.EndsWith("/versions/") AndAlso FullNames.Count = 3 Then PackType = 9 : Exit Try '压缩包
+ If Entry.FullName.EndsWithF("/versions/") AndAlso FullNames.Count = 3 Then PackType = 9 : Exit Try '压缩包
'确定为一级目录下
If FullNames.Count <> 2 Then Continue For
'判断是否为关键文件
@@ -61,7 +61,7 @@ Public Module ModModpack
If GetExceptionDetail(ex, True).Contains("Error.WinIOError") Then
Log(ex, "打开整合包文件失败", If(ShowHint, LogLevel.Hint, LogLevel.Normal))
Return False
- ElseIf File.ToLower.EndsWith(".rar") Then
+ ElseIf File.EndsWithF(".rar", True) Then
Log(ex, "PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试", If(ShowHint, LogLevel.Hint, LogLevel.Normal))
Return False
Else
@@ -186,7 +186,7 @@ Retry:
Dim FabricVersion As String = Nothing
For Each Entry In If(Json("minecraft")("modLoaders"), {})
Dim Id As String = If(Entry("id"), "").ToString.ToLower
- If Id.StartsWith("forge-") Then
+ If Id.StartsWithF("forge-") Then
'Forge 指定
If Id.Contains("recommended") Then
Log("[ModPack] 该整合包版本过老,已不支持进行安装!", LogLevel.Hint)
@@ -199,7 +199,7 @@ Retry:
Catch ex As Exception
Log(ex, "读取整合包 Forge 版本失败:" & Id)
End Try
- ElseIf Id.StartsWith("fabric-") Then
+ ElseIf Id.StartsWithF("fabric-") Then
'Fabric 指定
Try
Log("[ModPack] 整合包 Fabric 版本:" & Id)
@@ -277,7 +277,7 @@ Retry:
IsResourcePack =
(Not ModuleNames.Contains("META-INF")) AndAlso (Not ModuleNames.Contains("mcmod.info")) AndAlso '不包含 META-INF 或 mcmod.info
ModuleNames.Contains("pack.mcmeta") AndAlso '包含 pack.mcmeta
- (Not File.FileName.EndsWith(".jar")) '文件后缀不是 .jar
+ (Not File.FileName.EndsWithF(".jar", True)) '文件后缀不是 .jar
Else
IsResourcePack = False
End If
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
index 9926e4b..2c63b18 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
@@ -143,7 +143,7 @@
'输出文本
Dim Copyed As New List(Of String)
SyncLock WaitingLogLock
- If WaitingLog.Count = 0 Then Exit Sub
+ If Not WaitingLog.Any() Then Exit Sub
Copyed = WaitingLog
WaitingLog = New List(Of String)(1000)
End SyncLock
@@ -192,7 +192,7 @@
If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
LogProgress = 2
- ElseIf LogProgress < 3 AndAlso Text.ToLower.Contains("lwjgl version") Then
+ ElseIf LogProgress < 3 AndAlso Text.ContainsF("lwjgl version", True) Then
WatcherLog("日志 3/5:LWJGL 版本已确认")
LogProgress = 3
ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
@@ -269,7 +269,7 @@
If MinecraftWindow Is Nothing Then Exit Sub
Dim MinecraftWindowName = MinecraftWindow.Value.Value, MinecraftWindowHandle = MinecraftWindow.Value.Key
'已找到窗口
- If Not MinecraftWindowName.StartsWith("FML") Then
+ If Not MinecraftWindowName.StartsWithF("FML") Then
'已找到 Minecraft 窗口
WindowHandle = MinecraftWindowHandle
WatcherLog($"Minecraft 窗口已加载:{MinecraftWindowName}({MinecraftWindowHandle.ToInt64})")
@@ -317,7 +317,7 @@
Dim WindowText As String = str.ToString
'有的 Mod 可以修改窗口标题,所以不能检测是否为 Minecraft 打头,这并不准确
'部分版本会搞个 GLFW message window 出来所以得反选
- If Not (WindowText.StartsWith("FML") OrElse (WindowText <> "PopupMessageWindow") AndAlso Not WindowText.StartsWith("GLFW")) Then Exit Sub
+ If Not (WindowText.StartsWithF("FML") OrElse (WindowText <> "PopupMessageWindow") AndAlso Not WindowText.StartsWithF("GLFW")) Then Exit Sub
'获取窗口关联的进程
Dim ProcessId As Integer
GetWindowThreadProcessId(hwnd, ProcessId)
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/MyCompItem.xaml.vb b/Plain Craft Launcher 2/Modules/Minecraft/MyCompItem.xaml.vb
index 9acefae..6bf0f72 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/MyCompItem.xaml.vb
+++ b/Plain Craft Launcher 2/Modules/Minecraft/MyCompItem.xaml.vb
@@ -14,11 +14,11 @@
_Logo = value
Dim FileAddress = PathTemp & "CompLogo\" & GetHash(_Logo) & ".png"
Try
- If _Logo.ToLower.StartsWith("http") Then
+ If _Logo.StartsWithF("http", True) Then
'网络图片
If File.Exists(FileAddress) Then
PathLogo.Source = New MyBitmap(FileAddress)
- ElseIf _Logo.ToLower.EndsWith(".webp") Then 'Modrinth 林业 Mod 使用了不支持的 WebP 格式 Logo
+ ElseIf _Logo.EndsWithF(".webp", True) Then 'Modrinth 林业 Mod 使用了不支持的 WebP 格式 Logo
Log($"[Comp] 发现不支持的 WebP 格式图标,已更改为默认图标:{_Logo}")
PathLogo.Source = New MyBitmap(PathImage & "Icons/NoIcon.png")
Else
diff --git a/Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml b/Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml
index 8438648..a64f249 100644
--- a/Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml
+++ b/Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml
@@ -1,8 +1,9 @@
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:local="clr-namespace:PCL;assembly="
+ x:Name="PanBack" Height="44"
+ RenderTransformOrigin="0.5,0.5" Background="{StaticResource ColorBrushSemiTransparent}" SnapsToDevicePixels="True">
@@ -19,7 +20,7 @@
+ HorizontalAlignment="Right" VerticalAlignment="Center" Width="34" Height="34">
@@ -27,10 +28,26 @@
-
-
+ SnapsToDevicePixels="False" UseLayoutRounding="False">
+
+
+
+
+
{W_h
z%a%Mxac-EJXb!W`{2|qH#-4X)d8ORvd5m|%C3t2Dzuv&5FCzXk`sYW
z$iB~_Jzy3Co%KD8rG|Q=p|BP4?F|2uj6T#QDs8v`ndpi|@&r3K>H@4g#reow395qZ
zX*!7+Z&B!gafl_+sXE0~Fm7SF(0*M*W&-Cl(itSiyV|ba?l8@2MRlVewWs}o*Bc6|
zSlk&b{EL7gPBbss0ATTVrSrkXiFz(2774E#b={ML(idvGPl^=1@k*_D$$TZGER+;4
z?(%!2Po46&Q==23!*P&Oqp0xB^;nWvQm<$4m{+UhiR4ZYr3GLMH?=r`6I|1)r?j_Y
zbP1DNA-#PBBhtfe02swNDhUK{xYO{%
zoPAU)aQ*lqJDaqIj1p+c9c=KOJyJ;;p-UBkNULAZGGKB_rPUm|-4`QnMpN9vL_xlV
z8;%jBFG6$vjXx-a$I!6{RZR>jXdCd7l}^8Gi#sFvEGHOR&gDXR_(X%eIS%@;;FBAy
zW$%ha1eT-NLks6H-+AAZL-JeRjKwYbK3~Hw6IX=X_iLFU{AD39ydUw=dQ-guZ@uVD
z){mz?Q@Ar7dKXggHDL{A<%!bCIg2CpA!B!3LGnpGZ6E7(8rmpOvHz1fdnDOM!YWAV
z0UR}!HMIF<-St!(62ueR+%um`yxVvHIS;wV@zPLF`JMn4)oq=VuoJ1t6#&~uBtB7(tCqHV)l77g)q;Kd`>y#a$ZFxI^oKm2$00t2lOxvb#BuZPRB
z=pk>iHH$7_231llJKKKU|G6xyZu(uM{^Feera0Q{vEf)3o;pU^w@qtiuyr
zyUTF`#U99z`|yj)pMfbt7Hr++!3dD3&3XCRp}8MlkN60=
z944Q2!vp!xGHfO?s%A$9Lk8D_-s?EUu}uU$E%}iuUaV9Aea@Ehm@aRXRXaBoN%qt`F1
zpp|X$JyE<%4c0_5x8o7D0atKFT!xi+rn+e-*1{KNa(Glx@OB+w`yueF_rbd
z7f>ON4GNHx$|2B$WZF|v^Cvf6^C@Vj#mug7rIwETnh=oPKllA3>*w|i9vqdEB0;3Z
z(-+ga3$|^HC!IW#DL^N|=eT2{n-^5qYsYy^&rcbUiq~*t9=_NK84Y{Q8&4&ZD^Y9q
z{!lqSL0O@a01H6$zwHDMBEjkvu|d)<$R)tXiuR@5OWEd%)%`^pe_$(}?>3v*TYk+9
zFgv3Yma^`*VDjNfg-twZgy>VK0FN=}XY0s@sqG^AI`tN!cGxc7%o5a^^K=;#f8(lX
zBR>n_|FxNYK>y;nBC7%RC_QEBRC4}`<@YP+c;(S15g^9;<+MY+*x_k~d&Paj*h)1n
zr_$mm1Mfx_;d-Q0Th~)!Shr!HySaVTS%xyz_z*MfkpNTl3rorGoQO#3s}P=f-Lqk-i$oG
z20kxGpWPa0nB@aNG@#|K8UL#yfALqz3pZla*
z7adh$)FW`1UZE3;MZIIxEW2fJHinr9sGv~qc28D~#f0|m1??W7h_U=Iwd){wcuSnY
zM7Ab3VvbrmB=7Ei+7^t%E!J0AstRC1lOQv{FzV|pVBMoOp*?_9D`7UW=0Pl+q!e`apK@4nDH{=ydcDszv3*#w(HLtBqGWJ{sFU(8o
zsD{JdE$~=UfF#!8hGGZ#eg?Jfcb8wf>~CcBwpAw7w=cGTLD7>$%p^Dq(^D-DKIpfN
zMkrbWf5liYn~8eN>etCg^-TfV3mQ#2#*Zs{@pfsGIk`NU7Y4A<=XWbWF9fR>WG}4v
zhIE8}!#$3=tqtu
z;GuaC1ZO$@{pvRFK7^1kFlU1;cJde%jM|;%8^gc|%wJ3S7h6hPmmp#dK!uvwWcKuy
zEfG98Lk<@XcszxNbQ-p^FUBG~+vs%;z)wbzUyYbi`F{M_1mBeiv~J9LB}bi5YS`=o
z?sg97e@#f?^fO4bsjB9+Rkb0{MCBTe%)XX%*zJQp9TLeioUL@5DJFW0ccDTBu4w=;
z66$ljJ$Tmc2x(V67axK5S@0Pkbu3jL+2}`TIe@_nA^jA-Rk2H+&k`hS(#&tiuXf+<
zl-ZYHye>H`c(vTC{9>N+>#s49a=%&wI%)8K8u$5fm_%X=Tvztg)Qi844=V4^rL&
z4W|TXCHjTox~%l8?E^&XHl#Jy;?j=|CnT(_
zRoppmboC+B8gYN?hU7=AWOyhTutk$O+p`MU!n4wv6B?GKxB$(c4e3}ja6c}%HWD!P
zWblS%z@K!}I0b0WVKlF6#PDGIv4TwM>m>ikiQbn>V)z0xgxLEwQLz%+YR1!U(Hx1F
zb%CYaMemzs;>Z&5JeqmeFp2~s`!0npW6Xc>kNxwzAu-S>tQY~#4bxgioWVKB&u(xi
zlgpbJHpX+cS^6Ov?dU3|Rbm{&0>bf8&=o}ysG6M$`|?fHJi|mxS9XJ19NJsRXsv$X
zp17_)T*emqk&uhB!x#|J&C5dm|BFw(5!|U$93}pw_FIFy_mM}jtS6|MwWP7%y_*#`-N1_ewn*iGcKSZ@
zP@$qrRj&x6aajeqI_u{S^BPkBO&n2!q|wEzBlHs6It}%&x__>0d%0-5f)r!7Wx@BCL}h@?A-mEIU79C)Ep*r@ktk`Q
z;&&8xI+#<;O*&HIK7W_9(!0BI1G!Ad>`rfLYUoRq$l$^brh-0pf;9oA2LJ%`a*T(c
zMIvoAUnAr1&kegMYrk$P!=%#>)*MyEC;AZ$M(DNM>f$1FD+n*_3jm5~h1ZIJ$@ZJM
z7?TV}OTJ^s<82m;ljRtOx?VVFfkt
zk46z7O+^|aPad6-S60H0NoR2j7JUrb&?^5F5TB-F?&|s)vtR`(51pMWSQO+@_)=>Z
z#zv!UqI6y%!5~3B5+*7SMdckOc>{RpQj4|;H^prE8Ea&ei8G{2xb--GHqFJd<&onE
zDyet=I4Zp8QA@OD@In^dF6s=rw4}Yxj9Crl8i(O_^(f@lQh6_CQ^zJv+?+su@)S4Q
zvVMkYT7zf6A8`U{#IT)W`Q)zEx601>sss?{j^XBnKVr!tGY-3R(wUHlvtc`*Et%X2
zy{6IqvZMmL?oWi(8U)XqOzd`6v@L~2bBW4m6y)d*73|=W9>!5t&FkBpAA$~g7l6vN
z{0MeZIC?*mZ~8B;N=kZgwCVxUc5?E@-NiBClt_1$Ff`6)2s4lr5hDUlC%N6dTK~Ba
zP8d%@&g#8go~YCCkQp^#guhY9)qcn2F2>I6-D&v=I)tFfmcA!1mul=rxQLnoyvqcD
z?Rg{X9bayIsUj_M6tp9nK*X{o;D@^{plHbbZBHHzsWExPbyvDWwdj4`M#@2Ef9C3lIM
z%;aZ`Ov{omcODl{5#Zp~p%VyiVJK=Qv9JX{(EIFCkQ-`MdUd((DpRfRly(oo@189X
zpTBl*f^(zu@$}|^7lT~<9#ei#b^1@|v+hLCf7_0@pnJQ^Yp|8x(8x-KO1$n6gAt)tOBR~RHC9t2Ak1puE8GkN
zwHxEsa)1bbY-{u%uc7b?v6|RMHuyt^1Ng82I~i4`aZ>&l1oQhWGFQl8KD71%WnHzBP=gl-1w!C#d
zqpRNcZ(Yys(XLa4_=dTkR<)Y@G(tutRga3L1gk)&X3lbJT~%{xPF7yA8tW)tpkP6c^SB
zWVsJ0u<{}r*`EfWHQO|ULqjyUDR*qxyqbdE9=1(^J$J(~xHuqcFpwwCw>VEt62;t>
zNO}!GLn@fd)a=XOzobD7lZM%!swJcsazK$5m6UXRU5V%
zB$>GU73n*!Q;8uhNi#$~GN!6*1f3n{yp6um)4S>~ykDb^$618JXZ`DLH~yS;n~uI&
zSut4b+{^f;LbT6AEc6$E-QcF&SytvFg|fq%LIyf4l*3{;Z#)xqvU!C&D7`58AdFm`
z!D;E>wB$RSzWAx~Ka0(3X_dN=inP2_Dy4YDtImXGO4(y~lD~B9vuwGl
zjUC|n&3go4!>^rgD!#na+X6Srs{!qe@4-%SMbI>M5$9|&Xge_d=`f0rR#w%v?#d9b
z?`?4O0~feP=%x^ZV;%~XvOMFY$eeuS?~>5pD~PXM?CKKhMztbkm1V2gG60O3z|Zn8
zmD3s50#b)W^F$cA&Z^Ylhy1^mQ#?^&7inz9R^94R`o=`!Oi`lPtlSj*<4}b_@qFRR
zA+!k1&Kik~Bb%%U!9AFL4Y(bnmj3DDQ9!xPC9)SEh~Agq>!)Kr%lMiHYQSoUMS}Q3
z!>DPjcCk#4ufpvJWO$2r0Zn%y#p^EasNr4<*|LDi2Axahwp_5(iI9*^q09mH6CBX!
z+===rH*=Qo(_h_#Aq%Y?+{&ib)$ph&i~zIb$j|U-yv%c88gW-!g5g<=g=!veT!jc_PO4FR=weq
zBAVVGpMXNX{oxH7wU-4AY5~yz6))N4H`7h(pwo_#LaaWW$LDb~@qnpkd4s3tKiX#W
zo>=W>$?`~MIU!?i%pw5ldLpvls6BK#xNc_U4I~!l!zcze>3Jh;TZ&+Lvtaw)4=rh=
zQDuRRB|vRqph8sjbP$4z+26f@mmYw!0KNP@-Ke=-l_<+H@ALb*&;A8Vm}VrpaxzN%
zTGlR!y&O`nL7+SLOVElc;1XlgY-)8Ccbs6ZYuv6n-1XwKyE#+F)b)Z=*G$q-GVRxI
zfYo
zUYDg*adOD4RoVCD9o0{D-DRBTttW&>@N<%eZ@Vvc<&pi!AJ+=;$shJSs4wKhB
z7&C|EDI3s&Z05{EYiZfvt&?spkA;Cd!7SuCWCpzEYQmN2B0QP86zVQ>9=L^%-2EG^
z1F+)-b~xO&{n%f|!9cZcMBq@Zu2q3T6k<@D6XX)SnwwhHlpgRf8A}xAU+NUQ{&5ug
zJHUx%*UzWd4j|tmelkqImz3evQ3+*33E#fCX`a``iDXB+eW)(COa9;tx_*?fqnwrN#H*(2q4lw0d
zZk*;tX
zfs{#w4>ld)(pgIVE5rBd@$Xk7h(k;EHMw9ZY4$Wi1Zex!jOXA;-izXEi`BNa*x2D!
zS)_vph0|W^^Z;d{eTEqI`tqsh@hiy|7n0@W)~5PN4xZ=D*V4YHgle78{f8<@b#yQN
zJZeSxs)Pr;1~f6c_lyETv8GE&DxHF`HWYZ3%8nD6P^mnOo~RF5XjnZ|NL03H7^mBe
zFJwbwbZm=V
z&Tonpw-+%7Vp1oKBRngi<%r%M&kD|jEie<8LLcn9lHGZ}(%G*j6N1foSS31gYKgk3
z#iy&&(u8jT8WH^u8POgBIO(m7BBZSTd5o9!973mLz1l#TRZ9m=fl$-Xf&~7PeZ$N@
z=JJV+%i+CTT-&M_>0`{cGc!0+F(cVb^q?m)R9lsq0fpfHYdLkA&*KRs=@yh_lV-vm
zIOZF4-2Jg%t~3E)i1PXJG^+_lA{^V?j-*HqPlr~aDOrf@eMK@UqicrQLlA+mx&L%#
z%y_(b^S~~tw?B>d_*s+lyT!8vB7_mIi^Dn7po}@xoRD)L#rjglR1oMPFTcpU<~FX~
z{NWXD8xU+*YJ4v%w}#(9zVBUjuX7WFZ>m??<)2;$=Gu#Y{gN-pLeKin-d2B&3!&XU
zPF}_PWqRWM&a}X&D9K59rcVHBC7|Qv)`pqx8)5%`Z@Wvt;s^>vevyS!YmLX78}ThJ
z5GuTXG|l@JXdTH5qnlX7rD3`?85-{)B}XN0aAmnGTfSJP)s{RyC}lDAWaN)+drN#v
zpJ`;c%58WFHZw~SmPp!MUuO1D6u5d;^&ZuYydot1>>%ET$gQ%*#P;bEAgVZ=kFxjzqhMPyn5deW}3mR$r(`?Cb2
zj`_G;(G=3I>Xxj+cAkSAA5bfYkGXuj7+0^T*i!WO+u5Q^+`M1#rKKNVEhdjsAJGuM
z_%14pbsPLwh_Lq{OI9WxN@>Gfh{V*y$2{AIc3Mi%QxB-!4jjf2M~lL&6xmi5NvHL5
zpok&A(yO$^W~SWN|8;>0lE-@=%f@}{A;Gu0=5U$!2;ENH8)shn<+a|$&+@RAXwR_)
zI?DV}?`mRMR01Ka#IbMhI*f)OD3{<%C4O}CCqx6G->oycl(ToixbBc<
z;el5DH<(ju`OXv~7Bv68=jD{qrbYcE9AEdZU{~25{b?DQ+9m}?F>-9uM)Y>Jv8ADu
ze7*Ivs2}@cFwr)wO_+>LsT-dZV0X=q)eaHZAdG>lMLvEsurO#c|9?tcu*s4({>nf^
z0g1Wbyz`a~LPUmUXu_gY8hAbENH1+^xV^|<^dg_WVFvrj--X+m2W$b)J^7ZXr>r|*
zaUwV)NzR$e^gJzoV1~dpqrPzidq_Rt1pl4U`aVagyKIkpo^UVmDK`dq`TD|m!i>m~
zuS^)EsT?)buHIJ{8=4_4japl&9*u5~5*UFCeM8pgp>w*2W5X^QX5kw&%iV3}vuxa{
z*F%>|E@PqBv&;w;F|=`N%(@@OST`KgXcaKau&(w!UVDV|}ZcKl(?-j*lg
zf$xoG&w$BeJm^grF2h^(VE2Z8wO6e6(nBPh@S?38vjYU9=KIa!<%Xo5e|liIw?!Zd
zU9n6F`P$B)4K&fQUY@X|>JbQ5-rM0a*||9gY&DgUy2J8*F+>3F9|X|~rWB21bMIBK
zHmRL~i@g_}aSmcZiOS1SMS?<%R$@>U@#Y+!IQm%4qArMEU{k_RM}zeEg{qX+bNGqK
zhtJ;fuBYTxQ^G!QRZCcyNE(+>Z1ItGTZ&0@pX1_-)Y>srj?)uwJrTCAmhZ5XVr*HQ
zO>N|9*nZIj$k^X*JzMN0I;UWoD3LG(oCm?vB!3@`5Frk2!hwCuweQDT&d
zMLxgV=j#U?!#Bc})6RV2=LKR;`yX62=78=8$s#aQi>1N6^??*BnzJaU4O^ZP}_
z^GMHDnC8^V7$Nar((cHNsAg*4&J_LH11TK7on)7@rgruWQ59
z!#P>cf?EIE;(raQ%V?z?5TpY-OR4}@rlnj2$}H)jt+#jt`oD0JlX9)9Onk-4@w&|%
z`+i&?{=^c;VJC2Wh+cX>FVi5%JN-UUsW56KMbFH_3qxS|^L*!9+kSg;2xJ<*IQ
zrraOdyX2h{_DzhDC|JFe76LG#dalX8842Q%&f7R5jAFG5iJ1+~JEr(>DjE$M&x=*%
z#~-4iqv&J6C`j&tj@^w2yEk1N2jSB;NFnB2D<&|M^$1G?6?`XbWzJMv`JIZS_lU
z3-2bgs5y0|$5K;S|Fb$~irOD%`n%5+FjeTCH3(8(>TWn5FCSoYO
z(5bKzGx{c+VVSETf2V`lFom%euogNxs5Og3YKvKcL|c(0#5G)K9qU-_H$k`yA>h25
zXZrQaYe>J
zMU3q&$_SVDj;d5iU^u9%$;?z{M1YimqRANlj854aZjs{xB+E6T=(^oFgY>^Aufn4s
zVwZ>Nf3KIk#s4!%y0yt7?G$D)ZQMYk<{
zbjI*wI$IE`Tte}Qo)4oFomG_YqIs8DJ}wBRq)7NJ6q*y05hhDduey6cziB24(@j1q
zOdLH2@slH;JOE|MF0PILRtAz#b9viI4`i2X2UPHSG2CnEb2EnR8
z%vEb{RkyqTJuDDg`@<)C**#ZfDGcAxiMO?|!HY9EMkBqtZ=g6@OS@e1V|)tz
zUU=m!KvA8yS^0Ld@4YHE^#W^8*-kwYni1%OzrWHy#S6;bce`%PYK?%AyG~`1ZGD*o
z{g1(co7L^+$(J5_PZ7TvodK^~)r9ob^DlIf1iwrKkZFM~VqxDZS0L7QjlYva2tsz_
z*0a0>RO{;(qtV;pohsS1F9K3K`8u_%Zp2P+&h>OjA2OAHVt0ZjUv?eKp(U1sa#V$K
z3gA~K7b55YU;aQ}hlTj!>N1-tb>WZ)A5k4J~s2?u@M;uNbpJL;OsA
z>+2zn+t2naB&+;<-Ac&Gs
z!9Jdccs>L=$ao+|^2x|1P2CtnU91j)N&vf}pR?Yg=O~4y?!!~G_OR!8oxuyA_dTs?
zf;_1tx!W1lh=bl>p|lh-!6Tv&{E|~Cb0HZS@F5_Ct3rTSyQNpwHE7SK~--d
z&4OjqolR;Qp7dM|c$0*>2qffFB~-x>N!(5ub~gK<6|_ECJT8QqXVrR2_Z20K;D6x+
z4v!Zf<~N&%5P1)FY#B4b)(k8I8_Ua1;Ve7z7A2P1j;1Aalfw<9aTL!F{U7!dqA37V
zK&-#^8mM7lYG(Z9>fZzH@Am@ViM<#xYN-G`_5#xT;2v2v9g)y$zP@W=qjen|50uLX6yKeKbOZsiFFx@I(^$o+O$f|I
zUQbn=@kkQT^JlZL*3e7R3MbbP0?;iF~
zbzGO7m8f$c#5mo=%ze9;je7d5pjzS>LT?~NoIZOM`>*kZ8K6j3QF*@sVj+*Y2v5Uw
ze=69eOqt05#I>3K$4A@od-{h7uJ~^QbBcBOnQ3j{kFUPrC&6K`e?42l)PsaPP8{!#farQ@*=JRhg8KWHI2+CK@%>KD{
z!`cfKBI+;@eYF`w_N3J8XDm}1a%RJJ`bz#eCibzPE!6o{(1ls4H%k(q5{C9uGqkz=
zsy>+bohYpG8U*@fFSNn875P_pA8)l3e~HA5Iu<1?if5Eyd^
zwi;>Xc>p!P2Vd2rDh`f<^u@i9!|89x6gN0kF^n-!=&wEhwO#c0e+QIP7oSKOR=Esd
zf2A_>;uIT&e1Ipg9@G99;yhM*H9Gp5++QxNLgSY4>4mFeJZ%%8TWtntMi4*US@6ZG
zp>&~DUh(pnZU}U}rS=Ea4NK9+!D-ud8N+wxd05ZReX1egP30962|j(JWDxKYu(a&v
zbHMlPYsWu;DDEVrD^-TD;d_mL%~K8~;T@W6BnsMlgHr@!6pvElhexe)l4}I@{Y40)
zlUiz>qkb$|ylKh=9Z7W)WcdHHkP?N?Nqg6*RfD;a!?P%ENfu;%_(++$wO0=euvn*2
z(KcXhki1wgnuGmt#tZ$34GlY4K)^SK{fNZ=_JBdjj8`e=|L5SHyE(TJNq`VIF{ZhPF>3HRyIEcb*&qa^riawlj!h~RSxjv}IR3hj7R5>6C8DlRKTfj!+0r}MqWO{BkL6ja3b%q@Y
z38lf_(QL%5XT;4Jr#LM1n2FF=R@%Vg<;Tg9Gs+*GR31dc%RfAcN6;y8kh#|-6sgFVx=bD1Vv4xiE4f(rhj^OZE5ijvl>&i27W!tWq2Fb&HWmh@4#*!HEXS-U+&L3?-+
z-qw=Itry6BX3A~4THllwy5t*7f7cfHT^;>dwj=(=FtcbblzJZirnDJVCH<%kB$3HX
z3vkrYVXYaE$51I_U4&l$wud+^1AoHOG4!v4xe2(0QE_+I&6Xm%=xu1al`*$
zFHNm_NMSRLn-%tm$J3d!Hm<@GQjGP@x4~2?1t1oG(}oEu(beIL4$qPbr&`I$~V}
z$Hj1>IWc&?;N(PuY++3Rb8W_RtT24lf>Dx0VnB})(UKUa?;>5J&TM91NLN)U{f0fv
zErZ*G%M$3Sq%fdlwZzEUjE-YAG^4lo)C~u*t-W#HewWx{tB(S!C3!y}tfDvrTF=(Y
z{i&6m=8K=SA#>yb`bM^`*f^R--Np~qb!2}$*&HWi|DbZ#011*)f&{>=N^4LG7D7FX
zXu~NAG*=GqiLY=EgIGY`9;gp0YIP4OJo$(A9R!sUqN!&>wI;#5(}QuKK}4)!9mNH-
zS6hE(($ojmO;oDzgP@mMX`Yk|NV!n@%iDOmWWVmD-X0qG$zFq^gJ7sMr{m6z_iCQ1F6E#gP>o*iL%sU=uEZ9h(2wcG|kD1$|7
zUrMt&NzhlK3#i4KEv44!05MbLIiX1_nl*2jMR}=gvqURq{{
z4k_CPfEEQ(`eHonB{4B;8p4fw1848Da>49hxA=m9(8c^wH;pya5el5iqUBfKg48}I
z`pG!SySVMY4l6lC
zr9`W}5)m}G0hBLrh4p1*!Qg|oeVB8+i-kfGuBm%hm`2)=`%e?;{m3K6BHrnvgcP1I
z?Ug_jt|t)3Y|wFHWrRdBMcVoZy(}Ko+1%iw&TG=>&>P~1z88S{`R&bsL)PTzK6mo1
z(g{`m8bQUyx9b)7|KLJVOVF7N-`D!rB$Wy4(ah*u*WIDUX<9gI%`jw0a)tmLWPH&D
zHtsjMm$HNDGu??H%3JFA#63JTYT-&%w-^t38nd}^<0f>Rb+20M_lS`bW=!*j-A)!L
zcP4eTHvZy15jJ>}rJmLj?aU3y7-a+@SDL)~Qjt5fPQ_sPEV~Bp*U6#Y&Mv%E3)>Jc
zmiyvMt|RyQ3{){!(qJn)>cKnFhnCV?+RNk~^HDnKt-ur9;}&csr3K0V99=JWm52>5
zJ@eOSH>h0}i!);j58WM~k;1ldON1>+M}zw)R0Mi`G8bTZg4tn+G~KP~$+mt!h)K=4
z5Q(U9@cVH|CiWV-qij6J2RS#RDjC)q-4vs^L%^p6?ZTIK%faQl?Wz$two2SC5sU{q
zL#~H@yP~~<5YZZPsF=M9&>ef6-j9EBV~E5qX@XZo`uw+rFoeeT_SW0rC$~H2LK!oB
z?}>i2*o|`3&`lf-tzpOXB*wkRj~ONAv>X8RT}Hdeo67Qb7bU&q_=A!Ox%^!&gEWGyA$7S(1SK9pS)?iiluBwn=Oo9VNnCeq1cTgVySj1OIMFlAA5;WJAW}dO7ZXU(D5g!a
zI*u>em+f=PI1{-6Xpj@gT_WmUBK~-U36u~g-N8a0G^zYa-AF2AP5L&mQ;WHpf
zy~4Es=YEA*e9IWB-3p`X#}hCEgFpDwdMkT>i6>TL7>&2?x;5-EK{
z#%5NF(-5$SeIgEK*#Bv&5jn?ImYXj$#51-aHJ@nWSpmm;0a?L*rh2`-
zIohlCQTI`AN|AZ^r$U|lNF|sC$|BEBwv@V@_jr>W^mithg%eo3YAU0#pH%VU3Bx!Hr^6t|?J~)i>1y!`*bnf9yrwp)?#45bk{M!QI)Z`{c!J#W5oY0wV>l0rRexF+4c(UtEY
zJpaypW0vCdJR3
z*{Xg9(+?xZ4g<;VK;hmIjInMO79jFc_;!ULqDQja)|c%S20%MZ8ET@kG9>hLb=EUa7=
zq&h#{!P5yBvX^R#mJDqJFEjLTj^eTxc&a}BMH>>N^H3lr2FIPE6b>x1&>(MhC-|Fd
z0Z#V-MNhwmp;vSyr1N$Q4DmBu-?uh=8PkKHp%F*bw#Sh9
zsqO31AqK%b8W(B|_>+Apw7bAI89xuiEp-vg?badeFG9NiFqg#N*u6r5*`+
z8=x(~VbcwXfPv9F<7Bf3OfQAKyfCe0ggwN6#>
zg4`k!VoC*I6;Ron87e(^pTeofgB&{aPDfJLCv&2F5}qj*yvLK-+iB4-&x3<%j96`;
z4Nmz&DLC56jjSgM_j+S22|V`E^Oy^`^Fn{yDB|(~rLd*vH)I-{F_WIkzAbn~<*&-0kL{1IZ-Yv(;MK~@vR
z+)2dn+@fO@scgSj#!eEuRD?sgXVcUjAiI9=PTz-qjx8pC1N!V`Mr5#33a%OU2UFZJ
zqdHwet{XPj1myC#BQnx$vx6FI{2@G`IS9*G^^`*BT&DFbOFaoVw8Bb=iJjw?mA^lc
zgAAS`>=}IJ(Z~$V7=M`S54s5M3RSbIYh%&wDe3CF=$k3I6Xi&jza23HQKQq7S1Z$<
zCS#Yi`>`{_wQB4@0Xg~U@N!})k8^w{tw}ap9(FmI@clf_j6f
zR%g>s!HfI28N^OJOTSEKUa%XzH4vw~lXZt0NxaWQ!bP~lw!gx
zVe$CETd0y4D&{E`WRZ3VAkaSr34%X=~cR6w!OgNAJ)G%=KJ0o@0_GyVb?&
zFW4Lye>D_WT@`3r#=|-fVtHpA?Uc!`4{N&Sdhi_(nSr`AnsOo<`YAyJn!Kg-rixo)
ztc0)WfTKvSMN)*vBbddoFyA+d%~E%4huqZ2%|XL>(zrfEke}cxm7xIIa!rj}Ag*Kk
z``Lt2=-mt5dL|{Dg*so~PKCgC#KiQd>a@Hp6#)c^^h+I}TIFheZDgO(oIKjJQO#=t
z?G?c5m*Av|p*n~EzBfagT4pu506sL(<9+X!Z(pu7(9|AO^$i|Bt8}y~%J?~4jgB;L(brPbZGf_v>{+
zvFcpOY!1k_h<}HNk_TNjaA-O@PaSAkv-TMdM%nB0N=X)vPGObe6`i@2`N;8SN#
zdn1euNIH_D5GA;nj%{Xb%nstaCu=&wqon8Gz#qM8lZ0XViJjjzivczNC0M%i
zH+u;%hBGem5mS-m1<{FmET+a_kQ
z#se9I)I&&XF8yXhY;P0)F#8QVZVB#`1Xl@0Kqy=eYR(WrPi$FB4L2_~6fM8!8{J89
z*0NGIQ0j)^w!>0mfxIoTd=XrHBe2=jCJU#M
z9rgV~5Xr%)N(x~TQ`7;1V8npZxTOY&8CY}2ERa#rX!hywLGs`;9tt*^8fhOnmQuv!
zj1Dofp}Y!8p?|T^ZvYyJqZ5onYnJUcoW|LX^&(w(7L0y@S_;GD$+6lv=3*fvz~0E2
z*AT6+5kG$M&p!yfx6XqkM&cAsH!E6Xtwmn>5qk<*U=dbgw>8QZKM5ys7^z+
zI3A8lknS@oMfi*YpRK~gaL$1@ggg0JgsR=noFq6U3=`>yo3WENtJTJSHD7vFY0-Dc
z-6ilb4I$Z&SB2Z1KIDnMOSHp7R-!OYLBfW7h{ezA==`4@khp`6MZ6=7bcFZ7p%n$3
zm5C?$r%#;&`T&GttTWowBEm%wx*V5!>cBC#wOZ&f$PQE9MPql(ZHtP~j7
zv}}>$1b>iKTGM{6mid(@>~YZdSlzC2u^Lu4p(epWcI-Qy%se^kD$hhMGzk6$S;vnIY6cp$Q)-+vzVM=pbJQdSgB7wh%-ZraCROz#j-WqajK4p%
z)o3Fy?-)=e?`SlR6p%HFlt#X*ca^b_UIf6Y7-kjEjuK|dS|KwV-|$TjH?78u(D4-b
zh3wV12OFB(f}<5nL5lj3s~He>Z{Jv`e4Q9w`9T+lWxl{9>PrW}f0c)qCkCgb?k8`x%{!|}Wr6KcbyX$tiApA(_GwKW
zd5T6Ui~ksmigK7B=118(SoBB-ZW$uDvqvqQ)$jMc7!wjTT5$_Tc19qqJ&HN9&;3CTi0Z=7{
zkj6*U55KHFihdOzNp8~EI$Jxt0B+NfB3_Ib3`3=4XK*D^8DD~Wq5mlvw@)WNVQ$R;
zP0YmAP-QgTMuIyx0B5o5;f0(SiX0oN&Ub3EaFaI)4Jc*k#WLPTdhh}quAIZe@Neq{
z<_a^n{6Ao3F&JU$M5@-Ul=Duvu>`{olC)0C9UWEa=6?u%6
zHcoZm=m5N@glQe`!_4-=!_=~-1fru57a`%(lbvmuOt2pEH?1MHMXywIV_kRgU03sH
z9rJc-OjJkT(`$nl``5mRE>B>BuTjBt-e#iK1M^~uYjRpI1cUsKgSKRDw{5u$-klK(
zER+63dsRAU>y-4(fm&0kN-JS#L4^!L$?C?cEDGMZI>AsMWVgA*sf@xsQ2R0vPMdMV
z6&EJrA+ziRO6B=v{a0?u3$$g&~OA7xdsKSOfE{I{G6+
z8p(Pu!D?GgGmHJ-rF;(NTr>4AoQRhq`;iJa`n)#YmG-LXjzld+FBM2cA&6QQ1gx+Z
zKW*a-sYYZO)&Hs=9+ZZI7FkyyQs7}*ncg^W^=47tl%J9p9hBDjbpx>h#UC^qVveES
zV8GQb1QWJc%Q-7Y=cM5*)aB)*@%J|%f(uCQyLR2JgCJbuCWAEEp9i#JNxxzv$p{}f
z=|>mW0)(WP3wN`X;)*>bDiU6D*b>XPmujvKsw43q8tOEFID%TSWSvxF(;|R^pI8RE
zu;+L=7Tl8eJ}DIr;)g@i55+mj$_#h!gB2$jOL9QV8@YVmE^T>+SI0zEHEBIseKEs*
zlqs%Gw`7kRF$n{vT(Z=(n68J>C2tOMcBR<~=D*gZ{Z42);BnGR3t)2e#9Js@yKWL4
zP<_z*$m4cLonn$Z$?kgmUmP~~T4V28B9!zU`=lxt<
zM~2fvzxS-2QeLWD96-EYFM%PKkFN|?uDkY4fYqor?ZoFF;UgOpvhr-YcvI-hzp(b9
zg#@#PyN?0%!aI06Nd0#?SH^ULV;Fihfv!(EH6R|m70UC4cGbEvVH34s|IXA>sbO%t
zJ^bo?2zZ=kgGNRJQ9C-^UjlzRlVUe3@ABGCW~#nK=*;s+2Qt|(uT?Pm@b
zZ^Z@L69JmY5<7REU|*oN+Z;At*2s+EaMvN!h&
zSKywEn=bo)V|*0Ivf~(?Ki5q2cm1D>x6AL@92G{SYb6|+yjqammft$3-B4W0z(r@q
z|1ynM%ulrgdVV|g1YHw&mE9&O9+`lCTKXHEVnTelJIUdGtER
zwl$z&^$L8^2aA=fj5#lWe1nypmhtab&rSZ`GRr~{P3yXsJUz5k0vv^mLIa;7uh#1I
zrb~(|`#X@-ynX!%MIjW7|6#I!jO5PqBz!Ei!&gTc$D}FK{J4zn-m>c%wd8~eN_(X|
zJS|Q#>k@J3&0~zH+vJ+i$G=dB2l0*RE-Cj{BwB7}QQb0|Co|wpmF^0(Oqzh5UDlI-
z`uZ};+nErW(kmgOe~}JJlwr)*bTtNNY9NSQp^2dNULo?;QHhvAGtO#)^q#$NFBL;s^
zI$sZ1iCp(umaBX8@A9r{nHjbCcE+qX7C^i5Oy3W6)j654Q!NCAc$WqGsG*jt4PoC*X+6&rzb65{xPx@3YQ=xX
zy1=yr{^LxaNQ4fBySrTvWo}E*AH8>Tpjqwr`P-jNlu3Q#U=J3K2a`)}5PzL`bekFN
zeZ@%2(7`%+2YCc;D5)|!yK$qlU`TU*cqcB)Wo!tLP54Q)(Dz*IoN$yw*X*USWlqfi
zZb^apJ8HYTX0g{wrfj{O%eXEPfJ(^}frLNMm+&d7WC^J=zeN}?Bmsn6DaG3qIqX`@
zsp+f0-XcZ`{y&G^k{zg!&nze_eDa2Xj}SZJ*b$3{HE~AmqMvh6=s_!x9(I*5VA|XvFdOWv-t`(=@dYX1Rqn-mc$ZL$c?w>+_^yfawJ2Ah{bp*GBQ?z3yTF
zP}RNH1z83c%WOomt{;9tseSIggD6FbWVh{Ou;PxE3(h8
z*QcI1DLW}XIwQKZ!_Au1c_h3ot{@Ju`}xw)`DPr=?ya&QXi|+K`e|6;!npI-ZU`1q
zq2i8H(oPH{O%bgbi~^}8zCpAc-!xO)B_Um=^Eg6gtmNX{?+f1(i#Z7B>ehI
z&ds$j-vkNq&Lt81U4Vk2y!3S-ZTm%{u&54M`~2_ii&K<7)A`PTM=3`;}GnLFk%6s$RUoSf}}q1aM1bJ6Y>p0Mgp+0XP(-HA{3@Nk>h36;9N~De5*EX
z^Yq5g5cJOwd=f!Xh`?rz4T1Z)jUI&_f$5Ok$Hp%PtD
zBi1<{2VjSW7_g|L0;pP#@sXYP+PFHAGlUTK^?~9{gMuvv@yB}FyQ64o*wLR>Z(a{X
zk^<(LrUkrrw*s2!s~bSkMcXqvxkap39FJ({iOkKCplq>7am?_nQgq7^$~ZVa?~7ST
zQ20y%CD=!D4eMs(8*YzMXhFCDK@O|+I`ht4A8SL0WXq-a+4EX^UfJv~BJMZ5iryDy
z09PJd_uj^lfaTvaO|_3E6riI+9D)d}{2H^wd?1|_f8LJ9&ETs37;0eBbldHv6zZg^
z9{by!r7`L`HnXuz@nOMP2cH9t@Qc{(E(3%$_<Iv=
zAPgk7_pMA7QIX-7So=GiE6F&3G9?`2h=*U@fIpYw;&@nRw`9B}$Qvm#Ghvs|8if>3
z`_=s{&doKCC)5p@lpKS71}4$a`HJT&WHGQ
zZ=9ZDF9$U3AkA|xUJICZ+m>wAW4$qZH}xa1tl2JpzL%w}j>#4hjTI+=A{XXy(k|9Z
z{Tj`jo|4pNS(<#50&hWZ!?cbRlX?;WahjJz0`;X2hH3CuG(YM?B88fk85ml(@GupN
zV{4R4AuS^BIMN-)DPmGgUl~5P0T5E+`80Wy=Aa4GdTv&3^@3aI?p$Gylf8L>5$kyj6?Q3URWNh=(uVLydmfnu-DMe~S*9y?DCX-%BD5=Aktn{n^
zvII7nx0x$E=4f7Gpr%+VP1U;VA72ai9D>=hCEY}?k}U~f!(Ee*Fa!dJx3RYks&Tw+
z_nGdE1}*?_V>rhh79UL`RR~+AmP6qz%rMQ6bau$(bvDt0!7}Omr_AX
z#LQy#@QMYr{OdNbf5nd*WdYlf8^68Gge5AJU)~mR0*FtK%^`1l?d?@R1|N-;z{PDy
z6z94;W?E95`~RlzjcAA=1eiPVFHrzwjc$yBzAI}Ns?zxgjxgqXNaQVuW+)f5Hg&fP
z%XZl?=cMaV|J6TlZCcu;l%U+zh8#GGbySBRQyC8HuAg(&CR>bgf0dCZq>I#y
zQ7G=+f(mJJFtnE)3_4NyIA6w=aeXxgCa9xGJk!hf1A<_=|G|0GP);fHl^Y
zilMJ+|12_o}A#+UXimBc7#`>7r2p-GieVIVDjN=B{i0g4B=BdL8@3<)|_=Y
z&XJJCU1bJD{?+7;Mxt0z8nhWX21O<_Gzc`kW<=sjPF*R_XLL%Xam|`0xO6VOsN_}e+--sePZfXZuaQ5sg=%Yh#T$9VxwjoS#kbTTJA>1*j7xCeZjuAmLVR)5g^a((|
zntTr%%ek#D{dLh2q2?0UitVB!z>@ZBTc)Hm>sb5i!S7?)3AL;A)ASlg-Y?thZxo2VzqhC~^)uH`
zN0ev`09z}VC1V`s(jb-mE0x~a)t))6$WlUmjfk}B>`Up~4pBSybe=ubTxv(8`ECjQ
zIf$X!W>s_rt@Pi)!t85W5uirw*nXjKvSv}T==(!fIiQ#n9GzN>_w7OVYdW^lWRNli
zm8h$+4#=AdR?Dg7G^907^#IFaYN?=GY1=Cic6+sh;}%xtL+=`Mw+>M$1FJ7pDx_SNP^_
zF_|huA@#|X-dCwx8|+6^0QL)V5ot9>vLGiYD`0u(gR^Jp9~)o34LyR*$r#q<33XIS
z1^zkKPx8usN6UetEQFOD8IUJp5eN?Gl;lz|7LTD;CbEK#pc`x$MO%vCH$56y+Moin
zUDK-5u}A{iAN%g>+eI$`UTVP|SLJFyhfD!QusuLts-Kgt)pH6D&h<-{9flq-ZWhb43_IPg_!prN@<8Gv)Ky4`loW|;5!?K=5CFZ6Cz}GCEgM2xh
zxKN5Gj2Vhoq6`I%xFni@PE~22xF^jh9Qc35o#om_WWP$&d7Q;r#j~VM$uzn@Y}}{>
zJFbo;;`l(vjM*U6K`w?D&T^fLRa>|c1?8ga5Q=LLr_R0J*+FHkOAfkI01mtq`noon
zTki`s9vB%mez0WT(P0Vd^6V99!ON3AiHe|fJDT|TXY*T=yXPrE-gcLk*ZV`
z)WPh-{&0d$^w-H+uiyOte^4?QR?wyA1sM41Mw0FTs5X2ho*SAyP8>lO5UOFakGTc+
ztJj~aUT{BxoL2r>v)n`=LSq9;naCVl3k}8U1Kx0PM0w!`h8tw`N8<4c4^2#i`web
z**d9fN3WAZVP1X+v#%&wWN(h^q})6;t=5-3w6ffV(`I5dsGx%S&!a_*DcoRki*M@G
z=hMb^cnBo~1^?Gr4~;8jXtTXc(uF6ysZbH`7LU)sQ`$kl{-h~z7eOE#*lVUlPheMgav{7nYgcr^S^9Aih4G!$es1ud=aK7D
zBqmFQElqQzZVl8?=sU79s4VTvzL19fh2q}EWb{wEio|`l4-G@LEGw2c%OXuT%AThZEq14Q
zp~i?@^Ei_@{wKTw(u0M|eGiOq4R>P=|L3D?*e;ZlSaTIkn3C8T-=1DP_?D^)?Zp$7
z1f)}l1+Ip&&$541eUv`lCv$Xe?vyv{|F$KF$}wMZ^zb~S{|5s>d47E>P=H{(H-tvy
zMWj>i_0n4XfUa*L9bcM&OEYIr{ZyO661814ghXJtWYM#YfzLV;h$@qW~cQ&R8>)3B)|(@C=ZD6iHqIQi!RkiD&!
zAgOT<5$Y~F$GQ?D_PJEKVzeIewh?xvrlNg~rP4-uGeUx1mWeNj`ohS1XA6HONV8N`
zs;K}&jD%mxQ|KS|6u&%;+?|ExZ});9aUvw2bnx*ih^h2N@6#GIC;wFK?8~rNTpZ*a
zhe>Jwhz&o6t4$8u5a(f)sI9<&`phW_Mm#fdo{OJLSJXC8zVmr=Eko_N#UA98efq2f
z2$(2kBe%C8zUH%>A6KP3us}ekc0fE>ld>3j5M&AuA);he{*t1$b?GczMet$;Cw~Gg
z1y|(c(tCjz7zC*-4^lRY+k8A61y8Nlf06MZ(%sgZy%V$tFzZIKNTGCsAE+6qg5D>d
zfS8ZY&vo8YLX6~k@bUmd_p-#n_SRvKanimT8`(f^#&BEXUw|zUgfGSX*cm>!{wFw<
z*~URM4-*$(u35cOQEI(EVO)tdTl`H1Mdqp2eN)0BD+-B`0sf4bIv
za-^!*P3vd$uBdP}1}B>$*OmFt?s*e(iP6b@iYMpXSc1=#PcsEs+|H96t4=36cqsHH
z!Oso*w=U2#DVf~!r6WloH&TtvV5XPT^iBJl)zfQu93K+E354c58^jh;ui1kZt7_kG
zUAeDJlrRI~%GFt*0^82B{S$j$Q0`mufSW_Jbkin*Pg`vnY1bBa3x2PKo(E@`t(SE(
z3LXD6a6`AtrI3j*ftLGF4q+|3805hH2E$pr;Hy?QI{ZT2PQ0mb$*qZQlV!pKoF^l^
zk@g47C->B^V?MqmaC<){3)&!#sF(JPiK}>46D94(f?YzpBa&j<@Nx?wJus{x>stB1
z%8+}>=!oxf8QtfBX6r2y-_M)^KZk|Vdw%c!Ht~-8G3ya!0TdNPK);Lhq%EQ@5-fll
zbVTJz7HI7INZhb(18@HBWaI3;Rhcirq`BRZQzLO#uYSCU$?IoTq;H!CS`spqXaoNF
zfr9}cMozt?OB0of@`>#K!RSz&xcY;0w8^dedcH{XdM=R4yu&7ovHR4GVNMipXO;iw
z>BoN968*T21=LMzxRL)4(8mt5xO>q?nJWR)B*tg$;1vc6n_5j|YR=W5op?n(n1|_?
z>CFYFqF1smKU~be)7%ZJAPm6+vS)9$Y#%QyjIP|7cK)oPyF9R?TsESGXO~=%b{G}c
zPN@Pb5HgU_vzQH(eG^j_#lck=9;>P6F{TQgo6-<2TZ~4w{>*@B0e~ICwP7pB_vxeG
zVJ`HPgN1&PS|!PEF27Fz%s~8Te?+t{S{r72PVe$cac$W)FGwMJkyI-?Dwmn*77vCKD@DR
zH%|ohP9Ij+xP0+GE-zsLfd=funFsxUg8L1JpM
z-qqJd84Hg^bH&y?W3^r^F2LaoiBZZew;)5cO;^S|=0<%jGzPAHCT1-TIM0|b+7~9|
z&i0LyN(Nt3cm{YV)KNph0iDX{H84JBgWLS7%)^91@|m30Rz0koR>vY@sHOLxd}kJ=D%
zUp;ACU#)X|R+l5PZ*%&pB~8880QIQ?M}QJXflucK{J1rWrC8Sipd)9zqe4+^@?=
z%!0BresYDNAEFcv1n0ZrdCFDhphEnZMgJ4P&I>&rZ=*KP9c-7KNVU}|G5AQ#QOTM~
zTMnNS#0^iJ;ExzmXPg6BidX9|DSax6jSTQYgk@H2{89Vb<&nbNmDo9wpl?
zpG+lx2`CQf1j+)|EaQ#f^wMY&mnrnSdV#$fjx+oGlV@_~G@}Q0>k1XbJi?Ri_4jzy
zC3~}D8kEdLIlsS9J0xVmcYl-DOGJ8!)&!CMFfoot=~%$*)_UI
zhqw4l?vz`bX<1S`zZv@0Y#V0cL=}$O`bCev!z?3^Zc)pP1$Tv(FvXJ53kB-LC=%{O
z(0s3lEeaeaswjn;;=`{)YNz7v(pH{yV=@bkm$EGNg_J=^IL97ECZv_HywizJA55IR
z3tL5$l@r8UDJ&8xxhW&bQ@qPu^A49)-UW-d=|G&a0bLKS_sctQe9SmN7UyPMx$BdH
zkDY%dK5U)%Xzno!B5I!dT%14lwl#KN{aSa5f?JXn*Jr&*m%)eg^4@EONlTY;s5_4|
z_@g9gih9*%Jp3F3sk6#{m+n;yAKDOOxl@(;+ZP~(b9vN{p7MnHwm^YC#&}or%H`VQ
zumYOxd1LNe;r@^$y&EM!)Q_)LP9(W7u!;7ThF}r0*mT%hEE5Qy5&&`Q6m;Wf%oy-D
z1E;)=l^m`!(Hz^tYv>p{v4J|1Y$sEXMt(Dl3y~3%Xse#^0m^TVL@pE)y|6^yE>KgJ
zFzJN5181`EX{|+82!JZC^p@g)S(dIQ@K$cU&|S+WUdHFrc2`CP3~<7r9_B*cwVVM#
z{ngul(M~wGc|VJIVZxw6<%%|K_pP&~XcVpKpz;g|{AYFYUmguwuaPw#YznGuyC3_$
z+_`5O&-~b|x7px*m#Pw7K!1aNp6dpdV7s(_$eKaubymiv67(s(1>BD!P7?Kf)>LH%
z`yU^^w5+no;mpE1kTg85hX!xcD;l>Wyp=m`qAQOhv{ieQHxW5@{gr)!oo|qL@B~
z>_TVxvpWfAN`XHQ1E>d+#N=$yV2xEf?1T)*!(g&>YMfJBG)^-Y4i#=69|*saU|A|y
zVdNZ}ylV@5u?#JHe-%|lhd{AQG6Q8|4m1yQVIqhGz}#U1=IA@d*Sw&Usy5N_;z%$p
zkp^Yy1+#oJIY#5eEtsf;h?zsFwA%95&fTG{^$sUkyg6M}5C&-h;yy4yG9W$r^0cK9#BnT@td?Nq>d<&Bow)R$FehHsDE_&1<&-x9Wlkz=2M5!z66m)eyvvEmTH%
zA}TkOq{4rHOE=A1?7%mygIy>G%mY_%v)yyJ8eZqGfqMds*1I;a<*kx?BZM9nna)_`
z1Ls=k7LN+G0MVW{9bTpEIDXv>1OReNMa{1@B-x9_4<5X(
zVaoeD_M@!{b!qCTgP2H2&;i=}d#oNYnUp?xl|6>f*&H%xcINoCXXM(H|Il!7*5{US
ze8@RFRUX0xLa@Qk?e{J>$?=AQP(Q&vZxLg38fdMhn>*;u5r3bwmYruWU@*1kat=^0
zZx90Zq(r^_o;9==)lfWfDVguF_NTg0S@ugqp**m6&0>U1_=Yq_sv+u$I|@TWR46L|
z;)*xS3k?&hU;5_OQY)77OIIgorDJ|3SQnp%Y|k5*)plLyiGj=@72S`f0U4#KKo0=}
z0OaK>Ogs%>QC-FOpBi#VdJ^~o_OU6TNGegJy)PZ`kFv`V$?Ul2*y>+YzK}`bK|P$t
zFbs1@+Z0}keHG3zil4ftY&`cNQ5w2zTIp($YraQvIg{FPAn1#PPM#rOoA>)pYyz09*xDuShv-DFSchw5M@9lxcs1ivuxko~ZHBZud!h
zoQJ6XuryJfp32cA7o?1(%!|({E6}YYGyNn=#SJ7gBdz&Nuv?Z1dRk@IxC?+R?=pex
z6l-qY!dB(2%n;fvFs2s39prw(Sr>o9UDa7uG|o`6>djEN1W>=6)L@%fZ;QFbn^myu
z5EM6>uJym=70n9gZzG5ohpaW1;@Y}mIsme{%avAKOnJX88a3bX95TusPJ!C)_VSge
z6+bt#L%eLw@~Kq`AR61WX_KWU
z8BY`g?_PC*1@z08%z`{k(L(z}@sMAUv^PCG8VY8tEvgNvA?D|OM9j7F$g+C{IIL?D
zge?IicrJo2K67m#R%JGj7tZ}7SLJW!>En&qJJU_XU5CBS5|Km9Cj=tMC~?=7J{11P
z3jzNWph@omhex8o0zTjNryc%A-lAyXo&=1JJBKhVNwN0v7caB&=3@xSMp&;87Qur5}F`+gK5@6EqduHHjZ6I6~=qB;hs
zXoZ-n-2eiJv0rWIWz?@0_pu#{5p0b5*wLY=hpGHoQ2{RQbMAB5
zwpJ{Q@18fuKB8mT%>|2aTmISC*z+6;{BDKVFX1aAV=TmX`~{btor_XquF2JWzOj{+PbM&Jqfg|$tsRexCKI~+OXa`qQs4q2&S0Fja#ufzHZrZ*_bA!433+crp6UtSr-|gwWvpXpD*42b$BQ
zWTULwoHm8`j|%mhLJWNS^0DPl^+4W>j
z`3{7$AK6VkXKtHR=I9bo>mQv9lX$3I+^=?r`hn;idTD_bP`m&_K)t^xRiT!k{-a@V
z02O2QAGbNs;&&-#q{;J6_Xs8+0;cAnUnl^{4cWNwp?(6^n7i=3Our8#SaEZGEi>Z4
zy~YGCI}FzI1{F;^p3SoLQ)jPCKsOGBz8RYu7Pz*pa{!8X#*^)13lnN7rxq`>!N@A_
zB5DViAdSc+8|gk*>1)M|kO&4E_f+0QQlS#w^>?FV4P(M2?elc)+8Di)73qkp=ZM1swF*M{^X~qm@X&BSA+3MS&3rC`Juj_sA!eXdt-=9wu=TnzNhVAHHc65Y>GhcrA
z9)Qs0TYIYhx;X2b+6}^1HHj(?jAPd92K%ySWF!)^FHj2WkbPplgJuzWo<4(HG5-Gx
zhkt+{StTR%e)X!N&{54)U@4@Ae&9oM@ymYpf@*{>ZFL37@gcIg5HG6T3ixAv4SV5W
znIb*8SXEFavHh5PN0#a9Ylyf_hA9S>q-I2=^m%&`28M0q0nnT*+YlR%M69RCx!2CT
zPHZjuB?&)UMkcW?9n_~UK;$(RygAP10)|~>)-tK1$rm(s5~FsB?jG3gH8dgK_01#Y
zhHNb04_41$jHbC2tAs^MY!&y?#F8k2lTw#){;ih$Z&VeT4qVVU=#`xY?6mhY@P>q$
zz69x+80`mOZH=7=e2^nilM7fhTZK01>6`zXbpWfnm$2o>h9l@Jvm}E$w%;9{_D(}3
zzT?SM0~s$%qZrqO#hT8dk8rs%RzW;VEa&-CzLk?1f2{dga+qs>qXzc3<#NTCmAjRE
zb<{A5XYtW`Q;pQ*SQ85R%|~W_K{%qGB&NHu*=Bx!o?Tegqty;;M8X4trH14fHc9kn
zRCjW)Tuq%4W#fg9>q@q_`C((i=`2!o$YAMQ3$|Jc=9y92c(RNO{|)@V?GRWdXQ$2`oGbL456cDKe`&KSrKa
zC#_!F0WmJun34ooLt7Ja?K*HvrBo%oanWB_wM@}_w$d3)qzi~Rh=V{ApzFS4qk_3-
z<()c-Yoy#t7l^K>^5mZfW|I8R`j|H+7qe_
zNFTsXXI1#qf~}-wDu@|a@Tf)xBmQ%1?h2>qBI{vQ**&
zNQB_$GQ6FE*Gto6heJ`HHq`lLsOIef+fg5<&Vgx<{y3f#fuJ!l?U-|vRwj1u^Z$@o
z(X28ec$=kJgE?A(!15B6mCG-4S@$cWjPz{6X!>XOTNp)v;pb$*LTU8g*=1;
z-H8O!ifKdQf)T7yMZH^C%ppAVm=t+aAoAHokJLY!w}*L$g}oJx4L|+?AN~#$my2S`
z%L+Vz5U8?j#1GwTQ;~9Aw(6gysUrwqT>dOu#$zJ(+h-Ynj(MjY??K2ou(*mMXc>6B
zS82`!D%82c>@Wymb{nB7=^4ts>ty~5rIqFy^9ymyEJ;ZI}9e)vf_(uZ$J8^pYc
zaIJw6(Zzj3sC-5jT>G4A7X_9ojO{0#EY}5V@Ojmt_qd$ZCuZ4>!l?Sahx|gZmpD~|
z>Pr)Y)&NDg;OF0)iK`JoA!9o<4hcC2XogT!(oe|@1ea-R&>6RrW@hnPhVjH&D
z!E`@Ixco}k0>$Tq9^j#=G2xlt$nK|VOKbl$X2#Lw_blcH-5@wRddoX~fhJz3G65)t
zM~ft3h|qOCSUQAcijI$dI%jUddZ=AXC)-vO8mSaMd^%(4nA_(Bt79=X>HjF9as${*
zhhj?++@y*Y_*NUGP;tBf+Nkbojxib3c4W_D4Slpdf!7j7tkHAp
z+nq*d8iLilsu!s7q>*FnP9QRmKG=JW%atv)v!d)rOYCZn?u}u{TaYLpbU#9a)(a&L
zZDEnUV-2wLR5oXjlTkS|Iv$FF=4K78!0!-@Q)8sB*OGgWFzSRyO5jE_RAUL6goh}K
ztXMiqccBHSqi9%Iisb$@1lA_PKiv
zCJ?z)c<)F7wLDT2+hHpxrffC7Nep3E*B4AB2fo-d4wz^#Gl
zZN&O8FkxV(^ny4_7YvskV>EHy;1YLL|5dhHp)`Jl3x_gB
zT(4DD5+AEE{3$_bJ|)2r9ltQV;R@Z6u(YzbcKy{u?#dC~@pY0|%xU)K86SY&+8V=5
z8Yoa1<#JYak7XnBpE~dxmkg>PaIg_xl5ZjNM_@nzgzO~vbX^;wRFFFJ25`>v7M@SB
zq#z4=P@?2(e?h)`6khn7M=oyYDjO^tN-x0<&QeCYhcwLiOI9r{pqCljIi_NlrHqBj
z0Jbvfur@4W-LDvH;MCF07IkHnuNYp_l-@1nmu2g2oyT&6P}f~>5X!OGy~>OMDG&qn
zbx*?cp3}IVLq{iihhK7bt_rXbyu>E1DxgeCK+dWPJ!$j8-|GC%06#t-*JhN1MuL0V
z0ED51i|I1D*|I{5H_P!vZ(+Pk(;-Ccj>G_1AmRmMZY|F2nf{uRM@|NnvE~)}
zwb0*Y3);E6EvY+VmDS+-!gS4RY#+Juk(-fdmz?F>R9gRU8+mUF$krr@jCYR@%BEX%
zOFOOSw2zxuKBk7
zg>!+hj1Bxc9e&UZp_SfCcoq@zdeDh4v0Sz@tr}{fnaOT7+Z|?6-+BNL-
z*SrDCFGrrMsLji=m5FngcOnCK_N}BMf6%et@GpIV-ZCosQD_?Cx(zi9CO9=*-qL&y
zRvp4jEcfDL&>qc#aS3r=P775m4{)%_LPLQllS=Z8j7AJAwMnmi5Zp|7;I@Q;{8sd#
z%=4lu-t=QzQh7oicmzza)w8*j$#_(DfAbZw-)fO7z)`|Q8S^!ZyX#2KFI1!f|At5H
zWyqUlrI{by_2&?^d};$IKCXMqE}5EE-t4TO!9+e|-o^|z{N~G8XyE|WyNCmWKA}4!9@0|8+6(4pd~od!GJgvZrTC~!)GrUff{oE
zznOuQBN2%HUw04Mw?r0KI_GK!j7YlacMOzxYg<1ZRn~*m^B+jW6!miYVg)x-qO*CV
znMJcb-39eU`}yrYJ;CaYqN@;Dy@k4K$x28ejTE8xD9|FL&WLx+`5Q{cWpq@|tJRO<
zE3h_lIeNAJwNT2A`+xNRmMo_iB0k4FObZ&2E0}MMT3&+VVa3bT4n)wgflHpdbvpvsRp)7G}_g$Dx#t+;D!&
zzbWI9I-&haa&AZy?O_H(l5cqQuC~>|V4&t??!riSLYGlA-^|Sms}qWoB`?@_^YGU@
z!BpFxZ5n|2$zAj3O8r6tR4)&0O$L(KG1=Om{8E-Ppas)mPr)&(txr5p83H8eZvU9l
zF>(qM!E@v@iexLZO(6j~{bE;?i~vmU*I-Jc7cL4w01{^B%L}3%#v@)$64RR}^xkIL
zPn)yEvqkV7`DjWlc11=zZ{E^B>@m{)#&zVdliskyVQzxYg4#~;zP!>2JJ|ll-RhBq
zWL=}nnWYopABilh2y{QfCaESV>>SLT(w;K2*0Qn!d(gtLhhY~0{j8Bz13w?SLxyQR
zn4KN^vD}AEB2D05o9*7Yp$C@FU;u7-;TRR1+PDAig7eZB@vJfz?i3CGU#x7jmW2|N
z<&panU$f=JM0uhGbQ|NxMY3
zCxExS5{`?ON#`t0EdL6*LD=~UVo>P8Y2X~E{&=R%zlI9kfHQZ&>eI}XCGE!^(L(YT
z$R1_(F*`FK0iJ^{I><$|f111|6u`|`wC-)xC@kS>W0R85Y8Wsr134AggwrlC`(-8X
z4vx=CMN0R@f52EZIu__v-t6&1q_l)V6G6r18|yH<)TPTx@4-MIX!r)1?>WWmGFtUw
z^gC06H8ntR3LS~&C!GW(>0{!A{V$o80&wz0%wjE+Tt8ca@^8_s=0fuq%O=gPF+@8<
za*@t<{iO2LPxqcq=uvBN?ZUs=C2Cz3PW7rAcj6T;Y+SZ;tZD6ey6qg%!3gDi>iXc*2K=8iV%&(Tq8@Sx@j0573pz9QrP@>PIWa1*6#v
zpa|Rv?P6B>GnXQl?Y-GDl1CK&G2chl9jjje`z$qLCkxd9*;bDDX1U3sM*^`L{AqAq
zwZVi;`Evit5yF+f`}4bO1|v>HuTl%_UbM*P3_fH3-M|
z9n#ez^*kSVqkpHwNT85n4mvZ)T}CW3#bm)6$BsrX@#;?>f$dkYOW6TN1Q
z^|>eDIdxm*)xYSeZvmTml>Mcks}wP=8N{yx#QSJD-bg`r&!BUgi8VT>=%xBy66;4n
z6v-0t);7seJ%DV`!oK;VwoKfx%Y!^ze
zH3brV|Kgx5T<5-H9K2@4s&JDP%cj-_v3PJu{u9OV%@PLbkp#Kd0jjIRakt7iN5ESg3xfSSq
zfSjqs7WFZt_7fgcFAY?+VaQ7CG=7`cmbY1+v|Ln3Z#6zl1xOlIgK9G(L%w!8|bMVBam`AJP$m}
zGi=BvZ@g!=fgtSTfwqZ!|~5JGt|8$3#)l46wu(S)N&{s>U6Q{K2h-uxcy>!k@UA
zF@3Sz!tf06VBvr52;MP;3|I*WRRgQ5c&sl3aOy@-mN+P8kc`O3D5kNWg7m>QcB6A@
zQM0)%T{b6BWNwTg(5^Z|jOOqDrUvUr%$
zG))6kF>btZOFGsdj@saJoc+_e?9GCxl7~2tZ6_Dh4{$;p_yg=Pl7if|rk{Kl5bGLu
z(R2vc8VAB25uwLb>nn_AoWjCw*)4_>Gz_S6W;+${%+YN(8;XkH-}wQa{hge@ZmHE=5Z
z`_>Ako8oMNK6HBe+Qo`a5FjYAbo(N6MBqvad3>r1pl;eRE>wMpo(kuP$-8KLzOFdA
zlSU}=*Hkt;gP0JnisY^9mWZ6rL&}_UklT%QrKKZ~YQ+pSZ&1g}Q58uF&GD
zq8#6-Fw(^fBLgTu^Itqf58Id*d=fh5eo-DR2}dE`YBz=kpTGn7t^=fD_irijpYy>a1>6SW2_W>K8OERbzS$Q5dieTnsWna^4|34heiq%iWDeECSRvAo
zEB;nHtB7VGQ)Wk@6?T&sj$RFv_Kr-9wRgo*-Nflt@37RE?_``_R$3Vb4R2zFa!>!Q
zA)kNuP>I;;-f|hc0tu*{woq`wcr;x$)(U~{MwXV3aSL0u(%t$P0S`mMJl$ccL+)A;
z$`_`b<}QuDWEg|Zf)9Y}jQfn2AFLl9@YPGi=yK>tWF|NUDkL++ae2$gI_FMEpvbQxU3R7Ge!&j`H&ht$Zb&tukaz6np@$&960zyUi={g;ImW4ASP<$bmJfHlm{uSXu4V8=Vf!-z;>xtRCJ#IW?&K_&NtCUZ_~n&C
ze|<6mO9Vu(7tW4kPY^b9s}BWAWb`A9^G+NS$1Q?WvF2oHXU183fr_9+G_c|4FdfCt
zL9F)#7SSk8iS~WNty)?%f)Hv4Cza5Y@Hm^K;)(}RC@SaZXh_yvg5s3U&PjA*z{
zeW}F(;(0OQ3DL9t5Nrv$#+jO>AK@3Y-ca+2vAerhUH*+mF3gr0*pN-?iT9*Nu8l@`
z0bSrYcg7cbn^e-BdPBhQ832Z`Gr#r|-R9csnENjC2O;0=V2P=11TuO1%=NxtF(SIf
z`~v;-s25Gs)7EX!LvZ^J=jo9Hqp)*6o
zzGHMQ=#
zX|N{Y|L8Z3ttGtBP3pRiCwy}OX4`I!>WSqTZr|{z1=#&{5I6QL^1Xyc`2ZZlFvRS`
z_#>E=#Po^*LH;@JA|2>KG*n_9WaY7}e?z(XH0h((YhuC?!j*~%_H`7M<*n8J>@W2#TBbJ=Ha2rsE;_EUNvB0!`)ey`RPi62+7__j9s
zXIuRj$E0D`dcM%f6*fy-+WHJ6%qC*)R~F;cs#Sf~`#C8L{}&$$ly#n0RjHy&f5JM^|7u+drBV89)=1;o
zFnQVONMn?maM$CD@tw)+Z$#NN@)`sI2_7lLrDF?jAMD8DgI{I
zQ|2J#lgD@XAizW8WzyVidas&_oY6Zi-=Yj5izVUq1Ko`wp-2Kw+
zR^JkRblcp9SLPAX#a}&D@7p05Z9*uZ1%H<>tn#+@(7ucgml{f?EC?tba-%)rQW6mM
z)&yU*izP@Urf4RD!i4JF`>&fe4LL}uH5p{CsABnFN0b>F^D2sleE6S;4rv*M&oi=?
zH!o@W)db(t5d6;nenmJ*4UZj)W@G}x1PGHy6AD9#k!AchAEd1aL4#LNim%Yt25+WF
z!9Lx3O&k1efwv~}B^B#5k_&RrZ19Yc@Hwz?<)k;{sbND6
zAg+S;4x~5Nq&AEuQ0Fdn#~JFm5L=xl%xXum4F!6GMm8O~7lH@7V%9eCQ=NzB*$^Fv
z(9O*-;r4t>ULq+y_o68r`P?Xy?+s??)@*DojG8`1N7OX%MF%?U@N35N+!CT}s=6AL
zO>SxCxa`U6S>MAxEg>VUPIdp&en6JslIe+y=HA9*7AvhlgY
zsd?q>E15E|y6gwI7={~J7&AF&lVbskd%!#g`w0U=}q@X3Alhnsz9Cw!urUeYmVtb3AgF+-uKP*uIrE=vo%J)EjpK<}F
zUd{w)bRRFR1!;-Sh4yx%H2JTy#`L4Ie9?T#!-4Fi@TO9IOy1VA=Za;fhB=YcbtU&0rHjg|<^9OZ-`t3JTN#w^DX4ubR@;3KqI=)o{&
z>yb&wM}sUNoFW14;y7o4tYIU(vwnrWtR~24NvMyR^Q4`gyc%>EmB+~Q!`O;r@;k_`
z-r7Ke{}mctV<3PWw7oXmMqGOFJ_Lwsxf1sbd`=&hl6q1I2oD|#n+jBdNq*oMfP;vY
z@15GsP@J@+#O9)`3gSqu)}m%EjdH^+?!_J;d>q7mhR4!Rz;v$fk}EYaHj>N_z(3~z
z$vke@>}qaHxFo;AV}b!K!_HIxNFYbsGTD;Zc3RmEYZ?75`Y8)sjXSQW6i@?ez?d<5
zAr9u9x$?j?!@Zd&ag8e@;u1=i*awwMcn(si$RW%|ajW#0%4ACQC5vm
zR-AA=J&wu#_emtw>f}4qG`>Rpq&uuujyCJyYdB54HqHFXj;V1p`*o2gqzzoR%&|3r
zL^y7IQ&=VgaB4muk7D3$0m=%XQ-1kjVJDNfb}&g>WG*VRaFJoNlF#%-7qM{kWEV${
zIS=T!qY@W~Y1ivuG=YE_IB_UUae7K}Q>%n;mC1a;ex9${%pW_e)e*|O-+S%*fY7sD@YACT%
z)yyQIUo!`8seVU&0%+>Lgb&x;%4U*9Gt*yI;Ai5@menzghf7mjNI1{59R6%dk^_Od6a3sq#a
z`d;)a?w+p)&?jGICLN317Tm)ghOqC1)4AFz@I;{j)nSrMyd7k*~X)j(r;DyOtwf=}FYOh{>^
zDZ%xC!>k!iP06Qsua{{K0*BYOMW}HV7O1WB668sq)pZ6wt
z5EDP>QY%N|cJYu`8prf^>hU@1Ls*+3dIe#{Yl~I0@x5!l#%-R1+@lg$#nO;MwT5e8
z#zhztxX#8ueSsHDj8i>QPP#biF%`8!6_lJ2d*nS!NK3t9wEmLi6T7$<})+(Mn)$NxdgoqB)fH+m?}s9>}*1Yr<_
zSnPUu26?JltmoclY>_)=PqY+bxp78IZU
zvn?Z1-%lt%PE=(Ihi4jiT7N|mO~L(C07dXCnSI5zmel53h=XQGYFLx6b>^xBYhMq+r@cX^ZY1!tfkLHv_)xsBuQ?NS?zH>@Sr~
z%qna&JZ;MPYNBwoEhNM*IzQ<+ZPt&ujW63wYA#2|3#L=F@1Ef*s$bzlNCQP%DSs!Y
zH4IWQ4~7s1%|@qm3URhFHjAFTuh0?!Mwq
z-e$+zTM@~K9Ewh;!1`o>i7rj746(W<;M0cJFZ%IIQP9Cl}&izAdWXYVSb
zc12Iz5*fsuF>@|kQ-G80wj7xhzfJQX&(K@<*;(Fsx7&92xu@wtJlRwV`M$S%s#`6G
z2AR_(RqxOl3s$EcN3?Vu%X*x&gQfhZ23dT~UErT1Jf_au_@Cy@gf{tooa6U#%Bvu`
z25aJ%>NZ(dDa?jCtjH#=eS|K!f$6gL7`9C#h8D+l1TlJ$s4-3H^o%5zVwUW$5D7%~
z${wS{?=@(8X57wuO43h5k~4ZLp|HDMx^XhLe)
z^;GY^m3{{C2X=!-nv+xy{CQd5#PimA+)X)G#gR?9)*3$N{q|!R5tlH=tq4^Jzz^xD
zF{FP2`5QHW;Nxoi&Fw0?yAU@xMq&I{jg2y6Gphjuw1pw!v1PrSn2lW^c3IFbr!<4P
zFj{!X*%ir|WDB0~*;0}v6aG^Ezmo)X$t!y}z75<{`=$Y*1O9
zqgQ4;&z&PHa(TEa@iTGyFssXNs%ObrBtyY-ejb9P{kq4svx?^`m?Xz~x~uA_GTKZH
z1w=KF$IG|DADOL8xa7|;BG6LA2!@qb)Mm;_{>7p<2ZrnB??zg(0`r05Dr>j)Z_1T?
zEA&;tyF68;3G%{T2k&7JD!sD)8{F2JNC(eDojx(h#!%Vw-Ho@bqH`tz58X*HN-lzM
z(>1uh8#f^(PK6VuV`(aWbR}ZjdPc-rmqu&9NcIA>YdqZ6TJhk_|2{GxNv}J>kgcL2
zMZ~T9cG*w0z;RPgL*bG)pGA2(*4({XuvbWOq%bH%M
zo@I3bQ9rWhKT_(iI}kO_EchYg@3q<6pxcsZE>@AA_Rym_^(
zEb%U9xDJS1?aM3)nuLLa7p;YcY=sdG5VwaL9FvL_>QpA@GCJQL0N7rZiOc9cn(a#8
zVL3yIq)lx=$2s}(>NGSlhs>SDdCEK*ybs1&y(gD;lZF+GNX)r*^dKtz>y1fzJb0Q?
zkvq!Kj&EX}_|GxoPgM@0gZ3%rgd6KhCWLdp?pZ%qZAO+S9U*rr9kucw(W4%7d_i$0
zf@O{h@^)%gXP0O(&50X25#_fVUx16cd8ZaCg=A4h)J_vU5a?&~@Pg5t&Eotzlu`+l
zr&r>t?ghM7han6?V%>p4bm?+N!DJi`RyE#-04lB)?@tfP3E{A>>Rt2k)GY}0?IXn(
zZ(Gmbh6~AiD$VTkVFdgCq{iqJ(UzumhnzX5^8%tvpKm>Dbh{9wGoR_S0VQUprtSsW
zV>0;nir>~yKj;}iMLj}c^i3FECbTU9WZRW8$P+2hquU3@g+?!b>HGz~KlBE4|L%Zn
zY^mDZty)+o6EwadBa7$J!hZqVwrL|M(;toGR?ijyz=jzu6s(Ba$mviAZJ8$i!r57TE$8XELgC72N}p?
zp9HIBKp2E
z_3zTV8F$|_Yovh+aKs>Qmo~XE6OdFT*fxnAniWg-y&KER!IW`{Q`XV%_OWT(DkTX9
zLw{~hd@OQ62mEvR{opecu-Qt6Y3goYkW(z<#>Q!oW+6GKEx3gWRBG@&qhs871@E4YQxAeK~
zMbCjR?Y{Y|hS>iLQ^=1Vk_}j>TaU%#zR!}ed?2+SO61fFDIuaIdDVI^MH~FWo&ZfW
zNgon!)_wJ
zct^j=U3#{%#)d1BN63(s$U@2a;2u*h$2^Zr|Nv}7O>#eMqlc-?Vua^J_;Gjk&YN#zw)J734TvpDU0aIMj
z5qin3eI8mit^yO(A61Nk|MRm-*orWlS1NTMVK$3_;>-=)x$Qb1Enm*hp+Ok{rT8tq
zP5q1zb#sJK1VMS!3U`6nf8b7gI@^ukD)ND%miT+R9jmMQLtm6a8r>Hv$W`!38GmsI
zlY07N0gPW{)%jih`D^rw;XpY11K>I2Qhhv|O1lhm;|~jp(90$Y0t1__tU-xE)mS@U
z5U^dWgh{NA>(=Ez;s_#k1NHHYTCM_92=l^3)56p=a`E8LY)uXm`=2ST&fn
z^L_1aIyLf(M93qPFcxV_!WSe|9^;{f;`Y>$lH{K+1UI|$>-#{3(2F-Fw7PpC?H59+{f9QU}ENH5DZ%V
z!caqdw}0=R-|g&H0nbPN&M_sL3KhzXy%akxXu=lZfZ=+O5~St0))=iQT{`v6XhNY2
z=YTyiXay$8L}3W`qoP+6a2BgPUv>HcG0Ly5UR4dkgT4SfaRY_=b%JiX5>QuGUoTQ!
z^0x}BB7x<){eK>U5;)+J-w6GQZ9~}C$QH|?dd?BBT&WF_QrM9mxEIf!>xT29@iCv6sh6R!bFjVH}CC18FX$Q
z%`PQe_*qoD2p%E*5=mCrkB@!Hj+`YAl0`%Uh)n)xNgi8hnIeCf0X1VJ0rvGqWs^_A
zr=ptwKP>OsxRuw0h-)Fo2)mq49%`=h6Z)^`$%5r(XDLe4hx2C@AOqiGe|*xp(3X_B
zeISH!EjxsDexQOr9^QyX_(=TCRXduj4(qvED<S)?xt%4}4&5u~AzCLIgLS+F_gba`+D*|0b09%Cx?G|h
zIdEl)I2BfU$aYk$RD9g`$Bki86o{=v8Z=9ijjF*12N^{$ET6TGi9ES)q4b)f=vQ0&
zfg+<)2n`$UxGO&fW7!`g!+GiwOuR2>@mJ4F3|1P+p=4Eg*_vEPcx9Mmt1HtShaRnzA8^sni2`X%dHQIW8k$
zON9p0(qLh4#F%l|?~?aEWCsM&cH&3eghA!SPY6Uh1=~66Hg@s$vb3bAjjr3>P^eTx
z;NDV0fX`45R53@?@N%H_MXYSn?HTj!bL|SHi2}jpmSTD&*3Ga4*C!30Nx>Xk}i;D1lg*s!(Q|CPV<$;K(2xN
zH8U8{Ldu^(p3f+p3-Y7G`I@~p0BBXtcqI2y3dGAV7nB90&8b?3$*t%QhvU&6&H}9m
z1kS)(QUK^?I1qb#e~4J&WdLUi&d4OPJWnB(uH09bT42HfDYqfSR{`9jB-Wc^ggf
zA9BPBJ7Rx$Ataz|4yRljRcuEvBn*~=u1meQipW?y^iucl=(M8^uOUtjl>ElHvV3>&
z%;1xxVvLWN52NNEz>`W|+8z?$_=ZeU`2kR4%dVbr=E};`6
z*jyXx*6pkSpcx0omT~Nt#92zf{jKHZfY1;n(}k?x{F$F7?D+O2R>-cai=i|#7y%)X
zmxT6Ckg0-ZrMJD#>mRI2h9-1Qpw+``gYSlNIbdiKpQ&nTnr2du9+;7%a1;lt^~Ks-
z&K7-}8$LEzB-tS0CtWZ(a-C?e9PJKM5mS2tk+L973@krn5@p^M{d=hN=(Jr=U}~cA
z42x+7^Dn)5He*LP;0%)7+uZ%$VS`LDi8z|UCk7vZHQwB5$X&a@j$9GYMEt_(UU_9v
z5slhy<)jtn#nkVxJUyzs)NIlJ^M5Ge@{fFFockgBy!>J21kdf*+?fDoVZ+t6E1Bq+
z1?zLgaah4VIBuRM|D6L!Y>tl_&vHafT8MZ=8Qfh}!xSnwk{+3b$8Uf$6rP!eGh!L!
z*#uu&?#rzt7y^6CW;K_V#_7DrvZ^Mb!hY}@5*x7LTa6ID*x-xIW4o4P_ygjY^oZb^
z)88OsG&o+~!Y1|Ooj$2SbXK;x`I?$fZ9s820IRRElMV)8AK9+0I!kGT|S#e=5ZQH5^=B025($MQw3|SA6Ou)yVUnd
zI4>P`y>E-$O
zr)i;3s_1Moo4yodT&`cFZEMBoB3j4(mVw49gh6kHBK;@a^vxPmz>)kH9Yk~^Oc28Y
zbb1R2Ar_J^+on0mj#JJTjf%b$Q>}tlaKxax{Fz+hE`
zBsA-dOZ(yLzP8DNbtp9Z*Hl%WG61N{&7!e}FU}cb_fjTf@1m7kTzh~g5ygyF
z!NdGoxuuu+2X0B-q|*vT#O3PrFQh4Ngir7`fyjv5?k|E-C?Yd=wP)yW*4KEH%Jn>q
zL~s!tJVVZ;K}x9;3~W_~Y%nPSW=o}Sv;t~W#rXftZ~a~kTkx8LhxR)_9v2$`H2R8(
zz9ws7g-x=4#S64$MZ4J<0gkC}Pe{MB9`KR6Ij$mQ%J8z3mLqkn*>>~9@#N%0(=H*K
z^^WKX|IAVS^mC6?NUynTmKCdwoxtmT+l?MxIf2Bv;DVf3blw>R6vryYL*#S2!(u#n
zL81(`SR?ORI&_+;;I2c)8JHY7<4Qx-iupSFaG$Q}rmr`VQh?aa6k^`U?jPLWWLqP@
z%ow^-a~ltoF%LkMj;-|YLT5eLsV_QsP1Yt)T=rM6Qc
z9KBdYk;g0zfMUv^3&}gIl|c?p
zg1FIs?Zh?r2Z7?bM!MllUAD|f#r$YkZ$Ft|S;O0_iDii36Y*TNtMt4md
zrY`lygSxnmqSUBinyTAL*d9)zu}#f_C7FVn|yl&=6c{Zz46g7l0uX7`;!B9P_L%ho-
z=BdL@f?IJZv(erbK8>}s{oDQv3zmNPo*9+Dg`2mw15v+V&p*)i9BFU!QTJO8a{cb1
zcHk3e%7;OY7(5o4I0&l;=o>~ur|`nhlLF?P-8fNZIE3wnNx0#*E0{CCMv*-eIiWBj
z-re~sJd85h(!d(^dgN$-!EA( |