ResourceProject.vb 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  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.Value.FirstOrDefault(Function(c) c.Slugs.GetOrDefault(Platform) = 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?.ChineseName, RawName)
  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") = CInt(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. Select Case ModLoadersForDesc.Count
  493. Case 0
  494. If ModLoadersForDesc.IsSingle Then
  495. ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
  496. ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
  497. Else
  498. ModLoaderDescriptionFull = "未知"
  499. ModLoaderDescriptionPart = ""
  500. End If
  501. Case 1
  502. ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
  503. ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
  504. Case Else
  505. Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
  506. If ModLoadersForDesc.Contains(ModLoaders.Forge) AndAlso
  507. (NewestDrop < 140 OrElse ModLoadersForDesc.Contains(ModLoaders.Fabric)) AndAlso
  508. (NewestDrop < 200 OrElse ModLoadersForDesc.Contains(ModLoaders.NeoForge)) Then
  509. ModLoaderDescriptionFull = "任意"
  510. ModLoaderDescriptionPart = ""
  511. Else
  512. ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
  513. ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
  514. End If
  515. End Select
  516. '实例化 UI
  517. Return New MyVirtualizingElement(Of MyResourceItem)(
  518. Function()
  519. Dim NewItem As New MyResourceItem With {.Tag = Me}
  520. ApplyLogoToMyImage(NewItem.PathLogo)
  521. Dim Titles = GetControlTitle(True)
  522. NewItem.Title = Titles.Title
  523. If Titles.SubTitle = "" Then
  524. CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
  525. Else
  526. NewItem.SubTitle = Titles.SubTitle
  527. End If
  528. NewItem.Tags = Tags
  529. NewItem.Description = Description.ReplaceLineEndings("")
  530. '下边栏
  531. If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
  532. '全部隐藏
  533. CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
  534. CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
  535. NewItem.ColumnVersion1.Width = New GridLength(0)
  536. NewItem.ColumnVersion2.MaxWidth = 0
  537. NewItem.ColumnVersion3.Width = New GridLength(0)
  538. ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
  539. '全部显示
  540. NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
  541. ElseIf ShowMcVersionDesc Then
  542. '仅显示版本
  543. NewItem.LabVersion.Text = GameVersionDescription
  544. Else
  545. '仅显示 Mod 加载器
  546. NewItem.LabVersion.Text = ModLoaderDescriptionFull
  547. End If
  548. NewItem.LabSource.Text = Platform.ToString
  549. NewItem.LabTime.Text = StringUtils.FormatTimeSpan(LastUpdate - Date.Now, True)
  550. NewItem.LabDownload.Text =
  551. If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
  552. If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
  553. Return NewItem
  554. End Function) With {.Height = 64}
  555. End Function
  556. Public Sub ApplyLogoToMyImage(Img As MyImage)
  557. If String.IsNullOrEmpty(LogoUrl) Then
  558. Img.Source = PathImage & "Icons/NoIcon.png"
  559. Else
  560. Img.Source = LogoUrl
  561. Img.FallbackSource = DlSourceModGet(LogoUrl)
  562. End If
  563. End Sub
  564. Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
  565. '检查下列代码时可以参考 #1567 的测试例
  566. Dim Title As String = RawName
  567. Dim SubtitleList As List(Of String)
  568. If TranslatedName = RawName Then
  569. '没有中文翻译
  570. '将所有名称分段
  571. Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, True).
  572. Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
  573. If NameLists.IsSingle Then GoTo NoSubtitle
  574. '查找其中的缩写、Forge/Fabric 等版本标记
  575. SubtitleList = New List(Of String)
  576. Dim NormalNameList = New List(Of String)
  577. For Each Name In NameLists
  578. Dim LowerName As String = Name.Lower
  579. If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
  580. '缩写
  581. SubtitleList.Add(Name)
  582. ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
  583. Not LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").RegexCheck("[a-z]+") Then '去掉关键词后没有其他字母
  584. 'Forge/Fabric 等版本标记
  585. SubtitleList.Add(Name)
  586. Else
  587. '其他部分
  588. NormalNameList.Add(Name)
  589. End If
  590. Next
  591. '根据分类后的结果处理
  592. If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
  593. '同时包含 NormalName 和 Subtitle
  594. Title = NormalNameList.Join(" - ")
  595. Else
  596. '有中文翻译
  597. '尝试将文本分为三段:Title (EnglishName) - Suffix
  598. '检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
  599. Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
  600. Dim Suffix As String = ""
  601. If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
  602. Dim EnglishName As String = TranslatedName
  603. If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
  604. EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
  605. '中段的额外信息截取
  606. SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, True).
  607. Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
  608. If SubtitleList.Count > 1 AndAlso
  609. Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
  610. Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
  611. SubtitleList = New List(Of String) From {EnglishName} '使用原名
  612. End If
  613. '添加后缀
  614. If Suffix <> "" Then SubtitleList.Add(Suffix)
  615. End If
  616. SubtitleList = SubtitleList.Distinct.ToList()
  617. '设置标题与描述
  618. Dim Subtitle As String = ""
  619. If SubtitleList.Any Then
  620. For Each Ex In SubtitleList
  621. Dim IsModLoaderDescription As Boolean =
  622. Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
  623. '是否显示 ModLoader 信息
  624. If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
  625. '去除 “Forge/Fabric” 这一无意义提示
  626. If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
  627. '将 “Forge” 等提示改为 “Forge 版”
  628. If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
  629. Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
  630. Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
  631. End If
  632. '将 “forge” 等词语的首字母大写
  633. Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
  634. Subtitle &= " | " & Ex.Trim
  635. Next
  636. Else
  637. NoSubtitle:
  638. Subtitle = ""
  639. End If
  640. Return (Title, Subtitle)
  641. End Function
  642. ''' <summary>
  643. ''' 工程 ID 对应的工程实例的缓存。不会保存到文件。
  644. ''' </summary>
  645. Protected Friend Shared Cache As New Dictionary(Of String, ResourceProject)
  646. ''' <summary>
  647. ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
  648. ''' </summary>
  649. Public Function IsLike(Project As ResourceProject) As Boolean
  650. If Id = Project.Id Then Return True '相同实例
  651. '提取字符串中的字母和数字
  652. Dim GetRaw =
  653. Function(Data As String) As String
  654. Dim Result As New StringBuilder()
  655. For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
  656. Result.Append(r)
  657. Next
  658. Return Result.ToString.Lower
  659. End Function
  660. '来自不同的网站
  661. If Platform = Project.Platform Then Return False
  662. 'Mod 加载器一致
  663. If ModLoaders <> Project.ModLoaders Then Return False
  664. '若不为光影,则要求 MC 版本一致
  665. If Types <> ResourceTypes.Shader AndAlso Drops.Ordered.SequenceEqual(Project.Drops.Ordered) Then Return False
  666. '最近更新时间差距在一周以内
  667. If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
  668. 'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
  669. If TranslatedName = Project.TranslatedName OrElse
  670. RawName = Project.RawName OrElse Description = Project.Description OrElse
  671. GetRaw(Slug) = GetRaw(Project.Slug) Then
  672. Logger.Info($"将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
  673. '如果只有一个有 DatabaseEntry,设置给另外一个
  674. If WikiEntry Is Nothing AndAlso Project.WikiEntry IsNot Nothing Then WikiEntry = Project.WikiEntry
  675. If WikiEntry IsNot Nothing AndAlso Project.WikiEntry Is Nothing Then Project.WikiEntry = WikiEntry
  676. Return True
  677. End If
  678. Return False
  679. End Function
  680. '辅助函数
  681. Public Overrides Function ToString() As String
  682. Return $"{Id} ({Slug}): {RawName}"
  683. End Function
  684. Public Overrides Function Equals(obj As Object) As Boolean
  685. Dim project = TryCast(obj, ResourceProject)
  686. Return project IsNot Nothing AndAlso Id = project.Id
  687. End Function
  688. Public Shared Operator =(left As ResourceProject, right As ResourceProject) As Boolean
  689. Return EqualityComparer(Of ResourceProject).Default.Equals(left, right)
  690. End Operator
  691. Public Shared Operator <>(left As ResourceProject, right As ResourceProject) As Boolean
  692. Return Not left = right
  693. End Operator
  694. End Class