SongBrowserModel.cs 30 KB

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