public BeatmapDetails(IBeatmapLevel level) { if (level == null) { throw new ArgumentNullException(nameof(level), "IBeatmapLevel parameter 'level' cannot be null"); } else if (level.beatmapLevelData == null) { throw new ArgumentException("Provided IBeatmapLevel object cannot have the 'beatmapLevelData' property be null", nameof(level)); } // remove the directory part of a custom level ID LevelID = BeatmapDetailsLoader.GetSimplifiedLevelID(level); SongName = level.songName; BeatsPerMinute = level.beatsPerMinute; if (level.beatmapLevelData.audioClip != null) { SongDuration = level.beatmapLevelData.audioClip.length; } else { SongDuration = level.songDuration; if (level is CustomBeatmapLevel) { Logger.log.Debug("Using stored song duration for custom song (might not work)"); } } var levelDifficultyBeatmapSets = level.beatmapLevelData.difficultyBeatmapSets; DifficultyBeatmapSets = new SimplifiedDifficultyBeatmapSet[levelDifficultyBeatmapSets.Count()]; for (int i = 0; i < levelDifficultyBeatmapSets.Count(); ++i) { SimplifiedDifficultyBeatmapSet newSet = new SimplifiedDifficultyBeatmapSet(); DifficultyBeatmapSets[i] = newSet; newSet.CharacteristicName = levelDifficultyBeatmapSets[i].beatmapCharacteristic.serializedName; var levelDifficultyBeatmaps = levelDifficultyBeatmapSets[i].difficultyBeatmaps; newSet.DifficultyBeatmaps = new SimplifiedDifficultyBeatmap[levelDifficultyBeatmaps.Length]; for (int j = 0; j < levelDifficultyBeatmaps.Length; ++j) { SimplifiedDifficultyBeatmap newDiff = new SimplifiedDifficultyBeatmap(); newSet.DifficultyBeatmaps[j] = newDiff; newDiff.Difficulty = levelDifficultyBeatmaps[j].difficulty; newDiff.NoteJumpMovementSpeed = levelDifficultyBeatmaps[j].noteJumpMovementSpeed; newDiff.NotesCount = levelDifficultyBeatmaps[j].beatmapData.notesCount; newDiff.BombsCount = levelDifficultyBeatmaps[j].beatmapData.bombsCount; newDiff.ObstaclesCount = levelDifficultyBeatmaps[j].beatmapData.obstaclesCount; newDiff.SpawnRotationEventsCount = levelDifficultyBeatmaps[j].beatmapData.spawnRotationEventsCount; } } }
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); }
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); } }
/// <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 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(); }