SongBrowserModel.cs 30 KB

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