ResourceProject.vb 36 KB

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