ResourceSearcher.vb 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. Public Class ResourceSearcher
  2. Private Const RESULT_PAGE_SIZE = 40
  3. Private Const MSG_NO_CHINESE_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
  4. '输入
  5. Public Class SearchRequest
  6. '结果要求
  7. ''' <summary>
  8. ''' 结果存储器,会从中获取需加载的页面等。
  9. ''' 加载后也应输出到此处。
  10. ''' </summary>
  11. Public Storage As SearchResult
  12. ''' <summary>
  13. ''' 应当尽量达成的结果数量。
  14. ''' </summary>
  15. Public TargetResultCount As Integer
  16. ''' <summary>
  17. ''' 根据加载位置记录,是否还可以继续获取内容。
  18. ''' </summary>
  19. Public ReadOnly Property CanContinue As Boolean
  20. Get
  21. If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0
  22. If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0
  23. If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
  24. Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
  25. End Get
  26. End Property
  27. '输入内容
  28. ''' <summary>
  29. ''' 筛选资源种类。
  30. ''' </summary>
  31. Public Type As ResourceTypes
  32. ''' <summary>
  33. ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
  34. ''' </summary>
  35. Public Tag As String = ""
  36. ''' <summary>
  37. ''' 筛选 Mod 加载器类别。
  38. ''' </summary>
  39. Public ModLoader As ModLoaderTypes = ModLoaderTypes.Any
  40. ''' <summary>
  41. ''' 筛选 MC 版本。
  42. ''' </summary>
  43. Public GameVersion As String = Nothing
  44. ''' <summary>
  45. ''' 搜索的初始内容。
  46. ''' </summary>
  47. Public SearchText As String = Nothing
  48. ''' <summary>
  49. ''' 允许的来源。
  50. ''' </summary>
  51. Public Sources As ResourcePlatforms = ResourcePlatforms.Any
  52. Public Sub New(Type As ResourceTypes, Storage As SearchResult, TargetResultCount As Integer)
  53. Me.Type = Type
  54. Me.Storage = Storage
  55. Me.TargetResultCount = TargetResultCount
  56. End Sub
  57. '构造请求
  58. ''' <summary>
  59. ''' 获取对应的 CurseForge API 请求链接。
  60. ''' </summary>
  61. Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  62. If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
  63. '应用筛选参数
  64. Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={RESULT_PAGE_SIZE}"
  65. Select Case Type
  66. Case ResourceTypes.Mod
  67. Address += "&classId=6"
  68. Case ResourceTypes.ModPack
  69. Address += "&classId=4471"
  70. Case ResourceTypes.DataPack
  71. Address += "&classId=6945"
  72. Case ResourceTypes.Shader
  73. Address += "&classId=6552"
  74. Case ResourceTypes.ResourcePack
  75. Address += "&classId=12"
  76. End Select
  77. If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
  78. If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
  79. If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
  80. If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & EscapeUtils.UrlEscape(SearchText)
  81. If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
  82. Return Address
  83. End Function
  84. ''' <summary>
  85. ''' 获取对应的 Modrinth API 请求链接。
  86. ''' </summary>
  87. Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  88. If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
  89. '应用筛选参数
  90. Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
  91. If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & EscapeUtils.UrlEscape(SearchText)
  92. If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
  93. 'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
  94. Dim Facets As New List(Of String)
  95. Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
  96. If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
  97. If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).Lower}'""]")
  98. If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
  99. Address += "&facets=[" & String.Join(",", Facets) & "]"
  100. Return Address
  101. End Function
  102. '相同判断
  103. Public Overrides Function Equals(obj As Object) As Boolean
  104. Dim request = TryCast(obj, SearchRequest)
  105. Return request IsNot Nothing AndAlso
  106. Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
  107. Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Sources = request.Sources AndAlso
  108. GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
  109. End Function
  110. Public Shared Operator =(left As SearchRequest, right As SearchRequest) As Boolean
  111. Return EqualityComparer(Of SearchRequest).Default.Equals(left, right)
  112. End Operator
  113. Public Shared Operator <>(left As SearchRequest, right As SearchRequest) As Boolean
  114. Return Not left = right
  115. End Operator
  116. Public Overrides Function GetHashCode() As Integer
  117. Return (Type, Tag, ModLoader, GameVersion, SearchText, Sources).GetHashCode()
  118. End Function
  119. End Class
  120. '输出
  121. Public Class SearchResult
  122. '加载位置记录
  123. Public CurseForgeOffset As Integer = 0
  124. Public CurseForgeTotal As Integer = -1
  125. Public ModrinthOffset As Integer = 0
  126. Public ModrinthTotal As Integer = -1
  127. '结果列表
  128. ''' <summary>
  129. ''' 可供展示的所有工程的列表。
  130. ''' </summary>
  131. Public Results As New List(Of ResourceProject)
  132. ''' <summary>
  133. ''' 当前的错误信息。如果没有则为 Nothing。
  134. ''' </summary>
  135. Public ErrorMessage As String = Nothing
  136. End Class
  137. ''' <summary>
  138. ''' 根据搜索请求获取一系列的工程列表。
  139. ''' </summary>
  140. Public Shared Function GetLoader(InputDelegate As Func(Of SearchRequest)) As LoaderTask(Of SearchRequest, Integer)
  141. Return New LoaderTask(Of SearchRequest, Integer)("社区资源获取", AddressOf Run, InputDelegate) With {.ReloadTimeout = 60 * 1000}
  142. End Function
  143. Private Shared Sub Run(Task As LoaderTask(Of SearchRequest, Integer))
  144. Dim Request As SearchRequest = Task.Input
  145. Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
  146. #Region "前置检查"
  147. If Storage.Results.Count >= Request.TargetResultCount Then
  148. Log($"[Resource] 已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
  149. Return
  150. ElseIf Not Request.CanContinue Then
  151. If Not Storage.Results.Any() Then
  152. Throw New Exception("没有符合条件的结果")
  153. Else
  154. Log($"[Resource] 已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
  155. Return
  156. End If
  157. End If
  158. '拒绝 1.13- Quilt(这个版本根本没有 Quilt)
  159. If Request.ModLoader = ModLoaderTypes.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
  160. Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
  161. End If
  162. #End Region
  163. #Region "中文搜索"
  164. Dim RawSearchText As String = If(Request.SearchText, "").Trim
  165. RawSearchText = RawSearchText.Lower
  166. Log("[Resource] 工程列表搜索原始文本:" & RawSearchText)
  167. Dim IsChineseSearch As Boolean =
  168. RegexCheck(RawSearchText, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
  169. (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
  170. Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
  171. If IsChineseSearch Then
  172. '帮助方法:从搜索项提取可能的英文单词
  173. Dim ExtractWords =
  174. Function(Result As SearchEntry(Of WikiEntry), Source As ResourcePlatforms) As IEnumerable(Of String)
  175. '从各个可能的来源提取候选
  176. Dim Candidates As New List(Of String)
  177. If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = ResourcePlatforms.CurseForge Then
  178. Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
  179. End If
  180. If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = ResourcePlatforms.Modrinth Then
  181. Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
  182. End If
  183. Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
  184. Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
  185. '分词、清洗、去重
  186. Candidates = Candidates.
  187. SelectMany(Function(c) c.Split(" ")).
  188. Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
  189. Where(
  190. Function(w)
  191. If w.Length <= 1 Then Return False '单字或空白
  192. If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
  193. If Val(w) > 0 Then Return False '数字
  194. Return True
  195. End Function).Distinct.ToList
  196. '如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
  197. Dim CanForm As Func(Of String, Boolean) = Nothing
  198. CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
  199. Candidates = Candidates.Where(Function(w) Not Candidates.Any(Function(c) c.Length < w.Length AndAlso w.StartsWith(c) AndAlso CanForm(w.Substring(c.Length)))).ToList()
  200. Return Candidates
  201. End Function
  202. 'CurseForge
  203. If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) Then
  204. '数据库搜索
  205. Static CurseForgeSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
  206. WikiEntry.All.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
  207. Function(Entry) New SearchEntry(Of WikiEntry) With {
  208. .Item = Entry,
  209. .SearchSource = New List(Of SearchSource) From {
  210. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
  211. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
  212. }).ToList
  213. Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
  214. If CurseForgeSearchResults.Any Then
  215. '选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
  216. Dim CurseForgeTarget =
  217. If(CurseForgeSearchResults.First.AbsoluteRight,
  218. CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
  219. CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
  220. MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
  221. '后处理
  222. CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, ResourcePlatforms.CurseForge).Join(" ")
  223. Log("[Resource] 中文搜索关键词(CurseForge):" & CurseForgeAltSearchText, NotifyLevel.DebugModeOnly)
  224. End If
  225. End If
  226. 'Modrinth
  227. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) Then
  228. '数据库搜索
  229. Static ModrinthSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
  230. WikiEntry.All.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
  231. Function(Entry) New SearchEntry(Of WikiEntry) With {
  232. .Item = Entry,
  233. .SearchSource = New List(Of SearchSource) From {
  234. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
  235. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
  236. }).ToList
  237. Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
  238. If ModrinthSearchResults.Any Then
  239. '分词
  240. Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
  241. For Each Result In ModrinthSearchResults
  242. For Each Word In ExtractWords(Result, ResourcePlatforms.Modrinth)
  243. If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
  244. Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
  245. WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
  246. Next
  247. Next
  248. ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
  249. Log("[Resource] 中文搜索关键词(Modrinth):" & ModrinthAltSearchText, NotifyLevel.DebugModeOnly)
  250. '直接请求工程
  251. ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
  252. End If
  253. End If
  254. '结束
  255. If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
  256. Throw New Exception(MSG_NO_CHINESE_RESULT)
  257. End If
  258. End If
  259. Task.Progress = 0.05
  260. #End Region
  261. Dim RealResults As New List(Of ResourceProject)
  262. NextPage:
  263. Dim RawResults As New ConcurrentList(Of ResourceProject)
  264. #Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
  265. Dim WorkThreads As New List(Of Thread)
  266. Dim Errors As New ConcurrentList(Of (Ex As Exception, Source As ResourcePlatforms))
  267. '在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
  268. '所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
  269. Dim IgnoreModLoaderFilter = Request.ModLoader = ModLoaderTypes.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
  270. Try
  271. 'CurseForge 搜索
  272. If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) AndAlso
  273. Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
  274. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  275. WorkThreads.Add(RunInNewThread(
  276. Sub()
  277. Try
  278. '获取工程列表
  279. Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  280. Log("[Resource] 开始 CurseForge 搜索:" & CurseForgeUrl)
  281. Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
  282. Dim ProjectList As New List(Of ResourceProject)
  283. For Each JsonEntry As JObject In RequestResult("data")
  284. Dim Project As New ResourceProject(JsonEntry)
  285. If Request.Type = ResourceTypes.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
  286. ProjectList.Add(Project)
  287. Next
  288. '更新结果
  289. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  290. Storage.CurseForgeOffset += RequestResult("data").Count
  291. Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
  292. Log($"[Resource] 从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
  293. Catch ex As Exception
  294. Log(ex, "CurseForge 搜索失败")
  295. Storage.CurseForgeTotal = -1
  296. Errors.Add((ex, ResourcePlatforms.CurseForge))
  297. End Try
  298. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  299. End Sub, "CurseForge Search"))
  300. End If
  301. 'Modrinth 搜索
  302. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
  303. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  304. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  305. WorkThreads.Add(RunInNewThread(
  306. Sub()
  307. Try
  308. Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  309. Log("[Resource] 开始 Modrinth 搜索:" & ModrinthUrl)
  310. Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
  311. Dim ProjectList As New List(Of ResourceProject)
  312. For Each JsonEntry As JObject In RequestResult("hits")
  313. ProjectList.Add(New ResourceProject(JsonEntry))
  314. Next
  315. '更新结果
  316. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  317. Storage.ModrinthOffset += RequestResult("hits").Count
  318. Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
  319. Log($"[Resource] 从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
  320. Catch ex As Exception
  321. Log(ex, "Modrinth 搜索失败")
  322. Storage.ModrinthTotal = -1
  323. Errors.Add((ex, ResourcePlatforms.Modrinth))
  324. End Try
  325. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  326. End Sub, "Modrinth Search"))
  327. End If
  328. 'Modrinth 直接获取工程
  329. If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
  330. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  331. ModrinthSlugs.Any Then '有直接获取的 Slug
  332. WorkThreads.Add(RunInNewThread(
  333. Sub()
  334. Try
  335. Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
  336. Log("[Resource] 开始 Modrinth 直接获取:" & ModrinthUrl)
  337. Dim ProjectList As New List(Of ResourceProject)
  338. For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
  339. Dim Project As New ResourceProject(JsonEntry)
  340. '应用筛选
  341. If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
  342. If Not String.IsNullOrEmpty(Request.Tag) AndAlso
  343. Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
  344. If Request.ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter AndAlso
  345. Not Project.Loaders.Any(Function(m) m = Request.ModLoader) Then Continue For
  346. If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
  347. Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
  348. ProjectList.Add(Project)
  349. Next
  350. '更新结果
  351. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  352. Log($"[Resource] 从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
  353. ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
  354. Catch ex As Exception
  355. Log(ex, "Modrinth 直接获取失败")
  356. Errors.Add((ex, ResourcePlatforms.Modrinth))
  357. End Try
  358. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  359. End Sub, "Modrinth Get"))
  360. End If
  361. '等待线程结束
  362. For Each Thread In WorkThreads
  363. Thread.Join()
  364. If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
  365. Next
  366. '筛除不是 Forge 的 Mod
  367. If IgnoreModLoaderFilter Then
  368. RawResults = RawResults.Where(Function(p) Not p.Loaders.Any() OrElse p.Loaders.Contains(ModLoaderTypes.Forge)).ToList
  369. End If
  370. '确保存在结果
  371. Storage.ErrorMessage = Nothing
  372. If Not RawResults.Any() Then
  373. If Errors.Any() Then
  374. Throw Errors.First.Ex
  375. Else
  376. If IsChineseSearch AndAlso Not (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) Then
  377. Throw New Exception(MSG_NO_CHINESE_RESULT)
  378. ElseIf Request.Sources = ResourcePlatforms.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
  379. Throw New Exception("CurseForge 不兼容所选的类型")
  380. ElseIf Request.Sources = ResourcePlatforms.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
  381. Throw New Exception("Modrinth 不兼容所选的类型")
  382. Else
  383. Throw New Exception("没有搜索结果")
  384. End If
  385. End If
  386. ElseIf Errors.Any() Then
  387. '有结果但是有错误
  388. If Errors.Any(Function(e) e.Source = ResourcePlatforms.CurseForge) Then
  389. Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  390. Else
  391. Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  392. End If
  393. End If
  394. Finally
  395. For Each Thread In WorkThreads
  396. If Thread.IsAlive Then Thread.Interrupt()
  397. Next
  398. End Try
  399. #End Region
  400. #Region "提取非重复项,存储于 RealResults"
  401. '将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
  402. RawResults = RawResults.Where(Function(x) x.Platform = ResourcePlatforms.CurseForge).Concat(RawResults.Where(Function(x) x.Platform <> ResourcePlatforms.CurseForge)).ToList
  403. '去重
  404. RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b)).ToList
  405. '已有内容去重
  406. RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
  407. Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
  408. '加入列表
  409. RealResults.AddRange(RawResults)
  410. Log($"[Resource] 去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
  411. #End Region
  412. #Region "检查结果数量,如果不足且可继续,会继续加载下一页"
  413. If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
  414. Log($"[Resource] 总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
  415. If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
  416. Log("[Resource] 将继续尝试加载下一页")
  417. GoTo NextPage
  418. Else
  419. Log("[Resource] 无法继续加载,将强制结束")
  420. End If
  421. End If
  422. #End Region
  423. #Region "将结果排序并添加"
  424. Dim Scores As New Dictionary(Of ResourceProject, Double) '排序分
  425. Dim GetDownloadCountMult =
  426. Function(Project As ResourceProject) As Double
  427. Select Case Request.Type
  428. Case ResourceTypes.Mod, ResourceTypes.ModPack
  429. Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 5)
  430. Case ResourceTypes.DataPack
  431. Return If(Project.Platform = ResourcePlatforms.CurseForge, 10, 1)
  432. Case ResourceTypes.ResourcePack, ResourceTypes.Shader
  433. Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 4)
  434. Case Else
  435. Return 1
  436. End Select
  437. End Function
  438. If String.IsNullOrEmpty(RawSearchText) Then
  439. '如果没有搜索文本,按下载量将结果排序
  440. For Each Result As ResourceProject In RealResults
  441. Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
  442. Next
  443. Else
  444. '如果有搜索文本,按关联度将结果排序
  445. '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
  446. Dim SearchEntries As New List(Of SearchEntry(Of ResourceProject))
  447. For Each Result As ResourceProject In RealResults
  448. Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
  449. Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
  450. SearchEntries.Add(New SearchEntry(Of ResourceProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
  451. New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1),
  452. New SearchSource(Result.Description, 0.05)}})
  453. Next
  454. Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
  455. For Each OneResult In SearchResult
  456. Scores(OneResult.Item) +=
  457. If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
  458. If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
  459. Next
  460. End If
  461. '根据排序分得出结果并添加
  462. If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
  463. Storage.Results.AddRange(
  464. Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
  465. #End Region
  466. End Sub
  467. End Class