Loader.cs 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250
  1. using IPA.Utilities;
  2. using SongCore.Data;
  3. using SongCore.OverrideClasses;
  4. using SongCore.Utilities;
  5. using System;
  6. using System.Collections.Concurrent;
  7. using System.Collections.Generic;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Linq;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. using UnityEngine;
  15. using UnityEngine.SceneManagement;
  16. using static SongCore.Arcache.ArcacheLoader;
  17. using LogSeverity = IPA.Logging.Logger.Level;
  18. namespace SongCore
  19. {
  20. public class Loader : MonoBehaviour
  21. {
  22. static Loader()
  23. {
  24. InitFastLoad();
  25. }
  26. // Actions for loading and refreshing beatmaps
  27. public static event Action<Loader> LoadingStartedEvent;
  28. public static event Action<Loader, ConcurrentDictionary<string, CustomPreviewBeatmapLevel>> SongsLoadedEvent;
  29. public static event Action OnLevelPacksRefreshed;
  30. public static event Action DeletingSong;
  31. public static ConcurrentDictionary<string, CustomPreviewBeatmapLevel> CustomLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>();
  32. public static ConcurrentDictionary<string, CustomPreviewBeatmapLevel> CustomWIPLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>();
  33. public static ConcurrentDictionary<string, CustomPreviewBeatmapLevel> CachedWIPLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>();
  34. public static List<SeperateSongFolder> SeperateSongFolders = new List<SeperateSongFolder>();
  35. public static SongCoreCustomLevelCollection CustomLevelsCollection { get; private set; }
  36. public static SongCoreCustomLevelCollection WIPLevelsCollection { get; private set; }
  37. public static SongCoreCustomLevelCollection CachedWIPLevelCollection { get; private set; }
  38. public static SongCoreCustomBeatmapLevelPack CustomLevelsPack { get; private set; }
  39. public static SongCoreCustomBeatmapLevelPack WIPLevelsPack { get; private set; }
  40. public static SongCoreCustomBeatmapLevelPack CachedWIPLevelsPack { get; private set; }
  41. public static SongCoreBeatmapLevelPackCollectionSO CustomBeatmapLevelPackCollectionSO { get; private set; }
  42. private static readonly ConcurrentDictionary<string, OfficialSongEntry> OfficialSongs = new ConcurrentDictionary<string, OfficialSongEntry>();
  43. private static readonly ConcurrentDictionary<string, CustomPreviewBeatmapLevel> CustomLevelsById =
  44. new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>();
  45. public static bool AreSongsLoaded { get; private set; }
  46. public static bool AreSongsLoading { get; private set; }
  47. public static float LoadingProgress { get; internal set; }
  48. internal ProgressBar _progressBar;
  49. private HMTask _loadingTask;
  50. private bool _loadingCancelled;
  51. private static CustomLevelLoader _customLevelLoader;
  52. public static BeatmapLevelsModel BeatmapLevelsModelSO
  53. {
  54. get
  55. {
  56. if (_beatmapLevelsModel == null) _beatmapLevelsModel = Resources.FindObjectsOfTypeAll<BeatmapLevelsModel>().FirstOrDefault();
  57. return _beatmapLevelsModel;
  58. }
  59. }
  60. internal static BeatmapLevelsModel _beatmapLevelsModel;
  61. public static Sprite defaultCoverImage;
  62. public static CachedMediaAsyncLoader cachedMediaAsyncLoaderSO { get; private set; }
  63. public static BeatmapCharacteristicCollectionSO beatmapCharacteristicCollection { get; private set; }
  64. public static Loader Instance;
  65. public static void OnLoad()
  66. {
  67. if (Instance != null)
  68. {
  69. _beatmapLevelsModel = null;
  70. Instance.RefreshLevelPacks();
  71. return;
  72. }
  73. new GameObject("SongCore Loader").AddComponent<Loader>();
  74. }
  75. private void Awake()
  76. {
  77. Instance = this;
  78. _progressBar = ProgressBar.Create();
  79. MenuLoaded();
  80. Hashing.ReadCachedSongHashes();
  81. Hashing.ReadCachedAudioData();
  82. DontDestroyOnLoad(gameObject);
  83. BS_Utils.Utilities.BSEvents.menuSceneLoaded += MenuLoaded;
  84. Initialize();
  85. }
  86. private void Initialize()
  87. {
  88. if (Directory.Exists(Converter.oldFolderPath))
  89. Converter.PrepareExistingLibrary();
  90. else
  91. RefreshSongs();
  92. }
  93. internal void MenuLoaded()
  94. {
  95. if (AreSongsLoading)
  96. {
  97. //Scene changing while songs are loading. Since we are using a separate thread while loading, this is bad and could cause a crash.
  98. //So we have to stop loading.
  99. if (_loadingTask != null)
  100. {
  101. _loadingTask.Cancel();
  102. _loadingCancelled = true;
  103. AreSongsLoading = false;
  104. LoadingProgress = 0;
  105. StopAllCoroutines();
  106. _progressBar.ShowMessage("Loading cancelled\n<size=80%>Press Ctrl+R to refresh</size>");
  107. Logging.Log("Loading was cancelled by player since they loaded another scene.");
  108. }
  109. }
  110. BS_Utils.Gameplay.Gamemode.Init();
  111. if (_customLevelLoader == null)
  112. {
  113. _customLevelLoader = Resources.FindObjectsOfTypeAll<CustomLevelLoader>().FirstOrDefault();
  114. if (_customLevelLoader)
  115. {
  116. defaultCoverImage = _customLevelLoader.GetField<Sprite, CustomLevelLoader>("_defaultPackCover");
  117. cachedMediaAsyncLoaderSO = _customLevelLoader.GetField<CachedMediaAsyncLoader, CustomLevelLoader>("_cachedMediaAsyncLoader");
  118. beatmapCharacteristicCollection = _customLevelLoader.GetField<BeatmapCharacteristicCollectionSO, CustomLevelLoader>("_beatmapCharacteristicCollection");
  119. }
  120. else
  121. {
  122. var defaultCoverTex = Texture2D.blackTexture;
  123. defaultCoverImage = Sprite.Create(defaultCoverTex, new Rect(0f, 0f,
  124. defaultCoverTex.width, defaultCoverTex.height), new Vector2(0.5f, 0.5f));
  125. }
  126. }
  127. }
  128. /// <summary>
  129. /// This fuction will add/remove Level Packs from the Custom Levels tab if applicable
  130. /// </summary>
  131. public void RefreshLevelPacks()
  132. {
  133. CustomLevelsCollection?.UpdatePreviewLevels(CustomLevels?.Values?.OrderBy(l => l.songName).ToArray());
  134. WIPLevelsCollection?.UpdatePreviewLevels(CustomWIPLevels?.Values?.OrderBy(l => l.songName).ToArray());
  135. CachedWIPLevelCollection?.UpdatePreviewLevels(CachedWIPLevels?.Values?.OrderBy(l => l.songName).ToArray());
  136. if (CachedWIPLevelsPack != null)
  137. {
  138. if (CachedWIPLevels.Count > 0 && !CustomBeatmapLevelPackCollectionSO._customBeatmapLevelPacks.Contains(CachedWIPLevelsPack))
  139. {
  140. CustomBeatmapLevelPackCollectionSO.AddLevelPack(CachedWIPLevelsPack);
  141. }
  142. else if (CachedWIPLevels.Count == 0 && CustomBeatmapLevelPackCollectionSO._customBeatmapLevelPacks.Contains(CachedWIPLevelsPack))
  143. {
  144. CustomBeatmapLevelPackCollectionSO.RemoveLevelPack(CachedWIPLevelsPack);
  145. }
  146. }
  147. foreach (var folderEntry in SeperateSongFolders)
  148. {
  149. if (folderEntry.SongFolderEntry.Pack == FolderLevelPack.NewPack)
  150. {
  151. folderEntry.LevelCollection.UpdatePreviewLevels(folderEntry.Levels.Values.OrderBy(l => l.songName).ToArray());
  152. if (folderEntry.Levels.Count > 0 || (folderEntry is ModSeperateSongFolder && (folderEntry as ModSeperateSongFolder).AlwaysShow))
  153. {
  154. if (!CustomBeatmapLevelPackCollectionSO._customBeatmapLevelPacks.Contains(folderEntry.LevelPack))
  155. CustomBeatmapLevelPackCollectionSO.AddLevelPack(folderEntry.LevelPack);
  156. }
  157. // else if (CustomBeatmapLevelPackCollectionSO._customBeatmapLevelPacks.Contains(folderEntry.LevelPack))
  158. // CustomBeatmapLevelPackCollectionSO._customBeatmapLevelPacks.Remove(folderEntry.LevelPack);
  159. }
  160. }
  161. BeatmapLevelsModelSO.SetField<BeatmapLevelsModel, IBeatmapLevelPackCollection>("_customLevelPackCollection", CustomBeatmapLevelPackCollectionSO as IBeatmapLevelPackCollection);
  162. BeatmapLevelsModelSO.UpdateAllLoadedBeatmapLevelPacks();
  163. BeatmapLevelsModelSO.UpdateLoadedPreviewLevels();
  164. var filterNav = Resources.FindObjectsOfTypeAll<LevelFilteringNavigationController>().FirstOrDefault();
  165. // filterNav.InitPlaylists();
  166. // filterNav.UpdatePlaylistsData();
  167. if (filterNav.isActiveAndEnabled)
  168. filterNav?.UpdateCustomSongs();
  169. // AttemptReselectCurrentLevelPack(filterNav);
  170. OnLevelPacksRefreshed?.Invoke();
  171. }
  172. internal void AttemptReselectCurrentLevelPack(LevelFilteringNavigationController controller)
  173. {
  174. /*
  175. var collectionview = Resources.FindObjectsOfTypeAll<LevelCollectionViewController>().FirstOrDefault();
  176. var levelflow = Resources.FindObjectsOfTypeAll<LevelSelectionFlowCoordinator>().FirstOrDefault();
  177. var pack = levelflow.GetProperty<IBeatmapLevelPack>("selectedBeatmapLevelPack");
  178. IBeatmapLevelPack[] sectionpacks = new IBeatmapLevelPack[0];
  179. var selectedcategory = levelflow.GetProperty<SelectLevelCategoryViewController.LevelCategory>("selectedLevelCategory");
  180. switch (selectedcategory)
  181. {
  182. case SelectLevelCategoryViewController.LevelCategory.OstAndExtras:
  183. sectionpacks = controller.GetField<IBeatmapLevelPack[]>("_ostBeatmapLevelPacks");
  184. break;
  185. case SelectLevelCategoryViewController.LevelCategory.MusicPacks:
  186. sectionpacks = controller.GetField<IBeatmapLevelPack[]>("_musicPacksBeatmapLevelPacks");
  187. break;
  188. case SelectLevelCategoryViewController.LevelCategory.CustomSongs:
  189. sectionpacks = controller.GetField<IBeatmapLevelPack[]>("_customLevelPacks");
  190. break;
  191. case SelectLevelCategoryViewController.LevelCategory.All:
  192. sectionpacks = controller.GetField<IBeatmapLevelPack[]>("_allBeatmapLevelPacks");
  193. break;
  194. case SelectLevelCategoryViewController.LevelCategory.Favorites:
  195. return;
  196. }
  197. if (!sectionpacks.ToList().Contains(pack))
  198. pack = sectionpacks.FirstOrDefault();
  199. if (pack == null) return;
  200. controller.Setup(SongPackMask.all, pack, selectedcategory, false, true);
  201. */
  202. //controller.SelectAnnotatedBeatmapLevelCollection(pack);
  203. // collectionview.SetData(pack.beatmapLevelCollection, pack.packName, pack.coverImage, false, controller.GetField<GameObject>("_currentNoDataInfoPrefab"));
  204. }
  205. public void RefreshSongs(bool fullRefresh = true)
  206. {
  207. if (SceneManager.GetActiveScene().name == "GameCore") return;
  208. if (AreSongsLoading) return;
  209. Logging.Log(fullRefresh ? "Starting full song refresh" : "Starting song refresh");
  210. AreSongsLoaded = false;
  211. AreSongsLoading = true;
  212. LoadingProgress = 0;
  213. _loadingCancelled = false;
  214. if (LoadingStartedEvent != null)
  215. {
  216. try
  217. {
  218. LoadingStartedEvent(this);
  219. }
  220. catch (Exception e)
  221. {
  222. Logging.Log("Some plugin is throwing exception from the LoadingStartedEvent!", IPA.Logging.Logger.Level.Error);
  223. Logging.Log(e.ToString(), IPA.Logging.Logger.Level.Error);
  224. }
  225. }
  226. RetrieveAllSongs(fullRefresh);
  227. }
  228. private void RetrieveAllSongs(bool fullRefresh)
  229. {
  230. var stopwatch = new Stopwatch();
  231. #region ClearAllDictionaries
  232. // Clear all beatmap dictionaries on full refresh
  233. if (fullRefresh)
  234. {
  235. CustomBeatmapLevelPackCollectionSO = null;
  236. CustomLevels.Clear();
  237. CustomWIPLevels.Clear();
  238. CachedWIPLevels.Clear();
  239. Collections.levelHashDictionary.Clear();
  240. Collections.hashLevelDictionary.Clear();
  241. foreach (var folder in SeperateSongFolders) folder.Levels.Clear();
  242. }
  243. #endregion ClearAllDictionaries
  244. var foundSongPaths = fullRefresh
  245. ? new ConcurrentDictionary<string, bool>()
  246. : new ConcurrentDictionary<string, bool>(Hashing.cachedSongHashData.Keys.ToDictionary(x => x, _ => false));
  247. var baseProjectPath = CustomLevelPathHelper.baseProjectPath;
  248. var customLevelsPath = CustomLevelPathHelper.customLevelsDirectoryPath;
  249. Action job = delegate
  250. {
  251. #region AddOfficialBeatmaps
  252. try
  253. {
  254. void AddOfficialPackCollection(IBeatmapLevelPackCollection packCollection)
  255. {
  256. foreach (var pack in packCollection.beatmapLevelPacks)
  257. {
  258. foreach (var level in pack.beatmapLevelCollection.beatmapLevels)
  259. {
  260. OfficialSongs[level.levelID] = new OfficialSongEntry()
  261. {
  262. LevelPackCollection = packCollection,
  263. LevelPack = pack,
  264. PreviewBeatmapLevel = level
  265. };
  266. }
  267. }
  268. }
  269. OfficialSongs.Clear();
  270. AddOfficialPackCollection(BeatmapLevelsModelSO.ostAndExtrasPackCollection);
  271. AddOfficialPackCollection(BeatmapLevelsModelSO.dlcBeatmapLevelPackCollection);
  272. }
  273. catch (Exception ex)
  274. {
  275. Logging.logger.Error($"Error populating official songs: {ex.Message}");
  276. Logging.logger.Debug(ex);
  277. }
  278. #endregion AddOfficialBeatmaps
  279. #region AddCustomBeatmaps
  280. try
  281. {
  282. #region DirectorySetup
  283. var path = CustomLevelPathHelper.baseProjectPath;
  284. path = path.Replace('\\', '/');
  285. if (!Directory.Exists(customLevelsPath))
  286. {
  287. Directory.CreateDirectory(customLevelsPath);
  288. }
  289. if (!Directory.Exists(baseProjectPath + "/CustomWIPLevels"))
  290. {
  291. Directory.CreateDirectory(baseProjectPath + "/CustomWIPLevels");
  292. }
  293. #endregion DirectorySetup
  294. #region CacheZipWIPs
  295. // Get zip files in CustomWIPLevels and extract them to Cache folder
  296. if (fullRefresh)
  297. {
  298. try
  299. {
  300. var wipPath = Path.Combine(path, "CustomWIPLevels");
  301. var cachePath = Path.Combine(path, "CustomWIPLevels", "Cache");
  302. CacheZIPs(cachePath, wipPath);
  303. var cacheFolders = Directory.GetDirectories(cachePath).ToArray();
  304. LoadCachedZIPs(cacheFolders, fullRefresh, CachedWIPLevels);
  305. }
  306. catch (Exception ex)
  307. {
  308. Logging.logger.Error("Failed To Load Cached WIP Levels: " + ex);
  309. }
  310. }
  311. #endregion CacheZipWIPs
  312. #region CacheSeperateZIPs
  313. if (fullRefresh)
  314. {
  315. foreach (var songFolder in SeperateSongFolders)
  316. {
  317. if (songFolder.SongFolderEntry.CacheZIPs && songFolder.CacheFolder != null)
  318. {
  319. var cacheFolder = songFolder.CacheFolder;
  320. try
  321. {
  322. CacheZIPs(cacheFolder.SongFolderEntry.Path, songFolder.SongFolderEntry.Path);
  323. }
  324. catch (Exception ex)
  325. {
  326. Logging.logger.Error("Failed To Load Cached WIP Levels: " + ex);
  327. }
  328. }
  329. }
  330. }
  331. #endregion CacheSeperateZIPs
  332. //Thread.Sleep(1000 * 1); // waiting for ui, dont hang up
  333. stopwatch.Start();
  334. #region LoadCustomLevels
  335. // Get Levels from CustomLevels and CustomWIPLevels folders
  336. var songFolders = ExtractCustomLevelFoldersFromArchive().Concat(Directory.GetDirectories(Path.Combine(path, "CustomWIPLevels"))).ToArray();
  337. var loadedData = new ConcurrentBag<string>();
  338. var processedSongsCount = 0;
  339. Parallel.ForEach(songFolders, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount - 1 }, (folder) =>
  340. {
  341. var result = Path.Combine(folder, "info.dat");
  342. if (ArchiveExists(result) == false)
  343. {
  344. Logging.Log("Folder: '" + folder + "' is missing info.dat files!", LogSeverity.Notice);
  345. return;
  346. }
  347. //string[] results;
  348. //try
  349. //{
  350. // results = Directory.GetFiles(folder, "info.dat", SearchOption.TopDirectoryOnly);
  351. //}
  352. //catch (DirectoryNotFoundException ex)
  353. //{
  354. // Logging.Log($"Skipping missing or corrupt folder: '{folder}'", LogSeverity.Warning);
  355. // return;
  356. //}
  357. //if (results.Length == 0)
  358. //{
  359. // Logging.Log("Folder: '" + folder + "' is missing info.dat files!", LogSeverity.Notice);
  360. // return;
  361. //}
  362. //foreach (var result in results)
  363. {
  364. try
  365. {
  366. var songPath = Path.GetDirectoryName(result.Replace('\\', '/'));
  367. if (ExtractParentDirectoryName(songPath) != "Backups")
  368. {
  369. if (!fullRefresh)
  370. {
  371. if (CustomLevels.TryGetValue(songPath, out var c))
  372. {
  373. if (c != null)
  374. {
  375. loadedData.Add(c.levelID);
  376. goto jumpOutLoop;
  377. }
  378. }
  379. }
  380. var wip = songPath.Contains("CustomWIPLevels");
  381. var saveData = GetStandardLevelInfoSaveData(songPath);
  382. if (saveData != null)
  383. {
  384. if (_loadingCancelled) return;
  385. var level = LoadSongAndAddToDictionaries(saveData, songPath);
  386. if (level != null)
  387. {
  388. if (!wip)
  389. {
  390. CustomLevelsById[level.levelID] = level;
  391. CustomLevels[songPath] = level;
  392. }
  393. else
  394. CustomWIPLevels[songPath] = level;
  395. foundSongPaths.TryAdd(songPath, false);
  396. }
  397. }
  398. // if (loadedData.Any(x => x == saveData.))
  399. // {
  400. // Logging.Log("Duplicate song found at " + songPath, LogSeverity.Notice);
  401. // continue;
  402. // }
  403. // loadedData.Add(saveDat);
  404. //HMMainThreadDispatcher.instance.Enqueue(delegate
  405. //{
  406. }
  407. }
  408. catch (Exception e)
  409. {
  410. Logging.Log("Failed to load song folder: " + result, LogSeverity.Error);
  411. Logging.Log(e.ToString(), LogSeverity.Error);
  412. }
  413. }
  414. jumpOutLoop:
  415. LoadingProgress = (float)Interlocked.Increment(ref processedSongsCount) / songFolders.Length;
  416. });
  417. #endregion LoadCustomLevels
  418. #region LoadSeperateFolders
  419. // Load beatmaps in Seperate Song Folders (created in folders.xml or by other mods)
  420. // Assign beatmaps to their respective pack (custom levels, wip levels, or seperate)
  421. for (var k = 0; k < SeperateSongFolders.Count; k++)
  422. {
  423. try
  424. {
  425. var entry = SeperateSongFolders[k];
  426. Instance._progressBar.ShowMessage("Loading " + (SeperateSongFolders.Count - k) + " Additional Song folders");
  427. if (!Directory.Exists(entry.SongFolderEntry.Path)) continue;
  428. var entryFolders = Directory.GetDirectories(entry.SongFolderEntry.Path).ToList();
  429. float i2 = 0;
  430. foreach (var folder in entryFolders)
  431. {
  432. i2++;
  433. // Search for an info.dat in the beatmap folder
  434. string[] results;
  435. try
  436. {
  437. results = Directory.GetFiles(folder, "info.dat", SearchOption.TopDirectoryOnly);
  438. }
  439. catch (DirectoryNotFoundException ex)
  440. {
  441. Logging.Log($"Skipping missing or corrupt folder: '{folder}'", LogSeverity.Warning);
  442. continue;
  443. }
  444. if (results.Length == 0)
  445. {
  446. Logging.Log("Folder: '" + folder + "' is missing info.dat files!", LogSeverity.Notice);
  447. continue;
  448. }
  449. foreach (var result in results)
  450. {
  451. try
  452. {
  453. // On quick refresh: Check if the beatmap directory is already present in the respective beatmap dictionary
  454. // If it is already present on a non full refresh, it will be ignored (changes to the beatmap will not be applied)
  455. var songPath = Path.GetDirectoryName(result.Replace('\\', '/'));
  456. if (!fullRefresh)
  457. {
  458. if (entry.SongFolderEntry.Pack == FolderLevelPack.NewPack && SearchBeatmapInMapPack(entry.Levels, songPath)) continue;
  459. else if (entry.SongFolderEntry.Pack == FolderLevelPack.CustomLevels && SearchBeatmapInMapPack(CustomLevels, songPath)) continue;
  460. else if (entry.SongFolderEntry.Pack == FolderLevelPack.CustomWIPLevels && SearchBeatmapInMapPack(CustomWIPLevels, songPath)) continue;
  461. else if (entry.SongFolderEntry.Pack == FolderLevelPack.CachedWIPLevels && SearchBeatmapInMapPack(CachedWIPLevels, songPath)) continue;
  462. }
  463. if (entry.SongFolderEntry.Pack == FolderLevelPack.CustomLevels || (entry.SongFolderEntry.Pack == FolderLevelPack.NewPack && entry.SongFolderEntry.WIP == false))
  464. {
  465. if (AssignBeatmapToSeperateFolder(CustomLevels, songPath, entry.Levels)) continue;
  466. if (AssignBeatmapToSeperateFolder(CustomWIPLevels, songPath, entry.Levels)) continue;
  467. if (AssignBeatmapToSeperateFolder(CachedWIPLevels, songPath, entry.Levels)) continue;
  468. }
  469. var saveData = GetStandardLevelInfoSaveData(songPath);
  470. if (saveData == null)
  471. {
  472. // Logging.Log("Null save data", LogSeverity.Notice);
  473. continue;
  474. }
  475. var count = i2;
  476. //HMMainThreadDispatcher.instance.Enqueue(delegate
  477. //{
  478. if (_loadingCancelled) return;
  479. var level = LoadSongAndAddToDictionaries(saveData, songPath, entry.SongFolderEntry);
  480. if (level != null)
  481. {
  482. entry.Levels[songPath] = level;
  483. CustomLevelsById[level.levelID] = level;
  484. foundSongPaths.TryAdd(songPath, false);
  485. }
  486. LoadingProgress = count / entryFolders.Count;
  487. //});
  488. }
  489. catch (Exception e)
  490. {
  491. Logging.Log("Failed to load song folder: " + result, LogSeverity.Error);
  492. Logging.Log(e.ToString(), LogSeverity.Error);
  493. }
  494. }
  495. }
  496. }
  497. catch (Exception ex)
  498. {
  499. Logging.Log($"Failed to load Seperate Folder{SeperateSongFolders[k].SongFolderEntry.Name}" + ex, LogSeverity.Error);
  500. }
  501. }
  502. #endregion LoadSeperateFolders
  503. }
  504. catch (Exception e)
  505. {
  506. Logging.Log("RetrieveAllSongs failed:", LogSeverity.Error);
  507. Logging.Log(e.ToString(), LogSeverity.Error);
  508. }
  509. #endregion AddCustomBeatmaps
  510. };
  511. Action finish = delegate
  512. {
  513. #region CountBeatmapsAndUpdateLevelPacks
  514. stopwatch.Stop();
  515. var songCount = CustomLevels.Count + CustomWIPLevels.Count;
  516. var songCountWSF = songCount;
  517. foreach (var f in SeperateSongFolders)
  518. songCount += f.Levels.Count;
  519. Logging.Log($"Loaded {songCount} new songs ({songCountWSF}) in CustomLevels | {songCount - songCountWSF} in seperate folders) in {stopwatch.Elapsed.TotalSeconds} seconds");
  520. try
  521. {
  522. //Handle LevelPacks
  523. if (CustomBeatmapLevelPackCollectionSO == null || CustomBeatmapLevelPackCollectionSO.beatmapLevelPacks.Length == 0)
  524. {
  525. #region AddSeperateFolderBeatmapsToRespectivePacks
  526. var beatmapLevelPackCollectionSO = Resources.FindObjectsOfTypeAll<BeatmapLevelPackCollectionSO>().FirstOrDefault();
  527. CustomBeatmapLevelPackCollectionSO = SongCoreBeatmapLevelPackCollectionSO.CreateNew(); // (beatmapLevelPackCollectionSO);
  528. foreach (var folderEntry in SeperateSongFolders)
  529. {
  530. switch (folderEntry.SongFolderEntry.Pack)
  531. {
  532. case FolderLevelPack.CustomLevels:
  533. CustomLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>(CustomLevels.Concat(folderEntry.Levels.Where(x => !CustomLevels.ContainsKey(x.Key))).ToDictionary(x => x.Key, x => x.Value));
  534. break;
  535. case FolderLevelPack.CustomWIPLevels:
  536. CustomWIPLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>(CustomWIPLevels.Concat(folderEntry.Levels.Where(x => !CustomWIPLevels.ContainsKey(x.Key))).ToDictionary(x => x.Key, x => x.Value));
  537. break;
  538. case FolderLevelPack.CachedWIPLevels:
  539. CachedWIPLevels = new ConcurrentDictionary<string, CustomPreviewBeatmapLevel>(CachedWIPLevels.Concat(folderEntry.Levels.Where(x => !CachedWIPLevels.ContainsKey(x.Key))).ToDictionary(x => x.Key, x => x.Value));
  540. break;
  541. default:
  542. break;
  543. }
  544. }
  545. #endregion AddSeperateFolderBeatmapsToRespectivePacks
  546. Logging.logger.Info("XXYYZZ AddSeperateFolderBeatmapsToRespectivePacks done");
  547. #region CreateLevelPacks
  548. // Create level collections and level packs
  549. // Add level packs to the custom levels pack collection
  550. CustomLevelsCollection = new SongCoreCustomLevelCollection(CustomLevels.Values.ToArray());
  551. WIPLevelsCollection = new SongCoreCustomLevelCollection(CustomWIPLevels.Values.ToArray());
  552. CachedWIPLevelCollection = new SongCoreCustomLevelCollection(CachedWIPLevels.Values.ToArray());
  553. CustomLevelsPack = new SongCoreCustomBeatmapLevelPack(CustomLevelLoader.kCustomLevelPackPrefixId + "CustomLevels", "Custom Levels", defaultCoverImage, CustomLevelsCollection);
  554. WIPLevelsPack = new SongCoreCustomBeatmapLevelPack(CustomLevelLoader.kCustomLevelPackPrefixId + "CustomWIPLevels", "WIP Levels", UI.BasicUI.WIPIcon, WIPLevelsCollection);
  555. CachedWIPLevelsPack = new SongCoreCustomBeatmapLevelPack(CustomLevelLoader.kCustomLevelPackPrefixId + "CachedWIPLevels", "Cached WIP Levels", UI.BasicUI.WIPIcon, CachedWIPLevelCollection);
  556. CustomBeatmapLevelPackCollectionSO.AddLevelPack(CustomLevelsPack);
  557. CustomBeatmapLevelPackCollectionSO.AddLevelPack(WIPLevelsPack);
  558. CustomBeatmapLevelPackCollectionSO.AddLevelPack(CachedWIPLevelsPack);
  559. #endregion CreateLevelPacks
  560. Logging.logger.Info("XXYYZZ CreateLevelPacks done");
  561. }
  562. //Level Packs
  563. RefreshLevelPacks();
  564. Logging.logger.Info("XXYYZZ RefreshLevelPacks return");
  565. }
  566. catch (Exception ex)
  567. {
  568. Logging.logger.Error("Failed to Setup LevelPacks: " + ex);
  569. }
  570. #endregion CountBeatmapsAndUpdateLevelPacks
  571. AreSongsLoaded = true;
  572. AreSongsLoading = false;
  573. LoadingProgress = 1;
  574. _loadingTask = null;
  575. try
  576. {
  577. var arr = SongsLoadedEvent.GetInvocationList();
  578. Logging.logger.Info($"XXYYZZ SongsLoadedEvent.GetInvocationList : {arr.Length}");
  579. foreach (var item in arr)
  580. {
  581. Logging.logger.Info($"XXYYZZ -> {item.Method.DeclaringType.AssemblyQualifiedName}::{item.Method}");
  582. item.DynamicInvoke(this, CustomLevels);
  583. Logging.logger.Info($"XXYYZZ <- Return");
  584. }
  585. }
  586. catch (Exception e)
  587. {
  588. Logging.logger.Error($"XXYYZZ SongsLoadedEvent.GetInvocationList:{e.Message}");
  589. }
  590. //SongsLoadedEvent?.Invoke(this, CustomLevels);
  591. // Write our cached hash info and
  592. //Hashing.UpdateCachedHashesInternal(foundSongPaths.Keys);
  593. //Hashing.UpdateCachedAudioDataInternal(foundSongPaths.Keys);
  594. Logging.logger.Info("XXYYZZ SaveExtraSongData Call");
  595. SongCore.Collections.SaveExtraSongData();
  596. Logging.logger.Info("XXYYZZ Leave finish task");
  597. };
  598. _loadingTask = new HMTask(job, finish);
  599. _loadingTask.Run();
  600. }
  601. public static StandardLevelInfoSaveData GetStandardLevelInfoSaveData(string path)
  602. {
  603. var text = ReadJson(path + "/info.dat");
  604. return StandardLevelInfoSaveData.DeserializeFromJSONString(text);
  605. }
  606. /// <summary>
  607. /// Delete a beatmap (is only used by other mods)
  608. /// </summary>
  609. /// <param name="folderPath">Directory of the beatmap</param>
  610. /// <param name="deleteFolder">Option to delete the base folder of the beatmap</param>
  611. public void DeleteSong(string folderPath, bool deleteFolder = true)
  612. {
  613. DeletingSong?.Invoke();
  614. //Remove the level from SongCore Collections
  615. try
  616. {
  617. if (CustomLevels.TryRemove(folderPath, out var level))
  618. {
  619. }
  620. else if (CustomWIPLevels.TryRemove(folderPath, out level))
  621. {
  622. }
  623. else if (CachedWIPLevels.TryRemove(folderPath, out level))
  624. {
  625. }
  626. else
  627. {
  628. foreach (var folderEntry in SeperateSongFolders)
  629. {
  630. if (folderEntry.Levels.TryRemove(folderPath, out level))
  631. {
  632. }
  633. }
  634. }
  635. if (level != null)
  636. {
  637. if (Collections.levelHashDictionary.ContainsKey(level.levelID))
  638. {
  639. var hash = Collections.hashForLevelID(level.levelID);
  640. Collections.levelHashDictionary.TryRemove(level.levelID, out _);
  641. if (Collections.hashLevelDictionary.ContainsKey(hash))
  642. {
  643. Collections.hashLevelDictionary[hash].Remove(level.levelID);
  644. if (Collections.hashLevelDictionary[hash].Count == 0)
  645. Collections.hashLevelDictionary.TryRemove(hash, out _);
  646. }
  647. }
  648. Hashing.UpdateCachedHashes(new HashSet<string>((CustomLevels.Keys.Concat(CustomWIPLevels.Keys))));
  649. }
  650. //Delete the directory
  651. if (deleteFolder)
  652. if (Directory.Exists(folderPath))
  653. {
  654. Directory.Delete(folderPath, true);
  655. }
  656. RefreshLevelPacks();
  657. }
  658. catch (Exception ex)
  659. {
  660. Logging.Log("Exception trying to Delete song: " + folderPath, LogSeverity.Error);
  661. Logging.Log(ex.ToString(), LogSeverity.Error);
  662. }
  663. }
  664. /*
  665. public void RetrieveNewSong(string folderPath)
  666. {
  667. try
  668. {
  669. bool wip = false;
  670. if (folderPath.Contains("CustomWIPLevels"))
  671. wip = true;
  672. StandardLevelInfoSaveData saveData = GetStandardLevelInfoSaveData(folderPath);
  673. var level = LoadSong(saveData, folderPath, out string hash);
  674. if (level != null)
  675. {
  676. if (!wip)
  677. CustomLevels[folderPath] = level;
  678. else
  679. CustomWIPLevels[folderPath] = level;
  680. if (!Collections.levelHashDictionary.ContainsKey(level.levelID))
  681. {
  682. Collections.levelHashDictionary.Add(level.levelID, hash);
  683. if (Collections.hashLevelDictionary.ContainsKey(hash))
  684. Collections.hashLevelDictionary[hash].Add(level.levelID);
  685. else
  686. {
  687. var levels = new List<string>();
  688. levels.Add(level.levelID);
  689. Collections.hashLevelDictionary.Add(hash, levels);
  690. }
  691. }
  692. }
  693. HashSet<string> paths = new HashSet<string>( Hashing.cachedSongHashData.Keys);
  694. paths.Add(folderPath);
  695. Hashing.UpdateCachedHashes(paths);
  696. RefreshLevelPacks();
  697. }
  698. catch (Exception ex)
  699. {
  700. Logging.Log("Failed to Retrieve New Song from: " + folderPath, LogSeverity.Error);
  701. Logging.Log(ex.ToString(), LogSeverity.Error);
  702. }
  703. }
  704. */
  705. /// <summary>
  706. /// Load a beatmap, gather all beatmap information and create beatmap preview
  707. /// </summary>
  708. /// <param name="saveData">Save data of beatmap</param>
  709. /// <param name="songPath">Directory of beatmap</param>
  710. /// <param name="hash">Resulting hash for the beatmap, may contain beatmap folder name or 'WIP' at the end</param>
  711. /// <param name="folderEntry">Folder entry for beatmap folder</param>
  712. /// <returns></returns>
  713. public static CustomPreviewBeatmapLevel LoadSong(StandardLevelInfoSaveData saveData, string songPath, out string hash, SongFolderEntry folderEntry = null)
  714. {
  715. CustomPreviewBeatmapLevel result;
  716. var wip = songPath.Contains("CustomWIPLevels");
  717. if (folderEntry != null)
  718. {
  719. if ((folderEntry.Pack == FolderLevelPack.CustomWIPLevels) || (folderEntry.Pack == FolderLevelPack.CachedWIPLevels))
  720. wip = true;
  721. else if (folderEntry.WIP)
  722. wip = true;
  723. }
  724. hash = Hashing.GetCustomLevelHash(saveData, songPath);
  725. try
  726. {
  727. var folderName = ExtractParentDirectoryName(songPath);
  728. var levelID = CustomLevelLoader.kCustomLevelPrefixId + hash;
  729. // Fixed WIP status for duplicate song hashes
  730. if (Collections.levelHashDictionary.ContainsKey(levelID + (wip ? " WIP" : "")))
  731. levelID += "_" + folderName;
  732. if (wip) levelID += " WIP";
  733. var songName = saveData.songName;
  734. var songSubName = saveData.songSubName;
  735. var songAuthorName = saveData.songAuthorName;
  736. var levelAuthorName = saveData.levelAuthorName;
  737. var beatsPerMinute = saveData.beatsPerMinute;
  738. var songTimeOffset = saveData.songTimeOffset;
  739. var shuffle = saveData.shuffle;
  740. var shufflePeriod = saveData.shufflePeriod;
  741. var previewStartTime = saveData.previewStartTime;
  742. var previewDuration = saveData.previewDuration;
  743. var environmentSceneInfo = _customLevelLoader.LoadEnvironmentInfo(saveData.environmentName, false);
  744. var allDirectionEnvironmentInfo = _customLevelLoader.LoadEnvironmentInfo(saveData.allDirectionsEnvironmentName, true);
  745. var list = new List<PreviewDifficultyBeatmapSet>();
  746. foreach (var difficultyBeatmapSet in saveData.difficultyBeatmapSets)
  747. {
  748. var beatmapCharacteristicBySerializedName = beatmapCharacteristicCollection.GetBeatmapCharacteristicBySerializedName(difficultyBeatmapSet.beatmapCharacteristicName);
  749. var array = new BeatmapDifficulty[difficultyBeatmapSet.difficultyBeatmaps.Length];
  750. for (var j = 0; j < difficultyBeatmapSet.difficultyBeatmaps.Length; j++)
  751. {
  752. BeatmapDifficulty beatmapDifficulty;
  753. difficultyBeatmapSet.difficultyBeatmaps[j].difficulty.BeatmapDifficultyFromSerializedName(out beatmapDifficulty);
  754. array[j] = beatmapDifficulty;
  755. }
  756. list.Add(new PreviewDifficultyBeatmapSet(beatmapCharacteristicBySerializedName, array));
  757. }
  758. result = new CustomPreviewBeatmapLevel(defaultCoverImage, saveData, songPath,
  759. cachedMediaAsyncLoaderSO, cachedMediaAsyncLoaderSO, levelID, songName, songSubName,
  760. songAuthorName, levelAuthorName, beatsPerMinute, songTimeOffset, shuffle, shufflePeriod,
  761. previewStartTime, previewDuration, environmentSceneInfo, allDirectionEnvironmentInfo, list.ToArray());
  762. GetSongDuration(result, songPath, Path.Combine(songPath, saveData.songFilename));
  763. //Task.Factory.StartNew(() => { GetSongDuration(result, songPath, Path.Combine(songPath, saveData.songFilename));});
  764. }
  765. catch
  766. {
  767. Logging.Log("Failed to Load Song: " + songPath, LogSeverity.Error);
  768. result = null;
  769. }
  770. return result;
  771. }
  772. /// <summary>
  773. /// Refresh songs on "R" key, full refresh on "Ctrl"+"R"
  774. /// </summary>
  775. private void Update()
  776. {
  777. if (Input.GetKeyDown(KeyCode.R))
  778. {
  779. RefreshSongs(Input.GetKey(KeyCode.LeftControl));
  780. }
  781. }
  782. #region HelperFunctionsZIP
  783. /// <summary>
  784. /// Extracts beatmap ZIP files to the cache folder
  785. /// </summary>
  786. /// <param name="cachePath">Directory of cache folder</param>
  787. /// <param name="songFolderPath">Directory of folder containing the zips</param>
  788. private void CacheZIPs(string cachePath, string songFolderPath)
  789. {
  790. if (!Directory.Exists(cachePath))
  791. Directory.CreateDirectory(cachePath);
  792. var cache = new DirectoryInfo(cachePath);
  793. foreach (var file in cache.GetFiles())
  794. file.Delete();
  795. foreach (var folder in cache.GetDirectories())
  796. folder.Delete(true);
  797. var zips = Directory.GetFiles(songFolderPath, "*.zip", SearchOption.TopDirectoryOnly);
  798. foreach (var zip in zips)
  799. {
  800. var unzip = new Unzip(zip);
  801. try
  802. {
  803. unzip.ExtractToDirectory(cachePath + "/" + new FileInfo(zip).Name);
  804. }
  805. catch (Exception ex)
  806. {
  807. Logging.logger.Warn("Failed to extract zip: " + zip + ": " + ex);
  808. }
  809. unzip.Dispose();
  810. }
  811. }
  812. /// <summary>
  813. /// Loads the beatmaps of the cached
  814. /// </summary>
  815. /// <param name="cacheFolders">Directory of cache folder</param>
  816. /// <param name="fullRefresh"></param>
  817. /// <param name="BeatmapDictionary"></param>
  818. /// <param name="folderEntry"></param>
  819. private void LoadCachedZIPs(string[] cacheFolders, bool fullRefresh, ConcurrentDictionary<string, CustomPreviewBeatmapLevel> BeatmapDictionary, SongFolderEntry folderEntry = null)
  820. {
  821. foreach (var cachedFolder in cacheFolders)
  822. {
  823. string[] results;
  824. try
  825. {
  826. results = Directory.GetFiles(cachedFolder, "info.dat", SearchOption.TopDirectoryOnly);
  827. }
  828. catch (DirectoryNotFoundException ex)
  829. {
  830. Logging.Log($"Skipping missing or corrupt folder: '{cachedFolder}'", LogSeverity.Warning);
  831. continue;
  832. }
  833. if (results.Length == 0)
  834. {
  835. Logging.Log("Folder: '" + cachedFolder + "' is missing info.dat files!", LogSeverity.Notice);
  836. continue;
  837. }
  838. foreach (var result in results)
  839. {
  840. try
  841. {
  842. var songPath = Path.GetDirectoryName(result.Replace('\\', '/'));
  843. if (!fullRefresh && BeatmapDictionary != null)
  844. {
  845. if (SearchBeatmapInMapPack(BeatmapDictionary, songPath)) continue;
  846. }
  847. var saveData = GetStandardLevelInfoSaveData(songPath);
  848. if (saveData == null)
  849. {
  850. continue;
  851. }
  852. HMMainThreadDispatcher.instance.Enqueue(delegate
  853. {
  854. if (_loadingCancelled) return;
  855. var level = LoadSong(saveData, songPath, out var hash, folderEntry);
  856. if (level != null)
  857. {
  858. BeatmapDictionary[songPath] = level;
  859. }
  860. });
  861. }
  862. catch (Exception ex)
  863. {
  864. Logging.logger.Notice("Failed to load song from " + cachedFolder + ": " + ex);
  865. }
  866. }
  867. }
  868. }
  869. #endregion HelperFunctionsZIP
  870. #region HelperFunctionsLoading
  871. private bool SearchBeatmapInMapPack(ConcurrentDictionary<string, CustomPreviewBeatmapLevel> mapPack, string songPath)
  872. {
  873. if (mapPack.TryGetValue(songPath, out var c))
  874. {
  875. if (c != null) return true;
  876. }
  877. return false;
  878. }
  879. private bool AssignBeatmapToSeperateFolder(ConcurrentDictionary<string, CustomPreviewBeatmapLevel> mapPack, string songPath, ConcurrentDictionary<string, CustomPreviewBeatmapLevel> seperateFolder)
  880. {
  881. if (mapPack.TryGetValue(songPath, out var c))
  882. {
  883. if (c != null)
  884. {
  885. seperateFolder[songPath] = c;
  886. return true;
  887. }
  888. }
  889. return false;
  890. }
  891. private CustomPreviewBeatmapLevel LoadSongAndAddToDictionaries(StandardLevelInfoSaveData saveData, string songPath, SongFolderEntry entry = null)
  892. {
  893. var level = LoadSong(saveData, songPath, out var hash, entry);
  894. if (level != null)
  895. {
  896. if (!Collections.levelHashDictionary.ContainsKey(level.levelID))
  897. {
  898. // Add level to LevelHash-Dictionary
  899. Collections.levelHashDictionary.TryAdd(level.levelID, hash);
  900. // Add hash to HashLevel-Dictionary
  901. if (Collections.hashLevelDictionary.TryGetValue(hash, out var levels))
  902. levels.Add(level.levelID);
  903. else
  904. {
  905. levels = new List<string>();
  906. levels.Add(level.levelID);
  907. Collections.hashLevelDictionary.TryAdd(hash, levels);
  908. }
  909. }
  910. }
  911. return level;
  912. }
  913. #endregion HelperFunctionsLoading
  914. #region HelperFunctionsSearching
  915. /// <summary>
  916. /// Attempts to get a beatmap by LevelId. Returns null a matching level isn't found.
  917. /// </summary>
  918. /// <param name="levelId"></param>
  919. /// <returns></returns>
  920. public static IPreviewBeatmapLevel GetLevelById(string levelId)
  921. {
  922. if (string.IsNullOrEmpty(levelId))
  923. return null;
  924. IPreviewBeatmapLevel level = null;
  925. if (levelId.StartsWith("custom_level_"))
  926. {
  927. if (CustomLevelsById.TryGetValue(levelId, out var customLevel))
  928. level = customLevel;
  929. }
  930. else if (OfficialSongs.TryGetValue(levelId, out var song))
  931. {
  932. level = song.PreviewBeatmapLevel;
  933. }
  934. return level;
  935. }
  936. /// <summary>
  937. /// Attempts to get a custom level by hash (case-insensitive). Returns null a matching custom level isn't found.
  938. /// </summary>
  939. /// <param name="hash"></param>
  940. /// <returns></returns>
  941. public static CustomPreviewBeatmapLevel GetLevelByHash(string hash)
  942. {
  943. if (string.IsNullOrEmpty(hash))
  944. return null;
  945. CustomLevelsById.TryGetValue("custom_level_" + hash.ToUpper(), out var level);
  946. return level;
  947. }
  948. private static BeatmapDataLoader loader = new BeatmapDataLoader();
  949. private static void GetSongDuration(CustomPreviewBeatmapLevel level, string songPath, string oggfile)
  950. {
  951. try
  952. {
  953. var levelid = level.levelID;
  954. float length = 0;
  955. if (Hashing.cachedAudioData.TryGetValue(songPath, out var data))
  956. {
  957. if (data.id == levelid)
  958. length = data.duration;
  959. }
  960. if (length == 0)
  961. {
  962. try
  963. {
  964. length = GetLengthFromOgg(oggfile);
  965. }
  966. catch (Exception ex)
  967. {
  968. length = -1;
  969. }
  970. if (length <= 1)
  971. {
  972. // janky, but whatever
  973. Logging.logger.Warn($"Failed to parse song length from Ogg file, Approximating using Map length. Song: {level.customLevelPath}");
  974. length = GetLengthFromMap(level, songPath);
  975. }
  976. }
  977. if (data != null)
  978. {
  979. data.duration = length;
  980. data.id = levelid;
  981. }
  982. else
  983. {
  984. Hashing.cachedAudioData[songPath] = new AudioCacheData(levelid, length);
  985. }
  986. // Logging.logger.Debug($"{length}");
  987. level.SetField<CustomPreviewBeatmapLevel, float>("_songDuration", length);
  988. }
  989. catch (Exception ex)
  990. {
  991. Logging.logger.Warn("Failed to Parse Song Duration" + ex);
  992. }
  993. }
  994. public static float GetLengthFromMap(CustomPreviewBeatmapLevel level, string songPath)
  995. {
  996. var diff = level.standardLevelInfoSaveData.difficultyBeatmapSets.First().difficultyBeatmaps.Last().beatmapFilename;
  997. var beatmapsave = BeatmapSaveData.DeserializeFromJSONString(ReadJson(Path.Combine(songPath, diff)));
  998. float highestTime = 0;
  999. if (beatmapsave.notes.Count > 0)
  1000. highestTime = beatmapsave.notes.Max(x => x.time);
  1001. else if (beatmapsave.events.Count > 0)
  1002. highestTime = beatmapsave.events.Max(x => x.time);
  1003. return loader.GetRealTimeFromBPMTime(highestTime, level.beatsPerMinute, level.shuffle, level.shufflePeriod);
  1004. }
  1005. private static byte[] oggBytes = new byte[] { 0x4F, 0x67, 0x67, 0x53, 0x00, 0x04 };
  1006. public static float GetLengthFromOgg(string oggFile)
  1007. {
  1008. if (GetCachedOggLength(oggFile, out var len)) return len;
  1009. using (var fs = File.OpenRead(oggFile))
  1010. using (var br = new BinaryReader(fs, Encoding.ASCII))
  1011. {
  1012. /*
  1013. * Tries to find the array of bytes from the stream
  1014. */
  1015. bool FindBytes(byte[] bytes, int searchLength)
  1016. {
  1017. for (var i = 0; i < searchLength; i++)
  1018. {
  1019. var b = br.ReadByte();
  1020. if (b != bytes[0]) continue;
  1021. var by = br.ReadBytes(bytes.Length - 1);
  1022. // hardcoded 6 bytes compare, is fine because all inputs used are 6 bytes
  1023. // bitwise AND the last byte to read only the flag bit for lastSample searching
  1024. // shouldn't cause issues finding rate, hopefully
  1025. if (by[0] == bytes[1] && by[1] == bytes[2] && by[2] == bytes[3] && by[3] == bytes[4] && (by[4] & bytes[5]) == bytes[5]) return true;
  1026. var index = Array.IndexOf(@by, bytes[0]);
  1027. if (index != -1)
  1028. {
  1029. fs.Position += index - (bytes.Length - 1);
  1030. i += index;
  1031. }
  1032. else
  1033. i += (bytes.Length - 1);
  1034. }
  1035. return false;
  1036. }
  1037. var rate = -1;
  1038. long lastSample = -1;
  1039. //Skip Capture Pattern
  1040. fs.Position = 24;
  1041. //{0x76, 0x6F, 0x72, 0x62, 0x69, 0x73} = "vorbis" in byte values
  1042. var foundVorbis = FindBytes(new byte[] { 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73 }, 256);
  1043. if (foundVorbis)
  1044. {
  1045. fs.Position += 5;
  1046. rate = br.ReadInt32();
  1047. }
  1048. else
  1049. {
  1050. Logging.logger.Warn($"could not find rate for {oggFile}");
  1051. return -1;
  1052. }
  1053. /*
  1054. * this finds the last occurrence of OggS in the file by checking for a bit flag (0x04)
  1055. * reads in blocks determined by seekBlockSize
  1056. * 6144 does not add significant overhead and speeds up the search significantly
  1057. */
  1058. const int seekBlockSize = 6144;
  1059. const int seekTries = 10; // 60 KiB should be enough for any sane ogg file
  1060. for (var i = 0; i < seekTries; i++)
  1061. {
  1062. var seekPos = (i + 1) * seekBlockSize * -1;
  1063. var overshoot = Math.Max((int)(-seekPos - fs.Length), 0);
  1064. if (overshoot >= seekBlockSize)
  1065. {
  1066. break;
  1067. }
  1068. fs.Seek(seekPos + overshoot, SeekOrigin.End);
  1069. var foundOggS = FindBytes(oggBytes, seekBlockSize - overshoot);
  1070. if (foundOggS)
  1071. {
  1072. lastSample = br.ReadInt64();
  1073. break;
  1074. }
  1075. }
  1076. if (lastSample == -1)
  1077. {
  1078. Logging.logger.Warn($"could not find lastSample for {oggFile}");
  1079. return -1;
  1080. }
  1081. var length = lastSample / (float)rate;
  1082. return length;
  1083. }
  1084. }
  1085. /// <summary>
  1086. /// Attempts to get an official level by LevelId. Returns false if a matching level isn't found.
  1087. /// </summary>
  1088. /// <param name="levelId"></param>
  1089. /// <returns></returns>
  1090. public static bool TryGetOfficialLevelById(string levelId, out OfficialSongEntry song)
  1091. {
  1092. if (string.IsNullOrEmpty(levelId))
  1093. {
  1094. song = default(OfficialSongEntry);
  1095. return false;
  1096. }
  1097. return OfficialSongs.TryGetValue(levelId, out song);
  1098. }
  1099. #endregion HelperFunctionsSearching
  1100. public struct OfficialSongEntry
  1101. {
  1102. public IBeatmapLevelPackCollection LevelPackCollection;
  1103. public IBeatmapLevelPack LevelPack;
  1104. public IPreviewBeatmapLevel PreviewBeatmapLevel;
  1105. }
  1106. }
  1107. }