SongBrowserModel.cs 34 KB

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