From f4bc682ea18f1bebb9d4b599782afa3aa14c4e7c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 8 Sep 2023 17:46:44 -0400 Subject: [PATCH] Remove funkin.play.song.SongData and refactor app to match. --- docs/troubleshooting.md | 6 + source/funkin/Conductor.hx | 2 +- source/funkin/DialogueBox.hx | 12 +- source/funkin/FreeplayState.hx | 5 +- source/funkin/InitState.hx | 10 +- source/funkin/LoadingState.hx | 2 +- source/funkin/PauseSubState.hx | 4 +- source/funkin/modding/PolymodHandler.hx | 7 +- source/funkin/modding/events/ScriptEvent.hx | 6 +- source/funkin/play/PlayState.hx | 22 +- source/funkin/play/ResultState.hx | 2 +- .../funkin/play/event/FocusCameraSongEvent.hx | 9 +- .../play/event/PlayAnimationSongEvent.hx | 9 +- .../play/event/SetCameraBopSongEvent.hx | 9 +- source/funkin/play/event/SongEvent.hx | 4 +- source/funkin/play/event/SongEventData.hx | 235 ---- .../funkin/play/event/ZoomCameraSongEvent.hx | 12 +- source/funkin/play/notes/NoteSprite.hx | 2 +- source/funkin/play/notes/Strumline.hx | 2 +- source/funkin/play/notes/SustainTrail.hx | 2 +- .../funkin/play/notes/notestyle/NoteStyle.hx | 13 +- source/funkin/play/song/Song.hx | 163 ++- source/funkin/play/song/SongData.hx | 1021 ----------------- source/funkin/play/song/SongDataUtils.hx | 232 ---- source/funkin/play/song/SongMigrator.hx | 14 +- source/funkin/play/song/SongSerializer.hx | 4 +- source/funkin/play/song/SongValidator.hx | 20 +- .../ui/debug/charting/ChartEditorCommand.hx | 6 +- .../charting/ChartEditorDialogHandler.hx | 74 +- .../debug/charting/ChartEditorEventSprite.hx | 4 +- .../charting/ChartEditorHoldNoteSprite.hx | 2 +- .../debug/charting/ChartEditorNotePreview.hx | 4 +- .../debug/charting/ChartEditorNoteSprite.hx | 2 +- .../ui/debug/charting/ChartEditorState.hx | 23 +- .../charting/ChartEditorToolboxHandler.hx | 10 +- source/funkin/ui/story/Level.hx | 24 +- source/funkin/ui/story/StoryMenuState.hx | 13 +- source/funkin/ui/title/TitleState.hx | 12 +- source/funkin/util/SortUtil.hx | 11 + source/funkin/util/tools/IteratorTools.hx | 18 + 40 files changed, 339 insertions(+), 1693 deletions(-) create mode 100644 docs/troubleshooting.md delete mode 100644 source/funkin/play/event/SongEventData.hx delete mode 100644 source/funkin/play/song/SongData.hx delete mode 100644 source/funkin/play/song/SongDataUtils.hx diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 000000000..26958a467 --- /dev/null +++ b/docs/troubleshooting.md @@ -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. diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 9bd668b69..bd50b556c 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -4,7 +4,7 @@ import funkin.util.Constants; import flixel.util.FlxSignal; import flixel.math.FlxMath; 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, diff --git a/source/funkin/DialogueBox.hx b/source/funkin/DialogueBox.hx index 342fcba10..68d330dbe 100644 --- a/source/funkin/DialogueBox.hx +++ b/source/funkin/DialogueBox.hx @@ -37,7 +37,7 @@ class DialogueBox extends FlxSpriteGroup { super(); - switch (PlayState.instance.currentSong.songId.toLowerCase()) + switch (PlayState.instance.currentSong.id.toLowerCase()) { case 'senpai': FlxG.sound.playMusic(Paths.music('Lunchbox'), 0); @@ -78,7 +78,7 @@ class DialogueBox extends FlxSpriteGroup box = new FlxSprite(-20, 45); var hasDialog:Bool = false; - switch (PlayState.instance.currentSong.songId.toLowerCase()) + switch (PlayState.instance.currentSong.id.toLowerCase()) { case 'senpai': hasDialog = true; @@ -150,8 +150,8 @@ class DialogueBox extends FlxSpriteGroup override function update(elapsed:Float):Void { // HARD CODING CUZ IM STUPDI - if (PlayState.instance.currentSong.songId.toLowerCase() == 'roses') portraitLeft.visible = false; - if (PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') + if (PlayState.instance.currentSong.id.toLowerCase() == 'roses') portraitLeft.visible = false; + if (PlayState.instance.currentSong.id.toLowerCase() == 'thorns') { portraitLeft.color = FlxColor.BLACK; swagDialogue.color = FlxColor.WHITE; @@ -187,8 +187,8 @@ class DialogueBox extends FlxSpriteGroup { isEnding = true; - if (PlayState.instance.currentSong.songId.toLowerCase() == 'senpai' - || PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0); + if (PlayState.instance.currentSong.id.toLowerCase() == 'senpai' + || PlayState.instance.currentSong.id.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0); new FlxTimer().start(0.2, function(tmr:FlxTimer) { box.alpha -= 1 / 5; diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index c31e8c77b..6cd353233 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -20,6 +20,7 @@ import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; +import funkin.data.song.SongRegistry; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.Controls.Control; @@ -30,7 +31,6 @@ import funkin.freeplayStuff.LetterSort; import funkin.freeplayStuff.SongMenuItem; import funkin.play.HealthIcon; import funkin.play.PlayState; -import funkin.play.song.SongData.SongDataParser; import funkin.shaderslmfao.AngleMask; import funkin.shaderslmfao.PureColor; import funkin.shaderslmfao.StrokeShader; @@ -843,7 +843,8 @@ class FreeplayState extends MusicBeatSubState }*/ 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) { case 0: diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 82a357ae9..e7060abd7 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -17,11 +17,11 @@ import funkin.play.PlayStatePlaylist; import openfl.display.BitmapData; import funkin.data.level.LevelRegistry; 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.DialogueBoxDataParser; 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.character.CharacterData.CharacterDataParser; 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, // to ensure build macros work properly. + SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); SongEventParser.loadEventCache(); ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - SongDataParser.loadSongCache(); StageDataParser.loadStageCache(); CharacterDataParser.loadCharacterCache(); ModuleHandler.buildModuleCallbacks(); @@ -276,7 +276,7 @@ class InitState extends FlxState */ 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) { @@ -312,7 +312,7 @@ class InitState extends FlxState 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( { diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index 3ec2e1005..216d9ba74 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -159,7 +159,7 @@ class LoadingState extends MusicBeatState 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 diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 791a4bb9a..f93e5a450 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -10,7 +10,7 @@ import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; import funkin.play.PlayState; -import funkin.play.song.SongData.SongDataParser; +import funkin.data.song.SongRegistry; class PauseSubState extends MusicBeatSubState { @@ -197,7 +197,7 @@ class PauseSubState extends MusicBeatSubState regenMenu(); 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(); diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 47afb0a30..f7f69428b 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -3,18 +3,19 @@ package funkin.modding; import funkin.util.macro.ClassMacro; import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.song.SongData; +import funkin.data.song.SongData; import funkin.play.stage.StageData; import polymod.Polymod; import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; -import funkin.play.event.SongEventData.SongEventParser; +import funkin.data.event.SongEventData.SongEventParser; import funkin.util.FileUtil; import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser; +import funkin.data.song.SongRegistry; 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, // to ensure build macros work properly. + SongRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries(); SongEventParser.loadEventCache(); ConversationDataParser.loadConversationCache(); DialogueBoxDataParser.loadDialogueBoxCache(); SpeakerDataParser.loadSpeakerCache(); - SongDataParser.loadSongCache(); StageDataParser.loadStageCache(); CharacterDataParser.loadCharacterCache(); ModuleHandler.loadModuleCache(); diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 3f29ad833..586a6206c 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -1,6 +1,6 @@ package funkin.modding.events; -import funkin.play.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongNoteData; import flixel.FlxState; import flixel.FlxSubState; import funkin.play.notes.NoteSprite; @@ -435,9 +435,9 @@ class SongEventScriptEvent extends ScriptEvent * The note associated with this event. * 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); this.event = event; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index ed82d6e99..b20b34c65 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -35,7 +35,7 @@ import funkin.play.cutscene.dialogue.Conversation; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.VanillaCutscenes; 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.NoteDirection; import funkin.play.notes.Strumline; @@ -43,10 +43,10 @@ import funkin.play.notes.SustainTrail; import funkin.play.scoring.Scoring; import funkin.NoteSplash; import funkin.play.song.Song; -import funkin.play.song.SongData.SongDataParser; -import funkin.play.song.SongData.SongEventData; -import funkin.play.song.SongData.SongNoteData; -import funkin.play.song.SongData.SongPlayableChar; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongPlayableChar; import funkin.play.stage.Stage; import funkin.play.stage.StageData.StageDataParser; import funkin.ui.PopUpStuff; @@ -630,7 +630,7 @@ class PlayState extends MusicBeatSubState startingSong = true; // 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.playHorrorStartCutscene(); @@ -2495,9 +2495,9 @@ class PlayState extends MusicBeatSubState if (currentSong != null && currentSong.validScore) { // 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) @@ -2549,7 +2549,7 @@ class PlayState extends MusicBeatSubState vocals.stop(); // TODO: Softcode this cutscene. - if (currentSong.songId == 'eggnog') + if (currentSong.id == 'eggnog') { var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom, -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() { // 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. // TODO: Do this in the loading state. targetSong.cacheCharts(true); @@ -2577,7 +2577,7 @@ class PlayState extends MusicBeatSubState } else { - var targetSong:Song = SongDataParser.fetchSong(targetSongId); + var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId); // Load and cache the song's charts. // TODO: Do this in the loading state. targetSong.cacheCharts(true); diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index aaa2b6d1d..0c2984719 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -143,7 +143,7 @@ class ResultState extends MusicBeatSubState } else { - songName.text += PlayState.instance.currentSong.songId; + songName.text += PlayState.instance.currentSong.id; } songName.letterSpacing = -15; diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 8d677118b..5f63254b0 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -1,9 +1,12 @@ 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.SongEventData.SongEventFieldType; -import funkin.play.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventData.SongEventFieldType; /** * This class represents a handler for a type of song event. diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index a187ca285..6bc625517 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -2,10 +2,13 @@ package funkin.play.event; import flixel.FlxSprite; 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.SongEventData.SongEventFieldType; -import funkin.play.event.SongEventData.SongEventSchema; -import funkin.play.song.SongData; +import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventData.SongEventFieldType; class PlayAnimationSongEvent extends SongEvent { diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index b17d4511c..3cdeb9a67 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -3,10 +3,13 @@ package funkin.play.event; import flixel.tweens.FlxTween; import flixel.FlxCamera; 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.song.SongData; -import funkin.play.event.SongEventData; -import funkin.play.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventData.SongEventSchema; +import funkin.data.event.SongEventData.SongEventFieldType; /** * This class represents a handler for configuring camera bop intensity and rate. diff --git a/source/funkin/play/event/SongEvent.hx b/source/funkin/play/event/SongEvent.hx index 6acc745ff..36a886673 100644 --- a/source/funkin/play/event/SongEvent.hx +++ b/source/funkin/play/event/SongEvent.hx @@ -1,7 +1,7 @@ package funkin.play.event; -import funkin.play.song.SongData.SongEventData; -import funkin.play.event.SongEventData.SongEventSchema; +import funkin.data.song.SongData.SongEventData; +import funkin.data.event.SongEventData.SongEventSchema; /** * This class represents a handler for a type of song event. diff --git a/source/funkin/play/event/SongEventData.hx b/source/funkin/play/event/SongEventData.hx deleted file mode 100644 index 8c157b52a..000000000 --- a/source/funkin/play/event/SongEventData.hx +++ /dev/null @@ -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> = 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 = new Map(); - - 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 = 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 - { - return eventCache.keys().array(); - } - - public static function listEvents():Array - { - 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):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, currentTime:Float):Array - { - 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):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, - /** - * 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; diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 79425d564..1ae76039e 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -3,10 +3,13 @@ package funkin.play.event; import flixel.tweens.FlxTween; import flixel.FlxCamera; 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.song.SongData; -import funkin.play.event.SongEventData; -import funkin.play.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventData.SongEventFieldType; +import funkin.data.event.SongEventData.SongEventSchema; /** * This class represents a handler for camera zoom events. @@ -76,8 +79,7 @@ class ZoomCameraSongEvent extends SongEvent return; } - FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), - {ease: easeFunction}); + FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction}); } } diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index 25b23eee7..e6202a3a0 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -1,6 +1,6 @@ package funkin.play.notes; -import funkin.play.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongNoteData; import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 8847636bd..71cb99ee2 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -11,7 +11,7 @@ import funkin.play.notes.NoteHoldCover; import funkin.play.notes.NoteSplash; import funkin.play.notes.NoteSprite; import funkin.play.notes.SustainTrail; -import funkin.play.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongNoteData; import funkin.ui.PreferencesMenu; import funkin.util.SortUtil; diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 72d22191b..2601fd961 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -2,7 +2,7 @@ package funkin.play.notes; import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.NoteDirection; -import funkin.play.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongNoteData; import flixel.util.FlxDirectionFlags; import flixel.FlxSprite; import flixel.graphics.FlxGraphic; diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx index 97871b657..34c1ce9c3 100644 --- a/source/funkin/play/notes/notestyle/NoteStyle.hx +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -104,7 +104,8 @@ class NoteStyle implements IRegistryEntry noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); - if (noteFrames == null) { + if (noteFrames == null) + { throw 'Could not load note frames for note style: $id'; } @@ -139,13 +140,13 @@ class NoteStyle implements IRegistryEntry function buildNoteAnimations(target:NoteSprite):Void { 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); - 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); - 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); - 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 @@ -302,7 +303,7 @@ class NoteStyle implements IRegistryEntry return 'NoteStyle($id)'; } - public function _fetchData(id:String):Null + static function _fetchData(id:String):Null { return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id)); } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 715629a51..b008f6a8e 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -5,14 +5,16 @@ import openfl.utils.Assets; import funkin.modding.events.ScriptEvent; import funkin.modding.IScriptedClass; import funkin.audio.VoicesGroup; -import funkin.play.song.SongData.SongChartData; -import funkin.play.song.SongData.SongDataParser; -import funkin.play.song.SongData.SongEventData; -import funkin.play.song.SongData.SongMetadata; -import funkin.play.song.SongData.SongNoteData; -import funkin.play.song.SongData.SongPlayableChar; -import funkin.play.song.SongData.SongTimeChange; -import funkin.play.song.SongData.SongTimeFormat; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongPlayableChar; +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. @@ -23,9 +25,26 @@ import funkin.play.song.SongData.SongTimeFormat; * It also receives script events; scripted classes which extend this class * can be used to perform custom gameplay behaviors only on specific songs. */ -class Song implements IPlayStateScriptedClass +@:nullSafety +class Song implements IPlayStateScriptedClass implements IRegistryEntry { - 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 = 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; final _metadata:Array; @@ -39,33 +58,56 @@ class Song implements IPlayStateScriptedClass var difficultyIds:Array; + 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 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 = []; difficultyIds = []; difficulties = new Map(); - try + _data = _fetchData(id); + + _metadata = _data == null ? [] : [_data]; + + for (meta in fetchVariationMetadata(id)) + _metadata.push(meta); + + if (_metadata.length == 0) { - _metadata = SongDataParser.loadSongMetadata(songId); - } - catch (e) - { - _metadata = []; + trace('[WARN] Could not find song data for songId: $id'); + return; } - if (_metadata.length == 0 && !ignoreErrors) - { - throw 'Could not find song data for songId: $songId'; - } - else + variations.clear(); + variations.push('default'); + if (_data != null && _data.playData != null) { + for (vari in _data.playData.songVariations) + variations.push(vari); + populateFromMetadata(); } } @@ -74,7 +116,7 @@ class Song implements IPlayStateScriptedClass public static function buildRaw(songId:String, metadata:Array, variations:Array, charts:Map, validScore:Bool = false):Song { - var result:Song = new Song(songId, true); + var result:Song = new Song(songId); result._metadata.clear(); for (meta in metadata) @@ -112,6 +154,8 @@ class Song implements IPlayStateScriptedClass // Variations may have different artist, time format, generatedBy, etc. for (metadata in _metadata) { + if (metadata == null || metadata.playData == null) continue; + // 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) // 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.noteSkin = metadata.playData.noteSkin; + difficulties.set(diffId, difficulty); + difficulty.chars = new Map(); + if (metadata.playData.playableChars == null) continue; for (charId in metadata.playData.playableChars.keys()) { - var char = metadata.playData.playableChars.get(charId); - + var char:Null = metadata.playData.playableChars.get(charId); + if (char == null) continue; difficulty.chars.set(charId, char); } - - difficulties.set(diffId, difficulty); } } } @@ -157,11 +202,14 @@ class Song implements IPlayStateScriptedClass clearCharts(); } - trace('Caching ${variations.length} chart files for song $songId'); + trace('Caching ${variations.length} chart files for song $id'); for (variation in variations) { - var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation); - applyChartData(chartData, variation); + var version:Null = SongRegistry.instance.fetchEntryChartVersion(id, variation); + if (version == null) continue; + var chart:Null = SongRegistry.instance.parseEntryChartDataWithMigration(id, version, variation); + if (chart == null) continue; + applyChartData(chart, variation); } trace('Done caching charts.'); } @@ -181,8 +229,8 @@ class Song implements IPlayStateScriptedClass difficulties.set(diffId, difficulty); } // Add the chart data to the difficulty. - difficulty.notes = chartData.notes.get(diffId); - difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); + difficulty.notes = chartNotes.get(diffId) ?? []; + difficulty.scrollSpeed = chartData.getScrollSpeed(diffId) ?? 1.0; difficulty.events = chartData.events; } @@ -193,7 +241,7 @@ class Song implements IPlayStateScriptedClass * @param diffId The difficulty ID, such as `easy` or `hard`. * @return The difficulty data. */ - public inline function getDifficulty(diffId:String = null):SongDifficulty + public inline function getDifficulty(?diffId:String):Null { if (diffId == null) diffId = difficulties.keys().array()[0]; @@ -223,9 +271,11 @@ class Song implements IPlayStateScriptedClass public function toString():String { - return 'Song($songId)'; + return 'Song($id)'; } + public function destroy():Void {} + public function onPause(event:PauseScriptEvent):Void {}; public function onResume(event:ScriptEvent):Void {}; @@ -265,6 +315,27 @@ class Song implements IPlayStateScriptedClass public function onDestroy(event:ScriptEvent):Void {}; public function onUpdate(event:UpdateScriptEvent):Void {}; + + static function _fetchData(id:String):Null + { + trace('Fetching song metadata for $id'); + var version:Null = SongRegistry.instance.fetchEntryMetadataVersion(id); + if (version == null) return null; + return SongRegistry.instance.parseEntryMetadataWithMigration(id, '', version); + } + + function fetchVariationMetadata(id:String):Array + { + var result:Array = []; + for (vari in variations) + { + var version:Null = SongRegistry.instance.fetchEntryMetadataVersion(id, vari); + if (version == null) continue; + var meta:Null = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version); + if (meta != null) result.push(meta); + } + return result; + } } class SongDifficulty @@ -299,7 +370,7 @@ class SongDifficulty public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT; public var divisions:Null = SongValidator.DEFAULT_DIVISIONS; 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 = []; @@ -351,18 +422,18 @@ class SongDifficulty var currentPlayer:Null = getPlayableChar(currentPlayerId); if (currentPlayer != null) { - FlxG.sound.cache(Paths.inst(this.song.songId, currentPlayer.inst)); + FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst)); } 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 { 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); 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 []; } @@ -398,24 +469,24 @@ class SongDifficulty // For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`. 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)) { // Remove the last suffix. // For example, bf-car becomes bf. playerId = playerId.split('-').slice(0, -1).join('-'); // 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 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)) { // Remove the last suffix. opponentId = opponentId.split('-').slice(0, -1).join('-'); // 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 = []; @@ -424,7 +495,7 @@ class SongDifficulty if (voicePlayer == null && voiceOpponent == null) { // 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; } @@ -442,7 +513,7 @@ class SongDifficulty 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; } diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx deleted file mode 100644 index cef4c98f6..000000000 --- a/source/funkin/play/song/SongData.hx +++ /dev/null @@ -1,1021 +0,0 @@ -package funkin.play.song; - -import funkin.modding.events.ScriptEventDispatcher; -import funkin.modding.events.ScriptEvent; -import flixel.util.typeLimit.OneOfTwo; -import funkin.modding.events.ScriptEvent; -import funkin.modding.events.ScriptEventDispatcher; -import funkin.play.song.ScriptedSong; -import funkin.util.assets.DataAssets; -import haxe.DynamicAccess; -import haxe.Json; -import openfl.utils.Assets; -import thx.semver.Version; -import funkin.util.SerializerUtil; - -/** - * Contains utilities for loading and parsing stage data. - */ -class SongDataParser -{ - /** - * A list containing all the songs available to the game. - */ - static final songCache:Map = new Map(); - - static final DEFAULT_SONG_ID:String = 'UNKNOWN'; - static final SONG_DATA_PATH:String = 'songs/'; - static final MUSIC_DATA_PATH:String = 'music/'; - static final SONG_DATA_SUFFIX:String = '-metadata.json'; - - /** - * Parses and preloads the game's song metadata and scripts when the game starts. - * - * If you want to force song metadata to be reloaded, you can just call this function again. - */ - public static function loadSongCache():Void - { - clearSongCache(); - trace('Loading song cache...'); - - // - // SCRIPTED SONGS - // - var scriptedSongClassNames:Array = ScriptedSong.listScriptClasses(); - trace(' Instantiating ${scriptedSongClassNames.length} scripted songs...'); - for (songCls in scriptedSongClassNames) - { - var song:Song = ScriptedSong.init(songCls, DEFAULT_SONG_ID); - if (song != null) - { - trace(' Loaded scripted song: ${song.songId}'); - songCache.set(song.songId, song); - } - else - { - trace(' Failed to instantiate scripted song class: ${songCls}'); - } - } - - // - // UNSCRIPTED SONGS - // - var songIdList:Array = DataAssets.listDataFilesInPath(SONG_DATA_PATH, SONG_DATA_SUFFIX).map(function(songDataPath:String):String { - return songDataPath.split('/')[0]; - }); - var unscriptedSongIds:Array = songIdList.filter(function(songId:String):Bool { - return !songCache.exists(songId); - }); - trace(' Instantiating ${unscriptedSongIds.length} non-scripted songs...'); - for (songId in unscriptedSongIds) - { - try - { - var song:Song = new Song(songId); - if (song != null) - { - trace(' Loaded song data: ${song.songId}'); - songCache.set(song.songId, song); - } - } - catch (e) - { - trace(' An error occurred while loading song data: ${songId}'); - trace(e); - // Assume error was already logged. - continue; - } - } - - trace(' Successfully loaded ${Lambda.count(songCache)} stages.'); - } - - /** - * Retrieves a particular song from the cache. - * @param songId The ID of the song to retrieve. - * @return The song, or null if it was not found. - */ - public static function fetchSong(songId:String):Null - { - if (songCache.exists(songId)) - { - var song:Song = songCache.get(songId); - trace('Successfully fetch song: ${songId}'); - - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); - ScriptEventDispatcher.callEvent(song, event); - return song; - } - else - { - trace('Failed to fetch song, not found in cache: ${songId}'); - return null; - } - } - - static function clearSongCache():Void - { - if (songCache != null) - { - songCache.clear(); - } - } - - /** - * A list of all the song IDs available to the game. - * @return The list of song IDs. - */ - public static function listSongIds():Array - { - return songCache.keys().array(); - } - - /** - * Loads the song metadata for a particular song. - * @param songId The ID of the song to load. - * @return The song metadata for each variation, or an empty array if the song was not found. - */ - public static function loadSongMetadata(songId:String):Array - { - var result:Array = []; - - var jsonStr:String = loadSongMetadataFile(songId); - var jsonData:Dynamic = SerializerUtil.fromJSON(jsonStr); - - var songMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, songId); - songMetadata = SongValidator.validateSongMetadata(songMetadata, songId); - - if (songMetadata == null) - { - return result; - } - - result.push(songMetadata); - - var variations:Array = songMetadata.playData.songVariations; - - for (variation in variations) - { - var variationJsonStr:String = loadSongMetadataFile(songId, variation); - var variationJsonData:Dynamic = SerializerUtil.fromJSON(variationJsonStr); - var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}:${variation}'); - variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}:${variation}'); - if (variationSongMetadata != null) - { - variationSongMetadata.variation = variation; - result.push(variationSongMetadata); - } - } - - return result; - } - - static function loadSongMetadataFile(songPath:String, variation:String = ''):String - { - var songMetadataFilePath:String = (variation != '') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-metadata-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-metadata'); - - var rawJson:String = Assets.getText(songMetadataFilePath).trim(); - - while (!rawJson.endsWith('}') && rawJson.length > 0) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return rawJson; - } - - public static function parseMusicMetadata(musicId:String):SongMetadata - { - var rawJson:String = loadMusicMetadataFile(musicId); - var jsonData:Dynamic = null; - try - { - jsonData = Json.parse(rawJson); - } - catch (e) {} - - var musicMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, musicId); - musicMetadata = SongValidator.validateSongMetadata(musicMetadata, musicId); - - return musicMetadata; - } - - static function loadMusicMetadataFile(musicPath:String, variation:String = ''):String - { - var musicMetadataFilePath:String = (variation != '' || variation == "default") ? Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata-$variation.json') : Paths.file('$MUSIC_DATA_PATH$musicPath/$musicPath-metadata.json'); - - var rawJson:String = Assets.getText(musicMetadataFilePath).trim(); - - while (!rawJson.endsWith("}")) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return rawJson; - } - - public static function parseSongChartData(songId:String, variation:String = ''):SongChartData - { - var rawJson:String = loadSongChartDataFile(songId, variation); - var jsonData:Dynamic = null; - try - { - jsonData = Json.parse(rawJson); - } - catch (e) - { - trace('Failed to parse song chart data: ${songId} (${variation})'); - trace(e); - } - - var songChartData:SongChartData = SongMigrator.migrateSongChartData(jsonData, songId); - songChartData = SongValidator.validateSongChartData(songChartData, songId); - - if (songChartData == null) - { - trace('Failed to validate song chart data: ${songId}'); - return null; - } - - return songChartData; - } - - static function loadSongChartDataFile(songPath:String, variation:String = ''):String - { - var songChartDataFilePath:String = (variation != '' && variation != 'default') ? Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart-$variation') : Paths.json('$SONG_DATA_PATH$songPath/$songPath-chart'); - - var rawJson:String = Assets.getText(songChartDataFilePath).trim(); - - while (!rawJson.endsWith('}') && rawJson.length > 0) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return rawJson; - } -} - -typedef RawSongMetadata = -{ - /** - * A semantic versioning string for the song data format. - * - */ - var version:Version; - - var songName:String; - var artist:String; - var timeFormat:SongTimeFormat; - var divisions:Null; // Optional field - var timeChanges:Array; - var looped:Bool; - var playData:SongPlayData; - var generatedBy:String; - - /** - * Defaults to `default` or `''`. Populated later. - */ - var variation:String; -}; - -@:forward -abstract SongMetadata(RawSongMetadata) -{ - public function new(songName:String, artist:String, variation:String = 'default') - { - this = - { - version: SongMigrator.CHART_VERSION, - songName: songName, - artist: artist, - timeFormat: 'ms', - divisions: null, - timeChanges: [new SongTimeChange(-1, 0, 100, 4, 4, [4, 4, 4, 4])], - looped: false, - playData: - { - songVariations: [], - difficulties: ['normal'], - - playableChars: - { - bf: new SongPlayableChar('gf', 'dad'), - }, - - stage: 'mainStage', - noteSkin: 'Normal' - }, - generatedBy: SongValidator.DEFAULT_GENERATEDBY, - - // Variation ID. - variation: variation - }; - } - - public function clone(?newVariation:String = null):SongMetadata - { - var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation); - result.version = this.version; - result.timeFormat = this.timeFormat; - result.divisions = this.divisions; - result.timeChanges = this.timeChanges; - result.looped = this.looped; - result.playData = this.playData; - result.generatedBy = this.generatedBy; - - return result; - } -} - -typedef SongPlayData = -{ - var songVariations:Array; - var difficulties:Array; - - /** - * Keys are the player characters and the values give info on what opponent/GF/inst to use. - */ - var playableChars:DynamicAccess; - - var stage:String; - var noteSkin:String; -} - -typedef RawSongPlayableChar = -{ - var g:String; - var o:String; - var i:String; -} - -typedef RawSongNoteData = -{ - /** - * The timestamp of the note. The timestamp is in the format of the song's time format. - */ - var t:Float; - - /** - * Data for the note. Represents the index on the strumline. - * 0 = left, 1 = down, 2 = up, 3 = right - * `floor(direction / strumlineSize)` specifies which strumline the note is on. - * 0 = player, 1 = opponent, etc. - */ - var d:Int; - - /** - * Length of the note, if applicable. - * Defaults to 0 for single notes. - */ - var l:Float; - - /** - * The kind of the note. - * This can allow the note to include information used for custom behavior. - * Defaults to blank or `"normal"`. - */ - var k:String; -} - -abstract SongNoteData(RawSongNoteData) -{ - public function new(time:Float, data:Int, length:Float = 0, kind:String = '') - { - this = - { - t: time, - d: data, - l: length, - k: kind - }; - } - - /** - * The timestamp of the note, in milliseconds. - */ - public var time(get, set):Float; - - function get_time():Float - { - return this.t; - } - - function set_time(value:Float):Float - { - return this.t = value; - } - - /** - * The timestamp of the note, in steps. - */ - public var stepTime(get, never):Float; - - function get_stepTime():Float - { - return Conductor.getTimeInSteps(abstract.time); - } - - /** - * The raw data for the note. - */ - public var data(get, set):Int; - - function get_data():Int - { - return this.d; - } - - function set_data(value:Int):Int - { - return this.d = value; - } - - /** - * The direction of the note, if applicable. - * Strips the strumline index from the data. - * - * 0 = left, 1 = down, 2 = up, 3 = right - */ - public inline function getDirection(strumlineSize:Int = 4):Int - { - return abstract.data % strumlineSize; - } - - public function getDirectionName(strumlineSize:Int = 4):String - { - switch (abstract.data % strumlineSize) - { - case 0: - return 'Left'; - case 1: - return 'Down'; - case 2: - return 'Up'; - case 3: - return 'Right'; - default: - return 'Unknown'; - } - } - - /** - * The strumline index of the note, if applicable. - * Strips the direction from the data. - * - * 0 = player, 1 = opponent, etc. - */ - public inline function getStrumlineIndex(strumlineSize:Int = 4):Int - { - return Math.floor(abstract.data / strumlineSize); - } - - /** - * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side). - * TODO: The name of this function is a little misleading; what about mines? - * @param strumlineSize Defaults to 4. - * @return True if it's Boyfriend's note. - */ - public inline function getMustHitNote(strumlineSize:Int = 4):Bool - { - return getStrumlineIndex(strumlineSize) == 0; - } - - /** - * If this is a hold note, this is the length of the hold note in milliseconds. - * @default 0 (not a hold note) - */ - public var length(get, set):Float; - - function get_length():Float - { - return this.l; - } - - function set_length(value:Float):Float - { - return this.l = value; - } - - /** - * If this is a hold note, this is the length of the hold note in steps. - * @default 0 (not a hold note) - */ - public var stepLength(get, set):Float; - - function get_stepLength():Float - { - return Conductor.getTimeInSteps(abstract.time + abstract.length) - abstract.stepTime; - } - - function set_stepLength(value:Float):Float - { - return abstract.length = Conductor.getStepTimeInMs(value) - abstract.time; - } - - public var isHoldNote(get, never):Bool; - - public function get_isHoldNote():Bool - { - return this.l > 0; - } - - public var kind(get, set):String; - - function get_kind():String - { - if (this.k == null || this.k == '') return 'normal'; - - return this.k; - } - - function set_kind(value:String):String - { - if (value == 'normal' || value == '') value = null; - return this.k = value; - } - - @:op(A == B) - public function op_equals(other:SongNoteData):Bool - { - if (abstract.kind == '') - { - if (other.kind != '' && other.kind != 'normal') return false; - } - else - { - if (other.kind == '' || other.kind != abstract.kind) return false; - } - - return abstract.time == other.time && abstract.data == other.data && abstract.length == other.length; - } - - @:op(A != B) - public function op_notEquals(other:SongNoteData):Bool - { - if (abstract.kind == '') - { - if (other.kind != '' && other.kind != 'normal') return true; - } - else - { - if (other.kind == '' || other.kind != abstract.kind) return true; - } - - return abstract.time != other.time || abstract.data != other.data || abstract.length != other.length; - } - - @:op(A > B) - public function op_greaterThan(other:SongNoteData):Bool - { - return abstract.time > other.time; - } - - @:op(A < B) - public function op_lessThan(other:SongNoteData):Bool - { - return this.t < other.time; - } - - @:op(A >= B) - public function op_greaterThanOrEquals(other:SongNoteData):Bool - { - return this.t >= other.time; - } - - @:op(A <= B) - public function op_lessThanOrEquals(other:SongNoteData):Bool - { - return this.t <= other.time; - } -} - -typedef RawSongEventData = -{ - /** - * The timestamp of the event. The timestamp is in the format of the song's time format. - */ - var t:Float; - - /** - * The kind of the event. - * Examples include "FocusCamera" and "PlayAnimation" - * Custom events can be added by scripts with the `ScriptedSongEvent` class. - */ - var e:String; - - /** - * The data for the event. - * This can allow the event to include information used for custom behavior. - * Data type depends on the event kind. It can be anything that's JSON serializable. - */ - var v:DynamicAccess; - - /** - * Whether this event has been activated. - * This is only used internally by the game. It should not be serialized. - */ - @:optional var a:Bool; -} - -abstract SongEventData(RawSongEventData) -{ - public function new(time:Float, event:String, value:Dynamic = null) - { - this = - { - t: time, - e: event, - v: value, - a: false - }; - } - - public var time(get, set):Float; - - function get_time():Float - { - return this.t; - } - - function set_time(value:Float):Float - { - return this.t = value; - } - - public var stepTime(get, never):Float; - - function get_stepTime():Float - { - return Conductor.getTimeInSteps(abstract.time); - } - - public var event(get, set):String; - - function get_event():String - { - return this.e; - } - - function set_event(value:String):String - { - return this.e = value; - } - - public var value(get, set):Dynamic; - - function get_value():Dynamic - { - return this.v; - } - - function set_value(value:Dynamic):Dynamic - { - return this.v = value; - } - - public var activated(get, set):Bool; - - function get_activated():Bool - { - return this.a; - } - - function set_activated(value:Bool):Bool - { - return this.a = value; - } - - public inline function getDynamic(key:String):Null - { - return this.v.get(key); - } - - public inline function getBool(key:String):Null - { - return cast this.v.get(key); - } - - public inline function getInt(key:String):Null - { - return cast this.v.get(key); - } - - public inline function getFloat(key:String):Null - { - return cast this.v.get(key); - } - - public inline function getString(key:String):String - { - return cast this.v.get(key); - } - - public inline function getArray(key:String):Array - { - return cast this.v.get(key); - } - - public inline function getBoolArray(key:String):Array - { - return cast this.v.get(key); - } - - @:op(A == B) - public function op_equals(other:SongEventData):Bool - { - return this.t == other.time && this.e == other.event && this.v == other.value; - } - - @:op(A != B) - public function op_notEquals(other:SongEventData):Bool - { - return this.t != other.time || this.e != other.event || this.v != other.value; - } - - @:op(A > B) - public function op_greaterThan(other:SongEventData):Bool - { - return this.t > other.time; - } - - @:op(A < B) - public function op_lessThan(other:SongEventData):Bool - { - return this.t < other.time; - } - - @:op(A >= B) - public function op_greaterThanOrEquals(other:SongEventData):Bool - { - return this.t >= other.time; - } - - @:op(A <= B) - public function op_lessThanOrEquals(other:SongEventData):Bool - { - return this.t <= other.time; - } -} - -abstract SongPlayableChar(RawSongPlayableChar) -{ - public function new(girlfriend:String, opponent:String, inst:String = '') - { - this = - { - g: girlfriend, - o: opponent, - i: inst - }; - } - - public var girlfriend(get, set):String; - - function get_girlfriend():String - { - return this.g; - } - - function set_girlfriend(value:String):String - { - return this.g = value; - } - - public var opponent(get, set):String; - - function get_opponent():String - { - return this.o; - } - - function set_opponent(value:String):String - { - return this.o = value; - } - - public var inst(get, set):String; - - function get_inst():String - { - return this.i; - } - - function set_inst(value:String):String - { - return this.i = value; - } -} - -typedef RawSongChartData = -{ - var version:Version; - - var scrollSpeed:DynamicAccess; - var events:Array; - var notes:DynamicAccess>; - var generatedBy:String; -}; - -@:forward -abstract SongChartData(RawSongChartData) -{ - public function new(scrollSpeed:Float, events:Array, notes:Array) - { - this = - { - version: SongMigrator.CHART_VERSION, - - events: events, - notes: - { - normal: notes - }, - scrollSpeed: - { - normal: scrollSpeed - }, - generatedBy: SongValidator.DEFAULT_GENERATEDBY - } - } - - public function getScrollSpeed(diff:String = 'default'):Float - { - var result:Float = this.scrollSpeed.get(diff); - - if (result == 0.0 && diff != 'default') return getScrollSpeed('default'); - - return (result == 0.0) ? 1.0 : result; - } - - public function setScrollSpeed(value:Float, diff:String = 'default'):Float - { - return this.scrollSpeed.set(diff, value); - } - - public function getNotes(diff:String):Array - { - var result:Array = this.notes.get(diff); - - if (result == null && diff != 'normal') return getNotes('normal'); - - return (result == null) ? [] : result; - } - - public function setNotes(value:Array, diff:String):Array - { - return this.notes.set(diff, value); - } - - public function getEvents():Array - { - return this.events; - } - - public function setEvents(value:Array):Array - { - return this.events = value; - } -} - -typedef RawSongTimeChange = -{ - /** - * Timestamp in specified `timeFormat`. - */ - var t:Float; - - /** - * Time in beats (int). The game will calculate further beat values based on this one, - * so it can do it in a simple linear fashion. - */ - var b:Null; - - /** - * Quarter notes per minute (float). Cannot be empty in the first element of the list, - * but otherwise it's optional, and defaults to the value of the previous element. - */ - var bpm:Float; - - /** - * Time signature numerator (int). Optional, defaults to 4. - */ - var n:Int; - - /** - * Time signature denominator (int). Optional, defaults to 4. Should only ever be a power of two. - */ - var d:Int; - - /** - * Beat tuplets (Array or int). This defines how many steps each beat is divided into. - * It can either be an array of length `n` (see above) or a single integer number. - * Optional, defaults to `[4]`. - */ - var bt:OneOfTwo>; -} - -/** - * Add aliases to the minimalized property names of the typedef, - * to improve readability. - */ -abstract SongTimeChange(RawSongTimeChange) from RawSongTimeChange -{ - public function new(timeStamp:Float, ?beatTime:Float, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array) - { - this = - { - t: timeStamp, - b: beatTime, - bpm: bpm, - n: timeSignatureNum, - d: timeSignatureDen, - bt: beatTuplets, - } - } - - public var timeStamp(get, set):Float; - - function get_timeStamp():Float - { - return this.t; - } - - function set_timeStamp(value:Float):Float - { - return this.t = value; - } - - public var beatTime(get, set):Null; - - public function get_beatTime():Null - { - return this.b; - } - - public function set_beatTime(value:Null):Null - { - return this.b = value; - } - - public var bpm(get, set):Float; - - function get_bpm():Float - { - return this.bpm; - } - - function set_bpm(value:Float):Float - { - return this.bpm = value; - } - - public var timeSignatureNum(get, set):Int; - - function get_timeSignatureNum():Int - { - return this.n; - } - - function set_timeSignatureNum(value:Int):Int - { - return this.n = value; - } - - public var timeSignatureDen(get, set):Int; - - function get_timeSignatureDen():Int - { - return this.d; - } - - function set_timeSignatureDen(value:Int):Int - { - return this.d = value; - } - - public var beatTuplets(get, set):Array; - - function get_beatTuplets():Array - { - if (Std.isOfType(this.bt, Int)) - { - return [this.bt]; - } - else - { - return this.bt; - } - } - - function set_beatTuplets(value:Array):Array - { - return this.bt = value; - } -} - -enum abstract SongTimeFormat(String) from String to String -{ - var TICKS = 'ticks'; - var FLOAT = 'float'; - var MILLISECONDS = 'ms'; -} diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx deleted file mode 100644 index a7cbd1b6c..000000000 --- a/source/funkin/play/song/SongDataUtils.hx +++ /dev/null @@ -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, offset:Int):Array - { - 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, offset:Int):Array - { - 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, subtrahend:Array) - { - 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, subtrahend:Array) - { - 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, ?strumlineSize:Int = 4):Array - { - 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, ?timeOffset:Int = null):Array - { - 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, ?timeOffset:Int = null):Array - { - 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, desc:Bool = false):Array - { - // 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, desc:Bool = false):Array - { - // 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, start:Float, end:Float):Array - { - 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, start:Float, end:Float):Array - { - 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, start:Int, end:Int):Array - { - 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, data:Array):Array - { - return notes.filter(function(note:SongNoteData):Bool { - return data.indexOf(note.data) != -1; - }); - } -} - -typedef SongClipboardItems = -{ - notes:Array, - events:Array -} diff --git a/source/funkin/play/song/SongMigrator.hx b/source/funkin/play/song/SongMigrator.hx index bb8718bb7..48ae50037 100644 --- a/source/funkin/play/song/SongMigrator.hx +++ b/source/funkin/play/song/SongMigrator.hx @@ -1,11 +1,11 @@ package funkin.play.song; import funkin.play.song.formats.FNFLegacy; -import funkin.play.song.SongData.SongChartData; -import funkin.play.song.SongData.SongEventData; -import funkin.play.song.SongData.SongMetadata; -import funkin.play.song.SongData.SongNoteData; -import funkin.play.song.SongData.SongPlayableChar; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongData.SongPlayableChar; import funkin.util.VersionUtil; class SongMigrator @@ -176,7 +176,7 @@ class SongMigrator songMetadata.playData.songVariations = []; // Set the song's song variations. - songMetadata.playData.playableChars = {}; + songMetadata.playData.playableChars = []; try { 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 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; if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes)); diff --git a/source/funkin/play/song/SongSerializer.hx b/source/funkin/play/song/SongSerializer.hx index a08b722da..a0a468c5b 100644 --- a/source/funkin/play/song/SongSerializer.hx +++ b/source/funkin/play/song/SongSerializer.hx @@ -1,7 +1,7 @@ package funkin.play.song; -import funkin.play.song.SongData.SongChartData; -import funkin.play.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongMetadata; import funkin.util.SerializerUtil; import lime.utils.Bytes; import openfl.events.Event; diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index 16ea88664..3dacbf2c7 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -1,10 +1,11 @@ package funkin.play.song; -import funkin.play.song.SongData.SongChartData; -import funkin.play.song.SongData.SongMetadata; -import funkin.play.song.SongData.SongPlayData; -import funkin.play.song.SongData.SongTimeChange; -import funkin.play.song.SongData.SongTimeFormat; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongPlayData; +import funkin.data.song.SongData.SongTimeChange; +import funkin.data.song.SongData.SongTimeFormat; /** * For SongMetadata and SongChartData objects, @@ -20,13 +21,6 @@ class SongValidator public static final DEFAULT_STAGE:String = "mainStage"; 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). * @@ -59,7 +53,7 @@ class SongValidator } if (input.generatedBy == null) { - input.generatedBy = DEFAULT_GENERATEDBY; + input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; } input.timeChanges = validateTimeChanges(input.timeChanges, songId); diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index fd179c481..64ad3a3ff 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -1,8 +1,8 @@ package funkin.ui.debug.charting; -import funkin.play.song.SongData.SongEventData; -import funkin.play.song.SongData.SongNoteData; -import funkin.play.song.SongDataUtils; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.data.song.SongDataUtils; using Lambda; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index e5b2d332c..88cf31332 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -3,8 +3,8 @@ package funkin.ui.debug.charting; import funkin.play.character.CharacterData; import funkin.util.Constants; import funkin.util.SerializerUtil; -import funkin.play.song.SongData.SongChartData; -import funkin.play.song.SongData.SongMetadata; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongData.SongMetadata; import flixel.util.FlxTimer; import funkin.util.SortUtil; import funkin.input.Cursor; @@ -13,9 +13,9 @@ import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.song.Song; import funkin.play.song.SongMigrator; import funkin.play.song.SongValidator; -import funkin.play.song.SongData.SongDataParser; -import funkin.play.song.SongData.SongPlayableChar; -import funkin.play.song.SongData.SongTimeChange; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongPlayableChar; +import funkin.data.song.SongData.SongTimeChange; import funkin.util.FileUtil; import haxe.io.Path; import haxe.ui.components.Button; @@ -106,18 +106,17 @@ class ChartEditorDialogHandler var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox); - var songList:Array = SongDataParser.listSongIds(); + var songList:Array = SongRegistry.instance.listEntryIds(); songList.sort(SortUtil.alphabetically); for (targetSongId in songList) { - var songData:Song = SongDataParser.fetchSong(targetSongId); - + var songData:Null = SongRegistry.instance.fetchEntry(targetSongId); if (songData == null) continue; - var songName:Null = songData.getDifficulty('normal') ?.songName; - if (songName == null) songName = songData.getDifficulty() ?.songName; - if (songName == null) + var songName:Null = songData.getDifficulty('normal')?.songName; + if (songName == null) songName = songData.getDifficulty()?.songName; + if (songName == null) // Still null? { trace('[WARN] Could not fetch song name for ${targetSongId}'); } @@ -470,9 +469,9 @@ class ChartEditorDialogHandler var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown); dialogNoteSkin.onChange = function(event:UIEvent) { 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); dialogBPM.onChange = function(event:UIEvent) { @@ -481,7 +480,7 @@ class ChartEditorDialogHandler var timeChanges:Array = state.currentSongMetadata.timeChanges; 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 { @@ -502,7 +501,7 @@ class ChartEditorDialogHandler }; // Empty the character list. - state.currentSongMetadata.playData.playableChars = {}; + state.currentSongMetadata.playData.playableChars = []; // Add at least one character group with no Remove button. dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null)); @@ -516,7 +515,8 @@ class ChartEditorDialogHandler { var groupKey:String = key; - var getCharData:Void->SongPlayableChar = function() { + var getCharData:Void->Null = function():Null { + if (state.currentSongMetadata.playData == null) return null; if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}'; var result = state.currentSongMetadata.playData.playableChars.get(groupKey); @@ -528,42 +528,53 @@ class ChartEditorDialogHandler return result; } - var moveCharGroup:String->Void = function(target:String) { - var charData = getCharData(); + var moveCharGroup:String->Void = function(target:String):Void { + var charData:Null = getCharData(); + if (charData == null) return; + + if (state.currentSongMetadata.playData.playableChars == null) return; state.currentSongMetadata.playData.playableChars.remove(groupKey); state.currentSongMetadata.playData.playableChars.set(target, charData); 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); removeFunc(); } - var charData:SongPlayableChar = getCharData(); + var charData:Null = getCharData(); var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT); - var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown); - charGroupPlayer.onChange = function(event:UIEvent) { + var charGroupPlayer:Null = charGroup.findComponent('charGroupPlayer', DropDown); + 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; moveCharGroup(event.data.id); }; - var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); - charGroupOpponent.onChange = function(event:UIEvent) { + var charGroupOpponent:Null = charGroup.findComponent('charGroupOpponent', DropDown); + 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; }; - charGroupOpponent.value = getCharData().opponent; + charGroupOpponent.value = charData.opponent; - var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown); - charGroupGirlfriend.onChange = function(event:UIEvent) { + var charGroupGirlfriend:Null = charGroup.findComponent('charGroupGirlfriend', DropDown); + 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; }; - charGroupGirlfriend.value = getCharData().girlfriend; + charGroupGirlfriend.value = charData.girlfriend; - var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button); - charGroupRemove.onClick = function(event:UIEvent) { + var charGroupRemove:Null