2026-03-16 Chart Catalog And Levels Schema Update
This note records the 2026-03-16 change that simplified the backend levels table and the client bootstrap chart-directory flow.
Why this changed
The previous redesign pushed too much manifest-level data into MySQL and into the authenticated bootstrap response. That turned the levels table into a partial copy of local chart files.
That is not necessary for the current architecture:
- chart files and
manifest.ymlare still delivered through the resource sync pipeline - the backend only needs enough metadata to know which charts are enabled
- the client only needs enough metadata to decide whether a local song or difficulty file should be deserialized
Current model
The backend levels table is now a lightweight chart catalog. One row represents one playable chart.
Recommended core columns:
id:RawLevel.meta.levelIdname: chart display namedifficulty_constant: chart constantversion: chart or song version stringcreated_at: when the chart row was created in the server catalogis_active: whether this chart should be exposed to clients
Fields that already exist in local chart files are intentionally not duplicated here:
- icon
- composer
- unlock payloads
- comments
- tags
- resource-pack hashes
- other manifest-only presentation data
Server endpoint
The authenticated bootstrap endpoint is now:
GET /server/charts
Authorization: Bearer <serverAccessToken>
Example response:
[
{
"levelId": 100091,
"name": "Song Title",
"difficultyConstant": 10.5,
"version": "1.0",
"createdAt": "2026-03-16T20:30:00"
}
]
The endpoint only returns active charts.
Client flow
NetworkManager.initialize() now runs these bootstrap tasks in parallel after /server/login succeeds:
- resource integrity verification
GET /server/collections- remote resource-pack manifest loading
GET /server/charts
The response is stored as ActiveChartCatalog.
RawSongDeserializer then applies two checks:
- Each local difficulty file is checked by
meta.uid. - Only files whose
levelIdexists in the remote chart catalog are deserialized.
If the charts request fails or returns empty, the client falls back to the old behavior and loads every local chart under Data/Charts.
SQL scripts
Backend SQL artifacts now align with this lighter model:
src/main/resources/db/schema.sql- new environments use the minimal
levelsschema
- new environments use the minimal
src/main/resources/db/migrate_levels_to_catalog.sql- upgrades the legacy
levelstable into the lightweight chart catalog
- upgrades the legacy
src/main/resources/db/migrate_legacy_session_ids.sql- converts legacy numeric session foreign keys in
gameplay_recordsinto string session ids
- converts legacy numeric session foreign keys in
Compatibility note
RecordService.ensureLevelExists(...) still auto-creates a placeholder row if a record upload references an unknown level_id.
Those rows are intentionally minimal:
name = "Level <id>"version = unknownis_active = false
They exist only to keep uploads from failing before the real chart catalog is imported.