Remove funkin.play.song.SongData and refactor app to match.

This commit is contained in:
EliteMasterEric 2023-09-08 17:46:44 -04:00
parent dfedaa8838
commit f4bc682ea1
40 changed files with 339 additions and 1693 deletions

6
docs/troubleshooting.md Normal file
View file

@ -0,0 +1,6 @@
# Troubleshooting Common Issues
- Weird macro error with a very tall call stack: Restart Visual Studio Code
- `Get Thread Context Failed`: Turn off other expensive applications while building
- `Type not found: T1`: This is thrown by `json2object`, make sure the data type of `@:default` is correct.
- NOTE: `flixel.util.typeLimit.OneOfTwo` isn't supported.

View file

@ -4,7 +4,7 @@ import funkin.util.Constants;
import flixel.util.FlxSignal; import flixel.util.FlxSignal;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import funkin.play.song.Song.SongDifficulty; import funkin.play.song.Song.SongDifficulty;
import funkin.play.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
/** /**
* A core class which handles musical timing throughout the game, * A core class which handles musical timing throughout the game,

View file

@ -37,7 +37,7 @@ class DialogueBox extends FlxSpriteGroup
{ {
super(); super();
switch (PlayState.instance.currentSong.songId.toLowerCase()) switch (PlayState.instance.currentSong.id.toLowerCase())
{ {
case 'senpai': case 'senpai':
FlxG.sound.playMusic(Paths.music('Lunchbox'), 0); FlxG.sound.playMusic(Paths.music('Lunchbox'), 0);
@ -78,7 +78,7 @@ class DialogueBox extends FlxSpriteGroup
box = new FlxSprite(-20, 45); box = new FlxSprite(-20, 45);
var hasDialog:Bool = false; var hasDialog:Bool = false;
switch (PlayState.instance.currentSong.songId.toLowerCase()) switch (PlayState.instance.currentSong.id.toLowerCase())
{ {
case 'senpai': case 'senpai':
hasDialog = true; hasDialog = true;
@ -150,8 +150,8 @@ class DialogueBox extends FlxSpriteGroup
override function update(elapsed:Float):Void override function update(elapsed:Float):Void
{ {
// HARD CODING CUZ IM STUPDI // HARD CODING CUZ IM STUPDI
if (PlayState.instance.currentSong.songId.toLowerCase() == 'roses') portraitLeft.visible = false; if (PlayState.instance.currentSong.id.toLowerCase() == 'roses') portraitLeft.visible = false;
if (PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') if (PlayState.instance.currentSong.id.toLowerCase() == 'thorns')
{ {
portraitLeft.color = FlxColor.BLACK; portraitLeft.color = FlxColor.BLACK;
swagDialogue.color = FlxColor.WHITE; swagDialogue.color = FlxColor.WHITE;
@ -187,8 +187,8 @@ class DialogueBox extends FlxSpriteGroup
{ {
isEnding = true; isEnding = true;
if (PlayState.instance.currentSong.songId.toLowerCase() == 'senpai' if (PlayState.instance.currentSong.id.toLowerCase() == 'senpai'
|| PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0); || PlayState.instance.currentSong.id.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0);
new FlxTimer().start(0.2, function(tmr:FlxTimer) { new FlxTimer().start(0.2, function(tmr:FlxTimer) {
box.alpha -= 1 / 5; box.alpha -= 1 / 5;

View file

@ -20,6 +20,7 @@ import flixel.text.FlxText;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.data.song.SongRegistry;
import flixel.util.FlxSpriteUtil; import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.Controls.Control; import funkin.Controls.Control;
@ -30,7 +31,6 @@ import funkin.freeplayStuff.LetterSort;
import funkin.freeplayStuff.SongMenuItem; import funkin.freeplayStuff.SongMenuItem;
import funkin.play.HealthIcon; import funkin.play.HealthIcon;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser;
import funkin.shaderslmfao.AngleMask; import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor; import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader; import funkin.shaderslmfao.StrokeShader;
@ -843,7 +843,8 @@ class FreeplayState extends MusicBeatSubState
}*/ }*/
PlayStatePlaylist.isStoryMode = false; PlayStatePlaylist.isStoryMode = false;
var targetSong:Song = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase()); var songId:String = songs[curSelected].songName.toLowerCase();
var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
var targetDifficulty:String = switch (curDifficulty) var targetDifficulty:String = switch (curDifficulty)
{ {
case 0: case 0:

View file

@ -17,11 +17,11 @@ import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.event.SongEventData.SongEventParser; import funkin.data.event.SongEventData.SongEventParser;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
import funkin.play.stage.StageData.StageDataParser; import funkin.play.stage.StageData.StageDataParser;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
@ -197,13 +197,13 @@ class InitState extends FlxState
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names, // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
// to ensure build macros work properly. // to ensure build macros work properly.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache(); SongEventParser.loadEventCache();
ConversationDataParser.loadConversationCache(); ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache(); DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache(); SpeakerDataParser.loadSpeakerCache();
SongDataParser.loadSongCache();
StageDataParser.loadStageCache(); StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache(); CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks(); ModuleHandler.buildModuleCallbacks();
@ -276,7 +276,7 @@ class InitState extends FlxState
*/ */
function startSong(songId:String, difficultyId:String = 'normal'):Void function startSong(songId:String, difficultyId:String = 'normal'):Void
{ {
var songData:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
if (songData == null) if (songData == null)
{ {
@ -312,7 +312,7 @@ class InitState extends FlxState
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift(); var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(targetSongId); var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
LoadingState.loadAndSwitchState(new funkin.play.PlayState( LoadingState.loadAndSwitchState(new funkin.play.PlayState(
{ {

View file

@ -159,7 +159,7 @@ class LoadingState extends MusicBeatState
static function getSongPath():String static function getSongPath():String
{ {
return Paths.inst(PlayState.instance.currentSong.songId); return Paths.inst(PlayState.instance.currentSong.id);
} }
inline static public function loadAndSwitchState(nextState:FlxState, shouldStopMusic = false):Void inline static public function loadAndSwitchState(nextState:FlxState, shouldStopMusic = false):Void

View file

@ -10,7 +10,7 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
class PauseSubState extends MusicBeatSubState class PauseSubState extends MusicBeatSubState
{ {
@ -197,7 +197,7 @@ class PauseSubState extends MusicBeatSubState
regenMenu(); regenMenu();
case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT': case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
PlayState.instance.currentSong = SongDataParser.fetchSong(PlayState.instance.currentSong.songId.toLowerCase()); PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
PlayState.instance.currentDifficulty = daSelected.toLowerCase(); PlayState.instance.currentDifficulty = daSelected.toLowerCase();

View file

@ -3,18 +3,19 @@ package funkin.modding;
import funkin.util.macro.ClassMacro; import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.SongData; import funkin.data.song.SongData;
import funkin.play.stage.StageData; import funkin.play.stage.StageData;
import polymod.Polymod; import polymod.Polymod;
import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat; import polymod.format.ParseRules.TextFileFormat;
import funkin.play.event.SongEventData.SongEventParser; import funkin.data.event.SongEventData.SongEventParser;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
class PolymodHandler class PolymodHandler
{ {
@ -290,13 +291,13 @@ class PolymodHandler
// These MUST be imported at the top of the file and not referred to by fully qualified name, // These MUST be imported at the top of the file and not referred to by fully qualified name,
// to ensure build macros work properly. // to ensure build macros work properly.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache(); SongEventParser.loadEventCache();
ConversationDataParser.loadConversationCache(); ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache(); DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache(); SpeakerDataParser.loadSpeakerCache();
SongDataParser.loadSongCache();
StageDataParser.loadStageCache(); StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache(); CharacterDataParser.loadCharacterCache();
ModuleHandler.loadModuleCache(); ModuleHandler.loadModuleCache();

View file

@ -1,6 +1,6 @@
package funkin.modding.events; package funkin.modding.events;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import flixel.FlxState; import flixel.FlxState;
import flixel.FlxSubState; import flixel.FlxSubState;
import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteSprite;
@ -435,9 +435,9 @@ class SongEventScriptEvent extends ScriptEvent
* The note associated with this event. * The note associated with this event.
* You cannot replace it, but you can edit it. * You cannot replace it, but you can edit it.
*/ */
public var event(default, null):funkin.play.song.SongData.SongEventData; public var event(default, null):funkin.data.song.SongData.SongEventData;
public function new(event:funkin.play.song.SongData.SongEventData):Void public function new(event:funkin.data.song.SongData.SongEventData):Void
{ {
super(ScriptEvent.SONG_EVENT, true); super(ScriptEvent.SONG_EVENT, true);
this.event = event; this.event = event;

View file

@ -35,7 +35,7 @@ import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.VanillaCutscenes; import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene; import funkin.play.cutscene.VideoCutscene;
import funkin.play.event.SongEventData.SongEventParser; import funkin.data.event.SongEventData.SongEventParser;
import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteSprite;
import funkin.play.notes.NoteDirection; import funkin.play.notes.NoteDirection;
import funkin.play.notes.Strumline; import funkin.play.notes.Strumline;
@ -43,10 +43,10 @@ import funkin.play.notes.SustainTrail;
import funkin.play.scoring.Scoring; import funkin.play.scoring.Scoring;
import funkin.NoteSplash; import funkin.NoteSplash;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar; import funkin.data.song.SongData.SongPlayableChar;
import funkin.play.stage.Stage; import funkin.play.stage.Stage;
import funkin.play.stage.StageData.StageDataParser; import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.PopUpStuff; import funkin.ui.PopUpStuff;
@ -630,7 +630,7 @@ class PlayState extends MusicBeatSubState
startingSong = true; startingSong = true;
// TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead. // TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead.
if ((currentSong?.songId ?? '').toLowerCase() == 'winter-horrorland') if ((currentSong?.id ?? '').toLowerCase() == 'winter-horrorland')
{ {
// VanillaCutscenes will call startCountdown later. // VanillaCutscenes will call startCountdown later.
VanillaCutscenes.playHorrorStartCutscene(); VanillaCutscenes.playHorrorStartCutscene();
@ -2495,9 +2495,9 @@ class PlayState extends MusicBeatSubState
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
// crackhead double thingie, sets whether was new highscore, AND saves the song! // crackhead double thingie, sets whether was new highscore, AND saves the song!
Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.songId, songScore, currentDifficulty); Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.id, songScore, currentDifficulty);
Highscore.saveCompletionForDifficulty(currentSong.songId, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty); Highscore.saveCompletionForDifficulty(currentSong.id, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty);
} }
if (PlayStatePlaylist.isStoryMode) if (PlayStatePlaylist.isStoryMode)
@ -2549,7 +2549,7 @@ class PlayState extends MusicBeatSubState
vocals.stop(); vocals.stop();
// TODO: Softcode this cutscene. // TODO: Softcode this cutscene.
if (currentSong.songId == 'eggnog') if (currentSong.id == 'eggnog')
{ {
var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom, var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
-FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK); -FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
@ -2560,7 +2560,7 @@ class PlayState extends MusicBeatSubState
FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() { FlxG.sound.play(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree // no camFollow so it centers on horror tree
var targetSong:Song = SongDataParser.fetchSong(targetSongId); var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
// Load and cache the song's charts. // Load and cache the song's charts.
// TODO: Do this in the loading state. // TODO: Do this in the loading state.
targetSong.cacheCharts(true); targetSong.cacheCharts(true);
@ -2577,7 +2577,7 @@ class PlayState extends MusicBeatSubState
} }
else else
{ {
var targetSong:Song = SongDataParser.fetchSong(targetSongId); var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
// Load and cache the song's charts. // Load and cache the song's charts.
// TODO: Do this in the loading state. // TODO: Do this in the loading state.
targetSong.cacheCharts(true); targetSong.cacheCharts(true);

View file

@ -143,7 +143,7 @@ class ResultState extends MusicBeatSubState
} }
else else
{ {
songName.text += PlayState.instance.currentSong.songId; songName.text += PlayState.instance.currentSong.id;
} }
songName.letterSpacing = -15; songName.letterSpacing = -15;

View file

@ -1,9 +1,12 @@
package funkin.play.event; package funkin.play.event;
import funkin.play.song.SongData; // Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventData.SongEventSchema;
import funkin.play.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventData.SongEventFieldType;
/** /**
* This class represents a handler for a type of song event. * This class represents a handler for a type of song event.

View file

@ -2,10 +2,13 @@ package funkin.play.event;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.event.SongEventData.SongEventFieldType; import funkin.data.event.SongEventData.SongEventSchema;
import funkin.play.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventData.SongEventFieldType;
import funkin.play.song.SongData;
class PlayAnimationSongEvent extends SongEvent class PlayAnimationSongEvent extends SongEvent
{ {

View file

@ -3,10 +3,13 @@ package funkin.play.event;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.song.SongData; import funkin.data.event.SongEventData.SongEventSchema;
import funkin.play.event.SongEventData; import funkin.data.event.SongEventData.SongEventFieldType;
import funkin.play.event.SongEventData.SongEventFieldType;
/** /**
* This class represents a handler for configuring camera bop intensity and rate. * This class represents a handler for configuring camera bop intensity and rate.

View file

@ -1,7 +1,7 @@
package funkin.play.event; package funkin.play.event;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.event.SongEventData.SongEventSchema; import funkin.data.event.SongEventData.SongEventSchema;
/** /**
* This class represents a handler for a type of song event. * This class represents a handler for a type of song event.

View file

@ -1,235 +0,0 @@
package funkin.play.event;
import funkin.play.event.SongEventData.SongEventSchema;
import funkin.play.song.SongData.SongEventData;
import funkin.util.macro.ClassMacro;
import funkin.play.event.ScriptedSongEvent;
/**
* This class statically handles the parsing of internal and scripted song event handlers.
*/
class SongEventParser
{
/**
* Every built-in event class must be added to this list.
* Thankfully, with the power of `SongEventMacro`, this is done automatically.
*/
static final BUILTIN_EVENTS:List<Class<SongEvent>> = ClassMacro.listSubclassesOf(SongEvent);
/**
* Map of internal handlers for song events.
* These may be either `ScriptedSongEvents` or built-in classes extending `SongEvent`.
*/
static final eventCache:Map<String, SongEvent> = new Map<String, SongEvent>();
public static function loadEventCache():Void
{
clearEventCache();
//
// BASE GAME EVENTS
//
registerBaseEvents();
registerScriptedEvents();
}
static function registerBaseEvents()
{
trace('Instantiating ${BUILTIN_EVENTS.length} built-in song events...');
for (eventCls in BUILTIN_EVENTS)
{
var eventClsName:String = Type.getClassName(eventCls);
if (eventClsName == 'funkin.play.event.SongEvent' || eventClsName == 'funkin.play.event.ScriptedSongEvent') continue;
var event:SongEvent = Type.createInstance(eventCls, ["UNKNOWN"]);
if (event != null)
{
trace(' Loaded built-in song event: (${event.id})');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to load built-in song event: ${Type.getClassName(eventCls)}');
}
}
}
static function registerScriptedEvents()
{
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames)
{
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
if (event != null)
{
trace(' Loaded scripted song event: ${event.id}');
eventCache.set(event.id, event);
}
else
{
trace(' Failed to instantiate scripted song event class: ${eventCls}');
}
}
}
public static function listEventIds():Array<String>
{
return eventCache.keys().array();
}
public static function listEvents():Array<SongEvent>
{
return eventCache.values();
}
public static function getEvent(id:String):SongEvent
{
return eventCache.get(id);
}
public static function getEventSchema(id:String):SongEventSchema
{
var event:SongEvent = getEvent(id);
if (event == null) return null;
return event.getEventSchema();
}
static function clearEventCache()
{
eventCache.clear();
}
public static function handleEvent(data:SongEventData):Void
{
var eventType:String = data.event;
var eventHandler:SongEvent = eventCache.get(eventType);
if (eventHandler != null)
{
eventHandler.handleEvent(data);
}
else
{
trace('WARNING: No event handler for event with id: ${eventType}');
}
data.activated = true;
}
public static inline function handleEvents(events:Array<SongEventData>):Void
{
for (event in events)
{
handleEvent(event);
}
}
/**
* Given a list of song events and the current timestamp,
* return a list of events that should be handled.
*/
public static function queryEvents(events:Array<SongEventData>, currentTime:Float):Array<SongEventData>
{
return events.filter(function(event:SongEventData):Bool {
// If the event is already activated, don't activate it again.
if (event.activated) return false;
// If the event is in the future, don't activate it.
if (event.time > currentTime) return false;
return true;
});
}
/**
* Reset activation of all the provided events.
*/
public static function resetEvents(events:Array<SongEventData>):Void
{
for (event in events)
{
event.activated = false;
// TODO: Add an onReset() method to SongEvent?
}
}
}
enum abstract SongEventFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
}
typedef SongEventSchemaField =
{
/**
* The name of the property as it should be saved in the event data.
*/
name:String,
/**
* The title of the field to display in the UI.
*/
title:String,
/**
* The type of the field.
*/
type:SongEventFieldType,
/**
* Used for ENUM values.
* The key is the display name and the value is the actual value.
*/
?keys:Map<String, Dynamic>,
/**
* Used for INTEGER and FLOAT values.
* The minimum value that can be entered.
*/
?min:Float,
/**
* Used for INTEGER and FLOAT values.
* The maximum value that can be entered.
*/
?max:Float,
/**
* Used for INTEGER and FLOAT values.
* The step value that will be used when incrementing/decrementing the value.
*/
?step:Float,
/**
* An optional default value for the field.
*/
?defaultValue:Dynamic,
}
typedef SongEventSchema = Array<SongEventSchemaField>;

View file

@ -3,10 +3,13 @@ package funkin.play.event;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
// Data from the event schema
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.song.SongData; import funkin.data.event.SongEventData.SongEventFieldType;
import funkin.play.event.SongEventData; import funkin.data.event.SongEventData.SongEventSchema;
import funkin.play.event.SongEventData.SongEventFieldType;
/** /**
* This class represents a handler for camera zoom events. * This class represents a handler for camera zoom events.
@ -76,8 +79,7 @@ class ZoomCameraSongEvent extends SongEvent
return; return;
} }
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction});
{ease: easeFunction});
} }
} }

View file

@ -1,6 +1,6 @@
package funkin.play.notes; package funkin.play.notes;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite; import flixel.FlxSprite;

View file

@ -11,7 +11,7 @@ import funkin.play.notes.NoteHoldCover;
import funkin.play.notes.NoteSplash; import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteSprite;
import funkin.play.notes.SustainTrail; import funkin.play.notes.SustainTrail;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.ui.PreferencesMenu; import funkin.ui.PreferencesMenu;
import funkin.util.SortUtil; import funkin.util.SortUtil;

View file

@ -2,7 +2,7 @@ package funkin.play.notes;
import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection; import funkin.play.notes.NoteDirection;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import flixel.util.FlxDirectionFlags; import flixel.util.FlxDirectionFlags;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.graphics.FlxGraphic; import flixel.graphics.FlxGraphic;

View file

@ -104,7 +104,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
if (noteFrames == null) { if (noteFrames == null)
{
throw 'Could not load note frames for note style: $id'; throw 'Could not load note frames for note style: $id';
} }
@ -139,13 +140,13 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
function buildNoteAnimations(target:NoteSprite):Void function buildNoteAnimations(target:NoteSprite):Void
{ {
var leftData:AnimationData = fetchNoteAnimationData(LEFT); var leftData:AnimationData = fetchNoteAnimationData(LEFT);
target.animation.addByPrefix('purpleScroll', leftData.prefix); target.animation.addByPrefix('purpleScroll', leftData.prefix, leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
var downData:AnimationData = fetchNoteAnimationData(DOWN); var downData:AnimationData = fetchNoteAnimationData(DOWN);
target.animation.addByPrefix('blueScroll', downData.prefix); target.animation.addByPrefix('blueScroll', downData.prefix, downData.frameRate, downData.looped, downData.flipX, downData.flipY);
var upData:AnimationData = fetchNoteAnimationData(UP); var upData:AnimationData = fetchNoteAnimationData(UP);
target.animation.addByPrefix('greenScroll', upData.prefix); target.animation.addByPrefix('greenScroll', upData.prefix, upData.frameRate, upData.looped, upData.flipX, upData.flipY);
var rightData:AnimationData = fetchNoteAnimationData(RIGHT); var rightData:AnimationData = fetchNoteAnimationData(RIGHT);
target.animation.addByPrefix('redScroll', rightData.prefix); target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
} }
function fetchNoteAnimationData(dir:NoteDirection):AnimationData function fetchNoteAnimationData(dir:NoteDirection):AnimationData
@ -302,7 +303,7 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
return 'NoteStyle($id)'; return 'NoteStyle($id)';
} }
public function _fetchData(id:String):Null<NoteStyleData> static function _fetchData(id:String):Null<NoteStyleData>
{ {
return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
} }

View file

@ -5,14 +5,16 @@ import openfl.utils.Assets;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass; import funkin.modding.IScriptedClass;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongPlayableChar; import funkin.data.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeFormat; import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
import funkin.data.IRegistryEntry;
/** /**
* This is a data structure managing information about the current song. * This is a data structure managing information about the current song.
@ -23,9 +25,26 @@ import funkin.play.song.SongData.SongTimeFormat;
* It also receives script events; scripted classes which extend this class * It also receives script events; scripted classes which extend this class
* can be used to perform custom gameplay behaviors only on specific songs. * can be used to perform custom gameplay behaviors only on specific songs.
*/ */
class Song implements IPlayStateScriptedClass @:nullSafety
class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMetadata>
{ {
public final songId:String; public static final DEFAULT_SONGNAME:String = "Unknown";
public static final DEFAULT_ARTIST:String = "Unknown";
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
public static final DEFAULT_DIVISIONS:Null<Int> = null;
public static final DEFAULT_LOOPED:Bool = false;
public static final DEFAULT_STAGE:String = "mainStage";
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
public final id:String;
/**
* Song metadata as parsed from the JSON file.
* This is the data for the `default` variation specifically,
* and is needed for the IRegistryEntry interface.
* Will only be null if the song data could not be loaded.
*/
public final _data:Null<SongMetadata>;
final _metadata:Array<SongMetadata>; final _metadata:Array<SongMetadata>;
@ -39,33 +58,56 @@ class Song implements IPlayStateScriptedClass
var difficultyIds:Array<String>; var difficultyIds:Array<String>;
public var songName(get, never):String;
function get_songName():String
{
if (_data != null) return _data?.songName ?? DEFAULT_SONGNAME;
if (_metadata.length > 0) return _metadata[0]?.songName ?? DEFAULT_SONGNAME;
return DEFAULT_SONGNAME;
}
public var songArtist(get, never):String;
function get_songArtist():String
{
if (_data != null) return _data?.artist ?? DEFAULT_ARTIST;
if (_metadata.length > 0) return _metadata[0]?.artist ?? DEFAULT_ARTIST;
return DEFAULT_ARTIST;
}
/** /**
* @param id The ID of the song to load. * @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 ignoreErrors If false, an exception will be thrown if the song data could not be loaded.
*/ */
public function new(id:String, ignoreErrors:Bool = false) public function new(id:String)
{ {
this.songId = id; this.id = id;
variations = []; variations = [];
difficultyIds = []; difficultyIds = [];
difficulties = new Map<String, SongDifficulty>(); difficulties = new Map<String, SongDifficulty>();
try _data = _fetchData(id);
_metadata = _data == null ? [] : [_data];
for (meta in fetchVariationMetadata(id))
_metadata.push(meta);
if (_metadata.length == 0)
{ {
_metadata = SongDataParser.loadSongMetadata(songId); trace('[WARN] Could not find song data for songId: $id');
} return;
catch (e)
{
_metadata = [];
} }
if (_metadata.length == 0 && !ignoreErrors) variations.clear();
{ variations.push('default');
throw 'Could not find song data for songId: $songId'; if (_data != null && _data.playData != null)
}
else
{ {
for (vari in _data.playData.songVariations)
variations.push(vari);
populateFromMetadata(); populateFromMetadata();
} }
} }
@ -74,7 +116,7 @@ class Song implements IPlayStateScriptedClass
public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>, public static function buildRaw(songId:String, metadata:Array<SongMetadata>, variations:Array<String>, charts:Map<String, SongChartData>,
validScore:Bool = false):Song validScore:Bool = false):Song
{ {
var result:Song = new Song(songId, true); var result:Song = new Song(songId);
result._metadata.clear(); result._metadata.clear();
for (meta in metadata) for (meta in metadata)
@ -112,6 +154,8 @@ class Song implements IPlayStateScriptedClass
// Variations may have different artist, time format, generatedBy, etc. // Variations may have different artist, time format, generatedBy, etc.
for (metadata in _metadata) for (metadata in _metadata)
{ {
if (metadata == null || metadata.playData == null) continue;
// There may be more difficulties in the chart file than in the metadata, // There may be more difficulties in the chart file than in the metadata,
// (i.e. non-playable charts like the one used for Pico on the speaker in Stress) // (i.e. non-playable charts like the one used for Pico on the speaker in Stress)
// but all the difficulties in the metadata must be in the chart file. // but all the difficulties in the metadata must be in the chart file.
@ -134,15 +178,16 @@ class Song implements IPlayStateScriptedClass
difficulty.stage = metadata.playData.stage; difficulty.stage = metadata.playData.stage;
// difficulty.noteSkin = metadata.playData.noteSkin; // difficulty.noteSkin = metadata.playData.noteSkin;
difficulties.set(diffId, difficulty);
difficulty.chars = new Map<String, SongPlayableChar>(); difficulty.chars = new Map<String, SongPlayableChar>();
if (metadata.playData.playableChars == null) continue;
for (charId in metadata.playData.playableChars.keys()) for (charId in metadata.playData.playableChars.keys())
{ {
var char = metadata.playData.playableChars.get(charId); var char:Null<SongPlayableChar> = metadata.playData.playableChars.get(charId);
if (char == null) continue;
difficulty.chars.set(charId, char); difficulty.chars.set(charId, char);
} }
difficulties.set(diffId, difficulty);
} }
} }
} }
@ -157,11 +202,14 @@ class Song implements IPlayStateScriptedClass
clearCharts(); clearCharts();
} }
trace('Caching ${variations.length} chart files for song $songId'); trace('Caching ${variations.length} chart files for song $id');
for (variation in variations) for (variation in variations)
{ {
var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation); var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryChartVersion(id, variation);
applyChartData(chartData, variation); if (version == null) continue;
var chart:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataWithMigration(id, version, variation);
if (chart == null) continue;
applyChartData(chart, variation);
} }
trace('Done caching charts.'); trace('Done caching charts.');
} }
@ -181,8 +229,8 @@ class Song implements IPlayStateScriptedClass
difficulties.set(diffId, difficulty); difficulties.set(diffId, difficulty);
} }
// Add the chart data to the difficulty. // Add the chart data to the difficulty.
difficulty.notes = chartData.notes.get(diffId); difficulty.notes = chartNotes.get(diffId) ?? [];
difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); difficulty.scrollSpeed = chartData.getScrollSpeed(diffId) ?? 1.0;
difficulty.events = chartData.events; difficulty.events = chartData.events;
} }
@ -193,7 +241,7 @@ class Song implements IPlayStateScriptedClass
* @param diffId The difficulty ID, such as `easy` or `hard`. * @param diffId The difficulty ID, such as `easy` or `hard`.
* @return The difficulty data. * @return The difficulty data.
*/ */
public inline function getDifficulty(diffId:String = null):SongDifficulty public inline function getDifficulty(?diffId:String):Null<SongDifficulty>
{ {
if (diffId == null) diffId = difficulties.keys().array()[0]; if (diffId == null) diffId = difficulties.keys().array()[0];
@ -223,9 +271,11 @@ class Song implements IPlayStateScriptedClass
public function toString():String public function toString():String
{ {
return 'Song($songId)'; return 'Song($id)';
} }
public function destroy():Void {}
public function onPause(event:PauseScriptEvent):Void {}; public function onPause(event:PauseScriptEvent):Void {};
public function onResume(event:ScriptEvent):Void {}; public function onResume(event:ScriptEvent):Void {};
@ -265,6 +315,27 @@ class Song implements IPlayStateScriptedClass
public function onDestroy(event:ScriptEvent):Void {}; public function onDestroy(event:ScriptEvent):Void {};
public function onUpdate(event:UpdateScriptEvent):Void {}; public function onUpdate(event:UpdateScriptEvent):Void {};
static function _fetchData(id:String):Null<SongMetadata>
{
trace('Fetching song metadata for $id');
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id);
if (version == null) return null;
return SongRegistry.instance.parseEntryMetadataWithMigration(id, '', version);
}
function fetchVariationMetadata(id:String):Array<SongMetadata>
{
var result:Array<SongMetadata> = [];
for (vari in variations)
{
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari);
if (version == null) continue;
var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version);
if (meta != null) result.push(meta);
}
return result;
}
} }
class SongDifficulty class SongDifficulty
@ -299,7 +370,7 @@ class SongDifficulty
public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT; public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS; public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
public var looped:Bool = SongValidator.DEFAULT_LOOPED; public var looped:Bool = SongValidator.DEFAULT_LOOPED;
public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY; public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY;
public var timeChanges:Array<SongTimeChange> = []; public var timeChanges:Array<SongTimeChange> = [];
@ -351,18 +422,18 @@ class SongDifficulty
var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId); var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId);
if (currentPlayer != null) if (currentPlayer != null)
{ {
FlxG.sound.cache(Paths.inst(this.song.songId, currentPlayer.inst)); FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst));
} }
else else
{ {
FlxG.sound.cache(Paths.inst(this.song.songId)); FlxG.sound.cache(Paths.inst(this.song.id));
} }
} }
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void
{ {
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : ''; var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
FlxG.sound.playMusic(Paths.inst(this.song.songId, suffix), volume, looped); FlxG.sound.playMusic(Paths.inst(this.song.id, suffix), volume, looped);
} }
/** /**
@ -388,7 +459,7 @@ class SongDifficulty
var playableCharData:SongPlayableChar = getPlayableChar(id); var playableCharData:SongPlayableChar = getPlayableChar(id);
if (playableCharData == null) if (playableCharData == null)
{ {
trace('Could not find playable char $id for song ${this.song.songId}'); trace('Could not find playable char $id for song ${this.song.id}');
return []; return [];
} }
@ -398,24 +469,24 @@ class SongDifficulty
// For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`. // For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`.
var playerId:String = id; var playerId:String = id;
var voicePlayer:String = Paths.voices(this.song.songId, '-$id$suffix'); var voicePlayer:String = Paths.voices(this.song.id, '-$id$suffix');
while (voicePlayer != null && !Assets.exists(voicePlayer)) while (voicePlayer != null && !Assets.exists(voicePlayer))
{ {
// Remove the last suffix. // Remove the last suffix.
// For example, bf-car becomes bf. // For example, bf-car becomes bf.
playerId = playerId.split('-').slice(0, -1).join('-'); playerId = playerId.split('-').slice(0, -1).join('-');
// Try again. // Try again.
voicePlayer = playerId == '' ? null : Paths.voices(this.song.songId, '-${playerId}$suffix'); voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
} }
var opponentId:String = playableCharData.opponent; var opponentId:String = playableCharData.opponent;
var voiceOpponent:String = Paths.voices(this.song.songId, '-${opponentId}$suffix'); var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (voiceOpponent != null && !Assets.exists(voiceOpponent)) while (voiceOpponent != null && !Assets.exists(voiceOpponent))
{ {
// Remove the last suffix. // Remove the last suffix.
opponentId = opponentId.split('-').slice(0, -1).join('-'); opponentId = opponentId.split('-').slice(0, -1).join('-');
// Try again. // Try again.
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.songId, '-${opponentId}$suffix'); voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
} }
var result:Array<String> = []; var result:Array<String> = [];
@ -424,7 +495,7 @@ class SongDifficulty
if (voicePlayer == null && voiceOpponent == null) if (voicePlayer == null && voiceOpponent == null)
{ {
// Try to use `Voices.ogg` if no other voices are found. // Try to use `Voices.ogg` if no other voices are found.
if (Assets.exists(Paths.voices(this.song.songId, ''))) result.push(Paths.voices(this.song.songId, '$suffix')); if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
} }
return result; return result;
} }
@ -442,7 +513,7 @@ class SongDifficulty
if (voiceList.length == 0) if (voiceList.length == 0)
{ {
trace('Could not find any voices for song ${this.song.songId}'); trace('Could not find any voices for song ${this.song.id}');
return result; return result;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,232 +0,0 @@
package funkin.play.song;
import flixel.util.FlxSort;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongNoteData;
import funkin.util.ClipboardUtil;
import funkin.util.SerializerUtil;
using Lambda;
class SongDataUtils
{
/**
* Given an array of SongNoteData objects, return a new array of SongNoteData objects
* whose timestamps are shifted by the given amount.
* Does not mutate the original array.
*
* @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
});
}
/**
* Given an array of SongEventData objects, return a new array of SongEventData objects
* whose timestamps are shifted by the given amount.
* Does not mutate the original array.
*
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData>
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
});
}
/**
* Return a new array without a certain subset of notes from an array of SongNoteData objects.
* Does not mutate the original array.
*
* @param notes The array of notes to be subtracted from.
* @param subtrahend The notes to remove from the `notes` array. Yes, subtrahend is a real word.
*/
public static function subtractNotes(notes:Array<SongNoteData>, subtrahend:Array<SongNoteData>)
{
if (notes.length == 0 || subtrahend.length == 0) return notes;
var result = notes.filter(function(note:SongNoteData):Bool {
for (x in subtrahend)
// SongNoteData's == operation has been overridden so that this will work.
if (x == note) return false;
return true;
});
return result;
}
/**
* Return a new array without a certain subset of events from an array of SongEventData objects.
* Does not mutate the original array.
*
* @param events The array of events to be subtracted from.
* @param subtrahend The events to remove from the `events` array. Yes, subtrahend is a real word.
*/
public static function subtractEvents(events:Array<SongEventData>, subtrahend:Array<SongEventData>)
{
if (events.length == 0 || subtrahend.length == 0) return events;
return events.filter(function(event:SongEventData):Bool {
// SongEventData's == operation has been overridden so that this will work.
return !subtrahend.has(event);
});
}
/**
* Create an array of notes whose note data is flipped (player becomes opponent and vice versa)
* Does not mutate the original array.
*/
public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
var newData = note.data;
if (newData < strumlineSize) newData += strumlineSize;
else
newData -= strumlineSize;
return new SongNoteData(note.time, newData, note.length, note.kind);
});
}
/**
* Prepare an array of notes to be used as the clipboard data.
*
* Offset the provided array of notes such that the first note is at 0 milliseconds.
*/
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
{
if (notes.length == 0) return notes;
if (timeOffset == null) timeOffset = -Std.int(notes[0].time);
return offsetSongNoteData(sortNotes(notes), timeOffset);
}
/**
* Prepare an array of events to be used as the clipboard data.
*
* Offset the provided array of events such that the first event is at 0 milliseconds.
*/
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
{
if (events.length == 0) return events;
if (timeOffset == null) timeOffset = -Std.int(events[0].time);
return offsetSongEventData(sortEvents(events), timeOffset);
}
/**
* Sort an array of notes by strum time.
*/
public static function sortNotes(notes:Array<SongNoteData>, desc:Bool = false):Array<SongNoteData>
{
// TODO: Modifies the array in place. Is this okay?
notes.sort(function(a:SongNoteData, b:SongNoteData):Int {
return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time);
});
return notes;
}
/**
* Sort an array of events by strum time.
*/
public static function sortEvents(events:Array<SongEventData>, desc:Bool = false):Array<SongEventData>
{
// TODO: Modifies the array in place. Is this okay?
events.sort(function(a:SongEventData, b:SongEventData):Int {
return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time);
});
return events;
}
/**
* Serialize note and event data and write it to the clipboard.
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
var dataString = SerializerUtil.toJSON(data);
ClipboardUtil.setClipboard(dataString);
trace('Wrote ' + data.notes.length + ' notes and ' + data.events.length + ' events to clipboard.');
trace(dataString);
}
/**
* Read an array of note data from the clipboard and deserialize it.
*/
public static function readItemsFromClipboard():SongClipboardItems
{
var notesString = ClipboardUtil.getClipboard();
trace('Read ${notesString.length} characters from clipboard.');
var data:SongClipboardItems = notesString.parseJSON();
if (data == null)
{
trace('Failed to parse notes from clipboard.');
return {
notes: [],
events: []
};
}
else
{
trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
return data;
}
}
/**
* Filter a list of notes to only include notes that are within the given time range.
*/
public static function getNotesInTimeRange(notes:Array<SongNoteData>, start:Float, end:Float):Array<SongNoteData>
{
return notes.filter(function(note:SongNoteData):Bool {
return note.time >= start && note.time <= end;
});
}
/**
* Filter a list of events to only include events that are within the given time range.
*/
public static function getEventsInTimeRange(events:Array<SongEventData>, start:Float, end:Float):Array<SongEventData>
{
return events.filter(function(event:SongEventData):Bool {
return event.time >= start && event.time <= end;
});
}
/**
* Filter a list of notes to only include notes whose data is within the given range.
*/
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
{
return notes.filter(function(note:SongNoteData):Bool {
return note.data >= start && note.data <= end;
});
}
/**
* Filter a list of notes to only include notes whose data is one of the given values.
*/
public static function getNotesWithData(notes:Array<SongNoteData>, data:Array<Int>):Array<SongNoteData>
{
return notes.filter(function(note:SongNoteData):Bool {
return data.indexOf(note.data) != -1;
});
}
}
typedef SongClipboardItems =
{
notes:Array<SongNoteData>,
events:Array<SongEventData>
}

