SongBrowserModel.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. using SongBrowser.DataAccess;
  2. using SongBrowser.Internals;
  3. using SongBrowser.UI;
  4. using SongCore.OverrideClasses;
  5. using SongCore.Utilities;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Text.RegularExpressions;
  12. using UnityEngine;
  13. using static StandardLevelInfoSaveData;
  14. using Logger = SongBrowser.Logging.Logger;
  15. namespace SongBrowser
  16. {
  17. public class SongBrowserModel
  18. {
  19. private readonly String CUSTOM_SONGS_DIR = Path.Combine("Beat Saber_Data", "CustomLevels");
  20. private readonly DateTime EPOCH = new DateTime(1970, 1, 1);
  21. // song_browser_settings.xml
  22. private SongBrowserSettings _settings;
  23. // song list management
  24. private double _customSongDirLastWriteTime = 0;
  25. private Dictionary<String, CustomPreviewBeatmapLevel> _levelIdToCustomLevel;
  26. private Dictionary<String, double> _cachedLastWriteTimes;
  27. private Dictionary<string, int> _weights;
  28. private Dictionary<BeatmapDifficulty, int> _difficultyWeights;
  29. private Dictionary<string, ScrappedSong> _levelHashToDownloaderData = null;
  30. private Dictionary<string, int> _levelIdToPlayCount;
  31. public BeatmapCharacteristicSO CurrentBeatmapCharacteristicSO;
  32. public static Action<Dictionary<string, CustomPreviewBeatmapLevel>> didFinishProcessingSongs;
  33. /// <summary>
  34. /// Get the settings the model is using.
  35. /// </summary>
  36. public SongBrowserSettings Settings
  37. {
  38. get
  39. {
  40. return _settings;
  41. }
  42. }
  43. /// <summary>
  44. /// Get the last selected (stored in settings) level id.
  45. /// </summary>
  46. public String LastSelectedLevelId
  47. {
  48. get
  49. {
  50. return _settings.currentLevelId;
  51. }
  52. set
  53. {
  54. _settings.currentLevelId = value;
  55. _settings.Save();
  56. }
  57. }
  58. private Playlist _currentPlaylist;
  59. /// <summary>
  60. /// Manage the current playlist if one exists.
  61. /// </summary>
  62. public Playlist CurrentPlaylist
  63. {
  64. get
  65. {
  66. if (_currentPlaylist == null)
  67. {
  68. _currentPlaylist = Playlist.LoadPlaylist(this._settings.currentPlaylistFile);
  69. }
  70. return _currentPlaylist;
  71. }
  72. set
  73. {
  74. _settings.currentPlaylistFile = value.fileLoc;
  75. _currentPlaylist = value;
  76. }
  77. }
  78. /// <summary>
  79. /// Current editing playlist
  80. /// </summary>
  81. public Playlist CurrentEditingPlaylist;
  82. /// <summary>
  83. /// HashSet of LevelIds for quick lookup
  84. /// </summary>
  85. public HashSet<String> CurrentEditingPlaylistLevelIds;
  86. /// <summary>
  87. /// Constructor.
  88. /// </summary>
  89. public SongBrowserModel()
  90. {
  91. _levelIdToCustomLevel = new Dictionary<string, CustomPreviewBeatmapLevel>();
  92. _cachedLastWriteTimes = new Dictionary<String, double>();
  93. _levelIdToPlayCount = new Dictionary<string, int>();
  94. CurrentEditingPlaylistLevelIds = new HashSet<string>();
  95. // Weights used for keeping the original songs in order
  96. // Invert the weights from the game so we can order by descending and make LINQ work with us...
  97. /* Level4, Level2, Level9, Level5, Level10, Level6, Level7, Level1, Level3, Level8, Level11 */
  98. _weights = new Dictionary<string, int>
  99. {
  100. ["OneHopeLevel"] = 12,
  101. ["100Bills"] = 11,
  102. ["Escape"] = 10,
  103. ["Legend"] = 9,
  104. ["BeatSaber"] = 8,
  105. ["AngelVoices"] = 7,
  106. ["CountryRounds"] = 6,
  107. ["BalearicPumping"] = 5,
  108. ["Breezer"] = 4,
  109. ["CommercialPumping"] = 3,
  110. ["TurnMeOn"] = 2,
  111. ["LvlInsane"] = 1,
  112. ["100BillsOneSaber"] = 12,
  113. ["EscapeOneSaber"] = 11,
  114. ["LegendOneSaber"] = 10,
  115. ["BeatSaberOneSaber"] = 9,
  116. ["CommercialPumpingOneSaber"] = 8,
  117. ["TurnMeOnOneSaber"] = 8,
  118. };
  119. _difficultyWeights = new Dictionary<BeatmapDifficulty, int>
  120. {
  121. [BeatmapDifficulty.Easy] = 1,
  122. [BeatmapDifficulty.Normal] = 2,
  123. [BeatmapDifficulty.Hard] = 4,
  124. [BeatmapDifficulty.Expert] = 8,
  125. [BeatmapDifficulty.ExpertPlus] = 16,
  126. };
  127. }
  128. /// <summary>
  129. /// Init this model.
  130. /// </summary>
  131. /// <param name="songSelectionMasterView"></param>
  132. /// <param name="songListViewController"></param>
  133. public void Init()
  134. {
  135. _settings = SongBrowserSettings.Load();
  136. Logger.Info("Settings loaded, sorting mode is: {0}", _settings.sortMode);
  137. }
  138. /// <summary>
  139. /// Easy invert of toggling.
  140. /// </summary>
  141. public void ToggleInverting()
  142. {
  143. this.Settings.invertSortResults = !this.Settings.invertSortResults;
  144. }
  145. /// <summary>
  146. /// Get the song cache from the game.
  147. /// </summary>
  148. public void UpdateLevelRecords()
  149. {
  150. Stopwatch timer = new Stopwatch();
  151. timer.Start();
  152. // Calculate some information about the custom song dir
  153. String customSongsPath = Path.Combine(Environment.CurrentDirectory, CUSTOM_SONGS_DIR);
  154. String revSlashCustomSongPath = customSongsPath.Replace('\\', '/');
  155. double currentCustomSongDirLastWriteTIme = (File.GetLastWriteTimeUtc(customSongsPath) - EPOCH).TotalMilliseconds;
  156. bool customSongDirChanged = false;
  157. if (_customSongDirLastWriteTime != currentCustomSongDirLastWriteTIme)
  158. {
  159. customSongDirChanged = true;
  160. _customSongDirLastWriteTime = currentCustomSongDirLastWriteTIme;
  161. }
  162. if (!Directory.Exists(customSongsPath))
  163. {
  164. Logger.Error("CustomSong directory is missing...");
  165. return;
  166. }
  167. // Map some data for custom songs
  168. Regex r = new Regex(@"(\d+-\d+)", RegexOptions.IgnoreCase);
  169. Stopwatch lastWriteTimer = new Stopwatch();
  170. lastWriteTimer.Start();
  171. foreach (KeyValuePair<string, CustomPreviewBeatmapLevel> level in SongCore.Loader.CustomLevels)
  172. {
  173. // If we already know this levelID, don't both updating it.
  174. // SongLoader should filter duplicates but in case of failure we don't want to crash
  175. if (!_cachedLastWriteTimes.ContainsKey(level.Value.levelID) || customSongDirChanged)
  176. {
  177. // Always use the newest date.
  178. var lastWriteTime = File.GetLastWriteTimeUtc(level.Value.customLevelPath);
  179. var lastCreateTime = File.GetCreationTimeUtc(level.Value.customLevelPath);
  180. var lastTime = lastWriteTime > lastCreateTime ? lastWriteTime : lastCreateTime;
  181. _cachedLastWriteTimes[level.Value.levelID] = (lastTime - EPOCH).TotalMilliseconds;
  182. }
  183. if (!_levelIdToCustomLevel.ContainsKey(level.Value.levelID))
  184. {
  185. _levelIdToCustomLevel.Add(level.Value.levelID, level.Value);
  186. }
  187. }
  188. lastWriteTimer.Stop();
  189. Logger.Info("Determining song download time and determining mappings took {0}ms", lastWriteTimer.ElapsedMilliseconds);
  190. // Update song Infos, directory tree, and sort
  191. this.UpdatePlayCounts();
  192. // Check if we need to upgrade settings file favorites
  193. try
  194. {
  195. this.Settings.ConvertFavoritesToPlaylist(_levelIdToCustomLevel);
  196. }
  197. catch (Exception e)
  198. {
  199. Logger.Exception("FAILED TO CONVERT FAVORITES TO PLAYLIST!", e);
  200. }
  201. // load the current editing playlist or make one
  202. if (CurrentEditingPlaylist == null && !String.IsNullOrEmpty(this.Settings.currentEditingPlaylistFile))
  203. {
  204. Logger.Debug("Loading playlist for editing: {0}", this.Settings.currentEditingPlaylistFile);
  205. CurrentEditingPlaylist = Playlist.LoadPlaylist(this.Settings.currentEditingPlaylistFile);
  206. PlaylistsCollection.MatchSongsForPlaylist(CurrentEditingPlaylist, true);
  207. }
  208. if (CurrentEditingPlaylist == null)
  209. {
  210. Logger.Debug("Current editing playlist does not exit, create...");
  211. CurrentEditingPlaylist = new Playlist
  212. {
  213. playlistTitle = "Song Browser Favorites",
  214. playlistAuthor = "SongBrowser",
  215. fileLoc = this.Settings.currentEditingPlaylistFile,
  216. image = Base64Sprites.SpriteToBase64(Base64Sprites.BeastSaberLogo),
  217. songs = new List<PlaylistSong>(),
  218. };
  219. }
  220. CurrentEditingPlaylistLevelIds = new HashSet<string>();
  221. foreach (PlaylistSong ps in CurrentEditingPlaylist.songs)
  222. {
  223. // Sometimes we cannot match a song
  224. string levelId = null;
  225. if (ps.level != null)
  226. {
  227. levelId = ps.level.levelID;
  228. }
  229. else if (!String.IsNullOrEmpty(ps.levelId))
  230. {
  231. levelId = ps.levelId;
  232. }
  233. else
  234. {
  235. //Logger.Debug("MISSING SONG {0}", ps.songName);
  236. continue;
  237. }
  238. CurrentEditingPlaylistLevelIds.Add(levelId);
  239. }
  240. // Signal complete
  241. if (SongCore.Loader.CustomLevels.Count > 0)
  242. {
  243. didFinishProcessingSongs?.Invoke(SongCore.Loader.CustomLevels);
  244. }
  245. timer.Stop();
  246. Logger.Info("Updating songs infos took {0}ms", timer.ElapsedMilliseconds);
  247. }
  248. /// <summary>
  249. /// SongLoader doesn't fire event when we delete a song.
  250. /// </summary>
  251. /// <param name="levelPack"></param>
  252. /// <param name="levelId"></param>
  253. public void RemoveSongFromLevelPack(IBeatmapLevelPack levelPack, String levelId)
  254. {
  255. levelPack.beatmapLevelCollection.beatmapLevels.ToList().RemoveAll(x => x.levelID == levelId);
  256. }
  257. /// <summary>
  258. /// Update the gameplay play counts.
  259. /// </summary>
  260. /// <param name="gameplayMode"></param>
  261. private void UpdatePlayCounts()
  262. {
  263. // Reset current playcounts
  264. _levelIdToPlayCount = new Dictionary<string, int>();
  265. // Build a map of levelId to sum of all playcounts and sort.
  266. PlayerDataModelSO playerData = Resources.FindObjectsOfTypeAll<PlayerDataModelSO>().FirstOrDefault();
  267. foreach (var levelData in playerData.currentLocalPlayer.levelsStatsData)
  268. {
  269. if (!_levelIdToPlayCount.ContainsKey(levelData.levelID))
  270. {
  271. _levelIdToPlayCount.Add(levelData.levelID, 0);
  272. }
  273. _levelIdToPlayCount[levelData.levelID] += levelData.playCount;
  274. }
  275. }
  276. /// <summary>
  277. /// Map the downloader data for quick lookup.
  278. /// </summary>
  279. /// <param name="songs"></param>
  280. public void UpdateDownloaderDataMapping(List<ScrappedSong> songs)
  281. {
  282. _levelHashToDownloaderData = new Dictionary<string, ScrappedSong>();
  283. foreach (ScrappedSong song in songs)
  284. {
  285. if (_levelHashToDownloaderData.ContainsKey(song.Hash))
  286. {
  287. continue;
  288. }
  289. _levelHashToDownloaderData.Add(song.Hash, song);
  290. }
  291. }
  292. /// <summary>
  293. /// Add Song to Editing Playlist
  294. /// </summary>
  295. /// <param name="songInfo"></param>
  296. public void AddSongToEditingPlaylist(IBeatmapLevel songInfo)
  297. {
  298. if (this.CurrentEditingPlaylist == null)
  299. {
  300. return;
  301. }
  302. this.CurrentEditingPlaylist.songs.Add(new PlaylistSong()
  303. {
  304. songName = songInfo.songName,
  305. levelId = songInfo.levelID,
  306. hash = CustomHelpers.GetSongHash(songInfo.levelID),
  307. });
  308. this.CurrentEditingPlaylistLevelIds.Add(songInfo.levelID);
  309. this.CurrentEditingPlaylist.SavePlaylist();
  310. }
  311. /// <summary>
  312. /// Remove Song from editing playlist
  313. /// </summary>
  314. /// <param name="levelId"></param>
  315. public void RemoveSongFromEditingPlaylist(IBeatmapLevel songInfo)
  316. {
  317. if (this.CurrentEditingPlaylist == null)
  318. {
  319. return;
  320. }
  321. this.CurrentEditingPlaylist.songs.RemoveAll(x => x.level != null && x.level.levelID == songInfo.levelID);
  322. this.CurrentEditingPlaylistLevelIds.RemoveWhere(x => x == songInfo.levelID);
  323. this.CurrentEditingPlaylist.SavePlaylist();
  324. }
  325. /// <summary>
  326. /// Overwrite the current level pack.
  327. /// </summary>
  328. private void OverwriteCurrentLevelPack(IBeatmapLevelPack pack, List<IPreviewBeatmapLevel> sortedLevels)
  329. {
  330. Logger.Debug("Overwriting levelPack [{0}] beatmapLevelCollection.levels", pack);
  331. if (pack.packID == PluginConfig.CUSTOM_SONG_LEVEL_PACK_ID || (pack as SongCoreCustomBeatmapLevelPack) != null)
  332. {
  333. Logger.Debug("Overwriting SongCore Level Pack collection...");
  334. var newLevels = sortedLevels.Select(x => x as CustomPreviewBeatmapLevel);
  335. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_customPreviewBeatmapLevels", newLevels.ToArray());
  336. }
  337. else
  338. {
  339. // Hack to see if level pack is purchased or not.
  340. BeatmapLevelPackSO beatmapLevelPack = pack as BeatmapLevelPackSO;
  341. if ((pack as BeatmapLevelPackSO) != null)
  342. {
  343. Logger.Debug("Owned level pack...");
  344. var newLevels = sortedLevels.Select(x => x as BeatmapLevelSO);
  345. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  346. }
  347. else
  348. {
  349. Logger.Debug("Unowned DLC Detected...");
  350. var newLevels = sortedLevels.Select(x => x as PreviewBeatmapLevelSO);
  351. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  352. }
  353. Logger.Debug("Overwriting Regular Collection...");
  354. }
  355. }
  356. /// <summary>
  357. /// Sort the song list based on the settings.
  358. /// </summary>
  359. public void ProcessSongList(IBeatmapLevelPack pack)
  360. {
  361. Logger.Trace("ProcessSongList()");
  362. List<IPreviewBeatmapLevel> unsortedSongs = null;
  363. List<IPreviewBeatmapLevel> filteredSongs = null;
  364. List<IPreviewBeatmapLevel> sortedSongs = null;
  365. // This has come in handy many times for debugging issues with Newest.
  366. /*foreach (BeatmapLevelSO level in _originalSongs)
  367. {
  368. if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  369. {
  370. Logger.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
  371. }
  372. else
  373. {
  374. Logger.Debug("Missing KEY: {0}", level.levelID);
  375. }
  376. }*/
  377. // Abort
  378. if (pack == null)
  379. {
  380. Logger.Debug("Cannot process songs yet, no level pack selected...");
  381. return;
  382. }
  383. // fetch unsorted songs.
  384. if (this._settings.filterMode == SongFilterMode.Playlist && this.CurrentPlaylist != null)
  385. {
  386. unsortedSongs = null;
  387. }
  388. else
  389. {
  390. Logger.Debug("Using songs from level pack: {0}", pack.packID);
  391. unsortedSongs = pack.beatmapLevelCollection.beatmapLevels.ToList();
  392. }
  393. // filter
  394. Logger.Debug("Starting filtering songs...");
  395. Stopwatch stopwatch = Stopwatch.StartNew();
  396. switch (_settings.filterMode)
  397. {
  398. case SongFilterMode.Favorites:
  399. filteredSongs = FilterFavorites(pack);
  400. break;
  401. case SongFilterMode.Search:
  402. filteredSongs = FilterSearch(unsortedSongs);
  403. break;
  404. case SongFilterMode.Playlist:
  405. filteredSongs = FilterPlaylist(pack);
  406. break;
  407. case SongFilterMode.None:
  408. default:
  409. Logger.Info("No song filter selected...");
  410. filteredSongs = unsortedSongs;
  411. break;
  412. }
  413. stopwatch.Stop();
  414. Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);
  415. // sort
  416. Logger.Debug("Starting to sort songs...");
  417. stopwatch = Stopwatch.StartNew();
  418. switch (_settings.sortMode)
  419. {
  420. case SongSortMode.Original:
  421. sortedSongs = SortOriginal(filteredSongs);
  422. break;
  423. case SongSortMode.Newest:
  424. sortedSongs = SortNewest(filteredSongs);
  425. break;
  426. case SongSortMode.Author:
  427. sortedSongs = SortAuthor(filteredSongs);
  428. break;
  429. case SongSortMode.UpVotes:
  430. sortedSongs = SortUpVotes(filteredSongs);
  431. break;
  432. case SongSortMode.PlayCount:
  433. sortedSongs = SortBeatSaverPlayCount(filteredSongs);
  434. break;
  435. case SongSortMode.Rating:
  436. sortedSongs = SortBeatSaverRating(filteredSongs);
  437. break;
  438. case SongSortMode.YourPlayCount:
  439. sortedSongs = SortPlayCount(filteredSongs);
  440. break;
  441. case SongSortMode.PP:
  442. sortedSongs = SortPerformancePoints(filteredSongs);
  443. break;
  444. case SongSortMode.Difficulty:
  445. sortedSongs = SortDifficulty(filteredSongs);
  446. break;
  447. case SongSortMode.Random:
  448. sortedSongs = SortRandom(filteredSongs);
  449. break;
  450. case SongSortMode.Default:
  451. default:
  452. sortedSongs = SortSongName(filteredSongs);
  453. break;
  454. }
  455. if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
  456. {
  457. sortedSongs.Reverse();
  458. }
  459. stopwatch.Stop();
  460. Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
  461. this.OverwriteCurrentLevelPack(pack, sortedSongs);
  462. //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
  463. }
  464. /// <summary>
  465. /// For now the editing playlist will be considered the favorites playlist.
  466. /// Users can edit the settings file themselves.
  467. /// </summary>
  468. private List<IPreviewBeatmapLevel> FilterFavorites(IBeatmapLevelPack pack)
  469. {
  470. Logger.Info("Filtering song list as favorites playlist...");
  471. if (this.CurrentEditingPlaylist != null)
  472. {
  473. this.CurrentPlaylist = this.CurrentEditingPlaylist;
  474. }
  475. return this.FilterPlaylist(pack);
  476. }
  477. /// <summary>
  478. /// Filter for a search query.
  479. /// </summary>
  480. /// <param name="levels"></param>
  481. /// <returns></returns>
  482. private List<IPreviewBeatmapLevel> FilterSearch(List<IPreviewBeatmapLevel> levels)
  483. {
  484. // Make sure we can actually search.
  485. if (this._settings.searchTerms.Count <= 0)
  486. {
  487. Logger.Error("Tried to search for a song with no valid search terms...");
  488. SortSongName(levels);
  489. return levels;
  490. }
  491. string searchTerm = this._settings.searchTerms[0];
  492. if (String.IsNullOrEmpty(searchTerm))
  493. {
  494. Logger.Error("Empty search term entered.");
  495. SortSongName(levels);
  496. return levels;
  497. }
  498. Logger.Info("Filtering song list by search term: {0}", searchTerm);
  499. var terms = searchTerm.Split(' ');
  500. foreach (var term in terms)
  501. {
  502. levels = levels.Intersect(
  503. levels
  504. .Where(x => $"{x.songName} {x.songSubName} {x.songAuthorName} {x.levelAuthorName}".ToLower().Contains(term.ToLower()))
  505. .ToList(
  506. )
  507. ).ToList();
  508. }
  509. return levels;
  510. }
  511. /// <summary>
  512. /// Filter for a playlist (favorites uses this).
  513. /// </summary>
  514. /// <param name="pack"></param>
  515. /// <returns></returns>
  516. private List<IPreviewBeatmapLevel> FilterPlaylist(IBeatmapLevelPack pack)
  517. {
  518. // bail if no playlist, usually means the settings stored one the user then moved.
  519. if (this.CurrentPlaylist == null)
  520. {
  521. Logger.Error("Trying to load a null playlist...");
  522. this.Settings.filterMode = SongFilterMode.None;
  523. return null;
  524. }
  525. // Get song keys
  526. PlaylistsCollection.MatchSongsForPlaylist(this.CurrentPlaylist, true);
  527. Logger.Debug("Filtering songs for playlist: {0}", this.CurrentPlaylist.playlistTitle);
  528. Dictionary<String, IPreviewBeatmapLevel> levelDict = new Dictionary<string, IPreviewBeatmapLevel>();
  529. foreach (var level in pack.beatmapLevelCollection.beatmapLevels)
  530. {
  531. if (!levelDict.ContainsKey(level.levelID))
  532. {
  533. levelDict.Add(level.levelID, level);
  534. }
  535. }
  536. List<IPreviewBeatmapLevel> songList = new List<IPreviewBeatmapLevel>();
  537. foreach (PlaylistSong ps in this.CurrentPlaylist.songs)
  538. {
  539. if (ps.level != null && levelDict.ContainsKey(ps.level.levelID))
  540. {
  541. songList.Add(levelDict[ps.level.levelID]);
  542. }
  543. else
  544. {
  545. Logger.Debug("Could not find song in playlist: {0}", ps.songName);
  546. }
  547. }
  548. Logger.Debug("Playlist filtered song count: {0}", songList.Count);
  549. return songList;
  550. }
  551. /// <summary>
  552. /// Sorting returns original list.
  553. /// </summary>
  554. /// <param name="levels"></param>
  555. /// <returns></returns>
  556. private List<IPreviewBeatmapLevel> SortOriginal(List<IPreviewBeatmapLevel> levels)
  557. {
  558. Logger.Info("Sorting song list as original");
  559. return levels;
  560. }
  561. /// <summary>
  562. /// Sorting by newest (file time, creation+modified).
  563. /// </summary>
  564. /// <param name="levels"></param>
  565. /// <returns></returns>
  566. private List<IPreviewBeatmapLevel> SortNewest(List<IPreviewBeatmapLevel> levels)
  567. {
  568. Logger.Info("Sorting song list as newest.");
  569. return levels
  570. .OrderBy(x => _weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0)
  571. .ThenByDescending(x => !_levelIdToCustomLevel.ContainsKey(x.levelID) ? (_weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0) : _cachedLastWriteTimes[x.levelID])
  572. .ToList();
  573. }
  574. /// <summary>
  575. /// Sorting by the song author.
  576. /// </summary>
  577. /// <param name="levelIds"></param>
  578. /// <returns></returns>
  579. private List<IPreviewBeatmapLevel> SortAuthor(List<IPreviewBeatmapLevel> levelIds)
  580. {
  581. Logger.Info("Sorting song list by author");
  582. return levelIds
  583. .OrderBy(x => x.songAuthorName)
  584. .ThenBy(x => x.songName)
  585. .ToList();
  586. }
  587. /// <summary>
  588. /// Sorting by song play count.
  589. /// </summary>
  590. /// <param name="levels"></param>
  591. /// <returns></returns>
  592. private List<IPreviewBeatmapLevel> SortPlayCount(List<IPreviewBeatmapLevel> levels)
  593. {
  594. Logger.Info("Sorting song list by playcount");
  595. return levels
  596. .OrderByDescending(x => _levelIdToPlayCount.ContainsKey(x.levelID) ? _levelIdToPlayCount[x.levelID] : 0)
  597. .ThenBy(x => x.songName)
  598. .ToList();
  599. }
  600. /// <summary>
  601. /// Sorting by PP.
  602. /// </summary>
  603. /// <param name="levels"></param>
  604. /// <returns></returns>
  605. private List<IPreviewBeatmapLevel> SortPerformancePoints(List<IPreviewBeatmapLevel> levels)
  606. {
  607. Logger.Info("Sorting song list by performance points...");
  608. if (ScoreSaberDatabaseDownloader.ScoreSaberDataFile == null)
  609. {
  610. return levels;
  611. }
  612. return levels
  613. .OrderByDescending(x =>
  614. {
  615. var hash = CustomHelpers.GetSongHash(x.levelID);
  616. if (ScoreSaberDatabaseDownloader.ScoreSaberDataFile.SongHashToScoreSaberData.ContainsKey(hash))
  617. {
  618. return ScoreSaberDatabaseDownloader.ScoreSaberDataFile.SongHashToScoreSaberData[hash].diffs.Max(y => y.pp);
  619. }
  620. else
  621. {
  622. return 0;
  623. }
  624. })
  625. .ToList();
  626. }
  627. private List<IPreviewBeatmapLevel> SortDifficulty(List<IPreviewBeatmapLevel> levels)
  628. {
  629. Logger.Info("Sorting song list by difficulty...");
  630. IEnumerable<BeatmapDifficulty> difficultyIterator = Enum.GetValues(typeof(BeatmapDifficulty)).Cast<BeatmapDifficulty>();
  631. Dictionary<string, int> levelIdToDifficultyValue = new Dictionary<string, int>();
  632. foreach (IPreviewBeatmapLevel level in levels)
  633. {
  634. // only need to process a level once
  635. if (levelIdToDifficultyValue.ContainsKey(level.levelID))
  636. {
  637. continue;
  638. }
  639. // TODO - fix, not honoring beatmap characteristic.
  640. int difficultyValue = 0;
  641. if (level as BeatmapLevelSO != null)
  642. {
  643. var beatmapSet = (level as BeatmapLevelSO).difficultyBeatmapSets;
  644. difficultyValue = beatmapSet
  645. .SelectMany(x => x.difficultyBeatmaps)
  646. .Sum(x => _difficultyWeights[x.difficulty]);
  647. }
  648. else if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  649. {
  650. var beatmapSet = (_levelIdToCustomLevel[level.levelID] as CustomPreviewBeatmapLevel).standardLevelInfoSaveData.difficultyBeatmapSets;
  651. difficultyValue = beatmapSet
  652. .SelectMany(x => x.difficultyBeatmaps)
  653. .Sum(x => _difficultyWeights[(BeatmapDifficulty)Enum.Parse(typeof(BeatmapDifficulty), x.difficulty)]);
  654. }
  655. levelIdToDifficultyValue.Add(level.levelID, difficultyValue);
  656. }
  657. return levels
  658. .OrderBy(x => levelIdToDifficultyValue[x.levelID])
  659. .ThenBy(x => x.songName)
  660. .ToList();
  661. }
  662. /// <summary>
  663. /// Randomize the sorting.
  664. /// </summary>
  665. /// <param name="levelIds"></param>
  666. /// <returns></returns>
  667. private List<IPreviewBeatmapLevel> SortRandom(List<IPreviewBeatmapLevel> levelIds)
  668. {
  669. Logger.Info("Sorting song list by random (seed={0})...", this.Settings.randomSongSeed);
  670. System.Random rnd = new System.Random(this.Settings.randomSongSeed);
  671. return levelIds
  672. .OrderBy(x => rnd.Next())
  673. .ToList();
  674. }
  675. /// <summary>
  676. /// Sorting by the song name.
  677. /// </summary>
  678. /// <param name="levels"></param>
  679. /// <returns></returns>
  680. private List<IPreviewBeatmapLevel> SortSongName(List<IPreviewBeatmapLevel> levels)
  681. {
  682. Logger.Info("Sorting song list as default (songName)");
  683. return levels
  684. .OrderBy(x => x.songName)
  685. .ThenBy(x => x.songAuthorName)
  686. .ToList();
  687. }
  688. /// <summary>
  689. /// Sorting by Downloader UpVotes.
  690. /// </summary>
  691. /// <param name="levelIds"></param>
  692. /// <returns></returns>
  693. private List<IPreviewBeatmapLevel> SortUpVotes(List<IPreviewBeatmapLevel> levelIds)
  694. {
  695. Logger.Info("Sorting song list by UpVotes");
  696. // Do not always have data when trying to sort by UpVotes
  697. if (_levelHashToDownloaderData == null)
  698. {
  699. return levelIds;
  700. }
  701. return levelIds
  702. .OrderByDescending(x => {
  703. var hash = CustomHelpers.GetSongHash(x.levelID);
  704. if (_levelHashToDownloaderData.ContainsKey(hash))
  705. {
  706. return _levelHashToDownloaderData[hash].Upvotes;
  707. }
  708. else
  709. {
  710. return int.MinValue;
  711. }
  712. })
  713. .ToList();
  714. }
  715. /// <summary>
  716. /// Sorting by BeatSaver rating stat.
  717. /// </summary>
  718. /// <param name="levelIds"></param>
  719. /// <returns></returns>
  720. private List<IPreviewBeatmapLevel> SortBeatSaverPlayCount(List<IPreviewBeatmapLevel> levelIds)
  721. {
  722. Logger.Info("Sorting song list by BeatSaver PlayCount");
  723. // Do not always have data when trying to sort by UpVotes
  724. if (_levelHashToDownloaderData == null)
  725. {
  726. return levelIds;
  727. }
  728. return levelIds
  729. .OrderByDescending(x => {
  730. var hash = CustomHelpers.GetSongHash(x.levelID);
  731. if (_levelHashToDownloaderData.ContainsKey(hash))
  732. {
  733. return _levelHashToDownloaderData[hash].PlayedCount;
  734. }
  735. else
  736. {
  737. return int.MinValue;
  738. }
  739. })
  740. .ToList();
  741. }
  742. /// <summary>
  743. /// Sorting by BeatSaver rating stat.
  744. /// </summary>
  745. /// <param name="levelIds"></param>
  746. /// <returns></returns>
  747. private List<IPreviewBeatmapLevel> SortBeatSaverRating(List<IPreviewBeatmapLevel> levelIds)
  748. {
  749. Logger.Info("Sorting song list by BeatSaver Rating");
  750. // Do not always have data when trying to sort by UpVotes
  751. if (_levelHashToDownloaderData == null)
  752. {
  753. return levelIds;
  754. }
  755. return levelIds
  756. .OrderByDescending(x => {
  757. var hash = CustomHelpers.GetSongHash(x.levelID);
  758. if (_levelHashToDownloaderData.ContainsKey(hash))
  759. {
  760. return _levelHashToDownloaderData[hash].Rating;
  761. }
  762. else
  763. {
  764. return int.MinValue;
  765. }
  766. })
  767. .ToList();
  768. }
  769. }
  770. }