난이도표 로딩 통합과 팩 키 모드 우선순위 정리
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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<TextAsset>("ProjectQuint/Data/AeryData");
|
||||
var bundledCourseHeader = Resources.Load<TextAsset>("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<TextAsset>("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<AeryTableIndex> 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<AeryCourseIndex> 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<BmsDifficultyTableIndex> 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)
|
||||
{
|
||||
|
||||
@@ -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<int, PlayableNote> notesById,
|
||||
int[] laneMap)
|
||||
{
|
||||
Chart = chart;
|
||||
NotesById = notesById;
|
||||
LaneMap = laneMap;
|
||||
}
|
||||
|
||||
public PlayableChart Chart { get; }
|
||||
public IReadOnlyDictionary<int, PlayableNote> 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<int, PlayableNote>(playableChart.Notes.Count);
|
||||
foreach (var note in playableChart.Notes)
|
||||
notesById[note.Id] = note;
|
||||
return new PreparedChart(playableChart, notesById, conversion.LaneMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9656d1abb6d49fc84c4fc3313fcb726
|
||||
@@ -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<int, PlayableNote>(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<int, PlayableNote> notesById,
|
||||
int[] laneMap)
|
||||
{
|
||||
Chart = chart;
|
||||
NotesById = notesById;
|
||||
LaneMap = laneMap;
|
||||
}
|
||||
|
||||
public PlayableChart Chart { get; }
|
||||
public IReadOnlyDictionary<int, PlayableNote> NotesById { get; }
|
||||
public int[] LaneMap { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<AeryCourseDefinition> _courses;
|
||||
|
||||
private AeryCourseIndex(
|
||||
@@ -70,20 +68,6 @@ namespace ProjectQuint.Library
|
||||
}
|
||||
}
|
||||
|
||||
internal static ExternalDataResult<AeryCourseIndex> 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<AeryCourseIndex> LoadTableLocal(
|
||||
string providerId,
|
||||
string providerDisplayName,
|
||||
@@ -99,25 +83,6 @@ namespace ProjectQuint.Library
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static IEnumerator LoadOnline(
|
||||
string cachePath,
|
||||
string bundledJson,
|
||||
Action<ExternalDataResult<AeryCourseIndex>> 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);
|
||||
|
||||
|
||||
@@ -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<string, ChartCatalogEntry> _entries;
|
||||
|
||||
private AeryTableIndex(Dictionary<string, ChartCatalogEntry> entries)
|
||||
{
|
||||
_entries = entries ?? new Dictionary<string, ChartCatalogEntry>(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<AeryTableIndex> 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<ExternalDataResult<AeryTableIndex>> 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<AeryTableWrapper>($"{{\"items\":{json}}}");
|
||||
if (wrapper?.items == null || wrapper.items.Length == 0)
|
||||
return null;
|
||||
|
||||
var levelOrder = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
for (var index = 0; index < LevelOrder.Length; index++)
|
||||
levelOrder[LevelOrder[index]] = index;
|
||||
|
||||
var entries = new Dictionary<string, ChartCatalogEntry>(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<string, ChartCatalogEntry>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af68e030dde784779b15919621a9d264
|
||||
@@ -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(
|
||||
"<meta[^>]+name\\s*=\\s*[\\\"']bmstable[\\\"'][^>]+content\\s*=\\s*[\\\"'](?<url>[^\\\"']+)[\\\"'][^>]*>|" +
|
||||
"<meta[^>]+content\\s*=\\s*[\\\"'](?<url>[^\\\"']+)[\\\"'][^>]+name\\s*=\\s*[\\\"']bmstable[\\\"'][^>]*>",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private readonly Dictionary<string, ChartCatalogEntry> _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<ExternalDataResult<BmsDifficultyTableIndex>> 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<BmsDifficultyTableIndex> Unavailable(string status) =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4efa4bcb5efb47c6bc5c1115fc16903a
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SongLibraryEntry>())
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
|
||||
namespace ProjectQuint.Library
|
||||
{
|
||||
internal sealed class DifficultyTableLibraryData
|
||||
{
|
||||
public ExternalDataResult<BmsDifficultyTableIndex> Catalog { get; set; }
|
||||
public ExternalDataResult<AeryCourseIndex> 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<BmsDifficultyTableIndex> { Status = "No difficulty table configured" };
|
||||
return WithCourses(catalog);
|
||||
}
|
||||
|
||||
public IEnumerator Refresh(Action<DifficultyTableLibraryData> completed)
|
||||
{
|
||||
DifficultyTableRequest request;
|
||||
try
|
||||
{
|
||||
request = new DifficultyTableRequest(_providerId, _headerUrl, _headerCachePath, _dataCachePath);
|
||||
}
|
||||
catch (ArgumentException exception)
|
||||
{
|
||||
completed?.Invoke(WithCourses(new ExternalDataResult<BmsDifficultyTableIndex> { Status = exception.Message }));
|
||||
yield break;
|
||||
}
|
||||
ExternalDataResult<BmsDifficultyTableIndex> catalog = null;
|
||||
yield return BmsDifficultyTableIndex.LoadOnline(request, result => catalog = result);
|
||||
catalog ??= new ExternalDataResult<BmsDifficultyTableIndex>
|
||||
{
|
||||
Status = "Difficulty table request did not complete",
|
||||
};
|
||||
completed?.Invoke(WithCourses(catalog));
|
||||
}
|
||||
|
||||
private DifficultyTableLibraryData WithCourses(ExternalDataResult<BmsDifficultyTableIndex> catalog)
|
||||
{
|
||||
return new DifficultyTableLibraryData
|
||||
{
|
||||
Catalog = catalog,
|
||||
Courses = IsConfigured
|
||||
? AeryCourseIndex.LoadTableLocal(_providerId, catalog.Data?.DisplayName ?? _displayName, _headerCachePath)
|
||||
: new ExternalDataResult<AeryCourseIndex> { Status = "NO BMSTABLE COURSE DATA CONFIGURED" },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8573e06b88064463a2d2ac0dd6f4bc9c
|
||||
@@ -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(
|
||||
"<meta[^>]+name\\s*=\\s*[\"']bmstable[\"'][^>]+content\\s*=\\s*[\"'](?<url>[^\"']+)[\"'][^>]*>|" +
|
||||
"<meta[^>]+content\\s*=\\s*[\"'](?<url>[^\"']+)[\"'][^>]+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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e7de5e563944400971cfcf8f54705b7
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49eebe4a4a024c509a8838c4bae27cee
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef4096d016784c3cb487f3a803ee5ee8
|
||||
@@ -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";
|
||||
|
||||
@@ -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<RatingPlayRecord> BestRatings = new List<RatingPlayRecord>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ea5022d2731468f8d67c43e8e13d95b
|
||||
@@ -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<RatingPlayRecord> BestRatings = new List<RatingPlayRecord>();
|
||||
}
|
||||
|
||||
internal readonly struct SongPlayResult
|
||||
{
|
||||
public SongPlayResult(
|
||||
|
||||
@@ -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<string>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>());
|
||||
|
||||
public SongSearchDocument(
|
||||
string[] title,
|
||||
string[] artist,
|
||||
string[] fileName,
|
||||
string[] aliases)
|
||||
{
|
||||
Title = title ?? Array.Empty<string>();
|
||||
Artist = artist ?? Array.Empty<string>();
|
||||
FileName = fileName ?? Array.Empty<string>();
|
||||
Aliases = aliases ?? Array.Empty<string>();
|
||||
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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ad783a34277242ca96d0c370d76aeebe
|
||||
@@ -6,39 +6,6 @@ using UnityEngine;
|
||||
|
||||
namespace ProjectQuint.Library
|
||||
{
|
||||
internal sealed class SongSearchDocument
|
||||
{
|
||||
public static SongSearchDocument Empty { get; } = new SongSearchDocument(
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>(),
|
||||
Array.Empty<string>());
|
||||
|
||||
public SongSearchDocument(
|
||||
string[] title,
|
||||
string[] artist,
|
||||
string[] fileName,
|
||||
string[] aliases)
|
||||
{
|
||||
Title = title ?? Array.Empty<string>();
|
||||
Artist = artist ?? Array.Empty<string>();
|
||||
FileName = fileName ?? Array.Empty<string>();
|
||||
Aliases = aliases ?? Array.Empty<string>();
|
||||
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<SongLibraryEntry> songs, SearchAliasIndex aliases = null)
|
||||
{
|
||||
foreach (var song in songs ?? Array.Empty<SongLibraryEntry>())
|
||||
|
||||
@@ -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<VisualElement>(_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<Label>(button, "preset-name"),
|
||||
|
||||
@@ -134,17 +134,6 @@ namespace ProjectQuint.Tests
|
||||
Assert.That(result.Status, Does.Contain("unavailable"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AeryParser_ProducesGenericCatalogEntries()
|
||||
{
|
||||
const string json = "[{\"level\":\"LEVEL 14\",\"title\":\"Table Title\",\"artist\":\"Table Artist\",\"md5\":\"ABC123\"}]";
|
||||
var table = AeryTableIndex.Parse(json);
|
||||
|
||||
Assert.That(table.Count, Is.EqualTo(1));
|
||||
Assert.That(table.TryGet("abc123", out var entry), Is.True);
|
||||
Assert.That(entry.DifficultyLabel, Is.EqualTo("LEVEL 14"));
|
||||
Assert.That(entry.DifficultySortIndex, Is.EqualTo(13));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LibraryLocator_DiscoversArbitrarilyNamedSiblingPack()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectQuint.Bootstrap;
|
||||
using ProjectQuint.Core.Bms;
|
||||
using ProjectQuint.Library;
|
||||
|
||||
@@ -31,19 +30,19 @@ namespace ProjectQuint.Tests
|
||||
[Test]
|
||||
public void Presets_ContainVerifiedCoreAndUeTables()
|
||||
{
|
||||
Assert.That(BmsDifficultyTableIndex.Presets, Has.Length.EqualTo(6));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[0].Id, Is.EqualTo("aery"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[1].Id, Is.EqualTo("satellite"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[2].Id, Is.EqualTo("stella"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[3].Id, Is.EqualTo("genocide-insane"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[4].Id, Is.EqualTo("4ue"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[5].Id, Is.EqualTo("6ue"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[4].HeaderUrl,
|
||||
Assert.That(BmsDifficultyTablePresets.All, Has.Length.EqualTo(6));
|
||||
Assert.That(BmsDifficultyTablePresets.All[0].Id, Is.EqualTo("aery"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[1].Id, Is.EqualTo("satellite"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[2].Id, Is.EqualTo("stella"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[3].Id, Is.EqualTo("genocide-insane"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[4].Id, Is.EqualTo("4ue"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[5].Id, Is.EqualTo("6ue"));
|
||||
Assert.That(BmsDifficultyTablePresets.All[4].HeaderUrl,
|
||||
Is.EqualTo("https://classmaterma.github.io/4UE/header.json"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[5].HeaderUrl,
|
||||
Assert.That(BmsDifficultyTablePresets.All[5].HeaderUrl,
|
||||
Is.EqualTo("https://classmaterma.github.io/UE/table.html"));
|
||||
Assert.That(BmsDifficultyTableIndex.Presets[5].DefaultKeyCount, Is.EqualTo(6));
|
||||
foreach (var preset in BmsDifficultyTableIndex.Presets)
|
||||
Assert.That(BmsDifficultyTablePresets.All[5].DefaultKeyCount, Is.EqualTo(6));
|
||||
foreach (var preset in BmsDifficultyTablePresets.All)
|
||||
Assert.That(preset.HeaderUrl, Does.StartWith("https://"));
|
||||
}
|
||||
|
||||
@@ -58,12 +57,14 @@ namespace ProjectQuint.Tests
|
||||
};
|
||||
|
||||
Assert.That(
|
||||
ProjectQuintApp.ResolvePackScanKeyCount(pack, existingCustomTable),
|
||||
SongKeyModePolicy.Resolve(pack.ForcedKeyCount,
|
||||
SongKeyModePolicy.TableDefault(existingCustomTable.Id, existingCustomTable.HeaderUrl), 0).KeyCount,
|
||||
Is.EqualTo(6));
|
||||
|
||||
pack.ForcedKeyCount = 7;
|
||||
Assert.That(
|
||||
ProjectQuintApp.ResolvePackScanKeyCount(pack, existingCustomTable),
|
||||
SongKeyModePolicy.Resolve(pack.ForcedKeyCount,
|
||||
SongKeyModePolicy.TableDefault(existingCustomTable.Id, existingCustomTable.HeaderUrl), 0).KeyCount,
|
||||
Is.EqualTo(7));
|
||||
}
|
||||
|
||||
@@ -102,19 +103,19 @@ namespace ProjectQuint.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FourUeTable_MatchedChartUsesAuthoritativeFourKeyLayoutWithoutMutatingRawMetadata()
|
||||
public void FourKeyPreset_MatchedChartUsesTableDefaultWithoutMutatingMetadata()
|
||||
{
|
||||
const string header =
|
||||
"{\"name\":\"4UE Difficulty Table\",\"symbol\":\"4UE\",\"data_url\":\"data.json\"}";
|
||||
const string data =
|
||||
"[{\"md5\":\"862ea8f9a8f9463501b052c0c86cb4c1\",\"level\":\"20\"," +
|
||||
"\"title\":\"GET ON WiTH FUNKOT [4K UE]\"}]";
|
||||
"[{\"md5\":\"synthetic-four-key-chart\",\"level\":\"20\"," +
|
||||
"\"title\":\"Synthetic Four Key Chart\"}]";
|
||||
var table = BmsDifficultyTableIndex.Parse("4ue", header, data);
|
||||
var song = new SongLibraryEntry
|
||||
{
|
||||
Metadata = new BmsMetadata
|
||||
{
|
||||
Md5 = "862ea8f9a8f9463501b052c0c86cb4c1",
|
||||
Md5 = "synthetic-four-key-chart",
|
||||
KeyCount = 5,
|
||||
UsedKeyChannelMask = 31,
|
||||
},
|
||||
@@ -124,7 +125,7 @@ namespace ProjectQuint.Tests
|
||||
|
||||
Assert.That(matched, Is.EqualTo(1));
|
||||
Assert.That(song.Metadata.KeyCount, Is.EqualTo(5));
|
||||
Assert.That(song.Catalog.KeyCountOverride, Is.EqualTo(4));
|
||||
Assert.That(song.Catalog.DefaultKeyCount, Is.EqualTo(4));
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(4));
|
||||
}
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace ProjectQuint.Tests
|
||||
var cacheRoot = Path.Combine(_temporaryDirectory, "difficulty-tables");
|
||||
var registry = new BmsDifficultyTableRegistry(registryPath, cacheRoot);
|
||||
|
||||
Assert.That(registry.Tables, Has.Count.EqualTo(BmsDifficultyTableIndex.Presets.Length));
|
||||
Assert.That(registry.Tables, Has.Count.EqualTo(BmsDifficultyTablePresets.All.Length));
|
||||
Assert.That(registry.ActiveTable, Is.Null);
|
||||
|
||||
var custom = registry.Register(
|
||||
@@ -337,7 +337,7 @@ namespace ProjectQuint.Tests
|
||||
registry.Select("satellite");
|
||||
var reloaded = new BmsDifficultyTableRegistry(registryPath, cacheRoot);
|
||||
|
||||
Assert.That(reloaded.Tables, Has.Count.EqualTo(BmsDifficultyTableIndex.Presets.Length + 1));
|
||||
Assert.That(reloaded.Tables, Has.Count.EqualTo(BmsDifficultyTablePresets.All.Length + 1));
|
||||
Assert.That(reloaded.ActiveTable.Id, Is.EqualTo("satellite"));
|
||||
Assert.That(reloaded.Find(custom.Id).DisplayName, Is.EqualTo("Custom Global Table"));
|
||||
Assert.That(reloaded.HeaderCachePathFor(custom),
|
||||
@@ -352,7 +352,7 @@ namespace ProjectQuint.Tests
|
||||
var packRegistryPath = Path.Combine(_temporaryDirectory, "packs.json");
|
||||
var packRegistry = new BmsPackRegistry(packRegistryPath);
|
||||
var pack = packRegistry.RegisterFolder(CreatePackFolder("Legacy AERY"));
|
||||
pack.DifficultyTableUrl = BmsDifficultyTableIndex.Presets[0].HeaderUrl;
|
||||
pack.DifficultyTableUrl = BmsDifficultyTablePresets.All[0].HeaderUrl;
|
||||
|
||||
var cacheRoot = Path.Combine(_temporaryDirectory, "difficulty-tables");
|
||||
var legacyCacheRoot = Path.Combine(cacheRoot, pack.Id);
|
||||
@@ -397,7 +397,7 @@ namespace ProjectQuint.Tests
|
||||
new BmsPackDefinition
|
||||
{
|
||||
Id = pack.Id,
|
||||
DifficultyTableUrl = BmsDifficultyTableIndex.Presets[0].HeaderUrl,
|
||||
DifficultyTableUrl = BmsDifficultyTablePresets.All[0].HeaderUrl,
|
||||
},
|
||||
},
|
||||
pack.Id);
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using ProjectQuint.Core.Bms;
|
||||
using ProjectQuint.Library;
|
||||
|
||||
namespace ProjectQuint.Tests
|
||||
{
|
||||
public sealed class LibraryConfigurationTests
|
||||
{
|
||||
[TestCase(4)]
|
||||
[TestCase(5)]
|
||||
[TestCase(6)]
|
||||
[TestCase(7)]
|
||||
public void ExplicitPackMode_SurvivesCatalogMatchingAndRefresh(int keyCount)
|
||||
{
|
||||
var song = new SongLibraryEntry
|
||||
{
|
||||
ForcedKeyCount = keyCount,
|
||||
Metadata = new BmsMetadata { Md5 = "synthetic-chart", KeyCount = 7 },
|
||||
};
|
||||
foreach (var preset in BmsDifficultyTablePresets.All)
|
||||
{
|
||||
ChartCatalogAnnotator.Apply(new[] { song }, new Catalog(preset.Id));
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(keyCount));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.Pack));
|
||||
Assert.That(song.KeyMode.HasConflict, Is.EqualTo(keyCount != preset.DefaultKeyCount));
|
||||
Assert.That(song.Metadata.KeyCount, Is.EqualTo(7));
|
||||
}
|
||||
ChartCatalogAnnotator.Apply(new[] { song }, null);
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(keyCount));
|
||||
Assert.That(song.KeyMode.HasConflict, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CustomRegistrationOfPresetUrl_UsesTheSameDefaultInScanAndCatalogStages()
|
||||
{
|
||||
foreach (var preset in BmsDifficultyTablePresets.All)
|
||||
{
|
||||
var tableDefault = SongKeyModePolicy.TableDefault("custom-id", preset.HeaderUrl);
|
||||
var song = new SongLibraryEntry
|
||||
{
|
||||
TableDefaultKeyCount = tableDefault,
|
||||
Metadata = new BmsMetadata { Md5 = "synthetic-chart", KeyCount = 7 },
|
||||
};
|
||||
ChartCatalogAnnotator.Apply(new[] { song }, new Catalog("custom-id"));
|
||||
Assert.That(tableDefault, Is.EqualTo(preset.DefaultKeyCount));
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(preset.DefaultKeyCount));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.DifficultyTable));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnknownTable_DoesNotOverrideAutomaticChartDetection()
|
||||
{
|
||||
var tableDefault = SongKeyModePolicy.TableDefault("custom", "https://tables.example/header.json");
|
||||
Assert.That(tableDefault, Is.Zero);
|
||||
Assert.That(SongKeyModePolicy.Resolve(0, tableDefault, 0).KeyCount, Is.Zero);
|
||||
Assert.That(SongKeyModePolicy.Resolve(0, tableDefault, 6).KeyCount, Is.EqualTo(6));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutomaticMode_UsesTableDefaultAndThenDetectedChartMode()
|
||||
{
|
||||
var song = new SongLibraryEntry
|
||||
{
|
||||
Metadata = new BmsMetadata { Md5 = "synthetic-chart", KeyCount = 7 },
|
||||
};
|
||||
foreach (var preset in BmsDifficultyTablePresets.All)
|
||||
{
|
||||
ChartCatalogAnnotator.Apply(new[] { song }, new Catalog(preset.Id));
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(preset.DefaultKeyCount));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.DifficultyTable));
|
||||
}
|
||||
ChartCatalogAnnotator.Apply(new[] { song }, null);
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(7));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.Chart));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnmatchedChart_StillUsesExplicitPackMode()
|
||||
{
|
||||
var song = new SongLibraryEntry
|
||||
{
|
||||
ForcedKeyCount = 4,
|
||||
TableDefaultKeyCount = 5,
|
||||
Metadata = new BmsMetadata { Md5 = "unmatched", KeyCount = 7 },
|
||||
};
|
||||
Assert.That(ChartCatalogAnnotator.Apply(new[] { song }, new Catalog("example")), Is.Zero);
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(4));
|
||||
Assert.That(song.KeyMode.HasConflict, Is.True);
|
||||
}
|
||||
|
||||
[TestCase("https://mirror.example/aery/header.json?revision=2")]
|
||||
[TestCase("https://other.example/path/aery/index.html")]
|
||||
[TestCase("https://other.example/path/AERY/custom.json")]
|
||||
[TestCase("https://tables.example/header.json")]
|
||||
public void TableRequest_PreservesConfiguredHostPathQueryAndIdentity(string url)
|
||||
{
|
||||
var request = new DifficultyTableRequest("custom-id", url, "header-cache", "data-cache");
|
||||
Assert.That(request.HeaderUri.AbsoluteUri, Is.EqualTo(url));
|
||||
Assert.That(request.ProviderId, Is.EqualTo("custom-id"));
|
||||
Assert.That(request.HeaderCachePath, Is.EqualTo("header-cache"));
|
||||
Assert.That(request.DataCachePath, Is.EqualTo("data-cache"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PresetRequests_UseTheirRegisteredHeaderUrls()
|
||||
{
|
||||
foreach (var preset in BmsDifficultyTablePresets.All)
|
||||
{
|
||||
var request = new DifficultyTableRequest(preset.Id, preset.HeaderUrl, "h", "d");
|
||||
Assert.That(request.HeaderUri.AbsoluteUri, Is.EqualTo(preset.HeaderUrl));
|
||||
Assert.That(request.ProviderId, Is.EqualTo(preset.Id));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HeaderAndDataLinks_AreResolvedRelativeToTheirOwnDocuments()
|
||||
{
|
||||
var page = new Uri("https://mirror.example/custom/page/index.html");
|
||||
var header = DifficultyTableRequest.ResolveHeaderPage(page,
|
||||
"<html><meta content='../metadata/header.json?one=1&two=2' name='bmstable'></html>");
|
||||
Assert.That(header.AbsoluteUri, Is.EqualTo("https://mirror.example/custom/metadata/header.json?one=1&two=2"));
|
||||
Assert.That(DifficultyTableRequest.ResolveLink(header, "../charts/data.json").AbsoluteUri,
|
||||
Is.EqualTo("https://mirror.example/custom/charts/data.json"));
|
||||
}
|
||||
|
||||
[TestCase("http://tables.example/header.json")]
|
||||
[TestCase("file:///header.json")]
|
||||
[TestCase("header.json")]
|
||||
public void InvalidConfiguredUrl_IsRejected(string url)
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new DifficultyTableRequest("custom", url, "h", "d"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InsecureLinkedDocument_AndMissingMetadataAreRejected()
|
||||
{
|
||||
var origin = new Uri("https://tables.example/header.json");
|
||||
Assert.Throws<ArgumentException>(() => DifficultyTableRequest.ResolveLink(origin, "http://tables.example/data.json"));
|
||||
Assert.Throws<FormatException>(() => DifficultyTableRequest.ResolveHeaderPage(origin, "<html></html>"));
|
||||
Assert.Throws<FormatException>(() => DifficultyTableRequest.ResolveLink(origin, ""));
|
||||
}
|
||||
|
||||
private sealed class Catalog : IChartCatalog
|
||||
{
|
||||
public Catalog(string providerId) { ProviderId = providerId; }
|
||||
public string ProviderId { get; }
|
||||
public string DisplayName => "Synthetic table";
|
||||
public ExternalDataOrigin Origin => ExternalDataOrigin.Network;
|
||||
public int Count => 1;
|
||||
public bool TryGet(string hash, out ChartCatalogEntry entry)
|
||||
{
|
||||
entry = hash == "synthetic-chart" ? new ChartCatalogEntry { DifficultyValue = "10" } : null;
|
||||
return entry != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83e72e9a135640a7a49125d360f07d0f
|
||||
@@ -222,7 +222,7 @@ namespace ProjectQuint.Tests
|
||||
var recorded = new RatingSummary(
|
||||
"aery", "AERY", 5, false, 12.5, 29,
|
||||
activityPlayCount: 34);
|
||||
var tables = BmsDifficultyTableIndex.Presets
|
||||
var tables = BmsDifficultyTablePresets.All
|
||||
.Select(preset => new BmsDifficultyTableDefinition
|
||||
{
|
||||
Id = preset.Id,
|
||||
@@ -248,7 +248,7 @@ namespace ProjectQuint.Tests
|
||||
Does.Contain("GENOCIDE INSANE BMS (LEGACY MIRROR) 1+7K RATING"));
|
||||
Assert.That(contexts.Select(context => context.ContextLabel),
|
||||
Does.Contain("6UE DIFFICULTY TABLE 6K RATING"));
|
||||
Assert.That(contexts.Count, Is.EqualTo(BmsDifficultyTableIndex.Presets.Length));
|
||||
Assert.That(contexts.Count, Is.EqualTo(BmsDifficultyTablePresets.All.Length));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using ProjectQuint.Core.Bms;
|
||||
using ProjectQuint.Core.Charts;
|
||||
using ProjectQuint.Features.Loading;
|
||||
using ProjectQuint.Library;
|
||||
|
||||
namespace ProjectQuint.Tests
|
||||
{
|
||||
public sealed class SongChartPreparationTests
|
||||
{
|
||||
[TestCase(4, 5)]
|
||||
[TestCase(5, 4)]
|
||||
[TestCase(6, 7)]
|
||||
[TestCase(7, 6)]
|
||||
public void ExplicitPackMode_ReachesPlayableChartDespiteConflictingTable(int packKeys, int tableKeys)
|
||||
{
|
||||
var path = Path.Combine(_temporaryDirectory, "key-mode-conflict.bms");
|
||||
File.WriteAllText(path,
|
||||
"#TITLE Synthetic Layout\n#BPM 120\n#WAV01 note.wav\n" +
|
||||
"#00111:01\n#00112:01\n#00113:01\n#00114:01\n#00115:01\n#00118:01\n#00119:01\n");
|
||||
var entry = new SongLibraryEntry
|
||||
{
|
||||
ChartPath = path,
|
||||
ForcedKeyCount = packKeys,
|
||||
Metadata = new BmsMetadata { KeyCount = 7 },
|
||||
Catalog = new ChartCatalogAnnotation { DefaultKeyCount = tableKeys },
|
||||
};
|
||||
var prepared = SongChartPreparation.Prepare(entry, LaneModifier.Off, 0);
|
||||
Assert.That(prepared.Chart.Metadata.KeyCount, Is.EqualTo(packKeys));
|
||||
Assert.That(prepared.Chart.Metadata.TotalLaneCount, Is.EqualTo(packKeys));
|
||||
Assert.That(prepared.LaneMap.Length, Is.EqualTo(packKeys));
|
||||
Assert.That(prepared.Chart.Notes.All(note => note.Lane >= 0 && note.Lane < packKeys), Is.True);
|
||||
Assert.That(entry.KeyMode.HasConflict, Is.True);
|
||||
}
|
||||
|
||||
private string _temporaryDirectory;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_temporaryDirectory = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"ProjectQuint-ChartPreparationTests",
|
||||
Path.GetRandomFileName());
|
||||
Directory.CreateDirectory(_temporaryDirectory);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (Directory.Exists(_temporaryDirectory))
|
||||
Directory.Delete(_temporaryDirectory, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseAndTransform_AppliesTableMetadataAndLaneModifier()
|
||||
{
|
||||
var chartPath = Path.Combine(_temporaryDirectory, "sample.bms");
|
||||
File.WriteAllText(
|
||||
chartPath,
|
||||
"#TITLE Raw Title\n" +
|
||||
"#ARTIST Raw Artist\n" +
|
||||
"#BPM 120\n" +
|
||||
"#PLAYLEVEL 3\n" +
|
||||
"#WAV01 note.wav\n" +
|
||||
"#00111:01\n");
|
||||
var entry = new SongLibraryEntry
|
||||
{
|
||||
ChartPath = chartPath,
|
||||
Metadata = new BmsMetadata
|
||||
{
|
||||
Md5 = "table-md5",
|
||||
},
|
||||
Catalog = new ChartCatalogAnnotation
|
||||
{
|
||||
ProviderId = "example",
|
||||
ProviderDisplayName = "Example Table",
|
||||
DifficultyLabel = "LEVEL 14",
|
||||
DifficultySortIndex = 13,
|
||||
TitleOverride = "Table Title",
|
||||
ArtistOverride = "Table Artist",
|
||||
},
|
||||
};
|
||||
|
||||
var prepared = SongChartPreparation.Prepare(
|
||||
entry,
|
||||
LaneModifier.Mirror,
|
||||
randomSeed: 1234);
|
||||
|
||||
Assert.That(prepared.Chart.Metadata.Title, Is.EqualTo("Table Title"));
|
||||
Assert.That(prepared.Chart.Metadata.Artist, Is.EqualTo("Table Artist"));
|
||||
Assert.That(prepared.Chart.Metadata.DifficultyLabel, Is.EqualTo("LEVEL 14"));
|
||||
Assert.That(prepared.Chart.Metadata.SourceHash, Is.EqualTo("table-md5"));
|
||||
Assert.That(prepared.Chart.Notes, Has.Count.EqualTo(1));
|
||||
Assert.That(prepared.Chart.Notes[0].Lane, Is.EqualTo(4));
|
||||
Assert.That(prepared.NotesById.ContainsKey(prepared.Chart.Notes[0].Id), Is.True);
|
||||
Assert.That(prepared.LaneMap, Is.EqualTo(new[] { 4, 3, 2, 1, 0 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseAndTransform_FourKeyTableIgnoresStrayCenterChannel()
|
||||
{
|
||||
var chartPath = Path.Combine(_temporaryDirectory, "four-key-layout.bms");
|
||||
File.WriteAllText(
|
||||
chartPath,
|
||||
"#TITLE Synthetic Four Key Chart\n" +
|
||||
"#BPM 190\n" +
|
||||
"#WAV01 note.wav\n" +
|
||||
"#00111:01\n" +
|
||||
"#00112:01\n" +
|
||||
"#00113:01\n" +
|
||||
"#00114:01\n" +
|
||||
"#00115:01\n");
|
||||
var entry = new SongLibraryEntry
|
||||
{
|
||||
ChartPath = chartPath,
|
||||
Metadata = new BmsMetadata
|
||||
{
|
||||
Md5 = "synthetic-chart-md5",
|
||||
KeyCount = 5,
|
||||
UsedKeyChannelMask = 31,
|
||||
},
|
||||
Catalog = new ChartCatalogAnnotation
|
||||
{
|
||||
ProviderId = "four-key-table",
|
||||
DefaultKeyCount = 4,
|
||||
DifficultyLabel = "LEVEL 20",
|
||||
},
|
||||
};
|
||||
|
||||
var prepared = SongChartPreparation.Prepare(
|
||||
entry,
|
||||
LaneModifier.Off,
|
||||
randomSeed: 1);
|
||||
|
||||
Assert.That(prepared.Chart.Metadata.KeyCount, Is.EqualTo(4));
|
||||
Assert.That(prepared.Chart.Metadata.TotalLaneCount, Is.EqualTo(4));
|
||||
Assert.That(prepared.LaneMap, Is.EqualTo(new[] { 0, 1, 2, 3 }));
|
||||
Assert.That(prepared.Chart.Notes, Has.Count.EqualTo(4));
|
||||
Assert.That(prepared.Chart.Notes.Select(note => note.Lane),
|
||||
Is.EquivalentTo(new[] { 0, 1, 2, 3 }));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 528e4bfcd36e46e983efe99d6b78b114
|
||||
@@ -32,6 +32,38 @@ namespace ProjectQuint.Tests
|
||||
Directory.Delete(_directory, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ColdAndCachedScan_PreserveExplicitModeWhenTableDefaultConflicts()
|
||||
{
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
var scanner = new SongLibraryScanner(_cachePath, forcedKeyCount: 4, tableDefaultKeyCount: 5);
|
||||
var songs = scanner.Scan(_directory, CancellationToken.None);
|
||||
Assert.That(songs, Has.Count.EqualTo(1));
|
||||
var song = songs[0];
|
||||
song.Catalog = new ChartCatalogAnnotation { DefaultKeyCount = 5 };
|
||||
Assert.That(song.ForcedKeyCount, Is.EqualTo(4));
|
||||
Assert.That(song.TableDefaultKeyCount, Is.EqualTo(5));
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(4));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.Pack));
|
||||
Assert.That(song.KeyMode.HasConflict, Is.True);
|
||||
Assert.That(scanner.CacheHits, Is.EqualTo(pass));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SwitchingFromExplicitToAutomaticMode_DoesNotReuseDifferentLayoutCache()
|
||||
{
|
||||
new SongLibraryScanner(_cachePath, forcedKeyCount: 4, tableDefaultKeyCount: 5)
|
||||
.Scan(_directory, CancellationToken.None);
|
||||
var scanner = new SongLibraryScanner(_cachePath, forcedKeyCount: 0, tableDefaultKeyCount: 5);
|
||||
var song = scanner.Scan(_directory, CancellationToken.None)[0];
|
||||
Assert.That(song.ForcedKeyCount, Is.Zero);
|
||||
Assert.That(song.EffectiveKeyCount, Is.EqualTo(5));
|
||||
Assert.That(song.KeyMode.Source, Is.EqualTo(SongKeyModeSource.DifficultyTable));
|
||||
Assert.That(scanner.CacheHits, Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SecondScan_ReusesMetadataAndMd5FromPersistentCache()
|
||||
{
|
||||
|
||||
@@ -34,94 +34,6 @@ namespace ProjectQuint.Tests
|
||||
Directory.Delete(_temporaryDirectory, true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseAndTransform_AppliesTableMetadataAndLaneModifier()
|
||||
{
|
||||
var chartPath = Path.Combine(_temporaryDirectory, "sample.bms");
|
||||
File.WriteAllText(
|
||||
chartPath,
|
||||
"#TITLE Raw Title\n" +
|
||||
"#ARTIST Raw Artist\n" +
|
||||
"#BPM 120\n" +
|
||||
"#PLAYLEVEL 3\n" +
|
||||
"#WAV01 note.wav\n" +
|
||||
"#00111:01\n");
|
||||
var entry = new SongLibraryEntry
|
||||
{
|
||||
ChartPath = chartPath,
|
||||
Metadata = new BmsMetadata
|
||||
{
|
||||
Md5 = "table-md5",
|
||||
},
|
||||
Catalog = new ChartCatalogAnnotation
|
||||
{
|
||||
ProviderId = "aery",
|
||||
ProviderDisplayName = "AERY",
|
||||
DifficultyLabel = "LEVEL 14",
|
||||
DifficultySortIndex = 13,
|
||||
TitleOverride = "Table Title",
|
||||
ArtistOverride = "Table Artist",
|
||||
},
|
||||
};
|
||||
|
||||
var prepared = SongLoadCoordinator.ParseAndTransform(
|
||||
entry,
|
||||
LaneModifier.Mirror,
|
||||
randomSeed: 1234);
|
||||
|
||||
Assert.That(prepared.Chart.Metadata.Title, Is.EqualTo("Table Title"));
|
||||
Assert.That(prepared.Chart.Metadata.Artist, Is.EqualTo("Table Artist"));
|
||||
Assert.That(prepared.Chart.Metadata.DifficultyLabel, Is.EqualTo("LEVEL 14"));
|
||||
Assert.That(prepared.Chart.Metadata.SourceHash, Is.EqualTo("table-md5"));
|
||||
Assert.That(prepared.Chart.Notes, Has.Count.EqualTo(1));
|
||||
Assert.That(prepared.Chart.Notes[0].Lane, Is.EqualTo(4));
|
||||
Assert.That(prepared.NotesById.ContainsKey(prepared.Chart.Notes[0].Id), Is.True);
|
||||
Assert.That(prepared.LaneMap, Is.EqualTo(new[] { 4, 3, 2, 1, 0 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParseAndTransform_FourKeyTableIgnoresStrayCenterChannel()
|
||||
{
|
||||
var chartPath = Path.Combine(_temporaryDirectory, "get-on-with-funkot-regression.bms");
|
||||
File.WriteAllText(
|
||||
chartPath,
|
||||
"#TITLE GET ON WiTH FUNKOT [4K UE]\n" +
|
||||
"#BPM 190\n" +
|
||||
"#WAV01 note.wav\n" +
|
||||
"#00111:01\n" +
|
||||
"#00112:01\n" +
|
||||
"#00113:01\n" +
|
||||
"#00114:01\n" +
|
||||
"#00115:01\n");
|
||||
var entry = new SongLibraryEntry
|
||||
{
|
||||
ChartPath = chartPath,
|
||||
Metadata = new BmsMetadata
|
||||
{
|
||||
Md5 = "862ea8f9a8f9463501b052c0c86cb4c1",
|
||||
KeyCount = 5,
|
||||
UsedKeyChannelMask = 31,
|
||||
},
|
||||
Catalog = new ChartCatalogAnnotation
|
||||
{
|
||||
ProviderId = "4ue",
|
||||
KeyCountOverride = 4,
|
||||
DifficultyLabel = "4UE20",
|
||||
},
|
||||
};
|
||||
|
||||
var prepared = SongLoadCoordinator.ParseAndTransform(
|
||||
entry,
|
||||
LaneModifier.Off,
|
||||
randomSeed: 1);
|
||||
|
||||
Assert.That(prepared.Chart.Metadata.KeyCount, Is.EqualTo(4));
|
||||
Assert.That(prepared.Chart.Metadata.TotalLaneCount, Is.EqualTo(4));
|
||||
Assert.That(prepared.LaneMap, Is.EqualTo(new[] { 0, 1, 2, 3 }));
|
||||
Assert.That(prepared.Chart.Notes, Has.Count.EqualTo(4));
|
||||
Assert.That(prepared.Chart.Notes.Select(note => note.Lane),
|
||||
Is.EquivalentTo(new[] { 0, 1, 2, 3 }));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator LoadCancellation_AfterCandidateCreationRollsBackResourceRoot()
|
||||
|
||||
Reference in New Issue
Block a user