View file

@ -1,11 +1,11 @@
package funkin.play.song; package funkin.play.song;
import funkin.play.song.formats.FNFLegacy; import funkin.play.song.formats.FNFLegacy;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar; import funkin.data.song.SongData.SongPlayableChar;
import funkin.util.VersionUtil; import funkin.util.VersionUtil;
class SongMigrator class SongMigrator
@ -176,7 +176,7 @@ class SongMigrator
songMetadata.playData.songVariations = []; songMetadata.playData.songVariations = [];
// Set the song's song variations. // Set the song's song variations.
songMetadata.playData.playableChars = {}; songMetadata.playData.playableChars = [];
try try
{ {
Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2)); Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2));
@ -203,7 +203,7 @@ class SongMigrator
var songData:FNFLegacy = cast jsonData; var songData:FNFLegacy = cast jsonData;
var songChartData:SongChartData = new SongChartData(1.0, [], []); var songChartData:SongChartData = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0; var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0;
if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes)); if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes));

View file

@ -1,7 +1,7 @@
package funkin.play.song; package funkin.play.song;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import lime.utils.Bytes; import lime.utils.Bytes;
import openfl.events.Event; import openfl.events.Event;

View file

@ -1,10 +1,11 @@
package funkin.play.song; package funkin.play.song;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongPlayData; import funkin.data.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongPlayData;
import funkin.play.song.SongData.SongTimeFormat; import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
/** /**
* For SongMetadata and SongChartData objects, * For SongMetadata and SongChartData objects,
@ -20,13 +21,6 @@ class SongValidator
public static final DEFAULT_STAGE:String = "mainStage"; public static final DEFAULT_STAGE:String = "mainStage";
public static final DEFAULT_SCROLLSPEED:Float = 1.0; public static final DEFAULT_SCROLLSPEED:Float = 1.0;
public static var DEFAULT_GENERATEDBY(get, null):String;
static function get_DEFAULT_GENERATEDBY():String
{
return '${Constants.TITLE} - ${Constants.VERSION}';
}
/** /**
* Validates the fields of a SongMetadata object (excluding the version field). * Validates the fields of a SongMetadata object (excluding the version field).
* *
@ -59,7 +53,7 @@ class SongValidator
} }
if (input.generatedBy == null) if (input.generatedBy == null)
{ {
input.generatedBy = DEFAULT_GENERATEDBY; input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
} }
input.timeChanges = validateTimeChanges(input.timeChanges, songId); input.timeChanges = validateTimeChanges(input.timeChanges, songId);

View file

@ -1,8 +1,8 @@
package funkin.ui.debug.charting; package funkin.ui.debug.charting;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.play.song.SongDataUtils; import funkin.data.song.SongDataUtils;
using Lambda; using Lambda;

View file

@ -3,8 +3,8 @@ package funkin.ui.debug.charting;
import funkin.play.character.CharacterData; import funkin.play.character.CharacterData;
import funkin.util.Constants; import funkin.util.Constants;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.input.Cursor; import funkin.input.Cursor;
@ -13,9 +13,9 @@ import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.song.SongMigrator; import funkin.play.song.SongMigrator;
import funkin.play.song.SongValidator; import funkin.play.song.SongValidator;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongPlayableChar; import funkin.data.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import haxe.io.Path; import haxe.io.Path;
import haxe.ui.components.Button; import haxe.ui.components.Button;
@ -106,18 +106,17 @@ class ChartEditorDialogHandler
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox); var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds(); var songList:Array<String> = SongRegistry.instance.listEntryIds();
songList.sort(SortUtil.alphabetically); songList.sort(SortUtil.alphabetically);
for (targetSongId in songList) for (targetSongId in songList)
{ {
var songData:Song = SongDataParser.fetchSong(targetSongId); var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
if (songData == null) continue; if (songData == null) continue;
var songName:Null<String> = songData.getDifficulty('normal')?.songName; var songName:Null<String> = songData.getDifficulty('normal')?.songName;
if (songName == null) songName = songData.getDifficulty()?.songName; if (songName == null) songName = songData.getDifficulty()?.songName;
if (songName == null) if (songName == null) // Still null?
{ {
trace('[WARN] Could not fetch song name for ${targetSongId}'); trace('[WARN] Could not fetch song name for ${targetSongId}');
} }
@ -470,9 +469,9 @@ class ChartEditorDialogHandler
var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown); var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
dialogNoteSkin.onChange = function(event:UIEvent) { dialogNoteSkin.onChange = function(event:UIEvent) {
if (event.data.id == null) return; if (event.data.id == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id; state.currentSongNoteSkin = event.data.id;
}; };
state.currentSongMetadata.playData.noteSkin = null; state.currentSongNoteSkin = 'funkin';
var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper); var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
dialogBPM.onChange = function(event:UIEvent) { dialogBPM.onChange = function(event:UIEvent) {
@ -481,7 +480,7 @@ class ChartEditorDialogHandler
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges; var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0) if (timeChanges == null || timeChanges.length == 0)
{ {
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; timeChanges = [new SongTimeChange(0, event.value)];
} }
else else
{ {
@ -502,7 +501,7 @@ class ChartEditorDialogHandler
}; };
// Empty the character list. // Empty the character list.
state.currentSongMetadata.playData.playableChars = {}; state.currentSongMetadata.playData.playableChars = [];
// Add at least one character group with no Remove button. // Add at least one character group with no Remove button.
dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null)); dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null));
@ -516,7 +515,8 @@ class ChartEditorDialogHandler
{ {
var groupKey:String = key; var groupKey:String = key;
var getCharData:Void->SongPlayableChar = function() { var getCharData:Void->Null<SongPlayableChar> = function():Null<SongPlayableChar> {
if (state.currentSongMetadata.playData == null) return null;
if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}'; if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
var result = state.currentSongMetadata.playData.playableChars.get(groupKey); var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
@ -528,42 +528,53 @@ class ChartEditorDialogHandler
return result; return result;
} }
var moveCharGroup:String->Void = function(target:String) { var moveCharGroup:String->Void = function(target:String):Void {
var charData = getCharData(); var charData:Null<SongPlayableChar> = getCharData();
if (charData == null) return;
if (state.currentSongMetadata.playData.playableChars == null) return;
state.currentSongMetadata.playData.playableChars.remove(groupKey); state.currentSongMetadata.playData.playableChars.remove(groupKey);
state.currentSongMetadata.playData.playableChars.set(target, charData); state.currentSongMetadata.playData.playableChars.set(target, charData);
groupKey = target; groupKey = target;
} }
var removeGroup:Void->Void = function() { var removeGroup:Void->Void = function():Void {
if (state?.currentSongMetadata?.playData?.playableChars == null) return;
state.currentSongMetadata.playData.playableChars.remove(groupKey); state.currentSongMetadata.playData.playableChars.remove(groupKey);
removeFunc(); removeFunc();
} }
var charData:SongPlayableChar = getCharData(); var charData:Null<SongPlayableChar> = getCharData();
var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT); var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown); var charGroupPlayer:Null<DropDown> = charGroup.findComponent('charGroupPlayer', DropDown);
charGroupPlayer.onChange = function(event:UIEvent) { if (charGroupPlayer == null) throw 'Could not locate charGroupPlayer DropDown in Song Metadata dialog';
charGroupPlayer.onChange = function(event:UIEvent):Void {
if (charData != null) return;
charGroup.text = event.data.text; charGroup.text = event.data.text;
moveCharGroup(event.data.id); moveCharGroup(event.data.id);
}; };
var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); var charGroupOpponent:Null<DropDown> = charGroup.findComponent('charGroupOpponent', DropDown);
charGroupOpponent.onChange = function(event:UIEvent) { if (charGroupOpponent == null) throw 'Could not locate charGroupOpponent DropDown in Song Metadata dialog';
charGroupOpponent.onChange = function(event:UIEvent):Void {
if (charData == null) return;
charData.opponent = event.data.id; charData.opponent = event.data.id;
}; };
charGroupOpponent.value = getCharData().opponent; charGroupOpponent.value = charData.opponent;
var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown); var charGroupGirlfriend:Null<DropDown> = charGroup.findComponent('charGroupGirlfriend', DropDown);
charGroupGirlfriend.onChange = function(event:UIEvent) { if (charGroupGirlfriend == null) throw 'Could not locate charGroupGirlfriend DropDown in Song Metadata dialog';
charGroupGirlfriend.onChange = function(event:UIEvent):Void {
if (charData == null) return;
charData.girlfriend = event.data.id; charData.girlfriend = event.data.id;
}; };
charGroupGirlfriend.value = getCharData().girlfriend; charGroupGirlfriend.value = charData.girlfriend;
var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button); var charGroupRemove:Null<Button> = charGroup.findComponent('charGroupRemove', Button);
charGroupRemove.onClick = function(event:UIEvent) { if (charGroupRemove == null) throw 'Could not locate charGroupRemove Button in Song Metadata dialog';
charGroupRemove.onClick = function(event:UIEvent):Void {
removeGroup(); removeGroup();
}; };
@ -584,7 +595,8 @@ class ChartEditorDialogHandler
for (charKey in state.currentSongMetadata.playData.playableChars.keys()) for (charKey in state.currentSongMetadata.playData.playableChars.keys())
{ {
var charData:SongPlayableChar = state.currentSongMetadata.playData.playableChars.get(charKey); var charData:Null<SongPlayableChar> = state.currentSongMetadata.playData.playableChars.get(charKey);
if (charData == null) continue;
charIdsForVocals.push(charKey); charIdsForVocals.push(charKey);
if (charData.opponent != null) charIdsForVocals.push(charData.opponent); if (charData.opponent != null) charIdsForVocals.push(charData.opponent);
} }

View file

@ -1,6 +1,6 @@
package funkin.ui.debug.charting; package funkin.ui.debug.charting;
import funkin.play.event.SongEventData.SongEventParser; import funkin.data.event.SongEventData.SongEventParser;
import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxAtlasFrames;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import openfl.utils.Assets; import openfl.utils.Assets;
@ -10,7 +10,7 @@ import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames; import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
/** /**
* A event sprite that can be used to display a song event in a chart. * A event sprite that can be used to display a song event in a chart.

View file

@ -8,7 +8,7 @@ import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames; import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import funkin.play.notes.SustainTrail; import funkin.play.notes.SustainTrail;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
/** /**
* A hold note sprite that can be used to display a note in a chart. * A hold note sprite that can be used to display a note in a chart.

View file

@ -1,7 +1,7 @@
package funkin.ui.debug.charting; package funkin.ui.debug.charting;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.util.FlxColor; import flixel.util.FlxColor;

View file

@ -5,7 +5,7 @@ import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames; import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint; import flixel.math.FlxPoint;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
/** /**
* A note sprite that can be used to display a note in a chart. * A note sprite that can be used to display a note in a chart.

View file

@ -36,13 +36,13 @@ import funkin.play.notes.NoteSprite;
import funkin.play.notes.Strumline; import funkin.play.notes.Strumline;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar; import funkin.data.song.SongData.SongPlayableChar;
import funkin.play.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.ui.debug.charting.ChartEditorCommand; import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorCommand; import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
@ -844,7 +844,7 @@ class ChartEditorState extends HaxeUIState
var result:Null<SongChartData> = songChartData.get(selectedVariation); var result:Null<SongChartData> = songChartData.get(selectedVariation);
if (result == null) if (result == null)
{ {
result = new SongChartData(1.0, [], []); result = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
songChartData.set(selectedVariation, result); songChartData.set(selectedVariation, result);
} }
return result; return result;
@ -2555,8 +2555,7 @@ class ChartEditorState extends HaxeUIState
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()"; if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind);
selectedNoteKind);
if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind) if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind)
{ {
@ -3947,7 +3946,7 @@ class ChartEditorState extends HaxeUIState
*/ */
public function loadSongAsTemplate(songId:String):Void public function loadSongAsTemplate(songId:String):Void
{ {
var song:Null<Song> = SongDataParser.fetchSong(songId); var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
if (song == null) return; if (song == null) return;
@ -3965,7 +3964,7 @@ class ChartEditorState extends HaxeUIState
var metadataClone = Reflect.copy(metadata); var metadataClone = Reflect.copy(metadata);
if (metadataClone != null) songMetadata.set(variation, metadataClone); if (metadataClone != null) songMetadata.set(variation, metadataClone);
songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation)); songChartData.set(variation, SongRegistry.instance.parseEntryChartData(songId, metadata.variation));
} }
loadSong(songMetadata, songChartData); loadSong(songMetadata, songChartData);

