feat: Variation-specific song scripts

This commit is contained in:
Abnormal
2025-08-21 09:51:01 -05:00
parent 3d8b9a18b3
commit c94d6bbb66
18 changed files with 90 additions and 30 deletions
+1 -1
Submodule assets updated: e77b27834e...8d24090af6
+3 -3
View File
@@ -5,9 +5,9 @@ import funkin.util.VersionUtil;
import haxe.Constraints.Constructible;
/**
* The entry's constructor function must take a single argument, the entry's ID.
* The entry's constructor function takes 2 arguments, the entry ID and optional parameters.
*/
typedef EntryConstructorFunction = String->Void;
typedef EntryConstructorFunction = (String, ?Dynamic) -> Void;
/**
* A base type for a Registry, which is an object which handles loading scriptable objects.
@@ -186,7 +186,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param id The ID of the entry to fetch.
* @return The entry, or `null` if it does not exist.
*/
public function fetchEntry(id:String):Null<T>
public function fetchEntry(id:String, ?params:Null<Dynamic>):Null<T>
{
return entries.get(id);
}
+34 -5
View File
@@ -15,8 +15,7 @@ import funkin.data.DefaultRegistryImpl;
using funkin.data.song.migrator.SongDataMigrator;
@:nullSafety
class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleton implements DefaultRegistryImpl
@:nullSafety class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleton implements DefaultRegistryImpl
{
/**
* The current version string for the stage data format.
@@ -37,6 +36,8 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
public static var DEFAULT_GENERATEDBY(get, never):String;
public var scriptedSongVariations:Map<String, Song> = new Map<String, Song>();
static function get_DEFAULT_GENERATEDBY():String
{
return '${Constants.TITLE} - ${Constants.VERSION}';
@@ -63,9 +64,17 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
if (entry != null)
{
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry);
scriptedEntryIds.set(entry.id, entryCls);
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
{
@@ -120,6 +129,26 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
return parseEntryMetadataRaw(contents);
}
/**
* We override `fetchEntry` to handle song variations!
*/
public override function fetchEntry(id:String, ?params:Null<Dynamic>):Null<Song>
{
if (params != null && params.variation != null)
{
if (scriptedSongVariations.exists('${id}:${params.variation}'))
{
var variationSongScript:Null<Song> = scriptedSongVariations.get('${id}:${params.variation}');
if (variationSongScript != null)
{
return variationSongScript;
}
}
}
return super.fetchEntry(id, params);
}
public function parseEntryMetadata(id:String, ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
+2 -1
View File
@@ -939,7 +939,8 @@ class PauseSubState extends MusicBeatSubState
*/
static function changeDifficulty(state:PauseSubState, difficulty:String):Void
{
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase(),
{variation: PlayState.instance.currentChart.variation});
// Reset campaign score when changing difficulty
// So if you switch difficulty on the last song of a week you get a really low overall score.
+2 -1
View File
@@ -1706,7 +1706,8 @@ class PlayState extends MusicBeatSubState
{
throw "No lastParams to refer to";
}
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id) ?? throw "Could not load current song from ID. This shouldn't happen!";
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id,
{variation: currentVariation}) ?? throw "Could not load current song from ID. This shouldn't happen!";
LoadingState.loadPlayState(lastParams);
}
+4 -1
View File
@@ -832,7 +832,10 @@ class ResultState extends MusicBeatSubState
var stickerPackId:Null<String> = null;
var song:Null<Song> = params.songId == null ? null : SongRegistry.instance.fetchEntry(params.songId);
var song:Null<Song> = params.songId == null ? null : SongRegistry.instance.fetchEntry(params.songId,
{
variation: params?.variationId
});
if (song != null)
{
@@ -89,7 +89,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
var currentDialogueBox:Null<DialogueBox>;
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
super();
@@ -106,7 +106,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
return FullScreenScaleMode.wideScale.x - 0.05;
}
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
super();
this.id = id;
@@ -86,7 +86,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
return FullScreenScaleMode.wideScale.x - 0.05;
}
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
super();
@@ -39,7 +39,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
/**
* @param id The ID of the JSON file to parse.
*/
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
_data = _fetchData(id);
+13 -2
View File
@@ -131,13 +131,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return Constants.DEFAULT_CHARTER;
}
public var variation:Null<String> = null;
/**
* @param id The ID of the song to load.
* @param ignoreErrors If false, an exception will be thrown if the song data could not be loaded.
* @param targetVariation The variation to load, optional.
*/
public function new(id:String)
public function new(id:String, ?params:SongParams)
{
this.id = id;
this.variation = params?.variation;
difficulties = new Map<String, Map<String, SongDifficulty>>();
@@ -968,3 +971,11 @@ class SongDifficulty
return result;
}
}
typedef SongParams =
{
/**
* The variation to use for this song.
*/
variation:String
}
+1 -1
View File
@@ -62,7 +62,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
*
* @param id
*/
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
super();
+1 -1
View File
@@ -12,7 +12,7 @@ import flixel.graphics.FlxGraphic;
@:nullSafety
class Album implements IRegistryEntry<AlbumData>
{
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
this._data = _fetchData(id);
+21 -6
View File
@@ -380,7 +380,7 @@ class FreeplayState extends MusicBeatSubState
continue;
}
songs.push(new FreeplaySongData(song, level));
songs.push(new FreeplaySongData(songId, level, this));
}
}
@@ -2548,8 +2548,9 @@ class FreeplayState extends MusicBeatSubState
PlayStatePlaylist.isStoryMode = false;
var targetVariation:Null<String> = currentVariation;
var targetSongId:String = cap?.freeplayData?.data.id ?? 'unknown';
var targetSongNullable:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
var targetSongNullable:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId, {variation: targetVariation});
if (targetSongNullable == null)
{
FlxG.log.warn('WARN: could not find song with id (${targetSongId})');
@@ -2558,8 +2559,11 @@ class FreeplayState extends MusicBeatSubState
return;
}
var targetSong:Song = targetSongNullable;
var targetVariation:Null<String> = currentVariation;
var targetDifficultyId:String = currentDifficulty;
var targetLevelId:Null<String> = cap?.freeplayData?.levelId;
trace('target song: ${targetSongId} (${targetVariation})');
PlayStatePlaylist.campaignId = targetLevelId ?? null;
var targetDifficulty:Null<SongDifficulty> = targetSong.getDifficulty(currentDifficulty, currentVariation);
@@ -3038,7 +3042,13 @@ class FreeplaySongData
*
* @see Song
*/
public var data:Song;
public var data(get, never):Song;
function get_data():Song
{
@:privateAccess
return SongRegistry.instance.fetchEntry(songId, {variation: instance.currentVariation});
}
/**
* The level id of the song, useful for sorting from week1 -> week 7 + weekend1
@@ -3053,6 +3063,8 @@ class FreeplaySongData
var _levelId:String;
final songId:String;
/**
* Whether or not the song has been favorited.
*/
@@ -3088,10 +3100,13 @@ class FreeplaySongData
public var scoringRank(get, never):Null<ScoringRank>;
public function new(data:Song, levelData:Level)
public var instance:FreeplayState;
public function new(songId:String, levelData:Level, instance:FreeplayState)
{
this.data = data;
this.songId = songId;
_levelId = levelData.id;
this.instance = instance;
}
/**
+1 -1
View File
@@ -21,7 +21,7 @@ class FreeplayStyle implements IRegistryEntry<FreeplayStyleData>
* The full data for a freeplay style.
*/
// public final _data:FreeplayStyleData;
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
this._data = _fetchData(id);
@@ -15,7 +15,7 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
/**
* @param id The ID of the JSON file to parse.
*/
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
_data = _fetchData(id);
+1 -1
View File
@@ -18,7 +18,7 @@ class Level implements IRegistryEntry<LevelData>
/**
* @param id The ID of the JSON file to parse.
*/
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
_data = _fetchData(id);
@@ -20,7 +20,7 @@ class StickerPack implements IRegistryEntry<StickerData>
*/
public final _data:StickerData;
public function new(id:String)
public function new(id:String, ?params:Dynamic)
{
this.id = id;
this._data = _fetchData(id);