Просмотр исходного кода

Merge pull request #4045 from Pigeon0v0/neoforge

NeoForge 支持
龙腾猫跃 2 лет назад
Родитель
Сommit
1f4d6e3fa1

+ 13 - 0
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -128,6 +128,16 @@
                 ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
             Case 8
                 CompFilesCardPreload(Stack, Stack.Tag)
+            Case 13
+                Dim LoadingPickaxe As New MyLoading With {.Text = "正在获取版本列表", .Margin = New Thickness(5)}
+                Dim Loader = New LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))("DlNeoForgeVersion Main", AddressOf DlNeoForgeVersionMain)
+                LoadingPickaxe.State = Loader
+                Loader.Start(Stack.Tag)
+                AddHandler LoadingPickaxe.StateChanged, AddressOf FrmDownloadNeoForge.NeoForge_StateChanged
+                AddHandler LoadingPickaxe.Click, AddressOf FrmDownloadNeoForge.NeoForge_Click
+                Stack.Children.Add(LoadingPickaxe)
+            Case 14
+                NeoForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf NeoForgeSave_Click, True)
         End Select
         '实现控件虚拟化
         For Each Data As Object In Stack.Tag
@@ -171,6 +181,9 @@
                     Stack.Children.Add(CType(Data, HelpEntry).ToListItem)
                 Case 12
                     Stack.Children.Add(FabricDownloadListItem(CType(Data, JObject), AddressOf FrmDownloadInstall.Fabric_Selected))
+                Case 13
+                Case 14
+                    Stack.Children.Add(NeoForgeDownloadListItem(Data, AddressOf NeoForgeSave_Click, True))
                 Case Else
                     Log("未知的虚拟化种类:" & Type, LogLevel.Feedback)
             End Select

+ 1 - 0
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -1019,6 +1019,7 @@ Install:
         DownloadClient = 4
         DownloadOptiFine = 5
         DownloadForge = 6
+        DownloadNeoForge = 13
         DownloadFabric = 7
         DownloadLiteLoader = 8
         DownloadMod = 10

BIN
Plain Craft Launcher 2/Images/Blocks/NeoForge.png


+ 284 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb

@@ -721,6 +721,290 @@
 
 #End Region
 
+#Region "DlNeoForgeList | NeoForge Minecraft 版本列表"
+
+    Public Structure DlNeoForgeListResult
+        ''' <summary>
+        ''' 数据来源名称,如“Official”,“BMCLAPI”。
+        ''' </summary>
+        Public SourceName As String
+        ''' <summary>
+        ''' 是否为官方的实时数据。
+        ''' </summary>
+        Public IsOfficial As Boolean
+        ''' <summary>
+        ''' 获取到的数据。
+        ''' </summary>
+        Public Value As List(Of String)
+    End Structure
+
+    ''' <summary>
+    ''' NeoForge 版本列表,主加载器。
+    ''' </summary>
+    Public DlNeoForgeListLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Main", AddressOf DlNeoForgeListMain)
+    Private Sub DlNeoForgeListMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult))
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of Integer, DlNeoForgeListResult), Integer)(DlNeoForgeListBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' NeoForge 版本列表,官方源。
+    ''' </summary>
+    Public DlNeoForgeListOfficialLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Official", AddressOf DlNeoForgeListOfficialMain)
+    Private Sub DlNeoForgeListOfficialMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult))
+        Dim ResultLatest As String
+        Dim VersionsJArray As JArray
+        Try
+            ResultLatest = NetGetCodeByDownload("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
+        Catch ex As Exception
+            If GetExceptionSummary(ex).Contains("(404)") Then
+                Throw New Exception("没有可用版本")
+            Else
+                Throw
+            End If
+        End Try
+        If ResultLatest.Length < 50 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")")
+        VersionsJArray = GetJson(ResultLatest)("versions")
+        Dim Versions As New List(Of String)
+        Versions.Add("1.20.1")
+        Try
+            For Each Token As String In VersionsJArray
+                Dim Version As String = Token.Replace("neoforge-", "")
+                Dim Inherit As String = $"1.{Version.Split(".")(0)}.{Token.Split(".")(1)}"
+                If Inherit.EndsWith(".0") Then Inherit = Inherit.Replace(".0", "")
+                Versions.Add(Inherit)
+            Next
+            Versions = Versions.Distinct().ToList()
+        Catch ex As Exception
+            Log(ex, LogLevel.Feedback)
+        End Try
+        If Not Versions.Any() Then Throw New Exception("没有可用版本")
+        Loader.Output = New DlNeoForgeListResult With {.IsOfficial = True, .SourceName = "NeoForge 官方源", .Value = Versions}
+    End Sub
+
+    ''' <summary>
+    ''' NeoForge 版本列表,BMCLAPI。
+    ''' </summary>
+    Public DlNeoForgeListBmclapiLoader As New LoaderTask(Of Integer, DlNeoForgeListResult)("DlNeoForgeList Bmclapi", AddressOf DlNeoForgeListBmclapiMain)
+    Private Sub DlNeoForgeListBmclapiMain(Loader As LoaderTask(Of Integer, DlNeoForgeListResult))
+        Dim ResultLatest As String
+        Dim VersionsJArray As JArray
+        Try
+            ResultLatest = NetGetCodeByDownload("https://bmclapi2.bangbang93.com/neoforge/meta/api/maven/details/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
+        Catch ex As Exception
+            If GetExceptionSummary(ex).Contains("(404)") Then
+                Throw New Exception("没有可用版本")
+            Else
+                Throw
+            End If
+        End Try
+        If ResultLatest.Length < 50 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")")
+        VersionsJArray = GetJson(ResultLatest)("name")
+        Dim Versions As New List(Of String)
+        Versions.Add("1.20.1")
+        Try
+            For Each Token As String In VersionsJArray("name")
+                Dim Version As String = Token.Replace("neoforge-", "")
+                Dim Inherit As String = $"1.{Version.Split(".")(0)}.{Token.Split(".")(1)}"
+                If Inherit.EndsWith(".0") Then Inherit = Inherit.Replace(".0", "")
+                Versions.Add(Inherit)
+            Next
+            Versions = Versions.Distinct().ToList()
+        Catch ex As Exception
+            Log(ex, LogLevel.Feedback)
+        End Try
+        If Not Versions.Any() Then Throw New Exception("没有可用版本")
+        Loader.Output = New DlNeoForgeListResult With {.IsOfficial = False, .SourceName = "BMCLAPI", .Value = Versions}
+    End Sub
+#End Region
+
+#Region "DlNeoForgeVersion | NeoForge 版本列表"
+
+    Public Class DlNeoForgeVersionEntry
+        ''' <summary>
+        ''' 完整的版本名,如 “neoforge-20.4.30-beta”。
+        ''' </summary>
+        Public VersionName As String
+        ''' <summary>
+        ''' 对应的 Minecraft 版本,如“1.20.4”。
+        ''' </summary>
+        Public Inherit As String
+        ''' <summary>
+        ''' 是否是 Beta 版。若未提供则默认为 False。
+        ''' </summary>
+        Public IsBeta As Boolean = False
+        ''' <summary>
+        ''' 标准的版本号,如 “20.4.30”。
+        ''' </summary>
+        Public VersionCode As String
+        Public Structure DlNeoForgeListResult
+            ''' <summary>
+            ''' 数据来源名称,如“Official”,“BMCLAPI”。
+            ''' </summary>
+            Public SourceName As String
+            ''' <summary>
+            ''' 是否为官方的实时数据。
+            ''' </summary>
+            Public IsOfficial As Boolean
+            ''' <summary>
+            ''' 获取到的数据。
+            ''' </summary>
+            Public Value As JObject
+        End Structure
+        ''' <summary>
+        ''' 即将下载的文件全名。
+        ''' </summary>
+        Public ReadOnly Property FileName As String
+            Get
+                If VersionCode.StartsWith("47.") Then 'NeoForge 1.20.1 的版本命名有些特殊...
+                    Return "forge-" & "1.20.1" & "-" & VersionCode & If(IsBeta, "-beta", "") & "-" & "installer" & "." & "jar"
+                Else
+                    Return "neoforge-" & VersionCode & If(IsBeta, "-beta", "") & "-" & "installer" & "." & "jar"
+                End If
+            End Get
+        End Property
+        ''' <summary>
+        ''' 文件扩展名
+        ''' </summary>
+        Public ReadOnly Property FileSuffix As String = ".jar"
+    End Class
+    ''' <summary>
+    ''' NeoForge 版本列表,主加载器。
+    ''' </summary>
+    Public Sub DlNeoForgeVersionMain(Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)))
+        Dim DlNeoForgeVersionOfficialLoader As New LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))("DlNeoForgeVersion Official", AddressOf DlNeoForgeVersionOfficialMain)
+        Dim DlNeoForgeVersionBmclapiLoader As New LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))("DlNeoForgeVersion Bmclapi", AddressOf DlNeoForgeVersionBmclapiMain)
+        Select Case Setup.Get("ToolDownloadVersion")
+            Case 0
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionBmclapiLoader, 30),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionOfficialLoader, 60)
+                }, Loader.IsForceRestarting)
+            Case 1
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionOfficialLoader, 5),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionBmclapiLoader, 35)
+                }, Loader.IsForceRestarting)
+            Case Else
+                DlSourceLoader(Loader, New List(Of KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)) From {
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionOfficialLoader, 60),
+                    New KeyValuePair(Of LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)), Integer)(DlNeoForgeVersionBmclapiLoader, 60)
+                }, Loader.IsForceRestarting)
+        End Select
+    End Sub
+
+    ''' <summary>
+    ''' NeoForge 版本列表,官方源。
+    ''' </summary>
+    Public Sub DlNeoForgeVersionOfficialMain(Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)))
+        Dim ResultLatest As String
+        Dim ResultLegacy As String
+        Try
+            ResultLatest = NetGetCodeByDownload("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/neoforge", UseBrowserUserAgent:=True, IsJson:=True)
+            ResultLegacy = NetGetCodeByDownload("https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/forge", UseBrowserUserAgent:=True, IsJson:=True)
+        Catch ex As Exception
+            If GetExceptionSummary(ex).Contains("(404)") Then
+                Throw New Exception("没有可用版本")
+            Else
+                Throw
+            End If
+        End Try
+        If ResultLatest.Length < 50 AndAlso ResultLegacy.Length < 50 Then Throw New Exception("获取到的版本列表长度不足(" & ResultLatest & ")")
+        Dim ResultLatestJson As JObject = GetJson(ResultLatest)
+        Dim ResultLegacyJson As JObject = GetJson(ResultLegacy)
+        Dim VersionsJArray As JArray
+        Dim IsLegacyNeo As Boolean = False
+        If Loader.Input IsNot Nothing AndAlso Loader.Input.ToString().Contains("1.20.1") Then
+            VersionsJArray = ResultLegacyJson("versions")
+            IsLegacyNeo = True
+        Else
+            VersionsJArray = ResultLatestJson("versions")
+        End If
+        Dim Versions As New List(Of DlNeoForgeVersionEntry)
+        Try
+            For Each Token As String In VersionsJArray
+                Dim StdVersion As String
+                Dim IsBeta As Boolean
+                Dim rawVersion As String = Token
+                If rawVersion.Contains("-beta") Then
+                    StdVersion = Token.Replace("neoforge-", "").Replace("-beta", "").Replace("1.20.1-", "")
+                    IsBeta = True
+                Else
+                    StdVersion = Token.Replace("neoforge-", "").Replace("1.20.1-", "")
+                    IsBeta = False
+                End If
+                Dim Inherit As String = $"1.{StdVersion.Split(".")(0)}.{StdVersion.Split(".")(1)}"
+                If Inherit.EndsWithF(".0") Then Inherit.Remove(Inherit.LastIndexOfF(".0"), 1)
+                Inherit = If(IsLegacyNeo, "1.20.1", Inherit)
+                Dim Entry = New DlNeoForgeVersionEntry
+                If Loader.Input IsNot Nothing Then
+                    If IsLegacyNeo OrElse Inherit.Contains(Loader.Input.ToString().Replace("1.", "")) Then
+                        Entry = New DlNeoForgeVersionEntry With {.VersionName = rawVersion, .Inherit = Inherit, .IsBeta = IsBeta, .VersionCode = StdVersion}
+                        Versions.Add(Entry)
+                    End If
+                Else
+                    Entry = New DlNeoForgeVersionEntry With {.VersionName = rawVersion, .Inherit = Inherit, .IsBeta = IsBeta, .VersionCode = StdVersion}
+                    Versions.Add(Entry)
+                End If
+            Next
+        Catch ex As Exception
+            Log(ex, LogLevel.Feedback)
+        End Try
+        If Not Versions.Any() Then Throw New Exception("没有可用版本")
+        Dim VersionsArray = Versions.ToList()
+        VersionsArray.Reverse()
+        Loader.Output = VersionsArray
+    End Sub
+
+    ''' <summary>
+    ''' NeoForge 版本列表,BMCLAPI。
+    ''' </summary>
+    Public Sub DlNeoForgeVersionBmclapiMain(Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)))
+        Dim Json As JArray = NetGetCodeByRequestRetry("https://bmclapi2.bangbang93.com/neoforge/list/" & Loader.Input, IsJson:=True)
+        Dim Versions As New List(Of DlNeoForgeVersionEntry)
+        Try
+            For Each Token As JObject In Json
+                Dim StdVersion As String
+                Dim IsBeta As Boolean
+                Dim rawVersion As String = Token("rawVersion")
+                If rawVersion.Contains("-beta") Then
+                    StdVersion = Token("version").ToString().Replace("neoforge-", "").Replace("-beta", "").Replace("1.20.1-", "")
+                    IsBeta = True
+                Else
+                    StdVersion = Token("version").ToString().Replace("neoforge-", "").Replace("1.20.1-", "")
+                    IsBeta = False
+                End If
+                Dim Inherit As String = Token("mcversion")
+                Dim Entry = New DlNeoForgeVersionEntry With {.VersionName = rawVersion, .Inherit = Inherit, .IsBeta = IsBeta, .VersionCode = StdVersion}
+                Versions.Add(Entry)
+            Next
+        Catch ex As Exception
+            MyMsgBox(ex.ToString(), "错误")
+            Throw New Exception("版本列表解析失败(" & Json.ToString & ")", ex)
+        End Try
+        If Not Versions.Any() Then Throw New Exception("没有可用版本")
+        Dim VersionsArray = Versions.ToList()
+        VersionsArray.Reverse()
+        Loader.Output = VersionsArray
+    End Sub
+
+#End Region
+
 #Region "DlLiteLoaderList | LiteLoader 版本列表"
 
     Public Structure DlLiteLoaderListResult

