ModMod.vb 67 KB

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