ResourceFile.vb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. ''' <summary>
  2. ''' 本地的社区资源文件。
  3. ''' </summary>
  4. Public Class ResourceFile
  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(New FileInfo(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 ResourceFile)
  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 = Path.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 RegexCheck(_Version, "[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 File.FullName.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(File.FullName, "null") & ")")
  110. If Not IO.File.Exists(File.FullName) Then Throw New FileNotFoundException("未找到 Mod 文件(" & File.FullName & ")")
  111. '打开 Jar 文件
  112. Jar = New ZipArchive(New FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
  113. '信息获取
  114. LoadMetadataFromJar(Jar)
  115. Catch ex As UnauthorizedAccessException
  116. Log(ex, $"Mod 文件由于无权限无法打开({File.FullName})", NotifyLevel.DevelopOnly)
  117. Catch ex As Exception
  118. Log(ex, $"Mod 文件无法打开({File.FullName})", NotifyLevel.DevelopOnly)
  119. Finally
  120. If Jar IsNot Nothing Then Jar.Dispose()
  121. End Try
  122. End Sub
  123. Private Sub LoadMetadataFromJar(Jar As ZipArchive)
  124. #Region "尝试使用 mcmod.info"
  125. Try
  126. '获取信息文件
  127. Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
  128. Dim InfoString As String = Nothing
  129. If InfoEntry IsNot Nothing Then
  130. InfoString = ReadFile(InfoEntry.Open())
  131. If InfoString.Length < 15 Then InfoString = Nothing
  132. End If
  133. If InfoString Is Nothing Then Exit Try
  134. '获取可用 Json 项
  135. Dim InfoObject As JObject
  136. Dim JsonObject = GetJson(InfoString)
  137. If JsonObject.Type = JTokenType.Array Then
  138. InfoObject = JsonObject(0)
  139. Else
  140. InfoObject = JsonObject("modList")(0)
  141. End If
  142. '从文件中获取 Mod 信息项
  143. Display = InfoObject("name")
  144. Description = InfoObject("description")
  145. Version = InfoObject("version")
  146. Catch ex As Exception
  147. Log(ex, $"读取 mcmod.info 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
  148. End Try
  149. #End Region
  150. #Region "尝试使用 fabric.mod.json"
  151. Try
  152. '获取 fabric.mod.json 文件
  153. Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
  154. Dim FabricText As String = Nothing
  155. If FabricEntry IsNot Nothing Then
  156. FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
  157. If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
  158. End If
  159. If FabricText Is Nothing Then Exit Try
  160. Dim FabricObject As JObject = GetJson(FabricText)
  161. GotFabric:
  162. '从文件中获取 Mod 信息项
  163. If FabricObject.ContainsKey("name") Then Display = FabricObject("name")
  164. If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
  165. If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
  166. '加载成功
  167. GoTo Finished
  168. Catch ex As Exception
  169. Log(ex, "读取 fabric.mod.json 时出现未知错误(" & File.FullName & ")", NotifyLevel.DevelopOnly)
  170. End Try
  171. #End Region
  172. #Region "尝试使用 mods.toml"
  173. Try
  174. '获取 mods.toml 文件
  175. Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
  176. Dim TomlText As String = Nothing
  177. If TomlEntry IsNot Nothing Then
  178. TomlText = ReadFile(TomlEntry.Open())
  179. If TomlText.Length < 15 Then TomlText = Nothing
  180. End If
  181. If TomlText Is Nothing Then Exit Try
  182. '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
  183. Dim Lines As New List(Of String)
  184. For Each Line In TomlText.ReplaceLineEndings(vbLf).Split(vbLf) '统一换行符
  185. If Line.StartsWithF("#") Then '去除注释
  186. Continue For
  187. ElseIf Line.Contains("#") Then
  188. Line = Line.Substring(0, Line.IndexOfF("#"))
  189. End If
  190. Line = Line.Trim(New Char() {" "c, " "c, " "c}) '去除头尾的空格
  191. If Line.Any Then Lines.Add(Line) '去除空行
  192. Next
  193. '读取文件数据
  194. 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))}
  195. For i = 0 To Lines.Count - 1
  196. Dim Line As String = Lines(i)
  197. If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
  198. '段落标记
  199. Dim Header = Line.Trim("[]".ToCharArray)
  200. TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
  201. ElseIf Line.Contains("=") Then
  202. '字段标记
  203. Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, " "c, " "c})
  204. Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, " "c, " "c})
  205. Dim Value As Object
  206. If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
  207. '单行字符串
  208. Value = RawValue.Trim("""")
  209. ElseIf RawValue.StartsWithF("'''") Then
  210. '多行字符串
  211. Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
  212. If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
  213. ValueLines(0) = ValueLines(0).TrimEnd("'")
  214. Else
  215. Do Until i >= Lines.Count - 1
  216. i += 1
  217. Dim ValueLine As String = Lines(i)
  218. If ValueLine.EndsWithF("'''") Then
  219. ValueLines.Add(ValueLine.TrimEnd("'"))
  220. Exit Do
  221. Else
  222. ValueLines.Add(ValueLine)
  223. End If
  224. Loop
  225. End If
  226. Value = ValueLines.Join(vbLf).Trim(vbLf).ReplaceLineEndings(vbCrLf)
  227. ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
  228. '布尔型
  229. Value = (RawValue.Lower = "true")
  230. ElseIf Val(RawValue).ToString = RawValue Then
  231. '数字型
  232. Value = Val(RawValue)
  233. Else
  234. '不知道是个啥玩意儿,直接存储
  235. Value = RawValue
  236. End If
  237. TomlData.Last.Value(Key) = Value
  238. Else
  239. '不知道是个啥玩意儿
  240. Exit Try
  241. End If
  242. Next
  243. '从文件数据中获取信息
  244. Dim ModEntry As Dictionary(Of String, Object) = Nothing
  245. For Each TomlSubData In TomlData
  246. If TomlSubData.Key = "mods" Then
  247. ModEntry = TomlSubData.Value
  248. Exit For
  249. End If
  250. Next
  251. If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
  252. If ModEntry.ContainsKey("displayName") Then Display = ModEntry("displayName")
  253. If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
  254. If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
  255. '加载成功
  256. GoTo Finished
  257. Catch ex As Exception
  258. Log(ex, $"读取 mods.toml 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
  259. End Try
  260. #End Region
  261. #Region "尝试使用 fml_cache_annotation.json"
  262. Try
  263. '获取 fml_cache_annotation.json 文件
  264. Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
  265. Dim FmlText As String = Nothing
  266. If FmlEntry IsNot Nothing Then
  267. FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
  268. If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
  269. End If
  270. If FmlText Is Nothing Then Exit Try
  271. Dim FmlJson As JObject = GetJson(FmlText)
  272. '获取可用 Json 项
  273. Dim FmlObject As JObject = Nothing
  274. For Each ModFilePair In FmlJson
  275. Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
  276. If ModFileAnnos IsNot Nothing Then
  277. '先获取 Mod
  278. For Each ModFileAnno In ModFileAnnos
  279. Dim Name As String = If(ModFileAnno("name"), "")
  280. If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
  281. FmlObject = ModFileAnno("values")
  282. GoTo Got
  283. End If
  284. Next
  285. End If
  286. Next
  287. Exit Try
  288. Got:
  289. '从文件中获取 Mod 信息项
  290. If FmlObject.ContainsKey("name") Then Display = FmlObject("name")("value")
  291. If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
  292. '加载成功
  293. GoTo Finished
  294. Catch ex As Exception
  295. Log(ex, $"读取 fml_cache_annotation.json 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
  296. End Try
  297. #End Region
  298. Return '没能成功获取任何信息
  299. Finished:
  300. #Region "将 Version 代号转换为 META-INF 中的版本"
  301. If _Version = "version" Then
  302. Try
  303. Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
  304. If MetaEntry IsNot Nothing Then
  305. Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
  306. If MetaString.Contains("Implementation-Version:") Then
  307. MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
  308. MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
  309. Version = MetaString
  310. End If
  311. End If
  312. Catch ex As Exception
  313. Log($"获取 META-INF 中的版本信息失败({File.FullName})", NotifyLevel.DevelopOnly)
  314. Version = Nothing
  315. End Try
  316. End If
  317. If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
  318. #End Region
  319. RaiseEvent OnUpdate(Me)
  320. End Sub
  321. #End Region
  322. #Region "网络信息"
  323. ''' <summary>
  324. ''' 该 Mod 关联的网络项目。
  325. ''' </summary>
  326. Public Property Project As ResourceProject
  327. Get
  328. Return _Project
  329. End Get
  330. Set(value As ResourceProject)
  331. _Project = value
  332. RaiseEvent OnUpdate(Me)
  333. End Set
  334. End Property
  335. Private _Project As ResourceProject
  336. ''' <summary>
  337. ''' 本地文件对应的联网文件信息。
  338. ''' </summary>
  339. Public ProjectVersion As ResourceVersion
  340. ''' <summary>
  341. ''' 该 Mod 对应的联网最新版本。
  342. ''' </summary>
  343. Public Property UpdateFile As ResourceVersion
  344. Get
  345. Return _UpdateFile
  346. End Get
  347. Set(value As ResourceVersion)
  348. _UpdateFile = value
  349. RaiseEvent OnUpdate(Me)
  350. End Set
  351. End Property
  352. Private _UpdateFile As ResourceVersion
  353. ''' <summary>
  354. ''' 该 Mod 的更新日志网址。
  355. ''' </summary>
  356. Public ChangelogUrls As New List(Of String)
  357. ''' <summary>
  358. ''' 所有网络信息是否已成功加载。
  359. ''' </summary>
  360. Public OnlineDataLoaded As Boolean = False
  361. ''' <summary>
  362. ''' 将网络信息保存为 JSON。
  363. ''' </summary>
  364. Public Function ToJson() As JObject
  365. Dim Json As New JObject
  366. If Project IsNot Nothing Then Json.Add("Project", Project.ToJson())
  367. Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
  368. Json.Add("OnlineDataLoaded", OnlineDataLoaded)
  369. If ProjectVersion IsNot Nothing Then Json.Add("Version", ProjectVersion.ToCacheJson())
  370. If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToCacheJson())
  371. Return Json
  372. End Function
  373. ''' <summary>
  374. ''' 从 JSON 中读取网络信息。
  375. ''' </summary>
  376. Public Sub FromJson(Json As JObject)
  377. OnlineDataLoaded = Json("OnlineDataLoaded")
  378. If Json.ContainsKey("Project") Then Project = New ResourceProject(Json("Project"))
  379. If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
  380. If Json.ContainsKey("Version") Then ProjectVersion = ResourceVersion.FromCacheJson(Json("Version"))
  381. If Json.ContainsKey("UpdateFile") Then UpdateFile = ResourceVersion.FromCacheJson(Json("UpdateFile"))
  382. End Sub
  383. ''' <summary>
  384. ''' 该文件是否可以更新。
  385. ''' </summary>
  386. Public ReadOnly Property HasUpdate As Boolean
  387. Get
  388. Return Not Settings.Get("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
  389. End Get
  390. End Property
  391. ''' <summary>
  392. ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
  393. ''' </summary>
  394. Public ReadOnly Property CurseForgeHash As UInteger
  395. Get
  396. If _CurseForgeHash Is Nothing Then
  397. '读取缓存
  398. Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-C")
  399. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  400. If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
  401. _CurseForgeHash = Cached
  402. Return _CurseForgeHash
  403. End If
  404. '读取文件
  405. Dim data As New List(Of Byte)
  406. For Each b As Byte In ReadFileBytes(File.FullName)
  407. If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
  408. data.Add(b)
  409. Next
  410. '计算 MurmurHash2
  411. Dim length As Integer = data.Count
  412. Dim h As UInteger = 1 Xor length '1 是种子
  413. Dim i As Integer
  414. For i = 0 To length - 4 Step 4
  415. Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
  416. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  417. k = k Xor (k >> 24)
  418. k = (k * &H5BD1E995L) And &HFFFFFFFFL
  419. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  420. h = h Xor k
  421. Next
  422. Select Case length - i
  423. Case 3
  424. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  425. h = h Xor (CUInt(data(i + 2)) << 16)
  426. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  427. Case 2
  428. h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
  429. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  430. Case 1
  431. h = h Xor data(i)
  432. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  433. End Select
  434. h = h Xor (h >> 13)
  435. h = (h * &H5BD1E995L) And &HFFFFFFFFL
  436. h = h Xor (h >> 15)
  437. _CurseForgeHash = h
  438. '写入缓存
  439. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
  440. End If
  441. Return _CurseForgeHash
  442. End Get
  443. End Property
  444. Private _CurseForgeHash As UInteger?
  445. ''' <summary>
  446. ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
  447. ''' </summary>
  448. Public ReadOnly Property ModrinthHash As String
  449. Get
  450. If _ModrinthHash Is Nothing Then
  451. '读取缓存
  452. Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-M")
  453. Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
  454. If Cached <> "" Then
  455. _ModrinthHash = Cached
  456. Return _ModrinthHash
  457. End If
  458. '计算 SHA1
  459. _ModrinthHash = GetFileSHA1(File.FullName)
  460. '写入缓存
  461. WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
  462. End If
  463. Return _ModrinthHash
  464. End Get
  465. End Property
  466. Private _ModrinthHash As String
  467. #End Region
  468. #Region "API"
  469. Public Overrides Function ToString() As String
  470. Return File.FullName
  471. End Function
  472. Public Overrides Function Equals(obj As Object) As Boolean
  473. Dim Target = TryCast(obj, ResourceFile)
  474. Return Target IsNot Nothing AndAlso File.FullName = Target.File.FullName
  475. End Function
  476. #End Region
  477. End Class