+ 41 - 13
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -290,7 +290,7 @@ Public Module ModMinecraft
         Public ReadOnly Property Modable As Boolean
             Get
                 If Not IsLoaded Then Load()
-                Return Version.HasFabric OrElse Version.HasForge OrElse Version.HasLiteLoader OrElse
+                Return Version.HasFabric OrElse Version.HasForge OrElse Version.HasLiteLoader OrElse Version.HasNeoForge OrElse
                     DisplayType = McVersionCardType.API '#223
             End Get
         End Property
@@ -345,7 +345,7 @@ Public Module ModMinecraft
                                 End If
                             Next
                         End If
-                        '从 Forge Arguments 中获取版本号
+                        '从 Forge / NeoForge Arguments 中获取版本号
                         If JsonObject("arguments") IsNot Nothing AndAlso JsonObject("arguments")("game") IsNot Nothing Then
                             Dim Mark As Boolean = False
                             For Each Argument In JsonObject("arguments")("game")
@@ -729,12 +729,20 @@ Recheck:
                             State = McVersionState.Fabric
                             Version.FabricVersion = If(RegexSeek(RealJson, "(?<=(net.fabricmc:fabric-loader:)|(org.quiltmc:quilt-loader:))[0-9\.]+(\+build.[0-9]+)?"), "未知版本").Replace("+build", "")
                             Version.HasFabric = True
-                        ElseIf RealJson.Contains("minecraftforge") Then
+                        ElseIf RealJson.Contains("minecraftforge") AndAlso Not RealJson.Contains("neoforge") Then
                             State = McVersionState.Forge
                             Version.ForgeVersion = RegexSeek(RealJson, "(?<=forge:[0-9\.]+(_pre[0-9]*)?\-)[0-9\.]+")
                             If Version.ForgeVersion Is Nothing Then Version.ForgeVersion = RegexSeek(RealJson, "(?<=net\.minecraftforge:minecraftforge:)[0-9\.]+")
                             If Version.ForgeVersion Is Nothing Then Version.ForgeVersion = If(RegexSeek(RealJson, "(?<=net\.minecraftforge:fmlloader:[0-9\.]+-)[0-9\.]+"), "未知版本")
                             Version.HasForge = True
+                        ElseIf RealJson.Contains("neoforge") Then    '1.20.1 NeoForge Json 内信息不同,为了方便直接截断 orgeVersion;这里的实现比较抽象,正则我写不会
+                            State = McVersionState.NeoForge          '1.20.1 Json 标识:"--fml.forgeVersion"     1.20.1 以上 Json 标识:"--fml.neoForgeVersion"
+                            Dim JsonStr1() As String = RealJson.Replace(" ", "").Split("orgeVersion""," & vbCrLf & """")
+                            Dim JsonStr2() As String = JsonStr1(1).Split("""")
+                            Version.NeoForgeVersion = JsonStr2(0)
+                            If Version.NeoForgeVersion Is Nothing Then Version.NeoForgeVersion = RegexSeek(RealJson, "(?<=orgeVersion"",)[0-9\.]+")
+                            If Version.NeoForgeVersion Is Nothing Then Version.NeoForgeVersion = If(RegexSeek(RealJson, "(?<=net\.minecraftforge:fmlloader:[0-9\.]+-)[0-9\.]+"), "未知版本")
+                            Version.HasNeoForge = True
                         End If
                         Version.IsApiLoaded = True
                 End Select
@@ -752,6 +760,8 @@ ExitDataLoad:
                             Logo = PathImage & "Blocks/CobbleStone.png"
                         Case McVersionState.Forge
                             Logo = PathImage & "Blocks/Anvil.png"
+                        Case McVersionState.NeoForge
+                            Logo = PathImage & "Blocks/NeoForge.png"
                         Case McVersionState.Fabric
                             Logo = PathImage & "Blocks/Fabric.png"
                         Case McVersionState.OptiFine
@@ -780,7 +790,7 @@ ExitDataLoad:
                             End If
                         Case McVersionState.Old
                             Info = "远古版本"
-                        Case McVersionState.Original, McVersionState.Forge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
+                        Case McVersionState.Original, McVersionState.Forge, McVersionState.NeoForge, McVersionState.Fabric, McVersionState.OptiFine, McVersionState.LiteLoader
                             Info = Version.ToString
                         Case McVersionState.Fool
                             Info = GetMcFoolName(Version.McName)
@@ -813,6 +823,7 @@ ExitDataLoad:
                     WriteIni(Path & "PCL\Setup.ini", "VersionOptiFine", Version.OptiFineVersion)
                     WriteIni(Path & "PCL\Setup.ini", "VersionLiteLoader", Version.HasLiteLoader)
                     WriteIni(Path & "PCL\Setup.ini", "VersionForge", Version.ForgeVersion)
+                    WriteIni(Path & "PCL\Setup.ini", "VersionNeoForge", Version.NeoForgeVersion)
                     WriteIni(Path & "PCL\Setup.ini", "VersionApiCode", Version.SortCode)
                     WriteIni(Path & "PCL\Setup.ini", "VersionOriginal", Version.McName)
                     WriteIni(Path & "PCL\Setup.ini", "VersionOriginalMain", Version.McCodeMain)
@@ -852,6 +863,7 @@ ExitDataLoad:
         OptiFine
         Old
         Forge
+        NeoForge
         LiteLoader
         Fabric
     End Enum
@@ -903,6 +915,17 @@ ExitDataLoad:
         ''' </summary>
         Public ForgeVersion As String = ""
 
+        'NeoForge
+
+        ''' <summary>
+        ''' 该版本是否安装了 NeoForge。
+        ''' </summary>
+        Public HasNeoForge As Boolean = False
+        ''' <summary>
+        ''' NeoForge 版本号,如 21.0.2-beta、47.1.79。
+        ''' </summary>
+        Public NeoForgeVersion As String = ""
+
         'Fabric
 
         ''' <summary>
@@ -929,6 +952,7 @@ ExitDataLoad:
         Public Overrides Function ToString() As String
             ToString = ""
             If HasForge Then ToString += ", Forge" & If(ForgeVersion = "未知版本", "", " " & ForgeVersion)
+            If HasNeoForge Then ToString += ", NeoForge" & If(NeoForgeVersion = "未知版本", "", " " & NeoForgeVersion)
             If HasFabric Then ToString += ", Fabric" & If(FabricVersion = "未知版本", "", " " & FabricVersion)
             If HasOptiFine Then ToString += ", OptiFine" & If(OptiFineVersion = "未知版本", "", " " & OptiFineVersion)
             If HasLiteLoader Then ToString += ", LiteLoader"
@@ -955,15 +979,15 @@ ExitDataLoad:
                             Else
                                 Throw New Exception("无效的 Fabric 版本:" & ForgeVersion)
                             End If
-                        ElseIf HasForge Then
-                            If ForgeVersion = "未知版本" Then Return 0
-                            Dim SubVersions = ForgeVersion.Split(".")
+                        ElseIf HasForge OrElse HasNeoForge Then
+                            If ForgeVersion = "未知版本" AndAlso NeoForgeVersion = "未知版本" Then Return 0
+                            Dim SubVersions = If(HasForge, ForgeVersion.Split("."), NeoForgeVersion.Split("."))
                             If SubVersions.Length = 4 Then
                                 _SortCode = Val(SubVersions(0)) * 1000000 + Val(SubVersions(1)) * 10000 + Val(SubVersions(3))
                             ElseIf SubVersions.Length = 3 Then
                                 _SortCode = Val(SubVersions(0)) * 1000000 + Val(SubVersions(1)) * 10000 + Val(SubVersions(2))
                             Else
-                                Throw New Exception("无效的 Forge 版本:" & ForgeVersion)
+                                Throw New Exception(If(HasForge, "无效的 Forge 版本:" & ForgeVersion, "无效的 NeoForge 版本:" & NeoForgeVersion))
                             End If
                         ElseIf HasOptiFine Then
                             If OptiFineVersion = "未知版本" Then Return 0
@@ -1175,6 +1199,7 @@ OnLoaded:
                             Dim VersionInfo As New McVersionInfo With {
                                 .FabricVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionFabric", ""),
                                 .ForgeVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionForge", ""),
+                                .NeoForgeVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionNeoForge", ""),
                                 .OptiFineVersion = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOptiFine", ""),
                                 .HasLiteLoader = ReadIni(Version.Path & "PCL\Setup.ini", "VersionLiteLoader", False),
                                 .SortCode = ReadIni(Version.Path & "PCL\Setup.ini", "VersionApiCode", -1),
@@ -1183,9 +1208,10 @@ OnLoaded:
                                 .McCodeSub = ReadIni(Version.Path & "PCL\Setup.ini", "VersionOriginalSub", -1),
                                 .IsApiLoaded = True
                             }
-                            VersionInfo.HasFabric = VersionInfo.FabricVersion.Count > 1
-                            VersionInfo.HasForge = VersionInfo.ForgeVersion.Count > 1
-                            VersionInfo.HasOptiFine = VersionInfo.OptiFineVersion.Count > 1
+                            VersionInfo.HasFabric = VersionInfo.FabricVersion.Any()
+                            VersionInfo.HasForge = VersionInfo.ForgeVersion.Any()
+                            VersionInfo.HasNeoForge = VersionInfo.NeoForgeVersion.Any()
+                            VersionInfo.HasOptiFine = VersionInfo.OptiFineVersion.Any()
                             Version.Version = VersionInfo
                         End If
 
@@ -1273,7 +1299,7 @@ OnLoaded:
             McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Fool}, McVersionCardType.Fool)
 
             '筛选 API 版本
-            McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Forge, McVersionState.LiteLoader, McVersionState.Fabric}, McVersionCardType.API)
+            McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Forge, McVersionState.NeoForge, McVersionState.LiteLoader, McVersionState.Fabric}, McVersionCardType.API)
 
             '将老版本预先分类入不常用,只剩余原版、快照、OptiFine
             Dim VersionUseful As New List(Of McVersion)
@@ -1407,7 +1433,7 @@ OnLoaded:
             End Function)
         End If
 
-        'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
+        'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge,再放 NeoForge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
         If ResultVersionList.ContainsKey(McVersionCardType.API) Then
             ResultVersionList(McVersionCardType.API) = Sort(ResultVersionList(McVersionCardType.API),
             Function(Left As McVersion, Right As McVersion)
@@ -1419,6 +1445,8 @@ OnLoaded:
                         Return Left.Version.HasFabric
                     ElseIf Left.Version.HasForge Xor Right.Version.HasForge Then
                         Return Left.Version.HasForge
+                    ElseIf Left.Version.HasNeoForge Xor Right.Version.HasNeoForge Then
+                        Return Left.Version.HasNeoForge
                     ElseIf Not Left.Version.SortCode <> Right.Version.SortCode Then
                         Return Left.Version.SortCode > Right.Version.SortCode
                     Else

+ 19 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -183,6 +183,7 @@ Retry:
 
         '获取 Mod API 版本信息
         Dim ForgeVersion As String = Nothing
+        Dim NeoForgeVersion As String = Nothing
         Dim FabricVersion As String = Nothing
         For Each Entry In If(Json("minecraft")("modLoaders"), {})
             Dim Id As String = If(Entry("id"), "").ToString.ToLower
@@ -199,6 +200,15 @@ Retry:
                 Catch ex As Exception
                     Log(ex, "读取整合包 Forge 版本失败:" & Id)
                 End Try
+            ElseIf Id.StartsWithF("neoforge-") Then
+                'NeoForge 指定
+                Try
+                    Log("[ModPack] 整合包 NeoForge 版本:" & Id)
+                    NeoForgeVersion = Id.Split("-")(1)
+                    Exit For
+                Catch ex As Exception
+                    Log(ex, "读取整合包 NeoForge 版本失败:" & Id)
+                End Try
             ElseIf Id.StartsWithF("fabric-") Then
                 'Fabric 指定
                 Try
@@ -301,6 +311,7 @@ Retry:
             .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\",
             .MinecraftName = Json("minecraft")("version").ToString,
             .ForgeVersion = ForgeVersion,
+            .NeoForgeVersion = NeoForgeVersion,
             .FabricVersion = FabricVersion
         }
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
@@ -368,6 +379,7 @@ Retry:
         '获取 Mod API 版本信息
         Dim MinecraftVersion As String = Nothing
         Dim ForgeVersion As String = Nothing
