ModMod.vb 59 KB

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