ModMod.vb 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  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。
  22. ''' </summary>
  23. Public ReadOnly Property RawFileName As String
  24. Get
  25. Return FileName.Replace(".disabled", "")
  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") 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. '阶段 1:基础可用性检查
  253. Dim Jar As ZipArchive
  254. Try
  255. '打开 Jar 文件
  256. If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")")
  257. If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")")
  258. Jar = New ZipArchive(New FileStream(Path, FileMode.Open))
  259. If Jar.Entries.Count = 0 Then Throw New FileFormatException("文件内容为空")
  260. Catch ex As UnauthorizedAccessException
  261. Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
  262. _FileUnavailableReason = New UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex)
  263. Jar = Nothing
  264. Catch ex As Exception
  265. Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
  266. _FileUnavailableReason = ex
  267. Jar = Nothing
  268. End Try
  269. '阶段 2:信息获取
  270. If Jar IsNot Nothing Then LoadWithoutClass(Jar)
  271. '阶段 3: Class 信息获取
  272. If Jar IsNot Nothing AndAlso Not IsInfoWithoutClassAvailable Then LoadWithClass(Jar)
  273. '释放文件
  274. Try
  275. If Jar IsNot Nothing Then Jar.Dispose()
  276. Catch
  277. End Try
  278. '完成标记
  279. IsLoaded = True
  280. End Sub
  281. ''' <summary>
  282. ''' 进行不使用 .class 文件的信息获取。
  283. ''' </summary>
  284. Private Sub LoadWithoutClass(Jar As ZipArchive)
  285. #Region "尝试使用 mcmod.info"
  286. Try
  287. '获取信息文件
  288. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  289. Dim InfoString As String = Nothing
  290. If InfoEntry IsNot Nothing Then
  291. InfoString = ReadFile(InfoEntry.Open())
  292. If InfoString.Length < 15 Then InfoString = Nothing
  293. End If
  294. If InfoString Is Nothing Then Exit Try
  295. '获取可用 Json 项
  296. Dim InfoObject As JObject
  297. Dim JsonObject = GetJson(InfoString)
  298. If JsonObject.Type = JTokenType.Array Then
  299. InfoObject = JsonObject(0)
  300. Else
  301. InfoObject = JsonObject("modList")(0)
  302. End If
  303. '从文件中获取 Mod 信息项
  304. Name = InfoObject("name")
  305. Description = InfoObject("description")
  306. Version = InfoObject("version")
  307. Url = InfoObject("url")
  308. ModId = InfoObject("modid")
  309. Dim AuthorJson As JArray = InfoObject("authorList")
  310. If AuthorJson IsNot Nothing Then
  311. Dim Author As New List(Of String)
  312. For Each Token In AuthorJson
  313. Author.Add(Token.ToString)
  314. Next
  315. If Author.Count > 0 Then Authors = Join(Author, ", ")
  316. End If
  317. Dim Reqs As JArray = InfoObject("requiredMods")
  318. If Reqs IsNot Nothing Then
  319. For Each Token As String In Reqs
  320. If Not String.IsNullOrEmpty(Token) Then
  321. Token = Token.Substring(Token.IndexOf(":") + 1)
  322. If Token.Contains("@") Then
  323. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  324. Else
  325. AddDependency(Token)
  326. End If
  327. End If
  328. Next
  329. End If
  330. Reqs = InfoObject("dependancies")
  331. If Reqs IsNot Nothing Then
  332. For Each Token As String In Reqs
  333. If Not String.IsNullOrEmpty(Token) Then
  334. Token = Token.Substring(Token.IndexOf(":") + 1)
  335. If Token.Contains("@") Then
  336. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  337. Else
  338. AddDependency(Token)
  339. End If
  340. End If
  341. Next
  342. End If
  343. Catch ex As Exception
  344. Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer)
  345. End Try
  346. #End Region
  347. #Region "尝试使用 mods.toml"
  348. Try
  349. '获取 mods.toml 文件
  350. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  351. Dim TomlText As String = Nothing
  352. If TomlEntry IsNot Nothing Then
  353. TomlText = ReadFile(TomlEntry.Open())
  354. If TomlText.Length < 15 Then TomlText = Nothing
  355. End If
  356. If TomlText Is Nothing Then Exit Try
  357. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  358. Dim Lines As New List(Of String)
  359. For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
  360. If Line.StartsWith("#") Then '去除注释
  361. Continue For
  362. ElseIf Line.Contains("#") Then
  363. Line = Line.Substring(0, Line.IndexOf("#"))
  364. End If
  365. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  366. If Line.Count > 0 Then Lines.Add(Line) '去除空行
  367. Next
  368. '读取文件数据
  369. 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))}
  370. For i = 0 To Lines.Count - 1
  371. Dim Line As String = Lines(i)
  372. If Line.StartsWith("[") AndAlso Line.EndsWith("]") Then
  373. '段落标记
  374. Dim Header = Line.Trim("[]".ToCharArray)
  375. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  376. ElseIf Line.Contains("=") Then
  377. '字段标记
  378. Dim Key As String = Line.Substring(0, Line.IndexOf("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  379. Dim RawValue As String = Line.Substring(Line.IndexOf("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  380. Dim Value As Object
  381. If RawValue.StartsWith("""") AndAlso RawValue.EndsWith("""") Then
  382. '单行字符串
  383. Value = RawValue.Trim("""")
  384. ElseIf RawValue.StartsWith("'''") Then
  385. '多行字符串
  386. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  387. If ValueLines(0).EndsWith("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  388. ValueLines(0) = ValueLines(0).TrimEnd("'")
  389. Else
  390. Do Until i >= Lines.Count - 1
  391. i += 1
  392. Dim ValueLine As String = Lines(i)
  393. If ValueLine.EndsWith("'''") Then
  394. ValueLines.Add(ValueLine.TrimEnd("'"))
  395. Exit Do
  396. Else
  397. ValueLines.Add(ValueLine)
  398. End If
  399. Loop
  400. End If
  401. Value = Join(ValueLines, vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
  402. ElseIf RawValue.ToLower = "true" OrElse RawValue.ToLower = "false" Then
  403. '布尔型
  404. Value = (RawValue.ToLower = "true")
  405. ElseIf Val(RawValue).ToString = RawValue Then
  406. '数字型
  407. Value = Val(RawValue)
  408. Else
  409. '不知道是个啥玩意儿,直接存储
  410. Value = RawValue
  411. End If
  412. TomlData.Last.Value(Key) = Value
  413. Else
  414. '不知道是个啥玩意儿
  415. Exit Try
  416. End If
  417. Next
  418. '从文件数据中获取信息
  419. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  420. For Each TomlSubData In TomlData
  421. If TomlSubData.Key = "mods" Then
  422. ModEntry = TomlSubData.Value
  423. Exit For
  424. End If
  425. Next
  426. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  427. ModId = ModEntry("modId")
  428. If _ModId Is Nothing Then Exit Try '设置了无效的 ModID
  429. If ModEntry.ContainsKey("displayName") Then Name = ModEntry("displayName")
  430. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  431. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  432. If TomlData(0).Value.ContainsKey("displayURL") Then Url = TomlData(0).Value("displayURL")
  433. If TomlData(0).Value.ContainsKey("authors") Then Authors = TomlData(0).Value("authors")
  434. For Each TomlSubData In TomlData
  435. If TomlSubData.Key.ToLower = $"dependencies.{ModId.ToLower}" Then
  436. Dim DepEntry As Dictionary(Of String, Object) = TomlSubData.Value
  437. If DepEntry.ContainsKey("modId") AndAlso DepEntry.ContainsKey("mandatory") AndAlso DepEntry("mandatory") AndAlso
  438. DepEntry.ContainsKey("side") AndAlso Not DepEntry("side").ToString.ToLower = "server" Then
  439. AddDependency(DepEntry("modId"), If(DepEntry.ContainsKey("versionRange"), DepEntry("versionRange"), Nothing))
  440. End If
  441. End If
  442. Next
  443. '加载成功
  444. GoTo Finished
  445. Catch ex As Exception
  446. Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer)
  447. End Try
  448. #End Region
  449. #Region "尝试使用 fabric.mod.json"
  450. Try
  451. '获取 fabric.mod.json 文件
  452. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  453. Dim FabricText As String = Nothing
  454. If FabricEntry IsNot Nothing Then
  455. FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
  456. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  457. End If
  458. If FabricText Is Nothing Then Exit Try
  459. Dim FabricObject As JObject = GetJson(FabricText)
  460. GotFabric:
  461. '从文件中获取 Mod 信息项
  462. If FabricObject.ContainsKey("name") Then Name = FabricObject("name")
  463. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  464. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  465. If FabricObject.ContainsKey("id") Then ModId = FabricObject("id")
  466. If FabricObject.ContainsKey("contact") Then Url = If(FabricObject("contact")("homepage"), "")
  467. Dim AuthorJson As JArray = FabricObject("authors")
  468. If AuthorJson IsNot Nothing Then
  469. Dim Author As New List(Of String)
  470. For Each Token In AuthorJson
  471. Author.Add(Token.ToString)
  472. Next
  473. If Author.Count > 0 Then Authors = Join(Author, ", ")
  474. End If
  475. 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  476. ' '添加 Minecraft 依赖
  477. ' Dim DepMinecraft As String = If(If(FabricObject("acceptedMinecraftVersions") IsNot Nothing, FabricObject("acceptedMinecraftVersions")("value"), ""), "")
  478. ' If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  479. ' '添加其他依赖
  480. ' Dim Deps As String = If(If(FabricObject("dependencies") IsNot Nothing, FabricObject("dependencies")("value"), ""), "")
  481. ' If Deps <> "" Then
  482. ' For Each Dep In Deps.Split(";")
  483. ' If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
  484. ' Dep = Dep.Substring(Dep.IndexOf(":") + 1)
  485. ' If Dep.Contains("@") Then
  486. ' AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  487. ' Else
  488. ' AddDependency(Dep)
  489. ' End If
  490. ' Next
  491. ' End If
  492. 'End If
  493. '加载成功
  494. GoTo Finished
  495. Catch ex As Exception
  496. Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  497. End Try
  498. #End Region
  499. #Region "尝试使用 fml_cache_annotation.json"
  500. Try
  501. '获取 fml_cache_annotation.json 文件
  502. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  503. Dim FmlText As String = Nothing
  504. If FmlEntry IsNot Nothing Then
  505. FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
  506. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  507. End If
  508. If FmlText Is Nothing Then Exit Try
  509. Dim FmlJson As JObject = GetJson(FmlText)
  510. '获取可用 Json 项
  511. Dim FmlObject As JObject = Nothing
  512. For Each ModFilePair In FmlJson
  513. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  514. If ModFileAnnos IsNot Nothing Then
  515. '先获取 Mod
  516. For Each ModFileAnno In ModFileAnnos
  517. Dim Name As String = If(ModFileAnno("name"), "")
  518. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  519. FmlObject = ModFileAnno("values")
  520. GoTo Got
  521. End If
  522. Next
  523. End If
  524. Next
  525. Exit Try
  526. Got:
  527. '从文件中获取 Mod 信息项
  528. If FmlObject.ContainsKey("useMetadata") AndAlso If(FmlObject("useMetadata")("value"), "").ToString.ToLower = "true" Then
  529. '要求使用 mcmod.info 中的信息
  530. Dim value As String = FmlObject("modid")("value")
  531. If value Is Nothing Then Exit Try
  532. value = RegexSeek(value.ToLower, "[0-9a-z_]+")
  533. If value IsNot Nothing AndAlso value.ToLower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  534. If Not PossibleModId.Contains(value) Then PossibleModId.Add(value)
  535. End If
  536. Exit Try
  537. End If
  538. If FmlObject.ContainsKey("name") Then Name = FmlObject("name")("value")
  539. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  540. If FmlObject.ContainsKey("modid") Then ModId = FmlObject("modid")("value")
  541. If (Not FmlObject.ContainsKey("serverSideOnly")) OrElse FmlObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
  542. '添加 Minecraft 依赖
  543. Dim DepMinecraft As String = If(If(FmlObject("acceptedMinecraftVersions") IsNot Nothing, FmlObject("acceptedMinecraftVersions")("value"), ""), "")
  544. If DepMinecraft <> "" Then AddDependency("minecraft", DepMinecraft)
  545. '添加其他依赖
  546. Dim Deps As String = If(If(FmlObject("dependencies") IsNot Nothing, FmlObject("dependencies")("value"), ""), "")
  547. If Deps <> "" Then
  548. For Each Dep In Deps.Split(";")
  549. If Dep = "" OrElse Not Dep.StartsWith("required-") Then Continue For
  550. Dep = Dep.Substring(Dep.IndexOf(":") + 1)
  551. If Dep.Contains("@") Then
  552. AddDependency(Dep.Split("@")(0), Dep.Split("@")(1))
  553. Else
  554. AddDependency(Dep)
  555. End If
  556. Next
  557. End If
  558. End If
  559. Catch ex As Exception
  560. Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
  561. End Try
  562. #End Region
  563. Finished:
  564. #Region "将 Version 代号转换为 META-INF 中的版本"
  565. If _Version = "version" Then
  566. Try
  567. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  568. If MetaEntry IsNot Nothing Then
  569. Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
  570. If MetaString.Contains("Implementation-Version:") Then
  571. MetaString = MetaString.Substring(MetaString.IndexOf("Implementation-Version:") + "Implementation-Version:".Count)
  572. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  573. Version = MetaString
  574. End If
  575. End If
  576. Catch ex As Exception
  577. Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer)
  578. Version = Nothing
  579. End Try
  580. End If
  581. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  582. #End Region
  583. IsInfoWithoutClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
  584. End Sub
  585. ''' <summary>
  586. ''' 进行使用 .class 文件的信息获取。
  587. ''' </summary>
  588. Private Sub LoadWithClass(Jar As ZipArchive)
  589. Try
  590. '查找入口点文件
  591. Dim ModClass As String = Nothing
  592. Dim ModClassNotBest As String = Nothing '非完美匹配
  593. For Each Entry In Jar.Entries
  594. If Entry.Name.EndsWith(".class") Then
  595. Dim Temp As String = ReadFile(Entry.Open()).ToLower
  596. If Temp.Contains("#lnet/minecraftforge/fml/common/mod;") Then
  597. ModClass = Temp
  598. Exit For
  599. ElseIf Temp.Contains("modid") Then
  600. ModClassNotBest = Temp
  601. End If
  602. End If
  603. Next
  604. If ModClass Is Nothing Then ModClass = ModClassNotBest
  605. If ModClass Is Nothing Then Throw New FileNotFoundException("未找到 Mod 入口点")
  606. ModClass = ModClass.Replace("ljava/lang/string;", "")
  607. If ModClass.Count > 3000 Then ModClass = ModClass.Substring(0, 3000) '如果文件过大,截取前 3000 Byte
  608. Dim IndexHead As Integer, IndexTail As Integer, IncreaseCount As Integer
  609. '获取 ModID
  610. If _ModId Is Nothing Then
  611. IndexHead = ModClass.IndexOf("modid") + "modid".Length + 1
  612. IncreaseCount = 0
  613. Do While Convert.ToInt32(ModClass(IndexHead)) < 32
  614. IndexHead += 1
  615. IncreaseCount += 1
  616. If IncreaseCount > 10 Then Throw New Exception("ModID 头匹配失败")
  617. Loop
  618. IndexTail = IndexHead + 1
  619. IncreaseCount = 0
  620. Do Until Convert.ToInt32(ModClass(IndexTail)) < 32
  621. IndexTail += 1
  622. IncreaseCount += 1
  623. If IncreaseCount > 50 Then Throw New Exception("ModID 尾匹配失败")
  624. Loop
  625. ModId = ModClass.Substring(IndexHead, IndexTail - IndexHead)
  626. End If
  627. '获取 Version
  628. If _Version Is Nothing AndAlso ModClass.Contains("version") Then
  629. IndexHead = ModClass.IndexOf("version") + "version".Length + 1
  630. IncreaseCount = 0
  631. Do While Convert.ToInt32(ModClass(IndexHead)) < 32
  632. IndexHead += 1
  633. IncreaseCount += 1
  634. If IncreaseCount > 10 Then GoTo VersionFindFail
  635. Loop
  636. IndexTail = IndexHead + 1
  637. IncreaseCount = 0
  638. Do While ModClass(IndexTail) = "."c OrElse ModClass(IndexTail) = "-"c OrElse
  639. (ModClass(IndexTail) >= "0"c AndAlso ModClass(IndexTail) <= "9"c) OrElse
  640. (ModClass(IndexTail) >= "a"c AndAlso ModClass(IndexTail) <= "z"c)
  641. IndexTail += 1
  642. IncreaseCount += 1
  643. If IncreaseCount > 50 Then GoTo VersionFindFail
  644. Loop
  645. _Version = ModClass.Substring(IndexHead, IndexTail - IndexHead)
  646. End If
  647. VersionFindFail:
  648. '获取 Dependencies
  649. IndexHead = ModClass.IndexOf("dependencies")
  650. If IndexHead > 0 Then
  651. If ModClass.Count >= IndexHead + 300 Then ModClass = ModClass.Substring(IndexHead, 299)
  652. Dim Deps As List(Of String) = RegexSearch(ModClass, "(?<=required-((before|after|before-client|after-client)?):)[0-9a-z]+(@[\(\[]{1}[0-9.,]+[\)\]]{1})?")
  653. For Each Token As String In Deps
  654. If Not String.IsNullOrEmpty(Token) Then
  655. If Token.Contains("@") Then
  656. AddDependency(Token.Split("@")(0), Token.Split("@")(1))
  657. Else
  658. AddDependency(Token)
  659. End If
  660. End If
  661. Next
  662. End If
  663. Catch ex As Exception
  664. Log(ex, "Mod Class 信息不可用(" & If(Path, "null") & ")", LogLevel.Normal)
  665. End Try
  666. IsInfoWithClassAvailable = _ModId IsNot Nothing AndAlso _Version IsNot Nothing
  667. End Sub
  668. #End Region
  669. ''' <summary>
  670. ''' 是否可能为前置 Mod。
  671. ''' </summary>
  672. Public Function IsPresetMod() As Boolean
  673. Return Dependencies.Count = 0 AndAlso (Name IsNot Nothing) AndAlso (Name.ToLower.Contains("core") OrElse Name.ToLower.Contains("lib"))
  674. End Function
  675. ''' <summary>
  676. ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
  677. ''' </summary>
  678. Public Shared Function IsModFile(Path As String)
  679. If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
  680. Path = Path.ToLower
  681. If Path.EndsWith(".jar") OrElse Path.EndsWith(".zip") OrElse Path.EndsWith(".litemod") OrElse
  682. Path.EndsWith(".jar.disabled") OrElse Path.EndsWith(".zip.disabled") OrElse Path.EndsWith(".litemod.disabled") Then Return True
  683. Return False
  684. End Function
  685. End Class
  686. '加载 Mod 列表
  687. Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad)
  688. Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod)))
  689. Try
  690. RunInUiWait(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = False)
  691. '获取 Mod 文件夹下的可用文件列表
  692. Dim ModFileList As New List(Of FileInfo)
  693. If Directory.Exists(Loader.Input) Then
  694. Dim RawName As String = Loader.Input.ToLower
  695. For Each File As FileInfo In EnumerateFiles(Loader.Input)
  696. If File.DirectoryName.ToLower & "\" <> RawName Then
  697. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  698. If Not (PageVersionLeft.Version IsNot Nothing AndAlso PageVersionLeft.Version.Version.HasForge AndAlso
  699. PageVersionLeft.Version.Version.McCodeMain < 13 AndAlso
  700. File.Directory.Name = "1." & PageVersionLeft.Version.Version.McCodeMain & "." & PageVersionLeft.Version.Version.McCodeSub) Then
  701. Continue For
  702. End If
  703. End If
  704. If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
  705. Next
  706. End If
  707. '确定是否显示进度
  708. Loader.Progress = 0.03
  709. If ModFileList.Count > 100 Then RunInUi(Sub() If FrmVersionMod IsNot Nothing Then FrmVersionMod.Load.ShowProgress = True)
  710. '加载为 Mod 列表
  711. Dim ModList As New List(Of McMod)
  712. For Each ModFile As FileInfo In ModFileList
  713. Dim ModEntity As New McMod(ModFile.FullName)
  714. ModEntity.Load()
  715. ModList.Add(ModEntity)
  716. Loader.Progress += 0.9 / ModFileList.Count
  717. Next
  718. Loader.Progress = 0.93
  719. '排序
  720. ModList = Sort(ModList, Function(Left As McMod, Right As McMod) As Boolean
  721. If (Left.State = McMod.McModState.Unavaliable) <> (Right.State = McMod.McModState.Unavaliable) Then
  722. Return Left.State = McMod.McModState.Unavaliable
  723. Else
  724. Return Not Right.FileName.CompareTo(Left.FileName)
  725. End If
  726. End Function)
  727. Loader.Progress = 0.98
  728. '回设
  729. If Loader.IsAborted Then Exit Sub
  730. Loader.Output = ModList
  731. Catch ex As Exception
  732. Log(ex, "Mod 列表加载失败", LogLevel.Debug)
  733. Throw
  734. End Try
  735. End Sub
  736. #If DEBUG Then
  737. ''' <summary>
  738. ''' 检查 Mod 列表中存在的错误,返回错误信息的集合。
  739. ''' </summary>
  740. Public Function McModCheck(Version As McVersion, Mods As List(Of McMod)) As List(Of String)
  741. Dim Result As New List(Of String)
  742. '令所有 Mod 进行基础检查,并归纳需要检查的 Mod
  743. Dim CurrentModList As New List(Of McMod)
  744. For Each ModEntity In Mods
  745. If Not ModEntity.IsFileAvailable Then
  746. Result.Add("无法读取的 Mod 文件。" & vbCrLf & " - " & ModEntity.Path)
  747. Continue For
  748. End If
  749. If ModEntity.State = McMod.McModState.Fine AndAlso ModEntity.ModId IsNot Nothing Then CurrentModList.Add(ModEntity)
  750. Next
  751. '添加默认依赖
  752. Dim CurrentDependencies As New Dictionary(Of String, String()) '{DependencyVersion, Path}
  753. If Version.State = McVersionState.Forge Then CurrentDependencies.Add("forge", {Version.Version.ForgeVersion, "Forge"})
  754. CurrentDependencies.Add("minecraft", {Version.Version.McName, "Minecraft"})
  755. '检查重复的 Mod,并添加对应的依赖
  756. For Each ModEntity In CurrentModList
  757. For Each PossibleModId In ModEntity.PossibleModId
  758. If CurrentDependencies.ContainsKey(PossibleModId) Then
  759. If CurrentDependencies(PossibleModId)(2) = 1 Then
  760. Result.Add("重复添加了相同的 Mod,请尝试删除其中一个(ModID:" & PossibleModId & ")。" & vbCrLf &
  761. " - " & ModEntity.FileName & vbCrLf &
  762. " - " & CurrentDependencies(PossibleModId)(1))
  763. Else
  764. Log("[Minecraft] 由于可能有多个 ModID,跳过疑似的重复项(ModID:" & PossibleModId & ")。" & vbCrLf &
  765. " - " & ModEntity.FileName & vbCrLf &
  766. " - " & CurrentDependencies(PossibleModId)(1), LogLevel.Developer)
  767. End If
  768. Else
  769. CurrentDependencies.Add(PossibleModId, {ModEntity.Version, ModEntity.FileName, ModEntity.PossibleModId.Count})
  770. End If
  771. Next
  772. Next
  773. '检查依赖
  774. For Each ModEntity In CurrentModList
  775. Try
  776. For Each Dependency In ModEntity.Dependencies
  777. Dim ReqId As String = Dependency.Key
  778. If ReqId.Count < 2 Then Continue For '确保正常
  779. If ReqId = ModEntity.ModId Then Continue For '跳过自体引用
  780. If ReqId = "forgemultipartcbe" Then Continue For '跳过莫名其妙的引用
  781. If Dependency.Value IsNot Nothing Then
  782. '获取分段后的详细版本信息
  783. Dim ReqVersion As String = Dependency.Value
  784. Dim ReqVersionHeadCanEqual As Boolean = ReqVersion.StartsWith("[")
  785. Dim ReqVersionTailCanEqual As Boolean = ReqVersion.EndsWith("]")
  786. Dim ReqVersionHead As String
  787. Dim ReqVersionTail As String
  788. If ReqVersion.Contains(",") Then
  789. ReqVersionHead = ReqVersion.Split(",")(0).Trim("([ ".ToCharArray())
  790. ReqVersionTail = ReqVersion.Split(",")(1).Trim("]) ".ToCharArray())
  791. Else
  792. ReqVersionHead = ReqVersion.Trim("([]) ".ToCharArray())
  793. ReqVersionTail = ReqVersionHead
  794. If ReqId = "minecraft" AndAlso ReqVersionHead.Split(".").Count = 2 Then
  795. ReqVersionTail = ReqVersionHead.Split(".")(0) & "." & (Val(ReqVersionHead.Split(".")(1)) + 1)
  796. ReqVersionTailCanEqual = False
  797. End If
  798. End If
  799. If ReqVersionHead.StartsWith("1.") AndAlso ReqVersionHead.Contains("-") Then ReqVersionHead = ReqVersionHead.Substring(ReqVersionHead.LastIndexOf("-") + 1)
  800. If ReqVersionTail.StartsWith("1.") AndAlso ReqVersionTail.Contains("-") Then ReqVersionTail = ReqVersionTail.Substring(ReqVersionTail.LastIndexOf("-") + 1)
  801. '获取报错描述文本
  802. Dim VersionRequire As String
  803. If ReqVersionHead = ReqVersionTail Then
  804. VersionRequire = "应为 " & ReqVersionHead
  805. ElseIf ReqVersionHead.Contains(".") AndAlso ReqVersionTail.Contains(".") Then
  806. VersionRequire = "应为 " & ReqVersionHead & " 至 " & ReqVersionTail
  807. ElseIf ReqVersionHead.Contains(".") Then
  808. If ReqVersionHeadCanEqual Then
  809. VersionRequire = "最低应为 " & ReqVersionHead
  810. Else
  811. VersionRequire = "应高于 " & ReqVersionHead
  812. End If
  813. ElseIf ReqVersionTail.Contains(".") Then
  814. If ReqVersionTailCanEqual Then
  815. VersionRequire = "最高应为 " & ReqVersionHead
  816. Else
  817. VersionRequire = "应低于 " & ReqVersionHead
  818. End If
  819. Else
  820. VersionRequire = ""
  821. End If
  822. '检查前置 Mod 是否存在,并获取其版本
  823. If Not CurrentDependencies.ContainsKey(ReqId) Then
  824. Result.Add("缺少前置 Mod:" & ReqId & If(VersionRequire = "", "", ",其版本" & VersionRequire) & "。" & vbCrLf & " - " & ModEntity.FileName)
  825. Continue For
  826. End If
  827. Dim CurrentVersion As String = If(CurrentDependencies(ReqId)(0), "0.0")
  828. If CurrentVersion.StartsWith("1.") AndAlso CurrentVersion.Contains("-") Then CurrentVersion = CurrentVersion.Substring(CurrentVersion.LastIndexOf("-") + 1)
  829. '对比前置 Mod 头部版本
  830. If ReqVersionHead.Contains(".") Then
  831. If VersionSortInteger(ReqVersionHead, CurrentVersion) > If(ReqVersionHeadCanEqual, 0, -1) Then
  832. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过低,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  833. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  834. Continue For
  835. End If
  836. End If
  837. '对比前置 Mod 尾部版本
  838. If ReqVersionTail.Contains(".") Then
  839. If VersionSortInteger(CurrentVersion, ReqVersionTail) > If(ReqVersionTailCanEqual, 0, -1) Then
  840. Result.Add(ReqId.Substring(0, 1).ToUpper & ReqId.Substring(1) & " 版本过高,其版本" & VersionRequire & ",而当前版本为 " & CurrentVersion & "。" & vbCrLf &
  841. " - " & ModEntity.FileName & If(ReqId <> "minecraft" AndAlso ReqId <> "forge", vbCrLf & " - 前置:" & CurrentDependencies(ReqId)(1), ""))
  842. Continue For
  843. End If
  844. End If
  845. Else
  846. If Not CurrentDependencies.ContainsKey(Dependency.Key) Then
  847. Result.Add("缺少前置 Mod:" & Dependency.Key & "。" & vbCrLf & " - " & ModEntity.FileName)
  848. Continue For
  849. End If
  850. End If
  851. Next
  852. Catch ex As Exception
  853. Result.Add("检查 Mod 时出错:" & GetExceptionSummary(ex) & vbCrLf & " - " & ModEntity.FileName)
  854. Log(ex, "检查 Mod 时出错")
  855. End Try
  856. Next
  857. If Result.Count = 0 Then
  858. Log("[Minecraft] Mod 检查未发现异常")
  859. Else
  860. Log("[Minecraft] Mod 检查异常结果:" & vbCrLf & Join(Result, vbCrLf))
  861. End If
  862. Return Result
  863. End Function
  864. #End If
  865. End Module