diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartCard.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartCard.kt index f6a39b9b..ab969da1 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartCard.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartCard.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import xyz.sevive.arcaeaoffline.R import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass +import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClassDisplay import xyz.sevive.arcaeaoffline.core.database.entities.Chart import xyz.sevive.arcaeaoffline.ui.helpers.ArcaeaFormatters import xyz.sevive.arcaeaoffline.ui.theme.ArcaeaOfflineTheme @@ -92,7 +93,10 @@ fun ArcaeaChartCard( Text( text = ArcaeaFormatters.ratingText(it), modifier = Modifier.fillMaxWidth(), - color = ratingClassColor(it.ratingClass), + color = + ratingClassColor( + ArcaeaRatingClassDisplay.of(it.ratingClass, it.ratingClassAlias), + ), ) } } diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartSelector.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartSelector.kt index 6b9568a3..c4129e23 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartSelector.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaChartSelector.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.firstOrNull import org.koin.compose.koinInject import xyz.sevive.arcaeaoffline.R import xyz.sevive.arcaeaoffline.core.database.entities.Chart +import xyz.sevive.arcaeaoffline.core.database.entities.Difficulty import xyz.sevive.arcaeaoffline.core.database.entities.Song import xyz.sevive.arcaeaoffline.core.database.helpers.ChartFactory import xyz.sevive.arcaeaoffline.core.database.repositories.ChartRepository @@ -40,25 +41,30 @@ private fun rememberArcaeaSong( } @Composable -private fun rememberArcaeaCharts( +private fun rememberArcaeaDifficulties( difficultyRepo: DifficultyRepository, + song: Song?, +): State> = + produceState(initialValue = emptyList(), song?.id) { + song?.let { + difficultyRepo.findAllBySongId(it.id).collect { value = it } + } + } + +@Composable +private fun rememberArcaeaCharts( chartRepo: ChartRepository, song: Song?, + difficulties: List, allowFakeChart: Boolean = false, ): State> = - produceState(initialValue = emptyList(), song?.id, allowFakeChart) { + produceState(initialValue = emptyList(), song?.id, difficulties, allowFakeChart) { song?.let { - val availableRatingClasses = - difficultyRepo.findAllBySongId(song.id).firstOrNull()?.map { it.ratingClass } - ?: emptyList() - value = - availableRatingClasses.mapNotNull { ratingClass -> - var chart = chartRepo.find(song.id, ratingClass).firstOrNull() + difficulties.mapNotNull { difficulty -> + var chart = chartRepo.find(difficulty.songId, difficulty.ratingClass).firstOrNull() if (chart == null && allowFakeChart) { - difficultyRepo.find(song.id, ratingClass).firstOrNull()?.let { difficulty -> - chart = ChartFactory.fakeChart(song, difficulty) - } + chart = ChartFactory.fakeChart(song, difficulty) } chart } @@ -80,16 +86,17 @@ fun ArcaeaChartSelector( mutableStateOf(chart?.ratingClass) } val song by rememberArcaeaSong(songRepo = songRepo, songId = selectedSongId) + val difficulties by rememberArcaeaDifficulties(difficultyRepo = difficultyRepo, song = song) val charts by rememberArcaeaCharts( - difficultyRepo = difficultyRepo, chartRepo = chartRepo, song = song, + difficulties = difficulties, allowFakeChart = allowFakeChart, ) val selectorItems = - remember(charts) { - charts.toRatingClassSelectorItems() + remember(difficulties) { + difficulties.toRatingClassSelectorItems() } // Emit the change when either [selectedSongId] or [selectedRatingClass] changes. diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaRatingClassSelector.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaRatingClassSelector.kt index c4b7d5ff..32113f05 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaRatingClassSelector.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/components/ArcaeaRatingClassSelector.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.em import com.materialkolor.hct.Hct import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClassDisplay -import xyz.sevive.arcaeaoffline.core.database.entities.Chart +import xyz.sevive.arcaeaoffline.core.database.entities.Difficulty import xyz.sevive.arcaeaoffline.ui.theme.ArcaeaOfflineTheme import xyz.sevive.arcaeaoffline.ui.theme.ratingClassColor @@ -58,10 +58,10 @@ data class RatingClassSelectorItem( val ratingPlus: Boolean = false, ) -fun List.toRatingClassSelectorItems(): List = +fun List.toRatingClassSelectorItems(): List = map { RatingClassSelectorItem( - display = ArcaeaRatingClassDisplay.of(it.ratingClass), + display = ArcaeaRatingClassDisplay.of(it.ratingClass, it.ratingClassAlias), rating = it.rating, ratingPlus = it.ratingPlus, ) diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormatters.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormatters.kt index 444e6f79..68a6210a 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormatters.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormatters.kt @@ -9,6 +9,7 @@ import com.ionspin.kotlin.bignum.decimal.DecimalMode import com.ionspin.kotlin.bignum.decimal.RoundingMode import com.ionspin.kotlin.bignum.decimal.toBigDecimal import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass +import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClassDisplay import xyz.sevive.arcaeaoffline.core.database.entities.Chart import xyz.sevive.arcaeaoffline.core.database.entities.Difficulty @@ -96,12 +97,12 @@ object ArcaeaFormatters { } internal fun ratingText( - ratingClass: ArcaeaRatingClass, + ratingClassDisplay: ArcaeaRatingClassDisplay, rating: Int, ratingPlus: Boolean, constant: Int = 0, ) = buildString { - append(ratingClass.toString()) + append(ratingClassDisplay.name) append(' ') if (constant > 0) { @@ -114,18 +115,31 @@ object ArcaeaFormatters { } } + internal fun ratingText( + ratingClass: ArcaeaRatingClass, + rating: Int, + ratingPlus: Boolean, + constant: Int = 0, + ) = ratingText( + ArcaeaRatingClassDisplay.of(ratingClass), + rating, + ratingPlus, + constant, + ) + /** * Returns the readable rating text for the given difficulty. * * For example: * * `Difficulty(ratingClass=2, rating=2, ratingPlus=false)` > "FUTURE 2" * * `Difficulty(ratingClass=2, rating=10, ratingPlus=true)` > "FUTURE 10+" + * * `Difficulty(ratingClass=3, rating=11, ratingPlus=true, ratingClassAlias=1)` > "INSCRIBED 11+" */ fun ratingText(difficulty: Difficulty): String = ratingText( - ratingClass = difficulty.ratingClass, - rating = difficulty.rating, - ratingPlus = difficulty.ratingPlus, + ArcaeaRatingClassDisplay.of(difficulty.ratingClass, difficulty.ratingClassAlias), + difficulty.rating, + difficulty.ratingPlus, ) /** @@ -139,12 +153,13 @@ object ArcaeaFormatters { * * `Chart(ratingClass=2, rating=10, ratingPlus=true)` > "FUTURE 10+" * * `Chart(ratingClass=2, rating=10, ratingPlus=true, constant=108)` > "FUTURE 10.8" * * `Chart(ratingClass=2, rating=10, ratingPlus=true, constant=0)` > "FUTURE 10+" + * * `Chart(ratingClass=3, rating=11, ratingPlus=true, ratingClassAlias=1)` > "INSCRIBED 11+" */ fun ratingText(chart: Chart): String = ratingText( - ratingClass = chart.ratingClass, - rating = chart.rating, - ratingPlus = chart.ratingPlus, + ArcaeaRatingClassDisplay.of(chart.ratingClass, chart.ratingClassAlias), + chart.rating, + chart.ratingPlus, constant = chart.constant, ) } diff --git a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/utilities/UtilitiesCalculatorScreen.kt b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/utilities/UtilitiesCalculatorScreen.kt index 92e64b29..196c5b0a 100644 --- a/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/utilities/UtilitiesCalculatorScreen.kt +++ b/app/src/main/java/xyz/sevive/arcaeaoffline/ui/screens/utilities/UtilitiesCalculatorScreen.kt @@ -27,6 +27,7 @@ import org.koin.compose.koinInject import xyz.sevive.arcaeaoffline.R import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass import xyz.sevive.arcaeaoffline.core.database.repositories.ChartRepository +import xyz.sevive.arcaeaoffline.core.database.repositories.DifficultyRepository import xyz.sevive.arcaeaoffline.ui.SubScreenContainer import xyz.sevive.arcaeaoffline.ui.components.ArcaeaPackAndSongQuickSearch import xyz.sevive.arcaeaoffline.ui.components.ArcaeaRatingClassSelector @@ -39,6 +40,7 @@ import xyz.sevive.arcaeaoffline.ui.navigation.UtilitiesSubScreen fun UtilitiesCalculatorScreen( modifier: Modifier = Modifier, chartRepo: ChartRepository = koinInject(), + difficultyRepo: DifficultyRepository = koinInject(), ) { var constant by remember { mutableIntStateOf(0) } var selectedSongId by remember { mutableStateOf(null) } @@ -48,9 +50,14 @@ fun UtilitiesCalculatorScreen( value = chartRepo.findAllBySongId(it).firstOrNull() ?: emptyList() } } + val difficulties by produceState(initialValue = listOf(), selectedSongId) { + selectedSongId?.let { + value = difficultyRepo.findAllBySongId(it).firstOrNull() ?: emptyList() + } + } val selectorItems = - remember(charts) { - charts.toRatingClassSelectorItems() + remember(difficulties) { + difficulties.toRatingClassSelectorItems() } LaunchedEffect(selectedRatingClass, charts) { diff --git a/app/src/test/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormattersTest.kt b/app/src/test/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormattersTest.kt index 74337b8c..211e1a59 100644 --- a/app/src/test/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormattersTest.kt +++ b/app/src/test/java/xyz/sevive/arcaeaoffline/ui/helpers/ArcaeaFormattersTest.kt @@ -3,6 +3,7 @@ package xyz.sevive.arcaeaoffline.ui.helpers import org.junit.Assert.assertEquals import org.junit.Test import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass +import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClassDisplay class ArcaeaFormattersTest { @Test @@ -124,4 +125,28 @@ class ArcaeaFormattersTest { ArcaeaFormatters.ratingText(ArcaeaRatingClass.BEYOND, 11, true, 118), ) } + + @Test + fun ratingTextAliasTest() { + assertEquals( + "INSCRIBED 11", + ArcaeaFormatters.ratingText(ArcaeaRatingClassDisplay.INSCRIBED, 11, false), + ) + + assertEquals( + "INSCRIBED 11.5", + ArcaeaFormatters.ratingText(ArcaeaRatingClassDisplay.INSCRIBED, 11, false, 115), + ) + + // Unknown (ratingClass, alias) combos must fall back to the default + // display form of the rating class, not throw. + assertEquals( + "BEYOND 12", + ArcaeaFormatters.ratingText( + ArcaeaRatingClassDisplay.of(ArcaeaRatingClass.BEYOND, alias = 2), + 12, + false, + ), + ) + } } diff --git a/core/schemas/xyz.sevive.arcaeaoffline.core.database.ArcaeaOfflineDatabase/15.json b/core/schemas/xyz.sevive.arcaeaoffline.core.database.ArcaeaOfflineDatabase/15.json new file mode 100644 index 00000000..9a71a1ca --- /dev/null +++ b/core/schemas/xyz.sevive.arcaeaoffline.core.database.ArcaeaOfflineDatabase/15.json @@ -0,0 +1,689 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "695f3a8be7efe40bdffb285ef54f4a81", + "entities": [ + { + "tableName": "properties", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + }, + { + "tableName": "packs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "packs_localized", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lang` TEXT NOT NULL, `name` TEXT, `description` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `packs`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_packs_localized_id_lang", + "unique": true, + "columnNames": [ + "id", + "lang" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_packs_localized_id_lang` ON `${TABLE_NAME}` (`id`, `lang`)" + }, + { + "name": "index_packs_localized_lang", + "unique": false, + "columnNames": [ + "lang" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_packs_localized_lang` ON `${TABLE_NAME}` (`lang`)" + } + ], + "foreignKeys": [ + { + "table": "packs", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "songs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`idx` INTEGER NOT NULL, `id` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `set` TEXT NOT NULL, `bpm` TEXT, `bpm_base` REAL, `audio_preview` INTEGER, `audio_preview_end` INTEGER, `side` INTEGER NOT NULL, `version` TEXT, `date` INTEGER, `bg` TEXT, `bg_inverse` TEXT, `bg_day` TEXT, `bg_night` TEXT, `source` TEXT, `source_copyright` TEXT, `deleted_in_game` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "idx", + "columnName": "idx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "set", + "columnName": "set", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bpm", + "columnName": "bpm", + "affinity": "TEXT" + }, + { + "fieldPath": "bpmBase", + "columnName": "bpm_base", + "affinity": "REAL" + }, + { + "fieldPath": "audioPreview", + "columnName": "audio_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "audioPreviewEnd", + "columnName": "audio_preview_end", + "affinity": "INTEGER" + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER" + }, + { + "fieldPath": "bg", + "columnName": "bg", + "affinity": "TEXT" + }, + { + "fieldPath": "bgInverse", + "columnName": "bg_inverse", + "affinity": "TEXT" + }, + { + "fieldPath": "bgDay", + "columnName": "bg_day", + "affinity": "TEXT" + }, + { + "fieldPath": "bgNight", + "columnName": "bg_night", + "affinity": "TEXT" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + }, + { + "fieldPath": "sourceCopyright", + "columnName": "source_copyright", + "affinity": "TEXT" + }, + { + "fieldPath": "deletedInGame", + "columnName": "deleted_in_game", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "songs_localized", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lang` TEXT NOT NULL, `title` TEXT, `source` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `songs`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_songs_localized_id_lang", + "unique": true, + "columnNames": [ + "id", + "lang" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_songs_localized_id_lang` ON `${TABLE_NAME}` (`id`, `lang`)" + }, + { + "name": "index_songs_localized_lang", + "unique": false, + "columnNames": [ + "lang" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_songs_localized_lang` ON `${TABLE_NAME}` (`lang`)" + } + ], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "difficulties", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `rating_class` INTEGER NOT NULL, `rating_class_alias` INTEGER, `rating` INTEGER NOT NULL, `rating_plus` INTEGER NOT NULL, `chart_designer` TEXT, `jacket_designer` TEXT, `audio_override` INTEGER NOT NULL, `jacket_override` INTEGER NOT NULL, `jacket_night` TEXT, `title` TEXT, `artist` TEXT, `bg` TEXT, `bg_inverse` TEXT, `bpm` TEXT, `bpm_base` REAL, `version` TEXT, `date` INTEGER, PRIMARY KEY(`song_id`, `rating_class`))", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ratingClass", + "columnName": "rating_class", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingClassAlias", + "columnName": "rating_class_alias", + "affinity": "INTEGER" + }, + { + "fieldPath": "rating", + "columnName": "rating", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ratingPlus", + "columnName": "rating_plus", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "chartDesigner", + "columnName": "chart_designer", + "affinity": "TEXT" + }, + { + "fieldPath": "jacketDesigner", + "columnName": "jacket_designer", + "affinity": "TEXT" + }, + { + "fieldPath": "audioOverride", + "columnName": "audio_override", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "jacketOverride", + "columnName": "jacket_override", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "jacketNight", + "columnName": "jacket_night", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT" + }, + { + "fieldPath": "bg", + "columnName": "bg", + "affinity": "TEXT" + }, + { + "fieldPath": "bgInverse", + "columnName": "bg_inverse", + "affinity": "TEXT" + }, + { + "fieldPath": "bpm", + "columnName": "bpm", + "affinity": "TEXT" + }, + { + "fieldPath": "bpmBase", + "columnName": "bpm_base", + "affinity": "REAL" + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id", + "rating_class" + ] + } + }, + { + "tableName": "difficulties_localized", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `rating_class` INTEGER NOT NULL, `lang` TEXT NOT NULL, `title` TEXT, `artist` TEXT, PRIMARY KEY(`song_id`, `rating_class`), FOREIGN KEY(`song_id`, `rating_class`) REFERENCES `difficulties`(`song_id`, `rating_class`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ratingClass", + "columnName": "rating_class", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lang", + "columnName": "lang", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT" + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id", + "rating_class" + ] + }, + "indices": [ + { + "name": "index_difficulties_localized_song_id_rating_class_lang", + "unique": true, + "columnNames": [ + "song_id", + "rating_class", + "lang" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_difficulties_localized_song_id_rating_class_lang` ON `${TABLE_NAME}` (`song_id`, `rating_class`, `lang`)" + }, + { + "name": "index_difficulties_localized_lang", + "unique": false, + "columnNames": [ + "lang" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_difficulties_localized_lang` ON `${TABLE_NAME}` (`lang`)" + } + ], + "foreignKeys": [ + { + "table": "difficulties", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "song_id", + "rating_class" + ], + "referencedColumns": [ + "song_id", + "rating_class" + ] + } + ] + }, + { + "tableName": "charts_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song_id` TEXT NOT NULL, `rating_class` INTEGER NOT NULL, `constant` INTEGER NOT NULL, `notes` INTEGER, PRIMARY KEY(`song_id`, `rating_class`))", + "fields": [ + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ratingClass", + "columnName": "rating_class", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "constant", + "columnName": "constant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "song_id", + "rating_class" + ] + } + }, + { + "tableName": "play_results", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` BLOB NOT NULL, `song_id` TEXT NOT NULL, `rating_class` INTEGER NOT NULL, `score` INTEGER NOT NULL, `pure` INTEGER, `far` INTEGER, `lost` INTEGER, `date` INTEGER, `max_recall` INTEGER, `modifier` INTEGER, `clear_type` INTEGER, `comment` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "song_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ratingClass", + "columnName": "rating_class", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "score", + "columnName": "score", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pure", + "columnName": "pure", + "affinity": "INTEGER" + }, + { + "fieldPath": "far", + "columnName": "far", + "affinity": "INTEGER" + }, + { + "fieldPath": "lost", + "columnName": "lost", + "affinity": "INTEGER" + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER" + }, + { + "fieldPath": "maxRecall", + "columnName": "max_recall", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifier", + "columnName": "modifier", + "affinity": "INTEGER" + }, + { + "fieldPath": "clearType", + "columnName": "clear_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_play_results_uuid", + "unique": true, + "columnNames": [ + "uuid" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_play_results_uuid` ON `${TABLE_NAME}` (`uuid`)" + }, + { + "name": "index_play_results_song_id_rating_class", + "unique": false, + "columnNames": [ + "song_id", + "rating_class" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_play_results_song_id_rating_class` ON `${TABLE_NAME}` (`song_id`, `rating_class`)" + } + ] + }, + { + "tableName": "r30_entries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` BLOB NOT NULL, FOREIGN KEY(`uuid`) REFERENCES `play_results`(`uuid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_r30_entries_uuid", + "unique": false, + "columnNames": [ + "uuid" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_r30_entries_uuid` ON `${TABLE_NAME}` (`uuid`)" + } + ], + "foreignKeys": [ + { + "table": "play_results", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "uuid" + ], + "referencedColumns": [ + "uuid" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '695f3a8be7efe40bdffb285ef54f4a81')" + ] + } +} \ No newline at end of file diff --git a/core/src/androidTest/kotlin/xyz/sevive/arcaeaoffline/core/DatabaseMigrationsTest.kt b/core/src/androidTest/kotlin/xyz/sevive/arcaeaoffline/core/DatabaseMigrationsTest.kt index 4ac1eeba..9b5d2a42 100644 --- a/core/src/androidTest/kotlin/xyz/sevive/arcaeaoffline/core/DatabaseMigrationsTest.kt +++ b/core/src/androidTest/kotlin/xyz/sevive/arcaeaoffline/core/DatabaseMigrationsTest.kt @@ -15,7 +15,8 @@ import java.io.IOException @RunWith(AndroidJUnit4::class) class DatabaseMigrationsTest { - private val testDatabaseName = "arcaea-offline" + private val testDatabaseName7To8 = "arcaea-offline-7-8" + private val testDatabaseName14To15 = "arcaea-offline-14-15" @get:Rule val helper: MigrationTestHelper = @@ -30,7 +31,7 @@ class DatabaseMigrationsTest { val sqlFilename = "arcaea_offline_0.0.6.db.sql" var db = - helper.createDatabase(testDatabaseName, 7).apply { + helper.createDatabase(testDatabaseName7To8, 7).apply { // filling data execSqlScript(this, sqlFilename) @@ -39,7 +40,7 @@ class DatabaseMigrationsTest { } // perform migration - db = helper.runMigrationsAndValidate(testDatabaseName, 8, true, Migration_7_8) + db = helper.runMigrationsAndValidate(testDatabaseName7To8, 8, true, Migration_7_8) /** * schema passed @@ -70,6 +71,49 @@ class DatabaseMigrationsTest { } } + @Test + @Throws(IOException::class) + fun migrate14To15() { + helper.createDatabase(testDatabaseName14To15, 14).apply { + // NOT NULL columns must be provided; nullable ones are omitted. + execSQL( + "INSERT INTO difficulties " + + "(song_id, rating_class, rating, rating_plus, audio_override, jacket_override) " + + "VALUES ('test', 3, 110, 0, 0, 0)", + ) + close() + } + + val db = + helper.runMigrationsAndValidate(testDatabaseName14To15, 15, true) + + // Old rows get a null alias. + db + .query("SELECT rating_class_alias FROM difficulties WHERE song_id = 'test'") + .use { cursor -> + assertTrue(cursor.moveToFirst()) + assertTrue(cursor.isNull(0)) + } + + // The column accepts alias values. + db.execSQL("UPDATE difficulties SET rating_class_alias = 1 WHERE song_id = 'test'") + db + .query("SELECT rating_class_alias FROM difficulties WHERE song_id = 'test'") + .use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(1, cursor.getInt(0)) + } + + // The dropped `charts` view must not linger: a future `CREATE VIEW charts` + // would fail while the stale definition still exists. + db + .query("SELECT COUNT(*) FROM sqlite_master WHERE type = 'view' AND name = 'charts''") + .use { cursor -> + assertTrue(cursor.moveToFirst()) + assertEquals(0, cursor.getInt(0)) + } + } + private fun execSqlScript( db: SupportSQLiteDatabase, filename: String, diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/ArcaeaOfflineDatabase.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/ArcaeaOfflineDatabase.kt index 56070683..a47f1154 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/ArcaeaOfflineDatabase.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/ArcaeaOfflineDatabase.kt @@ -27,7 +27,6 @@ import xyz.sevive.arcaeaoffline.core.database.daos.R30EntryDao import xyz.sevive.arcaeaoffline.core.database.daos.RelationshipsDao import xyz.sevive.arcaeaoffline.core.database.daos.SongDao import xyz.sevive.arcaeaoffline.core.database.daos.SongLocalizedDao -import xyz.sevive.arcaeaoffline.core.database.entities.Chart import xyz.sevive.arcaeaoffline.core.database.entities.ChartInfo import xyz.sevive.arcaeaoffline.core.database.entities.Difficulty import xyz.sevive.arcaeaoffline.core.database.entities.DifficultyLocalized @@ -56,7 +55,6 @@ import xyz.sevive.arcaeaoffline.core.database.migrations.Migration_7_8 PlayResult::class, R30Entry::class, ], - views = [Chart::class], autoMigrations = [ AutoMigration(from = 4, to = 5), AutoMigration(from = 5, to = 6, spec = AutoMigration_5_6::class), @@ -66,8 +64,9 @@ import xyz.sevive.arcaeaoffline.core.database.migrations.Migration_7_8 AutoMigration(from = 11, to = 12), AutoMigration(from = 12, to = 13), AutoMigration(from = 13, to = 14), + AutoMigration(from = 14, to = 15), ], - version = 14, + version = 15, exportSchema = true, ) @TypeConverters( diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/ChartDao.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/ChartDao.kt index 00e3f93c..7a8e1e56 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/ChartDao.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/ChartDao.kt @@ -8,18 +8,83 @@ import xyz.sevive.arcaeaoffline.core.database.entities.Chart @Dao interface ChartDao { - @Query("SELECT * FROM charts WHERE song_id = :songId AND rating_class = :ratingClass") + // The former `charts` DatabaseView definition lives on as these explicit + // joins; keep them in sync with Chart's fields when the query changes. + @Query( + """SELECT + s.idx AS song_idx, d.song_id, d.rating_class, d.rating_class_alias, + d.rating, d.rating_plus, + COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, + s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, + s.audio_preview, s.audio_preview_end, s.side, + COALESCE(d.version, s.version) AS version, COALESCE(d.date, s.date) AS date, + COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, + s.bg_day, s.bg_night, s.source, s.source_copyright, + d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, + d.jacket_night, ci.constant, ci.notes + FROM difficulties d + INNER JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class + INNER JOIN songs s ON d.song_id = s.id + WHERE d.song_id = :songId AND d.rating_class = :ratingClass""", + ) fun find( songId: String, ratingClass: ArcaeaRatingClass, ): Flow - @Query("SELECT * FROM charts") + @Query( + """SELECT + s.idx AS song_idx, d.song_id, d.rating_class, d.rating_class_alias, + d.rating, d.rating_plus, + COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, + s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, + s.audio_preview, s.audio_preview_end, s.side, + COALESCE(d.version, s.version) AS version, COALESCE(d.date, s.date) AS date, + COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, + s.bg_day, s.bg_night, s.source, s.source_copyright, + d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, + d.jacket_night, ci.constant, ci.notes + FROM difficulties d + INNER JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class + INNER JOIN songs s ON d.song_id = s.id""", + ) fun findAll(): Flow> - @Query("SELECT * FROM charts WHERE song_id = :songId") + @Query( + """SELECT + s.idx AS song_idx, d.song_id, d.rating_class, d.rating_class_alias, + d.rating, d.rating_plus, + COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, + s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, + s.audio_preview, s.audio_preview_end, s.side, + COALESCE(d.version, s.version) AS version, COALESCE(d.date, s.date) AS date, + COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, + s.bg_day, s.bg_night, s.source, s.source_copyright, + d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, + d.jacket_night, ci.constant, ci.notes + FROM difficulties d + INNER JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class + INNER JOIN songs s ON d.song_id = s.id + WHERE d.song_id = :songId""", + ) fun findAllBySongId(songId: String): Flow> - @Query("SELECT * FROM charts WHERE song_id IN (:songIds)") + @Query( + """SELECT + s.idx AS song_idx, d.song_id, d.rating_class, d.rating_class_alias, + d.rating, d.rating_plus, + COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, + s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, + s.audio_preview, s.audio_preview_end, s.side, + COALESCE(d.version, s.version) AS version, COALESCE(d.date, s.date) AS date, + COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, + s.bg_day, s.bg_night, s.source, s.source_copyright, + d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, + d.jacket_night, ci.constant, ci.notes + FROM difficulties d + INNER JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class + INNER JOIN songs s ON d.song_id = s.id + WHERE d.song_id IN (:songIds)""", + ) fun findAllBySongIds(songIds: List): Flow> } diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/RelationshipsDao.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/RelationshipsDao.kt index e4be71c6..1735b918 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/RelationshipsDao.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/daos/RelationshipsDao.kt @@ -2,7 +2,6 @@ package xyz.sevive.arcaeaoffline.core.database.daos import androidx.room.Dao import androidx.room.Query -import androidx.room.RewriteQueriesToDropUnusedColumns import kotlinx.coroutines.flow.Flow import xyz.sevive.arcaeaoffline.core.database.entities.Chart import xyz.sevive.arcaeaoffline.core.database.entities.MinimumPlayResultPotentialFields @@ -10,14 +9,26 @@ import xyz.sevive.arcaeaoffline.core.database.entities.PlayResult @Dao interface RelationshipsDao { - @RewriteQueriesToDropUnusedColumns + // Duplicate column names across play_results and the chart join (song_id, + // rating_class, date) resolve to the play_results value; chart.songId and + // chart.ratingClass are correct by join equality, chart.date is not read + // by consumers of this query. @Query( """SELECT - * -FROM - play_results - LEFT JOIN charts ON play_results.song_id = charts.song_id - AND charts.rating_class = play_results.rating_class""", + pr.*, + s.idx AS song_idx, d.rating, d.rating_plus, d.rating_class_alias, + COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, + s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, + s.audio_preview, s.audio_preview_end, s.side, + COALESCE(d.version, s.version) AS version, + COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, + s.bg_day, s.bg_night, s.source, s.source_copyright, + d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, + d.jacket_night, ci.constant, ci.notes + FROM play_results pr + LEFT JOIN difficulties d ON pr.song_id = d.song_id AND pr.rating_class = d.rating_class + LEFT JOIN songs s ON d.song_id = s.id + LEFT JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class""", ) fun playResultsWithCharts(): Flow> diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Chart.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Chart.kt index 4d93106d..7072cde2 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Chart.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Chart.kt @@ -1,31 +1,13 @@ package xyz.sevive.arcaeaoffline.core.database.entities import androidx.room.ColumnInfo -import androidx.room.DatabaseView import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass -@DatabaseView( - """ - SELECT - s.idx AS song_idx, d.song_id, d.rating_class, d.rating, d.rating_plus, - COALESCE(d.title, s.title) AS title, COALESCE(d.artist, s.artist) AS artist, - s.`set`, COALESCE(d.bpm, s.bpm) AS bpm, COALESCE(d.bpm_base, s.bpm_base) AS bpm_base, - s.audio_preview, s.audio_preview_end, s.side, - COALESCE(d.version, s.version) AS version, COALESCE(d.date, s.date) AS date, - COALESCE(d.bg, s.bg) AS bg, COALESCE(d.bg_inverse, s.bg_inverse) AS bg_inverse, - s.bg_day, s.bg_night, s.source, s.source_copyright, - d.chart_designer, d.jacket_designer, d.audio_override, d.jacket_override, - d.jacket_night, ci.constant, ci.notes - FROM difficulties d - INNER JOIN charts_info ci ON d.song_id = ci.song_id AND d.rating_class = ci.rating_class - INNER JOIN songs s ON d.song_id = s.id - """, - "charts", -) data class Chart( @ColumnInfo(name = "song_idx") val songIdx: Int, @ColumnInfo(name = "song_id") val songId: String, @ColumnInfo(name = "rating_class") val ratingClass: ArcaeaRatingClass, + @ColumnInfo(name = "rating_class_alias") val ratingClassAlias: Int? = null, val rating: Int, @ColumnInfo(name = "rating_plus") val ratingPlus: Boolean, val title: String, diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Difficulty.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Difficulty.kt index 29fc1f49..2cee1608 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Difficulty.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/entities/Difficulty.kt @@ -8,6 +8,7 @@ import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass data class Difficulty( @ColumnInfo(name = "song_id") val songId: String, @ColumnInfo(name = "rating_class") val ratingClass: ArcaeaRatingClass, + @ColumnInfo(name = "rating_class_alias") val ratingClassAlias: Int? = null, val rating: Int, @ColumnInfo(name = "rating_plus") val ratingPlus: Boolean, @ColumnInfo(name = "chart_designer") val chartDesigner: String?, diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/ArcaeaListDefinitions.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/ArcaeaListDefinitions.kt index e6061c41..7b42241a 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/ArcaeaListDefinitions.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/ArcaeaListDefinitions.kt @@ -84,6 +84,7 @@ data class ArcaeaSonglistBgDayNightItem( @Serializable data class ArcaeaSonglistDifficultyItem( val ratingClass: Int, + val ratingClassAlias: Int? = null, val chartDesigner: String, val jacketDesigner: String, val rating: Int, diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporter.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporter.kt index 52024124..25b798e6 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporter.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporter.kt @@ -95,6 +95,7 @@ class ArcaeaSonglistImporter( Difficulty( songId = song.id, ratingClass = ArcaeaRatingClass.fromInt(difficulty.ratingClass), + ratingClassAlias = difficulty.ratingClassAlias, rating = difficulty.rating, ratingPlus = difficulty.ratingPlus ?: false, chartDesigner = difficulty.chartDesigner, diff --git a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/helpers/ChartFactory.kt b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/helpers/ChartFactory.kt index 649c2787..3d6a7028 100644 --- a/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/helpers/ChartFactory.kt +++ b/core/src/main/kotlin/xyz/sevive/arcaeaoffline/core/database/helpers/ChartFactory.kt @@ -13,6 +13,7 @@ object ChartFactory { songIdx = song.idx, songId = song.id, ratingClass = difficulty.ratingClass, + ratingClassAlias = difficulty.ratingClassAlias, rating = difficulty.rating, ratingPlus = difficulty.ratingPlus, title = difficulty.title ?: song.title, diff --git a/core/src/test/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporterTest.kt b/core/src/test/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporterTest.kt new file mode 100644 index 00000000..a419f28f --- /dev/null +++ b/core/src/test/kotlin/xyz/sevive/arcaeaoffline/core/database/externals/importers/ArcaeaSonglistImporterTest.kt @@ -0,0 +1,25 @@ +package xyz.sevive.arcaeaoffline.core.database.externals.importers + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import xyz.sevive.arcaeaoffline.core.constants.ArcaeaRatingClass + +class ArcaeaSonglistImporterTest { + private val fixture = + javaClass.classLoader!! + .getResource("songlist_alias_fixture.json")!! + .readText() + + @Test + fun ratingClassAliasTest() { + val difficulties = ArcaeaSonglistImporter(fixture).difficulties() + assertEquals(2, difficulties.size) + + val present = difficulties.single { it.ratingClass == ArcaeaRatingClass.PRESENT } + assertNull(present.ratingClassAlias) + + val beyond = difficulties.single { it.ratingClass == ArcaeaRatingClass.BEYOND } + assertEquals(1, beyond.ratingClassAlias) + } +} diff --git a/core/src/test/resources/songlist_alias_fixture.json b/core/src/test/resources/songlist_alias_fixture.json new file mode 100644 index 00000000..3ad10d52 --- /dev/null +++ b/core/src/test/resources/songlist_alias_fixture.json @@ -0,0 +1,34 @@ +{ + "songs": [ + { + "idx": 0, + "id": "aliastest", + "title_localized": { "en": "Alias Test" }, + "artist": "Test Artist", + "bpm": "100", + "bpm_base": 100.0, + "set": "testset", + "audioPreview": 0, + "audioPreviewEnd": 0, + "side": 0, + "bg": "testbg", + "date": 1720000000, + "version": "7.0", + "difficulties": [ + { + "ratingClass": 1, + "chartDesigner": "designer ant", + "jacketDesigner": "jl", + "rating": 7 + }, + { + "ratingClass": 3, + "chartDesigner": "designer ant", + "jacketDesigner": "jl", + "rating": 12, + "ratingClassAlias": 1 + } + ] + } + ] +}