package funkin.data.song; import funkin.data.freeplay.player.PlayerRegistry; import funkin.data.song.SongData; import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; import funkin.data.song.migrator.SongData_v2_1_0.SongMetadata_v2_1_0; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; import funkin.play.song.ScriptedSong; import funkin.play.song.Song; import funkin.util.assets.DataAssets; import funkin.util.VersionUtil; import funkin.util.tools.ISingleton; import funkin.data.DefaultRegistryImpl; using funkin.data.song.migrator.SongDataMigrator; @:nullSafety class SongRegistry extends BaseRegistry implements ISingleton implements DefaultRegistryImpl { /** * The current version string for the stage data format. * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.4"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0"; public static final SONG_CHART_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; public static final SONG_MUSIC_DATA_VERSION:thx.semver.Version = "2.0.0"; public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; public static var DEFAULT_GENERATEDBY(get, never):String; public var scriptedSongVariations:Map = new Map(); static function get_DEFAULT_GENERATEDBY():String { return '${Constants.TITLE} - ${Constants.VERSION}'; } public function new() { super('SONG', 'songs', SONG_METADATA_VERSION_RULE); } public override function loadEntries():Void { clearEntries(); // // SCRIPTED ENTRIES // var scriptedEntryClassNames:Array = getScriptedClassNames(); log('Parsing ${scriptedEntryClassNames.length} scripted entries...'); for (entryCls in scriptedEntryClassNames) { var entry:Song = createScriptedEntry(entryCls); if (entry != null) { if (entry.variation != null) { scriptedSongVariations.set('${entry.id}:${entry.variation}', entry); log('Successfully created scripted entry (${entryCls} = ${entry.id}, ${entry.variation})'); } else { entries.set(entry.id, entry); scriptedEntryIds.set(entry.id, entryCls); log('Successfully created scripted entry (${entryCls} = ${entry.id})'); } } else { log('Failed to create scripted entry (${entryCls})'); } } // // UNSCRIPTED ENTRIES // var entryIdList:Array = DataAssets.listDataFilesInPath('songs/', '-metadata.json').map(function(songDataPath:String):String { return songDataPath.split('/')[0]; }); var unscriptedEntryIds:Array = entryIdList.filter(function(entryId:String):Bool { return !entries.exists(entryId); }); log('Parsing ${unscriptedEntryIds.length} unscripted entries...'); for (entryId in unscriptedEntryIds) { try { var entry:Null = createEntry(entryId); if (entry != null) { trace(' Loaded entry data: ${entry}'); entries.set(entry.id, entry); } } catch (e:Dynamic) { // Print the error. trace(' Failed to load entry data: ${entryId}'); trace(e); continue; } } } /** * Read, parse, and validate the JSON data and produce the corresponding data object. */ public function parseEntryData(id:String):Null { return parseEntryMetadata(id); } /** * Parse, and validate the JSON data and produce the corresponding data object. */ public function parseEntryDataRaw(contents:String, ?fileName:String = 'raw'):Null { return parseEntryMetadataRaw(contents); } /** * We override `fetchEntry` to handle song variations! */ public override function fetchEntry(id:String, ?params:SongEntryParams):Null { var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION; if (variation != Constants.DEFAULT_VARIATION) { if (scriptedSongVariations.exists('${id}:${variation}')) { var variationSongScript:Null = scriptedSongVariations.get('${id}:${variation}'); if (variationSongScript != null) { return variationSongScript; } } } return super.fetchEntry(id, params); } public function parseEntryMetadata(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return cleanMetadata(parser.value, variation); } public function parseEntryMetadataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return cleanMetadata(parser.value, variation); } public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null { variation = variation ?? Constants.DEFAULT_VARIATION; // If a version rule is not specified, do not check against it. if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE)) { return parseEntryMetadata(id, variation); } else if (VersionUtil.validateVersion(version, "2.1.x")) { return parseEntryMetadata_v2_1_0(id, variation); } else if (VersionUtil.validateVersion(version, "2.0.x")) { return parseEntryMetadata_v2_0_0(id, variation); } else { throw '[${registryId}] Metadata entry ${id}:${variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.'; } } public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version, ?variation:String):Null { // If a version rule is not specified, do not check against it. if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE)) { return parseEntryMetadataRaw(contents, fileName, variation); } else if (VersionUtil.validateVersion(version, "2.1.x")) { return parseEntryMetadataRaw_v2_1_0(contents, fileName); } else if (VersionUtil.validateVersion(version, "2.0.x")) { return parseEntryMetadataRaw_v2_0_0(contents, fileName); } else { throw '[${registryId}] Metadata entry "${fileName}" does not support migration to version ${SONG_METADATA_VERSION_RULE}.'; } } function parseEntryMetadata_v2_1_0(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return cleanMetadata(parser.value.migrate(), variation); } function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; switch (loadEntryMetadataFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return cleanMetadata(parser.value.migrate(), variation); } function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return parser.value.migrate(); } function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return parser.value.migrate(); } public function parseMusicData(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; switch (loadMusicDataFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return parser.value; } public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = false; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return parser.value; } public function parseMusicDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; // If a version rule is not specified, do not check against it. if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE)) { return parseMusicData(id, variation); } else { throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.'; } } public function parseMusicDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null { // If a version rule is not specified, do not check against it. if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE)) { return parseMusicDataRaw(contents, fileName); } else { throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.'; } } public function parseEntryChartData(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; switch (loadEntryChartFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); default: return null; } if (parser.errors.length > 0) { printErrors(parser.errors, id); return null; } return cleanChartData(parser.value, variation); } public function parseEntryChartDataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; parser.fromJson(contents, fileName); if (parser.errors.length > 0) { printErrors(parser.errors, fileName); return null; } return cleanChartData(parser.value, variation); } public function parseEntryChartDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; // If a version rule is not specified, do not check against it. if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE)) { return parseEntryChartData(id, variation); } else { throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; } } public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null { // If a version rule is not specified, do not check against it. if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE)) { return parseEntryChartDataRaw(contents, fileName); } else { throw '[${registryId}] Chart entry "${fileName}" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.'; } } function loadEntryMetadataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); if (!openfl.Assets.exists(entryFilePath)) { trace(' [WARN] Could not locate file $entryFilePath'); return null; } var rawJson:Null = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; rawJson = rawJson.trim(); return {fileName: entryFilePath, contents: rawJson}; } function loadMusicDataFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); if (!openfl.Assets.exists(entryFilePath)) return null; var rawJson:String = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; rawJson = rawJson.trim(); return {fileName: entryFilePath, contents: rawJson}; } function hasMusicDataFile(id:String, ?variation:String):Bool { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json'); return openfl.Assets.exists(entryFilePath); } function loadEntryChartFile(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}'); if (!openfl.Assets.exists(entryFilePath)) return null; var rawJson:String = openfl.Assets.getText(entryFilePath); if (rawJson == null) return null; rawJson = rawJson.trim(); return {fileName: entryFilePath, contents: rawJson}; } public function fetchEntryMetadataVersion(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryMetadataFile(id, variation)?.contents; var entryVersion:Null = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } public function fetchEntryChartVersion(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; var entryStr:Null = loadEntryChartFile(id, variation)?.contents; var entryVersion:Null = VersionUtil.getVersionFromJSON(entryStr); return entryVersion; } function cleanMetadata(metadata:SongMetadata, variation:String):SongMetadata { metadata.variation = variation; return metadata; } function cleanChartData(chartData:SongChartData, variation:String):SongChartData { chartData.variation = variation; return chartData; } /** * A list of all difficulties for a specific character. */ public function listAllDifficulties(characterId:String):Array { var allDifficulties:Array = Constants.DEFAULT_DIFFICULTY_LIST.copy(); var character = PlayerRegistry.instance.fetchEntry(characterId); if (character == null) { trace(' [WARN] Could not locate character $characterId'); return allDifficulties; } allDifficulties = []; for (songId in listEntryIds()) { var song = fetchEntry(songId); if (song == null) continue; for (diff in song.listDifficulties(null, song.getVariationsByCharacter(character))) { if (!allDifficulties.contains(diff)) allDifficulties.push(diff); } } if (allDifficulties.length == 0) { trace(' [WARN] No difficulties found. Returning default difficulty list.'); allDifficulties = Constants.DEFAULT_DIFFICULTY_LIST.copy(); } return allDifficulties; } } typedef SongEntryParams = { /** * The variation ID for the song. */ var variation:String; }