SongBrowserModel.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  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, int> _levelIdToPlayCount;
  30. public BeatmapCharacteristicSO CurrentBeatmapCharacteristicSO;
  31. public static Func<IBeatmapLevelPack, List<IPreviewBeatmapLevel>> CustomFilterHandler;
  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. double lastWriteTime = GetSongUserDate(level.Value);
  178. _cachedLastWriteTimes[level.Value.levelID] = lastWriteTime;
  179. }
  180. if (!_levelIdToCustomLevel.ContainsKey(level.Value.levelID))
  181. {
  182. _levelIdToCustomLevel.Add(level.Value.levelID, level.Value);
  183. }
  184. }
  185. lastWriteTimer.Stop();
  186. Logger.Info("Determining song download time and determining mappings took {0}ms", lastWriteTimer.ElapsedMilliseconds);
  187. // Update song Infos, directory tree, and sort
  188. this.UpdatePlayCounts();
  189. // Check if we need to upgrade settings file favorites
  190. try
  191. {
  192. this.Settings.ConvertFavoritesToPlaylist(_levelIdToCustomLevel);
  193. }
  194. catch (Exception e)
  195. {
  196. Logger.Exception("FAILED TO CONVERT FAVORITES TO PLAYLIST!", e);
  197. }
  198. // load the current editing playlist or make one
  199. if (CurrentEditingPlaylist == null && !String.IsNullOrEmpty(this.Settings.currentEditingPlaylistFile))
  200. {
  201. Logger.Debug("Loading playlist for editing: {0}", this.Settings.currentEditingPlaylistFile);
  202. CurrentEditingPlaylist = Playlist.LoadPlaylist(this.Settings.currentEditingPlaylistFile);
  203. PlaylistsCollection.MatchSongsForPlaylist(CurrentEditingPlaylist, true);
  204. }
  205. if (CurrentEditingPlaylist == null)
  206. {
  207. Logger.Debug("Current editing playlist does not exit, create...");
  208. CurrentEditingPlaylist = new Playlist
  209. {
  210. playlistTitle = "Song Browser Favorites",
  211. playlistAuthor = "SongBrowser",
  212. fileLoc = this.Settings.currentEditingPlaylistFile,
  213. image = Base64Sprites.SpriteToBase64(Base64Sprites.BeastSaberLogo),
  214. songs = new List<PlaylistSong>(),
  215. };
  216. }
  217. CurrentEditingPlaylistLevelIds = new HashSet<string>();
  218. foreach (PlaylistSong ps in CurrentEditingPlaylist.songs)
  219. {
  220. // Sometimes we cannot match a song
  221. string levelId = null;
  222. if (ps.level != null)
  223. {
  224. levelId = ps.level.levelID;
  225. }
  226. else if (!String.IsNullOrEmpty(ps.levelId))
  227. {
  228. levelId = ps.levelId;
  229. }
  230. else
  231. {
  232. //Logger.Debug("MISSING SONG {0}", ps.songName);
  233. continue;
  234. }
  235. CurrentEditingPlaylistLevelIds.Add(levelId);
  236. }
  237. // Signal complete
  238. if (SongCore.Loader.CustomLevels.Count > 0)
  239. {
  240. didFinishProcessingSongs?.Invoke(SongCore.Loader.CustomLevels);
  241. }
  242. timer.Stop();
  243. Logger.Info("Updating songs infos took {0}ms", timer.ElapsedMilliseconds);
  244. }
  245. /// <summary>
  246. /// Try to get the date from the cover file, likely the most reliable.
  247. /// Fall back on the folders creation date.
  248. /// </summary>
  249. /// <param name="level"></param>
  250. /// <returns></returns>
  251. private double GetSongUserDate(CustomPreviewBeatmapLevel level)
  252. {
  253. var coverPath = Path.Combine(level.customLevelPath, level.standardLevelInfoSaveData.coverImageFilename);
  254. var lastTime = EPOCH;
  255. if (File.Exists(coverPath))
  256. {
  257. var lastWriteTime = File.GetLastWriteTimeUtc(coverPath);
  258. var lastCreateTime = File.GetCreationTimeUtc(coverPath);
  259. lastTime = lastWriteTime > lastCreateTime ? lastWriteTime : lastCreateTime;
  260. }
  261. else
  262. {
  263. var lastCreateTime = File.GetCreationTimeUtc(level.customLevelPath);
  264. lastTime = lastCreateTime;
  265. }
  266. return (lastTime - EPOCH).TotalMilliseconds;
  267. }
  268. /// <summary>
  269. /// SongLoader doesn't fire event when we delete a song.
  270. /// </summary>
  271. /// <param name="levelPack"></param>
  272. /// <param name="levelId"></param>
  273. public void RemoveSongFromLevelPack(IBeatmapLevelPack levelPack, String levelId)
  274. {
  275. levelPack.beatmapLevelCollection.beatmapLevels.ToList().RemoveAll(x => x.levelID == levelId);
  276. }
  277. /// <summary>
  278. /// Update the gameplay play counts.
  279. /// </summary>
  280. /// <param name="gameplayMode"></param>
  281. private void UpdatePlayCounts()
  282. {
  283. // Reset current playcounts
  284. _levelIdToPlayCount = new Dictionary<string, int>();
  285. // Build a map of levelId to sum of all playcounts and sort.
  286. PlayerDataModelSO playerData = Resources.FindObjectsOfTypeAll<PlayerDataModelSO>().FirstOrDefault();
  287. foreach (var levelData in playerData.currentLocalPlayer.levelsStatsData)
  288. {
  289. if (!_levelIdToPlayCount.ContainsKey(levelData.levelID))
  290. {
  291. _levelIdToPlayCount.Add(levelData.levelID, 0);
  292. }
  293. _levelIdToPlayCount[levelData.levelID] += levelData.playCount;
  294. }
  295. }
  296. /// <summary>
  297. /// Add Song to Editing Playlist
  298. /// </summary>
  299. /// <param name="songInfo"></param>
  300. public void AddSongToEditingPlaylist(IBeatmapLevel songInfo)
  301. {
  302. if (this.CurrentEditingPlaylist == null)
  303. {
  304. return;
  305. }
  306. this.CurrentEditingPlaylist.songs.Add(new PlaylistSong()
  307. {
  308. songName = songInfo.songName,
  309. levelId = songInfo.levelID,
  310. hash = CustomHelpers.GetSongHash(songInfo.levelID),
  311. });
  312. this.CurrentEditingPlaylistLevelIds.Add(songInfo.levelID);
  313. this.CurrentEditingPlaylist.SavePlaylist();
  314. }
  315. /// <summary>
  316. /// Remove Song from editing playlist
  317. /// </summary>
  318. /// <param name="levelId"></param>
  319. public void RemoveSongFromEditingPlaylist(IBeatmapLevel songInfo)
  320. {
  321. if (this.CurrentEditingPlaylist == null)
  322. {
  323. return;
  324. }
  325. this.CurrentEditingPlaylist.songs.RemoveAll(x => x.level != null && x.level.levelID == songInfo.levelID);
  326. this.CurrentEditingPlaylistLevelIds.RemoveWhere(x => x == songInfo.levelID);
  327. this.CurrentEditingPlaylist.SavePlaylist();
  328. }
  329. /// <summary>
  330. /// Overwrite the current level pack.
  331. /// </summary>
  332. private void OverwriteCurrentLevelPack(IBeatmapLevelPack pack, List<IPreviewBeatmapLevel> sortedLevels)
  333. {
  334. Logger.Debug("Overwriting levelPack [{0}] beatmapLevelCollection.levels", pack);
  335. if (pack.packID == PluginConfig.CUSTOM_SONG_LEVEL_PACK_ID || (pack as SongCoreCustomBeatmapLevelPack) != null)
  336. {
  337. Logger.Debug("Overwriting SongCore Level Pack collection...");
  338. var newLevels = sortedLevels.Select(x => x as CustomPreviewBeatmapLevel);
  339. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_customPreviewBeatmapLevels", newLevels.ToArray());
  340. }
  341. else
  342. {
  343. // Hack to see if level pack is purchased or not.
  344. BeatmapLevelPackSO beatmapLevelPack = pack as BeatmapLevelPackSO;
  345. if ((pack as BeatmapLevelPackSO) != null)
  346. {
  347. Logger.Debug("Owned level pack...");
  348. var newLevels = sortedLevels.Select(x => x as BeatmapLevelSO);
  349. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  350. }
  351. else
  352. {
  353. Logger.Debug("Unowned DLC Detected...");
  354. var newLevels = sortedLevels.Select(x => x as PreviewBeatmapLevelSO);
  355. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  356. }
  357. Logger.Debug("Overwriting Regular Collection...");
  358. }
  359. }
  360. /// <summary>
  361. /// Sort the song list based on the settings.
  362. /// </summary>
  363. public void ProcessSongList(IBeatmapLevelPack pack)
  364. {
  365. Logger.Trace("ProcessSongList()");
  366. List<IPreviewBeatmapLevel> unsortedSongs = null;
  367. List<IPreviewBeatmapLevel> filteredSongs = null;
  368. List<IPreviewBeatmapLevel> sortedSongs = null;
  369. // This has come in handy many times for debugging issues with Newest.
  370. /*foreach (BeatmapLevelSO level in _originalSongs)
  371. {
  372. if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  373. {
  374. Logger.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
  375. }
  376. else
  377. {
  378. Logger.Debug("Missing KEY: {0}", level.levelID);
  379. }
  380. }*/
  381. // Abort
  382. if (pack == null)
  383. {
  384. Logger.Debug("Cannot process songs yet, no level pack selected...");
  385. return;
  386. }
  387. // fetch unsorted songs.
  388. if (this._settings.filterMode == SongFilterMode.Playlist && this.CurrentPlaylist != null)
  389. {
  390. unsortedSongs = null;
  391. }
  392. else
  393. {
  394. Logger.Debug("Using songs from level pack: {0}", pack.packID);
  395. unsortedSongs = pack.beatmapLevelCollection.beatmapLevels.ToList();
  396. }
  397. // filter
  398. Logger.Debug("Starting filtering songs...");
  399. Stopwatch stopwatch = Stopwatch.StartNew();
  400. switch (_settings.filterMode)
  401. {
  402. case SongFilterMode.Favorites:
  403. filteredSongs = FilterFavorites(pack);
  404. break;
  405. case SongFilterMode.Search:
  406. filteredSongs = FilterSearch(unsortedSongs);
  407. break;
  408. case SongFilterMode.Playlist:
  409. filteredSongs = FilterPlaylist(pack);
  410. break;
  411. case SongFilterMode.Ranked:
  412. filteredSongs = FilterRanked(unsortedSongs, true, false);
  413. break;
  414. case SongFilterMode.Unranked:
  415. filteredSongs = FilterRanked(unsortedSongs, false, true);
  416. break;
  417. case SongFilterMode.Custom:
  418. Logger.Info("Song filter mode set to custom. Deferring filter behaviour to another mod.");
  419. filteredSongs = CustomFilterHandler != null ? CustomFilterHandler.Invoke(pack) : unsortedSongs;
  420. break;
  421. case SongFilterMode.None:
  422. default:
  423. Logger.Info("No song filter selected...");
  424. filteredSongs = unsortedSongs;
  425. break;
  426. }
  427. stopwatch.Stop();
  428. Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);
  429. // sort
  430. Logger.Debug("Starting to sort songs...");
  431. stopwatch = Stopwatch.StartNew();
  432. switch (_settings.sortMode)
  433. {
  434. case SongSortMode.Original:
  435. sortedSongs = SortOriginal(filteredSongs);
  436. break;
  437. case SongSortMode.Newest:
  438. sortedSongs = SortNewest(filteredSongs);
  439. break;
  440. case SongSortMode.Author:
  441. sortedSongs = SortAuthor(filteredSongs);
  442. break;
  443. case SongSortMode.UpVotes:
  444. sortedSongs = SortUpVotes(filteredSongs);
  445. break;
  446. case SongSortMode.PlayCount:
  447. sortedSongs = SortBeatSaverPlayCount(filteredSongs);
  448. break;
  449. case SongSortMode.Rating:
  450. sortedSongs = SortBeatSaverRating(filteredSongs);
  451. break;
  452. case SongSortMode.Heat:
  453. sortedSongs = SortBeatSaverHeat(filteredSongs);
  454. break;
  455. case SongSortMode.YourPlayCount:
  456. sortedSongs = SortPlayCount(filteredSongs);
  457. break;
  458. case SongSortMode.PP:
  459. sortedSongs = SortPerformancePoints(filteredSongs);
  460. break;
  461. case SongSortMode.Stars:
  462. sortedSongs = SortStars(filteredSongs);
  463. break;
  464. case SongSortMode.Difficulty:
  465. sortedSongs = SortDifficulty(filteredSongs);
  466. break;
  467. case SongSortMode.Random:
  468. sortedSongs = SortRandom(filteredSongs);
  469. break;
  470. case SongSortMode.Default:
  471. default:
  472. sortedSongs = SortSongName(filteredSongs);
  473. break;
  474. }
  475. if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
  476. {
  477. sortedSongs.Reverse();
  478. }
  479. stopwatch.Stop();
  480. Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
  481. this.OverwriteCurrentLevelPack(pack, sortedSongs);
  482. //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
  483. }
  484. /// <summary>
  485. /// For now the editing playlist will be considered the favorites playlist.
  486. /// Users can edit the settings file themselves.
  487. /// </summary>
  488. private List<IPreviewBeatmapLevel> FilterFavorites(IBeatmapLevelPack pack)
  489. {
  490. Logger.Info("Filtering song list as favorites playlist...");
  491. if (this.CurrentEditingPlaylist != null)
  492. {
  493. this.CurrentPlaylist = this.CurrentEditingPlaylist;
  494. }
  495. return this.FilterPlaylist(pack);
  496. }
  497. /// <summary>
  498. /// Filter for a search query.
  499. /// </summary>
  500. /// <param name="levels"></param>
  501. /// <returns></returns>
  502. private List<IPreviewBeatmapLevel> FilterSearch(List<IPreviewBeatmapLevel> levels)
  503. {
  504. // Make sure we can actually search.
  505. if (this._settings.searchTerms.Count <= 0)
  506. {
  507. Logger.Error("Tried to search for a song with no valid search terms...");
  508. SortSongName(levels);
  509. return levels;
  510. }
  511. string searchTerm = this._settings.searchTerms[0];
  512. if (String.IsNullOrEmpty(searchTerm))
  513. {
  514. Logger.Error("Empty search term entered.");
  515. SortSongName(levels);
  516. return levels;
  517. }
  518. Logger.Info("Filtering song list by search term: {0}", searchTerm);
  519. var terms = searchTerm.Split(' ');
  520. foreach (var term in terms)
  521. {
  522. levels = levels.Intersect(
  523. levels
  524. .Where(x => $"{x.songName} {x.songSubName} {x.songAuthorName} {x.levelAuthorName}".ToLower().Contains(term.ToLower()))
  525. .ToList(
  526. )
  527. ).ToList();
  528. }
  529. return levels;
  530. }
  531. /// <summary>
  532. /// Filter for a playlist (favorites uses this).
  533. /// </summary>
  534. /// <param name="pack"></param>
  535. /// <returns></returns>
  536. private List<IPreviewBeatmapLevel> FilterPlaylist(IBeatmapLevelPack pack)
  537. {
  538. // bail if no playlist, usually means the settings stored one the user then moved.
  539. if (this.CurrentPlaylist == null)
  540. {
  541. Logger.Error("Trying to load a null playlist...");
  542. this.Settings.filterMode = SongFilterMode.None;
  543. return null;
  544. }
  545. // Get song keys
  546. PlaylistsCollection.MatchSongsForPlaylist(this.CurrentPlaylist, true);
  547. Logger.Debug("Filtering songs for playlist: {0}", this.CurrentPlaylist.playlistTitle);
  548. Dictionary<String, IPreviewBeatmapLevel> levelDict = new Dictionary<string, IPreviewBeatmapLevel>();
  549. foreach (var level in pack.beatmapLevelCollection.beatmapLevels)
  550. {
  551. if (!levelDict.ContainsKey(level.levelID))
  552. {
  553. levelDict.Add(level.levelID, level);
  554. }
  555. }
  556. List<IPreviewBeatmapLevel> songList = new List<IPreviewBeatmapLevel>();
  557. foreach (PlaylistSong ps in this.CurrentPlaylist.songs)
  558. {
  559. if (ps.level != null && levelDict.ContainsKey(ps.level.levelID))
  560. {
  561. songList.Add(levelDict[ps.level.levelID]);
  562. }
  563. else
  564. {
  565. Logger.Debug("Could not find song in playlist: {0}", ps.songName);
  566. }
  567. }
  568. Logger.Debug("Playlist filtered song count: {0}", songList.Count);
  569. return songList;
  570. }
  571. /// <summary>
  572. /// Filter songs based on ranked or unranked status.
  573. /// </summary>
  574. /// <param name="levels"></param>
  575. /// <param name="includeRanked"></param>
  576. /// <param name="includeUnranked"></param>
  577. /// <returns></returns>
  578. private List<IPreviewBeatmapLevel> FilterRanked(List<IPreviewBeatmapLevel> levels, bool includeRanked, bool includeUnranked)
  579. {
  580. return levels.Where(x =>
  581. {
  582. var hash = CustomHelpers.GetSongHash(x.levelID);
  583. double maxPP = 0.0;
  584. if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
  585. {
  586. maxPP = SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs.Max(y => y.pp);
  587. }
  588. if (maxPP > 0f)
  589. {
  590. return includeRanked;
  591. }
  592. else
  593. {
  594. return includeUnranked;
  595. }
  596. }).ToList();
  597. }
  598. /// <summary>
  599. /// Sorting returns original list.
  600. /// </summary>
  601. /// <param name="levels"></param>
  602. /// <returns></returns>
  603. private List<IPreviewBeatmapLevel> SortOriginal(List<IPreviewBeatmapLevel> levels)
  604. {
  605. Logger.Info("Sorting song list as original");
  606. return levels;
  607. }
  608. /// <summary>
  609. /// Sorting by newest (file time, creation+modified).
  610. /// </summary>
  611. /// <param name="levels"></param>
  612. /// <returns></returns>
  613. private List<IPreviewBeatmapLevel> SortNewest(List<IPreviewBeatmapLevel> levels)
  614. {
  615. Logger.Info("Sorting song list as newest.");
  616. return levels
  617. .OrderBy(x => _weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0)
  618. .ThenByDescending(x => !_levelIdToCustomLevel.ContainsKey(x.levelID) ? (_weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0) : _cachedLastWriteTimes[x.levelID])
  619. .ToList();
  620. }
  621. /// <summary>
  622. /// Sorting by the song author.
  623. /// </summary>
  624. /// <param name="levelIds"></param>
  625. /// <returns></returns>
  626. private List<IPreviewBeatmapLevel> SortAuthor(List<IPreviewBeatmapLevel> levelIds)
  627. {
  628. Logger.Info("Sorting song list by author");
  629. return levelIds
  630. .OrderBy(x => x.songAuthorName)
  631. .ThenBy(x => x.songName)
  632. .ToList();
  633. }
  634. /// <summary>
  635. /// Sorting by song users play count.
  636. /// </summary>
  637. /// <param name="levels"></param>
  638. /// <returns></returns>
  639. private List<IPreviewBeatmapLevel> SortPlayCount(List<IPreviewBeatmapLevel> levels)
  640. {
  641. Logger.Info("Sorting song list by playcount");
  642. return levels
  643. .OrderByDescending(x => _levelIdToPlayCount.ContainsKey(x.levelID) ? _levelIdToPlayCount[x.levelID] : 0)
  644. .ThenBy(x => x.songName)
  645. .ToList();
  646. }
  647. /// <summary>
  648. /// Sorting by PP.
  649. /// </summary>
  650. /// <param name="levels"></param>
  651. /// <returns></returns>
  652. private List<IPreviewBeatmapLevel> SortPerformancePoints(List<IPreviewBeatmapLevel> levels)
  653. {
  654. Logger.Info("Sorting song list by performance points...");
  655. if (!SongDataCore.Plugin.ScoreSaber.IsDataAvailable())
  656. {
  657. return levels;
  658. }
  659. return levels
  660. .OrderByDescending(x =>
  661. {
  662. var hash = CustomHelpers.GetSongHash(x.levelID);
  663. if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
  664. {
  665. return SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs.Max(y => y.pp);
  666. }
  667. else
  668. {
  669. return 0;
  670. }
  671. })
  672. .ToList();
  673. }
  674. /// <summary>
  675. /// Sorting by star rating.
  676. /// </summary>
  677. /// <param name="levels"></param>
  678. /// <returns></returns>
  679. private List<IPreviewBeatmapLevel> SortStars(List<IPreviewBeatmapLevel> levels)
  680. {
  681. Logger.Info("Sorting song list by star points...");
  682. if (!SongDataCore.Plugin.ScoreSaber.IsDataAvailable())
  683. {
  684. return levels;
  685. }
  686. return levels
  687. .OrderByDescending(x =>
  688. {
  689. var hash = CustomHelpers.GetSongHash(x.levelID);
  690. var stars = 0.0;
  691. if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
  692. {
  693. var diffs = SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs;
  694. stars = diffs.Max(y => y.star);
  695. }
  696. //Logger.Debug("Stars={0}", stars);
  697. if (stars != 0)
  698. {
  699. return stars;
  700. }
  701. if (_settings.invertSortResults)
  702. {
  703. return double.MaxValue;
  704. }
  705. else
  706. {
  707. return double.MinValue;
  708. }
  709. })
  710. .ToList();
  711. }
  712. /// <summary>
  713. /// Attempt to sort by songs containing easy first
  714. /// </summary>
  715. /// <param name="levels"></param>
  716. /// <returns></returns>
  717. private List<IPreviewBeatmapLevel> SortDifficulty(List<IPreviewBeatmapLevel> levels)
  718. {
  719. Logger.Info("Sorting song list by difficulty...");
  720. IEnumerable<BeatmapDifficulty> difficultyIterator = Enum.GetValues(typeof(BeatmapDifficulty)).Cast<BeatmapDifficulty>();
  721. Dictionary<string, int> levelIdToDifficultyValue = new Dictionary<string, int>();
  722. foreach (IPreviewBeatmapLevel level in levels)
  723. {
  724. // only need to process a level once
  725. if (levelIdToDifficultyValue.ContainsKey(level.levelID))
  726. {
  727. continue;
  728. }
  729. // TODO - fix, not honoring beatmap characteristic.
  730. int difficultyValue = 0;
  731. if (level as BeatmapLevelSO != null)
  732. {
  733. var beatmapSet = (level as BeatmapLevelSO).difficultyBeatmapSets;
  734. difficultyValue = beatmapSet
  735. .SelectMany(x => x.difficultyBeatmaps)
  736. .Sum(x => _difficultyWeights[x.difficulty]);
  737. }
  738. else if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  739. {
  740. var beatmapSet = (_levelIdToCustomLevel[level.levelID] as CustomPreviewBeatmapLevel).standardLevelInfoSaveData.difficultyBeatmapSets;
  741. difficultyValue = beatmapSet
  742. .SelectMany(x => x.difficultyBeatmaps)
  743. .Sum(x => _difficultyWeights[(BeatmapDifficulty)Enum.Parse(typeof(BeatmapDifficulty), x.difficulty)]);
  744. }
  745. levelIdToDifficultyValue.Add(level.levelID, difficultyValue);
  746. }
  747. return levels
  748. .OrderBy(x => levelIdToDifficultyValue[x.levelID])
  749. .ThenBy(x => x.songName)
  750. .ToList();
  751. }
  752. /// <summary>
  753. /// Randomize the sorting.
  754. /// </summary>
  755. /// <param name="levelIds"></param>
  756. /// <returns></returns>
  757. private List<IPreviewBeatmapLevel> SortRandom(List<IPreviewBeatmapLevel> levelIds)
  758. {
  759. Logger.Info("Sorting song list by random (seed={0})...", this.Settings.randomSongSeed);
  760. System.Random rnd = new System.Random(this.Settings.randomSongSeed);
  761. return levelIds
  762. .OrderBy(x => rnd.Next())
  763. .ToList();
  764. }
  765. /// <summary>
  766. /// Sorting by the song name.
  767. /// </summary>
  768. /// <param name="levels"></param>
  769. /// <returns></returns>
  770. private List<IPreviewBeatmapLevel> SortSongName(List<IPreviewBeatmapLevel> levels)
  771. {
  772. Logger.Info("Sorting song list as default (songName)");
  773. return levels
  774. .OrderBy(x => x.songName)
  775. .ThenBy(x => x.songAuthorName)
  776. .ToList();
  777. }
  778. /// <summary>
  779. /// Sorting by BeatSaver UpVotes.
  780. /// </summary>
  781. /// <param name="levelIds"></param>
  782. /// <returns></returns>
  783. private List<IPreviewBeatmapLevel> SortUpVotes(List<IPreviewBeatmapLevel> levelIds)
  784. {
  785. Logger.Info("Sorting song list by BeatSaver UpVotes");
  786. // Do not always have data when trying to sort by UpVotes
  787. if (!SongDataCore.Plugin.BeatSaver.IsDataAvailable())
  788. {
  789. return levelIds;
  790. }
  791. return levelIds
  792. .OrderByDescending(x => {
  793. var hash = CustomHelpers.GetSongHash(x.levelID);
  794. if (SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
  795. {
  796. return SongDataCore.Plugin.BeatSaver.Data.Songs[hash].stats.upVotes;
  797. }
  798. else
  799. {
  800. return int.MinValue;
  801. }
  802. })
  803. .ToList();
  804. }
  805. /// <summary>
  806. /// Sorting by BeatSaver playcount stat.
  807. /// </summary>
  808. /// <param name="levelIds"></param>
  809. /// <returns></returns>
  810. private List<IPreviewBeatmapLevel> SortBeatSaverPlayCount(List<IPreviewBeatmapLevel> levelIds)
  811. {
  812. Logger.Info("Sorting song list by BeatSaver PlayCount");
  813. // Do not always have data when trying to sort by UpVotes
  814. if (!SongDataCore.Plugin.BeatSaver.IsDataAvailable())
  815. {
  816. return levelIds;
  817. }
  818. return levelIds
  819. .OrderByDescending(x => {
  820. var hash = CustomHelpers.GetSongHash(x.levelID);
  821. if (SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
  822. {
  823. return SongDataCore.Plugin.BeatSaver.Data.Songs[hash].stats.plays;
  824. }
  825. else
  826. {
  827. return int.MinValue;
  828. }
  829. })
  830. .ToList();
  831. }
  832. /// <summary>
  833. /// Sorting by BeatSaver rating stat.
  834. /// </summary>
  835. /// <param name="levelIds"></param>
  836. /// <returns></returns>
  837. private List<IPreviewBeatmapLevel> SortBeatSaverRating(List<IPreviewBeatmapLevel> levelIds)
  838. {
  839. Logger.Info("Sorting song list by BeatSaver Rating!");
  840. // Do not always have data when trying to sort by rating
  841. if (!SongDataCore.Plugin.BeatSaver.IsDataAvailable())
  842. {
  843. return levelIds;
  844. }
  845. return levelIds
  846. .OrderByDescending(x => {
  847. var hash = CustomHelpers.GetSongHash(x.levelID);
  848. if (SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
  849. {
  850. return SongDataCore.Plugin.BeatSaver.Data.Songs[hash].stats.rating;
  851. }
  852. else
  853. {
  854. return int.MinValue;
  855. }
  856. })
  857. .ToList();
  858. }
  859. /// <summary>
  860. /// Sorting by BeatSaver heat stat.
  861. /// </summary>
  862. /// <param name="levelIds"></param>
  863. /// <returns></returns>
  864. private List<IPreviewBeatmapLevel> SortBeatSaverHeat(List<IPreviewBeatmapLevel> levelIds)
  865. {
  866. Logger.Info("Sorting song list by BeatSaver Heat!");
  867. // Do not always have data when trying to sort by heat
  868. if (!SongDataCore.Plugin.BeatSaver.IsDataAvailable())
  869. {
  870. return levelIds;
  871. }
  872. return levelIds
  873. .OrderByDescending(x => {
  874. var hash = CustomHelpers.GetSongHash(x.levelID);
  875. if (SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
  876. {
  877. return SongDataCore.Plugin.BeatSaver.Data.Songs[hash].stats.heat;
  878. }
  879. else
  880. {
  881. return int.MinValue;
  882. }
  883. })
  884. .ToList();
  885. }
  886. }
  887. }