Browse Source

Merge pull request #4758 from youzi-3222/feat-98

导出整合包 复活
龙腾猫跃 1 year ago
parent
commit
1a0c7b0520

+ 23 - 7
Plain Craft Launcher 2/Controls/MyListItem.xaml.vb

@@ -337,7 +337,16 @@
             Return _Checked
             Return _Checked
         End Get
         End Get
         Set(value As Boolean)
         Set(value As Boolean)
-            SetChecked(value, False, True)
+            SetChecked(value, False, True, _Half)
+        End Set
+    End Property
+    Private _Half As Boolean = False
+    Public Property Half As Boolean
+        Get
+            Return _Half
+        End Get
+        Set(value As Boolean)
+            SetChecked(_Checked, False, True, value)
         End Set
         End Set
     End Property
     End Property
     ''' <summary>
     ''' <summary>
@@ -346,7 +355,8 @@
     ''' <param name="value">新的 Checked 属性。</param>
     ''' <param name="value">新的 Checked 属性。</param>
     ''' <param name="user">是否由用户引发。</param>
     ''' <param name="user">是否由用户引发。</param>
     ''' <param name="anime">是否执行动画。</param>
     ''' <param name="anime">是否执行动画。</param>
-    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean)
+    ''' <param name="half">是否为部分选中。</param>
+    Public Sub SetChecked(value As Boolean, user As Boolean, anime As Boolean, Optional half As Boolean = False)
         Try
         Try
 
 
             '自定义属性基础
             '自定义属性基础
@@ -364,8 +374,9 @@
                 End If
                 End If
                 _Checked = value
                 _Checked = value
             Else
             Else
-                If value = _Checked Then Exit Sub
+                If value = _Checked AndAlso half = _Half Then Exit Sub
                 _Checked = value
                 _Checked = value
+                _Half = half
                 If IsInitialized Then
                 If IsInitialized Then
                     RaiseEvent Changed(Me, ChangedEventArgs)
                     RaiseEvent Changed(Me, ChangedEventArgs)
                     If ChangedEventArgs.Handled Then
                     If ChangedEventArgs.Handled Then
@@ -429,9 +440,9 @@
                     '由无变有
                     '由无变有
                     If Not IsNothing(RectCheck) Then
                     If Not IsNothing(RectCheck) Then
                         Dim Delta = ActualHeight - RectCheck.ActualHeight - 12
                         Dim Delta = ActualHeight - RectCheck.ActualHeight - 12
-                        Anim.Add(AaHeight(RectCheck, Delta * 0.4, 200,, New AniEaseOutFluent(AniEasePower.Weak)))
-                        Anim.Add(AaHeight(RectCheck, Delta * 0.6, 300,, New AniEaseOutBack(AniEasePower.Weak)))
-                        Anim.Add(AaOpacity(RectCheck, 1 - RectCheck.Opacity, 30))
+                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.2, 0.4), 200,, New AniEaseOutFluent(AniEasePower.Weak)))
+                        Anim.Add(AaHeight(RectCheck, Delta * If(half, 0.3, 0.6), 300,, New AniEaseOutBack(AniEasePower.Weak)))
+                        Anim.Add(AaOpacity(RectCheck, If(half, 0.5, 1) - RectCheck.Opacity, 30))
                         RectCheck.VerticalAlignment = VerticalAlignment.Center
                         RectCheck.VerticalAlignment = VerticalAlignment.Center
                         RectCheck.Margin = New Thickness(-1, 0, 0, 0)
                         RectCheck.Margin = New Thickness(-1, 0, 0, 0)
                     End If
                     End If
@@ -450,12 +461,17 @@
             Else
             Else
                 '不使用动画
                 '不使用动画
                 AniStop("MyListItem Checked " & Uuid)
                 AniStop("MyListItem Checked " & Uuid)
-                If Checked Then
+
+                If Checked OrElse half Then
                     If Not IsNothing(RectCheck) Then
                     If Not IsNothing(RectCheck) Then
                         RectCheck.Height = Double.NaN
                         RectCheck.Height = Double.NaN
                         RectCheck.Margin = New Thickness(-1, 6, 0, 6)
                         RectCheck.Margin = New Thickness(-1, 6, 0, 6)
                         RectCheck.Opacity = 1
                         RectCheck.Opacity = 1
                         RectCheck.VerticalAlignment = VerticalAlignment.Stretch
                         RectCheck.VerticalAlignment = VerticalAlignment.Stretch
+                        If half Then
+                            RectCheck.Height = Height * 0.4
+                            RectCheck.Opacity = 0.5
+                        End If
                     End If
                     End If
                     SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2"))
                     SetResourceReference(ForegroundProperty, If(Height < 40, "ColorBrush3", "ColorBrush2"))
                 Else
                 Else

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

@@ -987,6 +987,7 @@ Public Class FormMain
         VersionSetup = 1
         VersionSetup = 1
         VersionMod = 2
         VersionMod = 2
         VersionModDisabled = 3
         VersionModDisabled = 3
+        VersionExport = 4
     End Enum
     End Enum
     ''' <summary>
     ''' <summary>
     ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
     ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。

+ 4 - 0
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -180,6 +180,10 @@ Public Module ModBase
         ''' </summary>
         ''' </summary>
         Public Const IconButtonServer As String = "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"
         Public Const IconButtonServer As String = "M224 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V224a64 64 0 0 0-64-64H224z m0 384h576v256H224v-256z m192 96v64h320v-64H416z m-128 0v64h64v-64H288zM224 224h576v256H224V224z m192 96v64h320v-64H416z m-128 0v64h64v-64H288z"
         ''' <summary>
         ''' <summary>
+        ''' 图标,向右箭头,1x
+        ''' </summary>
+        Public Const IconButtonRight As String = "M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z"
+        ''' <summary>
         ''' 图标,音符,1x
         ''' 图标,音符,1x
         ''' </summary>
         ''' </summary>
         Public Const IconMusic As String = "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"
         Public Const IconMusic As String = "M348.293565 716.53287V254.797913c0-41.672348 28.004174-78.358261 68.919652-90.37913L815.994435 40.826435c62.775652-18.610087 125.907478 26.579478 125.907478 89.933913v539.158261c8.013913 42.25113-8.94887 89.177043-47.014956 127.109565a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c21.014261 1.024 40.514783 5.164522 57.878261 12.065391V233.338435c0-12.109913-10.551652-20.034783-20.569044-20.034783a24.620522 24.620522 0 0 0-5.787826 0.934957L439.785739 338.18713a19.545043 19.545043 0 0 0-14.825739 19.144348v438.984348H423.846957c11.53113 43.987478-5.164522 94.208-45.412174 134.322087a232.848696 232.848696 0 0 1-170.785392 65.758609c-61.885217-2.938435-111.081739-33.435826-129.113043-80.050087-18.031304-46.614261-2.137043-102.177391 41.672348-145.853218a232.848696 232.848696 0 0 1 170.785391-65.80313c20.791652 1.024 40.069565 5.075478 57.299478 11.842783z"

+ 10 - 6
Plain Craft Launcher 2/Modules/Base/ModValidate.vb

