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