+        Dim NeoForgeVersion As String = Nothing
         Dim FabricVersion As String = Nothing
         For Each Entry As JProperty In If(Json("dependencies"), {})
             Select Case Entry.Name.ToLower
@@ -376,6 +388,9 @@ Retry:
                 Case "forge" 'eg. 14.23.5.2859 / 1.19-41.1.0
                     ForgeVersion = Entry.Value.ToString
                     Log("[ModPack] 整合包 Forge 版本:" & ForgeVersion)
+                Case "neoforge", "neo-forge" 'eg. 20.6.98-beta
+                    NeoForgeVersion = Entry.Value.ToString
+                    Log("[ModPack] 整合包 NeoForge 版本:" & NeoForgeVersion)
                 Case "fabric-loader" 'eg. 0.14.14
                     FabricVersion = Entry.Value.ToString
                     Log("[ModPack] 整合包 Fabric 版本:" & FabricVersion)
@@ -447,6 +462,7 @@ Retry:
             .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\",
             .MinecraftName = MinecraftVersion,
             .ForgeVersion = ForgeVersion,
+            .NeoForgeVersion = NeoForgeVersion,
             .FabricVersion = FabricVersion
         }
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
@@ -676,6 +692,8 @@ Retry:
                     Request.MinecraftName = Component("version")
                 Case "net.minecraftforge"
                     Request.ForgeVersion = Component("version")
+                Case "net.neoforged"
+                    Request.NeoForgeVersion = Component("version")
                 Case "net.fabricmc.fabric-loader"
                     Request.FabricVersion = Component("version")
                 Case "org.quiltmc.quilt-loader" 'eg. 1.0.0
@@ -772,6 +790,7 @@ Retry:
             .MinecraftName = Addons("game"),
             .OptiFineVersion = If(Addons.ContainsKey("optifine"), Addons("optifine"), Nothing),
             .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), Nothing),
+            .NeoForgeVersion = If(Addons.ContainsKey("neoforge"), Addons("neoforge"), Nothing),
             .FabricVersion = If(Addons.ContainsKey("fabric"), Addons("fabric"), Nothing)
         }
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)

+ 1 - 0
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -394,6 +394,7 @@ EndHint:
     Public FrmDownloadOptiFine As PageDownloadOptiFine
     Public FrmDownloadLiteLoader As PageDownloadLiteLoader
     Public FrmDownloadForge As PageDownloadForge
+    Public FrmDownloadNeoForge As PageDownloadNeoForge
     Public FrmDownloadFabric As PageDownloadFabric
     Public FrmDownloadMod As PageDownloadMod
     Public FrmDownloadPack As PageDownloadPack

+ 256 - 77
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -1005,15 +1005,16 @@ Retry:
 
 #End Region
 
-#Region "Forge 下载"
+#Region "Forge / NeoForge 下载"
 
-    Public Sub McDownloadForge(DownloadInfo As DlForgeVersionEntry)
+    Public Sub McDownloadForge(IsNeo As Boolean, Optional DownloadForgeInfo As DlForgeVersionEntry = Nothing, Optional DownloadNeoInfo As DlNeoForgeVersionEntry = Nothing)
+        Dim DownloadInfo = If(IsNeo, DownloadNeoInfo, DownloadForgeInfo)
         '老版本提示
         If DownloadInfo.Category = "client" Then
             If MyMsgBox("该 Forge 版本过于古老,PCL 暂不支持该版本的自动安装。" & vbCrLf &
                         "若你仍然希望继续,PCL 将把安装程序下载到你指定的位置,但不会进行安装。",
                         "版本过老", "继续", "取消") = 1 Then
-                McDownloadForgeSave(DownloadInfo)
+                McDownloadForgeSave(IsNeo, DownloadInfo)
             End If
             Exit Sub
         End If
@@ -1021,15 +1022,15 @@ Retry:
             If MyMsgBox("该 Forge 版本过于古老,PCL 暂不支持该版本的自动安装。" & vbCrLf &
                         "若你仍然希望继续,PCL 将把安装程序下载到你指定的位置,但不会进行安装。",
                         "版本过老", "继续", "取消") = 1 Then
-                McDownloadForgeSave(DownloadInfo)
+                McDownloadForgeSave(IsNeo, DownloadInfo)
             End If
             Exit Sub
         End If
         '初始化参数
-        Dim Id As String = DownloadInfo.Inherit & "-forge-" & DownloadInfo.Version
-        Dim Target As String = PathTemp & "Cache\Code\ForgeInstall-" & DownloadInfo.Version & "_" & GetUuid() & "." & DownloadInfo.FileSuffix
+        Dim Id As String = DownloadInfo.Inherit & $"-{If(IsNeo, "neo", "")}forge-" & DownloadInfo.Version
+        Dim Target As String = PathTemp & $"Cache\Code\{If(IsNeo, "Neo", "")}ForgeInstall-" & DownloadInfo.Version & "_" & GetUuid() & "." & DownloadInfo.FileSuffix
         Dim VersionFolder As String = PathMcFolder & "versions\" & Id & "\"
-        Dim DisplayName As String = "Forge " & DownloadInfo.Inherit & " - " & DownloadInfo.Version
+        Dim DisplayName As String = $"{If(IsNeo, "Neo", "")}Forge " & DownloadInfo.Inherit & " - " & DownloadInfo.Version
         Try
 
             '重复任务检查
@@ -1053,14 +1054,14 @@ Retry:
             End If
 
             '启动
-            Dim Loader As New LoaderCombo(Of String)(DisplayName & " 下载", McDownloadForgeLoader(DownloadInfo.Version, DownloadInfo.Inherit, DownloadInfo)) With {.OnStateChanged = AddressOf McInstallState}
+            Dim Loader As New LoaderCombo(Of String)(DisplayName & " 下载", McDownloadForgeLoader(IsNeo, DownloadInfo.Version, DownloadInfo.Inherit, DownloadInfo)) With {.OnStateChanged = AddressOf McInstallState}
             Loader.Start(VersionFolder)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
 
         Catch ex As Exception
-            Log(ex, "开始 Forge 下载失败", LogLevel.Feedback)
+            Log(ex, $"开始 {If(IsNeo, "Neo", "")}Forge 下载失败", LogLevel.Feedback)
         Finally
             '删除安装包
             Try
@@ -1069,10 +1070,11 @@ Retry:
             End Try
         End Try
     End Sub
-    Public Sub McDownloadForgeSave(DownloadInfo As DlForgeVersionEntry)
+    Public Sub McDownloadForgeSave(IsNeo As Boolean, Optional DownloadForgeInfo As DlForgeVersionEntry = Nothing, Optional DownloadNeoInfo As DlNeoForgeVersionEntry = Nothing)
+        Dim DownloadInfo = If(IsNeo, DownloadNeoInfo, DownloadForgeInfo)
         Try
-            Dim Target As String = SelectAs("选择保存位置", DownloadInfo.FileName, "Forge 文件 (*." & DownloadInfo.FileSuffix & ")|*." & DownloadInfo.FileSuffix)
-            Dim DisplayName As String = "Forge " & DownloadInfo.Inherit & " - " & DownloadInfo.Version
+            Dim Target As String = SelectAs("选择保存位置", DownloadInfo.FileName, $"{If(IsNeo, "Neo", "")}Forge 文件 (*." & DownloadInfo.FileSuffix & ")|*." & DownloadInfo.FileSuffix)
+            Dim DisplayName As String = $"{If(IsNeo, "Neo", "")}Forge " & DownloadInfo.Inherit & " - " & If(IsNeo, DownloadInfo.VersionCode, DownloadInfo.Version)
             If Not Target.Contains("\") Then Exit Sub
 
             '重复任务检查
@@ -1088,31 +1090,47 @@ Retry:
             '构造步骤加载器
             Dim Loaders As New List(Of LoaderBase)
             '获取下载地址
-            Loaders.Add(New LoaderTask(Of DlForgeVersionEntry, List(Of NetFile))("获取下载地址",
-            Sub(Task As LoaderTask(Of DlForgeVersionEntry, List(Of NetFile)))
-                Dim Files As New List(Of NetFile)
-                Files.Add(New NetFile({
-                    "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" & DownloadInfo.Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName,
-                    "https://files.minecraftforge.net/maven/net/minecraftforge/forge/" & DownloadInfo.Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName
-                }, Target, New FileChecker(MinSize:=64 * 1024, Hash:=DownloadInfo.Hash)))
-                Task.Output = Files
-            End Sub) With {.ProgressWeight = 0.1, .Show = False})
+            If IsNeo Then
+                'NeoForge
+                Loaders.Add(New LoaderTask(Of DlNeoForgeVersionEntry, List(Of NetFile))("获取下载地址",
+                Sub(Task As LoaderTask(Of DlNeoForgeVersionEntry, List(Of NetFile)))
+                    Dim Files As New List(Of NetFile)
+                    Dim IsLegacyNeo As Boolean = DownloadInfo.VersionCode.StartsWith("47.")
+                    Files.Add(New NetFile({
+                        $"https://bmclapi2.bangbang93.com/maven/net/neoforged/{If(IsLegacyNeo, "", "neo")}forge/{If(IsLegacyNeo, "1.20.1-", "")}{DownloadInfo.VersionCode}{If(DownloadInfo.IsBeta, "-beta", "")}/{DownloadInfo.FileName}",
+                        $"https://maven.neoforged.net/releases/net/neoforged/{If(IsLegacyNeo, "", "neo")}forge/{If(IsLegacyNeo, "1.20.1-", "")}{DownloadInfo.VersionCode}{If(DownloadInfo.IsBeta, "-beta", "")}/{DownloadInfo.FileName}"
+                    }, Target, New FileChecker(MinSize:=64 * 1024, Hash:=If(IsNeo, Nothing, DownloadInfo.Hash))))
+                    Task.Output = Files
+                End Sub) With {.ProgressWeight = 0.1, .Show = False})
+            Else
+                'Forge
+                Loaders.Add(New LoaderTask(Of DlForgeVersionEntry, List(Of NetFile))("获取下载地址",
+                Sub(Task As LoaderTask(Of DlForgeVersionEntry, List(Of NetFile)))
+                    Dim Files As New List(Of NetFile)
+                    Files.Add(New NetFile({
+                        "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" & DownloadInfo.Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName,
+                        "https://files.minecraftforge.net/maven/net/minecraftforge/forge/" & DownloadInfo.Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName
+                    }, Target, New FileChecker(MinSize:=64 * 1024, Hash:=If(IsNeo, Nothing, DownloadInfo.Hash))))
+                    Task.Output = Files
+                End Sub) With {.ProgressWeight = 0.1, .Show = False})
+            End If
             '下载
             Loaders.Add(New LoaderDownload("下载主文件", New List(Of NetFile)) With {.ProgressWeight = 6})
 
             '启动
-            Dim Loader As New LoaderCombo(Of DlForgeVersionEntry)(DisplayName & " 下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave}
+            Dim Loader = If(IsNeo, New LoaderCombo(Of DlNeoForgeVersionEntry)(DisplayName & " 下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave},
+                New LoaderCombo(Of DlForgeVersionEntry)(DisplayName & " 下载", Loaders) With {.OnStateChanged = AddressOf DownloadStateSave})
             Loader.Start(DownloadInfo)
             LoaderTaskbarAdd(Loader)
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
 
         Catch ex As Exception
-            Log(ex, "开始 Forge 下载失败", LogLevel.Feedback)
+            Log(ex, $"开始 {If(IsNeo, "Neo", "")}Forge 下载失败", LogLevel.Feedback)
         End Try
     End Sub
 
-    Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String, UseJavaWrapper As Boolean)
+    Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String, UseJavaWrapper As Boolean, IsNeoForge As Boolean)
         '选择 Java
         Dim Java As JavaEntry
         SyncLock JavaLock
@@ -1153,7 +1171,8 @@ Retry:
                 .RedirectStandardError = True,
                 .RedirectStandardOutput = True
             }
-            Log("[Download] 开始安装 Forge:" & Arguments)
+            Dim LoaderName As String = If(IsNeoForge, "NeoForge", "Forge")
+            Log($"[Download] 开始安装 {LoaderName}:" & Arguments)
             Dim process As New Process With {.StartInfo = Info}
             Dim LastResults As New Queue(Of String)
             Using outputWaitHandle As New AutoResetEvent(False)
@@ -1170,11 +1189,11 @@ Retry:
                             End If
                         Catch ex As ObjectDisposedException
                         Catch ex As Exception
