diff --git a/Project.xml b/Project.xml
index a1772a9ef..46ba7f155 100644
--- a/Project.xml
+++ b/Project.xml
@@ -4,10 +4,7 @@
-
-
-
+
diff --git a/hmm.json b/hmm.json
index 22dd6e42f..778b85604 100644
--- a/hmm.json
+++ b/hmm.json
@@ -56,7 +56,7 @@
"name": "haxeui-flixel",
"type": "git",
"dir": null,
- "ref": "9bd0b9e0fea40b8e06a89aac4949512d95064609",
+ "ref": "95c7d66e779626eabd6f48a1cd7aa7f9a503a7f3",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
diff --git a/source/Main.hx b/source/Main.hx
index 709ae2cf7..5fbb6747b 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 d74b5f639..13bcd306e 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -1,5 +1,6 @@
package funkin;
+import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.transition.LoadingState;
import flixel.FlxState;
import flixel.addons.transition.FlxTransitionableState;
@@ -27,6 +28,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;
import funkin.ui.transition.LoadingState;
#if discord_rpc
import Discord.DiscordClient;
@@ -250,8 +253,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 83b336606..e79e1a3f4 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 94c58f7fe..cce424b06 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1477,7 +1477,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();
@@ -2397,8 +2397,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 9562ef2ca..b48eda224 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 d7ba38e2a..c8cb8ce66 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -177,13 +177,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();
@@ -195,8 +195,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/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index d0dd0f8c3..382bab592 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1040,17 +1040,17 @@ class ChartEditorState extends HaxeUIState
function get_currentSongNoteStyle():String
{
- if (currentSongMetadata.playData.noteSkin == null)
+ if (currentSongMetadata.playData.noteStyle == null)
{
// Initialize to the default value if not set.
- currentSongMetadata.playData.noteSkin = 'funkin';
+ currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
}
- return currentSongMetadata.playData.noteSkin;
+ return currentSongMetadata.playData.noteStyle;
}
function set_currentSongNoteStyle(value:String):String
{
- return currentSongMetadata.playData.noteSkin = value;
+ return currentSongMetadata.playData.noteStyle = value;
}
var currentSongStage(get, set):String;
@@ -1320,10 +1320,22 @@ class ChartEditorState extends HaxeUIState
*/
// ==============================
- public function new()
+ /**
+ * The params which were passed in when the Chart Editor was initialized.
+ */
+ var params:Null;
+
+ /**
+ * The current file path which the chart editor is working with.
+ */
+ public var currentWorkingFilePath:Null;
+
+ public function new(?params:ChartEditorParams)
{
// Load the HaxeUI XML file.
super(CHART_EDITOR_LAYOUT);
+
+ this.params = params;
}
public override function dispatchEvent(event:ScriptEvent):Void
@@ -1404,7 +1416,33 @@ class ChartEditorState extends HaxeUIState
refresh();
- this.openWelcomeDialog(false);
+ if (params != null && params.fnfcTargetPath != null)
+ {
+ // Chart editor was opened from the command line. Open the FNFC file now!
+ if (ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath))
+ {
+ // Don't open the welcome dialog!
+
+ #if !mac
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Success',
+ body: 'Loaded chart (${params.fnfcTargetPath})',
+ type: NotificationType.Success,
+ expiryMs: Constants.NOTIFICATION_DISMISS_TIME
+ });
+ #end
+ }
+ else
+ {
+ // Song failed to load, open the Welcome dialog so we aren't in a broken state.
+ ChartEditorDialogHandler.openWelcomeDialog(this, false);
+ }
+ }
+ else
+ {
+ ChartEditorDialogHandler.openWelcomeDialog(this, false);
+ }
}
override function destroy():Void
@@ -1766,11 +1804,15 @@ class ChartEditorState extends HaxeUIState
noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
});
+ addUIRightClickListener('playbarNoteSnap', function(_) {
+ noteSnapQuantIndex--;
+ if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
+ });
// Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true));
- addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseWizard(true));
+ addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseFNFC(true));
addUIClickListener('menubarItemSaveChartAs', _ -> this.exportAllSongData());
addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true));
addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true));
@@ -1912,7 +1954,7 @@ class ChartEditorState extends HaxeUIState
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
var volume:Float = (event?.value ?? 0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
- vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
+ vocalsVolumeLabel.text = 'Voices - ${Std.int(event.value)}%';
});
}
@@ -3780,7 +3822,7 @@ class ChartEditorState extends HaxeUIState
// CTRL + O = Open Chart
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O)
{
- this.openBrowseWizard(true);
+ this.openBrowseFNFC(true);
}
// CTRL + SHIFT + S = Save As
@@ -3796,6 +3838,16 @@ class ChartEditorState extends HaxeUIState
}
}
+ @:nullSafety(Off)
+ function quitChartEditor():Void
+ {
+ autoSave();
+ stopWelcomeMusic();
+ // TODO: PR Flixel to make onComplete nullable.
+ if (audioInstTrack != null) audioInstTrack.onComplete = null;
+ FlxG.switchState(new MainMenuState());
+ }
+
/**
* Handle keybinds for edit menu items.
*/
@@ -4089,16 +4141,6 @@ class ChartEditorState extends HaxeUIState
#end
}
- /**
- * Called when the user presses the Quit button.
- */
- function quitChartEditor():Void
- {
- autoSave();
- stopWelcomeMusic();
- FlxG.switchState(new MainMenuState());
- }
-
/**
* Called when the window is closed while we are in the chart editor.
* @param exitCode The exit code of the window.
@@ -4572,7 +4614,7 @@ class ChartEditorState extends HaxeUIState
if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage;
var inputNoteStyle:Null = toolbox.findComponent('inputNoteStyle', DropDown);
- if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteSkin;
+ if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteStyle;
var inputBPM:Null = toolbox.findComponent('inputBPM', NumberStepper);
if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
@@ -4716,6 +4758,14 @@ enum ChartEditorLiveInputStyle
WASD;
}
+typedef ChartEditorParams =
+{
+ /**
+ * If non-null, load this song immediately instead of the welcome screen.
+ */
+ var ?fnfcTargetPath:String;
+};
+
/**
* Available themes for the chart editor state.
*/
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 072004a43..5ea125eb4 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -258,7 +258,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/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index bb066a923..f5cbccff6 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -52,12 +52,13 @@ class ChartEditorDialogHandler
// Paths to HaxeUI layout files for each dialog.
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');
@@ -84,6 +85,11 @@ class ChartEditorDialogHandler
var dialog:Null