diff --git a/Assets/ProjectQuint/Runtime/Unity/Bootstrap/PinnedPackPremixCacheService.cs b/Assets/ProjectQuint/Runtime/Unity/Bootstrap/PinnedPackPremixCacheService.cs index 3febfae..88c36fb 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Bootstrap/PinnedPackPremixCacheService.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Bootstrap/PinnedPackPremixCacheService.cs @@ -146,7 +146,7 @@ namespace ProjectQuint.Bootstrap foreach (var entry in entries) { cancellationToken.ThrowIfCancellationRequested(); - var prepared = SongLoadCoordinator.ParseAndTransform(entry, LaneModifier.Off, 0); + var prepared = SongChartPreparation.Prepare(entry, LaneModifier.Off, 0); var key = _cache.BuildKey(prepared.Chart, outputSampleRate, cancellationToken); if (unique.ContainsKey(key)) continue; @@ -305,10 +305,10 @@ namespace ProjectQuint.Bootstrap continue; } - SongLoadCoordinator.PreparedChart prepared; + PreparedChart prepared; try { - prepared = SongLoadCoordinator.ParseAndTransform(item.Entry, LaneModifier.Off, 0); + prepared = SongChartPreparation.Prepare(item.Entry, LaneModifier.Off, 0); } catch (Exception exception) { diff --git a/Assets/ProjectQuint/Runtime/Unity/Bootstrap/ProjectQuintApp.cs b/Assets/ProjectQuint/Runtime/Unity/Bootstrap/ProjectQuintApp.cs index c0649cb..d4f65dd 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Bootstrap/ProjectQuintApp.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Bootstrap/ProjectQuintApp.cs @@ -154,10 +154,10 @@ namespace ProjectQuint.Bootstrap private SongLibraryEntry _loadingEntry; private SongLibraryEntry _playingEntry; private AeryCourseIndex _courseIndex; - private AeryTableIndex _aeryCatalog; + private IChartCatalog _activeCatalog; private SearchAliasIndex _searchAliases = SearchAliasIndex.Empty; - private int _aeryMatchedCharts; + private int _catalogMatchedCharts; private int _libraryScanGeneration; private Texture2D _libraryThumbnail; @@ -1554,57 +1554,14 @@ namespace ProjectQuint.Bootstrap yield break; } - var isAeryTable = IsAeryTable(_activeDifficultyTable?.HeaderUrl); - var cachePath = _activeDifficultyTable == null - ? string.Empty - : _difficultyTableRegistry.DataCachePathFor(_activeDifficultyTable); - var courseCachePath = _activeDifficultyTable == null - ? string.Empty - : _difficultyTableRegistry.HeaderCachePathFor(_activeDifficultyTable); - var bundledTable = Resources.Load("ProjectQuint/Data/AeryData"); - var bundledCourseHeader = Resources.Load("ProjectQuint/Data/AeryHeader"); - ExternalDataOrigin catalogOrigin; - string catalogStatus; - string courseLocalStatus; - if (isAeryTable) - { - var tableResult = AeryTableIndex.LoadLocal( - cachePath, - bundledTable != null ? bundledTable.text : string.Empty); - _aeryCatalog = tableResult.Data; - _activeCatalog = _aeryCatalog; - catalogOrigin = tableResult.Origin; - catalogStatus = tableResult.Status; - - var courseResult = AeryCourseIndex.LoadLocal( - courseCachePath, - bundledCourseHeader != null ? bundledCourseHeader.text : string.Empty); - _courseIndex = courseResult.Data; - _courseStatus = courseResult.Status; - courseLocalStatus = courseResult.Status; - } - else - { - _aeryCatalog = null; - var localTable = _activeDifficultyTable == null - ? null - : BmsDifficultyTableIndex.LoadLocal( - _activeDifficultyTable.Id, - courseCachePath, - cachePath); - _activeCatalog = localTable?.Data; - catalogOrigin = localTable?.Origin ?? ExternalDataOrigin.Unavailable; - catalogStatus = localTable?.Status ?? "No difficulty table configured"; - var localCourses = _activeDifficultyTable == null - ? null - : AeryCourseIndex.LoadTableLocal( - _activeDifficultyTable.Id, - localTable?.Data?.DisplayName ?? _activeDifficultyTable.DisplayName, - courseCachePath); - _courseIndex = localCourses?.Data; - _courseStatus = localCourses?.Status ?? "NO BMSTABLE COURSE DATA CONFIGURED"; - courseLocalStatus = _courseStatus; - } + var tableSource = new DifficultyTableLibrarySource(_activeDifficultyTable, _difficultyTableRegistry); + var localData = tableSource.LoadLocal(); + _activeCatalog = localData.Catalog.Data; + _courseIndex = localData.Courses.Data; + var catalogOrigin = localData.Catalog.Origin; + var catalogStatus = localData.Catalog.Status; + var courseLocalStatus = localData.Courses.Status; + _courseStatus = courseLocalStatus; var bundledSearchAliases = Resources.Load("ProjectQuint/Data/SearchAliases"); _searchAliases = SearchAliasIndex.Load( bundledSearchAliases != null ? bundledSearchAliases.text : string.Empty, @@ -1619,7 +1576,8 @@ namespace ProjectQuint.Bootstrap libraryIndexCachePath, legacyLibraryIndexCachePath, _activePack.Id, - ResolvePackScanKeyCount(_activePack, _activeDifficultyTable)); + _activePack.ForcedKeyCount, + SongKeyModePolicy.TableDefault(_activeDifficultyTable?.Id, _activeDifficultyTable?.HeaderUrl)); _scanCancellation?.Cancel(); _scanCancellation?.Dispose(); _scanCancellation = new CancellationTokenSource(); @@ -1651,7 +1609,7 @@ namespace ProjectQuint.Bootstrap var scannedSongs = scanTask.Result; _catalogMatchedCharts = ChartCatalogAnnotator.Apply(scannedSongs, _activeCatalog); - _aeryMatchedCharts = isAeryTable ? _catalogMatchedCharts : 0; + SongSearchIndexer.Rebuild(scannedSongs, _searchAliases); _favoriteRepository.Apply(scannedSongs); foreach (var song in scannedSongs) @@ -1691,94 +1649,25 @@ namespace ProjectQuint.Bootstrap RefreshLibraryThumbnail(); if (_openLibraryWhenScanCompletes) _mode = AppMode.Library; _openLibraryWhenScanCompletes = false; - if (isAeryTable) - { - StartCoroutine(RefreshAeryIntegration( - scanGeneration, - cachePath, - bundledTable != null ? bundledTable.text : string.Empty, - courseCachePath, - bundledCourseHeader != null ? bundledCourseHeader.text : string.Empty)); - } - else if (_activeDifficultyTable != null) - { - StartCoroutine(RefreshGenericDifficultyTable(scanGeneration, _activeDifficultyTable, _activePack)); - } + if (tableSource.IsConfigured) + StartCoroutine(RefreshDifficultyTable(scanGeneration, _activeDifficultyTable, _activePack, tableSource)); Debug.Log($"[ProjectQuint] Indexed {scannedSongs.Count} charts from {_libraryRoot}. Failed: {_scanner.Failed}. " + $"Index cache: {_scanner.CacheStatus}. " + $"Difficulty table: {catalogStatus}; {_catalogMatchedCharts} matched from {catalogOrigin}. " + + $"Key mode conflicts: {scannedSongs.Count(song => song.KeyMode.HasConflict)} (pack setting preserved). " + $"Default view: {SongLibraryView.AllLevelsFilter} " + $"({_libraryController.State.VisibleSongs.Count} charts)."); } - private IEnumerator RefreshAeryIntegration( - int scanGeneration, - string tableCachePath, - string bundledTableJson, - string courseCachePath, - string bundledCourseJson) - { - ExternalDataResult tableResult = null; - yield return AeryTableIndex.LoadOnline( - tableCachePath, - bundledTableJson, - result => tableResult = result); - if (scanGeneration != _libraryScanGeneration || !_libraryReady) - yield break; - - if (tableResult?.Data != null && tableResult.Data.Count > 0) - { - _aeryCatalog = tableResult.Data; - _activeCatalog = _aeryCatalog; - _catalogMatchedCharts = ChartCatalogAnnotator.Apply( - _libraryController.State.AllSongs, - _aeryCatalog); - _aeryMatchedCharts = _catalogMatchedCharts; - SongSearchIndexer.Rebuild(_libraryController.State.AllSongs, _searchAliases); - _libraryController.RefreshCatalogMetadata(); - if (_mode == AppMode.Library || _mode == AppMode.QuickOptions) - RefreshLibraryThumbnail(); - } - - ExternalDataResult courseResult = null; - yield return AeryCourseIndex.LoadOnline( - courseCachePath, - bundledCourseJson, - result => courseResult = result); - if (scanGeneration != _libraryScanGeneration || !_libraryReady) - yield break; - - if (courseResult?.Data != null && courseResult.Data.Courses.Count > 0) - { - _courseIndex = courseResult.Data; - _courseIndex.ResolveSongs(_libraryController.State.AllSongs); - } - - var availableCourses = _courseIndex?.Courses.Count(course => course.IsPlayable) ?? 0; - var totalCourses = _courseIndex?.Courses.Count ?? 0; - _courseStatus = $"{courseResult?.Status ?? "AERY courses: unavailable"} // " + - $"{availableCourses}/{totalCourses} COURSES READY"; - _status = $"BMS {_libraryController.State.AllSongs.Count:N0} // " + - $"AERY {_aeryMatchedCharts:N0} matched ({_aeryCatalog?.Origin ?? ExternalDataOrigin.Unavailable}) // " + - $"{(_scanner?.CacheHits ?? 0):N0} cached // {(_scanner?.CacheMisses ?? 0):N0} refreshed"; - Debug.Log( - $"[ProjectQuint] Optional AERY refresh complete: " + - $"{_aeryMatchedCharts} chart matches from {tableResult?.Origin ?? ExternalDataOrigin.Unavailable}; " + - $"{availableCourses}/{totalCourses} courses from {courseResult?.Origin ?? ExternalDataOrigin.Unavailable}."); - } - - private IEnumerator RefreshGenericDifficultyTable( + private IEnumerator RefreshDifficultyTable( int scanGeneration, BmsDifficultyTableDefinition table, - BmsPackDefinition sourcePack) + BmsPackDefinition sourcePack, + DifficultyTableLibrarySource source) { - ExternalDataResult result = null; - yield return BmsDifficultyTableIndex.LoadOnline( - table.Id, - table.HeaderUrl, - _difficultyTableRegistry.HeaderCachePathFor(table), - _difficultyTableRegistry.DataCachePathFor(table), - loaded => result = loaded); + DifficultyTableLibraryData refreshed = null; + yield return source.Refresh(loaded => refreshed = loaded); + var result = refreshed?.Catalog; if (scanGeneration != _libraryScanGeneration || !_libraryReady || _activePack == null || !string.Equals(_activePack.Id, sourcePack.Id, StringComparison.OrdinalIgnoreCase) || !string.Equals( @@ -1801,10 +1690,7 @@ namespace ProjectQuint.Bootstrap RefreshLibraryThumbnail(); } - var courseResult = AeryCourseIndex.LoadTableLocal( - table.Id, - result?.Data?.DisplayName ?? table.DisplayName, - _difficultyTableRegistry.HeaderCachePathFor(table)); + var courseResult = refreshed.Courses; if (courseResult.Data != null) { _courseIndex = courseResult.Data; @@ -1819,13 +1705,10 @@ namespace ProjectQuint.Bootstrap $"({result?.Origin ?? ExternalDataOrigin.Unavailable}) // " + $"{(_scanner?.CacheHits ?? 0):N0} cached // {(_scanner?.CacheMisses ?? 0):N0} refreshed"; Debug.Log($"[ProjectQuint] Difficulty table refresh: {result?.Status}; {_catalogMatchedCharts} chart matches; " + + $"{_libraryController.State.AllSongs.Count(song => song.KeyMode.HasConflict)} key mode conflicts (pack setting preserved); " + $"{availableCourses}/{totalCourses} courses from {courseResult.Origin}."); } - private static bool IsAeryTable(string url) => - !string.IsNullOrWhiteSpace(url) && - url.IndexOf("/aery/", StringComparison.OrdinalIgnoreCase) >= 0; - private void UpdateHomeInput() { var keyboard = Keyboard.current; @@ -2448,20 +2331,6 @@ namespace ProjectQuint.Bootstrap return -1; } - internal static int ResolvePackScanKeyCount( - BmsPackDefinition pack, - BmsDifficultyTableDefinition table) - { - if (pack?.ForcedKeyCount >= 4 && pack.ForcedKeyCount <= 7) - return pack.ForcedKeyCount; - var preset = BmsDifficultyTableIndex.FindPreset(table?.Id, table?.HeaderUrl); - return preset.HasValue && - preset.Value.DefaultKeyCount >= 4 && - preset.Value.DefaultKeyCount <= 7 - ? preset.Value.DefaultKeyCount - : 0; - } - private void RequestPackRemoval() { var packs = _packRegistry?.Packs; @@ -2595,10 +2464,10 @@ namespace ProjectQuint.Bootstrap _libraryRoot = string.Empty; _libraryLoadError = string.Empty; _activeCatalog = null; - _aeryCatalog = null; + _courseIndex = null; _searchAliases = SearchAliasIndex.Empty; - _aeryMatchedCharts = 0; + _catalogMatchedCharts = 0; ClearLoadedSongState(); _courseSession.ClearRun(); @@ -3152,10 +3021,10 @@ namespace ProjectQuint.Bootstrap keyboard.digit4Key.wasPressedThisFrame ? 3 : keyboard.digit5Key.wasPressedThisFrame ? 4 : keyboard.digit6Key.wasPressedThisFrame ? 5 : -1; - if (presetIndex >= 0 && presetIndex < BmsDifficultyTableIndex.Presets.Length) + if (presetIndex >= 0 && presetIndex < BmsDifficultyTablePresets.All.Length) { - _tableUrlDraft = BmsDifficultyTableIndex.Presets[presetIndex].HeaderUrl; - _tableSetupStatus = $"SELECTED // {BmsDifficultyTableIndex.Presets[presetIndex].DisplayName} // ENTER TO APPLY"; + _tableUrlDraft = BmsDifficultyTablePresets.All[presetIndex].HeaderUrl; + _tableSetupStatus = $"SELECTED // {BmsDifficultyTablePresets.All[presetIndex].DisplayName} // ENTER TO APPLY"; return; } if (keyboard.cKey.wasPressedThisFrame) @@ -3198,10 +3067,10 @@ namespace ProjectQuint.Bootstrap _tableUrlDraft = selected?.HeaderUrl ?? string.Empty; _tableSetupStatus = $"SELECTED // {selected?.DisplayName ?? "TABLE"} // ASSIGN TO PACK"; } - if (result.PresetIndex >= 0 && result.PresetIndex < BmsDifficultyTableIndex.Presets.Length) + if (result.PresetIndex >= 0 && result.PresetIndex < BmsDifficultyTablePresets.All.Length) { - _tableUrlDraft = BmsDifficultyTableIndex.Presets[result.PresetIndex].HeaderUrl; - _tableSetupStatus = $"SELECTED // {BmsDifficultyTableIndex.Presets[result.PresetIndex].DisplayName} // APPLY TO SAVE"; + _tableUrlDraft = BmsDifficultyTablePresets.All[result.PresetIndex].HeaderUrl; + _tableSetupStatus = $"SELECTED // {BmsDifficultyTablePresets.All[result.PresetIndex].DisplayName} // APPLY TO SAVE"; } if (result.Clear) { @@ -4639,11 +4508,13 @@ namespace ProjectQuint.Bootstrap } else { + var table = ResolvePackDifficultyTable(pack); var scanner = new SongLibraryScanner( _packRegistry.CachePathFor(pack), legacyCachePath: null, pack.Id, - pack.ForcedKeyCount); + pack.ForcedKeyCount, + SongKeyModePolicy.TableDefault(table?.Id, table?.HeaderUrl)); var scanTask = Task.Run(() => scanner.Scan(pack.ContentRoot, token), token); while (!scanTask.IsCompleted) { diff --git a/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs new file mode 100644 index 0000000..dd95cd9 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using ProjectQuint.Core.Bms; +using ProjectQuint.Core.Charts; +using ProjectQuint.Library; + +namespace ProjectQuint.Features.Loading +{ + internal sealed class PreparedChart + { + public PreparedChart( + PlayableChart chart, + IReadOnlyDictionary notesById, + int[] laneMap) + { + Chart = chart; + NotesById = notesById; + LaneMap = laneMap; + } + + public PlayableChart Chart { get; } + public IReadOnlyDictionary NotesById { get; } + public int[] LaneMap { get; } + } + + internal static class SongChartPreparation + { + public static PreparedChart Prepare( + SongLibraryEntry entry, + LaneModifier laneModifier, + int randomSeed) + { + if (entry == null) + throw new ArgumentNullException(nameof(entry)); + + var document = BmsParser.ParseFile( + entry.ChartPath, + forcedKeyCount: entry.EffectiveKeyCount); + var chart = BmsTimelineCompiler.Compile(document); + chart.Metadata.Md5 = entry.Metadata.Md5; + chart.Metadata.Sha256 = entry.Metadata.Sha256; + if (!string.IsNullOrWhiteSpace(entry.DisplayTitle)) + chart.Metadata.Title = entry.DisplayTitle; + if (!string.IsNullOrWhiteSpace(entry.DisplayArtist)) + chart.Metadata.Artist = entry.DisplayArtist; + + var conversion = BmsPlayableChartConverter.Convert(chart, laneModifier, randomSeed); + var playableChart = conversion.Chart; + playableChart.Metadata.DifficultyLabel = entry.DifficultyLabel; + var notesById = new Dictionary(playableChart.Notes.Count); + foreach (var note in playableChart.Notes) + notesById[note.Id] = note; + return new PreparedChart(playableChart, notesById, conversion.LaneMap); + } + } +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs.meta new file mode 100644 index 0000000..3c5288a --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongChartPreparation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a9656d1abb6d49fc84c4fc3313fcb726 diff --git a/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongLoadCoordinator.cs b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongLoadCoordinator.cs index 84290da..84509d3 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongLoadCoordinator.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Features/Loading/SongLoadCoordinator.cs @@ -109,7 +109,7 @@ namespace ProjectQuint.Features.Loading } progress?.Invoke(new SongLoadProgress("Parsing BMS timeline")); - var parseTask = Task.Run(() => ParseAndTransform( + var parseTask = Task.Run(() => SongChartPreparation.Prepare( request.Entry, request.LaneModifier, request.LaneRandomSeed)); @@ -294,34 +294,6 @@ namespace ProjectQuint.Features.Loading } } - internal static PreparedChart ParseAndTransform( - SongLibraryEntry entry, - LaneModifier laneModifier, - int randomSeed) - { - if (entry == null) - throw new ArgumentNullException(nameof(entry)); - - var document = BmsParser.ParseFile( - entry.ChartPath, - forcedKeyCount: entry.EffectiveKeyCount); - var chart = BmsTimelineCompiler.Compile(document); - chart.Metadata.Md5 = entry.Metadata.Md5; - chart.Metadata.Sha256 = entry.Metadata.Sha256; - if (!string.IsNullOrWhiteSpace(entry.DisplayTitle)) - chart.Metadata.Title = entry.DisplayTitle; - if (!string.IsNullOrWhiteSpace(entry.DisplayArtist)) - chart.Metadata.Artist = entry.DisplayArtist; - - var conversion = BmsPlayableChartConverter.Convert(chart, laneModifier, randomSeed); - var playableChart = conversion.Chart; - playableChart.Metadata.DifficultyLabel = entry.DifficultyLabel; - var notesById = new Dictionary(playableChart.Notes.Count); - foreach (var note in playableChart.Notes) - notesById[note.Id] = note; - return new PreparedChart(playableChart, notesById, conversion.LaneMap); - } - internal static bool ShouldCancel(int loadGeneration, int currentGeneration, bool isLoadingMode) => loadGeneration != currentGeneration || !isLoadingMode; @@ -333,6 +305,7 @@ namespace ProjectQuint.Features.Loading return $"[ProjectQuint] Playing {chart.Metadata.Title}: {chart.Notes.Count} notes, " + $"{chart.BackgroundAudioEvents.Count} BGM events, " + $"{ChartKeyMode.FormatLabel(chart.Metadata.KeyCount, chart.Metadata.HasScratch)}, " + + $"key mode source {request.Entry.KeyMode.Source}, table conflict {request.Entry.KeyMode.HasConflict}, " + $"{chart.ScratchObjectCount} scratch objects, " + $"{song.Audio.LoadedClipCount} clips loaded, {song.Audio.MissingClipCount} missing, " + $"{song.Audio.DecodeFailedClipCount} decode failed, " + @@ -386,22 +359,5 @@ namespace ProjectQuint.Features.Loading completed => { _ = completed.Exception; }, TaskContinuationOptions.OnlyOnFaulted); } - - internal sealed class PreparedChart - { - public PreparedChart( - PlayableChart chart, - IReadOnlyDictionary notesById, - int[] laneMap) - { - Chart = chart; - NotesById = notesById; - LaneMap = laneMap; - } - - public PlayableChart Chart { get; } - public IReadOnlyDictionary NotesById { get; } - public int[] LaneMap { get; } - } } } diff --git a/Assets/ProjectQuint/Runtime/Unity/Features/Profile/ProfileRatingContextBuilder.cs b/Assets/ProjectQuint/Runtime/Unity/Features/Profile/ProfileRatingContextBuilder.cs index 6d6a867..5c83f9c 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Features/Profile/ProfileRatingContextBuilder.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Features/Profile/ProfileRatingContextBuilder.cs @@ -55,7 +55,7 @@ namespace ProjectQuint.Features.Profile { if (table == null) continue; - var preset = BmsDifficultyTableIndex.FindPreset(table.Id, table.HeaderUrl); + var preset = BmsDifficultyTablePresets.Find(table.Id, table.HeaderUrl); if (preset?.DefaultKeyCount >= 4) AddResolved(table.Id, table.DisplayName, preset.Value.DefaultKeyCount, preset.Value.DefaultHasScratch); diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/AeryCourseIndex.cs b/Assets/ProjectQuint/Runtime/Unity/Library/AeryCourseIndex.cs index 5eebde9..463891c 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/AeryCourseIndex.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/AeryCourseIndex.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using ProjectQuint.Core.Gauge; @@ -9,7 +8,6 @@ namespace ProjectQuint.Library { public sealed class AeryCourseIndex { - public const string HeaderUrl = "https://asumatoki.kr/table/aery/header.json"; private readonly List _courses; private AeryCourseIndex( @@ -70,20 +68,6 @@ namespace ProjectQuint.Library } } - internal static ExternalDataResult LoadLocal( - string cachePath, - string bundledJson) - { - var result = ExternalJsonDataSource.LoadLocal( - "AERY courses", - cachePath, - bundledJson, - json => ParseValidated("aery", "AERY", json)); - result.Data ??= Empty("aery", "AERY"); - result.Data.Origin = result.Origin; - return result; - } - internal static ExternalDataResult LoadTableLocal( string providerId, string providerDisplayName, @@ -99,25 +83,6 @@ namespace ProjectQuint.Library return result; } - internal static IEnumerator LoadOnline( - string cachePath, - string bundledJson, - Action> completed) - { - yield return ExternalJsonDataSource.LoadOnline( - "AERY courses", - HeaderUrl, - cachePath, - bundledJson, - json => ParseValidated("aery", "AERY", json), - result => - { - result.Data ??= Empty("aery", "AERY"); - result.Data.Origin = result.Origin; - completed?.Invoke(result); - }); - } - internal static AeryCourseIndex Parse(string json) => Parse("aery", "AERY", json); diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs b/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs deleted file mode 100644 index 0f2e24a..0000000 --- a/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -namespace ProjectQuint.Library -{ - public sealed class AeryTableIndex : IChartCatalog - { - public const string DataUrl = "https://asumatoki.kr/table/aery/data.json"; - private static readonly string[] LevelOrder = - { - "LEVEL 1", "LEVEL 2", "LEVEL 3", "LEVEL 4", "LEVEL 5", "LEVEL 6", "LEVEL 7", "LEVEL 8", - "LEVEL 9", "LEVEL 10", "LEVEL 11", "LEVEL 12", "LEVEL 13", "LEVEL 14", "LEVEL 15", "LEVEL 15+", - "LEVEL 16", "LEVEL 16+", "LEVEL 17", "LEVEL 17+", "LEVEL 18", "LEVEL 18+", "LEVEL 19", "LEVEL 19+", - "LEVEL 20", "LEVEL 20+", "LEVEL 99", "LEVEL GIMMICK", "LEVEL SPECIAL", "LEVEL DUMMY", "OLD CHARTS", - }; - - private readonly Dictionary _entries; - - private AeryTableIndex(Dictionary entries) - { - _entries = entries ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public string ProviderId => "aery"; - public string DisplayName => "AERY"; - public ExternalDataOrigin Origin { get; private set; } - public int Count => _entries.Count; - - public bool TryGet(string chartHash, out ChartCatalogEntry entry) => - _entries.TryGetValue((chartHash ?? string.Empty).Trim(), out entry); - - internal static ExternalDataResult LoadLocal(string cachePath, string bundledJson) - { - var result = ExternalJsonDataSource.LoadLocal( - "AERY table", - cachePath, - bundledJson, - ParseValidated); - result.Data ??= Empty(); - result.Data.Origin = result.Origin; - return result; - } - - internal static IEnumerator LoadOnline( - string cachePath, - string bundledJson, - Action> completed) - { - yield return ExternalJsonDataSource.LoadOnline( - "AERY table", - DataUrl, - cachePath, - bundledJson, - ParseValidated, - result => - { - result.Data ??= Empty(); - result.Data.Origin = result.Origin; - completed?.Invoke(result); - }); - } - - internal static AeryTableIndex Parse(string json) => - ParseValidated(json) ?? Empty(); - - private static AeryTableIndex ParseValidated(string json) - { - var wrapper = JsonUtility.FromJson($"{{\"items\":{json}}}"); - if (wrapper?.items == null || wrapper.items.Length == 0) - return null; - - var levelOrder = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var index = 0; index < LevelOrder.Length; index++) - levelOrder[LevelOrder[index]] = index; - - var entries = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var item in wrapper.items) - { - if (item == null || string.IsNullOrWhiteSpace(item.md5)) - continue; - var level = item.level ?? string.Empty; - entries[item.md5.Trim()] = new ChartCatalogEntry - { - DifficultyLabel = level, - DifficultyValue = level, - DifficultySortIndex = levelOrder.TryGetValue(level, out var sortIndex) - ? sortIndex - : int.MaxValue, - TitleOverride = item.title ?? string.Empty, - ArtistOverride = item.artist ?? string.Empty, - }; - } - return entries.Count > 0 ? new AeryTableIndex(entries) : null; - } - - private static AeryTableIndex Empty() => - new AeryTableIndex(new Dictionary(StringComparer.OrdinalIgnoreCase)); - - [Serializable] - private sealed class AeryTableWrapper - { - public AeryTableEntry[] items; - } - - [Serializable] - private sealed class AeryTableEntry - { - public string level; - public string title; - public string artist; - public string md5; - } - } -} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs.meta deleted file mode 100644 index 791228c..0000000 --- a/Assets/ProjectQuint/Runtime/Unity/Library/AeryTableIndex.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: af68e030dde784779b15919621a9d264 \ No newline at end of file diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableIndex.cs b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableIndex.cs index ce419fd..74860c2 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableIndex.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableIndex.cs @@ -8,87 +8,8 @@ using UnityEngine.Networking; namespace ProjectQuint.Library { - public readonly struct BmsDifficultyTablePreset - { - public BmsDifficultyTablePreset( - string id, - string displayName, - string headerUrl, - int defaultKeyCount = 0, - bool defaultHasScratch = false) - { - Id = id; - DisplayName = displayName; - HeaderUrl = headerUrl; - DefaultKeyCount = defaultKeyCount; - DefaultHasScratch = defaultHasScratch; - } - - public string Id { get; } - public string DisplayName { get; } - public string HeaderUrl { get; } - public int DefaultKeyCount { get; } - public bool DefaultHasScratch { get; } - } - public sealed class BmsDifficultyTableIndex : IChartCatalog { - public static readonly BmsDifficultyTablePreset[] Presets = - { - new BmsDifficultyTablePreset( - "aery", - "5KEYS AERY", - "https://asumatoki.kr/table/aery/header.json", - 5), - new BmsDifficultyTablePreset( - "satellite", - "Satellite", - "https://stellabms.xyz/sl/header.json", - 7, - true), - new BmsDifficultyTablePreset( - "stella", - "Stella", - "https://stellabms.xyz/st/header.json", - 7, - true), - new BmsDifficultyTablePreset( - "genocide-insane", - "GENOCIDE Insane BMS (legacy mirror)", - "https://miraiscarlet.github.io/bms/table/genocide_insane/header_insane.json", - 7, - true), - new BmsDifficultyTablePreset( - "4ue", - "4UE Difficulty Table", - "https://classmaterma.github.io/4UE/header.json", - 4), - new BmsDifficultyTablePreset( - "6ue", - "6UE Difficulty Table", - "https://classmaterma.github.io/UE/table.html", - 6), - }; - - public static BmsDifficultyTablePreset? FindPreset(string tableId, string headerUrl = null) - { - foreach (var preset in Presets) - { - if (!string.IsNullOrWhiteSpace(tableId) && - string.Equals(preset.Id, tableId.Trim(), StringComparison.OrdinalIgnoreCase)) - return preset; - if (!string.IsNullOrWhiteSpace(headerUrl) && - string.Equals(preset.HeaderUrl, headerUrl.Trim(), StringComparison.OrdinalIgnoreCase)) - return preset; - } - return null; - } - - private static readonly Regex BmsTableMetaRegex = new Regex( - "]+name\\s*=\\s*[\\\"']bmstable[\\\"'][^>]+content\\s*=\\s*[\\\"'](?[^\\\"']+)[\\\"'][^>]*>|" + - "]+content\\s*=\\s*[\\\"'](?[^\\\"']+)[\\\"'][^>]+name\\s*=\\s*[\\\"']bmstable[\\\"'][^>]*>", - RegexOptions.IgnoreCase | RegexOptions.Compiled); - private readonly Dictionary _entries; private readonly int _chartCount; @@ -139,18 +60,13 @@ namespace ProjectQuint.Library } internal static IEnumerator LoadOnline( - string providerId, - string configuredUrl, - string headerCachePath, - string dataCachePath, + DifficultyTableRequest source, Action> completed) { - if (!Uri.TryCreate(configuredUrl, UriKind.Absolute, out var configuredUri) || - configuredUri.Scheme != Uri.UriSchemeHttps) - { - completed?.Invoke(Unavailable("Difficulty table URL must use HTTPS")); - yield break; - } + var configuredUri = source.HeaderUri; + var providerId = source.ProviderId; + var headerCachePath = source.HeaderCachePath; + var dataCachePath = source.DataCachePath; string firstDocument = null; string requestFailure = null; @@ -166,21 +82,17 @@ namespace ProjectQuint.Library var headerUrl = configuredUri.AbsoluteUri; var headerJson = firstDocument; - if (!string.IsNullOrWhiteSpace(firstDocument) && LooksLikeHtml(firstDocument)) + if (!string.IsNullOrWhiteSpace(firstDocument) && DifficultyTableRequest.LooksLikeHtml(firstDocument)) { - var match = BmsTableMetaRegex.Match(firstDocument); - if (!match.Success) + try { - completed?.Invoke(Unavailable("Table page has no bmstable header metadata")); + headerUrl = DifficultyTableRequest.ResolveHeaderPage(configuredUri, firstDocument).AbsoluteUri; + } + catch (Exception exception) + { + completed?.Invoke(Unavailable($"Difficulty table header URL invalid: {exception.Message}")); yield break; } - var resolvedHeaderUri = new Uri(configuredUri, match.Groups["url"].Value); - if (resolvedHeaderUri.Scheme != Uri.UriSchemeHttps) - { - completed?.Invoke(Unavailable("Difficulty table header URL must use HTTPS")); - yield break; - } - headerUrl = resolvedHeaderUri.AbsoluteUri; using var headerRequest = UnityWebRequest.Get(headerUrl); headerRequest.timeout = 12; yield return headerRequest.SendWebRequest(); @@ -211,10 +123,14 @@ namespace ProjectQuint.Library yield break; } - var dataUri = new Uri(new Uri(headerUrl), header.data_url); - if (dataUri.Scheme != Uri.UriSchemeHttps) + Uri dataUri; + try { - completed?.Invoke(Unavailable("Difficulty table data URL must use HTTPS")); + dataUri = DifficultyTableRequest.ResolveLink(new Uri(headerUrl), header.data_url); + } + catch (Exception exception) + { + completed?.Invoke(Unavailable($"Difficulty table data URL invalid: {exception.Message}")); yield break; } var dataUrl = dataUri.AbsoluteUri; @@ -346,9 +262,6 @@ namespace ProjectQuint.Library return symbol + level; } - private static bool LooksLikeHtml(string text) => - TrimBom(text).TrimStart().StartsWith("<", StringComparison.Ordinal); - private static string TrimBom(string text) => (text ?? string.Empty).TrimStart('\uFEFF'); private static ExternalDataResult Unavailable(string status) => diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs new file mode 100644 index 0000000..86ea81a --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs @@ -0,0 +1,82 @@ +using System; + +namespace ProjectQuint.Library +{ + public readonly struct BmsDifficultyTablePreset + { + public BmsDifficultyTablePreset( + string id, + string displayName, + string headerUrl, + int defaultKeyCount = 0, + bool defaultHasScratch = false) + { + Id = id; + DisplayName = displayName; + HeaderUrl = headerUrl; + DefaultKeyCount = defaultKeyCount; + DefaultHasScratch = defaultHasScratch; + } + + public string Id { get; } + public string DisplayName { get; } + public string HeaderUrl { get; } + public int DefaultKeyCount { get; } + public bool DefaultHasScratch { get; } + } + + public static class BmsDifficultyTablePresets + { + public static readonly BmsDifficultyTablePreset[] All = + { + new BmsDifficultyTablePreset( + "aery", + "5KEYS AERY", + "https://asumatoki.kr/table/aery/header.json", + 5), + new BmsDifficultyTablePreset( + "satellite", + "Satellite", + "https://stellabms.xyz/sl/header.json", + 7, + true), + new BmsDifficultyTablePreset( + "stella", + "Stella", + "https://stellabms.xyz/st/header.json", + 7, + true), + new BmsDifficultyTablePreset( + "genocide-insane", + "GENOCIDE Insane BMS (legacy mirror)", + "https://miraiscarlet.github.io/bms/table/genocide_insane/header_insane.json", + 7, + true), + new BmsDifficultyTablePreset( + "4ue", + "4UE Difficulty Table", + "https://classmaterma.github.io/4UE/header.json", + 4), + new BmsDifficultyTablePreset( + "6ue", + "6UE Difficulty Table", + "https://classmaterma.github.io/UE/table.html", + 6), + }; + + public static BmsDifficultyTablePreset? Find(string tableId, string headerUrl = null) + { + foreach (var preset in All) + { + if (!string.IsNullOrWhiteSpace(tableId) && + string.Equals(preset.Id, tableId.Trim(), StringComparison.OrdinalIgnoreCase)) + return preset; + if (!string.IsNullOrWhiteSpace(headerUrl) && + string.Equals(preset.HeaderUrl, headerUrl.Trim(), StringComparison.OrdinalIgnoreCase)) + return preset; + } + return null; + } + + } +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs.meta new file mode 100644 index 0000000..1d0cc40 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTablePresets.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4efa4bcb5efb47c6bc5c1115fc16903a diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableRegistry.cs b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableRegistry.cs index ba557d4..b42c638 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableRegistry.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/BmsDifficultyTableRegistry.cs @@ -102,9 +102,9 @@ namespace ProjectQuint.Library public BmsDifficultyTableDefinition RegisterPreset(int presetIndex, bool activate = true) { - if (presetIndex < 0 || presetIndex >= BmsDifficultyTableIndex.Presets.Length) + if (presetIndex < 0 || presetIndex >= BmsDifficultyTablePresets.All.Length) throw new ArgumentOutOfRangeException(nameof(presetIndex)); - var preset = BmsDifficultyTableIndex.Presets[presetIndex]; + var preset = BmsDifficultyTablePresets.All[presetIndex]; return Register(preset.HeaderUrl, preset.DisplayName, preset.Id, activate); } @@ -223,7 +223,7 @@ namespace ProjectQuint.Library private void EnsurePresets() { var changed = false; - foreach (var preset in BmsDifficultyTableIndex.Presets) + foreach (var preset in BmsDifficultyTablePresets.All) { if (FindByUrl(preset.HeaderUrl) != null) continue; @@ -308,7 +308,7 @@ namespace ProjectQuint.Library private static BmsDifficultyTablePreset? FindPresetByUrl(string url) { - foreach (var preset in BmsDifficultyTableIndex.Presets) + foreach (var preset in BmsDifficultyTablePresets.All) if (string.Equals(preset.HeaderUrl, url, StringComparison.OrdinalIgnoreCase)) return preset; return null; diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/ChartCatalog.cs b/Assets/ProjectQuint/Runtime/Unity/Library/ChartCatalog.cs index 37abae0..b3572ba 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/ChartCatalog.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/ChartCatalog.cs @@ -41,7 +41,7 @@ namespace ProjectQuint.Library public int DifficultySortIndex { get; set; } = int.MaxValue; public string TitleOverride { get; set; } = string.Empty; public string ArtistOverride { get; set; } = string.Empty; - public int KeyCountOverride { get; set; } + public int DefaultKeyCount { get; set; } public bool HasDifficulty => !string.IsNullOrWhiteSpace(DifficultyLabel); } @@ -53,10 +53,9 @@ namespace ProjectQuint.Library var matched = 0; var preset = catalog == null ? null - : BmsDifficultyTableIndex.FindPreset(catalog.ProviderId); - var keyCountOverride = preset.HasValue && - preset.Value.DefaultKeyCount >= 4 && - preset.Value.DefaultKeyCount <= 7 + : BmsDifficultyTablePresets.Find(catalog.ProviderId); + var defaultKeyCount = preset.HasValue && + SongKeyModePolicy.IsSupported(preset.Value.DefaultKeyCount) ? preset.Value.DefaultKeyCount : 0; foreach (var song in songs ?? Array.Empty()) @@ -82,7 +81,7 @@ namespace ProjectQuint.Library DifficultySortIndex = entry.DifficultySortIndex, TitleOverride = entry.TitleOverride ?? string.Empty, ArtistOverride = entry.ArtistOverride ?? string.Empty, - KeyCountOverride = keyCountOverride, + DefaultKeyCount = defaultKeyCount, }; matched++; } diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs new file mode 100644 index 0000000..ee5ec60 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections; + +namespace ProjectQuint.Library +{ + internal sealed class DifficultyTableLibraryData + { + public ExternalDataResult Catalog { get; set; } + public ExternalDataResult Courses { get; set; } + } + + internal sealed class DifficultyTableLibrarySource + { + private readonly string _providerId; + private readonly string _displayName; + private readonly string _headerUrl; + private readonly string _headerCachePath; + private readonly string _dataCachePath; + + public DifficultyTableLibrarySource(BmsDifficultyTableDefinition table, BmsDifficultyTableRegistry registry) + { + // Snapshot registration values so a later selection cannot redirect an in-flight load. + _providerId = table?.Id ?? string.Empty; + _displayName = table?.DisplayName ?? string.Empty; + _headerUrl = table?.HeaderUrl ?? string.Empty; + _headerCachePath = table == null ? string.Empty : registry.HeaderCachePathFor(table); + _dataCachePath = table == null ? string.Empty : registry.DataCachePathFor(table); + } + + public bool IsConfigured => !string.IsNullOrWhiteSpace(_headerUrl); + + public DifficultyTableLibraryData LoadLocal() + { + var catalog = IsConfigured + ? BmsDifficultyTableIndex.LoadLocal(_providerId, _headerCachePath, _dataCachePath) + : new ExternalDataResult { Status = "No difficulty table configured" }; + return WithCourses(catalog); + } + + public IEnumerator Refresh(Action completed) + { + DifficultyTableRequest request; + try + { + request = new DifficultyTableRequest(_providerId, _headerUrl, _headerCachePath, _dataCachePath); + } + catch (ArgumentException exception) + { + completed?.Invoke(WithCourses(new ExternalDataResult { Status = exception.Message })); + yield break; + } + ExternalDataResult catalog = null; + yield return BmsDifficultyTableIndex.LoadOnline(request, result => catalog = result); + catalog ??= new ExternalDataResult + { + Status = "Difficulty table request did not complete", + }; + completed?.Invoke(WithCourses(catalog)); + } + + private DifficultyTableLibraryData WithCourses(ExternalDataResult catalog) + { + return new DifficultyTableLibraryData + { + Catalog = catalog, + Courses = IsConfigured + ? AeryCourseIndex.LoadTableLocal(_providerId, catalog.Data?.DisplayName ?? _displayName, _headerCachePath) + : new ExternalDataResult { Status = "NO BMSTABLE COURSE DATA CONFIGURED" }, + }; + } + } +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs.meta new file mode 100644 index 0000000..075a2c5 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableLibrarySource.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8573e06b88064463a2d2ac0dd6f4bc9c diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs new file mode 100644 index 0000000..b20815a --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs @@ -0,0 +1,53 @@ +using System; +using System.Net; +using System.Text.RegularExpressions; + +namespace ProjectQuint.Library +{ + internal sealed class DifficultyTableRequest + { + private static readonly Regex HeaderMeta = new Regex( + "]+name\\s*=\\s*[\"']bmstable[\"'][^>]+content\\s*=\\s*[\"'](?[^\"']+)[\"'][^>]*>|" + + "]+content\\s*=\\s*[\"'](?[^\"']+)[\"'][^>]+name\\s*=\\s*[\"']bmstable[\"'][^>]*>", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public DifficultyTableRequest(string providerId, string headerUrl, + string headerCachePath, string dataCachePath) + { + ProviderId = providerId ?? string.Empty; + HeaderUri = RequireHttps(headerUrl); + HeaderCachePath = headerCachePath ?? string.Empty; + DataCachePath = dataCachePath ?? string.Empty; + } + + public string ProviderId { get; } + public Uri HeaderUri { get; } + public string HeaderCachePath { get; } + public string DataCachePath { get; } + + public static bool LooksLikeHtml(string text) => + (text ?? string.Empty).TrimStart('\uFEFF').TrimStart().StartsWith("<", StringComparison.Ordinal); + + public static Uri ResolveHeaderPage(Uri pageUri, string document) + { + var match = HeaderMeta.Match(document ?? string.Empty); + if (!match.Success) + throw new FormatException("Table page has no bmstable header metadata"); + return ResolveLink(pageUri, WebUtility.HtmlDecode(match.Groups["url"].Value)); + } + + public static Uri ResolveLink(Uri documentUri, string relativeUrl) + { + if (string.IsNullOrWhiteSpace(relativeUrl)) + throw new FormatException("Difficulty table document URL is missing"); + return RequireHttps(new Uri(documentUri, relativeUrl).AbsoluteUri); + } + + private static Uri RequireHttps(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || uri.Scheme != Uri.UriSchemeHttps) + throw new ArgumentException("Difficulty table URL must use HTTPS", nameof(url)); + return uri; + } + } +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs.meta new file mode 100644 index 0000000..21bc3bb --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/DifficultyTableRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6e7de5e563944400971cfcf8f54705b7 diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs new file mode 100644 index 0000000..75e4a68 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs @@ -0,0 +1,48 @@ +namespace ProjectQuint.Library +{ + public enum SongKeyModeSource + { + Chart, + DifficultyTable, + Pack, + } + + public readonly struct SongKeyModeResolution + { + public SongKeyModeResolution(int keyCount, SongKeyModeSource source, bool hasConflict) + { + KeyCount = keyCount; + Source = source; + HasConflict = hasConflict; + } + + public int KeyCount { get; } + public SongKeyModeSource Source { get; } + public bool HasConflict { get; } + } + + public static class SongKeyModePolicy + { + public static bool IsSupported(int keyCount) => keyCount >= 4 && keyCount <= 7; + + public static SongKeyModeResolution Resolve(int forcedKeyCount, int tableKeyCount, int chartKeyCount) + { + var conflict = IsSupported(forcedKeyCount) && IsSupported(tableKeyCount) && + forcedKeyCount != tableKeyCount; + // A table describes its expected layout; only the pack setting is an explicit user choice. + if (IsSupported(forcedKeyCount)) + return new SongKeyModeResolution(forcedKeyCount, SongKeyModeSource.Pack, conflict); + if (IsSupported(tableKeyCount)) + return new SongKeyModeResolution(tableKeyCount, SongKeyModeSource.DifficultyTable, conflict); + return new SongKeyModeResolution(chartKeyCount, SongKeyModeSource.Chart, conflict); + } + + public static int TableDefault(string tableId, string headerUrl) + { + var preset = BmsDifficultyTablePresets.Find(tableId, headerUrl); + return preset.HasValue && IsSupported(preset.Value.DefaultKeyCount) + ? preset.Value.DefaultKeyCount + : 0; + } + } +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs.meta new file mode 100644 index 0000000..b730c82 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongKeyModePolicy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 49eebe4a4a024c509a8838c4bae27cee diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs new file mode 100644 index 0000000..95f0d1f --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs @@ -0,0 +1,72 @@ +using System; +using ProjectQuint.Core.Analysis; +using ProjectQuint.Core.Bms; + +namespace ProjectQuint.Library +{ + public enum SongClearLamp + { + None = 0, + NormalClear = 1, + HardClear = 2, + Failed = 3, + NormalFailed = 4, + ExHardClear = 5, + AutoScratchClear = 6, + } + + public sealed class SongLibraryEntry + { + public string ChartPath { get; set; } = string.Empty; + public string PackId { get; set; } = string.Empty; + public int ForcedKeyCount { get; set; } + public int TableDefaultKeyCount { get; set; } + public string ThumbnailFile { get; set; } = string.Empty; + public BmsMetadata Metadata { get; set; } = new BmsMetadata(); + public ChartCatalogAnnotation Catalog { get; set; } = new ChartCatalogAnnotation(); + public SongClearLamp ClearLamp { get; set; } + public SongPlayRecord BestRecord { get; set; } = new SongPlayRecord(); + public bool IsFavorite { get; set; } + public double DurationSeconds { get; set; } + public bool DurationIsEstimated { get; set; } + public ChartFeatureVector Features { get; set; } + public string FeatureAnalysisError { get; set; } = string.Empty; + public string SearchIndex { get; private set; } = string.Empty; + internal SongSearchDocument SearchDocument { get; private set; } = SongSearchDocument.Empty; + + public string DisplayTitle => !string.IsNullOrWhiteSpace(Catalog?.TitleOverride) + ? Catalog.TitleOverride + : Metadata?.Title ?? string.Empty; + public string DisplayArtist => !string.IsNullOrWhiteSpace(Catalog?.ArtistOverride) + ? Catalog.ArtistOverride + : Metadata?.Artist ?? string.Empty; + public string DifficultyLabel => Catalog?.HasDifficulty == true + ? Catalog.DifficultyLabel + : Metadata?.PlayLevel > 0 ? $"BMS LEVEL {Metadata.PlayLevel}" : string.Empty; + public string CompactDifficultyLabel => Catalog?.HasDifficulty == true + ? !string.IsNullOrWhiteSpace(Catalog.DifficultyValue) + ? Catalog.DifficultyValue + : Catalog.DifficultyLabel + : Metadata?.PlayLevel > 0 ? Metadata.PlayLevel.ToString() : string.Empty; + public int DifficultySortIndex => Catalog?.HasDifficulty == true + ? Catalog.DifficultySortIndex + : Metadata?.PlayLevel ?? int.MaxValue; + public bool HasCatalogDifficulty => Catalog?.HasDifficulty == true; + public string DifficultySourceLabel => HasCatalogDifficulty + ? Catalog.ProviderDisplayName + : Metadata?.PlayLevel > 0 ? "BMS PLAYLEVEL" : "UNRANKED"; + public SongKeyModeResolution KeyMode => SongKeyModePolicy.Resolve( + ForcedKeyCount, + SongKeyModePolicy.IsSupported(Catalog?.DefaultKeyCount ?? 0) + ? Catalog.DefaultKeyCount : TableDefaultKeyCount, + Metadata?.KeyCount ?? 5); + public int EffectiveKeyCount => KeyMode.KeyCount; + + internal void UpdateSearchDocument(SongSearchDocument document) + { + SearchDocument = document ?? SongSearchDocument.Empty; + SearchIndex = SearchDocument.CombinedIndex; + } + } + +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs.meta new file mode 100644 index 0000000..1a2730f --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ef4096d016784c3cb487f3a803ee5ee8 diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryScanner.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryScanner.cs index 57b2dd1..806a890 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryScanner.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongLibraryScanner.cs @@ -11,67 +11,6 @@ using UnityEngine; namespace ProjectQuint.Library { - public enum SongClearLamp - { - None = 0, - NormalClear = 1, - HardClear = 2, - Failed = 3, - NormalFailed = 4, - ExHardClear = 5, - AutoScratchClear = 6, - } - - public sealed class SongLibraryEntry - { - public string ChartPath { get; set; } = string.Empty; - public string PackId { get; set; } = string.Empty; - public string ThumbnailFile { get; set; } = string.Empty; - public BmsMetadata Metadata { get; set; } = new BmsMetadata(); - public ChartCatalogAnnotation Catalog { get; set; } = new ChartCatalogAnnotation(); - public SongClearLamp ClearLamp { get; set; } - public SongPlayRecord BestRecord { get; set; } = new SongPlayRecord(); - public bool IsFavorite { get; set; } - public double DurationSeconds { get; set; } - public bool DurationIsEstimated { get; set; } - public ChartFeatureVector Features { get; set; } - public string FeatureAnalysisError { get; set; } = string.Empty; - public string SearchIndex { get; private set; } = string.Empty; - internal SongSearchDocument SearchDocument { get; private set; } = SongSearchDocument.Empty; - - public string DisplayTitle => !string.IsNullOrWhiteSpace(Catalog?.TitleOverride) - ? Catalog.TitleOverride - : Metadata?.Title ?? string.Empty; - public string DisplayArtist => !string.IsNullOrWhiteSpace(Catalog?.ArtistOverride) - ? Catalog.ArtistOverride - : Metadata?.Artist ?? string.Empty; - public string DifficultyLabel => Catalog?.HasDifficulty == true - ? Catalog.DifficultyLabel - : Metadata?.PlayLevel > 0 ? $"BMS LEVEL {Metadata.PlayLevel}" : string.Empty; - public string CompactDifficultyLabel => Catalog?.HasDifficulty == true - ? !string.IsNullOrWhiteSpace(Catalog.DifficultyValue) - ? Catalog.DifficultyValue - : Catalog.DifficultyLabel - : Metadata?.PlayLevel > 0 ? Metadata.PlayLevel.ToString() : string.Empty; - public int DifficultySortIndex => Catalog?.HasDifficulty == true - ? Catalog.DifficultySortIndex - : Metadata?.PlayLevel ?? int.MaxValue; - public bool HasCatalogDifficulty => Catalog?.HasDifficulty == true; - public string DifficultySourceLabel => HasCatalogDifficulty - ? Catalog.ProviderDisplayName - : Metadata?.PlayLevel > 0 ? "BMS PLAYLEVEL" : "UNRANKED"; - public int EffectiveKeyCount => - Catalog?.KeyCountOverride >= 4 && Catalog.KeyCountOverride <= 7 - ? Catalog.KeyCountOverride - : Metadata?.KeyCount ?? 5; - - internal void RebuildSearchIndex(SearchAliasIndex aliases = null) - { - SearchDocument = SongSearchIndexer.Build(this, aliases); - SearchIndex = SearchDocument.CombinedIndex; - } - } - public sealed class SongLibraryScanner { private const int CacheVersion = 6; @@ -103,18 +42,23 @@ namespace ProjectQuint.Library private readonly string _legacyCachePath; private readonly string _packId; private readonly int _forcedKeyCount; + private readonly int _tableDefaultKeyCount; + private readonly int _scanKeyCount; private int _loadedCacheVersion; public SongLibraryScanner( string cachePath = null, string legacyCachePath = null, string packId = null, - int forcedKeyCount = 0) + int forcedKeyCount = 0, + int tableDefaultKeyCount = 0) { _cachePath = cachePath; _legacyCachePath = legacyCachePath; _packId = packId ?? string.Empty; _forcedKeyCount = forcedKeyCount; + _tableDefaultKeyCount = tableDefaultKeyCount; + _scanKeyCount = SongKeyModePolicy.Resolve(forcedKeyCount, tableDefaultKeyCount, 0).KeyCount; } public int Completed => Volatile.Read(ref _completed); @@ -240,7 +184,7 @@ namespace ProjectQuint.Library { var thumbnailDocument = BmsParser.ParseFile( fullPath, - forcedKeyCount: _forcedKeyCount); + forcedKeyCount: _scanKeyCount); var thumbnailChart = BmsTimelineCompiler.Compile(thumbnailDocument); thumbnailFile = SelectThumbnailFile( fullPath, @@ -273,7 +217,7 @@ namespace ProjectQuint.Library } else { - var document = BmsParser.ParseFile(fullPath, forcedKeyCount: _forcedKeyCount); + var document = BmsParser.ParseFile(fullPath, forcedKeyCount: _scanKeyCount); rawMetadata = document.Metadata; var hashes = ComputeHashes(fullPath); rawMetadata.Md5 = hashes.Md5; @@ -318,6 +262,8 @@ namespace ProjectQuint.Library { ChartPath = path, PackId = _packId, + ForcedKeyCount = _forcedKeyCount, + TableDefaultKeyCount = _tableDefaultKeyCount, ThumbnailFile = thumbnailFile, Metadata = CloneMetadata(rawMetadata), DurationSeconds = durationSeconds, @@ -369,7 +315,7 @@ namespace ProjectQuint.Library if (document == null || document.Version < 1 || document.Version > CacheVersion || !string.Equals(Path.GetFullPath(document.LibraryRoot ?? string.Empty), libraryRoot, StringComparison.OrdinalIgnoreCase) || - document.ForcedKeyCount != _forcedKeyCount) + document.ForcedKeyCount != _scanKeyCount) { CacheStatus = "cache version, library root or key mode changed"; return result; @@ -409,7 +355,7 @@ namespace ProjectQuint.Library { Version = CacheVersion, LibraryRoot = libraryRoot, - ForcedKeyCount = _forcedKeyCount, + ForcedKeyCount = _scanKeyCount, Entries = entries.ToArray(), }; var temporaryPath = _cachePath + ".tmp"; diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs new file mode 100644 index 0000000..83523dc --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + +namespace ProjectQuint.Library +{ + [Serializable] + public sealed class RatingPlayRecord + { + public bool HasRecord; + public string EventId = string.Empty; + public string ProviderId = string.Empty; + public string ProviderDisplayName = string.Empty; + public string DifficultyLabel = string.Empty; + public string DifficultyValue = string.Empty; + public string Title = string.Empty; + public string Artist = string.Empty; + public double ChartConstant; + public int KeyCount; + public bool HasScratch; + public string LaneModifier = string.Empty; + public bool HardClear; + public double AccuracyPercent; + public double PlayRating; + public int PlayCount; + public int RatingRulesVersion; + public long RecordedUtcTicks; + } + + [Serializable] + public sealed class SongPlayRecord + { + public bool HasRecord; + public int BestScore; + public double BestAccuracyPercent; + public int BestMaxCombo; + public bool HasBestScoreJudgementBreakdown; + public int BestScoreKoolCount; + public int BestScoreCoolCount; + public int BestScoreGoodCount; + public int BestScoreMissCount; + public int BestScorePurePerfectCount; + public int PlayCount; + public bool HasJudgementBreakdown; + public int LastScore; + public double LastAccuracyPercent; + public int LastMaxCombo; + public int LastKoolCount; + public int LastCoolCount; + public int LastGoodCount; + public int LastMissCount; + public int LastFastCount; + public int LastLateCount; + public string LastAppliedEventId = string.Empty; + public int LastPurePerfectCount; + public int LastJudgedUnits; + public int LastTotalScoringUnits; + public bool LastCleared; + public bool LastHardClear; + public bool EverFullCombo; + public bool EverAllPure; + public RatingPlayRecord BestRating = new RatingPlayRecord(); + public List BestRatings = new List(); + } + +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs.meta new file mode 100644 index 0000000..05487d9 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecord.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6ea5022d2731468f8d67c43e8e13d95b diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecordStore.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecordStore.cs index c592100..819b6ee 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecordStore.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongPlayRecordStore.cs @@ -5,65 +5,6 @@ using ProjectQuint.Persistence; namespace ProjectQuint.Library { - [Serializable] - public sealed class RatingPlayRecord - { - public bool HasRecord; - public string EventId = string.Empty; - public string ProviderId = string.Empty; - public string ProviderDisplayName = string.Empty; - public string DifficultyLabel = string.Empty; - public string DifficultyValue = string.Empty; - public string Title = string.Empty; - public string Artist = string.Empty; - public double ChartConstant; - public int KeyCount; - public bool HasScratch; - public string LaneModifier = string.Empty; - public bool HardClear; - public double AccuracyPercent; - public double PlayRating; - public int PlayCount; - public int RatingRulesVersion; - public long RecordedUtcTicks; - } - - [Serializable] - public sealed class SongPlayRecord - { - public bool HasRecord; - public int BestScore; - public double BestAccuracyPercent; - public int BestMaxCombo; - public bool HasBestScoreJudgementBreakdown; - public int BestScoreKoolCount; - public int BestScoreCoolCount; - public int BestScoreGoodCount; - public int BestScoreMissCount; - public int BestScorePurePerfectCount; - public int PlayCount; - public bool HasJudgementBreakdown; - public int LastScore; - public double LastAccuracyPercent; - public int LastMaxCombo; - public int LastKoolCount; - public int LastCoolCount; - public int LastGoodCount; - public int LastMissCount; - public int LastFastCount; - public int LastLateCount; - public string LastAppliedEventId = string.Empty; - public int LastPurePerfectCount; - public int LastJudgedUnits; - public int LastTotalScoringUnits; - public bool LastCleared; - public bool LastHardClear; - public bool EverFullCombo; - public bool EverAllPure; - public RatingPlayRecord BestRating = new RatingPlayRecord(); - public List BestRatings = new List(); - } - internal readonly struct SongPlayResult { public SongPlayResult( diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs new file mode 100644 index 0000000..85c11fb --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; + +namespace ProjectQuint.Library +{ + internal sealed class SongSearchDocument + { + public static SongSearchDocument Empty { get; } = new SongSearchDocument( + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty()); + + public SongSearchDocument( + string[] title, + string[] artist, + string[] fileName, + string[] aliases) + { + Title = title ?? Array.Empty(); + Artist = artist ?? Array.Empty(); + FileName = fileName ?? Array.Empty(); + Aliases = aliases ?? Array.Empty(); + CombinedIndex = string.Join("\n", Title + .Concat(Artist) + .Concat(FileName) + .Concat(Aliases) + .Distinct(StringComparer.Ordinal) + .OrderBy(token => token, StringComparer.Ordinal)); + } + + public string[] Title { get; } + public string[] Artist { get; } + public string[] FileName { get; } + public string[] Aliases { get; } + public string CombinedIndex { get; } + } + +} diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs.meta b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs.meta new file mode 100644 index 0000000..e6f8d88 --- /dev/null +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ad783a34277242ca96d0c370d76aeebe diff --git a/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchIndex.cs b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchIndex.cs index 3eb0872..00b16df 100644 --- a/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchIndex.cs +++ b/Assets/ProjectQuint/Runtime/Unity/Library/SongSearchIndex.cs @@ -6,39 +6,6 @@ using UnityEngine; namespace ProjectQuint.Library { - internal sealed class SongSearchDocument - { - public static SongSearchDocument Empty { get; } = new SongSearchDocument( - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty()); - - public SongSearchDocument( - string[] title, - string[] artist, - string[] fileName, - string[] aliases) - { - Title = title ?? Array.Empty(); - Artist = artist ?? Array.Empty(); - FileName = fileName ?? Array.Empty(); - Aliases = aliases ?? Array.Empty(); - CombinedIndex = string.Join("\n", Title - .Concat(Artist) - .Concat(FileName) - .Concat(Aliases) - .Distinct(StringComparer.Ordinal) - .OrderBy(token => token, StringComparer.Ordinal)); - } - - public string[] Title { get; } - public string[] Artist { get; } - public string[] FileName { get; } - public string[] Aliases { get; } - public string CombinedIndex { get; } - } - [Serializable] internal sealed class SearchAliasDocument { @@ -152,6 +119,11 @@ namespace ProjectQuint.Library internal static class SongSearchIndexer { + public static void RebuildSearchIndex(this SongLibraryEntry song, SearchAliasIndex aliases = null) + { + song?.UpdateSearchDocument(Build(song, aliases)); + } + public static void Rebuild(IReadOnlyList songs, SearchAliasIndex aliases = null) { foreach (var song in songs ?? Array.Empty()) diff --git a/Assets/ProjectQuint/Runtime/Unity/UI/PackMusicScreenPresenter.cs b/Assets/ProjectQuint/Runtime/Unity/UI/PackMusicScreenPresenter.cs index 8129284..b4d829c 100644 --- a/Assets/ProjectQuint/Runtime/Unity/UI/PackMusicScreenPresenter.cs +++ b/Assets/ProjectQuint/Runtime/Unity/UI/PackMusicScreenPresenter.cs @@ -142,7 +142,7 @@ namespace ProjectQuint.UI private readonly Label _tableStatus; private readonly TextField _tableUrlField; private readonly VisualElement[] _tablePresetButtons = - new VisualElement[BmsDifficultyTableIndex.Presets.Length]; + new VisualElement[BmsDifficultyTablePresets.All.Length]; private string _activeTableId = string.Empty; private string _tableUrl = string.Empty; private bool _tableUrlFocused; @@ -1437,7 +1437,7 @@ namespace ProjectQuint.UI break; } - var preset = BmsDifficultyTableIndex.FindPreset(table?.Id, table?.HeaderUrl); + var preset = BmsDifficultyTablePresets.Find(table?.Id, table?.HeaderUrl); if (!preset.HasValue || preset.Value.DefaultKeyCount < 4 || preset.Value.DefaultKeyCount > 7) @@ -1462,7 +1462,7 @@ namespace ProjectQuint.UI break; } - var preset = BmsDifficultyTableIndex.FindPreset(table?.Id, table?.HeaderUrl); + var preset = BmsDifficultyTablePresets.Find(table?.Id, table?.HeaderUrl); return preset.HasValue && preset.Value.DefaultKeyCount >= 4 && preset.Value.DefaultKeyCount <= 7 @@ -1500,7 +1500,7 @@ namespace ProjectQuint.UI }; return string.IsNullOrWhiteSpace(presetId) ? null - : BmsDifficultyTableIndex.FindPreset(presetId); + : BmsDifficultyTablePresets.Find(presetId); } private void ConfigureTableList() @@ -1594,8 +1594,8 @@ namespace ProjectQuint.UI var capturedIndex = index; var button = Require(_tableRoot, $"table-preset-{index}"); _tablePresetButtons[index] = button; - var preset = index < BmsDifficultyTableIndex.Presets.Length - ? BmsDifficultyTableIndex.Presets[index] + var preset = index < BmsDifficultyTablePresets.All.Length + ? BmsDifficultyTablePresets.All[index] : default; SetText( Require