From ffd0a9839346cb65ab95643ba2f5fa790988c20c Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Sat, 21 Oct 2023 01:04:50 -0400 Subject: [PATCH] FNFC file rework (includes command line quicklaunch) --- Project.xml | 5 +- assets | 2 +- source/Main.hx | 1 + source/funkin/InitState.hx | 20 +- source/{ => funkin}/Preloader.hx | 5 +- source/funkin/data/DataParse.hx | 16 ++ source/funkin/data/DataWrite.hx | 24 +- source/funkin/data/animation/AnimationData.hx | 3 + .../data/notestyle/NoteStyleRegistry.hx | 4 +- source/funkin/data/song/SongData.hx | 32 ++- source/funkin/data/song/SongRegistry.hx | 52 +++- .../data/song/importer/ChartManifestData.hx | 84 +++++++ .../data/song/migrator/SongDataMigrator.hx | 56 ++++- .../data/song/migrator/SongData_v2_0_0.hx | 9 +- .../data/song/migrator/SongData_v2_1_0.hx | 108 ++++++++ source/funkin/play/PlayState.hx | 6 +- source/funkin/play/song/Song.hx | 25 +- source/funkin/play/stage/Stage.hx | 23 +- source/funkin/play/stage/StageData.hx | 130 +++++++--- .../funkin/save/migrator/SaveDataMigrator.hx | 5 +- .../debug/charting/ChartEditorAudioHandler.hx | 2 +- .../ui/debug/charting/ChartEditorCommand.hx | 16 +- .../charting/ChartEditorDialogHandler.hx | 158 ++++++++++-- .../ChartEditorImportExportHandler.hx | 232 +++++++++++++++--- .../ui/debug/charting/ChartEditorState.hx | 93 +++++-- source/funkin/ui/haxeui/HaxeUIState.hx | 17 ++ source/funkin/util/CLIUtil.hx | 134 ++++++++++ source/funkin/util/FileUtil.hx | 65 ++++- source/funkin/util/SerializerUtil.hx | 2 + source/funkin/util/VersionUtil.hx | 18 ++ .../data/notestyle/NoteStyleRegistryTest.hx | 2 +- 31 files changed, 1162 insertions(+), 187 deletions(-) rename source/{ => funkin}/Preloader.hx (93%) create mode 100644 source/funkin/data/song/importer/ChartManifestData.hx create mode 100644 source/funkin/data/song/migrator/SongData_v2_1_0.hx create mode 100644 source/funkin/util/CLIUtil.hx diff --git a/Project.xml b/Project.xml index 69400d8b1..9fad26fd7 100644 --- a/Project.xml +++ b/Project.xml @@ -4,10 +4,7 @@ - - - + diff --git a/assets b/assets index ef79a6cf1..118b62295 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit ef79a6cf1ae3dcbd86a5b798f8117a6c692c0156 +Subproject commit 118b622953171aaf127cb160538e21bc468620e2 diff --git a/source/Main.hx b/source/Main.hx index dffe666b7..726c1fdbf 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -11,6 +11,7 @@ import openfl.display.Sprite; import openfl.events.Event; import openfl.Lib; import openfl.media.Video; +import funkin.util.CLIUtil; import openfl.net.NetStream; class Main extends Sprite diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index ecfa32eb3..fbde22e1b 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.ui.debug.charting.ChartEditorState; import flixel.FlxState; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond; @@ -26,6 +27,8 @@ import funkin.play.stage.StageData.StageDataParser; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.modding.module.ModuleHandler; import funkin.ui.title.TitleState; +import funkin.util.CLIUtil; +import funkin.util.CLIUtil.CLIParams; #if discord_rpc import Discord.DiscordClient; #end @@ -247,8 +250,21 @@ class InitState extends FlxState */ function startGameNormally():Void { - FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); - FlxG.switchState(new TitleState()); + var params:CLIParams = CLIUtil.processArgs(); + trace('Command line args: ${params}'); + + if (params.chart.shouldLoadChart) + { + FlxG.switchState(new ChartEditorState( + { + fnfcTargetPath: params.chart.chartPath, + })); + } + else + { + FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); + FlxG.switchState(new TitleState()); + } } /** diff --git a/source/Preloader.hx b/source/funkin/Preloader.hx similarity index 93% rename from source/Preloader.hx rename to source/funkin/Preloader.hx index 3603d1a16..24015be05 100644 --- a/source/Preloader.hx +++ b/source/funkin/Preloader.hx @@ -1,4 +1,4 @@ -package; +package funkin; import flash.Lib; import flash.display.Bitmap; @@ -7,6 +7,7 @@ import flash.display.BlendMode; import flash.display.Sprite; import flixel.system.FlxBasePreloader; import openfl.display.Sprite; +import funkin.util.CLIUtil; @:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {} @@ -15,6 +16,8 @@ class Preloader extends FlxBasePreloader public function new(MinDisplayTime:Float = 0, ?AllowedURLs:Array) { super(MinDisplayTime, AllowedURLs); + + CLIUtil.resetWorkingDir(); // Bug fix for drag-and-drop. } var logo:Sprite; diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx index 64a53d2a4..cbd168a61 100644 --- a/source/funkin/data/DataParse.hx +++ b/source/funkin/data/DataParse.hx @@ -104,6 +104,22 @@ class DataParse } } + /** + * Parser which outputs a `Either>`. + */ + public static function eitherFloatOrFloats(json:Json, name:String):Null>> + { + switch (json.value) + { + case JNumber(f): + return Either.Left(Std.parseFloat(f)); + case JArray(fields): + return Either.Right(fields.map((field) -> cast Tools.getValue(field))); + default: + throw 'Expected property $name to be one or multiple floats, but it was ${json.value}.'; + } + } + /** * Parser which outputs a `Either`. * Used by the FNF legacy JSON importer. diff --git a/source/funkin/data/DataWrite.hx b/source/funkin/data/DataWrite.hx index 2f3a7632f..e277cb01c 100644 --- a/source/funkin/data/DataWrite.hx +++ b/source/funkin/data/DataWrite.hx @@ -3,11 +3,14 @@ package funkin.data; import funkin.util.SerializerUtil; import thx.semver.Version; import thx.semver.VersionRule; +import haxe.ds.Either; /** * `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON. * * Functions must be of the signature `(T) -> String`, where `T` is the type of the property. + * + * NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you! */ class DataWrite { @@ -23,11 +26,12 @@ class DataWrite } /** + * * `@:jcustomwrite(funkin.data.DataWrite.semverVersion)` */ public static function semverVersion(value:Version):String { - return value.toString(); + return '"${value.toString()}"'; } /** @@ -35,6 +39,22 @@ class DataWrite */ public static function semverVersionRule(value:VersionRule):String { - return value.toString(); + return '"${value.toString()}"'; + } + + /** + * `@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)` + */ + public static function eitherFloatOrFloats(value:Null>>):String + { + switch (value) + { + case null: + return '${1.0}'; + case Left(inner): + return '$inner'; + case Right(inner): + return dynamicValue(inner); + } } } diff --git a/source/funkin/data/animation/AnimationData.hx b/source/funkin/data/animation/AnimationData.hx index 9765f784c..a0214096c 100644 --- a/source/funkin/data/animation/AnimationData.hx +++ b/source/funkin/data/animation/AnimationData.hx @@ -59,7 +59,10 @@ typedef UnnamedAnimationData = * The prefix for the frames of the animation as defined by the XML file. * This will may or may not differ from the `name` of the animation, * depending on how your animator organized their FLA or whatever. + * + * NOTE: For Sparrow animations, this is not optional, but for Packer animations it is. */ + @:optional var prefix:String; /** diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx index da45da5f2..4255a644b 100644 --- a/source/funkin/data/notestyle/NoteStyleRegistry.hx +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -15,8 +15,6 @@ class NoteStyleRegistry extends BaseRegistry public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; - public static final DEFAULT_NOTE_STYLE_ID:String = "funkin"; - public static final instance:NoteStyleRegistry = new NoteStyleRegistry(); public function new() @@ -26,7 +24,7 @@ class NoteStyleRegistry extends BaseRegistry public function fetchDefault():NoteStyle { - return fetchEntry(DEFAULT_NOTE_STYLE_ID); + return fetchEntry(Constants.DEFAULT_NOTE_STYLE); } /** diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 88993e519..c0bd26332 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -1,9 +1,13 @@ package funkin.data.song; -import flixel.util.typeLimit.OneOfTwo; import funkin.data.song.SongRegistry; import thx.semver.Version; +/** + * Data containing information about a song. + * It should contain all the data needed to display a song in the Freeplay menu, or to load the assets required to play its chart. + * Data which is only necessary in-game should be stored in the SongChartData. + */ @:nullSafety class SongMetadata { @@ -35,13 +39,11 @@ class SongMetadata */ public var playData:SongPlayData; - // @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; - // @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS) public var timeFormat:SongTimeFormat; - // @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_SONGTIMECHANGES) public var timeChanges:Array; /** @@ -64,7 +66,7 @@ class SongMetadata this.playData.difficulties = []; this.playData.characters = new SongCharacterData('bf', 'gf', 'dad'); this.playData.stage = 'mainStage'; - this.playData.noteSkin = 'funkin'; + this.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE; this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; // Variation ID. this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation; @@ -298,23 +300,27 @@ class SongPlayData /** * The note style used by this song. - * TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format. */ - public var noteSkin:String; + public var noteStyle:String; /** - * The difficulty rating for this song as displayed in Freeplay. - * TODO: Adding this is a non-breaking change to the metadata format. + * The difficulty ratings for this song as displayed in Freeplay. + * Key is a difficulty ID or `default`. */ - // public var rating:Int; + @:default(['default' => 1]) + public var ratings:Map; /** * The album ID for the album to display in Freeplay. - * TODO: Adding this is a non-breaking change to the metadata format. + * If `null`, display no album. */ - // public var album:String; + @:optional + public var album:Null; - public function new() {} + public function new() + { + ratings = new Map(); + } /** * Produces a string representation suitable for debugging. diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index 889fca707..8e0f4577d 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -2,6 +2,7 @@ package funkin.data.song; import funkin.data.song.SongData; import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; +import funkin.data.song.migrator.SongData_v2_1_0.SongMetadata_v2_1_0; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongMetadata; import funkin.play.song.ScriptedSong; @@ -18,9 +19,9 @@ class SongRegistry extends BaseRegistry * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. */ - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0"; + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.0"; - public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x"; + public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0"; @@ -165,6 +166,10 @@ class SongRegistry extends BaseRegistry { return parseEntryMetadata(id, variation); } + else if (VersionUtil.validateVersion(version, "2.1.x")) + { + return parseEntryMetadata_v2_1_0(id, variation); + } else if (VersionUtil.validateVersion(version, "2.0.x")) { return parseEntryMetadata_v2_0_0(id, variation); @@ -182,6 +187,10 @@ class SongRegistry extends BaseRegistry { return parseEntryMetadataRaw(contents, fileName); } + else if (VersionUtil.validateVersion(version, "2.1.x")) + { + return parseEntryMetadataRaw_v2_1_0(contents, fileName); + } else if (VersionUtil.validateVersion(version, "2.0.x")) { return parseEntryMetadataRaw_v2_0_0(contents, fileName); @@ -192,12 +201,12 @@ class SongRegistry extends BaseRegistry } } - function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null + function parseEntryMetadata_v2_1_0(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; - var parser = new json2object.JsonParser(); - switch (loadEntryMetadataFile(id)) + var parser = new json2object.JsonParser(); + switch (loadEntryMetadataFile(id, variation)) { case {fileName: fileName, contents: contents}: parser.fromJson(contents, fileName); @@ -209,6 +218,39 @@ class SongRegistry extends BaseRegistry printErrors(parser.errors, id); return null; } + return cleanMetadata(parser.value.migrate(), variation); + } + + function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null + { + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; + + var parser = new json2object.JsonParser(); + switch (loadEntryMetadataFile(id, variation)) + { + case {fileName: fileName, contents: contents}: + parser.fromJson(contents, fileName); + default: + return null; + } + if (parser.errors.length > 0) + { + printErrors(parser.errors, id); + return null; + } + return cleanMetadata(parser.value.migrate(), variation); + } + + function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null + { + var parser = new json2object.JsonParser(); + parser.fromJson(contents, fileName); + + if (parser.errors.length > 0) + { + printErrors(parser.errors, fileName); + return null; + } return parser.value.migrate(); } diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx new file mode 100644 index 000000000..0c7d2f0b0 --- /dev/null +++ b/source/funkin/data/song/importer/ChartManifestData.hx @@ -0,0 +1,84 @@ +package funkin.data.song.importer; + +/** + * A helper JSON blob found in `.fnfc` files. + */ +class ChartManifestData +{ + /** + * The current semantic version of the chart manifest data. + */ + public static final CHART_MANIFEST_DATA_VERSION:thx.semver.Version = "1.0.0"; + + @:default(funkin.data.song.importer.ChartManifestData.CHART_MANIFEST_DATA_VERSION) + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) + public var version:thx.semver.Version; + + /** + * The internal song ID for this chart. + * The metadata and chart data file names are derived from this. + */ + public var songId:String; + + public function new(songId:String) + { + this.version = CHART_MANIFEST_DATA_VERSION; + this.songId = songId; + } + + public function getMetadataFileName(?variation:String):String + { + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; + + return '$songId-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}'; + } + + public function getChartDataFileName(?variation:String):String + { + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; + + return '$songId-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}'; + } + + public function getInstFileName(?variation:String):String + { + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; + + return 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + } + + public function getVocalsFileName(charId:String, ?variation:String):String + { + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; + + return 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; + } + + /** + * Serialize this ChartManifestData into a JSON string. + * @return The JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } + + public static function deserialize(contents:String):Null + { + var parser = new json2object.JsonParser(); + parser.fromJson(contents, 'manifest.json'); + + if (parser.errors.length > 0) + { + trace('[ChartManifest] Failed to parse chart file manifest'); + + for (error in parser.errors) + DataError.printError(error); + + return null; + } + return parser.value; + } +} diff --git a/source/funkin/data/song/migrator/SongDataMigrator.hx b/source/funkin/data/song/migrator/SongDataMigrator.hx index b5e08c832..2603ab1f8 100644 --- a/source/funkin/data/song/migrator/SongDataMigrator.hx +++ b/source/funkin/data/song/migrator/SongDataMigrator.hx @@ -7,6 +7,8 @@ import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0; import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0; +using funkin.data.song.migrator.SongDataMigrator; // Does this even work lol? + /** * This class contains functions to migrate older data formats to the current one. * @@ -15,6 +17,48 @@ import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0; */ class SongDataMigrator { + public static overload extern inline function migrate(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata + { + return migrate_SongMetadata_v2_1_0(input); + } + + public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata + { + var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation); + result.version = SongRegistry.SONG_METADATA_VERSION; + result.timeFormat = input.timeFormat; + result.divisions = input.divisions; + result.timeChanges = input.timeChanges; + result.looped = input.looped; + result.playData = input.playData.migrate(); + result.generatedBy = input.generatedBy; + + return result; + } + + public static overload extern inline function migrate(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData + { + return migrate_SongPlayData_v2_1_0(input); + } + + public static function migrate_SongPlayData_v2_1_0(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData + { + var result:SongPlayData = new SongPlayData(); + result.songVariations = input.songVariations; + result.difficulties = input.difficulties; + result.stage = input.stage; + result.characters = input.characters; + + // Renamed + result.noteStyle = input.noteSkin; + + // Added + result.ratings = ['default' => 1]; + result.album = null; + + return result; + } + public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata { return migrate_SongMetadata_v2_0_0(input); @@ -23,12 +67,12 @@ class SongDataMigrator public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata { var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation); - result.version = input.version; + result.version = SongRegistry.SONG_METADATA_VERSION; result.timeFormat = input.timeFormat; result.divisions = input.divisions; result.timeChanges = input.timeChanges; result.looped = input.looped; - result.playData = migrate_SongPlayData_v2_0_0(input.playData); + result.playData = input.playData.migrate(); result.generatedBy = input.generatedBy; return result; @@ -45,7 +89,13 @@ class SongDataMigrator result.songVariations = input.songVariations; result.difficulties = input.difficulties; result.stage = input.stage; - result.noteSkin = input.noteSkin; + + // Added + result.ratings = ['default' => 1]; + result.album = null; + + // Renamed + result.noteStyle = input.noteSkin; // Fetch the first playable character and migrate it. var firstCharKey:Null = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0]; diff --git a/source/funkin/data/song/migrator/SongData_v2_0_0.hx b/source/funkin/data/song/migrator/SongData_v2_0_0.hx index eeeed2f2b..62e3faf4c 100644 --- a/source/funkin/data/song/migrator/SongData_v2_0_0.hx +++ b/source/funkin/data/song/migrator/SongData_v2_0_0.hx @@ -42,6 +42,7 @@ class SongMetadata_v2_0_0 @:default(false) public var looped:Bool; + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; public var timeFormat:SongData.SongTimeFormat; @@ -70,6 +71,13 @@ class SongPlayData_v2_0_0 */ public var playableChars:Map; + /** + * In metadata version `v2.2.0`, this was renamed to `noteStyle`. + */ + public var noteSkin:String; + + // In 2.2.0, the ratings value was added. + // In 2.2.0, the album value was added. // ========== // UNMODIFIED VALUES // ========== @@ -77,7 +85,6 @@ class SongPlayData_v2_0_0 public var difficulties:Array; public var stage:String; - public var noteSkin:String; public function new() {} diff --git a/source/funkin/data/song/migrator/SongData_v2_1_0.hx b/source/funkin/data/song/migrator/SongData_v2_1_0.hx new file mode 100644 index 000000000..57e4102d9 --- /dev/null +++ b/source/funkin/data/song/migrator/SongData_v2_1_0.hx @@ -0,0 +1,108 @@ +package funkin.data.song.migrator; + +import funkin.data.song.SongData; +import funkin.data.song.SongRegistry; +import thx.semver.Version; + +@:nullSafety +class SongMetadata_v2_1_0 +{ + // ========== + // MODIFIED VALUES + // =========== + + /** + * In metadata `v2.2.0`, `SongPlayData` was refactored. + */ + public var playData:SongPlayData_v2_1_0; + + // ========== + // UNMODIFIED VALUES + // ========== + @:jcustomparse(funkin.data.DataParse.semverVersion) + @:jcustomwrite(funkin.data.DataWrite.semverVersion) + public var version:Version; + + @:default("Unknown") + public var songName:String; + + @:default("Unknown") + public var artist:String; + + @:optional + @:default(96) + public var divisions:Null; // Optional field + + @:optional + @:default(false) + public var looped:Bool; + + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) + public var generatedBy:String; + + public var timeFormat:SongData.SongTimeFormat; + + public var timeChanges:Array; + + /** + * Defaults to `Constants.DEFAULT_VARIATION`. Populated later. + */ + @:jignored + public var variation:String; + + public function new(songName:String, artist:String, ?variation:String) + { + this.version = SongRegistry.SONG_METADATA_VERSION; + this.songName = songName; + this.artist = artist; + this.timeFormat = 'ms'; + this.divisions = null; + this.timeChanges = [new SongTimeChange(0, 100)]; + this.looped = false; + this.playData = new SongPlayData_v2_1_0(); + this.playData.songVariations = []; + this.playData.difficulties = []; + this.playData.characters = new SongCharacterData('bf', 'gf', 'dad'); + this.playData.stage = 'mainStage'; + this.playData.noteSkin = 'funkin'; + this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; + // Variation ID. + this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation; + } + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongMetadata[LEGACY:v2.1.0](${this.songName} by ${this.artist}, variation ${this.variation})'; + } +} + +class SongPlayData_v2_1_0 +{ + /** + * In `v2.2.0`, this value was renamed to `noteStyle`. + */ + public var noteSkin:String; + + // In 2.2.0, the ratings value was added. + // In 2.2.0, the album value was added. + // ========== + // UNMODIFIED VALUES + // ========== + public var songVariations:Array; + public var difficulties:Array; + public var characters:SongData.SongCharacterData; + public var stage:String; + + public function new() {} + + /** + * Produces a string representation suitable for debugging. + */ + public function toString():String + { + return 'SongPlayData[LEGACY:v2.1.0](${this.songVariations}, ${this.difficulties})'; + } +} diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 1d3480efe..9a126e509 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1471,7 +1471,7 @@ class PlayState extends MusicBeatSubState { case 'school': 'pixel'; case 'schoolEvil': 'pixel'; - default: 'funkin'; + default: Constants.DEFAULT_NOTE_STYLE; } var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); @@ -2389,8 +2389,8 @@ class PlayState extends MusicBeatSubState #if sys // spitter for ravy, teehee!! - - var output = SerializerUtil.toJSON(inputSpitter); + var writer = new json2object.JsonWriter>(); + var output = writer.write(inputSpitter, ' '); sys.io.File.saveContent("./scores.json", output); #end diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 60b8b9864..90920a710 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -96,11 +96,13 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + function fetchVariationMetadata(id:String, vari:String):Null { - 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; + var version:Null = SongRegistry.instance.fetchEntryMetadataVersion(id, vari); + if (version == null) return null; + var meta:Null = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version); + return meta; } } diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index d9875e456..89b85d14c 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -176,13 +176,13 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass continue; } - if (Std.isOfType(dataProp.scale, Array)) + switch (dataProp.scale) { - propSprite.scale.set(dataProp.scale[0], dataProp.scale[1]); - } - else - { - propSprite.scale.set(dataProp.scale); + case Left(value): + propSprite.scale.set(value); + + case Right(values): + propSprite.scale.set(values[0], values[1]); } propSprite.updateHitbox(); @@ -194,8 +194,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass // If pixel, disable antialiasing. propSprite.antialiasing = !dataProp.isPixel; - propSprite.scrollFactor.x = dataProp.scroll[0]; - propSprite.scrollFactor.y = dataProp.scroll[1]; + switch (dataProp.scroll) + { + case Left(value): + propSprite.scrollFactor.x = value; + propSprite.scrollFactor.y = value; + case Right(values): + propSprite.scrollFactor.x = values[0]; + propSprite.scrollFactor.y = values[1]; + } propSprite.zIndex = dataProp.zIndex; diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx index c14e05aaf..29ca03b84 100644 --- a/source/funkin/play/stage/StageData.hx +++ b/source/funkin/play/stage/StageData.hx @@ -1,7 +1,6 @@ package funkin.play.stage; import funkin.data.animation.AnimationData; -import flixel.util.typeLimit.OneOfTwo; import funkin.play.stage.ScriptedStage; import funkin.play.stage.Stage; import funkin.util.VersionUtil; @@ -157,15 +156,26 @@ class StageDataParser return rawJson; } - static function migrateStageData(rawJson:String, stageId:String) + static function migrateStageData(rawJson:String, stageId:String):Null { // If you update the stage data format in a breaking way, // handle migration here by checking the `version` value. try { - var stageData:StageData = cast Json.parse(rawJson); - return stageData; + var parser = new json2object.JsonParser(); + parser.fromJson(rawJson, '$stageId.json'); + + if (parser.errors.length > 0) + { + trace('[STAGE] Failed to parse stage data'); + + for (error in parser.errors) + funkin.data.DataError.printError(error); + + return null; + } + return parser.value; } catch (e) { @@ -269,24 +279,29 @@ class StageDataParser inputProp.danceEvery = DEFAULT_DANCEEVERY; } - if (inputProp.scale == null) - { - inputProp.scale = DEFAULT_SCALE; - } - if (inputProp.animType == null) { inputProp.animType = DEFAULT_ANIMTYPE; } - if (Std.isOfType(inputProp.scale, Float)) + switch (inputProp.scale) { - inputProp.scale = [inputProp.scale, inputProp.scale]; + case null: + inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]); + case Left(value): + inputProp.scale = Right([value, value]); + case Right(_): + // Do nothing } - if (inputProp.scroll == null) + switch (inputProp.scroll) { - inputProp.scroll = DEFAULT_SCROLL; + case null: + inputProp.scroll = Right(DEFAULT_SCROLL); + case Left(value): + inputProp.scroll = Right([value, value]); + case Right(_): + // Do nothing } if (inputProp.alpha == null) @@ -294,11 +309,6 @@ class StageDataParser inputProp.alpha = DEFAULT_ALPHA; } - if (Std.isOfType(inputProp.scroll, Float)) - { - inputProp.scroll = [inputProp.scroll, inputProp.scroll]; - } - if (inputProp.animations == null) { inputProp.animations = []; @@ -392,23 +402,39 @@ class StageDataParser } } -typedef StageData = +class StageData { /** * The sematic version number of the stage data JSON format. * Supports fancy comparisons like NPM does it's neat. */ - var version:String; + public var version:String; - var name:String; - var cameraZoom:Null; - var props:Array; - var characters: - { - bf:StageDataCharacter, - dad:StageDataCharacter, - gf:StageDataCharacter, - }; + public var name:String; + public var cameraZoom:Null; + public var props:Array; + public var characters:StageDataCharacters; + + public function new() + { + this.version = StageDataParser.STAGE_DATA_VERSION; + } + + /** + * Convert this StageData into a JSON string. + */ + public function serialize(pretty:Bool = true):String + { + var writer = new json2object.JsonWriter(); + return writer.write(this, pretty ? ' ' : null); + } +} + +typedef StageDataCharacters = +{ + var bf:StageDataCharacter; + var dad:StageDataCharacter; + var gf:StageDataCharacter; }; typedef StageDataProp = @@ -417,6 +443,7 @@ typedef StageDataProp = * The name of the prop for later lookup by scripts. * Optional; if unspecified, the prop can't be referenced by scripts. */ + @:optional var name:String; /** @@ -435,27 +462,35 @@ typedef StageDataProp = * This is just like CSS, it isn't hard. * @default 0 */ - var zIndex:Null; + @:optional + @:default(0) + var zIndex:Int; /** * If set to true, anti-aliasing will be forcibly disabled on the sprite. * This prevents blurry images on pixel-art levels. * @default false */ - var isPixel:Null; + @:optional + @:default(false) + var isPixel:Bool; /** * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. - * @default 1 */ - var scale:OneOfTwo>; + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) + @:optional + var scale:haxe.ds.Either>; /** * The alpha of the prop, as a float. * @default 1.0 */ - var alpha:Null; + @:optional + @:default(1.0) + var alpha:Float; /** * If not zero, this prop will play an animation every X beats of the song. @@ -464,7 +499,9 @@ typedef StageDataProp = * * @default 0 */ - var danceEvery:Null; + @:default(0) + @:optional + var danceEvery:Int; /** * How much the prop scrolls relative to the camera. Used to create a parallax effect. @@ -474,25 +511,32 @@ typedef StageDataProp = * [0, 0] means the prop is not moved. * @default [0, 0] */ - var scroll:OneOfTwo>; + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) + @:optional + var scroll:haxe.ds.Either>; /** * An optional array of animations which the prop can play. * @default Prop has no animations. */ + @:optional var animations:Array; /** * If animations are used, this is the name of the animation to play first. * @default Don't play an animation. */ - var startingAnimation:String; + @:optional + var startingAnimation:Null; /** * The animation type to use. * Options: "sparrow", "packer" * @default "sparrow" */ + @:default("sparrow") + @:optional var animType:String; }; @@ -503,16 +547,22 @@ typedef StageDataCharacter = * Again, just like CSS. * @default 0 */ - ?zIndex:Int, + @:optional + @:default(0) + var zIndex:Int; /** * The position to render the character at. */ - position:Array, + @:optional + @:default([0, 0]) + var position:Array; /** * The camera offsets to apply when focusing on the character on this stage. * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF */ - cameraOffsets:Array, + @:optional + @:default([0, 0]) + var cameraOffsets:Array; }; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index e7b7c7583..d5b23cfd9 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -13,8 +13,7 @@ class SaveDataMigrator */ public static function migrate(inputData:Dynamic):Save { - // This deserializes directly into a `Version` object, not a `String`. - var version:Null = inputData?.version ?? null; + var version:Null = VersionUtil.parseVersion(inputData?.version ?? null); if (version == null) { @@ -24,7 +23,7 @@ class SaveDataMigrator } else { - if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE)) + if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) { // Simply cast the structured data. var save:Save = inputData; diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx index b5a6f36be..6f390e604 100644 --- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx @@ -265,7 +265,7 @@ class ChartEditorAudioHandler { var data:Null = state.audioVocalTrackData.get(key); if (data == null) continue; - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data)); + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data)); } return zipEntries; diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 1014e67c2..98ca810d3 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -182,8 +182,8 @@ class SelectItemsCommand implements ChartEditorCommand state.currentEventSelection.push(event); } - state.noteDisplayDirty = true; - state.notePreviewDirty = true; + // state.noteDisplayDirty = true; + // state.notePreviewDirty = true; } public function undo(state:ChartEditorState):Void @@ -191,8 +191,8 @@ class SelectItemsCommand implements ChartEditorCommand state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); - state.noteDisplayDirty = true; - state.notePreviewDirty = true; + // state.noteDisplayDirty = true; + // state.notePreviewDirty = true; } public function toString():String @@ -452,8 +452,8 @@ class DeselectItemsCommand implements ChartEditorCommand state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); - state.noteDisplayDirty = true; - state.notePreviewDirty = true; + // state.noteDisplayDirty = true; + // state.notePreviewDirty = true; } public function undo(state:ChartEditorState):Void @@ -468,8 +468,8 @@ class DeselectItemsCommand implements ChartEditorCommand state.currentEventSelection.push(event); } - state.noteDisplayDirty = true; - state.notePreviewDirty = true; + // state.noteDisplayDirty = true; + // state.notePreviewDirty = true; } public function toString():String diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index c26f6c805..dd5ddb06c 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -51,12 +51,13 @@ class ChartEditorDialogHandler { static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about'); static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome'); + static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); - static final CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart'); - static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry'); + static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); + static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation'); @@ -83,6 +84,11 @@ class ChartEditorDialogHandler var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); if (dialog == null) throw 'Could not locate Welcome dialog'; + dialog.onDialogClosed = function(_event) { + // Called when the Welcome dialog is closed while it is closable. + state.stopWelcomeMusic(); + } + // Create New Song "Easy/Normal/Hard" var linkCreateBasic:Null = dialog.findComponent('splashCreateFromSongBasic', Link); if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; @@ -129,7 +135,7 @@ class ChartEditorDialogHandler state.stopWelcomeMusic(); // Open the "Open Chart" dialog - openBrowseWizard(state, false); + openBrowseFNFC(state, false); } var splashTemplateContainer:Null = dialog.findComponent('splashTemplateContainer', VBox); @@ -168,6 +174,126 @@ class ChartEditorDialogHandler return dialog; } + public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null + { + var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable); + if (dialog == null) throw 'Could not locate Upload Chart dialog'; + + dialog.onDialogClosed = function(_event) { + if (_event.button == DialogButton.APPLY) + { + // Simply let the dialog close. + } + else + { + // User cancelled the wizard! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + + var buttonCancel:Null