-                            Log(ex, "读取 Forge 安装器信息失败")
+                            Log(ex, $"读取 {LoaderName} 安装器信息失败")
                         End Try
                         Try
                             If Task.State = LoadState.Aborted AndAlso Not process.HasExited Then
-                                Log("[Installer] 由于任务取消,已中止 Forge 安装")
+                                Log($"[Installer] 由于任务取消,已中止 {LoaderName} 安装")
                                 process.Kill()
                             End If
                         Catch
@@ -1193,11 +1212,11 @@ Retry:
                             End If
                         Catch ex As ObjectDisposedException
                         Catch ex As Exception
-                            Log(ex, "读取 Forge 安装器错误信息失败")
+                            Log(ex, $"读取 {LoaderName} 安装器错误信息失败")
                         End Try
                         Try
                             If Task.State = LoadState.Aborted AndAlso Not process.HasExited Then
-                                Log("[Installer] 由于任务取消,已中止 Forge 安装")
+                                Log($"[Installer] 由于任务取消,已中止 {LoaderName} 安装")
                                 process.Kill()
                             End If
                         Catch
@@ -1222,7 +1241,7 @@ Retry:
                     For i As Integer = Math.Max(0, LastResults.Count - 5) To LastResults.Count - 1 '最后 5 行
                         LastLines &= vbCrLf & LastResults(i)
                     Next
-                    Throw New Exception("Forge 安装器出错,日志结束部分为:" & LastLines)
+                    Throw New Exception($"{LoaderName} 安装器出错,日志结束部分为:" & LastLines)
                 End Using
             End Using
         End SyncLock
@@ -1273,31 +1292,46 @@ Retry:
     End Sub
 
     ''' <summary>
-    ''' 获取下载某个 Forge 版本的加载器列表。
+    ''' 获取下载某个 Forge / NeoForge 版本的加载器列表。
     ''' </summary>
-    Private Function McDownloadForgeLoader(Version As String, Inherit As String, Optional DownloadInfo As DlForgeVersionEntry = Nothing, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional ClientFolder As String = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase)
+    Private Function McDownloadForgeLoader(IsNeo As Boolean, Version As String, Inherit As String, Optional DownloadForgeInfo As DlForgeVersionEntry = Nothing, Optional DownloadNeoInfo As DlNeoForgeVersionEntry = Nothing, Optional McFolder As String = Nothing, Optional ClientDownloadLoader As LoaderCombo(Of String) = Nothing, Optional ClientFolder As String = Nothing, Optional FixLibrary As Boolean = True) As List(Of LoaderBase)
 
         '参数初始化
+        Dim DownloadInfo = If(IsNeo, DownloadNeoInfo, DownloadForgeInfo)
+        If IsNeo Then '兜底,有的时候 DownloadInfo 可能是空的
+            If DownloadNeoInfo Is Nothing Then
+                DownloadInfo = New DlNeoForgeVersionEntry With {.VersionName = Version}
+                Version = Version.Replace("neoforge-", "").Replace("-beta", "").Replace("1.20.1-", "")
+                DownloadInfo.VersionCode = Version
+                If Version.Contains("-beta") Then DownloadInfo.IsBeta = True
+            Else
+                DownloadInfo = DownloadNeoInfo
+                Version = DownloadInfo.VersionCode
+            End If
+        Else
+            DownloadInfo = DownloadForgeInfo
+        End If
         McFolder = If(McFolder, PathMcFolder)
-        If Version.StartsWithF("1.") AndAlso Version.Contains("-") Then
+        If Not IsNeo AndAlso Version.StartsWithF("1.") AndAlso Version.Contains("-") Then
             '类似 1.19.3-41.2.8 格式,优先使用 Version 中要求的版本而非 Inherit(例如 1.19.3 却使用了 1.19 的 Forge)
             Inherit = Version.Before("-")
             Version = Version.After("-")
         End If
+        Dim LoaderName As String = If(IsNeo, "NeoForge", "Forge")
         Dim IsCustomFolder As Boolean = McFolder <> PathMcFolder
-        Dim Id As String = Inherit & "-forge-" & Version
-        Dim InstallerAddress As String = PathTemp & "Cache\Code\ForgeInstall-" & Version & "_" & RandomInteger(0, 100000)
+        Dim Id As String = If(IsNeo, DownloadInfo.VersionName, Inherit & "-forge-" & Version)
+        Dim InstallerAddress As String = PathTemp & $"Cache\Code\{LoaderName}Install-" & Version & "_" & RandomInteger(0, 100000)
         Dim VersionFolder As String = McFolder & "versions\" & Id & "\"
-        Dim DisplayName As String = "Forge " & Inherit & " - " & Version
+        Dim DisplayName As String = $"{LoaderName} " & Inherit & " - " & Version
         Dim Loaders As New List(Of LoaderBase)
         Dim LibVersionFolder As String = PathMcFolder & "versions\" & Id & "\" '作为 Lib 文件目标的版本文件夹
 
         '获取下载信息
         If DownloadInfo Is Nothing Then
-            Loaders.Add(New LoaderTask(Of String, String)("获取 Mod 加载器详细信息",
+            Loaders.Add(New LoaderTask(Of String, String)($"获取 {LoaderName} 详细信息",
             Sub(Task As LoaderTask(Of String, String))
-                '获取 Forge 版本列表
-                Dim ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("McDownloadForgeLoader " & Inherit, AddressOf DlForgeVersionMain)
+                '获取 Forge / NeoForge 版本列表
+                Dim ForgeLoader = If(IsNeo, New LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))("McDownloadNeoForgeLoader " & Inherit, AddressOf DlNeoForgeVersionMain), New LoaderTask(Of String, List(Of DlForgeVersionEntry))("McDownloadForgeLoader " & Inherit, AddressOf DlForgeVersionMain))
                 ForgeLoader.WaitForExit(Inherit)
                 Task.Progress = 0.8
                 '查找对应版本
@@ -1307,11 +1341,11 @@ Retry:
                         Exit Sub
                     End If
                 Next
-                Throw New Exception("未能找到 Forge " & Inherit & "-" & Version & " 的详细信息!")
+                Throw New Exception($"未能找到 {LoaderName} " & Inherit & "-" & Version & " 的详细信息!")
             End Sub) With {.ProgressWeight = 3})
         End If
