ResourceFileLoader.vb 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. Public Module ResourceFileLoader
  2. Private Const ResourceFileCacheVersion As Integer = 16
  3. '加载 Mod 列表
  4. Public McModLoader As New LoaderTask(Of String, List(Of ResourceFile))("Mod List Loader", AddressOf McModLoad)
  5. Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of ResourceFile)))
  6. Try
  7. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
  8. '等待 Mod 更新完成
  9. If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
  10. Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
  11. Try
  12. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
  13. Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
  14. If Loader.IsInterrupted Then Return
  15. Thread.Sleep(100)
  16. Loop
  17. Finally
  18. RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
  19. End Try
  20. FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
  21. End If
  22. '获取 Mod 文件夹下的可用文件列表
  23. Dim ModFileList As New List(Of FileInfo)
  24. If Directory.Exists(Loader.Input) Then
  25. Dim RawName As String = Loader.Input.Lower
  26. For Each File As FileInfo In EnumerateFiles(Loader.Input)
  27. If File.DirectoryName.Lower & "\" <> RawName Then
  28. '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
  29. If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
  30. PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
  31. File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
  32. Continue For
  33. End If
  34. End If
  35. Static IsModFile As Func(Of String, Boolean) =
  36. Function(FullName As String)
  37. If FullName Is Nothing OrElse Not FullName.Contains(".") Then Return False
  38. FullName = FullName.Lower
  39. If FullName.EndsWithF(".jar", True) OrElse FullName.EndsWithF(".zip", True) OrElse FullName.EndsWithF(".litemod", True) OrElse
  40. FullName.EndsWithF(".jar.disabled", True) OrElse FullName.EndsWithF(".zip.disabled", True) OrElse FullName.EndsWithF(".litemod.disabled", True) OrElse
  41. FullName.EndsWithF(".jar.old", True) OrElse FullName.EndsWithF(".zip.old", True) OrElse FullName.EndsWithF(".litemod.old", True) Then Return True
  42. Return False
  43. End Function
  44. If IsModFile(File.FullName) Then ModFileList.Add(File)
  45. Next
  46. End If
  47. '获取本地文件缓存
  48. Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
  49. Dim Cache As New JObject
  50. Try
  51. Dim CacheContent As String = ReadFile(CachePath)
  52. If Not String.IsNullOrWhiteSpace(CacheContent) Then
  53. Cache = GetJson(CacheContent)
  54. If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> ResourceFileCacheVersion Then
  55. Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", NotifyLevel.DebugModeOnly)
  56. Cache = New JObject
  57. End If
  58. End If
  59. Catch ex As Exception
  60. Log(ex, "读取本地 Mod 信息缓存失败,已重置")
  61. Cache = New JObject
  62. End Try
  63. Cache("version") = ResourceFileCacheVersion
  64. '加载 Mod 列表
  65. Dim ModList As New List(Of ResourceFile)
  66. For Each ModFile As FileInfo In ModFileList
  67. If Loader.IsInterrupted Then Return
  68. Dim ModEntry As New ResourceFile(ModFile)
  69. Dim DumpMod As ResourceFile = ModList.FirstOrDefault(Function(m) m.EnabledName = ModEntry.EnabledName) '存在两个文件,名称相同,但一个启用一个禁用
  70. If DumpMod IsNot Nothing Then
  71. Dim DisabledMod As ResourceFile = If(DumpMod.IsEnabled, ModEntry, DumpMod)
  72. Log($"[Mod] 重复的 Mod 文件:{DumpMod.File.Name} 与 {ModEntry.File.Name},已忽略 {DisabledMod.File.Name}", NotifyLevel.DebugModeOnly)
  73. If DisabledMod Is ModEntry Then
  74. Continue For
  75. Else
  76. ModList.Remove(DisabledMod)
  77. End If
  78. End If
  79. ModList.Add(ModEntry)
  80. Next
  81. Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
  82. '排序
  83. ModList = ModList.OrderBy(Function(m) m.File.Name).ToList
  84. '回设
  85. If Loader.IsInterrupted Then Return
  86. Loader.Output = ModList
  87. '开始联网加载
  88. 'TODO: 添加信息获取中提示
  89. McModDetailLoader.Start(New KeyValuePair(Of List(Of ResourceFile), JObject)(ModList, Cache), IsForceRestart:=True)
  90. Catch ex As Exception
  91. Log(ex, "Mod 列表加载失败", NotifyLevel.DebugModeOnly)
  92. Throw
  93. End Try
  94. End Sub
  95. '联网加载 Mod 详情
  96. Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
  97. Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer))
  98. Dim Mods As New List(Of ResourceFile)
  99. Dim Cache As JObject = Loader.Input.Value
  100. Static GetTargetModLoaders As Func(Of List(Of ModLoaderTypes)) =
  101. Function() As List(Of ModLoaderTypes)
  102. Dim Loaders As New List(Of ModLoaderTypes)
  103. If PageInstanceLeft.Instance.Version.HasForge Then Loaders.Add(ModLoaderTypes.Forge)
  104. If PageInstanceLeft.Instance.Version.HasNeoForge Then Loaders.Add(ModLoaderTypes.NeoForge)
  105. If PageInstanceLeft.Instance.Version.HasFabric Then Loaders.Add(ModLoaderTypes.Fabric)
  106. If PageInstanceLeft.Instance.Version.HasLiteLoader Then Loaders.Add(ModLoaderTypes.LiteLoader)
  107. If Not Loaders.Any() Then Loaders.AddRange({ModLoaderTypes.Forge, ModLoaderTypes.NeoForge, ModLoaderTypes.Fabric, ModLoaderTypes.LiteLoader, ModLoaderTypes.Quilt})
  108. Return Loaders
  109. End Function
  110. '读取缓存,获取需要更新的 Mod 列表
  111. For Each ModEntry As ResourceFile In Loader.Input.Key
  112. If Loader.IsInterrupted Then Return
  113. Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
  114. If Cache.ContainsKey(CacheKey) Then
  115. ModEntry.FromJson(Cache(CacheKey))
  116. '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
  117. If ModEntry.OnlineDataLoaded AndAlso Date.Now - Cache(CacheKey)("Project")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
  118. End If
  119. Mods.Add(ModEntry)
  120. Next
  121. Log($"[Mod] 有 {Mods.Where(Function(m) m.Project Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Project IsNot Nothing).Count} 个 Mod 需要更新信息")
  122. If Not Mods.Any Then
  123. Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar()) '从 JAR 中获取缺失的版本信息(下面有另一个分支)
  124. Return
  125. End If
  126. '获取作为检查目标的加载器和版本
  127. '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
  128. Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
  129. Dim ModLoaders = GetTargetModLoaders()
  130. Dim VanillaVersion = TargetMcVersion.VanillaName
  131. '开始网络获取
  132. Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
  133. Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
  134. Dim CurrentTaskThread As Thread = Thread.CurrentThread
  135. '从 Modrinth 获取信息
  136. RunInNewThread(
  137. Sub()
  138. Try
  139. '步骤 1:获取 Hash 与对应的工程 ID
  140. Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
  141. Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
  142. $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
  143. Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
  144. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  145. If ModrinthVersion.Count = 0 Then Return
  146. Dim ModrinthMapping As New Dictionary(Of String, List(Of ResourceFile))
  147. For Each Entry In Mods
  148. If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
  149. If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
  150. Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
  151. If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
  152. If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of ResourceFile)
  153. ModrinthMapping(ProjectId).Add(Entry)
  154. '记录对应的 ProjectVersion
  155. Dim File = ResourceVersion.FromPlatformJson(ModrinthVersion(Entry.ModrinthHash), ResourceTypes.Mod)
  156. If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then
  157. Entry.ProjectVersion = File
  158. Else
  159. Entry.ProjectVersion.Version = File.Version '使用来自 Modrinth 的版本号
  160. End If
  161. Next
  162. If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
  163. Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
  164. '步骤 3:获取工程信息
  165. If Not ModrinthMapping.Any() Then Return
  166. Dim ModrinthProject As JArray = DlModRequest(
  167. $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
  168. For Each ProjectJson In ModrinthProject
  169. Dim Project As New ResourceProject(ProjectJson)
  170. For Each Entry In ModrinthMapping(Project.Id)
  171. Entry.Project = Project
  172. Next
  173. Next
  174. Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
  175. '步骤 4:获取更新信息
  176. Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
  177. $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"",
  178. ""loaders"": [""{ModLoaders.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
  179. For Each Entry In Mods
  180. If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.ProjectVersion Is Nothing Then Continue For
  181. Dim UpdateFile = ResourceVersion.FromPlatformJson(ModrinthUpdate(Entry.ModrinthHash), ResourceTypes.Mod)
  182. If Not UpdateFile.DownloadAvailable Then Continue For
  183. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
  184. If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
  185. '设置更新日志与更新文件
  186. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  187. Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}")
  188. UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
  189. Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
  190. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
  191. Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={VanillaVersion}"}
  192. Entry.UpdateFile = UpdateFile
  193. End If
  194. Next
  195. Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
  196. Catch ex As Exception
  197. Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
  198. IsFailed = True
  199. Finally
  200. EndedThreadCount += 1
  201. End Try
  202. End Sub, "Mod List Detail Loader Modrinth")
  203. '从 CurseForge 获取信息
  204. RunInNewThread(
  205. Sub()
  206. Try
  207. '步骤 1:获取 Hash 与对应的工程 ID
  208. Dim CurseForgeHashes As New List(Of UInteger)
  209. For Each Entry In Mods
  210. CurseForgeHashes.Add(Entry.CurseForgeHash)
  211. If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
  212. Next
  213. Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
  214. $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
  215. Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
  216. '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
  217. If Not CurseForgeRaw.Any() Then Return
  218. Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of ResourceFile))
  219. For Each Project In CurseForgeRaw
  220. Dim ProjectId = Project("id").ToString
  221. Dim Hash As UInteger = Project("file")("fileFingerprint")
  222. For Each Entry In Mods
  223. If Entry.CurseForgeHash <> Hash Then Continue For
  224. If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
  225. If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of ResourceFile)
  226. CurseForgeMapping(ProjectId).Add(Entry)
  227. '记录对应的 ProjectVersion
  228. Dim File = ResourceVersion.FromPlatformJson(Project("file"), ResourceTypes.Mod)
  229. If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File
  230. Next
  231. Next
  232. If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
  233. Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
  234. '步骤 3:获取工程信息
  235. If Not CurseForgeMapping.Any() Then Return
  236. Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
  237. $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
  238. Dim UpdateFileIds As New Dictionary(Of Integer, List(Of ResourceFile)) 'FileId -> 本地 Mod 文件列表
  239. Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
  240. For Each ProjectJson In CurseForgeProject
  241. If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
  242. '设置 Entry 中的工程信息
  243. Dim Project As New ResourceProject(ProjectJson)
  244. For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
  245. If Entry.Project IsNot Nothing AndAlso Entry.Project.Platform = ResourcePlatforms.Modrinth Then
  246. Entry.Project = Entry.Project '再次触发修改事件
  247. Continue For
  248. End If
  249. Entry.Project = Project
  250. Next
  251. '查找或许版本更新的文件列表
  252. If ModLoaders.IsSingle Then
  253. Dim NewestVersion As String = Nothing
  254. Dim NewestFileIds As New List(Of Integer)
  255. For Each IndexEntry In ProjectJson("latestFilesIndexes")
  256. If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
  257. Dim IndexVersion As String = IndexEntry("gameVersion")
  258. If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
  259. '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
  260. If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
  261. If NewestVersion <> IndexVersion Then
  262. NewestVersion = IndexVersion
  263. NewestFileIds.Clear()
  264. End If
  265. NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
  266. Next
  267. For Each FileId In NewestFileIds
  268. If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of ResourceFile)
  269. UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
  270. FileIdToProjectSlug(FileId) = Project.Slug
  271. Next
  272. End If
  273. Next
  274. Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
  275. '步骤 4:获取更新文件信息
  276. If Not UpdateFileIds.Any() Then Return
  277. Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
  278. $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
  279. Dim UpdateFiles As New Dictionary(Of ResourceFile, ResourceVersion)
  280. For Each FileJson In CurseForgeFiles
  281. Dim File = ResourceVersion.FromPlatformJson(FileJson, ResourceTypes.Mod)
  282. If Not File.DownloadAvailable Then Continue For
  283. For Each Entry As ResourceFile In UpdateFileIds(File.Id)
  284. If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
  285. UpdateFiles(Entry) = File
  286. Next
  287. Next
  288. For Each Pair In UpdateFiles
  289. Dim Entry As ResourceFile = Pair.Key
  290. Dim UpdateFile As ResourceVersion = Pair.Value
  291. If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
  292. If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
  293. '设置更新日志与更新文件
  294. If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
  295. Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
  296. Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
  297. ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
  298. Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
  299. Entry.UpdateFile = UpdateFile
  300. End If
  301. Next
  302. Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
  303. Catch ex As Exception
  304. Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
  305. IsFailed = True
  306. Finally
  307. EndedThreadCount += 1
  308. End Try
  309. End Sub, "Mod List Detail Loader CurseForge")
  310. '从 JAR 中获取缺失的版本信息(上面有另一个分支)
  311. Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar())
  312. '等待线程结束
  313. Do Until EndedThreadCount = 2
  314. Thread.Sleep(10)
  315. If Loader.IsInterrupted Then Return
  316. Loop
  317. '保存缓存
  318. Mods = Mods.Where(Function(m) m.Project IsNot Nothing).ToList()
  319. Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
  320. If Not Mods.Any() Then Return
  321. For Each Entry In Mods
  322. Entry.OnlineDataLoaded = Not IsFailed
  323. Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
  324. Next
  325. FileUtils.Write(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
  326. '刷新边栏
  327. If Loader.IsInterrupted Then Return
  328. If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
  329. RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
  330. Else
  331. RunInUi(Sub() FrmInstanceMod?.RefreshBars())
  332. End If
  333. End Sub
  334. End Module