ResourceProject.vb 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. ''' <summary>
  2. ''' 社区资源的项目信息。
  3. ''' </summary>
  4. Public Class ResourceProject
  5. '源信息
  6. ''' <summary>
  7. ''' 该项目信息的所属平台。
  8. ''' </summary>
  9. Public ReadOnly Platform As ResourcePlatforms
  10. ''' <summary>
  11. ''' 工程中包含的文件种类。
  12. ''' 若为 Modrinth 工程,可能为 Mod 或 数据包。
  13. ''' </summary>
  14. Public ReadOnly Types As ResourceTypes
  15. ''' <summary>
  16. ''' 工程的短名。例如 technical-enchant。
  17. ''' </summary>
  18. Public ReadOnly Slug As String
  19. ''' <summary>
  20. ''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
  21. ''' </summary>
  22. Public ReadOnly Id As String
  23. ''' <summary>
  24. ''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
  25. ''' </summary>
  26. Public ReadOnly CurseForgeFileIds As List(Of Integer)
  27. '描述性信息
  28. ''' <summary>
  29. ''' 原始的英文名称。
  30. ''' </summary>
  31. Public ReadOnly RawName As String
  32. ''' <summary>
  33. ''' 英文描述。
  34. ''' </summary>
  35. Public ReadOnly Description As String
  36. ''' <summary>
  37. ''' 来源网站的工程页面网址。确保格式一定标准。
  38. ''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
  39. ''' Modrinth:https://modrinth.com/mod/technical-enchant
  40. ''' </summary>
  41. Public ReadOnly Website As String
  42. ''' <summary>
  43. ''' 最后一次更新的时间。
  44. ''' </summary>
  45. Public ReadOnly LastUpdate As Date
  46. ''' <summary>
  47. ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
  48. ''' </summary>
  49. Public ReadOnly DownloadCount As Integer
  50. ''' <summary>
  51. ''' 所有支持的 Mod 加载器。可能为 <see cref="ModLoaders.None"/>。
  52. ''' </summary>
  53. Public ReadOnly ModLoaders As ModLoaders = ModLoaders.None
  54. ''' <summary>
  55. ''' 描述性标签的内容。已转换为中文。
  56. ''' </summary>
  57. Public ReadOnly Tags As List(Of String)
  58. ''' <summary>
  59. ''' Logo 图片的下载地址。
  60. ''' 若为 Nothing 则没有,保证不为空字符串。
  61. ''' </summary>
  62. Public LogoUrl As String = Nothing
  63. ''' <summary>
  64. ''' 支持的 Drop 编号,从高到低排序,不为 Nothing。
  65. ''' 例如:261(26.1.x)、180(1.18.x)。
  66. ''' </summary>
  67. Public ReadOnly Drops As List(Of Integer)
  68. ''' <summary>
  69. ''' Modrinth API 返回的原始版本列表。
  70. ''' 仅用于 Modrinth 工程的二次筛选,不会被缓存,不会被预处理。
  71. ''' 若非从 Modrinth 获取则为 Nothing。
  72. ''' </summary>
  73. Public ReadOnly UnsafeGameVersions As List(Of String)
  74. '数据库信息
  75. Private LoadedDatabase As Boolean = False
  76. Private _WikiEntry As WikiEntry = Nothing
  77. ''' <summary>
  78. ''' 关联的数据库条目。若为 Nothing 则没有。
  79. ''' </summary>
  80. Private Property WikiEntry As WikiEntry
  81. Get
  82. If Not LoadedDatabase Then
  83. LoadedDatabase = True
  84. If Types.HasFlag(ResourceTypes.Mod) OrElse Types.HasFlag(ResourceTypes.DataPack) Then
  85. _WikiEntry = WikiEntry.All.FirstOrDefault(Function(c) If(Platform = ResourcePlatforms.CurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
  86. End If
  87. End If
  88. Return _WikiEntry
  89. End Get
  90. Set(value As WikiEntry)
  91. LoadedDatabase = True
  92. _WikiEntry = value
  93. End Set
  94. End Property
  95. ''' <summary>
  96. ''' MC 百科的页面 ID。若为 0 则没有。
  97. ''' </summary>
  98. Public ReadOnly Property WikiId As Integer
  99. Get
  100. Return If(WikiEntry Is Nothing, 0, WikiEntry.Id)
  101. End Get
  102. End Property
  103. ''' <summary>
  104. ''' 翻译后的中文名。若数据库没有则等同于 RawName。
  105. ''' </summary>
  106. Public ReadOnly Property TranslatedName As String
  107. Get
  108. Return If(WikiEntry Is Nothing OrElse WikiEntry.ChineseName = "", RawName, WikiEntry.ChineseName)
  109. End Get
  110. End Property
  111. '实例化
  112. ''' <summary>
  113. ''' 从工程 Json 中初始化实例。若出错会抛出异常。
  114. ''' </summary>
  115. Public Sub New(Data As JObject)
  116. If Data.ContainsKey("Tags") Then
  117. #Region "缓存 JSON"
  118. Platform = Data("Platform").ToObject(Of Integer)
  119. Types = Data("Types").ToObject(Of Integer)
  120. Slug = Data("Slug")
  121. Id = Data("Id")
  122. If Data.ContainsKey("CurseForgeFileIds") Then CurseForgeFileIds = CType(Data("CurseForgeFileIds"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
  123. RawName = Data("RawName")
  124. Description = Data("Description")
  125. Website = Data("Website")
  126. LastUpdate = Data("LastUpdate")
  127. DownloadCount = Data("DownloadCount")
  128. If Data.ContainsKey("ModLoaders") Then ModLoaders = Data("ModLoaders").ToObject(Of Integer)
  129. Tags = CType(Data("Tags"), JArray).Select(Function(t) t.ToString).ToList
  130. If Data.ContainsKey("LogoUrl") Then LogoUrl = Data("LogoUrl")
  131. If Data.ContainsKey("Drops") Then
  132. Drops = CType(Data("Drops"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
  133. Else
  134. Drops = New List(Of Integer)
  135. End If
  136. #End Region
  137. Else
  138. Platform = If(Data.ContainsKey("summary"), ResourcePlatforms.CurseForge, ResourcePlatforms.Modrinth)
  139. If Platform = ResourcePlatforms.CurseForge Then
  140. #Region "CurseForge"
  141. '简单信息
  142. Id = Data("id")
  143. Slug = Data("slug")
  144. RawName = Data("name")
  145. Description = Data("summary")
  146. Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
  147. LastUpdate = Data("dateReleased") '#1194
  148. DownloadCount = Data("downloadCount")
  149. If Data("logo").Count > 0 Then
  150. If Data("logo")("thumbnailUrl") Is Nothing OrElse Data("logo")("thumbnailUrl") = "" Then
  151. LogoUrl = Data("logo")("url")
  152. Else
  153. LogoUrl = Data("logo")("thumbnailUrl")
  154. End If
  155. End If
  156. If LogoUrl = "" Then LogoUrl = Nothing
  157. 'Type
  158. If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
  159. Types = ResourceTypes.Mod
  160. ElseIf Website.Contains("/modpacks/") Then
  161. Types = ResourceTypes.ModPack
  162. ElseIf Website.Contains("/resourcepacks/") Then
  163. Types = ResourceTypes.ResourcePack
  164. ElseIf Website.Contains("/texture-packs/") Then
  165. Types = ResourceTypes.ResourcePack
  166. ElseIf Website.Contains("/shaders/") Then
  167. Types = ResourceTypes.Shader
  168. Else
  169. Types = ResourceTypes.DataPack
  170. End If
  171. 'FileIndexes / VanillaMajorVersions / ModLoaders
  172. ModLoaders = ModLoaders.None
  173. Dim Files As New List(Of KeyValuePair(Of Integer, List(Of String))) 'FileId, GameVersions
  174. For Each File In If(Data("latestFiles"), New JArray)
  175. Dim NewFile = ResourceVersion.FromPlatformJson(File, Types)
  176. If Not NewFile.DownloadAvailable Then Continue For
  177. ModLoaders = ModLoaders Or NewFile.ModLoaders
  178. Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
  179. If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
  180. Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
  181. Next
  182. For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
  183. If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
  184. Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
  185. Next
  186. CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
  187. Drops = Files.SelectMany(Function(f) f.Value).
  188. Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
  189. 'Tags
  190. Tags = New List(Of String)
  191. For Each Category In If(Data("categories"), New JArray). '镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831)
  192. Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
  193. Select Case Category
  194. 'Mod
  195. Case 406 : Tags.Add("世界元素")
  196. Case 407 : Tags.Add("生物群系")
  197. Case 410 : Tags.Add("维度")
  198. Case 408 : Tags.Add("矿物/资源")
  199. Case 409 : Tags.Add("天然结构")
  200. Case 412 : Tags.Add("科技")
  201. Case 415 : Tags.Add("管道/物流")
  202. Case 4843 : Tags.Add("自动化")
  203. Case 417 : Tags.Add("能源")
  204. Case 4558 : Tags.Add("红石")
  205. Case 436 : Tags.Add("食物/烹饪")
  206. Case 416 : Tags.Add("农业")
  207. Case 414 : Tags.Add("运输")
  208. Case 420 : Tags.Add("仓储")
  209. Case 419 : Tags.Add("魔法")
  210. Case 422 : Tags.Add("冒险")
  211. Case 424 : Tags.Add("装饰")
  212. Case 411 : Tags.Add("生物")
  213. Case 434 : Tags.Add("装备")
  214. Case 6814 : Tags.Add("性能优化")
  215. Case 9026 : Tags.Add("创造模式")
  216. Case 423 : Tags.Add("信息显示")
  217. Case 435 : Tags.Add("服务器")
  218. Case 5191 : Tags.Add("改良")
  219. Case 421 : Tags.Add("支持库")
  220. '整合包
  221. Case 4484 : Tags.Add("多人")
  222. Case 4479 : Tags.Add("硬核")
  223. Case 4483 : Tags.Add("战斗")
  224. Case 4478 : Tags.Add("任务")
  225. Case 4472 : Tags.Add("科技")
  226. Case 4473 : Tags.Add("魔法")
  227. Case 4475 : Tags.Add("冒险")
  228. Case 4476 : Tags.Add("探索")
  229. Case 4477 : Tags.Add("小游戏")
  230. Case 4474 : Tags.Add("科幻")
  231. Case 4736 : Tags.Add("空岛")
  232. Case 5128 : Tags.Add("原版改良")
  233. Case 4487 : Tags.Add("FTB")
  234. Case 4480 : Tags.Add("基于地图")
  235. Case 4481 : Tags.Add("轻量")
  236. Case 4482 : Tags.Add("大型")
  237. '资源包
  238. Case 403 : Tags.Add("原版风")
  239. Case 400 : Tags.Add("写实风")
  240. Case 401 : Tags.Add("现代风")
  241. Case 402 : Tags.Add("中世纪")
  242. Case 399 : Tags.Add("蒸汽朋克")
  243. Case 5244 : Tags.Add("含字体")
  244. Case 404 : Tags.Add("动态效果")
  245. Case 4465 : Tags.Add("兼容 Mod")
  246. Case 393 : Tags.Add("16x")
  247. Case 394 : Tags.Add("32x")
  248. Case 395 : Tags.Add("64x")
  249. Case 396 : Tags.Add("128x")
  250. Case 397 : Tags.Add("256x")
  251. Case 398 : Tags.Add("超高清")
  252. Case 5193 : Tags.Add("数据包") '有这个 Tag 的项会从资源包请求中被移除
  253. '光影包
  254. Case 6553 : Tags.Add("写实风")
  255. Case 6554 : Tags.Add("幻想风")
  256. Case 6555 : Tags.Add("原版风")
  257. '数据包
  258. Case 6948 : Tags.Add("冒险")
  259. Case 6949 : Tags.Add("幻想")
  260. Case 6950 : Tags.Add("支持库")
  261. Case 6952 : Tags.Add("魔法")
  262. Case 6946 : Tags.Add("Mod 相关")
  263. Case 6951 : Tags.Add("科技")
  264. Case 6953 : Tags.Add("实用")
  265. End Select
  266. Next
  267. #End Region
  268. Else
  269. #Region "Modrinth"
  270. '简单信息
  271. Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
  272. Slug = Data("slug")
  273. RawName = Data("title")
  274. Description = Data("description")
  275. LastUpdate = If(Data("date_modified"), Data("updated"))
  276. DownloadCount = Data("downloads")
  277. LogoUrl = Data("icon_url")
  278. If LogoUrl = "" Then LogoUrl = Nothing
  279. Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
  280. 'GameVersions
  281. '搜索结果的键为 versions,获取特定工程的键为 game_versions
  282. UnsafeGameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).Select(Function(v) v.ToString).Distinct.ToList
  283. Drops = UnsafeGameVersions.Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
  284. 'Type
  285. Select Case Data("project_type").ToString
  286. Case "modpack" : Types = ResourceTypes.ModPack
  287. Case "resourcepack" : Types = ResourceTypes.ResourcePack
  288. Case "shader" : Types = ResourceTypes.Shader
  289. Case Else : Types = ResourceTypes.Mod 'Modrinth 将数据包标为 Mod,但 categories 字段里有 datapack
  290. End Select
  291. 'Tags & ModLoaders
  292. Tags = New List(Of String)
  293. ModLoaders = ModLoaders.None
  294. For Each Category In If(Data("loaders")?.Select(Function(t) t.ToString), {}).Concat(Data("categories").Select(Function(t) t.ToString))
  295. For Each Loader In EnumUtils.GetAllFlags(Of ModLoaders)()
  296. If Category = Loader.ToString.Lower Then
  297. ModLoaders = ModLoaders Or Loader
  298. Exit For
  299. End If
  300. Next
  301. Next
  302. For Each Category In Data("categories").Select(Function(t) t.ToString)
  303. Select Case Category
  304. Case "datapack" : Types = ResourceTypes.DataPack
  305. '共用
  306. Case "technology" : Tags.Add("科技")
  307. Case "magic" : Tags.Add("魔法")
  308. Case "adventure" : Tags.Add("冒险")
  309. Case "utility" : Tags.Add("实用")
  310. Case "optimization" : Tags.Add("性能优化")
  311. Case "vanilla-like" : Tags.Add("原版风")
  312. Case "realistic" : Tags.Add("写实风")
  313. 'Mod/数据包
  314. Case "worldgen" : Tags.Add("世界元素")
  315. Case "food" : Tags.Add("食物/烹饪")
  316. Case "game-mechanics" : Tags.Add("游戏机制")
  317. Case "transportation" : Tags.Add("运输")
  318. Case "storage" : Tags.Add("仓储")
  319. Case "decoration" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装饰")
  320. Case "mobs" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("生物")
  321. Case "equipment" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装备")
  322. Case "social" : Tags.Add("服务器")
  323. Case "library" : Tags.Add("支持库")
  324. '整合包
  325. Case "multiplayer" : Tags.Add("多人")
  326. Case "challenging" : Tags.Add("硬核")
  327. Case "combat" : Tags.Add("战斗")
  328. Case "quests" : Tags.Add("任务")
  329. Case "kitchen-sink" : Tags.Add("水槽包")
  330. Case "lightweight" : Tags.Add("轻量")
  331. '资源包
  332. Case "simplistic" : Tags.Add("简洁")
  333. Case "combat" : Tags.Add("战斗")
  334. Case "tweaks" : Tags.Add("改良")
  335. Case "8x-" : Tags.Add("极简")
  336. Case "16x" : Tags.Add("16x")
  337. Case "32x" : Tags.Add("32x")
  338. Case "48x" : Tags.Add("48x")
  339. Case "64x" : Tags.Add("64x")
  340. Case "128x" : Tags.Add("128x")
  341. Case "256x" : Tags.Add("256x")
  342. Case "512x+" : Tags.Add("超高清")
  343. Case "audio" : Tags.Add("含声音")
  344. Case "fonts" : Tags.Add("含字体")
  345. Case "models" : Tags.Add("含模型")
  346. Case "gui" : Tags.Add("含 UI")
  347. Case "locale" : Tags.Add("含语言")
  348. Case "core-shaders" : Tags.Add("核心着色器")
  349. Case "modded" : Tags.Add("兼容 Mod")
  350. '光影包
  351. Case "fantasy" : Tags.Add("幻想风")
  352. Case "semi-realistic" : Tags.Add("半写实风")
  353. Case "cartoon" : Tags.Add("卡通风")
  354. '暂时不添加性能负荷 Tag
  355. 'Case "potato" : Tags.Add("极低")
  356. 'Case "low" : Tags.Add("低")
  357. 'Case "medium" : Tags.Add("中")
  358. 'Case "high" : Tags.Add("高")
  359. Case "colored-lighting" : Tags.Add("彩色光照")
  360. Case "path-tracing" : Tags.Add("路径追踪")
  361. Case "pbr" : Tags.Add("PBR")
  362. Case "reflections" : Tags.Add("反射")
  363. Case "iris" : Tags.Add("Iris")
  364. Case "optifine" : Tags.Add("OptiFine")
  365. Case "vanilla" : Tags.Add("原版可用")
  366. End Select
  367. Next
  368. If Types = ResourceTypes.DataPack AndAlso ModLoaders <> ModLoaders.None Then Types = ResourceTypes.DataPack Or ResourceTypes.Mod
  369. #End Region
  370. End If
  371. If Not Tags.Any() Then Tags.Add("其他")
  372. Tags.Sort()
  373. End If
  374. '保存缓存
  375. Cache(Id) = Me
  376. End Sub
  377. ''' <summary>
  378. ''' 从工程 ID 联网获取对应工程的信息。
  379. ''' </summary>
  380. Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms) As ResourceProject
  381. If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
  382. If Cache.ContainsKey(ProjectId) Then '存在缓存
  383. Return Cache(ProjectId)
  384. ElseIf Platform = ResourcePlatforms.CurseForge Then 'CurseForge
  385. Return New ResourceProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data"))
  386. Else 'Modrinth
  387. Return New ResourceProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId))
  388. End If
  389. End Function
  390. ''' <summary>
  391. ''' 从工程 ID 列表联网获取工程信息,结果不按原顺序排列。
  392. ''' </summary>
  393. Public Shared Iterator Function FromProjectIds(ProjectIds As IEnumerable(Of String), Platform As ResourcePlatforms) As IEnumerable(Of ResourceProject)
  394. If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
  395. '获取缓存
  396. Dim MissingProjectIds As New List(Of String)
  397. For Each ProjectId In ProjectIds
  398. If Cache.ContainsKey(ProjectId) Then
  399. Yield Cache(ProjectId)
  400. Else
  401. MissingProjectIds.Add(ProjectId)
  402. End If
  403. Next
  404. If Not MissingProjectIds.Any() Then Return
  405. '联网获取剩余的工程对象
  406. Dim Projects As JArray
  407. If Platform.HasFlag(ResourcePlatforms.CurseForge) Then
  408. Projects = DlModRequest("https://api.curseforge.com/v1/mods",
  409. HttpMethod.Post, "{""modIds"": [" & MissingProjectIds.Join(","c) & "]}", "application/json")("data")
  410. Else
  411. Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{MissingProjectIds.Join(""",""")}""]")
  412. End If
  413. For Each Project In Projects
  414. Yield New ResourceProject(Project)
  415. Next
  416. End Function
  417. ''' <summary>
  418. ''' 将当前实例转为可用于保存缓存的 Json。
  419. ''' </summary>
  420. Public Function ToJson() As JObject
  421. Dim Json As New JObject
  422. Json("Platform") = CInt(Platform)
  423. Json("Types") = CInt(Types)
  424. Json("Slug") = Slug
  425. Json("Id") = Id
  426. If CurseForgeFileIds IsNot Nothing Then Json("CurseForgeFileIds") = New JArray(CurseForgeFileIds)
  427. Json("RawName") = RawName
  428. Json("Description") = Description
  429. Json("Website") = Website
  430. Json("LastUpdate") = LastUpdate
  431. Json("DownloadCount") = DownloadCount
  432. If ModLoaders <> ModLoaders.None Then Json("ModLoaders") = ModLoaders
  433. Json("Tags") = New JArray(Tags)
  434. If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
  435. If Drops.Any Then Json("Drops") = New JArray(Drops)
  436. Json("CacheTime") = Date.Now '用于检查缓存时间
  437. Return Json
  438. End Function
  439. ''' <summary>
  440. ''' 将当前工程信息实例化为控件。
  441. ''' </summary>
  442. Public Function ToResourceItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean) As MyVirtualizingElement(Of MyResourceItem)
  443. '获取版本描述
  444. Dim GameVersionDescription As String
  445. If Drops Is Nothing OrElse Not Drops.Any() Then
  446. GameVersionDescription = "仅快照版本" '#5412
  447. Else
  448. Dim Segments As New List(Of String)
  449. Dim IsOld As Boolean = False
  450. For i = 0 To Drops.Count - 1 '版本号一定为降序
  451. '获取当前连续的版本号段
  452. Dim StartDrop As Integer = Drops(i), EndDrop As Integer = Drops(i)
  453. If StartDrop < 120 Then '如果支持新版本,则不显示 1.11-
  454. If Segments.Any() AndAlso Not IsOld Then
  455. Exit For
  456. Else
  457. IsOld = True
  458. End If
  459. End If
  460. For ii = i + 1 To Drops.Count - 1
  461. If AllDrops Is Nothing OrElse AllDrops.IndexOf(Drops(ii)) <> AllDrops.IndexOf(EndDrop) + 1 Then Exit For
  462. EndDrop = Drops(ii)
  463. i = ii
  464. Next
  465. '将版本号段转为描述文本
  466. Dim StartName = McVersion.DropToVersion(StartDrop)
  467. Dim EndName = McVersion.DropToVersion(EndDrop)
  468. If StartDrop = EndDrop Then
  469. Segments.Add(StartName)
  470. ElseIf AllDrops IsNot Nothing AndAlso StartDrop >= AllDrops.First Then
  471. If EndDrop < 120 Then
  472. Segments.Clear()
  473. Segments.Add("全版本")
  474. Exit For
  475. Else
  476. Segments.Add(EndName & "+")
  477. End If
  478. ElseIf EndDrop < 120 Then
  479. Segments.Add(StartName & "-")
  480. Exit For
  481. ElseIf AllDrops Is Nothing OrElse AllDrops.IndexOf(EndDrop) - AllDrops.IndexOf(StartDrop) = 1 Then
  482. Segments.Add(StartName & ", " & EndName)
  483. Else
  484. Segments.Add(StartName & "~" & EndName)
  485. End If
  486. Next
  487. GameVersionDescription = Segments.Join(", ")
  488. End If
  489. '获取 Mod 加载器描述
  490. Dim ModLoaderDescriptionFull As String, ModLoaderDescriptionPart As String
  491. Dim ModLoadersForDesc = ModLoaders.Flags().ToList()
  492. If Settings.Get(Of Boolean)("ToolDownloadIgnoreQuilt") Then ModLoadersForDesc.Remove(ModLoaders.Quilt)
  493. Select Case ModLoadersForDesc.Count
  494. Case 0
  495. If ModLoadersForDesc.IsSingle Then
  496. ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
  497. ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
  498. Else
  499. ModLoaderDescriptionFull = "未知"
  500. ModLoaderDescriptionPart = ""
  501. End If
  502. Case 1
  503. ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
  504. ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
  505. Case Else
  506. Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
  507. If ModLoadersForDesc.Contains(ModLoaders.Forge) AndAlso
  508. (NewestDrop < 140 OrElse ModLoadersForDesc.Contains(ModLoaders.Fabric)) AndAlso
  509. (NewestDrop < 200 OrElse ModLoadersForDesc.Contains(ModLoaders.NeoForge)) AndAlso
  510. (NewestDrop < 140 OrElse ModLoadersForDesc.Contains(ModLoaders.Quilt) OrElse Settings.Get(Of Boolean)("ToolDownloadIgnoreQuilt")) Then
  511. ModLoaderDescriptionFull = "任意"
  512. ModLoaderDescriptionPart = ""
  513. Else
  514. ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
  515. ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
  516. End If
  517. End Select
  518. '实例化 UI
  519. Return New MyVirtualizingElement(Of MyResourceItem)(
  520. Function()
  521. Dim NewItem As New MyResourceItem With {.Tag = Me}
  522. ApplyLogoToMyImage(NewItem.PathLogo)
  523. Dim Titles = GetControlTitle(True)
  524. NewItem.Title = Titles.Title
  525. If Titles.SubTitle = "" Then
  526. CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
  527. Else
  528. NewItem.SubTitle = Titles.SubTitle
  529. End If
  530. NewItem.Tags = Tags
  531. NewItem.Description = Description.ReplaceLineEndings("")
  532. '下边栏
  533. If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
  534. '全部隐藏
  535. CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
  536. CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
  537. NewItem.ColumnVersion1.Width = New GridLength(0)
  538. NewItem.ColumnVersion2.MaxWidth = 0
  539. NewItem.ColumnVersion3.Width = New GridLength(0)
  540. ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
  541. '全部显示
  542. NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
  543. ElseIf ShowMcVersionDesc Then
  544. '仅显示版本
  545. NewItem.LabVersion.Text = GameVersionDescription
  546. Else
  547. '仅显示 Mod 加载器
  548. NewItem.LabVersion.Text = ModLoaderDescriptionFull
  549. End If
  550. NewItem.LabSource.Text = Platform.ToString
  551. NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
  552. NewItem.LabDownload.Text =
  553. If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
  554. If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
  555. Return NewItem
  556. End Function) With {.Height = 64}
  557. End Function
  558. Public Sub ApplyLogoToMyImage(Img As MyImage)
  559. If String.IsNullOrEmpty(LogoUrl) Then
  560. Img.Source = PathImage & "Icons/NoIcon.png"
  561. Else
  562. Img.Source = LogoUrl
  563. Img.FallbackSource = DlSourceModGet(LogoUrl)
  564. End If
  565. End Sub
  566. Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
  567. '检查下列代码时可以参考 #1567 的测试例
  568. Dim Title As String = RawName
  569. Dim SubtitleList As List(Of String)
  570. If TranslatedName = RawName Then
  571. '没有中文翻译
  572. '将所有名称分段
  573. Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, True).
  574. Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
  575. If NameLists.IsSingle Then GoTo NoSubtitle
  576. '查找其中的缩写、Forge/Fabric 等版本标记
  577. SubtitleList = New List(Of String)
  578. Dim NormalNameList = New List(Of String)
  579. For Each Name In NameLists
  580. Dim LowerName As String = Name.Lower
  581. If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
  582. '缩写
  583. SubtitleList.Add(Name)
  584. ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
  585. Not LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").RegexCheck("[a-z]+") Then '去掉关键词后没有其他字母
  586. 'Forge/Fabric 等版本标记
  587. SubtitleList.Add(Name)
  588. Else
  589. '其他部分
  590. NormalNameList.Add(Name)
  591. End If
  592. Next
  593. '根据分类后的结果处理
  594. If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
  595. '同时包含 NormalName 和 Subtitle
  596. Title = NormalNameList.Join(" - ")
  597. Else
  598. '有中文翻译
  599. '尝试将文本分为三段:Title (EnglishName) - Suffix
  600. '检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
  601. Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
  602. Dim Suffix As String = ""
  603. If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
  604. Dim EnglishName As String = TranslatedName
  605. If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
  606. EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
  607. '中段的额外信息截取
  608. SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, True).
  609. Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
  610. If SubtitleList.Count > 1 AndAlso
  611. Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
  612. Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
  613. SubtitleList = New List(Of String) From {EnglishName} '使用原名
  614. End If
  615. '添加后缀
  616. If Suffix <> "" Then SubtitleList.Add(Suffix)
  617. End If
  618. SubtitleList = SubtitleList.Distinct.ToList()
  619. '设置标题与描述
  620. Dim Subtitle As String = ""
  621. If SubtitleList.Any Then
  622. For Each Ex In SubtitleList
  623. Dim IsModLoaderDescription As Boolean =
  624. Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
  625. '是否显示 ModLoader 信息
  626. If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
  627. '去除 “Forge/Fabric” 这一无意义提示
  628. If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
  629. '将 “Forge” 等提示改为 “Forge 版”
  630. If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
  631. Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
  632. Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
  633. End If
  634. '将 “forge” 等词语的首字母大写
  635. Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
  636. Subtitle &= " | " & Ex.Trim
  637. Next
  638. Else
  639. NoSubtitle:
  640. Subtitle = ""
  641. End If
  642. Return (Title, Subtitle)
  643. End Function
  644. ''' <summary>
  645. ''' 工程 ID 对应的工程实例的缓存。不会保存到文件。
  646. ''' </summary>
  647. Protected Friend Shared Cache As New Dictionary(Of String, ResourceProject)
  648. ''' <summary>
  649. ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
  650. ''' </summary>
  651. Public Function IsLike(Project As ResourceProject) As Boolean
  652. If Id = Project.Id Then Return True '相同实例
  653. '提取字符串中的字母和数字
  654. Dim GetRaw =
  655. Function(Data As String) As String
  656. Dim Result As New StringBuilder()
  657. For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
  658. Result.Append(r)
  659. Next
  660. Return Result.ToString.Lower
  661. End Function
  662. '来自不同的网站
  663. If Platform = Project.Platform Then Return False
  664. 'Mod 加载器一致
  665. If ModLoaders <> Project.ModLoaders Then Return False
  666. '若不为光影,则要求 MC 版本一致
  667. If Types <> ResourceTypes.Shader AndAlso Drops.Ordered.SequenceEqual(Project.Drops.Ordered) Then Return False
  668. '最近更新时间差距在一周以内
  669. If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
  670. 'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
  671. If TranslatedName = Project.TranslatedName OrElse
  672. RawName = Project.RawName OrElse Description = Project.Description OrElse
  673. GetRaw(Slug) = GetRaw(Project.Slug) Then
  674. Logger.Info($"将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
  675. '如果只有一个有 DatabaseEntry,设置给另外一个
  676. If WikiEntry Is Nothing AndAlso Project.WikiEntry IsNot Nothing Then WikiEntry = Project.WikiEntry
  677. If WikiEntry IsNot Nothing AndAlso Project.WikiEntry Is Nothing Then Project.WikiEntry = WikiEntry
  678. Return True
  679. End If
  680. Return False
  681. End Function
  682. '辅助函数
  683. Public Overrides Function ToString() As String
  684. Return $"{Id} ({Slug}): {RawName}"
  685. End Function
  686. Public Overrides Function Equals(obj As Object) As Boolean
  687. Dim project = TryCast(obj, ResourceProject)
  688. Return project IsNot Nothing AndAlso Id = project.Id
  689. End Function
  690. Public Shared Operator =(left As ResourceProject, right As ResourceProject) As Boolean
  691. Return EqualityComparer(Of ResourceProject).Default.Equals(left, right)
  692. End Operator
  693. Public Shared Operator <>(left As ResourceProject, right As ResourceProject) As Boolean
  694. Return Not left = right
  695. End Operator
  696. End Class