ModMod.vb 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278
  1. Imports System.IO.Compression
  2. Public Module ModMod
  3. Public Class McMod
  4. #Region "基础"
  5. ''' <summary>
  6. ''' Mod 文件的地址。
  7. ''' </summary>
  8. Public ReadOnly Path As String
  9. Public Sub New(Path As String)
  10. Me.Path = If(Path, "")
  11. End Sub
  12. ''' <summary>
  13. ''' Mod 的完整路径,去除最后的 .disabled 和 .old。
  14. ''' </summary>
  15. Public ReadOnly Property RawPath As String
  16. Get
  17. Return GetPathFromFullPath(Path) & RawFileName
  18. End Get
  19. End Property
  20. ''' <summary>
  21. ''' Mod 的完整文件名。
  22. ''' </summary>
  23. Public ReadOnly Property FileName As String
  24. Get
  25. Return GetFileNameFromPath(Path)
  26. End Get
  27. End Property
  28. ''' <summary>
  29. ''' Mod 的完整文件名,去除最后的 .disabled 和 .old。
  30. ''' </summary>
  31. Public ReadOnly Property RawFileName As String
  32. Get
  33. Return FileName.Replace(".disabled", "").Replace(".old", "")
  34. End Get
  35. End Property
  36. ''' <summary>
  37. ''' Mod 的状态。
  38. ''' </summary>
  39. Public ReadOnly Property State As McModState
  40. Get
  41. Load()
  42. If Not IsFileAvailable Then
  43. Return McModState.Unavaliable
  44. ElseIf Path.EndsWithF(".disabled", True) OrElse Path.EndsWithF(".old", True) Then
  45. Return McModState.Disabled
  46. Else
  47. Return McModState.Fine
  48. End If
  49. End Get
  50. End Property
  51. Public Enum McModState As Integer
  52. Fine = 0
  53. Disabled = 1
  54. Unavaliable = 2
  55. End Enum
  56. #End Region
  57. #Region "信息项"
  58. ''' <summary>
  59. ''' Mod 的名称。若不可用则为 ModID 或无扩展的文件名。
  60. ''' </summary>
  61. Public Property Name As String
  62. Get
  63. If _Name Is Nothing Then Load()
  64. If _Name Is Nothing Then _Name = _ModId
  65. If _Name Is Nothing Then _Name = GetFileNameWithoutExtentionFromPath(Path)
  66. Return _Name
  67. End Get
  68. Set(value As String)
  69. If _Name Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
  70. value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  71. _Name = value
  72. End If
  73. End Set
  74. End Property
  75. Private _Name As String = Nothing
  76. ''' <summary>
  77. ''' Mod 的描述信息。
  78. ''' </summary>
  79. Public Property Description As String
  80. Get
  81. If _Description Is Nothing Then Load()
  82. If _Description Is Nothing AndAlso FileUnavailableReason IsNot Nothing Then _Description = FileUnavailableReason.Message
  83. 'If _Description Is Nothing Then _Description = Path
  84. Return _Description
  85. End Get
  86. Set(value As String)
  87. If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then
  88. _Description = value.ToString.Trim(vbLf)
  89. '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
  90. If _Description.ToLower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
  91. End If
  92. End Set
  93. End Property
  94. Private _Description As String = Nothing
  95. ''' <summary>
  96. ''' Mod 的版本,不保证符合版本格式规范。
  97. ''' </summary>
  98. Public Property Version As String
  99. Get
  100. If _Version Is Nothing Then Load()
  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?.ContainsF("version", True) Then value = "version" '需要修改的标识
  106. _Version = value
  107. End Set
  108. End Property
  109. Public _Version As String = Nothing
  110. ''' <summary>
  111. ''' 用于依赖检查的 ModID。
  112. ''' </summary>
  113. Public Property ModId As String
  114. Get
  115. If _ModId Is Nothing Then Load()
  116. Return _ModId
  117. End Get
  118. Set(value As String)
  119. If value Is Nothing Then Return
  120. value = RegexSeek(value, "[0-9a-zA-Z_-]+")
  121. If value Is Nothing OrElse value.Count <= 1 OrElse Val(value).ToString = value Then Return
  122. If value.ContainsF("name", True) OrElse value.ContainsF("modid", True) Then Return
  123. If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
  124. If _ModId Is Nothing Then _ModId = value
  125. End Set
  126. End Property
  127. Private _ModId As String = Nothing
  128. ''' <summary>
  129. ''' 其他可能的 ModID。
  130. ''' </summary>
  131. Public PossibleModId As New List(Of String)
  132. ''' <summary>
  133. ''' Mod 的主页。
  134. ''' </summary>
  135. Public Property Url As String
  136. Get
  137. If _Url Is Nothing Then Load()
  138. Return _Url
  139. End Get
  140. Set(value As String)
  141. If _Url Is Nothing AndAlso value IsNot Nothing AndAlso value.StartsWithF("http") Then
  142. _Url = value
  143. End If
  144. End Set
  145. End Property
  146. Private _Url As String = Nothing
  147. ''' <summary>
  148. ''' Mod 的作者列表。
  149. ''' </summary>
  150. Public Property Authors As String
  151. Get
  152. If _Authors Is Nothing Then Load()
  153. Return _Authors
  154. End Get
  155. Set(value As String)
  156. If _Authors Is Nothing AndAlso Not String.IsNullOrWhiteSpace(value) Then
  157. _Authors = value
  158. End If
  159. End Set
  160. End Property
  161. Private _Authors As String = Nothing
  162. ''' <summary>
  163. ''' 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。
  164. ''' </summary>
  165. Public ReadOnly Property Dependencies As Dictionary(Of String, String)
  166. Get
  167. Load()
  168. Return _Dependencies
  169. End Get
  170. End Property
  171. Private _Dependencies As New Dictionary(Of String, String)
  172. Private Sub AddDependency(ModID As String, Optional VersionRequirement As String = Nothing)
  173. '确保信息正确
  174. If ModID Is Nothing OrElse ModID.Count < 2 Then Exit Sub
  175. ModID = ModID.ToLower
  176. If ModID = "name" OrElse Val(ModID).ToString = ModID Then Exit Sub '跳过 name 与纯数字 id
  177. If VersionRequirement Is Nothing OrElse ((Not VersionRequirement.Contains(".")) AndAlso (Not VersionRequirement.Contains("-"))) OrElse VersionRequirement.Contains("$") Then
  178. VersionRequirement = Nothing
  179. Else
  180. If (Not VersionRequirement.StartsWithF("[")) AndAlso (Not VersionRequirement.StartsWithF("(")) AndAlso (Not VersionRequirement.EndsWithF("]")) AndAlso (Not VersionRequirement.EndsWithF(")")) Then VersionRequirement = "[" & VersionRequirement & ",)"
  181. End If
  182. '向依赖项中添加
  183. If _Dependencies.ContainsKey(ModID) Then
  184. If _Dependencies(ModID) Is Nothing Then _Dependencies(ModID) = VersionRequirement
  185. Else
  186. _Dependencies.Add(ModID, VersionRequirement)
  187. End If
  188. End Sub
  189. #End Region
  190. #Region "加载步骤标记"
  191. '1. 进行文件可用性检查
  192. ' 成功:继续第二步。
  193. ' 失败:标记 FileUnavailableReason, 并停止后续加载。
  194. ''' <summary>
  195. ''' 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步)
  196. ''' </summary>
  197. Private IsLoaded As Boolean = False
  198. ''' <summary>
  199. ''' Mod 文件是否可被正常读取。
  200. ''' </summary>
  201. Public ReadOnly Property IsFileAvailable As Boolean
  202. Get
  203. Load()
  204. Return FileUnavailableReason Is Nothing
  205. End Get
  206. End Property
  207. ''' <summary>
  208. ''' Mod 文件出错的原因。若无错误,则为 Nothing。
  209. ''' </summary>
  210. Public ReadOnly Property FileUnavailableReason As Exception
  211. Get
  212. Load()
  213. Return _FileUnavailableReason
  214. End Get
  215. End Property
  216. Private _FileUnavailableReason As Exception = Nothing
  217. '2. 进行 .class 以外的信息获取
  218. ' 成功:标记 IsInfoWithoutClassAvailable。
  219. ' 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。
  220. ''' <summary>
  221. ''' 是否已在不获取 .class 文件的前提下完成了所需信息的加载。
  222. ''' </summary>
  223. Private IsInfoWithoutClassAvailable As Boolean = False
  224. '3. 尝试从 .class 文件中获取信息
  225. ' 成功:标记 IsInfoWithClassAvailable。
  226. ' 失败:什么也不干。
  227. ''' <summary>
  228. ''' 是否已进行 .class 文件的信息获取。
  229. ''' </summary>
  230. Private IsInfoWithClassLoaded As Boolean = False
  231. ''' <summary>
  232. ''' 是否已在 .class 文件中完成了所需信息的加载。
  233. ''' </summary>
  234. Private IsInfoWithClassAvailable As Boolean = False
  235. #End Region
  236. #Region "加载"
  237. ''' <summary>
  238. ''' 初始化所有数据。
  239. ''' </summary>
  240. Private Sub Init()
  241. _Name = Nothing
  242. _Description = Nothing
  243. _Version = Nothing
  244. _ModId = Nothing
  245. PossibleModId = New List(Of String)
  246. _Dependencies = New Dictionary(Of String, String)
  247. IsLoaded = False
  248. _FileUnavailableReason = Nothing
  249. IsInfoWithClassLoaded = False
  250. IsInfoWithClassAvailable = False
  251. End Sub
  252. ''' <summary>
  253. ''' 进行文件可用性检查与 .class 以外的信息获取。
  254. ''' </summary>
  255. Public Sub Load(Optional ForceReload As Boolean = False)
  256. If IsLoaded AndAlso Not ForceReload Then Exit Sub
  257. '初始化
  258. Init()
  259. Dim Jar As ZipArchive = Nothing
  260. Try
  261. '基础可用性检查、打开 Jar 文件
  262. If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")")
  263. If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")")
  264. Jar = New ZipArchive(New FileStream(Path, FileMode.Open))
  265. '信息获取
  266. LookupMetadata(Jar)
  267. Catch ex As UnauthorizedAccessException
  268. Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
  269. _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex)
  270. Catch ex As Exception
  271. Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
  272. _FileUnavailableReason = ex
  273. Finally
  274. If Jar IsNot Nothing Then Jar.Dispose()
  275. End Try
  276. '完成标记
  277. IsLoaded = True
  278. End Sub
  279. ''' <summary>
  280. ''' 从 jar 文件中获取 Mod 信息。
  281. ''' </summary>
  282. Private Sub LookupMetadata(Jar As ZipArchive)
  283. #Region "尝试使用 mcmod.info"
  284. Try
  285. '获取信息文件
  286. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  287. Dim InfoString As String = Nothing
  288. If InfoEntry IsNot Nothing Then
  289. InfoString = ReadFile(InfoEntry.Open())
  290. If InfoString.Length < 15 Then InfoString = Nothing
  291. End If
  292. If InfoString Is Nothing Then Exit Try
  293. '获取可用 Json 项
  294. Dim InfoObject As JObject
  295. Dim JsonObject = GetJson(InfoString)
  296. If JsonObject.Type = JTokenType.Array Then
  297. InfoObject = JsonObject(0)
  298. Else
  299. InfoObject = JsonObject("modList")(0)
  300. End If
  301. '从文件中获取 Mod 信息项
  302. Name = InfoObject("name")
  303. Description = InfoObject("description")
  304. Version = InfoObject("version")
  305. Url = InfoObject("url")
  306. ModId = InfoObject("modid")
  307. Dim AuthorJson As JArray = InfoObject("authorList")
  308. If AuthorJson IsNot Nothing Then
  309. Dim Author As New List(Of String)
  310. For Each Token In AuthorJson
  311. Author.Add(Token.ToString)
  312. Next
  313. If Author.Any Then Authors = Join(Author, ", ")
  314. End If
  315. Dim Reqs As JArray = InfoObject("requiredMods")
  316. If Reqs IsNot Nothing Then
  317. For Each Token As String In Reqs
  318. If Not String.IsNullOrEmpty(Token) Then
  319. Token = Token.Substring(Token.IndexOfF(":") + 1)
  320. If Token.Contains("@") Then
  321. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  322. Else
  323. AddDependency(Token)
  324. End If
  325. End If
  326. Next
  327. End If
  328. Reqs = InfoObject("dependancies")
  329. If Reqs IsNot Nothing Then
  330. For Each Token As String In Reqs
  331. If Not String.IsNullOrEmpty(Token) Then
  332. Token = Token.Substring(Token.IndexOfF(":") + 1)
  333. If Token.Contains("@") Then
  334. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  335. Else
  336. AddDependency(Token)
  337. End If
  338. End If
  339. Next
  340. End If
  341. Catch ex As Exception
  342. Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer)
  343. End Try
  344. #End Region
  345. #Region "尝试使用 fabric.mod.json"
  346. Try
  347. '获取 fabric.mod.json 文件
  348. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  349. Dim FabricText As String = Nothing
  350. If FabricEntry IsNot Nothing Then
  351. FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
  352. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  353. End If
  354. If FabricText Is Nothing Then Exit Try
  355. Dim FabricObject As JObject = GetJson(FabricText)
  356. GotFabric:
  357. '从文件中获取 Mod 信息项
  358. If FabricObject.ContainsKey("name") Then Name = FabricObject("name")
  359. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  360. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  361. If FabricObject.ContainsKey("id") Then ModId = FabricObject("id")
  362. If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "")
  363. Dim AuthorJson As JArray = FabricObject("authors")
  364. If AuthorJson IsNot Nothing Then
  365. Dim Author As New List(Of String)
  366. For Each Token In AuthorJson
  367. Author.Add(Token.ToString)
  368. Next
  369. If Author.Any Then Authors = Join(Author, ", ")
  370. End If
  371. 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  372. ' '添加 Minecraft 依赖
  373. ' Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "")
  374. ' If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  375. ' '添加其他依赖
  376. ' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "")
  377. ' If Deps <> "" Then
  378. ' For Each Dep In Deps.Split(";")
  379. ' If Dep = "" OrElse Not Dep.StartsWithF("required-") Then Continue For
  380. ' Dep = Dep.Substring(Dep.IndexOfF(":") + 1)
  381. ' If Dep.Contains("@") Then
  382. ' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  383. ' Else
  384. ' AddDependency(Dep)
  385. ' End If
  386. ' Next
  387. ' End If
  388. 'End If
  389. '加载成功
  390. GoTo Finished
  391. Catch ex As Exception
  392. Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  393. End Try
  394. #End Region
  395. #Region "尝试使用 mods.toml"
  396. Try
  397. '获取 mods.toml 文件
  398. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  399. Dim TomlText As String = Nothing
  400. If TomlEntry IsNot Nothing Then
  401. TomlText = ReadFile(TomlEntry.Open())
  402. If TomlText.Length < 15 Then TomlText = Nothing
  403. End If
  404. If TomlText Is Nothing Then Exit Try
  405. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  406. Dim Lines As New List(Of String)
  407. For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
  408. If Line.StartsWithF("#") Then '去除注释
  409. Continue For
  410. ElseIf Line.Contains("#") Then
  411. Line = Line.Substring(0, Line.IndexOfF("#"))
  412. End If
  413. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  414. If Line.Any Then Lines.Add(Line) '去除空行
  415. Next
  416. '读取文件数据
  417. 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))}
  418. For i = 0 To Lines.Count - 1
  419. Dim Line As String = Lines(i)
  420. If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
  421. '段落标记
  422. Dim Header = Line.Trim("[]".ToCharArray)
  423. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  424. ElseIf Line.Contains("=") Then
  425. '字段标记
  426. Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  427. Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  428. Dim Value As Object
  429. If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
  430. '单行字符串
  431. Value = RawValue.Trim("""")
  432. ElseIf RawValue.StartsWithF("'''") Then
  433. '多行字符串
  434. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  435. If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  436. ValueLines(0) = ValueLines(0).TrimEnd("'")
  437. Else
  438. Do Until i >= Lines.Count - 1
  439. i += 1
  440. Dim ValueLine As String = Lines(i)
  441. If ValueLine.EndsWithF("'''") Then
  442. ValueLines.Add(ValueLine.TrimEnd("'"))
  443. Exit Do
  444. Else
  445. ValueLines.Add(ValueLine)
  446. End If
  447. Loop
  448. End If
  449. Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
  450. ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then
  451. '布尔型
  452. Value = (RawValue.ToLower = "true")
  453. ElseIf Val(RawValue).ToString = RawValue Then
  454. '数字型
  455. Value = Val(RawValue)
  456. Else
  457. '不知道是个啥玩意儿,直接存储
  458. Value = RawValue
  459. End If
  460. TomlData.Last.Value(Key) = Value
  461. Else
  462. '不知道是个啥玩意儿
  463. Exit Try
  464. End If
  465. Next
  466. '从文件数据中获取信息
  467. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  468. For Each TomlSubData In TomlData
  469. If TomlSubData.Key = "mods" Then
  470. ModEntry = TomlSubData.Value
  471. Exit For
  472. End If
  473. Next
  474. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  475. ModId = ModEntry("modId")
  476. If _ModId Is Nothing Then Exit Try '设置了无效的 ModID
  477. If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName")
  478. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  479. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  480. If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL")
  481. If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors")
  482. For Each TomlSubData In TomlData
  483. If TomlSubData.Key.ToLower = $"dependencies.{ModId.ToLower}" Then
  484. Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value
  485. If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso
  486. DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then
  487. AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing))
  488. End If
  489. End If
  490. Next
  491. '加载成功
  492. GoTo Finished
  493. Catch ex As Exception
  494. Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer)
  495. End Try
  496. #End Region
  497. #Region "尝试使用 fml_cache_annotation.json"
  498. Try
  499. '获取 fml_cache_annotation.json 文件
  500. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  501. Dim FmlText As String = Nothing
  502. If FmlEntry IsNot Nothing Then
  503. FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
  504. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  505. End If
  506. If FmlText Is Nothing Then Exit Try
  507. Dim FmlJson As JObject = GetJson(FmlText)
  508. '获取可用 Json 项
  509. Dim FmlObject As JObject = Nothing
  510. For Each ModFilePair In FmlJson
  511. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  512. If ModFileAnnos IsNot Nothing Then
  513. '先获取 Mod
  514. For Each ModFileAnno In ModFileAnnos
  515. Dim Name As String = If(ModFileAnno("name"), "")
  516. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  517. FmlObject = ModFileAnno("values")
  518. GoTo Got
  519. End If
  520. Next
  521. End If
  522. Next
  523. Exit Try
  524. Got:
  525. '从文件中获取 Mod 信息项
  526. If FmlObject.ContainsKey("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then
  527. '要求使用 mcmod.info 中的信息
  528. Dim value As String = FmlObject("modid")("value")
  529. If value Is Nothing Then Exit Try
  530. value = RegexSeek(value.ToLower, "[0-9a-z_]+")
  531. If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  532. If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
  533. End If
  534. Exit Try
  535. End If
  536. If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value")
  537. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  538. If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value")
  539. If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  540. '添加 Minecraft 依赖
  541. Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "")
  542. If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  543. '添加其他依赖
  544. Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "")
  545. If Deps <> "" Then
  546. For Each Dep In Deps.Split(";")
  547. If Dep = "" OrElse Not Dep.StartsWithF("required-") Then Continue For
  548. Dep = Dep.Substring(Dep.IndexOfF(":") + 1)
  549. If Dep.Contains("@") Then
  550. AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  551. Else
  552. AddDependency(Dep)
  553. End If
  554. Next
  555. End If
  556. End If
  557. Catch ex As Exception
  558. Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  559. End Try
  560. #End Region
  561. Finished:
  562. #Region "将 Version 代号转换为 META-INF 中的版本"
  563. If _Version = "version" Then
  564. Try
  565. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  566. If MetaEntry IsNot Nothing Then
  567. Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
  568. If MetaString.Contains("Implementation-Version:") Then
  569. MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
  570. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  571. Version = MetaString
  572. End If
  573. End If
  574. Catch ex As Exception
  575. Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer)
  576. Version = Nothing
  577. End Try
  578. End If
  579. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  580. #End Region
  581. End Sub
  582. #End Region
  583. #Region "网络信息"
  584. ''' <summary>
  585. ''' 当任何网络信息更新时触发。
  586. ''' </summary>
  587. Public Event OnCompUpdate(sender As McMod)
  588. ''' <summary>
  589. ''' 该 Mod 关联的网络项目。
  590. ''' </summary>
  591. Public Property Comp As CompProject
  592. Get
  593. Return _Comp
  594. End Get
  595. Set(value As CompProject)
  596. _Comp = value
  597. RaiseEvent OnCompUpdate(Me)
  598. End Set
  599. End Property
  600. Private _Comp As CompProject
  601. ''' <summary>
  602. ''' 本地文件对应的联网文件信息。
  603. ''' </summary>
  604. Public CompFile As CompFile
  605. ''' <summary>
  606. ''' 该 Mod 对应的联网最新版本。
  607. ''' </summary>
  608. Public Property UpdateFile As CompFile
  609. Get
  610. Return _UpdateFile
  611. End Get
  612. Set(value As CompFile)
  613. _UpdateFile = value
  614. RaiseEvent OnCompUpdate(Me)
  615. End Set
  616. End Property
  617. Private _UpdateFile As CompFile
  618. ''' <summary>
  619. ''' 该 Mod 的更新日志网址。
  620. ''' </summary>
  621. Public ChangelogUrls As New List(Of String)
  622. ''' <summary>
  623. ''' 所有网络信息是否已成功加载。
  624. ''' </summary>
  625. Public CompLoaded As Boolean = False
  626. ''' <summary>
  627. ''' 将网络信息保存为 Json。
  628. ''' </summary>
  629. Public Function ToJson() As JObject
  630. Dim Json As New JObject
  631. If Comp IsNot Nothing Then Json.Add("Comp", Comp.ToJson())
  632. Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
  633. Json.Add("CompLoaded", CompLoaded)
  634. If CompFile IsNot Nothing Then Json.Add("CompFile", CompFile.ToJson())
  635. If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToJson())
  636. Return Json
  637. End Function
  638. ''' <summary>
  639. ''' 从 Json 中读取网络信息。
  640. ''' </summary>
  641. Public Sub FromJson(Json As JObject)
  642. CompLoaded = Json("CompLoaded")
  643. If Json.ContainsKey("Comp") Then Comp = New CompProject(Json("Comp"))
  644. If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
  645. If Json.ContainsKey("CompFile") Then CompFile = New CompFile(Json("CompFile"), CompType.Mod)
  646. If Json.ContainsKey("UpdateFile") Then UpdateFile = New CompFile(Json("UpdateFile"), CompType.Mod)
  647. End Sub
  648. ''' <summary>
  649. ''' 该文件是否可以更新。
  650. ''' </summary>
  651. Public ReadOnly Property CanUpdate As Boolean
  652. Get
  653. Return ChangelogUrls.Any()
  654. End Get
  655. End Property
  656. ''' <summary>
  657. ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
  658. ''' </summary>
  659. Public ReadOnly Property CurseForgeHash As UInteger
  660. Get
  661. If _CurseForgeHash Is Nothing Then
  662. '读取缓存
  663. Dim Info As New FileInfo(Path)
  664. Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-C")
  665. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  666. If Cached <> "" Then
  667. _CurseForgeHash = Cached
  668. Return _CurseForgeHash
  669. End If
  670. '读取文件
  671. Dim data As New List(Of Byte)
  672. For Each b As Byte In File.ReadAllBytes(Path)
  673. If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
  674. data.Add(b)
  675. Next
  676. '计算 MurmurHash2
  677. Dim length As Integer = data.Count
  678. Dim h As UInteger = 1 Xor length '1 是种子
  679. Dim i As Integer
  680. For i = 0 To length - 4 Step 4
  681. Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
  682. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  683. k = k Xor (k >> 24)
  684. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  685. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  686. h = h Xor k
  687. Next
  688. Select Case length - i
  689. Case 3
  690. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  691. h = h Xor (CUInt(data(i + 2)) << 16)
  692. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  693. Case 2
  694. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  695. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  696. Case 1
  697. h = h Xor data(i)
  698. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  699. End Select
  700. h = h Xor (h >> 13)
  701. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  702. h = h Xor (h >> 15)
  703. _CurseForgeHash = h
  704. '写入缓存
  705. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
  706. End If
  707. Return _CurseForgeHash
  708. End Get
  709. End Property
  710. Private _CurseForgeHash As UInteger?
  711. ''' <summary>
  712. ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
  713. ''' </summary>
  714. Public ReadOnly Property ModrinthHash As String
  715. Get
  716. If _ModrinthHash Is Nothing Then
  717. '读取缓存
  718. Dim Info As New FileInfo(Path)
  719. Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-M")
  720. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  721. If Cached <> "" Then
  722. _ModrinthHash = Cached
  723. Return _ModrinthHash
  724. End If
  725. '计算 SHA1
  726. _ModrinthHash = GetFileSHA1(Path)
  727. '写入缓存
  728. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
  729. End If
  730. Return _ModrinthHash
  731. End Get
  732. End Property
  733. Private _ModrinthHash As String
  734. #End Region
  735. #Region "API"
  736. Public Overrides Function ToString() As String
  737. Return $"{State} - {Path}"
  738. End Function
  739. Public Overrides Function Equals(obj As Object) As Boolean
  740. Dim target = TryCast(obj, McMod)
  741. Return target IsNot Nothing AndAlso Path = target.Path
  742. End Function
  743. #End Region
  744. ''' <summary>
  745. ''' 是否可能为前置 Mod。
  746. ''' </summary>
  747. Public Function IsPresetMod() As Boolean
  748. Return Not Dependencies.Any() AndAlso Name IsNot Nothing AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib"))
  749. End Function
  750. ''' <summary>
  751. ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
  752. ''' </summary>
  753. Public Shared Function IsModFile(Path As String)
  754. If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
  755. Path = Path.ToLower
  756. If Path.EndsWithF(".jar", True) OrElse Path.EndsWithF(".zip", True) OrElse Path.EndsWithF(".litemod", True) OrElse
  757. Path.EndsWithF(".jar.disabled", True) OrElse Path.EndsWithF(".zip.disabled", True) OrElse Path.EndsWithF(".litemod.disabled", True) OrElse
  758. Path.EndsWithF(".jar.old", True) OrElse Path.EndsWithF(".zip.old", True) OrElse Path.EndsWithF(".litemod.old", True) Then Return True
  759. Return False
  760. End Function
  761. End Class
  762. '加载 Mod 列表
  763. Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad)
  764. Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod)))
  765. Try
  766. RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False)
  767. '等待 Mod 更新完成
  768. If PageVersionMod.UpdatingVersions.Contains(Loader.Input) Then
  769. Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表")
  770. Try
  771. RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.Text = "正在更新 Mod")
  772. Do Until Not PageVersionMod.UpdatingVersions.Contains(Loader.Input)
  773. If Loader.IsAborted Then Exit Sub
  774. Thread.Sleep(100)
  775. Loop
  776. Finally
  777. RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.Text = "正在加载 Mod 列表")
  778. End Try
  779. End If
  780. '获取 Mod 文件夹下的可用文件列表
  781. Dim ModFileList As New List(Of FileInfo)
  782. If Directory.Exists(Loader.Input) Then
  783. Dim RawName As String = Loader.Input.ToLower
  784. For Each File As FileInfo In EnumerateFiles(Loader.Input)
  785. If File.DirectoryName.ToLower & "\" <> RawName Then
  786. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  787. If Not (PageVersionLeft.Version IsNot Nothing AndAlso PageVersionLeft.Version.Version.HasForge AndAlso
  788. PageVersionLeft.Version.Version.McCodeMain < 13 AndAlso
  789. File.Directory.Name = "1." & PageVersionLeft.Version.Version.McCodeMain & "." & PageVersionLeft.Version.Version.McCodeSub) Then
  790. Continue For
  791. End If
  792. End If
  793. If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
  794. Next
  795. End If
  796. '确定是否显示进度
  797. Loader.Progress = 0.05
  798. If ModFileList.Count > 50 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True)
  799. '获取本地文件缓存
  800. Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
  801. Dim Cache As New JObject
  802. Try
  803. Dim CacheContent As String = ReadFile(CachePath)
  804. If CacheContent <> "" Then
  805. Cache = GetJson(CacheContent)
  806. If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
  807. Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", LogLevel.Debug)
  808. Cache = New JObject
  809. End If
  810. End If
  811. Catch ex As Exception
  812. Log(ex, "读取本地 Mod 信息缓存失败,已重置")
  813. End Try
  814. Cache("version") = LocalModCacheVersion
  815. '加载 Mod 列表
  816. Dim ModList As New List(Of McMod)
  817. Dim ModUpdateList As New List(Of McMod)
  818. For Each ModFile As FileInfo In ModFileList
  819. Loader.Progress += 0.94 / ModFileList.Count
  820. If Loader.IsAborted Then Exit Sub
  821. '加载 McMod 对象
  822. Dim ModEntry As New McMod(ModFile.FullName)
  823. ModEntry.Load()
  824. Dim DumpMod As McMod = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName)
  825. If DumpMod IsNot Nothing Then
  826. Dim DisabledMod As McMod = If(DumpMod.State = McMod.McModState.Disabled, DumpMod, ModEntry)
  827. Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug)
  828. If DisabledMod Is ModEntry Then
  829. Continue For
  830. Else
  831. ModList.Remove(DisabledMod)
  832. ModUpdateList.Remove(DisabledMod)
  833. End If
  834. End If
  835. ModList.Add(ModEntry)
  836. '读取 Comp 缓存
  837. If ModEntry.State = McMod.McModState.Unavaliable Then Continue For
  838. Dim CacheKey = ModEntry.ModrinthHash & PageVersionLeft.Version.Version.McName & GetTargetModLoaders().Join("")
  839. If Cache.ContainsKey(CacheKey) Then
  840. ModEntry.FromJson(Cache(CacheKey))
  841. '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
  842. If ModEntry.CompLoaded AndAlso Date.Now - Cache(CacheKey)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
  843. End If
  844. ModUpdateList.Add(ModEntry)
  845. Next
  846. Loader.Progress = 0.99
  847. Log($"[Mod] 共有 {ModList.Count} 个 Mod,其中 {ModUpdateList.Where(Function(m) m.Comp Is Nothing).Count} 个需要联网获取信息,{ModUpdateList.Where(Function(m) m.Comp IsNot Nothing).Count} 个需要更新信息")
  848. '排序
  849. ModList = Sort(ModList,
  850. Function(Left As McMod, Right As McMod) As Boolean
  851. If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then
  852. Return Left.State = McMod.McModState.Unavaliable
  853. Else
  854. Return Not Right.FileName.CompareTo(Left.FileName)
  855. End If
  856. End Function)
  857. '回设
  858. If Loader.IsAborted Then Exit Sub
  859. Loader.Output = ModList
  860. '开始联网加载
  861. If ModUpdateList.Any() Then
  862. 'TODO: 添加信息获取中提示
  863. McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModUpdateList, Cache), IsForceRestart:=True)
  864. End If
  865. Catch ex As Exception
  866. Log(ex, "Mod 列表加载失败", LogLevel.Debug)
  867. Throw
  868. End Try
  869. End Sub
  870. '联网加载 Mod 详情
  871. Private Const LocalModCacheVersion As Integer = 5
  872. Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
  873. Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
  874. Dim Mods As List(Of McMod) = Loader.Input.Key
  875. Dim Cache As JObject = Loader.Input.Value
  876. '获取作为检查目标的加载器和版本
  877. Dim TargetMcVersion As McVersionInfo = PageVersionLeft.Version.Version
  878. Dim ModLoaders = GetTargetModLoaders()
  879. Dim McVersion = TargetMcVersion.McName
  880. '暂不向下扩展检查的 MC 小版本
  881. '例如:Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
  882. 'If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
  883. ' McVersions.Add($"1.{TargetMcVersion.McCodeMain}")
  884. ' For i = 1 To TargetMcVersion.McCodeSub
  885. ' McVersions.Add($"1.{TargetMcVersion.McCodeMain}.{i}")
  886. ' Next
  887. 'End If
  888. 'McVersions = McVersions.Distinct().ToList()
  889. '开始网络获取
  890. Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{McVersion}")
  891. Dim EndedThreadCount As Integer = 0, SucceedThreadCount As Integer = 0
  892. Dim MainThread As Thread = Thread.CurrentThread
  893. '从 Modrinth 获取信息
  894. RunInNewThread(
  895. Sub()
  896. Try
  897. '步骤 1:获取 Hash 与对应的工程 ID
  898. Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
  899. Dim ModrinthVersion = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files", "POST",
  900. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")), JObject)
  901. Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
  902. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  903. If ModrinthVersion.Count = 0 Then Exit Sub
  904. Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
  905. For Each Entry In Mods
  906. If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
  907. If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
  908. Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
  909. If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
  910. If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
  911. ModrinthMapping(ProjectId).Add(Entry)
  912. '记录对应的 CompFile
  913. Dim File As New CompFile(ModrinthVersion(Entry.ModrinthHash), CompType.Mod)
  914. If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File
  915. Next
  916. If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
  917. Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
  918. '步骤 3:获取工程信息
  919. If Not ModrinthMapping.Any() Then Exit Sub
  920. Dim ModrinthProject = CType(GetJson(NetRequestRetry(
  921. $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]",
  922. "GET", "", "application/json")), JArray)
  923. For Each ProjectJson In ModrinthProject
  924. Dim Project As New CompProject(ProjectJson)
  925. For Each Entry In ModrinthMapping(Project.Id)
  926. If Entry.Comp IsNot Nothing AndAlso Entry.Comp.FromCurseForge Then Project.LogoUrl = Entry.Comp.LogoUrl 'Modrinth 的部分 Logo 不是图片,如果可能,使用 CurseForge 的 Logo
  927. Entry.Comp = Project
  928. Next
  929. Next
  930. Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
  931. '步骤 4:获取更新信息
  932. Dim ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
  933. $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"",
  934. ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersion}""]}}", "application/json")), JObject)
  935. For Each Entry In Mods
  936. If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
  937. Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod)
  938. If Not UpdateFile.Available Then Continue For
  939. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
  940. If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
  941. '设置更新日志与更新文件
  942. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  943. Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={McVersion}")
  944. UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
  945. Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
  946. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
  947. Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={McVersion}"}
  948. Entry.UpdateFile = UpdateFile
  949. End If
  950. Next
  951. Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
  952. SucceedThreadCount += 1
  953. Catch ex As Exception
  954. Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
  955. Finally
  956. EndedThreadCount += 1
  957. End Try
  958. End Sub, "Mod List Detail Loader Modrinth")
  959. '从 CurseForge 获取信息
  960. RunInNewThread(
  961. Sub()
  962. Try
  963. '步骤 1:获取 Hash 与对应的工程 ID
  964. Dim CurseForgeHashes As New List(Of UInteger)
  965. For Each Entry In Mods
  966. CurseForgeHashes.Add(Entry.CurseForgeHash)
  967. If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
  968. Next
  969. Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
  970. $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
  971. Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
  972. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  973. If Not CurseForgeRaw.Any() Then Exit Sub
  974. Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of McMod))
  975. For Each Project In CurseForgeRaw
  976. Dim ProjectId = Project("id").ToString
  977. Dim Hash As UInteger = Project("file")("fileFingerprint")
  978. For Each Entry In Mods
  979. If Entry.CurseForgeHash <> Hash Then Continue For
  980. If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
  981. If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
  982. CurseForgeMapping(ProjectId).Add(Entry)
  983. '记录对应的 CompFile
  984. Dim File As New CompFile(Project("file"), CompType.Mod)
  985. If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File
  986. Next
  987. Next
  988. If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
  989. Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
  990. '步骤 3:获取工程信息
  991. If Not CurseForgeMapping.Any() Then Exit Sub
  992. Dim CurseForgeProject = CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods", "POST",
  993. $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")), JObject)("data")
  994. Dim UpdateFileIds As New Dictionary(Of Integer, List(Of McMod)) 'FileId -> 本地 Mod 文件列表
  995. Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
  996. For Each ProjectJson In CurseForgeProject
  997. If Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
  998. '设置 Entry 中的工程信息
  999. Dim Project As New CompProject(ProjectJson)
  1000. For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
  1001. If Entry.Comp IsNot Nothing AndAlso Not Entry.Comp.FromCurseForge Then
  1002. Entry.Comp.LogoUrl = Project.LogoUrl 'Modrinth 部分 Logo 加载不出来
  1003. Entry.Comp = Entry.Comp '再次触发修改事件
  1004. Continue For
  1005. End If
  1006. Entry.Comp = Project
  1007. Next
  1008. '查找或许版本更新的文件列表
  1009. If ModLoaders.Count = 1 Then 'TODO: 结果有多个 ModLoader 时提示无法检测更新
  1010. Dim NewestVersion As String = Nothing
  1011. Dim NewestFileIds As New List(Of Integer)
  1012. For Each IndexEntry In ProjectJson("latestFilesIndexes")
  1013. If ModLoaders.Single <> IndexEntry("modLoader")?.ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
  1014. Dim IndexVersion As String = IndexEntry("gameVersion")
  1015. If McVersion <> IndexVersion Then Continue For 'MC 版本匹配
  1016. '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
  1017. If NewestVersion IsNot Nothing AndAlso VersionSortInteger(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
  1018. If NewestVersion <> IndexVersion Then
  1019. NewestVersion = IndexVersion
  1020. NewestFileIds.Clear()
  1021. End If
  1022. NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
  1023. Next
  1024. For Each FileId In NewestFileIds
  1025. If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of McMod)
  1026. UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
  1027. FileIdToProjectSlug(FileId) = Project.Slug
  1028. Next
  1029. End If
  1030. Next
  1031. Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
  1032. '步骤 4:获取更新文件信息
  1033. If Not UpdateFileIds.Any() Then Exit Sub
  1034. Dim CurseForgeFiles = CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST",
  1035. $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")), JObject)("data")
  1036. Dim UpdateFiles As New Dictionary(Of McMod, CompFile)
  1037. For Each FileJson In CurseForgeFiles
  1038. Dim File As New CompFile(FileJson, CompType.Mod)
  1039. If Not File.Available Then Continue For
  1040. For Each Entry As McMod In UpdateFileIds(File.Id)
  1041. If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
  1042. UpdateFiles(Entry) = File
  1043. Next
  1044. Next
  1045. For Each Pair In UpdateFiles
  1046. Dim Entry As McMod = Pair.Key
  1047. Dim UpdateFile As CompFile = Pair.Value
  1048. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
  1049. If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
  1050. '设置更新日志与更新文件
  1051. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  1052. Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
  1053. Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
  1054. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
  1055. Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
  1056. Entry.UpdateFile = UpdateFile
  1057. End If
  1058. Next
  1059. Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
  1060. SucceedThreadCount += 1
  1061. Catch ex As Exception
  1062. Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
  1063. Finally
  1064. EndedThreadCount += 1
  1065. End Try
  1066. End Sub, "Mod List Detail Loader CurseForge")
  1067. '等待线程结束
  1068. Do Until EndedThreadCount = 2
  1069. If Loader.IsAborted Then Exit Sub
  1070. Thread.Sleep(10)
  1071. Loop
  1072. '保存缓存
  1073. Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
  1074. Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
  1075. If Not Mods.Any() Then Exit Sub
  1076. For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
  1077. Entry.CompLoaded = SucceedThreadCount = 2
  1078. Cache(Entry.ModrinthHash & McVersion & ModLoaders.Join("")) = Entry.ToJson()
  1079. Next
  1080. WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
  1081. End Sub
  1082. Private Function GetTargetModLoaders() As List(Of CompModLoaderType)
  1083. Dim ModLoaders As New List(Of CompModLoaderType)
  1084. If PageVersionLeft.Version.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
  1085. If PageVersionLeft.Version.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)
  1086. If PageVersionLeft.Version.Version.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
  1087. If PageVersionLeft.Version.Version.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
  1088. If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.NeoForge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
  1089. Return ModLoaders
  1090. End Function
  1091. #If DEBUG Then
  1092. ''' <summary>
  1093. ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。
  1094. ''' </summary>
  1095. Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String)
  1096. Dim Result As New List(Of String)
  1097. '令所有 Mod 进行基础检查,并归纳需要检查的 Mod
  1098. Dim CurrentModList As New List(Of McMod)
  1099. For Each ModEntity In Mods
  1100. If Not ModEntity.IsFileAvailable Then
  1101. Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path)
  1102. Continue For
  1103. End If
  1104. If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity)
  1105. Next
  1106. '添加默认依赖
  1107. Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path}
  1108. If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"})
  1109. CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"})
  1110. '检查重复的 Mod,并添加对应的依赖
  1111. For Each ModEntity In CurrentModList
  1112. For Each PossibleModId In ModEntity.PossibleModId
  1113. If CurrentDependencies.ContainsKey(PossibleModId) Then
  1114. If CurrentDependencies(PossibleModId)(2) = 1 Then
  1115. Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf &
  1116. " - " & ModEntity.FileName & vbCrLf &
  1117. " - " & CurrentDependencies(PossibleModId)(1))
  1118. Else
  1119. Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf &
  1120. " - " & ModEntity.FileName & vbCrLf &
  1121. " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
  1122. End If
  1123. Else
  1124. CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count})
  1125. End If
  1126. Next
  1127. Next
  1128. '检查依赖
  1129. For Each ModEntity In CurrentModList
  1130. Try
  1131. For Each Dependency In ModEntity.Dependencies
  1132. Dim ReqId As String = Dependency.Key
  1133. If ReqId.Count < 2 Then Continue For '确保正常
  1134. If ReqId = ModEntity.ModId Then Continue For '跳过自体引用
  1135. If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用
  1136. If Dependency.Value IsNot Nothing Then
  1137. '获取分段后的详细版本信息
  1138. Dim ReqVersion As String = Dependency.Value
  1139. Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWithF("[")
  1140. Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWithF("]")
  1141. Dim ReqVersionHead As String
  1142. Dim ReqVersionTail As String
  1143. If ReqVersion.Contains(",") Then
  1144. ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray())
  1145. ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray())
  1146. Else
  1147. ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray())
  1148. ReqVersionTail = ReqVersionHead
  1149. If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then
  1150. ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1)
  1151. ReqVersionTailCanEqual = False
  1152. End If
  1153. End If
  1154. If ReqVersionHead.StartsWithF("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOfF("-") + 1)
  1155. If ReqVersionTail.StartsWithF("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOfF("-") + 1)
  1156. '获取报错描述文本
  1157. Dim VersionRequire As String
  1158. If ReqVersionHead = ReqVersionTail Then
  1159. VersionRequire = "应为 " & ReqVersionHead
  1160. ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then
  1161. VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail
  1162. ElseIf ReqVersionHead.Contains(".") Then
  1163. If ReqVersionHeadCanEqual Then
  1164. VersionRequire = "最低应为 " & ReqVersionHead
  1165. Else
  1166. VersionRequire = "应高于 " & ReqVersionHead
  1167. End If
  1168. ElseIf ReqVersionTail.Contains(".") Then
  1169. If ReqVersionTailCanEqual Then
  1170. VersionRequire = "最高应为 " & ReqVersionHead
  1171. Else
  1172. VersionRequire = "应低于 " & ReqVersionHead
  1173. End If
  1174. Else
  1175. VersionRequire = ""
  1176. End If
  1177. '检查前置 Mod 是否存在,并获取其版本
  1178. If Not CurrentDependencies.ContainsKey(ReqId) Then
  1179. Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName)
  1180. Continue For
  1181. End If
  1182. Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0")
  1183. If CurrentVersion.StartsWithF("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOfF("-") + 1)
  1184. '对比前置 Mod 头部版本
  1185. If ReqVersionHead.Contains(".") Then
  1186. If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then
  1187. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  1188. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  1189. Continue For
  1190. End If
  1191. End If
  1192. '对比前置 Mod 尾部版本
  1193. If ReqVersionTail.Contains(".") Then
  1194. If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then
  1195. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  1196. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  1197. Continue For
  1198. End If
  1199. End If
  1200. Else
  1201. If Not CurrentDependencies.ContainsKey(Dependency.Key) Then
  1202. Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName)
  1203. Continue For
  1204. End If
  1205. End If
  1206. Next
  1207. Catch ex As Exception
  1208. Result.Add("检查 Mod 时出错:" & GetExceptionSummary(ex) & vbCrLf & " - " & ModEntity.FileName)
  1209. Log(ex, "检查 Mod 时出错")
  1210. End Try
  1211. Next
  1212. If Not Result.Any() Then
  1213. Log("[Minecraft] Mod 检查未发现异常")
  1214. Else
  1215. Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf))
  1216. End If
  1217. Return Result
  1218. End Function
  1219. #End If
  1220. End Module