Mirror for Friday Night Funkin

FNFC file rework (includes command line quicklaunch)

+1157 -182
+1 -4
Project.xml
··· 4 4 <app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.0" company="ninjamuffin99" /> 5 5 <!--Switch Export with Unique ApplicationID and Icon--> 6 6 <set name="APP_ID" value="0x0100f6c013bbc000" /> 7 - <!--The flixel preloader is not accurate in Chrome. You can use it regularly if you embed the swf into a html file 8 - or you can set the actual size of your file manually at "FlxPreloaderBase-onUpdate-bytesTotal"--> 9 - <!-- <app preloader="Preloader" resizable="true" /> --> 10 - <app preloader="Preloader" /> 7 + <app preloader="funkin.Preloader" /> 11 8 <!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2--> 12 9 <set name="SWF_VERSION" value="11.8" /> 13 10 <!-- ____________________________ Window Settings ___________________________ -->
+1
source/Main.hx
··· 11 11 import openfl.events.Event; 12 12 import openfl.Lib; 13 13 import openfl.media.Video; 14 + import funkin.util.CLIUtil; 14 15 import openfl.net.NetStream; 15 16 16 17 class Main extends Sprite
+4 -1
source/Preloader.hx source/funkin/Preloader.hx
··· 1 - package; 1 + package funkin; 2 2 3 3 import flash.Lib; 4 4 import flash.display.Bitmap; ··· 7 7 import flash.display.Sprite; 8 8 import flixel.system.FlxBasePreloader; 9 9 import openfl.display.Sprite; 10 + import funkin.util.CLIUtil; 10 11 11 12 @:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {} 12 13 ··· 15 16 public function new(MinDisplayTime:Float = 0, ?AllowedURLs:Array<String>) 16 17 { 17 18 super(MinDisplayTime, AllowedURLs); 19 + 20 + CLIUtil.resetWorkingDir(); // Bug fix for drag-and-drop. 18 21 } 19 22 20 23 var logo:Sprite;
+18 -2
source/funkin/InitState.hx
··· 1 1 package funkin; 2 2 3 + import funkin.ui.debug.charting.ChartEditorState; 3 4 import flixel.FlxState; 4 5 import flixel.addons.transition.FlxTransitionableState; 5 6 import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond; ··· 26 27 import funkin.play.character.CharacterData.CharacterDataParser; 27 28 import funkin.modding.module.ModuleHandler; 28 29 import funkin.ui.title.TitleState; 30 + import funkin.util.CLIUtil; 31 + import funkin.util.CLIUtil.CLIParams; 29 32 #if discord_rpc 30 33 import Discord.DiscordClient; 31 34 #end ··· 247 250 */ 248 251 function startGameNormally():Void 249 252 { 250 - FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); 251 - FlxG.switchState(new TitleState()); 253 + var params:CLIParams = CLIUtil.processArgs(); 254 + trace('Command line args: ${params}'); 255 + 256 + if (params.chart.shouldLoadChart) 257 + { 258 + FlxG.switchState(new ChartEditorState( 259 + { 260 + fnfcTargetPath: params.chart.chartPath, 261 + })); 262 + } 263 + else 264 + { 265 + FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu')); 266 + FlxG.switchState(new TitleState()); 267 + } 252 268 } 253 269 254 270 /**
+16
source/funkin/data/DataParse.hx
··· 105 105 } 106 106 107 107 /** 108 + * Parser which outputs a `Either<Float, Array<Float>>`. 109 + */ 110 + public static function eitherFloatOrFloats(json:Json, name:String):Null<Either<Float, Array<Float>>> 111 + { 112 + switch (json.value) 113 + { 114 + case JNumber(f): 115 + return Either.Left(Std.parseFloat(f)); 116 + case JArray(fields): 117 + return Either.Right(fields.map((field) -> cast Tools.getValue(field))); 118 + default: 119 + throw 'Expected property $name to be one or multiple floats, but it was ${json.value}.'; 120 + } 121 + } 122 + 123 + /** 108 124 * Parser which outputs a `Either<Float, LegacyScrollSpeeds>`. 109 125 * Used by the FNF legacy JSON importer. 110 126 */
+22 -2
source/funkin/data/DataWrite.hx
··· 3 3 import funkin.util.SerializerUtil; 4 4 import thx.semver.Version; 5 5 import thx.semver.VersionRule; 6 + import haxe.ds.Either; 6 7 7 8 /** 8 9 * `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON. 9 10 * 10 11 * Functions must be of the signature `(T) -> String`, where `T` is the type of the property. 12 + * 13 + * NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you! 11 14 */ 12 15 class DataWrite 13 16 { ··· 23 26 } 24 27 25 28 /** 29 + * 26 30 * `@:jcustomwrite(funkin.data.DataWrite.semverVersion)` 27 31 */ 28 32 public static function semverVersion(value:Version):String 29 33 { 30 - return value.toString(); 34 + return '"${value.toString()}"'; 31 35 } 32 36 33 37 /** ··· 35 39 */ 36 40 public static function semverVersionRule(value:VersionRule):String 37 41 { 38 - return value.toString(); 42 + return '"${value.toString()}"'; 43 + } 44 + 45 + /** 46 + * `@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)` 47 + */ 48 + public static function eitherFloatOrFloats(value:Null<Either<Float, Array<Float>>>):String 49 + { 50 + switch (value) 51 + { 52 + case null: 53 + return '${1.0}'; 54 + case Left(inner): 55 + return '$inner'; 56 + case Right(inner): 57 + return dynamicValue(inner); 58 + } 39 59 } 40 60 }
+3
source/funkin/data/animation/AnimationData.hx
··· 59 59 * The prefix for the frames of the animation as defined by the XML file. 60 60 * This will may or may not differ from the `name` of the animation, 61 61 * depending on how your animator organized their FLA or whatever. 62 + * 63 + * NOTE: For Sparrow animations, this is not optional, but for Packer animations it is. 62 64 */ 65 + @:optional 63 66 var prefix:String; 64 67 65 68 /**
+1 -3
source/funkin/data/notestyle/NoteStyleRegistry.hx
··· 15 15 16 16 public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x"; 17 17 18 - public static final DEFAULT_NOTE_STYLE_ID:String = "funkin"; 19 - 20 18 public static final instance:NoteStyleRegistry = new NoteStyleRegistry(); 21 19 22 20 public function new() ··· 26 24 27 25 public function fetchDefault():NoteStyle 28 26 { 29 - return fetchEntry(DEFAULT_NOTE_STYLE_ID); 27 + return fetchEntry(Constants.DEFAULT_NOTE_STYLE); 30 28 } 31 29 32 30 /**
+19 -13
source/funkin/data/song/SongData.hx
··· 1 1 package funkin.data.song; 2 2 3 - import flixel.util.typeLimit.OneOfTwo; 4 3 import funkin.data.song.SongRegistry; 5 4 import thx.semver.Version; 6 5 6 + /** 7 + * Data containing information about a song. 8 + * 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. 9 + * Data which is only necessary in-game should be stored in the SongChartData. 10 + */ 7 11 @:nullSafety 8 12 class SongMetadata 9 13 { ··· 35 39 */ 36 40 public var playData:SongPlayData; 37 41 38 - // @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) 42 + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) 39 43 public var generatedBy:String; 40 44 41 - // @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS) 42 45 public var timeFormat:SongTimeFormat; 43 46 44 - // @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_SONGTIMECHANGES) 45 47 public var timeChanges:Array<SongTimeChange>; 46 48 47 49 /** ··· 64 66 this.playData.difficulties = []; 65 67 this.playData.characters = new SongCharacterData('bf', 'gf', 'dad'); 66 68 this.playData.stage = 'mainStage'; 67 - this.playData.noteSkin = 'funkin'; 69 + this.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE; 68 70 this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; 69 71 // Variation ID. 70 72 this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation; ··· 298 300 299 301 /** 300 302 * The note style used by this song. 301 - * TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format. 302 303 */ 303 - public var noteSkin:String; 304 + public var noteStyle:String; 304 305 305 306 /** 306 - * The difficulty rating for this song as displayed in Freeplay. 307 - * TODO: Adding this is a non-breaking change to the metadata format. 307 + * The difficulty ratings for this song as displayed in Freeplay. 308 + * Key is a difficulty ID or `default`. 308 309 */ 309 - // public var rating:Int; 310 + @:default(['default' => 1]) 311 + public var ratings:Map<String, Int>; 310 312 311 313 /** 312 314 * The album ID for the album to display in Freeplay. 313 - * TODO: Adding this is a non-breaking change to the metadata format. 315 + * If `null`, display no album. 314 316 */ 315 - // public var album:String; 317 + @:optional 318 + public var album:Null<String>; 316 319 317 - public function new() {} 320 + public function new() 321 + { 322 + ratings = new Map<String, Int>(); 323 + } 318 324 319 325 /** 320 326 * Produces a string representation suitable for debugging.
+45 -3
source/funkin/data/song/SongRegistry.hx
··· 2 2 3 3 import funkin.data.song.SongData; 4 4 import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0; 5 + import funkin.data.song.migrator.SongData_v2_1_0.SongMetadata_v2_1_0; 5 6 import funkin.data.song.SongData.SongChartData; 6 7 import funkin.data.song.SongData.SongMetadata; 7 8 import funkin.play.song.ScriptedSong; ··· 18 19 * Handle breaking changes by incrementing this value 19 20 * and adding migration to the `migrateStageData()` function. 20 21 */ 21 - public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0"; 22 + public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.0"; 22 23 23 - public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x"; 24 + public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x"; 24 25 25 26 public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0"; 26 27 ··· 165 166 { 166 167 return parseEntryMetadata(id, variation); 167 168 } 169 + else if (VersionUtil.validateVersion(version, "2.1.x")) 170 + { 171 + return parseEntryMetadata_v2_1_0(id, variation); 172 + } 168 173 else if (VersionUtil.validateVersion(version, "2.0.x")) 169 174 { 170 175 return parseEntryMetadata_v2_0_0(id, variation); ··· 182 187 { 183 188 return parseEntryMetadataRaw(contents, fileName); 184 189 } 190 + else if (VersionUtil.validateVersion(version, "2.1.x")) 191 + { 192 + return parseEntryMetadataRaw_v2_1_0(contents, fileName); 193 + } 185 194 else if (VersionUtil.validateVersion(version, "2.0.x")) 186 195 { 187 196 return parseEntryMetadataRaw_v2_0_0(contents, fileName); ··· 192 201 } 193 202 } 194 203 204 + function parseEntryMetadata_v2_1_0(id:String, ?variation:String):Null<SongMetadata> 205 + { 206 + variation = variation == null ? Constants.DEFAULT_VARIATION : variation; 207 + 208 + var parser = new json2object.JsonParser<SongMetadata_v2_1_0>(); 209 + switch (loadEntryMetadataFile(id, variation)) 210 + { 211 + case {fileName: fileName, contents: contents}: 212 + parser.fromJson(contents, fileName); 213 + default: 214 + return null; 215 + } 216 + if (parser.errors.length > 0) 217 + { 218 + printErrors(parser.errors, id); 219 + return null; 220 + } 221 + return cleanMetadata(parser.value.migrate(), variation); 222 + } 223 + 195 224 function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata> 196 225 { 197 226 variation = variation == null ? Constants.DEFAULT_VARIATION : variation; 198 227 199 228 var parser = new json2object.JsonParser<SongMetadata_v2_0_0>(); 200 - switch (loadEntryMetadataFile(id)) 229 + switch (loadEntryMetadataFile(id, variation)) 201 230 { 202 231 case {fileName: fileName, contents: contents}: 203 232 parser.fromJson(contents, fileName); ··· 207 236 if (parser.errors.length > 0) 208 237 { 209 238 printErrors(parser.errors, id); 239 + return null; 240 + } 241 + return cleanMetadata(parser.value.migrate(), variation); 242 + } 243 + 244 + function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata> 245 + { 246 + var parser = new json2object.JsonParser<SongMetadata_v2_1_0>(); 247 + parser.fromJson(contents, fileName); 248 + 249 + if (parser.errors.length > 0) 250 + { 251 + printErrors(parser.errors, fileName); 210 252 return null; 211 253 } 212 254 return parser.value.migrate();
+84
source/funkin/data/song/importer/ChartManifestData.hx
··· 1 + package funkin.data.song.importer; 2 + 3 + /** 4 + * A helper JSON blob found in `.fnfc` files. 5 + */ 6 + class ChartManifestData 7 + { 8 + /** 9 + * The current semantic version of the chart manifest data. 10 + */ 11 + public static final CHART_MANIFEST_DATA_VERSION:thx.semver.Version = "1.0.0"; 12 + 13 + @:default(funkin.data.song.importer.ChartManifestData.CHART_MANIFEST_DATA_VERSION) 14 + @:jcustomparse(funkin.data.DataParse.semverVersion) 15 + @:jcustomwrite(funkin.data.DataWrite.semverVersion) 16 + public var version:thx.semver.Version; 17 + 18 + /** 19 + * The internal song ID for this chart. 20 + * The metadata and chart data file names are derived from this. 21 + */ 22 + public var songId:String; 23 + 24 + public function new(songId:String) 25 + { 26 + this.version = CHART_MANIFEST_DATA_VERSION; 27 + this.songId = songId; 28 + } 29 + 30 + public function getMetadataFileName(?variation:String):String 31 + { 32 + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; 33 + 34 + return '$songId-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}'; 35 + } 36 + 37 + public function getChartDataFileName(?variation:String):String 38 + { 39 + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; 40 + 41 + return '$songId-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_DATA}'; 42 + } 43 + 44 + public function getInstFileName(?variation:String):String 45 + { 46 + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; 47 + 48 + return 'Inst${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; 49 + } 50 + 51 + public function getVocalsFileName(charId:String, ?variation:String):String 52 + { 53 + if (variation == null || variation == '') variation = Constants.DEFAULT_VARIATION; 54 + 55 + return 'Voices-$charId${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.${Constants.EXT_SOUND}'; 56 + } 57 + 58 + /** 59 + * Serialize this ChartManifestData into a JSON string. 60 + * @return The JSON string. 61 + */ 62 + public function serialize(pretty:Bool = true):String 63 + { 64 + var writer = new json2object.JsonWriter<ChartManifestData>(); 65 + return writer.write(this, pretty ? ' ' : null); 66 + } 67 + 68 + public static function deserialize(contents:String):Null<ChartManifestData> 69 + { 70 + var parser = new json2object.JsonParser<ChartManifestData>(); 71 + parser.fromJson(contents, 'manifest.json'); 72 + 73 + if (parser.errors.length > 0) 74 + { 75 + trace('[ChartManifest] Failed to parse chart file manifest'); 76 + 77 + for (error in parser.errors) 78 + DataError.printError(error); 79 + 80 + return null; 81 + } 82 + return parser.value; 83 + } 84 + }
+53 -3
source/funkin/data/song/migrator/SongDataMigrator.hx
··· 7 7 import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0; 8 8 import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0; 9 9 10 + using funkin.data.song.migrator.SongDataMigrator; // Does this even work lol? 11 + 10 12 /** 11 13 * This class contains functions to migrate older data formats to the current one. 12 14 * ··· 15 17 */ 16 18 class SongDataMigrator 17 19 { 20 + public static overload extern inline function migrate(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata 21 + { 22 + return migrate_SongMetadata_v2_1_0(input); 23 + } 24 + 25 + public static function migrate_SongMetadata_v2_1_0(input:SongData_v2_1_0.SongMetadata_v2_1_0):SongMetadata 26 + { 27 + var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation); 28 + result.version = SongRegistry.SONG_METADATA_VERSION; 29 + result.timeFormat = input.timeFormat; 30 + result.divisions = input.divisions; 31 + result.timeChanges = input.timeChanges; 32 + result.looped = input.looped; 33 + result.playData = input.playData.migrate(); 34 + result.generatedBy = input.generatedBy; 35 + 36 + return result; 37 + } 38 + 39 + public static overload extern inline function migrate(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData 40 + { 41 + return migrate_SongPlayData_v2_1_0(input); 42 + } 43 + 44 + public static function migrate_SongPlayData_v2_1_0(input:SongData_v2_1_0.SongPlayData_v2_1_0):SongPlayData 45 + { 46 + var result:SongPlayData = new SongPlayData(); 47 + result.songVariations = input.songVariations; 48 + result.difficulties = input.difficulties; 49 + result.stage = input.stage; 50 + result.characters = input.characters; 51 + 52 + // Renamed 53 + result.noteStyle = input.noteSkin; 54 + 55 + // Added 56 + result.ratings = ['default' => 1]; 57 + result.album = null; 58 + 59 + return result; 60 + } 61 + 18 62 public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata 19 63 { 20 64 return migrate_SongMetadata_v2_0_0(input); ··· 23 67 public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata 24 68 { 25 69 var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation); 26 - result.version = input.version; 70 + result.version = SongRegistry.SONG_METADATA_VERSION; 27 71 result.timeFormat = input.timeFormat; 28 72 result.divisions = input.divisions; 29 73 result.timeChanges = input.timeChanges; 30 74 result.looped = input.looped; 31 - result.playData = migrate_SongPlayData_v2_0_0(input.playData); 75 + result.playData = input.playData.migrate(); 32 76 result.generatedBy = input.generatedBy; 33 77 34 78 return result; ··· 45 89 result.songVariations = input.songVariations; 46 90 result.difficulties = input.difficulties; 47 91 result.stage = input.stage; 48 - result.noteSkin = input.noteSkin; 92 + 93 + // Added 94 + result.ratings = ['default' => 1]; 95 + result.album = null; 96 + 97 + // Renamed 98 + result.noteStyle = input.noteSkin; 49 99 50 100 // Fetch the first playable character and migrate it. 51 101 var firstCharKey:Null<String> = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0];
+8 -1
source/funkin/data/song/migrator/SongData_v2_0_0.hx
··· 42 42 @:default(false) 43 43 public var looped:Bool; 44 44 45 + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) 45 46 public var generatedBy:String; 46 47 47 48 public var timeFormat:SongData.SongTimeFormat; ··· 70 71 */ 71 72 public var playableChars:Map<String, SongPlayableChar_v2_0_0>; 72 73 74 + /** 75 + * In metadata version `v2.2.0`, this was renamed to `noteStyle`. 76 + */ 77 + public var noteSkin:String; 78 + 79 + // In 2.2.0, the ratings value was added. 80 + // In 2.2.0, the album value was added. 73 81 // ========== 74 82 // UNMODIFIED VALUES 75 83 // ========== ··· 77 85 public var difficulties:Array<String>; 78 86 79 87 public var stage:String; 80 - public var noteSkin:String; 81 88 82 89 public function new() {} 83 90
+108
source/funkin/data/song/migrator/SongData_v2_1_0.hx
··· 1 + package funkin.data.song.migrator; 2 + 3 + import funkin.data.song.SongData; 4 + import funkin.data.song.SongRegistry; 5 + import thx.semver.Version; 6 + 7 + @:nullSafety 8 + class SongMetadata_v2_1_0 9 + { 10 + // ========== 11 + // MODIFIED VALUES 12 + // =========== 13 + 14 + /** 15 + * In metadata `v2.2.0`, `SongPlayData` was refactored. 16 + */ 17 + public var playData:SongPlayData_v2_1_0; 18 + 19 + // ========== 20 + // UNMODIFIED VALUES 21 + // ========== 22 + @:jcustomparse(funkin.data.DataParse.semverVersion) 23 + @:jcustomwrite(funkin.data.DataWrite.semverVersion) 24 + public var version:Version; 25 + 26 + @:default("Unknown") 27 + public var songName:String; 28 + 29 + @:default("Unknown") 30 + public var artist:String; 31 + 32 + @:optional 33 + @:default(96) 34 + public var divisions:Null<Int>; // Optional field 35 + 36 + @:optional 37 + @:default(false) 38 + public var looped:Bool; 39 + 40 + @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) 41 + public var generatedBy:String; 42 + 43 + public var timeFormat:SongData.SongTimeFormat; 44 + 45 + public var timeChanges:Array<SongData.SongTimeChange>; 46 + 47 + /** 48 + * Defaults to `Constants.DEFAULT_VARIATION`. Populated later. 49 + */ 50 + @:jignored 51 + public var variation:String; 52 + 53 + public function new(songName:String, artist:String, ?variation:String) 54 + { 55 + this.version = SongRegistry.SONG_METADATA_VERSION; 56 + this.songName = songName; 57 + this.artist = artist; 58 + this.timeFormat = 'ms'; 59 + this.divisions = null; 60 + this.timeChanges = [new SongTimeChange(0, 100)]; 61 + this.looped = false; 62 + this.playData = new SongPlayData_v2_1_0(); 63 + this.playData.songVariations = []; 64 + this.playData.difficulties = []; 65 + this.playData.characters = new SongCharacterData('bf', 'gf', 'dad'); 66 + this.playData.stage = 'mainStage'; 67 + this.playData.noteSkin = 'funkin'; 68 + this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY; 69 + // Variation ID. 70 + this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation; 71 + } 72 + 73 + /** 74 + * Produces a string representation suitable for debugging. 75 + */ 76 + public function toString():String 77 + { 78 + return 'SongMetadata[LEGACY:v2.1.0](${this.songName} by ${this.artist}, variation ${this.variation})'; 79 + } 80 + } 81 + 82 + class SongPlayData_v2_1_0 83 + { 84 + /** 85 + * In `v2.2.0`, this value was renamed to `noteStyle`. 86 + */ 87 + public var noteSkin:String; 88 + 89 + // In 2.2.0, the ratings value was added. 90 + // In 2.2.0, the album value was added. 91 + // ========== 92 + // UNMODIFIED VALUES 93 + // ========== 94 + public var songVariations:Array<String>; 95 + public var difficulties:Array<String>; 96 + public var characters:SongData.SongCharacterData; 97 + public var stage:String; 98 + 99 + public function new() {} 100 + 101 + /** 102 + * Produces a string representation suitable for debugging. 103 + */ 104 + public function toString():String 105 + { 106 + return 'SongPlayData[LEGACY:v2.1.0](${this.songVariations}, ${this.difficulties})'; 107 + } 108 + }
+3 -3
source/funkin/play/PlayState.hx
··· 1471 1471 { 1472 1472 case 'school': 'pixel'; 1473 1473 case 'schoolEvil': 'pixel'; 1474 - default: 'funkin'; 1474 + default: Constants.DEFAULT_NOTE_STYLE; 1475 1475 } 1476 1476 var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); 1477 1477 if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); ··· 2389 2389 2390 2390 #if sys 2391 2391 // spitter for ravy, teehee!! 2392 - 2393 - var output = SerializerUtil.toJSON(inputSpitter); 2392 + var writer = new json2object.JsonWriter<Array<ScoreInput>>(); 2393 + var output = writer.write(inputSpitter, ' '); 2394 2394 sys.io.File.saveContent("./scores.json", output); 2395 2395 #end 2396 2396
+11 -14
source/funkin/play/song/Song.hx
··· 96 96 if (_data != null && _data.playData != null) 97 97 { 98 98 for (vari in _data.playData.songVariations) 99 + { 99 100 variations.push(vari); 100 - } 101 101 102 - for (meta in fetchVariationMetadata(id)) 103 - _metadata.push(meta); 102 + var variMeta = fetchVariationMetadata(id, vari); 103 + if (variMeta != null) _metadata.push(variMeta); 104 + } 105 + } 104 106 105 107 if (_metadata.length == 0) 106 108 { ··· 178 180 difficulty.generatedBy = metadata.generatedBy; 179 181 180 182 difficulty.stage = metadata.playData.stage; 181 - difficulty.noteStyle = metadata.playData.noteSkin; 183 + difficulty.noteStyle = metadata.playData.noteStyle; 182 184 183 185 difficulties.set(diffId, difficulty); 184 186 ··· 337 339 return SongRegistry.instance.parseEntryMetadataWithMigration(id, Constants.DEFAULT_VARIATION, version); 338 340 } 339 341 340 - function fetchVariationMetadata(id:String):Array<SongMetadata> 342 + function fetchVariationMetadata(id:String, vari:String):Null<SongMetadata> 341 343 { 342 - var result:Array<SongMetadata> = []; 343 - for (vari in variations) 344 - { 345 - var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari); 346 - if (version == null) continue; 347 - var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version); 348 - if (meta != null) result.push(meta); 349 - } 350 - return result; 344 + var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id, vari); 345 + if (version == null) return null; 346 + var meta:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataWithMigration(id, vari, version); 347 + return meta; 351 348 } 352 349 } 353 350
+15 -8
source/funkin/play/stage/Stage.hx
··· 176 176 continue; 177 177 } 178 178 179 - if (Std.isOfType(dataProp.scale, Array)) 180 - { 181 - propSprite.scale.set(dataProp.scale[0], dataProp.scale[1]); 182 - } 183 - else 179 + switch (dataProp.scale) 184 180 { 185 - propSprite.scale.set(dataProp.scale); 181 + case Left(value): 182 + propSprite.scale.set(value); 183 + 184 + case Right(values): 185 + propSprite.scale.set(values[0], values[1]); 186 186 } 187 187 propSprite.updateHitbox(); 188 188 ··· 194 194 // If pixel, disable antialiasing. 195 195 propSprite.antialiasing = !dataProp.isPixel; 196 196 197 - propSprite.scrollFactor.x = dataProp.scroll[0]; 198 - propSprite.scrollFactor.y = dataProp.scroll[1]; 197 + switch (dataProp.scroll) 198 + { 199 + case Left(value): 200 + propSprite.scrollFactor.x = value; 201 + propSprite.scrollFactor.y = value; 202 + case Right(values): 203 + propSprite.scrollFactor.x = values[0]; 204 + propSprite.scrollFactor.y = values[1]; 205 + } 199 206 200 207 propSprite.zIndex = dataProp.zIndex; 201 208
+90 -40
source/funkin/play/stage/StageData.hx
··· 1 1 package funkin.play.stage; 2 2 3 3 import funkin.data.animation.AnimationData; 4 - import flixel.util.typeLimit.OneOfTwo; 5 4 import funkin.play.stage.ScriptedStage; 6 5 import funkin.play.stage.Stage; 7 6 import funkin.util.VersionUtil; ··· 157 156 return rawJson; 158 157 } 159 158 160 - static function migrateStageData(rawJson:String, stageId:String) 159 + static function migrateStageData(rawJson:String, stageId:String):Null<StageData> 161 160 { 162 161 // If you update the stage data format in a breaking way, 163 162 // handle migration here by checking the `version` value. 164 163 165 164 try 166 165 { 167 - var stageData:StageData = cast Json.parse(rawJson); 168 - return stageData; 166 + var parser = new json2object.JsonParser<StageData>(); 167 + parser.fromJson(rawJson, '$stageId.json'); 168 + 169 + if (parser.errors.length > 0) 170 + { 171 + trace('[STAGE] Failed to parse stage data'); 172 + 173 + for (error in parser.errors) 174 + funkin.data.DataError.printError(error); 175 + 176 + return null; 177 + } 178 + return parser.value; 169 179 } 170 180 catch (e) 171 181 { ··· 269 279 inputProp.danceEvery = DEFAULT_DANCEEVERY; 270 280 } 271 281 272 - if (inputProp.scale == null) 273 - { 274 - inputProp.scale = DEFAULT_SCALE; 275 - } 276 - 277 282 if (inputProp.animType == null) 278 283 { 279 284 inputProp.animType = DEFAULT_ANIMTYPE; 280 285 } 281 286 282 - if (Std.isOfType(inputProp.scale, Float)) 287 + switch (inputProp.scale) 283 288 { 284 - inputProp.scale = [inputProp.scale, inputProp.scale]; 289 + case null: 290 + inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]); 291 + case Left(value): 292 + inputProp.scale = Right([value, value]); 293 + case Right(_): 294 + // Do nothing 285 295 } 286 296 287 - if (inputProp.scroll == null) 297 + switch (inputProp.scroll) 288 298 { 289 - inputProp.scroll = DEFAULT_SCROLL; 299 + case null: 300 + inputProp.scroll = Right(DEFAULT_SCROLL); 301 + case Left(value): 302 + inputProp.scroll = Right([value, value]); 303 + case Right(_): 304 + // Do nothing 290 305 } 291 306 292 307 if (inputProp.alpha == null) 293 308 { 294 309 inputProp.alpha = DEFAULT_ALPHA; 295 - } 296 - 297 - if (Std.isOfType(inputProp.scroll, Float)) 298 - { 299 - inputProp.scroll = [inputProp.scroll, inputProp.scroll]; 300 310 } 301 311 302 312 if (inputProp.animations == null) ··· 392 402 } 393 403 } 394 404 395 - typedef StageData = 405 + class StageData 396 406 { 397 407 /** 398 408 * The sematic version number of the stage data JSON format. 399 409 * Supports fancy comparisons like NPM does it's neat. 400 410 */ 401 - var version:String; 411 + public var version:String; 402 412 403 - var name:String; 404 - var cameraZoom:Null<Float>; 405 - var props:Array<StageDataProp>; 406 - var characters: 407 - { 408 - bf:StageDataCharacter, 409 - dad:StageDataCharacter, 410 - gf:StageDataCharacter, 411 - }; 413 + public var name:String; 414 + public var cameraZoom:Null<Float>; 415 + public var props:Array<StageDataProp>; 416 + public var characters:StageDataCharacters; 417 + 418 + public function new() 419 + { 420 + this.version = StageDataParser.STAGE_DATA_VERSION; 421 + } 422 + 423 + /** 424 + * Convert this StageData into a JSON string. 425 + */ 426 + public function serialize(pretty:Bool = true):String 427 + { 428 + var writer = new json2object.JsonWriter<StageData>(); 429 + return writer.write(this, pretty ? ' ' : null); 430 + } 431 + } 432 + 433 + typedef StageDataCharacters = 434 + { 435 + var bf:StageDataCharacter; 436 + var dad:StageDataCharacter; 437 + var gf:StageDataCharacter; 412 438 }; 413 439 414 440 typedef StageDataProp = ··· 417 443 * The name of the prop for later lookup by scripts. 418 444 * Optional; if unspecified, the prop can't be referenced by scripts. 419 445 */ 446 + @:optional 420 447 var name:String; 421 448 422 449 /** ··· 435 462 * This is just like CSS, it isn't hard. 436 463 * @default 0 437 464 */ 438 - var zIndex:Null<Int>; 465 + @:optional 466 + @:default(0) 467 + var zIndex:Int; 439 468 440 469 /** 441 470 * If set to true, anti-aliasing will be forcibly disabled on the sprite. 442 471 * This prevents blurry images on pixel-art levels. 443 472 * @default false 444 473 */ 445 - var isPixel:Null<Bool>; 474 + @:optional 475 + @:default(false) 476 + var isPixel:Bool; 446 477 447 478 /** 448 479 * Either the scale of the prop as a float, or the [w, h] scale as an array of two floats. 449 480 * Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory. 450 - * @default 1 451 481 */ 452 - var scale:OneOfTwo<Float, Array<Float>>; 482 + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) 483 + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) 484 + @:optional 485 + var scale:haxe.ds.Either<Float, Array<Float>>; 453 486 454 487 /** 455 488 * The alpha of the prop, as a float. 456 489 * @default 1.0 457 490 */ 458 - var alpha:Null<Float>; 491 + @:optional 492 + @:default(1.0) 493 + var alpha:Float; 459 494 460 495 /** 461 496 * If not zero, this prop will play an animation every X beats of the song. ··· 464 499 * 465 500 * @default 0 466 501 */ 467 - var danceEvery:Null<Int>; 502 + @:default(0) 503 + @:optional 504 + var danceEvery:Int; 468 505 469 506 /** 470 507 * How much the prop scrolls relative to the camera. Used to create a parallax effect. ··· 474 511 * [0, 0] means the prop is not moved. 475 512 * @default [0, 0] 476 513 */ 477 - var scroll:OneOfTwo<Float, Array<Float>>; 514 + @:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats) 515 + @:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats) 516 + @:optional 517 + var scroll:haxe.ds.Either<Float, Array<Float>>; 478 518 479 519 /** 480 520 * An optional array of animations which the prop can play. 481 521 * @default Prop has no animations. 482 522 */ 523 + @:optional 483 524 var animations:Array<AnimationData>; 484 525 485 526 /** 486 527 * If animations are used, this is the name of the animation to play first. 487 528 * @default Don't play an animation. 488 529 */ 489 - var startingAnimation:String; 530 + @:optional 531 + var startingAnimation:Null<String>; 490 532 491 533 /** 492 534 * The animation type to use. 493 535 * Options: "sparrow", "packer" 494 536 * @default "sparrow" 495 537 */ 538 + @:default("sparrow") 539 + @:optional 496 540 var animType:String; 497 541 }; 498 542 ··· 503 547 * Again, just like CSS. 504 548 * @default 0 505 549 */ 506 - ?zIndex:Int, 550 + @:optional 551 + @:default(0) 552 + var zIndex:Int; 507 553 508 554 /** 509 555 * The position to render the character at. 510 556 */ 511 - position:Array<Float>, 557 + @:optional 558 + @:default([0, 0]) 559 + var position:Array<Float>; 512 560 513 561 /** 514 562 * The camera offsets to apply when focusing on the character on this stage. 515 563 * @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF 516 564 */ 517 - cameraOffsets:Array<Float>, 565 + @:optional 566 + @:default([0, 0]) 567 + var cameraOffsets:Array<Float>; 518 568 };
+2 -3
source/funkin/save/migrator/SaveDataMigrator.hx
··· 13 13 */ 14 14 public static function migrate(inputData:Dynamic):Save 15 15 { 16 - // This deserializes directly into a `Version` object, not a `String`. 17 - var version:Null<Version> = inputData?.version ?? null; 16 + var version:Null<thx.semver.Version> = VersionUtil.parseVersion(inputData?.version ?? null); 18 17 19 18 if (version == null) 20 19 { ··· 24 23 } 25 24 else 26 25 { 27 - if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE)) 26 + if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE)) 28 27 { 29 28 // Simply cast the structured data. 30 29 var save:Save = inputData;
+1 -1
source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
··· 265 265 { 266 266 var data:Null<Bytes> = state.audioVocalTrackData.get(key); 267 267 if (data == null) continue; 268 - zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data)); 268 + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Voices-${key}.ogg', data)); 269 269 } 270 270 271 271 return zipEntries;
+8 -8
source/funkin/ui/debug/charting/ChartEditorCommand.hx
··· 182 182 state.currentEventSelection.push(event); 183 183 } 184 184 185 - state.noteDisplayDirty = true; 186 - state.notePreviewDirty = true; 185 + // state.noteDisplayDirty = true; 186 + // state.notePreviewDirty = true; 187 187 } 188 188 189 189 public function undo(state:ChartEditorState):Void ··· 191 191 state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); 192 192 state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); 193 193 194 - state.noteDisplayDirty = true; 195 - state.notePreviewDirty = true; 194 + // state.noteDisplayDirty = true; 195 + // state.notePreviewDirty = true; 196 196 } 197 197 198 198 public function toString():String ··· 452 452 state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentNoteSelection, this.notes); 453 453 state.currentEventSelection = SongDataUtils.subtractEvents(state.currentEventSelection, this.events); 454 454 455 - state.noteDisplayDirty = true; 456 - state.notePreviewDirty = true; 455 + // state.noteDisplayDirty = true; 456 + // state.notePreviewDirty = true; 457 457 } 458 458 459 459 public function undo(state:ChartEditorState):Void ··· 468 468 state.currentEventSelection.push(event); 469 469 } 470 470 471 - state.noteDisplayDirty = true; 472 - state.notePreviewDirty = true; 471 + // state.noteDisplayDirty = true; 472 + // state.notePreviewDirty = true; 473 473 } 474 474 475 475 public function toString():String
+142 -16
source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
··· 51 51 { 52 52 static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about'); 53 53 static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome'); 54 + static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); 54 55 static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); 55 56 static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata'); 56 57 static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals'); 57 58 static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry'); 58 - static final CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart'); 59 - static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry'); 59 + static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts'); 60 + static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry'); 60 61 static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart'); 61 62 static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); 62 63 static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation'); ··· 82 83 { 83 84 var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); 84 85 if (dialog == null) throw 'Could not locate Welcome dialog'; 86 + 87 + dialog.onDialogClosed = function(_event) { 88 + // Called when the Welcome dialog is closed while it is closable. 89 + state.stopWelcomeMusic(); 90 + } 85 91 86 92 // Create New Song "Easy/Normal/Hard" 87 93 var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link); ··· 129 135 state.stopWelcomeMusic(); 130 136 131 137 // Open the "Open Chart" dialog 132 - openBrowseWizard(state, false); 138 + openBrowseFNFC(state, false); 133 139 } 134 140 135 141 var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox); ··· 168 174 return dialog; 169 175 } 170 176 177 + public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog> 178 + { 179 + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable); 180 + if (dialog == null) throw 'Could not locate Upload Chart dialog'; 181 + 182 + dialog.onDialogClosed = function(_event) { 183 + if (_event.button == DialogButton.APPLY) 184 + { 185 + // Simply let the dialog close. 186 + } 187 + else 188 + { 189 + // User cancelled the wizard! Back to the welcome dialog. 190 + openWelcomeDialog(state); 191 + } 192 + }; 193 + 194 + var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); 195 + if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog'; 196 + 197 + buttonCancel.onClick = function(_event) { 198 + dialog.hideDialog(DialogButton.CANCEL); 199 + } 200 + 201 + var chartBox:Null<Box> = dialog.findComponent('chartBox', Box); 202 + if (chartBox == null) throw 'Could not locate chartBox in Upload Chart dialog'; 203 + 204 + chartBox.onMouseOver = function(_event) { 205 + chartBox.swapClass('upload-bg', 'upload-bg-hover'); 206 + Cursor.cursorMode = Pointer; 207 + } 208 + 209 + chartBox.onMouseOut = function(_event) { 210 + chartBox.swapClass('upload-bg-hover', 'upload-bg'); 211 + Cursor.cursorMode = Default; 212 + } 213 + 214 + var onDropFile:String->Void; 215 + 216 + chartBox.onClick = function(_event) { 217 + Dialogs.openBinaryFile('Open Chart', [ 218 + {label: 'Friday Night Funkin\' Chart (.fnfc)', extension: 'fnfc'}], function(selectedFile:SelectedFileInfo) { 219 + if (selectedFile != null && selectedFile.bytes != null) 220 + { 221 + try 222 + { 223 + if (ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes)) 224 + { 225 + #if !mac 226 + NotificationManager.instance.addNotification( 227 + { 228 + title: 'Success', 229 + body: 'Loaded chart (${selectedFile.name})', 230 + type: NotificationType.Success, 231 + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME 232 + }); 233 + #end 234 + 235 + if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath; 236 + dialog.hideDialog(DialogButton.APPLY); 237 + removeDropHandler(onDropFile); 238 + } 239 + } 240 + catch (err) 241 + { 242 + #if !mac 243 + NotificationManager.instance.addNotification( 244 + { 245 + title: 'Failure', 246 + body: 'Failed to load chart (${selectedFile.name}): ${err}', 247 + type: NotificationType.Error, 248 + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME 249 + }); 250 + #end 251 + } 252 + } 253 + }); 254 + } 255 + 256 + onDropFile = function(pathStr:String) { 257 + var path:Path = new Path(pathStr); 258 + trace('Dropped file (${path})'); 259 + 260 + try 261 + { 262 + if (ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString())) 263 + { 264 + #if !mac 265 + NotificationManager.instance.addNotification( 266 + { 267 + title: 'Success', 268 + body: 'Loaded chart (${path.toString()})', 269 + type: NotificationType.Success, 270 + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME 271 + }); 272 + #end 273 + 274 + dialog.hideDialog(DialogButton.APPLY); 275 + removeDropHandler(onDropFile); 276 + } 277 + } 278 + catch (err) 279 + { 280 + #if !mac 281 + NotificationManager.instance.addNotification( 282 + { 283 + title: 'Failure', 284 + body: 'Failed to load chart (${path.toString()}): ${err}', 285 + type: NotificationType.Error, 286 + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME 287 + }); 288 + #end 289 + } 290 + }; 291 + 292 + addDropHandler(chartBox, onDropFile); 293 + 294 + return dialog; 295 + } 296 + 171 297 /** 172 298 * Open the wizard for opening an existing chart from individual files. 173 299 * @param state ··· 622 748 if (inputNoteStyle == null) throw 'Could not locate inputNoteStyle DropDown in Song Metadata dialog'; 623 749 inputNoteStyle.onChange = function(event:UIEvent) { 624 750 if (event.data.id == null) return; 625 - newSongMetadata.playData.noteSkin = event.data.id; 751 + newSongMetadata.playData.noteStyle = event.data.id; 626 752 }; 627 - var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteSkin); 753 + var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteStyle); 628 754 inputNoteStyle.value = startingValueNoteStyle; 629 755 630 756 var inputCharacterPlayer:Null<FunkinDropDown> = dialog.findComponent('inputCharacterPlayer', FunkinDropDown); ··· 765 891 }); 766 892 #end 767 893 #if FILE_DROP_SUPPORTED 768 - vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; 894 + vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}'; 769 895 #else 770 - vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${path.file}.${path.ext}'; 896 + vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}'; 771 897 #end 772 898 773 899 dialogNoVocals.hidden = true; ··· 820 946 }); 821 947 #end 822 948 #if FILE_DROP_SUPPORTED 823 - vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; 949 + vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}'; 824 950 #else 825 - vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}'; 951 + vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}'; 826 952 #end 827 953 828 954 dialogNoVocals.hidden = true; ··· 877 1003 @:haxe.warning('-WVarInit') 878 1004 public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog 879 1005 { 880 - var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable); 1006 + var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT, true, closable); 881 1007 if (dialog == null) throw 'Could not locate Open Chart dialog'; 882 1008 883 1009 var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button); ··· 915 1041 } 916 1042 917 1043 // Build an entry for -chart.json. 918 - var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); 1044 + var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); 919 1045 var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); 920 1046 if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; 921 1047 #if FILE_DROP_SUPPORTED ··· 931 1057 for (variation in variations) 932 1058 { 933 1059 // Build entries for -metadata-<variation>.json. 934 - var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); 1060 + var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); 935 1061 var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); 936 1062 if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; 937 1063 #if FILE_DROP_SUPPORTED ··· 955 1081 chartContainerB.addComponent(songVariationMetadataEntry); 956 1082 957 1083 // Build entries for -chart-<variation>.json. 958 - var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); 1084 + var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); 959 1085 var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); 960 1086 if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; 961 1087 #if FILE_DROP_SUPPORTED ··· 1230 1356 }); 1231 1357 } 1232 1358 1233 - var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT); 1359 + var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); 1234 1360 var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label); 1235 1361 if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; 1236 1362 ··· 1447 1573 1448 1574 var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown); 1449 1575 if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog'; 1450 - dialogNoteStyle.value = state.currentSongMetadata.playData.noteSkin; 1576 + dialogNoteStyle.value = state.currentSongMetadata.playData.noteStyle; 1451 1577 1452 1578 var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown); 1453 1579 if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog'; ··· 1479 1605 var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase()); 1480 1606 1481 1607 pendingVariation.playData.stage = dialogStage.value.id; 1482 - pendingVariation.playData.noteSkin = dialogNoteStyle.value; 1608 + pendingVariation.playData.noteStyle = dialogNoteStyle.value; 1483 1609 pendingVariation.timeChanges[0].bpm = dialogBPM.value; 1484 1610 1485 1611 state.songMetadata.set(pendingVariation.variation, pendingVariation);
+198 -30
source/funkin/ui/debug/charting/ChartEditorImportExportHandler.hx
··· 1 1 package funkin.ui.debug.charting; 2 2 3 + import funkin.util.VersionUtil; 3 4 import haxe.ui.notifications.NotificationType; 4 5 import funkin.util.DateUtil; 5 6 import haxe.io.Path; ··· 7 8 import haxe.ui.notifications.NotificationManager; 8 9 import funkin.util.FileUtil; 9 10 import funkin.util.FileUtil; 11 + import haxe.io.Bytes; 10 12 import funkin.play.song.Song; 11 13 import funkin.data.song.SongData.SongChartData; 12 14 import funkin.data.song.SongData.SongMetadata; 13 15 import funkin.data.song.SongRegistry; 16 + import funkin.data.song.importer.ChartManifestData; 14 17 15 18 /** 16 19 * Contains functions for importing, loading, saving, and exporting charts. ··· 104 107 } 105 108 106 109 /** 107 - * Loads song metadata and chart data into the editor. 110 + * Loads a chart from parsed song metadata and chart data into the editor. 108 111 * @param newSongMetadata The song metadata to load. 109 112 * @param newSongChartData The song chart data to load. 110 113 */ ··· 135 138 } 136 139 } 137 140 141 + public static function loadFromFNFCPath(state:ChartEditorState, path:String):Bool 142 + { 143 + var bytes:Null<Bytes> = FileUtil.readBytesFromPath(path); 144 + if (bytes == null) return false; 145 + 146 + trace('Loaded ${bytes.length} bytes from $path'); 147 + 148 + var result:Bool = loadFromFNFC(state, bytes); 149 + if (result) 150 + { 151 + state.currentWorkingFilePath = path; 152 + } 153 + 154 + return result; 155 + } 156 + 138 157 /** 139 - * @param force Whether to force the export without prompting the user for a file location. 158 + * Load a chart's metadata, chart data, and audio from an FNFC archive.. 159 + * @param state 160 + * @param bytes 161 + * @param instId 162 + * @return Bool 140 163 */ 141 - public static function exportAllSongData(state:ChartEditorState, force:Bool = false):Void 164 + public static function loadFromFNFC(state:ChartEditorState, bytes:Bytes):Bool 142 165 { 143 - var tmp = false; 166 + var songMetadatas:Map<String, SongMetadata> = []; 167 + var songChartDatas:Map<String, SongChartData> = []; 168 + 169 + var fileEntries:Array<haxe.zip.Entry> = FileUtil.readZIPFromBytes(bytes); 170 + var mappedFileEntries:Map<String, haxe.zip.Entry> = FileUtil.mapZIPEntriesByName(fileEntries); 171 + 172 + var manifestBytes:Null<Bytes> = mappedFileEntries.get('manifest.json')?.data; 173 + if (manifestBytes == null) throw 'Could not locate manifest.'; 174 + var manifestString = manifestBytes.toString(); 175 + var manifest:Null<ChartManifestData> = ChartManifestData.deserialize(manifestString); 176 + if (manifest == null) throw 'Could not read manifest.'; 177 + 178 + // Get the song ID. 179 + var songId:String = manifest.songId; 180 + 181 + var baseMetadataPath:String = manifest.getMetadataFileName(); 182 + var baseChartDataPath:String = manifest.getChartDataFileName(); 183 + 184 + var baseMetadataBytes:Null<Bytes> = mappedFileEntries.get(baseMetadataPath)?.data; 185 + if (baseMetadataBytes == null) throw 'Could not locate metadata (default).'; 186 + var baseMetadataString:String = baseMetadataBytes.toString(); 187 + var baseMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(baseMetadataString); 188 + if (baseMetadataVersion == null) throw 'Could not read metadata version (default).'; 189 + 190 + var baseMetadata:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(baseMetadataString, baseMetadataPath, baseMetadataVersion); 191 + if (baseMetadata == null) throw 'Could not read metadata (default).'; 192 + songMetadatas.set(Constants.DEFAULT_VARIATION, baseMetadata); 193 + 194 + var baseChartDataBytes:Null<Bytes> = mappedFileEntries.get(baseChartDataPath)?.data; 195 + if (baseChartDataBytes == null) throw 'Could not locate chart data (default).'; 196 + var baseChartDataString:String = baseChartDataBytes.toString(); 197 + var baseChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(baseChartDataString); 198 + if (baseChartDataVersion == null) throw 'Could not read chart data (default) version.'; 199 + 200 + var baseChartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(baseChartDataString, baseChartDataPath, 201 + baseChartDataVersion); 202 + if (baseChartData == null) throw 'Could not read chart data (default).'; 203 + songChartDatas.set(Constants.DEFAULT_VARIATION, baseChartData); 204 + 205 + var variationList:Array<String> = baseMetadata.playData.songVariations; 206 + 207 + for (variation in variationList) 208 + { 209 + var variMetadataPath:String = manifest.getMetadataFileName(variation); 210 + var variChartDataPath:String = manifest.getChartDataFileName(variation); 211 + 212 + var variMetadataBytes:Null<Bytes> = mappedFileEntries.get(variMetadataPath)?.data; 213 + if (variMetadataBytes == null) throw 'Could not locate metadata ($variation).'; 214 + var variMetadataString:String = variMetadataBytes.toString(); 215 + var variMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(variMetadataString); 216 + if (variMetadataVersion == null) throw 'Could not read metadata ($variation) version.'; 217 + 218 + var variMetadata:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(baseMetadataString, variMetadataPath, variMetadataVersion); 219 + if (variMetadata == null) throw 'Could not read metadata ($variation).'; 220 + songMetadatas.set(variation, variMetadata); 221 + 222 + var variChartDataBytes:Null<Bytes> = mappedFileEntries.get(variChartDataPath)?.data; 223 + if (variChartDataBytes == null) throw 'Could not locate chart data ($variation).'; 224 + var variChartDataString:String = variChartDataBytes.toString(); 225 + var variChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(variChartDataString); 226 + if (variChartDataVersion == null) throw 'Could not read chart data version ($variation).'; 227 + 228 + var variChartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(variChartDataString, variChartDataPath, 229 + variChartDataVersion); 230 + if (variChartData == null) throw 'Could not read chart data ($variation).'; 231 + songChartDatas.set(variation, variChartData); 232 + } 233 + 234 + ChartEditorAudioHandler.stopExistingInstrumental(state); 235 + ChartEditorAudioHandler.stopExistingVocals(state); 236 + 237 + // Load instrumentals 238 + for (variation in [Constants.DEFAULT_VARIATION].concat(variationList)) 239 + { 240 + var variMetadata:Null<SongMetadata> = songMetadatas.get(variation); 241 + if (variMetadata == null) continue; 242 + 243 + var instId:String = variMetadata?.playData?.characters?.instrumental ?? ''; 244 + var playerCharId:String = variMetadata?.playData?.characters?.player ?? Constants.DEFAULT_CHARACTER; 245 + var opponentCharId:Null<String> = variMetadata?.playData?.characters?.opponent; 246 + 247 + var instFileName:String = manifest.getInstFileName(instId); 248 + var instFileBytes:Null<Bytes> = mappedFileEntries.get(instFileName)?.data; 249 + if (instFileBytes != null) 250 + { 251 + if (!ChartEditorAudioHandler.loadInstFromBytes(state, instFileBytes, instId)) 252 + { 253 + throw 'Could not load instrumental ($instFileName).'; 254 + } 255 + } 256 + else 257 + { 258 + throw 'Could not find instrumental ($instFileName).'; 259 + } 260 + 261 + var playerVocalsFileName:String = manifest.getVocalsFileName(playerCharId); 262 + var playerVocalsFileBytes:Null<Bytes> = mappedFileEntries.get(playerVocalsFileName)?.data; 263 + if (playerVocalsFileBytes != null) 264 + { 265 + if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, playerVocalsFileBytes, playerCharId, instId)) 266 + { 267 + throw 'Could not load vocals ($playerCharId).'; 268 + } 269 + } 270 + else 271 + { 272 + throw 'Could not find vocals ($playerVocalsFileName).'; 273 + } 274 + 275 + if (opponentCharId != null) 276 + { 277 + var opponentVocalsFileName:String = manifest.getVocalsFileName(opponentCharId); 278 + var opponentVocalsFileBytes:Null<Bytes> = mappedFileEntries.get(opponentVocalsFileName)?.data; 279 + if (opponentVocalsFileBytes != null) 280 + { 281 + if (!ChartEditorAudioHandler.loadVocalsFromBytes(state, opponentVocalsFileBytes, opponentCharId, instId)) 282 + { 283 + throw 'Could not load vocals ($opponentCharId).'; 284 + } 285 + } 286 + else 287 + { 288 + throw 'Could not load vocals ($playerCharId-$instId).'; 289 + } 290 + } 291 + } 292 + 293 + // Apply chart data. 294 + trace(songMetadatas); 295 + trace(songChartDatas); 296 + loadSong(state, songMetadatas, songChartDatas); 297 + 298 + state.switchToCurrentInstrumental(); 299 + 300 + return true; 301 + } 302 + 303 + /** 304 + * @param force Whether to export without prompting. `false` will prompt the user for a location. 305 + * @param targetPath where to export if `force` is `true`. If `null`, will export to the `backups` folder. 306 + */ 307 + public static function exportAllSongData(state:ChartEditorState, force:Bool = false, ?targetPath:String):Void 308 + { 144 309 var zipEntries:Array<haxe.zip.Entry> = []; 145 310 146 - for (variation in state.availableVariations) 311 + var variations = state.availableVariations; 312 + 313 + for (variation in variations) 147 314 { 148 315 var variationId:String = variation; 149 316 if (variation == '' || variation == 'default' || variation == 'normal') ··· 162 329 { 163 330 var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation); 164 331 if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json', 165 - SerializerUtil.toJSON(variationMetadata))); 332 + variationMetadata.serialize())); 166 333 var variationChart:Null<SongChartData> = state.songChartData.get(variation); 167 - if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', 168 - SerializerUtil.toJSON(variationChart))); 334 + if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json', variationChart.serialize())); 169 335 } 170 336 } 171 337 172 - if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state)); 173 - if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state)); 338 + if (state.audioInstTrackData != null) zipEntries = zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state)); 339 + if (state.audioVocalTrackData != null) zipEntries = zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state)); 340 + 341 + var manifest:ChartManifestData = new ChartManifestData(state.currentSongId); 342 + zipEntries.push(FileUtil.makeZIPEntry('manifest.json', manifest.serialize())); 174 343 175 344 trace('Exporting ${zipEntries.length} files to ZIP...'); 176 345 177 346 if (force) 178 347 { 179 - var targetPath:String = if (tmp) 180 - { 181 - Path.join([ 182 - FileUtil.getTempDir(), 183 - 'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' 184 - ]); 185 - } 186 - else 348 + if (targetPath == null) 187 349 { 188 - Path.join([ 350 + targetPath = Path.join([ 189 351 './backups/', 190 - 'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' 352 + 'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}' 191 353 ]); 192 354 } 193 355 194 356 // We have to force write because the program will die before the save dialog is closed. 195 357 trace('Force exporting to $targetPath...'); 196 358 FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath); 197 - return; 198 359 } 199 - 200 - // Prompt and save. 201 - var onSave:Array<String>->Void = function(paths:Array<String>) { 202 - trace('Successfully exported files.'); 203 - }; 360 + else 361 + { 362 + // Prompt and save. 363 + var onSave:Array<String>->Void = function(paths:Array<String>) { 364 + trace('Successfully exported files.'); 365 + }; 204 366 205 - var onCancel:Void->Void = function() { 206 - trace('Export cancelled.'); 207 - }; 367 + var onCancel:Void->Void = function() { 368 + trace('Export cancelled.'); 369 + }; 208 370 209 - FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '${state.currentSongId}-chart.${Constants.EXT_CHART}'); 371 + trace('Exporting to user-defined location...'); 372 + try 373 + { 374 + FileUtil.saveChartAsFNFC(zipEntries, onSave, onCancel, '${state.currentSongId}.${Constants.EXT_CHART}'); 375 + } 376 + catch (e) {} 377 + } 210 378 } 211 379 }
+73 -20
source/funkin/ui/debug/charting/ChartEditorState.hx
··· 1011 1011 1012 1012 function get_currentSongNoteStyle():String 1013 1013 { 1014 - if (currentSongMetadata.playData.noteSkin == null) 1014 + if (currentSongMetadata.playData.noteStyle == null) 1015 1015 { 1016 1016 // Initialize to the default value if not set. 1017 - currentSongMetadata.playData.noteSkin = 'funkin'; 1017 + currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE; 1018 1018 } 1019 - return currentSongMetadata.playData.noteSkin; 1019 + return currentSongMetadata.playData.noteStyle; 1020 1020 } 1021 1021 1022 1022 function set_currentSongNoteStyle(value:String):String 1023 1023 { 1024 - return currentSongMetadata.playData.noteSkin = value; 1024 + return currentSongMetadata.playData.noteStyle = value; 1025 1025 } 1026 1026 1027 1027 var currentSongStage(get, set):String; ··· 1232 1232 1233 1233 var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite> = new FlxTypedSpriteGroup<FlxSprite>(); 1234 1234 1235 - public function new() 1235 + /** 1236 + * The params which were passed in when the Chart Editor was initialized. 1237 + */ 1238 + var params:Null<ChartEditorParams>; 1239 + 1240 + /** 1241 + * The current file path which the chart editor is working with. 1242 + */ 1243 + public var currentWorkingFilePath:Null<String>; 1244 + 1245 + public function new(?params:ChartEditorParams) 1236 1246 { 1237 1247 // Load the HaxeUI XML file. 1238 1248 super(CHART_EDITOR_LAYOUT); 1249 + 1250 + this.params = params; 1239 1251 } 1240 1252 1241 1253 override function create():Void ··· 1251 1263 fixCamera(); 1252 1264 1253 1265 // Get rid of any music from the previous state. 1254 - FlxG.sound.music.stop(); 1266 + if (FlxG.sound.music != null) FlxG.sound.music.stop(); 1255 1267 1256 1268 // Play the welcome music. 1257 1269 setupWelcomeMusic(); ··· 1277 1289 1278 1290 refresh(); 1279 1291 1280 - ChartEditorDialogHandler.openWelcomeDialog(this, false); 1292 + if (params != null && params.fnfcTargetPath != null) 1293 + { 1294 + // Chart editor was opened from the command line. Open the FNFC file now! 1295 + if (ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath)) 1296 + { 1297 + // Don't open the welcome dialog! 1298 + 1299 + #if !mac 1300 + NotificationManager.instance.addNotification( 1301 + { 1302 + title: 'Success', 1303 + body: 'Loaded chart (${params.fnfcTargetPath})', 1304 + type: NotificationType.Success, 1305 + expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME 1306 + }); 1307 + #end 1308 + } 1309 + else 1310 + { 1311 + // Song failed to load, open the Welcome dialog so we aren't in a broken state. 1312 + ChartEditorDialogHandler.openWelcomeDialog(this, false); 1313 + } 1314 + } 1315 + else 1316 + { 1317 + ChartEditorDialogHandler.openWelcomeDialog(this, false); 1318 + } 1281 1319 } 1282 1320 1283 1321 function setupWelcomeMusic() ··· 1632 1670 noteSnapQuantIndex++; 1633 1671 if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0; 1634 1672 }); 1673 + addUIRightClickListener('playbarNoteSnap', function(_) { 1674 + noteSnapQuantIndex--; 1675 + if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1; 1676 + }); 1635 1677 1636 1678 // Add functionality to the menu items. 1637 1679 1638 1680 addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); 1639 - addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseWizard(this, true)); 1681 + addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true)); 1640 1682 addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this)); 1641 1683 addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true)); 1642 1684 addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true)); ··· 1776 1818 addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) { 1777 1819 var volume:Float = (event?.value ?? 0) / 100.0; 1778 1820 if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; 1779 - vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%'; 1821 + vocalsVolumeLabel.text = 'Voices - ${Std.int(event.value)}%'; 1780 1822 }); 1781 1823 } 1782 1824 ··· 1913 1955 { 1914 1956 FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); 1915 1957 FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); 1958 + 1959 + // Add a debug value which displays the current size of the note pool. 1960 + // The pool will grow as more notes need to be rendered at once. 1961 + // If this gets too big, something needs to be optimized somewhere! -Eric 1962 + if (renderedNotes != null && renderedNotes.members != null) FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length); 1963 + if (renderedHoldNotes != null && renderedHoldNotes.members != null) FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length); 1964 + if (renderedEvents != null && renderedEvents.members != null) FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length); 1965 + if (currentNoteSelection != null) FlxG.watch.addQuick("notesSelected", currentNoteSelection.length); 1966 + if (currentEventSelection != null) FlxG.watch.addQuick("eventsSelected", currentEventSelection.length); 1916 1967 } 1917 1968 1918 1969 /** ··· 3037 3088 // Sort the events DESCENDING. This keeps the sustain behind the associated note. 3038 3089 renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() 3039 3090 } 3040 - 3041 - // Add a debug value which displays the current size of the note pool. 3042 - // The pool will grow as more notes need to be rendered at once. 3043 - // If this gets too big, something needs to be optimized somewhere! -Eric 3044 - FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length); 3045 - FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length); 3046 - FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length); 3047 - FlxG.watch.addQuick("notesSelected", currentNoteSelection.length); 3048 - FlxG.watch.addQuick("eventsSelected", currentEventSelection.length); 3049 3091 } 3050 3092 3051 3093 /** ··· 3152 3194 // CTRL + O = Open Chart 3153 3195 if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O) 3154 3196 { 3155 - ChartEditorDialogHandler.openBrowseWizard(this, true); 3197 + ChartEditorDialogHandler.openBrowseFNFC(this, true); 3156 3198 } 3157 3199 3158 3200 // CTRL + SHIFT + S = Save As ··· 3168 3210 } 3169 3211 } 3170 3212 3213 + @:nullSafety(Off) 3171 3214 function quitChartEditor():Void 3172 3215 { 3173 3216 autoSave(); 3174 3217 stopWelcomeMusic(); 3218 + // TODO: PR Flixel to make onComplete nullable. 3219 + if (audioInstTrack != null) audioInstTrack.onComplete = null; 3175 3220 FlxG.switchState(new MainMenuState()); 3176 3221 } 3177 3222 ··· 3691 3736 if (inputStage != null) inputStage.value = currentSongMetadata.playData.stage; 3692 3737 3693 3738 var inputNoteStyle:Null<DropDown> = toolbox.findComponent('inputNoteStyle', DropDown); 3694 - if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteSkin; 3739 + if (inputNoteStyle != null) inputNoteStyle.value = currentSongMetadata.playData.noteStyle; 3695 3740 3696 3741 var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper); 3697 3742 if (inputBPM != null) inputBPM.value = currentSongMetadata.timeChanges[0].bpm; ··· 4351 4396 NumberKeys; 4352 4397 WASD; 4353 4398 } 4399 + 4400 + typedef ChartEditorParams = 4401 + { 4402 + /** 4403 + * If non-null, load this song immediately instead of the welcome screen. 4404 + */ 4405 + var ?fnfcTargetPath:String; 4406 + };
+17
source/funkin/ui/haxeui/HaxeUIState.hx
··· 108 108 } 109 109 } 110 110 111 + /** 112 + * Add an onRightClick listener to a HaxeUI menu bar item. 113 + */ 114 + function addUIRightClickListener(key:String, callback:MouseEvent->Void):Void 115 + { 116 + var target:Component = findComponent(key); 117 + if (target == null) 118 + { 119 + // Gracefully handle the case where the item can't be located. 120 + trace('WARN: Could not locate menu item: $key'); 121 + } 122 + else 123 + { 124 + target.onRightClick = callback; 125 + } 126 + } 127 + 111 128 function setComponentText(key:String, text:String):Void 112 129 { 113 130 var target:Component = findComponent(key);
+134
source/funkin/util/CLIUtil.hx
··· 1 + package funkin.util; 2 + 3 + /** 4 + * Utilties for interpreting command line arguments. 5 + */ 6 + @:nullSafety 7 + class CLIUtil 8 + { 9 + /** 10 + * If we don't do this, dragging and dropping a file onto the executable 11 + * causes it to be unable to find the assets folder. 12 + */ 13 + public static function resetWorkingDir():Void 14 + { 15 + #if sys 16 + var exeDir:String = haxe.io.Path.directory(Sys.programPath()); 17 + trace('Changing working directory from ${Sys.getCwd()} to ${exeDir}'); 18 + Sys.setCwd(exeDir); 19 + #end 20 + } 21 + 22 + public static function processArgs():CLIParams 23 + { 24 + #if sys 25 + return interpretArgs(cleanArgs(Sys.args())); 26 + #else 27 + return buildDefaultParams(); 28 + #end 29 + } 30 + 31 + static function interpretArgs(args:Array<String>):CLIParams 32 + { 33 + var result = buildDefaultParams(); 34 + 35 + result.args = [for (arg in args) arg]; // Copy the array. 36 + 37 + while (args.length > 0) 38 + { 39 + var arg:Null<String> = args.shift(); 40 + if (arg == null) continue; 41 + 42 + if (arg.startsWith('-')) 43 + { 44 + switch (arg) 45 + { 46 + // Flags 47 + case '-h' | '--help': 48 + printUsage(); 49 + case '-v' | '--version': 50 + trace(Constants.GENERATED_BY); 51 + case '--chart': 52 + if (args.length == 0) 53 + { 54 + trace('No chart path provided.'); 55 + printUsage(); 56 + } 57 + else 58 + { 59 + result.chart.shouldLoadChart = true; 60 + result.chart.chartPath = args.shift(); 61 + } 62 + } 63 + } 64 + else 65 + { 66 + // Make an attempt to interpret the argument. 67 + 68 + if (arg.endsWith(Constants.EXT_CHART)) 69 + { 70 + result.chart.shouldLoadChart = true; 71 + result.chart.chartPath = arg; 72 + } 73 + else 74 + { 75 + trace('Unrecognized argument: ${arg}'); 76 + printUsage(); 77 + } 78 + } 79 + } 80 + 81 + return result; 82 + } 83 + 84 + static function printUsage():Void 85 + { 86 + trace('Usage: Funkin.exe [--chart <chart>]'); 87 + } 88 + 89 + static function buildDefaultParams():CLIParams 90 + { 91 + return { 92 + args: [], 93 + 94 + chart: 95 + { 96 + shouldLoadChart: false, 97 + chartPath: null 98 + } 99 + }; 100 + } 101 + 102 + /** 103 + * Clean up the arguments passed to the application before parsing them. 104 + * @param args The arguments to clean up. 105 + * @return The cleaned up arguments. 106 + */ 107 + static function cleanArgs(args:Array<String>):Array<String> 108 + { 109 + var result:Array<String> = []; 110 + 111 + if (args == null || args.length == 0) return result; 112 + 113 + return args.map(function(arg:String):String { 114 + if (arg == null) return ''; 115 + 116 + return arg.trim(); 117 + }).filter(function(arg:String):Bool { 118 + return arg != null && arg != ''; 119 + }); 120 + } 121 + } 122 + 123 + typedef CLIParams = 124 + { 125 + var args:Array<String>; 126 + 127 + var chart:CLIChartParams; 128 + } 129 + 130 + typedef CLIChartParams = 131 + { 132 + var shouldLoadChart:Bool; 133 + var chartPath:Null<String>; 134 + };
+59 -6
source/funkin/util/FileUtil.hx
··· 14 14 */ 15 15 class FileUtil 16 16 { 17 + public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); 18 + public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); 19 + 17 20 /** 18 21 * Browses for a single file, then calls `onSelect(path)` when a path chosen. 19 22 * Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead. ··· 173 176 * 174 177 * @return Whether the file dialog was opened successfully. 175 178 */ 176 - public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool 179 + public static function saveFile(data:Bytes, ?typeFilter:Array<FileFilter>, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, 180 + ?dialogTitle:String):Bool 177 181 { 178 182 #if desktop 179 - var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; 183 + var filter:String = convertTypeFilter(typeFilter); 180 184 181 185 var fileDialog:FileDialog = new FileDialog(); 182 186 if (onSave != null) fileDialog.onSelect.add(onSave); ··· 231 235 } 232 236 catch (_) 233 237 { 234 - trace('Failed to write file (probably already exists): $filePath' + filePath); 235 - continue; 238 + throw 'Failed to write file (probably already exists): $filePath'; 236 239 } 237 240 paths.push(filePath); 238 241 } ··· 269 272 }; 270 273 271 274 // Prompt the user to save the ZIP file. 272 - saveFile(zipBytes, onSave, onCancel, defaultPath, 'Save files as ZIP...'); 275 + saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...'); 276 + 277 + return true; 278 + } 279 + 280 + /** 281 + * Takes an array of file entries and prompts the user to save them as a FNFC file. 282 + */ 283 + public static function saveChartAsFNFC(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, 284 + force:Bool = false):Bool 285 + { 286 + // Create a ZIP file. 287 + var zipBytes:Bytes = createZIPFromEntries(resources); 288 + 289 + var onSave:String->Void = function(path:String) { 290 + onSave([path]); 291 + }; 292 + 293 + // Prompt the user to save the ZIP file. 294 + saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...'); 273 295 274 296 return true; 275 297 } ··· 322 344 public static function readBytesFromPath(path:String):Bytes 323 345 { 324 346 #if sys 325 - return Bytes.ofString(sys.io.File.getContent(path)); 347 + if (!sys.FileSystem.exists(path)) return null; 348 + return sys.io.File.getBytes(path); 326 349 #else 327 350 return null; 328 351 #end ··· 557 580 zipWriter.write(entries.list()); 558 581 559 582 return o.getBytes(); 583 + } 584 + 585 + public static function readZIPFromBytes(input:Bytes):Array<Entry> 586 + { 587 + trace('TEST: ' + input.length); 588 + trace(input.sub(0, 30).toHex()); 589 + 590 + var bytesInput = new haxe.io.BytesInput(input); 591 + var zippedEntries = haxe.zip.Reader.readZip(bytesInput); 592 + 593 + var results:Array<Entry> = []; 594 + for (entry in zippedEntries) 595 + { 596 + if (entry.compressed) 597 + { 598 + entry.data = haxe.zip.Reader.unzip(entry); 599 + } 600 + results.push(entry); 601 + } 602 + return results; 603 + } 604 + 605 + public static function mapZIPEntriesByName(input:Array<Entry>):Map<String, Entry> 606 + { 607 + var results:Map<String, Entry> = []; 608 + for (entry in input) 609 + { 610 + results.set(entry.fileName, entry); 611 + } 612 + return results; 560 613 } 561 614 562 615 /**
+2
source/funkin/util/SerializerUtil.hx
··· 21 21 22 22 /** 23 23 * Convert a Haxe object to a JSON string. 24 + * NOTE: Use `json2object.JsonWriter<T>` WHEREVER POSSIBLE. Do not use this one unless you ABSOLUTELY HAVE TO it's SLOW! 25 + * And don't even THINK about using `haxe.Json.stringify` without the replacer! 24 26 */ 25 27 public static function toJSON(input:Dynamic, pretty:Bool = true):String 26 28 {
+18
source/funkin/util/VersionUtil.hx
··· 61 61 var version:thx.semver.Version = versionStr; // Implicit, not explicit, cast. 62 62 return version; 63 63 } 64 + 65 + public static function parseVersion(input:Dynamic):Null<thx.semver.Version> 66 + { 67 + if (input == null) return null; 68 + 69 + if (Std.isOfType(input, String)) 70 + { 71 + var inputStr:String = input; 72 + var version:thx.semver.Version = inputStr; 73 + return version; 74 + } 75 + else 76 + { 77 + var semVer:thx.semver.Version.SemVer = input; 78 + var version:thx.semver.Version = semVer; 79 + return version; 80 + } 81 + } 64 82 }
+1 -1
tests/unit/source/funkin/data/notestyle/NoteStyleRegistryTest.hx
··· 95 95 96 96 // Verify the underlying call. 97 97 98 - nsrMock.fetchEntry(NoteStyleRegistry.DEFAULT_NOTE_STYLE_ID).verify(times(1)); 98 + nsrMock.fetchEntry(Constants.DEFAULT_NOTE_STYLE).verify(times(1)); 99 99 } 100 100 }