SongBrowserModel.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. using SongBrowserPlugin.DataAccess;
  2. using SongBrowserPlugin.UI;
  3. using SongLoaderPlugin;
  4. using SongLoaderPlugin.OverrideClasses;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.IO;
  9. using System.Linq;
  10. using UnityEngine;
  11. namespace SongBrowserPlugin
  12. {
  13. class FolderBeatMapData : BeatmapData
  14. {
  15. public FolderBeatMapData(BeatmapLineData[] beatmapLinesData, BeatmapEventData[] beatmapEventData) :
  16. base(beatmapLinesData, beatmapEventData)
  17. {
  18. }
  19. }
  20. class FolderBeatMapDataSO : BeatmapDataSO
  21. {
  22. public FolderBeatMapDataSO()
  23. {
  24. BeatmapLineData lineData = new BeatmapLineData();
  25. lineData.beatmapObjectsData = new BeatmapObjectData[0];
  26. this._beatmapData = new FolderBeatMapData(
  27. new BeatmapLineData[1]
  28. {
  29. lineData
  30. },
  31. new BeatmapEventData[1]
  32. {
  33. new BeatmapEventData(0, BeatmapEventType.Event0, 0)
  34. });
  35. }
  36. }
  37. class FolderLevel : StandardLevelSO
  38. {
  39. public void Init(String relativePath, String name, Sprite coverImage)
  40. {
  41. _songName = name;
  42. _songSubName = "";
  43. _songAuthorName = "Folder";
  44. _levelID = $"Folder_{relativePath}";
  45. var beatmapData = new FolderBeatMapDataSO();
  46. var difficultyBeatmaps = new List<CustomLevel.CustomDifficultyBeatmap>();
  47. var newDiffBeatmap = new CustomLevel.CustomDifficultyBeatmap(this, LevelDifficulty.Easy, 0, 0, beatmapData);
  48. difficultyBeatmaps.Add(newDiffBeatmap);
  49. var sceneInfo = Resources.Load<SceneInfo>("SceneInfo/" + "DefaultEnvironment" + "SceneInfo");
  50. this.InitFull(_levelID, _songName, _songSubName, _songAuthorName, SongLoaderPlugin.SongLoader.TemporaryAudioClip, 1, 1, 1, 1, 1, 1, 1, coverImage, difficultyBeatmaps.ToArray(), sceneInfo);
  51. this.InitData();
  52. }
  53. }
  54. class DirectoryNode
  55. {
  56. public string Key { get; private set; }
  57. public Dictionary<String, DirectoryNode> Nodes;
  58. public List<StandardLevelSO> Levels;
  59. public DirectoryNode(String key)
  60. {
  61. Key = key;
  62. Nodes = new Dictionary<string, DirectoryNode>();
  63. Levels = new List<StandardLevelSO>();
  64. }
  65. }
  66. public class SongBrowserModel
  67. {
  68. private const String CUSTOM_SONGS_DIR = "CustomSongs";
  69. private DateTime EPOCH = new DateTime(1970, 1, 1);
  70. private Logger _log = new Logger("SongBrowserModel");
  71. // song_browser_settings.xml
  72. private SongBrowserSettings _settings;
  73. // song list management
  74. private List<StandardLevelSO> _sortedSongs;
  75. private List<StandardLevelSO> _originalSongs;
  76. private Dictionary<String, SongLoaderPlugin.OverrideClasses.CustomLevel> _levelIdToCustomLevel;
  77. private SongLoaderPlugin.OverrideClasses.CustomLevelCollectionSO _gameplayModeCollection;
  78. private Dictionary<String, double> _cachedLastWriteTimes;
  79. private Dictionary<string, int> _weights;
  80. private Dictionary<String, DirectoryNode> _directoryTree;
  81. private Stack<DirectoryNode> _directoryStack = new Stack<DirectoryNode>();
  82. private GameplayMode _currentGamePlayMode;
  83. /// <summary>
  84. /// Toggle whether inverting the results.
  85. /// </summary>
  86. public bool InvertingResults { get; private set; }
  87. /// <summary>
  88. /// Get the settings the model is using.
  89. /// </summary>
  90. public SongBrowserSettings Settings
  91. {
  92. get
  93. {
  94. return _settings;
  95. }
  96. }
  97. /// <summary>
  98. /// Get the sorted song list for the current working directory.
  99. /// </summary>
  100. public List<StandardLevelSO> SortedSongList
  101. {
  102. get
  103. {
  104. return _sortedSongs;
  105. }
  106. }
  107. /// <summary>
  108. /// Map LevelID to Custom Level info.
  109. /// </summary>
  110. public Dictionary<String, SongLoaderPlugin.OverrideClasses.CustomLevel> LevelIdToCustomSongInfos
  111. {
  112. get
  113. {
  114. return _levelIdToCustomLevel;
  115. }
  116. }
  117. /// <summary>
  118. /// How deep is the directory stack.
  119. /// </summary>
  120. public int DirStackSize
  121. {
  122. get
  123. {
  124. return _directoryStack.Count;
  125. }
  126. }
  127. /// <summary>
  128. /// Get the last selected (stored in settings) level id.
  129. /// </summary>
  130. public String LastSelectedLevelId
  131. {
  132. get
  133. {
  134. return _settings.currentLevelId;
  135. }
  136. set
  137. {
  138. _settings.currentLevelId = value;
  139. _settings.Save();
  140. }
  141. }
  142. public String CurrentDirectory
  143. {
  144. get
  145. {
  146. return _settings.currentDirectory;
  147. }
  148. set
  149. {
  150. _settings.currentDirectory = value;
  151. _settings.Save();
  152. }
  153. }
  154. /// <summary>
  155. /// Constructor.
  156. /// </summary>
  157. public SongBrowserModel()
  158. {
  159. _cachedLastWriteTimes = new Dictionary<String, double>();
  160. // Weights used for keeping the original songs in order
  161. // Invert the weights from the game so we can order by descending and make LINQ work with us...
  162. /* Level4, Level2, Level9, Level5, Level10, Level6, Level7, Level1, Level3, Level8, Level11 */
  163. _weights = new Dictionary<string, int>
  164. {
  165. ["Level4"] = 11,
  166. ["Level2"] = 10,
  167. ["Level9"] = 9,
  168. ["Level5"] = 8,
  169. ["Level10"] = 7,
  170. ["Level6"] = 6,
  171. ["Level7"] = 5,
  172. ["Level1"] = 4,
  173. ["Level3"] = 3,
  174. ["Level8"] = 2,
  175. ["Level11"] = 1
  176. };
  177. }
  178. /// <summary>
  179. /// Init this model.
  180. /// </summary>
  181. /// <param name="songSelectionMasterView"></param>
  182. /// <param name="songListViewController"></param>
  183. public void Init()
  184. {
  185. _settings = SongBrowserSettings.Load();
  186. _log.Info("Settings loaded, sorting mode is: {0}", _settings.sortMode);
  187. }
  188. /// <summary>
  189. /// Easy invert of toggling.
  190. /// </summary>
  191. public void ToggleInverting()
  192. {
  193. this.InvertingResults = !this.InvertingResults;
  194. }
  195. /// <summary>
  196. /// Get the song cache from the game.
  197. /// TODO: This might not even be necessary anymore. Need to test interactions with BeatSaverDownloader.
  198. /// </summary>
  199. public void UpdateSongLists(GameplayMode gameplayMode)
  200. {
  201. _currentGamePlayMode = gameplayMode;
  202. String customSongsPath = Path.Combine(Environment.CurrentDirectory, CUSTOM_SONGS_DIR);
  203. String cachedSongsPath = Path.Combine(customSongsPath, ".cache");
  204. DateTime currentLastWriteTIme = File.GetLastWriteTimeUtc(customSongsPath);
  205. IEnumerable<string> directories = Directory.EnumerateDirectories(customSongsPath, "*.*", SearchOption.AllDirectories);
  206. // Get LastWriteTimes
  207. foreach (var level in SongLoader.CustomLevels)
  208. {
  209. // Flip slashes, match SongLoaderPlugin
  210. //string slashed_dir = dir.Replace("\\", "/");
  211. //_log.Debug("Fetching LastWriteTime for {0}", slashed_dir);
  212. _cachedLastWriteTimes[level.levelID] = (File.GetLastWriteTimeUtc(level.customSongInfo.path) - EPOCH).TotalMilliseconds;
  213. }
  214. // Update song Infos, directory tree, and sort
  215. this.UpdateSongInfos(_currentGamePlayMode);
  216. this.UpdateDirectoryTree(customSongsPath);
  217. this.ProcessSongList();
  218. }
  219. /// <summary>
  220. /// Get the song infos from SongLoaderPluging
  221. /// </summary>
  222. private void UpdateSongInfos(GameplayMode gameplayMode)
  223. {
  224. _log.Trace("UpdateSongInfos for Gameplay Mode {0}", gameplayMode);
  225. // Get the level collection from song loader
  226. SongLoaderPlugin.OverrideClasses.CustomLevelCollectionsForGameplayModes collections = SongLoaderPlugin.SongLoader.Instance.GetPrivateField<SongLoaderPlugin.OverrideClasses.CustomLevelCollectionsForGameplayModes>("_customLevelCollectionsForGameplayModes");
  227. _gameplayModeCollection = collections.GetCollection(gameplayMode) as SongLoaderPlugin.OverrideClasses.CustomLevelCollectionSO;
  228. _originalSongs = collections.GetLevels(gameplayMode).ToList();
  229. _sortedSongs = _originalSongs;
  230. _levelIdToCustomLevel = new Dictionary<string, SongLoaderPlugin.OverrideClasses.CustomLevel>();
  231. foreach (var level in SongLoader.CustomLevels)
  232. {
  233. if (!_levelIdToCustomLevel.Keys.Contains(level.levelID))
  234. _levelIdToCustomLevel.Add(level.levelID, level);
  235. }
  236. _log.Debug("Song Browser knows about {0} songs from SongLoader...", _sortedSongs.Count);
  237. }
  238. /// <summary>
  239. /// Make the directory tree.
  240. /// </summary>
  241. /// <param name="customSongsPath"></param>
  242. private void UpdateDirectoryTree(String customSongsPath)
  243. {
  244. // Determine folder mapping
  245. Uri customSongDirUri = new Uri(customSongsPath);
  246. _directoryTree = new Dictionary<string, DirectoryNode>();
  247. _directoryTree[CUSTOM_SONGS_DIR] = new DirectoryNode(CUSTOM_SONGS_DIR);
  248. foreach (StandardLevelSO level in _originalSongs)
  249. {
  250. AddItemToDirectoryTree(customSongDirUri, level);
  251. }
  252. // Determine starting location
  253. if (_directoryStack.Count < 1)
  254. {
  255. DirectoryNode currentNode = _directoryTree[CUSTOM_SONGS_DIR];
  256. _directoryStack.Push(currentNode);
  257. // Try to navigate directory path
  258. if (!String.IsNullOrEmpty(this.CurrentDirectory))
  259. {
  260. String[] paths = this.CurrentDirectory.Split('/');
  261. for (int i = 1; i < paths.Length; i++)
  262. {
  263. if (currentNode.Nodes.ContainsKey(paths[i]))
  264. {
  265. currentNode = currentNode.Nodes[paths[i]];
  266. _directoryStack.Push(currentNode);
  267. }
  268. }
  269. }
  270. }
  271. PrintDirectory(_directoryTree[CUSTOM_SONGS_DIR], 1);
  272. }
  273. /// <summary>
  274. /// Add a song to directory tree. Determine its place in the tree by walking the split directory path.
  275. /// </summary>
  276. /// <param name="customSongDirUri"></param>
  277. /// <param name="level"></param>
  278. private void AddItemToDirectoryTree(Uri customSongDirUri, StandardLevelSO level)
  279. {
  280. //_log.Debug("Processing item into directory tree: {0}", level.levelID);
  281. DirectoryNode currentNode = _directoryTree[CUSTOM_SONGS_DIR];
  282. // Just add original songs to root and bail
  283. if (level.levelID.Length < 32)
  284. {
  285. currentNode.Levels.Add(level);
  286. return;
  287. }
  288. CustomSongInfo songInfo = _levelIdToCustomLevel[level.levelID].customSongInfo;
  289. Uri customSongUri = new Uri(songInfo.path);
  290. Uri pathDiff = customSongDirUri.MakeRelativeUri(customSongUri);
  291. string relPath = Uri.UnescapeDataString(pathDiff.OriginalString);
  292. string[] paths = relPath.Split('/');
  293. Sprite folderIcon = Base64Sprites.Base64ToSprite(Base64Sprites.Folder);
  294. // Prevent cache directory from building into the tree, will add all its leafs to root.
  295. bool isCache = false;
  296. if (paths.Length > 2)
  297. {
  298. isCache = paths[1].Contains(".cache");
  299. }
  300. for (int i = 1; i < paths.Length; i++)
  301. {
  302. string path = paths[i];
  303. if (path == Path.GetFileName(songInfo.path))
  304. {
  305. //_log.Debug("\tLevel Found Adding {0}->{1}", currentNode.Key, level.levelID);
  306. currentNode.Levels.Add(level);
  307. break;
  308. }
  309. else if (currentNode.Nodes.ContainsKey(path))
  310. {
  311. currentNode = currentNode.Nodes[path];
  312. }
  313. else if (!isCache)
  314. {
  315. currentNode.Nodes[path] = new DirectoryNode(path);
  316. FolderLevel folderLevel = new FolderLevel();
  317. folderLevel.Init(relPath, path, folderIcon);
  318. //_log.Debug("Adding folder level {0}->{1}", currentNode.Key, path);
  319. currentNode.Levels.Add(folderLevel);
  320. _cachedLastWriteTimes[folderLevel.levelID] = (File.GetLastWriteTimeUtc(relPath) - EPOCH).TotalMilliseconds;
  321. currentNode = currentNode.Nodes[path];
  322. }
  323. }
  324. }
  325. /// <summary>
  326. /// Push a dir onto the stack.
  327. /// </summary>
  328. public void PushDirectory(IStandardLevel level)
  329. {
  330. DirectoryNode currentNode = _directoryStack.Peek();
  331. _log.Debug("Pushing directory {0}", level.songName);
  332. if (!currentNode.Nodes.ContainsKey(level.songName))
  333. {
  334. _log.Debug("Trying to push a directory that doesn't exist at this level.");
  335. return;
  336. }
  337. _directoryStack.Push(currentNode.Nodes[level.songName]);
  338. this.CurrentDirectory = level.levelID;
  339. ProcessSongList();
  340. }
  341. /// <summary>
  342. /// Pop a dir off the stack.
  343. /// </summary>
  344. public void PopDirectory()
  345. {
  346. if (_directoryStack.Count > 1)
  347. {
  348. _directoryStack.Pop();
  349. String currentDir = "";
  350. foreach (DirectoryNode node in _directoryStack)
  351. {
  352. currentDir = node.Key + "/" + currentDir;
  353. }
  354. this.CurrentDirectory = "Folder_" + currentDir;
  355. ProcessSongList();
  356. }
  357. }
  358. /// <summary>
  359. /// Print the directory structure parsed.
  360. /// </summary>
  361. /// <param name="node"></param>
  362. /// <param name="depth"></param>
  363. private void PrintDirectory(DirectoryNode node, int depth)
  364. {
  365. String levelStr = "";
  366. String nodeStr = "";
  367. Console.WriteLine("Dir: {0}".PadLeft(depth*4, ' '), node.Key);
  368. node.Levels.ForEach(x => Console.WriteLine("{0}".PadLeft((depth + 1)*4, ' '), x.levelID));
  369. foreach (KeyValuePair<string, DirectoryNode> childNode in node.Nodes)
  370. {
  371. PrintDirectory(childNode.Value, depth + 1);
  372. }
  373. }
  374. /// <summary>
  375. /// Sort the song list based on the settings.
  376. /// </summary>
  377. private void ProcessSongList()
  378. {
  379. _log.Trace("ProcessSongList()");
  380. // This has come in handy many times for debugging issues with Newest.
  381. /*foreach (StandardLevelSO level in _originalSongs)
  382. {
  383. if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  384. {
  385. _log.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
  386. }
  387. else
  388. {
  389. _log.Debug("Missing KEY: {0}", level.levelID);
  390. }
  391. }*/
  392. Stopwatch stopwatch = Stopwatch.StartNew();
  393. _log.Debug("Showing songs for directory: {0}", _directoryStack.Peek().Key);
  394. List<StandardLevelSO> songList = _directoryStack.Peek().Levels;
  395. switch (_settings.sortMode)
  396. {
  397. case SongSortMode.Favorites:
  398. SortFavorites(songList);
  399. break;
  400. case SongSortMode.Original:
  401. SortOriginal(songList);
  402. break;
  403. case SongSortMode.Newest:
  404. SortNewest(songList);
  405. break;
  406. case SongSortMode.Author:
  407. SortAuthor(songList);
  408. break;
  409. case SongSortMode.PlayCount:
  410. SortPlayCount(songList, _currentGamePlayMode);
  411. break;
  412. case SongSortMode.Random:
  413. SortRandom(songList);
  414. break;
  415. case SongSortMode.Search:
  416. SortSearch(songList);
  417. break;
  418. case SongSortMode.Default:
  419. default:
  420. SortSongName(songList);
  421. break;
  422. }
  423. if (this.InvertingResults && _settings.sortMode != SongSortMode.Random)
  424. {
  425. _sortedSongs.Reverse();
  426. }
  427. stopwatch.Stop();
  428. _log.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
  429. }
  430. private void SortFavorites(List<StandardLevelSO> levels)
  431. {
  432. _log.Info("Sorting song list as favorites");
  433. _sortedSongs = levels
  434. .AsQueryable()
  435. .OrderBy(x => _settings.favorites.Contains(x.levelID) == false)
  436. .ThenBy(x => x.songName)
  437. .ThenBy(x => x.songAuthorName)
  438. .ToList();
  439. }
  440. private void SortOriginal(List<StandardLevelSO> levels)
  441. {
  442. _log.Info("Sorting song list as original");
  443. _sortedSongs = levels
  444. .AsQueryable()
  445. .OrderByDescending(x => _weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0)
  446. .ThenBy(x => x.songName)
  447. .ToList();
  448. }
  449. private void SortNewest(List<StandardLevelSO> levels)
  450. {
  451. _log.Info("Sorting song list as newest.");
  452. _sortedSongs = levels
  453. .AsQueryable()
  454. .OrderBy(x => _weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0)
  455. .ThenByDescending(x => x.levelID.StartsWith("Level") ? _weights[x.levelID] : _cachedLastWriteTimes[x.levelID])
  456. .ToList();
  457. }
  458. private void SortAuthor(List<StandardLevelSO> levels)
  459. {
  460. _log.Info("Sorting song list by author");
  461. _sortedSongs = levels
  462. .AsQueryable()
  463. .OrderBy(x => x.songAuthorName)
  464. .ThenBy(x => x.songName)
  465. .ToList();
  466. }
  467. private void SortPlayCount(List<StandardLevelSO> levels, GameplayMode gameplayMode)
  468. {
  469. _log.Info("Sorting song list by playcount");
  470. // Build a map of levelId to sum of all playcounts and sort.
  471. PlayerDynamicData playerData = GameDataModel.instance.gameDynamicData.GetCurrentPlayerDynamicData();
  472. IEnumerable<LevelDifficulty> difficultyIterator = Enum.GetValues(typeof(LevelDifficulty)).Cast<LevelDifficulty>();
  473. Dictionary<string, int> levelIdToPlayCount = new Dictionary<string, int>();
  474. foreach (var level in levels)
  475. {
  476. if (!levelIdToPlayCount.ContainsKey(level.levelID))
  477. {
  478. // Skip folders
  479. if (level.levelID.StartsWith("Folder_"))
  480. {
  481. levelIdToPlayCount.Add(level.levelID, 0);
  482. }
  483. else
  484. {
  485. int playCountSum = 0;
  486. foreach (LevelDifficulty difficulty in difficultyIterator)
  487. {
  488. PlayerLevelStatsData stats = playerData.GetPlayerLevelStatsData(level.levelID, difficulty, gameplayMode);
  489. playCountSum += stats.playCount;
  490. }
  491. levelIdToPlayCount.Add(level.levelID, playCountSum);
  492. }
  493. }
  494. }
  495. _sortedSongs = levels
  496. .AsQueryable()
  497. .OrderByDescending(x => levelIdToPlayCount[x.levelID])
  498. .ThenBy(x => x.songName)
  499. .ToList();
  500. }
  501. private void SortRandom(List<StandardLevelSO> levels)
  502. {
  503. _log.Info("Sorting song list by random");
  504. System.Random rnd = new System.Random(Guid.NewGuid().GetHashCode());
  505. _sortedSongs = levels
  506. .AsQueryable()
  507. .OrderBy(x => rnd.Next())
  508. .ToList();
  509. }
  510. private void SortSearch(List<StandardLevelSO> levels)
  511. {
  512. // Make sure we can actually search.
  513. if (this._settings.searchTerms.Count <= 0)
  514. {
  515. _log.Error("Tried to search for a song with no valid search terms...");
  516. SortSongName(levels);
  517. return;
  518. }
  519. string searchTerm = this._settings.searchTerms[0];
  520. if (String.IsNullOrEmpty(searchTerm))
  521. {
  522. _log.Error("Empty search term entered.");
  523. SortSongName(levels);
  524. return;
  525. }
  526. _log.Info("Sorting song list by search term: {0}", searchTerm);
  527. //_originalSongs.ForEach(x => _log.Debug($"{x.songName} {x.songSubName} {x.songAuthorName}".ToLower().Contains(searchTerm.ToLower()).ToString()));
  528. _sortedSongs = levels
  529. .AsQueryable()
  530. .Where(x => $"{x.songName} {x.songSubName} {x.songAuthorName}".ToLower().Contains(searchTerm.ToLower()))
  531. .ToList();
  532. //_sortedSongs.ForEach(x => _log.Debug(x.levelID));
  533. }
  534. private void SortSongName(List<StandardLevelSO> levels)
  535. {
  536. _log.Info("Sorting song list as default (songName)");
  537. _sortedSongs = levels
  538. .AsQueryable()
  539. .OrderBy(x => x.songName)
  540. .ThenBy(x => x.songAuthorName)
  541. .ToList();
  542. }
  543. }
  544. }