@@ -262,6 +262,7 @@ Public Class ValidateFileName
     Public Property IgnoreCase As Boolean = True
     Public Property IgnoreCase As Boolean = True
     Public Property ParentFolder As String = Nothing
     Public Property ParentFolder As String = Nothing
     Public Property RequireParentFolderExists = True
     Public Property RequireParentFolderExists = True
+    Public Property AllowNull As Boolean = False
     Public Sub New()
     Public Sub New()
     End Sub
     End Sub
     Public Sub New(Name As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
     Public Sub New(Name As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
@@ -271,14 +272,16 @@ Public Class ValidateFileName
     End Sub
     End Sub
     Public Overrides Function Validate(Str As String) As String
     Public Overrides Function Validate(Str As String) As String
         Try
         Try
-            '检查是否为空
-            Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-            If Not LengthCheck = "" Then Return LengthCheck
+            If Not AllowNull Then
+                '检查是否为空
+                Dim NullCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
+                If Not NullCheck = "" Then Return NullCheck
+            End If
             '检查空格
             '检查空格
             If Str.StartsWithF(" ") Then Return "文件名不能以空格开头!"
             If Str.StartsWithF(" ") Then Return "文件名不能以空格开头!"
             If Str.EndsWithF(" ") Then Return "文件名不能以空格结尾!"
             If Str.EndsWithF(" ") Then Return "文件名不能以空格结尾!"
             '检查长度
             '检查长度
-            LengthCheck = New ValidateLength(1, 253).Validate(Str & If(ParentFolder, ""))
+            Dim LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 253).Validate(Str & If(ParentFolder, ""))
             If Not LengthCheck = "" Then Return LengthCheck
             If Not LengthCheck = "" Then Return LengthCheck
             '检查尾部小数点
             '检查尾部小数点
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
@@ -315,6 +318,7 @@ End Class
 Public Class ValidateFolderPath
 Public Class ValidateFolderPath
     Inherits Validate
     Inherits Validate
     Public Property UseMinecraftCharCheck As Boolean = True
     Public Property UseMinecraftCharCheck As Boolean = True
+    Public Property AllowNull As Boolean = False
     Public Sub New()
     Public Sub New()
     End Sub
     End Sub
     Public Sub New(UseMinecraftCharCheck As Boolean)
     Public Sub New(UseMinecraftCharCheck As Boolean)
@@ -326,9 +330,9 @@ Public Class ValidateFolderPath
         If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
         If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
         '检查是否为空
         '检查是否为空
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
-        If Not LengthCheck = "" Then Return LengthCheck
+        If Not AllowNull AndAlso Not LengthCheck = "" Then Return LengthCheck
         '检查长度
         '检查长度
-        LengthCheck = New ValidateLength(1, 254).Validate(Str)
+        LengthCheck = New ValidateLength(If(AllowNull, 0, 1), 254).Validate(Str)
         If Not LengthCheck = "" Then Return LengthCheck
         If Not LengthCheck = "" Then Return LengthCheck
         '检查开头
         '检查开头
         If Str.StartsWithF("\\Mac\") Then GoTo Fin
         If Str.StartsWithF("\\Mac\") Then GoTo Fin

+ 1 - 1
Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb

@@ -1137,7 +1137,7 @@ Finished:
         '刷新边栏
         '刷新边栏
         RunInUi(Sub() FrmVersionMod?.RefreshBars())
         RunInUi(Sub() FrmVersionMod?.RefreshBars())
     End Sub
     End Sub
-    Private Function GetTargetModLoaders() As List(Of CompModLoaderType)
+    Public Function GetTargetModLoaders() As List(Of CompModLoaderType)
         Dim ModLoaders As New List(Of CompModLoaderType)
         Dim ModLoaders As New List(Of CompModLoaderType)
         If PageVersionLeft.Version.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
         If PageVersionLeft.Version.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
         If PageVersionLeft.Version.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)
         If PageVersionLeft.Version.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)

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

@@ -3,6 +3,7 @@ Imports System.Linq.Expressions
 
 
 Public Module ModModpack
 Public Module ModModpack
 
 
+#Region "安装"
     '触发整合包安装的外部接口
     '触发整合包安装的外部接口
     ''' <summary>
     ''' <summary>
     ''' 弹窗要求选择一个整合包文件并进行安装。
     ''' 弹窗要求选择一个整合包文件并进行安装。
@@ -905,6 +906,340 @@ Retry:
         Return Loader
         Return Loader
     End Function
     End Function
 
 
+#End Region
+#End Region
+
+#Region "导出"
+    Private ExpTempDir As String = PathTemp & "PackExport\"
+    ''' <summary>
+    ''' 整合包类型。
+    ''' </summary>
+    Public Enum ModpackType
+        Modrinth
+    End Enum
+    ''' <summary>
+    ''' 导出整合包的选项。
+    ''' </summary>
+    Public Class ExportOptions
+        ''' <summary>
+        ''' 要导出的版本。
+        ''' </summary>
+        Public Version As McVersion
+        ''' <summary>
+        ''' 要保存整合包的位置。
+        ''' </summary>
+        Public Dest As String
+        ''' <summary>
+        ''' 是否包括 PCL 文件。
+        ''' </summary>
+        Public IncludePCL As Boolean
+        ''' <summary>
+        ''' 整合包类型。
+        ''' </summary>
+        Public Type As ModpackType
+        ''' <summary>
+        ''' 要保留到整合包的文件。
+        ''' </summary>
+        Public Additional As String()
+        ''' <summary>
+        ''' 是否需要保留 PCL 全局设置。仅在包括 PCL 时有效。
+        ''' </summary>
+        Public PCLSetupGlobal As Boolean = True
+        ''' <summary>
+        ''' 整合包名称。
+        ''' </summary>
+        Public Name As String = ""
+        ''' <summary>
+        ''' 整合包描述。
+        ''' </summary>
+        Public Desc As String = ""
+        ''' <summary>
+        ''' 版本号。
+        ''' </summary>
+        Public VerID As String = ""
+        Public Sub New(Version As McVersion,
+                       Dest As String,
+                       IncludePCL As Boolean,
+                       Additional As String(),
+                       Optional PCLSetupGlobal As Boolean = True,
+                       Optional Name As String = "",
+                       Optional Desc As String = "",
+                       Optional VerID As String = "")
+            Me.Version = Version
+            Me.Dest = Dest
+            Me.IncludePCL = IncludePCL
+            Me.Additional = Additional
+            Me.PCLSetupGlobal = PCLSetupGlobal
+            Me.Name = Name
+            Me.Desc = Desc
+            Me.VerID = VerID
+        End Sub
+    End Class
+    ''' <summary>
+    ''' 导出整合包,返回是否成功。
+    ''' </summary>
+    Public Function ModpackExport(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        If Task.Input.IncludePCL Then Return ExportCompressed(Task)
+        Return ExportModrinth(Task)
+    End Function
+
+#Region "冗余"
+    Private VersionRedundant As String() = {"^screenshots", "^backups", "^command_history\.txt$", '个人文件
+        "^.*-natives", "^server-resource-packs", "^user.*cache\.json$", "^\.optifine", "^\.fabric", "^\.mixin\.out", '缓存
+        "^.*\.jar$", "^downloads", "^realms_persistence.json$", "^\$\{natives_directory\}", "^essential", '可联网更新
+        "^logs", "^crash-reports", ".*\.log$", "^debug", '日志
+        ".*\.dat_old$", ".*\.old$", '备份
+        "^\$\{quickPlayPath\}", '服务器
+        "^\.replay_cache", "^replay_recordings", "^replay_videos", 'ReplayMod
+        "^irisUpdateInfo\.json$", 'Iris
+        "^modernfix", 'ModernFix 模组
+        "^modtranslations", 'Mod 翻译
+        "^schematics", 'schematics 模组
+        "^journeymap\\data", 'JourneyMap 模组
+        "^mods\\\.connector", '信雅互联
+        "^.*\.BakaCoreInfo$", 'BakaXL 配置
+        "^hmclversion\.cfg$", "^log4j2\.xml$", 'HMCL 配置
+        "^assets$", "^libraries$", "^\$natives$", "^launcher_profiles\.json$", "^versions$" '开启版本隔离时排除的文件
+    }
+    Public Function IsVerRedundant(FilePath As String, PathIndie As String) As Boolean
+        FilePath = FilePath.Replace("/", "\")
+        For Each regex In VersionRedundant
+            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) Then Return True
+        Next
+        Return False
+    End Function
+    Private MustExport As String() = {
+        "mods", "PCL", ".*\.json"
+    }
+    Public Function IsMustExport(FilePath As String, PathIndie As String) As Boolean
+        For Each regex In MustExport
+            If RegexCheck(FilePath.Replace(PathIndie, ""), regex, RegularExpressions.RegexOptions.IgnoreCase) AndAlso Not (FilePath.EndsWithF(".zip") OrElse FilePath.EndsWithF("\")) Then Return True
+        Next
+        Return False
+    End Function
+
+#End Region
+
+#Region "不同类型整合包的导出方法"
+    Private Function ExportModrinth(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        Try
+            Dim Version = Task.Input.Version
+            Dim DestPath = Task.Input.Dest
+            Dim Additional = Task.Input.Additional
+            Dim Name = Task.Input.Name
+            Dim Description = Task.Input.Desc
+            Dim VerID = Task.Input.VerID
+            Log($"[Export] 导出整合包(Modrinth):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个")
+            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
+            Log($"[Export] 缓存文件夹:{tempDir}")
+            DeleteDirectory(tempDir)
+            Directory.CreateDirectory(tempDir)
+
+#Region "0.04-0.40:从 Modrinth 获取 Mod 工程信息,得到 URL"
+            Dim Mods As New Dictionary(Of String, McMod)
+            If Directory.Exists(Version.Path & "mods\") Then
+                For Each m In Directory.EnumerateFiles(Version.Path & "mods\")
+                    If m.EndsWithF(".jar") Then Mods.Add(GetFileNameFromPath(m), New McMod(m))
+                Next
+            End If
+
+            '从 Modrinth 获取信息
+            Dim ModrinthMapping As New Dictionary(Of String, JObject) 'Modrinth 获取到的 Mod
+            Dim CurseForgeMapping As New Dictionary(Of String, JObject) 'CurseForge 获取到的 Mod
+            Dim Failed = Mods.ToArray().ToDictionary(Function(a) a.Key, Function(a) a.Value) '获取失败的 Mod
+            Dim ModrinthHashes = Mods.Select(Function(m) m.Value.ModrinthHash).ToList()
+            If Mods.Count = 0 Then GoTo JumpMod
+            Dim ModrinthRaw = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files", "POST",
+    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
+            Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地 Mod 的对应信息")
+            Task.Progress = 0.35
+
+            For Each Entry In Mods
+                If (Not ModrinthRaw.ContainsKey(Entry.Value.ModrinthHash)) OrElse
+                   (ModrinthRaw(Entry.Value.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.Value.ModrinthHash) Then
+                    Continue For
+                End If
+                Failed.Remove(Entry.Key)
+                ModrinthMapping.Add(Entry.Key, ModrinthRaw(Entry.Value.ModrinthHash)("files")(0))
+            Next
+            Task.Progress = 0.4
+#End Region
+
+#Region "0.40-0.80:从 CurseForge 继续获取"
+            Dim CurseForgeHashes = Mods.Select(Function(m) m.Value.CurseForgeHash).ToList()
+            Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
+                                    $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
+            Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
+            Task.Progress = 0.7
+
+            For Each m In CurseForgeRaw
+                Dim hash As String = ""
+                For Each h In m("file")("hashes") '获取 Modrinth Hash
+                    If h("algo").ToString = 1 Then
+                        hash = h("value")
+                        Exit For
+                    End If
+                Next
+                If String.IsNullOrEmpty(hash) OrElse String.IsNullOrEmpty(m("file")("downloadUrl")) Then Continue For
+
+                m("file")("ModrinthHash") = hash
+                Dim f = Mods.ToArray()
+                For Each file In f
+                    If file.Value.ModrinthHash = hash Then
+                        CurseForgeMapping.Add(file.Key, m("file"))
+                        Failed.Remove(file.Key)
+                        Exit For
+                    End If
+                Next
+            Next
+            Task.Progress = 0.8
+#End Region
+
+JumpMod:
+#Region "0.80-0.90:合并下载链接,复制 Logo,写入 Json 文件"
+            '获取作为检查目标的加载器和版本
+            Dim ModLoaders = GetTargetModLoaders()
+            Dim McVersion = Version.Version.McName
+            Log($"[Export] 目标加载器:{ModLoaders.Join("/")},版本:{McVersion}")
+
+            '合并下载链接
+            Dim BothUrls As New Dictionary(Of String, String()) '两个下载源都有 URL 的 Mod
+            For Each modrinth In ModrinthMapping
+                Dim curseforge As New JObject
+                If CurseForgeMapping.TryGetValue(modrinth.Key, curseforge) Then
+                    BothUrls.Add(modrinth.Key, {modrinth.Value("url"), curseforge("downloadUrl")})
+                End If
+            Next
+            Task.Progress = 0.83
+
+            Dim files As New JArray
+            For Each m In ModrinthMapping
+                files.Add(New JObject From {
+                    {"path", $"mods/{m.Key}"},
+                    {"hashes", m.Value("hashes")},
+                    {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
+                    {"downloads", If(BothUrls.ContainsKey(m.Key), JArray.FromObject(BothUrls(m.Key)), New JArray From {m.Value("url")})},
+                    {"fileSize", m.Value("size")}
+                })
+            Next
+            For Each m In CurseForgeMapping
+                If Not BothUrls.ContainsKey(m.Key) Then
+                    files.Add(New JObject From {
+                        {"path", $"mods/{m.Key}"},
+                        {"hashes", New JObject From {{"sha1", m.Value("ModrinthHash").ToString}}},
+                        {"env", New JObject From {{"client", "required"}, {"server", "required"}}},
+                        {"downloads", New JArray From {m.Value("downloadUrl")}},
+                        {"fileSize", m.Value("fileLength")}
+                    })
+                End If
+            Next
+            Task.Progress = 0.86
+            'If Not String.IsNullOrEmpty(Logo) Then
+            '    Dim LogoObj As New MyBitmap(Logo)
+            '    LogoObj.Save(tempDir & "Logo.png")
+            'End If
+            'Task.Progress = 0.88
+
+            Dim depend As New JObject From {{"minecraft", McVersion}}
+            If Version.Version.HasForge Then depend.Add("forge", Version.Version.ForgeVersion)
+            If Version.Version.HasFabric Then depend.Add("fabric-loader", Version.Version.FabricVersion)
+            If Version.Version.HasNeoForge Then depend.Add("neoforge", Version.Version.NeoForgeVersion)
+
+            Dim json As New JObject From {
+                {"game", "minecraft"},
+                {"formatVersion", 1},
+                {"versionId", VerID},
+                {"name", If(String.IsNullOrEmpty(Name), Version.Name, Name)},
+                {"summary", Description},
+                {"files", files},
+                {"dependencies", depend}
+            }
+            'If Not String.IsNullOrEmpty(Logo) Then json.Add("logo", tempDir & "Logo.png")
+
+            File.WriteAllText(tempDir & "modrinth.index.json", json.ToString)
+            Task.Progress = 0.9
+#End Region
+
+#Region "0.90-0.99:将其余文件保存到 overrides 目录"
+            For Each m In Failed
+                CopyFile(m.Value.Path, tempDir & "overrides\mods\" & m.Value.FileName)
+            Next
+            Task.Progress = 0.92
+
+            '额外文件
+            For Each p In Additional
+                If String.IsNullOrWhiteSpace(p) Then Continue For '传空字串进去会直接把整个版本文件夹拷过去
+                If Not p.StartsWithF(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), IgnoreCase:=True) Then Continue For
+                Dim relative As String = p.Replace(Version.Path, "").Replace(GetPathFromFullPath(GetPathFromFullPath(Version.Path)), "")
+                If File.Exists(p) AndAlso Not IsVerRedundant(p, Version.PathIndie) Then
+                    CopyFile(p, $"{tempDir}overrides\{relative}")
+                End If
+            Next
+            Task.Progress = 0.96
+
+            '如果 resourcepacks shaderpacks 文件夹中有文件夹格式的包,则先将文件夹压缩成 Zip 文件后再打包入对应位置
+            '这可以避免安装时出现路径长度限制而出错
+            If Directory.Exists($"{tempDir}overrides\resourcepacks\") Then
+                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\resourcepacks\")
+                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
+                    DeleteDirectory(d)
+                Next
+            End If
+            If Directory.Exists($"{tempDir}overrides\shaderpacks\") Then
+                For Each d In Directory.EnumerateDirectories($"{tempDir}overrides\shaderpacks\")
+                    If Not File.Exists(d + ".zip") Then ZipFile.CreateFromDirectory(d, d + ".zip")
+                    DeleteDirectory(d)
+                Next
+            End If
+#End Region
+
+            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
+            ZipFile.CreateFromDirectory(tempDir, DestPath)
+            DeleteDirectory(tempDir)
+            Return True
+        Catch ex As Exception
+            Log(ex, "导出整合包失败", LogLevel.Msgbox)
+            Return False
+        End Try
+    End Function
+    Private Function ExportCompressed(Task As LoaderTask(Of ExportOptions, Boolean)) As Boolean
+        Try
+            Dim Version = Task.Input.Version
+            Dim DestPath = Task.Input.Dest
+            Dim Additional = Task.Input.Additional
+            Dim Name = Task.Input.Name
+            Dim VerID = Task.Input.VerID
+            Dim PCLSetupGlobal = Task.Input.PCLSetupGlobal
+            Log($"[Export] 导出整合包(含启动器):{Version.Path} -> {DestPath},额外版本文件 {Additional.Count} 个,全局设置 {If(PCLSetupGlobal, "导出", "不导出")}")
+            Dim tempDir As String = $"{ExpTempDir}{GetUuid()}\"
+            Log($"[Export] 最终压缩包的缓存文件夹:{tempDir}")
+            Directory.CreateDirectory(tempDir)
+
+            Task.Progress = 0.04
+
+            Log("[Export] 开始导出 Modrinth 整合包")
+            Task.Input.Dest = $"{tempDir}modpack.mrpack"
+            ExportModrinth(Task)
+
+            Log($"[Export] 正在复制 PCL 本体")
+            CopyFile(PathWithName, tempDir & GetFileNameFromPath(PathWithName))
+
+            If PCLSetupGlobal Then
+                Log($"[Export] 正在复制 PCL 全局配置")
+                CopyFile(Path & "PCL\Setup.ini", tempDir & "PCL\Setup.ini")
+            End If
+
+            If File.Exists(DestPath) Then File.Delete(DestPath) '选择文件的时候已经确认了要替换
+            ZipFile.CreateFromDirectory(tempDir, DestPath)
+            DeleteDirectory(tempDir)
+            Return True
+        Catch ex As Exception
+            Log(ex, "导出整合包失败", LogLevel.Msgbox)
+            Return False
+        End Try
+    End Function
+#End Region
+
 #End Region
 #End Region
 
 
 End Module
 End Module

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

@@ -434,6 +434,7 @@ EndHint:
     Public FrmVersionMod As PageVersionMod
     Public FrmVersionMod As PageVersionMod
     Public FrmVersionModDisabled As PageVersionModDisabled
     Public FrmVersionModDisabled As PageVersionModDisabled
     Public FrmVersionSetup As PageVersionSetup
     Public FrmVersionSetup As PageVersionSetup
+    Public FrmVersionExport As PageVersionExport
 
 
     '资源信息分页声明
     '资源信息分页声明
     Public FrmDownloadCompDetail As PageDownloadCompDetail
     Public FrmDownloadCompDetail As PageDownloadCompDetail

+ 121 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml

@@ -0,0 +1,121 @@
+<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" 
+    xmlns:System="clr-namespace:System;assembly=mscorlib" 
+    mc:Ignorable="d" x:Class="PageVersionExport"
+    PanScroll="{Binding ElementName=PanBack}">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+        <StackPanel Margin="25,10">
+            <local:MyHint Margin="0,10,0,0" IsWarn="False" Text="当前页面导出的整合包是 Modrinth 格式(.mrpack)。" />
+
+            <local:MyCard Margin="0,15" Title="导出预览">
+                <StackPanel Margin="25,38,15,15">
+                    <Grid Margin="-10,-5,10,0">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="150" />
+                        </Grid.ColumnDefinitions>
+                        <Grid Margin="10,7,10,7" Name="PanDisplayItem" Grid.Column="0" />
+                        <local:MyButton x:Name="BtnExportExport" Grid.Column="1" HorizontalAlignment="Right" 
+                                    Text="导出整合包" Width="150" Height="35" ColorType="Highlight" />
+                    </Grid>
+                    <Grid Margin="0,5,10,5">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="60" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="28" />
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="28" />
+                            <!--<RowDefinition Height="9" />
+                            <RowDefinition Height="28" />-->
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="28" />
+                        </Grid.RowDefinitions>
+                        <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包名称" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="0" x:Name="TbExportName" Grid.ColumnSpan="2" Grid.Column="1" MaxLength="100" ShowValidateResult="True" />
+                        <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="整合包版本" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="2" x:Name="TbExportVersion" HintText="1.0.0" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" MaxLength="100" />
+                        <!--<TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="图标" Margin="0,0,25,0"  />
+                        <local:MyComboBox x:Name="ComboDisplayLogo" Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1">
+                            <local:MyComboBoxItem Content="自动" IsSelected="True" Tag="" />
+                            <local:MyComboBoxItem x:Name="ItemDisplayLogoCustom" Content="自定义..." />
+                            <local:MyComboBoxItem Content="圆石" Tag="pack://application:,,,/images/Blocks/CobbleStone.png" />
+                            <local:MyComboBoxItem Content="命令方块" Tag="pack://application:,,,/images/Blocks/CommandBlock.png" />
+                            <local:MyComboBoxItem Content="金块" Tag="pack://application:,,,/images/Blocks/GoldBlock.png" />
+                            <local:MyComboBoxItem Content="草方块" Tag="pack://application:,,,/images/Blocks/Grass.png" />
+                            <local:MyComboBoxItem Content="草径" Tag="pack://application:,,,/images/Blocks/GrassPath.png" />
+                            <local:MyComboBoxItem Content="铁砧" Tag="pack://application:,,,/images/Blocks/Anvil.png" />
+                            <local:MyComboBoxItem Content="红石块" Tag="pack://application:,,,/images/Blocks/RedstoneBlock.png" />
+                            <local:MyComboBoxItem Content="红石灯(开)" Tag="pack://application:,,,/images/Blocks/RedstoneLampOn.png" />
+                            <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 Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="描述" Margin="0,0,25,0" />
+                        <local:MyTextBox Grid.Row="4" x:Name="TbExportDesc" Grid.ColumnSpan="2" Grid.Column="1" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+
+            <local:MyCard x:Name="CardSwitch" Margin="0,0,0,15" Title="基础选项">
+                <StackPanel Margin="25,38,15,15">
+                    <TextBlock x:Name="TbHint" Margin="0,1,0,1" Text="你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" TextWrapping="Wrap" />
+                    <!-- 注意:以上 TextBlock 的 Text 属性可能会在代码中被改动。 -->
+                    <Grid x:Name="PanCommonFiles">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="18" />
+                            <ColumnDefinition Width="1.7*" />
+                            <ColumnDefinition Width="1.9*" />
+                            <ColumnDefinition Width="1.4*" />
+                            <ColumnDefinition Width="1.0*" />
+                            <ColumnDefinition Width="1.0*" />
+                            <ColumnDefinition Width="1.0*" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="4" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+                        <TextBlock Text="游戏配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="1" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="服务器列表"
+                                          Tag="servers.dat" ToolTip="servers.dat 文件" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="Mod 配置"
+                                          Tag="config" ToolTip="config 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="游戏配置"
+                                          Tag="options.txt" ToolTip="options.txt 文件" />
+                        <TextBlock Text="游戏文件" VerticalAlignment="Center" Grid.Column="0" Grid.Row="2" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="存档"
+                                          Tag="saves" ToolTip="saves 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="资源包"
+                                          Tag="resourcepacks" ToolTip="resourcepacks 文件夹" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="光影包"
+                                          Tag="shaderpacks" ToolTip="shaderpacks 文件夹" />
+                        <TextBlock Text="PCL 配置" VerticalAlignment="Center" Grid.Column="0" Grid.Row="3" />
+                        <local:MyCheckBox x:Name="CheckIncludePCL" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="包含启动器"
+                                          Tag="" ToolTip="将 PCL 程序包含在整合包内" />
+                        <local:MyCheckBox x:Name="CheckIncludeSetup" Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="PCL 全局配置"
+                                          Tag="" ToolTip="将 PCL 全局配置包含在整合包内" />
+                    </Grid>
+                </StackPanel>
+
+            </local:MyCard>
+            <local:MyCard x:Name="CardMore" Title="高级选项" Margin="0,0,0,15" CanSwap="True" IsSwaped="True">
+                <StackPanel x:Name="PanFileList" Margin="25,40,23,15">
+                    <TextBlock Margin="0,1,0,10" Text="你可以在此勾选其他需要包含进整合包的内容。&#xa;请注意:部分必须包含在整合包中的内容(如 Mods 等),以及被排除的冗余文件(如游戏日志等),将不会在列表中展示。" TextWrapping="Wrap" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M889.018182 418.909091H884.363636V316.509091a93.090909 93.090909 0 0 0-99.607272-89.832727h-302.545455l-93.090909-76.334546A46.545455 46.545455 0 0 0 358.865455 139.636364H146.152727A93.090909 93.090909 0 0 0 46.545455 229.469091V837.818182a46.545455 46.545455 0 0 0 46.545454 46.545454 46.545455 46.545455 0 0 0 16.756364-3.258181 109.381818 109.381818 0 0 0 25.134545 3.258181h586.472727a85.178182 85.178182 0 0 0 87.04-63.301818l163.374546-302.545454a46.545455 46.545455 0 0 0 5.585454-21.876364A82.385455 82.385455 0 0 0 889.018182 418.909091z m-744.727273-186.181818h198.283636l93.09091 76.334545a46.545455 46.545455 0 0 0 29.323636 10.705455h319.301818a12.101818 12.101818 0 0 1 6.516364 0V418.909091H302.545455a85.178182 85.178182 0 0 0-87.04 63.301818L139.636364 622.778182V232.727273a19.549091 19.549091 0 0 1 6.516363 0z m578.094546 552.029091a27.461818 27.461818 0 0 0-2.792728 6.516363H154.530909l147.083636-272.290909a27.461818 27.461818 0 0 0 2.792728-6.981818h565.061818z" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M586.9 436h-74.7V62.4H177.1c-20.6 0-37.4 16.7-37.4 37.4v821.9c0 20.6 16.7 37.4 37.4 37.4h672.5c20.6 0 37.4-16.7 37.4-37.4V436H586.9z m0-74.7h297.8c-7.3-57.4-31.2-70.9-58.4-98.1l-66.5-66.5-0.1-0.1 0.1 0.1-0.9-0.9-22.8-22.8-0.9-0.9 0.1 0.1-0.1-0.1-66.5-66.5c-21.8-21.8-38.8-36.5-81.7-41.4l-0.1 297.1z" />
+                    <local:MyListItem Type="CheckBox" Title="游戏存档" Height="35" Logo="M757.792745 435.407215L419.597482 96.904967c-40.010393-40.010393-104.886579-40.010393-144.896972 0-40.010393 40.010393-40.010393 104.988908 0 144.9993L540.344959 507.855701 274.70051 773.807135c-40.010393 40.112721-40.010393 104.988908 0 144.9993 40.010393 40.010393 104.886579 40.010393 144.896972 0l338.092935-338.39992c40.112721-40.010393 40.112721-104.988908 0.102328-144.9993z" />
+                </StackPanel>
+            </local:MyCard>
+
+        </StackPanel>
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 346 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionExport.xaml.vb

@@ -0,0 +1,346 @@
+Public Class PageVersionExport
+    Private CurrentVer As String = ""
+    Private Sub PageVersionExport_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        Reload()
+    End Sub
+
+    Public ItemVersion As MyListItem
+    Public Sub Reload()
+        AniControlEnabled += 1
+
+        If CurrentVer <> PageVersionLeft.Version.Path Then
+            '说明切换到另一个版本了,所有的设置都要重置
+            CurrentPath = ""
+            Selected = New List(Of String)
+            CurrentVer = PageVersionLeft.Version.Path
+
+            TbExportName.Text = ""
+            TbExportVersion.Text = ""
+            'ComboDisplayLogo.SelectedIndex = 0
+            CustomLogoVal = Nothing
+            TbExportDesc.Text = ""
+        End If
+
+        TbExportName.ShowValidateResult = True
+        TbExportName.ValidateRules = New ObjectModel.Collection(Of Validate) From {New ValidateFileName() With {.AllowNull = True}}
+        TbExportName.Validate()
+
+        TbExportName.HintText = PageVersionLeft.Version.Name
+        TbExportDesc.HintText = PageVersionLeft.Version.Info
+        PanDisplayItem.Children.Clear()
+        ItemVersion = PageSelectRight.McVersionListItem(PageVersionLeft.Version.Load())
+        ItemVersion.IsHitTestVisible = False
+        If Not String.IsNullOrEmpty(CustomLogo) Then ItemVersion.Logo = CustomLogo
+        PanDisplayItem.Children.Add(ItemVersion)
+        ItemVersion.Title = If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)
+        ItemVersion.Info = If(String.IsNullOrWhiteSpace(TbExportDesc.Text), PageVersionLeft.Version.Info, TbExportDesc.Text)
+#If BETA Then
+        CheckIncludePCL.IsEnabled = True
+#Else
+        CheckIncludePCL.IsEnabled = False
+        TbHint.Text = "你可以在此勾选需要包含进整合包的文件或文件夹,其他文件或文件夹在高级选项添加。" & vbCrLf & "你当前的 PCL 不是正式版,有二次分发的限制,因此整合包不能包含 PCL 程序。"
+#End If
+        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
+        ReloadFileList(CurrentPath)
+
+        AniControlEnabled -= 1
+    End Sub
+
+#Region "文件列表"
+    ''' <summary>
+    ''' 当前所在的文件夹。注意是绝对路径!以 \ 结尾。
+    ''' </summary>
+    Private CurrentPath As String = ""
+    Private Sub ReloadFileList(Optional NewPath As String = "")
+        If String.IsNullOrWhiteSpace(NewPath) OrElse NewPath = PathMcFolder Then NewPath = PageVersionLeft.Version.Path
+        If Not NewPath.EndsWithF("\") Then NewPath &= "\"
+        Dim tb As TextBlock = PanFileList.Children(0)
+        PanFileList.Children.Clear()
+        PanFileList.Children.Add(tb)
+
+        If NewPath <> PageVersionLeft.Version.Path Then
+            Dim listItem As New MyListItem With {.Title = "返回上一级目录", .Tag = "..", .Height = 35, .Type = MyListItem.CheckType.Clickable}
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+        End If
+
+        Dim fileCount As Integer = 0
+        Dim directories = Directory.EnumerateDirectories(NewPath)
+        Dim files = Directory.EnumerateFiles(NewPath)
+        If (NewPath = PageVersionLeft.Version.Path) AndAlso PageVersionLeft.Version.PathIndie = PathMcFolder Then
+            '未开启版本隔离时 .minecraft 下的文件
+            directories = directories.Concat(Directory.EnumerateDirectories(PathMcFolder))
+            files = files.Concat(Directory.EnumerateFiles(PathMcFolder))
+        End If
+
+        For Each d In directories
+            If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
+            If IsVerRedundant(d, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(d, PageVersionLeft.Version.PathIndie) Then Continue For
+            Dim title As String = ""
+            Dim listItem As New MyListItem
+            If Titles.TryGetValue(GetFolderNameFromPath(d), title) Then 'title 作为引用类型传入
+                listItem.Title = title
+            Else
+                listItem.Title = GetFolderNameFromPath(d)
+            End If
+            listItem.Info = d
+            '图标按钮
+            Dim btnSwap As New MyIconButton
+            btnSwap.Logo = Logo.IconButtonRight
+            btnSwap.LogoScale = 1.0
+            btnSwap.Tag = d & "\"
+            AddHandler btnSwap.Click, AddressOf BtnSwap_Click
+            listItem.Buttons = {btnSwap}
+            listItem.Type = MyListItem.CheckType.CheckBox
+            listItem.Height = 35
+            listItem.Logo = Logo.IconButtonOpen
+            listItem.Tag = d & "\"
+            Select Case IsSelected(d & "\")
+                Case 1
+                    listItem.Half = True
+                Case 2
+                    listItem.Checked = True
+            End Select
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+            fileCount += 1
+        Next
+
+        For Each f In files
+            If IsVerRedundant(f, PageVersionLeft.Version.PathIndie) OrElse IsMustExport(f, PageVersionLeft.Version.PathIndie) Then Continue For
+            Dim title As String = ""
+            Dim listItem As New MyListItem
+            If Titles.TryGetValue(f, title) Then 'title 作为引用类型传入
+                listItem.Title = title
+            Else
+                listItem.Title = GetFileNameFromPath(f)
+            End If
+            listItem.Info = f
+            listItem.Type = MyListItem.CheckType.CheckBox
+            listItem.Height = 35
+            listItem.Tag = f
+            listItem.Checked = (IsSelected(f) = 2)
+            AddHandler listItem.Click, AddressOf ListItem_Click
+            PanFileList.Children.Add(listItem)
+            fileCount += 1
+        Next
+
+        CurrentPath = NewPath
+        CardMore.Title = If(NewPath = PageVersionLeft.Version.Path, "高级选项", $"高级选项 ({NewPath.Replace(PageVersionLeft.Version.Path, "").TrimEnd("\")})")
+    End Sub
+    Private Titles As New Dictionary(Of String, String) From {
+        {"saves", "存档"},
+        {"resourcepacks", "资源包"},
+        {"config", "Mod 配置"},
+        {"shaderpacks", "光影包"},
+        {"options.txt", "游戏配置"},
+        {"servers.dat", "服务器列表"}
+    }
+    Private Sub BtnSwap_Click(sender As MyIconButton, e As Object)
+        Dim actualPath As String = sender.Tag
+        'Dim relative As String = actualPath.Replace(PageVersionLeft.Version.Path, "")
+
+        If Not Directory.Exists(actualPath) Then
+            Hint($"找不到目录:{actualPath}", HintType.Critical)
+            Exit Sub
+        End If
+        ReloadFileList(actualPath)
+    End Sub
+    ''' <summary>
+    ''' 选中的文件列表,绝对路径。
+    ''' </summary>
+    Private Selected As New List(Of String)
+    Private Sub SelectedChange(IsAdd As Boolean, TargetPath As String)
+        If File.Exists(TargetPath) Then
+            If IsAdd Then Selected.Add(TargetPath) Else Selected.Remove(TargetPath)
+            Exit Sub
+        End If
+        If Directory.Exists(TargetPath) Then
+            For Each d In Directory.EnumerateDirectories(TargetPath)
+                SelectedChange(IsAdd, d)
+            Next
+            For Each f In Directory.EnumerateFiles(TargetPath)
+                If IsAdd Then Selected.Add(f) Else Selected.Remove(f)
+            Next
+        End If
+        Selected = Selected.Distinct().ToList()
+    End Sub
+    ''' <summary>
+    ''' 获取 TargetPath 是否选中。
+    ''' </summary>
+    ''' <returns>全部未选中为 0,部分选中为 1,全部选中为 2。</returns>
+    Private Function IsSelected(TargetPath As String, Optional AllowEmpty As Boolean = False) As Integer
+        If File.Exists(TargetPath) Then
+            Return If(Selected.Contains(TargetPath), 2, 0)
+        End If
+        If Directory.Exists(TargetPath) Then
+            Dim di As New DirectoryInfo(TargetPath)
+            If di.GetDirectories.Length + di.GetFiles.Length = 0 Then Return If(AllowEmpty, 2, 0)
+            '如果有一个文件不在列表,则 hasExclude = True
+            Dim hasExclude As Boolean = False
+            '如果有一个文件在列表,则 hasInclude = True
+            Dim hasInclude As Boolean = False
+            For Each d In Directory.EnumerateDirectories(TargetPath)
+                If Directory.EnumerateFileSystemEntries(d).Count = 0 Then Continue For
+                Select Case IsSelected(d, True)
+                    Case 0
+                        hasExclude = True
+                    Case 1
+                        Return 1
+                    Case 2
+                        hasInclude = True
+                End Select
+                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
+            Next
+            For Each f In Directory.EnumerateFiles(TargetPath)
+                If Selected.Contains(f) Then hasInclude = True Else hasExclude = True
+                If hasInclude AndAlso hasExclude Then Return 1 '如果有文件在、有文件不在列表中,可以断定部分选中
+            Next
+            If hasExclude AndAlso Not hasInclude Then Return 0
+            If hasInclude AndAlso Not hasExclude Then Return 2
+        End If
+        Return 0
+    End Function
+    Private Sub ListItem_Click(sender As MyListItem, e As Object)
+        If sender.Tag = ".." Then '回到上一级目录
+            ReloadFileList(GetPathFromFullPath(CurrentPath))
+            Exit Sub
+        End If
+        SelectedChange(Not sender.Checked, '非常难绷的逻辑,很难描述清楚,自己打断点看 Checked 属性值吧……
+                       sender.Tag)
+    End Sub
+
+    'Private ReadOnly Property Saves As List(Of String)
+    '    Get
+    '        If Directory.Exists(PageVersionLeft.Version.Path & "saves\") Then
+    '            Return Directory.EnumerateDirectories(PageVersionLeft.Version.Path & "saves\").ToList
+    '        End If
+    '        Return New List(Of String)
+    '    End Get
+    'End Property
+
+    '复选框与高级选项同步
+    Private Sub PanCommonFiles_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanCommonFiles.MouseLeftButtonUp
+        For Each c In PanCommonFiles.Children
+            If String.IsNullOrEmpty(c.Tag) Then Continue For
+            For Each l In PanFileList.Children
+                If TypeOf l Is MyListItem Then
+                    If If(l.Tag.EndsWith("\"), GetFolderNameFromPath(l.Tag), GetFileNameFromPath(l.Tag)) = c.Tag Then
+                        l.Checked = c.Checked
+                        SelectedChange(c.Checked, PageVersionLeft.Version.Path & c.Tag)
+                    End If
+                End If
+            Next
+        Next
+    End Sub
+    '高级选项与复选框同步
+    Private Sub PanFileList_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles PanFileList.MouseLeftButtonUp
+        For Each c In PanCommonFiles.Children
+            If TypeOf c Is MyCheckBox Then
+                c.Checked = IsSelected(PageVersionLeft.Version.Path & c.Tag) = 2 OrElse IsSelected(PathMcFolder & c.Tag) = 2
+            End If
+        Next
+    End Sub
+#End Region
+
+#Region "整合包信息 & 预览栏"
+    Private Sub TbExportName_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportName.TextChanged
+        If String.IsNullOrEmpty(TbExportName.ValidateResult) Then
+            ItemVersion.Title = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Name, sender.Text)
+        End If
+    End Sub
+
+    Private Sub TbExportDesc_TextChanged(sender As MyTextBox, e As TextChangedEventArgs) Handles TbExportDesc.TextChanged
+        ItemVersion.Info = If(String.IsNullOrWhiteSpace(sender.Text), PageVersionLeft.Version.Info, sender.Text)
+    End Sub
+
+    Private CustomLogoVal As String
+    ''' <summary>
+    ''' 自定义 Logo 的路径。
+    ''' </summary>
+    Public Property CustomLogo() As String
+        Get
+            Return CustomLogoVal
+        End Get
+        Set(value As String)
+            If AniControlEnabled <> 0 Then Exit Property
+            CustomLogoVal = value
+            ItemVersion.Logo = value
+            Reload()
+        End Set
+    End Property
+
+    'Private Sub ComboDisplayLogo_SelectionChanged(sender As MyComboBox, e As SelectionChangedEventArgs) Handles ComboDisplayLogo.SelectionChanged
+    '    If Not AniControlEnabled = 0 Then Exit Sub
+    '    '选择 自定义 时修改图片
+    '    If ComboDisplayLogo.SelectedItem Is ItemDisplayLogoCustom Then
+    '        Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片")
+    '        If String.IsNullOrWhiteSpace(FileName) Then Exit Sub
+    '        CustomLogo = FileName
+    '    Else
+    '        CustomLogo = sender.SelectedItem.Tag
+    '    End If
+    'End Sub
+#End Region
+
+#Region "基本信息"
+    Private Sub CheckIncludePCL_Change(sender As Object, user As Boolean) Handles CheckIncludePCL.Change
+        CheckIncludeSetup.IsEnabled = CheckIncludePCL.IsEnabled
+    End Sub
+#End Region
+
+#Region "导出"
+    'Private Sub ComboExportType_Change(sender As ComboBox, e As SelectionChangedEventArgs) Handles ComboExportType.SelectionChanged
+    '    If AniControlEnabled <> 0 Then Exit Sub
+    '    Type = sender.SelectedIndex
+    '    Select Case Type
+    '        Case ModpackType.Modrinth
+    '            If IncludePCL() Then 'TODO:修改这里的条件
+    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Visible
+    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Collapsed
+    '            Else
+    '                If GridCompressed IsNot Nothing Then GridCompressed.Visibility = Visibility.Collapsed
+    '                If GridModrinth IsNot Nothing Then GridModrinth.Visibility = Visibility.Visible
+    '            End If
+    '    End Select
+    '    CardExport.TriggerForceResize()
+    'End Sub
+    Private Sub BtnExportExport_Click() Handles BtnExportExport.Click
+        Dim savePath As String = SelectAs(
+            "选择导出位置", $"整合包 - {If(String.IsNullOrWhiteSpace(TbExportName.Text), PageVersionLeft.Version.Name, TbExportName.Text)}" & If(CheckIncludePCL.Checked, ".zip", ".mrpack"),
+            If(CheckIncludePCL.Checked, "整合包文件(*.zip)|*.zip", "Modrinth 整合包(*.mrpack)|*.mrpack"))
+        If String.IsNullOrEmpty(savePath) Then Exit Sub
+
+        '获取需要包含的文件
+        Dim contains As List(Of String) = Selected
+        For Each c In PanCommonFiles.Children
+            If TypeOf c IsNot MyCheckBox Then Continue For
+            If String.IsNullOrEmpty(c.Tag) Then Continue For
+            If c.Checked Then contains.Add(PageVersionLeft.Version.Path & c.Tag)
+        Next
+        '重复任务检查
+        For Each OngoingLoader In LoaderTaskbar
+            If OngoingLoader.Name <> $"导出整合包 {PageVersionLeft.Version.Name}" Then Continue For
+            Hint("正在处理中,请稍候!", HintType.Critical)
+            Exit Sub
+        Next
+        '启动!
+        Dim Loader As New LoaderTask(Of ExportOptions, Boolean)($"导出整合包 {PageVersionLeft.Version.Name}",
+        Sub(Task As LoaderTask(Of ExportOptions, Boolean))
+            Task.Output = ModpackExport(Task)
+            Task.Progress = 1.0
+            If Task.Output Then
+                Hint($"{Task.Name} 成功!", HintType.Finish)
+                OpenExplorer($"/select,""{Task.Input.Dest}""")
+            End If
+        End Sub)
+        Dim Combo = New LoaderCombo(Of ExportOptions)($"导出整合包 {PageVersionLeft.Version.Name}", {Loader})
+        Combo.Start(New ExportOptions(PageVersionLeft.Version, savePath, CheckIncludePCL.Checked, contains.Distinct.ToArray,
+                                       PCLSetupGlobal:=CheckIncludeSetup.Checked, Name:=TbExportName.Text, Desc:=TbExportDesc.Text,
+                                       VerID:=TbExportVersion.Text))
+        LoaderTaskbarAdd(Combo)
+    End Sub
+
+#End Region
+
+End Class

+ 3 - 0
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml

@@ -23,5 +23,8 @@
         </local:MyListItem>
         </local:MyListItem>
         <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
         <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
+        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出整合包"
+                          Logo="M511.488 995.328a128.654222 128.654222 0 0 1-57.116444-13.112889L70.769778 791.808a126.833778 126.833778 0 0 1-70.769778-113.777778V311.608889a126.179556 126.179556 0 0 1 15.36-60.103111V248.604444c1.479111-2.901333 3.356444-5.603556 5.518222-8.021333a127.630222 127.630222 0 0 1 49.891556-42.325333L454.371556 13.368889a128.739556 128.739556 0 0 1 112.981333 0l383.601778 190.407111a126.862222 126.862222 0 0 1 72.049777 113.379556v360.874666a126.805333 126.805333 0 0 1-70.769777 115.939556L568.604444 984.32c-17.92 7.964444-37.461333 11.747556-57.116444 11.008z m42.638222-470.897778v370.204445l360.192-178.545778c14.449778-7.253333 23.552-21.987556 23.438222-38.087111v-335.928889L554.097778 524.430222zM85.248 330.666667v347.335111a42.268444 42.268444 0 0 0 23.438222 38.087111l360.192 178.545778V523.576889L85.248 330.666667zM135.537778 260.835556l375.950222 189.952 137.671111-65.564445L286.435556 188.074667 135.537778 260.864z m245.105778-118.471112l363.576888 197.973334 150.897778-71.480889-365.283555-180.224a42.922667 42.922667 0 0 0-37.518223 0l-111.672888 53.731555z" >
+        </local:MyListItem>
     </StackPanel>
     </StackPanel>
 </local:MyPageLeft>
 </local:MyPageLeft>

+ 4 - 1
Plain Craft Launcher 2/Pages/PageVersion/PageVersionLeft.xaml.vb

@@ -25,7 +25,7 @@
     ''' <summary>
     ''' <summary>
     ''' 勾选事件改变页面。
     ''' 勾选事件改变页面。
     ''' </summary>
     ''' </summary>
-    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check
+    Private Sub PageCheck(sender As MyListItem, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check, ItemExport.Check
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
@@ -46,6 +46,9 @@
             Case FormMain.PageSubType.VersionSetup
             Case FormMain.PageSubType.VersionSetup
                 If IsNothing(FrmVersionSetup) Then FrmVersionSetup = New PageVersionSetup
                 If IsNothing(FrmVersionSetup) Then FrmVersionSetup = New PageVersionSetup
                 Return FrmVersionSetup
                 Return FrmVersionSetup
+            Case FormMain.PageSubType.VersionExport
+                If FrmVersionExport Is Nothing Then FrmVersionExport = New PageVersionExport
+                Return FrmVersionExport
             Case Else
             Case Else
                 Throw New Exception("未知的版本设置子页面种类:" & ID)
                 Throw New Exception("未知的版本设置子页面种类:" & ID)
         End Select
         End Select

+ 9 - 2
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -290,6 +290,9 @@
     <Compile Include="Pages\PageLink\PageLinkLeft.xaml.vb">
     <Compile Include="Pages\PageLink\PageLinkLeft.xaml.vb">
       <DependentUpon>PageLinkLeft.xaml</DependentUpon>
       <DependentUpon>PageLinkLeft.xaml</DependentUpon>
     </Compile>
     </Compile>
+    <Compile Include="Pages\PageVersion\PageVersionExport.xaml.vb">
+      <DependentUpon>PageVersionExport.xaml</DependentUpon>
+    </Compile>
     <Compile Include="Pages\PageVersion\PageVersionLeft.xaml.vb">
     <Compile Include="Pages\PageVersion\PageVersionLeft.xaml.vb">
       <DependentUpon>PageVersionLeft.xaml</DependentUpon>
       <DependentUpon>PageVersionLeft.xaml</DependentUpon>
     </Compile>
     </Compile>
@@ -476,10 +479,10 @@
       <DependentUpon>MyRadioButton.xaml</DependentUpon>
       <DependentUpon>MyRadioButton.xaml</DependentUpon>
     </Compile>
     </Compile>
     <Compile Include="FormMain.xaml.vb">
     <Compile Include="FormMain.xaml.vb">
+
       <DependentUpon>FormMain.xaml</DependentUpon>
       <DependentUpon>FormMain.xaml</DependentUpon>
       <SubType>Code</SubType>
       <SubType>Code</SubType>
     </Compile>
     </Compile>
-
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
@@ -561,6 +564,10 @@
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
     </Page>
     </Page>
+    <Page Include="Pages\PageVersion\PageVersionExport.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
     <Page Include="Resources\Language\en_US.xaml">
     <Page Include="Resources\Language\en_US.xaml">
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
@@ -910,11 +917,11 @@
     <Resource Include="Images\Icons\Disabled.png" />
     <Resource Include="Images\Icons\Disabled.png" />
     <Resource Include="Images\Icons\Unavailable.png" />
     <Resource Include="Images\Icons\Unavailable.png" />
   </ItemGroup>
   </ItemGroup>
+
   <ItemGroup>
   <ItemGroup>
     <Resource Include="Images\Heads\MCBBS.png" />
     <Resource Include="Images\Heads\MCBBS.png" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
-
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <Resource Include="Images\Blocks\NeoForge.png" />
     <Resource Include="Images\Blocks\NeoForge.png" />