ModMod.vb 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  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.EndsWith(".disabled") OrElse Path.EndsWith(".old") 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
  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 (_Version.Contains(".") OrElse _Version.Contains("-")) Then Exit Property
  105. If value IsNot Nothing AndAlso value.ToLower.Contains("version") Then value = "version" '需要修改的标识
  106. _Version = value
  107. End Set
  108. End Property
  109. Private _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 Exit Property
  120. value = RegexSeek(value.ToLower, "[0-9a-z_-]+")
  121. If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  122. If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
  123. If _ModId Is Nothing Then _ModId = value
  124. End If
  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.StartsWith("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.StartsWith("[")) AndAlso (Not VersionRequirement.StartsWith("(")) AndAlso (Not VersionRequirement.EndsWith("]")) AndAlso (Not VersionRequirement.EndsWith(")")) 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. IsInfoWithoutClassAvailable = False
  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. '阶段 1:基础可用性检查、打开 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. If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空")
  267. '阶段 2:信息获取
  268. LoadWithoutClass(Jar)
  269. '阶段 3: Class 信息获取
  270. If Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar)
  271. Catch ex As UnauthorizedAccessException
  272. Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
  273. _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex)
  274. Catch ex As Exception
  275. Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
  276. _FileUnavailableReason = ex
  277. Finally
  278. If Jar IsNot Nothing Then Jar.Dispose()
  279. End Try
  280. '完成标记
  281. IsLoaded = True
  282. End Sub
  283. ''' <summary>
  284. ''' 进行不使用 .class 文件的信息获取。
  285. ''' </summary>
  286. Private Sub LoadWithoutClass(Jar As ZipArchive)
  287. #Region "尝试使用 mcmod.info"
  288. Try
  289. '获取信息文件
  290. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  291. Dim InfoString As String = Nothing
  292. If InfoEntry IsNot Nothing Then
  293. InfoString = ReadFile(InfoEntry.Open())
  294. If InfoString.Length < 15 Then InfoString = Nothing
  295. End If
  296. If InfoString Is Nothing Then Exit Try
  297. '获取可用 Json 项
  298. Dim InfoObject As JObject
  299. Dim JsonObject = GetJson(InfoString)
  300. If JsonObject.Type = JTokenType.Array Then
  301. InfoObject = JsonObject(0)
  302. Else
  303. InfoObject = JsonObject("modList")(0)
  304. End If
  305. '从文件中获取 Mod 信息项
  306. Name = InfoObject("name")
  307. Description = InfoObject("description")
  308. Version = InfoObject("version")
  309. Url = InfoObject("url")
  310. ModId = InfoObject("modid")
  311. Dim AuthorJson As JArray = InfoObject("authorList")
  312. If AuthorJson IsNot Nothing Then
  313. Dim Author As New List(Of String)
  314. For Each Token In AuthorJson
  315. Author.Add(Token.ToString)
  316. Next
  317. If Author.Count > 0 Then Authors = Join(Author, ", ")
  318. End If
  319. Dim Reqs As JArray = InfoObject("requiredMods")
  320. If Reqs IsNot Nothing Then
  321. For Each Token As String In Reqs
  322. If Not String.IsNullOrEmpty(Token) Then
  323. Token = Token.Substring(Token.IndexOf(":") + 1)
  324. If Token.Contains("@") Then
  325. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  326. Else
  327. AddDependency(Token)
  328. End If
  329. End If
  330. Next
  331. End If
  332. Reqs = InfoObject("dependancies")
  333. If Reqs IsNot Nothing Then
  334. For Each Token As String In Reqs
  335. If Not String.IsNullOrEmpty(Token) Then
  336. Token = Token.Substring(Token.IndexOf(":") + 1)
  337. If Token.Contains("@") Then
  338. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  339. Else
  340. AddDependency(Token)
  341. End If
  342. End If
  343. Next
  344. End If
  345. Catch ex As Exception
  346. Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer)
  347. End Try
  348. #End Region
  349. #Region "尝试使用 fabric.mod.json"
  350. Try
  351. '获取 fabric.mod.json 文件
  352. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  353. Dim FabricText As String = Nothing
  354. If FabricEntry IsNot Nothing Then
  355. FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
  356. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  357. End If
  358. If FabricText Is Nothing Then Exit Try
  359. Dim FabricObject As JObject = GetJson(FabricText)
  360. GotFabric:
  361. '从文件中获取 Mod 信息项
  362. If FabricObject.ContainsKey("name") Then Name = FabricObject("name")
  363. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  364. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  365. If FabricObject.ContainsKey("id") Then ModId = FabricObject("id")
  366. If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "")
  367. Dim AuthorJson As JArray = FabricObject("authors")
  368. If AuthorJson IsNot Nothing Then
  369. Dim Author As New List(Of String)
  370. For Each Token In AuthorJson
  371. Author.Add(Token.ToString)
  372. Next
  373. If Author.Count > 0 Then Authors = Join(Author, ", ")
  374. End If
  375. 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  376. ' '添加 Minecraft 依赖
  377. ' Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "")
  378. ' If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  379. ' '添加其他依赖
  380. ' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "")
  381. ' If Deps <> "" Then
  382. ' For Each Dep In Deps.Split(";")
  383. ' If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
  384. ' Dep = Dep.Substring(Dep.IndexOf(":") + 1)
  385. ' If Dep.Contains("@") Then
  386. ' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  387. ' Else
  388. ' AddDependency(Dep)
  389. ' End If
  390. ' Next
  391. ' End If
  392. 'End If
  393. '加载成功
  394. GoTo Finished
  395. Catch ex As Exception
  396. Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  397. End Try
  398. #End Region
  399. #Region "尝试使用 mods.toml"
  400. Try
  401. '获取 mods.toml 文件
  402. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  403. Dim TomlText As String = Nothing
  404. If TomlEntry IsNot Nothing Then
  405. TomlText = ReadFile(TomlEntry.Open())
  406. If TomlText.Length < 15 Then TomlText = Nothing
  407. End If
  408. If TomlText Is Nothing Then Exit Try
  409. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  410. Dim Lines As New List(Of String)
  411. For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
  412. If Line.StartsWith("#") Then '去除注释
  413. Continue For
  414. ElseIf Line.Contains("#") Then
  415. Line = Line.Substring(0, Line.IndexOf("#"))
  416. End If
  417. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  418. If Line.Count > 0 Then Lines.Add(Line) '去除空行
  419. Next
  420. '读取文件数据
  421. 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))}
  422. For i = 0 To Lines.Count - 1
  423. Dim Line As String = Lines(i)
  424. If Line.StartsWith("[") AndAlso Line.EndsWith("]") Then
  425. '段落标记
  426. Dim Header = Line.Trim("[]".ToCharArray)
  427. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  428. ElseIf Line.Contains("=") Then
  429. '字段标记
  430. Dim Key As String = Line.Substring(0, Line.IndexOf("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  431. Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  432. Dim Value As Object
  433. If RawValue.StartsWith("""") AndAlso RawValue.EndsWith("""") Then
  434. '单行字符串
  435. Value = RawValue.Trim("""")
  436. ElseIf RawValue.StartsWith("'''") Then
  437. '多行字符串
  438. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  439. If ValueLines(0).EndsWith("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  440. ValueLines(0) = ValueLines(0).TrimEnd("'")
  441. Else
  442. Do Until i >= Lines.Count - 1
  443. i += 1
  444. Dim ValueLine As String = Lines(i)
  445. If ValueLine.EndsWith("'''") Then
  446. ValueLines.Add(ValueLine.TrimEnd("'"))
  447. Exit Do
  448. Else
  449. ValueLines.Add(ValueLine)
  450. End If
  451. Loop
  452. End If
  453. Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
  454. ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then
  455. '布尔型
  456. Value = (RawValue.ToLower = "true")
  457. ElseIf Val(RawValue).ToString = RawValue Then
  458. '数字型
  459. Value = Val(RawValue)
  460. Else
  461. '不知道是个啥玩意儿,直接存储
  462. Value = RawValue
  463. End If
  464. TomlData.Last.Value(Key) = Value
  465. Else
  466. '不知道是个啥玩意儿
  467. Exit Try
  468. End If
  469. Next
  470. '从文件数据中获取信息
  471. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  472. For Each TomlSubData In TomlData
  473. If TomlSubData.Key = "mods" Then
  474. ModEntry = TomlSubData.Value
  475. Exit For
  476. End If
  477. Next
  478. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  479. ModId = ModEntry("modId")
  480. If _ModId Is Nothing Then Exit Try '设置了无效的 ModID
  481. If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName")
  482. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  483. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  484. If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL")
  485. If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors")
  486. For Each TomlSubData In TomlData
  487. If TomlSubData.Key.ToLower = $"dependencies.{ModId.ToLower}" Then
  488. Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value
  489. If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso
  490. DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then
  491. AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing))
  492. End If
  493. End If
  494. Next
  495. '加载成功
  496. GoTo Finished
  497. Catch ex As Exception
  498. Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer)
  499. End Try
  500. #End Region
  501. #Region "尝试使用 fml_cache_annotation.json"
  502. Try
  503. '获取 fml_cache_annotation.json 文件
  504. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  505. Dim FmlText As String = Nothing
  506. If FmlEntry IsNot Nothing Then
  507. FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
  508. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  509. End If
  510. If FmlText Is Nothing Then Exit Try
  511. Dim FmlJson As JObject = GetJson(FmlText)
  512. '获取可用 Json 项
  513. Dim FmlObject As JObject = Nothing
  514. For Each ModFilePair In FmlJson
  515. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  516. If ModFileAnnos IsNot Nothing Then
  517. '先获取 Mod
  518. For Each ModFileAnno In ModFileAnnos
  519. Dim Name As String = If(ModFileAnno("name"), "")
  520. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  521. FmlObject = ModFileAnno("values")
  522. GoTo Got
  523. End If
  524. Next
  525. End If
  526. Next
  527. Exit Try
  528. Got:
  529. '从文件中获取 Mod 信息项
  530. If FmlObject.ContainsKey("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then
  531. '要求使用 mcmod.info 中的信息
  532. Dim value As String = FmlObject("modid")("value")
  533. If value Is Nothing Then Exit Try
  534. value = RegexSeek(value.ToLower, "[0-9a-z_]+")
  535. If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  536. If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
  537. End If
  538. Exit Try
  539. End If
  540. If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value")
  541. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  542. If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value")
  543. If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  544. '添加 Minecraft 依赖
  545. Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "")
  546. If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  547. '添加其他依赖
  548. Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "")
  549. If Deps <> "" Then
  550. For Each Dep In Deps.Split(";")
  551. If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
  552. Dep = Dep.Substring(Dep.IndexOf(":") + 1)
  553. If Dep.Contains("@") Then
  554. AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  555. Else
  556. AddDependency(Dep)
  557. End If
  558. Next
  559. End If
  560. End If
  561. Catch ex As Exception
  562. Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  563. End Try
  564. #End Region
  565. Finished:
  566. #Region "将 Version 代号转换为 META-INF 中的版本"
  567. If _Version = "version" Then
  568. Try
  569. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  570. If MetaEntry IsNot Nothing Then
  571. Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
  572. If MetaString.Contains("Implementation-Version:") Then
  573. MetaString = MetaString.Substring(MetaString.IndexOf("Implementation-Version:") + "Implementation-Version:".Count)
  574. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  575. Version = MetaString
  576. End If
  577. End If
  578. Catch ex As Exception
  579. Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer)
  580. Version = Nothing
  581. End Try
  582. End If
  583. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  584. #End Region
  585. IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
  586. End Sub
  587. ''' <summary>
  588. ''' 进行使用 .class 文件的信息获取。
  589. ''' </summary>
  590. Private Sub LoadWithClass(Jar As ZipArchive)
  591. Try
  592. '查找入口点文件
  593. Dim ModClass As String = Nothing
  594. Dim ModClassNotBest As String = Nothing '非完美匹配
  595. For Each Entry In Jar.Entries
  596. If Entry.Name.EndsWith(".class") Then
  597. Dim Temp As String = ReadFile(Entry.Open()).ToLower
  598. If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then
  599. ModClass = Temp
  600. Exit For
  601. ElseIf Temp.Contains("modid") Then
  602. ModClassNotBest = Temp
  603. End If
  604. End If
  605. Next
  606. If ModClass Is Nothing Then ModClass = ModClassNotBest
  607. If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点")
  608. ModClass = ModClass.Replace("ljava/lang/string;", "")
  609. If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte
  610. Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer
  611. '获取 ModID
  612. If _ModId Is Nothing Then
  613. IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1
  614. IncreaseCount = 0
  615. Do While Convert.ToInt32(ModClass(IndexHead)) < 32
  616. IndexHead += 1
  617. IncreaseCount += 1
  618. If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败")
  619. Loop
  620. IndexTail = IndexHead + 1
  621. IncreaseCount = 0
  622. Do Until Convert.ToInt32(ModClass(IndexTail)) < 32
  623. IndexTail += 1
  624. IncreaseCount += 1
  625. If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败")
  626. Loop
  627. ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead)
  628. End If
  629. '获取 Version
  630. If _Version Is Nothing AndAlso ModClass.Contains("version") Then
  631. IndexHead = ModClass.IndexOf("version") + "version".Length + 1
  632. IncreaseCount = 0
  633. Do While Convert.ToInt32(ModClass(IndexHead)) < 32
  634. IndexHead += 1
  635. IncreaseCount += 1
  636. If IncreaseCount > 10 Then GoTo VersionFindFail
  637. Loop
  638. IndexTail = IndexHead + 1
  639. IncreaseCount = 0
  640. Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse
  641. (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse
  642. (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c)
  643. IndexTail += 1
  644. IncreaseCount += 1
  645. If IncreaseCount > 50 Then GoTo VersionFindFail
  646. Loop
  647. _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead)
  648. End If
  649. VersionFindFail:
  650. '获取 Dependencies
  651. IndexHead = ModClass.IndexOf("dependencies")
  652. If IndexHead > 0 Then
  653. If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299)
  654. Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?")
  655. For Each Token As String In Deps
  656. If Not String.IsNullOrEmpty(Token) Then
  657. If Token.Contains("@") Then
  658. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  659. Else
  660. AddDependency(Token)
  661. End If
  662. End If
  663. Next
  664. End If
  665. Catch ex As Exception
  666. Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal)
  667. End Try
  668. IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
  669. End Sub
  670. #End Region
  671. #Region "网络信息"
  672. ''' <summary>
  673. ''' 该 Mod 关联的网络项目。
  674. ''' </summary>
  675. Public Property Comp As CompProject
  676. Get
  677. Return _Comp
  678. End Get
  679. Set(value As CompProject)
  680. _Comp = value
  681. RaiseEvent OnGetCompProject(Me)
  682. End Set
  683. End Property
  684. Private _Comp As CompProject
  685. Public IsCompFromModrinth = False
  686. Public Event OnGetCompProject(sender As McMod)
  687. ''' <summary>
  688. ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
  689. ''' </summary>
  690. Public ReadOnly Property CurseForgeHash As UInteger
  691. Get
  692. If _CurseForgeHash Is Nothing Then
  693. '读取文件
  694. Dim data As New List(Of Byte)
  695. For Each b As Byte In File.ReadAllBytes(Path)
  696. If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
  697. data.Add(b)
  698. Next
  699. '计算 MurmurHash2
  700. Dim length As Integer = data.Count
  701. Dim h As UInteger = 1 Xor length '1 是种子
  702. Dim i As Integer
  703. For i = 0 To length - 4 Step 4
  704. Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
  705. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  706. k = k Xor (k >> 24)
  707. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  708. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  709. h = h Xor k
  710. Next
  711. Select Case length - i
  712. Case 3
  713. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  714. h = h Xor (CUInt(data(i + 2)) << 16)
  715. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  716. Case 2
  717. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  718. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  719. Case 1
  720. h = h Xor data(i)
  721. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  722. End Select
  723. h = h Xor (h >> 13)
  724. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  725. h = h Xor (h >> 15)
  726. _CurseForgeHash = h
  727. End If
  728. Return _CurseForgeHash
  729. End Get
  730. End Property
  731. Private _CurseForgeHash As UInteger?
  732. ''' <summary>
  733. ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
  734. ''' </summary>
  735. Public ReadOnly Property ModrinthHash As String
  736. Get
  737. If _ModrinthHash Is Nothing Then
  738. _ModrinthHash = GetFileSHA1(Path)
  739. End If
  740. Return _ModrinthHash
  741. End Get
  742. End Property
  743. Private _ModrinthHash As String
  744. #End Region
  745. #Region "API"
  746. Public Overrides Function ToString() As String
  747. Return $"{State} - {Path}"
  748. End Function
  749. Public Overrides Function Equals(obj As Object) As Boolean
  750. Dim target = TryCast(obj, McMod)
  751. Return target IsNot Nothing AndAlso Path = target.Path
  752. End Function
  753. #End Region
  754. ''' <summary>
  755. ''' 是否可能为前置 Mod。
  756. ''' </summary>
  757. Public Function IsPresetMod() As Boolean
  758. Return Dependencies.Count = 0 AndAlso Name IsNot Nothing AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib"))
  759. End Function
  760. ''' <summary>
  761. ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
  762. ''' </summary>
  763. Public Shared Function IsModFile(Path As String)
  764. If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
  765. Path = Path.ToLower
  766. If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse
  767. Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") OrElse
  768. Path.EndsWith(".jar.old") OrElse Path.EndsWith(".zip.old") OrElse Path.EndsWith(".litemod.old") Then Return True
  769. Return False
  770. End Function
  771. End Class
  772. '加载 Mod 列表
  773. Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad)
  774. Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod)))
  775. Try
  776. RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False)
  777. '获取 Mod 文件夹下的可用文件列表
  778. Dim ModFileList As New List(Of FileInfo)
  779. If Directory.Exists(Loader.Input) Then
  780. Dim RawName As String = Loader.Input.ToLower
  781. For Each File As FileInfo In EnumerateFiles(Loader.Input)
  782. If File.DirectoryName.ToLower & "\" <> RawName Then
  783. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  784. If Not (PageVersionLeft.Version IsNot Nothing AndAlso PageVersionLeft.Version.Version.HasForge AndAlso
  785. PageVersionLeft.Version.Version.McCodeMain < 13 AndAlso
  786. File.Directory.Name = "1." & PageVersionLeft.Version.Version.McCodeMain & "." & PageVersionLeft.Version.Version.McCodeSub) Then
  787. Continue For
  788. End If
  789. End If
  790. If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
  791. Next
  792. End If
  793. '确定是否显示进度
  794. Loader.Progress = 0.05
  795. If ModFileList.Count > 50 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True)
  796. '获取本地文件缓存
  797. Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
  798. Dim Cache As New JObject
  799. Try
  800. Dim CacheContent As String = ReadFile(CachePath)
  801. If CacheContent <> "" Then
  802. Cache = GetJson(CacheContent)
  803. If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
  804. Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息")
  805. Cache = New JObject
  806. End If
  807. End If
  808. Catch ex As Exception
  809. Log(ex, "读取本地 Mod 信息缓存失败,已重置")
  810. End Try
  811. Cache("version") = LocalModCacheVersion
  812. '加载 Mod 列表
  813. Dim ModList As New List(Of McMod)
  814. Dim ModUpdateList As New List(Of McMod)
  815. For Each ModFile As FileInfo In ModFileList
  816. Loader.Progress += 0.95 / ModFileList.Count
  817. If Loader.IsAborted Then Exit Sub
  818. '加载 McMod 对象
  819. Dim ModEntry As New McMod(ModFile.FullName)
  820. ModEntry.Load()
  821. Dim DumpMod As McMod = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName)
  822. If DumpMod IsNot Nothing Then
  823. Dim DisabledMod As McMod = If(DumpMod.State = McMod.McModState.Disabled, DumpMod, ModEntry)
  824. Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug)
  825. If DisabledMod Is ModEntry Then
  826. Continue For
  827. Else
  828. ModList.Remove(DisabledMod)
  829. ModUpdateList.Remove(DisabledMod)
  830. End If
  831. End If
  832. ModList.Add(ModEntry)
  833. '读取 Comp 缓存
  834. If ModEntry.State = McMod.McModState.Unavaliable Then Continue For
  835. Dim Hash = ModEntry.ModrinthHash
  836. If Cache.ContainsKey(Hash) Then
  837. ModEntry.Comp = New CompProject(Cache(Hash))
  838. ModEntry.IsCompFromModrinth = Not ModEntry.Comp.FromCurseForge
  839. '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
  840. If Date.Now - Cache(Hash)("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
  841. End If
  842. ModUpdateList.Add(ModEntry)
  843. Next
  844. Loader.Progress = 0.99
  845. Log($"[Mod] 共有 {ModList.Count} 个 Mod,其中 {ModUpdateList.Where(Function(m) m.Comp Is Nothing).Count} 个需要联网获取信息,{ModUpdateList.Where(Function(m) m.Comp IsNot Nothing).Count} 个需要更新信息")
  846. '排序
  847. ModList = Sort(ModList,
  848. Function(Left As McMod, Right As McMod) As Boolean
  849. If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then
  850. Return Left.State = McMod.McModState.Unavaliable
  851. Else
  852. Return Not Right.FileName.CompareTo(Left.FileName)
  853. End If
  854. End Function)
  855. '回设
  856. If Loader.IsAborted Then Exit Sub
  857. Loader.Output = ModList
  858. '开始联网加载
  859. If ModUpdateList.Any() Then
  860. McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModUpdateList, Cache), IsForceRestart:=True)
  861. End If
  862. Loader.Progress = 1
  863. Catch ex As Exception
  864. Log(ex, "Mod 列表加载失败", LogLevel.Debug)
  865. Throw
  866. End Try
  867. End Sub
  868. '联网加载 Mod 详情
  869. Private Const LocalModCacheVersion As Integer = 1
  870. Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
  871. Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
  872. Dim Mods As List(Of McMod) = Loader.Input.Key
  873. Dim Cache As JObject = Loader.Input.Value
  874. '获取作为检查目标的加载器和版本
  875. Dim TargetMcVersion As McVersionInfo = PageVersionLeft.Version.Version
  876. Dim ModLoaders As New List(Of String)
  877. If TargetMcVersion.HasForge Then ModLoaders.Add("forge")
  878. If TargetMcVersion.HasFabric Then ModLoaders.Add("fabric")
  879. If TargetMcVersion.HasLiteLoader Then ModLoaders.Add("liteloader")
  880. If Not ModLoaders.Any() Then ModLoaders.AddRange({"forge", "fabric", "liteloader"})
  881. Dim McVersions As New List(Of String) From {TargetMcVersion.McName}
  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("/")},版本:{McVersions.Join("/")}")
  891. Dim CompletedThread 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 ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
  900. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1"",
  901. ""loaders"": [""{ModLoaders.Join(""",""")}""],""game_versions"": [""{McVersions.Join(""",""")}""]}}", "application/json")), JObject)
  902. Log($"[Mod] 从 Modrinth 获取到 {ModrinthUpdate.Count} 个本地 Mod 的对应信息")
  903. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  904. If ModrinthUpdate.Count = 0 Then Exit Sub
  905. Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
  906. For Each ModEntity In Mods
  907. If Not ModrinthUpdate.ContainsKey(ModEntity.ModrinthHash) Then Continue For
  908. Dim ProjectId = ModrinthUpdate(ModEntity.ModrinthHash)("project_id").ToString
  909. If CompProjectCache.ContainsKey(ProjectId) Then
  910. ModEntity.Comp = CompProjectCache(ProjectId)
  911. Else
  912. If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
  913. ModrinthMapping(ProjectId).Add(ModEntity)
  914. End If
  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 Not Entry.IsCompFromModrinth Then
  927. Project.LogoUrl = Entry.Comp.LogoUrl 'Modrinth 的部分 Logo 加载不出来
  928. End If
  929. Entry.IsCompFromModrinth = True
  930. Entry.Comp = Project
  931. Next
  932. Next
  933. Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
  934. Catch ex As Exception
  935. Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
  936. Finally
  937. CompletedThread += 1
  938. End Try
  939. End Sub, "Mod List Detail Loader Modrinth")
  940. '从 CurseForge 获取信息
  941. RunInNewThread(
  942. Sub()
  943. Try
  944. '步骤 1:获取 Hash 与对应的工程 ID
  945. Dim CurseForgeHashes As New List(Of UInteger)
  946. For Each ModEntity In Mods
  947. CurseForgeHashes.Add(ModEntity.CurseForgeHash)
  948. If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
  949. Next
  950. Dim CurseForgeRaw = CType(CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/fingerprints/432/", "POST",
  951. $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")), JObject)("data")("exactMatches"), JContainer)
  952. Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
  953. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  954. If CurseForgeRaw.Count = 0 Then Exit Sub
  955. Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of McMod))
  956. For Each Project In CurseForgeRaw
  957. Dim ProjectId = Project("id").ToString
  958. Dim Hash As UInteger = Project("file")("fileFingerprint")
  959. For Each Entry In Mods
  960. If Entry.CurseForgeHash <> Hash Then Continue For
  961. If CompProjectCache.ContainsKey(ProjectId) Then
  962. Entry.Comp = CompProjectCache(ProjectId)
  963. Else
  964. If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
  965. CurseForgeMapping(ProjectId).Add(Entry)
  966. End If
  967. Next
  968. Next
  969. If Loader.IsAbortedWithThread(MainThread) Then Exit Sub
  970. Log($"[Mod] 读取缓存后还需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
  971. '步骤 3:获取工程信息
  972. If Not CurseForgeMapping.Any() Then Exit Sub
  973. Dim CurseForgeProject = CType(GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods", "POST",
  974. $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")), JObject)("data")
  975. For Each ProjectJson In CurseForgeProject
  976. If Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
  977. Dim Project As New CompProject(ProjectJson)
  978. For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
  979. If Entry.Comp IsNot Nothing AndAlso Entry.IsCompFromModrinth Then
  980. Entry.Comp.LogoUrl = Project.LogoUrl 'Modrinth 部分 Logo 加载不出来
  981. Entry.Comp = Entry.Comp '再次触发修改事件
  982. Continue For
  983. End If
  984. Entry.IsCompFromModrinth = False
  985. Entry.Comp = Project
  986. Next
  987. Next
  988. Log($"[Mod] 从 CurseForge 获取本地 Mod 信息结束")
  989. Catch ex As Exception
  990. Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
  991. Finally
  992. CompletedThread += 1
  993. End Try
  994. End Sub, "Mod List Detail Loader CurseForge")
  995. '等待线程结束
  996. Do Until CompletedThread = 2
  997. If Loader.IsAborted Then Exit Sub
  998. Thread.Sleep(10)
  999. Loop
  1000. '保存缓存
  1001. Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
  1002. Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
  1003. If Not Mods.Any() Then Exit Sub
  1004. For Each ModEntity In Mods
  1005. Cache(ModEntity.ModrinthHash) = ModEntity.Comp.ToJson()
  1006. Next
  1007. WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
  1008. End Sub
  1009. #If DEBUG Then
  1010. ''' <summary>
  1011. ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。
  1012. ''' </summary>
  1013. Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String)
  1014. Dim Result As New List(Of String)
  1015. '令所有 Mod 进行基础检查,并归纳需要检查的 Mod
  1016. Dim CurrentModList As New List(Of McMod)
  1017. For Each ModEntity In Mods
  1018. If Not ModEntity.IsFileAvailable Then
  1019. Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path)
  1020. Continue For
  1021. End If
  1022. If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity)
  1023. Next
  1024. '添加默认依赖
  1025. Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path}
  1026. If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"})
  1027. CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"})
  1028. '检查重复的 Mod,并添加对应的依赖
  1029. For Each ModEntity In CurrentModList
  1030. For Each PossibleModId In ModEntity.PossibleModId
  1031. If CurrentDependencies.ContainsKey(PossibleModId) Then
  1032. If CurrentDependencies(PossibleModId)(2) = 1 Then
  1033. Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf &
  1034. " - " & ModEntity.FileName & vbCrLf &
  1035. " - " & CurrentDependencies(PossibleModId)(1))
  1036. Else
  1037. Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf &
  1038. " - " & ModEntity.FileName & vbCrLf &
  1039. " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
  1040. End If
  1041. Else
  1042. CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count})
  1043. End If
  1044. Next
  1045. Next
  1046. '检查依赖
  1047. For Each ModEntity In CurrentModList
  1048. Try
  1049. For Each Dependency In ModEntity.Dependencies
  1050. Dim ReqId As String = Dependency.Key
  1051. If ReqId.Count < 2 Then Continue For '确保正常
  1052. If ReqId = ModEntity.ModId Then Continue For '跳过自体引用
  1053. If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用
  1054. If Dependency.Value IsNot Nothing Then
  1055. '获取分段后的详细版本信息
  1056. Dim ReqVersion As String = Dependency.Value
  1057. Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[")
  1058. Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]")
  1059. Dim ReqVersionHead As String
  1060. Dim ReqVersionTail As String
  1061. If ReqVersion.Contains(",") Then
  1062. ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray())
  1063. ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray())
  1064. Else
  1065. ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray())
  1066. ReqVersionTail = ReqVersionHead
  1067. If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then
  1068. ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1)
  1069. ReqVersionTailCanEqual = False
  1070. End If
  1071. End If
  1072. If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1)
  1073. If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1)
  1074. '获取报错描述文本
  1075. Dim VersionRequire As String
  1076. If ReqVersionHead = ReqVersionTail Then
  1077. VersionRequire = "应为 " & ReqVersionHead
  1078. ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then
  1079. VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail
  1080. ElseIf ReqVersionHead.Contains(".") Then
  1081. If ReqVersionHeadCanEqual Then
  1082. VersionRequire = "最低应为 " & ReqVersionHead
  1083. Else
  1084. VersionRequire = "应高于 " & ReqVersionHead
  1085. End If
  1086. ElseIf ReqVersionTail.Contains(".") Then
  1087. If ReqVersionTailCanEqual Then
  1088. VersionRequire = "最高应为 " & ReqVersionHead
  1089. Else
  1090. VersionRequire = "应低于 " & ReqVersionHead
  1091. End If
  1092. Else
  1093. VersionRequire = ""
  1094. End If
  1095. '检查前置 Mod 是否存在,并获取其版本
  1096. If Not CurrentDependencies.ContainsKey(ReqId) Then
  1097. Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName)
  1098. Continue For
  1099. End If
  1100. Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0")
  1101. If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1)
  1102. '对比前置 Mod 头部版本
  1103. If ReqVersionHead.Contains(".") Then
  1104. If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then
  1105. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  1106. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  1107. Continue For
  1108. End If
  1109. End If
  1110. '对比前置 Mod 尾部版本
  1111. If ReqVersionTail.Contains(".") Then
  1112. If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then
  1113. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  1114. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  1115. Continue For
  1116. End If
  1117. End If
  1118. Else
  1119. If Not CurrentDependencies.ContainsKey(Dependency.Key) Then
  1120. Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName)
  1121. Continue For
  1122. End If
  1123. End If
  1124. Next
  1125. Catch ex As Exception
  1126. Result.Add("检查 Mod 时出错:" & GetExceptionSummary(ex) & vbCrLf & " - " & ModEntity.FileName)
  1127. Log(ex, "检查 Mod 时出错")
  1128. End Try
  1129. Next
  1130. If Result.Count = 0 Then
  1131. Log("[Minecraft] Mod 检查未发现异常")
  1132. Else
  1133. Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf))
  1134. End If
  1135. Return Result
  1136. End Function
  1137. #End If
  1138. End Module