LocalResourceLoaders.vb 22 KB

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