PageInstanceExport.xaml.vb 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. Public Class ExportOption
  2. Public Property Title As String
  3. Public Property Description As String
  4. Public Property Rules As String
  5. ''' <summary>
  6. ''' 如果 Rules 为空,则根据 ShowRules 的内容判断是否应该显示这个复选框。
  7. ''' 如果 ShowRules 也为空,则始终显示。
  8. ''' </summary>
  9. Public Property ShowRules As String
  10. Public Property DefaultChecked As Boolean
  11. Public Property RequireModLoader As Boolean = False
  12. Public Property RequireOptiFine As Boolean = False
  13. Public Property RequireModLoaderOrOptiFine As Boolean = False
  14. End Class
  15. Public Class PageInstanceExport
  16. Implements IRefreshable
  17. Private CurrentInstance As String = ""
  18. Private Sub PageInstanceExport_Initialized(sender As Object, e As EventArgs) Handles Me.Initialized
  19. AddHandler PageInstanceLeft.CurrentJavaWorker.Stopped, Sub() RunInUi(AddressOf RefreshJavaInfo)
  20. End Sub
  21. Private Sub PageInstanceExport_Loaded() Handles Me.Loaded
  22. AniControlEnabled += 1
  23. If Not PathUtils.IsSame(CurrentInstance, PageInstanceLeft.Instance.PathVersion) Then RefreshAll() '切换到了另一个版本,重置页面
  24. CustomEventService.SetEventData(BtnAdvancedHelp,
  25. If(BuildType = BuildTypes.Release, "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
  26. AniControlEnabled -= 1
  27. End Sub
  28. Public Sub RefreshAll() Implements IRefreshable.Refresh
  29. Logger.Info("刷新导出页面")
  30. HintOptiFine.Visibility = If(PageInstanceLeft.Instance.Version.HasOptiFine AndAlso Not PageInstanceLeft.Instance.Version.OptiFineAsMod, Visibility.Visible, Visibility.Collapsed)
  31. CurrentInstance = PageInstanceLeft.Instance.PathVersion
  32. TextExportName.Text = ""
  33. TextExportName.HintText = PageInstanceLeft.Instance.Name
  34. TextExportVersion.Text = ""
  35. TextExportVersion.HintText = "1.0.0"
  36. CheckAdvancedInclude.Checked = False
  37. CheckAdvancedModrinth.Checked = False
  38. GetExportOption(CheckOptionsBasic).Description = PageInstanceLeft.Instance.VersionDisplayName()
  39. ResetConfigOverrides()
  40. ReloadAllSubOptions()
  41. RefreshAllOptionsUI()
  42. RefreshJavaInfo()
  43. PanBack.ScrollToHome()
  44. End Sub
  45. #Region "子选项"
  46. Private SubOptionBlackList As String() = {"Quark Programmer Art.zip", "+ EuphoriaPatches_", "PCL2 Skin.zip"}
  47. ''' <summary>
  48. ''' 动态生成子文件夹下的选项,例如资源包、存档等。
  49. ''' </summary>
  50. Private Sub ReloadAllSubOptions()
  51. ReloadSubOptions(PanOptionsResourcePacks, True, True, "resourcepacks", "texturepacks")
  52. ReloadSubOptions(PanOptionsSaves, False, True, "saves")
  53. ReloadSubOptions(PanOptionsShaderPacks, True, True, "shaderpacks")
  54. End Sub
  55. Private Sub ReloadSubOptions(Panel As StackPanel, AcceptCompressedFile As Boolean, AcceptFolder As Boolean, ParamArray Folders As String())
  56. Panel.Children.Clear()
  57. For Each Folder In Folders
  58. Dim TargetFolder = DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie & Folder)
  59. If Not TargetFolder.Exists() Then Continue For
  60. '查找文件夹下的对应项
  61. If AcceptCompressedFile Then
  62. For Each File In TargetFolder.EnumerateFiles("*.zip").Concat(TargetFolder.EnumerateFiles("*.rar"))
  63. If SubOptionBlackList.Any(Function(b) File.Name.Contains(b)) Then Continue For
  64. Panel.Children.Add(New MyCheckBox With {. _
  65. Tag = New ExportOption With {.Title = File.Name, .DefaultChecked = True, .Rules = StringUtils.LikePatternEscape($"{Folder}/{File.Name}")}})
  66. Next
  67. End If
  68. If AcceptFolder Then
  69. For Each SubFolder In TargetFolder.EnumerateDirectories().OrderByDescending(Function(f) f.LastWriteTime)
  70. If SubOptionBlackList.Any(Function(b) SubFolder.Name.Contains(b)) Then Continue For
  71. If Not SubFolder.EnumerateFileSystemInfos().Any() Then Continue For
  72. Dim NewCheckBox As New MyCheckBox With {. _
  73. Tag = New ExportOption With {.Title = SubFolder.Name, .DefaultChecked = True, .Rules = StringUtils.LikePatternEscape($"{Folder}/{SubFolder.Name}/")}}
  74. If AcceptCompressedFile Then GetExportOption(NewCheckBox).Description = "文件夹"
  75. If Panel Is PanOptionsSaves Then GetExportOption(NewCheckBox).Description = SubFolder.LastWriteTime.ToString("yyyy'/'MM'/'dd HH':'mm")
  76. Panel.Children.Add(NewCheckBox)
  77. Next
  78. End If
  79. Next
  80. End Sub
  81. #End Region
  82. #Region "选项"
  83. ''' <summary>
  84. ''' 重新确认是否应该显示每个选项,并将 ExportOption 同步到 UI。
  85. ''' </summary>
  86. Private Sub RefreshAllOptionsUI()
  87. '预先归纳所有至多二级的文件/文件夹
  88. Dim AllEntries As New List(Of String)
  89. Dim IsValidDirectory = '检查文件夹不为空
  90. Function(Folder As DirectoryInfo) As Boolean
  91. Try
  92. Return Folder.Exists AndAlso
  93. Folder.EnumerateFileSystemInfos().Any(Function(i) Not SubOptionBlackList.Any(Function(b) i.Name.Contains(b)))
  94. Catch '一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接)
  95. Return False
  96. End Try
  97. End Function
  98. Dim PathInfo = DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie)
  99. AllEntries.AddRange(PathInfo.EnumerateFiles().Select(Function(f) f.Name))
  100. For Each SubFolder In PathInfo.EnumerateDirectories().Where(IsValidDirectory)
  101. AllEntries.Add($"{SubFolder.Name}\")
  102. AllEntries.AddRange(SubFolder.EnumerateFiles().Select(Function(f) $"{SubFolder.Name}\{f.Name}"))
  103. AllEntries.AddRange(SubFolder.EnumerateDirectories().Where(IsValidDirectory).Select(Function(d) $"{SubFolder.Name}\{d.Name}\"))
  104. Next
  105. Logger.Info($"共发现 {AllEntries.Count} 个可行的二级文件/文件夹")
  106. '确认选项是否应该被显示
  107. Dim IsVisible =
  108. Function(TargetOption As ExportOption) As Boolean
  109. '检查需要 OptiFine(例如光影)或 Mod 加载器(例如 mods/)
  110. If TargetOption.RequireOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine Then Return False
  111. If TargetOption.RequireModLoader AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
  112. If TargetOption.RequireModLoaderOrOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
  113. '粗略检查是否可能有符合规则的文件/文件夹
  114. Return StandardizeLines(If(TargetOption.Rules, TargetOption.ShowRules).Split("|"c), True).Any(
  115. Function(Rule As String)
  116. If Rule.StartsWithF("!") Then Return False '只看正向规则
  117. '检查前两级
  118. Try
  119. If AllEntries.Any(Function(Entry) Entry.Lower Like Rule.Lower) Then Return True
  120. Catch ex As Exception
  121. Logger.Error(ex, $"错误的规则:{Rule}", LogBehavior.Toast)
  122. Return False
  123. End Try
  124. '粗略检查所有级
  125. Rule = Rule.Trim("*?".ToCharArray)
  126. If Rule.Split("\"c, True).Count >= 3 Then
  127. If Rule.EndsWithF("\") Then
  128. Return IsValidDirectory(DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie & Rule)) '文件夹有效
  129. Else
  130. Return FileUtils.Exists(PageInstanceLeft.Instance.PathIndie & Rule) '文件有效
  131. End If
  132. Else
  133. Return False
  134. End If
  135. End Function)
  136. End Function
  137. '逐个检查选项
  138. For Each CheckBox In GetAllOptions(True)
  139. Dim TargetOption = GetExportOption(CheckBox)
  140. '可见性、默认勾选
  141. If String.IsNullOrEmpty(TargetOption.Rules) AndAlso String.IsNullOrEmpty(TargetOption.ShowRules) Then
  142. CheckBox.Visibility = Visibility.Visible
  143. CheckBox.Checked = TargetOption.DefaultChecked
  144. Else
  145. Dim Pass As Boolean = IsVisible(TargetOption)
  146. CheckBox.Visibility = If(Pass, Visibility.Visible, Visibility.Collapsed)
  147. CheckBox.Checked = TargetOption.DefaultChecked AndAlso Pass
  148. End If
  149. '名称与简介
  150. CheckBox.Inlines.Clear()
  151. If CheckBox.Visibility <> Visibility.Visible Then Continue For
  152. CheckBox.Inlines.Add(New Run(TargetOption.Title))
  153. If Not String.IsNullOrEmpty(TargetOption.Description) Then
  154. CheckBox.Inlines.Add(New Run(" " & TargetOption.Description) With {.Foreground = ColorGray5})
  155. End If
  156. Next
  157. End Sub
  158. ''' <summary>
  159. ''' 对文本行进行标准化处理,以便使用 Like 进行匹配。
  160. ''' </summary>
  161. Private Iterator Function StandardizeLines(Raw As IEnumerable(Of String), AddSuffixStarToFolderPath As Boolean) As IEnumerable(Of String)
  162. For Each IgnoreLine In Raw
  163. IgnoreLine = IgnoreLine.Trim
  164. If IgnoreLine = "" OrElse IgnoreLine.StartsWithF("#") OrElse IgnoreLine.StartsWithF("=") Then Continue For
  165. IgnoreLine = IgnoreLine.Replace("/", "\")
  166. Yield IgnoreLine & If(IgnoreLine.EndsWithF("\") AndAlso AddSuffixStarToFolderPath, "*", "")
  167. Next
  168. End Function
  169. ''' <summary>
  170. ''' 获取所有可作为选项的 CheckBox。
  171. ''' </summary>
  172. Private Iterator Function GetAllOptions(IncludeHidden As Boolean) As IEnumerable(Of MyCheckBox)
  173. For Each Element In PanOptions.Children
  174. If Not IncludeHidden AndAlso Element.Visibility <> Visibility.Visible Then Continue For
  175. If TypeOf Element Is MyCheckBox Then
  176. Yield Element
  177. ElseIf TypeOf Element Is StackPanel Then
  178. For Each SubElement In DirectCast(Element, StackPanel).Children
  179. If Not IncludeHidden AndAlso SubElement.Visibility <> Visibility.Visible Then Continue For
  180. If TypeOf SubElement Is MyCheckBox Then Yield SubElement
  181. Next
  182. End If
  183. Next
  184. End Function
  185. ''' <summary>
  186. ''' 获取该 CheckBox 对应的 ExportOption。
  187. ''' </summary>
  188. Private Function GetExportOption(CheckBox As MyCheckBox) As ExportOption
  189. Return CType(CheckBox.Tag, ExportOption)
  190. End Function
  191. #End Region
  192. #Region "配置文件"
  193. Private Const Sperator As String = "=============================================================="
  194. ' ================ 导出内容段 ================
  195. ''' <summary>
  196. ''' 从配置文件中读取的规则。
  197. ''' 如果不为 Nothing,则会覆写当前勾选的规则并禁用对应 UI。
  198. ''' </summary>
  199. Private Property RulesOverrides As List(Of String)
  200. Get
  201. Return _RulesOverrides
  202. End Get
  203. Set(value As List(Of String))
  204. _RulesOverrides = value
  205. If value Is Nothing Then
  206. BtnOverrideCancel.Visibility = Visibility.Collapsed
  207. PanOptions.Visibility = Visibility.Visible
  208. CardOptions.Inlines.Clear()
  209. CardOptions.Inlines.Add(New Run("导出内容列表") With {.FontWeight = FontWeights.Bold})
  210. Else
  211. BtnOverrideCancel.Visibility = Visibility.Visible
  212. PanOptions.Visibility = Visibility.Collapsed
  213. CardOptions.Inlines.Clear()
  214. CardOptions.Inlines.Add(New Run("导出内容列表:    ") With {.FontWeight = FontWeights.Bold})
  215. CardOptions.Inlines.Add(New Run("从配置文件中读取") With {.FontWeight = FontWeights.Normal})
  216. End If
  217. End Set
  218. End Property
  219. Private _RulesOverrides As List(Of String) = Nothing
  220. ''' <summary>
  221. ''' 获取当前实际生效的所有规则。
  222. ''' </summary>
  223. Private Iterator Function GetAllRules() As IEnumerable(Of String)
  224. If RulesOverrides IsNot Nothing Then
  225. '返回覆盖的列表
  226. For Each Rule In RulesOverrides
  227. Yield Rule
  228. Next
  229. Else
  230. '从当前勾选的所有选项中获取所有规则行
  231. Yield ""
  232. Yield "# 修改下方的规则以控制需要导出的内容。"
  233. Yield "# 以 ! 开头以反选。可以使用 *、?、[] 通配符。靠后的行覆盖靠前的。"
  234. Yield ""
  235. For Each CheckBox In GetAllOptions(False)
  236. If Not CheckBox.Checked Then Continue For
  237. Dim TargetOption = GetExportOption(CheckBox)
  238. Dim Rules = If(TargetOption.Rules?.Split("|"c).ToList(), New List(Of String))
  239. '动态 Rules
  240. If CheckBox Is CheckOptionsShaderPackSettings Then '光影包设置:只导出勾选的光影包的设置
  241. For Each ShaderPackCheckBox In PanOptionsShaderPacks.Children.OfType(Of MyCheckBox)()
  242. If Not ShaderPackCheckBox.Checked Then Continue For
  243. Dim ShaderPackOption = GetExportOption(ShaderPackCheckBox)
  244. Dim SettingsFile = $"shaderpacks/{ShaderPackOption.Title}.txt"
  245. If Not FileUtils.Exists(Path.Combine(PageInstanceLeft.Instance.PathIndie, SettingsFile)) Then Continue For
  246. Rules.Add(StringUtils.LikePatternEscape(SettingsFile))
  247. Next
  248. End If
  249. If Not Rules.Any() Then Continue For
  250. Yield $"# {TargetOption.Title}"
  251. For Each Rule In Rules
  252. Yield Rule
  253. Next
  254. Yield ""
  255. Next
  256. Yield "# 排除的文件"
  257. Yield "!*.log"
  258. Yield "!*.dat_old"
  259. Yield "!*.BakaCoreInfo"
  260. Yield "!hmclversion.cfg"
  261. Yield "!log4j2.xml"
  262. Yield ""
  263. End If
  264. End Function
  265. ' ================ 追加内容段 ================
  266. Private ExtraFiles As List(Of String) = Nothing
  267. ''' <summary>
  268. ''' 获取当前实际生效的追加内容。
  269. ''' </summary>
  270. Private Iterator Function GetExtraFileLines() As IEnumerable(Of String)
  271. If ExtraFiles IsNot Nothing Then
  272. '返回覆盖的列表
  273. For Each File In ExtraFiles
  274. Yield File
  275. Next
  276. Else
  277. '从当前勾选的所有选项中获取所有规则行
  278. Yield ""
  279. Yield "# 如果想将额外的文件自动放到压缩包根目录中,可以将它们的路径写在下方。"
  280. Yield "# 必须是完整路径。每行中,若以 \ 结尾则代表是文件夹,不以 \ 结尾则代表是文件。"
  281. Yield ""
  282. End If
  283. End Function
  284. ' ================ 重置 ================
  285. ''' <summary>
  286. ''' 重置配置文件所带来的影响。
  287. ''' </summary>
  288. Private Sub ResetConfigOverrides()
  289. RulesOverrides = Nothing
  290. ConfigPackPath = Nothing
  291. ExtraFiles = Nothing
  292. PanBack.ScrollToHome()
  293. End Sub
  294. Private Sub CardOptions_MouseLeftButtonDown() Handles CardOptions.MouseLeftButtonDown
  295. If RulesOverrides Is Nothing Then Return
  296. ResetConfigOverrides()
  297. End Sub
  298. ' ================ 保存 / 读取 ================
  299. '保存配置文件
  300. Private Sub ExportConfig() Handles BtnAdvancedExport.Click
  301. Try
  302. Dim ConfigPath As String = Dialogs.SaveFile("选择文件位置", "export_config.txt", Settings.Get(Of String)("CacheExportConfig"), {("txt", "整合包导出配置")})
  303. If ConfigPath Is Nothing Then Return
  304. Settings.Set("CacheExportConfig", ConfigPath)
  305. Dim ConfigLines As New List(Of String)
  306. 'ini 段
  307. ConfigLines.Add("Name:" & TextExportName.Text)
  308. ConfigLines.Add("Version:" & TextExportVersion.Text)
  309. ConfigLines.Add("")
  310. ConfigLines.Add($"# 当版本设置中选择了 {vbLQ}使用版本文件夹中的 Java{vbRQ} 时,是否打包版本文件夹中的 Java。")
  311. ConfigLines.Add("IncludeJava:" & CheckOptionsJava.Checked)
  312. ConfigLines.Add("")
  313. ConfigLines.Add("# 是否打包正式版 PCL,以便没有启动器的玩家安装整合包。")
  314. ConfigLines.Add("IncludeLauncher:" & CheckOptionsPcl.Checked)
  315. ConfigLines.Add("")
  316. ConfigLines.Add("# 是否打包 PCL 个性化内容,例如功能隐藏设置、主页、背景音乐和图片等。")
  317. ConfigLines.Add("IncludeLauncherCustom:" & CheckOptionsPclCustom.Checked)
  318. ConfigLines.Add("")
  319. ConfigLines.Add("# 是否将 Mod、资源包、光影包的文件直接放入整合包中,这样在导入时就无需联网下载它们。")
  320. ConfigLines.Add("# 建议仅在无法稳定连接 CurseForge 或 Modrinth 时才考虑启用。")
  321. ConfigLines.Add("# 二次分发可能违反使用协议,请尽量不要公开发布包含资源文件的整合包!")
  322. ConfigLines.Add("DontCheckHostedAssets:" & CheckAdvancedInclude.Checked)
  323. ConfigLines.Add("")
  324. ConfigLines.Add("# 若资源未在 Modrinth 上托管,则将其原始文件直接放入整合包中。")
  325. ConfigLines.Add("# 如果你想将整合包上传到 Modrinth,这能帮助你分辨哪些文件未在 Modrinth 上托管。")
  326. ConfigLines.Add("# 此选项与 DontCheckHostedAssets 冲突。")
  327. ConfigLines.Add("ModrinthUploadMode:" & CheckAdvancedModrinth.Checked)
  328. ConfigLines.Add("")
  329. ConfigLines.Add("# 导出的文件的存放位置。")
  330. ConfigLines.Add("# 若设置了此项,在导出时会直接将文件放到此路径,不会弹窗要求选择。")
  331. ConfigLines.Add("# 若 IncludeLauncher 为 True,应以 .zip 结尾;若为 False,应以 .mrpack 结尾。")
  332. ConfigLines.Add("PackPath:" & If(ConfigPackPath, ""))
  333. ConfigLines.Add("")
  334. '导出内容段
  335. ConfigLines.Add(Sperator)
  336. ConfigLines.AddRange(GetAllRules())
  337. '追加内容段
  338. ConfigLines.Add(Sperator)
  339. ConfigLines.AddRange(GetExtraFileLines())
  340. '结束
  341. FileUtils.Write(ConfigPath, ConfigLines.Join(vbCrLf))
  342. Hint("已保存配置文件:" & ConfigPath, HintType.Green)
  343. OpenExplorer(ConfigPath)
  344. Catch ex As Exception
  345. Logger.Error(ex, "保存配置失败", LogBehavior.Alert)
  346. End Try
  347. End Sub
  348. '读取配置文件
  349. Private Sub ImportConfig() Handles BtnAdvancedImport.Click
  350. Try
  351. Dim ConfigPath As String = Dialogs.SelectFile("选择配置文件", False, Settings.Get(Of String)("CacheExportConfig"), {({"txt"}, "整合包导出配置")}).FirstOrDefault()
  352. If String.IsNullOrEmpty(ConfigPath) Then Return
  353. Settings.Set("CacheExportConfig", ConfigPath)
  354. Dim Segments As String() = FileUtils.ReadAsString(ConfigPath).Split(Sperator)
  355. 'ini 段
  356. Dim Ini As New Dictionary(Of String, String)
  357. For Each Line In Segments(0).SplitLines(True)
  358. Line = Line.Trim
  359. If Line = "" OrElse Line.StartsWithF("#") OrElse Line.StartsWithF("=") Then Continue For
  360. Dim Index As Integer = Line.IndexOfF(":")
  361. If Index > 0 Then Ini(Line.Substring(0, Index)) = Line.Substring(Index + 1)
  362. Next
  363. TextExportName.Text = Ini.GetOrDefault("Name", "")
  364. TextExportVersion.Text = Ini.GetOrDefault("Version", "")
  365. CheckOptionsJava.Checked = Ini.GetOrDefault("IncludeJava", True)
  366. CheckOptionsPcl.Checked = Ini.GetOrDefault("IncludeLauncher", True)
  367. CheckOptionsPclCustom.Checked = Ini.GetOrDefault("IncludeLauncherCustom", True)
  368. CheckAdvancedInclude.Checked = Ini.GetOrDefault("DontCheckHostedAssets", False)
  369. If Not CheckAdvancedInclude.Checked Then CheckAdvancedModrinth.Checked = Ini.GetOrDefault("ModrinthUploadMode", False) '#7979,加个特判
  370. ConfigPackPath = Ini.GetOrDefault("PackPath", Nothing)
  371. '导出内容段
  372. RulesOverrides = Segments(1).SplitLines(True).ToList
  373. '追加内容段
  374. If Segments.Length > 2 Then
  375. ExtraFiles = Segments(2).SplitLines(True).ToList
  376. Else
  377. ExtraFiles = Nothing
  378. End If
  379. '结束
  380. Hint("已读取配置文件:" & ConfigPath, HintType.Green)
  381. Catch ex As Exception
  382. Logger.Error(ex, "读取配置失败", LogBehavior.Alert)
  383. End Try
  384. End Sub
  385. #End Region
  386. #Region "导出"
  387. ''' <summary>
  388. ''' 配置文件中指定的导出位置。
  389. ''' </summary>
  390. Private ConfigPackPath As String = Nothing
  391. ''' <summary>
  392. ''' 开始导出。
  393. ''' </summary>
  394. Private Sub StartExport() Handles BtnExport.Click
  395. Dim PackName As String = If(String.IsNullOrEmpty(TextExportName.Text), TextExportName.HintText, TextExportName.Text)
  396. Dim PackVersion As String = If(String.IsNullOrEmpty(TextExportVersion.Text), "1.0.0", TextExportVersion.Text)
  397. '重复任务检查
  398. Dim LoaderName As String = $"导出整合包:" & PackName
  399. For Each OngoingLoader In LoaderTaskbar
  400. If OngoingLoader.Name <> LoaderName Then Continue For
  401. FrmMain.PageChange(FormMain.PageType.TaskManager)
  402. Return
  403. Next
  404. '确认导出位置
  405. Dim PackPath As String = Nothing
  406. If Not String.IsNullOrWhiteSpace(ConfigPackPath) AndAlso
  407. (Not ConfigPackPath.EndsWithF("\") AndAlso Not ConfigPackPath.EndsWithF("/")) Then
  408. Try
  409. DirectoryUtils.Create(PathUtils.RemoveLastPart(ConfigPackPath))
  410. PackPath = ConfigPackPath
  411. Logger.Info($"使用配置文件中指定的导出路径:{ConfigPackPath}")
  412. Catch ex As Exception
  413. Logger.Warn(ex, $"无法使用配置文件中指定的导出路径({ConfigPackPath})")
  414. If MyMsgBox($"指定的路径:{ConfigPackPath}{vbCrLf}{vbCrLf}{ex.GetDisplay(True)}", "无法使用配置文件中指定的导出路径", "确定", "取消") = 2 Then Return
  415. End Try
  416. End If
  417. If PackPath Is Nothing Then
  418. Dim Extensions As New List(Of (Extension As String, Display As String))
  419. If Not CheckOptionsPcl.Checked AndAlso CheckAdvancedModrinth.Checked Then Extensions.Add(("mrpack", "Modrinth 整合包文件"))
  420. Extensions.Add(("zip", "压缩文件"))
  421. If Not CheckOptionsPcl.Checked AndAlso Not CheckAdvancedModrinth.Checked Then Extensions.Add(("mrpack", "Modrinth 整合包文件"))
  422. PackPath = Dialogs.SaveFile("选择导出位置",
  423. PackName & If(String.IsNullOrEmpty(TextExportVersion.Text), "", " " & TextExportVersion.Text), filter:=Extensions)
  424. Logger.Info($"手动指定的导出路径:{PackPath}")
  425. End If
  426. If PackPath Is Nothing Then Return
  427. '缓存所需参数
  428. Dim CacheFolder = RequestTaskTempFolder()
  429. Dim OverridesFolder = CacheFolder & "modpack\overrides\"
  430. Dim TargetInstance = PageInstanceLeft.Instance
  431. Dim IncludeJava As Boolean =
  432. (RulesOverrides Is Nothing AndAlso CheckOptionsJava.Visibility = Visibility.Visible AndAlso CheckOptionsJava.Checked) OrElse
  433. (RulesOverrides IsNot Nothing AndAlso CheckOptionsJava.Checked AndAlso Settings.Get(Of Integer)("VersionArgumentJavaV2", TargetInstance) = 2)
  434. Dim PathIndie As String = TargetInstance.PathIndie
  435. Dim CheckHostedAssets As Boolean = Not CheckAdvancedInclude.Checked
  436. Dim ModrinthUploadMode As Boolean = CheckAdvancedModrinth.Checked
  437. Dim IncludePCL As Boolean = CheckOptionsPcl.Checked
  438. Dim IncludePCLCustom As Boolean = IncludePCL AndAlso CheckOptionsPclCustom.Checked
  439. Dim AllRules = StandardizeLines(GetAllRules(), True).ToList()
  440. Dim AllExtraFiles = StandardizeLines(GetExtraFileLines(), False).ToList()
  441. Logger.Info($"准备导出整合包,共有 {AllRules.Count} 条规则,{AllExtraFiles.Count} 条追加内容行")
  442. '构造步骤加载器
  443. Dim Loaders As New List(Of LoaderBase)
  444. #Region "准备 PCL 文件"
  445. If BuildType <> BuildTypes.Release AndAlso IncludePCL Then
  446. Loaders.Add(New LoaderTask(Of Integer, Integer)("下载 PCL 正式版",
  447. Sub(Loader As LoaderTask(Of Integer, Integer))
  448. DownloadLatestPCL(Loader)
  449. FileUtils.Copy(PathTemp & "Latest.exe", CacheFolder & "Plain Craft Launcher.exe")
  450. End Sub) With {.ProgressWeight = 0.5, .Block = False})
  451. End If
  452. #End Region
  453. #Region "复制文件"
  454. Loaders.Add(New LoaderTask(Of Integer, List(Of LocalResourceFile))("复制导出内容",
  455. Sub(Loader As LoaderTask(Of Integer, List(Of LocalResourceFile)))
  456. Loader.Output = New List(Of LocalResourceFile)
  457. '复制版本文件
  458. Dim Progress As Integer = 0
  459. Dim SearchFolder As Action(Of DirectoryInfo)
  460. SearchFolder =
  461. Sub(Folder As DirectoryInfo)
  462. '文件夹:进一步搜索
  463. For Each SubFolder In Folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
  464. '跳过部分又没用文件又多的文件夹,加快搜索
  465. If {"assets", "versions", "libraries"}.Contains(SubFolder.Name) AndAlso PathUtils.IsSame(PathIndie, Folder.FullName) Then Continue For
  466. If {"structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache"}.Contains(SubFolder.Name) Then Continue For
  467. SearchFolder(SubFolder)
  468. Next
  469. '文件:检查规则并复制
  470. For Each Entry In Folder.EnumerateFiles("*", SearchOption.TopDirectoryOnly)
  471. Dim RelativePath As String = Entry.FullName.AfterFirst(PathIndie)
  472. '检查规则
  473. Dim ShouldKeep As Boolean = False
  474. For Each Rule In AllRules
  475. Dim Revert = Rule.StartsWithF("!")
  476. If RelativePath.Lower Like Rule.TrimStart("!").Lower Then ShouldKeep = Not Revert
  477. Next
  478. If Not ShouldKeep Then Continue For
  479. Dim TargetPath As String = OverridesFolder & RelativePath
  480. FileUtils.Copy(Entry.FullName, TargetPath)
  481. '若为压缩包,考虑联网获取路径
  482. If CheckHostedAssets AndAlso
  483. {".zip", ".rar", ".jar", ".disabled", ".old"}.Contains(Entry.Extension.Lower) AndAlso
  484. {"mods", "packs", "openloader", "resource"}.Any(Function(s) RelativePath.Contains(s)) Then
  485. Dim ModFile As New LocalResourceFile(TargetPath)
  486. Dim Unused = ModFile.ModrinthHash '提前计算 Hash
  487. Unused = ModFile.CurseForgeHash
  488. Loader.Output.Add(ModFile)
  489. End If
  490. '更新进度(进度并不准确,主要突出一个我还没似)
  491. Progress += 1
  492. If Progress = 25 Then
  493. Loader.Progress += (0.84 - Loader.Progress) * 0.012
  494. Progress = 0
  495. End If
  496. Next
  497. End Sub
  498. SearchFolder(DirectoryUtils.GetInfo(PathIndie))
  499. Logger.Info($"复制 overrides 文件完成,有 {Loader.Output.Count} 个文件需要联网检查")
  500. Loader.Progress = 0.85
  501. '复制 Java
  502. If IncludeJava Then
  503. Dim JavaToExport = SelectOrDownloadJava(TargetInstance, False, Loader.CreateCancellationToken, Loader.CreateSyncProgressProvider(0.85, 0.93))
  504. If JavaToExport Is Nothing Then Throw New FileNotFoundException("版本文件夹中没有找到可导出的 Java。")
  505. Dim JavaFolder = PathUtils.RemoveLastPart(JavaToExport.Folder)
  506. Dim DestFolder As String = Path.Combine(OverridesFolder, JavaFolder.AfterFirst(TargetInstance.PathVersion, True))
  507. DirectoryUtils.Copy(JavaFolder, DestFolder)
  508. Logger.Info($"已复制版本文件夹中的 Java:{JavaFolder} → {DestFolder}")
  509. End If
  510. Loader.Progress = 0.95
  511. '复制追加内容到根目录
  512. Dim BaseFolder As String = If(IncludePCL, CacheFolder, CacheFolder & "modpack\")
  513. For Each Line In AllExtraFiles
  514. If Line.EndsWithF("\") OrElse Line.EndsWithF("/") Then
  515. If DirectoryUtils.Exists(Line) Then
  516. DirectoryUtils.Copy(Line, BaseFolder & PathUtils.GetLastPart(Line) & "\")
  517. Else
  518. Hint($"未找到配置文件中指定的文件夹:{Line}", HintType.Red)
  519. End If
  520. Else
  521. If FileUtils.Exists(Line) Then
  522. FileUtils.Copy(Line, BaseFolder & PathUtils.GetLastPart(Line))
  523. Else
  524. Hint($"未找到配置文件中指定的单个文件:{Line}", HintType.Red)
  525. End If
  526. End If
  527. Next
  528. Loader.Progress = 0.97
  529. '复制 PCL 版本设置
  530. If DirectoryUtils.Exists(TargetInstance.PathVersion & "PCL\") Then DirectoryUtils.Copy(TargetInstance.PathVersion & "PCL\", OverridesFolder & "PCL\")
  531. WriteIni(OverridesFolder & "PCL\Setup.ini", "IsStar", False)
  532. '复制 PCL 本体(正式版)
  533. If BuildType = BuildTypes.Release AndAlso IncludePCL Then FileUtils.Copy(PathExe, CacheFolder & "Plain Craft Launcher.exe")
  534. '复制 PCL 个性化内容
  535. If IncludePCLCustom Then
  536. If DirectoryUtils.Exists(Paths.Base & "PCL\Pictures\") Then DirectoryUtils.Copy(Paths.Base & "PCL\Pictures\", CacheFolder & "PCL\Pictures\")
  537. If DirectoryUtils.Exists(Paths.Base & "PCL\Musics\") Then DirectoryUtils.Copy(Paths.Base & "PCL\Musics\", CacheFolder & "PCL\Musics\")
  538. If DirectoryUtils.Exists(Paths.Base & "PCL\Help\") Then DirectoryUtils.Copy(Paths.Base & "PCL\Help\", CacheFolder & "PCL\Help\")
  539. If FileUtils.Exists(Paths.Base & "PCL\Custom.xaml") Then FileUtils.Copy(Paths.Base & "PCL\Custom.xaml", CacheFolder & "PCL\Custom.xaml")
  540. If FileUtils.Exists(Paths.Base & "PCL\Setup.ini") Then FileUtils.Copy(Paths.Base & "PCL\Setup.ini", CacheFolder & "PCL\Setup.ini")
  541. If FileUtils.Exists(Paths.Base & "PCL\hints.txt") Then FileUtils.Copy(Paths.Base & "PCL\hints.txt", CacheFolder & "PCL\hints.txt")
  542. If FileUtils.Exists(Paths.Base & "PCL\Logo.png") Then FileUtils.Copy(Paths.Base & "PCL\Logo.png", CacheFolder & "PCL\Logo.png")
  543. End If
  544. End Sub) With {.ProgressWeight = 5})
  545. #End Region
  546. #Region "联网检查"
  547. Loaders.Add(New LoaderTask(Of List(Of LocalResourceFile), Dictionary(Of LocalResourceFile, List(Of String)))("联网获取文件信息",
  548. Sub(Loader As LoaderTask(Of List(Of LocalResourceFile), Dictionary(Of LocalResourceFile, List(Of String))))
  549. Loader.Output = New Dictionary(Of LocalResourceFile, List(Of String))
  550. If Not CheckHostedAssets Then Logger.Info($"要求跳过联网获取步骤") : Return
  551. If Not Loader.Input.Any Then Logger.Info($"没有需要联网检查的文件,跳过联网获取步骤") : Return
  552. '分平台获取下载地址
  553. Dim EndedThreadCount As Integer = 0, FailedExceptions As New List(Of Exception)
  554. '从 Modrinth 获取信息
  555. RunInNewThread(
  556. Sub()
  557. Try
  558. Dim ModrinthHashes = Loader.Input.Select(Function(m) m.ModrinthHash)
  559. Dim ModrinthRaw As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
  560. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
  561. For Each ModFile In Loader.Input
  562. '查找对应的文件
  563. If Not ModrinthRaw.ContainsKey(ModFile.ModrinthHash) Then Continue For
  564. If ModrinthRaw(ModFile.ModrinthHash)?("files")?(0)("hashes")?("sha1") <> ModFile.ModrinthHash Then Continue For
  565. '写入下载地址
  566. Loader.Output.AddIntoValueCollection(ModFile, ModrinthRaw(ModFile.ModrinthHash)("files")(0)("url"))
  567. Next
  568. Logger.Info($"从 Modrinth 获取到 {ModrinthRaw.Count} 个本地资源项的对应信息")
  569. Catch ex As Exception
  570. Logger.Warn(ex, "从 Modrinth 获取本地 Mod 信息失败")
  571. FailedExceptions.Add(ex)
  572. Finally
  573. EndedThreadCount += 1
  574. Loader.Progress += 0.45
  575. End Try
  576. End Sub, "Modrinth - " & LoaderName)
  577. '从 CurseForge 获取信息
  578. RunInNewThread(
  579. Sub()
  580. Try
  581. If ModrinthUploadMode Then Return 'Modrinth 上传模式下,不从 CurseForge 获取信息
  582. Dim CurseForgeHashes = Loader.Input.Select(Function(m) m.CurseForgeHash)
  583. Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
  584. $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
  585. For Each ResultJson As JObject In CurseForgeRaw
  586. If Not ResultJson.ContainsKey("file") Then Continue For
  587. Dim File As JObject = ResultJson("file")
  588. If String.IsNullOrEmpty(File("downloadUrl")) Then Continue For
  589. '查找对应的文件
  590. Dim ModFile As LocalResourceFile = Loader.Input.FirstOrDefault(Function(m) m.CurseForgeHash = File("fileFingerprint").ToObject(Of UInteger))
  591. If ModFile Is Nothing Then Continue For
  592. '写入下载地址
  593. For Each Address In ResourceVersion.ParseCurseForgeDownloadUrls(File("downloadUrl").ToString)
  594. Loader.Output.AddIntoValueCollection(ModFile, Address)
  595. Next
  596. Next
  597. Logger.Info($"从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地资源项的对应信息")
  598. Catch ex As Exception
  599. Logger.Warn(ex, "从 CurseForge 获取本地 Mod 信息失败")
  600. FailedExceptions.Add(ex)
  601. Finally
  602. EndedThreadCount += 1
  603. Loader.Progress += 0.45
  604. End Try
  605. End Sub, "CurseForge - " & LoaderName)
  606. '等待线程结束
  607. Do Until EndedThreadCount = 2
  608. If Loader.IsCanceled Then Return
  609. Thread.Sleep(10)
  610. Loop
  611. '若失败,确认是否继续
  612. If FailedExceptions.Count = 1 Then
  613. If MyMsgBox("联网获取部分文件信息失败,是否继续导出?" & vbCrLf & vbCrLf &
  614. "若继续,无法获取信息的文件将被直接打包。" & vbCrLf &
  615. "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!",
  616. "部分文件信息获取失败", "继续", "取消") = 2 Then Throw FailedExceptions.First
  617. ElseIf FailedExceptions.Count > 1 Then
  618. If MyMsgBox("联网获取文件信息失败,是否继续导出?" & vbCrLf & vbCrLf &
  619. "若继续,所有文件都将被直接打包。" & vbCrLf &
  620. "由于二次分发可能违反使用协议,请尽量不要公开发布导出的整合包!",
  621. "文件信息获取失败", "继续", "取消") = 2 Then Throw FailedExceptions.First
  622. End If
  623. End Sub) With {.Show = CheckHostedAssets, .ProgressWeight = If(CheckHostedAssets, 2, 0.01)})
  624. #End Region
  625. #Region "生成压缩包"
  626. Loaders.Add(New LoaderTask(Of Dictionary(Of LocalResourceFile, List(Of String)), Integer)("生成压缩包",
  627. Sub(Loader As LoaderTask(Of Dictionary(Of LocalResourceFile, List(Of String)), Integer))
  628. '整理文件列表
  629. Dim Files As New JArray
  630. For Each Pair In Loader.Input
  631. Dim ModFile As LocalResourceFile = Pair.Key
  632. Files.Add(New JObject From {
  633. {"path", PathUtils.RemoveExtendedPrefix(ModFile.File.FullName).AfterFirst(OverridesFolder).Replace("\", "/")},
  634. {"hashes", New JObject From {{"sha1", ModFile.ModrinthHash}, {"sha512", CryptographyUtils.ComputeFileHash(ModFile.File.FullName, CryptographyUtils.HashMethod.Sha512)}}},
  635. {"downloads", New JArray(Pair.Value.OrderBy(Function(u) u.Contains("modrinth.com")))}, '不优先选择 Modrinth
  636. {"fileSize", ModFile.File.Length}
  637. })
  638. ModFile.File.Delete()
  639. Next
  640. Loader.Progress = 0.2
  641. '导出最终 JSON 文件
  642. Dim Dependencies As New JObject From {{"minecraft", TargetInstance.Version.VanillaName}}
  643. If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then Dependencies.Add("forge", TargetInstance.Version.ModLoaderVersions(ModLoaders.Forge))
  644. If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Fabric) Then Dependencies.Add("fabric-loader", TargetInstance.Version.ModLoaderVersions(ModLoaders.Fabric))
  645. If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge) Then Dependencies.Add("neoforge", TargetInstance.Version.ModLoaderVersions(ModLoaders.NeoForge))
  646. Dim ResultJson As New JObject From {
  647. {"game", "minecraft"},
  648. {"formatVersion", 1},
  649. {"versionId", PackVersion},
  650. {"name", PackName},
  651. {"summary", TargetInstance.Info},
  652. {"files", Files},
  653. {"dependencies", Dependencies}
  654. }
  655. FileUtils.Write(CacheFolder & "modpack\modrinth.index.json", ResultJson.ToString(Newtonsoft.Json.Formatting.Indented))
  656. '打包
  657. If IncludePCL Then
  658. '首次压缩整合包
  659. FileUtils.CreateZipFromDirectory(CacheFolder & "modpack.mrpack", CacheFolder & "modpack\")
  660. Loader.Progress = 0.5
  661. DirectoryUtils.Delete(CacheFolder & "modpack\")
  662. Loader.Progress = 0.6
  663. '二次压缩整合包
  664. FileUtils.CreateZipFromDirectory(PackPath, CacheFolder)
  665. Loader.Progress = 0.9
  666. Else
  667. '直接压缩整合包
  668. FileUtils.CreateZipFromDirectory(PackPath, CacheFolder & "modpack\")
  669. Loader.Progress = 0.8
  670. End If
  671. OpenExplorer(PackPath)
  672. End Sub) With {.ProgressWeight = 6})
  673. #End Region
  674. '启动
  675. Dim MainLoader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
  676. MainLoader.Start()
  677. LoaderTaskbarAdd(MainLoader)
  678. FrmMain.BtnExtraDownload.ShowRefresh()
  679. FrmMain.BtnExtraDownload.Ribble()
  680. FrmMain.PageChange(FormMain.PageType.TaskManager)
  681. End Sub
  682. #End Region
  683. Private Sub RefreshJavaInfo() Handles Me.Loaded
  684. Dim JavaType As Integer = Settings.Get(Of Integer)("VersionArgumentJavaV2", PageInstanceLeft.Instance)
  685. Dim JavaWorker = PageInstanceLeft.CurrentJavaWorker
  686. '导出选项
  687. Dim JavaToExport = If(JavaType = 2 AndAlso JavaWorker.LastSucceeded AndAlso Not JavaWorker.Running, JavaWorker.LastResult, Nothing)
  688. If JavaToExport Is Nothing Then
  689. CheckOptionsJava.Visibility = Visibility.Collapsed
  690. Else
  691. CheckOptionsJava.Visibility = Visibility.Visible
  692. CheckOptionsJava.Inlines.Clear()
  693. CheckOptionsJava.Inlines.Add(New Run(
  694. GetExportOption(CheckOptionsJava).Title))
  695. CheckOptionsJava.Inlines.Add(New Run(" " &
  696. $"Java {JavaToExport.Version.Major} ({JavaToExport.Version}):{ _
  697. PathUtils.AddSlashSuffix(PathUtils.RemoveLastPart(JavaToExport.Folder.Replace(PathUtils.RemoveLastPart(PathUtils.RemoveLastPart(PageInstanceLeft.Instance.PathVersion)), ""))).TrimStart("\")}") With {.Foreground = ColorGray5})
  698. End If
  699. '警告条
  700. If JavaType = 3 Then
  701. BtnExport.IsEnabled = True
  702. HintJava.Visibility = Visibility.Visible
  703. HintJava.Theme = MyHint.Themes.Yellow
  704. HintJava.Text = $"该版本设置要求 {vbLQ}使用指定的 Java{vbRQ},这在导出后很可能会失效。建议修改为 {vbLQ}使用版本文件夹中的 Java{vbRQ}。"
  705. ElseIf JavaType = 2 AndAlso JavaWorker.HasSucceeded AndAlso JavaWorker.LastResult Is Nothing Then
  706. BtnExport.IsEnabled = False
  707. HintJava.Visibility = Visibility.Visible
  708. HintJava.Theme = MyHint.Themes.Red
  709. HintJava.Text = $"该版本设置要求 {vbLQ}使用版本文件夹中的 Java{vbRQ},但版本文件夹中没能找到任何 Java。"
  710. Else
  711. BtnExport.IsEnabled = True
  712. HintJava.Visibility = Visibility.Collapsed
  713. End If
  714. End Sub
  715. '自动填写整合包名称
  716. Private Sub TextExportName_GotFocus() Handles TextExportName.GotFocus
  717. If TextExportName.Text = "" Then
  718. TextExportName.Text = TextExportName.HintText
  719. TextExportName.SelectionStart = TextExportName.Text.Length
  720. End If
  721. End Sub
  722. Private Sub CheckAdvancedInclude_Change(sender As Object, user As Boolean) Handles CheckAdvancedInclude.Change
  723. '勾选打包资源文件时,禁止开启仅 Modrinth 资源模式
  724. If CheckAdvancedInclude.Checked Then CheckAdvancedModrinth.Checked = False
  725. CheckAdvancedModrinth.IsEnabled = Not CheckAdvancedInclude.Checked
  726. End Sub
  727. Private Sub CheckAdvancedModrinth_Change(sender As Object, user As Boolean) Handles CheckAdvancedModrinth.Change, CheckOptionsPcl.Change
  728. HintModrinthFormat.Visibility = (CheckAdvancedModrinth.Checked AndAlso CheckOptionsPcl.Checked).ToVisibility
  729. End Sub
  730. End Class