ModComp.vb 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403
  1. Public Module ModComp
  2. Public Enum CompType
  3. ''' <summary>
  4. ''' Mod。
  5. ''' </summary>
  6. [Mod]
  7. ''' <summary>
  8. ''' 整合包。
  9. ''' </summary>
  10. ModPack
  11. ''' <summary>
  12. ''' 资源包。
  13. ''' </summary>
  14. ResourcePack
  15. End Enum
  16. Public Enum CompModLoaderType
  17. 'https://docs.curseforge.com/?http#tocS_ModLoaderType
  18. Any = 0
  19. Forge = 1
  20. Fabric = 4
  21. Quilt = 5
  22. End Enum
  23. <Flags> Public Enum CompSourceType
  24. CurseForge = 1
  25. Modrinth = 2
  26. Any = CurseForge Or Modrinth
  27. End Enum
  28. #Region "CompDatabase | Mod 数据库"
  29. Private _CompDatabase As List(Of CompDatabaseEntry) = Nothing
  30. Private ReadOnly Property CompDatabase As List(Of CompDatabaseEntry)
  31. Get
  32. If _CompDatabase IsNot Nothing Then Return _CompDatabase
  33. '初始化数据库
  34. _CompDatabase = New List(Of CompDatabaseEntry)
  35. Dim i As Integer = 0
  36. For Each Line In DecodeBytes(GetResources("ModData")).Replace(vbCrLf, vbLf).Replace(vbCr, "").Split(vbLf)
  37. i += 1
  38. If Line = "" Then Continue For
  39. For Each EntryData As String In Line.Split("¨")
  40. Dim Entry = New CompDatabaseEntry
  41. Dim SplitedLine = EntryData.Split("|")
  42. If SplitedLine(0).StartsWith("@") Then
  43. Entry.CurseForgeSlug = Nothing
  44. Entry.ModrinthSlug = SplitedLine(0).Replace("@", "")
  45. ElseIf SplitedLine(0).EndsWith("@") Then
  46. Entry.CurseForgeSlug = SplitedLine(0).TrimEnd("@")
  47. Entry.ModrinthSlug = Entry.CurseForgeSlug
  48. ElseIf SplitedLine(0).Contains("@") Then
  49. Entry.CurseForgeSlug = SplitedLine(0).Split("@")(0)
  50. Entry.ModrinthSlug = SplitedLine(0).Split("@")(1)
  51. Else
  52. Entry.CurseForgeSlug = SplitedLine(0)
  53. Entry.ModrinthSlug = Nothing
  54. End If
  55. Entry.WikiId = i
  56. If SplitedLine.Count >= 2 Then
  57. Entry.ChineseName = SplitedLine(1)
  58. Entry.MCBBS = If(SplitedLine.Count >= 3, SplitedLine(2), Nothing)
  59. If Entry.ChineseName.Contains("*") Then '处理 *
  60. Entry.ChineseName = Entry.ChineseName.Replace("*", " (" &
  61. String.Join(" ", If(Entry.CurseForgeSlug, Entry.ModrinthSlug).Split("-").Select(Function(w) w.Substring(0, 1).ToUpper & w.Substring(1, w.Length - 1))) & ")")
  62. End If
  63. End If
  64. _CompDatabase.Add(Entry)
  65. Next
  66. Next
  67. Return _CompDatabase
  68. End Get
  69. End Property
  70. Private Class CompDatabaseEntry
  71. ''' <summary>
  72. ''' McMod 的对应 ID。
  73. ''' </summary>
  74. Public WikiId As Integer
  75. ''' <summary>
  76. ''' 中文译名。空字符串代表没有翻译。
  77. ''' </summary>
  78. Public ChineseName As String = ""
  79. ''' <summary>
  80. ''' MCBBS 发布帖编号(例如 195107)。若为 Nothing 则没有发布帖。
  81. ''' </summary>
  82. Public MCBBS As Integer?
  83. ''' <summary>
  84. ''' CurseForge Slug(例如 advanced-solar-panels)。
  85. ''' </summary>
  86. Public CurseForgeSlug As String = Nothing
  87. ''' <summary>
  88. ''' Modrinth Slug(例如 advanced-solar-panels)。
  89. ''' </summary>
  90. Public ModrinthSlug As String = Nothing
  91. Public Overrides Function ToString() As String
  92. Return If(CurseForgeSlug, "") & "&" & If(ModrinthSlug, "") & "|" & WikiId & "|" & ChineseName & If(MCBBS Is Nothing, "", "|" & MCBBS)
  93. End Function
  94. End Class
  95. #End Region
  96. #Region "CompProject | 工程信息"
  97. '类定义
  98. Public Class CompProject
  99. '源信息
  100. ''' <summary>
  101. ''' 该工程信息来自 CurseForge 还是 Modrinth。
  102. ''' </summary>
  103. Public ReadOnly FromCurseForge As Boolean
  104. ''' <summary>
  105. ''' 工程的种类。
  106. ''' </summary>
  107. Public ReadOnly Type As CompType
  108. ''' <summary>
  109. ''' 工程的短名。例如 technical-enchant。
  110. ''' </summary>
  111. Public ReadOnly Slug As String
  112. ''' <summary>
  113. ''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
  114. ''' </summary>
  115. Public ReadOnly Id As String
  116. ''' <summary>
  117. ''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
  118. ''' </summary>
  119. Public ReadOnly CurseForgeFileIds As List(Of Integer)
  120. '描述性信息
  121. ''' <summary>
  122. ''' 原始的英文名称。
  123. ''' </summary>
  124. Public ReadOnly RawName As String
  125. ''' <summary>
  126. ''' 英文描述。
  127. ''' </summary>
  128. Public ReadOnly Description As String
  129. ''' <summary>
  130. ''' 来源网站的工程页面网址。确保格式一定标准。
  131. ''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
  132. ''' Modrinth:https://modrinth.com/mod/technical-enchant
  133. ''' </summary>
  134. Public ReadOnly Website As String
  135. ''' <summary>
  136. ''' 最后一次更新的时间。可能为 Nothing。
  137. ''' </summary>
  138. Public ReadOnly LastUpdate As Date? = Nothing
  139. ''' <summary>
  140. ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
  141. ''' </summary>
  142. Public ReadOnly DownloadCount As Integer
  143. ''' <summary>
  144. ''' 支持的 Mod 加载器列表。可能为空。
  145. ''' </summary>
  146. Public ReadOnly ModLoaders As List(Of CompModLoaderType)
  147. ''' <summary>
  148. ''' 描述性标签的内容。已转换为中文。
  149. ''' </summary>
  150. Public ReadOnly Tags As List(Of String)
  151. ''' <summary>
  152. ''' Logo 图片的下载地址。若为 Nothing 则没有。
  153. ''' </summary>
  154. Public ReadOnly LogoUrl As String = Nothing
  155. ''' <summary>
  156. ''' 游戏大版本列表。例如:18, 16, 15……
  157. ''' </summary>
  158. Public ReadOnly GameVersions As List(Of Integer)
  159. '数据库信息
  160. Private LoadedDatabase As Boolean = False
  161. Private _DatabaseEntry As CompDatabaseEntry = Nothing
  162. ''' <summary>
  163. ''' 关联的数据库条目。若为 Nothing 则没有。
  164. ''' </summary>
  165. Private Property DatabaseEntry As CompDatabaseEntry
  166. Get
  167. If Not LoadedDatabase Then
  168. LoadedDatabase = True
  169. If Type = CompType.Mod Then _DatabaseEntry = CompDatabase.FirstOrDefault(Function(c) If(FromCurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
  170. End If
  171. Return _DatabaseEntry
  172. End Get
  173. Set(value As CompDatabaseEntry)
  174. LoadedDatabase = True
  175. _DatabaseEntry = value
  176. End Set
  177. End Property
  178. ''' <summary>
  179. ''' MC 百科的页面 ID。若为 0 则没有。
  180. ''' </summary>
  181. Public ReadOnly Property WikiId As Integer
  182. Get
  183. Return If(DatabaseEntry Is Nothing, 0, DatabaseEntry.WikiId)
  184. End Get
  185. End Property
  186. ''' <summary>
  187. ''' MCBBS 的帖子 ID。若为 0 则没有。
  188. ''' </summary>
  189. Public ReadOnly Property MCBBS As Integer
  190. Get
  191. Return If(DatabaseEntry Is Nothing, 0, If(DatabaseEntry.MCBBS, 0))
  192. End Get
  193. End Property
  194. ''' <summary>
  195. ''' 翻译后的中文名。若数据库没有则等同于 RawName。
  196. ''' </summary>
  197. Public ReadOnly Property TranslatedName As String
  198. Get
  199. Return If(DatabaseEntry Is Nothing OrElse DatabaseEntry.ChineseName = "", RawName, DatabaseEntry.ChineseName)
  200. End Get
  201. End Property
  202. '实例化
  203. ''' <summary>
  204. ''' 从工程 Json 中初始化实例。若出错会抛出异常。
  205. ''' </summary>
  206. Public Sub New(Data As JObject)
  207. FromCurseForge = Data.ContainsKey("gameId")
  208. If FromCurseForge Then
  209. #Region "CurseForge"
  210. '简单信息
  211. Id = Data("id")
  212. Slug = Data("slug")
  213. RawName = Data("name")
  214. Description = Data("summary")
  215. Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
  216. LastUpdate = Data("dateReleased") '#1194
  217. DownloadCount = Data("downloadCount")
  218. 'Logo
  219. If Data("logo").Count > 0 Then
  220. If GetPixelSize(1) > 1.25 Then
  221. LogoUrl = Data("logo")("thumbnailUrl").ToString '使用 256x256 图标
  222. Else
  223. LogoUrl = Data("logo")("thumbnailUrl").ToString.Replace("/256/256/", "/64/64/") '使用 64x64 图标
  224. End If
  225. End If
  226. 'FileIndexes / GameVersions
  227. Dim Files = If(CType(Data("latestFilesIndexes"), JArray), New JArray).
  228. Where(Function(i) i("gameVersion").ToString.Contains("1."))
  229. CurseForgeFileIds = Files.Select(Of Integer)(Function(v) v("fileId")).ToList
  230. GameVersions = Files.Select(Of Integer)(Function(v) Val(v("gameVersion").ToString.Split(".")(1).Split("-").First)).Where(Function(v) v > 0).
  231. Distinct.OrderByDescending(Function(v) v).ToList
  232. 'Type
  233. If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
  234. Type = CompType.Mod
  235. ElseIf Website.Contains("/modpacks/") Then
  236. Type = CompType.ModPack
  237. Else
  238. Type = CompType.ResourcePack
  239. End If
  240. 'Tags
  241. Tags = New List(Of String)
  242. For Each Category In Data("categories").Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
  243. Select Case Category
  244. 'Mod
  245. Case 406 : Tags.Add("世界元素")
  246. Case 407 : Tags.Add("生物群系")
  247. Case 410 : Tags.Add("维度")
  248. Case 408 : Tags.Add("矿物/资源")
  249. Case 409 : Tags.Add("天然结构")
  250. Case 412 : Tags.Add("科技")
  251. Case 415 : Tags.Add("管道/物流")
  252. Case 4843 : Tags.Add("自动化")
  253. Case 417 : Tags.Add("能源")
  254. Case 4558 : Tags.Add("红石")
  255. Case 436 : Tags.Add("食物/烹饪")
  256. Case 416 : Tags.Add("农业")
  257. Case 414 : Tags.Add("运输")
  258. Case 420 : Tags.Add("仓储")
  259. Case 419 : Tags.Add("魔法")
  260. Case 422 : Tags.Add("冒险")
  261. Case 424 : Tags.Add("装饰")
  262. Case 411 : Tags.Add("生物")
  263. Case 434 : Tags.Add("装备")
  264. Case 423 : Tags.Add("信息显示")
  265. Case 435 : Tags.Add("服务器")
  266. Case 5191 : Tags.Add("改良")
  267. Case 421 : Tags.Add("支持库")
  268. '整合包
  269. Case 4484 : Tags.Add("多人")
  270. Case 4479 : Tags.Add("硬核")
  271. Case 4483 : Tags.Add("战斗")
  272. Case 4478 : Tags.Add("任务")
  273. Case 4472 : Tags.Add("科技")
  274. Case 4473 : Tags.Add("魔法")
  275. Case 4475 : Tags.Add("冒险")
  276. Case 4476 : Tags.Add("探索")
  277. Case 4477 : Tags.Add("小游戏")
  278. Case 4471 : Tags.Add("科幻")
  279. Case 4736 : Tags.Add("空岛")
  280. Case 5128 : Tags.Add("原版改良")
  281. Case 4487 : Tags.Add("FTB")
  282. Case 4480 : Tags.Add("基于地图")
  283. Case 4481 : Tags.Add("轻量")
  284. Case 4482 : Tags.Add("大型")
  285. 'FUTURE: Res
  286. End Select
  287. Next
  288. If Tags.Count = 0 Then Tags.Add("杂项")
  289. 'ModLoaders
  290. ModLoaders = New List(Of CompModLoaderType)
  291. For Each File In If(Data("latestFiles"), {})
  292. Dim NewFile As New CompFile(File, Type)
  293. If Not NewFile.Available Then Continue For
  294. ModLoaders.AddRange(NewFile.ModLoaders)
  295. Next
  296. ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
  297. #End Region
  298. Else
  299. #Region "Modrinth"
  300. '简单信息
  301. Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
  302. Slug = Data("slug")
  303. RawName = Data("title")
  304. Description = Data("description")
  305. LastUpdate = Data("date_modified")
  306. DownloadCount = Data("downloads")
  307. LogoUrl = Data("icon_url")
  308. If LogoUrl = "" Then LogoUrl = Nothing
  309. Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
  310. 'GameVersions
  311. GameVersions = If(CType(Data("versions"), JArray), New JArray).
  312. Select(Function(v) v.ToString).Where(Function(v) v.StartsWith("1.")).
  313. Select(Of Integer)(Function(v) Val(v.Split(".")(1).Split("-").First)).Where(Function(v) v > 0).
  314. Distinct.OrderByDescending(Function(v) v).ToList
  315. 'Type
  316. Select Case Data("project_type").ToString
  317. Case "mod" : Type = CompType.Mod
  318. Case "modpack" : Type = CompType.ModPack
  319. Case "resourcepack" : Type = CompType.ResourcePack
  320. End Select
  321. 'Tags & ModLoaders
  322. Tags = New List(Of String)
  323. ModLoaders = New List(Of CompModLoaderType)
  324. For Each Category In Data("categories").Select(Function(t) t.ToString)
  325. Select Case Category
  326. '加载器
  327. Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
  328. Case "fabric" : ModLoaders.Add(CompModLoaderType.Fabric)
  329. Case "quilt" : ModLoaders.Add(CompModLoaderType.Quilt)
  330. 'Mod
  331. Case "worldgen" : Tags.Add("世界元素")
  332. Case "technology" : Tags.Add("科技")
  333. Case "food" : Tags.Add("食物/烹饪")
  334. Case "game-mechanics" : Tags.Add("游戏机制")
  335. Case "transportation" : Tags.Add("运输")
  336. Case "storage" : Tags.Add("仓储")
  337. Case "magic" : Tags.Add("魔法")
  338. Case "adventure" : Tags.Add("冒险")
  339. Case "decoration" : Tags.Add("装饰")
  340. Case "mobs" : Tags.Add("生物")
  341. Case "equipment" : Tags.Add("装备")
  342. Case "optimization" : Tags.Add("性能优化")
  343. Case "social" : Tags.Add("服务器")
  344. Case "utility" : Tags.Add("改良")
  345. Case "library" : Tags.Add("支持库")
  346. '整合包
  347. Case "multiplayer" : Tags.Add("多人")
  348. Case "optimization" : Tags.Add("性能优化")
  349. Case "challenging" : Tags.Add("硬核")
  350. Case "combat" : Tags.Add("战斗")
  351. Case "quests" : Tags.Add("任务")
  352. Case "technology" : Tags.Add("科技")
  353. Case "magic" : Tags.Add("魔法")
  354. Case "adventure" : Tags.Add("冒险")
  355. Case "kitchen-sink" : Tags.Add("烹饪")
  356. Case "lightweight" : Tags.Add("轻量")
  357. 'FUTURE: Res
  358. End Select
  359. Next
  360. If Tags.Count = 0 Then Tags.Add("杂项")
  361. Tags.Sort()
  362. ModLoaders.Sort()
  363. #End Region
  364. End If
  365. '保存缓存
  366. CompProjectCache(Id) = Me
  367. End Sub
  368. ''' <summary>
  369. ''' 将当前工程信息实例化为控件。
  370. ''' </summary>
  371. Public Function ToCompItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean, Optional OnClick As MyCompItem.ClickEventHandler = Nothing) As MyCompItem
  372. '获取版本描述
  373. Dim GameVersionDescription As String
  374. If GameVersions Is Nothing OrElse GameVersions.Count = 0 Then
  375. GameVersionDescription = "未知"
  376. Else
  377. Dim SpaVersions As New List(Of String)
  378. For i = 0 To GameVersions.Count - 1
  379. Dim StartVersion As Integer = GameVersions(i), EndVersion As Integer = GameVersions(i)
  380. For ii = i + 1 To GameVersions.Count - 1
  381. If GameVersions(ii) = EndVersion - 1 Then
  382. EndVersion = GameVersions(ii)
  383. i = ii
  384. Else
  385. Exit For
  386. End If
  387. Next
  388. If StartVersion = EndVersion Then
  389. SpaVersions.Add("1." & StartVersion)
  390. ElseIf McVersionHighest > -1 AndAlso StartVersion >= McVersionHighest Then
  391. SpaVersions.Add("1." & EndVersion & "+")
  392. ElseIf EndVersion <= 7 Then
  393. SpaVersions.Add("1." & StartVersion & "-")
  394. Exit For
  395. ElseIf StartVersion - EndVersion = 1 Then
  396. SpaVersions.Add("1." & StartVersion & ", 1." & EndVersion)
  397. Else
  398. SpaVersions.Add("1." & StartVersion & "~1." & EndVersion)
  399. End If
  400. Next
  401. GameVersionDescription = SpaVersions.Join(", ")
  402. End If
  403. '实例化 UI
  404. Dim NewItem As New MyCompItem With {.Tag = Me}
  405. '整理 FinalName 与 ExNameList
  406. '检查下列代码时可以参考 #1567 的测试例
  407. Dim FinalName As String = RawName
  408. Dim ExNameList As List(Of String)
  409. If TranslatedName = RawName Then
  410. '没有中文翻译
  411. '将所有名称分段
  412. Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
  413. Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
  414. If NameLists.Count = 1 Then GoTo NoExName
  415. '查找其中的缩写、Forge/Fabric 等版本标记
  416. ExNameList = New List(Of String)
  417. Dim NormalNameList = New List(Of String)
  418. For Each Name In NameLists
  419. Dim LowerName As String = Name.ToLower
  420. If Name.ToUpper = Name AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
  421. '缩写
  422. ExNameList.Add(Name)
  423. ElseIf (LowerName.Contains("forge") OrElse LowerName.Contains("fabric") OrElse LowerName.Contains("quilt")) AndAlso
  424. Not RegexCheck(LowerName.Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母
  425. 'Forge/Fabric 等版本标记
  426. ExNameList.Add(Name)
  427. Else
  428. '其他部分
  429. NormalNameList.Add(Name)
  430. End If
  431. Next
  432. '根据分类后的结果处理
  433. If NormalNameList.Count = 0 OrElse ExNameList.Count = 0 Then GoTo NoExName
  434. '同时包含 NormalName 和 ExName
  435. FinalName = NormalNameList.Join(" - ")
  436. ExNameList = ExNameList
  437. Else
  438. '有中文翻译
  439. '尝试将文本分为三段:FinalName (EnglishName) - Suffix
  440. '检查时注意 Carpet:它没有中文译名,但有 Suffix
  441. FinalName = TranslatedName.Split(" (").First.Split(" - ").First
  442. Dim Suffix As String = ""
  443. If TranslatedName.Split(")").Last.Contains(" - ") Then Suffix = TranslatedName.Split(")").Last.Split(" - ").Last
  444. Dim EnglishName As String = TranslatedName
  445. If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
  446. EnglishName = EnglishName.Replace(FinalName, "").Trim("("c, ")"c, " "c)
  447. '中段的额外信息截取
  448. ExNameList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
  449. Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
  450. If ExNameList.Count > 1 AndAlso
  451. Not ExNameList.Any(Function(s) s.ToLower.Contains("forge") OrElse s.ToLower.Contains("fabric") OrElse s.ToLower.Contains("quilt")) AndAlso '不是标注 XX 版
  452. Not (ExNameList.Count = 2 AndAlso ExNameList.Last.ToUpper = ExNameList.Last) Then '不是缩写
  453. ExNameList = New List(Of String) From {EnglishName} '使用原名
  454. End If
  455. '添加后缀
  456. If Suffix <> "" Then ExNameList.Add(Suffix)
  457. End If
  458. ExNameList = ExNameList.Distinct().ToList()
  459. '显示 FinalName 与 ExNameList
  460. If ExNameList.Count = 0 Then
  461. NoExName:
  462. NewItem.LabTitle.Text = FinalName
  463. CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
  464. Else
  465. NewItem.LabTitle.Text = FinalName
  466. Dim ExName As String = ""
  467. For Each Ex In ExNameList
  468. '去除 “Forge/Fabric” 这一无意义提示
  469. If Ex.Length < 16 AndAlso Ex.ToLower.Contains("fabric") AndAlso Ex.ToLower.Contains("forge") Then Continue For
  470. '将 “Forge” 等提示改为 “Forge 版”
  471. If (Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")) AndAlso Not Ex.Contains("版") AndAlso
  472. Ex.ToLower.Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
  473. Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
  474. End If
  475. '将 “forge” 等词语的首字母大写
  476. Ex = Ex.Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
  477. ExName &= " | " & Ex.Trim
  478. Next
  479. If ExName = "" Then GoTo NoExName
  480. NewItem.LabTitleRaw.Text = ExName
  481. End If
  482. 'Tag
  483. For Each TagText In Tags
  484. Dim NewTag = GetObjectFromXML(
  485. "<Border xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
  486. Background=""#11000000"" Padding=""3,1"" CornerRadius=""3"" Margin=""0,0,3,0""
  487. SnapsToDevicePixels=""True"" UseLayoutRounding=""False"">
  488. <TextBlock Text=""" & TagText & """ Foreground=""#868686"" FontSize=""11"" />
  489. </Border>")
  490. NewItem.PanTags.Children.Add(NewTag)
  491. Next
  492. '下边栏
  493. If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
  494. '全部隐藏
  495. CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
  496. CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
  497. NewItem.ColumnVersion1.Width = New GridLength(0)
  498. NewItem.ColumnVersion2.MaxWidth = 0
  499. NewItem.ColumnVersion3.Width = New GridLength(0)
  500. ElseIf ShowMcVersionDesc AndAlso ShowMcVersionDesc Then
  501. '全部显示
  502. Dim ModLoaderDesc As String = If(ModLoaders.Count > 0 AndAlso ModLoaders.Count < 3 AndAlso
  503. Not (Type = CompType.Mod AndAlso Setup.Get("ToolDownloadIgnoreQuilt") AndAlso ModLoaders.Contains(CompModLoaderType.Forge) AndAlso ModLoaders.Contains(CompModLoaderType.Fabric)),
  504. ModLoaders.Join(" / ") & " ", "")
  505. Dim VersionDesc As String = If(GameVersionDescription = "1.7+", "全版本", GameVersionDescription)
  506. NewItem.LabVersion.Text = ModLoaderDesc & VersionDesc
  507. ElseIf ShowMcVersionDesc Then
  508. '仅显示版本
  509. NewItem.LabVersion.Text = If(GameVersionDescription = "1.7+", "全版本", GameVersionDescription)
  510. Else
  511. '仅显示 Mod 加载器
  512. Select Case ModLoaders.Count
  513. Case 0
  514. NewItem.LabVersion.Text = "未知"
  515. Case 1
  516. NewItem.LabVersion.Text = If(Type = CompType.Mod, "仅 ", "") & ModLoaders(0).ToString
  517. Case 2
  518. If Type = CompType.Mod Then
  519. NewItem.LabVersion.Text =
  520. If(Not (Setup.Get("ToolDownloadIgnoreQuilt") AndAlso
  521. ModLoaders.Contains(CompModLoaderType.Forge) AndAlso ModLoaders.Contains(CompModLoaderType.Fabric)),
  522. ModLoaders.Join(" / "), "任意")
  523. Else
  524. NewItem.LabVersion.Text = ModLoaders.Join(" / ")
  525. End If
  526. Case 3
  527. If Type = CompType.Mod Then
  528. NewItem.LabVersion.Text = "任意"
  529. Else
  530. NewItem.LabVersion.Text = ModLoaders.Join(" / ")
  531. End If
  532. End Select
  533. End If
  534. NewItem.LabInfo.Text = Description.Replace(vbCr, "").Replace(vbLf, "")
  535. NewItem.LabSource.Text = If(FromCurseForge, "CurseForge", "Modrinth")
  536. If LastUpdate IsNot Nothing Then
  537. NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
  538. Else
  539. NewItem.LabTime.Visibility = Visibility.Collapsed
  540. NewItem.ColumnTime1.Width = New GridLength(0)
  541. NewItem.ColumnTime2.Width = New GridLength(0)
  542. NewItem.ColumnTime3.Width = New GridLength(0)
  543. End If
  544. NewItem.LabDownload.Text =
  545. If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
  546. If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
  547. '其他
  548. If LogoUrl Is Nothing Then
  549. NewItem.Logo = "pack://application:,,,/images/Icons/NoIcon.png"
  550. Else
  551. NewItem.Logo = LogoUrl
  552. End If
  553. If OnClick IsNot Nothing Then
  554. AddHandler NewItem.Click, OnClick
  555. NewItem.HasAnimation = True
  556. Else
  557. NewItem.HasAnimation = False
  558. End If
  559. Return NewItem
  560. End Function
  561. '辅助函数
  562. ''' <summary>
  563. ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
  564. ''' </summary>
  565. Public Function IsLike(Project As CompProject) As Boolean
  566. If Id = Project.Id Then Return True '相同实例
  567. '提取字符串中的字母和数字
  568. Dim GetRaw =
  569. Function(Data As String) As String
  570. Dim Result As New StringBuilder()
  571. For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
  572. Result.Append(r)
  573. Next
  574. Return Result.ToString.ToLower
  575. End Function
  576. '来自不同的网站
  577. If FromCurseForge = Project.FromCurseForge Then Return False
  578. 'Mod 加载器一致
  579. If ModLoaders.Count <> Project.ModLoaders.Count OrElse ModLoaders.Except(Project.ModLoaders).Count > 0 Then Return False
  580. 'MC 版本一致
  581. If GameVersions.Count <> Project.GameVersions.Count OrElse GameVersions.Except(Project.GameVersions).Count > 0 Then Return False
  582. 'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
  583. If TranslatedName = Project.TranslatedName OrElse
  584. RawName = Project.RawName OrElse Description = Project.Description OrElse
  585. GetRaw(Slug) = GetRaw(Project.Slug) Then
  586. Log($"[Comp] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
  587. '如果只有一个有 DatabaseEntry,设置给另外一个
  588. If DatabaseEntry Is Nothing AndAlso Project.DatabaseEntry IsNot Nothing Then DatabaseEntry = Project.DatabaseEntry
  589. If DatabaseEntry IsNot Nothing AndAlso Project.DatabaseEntry Is Nothing Then Project.DatabaseEntry = DatabaseEntry
  590. Return True
  591. End If
  592. Return False
  593. End Function
  594. Public Overrides Function ToString() As String
  595. Return $"{Id} ({Slug}): {RawName}"
  596. End Function
  597. Public Overrides Function Equals(obj As Object) As Boolean
  598. Dim project = TryCast(obj, CompProject)
  599. Return project IsNot Nothing AndAlso Id = project.Id
  600. End Function
  601. Public Shared Operator =(left As CompProject, right As CompProject) As Boolean
  602. Return EqualityComparer(Of CompProject).Default.Equals(left, right)
  603. End Operator
  604. Public Shared Operator <>(left As CompProject, right As CompProject) As Boolean
  605. Return Not left = right
  606. End Operator
  607. End Class
  608. '输入与输出
  609. Public Class CompProjectRequest
  610. '结果要求
  611. ''' <summary>
  612. ''' 加载后应输出到的结果存储器。
  613. ''' </summary>
  614. Public Storage As CompProjectStorage
  615. ''' <summary>
  616. ''' 应当尽量达成的结果数量。
  617. ''' </summary>
  618. Public TargetResultCount As Integer
  619. ''' <summary>
  620. ''' 根据加载位置记录,是否还可以继续获取内容。
  621. ''' </summary>
  622. Public ReadOnly Property CanContinue As Boolean
  623. Get
  624. If Tag.StartsWith("/") OrElse Not Source.HasFlag(CompSourceType.CurseForge) Then Storage.CurseForgeTotal = 0
  625. If Tag.EndsWith("/") OrElse Not Source.HasFlag(CompSourceType.Modrinth) Then Storage.ModrinthTotal = 0
  626. If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
  627. Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
  628. End Get
  629. End Property
  630. '输入内容
  631. ''' <summary>
  632. ''' 筛选资源种类。
  633. ''' </summary>
  634. Public Type As CompType
  635. ''' <summary>
  636. ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
  637. ''' </summary>
  638. Public Tag As String = ""
  639. ''' <summary>
  640. ''' 筛选 Mod 加载器类别。
  641. ''' </summary>
  642. Public ModLoader As CompModLoaderType = CompModLoaderType.Any
  643. ''' <summary>
  644. ''' 筛选 MC 版本。
  645. ''' </summary>
  646. Public GameVersion As String = Nothing
  647. ''' <summary>
  648. ''' 搜索的文本内容。
  649. ''' </summary>
  650. Public SearchText As String = Nothing
  651. ''' <summary>
  652. ''' 允许的来源。
  653. ''' </summary>
  654. Public Source As CompSourceType = CompSourceType.Any
  655. ''' <summary>
  656. ''' 构造函数。
  657. ''' </summary>
  658. Public Sub New(Type As CompType, Storage As CompProjectStorage, TargetResultCount As Integer)
  659. Me.Type = Type
  660. Me.Storage = Storage
  661. Me.TargetResultCount = TargetResultCount
  662. End Sub
  663. '构造请求
  664. ''' <summary>
  665. ''' 获取对应的 CurseForge API 请求链接。若返回 Nothing 则为不进行 CurseForge 请求。
  666. ''' </summary>
  667. Public Function GetCurseForgeAddress() As String
  668. If Not Source.HasFlag(CompSourceType.CurseForge) Then Return Nothing
  669. If Tag.StartsWith("/") Then Storage.CurseForgeTotal = 0
  670. If Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset Then Return Nothing
  671. '应用筛选参数
  672. Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={CompPageSize}"
  673. Select Case Type
  674. Case CompType.Mod
  675. Address += "&classId=6"
  676. Case CompType.ModPack
  677. Address += "&classId=4471"
  678. Case CompType.ResourcePack
  679. 'FUTURE: Res
  680. End Select
  681. Address += "&categoryId=" & If(Tag = "", "0", Tag.Split("/").First)
  682. If ModLoader <> CompModLoaderType.Any Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
  683. If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
  684. If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & Net.WebUtility.UrlEncode(SearchText)
  685. If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
  686. Return Address
  687. End Function
  688. ''' <summary>
  689. ''' 获取对应的 Modrinth API 请求链接。若返回 Nothing 则为不进行 Modrinth 请求。
  690. ''' </summary>
  691. Public Function GetModrinthAddress() As String
  692. If Not Source.HasFlag(CompSourceType.Modrinth) Then Return Nothing
  693. If Tag.EndsWith("/") Then Storage.ModrinthTotal = 0
  694. If Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset Then Return Nothing
  695. '应用筛选参数
  696. Dim Address As String = $"https://api.modrinth.com/v2/search?limit={CompPageSize}&index=relevance"
  697. If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & Net.WebUtility.UrlEncode(SearchText)
  698. If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
  699. 'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
  700. Dim Facets As New List(Of String)
  701. Facets.Add($"[""project_type:{GetStringFromEnum(Type).ToLower}""]")
  702. If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.Split("/").Last}'""]")
  703. If ModLoader <> CompModLoaderType.Any Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).ToLower}'""]")
  704. If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
  705. Address += "&facets=[" & String.Join(",", Facets) & "]"
  706. Return Address
  707. End Function
  708. '相同判断
  709. Public Overrides Function Equals(obj As Object) As Boolean
  710. Dim request = TryCast(obj, CompProjectRequest)
  711. Return request IsNot Nothing AndAlso
  712. Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
  713. Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Source = request.Source AndAlso
  714. GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
  715. End Function
  716. Public Shared Operator =(left As CompProjectRequest, right As CompProjectRequest) As Boolean
  717. Return EqualityComparer(Of CompProjectRequest).Default.Equals(left, right)
  718. End Operator
  719. Public Shared Operator <>(left As CompProjectRequest, right As CompProjectRequest) As Boolean
  720. Return Not left = right
  721. End Operator
  722. End Class
  723. Public Class CompProjectStorage
  724. '加载位置记录
  725. Public CurseForgeOffset As Integer = 0
  726. Public CurseForgeTotal As Integer = -1
  727. Public ModrinthOffset As Integer = 0
  728. Public ModrinthTotal As Integer = -1
  729. '结果列表
  730. ''' <summary>
  731. ''' 可供展示的所有工程的列表。
  732. ''' </summary>
  733. Public Results As New List(Of CompProject)
  734. End Class
  735. '实际的获取
  736. Private Const CompPageSize = 40
  737. ''' <summary>
  738. ''' 已知工程信息的缓存。
  739. ''' </summary>
  740. Private CompProjectCache As New Dictionary(Of String, CompProject)
  741. ''' <summary>
  742. ''' 根据搜索请求获取一系列的工程列表。需要基于加载器运行。
  743. ''' </summary>
  744. Public Sub CompProjectsGet(Task As LoaderTask(Of CompProjectRequest, Integer))
  745. Dim Storage = Task.Input.Storage '避免多线程问题
  746. If Task.Input.Storage.Results.Count >= Task.Input.TargetResultCount Then
  747. Log($"[Comp] 已有 {Task.Input.Storage.Results.Count} 个结果,多于所需的 {Task.Input.TargetResultCount} 个结果,结束处理")
  748. Exit Sub
  749. ElseIf Not Task.Input.CanContinue Then
  750. If Task.Input.Storage.Results.Count = 0 Then
  751. Throw New Exception("没有符合条件的结果")
  752. Else
  753. Log($"[Comp] 已有 {Task.Input.Storage.Results.Count} 个结果,少于所需的 {Task.Input.TargetResultCount} 个结果,但无法继续获取,结束处理")
  754. Exit Sub
  755. End If
  756. End If
  757. #Region "拒绝 1.13- Quilt(这个版本根本没有 Quilt)"
  758. If Task.Input.ModLoader = CompModLoaderType.Quilt AndAlso VersionSortInteger(If(Task.Input.GameVersion, "1.15"), "1.14") = -1 Then
  759. Throw New Exception("Quilt 不支持 Minecraft " & Task.Input.GameVersion)
  760. End If
  761. #End Region
  762. #Region "处理搜索文本,赋值回 Task.Input.SearchText"
  763. Dim RawFilter As String = If(Task.Input.SearchText, "").Trim
  764. Task.Input.SearchText = RawFilter
  765. RawFilter = RawFilter.ToLower
  766. Log("[Comp] 工程列表搜索原始文本:" & RawFilter)
  767. '中文请求关键字处理
  768. Dim IsChineseSearch As Boolean = RegexCheck(RawFilter, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawFilter)
  769. If IsChineseSearch AndAlso Task.Input.Type = CompType.Mod Then
  770. '构造搜索请求
  771. Dim SearchEntries As New List(Of SearchEntry(Of CompDatabaseEntry))
  772. For Each Entry In CompDatabase
  773. If Entry.ChineseName.Contains("动态的树") Then Continue For '这玩意儿附属太多了
  774. SearchEntries.Add(New SearchEntry(Of CompDatabaseEntry) With {
  775. .Item = Entry,
  776. .SearchSource = New List(Of KeyValuePair(Of String, Double)) From {
  777. New KeyValuePair(Of String, Double)(Entry.ChineseName & If(Entry.CurseForgeSlug, "") & If(Entry.ModrinthSlug, ""), 1)
  778. }
  779. })
  780. Next
  781. '获取搜索结果
  782. Dim SearchResults = Search(SearchEntries, Task.Input.SearchText, 3)
  783. If SearchResults.Count = 0 Then Throw New Exception("无搜索结果,请尝试搜索英文名称")
  784. Dim SearchResult As String = ""
  785. For i = 0 To Math.Min(4, SearchResults.Count - 1) '就算全是准确的,也最多只要 5 个
  786. If Not SearchResults(i).AbsoluteRight AndAlso i >= Math.Min(2, SearchResults.Count - 1) Then Exit For '把 3 个结果拼合以提高准确度
  787. If SearchResults(i).Item.CurseForgeSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.CurseForgeSlug.Replace("-", " ") & " "
  788. If SearchResults(i).Item.ModrinthSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.ModrinthSlug.Replace("-", " ") & " "
  789. SearchResult += SearchResults(i).Item.ChineseName.Split(" (").Last.TrimEnd(") ").Split(" - ").First.
  790. Replace(":", "").Replace("(", "").Replace(")", "").ToLower & " "
  791. Next
  792. Log("[Comp] 中文搜索原始关键词:" & SearchResult, LogLevel.Developer)
  793. '去除常见连接词
  794. Dim RealFilter As String = ""
  795. For Each Word In SearchResult.Split(" ")
  796. If {"the", "of", "a", "mod", "and"}.Contains(Word.ToLower) OrElse Val(Word) > 0 Then Continue For
  797. If SearchResult.Split(" ").Count > 3 AndAlso {"ftb"}.Contains(Word.ToLower) Then Continue For
  798. RealFilter += Word.TrimStart("{[(").TrimEnd("}])") & " "
  799. Next
  800. Task.Input.SearchText = RealFilter
  801. Log("[Comp] 中文搜索最终关键词:" & RealFilter, LogLevel.Developer)
  802. End If
  803. '驼峰英文请求关键字处理
  804. Dim SpacedKeywords = RegexReplace(Task.Input.SearchText, "$& ", "([A-Z]+|[a-z]+?)(?=[A-Z]+[a-z]+[a-z ]*)")
  805. Dim ConnectedKeywords = Task.Input.SearchText.Replace(" ", "")
  806. Dim AllPossibleKeywords = (SpacedKeywords & " " & If(IsChineseSearch, Task.Input.SearchText, ConnectedKeywords & " " & RawFilter)).ToLower
  807. '最终处理关键字:分割、去重
  808. Dim RightKeywords As New List(Of String)
  809. For Each Keyword In AllPossibleKeywords.Split(" ")
  810. Keyword = Keyword.Trim("["c, "]"c)
  811. If Keyword = "" Then Continue For
  812. If {"forge", "fabric", "for", "mod",
  813. "forge/fabric", "fabric/forge", "fabric/quilt", "quilt/fabric"}.Contains(Keyword) Then '#208
  814. Log("[Comp] 已跳过搜索关键词:" & Keyword)
  815. Continue For
  816. End If
  817. RightKeywords.Add(Keyword)
  818. Next
  819. If RawFilter.Length > 0 AndAlso RightKeywords.Count = 0 Then
  820. Task.Input.SearchText = RawFilter '全都被过滤掉了
  821. Else
  822. Task.Input.SearchText = Join(RightKeywords.Distinct.ToList, " ").ToLower
  823. End If
  824. '例外项:OptiForge、OptiFabric(拆词后因为包含 Forge/Fabric 导致无法搜到实际的 Mod)
  825. If RawFilter.Replace(" ", "").ToLower.Contains("optiforge") Then Task.Input.SearchText = "optiforge"
  826. If RawFilter.Replace(" ", "").ToLower.Contains("optifabric") Then Task.Input.SearchText = "optifabric"
  827. Log("[Comp] 工程列表搜索最终文本:" & Task.Input.SearchText, LogLevel.Developer)
  828. Task.Progress = 0.1
  829. #End Region
  830. Dim RealResults As New List(Of CompProject)
  831. Retry:
  832. Dim RawResults As New List(Of CompProject)
  833. Dim [Error] As Exception = Nothing
  834. #Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
  835. Dim CurseForgeThread As Thread = Nothing
  836. Dim ModrinthThread As Thread = Nothing
  837. Dim ResultsLock As New Object
  838. Try
  839. '启动 CurseForge 线程
  840. Dim CurseForgeUrl As String = Task.Input.GetCurseForgeAddress()
  841. If CurseForgeUrl IsNot Nothing Then
  842. CurseForgeThread = RunInNewThread(
  843. Sub()
  844. Try
  845. '获取工程列表
  846. Log("[Comp] 开始从 CurseForge 获取工程列表:" & CurseForgeUrl)
  847. Dim RequestResult As JObject = NetGetCodeByRequestRetry(CurseForgeUrl, IsJson:=True, Encode:=Encoding.UTF8)
  848. Task.Progress += 0.2
  849. Dim ProjectList As New List(Of CompProject)
  850. For Each JsonEntry As JObject In RequestResult("data")
  851. ProjectList.Add(New CompProject(JsonEntry))
  852. Next
  853. '更新结果
  854. SyncLock ResultsLock
  855. RawResults.AddRange(ProjectList)
  856. End SyncLock
  857. Storage.CurseForgeOffset += ProjectList.Count
  858. Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
  859. Log($"[Comp] 从 CurseForge 获取到了 {ProjectList.Count} 个工程(已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
  860. Catch ex As Exception
  861. Log(ex, "从 CurseForge 获取工程列表失败")
  862. Storage.CurseForgeTotal = Storage.CurseForgeOffset
  863. [Error] = ex
  864. End Try
  865. End Sub, "CurseForge Project Request")
  866. End If
  867. '启动 Modrinth 线程
  868. Dim ModrinthUrl As String = Task.Input.GetModrinthAddress()
  869. If ModrinthUrl IsNot Nothing Then
  870. ModrinthThread = RunInNewThread(
  871. Sub()
  872. Try
  873. Log("[Comp] 开始从 Modrinth 获取工程列表:" & ModrinthUrl)
  874. Dim RequestResult As JObject = NetGetCodeByRequestRetry(ModrinthUrl, IsJson:=True, Encode:=Encoding.UTF8)
  875. Task.Progress += 0.2
  876. Dim ProjectList As New List(Of CompProject)
  877. For Each JsonEntry As JObject In RequestResult("hits")
  878. ProjectList.Add(New CompProject(JsonEntry))
  879. Next
  880. '更新结果
  881. SyncLock ResultsLock
  882. For Each Project In ProjectList
  883. If Task.Input.Type = CompType.Mod AndAlso Not Project.ModLoaders.Any() Then Continue For '过滤插件(#2458)
  884. RawResults.Add(Project)
  885. Next
  886. End SyncLock
  887. Storage.ModrinthOffset += ProjectList.Count
  888. Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
  889. Log($"[Comp] 从 Modrinth 获取到了 {ProjectList.Count} 个工程(已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
  890. Catch ex As Exception
  891. Log(ex, "从 Modrinth 获取工程列表失败")
  892. Storage.ModrinthTotal = Storage.ModrinthOffset
  893. [Error] = ex
  894. End Try
  895. End Sub, "Modrinth Project Request")
  896. End If
  897. '等待线程结束
  898. If CurseForgeThread IsNot Nothing Then CurseForgeThread.Join()
  899. If Task.IsAborted Then Exit Sub '会自动触发 Finally
  900. If ModrinthThread IsNot Nothing Then ModrinthThread.Join()
  901. If Task.IsAborted Then Exit Sub
  902. '确认存在结果
  903. If RawResults.Count = 0 Then
  904. If [Error] IsNot Nothing Then
  905. Throw [Error]
  906. Else
  907. If IsChineseSearch AndAlso Task.Input.Type <> CompType.Mod Then
  908. Throw New Exception($"{If(Task.Input.Type = CompType.ModPack, "整合包", "资源包")}搜索仅支持英文")
  909. ElseIf Task.Input.Source = CompSourceType.CurseForge AndAlso Task.Input.Tag.StartsWith("/") Then
  910. Throw New Exception("CurseForge 不兼容所选的类型")
  911. ElseIf Task.Input.Source = CompSourceType.Modrinth AndAlso Task.Input.Tag.EndsWith("/") Then
  912. Throw New Exception("Modrinth 不兼容所选的类型")
  913. Else
  914. Throw New Exception("没有符合条件的结果")
  915. End If
  916. End If
  917. End If
  918. Finally
  919. If CurseForgeThread IsNot Nothing Then CurseForgeThread.Interrupt()
  920. If ModrinthThread IsNot Nothing Then ModrinthThread.Interrupt()
  921. End Try
  922. #End Region
  923. #Region "提取非重复项,存储于 RealResults"
  924. '将 Modrinth 排在 CurseForge 的前面,避免加载结束顺序不同导致排名不同
  925. '这样做的话,去重后将优先保留 CurseForge 内容(考虑到 CurseForge 热度更高)
  926. RawResults = RawResults.Where(Function(x) Not x.FromCurseForge).Concat(RawResults.Where(Function(x) x.FromCurseForge)).ToList
  927. 'RawResults 去重
  928. RawResults = Distinct(RawResults, Function(a, b) a.IsLike(b))
  929. '已有内容去重
  930. RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
  931. Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
  932. '加入列表
  933. RealResults.AddRange(RawResults)
  934. Log($"[Comp] 去重、筛选后累计新增结果 {RealResults.Count} 个")
  935. #End Region
  936. #Region "检查结果数量,如果不足且可继续,会继续加载下一页"
  937. If RealResults.Count + Storage.Results.Count < Task.Input.TargetResultCount Then
  938. Log($"[Comp] 总结果数需求最少 {Task.Input.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
  939. If Task.Input.CanContinue Then
  940. Log("[Comp] 将继续尝试加载下一页")
  941. GoTo Retry
  942. Else
  943. Log("[Comp] 无法继续加载,将强制结束")
  944. End If
  945. End If
  946. #End Region
  947. #Region "将结果排序并添加"
  948. Dim Scores As New Dictionary(Of CompProject, Double) '排序分
  949. If String.IsNullOrEmpty(Task.Input.SearchText) Then
  950. '如果没有搜索文本,按下载量将结果排序
  951. For Each Result As CompProject In RealResults
  952. Scores.Add(Result, Result.DownloadCount * If(Result.FromCurseForge, 1, 30))
  953. Next
  954. Else
  955. '如果有搜索文本,按关联度将结果排序
  956. '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
  957. Dim Entry As New List(Of SearchEntry(Of CompProject))
  958. For Each Result As CompProject In RealResults
  959. Scores.Add(Result, If(Result.WikiId > 0, 0.2, 0) +
  960. Math.Log10(Math.Max(Result.DownloadCount, 1) * If(Result.FromCurseForge, 1, 30)) / 9)
  961. Entry.Add(New SearchEntry(Of CompProject) With {.Item = Result, .SearchSource = New List(Of KeyValuePair(Of String, Double)) From {
  962. New KeyValuePair(Of String, Double)(If(IsChineseSearch, Result.TranslatedName, Result.RawName), 1),
  963. New KeyValuePair(Of String, Double)(Result.Description, 0.05)}})
  964. Next
  965. Dim SearchResult = Search(Entry, RawFilter, 101, -1)
  966. For Each OneResult In SearchResult
  967. Scores(OneResult.Item) += OneResult.Similarity / SearchResult(0).Similarity '最高 1 分的相似度分
  968. Next
  969. End If
  970. '根据排序分得出结果并添加
  971. Storage.Results.AddRange(
  972. Sort(Scores.ToList, Function(a, b) a.Value > b.Value).Select(Function(r) r.Key).ToList)
  973. #End Region
  974. End Sub
  975. #End Region
  976. #Region "CompFile | 文件信息"
  977. '类定义
  978. Public Enum CompFileStatus
  979. Release = 1 '枚举值来源:https://docs.curseforge.com/#tocS_FileReleaseType
  980. Beta = 2
  981. Alpha = 3
  982. End Enum
  983. Public Class CompFile
  984. '源信息
  985. ''' <summary>
  986. ''' 文件的种类。
  987. ''' </summary>
  988. Public ReadOnly Type As CompType
  989. ''' <summary>
  990. ''' 该文件来自 CurseForge 还是 Modrinth。
  991. ''' </summary>
  992. Public ReadOnly FromCurseForge As Boolean
  993. ''' <summary>
  994. ''' 用于唯一性鉴别该文件的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
  995. ''' </summary>
  996. Public ReadOnly Id As String
  997. '描述性信息
  998. ''' <summary>
  999. ''' 文件描述名(并非文件名,是自定义的字段)。
  1000. ''' </summary>
  1001. Public DisplayName As String
  1002. ''' <summary>
  1003. ''' 发布时间。
  1004. ''' </summary>
  1005. Public ReadOnly ReleaseDate As Date
  1006. ''' <summary>
  1007. ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量,且 CurseForge 可能错误地返回 0。
  1008. ''' </summary>
  1009. Public ReadOnly DownloadCount As Integer
  1010. ''' <summary>
  1011. ''' 支持的 Mod 加载器列表。可能为空。
  1012. ''' </summary>
  1013. Public ReadOnly ModLoaders As List(Of CompModLoaderType)
  1014. ''' <summary>
  1015. ''' 支持的游戏版本列表。类型包括:"1.18.5","1.18","1.18 快照","21w15a","未知版本"。
  1016. ''' </summary>
  1017. Public ReadOnly GameVersions As List(Of String)
  1018. ''' <summary>
  1019. ''' 发布状态:Release/Beta/Alpha。
  1020. ''' </summary>
  1021. Public ReadOnly Status As CompFileStatus
  1022. ''' <summary>
  1023. ''' 发布状态的友好描述。例如:"正式版","Beta 版"。
  1024. ''' </summary>
  1025. Public ReadOnly Property StatusDescription As String
  1026. Get
  1027. Select Case Status
  1028. Case CompFileStatus.Release
  1029. Return "正式版"
  1030. Case CompFileStatus.Beta
  1031. Return If(ModeDebug, "Beta 版", "测试版")
  1032. Case Else
  1033. Return If(ModeDebug, "Alpha 版", "测试版")
  1034. End Select
  1035. End Get
  1036. End Property
  1037. '下载信息
  1038. ''' <summary>
  1039. ''' 下载信息是否可用。
  1040. ''' </summary>
  1041. Public ReadOnly Property Available As Boolean
  1042. Get
  1043. Return FileName IsNot Nothing
  1044. End Get
  1045. End Property
  1046. ''' <summary>
  1047. ''' 下载的文件名。
  1048. ''' </summary>
  1049. Public ReadOnly FileName As String = Nothing
  1050. ''' <summary>
  1051. ''' 文件所有可能的下载源。
  1052. ''' </summary>
  1053. Private ReadOnly DownloadAddress As String()
  1054. ''' <summary>
  1055. ''' 该文件的所有依赖工程的原始 ID。
  1056. ''' 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
  1057. ''' </summary>
  1058. Public ReadOnly RawDependencies As New List(Of String)
  1059. ''' <summary>
  1060. ''' 该文件的所有依赖工程的 Project.Id。
  1061. ''' </summary>
  1062. Public ReadOnly Dependencies As New List(Of String)
  1063. ''' <summary>
  1064. ''' 获取下载信息。
  1065. ''' </summary>
  1066. ''' <param name="LocalAddress">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
  1067. Public Function ToNetFile(LocalAddress As String) As NetFile
  1068. Return New NetFile(DownloadAddress, LocalAddress & If(LocalAddress.EndsWith("\"), FileName, ""))
  1069. End Function
  1070. '实例化
  1071. ''' <summary>
  1072. ''' 从文件 Json 中初始化实例。若出错会抛出异常。
  1073. ''' </summary>
  1074. Public Sub New(Data As JObject, Type As CompType)
  1075. Me.Type = Type
  1076. FromCurseForge = Data.ContainsKey("gameId")
  1077. If FromCurseForge Then
  1078. #Region "CurseForge"
  1079. '简单信息
  1080. Id = Data("id")
  1081. DisplayName = Data("displayName").ToString.Replace(" ", "").Trim(" ")
  1082. ReleaseDate = Data("fileDate")
  1083. Status = CType(Data("releaseType").ToObject(Of Integer), CompFileStatus)
  1084. DownloadCount = Data("downloadCount")
  1085. FileName = Data("fileName")
  1086. 'DownloadAddress
  1087. Dim Url = Data("downloadUrl").ToString
  1088. If Url = "" Then Url = "https://media.forgecdn.net/files/" & Id.ToString.Substring(0, 4) & "/" & Id.ToString.Substring(4).TrimStart("0") & "/" & FileName
  1089. Url = Url.Replace(FileName, Net.WebUtility.UrlEncode(FileName)) '对文件名进行编码
  1090. DownloadAddress = {Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge", "://media"),
  1091. Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
  1092. Url.Replace("://edge", "://media"),
  1093. Url}.Distinct.ToArray
  1094. 'Dependencies
  1095. If Type = CompType.Mod Then
  1096. RawDependencies = Data("dependencies").
  1097. Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
  1098. d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
  1099. Select(Function(d) d("modId").ToString).ToList
  1100. End If
  1101. 'GameVersions
  1102. Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.ToLower).ToList
  1103. GameVersions = RawVersions.Where(Function(v) v.StartsWith("1.")).Select(Function(v) v.Replace("-snapshot", " 快照")).ToList
  1104. If GameVersions.Count > 1 Then
  1105. GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
  1106. If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
  1107. ElseIf GameVersions.Count = 1 Then
  1108. GameVersions = GameVersions.ToList
  1109. Else
  1110. GameVersions = New List(Of String) From {"未知版本"}
  1111. End If
  1112. 'ModLoaders
  1113. ModLoaders = New List(Of CompModLoaderType)
  1114. If RawVersions.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
  1115. If RawVersions.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
  1116. If RawVersions.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
  1117. #End Region
  1118. Else
  1119. #Region "Modrinth"
  1120. '简单信息
  1121. Id = Data("id")
  1122. DisplayName = Data("name").ToString.Replace(" ", "").Trim(" ")
  1123. ReleaseDate = Data("date_published")
  1124. Status = If(Data("version_type").ToString = "release", CompFileStatus.Release, If(Data("version_type").ToString = "beta", CompFileStatus.Beta, CompFileStatus.Alpha))
  1125. DownloadCount = Data("downloads")
  1126. '可能为空
  1127. If CType(Data("files"), JArray).Count > 0 Then
  1128. FileName = Data("files")(0)("filename")
  1129. DownloadAddress = {Data("files")(0)("url").ToString.Replace(FileName, FileName)}
  1130. End If
  1131. 'Dependencies
  1132. If Type = CompType.Mod Then
  1133. RawDependencies = Data("dependencies").
  1134. Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
  1135. d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
  1136. d("project_id").ToString.Length > 0). '有时候真的会空……
  1137. Select(Function(d) d("project_id").ToString).ToList
  1138. End If
  1139. 'GameVersions
  1140. Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.ToLower).ToList
  1141. GameVersions = RawVersions.Where(Function(v) v.StartsWith("1.")).
  1142. Select(Function(v) If(v.Contains("-"), v.Split("-").First & " 快照", v)).ToList
  1143. If GameVersions.Count > 1 Then
  1144. GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
  1145. If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
  1146. ElseIf GameVersions.Count = 1 Then
  1147. GameVersions = GameVersions.ToList
  1148. ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")) Then
  1149. GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]{1}")).ToList
  1150. Else
  1151. GameVersions = New List(Of String) From {"未知版本"}
  1152. End If
  1153. 'ModLoaders
  1154. Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
  1155. ModLoaders = New List(Of CompModLoaderType)
  1156. If RawLoaders.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
  1157. If RawLoaders.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
  1158. If RawLoaders.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
  1159. #End Region
  1160. End If
  1161. End Sub
  1162. ''' <summary>
  1163. ''' 将当前文件信息实例化为控件。
  1164. ''' </summary>
  1165. Public Function ToListItem(OnClick As MyListItem.ClickEventHandler, Optional OnSaveClick As MyIconButton.ClickEventHandler = Nothing,
  1166. Optional BadDisplayName As Boolean = False) As MyListItem
  1167. '获取描述信息
  1168. Dim Info As String = ""
  1169. Select Case Type
  1170. Case CompType.Mod
  1171. Info += If(ModLoaders.Count > 0,
  1172. "适用于 " & Join(ModLoaders.Select(Function(m) GetStringFromEnum(m)).ToList, "/") & ",", "")
  1173. Info += If(ModeDebug AndAlso Dependencies.Count > 0, Dependencies.Count & " 个前置 Mod,", "")
  1174. Case CompType.ModPack
  1175. If GameVersions.All(Function(v) v.Contains("w")) Then
  1176. Info += $"游戏版本 {Join(GameVersions, "、")},"
  1177. End If
  1178. End Select
  1179. If DownloadCount > 0 Then 'CurseForge 的下载次数经常错误地返回 0
  1180. Info += If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万次下载,", DownloadCount & " 次下载,")
  1181. End If
  1182. Info += GetTimeSpanString(ReleaseDate - Date.Now, False) & "更新"
  1183. Info += If(Status = CompFileStatus.Release, "", "," & StatusDescription)
  1184. '建立控件
  1185. Dim NewItem As New MyListItem With {
  1186. .Title = If(BadDisplayName, FileName, DisplayName),
  1187. .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Me,
  1188. .Info = Info
  1189. }
  1190. Select Case Status
  1191. Case CompFileStatus.Release
  1192. NewItem.Logo = "pack://application:,,,/images/Icons/R.png"
  1193. Case CompFileStatus.Beta
  1194. NewItem.Logo = "pack://application:,,,/images/Icons/B.png"
  1195. Case Else 'Alpha
  1196. NewItem.Logo = "pack://application:,,,/images/Icons/A.png"
  1197. End Select
  1198. AddHandler NewItem.Click, OnClick
  1199. '建立另存为按钮
  1200. If OnSaveClick IsNot Nothing Then
  1201. Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
  1202. ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
  1203. ToolTipService.SetVerticalOffset(BtnSave, 30)
  1204. ToolTipService.SetHorizontalOffset(BtnSave, 2)
  1205. AddHandler BtnSave.Click, OnSaveClick
  1206. NewItem.Buttons = {BtnSave}
  1207. NewItem.PaddingRight = 35
  1208. End If
  1209. '结束
  1210. Return NewItem
  1211. End Function
  1212. '辅助函数
  1213. Public Overrides Function ToString() As String
  1214. Return $"{Id}: {DisplayName}"
  1215. End Function
  1216. End Class
  1217. '获取
  1218. ''' <summary>
  1219. ''' 已知文件信息的缓存。
  1220. ''' </summary>
  1221. Private CompFilesCache As New Dictionary(Of String, List(Of CompFile))
  1222. ''' <summary>
  1223. ''' 获取某个工程下的全部文件列表。
  1224. ''' 必须在工作线程执行,失败会抛出异常。
  1225. ''' </summary>
  1226. Public Function CompFilesGet(ProjectId As String, FromCurseForge As Boolean) As List(Of CompFile)
  1227. '使用缓存
  1228. If CompFilesCache.ContainsKey(ProjectId) Then Return CompFilesCache(ProjectId)
  1229. '获取工程对象
  1230. Dim TargetProject As CompProject
  1231. If CompProjectCache.ContainsKey(ProjectId) Then '存在缓存
  1232. TargetProject = CompProjectCache(ProjectId)
  1233. ElseIf FromCurseForge Then 'CurseForge
  1234. TargetProject = New CompProject(NetGetCodeByRequestRetry("https://api.curseforge.com/v1/mods/" & ProjectId, IsJson:=True, Encode:=Encoding.UTF8)("data"))
  1235. Else 'Modrinth
  1236. TargetProject = New CompProject(NetGetCodeByRequestRetry("https://api.modrinth.com/v2/project/" & ProjectId, IsJson:=True, Encode:=Encoding.UTF8))
  1237. End If
  1238. '获取工程对象的文件列表
  1239. Log("[Comp] 开始获取文件列表:" & ProjectId)
  1240. Dim ResultJsonArray As JArray
  1241. If FromCurseForge Then
  1242. 'CurseForge
  1243. If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
  1244. ResultJsonArray = GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json"))("data")
  1245. Else '否则使用全部文件
  1246. ResultJsonArray = NetGetCodeByRequestRetry($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999", Accept:="application/json", IsJson:=True)("data")
  1247. End If
  1248. Else
  1249. 'Modrinth
  1250. ResultJsonArray = NetGetCodeByRequestRetry($"https://api.modrinth.com/v2/project/{ProjectId}/version", Accept:="application/json", IsJson:=True)
  1251. End If
  1252. CompFilesCache(ProjectId) = Distinct(ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Type)).
  1253. Where(Function(a) a.Available).ToList, Function(a, b) a.Id = b.Id) 'CurseForge 可能会重复返回相同项(#1330)
  1254. '获取前置 Mod 列表
  1255. If TargetProject.Type <> CompType.Mod Then Return CompFilesCache(ProjectId)
  1256. Dim Deps As List(Of String) = CompFilesCache(ProjectId).
  1257. SelectMany(Function(f) f.RawDependencies).Distinct().ToList
  1258. Dim UndoneDeps = Deps.Where(Function(f) Not CompProjectCache.ContainsKey(f)).ToList
  1259. '获取前置 Mod 工程信息
  1260. If UndoneDeps.Count > 0 Then
  1261. Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置 Mod:{Join(UndoneDeps, ",")}")
  1262. Dim Projects As JArray
  1263. If TargetProject.FromCurseForge Then
  1264. Projects = GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods",
  1265. "POST", "{""modIds"": [" & Join(UndoneDeps, ",") & "]}", "application/json"))("data")
  1266. Else
  1267. Projects = NetGetCodeByRequestRetry($"https://api.modrinth.com/v2/projects?ids=[""{Join(UndoneDeps, """,""")}""]",
  1268. Accept:="application/json", IsJson:=True)
  1269. End If
  1270. For Each Project In Projects
  1271. Dim NewProject As New CompProject(Project) '在 New 的时候就添加了缓存
  1272. Next
  1273. End If
  1274. '更新前置 Mod 信息
  1275. If Deps.Count > 0 Then
  1276. For Each DepProject In Deps.Select(Function(id) CompProjectCache(id))
  1277. For Each File In CompFilesCache(ProjectId)
  1278. If File.RawDependencies.Contains(DepProject.Id) AndAlso DepProject.Id <> ProjectId Then
  1279. File.Dependencies.Add(DepProject.Id)
  1280. End If
  1281. Next
  1282. Next
  1283. End If
  1284. Return CompFilesCache(ProjectId)
  1285. End Function
  1286. ''' <summary>
  1287. ''' 预载包含大量 CompFile 的卡片,添加必要的元素和前置 Mod 列表。
  1288. ''' </summary>
  1289. Public Sub CompFilesCardPreload(Stack As StackPanel, Files As List(Of CompFile), OnClick As MyListItem.ClickEventHandler)
  1290. '获取卡片对应的前置 ID
  1291. '如果为整合包就不会有 Dependencies 信息,所以不用管
  1292. Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
  1293. Deps.Sort()
  1294. If Deps.Count = 0 Then Exit Sub
  1295. Deps = Deps.Where(Function(dep)
  1296. If Not CompProjectCache.ContainsKey(dep) Then Log($"[Comp] 未找到 ID {dep} 的前置 Mod 信息", LogLevel.Debug)
  1297. Return CompProjectCache.ContainsKey(dep)
  1298. End Function).ToList
  1299. '添加开头间隔
  1300. Stack.Children.Add(New TextBlock With {.Text = "前置 Mod", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
  1301. '添加前置 Mod 列表
  1302. For Each Dep In Deps
  1303. Dim Item = CompProjectCache(Dep).ToCompItem(False, False, AddressOf FrmDownloadMod.ProjectClick)
  1304. Stack.Children.Add(Item)
  1305. Next
  1306. '添加结尾间隔
  1307. Stack.Children.Add(New TextBlock With {.Text = "可选版本", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
  1308. End Sub
  1309. #End Region
  1310. End Module