/// <summary> /// Load beatmaps that have already been cached or are OST. It is highly recommended that LoadBeatmaps is used instead. /// </summary> /// <param name="levels">Array of IPreviewBeatmapLevel objects to get the beatmap details of.</param> /// <returns>Array of BeatmapDetails objects that represent the passed IPreviewBeatmapLevel objects.</returns> public BeatmapDetails[] LoadBeatmapsInstant(IPreviewBeatmapLevel[] levels) { BeatmapDetails[] detailsList = new BeatmapDetails[levels.Length]; int notLoadedCount = 0; for (int i = 0; i < levels.Length; ++i) { IPreviewBeatmapLevel level = levels[i]; string levelID = GetLevelID(level); if (level is IBeatmapLevel) { detailsList[i] = new BeatmapDetails(level as IBeatmapLevel); } else if (_cache.ContainsKey(levelID)) { detailsList[i] = _cache[levelID]; } else { // unable to load from cache or convert directly to BeatmapDetails object detailsList[i] = null; ++notLoadedCount; } } if (notLoadedCount > 0) { Logger.log.Warn($"LoadBeatmapsInstant was unable to retrieve all BeatmapDetails objects from cache ({notLoadedCount} could not be loaded)"); } return(detailsList); }
private async Task <Tuple <int, BeatmapDetails> > GetCustomBeatmapDetailsAsync(CustomPreviewBeatmapLevel level, int index) { try { CustomBeatmapLevel customLevel = await LoadCustomBeatmapLevelAsync(level, _loadingTokenSource.Token); var details = new BeatmapDetails(customLevel); _cache[GetLevelID(level)] = details; return(new Tuple <int, BeatmapDetails>(index, details)); } catch (OperationCanceledException) { } return(new Tuple <int, BeatmapDetails>(index, null)); }
/// <summary> /// Loads a single custom beatmap level. /// </summary> /// <param name="level">The custom preview beatmap for which you want to load the IBeatmapLevel for.</param> /// <param name="onFinish">The function that is called when the IBeatmapLevel is retrieved.</param> /// <returns>An awaitable Task.</returns> public async Task LoadSingleBeatmapAsync(CustomPreviewBeatmapLevel level, Action <IBeatmapLevel> onFinish) { CancellationTokenSource tokenSource = new CancellationTokenSource(TimeoutDelay); CustomBeatmapLevel customLevel = new CustomBeatmapLevel(CreateLevelCopyWithReplacedMediaLoader(level, MediaLoader), null, null); try { BeatmapLevelData beatmapData = await LevelLoader.LoadBeatmapLevelDataAsync(level.customLevelPath, customLevel, level.standardLevelInfoSaveData, tokenSource.Token); if (beatmapData != null) { customLevel.SetBeatmapLevelData(beatmapData); string levelID = GetSimplifiedLevelID(customLevel); if (!_cache.ContainsKey(levelID) && !IsCaching) { _cache[GetSimplifiedLevelID(customLevel)] = new BeatmapDetails(customLevel); } try { onFinish?.Invoke(customLevel); } catch (Exception e) { Logger.log.Warn("Unexpected exception occurred in delegate after loading beatmap"); Logger.log.Debug(e); } } else { Logger.log.Debug($"Unable to load beatmap level data for '{level.songName}' (no data returned)"); } } catch (OperationCanceledException) { Logger.log.Debug($"Unable to load beatmap level data for '{level.songName}' (load task timed out)"); } tokenSource.Dispose(); }
private static IEnumerator <OrderedBeatmapDetails> GetOrderedCustomBeatmapDetailsCoroutine(CustomPreviewBeatmapLevel level, int index) { IEnumerator <BeatmapDetails> loadingCoroutine = BeatmapDetails.CreateBeatmapDetailsFromFilesCoroutine(level); while (loadingCoroutine.MoveNext()) { BeatmapDetails beatmapDetails = loadingCoroutine.Current; if (beatmapDetails != null) { _cache[beatmapDetails.LevelID] = beatmapDetails; yield return(new OrderedBeatmapDetails(index, beatmapDetails)); yield break; } else { yield return(null); } } yield return(new OrderedBeatmapDetails(index, null)); }
private async Task CacheCustomBeatmapDetailsAsync(CustomPreviewBeatmapLevel level) { CustomBeatmapLevel customLevel = await LoadCustomBeatmapLevelAsync(level, _cachingTokenSource.Token).ConfigureAwait(false); _cache[GetLevelID(level)] = new BeatmapDetails(customLevel); }
public static IEnumerator <BeatmapDetails> CreateBeatmapDetailsFromFilesCoroutine(CustomPreviewBeatmapLevel customLevel) { StandardLevelInfoSaveData infoData = customLevel.standardLevelInfoSaveData; BeatmapDetails beatmapDetails = new BeatmapDetails(); beatmapDetails.LevelID = BeatmapDetailsLoader.GetSimplifiedLevelID(customLevel); beatmapDetails.SongName = customLevel.songName; beatmapDetails.BeatsPerMinute = infoData.beatsPerMinute; // load difficulties beatmapDetails.DifficultyBeatmapSets = new SimplifiedDifficultyBeatmapSet[infoData.difficultyBeatmapSets.Length]; for (int i = 0; i < infoData.difficultyBeatmapSets.Length; ++i) { var currentSimplifiedSet = new SimplifiedDifficultyBeatmapSet(); beatmapDetails.DifficultyBeatmapSets[i] = currentSimplifiedSet; var currentSet = infoData.difficultyBeatmapSets[i]; currentSimplifiedSet.CharacteristicName = currentSet.beatmapCharacteristicName; currentSimplifiedSet.DifficultyBeatmaps = new SimplifiedDifficultyBeatmap[currentSet.difficultyBeatmaps.Length]; for (int j = 0; j < currentSet.difficultyBeatmaps.Length; ++j) { var currentSimplifiedDiff = new SimplifiedDifficultyBeatmap(); currentSimplifiedSet.DifficultyBeatmaps[j] = currentSimplifiedDiff; var currentDiff = currentSet.difficultyBeatmaps[j]; currentDiff.difficulty.BeatmapDifficultyFromSerializedName(out currentSimplifiedDiff.Difficulty); currentSimplifiedDiff.NoteJumpMovementSpeed = currentDiff.noteJumpMovementSpeed; string diffFilePath = Path.Combine(customLevel.customLevelPath, currentDiff.beatmapFilename); string fileContents = null; IEnumerator <string> textLoader = UnityMediaLoader.LoadTextCoroutine(diffFilePath); while (textLoader.MoveNext()) { fileContents = textLoader.Current; if (fileContents == null) { yield return(null); } } if (string.IsNullOrEmpty(fileContents)) { yield break; } BeatmapSaveData beatmapSaveData = null; try { beatmapSaveData = BeatmapSaveData.DeserializeFromJSONString(fileContents); } catch (Exception e) { Logger.log.Warn($"Exception occurred while trying to deserialize difficulty beatmap to BeatmapSaveData for '{customLevel.songName}'"); Logger.log.Debug(e); yield break; } // missing difficulty files if (beatmapSaveData == null) { yield break; } // count notes and bombs currentSimplifiedDiff.NotesCount = 0; currentSimplifiedDiff.BombsCount = 0; foreach (var note in beatmapSaveData.notes) { if (note.type.IsBasicNote()) { ++currentSimplifiedDiff.NotesCount; } else if (note.type == NoteType.Bomb) { ++currentSimplifiedDiff.BombsCount; } } // count rotation events currentSimplifiedDiff.SpawnRotationEventsCount = 0; foreach (var mapEvent in beatmapSaveData.events) { if (mapEvent.type.IsRotationEvent()) { ++currentSimplifiedDiff.SpawnRotationEventsCount; } } currentSimplifiedDiff.ObstaclesCount = beatmapSaveData.obstacles.Count; } } // load audio string audioFilePath = Path.Combine(customLevel.customLevelPath, infoData.songFilename); AudioClip audioClip = null; IEnumerator <AudioClip> audioLoader = UnityMediaLoader.LoadAudioClipCoroutine(audioFilePath); while (audioLoader.MoveNext()) { audioClip = audioLoader.Current; if (audioClip == null) { yield return(null); } } if (audioClip == null) { yield break; } beatmapDetails.SongDuration = audioClip.length; yield return(beatmapDetails); }
/// <summary> /// Loads files associated with a custom beatmap and creates a BeatmapDetails object with the information contained in the files. /// </summary> /// <param name="customLevel">A custom level to create the BeatmapDetails object for.</param> /// <returns>BeatmapDetails object on success, otherwise null.</returns> public static BeatmapDetails CreateBeatmapDetailsFromFiles(CustomPreviewBeatmapLevel customLevel) { StandardLevelInfoSaveData infoData = customLevel.standardLevelInfoSaveData; BeatmapDetails beatmapDetails = new BeatmapDetails(); beatmapDetails.LevelID = BeatmapDetailsLoader.GetSimplifiedLevelID(customLevel); beatmapDetails.SongName = customLevel.songName; beatmapDetails.BeatsPerMinute = infoData.beatsPerMinute; // load difficulties for note info beatmapDetails.DifficultyBeatmapSets = new SimplifiedDifficultyBeatmapSet[infoData.difficultyBeatmapSets.Length]; for (int i = 0; i < infoData.difficultyBeatmapSets.Length; ++i) { var currentSimplifiedSet = new SimplifiedDifficultyBeatmapSet(); beatmapDetails.DifficultyBeatmapSets[i] = currentSimplifiedSet; var currentSet = infoData.difficultyBeatmapSets[i]; currentSimplifiedSet.CharacteristicName = currentSet.beatmapCharacteristicName; currentSimplifiedSet.DifficultyBeatmaps = new SimplifiedDifficultyBeatmap[currentSet.difficultyBeatmaps.Length]; for (int j = 0; j < currentSet.difficultyBeatmaps.Length; ++j) { var currentSimplifiedDiff = new SimplifiedDifficultyBeatmap(); currentSimplifiedSet.DifficultyBeatmaps[j] = currentSimplifiedDiff; var currentDiff = currentSet.difficultyBeatmaps[j]; currentDiff.difficulty.BeatmapDifficultyFromSerializedName(out currentSimplifiedDiff.Difficulty); currentSimplifiedDiff.NoteJumpMovementSpeed = currentDiff.noteJumpMovementSpeed; string diffFilePath = Path.Combine(customLevel.customLevelPath, currentDiff.beatmapFilename); if (!File.Exists(diffFilePath)) { return(null); } BeatmapSaveData beatmapSaveData = null; try { beatmapSaveData = BeatmapSaveData.DeserializeFromJSONString(File.ReadAllText(diffFilePath)); } catch (Exception e) { Logger.log.Debug("Unable to create BeatmapDetails object from files (unexpected exception occurred trying to load BeatmapSaveData from file)"); Logger.log.Debug(e); return(null); } if (beatmapSaveData == null) { Logger.log.Debug("Unable to create BeatmapDetails object from files (could not load BeatmapSaveData from file)"); return(null); } // count notes and bombs currentSimplifiedDiff.NotesCount = 0; currentSimplifiedDiff.BombsCount = 0; foreach (var note in beatmapSaveData.notes) { if (note.type.IsBasicNote()) { ++currentSimplifiedDiff.NotesCount; } else if (note.type == NoteType.Bomb) { ++currentSimplifiedDiff.BombsCount; } } // count rotation events currentSimplifiedDiff.SpawnRotationEventsCount = 0; foreach (var mapEvent in beatmapSaveData.events) { if (mapEvent.type.IsRotationEvent()) { ++currentSimplifiedDiff.SpawnRotationEventsCount; } } currentSimplifiedDiff.ObstaclesCount = beatmapSaveData.obstacles.Count; } } // load audio for map length string audioFilePath = Path.Combine(customLevel.customLevelPath, infoData.songFilename); AudioClip audioClip = UnityMediaLoader.LoadAudioClip(audioFilePath); if (audioClip == null) { return(null); } beatmapDetails.SongDuration = audioClip.length; return(beatmapDetails); }
private void CachingThread() { try { var sw = Stopwatch.StartNew(); // load cache from file List <BeatmapDetails> loadedCache = BeatmapDetailsCache.GetBeatmapDetailsFromCache(BeatmapDetailsLoader.CachedBeatmapDetailsFilePath); if (loadedCache != null) { if (loadedCache.Count > 0) { Logger.log.Info($"Retrieved {loadedCache.Count} cached beatmap details from file"); } foreach (var detail in loadedCache) { if (!BeatmapDetailsLoader._cache.ContainsKey(detail.LevelID)) { BeatmapDetailsLoader._cache.Add(detail.LevelID, detail); } } } List <IEnumerator <BeatmapDetails> > taskList = new List <IEnumerator <BeatmapDetails> >(WorkChunkSize); List <IPreviewBeatmapLevel> allCustomLevels = BeatmapDetailsLoader.GetAllCustomLevels(); List <SongDataCoreDataStatus> sdcErrorStatusList = new List <SongDataCoreDataStatus>(allCustomLevels.Count); int index = 0; int errorCount = 0; long elapsed = 0; while (index < allCustomLevels.Count) { if (sw.ElapsedMilliseconds > 30000 + elapsed) { elapsed = sw.ElapsedMilliseconds; Logger.log.Debug($"Caching thread has finished caching {index} beatmaps out of {allCustomLevels.Count} ({elapsed} ms elapsed)"); } _manualResetEvent.WaitOne(); if (_isOperationCancelled) { return; } for (int i = 0; i < WorkChunkSize && index < allCustomLevels.Count; ++index) { IPreviewBeatmapLevel level = allCustomLevels[index]; string levelID = GetSimplifiedLevelID(level); BeatmapDetails beatmapDetails; if (BeatmapDetailsLoader._cache.ContainsKey(levelID) && BeatmapDetailsLoader._cache[levelID].SongDuration > 0.01f) { continue; } SongDataCoreDataStatus status = SongDataCoreTweaks.GetBeatmapDetails(level as CustomPreviewBeatmapLevel, out beatmapDetails); if (status == SongDataCoreDataStatus.Success) { if (beatmapDetails.DifficultyBeatmapSets.Any(set => set.DifficultyBeatmaps.Any(diff => diff.NoteJumpMovementSpeed == 0))) { Logger.log.Debug($"BeatmapDetails object generated for '{beatmapDetails.SongName}' from BeatSaver data has some incomplete fields. " + "Discarding and generating BeatmapDetails object from locally stored information instead"); taskList.Add(BeatmapDetails.CreateBeatmapDetailsFromFilesCoroutine(level as CustomPreviewBeatmapLevel)); ++i; } else { BeatmapDetailsLoader._cache[levelID] = beatmapDetails; } } else { if (SongDataCoreTweaks.IsModAvailable) { sdcErrorStatusList.Add(status); } taskList.Add(BeatmapDetails.CreateBeatmapDetailsFromFilesCoroutine(level as CustomPreviewBeatmapLevel)); ++i; } } while (taskList.Any()) { _manualResetEvent.WaitOne(); if (_isOperationCancelled) { return; } for (int i = 0; i < taskList.Count; ++i) { IEnumerator <BeatmapDetails> loadCoroutine = taskList[i]; if (loadCoroutine.MoveNext()) { BeatmapDetails beatmapDetails = loadCoroutine.Current; if (beatmapDetails != null) { BeatmapDetailsLoader._cache[beatmapDetails.LevelID] = beatmapDetails; taskList.Remove(loadCoroutine); --i; } } else { ++errorCount; taskList.Remove(loadCoroutine); --i; } } } } sw.Stop(); Logger.log.Info($"Finished caching the details of {allCustomLevels.Count} beatmaps (took {sw.ElapsedMilliseconds / 1000f} seconds)"); if (errorCount > 0) { Logger.log.Warn($"Unable to cache the beatmap details for {errorCount} songs"); } if (sdcErrorStatusList.Count > 0) { // NOTE: this will need to be updated if i ever add more error status markers Logger.log.Debug($"Unable to retrieve some data from SongDataCore: (" + $"NoData = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.NoData)}, " + $"InvalidBPM = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidBPM)}, " + $"InvalidDuration = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidDuration)}, " + $"InvalidCharacteristicString = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidCharacteristicString)}, " + $"InvalidDifficultyString = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidDifficultyString)}, " + $"ExceptionThrown = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.ExceptionThrown)})"); } BeatmapDetailsLoader.instance.SaveCacheToFile(); HMMainThreadDispatcher.instance.Enqueue(delegate() { _thread = null; _manualResetEvent.Dispose(); _manualResetEvent = null; CachingFinished?.Invoke(); }); } catch (Exception e) { Logger.log.Warn("Unexpected exception occurred in caching thread"); Logger.log.Debug(e); } }
public OrderedBeatmapDetails(int position, BeatmapDetails beatmapDetails) { this.Position = position; this.Details = beatmapDetails; }
/// <summary> /// Gets the hash of a <see cref="BeatmapDetails"/> using its level ID. /// </summary> /// <param name="details"><see cref="BeatmapDetails"/> to get the hash for.</param> /// <returns>A string containing the level's hash or an empty string if unsuccessful.</returns> public static string GetCustomLevelHash(BeatmapDetails details) => details.IsOST ? string.Empty : GetCustomLevelHash(details.LevelID);
private IEnumerator CacheAllBeatmapDetailsCoroutine() { CachingStarted?.Invoke(); var sw = Stopwatch.StartNew(); // load beatmap details from cache if it exists var loadCache = PopulateCacheFromFileCoroutine(); while (loadCache.MoveNext()) { yield return(loadCache.Current); } // we don't have to cache OST levels, since they can be immediately cast into IBeatmapLevel objects List <IPreviewBeatmapLevel> allCustomLevels = BeatmapDetailsLoader.GetAllCustomLevels(); // record errors from SongDataCore for logging List <SongDataCoreDataStatus> sdcErrorStatusList = new List <SongDataCoreDataStatus>(allCustomLevels.Count); List <IEnumerator <BeatmapDetails> > taskList = new List <IEnumerator <BeatmapDetails> >(WorkChunkSize); int index = 0; int errorCount = 0; long elapsed = 0; while (index < allCustomLevels.Count) { if (sw.ElapsedMilliseconds > 30000 + elapsed) { elapsed = sw.ElapsedMilliseconds; Logger.log.Debug($"Caching coroutine has finished caching {index} beatmaps out of {allCustomLevels.Count} ({elapsed} ms elapsed)"); } while (_cachingPaused) { yield return(null); } int startingIndex = index; for (int i = 0; i < WorkChunkSize && index < allCustomLevels.Count && index - startingIndex < WorkQueryChunkSize; ++index) { string levelID = GetSimplifiedLevelID(allCustomLevels[index]); if (BeatmapDetailsLoader._cache.ContainsKey(levelID) && BeatmapDetailsLoader._cache[levelID].SongDuration > 0.01f) { continue; } SongDataCoreDataStatus status = SongDataCoreTweaks.GetBeatmapDetails(allCustomLevels[index] as CustomPreviewBeatmapLevel, out var beatmapDetails); if (status == SongDataCoreDataStatus.Success) { // load the beatmap details manually if some data from BeatSaver is incomplete if (beatmapDetails.DifficultyBeatmapSets.Any(set => set.DifficultyBeatmaps.Any(diff => diff.NoteJumpMovementSpeed == 0))) { Logger.log.Debug($"BeatmapDetails object generated for '{beatmapDetails.SongName}' from BeatSaver data has some incomplete fields. " + "Discarding and generating BeatmapDetails object from locally stored information instead"); taskList.Add(BeatmapDetails.CreateBeatmapDetailsFromFilesCoroutine(allCustomLevels[index] as CustomPreviewBeatmapLevel)); ++i; } else { BeatmapDetailsLoader._cache[levelID] = beatmapDetails; } } else { if (SongDataCoreTweaks.IsModAvailable) { sdcErrorStatusList.Add(status); } taskList.Add(BeatmapDetails.CreateBeatmapDetailsFromFilesCoroutine(allCustomLevels[index] as CustomPreviewBeatmapLevel)); ++i; } } if (taskList.Any()) { yield return(null); } while (taskList.Any()) { while (_cachingPaused) { yield return(null); } for (int i = 0; i < taskList.Count; ++i) { IEnumerator <BeatmapDetails> loadCoroutine = taskList[i]; if (loadCoroutine.MoveNext()) { BeatmapDetails beatmapDetails = loadCoroutine.Current; if (beatmapDetails != null) { BeatmapDetailsLoader._cache[beatmapDetails.LevelID] = beatmapDetails; taskList.Remove(loadCoroutine); --i; } } else { ++errorCount; taskList.Remove(loadCoroutine); --i; } } if (taskList.Any()) { yield return(null); } } if (index < allCustomLevels.Count) { yield return(null); } } // check for pause before writing to disk while (_cachingPaused) { yield return(null); } sw.Stop(); Logger.log.Info($"Finished caching the details of {allCustomLevels.Count} beatmaps (took {sw.ElapsedMilliseconds / 1000f} seconds)"); if (errorCount > 0) { Logger.log.Warn($"Unable to cache the beatmap details for {errorCount} songs"); } if (sdcErrorStatusList.Count > 0) { // NOTE: this will need to be updated if i ever add more error status markers Logger.log.Debug($"Unable to retrieve some data from SongDataCore: (" + $"NoData = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.NoData)}, " + $"InvalidBPM = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidBPM)}, " + $"InvalidDuration = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidDuration)}, " + $"InvalidCharacteristicString = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidCharacteristicString)}, " + $"InvalidDifficultyString = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.InvalidDifficultyString)}, " + $"ExceptionThrown = {sdcErrorStatusList.Count(x => x == SongDataCoreDataStatus.ExceptionThrown)})"); } BeatmapDetailsLoader.instance.SaveCacheToFile(); _cachingCoroutine = null; CachingFinished?.Invoke(); }