-        '下载 Forge 主文件
-        Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("准备 Mod 加载器下载",
+        '下载 Forge / NeoForge 主文件
+        Loaders.Add(New LoaderTask(Of String, List(Of NetFile))($"准备下载 {LoaderName}",
             Sub(Task As LoaderTask(Of String, List(Of NetFile)))
                 '启动依赖版本的下载
                 If ClientDownloadLoader Is Nothing Then
@@ -1319,19 +1353,28 @@ Retry:
                     ClientDownloadLoader = McDownloadClient(NetPreDownloadBehaviour.ExitWhileExistsOrDownloading, Inherit)
                 End If
                 '添加主文件
-                Dim Files As New List(Of NetFile) From {New NetFile({
+                Dim Files
+                If IsNeo Then 'NeoForge
+                    Dim IsLegacyNeo As Boolean = DownloadInfo.VersionCode.StartsWith("47.")
+                    Files = New List(Of NetFile) From {New NetFile({
+                        $"https://bmclapi2.bangbang93.com/maven/net/neoforged/{If(IsLegacyNeo, "", "neo")}forge/{If(IsLegacyNeo, "1.20.1-", "")}{DownloadInfo.VersionCode}{If(DownloadInfo.IsBeta, "-beta", "")}/{DownloadInfo.FileName}",
+                        $"https://maven.neoforged.net/releases/net/neoforged/{If(IsLegacyNeo, "", "neo")}forge/{If(IsLegacyNeo, "1.20.1-", "")}{DownloadInfo.VersionCode}{If(DownloadInfo.IsBeta, "-beta", "")}/{DownloadInfo.FileName}"
+                    }, InstallerAddress, New FileChecker(MinSize:=64 * 1024, Hash:=If(IsNeo, Nothing, DownloadInfo.Hash)))}
+                Else 'Forge
+                    Files = New List(Of NetFile) From {New NetFile({
                     "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/" & Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName,
                     "https://files.minecraftforge.net/maven/net/minecraftforge/forge/" & Inherit & "-" & DownloadInfo.FileVersion & "/" & DownloadInfo.FileName
-                }, InstallerAddress, New FileChecker(MinSize:=64 * 1024, Hash:=DownloadInfo.Hash))}
+                }, InstallerAddress, New FileChecker(MinSize:=64 * 1024, Hash:=If(IsNeo, Nothing, DownloadInfo.Hash)))}
+                End If
                 Task.Output = Files
             End Sub) With {.ProgressWeight = 0.5, .Show = False})
-        Loaders.Add(New LoaderDownload("下载 Mod 加载器主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
+        Loaders.Add(New LoaderDownload($"下载 {LoaderName} 主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
 
         '安装(仅在新版安装时需要原版 Jar)
         If Version.Before(".") >= 20 Then
-            Log("[Download] 检测为新版 Forge:" & Version)
+            Log($"[Download] 检测为{If(IsNeo, " Neo", "新版 ")}Forge:" & Version)
             Dim Libs As List(Of McLibToken) = Nothing
-            Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析 Mod 加载器支持库文件",
+            Loaders.Add(New LoaderTask(Of String, List(Of NetFile))($"分析 {LoaderName} 支持库文件",
             Sub(Task As LoaderTask(Of String, List(Of NetFile)))
                 Task.Output = New List(Of NetFile)
                 Dim Installer As ZipArchive = Nothing
@@ -1357,27 +1400,27 @@ Retry:
                         Log("[Download] 需要下载 Mappings:" & Address)
                     End If
                     Task.Progress = 0.8
-                    '去除其中的原始 Forge 项
+                    '去除其中的原始 Forge / NeoForge 项
                     For i = 0 To Libs.Count - 1
-                        If Libs(i).LocalPath.EndsWithF("forge-" & Inherit & "-" & Version & ".jar") OrElse
+                        If Libs(i).LocalPath.EndsWithF($"{If(IsNeo, "neo", "")}forge-" & Inherit & "-" & Version & ".jar") OrElse
                                                                                                       Libs(i).LocalPath.EndsWithF("forge-" & Inherit & "-" & Version & "-client.jar") Then
-                            Log("[Download] 已从待下载 Forge 支持库中移除:" & Libs(i).LocalPath, LogLevel.Debug)
+                            Log($"[Download] 已从待下载 {LoaderName} 支持库中移除:" & Libs(i).LocalPath, LogLevel.Debug)
                             Libs.RemoveAt(i)
                             Exit For
                         End If
                     Next
                     Task.Output = McLibFixFromLibToken(Libs, PathMcFolder)
                 Catch ex As Exception
-                    Throw New Exception("获取新版 Forge 支持库列表失败", ex)
+                    Throw New Exception($"获取{If(IsNeo, " Neo", "新版 ")}Forge 支持库列表失败", ex)
                 Finally
                     '释放文件
                     If Installer IsNot Nothing Then Installer.Dispose()
                 End Try
             End Sub) With {.ProgressWeight = 2})
-            Loaders.Add(New LoaderDownload("下载 Mod 加载器支持库文件", New List(Of NetFile)) With {.ProgressWeight = 12})
-            Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("获取 Mod 下载器支持库文件",
+            Loaders.Add(New LoaderDownload($"下载 {LoaderName} 支持库文件", New List(Of NetFile)) With {.ProgressWeight = 12})
+            Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)($"获取 {LoaderName} 支持库文件",
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
-#Region "Forge 文件"
+#Region "Forge / NeoForge 文件"
                 If IsCustomFolder Then
                     For Each LibFile As McLibToken In Libs
                         Dim RealPath As String = LibFile.LocalPath.Replace(PathMcFolder, McFolder)
@@ -1385,7 +1428,7 @@ Retry:
                             Directory.CreateDirectory(IO.Path.GetDirectoryName(RealPath))
                             CopyFile(LibFile.LocalPath, RealPath)
                         End If
-                        If ModeDebug Then Log("[Download] 复制的 Forge 支持库文件:" & LibFile.LocalPath)
+                        If ModeDebug Then Log($"[Download] 复制的 {LoaderName} 支持库文件:" & LibFile.LocalPath)
                     Next
                 End If
 #End Region
@@ -1395,7 +1438,7 @@ Retry:
                 Dim TargetLoaders As List(Of LoaderBase) =
                                                                                 ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                                                                                 Where(Function(l) l.State <> LoadState.Finished).ToList
-                If TargetLoaders.Any Then Log("[Download] Forge 安装正在等待原版文件下载完成")
+                If TargetLoaders.Any Then Log($"[Download] {LoaderName} 安装正在等待原版文件下载完成")
                 Do While TargetLoaders.Any AndAlso Not Task.IsAborted
                     TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
                     Thread.Sleep(50)
@@ -1415,11 +1458,11 @@ Retry:
                 End SyncLock
 #End Region
             End Sub) With {.ProgressWeight = 0.1, .Show = False})
-            Loaders.Add(New LoaderTask(Of Boolean, Boolean)("安装 Mod 加载器(方式 A)",
+            Loaders.Add(New LoaderTask(Of Boolean, Boolean)(If(IsNeo, "安装 NeoForge", "安装 Forge(方式 A)"),
             Sub(Task As LoaderTask(Of Boolean, Boolean))
                 Dim Installer As ZipArchive = Nothing
                 Try
-                    Log("[Download] 开始进行新版方式 Forge 安装:" & InstallerAddress)
+                    Log($"[Download] 开始进行{If(IsNeo, " Neo", "新版方式 ")}Forge 安装:" & InstallerAddress)
                     '记录当前文件夹列表(在新建目标文件夹之前)
                     Dim OldList = New DirectoryInfo(McFolder & "versions\").EnumerateDirectories.
                                                                         Select(Function(i) i.FullName).ToList()
@@ -1440,15 +1483,15 @@ Retry:
                         WriteFile(PathTemp & "Cache\forge_installer.jar", GetResources("ForgeInstaller"))
                         Task.Progress = 0.06
                         '运行注入器
-                        ForgeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper)
+                        ForgeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper, False)
                         Task.Progress = 0.97
                     Catch ex As Exception
                         If UseJavaWrapper Then
-                            Log(ex, "使用 JavaWrapper 安装 Forge 失败,将不使用 JavaWrapper 并重试")
+                            Log(ex, $"使用 JavaWrapper 安装 {LoaderName} 失败,将不使用 JavaWrapper 并重试")
                             UseJavaWrapper = False
                             GoTo Retry
                         Else
-                            Throw New Exception("运行 Forge 安装器失败", ex)
+                            Throw New Exception($"运行 {LoaderName} 安装器失败", ex)
                         End If
                     End Try
                     '拷贝新增的版本 Json
@@ -1457,7 +1500,7 @@ Retry:
                     If DeltaList.Count > 1 Then
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空
-                        DeltaList = DeltaList.Where(Function(l) l.Name.Contains("forge") AndAlso l.EnumerateFiles.Any).ToList
+                        DeltaList = DeltaList.Where(Function(l) l.Name.Contains($"{If(IsNeo, "neo", "")}forge") AndAlso l.EnumerateFiles.Any).ToList
                     End If
                     If DeltaList.Count = 1 Then '如果没有新增文件夹,那么预测的文件夹名就是正确的
                         Dim JsonFile As FileInfo = DeltaList(0).EnumerateFiles.First()
@@ -1466,20 +1509,20 @@ Retry:
                     '新建 mods 文件夹
                     Directory.CreateDirectory(New McVersion(VersionFolder).GetPathIndie(True) & "mods\")
                 Catch ex As Exception
-                    Throw New Exception("安装新 Forge 版本失败", ex)
+                    Throw New Exception($"安装新 {LoaderName} 版本失败", ex)
                 Finally
                     '清理文件
                     Try
                         If Installer IsNot Nothing Then Installer.Dispose()
                         If File.Exists(InstallerAddress) Then File.Delete(InstallerAddress)
                     Catch ex As Exception
-                        Log(ex, "安装 Forge 清理文件时出错")
+                        Log(ex, $"安装 {LoaderName} 清理文件时出错")
                     End Try
                 End Try
             End Sub) With {.ProgressWeight = 10})
         Else
             Log("[Download] 检测为非新版 Forge:" & Version)
-            Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("安装 Mod 加载器(方式 B)",
+            Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("安装 Forge(方式 B)",
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
                 Dim Installer As ZipArchive = Nothing
                 Try
@@ -1534,8 +1577,8 @@ Retry:
             End Sub) With {.ProgressWeight = 1})
             If FixLibrary Then
                 If IsCustomFolder Then Throw New Exception("若需要补全支持库,就不能自定义 MC 文件夹")
-                Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析 Mod 加载器支持库文件", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionFolder))) With {.ProgressWeight = 1, .Show = False})
-                Loaders.Add(New LoaderDownload("下载 Mod 加载器支持库文件", New List(Of NetFile)) With {.ProgressWeight = 11})
+                Loaders.Add(New LoaderTask(Of String, List(Of NetFile))($"分析 {LoaderName} 支持库文件", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionFolder))) With {.ProgressWeight = 1, .Show = False})
+                Loaders.Add(New LoaderDownload($"下载 {LoaderName} 支持库文件", New List(Of NetFile)) With {.ProgressWeight = 11})
             End If
         End If
 
@@ -1634,7 +1677,7 @@ Retry:
         Else
             Version = sender.Parent.Parent.Tag
         End If
-        McDownloadForgeSave(Version)
+        McDownloadForgeSave(False, DownloadForgeInfo:=Version)
     End Sub
 
 #End Region
@@ -1696,6 +1739,103 @@ Retry:
 
 #End Region
 
+#Region "NeoForge 下载菜单"
+
+    Public Sub NeoForgeDownloadListItemPreload(Stack As StackPanel, Entrys As List(Of DlNeoForgeVersionEntry), OnClick As MyListItem.ClickEventHandler, IsSaveOnly As Boolean)
+        '获取最新稳定版和测试版
+        Dim FreshStableVersion As DlNeoForgeVersionEntry = Nothing
+        Dim FreshBetaVersion As DlNeoForgeVersionEntry = Nothing
+        If Entrys.Any Then
+            Dim VersionsArray = Entrys.ToList()
+            VersionsArray.Reverse()
+            For Each Entry In VersionsArray
+                If Entry.IsBeta Then
+                    FreshBetaVersion = Entry
+                Else
+                    If Entry.VersionCode.Contains("47.1.82") Then Continue For
+                    FreshStableVersion = Entry
+                End If
+            Next
+        Else
+            Log("[System] 未找到可用的 NeoForge 版本", LogLevel.Debug)
+        End If
+        '显示各个版本
+        If FreshStableVersion IsNot Nothing Then
+            Dim Fresh = NeoForgeDownloadListItem(FreshStableVersion, OnClick, IsSaveOnly)
+            Fresh.Info = "最新版" & If(Fresh.Info = "", "", "," & Fresh.Info)
+            Stack.Children.Add(Fresh)
+        End If
+        If FreshBetaVersion IsNot Nothing Then
+            Dim Fresh = NeoForgeDownloadListItem(FreshBetaVersion, OnClick, IsSaveOnly)
+            Fresh.Info = "最新版" & If(Fresh.Info = "", "", "," & Fresh.Info)
+            Stack.Children.Add(Fresh)
+        End If
+        '添加间隔
+        Stack.Children.Add(New TextBlock With {.Text = "全部版本 (" & Entrys.Count & ")", .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 13, 0, 4)})
+    End Sub
+    Public Function NeoForgeDownloadListItem(Entry As DlNeoForgeVersionEntry, OnClick As MyListItem.ClickEventHandler, IsSaveOnly As Boolean) As MyListItem
+        '建立控件
+        Dim NewItem As New MyListItem With {
+            .Title = If(Entry.IsBeta, Entry.VersionCode & "-beta", Entry.VersionCode), .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
+            .Info = If(Entry.IsBeta, "测试版", "稳定版"),
+            .Logo = PathImage & "Blocks/NeoForge.png"
+        }
+        AddHandler NewItem.Click, OnClick
+        '建立菜单
+        If IsSaveOnly Then
+            NewItem.ContentHandler = AddressOf NeoForgeSaveContMenuBuild
+        Else
+            NewItem.ContentHandler = AddressOf NeoForgeContMenuBuild
+        End If
+        '结束
+        Return NewItem
+    End Function
+    Private Sub NeoForgeContMenuBuild(sender As MyListItem, e As EventArgs)
+        Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
+        ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
+        ToolTipService.SetVerticalOffset(BtnSave, 30)
+        ToolTipService.SetHorizontalOffset(BtnSave, 2)
+        AddHandler BtnSave.Click, AddressOf NeoForgeSave_Click
+        Dim BtnInfo As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonInfo, .ToolTip = "更新日志"}
+        ToolTipService.SetPlacement(BtnInfo, Primitives.PlacementMode.Center)
+        ToolTipService.SetVerticalOffset(BtnInfo, 30)
+        ToolTipService.SetHorizontalOffset(BtnInfo, 2)
+        AddHandler BtnInfo.Click, AddressOf NeoForgeLog_Click
+        sender.Buttons = {BtnSave, BtnInfo}
+    End Sub
+    Private Sub NeoForgeSaveContMenuBuild(sender As MyListItem, e As EventArgs)
+        Dim BtnInfo As New MyIconButton With {.LogoScale = 1.05, .Logo = Logo.IconButtonInfo, .ToolTip = "更新日志"}
+        ToolTipService.SetPlacement(BtnInfo, Primitives.PlacementMode.Center)
+        ToolTipService.SetVerticalOffset(BtnInfo, 30)
+        ToolTipService.SetHorizontalOffset(BtnInfo, 2)
+        AddHandler BtnInfo.Click, AddressOf NeoForgeLog_Click
+        sender.Buttons = {BtnInfo}
+    End Sub
+    Private Sub NeoForgeLog_Click(sender As Object, e As RoutedEventArgs)
+        Dim Version As DlNeoForgeVersionEntry
+        If sender.Tag IsNot Nothing Then
+            Version = sender.Tag
+        ElseIf sender.Parent.Tag IsNot Nothing Then
+            Version = sender.Parent.Tag
+        Else
+            Version = sender.Parent.Parent.Tag
+        End If
+        OpenWebsite("https://maven.neoforged.net/releases/net/neoforged/neoforge/" & Version.VersionCode & "/" & Version.VersionName & "-changelog.txt")
+    End Sub
+    Public Sub NeoForgeSave_Click(sender As Object, e As RoutedEventArgs)
+        Dim Version As DlNeoForgeVersionEntry
+        If sender.Tag IsNot Nothing Then
+            Version = sender.Tag
+        ElseIf sender.Parent.Tag IsNot Nothing Then
+            Version = sender.Parent.Tag
+        Else
+            Version = sender.Parent.Parent.Tag
+        End If
+        McDownloadForgeSave(True, DownloadNeoInfo:=Version)
+    End Sub
+
+#End Region
+
 #Region "Fabric 下载"
 
     Public Sub McDownloadFabricLoaderSave(DownloadInfo As JObject)
@@ -1861,6 +2001,16 @@ Retry:
         ''' </summary>
         Public ForgeEntry As DlForgeVersionEntry = Nothing
 
+        '若要下载 NeoForge,则需要在下面两项中完成至少一项
+        ''' <summary>
+        ''' 欲下载的 NeoForge 版本名。
+        ''' </summary>
+        Public NeoForgeVersion As String = Nothing
+        ''' <summary>
+        ''' 欲下载的 NeoForge。
+        ''' </summary>
+        Public NeoForgeEntry As DlNeoForgeVersionEntry = Nothing
+
         ''' <summary>
         ''' 欲下载的 Fabric Loader 版本名。
         ''' </summary>
@@ -1961,7 +2111,7 @@ Retry:
     Public Function McInstallLoader(Request As McInstallRequest, Optional DontFixLibraries As Boolean = False) As List(Of LoaderBase)
         '获取缓存目录
         Dim PathInstallTemp As String
-        If PathTemp.Contains(" ") AndAlso Request.ForgeEntry IsNot Nothing Then
+        If PathTemp.Contains(" ") AndAlso (Request.ForgeEntry IsNot Nothing OrElse Request.NeoForgeEntry IsNot Nothing) Then
             PathInstallTemp = OsDrive & "ProgramData\PCL\Install\"
         Else
             PathInstallTemp = PathTemp & "Install\"
@@ -1998,6 +2148,9 @@ Retry:
         Dim ForgeFolder As String = Nothing
         If Request.ForgeEntry IsNot Nothing Then Request.ForgeVersion = If(Request.ForgeVersion, Request.ForgeEntry.Version)
         If Request.ForgeVersion IsNot Nothing Then ForgeFolder = TempMcFolder & "versions\" & Request.MinecraftName & "-forge-" & Request.ForgeVersion
+        Dim NeoForgeFolder As String = Nothing
+        If Request.NeoForgeEntry IsNot Nothing Then Request.NeoForgeVersion = If(Request.NeoForgeVersion, Request.NeoForgeEntry.VersionName)
+        If Request.NeoForgeVersion IsNot Nothing Then NeoForgeFolder = TempMcFolder & "versions\" & If(Request.NeoForgeVersion.StartsWithF("1.20.1") OrElse Request.NeoForgeVersion.StartsWithF("47."), "1.20.1-forge-", "") & Request.NeoForgeVersion.Replace("1.20.1-forge-", "").Replace("1.20.1-", "")
         Dim FabricFolder As String = Nothing
         If Request.FabricVersion IsNot Nothing Then FabricFolder = TempMcFolder & "versions\fabric-loader-" & Request.FabricVersion & "-" & Request.MinecraftName
         Dim LiteLoaderFolder As String = Nothing
@@ -2014,6 +2167,8 @@ Retry:
         '记录日志
         If OptiFineFolder IsNot Nothing Then Log("[Download] OptiFine 缓存:" & OptiFineFolder)
         If ForgeFolder IsNot Nothing Then Log("[Download] Forge 缓存:" & ForgeFolder)
+        If NeoForgeFolder IsNot Nothing Then Log("[Download] NeoForge 缓存:" & NeoForgeFolder)
+        If Request.NeoForgeVersion IsNot Nothing Then Log("[Download] NeoForge 版本:" & Request.NeoForgeVersion)
         If FabricFolder IsNot Nothing Then Log("[Download] Fabric 缓存:" & FabricFolder)
         If LiteLoaderFolder IsNot Nothing Then Log("[Download] LiteLoader 缓存:" & LiteLoaderFolder)
         Log("[Download] 对应的原版版本:" & Request.MinecraftName)
@@ -2048,7 +2203,11 @@ Retry:
         End If
         'Forge
         If Request.ForgeVersion IsNot Nothing Then
-            LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & Request.ForgeVersion, McDownloadForgeLoader(Request.ForgeVersion, Request.MinecraftName, Request.ForgeEntry, TempMcFolder, ClientLoader, Request.TargetVersionFolder, False)) With {.Show = False, .ProgressWeight = 25, .Block = Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
+            LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & Request.ForgeVersion, McDownloadForgeLoader(False, Request.ForgeVersion, Request.MinecraftName, Request.ForgeEntry, , TempMcFolder, ClientLoader, Request.TargetVersionFolder, False)) With {.Show = False, .ProgressWeight = 25, .Block = Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing AndAlso Request.NeoForgeEntry Is Nothing})
+        End If
+        'NeoForge
+        If Request.NeoForgeVersion IsNot Nothing Then
+            LoaderList.Add(New LoaderCombo(Of String)("下载 NeoForge " & Request.NeoForgeVersion, McDownloadForgeLoader(True, Request.NeoForgeVersion, Request.MinecraftName, , Request.NeoForgeEntry, TempMcFolder, ClientLoader, Request.TargetVersionFolder, False)) With {.Show = False, .ProgressWeight = 25, .Block = Request.ForgeEntry Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
         End If
         'LiteLoader
         If Request.LiteLoaderEntry IsNot Nothing Then
@@ -2061,14 +2220,14 @@ Retry:
         '合并安装
         LoaderList.Add(New LoaderTask(Of String, String)("安装游戏",
             Sub(Task As LoaderTask(Of String, String))
-                InstallMerge(OutputFolder, OutputFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, FabricFolder, LiteLoaderFolder)
+                InstallMerge(OutputFolder, OutputFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, NeoForgeFolder, Request.NeoForgeVersion, FabricFolder, LiteLoaderFolder)
                 Task.Progress = 0.3
                 If Directory.Exists(TempMcFolder & "libraries") Then CopyDirectory(TempMcFolder & "libraries", PathMcFolder & "libraries")
                 If Directory.Exists(TempMcFolder & "mods") Then CopyDirectory(TempMcFolder & "mods", PathMcFolder & "mods")
             End Sub) With {.ProgressWeight = 2, .Block = True})
         '补全文件
         If Not DontFixLibraries AndAlso
-        (Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.Before(".") >= 20) OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing) Then
+        (Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.Before(".") >= 20) OrElse (Request.NeoForgeVersion IsNot Nothing) OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing) Then
             Dim LoadersLib As New List(Of LoaderBase)
             LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(OutputFolder))) With {.ProgressWeight = 1, .Show = False})
             LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
