ModMod.vb 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. Imports System.IO.Compression
  2. Public Module ModMod
  3. Private Const LocalModCacheVersion As Integer = 10
  4. Public Class McMod
  5. #Region "基础"
  6. ''' <summary>
  7. ''' Mod 文件的地址。
  8. ''' </summary>
  9. Public ReadOnly Path As String
  10. Public Sub New(Path As String)
  11. Me.Path = If(Path, "")
  12. End Sub
  13. ''' <summary>
  14. ''' Mod 的完整路径,去除最后的 .disabled 和 .old。
  15. ''' </summary>
  16. Public ReadOnly Property RawPath As String
  17. Get
  18. Return GetPathFromFullPath(Path) & RawFileName
  19. End Get
  20. End Property
  21. ''' <summary>
  22. ''' Mod 的完整文件名。
  23. ''' </summary>
  24. Public ReadOnly Property FileName As String
  25. Get
  26. Return GetFileNameFromPath(Path)
  27. End Get
  28. End Property
  29. ''' <summary>
  30. ''' Mod 的完整文件名,去除最后的 .disabled 和 .old。
  31. ''' </summary>
  32. Public ReadOnly Property RawFileName As String
  33. Get
  34. Return FileName.Replace(".disabled", "").Replace(".old", "")
  35. End Get
  36. End Property
  37. ''' <summary>
  38. ''' Mod 的状态。
  39. ''' </summary>
  40. Public ReadOnly Property State As McModState
  41. Get
  42. If Path.EndsWithF(".disabled", True) OrElse Path.EndsWithF(".old", True) Then
  43. Return McModState.Disabled
  44. Else
  45. Return McModState.Fine
  46. End If
  47. End Get
  48. End Property
  49. Public Enum McModState As Integer
  50. Fine = 0
  51. Disabled = 1
  52. End Enum
  53. #End Region
  54. #Region "信息项"
  55. ''' <summary>
  56. ''' 当任何信息在初始化后更新时触发。
  57. ''' </summary>
  58. Public Event OnUpdate(sender As McMod)
  59. ''' <summary>
  60. ''' Mod 的可读名称。
  61. ''' 可能为无扩展的文件名,但不会是 Nothing。
  62. ''' </summary>
  63. Public Property DisplayName As String
  64. Get
  65. If _Name Is Nothing Then _Name = CompFile?.DisplayName
  66. If _Name Is Nothing Then _Name = IO.Path.GetFileNameWithoutExtension(Path)
  67. Return _Name
  68. End Get
  69. Set(value As String)
  70. If _Name Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
  71. value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  72. _Name = value
  73. End If
  74. End Set
  75. End Property
  76. Private _Name As String = Nothing
  77. ''' <summary>
  78. ''' Mod 的描述信息。
  79. ''' 可能为 Nothing。
  80. ''' </summary>
  81. Public Property Description As String
  82. Get
  83. Return _Description
  84. End Get
  85. Set(value As String)
  86. If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then
  87. _Description = value.ToString.Trim(vbLf)
  88. '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
  89. If _Description.ToLower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
  90. End If
  91. End Set
  92. End Property
  93. Private _Description As String = Nothing
  94. ''' <summary>
  95. ''' Mod 的版本。
  96. ''' 不保证符合版本格式规范,且可能为 Nothing。
  97. ''' </summary>
  98. Public Property Version As String
  99. Get
  100. If _Version Is Nothing Then _Version = CompFile?.Version
  101. Return _Version
  102. End Get
  103. Set(value As String)
  104. If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
  105. If value IsNot Nothing AndAlso value.ContainsF("version", True) Then value = "version" '需要修改的标识
  106. _Version = value
  107. End Set
  108. End Property
  109. Public _Version As String = Nothing
  110. #End Region
  111. #Region "从 JAR 获取信息"
  112. ''' <summary>
  113. ''' 从 JAR 文件中获取 Mod 信息。
  114. ''' </summary>
  115. Public Sub LoadMetadataFromJar()
  116. Static IsLoaded As Boolean = False
  117. If IsLoaded Then Return
  118. IsLoaded = True
  119. Dim Jar As ZipArchive = Nothing
  120. Try
  121. '基础可用性检查、打开 Jar 文件
  122. If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")")
  123. If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")")
  124. Jar = New ZipArchive(New FileStream(Path, FileMode.Open))
  125. '信息获取
  126. LoadMetadataFromJar(Jar)
  127. Catch ex As UnauthorizedAccessException
  128. Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
  129. Catch ex As Exception
  130. Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
  131. Finally
  132. If Jar IsNot Nothing Then Jar.Dispose()
  133. End Try
  134. End Sub
  135. Private Sub LoadMetadataFromJar(Jar As ZipArchive)
  136. #Region "尝试使用 mcmod.info"
  137. Try
  138. '获取信息文件
  139. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  140. Dim InfoString As String = Nothing
  141. If InfoEntry IsNot Nothing Then
  142. InfoString = ReadFile(InfoEntry.Open())
  143. If InfoString.Length < 15 Then InfoString = Nothing
  144. End If
  145. If InfoString Is Nothing Then Exit Try
  146. '获取可用 Json 项
  147. Dim InfoObject As JObject
  148. Dim JsonObject = GetJson(InfoString)
  149. If JsonObject.Type = JTokenType.Array Then
  150. InfoObject = JsonObject(0)
  151. Else
  152. InfoObject = JsonObject("modList")(0)
  153. End If
  154. '从文件中获取 Mod 信息项
  155. DisplayName = InfoObject("name")
  156. Description = InfoObject("description")
  157. Version = InfoObject("version")
  158. Catch ex As Exception
  159. Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer)
  160. End Try
  161. #End Region
  162. #Region "尝试使用 fabric.mod.json"
  163. Try
  164. '获取 fabric.mod.json 文件
  165. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  166. Dim FabricText As String = Nothing
  167. If FabricEntry IsNot Nothing Then
  168. FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
  169. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  170. End If
  171. If FabricText Is Nothing Then Exit Try
  172. Dim FabricObject As JObject = GetJson(FabricText)
  173. GotFabric:
  174. '从文件中获取 Mod 信息项
  175. If FabricObject.ContainsKey("name") Then DisplayName = FabricObject("name")
  176. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  177. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  178. '加载成功
  179. GoTo Finished
  180. Catch ex As Exception
  181. Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  182. End Try
  183. #End Region
  184. #Region "尝试使用 mods.toml"
  185. Try
  186. '获取 mods.toml 文件
  187. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  188. Dim TomlText As String = Nothing
  189. If TomlEntry IsNot Nothing Then
  190. TomlText = ReadFile(TomlEntry.Open())
  191. If TomlText.Length < 15 Then TomlText = Nothing
  192. End If
  193. If TomlText Is Nothing Then Exit Try
  194. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  195. Dim Lines As New List(Of String)
  196. For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
  197. If Line.StartsWithF("#") Then '去除注释
  198. Continue For
  199. ElseIf Line.Contains("#") Then
  200. Line = Line.Substring(0, Line.IndexOfF("#"))
  201. End If
  202. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  203. If Line.Any Then Lines.Add(Line) '去除空行
  204. Next
  205. '读取文件数据
  206. Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))}
  207. For i = 0 To Lines.Count - 1
  208. Dim Line As String = Lines(i)
  209. If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
  210. '段落标记
  211. Dim Header = Line.Trim("[]".ToCharArray)
  212. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  213. ElseIf Line.Contains("=") Then
  214. '字段标记
  215. Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  216. Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  217. Dim Value As Object
  218. If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
  219. '单行字符串
  220. Value = RawValue.Trim("""")
  221. ElseIf RawValue.StartsWithF("'''") Then
  222. '多行字符串
  223. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  224. If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  225. ValueLines(0) = ValueLines(0).TrimEnd("'")
  226. Else
  227. Do Until i >= Lines.Count - 1
  228. i += 1
  229. Dim ValueLine As String = Lines(i)
  230. If ValueLine.EndsWithF("'''") Then
  231. ValueLines.Add(ValueLine.TrimEnd("'"))
  232. Exit Do
  233. Else
  234. ValueLines.Add(ValueLine)
  235. End If
  236. Loop
  237. End If
  238. Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
  239. ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then
  240. '布尔型
  241. Value = (RawValue.ToLower = "true")
  242. ElseIf Val(RawValue).ToString = RawValue Then
  243. '数字型
  244. Value = Val(RawValue)
  245. Else
  246. '不知道是个啥玩意儿,直接存储
  247. Value = RawValue
  248. End If
  249. TomlData.Last.Value(Key) = Value
  250. Else
  251. '不知道是个啥玩意儿
  252. Exit Try
  253. End If
  254. Next
  255. '从文件数据中获取信息
  256. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  257. For Each TomlSubData In TomlData
  258. If TomlSubData.Key = "mods" Then
  259. ModEntry = TomlSubData.Value
  260. Exit For
  261. End If
  262. Next
  263. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  264. If ModEntry.ContainsKey("displayName") Then DisplayName = ModEntry("displayName")
  265. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  266. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  267. '加载成功
  268. GoTo Finished
  269. Catch ex As Exception
  270. Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer)
  271. End Try
  272. #End Region
  273. #Region "尝试使用 fml_cache_annotation.json"
  274. Try
  275. '获取 fml_cache_annotation.json 文件
  276. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  277. Dim FmlText As String = Nothing
  278. If FmlEntry IsNot Nothing Then
  279. FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
  280. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  281. End If
  282. If FmlText Is Nothing Then Exit Try
  283. Dim FmlJson As JObject = GetJson(FmlText)
  284. '获取可用 Json 项
  285. Dim FmlObject As JObject = Nothing
  286. For Each ModFilePair In FmlJson
  287. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  288. If ModFileAnnos IsNot Nothing Then
  289. '先获取 Mod
  290. For Each ModFileAnno In ModFileAnnos
  291. Dim Name As String = If(ModFileAnno("name"), "")
  292. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  293. FmlObject = ModFileAnno("values")
  294. GoTo Got
  295. End If
  296. Next
  297. End If
  298. Next
  299. Exit Try
  300. Got:
  301. '从文件中获取 Mod 信息项
  302. If FmlObject.ContainsKey("name") Then DisplayName = FmlObject("name")("value")
  303. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  304. '加载成功
  305. GoTo Finished
  306. Catch ex As Exception
  307. Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  308. End Try
  309. #End Region
  310. Return '没能成功获取任何信息
  311. Finished:
  312. #Region "将 Version 代号转换为 META-INF 中的版本"
  313. If _Version = "version" Then
  314. Try
  315. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  316. If MetaEntry IsNot Nothing Then
  317. Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
  318. If MetaString.Contains("Implementation-Version:") Then
  319. MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
  320. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  321. Version = MetaString
  322. End If
  323. End If
  324. Catch ex As Exception
  325. Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer)
  326. Version = Nothing
  327. End Try
  328. End If
  329. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  330. #End Region
  331. RaiseEvent OnUpdate(Me)
  332. End Sub
  333. #End Region
  334. #Region "网络信息"
  335. ''' <summary>
  336. ''' 该 Mod 关联的网络项目。
  337. ''' </summary>
  338. Public Property Comp As CompProject
  339. Get
  340. Return _Comp
  341. End Get
  342. Set(value As CompProject)
  343. _Comp = value
  344. RaiseEvent OnUpdate(Me)
  345. End Set
  346. End Property
  347. Private _Comp As CompProject
  348. ''' <summary>
  349. ''' 本地文件对应的联网文件信息。
  350. ''' </summary>
  351. Public CompFile As CompFile
  352. ''' <summary>
  353. ''' 该 Mod 对应的联网最新版本。
  354. ''' </summary>
  355. Public Property UpdateFile As CompFile
  356. Get
  357. Return _UpdateFile
  358. End Get
  359. Set(value As CompFile)
  360. _UpdateFile = value
  361. RaiseEvent OnUpdate(Me)
  362. End Set
  363. End Property
  364. Private _UpdateFile As CompFile
  365. ''' <summary>
  366. ''' 该 Mod 的更新日志网址。
  367. ''' </summary>
  368. Public ChangelogUrls As New List(Of String)
  369. ''' <summary>
  370. ''' 所有网络信息是否已成功加载。
  371. ''' </summary>
  372. Public CompLoaded As Boolean = False
  373. ''' <summary>
  374. ''' 将网络信息保存为 Json。
  375. ''' </summary>
  376. Public Function ToJson() As JObject
  377. Dim Json As New JObject
  378. If Comp IsNot Nothing Then Json.Add("Comp", Comp.ToJson())
  379. Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
  380. Json.Add("CompLoaded", CompLoaded)
  381. If CompFile IsNot Nothing Then Json.Add("CompFile", CompFile.ToJson())
  382. If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToJson())
  383. Return Json
  384. End Function
  385. ''' <summary>
  386. ''' 从 Json 中读取网络信息。
  387. ''' </summary>
  388. Public Sub FromJson(Json As JObject)
  389. CompLoaded = Json("CompLoaded")
  390. If Json.ContainsKey("Comp") Then Comp = New CompProject(Json("Comp"))
  391. If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
  392. If Json.ContainsKey("CompFile") Then CompFile = New CompFile(Json("CompFile"), CompType.Mod)
  393. If Json.ContainsKey("UpdateFile") Then UpdateFile = New CompFile(Json("UpdateFile"), CompType.Mod)
  394. End Sub
  395. ''' <summary>
  396. ''' 该文件是否可以更新。
  397. ''' </summary>
  398. Public ReadOnly Property CanUpdate As Boolean
  399. Get
  400. Return Not Setup.Get("UiHiddenFunctionModUpdate") AndAlso Not Setup.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso ChangelogUrls.Any()
  401. End Get
  402. End Property
  403. ''' <summary>
  404. ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
  405. ''' </summary>
  406. Public ReadOnly Property CurseForgeHash As UInteger
  407. Get
  408. If _CurseForgeHash Is Nothing Then
  409. '读取缓存
  410. Dim Info As New FileInfo(Path)
  411. Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-C")
  412. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  413. If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
  414. _CurseForgeHash = Cached
  415. Return _CurseForgeHash
  416. End If
  417. '读取文件
  418. Dim data As New List(Of Byte)
  419. For Each b As Byte In ReadFileBytes(Path)
  420. If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
  421. data.Add(b)
  422. Next
  423. '计算 MurmurHash2
  424. Dim length As Integer = data.Count
  425. Dim h As UInteger = 1 Xor length '1 是种子
  426. Dim i As Integer
  427. For i = 0 To length - 4 Step 4
  428. Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
  429. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  430. k = k Xor (k >> 24)
  431. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  432. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  433. h = h Xor k
  434. Next
  435. Select Case length - i
  436. Case 3
  437. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  438. h = h Xor (CUInt(data(i + 2)) << 16)
  439. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  440. Case 2
  441. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  442. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  443. Case 1
  444. h = h Xor data(i)
  445. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  446. End Select
  447. h = h Xor (h >> 13)
  448. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  449. h = h Xor (h >> 15)
  450. _CurseForgeHash = h
  451. '写入缓存
  452. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
  453. End If
  454. Return _CurseForgeHash
  455. End Get
  456. End Property
  457. Private _CurseForgeHash As UInteger?
  458. ''' <summary>
  459. ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
  460. ''' </summary>
  461. Public ReadOnly Property ModrinthHash As String
  462. Get
  463. If _ModrinthHash Is Nothing Then
  464. '读取缓存
  465. Dim Info As New FileInfo(Path)
  466. Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-M")
  467. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  468. If Cached <> "" Then
  469. _ModrinthHash = Cached
  470. Return _ModrinthHash
  471. End If
  472. '计算 SHA1
  473. _ModrinthHash = GetFileSHA1(Path)
  474. '写入缓存
  475. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
  476. End If
  477. Return _ModrinthHash
  478. End Get
  479. End Property
  480. Private _ModrinthHash As String
  481. #End Region
  482. #Region "API"
  483. Public Overrides Function ToString() As String
  484. Return $"{State} - {Path}"
  485. End Function
  486. Public Overrides Function Equals(obj As Object) As Boolean
  487. Dim target = TryCast(obj, McMod)
  488. Return target IsNot Nothing AndAlso Path = target.Path
  489. End Function
  490. #End Region
  491. ''' <summary>
  492. ''' 是否可能为前置 Mod。目前非常不准确。
  493. ''' </summary>
  494. Public Function IsPresetMod() As Boolean
  495. Return DisplayName IsNot Nothing AndAlso (DisplayName.ToLower.Contains("core") OrElse DisplayName.ToLower.Contains("lib"))
  496. End Function
  497. ''' <summary>
  498. ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
  499. ''' </summary>
  500. Public Shared Function IsModFile(Path As String)
  501. If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
  502. Path = Path.ToLower
  503. If Path.EndsWithF(".jar", True) OrElse Path.EndsWithF(".zip", True) OrElse Path.EndsWithF(".litemod", True) OrElse
  504. Path.EndsWithF(".jar.disabled", True) OrElse Path.EndsWithF(".zip.disabled", True) OrElse Path.EndsWithF(".litemod.disabled", True) OrElse
  505. Path.EndsWithF(".jar.old", True) OrElse Path.EndsWithF(".zip.old", True) OrElse Path.EndsWithF(".litemod.old", True) Then Return True
  506. Return False
  507. End Function
  508. End Class
  509. '加载 Mod 列表
  510. Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad)
  511. Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod)))
  512. Try
  513. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
  514. '等待 Mod 更新完成
  515. If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
  516. Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
  517. Try
  518. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
  519. Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
  520. If Loader.IsAborted Then Return
  521. Thread.Sleep(100)
  522. Loop
  523. Finally
  524. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
  525. End Try
  526. FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
  527. End If
  528. '获取 Mod 文件夹下的可用文件列表
  529. Dim ModFileList As New List(Of FileInfo)
  530. If Directory.Exists(Loader.Input) Then
  531. Dim RawName As String = Loader.Input.ToLower
  532. For Each File As FileInfo In EnumerateFiles(Loader.Input)
  533. If File.DirectoryName.ToLower & "\" <> RawName Then
  534. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  535. If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
  536. PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
  537. File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
  538. Continue For
  539. End If
  540. End If
  541. If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
  542. Next
  543. End If
  544. '获取本地文件缓存
  545. Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
  546. Dim Cache As New JObject
  547. Try
  548. Dim CacheContent As String = ReadFile(CachePath)
  549. If Not String.IsNullOrWhiteSpace(CacheContent) Then
  550. Cache = GetJson(CacheContent)
  551. If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
  552. Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", LogLevel.Debug)
  553. Cache = New JObject
  554. End If
  555. End If
  556. Catch ex As Exception
  557. Log(ex, "读取本地 Mod 信息缓存失败,已重置")
  558. Cache = New JObject
  559. End Try
  560. Cache("version") = LocalModCacheVersion
  561. '加载 Mod 列表
  562. Dim ModList As New List(Of McMod)
  563. For Each ModFile As FileInfo In ModFileList
  564. If Loader.IsAborted Then Return
  565. Dim ModEntry As New McMod(ModFile.FullName)
  566. Dim DumpMod As McMod = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName)
  567. If DumpMod IsNot Nothing Then
  568. Dim DisabledMod As McMod = If(DumpMod.State = McMod.McModState.Disabled, DumpMod, ModEntry)
  569. Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug)
  570. If DisabledMod Is ModEntry Then
  571. Continue For
  572. Else
  573. ModList.Remove(DisabledMod)
  574. End If
  575. End If
  576. ModList.Add(ModEntry)
  577. Next
  578. Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
  579. '排序
  580. ModList = ModList.SortByComparison(
  581. Function(Left, Right) As Boolean
  582. Return Not Right.FileName.CompareTo(Left.FileName)
  583. End Function)
  584. '回设
  585. If Loader.IsAborted Then Return
  586. Loader.Output = ModList
  587. '开始联网加载
  588. 'TODO: 添加信息获取中提示
  589. McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModList, Cache), IsForceRestart:=True)
  590. Catch ex As Exception
  591. Log(ex, "Mod 列表加载失败", LogLevel.Debug)
  592. Throw
  593. End Try
  594. End Sub
  595. '联网加载 Mod 详情
  596. Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
  597. Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
  598. Dim Mods As New List(Of McMod)
  599. Dim Cache As JObject = Loader.Input.Value
  600. '读取 Comp 缓存,获取需要更新的 Mod 列表
  601. For Each ModEntry As McMod In Loader.Input.Key
  602. If Loader.IsAborted Then Return
  603. Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
  604. If Cache.ContainsKey(CacheKey) Then
  605. ModEntry.FromJson(Cache(CacheKey))
  606. '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
  607. If ModEntry.CompLoaded AndAlso Date.Now - Cache(CacheKey)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
  608. End If
  609. Mods.Add(ModEntry)
  610. Next
  611. Log($"[Mod] 有 {Mods.Where(Function(m) m.Comp Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Comp IsNot Nothing).Count} 个 Mod 需要更新信息")
  612. If Not Mods.Any Then
  613. Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar()) '从 JAR 中获取缺失的版本信息(下面有另一个分支)
  614. Return
  615. End If
  616. '获取作为检查目标的加载器和版本
  617. '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
  618. Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
  619. Dim ModLoaders = GetTargetModLoaders()
  620. Dim VanillaVersion = TargetMcVersion.VanillaName
  621. '开始网络获取
  622. Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
  623. Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
  624. Dim CurrentTaskThread As Thread = Thread.CurrentThread
  625. '从 Modrinth 获取信息
  626. RunInNewThread(
  627. Sub()
  628. Try
  629. '步骤 1:获取 Hash 与对应的工程 ID
  630. Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
  631. Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
  632. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
  633. Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
  634. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  635. If ModrinthVersion.Count = 0 Then Return
  636. Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
  637. For Each Entry In Mods
  638. If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
  639. If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
  640. Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
  641. If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
  642. If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
  643. ModrinthMapping(ProjectId).Add(Entry)
  644. '记录对应的 CompFile
  645. Dim File As New CompFile(ModrinthVersion(Entry.ModrinthHash), CompType.Mod)
  646. If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then
  647. Entry.CompFile = File
  648. Else
  649. Entry.CompFile.Version = File.Version '使用来自 Modrinth 的版本号
  650. End If
  651. Next
  652. If Loader.IsAbortedWithThread(CurrentTaskThread) Then Return
  653. Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
  654. '步骤 3:获取工程信息
  655. If Not ModrinthMapping.Any() Then Return
  656. Dim ModrinthProject As JArray = DlModRequest(
  657. $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
  658. For Each ProjectJson In ModrinthProject
  659. Dim Project As New CompProject(ProjectJson)
  660. For Each Entry In ModrinthMapping(Project.Id)
  661. Entry.Comp = Project
  662. Next
  663. Next
  664. Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
  665. '步骤 4:获取更新信息
  666. Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
  667. $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"",
  668. ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
  669. For Each Entry In Mods
  670. If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
  671. Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod)
  672. If Not UpdateFile.Available Then Continue For
  673. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
  674. If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
  675. '设置更新日志与更新文件
  676. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  677. Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}")
  678. UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
  679. Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
  680. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
  681. Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}"}
  682. Entry.UpdateFile = UpdateFile
  683. End If
  684. Next
  685. Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
  686. Catch ex As Exception
  687. Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
  688. IsFailed = True
  689. Finally
  690. EndedThreadCount += 1
  691. End Try
  692. End Sub, "Mod List Detail Loader Modrinth")
  693. '从 CurseForge 获取信息
  694. RunInNewThread(
  695. Sub()
  696. Try
  697. '步骤 1:获取 Hash 与对应的工程 ID
  698. Dim CurseForgeHashes As New List(Of UInteger)
  699. For Each Entry In Mods
  700. CurseForgeHashes.Add(Entry.CurseForgeHash)
  701. If Loader.IsAbortedWithThread(CurrentTaskThread) Then Return
  702. Next
  703. Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
  704. $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
  705. Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
  706. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  707. If Not CurseForgeRaw.Any() Then Return
  708. Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of McMod))
  709. For Each Project In CurseForgeRaw
  710. Dim ProjectId = Project("id").ToString
  711. Dim Hash As UInteger = Project("file")("fileFingerprint")
  712. For Each Entry In Mods
  713. If Entry.CurseForgeHash <> Hash Then Continue For
  714. If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
  715. If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
  716. CurseForgeMapping(ProjectId).Add(Entry)
  717. '记录对应的 CompFile
  718. Dim File As New CompFile(Project("file"), CompType.Mod)
  719. If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File
  720. Next
  721. Next
  722. If Loader.IsAbortedWithThread(CurrentTaskThread) Then Return
  723. Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
  724. '步骤 3:获取工程信息
  725. If Not CurseForgeMapping.Any() Then Return
  726. Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
  727. $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
  728. Dim UpdateFileIds As New Dictionary(Of Integer, List(Of McMod)) 'FileId -> 本地 Mod 文件列表
  729. Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
  730. For Each ProjectJson In CurseForgeProject
  731. If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
  732. '设置 Entry 中的工程信息
  733. Dim Project As New CompProject(ProjectJson)
  734. For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
  735. If Entry.Comp IsNot Nothing AndAlso Not Entry.Comp.FromCurseForge Then
  736. Entry.Comp = Entry.Comp '再次触发修改事件
  737. Continue For
  738. End If
  739. Entry.Comp = Project
  740. Next
  741. '查找或许版本更新的文件列表
  742. If ModLoaders.Count = 1 Then
  743. Dim NewestVersion As String = Nothing
  744. Dim NewestFileIds As New List(Of Integer)
  745. For Each IndexEntry In ProjectJson("latestFilesIndexes")
  746. If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
  747. Dim IndexVersion As String = IndexEntry("gameVersion")
  748. If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
  749. '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
  750. If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
  751. If NewestVersion <> IndexVersion Then
  752. NewestVersion = IndexVersion
  753. NewestFileIds.Clear()
  754. End If
  755. NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
  756. Next
  757. For Each FileId In NewestFileIds
  758. If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of McMod)
  759. UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
  760. FileIdToProjectSlug(FileId) = Project.Slug
  761. Next
  762. End If
  763. Next
  764. Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
  765. '步骤 4:获取更新文件信息
  766. If Not UpdateFileIds.Any() Then Return
  767. Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
  768. $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
  769. Dim UpdateFiles As New Dictionary(Of McMod, CompFile)
  770. For Each FileJson In CurseForgeFiles
  771. Dim File As New CompFile(FileJson, CompType.Mod)
  772. If Not File.Available Then Continue For
  773. For Each Entry As McMod In UpdateFileIds(File.Id)
  774. If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
  775. UpdateFiles(Entry) = File
  776. Next
  777. Next
  778. For Each Pair In UpdateFiles
  779. Dim Entry As McMod = Pair.Key
  780. Dim UpdateFile As CompFile = Pair.Value
  781. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
  782. If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
  783. '设置更新日志与更新文件
  784. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  785. Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
  786. Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
  787. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
  788. Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
  789. Entry.UpdateFile = UpdateFile
  790. End If
  791. Next
  792. Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
  793. Catch ex As Exception
  794. Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
  795. IsFailed = True
  796. Finally
  797. EndedThreadCount += 1
  798. End Try
  799. End Sub, "Mod List Detail Loader CurseForge")
  800. '从 JAR 中获取缺失的版本信息(上面有另一个分支)
  801. Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar())
  802. '等待线程结束
  803. Do Until EndedThreadCount = 2
  804. Thread.Sleep(10)
  805. If Loader.IsAborted Then Return
  806. Loop
  807. '保存缓存
  808. Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
  809. Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
  810. If Not Mods.Any() Then Return
  811. For Each Entry In Mods
  812. Entry.CompLoaded = Not IsFailed
  813. Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
  814. Next
  815. WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
  816. '刷新边栏
  817. If Loader.IsAborted Then Return
  818. If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
  819. RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
  820. Else
  821. RunInUi(Sub() FrmInstanceMod?.RefreshBars())
  822. End If
  823. End Sub
  824. Public Function GetTargetModLoaders() As List(Of CompModLoaderType)
  825. Dim ModLoaders As New List(Of CompModLoaderType)
  826. If PageInstanceLeft.Instance.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
  827. If PageInstanceLeft.Instance.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)
  828. If PageInstanceLeft.Instance.Version.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
  829. If PageInstanceLeft.Instance.Version.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
  830. If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.NeoForge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
  831. Return ModLoaders
  832. End Function
  833. End Module