View file

@ -4,8 +4,8 @@ import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.TreeViewNode;
import funkin.play.character.BaseCharacter.CharacterType; import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.event.SongEvent; import funkin.play.event.SongEvent;
import funkin.play.event.SongEventData; import funkin.data.event.SongEventData;
import funkin.play.song.SongData.SongTimeChange; import funkin.data.song.SongData.SongTimeChange;
import funkin.play.song.SongSerializer; import funkin.play.song.SongSerializer;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import haxe.ui.components.Button; import haxe.ui.components.Button;
@ -541,9 +541,9 @@ class ChartEditorToolboxHandler
var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown); var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
inputNoteSkin.onChange = function(event:UIEvent) { inputNoteSkin.onChange = function(event:UIEvent) {
if ((event?.data?.id ?? null) == null) return; if ((event?.data?.id ?? null) == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id; state.currentSongNoteSkin = event.data.id;
}; };
inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin; inputNoteSkin.value = state.currentSongNoteSkin;
var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper); var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
inputBPM.onChange = function(event:UIEvent) { inputBPM.onChange = function(event:UIEvent) {
@ -552,7 +552,7 @@ class ChartEditorToolboxHandler
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges; var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0) if (timeChanges == null || timeChanges.length == 0)
{ {
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; timeChanges = [new SongTimeChange(0, event.value)];
} }
else else
{ {

View file

@ -4,6 +4,7 @@ import flixel.FlxSprite;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.data.IRegistryEntry; import funkin.data.IRegistryEntry;
import funkin.data.song.SongRegistry;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.level.LevelData; import funkin.data.level.LevelData;
@ -70,17 +71,20 @@ class Level implements IRegistryEntry<LevelData>
public function getSongDisplayNames(difficulty:String):Array<String> public function getSongDisplayNames(difficulty:String):Array<String>
{ {
var songList:Array<String> = getSongs() ?? []; var songList:Array<String> = getSongs() ?? [];
var songNameList:Array<String> = songList.map(function(songId) { var songNameList:Array<String> = songList.map(function(songId:String) {
var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); return getSongDisplayName(songId, difficulty);
if (song == null) return 'Unknown';
var songDifficulty:SongDifficulty = song.getDifficulty(difficulty);
if (songDifficulty == null) songDifficulty = song.getDifficulty();
var songName:String = songDifficulty?.songName;
return songName ?? 'Unknown';
}); });
return songNameList; return songNameList;
} }
static function getSongDisplayName(songId:String, difficulty:String):String
{
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
if (song == null) return 'Unknown';
return song.songName;
}
/** /**
* Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon. * Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon.
* TODO: Change this behavior in a later release. * TODO: Change this behavior in a later release.
@ -120,7 +124,7 @@ class Level implements IRegistryEntry<LevelData>
var songList = getSongs(); var songList = getSongs();
var firstSongId:String = songList[0]; var firstSongId:String = songList[0];
var firstSong:Song = funkin.play.song.SongData.SongDataParser.fetchSong(firstSongId); var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId);
if (firstSong != null) if (firstSong != null)
{ {
@ -134,7 +138,7 @@ class Level implements IRegistryEntry<LevelData>
for (songIndex in 1...songList.length) for (songIndex in 1...songList.length)
{ {
var songId:String = songList[songIndex]; var songId:String = songList[songIndex];
var song:Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); var song:Song = SongRegistry.instance.fetchEntry(songId);
if (song == null) continue; if (song == null) continue;
@ -179,7 +183,7 @@ class Level implements IRegistryEntry<LevelData>
return 'Level($id)'; return 'Level($id)';
} }
public function _fetchData(id:String):Null<LevelData> static function _fetchData(id:String):Null<LevelData>
{ {
return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id)); return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));
} }

View file

@ -16,8 +16,8 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist; import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMusicData;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
class StoryMenuState extends MusicBeatState class StoryMenuState extends MusicBeatState
{ {
@ -201,8 +201,11 @@ class StoryMenuState extends MusicBeatState
{ {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{ {
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu'); var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null)
{
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
}
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7); FlxG.sound.music.fadeIn(4, 0, 0.7);
@ -509,7 +512,7 @@ class StoryMenuState extends MusicBeatState
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift(); var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:Song = SongDataParser.fetchSong(targetSongId); var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
PlayStatePlaylist.campaignId = currentLevel.id; PlayStatePlaylist.campaignId = currentLevel.id;
PlayStatePlaylist.campaignTitle = currentLevel.getTitle(); PlayStatePlaylist.campaignTitle = currentLevel.getTitle();

View file

@ -12,8 +12,8 @@ import flixel.util.FlxTimer;
import funkin.audiovis.SpectogramSprite; import funkin.audiovis.SpectogramSprite;
import funkin.shaderslmfao.ColorSwap; import funkin.shaderslmfao.ColorSwap;
import funkin.shaderslmfao.LeftMaskShader; import funkin.shaderslmfao.LeftMaskShader;
import funkin.play.song.SongData.SongDataParser; import funkin.data.song.SongRegistry;
import funkin.play.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMusicData;
import funkin.shaderslmfao.TitleOutline; import funkin.shaderslmfao.TitleOutline;
import funkin.ui.AtlasText; import funkin.ui.AtlasText;
import openfl.Assets; import openfl.Assets;
@ -216,9 +216,11 @@ class TitleState extends MusicBeatState
{ {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) if (FlxG.sound.music == null || !FlxG.sound.music.playing)
{ {
var freakyMenuMetadata:SongMetadata = SongDataParser.parseMusicMetadata('freakyMenu'); var freakyMenuMetadata:Null<SongMusicData> = SongRegistry.instance.parseMusicData('freakyMenu');
if (freakyMenuMetadata != null)
{
Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges); Conductor.mapTimeChanges(freakyMenuMetadata.timeChanges);
}
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0); FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7); FlxG.sound.music.fadeIn(4, 0, 0.7);
} }

View file

@ -1,5 +1,6 @@
package funkin.util; package funkin.util;
import flixel.graphics.frames.FlxFrame;
#if !macro #if !macro
import flixel.FlxBasic; import flixel.FlxBasic;
import flixel.util.FlxSort; import flixel.util.FlxSort;
@ -41,6 +42,16 @@ class SortUtil
return FlxSort.byValues(order, a.noteData.time, b.noteData.time); return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
} }
/**
* Given two FlxFrames, sort their names alphabetically.
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
*/
public static inline function byFrameName(a:FlxFrame, b:FlxFrame)
{
return alphabetically(a.name, b.name);
}
/** /**
* Sort predicate for sorting strings alphabetically. * Sort predicate for sorting strings alphabetically.
* @param a The first string to compare. * @param a The first string to compare.

View file

@ -13,4 +13,22 @@ class IteratorTools
{ {
return [for (i in iterator) i]; return [for (i in iterator) i];
} }
public static function count<T>(iterator:Iterator<T>, ?predicate:(item:T) -> Bool):Int
{
var n = 0;
if (predicate == null)
{
for (_ in iterator)
n++;
}
else
{
for (x in iterator)
if (predicate(x)) n++;
}
return n;
}
} }