ModComp.vb 95 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776
  1. Imports System.IO.Compression
  2. Public Module ModComp
  3. <Flags> Public Enum CompType
  4. ''' <summary>
  5. ''' Mod。
  6. ''' </summary>
  7. [Mod] = 1
  8. ''' <summary>
  9. ''' 整合包。
  10. ''' </summary>
  11. ModPack = 2
  12. ''' <summary>
  13. ''' 资源包。
  14. ''' </summary>
  15. ResourcePack = 4
  16. ''' <summary>
  17. ''' 光影包。
  18. ''' </summary>
  19. Shader = 8
  20. ''' <summary>
  21. ''' 数据包。
  22. ''' </summary>
  23. DataPack = 16
  24. ''' <summary>
  25. ''' 服务端插件。
  26. ''' </summary>
  27. Plugin = 32
  28. ''' <summary>
  29. ''' 同时包含数据包以及 Mod。
  30. ''' </summary>
  31. ModOrDataPack = [Mod] Or DataPack
  32. ''' <summary>
  33. ''' 允许任意种类,或种类未知。
  34. ''' </summary>
  35. Any = [Mod] Or ModPack Or ResourcePack Or Shader Or DataPack Or Plugin
  36. End Enum
  37. Public Enum CompModLoaderType
  38. 'https://docs.curseforge.com/?http#tocS_ModLoaderType
  39. Any = 0
  40. Forge = 1
  41. LiteLoader = 3
  42. Fabric = 4
  43. Quilt = 5
  44. NeoForge = 6
  45. End Enum
  46. <Flags> Public Enum CompSourceType
  47. CurseForge = 1
  48. Modrinth = 2
  49. Any = CurseForge Or Modrinth
  50. End Enum
  51. #Region "CompDatabase | Mod 数据库"
  52. Private ReadOnly Property CompDatabase As List(Of CompDatabaseEntry)
  53. Get
  54. Static _CompDatabase As List(Of CompDatabaseEntry) = Nothing
  55. If _CompDatabase IsNot Nothing Then Return _CompDatabase
  56. '初始化数据库
  57. _CompDatabase = New List(Of CompDatabaseEntry)
  58. Dim i As Integer = 0
  59. Using Archive As New ZipArchive(New MemoryStream(GetResources("ModData")), ZipArchiveMode.Read)
  60. For Each Line In ReadFile(Archive.GetEntry("moddata.txt").Open(), Encoding.UTF8).Replace(vbCrLf, vbLf).Replace(vbCr, "").Split(vbLf)
  61. i += 1
  62. If Line = "" Then Continue For
  63. For Each EntryData As String In Line.Split("¨")
  64. Dim Entry = New CompDatabaseEntry
  65. Dim Splited = EntryData.Split("|")
  66. Entry.Popularity = Val(Splited(0))
  67. If Splited(1).StartsWithF("@") Then
  68. Entry.CurseForgeSlug = Nothing
  69. Entry.ModrinthSlug = Splited(1).Replace("@", "")
  70. ElseIf Splited(1).EndsWithF("@") Then
  71. Entry.CurseForgeSlug = Splited(1).TrimEnd("@")
  72. Entry.ModrinthSlug = Entry.CurseForgeSlug
  73. ElseIf Splited(1).Contains("@") Then
  74. Entry.CurseForgeSlug = Splited(1).Split("@")(0)
  75. Entry.ModrinthSlug = Splited(1).Split("@")(1)
  76. Else
  77. Entry.CurseForgeSlug = Splited(1)
  78. Entry.ModrinthSlug = Nothing
  79. End If
  80. Entry.WikiId = i
  81. If Splited.Count >= 3 Then
  82. Entry.ChineseName = Splited.Last
  83. If Entry.ChineseName.Contains("*") Then '处理 *
  84. Entry.ChineseName = Entry.ChineseName.Replace("*",
  85. $" ({If(Entry.CurseForgeSlug, Entry.ModrinthSlug).Replace("-", " ").Capitalize})")
  86. End If
  87. End If
  88. _CompDatabase.Add(Entry)
  89. Next
  90. Next
  91. End Using
  92. Return _CompDatabase
  93. End Get
  94. End Property
  95. Private Class CompDatabaseEntry
  96. ''' <summary>
  97. ''' McMod 的对应 ID。
  98. ''' </summary>
  99. Public WikiId As Integer
  100. ''' <summary>
  101. ''' 中文译名。空字符串代表没有翻译。
  102. ''' </summary>
  103. Public ChineseName As String = ""
  104. ''' <summary>
  105. ''' CurseForge Slug(例如 advanced-solar-panels)。
  106. ''' </summary>
  107. Public CurseForgeSlug As String = Nothing
  108. ''' <summary>
  109. ''' Modrinth Slug(例如 advanced-solar-panels)。
  110. ''' </summary>
  111. Public ModrinthSlug As String = Nothing
  112. ''' <summary>
  113. ''' MC 百科的浏览量逆序排行,1 代表浏览量最低。
  114. ''' </summary>
  115. Public Popularity As Integer
  116. Public Overrides Function ToString() As String
  117. Return If(CurseForgeSlug, "") & "&" & If(ModrinthSlug, "") & "|" & WikiId & "|" & ChineseName & ", Rank " & Popularity
  118. End Function
  119. End Class
  120. #End Region
  121. #Region "CompProject | 工程信息"
  122. '类定义
  123. Public Class CompProject
  124. '源信息
  125. ''' <summary>
  126. ''' 该工程信息来自 CurseForge 还是 Modrinth。
  127. ''' </summary>
  128. Public ReadOnly FromCurseForge As Boolean
  129. ''' <summary>
  130. ''' 工程中包含的文件种类。
  131. ''' 若为 Modrinth 工程,可能为 Mod 或 数据包。
  132. ''' </summary>
  133. Public ReadOnly Types As CompType
  134. ''' <summary>
  135. ''' 工程的短名。例如 technical-enchant。
  136. ''' </summary>
  137. Public ReadOnly Slug As String
  138. ''' <summary>
  139. ''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
  140. ''' </summary>
  141. Public ReadOnly Id As String
  142. ''' <summary>
  143. ''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
  144. ''' </summary>
  145. Public ReadOnly CurseForgeFileIds As List(Of Integer)
  146. '描述性信息
  147. ''' <summary>
  148. ''' 原始的英文名称。
  149. ''' </summary>
  150. Public ReadOnly RawName As String
  151. ''' <summary>
  152. ''' 英文描述。
  153. ''' </summary>
  154. Public ReadOnly Description As String
  155. ''' <summary>
  156. ''' 来源网站的工程页面网址。确保格式一定标准。
  157. ''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
  158. ''' Modrinth:https://modrinth.com/mod/technical-enchant
  159. ''' </summary>
  160. Public ReadOnly Website As String
  161. ''' <summary>
  162. ''' 最后一次更新的时间。
  163. ''' </summary>
  164. Public ReadOnly LastUpdate As Date
  165. ''' <summary>
  166. ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
  167. ''' </summary>
  168. Public ReadOnly DownloadCount As Integer
  169. ''' <summary>
  170. ''' 支持的 Mod 加载器列表。可能为空。
  171. ''' </summary>
  172. Public ReadOnly ModLoaders As List(Of CompModLoaderType)
  173. ''' <summary>
  174. ''' 描述性标签的内容。已转换为中文。
  175. ''' </summary>
  176. Public ReadOnly Tags As List(Of String)
  177. ''' <summary>
  178. ''' Logo 图片的下载地址。
  179. ''' 若为 Nothing 则没有,保证不为空字符串。
  180. ''' </summary>
  181. Public LogoUrl As String = Nothing
  182. ''' <summary>
  183. ''' 支持的 Drop 编号,从高到低排序,不为 Nothing。
  184. ''' 例如:261(26.1.x)、180(1.18.x)。
  185. ''' </summary>
  186. Public ReadOnly Drops As List(Of Integer)
  187. ''' <summary>
  188. ''' Modrinth API 返回的原始版本列表。
  189. ''' 仅用于 Modrinth 工程的二次筛选,不会被缓存,不会被预处理。
  190. ''' 若非从 Modrinth 获取则为 Nothing。
  191. ''' </summary>
  192. Public ReadOnly UnsafeGameVersions As List(Of String)
  193. '数据库信息
  194. Private LoadedDatabase As Boolean = False
  195. Private _DatabaseEntry As CompDatabaseEntry = Nothing
  196. ''' <summary>
  197. ''' 关联的数据库条目。若为 Nothing 则没有。
  198. ''' </summary>
  199. Private Property DatabaseEntry As CompDatabaseEntry
  200. Get
  201. If Not LoadedDatabase Then
  202. LoadedDatabase = True
  203. If Types.HasFlag(CompType.Mod) OrElse Types.HasFlag(CompType.DataPack) Then
  204. _DatabaseEntry = CompDatabase.FirstOrDefault(Function(c) If(FromCurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
  205. End If
  206. End If
  207. Return _DatabaseEntry
  208. End Get
  209. Set(value As CompDatabaseEntry)
  210. LoadedDatabase = True
  211. _DatabaseEntry = value
  212. End Set
  213. End Property
  214. ''' <summary>
  215. ''' MC 百科的页面 ID。若为 0 则没有。
  216. ''' </summary>
  217. Public ReadOnly Property WikiId As Integer
  218. Get
  219. Return If(DatabaseEntry Is Nothing, 0, DatabaseEntry.WikiId)
  220. End Get
  221. End Property
  222. ''' <summary>
  223. ''' 翻译后的中文名。若数据库没有则等同于 RawName。
  224. ''' </summary>
  225. Public ReadOnly Property TranslatedName As String
  226. Get
  227. Return If(DatabaseEntry Is Nothing OrElse DatabaseEntry.ChineseName = "", RawName, DatabaseEntry.ChineseName)
  228. End Get
  229. End Property
  230. '实例化
  231. ''' <summary>
  232. ''' 从工程 Json 中初始化实例。若出错会抛出异常。
  233. ''' </summary>
  234. Public Sub New(Data As JObject)
  235. If Data.ContainsKey("Tags") Then
  236. #Region "CompJson"
  237. FromCurseForge = Data("DataSource") = "CurseForge"
  238. Types = Data("Types").ToObject(Of Integer)
  239. Slug = Data("Slug")
  240. Id = Data("Id")
  241. If Data.ContainsKey("CurseForgeFileIds") Then CurseForgeFileIds = CType(Data("CurseForgeFileIds"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
  242. RawName = Data("RawName")
  243. Description = Data("Description")
  244. Website = Data("Website")
  245. LastUpdate = Data("LastUpdate")
  246. DownloadCount = Data("DownloadCount")
  247. If Data.ContainsKey("ModLoaders") Then
  248. ModLoaders = CType(Data("ModLoaders"), JArray).Select(Function(t) CType(t.ToObject(Of Integer), CompModLoaderType)).ToList
  249. Else
  250. ModLoaders = New List(Of CompModLoaderType)
  251. End If
  252. Tags = CType(Data("Tags"), JArray).Select(Function(t) t.ToString).ToList
  253. If Data.ContainsKey("LogoUrl") Then LogoUrl = Data("LogoUrl")
  254. If Data.ContainsKey("Drops") Then
  255. Drops = CType(Data("Drops"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
  256. Else
  257. Drops = New List(Of Integer)
  258. End If
  259. #End Region
  260. Else
  261. FromCurseForge = Data.ContainsKey("summary")
  262. If FromCurseForge Then
  263. #Region "CurseForge"
  264. '简单信息
  265. Id = Data("id")
  266. Slug = Data("slug")
  267. RawName = Data("name")
  268. Description = Data("summary")
  269. Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
  270. LastUpdate = Data("dateReleased") '#1194
  271. DownloadCount = Data("downloadCount")
  272. If Data("logo").Count > 0 Then
  273. If Data("logo")("thumbnailUrl") Is Nothing OrElse Data("logo")("thumbnailUrl") = "" Then
  274. LogoUrl = Data("logo")("url")
  275. Else
  276. LogoUrl = Data("logo")("thumbnailUrl")
  277. End If
  278. End If
  279. If LogoUrl = "" Then LogoUrl = Nothing
  280. 'Type
  281. If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
  282. Types = CompType.Mod
  283. ElseIf Website.Contains("/modpacks/") Then
  284. Types = CompType.ModPack
  285. ElseIf Website.Contains("/resourcepacks/") Then
  286. Types = CompType.ResourcePack
  287. ElseIf Website.Contains("/texture-packs/") Then
  288. Types = CompType.ResourcePack
  289. ElseIf Website.Contains("/shaders/") Then
  290. Types = CompType.Shader
  291. Else
  292. Types = CompType.DataPack
  293. End If
  294. 'FileIndexes / VanillaMajorVersions / ModLoaders
  295. ModLoaders = New List(Of CompModLoaderType)
  296. Dim Files As New List(Of KeyValuePair(Of Integer, List(Of String))) 'FileId, GameVersions
  297. For Each File In If(Data("latestFiles"), New JArray)
  298. Dim NewFile As New CompFile(File, Types)
  299. If Not NewFile.Available Then Continue For
  300. ModLoaders.AddRange(NewFile.ModLoaders)
  301. Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
  302. If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
  303. Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
  304. Next
  305. For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
  306. If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
  307. Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
  308. Next
  309. CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
  310. Drops = Files.SelectMany(Function(f) f.Value).
  311. Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
  312. ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
  313. 'Tags
  314. Tags = New List(Of String)
  315. For Each Category In If(Data("categories"), New JArray). '镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831)
  316. Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
  317. Select Case Category
  318. 'Mod
  319. Case 406 : Tags.Add("世界元素")
  320. Case 407 : Tags.Add("生物群系")
  321. Case 410 : Tags.Add("维度")
  322. Case 408 : Tags.Add("矿物/资源")
  323. Case 409 : Tags.Add("天然结构")
  324. Case 412 : Tags.Add("科技")
  325. Case 415 : Tags.Add("管道/物流")
  326. Case 4843 : Tags.Add("自动化")
  327. Case 417 : Tags.Add("能源")
  328. Case 4558 : Tags.Add("红石")
  329. Case 436 : Tags.Add("食物/烹饪")
  330. Case 416 : Tags.Add("农业")
  331. Case 414 : Tags.Add("运输")
  332. Case 420 : Tags.Add("仓储")
  333. Case 419 : Tags.Add("魔法")
  334. Case 422 : Tags.Add("冒险")
  335. Case 424 : Tags.Add("装饰")
  336. Case 411 : Tags.Add("生物")
  337. Case 434 : Tags.Add("装备")
  338. Case 6814 : Tags.Add("性能优化")
  339. Case 9026 : Tags.Add("创造模式")
  340. Case 423 : Tags.Add("信息显示")
  341. Case 435 : Tags.Add("服务器")
  342. Case 5191 : Tags.Add("改良")
  343. Case 421 : Tags.Add("支持库")
  344. '整合包
  345. Case 4484 : Tags.Add("多人")
  346. Case 4479 : Tags.Add("硬核")
  347. Case 4483 : Tags.Add("战斗")
  348. Case 4478 : Tags.Add("任务")
  349. Case 4472 : Tags.Add("科技")
  350. Case 4473 : Tags.Add("魔法")
  351. Case 4475 : Tags.Add("冒险")
  352. Case 4476 : Tags.Add("探索")
  353. Case 4477 : Tags.Add("小游戏")
  354. Case 4474 : Tags.Add("科幻")
  355. Case 4736 : Tags.Add("空岛")
  356. Case 5128 : Tags.Add("原版改良")
  357. Case 4487 : Tags.Add("FTB")
  358. Case 4480 : Tags.Add("基于地图")
  359. Case 4481 : Tags.Add("轻量")
  360. Case 4482 : Tags.Add("大型")
  361. '资源包
  362. Case 403 : Tags.Add("原版风")
  363. Case 400 : Tags.Add("写实风")
  364. Case 401 : Tags.Add("现代风")
  365. Case 402 : Tags.Add("中世纪")
  366. Case 399 : Tags.Add("蒸汽朋克")
  367. Case 5244 : Tags.Add("含字体")
  368. Case 404 : Tags.Add("动态效果")
  369. Case 4465 : Tags.Add("兼容 Mod")
  370. Case 393 : Tags.Add("16x")
  371. Case 394 : Tags.Add("32x")
  372. Case 395 : Tags.Add("64x")
  373. Case 396 : Tags.Add("128x")
  374. Case 397 : Tags.Add("256x")
  375. Case 398 : Tags.Add("超高清")
  376. Case 5193 : Tags.Add("数据包") '有这个 Tag 的项会从资源包请求中被移除
  377. '光影包
  378. Case 6553 : Tags.Add("写实风")
  379. Case 6554 : Tags.Add("幻想风")
  380. Case 6555 : Tags.Add("原版风")
  381. '数据包
  382. Case 6948 : Tags.Add("冒险")
  383. Case 6949 : Tags.Add("幻想")
  384. Case 6950 : Tags.Add("支持库")
  385. Case 6952 : Tags.Add("魔法")
  386. Case 6946 : Tags.Add("Mod 相关")
  387. Case 6951 : Tags.Add("科技")
  388. Case 6953 : Tags.Add("实用")
  389. End Select
  390. Next
  391. #End Region
  392. Else
  393. #Region "Modrinth"
  394. '简单信息
  395. Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
  396. Slug = Data("slug")
  397. RawName = Data("title")
  398. Description = Data("description")
  399. LastUpdate = If(Data("date_modified"), Data("updated"))
  400. DownloadCount = Data("downloads")
  401. LogoUrl = Data("icon_url")
  402. If LogoUrl = "" Then LogoUrl = Nothing
  403. Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
  404. 'GameVersions
  405. '搜索结果的键为 versions,获取特定工程的键为 game_versions
  406. UnsafeGameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).Select(Function(v) v.ToString).Distinct.ToList
  407. Drops = UnsafeGameVersions.Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
  408. 'Type
  409. Select Case Data("project_type").ToString
  410. Case "modpack" : Types = CompType.ModPack
  411. Case "resourcepack" : Types = CompType.ResourcePack
  412. Case "shader" : Types = CompType.Shader
  413. Case Else : Types = CompType.Mod 'Modrinth 将数据包标为 Mod,但 categories 字段里有 datapack
  414. End Select
  415. 'Tags & ModLoaders
  416. Tags = New List(Of String)
  417. ModLoaders = New List(Of CompModLoaderType)
  418. If Data.ContainsKey("loaders") Then
  419. For Each Category In Data("loaders").Select(Function(t) t.ToString)
  420. Select Case Category
  421. Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
  422. Case "fabric" : ModLoaders.Add(CompModLoaderType.Fabric)
  423. Case "quilt" : ModLoaders.Add(CompModLoaderType.Quilt)
  424. Case "neoforge" : ModLoaders.Add(CompModLoaderType.NeoForge)
  425. End Select
  426. Next
  427. End If
  428. For Each Category In Data("categories").Select(Function(t) t.ToString)
  429. Select Case Category
  430. '加载器
  431. Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
  432. Case "fabric" : ModLoaders.Add(CompModLoaderType.Fabric)
  433. Case "quilt" : ModLoaders.Add(CompModLoaderType.Quilt)
  434. Case "neoforge" : ModLoaders.Add(CompModLoaderType.NeoForge)
  435. Case "datapack" : Types = CompType.DataPack
  436. '共用
  437. Case "technology" : Tags.Add("科技")
  438. Case "magic" : Tags.Add("魔法")
  439. Case "adventure" : Tags.Add("冒险")
  440. Case "utility" : Tags.Add("实用")
  441. Case "optimization" : Tags.Add("性能优化")
  442. Case "vanilla-like" : Tags.Add("原版风")
  443. Case "realistic" : Tags.Add("写实风")
  444. 'Mod/数据包
  445. Case "worldgen" : Tags.Add("世界元素")
  446. Case "food" : Tags.Add("食物/烹饪")
  447. Case "game-mechanics" : Tags.Add("游戏机制")
  448. Case "transportation" : Tags.Add("运输")
  449. Case "storage" : Tags.Add("仓储")
  450. Case "decoration" : If Types <> CompType.ResourcePack Then Tags.Add("装饰")
  451. Case "mobs" : If Types <> CompType.ResourcePack Then Tags.Add("生物")
  452. Case "equipment" : If Types <> CompType.ResourcePack Then Tags.Add("装备")
  453. Case "social" : Tags.Add("服务器")
  454. Case "library" : Tags.Add("支持库")
  455. '整合包
  456. Case "multiplayer" : Tags.Add("多人")
  457. Case "challenging" : Tags.Add("硬核")
  458. Case "combat" : Tags.Add("战斗")
  459. Case "quests" : Tags.Add("任务")
  460. Case "kitchen-sink" : Tags.Add("水槽包")
  461. Case "lightweight" : Tags.Add("轻量")
  462. '资源包
  463. Case "simplistic" : Tags.Add("简洁")
  464. Case "combat" : Tags.Add("战斗")
  465. Case "tweaks" : Tags.Add("改良")
  466. Case "8x-" : Tags.Add("极简")
  467. Case "16x" : Tags.Add("16x")
  468. Case "32x" : Tags.Add("32x")
  469. Case "48x" : Tags.Add("48x")
  470. Case "64x" : Tags.Add("64x")
  471. Case "128x" : Tags.Add("128x")
  472. Case "256x" : Tags.Add("256x")
  473. Case "512x+" : Tags.Add("超高清")
  474. Case "audio" : Tags.Add("含声音")
  475. Case "fonts" : Tags.Add("含字体")
  476. Case "models" : Tags.Add("含模型")
  477. Case "gui" : Tags.Add("含 UI")
  478. Case "locale" : Tags.Add("含语言")
  479. Case "core-shaders" : Tags.Add("核心着色器")
  480. Case "modded" : Tags.Add("兼容 Mod")
  481. '光影包
  482. Case "fantasy" : Tags.Add("幻想风")
  483. Case "semi-realistic" : Tags.Add("半写实风")
  484. Case "cartoon" : Tags.Add("卡通风")
  485. '暂时不添加性能负荷 Tag
  486. 'Case "potato" : Tags.Add("极低")
  487. 'Case "low" : Tags.Add("低")
  488. 'Case "medium" : Tags.Add("中")
  489. 'Case "high" : Tags.Add("高")
  490. Case "colored-lighting" : Tags.Add("彩色光照")
  491. Case "path-tracing" : Tags.Add("路径追踪")
  492. Case "pbr" : Tags.Add("PBR")
  493. Case "reflections" : Tags.Add("反射")
  494. Case "iris" : Tags.Add("Iris")
  495. Case "optifine" : Tags.Add("OptiFine")
  496. Case "vanilla" : Tags.Add("原版可用")
  497. End Select
  498. Next
  499. If Types = CompType.DataPack AndAlso ModLoaders.Any Then Types = CompType.DataPack Or CompType.Mod
  500. #End Region
  501. End If
  502. If Not Tags.Any() Then Tags.Add("其他")
  503. Tags.Sort()
  504. ModLoaders.Sort()
  505. End If
  506. '保存缓存
  507. CompProjectCache(Id) = Me
  508. End Sub
  509. ''' <summary>
  510. ''' 将当前实例转为可用于保存缓存的 Json。
  511. ''' </summary>
  512. Public Function ToJson() As JObject
  513. Dim Json As New JObject
  514. Json("DataSource") = If(FromCurseForge, "CurseForge", "Modrinth")
  515. Json("Types") = CInt(Types)
  516. Json("Slug") = Slug
  517. Json("Id") = Id
  518. If CurseForgeFileIds IsNot Nothing Then Json("CurseForgeFileIds") = New JArray(CurseForgeFileIds)
  519. Json("RawName") = RawName
  520. Json("Description") = Description
  521. Json("Website") = Website
  522. Json("LastUpdate") = LastUpdate
  523. Json("DownloadCount") = DownloadCount
  524. If ModLoaders IsNot Nothing AndAlso ModLoaders.Any Then Json("ModLoaders") = New JArray(ModLoaders.Select(Function(m) CInt(m)))
  525. Json("Tags") = New JArray(Tags)
  526. If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
  527. If Drops.Any Then Json("Drops") = New JArray(Drops)
  528. Json("CacheTime") = Date.Now '用于检查缓存时间
  529. Return Json
  530. End Function
  531. ''' <summary>
  532. ''' 将当前工程信息实例化为控件。
  533. ''' </summary>
  534. Public Function ToCompItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean) As MyVirtualizingElement(Of MyCompItem)
  535. '获取版本描述
  536. Dim GameVersionDescription As String
  537. If Drops Is Nothing OrElse Not Drops.Any() Then
  538. GameVersionDescription = "仅快照版本" '#5412
  539. Else
  540. Dim Segments As New List(Of String)
  541. Dim IsOld As Boolean = False
  542. For i = 0 To Drops.Count - 1 '版本号一定为降序
  543. '获取当前连续的版本号段
  544. Dim StartDrop As Integer = Drops(i), EndDrop As Integer = Drops(i)
  545. If StartDrop < 120 Then '如果支持新版本,则不显示 1.11-
  546. If Segments.Any() AndAlso Not IsOld Then
  547. Exit For
  548. Else
  549. IsOld = True
  550. End If
  551. End If
  552. For ii = i + 1 To Drops.Count - 1
  553. If AllDrops Is Nothing OrElse AllDrops.IndexOf(Drops(ii)) <> AllDrops.IndexOf(EndDrop) + 1 Then Exit For
  554. EndDrop = Drops(ii)
  555. i = ii
  556. Next
  557. '将版本号段转为描述文本
  558. Dim StartName = McVersion.DropToVersion(StartDrop)
  559. Dim EndName = McVersion.DropToVersion(EndDrop)
  560. If StartDrop = EndDrop Then
  561. Segments.Add(StartName)
  562. ElseIf AllDrops IsNot Nothing AndAlso StartDrop >= AllDrops.First Then
  563. If EndDrop < 120 Then
  564. Segments.Clear()
  565. Segments.Add("全版本")
  566. Exit For
  567. Else
  568. Segments.Add(EndName & "+")
  569. End If
  570. ElseIf EndDrop < 120 Then
  571. Segments.Add(StartName & "-")
  572. Exit For
  573. ElseIf AllDrops Is Nothing OrElse AllDrops.IndexOf(EndDrop) - AllDrops.IndexOf(StartDrop) = 1 Then
  574. Segments.Add(StartName & ", " & EndName)
  575. Else
  576. Segments.Add(StartName & "~" & EndName)
  577. End If
  578. Next
  579. GameVersionDescription = Segments.Join(", ")
  580. End If
  581. '获取 Mod 加载器描述
  582. Dim ModLoaderDescriptionFull As String, ModLoaderDescriptionPart As String
  583. Dim ModLoadersForDesc As New List(Of CompModLoaderType)(ModLoaders)
  584. If Settings.Get("ToolDownloadIgnoreQuilt") Then ModLoadersForDesc.Remove(CompModLoaderType.Quilt)
  585. Select Case ModLoadersForDesc.Count
  586. Case 0
  587. If ModLoaders.IsSingle Then
  588. ModLoaderDescriptionFull = "仅 " & ModLoaders.Single.ToString
  589. ModLoaderDescriptionPart = ModLoaders.Single.ToString
  590. Else
  591. ModLoaderDescriptionFull = "未知"
  592. ModLoaderDescriptionPart = ""
  593. End If
  594. Case 1
  595. ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
  596. ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
  597. Case Else
  598. Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
  599. If ModLoaders.Contains(CompModLoaderType.Forge) AndAlso
  600. (NewestDrop < 140 OrElse ModLoaders.Contains(CompModLoaderType.Fabric)) AndAlso
  601. (NewestDrop < 200 OrElse ModLoaders.Contains(CompModLoaderType.NeoForge)) AndAlso
  602. (NewestDrop < 140 OrElse ModLoaders.Contains(CompModLoaderType.Quilt) OrElse Settings.Get("ToolDownloadIgnoreQuilt")) Then
  603. ModLoaderDescriptionFull = "任意"
  604. ModLoaderDescriptionPart = ""
  605. Else
  606. ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
  607. ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
  608. End If
  609. End Select
  610. '实例化 UI
  611. Return New MyVirtualizingElement(Of MyCompItem)(
  612. Function()
  613. Dim NewItem As New MyCompItem With {.Tag = Me}
  614. ApplyLogoToMyImage(NewItem.PathLogo)
  615. Dim Titles = GetControlTitle(True)
  616. NewItem.Title = Titles.Title
  617. If Titles.SubTitle = "" Then
  618. CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
  619. Else
  620. NewItem.SubTitle = Titles.SubTitle
  621. End If
  622. NewItem.Tags = Tags
  623. NewItem.Description = Description.Replace(vbCr, "").Replace(vbLf, "")
  624. '下边栏
  625. If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
  626. '全部隐藏
  627. CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
  628. CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
  629. NewItem.ColumnVersion1.Width = New GridLength(0)
  630. NewItem.ColumnVersion2.MaxWidth = 0
  631. NewItem.ColumnVersion3.Width = New GridLength(0)
  632. ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
  633. '全部显示
  634. NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
  635. ElseIf ShowMcVersionDesc Then
  636. '仅显示版本
  637. NewItem.LabVersion.Text = GameVersionDescription
  638. Else
  639. '仅显示 Mod 加载器
  640. NewItem.LabVersion.Text = ModLoaderDescriptionFull
  641. End If
  642. NewItem.LabSource.Text = If(FromCurseForge, "CurseForge", "Modrinth")
  643. NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
  644. NewItem.LabDownload.Text =
  645. If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
  646. If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
  647. Return NewItem
  648. End Function) With {.Height = 64}
  649. End Function
  650. Public Sub ApplyLogoToMyImage(Img As MyImage)
  651. If String.IsNullOrEmpty(LogoUrl) Then
  652. Img.Source = PathImage & "Icons/NoIcon.png"
  653. Else
  654. Img.Source = LogoUrl
  655. Img.FallbackSource = DlSourceModGet(LogoUrl)
  656. End If
  657. End Sub
  658. Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
  659. '检查下列代码时可以参考 #1567 的测试例
  660. Dim Title As String = RawName
  661. Dim SubtitleList As List(Of String)
  662. If TranslatedName = RawName Then
  663. '没有中文翻译
  664. '将所有名称分段
  665. Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
  666. Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
  667. If NameLists.IsSingle Then GoTo NoSubtitle
  668. '查找其中的缩写、Forge/Fabric 等版本标记
  669. SubtitleList = New List(Of String)
  670. Dim NormalNameList = New List(Of String)
  671. For Each Name In NameLists
  672. Dim LowerName As String = Name.Lower
  673. If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
  674. '缩写
  675. SubtitleList.Add(Name)
  676. ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
  677. Not RegexCheck(LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母
  678. 'Forge/Fabric 等版本标记
  679. SubtitleList.Add(Name)
  680. Else
  681. '其他部分
  682. NormalNameList.Add(Name)
  683. End If
  684. Next
  685. '根据分类后的结果处理
  686. If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
  687. '同时包含 NormalName 和 Subtitle
  688. Title = NormalNameList.Join(" - ")
  689. Else
  690. '有中文翻译
  691. '尝试将文本分为三段:Title (EnglishName) - Suffix
  692. '检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
  693. Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
  694. Dim Suffix As String = ""
  695. If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
  696. Dim EnglishName As String = TranslatedName
  697. If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
  698. EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
  699. '中段的额外信息截取
  700. SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
  701. Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
  702. If SubtitleList.Count > 1 AndAlso
  703. Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
  704. Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
  705. SubtitleList = New List(Of String) From {EnglishName} '使用原名
  706. End If
  707. '添加后缀
  708. If Suffix <> "" Then SubtitleList.Add(Suffix)
  709. End If
  710. SubtitleList = SubtitleList.Distinct.ToList()
  711. '设置标题与描述
  712. Dim Subtitle As String = ""
  713. If SubtitleList.Any Then
  714. For Each Ex In SubtitleList
  715. Dim IsModLoaderDescription As Boolean =
  716. Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
  717. '是否显示 ModLoader 信息
  718. If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
  719. '去除 “Forge/Fabric” 这一无意义提示
  720. If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
  721. '将 “Forge” 等提示改为 “Forge 版”
  722. If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
  723. Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
  724. Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
  725. End If
  726. '将 “forge” 等词语的首字母大写
  727. Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
  728. Subtitle &= " | " & Ex.Trim
  729. Next
  730. Else
  731. NoSubtitle:
  732. Subtitle = ""
  733. End If
  734. Return (Title, Subtitle)
  735. End Function
  736. '辅助函数
  737. ''' <summary>
  738. ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
  739. ''' </summary>
  740. Public Function IsLike(Project As CompProject) As Boolean
  741. If Id = Project.Id Then Return True '相同实例
  742. '提取字符串中的字母和数字
  743. Dim GetRaw =
  744. Function(Data As String) As String
  745. Dim Result As New StringBuilder()
  746. For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
  747. Result.Append(r)
  748. Next
  749. Return Result.ToString.Lower
  750. End Function
  751. '来自不同的网站
  752. If FromCurseForge = Project.FromCurseForge Then Return False
  753. 'Mod 加载器一致
  754. If ModLoaders.Count <> Project.ModLoaders.Count OrElse ModLoaders.Except(Project.ModLoaders).Any() Then Return False
  755. '若不为光影,则要求 MC 版本一致
  756. If Types <> CompType.Shader AndAlso (Drops.Count <> Project.Drops.Count OrElse Drops.Except(Project.Drops).Any()) Then Return False
  757. '最近更新时间差距在一周以内
  758. If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
  759. 'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
  760. If TranslatedName = Project.TranslatedName OrElse
  761. RawName = Project.RawName OrElse Description = Project.Description OrElse
  762. GetRaw(Slug) = GetRaw(Project.Slug) Then
  763. Log($"[Comp] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
  764. '如果只有一个有 DatabaseEntry,设置给另外一个
  765. If DatabaseEntry Is Nothing AndAlso Project.DatabaseEntry IsNot Nothing Then DatabaseEntry = Project.DatabaseEntry
  766. If DatabaseEntry IsNot Nothing AndAlso Project.DatabaseEntry Is Nothing Then Project.DatabaseEntry = DatabaseEntry
  767. Return True
  768. End If
  769. Return False
  770. End Function
  771. Public Overrides Function ToString() As String
  772. Return $"{Id} ({Slug}): {RawName}"
  773. End Function
  774. Public Overrides Function Equals(obj As Object) As Boolean
  775. Dim project = TryCast(obj, CompProject)
  776. Return project IsNot Nothing AndAlso Id = project.Id
  777. End Function
  778. Public Shared Operator =(left As CompProject, right As CompProject) As Boolean
  779. Return EqualityComparer(Of CompProject).Default.Equals(left, right)
  780. End Operator
  781. Public Shared Operator <>(left As CompProject, right As CompProject) As Boolean
  782. Return Not left = right
  783. End Operator
  784. End Class
  785. '输入与输出
  786. Public Class CompProjectRequest
  787. '结果要求
  788. ''' <summary>
  789. ''' 加载后应输出到的结果存储器。
  790. ''' </summary>
  791. Public Storage As CompProjectStorage
  792. ''' <summary>
  793. ''' 应当尽量达成的结果数量。
  794. ''' </summary>
  795. Public TargetResultCount As Integer
  796. ''' <summary>
  797. ''' 根据加载位置记录,是否还可以继续获取内容。
  798. ''' </summary>
  799. Public ReadOnly Property CanContinue As Boolean
  800. Get
  801. If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(CompSourceType.CurseForge) Then Storage.CurseForgeTotal = 0
  802. If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(CompSourceType.Modrinth) Then Storage.ModrinthTotal = 0
  803. If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
  804. Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
  805. End Get
  806. End Property
  807. '输入内容
  808. ''' <summary>
  809. ''' 筛选资源种类。
  810. ''' </summary>
  811. Public Type As CompType
  812. ''' <summary>
  813. ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
  814. ''' </summary>
  815. Public Tag As String = ""
  816. ''' <summary>
  817. ''' 筛选 Mod 加载器类别。
  818. ''' </summary>
  819. Public ModLoader As CompModLoaderType = CompModLoaderType.Any
  820. ''' <summary>
  821. ''' 筛选 MC 版本。
  822. ''' </summary>
  823. Public GameVersion As String = Nothing
  824. ''' <summary>
  825. ''' 搜索的初始内容。
  826. ''' </summary>
  827. Public SearchText As String = Nothing
  828. ''' <summary>
  829. ''' 允许的来源。
  830. ''' </summary>
  831. Public Sources As CompSourceType = CompSourceType.Any
  832. Public Sub New(Type As CompType, Storage As CompProjectStorage, TargetResultCount As Integer)
  833. Me.Type = Type
  834. Me.Storage = Storage
  835. Me.TargetResultCount = TargetResultCount
  836. End Sub
  837. '构造请求
  838. ''' <summary>
  839. ''' 获取对应的 CurseForge API 请求链接。
  840. ''' </summary>
  841. Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  842. If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
  843. '应用筛选参数
  844. Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={COMP_PAGE_SIZE}"
  845. Select Case Type
  846. Case CompType.Mod
  847. Address += "&classId=6"
  848. Case CompType.ModPack
  849. Address += "&classId=4471"
  850. Case CompType.DataPack
  851. Address += "&classId=6945"
  852. Case CompType.Shader
  853. Address += "&classId=6552"
  854. Case CompType.ResourcePack
  855. Address += "&classId=12"
  856. End Select
  857. If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
  858. If ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
  859. If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
  860. If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & Net.WebUtility.UrlEncode(SearchText)
  861. If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
  862. Return Address
  863. End Function
  864. ''' <summary>
  865. ''' 获取对应的 Modrinth API 请求链接。
  866. ''' </summary>
  867. Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
  868. If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
  869. '应用筛选参数
  870. Dim Address As String = $"https://api.modrinth.com/v2/search?limit={COMP_PAGE_SIZE}&index=relevance"
  871. If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & Net.WebUtility.UrlEncode(SearchText)
  872. If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
  873. 'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
  874. Dim Facets As New List(Of String)
  875. Facets.Add($"[""project_type:{GetStringFromEnum(Type).Lower}""]")
  876. If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
  877. If ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).Lower}'""]")
  878. If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
  879. Address += "&facets=[" & String.Join(",", Facets) & "]"
  880. Return Address
  881. End Function
  882. '相同判断
  883. Public Overrides Function Equals(obj As Object) As Boolean
  884. Dim request = TryCast(obj, CompProjectRequest)
  885. Return request IsNot Nothing AndAlso
  886. Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
  887. Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Sources = request.Sources AndAlso
  888. GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
  889. End Function
  890. Public Shared Operator =(left As CompProjectRequest, right As CompProjectRequest) As Boolean
  891. Return EqualityComparer(Of CompProjectRequest).Default.Equals(left, right)
  892. End Operator
  893. Public Shared Operator <>(left As CompProjectRequest, right As CompProjectRequest) As Boolean
  894. Return Not left = right
  895. End Operator
  896. Public Overrides Function GetHashCode() As Integer
  897. Return (Type, Tag, ModLoader, GameVersion, SearchText, Sources).GetHashCode()
  898. End Function
  899. End Class
  900. Public Class CompProjectStorage
  901. '加载位置记录
  902. Public CurseForgeOffset As Integer = 0
  903. Public CurseForgeTotal As Integer = -1
  904. Public ModrinthOffset As Integer = 0
  905. Public ModrinthTotal As Integer = -1
  906. '结果列表
  907. ''' <summary>
  908. ''' 可供展示的所有工程的列表。
  909. ''' </summary>
  910. Public Results As New List(Of CompProject)
  911. ''' <summary>
  912. ''' 当前的错误信息。如果没有则为 Nothing。
  913. ''' </summary>
  914. Public ErrorMessage As String = Nothing
  915. End Class
  916. '实际的获取
  917. Private Const COMP_PAGE_SIZE = 40
  918. Private Const MSG_NO_CHINESE_SEARCH_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
  919. ''' <summary>
  920. ''' 已知工程信息的缓存。
  921. ''' </summary>
  922. Public CompProjectCache As New Dictionary(Of String, CompProject)
  923. ''' <summary>
  924. ''' 根据搜索请求获取一系列的工程列表。需要基于加载器运行。
  925. ''' </summary>
  926. Public Sub CompProjectsGet(Task As LoaderTask(Of CompProjectRequest, Integer))
  927. Dim Request As CompProjectRequest = Task.Input
  928. Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
  929. #Region "前置检查"
  930. If Storage.Results.Count >= Request.TargetResultCount Then
  931. Log($"[Comp] 已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
  932. Return
  933. ElseIf Not Request.CanContinue Then
  934. If Not Storage.Results.Any() Then
  935. Throw New Exception("没有符合条件的结果")
  936. Else
  937. Log($"[Comp] 已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
  938. Return
  939. End If
  940. End If
  941. '拒绝 1.13- Quilt(这个版本根本没有 Quilt)
  942. If Request.ModLoader = CompModLoaderType.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
  943. Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
  944. End If
  945. #End Region
  946. #Region "中文搜索"
  947. Dim RawSearchText As String = If(Request.SearchText, "").Trim
  948. RawSearchText = RawSearchText.Lower
  949. Log("[Comp] 工程列表搜索原始文本:" & RawSearchText)
  950. Dim IsChineseSearch As Boolean =
  951. RegexCheck(RawSearchText, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
  952. (Request.Type = CompType.Mod OrElse Request.Type = CompType.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
  953. Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
  954. If IsChineseSearch Then
  955. '帮助方法:从搜索项提取可能的英文单词
  956. Dim ExtractWords =
  957. Function(Result As SearchEntry(Of CompDatabaseEntry), Source As CompSourceType) As IEnumerable(Of String)
  958. '从各个可能的来源提取候选
  959. Dim Candidates As New List(Of String)
  960. If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = CompSourceType.CurseForge Then
  961. Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
  962. End If
  963. If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = CompSourceType.Modrinth Then
  964. Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
  965. End If
  966. Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
  967. Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
  968. '分词、清洗、去重
  969. Candidates = Candidates.
  970. SelectMany(Function(c) c.Split(" ")).
  971. Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
  972. Where(
  973. Function(w)
  974. If w.Length <= 1 Then Return False '单字或空白
  975. If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
  976. If Val(w) > 0 Then Return False '数字
  977. Return True
  978. End Function).Distinct.ToList
  979. '如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
  980. Dim CanForm As Func(Of String, Boolean) = Nothing
  981. CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
  982. 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()
  983. Return Candidates
  984. End Function
  985. 'CurseForge
  986. If Request.Sources.HasFlag(CompSourceType.CurseForge) Then
  987. '数据库搜索
  988. Static CurseForgeSearchEntries As List(Of SearchEntry(Of CompDatabaseEntry)) =
  989. CompDatabase.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
  990. Function(Entry) New SearchEntry(Of CompDatabaseEntry) With {
  991. .Item = Entry,
  992. .SearchSource = New List(Of SearchSource) From {
  993. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
  994. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
  995. }).ToList
  996. Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
  997. If CurseForgeSearchResults.Any Then
  998. '选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
  999. Dim CurseForgeTarget =
  1000. If(CurseForgeSearchResults.First.AbsoluteRight,
  1001. CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
  1002. CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
  1003. MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
  1004. '后处理
  1005. CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, CompSourceType.CurseForge).Join(" ")
  1006. Log("[Comp] 中文搜索关键词(CurseForge):" & CurseForgeAltSearchText, LogLevel.Debug)
  1007. End If
  1008. End If
  1009. 'Modrinth
  1010. If Request.Sources.HasFlag(CompSourceType.Modrinth) Then
  1011. '数据库搜索
  1012. Static ModrinthSearchEntries As List(Of SearchEntry(Of CompDatabaseEntry)) =
  1013. CompDatabase.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
  1014. Function(Entry) New SearchEntry(Of CompDatabaseEntry) With {
  1015. .Item = Entry,
  1016. .SearchSource = New List(Of SearchSource) From {
  1017. New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
  1018. New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
  1019. }).ToList
  1020. Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
  1021. If ModrinthSearchResults.Any Then
  1022. '分词
  1023. Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
  1024. For Each Result In ModrinthSearchResults
  1025. For Each Word In ExtractWords(Result, CompSourceType.Modrinth)
  1026. If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
  1027. Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
  1028. WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
  1029. Next
  1030. Next
  1031. ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
  1032. Log("[Comp] 中文搜索关键词(Modrinth):" & ModrinthAltSearchText, LogLevel.Debug)
  1033. '直接请求工程
  1034. ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
  1035. End If
  1036. End If
  1037. '结束
  1038. If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
  1039. Throw New Exception(MSG_NO_CHINESE_SEARCH_RESULT)
  1040. End If
  1041. End If
  1042. Task.Progress = 0.05
  1043. #End Region
  1044. Dim RealResults As New List(Of CompProject)
  1045. NextPage:
  1046. Dim RawResults As New SafeList(Of CompProject)
  1047. #Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
  1048. Dim WorkThreads As New List(Of Thread)
  1049. Dim Errors As New SafeList(Of (Ex As Exception, Source As CompSourceType))
  1050. '在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
  1051. '所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
  1052. Dim IgnoreModLoaderFilter = Request.ModLoader = CompModLoaderType.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
  1053. Try
  1054. 'CurseForge 搜索
  1055. If Request.Sources.HasFlag(CompSourceType.CurseForge) AndAlso
  1056. Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
  1057. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  1058. WorkThreads.Add(RunInNewThread(
  1059. Sub()
  1060. Try
  1061. '获取工程列表
  1062. Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  1063. Log("[Comp] 开始 CurseForge 搜索:" & CurseForgeUrl)
  1064. Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
  1065. Dim ProjectList As New List(Of CompProject)
  1066. For Each JsonEntry As JObject In RequestResult("data")
  1067. Dim Project As New CompProject(JsonEntry)
  1068. If Request.Type = CompType.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
  1069. ProjectList.Add(Project)
  1070. Next
  1071. '更新结果
  1072. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  1073. Storage.CurseForgeOffset += RequestResult("data").Count
  1074. Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
  1075. Log($"[Comp] 从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
  1076. Catch ex As Exception
  1077. Log(ex, "CurseForge 搜索失败")
  1078. Storage.CurseForgeTotal = -1
  1079. Errors.Add((ex, CompSourceType.CurseForge))
  1080. End Try
  1081. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  1082. End Sub, "CurseForge Search"))
  1083. End If
  1084. 'Modrinth 搜索
  1085. If Request.Sources.HasFlag(CompSourceType.Modrinth) AndAlso
  1086. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  1087. (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
  1088. WorkThreads.Add(RunInNewThread(
  1089. Sub()
  1090. Try
  1091. Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
  1092. Log("[Comp] 开始 Modrinth 搜索:" & ModrinthUrl)
  1093. Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
  1094. Dim ProjectList As New List(Of CompProject)
  1095. For Each JsonEntry As JObject In RequestResult("hits")
  1096. ProjectList.Add(New CompProject(JsonEntry))
  1097. Next
  1098. '更新结果
  1099. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  1100. Storage.ModrinthOffset += RequestResult("hits").Count
  1101. Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
  1102. Log($"[Comp] 从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
  1103. Catch ex As Exception
  1104. Log(ex, "Modrinth 搜索失败")
  1105. Storage.ModrinthTotal = -1
  1106. Errors.Add((ex, CompSourceType.Modrinth))
  1107. End Try
  1108. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  1109. End Sub, "Modrinth Search"))
  1110. End If
  1111. 'Modrinth 直接获取工程
  1112. If Request.Sources.HasFlag(CompSourceType.Modrinth) AndAlso
  1113. Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
  1114. ModrinthSlugs.Any Then '有直接获取的 Slug
  1115. WorkThreads.Add(RunInNewThread(
  1116. Sub()
  1117. Try
  1118. Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
  1119. Log("[Comp] 开始 Modrinth 直接获取:" & ModrinthUrl)
  1120. Dim ProjectList As New List(Of CompProject)
  1121. For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
  1122. Dim Project As New CompProject(JsonEntry)
  1123. '应用筛选
  1124. If Request.Type <> CompType.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
  1125. If Not String.IsNullOrEmpty(Request.Tag) AndAlso
  1126. Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
  1127. If Request.ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter AndAlso
  1128. Not Project.ModLoaders.Any(Function(m) m = Request.ModLoader) Then Continue For
  1129. If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
  1130. Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
  1131. ProjectList.Add(Project)
  1132. Next
  1133. '更新结果
  1134. ProjectList.ForEach(Sub(p) RawResults.Add(p))
  1135. Log($"[Comp] 从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
  1136. ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
  1137. Catch ex As Exception
  1138. Log(ex, "Modrinth 直接获取失败")
  1139. Errors.Add((ex, CompSourceType.Modrinth))
  1140. End Try
  1141. If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
  1142. End Sub, "Modrinth Get"))
  1143. End If
  1144. '等待线程结束
  1145. For Each Thread In WorkThreads
  1146. Thread.Join()
  1147. If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
  1148. Next
  1149. '筛除不是 Forge 的 Mod
  1150. If IgnoreModLoaderFilter Then
  1151. RawResults = RawResults.Where(Function(p) Not p.ModLoaders.Any() OrElse p.ModLoaders.Contains(CompModLoaderType.Forge)).ToList
  1152. End If
  1153. '确保存在结果
  1154. Storage.ErrorMessage = Nothing
  1155. If Not RawResults.Any() Then
  1156. If Errors.Any() Then
  1157. Throw Errors.First.Ex
  1158. Else
  1159. If IsChineseSearch AndAlso Not (Request.Type = CompType.Mod OrElse Request.Type = CompType.DataPack) Then
  1160. Throw New Exception(MSG_NO_CHINESE_SEARCH_RESULT)
  1161. ElseIf Request.Sources = CompSourceType.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
  1162. Throw New Exception("CurseForge 不兼容所选的类型")
  1163. ElseIf Request.Sources = CompSourceType.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
  1164. Throw New Exception("Modrinth 不兼容所选的类型")
  1165. Else
  1166. Throw New Exception("没有搜索结果")
  1167. End If
  1168. End If
  1169. ElseIf Errors.Any() Then
  1170. '有结果但是有错误
  1171. If Errors.Any(Function(e) e.Source = CompSourceType.CurseForge) Then
  1172. Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  1173. Else
  1174. Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
  1175. End If
  1176. End If
  1177. Finally
  1178. For Each Thread In WorkThreads
  1179. If Thread.IsAlive Then Thread.Interrupt()
  1180. Next
  1181. End Try
  1182. #End Region
  1183. #Region "提取非重复项,存储于 RealResults"
  1184. '将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
  1185. RawResults = RawResults.Where(Function(x) x.FromCurseForge).Concat(RawResults.Where(Function(x) Not x.FromCurseForge)).ToList
  1186. '去重
  1187. RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b))
  1188. '已有内容去重
  1189. RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
  1190. Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
  1191. '加入列表
  1192. RealResults.AddRange(RawResults)
  1193. Log($"[Comp] 去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
  1194. #End Region
  1195. #Region "检查结果数量,如果不足且可继续,会继续加载下一页"
  1196. If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
  1197. Log($"[Comp] 总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
  1198. If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
  1199. Log("[Comp] 将继续尝试加载下一页")
  1200. GoTo NextPage
  1201. Else
  1202. Log("[Comp] 无法继续加载,将强制结束")
  1203. End If
  1204. End If
  1205. #End Region
  1206. #Region "将结果排序并添加"
  1207. Dim Scores As New Dictionary(Of CompProject, Double) '排序分
  1208. Dim GetDownloadCountMult =
  1209. Function(Project As CompProject) As Double
  1210. Select Case Request.Type
  1211. Case CompType.Mod, CompType.ModPack
  1212. Return If(Project.FromCurseForge, 1, 5)
  1213. Case CompType.DataPack
  1214. Return If(Project.FromCurseForge, 10, 1)
  1215. Case CompType.ResourcePack, CompType.Shader
  1216. Return If(Project.FromCurseForge, 1, 4)
  1217. Case Else
  1218. Return 1
  1219. End Select
  1220. End Function
  1221. If String.IsNullOrEmpty(RawSearchText) Then
  1222. '如果没有搜索文本,按下载量将结果排序
  1223. For Each Result As CompProject In RealResults
  1224. Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
  1225. Next
  1226. Else
  1227. '如果有搜索文本,按关联度将结果排序
  1228. '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
  1229. Dim SearchEntries As New List(Of SearchEntry(Of CompProject))
  1230. For Each Result As CompProject In RealResults
  1231. Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
  1232. Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
  1233. SearchEntries.Add(New SearchEntry(Of CompProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
  1234. New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1),
  1235. New SearchSource(Result.Description, 0.05)}})
  1236. Next
  1237. Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
  1238. For Each OneResult In SearchResult
  1239. Scores(OneResult.Item) +=
  1240. If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
  1241. If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
  1242. Next
  1243. End If
  1244. '根据排序分得出结果并添加
  1245. If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
  1246. Storage.Results.AddRange(
  1247. Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
  1248. #End Region
  1249. End Sub
  1250. #End Region
  1251. #Region "CompFile | 文件信息"
  1252. '类定义
  1253. Public Enum CompFileStatus
  1254. Release = 1 '枚举值来源:https://docs.curseforge.com/#tocS_FileReleaseType
  1255. Beta = 2
  1256. Alpha = 3
  1257. End Enum
  1258. Public Class CompFile
  1259. '源信息
  1260. ''' <summary>
  1261. ''' 文件的种类。
  1262. ''' </summary>
  1263. Public ReadOnly Type As CompType
  1264. ''' <summary>
  1265. ''' 该文件来自 CurseForge 还是 Modrinth。
  1266. ''' </summary>
  1267. Public ReadOnly FromCurseForge As Boolean
  1268. ''' <summary>
  1269. ''' 用于唯一性鉴别该文件的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
  1270. ''' </summary>
  1271. Public ReadOnly Id As String
  1272. '描述性信息
  1273. ''' <summary>
  1274. ''' 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。
  1275. ''' </summary>
  1276. Public DisplayName As String
  1277. ''' <summary>
  1278. ''' 发布时间。
  1279. ''' </summary>
  1280. Public ReadOnly ReleaseDate As Date
  1281. ''' <summary>
  1282. ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量,且 CurseForge 可能错误地返回 0。
  1283. ''' </summary>
  1284. Public ReadOnly DownloadCount As Integer
  1285. ''' <summary>
  1286. ''' Mod 版本号。
  1287. ''' 不一定是标准格式。CurseForge 上默认为 Nothing。
  1288. ''' </summary>
  1289. Public Version As String
  1290. ''' <summary>
  1291. ''' 支持的 Mod 加载器列表。可能为空。
  1292. ''' </summary>
  1293. Public ReadOnly ModLoaders As List(Of CompModLoaderType)
  1294. ''' <summary>
  1295. ''' 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
  1296. ''' </summary>
  1297. Public ReadOnly GameVersions As List(Of String)
  1298. ''' <summary>
  1299. ''' 发布状态:Release/Beta/Alpha。
  1300. ''' </summary>
  1301. Public ReadOnly Status As CompFileStatus
  1302. ''' <summary>
  1303. ''' 发布状态的友好描述。例如:"正式版","Beta 版"。
  1304. ''' </summary>
  1305. Public ReadOnly Property StatusDescription As String
  1306. Get
  1307. Select Case Status
  1308. Case CompFileStatus.Release
  1309. Return "正式版"
  1310. Case CompFileStatus.Beta
  1311. Return If(ModeDebug, "Beta 版", "测试版")
  1312. Case Else
  1313. Return If(ModeDebug, "Alpha 版", "早期测试版")
  1314. End Select
  1315. End Get
  1316. End Property
  1317. '下载信息
  1318. ''' <summary>
  1319. ''' 下载信息是否可用。
  1320. ''' </summary>
  1321. Public ReadOnly Property Available As Boolean
  1322. Get
  1323. Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
  1324. End Get
  1325. End Property
  1326. ''' <summary>
  1327. ''' 下载的文件名。
  1328. ''' </summary>
  1329. Public ReadOnly FileName As String = Nothing
  1330. ''' <summary>
  1331. ''' 文件所有可能的下载源。
  1332. ''' </summary>
  1333. Public DownloadUrls As List(Of String)
  1334. ''' <summary>
  1335. ''' 文件的 SHA1 或 MD5。
  1336. ''' </summary>
  1337. Public ReadOnly Hash As String = Nothing
  1338. ''' <summary>
  1339. ''' 文件大小。不可用时为 -1。
  1340. ''' </summary>
  1341. Public ReadOnly Size As Integer = -1
  1342. ''' <summary>
  1343. ''' 该文件的所有依赖工程的原始 ID。
  1344. ''' 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
  1345. ''' </summary>
  1346. Public ReadOnly RawDependencies As New List(Of String)
  1347. ''' <summary>
  1348. ''' 该文件的所有依赖工程的 Project.Id。
  1349. ''' </summary>
  1350. Public ReadOnly Dependencies As New List(Of String)
  1351. ''' <summary>
  1352. ''' 获取下载信息。
  1353. ''' </summary>
  1354. ''' <param name="LocalAddress">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
  1355. Public Function ToNetFile(LocalAddress As String) As NetFile
  1356. Return New NetFile(DownloadUrls, LocalAddress & If(LocalAddress.EndsWithF("\"), FileName, ""), New FileChecker(Hash:=Hash, ActualSize:=Size), SimulateBrowserHeaders:=True)
  1357. End Function
  1358. '实例化
  1359. ''' <summary>
  1360. ''' 从 json 中初始化实例。若出错会抛出异常。
  1361. ''' </summary>
  1362. Public Sub New(Data As JObject, DefaultType As CompType)
  1363. Type = DefaultType
  1364. If Data.ContainsKey("FromCurseForge") Then
  1365. #Region "CompJson"
  1366. FromCurseForge = Data("FromCurseForge").ToObject(Of Boolean)
  1367. Id = Data("Id").ToString
  1368. DisplayName = Data("DisplayName").ToString
  1369. If Data.ContainsKey("Version") Then Version = Data("Version").ToString
  1370. ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
  1371. DownloadCount = Data("DownloadCount").ToObject(Of Integer)
  1372. Status = CType(Data("Status").ToObject(Of Integer), CompFileStatus)
  1373. If Data.ContainsKey("FileName") Then FileName = Data("FileName").ToString
  1374. If Data.ContainsKey("DownloadUrls") Then DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
  1375. If Data.ContainsKey("ModLoaders") Then ModLoaders = Data("ModLoaders").ToObject(Of List(Of CompModLoaderType))
  1376. If Data.ContainsKey("Size") Then Size = Data("Size").ToObject(Of Integer)
  1377. If Data.ContainsKey("Hash") Then Hash = Data("Hash").ToString
  1378. If Data.ContainsKey("GameVersions") Then GameVersions = Data("GameVersions").ToObject(Of List(Of String))
  1379. If Data.ContainsKey("RawDependencies") Then RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
  1380. If Data.ContainsKey("Dependencies") Then Dependencies = Data("Dependencies").ToObject(Of List(Of String))
  1381. #End Region
  1382. Else
  1383. FromCurseForge = Data.ContainsKey("gameId")
  1384. If FromCurseForge Then
  1385. #Region "CurseForge"
  1386. '简单信息
  1387. Id = Data("id")
  1388. DisplayName = Data("displayName").ToString.Replace(" ", "").Trim(" ")
  1389. Version = Nothing
  1390. ReleaseDate = Data("fileDate")
  1391. Status = CType(Data("releaseType").ToObject(Of Integer), CompFileStatus)
  1392. DownloadCount = Data("downloadCount")
  1393. FileName = Data("fileName")
  1394. Size = Data("fileLength")
  1395. Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
  1396. If Hash Is Nothing Then Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
  1397. 'DownloadAddress
  1398. Dim Url = Data("downloadUrl").ToString
  1399. If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}"
  1400. DownloadUrls = HandleCurseForgeDownloadUrls(Url.Replace(FileName, WebUtility.UrlEncode(FileName))) '对脑残 CurseForge 的下载地址进行多种修正
  1401. DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
  1402. DownloadUrls = DownloadUrls.Distinct.ToList '最终去重
  1403. 'Dependencies
  1404. If Data.ContainsKey("dependencies") Then
  1405. RawDependencies = Data("dependencies").
  1406. Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
  1407. d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
  1408. Select(Function(d) d("modId").ToString).ToList
  1409. End If
  1410. 'GameVersions
  1411. Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
  1412. GameVersions = RawVersions.
  1413. Where(Function(v) McVersion.IsFormatFit(v)).
  1414. Select(Function(v) v.Replace("-snapshot", " 预览版")).
  1415. Distinct.ToList
  1416. If GameVersions.IsSingle Then
  1417. GameVersions = GameVersions.ToList
  1418. ElseIf GameVersions.Count > 1 Then
  1419. GameVersions = GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
  1420. If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)} '整合包理应只 “支持” 一个版本
  1421. Else
  1422. GameVersions = New List(Of String) From {"未知版本"}
  1423. End If
  1424. 'ModLoaders
  1425. ModLoaders = New List(Of CompModLoaderType)
  1426. If RawVersions.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
  1427. If RawVersions.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
  1428. If RawVersions.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
  1429. If RawVersions.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge)
  1430. #End Region
  1431. Else
  1432. #Region "Modrinth"
  1433. '简单信息
  1434. Id = Data("id")
  1435. DisplayName = Data("name").ToString.Replace(" ", "").Trim(" ")
  1436. Version = Data("version_number")
  1437. ReleaseDate = Data("date_published")
  1438. Status = If(Data("version_type").ToString = "release", CompFileStatus.Release, If(Data("version_type").ToString = "beta", CompFileStatus.Beta, CompFileStatus.Alpha))
  1439. DownloadCount = Data("downloads")
  1440. If CType(Data("files"), JArray).Any() Then '可能为空
  1441. Dim File As JToken = Data("files")(0)
  1442. FileName = File("filename")
  1443. DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
  1444. Size = File("size")
  1445. Hash = File("hashes")("sha1")
  1446. End If
  1447. 'Loaders
  1448. '结果可能混杂着 Mod、数据包和服务端插件
  1449. Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
  1450. ModLoaders = New List(Of CompModLoaderType)
  1451. If Type.HasFlag(CompType.Mod) OrElse Type.HasFlag(CompType.DataPack) Then
  1452. If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then Type = CompType.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
  1453. If RawLoaders.Contains("datapack") Then Type = CompType.DataPack
  1454. If RawLoaders.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge) : Type = CompType.Mod
  1455. If RawLoaders.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge) : Type = CompType.Mod
  1456. If RawLoaders.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric) : Type = CompType.Mod
  1457. If RawLoaders.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt) : Type = CompType.Mod
  1458. Else
  1459. '使用传入的类别,不作修改(#8377)
  1460. End If
  1461. 'Dependencies
  1462. If Data.ContainsKey("dependencies") Then
  1463. RawDependencies = Data("dependencies").
  1464. Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
  1465. d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
  1466. d("project_id").ToString.Length > 0). '有时候真的会空……
  1467. Select(Function(d) d("project_id").ToString).ToList
  1468. End If
  1469. 'GameVersions
  1470. Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
  1471. GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
  1472. Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
  1473. Distinct.ToList
  1474. If GameVersions.IsSingle Then
  1475. '无需处理
  1476. ElseIf GameVersions.Count > 1 Then
  1477. GameVersions = GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
  1478. If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)} '整合包理应只 “支持” 一个版本
  1479. ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")) Then
  1480. GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")).ToList
  1481. Else
  1482. GameVersions = New List(Of String) From {"未知版本"}
  1483. End If
  1484. #End Region
  1485. End If
  1486. End If
  1487. End Sub
  1488. ''' <summary>
  1489. ''' 重新整理 CurseForge 的下载地址。
  1490. ''' </summary>
  1491. Public Shared Function HandleCurseForgeDownloadUrls(Url As String) As List(Of String)
  1492. Return {
  1493. Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  1494. Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
  1495. Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
  1496. Url.Replace("://media.", "://edge."),
  1497. Url
  1498. }.Distinct.ToList
  1499. End Function
  1500. ''' <summary>
  1501. ''' 将当前实例转为可用于保存缓存的 Json。
  1502. ''' </summary>
  1503. Public Function ToJson() As JObject
  1504. Dim Json As New JObject
  1505. Json.Add("FromCurseForge", FromCurseForge)
  1506. Json.Add("Id", Id)
  1507. If Version IsNot Nothing Then Json.Add("Version", Version)
  1508. Json.Add("DisplayName", DisplayName)
  1509. Json.Add("ReleaseDate", ReleaseDate)
  1510. Json.Add("DownloadCount", DownloadCount)
  1511. Json.Add("ModLoaders", New JArray(ModLoaders.Select(Function(m) CInt(m))))
  1512. Json.Add("GameVersions", New JArray(GameVersions))
  1513. Json.Add("Status", CInt(Status))
  1514. If FileName IsNot Nothing Then Json.Add("FileName", FileName)
  1515. If DownloadUrls IsNot Nothing Then Json.Add("DownloadUrls", New JArray(DownloadUrls))
  1516. If Hash IsNot Nothing Then Json.Add("Hash", Hash)
  1517. If Size >= 0 Then Json.Add("Size", Size)
  1518. Json.Add("RawDependencies", New JArray(RawDependencies))
  1519. Json.Add("Dependencies", New JArray(Dependencies))
  1520. Return Json
  1521. End Function
  1522. ''' <summary>
  1523. ''' 将当前文件信息实例化为控件。
  1524. ''' </summary>
  1525. Public Function ToListItem(OnClick As MyListItem.ClickEventHandler, Optional OnSaveClick As MyIconButton.ClickEventHandler = Nothing,
  1526. Optional BadDisplayName As Boolean = False) As MyVirtualizingElement(Of MyListItem)
  1527. Return New MyVirtualizingElement(Of MyListItem)(
  1528. Function()
  1529. '获取描述信息
  1530. Dim Title As String = If(BadDisplayName, FileName, DisplayName)
  1531. Dim Info As New List(Of String)
  1532. If Title <> FileName.BeforeLast(".") Then Info.Add(FileName.BeforeLast("."))
  1533. If Dependencies.Any Then Info.Add(Dependencies.Count & " 项前置")
  1534. If GameVersions.All(
  1535. Function(VerName)
  1536. Return Not VerName.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) VerName.ContainsF(s, True))
  1537. End Function) Then Info.Add($"游戏版本 {GameVersions.Join("、"c)}")
  1538. If DownloadCount > 0 Then 'CurseForge 的下载次数经常错误地返回 0
  1539. Info.Add("下载 " & If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万次", DownloadCount & " 次"))
  1540. End If
  1541. Info.Add("更新于 " & GetTimeSpanString(ReleaseDate - Date.Now, False))
  1542. If Status <> CompFileStatus.Release Then Info.Add(StatusDescription)
  1543. '建立控件
  1544. Dim NewItem As New MyListItem With {
  1545. .Title = Title,
  1546. .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Me,
  1547. .Info = Info.Join(","c)
  1548. }
  1549. Select Case Status
  1550. Case CompFileStatus.Release
  1551. NewItem.Logo = PathImage & "Icons/R.png"
  1552. Case CompFileStatus.Beta
  1553. NewItem.Logo = PathImage & "Icons/B.png"
  1554. Case Else 'Alpha
  1555. NewItem.Logo = PathImage & "Icons/A.png"
  1556. End Select
  1557. AddHandler NewItem.Click, OnClick
  1558. '建立另存为按钮
  1559. If OnSaveClick IsNot Nothing Then
  1560. Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
  1561. ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
  1562. ToolTipService.SetVerticalOffset(BtnSave, 30)
  1563. ToolTipService.SetHorizontalOffset(BtnSave, 2)
  1564. AddHandler BtnSave.Click, OnSaveClick
  1565. NewItem.Buttons = {BtnSave}
  1566. End If
  1567. Return NewItem
  1568. End Function) With {.Height = 42}
  1569. End Function
  1570. Public Overrides Function ToString() As String
  1571. Return $"{Id}: {FileName}"
  1572. End Function
  1573. End Class
  1574. '获取
  1575. ''' <summary>
  1576. ''' 已知文件信息的缓存。
  1577. ''' </summary>
  1578. Public CompFilesCache As New Dictionary(Of String, List(Of CompFile))
  1579. ''' <summary>
  1580. ''' 获取某个工程下的全部文件列表。
  1581. ''' 必须在工作线程执行,失败会抛出异常。
  1582. ''' </summary>
  1583. Public Function CompFilesGet(ProjectId As String, FromCurseForge As Boolean) As List(Of CompFile)
  1584. '获取工程对象
  1585. Dim TargetProject As CompProject
  1586. If CompProjectCache.ContainsKey(ProjectId) Then '存在缓存
  1587. TargetProject = CompProjectCache(ProjectId)
  1588. ElseIf FromCurseForge Then 'CurseForge
  1589. TargetProject = New CompProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data"))
  1590. Else 'Modrinth
  1591. TargetProject = New CompProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId))
  1592. End If
  1593. '获取工程对象的文件列表
  1594. If Not CompFilesCache.ContainsKey(ProjectId) Then '有缓存也不能直接返回,这时候前置可能没获取(#5173)
  1595. Log("[Comp] 开始获取文件列表:" & ProjectId)
  1596. Dim ResultJsonArray As JArray = Nothing
  1597. If FromCurseForge Then
  1598. 'CurseForge
  1599. For RetryCount As Integer = 0 To 2
  1600. Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
  1601. (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
  1602. 'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
  1603. '之前只请求一部分文件的方法备份如下:
  1604. 'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
  1605. ' ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
  1606. 'Else '否则使用全部文件
  1607. ' ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
  1608. 'End If
  1609. If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
  1610. ResultJsonArray = ResultJson("data")
  1611. Exit For
  1612. ElseIf RetryCount < 2 Then
  1613. Log($"[Comp] CurseForge 返回的文件列表存在缺失,即将进行第 {RetryCount + 1} 次重试", LogLevel.Debug) '#6224
  1614. Log($"[Comp] 返回的原始内容如下:{vbCrLf}{ResultJson}")
  1615. Else
  1616. Log($"[Comp] CurseForge 返回的文件列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
  1617. Throw New Exception("CurseForge 返回的文件列表存在缺失")
  1618. End If
  1619. Next
  1620. Else
  1621. 'Modrinth
  1622. ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
  1623. End If
  1624. CompFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Types)).
  1625. Where(Function(a) a.Available).ToList.
  1626. Distinct(Function(a, b) a.Id = b.Id) 'CurseForge 可能会重复返回相同项(#1330)
  1627. End If
  1628. '获取前置列表
  1629. Dim Deps As List(Of String) = CompFilesCache(ProjectId).SelectMany(Function(f) f.RawDependencies).Distinct().ToList
  1630. Dim UndoneDeps = Deps.Where(Function(f) Not CompProjectCache.ContainsKey(f)).ToList
  1631. '获取前置工程信息
  1632. If UndoneDeps.Any Then
  1633. Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置:{UndoneDeps.Join(","c)}")
  1634. Dim Projects As JArray
  1635. If TargetProject.FromCurseForge Then
  1636. Projects = DlModRequest("https://api.curseforge.com/v1/mods",
  1637. HttpMethod.Post, "{""modIds"": [" & UndoneDeps.Join(","c) & "]}", "application/json")("data")
  1638. Else
  1639. Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{UndoneDeps.Join(""",""")}""]")
  1640. End If
  1641. For Each Project In Projects
  1642. Dim Unused As New CompProject(Project) '在 New 的时候会添加缓存以便之后读取
  1643. Next
  1644. End If
  1645. '更新前置信息
  1646. If Deps.Any Then
  1647. For Each DepProject In Deps.Where(Function(id) CompProjectCache.ContainsKey(id)).Select(Function(id) CompProjectCache(id))
  1648. For Each File In CompFilesCache(ProjectId)
  1649. If File.RawDependencies.Contains(DepProject.Id) AndAlso DepProject.Id <> ProjectId Then
  1650. If Not File.Dependencies.Contains(DepProject.Id) Then File.Dependencies.Add(DepProject.Id)
  1651. End If
  1652. Next
  1653. Next
  1654. End If
  1655. Return CompFilesCache(ProjectId)
  1656. End Function
  1657. ''' <summary>
  1658. ''' 预载包含大量 CompFile 的卡片,添加必要的元素和前置列表。
  1659. ''' </summary>
  1660. Public Sub CompFilesCardPreload(Stack As StackPanel, Files As List(Of CompFile))
  1661. '获取卡片对应的前置 ID
  1662. '如果为整合包就不会有 Dependencies 信息,所以不用管
  1663. Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
  1664. Deps.Sort()
  1665. If Not Deps.Any() Then Return
  1666. Deps = Deps.Where(
  1667. Function(dep)
  1668. If Not CompProjectCache.ContainsKey(dep) Then Log($"[Comp] 未找到 ID {dep} 的前置信息", LogLevel.Debug)
  1669. Return CompProjectCache.ContainsKey(dep)
  1670. End Function).ToList
  1671. '添加开头间隔
  1672. Stack.Children.Add(New TextBlock With {.Text = "前置资源", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
  1673. '添加前置列表
  1674. For Each Dep In Deps
  1675. Dim Item = CompProjectCache(Dep).ToCompItem(False, False)
  1676. Stack.Children.Add(Item)
  1677. Next
  1678. '添加结尾间隔
  1679. Stack.Children.Add(New TextBlock With {.Text = "版本列表", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
  1680. End Sub
  1681. #End Region
  1682. End Module