1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-09-04 04:38:09 +00:00
Funkin/source/funkin/data/song/SongRegistry.hx

559 lines
18 KiB
Haxe
Raw Permalink Normal View History

2023-09-08 21:45:47 +00:00
package funkin.data.song;
2024-11-06 21:42:43 +00:00
import funkin.data.freeplay.player.PlayerRegistry;
2023-09-08 21:45:47 +00:00
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;
2023-09-08 21:45:47 +00:00
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;
2023-09-08 21:45:47 +00:00
using funkin.data.song.migrator.SongDataMigrator;
2025-08-11 21:07:20 +00:00
@:nullSafety class SongRegistry extends BaseRegistry<Song, SongMetadata, SongEntryParams> implements ISingleton implements DefaultRegistryImpl
2023-09-08 21:45:47 +00:00
{
/**
* 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";
2023-09-08 21:45:47 +00:00
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
2023-09-08 21:45:47 +00:00
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;
2023-09-08 21:45:47 +00:00
2025-05-27 07:20:50 +00:00
public var scriptedSongVariations:Map<String, Song> = new Map<String, Song>();
2023-09-08 21:45:47 +00:00
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<String> = getScriptedClassNames();
2024-02-07 23:45:13 +00:00
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
2023-09-08 21:45:47 +00:00
for (entryCls in scriptedEntryClassNames)
{
var entry:Song = createScriptedEntry(entryCls);
if (entry != null)
{
2025-05-27 07:20:50 +00:00
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})');
}
2023-09-08 21:45:47 +00:00
}
else
{
log('Failed to create scripted entry (${entryCls})');
}
}
//
// UNSCRIPTED ENTRIES
//
var entryIdList:Array<String> = DataAssets.listDataFilesInPath('songs/', '-metadata.json').map(function(songDataPath:String):String {
return songDataPath.split('/')[0];
});
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId);
});
2024-02-07 23:45:13 +00:00
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
2023-09-08 21:45:47 +00:00
for (entryId in unscriptedEntryIds)
{
try
{
var entry:Null<Song> = createEntry(entryId);
2023-09-08 21:45:47 +00:00
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<SongMetadata>
{
return parseEntryMetadata(id);
}
/**
* Parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
return parseEntryMetadataRaw(contents);
}
2025-05-27 07:20:50 +00:00
/**
* We override `fetchEntry` to handle song variations!
*/
public override function fetchEntry(id:String, ?params:SongEntryParams):Null<Song>
2025-05-27 07:20:50 +00:00
{
var variation:String = params?.variation ?? Constants.DEFAULT_VARIATION;
if (variation != Constants.DEFAULT_VARIATION)
2025-05-27 07:20:50 +00:00
{
if (scriptedSongVariations.exists('${id}:${variation}'))
2025-05-27 07:20:50 +00:00
{
var variationSongScript:Null<Song> = scriptedSongVariations.get('${id}:${variation}');
2025-05-27 07:20:50 +00:00
if (variationSongScript != null)
{
return variationSongScript;
}
}
}
return super.fetchEntry(id, params);
}
2023-09-26 03:24:18 +00:00
public function parseEntryMetadata(id:String, ?variation:String):Null<SongMetadata>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
2023-09-08 21:45:47 +00:00
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = true;
2023-12-19 06:23:42 +00:00
switch (loadEntryMetadataFile(id, variation))
2023-09-08 21:45:47 +00:00
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
2023-09-26 03:24:18 +00:00
return cleanMetadata(parser.value, variation);
2023-09-08 21:45:47 +00:00
}
2023-09-26 03:24:18 +00:00
public function parseEntryMetadataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null<SongMetadata>
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
2023-09-26 03:24:18 +00:00
return cleanMetadata(parser.value, variation);
}
public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
2023-09-08 21:45:47 +00:00
{
2025-03-03 18:03:01 +00:00
variation = variation ?? Constants.DEFAULT_VARIATION;
2023-09-26 03:24:18 +00:00
2023-09-08 21:45:47 +00:00
// 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))
{
2023-09-26 03:24:18 +00:00
return parseEntryMetadata(id, variation);
2023-09-08 21:45:47 +00:00
}
else if (VersionUtil.validateVersion(version, "2.1.x"))
{
return parseEntryMetadata_v2_1_0(id, variation);
}
else if (VersionUtil.validateVersion(version, "2.0.x"))
{
2023-09-26 03:24:18 +00:00
return parseEntryMetadata_v2_0_0(id, variation);
}
2023-09-08 21:45:47 +00:00
else
{
2023-09-26 03:24:18 +00:00
throw '[${registryId}] Metadata entry ${id}:${variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
2023-09-08 21:45:47 +00:00
}
}
public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version,
?variation:String):Null<SongMetadata>
{
// 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<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
parser.ignoreUnknownVariables = true;
2023-12-19 06:23:42 +00:00
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<SongMetadata>
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = true;
2023-12-19 06:23:42 +00:00
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<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
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<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value.migrate();
}
2023-09-26 03:24:18 +00:00
public function parseMusicData(id:String, ?variation:String):Null<SongMusicData>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
2023-09-08 21:45:47 +00:00
var parser = new json2object.JsonParser<SongMusicData>();
2023-12-19 06:23:42 +00:00
parser.ignoreUnknownVariables = false;
switch (loadMusicDataFile(id, variation))
2023-09-08 21:45:47 +00:00
{
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<SongMusicData>
{
var parser = new json2object.JsonParser<SongMusicData>();
2023-12-19 06:23:42 +00:00
parser.ignoreUnknownVariables = false;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
2023-09-26 03:24:18 +00:00
public function parseMusicDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongMusicData>
{
2023-09-26 03:24:18 +00:00
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<SongMusicData>
{
// 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}.';
}
}
2023-09-26 03:24:18 +00:00
public function parseEntryChartData(id:String, ?variation:String):Null<SongChartData>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
2023-09-08 21:45:47 +00:00
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = true;
2023-09-08 21:45:47 +00:00
switch (loadEntryChartFile(id, variation))
2023-09-08 21:45:47 +00:00
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
2023-09-26 03:24:18 +00:00
return cleanChartData(parser.value, variation);
2023-09-08 21:45:47 +00:00
}
2023-09-26 03:24:18 +00:00
public function parseEntryChartDataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null<SongChartData>
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.ignoreUnknownVariables = true;
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
2023-09-26 03:24:18 +00:00
return cleanChartData(parser.value, variation);
}
2023-09-26 03:24:18 +00:00
public function parseEntryChartDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongChartData>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
2023-09-08 21:45:47 +00:00
// 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
{
2023-09-26 03:24:18 +00:00
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
2023-09-08 21:45:47 +00:00
}
}
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongChartData>
{
// 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}.';
}
}
2024-03-26 16:33:54 +00:00
function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
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<String> = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
rawJson = rawJson.trim();
2023-09-08 21:45:47 +00:00
return {fileName: entryFilePath, contents: rawJson};
}
2024-03-26 16:33:54 +00:00
function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
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();
2023-09-08 21:45:47 +00:00
return {fileName: entryFilePath, contents: rawJson};
}
2024-03-12 03:42:32 +00:00
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);
}
2024-03-26 16:33:54 +00:00
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
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();
2023-09-08 21:45:47 +00:00
return {fileName: entryFilePath, contents: rawJson};
}
2023-09-26 03:24:18 +00:00
public function fetchEntryMetadataVersion(id:String, ?variation:String):Null<thx.semver.Version>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
2023-09-08 21:45:47 +00:00
return entryVersion;
}
2023-09-26 03:24:18 +00:00
public function fetchEntryChartVersion(id:String, ?variation:String):Null<thx.semver.Version>
2023-09-08 21:45:47 +00:00
{
2023-09-26 03:24:18 +00:00
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
2023-09-08 21:45:47 +00:00
return entryVersion;
}
2023-09-26 03:24:18 +00:00
function cleanMetadata(metadata:SongMetadata, variation:String):SongMetadata
{
metadata.variation = variation;
return metadata;
}
function cleanChartData(chartData:SongChartData, variation:String):SongChartData
{
chartData.variation = variation;
return chartData;
}
2024-11-06 21:42:43 +00:00
/**
* A list of all difficulties for a specific character.
*/
public function listAllDifficulties(characterId:String):Array<String>
{
var allDifficulties:Array<String> = 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;
}
2023-09-08 21:45:47 +00:00
}
2025-08-11 21:07:20 +00:00
typedef SongEntryParams =
{
/**
* The variation ID for the song.
*/
var variation:String;
}