ModComp.vb 87 KB

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