@@ -2084,17 +2243,18 @@ Retry:
     ''' <summary>
     ''' 将多个版本 Json 进行合并,如果目标已存在则直接覆盖。失败会抛出异常。
     ''' </summary>
-    Private Sub InstallMerge(OutputFolder As String, MinecraftFolder As String, Optional OptiFineFolder As String = Nothing, Optional OptiFineAsMod As Boolean = False, Optional ForgeFolder As String = Nothing, Optional ForgeVersion As String = Nothing, Optional FabricFolder As String = Nothing, Optional LiteLoaderFolder As String = Nothing)
+    Private Sub InstallMerge(OutputFolder As String, MinecraftFolder As String, Optional OptiFineFolder As String = Nothing, Optional OptiFineAsMod As Boolean = False, Optional ForgeFolder As String = Nothing, Optional ForgeVersion As String = Nothing, Optional NeoForgeFolder As String = Nothing, Optional NeoForgeVersion As String = Nothing, Optional FabricFolder As String = Nothing, Optional LiteLoaderFolder As String = Nothing)
         Log("[Download] 开始进行版本合并,输出:" & OutputFolder & ",Minecraft:" & MinecraftFolder &
             If(OptiFineFolder IsNot Nothing, ",OptiFine:" & OptiFineFolder, "") &
             If(ForgeFolder IsNot Nothing, ",Forge:" & ForgeFolder, "") &
+            If(NeoForgeFolder IsNot Nothing, ",NeoForge:" & NeoForgeFolder, "") &
             If(LiteLoaderFolder IsNot Nothing, ",LiteLoader:" & LiteLoaderFolder, "") &
             If(FabricFolder IsNot Nothing, ",Fabric:" & FabricFolder, ""))
         Directory.CreateDirectory(OutputFolder)
 
-        Dim HasOptiFine As Boolean = OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod, HasForge As Boolean = ForgeFolder IsNot Nothing, HasLiteLoader As Boolean = LiteLoaderFolder IsNot Nothing, HasFabric As Boolean = FabricFolder IsNot Nothing
-        Dim OutputName As String, MinecraftName As String, OptiFineName As String, ForgeName As String, LiteLoaderName As String, FabricName As String
-        Dim OutputJsonPath As String, MinecraftJsonPath As String, OptiFineJsonPath As String = Nothing, ForgeJsonPath As String = Nothing, LiteLoaderJsonPath As String = Nothing, FabricJsonPath As String = Nothing
+        Dim HasOptiFine As Boolean = OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod, HasForge As Boolean = ForgeFolder IsNot Nothing, HasNeoForge As Boolean = NeoForgeFolder IsNot Nothing, HasLiteLoader As Boolean = LiteLoaderFolder IsNot Nothing, HasFabric As Boolean = FabricFolder IsNot Nothing
+        Dim OutputName As String, MinecraftName As String, OptiFineName As String, ForgeName As String, NeoForgeName As String, LiteLoaderName As String, FabricName As String
+        Dim OutputJsonPath As String, MinecraftJsonPath As String, OptiFineJsonPath As String = Nothing, ForgeJsonPath As String = Nothing, NeoForgeJsonPath As String = Nothing, LiteLoaderJsonPath As String = Nothing, FabricJsonPath As String = Nothing
         Dim OutputJar As String, MinecraftJar As String
 #Region "初始化路径信息"
         If Not OutputFolder.EndsWithF("\") Then OutputFolder += "\"
@@ -2119,6 +2279,12 @@ Retry:
             ForgeJsonPath = ForgeFolder & ForgeName & ".json"
         End If
 
+        If HasNeoForge Then
+            If Not NeoForgeFolder.EndsWithF("\") Then NeoForgeFolder += "\"
+            NeoForgeName = GetFolderNameFromPath(NeoForgeFolder)
+            NeoForgeJsonPath = NeoForgeFolder & NeoForgeName & ".json"
+        End If
+
         If HasLiteLoader Then
             If Not LiteLoaderFolder.EndsWithF("\") Then LiteLoaderFolder += "\"
             LiteLoaderName = GetFolderNameFromPath(LiteLoaderFolder)
@@ -2132,7 +2298,7 @@ Retry:
         End If
 #End Region
 
-        Dim OutputJson As JObject, MinecraftJson As JObject, OptiFineJson As JObject = Nothing, ForgeJson As JObject = Nothing, LiteLoaderJson As JObject = Nothing, FabricJson As JObject = Nothing
+        Dim OutputJson As JObject, MinecraftJson As JObject, OptiFineJson As JObject = Nothing, ForgeJson As JObject = Nothing, NeoForgeJson As JObject = Nothing, LiteLoaderJson As JObject = Nothing, FabricJson As JObject = Nothing
 #Region "读取文件并检查文件是否合规"
         Dim MinecraftJsonText As String = ReadFile(MinecraftJsonPath)
         If Not MinecraftJsonText.StartsWithF("{") Then Throw New Exception("Minecraft json 有误,地址:" & MinecraftJsonPath & ",前段内容:" & MinecraftJsonText.Substring(0, Math.Min(MinecraftJsonText.Length, 1000)))
@@ -2150,6 +2316,12 @@ Retry:
             ForgeJson = GetJson(ForgeJsonText)
         End If
 
+        If HasNeoForge Then
+            Dim NeoForgeJsonText As String = ReadFile(NeoForgeJsonPath)
+            If Not NeoForgeJsonText.StartsWithF("{") Then Throw New Exception("NeoForge json 有误,地址:" & NeoForgeJsonPath & ",前段内容:" & NeoForgeJsonText.Substring(0, Math.Min(NeoForgeJsonText.Length, 1000)))
+            NeoForgeJson = GetJson(NeoForgeJsonText)
+        End If
+
         If HasLiteLoader Then
             Dim LiteLoaderJsonText As String = ReadFile(LiteLoaderJsonPath)
             If Not LiteLoaderJsonText.StartsWithF("{") Then Throw New Exception("LiteLoader json 有误,地址:" & LiteLoaderJsonPath & ",前段内容:" & LiteLoaderJsonText.Substring(0, Math.Min(LiteLoaderJsonText.Length, 1000)))
@@ -2169,6 +2341,7 @@ Retry:
             If(MinecraftJson("minecraftArguments"), " ").ToString & " " &
             If(OptiFineJson IsNot Nothing, If(OptiFineJson("minecraftArguments"), " ").ToString, " ") & " " &
             If(ForgeJson IsNot Nothing, If(ForgeJson("minecraftArguments"), " ").ToString, " ") & " " &
+            If(NeoForgeJson IsNot Nothing, If(NeoForgeJson("minecraftArguments"), " ").ToString, " ") & " " &
             If(LiteLoaderJson IsNot Nothing, If(LiteLoaderJson("minecraftArguments"), " ").ToString, " ")
         '分割参数字符串
         Dim RawArguments As List(Of String) = AllArguments.Split(" ").Where(Function(l) l <> "").Select(Function(l) l.Trim).ToList
@@ -2198,6 +2371,12 @@ Retry:
             ForgeJson.Remove("time")
             OutputJson.Merge(ForgeJson)
         End If
+        If HasNeoForge Then
+            '合并 NeoForge
+            NeoForgeJson.Remove("releaseTime")
+            NeoForgeJson.Remove("time")
+            OutputJson.Merge(NeoForgeJson)
+        End If
         If HasLiteLoader Then
             '合并 LiteLoader
             LiteLoaderJson.Remove("releaseTime")

+ 19 - 0
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml

@@ -79,6 +79,24 @@
                                         Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
                         </Grid>
                     </local:MyCard>
+                    <local:MyCard Title="NeoForge" Height="40" Margin="0,15,0,0" x:Name="CardNeoForge" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
+                        <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanNeoForge" />
+                        <Grid x:Name="PanNeoForgeInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
+                            <Grid.RenderTransform>
+                                <TranslateTransform />
+                            </Grid.RenderTransform>
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" />
+                                <ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <Image x:Name="ImgNeoForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/NeoForge.png" />
+                            <TextBlock x:Name="LabNeoForge" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
+                        </Grid>
+                        <Grid x:Name="BtnNeoForgeClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
+                            <Path x:Name="BtnNeoForgeClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
+                    Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        </Grid>
+                    </local:MyCard>
                     <local:MyCard Title="Fabric" Height="40" Margin="0,15,0,0" x:Name="CardFabric" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabric" />
                         <Grid x:Name="PanFabricInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
@@ -178,6 +196,7 @@
             <local:MyLoading Text="正在获取版本列表" Margin="20,20,20,17" x:Name="LoadMinecraft" HorizontalAlignment="Center" VerticalAlignment="Center" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadOptiFine" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadForge" HasAnimation="False" />
+            <local:MyLoading Visibility="Collapsed" x:Name="LoadNeoForge" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadLiteLoader" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadFabric" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadFabricApi" HasAnimation="False" />

+ 141 - 2
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb

@@ -51,6 +51,7 @@
         CardOptiFine.IsSwaped = True
         CardLiteLoader.IsSwaped = True
         CardForge.IsSwaped = True
+        CardNeoForge.IsSwaped = True
         CardFabric.IsSwaped = True
         CardFabricApi.IsSwaped = True
         CardOptiFabric.IsSwaped = True
@@ -73,6 +74,13 @@
             ForgeLoader.Start(SelectedMinecraftId)
         End If
 
+        '启动 NeoForge 加载
+        If SelectedMinecraftId.StartsWith("1.") AndAlso Int(SelectedMinecraftId.Split(".")(1)) > 19 Then
+            Dim NeoForgeLoader = New LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))("DlNeoForgeVersion " & SelectedMinecraftId, AddressOf DlNeoForgeVersionMain)
+            LoadNeoForge.State = NeoForgeLoader
+            NeoForgeLoader.Start(SelectedMinecraftId)
+        End If
+
         '启动 Fabric API、OptiFabric 加载
         DlFabricApiLoader.Start()
         DlOptiFabricLoader.Start()
@@ -86,6 +94,7 @@
                        OptiFine_Loaded()
                        LiteLoader_Loaded()
                        Forge_Loaded()
+                       NeoForge_Loaded()
                        Fabric_Loaded()
                        FabricApi_Loaded()
                        OptiFabric_Loaded()
@@ -102,6 +111,7 @@
                        BtnOptiFineClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardOptiFine.MainTextBlock, .Mode = BindingMode.OneWay})
                        BtnLiteLoaderClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardLiteLoader.MainTextBlock, .Mode = BindingMode.OneWay})
                        BtnForgeClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardForge.MainTextBlock, .Mode = BindingMode.OneWay})
+                       BtnNeoForgeClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardNeoForge.MainTextBlock, .Mode = BindingMode.OneWay})
                        BtnFabricClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardFabric.MainTextBlock, .Mode = BindingMode.OneWay})
                        BtnFabricApiClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardFabricApi.MainTextBlock, .Mode = BindingMode.OneWay})
                        BtnOptiFabricClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardOptiFabric.MainTextBlock, .Mode = BindingMode.OneWay})
@@ -213,6 +223,26 @@
         End If
     End Sub
 
+    'NeoForge
+    Private SelectedNeoForge As DlNeoForgeVersionEntry = Nothing
+    Private Sub SetNeoForgeInfoShow(IsShow As String)
+        If PanNeoForgeInfo.Tag = IsShow Then Exit Sub
+        PanNeoForgeInfo.Tag = IsShow
+        If IsShow = "True" Then
+            '显示信息栏
+            AniStart({
+                AaTranslateY(PanNeoForgeInfo, -CType(PanNeoForgeInfo.RenderTransform, TranslateTransform).Y, 270, 100, Ease:=New AniEaseOutBack),
+                AaOpacity(PanNeoForgeInfo, 1 - PanNeoForgeInfo.Opacity, 100, 90)
+            }, "SetNeoForgeInfoShow")
+        Else
+            '隐藏信息栏
+            AniStart({
+                AaTranslateY(PanNeoForgeInfo, 6 - CType(PanNeoForgeInfo.RenderTransform, TranslateTransform).Y, 200),
+                AaOpacity(PanNeoForgeInfo, -PanNeoForgeInfo.Opacity, 100)
+            }, "SetNeoForgeInfoShow")
+        End If
+    End Sub
+
     'Fabric
     Private SelectedFabric As String = Nothing
     Private Sub SetFabricInfoShow(IsShow As String)
@@ -277,7 +307,7 @@
     ''' <summary>
     ''' 重载已选择的项目的显示。
     ''' </summary>
