Exemple #1
0
        /// <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);
        }
Exemple #2
0
        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));
        }
Exemple #5
0
        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);
        }
Exemple #8
0
            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);
Exemple #11
0
            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();
            }