ModComp.vb 60 KB

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