-    Private Sub SelectReload() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
+    Private Sub SelectReload() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardNeoForge.Swap, LoadNeoForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
         If SelectedMinecraftId Is Nothing OrElse IsReloading Then Exit Sub
         IsReloading = True
         '主预览
@@ -341,6 +371,26 @@
             LabForge.Text = SelectedForge.Version
             LabForge.Foreground = ColorGray1
         End If
+        'NeoForge
+        If Not SelectedMinecraftId.Contains("1.") OrElse Val(SelectedMinecraftId.Split(".")(1)) <= 19 Then
+            CardNeoForge.Visibility = Visibility.Collapsed
+        Else
+            Dim NeoForgeError As String = LoadNeoForgeGetError()
+            CardNeoForge.MainSwap.Visibility = If(NeoForgeError Is Nothing, Visibility.Visible, Visibility.Collapsed)
+            If NeoForgeError IsNot Nothing Then CardNeoForge.IsSwaped = True
+            SetNeoForgeInfoShow(CardNeoForge.IsSwaped)
+            If SelectedNeoForge Is Nothing Then
+                BtnNeoForgeClear.Visibility = Visibility.Collapsed
+                ImgNeoForge.Visibility = Visibility.Collapsed
+                LabNeoForge.Text = If(NeoForgeError, "点击选择")
+                LabNeoForge.Foreground = ColorGray4
+            Else
+                BtnNeoForgeClear.Visibility = Visibility.Visible
+                ImgNeoForge.Visibility = Visibility.Visible
+                LabNeoForge.Text = SelectedNeoForge.VersionCode & If(SelectedNeoForge.IsBeta, "-beta", "")
+                LabNeoForge.Foreground = ColorGray1
+            End If
+        End If
         'Fabric
         If SelectedMinecraftId.Contains("1.") AndAlso Val(SelectedMinecraftId.Split(".")(1)) <= 13 Then
             CardFabric.Visibility = Visibility.Collapsed
@@ -434,6 +484,7 @@
         SelectedOptiFine = Nothing
         SelectedLiteLoader = Nothing
         SelectedForge = Nothing
+        SelectedNeoForge = Nothing
         SelectedFabric = Nothing
         SelectedFabricApi = Nothing
         SelectedOptiFabric = Nothing
@@ -451,6 +502,9 @@
         If SelectedForge IsNot Nothing Then
             Name += "-Forge_" & SelectedForge.Version
         End If
+        If SelectedNeoForge IsNot Nothing Then
+            Name += "-NeoForge_" & SelectedNeoForge.VersionCode & If(SelectedNeoForge.IsBeta, "-beta", "")
+        End If
         If SelectedLiteLoader IsNot Nothing Then
             Name += "-LiteLoader"
         End If
@@ -470,6 +524,9 @@
         If SelectedForge IsNot Nothing Then
             Info += ", Forge " & SelectedForge.Version
         End If
+        If SelectedNeoForge IsNot Nothing Then
+            Info += ", NeoForge " & SelectedNeoForge.VersionCode & If(SelectedNeoForge.IsBeta, "-beta", "")
+        End If
         If SelectedLiteLoader IsNot Nothing Then
             Info += ", LiteLoader"
         End If
@@ -487,6 +544,8 @@
             Return "pack://application:,,,/images/Blocks/Fabric.png"
         ElseIf SelectedForge IsNot Nothing Then
             Return "pack://application:,,,/images/Blocks/Anvil.png"
+        ElseIf SelectedNeoForge IsNot Nothing Then
+            Return "pack://application:,,,/images/Blocks/NeoForge.png"
         ElseIf SelectedLiteLoader IsNot Nothing Then
             Return "pack://application:,,,/images/Blocks/Egg.png"
         ElseIf SelectedOptiFine IsNot Nothing Then
@@ -706,6 +765,7 @@
         If SelectedForge IsNot Nothing AndAlso Not IsOptiFineSuitForForge(SelectedOptiFine, SelectedForge) Then SelectedForge = Nothing
         OptiFabric_Loaded()
         Forge_Loaded()
+        NeoForge_Loaded()
         CardOptiFine.IsSwaped = True
         SelectReload()
     End Sub
@@ -715,6 +775,7 @@
         CardOptiFine.IsSwaped = True
         e.Handled = True
         Forge_Loaded()
+        NeoForge_Loaded()
         SelectReload()
     End Sub
 
@@ -730,6 +791,7 @@
         If LoadLiteLoader Is Nothing OrElse LoadLiteLoader.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……"
         If LoadLiteLoader.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadLiteLoader.State, Object).Error.Message
         For Each Version As DlLiteLoaderListEntry In DlLiteLoaderListLoader.Output.Value
+            If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
             If Version.Inherit = SelectedMinecraftId Then Return Nothing
         Next
         Return "没有可用版本"
@@ -800,6 +862,7 @@
         Dim NotSuitForOptiFine As Boolean = False
         For Each Version In Loader.Output
             If Version.Category = "universal" OrElse Version.Category = "client" Then Continue For '跳过无法自动安装的版本
+            If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
             If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
             If SelectedOptiFine IsNot Nothing AndAlso
                 VersionSortInteger(SelectedMinecraftId, "1.13") >= 0 AndAlso VersionSortInteger("1.14.3", SelectedMinecraftId) >= 0 Then
@@ -866,6 +929,80 @@
 
 #End Region
 
