LocalResourceFile.vb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. ''' <summary>
  2. ''' 本地的社区资源文件。
  3. ''' </summary>
  4. Public Class LocalResourceFile
  5. #Region "文件信息"
  6. ''' <summary>
  7. ''' 该文件的文件信息。
  8. ''' </summary>
  9. Public ReadOnly File As FileInfo
  10. Public Sub New(FileInfo As FileInfo)
  11. Me.File = FileInfo
  12. End Sub
  13. Public Sub New(FullName As String)
  14. Me.New(FileUtils.GetInfo(FullName))
  15. End Sub
  16. ''' <summary>
  17. ''' 该文件是否没有被禁用。
  18. ''' </summary>
  19. Public ReadOnly Property IsEnabled As Boolean
  20. Get
  21. Return {".jar", ".zip", ".litemod"}.Contains(File.Extension.Lower)
  22. End Get
  23. End Property
  24. ''' <summary>
  25. ''' 假若该资源未被禁用时,文件的文件名。
  26. ''' </summary>
  27. Public ReadOnly Property EnabledName As String
  28. Get
  29. Return File.Name.Replace(".disabled", "").Replace(".old", "")
  30. End Get
  31. End Property
  32. ''' <summary>
  33. ''' 假若该资源未被禁用时,文件的完整路径。
  34. ''' </summary>
  35. Public ReadOnly Property EnabledFullName As String
  36. Get
  37. Return $"{File.DirectoryName}\{EnabledName}"
  38. End Get
  39. End Property
  40. #End Region
  41. #Region "信息项"
  42. ''' <summary>
  43. ''' 在初始化结束之后,任何信息被后续更新时触发此事件。
  44. ''' </summary>
  45. Public Event OnUpdate(sender As LocalResourceFile)
  46. ''' <summary>
  47. ''' 可读名称。
  48. ''' 可能为无扩展的文件名,但不会是 Nothing。
  49. ''' </summary>
  50. Public Property Display As String
  51. Get
  52. If _Display Is Nothing Then _Display = ProjectVersion?.Display
  53. If _Display Is Nothing Then _Display = PathUtils.GetFileNameWithoutExtension(File.FullName)
  54. Return _Display
  55. End Get
  56. Set(value As String)
  57. If _Display Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
  58. value.Lower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
  59. _Display = value
  60. End If
  61. End Set
  62. End Property
  63. Private _Display As String = Nothing
  64. ''' <summary>
  65. ''' Mod 的描述信息。
  66. ''' 可能为 Nothing。
  67. ''' </summary>
  68. Public Property Description As String
  69. Get
  70. Return _Description
  71. End Get
  72. Set(value As String)
  73. If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then
  74. _Description = value.ToString.Trim(vbLf)
  75. '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
  76. If _Description.Lower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
  77. End If
  78. End Set
  79. End Property
  80. Private _Description As String = Nothing
  81. ''' <summary>
  82. ''' Mod 的版本。
  83. ''' 不保证符合版本格式规范,且可能为 Nothing。
  84. ''' </summary>
  85. Public Property Version As String
  86. Get
  87. If _Version Is Nothing Then _Version = ProjectVersion?.Version
  88. Return _Version
  89. End Get
  90. Set(value As String)
  91. If _Version IsNot Nothing AndAlso _Version.RegexCheck("[0-9.\-]+") Then Return
  92. If value IsNot Nothing AndAlso value.ContainsIgnoreCase("version") Then value = "version" '需要修改的标识
  93. _Version = value
  94. End Set
  95. End Property
  96. Public _Version As String = Nothing
  97. #End Region
  98. #Region "从 JAR 获取信息"
  99. ''' <summary>
  100. ''' 从 JAR 文件中获取 Mod 信息。
  101. ''' </summary>
  102. Public Sub LoadMetadataFromJar()
  103. Static IsLoaded As Boolean = False
  104. If IsLoaded Then Return
  105. IsLoaded = True
  106. Dim Jar As ZipArchive = Nothing
  107. Try
  108. '基础可用性检查
  109. If Not FileUtils.Exists(File.FullName) Then Throw New FileNotFoundException("未找到 Mod 文件(" & File.FullName & ")")
  110. '打开 Jar 文件
  111. Jar = FileUtils.OpenZip(File.FullName)
  112. '信息获取
  113. LoadMetadataFromJar(Jar)
  114. Catch ex As UnauthorizedAccessException
  115. Logger.Warn(ex, $"Mod 文件由于无权限无法打开({File.FullName})")
  116. Catch ex As Exception
  117. Logger.Warn(ex, $"Mod 文件无法打开({File.FullName})")
  118. Finally
  119. If Jar IsNot Nothing Then Jar.Dispose()
  120. End Try
  121. End Sub
  122. Private Sub LoadMetadataFromJar(Jar As ZipArchive)
  123. #Region "尝试使用 mcmod.info"
  124. Try
  125. '获取信息文件
  126. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  127. Dim InfoString As String = Nothing
  128. If InfoEntry IsNot Nothing Then
  129. InfoString = InfoEntry.Open().ReadString()
  130. If InfoString.Length < 15 Then InfoString = Nothing
  131. End If
  132. If InfoString Is Nothing Then Exit Try
  133. '获取可用 Json 项
  134. Dim InfoObject As JObject
  135. Dim JsonObject = GetJson(InfoString)
  136. If JsonObject.Type = JTokenType.Array Then
  137. InfoObject = JsonObject(0)
  138. Else
  139. InfoObject = JsonObject("modList")(0)
  140. End If
  141. '从文件中获取 Mod 信息项
  142. Display = InfoObject("name")
  143. Description = InfoObject("description")
  144. Version = InfoObject("version")
  145. Catch ex As Exception
  146. Logger.Warn(ex, $"读取 mcmod.info 时出现未知错误({File.FullName})")
  147. End Try
  148. #End Region
  149. #Region "尝试使用 fabric.mod.json"
  150. Try
  151. '获取 fabric.mod.json 文件
  152. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  153. Dim FabricText As String = Nothing
  154. If FabricEntry IsNot Nothing Then
  155. FabricText = FabricEntry.Open().ReadString(Encoding.UTF8)
  156. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  157. End If
  158. If FabricText Is Nothing Then Exit Try
  159. Dim FabricObject As JObject = GetJson(FabricText)
  160. GotFabric:
  161. '从文件中获取 Mod 信息项
  162. If FabricObject.ContainsKey("name") Then Display = FabricObject("name")
  163. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  164. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  165. '加载成功
  166. GoTo Finished
  167. Catch ex As Exception
  168. Logger.Warn(ex, $"读取 fabric.mod.json 时出现未知错误({File.FullName})")
  169. End Try
  170. #End Region
  171. #Region "尝试使用 mods.toml"
  172. Try
  173. '获取 mods.toml 文件
  174. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  175. Dim TomlText As String = Nothing
  176. If TomlEntry IsNot Nothing Then
  177. TomlText = TomlEntry.Open().ReadString()
  178. If TomlText.Length < 15 Then TomlText = Nothing
  179. End If
  180. If TomlText Is Nothing Then Exit Try
  181. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  182. Dim Lines As New List(Of String)
  183. For Each Line In TomlText.SplitLines(True)
  184. If Line.StartsWithF("#") Then '去除注释
  185. Continue For
  186. ElseIf Line.Contains("#") Then
  187. Line = Line.Substring(0, Line.IndexOfF("#"))
  188. End If
  189. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  190. If Line.Any Then Lines.Add(Line) '去除空行
  191. Next
  192. '读取文件数据
  193. 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))}
  194. For i = 0 To Lines.Count - 1
  195. Dim Line As String = Lines(i)
  196. If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
  197. '段落标记
  198. Dim Header = Line.Trim("[]".ToCharArray)
  199. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  200. ElseIf Line.Contains("=") Then
  201. '字段标记
  202. Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  203. Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  204. Dim Value As Object
  205. If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
  206. '单行字符串
  207. Value = RawValue.Trim("""")
  208. ElseIf RawValue.StartsWithF("'''") Then
  209. '多行字符串
  210. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  211. If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  212. ValueLines(0) = ValueLines(0).TrimEnd("'")
  213. Else
  214. Do Until i >= Lines.Count - 1
  215. i += 1
  216. Dim ValueLine As String = Lines(i)
  217. If ValueLine.EndsWithF("'''") Then
  218. ValueLines.Add(ValueLine.TrimEnd("'"))
  219. Exit Do
  220. Else
  221. ValueLines.Add(ValueLine)
  222. End If
  223. Loop
  224. End If
  225. Value = ValueLines.Join(vbLf).Trim(vbLf).ReplaceLineEndings(vbCrLf)
  226. ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
  227. '布尔型
  228. Value = (RawValue.Lower = "true")
  229. ElseIf Val(RawValue).ToString = RawValue Then
  230. '数字型
  231. Value = Val(RawValue)
  232. Else
  233. '不知道是个啥玩意儿,直接存储
  234. Value = RawValue
  235. End If
  236. TomlData.Last.Value(Key) = Value
  237. Else
  238. '不知道是个啥玩意儿
  239. Exit Try
  240. End If
  241. Next
  242. '从文件数据中获取信息
  243. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  244. For Each TomlSubData In TomlData
  245. If TomlSubData.Key = "mods" Then
  246. ModEntry = TomlSubData.Value
  247. Exit For
  248. End If
  249. Next
  250. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  251. If ModEntry.ContainsKey("displayName") Then Display = ModEntry("displayName")
  252. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  253. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  254. '加载成功
  255. GoTo Finished
  256. Catch ex As Exception
  257. Logger.Warn(ex, $"读取 mods.toml 时出现未知错误({File.FullName})")
  258. End Try
  259. #End Region
  260. #Region "尝试使用 fml_cache_annotation.json"
  261. Try
  262. '获取 fml_cache_annotation.json 文件
  263. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  264. Dim FmlText As String = Nothing
  265. If FmlEntry IsNot Nothing Then
  266. FmlText = FmlEntry.Open().ReadString(Encoding.UTF8)
  267. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  268. End If
  269. If FmlText Is Nothing Then Exit Try
  270. Dim FmlJson As JObject = GetJson(FmlText)
  271. '获取可用 Json 项
  272. Dim FmlObject As JObject = Nothing
  273. For Each ModFilePair In FmlJson
  274. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  275. If ModFileAnnos IsNot Nothing Then
  276. '先获取 Mod
  277. For Each ModFileAnno In ModFileAnnos
  278. Dim Name As String = If(ModFileAnno("name"), "")
  279. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  280. FmlObject = ModFileAnno("values")
  281. GoTo Got
  282. End If
  283. Next
  284. End If
  285. Next
  286. Exit Try
  287. Got:
  288. '从文件中获取 Mod 信息项
  289. If FmlObject.ContainsKey("name") Then Display = FmlObject("name")("value")
  290. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  291. '加载成功
  292. GoTo Finished
  293. Catch ex As Exception
  294. Logger.Warn(ex, $"读取 fml_cache_annotation.json 时出现未知错误({File.FullName})")
  295. End Try
  296. #End Region
  297. Return '没能成功获取任何信息
  298. Finished:
  299. #Region "将 Version 代号转换为 META-INF 中的版本"
  300. If _Version = "version" Then
  301. Try
  302. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  303. If MetaEntry IsNot Nothing Then
  304. Dim MetaString As String = MetaEntry.Open().ReadString().Replace(" :", ":").Replace(": ", ":")
  305. If MetaString.Contains("Implementation-Version:") Then
  306. MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
  307. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  308. Version = MetaString
  309. End If
  310. End If
  311. Catch ex As Exception
  312. Logger.Warn($"获取 META-INF 中的版本信息失败({File.FullName})")
  313. Version = Nothing
  314. End Try
  315. End If
  316. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  317. #End Region
  318. RaiseEvent OnUpdate(Me)
  319. End Sub
  320. #End Region
  321. #Region "网络信息"
  322. ''' <summary>
  323. ''' 该 Mod 关联的网络项目。
  324. ''' </summary>
  325. Public Property Project As ResourceProject
  326. Get
  327. Return _Project
  328. End Get
  329. Set(value As ResourceProject)
  330. _Project = value
  331. RaiseEvent OnUpdate(Me)
  332. End Set
  333. End Property
  334. Private _Project As ResourceProject
  335. ''' <summary>
  336. ''' 本地文件对应的联网文件信息。
  337. ''' </summary>
  338. Public ProjectVersion As ResourceVersion
  339. ''' <summary>
  340. ''' 该 Mod 对应的联网最新版本。
  341. ''' </summary>
  342. Public Property UpdateFile As ResourceVersion
  343. Get
  344. Return _UpdateFile
  345. End Get
  346. Set(value As ResourceVersion)
  347. _UpdateFile = value
  348. RaiseEvent OnUpdate(Me)
  349. End Set
  350. End Property
  351. Private _UpdateFile As ResourceVersion
  352. ''' <summary>
  353. ''' 该 Mod 的更新日志网址。
  354. ''' </summary>
  355. Public ChangelogUrls As New List(Of String)
  356. ''' <summary>
  357. ''' 所有网络信息是否已成功加载。
  358. ''' </summary>
  359. Public OnlineDataLoaded As Boolean = False
  360. ''' <summary>
  361. ''' 将网络信息保存为 JSON。
  362. ''' </summary>
  363. Public Function ToJson() As JObject
  364. Dim Json As New JObject
  365. If Project IsNot Nothing Then Json.Add("Project", Project.ToJson())
  366. Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
  367. Json.Add("OnlineDataLoaded", OnlineDataLoaded)
  368. If ProjectVersion IsNot Nothing Then Json.Add("Version", ProjectVersion.ToCacheJson())
  369. If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToCacheJson())
  370. Return Json
  371. End Function
  372. ''' <summary>
  373. ''' 从 JSON 中读取网络信息。
  374. ''' </summary>
  375. Public Sub FromJson(Json As JObject)
  376. OnlineDataLoaded = Json("OnlineDataLoaded")
  377. If Json.ContainsKey("Project") Then Project = New ResourceProject(Json("Project"))
  378. If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
  379. If Json.ContainsKey("Version") Then ProjectVersion = ResourceVersion.FromCacheJson(Json("Version"))
  380. If Json.ContainsKey("UpdateFile") Then UpdateFile = ResourceVersion.FromCacheJson(Json("UpdateFile"))
  381. End Sub
  382. ''' <summary>
  383. ''' 该文件是否可以更新。
  384. ''' </summary>
  385. Public ReadOnly Property HasUpdate As Boolean
  386. Get
  387. Return Not Settings.Get(Of Boolean)("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get(Of Boolean)("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
  388. End Get
  389. End Property
  390. ''' <summary>
  391. ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
  392. ''' </summary>
  393. Public ReadOnly Property CurseForgeHash As UInteger
  394. Get
  395. If _CurseForgeHash Is Nothing Then
  396. '读取缓存
  397. Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-C")
  398. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  399. If Cached <> "" AndAlso Cached.RegexCheck("^\d+$") Then '#5062
  400. _CurseForgeHash = Cached
  401. Return _CurseForgeHash
  402. End If
  403. '读取文件
  404. Dim data As New List(Of Byte)
  405. For Each b As Byte In FileUtils.ReadAsBytes(File.FullName)
  406. If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
  407. data.Add(b)
  408. Next
  409. '计算 MurmurHash2
  410. Dim length As Integer = data.Count
  411. Dim h As UInteger = 1 Xor length '1 是种子
  412. Dim i As Integer
  413. For i = 0 To length - 4 Step 4
  414. Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
  415. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  416. k = k Xor (k >> 24)
  417. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  418. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  419. h = h Xor k
  420. Next
  421. Select Case length - i
  422. Case 3
  423. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  424. h = h Xor (CUInt(data(i + 2)) << 16)
  425. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  426. Case 2
  427. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  428. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  429. Case 1
  430. h = h Xor data(i)
  431. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  432. End Select
  433. h = h Xor (h >> 13)
  434. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  435. h = h Xor (h >> 15)
  436. _CurseForgeHash = h
  437. '写入缓存
  438. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
  439. End If
  440. Return _CurseForgeHash
  441. End Get
  442. End Property
  443. Private _CurseForgeHash As UInteger?
  444. ''' <summary>
  445. ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
  446. ''' </summary>
  447. Public ReadOnly Property ModrinthHash As String
  448. Get
  449. If _ModrinthHash Is Nothing Then
  450. '读取缓存
  451. Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-M")
  452. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  453. If Cached <> "" Then
  454. _ModrinthHash = Cached
  455. Return _ModrinthHash
  456. End If
  457. '计算 SHA1
  458. _ModrinthHash = HashUtils.ComputeFromFile(File.FullName, HashUtils.HashMethod.Sha1)
  459. '写入缓存
  460. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
  461. End If
  462. Return _ModrinthHash
  463. End Get
  464. End Property
  465. Private _ModrinthHash As String
  466. #End Region
  467. #Region "API"
  468. Public Overrides Function ToString() As String
  469. Return File.FullName
  470. End Function
  471. Public Overrides Function Equals(obj As Object) As Boolean
  472. Dim Target = TryCast(obj, LocalResourceFile)
  473. Return Target IsNot Nothing AndAlso File.FullName = Target.File.FullName
  474. End Function
  475. #End Region
  476. End Class