ModComp.vb 59 KB

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