+#Region "NeoForge 列表"
+
+    ''' <summary>
+    ''' 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。
+    ''' </summary>
+    Private Function LoadNeoForgeGetError() As String
+        If Not SelectedMinecraftId.StartsWith("1.") Then Return "没有可用版本"
+        If Not LoadNeoForge.State.IsLoader Then Return "正在获取版本列表……"
+        Dim Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)) = LoadNeoForge.State
+        If SelectedMinecraftId <> Loader.Input Then Return "正在获取版本列表……"
+        If Loader.State = LoadState.Loading Then Return "正在获取版本列表……"
+        If Loader.State = LoadState.Failed Then
+            Dim ErrorMessage As String = Loader.Error.Message
+            If ErrorMessage.Contains("没有可用版本") Then
+                Return "没有可用版本"
+            Else
+                Return "获取版本列表失败:" & ErrorMessage
+            End If
+        End If
+        If Loader.State <> LoadState.Finished Then Return "获取版本列表失败:未知错误,状态为 " & GetStringFromEnum(Loader.State)
+        For Each Version In Loader.Output
+            If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容"
+            If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
+            Return Nothing
+        Next
+        Return False
+    End Function
+
+    '限制展开
+    Private Sub CardNeoForge_PreviewSwap(sender As Object, e As RouteEventArgs) Handles CardNeoForge.PreviewSwap
+        If LoadNeoForgeGetError() IsNot Nothing Then e.Handled = True
+    End Sub
+
+    ''' <summary>
+    ''' 尝试重新可视化 NeoForge 版本列表。
+    ''' </summary>
+    Private Sub NeoForge_Loaded() Handles LoadForge.StateChanged
+        Try
+            If Not LoadNeoForge.State.IsLoader Then Exit Sub
+            Dim Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)) = LoadNeoForge.State
+            If SelectedMinecraftId <> Loader.Input Then Exit Sub
+            If Loader.State <> LoadState.Finished Then Exit Sub
+            '获取要显示的版本
+            Dim Versions As New List(Of DlNeoForgeVersionEntry)
+            Versions.AddRange(Loader.Output) '复制数组,以免 Output 在实例化后变空
+            If Not Loader.Output.Any() Then Exit Sub
+            PanNeoForge.Children.Clear()
+
+            NeoForgeDownloadListItemPreload(PanNeoForge, Versions, AddressOf NeoForge_Selected, False)
+            For Each Version In Versions
+                PanNeoForge.Children.Add(NeoForgeDownloadListItem(Version, AddressOf NeoForge_Selected, False))
+            Next
+        Catch ex As Exception
+            Log(ex, "可视化 NeoForge 安装版本列表出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    '选择与清除
+    Private Sub NeoForge_Selected(sender As MyListItem, e As EventArgs)
+        SelectedNeoForge = sender.Tag
+        CardNeoForge.IsSwaped = True
+        OptiFine_Loaded()
+        SelectReload()
+    End Sub
+    Private Sub NeoForge_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnNeoForgeClear.MouseLeftButtonUp
+        SelectedNeoForge = Nothing
+        CardNeoForge.IsSwaped = True
+        e.Handled = True
+        OptiFine_Loaded()
+        SelectReload()
+    End Sub
+
+#End Region
+
 #Region "Fabric 列表"
 
     ''' <summary>
@@ -876,6 +1013,7 @@
         If LoadFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadFabric.State, Object).Error.Message
         For Each Version As JObject In DlFabricListLoader.Output.Value("game")
             If Version("version").ToString = SelectedMinecraftId.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3") Then
+                If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
                 If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容"
                 'If SelectedOptiFine IsNot Nothing Then Return "与 OptiFine 不兼容"
                 Return Nothing
@@ -1141,7 +1279,7 @@
     End Sub
     Private Sub BtnSelectStart_Click() Handles BtnSelectStart.Click
         '确认版本隔离
-        If (SelectedForge IsNot Nothing OrElse SelectedFabric IsNot Nothing) AndAlso
+        If (SelectedForge IsNot Nothing OrElse SelectedFabric IsNot Nothing OrElse SelectedNeoForge IsNot Nothing) AndAlso
            (Setup.Get("LaunchArgumentIndie") = 0 OrElse Setup.Get("LaunchArgumentIndie") = 2) Then
             If MyMsgBox("你尚未开启版本隔离,这会导致多个 MC 共用同一个 Mod 文件夹。" & vbCrLf &
                         "因此在切换 MC 版本时,MC 会因为读取到与当前版本不符的 Mod 而崩溃。" & vbCrLf &
@@ -1157,6 +1295,7 @@
             .MinecraftName = SelectedMinecraftId,
             .OptiFineEntry = SelectedOptiFine,
             .ForgeEntry = SelectedForge,
+            .NeoForgeEntry = SelectedNeoForge,
             .FabricVersion = SelectedFabric,
             .FabricApi = SelectedFabricApi,
             .OptiFabric = SelectedOptiFabric,

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml


+ 10 - 1
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.vb

@@ -10,7 +10,7 @@
     ''' <summary>
     ''' 勾选事件改变页面。
     ''' </summary>
-    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemInstall.Check, ItemClient.Check, ItemOptiFine.Check, ItemForge.Check, ItemLiteLoader.Check, ItemMod.Check, ItemFabric.Check, ItemPack.Check
+    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemInstall.Check, ItemClient.Check, ItemOptiFine.Check, ItemForge.Check, ItemNeoForge.Check, ItemLiteLoader.Check, ItemMod.Check, ItemFabric.Check, ItemPack.Check
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
@@ -31,6 +31,9 @@
             Case FormMain.PageSubType.DownloadForge
                 If FrmDownloadForge Is Nothing Then FrmDownloadForge = New PageDownloadForge
                 Return FrmDownloadForge
+            Case FormMain.PageSubType.DownloadNeoForge
+                If FrmDownloadNeoForge Is Nothing Then FrmDownloadNeoForge = New PageDownloadNeoForge
+                Return FrmDownloadNeoForge
             Case FormMain.PageSubType.DownloadLiteLoader
                 If FrmDownloadLiteLoader Is Nothing Then FrmDownloadLiteLoader = New PageDownloadLiteLoader
                 Return FrmDownloadLiteLoader
@@ -91,6 +94,7 @@
                 DlClientListLoader.Start(IsForceRestart:=True)
                 DlOptiFineListLoader.Start(IsForceRestart:=True)
                 DlForgeListLoader.Start(IsForceRestart:=True)
+                DlNeoForgeListLoader.Start(IsForceRestart:=True)
                 DlLiteLoaderListLoader.Start(IsForceRestart:=True)
                 DlFabricListLoader.Start(IsForceRestart:=True)
                 DlFabricApiLoader.Start(IsForceRestart:=True)
@@ -117,6 +121,9 @@
             Case FormMain.PageSubType.DownloadForge
                 DlForgeListLoader.Start(IsForceRestart:=True)
                 ItemForge.Checked = True
+            Case FormMain.PageSubType.DownloadNeoForge
+                DlNeoForgeListLoader.Start(IsForceRestart:=True)
+                ItemNeoForge.Checked = True
             Case FormMain.PageSubType.DownloadLiteLoader
                 DlLiteLoaderListLoader.Start(IsForceRestart:=True)
                 ItemLiteLoader.Checked = True
@@ -154,6 +161,7 @@
         ItemOptiFine.Visibility = Visibility.Visible
         ItemFabric.Visibility = Visibility.Visible
         ItemForge.Visibility = Visibility.Visible
+        ItemNeoForge.Visibility = Visibility.Visible
         ItemLiteLoader.Visibility = Visibility.Visible
         RunInThread(Sub()
                         Thread.Sleep(20)
@@ -172,6 +180,7 @@
         ItemOptiFine.Visibility = Visibility.Collapsed
         ItemFabric.Visibility = Visibility.Collapsed
         ItemForge.Visibility = Visibility.Collapsed
+        ItemNeoForge.Visibility = Visibility.Collapsed
         ItemLiteLoader.Visibility = Visibility.Collapsed
         RunInThread(Sub()
                         Thread.Sleep(20)

+ 29 - 0
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml

@@ -0,0 +1,29 @@
+<local:MyPageRight
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+    mc:Ignorable="d" x:Class="PageDownloadNeoForge"
+    PanScroll="{Binding ElementName=PanBack}">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
+        <StackPanel Orientation="Vertical" Margin="5">
+            <local:MyCard Margin="20,20,20,0" Grid.IsSharedSizeScope="True" Title="NeoForge 简介" x:Name="CardTip">
+                <StackPanel Margin="25,40,15,20">
+                    <TextBlock Margin="0,0,0,11" Text="NeoForge 是新版 Minecraft 下的 Mod 加载器,你需要先安装它才能安装各种 NeoForge 模组。&#xa;本页面提供 NeoForge 安装器下载,在下载后你需要手动打开安装器进行安装。" TextWrapping="Wrap" />
+                    <Grid Height="35">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnWeb" Grid.Column="0" MinWidth="140" Text="打开官网" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <!--<local:MyCard x:Name="CardVersions" Margin="20,15,20,20" Title="版本列表">
+                <StackPanel x:Name="PanVersions" Margin="20,40,18,20" />
+            </local:MyCard>-->
+            <StackPanel Name="PanMain" Margin="20,15,20,5" Grid.IsSharedSizeScope="True" />
+            <local:MyCard HorizontalAlignment="Center" Margin="40,50" SnapsToDevicePixels="True" x:Name="PanLoad" UseAnimation="False">
+                <local:MyLoading Text="正在获取 NeoForge 列表" Margin="20,20,20,17" x:Name="Load" HorizontalAlignment="Center" VerticalAlignment="Center" />
+            </local:MyCard>
+        </StackPanel>
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 54 - 0
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadNeoForge.xaml.vb

@@ -0,0 +1,54 @@
+Public Class PageDownloadNeoForge
+
+    Private Sub LoaderInit() Handles Me.Initialized
+        PageLoaderInit(Load, PanLoad, PanMain, CardTip, DlNeoForgeListLoader, AddressOf Load_OnFinish)
+    End Sub
+    Private Sub Init() Handles Me.Loaded
+        PanBack.ScrollToHome()
+    End Sub
+
+    Private Sub Load_OnFinish()
+        '结果数据化
+        Try
+            Dim Versions = Sort(DlNeoForgeListLoader.Output.Value, AddressOf VersionSortBoolean)
+            PanMain.Children.Clear()
+            For Each Version As String In Versions
+                '增加卡片
+                Dim NewCard As New MyCard With {.Title = Version.Replace("_p", " P"), .Margin = New Thickness(0, 0, 0, 15), .SwapType = 13}
+                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Version}
+                NewCard.Children.Add(NewStack)
+                NewCard.SwapControl = NewStack
+                NewCard.IsSwaped = True
+                PanMain.Children.Add(NewCard)
+            Next
+        Catch ex As Exception
+            Log(ex, "可视化 NeoForge 版本列表出错", LogLevel.Feedback)
+        End Try
+    End Sub
+    Public Sub NeoForge_StateChanged(sender As MyLoading, newState As MyLoading.MyLoadingState, oldState As MyLoading.MyLoadingState)
+        If newState <> MyLoading.MyLoadingState.Stop Then Exit Sub
+
+        Dim Card As MyCard = CType(sender.Parent, FrameworkElement).Parent
+        Dim Loader As LoaderTask(Of String, List(Of DlNeoForgeVersionEntry)) = sender.State
+        '载入列表
+        Card.SwapControl.Children.Clear()
+        Card.SwapControl.Tag = Loader.Output
+        Card.SwapType = 14
+        Card.StackInstall()
+    End Sub
+    Public Sub NeoForge_Click(sender As MyLoading, e As MouseButtonEventArgs)
+        If sender.State.LoadingState = MyLoading.MyLoadingState.Error Then
+            CType(sender.State, LoaderTask(Of String, List(Of DlNeoForgeVersionEntry))).Start(IsForceRestart:=True)
+        End If
+    End Sub
+    Private Sub NeoForge_Selected(sender As MyListItem, e As EventArgs)
+        McDownloadForgeSave(True, sender.Tag)
+    End Sub
+    Public Sub DownloadStart(sender As MyListItem, e As Object)
+        McDownloadForge(True, sender.Tag)
+    End Sub
+    Private Sub BtnWeb_Click(sender As Object, e As EventArgs) Handles BtnWeb.Click
+        OpenWebsite("https://neoforged.net/")
+    End Sub
+
+End Class

+ 5 - 1
Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb

@@ -38,19 +38,23 @@
                         CardName = "常规版本"
                     Case McVersionCardType.API
                         Dim IsForgeExists As Boolean = False
+                        Dim IsNeoExists As Boolean = False
                         Dim IsFabricExists As Boolean = False
                         Dim IsLiteExists As Boolean = False
                         For Each Version As McVersion In Card.Value
                             If Version.Version.HasFabric Then IsFabricExists = True
                             If Version.Version.HasLiteLoader Then IsLiteExists = True
                             If Version.Version.HasForge Then IsForgeExists = True
+                            If Version.Version.HasNeoForge Then IsNeoExists = True
                         Next
-                        If If(IsLiteExists, 1, 0) + If(IsForgeExists, 1, 0) + If(IsFabricExists, 1, 0) > 1 Then
+                        If If(IsLiteExists, 1, 0) + If(IsForgeExists, 1, 0) + If(IsFabricExists, 1, 0) + If(IsNeoExists, 1, 0) > 1 Then
                             CardName = "可安装 Mod"
                         ElseIf IsForgeExists Then
                             CardName = "Forge 版本"
                         ElseIf IsLiteExists Then
                             CardName = "LiteLoader 版本"
+                        ElseIf IsNeoExists Then
+                            CardName = "NeoForge 版本"
                         Else
                             CardName = "Fabric 版本"
                         End If

+ 1 - 1
Plain Craft Launcher 2/Pages/PageVersion/PageVersionModDisabled.xaml

@@ -22,7 +22,7 @@
             </Grid.ColumnDefinitions>
             <TextBlock Grid.Row="0" Grid.ColumnSpan="4" Margin="0,0,0,9" HorizontalAlignment="Center" Text="该版本不可使用 Mod" FontSize="19" UseLayoutRounding="True" SnapsToDevicePixels="True" Foreground="{DynamicResource ColorBrush3}" />
             <Rectangle Grid.Row="1" Grid.ColumnSpan="4" HorizontalAlignment="Stretch" Height="2" Fill="{DynamicResource ColorBrush3}" />
-            <TextBlock Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5" Text="你需要先安装 Forge 或 Fabric 等 Mod 加载器才能使用 Mod,请在下载页面重新安装这些版本。&#xa;如果你已经安装过了 Mod 加载器,那么你很可能选择了错误的版本,请点击版本选择按钮切换版本,并注意 Forge 在版本选择列表中的图标为铁砧,Fabric 的图标为丝绸。" TextWrapping="Wrap" />
+            <TextBlock Grid.Row="2" Grid.ColumnSpan="4" Margin="10,15,10,5" Text="你需要先安装 Forge、NeoForge 或 Fabric 等 Mod 加载器才能使用 Mod,请在下载页面重新安装这些版本。&#xa;如果你已经安装过了 Mod 加载器,那么你很可能选择了错误的版本,请点击版本选择按钮切换版本,并注意 Forge 在版本选择列表中的图标为铁砧,NeoForge 的图标为狐狸,Fabric 的图标为丝绸。" TextWrapping="Wrap" />
             <local:MyButton Grid.Row="3" Grid.Column="1" Height="35" HorizontalAlignment="Center" x:Name="BtnDownload" MinWidth="140" Text="转到下载页面" Margin="10,10,10,0" Padding="13,0" ColorType="Highlight" />
             <local:MyButton Grid.Row="3" Grid.Column="2" Height="35" HorizontalAlignment="Center" x:Name="BtnVersion" MinWidth="140" Text="版本选择" Margin="10,10,10,0" Padding="13,0" />
         </Grid>

+ 1 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml

@@ -37,6 +37,7 @@
                             <local:MyComboBoxItem Content="红石灯(关)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOff.png" />
                             <local:MyComboBoxItem Content="鸡蛋" Tag="pack://application:,,,/images/Blocks/Egg.png" />
                             <local:MyComboBoxItem Content="布料(Fabric)" Tag="pack://application:,,,/images/Blocks/Fabric.png" />
+                            <local:MyComboBoxItem Content="狐狸(NeoForge)" Tag="pack://application:,,,/images/Blocks/NeoForge.png" />
                         </local:MyComboBox>
                         <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="分类" Margin="0,0,25,0" />
                         <local:MyComboBox Grid.Column="1" Grid.Row="2" x:Name="ComboDisplayType">

+ 8 - 6
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -185,9 +185,6 @@
     <Compile Include="Controls\MyIconTextButton.xaml.vb">
       <DependentUpon>MyIconTextButton.xaml</DependentUpon>
     </Compile>
-
-
-
     <Compile Include="Modules\Minecraft\ModComp.vb" />
     <Compile Include="Modules\Minecraft\ModJava.vb" />
     <Compile Include="Modules\Minecraft\ModMod.vb" />
@@ -232,6 +229,9 @@
     <Compile Include="Modules\Minecraft\MyCompItem.xaml.vb">
       <DependentUpon>MyCompItem.xaml</DependentUpon>
     </Compile>
+    <Compile Include="Pages\PageDownload\PageDownloadNeoForge.xaml.vb">
+      <DependentUpon>PageDownloadNeoForge.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\PageDownload\PageDownloadMod.xaml.vb">
       <DependentUpon>PageDownloadMod.xaml</DependentUpon>
     </Compile>
@@ -469,7 +469,6 @@
       <DependentUpon>FormMain.xaml</DependentUpon>
       <SubType>Code</SubType>
     </Compile>
-
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
@@ -478,6 +477,10 @@
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
     </Page>
+    <Page Include="Pages\PageDownload\PageDownloadNeoForge.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Pages\PageDownload\PageDownloadMod.xaml">
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
@@ -921,8 +924,7 @@
     <Resource Include="Images\Heads\MCBBS.png" />
   </ItemGroup>
   <ItemGroup>
-
+    <Resource Include="Images\Blocks\NeoForge.png" />
   </ItemGroup>
-  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
 </Project>

+ 2 - 0
Plain Craft Launcher 2/Resources/Custom.xaml

@@ -206,6 +206,8 @@
 			<!-- 鸡蛋 -->
 			<Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/Fabric.png"/>
 			<!-- 布料(Fabric 图标) -->
+      <Image Width="30" Margin="5,0,0,0" Source="pack://application:,,,/images/Blocks/NeoForge.png"/>
+      <!-- 狐狸(NeoForge 图标) -->
 		</StackPanel>
         <TextBlock TextWrapping="Wrap" Margin="0,0,0,4"
                     Text="使用时,将上方对应行的 Source 复制到 MyListItem 的 Logo 属性处即可。"/>

Некоторые файлы не были показаны из-за большого количества измененных файлов